docs(契约): v0.89 修订 —— 白名单收窄六项 + 边界行为降为实现细节 + 补 IM 出口口径
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

与安卓(客户端)会话对齐后的三处修订:

- 白名单八项 -> 六项(去掉 u / s):它们在她 100 条输出里 0 命中,
  不值得让各端为它们返工;各端可额外容错,但契约只承诺六项
- 结构不平衡的粒度(整段降级 vs 只坏那一对保持字面)降为实现细节:
  契约只承诺「不产生行内渲染 + 字符一个不丢」;这类输入极罕见,
  为它让任一端返工不划算
- 新增 IM 出口口径:成对且平衡的白名单标签 -> 剥标签留文本;
  br -> 换行;白名单外与不平衡 -> 原样保留(逐字不丢)。
  与 action / narration 的「整体剥除」语义不同(那是角色扮演标记)

大小写口径(Web 不敏感 / 安卓只认小写)留待各端自定,已与安卓协商。
This commit is contained in:
AskaEth committed 2026-10-07 17:19:48 +08:00
1 parent b989f0bb0c
commit d27c1d4e33
1 file changed
+6 -3
+6 -3
View File
@@ -1493,12 +1493,15 @@ curl -s -X POST -H "X-API-Key: $KEY" \
- 🔴 **背景(实测)**:她输出的**不是 Markdown**(最近 12 条 assistant 消息里 `**` 记号 0 个、列表行 0 个),而是**有限的一小组 HTML 标签**:`<p>` 96 次、`<action>` 34 次、**`<b>` 6 次**。`<p>` / `<action>` 各端早已处理,**`<b>` 此前没有任何一端处理 ⇒ 用户直接看到裸标签**。
- **新增渲染口径(跨端一致)**:助手消息正文里,以下**行内标签**应被渲染成对应样式,而**不是**按字面显示:
`b` / `strong`(粗)· `i` / `em`(斜)· `code`(等宽)· `u`(下划线)· `s`(删除线)· `br`(换行,`<br>`/`<br/>`/`</br>` 按 HTML5 口径等价)。
**标签名大小写不敏感**(`<B>` = `<b>`)。
`b` / `strong`(粗)· `i` / `em`(斜)· `code`(等宽)· `br`(换行,`<br>`/`<br/>`/`</br>` 按 HTML5 口径等价)。
⚠️ **同日晚修订:白名单由八项收窄为上面六项**(去掉 `u`/`s`)—— 它们在她 100 条输出里 0 命中,不值得让各端为它们返工;各端**可以**额外容错渲染别的行内标签,但**契约只承诺这六项**。
- **🔴 三条硬口径(各端必须照此实现,否则跨端显示不一致)**:
1. **白名单外一律按纯文本原样显示** —— `<script>` 显示成字面量 `&lt;script&gt;`,**字符一个不丢**;
2. **属性一律不解析** —— `<b class=x onclick=y>` 只取标签名,**属性不得变成任何 DOM/视图属性**;
3. **结构不平衡**(未闭合 / 落单闭标签 / 交错嵌套)⇒ **整段降级为纯文本原文**(粗粒度是有意为之:换来「字符一个不丢」可被逐字断言)。
3. **结构不平衡**(未闭合 / 落单闭标签 / 交错嵌套)⇒ **该段不产生任何行内渲染,且字符一个不丢**。
⚠️ **同日晚修订**:具体粒度(「整段降级为原文」还是「只让坏的那一对保持字面」)**降级为实现细节**,契约不再强制统一 —— 这类输入极罕见(安卓扫 100 条、Web 扫 12 条,均 0 例),为它让任一端返工不划算。
- **🆕 行内标签在 IM 出口的行为(同日晚补)**:IM 渠道只收纯聊天文本 ⇒ **成对且平衡的白名单行内标签在投递前剥除标签、保留内部文本**(`<b>18:35</b>` ⇒ `18:35`),**`<br>` ⇒ 换行**;**白名单外与结构不平衡的标签一律原样保留**(它们是文本,逐字不丢)。这与既有的 `<action>`/`<narration>`「IM 出口整体剥除」**语义不同**(那两个是角色扮演标记,要丢;行内标签是排版标记,剥样式留文本)。
- **大小写口径(各端自行决定,契约不强制)**:Web 端**不敏感**(`<B>` = `<b>`);安卓端**只认小写**(与它 `parse` 只认小写 `<p>` 同口径)。两者都能通过本契约的硬口径,**这一条留待各端自定**(已与安卓端协商)。
- 🔴 **安全红线**:助手文本是**不可信内容**(模型生成 + 用户可诱导)⇒ **任何端都不许把整段内容当 HTML 注入**(Web 端禁 `dangerouslySetInnerHTML`,原生端禁等价手段)。正确姿势是**解析成结构化节点、交给各自的安全渲染原语**(Web:React 子节点自动转义)。Web 端已由「**全程无 `dangerouslySetInnerHTML`** + 节点类型里**没有属性位**」两条结构性保证锁住,并以「白名单外的 `script` 必须逐字原样」为测试断言。
- **范围克制**:**不做块级**(段落仍由 §4.16 的 `<p>` 机制表达)· **不做 `<a>`**(链接有独立安全面,且她目前不用)· **不做 Markdown**(她不用)。
- **不改既有**:`<action>` / `<narration>` 的既有行为逐字不变(Web 灰斜体、IM 出口剥除);**注记内部不参与行内渲染**(注记里写 `<b>` 仍是字面量)。