Files
Cyrene_v2/Docs/api-reference.md
T
AskaEth 58cba0b8d2
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(api): api-client → v0.53(阶段 5 全部契约)+ api-reference 同步 + .env.example 补 9 键
- 新端点 GET /admin/llm/catalog 全契约(200/gzip/503 catalog_unavailable/快照副作用/不进热路径/审计 llm.catalog)
- models.json 组级三键:reasoning.kind(形状)+ personaModels/personaProviders(角色级模型/协议路由),
  含「缺省=保留、给出=整块/整表替换」语义、Group.Validate 的 400 口径、回落与 WARN 规则
- GET /admin/llm 新增恒下发的 reasoning 与两个取值域(reasoningKinds/personaProviderOptions)
- 语义修正(此前文档写反):multimodal = 主模型能否收图;LLM_VISION_MODEL/LLM_OCR_MODEL 只配视觉/OCR 工具
- 新工具 image-crop(x/y/width/height、50MiB/16MP/8MiB 上限、越界报错不 clamp、策略默认 owner-only)
- 推理开关按形状下发:替换 v0.41「Qwen 只 WARN 不发」;effort 非法值改为丢弃+WARN(不降档)、effort:none 显式关思考
- .env.example 新增 9 键(LLM_REASONING_KIND / LLM_OCR_MODEL / LLM_LOCAL_* / CAPACITY_L3_*),默认均为保守值
- 权限矩阵计数按 routes.go 校正 56→59(owner/admin 只读档 9→10,含 /admin/llm/catalog)
2026-10-04 13:07:09 +08:00

97 KiB
Raw Blame History

Cyrene v2 后端 API 参考(api-reference.md)

