package gateway import ( "context" "net/http" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/iot" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/emotion" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader" personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/events" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/flowgraph" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/knowledge" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/task" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink" ) // 本文件声明网关所消费的依赖契约。每一个都是窄接口, // 由组合根注入的具体后端满足(见 deps.go 中的 // Deps),因此网关绝不直接取用具体实现, // 测试也能替换成桩。 // Authenticator 抽象凭据 + RBAC 服务。*auth.JWTService 是 // 生产实现;它是接口,因此测试可替换为 mock。 type Authenticator interface { // Sign 为 sub 签发一个带给定角色与生命周期的 token。 Sign(sub string, role auth.Role, ttl time.Duration) (string, error) // Verify 校验一个 token 并返回其 claims。 Verify(token string) (*auth.Claims, error) // Authorize 返回一个 HTTP 中间件,强制至少一个所需角色。 Authorize(required ...auth.Role) func(http.Handler) http.Handler } // CapabilityStore 抽象管理 API 中用于枚举插件与渠道的全局 // 能力注册表。*capability.Registry 是生产 // 实现。 type CapabilityStore interface { // List 返回 Kind 匹配的每一个能力,kind 为空时 // 返回全部。 List(kind capreg.Kind) []capreg.Capability // SetPolicy 替换指定能力的可达性标记。 SetPolicy(name string, p capreg.ToolPolicy) error } // Chatter 运行单次 agent 回合。*loop.Loop 是生产实现。 type Chatter interface { // Run 为 sessionID 驱动一个回合并返回最终答案。 Run(ctx context.Context, sessionID string, messages []model.Message) (*loop.Result, error) } // SessionStore 列出已知的渠道会话。*session.SessionManager 是 // 生产实现。 type SessionStore interface { List(ctx context.Context) ([]session.Session, error) } // PersonaStore 列出已注册角色(每个与一个人格一一绑定)。 // *persona/registry.Registry 是生产实现。网关把 // 每个 Character 映射为其线上摘要;该接口刻意返回 // 领域类型,使线上结构不进入存储契约。 type PersonaStore interface { List() []personareg.Character // Persona 返回给定 id 的人格(用于读取其视觉资产)。 // 生产注册表实现了它;线上摘要从它读取 Avatar / // ChatBackground,使每个角色的视觉保持角色作用域。 Persona(id string) (*loader.Persona, bool) } // PersonaMoodStore 是人格存储的*可选*能力,该存储同时持有 // 每人格的情绪状态(design/01 §2:情绪跟随人格)。 // 生产实现 *personareg.Registry 实现了它;网关通过对这个窄 // 接口做断言来读取某角色的 mood,因此只列出人格的存储 // (测试替身、未来的只读目录)无需持有情绪机 // 就能继续满足 PersonaStore。当存储未实现它时, // 某角色的 mood 只读作 "",而不是被凭空造出。 type PersonaMoodStore interface { Emotions() *emotion.Manager } // MemoryStore 列出记忆条目。*runtime/memory.Store 是生产 // 实现。网关把每个 MemoryItem 映射为其线上摘要。 type MemoryStore interface { List(ctx context.Context) ([]memory.MemoryItem, error) } // HistoryStore 是持久化聊天历史存储(design/16)。*runtime/history.Store // 是生产实现。为 nil 时会话/消息端点退化:列出 // 会话回退到渠道会话视图,而创建/取消息/删除 // 端点返回明确的 501。 type HistoryStore interface { Append(ctx context.Context, msg history.Message) error // AppendFull 像 Append 一样持久化 msg,但返回完整的已存消息 // (含分配的 id/时间戳),使编排器能广播它以进行 // 跨端实时同步(design/16 §4.2)。 AppendFull(ctx context.Context, msg history.Message) (history.Message, error) List(ctx context.Context, sessionID string, before int64, limit int, platform string) ([]history.Message, error) // ListAfter 按最旧优先返回至多 limit 条严格新于 after 的消息, // 用于重连后的前向增量追赶("before" 游标只能 // 向后走)。platform 的过滤同 List。 ListAfter(ctx context.Context, sessionID string, after int64, limit int, platform string) ([]history.Message, error) Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error) ListPlatformLog(ctx context.Context, platform string, before int64, limit int) ([]history.Message, error) ListAdapterLog(ctx context.Context, adapterID string, before int64, limit int) ([]history.Message, error) ListAllLog(ctx context.Context, before int64, limit int) ([]history.Message, error) DeleteSession(ctx context.Context, sessionID string) error // Search 为 ?q= 消息搜索路径返回会话作用域的内容匹配 // (最新优先)。空查询得到空切片。 Search(ctx context.Context, sessionID, query string, before int64, limit int) ([]history.Message, error) // UpdateMessage 编辑单条消息的内容并返回更新后的 // 消息。未知 id 返回 history.ErrMessageNotFound。 UpdateMessage(ctx context.Context, id, content string) (history.Message, error) // DeleteMessage 移除单条消息。未知 id 返回 // history.ErrMessageNotFound。 DeleteMessage(ctx context.Context, id string) error // RenameSession 设置会话的显示标题(main 或 sub)。未知 id // 返回 history.ErrSessionNotFound。 RenameSession(ctx context.Context, sessionID, title string) error // SetPinned 置顶/取消置顶一个会话(main 或 sub)。未知 id 返回 // history.ErrSessionNotFound。 SetPinned(ctx context.Context, sessionID string, pinned bool) error NewSubSession(ctx context.Context, ownerID string) (string, error) // MarkSessionRead 把会话的读游标(D4)推进到 before 并返回 // 结果游标作为 Unix 毫秒时间戳;before 为零意味着 // 「现在」。游标只向前移动。未知 id 返回 // history.ErrSessionNotFound。 MarkSessionRead(ctx context.Context, sessionID string, before time.Time) (int64, error) ListSessions(ctx context.Context) ([]history.SessionMeta, error) } // IoTStore 列出 IoT 设备。*iot.Manager 是生产实现。 type IoTStore interface { List(ctx context.Context) ([]iot.Device, error) } // SubagentAuditor 为 subagent_ask WS 路由审计子代理的对话 // 上下文。*orchestration.Scheduler 是生产实现。 type SubagentAuditor interface { AuditContext(ctx context.Context, id string, mode orchestration.AuditMode, n int, query string) (*orchestration.AuditResult, error) } // TaskOutputStore 为 task_output_query WS 路由查询任务输出。 // *task.Manager 是生产实现。 type TaskOutputStore interface { TaskOutput(ctx context.Context, id string) (task.OutputEvent, error) } // AutomationStore 为管理列表枚举自动化与提醒。 // *automations.Service 是生产实现。网关把每条 // 记录映射为其线上结构。 type AutomationStore interface { List() []automations.AutomationRecord } // UserStore 枚举管理面板可见的账号与 API key。 // 生产实现读取已配置的管理账号加上 // auth.APIKey 表。网关把每个摘要映射为其线上结构。 type UserStore interface { List() []auth.UserSummary } // RefreshGrantStore 管理非透明 refresh token(auth.RefreshStore 是 // 生产实现)。它是登录与 POST /auth/refresh、/auth/logout 端点 // 所返回 refreshToken 背后的凭据。只持久化哈希; // 明文 token 每次签发/轮换恰好穿过该接口一次。 // 可选:为 nil 时登录省略 refreshToken, // refresh 与 logout 端点返回明确的 not-configured 错误。 type RefreshGrantStore interface { // Issue 为 userID/role 铸造一个 refresh token,返回其明文 // (仅可见一次)及其过期时间。 Issue(ctx context.Context, userID string, role auth.Role, ttl time.Duration) (string, time.Time, error) // Rotate 校验 plaintext,并在 opts.Rotate 设置时吊销它并 // 返回替代品。对未知、已吊销或已过期的 token 返回 // auth.ErrInvalidGrant 错误。 Rotate(ctx context.Context, plaintext string, opts auth.RotateOptions) (auth.RefreshToken, string, error) // Revoke 把匹配 plaintext 的 token 标记为已吊销,并返回是否 // 找到了存活的 token。它是幂等的。 Revoke(ctx context.Context, plaintext string) (bool, error) // RevokeAll 删除每一个 refresh token(秘密轮换时的全局 // 失效)并返回移除了多少个。 RevokeAll(ctx context.Context) (int64, error) // RevokeByUser 删除 userID 的每一个 refresh token(按访问 // 令牌登出,此时不知道具体是哪一份授权)并返回 // 移除了多少个。 RevokeByUser(ctx context.Context, userID string) (int64, error) } // BackthinkStore 为 M9/M10 监控面板枚举持久化的后台思考 // 记录。*backthink.Backthink 是生产实现。 type BackthinkStore interface { List(ctx context.Context, limit int) ([]backthink.Record, error) } // ChannelConfigStore 是运行时可编辑的渠道适配器配置 // (channels.json,design/04 §3.1)。它由组合根的渠道存储 // 实现:Load 返回按平台生效的账号(文件 → 环境变量 // 回退),Path 报告 channels.json 路径,Reload 在面板保存后 // 热替换某平台运行中的适配器(Bridge.Reload)。Load 返回 // 带真实 token 的账号配置,使 owner 写路径能持久化它们; // 管理读路径在返回客户端前对它们脱敏。 type ChannelConfigStore interface { Path() string // Load 返回按平台生效的账号(文件 → 环境变量回退), // 含持久化它们所需的秘密 token。 Load() map[string][]config.ChannelAccount // Reload 重建并热替换该平台运行中的适配器。 Reload(platform string) error } // OwnerRegistrar 把一个渠道账号 id 登记进主人白名单 // (configs/users/owner.yaml)。它是结构化的身份红线:扫码登录 // 确认会把扫码者绑定的 ilink_user_id 盖进白名单,使 // 渠道 bridge 经 actor 元数据认出它是主人,绝不是提示词。 // 组合根实现了它(cmd/cyrene 的 ownerRegistrar)以持久化 // 白名单并热更新活的 owner profile。登记是尽力而为: // 失败由调用方记日志,但不会让已经施加的 // channels.json 持久化失败。 type OwnerRegistrar interface { RegisterChannelOwner(ctx context.Context, platform, accountID string) error } // RagStore 支撑管理侧知识库 RAG 模块(design/10 §3.12 module 8)。 // *runtime/knowledge.Store 是生产实现:它列出/搜索 // 项目索引、导入文档(切分 + 索引)、删除文档 // (项目条目 + 级联章节)。不能写入的实现 // 会报告明确的错误(knowledge.ErrWriteUnsupported),而不是编造 // 成功;handler 把文档缺失映射为 404。 type RagStore interface { // ListDocuments 枚举知识库文档(项目索引 // 条目)。 ListDocuments(ctx context.Context) ([]knowledge.Doc, error) // Search 应答一次知识库查询(行级层级 + 可选 // 语义/向量排序)。 Search(ctx context.Context, query string) ([]knowledge.Hit, error) // IndexDocument 把一份文档上传/切分/嵌入进项目索引。 IndexDocument(ctx context.Context, doc knowledge.Doc) error // DeleteDocument 按 id 移除一份文档(及其章节),在 // 不存在该文档时返回 knowledge.ErrDocumentNotFound。 DeleteDocument(ctx context.Context, id string) error } // LlmUsageStore 是 LLM 用量汇总的**只读**出口(切片 U1, // GET /api/v1/admin/llm/usage)。*runtime/model.UsageReader 是生产 // 实现:它在一块时间窗内聚合 `llm_usage` 表的 token 计数与缓存两档, // 按用途与模型分解。 // // 汇总里**没有任何凭据、也没有消息正文** —— 那些东西根本没进 // `llm_usage` 表(写入侧 model.UsageWriter 的样本里就没有这些字段)。 // 为 nil 时端点诚实作答 500 usage_unavailable,绝不回一份全 0 // 的假汇总。 type LlmUsageStore interface { // Summary 聚合 q 描述的时间窗与用途过滤。q 的时间窗由 // handler 校验并补齐(缺省近 24h、上限 90 天)。 Summary(ctx context.Context, q model.UsageQuery) (model.UsageSummary, error) } // ChannelEventStore 是渠道事件留存的**只读查询出口**(design/26 §4.5 / §6.5; // 切片 E6)。生产实现是 `internal/runtime/events` 的 `*SQLStore`,组合根把 // 同一个句柄既交给桥接层(写入面,E3)也交给网关(本查询面)。 // // 它刻意只声明三个**读**方法:写入、计数落盘与保留期清扫是桥接层与组合根的 // 职责,管理查询面拿不到、也不该拿到它们(最小权限)。 // // 查询结果里**没有任何平台原文 / 媒体 URL / 凭据 / OneBot `flag`** —— 事件表 // 本身就没有这些列(§4.2 红线),因此这里不存在「回显敏感字段」的代码路径。 // // 为 nil 时两个端点诚实作答 500 `channel_events_unavailable`,绝不回一份全 0 // 的假汇总(与 `usage_unavailable` / `storage_unavailable` 同一口径)。 type ChannelEventStore interface { // List 按时间倒序返回符合条件的事件行(Limit/Offset 见 events.Filter)。 List(ctx context.Context, f events.Filter) ([]events.Event, error) // Count 统计符合条件的事件行数(List 的分页配套)。 Count(ctx context.Context, f events.Filter) (int64, error) // Counters 返回逐类计数汇总;platform 为空表示全部平台。它读的是**已落盘** // 的表,不含内存里待 flush 的部分(即 §4.3 的「近似值」)。 Counters(ctx context.Context, platform string) ([]events.Counter, error) } // FlowGraphSource 是**最近一轮链路图**的只读出口(契约 v0.71 的 // `GET /api/v1/admin/flow/graph`;design/19 §4 的 P3)。生产实现就是 // `*loop.FlowPublisher`(`internal/runtime/loop/flow.go` 的 `LastGraph`), // 组合根把**同一个**发布器既交给 loop/orchestration(写入面:每帧 span // 发布 `ws.graph_update`)也交给网关(本查询面)。 // // 它刻意只声明**两个只读方法**(v0.77 起是两个:`LastGraph` + `LatestLanes`): // 发布、建图、轮次计数与发布会话白名单都留在发布器自己手里,管理查询面拿不到、 // 也不该拿到它们(最小权限)——因此只读查询**不可能**改变任何会话的实时图, // 也不可能把面板看不见的会话变成可查的(白名单外的会话从未建图,两者自然回空)。 // // 为 nil 时端点诚实作答 500 `flow_unavailable`,绝不回 `graph:null` // (那会把「图源没接线」伪装成「这一轮确实没有图」—— // 与 `usage_unavailable` / `channel_events_unavailable` 同一口径)。 type FlowGraphSource interface { // LastGraph 返回 sessionID 最近一轮图的一份只读快照;该会话还没有图 // (从未跑过带 span 的轮次,或本就不在发布会话范围内)时返回 nil。 LastGraph(sessionID string) *trace.FlowGraph // LatestLanes 返回 sessionID **最近一轮每一路**的图快照(契约 v0.77 的 // `lanes[]`),按路的开始时间升序;没有图时返回空切片。 // // 真并行时同一会话同一轮有多条路(design/15 §2.5.5),`LastGraph` 只给出 // 其中最新的一条 —— 管理面板要靠本方法才看得全「两路各自的图」,否则 // 后起的那一路会把前一路从面板上顶掉(design/15 §2.5.10-1)。它同样是 // **只读**的:不发布、不改图、不动轮次计数。 LatestLanes(sessionID string) []*trace.FlowGraph } // FlowGraphStore 是**已落盘轮次图**的只读出口(契约 v0.73 的 // `GET /api/v1/admin/flow/graph` 的 `?turnId=` 与 `turns[]`;design/19 §4 的 P3 // 「多轮历史回放」;**v0.76 ① 登记的工具回看面已由本包的 `GraphsByTurnID` 接上** // (版本号由主会话在 `Docs/api-client.md` 统一登记))。生产实现是 // `internal/runtime/flowgraph` 的 `*SQLStore`,组合根把**同一个**句柄交给 // 发布器(写入面:EndRound 每路落一条)与本查询面(管理面板 + 消息读取面)。 // // 它刻意只声明四个**读**方法:写入(RecordRound)与保留期清扫(Sweep)是发布器与 // 组合根的职责,查询面拿不到、也不该拿到它们(最小权限)。 // // 为 nil 时管理端点回退到**只有内存**的行为(v0.71 口径):`turns[]` 为空、`?turnId=` // 只能命中内存里那一轮;消息读取面则整体失去工具回看面(`toolCalls` 缺席,正文不受 // 影响)。内存与落盘**都没接线**时才回 500 `flow_unavailable`。 type FlowGraphStore interface { // Graph 按 (sessionID, turnID) 取回一轮的图;该轮不存在时返回 (nil, nil)。 Graph(ctx context.Context, sessionID, turnID string) (*trace.FlowGraph, error) // Latest 取该会话最近一轮已落盘的图;没有已落盘的轮次时返回 (nil, nil)。 Latest(ctx context.Context, sessionID string) (*trace.FlowGraph, error) // Turns 返回该会话已落盘轮次的摘要(最新在前),供面板选历史轮。 Turns(ctx context.Context, sessionID string, limit int) ([]flowgraph.Turn, error) // GraphsByTurnID 按**路标识**(`chat_messages.turn_id`,迁移 0018)批量取图: // 返回 `路标识 → 该路的图` 的映射,**取不到的标识不出现在映射里**。 // // 它是消息读取面「工具回看面」(契约 v0.76 ① 的 `toolCalls[]`;切片 CODE-TC) // 的读侧入口,补的正是 v0.76 登记的那块缺口:`Graph` 按**图 id**取(`turn_id` // 列),而消息面手里只有**路标识**(`lane_turn_id` 列,迁移 0023)—— 两者不同源, // 因此必须有这个按路查的入口。一页消息的标识集合**一次查完**,调用方绝不按消息 // 逐条查(禁 N+1)。 // // 查询失败如实上抛(调用方据此让回看面缺席并记 WARN);**取不到不是错误**, // 缺席天然落在「映射里没有这个键」上。 GraphsByTurnID(ctx context.Context, sessionID string, laneTurnIDs []string) (map[string]*trace.FlowGraph, error) }