Files
Cyrene_v2/Docs/api-client.md
T
AskaEth cdc370dc82
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(weixin-ilink,gateway): 会话过期显式化 — 不再静默变哑/假 OK 重连,面板可见「需重扫」
- 适配器新增过期状态机(plugins/channels/weixin_ilink/session_state.go):getupdates/sendmessage/getuploadurl 收到 errcode=-14 即 markExpired(ERROR 仅一次,只带 bot_id,token 永不入日志),扫码登录拿到新 token 或配置换新 token 时清除(惰性比对)
- Connect/HealthCheck 在过期时返回 ErrSessionExpired(可与 ErrNotConnected 区分):不再对死 token 设 connected=true 报假成功;发送路径同样返回该错误而不是笼统 ErrNotConnected
- 网关:POST /admin/channels/open 遇过期 → 409 session_expired + 明确文案(不再吞错回 200);GET /admin/channels 摘要新增 needsRescan/lastError(经可选接口类型断言读取,取不到不伪造)
- channelbridge:读循环非取消退出时把自身记账的 connected 置 false(热重载被替换者退出不抹掉新实例)
- login/login_admin:过期时 StartLogin 允许重新出码、LoginStatus 如实报 failed(原先可能把死 token 当 confirmed 重新落盘)
- Docs/api-client v0.39;新增 5 组测试 + 变异验证(去掉标记/恢复假成功/网关不填字段 均 FAIL)
2026-10-03 18:00:01 +08:00

101 KiB
Raw Blame History

Cyrene v2 客户端 API 文档(用户侧)

完整版见 Docs/api-reference.md(后端全部 API(REST + WebSocket)超详细调用文档,字段/鉴权/状态码以代码为准)。本文档为摘要,契约如有出入以 api-reference.md 为准。

面向: Web 前端 / 移动端原生客户端 / IM 桥接等跨平台客户端开发者。 维护铁律: 每实现/变更一个 API(REST 端点、WS 事件、admin API)立即更新本文件;契约变更必须版本化,不破坏既有客户端。 状态: gateway 已在 M8 落地核心端点;M9 前端按此开发。✅ = 已实现,🕐 = 规划。


0. 基础约定

  • Base URL: http://<host>:8080(网关唯一对外端口;生产一般经反代/HTTPS)。
  • 版本: REST 全部挂在 /api/v1/{resource};未来破坏性变更升 v2。
  • 认证: Authorization: Bearer <JWT>(RBAC:owner/admin/viewer);管理端另支持 X-API-Key。
  • 响应统一: {"data": ...} / {"error": {"code": <string>, "message": <string>}};状态码 200/201/204/400/401/403/404/429/500。
  • 错误码:业务错误带 code 字符串(如 unauthorized / not_found),便于客户端分支。

1. REST 端点(公开)