权威归属(先读这条):契约以 Docs/api-client.md 为准——那是版本化的跨平台客户端契约,每个端点的形状 / 语义 / 版本变更都记在那里(当前 v0.53)。本文件是内部与运维参考:端点全集索引 + 示例 + 部署细节。凡是与 api-client 重复的契约性描述,一律以 api-client 为准(本文件不再维护第二份契约);发现两者不一致时,改本文件而不是改 api-client。 本文件的正确用法:查「有哪些端点、大致形状、错误码、部署注意事项」可以;写客户端代码时请以 Docs/api-client.md 为准,本文件的字段表 / 示例可能滞后。 历史包袱(如实):本文件长期自称「全部后端 API 的唯一详细参考 / 超详细主文档 / 每变更必同步」,但实际系统性滞后于实现与 api-client(2026-10-03 的 16 路 design-vs-code 审计结论)。本次已修正与实现直接矛盾的陈述、并把若干复述型段落改为引用 api-client;仍未登记的端点 / WS 事件清单见 DevDocs/audit/00-总览.md §4④,属后续「文档补全」批次,本文件暂不补全。 其余仍成立:所有字段名、取值、鉴权规则、状态码最终以代码为准(internal/services/gateway/*.go);契约变更版本化(REST 走 /api/v1,破坏性变更升 v2),不得破坏既有客户端(版本化承诺本身记在 api-client §5)。


0. 基础约定(全局)

  • Base URL:http://<host>:8080。网关是唯一对外端口(单二进制、单端口 :8080、零跨进程 IPC 红线);生产一般经反向代理 + HTTPS。
  • 版本:REST 全部挂在 /api/v1/{resource};WS 挂 /ws/chat;管理端点挂 /api/v1/admin/*。
  • 响应统一信封:
    • 成功:{"data": <payload>}(HTTP 200 / 201 / 202 / 204)。
    • 失败:{"error": {"code": <string>, "message": <string>}}(HTTP 400/401/403/404/409/500/501 等)。
  • 错误码:业务错误带稳定字符串 code(如 unauthorized / not_found / bad_request / chat_busy / not_implemented),便于客户端分支;message 面向人读。
  • CORS:Access-Control-Allow-Origin 为配置的 ALLOWED_ORIGINS 白名单(* 时放开);Allow-Methods: GET, POST, PUT, DELETE, OPTIONS;Allow-Headers: Authorization, Content-Type, X-API-Version;Access-Control-Expose-Headers: X-Voice-Provider(跨域浏览器可读语音降级信号头);Max-Age: 600。OPTIONS 预检直接回 204。同源/无 Origin 请求放行。注意:X-API-Version 仅作advisory惯例头(见 §0.2)——CORS 已放行但它不参与服务端版本协商**,服务端当前不校验、不据此改变行为。**
  • 请求日志:每个请求由 logging 中间件记录一行(method/path/status/duration_ms),并计入可观测性指标。panic 由 recover 中间件兜底为 500 {error:{code:internal,...}},不崩溃进程。
  • 超时:WS 连接 ping 周期 54s、pong 等待 60s、写超时 10s;读上限 4096 字节/帧。

0.1 鉴权模型(RBAC)

认证基于 JWT Bearer(internal/auth),角色层级 owner > admin > viewer(owner 满足 admin/viewer,admin 满足 viewer,viewer 仅满足 viewer)。

  • 获取 token:POST /api/v1/auth/login(见 §2.2)。token 有效期 24 小时(tokenTTL),签发角色恒为 owner。
  • REST 鉴权:请求头 Authorization: Bearer <JWT>。s.auth.Authorize(...) 中间件:
    • 缺失/无效/过期 token → 401 {error:{code:"unauthorized",...}}。
    • 有效 token 但角色不足 → 403 {error:{code:"forbidden",...}}。
    • Authorize()(无参)→ 任意已认证角色通过(only read metadata, e.g. GET /sessions、GET /personas)。
    • Authorize(owner) → 仅 owner;Authorize(owner, admin) → owner 或 admin。
  • WS 鉴权(连接门与帧门是两回事):GET /ws/chat 通过 ?token=<JWT> 或 Authorization: Bearer 取 token(wsToken),Verify 通过即可升级——连接本身不校验角色(任意有效 token,含 viewer,都可连接)。但连接之后的帧按角色分档(design/11 §7.2 上行 + §7.1 下行,2026-10-03 落实;权威描述见 api-client §3 顶部的「两把角色门」,本节不重复维护):
    • 上行帧:非 owner/admin 连接发来的任何上行 JSON 帧一律 fail-closed 默认拒,定向回 {type:"error",data:{code:"forbidden"}}——不广播、不驱动模型、不落库(internal/services/gateway/ws_route.go 的 route 首条语句即此门)。协议级帧(WS ping/pong/close)不受影响。
    • 握手 history 回放:仅 owner/admin 连接收到(internal/services/gateway/ws_history.go 的 replayHistory 首行门)。
    • 下行帧:只有 7 个不含内容的状态机信号(session_busy/pending_ack/pending_drain/session_idle/session_interrupted/pending_full/session_read)发给所有已认证连接;其余全部(正文 / 推理 / 工具 / 持久化与编辑删除的消息 / graph_update / subagent_event / task_output / mood / device_update / reminder …)仅发 owner+admin;未登记的新帧类型默认只发 owner/admin(internal/services/gateway/ws_frame_scope.go 的 frameReachesAll,fail-closed)。
    • 口径:viewer = 可连接、可读 12 条只读监控 REST、可收 7 个状态机信号帧;除此以外 WS 上拿不到任何内容、也发不出任何指令。
  • API Key(X-API-Key):API key 已接入鉴权(v0.51,见 api-client.md §认证)——已认证端点同时接受该头,命中签发表即通过且一律 owner 级,带该头时只走 key 分支(未知/已吊销/空值回 401、不回落 Bearer);WS 握手仍只认 Bearer。

0.2 X-API-Version 头

Docs/api-client.md 建议客户端可带 X-API-Version: 1。该头是 advisory 惯例头——服务端不校验、不据此协商版本,带不带行为完全不变(CORS Allow-Headers 已放行它,跨域客户端发送不会预检失败)。若未来要真正版本化(/api/v1 → 语义化协商),需另行实现服务端校验;在那之前客户端不应依赖它做任何行为分支。

0.3 上传 / 媒体(重要确认)

  • POST /api/v1/upload(B1)上传图片 → 返回同源 URL(/api/v1/files/<name>),可直接作 chat.image;也可继续用 base64 data: URI 或 http(s) URL 走 POST /api/v1/chat / WS chat 的 image 字段(入视觉路由,见 §6 流式与直连)。语义(v0.53 修正,见 api-client §1 行 3):主模型能否收图 = 生效 models.json 组的 multimodal(不是「有没有配独立视觉模型」);LLM_VISION_MODEL / LLM_OCR_MODEL 只配视觉/OCR 工具用的模型;主模型不收图时由 vision/OCR 工具返回文本描述(另有 image-crop 工具可裁区域后重读,便于大图分区精读)。
  • REST 语音端点已存在:POST /api/v1/audio/transcribe(STT)、POST /api/v1/audio/speech(TTS)、POST /api/v1/audio/voice-clone(声音复刻),均 owner-only + 审计,绕开 WS 4KB 帧限。语音也可走 WS voice_input(上行)/voice_output(下行)。
  • 管理端导入知识文档(RAG)走 JSON body 文本(POST /api/v1/admin/rag),非文件上传。

1. REST 端点总览

下表为全量登记;下文 §2–§5 每端点给出字段级说明。鉴权列:无 / JWT(任意已认证) / owner|admin / owner。里程碑 为代码/设计归属的大致里程碑(M8 网关、M9 前端、M10 补齐写、M16 会话持久化等)。

1.1 公开(无鉴权)

# 方法 路径 状态 说明 里程碑
1 GET /api/v1/health 200 存活探针 {status:"ok"} M8
2 POST /api/v1/auth/login 200 换取 JWT(恒 owner) M8

1.2 用户侧(已认证)

# 方法 路径 鉴权 状态 说明 里程碑
3 POST /api/v1/chat owner|admin 200/202/409 发消息(同步回复或触发 WS 流式) M8/M15
4 GET /api/v1/sessions JWT 200 会话列表(持久化会话 or 渠道会话回退) M16
5 GET /api/v1/personas JWT 200 人格列表 M8
6 GET /api/v1/sessions/{id}/messages owner|admin 200 会话消息热分页 M16
7 POST /api/v1/sessions owner|admin 201 新建次级会话 M16
8 DELETE /api/v1/sessions/{id} owner|admin 200 删除次级会话 M16
9 PATCH /api/v1/sessions/{id} owner|admin 200 会话改名(A4) + 置顶(pinned, D5) body {title?,pinned?} M16
10 PATCH /api/v1/messages/{id} owner|admin 200 编辑单条消息(D3) body {content} M16
11 DELETE /api/v1/messages/{id} owner|admin 200 删除单条消息(D3) M16
12 GET /api/v1/meta/enums JWT 200 客户端共享枚举权威表(B4):platform/mood/mode + unknown 回落约定 M8

1.3 管理后台(/api/v1/admin/*)

鉴权分档(以 internal/services/gateway/routes.go 为准;权威矩阵见 Docs/api-client.md §2.0,本表已按其逐条核对):管理读分两层——只读监控档 owner|admin|viewer(12 条非敏感 GET:personas / sessions / plugins / plugins/installed / channels / iot / automations / rag / observability/{metrics,trace,logs} / notifications,后三条尚未登记进下表、见审计 00-总览.md §4④)与 owner|admin 档(含他人数据 / 消息正文 / 凭据面 / 审计流水);写一律 owner + 全程审计。下表按注册顺序,标注鉴权与状态。

# 方法 路径 鉴权 状态 说明 里程碑
9 GET /api/v1/admin/personas owner|admin|viewer 200 人格列表 M8
10 GET /api/v1/admin/sessions owner|admin|viewer 200 会话列表 M16
11 GET /api/v1/admin/memories owner|admin 200 记忆列表 M8
12 GET /api/v1/admin/plugins owner|admin|viewer 200 插件/能力列表 M8
13 GET /api/v1/admin/channels owner|admin|viewer 200 渠道列表 + 已配账号(掩码+bot 身份+capabilities) M8/M17
14 GET /api/v1/admin/channels/messages owner|admin 200 IM 适配器日志流(全量/平台/适配器三粒度) M16
15 GET /api/v1/admin/channels/{platform}/messages owner|admin 200 某平台 IM 适配器日志流 M16
16 GET /api/v1/admin/iot owner|admin|viewer 200 IoT 设备列表 M8
17 GET /api/v1/admin/llm owner|admin 200 LLM 多厂商多模型组(APIKey 不回显) M8/M15
18 GET /api/v1/admin/automations owner|admin|viewer 200 自动化/提醒列表 M8
19 GET /api/v1/admin/users owner|admin 200 账号/API-key 列表 M8
20 GET /api/v1/admin/backthink owner|admin 200 后台思考记录 M9/M10
21 GET /api/v1/admin/rag owner|admin|viewer 200 知识库文档列表(可带 ?query= 并入命中) M10
22 POST /api/v1/admin/rag/query owner|admin 200 知识库检索 M10
23 POST /api/v1/admin/rag/delete owner 200/404 删除知识文档(项目索引条目 + 级联章节,真实删除;不存在 404) 后 M12
24 GET /api/v1/admin/observability/metrics owner|admin|viewer 200 网关请求指标 M10
25 GET /api/v1/admin/observability/trace owner|admin|viewer 200 最近 trace span M10
26 GET /api/v1/admin/observability/audit owner|admin 200 审计记录 M10
27 POST /api/v1/admin/plugins/policy owner 200 写工具策略(审计) M8
28 GET /api/v1/admin/sessions/{id}/export owner|admin 200 导出会话元数据 M8
29 POST /api/v1/admin/personas owner 201 新建人格 M8
30 PUT /api/v1/admin/personas/{id} owner 200 编辑人格 M8
31 POST /api/v1/admin/users owner 501 新建用户(M10) M10
32 POST /api/v1/admin/users/apikey owner 201 签发 API key M8
33 DELETE /api/v1/admin/users/apikey/{id} owner 200 吊销 API key M8
34 POST /api/v1/admin/iot/{id}/control owner 200 设备控制 M8
35 POST /api/v1/admin/iot/scenarios owner 501 IoT 场景(M10) M10
36 POST /api/v1/admin/iot/rules owner 501 IoT 规则(M10) M10
37 POST /api/v1/admin/automations owner 201 新建自动化/提醒 M8
38 POST /api/v1/admin/automations/{id}/run owner 200 手动触发规则 M8
39 DELETE /api/v1/admin/automations/{id} owner 200 删除规则 M8
40 POST /api/v1/admin/llm owner 201 保存 LLM provider 组(落盘 + 热更新) M8/M15
41 POST /api/v1/admin/llm/test owner 200 真实模型调用测试 M8/M15
42 POST /api/v1/admin/channels/open owner 200 连接渠道 M8
43 POST /api/v1/admin/channels/close owner 200 断开渠道 M8
44 POST /api/v1/admin/channels/test owner 200 结构性身份识别测试 M8/M16
45 GET /api/v1/admin/channels/{platform}/login-qr owner 200 微信 iLink 扫码绑定 QR M16
46 GET /api/v1/admin/channels/{platform}/login-status owner 200 微信 iLink 扫码状态 M16
47 GET /api/v1/admin/channels/{platform}/view owner 200 KOOK 账号聚合视图 M17
48 GET /api/v1/admin/channels/{platform}/friends owner 200 KOOK 好友/申请/屏蔽 M17
49 POST /api/v1/admin/channels/{platform}/friend/handle owner 200 KOOK 处理好友申请 M17
50 POST /api/v1/admin/channels/{platform}/message/test owner 200 KOOK 发送测试消息 M17
51 PUT /api/v1/admin/channels/{platform}/config owner 201 保存渠道账号配置(落盘 + 热更新) M16
52 DELETE /api/v1/admin/channels/{platform}/config owner 200 删除渠道账号 M16
53 POST /api/v1/admin/plugins/install owner 200 热安装 MCP server 插件(body {kind:"mcp",name,config:{transport,endpoint?/command?,args?,apiKey?,env?}} → 连接+发现+RegisterTool 进 registry → 落盘 plugins.json → 200 {data:{installed,name,kind,transport,toolCount}});连接/发现失败 502 plugin_install_failed 且不落盘;非法 400;manager 未注入 501 后 M12
54 POST /api/v1/admin/plugins/uninstall owner 200 移除插件(MCP → Unregister+删 plugins.json 配置;普通能力 → reg.Remove) M8
55 POST /api/v1/admin/memories owner 201 手动添加记忆 M8
56 POST /api/v1/admin/memories/search owner 200 记忆检索 M8
57 POST /api/v1/admin/rag owner 201 导入知识文档 M10
58 POST /api/v1/admin/system/revoke-keys owner 200 吊销全部 API key M10/M11
59 POST /api/v1/admin/system/rotate-jwt owner 200 轮换 JWT 密钥 M10/M11
60 POST /api/v1/admin/system/backup owner 200 备份数据库 M10/M11
61 POST /api/v1/admin/system/restore owner 200 恢复数据库 M10/M11

另:外部 webhook 回调(如 webhook 渠道的 HMAC 回调)由 composition root 通过 Deps.Webhooks 动态挂载,不走网关鉴权(handler 自校验签名/授权);路径由调用方决定,不在上表枚举。

WS:GET /ws/chat(握手,JWT,见 §6)。


2. 公开端点

2.1 GET /api/v1/health

  • 鉴权:无(探针)。
  • 响应:200 {"data":{"status":"ok"}}。
  • 备注:仅存活探针;不含依赖健康状态。示例:
curl -s http://<host>:8080/api/v1/health
# => {"data":{"status":"ok"}}

2.2 POST /api/v1/auth/login

  • 鉴权:无(换取 JWT)。
  • 请求 Body(loginRequest):
字段 类型 必填 说明
username string 是 管理员用户名(对照 cfg.Auth.AdminUser)
password string 是 管理员密码(对照 cfg.Auth.AdminPass)
{"username":"admin","password":"s3cret"}
  • 响应:200 {"data":{"token":"<JWT>","expiresAt":<unix秒>,"role":"owner","refreshToken":"<opaque>"}}(loginResponse)。
字段 类型 说明
token string 已签名 JWT(HS256),下文请求头 Authorization: Bearer <token>
expiresAt int64 过期时间,Unix 秒 = now + 24h(tokenTTL=24h)
role string 恒为 "owner"(代码硬编码 auth.RoleOwner)
refreshToken string 不透明 256-bit 随机串(B2)——服务端仅存其 SHA-256 哈希,明文只在响应出现一次;用于 POST /api/v1/auth/refresh 静默续期,AUTH_REFRESH_TTL 默认 720h
  • 错误:400 {"error":{"code":"bad_request","message":"invalid request body"}}(JSON 解析失败);401 {"error":{"code":"unauthorized","message":"invalid username or password"}};500 {"error":{"code":"login_failed",...}}(签名失败)。
  • 备注:登录响应以代码为准 = {token, expiresAt, role}(不是 {token,jwt})。旧 api-client.md §1 表内一行写 {token,jwt} 为历史笔误,以本档为准。curl:
curl -s -X POST http://<host>:8080/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"s3cret"}'
# => {"data":{"token":"eyJ...","expiresAt":1799...,"role":"owner","refreshToken":"...base64url..."}}

2.3 POST /api/v1/auth/refresh(B2)

  • 鉴权:公开,body 带 refreshToken。
  • 请求 Body:{refreshToken:"<opaque>"}。
  • 响应:200 {"data":{"token":"<新JWT>","expiresAt":<unix秒>,"refreshToken":"<新opaque?>"}}——默认单次使用旋转:旧 refresh 立即作废并签发新的;AUTH_REFRESH_ROTATE_DISABLED=true 时旧 token 保持有效、不返回新 refresh(refreshToken 省略)。
  • 错误:400 bad_request(JSON 解析失败 / 空 token);401 {"error":{"code":"invalid_grant",...}}(未知/已撤销/已过期,统一口径,防枚举);仅成功才审计。
  • curl:curl -s -X POST http://<host>:8080/api/v1/auth/refresh -H 'Content-Type: application/json' -d '{"refreshToken":"<opaque>"}'

2.4 POST /api/v1/auth/logout(B2)

  • 鉴权:公开,两种方式二选一——body {refreshToken} 撤单个 grant(幂等);或无 body 但带 Authorization: Bearer <jwt> 撤该账号全部 grant(全设备登出)。
  • 响应:200 {"data":{"ok":true,"revokedCount":<n>}}(单撤 revokedCount 为 0/1;全撤为该账号 refresh 行数)。
  • 错误:不存在的 token 为幂等 no-op(仍 200);非法 body 400。

3. 用户侧端点(已认证)

3.1 POST /api/v1/chat

  • 鉴权:Authorization: Bearer <JWT>,Authorize(owner, admin)(owner 或 admin)。viewer 不能写。
  • 请求 Body(chatRequest):
字段 类型 必填 说明
sessionId string 否 目标会话 id;缺省 "web"(主会话)。子会话用 web:<uuid>
text string 是* 用户文本。text 与 image 至少一项非空
image string 否 视觉输入:base64 data: URI 或 http(s) URL;入 vision/OCR 工具或主模型多模态(design/06)。图文并存则为带图 + 文本的 turn。语义(v0.53):主模型能否收图看生效组的 multimodal;LLM_VISION_MODEL/LLM_OCR_MODEL 只配视觉/OCR 工具的模型(见 api-client §1 行 3)
deviceId string 否 客户端生成的端标识(design/16 §4.5,如 web:chrome:a1b2c3),落库到消息,可选
mode string 否 行为模式(design/01 §3、design/16 §5.1,如 chat/code/…);随该轮落库供气泡渲染;IM 恒 chat、缺省回落 chat(chatRequest.Mode)
stream bool 否 true = 经 WS 触发流式(publishChatStream 发 ws.chat),立即返回;缺省 / false = 同步返回完整回复(Go bool 零值即 false,仍无「必填」约束)
{"sessionId":"web","text":"你好","deviceId":"web:chrome:a1b2c3","stream":false}
  • 响应:
    • 同步(stream=false,会话空闲)200 {"data":{"reply":"<助手文本>","steps":<int>,"stopReason":"<string>"}}(chatResponse)。steps 为工具循环步数,stopReason 为停止原因(如 stop/idle/error)。
    • 同步但会话正忙(SubmitSync 返回 Pending)202 {"data":{"queued":true,"queueDepth":<int>}}(非阻塞 ack,稍后按上下文续接)。
    • 流式(stream=true)200 {"data":{"stream":true}} —— 实际文本经 WS 推送(见 §6)。
  • 错误:
    • 400 {"error":{"code":"bad_request","message":"text or image is required"}}(空消息)。
    • 409 {"error":{"code":"chat_busy","message":"会话正忙,待处理消息已满"}}(pending 队列满,PendingFull)。
    • 500 {"error":{"code":"chat_unavailable",...}}(runner 未注入)/ {"error":{"code":"chat_failed",...}}(回合运行出错)。
  • 备注:
    • 会话编排(design/15):同会话正忙时新消息不阻塞、入 pending(默认上限 5);满则 409 chat_busy。状态经 WS 状态事件通知(§6.4)。
    • 主会话 web 绑定全量访问 env(loop.OwnerEnv():记忆读写 + 行为不变)。
    • 敏感字段(image 可能为 base64)不走日志明文。
    • curl:
TOKEN=$(curl -s -X POST http://<host>:8080/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"s3cret"}' | jq -r .data.token)
curl -s -X POST http://<host>:8080/api/v1/chat \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'X-API-Version: 1' \
  -d '{"sessionId":"web","text":"讲个笑话","stream":false}'
# => {"data":{"reply":"...","steps":2,"stopReason":"stop"}}

3.2 GET /api/v1/sessions

  • 鉴权:Authorize()(任意已认证角色可读元数据)。
  • 响应:注入 HistoryStore 时返回持久化会话(historySessionSummary);否则回退渠道会话列表(SessionSummary,或 [])。

A) 持久化会话(history != nil,主 + 子):

{"data":[
  {"id":"web","type":"main","title":"","createdAt":1753...},
  {"id":"web:<uuid>","type":"sub","title":"临时小事","createdAt":1753...}
]}
  • id:主会话恒 "web";次级为 web:<uuid>。
  • type:"main" | "sub"。
  • title:可选(omitempty)。
  • createdAt:int64(Unix 毫秒)。

B) 渠道会话回退(history == nil):

{"data":[{"platform":"kook","channelId":"...","sessionId":"...","actorRole":"owner","owner":true,"isGroup":false,"memWritable":true}]}

SessionSummary 字段:platform / channelId / sessionId / actorRole(owner|admin|viewer|...)/ owner(bool) / isGroup(bool) / memWritable(bool)。

  • 错误:500 {"error":{"code":"sessions_failed",...}}(列表失败)。
  • 备注:HistoryStore 未注入时后端不报错,退化为渠道视图(向后兼容)。curl:
curl -s http://<host>:8080/api/v1/sessions -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

3.3 GET /api/v1/personas

  • 鉴权:Authorize()(任意已认证角色)。
  • 响应:200 {"data":[<PersonaSummary>...]}(无 store 时 [])。

PersonaSummary 字段(契约见 api-client §1 行 9;用户侧 GET /api/v1/personas 与管理员侧 GET /api/v1/admin/personas 同形,唯一差别是 admin 侧多一个 thinking?):

字段 类型 说明
id string 人格 id(PersonaID,回退 ID)
name string 显示名(c.Name)
character string 角色名(当前恒 = name)
isDefault bool 是否默认人格
mode string 首个行为模式(modes[0],未设置为 "";内置默认人格未配置 modes,故实际为 "")
memoryNamespace string 记忆命名空间。读侧当前恒为空串 ""(collectPersonas 不填此字段——已知实现事实,客户端不要依赖读侧的该字段);人格写响应(POST/PUT /admin/personas)才填真实值,未显式配置时为派生默认 persona.<id>(loader.Persona.MemoryNamespace())
avatar string 视觉头像 URL(omitempty;该人格自己的,缺失则为空)
chatBackground object omitempty;{landscape, portrait} 双朝向壁纸,仅该人格自己的
mood string 该角色当前情绪(design/01 §2;恒返回,无 omitempty)。取值见 GET /meta/enums 的 moods;"" = 该角色暂无被跟踪的情绪(不伪造默认值)
thinking object 仅 GET /admin/personas 返回(omitempty,未配置则无该键):{budgetTokens,effort,showToUser,mode} 推理/呈现旋钮;用户侧 GET /personas 不返回(详见 api-client「人格 thinking 读侧契约」)
{"data":[{"id":"cyrene","name":"Cyrene","character":"Cyrene","isDefault":true,"mode":"","memoryNamespace":"","mood":"calm","avatar":"/images/Cyrene_Avatar/3rd_Form/Cyrene-3F-Q-Happy-1.png","chatBackground":{"landscape":"/images/Cyrene_ChatBackground/Landscape/3rd_Form/1.png","portrait":"/images/Cyrene_ChatBackground/Vertical/2nd_Form/1.png"}}]}

上例中 memoryNamespace:"" 是读侧的当前真实值;mood:"calm" 来自默认人格 configs/personas/cyrene.yaml 的 emotion.initialState(其它角色可能为 "")。

  • 备注:视觉资产按角色作用域读取,绝不返回别的角色的;缺失时前端回退中性默认。curl:
curl -s http://<host>:8080/api/v1/personas -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

3.4 GET /api/v1/sessions/{id}/messages

  • 鉴权:Authorize(owner, admin)(会话消息/thinking 属主人私密历史,viewer 不可读)。
  • Path:{id} = 会话 id(web 或 web:<uuid>;IM 隔离会话 sess-xxx 亦可用)。
  • Query:
    参数 类型 必填 默认 说明
    limit int 否 50 页大小,需 >=1,钳制上限 200(maxPageLimit);非法回 400
    before int64 否 0 游标:上一页最旧消息的 createdAt(Unix 毫秒),向更旧翻页
    after int64 否 0 正向增量(A5):返回 created_at > after,升序;与 before 互斥(同传 400);负数/非数字回 400
    q string 否 - 会话内搜索(D2):content LIKE 大小写不敏感,%/_/\ 转义为字面量;仍倒序 + before/limit 游标;与 after 互斥(同传 400);空 q 走普通分页;严格限定单会话(隔离会话不外泄)
    platform string 否 - 来源平台过滤(design/16 §4.4)。Web 端不传 = 全量共享主会话;IM 传本平台(如 telegram)只拉本渠道消息
  • 响应:200 {"data":[<historyMessage>...]};before 时按时间倒序(newest first,需 reverse() 渲染);after 时按时间升序(最旧在前,可直接顺序渲染/追加)。

historyMessage 全字段:

字段 类型 说明
id string 后端生成的消息 id(客户端据此去重)
sessionId string 会话 id
role string user / assistant
content string 正文(纯文本/图像消息文本;图像为 image 字段)
mode string 该条的模式(omitempty,供渲染气泡不用猜)
mood string 情绪标记(omitempty)
image string 图像引用(omitempty)
thinking string 思维链(omitempty;本端点 owner/admin-only,故可达;公开回放/持久化广播中已剥离)
platform string 来源平台(web / telegram / weixin-ilink / qq-obv11...,omitempty)
deviceId string 客户端端标识(omitempty,如 web:chrome:a1b2c3)
senderName string 第三方发送者昵称(omitempty;群聊/陌生人消息带,主人私聊为空)
adapterId string 产生该消息的适配器注册名(omitempty,如 kook:3bef...;Web 消息为空)
createdAt int64 Unix 毫秒(兼作分页游标)
{"data":[{"id":"m1","sessionId":"web","role":"assistant","content":"你好","mode":"normal","mood":"calm","platform":"web","deviceId":"web:chrome:a1b2c3","createdAt":1753...}]}
  • 错误:400(limit/before/after 非法、before+after 同传、id 缺失);501 {"error":{"code":"not_implemented","message":"会话历史服务未配置(未注入 HistoryStore)"}};500 {"error":{"code":"messages_failed",...}}。
  • 备注:热分页(design/16 §2.2,§5.1)。thinking 只在本 owner/admin 端点可见;WS 回放/广播已剥离。curl:
curl -s 'http://<host>:8080/api/v1/sessions/web/messages?limit=50&before=1753000000000&platform=web' \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

3.5 POST /api/v1/sessions

  • 鉴权:Authorize(owner, admin)(写操作,变更持久态)。
  • 请求 Body(可选):{"personaId"?}——body 可完全省略或为空。personaId 非空时把新会话绑定到该角色(未知角色回 400 bad_request);省略 / 空 = 不绑(persona_id='',继承全局默认角色)。会话角色只在创建时写入、之后不可变(design/22 §8-1;PATCH /sessions/{id} 一旦收到 personaId 键即 400)。ownerID 从 JWT claims 取作子会话归属。契约见 api-client §1 行 5。
  • 响应:201 {"data":{"sessionId":"web:<uuid>"}};personaId 非空时回显该字段(省略时响应形状与旧版一致)。
  • 另:若注入的 HistoryStore 不支持角色绑定且显式传了 personaId,回 501 not_implemented(不假装写入)。
  • 错误:501 {"error":{"code":"not_implemented","message":"会话历史服务未配置(未注入 HistoryStore)"}};500 {"error":{"code":"session_failed",...}}。
  • 备注:新建次级会话(NewSubSession),独立上下文回溯。curl:
curl -s -X POST http://<host>:8080/api/v1/sessions \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1' -d '{}'
# => {"data":{"sessionId":"web:0123abcd..."}}

3.6 DELETE /api/v1/sessions/{id}

  • 鉴权:Authorize(owner, admin)。
  • Path:{id} = 会话 id。
  • 响应:200 {"data":{"deleted":"<id>"}}。
  • 错误:
    • 403 {"error":{"code":"forbidden","message":"the main session cannot be deleted"}}(history.ErrImmutableMain,主会话 web 不可删)。
    • 400(id 缺失);501(无 HistoryStore);500 {"error":{"code":"session_failed",...}}。
  • 备注:删除次级会话及其全部消息。curl:
curl -s -X DELETE http://<host>:8080/api/v1/sessions/web:0123abcd... \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'
# => {"data":{"deleted":"web:0123abcd..."}}

3.7 GET /api/v1/meta/enums

  • 鉴权:Authorize()(任意已认证角色:owner/admin/viewer)——只读、无敏感信息,与 GET /api/v1/sessions(§3.2)同级。无 token 回 401。
  • 用途:客户端共享枚举权威表(Android 协同 B4,design/07 §6)。platform(消息来源平台)/ mood(情绪)/ mode(行为·呈现模式)此前只在文档里给示例,客户端各自硬编码映射导致来源角标与情绪标签漂移;本端点把三张表做成机器可读的单一事实来源,Web / Android / IM bridge 共用。
  • 响应:200
    {"data":{
      "platforms":[{"id":"web","label":"网页"},{"id":"kook","label":"KOOK"},{"id":"qq-obv11","label":"QQ"},{"id":"weixin-ilink","label":"微信"},{"id":"telegram","label":"Telegram"},{"id":"webhook","label":"Webhook"}],
      "moods":[{"id":"calm","label":"平静"},{"id":"happy","label":"开心"},{"id":"excited","label":"兴奋"},{"id":"sad","label":"低落"},{"id":"angry","label":"生气"}],
      "modes":[{"id":"chat","label":"对话"},{"id":"code","label":"代码"},{"id":"home","label":"家居"},{"id":"work","label":"工作"},{"id":"docs","label":"文档"},{"id":"voice","label":"语音"},{"id":"vision","label":"视觉"},{"id":"agents","label":"智能体"},{"id":"settings","label":"设置"}],
      "unknown":"display-raw"
    }}
    
  • 字段:
    字段 类型 说明
    platforms []{id,label} 来源平台:各内置适配器 Name()(kook/qq-obv11/weixin-ilink/telegram/webhook)+ Web 主会话来源 web(history.PlatformWeb)
    moods []{id,label} 情绪:internal/persona/emotion 内置状态。状态机可扩展(persona 可经 AddTransition 注册更多状态),此表非封闭集合
    modes []{id,label} 九模式(design/07 §1);mode 只驱动呈现,不构成能力边界
    unknown string 恒 "display-raw":未知值回落约定(机器可读)
    label string 默认中文标签,客户端可直接显示、也可按 i18n 覆盖;客户端比对应以 id 为准(小写、大小写敏感)
  • 错误:仅 401(未认证)。无存储依赖,不会 501/500。
  • 备注:
    • 未知值回落约定(红线):客户端遇不在表中的取值原样显示原始字符串——不丢弃、不报错、不替换占位符;服务端新增取值是非破坏性变更(forward-compatible),旧客户端无需升级,也不得因未知值清空角标或抛错。
    • 单一事实来源在 Go 侧:internal/contract/enums(Platforms()/Moods()/Modes()/UnknownFallback),网关只做投影(internal/services/gateway/meta.go)。
    • 反漂移:internal/contract/enums 的测试断言三张表与「适配器 Name()」「emotion 内置状态」「前端 types.ts 的 MODES」一致;cmd/cyrene 另断言表内每个平台都能被渠道工厂构建——漏登记或幽灵条目即 FAIL。
    • Web 侧镜像:frontend/web/src/lib/enums.ts(labelFor(kind, value) 未知值返回原值),顶栏 / 侧栏 / 消息气泡的心情标签已改用。
    • curl:
      curl -s http://<host>:8080/api/v1/meta/enums -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'
      # => {"data":{"platforms":[...],"moods":[...],"modes":[...],"unknown":"display-raw"}}
      

4. 管理后台 · 读端点(owner|admin)

以下端点 鉴权 = Authorize(owner, admin)(除特别标注 owner)。所有管理读不审计;管理写(§5)全程审计(who/what/when/result,见 §4.15)。

4.1 GET /api/v1/admin/personas

  • 同 GET /api/v1/personas(§3.3),返回 PersonaSummary[]。adminListPersonas 直接复用 collectPersonas()。
  • 200 {"data":[<PersonaSummary>...]}。curl:
curl -s http://<host>:8080/api/v1/admin/personas -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.2 GET /api/v1/admin/sessions

  • 同 GET /api/v1/sessions(§3.2):注入 HistoryStore 返回持久化会话(主 + 子);否则回退渠道会话视图(collectSessions)。
  • 200 {"data":[...]}。curl:
curl -s http://<host>:8080/api/v1/admin/sessions -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.3 GET /api/v1/admin/memories

  • 响应:200 {"data":[<MemorySummary>...]}(无 store 时 [])。MemorySummary = {id, scope, text}。scope ∈ user|persona|project。curl:
curl -s http://<host>:8080/api/v1/admin/memories -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.4 GET /api/v1/admin/plugins

  • 响应:200 {"data":[<PluginSummary>...]}(PluginSummary = {name, kind, description, allowInGroup, allowInStranger})。kind 为能力类型,实际取值 = tool|skill|channel|mcp|iot(internal/capability/registry 的 KindTool/KindSkill/KindChannel/KindMCP/KindIoT);allowInGroup/allowInStranger 为可达性策略(落盘 tool_policies,重启保留——迁移 0012_tool_policy;SetPolicy 先落库后改内存,落盘失败内存保持旧值并返回错误,无行的能力按默认 owner-only)。curl:
curl -s http://<host>:8080/api/v1/admin/plugins -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.5 GET /api/v1/admin/channels

  • 响应:200 {"data":[<ChannelSummary>...]}。

ChannelSummary = {name, connected, accounts?}。accounts 为 AdminChannelAccount[](掩码无 raw token),仅当 s.channels 注入且平台有已配账号时出现。

  • name:平台名(如 kook、telegram、weixin-ilink)。

  • connected:bool,该平台是否已连接。

  • accounts[](AdminChannelAccount)字段:id / config(掩码后的配置 map)/ allowGroups / denyGroups / allowUsers / denyUsers / groupMsgPerMin / msgPerMin / maxMessages / botId? / botUsername? / botNickname? / capabilities?(如 kook 的 ["friends","user_chats","channels","guilds","message_test"])。

    • botId/botUsername/botNickname 为已连接 bot 的真实身份(如 KOOK Connect 时调 /api/v3/user/me 解析),非敏感来自 live adapter。
    • config 值已脱敏(token 掩码 **** + 尾 4);bot_token 等密钥绝不回显。
  • 聚合逻辑:channels.json 是账号存在性的真源;一个裸平台 + 多账号合并为一行;无注册 adapter 但已配账号的平台仍出现(供查看配置)。curl:

curl -s http://<host>:8080/api/v1/admin/channels -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.6 GET /api/v1/admin/channels/messages

  • 鉴权:owner|admin。
  • Query:limit(默认50,上限200) / before(游标,Unix 毫秒) / adapter(适配器注册名,如 kook:3bef...) / platform。三粒度(互斥,adapter 优先,其次 platform,都不传=全量 IM 总览)。
  • 响应:200 {"data":[<historyMessage>...]}(newest first)。本视图不过滤主会话(因为它带 adapter_id);按 adapter_id/platform 过滤。historyMessage 见 §3.4。
  • 错误:501(无 HistoryStore)。
  • 备注:IM 适配器日志页全量数据源 + 轮询兜底;实时增量走 WS channel_persisted(§6.3)。curl:
curl -s 'http://<host>:8080/api/v1/admin/channels/messages?adapter=kook:3bef...&limit=50' \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.7 GET /api/v1/admin/channels/{platform}/messages

  • 鉴权:owner|admin。
  • Path:{platform} = IM 平台(如 telegram)。
  • Query:limit(默认50,上限200) / before(Unix 毫秒)。
  • 响应:200 {"data":[<historyMessage>...]}。排除主会话 web,只返回该平台非主人消息流(隔离 sess-xxx)。senderName 标发送者昵称;adapterId 标来源适配器。
  • 错误:400(platform 缺失);501(无 HistoryStore);500。
  • curl:
curl -s 'http://<host>:8080/api/v1/admin/channels/telegram/messages?limit=50' \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.8 GET /api/v1/admin/iot

  • 响应:200 {"data":[<iotDeviceSummary>...]}(iotDeviceSummary = {id, name, type, state, online};state 为 map[string]any)。无 store 时 []。
  • 错误:500 {"error":{"code":"iot_failed",...}}。curl:
curl -s http://<host>:8080/api/v1/admin/iot -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.9 GET /api/v1/admin/llm

  • 响应:200 {"data":{"providers":[<AdminLlmProvider>...]}}(AdminLlmConfig)。
  • AdminLlmProvider:{id, name, provider, baseUrl, model, multimodal, enabled, fallback?, maxTokens?, reasoning, personaModels?, personaProviders?}(v0.53 新增三键,见 api-client 的「模型配置契约(v0.53)」);顶层 response 另含 reasoningKinds / personaProviderOptions 两个取值域数组(面板表单选项的唯一来源)。multimodal = 主模型自己能否收图(裁决 14.4.1)——它不表示「本部署有视觉能力」;视觉/OCR 工具的模型由 .env 的 LLM_VISION_MODEL / LLM_OCR_MODEL 单独配置,与 multimodal 无关。reasoning 恒下发({kind},kind:"" = 未声明形状)。
    • provider 协议 ∈ openai|anthropic;name 为友好标签(OpenAI/Anthropic 或组 id)。
    • enabled:index 0 的组为生效组 = true。
    • maxTokens:该组的 token 预算(omitempty,缺省/0 = 未设);缺省时链末端回退步回落 LLM_FALLBACK_MAX_TOKENS(.env,默认 8192),再回落适配器默认(anthropic 8192)。
    • reasoning(v0.53,恒下发):{kind} —— kind ∈ auto|deepseek|openai|vllm|dashscope,决定该组用哪种提供商开关形状下发推理指令(""/auto = 按模型名/baseURL 推断,形状有歧义就不下发);详见 api-client「模型配置契约(v0.53)」。
    • personaModels / personaProviders(v0.53,omitempty):角色级模型 / 协议路由表({personaId: modelId} / {personaId: "openai"|"anthropic"});未配置 = 该键不出现。
    • multimodal:主模型自己能否收图(= 面板的「多模态」勾选);视觉/OCR 工具的模型是 .env 的 LLM_VISION_MODEL/LLM_OCR_MODEL,与本字段无关(裁决 14.4.1 修语义)。
    • API key 永不返回(wire 无 key 字段)。
  • 数据源:models.json 各组(index 0 = 生效组);无 models.json 时按 .env 合成单组。curl:
curl -s http://<host>:8080/api/v1/admin/llm -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.10 GET /api/v1/admin/automations

  • 响应:200 {"data":[<AdminAutomationRecord>...]}。
  • AdminAutomationRecord:{id, name, kind, cron?, enabled, lastRun?, nextRun?}。kind ∈ cron|event;lastRun/nextRun 为 RFC3339(未知省略)。curl:
curl -s http://<host>:8080/api/v1/admin/automations -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.11 GET /api/v1/admin/users

  • 响应:200 {"data":[<AdminUserRecord>...]}。
  • AdminUserRecord:{id, username, role, createdAt?}(createdAt RFC3339)。无 UserStore 时仅有配置的 admin 账号(id:"admin"、role:"owner");注入 UserStore 后追加 API key 摘要(username:"apikey"、id 为掩码)。curl:
curl -s http://<host>:8080/api/v1/admin/users -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.12 GET /api/v1/admin/backthink

  • 响应:200 {"data":[<AdminBackthinkItem>...]}(无 store 时 [])。AdminBackthinkItem:{id, trigger, conclusion?, shouldNotify, pushed, at}(at 为 Unix 毫秒)。默认取最近 50 条。curl:
curl -s http://<host>:8080/api/v1/admin/backthink -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.13 GET /api/v1/admin/rag

  • Query:?query=<string>(可选;带则并入检索命中)。
  • 响应:200 {"data":{"documents":[<AdminRagDoc>...], "hits"?:[<AdminRagHit>...]}}(AdminRagResponse)。
    • AdminRagDoc:{id, name, desc?, chunks, embedded}(embedded 反映是否配置向量增强)。
    • AdminRagHit:{docId, docName, chapter?, text, score, semantic}(semantic 布尔,语义/向量命中为 true)。
  • 错误:500。无 store 时返回空文档列表(不 500)。curl:
curl -s 'http://<host>:8080/api/v1/admin/rag?query=部署' -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.14 POST /api/v1/admin/rag/query

  • 鉴权:owner|admin。
  • Body:{"query":"<string>"}(必填,非空)。
  • 响应:200 {"data":{"query":"<string>","hits":[<AdminRagHit>...]}}。审计:rag.query(成功 ok / 失败 error)。
  • 错误:400(query 缺失/空);501(未注入 RagStore);500。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/rag/query \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"query":"部署要求是什么"}'

4.15 GET /api/v1/admin/observability/audit

  • Query:?limit=N(默认 50,上限 500)。
  • 响应:200 {"data":[<AuditRecord>...]}(newest first)。AuditRecord = {id(int64递增), who, what, when(RFC3339), result}。who 形如 admin:owner;what 为审计动作(如 system.backup);result ∈ ok|error。
  • 来源:SQLite audit_logs(SQLiteAuditStore);未注入时退化为进程内存。
  • 示例:{"data":[{"id":1,"who":"admin:owner","what":"system.backup","when":"2026-09-08T00:00:00Z","result":"ok"}]}。curl:
curl -s 'http://<host>:8080/api/v1/admin/observability/audit?limit=100' -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.16 GET /api/v1/admin/observability/metrics

  • Query:无(不接收 limit)。
  • 响应:200 {"data":<AdminMetrics>}。AdminMetrics = {totalRequests, qps, avgLatencyMs, maxLatencyMs, perMethod{}, perPath{}, statusCounts{}, llmCalls, subagentEvents}。
    • qps:近 1 秒窗口计数;avgLatencyMs/maxLatencyMs:均值/最大耗时(毫秒)。
    • perMethod/perPath/statusCounts:分布计数。
    • llmCalls/subagentEvents:由实际观测的 trace span 按 kind 累计(Tracer 未注入为 0)。
    • Token/成本后端不统计、不返回(不伪造指标)。curl:
curl -s http://<host>:8080/api/v1/admin/observability/metrics -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.17 GET /api/v1/admin/observability/trace

  • Query:?limit=N(默认 50,上限 500)。
  • 响应:200 {"data":[<AdminTraceSpan>...]}(newest first)。AdminTraceSpan = {id, parentId?, name, kind, status, startedAt, endedAt?, durationNs, props?, events?}。kind ∈ llm_call|tool_call|orchestration|state_transition|subagent|memory|guardrail;events 为 [{time,type,props?}];时间 RFC3339。
  • 旁路节点的 span 现已真实产生(裁决 11.4.2 / 11.4.3,2026-10-04):memory.recall / memory.write / guardrail.input / guardrail.output / orchestration.spawn 均由真实生产者开出,因此 kind 取值集里的 memory|guardrail|orchestration 不再只是枚举——trace 列表中会真的出现这三类 span(此前三者无生产者,全仓只有 llm_call/tool_call/subagent/state_transition)。它们只带元数据(记忆作用域/条数/字节数、护栏裁决与耗时、被派发的子代理 id 与目标角色 id),不含记忆正文、不含提示词、不含密钥;见 design/19 §1。
  • 来源:环形缓冲(默认 512 条),由 ObserveSpan/TraceSink 注入。curl:
curl -s 'http://<host>:8080/api/v1/admin/observability/trace?limit=100' -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

4.18 GET /api/v1/admin/sessions/{id}/export

  • 鉴权:owner|admin(注册为 adminAuth)。
  • Path:{id} = 会话 id。
  • 响应:200 {"data":{"session":<SessionSummary>}}(summaryOfSession:{platform, channelId, sessionId, actorRole, owner, isGroup, memWritable})。
  • 错误:404(会话未找到);500。
  • 备注:导出会话身份元数据;完整消息转录由 M10 提供。curl:
curl -s http://<host>:8080/api/v1/admin/sessions/web:0123.../export -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

5. 管理后台 · 写端点(owner,全程审计)

以下端点鉴权 = Authorize(owner)(仅主人)。每个写操作都会调用 auditClaims(r, "<action>", "ok"|"error") 记录 who/what/when/result(落 audit_logs + 网关日志)。敏感值(token/密钥/新 JWT 密钥/DB 路径)不在响应/日志回显。

5.1 POST /api/v1/admin/plugins/policy

  • Body(policyRequest):{"name":"<能力名>","allowInGroup":bool,"allowInStranger":bool}。
  • 响应:200 {"data":{"name":"<能力名>","allowInGroup":bool,"allowInStranger":bool}}。
  • 错误:400(name 缺失);500(cap service 未配置);404(能力不存在);500。
  • 审计:policy.set。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/plugins/policy \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"name":"web_search","allowInGroup":true,"allowInStranger":false}'

5.2 POST /api/v1/admin/personas

  • Body(personaUpsertRequest):{"id","name","characterRef?","role","systemPrompt?","personality?","speechStyle?","catchphrase?","background?","values?[]","modes?[]"/"mode?","memoryNamespace?"}。id/name/role 必填;systemPrompt 空时派生默认;characterRef 空时 = id。
  • 响应:201 {"data":<PersonaSummary>}。注册进 persona registry(真实运行效果)+ 持久化到 configs/personas/<id>.yaml(重启存活)。
  • 错误:400(id/name/role 缺失);409 {"error":{"code":"duplicate","message":"人格已存在,请使用编辑"}};500(registry 未配置 / 持久化失败 / 注册失败)。
  • 审计:persona.create。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/personas \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"id":"luna","name":"Luna","role":"女仆","modes":["normal"],"memoryNamespace":"luna"}'
# => {"data":{"id":"luna","name":"Luna","character":"Luna","isDefault":false,"mode":"normal","memoryNamespace":"luna"}}

5.3 PUT /api/v1/admin/personas/{id}

  • Path:{id} = 人格 id。Body 同 5.2(不含 id;服务端以 path 覆盖 req.ID)。name/role 必填。
  • 响应:200 {"data":<PersonaSummary>}。
  • 错误:400(name/role 缺失);404(不存在);500(registry 未配置 / 持久化 / 更新失败)。
  • 审计:persona.update。curl:
curl -s -X PUT http://<host>:8080/api/v1/admin/personas/luna \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"name":"Luna","role":"女仆","modes":["gentle"]}'

5.4 POST /api/v1/admin/users

  • 响应:501 {"error":{"code":"not_implemented","message":"新增用户/角色存储为 M10 提供(当前为单 admin + APIKey 现状)"}}。审计:user.create(error)。
  • 备注:诚实不伪造(无持久用户表)。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/users \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' -d '{}'

5.5 POST /api/v1/admin/users/apikey

  • Body(apiKeyIssueRequest):{"role":"owner|admin|viewer"}。
  • 响应:201 {"data":{"id":"<掩码>","key":"<明文·仅此一次>","role":"<role>"}}。key 为 base64 raw URL 编码的 32 字节随机串,仅此响应可见一次;id 为掩码(xxxx....xxxx),用于后续吊销。
  • 错误:400(invalid role/role 缺失);500(user service 未配置 / 生成失败)。
  • 审计:apikey.issue。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/users/apikey \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"role":"admin"}'
# => {"data":{"id":"abcd****wxyz","key":"LQV...","role":"admin"}}

5.6 DELETE /api/v1/admin/users/apikey/{id}

  • Path:{id} = 列表返回的掩码 id。
  • 响应:200 {"data":{"id":"<掩码>","revoked":true}}。
  • 错误:404(掩码未命中);500(user service 未配置)。
  • 审计:apikey.revoke。curl:
curl -s -X DELETE http://<host>:8080/api/v1/admin/users/apikey/abcd****wxyz \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

5.7 POST /api/v1/admin/iot/{id}/control

  • Path:{id} = 设备 id。Body(iotControlRequest):{"action":"<string>","params":{...}?}。
  • 响应:200 {"data":{<iotDeviceSummary>}}({id,name,type,state,online})——真实下发到 IoT provider,并发布 device_update 事件。
  • 错误:400(action 缺失 / 动作不支持 / 参数错);404(设备不存在);409(设备离线);500(IoT 服务未配置 / 控制失败)。
  • 审计:iot.control。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/iot/d1/control \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"action":"toggle","params":{"brightness":50}}'

5.8 POST /api/v1/admin/iot/scenarios

  • 响应:501 {"error":{"code":"not_implemented","message":"IoT 场景未实现:IoT 子系统仅有设备列表/控制/状态(Provider 接口无场景模型与存储);场景 CRUD 需先新增模型 + 执行器"}}。审计:iot.scenario(error)。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/iot/scenarios -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}'

5.9 POST /api/v1/admin/iot/rules

  • 响应:501 {"error":{"code":"not_implemented","message":"IoT 自动化规则未实现:IoT 子系统无规则模型与存储(通用 automations 服务只跑 cron/事件规则,不含设备状态条件 + 设备动作执行器)"}}(同 5.8,审计 iot.rule)。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/iot/rules -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}'

5.10 POST /api/v1/admin/automations

  • Body(automationCreateRequest):{"name","kind":"cron|event","expression"}(均必填)。
  • 响应:201 {"data":{"id":"<id>","name":"<name>","kind":"<kind>"}}——真实注册规则。
  • 错误:400(字段缺失 / invalid_cron / invalid_rule);500(自动化服务未配置 / 添加失败)。
  • 审计:automation.create。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/automations \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"name":"晨间问候","kind":"cron","expression":"0 8 * * *"}'

5.11 POST /api/v1/admin/automations/{id}/run

  • Path:{id}。Body:无。
  • 响应:200 {"data":{"id":"<id>","output":"<string>"}}(手动触发一次)。
  • 错误:404;500。
  • 审计:automation.run。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/automations/<id>/run -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

5.12 DELETE /api/v1/admin/automations/{id}

  • Path:{id}。响应:200 {"data":{"id":"<id>","deleted":true}}。
  • 错误:404;500。审计:automation.delete。curl:
curl -s -X DELETE http://<host>:8080/api/v1/admin/automations/<id> -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

5.13 POST /api/v1/admin/llm

  • Body(llmProviderRequest):{"id?","name?","provider?","baseUrl","apiKey?","model","multimodal?","fallback?","maxTokens?","reasoning?","personaModels?","personaProviders?"}。baseUrl+model 必填;provider 空默认 openai(仅允许 openai|anthropic);maxTokens 缺省/0 = 未设,负数回 400。v0.53 三键缺省 = 保留已存值(reasoning 整块给出即整块替换;两张 persona 表给出即整表替换);非法 reasoning.kind、非法协议名、anthropic 组声明 openai 系形状 → 400 bad_request(Group.Validate 报错,不落盘)。契约见 api-client「模型配置契约(v0.53)」。
  • 响应:201 {"data":{"provider":<AdminLlmProvider>,"persisted":true,"path":"<models.json 绝对路径>"}}。真实持久化:读-改-写 models.json,保存组提升为生效组(index 0),并热重载运行中的 LLM stack(无需重启)。
  • 错误:400(缺 baseUrl/model / invalid provider);500(LLM 服务未注入 / 写盘失败 / 热重载失败)。API key 写入文件但永不回显、不落日志。
  • 审计:llm.addProvider。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/llm \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"provider":"openai","baseUrl":"https://api.openai.com/v1","apiKey":"sk-...","model":"gpt-4o","multimodal":true}'

5.14 POST /api/v1/admin/llm/test

  • Body(llmTestRequest):{"provider?","baseUrl","model","apiKey?","prompt?"}。model+baseUrl 必填;provider 空默认 openai;prompt 空用探针 "Reply with the single word: OK"。
  • 响应:200 {"data":{"ok":true,"provider":"openai","model":"gpt-4o","reply":"<模型回复>","tokens":<int>}}——真实模型调用(超时 30s)。
  • 错误:400(缺 model/baseUrl / invalid provider);500(调用失败)。
  • 审计:llm.test。API key 仅本次请求内使用,不落库、不进 wire。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/llm/test \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"provider":"openai","baseUrl":"https://api.openai.com/v1","model":"gpt-4o","apiKey":"sk-...","prompt":"你好"}'

5.15 POST /api/v1/admin/channels/open

  • Body(channelActionRequest):{"name":"<渠道名>"}(如 "kook" / "kook:conf0" / 裸平台名解析到该平台首个账号)。
  • 响应:200 {"data":{"name":"<name>","connected":true}}——真实 Connect。
  • 错误:400(name 缺失);404(渠道未注册);409 {"error":{"code":"needs_login","message":"该渠道尚未登录:请先用「扫码登录」绑定,再连接"}}(weixin-iLink 无 token);500(连接失败)。
  • 审计:channel.open。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/channels/open \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"name":"kook"}'

5.16 POST /api/v1/admin/channels/close

  • Body:{"name":"<渠道名>"}。响应:200 {"data":{"name":"<name>","connected":false}}——真实 Disconnect。
  • 错误:400 / 404 / 500。审计:channel.close。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/channels/close \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' -d '{"name":"kook"}'

5.17 POST /api/v1/admin/channels/test

  • Body(channelTestRequest):{"platform?","accountId","isGroup"}(accountId 必填)。
  • 响应:200 {"data":{"role":"owner|member|...","owner":bool,"isGroup":bool,"env":"owner|...|","verdict":"<人读结论>"}}。结构性身份识别:channel 层 actor 元数据 × configs/users/owner.yaml 白名单,绝不靠提示词。verdict 说明:群聊→隔离+记忆只读+仅允许工具;命中白名单→owner(全量访问);未命中→按陌生人(default deny)。
  • 错误:400(accountId 缺失)。审计:channel.identity-test。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/channels/test \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"platform":"weixin-ilink","accountId":"wx_uid_123","isGroup":false}'
# => {"data":{"role":"owner","owner":true,"isGroup":false,"env":"owner","verdict":"匹配主人白名单 → owner(全量访问,记忆可读写)"}}

5.18 GET /api/v1/admin/channels/{platform}/login-qr

  • 鉴权:owner。Path:{platform} = weixin-ilink(其它平台 501)。Query:?accountId=(可选)。
  • 响应:200 {"data":{"qrUrl?","qrCode?","status":"confirmed|<状态>"}}。qrUrl 为微信二维码图 URL(前端据此渲染可扫 QR),qrCode 为原始扫码标识;已登录时 status:"confirmed"。
  • 错误:501(非 weixin-ilink);404(账号未找到,提示先保存账号);500(取码失败)。
  • 审计:channel.weixin.login.qr。curl:
curl -s 'http://<host>:8080/api/v1/admin/channels/weixin-ilink/login-qr?accountId=conf0' \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

5.19 GET /api/v1/admin/channels/{platform}/login-status

  • 鉴权:owner。Path:{platform} = weixin-ilink。Query:?accountId=(可选)。
  • 响应:200 {"data":{"status":"confirmed|failed|...","botId?","botToken?","err?"}}。botToken 为掩码(****+尾4);status==confirmed 时后端已把刚取到的 bot_token/bot_id/owner_id 写入 channels.json 并热加载该平台(bridge 连上已配置的 adapter,无需重启),且(owner_id 存在时)把扫码者绑定 id 登记进 owner 白名单(owner.yaml,热生效)。status==failed 时 err 带原因。
  • 错误:501 / 404 / 500。
  • 审计:channel.weixin.login.status(+ channel.weixin.login.persist(确认时))。curl:
curl -s 'http://<host>:8080/api/v1/admin/channels/weixin-ilink/login-status?accountId=conf0' \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'
# 确认后 => {"data":{"status":"confirmed","botId":"wxid_xx","botToken":"****abcd"}}

5.20 GET /api/v1/admin/channels/{platform}/view

  • 鉴权:owner。Path:{platform} = kook(其它 501)。Query:?accountId=(可选)。
  • 响应:200 {"data":{"platform":"kook","accountId?","friends?","userChats?","guilds?","channels?","members?","errors?"}}。聚合该账号真实数据(好友/申请/屏蔽、私信会话、服务器 + 首个服务器频道与成员)。errors 为 {子项:原因} 记录失败子项(不伪造)。token/密钥不打印不落日志。
  • 错误:501(非 kook);404(账号未找到);500。
  • 审计:channel.platform.view。curl:
curl -s 'http://<host>:8080/api/v1/admin/channels/kook/view?accountId=3bef...' \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

5.21 GET /api/v1/admin/channels/{platform}/friends

  • 鉴权:owner。Path:kook。Query:?type=friend|request|block&accountId=(type 空=全部)。
  • 响应:200 {"data":<KOOK FriendList>}(含 friends/requests/blocked)。
  • 错误:501 / 404 / 500。审计:channel.friends。curl:
curl -s 'http://<host>:8080/api/v1/admin/channels/kook/friends?type=request&accountId=3bef...' \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

5.22 POST /api/v1/admin/channels/{platform}/friend/handle

  • 鉴权:owner。Path:kook。Query:?accountId=。Body(kookFriendHandleRequest):{"id":<int64>,"accept":bool}(id 必填)。
  • 响应:200 {"data":{"handled":true,"id":<int64>,"accept":bool}}。
  • 错误:400(id 缺失);501 / 404 / 500。审计:channel.friend.handle。curl:
curl -s -X POST 'http://<host>:8080/api/v1/admin/channels/kook/friend/handle?accountId=3bef...' \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"id":12345,"accept":true}'

5.23 POST /api/v1/admin/channels/{platform}/message/test

  • 鉴权:owner。Path:kook。Query:?accountId=。Body(kookMessageTestRequest):{"targetId":"<频道id>","content":"<文本>"}(均必填)。
  • 响应:200 {"data":{<KOOK TestMessageResult>}}(如 {msg_id,msg_timestamp})——真实向目标发一条 kmarkdown 测试消息。
  • 错误:400(字段缺失);501 / 404 / 500。审计:channel.message.test。curl:
curl -s -X POST 'http://<host>:8080/api/v1/admin/channels/kook/message/test?accountId=3bef...' \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"targetId":"123456","content":"ping"}'

5.24 PUT /api/v1/admin/channels/{platform}/config

  • 鉴权:owner。Path:{platform}。Body(channelConfigRequest):
    字段 类型 说明
    id string 账号 id(可选;空则由服务端生成)
    token string 快捷 token,并入 config["bot_token"](若 config 未显式给)
    baseURL string → config["base_url"]
    remoteURL string → config["remote_url"]
    listenAddr string → config["listen_addr"]
    mode string → config["mode"]
    accessToken string → config["access_token"]
    selfID string → config["self_id"]
    config object 平台专属键值(如 bot_token/base_url/remote_url/endpointPath/receive_mode/webhook_verify_token...)
    allowGroups/denyGroups string[] 黑白名单(群)
    allowUsers/denyUsers string[] 黑白名单(用户)
    groupMsgPerMin int 群消息限流
    msgPerMin int 用户消息限流
    maxMessages int 单账号最大消息数

    空配置时:weixin-ilink 豁免(先建账号再扫码),其它平台 400。

  • 响应:201 {"data":{"persisted":true,"path":"<channels.json 路径>","platform":"<platform>","accountId":"<accountId>"}}。真实持久化 + 热更新(读 channels.json→UpsertChannel→落盘→Reload(platform) 热替换 adapter,无需重启)。token 写入文件但不回显(GET 列表掩码)。
  • 错误:400(platform 缺失 / 空配置);500(渠道配置服务未注入 / 写盘失败 / 热重载失败)。
  • 审计:channel.save。curl:
curl -s -X PUT http://<host>:8080/api/v1/admin/channels/kook/config \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"id":"conf0","token":"<bot_token>","allowUsers":["user_i_like"],"groupMsgPerMin":20}'

5.25 DELETE /api/v1/admin/channels/{platform}/config

  • 鉴权:owner。Path:{platform}。Query:?id=<accountId>(必填)。
  • 响应:200 {"data":{"persisted":true,"path":"<channels.json>","platform":"","accountId":"<id>"}}。删除账号 → 落盘 → Reload(platform) 热更新(对应 adapter 优雅关停)。
  • 错误:400(platform/id 缺失);500(未注入 / 写盘 / 重载失败)。
  • 审计:channel.delete。curl:
curl -s -X DELETE 'http://<host>:8080/api/v1/admin/channels/kook/config?id=conf0' \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1'

5.26 POST /api/v1/admin/plugins/install

  • Body(pluginInstallRequest):{"kind":"mcp","name":"<名>","config":{"transport":"stdio|http|sse","endpoint"?|"command"?, "args"?, "apiKey"?, "env"?}}。name 必填;kind 缺省 "mcp",仅 mcp 可装(其它回 400)。endpoint 的别名是 url。
  • 响应:200 {"data":{"installed":true,"name":"<名>","kind":"mcp","transport":"<t>","toolCount":<n>}}——真实安装:connect + 发现工具 + 热注册进 capability registry(下一轮模型请求即注入,无需重启)→ 落盘 plugins.json(gitignore、0600)→ 持有 live server 供后续刷新/卸载。同名重复安装 = 刷新(先卸旧再装新,失败回滚旧)。
  • 错误:400(缺 name / kind 非 mcp / transport 非法 / stdio 缺 command / http|sse 缺 endpoint);502 plugin_install_failed(连接/发现/注册失败,且不落盘,绝不装一半);501 not_implemented(未注入 plugin manager)。
  • 审计:plugin.install。契约见 api-client §2 的 POST /admin/plugins/install 条(本节为索引,不重复维护契约)。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/plugins/install \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"kind":"mcp","name":"fs","config":{"transport":"stdio","command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"]}}'

5.27 POST /api/v1/admin/plugins/uninstall

  • Body(pluginActionRequest):{"name":"<能力名>"}(必填)。
  • 响应:已装 MCP → 200 {"data":{"name":"<name>","uninstalled":true,"kind":"mcp"}}(Unregister 移除其工具 + close + 删 plugins.json 条目);非 MCP / 普通能力 → 200 {"data":{"name":"<name>","uninstalled":true}}(走 registry.Remove)。
  • 错误:400(name 缺失);404(不存在);500。
  • 审计:plugin.uninstall。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/plugins/uninstall \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' -d '{"name":"web_search"}'

5.28 POST /api/v1/admin/memories

  • Body(memoryAddRequest):{"personaId?","content","scope?"}。content 必填;personaId 缺省 "cyrene";scope ∈ user|persona|project(缺省 persona;无效非空回 400)。
  • 响应:201 {"data":{"id":"<id>","scope":"<scope>","text":"<内容>"}}——真实写入 memory store。
  • 错误:400(content 缺失 / scope 非法);501(memory store 未配置 / 未注入 memory.Store);500。
  • 审计:memory.add。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/memories \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"personaId":"cyrene","content":"主人喜欢喝美式咖啡","scope":"user"}'

5.29 POST /api/v1/admin/memories/search

  • Body(memorySearchRequest):{"query","personaId?","topK?"}。query 必填;personaId 省略时回落到库里注册的默认角色(defaultPersonaID(),不再写死 "cyrene"),既未传又无默认角色 → 400 bad_request(诚实报错,不拿不可能命中的 id 去查);topK ≤0 取 10。
  • 响应:200 {"data":{"query":"<query>","personaId":"<personaId>","semantic":false,"hits":[{"id","scope","text","score"}]}}。真实检索(keyword × 衰减)。semantic 当前仍恒为 false,但原因已变:向量索引已由组合根注入(cmd/cyrene/wire.go 的 memory.WithVectorIndex),真实原因是索引内当前无向量(生产路径无向索引写入向量,仅测试有 Index.Add),故语义打分返回空;这是诚实反映,不是伪造。
  • 错误:400(query 缺失);500(memory store 未配置 / 检索失败)。
  • 审计:memory.search。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/memories/search \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"query":"咖啡","personaId":"cyrene","topK":5}'

5.30 POST /api/v1/admin/rag

  • Body(ragIndexRequest):{"name","desc?","content"}。name+content 必填。
  • 响应:201 {"data":{"name":"<name>"}}——真实导入:分块并落项目索引,立即可被关键词/行上下文检索。
  • 错误:400(name/content 缺失);501(未注入 RagStore);500。
  • 审计:rag.index。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/rag \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"name":"部署说明","desc":"v2 部署要求","content":"单二进制...\n单端口 8080..."}'

5.31 POST /api/v1/admin/system/revoke-keys

  • Body:无。
  • 响应:200 {"data":{"revoked":<int>}}——清空 API key 表,先前签发的所有 key 立即失效。
  • 错误:500(user service 未配置,users_unavailable)。
  • 审计:system.revoke_keys。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/system/revoke-keys \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1' -d '{}'

5.32 POST /api/v1/admin/system/rotate-jwt

  • Body(rotateJWTRequest):{"newSecret":"<≥32 位>"}(必填)。
  • 响应:200 {"data":{"rotated":true,"refreshTokensRevoked":<int>,"note":"现有 token 与 refresh token 已全部失效,需重新登录"}}(refreshTokensRevoked = 一并删除的 refresh token 行数)。
  • 错误:400(newSecret 缺失 / 长度 <32,"新密钥长度不足 32 位");500(认证服务未配置 / 轮换失败)。
  • 审计:system.rotate_jwt。新密钥永不回显、不落日志。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/system/rotate-jwt \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"newSecret":"a-32-char-long-random-secret-12345678"}'

5.33 POST /api/v1/admin/system/backup

  • Body:无。
  • 响应:200 {"data":{"path":"<dataDir>/backup-<ts>.db","size":<字节数>}}。经 SQLite VACUUM INTO(含已提交 WAL)快照到数据目录。
  • 错误:500(DB 路径未配置 backup_unavailable / 备份失败)。
  • 审计:system.backup。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/system/backup \
  -H "Authorization: Bearer $TOKEN" -H 'X-API-Version: 1' -d '{}'

5.34 POST /api/v1/admin/system/restore

  • Body(restoreRequest):{"path":"<dataDir 内的备份文件>"}(必填)。
  • 响应:200 {"data":{"restored":true,"backup":"<恢复前自动快照路径>","note":"已安排恢复,需重启服务后生效"}}。流程:校验 path 在 dataDir 内(filepath.Rel 防路径穿越,.. 逃逸回 400)→ 文件存在校验 → 先自动备份当前 DB → 复制替换 DB 文件 → 返回(替换后需重启服务生效,因运行进程持有 DB 句柄)。
  • 错误:400(path 缺失 / 逃逸数据目录);404(备份文件不存在);500(DB 路径未配置 / 恢复前备份失败 / 替换失败)。
  • 审计:system.restore。curl:
curl -s -X POST http://<host>:8080/api/v1/admin/system/restore \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -H 'X-API-Version: 1' \
  -d '{"path":"/abs/path/data/backup-20260908T000000.000000000Z.db"}'

6. WebSocket 实时协议(GET /ws/chat)

唯一实时通道。单二进制、单端口:同一 :8080 上,REST/WS/admin 共用。生产者(loop/IoT/task/automation/reminder/subagent)经进程内 bus 发布,hub 订阅 ws.> 及桥接模式并规范化成 WS 帧广播给客户端。上行与下行都有角色门(fail-closed):① 上行帧——只有 owner/admin 能发,非 owner/admin 连接发来的任何上行 JSON 帧被定向拒为 {type:"error",data:{code:"forbidden"}}(ws_route.go 的 route 首条语句,默认拒,不广播/不驱动模型/不落库);② 握手 history 回放——仅 owner/admin(ws_history.go 的 replayHistory 首行);③ 下行帧——只有 7 个不含内容的状态机信号(含 session_read)发给所有已认证连接,其余全部(含 proactive_message)仅 owner/admin(ws_frame_scope.go)。权威口径与帧清单见 Docs/api-client.md §3 顶部的两把角色门;本节其余部分只描述帧形状,鉴权以 api-client 为准。

6.1 握手与连接

  • URL:ws://<host>:8080/ws/chat?token=<JWT>(或 Authorization: Bearer <JWT> 头)。
  • 方法:必须 GET(否则握手前回 400 {"error":{"code":"bad_request","message":"websocket requires GET"}})。
  • 鉴权:s.auth.Verify(wsToken(r))。wsToken 先取 ?token=,再取 Authorization: Bearer。失败 → 握手前回 401 {"error":{"code":"unauthorized",...}}(浏览器/IM 客户端收到干净拒绝)。连接层不校验角色(任意有效 JWT——含 viewer——都可连)。但连接之后的帧按角色分档:上行仅 owner/admin 可发、history 回放仅 owner/admin 可收、下行除 7 个状态机信号外仅 owner/admin(见本节开头与 api-client §3 顶部两把角色门)。
  • Origin 校验:CheckOrigin 允许「同一 origin」+「同一 host」+ 白名单;* 全放行。同 host 放宽:production 内嵌前端页面 origin 即网关 Host,故 PORT 改变无需改 .env。跨源客户端(外部 IM / 另一 origin)需在 ALLOWED_ORIGINS 白名单。
  • 升级后流程:注册连接 → replayHistory(c, "web")(首帧推 history 回放)→ 启动 writePump(唯一写者,含 ping/pong)与 readPump(读客户端帧)。
  • 心跳/限帧:读上限 4096 字节/帧;写超时 10s;pong 等待 60s;ping 周期 54s。发送缓冲 32(慢客户端满则丢帧,生产者不阻塞)。
  • history 回放(首帧):{type:"history", sessionId:"web", data:{messages:[historyMessage...]}}——主会话最近 50 条(wsReplayLimit=50),thinking 已剥离;仅 owner/admin 连接收到(replayHistory 首行角色门,design/11 §7.2 A;viewer 连接建立后不收回放)。回放仅在 history != nil 时发生,失败静默跳过。

6.2 帧壳(Frame Shell)

  • 下行(服务端→客户端) wsEvent:

    字段 类型 必填 说明
    type string 是 事件类型(见 §6.3 表)
    turnId string 否 一轮交换的关联 id(流式;turn-<hex>);OnStream 结构化流路径不带
    seq int 否 hub 级单调递增序号,供客户端排序关联流式突发
    sessionId string 否 能确定会话的事件带上,客户端按激活会话过滤
    data any 否 类型化负载(每个事件见 §6.3)
  • 上行(客户端→服务端) wsInbound:

    字段 类型 必填 说明
    type string 是 事件类型(见 §6.3 上行表)
    reqId string 否 请求关联 id(当前服务端不回填,预留)
    data object 否 每类型负载(raw JSON)

6.3 上行事件(客户端 → 服务端)

由 wsConn.route(in) 按 in.Type 分派。未知类型回 {type:"error", data:{message:"unknown event type"}}。角色门(design/11 §7.2,fail-closed):route 的第一条语句就是角色检查——非 owner/admin 连接发来的任何上行帧一律定向回 {type:"error",data:{code:"forbidden"}},不广播、不驱动模型、不落库;因此下表只对 owner/admin 连接生效(viewer 连上也发不出任何一条)。权威帧清单与版本记录见 api-client §3。

type 数据负载 服务端行为 结果
mode_switch 任意(如 {"mode":"chat"}) 发布 ws.mode_switch → hub 规范化广播 下行 mode_switch(广播)
chat {sessionId?, text, image?, deviceId?, mode?} 发布 ws.chat → streamChat(跑 loop 并流式) 下行 stream_chunk/stream_end 或 message.*/thinking.*/tool_call.*(广播)
interrupt {sessionId} 会话编排器中断:取消在跑 turn + 清空 pending 下行 session_interrupted
voice_input {audio?, format?, text?, sessionId?} STT→chat→TTS(缺省 sessionId=web) 下行 voice_output(定向回请求连接)
subagent_ask {id, mode?(""/full/tail/keyword), n, query?} 审计子代理上下文(缺省 mode=full) 下行 subagent_event(或 error)
task_output_query {taskId} 查询后台任务输出 下行 task_output(或 error)

上行各负载字段

  • chat(chatStreamRequest):{sessionId(缺省"web"), text, image?(base64 data: URI 或 URL), deviceId?, mode?(行为模式,缺省回落 chat)}。text+image 至少一项;runner 未注入或两者皆空 → 直接广播 stream_end{stopReason:"idle"}。主会话 web 绑定全量 env(loop.OwnerEnv())。chat 进入时若该会话正忙 → 由编排器入队(见 §6.4),非阻塞。仅 owner/admin 连接可发(见本节角色门)。
  • interrupt:{sessionId}。命中空闲会话为 no-op。
  • voice_input:{audio(base64 或 data: URI), format(wav|mp3|ogg|webm|mpeg,空由 mime 猜,默认 wav), text(已转写则跳过 STT), sessionId?}。无 voice 服务 → {type:"error",data:{message:"voice unavailable"}}。
  • subagent_ask:{id(必填), mode(空/full/tail/keyword), n(tail 查询窗长), query(keyword 查询词)}。无 auditor → error;id 缺失 → error。
  • task_output_query:{taskId}。无 task store → error;运行中任务返回截至目前输出;输出拼接 stdout + "\n" + stderr。

6.4 下行事件全表

角色门(design/11 §7.1,fail-closed):下行帧按类型分档,收口在 internal/services/gateway/ws_frame_scope.go 的 frameReachesAll(唯一一处判定,各调用点不自行决定可见性)。只有 7 个不含内容的状态机信号发给所有已认证连接(含 viewer):session_busy / pending_ack / pending_drain / session_idle / session_interrupted / pending_full / session_read。其余全部只发 owner+admin:流式内容(message.* / thinking.* / tool_call.* / tool_execution.update / stream_chunk / stream_end)、持久化(message_persisted / message_edited / message_deleted / channel_persisted)、系统内部(graph_update / subagent_event / task_output / task_done / tool_used / capacity_awaken / mode_switch)、私有状态(mood / device_update / reminder / proactive_message);未登记的新帧类型默认只发 owner/admin。本表并非全表:session_read / message_edited / message_deleted 等已实现事件尚未逐行登记于此;权威清单与逐帧口径见 Docs/api-client.md §3。

来源标记:[direct] 网关直接 emit;[relay] hub 从 bus ws.> 规范化(任意 ws.X 主题 → 类型 X);[bridge] 桥接 producer 主题;[target] 仅定向特定连接。

type 来源 数据(data) 用途
history direct {messages:[historyMessage]} 连接首帧回放主会话最近 50 条(thinking 剥离);仅 owner/admin 连接收到(viewer 不收)
message.start direct {index} 正文段开始
message.delta direct {delta,index} 正文 token 增量(累加到当前正文)
message.end direct {index} 正文段结束(不结束整轮,由 stream_end 收尾)
thinking.start direct {index} 思维链段开始(前端折叠块)
thinking.delta direct {delta,index} 思维链 token 增量
thinking.end direct {index} 思维链段结束
tool_call.start direct {id,index} 工具调用中间态开始
tool_call.delta direct {id,index,delta} 工具调用参数增量(如 input_json_delta)
tool_call.end direct {id,index} 工具调用中间态结束
tool_execution.update direct {id,index,delta} 工具执行进度/局部输出
stream_chunk direct {delta} 向后兼容整段文本(仅非流式回退路径;流式路径不 emit)
stream_end direct {stopReason, steps?} / {stopReason:"idle"} / {stopReason:"error", error} 一轮结束
session_busy direct(编排) {sessionId, mode} 会话进入忙碌态(mode∈idle/thinking/executing/subagent)
pending_ack direct(编排) {sessionId, mode, queueDepth} 忙时新消息已入队「收到,稍后回」
pending_drain direct(编排) {sessionId, mode, queueDepth} 忙完开始消费下一待处理消息
session_idle direct(编排) {sessionId, mode} 会话回到空闲、队列清空
session_interrupted direct(编排) {sessionId, mode, dropped} 用户 interrupt,当前任务取消、pending 清空
pending_full direct(编排) {sessionId, mode, max} 单会话 pending 达上限(默认 5)拒绝入队
message_persisted direct {message:historyMessage}(sessionId 设) 每条 user/assistant 消息落库后广播(主会话 web/web:<uuid>),thinking 剥离
channel_persisted direct {message:historyMessage}(sessionId=sess-xxx) 群聊/陌生人(非主人)消息落库后广播,供 IM 适配器日志页
voice_output direct/target {audio, format, text, reply} 语音合成回复(定向回请求连接)
subagent_event direct+relay 审计结果 或 {id,name,status,role,mode} 子代理上下文查询应答 / 子代理进度
task_output direct+bridge {taskId, output, status} 后台任务输出查询应答 / task.output 桥接
capacity_awaken direct {fromState, toState, tool?} 能力状态迁移(design/14 §5,sessionId 设)
proactive_message direct {platform?, channelId?, channelType?, content, contentType?, sender?, timestamp?, messageId?} 自主主动推送(仅 owner/admin 连接收到)
device_update bridge producer payload IoT 设备状态更新(iot.device_update)
task_done bridge producer payload 后台任务完成(task.done)
automation_result bridge producer payload 自动化规则结果(automation.result)
reminder bridge producer payload 提醒触发(reminder.trigger)
mode_switch direct 上行原样(任意) 行为模式切换回显(ws.mode_switch)
mood/tool_used/graph_update/… relay ws.X 主题负载(ws.mood→mood,ws.tool_used→tool_used,ws.graph_update→graph_update,…) 生产者在 bus 上发 ws.X 即规范化成类型 X 广播
error direct {message} 非法 JSON / 未知类型 / 语音·子代理·任务错误等

auth(旧 design/07 列表):当前不由网关直接 emit;若未来有 ws.auth 生产者主题,则按 [relay] 规则广播。本文档以代码为准。

message_persisted / channel_persisted / history 的 historyMessage 全字段(含 thinking 剥离规则):

字段 类型 说明
id string 后端生成消息 id(客户端去重键)
sessionId string 会话 id
role string user / assistant
content string 正文
mode string omitempty
mood string omitempty
image string omitempty
thinking string omitempty,但在这些实时帧中恒为空(BroadcastPersisted/replayHistory 均置空;/messages owner/admin 端点才带)
platform string omitempty(web/telegram/weixin-ilink/qq-obv11...)
deviceId string omitempty
senderName string omitempty(第三方昵称,主人私聊为空)
adapterId string omitempty(仅 channel_persisted 常见,如 kook:3bef...)
createdAt int64 Unix 毫秒

事件类型分流(BroadcastPersisted):sessionId == "web" 或以 web: 前缀 → message_persisted(Web 聊天实时同步);其它(隔离 sess-xxx)→ channel_persisted(IM 适配器日志专用)。Web 聊天无 channel_persisted handler,天然忽略。

6.5 会话编排状态(design/15,data.sessionId 区分)

  • 语义:同一会话 chat 到达时若「她忙」(跑 loop/工具/子代理/长回复)→ 新消息不阻塞、入该会话 pending(上限默认 5);状态经上述 session_busy/pending_ack/pending_drain/session_idle/session_interrupted/pending_full 推给前端,供「正在做 xx… 有新消息稍后回」轻状态展示。
  • 续接:忙完重开时新回合带上上一轮上下文(tail 默认 20 条),非清空重来。
  • mode 取值:idle / thinking / executing / subagent。
  • REST 同步路径(§3.1)在忙时返回 202 queued / 满 409 chat_busy,与这些 WS 状态事件一致。

6.6 流式关联(turnId / seq)

  • turnId:runTurn 每次生成 turn-<hex>(crypto/rand),用于关联一轮流式突发;stream_end/stream_chunk(回退路径)带 turnId。OnStream 结构化流路径(message.* 等)由 broadcastStream 只填 seq、不带 turnId(OnStream bus 负载未携带 turn 身份)。
  • seq:hub 级单调递增 atomic.Uint64,保证一次突发有序,即便 loop 多 goroutine 触发。

6.7 WS 示例

握手 + 收 history + 发 chat 收流式(伪代码/curl-like):

# 1) 拿 token
TOKEN=$(curl -s -X POST http://<host>:8080/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"s3cret"}' | jq -r .data.token)

# 2) 连接(浏览器用 ws = new WebSocket('ws://host:8080/ws/chat?token='+TOKEN))
# 首帧:{"type":"history","sessionId":"web","data":{"messages":[...]}}

# 3) 上行 chat
# {"type":"chat","data":{"sessionId":"web","text":"你好","deviceId":"web:chrome:a1b2c3"}}
# 下行(流式回退):{"type":"stream_chunk","turnId":"turn-abc123","seq":1,"sessionId":"web","data":{"delta":"你"}}
# ... {"type":"stream_chunk",...} ...
# {"type":"stream_end","turnId":"turn-abc123","seq":8,"sessionId":"web","data":{"stopReason":"stop","steps":2}}

# 4) 上行 interrupt
# {"type":"interrupt","data":{"sessionId":"web"}}
# 下行:{"type":"session_interrupted","data":{"sessionId":"web","mode":"idle","dropped":2}}

# 5) 上行 voice_input
# {"type":"voice_input","data":{"audio":"<base64>","format":"wav","sessionId":"web"}}
# 下行(定向):{"type":"voice_output","data":{"audio":"<base64>","format":"wav","text":"你好","reply":"你好呀"}}

# 6) 上行 subagent_ask
# {"type":"subagent_ask","data":{"id":"sub-1","mode":"full"}}
# 下行:{"type":"subagent_event","data":{...审计结果...}}

# 7) 上行 task_output_query
# {"type":"task_output_query","data":{"taskId":"task-1"}}
# 下行:{"type":"task_output","data":{"taskId":"task-1","output":"...","status":"done"}}

7. 上传 / 媒体说明

⚠️ 本节曾长期写「无 /upload 端点 / 无 REST 语音端点」,与实现恰好相反,已于 2026-10-03 修正——正确口径见上文 §0.3 与 api-client §1 行 10–14。

  • POST /api/v1/upload 存在(owner/admin + 审计):multipart file(光栅图片,5MB 上限)→ 201 {"data":{"url","size","mime","name"}};url 为同源访问地址 /api/v1/files/<name>,可直接作 chat.image。
  • GET /api/v1/files/{name} 存在(无需鉴权——128-bit 随机能力 URL,<img>/chat.image 无法带 Bearer):从 <dataDir>/uploads/ 读该文件;name 仅接受「32 位小写 hex + 白名单扩展名」,路径穿越全形态拒绝。契约见 api-client §1 行 14。
  • REST 语音端点存在(均 owner-only + 审计):POST /api/v1/audio/{transcribe,speech,voice-clone},供原生/移动端绕开 WS 4KB 帧限;语音也可走 WS 上行 voice_input / 下行 voice_output(定向回请求连接)。
  • 图片 + 文本并存:image 非空时 turn 变为「带图 + 可选文本」;text 空 + image 有 → 纯图像 turn。
  • 文档导入:走 POST /api/v1/admin/rag 的 JSON content 文本,不是文件上传。
  • 敏感面:image(base64)与 bot_token/apiKey/newSecret 等概不作为日志明文输出;wire 响应对 token/key 做掩码或不返回。

8. 附录

8.1 通用错误码 / 状态码

状态码 code(示例) 说明
200 - 成功
201 - 创建成功(personas/子会话/apikey/automations/llm/channels config/memories/rag)
202 - 已接受(chat 排队 queued:true,queueDepth)
204 - CORS 预检 OPTIONS 成功无内容
400 bad_request / invalid_cron / invalid_rule / invalid_role / unsupported_action / invalid_param / invalid_provider / invalid_scope 请求体/参数非法
401 unauthorized 认证缺失/失效/过期(REST Bearer 与 WS)
403 forbidden 角色不足(viewer 读管理/主会话删除/写操作);主会话不可删
404 not_found 资源不存在
409 chat_busy / duplicate / device_offline / needs_login 冲突:队列满 / 人格已存在 / 设备离线 / 渠道未登录
500 internal / chat_failed / sessions_failed / … 服务端错误(含未注入依赖)
501 not_implemented 后端确定未实现(新增用户 / IoT 场景·规则 / 主人档案保存 / 编辑环境变量·重启服务·检查更新 / 完整会话转录导出等)——诚实返回,不伪造成功。注:插件安装(§5.26)与知识库删除(§1.3 #23)已实现,不再返回 501

8.2 依赖注入与「未配置」行为

网关依赖(Deps)大多为 nil 容忍,未注入时端点要么回退、要么如实报错,绝不伪造成功:

  • History 未注入:GET /sessions 回退渠道会话视图;/messages、POST /sessions、DELETE /sessions/{id}、/channels/messages 回 501 not_implemented。
  • RAG 未注入:GET /admin/rag 返回空文档列表;POST /admin/rag、/admin/rag/delete 回 501。
  • Runner(Loop) 未注入:/chat 回 500 chat_unavailable;WS chat 直接广播 stream_end{stopReason:"idle"}。
  • Channels 未注入:/admin/channels/config 回 500 channels_unavailable。
  • LLM 未注入:/admin/llm 按 .env 合成单组;POST /admin/llm 回 500 llm_unavailable。
  • Voice 未注入:WS voice_input 回 error {message:"voice unavailable"}。
  • Scheduler(auditor) 未注入:WS subagent_ask 回 error {message:"subagent audit unavailable"}。
  • Tasks 未注入:WS task_output_query 回 error {message:"task output unavailable"}。
  • AuditStore 未注入:审计退化为进程内存(端点仍返回数据)。
  • 管理员只读端点若依赖缺失,大多返回 [],不 500。

8.3 审计动作清单(what 字段,落 audit_logs)

policy.set / persona.create / persona.update / user.create / apikey.issue / apikey.revoke / iot.control / iot.scenario / iot.rule / automation.create / automation.run / automation.delete / llm.addProvider / llm.test / channel.open / channel.close / channel.identity-test / channel.weixin.login.qr / channel.weixin.login.status / channel.weixin.login.persist / channel.platform.view / channel.friends / channel.friend.handle / channel.message.test / channel.save / channel.delete / plugin.install / plugin.uninstall / memory.add / memory.search / rag.index / rag.query / rag.delete / session.export / system.revoke_keys / system.rotate_jwt / system.backup / system.restore。 (what 后缀 _failed/_persist_failed/_reload_failed 等计入 error 分支。)

8.4 里程碑与未实现集合(诚实标注)

  • 已真实实现:登录/健康检查/聊天(同步+流式)/会话(列表/新建/删除/消息分页)/人格/管理读(personas/sessions/memories/plugins/channels/iot/llm/users/automations/backthink/rag/observability×4:metrics/trace/audit/logs)/管理写(人格/API key/设备控制/自动化/LLM 保存·测试/渠道连接·断开·身份测试/渠道配置·删除/KOOK 面板/微信扫码/记忆添加·检索/插件安装·卸载/RAG 导入·删除/主人档案读写(GET/PUT /api/v1/admin/owner-profile,v0.51)/系统设置 4 项)。
  • 仍为后置 M10(返回 501 not_implemented,不伪造):新增用户(无用户表/登录)、IoT 场景与规则(无模型/存储)、编辑环境变量/重启服务/检查更新、完整会话转录导出(当前导出身份元数据)。
  • 语义/向量增强:memory.search 的 semantic 当前恒为 false——向量索引已由组合根注入(cmd/cyrene/wire.go 的 memory.WithVectorIndex),但索引内当前无向量(生产无写入),故语义打分返回空,属诚实反映而非伪造;RAG embedded 反映部署是否配置向量增强;配置了增强必须真实走语义/向量路径(红线,非空壳)。
  • API key 认证:X-API-Key 已接入鉴权(v0.51,见 §0.1 注与 api-client.md §认证)——命中签发表即通过、一律 owner 级(取消 role 分级),带该头时只走 key 分支(坏 key 回 401、不回落 Bearer);签发/吊销/列表同样真实生效。

8.5 客户端接入快速索引

  1. POST /api/v1/auth/login → {token, expiresAt, role}(恒 owner)。
  2. 之后每个 REST 请求带 Authorization: Bearer <token>;管理写需 owner;管理读需 owner|admin;GET /sessions、GET /personas 任意已认证。
  3. 实时:ws://<host>:8080/ws/chat?token=<token>;首帧 history 回放(仅 owner/admin 连接收到);上行 chat/interrupt/voice_input/subagent_ask/task_output_query/mode_switch(仅 owner/admin 连接可发,viewer 发送一律被定向拒);下行见 §6.4 与 api-client §3。
  4. 身份判定永远来自后端 channel 层 actor 元数据 + 白名单(configs/users/owner.yaml),客户端不要用提示词/文本判身份;thinking 在公开回放/实时帧中已剥离,客户端不应依赖。
  5. 发送消息带 deviceId(推荐)以便端标识落库;platform 用于 IM 渠道过滤。