DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
C

dsh-cache-control

Cache Control

DSH Desktop 会话策略:常驻规则注入(会话守则 / ponytail / 输出形状)、按需审查、工具输出压缩与取回、对话外观和存储管理。迁移及生效范围见 README。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:Raylen-berry/dsh-cache-control#6c830d9c0e76add87312ed8389f2426dbd895018
README兼容性版本

兼容性与来源证明

Cache Control 以 dsh-cache-control 发布,当前版本为 1.16.20。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/10/6

版本

1.16.20stable
2026/10/6
1.16.0stable
2026/9/29
1.13.2stable
2026/9/24
查看其余 9 个版本收起版本
1.12.4stable
2026/9/23
1.12.0stable
2026/9/20
1.11.1stable
2026/9/20
1.9.7stable
2026/9/20
1.9.1stable
2026/9/17
1.8.0stable
2026/9/15
1.6.2stable
2026/9/14
1.5.0stable
2026/9/12
1.4.2stable
2026/9/9

相关插件

正在加载相关插件…

最新版
1.16.20
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/10/11
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 security-access 分类下经过校验的插件。

Experimental Auto Review@deepseek-ai/dsh-experimental-auto-review针对每个工具的 LLM 授权审查,适用于 DeepSeek Harness Auto 权限预设Cc Safety Netcc-safety-net编码代理 CLI 钩子——阻止破坏性命令和访问机密文件Doctor@linxin666/dsh-doctorDSH 配置档案的事务性救援模式,配备受监督的启动器、隔离的恢复容器、确定性修复、健康监控以及本地 Web 恢复控制台Pocketdsh-pocket把 DeepSeek Harness 装进你的口袋:一个包、一个设置页,手机扫码即同步访问电脑上的 DSH(局域网 + 公网,实时同屏)。

README

dsh-cache-control(会话策略:省 token / 会话守则 / ponytail / 输出形状 / 自动审查 / 气泡置顶 / 对话页 / 存储)

给 DSH Desktop(web profile)加一个「会话策略」控制台,按用途分为四页:

分区内容
规则策略预设、会话单独设置、生效检查、三段规则与历史、按需代码审查
省 token工具输出压缩、去重、原文取回与本次运行统计
对话外观提问气泡置顶、透明度效果、阅读宽度与拖拽把手
存储分类占用、缓存回收预览、单独清空回收目录

输入框旁的 提问 / 懒码 / 形状 三段仍可独立切换,有当前会话时只修改本会话;设置页下方三段规则是全局默认。开关自动保存;规则正文要点「保存并生效」。

1.16.0 策略管理(2026-09-28)

  • 策略预设:提供「日常聊天 / 写代码 / 深入分析」,支持保存、重命名、删除自己的组合。保存三段规则的开关和原始正文快照;内置组合恢复插件自带正文。选择全局或仅当前会话,先预览开关与正文差异再确认应用。外观、审查与省 token 开关不包含在预设中。
  • 历史与恢复:每张规则卡下方可查看历史、对比当前内容、恢复所选版本。编辑、恢复内置、应用全局预设前都会记录旧正文;相同内容重复保存不增加版本。页面列最近 50 版,更早文件保留在磁盘;恢复本身也会备份当前版本。
  • 生效检查:分别核对插件是否加载、三段规则是否挂载、当前会话最近一次模型请求的系统文本是否含应有规则。改设置后旧证据会标过期;还没有新请求就显示等待,不会替你发模型请求。包含规则只能证明已带上,不能保证模型遵守。
  • 当前会话:三段分别可选「跟随全局 / 仅此会话开启 / 仅此会话关闭」,也可一键恢复跟随全局。会话预设保存独立正文快照,随后修改全局不会改变该快照;选回「跟随全局」同时取消该段正文快照。其他会话、子会话保持各自设置。输入框旁快捷开关标明「本会话」;没有会话标识的旧宿主仍使用全局默认。
  • 规则负担与检查:编辑时显示启用正文的字节量与 token 粗估范围,提示疑似重复行和常见简短/详细要求冲突,列出位置。检查在本地进行,不调用模型、不改写正文;估算不是实测或计费值,语义提示可能误报或漏报。

全局规则仍使用原有 settings.json 和三份 .md;预设与会话设置存于 $DSH_HOME/dsh-cache-control/policy.json,历史在同目录的 history/。会话覆盖最多 300 项,自定义预设最多 40 套。全局应用共用串行写入队列,并记录事务备份;正常回滚或下次启动恢复中断写入。写入期间请求继续读原来的完整规则,避免半套新旧内容。

本次需要重启 DSH Desktop 才能加载新增的宿主接口和请求检查;仅刷新页面会提示重启。重启后更改规则从下一个模型请求生效。诊断只保留内存中的时间、字节数和检查结果,最多 100 个会话,重启即清空,不保存对话正文。

验证:5 项语法检查、12 套离线测试、29 项隔离浏览器检查通过;另用本机 DSH 0.1.7 的真实提示词组装模块通过 39 项策略检查。浏览器的策略、规则与设置走真实本地接口和临时目录,统计与存储使用合成数据;未写用户配置或发起真实模型请求。

1.15.0 改良(2026-09-28)

  • 顶部显示规则启用数、每次请求附加的规则字节量与审查可用状态;字节量不等于计费 token。
  • 宽窗双列、窄窗单列,跟随宿主明暗主题;标签支持方向键。规则草稿切换分区后仍保留,失败可重试。
  • 设置只提交修改的字段,写入串行执行,后续输入合并成最新值;失败时保留待保存内容,迟到的旧读取不会覆盖新设置。
  • 规则正文响应与规则开关分别处理,审查使用独立接口;三段规则复用同一读写流程,失败不清空草稿。
  • 只挂载当前分区。统计仅在分区打开且页面可见时轮询,退出即取消读取;存储仅在进入时扫描。
  • 存储同一时刻共用一次扫描,5 秒内复用结果,主动刷新和清理后重新统计。达到扫描上限时会明确提示。
  • 缓存回收排除提示词产物与回收目录自身;批量提交预览中的具体路径,清理前确认,部分失败也会显示原因。
  • 移入回收只是挪动文件,清空回收目录才释放磁盘空间;会话历史、附件、登录信息不列入缓存清理。

更新本地文件后,刷新页面加载新界面;index.js 的存储改进需要重启 DSH Desktop。 验证:npm test 跑离线套件;tools/verify-workbench-browser.mjs 测试真实浏览器交互与截图,不连接用户的 DSH 配置。 本版通过 4 项语法检查、11 套离线测试和 16 项隔离浏览器检查;存储测试含真实本地 HTTP 请求与临时目录清理验证。 运行浏览器套件时可用 CC_PLAYWRIGHT_MODULE、CC_BROWSER_PATH 指定本机运行时,CC_SCREENSHOT_DIR 指定截图目录。

v1.14.0 移除:原第一块「省缓存 · 压缩策略」(改写 standard preset 里 compaction-basic 那一行)** 已随 DSH 0.1.7 一并删除** —— 那个版本起 agent preset 不再是可编辑的 agent.cordis.yml,接管没有落点, 只会让设置页每次打开都 500。理由与善后见「压缩接管:为什么删掉」。

省 token · 结构化压缩会话守则 · 长期规则ponytail · 编码纪律输出形状 · 回复形状自动审查 · 按需技能
动的是什么工具输出本身(tools/post-execute 改写一次)+ 注册一个模型工具 save_token_expandsystem prompt 里一个常驻段(规则文本)system prompt 里的第二个常驻段(与守则同构、独立开关)system prompt 里的第三个常驻段(同上,但默认开)宿主 skills 目录里的一个条目(不进 system prompt)
生效时机每次工具输出返回时(当次请求就生效,不用重启)适用会话的下一个 model step(全局默认可被会话单独设置覆盖)同「会话守则」同「会话守则」模型/用户调用技能那一刻
会被压缩冲掉吗折叠掉的是改写后的短文本,原文在 spillStore 里,save_token_expand 取回内存中的完整原文;超长原文或重启后可能需按提示路径调用 read不会:压缩只折叠对话历史,system prompt 每请求重发同「会话守则」同「会话守则」不适用(压根不在 prompt 里)
代价工具表多一行(唯一一个模型工具);统计只存计数与字节数,两个开关存内存、重启回默认开内置正文约 4.3 KB × 启用此规则的请求,token 依模型而异内置正文约 3.6 KB × 启用此规则的请求内置正文约 4.1 KB × 启用此规则的请求,默认开启注册本身不注入常驻正文;调用时加载技能内容

1.13.3 检查与生效范围(2026-09-28)

  • 空会话页「工作区行」(DeepSeek / 标准模式 / PPT / 生图 那一行)左边缘对齐输入卡:两条静态 CSS 规则 —— 行内边距改走 --dsh-composer-side-clearance(与卡容器同源),行内第一个子项再补 margin-left:max(0px, calc((100% - var(--dsh-composer-card-max-width,100%)) / 2))。 无条件生效,与「对话页宽度」开关、百分比取值都无关;max(0px,…) 兜住卡片被 width:100% 夹住的窄窗。
  • 1.12.1 的 CHANGELOG 曾声称修好这条,但那两条规则从未进过任何一版 client.js(用户因此又报了一次)。 本轮真正落地,并在 verify-gate-client.mjs 加了 I 组两条源码守卫(101 → 103 项),让"只写在 CHANGELOG 里的修复"变红灯。
  • 本机真浏览器实测:卡宽 90% 时 chip 与卡左边缘差 −0.015px(亚像素舍入;改前 47.03px); 卡宽 400px / 2000px / 100% / 60% 四档偏差都是 0。
  • 生效方式同 client 半:client.js 存盘后刷新页面即生效(?rev= 是内容哈希),不必重启 DSH Desktop。

1.13.2 检查与生效范围(2026-09-24)

  • 取回:不再把超过 262,144 字符的内存预览当作完整结果;支持同步/异步读回,当前宿主没有读回接口时明确给出原文路径。重置计数保留取回、去重及回放索引。
  • 界面:请求完成后才安排下一轮;修改开关使旧读取失效;写入后回读完成才解锁;失败和超时可见,初次失败可重试;退出页面取消请求。重启语义直接展示,KPI 随可用宽度换列。
  • 测试:4 项语法检查、11 套离线测试;save-token 47 项、React 生命周期 18 项,gate-client 本机 101 项。生命周期测试真正挂载组件并执行 effect,和 SSR 格式测试互补。
  • client.js 保存后刷新页面生效;save-token-host.js 需要重启 DSH Desktop。本轮没有自动重启,避免清掉当前进程中的取回映射与统计。
  • 不新增确定性压缩兜底,不改变 compaction-basic 所有权。字节变化只能证明体积变化,不能证明摘要保留了全部关键事实。

为什么分区标题没有编号(v1.11.1)

编号是位置属性、名字是身份属性。把位置写进标题,代价是任何一次插入都要把全部下游引用重排一遍: v1.10.2 修的是 ponytail 曾写作 "②b" 导致页面上出现两个 ②;v1.11.0 插入「自动审查」又让后面三块全部顺延 (README、CHANGELOG、跨插件指路、测试断言各扫一遍)。v1.11.1 起标题只留名字,顺序由卡片书写顺序决定, 插一张卡只改一处。要指代某块直接说名字。

verify-gate-client.mjs 里两条断言钉住这件事:① 页面(含抽屉正文)不许出现圈符;② 八个名字必须齐全且顺序正确。 注意这里只剩行文里的列举序号不算违规 —— 断言扫的是渲染出的整页文本,所以 README/注释以外, client.js 里给用户看的字符串也不能带圈符。

