- 新增 internal/contract/enums 叶子包:platform/mood/mode 三表 + UnknownFallback="display-raw";每次调用返回新切片,无包级可变全局 - GET /api/v1/meta/enums(anyAuth 只读、纯投影、无 store/无错误路径) - 反漂移 4 处:真构造内置适配器断言 Name() 与表双向相等、真工厂 buildAccountAdapter 逐项可建、mood 对齐 emotion 常量 + 默认转移表、mode 对齐前端 MODES(跨语言,文件缺失则 skip) - Docs/api-client §1.1 枚举权威表 + 行15 + v0.38;api-reference §3.7 + 行12
207 lines
13 KiB
Go
207 lines
13 KiB
Go
package gateway
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
|
)
|
|
|
|
// This file owns the HTTP surface: the route table (REST /api/v1, the realtime
|
|
// WebSocket hub, /api/v1/admin/* and externally-mounted webhook callbacks) and
|
|
// the outer middleware chain. Route registration is split by audience so the
|
|
// RBAC gate guarding each group stays visible: public, authenticated user,
|
|
// private history, admin reads and owner-only writes.
|
|
|
|
// Handler returns the fully assembled http.Handler: the ServeMux routing
|
|
// REST + WS + admin, wrapped in recovery, CORS and request logging.
|
|
func (s *Server) Handler() http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
// The RBAC middlewares are built once per role set and shared by every route
|
|
// that requires it (owner>admin>viewer, DevDocs/02 §九).
|
|
anyAuth := s.auth.Authorize()
|
|
ownerAuth := s.auth.Authorize(auth.RoleOwner)
|
|
writeAuth := s.auth.Authorize(auth.RoleOwner, auth.RoleAdmin)
|
|
|
|
s.registerPublicRoutes(mux)
|
|
s.registerUserRoutes(mux, anyAuth, ownerAuth, writeAuth)
|
|
s.registerHistoryRoutes(mux, writeAuth)
|
|
s.registerAdminRoutes(mux, ownerAuth, writeAuth)
|
|
s.mountWebhooks(mux)
|
|
|
|
return s.withMiddleware(mux)
|
|
}
|
|
|
|
// registerPublicRoutes mounts the unauthenticated surface: liveness plus the
|
|
// credential endpoints. Refresh/logout are public by design — the opaque refresh
|
|
// token is itself the credential, so a client with an expired access token can
|
|
// still rotate or revoke it. Both fail closed with 401 invalid_grant on a bad
|
|
// token.
|
|
func (s *Server) registerPublicRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /api/v1/health", s.handleHealth)
|
|
mux.HandleFunc("POST /api/v1/auth/login", s.handleLogin)
|
|
mux.HandleFunc("POST /api/v1/auth/refresh", s.handleRefresh)
|
|
mux.HandleFunc("POST /api/v1/auth/logout", s.handleLogout)
|
|
}
|
|
|
|
// registerUserRoutes mounts the authenticated user-facing surface. Chat is a
|
|
// write (owner/admin); reads accept any authenticated role so a viewer can still
|
|
// list personas/sessions. Audio STT/TTS and image upload are owner/admin-only (the
|
|
// owner's private assistant, audited) and are the REST surface native clients use
|
|
// to bypass the 4KB WebSocket frame limit.
|
|
func (s *Server) registerUserRoutes(
|
|
mux *http.ServeMux,
|
|
anyAuth func(http.Handler) http.Handler,
|
|
ownerAuth func(http.Handler) http.Handler,
|
|
writeAuth func(http.Handler) http.Handler,
|
|
) {
|
|
mux.Handle("POST /api/v1/chat", writeAuth(http.HandlerFunc(s.handleChat)))
|
|
mux.Handle("GET /api/v1/sessions", anyAuth(http.HandlerFunc(s.handleListSessions)))
|
|
mux.Handle("GET /api/v1/personas", anyAuth(http.HandlerFunc(s.handleListPersonas)))
|
|
// Shared client enum tables (Android collaboration B4, design/07 §6): the
|
|
// authority every client maps ids to labels from, so a source badge or a mood
|
|
// label cannot drift per client. A read-only projection of
|
|
// internal/contract/enums with nothing sensitive in it, so any authenticated
|
|
// role may read it (same gate as /sessions above).
|
|
mux.Handle("GET /api/v1/meta/enums", anyAuth(http.HandlerFunc(s.handleListEnums)))
|
|
|
|
mux.Handle("POST /api/v1/audio/transcribe", ownerAuth(http.HandlerFunc(s.handleTranscribe)))
|
|
mux.Handle("POST /api/v1/audio/speech", ownerAuth(http.HandlerFunc(s.handleSpeech)))
|
|
mux.Handle("POST /api/v1/audio/voice-clone", ownerAuth(http.HandlerFunc(s.handleVoiceClone)))
|
|
|
|
// Image upload (B1): a multipart POST that stores a raster image under the
|
|
// resolved data dir and returns a same-origin URL usable directly as
|
|
// chat.image. Upload is owner/admin + audited; the static route is a
|
|
// capability URL (unguessable name) because chat.image / <img> cannot carry a
|
|
// bearer token. Both ride the one gateway port (single-binary red line).
|
|
mux.Handle("POST /api/v1/upload", writeAuth(http.HandlerFunc(s.handleUpload)))
|
|
mux.HandleFunc("GET /api/v1/files/{name}", s.handleUploadFile)
|
|
}
|
|
|
|
// registerHistoryRoutes mounts the persistent chat-history surface (design/16)
|
|
// plus the realtime endpoint. Session/model reads and writes are the owner's
|
|
// private history, so owner/admin only (chat uses the same roles); the message
|
|
// content (and thinking) is owner/admin-only per design/16 §6, while the session
|
|
// metadata in GET /sessions above stays readable by any authenticated role.
|
|
// Creating and deleting a sub session mutate persistent state.
|
|
func (s *Server) registerHistoryRoutes(mux *http.ServeMux, writeAuth func(http.Handler) http.Handler) {
|
|
mux.Handle("GET /api/v1/sessions/{id}/messages", writeAuth(http.HandlerFunc(s.handleListMessages)))
|
|
mux.Handle("POST /api/v1/sessions", writeAuth(http.HandlerFunc(s.handleCreateSubSession)))
|
|
mux.Handle("DELETE /api/v1/sessions/{id}", writeAuth(http.HandlerFunc(s.handleDeleteSession)))
|
|
mux.Handle("PATCH /api/v1/sessions/{id}", writeAuth(http.HandlerFunc(s.handlePatchSession)))
|
|
// Read cursor (D4 / Android collaboration B3): advancing it clears the session's
|
|
// unread badge on every end of the account, so it is an owner/admin history write
|
|
// like the other session mutations. It broadcasts a `session_read` frame.
|
|
mux.Handle("POST /api/v1/sessions/{id}/read", writeAuth(http.HandlerFunc(s.handleMarkSessionRead)))
|
|
// Single-message edit/delete (Android collaboration batch ③a, D3). A message is
|
|
// addressed by its own opaque id; both are owner/admin-gated history writes.
|
|
mux.Handle("PATCH /api/v1/messages/{id}", writeAuth(http.HandlerFunc(s.handlePatchMessage)))
|
|
mux.Handle("DELETE /api/v1/messages/{id}", writeAuth(http.HandlerFunc(s.handleDeleteMessage)))
|
|
|
|
// Realtime.
|
|
mux.HandleFunc("/ws/chat", s.handleWS)
|
|
}
|
|
|
|
// registerAdminRoutes mounts the admin API: reads require owner or admin, the
|
|
// audited policy write requires owner, and every other write is reserved for the
|
|
// owner (RBAC red line). Each write handler persists to the real backing store or
|
|
// answers honestly with a clear "M10 / not-wired" error rather than fabricating a
|
|
// success.
|
|
func (s *Server) registerAdminRoutes(
|
|
mux *http.ServeMux,
|
|
ownerAuth func(http.Handler) http.Handler,
|
|
writeAuth func(http.Handler) http.Handler,
|
|
) {
|
|
// Admin reads: owner/admin.
|
|
mux.Handle("GET /api/v1/admin/personas", writeAuth(http.HandlerFunc(s.adminListPersonas)))
|
|
mux.Handle("GET /api/v1/admin/sessions", writeAuth(http.HandlerFunc(s.adminListSessions)))
|
|
mux.Handle("GET /api/v1/admin/memories", writeAuth(http.HandlerFunc(s.adminListMemories)))
|
|
mux.Handle("GET /api/v1/admin/plugins", writeAuth(http.HandlerFunc(s.adminListPlugins)))
|
|
mux.Handle("GET /api/v1/admin/plugins/installed", writeAuth(http.HandlerFunc(s.adminListInstalledPlugins)))
|
|
mux.Handle("GET /api/v1/admin/channels", writeAuth(http.HandlerFunc(s.adminListChannels)))
|
|
mux.Handle("GET /api/v1/admin/channels/messages", writeAuth(http.HandlerFunc(s.handleChannelMessages)))
|
|
mux.Handle("GET /api/v1/admin/channels/{platform}/messages", writeAuth(http.HandlerFunc(s.handleChannelPlatformLog)))
|
|
mux.Handle("GET /api/v1/admin/iot", writeAuth(http.HandlerFunc(s.adminListIoT)))
|
|
mux.Handle("GET /api/v1/admin/llm", writeAuth(http.HandlerFunc(s.adminGetLlm)))
|
|
mux.Handle("GET /api/v1/admin/automations", writeAuth(http.HandlerFunc(s.adminListAutomations)))
|
|
mux.Handle("GET /api/v1/admin/users", writeAuth(http.HandlerFunc(s.adminListUsers)))
|
|
mux.Handle("GET /api/v1/admin/backthink", writeAuth(http.HandlerFunc(s.adminListBackthink)))
|
|
mux.Handle("GET /api/v1/admin/rag", writeAuth(http.HandlerFunc(s.adminListRag)))
|
|
mux.Handle("POST /api/v1/admin/rag/query", writeAuth(http.HandlerFunc(s.adminSearchRag)))
|
|
mux.Handle("GET /api/v1/admin/observability/metrics", writeAuth(http.HandlerFunc(s.adminObservabilityMetrics)))
|
|
mux.Handle("GET /api/v1/admin/observability/trace", writeAuth(http.HandlerFunc(s.adminObservabilityTrace)))
|
|
mux.Handle("GET /api/v1/admin/observability/audit", writeAuth(http.HandlerFunc(s.adminObservabilityAudit)))
|
|
// Backend log tail: the logger's own bounded ring buffer, injected by the
|
|
// composition root (501 until then). Newest first, `?level=`/`?limit=` filtered.
|
|
mux.Handle("GET /api/v1/admin/observability/logs", writeAuth(http.HandlerFunc(s.adminObservabilityLogs)))
|
|
// Unified notification feed (design/10 §3 #16): a read-only projection of the
|
|
// audit trail + backthink pushes, so the panel's notification page needs no
|
|
// new persisted model.
|
|
mux.Handle("GET /api/v1/admin/notifications", writeAuth(http.HandlerFunc(s.adminListNotifications)))
|
|
mux.Handle("POST /api/v1/admin/plugins/policy", ownerAuth(http.HandlerFunc(s.adminSetPolicy)))
|
|
mux.Handle("GET /api/v1/admin/sessions/{id}/export", writeAuth(http.HandlerFunc(s.adminExportSession)))
|
|
|
|
// Admin writes: owner-only, audited.
|
|
mux.Handle("POST /api/v1/admin/personas", ownerAuth(http.HandlerFunc(s.adminCreatePersona)))
|
|
mux.Handle("PUT /api/v1/admin/personas/{id}", ownerAuth(http.HandlerFunc(s.adminUpdatePersona)))
|
|
mux.Handle("POST /api/v1/admin/users", ownerAuth(http.HandlerFunc(s.adminCreateUser)))
|
|
mux.Handle("POST /api/v1/admin/users/apikey", ownerAuth(http.HandlerFunc(s.adminIssueApiKey)))
|
|
mux.Handle("DELETE /api/v1/admin/users/apikey/{id}", ownerAuth(http.HandlerFunc(s.adminRevokeApiKey)))
|
|
mux.Handle("POST /api/v1/admin/iot/{id}/control", ownerAuth(http.HandlerFunc(s.adminIoTControl)))
|
|
mux.Handle("POST /api/v1/admin/iot/scenarios", ownerAuth(http.HandlerFunc(s.adminIoTScenario)))
|
|
mux.Handle("POST /api/v1/admin/iot/rules", ownerAuth(http.HandlerFunc(s.adminIoTRule)))
|
|
mux.Handle("POST /api/v1/admin/automations", ownerAuth(http.HandlerFunc(s.adminCreateAutomation)))
|
|
mux.Handle("POST /api/v1/admin/automations/{id}/run", ownerAuth(http.HandlerFunc(s.adminRunAutomation)))
|
|
mux.Handle("DELETE /api/v1/admin/automations/{id}", ownerAuth(http.HandlerFunc(s.adminDeleteAutomation)))
|
|
mux.Handle("POST /api/v1/admin/llm", ownerAuth(http.HandlerFunc(s.adminAddLlmProvider)))
|
|
mux.Handle("POST /api/v1/admin/llm/test", ownerAuth(http.HandlerFunc(s.adminTestLlm)))
|
|
mux.Handle("POST /api/v1/admin/channels/open", ownerAuth(http.HandlerFunc(s.adminChannelOpen)))
|
|
mux.Handle("POST /api/v1/admin/channels/close", ownerAuth(http.HandlerFunc(s.adminChannelClose)))
|
|
mux.Handle("POST /api/v1/admin/channels/test", ownerAuth(http.HandlerFunc(s.adminChannelTest)))
|
|
mux.Handle("GET /api/v1/admin/channels/{platform}/login-qr", ownerAuth(http.HandlerFunc(s.adminWeixinLoginQR)))
|
|
mux.Handle("GET /api/v1/admin/channels/{platform}/login-status", ownerAuth(http.HandlerFunc(s.adminWeixinLoginStatus)))
|
|
mux.Handle("GET /api/v1/admin/channels/{platform}/view", ownerAuth(http.HandlerFunc(s.adminChannelPlatformView)))
|
|
mux.Handle("GET /api/v1/admin/channels/{platform}/friends", ownerAuth(http.HandlerFunc(s.adminChannelPlatformFriends)))
|
|
mux.Handle("POST /api/v1/admin/channels/{platform}/friend/handle", ownerAuth(http.HandlerFunc(s.adminChannelFriendHandle)))
|
|
mux.Handle("POST /api/v1/admin/channels/{platform}/message/test", ownerAuth(http.HandlerFunc(s.adminChannelMessageTest)))
|
|
mux.Handle("PUT /api/v1/admin/channels/{platform}/config", ownerAuth(http.HandlerFunc(s.adminSaveChannelConfig)))
|
|
mux.Handle("DELETE /api/v1/admin/channels/{platform}/config", ownerAuth(http.HandlerFunc(s.adminDeleteChannelAccount)))
|
|
mux.Handle("POST /api/v1/admin/plugins/install", ownerAuth(http.HandlerFunc(s.adminPluginInstall)))
|
|
mux.Handle("POST /api/v1/admin/plugins/uninstall", ownerAuth(http.HandlerFunc(s.adminPluginUninstall)))
|
|
mux.Handle("POST /api/v1/admin/memories", ownerAuth(http.HandlerFunc(s.adminAddMemory)))
|
|
mux.Handle("POST /api/v1/admin/memories/search", ownerAuth(http.HandlerFunc(s.adminMemorySearch)))
|
|
mux.Handle("POST /api/v1/admin/rag", ownerAuth(http.HandlerFunc(s.adminIndexRag)))
|
|
mux.Handle("POST /api/v1/admin/rag/delete", ownerAuth(http.HandlerFunc(s.adminDeleteRag)))
|
|
mux.Handle("POST /api/v1/admin/system/revoke-keys", ownerAuth(http.HandlerFunc(s.adminRevokeAllKeys)))
|
|
mux.Handle("POST /api/v1/admin/system/rotate-jwt", ownerAuth(http.HandlerFunc(s.adminRotateJWT)))
|
|
mux.Handle("POST /api/v1/admin/system/backup", ownerAuth(http.HandlerFunc(s.adminBackup)))
|
|
mux.Handle("POST /api/v1/admin/system/restore", ownerAuth(http.HandlerFunc(s.adminRestore)))
|
|
}
|
|
|
|
// mountWebhooks mounts externally-supplied inbound callbacks (e.g. the webhook
|
|
// channel) without gateway auth; each handler enforces its own
|
|
// signature/authorization.
|
|
func (s *Server) mountWebhooks(mux *http.ServeMux) {
|
|
for path, hdl := range s.webhooks {
|
|
if path == "" {
|
|
continue
|
|
}
|
|
mux.Handle(path, hdl)
|
|
}
|
|
}
|
|
|
|
// withMiddleware wraps the routed handler in the outer layers, outermost first:
|
|
// the upload capability-URL guard, the SPA fallback (when a frontend is
|
|
// configured), panic recovery, CORS and request logging.
|
|
func (s *Server) withMiddleware(mux http.Handler) http.Handler {
|
|
var h http.Handler = mux
|
|
h = guardUploadPaths(h)
|
|
if s.frontend != nil {
|
|
h = s.spa(h)
|
|
}
|
|
h = s.recover(h)
|
|
h = s.cors(h)
|
|
h = s.logging(h)
|
|
return h
|
|
}
|