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_expand | system 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 坑(都踩过,别再改回去):
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 告警)。
- 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 里找它们,也别指望导出/导入带走。
主动不做的三件事(都是真做过判断的,不是漏了):
- 上游的 compaction assist 整段删除(上游 "arm 3":
agent/pre-step 压力触发、水位线、冷却期、
compactStats、ctx.get('compaction'))。它和本插件自己的「省缓存」抢同一个压缩引擎 ——
当初(v1.13.0)的判断是"两套阈值同时改写同一个 preset 行必然互相打架",所以删除而不是默认关掉:
默认关掉只是把冲突藏起来,以后谁手滑打开就又撞上。
注:v1.14.0 起「省缓存」本身已随压缩接管一并移除(见上),这条记录的是当时的理由。
- 上游的
conversation.composer.dock 常驻小条不装。那个槽位归 dsh-bill(它在同一位置画每会话
费用行),同一格塞两块 UI 会互相挤掉;统计仍在,只是改由设置页这张卡呈现。
- 不落盘任何 prompt 文本(与上游口径一致:只留计数与字节数)。别名/标签可能出现在活动表里,
正文不会。
安装
前提:DSH Desktop(dsh CLI 可用),并在装完后重启桌面应用一次。DSH 关闭状态下任选其一:
- 从本仓库装(推荐):
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: 改动即生效,便于边改边试。)
- 手工接线:在
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.mjs | 31 项通过 |
tools/verify-host-width.mjs | 全部 PASS |
tools/verify-settings-payload.mjs | 9 项通过(v1.7.0 起含 hideDivider 对齐断言;v1.11.0 起含 reviewSkillEnabled 与"载荷锚点定位"两条) |
tools/verify-panel-and-resizer.mjs | 25 项通过(v1.7.0 起两类把手分开关断言) |
tools/verify-session-gate.mjs | 39 项通过(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 出来就是空文件。
卸载 / 回退
- 关闭会话守则 / ponytail / 输出形状三个开关即可让对应规则段消失(空段在渲染时被丢弃)。
v1.14.0 起不再需要为「省缓存」做任何还原动作 —— 压缩接管已整条移除,它当年可能写进
agent.cordis.yml 的 # managed by dsh-cache-control config 块也随该文件一起不存在了。
- 在 profile 移除依赖与 bundle 项、删除 junction;或
dsh plugin --profile web remove dsh-cache-control。
- 重启应用。插件停用/卸载后不残留任何行为改动(
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 依赖换成仓库内最小夹具。这段留在这里是提醒:测试脚本与宿主导出脱节时,崩在中途的套件
会伪装成"只坏了前半截" —— 排除理由里写"既有失败"之前先数一下它到底跑到了第几行。