Files
Cyrene_v2/Docs/api-reference.md
T
AskaEth 026b27b310
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(auth): refresh token 续期(B2) — 不透明哈希+单次旋转+/auth/refresh+/auth/logout
- auth/refresh.go: RefreshStore(Issue/Rotate/Revoke/RevokeAll/RevokeByUser), SHA-256哈希256-bit随机串, 单次旋转事务(并发重放fail-closed)
- 迁移0008_refresh_tokens(id/user_id/role/token_hash UNIQUE/expires_at/revoked_at + 索引)
- gateway: POST /v1/auth/refresh(旋转/续期) + /v1/auth/logout(单/全设备); login响应增refreshToken
- config: AUTH_REFRESH_TTL(720h)/AUTH_REFRESH_ROTATE_DISABLED; decode.go 支持time.Duration
- rotate-jwt成功后RevokeAll(响应refreshTokensRevoked), revoke-keys不动refresh
- 14测试+真实二进制e2e, build/vet/test绿+交叉编译
- Docs/api-client v0.25 + api-reference §2.3/2.4 同步
2026-09-08 21:45:00 +08:00

1168 lines
78 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Cyrene v2 后端 API 完整参考(api-reference.md)
> **用途**:本文档是 Cyrene v2 **全部后端 API(REST + WebSocket)的唯一详细参考**,供 Web 前端 / 移动端 / 原生客户端 / IM 桥接等**跨平台客户端开发者**使用。所有字段名、取值、鉴权规则、状态码均**以代码为准**(`internal/services/gateway/*.go`)。
> **主文档关系**:`Docs/api-client.md` 为面向使用者的**摘要**;`Docs/api-reference.md` 为**超详细主文档**。
> **维护铁律**:每实现/变更一个端点或 WS 事件必须同步更新本文档;契约变更版本化(REST 走 `/api/v1`,未来破坏性变更升 v2),不得破坏既有客户端。
> **当前版本**:以代码为准(gateway 已在 M0–M16 若干里程碑落地;详见各端点「里程碑」注记)。
---
## 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 均可连接),但 `history` 回放与 `proactive` 推送等按角色处理(见 §6)。
- **API Key(`X-API-Key`)**:⚠️ **当前实现未接入**。认证/授权中间件 `Authorize` 只校验 `Authorization: Bearer <JWT>`。`POST /api/v1/admin/users/apikey` 会**签发**一个明文 API key(仅返回一次),`GET /api/v1/admin/users` 会列出其掩码,但**网关的鉴权中间件目前不接受 `X-API-Key` 头**(`auth.APIKey.Verify/Lookup` 仅在单元测试中调用)。本文档按代码如实标注:**所有端点实际鉴权 = Bearer JWT**。若未来把 API key 接入中间件,需同步更新本档。
### 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 流式与直连)。
- **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 |
### 1.3 管理后台(`/api/v1/admin/*`)
读(`owner|admin`)+ 写(`owner`,全程审计)。下表按注册顺序,标注鉴权与状态。
| # | 方法 | 路径 | 鉴权 | 状态 | 说明 | 里程碑 |
|---|------|------|------|:---:|------|:---:|
| 9 | GET | `/api/v1/admin/personas` | owner\|admin | 200 | 人格列表 | M8 |
| 10 | GET | `/api/v1/admin/sessions` | owner\|admin | 200 | 会话列表 | M16 |
| 11 | GET | `/api/v1/admin/memories` | owner\|admin | 200 | 记忆列表 | M8 |
| 12 | GET | `/api/v1/admin/plugins` | owner\|admin | 200 | 插件/能力列表 | M8 |
| 13 | GET | `/api/v1/admin/channels` | owner\|admin | 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 | 200 | IoT 设备列表 | M8 |
| 17 | GET | `/api/v1/admin/llm` | owner\|admin | 200 | LLM 多厂商多模型组(APIKey 不回显) | M8/M15 |
| 18 | GET | `/api/v1/admin/automations` | owner\|admin | 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 | 200 | 知识库文档列表(可带 `?query=` 并入命中) | M10 |
| 22 | POST | `/api/v1/admin/rag/query` | owner\|admin | 200 | 知识库检索 | M10 |
| 23 | POST | `/api/v1/admin/rag/delete` | owner\|admin | 501 | 删除文档(写为 M10,当前 501) | M10 |
| 24 | GET | `/api/v1/admin/observability/metrics` | owner\|admin | 200 | 网关请求指标 | M10 |
| 25 | GET | `/api/v1/admin/observability/trace` | owner\|admin | 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** | 501 | 安装插件(需源码清单) | M10 |
| 54 | POST | `/api/v1/admin/plugins/uninstall` | **owner** | 200 | 移除插件 | 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"}}`。
- **备注**:仅存活探针;不含依赖健康状态。示例:
```bash
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`) |
```json
{"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:
```bash
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 |
| `deviceId` | string | 否 | 客户端生成的端标识(design/16 §4.5,如 `web:chrome:a1b2c3`),落库到消息,可选 |
| `stream` | bool | 是 | `true` = 经 WS 触发流式(`publishChatStream` 发 `ws.chat`),**立即返回**;`false` = 同步返回完整回复 |
```json
{"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:
```bash
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`,主 + 子):
```json
{"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`):
```json
{"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:
```bash
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` 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 人格 id(`PersonaID`,回退 `ID`) |
| `name` | string | 显示名(`c.Name`) |
| `character` | string | 角色名(当前恒 = `name`) |
| `isDefault` | bool | 是否默认人格 |
| `mode` | string | 首个行为模式(`modes[0]`,未设置为 `""`) |
| `memoryNamespace` | string | 记忆命名空间(该人格自己的) |
| `avatar` | string | 视觉头像 URL(`omitempty`;该人格自己的,缺失则为空) |
| `chatBackground` | object | `omitempty`;`{landscape, portrait}` 双朝向壁纸,仅该人格自己的 |
```json
{"data":[{"id":"cyrene","name":"Cyrene","character":"Cyrene","isDefault":true,"mode":"normal","memoryNamespace":"cyrene","avatar":"/assets/cyrene.png","chatBackground":{"landscape":"/bg/b1.png","portrait":"/bg/b1p.png"}}]}
```
- **备注**:视觉资产按角色作用域读取,绝不返回别的角色的;缺失时前端回退中性默认。curl:
```bash
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 毫秒),向更旧翻页 |
| `platform` | string | 否 | - | 来源平台过滤(design/16 §4.4)。Web 端不传 = 全量共享主会话;IM 传本平台(如 `telegram`)只拉本渠道消息 |
- **响应**:`200` `{"data":[<historyMessage>...]}`,**按时间倒序**(newest first);需 `reverse()` 渲染。
`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 毫秒(兼作分页游标) |
```json
{"data":[{"id":"m1","sessionId":"web","role":"assistant","content":"你好","mode":"normal","mood":"calm","platform":"web","deviceId":"web:chrome:a1b2c3","createdAt":1753...}]}
```
- **错误**:`400`(`limit`/`before` 非法、`id` 缺失);`501` `{"error":{"code":"not_implemented","message":"会话历史服务未配置(未注入 HistoryStore)"}}`;`500` `{"error":{"code":"messages_failed",...}}`。
- **备注**:热分页(design/16 §2.2,§5.1)。`thinking` 只在本 owner/admin 端点可见;WS 回放/广播已剥离。curl:
```bash
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(空)。可从 JWT claims 取 `ownerID` 作为子会话归属。
- **响应**:`201` `{"data":{"sessionId":"web:<uuid>"}}`。
- **错误**:`501` `{"error":{"code":"not_implemented","message":"会话历史服务未配置(未注入 HistoryStore)"}}`;`500` `{"error":{"code":"session_failed",...}}`。
- **备注**:新建次级会话(`NewSubSession`),独立上下文回溯。curl:
```bash
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:
```bash
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..."}}
```
---
## 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:
```bash
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:
```bash
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:
```bash
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` 为能力类型(如 `plugin|channel|tool|mcp`);`allowInGroup/allowInStranger` 为可达性策略。curl:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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?}`。
- `provider` 协议 ∈ `openai|anthropic`;`name` 为友好标签(OpenAI/Anthropic 或组 id)。
- `enabled`:index 0 的组为生效组 = `true`。
- **API key 永不返回**(wire 无 key 字段)。
- 数据源:`models.json` 各组(index 0 = 生效组);无 `models.json` 时按 `.env` 合成单组。curl:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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。
- 来源:环形缓冲(默认 512 条),由 `ObserveSpan`/`TraceSink` 注入。curl:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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 场景存储为 M10 提供(当前 IoT 仅有设备列表/控制/状态)"}}`。**审计**:`iot.scenario`(error)。curl:
```bash
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`(同 5.8,`iot.rule`)。curl:
```bash
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:
```bash
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:
```bash
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:
```bash
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?"}`。`baseUrl`+`model` 必填;`provider` 空默认 `openai`(仅允许 `openai|anthropic`)。
- **响应**:`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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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
- **响应**:`501 {"error":{"code":"not_implemented","message":"插件安装需插件源码清单(编译二进制/YAML)为 M10 提供;卸载可直接调用 registry"}}`。**审计**:`plugin.install`(error)。curl:
```bash
curl -s -X POST http://<host>:8080/api/v1/admin/plugins/install \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"name":"x"}'
```
### 5.27 POST /api/v1/admin/plugins/uninstall
- **Body**(`pluginActionRequest`):`{"name":"<能力名>"}`(必填)。
- **响应**:`200 {"data":{"name":"<name>","uninstalled":true}}`——真实从 registry 移除。
- **错误**:`400`(name 缺失);`404`(不存在);`500`。
- **审计**:`plugin.uninstall`。curl:
```bash
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:
```bash
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` 缺省 `"cyrene"`;`topK` ≤0 取 10。
- **响应**:`200 {"data":{"query":"<query>","personaId":"<personaId>","semantic":false,"hits":[{"id","scope","text","score"}]}}`。真实检索(keyword×衰减);语义/向量路径需注入 vector index(`semantic` 当前恒 `false`,诚实反映未接向量器)。
- **错误**:`400`(query 缺失);`500`(memory store 未配置 / 检索失败)。
- **审计**:`memory.search`。curl:
```bash
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:
```bash
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:
```bash
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,"note":"现有 token 已全部失效,需重新登录"}}`。
- **错误**:`400`(newSecret 缺失 / 长度 <32,`"新密钥长度不足 32 位"`);`500`(认证服务未配置 / 轮换失败)。
- **审计**:`system.rotate_jwt`。**新密钥永不回显、不落日志**。curl:
```bash
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:
```bash
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:
```bash
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 帧广播给客户端。**WS 上行不鉴权角色门槛**,但特定下行按角色(`history` 回放对任何已认证连接,`proactive` 仅 owner/admin)。
### 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 可连)。
- **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` 已剥离**;回放仅在 `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"}}`。
| type | 数据负载 | 服务端行为 | 结果 |
|------|----------|-----------|------|
| `mode_switch` | 任意(如 `{"mode":"chat"}`) | 发布 `ws.mode_switch` → hub 规范化广播 | 下行 `mode_switch`(广播) |
| `chat` | `{sessionId?, text, image?, deviceId?}` | 发布 `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?`}。text+image 至少一项;`runner` 未注入或两者皆空 → 直接广播 `stream_end{stopReason:"idle"}`。主会话 `web` 绑定全量 env(`loop.OwnerEnv()`)。`chat` 进入时若该会话正忙 → 由编排器入队(见 §6.4),非阻塞。
- **`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 下行事件全表
> **来源标记**:`[direct]` 网关直接 emit;`[relay]` hub 从 bus `ws.>` 规范化(任意 `ws.X` 主题 → 类型 `X`);`[bridge]` 桥接 producer 主题;`[target]` 仅定向特定连接。
| type | 来源 | 数据(`data`) | 用途 |
|------|------|----------------|------|
| `history` | direct | `{messages:[historyMessage]} ` | 连接首帧回放主会话最近 50 条(thinking 剥离) |
| `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):
```bash
# 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` 端点**:网关未注册任何文件上传路由。图片通过 `POST /api/v1/chat` 或 WS `chat` 的 `image` 字段以 **base64 data: URI** 或 **http(s) URL** 传入,入视觉真推理路径(vision/OCR 或主模型多模态,design/06)。文档导入走 `POST /api/v1/admin/rag` 的 JSON `content` 文本。
- **无 REST 语音端点**:语音只走 WS —— 上行 `voice_input`(base64 音频或已转写 `text`),下行 `voice_output`(合成音频 base64 + 识别文本 + 回复文本,定向回请求连接)。REST 侧没有语音流。
- **图片 + 文本并存**:`image` 非空时 turn 变为「带图 + 可选文本」;`text` 空 + `image` 有 → 纯图像 turn。
- **敏感面**:`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 场景·规则/插件安装/RAG 删除)——诚实返回,不伪造成功 |
### 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×3)/管理写(人格/API key/设备控制/自动化/LLM 保存·测试/渠道连接·断开·身份测试/渠道配置·删除/KOOK 面板/微信扫码/记忆添加·检索/插件卸载/RAG 导入/系统设置 4 项)。
- **仍为后置 M10(返回 `501 not_implemented`,不伪造)**:新增用户、IoT 场景与规则、插件安装(需源码清单)、RAG 删除、主人档案保存、编辑环境变量/重启服务/检查更新、完整会话转录导出(当前导出身份元数据)。
- **语义/向量增强**:`memory.search` 的 `semantic` 恒为 `false`(未接向量器时诚实反映);RAG `embedded` 反映是否配置向量增强;配置了增强必须真实走语义/向量路径(红线,非空壳)。
- **API key 认证**:`X-API-Key` 当前未被 `Authorize` 中间件校验(见 §0.1 注);key 的签发/吊销/列表已真实生效。
### 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` 回放;上行 `chat`/`interrupt`/`voice_input`/`subagent_ask`/`task_output_query`/`mode_switch`;下行见 §6.4。
4. 身份判定永远来自后端 channel 层 actor 元数据 + 白名单(`configs/users/owner.yaml`),**客户端不要用提示词/文本判身份**;`thinking` 在公开回放/实时帧中已剥离,客户端不应依赖。
5. 发送消息带 `deviceId`(推荐)以便端标识落库;`platform` 用于 IM 渠道过滤。