压缩接管:为什么删掉(v1.14.0)

这一块(设置页原第一张卡「省缓存」)做的事是改写 standard agent preset 组装文件里 @deepseek-ai/dsh-compaction-basic 那一行的 config(开 = 写入 thresholdRatio / retainRatio / auto, 关 = 逐字节还原首次接管前的原文;原文存在 settings.json 的 compactionBackup 里)。 v1.6.1 还专门修过它的破坏性缺陷:只有压缩字段变化才碰那个文件,只改外观开关不会再抹掉用户自己的配置。

DSH 0.1.7 起这条路没有落点了:

  • 打包形态变了:resources\app\(真目录)换成 resources\app.asar + app.asar.unpacked\, 旧 junction(profiles\node_modules\@deepseek-ai\dsh-agent-presets)指向的目标已不存在。
  • preset 本身也不再是组装文件:安装里搜不到任何 agent.cordis.yml;包由 dsh-agent-presets 拆成 dsh-agent-preset + dsh-agent-preset-registry,组装改由代码注册表负责。
  • 后果:GET /cc/settings.json 每次都探那两个死路径,探不到就 500 ⇒ 设置页整页打不开; 启动时也刷一条 cannot locate standard preset 的告警。

删的是整条写盘路径(读组装文件、splice 那一行块、备份/还原、boot 对账、GET/PUT 的 applied / backupAt / hasBackup 字段),不是"默认关掉"—— 默认关掉只会把 500 藏起来。

善后:

  • settings.json 里遗留的 enabled / triggerPct / retainPct / auto 不再被 sanitize 保留, 下一次保存就从盘上消失(它们已不驱动任何行为)。
  • compactionBackup 保留、只读:它存的是用户接管前的 preset 原文,而那个文件现在已不存在, 它是磁盘上唯一的副本 —— 按"不认识的数据不当垃圾删"处理,永不写入、也永不丢弃。
  • 界面上的四行控件(总开关 / 触发点 / 保留尾部 / 自动压缩)与 chip 上原第一段「省缓存」一并删除, 不留"点了没反应"的开关在面板上。

会话守则 · 长期规则

插件内的 session-gate.md 就是规则本体,作为一份可长期演进的 md随包分发:

  • R1 独立研判:不默认用户是对的;命中「事实/技术错误、目标与手段冲突、与既有约束冲突、 代价不划算」四类必须指出,且反对要可核查(对象 + 理由 + 替代方案);最终裁决权在用户, 但不可逆损失(删数据、覆盖无备份、改生产、花钱、对外发布)必须先确认。
  • R2 必要提问:只有「不同理解会改变结果」且「答案查不到」时才停下来问;能查的先查; 一轮最多三问、带候选项与推荐;拿不准但可回滚就先做完再标注。v1.9.6 起补两条时机细化 (蒸自 Claude Code "Delivering work" 条款,MIT):改变方案形状的缺口问在动手前, 只影响实现细节的按默认假设做完再标注;阻塞式提问是最后手段,否则先做完不依赖答案的部分。
  • R3 分工固定:用户定目标、补真实情况、判定可用性;助手负责搜索、执行、制作、验证、交付。 交付必须可判断(改动路径 + 依据 + 未覆盖项 + 风险与回退)。
  • R5 少犯错优先(编码行为四原则):蒸馏自 multica-ai/andrej-karpathy-skills (214K★,Karpathy 诊断、Jiayuan Chang 成文,v1.9.2 起收录)——动手前说假设 / 最小实现 / 只动必须动的行 / 任务转成可验证目标。与 R1 互补:R1 管"敢反对",R5 管"少犯错"。
  • R6 查证再下结论:蒸馏自 duolahypercho/andrej-karpathy-skills 第 4 条的验证口径(v1.9.4 起收录)——先查证再断言、未验证标【假设】、完成前跑验收动作并附证据、 没跑检查就明说没跑。
  • R7 谨慎执行:蒸馏自 Claude Code 出厂条款 "Executing actions with care"(经 Piebald-AI/claude-code-system-prompts,MIT, v1.9.5 起收录)——不可逆/共享状态动作先确认、授权只按当次范围算、破坏性操作不当绕路捷径、 不认识的状态当用户半成品(能移不删)。git status 一条刻意未收:DSH 的沙箱与审批门已覆盖大半。

界面上:

  • 控件用宿主官方原子(v1.9.3):开/关行是「说明在左 + dsh-client-ui-primitives 的 Switch 在右」, 按钮走宿主 Button —— 随宿主主题与明暗自动一致。primitives 取不到时软回退到自带的 checkbox/.cc-btn, 功能两种情况完全一致(同一 require 失败只影响观感,不影响开关)。
  • 启用开关独立于其它规则段,勾选框各管各的。chip 是固定版式的三段状态: 提问 [开/关] | 懒码 [开/关] | 形状 [开/关],[开/关] 复用同一个徽标元件(.cc-badge), 面板里各分区头也用它。
  • 面板(输入框右侧)里三块用分隔线分区,可「查看规则」直接看当前生效文本。
  • 设置页里可编辑规则:写入 $DSH_HOME/dsh-cache-control/gate.md(override), 不改动插件目录里的内置 session-gate.md;点「清除自定义,回到内置」即删除 override。

实现要点(也是几条硬约束的理由):

  • 注入走 ctx.inject(['systemPrompt']) → systemPrompt.section({ name, order: 400, text }), 与 dsh-web-app 注入 app:web-surface 同一条路;text 是函数,DSH 每个 model step 重新 assemble(),所以改开关/改文本不用重启也不用新会话。拿不到 systemPrompt 服务时只 关掉会话守则,不影响其它规则段。
  • 关 = text 返回空串,renderPrompt 会丢弃空段 ⇒ 提示词里一个字都不留。
  • 文本按 mtime+size 缓存并即时重读:直接编辑 md 存盘,下一个请求就是新内容(无需重启、无需刷新页面)。
  • 注入前把成对花括号 {{ / }} 替换成全角 {{ / }}:renderPrompt 对未知变量引用是 抛错策略,用户编辑规则时写了 {{...}} 会让每次请求组装失败 —— 所以宁可改字形也不让会话挂。
  • 上限 16,384 字节(16 KB,约 6.6k tokens);超限自动截断,避免规则膨胀悄悄吃掉上下文。 (初版是 6 KB;加完 R4 输出形状一节后余量只剩 92 B,故 v1.9.0 放宽到 16 KB —— 这段文本每请求 重复计费,上限本身仍然保留,只是不再卡在刚好够用的位置。)
  • R4 输出形状不在本文件里:2026-09-16 起本文件只剩一句归属声明,真源是同目录 shape-gate.md, 由本插件作为第三段常驻规则注入(详见下面「输出形状」一节)。别在这里再抄一份,否则同一套规则会被注入两遍。 要收回原文备份:git show 98fa1af:session-gate.R4-backup-2026-09-16.md, 或看 v1.9.1 之前的 git show 566d276:session-gate.md 里的整节(同目录那个备份文件 2026-09-23 已清出仓库)。
  • 怎么知道自己的规则被砍了:/cc/gate.json 与 /cc/settings.json 内嵌的 gate 里有三个字段 —— truncated(显式布尔标记)、originalBytes(规则原文字节数)、keptBytes(实际注入 字节数,与老字段 bytes 同值)。不要用 bytes >= maxBytes 去反推"有没有截断过":截断后的 长度必然小于上限(6 KB 上限年代的实测:19,998 字节的中文规则 → 保留 5,839 字节; 10,000 字节 ASCII → 5,590 字节), 那个判据对已截断的规则恒为 false,界面于是显示"未截断";反过来原文恰好等于上限时文本原样 返回、bytes === maxBytes,它又会误报"已截断"。这正是 v1.6.2 修掉的缺陷, tools/verify-gate-truncation.mjs 就是它的回归套件(含"恰好压线"与"上限 -1"两个边界, 样本长度与期望值都相对上限生成,改动 GATE_MAX_BYTES 不会再让套件失效)。
  • 界面上被截断时显示「已截断:原 N B → 保留 M B」标签,卡片里另写一句 「你的规则被截断了:原文 N 字节,实际注入 M 字节(超出 X 字节未进入提示词)」;编辑框里草稿 超限时也会提前提示"保存后会被截断"。老客户端只读 bytes/maxBytes 照旧可用(字段只增不改)。

ponytail · 编码纪律(v1.10.0)

与「会话守则」同构的第二段常驻规则:蒸馏自 GitHub DietrichGebert/ponytail(MIT), 最懒资深工程师的七级梯子与硬约束。独立开关 ponytailEnabled(默认关),提示词段 dsh-cache-control:ponytail-gate(order 405,紧挨守则 400、在输出形状 410 之前)。 两段各读各的文件(session-gate.md / ponytail.md)、各有独立的 mtime+size 缓存,互不顶掉; 自定义覆盖走 /cc/ponytail.json(GET 预览 / PUT 写 override / 空文删回内置)。 代价:开着约 1.3K token/请求 × 所有会话(含子代理与非编码会话),正文里写明"只对编码任务生效"兜底行为。

输出形状 · 回复形状(v1.12.0,并入自 dsh-output-shape)

与上面两段同构的第三段常驻规则:蒸馏自 GitHub ayghri/i-have-adhd(MIT)的 「ADHD 友好输出」十条规则(首行给下一步 / 多步编号 / 状态复述 / 跑题后置 / 时间给量级 / 战果可见 / 报错讲因果 / 展示分组 ≤5 / 无开场白无客套)+六条破例 + 发送前自检。独立开关 shapeEnabled, 提示词段 dsh-cache-control:shape-gate(order 410,紧随 ponytail 405、plan 政策 500 之前)。

  • 默认开,与另两段相反:并入前它由独立插件 dsh-output-shape 的 bundle config 默认开启(那插件是这套规则的真源, 会话守则里的 R4 早在 2026-09-16 就摘出交给它)。合并时保持同默认 —— 否则升级即静默改变行为,用户只看到"形状规则没了"。 不想要这份 token 就去设置页或 chip 第四段关掉它。判据是 !== false(不是 === true):旧盘上没有这个键时按开处理。
  • 文件与路由:内置 shape-gate.md、override $DSH_HOME/dsh-cache-control/shape.md、 /cc/shape.json(GET 元信息 / PUT 写 override / 空文删回内置)、独立 mtime+size 缓存(三段互不顶掉)。
  • 原独立插件已下线:dsh-output-shape 从 web profile 的依赖与 bundle 列表里摘除,仓库目录归档。 段名由 dsh-output-shape:output-shape 改为 dsh-cache-control:shape-gate(段名只在运行时用,盘上没有引用,无需迁移)。
  • 它还接手了两条按需技能:i-have-adhd 与 ponytail(原来是那个插件注册的)。两条技能无开关、零常驻 token, 正文直接读本插件的规则文件(shape-gate.md / ponytail-gate.md,override 优先)—— 于是"技能里读到的规则"与 "每请求注入的规则"永远同一份,不会漂。verify-shape-gate.mjs 第 8 节把这条钉死。
  • 逃生开关沿用原名 DSH_OUTPUT_SHAPE_DISABLE=1(改名等于把别人环境里/脚本里的开关悄悄拔掉): 置 1 则无论设置如何都不注入常驻段;技能注册不受影响。界面在卡片与面板里都会写明"被环境变量强制关闭"。

自动审查 · 按需技能(v1.11.0)