# 端点 方法 认证 状态 说明
1 /api/v1/auth/login POST 否 ✅ {username,password} → {data:{token,expiresAt,role,refreshToken?}}(access 24h;新增 refreshToken——不透明随机串,仅哈希落库,AUTH_REFRESH_TTL 默认 720h,用于静默续期;role=owner)
1a /api/v1/auth/refresh POST refreshToken ✅ 静默续期(B2):body {refreshToken} → {data:{token,expiresAt,refreshToken?}}(新 access + 新 refresh,默认单次使用旋转——旧 refresh 立即作废,并发重放 fail-closed 401 invalid_grant)。AUTH_REFRESH_ROTATE_DISABLED=true 关闭轮换时旧 token 保持有效、不返回新 refresh。过期/已撤销/乱串统一 401 {error:{code:"invalid_grant"}};成功才审计
1b /api/v1/auth/logout POST refreshToken 或 JWT ✅ 登出(B2):body {refreshToken} 撤单个 grant(幂等);带 Authorization: Bearer <jwt> 且无 body → 撤该账号全部 grant(全设备登出,返回 {revokedCount})。refresh 相关操作成功才审计
2 /api/v1/health GET 否 ✅ 健康检查 {data:{status:"ok"}}
3 /api/v1/chat POST JWT ✅ {text,image?,deviceId?,mode?,stream?} → {data:{reply}};stream:true 时经 WS 触发流式。image 为 base64 data: URI 或 http(s) URL,入视觉路由(design/06)——真视觉需配 LLM_VISION_MODEL/凭据,无凭据时降级为主模型直接看图或视觉·OCR 工具文本描述;deviceId 为客户端生成的具体端标识(design/16 §4.5,如 web:chrome:a1b2c3,可选),落库到消息、便于主人知道从哪个端发来;mode 为行为模式(design/01 §3 / design/16 §5.1,如 chat/code/home/docs/…,可选),随该轮次落库——IM 恒 chat、缺省回落 chat,供前端按模式渲染气泡(仅 chat 才解析 <p> 拆气泡)。会话编排(design/15):非流式且该会话「正忙」时不阻塞,返回 202 {data:{queued:true,queueDepth}}(稍后按上下文续接);待处理队列满时返回 409 {error:{code:"chat_busy"}}
4 /api/v1/sessions GET JWT ✅ {data:[session...]}。会话持久化(design/16):注入 HistoryStore 后返回持久化会话元数据 `{data:[{id,type("main"
5 /api/v1/sessions POST JWT(owner/admin) ✅ 新建次级会话 → 201 {data:{sessionId}}(sessionId="web:<uuid>");未注入 HistoryStore 回 501 not_implemented
6 /api/v1/sessions/{id}/messages GET JWT(owner/admin) ✅ 热分页拉历史:?limit=&before=&platform=&after=(A5)默认 limit=50(上限 200)。before=上一页最旧 createdAt(毫秒),按时间倒序;after=<createdAt 毫秒>,按时间升序返回 created_at > after 的消息(正向增量,客户端把上次看到的 createdAt 当游标补断线期间漏的,解决重连只回放 50 条的缺口);after 与 before 互斥,同传回 400;负数/非数字回 400。platform 为可选来源平台过滤(design/16 §4.4)——Web 端不传(=全量共享主会话),IM 适配器传本平台(如 telegram)只拉本渠道消息;deviceId 为客户端具体端标识;senderName 为第三方发送者昵称;q(D2)=会话内内容搜索(大小写不敏感 ASCII LIKE,%/_/\ 已转义为字面量),仍按时间倒序、before/limit 同分页语义;q 与 after 互斥(同传 400),空 q 视为未传(走普通分页),搜索严格限定在 {id} 会话内(绝不跨会话,群聊/陌生人隔离会话不会外泄);非法 limit/before/after 回 400;未注入 HistoryStore 回 501
6a /api/v1/messages/{id} PATCH JWT(owner/admin) ✅ 编辑单条消息(D3):body {content}(非空、≤4000 rune,空/纯空白/超长回 400)→ {data:<消息DTO>}(data 直接就是更新后的完整消息 DTO,含 id/sessionId/role/content/.../createdAt,无 message 包一层;只改 content,role/时间戳/平台保持不变);消息不存在回 404 not_found;未注入 HistoryStore 回 501。成功后广播下行 message_edited(§3.8,账号其它端免刷新同步)。重发不单独设端点——客户端用「编辑后的内容再 POST /chat」即可(避免重复轮次语义)
6b /api/v1/messages/{id} DELETE JWT(owner/admin) ✅ 删除单条消息(D3)→ {data:{deleted:<id>}};不存在回 404 not_found;未注入 HistoryStore 回 501。成功后广播下行 message_deleted(§3.8,账号其它端按 id 移除)
7 /api/v1/sessions/{id} DELETE JWT(owner/admin) ✅ 删除次级会话及其消息 → {data:{deleted:<id>}};主会话不可删回 403 forbidden;未注入 HistoryStore 回 501
8 /api/v1/sessions/{id} PATCH JWT(owner/admin) ✅ 会话元数据更新(A4 改名 + D5 置顶):body {title?,pinned?}(至少一个字段,都缺回 400)。title trim、≤80 rune,空/超长回 400;pinned 为 bool(置顶/取消,主会话与次级会话均可)。响应只回显本次变更的字段:{data:{id,title?}} / {data:{id,pinned?}} / 两者都改则都有(title-only 保持 A4 原 {id,title} 形状);不存在回 404;主会话(id="web")可改名/置顶但不可删;未注入 HistoryStore 回 501。另:会话首条用户消息到达时自动命名 title(取前 20 rune 折叠空白;纯空白回落 主会话/次级会话),列表不再空名
8a /api/v1/sessions/{id}/read POST JWT(owner/admin) ✅ 推进已读游标(D4 / Android 协同 B3):body {before?}——before 为可选的毫秒 Unix 时间戳,缺省(或空 body)= 现在,即「我看到的都算已读」。→ 200 {data:{sessionId,readAt}}(readAt 为推进后的游标毫秒)。判定:createdAt <= readAt 的消息算已读(列表 unreadCount 只数 createdAt > readAt 的);语义:会话级单一游标、单 owner 账号——读一处 = 该账号所有端已读(不做 per-device 游标,与「同一账号所有浏览器都属于同一 owner」一致);只前进不后退(SQL read_at = MAX(read_at, before)),所以乱序/并发/重试请求幂等安全、绝不会把已读变回未读。before 为 0/负数/非数字、或 body 不是 JSON 对象 → 400;未知会话(含隔离 sess-xxx,不在 chat_sessions)→ 404;未注入 HistoryStore → 501 not_implemented。成功后广播下行 session_read(§3.9)让同账号其它端清红点;失败路径不广播。升级回填:迁移 0010 把既有会话游标回填为「该会话现有最新消息时间」,升级当天不误报未读
9 /api/v1/personas GET JWT ✅ {data:[persona...]}
10 /api/v1/audio/transcribe POST JWT(owner) ✅ 语音→文本:body {audio:"<base64>",format?,deviceId?} → {data:{text, provider?}}(走后端 STT,百炼 qwen-audio-3.0-asr-flash;当结果为空文本/silent 降级时响应体带 provider(如 "silent",非空文本省略),客户端据此提示「没识别到声音」)
11 /api/v1/audio/speech POST JWT(owner) ✅ 文本→音频:body {text,voice?,format?,instruction?} → 200 音频字节(Content-Type: audio/<format>,非 JSON envelope,走后端 TTS,百炼 cosyvoice-v3-flash,复刻音色可带 instruction 情绪指令;静默降级空音频时仍 500 tts_empty,并附 X-Voice-Provider: silent 响应头,客户端据此提示「没生成声音」)
12 /api/v1/audio/voice-clone POST JWT(owner) ✅ 声音复刻建音色:body {url:"<公网参考音频URL>",prefix?} → {data:{voiceId}}(走百炼 voice-enrollment create_voice)
13 /api/v1/upload POST JWT(owner/admin) ✅ 图片上传(B1):multipart/form-data,字段 file(raster image,5MB 上限)→ 201 {data:{url,size,mime,name}};url 为同源访问地址(<host>/api/v1/files/<name>,可直接作 chat.image)。校验:MaxBytesReader 防超大 → DetectContentType 嗅探(不信客户端 Content-Type)→ 白名单 png/jpeg/gif/webp/bmp(拒 SVG/HTML);文件名用 crypto/rand 128-bit 随机名(原始文件名丢弃),存 <dataDir>/uploads/(0700)。错误码:缺文件 400 bad_request、超限 413 payload_too_large、非白名单 415 unsupported_media_type、未授权 401/403
14 /api/v1/files/{name} GET 无需(能力URL) ✅ 上传文件访问(B1):从 <dataDir>/uploads/ 读取,同名端口服务。name 仅接受「32 位小写 hex + 白名单扩展名」;路径穿越全形态拒绝(../%2f/%2e%2e 等)。返回 Content-Type/Content-Length/Cache-Control: private,max-age=86400,immutable/X-Content-Type-Options: nosniff(支持 Range)。设计取舍:GET 不鉴权——该 URL 是128-bit 随机能力地址(chat.image/<img> 无法带 Bearer),POST 仍 owner/admin+审计;任何持有该 URL 者都能取文件,勿外泄到他人

| 15 | /api/v1/meta/enums | GET | JWT(任意角色) | ✅ | 共享枚举权威表(B4):{data:{platforms,moods,modes,unknown}},每项 {id,label}——platform/mood/mode 的唯一事实来源,客户端不必硬编码即可拿到稳定映射(来源角标 / 情绪标签不再漂移)。只读、无敏感信息,与 GET /sessions 同级(owner/admin/viewer 均可读;无 token 回 401)。详见 §1.1 |

语音 REST 源(/api/v1/audio/*,均 owner-only + 审计):原生/移动端用它上传/获取音频,绕开 WS 4KB 帧限。

  • POST /api/v1/audio/transcribe:body {audio:"<base64>", format?:"wav", deviceId?} → 200 {data:{text}}。audio 为 base64(或 data: URI);format 为编码(wav/mp3/ogg,缺省 wav);空 audio 回 400;未注入语音服务 500 voice_unavailable、识别失败 500 stt_failed。
  • POST /api/v1/audio/speech:body {text, voice?, format?, instruction?} → 200 原始音频字节(Content-Type: audio/<format>),非 JSON envelope。voice/format 目前遵循服务端 VOICE_TTS_VOICE_ID/VOICE_TTS_FORMAT 配置(后续支持按请求覆盖);instruction 为可选 per-request 情绪/语气指令(如 "语气开朗俏皮,像女朋友撒娇"),只对复刻/设计音色生效——系统音色 longxiaochun_v3 被服务端忽略,永不发送自由指令(CosyVoice 系统音色需固定格式);长度 ≤100 字符(汉字记 2);未传时回退到服务端 VOICE_TTS_INSTRUCTION。空 text 回 400;未注入语音服务 500 voice_unavailable、合成失败 500 tts_failed、静默空音频 500 tts_empty。
  • POST /api/v1/audio/voice-clone:body {url:"<公网参考音频URL>", prefix?:"cyrene"} → 200 {data:{voiceId}}。url 为公网可访问的参考音频 URL(create_voice 只收 input.url,不收 base64),音频要求 10~20s 连续清晰朗读、≤10MB、≥16kHz;prefix 为音色名前缀(≤10 字母/数字,缺省 cyrene)。返回的 voiceId 填到 VOICE_TTS_VOICE_ID 即可用该复刻音色合成(target_model 固定为 VOICE_TTS_MODEL,须与合成模型一致)。URL 不写审计/日志(红线,绝不打日志)。空 url 回 400;未注入语音服务 500 voice_unavailable;建音色失败 500 voice_clone_failed。⚠️ 真建音色需主人提供公网参考音频 URL(当前无,断言待提供);且需北京地域 API Key(CosyVoice 声音复刻仅华北2北京地域)。

1.1 枚举权威表(GET /api/v1/meta/enums,B4)

背景:platform / mood / mode 此前只在文档里给示例(web/telegram/…;calm;normal),客户端只能自建映射表——来源角标与情绪标签因此会随客户端漂移。本端点发布权威枚举表 + 未知值回落约定:Web / Android / IM bridge 共用同一份,客户端不必硬编码。

  • 鉴权:Authorization: Bearer <JWT>,任意已认证角色可读(owner/admin/viewer),与 GET /api/v1/sessions 同级——只读、无敏感信息。无 token / 令牌无效回 401。无其它错误码(纯内存投影,无存储、无 501)。
  • 响应:200
    {"data":{
      "platforms":[{"id":"web","label":"网页"}, ...],
      "moods":[{"id":"calm","label":"平静"}, ...],
      "modes":[{"id":"chat","label":"对话"}, ...],
      "unknown":"display-raw"
    }}
    
    每项固定为 {"id":"<枚举 id>","label":"<默认中文标签>"}。label 是默认值,自带 i18n 的客户端可覆盖显示;id 才是必须比对的值。
  • unknown(未知值回落约定,机器可读):恒为 "display-raw"。语义见下。

platform(消息来源平台,{id:label} 逐值)

id label 说明
web 网页 Web 主会话来源(chat_messages.platform 默认值)
kook KOOK KOOK 适配器
qq-obv11 QQ OneBot v11(QQ)适配器
weixin-ilink 微信 微信 iLink 适配器
telegram Telegram Telegram Bot API 适配器
webhook Webhook 通用 Webhook 适配器

mood(情绪,{id:label} 逐值)

id label
calm 平静
happy 开心
excited 兴奋
sad 低落
angry 生气

⚠️ 情绪状态机可扩展(persona 可注册更多状态),因此上表不是封闭集合——未知 mood 必须按回落约定原样显示。

mode(行为/呈现模式,{id:label} 逐值)

id label
chat 对话
code 代码
home 家居
work 工作
docs 文档
voice 语音
vision 视觉
agents 智能体
settings 设置

未知值回落约定(红线)

  • 客户端遇到不在表中的取值(platform / mood / mode 任一):原样显示该原始字符串——不丢弃、不报错、不替换占位符、不猜测。
  • 服务端新增枚举取值是非破坏性变更(forward-compatible):表中新增一项不改变既有字段/端点形状,旧客户端无需升级;不要因为读到未知值而清空角标、抛异常或中断渲染。
  • 判定只比对 id(大小写敏感,契约 id 为小写);label 仅作显示默认值。
  • Web 端镜像实现在 frontend/web/src/lib/enums.ts(labelFor(kind, value) 对未知值返回原值),权威仍以本端点为准。
  • 反漂移保证(后端):后端有测试断言「每个内置适配器的 Name() 都在 platforms 表内」「moods 与 persona 情绪状态机的内置状态一致」「modes 与前端 MODES 九模式一致」——漏登记即 CI FAIL,客户端拿到的表不会滞后于代码。

契约版本 v0.39(新增可选字段,向后兼容)。

2. REST 端点(管理,/api/v1/admin/*)

仅供管理面板与授权客户端;RBAC:owner/admin/viewer(viewer 只读、写 403);写操作全程审计。

# 端点 方法 状态 说明
1 /api/v1/admin/personas GET ✅ 人格列表
2 /api/v1/admin/sessions GET ✅ 持久化会话列表(主会话 + 次级会话,design/16 §2);未注入 HistoryStore 时回退渠道会话视图
3 /api/v1/admin/memories GET ✅ 记忆列表(手动添加/检索已实现;编辑/删除/衰减随 M10 扩展)
4 /api/v1/admin/plugins GET ✅ 插件/能力列表(含 MCP/渠道)
5 /api/v1/admin/plugins/policy POST ✅ 写工具策略(owner-only,审计)
5a /api/v1/admin/plugins/installed GET ✅ 已装运行时插件列表(owner/admin 读):{data:[{id,name,kind,transport,endpoint?,command?,args?,apiKey?,env?,createdAt?,live,toolCount}]},apiKey/敏感 env 值掩码(如 ****1234),live = 是否已连接注册,toolCount = 该 server 注册工具数
6 /api/v1/admin/channels GET ✅ 渠道列表/健康 + 每平台已配账号(脱敏 token 掩码,design/04 §3.1);已连接渠道的账号附 bot 身份(botId/botUsername/botNickname,如 KOOK GET /api/v3/user/me 自动获取);适配器要求重新授权时附 needsRescan + lastError(v0.39,仅微信 iLink 过期 token)
7 /api/v1/admin/iot GET ✅ IoT 设备列表
8 /api/v1/admin/users GET/POST ✅(GET) 用户/角色/API key(GET 已实现;POST 创建用户/签发 key 随 M10 落地)
9 /api/v1/admin/llm GET/POST ✅ LLM 多厂商多模型(models.json):GET 列表(APIKey 脱敏,含 provider 协议与 maxTokens 预算);POST 保存组→落盘 models.json→热更新运行中的 LLM stack(design/04 §3.1,无需重启)
10 /api/v1/admin/automations GET/POST ✅(GET) 自动化与提醒(GET 已实现;POST 创建 M10)
11 /api/v1/admin/backthink GET ✅ 后台思考记录
12 /api/v1/admin/rag GET ✅ 知识库文档列表(分块/嵌入状态);带 ?query= 时并入检索结果
13 /api/v1/admin/rag/query POST ✅ 知识库检索(真实检索;配置向量增强走语义路径)
14 /api/v1/admin/rag/delete POST ✅(写) 删除知识库文档(真实删除:项目索引条目 + 级联章节);owner-only,{id} → {id,deleted:true},不存在 404
15 /api/v1/admin/observability/metrics GET ✅ 网关请求指标(总数/QPS/延迟/方法·路径·状态计数/LLM·子代理计数)
16 /api/v1/admin/observability/trace GET ✅ 最近 trace span(环形缓冲,默认 512 条;loop/工具/编排的真实 span)
17 /api/v1/admin/observability/audit GET ✅ 审计记录(who/what/when/result,SQLite 持久化)
17b /api/v1/admin/observability/logs GET ✅ 后端日志 tail(logger 环形缓冲,默认 2000 条;?level=/?limit=,见下方「可观测性契约」)
17a /api/v1/admin/notifications GET ✅ 统一通知 feed(审计 + 主动推送聚合为只读 feed,design/10 §3 #16;见下方「通知聚合契约」)
18 /api/v1/admin/memories POST ✅(写) 手动添加记忆(owner-only,审计);body {personaId?,content,scope?},无 store 回 501
19 /api/v1/admin/rag POST ✅(写) 导入知识文档(owner-only,审计);body {name,desc?,content},分块后落项目索引可检索;无 store 回 501
20 /api/v1/admin/system/revoke-keys POST ✅(写) 吊销全部 API key(owner-only,审计);返回 {revoked:n}——key 表清空,先前签发的所有 key 立即失效
21 /api/v1/admin/system/rotate-jwt POST ✅(写) 轮换 JWT 密钥(owner-only,审计);body {newSecret}(≥32 位,不强校验熵)→ {rotated:true,note,refreshTokensRevoked}——现有 access token 立即失效需重新登录,且所有 refresh token 一并作废(refreshTokensRevoked 为删除条数);新密钥不回显、不落日志
22 /api/v1/admin/system/backup POST ✅(写) 备份数据(owner-only,审计);用 SQLite VACUUM INTO 快照(含已提交 WAL 内容)到 <dataDir>/backup-<ts>.db → {path,size}
23 /api/v1/admin/system/restore POST ✅(写) 恢复数据(owner-only,审计);body {path} → 校验路径在 dataDir 内→先备份当前→替换 DB 文件→{restored:true,backup,note:"需重启后生效"}(运行进程持有 DB 句柄,热替换需重启可见)

M10 前端契约补充(管理面板):GET /api/v1/admin/users · GET /api/v1/admin/llm · GET /api/v1/admin/automations · GET /api/v1/admin/backthink · GET /api/v1/admin/rag · POST /api/v1/admin/rag/query · GET /api/v1/admin/observability/metrics · GET /api/v1/admin/observability/trace · GET /api/v1/admin/observability/audit · GET /api/v1/admin/observability/logs · GET /api/v1/admin/notifications 已实现,返回真实数据(日志 tail 见下方「可观测性契约」;未注册 GET /api/v1/admin/logs)。写操作已补齐(见下方「管理面板写操作契约」):人格/API key/设备控制/自动化/LLM 测试/渠道连接与识别测试/记忆检索/会话导出/插件卸载真实持久化或真实生效;系统设置安全与存储类(吊销 API key / 轮换 JWT 密钥 / 备份 / 恢复)也已真实实现(见下方「系统设置契约」);仍为后置 M10:新增用户、IoT 场景与规则、主人档案保存、编辑环境变量/重启服务/检查更新——后端如实返回 501 not_implemented,不伪造成功。(知识库删除已实现:POST /admin/rag/delete 真实删除,见下方写操作契约;插件安装已实现:v1 为 MCP server 热安装。)

管理面板写操作契约(POST/PUT/DELETE /api/v1/admin/*,均 owner-only,读为 owner/admin;写操作经 audit 落 audit_logs;成功 {data} / 失败 {error:{code,message}}):

  • POST /admin/personas body {id,name,role,systemPrompt?,characterRef?,modes?/mode?,memoryNamespace?} → 201 {data:{id,name,character,isDefault,mode,memoryNamespace}}(注册进 persona registry + 持久化到 configs/personas/<id>.yaml);id 已存在回 409 duplicate。
  • PUT /admin/personas/{id} body 同上(不含 id)→ 200 {data:PersonaSummary};不存在回 404。
  • POST /admin/users → 501 not_implemented(无用户表:登录仅用 .env 单 admin ADMIN_USERNAME/ADMIN_PASSWORD,角色为 owner>admin>viewer 常量;多用户存储 + 登录接入需先设计——design/11 单用户为主。不做「建了却登不进」的假用户)。
  • POST /admin/users/apikey body {role:"owner|admin|viewer"} → 201 {data:{id:<掩码>, key:<明文·仅此一次>, role}}。key 仅此响应出现一次。
  • DELETE /admin/users/apikey/{id}(id=列表掩码)→ 200 {data:{id,revoked:true}};未命中 404。
  • POST /admin/iot/{id}/control body {action,params?} → 200 {data:{id,name,type,state,online}}(真实下发到 IoT provider);404 设备不存在 / 400 动作不支持或参数错 / 409 设备离线。
  • POST /admin/iot/scenarios、POST /admin/iot/rules → 501 not_implemented(IoT 无场景/规则模型与存储:iot.Provider 只有设备列表/控制/状态;通用 automations 只跑 cron/事件规则,不含设备状态条件 + 设备动作执行器。场景/规则 CRUD 需先新增模型 + 执行器)。
  • POST /admin/automations body {name,kind:"cron|event",expression} → 201 {data:{id,name,kind}}(真实注册规则);无效 cron/规则回 400 invalid_cron|invalid_rule。
  • POST /admin/automations/{id}/run → 200 {data:{id,output}}(手动触发);404。
  • DELETE /admin/automations/{id} → 200 {data:{id,deleted:true}};404。
  • POST /admin/llm body {id?,provider?,baseUrl,apiKey?,model,multimodal?,fallback?,maxTokens?} → 201 {data:{provider:{id,name,provider,baseUrl,model,multimodal,enabled,fallback?,maxTokens?}, persisted:true, path}}。真实持久化:读-改-写 models.json(本地、gitignore),保存组提升为生效组(index 0),并热更新运行中的 LLM provider(path 为落盘文件路径);API key 写入文件但永不回显/不落日志。provider 为空默认 openai;缺 baseUrl/model 回 400;maxTokens 为负回 400;未注入 LLM stack 回 500 llm_unavailable、写盘失败 500 llm_persist_failed、热重载失败 500 llm_reload_failed。maxTokens 缺省/0 = 未设(回落 LLM_FALLBACK_MAX_TOKENS / 适配器默认)。.env 为兜底。
  • POST /admin/llm/test body {provider?,baseUrl,model,apiKey?,prompt?} → 200 {data:{ok,provider,model,reply,tokens}}(真实模型调用);无效/失败回 400 invalid_provider / 500 llm_test_failed。API key 仅本次请求内使用,不落库、不进 wire。
  • POST /admin/channels/open、POST /admin/channels/close body {name} → 200 {data:{name,connected}}(真实 Connect/Disconnect);404(未注册渠道)/409 session_expired(微信 iLink bot token 已过期,重连无法修复,须先「扫码登录」重新绑定,v0.39)/409 needs_login(微信 iLink 尚无 token)/500(其它凭据缺失等连接失败)。
  • POST /admin/channels/test body {platform?,accountId,isGroup} → 200 {data:{role,owner,isGroup,env,verdict}}。结构性判定:channel 层 actor 元数据 × configs/users/owner.yaml 白名单,绝不靠提示词。
  • 微信 iLink「会话过期」契约(v0.39,适配器级,非端点):iLink 返回 errcode=-14(bot token 过期)时适配器进入显式过期态——IsConnected() 恒 false;Connect() 不再返回成功,而是返回「需要重新扫码」类错误(POST /admin/channels/open 对应 409 session_expired);HealthCheck 同样把「过期」与「未连接」区分开。GET /admin/channels 该平台行带 needsRescan:true + lastError(人类可读原因,绝不含 token)。清除条件:扫码登录拿到新 token(login-status 报 confirmed)或配置里换了新 token(保存后热重载)。过期期间:login-status 不假报 confirmed 而是 failed + 过期原因(避免面板把死 token 当成功重新落盘);login-qr 仍可正常出码(不回 already_logged_in 挡住重新扫码);get_bot_qrcode 不再携带已失效 token(否则服务端会以「已绑定」应答而不发新凭据)。未实现该能力的平台:needsRescan/lastError 不出现(omitempty),客户端按缺省处理。
  • KOOK 适配器面板端点(design/17 §2,owner-only + 审计,仅 kook 平台实现;其它平台回 501 not_implemented,账号未找到回 404):这些端点均带可选 ?accountId= 定位到具体 channels.json 账号(不传=该平台首个账号)。token/密钥不打印、不落日志;子项拉取失败不伪造、记入 errors。
    • GET /admin/channels/{platform}/view?accountId= → 200 {data:{platform,accountId?,friends?,userChats?,guilds?,channels?,members?,errors?}}。聚合该账号真实数据(好友/申请/屏蔽、服务器、首个服务器频道与成员、私信会话);errors 为 {子项:原因} 记录失败的子项。
    • GET /admin/channels/{platform}/friends?type=&accountId= → 200 {data:{friends?,requests?,blocked?}}。type ∈ friend|request|block(空=全部),调用 KOOK GET /friend?type=。
    • POST /admin/channels/{platform}/friend/handle?accountId= body {id,accept} → 200 {data:{handled:true,id,accept}}。同意/拒绝好友申请(KOOK POST /friend/handle-request)。
    • POST /admin/channels/{platform}/message/test?accountId= body {targetId,content} → 200 {data:{msg_id,msg_timestamp}}。真实向 targetId(频道 id)发一条 kmarkdown 测试消息(KOOK POST /message/create),验证 bot 可达性。
    • GET /admin/channels 每条账号附 capabilities(可选 []string,如 kook 的 ["friends","user_chats","channels","guilds","message_test"]),供面板按平台渲染专属功能卡片。
  • KOOK「token 即用、其余自动获取」(适配器契约,非端点):填 KOOK 渠道时只必填 token(bot_token)。base_url 留空即用官方默认 https://www.kookapp.cn/api/v3;receive_mode 空默认 ws(WS 模式地址由 GET /api/v3/gateway/index?compress=0 自动获取,已接入);webhook_path 空默认 /webhook/kook。高级值(改用 webhook、webhook_verify_token、webhook_encrypt_key,仅 webhook 模式需要)经 PUT 的 config JSON 传入(前端走「额外 config」,如 {"receive_mode":"webhook","webhook_verify_token":"..."})。Connect 拿到 token 后自动调用 GET /api/v3/user/me(Bearer 鉴权)解析 bot 账号(id/username/nickname),存到 adapter 并随 GET /admin/channels 账号的 botId/botUsername/botNickname 显示;user/me 失败仅记日志、bot 身份为空,不中断连接/收发。token 只在 header / 写文件,不打印、不进日志。
  • PUT /admin/channels/{platform}/config(owner-only,审计)body {id?,token?,baseURL?,remoteURL?,listenAddr?,mode?,accessToken?,selfID?,config?,allowGroups?,denyGroups?,allowUsers?,denyUsers?,groupMsgPerMin?,msgPerMin?,maxMessages?} → 201 {data:{persisted:true,path,platform,accountId}}。真实持久化 + 热更新(design/04 §3.1,无需重启):读当前有效配置(channels.json→env 兜底)→ UpsertChannel 该平台账号 → 落盘 channels.json(本地、gitignore,0600)→ bridge.Reload(platform) 热替换该平台 adapter(进行中消息用旧、新消息用新);token 写入文件但永不在响应/日志回显(GET 列表为掩码)。platform 必填;空配置回 400;未注入渠道配置服务回 500 channels_unavailable、写盘失败 500 channel_persist_failed、热重载失败 500 channel_reload_failed。.env(CHANNELS_JSON / CHANNEL_*_TOKEN)为兜底。
  • DELETE /admin/channels/{platform}/config?id={accountId}(owner-only,审计)→ 200 {data:{persisted:true,path,platform,accountId}}。删除该平台指定账号 → 落盘 channels.json → bridge.Reload(platform) 热更新(该账号 adapter 优雅关停)。id 必填;未注入服务回 500 channels_unavailable。
  • GET /admin/channels/{platform}/messages?limit=&before=(owner/admin 读)→ 200 {data:[{id,sessionId,role,content,mode?,mood?,image?,platform?,deviceId?,senderName?,createdAt}]}。IM 适配器日志流(design/16 §4.6 §4):该平台所有非主人私聊/群聊消息(隔离会话 sess-xxx,非主会话 web)以消息流显示,senderName 标发送者昵称;未注入 HistoryStore 回 501。
  • POST /admin/plugins/install(owner-only + 审计 plugin.install)body {kind:"mcp",name,config:{transport:"stdio|http|sse",endpoint?|command?,args?,apiKey?,env?}} → 200 {data:{installed:true,name,kind:"mcp",transport,toolCount}}。v1 只装 MCP server(单二进制无法热装 Go 插件):真实 connect+discover,把发现的工具热注册进 capability registry(下次模型请求即注入,无需重启)→ 落盘 plugins.json(本地、gitignore、0600,apiKey 写入但永不回显/不落日志)→ 持有 live server 供后续刷新/卸载。同名重复安装 = 刷新(先卸旧再装新,失败回滚旧)。校验:name 必填、kind 非 mcp 回 400、transport 非法/stdio 缺 command/http|sse 缺 endpoint 回 400;连接/发现/注册失败回 502 plugin_install_failed 且不落盘(诚实,不静默装一半)。未注入 plugin manager 回 501 not_implemented。
  • POST /admin/plugins/uninstall(owner-only + 审计 plugin.uninstall)body {name} → 200 {data:{name,uninstalled:true,kind:"mcp"}}(已装 MCP:Unregister 移除其工具 + close + 删 plugins.json 条目;非 MCP/普通能力走原 registry.Remove,返回 {data:{name,uninstalled:true}});未命中回 404。
  • POST /admin/memories body {personaId?,content,scope?} → 201 {data:{id,scope,text}}。手动添加记忆(owner 私聊直写);scope ∈ user|persona|project,缺省 persona(与「记住…」同路径);无效非空 scope 回 400;未注入 memory.Store 回 501。
  • POST /admin/memories/search body {query,personaId?,topK?} → 200 {data:{query,personaId,semantic,hits:[{id,scope,text,score}]}}。真实检索(keyword×衰减);语义/向量路径需主会话注入 vector index(semantic 当前恒 false,诚实反映未接向量器)。
  • GET /admin/sessions/{id}/export → 200 {data:{session:{platform,channelId,sessionId,actorRole,owner,isGroup,memWritable}}};404。当前导出会话身份元数据(完整消息转录由 M10 提供)。

系统设置契约(POST /api/v1/admin/system/*,均 owner-only,审计;成功 {data} / 失败 {error:{code,message}}):

  • POST /system/revoke-keys(无 body)→ 200 {data:{revoked:n}}。吊销全部 API key:清空 key 表(先前签发的所有 key 立即失效),revoked 为吊销数量;未注入 API key 服务回 500 users_unavailable。
  • POST /system/rotate-jwt body {newSecret} → 200 {data:{rotated:true,note:"现有 token 已全部失效,需重新登录"}}。轮换 JWT 密钥:newSecret ≥32 位,替换当前签名密钥,先前 token 全部失效;newSecret 缺失/空白回 400;长度 <32 回 400;认证服务未配置回 500 auth_unavailable。新密钥永不回显、不落日志。
  • POST /system/backup(无 body)→ 200 {data:{path,size}}。备份数据:经 SQLite VACUUM INTO(含已提交 WAL 内容)快照到 <dataDir>/backup-<ts>.db,path 为备份文件绝对路径、size 为字节数;数据库路径未配置回 500 backup_unavailable、快照失败回 500 backup_failed。
  • POST /system/restore body {path} → 200 {data:{restored:true,backup,note:"已安排恢复,需重启服务后生效"}}。恢复数据:path 须为 dataDir 内的备份文件(路径穿越防护——filepath.Rel 校验,.. 逃逸回 400);文件不存在回 404;先自动备份当前数据库(backup 为快照路径)再替换 DB 文件;运行进程持有 DB 句柄,替换后需重启服务生效;数据库路径未配置回 500 backup_unavailable、替换失败回 500 restore_failed。

知识库 RAG 契约:

  • GET /admin/rag → {"data":{"documents":[{id,name,desc?,chunks,embedded}],"hits"?:[{docId,docName,chapter?,text,score,semantic}]}}。hits 仅在携带 ?query= 时出现;embedded 反映部署是否配置向量增强(未配置为待嵌入)。
  • POST /admin/rag body {"name","desc?","content"} → 201 {"data":{"name"}}。导入文档:后端分块(空行分段,超长段按固定长度切)并落项目索引(项目+章节),立即可被关键词/行上下文检索;未注入 RagStore 回 501。语义/向量嵌入为优雅降级(知识库 Store 不直属向量索引,导入文档走 keyword/line 路径;语义检索仍对既有 project-linked 记忆生效)。
  • POST /admin/rag/query body {"query":"..."} → {"data":{"query":"...","hits":[{docId,docName,chapter?,text,score,semantic}]}}。检索走项目索引关键词+行上下文;配置 pgvector/向量增强时并入语义/向量命中(semantic:true)。
  • POST /admin/rag/delete body {"id":"..."} → 200 {"data":{"id":"...","deleted":true}}。真实删除:从项目索引删除该文档条目并级联删除其全部章节(单事务),删除后 GET /admin/rag 不再出现、检索不再命中。id 缺失/空白回 400;文档不存在回 404 not_found;未注入 RagStore 回 501(owner-only + 审计 rag.delete)。

可观测性契约(GET /api/v1/admin/observability/*,均需 owner/admin;metrics/trace/audit 的 ?limit=N 缺省 50、上限 500):

  • GET /metrics → {"data":{"totalRequests","qps","avgLatencyMs","maxLatencyMs","perMethod{}","perPath{}","statusCounts{}","llmCalls","subagentEvents"}}。qps 为近 1 秒窗口计数;llmCalls/subagentEvents 由实际观测到的 trace span 按 kind 累计(Tracer 未注入时为 0)。Token/成本后端不统计、不返回(不伪造指标)。
  • GET /trace?limit=100 → {"data":[{id,parentId?,name,kind,status,startedAt,endedAt?,durationNs,props?,events?]}。kind ∈ llm_call|tool_call|orchestration|state_transition|subagent|memory|guardrail;durationNs 为纳秒;events 为 [{time,type,props?}]。
  • GET /audit?limit=100 → {"data":[{id,who,what,when,result}]}。id 为整数自增;when 为 RFC3339;来源为 SQLite audit_logs 表(未注入 AuditStore 时退化为进程内存)。
  • GET /logs?level=&limit= → {"data":[{time,level,source,msg}]}(后端日志 tail,v0.35)。
    • 来源:logger 进程内有界环形缓冲(容量 2000,满则丢弃最旧),只存 logger 已产生的记录,不额外打印、不重渲染、不落库;控制台/文件输出完全不受影响。source 取自 logger.WithSource 的 source= 属性(logger.Scoped 的记录回退为 module/sub),无来源记 ""。
    • 排序:time 倒序(最新在前)。time 为 RFC3339(UTC,秒精度),与 trace.startedAt / audit.when 一致;level ∈ debug|info|warn|error;msg 为记录消息。
    • 查询参数:level ∈ debug|info|warn|error(大小写不敏感)为最低等级过滤(如 level=warn 返回 warn + error),空/缺省 = 全部,未知值回 400 invalid_level;limit 缺省 200、上限 1000(小于等于 0 或非法值回落缺省)。日志量超过 limit 时按时间倒序截断(先按 level 过滤再取 limit,缓冲全量 2000 条内扫描)。
    • 诚实边界:组合根未注入日志环形缓冲(非二进制主链路启动)时回 501 not_implemented,不返回伪造/空的成功 tail。日志内容为 logger 已产出内容,敏感信息按既定规范本就不落日志(本端点不新增任何打印)。

通知聚合契约(GET /api/v1/admin/notifications,owner/admin 读;design/10 §3 #16):

  • GET /admin/notifications?type=&before=&limit= → 200 {"data":[{id,type,title,body?,source,createdAt}]},按 createdAt 倒序(同毫秒按 id 倒序稳定)。
  • 查询参数:type ∈ system|channel|backthink|audit(空/缺省 = 全部,未知值回 400 invalid_type);limit 缺省 50、上限 200;before 为 createdAt(epoch 毫秒)游标,含游标本身(createdAt <= before——两源时间戳精度不同(审计秒/纳秒、backthink 毫秒),页边界可能落在同一毫秒,客户端按 id 去重)。缺省/非法 before = 从最新开始。
  • 字段:id = <source>:<原生 id>(audit:<audit_logs.id> / backthink:<backthink_records.id>),跨页稳定、可去重;source ∈ audit|backthink;createdAt 为 epoch 毫秒(无法解析的审计时间戳记 0,不伪造时间)。title/body 由各自源派生:审计项 title = what(如 channel.open)、body = "<who> · <result>";backthink 项 title = "主动推送(<trigger> · 已送达|未送达)"、body = 结论摘要(≤200 rune,超出加 …)。
  • 类型归类(审计源按操作前缀派生):channel.* → channel;system.* / plugin.* / policy.* → system;其余审计操作(persona/user/memory/rag/session/upload/audio/…)→ audit;backthink 主动推送 → backthink。
  • 数据源与诚实边界:只聚合现有源(审计 audit_logs + backthink 记录),不新建 notifications 表。backthink 仅 shouldPush=true 的记录算通知(纯思考记录不推给主人);未注入的源 → 该类型为空,不伪造(无 backthink 服务即无 backthink 项);源报错回 500 notifications_failed(不静默少给)。本版为只读 feed:未读/已读游标后置(不建重表)。

3. WebSocket /ws/chat(实时)

  • 握手: ws://<host>:8080/ws/chat?token=<JWT> 或 Authorization: Bearer;未认证在握手前回 401 JSON。

  • 下行(服务端→客户端): 经 bus 广播 {type:"<event>", data:..., turnId?, seq?, sessionId?}(turnId/seq 为流式关联字段,见 §3.1;sessionId 为会话持久化(design/16)新增,能确定会话的事件带上,供客户端按激活会话过滤;v0.36 起结构化流帧 message.* / thinking.* / tool_call.* / tool_execution.update 也带 sessionId + turnId——多会话并发流式时客户端据此把 delta 路由到激活会话,见 §3.1)——message.start/delta/end / thinking.start/delta/end / tool_call.start/delta/end / tool_execution.update(结构化流,design/04 §1.1),以及向后兼容的 stream_chunk / stream_end / thinking(思维链) / mood(情绪变化,见 §3.7) / reminder / device_update / tool_used(工具回显) / subagent_event / graph_update / message_persisted(会话持久化增量,见 §3.3)/ message_edited、message_deleted(消息编辑/删除增量,见 §3.8)/ session_read(已读游标推进,见 §3.9)。会话编排状态(design/15,均为广播,data.sessionId 区分会话):session_busy / pending_ack / pending_drain / session_idle / session_interrupted / pending_full(见 §3.2)。语音回复走 voice_output(定向回请求连接,非广播)。

    history 回放事件(design/16,新增):WS 握手升级成功后、该连接下一帧即收到 {type:"history", sessionId:"web", data:{messages:[{id,sessionId,role,content,mode?,mood?,image?,platform?,deviceId?,createdAt}]}}——回放主会话最新 50 条(thinking 已剥离,不泄给非 owner;默认主会话,次级会话由客户端按激活会话走 GET /sessions/{id}/messages 拉取)。客户端据此在重连/新开连接后补全上下文。消息体含 senderName?(第三方昵称,design/16 §4.6,主人私聊为空)。

3.1 结构化流事件(message.* / thinking.* / tool_call.* / tool_execution.*)

  • 事件壳:{type, sessionId?, turnId?, seq?, data}, data 为类型化结构(非 any)。seq 为 hub 级单调递增序号,供客户端将一次突发的流式事件排序关联。sessionId 为事件所属会话、turnId 为该轮助手回合 id(均 v0.36 起带上):sessionId 由 loop 在回合边界注入(模型适配器在 loop 之下、不感知会话,恒留零值;loop 外无会话上下文的事件该字段为空),turnId 由网关每个回合新生成并挂进运行上下文——同一会话同时只有一个 in-flight 回合,故同轮内所有帧的 turnId 一致。客户端据此把 message.delta / thinking.delta / tool_call.* / tool_execution.update 过滤到 sessionId === activeSessionId 的会话,忽略非激活会话的帧(多会话并发流式不再串话)。
  • message.start({index}) / message.delta({delta, index}) / message.end({index}):正文段。delta 为正文 token 增量;客户端对 message.delta 累加到当前消息正文;message.start/end 划定正文段边界,不结束整轮(结束由 stream_end 负责)。
  • thinking.start({index}) / thinking.delta({delta, index}) / thinking.end({index}):思维链段(前端折叠块)。thinking.delta 累加增量。
  • tool_call.start({id, index}) / tool_call.delta({id, index, delta}) / tool_call.end({id, index}):工具调用中间态;tool_call.delta 携带参数增量(如 Anthropic input_json_delta)。
  • tool_execution.update({id, index, delta}):工具执行进度/局部输出(保留,工具循环 §1.2 后置实现)。
  • 向后兼容:未映射/无法确定的事件仍走 stream_chunk + stream_end(非流式回退路径)。
  • 上行(客户端→服务端): mode_switch / chat(跑 loop 并广播流式) / voice_input / subagent_ask / task_output_query / interrupt。
  • interrupt(上行,客户端→服务端):{sessionId} → 会话编排器中断(design/15 §3)——取消该会话当前在跑的任务(context cancel)、清空其待处理队列、回 session_interrupted;用户说「停下/算了/换个话题」时发。命中空闲会话为 no-op。
  • voice_input(上行,客户端→服务端):_{audio?, format?, text?, sessionId?, deviceId?}。audio 为 base64(或 data: URI)音频,format 为其编码(wav/mp3/ogg);text 为已转写文本时跳过 STT;deviceId 为客户端具体端标识(design/16 §4.5,如 voiceassist:myapp:v1,可选),随该语音轮次落库、聊天气泡可标注来源。服务端走 STT→agent 聊天→TTS(design/06 §1),回复经 voice_output 定向回该连接。真 STT/TTS 已接通阿里百炼(DASHSCOPE_API_KEY 必填:STT=qwen-audio-3.0-asr-flash、TTS=cosyvoice-v3-flash+longxiaochun_v3);无凭据时后端走降级链(cloud → edge-tts/espeak(未实现)→ silent 空音频)。无注入语音服务时回 error 帧(voice unavailable)。
  • voice_output(下行,服务端→客户端):{audio, format, text, reply, provider?}。audio 为 base64 合成音频,format 为其编码,text 为识别文本,reply 为助手回复文本;当合成音频为空(silent 降级)时带 provider(如 "silent",非空音频省略)——客户端据此提示「没生成声音」,但该帧仍照常发出、不算业务错误。
  • 向后兼容:未映射/无法确定的事件仍走 stream_chunk + stream_end(非流式回退路径)。

3.2 会话编排状态事件(design/15)

  • 触发:同一会话内,chat 到达时若「她忙」(正在跑 loop/工具循环/子代理/长回复)→ 新消息不阻塞、入该会话 pending;这一层的状态经以下事件推给前端,供「正在做 xx… 有新消息稍后回」轻状态展示。
  • session_busy:会话进入忙碌态({sessionId, mode},mode ∈ idle|thinking|executing|subagent)。
  • pending_ack:忙时新消息已入队({sessionId, mode, queueDepth})——「收到,稍后回」。
  • pending_drain:忙完开始消费下一待处理消息({sessionId, mode, queueDepth})。
  • session_idle:会话回到空闲、队列清空({sessionId, mode})。
  • session_interrupted:用户 interrupt 中断,当前任务取消、pending 清空({sessionId, mode, dropped})。
  • pending_full:单会话 pending 达上限(默认 5)拒绝入队({sessionId, mode, max})——防无限积压(design/15 §4)。
  • 上下文续接:忙完重开时新回合带上一轮上下文({mode, queueDepth} 之外的续接由服务端完成),非清空重来(design/15 §2.2)。

3.3 消息持久化增量事件(message_persisted,design/16)

  • 触发:每一条 user / assistant 消息落库后由编排层广播——Web 主会话路径走 persistUserMessages/persistAssistantReply,IM 渠道主人私聊(design/16 §4.4)由 channelbridge 把消息落主会话 web(带 platform 标记)后复用同一 message_persisted 广播(同一 hub 的 BroadcastPersisted),保证 Web 端实时镜像 IM 消息。
  • 载荷:{type:"message_persisted", sessionId, data:{message:{id, sessionId, role, content, mode?, mood?, image?, platform?, deviceId?, senderName?, createdAt}}}——role ∈ user|assistant;id 为后端生成的消息 id(客户端据此去重);platform 为来源平台(web 或渠道如 telegram/weixin-ilink/qq-obv11);deviceId 为具体端/客户端标识(design/16 §4.5,如 web:chrome:a1b2c3 / weixin-ilink:conf0:wx_uid);senderName 为第三方发送者昵称(design/16 §4.6,群聊/陌生人消息带,主人私聊为空,omitempty);thinking 已剥离(design/16 §6)。
  • 注:群聊/陌生人(非主人)消息只在该 IM 适配器日志界面以消息流显示,不经 message_persisted 进 Web 主会话(design/16 §4.6 §3-4);该日志流由 GET /api/v1/admin/channels/{platform}/messages 拉取,落库后另经 channel_persisted(§3.4)实时推送。
  • 客户端处理:仅当 data.message.sessionId === activeSessionId(激活会话)时处理;按 id upsert——本端已预建的同 role 乐观气泡(本地 uid 无后端 id)被归一(uid → 后端 id),异端无本地气泡则直接追加,两者最终收敛为同一条 id 相同的消息,「A 发消息、A 与 B 都无需刷新且各出现一次」。

3.4 适配器消息实时事件(channel_persisted,design/16)

  • 触发:每一条群聊/陌生人(非主人 / 隔离 sess-xxx)消息落库后由 channelbridge persistIsolated 广播;该消息只写自己的隔离会话,绝不写主会话 web,channel_persisted 仅是事件通知,不是把内容塞进主会话/Web 上下文(design/16 §4.6 §3-4)。
  • 载荷:{type:"channel_persisted", sessionId, data:{message:{id, sessionId, role, content, mode?, platform?, deviceId?, adapterId?, senderName?, createdAt}}}——与 historyMessage wire 一致;adapterId 为产生该消息的适配器注册名(如 kook:3bef...,adapter != '');sessionId 为隔离会话 id(sess-xxx,非 web);role ∈ user|assistant;senderName 为第三方昵称。
  • 事件类型分流(BroadcastPersisted 按 sessionId 分流):sessionId == "web" 或其 web:<uuid> 前缀 → message_persisted(Web 聊天实时同步);其它(隔离 sess-xxx)→ channel_persisted(适配器日志专用)。
  • 客户端处理:Web 聊天无 channel_persisted handler,天然忽略、不进会话;IM 适配器日志页订阅该事件,按 adapterId === 当前适配器(或总览 adapterId != '')过滤后仅追加(按 id 去重、不整页重载),首次拉取仍由 GET /api/v1/admin/channels/messages + 轮询兜底。

3.5 实时全链路流程事件(graph_update,design/19)

  • 触发:主会话 Web 每次会话轮次中,loop 的每个 trace span 开始/结束时由 loop.FlowPublisher 经 bus ws.graph_update 广播增量 FlowGraph。仅主人 Web 会话(sessionId == "web" 或其 web:<uuid> 前缀)发布——IM / 群聊 / 陌生人 / 子代理轮次不发布,防敏感信息泄向面板。管理员/主人 Web 端订阅后实时渲染「输入 → 思考(LLM) → 工具 → 输出」主干图。
  • 载荷:{type:"graph_update", data:{id, nodes:[{id,label,kind?,status?,startedAt?,endedAt?,props?,log?}], edges:[{from,to}], sessionId, startedAt, endedAt}}。
    • id 为 sessionId-round(每轮一图);sessionId 为会话 id;startedAt/endedAt 为 RFC3339。
    • kind ∈ input|llm|tool|subagent|memory|guardrail|orchestration|output。
    • status ∈ idle|running|done|error(running = span 在跑 / done = 成功 / error = 失败)。
    • props 仅含白名单摘要(如 stop_reason / error_kind / result_summary),工具原始参数与密钥绝不前送;长文本截断到 ~80 字。
    • log 为该节点累积的 span 事件 [{time,type,props?}](设计 §2「节点实时日志」)。
  • 客户端处理:graph_update → setFlowGraph(parseGraph(data))(src/lib/wsParse.ts),管理面板「全链路流程」页渲染 FlowGraphView,节点可点开看参数/结果/实时日志;无图时诚实显示「暂无流程」。

3.6 主动推送事件(proactive_message,design/12)

  • 触发:后台思考(backthink)自主决定「此刻主动发一条」时,经 SendProactive 投递给当前所有在线 owner/admin 连接(viewer / 未登录连接永不投递)。SendProactive 返回实际送达的连接数——0 时调用方回退 IM 桥(im_bridge),把该消息经渠道主动推送发给你(KOOK/微信等已接)。
  • 载荷:{type:"proactive_message", data:{platform?, channelId?, channelType?, content, contentType?, sender?, timestamp?, messageId?}}(由 adapter.UnifiedMessage 脱敏投影:不含 RawData / 媒体字节 / token,绝不让凭据漏到前端)。
  • 客户端处理:仅 owner/admin 收到;渲染成一条「她从某平台主动发来」的气泡(内容 + platform + sender + timestamp)。与 message_persisted 的区别:message_persisted 是你与她的对话回显;proactive_message 是她未经你输入主动推的消息(如 IoT 完成事件、后台思考结论),两路不同、不要混用去重。
  • 红线:owner-only;payload 已脱敏;backthink 每次主动推送只发一个决策平台(决策渠道→最近活跃→旋转回退),Web 端与 IM 桥为两条投递路,客户端不要据此断言「所有平台都发了」。
  • 推送出口(D1,对客户端无新端点):在 WS/IM 投递之外,后端会把主动消息异步镜像到「推送出口」——主通道是主人自建统一推送服务(PUSH_EXPORTER_URL webhook,HTTP POST JSON {source,title,content,platform,timestamp},可带 Authorization: Bearer <PUSH_EXPORTER_TOKEN>),回退是个推 Getui(字段预留、SDK 未接入)。该出口只影响出站投递,不改变 WS 事件契约;未配/不可达时只记日志(注明「个推回退待接入」),不影响上面的 WS/IM 投递。移动端接入自建推送服务即可收到主人主动消息。

3.7 情绪事件(mood,design/01 §2 情绪状态机 + 好感度)

  • 触发:每轮对话结束(loop 回合边界,每轮一次、非每步)把该轮用户消息的情绪信号应用到该角色自己的情绪状态机(按 persona 隔离),且状态真的变化时才广播一次 mood;状态不变(含「一直 calm」)不发,避免每轮噪声。
  • 载荷:{type:"mood", data:{mood, personaId, affection}}——mood ∈ calm|happy|excited|sad|angry(情绪状态机 state id,与消息体 mood 同源同值);personaId 为该角色 id(情绪/好感度跟随角色,不跟随行为模式);affection 为该角色当前好感度(受角色配置 affection.min/max 约束)。角色级数值,不带 sessionId。
  • 与消息体 mood 的关系:assistant 消息体的 mood 是该轮结束后的当前情绪(每轮都写,见 §3.3),而 mood 事件只在变化时发——前者答「这条回复时她什么心情」,后者答「她的心情刚刚变了」。
  • 群聊/陌生人(MemWritable=false):情绪只读不写——第三方消息不改变角色情绪与好感度、不发 mood 事件、该轮 assistant 消息 mood 为空(与记忆只读同一隔离边界,design/02 §3 / design/11 §2)。
  • 状态转移与衰减:按角色配置的转移表(缺省内置表)与 emotion.decayRate——有情绪信号的轮次沿合法转移走一步指向对应状态;无信号轮次衰减一步趋向 calm;decayRate ≤ 0 关闭衰减。好感度按角色 affection.delta 随情绪轮次增减(warm +delta / cold −delta,clamped 到 [min,max])。
  • 客户端处理:mood → setMood(data.mood)(src/lib/ws.ts),顶栏/侧栏显示「心情 · 」;消息体内 mood 由聊天气泡显示(chat.tsx)。
  • 状态持久性:情绪与好感度为进程内、按角色状态,重启回到角色配置的 initialState(design/01 未要求落库;如需跨重启保留,待主人确认后加表)。

3.8 消息编辑 / 删除实时事件(message_edited / message_deleted,D3 实时同步)

  • 触发:PATCH /api/v1/messages/{id}(改 content)或 DELETE /api/v1/messages/{id} 成功写库后由网关广播(与 message_persisted 同一条 hub 广播路径,全连接广播 + 客户端按会话过滤——hub 无按会话订阅;消息体的唯一真源始终在后端,这两个事件只驱动渲染同步,不携带新真源)。
  • message_edited:{type:"message_edited", sessionId, data:{id, content, mode?, mood?, createdAt?, ...}}——data 为编辑后的完整消息 DTO(与 §3.3 的 message.message 同形,至少含 id/content/sessionId/createdAt,另带 role/mode?/mood?/image?/platform?/deviceId?);thinking 已剥离(与 message_persisted 一致)。只改 content,role/时间戳/平台等保持原值。
  • message_deleted:{type:"message_deleted", sessionId, data:{id}}——只带被删除消息的 id(sessionId 为该消息所属会话,由后端在删除前读取后填入;客户端按它过滤,不据此删除别的会话的行)。
  • 客户端处理:与 message_persisted 同一过滤规则——仅当 sessionId === activeSessionId 时处理(message_deleted 的 sessionId 缺失时按 id 兜底:消息 id 全局唯一,只会命中它自己那条)。message_edited → 按 id 原位更新该条的 content(不新增行:找不到该 id 即忽略,避免把别的会话的消息注入当前列表);message_deleted → 按 id 从列表移除。两个操作都不触发整页重拉——这正是它相对「重拉绕行」的收益。
  • 失败不广播:404(消息不存在)/ 400(内容非法)/ 501(未注入 HistoryStore)不产生任何事件,客户端不会因一次失败请求而丢掉本地消息。

3.9 已读游标事件(session_read,D4)

  • 触发:POST /api/v1/sessions/{id}/read 成功推进游标后由网关广播(与 message_persisted 同一条 hub 广播路径:全连接广播 + 客户端按 sessionId 过滤,hub 无按会话订阅)。用途是同账号多端清红点:一端读了,其它端不用重拉列表就把该会话的未读清零。
  • 载荷:{type:"session_read", sessionId, data:{readAt}}——readAt 为该会话推进后的已读游标(毫秒 Unix 时间戳,与该会话消息的 createdAt 同单位)。
  • 客户端处理:仅当 sessionId === activeSessionId(或该会话在自己维护的会话列表里)时处理 → 把该会话 unreadCount 置 0(更精确的做法是按 readAt 重算本地未读;本地没有更完整的数据时直接清零也不会错,因为 readAt 只前进)。它只驱动渲染:唯一真源仍是后端 chat_sessions.read_at,漏收该帧的端在下一次 GET /api/v1/sessions 自然收敛(不需要补发、不需要重连)。
  • 失败不广播:400(before 非法)/ 404(会话不存在)/ 501(未注入 HistoryStore)不产生事件,其它端不会误清红点。

4. 客户端接入指引(Web / 移动 / 原生 / IM bridge 通用)

4.1 一次性接入流程(三步走)

  1. 拿令牌:POST /api/v1/auth/login {username,password} → {data:{token,expiresAt,role}}(JWT,默认 24h,role ∈ owner|admin|viewer)。
  2. 声明版本(可选/advisory):可带 X-API-Version: 1 + Authorization: Bearer <token>;WS 握手用 ws://<host>:<port>/ws/chat?token=<token>(或 Authorization: Bearer)。X-API-Version 是advisory惯例头——服务端当前不校验、不据此协商版本,带不带行为不变;viewer 只能读会话/人格元数据;读写会话消息、管理写操作需 owner/admin。
  3. 带端标识(可选但推荐):每条消息/请求可带 deviceId(见 §4.3),让她知道你从哪个端来。

4.2 会话与消息用法(聊天)

  • 列出会话:GET /api/v1/sessions → [{id,type:"main"|"sub",title?,createdAt,pinned,unreadCount,lastMessage?}]。排序:置顶会话(pinned=true)最前,其次主会话 id="web"(她与你的主线,最大化继承上下文),再按创建时间升序;次级 web:<uuid> 由 POST /api/v1/sessions 新建(临时小事/专业问题,独立上下文回溯)。
  • 改名 / 置顶:PATCH /api/v1/sessions/{id} body {title?}(≤80 rune)或 {pinned?:bool}(至少一个字段)→ {data:{id,title?,pinned?}}(只回显变更字段)。
  • 未读 / 已读游标(D4):列表项的 unreadCount 直接用于会话列表红点(恒返回整数,0 = 无未读)。客户端进入会话、或滚到底看到最新消息后调用 POST /api/v1/sessions/{id}/read:body {"before":<毫秒>}(如「本次已渲染的最新一条的 createdAt」),或省略 body = 标记到现在。后端只前进地推进该会话唯一游标(单 owner 账号:读一处 = 所有端已读),并广播 session_read(§3.9)——同账号其它端免刷新清红点。列表 → 读 → 再看列表即可自洽(漏收广播的端下次拉列表自然收敛)。
  • 拉取激活会话历史:GET /api/v1/sessions/{id}/messages?limit=50&before=<ms>&platform=&q=。
    • 热分页:limit 默认 50(上限 200),before = 当前最旧一条的 createdAt(毫秒),向上翻页。platform 可选(IM 适配器传本平台只拉本渠道;Web/通用客户端不传 = 全量共享主会话)。
    • 搜索:q 非空 = 会话内内容搜索(倒序,仍支持 before/limit 分页);空 q = 普通分页;q 与 after 互斥。不跨会话。
    • 返回按时间倒序,需 reverse() 成旧→新用于渲染。
  • 编辑 / 删除单条消息:PATCH /api/v1/messages/{id} {content}(≤4000 rune)→ {data:<消息DTO>}(裸 DTO,无 message 包裹);DELETE /api/v1/messages/{id} → {data:{deleted:<id>}}。不存在回 404。重发用「编辑后内容再 POST /chat」即可(无独立重发端点)。两者成功后都会广播(message_edited / message_deleted,§3.8),同账号其它端免刷新同步。
  • 实时增量:连接 WS 后,消息落库会广播 message_persisted(§3.3)——用其中的 id 去重,sessionId 匹配激活会话才渲染;新开/重连连接首帧即 history 回放(主会话最近 50 条)。消息被编辑/删除则分别广播 message_edited / message_deleted(§3.8)——同一 sessionId 过滤规则,前者按 id 原位改 content、后者按 id 移除,都不需要重拉整页。
  • 发消息:POST /api/v1/chat {text, deviceId?, image?, stream:true}。stream:true 走 WS 流式;非流式返回完整 reply(会话正忙时 202 queued、排队满 409 chat_busy,见 §3.2)。

4.3 deviceId 与 platform 规范(design/16 §4.4-4.5)

  • platform(来源平台):web / 渠道如 telegram、weixin-ilink、qq-obv11。Web 端恒 web;IM 渠道消息带各自平台。同一平台多个端 → 相同 platform、不同 deviceId。
  • deviceId(具体端,客户端自己生成):Web 每个浏览器 localStorage 持久化 web:<browser>:<随机短id>(如 web:chrome:a1b2c3);IM 按 <im平台名>:<本地配置唯一id>:<用户平台唯一uid>;某 APP 可自定可读前缀。发送时带上,服务端落库消息,聊天气泡可标注来源。
  • senderName(第三方昵称):只有群聊/陌生人(非主人)消息带,主人私聊恒空(omitempty)。IM 适配器_log:该平台非主人私聊在 GET /api/v1/admin/channels/{platform}/messages 以消息流显示。

4.4 常见错误码 / 边界

  • 401 未认证/令牌失效 → 重新登录。403 权限不足(viewer 读管理写/主会话删除)。
  • 404 资源不存在(消息/会话 id 未找到)或端点未实现;501 not_implemented 后端明确未提供(如知识库删除、新增用户)。
  • 400 请求体/参数非法(limit/before 越界、空 content)。409 chat_busy 排队满。
  • 红线:身份判定永远来自后端 channel 层 actor 元数据 + 白名单,客户端不要用提示词/文本判身份;thinking(思维链)在公开消息/回放中已剥离,客户端不应依赖它。

4.5 离线 / 缓存建议(原生端)

  • PWA / 原生端缓存 sessions/personas 列表 + 最近一屏消息;重连先本地渲染再拉增量。
  • 消息真源在后端,本地只做渲染缓存;用 message_persisted.id 去重,不要用本地自增 id。
  • 语音:上行 voice_input(base64 音频或已转写 text),下行 voice_output(合成音频 base64 + text)。

5. 变更记录

  • v0.39(微信 iLink 会话过期可见化 + 不再假 OK,对客户端新增两个可选字段、一处错误码,向后兼容):① GET /api/v1/admin/channels 每平台行新增可选 needsRescan(bool,omitempty)+ lastError(string,omitempty)——仅当适配器真的要求重新授权时出现(今天只有微信 iLink 的 errcode=-14 过期 token 这一种),取值来自适配器新增的可选能力接口 RescanRequired() (bool, string),网关经类型断言读取(主 PlatformAdapter 契约不变;未实现该能力的平台字段缺省不出现)。用途:把「需要重新扫码」与「主人自己关掉的未连接」区分开——后者仅 connected:false。② POST /admin/channels/open 对「已过期」回 409 session_expired(message 明确要求用「扫码登录」重新绑定),不再吞掉错误回 200;409 needs_login(尚无 token,v0.8 起)不变。③ 适配器行为:iLink errcode=-14 进入显式过期态——IsConnected() 恒 false;Connect() 返回过期错误(不再对已过期 token 报成功);HealthCheck 区分「过期」与「未连接」;并以 ERROR 级日志写明「需要重新扫码登录」(只带 bot_id 等非敏感标识,token 永不入日志)。过期态在读循环与发送两条路径都会置位,并仅在扫码登录拿到新 token 或配置换了新 token(保存 + 热重载)时清除。④ 扫码登录侧:过期期间 login-status 报 failed + 过期原因(不再假报 confirmed,否则面板会把死 token 当成功重新落盘);login-qr 仍可正常出码(不回 already_logged_in,否则重新扫码这条路被堵死);get_bot_qrcode 不再携带已失效的 token(否则服务端以「已绑定」应答、不发新凭据)。向后兼容:只新增可选字段与一处更诚实的错误码;不读新字段、不处理 409 的客户端行为不变(此时面板显示「未连接」,仍可用「扫码登录」恢复)。详见 §2 行 6 与「微信 iLink『会话过期』契约」。
  • v0.38(B4 客户端共享枚举权威表 + 未知值回落约定,对客户端新增一个只读 REST 端点):新增 GET /api/v1/meta/enums(任意已认证角色可读,与 GET /sessions 同级;无 token 401)→ 200 {data:{platforms,moods,modes,unknown}},每项 {id,label}:① platforms(6)= web/kook/qq-obv11/weixin-ilink/telegram/webhook——即各内置适配器 Name() + Web 主会话来源 web;② moods(5)= calm/happy/excited/sad/angry,与 persona 情绪状态机内置状态一致(该状态机可扩展,故非封闭集合);③ modes(9)= chat/code/home/work/docs/voice/vision/agents/settings(design/07 §1);④ unknown 恒 "display-raw" = 未知值回落约定机器可读:客户端遇不在表中的取值原样显示原始字符串(不丢弃、不报错、不占位符),服务端新增取值视为非破坏性变更(forward-compatible)。单一事实来源在 Go 侧(internal/contract/enums),并有反漂移测试断言适配器 Name() / 情绪内置状态 / 前端九模式与三张表一致,漏登记即 CI FAIL。Web 侧镜像 frontend/web/src/lib/enums.ts(labelFor 未知值返回原值),顶栏/侧栏/消息气泡的「心情」标签已改用它渲染。向后兼容:仅新增端点,既有契约形状不变。详见 §1 行 15 与 §1.1。
  • v0.37(D4 未读状态 / 已读游标,对客户端新增一个语义字段、一个 REST 端点、一个下行事件):① chat_sessions 新增 read_at INTEGER NOT NULL DEFAULT 0(迁移 0010,down 可逆;毫秒 Unix 时间戳,与 chat_messages.created_at 同单位)。② 语义:会话级单一已读游标 + 单 owner 账号——同一账号所有端共用一个游标,读一处 = 全端已读(不做 per-device/per-user 游标,与「同一账号所有浏览器都属于同一 owner」一致);createdAt <= readAt 的消息算已读,未读数 = createdAt > readAt 的条数。③ GET /api/v1/sessions 每项新增恒返回的 unreadCount(整数,0 = 无未读):与 lastMessage 同一条 SQL 派生(关联子查询 COUNT(*) WHERE session_id = s.id AND created_at > s.read_at,走已有 idx_chat_msgs_session_created,无 N+1、不新增索引)。④ 新增 POST /api/v1/sessions/{id}/read(owner/admin,与 PATCH /sessions/{id} 同鉴权):body {before?}(可选毫秒;缺省= 现在)→ 200 {data:{sessionId,readAt}};before 为 0/负数/非数字/body 非对象 → 400,未知会话(含隔离 sess-xxx)→ 404,未注入 HistoryStore → 501。游标只前进不后退(read_at = MAX(read_at, before)),乱序/并发/重试请求幂等安全。⑤ 新增下行事件 session_read:{type:"session_read", sessionId, data:{readAt}},全连接广播 + 客户端按 sessionId 过滤,用于同账号其它端清红点;失败路径不广播(§3.9)。⑥ 升级回填:0010 把既有会话游标回填为「该会话现有最新消息时间」,升级当天不误报未读(既有历史全部视为已读)。⑦ HistoryStore(网关消费侧契约)新增 MarkSessionRead(ctx, sessionID, before);SessionMeta 新增 UnreadCount。向后兼容:仅新增字段/端点/事件,既有契约形状不变;不消费 unreadCount/session_read 的客户端行为不变。详见 §1 行 4/8a 与 §3.9。
  • v0.36(A1 流式帧附加会话 / 回合标识,对客户端新增两个可选字段、无端点变更):结构化流帧 message.start/delta/end、thinking.start/delta/end、tool_call.start/delta/end、tool_execution.update 现在带 sessionId + turnId(向后兼容:只新增帧字段,data 结构与既有事件形状不变)。sessionId 由 loop 在回合边界从运行上下文注入(模型适配器在 loop 之下、天然不感知会话,保持零值;loop 外无会话上下文时该字段为空);turnId 由网关每回合新生成(newTurnID)并经 loop.WithTurnID 挂进运行上下文,runTurn 是编排层与 legacy 直连路径唯一的回合收口点,两路都覆盖——同一会话同时只有一个 in-flight 回合,故同轮内所有帧的 turnId 一致。此前这几帧属于 design/16 §4.2 标注的「若知道就带上」缺口(v0.10 起记录):多会话并发流式时前端会把别的会话的 delta 接进当前会话气泡,现在客户端按 sessionId === activeSessionId 过滤即可路由到激活会话。stream_chunk / stream_end 的 sessionId/turnId 行为不变。详见 §3.1。
  • v0.35(可观测性收尾:后端日志 tail 端点,对客户端新增一个 admin 只读端点):GET /api/v1/admin/observability/logs?level=&limit=(owner/admin)→ 200 {data:[{time,level,source,msg}]},最新在前。数据源为 logger 新增的有界环形缓冲(internal/logger,容量 2000,满则丢最旧、并发安全、不阻塞、不改动既有控制台/文件输出),组合根在构建 logger 时挂上、在网关就绪后经 SetLogBuffer 注入;缓冲未注入回 501 not_implemented(不伪造空 tail)。time 为 RFC3339 UTC(秒精度),与 trace.startedAt/audit.when 一致;level ∈ debug|info|warn|error(大小写不敏感)为最低等级过滤,空 = 全部,未知值 400 invalid_level;limit 缺省 200、上限 1000。source 取 logger.WithSource 的 source=(Scoped 记录回落 module/sub),无来源为空串。详见 §2「可观测性契约」。
  • v0.34(D3 收尾:消息编辑 / 删除的 WS 实时广播,对客户端新增两个下行事件、无 REST 契约变更):PATCH /api/v1/messages/{id} 成功后广播 message_edited({type, sessionId, data:<更新后的完整消息DTO>},thinking 已剥离)、DELETE /api/v1/messages/{id} 成功后广播 message_deleted({type, sessionId, data:{id}});两者复用 message_persisted 的同一 hub 广播路径(全连接广播 + 客户端按 sessionId 过滤,hub 无按会话订阅),客户端按 id 原位改 content / 按 id 移除,不再需要「改/删后重拉」的绕行。message_deleted 的 sessionId 由后端在删除前读取消息所属会话填入(HistoryStore 新增 GetMessage;读不到时客户端按全局唯一的 id 兜底)。404/400/501 等失败路径不广播。Web 端已消费(lib/ws.ts + store/useAppStore 新增 updateMessageById/removeMessageById)。详见 §3.8。
  • v0.33(A2 记忆异步提取 / A4 可观测性 Tracing 接线 / A6 通知页后端):① A6 新增 GET /api/v1/admin/notifications(owner/admin 读,全新端点):把现有源(审计 audit_logs + backthink 主动推送记录)聚合为统一只读通知 feed,type/before/limit 过滤与游标见 §2「通知聚合契约」;不新建表,未注入的源该类型为空、不伪造;未读游标后置。② A4 trace 端点由恒空变真实数据(无契约变更,GET /admin/observability/trace 与 metrics 的 llmCalls/subagentEvents 从此反映 loop/工具/编排的真实 span):此前 TraceSink 从未接线、环形缓冲无人喂;现在组合根把 loop 产生的 span 扇出到网关观测缓冲。响应字段/形状不变。③ A2 LLM 异步记忆提取接线(对客户端无变更):loop 回合结束后改由 LLM 提取器后台异步抽取记忆(不阻塞回复、失败仅记日志),群聊/陌生人仍只读不写;无可用模型凭据时回落确定性 ManualExtractor(记住 X 路径不变)。
  • v0.32(A1 情绪系统接线:情绪/好感度真跑起来):后端把既有的 persona/emotion 状态机接进 loop 回合边界——每轮结束按该轮用户消息的情绪信号移动该角色自己的情绪状态机(沿配置转移表走一步)并调整好感度(按角色 affection.delta);无信号轮次衰减一步趋向 calm。① 消息 mood 不再恒空:assistant 消息落库带该轮情绪,message_persisted / history 回放 / GET /sessions/{id}/messages 的 mood 为真实值(user 消息不带)。② 新增下行 mood 事件({data:{mood,personaId,affection}},仅情绪变化时广播,见 §3.7),前端顶栏「心情 · x」与消息气泡接真数据。③ 隔离:群聊/陌生人(MemWritable=false)情绪只读不写——不改情绪/好感度、不发事件、该轮 mood 空。④ 持久性:情绪/好感度为进程内按角色状态,重启回 initialState(未落库;design/01 未要求)。触发词表与「每轮一步」为 design/01 未规定处的最小实现,待主人确认。契约新增(向后兼容):mood 事件 + 消息 mood 变真实值。
  • v0.31(Android 协同批次 ③b:B5 剩余 501 + D1 推送出口钩子):① B5-知识库删除由 501 改为真实实现:POST /api/v1/admin/rag/delete body {"id"} → 200 {data:{id,deleted:true}}——从项目索引删除该文档条目并级联删除全部章节(单事务);不存在回 404 not_found,id 空回 400,未注入 store 回 501;路由由 admin 收紧为 owner-only(与其它 admin 写一致)+ 审计 rag.delete。② B5-新增用户 / IoT 场景 / IoT 规则仍诚实 501:无用户表(登录仅 .env 单 admin)、IoT 无场景/规则模型与存储(iot.Provider 只有设备列表/控制/状态),错误消息明确缺什么,不伪造 store/假成功。③ D1 推送出口钩子:主动消息在 WS/IM 投递之外异步镜像到外部推送出口,自建统一推送服务优先 → 个推(Getui)兜底(个推 SDK 未接入,仅预留 GETUI_* 字段,不可达时日志注明待接入);.env 新增 PUSH_EXPORTER_ENABLED/URL/TOKEN/TIMEOUT_SEC,token 只走 Authorization 头、不落日志;对客户端无新端点(详见 §3.6)。
  • v0.30(Android 协同批次 ③a:D2 消息搜索 / D3 消息编辑删除 / D5 会话置顶):① D2:GET /api/v1/sessions/{id}/messages 新增可选 q——会话内内容搜索(大小写不敏感 ASCII LIKE,%/_/\ 转义为字面量),仍倒序 + before/limit 游标;q 与 after 互斥(同传 400),空 q 走普通分页;严格限定单会话,不新增跨会话搜索(避免群聊/陌生人隔离会话外泄,红线)。HistoryStore 新增 Search。② D3:新增 PATCH /api/v1/messages/{id}(body {content},非空 ≤4000 rune → {data:{message}},只改 content)与 DELETE /api/v1/messages/{id}(→ {data:{deleted:id}});不存在回 404 not_found;HistoryStore 新增 UpdateMessage/DeleteMessage + ErrMessageNotFound。不新增重发端点(客户端用编辑后的 content 再发)。③ D5:chat_sessions 新增 pinned INTEGER NOT NULL DEFAULT 0(迁移 0009,down 可逆);GET /sessions 排序改为 pinned DESC → 主会话优先 → createdAt ASC,会话 DTO 新增恒返回的 pinned bool;PATCH /sessions/{id} 扩展为 body {title?,pinned?}(至少一个字段,只回显变更字段,title-only 保持 A4 形状);HistoryStore 新增 SetPinned。全部为 owner/admin 写、读消息 owner/admin;未注入 HistoryStore 回 501。
  • v0.29(/admin/llm 读-改-写合并):POST /api/v1/admin/llm 由「全量替换」改为部分编辑合并——请求体字段用指针区分「未给」与「显式零值」;maxTokens / apiKey / provider / baseUrl / model / fallback / multimodal 省略即保留原值(不再把省略的 maxTokens 重置为 0),新组仍要求 baseUrl+model、非法 provider / 负 maxTokens 仍 400。面板 LLM 页新增每组 maxTokens 展示/编辑 + 新增/编辑表单;面板插件页新增「已装插件列表」+「安装 MCP 插件」表单(kind/name/transport/endpoint|command+args/apiKey/env),api.ts 请求体带 config(修掉旧 body 400)。runtimeconfig/models.go 新增 Find。
  • v0.28(MCP 插件热安装/卸载):POST /api/v1/admin/plugins/install(由 501 改为真实实现,owner-only + 审计)body {kind:"mcp",name,config:{transport,endpoint?|command?,args?,apiKey?,env?}} → 200 {data:{installed,name,kind,transport,toolCount}}——真实连接 MCP server、发现并热注册工具进 capability registry(下一轮模型请求即注入),配置落盘 plugins.json(gitignore、0600);连接失败回 502 且不落盘;同名重复 = 刷新。POST /api/v1/admin/plugins/uninstall 扩展:MCP 插件走 Unregister(移除工具 + close + 删配置),普通能力保持原 registry.Remove。新增 GET /api/v1/admin/plugins/installed(owner/admin 读)列出已装插件(apiKey/敏感 env 掩码 + live/toolCount)。mcp.MCPConfig 新增 APIKey(http/sse 以 Authorization: Bearer 发送)与 Env(stdio 子进程环境);plugins.json 于启动时 Restore 重新安装。契约版本 v0.28。
  • v0.27(LLM token 预算可配置化):models.json 模型组新增可选 maxTokens(int,缺省/0 = 未设),作为该组主/备链的 token 预算;GET /api/v1/admin/llm 的 AdminLlmProvider 与 POST /api/v1/admin/llm body/回显同步该字段(omitempty,未设为 0 时不出现)。.env 新增 LLM_FALLBACK_MAX_TOKENS(int,默认 8192,0 = 关闭兜底),只作用于确有回退模型时链末端的回退步。优先级:显式 Request.MaxTokens(审查/判定类小预算)> 生效组 maxTokens > LLM_FALLBACK_MAX_TOKENS(仅链末端、且组未设时)> 适配器默认(anthropic 8192)。单步链(无 fallback)不套用 env 兜底。maxTokens 为负回 400 bad_request。契约版本 v0.27。
  • v0.26(A5 消息正向增量):GET /api/v1/sessions/{id}/messages 新增 after 查询参数(<createdAt 毫秒>,升序返回 created_at > after 的消息,默认 limit=50/上限 200)——解决「WS 重连只回放主会话最近 50 条、断线超 50 条永久丢」的缺口;before 现行为不变;after 与 before 互斥(同传 400);负数/非数字 400。HistoryStore 新增 ListAfter(走 idx_chat_msgs_session_created,rowid 兜底同毫秒并列)。游标契约:客户端用「本页最后一条 createdAt」作下一页 after;同毫秒消息 > limit 时纯 createdAt 游标会跨页重读(超罕见,客户端按 id 去重兜底,与现有绕行一致)。
  • v0.25(B2 refresh token):POST /api/v1/auth/refresh(新增,{refreshToken} → 新 access + 新 refresh,默认单次使用旋转,并发重放 fail-closed invalid_grant)+ POST /api/v1/auth/logout(新增,撤单个或全设备)。POST /api/v1/auth/login 在 {token,expiresAt,role} 之外新增 refreshToken(不透明 256-bit 随机串,仅 SHA-256 哈希落库,AUTH_REFRESH_TTL 默认 720h,AUTH_REFRESH_ROTATE_DISABLED 关旋转)。新表 refresh_tokens(迁移 0008,id/user_id/role/token_hash UNIQUE/expires_at/revoked_at;含过期/轮换原子事务);rotate-jwt 成功即删除全部 refresh(返回 refreshTokensRevoked),revoke-keys 不动 refresh。密码/secret 记录见 internal/auth;refresh 全文绝不落日志/审计。
  • v0.24(B1 图片上传端点):POST /api/v1/upload(新增,multipart file,owner/admin+审计 → 201 {data:{url,size,mime,name}})+ GET /api/v1/files/{name}(新增,同端口静态服务)。安全:MaxBytesReader(5MB)、DetectContentType 嗅探(不信客户端 Content-Type)、白名单 raster image(拒 SVG/HTML)、crypto/rand 128-bit 随机名(丢弃原始文件名)、非可执行上传目录(0700)、nosniff;路径穿越全形态拒绝(含编码 %2f/%2e%2e)。错误码 400/413/415(internal/errors 增 ErrPayloadTooLarge/ErrUnsupportedMediaType)。设计取舍:GET 不鉴权(能力 URL,chat.image/<img> 无法带 Bearer),POST 仍 owner/admin。url 为绝对地址(X-Forwarded-Proto/TLS + r.Host,因 ParseImageRef 只认 http(s)://)。Deps.DataDir 由 wire 注入(resolveDataDir)。
  • v0.23(C2 语音 silent 降级信号):三处统一用 provider 只在「结果为空」时出现(非空/成功时才省略)——POST /api/v1/audio/transcribe 空文本响应体带 {text:"", provider:"silent"};WS voice_output 空音频 payload 带 provider;POST /api/v1/audio/speech 空音频仍 500 tts_empty 并加 X-Voice-Provider: silent 响应头(CORS Access-Control-Expose-Headers 已放开)。voice.Chain/Service 新增 STTWithProvider/TTSWithProvider(返回真实档位 cloud/local/edge-tts/espeak/silent),网关用可选接口 voiceProviderReporter 透传,自定义实现回退 silent。含 X-Voice-Provider(apperr 包装、providerSignal);DASHSCOPE_API_KEY 无/降级时信号为 silent。(go test -race 在本机受内核 TSAN VMA 限制失败,非本代码问题。)
  • v0.22(B3 会话列表 lastMessage 预览):GET /api/v1/sessions 与 /api/v1/admin/sessions 每个会话新增可选 lastMessage {content,role,createdAt}(content 折叠空白截前 60 rune + …;空会话省略),单条 SQL(LEFT JOIN 取每会话 ORDER BY created_at DESC,rowid DESC LIMIT 1,无 N+1,走 idx_chat_msgs_session_created 覆盖索引)。unreadCount 需已读游标设计,后置。
  • v0.21(A4 会话 title):PATCH /api/v1/sessions/{id}(新增,body {title} ≤80 rune → {data:{id,title}};空/超长 400、不存在 404、主会话可改名);会话首条用户消息到达时自动命名(history.append 前 ensureTitle:折叠空白取前 20 rune,纯空白回落 主会话/次级会话),仅首次命名、不覆盖。HistoryStore 新增 RenameSession;隔离会话(sess-xxx)不在 chat_sessions 有行、不自动命名(其本不在 /sessions 列表)。边界:前端 createSubSession(title?) 的 POST body 含 title 但后端暂忽略(会被首条消息自动命名覆盖),未处理。
  • v0.20(A3 消息 mode 落库):POST /api/v1/chat 与 ws.chat 上行新增可选 mode(行为模式,design/01 §3 / design/16 §5.1)。后端把每条持久化消息的 Mode 真实写入(IM 路径恒 chat;Web 路径经 ctx 透传本轮 mode,缺省回落 chat;恢复/续接路径沿用发起轮次的 mode),消息 DTO、message_persisted、channel_persisted 的 mode 不再恒空,供前端按模式渲染气泡(仅 chat 解析 <p> 拆气泡,其它模式原样整条)。
  • v0.19(百炼声音复刻 create_voice + TTS instruction 情绪驱动,主 TTS 用昔涟复刻音色):POST /api/v1/audio/voice-clone(新增,owner-only + 审计,body {url:"<公网参考音频URL>", prefix?} → {data:{voiceId}})——走后端 voice.DashscopeCloner(百炼 voice-enrollment create_voice,POST {base}/services/audio/tts/customization,input{action:create_voice, target_model:VOICE_TTS_MODEL, prefix, url},只收公网 URL 不收 base64,返回 output.voice_id;URL 不打日志不写审计)。POST /api/v1/audio/speech 新增可选 instruction(per-request 情绪/语气指令,如 "语气开朗俏皮,像女朋友撒娇"),经 ctx 透传给 TTS adapter;只对复刻/设计音色生效(cosyvoice-v3-flash 复刻音色可任意指令;系统音色 longxiaochun_v3 需固定格式,服务端 canInstruct 判定后永不发送)。config 新增 VOICE_TTS_INSTRUCTION(默认 "")、VOICE_CLONE_PREFIX(默认 cyrene,≤10 字母/数字);voice.Chain/voice.Service 新增 CloneVoice,VoiceCloner 接口(cloud rung 提供;未配 cloud 回 voice_unavailable)。⚠️ 真建昔涟音色仍待主人提供公网参考音频 URL(当前无),且 CosyVoice 声音复刻仅华北2北京地域(确认 key 地域);TTS instruction 用现有复刻音色或先仅单测请求体,避免改系统音色行为。
  • v0.18(后端语音接通阿里百炼 STT + TTS 主链路):POST /api/v1/audio/transcribe(新增,owner + 审计,body {audio,format?,deviceId?} → {data:{text}},走 STT);POST /api/v1/audio/speech(新增,owner + 审计,body {text,voice?,format?} → 音频字节 audio/<format>,走 TTS)——REST 语音源,原生/移动端经它绕开 WS 4KB 帧限上传/获取音频;WS voice_input 上行新增 deviceId(随语音轮次落库,design/16 §4.5)。后端 internal/runtime/multimodal/voice 的 cloud rung 由空 endpoint stub 改为真实百炼 adapter:STT qwen-audio-3.0-asr-flash(base64 原始字节→文本)、TTS cosyvoice-v3-flash(按 voice_id 合成,取非流式返回的 24h 音频 URL 下载字节)。实测(DASHSCOPE_API_KEY 实跑):TTS「你好,我是昔涟」→ 106124 字节 wav;把该 wav 喂给 STT → 文本「你好,我是西联。」(TTS 发音把「昔涟」读得近似「西联」,STT 识别正确);语音链 (cloud→…→silent) 返回真实字节/文本。已实测修正:cosyvoice-v3.5-plus/longxiaochun(v2 音色)走 HTTP SpeechSynthesizer 会回 418(引擎拒绝),改用 cosyvoice-v3-flash + longxiaochun_v3(v3 音色,各模型音色不可混用)。声音复刻(响昔涟音色)仍待:需参考音频(公网 URL)在百炼建自定义音色后把 voice_id 填入 VOICE_TTS_VOICE_ID(本项目不做 create_voice)。config 新增 DASHSCOPE_BASE_URL/VOICE_STT_\*/VOICE_TTS_\*(VOICE_TTS_SAMPLE_RATE 默认 24000)。
  • v0.17(系统设置安全与存储:吊销全部 API key / 轮换 JWT 密钥 / 备份 / 恢复,design/11 §4 + design/10 §3 模块14):新增 POST /api/v1/admin/system/{revoke-keys,rotate-jwt,backup,restore}(均 owner-only + 审计)。revoke-keys 走 auth.APIKey.RevokeAll()(清空 key 表,返回 {revoked:n});rotate-jwt 走 auth.JWTService.RotateSecret(≥32 位,新密钥不回显/不落日志,现有 token 立即失效);backup 用 SQLite VACUUM INTO 快照到 <dataDir>/backup-<ts>.db(含已提交 WAL 内容)返回 {path,size};restore 先校验路径在 dataDir 内(filepath.Rel 防路径穿越,逃逸回 400)→ 先自动备份当前 DB → 替换 DB 文件 → 返回 {restored:true,backup,note:"需重启后生效"}(运行进程持有 DB 句柄,热替换需重启可见)。gateway 新增 Deps.DatabasePath(composition root 传入实际打开的 SQLite 路径)。编辑环境变量 / 重启服务 / 检查更新 仍为后置,未实现、不伪造。
  • v0.17(KOOK 适配器面板 + 每适配器专属页,design/17 §2):GET /api/v1/admin/channels/{platform}/view、GET /api/v1/admin/channels/{platform}/friends?type=、POST /api/v1/admin/channels/{platform}/friend/handle、POST /api/v1/admin/channels/{platform}/message/test(新增,均 owner-only + 审计,带可选 ?accountId=,仅 kook 实现,其它平台 501);GET /api/v1/admin/channels 每条账号附 capabilities(面板按平台渲染专属卡片)。KOOK 面板:好友/申请、服务器/频道/成员、私信会话、发消息测试——经适配器 REST(Authorization: Bot <token>)拉真实数据,token 不打印不落日志,子项失败记入 errors 不伪造。
  • v0.16(运行时配置体系:渠道适配器配置落盘 + 热更新,design/04 §3.1):PUT /api/v1/admin/channels/{platform}/config(新增,owner-only + 审计)body {id?,token?,baseURL?,remoteURL?,listenAddr?,mode?,accessToken?,selfID?,config?,allowGroups?,denyGroups?,allowUsers?,denyUsers?,groupMsgPerMin?,msgPerMin?,maxMessages?} → 201 {persisted:true,path,platform,accountId}——读当前有效配置 → UpsertChannel → 落盘 channels.json(本地、gitignore、0600)→ bridge.Reload(platform) 热替换运行中 adapter(进行中消息用旧、新消息用新),无需重启;token 写入文件但不回显/不落日志。DELETE /api/v1/admin/channels/{platform}/config?id=(新增)删除账号并热更新。GET /api/v1/admin/channels 每条渠道附 accounts(每平台已配账号,token 脱敏掩码)。.env(CHANNELS_JSON/CHANNEL_*_TOKEN)退为兜底。
  • v0.15(运行时配置体系:LLM 多厂商多模型独立组落盘 + 热更新,design/04 §3.1):GET /api/v1/admin/llm 返回 models.json 的全部组(provider 协议字段新增,index 0 = 生效组;APIKey 仍脱敏不回显);POST /api/v1/admin/llm 由「临时记录」改为真实持久化 + 热更新(body {id?,provider,baseUrl,apiKey?,model,multimodal?,fallback?} → 201 {provider,persisted:true,path},读-改-写 models.json、保存组提升为生效组并热重载运行中的 LLM stack,无需重启)。.env 退为兜底(无 models.json 时按 LLM_PROVIDER/LLM_*/ANTHROPIC_* 合成单组)。
  • v0.14(IM 多会话隔离 + 第三方身份 + 黑白名单,design/16 §4.6):chat_messages 增 sender_name 列(迁移 0006,默认 '');消息 DTO(REST 消息、WS history 回放、WS message_persisted)均带 senderName(第三方发送者昵称,主人私聊为空 omitempty)。第三方身份边界:IM 群聊/陌生人会话在上下文构建时对第三方消息注入 [第三方 · <昵称>] 前缀(不改源消息正文,落库保留原文)+ 前置系统提示词"每个 IM 的非管理员账户 = 第三方人员,和主人无关"——身份判定基于 channel 层 actor 元数据 + 白名单(MemWritable=false 即群聊/陌生人),绝不靠 LLM 猜。会话隔离:群聊/陌生人消息落独立隔离会话(sess-xxx,带 platform+senderName,不并入主会话 web、不走 message_persisted 广播),供 GET /api/v1/admin/channels/{platform}/messages(owner/admin,management 面板渠道日志区)以消息流显示;前端主会话只显示各平台管理员(owner)私聊;群聊/陌生人记忆只读不写主会话(memory_gating 已按 MemWritable 门控)。多账号:CHANNELS_JSON 支持同一平台多个接入配置(每个 account 独立 session/账号,见 §2 渠道契约)。黑白名单:每平台每账号可配 allowGroups/denyGroups/allowUsers/denyUsers,channelbridge 入站即拦截(黑名单/不在白名单 → 丢弃,audit 可查,不进 LLM);白名单按 platform+account(群 id / 用户 uid)。
  • v0.13(每条消息带 deviceID/client 标识,design/16 §4.5):chat_messages 增 device_id 列(迁移 0005,默认 '');POST /api/v1/chat 请求体新增可选 deviceId(客户端生成的端标识,如 web:chrome:a1b2c3);消息 DTO(REST 消息、WS history 回放、WS message_persisted)均带 deviceId。platform(平台)与 deviceId(具体端)互补:同一平台多个端 → 相同 platform、不同 deviceId。Web 端由 lib/client.ts 用 localStorage 持久化每个浏览器一个的 web:<browser>:<随机短id>,发送时自动带上,聊天气泡默认小字标注来源(仅当 deviceId 存在才显示);IM 端按 <im平台名>:<本地配置唯一id>:<用户平台唯一uid> 构造(当前 UnifiedMessage 尚无 deviceId 概念,channelbridge 以 <platform>:<channelID>:<senderID> 占位,<本地配置唯一id> 待适配器提供后替换);Web 端聊天不回流 IM。
  • v0.12(IM 渠道并入主会话 + 平台标记,design/16 §4.4):chat_messages 增 platform 列(迁移 0004,默认 'web');GET /api/v1/sessions/{id}/messages 支持可选 ?platform= 过滤(Web 端不传=全量共享主会话;IM 适配器传本平台只拉本渠道);消息 DTO(REST 与 WS message_persisted)均带 platform。IM 渠道主人私聊(!IsGroup && Owner)经 channelbridge 落主会话 web(platform=<渠道>)并广播 message_persisted → Web 端免刷新镜像;群聊/陌生人保持独立隔离上下文、不落主会话。Web 端聊天(platform=web)不回流 IM。
  • v0.11(跨端实时同步,design/16 §4.2):WS 下行新增 message_persisted 事件——每条 user/assistant 消息落库后广播 {type, sessionId, data:{message:{id,sessionId,role,content,mode?,mood?,image?,createdAt}}}(id 为后端生成,thinking 已剥离)。客户端按 sessionId === activeSessionId 过滤 + 按 id upsert(含本端乐观气泡 uid→后端 id 归一)去重,实现「A 发消息、A 与 B 都无需刷新且各出现一次」。后端 history.Store 新增 AppendFull 供编排层取含 id 的落库消息。
  • v0.10(会话消息持久化 — WS 契约,design/16,后端-2):WS 下行帧新增可选 sessionId 字段(能确定会话的事件带上);新增 history 回放事件——WS 握手成功即回放主会话最新 50 条({type:"history", sessionId:"web", data:{messages:[...]}},thinking 已剥离)。stream_end/stream_chunk/会话编排状态帧(session_busy 等)在能确定 session 时带 sessionId;message.delta 等结构化流帧当时因 model.StreamEvent 无 session 上下文不带(即"若知道就带上"边界)——该缺口已在 v0.36 补齐(结构化流帧现在带 sessionId + turnId,见 §3.1)。
  • v0.9(会话消息持久化 + 主/次级会话,design/16,后端-1):新增 POST /api/v1/sessions(新建次级会话)、GET /api/v1/sessions/{id}/messages?limit=&before=(游标热分页拉历史,时间倒序)、DELETE /api/v1/sessions/{id}(删次级会话及其消息);GET /api/v1/sessions 在注入 HistoryStore 后改返回持久化会话元数据数组(主 + 次级,type 区分),未注入时保持原渠道会话列表向后兼容。读消息/写会话为 owner/admin(用户侧私有历史,thinking 不达 viewer);会话元数据列表保持任意认证角色可读。
  • v0.8(管理面板写操作 CRUD):补齐管理面板写端点(见 §2「管理面板写操作契约」,均 owner-only + 审计)——POST/PUT /admin/personas(真实注册 + 持久化 configs/personas/<id>.yaml)、POST /admin/users/apikey + DELETE /admin/users/apikey/{id}(真实签发/撤销)、POST /admin/iot/{id}/control(真实设备控制)、POST /admin/automations + /run + DELETE(真实注册/触发/移除规则)、POST /admin/llm(持久化 models.json + 热更新,v0.15 起)+ /admin/llm/test(真实模型调用)、POST /admin/channels/{open,close,test}(真实连接/断开 + 结构性身份识别)、POST /admin/plugins/uninstall(真实 registry 移除)、POST /admin/memories/search(真实检索)、GET /admin/sessions/{id}/export。无存储的写(新增用户 / IoT 场景·规则 / 插件安装)如实返回 501 not_implemented。
  • v0.7(会话消息编排 design/15):POST /api/v1/chat 非流式且会话「正忙」时改回 202 {data:{queued:true,queueDepth}}(原同步 {data:{reply}} 仍为空闲时返回);pending 满回 409 chat_busy。WS 上行新增 interrupt({sessionId},中断当前任务+清 pending);WS 下行新增会话编排状态事件 session_busy / pending_ack / pending_drain / session_idle / session_interrupted / pending_full(见 §3.2)。既有 stream_chunk/stream_end 与结构化流事件不变,契约向后兼容。
  • v0.6(可观测性):新增 GET /api/v1/admin/observability/{metrics,trace,audit}(owner/admin)——metrics 返回网关实时请求计数/QPS/延迟/方法·路径·状态分布 + LLM/子代理 span 计数;trace 返回 gateway 环形缓冲里的最近 span(经 TraceSink 对接 runtime tracer,映射为 AdminTraceSpan);audit 返回持久化审计(SQLite audit_logs,未注入时退化为内存)。llmCalls/subagentEvents 由实际观测 span 累计;Token/成本后端不统计、不返回(不伪造指标)。
  • v0.5(批次B·视觉真推理 + 语音端):POST /api/v1/chat 增 image 字段(base64 data: URI 或 URL),入 loop 视觉路由(vision/OCR 工具或主模型多模态,design/06 §0「先看图再对话」);WS 上行 voice_input 接 STT→聊天→TTS 管道,下行新增 voice_output(定向回请求连接,含合成音频 + 识别文本 + 回复文本)。
  • v0.4(结构化流):WS 下行新增 message.* / thinking.* / tool_call.* / tool_execution.update 结构化流事件(event shell 补 turnId/seq,data 类型化),对齐 OpenClaw AssistantMessageEvent;保留 stream_chunk/stream_end 向后兼容。
  • v0.3(M10):管理面板 /admin 落地——按 §2 契约封装 admin 只读端点(personas/sessions/memories/plugins/channels/iot)+ owner-only plugins/policy 写;其余 M10 端点以契约路径封装、后端未实现时前端显式"待实现"壳。
  • v0.2(M8):gateway 落地——登录/health/chat/sessions/personas + admin 只读端点 + plugins/policy 写 + WS /ws/chat(JWT + bus 广播)。
  • v0.1(契约先行):骨架 + 端点登记表。