From af61b8ccfe91ee5bd9ce496cc61266609803535b Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 26 Oct 2026 14:10:40 +0800 Subject: [PATCH] =?UTF-8?q?docs(contract):=20v0.96=20=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E5=8F=91=E9=9F=B3=E4=BA=BA=E7=BB=91=E7=94=9F=E6=95=88=E4=BA=BA?= =?UTF-8?q?=E6=A0=BC=EF=BC=88sessionId/=E4=B8=A4=E4=B8=AA=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=A0=81=EF=BC=89+=20=E7=AE=A1=E7=90=86=E9=9D=A2?= =?UTF-8?q?=E8=84=B1=E6=95=8F=E6=8A=95=E5=BD=B1=E4=B8=8E=20props.omitted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docs/api-client.md | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/Docs/api-client.md b/Docs/api-client.md index 951ad8c..c00c55a 100644 --- a/Docs/api-client.md +++ b/Docs/api-client.md @@ -1,6 +1,6 @@ # Cyrene v2 客户端 API 契约 -> **契约版本:`v0.95`**(上一版 `v0.94`,其条目见 §6;`v0.89` / `v0.88` / `v0.87` / `v0.86` / `v0.85` / `v0.84` / `v0.83` / `v0.82` / `v0.81` / `v0.80` / `v0.79` / `v0.78` / `v0.77` / `v0.76` / `v0.75` / `v0.74` / `v0.73` / `v0.72` / `v0.71` 及更早见本文件 §6,`v0.67` 及更早的基线已归档、只读:`Docs/archive/api-client.v0.65.md`)。 +> **契约版本:`v0.96`**(上一版 `v0.95`,其条目见 §6;`v0.89` / `v0.88` / `v0.87` / `v0.86` / `v0.85` / `v0.84` / `v0.83` / `v0.82` / `v0.81` / `v0.80` / `v0.79` / `v0.78` / `v0.77` / `v0.76` / `v0.75` / `v0.74` / `v0.73` / `v0.72` / `v0.71` 及更早见本文件 §6,`v0.67` 及更早的基线已归档、只读:`Docs/archive/api-client.v0.65.md`)。 > **读者**:Web 前端 / 移动端原生客户端(含 Android)/ IM 桥接等**跨平台客户端开发者**。 > **唯一真源 = 当前代码**(`internal/services/gateway/*` 等)。本文档由代码逐条核对得出;与代码不符时**以代码为准**,并回改本文档。 > **旧的超详细版已归档、只读**:`Docs/archive/api-client.v0.65.md`(412 KB,含 v0.28→v0.65 全部历史条目)与 `Docs/archive/api-reference.v0.65.md`(170 KB,后端全量字段级文档)。归档的原因是**结构性臃肿 + 历史条目并列 + 长期漂移**;本文档**不复制**那段历史,需要考古时去归档。 @@ -123,7 +123,7 @@ | U3 | GET | `/api/v1/personas` | 任意已认证 | 200 | 角色目录 `{data:[{id,name,character,isDefault,mode,memoryNamespace,avatar?,chatBackground?,mood}]}`。`mood` 为空 = 该角色暂无被跟踪情绪(不伪造)。**不含** admin 专属 `thinking` 旋钮。 | | U4 | GET | `/api/v1/meta/enums` | 任意已认证 | 200 | 共享枚举权威表 `{data:{platforms,moods,modes,unknown}}`;每项 `{id,label}`。`unknown` 恒 `"display-raw"`。**未知取值原样显示,不要丢弃/报错**(服务端新增取值是非破坏性变更)。详见 [§4.7](#47-枚举权威表)。 | | U5 | POST | `/api/v1/audio/transcribe` | **owner** | 200 / 400 / 500 | STT。body `{audio,format?,deviceId?}` → `{data:{text, provider?}}`。`provider` 只在结果为空/降级时出现(如 `"silent"`)。空 audio → `400`;未接语音 → `500 voice_unavailable`;识别失败 → `500 stt_failed`。**owner-only + 审计**。 | -| U6 | POST | `/api/v1/audio/speech` | **owner** | 200 / 400 / 500 | TTS。body `{text,voice?,format?,instruction?}` → **`200` 原始音频字节**(`Content-Type: audio/`,**非 JSON 信封**)。空 text → `400`;未接语音 → `500 voice_unavailable`;失败 → `500 tts_failed`;静默空音频 → `500 tts_empty` 且响应头 `X-Voice-Provider: silent`。**owner-only + 审计**。 | +| U6 | POST | `/api/v1/audio/speech` | **owner** | 200 / 400 / 500 | TTS。body `{text, voice?, format?, instruction?, sessionId?}` → **`200` 原始音频字节**(**非 JSON 信封**)。🔴 **v0.96 起发音人按 `sessionId` 的生效人格解析**(会话绑定 → 全局默认人格 → 空 ⇒ 配置的 `default`),**省略 `sessionId` 不等于没有角色**;请求体 `voice` 接受但不生效;产物恒 WAV,实际格式按字节嗅探(见 §6 v0.96)。空 text → `400`;未接语音 → `500 voice_unavailable`;失败 → `500 tts_failed`;人格无映射 → `500 tts_persona_unmapped`;连生效人格都没有且没配 default → `500 tts_no_character`;静默空音频 → `500 tts_empty` 且响应头 `X-Voice-Provider: silent`。**owner-only + 审计**。 | | U7 | POST | `/api/v1/audio/voice-clone` | **owner** | 200 / 400 / 500 | 声音复刻建音色。body `{url,prefix?}` → `{data:{voiceId}}`。参考 URL **绝不进日志/审计**。空 url → `400`;失败 → `500 voice_clone_failed`。 | | U8 | POST | `/api/v1/upload` | owner/admin | 201 / 400 / 413 / 415 / 500 | **图片上传**,`multipart/form-data` 字段 `file`(光栅图,**单文件字节 ≤ 5 MiB**,即 `maxUploadBytes = 5 << 20`)→ `{data:{url,size,mime,name}}`。`url` 是**绝对同源 URL**,可直接作 `chat.image`(该路径的 5 MiB 在**上传时**定死,与 `chat.image` 的字符长度判据无关,见 [§4.5](#45-图片--文档--语音链路))。嗅探裁决 png/jpeg/gif/webp/bmp(拒 SVG/HTML);原始文件名丢弃,用 128-bit 随机名。缺文件/空文件 `400`、超限 `413 payload_too_large`、非白名单 `415 unsupported_media_type`。**带审计**。 | | U9 | GET | `/api/v1/files/{name}` | **无需**(能力 URL) | 200 / 404 | 上传文件读取。`name` 仅接受「32 位小写 hex + 白名单扩展名」;路径穿越全形态拒绝。**GET 不鉴权**是该 URL 为 128-bit 随机能力地址(``/provider 无法带 token);任何持有 URL 者可取,勿外泄。 | @@ -1537,7 +1537,7 @@ curl -s -X POST -H "X-API-Key: $KEY" \ | `DOCUMENT_MAX_INJECT_CHARS` | 空 → 预算 1/8 | 单份附件注入上限 | | `GUARDRAIL_MAX_INPUT_CHARS` / `GUARDRAIL_KEYWORDS` | 空 | 请求侧字符上限 / 关键词护栏 | | `VOICE_STT_TIMEOUT_SEC` | `20` | **只约束 IM 渠道入站语音转写**;Web 客户端 `voice_input` 不受它约束 | -| `VOICE_TTS_VOICE_ID` / `VOICE_TTS_FORMAT` / `VOICE_TTS_MODEL` | 空 | `POST /audio/speech` 当前遵循的服务端配置(请求体 `voice`/`format` 为前向兼容接受但暂不生效) | +| `VOICE_TTS_VOICE_ID` / `VOICE_TTS_FORMAT` / `VOICE_TTS_MODEL` | 空 | ⚠️ **v0.96 起不再是发音人的真源**:发音人由**生效人格 → `$DATA_DIR/config/voice-tts.json` 的 `personas` 映射**决定(面板文件,零新 `.env` 键);请求体 `voice`/`format` 仍为前向兼容接受但不生效,产物恒 WAV | | `VOICE_TTS_INSTRUCTION` | 空 | 系统音色 `longxiaochun_v3` 固定格式、**不使用自由 instruction** | | `LLM_PROVIDER` / `LLM_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL` / `LLM_FALLBACK_MODEL` / `LLM_REASONING_KIND` 等 | 见模板 | LLM provider 组的 `.env` 合成组兜底(有有效 `models.json` 时被其覆盖) | | `DOCUMENT_GC_INTERVAL` / `DOCUMENT_RETENTION` | `1h` / `168h` | 文档 GC(`0`/负 = 关闭 GC) | @@ -1582,7 +1582,28 @@ curl -s -X POST -H "X-API-Key: $KEY" \ > 只保留最近若干版(**v0.60 起**);更早的全部历史见归档 `Docs/archive/api-client.v0.65.md` §5(v0.28 → v0.59)。 > 每版一句话 + 三档标注。**三档定义见 [§0.4](#04-三档标注惯例)。** -### v0.95(当前契约版本) +### v0.96(当前契约版本) + +**「语音发音人绑人格 + 管理面脱敏投影」版(M35 / M42;纯增量,无破坏性变更)** + +- **`POST /api/v1/audio/speech` 新增可选 `sessionId`**(纯增量,`omitempty`)。发音人不再由请求体决定,而是按该会话的**生效人格**解析:**会话绑定 → 全局默认人格 → 空**。 + - 🔴 **省略 `sessionId` 不等于「没有角色」**:此时按**全局默认人格**选发音人(常规安装必有全局默认人格 ⇒ 仍走 `personas` 映射)。只有**连全局默认人格都没有**时才落到 `voice-tts.json` 的 `default`(没配则不合成)。 + - ⚠️ **不要**写成「`sessionId` 为空 ⇒ 发音人置空」——那会让本口永远用 `default` 那个声音,正是「换了人格还用老角色」的病害。 + - 请求体的 `voice` 仍**接受但不生效**(前向兼容);音色只能由服务端 `personas` 映射决定。 + - 产物**恒为 WAV**(服务端无转码),实际格式一律按**字节嗅探**判定,客户端**不要**照抄 `Content-Type`。请求体 `format` 仅为前向兼容接受。 +- **新增两个错误码**(状态码仍是 `500` —— 这是**服务端配置缺失**,不是客户端的错): + | `error.code` | 含义 | + | --- | --- | + | `tts_persona_unmapped` | 这个生效人格在 `personas` 里没有映射(去面板补) | + | `tts_no_character` | 连生效人格都没有,且没配 `default` | + | `tts_failed` | 其余合成失败(原口径) | +- 🔴 **管理面两条读端点是「脱敏投影」,不是原始数据**(M42): + - `GET /api/v1/admin/observability/logs`(A10):每行 `msg` 先过凭据打码,**上限 4096 rune**(够一行日志全文,但不会把 MiB 级行塞进 JSON)。`time`/`level`/`source` **原样**,条目数与最新优先序不变。 + - `GET /api/v1/admin/observability/trace`(A11):每个 span 先过 `SanitizeRecord` ⇒ `props` 变**白名单投影**,表外键折成 `omitted: {键名 → {bytes, runes, sha256 前缀}}`(**仍能回答「这次输出了多大、两次是不是同一份」**);事件里按定义是正文的键同样折成 Digest。**字段名与帧形状不变**,客户端**不得依赖**未声明的 props。 + - `id/name/kind/status/startedAt/endedAt/durationNs`、`level/source/time`、条数与排序——**排障字段一律保留**。 + +### v0.95 + **「渠道静默窗口可写」版 —— 群里「一鼓作气打三句」只回一次,且参数在面板上调(切片 M23)** @@ -2129,7 +2150,7 @@ curl -s -X POST -H "X-API-Key: $KEY" \ - **`task_done` / `automation_result` / `subagent_result` 不是会话消息**:它们**不进消息 store、不是气泡**(后端也没有把它们写成 `chat_messages` 行;`subagent_result` 更是**错过即永久丢**)。客户端应把它们落到自己的可观测面(Web 端落在本端 WS 事件回显 + 任务日志)。 - `GET /api/v1/files/{name}` **无鉴权**(能力 URL);文档读侧相反、**必须 Bearer**。 - `GET /admin/llm/usage` **不按 `session_id`** 过滤/分组(尽管该列已落库)。 -- `POST /audio/speech` 的请求体 `voice`/`format` **当前不生效**(合成遵循服务端 `VOICE_TTS_VOICE_ID`/`VOICE_TTS_FORMAT`)。 +- `POST /audio/speech` 的请求体 `voice`/`format` **不生效**;**v0.96 起发音人不再是服务端 env 决定**,而是按 `sessionId` 的**生效人格** → `$DATA_DIR/config/voice-tts.json` 的 `personas` 映射(见 §6 v0.96)。产物恒 WAV。 - `/audio/voice-clone` **真建音色**需公网参考音频 URL(当前无,待提供)+ 北京地域 API Key。 - 可观测三帧 `observability_*`:`?level=` 分级过滤**由客户端本地做**(帧广播给所有 owner/admin 连接,订阅只决定循环跑不跑)。审计 tab 仍 5s REST 轮询(**未做** `observability_audit` 帧)。 - **`thinking` 恒缺席(v0.66 口径更正)**:消息 DTO 的 `thinking` 在**所有**读取面都不出现(`history.Message.Thinking` 全仓无写入者)。思维链只有实时 `thinking.*` 帧(owner/admin)。**旧文档称「历史思维链可由 `GET /sessions/{id}/messages` 返回」是错的**,见 §3.4.4。