注册宿主 skills 服务里的 auto-code-review(正文 skills/auto-code-review/SKILL.md)。 这张卡与「会话守则」「ponytail」两段的根本区别:一个字都不进 system prompt —— 注册状态零 token,正文只在模型或用户 真调用那一刻加载。开关关掉 ⇒ dispose,目录里连条目都不留。

为什么不蒸成常驻规则(这是本卡存在的理由)

上游 alibaba/open-code-review(Apache-2.0,阿里内部官方 AI 审查助手开源化)的 README 把"通用 agent + 自然语言 skill 做审查"列为反面教材:

通病机制原因
覆盖不全大 changeset 上 agent"抄近路",选择性只审几个文件
位置漂移报出的问题与实际行号/文件对不上
质量不稳自然语言 skill 难调试,prompt 微调就大幅波动

根因一句话:纯语言驱动对审查过程没有硬约束。它的解法是「确定性工程 × agent 混合」—— 选文件、分组、按文件特征匹配规则、评论定位、反思复核全部由代码保证;基准 AACR-bench (50 仓库 / 200 真实 PR / 1,505 条标注,80+ 资深工程师交叉验证)显示同模型下 F1 更高、 token 只用通用 agent 的约 1/9,而 recall 有意更低(偏精确率、压噪声)。

⇒ 抄规则文本进常驻段 = 只拿走它论证过会失败的那一半。ponytail 能常驻是因为它是风格取向 (YAGNI、梯子),没有"覆盖率""定位准确率"这类可度量指标;审查有。所以本插件不存规则正文, 每次现向 ocr 取,规则跟着上游升级、不在本仓库里腐烂。署名见 NOTICE。

三步(技能正文就是照这个契约写的,本机 v1.12.7 实测)

ocr delegate preview --format json          # 该审哪些文件(reviewable_files[] / excluded_count)
ocr delegate rule --format json <path>...   # 这些文件命中哪些规则(groups[].rule + files[])
# 然后逐组对着 diff 出结论:文件:行 + 违反哪条 + 一句为什么

范围不对时用 --from main --to feat 或 -c <commit> 重取,而不是手工挑文件。

运行期依赖(本插件不打包、不下载、不自动安装)

需要用户自行 npm install -g @alibaba-group/open-code-review(前置 Git ≥ 2.41)。 卡的第三行直接显示探测结果:装了 ⇒ ocr 已就绪:v1.12.7(<命令路径>);没装 ⇒ 黄字给安装命令。 探测不到不影响注册 —— 技能照常可用,只是跑到第一步就会停下说明,不会瞎猜。

⚠ 两个 Windows 坑(都踩过,别再改回去):

  1. execFile('…ocr.cmd') 在 Node ≥18.20/20.12/24 抛 EINVAL(CVE-2024-27980 修复),不是 ENOENT —— npm 在 Windows 上装的全局 CLI 恰恰就是 .cmd shim。所以 .cmd 那一路走 shell:true 且整条命令进 shell、不传 args(传 args + shell 会刷 DEP0190 告警)。
  2. DSH Desktop 宿主进程的 PATH 里没有 npm 全局 bin:命令行里 ocr 能跑、插件里 spawn 'ocr' 报 ENOENT。故 ocrCandidates() 显式补 %APPDATA%\npm\ocr.cmd。探测结果按 TTL 60s 记忆化, 不每次 GET 都 spawn 一遍子进程。

与会话守则 R5 / ponytail 的分工

那两个管"少写、写最小实现",这张卡管"写出来的东西对不对"。重叠处(死代码、过度抽象) 以 ponytail 的判断为准,技能正文里已写明不要重复提。

气泡置顶(纯界面,与前四块独立)

开关效果实现
pinLastUser滚动时把**已越过会话区上沿的最后一条「我的提问」**钉在顶部:往上翻会换成第 4、3 条……滚到底才钉最近那条(分节标题语义)从 [class*="_userRow"] 爬到 [data-chat-flow] 的直接子元素打 data-cc-pin + position:sticky;"钉哪一条"按 rect.top <= 滚动区上沿 + 2px 判定,scroll 触发重选;底衬画在该元素的 ::before 上(见下)
clearBubble我的气泡背景透明,露出壁纸(配合 dsh-bg-atelier)[class*="_userRow"] [class*="_bubble"]{background:transparent}
pinBlur钉顶底衬的模糊度滑杆,0–24px(0 = 只留半透明底、不模糊);小值有小数档:<3.5 按 0.1 步进(可选 1.3 / 1.5 / 1.7),≥3.5 按 0.5 步进<html style="--cc-pin-blur:Npx">,底衬规则写 backdrop-filter:blur(var(--cc-pin-blur,10px)) —— 拖滑杆只改一个变量,不重注入样式

我的提问气泡的动态贴合(v1.4.0 起始终生效,无开关;几何由 probe-userrow.mjs 用真浏览器 + 真函数源码实测):

为什么必须用 JS 量一次:块盒的宽度只跟"可用宽/上限"有关,与文字实际末端无关(inline 盒虽然贴字, 但背景逐行着色、行内 padding 只落在首末片段 ⇒ 框"超"到文字之外、文字也不垂直居中)。所以:

  • CSS 定骨架(尺寸一律 em / 宿主字号变量):userRow = display:block; position:relative; box-sizing:border-box; width:fit-content; margin-left:auto; max-width:none(只当"右侧车道"), 右缘再留一条轨道 padding-right:var(--cc-tail-room-px, 2.4em);userStack 只给骨架: display:block; width:fit-content; margin-left:auto; 宽度上限由 JS 逐条写 inline(v1.16.16): fitUserBubbles() 对每条提问判"在不在被钉住的那条里面" ⇒ stack.style.setProperty('max-width', 消息列宽 × (钉顶? pinWidthPct : userWidthPct) / 100 + 'px', 'important')。 为什么用 inline 而不是 CSS 规则:两套百分比共用同一批布局属性时,"哪条规则赢"换版本就会翻车 (1.16.12 拆变量 → 1.16.13 :not(后代组合器) 被 Electron 整条丢弃 → 1.16.15 两规则各写全仍不稳); inline + important 优先级最高,没有分胜负这回事,且逐条判定天然支持"1 条钉顶 + N 条普通"。 CSS 里两条规则只留骨架 + 一条不带 !important 的兜底上限(JS 还没跑时用)。 bubble = 块盒 + 上下对称 padding .47em + 圆角 1.45em;图标行 position:absolute; right:.13em; left:auto; top:var(--cc-tail-y,auto) ⇒ 落在轨道里 = 气泡右侧;图标尺寸 calc(1.5em + var(--dsh-content-font-delta,0px)) 跟宿主字号走。字号变、页面缩放变,这些一起变 —— 不再有任何"某次量出来好看就钉死"的像素数。
  • 为什么上限不能写在行上(v1.16.7 实测):margin-left:auto 只吃"包含块宽 − 已用宽", 而 width:fit-content 的已用宽是先被 max-width 夹出来的 ⇒ 剩余为 0 ⇒ auto 退化成 0, 行停在左边缘、短句气泡看着像"居中"。离线复现见 tools/verify-pin-width.mjs (列 1140、45%:行上写 max-width ⇒ 气泡右缘差 663px;上限搬到栈上 ⇒ 36px)。
  • JS 量一次(fitUserBubbles()):先用 naturalBubbleWidth() 量文字自然宽 (内容拷进 width:max-content 的隐藏盒量一次,与当下折行无关),再 stack.style.width = 自然宽 + 左右内边距(框贴文字,超上限由栈的 max-width 夹回);接着实测图标行宽高 ⇒ 写 --cc-tail-room(轨道 = 图标行宽 + .45×图标高)与 --cc-tail-y(与最后一行同高)。 自然宽按元素缓存在 WeakMap 里,文字或列宽变了才重量。
  • 为什么不能量"折好的行"(v1.16.7 实测):那种量法会在内联宽先被写小的时候自锁 —— 文字在那个小宽度里折行,量出来最宽的一行也正好 ≈ 那个小宽度,下一轮把错值再写一遍, data-cc-fit 签名再把它焊死(用户实测到的 192px 就是这么来的)。
  • 实测(列宽 1180、字号 15px):10 字 174px、30 字 444px、长文 579px;复制键距气泡右缘 13.1px(em 轨道,随字号缩放)且与末行同高;上下留白 8/8.1 相等;420px 窄容器不越框。
  • 两条踩过的坑(别改回去): ① 用 bubble.children.length > 0 判"含内嵌块就跳过",会把带 @路径 引用的提问(宿主渲染 成 <span>)整条漏掉 ⇒ 框宽退回"块宽 = 上限"的固定观感(就是"完全不动态"那次反馈)。现在 只跳过真的含 img/video/canvas 或某行矩形异常高(内嵌块)的气泡。 ② applyAppearance() 里任何一步抛错(例如常量改名)会连带把后面的观察器全跳过 ⇒ 被钉元素 不出现,看起来就是"模糊度失效"。现在每段各自 + ,首屏再补量两次,并在 后清签名重算(字体切换会改行宽,一次量错会被签名锁住)。

底衬形态的三次选定: 2026-09-07 选半透明毛玻璃(不是实底、不是无底衬); 2026-09-07 改成圆角矩形——原先把毛玻璃铺在被钉住的整行上,而行宽 = 整个会话列宽, 于是气泡左边一大片空白也在模糊。现在:

  • 行自身只留 position:sticky,background / backdrop-filter 一律不再铺;
  • 底衬画在 ::before 上:right:-6px(右缘贴住气泡右缘,宿主 .userRow 是 align-items:flex-end 右对齐),宽度 var(--cc-pin-w, calc(min(calc(var(--dsh-chat-content-width,748px) * .55), 100%) + 12px)) —— v1.4.2 起 --cc-pin-w 由 JS 按这条提问的实测宽度写入(气泡含右侧图标轨道的像素宽 + .8em 呼吸位), 短句就是短底衬、长文就是长底衬;读不到几何时(getBoundingClientRect 拿不到正宽)退回括号里那个 "列宽 × .55 + 12px" 的旧上限兜底;
  • 长文钉顶时不再无限撑高:气泡本体 max-height:38vh + overflow-y:auto + overscroll-behavior:contain(在气泡内滚,滚到底才交还给会话流),scrollbar-width:thin;
  • border-radius:16px 圆角矩形;四周出 2–6px 呼吸位(top:-2px;bottom:-2px,不加 padding ⇒ 不挤动布局);
  • z-index:-1:被钉行有 z-index:6 自成堆叠上下文,负层因此落在"正文之上、气泡之下", backdrop-filter 采到的正是身后滚过去的正文;
  • 模糊度走 --cc-pin-blur 可调变量,默认 10px。

宽度怎么来的(v1.4.2):updatePinPlate() 在"钉住哪一条"确定后、以及每次重排(fitUserBubbles) 结束时各跑一次 —— 量被钉行里 [class*="_userRow"] 的实际像素宽,加上 根字号 × .8 当呼吸位, Math.ceil 后写进该行的 --cc-pin-w;量不到就 removeProperty,让 CSS 里的旧上限接管。

一个必须知道的真实边界:sticky 的移动量 = 父级高度 − 自身高度,所以只有当你那条提问下面 还有比它更高的内容(通常是长回答)时,钉顶才看得出来;短回答或空会话里它就像没生效。

