# dsh-unicode-guard [English](README.md) | **中文** 一个 DSH host 端插件:把某一类罕见字形挡在所有「模型可见面」之外。 ## 它防的是什么 部分推理端点会在请求体含有某个特定字形时直接拒收,返回 HTTP 400,理由只有一个 `Content Exists Risk`。这种字形进入请求的方式几乎总是意外:agent 抓取了一个网页, 页面里的语言切换菜单或页脚用了国旗类 emoji,整页落进 tool 结果,此后该会话的**每一次** 请求都会重放它。每次重试的字节完全一样,于是每次都同样失败,会话无法自行恢复。 关键在于「区域指示符对」(regional indicator pair):U+1F1E6..U+1F1FF 区间里相邻的两个码位, 这正是国旗 emoji 的编码方式。页面本身没有任何问题——一个页脚就够了。 ## 插件做什么 它在载荷造成伤害之前改写载荷,分三层: | 钩子 | 层次 | 作用 | | --- | --- | --- | | `tools/post-execute` | 工具结果 | 在结果被追加进会话**之前**改写内容 | | `agent/pre-step` | 已受影响的会话 | 用净化后的副本替换出问题的 surface 节点,使该会话的下一轮就是干净的 | | `llm/stream` | 出站请求 | 直接擦洗序列化后的请求体,是适配器派发前的最后一道钩子 | 三层缺一不可,因为它们覆盖的产出方不同。只有第一层看得见工具结果;只有第三层看得见 那些从不以工具结果形式流动的文本(压缩摘要、指令文件正文、MCP 服务器 instructions、 定时任务提示词)。 区域指示符对会被改写成可读的 ASCII 标签。紧跟在改写之后的变体选择符(U+FE0F)会被删除, 以免留下裸露的选择符。其余内容一律不动——普通 emoji、中日韩文字、以及其它任何非 BMP 字符 都原样通过。 ## 安装 ### 作为组合包(推荐) ```sh dsh plugin add dsh-unicode-guard ``` 本包自带 `cordis.patch.yml`,会把自身的插件行插入 profile,所以把它加进 profile 的 bundles 列表即可。 ### 手工装进 profile 把目录链进 profile 的 `node_modules`,并在 profile 的 `cordis.patch.yml` 末尾追加一行: ```yaml - insert: - id: unicode-guard name: dsh-unicode-guard config: mode: regional verbose: true ``` 改 `cordis.patch.yml` 会热重载;新建链接本身需要重启。 ## 配置 | 键 | 默认 | 含义 | | --- | --- | --- | | `mode` | `regional` | `regional` 只处理区域指示符对。`astral` 会把**所有**非 BMP 字符替换成 `replacement`,那会破坏正常输出——要刻意开启。 | | `replacement` | `U+FFFD` | 仅 `astral` 模式使用的替换字符。 | | `toolResults` | `true` | 工具结果落盘前擦洗。 | | `userMessages` | `true` | 会话自愈时擦洗 user 角色消息。 | | `assistantMessages` | `false` | 会话自愈时擦洗 assistant 消息。 | | `request` | `true` | 启用 `llm/stream` 兜底。 | | `verbose` | `true` | 每次改写通过 `ctx.logger` 记一行。 | 配置项写错(未知键或类型不对)会在挂载时报错,而不是静默失效。 ## 怎么验证它在工作 挂载成功会记且只记一行: ``` [I] [unicode-guard] unicode-guard: mounted (mode=regional, toolResults=true, ...) ``` 每次改写都会记一条 warning,所以可以实时观察: ``` [W] [unicode-guard] unicode-guard: scrubbed the web_fetch tool result [W] [unicode-guard] unicode-guard: healed 1 surface node(s), removed 2 glyph(s) [W] [unicode-guard] unicode-guard: scrubbed the outgoing request ``` 一个不依赖任何日志的端到端验证:读一个含区域指示符对的文件,工具结果里必须显示 `[FLAG:AB]` 形式,而不是文件里的原始字形。 ## 已经中毒的会话如何恢复 已遭拒收的会话会在**下一轮**自愈,不需要重试:`agent/pre-step` 在该轮请求组装之前运行, 所以节点会先被替换掉,这一轮的第一个请求就已经是干净的。 修复手段是 *surface replacement*(表面替换)。会话日志是 append-only 的,所以原始记录 仍留在磁盘上,只是在当前表面上被遮蔽了。这与内核自身做 tool 结果剪枝用的是同一套机制, 也正是它保住了日志的完整性承诺。 ## 设计要点 - **投影而非就地修改。** `llm/stream` 里的 message 是深度冻结的;被改动过的 message 必须以 克隆体替换。就地改要么抛异常,要么破坏其它监听器还持有的那份对象。 - **`assistant/message` 不能携带 `sourceEventSeqs`。** 内核会拒绝,因为该事件自身内嵌了来源 数据流,所以替换意图因事件类型而异。 - **擦洗整个载荷,而不只是 message。** tool 结果可能在 `data.meta` 里携带第二份载荷副本 (`read` 工具把它带行号的窗口存在 `meta.lines`)。这份副本从不发给服务商,但留着它会让 会话文件在 fork、导出、UI 回放时依然脏,所以一并擦洗。 - **修复动作绝不能反过来弄坏它所修复的那一轮。** 自愈过程中抛异常的节点会被记日志并跳过, 该轮继续正常进行。 - **UTF-16 细节。** 区域指示符是代理对。配对时按码位行走,绝不按裸码元,孤立代理会被原样 通过而不是被弄坏。 ## 测试 ```sh node --test test/ ``` 测试套件没有任何依赖,也不需要下载 fixture。覆盖掩码器的边界情况(配对、落单、变体选择符、 幂等、代理完整性)、钩子契约,以及请求期兜底的改写。 `test/selfcheck.test.mjs` 会在本仓库任何文件里出现真实非 BMP 字符时让整个测试失败。 一个自己就带着触发字形的插件本身就是污染源,所以这条性质是被强制的,而不是被承诺的。 ## 适用范围与边界 - 它只处理被配置的字形类别;`mode: astral` 会扩大到所有非 BMP 字符。 - 它会改变模型看到的内容:国旗类 emoji 变成 `[FLAG:AB]`。这正是目的——用 ASCII 标签保住 信息可读性,同时去掉让请求失败的那些字节。 - 修复不会让日志变小:被遮蔽的记录仍留在磁盘上。 - 它是客户端侧的健壮性措施,不替代服务端可能做的任何事。如果端点不再拒收这类字形, 本插件就退化为一个无害的规范化器。 ## 许可 MIT