docs(契约): br 改为「气泡/消息分隔符」(主人口径),行内白名单收窄到五项
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

主人 2026-10-07:「对消息内的 <br> 始终处理为多条消息隔断」。

- br 语义与 p 同级:两边文本成为两条独立消息(IM)/ 两个独立气泡(Web),
  而不是同一气泡内的换行;<br> / <br/> / </br> 三种写法等价
- 行内白名单据此收窄为五项(b/strong/i/em/code),br 移出白名单
- 明确影响面:含 br 的回复在 IM 上会变成多条消息(受既有气泡间隔节流)
- 标注「实现跟进中」:Web 端 2dae469 当时把 br 当行内换行、IM 出站当时
  连行内标签都还没剥,两侧都在改;以契约为准,不以当日实现为准
- IM 出口那节补一句:br 不按行内标签处理,按切分走
This commit is contained in:
AskaEth committed 2026-10-07 17:20:47 +08:00
1 parent d27c1d4e33
commit 6ac214366e
1 file changed
+9 -3
+9 -3
View File
@@ -1493,14 +1493,20 @@ 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`(等宽)· `br`(换行,`<br>`/`<br/>`/`</br>` 按 HTML5 口径等价)。
⚠️ **同日晚修订:白名单由八项收窄为上面六项**(去掉 `u`/`s`)—— 它们在她 100 条输出里 0 命中,不值得让各端为它们返工;各端**可以**额外容错渲染别的行内标签,但**契约只承诺这六项**。
`b` / `strong`(粗)· `i` / `em`(斜)· `code`(等宽)。
⚠️ **同日晚修订:白名单由八项收窄为上面五项**(去掉 `u`/`s`/`br`)—— `u`/`s` 在她 100 条输出里 0 命中,不值得让各端为它们返工;`br` 见下一节(它不是行内标签)。
- **🔴 `<br>` 是「气泡/消息分隔符」,不是行内标签(同日追加,主人口径)**:主人原话「**对消息内的 `<br>` 始终处理为多条消息隔断**」。
- **语义与 `<p>` 同级**:`<br>` 两边的文本**必须成为两条独立的消息**(IM 侧)/ **两个独立的气泡**(Web 侧),**而不是**同一条消息里的一个换行。
- `<br>` / `<br/>` / `</br>` 三种写法**等价**(HTML5 口径)。
- **空气泡照旧丢弃**(与既有 `<p>` 空气泡口径一致);`<br>` 两侧都是空白 ⇒ 不产生多余空气泡。
- ⚠️ **影响面(各端须知)**:一条含 `<br>` 的回复在 IM 上会变成**多条消息**(与 `<p>` 一样受既有的气泡间隔节流约束)。
- ⚠️ **实现状态**:本条发出时**实现跟进中**(Web 侧 `2dae469` 当时把 `<br>` 当行内换行,正在改;IM 出站侧当时连行内标签都还没剥,正在改)。**以本契约为准,不以当日实现为准。**
- **🔴 三条硬口径(各端必须照此实现,否则跨端显示不一致)**:
1. **白名单外一律按纯文本原样显示** —— `<script>` 显示成字面量 `&lt;script&gt;`,**字符一个不丢**;
2. **属性一律不解析** —— `<b class=x onclick=y>` 只取标签名,**属性不得变成任何 DOM/视图属性**;
3. **结构不平衡**(未闭合 / 落单闭标签 / 交错嵌套)⇒ **该段不产生任何行内渲染,且字符一个不丢**。
⚠️ **同日晚修订**:具体粒度(「整段降级为原文」还是「只让坏的那一对保持字面」)**降级为实现细节**,契约不再强制统一 —— 这类输入极罕见(安卓扫 100 条、Web 扫 12 条,均 0 例),为它让任一端返工不划算。
- **🆕 行内标签在 IM 出口的行为(同日晚补)**:IM 渠道只收纯聊天文本 ⇒ **成对且平衡的白名单行内标签在投递前剥除标签、保留内部文本**(`<b>18:35</b>` ⇒ `18:35`),**`<br>` ⇒ 换行**;**白名单外与结构不平衡的标签一律原样保留**(它们是文本,逐字不丢)。这与既有的 `<action>`/`<narration>`「IM 出口整体剥除」**语义不同**(那两个是角色扮演标记,要丢;行内标签是排版标记,剥样式留文本)。
- **🆕 行内标签在 IM 出口的行为**:IM 渠道只收纯聊天文本 ⇒ **成对且平衡的白名单行内标签在投递前剥除标签、保留内部文本**(`<b>18:35</b>` ⇒ `18:35`);**白名单外与结构不平衡的标签一律原样保留**(它们是文本,逐字不丢)。这与既有的 `<action>`/`<narration>`「IM 出口整体剥除」**语义不同**(那两个是角色扮演标记,要丢;行内标签是排版标记,剥样式留文本)。**`<br>` 不在此列** —— 它按上一节走「切分成多条消息」。
- **大小写口径(各端自行决定,契约不强制)**:Web 端**不敏感**(`<B>` = `<b>`);安卓端**只认小写**(与它 `parse` 只认小写 `<p>` 同口径)。两者都能通过本契约的硬口径,**这一条留待各端自定**(已与安卓端协商)。
- 🔴 **安全红线**:助手文本是**不可信内容**(模型生成 + 用户可诱导)⇒ **任何端都不许把整段内容当 HTML 注入**(Web 端禁 `dangerouslySetInnerHTML`,原生端禁等价手段)。正确姿势是**解析成结构化节点、交给各自的安全渲染原语**(Web:React 子节点自动转义)。Web 端已由「**全程无 `dangerouslySetInnerHTML`** + 节点类型里**没有属性位**」两条结构性保证锁住,并以「白名单外的 `script` 必须逐字原样」为测试断言。
- **范围克制**:**不做块级**(段落仍由 §4.16 的 `<p>` 机制表达)· **不做 `<a>`**(链接有独立安全面,且她目前不用)· **不做 Markdown**(她不用)。