# Cyrene v2 客户端 API 文档(用户侧) > **面向**: Web 前端 / 移动端原生客户端 / IM 桥接等**跨平台客户端开发者**。 > **维护铁律**: 每实现/变更一个 API(REST 端点、WS 事件、admin API)**立即更新本文件**;契约变更必须版本化,不破坏既有客户端。 > **状态**: gateway 已在 M8 落地核心端点;M9 前端按此开发。**`✅` = 已实现**,`🕐` = 规划。 --- ## 0. 基础约定 - **Base URL**: `http://:8080`(网关唯一对外端口;生产一般经反代/HTTPS)。 - **版本**: REST 全部挂在 `/api/v1/{resource}`;未来破坏性变更升 v2。 - **认证**: `Authorization: Bearer `(RBAC:`owner`/`admin`/`viewer`);管理端另支持 `X-API-Key`。 - **响应统一**: `{"data": ...}` / `{"error": {"code": , "message": }}`;状态码 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,jwt}}`(24h,owner) | | 2 | `/api/v1/health` | GET | 否 | ✅ | 健康检查 `{data:{status:"ok"}}` | | 3 | `/api/v1/chat` | POST | JWT | ✅ | `{text,image?,deviceId?,stream?}` → `{data:{reply}}`;`stream:true` 时经 WS 触发流式。`image` 为 base64 data: URI 或 http(s) URL,入视觉真推理(design/06);`deviceId` 为**客户端生成的具体端标识**(design/16 §4.5,如 `web:chrome:a1b2c3`,可选),落库到消息、便于主人知道从哪个端发来。**会话编排(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"|"sub"),title?,createdAt}]}`(主会话 `id="web"` 恒在首位,次级 `web:`);未注入时退化回渠道会话列表 | | 5 | `/api/v1/sessions` | POST | JWT(owner/admin) | ✅ | **新建次级会话** → `201 {data:{sessionId}}`(`sessionId="web:"`);未注入 HistoryStore 回 `501 not_implemented` | | 6 | `/api/v1/sessions/{id}/messages` | GET | JWT(owner/admin) | ✅ | **热分页拉历史**:`?limit=&before=&platform=` 默认 `limit=50`(上限 200)、`before`=上一页最旧 `createdAt`(毫秒),按时间倒序 `{data:[{id,sessionId,role,content,mode?,mood?,image?,thinking?,platform?,deviceId?,senderName?,createdAt}]}`;`platform` 为**可选**来源平台过滤(design/16 §4.4)——Web 端不传(=全量共享主会话),IM 适配器传**本平台**(如 `telegram`)只拉本渠道消息;`deviceId` 为**客户端具体端标识**(design/16 §4.5,如 `web:chrome:a1b2c3` / `weixin-ilink:conf0:wx_uid`);`senderName` 为**第三方发送者昵称**(design/16 §4.6,群聊/陌生人私聊消息带,主人私聊为空,`omitempty`);非法 `limit`/`before` 回 `400`;未注入 HistoryStore 回 `501` | | 7 | `/api/v1/sessions/{id}` | DELETE | JWT(owner/admin) | ✅ | **删除次级会话及其消息** → `{data:{deleted:}}`;主会话不可删回 `403 forbidden`;未注入 HistoryStore 回 `501` | | 8 | `/api/v1/personas` | GET | JWT | ✅ | `{data:[persona...]}` | ## 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,审计) | | 6 | `/api/v1/admin/channels` | GET | ✅ | 渠道列表/健康 + 每平台已配账号(脱敏 token 掩码,design/04 §3.1);已连接渠道的账号附 **bot 身份**(`botId`/`botUsername`/`botNickname`,如 KOOK `GET /api/v3/user/me` 自动获取) | | 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` 协议);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 | 🕐(写) | 删除知识库文档;写操作 M10,当前返回 `501 not_implemented` | | 15 | `/api/v1/admin/observability/metrics` | GET | ✅ | 网关请求指标(总数/QPS/延迟/方法·路径·状态计数/LLM·子代理计数) | | 16 | `/api/v1/admin/observability/trace` | GET | ✅ | 最近 trace span(环形缓冲,默认 512 条) | | 17 | `/api/v1/admin/observability/audit` | GET | ✅ | 审计记录(who/what/when/result,SQLite 持久化) | | 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}`——**现有 token 立即失效需重新登录**;新密钥**不回显、不落日志** | | 22 | `/api/v1/admin/system/backup` | POST | ✅(写) | 备份数据(owner-only,审计);用 SQLite `VACUUM INTO` 快照(含已提交 WAL 内容)到 `/backup-.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/logs` 仍🕐未实现(前端按契约封装、后端 404 时显示"待实现"壳,不报错)。**写操作已补齐**(见下方「管理面板写操作契约」):人格/API key/设备控制/自动化/LLM 测试/渠道连接与识别测试/记忆检索/会话导出/插件卸载**真实持久化或真实生效**;**系统设置安全与存储类**(吊销 API key / 轮换 JWT 密钥 / 备份 / 恢复)也已**真实实现**(见下方「系统设置契约」);仍为后置 M10:**新增用户**、**IoT 场景与规则**、**插件安装(需源码清单)**、**主人档案保存**、**编辑环境变量/重启服务/检查更新**、**知识库删除**——后端如实返回 `501 not_implemented`,不伪造成功。 **管理面板写操作契约**(`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/.yaml`);id 已存在回 `409 duplicate`。 - `PUT /admin/personas/{id}` body 同上(不含 id)→ `200 {data:PersonaSummary}`;不存在回 `404`。 - `POST /admin/users` → `501 not_implemented`(无持久用户表,当前为单 admin + APIKey 现状)。 - `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 无场景/规则存储)。 - `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?}` → `201 {data:{provider:{id,name,provider,baseUrl,model,multimodal,enabled,fallback?}, persisted:true, path}}`。**真实持久化**:读-改-写 `models.json`(本地、gitignore),保存组提升为**生效组**(index 0),并**热更新**运行中的 LLM provider(`path` 为落盘文件路径);API key 写入文件但**永不回显/不落日志**。`provider` 为空默认 `openai`;缺 `baseUrl`/`model` 回 `400`;未注入 LLM stack 回 `500 llm_unavailable`、写盘失败 `500 llm_persist_failed`、热重载失败 `500 llm_reload_failed`。`.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`(未注册渠道)/`500`(凭据缺失等连接失败)。 - `POST /admin/channels/test` body `{platform?,accountId,isGroup}` → `200 {data:{role,owner,isGroup,env,verdict}}`。**结构性判定**:channel 层 actor 元数据 × `configs/users/owner.yaml` 白名单,绝不靠提示词。 - **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` → `501 not_implemented`(需插件源码清单)。 - `POST /admin/plugins/uninstall` body `{name}` → `200 {data:{name,uninstalled:true}}`(真实从 registry 移除);`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 内容)快照到 `/backup-.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":"..."}` → 删除为后置 M10,返回 `501` `{"error":{"code":"not_implemented",...}}`。 **可观测性契约**(`GET /api/v1/admin/observability/*`,均需 owner/admin,`?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 时退化为进程内存)。 ## 3. WebSocket `/ws/chat`(实时) - **握手**: `ws://:8080/ws/chat?token=` 或 `Authorization: Bearer`;未认证在握手前回 `401` JSON。 - **下行(服务端→客户端)**: 经 bus 广播 `{type:"", data:..., turnId?, seq?, sessionId?}`(`turnId`/`seq` 为流式关联字段,见 §3.1;`sessionId` 为**会话持久化(design/16)新增**,能确定会话的事件带上,供客户端按激活会话过滤)——`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` / `reminder` / `device_update` / `tool_used`(工具回显) / `subagent_event` / `graph_update` / **`message_persisted`**(会话持久化增量,见 §3.3)。**会话编排状态(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, turnId?, seq?, data}`, `data` 为**类型化结构**(非 `any`)。`seq` 为 hub 级单调递增序号,供客户端将一次突发的流式事件排序关联。 - **`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?}`。`audio` 为 base64(或 data: URI)音频,`format` 为其编码(`wav`/`mp3`/`ogg`);`text` 为已转写文本时跳过 STT。服务端走 STT→agent 聊天→TTS(design/06 §1),回复经 `voice_output` 定向回该连接。无注入语音服务时回 `error` 帧(`voice unavailable`)。 - **`voice_output`**(下行,服务端→客户端):`{audio, format, text, reply}`。`audio` 为 base64 合成音频,`format` 为其编码,`text` 为识别文本,`reply` 为助手回复文本。 - **向后兼容**:未映射/无法确定的事件仍走 `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:` 前缀 → **`message_persisted`**(Web 聊天实时同步);**其它**(隔离 `sess-xxx`)→ **`channel_persisted`**(适配器日志专用)。 - **客户端处理**:Web 聊天**无** `channel_persisted` handler,天然忽略、不进会话;**IM 适配器日志页**订阅该事件,按 `adapterId === 当前适配器`(或总览 `adapterId != ''`)过滤后**仅追加**(按 `id` 去重、不整页重载),首次拉取仍由 `GET /api/v1/admin/channels/messages` + 轮询兜底。 ## 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. **声明版本**:后续每个请求头带上 `X-API-Version: 1` + `Authorization: Bearer `;WS 握手用 `ws://:/ws/chat?token=`(或 `Authorization: Bearer`)。**viewer 只能读会话/人格元数据;读写会话消息、管理写操作需 `owner`/`admin`。** 3. **带端标识**(可选但推荐):每条消息/请求可带 `deviceId`(见 §4.3),让她知道你从哪个端来。 ### 4.2 会话与消息用法(聊天) - **列出会话**:`GET /api/v1/sessions` → `[{id,type:"main"|"sub",title?,createdAt}]`。**主会话 `id="web"` 恒在首位**(她与你的主线,最大化继承上下文);次级 `web:` 由 `POST /api/v1/sessions` 新建(临时小事/专业问题,独立上下文回溯)。 - **拉取激活会话历史**:`GET /api/v1/sessions/{id}/messages?limit=50&before=&platform=`。 - **热分页**:`limit` 默认 50(上限 200),`before` = 当前最旧一条的 `createdAt`(毫秒),向上翻页。`platform` 可选(IM 适配器传本平台只拉本渠道;Web/通用客户端不传 = 全量共享主会话)。 - 返回**按时间倒序**,需 `reverse()` 成旧→新用于渲染。 - **发消息**:`POST /api/v1/chat` `{text, deviceId?, image?, stream:true}`。`stream:true` 走 WS 流式;非流式返回完整 `reply`(会话正忙时 `202 queued`、排队满 `409 chat_busy`,见 §3.2)。 - **实时增量**:连接 WS 后,消息落库会广播 `message_persisted`(§3.3)——**用其中的 `id` 去重**,`sessionId` 匹配激活会话才渲染;新开/重连连接首帧即 `history` 回放(主会话最近 50 条)。 ### 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::<随机短id>`(如 `web:chrome:a1b2c3`);IM 按 `:<本地配置唯一id>:<用户平台唯一uid>`;某 APP 可自定可读前缀。发送时带上,服务端落库消息,聊天气泡可标注来源。 - **`senderName`**(第三方昵称):只有群聊/陌生人(非主人)消息带,主人私聊恒空(`omitempty`)。**IM 适配器_log**:该平台非主人私聊在 `GET /api/v1/admin/channels/{platform}/messages` 以消息流显示。 ### 4.4 常见错误码 / 边界 - `401` 未认证/令牌失效 → 重新登录。`403` 权限不足(viewer 读管理写/主会话删除)。 - `404` 端点未实现;`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.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` 快照到 `/backup-.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 不打印不落日志,子项失败记入 `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::<随机短id>`,发送时自动带上,聊天气泡默认小字标注来源(仅当 `deviceId` 存在才显示);**IM 端**按 `:<本地配置唯一id>:<用户平台唯一uid>` 构造(当前 `UnifiedMessage` 尚无 deviceId 概念,channelbridge 以 `::` 占位,`<本地配置唯一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.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/.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`(契约先行):骨架 + 端点登记表。