为什么不是纯 CSS 一行:sticky 的移动量 = 父级高度 − 自身高度。真实结构(读自 dsh-client-ui-chat 的产物)是 [data-conversation-scroll] > … > [data-chat-flow] > [data-chat-flow-key] > .userRow > .userStack > .bubble, 给内层 .userRow 直接加 sticky 不会动(父级等高、没有剩余高度),所以要钉的是 [data-chat-flow] 的直接子元素(每条消息一层)。找行用 [class*="_userRow"], 定层用宿主的稳定属性 [data-chat-flow](该属性在 column 上、每条消息的 data-chat-flow-key 在其子层,均在产物里核实过);属性不在时退回"按剩余高度往上爬"的通用判据。 气泡透明只能按类名匹配,而 uSmzmW_ 这类前缀是构建哈希 ⇒ 用后缀选择器 [class*="_userRow"] [class*="_bubble"],宿主升级改名时最坏结果是这条样式不生效,不会连累其它功能。 设置页会把"钉到了哪个元素 + 共几条提问"打印出来供自检。

观察器分两条:钉顶那条只在开关打开时挂 document.body(childList + subtree,rAF 去抖),关闭即断开并清掉所有 data-cc-pin;气泡贴合那条只挂 [data-chat-flow] 容器(+ scroll / resize,90ms 去抖),负责重算框宽、 字尾位置和"钉哪一条"。插件停用/卸载时 stopFitWatch() 会撤掉观察器并清干净 stack.style.width、 --cc-tail-*、data-cc-fit 三样内联痕迹。

输入框右下角 chip 里的「开 / 关」徽标不吃背景(2026-09-07):.cc-chip .cc-badge 三条规则 把 background / border-color 都置 transparent,状态只靠文字颜色区分(开=品牌蓝、关=三级灰、 未装载=警示黄)。面板与分区头里的同名徽标保持原样(那里有底色对比的需要),所以规则限定在 .cc-chip 作用域内。

对话页(固定会话列宽,v1.3.0 从 dsh-bg-atelier 移入)

开关效果
chatWidthEnabled + chatWidth关闭 = 跟随 DSH 自适应;打开 = 把会话列宽钉在可用宽度的 30–100%(含 60/70/80/90/100 快捷键;v1.5.0 起是百分比,之前是 640–3840px)
hideResizer(v1.6.0,v1.7.0 起只管两竖杠)关闭 = 保留会话区两竖杠(宽度把手);打开 = 把它们隐藏。列宽一旦钉成固定百分比,这两条把手拖了就不再改变列宽,只剩"鼠标扫过去冒出两竖杠、拖了没反应"的误触(用户 2026-09-14 反馈)
hideDivider(v1.7.0)关闭 = 保留侧栏/详情栏分隔条;打开 = 把它隐藏。分隔条与两竖杠不同,拖它仍然有效(改侧栏宽度),所以拆成独立开关、默认不藏
  • 隐藏把手按语义识别、不按类名:宿主的类名是 CSS-module 哈希(实测 ._1tdjgG_handle / ._8JRpoa_widthHandle), DSH 一升级就变,而 cursor 含 resize 才是"这是条把手"的功能特征。两类再用宿主直接写在把手元素上的 data-width-handle 属性区分(有 = 会话区宽度把手,没有 = 框架分隔条)——数据属性不是哈希,比类名耐升级。 实现是给命中的元素分别打 data-cc-hide-resizer / data-cc-hide-divider,再由插件样式表 [data-cc-hide-*]{display:none!important} 隐藏;只认可见元素、排除自己面板内的元素, 并挂一条 MutationObserver(300ms 去抖)—— 宿主重建框架会换掉那些把手。 关掉对应开关或停用插件时把标记撤干净,不给宿主留一条看不见的把手。 两个开关各有独立能力位(resizerReady / dividerReady):旧 host 不认识某个键时只禁用那一个开关, 不连带其它。

  • 钉法:先按 [data-composer-card] 往上找到内联带 --dsh-conversation-column-width 的那个祖先 (= 会话根,宿主 publishWidths 就在它身上标定列宽),再往它身上写 --dsh-chat-content-width / --dsh-composer-card-max-width(宽 +32)/ --dsh-chat-user-width 三个变量并带 !important,绕过宿主的响应式 clamp;另有一条 :root{--dsh-chat-user-width:…!important} 兜底,覆盖 composer 还没挂上的窗口期。关闭时逐个 removeProperty + 删掉兜底样式,交回自适应。

  • 会话根随切会话/导航会重建 ⇒ 另挂一条 MutationObserver(300ms 去抖)只在开关开着时存在, 根节点一重建就把变量补写回去。

  • 滑杆拖动过程中只做即时预览(局部 state + 直接钉 CSS 变量),松手/失焦/方向键才 STORE.set → 存盘: 否则每拖一格都会把设置页整页重渲染一遍,手感发涩。

  • 与「气泡置顶」的联动:底衬宽度 v1.4.2 起跟随这条提问的实测宽度(--cc-pin-w),读不到几何时才退回 --dsh-chat-content-width × .55 的旧上限;所以这里改列宽,只在"兜底路径"下才会等比影响钉顶底衬。

  • 一次性迁移:这两项原先存在 $DSH_HOME/dsh-bg-atelier/settings.json。host 启动时若发现自家 settings.json 缺 chatWidth / chatWidthEnabled,就读底图工坊那份搬过来并写盘 (migrateFromAtelier(),日志 对话页宽度已从 dsh-bg-atelier 迁入);搬完之后自家有字段就不再读对方, 你之后改的值不会被对方旧值盖回。bg-atelier v1.3.0 起客户端不再声明这两个字段,也就不会再 PUT 回去。

存储(v1.8.0)

按用途分别统计各目录占了多少盘(会话记录 / 投影缓存 / 附件副本 / 浏览器观察窗 / 生图产物 / 费用记录 / 本插件数据),给出文件数·字节·最新最旧时间与总计。清理只收"明确可再生成"的东西, 且先给候选清单再确认、动作是移入回收而非删除;purge 单独确认后清空回收目录。 候选仅含观察窗截图与浏览器磁盘缓存,提示词产物与回收目录本身不再列入。 GET /cc/storage?refresh=1 强制重新统计;普通读取共享在途扫描并短暂缓存 5 秒。 host 三条路由:GET /cc/storage、POST /cc/storage/clean、POST /cc/storage/purge。

省 token · 结构化压缩(v1.13.0,并入原 dsh-plugin-save-token)

来源:并入自原独立插件 dsh-plugin-save-token v2.4.1(MIT,© playwithai / vibe-any), 上游 host 半 src/index.js 与纯函数核 src/compress.js 原样搬进本仓库 (save-token-host.js / save-token-core.js,两个文件头都写着相对上游删了什么)。 署名、改动范围与"为什么这样改"见 NOTICE。它不再是独立插件:没有自己的 roster 条目, 由 index.js 以 ctx.plugin(saveToken, config) 嵌套挂载,是这张设置页卡片的 host 半。

做什么:在 tools/post-execute 上把工具输出改写一次 —— 结构感知压缩(JSON / 表格类先认结构再 重排)、无损重编码(同一份数据换更短的表示,零损失)、重复调用去重 —— 原文写进 spillStore, 改写后的短文本留在上下文里,并附一条取回提示。模型需要原样内容时调 save_token_expand(按 locator 从 spillStore 逐字节取回)。压缩只在这一个点上发生一次:不去改写历史消息,已发出的请求前缀 逐字节不变,缓存命中不受影响。

  • 唯一一个模型工具:save_token_expand。此前本插件在工具表上是零占位,v1.13.0 起多一行。
  • 依赖 spillStore:拿不到它(宿主没提供 / 磁盘写不进去)时压缩永久关闭、工具输出原样进 上下文,设置页那张卡会显式写出"可逆存储(spillStore)当前不可用"与原因 —— 宁可不用,也不做 一份取不回来的压缩。
  • 路由(前缀注册 /cc/st,只服务本插件自己的设置页):GET /cc/st/api/dashboard、 POST /cc/st/api/set-enabled、POST /cc/st/api/reset。GET 的响应里没有 ok 字段(判断取数成功 看 flags),POST 返回 {ok:true|false};未知端点 404、内部错误 500。
  • 设置页卡片:4 个 KPI(模型请求 / 输入 token / 输出 token / 省下 token)+ 一条内联 SVG sparkline(灰=实际送出、绿=省下)+ 按工具归一化的省字节条 + 最近活动表(时间、中文类型标签、 只给省下的量)+ 「说明」抽屉。每 2.5 秒拉一次 dashboard,卡收起时照拉(数字要活着),卸载时清掉定时器。
  • 两个开关存内存:压缩 与 去重(默认都开)不写 settings.json,重启回默认; 计数同理(重启归零)。这是刻意的:这两个开关是"这一轮我想不想让它插手"的临时闸,不是配置。 ⚠ 它与本插件别处的开关不一样,别去 settings.json 里找它们,也别指望导出/导入带走。

主动不做的三件事(都是真做过判断的,不是漏了):

  1. 上游的 compaction assist 整段删除(上游 "arm 3":agent/pre-step 压力触发、水位线、冷却期、 compactStats、ctx.get('compaction'))。它和本插件自己的「省缓存」抢同一个压缩引擎 —— 当初(v1.13.0)的判断是"两套阈值同时改写同一个 preset 行必然互相打架",所以删除而不是默认关掉: 默认关掉只是把冲突藏起来,以后谁手滑打开就又撞上。 注:v1.14.0 起「省缓存」本身已随压缩接管一并移除(见上),这条记录的是当时的理由。
  2. 上游的 conversation.composer.dock 常驻小条不装。那个槽位归 dsh-bill(它在同一位置画每会话 费用行),同一格塞两块 UI 会互相挤掉;统计仍在,只是改由设置页这张卡呈现。
  3. 不落盘任何 prompt 文本(与上游口径一致:只留计数与字节数)。别名/标签可能出现在活动表里, 正文不会。

安装

前提:DSH Desktop(dsh CLI 可用),并在装完后重启桌面应用一次。DSH 关闭状态下任选其一:

  1. 从本仓库装(推荐):
    git clone https://github.com/Raylen-berry/dsh-cache-control.git D:\dsh-plugins\dsh-cache-control
    dsh plugin --profile web add link:D:/dsh-plugins/dsh-cache-control
    
    (Linux/macOS 把路径换成自己的绝对路径即可;link: 改动即生效,便于边改边试。)
  2. 手工接线:在 profiles/web/package.json 的 dependencies 与 dsh.profile.bundles 里加 dsh-cache-control,并把 profiles/web/node_modules/dsh-cache-control 做成指向本目录的 junction / symlink。

装完后 host 半(index.js,含会话守则段注册)随 profile 装载。

⚠️ 改了 client.js 必须重启,光刷新页面没有用(我此前说过"刷新即可",那是错的)。 依据(宿主 dsh-client-modules 的产物 + 实测):插件 client 半不是按请求从磁盘读的 —— 它在 服务启动时一次性 compose 成带 rev 哈希的 combo,挂在 /plugins 前缀下、以 cache-control: public, max-age=31536000, immutable 提供。原始路径 /plugins/dsh-cache-control/client.js 实测是 404(我先前在这里写过它,是错的), 只有 compose 后的哈希 URL 才有响应。唯一能让新字节进图的入口是 rebuilt(id), 而它属于 HMR watch(需要 pnpm run dev:web 在跑)。打包运行的桌面应用没有这个 watch ⇒ 磁盘上的新 client.js 只有重启才会进组合。重启后 rev 变了、index 注入的是新 URL, 所以浏览器那份一年期 immutable 缓存不会挡住新版。

此后:改规则 md 立即生效(host 每次组装重读磁盘);开关与滑杆改动即时写盘; 压缩参数对之后新建的会话生效;界面与 chip 的改动要重启才可见。

