dsh-cache-control(会话策略:省缓存 / 会话守则 / ponytail / 输出形状 / 自动审查 / 气泡置顶 / 对话页 / 存储)
给 DSH Desktop(dsh 0.7.2-alpha,web profile)加八个互相独立的开关,设置页名称就叫
会话策略(4 字,八个分区标题 省缓存 / 会话守则 / ponytail / 输出形状 / 自动审查 / 气泡置顶 /
对话页 / 存储 一律 2–4 字,不带序号 —— v1.11.1 起去掉编号,理由见下面「为什么分区标题没有编号」):
chip 上 省缓存 / 提问 / 懒码 / 形状 四段各自可点,自动审查、气泡置顶、对话页、存储全部走设置页。
| 省缓存 · 压缩策略 | 会话守则 · 长期规则 | ponytail · 编码纪律 | 输出形状 · 回复形状 | 自动审查 · 按需技能 |
|---|
| 动的是什么 | standard preset 里 @deepseek-ai/dsh-compaction-basic 的 config | system prompt 里一个常驻段(规则文本) | system prompt 里的第二个常驻段(与守则同构、独立开关) | system prompt 里的第三个常驻段(同上,但默认开) | 宿主 skills 目录里的一个条目(不进 system prompt) |
| 生效时机 | 之后新建的会话(preset 按组装文件 mtime 分代) | 所有会话的下一个 model step(含当前会话,无需重启) | 同「会话守则」 | 同「会话守则」 | 模型/用户调用技能那一刻 |
| 会被压缩冲掉吗 | — | 不会:压缩只折叠对话历史,system prompt 每请求重发 | 同「会话守则」 | 同「会话守则」 | 不适用(压根不在 prompt 里) |
| 代价 | 无额外 token | 规则体积 × 每请求(含子代理/工作流子会话) | 约 1.3K token/请求 × 所有会话 | 约 1.2K token/请求 × 所有会话(默认就开着) | 注册零 token;成本只在真调用时 |
「气泡置顶」与「对话页」是纯界面开关,只改样式与 CSS 变量,不动消息数据、不动宿主代码;
「存储」只做各用途占盘统计与"移入回收"式清理。
只影响 standard 之外的说明:会话守则走宿主全局提示词层,对所有 preset、子代理、workflow 子会话都生效。
「气泡置顶」与「对话页」是纯界面开关,只改样式与 CSS 变量,不动消息数据、不动宿主代码;
「存储」只做各用途占盘统计与"移入回收"式清理。
只影响 standard 之外的说明:会话守则走宿主全局提示词层,对所有 preset、子代理、workflow 子会话都生效。
为什么分区标题没有编号(v1.11.1)
编号是位置属性、名字是身份属性。把位置写进标题,代价是任何一次插入都要把全部下游引用重排一遍:
v1.10.2 修的是 ponytail 曾写作 "②b" 导致页面上出现两个 ②;v1.11.0 插入「自动审查」又让后面三块全部顺延
(README、CHANGELOG、跨插件指路、测试断言各扫一遍)。v1.11.1 起标题只留名字,顺序由卡片书写顺序决定,
插一张卡只改一处。要指代某块直接说名字。
verify-gate-client.mjs 里两条断言钉住这件事:① 页面(含抽屉正文)不许出现圈符;② 八个名字必须齐全且顺序正确。
注意这里只剩行文里的列举序号不算违规 —— 断言扫的是渲染出的整页文本,所以 README/注释以外,
client.js 里给用户看的字符串也不能带圈符。
省缓存 · 压缩策略
设置页(设置 → “会话策略”)与对话区快捷面板里各有一条独立开关:
- 总开关:启用 → 把下列参数写进 standard preset 的 compaction-basic 行;
关闭 → 还原首次接管前那一行块的原文(v1.6.1 起;原文存在
$DSH_HOME/dsh-cache-control/settings.json 的 compactionBackup 字段里,逐字节回写)。
没有备份可还原时(例如手工删过 settings.json)才回落到"移除受管 config、恢复 DSH 出厂默认"。
- 只有压缩字段会碰那个文件(v1.6.1 起):
enabled / triggerPct / retainPct / auto
没变时,保存设置完全不触碰 standard 组装文件 —— 只改「气泡置顶」「对话页」的外观开关不会再把你
自己写在那一行里的压缩配置抹掉(旧版每次保存都无条件重写,这是破坏性缺陷)。
- 压缩触发点:占路由模型上下文窗口的百分比,默认 25%(deepseek-v4-flash 窗口 1,000,000
tokens ⇒ 约 250k 触发)。
- 保留原文尾部:逐字保留最近内容的窗口百分比(必须小于触发点),默认 5%(⇒ 约 50k)。
- 自动压缩:
auto 开关;关闭后不再自动压缩与溢出恢复,仅保留手动 /compact。
会话守则 · 长期规则
插件内的 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,
由本插件作为第三段常驻规则注入(详见下面「输出形状」一节)。别在这里再抄一份,否则同一套规则会被注入两遍。
要收回原文备份:session-gate.R4-backup-2026-09-16.md。
- 怎么知道自己的规则被砍了:
/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,上限 min(列宽×.55, var(--cc-user-bubble-max,41em)),右缘再留一条轨道 padding-right:var(--cc-tail-room,2.4em);
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)) 跟宿主字号走。字号变、页面缩放变,这些一起变 ——
不再有任何"某次量出来好看就钉死"的像素数。
- JS 量一次(
fitUserBubbles()):Range.getClientRects() 取逐行矩形 ⇒
① stack.style.width = 最宽行 + 左右内边距(框贴文字;列宽变窄靠 max-width:100% 自动夹回);
② 实测图标行宽高 ⇒ 写 --cc-tail-room(轨道 = 图标行宽 + .45×图标高)与 --cc-tail-y
(与最后一行同高)。变量必须写在 userRow 上:图标行是 row 的子节点,写到 userStack 上
继承不到 —— 这是复制键一度跑偏的直接原因。
- 实测(列宽 1180、字号 15px):10 字 174px、30 字 444px、长文 579px;复制键距气泡右缘
13.1px(em 轨道,随字号缩放)且与末行同高;上下留白 8/8.1 相等;420px 窄容器不越框。
- 两条踩过的坑(别改回去):
① 用
bubble.children.length > 0 判"含内嵌块就跳过",会把带 @路径 引用的提问(宿主渲染
成 <span>)整条漏掉 ⇒ 框宽退回"块宽 = 上限"的固定观感(就是"完全不动态"那次反馈)。现在
只跳过真的含 img/video/canvas 或某行矩形异常高(内嵌块)的气泡。
② applyAppearance() 里任何一步抛错(例如常量改名)会连带把后面的观察器全跳过 ⇒ 被钉元素
不出现,看起来就是"模糊度失效"。现在每段各自 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)。
- 可调点:
USER_BUBBLE_MAX_EM(上限,默认 41em;测试缝 internals.setBubbleMaxEm /
setBubbleMaxPx)、--cc-tail-room(轨道宽 = 复制键离框缘的距离,JS 自动量、也可手动覆盖)、
FIT_ICON_H(仅量不到图标时的兜底高度)。钉顶底衬宽度自 v1.4.2 起按这条提问的实测宽度
写入 --cc-pin-w(量不到才退回 的旧上限)。
底衬形态的三次选定:
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 才清空回收目录。
host 三条路由:GET /cc/storage、POST /cc/storage/clean、POST /cc/storage/purge。
安装
前提: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% 下的真实 preset / settings.json。
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,夹具化见下面那条)。
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%
真实 preset/settings ——换成仓库内最小夹具 + 「真实 home 存在才比对」。它 import 的
@deepseek-ai/dsh-system-prompt 走自己 devDependencies 里那份(createRequire 从仓库 package.json
解析,版本与本安装宿主一致),所以把 DSH_APP_MODULES 指到空目录仍 39/39。
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)。
未纳入 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 才需要去关。)
D. 「省缓存」改的是 preset,不是插件目录
它把参数写进 $DSH_HOME/profiles/**/standard/agent.yml 里 compaction-basic 那一行
(带 # managed by dsh-cache-control 标记)。换机器/换 profile 后,必须在新机器上再打开一次总开关
才会重新写进去;关闭总开关会把该文件还原成上次接管前的原文(v1.6.1 起;
备份在 $DSH_HOME/dsh-cache-control/settings.json 的 compactionBackup 里,不随仓库走,
所以换机器后新机器上关一次开关只回到它自己那份原文)。
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,管不到这个脚本,所以以后加开关记得三处都补。
⚠ 「省缓存」不在这个文件里:它改的是 $DSH_HOME 里 standard preset 的那一行,
新机器导入后在设置页把总开关关一次再打开即可重新写入。
F. 换机后自查(30 秒)
node tools/verify-audit.mjs 2>$null; node tools/verify-host-width.mjs # 期望全绿 / 无 FAIL
# 设置页应出现「会话策略」八个分区(标题无编号);对话页默认 80%(百分比,v1.5.0 起)
验证
回归与探针脚本都在本仓库 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 响应契约
「自动审查」(v1.11.0)没有独立套件,断言分挂在两处:payload 对齐(reviewSkillEnabled 必须
出现在主 PUT 载荷里 —— 这条正是 v1.6.0 hideResizer 那个"拨得动不落盘"bug 的守门人)与
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 出来就是空文件。
卸载 / 回退
- 关闭省缓存总开关(把 standard 组装文件还原成接管前的原文 —— v1.6.1 起是逐字节还原,
不是删掉那一行);没有备份时手动删除组装文件里带
# managed by dsh-cache-control 的 config 块。
关闭会话守则开关即可让规则段消失。
- 在 profile 移除依赖与 bundle 项、删除 junction;或
dsh plugin --profile web remove dsh-cache-control。
- 重启应用。插件停用/卸载后不残留任何行为改动(
gate.md override 与 settings.json 是数据,需自行删除)。
版本与变更记录
说明:本插件只写 $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 依赖换成仓库内最小夹具。这段留在这里是提醒:测试脚本与宿主导出脱节时,崩在中途的套件
会伪装成"只坏了前半截" —— 排除理由里写"既有失败"之前先数一下它到底跑到了第几行。