🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

上下文守卫 2JumpSinA/dsh-context-guard

DeepSeek 客户端v0.3.4
2JumpSinA9 天前有提交已提供中文文档扫描:含敏感能力兼容 / 相关生态

会话太长就提醒你收尾换会话的 DeepSeek Harness 插件。

31.4/100
综合分
2
GitHub 星标
1,048
npm 周下载
—/5
用户评分

数据截至 2026/10/26 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/2JumpSinA/dsh-context-guard.git

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
生态插件
可安装,未声明 dsh 能力
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功(非静态推断);本次验证针对插件 v0.7.7,当前版本为 v0.3.4
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 9 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包@2jumpsina/dsh-context-guard @ 0.3.4
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/15 09:45:38

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 16 处,展示前 16 条)
  • 写入本地文件
    lib/index.js:81
    appendFileSync(
  • 发起网络请求
    lib/client.js:988
    return fetch(url, {
  • 写入剪贴板
    lib/client.js:1282
    return navigator.clipboard.writeText(text).then(
  • 发起网络请求
    lib/client.js:1486
    fetch(url, {
  • 读取本地文件
    lib/handover.mjs:206
    await lstat(abs);
  • 读取本地文件
    lib/handover.mjs:216
    const st = await lstat(abs);
  • 读取本地文件
    lib/handover.mjs:597
    return todoFilledIn(await readFile(file, 'utf8'), sessionId);
  • 读取本地文件
    lib/handover.mjs:721
    existing = await readFile(file, 'utf8');
  • 读取本地文件
    lib/handover.mjs:731
    const dir = await lstat(dirname(file));
  • 读取本地文件
    lib/handover.mjs:737
    const st = await lstat(file);
  • 写入本地文件
    lib/handover.mjs:753
    await writeFile(tmp, text, { encoding: 'utf8', mode: 0o600 });
  • 写入本地文件
    lib/handover.mjs:755
    await rename(tmp, file);
  • 删除本地文件
    lib/handover.mjs:762
    await unlink(tmp);
  • 发起网络请求
    lib/client-source.js:177
    return fetch(url, {
  • 写入剪贴板
    lib/client-source.js:471
    return navigator.clipboard.writeText(text).then(
  • 发起网络请求
    lib/client-source.js:675
    fetch(url, {
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 会话过长提醒收尾
  • 自动起草交接文档
  • 写入机器事实交接块

DeepSeek Harness 插件,在会话变得太长之前提醒你收尾并开新会话:徽标 + 一次性横幅,数据来自官方 contextPressure 投影,占用率到 45% 就自动往你工作目录里写一份机器事实交接块。零上下文开销,中英双语。

README

由 DeepSeek 最新模型翻译生成
dsh-context-guard npm 包名:@2jumpsina/dsh-context-guard · English 会话太长就提醒你收尾换会话的 DeepSeek Harness 插件。 消费官方 dsh-token-meter 已经算好的 contextPressure 投影,在一轮结束之后按双档阈值提示:「先把交接写进文档,再开新会话」。它会顺着帮你把这句提示变成动作 —— 跨过阈值时自动往会话工作目录起草一份交接草稿。 上下文税 = 0:不注册任何 model-facing 工具、不注入 prompt 段落、不额外发请求。宿主算数,UI 说话。 🆕 当前版本已发布到 npm latest(0.7.x):45% 只静默起草交接、60% 才提醒一次;压缩点在角标 tooltip 与 banner 正文里给出(按 min(窗口×0.8, 窗口−预留−64K) 实时推算,标来源;读不到平台压缩配置时标「推算」);平台压缩那一刻把压缩前读数抢存进 HANDOVER.md 的压缩账。 v0.5.0 起删掉了微信外推与跨会话趋势(相关配置键已在 v0.7.2 从 schema 退场,旧残留值被安全忽略)。 详见 HANDOVER.md §46–§48。
展开完整文档(README 剩余 1 节)
dsh-context-guard

npm 包名:@2jumpsina/dsh-context-guard · English

会话太长就提醒你收尾换会话的 DeepSeek Harness 插件。

消费官方 dsh-token-meter 已经算好的 contextPressure 投影,在一轮结束之后按双档阈值提示:「先把交接写进文档,再开新会话」。它会顺着帮你把这句提示变成动作 —— 跨过阈值时自动往会话工作目录起草一份交接草稿。

上下文税 = 0:不注册任何 model-facing 工具、不注入 prompt 段落、不额外发请求。宿主算数,UI 说话。

🆕 当前版本已发布到 npm latest(0.7.x):45% 只静默起草交接、60% 才提醒一次;压缩点在角标 tooltip 与 banner 正文里给出(按 min(窗口×0.8, 窗口−预留−64K) 实时推算,标来源;读不到平台压缩配置时标「推算」);平台压缩那一刻把压缩前读数抢存进 HANDOVER.md 的压缩账。
v0.5.0 起删掉了微信外推与跨会话趋势(相关配置键已在 v0.7.2 从 schema 退场,旧残留值被安全忽略)。
详见 HANDOVER.md §46–§48。

⚠️ 别敲 npm i dsh-context-guard —— 那个不带 scope 的名字是第三方的包(作者 greenlv)。
本插件的包名是 scoped 的:@2jumpsina/dsh-context-guard,已发布到 npm(见安装)。

🙂 用起来了?回我一条吧 —— 我想知道还有哪些可以改善的地方。
一句话就够:它在你那儿做对了什么、哪里别扭。

会话角标(ctx 69%)与跨过 60% 时的那一次提醒 —— 两张都从真实界面按元素裁剪,会话标题与正文不在画面内 45% 起自动写进工作目录的交接草稿 —— 由插件自身的 composeHandoverBlock() 用演示数据渲染

⚠️ 它会写你的工作目录(装之前先看这一段)

- 默认开启(handoverOnWarn: true)。当某个会话的占用率跨过 warnRatio(默认 45%)时,插件会在该会话的工作目录下创建/改写 handoverPath(默认 HANDOVER.md),此后水位每涨 5 个点刷新一次。
- 写入的内容是机器事实:会话 id、标题、工作目录、占用率、轮数/调用数、账本花费、 本次会话写过的文件清单、最近几轮的用户诉求与回复摘要,以及一段留给 agent 的「待补」空槽。
⇒ 它可能包含敏感内容(文件路径、你的提问与回复片段),也可能被 git 提交。
- 建议:把 HANDOVER.md 加进你的 .gitignore;或把 handoverPath 指到一个不在仓库里的路径;或直接 handoverOnWarn: false。
- 边界:目标父目录不存在 ⇒ 拒绝写入;目标已存在但不是文本类文件(.json/.yml/…)⇒ 拒绝改写;目标是指向别处的符号链接 ⇒ 拒绝;写入使用原子替换(临时文件 + rename)并收紧权限(0o600)。
任何失败都只记录状态,绝不影响徽标 / banner / 推送。

为什么需要它

一句话:长会话不是技术故障,而是每一轮都在涨的账单。这个插件存在的意义,就是在你还能行动的那一刻 把这张账单摆到面前 —— 一轮已经结束、下一次请求还没发出的那几秒。

这个问题是可测量的

成本 ≈(平均 prompt 大小)×(调用次数)×(单价)。长会话会把前两个因子同时推上去,而单价本身也随占用率上升。
拿标定这台机器自己的账本(本地 session 投影缓存,84 个会话 / 14 天,自报 ¥252.29)对着官方价表核:

| 测量项 | 数值 |
| 自检(价表 × contextTimeline.cost,对账本总额) | ¥254.29 vs ¥252.29 ⇒ 偏差 0.8%;逐请求金额覆盖总额 85.4% |
| 单价随占用率 | 0–10% ¥0.011/次 → 70–80% ¥0.045/次(4.2×) |
| 历史花费落在哪里 | ≥40% 占用时的调用吃掉 37.4% 的花费;≥60% 吃掉 20.0% |
| 会话峰值 | 观测到的 75/75 个会话都不超过 ~80%;真正的压缩点由公式决定(本机 deepseek-flash ≈ 67.8%) |
| 打扰成本(线画在 40% / 50% / 60%) | 每天 0.78 / 0.57 / 0.50 次 |

再举一次真实会话 —— 就发生在开发这个插件的仓库里:584 次调用、2.74 亿 token、¥31.66,其中 98.6% 花在重读上下文,模型输出只占 0.17%。 这正是它要对付的失败形态:不崩溃、不报错,只是一张发票。

为什么它是这笔交易里便宜的那一半

上面那个公式有个值得写明的形状。一个会话的总额是

总额 ≈ Σ_轮 ( prompt_i × 单价(占用_i) )

而两个因子是同向动的:prompt_i 随每一轮单调增长,单价本身也随占用率上升(上面实测过: 0–10% 时 ¥0.011/次 → 70–80% 时 ¥0.045/次)。所以一个会话不是「跑得越久、贵得越线性」,而是 超线性地变贵 —— 这就是为什么长会话的最后三分之一,明明轮数一样,却可能比头三分之一贵得多。

这正是这个插件值得装的全部理由:它的边际成本严格为零。

- 不注册 model-facing 工具、不注入 prompt 段落、不额外发请求 ⇒ 它不往任何请求里加 token,账本上根本查不到它。
- 它唯一让你付出的是结尾那份交接 —— 而那不是付给插件的费用,是你本来就要做的事(而且通常做得更糟:凭记忆,在一个已经被放弃的会话之后)。
- 它只在轮边界出声。轮内的提示没有用 —— 那一轮的 prompt 已经发出去、已经付过钱;晚两轮的提示是一张你已无法回避的发票。

所以这笔交易不是「花钱省钱」,而是:在一张你已经在付的账单上,免费装一个读数 ——而且装在数字还来得及改变决定的那一刻。

为什么别的机制接不住它

- 平台会压缩,但不会跟你商量。 自动压缩点是 min(窗口 × 0.8, 窗口 − 输出预留 − 65,536) (本机 deepseek-flash ≈ 67.8%),到了就压、不问。压缩是会话的安全网,不是给你的通知 —— 等它触发时,你已经为整段爬升付过钱了。
- 模型自己当不了这个计量器。 projectedTokens 由 dsh-token-meter 从真实请求算出,模型看不到自己的 prompt 有多大;而提示只有在一轮结束之后才有意义(那一轮的 prompt 已经发出去了)。何况任何「让模型自己注意上下文」的做法都要注入 prompt 段落或注册工具 —— 那等于给一个上下文税问题再加一笔上下文税。
- 你手上没有读数。 占用率可以看见,但「该收尾了」不是一次读数能定的:它需要迟滞、冷却、边沿触发、跨会话计数,而且要在你点进一个已经满了的历史会话时就响。没人一边干活一边盯百分比。

它做了什么 —— 以及刻意不做什么

- 上下文税 = 0:不注册 model-facing 工具、不注入 prompt 段落、不发额外请求。防上下文膨胀的东西,自己不能吃上下文。
- 只消费官方计量器已经算好的数字:不重复计量、不估算、不猜。
- 每档只响一次,且只在一轮结束时:双档 + 迟滞 + 边沿触发 + 冷却。
- fail-closed:没有分母就完全不判定,徽标显示 ctx — 而不是假装 0%。
- 把一句话变成动作:跨过 warnRatio 就地起草一块机器事实草稿(会话、水位、账本花费、本会话写过的文件、最近几轮的诉求 / 回应预览,外加「待补」空槽)写进会话工作目录 —— 零 LLM、零额外请求。
- 本插件不联网(v0.5.0 起删除了微信外推);能力清单只剩 fs-read / fs-write。

这一节不声称什么

- 省下的钱是反事实上界,不是承诺。 拿本机账本重放:≥40% 占用时发生的那 2741 次调用,若都发生在 0–20%,可省 ¥59(23%);≥60% 的那批可省 ¥34(14%)。收尾不是免费的 —— 它恰好要付一次交接的成本。
这里只声称一件事:把数字摆在面前时,这个决定能做得更好。
- 阈值是标定值,不是最优解。 0.45 / 0.60 来自那 84 个会话;它们是默认值,且每一个都可配置。
- 样本是单机、单 provider、两周。 价格与压缩行为都会变。
- 它不判断你的任务值不值得做完。 它只说一句:从这里继续,每轮比重新开始贵好几倍 —— 交接草稿在这里。

它做什么

| 半边 | 做什么 |
| host(lib/index.js) | 订阅 ctx.sessionProjections.onChanged,把 contextPressure 喂给纯策略;在 turn/end 结算一轮;持有设置;session/created(新建或从存储恢复)时立刻判一次;compaction/end、request/header 参与判定;把判定注册成 contextGuard wire 投影给浏览器半边;注册 GET /api/context-guard/state(只读调试镜像);按设置把结论外推到微信;跨过阈值时起草交接草稿 |
| client(lib/client.js,手写单文件 bundle) | 会话头徽标(实时占用率,只显示数字、不出声)+ 跨档时一次性 banner(占比 + 下一步 + 指向交接文档);档位/轮次/冷却一律 useProjection("contextGuard") 只读宿主投影 —— 没有本地状态机、不读阈值、不发额外请求(见下面「架构」一节) |

行为要点:

- 判定时机 = 投影变化时(不是 turn/end):contextPressure 在每次请求后更新,插件随即判一次; turn/end 只做轮计数与冷却衰减,compaction/end 与 request/header 参与判定。
⚠️ 早期本节写的是「绝不 mid-turn」,与实现不符。2026-10-17 代码审查后定案保持实现、改文档,理由是风险不对称:改成只在 turn/end 判,一旦该事件没送到就完全不提示(漏报);投影即判最坏只是提示出现得早一点。详见 HANDOVER.md §33。
- 只响一次:双档 + 迟滞(回落到 warn − hysteresis 才重新武装)+ 边沿触发 + 冷却 N 轮。
- 跨档时给你两个选择:写好交接,我复制(宿主立刻写一次机器事实草稿,并把可复制的开场文本摆出来)与 知道了(纯关闭,没有附带动作)。
- fail-closed:没有分母(contextWindow 缺席)就不判定,徽标显式显示 ctx —,不假装 0%。

架构(v0.4.3):宿主判,浏览器只读(一份判定)

判定只有一处、显示只有一处,中间靠一条 wire 投影连着:

| 层 | 在哪 | 负责什么 |
| 判定 | lib/engine.mjs(只在宿主) | 状态机是纯折叠(init + apply(state, event)):档位 / 武装 / 轮次 / 冷却 / 迟滞只算一遍,compaction/ 折算进状态 |
| 投影 | lib/index.js(宿主) | 把判定注册成 wire 投影 contextGuard:{ level, armed, turn, cooldown, ratio },扁平 JSON,未变化时保持同一引用 |
| 显示 | lib/client-source.js(浏览器) | useProjection("contextGuard") 只读显示(徽标 + 一次性 banner);没有阈值、没有状态机、不发额外请求 |
| 调试镜像 | GET /api/context-guard/state | 只读镜像:宿主当前配置 / 最近判定 / 压缩账计数 / 交接统计。浏览器半边不再从它抄阈值 |

这样分工换来两件事:

- 一套判定。以前浏览器半边自带状态机,启动时再从状态路由抄一份阈值 ⇒ 两套冷却、两套迟滞, settleTurn 还挂在 banner 按钮上(点一下就能在客户端推进轮次)。现在判定只住在 lib/engine.mjs,连 bundle 里都没有它 —— 这是结构性的:test/client-sync.test.mjs 还会静态禁用 hostSync / setConfig / settleTurn 这些标识符。
- 读到什么就显示什么。排障面 window.__DSH_CONTEXT_GUARD__.store.dump()(全部会话)与
viewOf(sessionId)(单个会话)记录的是宿主投影说了什么,不是客户端算了什么;
投影缺席(旧宿主 / 没装 token-meter)时徽标仍显示占用率,但不出 banner ——宁可少提示,也不退回本地自算一份。

安装

⚠️ 别敲 npm i dsh-context-guard。 不带 scope 的 dsh-context-guard 归第三方所有(greenlv <lgr5945@gmail.com>;latest 0.2.1,现已 deprecated 并改名为 dsh-completion-guard)。
敲那条命令装到的是他们的插件,不是这个。本插件的包名是 scoped 的 @2jumpsina/dsh-context-guard,已发布到 npm。

装进 DSH(推荐)

dsh plugin 只是 pnpm 的薄封装:它在 profile 目录里做包管理,然后把这个新 bundle 登记进 profile。一条命令,其余全自动:

dsh plugin --profile web add @2jumpsina/dsh-context-guard

DSH 随后会替你做好这些(都在真机 profile 上验证过):把依赖写进该 profile 的 package.json ·把包名追加进 dsh.profile.bundles —— 这一步才是「它成了一层插件」 · 应用包内 cordis.patch.yml 挂上宿主半边,并按 dsh.client 声明注入浏览器半边。

⚠️ 装完要重启 dsh。 源码改动不自愈,新装上的层在启动时才生效。

⚠️ 「装包」≠「挂上」。 单独 npm i 只把文件放进 node_modules,不会改任何 profile。

从本仓库安装(想跑最新源码时)

dsh plugin --profile web add git+ssh://git@github.com/2JumpSinA/dsh-context-guard.git

前提:机器上有 git,且 GitHub SSH 密钥已配好。⛔ 别用 github: 简写(dsh plugin --profile web add github:2JumpSinA/dsh-context-guard)—— 它会被解析成 HTTPS clone,在某些网络下会失败,而报错完全不提「传输方式」这回事。要用就走上面的 git+ssh:// 或 npm 包名。

纯 npm(不用 DSH 的包管理)

只想把文件取下来、自己挂:

npm i @2jumpsina/dsh-context-guard

本地开发时(不发布、用 junction 挂进某个 profile)也常见:

把仓库挂进目标 profile 的 node_modules(路径按你的环境替换) New-Item -ItemType Junction -Path $env:DSH_HOME\profiles\<profile>\node_modules\@2jumpsina\dsh-context-guard -Target <仓库路径>

然后在那个 profile 的 cordis.patch.yml(用户 patch 层)末尾加一条 insert:

- insert:
- id: context-guard
name: '@2jumpsina/dsh-context-guard'

⚠️ 不要把这个包写进 profile 的 dsh.profile.bundles —— 会报 cannot resolve profile bundle。
⚠️ junction 挂载后,Node 从真实路径往上找 node_modules ⇒ 仓库里要有它自己的依赖(或让 DSH 从安装位置解析)。

配置项

设置页可改,全部即时生效;config 字段都是 volatile ⇒ 改完不用重启(但改源码要重启,见下)。

| 字段 | 默认 | 范围 | 含义 |
| enabled | true | bool | 总开关 |
| locale | auto | auto\|zh\|en | 文案语言;auto 由两半边各自推断(宿主看会话首条输入,浏览器看 navigator.language,均兜底中文)。⚠️ 这一项只作用于宿主侧(草稿块 / 宿主日志 / 推送),浏览器侧的强制项是加载前逃生阀,见「语言」一节 |
| warnRatio | 0.45 | 0.1–0.95 | 「该收尾了」的占用率(projectedTokens / contextWindow) |
| hardRatio | 0.60 | 0.1–1 | 「该换会话了」;必须 > warnRatio |
| hysteresisRatio | 0.05 | 0–0.2 | 迟滞:回落到 warn − 此值 以下才重新武装 |
| cooldownTurns ~ | 5 | 0–100 | v0.7.1 起 no-op:冷却闸门已删(跨档每武装周期本就只响一次) |
| onResume | true | bool | 进入一个已经很高的历史会话时也提示 |
| respectCompaction | true | bool | 只影响宿主日志:紧跟压缩那一轮不记判定日志;不改变徽标/banner 显示 |
| handoverOnWarn | true | bool | 跨过 warnRatio 时自动起草交接草稿 |
| handoverPath | HANDOVER.md | string | 相对会话工作目录,或绝对路径 |
| handoverRefreshPercent | 5 | 1–25 | 水位每再涨这么多点刷新一次草稿 |
| handoverTurns | 6 | 1–20 | 草稿里带几轮「诉求 / 回应」摘要 |
| handoverFiles | 12 | 1–50 | 草稿里带几个最近写过的文件 |

warnRatio 的 schema 下限是 0.1:配 0.05 之类的值会让整个插件挂不上(ValidationError: invalid config: $.warnRatio expected number >= 0.1,状态路由随之 404)。

语言 / Language

所有给人看的文案都是双语的(中文 + 英文):banner、草稿块、宿主日志,以及徽标 / banner 的 UI 文字。
默认 auto,两半边各自解析 —— 它们能看到的线索本来就不一样:

| 半边 | auto 看什么 | 兜底 |
| 浏览器(徽标 tooltip + banner) | 加载前逃生阀 window.__DSH_CONTEXT_GUARD__.config.locale > navigator.language(以 zh 开头 ⇒ 中文,其余 ⇒ 英文) | 中文 |
| 宿主(草稿块 + 日志 + 推送) | 该会话首条用户输入里有没有汉字(titleInput —— 走注册表的宿主状态 stateOf 读,它没有 wire、onChanged 收不到) | 中文 |

- 浏览器侧语言 = 加载前逃生阀 config.locale > navigator.language > zh。 逃生阀必须在 bundle 加载之前预置(addInitScript 之类):语言在模块求值时就固化了。它默认不存在 ⇒ auto 几乎总是按浏览器语言推断。
- 宿主侧的 locale 只影响宿主自己的面(草稿块、宿主日志的文案)。v0.4.3 起浏览器半边 不再从宿主读语言(那次 GET /state 已经拆掉),所以「写 locale: 'zh' 让两半边都说中文」这句话不再成立 —— 想强制浏览器侧,只有上面那个加载前逃生阀。
- 设置页的字段说明永远是「English / 中文」合并式一行:DSH 的 config schema 是加载期静态元数据,没有 per-locale 描述机制,设置页也无从知道你在看哪个会话。(将来 DSH 支持按 locale 描述,这些字符串就搬进文案表,这条注释随之删掉。)
- 逃生阀:没有会话可看时(启动日志、设置页、状态路由),宿主半边读环境变量 DSH_CONTEXT_GUARD_LOCALE=zh|en;其它取值一律忽略。
- 面向机器的面在任何语言下都是英文:状态路由的 JSON 键名、config 字段名、日志 code。
被翻译的只有给人看的句子。
- 下面那段草稿示例是中文会话产出的样子;英文会话结构完全一致,只是标签是英文。

宿主状态路由

GET /api/context-guard/state —— 只读调试镜像:给 curl、排障和验收脚本看宿主当前的判定设置与统计。
⚠️ v0.4.3 起浏览器半边不再从它取数据(档位/冷却走 contextGuard 投影,见「架构」一节),所以它坏掉不会影响徽标与 banner —— 但会让排障少一只眼。

{
"plugin": "context-guard",
"at": 1790000000000,
"config": { / 上面那张表;handoverPath 只回相对名/文件名,绝不回绝对路径 */ },
"recent": [ { "sessionId": "…", "level": "warn|hard", "ratioPercent": 52, "title": "…", "body": "…" } ],
"ledger": { "written": 1, "failed": 0, "unchanged": 0,
"last": { "at": 1790000000000, "status": "appended", "row": 1 } },
"handover": { "enabled": true, "path": "HANDOVER.md", "refreshPercent": 5,
"written": 1, "failed": 0, "skipped": 0,
"last": { "status": "appended|replaced|error|skipped", "path": "HANDOVER.md", "bytes": 1234, "error": null },
"history": [] }
}

自动交接草稿

跨过 warnRatio(或打开一个已经超过该阈值的会话)时,插件在会话工作目录里维护一块带标记的草稿:

🤖 自动交接草稿(dsh-context-guard · 时间 · 占用 52%)… 机器事实表格 · 改过的文件 · 最近几轮摘要 · 「待补」空槽 …

- 幂等:一个会话一块,刷新时整块替换,不重复追加;人工可以整块删除。
- 也能手动触发(v0.3.5):banner 上的「写好交接,我复制」会 POST /api/context-guard/draft,跳过「水位涨够才刷」的步进闸门立刻写一次,然后给出这段可复制的开场白: @HANDOVER.md 接着这份交接继续。请先把「待补」补完再继续任务。 —— 粘到新会话即可接着干。
(路由与状态路由一样是本机无认证的:响应里只回文件名,绝不回绝对路径。)
- 只写机器事实、不写结论 —— 插件不知道你干了什么。请让 agent 在「待补」里写结论/坑/下一步,或折进你自己的交接文档后把整块删掉。
- 素材从哪来(2026-09-30 真机核实,0.3.2 修正 —— 包名改成 @2jumpsina 作用域的第一个发布版本):机器事实取自宿主状态真源 ——注册表 sessionProjections.stateOf(session, key),而不是投影变更推送的 wire 视图。
原因:contextTimeline(由 dsh-context 这类插件注册)的 wire 视图在按需 detail 通道启用后是个 slim head:水位/规模/工具分布/花费都在,唯独没有 fileOps —— 重集合只存在于单元状态里。v0.3.0 只读 wire 视图 ⇒「改过的文件」永远是 0 个(恢复会话与会话运行中完全一样,不是两条路的差别);另外 turnOutline 的 wire 视图是数组而 v0.3.0 按 {turns} 读 ⇒ 「最近几轮」永远印「宿主没有提供」。两处均已修,并各有单测/自检守着。
- 依赖与降级:fileOps、工具分布依赖注册 contextTimeline 的插件(如 dsh-context);花费依赖 tokenCost(如 dsh-damage-pulse)。它们缺席时对应段落显示为空 —— 插件不编造 也不失败;宿主注册表若没有 stateOf(老版本),自动退回 wire 视图(即 v0.3.0 的行为)。

保真的边界(v0.3.6,读这一节比读功能列表有用)

| 东西 | 保真度 | 为什么 |
| 会话 / 水位 / 花费 / 改过的文件 / 时间 | 高(可核对) | 全部来自官方投影;插件不改写、不估算。数字对不上就是宿主投影的问题,不是叙述的问题 |
| 最近 N 轮摘要 | 中(截断) | 那是压缩后的「诉求 / 回应」,不是原文 ⇒ 只能当线索,不能当证据 |
| 待补(结论 / 坑 / 下一步) | 空 | 插件不判断你干了什么。这三条要由看过完整对话的人(你,或旧会话的 agent)来写 |

两个推论,很重要:

1. 只看到摘要的新会话 agent 会「推断」 —— 它没见过那几十轮里的失败尝试、被否掉的方案、口头约定。
所以请把它补的结论当草稿读,不对就改。
2. 旧会话的 agent 写结论比新会话准(它还在上下文里)。按钮负责零成本的机器事实;结论建议在旧会话里一句话让它写,写进块外的正文。

人工区不会被刷新冲掉(v0.3.6):待补 区有独立子标记 ;水位再涨触发刷新时只更新上面的机器事实,待补 里的内容原样保留。
(≤0.3.5 的老块没有这对子标记,靠 - [ ] 行锚点兼容一次。)

阈值与设计取舍(为什么是 0.45 / 0.60)

- 占用率 = projectedTokens / contextWindow(官方口径:下一次请求的 prompt 有多大,不含 output)。
一律用比率:换模型即换窗口。
- 成本随占用率上升:按官方定价把「每次调用成本」对占用率拟合,高档位每次调用约是空会话的数倍,越晚收尾越贵;越早换会话省下的越多,代价只有一份交接(把它写短)。
- 0.60 这个 hard 线是刻意早于平台自己的动作:平台的自动压缩点不是固定 80%,而是
min(窗口 × 0.8, 窗口 − 输出预留 − 65,536)。本机 deepseek-flash(1,000,000 窗口 / 256,000 输出预留)算出来是
67.8%(678,464 token)——与真机观察一致。把 hard 放在压缩点上会与它抢同一时刻,而 respectCompaction: true(v0.7.1 起只静音宿主日志)又会让宿主半边在被压缩后不出声 ⇒ 宿主判定与界面可能短暂不一致,所以 hard 仍然要早于压缩点。
- 说真话(v0.6.1):硬档正文会写上这台机器按同一公式推算的压缩点(例如「约 67.8%」);窗口或输出预留未知时不写数字,只说「会静默压缩」。注意口径:不是「读数永久消失」——本插件的压缩账会抢存一行(没记下才是真没了)。
- v0.6.12:普适性边界(换机器/换模型能自动跟着变,但有一处读不到)。 窗口来自 contextPressure.contextWindow、输出预留来自 request/header.config.maxTokens (缺则回落适配器 defaultMaxTokens)——都是运行期按会话/路由读的,所以换机器、换模型会自动变。
但 dsh-compaction-basic 的 thresholdRatio(0.8)/ headroomTokens(65,536)/ modelPolicies 是挂在每个 agent leaf 上的服务配置,第三方插件读不到(已核实:根 ctx 注入与 agent scoped ctx 都拿不到),只能用出厂默认值推算 —— 如果你在 profile 里覆盖了这三项,本插件的数字会偏。
所以文案固定标「推算」并写明来源,不声称精确。徽标 tooltip 同时给出环境(provider/model · 智能体预设)。
- 两档的分工(v0.6.3):warnRatio(0.45)= 静默起草交接草稿,不再弹 banner; hardRatio(0.60)= 唯一提醒(跨档时出现一次,直到你点「知道了 / ×」)。

压缩账(v0.5.0)

平台压缩之后,压缩前的读数会从平台投影里永久消失(平台自己吃掉了自己的证据;插件只能抢存一份)。
插件在 compaction/end 时往工作目录的 HANDOVER.md 追加一行账(独立标记块,不会被草稿刷新冲掉):

🗜️ 压缩账(自动追加,勿手改)

| # | 时间 | 会话 | 压缩前峰值 | 尺寸 | 累计花费 | 改过文件 | 事件 |
| 1 | 2026-10-19 15:42 | a4b6cffb | 79.8% | ≈160K/1000K | ¥1.84 | 5 | 7f19c0a1 |

三条不变量:按 compactionId 幂等(同一事件重复投递只落一行)、取压缩前峰值(不是压缩后的低值)、 失败不静默(写失败计入 /state.ledger.failed)。压缩后 banner 的降级态会多一行回执:「压缩前 79.8% · 账本第 1 行 · 现在 20%」。

与 handoverOnWarn 解耦:关掉自动起草是关提示,不是关记录。

安全与隐私

- 状态路由没有插件级认证:它注册在宿主的 webServer 上(与 DSH Web 同一访问控制)。
插件侧做的防护是:只回相对名/文件名(绝不回本机绝对路径)、Cache-Control: no-store、不发 CORS 头。不要把 DSH Web 暴露到 0.0.0.0。
- 写文件的副作用见文首警告;写入路径来自你自己的设置,不是远端输入。
- 浏览器半边的调试把手 window.__DSH_CONTEXT_GUARD__:可读状态、可调档(override()),但被 Object.defineProperty 固定为不可整体替换。它只在页面内可用,不提升任何权限(那段代码本来就跑在你的 DSH Web 页面里)。
- 插件不联网、不调用任何 LLM、不发送会话内容(除非你显式开微信外推)。

测试与本地验收

npm test # 单元测试 + "bundle 与源码逐字节同源"校验
npm run build # 重新生成 lib/client.js(改了 lib/client-source.js 或 lib/policy.mjs 后必须跑)

要求 Node ^22.19.0 || >=24.0.0。

排障(都踩过,写下来别再犯)

1. .volatile() 字段拿到的是引用,不是值:apply(ctx, config) 里读 config.warnRatio 永远是 undefined ⇒ 静默用默认值。必须逐字段 isVolatile(v) ? v.get() : v(见 lib/policy.mjs 的 plainConfig())。
设置页显示 0.33、插件却按 0.45 判断,就是这么来的。
2. 改了源码不会自愈:运行中的实例不重读 profile 的 cordis.patch.yml,也不重载已装载的模块 ⇒ 改配置或改源码后要重启那个 dsh 进程(浏览器半边相反:服务端按请求重读 bundle,刷新页面即新)。
3. patchReload: live 不是文件监视:它管的是那一次组合,不是热重载。
4. junction 安装时 Node 按真实路径解析依赖 ⇒ 仓库里得有它自己的 node_modules(或让 DSH 从安装位置解析)。
5. --profile 是顶层旗标:dsh --profile --port <n> 对;dsh web --profile 会报 select a profile only once。

卸载

如果是用 dsh plugin add 装的(正规路径):一条命令,登记与依赖会被自动对账清掉,然后重启那个 dsh 进程。

dsh plugin --profile web remove @2jumpsina/dsh-context-guard

如果是手工 junction 挂的(本地开发):删掉 profile 的 cordis.patch.yml 里那条 insert (以及设置页可能写在同文件里的 - id: context-guard 配置行),再删掉 profile 的 node_modules\@2jumpsina\dsh-context-guard,然后重启那个 dsh 进程。
(用 cmd /c rmdir 删 junction:只删链接、不要删到仓库本身。)

License

MIT
预览分享卡片长什么样 ↓2JumpSinA/dsh-context-guard
分享 · DeepSeek Harness Hub
2JumpSinA/dsh-context-guard

会话太长就提醒你收尾换会话的 DeepSeek Harness 插件。

这是你的仓库?认领条目

回到顶部