发布前检查(CI 与本地同一条命令)

push / PR 都会跑 .github/workflows/ci.yml,它做两件事:npm ci(只装 devDependencies)→ npm test。 本地跑的就是同一条命令:

npm install                    # 只装 devDependencies(就一个 react);CI 用 npm ci
npm test                       # = node tools/run-all.mjs
node tools/run-all.mjs --list  # 只看清单:跑哪些、以及哪些被排除、为什么

出网边界:只有 npm ci / npm install 那一步出网(按 package-lock.json 装 devDependencies)。 npm test 本身不出网 —— 不做真实下载、不调模型、不读 %APPDATA% 下的真实 settings.json。

可迁移性(2026-09-30 起):tools/ 下各套件不再写死开发机路径 ——

  • 插件目录默认按本文件位置推导(../),换盘符/换目录直接跑;
  • DSH_APP_MODULES 默认指向仓库自身的 node_modules/,所以先 npm ci 装出 devDependencies 即可, 不再依赖本机 DSH Desktop 的安装目录;
  • 各环境的 DSH_CC_PLUGIN / DSH_APP_MODULES / DSH_TOOL_HOME / DSH_BGA_CLIENT 覆盖项全部保留。

所以在任何机器上,只要 npm ci && npm test,就能拿到与 CI 一致的结果。

tools/run-all.mjs 把每套都跑完再汇总(不用 && 串,避免第一套一失败就看不到后面), 任一套非 0 退出 ⇒ npm test 退出码 1 ⇒ CI 变红。CI 用 Node 20/22/24 三档矩阵、windows-latest。

干净环境实测(DSH_HOME / APPDATA / LOCALAPPDATA / USERPROFILE / DSH_APP_MODULES 全指空目录, 独立下载的 node),三档(Node 20/22/24)结果一致 —— 5/5 套件通过:

套件结果
tools/verify-gate-truncation.mjs31 项通过
tools/verify-host-width.mjs全部 PASS
tools/verify-settings-payload.mjs9 项通过(v1.7.0 起含 hideDivider 对齐断言;v1.11.0 起含 reviewSkillEnabled 与"载荷锚点定位"两条)
tools/verify-panel-and-resizer.mjs25 项通过(v1.7.0 起两类把手分开关断言)
tools/verify-session-gate.mjs39 项通过(v1.9.4 起纳入;本机跑时另加真实 home 对照 3 条)

上表是 v1.9.x 那次"干净机器实测"的记录,当时确实只有 5 套。v1.10.2 起进 CI 的是 8 套: 多出 verify-ponytail-gate.mjs(23 项,v1.10.0 纳入)、verify-shape-gate.mjs (60 项,v1.12.0 纳入)与 verify-gate-client.mjs (v1.12.0 起本机 80 项 / 无真实 home 时 78 项 + SKIP,夹具化见下面那条)。 v1.13.0 起是 10 套:再加 verify-save-token.mjs(40 项;verify-settings-concurrency.mjs 更早已在列,本条只补上面漏掉的那 8→10 差额);verify-gate-client.mjs 本机 101 项 (v1.13.0 加 I 组 19 条;v1.13.1 再加 2 条源码守卫 —— 位置调用口径 + 设置页错误边界, 两条守的都是"1.13.0 整页白屏为什么 99 条没拦住")。

verify-panel-and-resizer.mjs 原来因为"要本机 DSH 安装目录的 node_modules/react"被排除。 现在 react 进 devDependencies,run-all.mjs 把 DSH_APP_MODULES 指向仓库自己的 node_modules/, 于是本地与 CI 都不再依赖任何人的安装路径(反向证据:把 DSH_APP_MODULES 指回空目录, 该套件立刻报 ERR_MODULE_NOT_FOUND: Cannot find module '<空目录>/react/index.js')。

verify-session-gate.mjs 被排除的原因(v1.9.4 已消除两条):① DEPLOYMENT_PERSONA TypeError ——根因是断言引用了宿主从未导出的符号名,套件在段序断言处崩、后面 20+ 条从没跑过;② 读 %APPDATA% 真实 settings ——换成"真实 home 存在才比对"。它 import 的 @deepseek-ai/dsh-system-prompt 走自己 devDependencies 里那份(createRequire 从仓库 package.json 解析,版本与本安装宿主一致),所以把 DSH_APP_MODULES 指到空目录仍 39/39。 v1.14.0 起该套件连 preset 夹具也不需要了(压缩接管删除后它只碰 settings.json / gate.md)。

verify-gate-client.mjs 同样在 v1.10.2 挪回 CI,消除的是三条本机依赖:① preset 从 %APPDATA% 复制 ⇒ 换成与 verify-session-gate 同款的最小夹具;② 收尾"真实 settings.json/gate.md 未被改动" 两条硬读真实 home ⇒ 真实 home 不存在时 SKIP 并如实打印(v1.12.0 起本机 80 条,CI 上 78 条 + SKIP); ③ primitives 桩原来要求"能读到宿主真包源码"才装载 ⇒ 改为始终按签名复刻桩,读不到只打一行 NOTE。 react-dom 随之补进 devDependencies(钉到与 react 同版本 18.3.1,原来只有 react、server.js 靠宿主目录)。 反向证据(本轮实测):APPDATA 指空目录后跑 npm test ⇒ 8/8 套件通过,该套件 78 passed / 0 failed + SKIP; verify-session-gate 同步退化为 36 项(它自己的真实 home 对照 3 条也走 SKIP)。 v1.14.0 起该套件不再需要 preset 夹具,真实 home 对照也从 3 条变 2 条(只剩下 settings.json / gate.md)。

未纳入 CI 的套件(原因同时写在 tools/run-all.mjs 的 EXCLUDED 里): verify-ui-appearance.mjs / verify-gate-http.mjs(要 %APPDATA% 下的真实 preset)。

verify-gate-http.mjs 本轮没有做夹具化:它先把真实 preset 复制到临时 DSH_HOME, 然后有几条断言是"逐字节比对真实 preset 有没有被本次验证改动" (fs.readFileSync(REAL_PRESET) + sha256)。换成仓库内夹具就得重写那几条的比对对象, 而那属于放宽验证口径 —— 本轮的规矩是只允许"等价或更强"的改动,所以保持排除并在此写明。

换台机器:可迁移性与必须手动的步骤

给后续在任何一台机器上接手的人或 agent:本插件装起来不需要任何手工点击, 但下面几条"换机后不生效 / 得手动做"的事,必须先看清楚,别以为"克隆下来就完事"。 (起因:用户 2026-09-12 反馈"工作电脑上传、回家发现可用性很差、必须手动操作"。)

A. 装(agent 可全自动)

git clone https://github.com/Raylen-berry/dsh-cache-control.git <你放插件的绝对路径>
dsh plugin --profile web add link:<同一个绝对路径>

link: 挂载的意义:改完即生效(开发态),不需要每次重装。

B. 必须重启 DSH Desktop(人工触发,agent 不能替你决定) client 半在服务启动时才 compose 进图(依据见上一节),所以"装完刷新页面"是没用的。 重启会掐断正在跑的会话轮次 —— 让用户自己挑时间。

C. 设置不随仓库走**(换机器后各开关全是默认关)** 所有状态都在 $DSH_HOME/dsh-cache-control/:settings.json(各开关 + 数值)、 gate.md(会话守则的自定义覆盖,可选)、ponytail.md(ponytail 的自定义覆盖,可选)、 shape.md(输出形状的自定义覆盖,可选)。 仓库里没有它们,因此换机器后要重新打开: 「会话守则」「ponytail」「气泡置顶」「对话页」—— 否则会表现为"插件装了但什么都没发生"。 (「自动审查」与「输出形状」按 DEFAULTS 默认开,换机器不用拨:前者只注册一个技能,ocr 没装也不报错, 只是用到时第一步会停下说明;后者是常驻规则,不想要那份 token 才需要去关。 「省 token」的压缩/去重两个开关不写 settings.json、只活在 host 进程内存里,换机器后本来就是默认开, 所以它既不用拨、也不会被导入导出漏掉。)

