Files
Cyrene_v2/internal/services/gateway/routes.go
T
AskaEth 074ffc4ebf
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批:
已推库 16 个文件 + 本批剩余文件)。
校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0);
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
2026-10-03 22:28:25 +08:00

229 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
import (
"net/http"
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
)
// 本文件拥有 HTTP 表面:路由表(REST /api/v1、实时
// WebSocket hub、/api/v1/admin/* 与外部挂载的 webhook 回调)以及
// 外层中间件链。路由注册按受众切分,使守护每组
// 的 RBAC 门保持可见:公开、已认证用户、
// 私有历史、管理读与仅 owner 写。
// Handler 返回完整组装的 http.Handler:路由 REST + WS + 管理的
// ServeMux,外面包上 recover、CORS 与请求日志。
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
// RBAC 中间件按角色集各构建一次,并被每条需要它的路由
// 共用(owner>admin>viewer,DevDocs/02 §九)。viewerAuth 是「每个
// 已认证角色」:它指 viewer 角色存在的那个只读监控
// 表面(模块清单、运行时指标/trace、日志尾部
// 与通知 feed)—— 见 registerAdminRoutes。
anyAuth := s.auth.Authorize()
ownerAuth := s.auth.Authorize(auth.RoleOwner)
writeAuth := s.auth.Authorize(auth.RoleOwner, auth.RoleAdmin)
viewerAuth := s.auth.Authorize(auth.RoleOwner, auth.RoleAdmin, auth.RoleViewer)
s.registerPublicRoutes(mux)
s.registerUserRoutes(mux, anyAuth, ownerAuth, writeAuth)
s.registerHistoryRoutes(mux, writeAuth)
s.registerAdminRoutes(mux, ownerAuth, writeAuth, viewerAuth)
s.mountWebhooks(mux)
return s.withMiddleware(mux)
}
// registerPublicRoutes 挂载未认证表面:存活探针加上
// 凭据端点。refresh/logout 按设计是公开的 —— 非透明 refresh
// token 本身就是凭据,因此访问令牌已过期的客户端仍能
// 轮换或吊销它。二者在 token 非法时都以 401 invalid_grant
// 失败关闭。
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 挂载已认证的面向用户表面。Chat 是
// 写(owner/admin);读接受任何已认证角色,因此 viewer 仍能
// 列出人格/会话。音频 STT/TTS 与图片上传仅 owner/admin(主人的
// 私密助手,带审计),它们是原生客户端用来
// 绕过 4KB WebSocket 帧上限的 REST 表面。
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)))
// 共享的客户端枚举表(Android 协作 B4,design/07 §6):
// 每个客户端据此把 id 映射为标签的权威,因此来源徽标或
// 情绪标签不会逐客户端漂移。它是
// internal/contract/enums 的只读投影,不含任何敏感内容,因此任何已认证
// 角色都可读它(上面的 /sessions 同一道门)。
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)))
// 图片上传(B1):一个 multipart POST,把光栅图片存到
// 解析后的数据目录下,返回可直接用作
// chat.image 的同源 URL。上传是 owner/admin + 带审计;静态路由是
// 能力 URL(名字不可猜)因为 chat.image / <img> 无法携带
// bearer token。二者都走同一个网关端口(单二进制红线)。
mux.Handle("POST /api/v1/upload", writeAuth(http.HandlerFunc(s.handleUpload)))
mux.HandleFunc("GET /api/v1/files/{name}", s.handleUploadFile)
}
// registerHistoryRoutes 挂载持久化聊天历史表面(design/16)
// 加上实时端点。会话/模型读与写是主人的
// 私有历史,因此仅 owner/admin(chat 用同样的角色);消息
// 内容(与 thinking)按 design/16 §6 仅 owner/admin,而上面
// GET /sessions 中的会话元数据仍可被任何已认证角色读取。
// 创建与删除子会话会改变持久化状态。
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)))
// 读游标(D4 / Android 协作 B3):推进它会为该账号
// 每一端清除该会话的未读角标,因此它和其他会话变更一样
// 是 owner/admin 历史写。它会广播一个 `session_read` 帧。
mux.Handle("POST /api/v1/sessions/{id}/read", writeAuth(http.HandlerFunc(s.handleMarkSessionRead)))
// 单条消息编辑/删除(Android 协作批次 ③a,D3)。消息由
// 自身非透明 id 定位;二者都是受 owner/admin 门控的历史写。
mux.Handle("PATCH /api/v1/messages/{id}", writeAuth(http.HandlerFunc(s.handlePatchMessage)))
mux.Handle("DELETE /api/v1/messages/{id}", writeAuth(http.HandlerFunc(s.handleDeleteMessage)))
// 实时。
mux.HandleFunc("/ws/chat", s.handleWS)
}
// registerAdminRoutes 挂载管理 API。三个读层加两个写
// 层,镜像角色层级(RBAC 红线):
//
// - viewerAuth(owner/admin/viewer):只读监控表面 —— 模块
// 清单、运行时指标/trace、后端日志尾部与
// 通知 feed。这里每个载荷都是非敏感投影,不含
// 凭据、不含其他账号的数据、不含消息体,这正是
// 让 viewer 角色成为可用的看板/日志/监控账号的原因。
// - writeAuth 读(owner/admin):viewer 不得看到的一切 —— 记忆、
// LLM provider 栈、账号/API key、后台思考监控、审计
// 链、渠道消息日志/转录、完整会话导出。
// - writeAuth 写(owner/admin):带审计的知识搜索。
// - ownerAuth(owner):带审计的策略写、每个获取凭据的
// 渠道端点(login-qr/login-status)以及每一个其他变更。
//
// 每个写 handler 都会持久化到真实后端存储,或用明确的
// "M10 / not-wired" 错误诚实作答,而不是编造成功。
func (s *Server) registerAdminRoutes(
mux *http.ServeMux,
ownerAuth func(http.Handler) http.Handler,
writeAuth func(http.Handler) http.Handler,
viewerAuth func(http.Handler) http.Handler,
) {
// 对 viewer 开放的管理读:只读监控/清单表面。
mux.Handle("GET /api/v1/admin/personas", viewerAuth(http.HandlerFunc(s.adminListPersonas)))
mux.Handle("GET /api/v1/admin/sessions", viewerAuth(http.HandlerFunc(s.adminListSessions)))
mux.Handle("GET /api/v1/admin/plugins", viewerAuth(http.HandlerFunc(s.adminListPlugins)))
mux.Handle("GET /api/v1/admin/plugins/installed", viewerAuth(http.HandlerFunc(s.adminListInstalledPlugins)))
mux.Handle("GET /api/v1/admin/channels", viewerAuth(http.HandlerFunc(s.adminListChannels)))
mux.Handle("GET /api/v1/admin/iot", viewerAuth(http.HandlerFunc(s.adminListIoT)))
mux.Handle("GET /api/v1/admin/automations", viewerAuth(http.HandlerFunc(s.adminListAutomations)))
// 知识库文档列表:名字/描述加上切分/嵌入
// 状态。`?query=` 搜索在旁一并提供,且保持只读。
mux.Handle("GET /api/v1/admin/rag", viewerAuth(http.HandlerFunc(s.adminListRag)))
mux.Handle("GET /api/v1/admin/observability/metrics", viewerAuth(http.HandlerFunc(s.adminObservabilityMetrics)))
mux.Handle("GET /api/v1/admin/observability/trace", viewerAuth(http.HandlerFunc(s.adminObservabilityTrace)))
// 后端日志尾部:logger 自带有界环形缓冲,由组合根注入
// (在那之前 501)。最新优先,按 `?level=`/`?limit=` 过滤。
mux.Handle("GET /api/v1/admin/observability/logs", viewerAuth(http.HandlerFunc(s.adminObservabilityLogs)))
// 统一通知 feed(design/10 §3 #16):审计链 + 后台思考推送的
// 只读投影,因此面板的通知页不需要
// 任何新的持久化模型。
mux.Handle("GET /api/v1/admin/notifications", viewerAuth(http.HandlerFunc(s.adminListNotifications)))
// 保留给 owner/admin 的管理读:私有内容、其他账号的
// 数据与审计链。不对 viewer 开放。
mux.Handle("GET /api/v1/admin/memories", writeAuth(http.HandlerFunc(s.adminListMemories)))
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/llm", writeAuth(http.HandlerFunc(s.adminGetLlm)))
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/observability/audit", writeAuth(http.HandlerFunc(s.adminObservabilityAudit)))
mux.Handle("GET /api/v1/admin/sessions/{id}/export", writeAuth(http.HandlerFunc(s.adminExportSession)))
mux.Handle("POST /api/v1/admin/rag/query", writeAuth(http.HandlerFunc(s.adminSearchRag)))
mux.Handle("POST /api/v1/admin/plugins/policy", ownerAuth(http.HandlerFunc(s.adminSetPolicy)))
// 管理写:仅 owner,带审计。
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 挂载外部提供的入站回调(例如 webhook
// 渠道)且不经网关认证;每个 handler 强制自己的
// 签名/授权。
func (s *Server) mountWebhooks(mux *http.ServeMux) {
for path, hdl := range s.webhooks {
if path == "" {
continue
}
mux.Handle(path, hdl)
}
}
// withMiddleware 把已路由的 handler 包进外层,最外层在前:
// 上传能力 URL 防护、SPA 回退(当配置了前端时)、
// panic recover、CORS 与请求日志。
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
}