dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Zero-token contract check for DSH plugins — verifies output.render() / session-event vocabulary, warn-only. 无耗检查:DSH 插件契约体检,零 token 成本,只告警不拦截。
它检查「契约合规」,不检测恶意,也不拦截执行。
本工具回答一个问题:当前装着的插件,有没有在违反 DSH 的内核契约—— 写坏会话历史的那个雷,现在是不是还埋着。
render() 返回字符串(会让整条会话历史无法加载)、插件写入词汇表外的会话事件类型如果你要的是"防投毒",这个工具帮不了你;如果答案是"是"的那类事故——它真能挡住。
dsh plugin --profile <你的 profile> add dsh-contract-check装完你会看到:
dsh-contract-check scan 、dsh-contract-check package
它只回答"合规吗",不回答"安全吗" —— 见下一节。
DSH 社区已有成熟的插件审计工具,本工具不是首创。诚实的定位如下:
· omdsh-dev/dsh-plugin-check · 本工具 扫描对象 · 插件仓库目录(发布前) · 运行中的工具注册表(安装后) 检查内容 · 清单协议 / patch 格式 / 构建陷阱 / hub 收录 / 生态合规,33 项 · 只两条:render 契约、会话事件类型词汇表 覆盖范围 · 更广(打包、命名、patch、hub 状态) · 更窄(但更深) 压缩/无源码的插件 · 看不到(扫源码树) · 照样看得见(读的是注册表里的定义) GUI 状态徽章 · ❌ · ✅ token 成本 · ⚠️ 注册了模型工具 plugin_check(每请求付 schema 税) · 0
同组织还有:dsh-session-health(会话文件帧级健康)、dsh-security-audit(凭据/暴露面)。
因此:
本仓库的 package 子命令只是轻量版(只查"声明要用的文件会不会被漏发"这一类), 够用就好,不够请用他们那个。
盯让会话打不开的那两个契约、并把状态做成零 token 的 GUI 角标的。
两者互补而非竞争:他们管"发出去之前对不对",本工具管"装进来之后有没有在埋雷"。
本插件不注册模型工具、不注入提示词、不产生模型可见输出。
它唯一的对外输出是 ctx.logger.warn(...)——只进控制台/日志,不进模型上下文。 所以它对你每轮对话的 token 消耗零影响。
这条是硬约束,不是巧合:一个模型工具的定义(名字+描述+参数 schema)会进入每一次请求的 上下文,几十轮对话就是上万 token,不管你用没用它。任何"要让 agent 知道"的功能都要付这笔税, 因此本插件永远不加模型工具。
GUI 状态页/徽章不受此约束:它是浏览器侧代码,不进模型上下文,所以 token 成本仍然是零。
装进 profile 并重启后,会话头部会多出一个小按钮:一个颜色圆点 + 「体检」二字。 鼠标移上去展开完整状态条:
🟢 无耗 · 检查 21 个插件 · 违规 0 · 不可判定 1界面上就只有这一个按钮,没有 Tab、没有面板。理由很直接: 体检是后台该做的事,不该要求用户主动去翻页签。检查由插件自己跑 (加载时 + 工具注册表变化时),按钮只是"想立刻确认一下"的入口。
点击后的反馈(0.5 秒):圆点变成转圈、文字变「体检中…」、按钮禁用防连点。 即使体检只花几十毫秒,动画也会转完这半秒——否则就是"点一下闪一下",比不动还难看。
另外设置页会多出一栏「无耗检查」,里面是自愿打赏(装了 dsh-tip-jar 才显示)。 打赏放在设置页而不是日常视野里,是因为它跟体检本身没关系,不该天天占地方。
颜色 · 含义 🟢 绿 · 体检过,0 违规,且确实验证到了东西 —— 健康 🟡 黄 · 体检过,有违规 —— 有插件在写坏会话的风险 ⚪ 灰 · 尚未体检 / 无法枚举 / 全部判不准 —— 诚实显示"不知道",绝不假装健康
不可判定(unknown)不影响颜色:那是"判不准",不是"有问题"。 把它算成风险会让你天天看到黄色,然后就再也不看这个颜色了。
但有一个例外(实机教训):如果 21 个插件全部判不准,那不是"健康",是"什么都没验" —— 这时显示灰,状态条会写明"全部判不准,等于没有验证"。 绿色必须意味着"真验过了"。
手段 · 做法 · 局限 静态 · 读 render 源码,看 return 的是不是数组 · 对 `defineTool` 写的工具永远失效(见下) 实测 · 按 output.schema 造假数据,真调一次 render,看返回值 · 假数据不合形状时会抛异常 → 归 unknown
内核的 defineTool 会把作者的 render 包一层:
// 我们读到的是这个壳 —— 函数体只是一句"调用别人",静态永远看不透
render(args, value) { return userRender(args, value); }所以静态检查对绝大多数正经插件都只能报 unknown(实机实测:21/21 全 unknown = 等于没检查)。 实测是必需品,不是可选项,因此默认开启(probeRender: false 可关)。
实测的边界(重要):调用失败绝不等于违规 —— 假数据不合形状会让 render 抛异常, 那是检查器自己的问题,所以一律归 unknown,绝不误报成 violation。
实机效果:21 个插件 → 实测判定 20,剩 1 个如实说"判不准"。
实现方式(刻意从简):
ctx.webServer 注册两条同源 HTTP 路由:GET /contract-check/status —— 最近一次体检结果(JSON,含 diag 诊断)POST /contract-check/audit —— 立刻体检一次src/client.js → lib/client.js)只做两件事:fetch 上面两条路由 + 渲染409(枚举注册表是 CPU 活,不排队堆叠)ctx.inject([...], cb) 延迟注入:tools 与 webServer 都可能在 apply 之后才就绪(实机踩到过两次: 直接读 ctx.tools / ctx.webServer 会静默失败,界面只能是灰点)。 /status 的 diag 字段会告诉你注入是否触发、用了哪个访问器、view() 返回什么形态。
⚠️ 客户端产物需要构建:npm run build(用本机已有的 esbuild,不联网安装)。构建产物 lib/client.js 必须存在,否则 DSH 启动会报 "client bundles not found"。files白名单已含lib,避免打包时漏发(这正是本工具自己检查的那类错误)。
DSH 的写路径宽松、读路径严格,而失败模式是整条会话而不是"那一条记录":
契约 · 违反的后果 · 真实事故 output.render() 必须返回 ContentBlock[] · 加载器报 SessionPersistenceCorruptionError,整条历史打不开 · 2026-09:dsh-ssh-ops 的 sftp_*/tunnel_* 返回裸字符串,多个会话失联 会话事件类型必须在 KNOWN_SESSION_EVENT_TYPES 内 · 加载器拒绝解释整条日志("unknown to this harness and not marked ignorable") · 社区多次报告(dsh-agent-teams 等)
第二条有个容易忽略的硬事实(本工具核验过内核源码):
if (KNOWN_SESSION_EVENT_TYPES.has(event.type) || event.ignorable === true) continue;Session.append(type, data, ...opts) 的 opts 只接受 sourceEventSeqs / surfaceOp—— 插件没有设置 ignorable 的入口;而且整个内核里不存在**任何 ignorable: true 的赋值
结论:插件写一个自定义事件名,默认就会让使用者的会话打不开。
本文档早前写过"且作者无法自救"。这句话不成立,证据来自生态里的真实代码 (@flowingspring/dsh-voco):
function registerVoiceSessionEventTypes(eventTypes) {
const writable = eventTypes
for (const type of VOICE_SESSION_EVENT_TYPES) writable.add(type)
}
registerVoiceSessionEventTypes(KNOWN_SESSION_EVENT_TYPES) // 自己 import 的那份
// 还要额外改「宿主」那一份,否则改的是另一个模块实例、不起作用:
const hostSession = createRequire(resolve(process.argv[1]))('@deepseek-ai/dsh-session')
hostSession.KNOWN_SESSION_EVENT_TYPES && registerVoiceSessionEventTypes(hostSession.KNOWN_SESSION_EVENT_TYPES)`KNOWN_SESSION_EVENT_TYPES` 是一个导出的可变 `Set`。 插件在运行时往里 .add(), 而加载器 KNOWN_SESSION_EVENT_TYPES.has(event.type) 用的是同一个对象引用 —— 注册真的生效。
所以准确的表述是:
· ignorable · 插件确实设不了(opts 只认那两个字段) 词汇表 · 插件可以用改 Set 的方式把自己的类型注册进去 —— 脆弱(依赖拿到宿主那份模块实例)、但可行 因此 · 写自定义事件类型不是绝对禁忌,前提是必须同时完成注册;只 append 不注册,才会让使用者的会话打不开
本工具运行时只能看到"这个类型不在内核初始词汇表内",看不出该插件有没有注册过它 —— 所以这类告警的措辞是"看起来不在词汇表内,请确认你是否已注册",而不是断言违规。
把包加进 profile 依赖与 bundle 列表:
// profiles/<profile>/package.json
{
"dependencies": { "dsh-contract-check": "^0.1.0" },
"dsh": { "profile": { "bundles": ["...", "dsh-contract-check"] } }
}本地开发可以写成 "dsh-contract-check": "link:D:/tool/claude_code/dsh-contract-check"——改代码即时生效,但改完客户端要重跑 `npm run build` 再重启 DSH。
它会做三件事:
ctx.tools.view() 里可见的工具定义,逐个静态检查 output.rendersession/event,发现表外事件类型立刻告警插件注册顺序不可控,所以本插件也会监听 tools/change:先加载时注册表还是空的就保持安静,等注册表变化后再体检(同一问题只告警一次,不刷屏)。
dsh-contract-check scan <文件或目录> # 递归扫源码里的 render 定义
dsh-contract-check package <包目录> # 检查 npm 打包会不会漏发声明要用的文件
dsh-contract-check vocab # 打印内嵌词汇表版本与条目数`package` 检查的是"整树起不来"那类事故:package.json 声明要用的文件 (dsh.bundle.patch / exports 目标 / types)有没有被 files 白名单漏在包外。
⚠️ 这是轻量版:只覆盖"声明要用的文件会不会被漏发"这一小类。
完整的插件仓库体检(清单协议 / patch 格式 / 构建陷阱 / 命名 / hub 收录,共 33 项)
请用社区成熟的 omdsh-dev/dsh-plugin-check。本工具的重点始终是运行中的注册表与会让会话打不开的契约,不是发布前的仓库检查。