D. 「省缓存」改的是 preset(v1.14.0 起已移除) 它原先把参数写进 $DSH_HOME/profiles/**/standard/agent.yml 里 compaction-basic 那一行 (带 # managed by dsh-cache-control 标记)。DSH 0.1.7 起该组装文件不再存在、这条路径已整条删除, 换机器不再需要任何与压缩参数有关的操作(理由与善后见「压缩接管:为什么删掉」)。

E. 已知的宿主坑:插件会被 generation 迁移搬走(本机踩过) 部分 DSH Desktop 版本在启动时会做 installGeneration 迁移,会把 link: 挂载的插件重新 stage 一遍,期间把一个绝对路径当相对路径拼接⇒ ENOENT、迁移被 defer, profiles/web/.install-complete 永远写不出来的同时插件也可能不加载。 本机的处置是给应用 bundle 打一个本地补丁(把本插件加进 KEEP_IN_SHARED_TREE)——该补丁不在本仓库里, 它属于"每台机器各自的 DSH 应用目录"。识别方法:启动日志出现 migration deferred / could not stage,或 profiles/web/.generations-deferred.json 反复生成。 遇到就按本机 dsh-local-patches/README.md 的脚本处理(DSH 每次升级都会覆盖该补丁,升级后要重跑)。

G. 设置导出/导入(换机器一键搬配置,v1.5.0 新增)

node tools/settings.mjs export --out D:\cc-settings.json    # 旧机器
node tools/settings.mjs import D:\cc-settings.json --yes    # 新机器(覆盖前自动备份 settings.json / gate.md)

show 看当前值;不带 --yes 演练。它连 gate.md(会话守则的自定义规则)一起搬 —— 这是本插件最不该手抄的东西。导入会按与 host sanitize 同口径的规则钳制 (触发点 5–95、保留尾部 < 触发点、底衬模糊 0–24 且 1 位小数、钉顶上限 12–80vh、 对话页宽度 30–100% 且旧 px 值归一 80%),未知字段丢弃并列出来; host 读盘时还会再 sanitize 一次,所以口径即使漂了也不会写坏引擎侧。 ⚠ 导出/导入只搬 settings.json 与 gate.md:ponytail.md / shape.md 两个 override 要自己拷 (它们就在同一个目录下)。v1.12.0 修掉一处静默丢键:这个脚本的 DEFAULTS 曾经漏了 ponytailEnabled / reviewSkillEnabled,于是"导出再导入"会把这两个开关悄悄抹回默认 —— verify-settings-payload.mjs 只比对 host 与 client,管不到这个脚本,所以以后加开关记得三处都补。 (v1.14.0 注:原「省缓存」本来就不在这个文件里 —— 它改的是 $DSH_HOME 里 standard preset 的那一行; 该功能已随 DSH 0.1.7 移除,现在整份配置都能靠导出/导入带走。)

F. 换机后自查(30 秒)

node tools/verify-audit.mjs 2>$null; node tools/verify-host-width.mjs   # 期望全绿 / 无 FAIL
# 设置页应出现「会话策略」八个分区(标题无编号);对话页默认 80%(百分比,v1.5.0 起)
# 「省 token」那张卡应显示 4 个 KPI、按工具省字节条与最近活动;没跑过工具时是"还没有…"空态

验证

回归与探针脚本都在本仓库 tools/ 下(只用于开发,不进 npm 包,见 package.json 的 files)。 分两层:逻辑回归(Node 里跑,写盘全部落在临时 home,不碰你真实的 $DSH_HOME)与 浏览器实测(无头 Chrome,把宿主产物里的真实 CSS 规则与真实类名塞进复刻约束的夹具,判定用数字不用肉眼)。

node tools/verify-session-gate.mjs    # 规则解析 / 花括号防御 / 截断 / 压缩行无回归
node tools/verify-ponytail-gate.mjs   # ponytail 段:独立开关 / 独立缓存 / 截断 / override 往返
node tools/verify-shape-gate.mjs      # 输出形状段(v1.12.0):默认开 / 逃生开关 / 三段独立 / 技能同源 / 不重复注入
node tools/verify-gate-http.mjs       # host 半真起 http 服务:路由、两开关正交、异常输入
node tools/verify-gate-client.mjs     # client 半真渲染:磁盘 → 路由 → STORE → DOM(含抽屉展开态)
node tools/verify-ui-appearance.mjs   # 外观引擎 + 置顶跟随滚动选条 + chip 点击语义 + 对话页宽度钉法
node tools/verify-host-width.mjs      # host:字段钳制 + 从底图工坊的一次性迁移(临时 DSH_HOME)
node tools/verify-gate-truncation.mjs # 规则截断:原/留长度 + 显式标记 + 边界与两个 HTTP 响应契约
node tools/verify-save-token.mjs      # 省 token(v1.13.0):嵌套挂载面、expand 工具、字节往返、/cc/st 路由

「省 token」(v1.13.0)的断言分两处:host 半在 verify-save-token.mjs(真压缩一份 15,862 B 的 工具输出 → 6,526 B,再用 save_token_expand 逐字节还原比对;无 spillStore 时原样透传; /cc/st/api/* 三个端点含 404 与开关键名),client 半在 verify-gate-client.mjs 的 I 组 (喂夹具渲染纯视图:KPI 换算 / 字节条归一 / 空态 / spillStore 不可用降级 / 点击回调送出正确的键与值)。 取数那半步(useEffect 里的 fetch + 2.5s 轮询)在 SSR 套件里跑不到(renderToStaticMarkup 不执行 effect),1.13.2 起由 verify-token-lifecycle.mjs 用真实 React 挂载补齐:取数、竞态、点击回调、卸载中止、 超时、重试、后台暂停与前台恢复共 23 条;视图因此拆成独立的纯函数 renderSaveTokenView(组件 SaveTokenCard 负责连接取数与 视图,internals.SaveTokenView 是它的旧名别名)。

本机自检:改 client 半不必重启 DSH(v1.13.1 实测)。__DSH_BOOT__.entries[].rev 是整包一次修订 (本机 66 个条目共用前缀 d168e781a0c293b0- + 各自序号,不是逐文件哈希):内容一变前缀就变,所以 改完 client.js 存盘、刷新页面即加载新代码,还能用 fetch('/plugins/??<id>/client.js&rev=<新 rev>') 读出自己刚加的字符串自证。host 半相反: index.js / save-token-host.js 只有 name/inject/group 变了才重新 import,改函数体 必须重启 DSH Desktop。但"白屏"与"跑不到"是两回事:client 半的新代码即使加载成功, 渲染期抛错在 production React + 槽的错误边界下一个字都不打(1.13.0 就是这样白屏的), 所以排障第一步是刷新后把 console.error 钩住、或直接看面板里 PageBoundary 是否吐了堆栈。

「自动审查」的断言分挂在两处:保存行为(逐一触发每个设置控件,审查开关走独立接口, 普通设置只提交改动项,防止旧快照覆盖其它接口的最新值)与 client 渲染(八个分区标题名字齐全且顺序正确、页面里不许出现圈符编号)。ocr 探测本身是 子进程调用,不进套件(CI 上没有 ocr,且它属于"环境事实"而不是逻辑);改探测逻辑时手工跑一次 node -e "import('./index.js').then(h=>h.probeOcr().then(console.log))" 看结果。

浏览器侧(会往 tools/*-out/ 落 HTML/JSON/PNG,已在 .gitignore 里):

node tools/probe-userrow.mjs          # 气泡贴文字 / 复制键在气泡右侧轨道 / 上下留白对称(跑的是 client.js 里的真函数源码)
node tools/cc-appear-probe.mjs        # 钉顶底衬形态与模糊度:--dump-dom 让页面自量自报
node tools/cc-fixture.mjs             # 面板朝向与 portal:插槽/portal × 旧朝向/新朝向
node tools/cc-appear-fixture.mjs      # 同一批判定的 CDP 版

脚本里的默认路径是本机(Windows + DSH Desktop)的绝对路径,换机器用环境变量覆盖即可: DSH_CC_PLUGIN(本插件目录)、DSH_APP_MODULES(宿主 node_modules)、DSH_CHAT_BUNDLE (dsh-client-ui-chat/lib/client.js)、DSH_CC_INDEX、DSH_TOOL_HOME(临时 home)、 DSH_TOOL_OUT(夹具输出目录)、CHROME(Chrome 可执行文件)。 verify-gate-client.mjs 里对底图工坊的交叉断言在找不到对面插件时会自动 SKIP(DSH_BGA_CLIENT 可指定)。

夹具里量出来的关键数字(写在这里,下次改动好比对是否退化): 旧朝向面板可见比例 0.021(超出视口底部 411px);新朝向完整可见。 钉顶:position:sticky、top:0px、被钉元素 y:0 h:84、travelPx:676、滚动 1298px 后仍在滚动区顶; 透明:开 rgba(0, 0, 0, 0) / 关 rgb(47, 47, 52)(后者是宿主 --dsw-specific-bubble 的真值)。 底衬(2026 形态,cc-appear-probe.mjs 实量,夹具视口 1280 / 列宽 748):整行 rowBg rgba(0, 0, 0, 0) + rowBackdrop none(左侧干净);::before 底衬 v1.4.2 起按该条提问实测宽度:夹具里 411px 宽的气泡 ⇒ --cc-pin-w ≈ 423.4px (= 411 + 根字号 15 × .8,向上取整),对整行 748px ⇒ 左边留出 ~324px 不糊;短句提问则底衬跟着变短。 (v1.4.2 之前是"定长":423.4px = 748×.55 + 12,短消息时明显比气泡宽;v1.4.0 之前是 537.094px = 748×.702 + 12。) border-radius:16px; position:absolute / z-index:-1;右缘 right:-6px 与气泡右缘差 6px; --cc-pin-blur 未设 ⇒ blur(10px),设 0px ⇒ blur(0px),设 20px ⇒ blur(20px)。 注意夹具的坑:最后一条提问下面若没有长回答,sticky 没有移动量,量出来会误判成"没钉住"; --dump-dom 那条还必须给每个状态独立的 --user-data-dir,否则后启动的实例会把活儿交给 已在跑的浏览器进程、自己退出,dump 出来就是空文件。

卸载 / 回退

  1. 关闭会话守则 / ponytail / 输出形状三个开关即可让对应规则段消失(空段在渲染时被丢弃)。 v1.14.0 起不再需要为「省缓存」做任何还原动作 —— 压缩接管已整条移除,它当年可能写进 agent.cordis.yml 的 # managed by dsh-cache-control config 块也随该文件一起不存在了。
  2. 在 profile 移除依赖与 bundle 项、删除 junction;或 dsh plugin --profile web remove dsh-cache-control。
  3. 重启应用。插件停用/卸载后不残留任何行为改动(gate.md override 与 settings.json 是数据,需自行删除)。

版本与变更记录

下面这份清单停在 v1.7.x(1.8–1.12 的条目都在 CHANGELOG.md 里,没往回补)。新增条目往头上加。

  • v1.14.0(删掉「省缓存 · 压缩策略」整条写盘路径)—— 详见 CHANGELOG.md 的 1.14.0 条目与上面 「压缩接管:为什么删掉」。要点:DSH 0.1.7 起 agent preset 不再是可编辑的 agent.cordis.yml (安装里已无该文件,包由 dsh-agent-presets 拆成 dsh-agent-preset + dsh-agent-preset-registry), 接管没有落点、每次开设置页必然 500 ⇒ 删除读组装文件 / splice / 备份还原 / boot 对账 / GET·PUT 的 applied·backupAt·hasBackup,以及设置页那张卡与 chip 的「省缓存」段;compactionBackup 保留为只读历史数据。分区 9 → 8、chip 4 段 → 3 段、DEFAULTS 16 → 12 个键。

  • v1.13.1(修 1.13.0 的回归:设置页点「会话策略」整页白屏)—— 详见 CHANGELOG.md 的 1.13.1 条目。 要点:SaveTokenCard 把纯视图误写为元素调用 h(SaveTokenView, { d, … }),视图签名是 (夹具, 回调), props 对象被当夹具 ⇒ 读 d.flags.expandTool 抛错 ⇒ CacheControlPage 整棵树消失;production React 在槽的错误边界里不打日志,所以"一片空白而且控制台干净"。改为同口径的位置调用 + 新增 PageBoundary (抛错把堆栈显示在面板里并 console.error)。verify-gate-client 99 → 101 条(两条源码守卫: 调用口径、错误边界),版本 → 1.13.1。

  • v1.13.0(并入原 dsh-plugin-save-token;本插件第一次有模型工具)—— 详见上面「省 token · 结构化压缩」 与 CHANGELOG.md 的 1.13.0 条目。要点:save-token-host.js / save-token-core.js 两个文件原样入库 (MIT,NOTICE 有署名与删改清单),以 ctx.plugin() 嵌套挂载;上游 compaction assist 整段删除 (与「省缓存」抢同一个引擎),上游 composer.dock 小条不装(那个槽位是 dsh-bill 的); 新增工具 save_token_expand 与 /cc/st/api/* 三条路由;files 补两个文件、版本 → 1.13.0; npm test 10/10 通过(新增 verify-save-token.mjs 40 条 + verify-gate-client I 组 19 条)。

  • v1.7.0(把"隐藏拖拽条"拆成两个开关:两竖杠与侧栏分隔条分开管)

    • 起因:v1.6.0 的 hideResizer 一条开关按 cursor:*-resize 语义标记,实测会同时命中三类元素: 会话区左右两条宽度把手(._8JRpoa_widthHandle,就是"误触冒出两竖杠、拖了没反应"的那两位) 与 AppFrame 的侧栏/详情栏分隔条(._1tdjgG_handle,8px 宽)。前两位拖了确实没反应, 但分隔条是能用的(拖它会改侧栏宽度)——一个开关把死控件和活控件一起藏了(用户 2026-09-14 要求拆开)。
    • 改法:hideResizer 语义收窄为只管带 data-width-handle 属性的宽度把手(存量盘上值不动, 升级后"两竖杠仍被藏、分隔条自动回来");新增 hideDivider(默认关)管分隔条。 两类共用 cursor 含 resize 的语义识别,再用宿主写在把手元素上的 data-width-handle 数据属性区分 (它不是 CSS-module 哈希,耐升级);两套标记各一个属性(data-cc-hide-resizer / ), 各一条 CSS,互不牵连:关一个开关不会把另一类也撤了。 与 同样是 ,旧 host 重启前只禁用新开关,不影响已有的。

说明:本插件只写 $DSH_HOME/dsh-cache-control/* 与 standard preset 的 compaction-basic 行。 任何替你改动运行时开关值的行为都应算作越界——v1.1.0 曾直接写入过 gateEnabled: true,已记在此处。

说明与限制

  • 会话守则是软约束:它让规则每请求都在提示词里、且不被压缩稀释,但不产生技术硬拦截 —— 模型仍可违反。 真要拦截得走工具层/审批钩子,那是另一件事。
  • 修改点位于打包目录(app 安装的 node_modules,profile 以 junction 指向它); 若 DSH 升级重建该文件,插件启动时会自动对账并重新应用当前设置(写回标记行)。
  • 浏览器端刻意不编辑组装文本;本插件的“编辑”由其 host 进程完成,页面只有开关、滑杆与规则编辑器。
  • 数值换算显示用 ROUTED_CONTEXT_WINDOW = 1_000_000 这个常量(当前路由 qwen3.8-flash 声明的窗口 也是 1,000,000,所以对你这台机器是对的);若换到别的窗口的模型,界面上的 token 数会失真, 但引擎侧是比例式阈值,实际触发点仍按窗口同比变化。token 数为 token-meter 的估算口径。
  • v1.2.0 起 chip 固定为三段:省缓存 [开/关] | 提问 [开/关] | ▾,前两段点击即切换、 ▾ 弹滑杆与规则面板;标签不随状态改名(v1.1.0 那套"两个都开就叫会话策略"已去掉)。
  • tools/verify-session-gate.mjs 的既有失败已于 v1.9.4 修复并纳入 CI(此前记录:从宿主包取 FIRST_PARTY_SECTION_ORDER.DEPLOYMENT_PERSONA,而该导出名从未存在 ⇒ 段序断言处崩、后面 20+ 条从没跑过)。 修法见 CHANGELOG 1.9.4:段序判据改读已解析包的源码文本数值(persona=0 / plan=500), preset 依赖换成仓库内最小夹具。这段留在这里是提醒:测试脚本与宿主导出脱节时,崩在中途的套件 会伪装成"只坏了前半截" —— 排除理由里写"既有失败"之前先数一下它到底跑到了第几行。
try/catch
warnOnce
document.fonts.ready
  • 设置页「气泡置顶」卡里带一行底衬实测读数:被钉元素有/无、--cc-pin-blur、 getComputedStyle(el,'::before').backdropFilter、底衬宽、会话字号 +「重读」按钮 ⇒ 以后"看起来失效"能当场分辨是哪一种成因。
  • 只处理纯文字气泡;data-cc-fit 记签名,流式输出不会每帧重排;停用插件时 stopFitWatch() 把 stack.style.width / --cc-tail-* / data-cc-fit 全撤干净。
  • 时间戳零占位:平时 opacity:0 却仍占位 ⇒ max-width:0;padding:0;overflow:hidden, :hover 才展开(展开后的内边距也是 em)。
  • 可调点:userWidthPct(设置页「提问气泡宽度」滑杆,30–100% 消息列宽,默认 58, 写 --cc-user-width-pct,只管普通提问气泡;v1.16.7 新增;v1.16.9 曾改成会话区、 v1.16.11 按用户实测改回消息列**)、 pinWidthPct(设置页「钉顶气泡宽度」滑杆,同样 30–100% 消息列宽,写 --cc-pin-width-cap, 只管被钉住那条;v1.16.12 起两个滑杆才真正分开 —— 此前它们共用一个变量, 于是"提问气泡宽度"实际在控钉顶、普通气泡没有 UI 可调)、 --cc-col-w = 消息列实测宽(气泡宽度基数,已含「对话页宽度」% ⇒ 两个设置相乘)、 --cc-area-w = 会话区实测宽(同一函数 measureWidthVars() 写入;量不到时 CSS 退回比例基数)、 --cc-tail-room-px(轨道宽 = 复制键离框缘的距离,JS 自动量、也可手动覆盖)、 FIT_ICON_H(仅量不到图标时的兜底高度)。 v1.16.10:USER_BUBBLE_MAX_EM(41em)已删 —— 每个气泡的宽度只有各自那个百分比旋钮, 之前它是 min() 的第二条上限,导致百分比拉到 100% 也只到 ~645px。钉顶底衬宽度自 v1.4.2 起按这条提问的实测宽度**写入 --cc-pin-w(量不到才退回 消息列宽×.55 + .8em)。
  • data-cc-hide-divider
    dividerReady
    resizerReady
    独立能力位
  • 顺带补漏:tools/settings.mjs(导出/导入)的字段名单停在 v1.4.x,hideResizer 从来不在里面 —— 导入一份开着把手隐藏的设置会被"丢弃未知字段"静默抹掉。现在 hideResizer / hideDivider 都在名单里。
  • 验证:verify-panel-and-resizer 从 19 条扩到 25 通过 / 0 失败(新增:只开一个开关时另一类 不被标记、关一个不撤另一个、能力位单独禁用、isWidthHandle 分类、setHideDivider no-op 路径); verify-settings-payload 9 通过(新增 hideDivider 载荷对齐断言);verify-host-width 全绿 (sanitize 键齐全断言扩到 hideDivider);npm test 套件 4/4 通过。
  • v1.6.2(修"截断标记判错":规则被砍了,界面却说没截断)

    • 症状:会话守则规则超过 6,144 字节时会被截断,但界面不显示"已截断",用户以为整份规则都进了 提示词 —— 实际超出部分从没被模型看到。
    • 根因:host 的截断函数只返回一个字符串,界面判断"有没有截断"是靠结果长度猜的: truncated: bytes >= GATE_MAX_BYTES。而截断后的长度必然小于上限(末尾那句省略提示算在内 也只有 5.6–6.1 KB 一档,且随原文的字节/字符比浮动)⇒ 该判据对已截断的文本恒为 false。 反方向也错:原文恰好 6,144 字节时文本原样返回、bytes === maxBytes,它又把没截断的判成截断。
    • 修法:截断函数 truncateBytes() 改为直接返回全部事实 { text, originalBytes, keptBytes, truncated },调用方一律读 truncated;gateMeta 与 PUT /cc/gate.json 的响应原样带出这三个事实(bytes/maxBytes/lines/text/source/enabled 等老字段一个不动,老客户端不受影响);界面标签与提示改成 「已截断:原 N B → 保留 M B」+「你的规则被截断了:原文 N 字节,实际注入 M 字节(超出 X 字节未进入提示词)」。
    • 同类第二处:全仓库再找了一遍"靠长度/结果反推状态"的写法,结论是只有这一处是缺陷。 保留的同类写法都有正当理由,逐条记录在这里:readBody 用显式 overflow 布尔标记(正确做法); truncateBytes 里的 buf.length <= max 与界面编辑框的 draftBytes > gateMaxBytes、 tools/settings.mjs 的"导入前预警"都是输入侧判据(比的是"待处理文本自己 vs 上限", 不是在反推已经发生的截断);client.js 的 atBottom = scrollTop >= max - 1 是位置判定 (滚动几何的即时谓词),与"用产物长度推状态"不是一回事。
    • 回归:新增 tools/verify-gate-truncation.mjs(v1.6.2 时 31 通过 / 0 失败;v1.9.0 把上限 放宽到 16 KB 后,样本长度与期望值改为相对上限生成,现为 30 通过 / 0 失败 —— 本条其余 数字是当时的记录):① 超长(中文 19,998 B / ASCII 10,000 B)⇒ truncated:true 且 originalBytes/keptBytes 齐备(中文那条正好复现审计给的 5,839 B);② 恰好 6,144 B ⇒ false 且内容逐字节原样;③ 6,143 B ⇒ false;④ 空/极短 ⇒ false 且内容原样;⑤ 六种输入的"标记 / 原文长度 / 保留长度 / 实际内容"四者互相对得上(没有"标记为假 但内容变短"的样本);⑥ 老字段名齐全;⑦ 两个 HTTP 响应(GET /cc/gate.json、GET /cc/settings.json、PUT /cc/gate.json)都带标记且与文本一致;⑧ 界面接线(读标记、不自己比长度)。 另有两条照妖镜断言直接把旧判据钉死:同一个样本上断言"bytes < maxBytes 而标记为 true"与 "bytes === maxBytes 而标记为 false"。verify-gate-client.mjs 从 67 条扩到 72 条 (新增 5 条:真渲染出的 HTML 里出现「已截断:原 19998 B → 保留 5839 B」、那句人话含两个字节数、 用警示色类、数字与 host 响应一致、未截断时一个截断字样都不出现)。
    • 反向验证:把新套件用 DSH_CC_INDEX 指向 git show HEAD:index.js 那份改动前实现 ⇒ 25 失败 / 6 通过(退出码 1);指回本仓库这份 ⇒ 31 通过 / 0 失败。这证明断言真的盯着这个缺陷 而不是"假绿"。
    • 验证数字:本仓库套件全绿 —— verify-gate-client 72、verify-gate-http 43、 verify-gate-truncation 31、verify-host-width 51、verify-ui-appearance 50、 verify-panel-and-resizer 19、verify-settings-payload 8,全部 0 失败; verify-session-gate 仍为既有失败(见文末"说明与限制",与本轮无关)。
  • v1.6.1(修一个破坏性配置缺陷:保存外观开关会抹掉用户自己的压缩配置)

    • 症状:只改 ③ 气泡置顶 / ④ 对话页的任何一个外观值(钉顶、气泡透明、模糊度、宽度、 隐藏拖拽条),standard 组装文件就被重写一遍;如果当时总开关是"关",那一行块里 用户自己写的压缩配置会直接消失(回落到 DSH 出厂默认 0.8 / 0.16)。
    • 根因:保存入口 PUT /cc/settings.json 无条件 applyToStandard(settings),而 applyToStandard 在 enabled === false 时执行 spliceCompactionRow(text, null) —— 也就是"删掉那一行块里的 config"。链路与 tools/verify-host-width.mjs 里的断言一一对应。
    • 修法(三条,全部有断言):
      1. 只有压缩字段变化才碰那个文件:compactionFieldsChanged() 只看 enabled / triggerPct / retainPct / auto;外观/门禁字段的保存完全不触碰组装文件 (响应里 touched:false 即"这次没碰")。
      2. 接管前先备份、关闭时还原:首次接管时把接管前的整篇原文存进 settings.json.compactionBackup = { text, at };关闭开关时优先逐字节还原这份原文, 还原后清掉备份(下次再开 = 重新接管、重新采备份)。只有没有备份可还原时才回落到 旧的"摘掉受管 config"。受管行的识别沿用原有 # managed by dsh-cache-control 标记,没新造标记。
      3. 幂等:保留 next === text 早退 —— 重复保存同一份设置不会产生第二次写盘; 重写行块时只替换"标记 + 其后的 config 块",name: 与用户手写的注释原样保留。
    • 修的过程中被断言抓出来的两个次生问题(都已修,都有断言):
      • CRLF:原来 split('\n') 把 \r 留在行尾,config: 的整行比较会失败 ⇒ 旧 config 不被替换、 反而是又追加一份(YAML 重复键)。改成 split(/\r?\n/)。
      • 未受管时也追加:首次接管(文件里本来就有用户的 config: 块)时,旧 config 没被摘掉 ⇒ thresholdRatio 出现两次。现在"行块末尾的 config 块"一律整段替换。
    • 回归:tools/verify-host-width.mjs 从 17 条扩到 51 条(本次新增 34 条,走真 HTTP 路由, DSH_HOME 指临时目录):① 只改外观(7 个字段一次提交)⇒ 该文件哈希与 mtime 都不变; ② 改压缩字段 ⇒ 确实被改(哈希变化、thresholdRatio 只出现一次、用户注释仍在、备份 === 接管前原文); ③ 重复保存同一份设置 ⇒ changed:false + 哈希/mtime 不动;④ 关闭 ⇒ 逐字节还原、 用户原有的 0.66/0.11 回来、备份被清、再关一次不再写盘;④b 无备份回落路径; 启动对账(开关=开但文件无受管行 / 开关=关但文件有受管行)两条路径同样受这套语义约束; ⑤ sanitize 带出 compactionBackup(任何一次写盘都不许吃掉它);⑥ splice 幂等与纯函数性。
    • 验证数字:verify-host-width 51 通过 / 0 失败、verify-gate-http 43(新增 3 条: 备份===接管前原文、只动门禁的保存 touched:false、关压缩逐字节还原 + 真实 preset 逐字节未变)、 verify-gate-client 67、verify-ui-appearance 50、verify-panel-and-resizer 19, 全部 0 失败;verify-session-gate 为既有失败(见文末"说明与限制")。
  • v1.6.0(两个独立改动,同一个文件所以同一笔提交:修一个真 bug + 加一个开关)

    • 修:滑杆面板 ▾ "点不开"(用户 2026-09-14 反馈)。不是点击没接上 —— 点了 aria-expanded 会变 true、DOM 里也有 .cc-panel,但它被摆到屏幕外(实测 rect.y = 33554004、 内联 bottom: -6.71e7px)。根因是那次"一次性校正"的 effect 量纲混用:期望值写成"距视口底部的距离" (vh - c.top + 6),实测值 r.bottom 却是视口 Y 坐标,两者相减恒不为 0;而 patch 又把 dy 累加回 bottom、依赖数组里还含 pos.bottom ⇒ 每帧误差翻倍,二十来帧就发散到 -6.7e7px。 修法:定位数学抽成纯函数 computePanelPatch()(两边统一用视口 Y,一次补到位即收敛), 并加 ±400px 量级闸门 —— 任何不可信的测量只能被忽略,不可能再把面板丢出屏幕。 回归:新增 tools/verify-panel-and-resizer.mjs(19 通过 / 0 失败),把"已对齐返回 null / 有偏移一步到位 / 一步后收敛 / 离谱值被闸门挡掉 / NaN 挡掉 / 翻转分支同量纲"全部锁住。
    • 增:④ 对话页新增 hideResizer(默认关),可选隐藏 DSH 原生分栏把手,理由与实现见上面 ④ 那节。 host 侧 sanitize 只负责原样存取;client 侧用一个独立能力位 resizerReady 判断 "host 认不认识这个键",不并进 appearanceReady —— 否则旧 host 下会把已有几个外观开关一起禁用。
    • 验证:本仓库套件全绿(verify-ui-appearance 50、verify-gate-client 67、verify-gate-http 40、 verify-panel-and-resizer 19、verify-host-width 通过;verify-session-gate 是既有失败, 见文末"说明与限制")。
    • 顺带:README ④ 那节的表格还写着 640–3840px(v1.5.0 已改成百分比却没同步),一并改正。
  • v1.5.0(④ 对话页:宽度单位 px → 百分比)

    • 起因(用户 2026-09-12 原话):"改成百分比,具体的数值不仅会随着全屏或是缩小有变动, 还会因为显示器的比例出现不协调。"固定 640–3840px 在全屏/缩窗时不跟着走,换显示器比例就失配。
    • 改法:chatWidth 语义从 px 变成 30–100 的百分比,三个变量写同一个 P% —— --dsh-chat-content-width / --dsh-composer-card-max-width / --dsh-chat-user-width 都写 P%(:root 兜底那条同样写 %);滑杆 30–100、步长 1,快捷键改成 60/70/80/90/100%。
    • 真浏览器实测(本机会话区 clientWidth 1139,在真会话里量消息列与输入卡): 80% ⇒ 消息列 860px、输入卡 886px;60% ⇒ 645 / 664;100% ⇒ 1075(满宽)/ 1107。 即三个变量都是"可用内容区的 P%"(100% 时 1075 = 1139 − 两侧 32px 内边距), 而输入卡恰好 = P% ×(内容区 + 32) ⇒ 卡片永远比消息列宽 32×P 像素 —— 与 px 时代"+32px 出挑"的关系完全一致,只是整体按比例缩放。 (一度以为会"复合两次"导致卡片过窄:那是在空会话页量的,那页的 composer stack 宽度不同; 真会话里不复合。这条记在这里,免得下次又被空页面误导。)
    • 迁移:盘上的旧 px 值(>100,例如你现在的 900)不再按 px 用,一律落到默认 80% —— 900px 在本机 1139px 会话区里正好≈79%,观感等价;由 normalizeChatWidth() 在 host 与 client 两端同口径处理(sanitize / clampChatWidth), 底图工坊那次一次性迁移(migrateFromAtelier)也一并走这条口径。
    • 验证:verify-host-width.mjs 全绿(新增"60 原样 / 10→30 / 900 与 3840 旧 px → 80"四条); verify-ui-appearance.mjs 50 passed / 0 failed(新增"三个变量都是 90%"与"旧 px 1600 → 80%"); verify-gate-client.mjs 67 passed / 0 failed。
    • 顺手修了 verify-gate-client.mjs 里一条早就假失败的断言:它要求底衬规则含 width:calc(min(,而 v1.4.2 起那层已经是 width:var(--cc-pin-w, calc(min(…))) (早于本轮的单位改动,与本轮无关,只是这次跑测试才发现)。 另把 verify-host-width.mjs 里"删掉文件再看它不存在"的断言改成哨兵文件写法 —— 本机沙箱会把 fs.rmSync 拦成空操作(实测 rmSync 之后 existsSync 仍为 true), 那种写法会假失败,改成"跑完看哨兵内容有没有被改写",反而更直接地证明"不写盘"。
  • v1.4.2(钉顶底衬改"实测宽度" + 长文限高)

    • 底衬宽度不再"定长":新增 updatePinPlate(),在钉住哪一条确定后、以及每次气泡重排 (fitUserBubbles)结束时,量出被钉行里 [class*="_userRow"] 的实际像素宽 + 根字号 × .8 呼吸位,写进该行的 --cc-pin-w(短句 ⇒ 短底衬);量不到就 removeProperty, 由 CSS 里的旧上限 min(列宽×.55, 上限) + .8em 兜底。
    • 长提问钉顶不再无限撑高:气泡本体 max-height:38vh + overflow-y:auto + overscroll-behavior:contain + scrollbar-width:thin —— 超出时在气泡内滚,滚到底才交还会话流。
    • 只动 client.js 与版本号;host 半、路由、settings 字段均未变,不需要重启(客户端热更新即可)。
  • v1.4.1(根因修复 + 尺寸自适应)

    • applyAppearance 每段各自 try/catch + warnOnce:一处抛错不再连带把钉顶/贴合观察器全部 跳过(那会让"底衬模糊度"看着像失效);首屏补量两次 + document.fonts.ready 后清签名重算。
    • 气泡量测不再按 children.length 整条跳过(带 @路径 <span> 的提问曾被全漏 ⇒ 框宽退回固定 上限),改为只跳过真含 img/video/canvas 或内嵌块的气泡;轨道宽与纵向对齐改为实测图标行。
    • 尺寸全面 em 化:气泡内边距/圆角/图标尺寸/轨道宽/上限(USER_BUBBLE_MAX_EM 取代 px 常量)/ chip 徽标与标签位移一律 em 或实测值,跟随会话字号与页面缩放自适应。
    • 设置页 ③ 新增底衬实测读数自检行 +「重读」按钮;提问气泡图标区隐藏宿主 tooltip。
  • v1.4.0(改名 / 气泡微调 / 小数档)

    • 分区名改:② 门禁 → ② 会话守则(chip 工具提示、设置页、规则 md 标题同步); ③ 外观 → ③ 气泡置顶。
    • 我的提问气泡微调(始终生效):宽度上限系数 .702 → .55;上下 padding 压到 6px (行距/字号不动);时间/复制那一行从气泡下方挪到气泡文末右侧同排,放不下才换行; 钉顶底衬公式同步改用 .55(实测列宽 748 ⇒ 气泡 411px / 底衬 ~423.4px)。
    • pinBlur 支持 1 位小数:<3.5 按 0.1 步进(可到 1.3 / 1.5 / 1.7),≥3.5 按 0.5 步进; host sanitize 同口径保留 1 位小数。
  • v1.3.0(本轮四项)

    • 钉顶底衬改形态:从"铺满被钉住的整行"改为画在 ::before 上的定长圆角矩形, 整行不再有 background / backdrop-filter ⇒ 会话列左侧不再被模糊;新增 可调模糊度 pinBlur(0–24px,写在 <html> 的 --cc-pin-blur 上,拖滑杆不重注入样式)。
    • chip 的「开 / 关」徽标改纯透明:.cc-chip .cc-badge 三条规则去掉背景与描边, 状态只靠文字颜色;面板内的同名徽标不受影响。
    • 名称一律压到 2–4 字:设置页导航条目 会话策略 · 省缓存/守则 → 会话策略; 分区标题 → 省缓存 / 会话守则 / 气泡置顶 / 对话页;快捷面板分区头同步缩短。 长说明没删,都挪到各卡正文与 hover 里。
    • 接住 bg-atelier 的「对话页固定宽度」:新增 ④ 对话页卡(开关 + 640–3840px 滑杆 + 常用宽度快捷键 + 会话根重建时的补写观察器),并由 migrateFromAtelier() 在启动时 一次性把 $DSH_HOME/dsh-bg-atelier/settings.json 里的 chatWidth / chatWidthEnabled 搬进自家 settings.json(只在自家缺这两个字段时读对方,之后不再读)。
    • sanitize 新增三字段钳制:pinBlur 0–24(v1.4.0 起保留 1 位小数)、chatWidth 640–3840、chatWidthEnabled 只认 true。
    • 验证补三条:cc-appear-probe.mjs(无 CDP 的浏览器实量,本沙箱里唯一跑得通的那条)、 verify-host-width.mjs(钳制 + 迁移幂等)、verify-bga-trim.mjs(对面插件的回归)。
  • v1.2.0

    • 新增 ③ 会话区外观:pinLastUser(最近一条「我的提问」钉顶)、clearBubble(我的气泡背景透明)。 纯客户端样式 + 运行时打标记,设置项持久化在同一个 settings.json。 两项形态按用户选定实现:钉在会话滚动区顶部、钉住条目自带半透明毛玻璃底衬。 开关默认 false,不由插件替用户打开。
    • chip 改为三段可点:省缓存 [开/关] | 提问 [开/关] | ▾。点前两段直接切对应开关, ▾ 才弹滑杆与规则面板(不用进设置页);每段 hover 有独立介绍。
    • 修"点开面板看不见":旧写法 top: chip 下沿 + 6,而输入条贴在视口底部 ⇒ 面板整块开到屏幕外。 无头 Chrome 夹具实测(03-调试临时/cc-fixture.mjs,三个 variant 只动「父级」与「朝向」两个变量): 旧朝向可见比例 0.021(超出视口底部 411px);改成贴 chip 上沿 + max-height 锁进可用空间后完整可见。 同时把面板 portal 到 body(与宿主 dsh-client-ui-attachment 同源做法)——实测它不是主因: 朝向正确时留在插槽里也完整可见;portal 消除的是 contain:paint/"裁剪盒恰是包含块"这类组合。
    • 新增两道能力/状态闸:loaded(没成功读到设置就拒绝保存,防止默认值盖掉真实配置)、 gateReady / appearanceReady(旧 host 不认识的字段一律禁用对应开关,避免写盘被抹)。
    • 修 readBody 超限时 req.destroy() 打断连接的缺陷(客户端 ECONNRESET + Windows libuv 断言),改为排空后回 400。
  • v1.0.0 仅省缓存(压缩策略)单开关。