dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Jvavscpppt(doge)我不会,我只会C++
本项目无限期搁置,该项目功能已被官方harness正式版取代
DSH(DeepSeek Harness)峰谷调度插件:高峰前 N 分钟自动生成"仅供 AI 恢复用"的简报并停掉自动续跑,高峰期间硬拦模型请求(被拦的对话在谷期原地自动重发,带一键放行/取消),闲时自动注入简报继续任务。
当前状态:P0–P6 + 设置界面。
npm run accept的 9 条验收标准 9/9 通过; 另有两组"真实宿主"测试:用 DSH 运行时里真实的@deepseek-ai/cordis+ 真实LlmRuntime+ 真实dsh-settings-file装载本插件。 门控默认enabled: false,所以挂载它本身不会拦截任何请求、不排任何定时器。
DeepSeek 官方 API 在高峰时段按 2 倍计价(北京时间工作日 09:00–12:00、14:00–18:00;周末全天闲时)。
现有插件要么只做"提示"(各种徽标/倒计时),要么只做"暂停"(时间窗冻结),
没有一个做到"先让 AI 把活交接清楚,再停"。本插件补的就是这一步。
T-leadMinutes(默认 10 分钟):等到会话空闲(agent.whenIdle()),跑一次
agent.runMaintenance() 生成简报,然后只停"自动续跑"——你手动操作照常。T-0(高峰起点):llm/stream waterfall 拦截一切模型请求(硬拦),
可用 /peak-allow [分钟] 临时放行。agent.inject(简报) 给上下文 → agent.followup() 起一轮
→ 删除简报文件(用完即弃)。isPeakDay(d) = isStatutoryWorkday(d) && weekday(d) ∈ {周一…周五}
| 日型 | 判定 |
|---|---|
| 法定节假日 | 全天闲时 |
| 调休上班日(周末上班) | 全天闲时 |
| 普通工作日 | 按 peakWindows |
| 普通周末 | 全天闲时 |
| 未知年份 | 降级为"周一至周五按峰谷",绝不崩 |
拦截点是 llm/stream —— 一个 waterfall:放行就是 return next(),拦截就是返回一个
只含终止 chunk 的流。三条经过实测/查证的设计决定:
error 而不是 aborted。aborted 的语义是"被取消",会让界面看起来
像是用户自己中断的;这里是策略拒绝,必须说清原因。PEAK_BRIEF_BLOCKED 是刻意自定义的。dsh-llm-retry 只重试
EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT;如果复用其中任何一个,
一个必然再次被拦的请求会被无限重试。blockAuxiliary 默认 true(连会话标题、自动压缩这类辅助请求一起拦)。
想留一条缝就把它设为 false;普通对话请求在任何情况下都拦。逃生窗口的语义是"放过这一段":正在高峰时,窗口不会超过本段高峰的结束时刻—— 在 14:10 申请 10 小时放行,实际只到 18:00。
被拦时会告诉你。 门控会在高峰期间拦下每一次请求,逐次弹提示会把界面刷屏,
所以按「本地日期 + 高峰窗口起点」去重:一段高峰只弹一次(红色 peak 级横幅,
带 /peak-allow 用法)。离开这一段后 key 改变,下一段会重新提醒。
面向模型而非人:紧凑、结构化、不追求可读性。三条设计约束:
@deepseek-ai/* 裸包名——
profile 的 .dsh-module-fallback/node_modules 里只有客户端种子模块
(dsh-client-ui-primitives / dsh-client-ui-slots),没有 host 包。
所以这里既不 import BlockAssembler,也不 import createUserMessage:
文本自己累积(lib/brief.js 的 createStreamCollector),消息字面量自己拼。fallbackBrief()(用 goal 目标或最后一条用户消息),
并标记 degraded: true + 弹一条 warn 横幅。"没有简报就不许恢复"会让
一次解析失败变成工作丢失。in_progress|done|blocked,resumable 必须是布尔,
数组项必须是字符串。形状不对整体判失败,不猜不补——半对的简报会把恢复的 AI
带向错误方向。status: "done" 时强制 resumable: false。purpose 字段故意不传:它是封闭联合('compaction' | 'session-title'),
乱填会踩到适配器的 purpose 分支;省略是官方文档里"普通请求"的默认行为。
代价是思考模式可能开启(多花一点推理 token),换来的是语义诚实。
生成的简报默认落在 ~/.dsh/peak-brief/<sessionId>.json —— 一个独立文件夹,
不污染任何项目仓库。config.brief.dir 可改。
删除边界:P4 恢复成功后删掉的是这个文件。注入到会话里的那条
user/message受model-visible ⟺ logged约束,无法从会话记录抹除, 只能之后被常规压缩带走。
两处关键机制选择,都有依据:
goals.disarm() 而不是 pause()。dsh-goal 的类型文档写明
disarm「只移除进程内的续跑授权,不改持久相位与 revision」,而 resume() 明确支持
「重新武装一个 active 但 disarmed 的目标」。用 pause 会把语义记成"被用户暂停了",
高峰结束后再 resume 就变成两件事。followup(),不押在 inject() 上。inject() 的文档明说可能
"错过 pre-step 已认领的批次"——把恢复载荷全押在它上面,会在竞态下静默丢失,
那等于活白交接了。所以:inject 只带一行 ≤120 字的 notice(省 token、界面有摘要),
followup 携带完整简报,保证送达。周期决策是纯函数(decideCycle),定时器每次醒来只做一件事:
| 相位 | 动作 |
|---|---|
lead | 生成简报 + disarm 目标(同一高峰段内不重复——接受标准 7) |
peak | 不做任何事(门控本身按相位生效) |
off | 若存在未恢复的周期:inject + followup + 重新武装目标 + 删文件 |
故障是隔离的。 session.deriveMessages() 这类真实契约是离线验证不到的,
所以:① 简报生成异常不会跳过"停自动续跑"(高峰定价才是要躲的东西,简报只是让恢复更顺);
② 异常会被如实记录进 tick 结果并推一条 peak 级告警,而不是吞掉;
③ 单个会话炸掉不会带走其它会话的调度;④ 没有简报时闲时仍会交回一条明确写着
"缺少简报"的恢复消息,而不是静默什么都不做。
约定:简报文件存在 ⟺ 有一次尚未恢复的暂停(恢复成功就删文件,所以文件的存在 本身就是持久标记)。
没有这一步会有一个静默的洞:进程重启后内存里的 cycle 消失,decideCycle 认为
"没有待办",那份简报就永远不会被交回——任务静默地卡在暂停里。
agent/created 时按上述约定对账,并带两道保护:超过 maxPendingAgeMs
(默认 12 小时)的旧简报不认领;恢复的目标只在"确实是我们 disarm 过、且它原本 armed"
时才重新武装。
前面那套是"高峰前把活交接清楚";这一节补的是另一半:你真的在高峰里发了一条消息, 怎么办。
默认行为(resumeBlocked: true):请求不以错误结束,而是停在那里等,进入谷期
(或你点「立即放行」)后重发同一个请求——对话原地接上。
dsh-agent-loop 消费到终止 finish 后会走一条被 await 的 waterfall:
// node_modules/@deepseek-ai/dsh-agent-loop/lib/index.js:1081-1098
if (finish.kind === "error" || finish.kind === "aborted") {
const action = await this.dispatch.waterfall("agent/request-error", { …, failure: finish.failure, signal }, () => Promise.resolve(undefined));
signal.throwIfAborted();
if (action?.kind !== "retry") throw new LlmError(…);
continue; // ← 重发同一个请求
}
监听器返回 { kind: 'retry' } 就重发,而此时 loop 的 firstAttempt 已经是 false,
不会重复写用户消息。所以本插件只是"把 promise 挂到谷期再返回 retry":
第一方插件 dsh-llm-retry 用同一条通道做退避重试(也是"合并 AbortSignal → 持久记录 →
可取消等待 → 返回 retry")。它默认 normal 模式、PEAK_BRIEF_BLOCKED 不在它的
retryableCodes 里,于是它 return next() 把决定权交给下游(也就是我们),不打架。
| 触发 | 结果 | 落盘兜底记录 |
|---|---|---|
| 进入谷期 / 闸门不再拦 | 返回 { kind: 'retry' } → 原地重发 | 删除(已经自己续上了) |
| 用户点「立即放行」 | 同上(立刻重发) | 删除 |
| 用户点「取消推迟」 | 以 PEAK_BRIEF_BLOCKED 结束 | 删除(用户放弃了,重启后不该再续) |
| 用户按"停止"(turn signal abort) | 立刻结束 | 删除 |
| 插件卸载 / 进程没了 | 立刻结算等待 | 保留 → 重启对账在谷期用既有链路接回 |
| 等过"本段高峰结束 + 60 秒"仍在拦 | 结束(防御性,正常到不了) | 保留 |
gate.decide():它会累加 blockedCount。每 5 秒轮询一次的话,
一次真实拦截会被记成几百次。所以另有一个纯函数 gate.wouldBlock(),
并由 test/gate.test.mjs 的矩阵断言守着它与 decide().block 永远一致。还有一条避免续跑两次:park 判为 allowed 时会把该会话的周期记录清掉。否则
(尤其是 agent 被重建过、重启对账已经按落盘文件认领了一份简报的情况)同一个会话会
既原地重发、又被注入一次简报 + followup。
session-title / compaction)继续"拦下即失败"——
把起标题挂 3 小时没有意义;status.queue 暴露排队中的会话与最近的结算记录,用于取证。由 ctx.commands.register 注册(不放进 inject:inject 里缺一个服务会让整个插件
拒绝加载,而主功能都不依赖命令;服务不可用时状态接口会如实报告
commands.registered: false,而不是假装成功)。
| 命令 | 作用 |
|---|---|
/peak-allow [分钟] | 逃生:临时放行(默认 30 分钟,且不超过本段高峰结束) |
/peak-status | 相位、门控、拦截次数、未恢复的暂停数 |
/peak-brief | 立刻为当前会话生成一份恢复简报 |
/peak-resume | 立刻按恢复简报继续(跳过等待闲时) |
在库里实现、并且在 Host 里接好了(配置项 holidayRefresh):
holidayRefresh:
enabled: true
url: 'https://cdn.jsdelivr.net/npm/chinese-days/dist/years/{year}.json'
years: [2027, 2028] # 省略则取"当前年 + 下一年"
onStart: true # 启动时自动跑一次
三条纪律:永不抛异常(失败只是那一年继续用内置表)、永不覆盖已有数据
(空表/脏表在 normalizeHolidayTable 就被拒,不会静默遮蔽内置表)、
不阻塞启动(异步跑,判定即时可用)。
并入的数据立即生效,不需要重建日历:createCalendar 每次判定时才按引用读
运行期表。手动触发:POST /api/peak-brief.refresh-holidays。
| 已验证 | 方式 |
|---|---|
插件在真实 cordis 下装载、inject 满足、apply 真的执行 | test/real-host.test.mjs(真 Context + 真 LlmRuntime) |
拦截走的是真实 llm/stream waterfall;放行时请求真的抵达适配器;拦截时适配器一次都不被调用 | 同上(桩适配器计数) |
我们构造的终止 chunk 能被官方 BlockAssembler 正确消费(不是自说自话的形状) | 同上 |
| 卸载后路由与 waterfall 监听一并拆除 | 同上(fork.dispose()) |
Client 半身:注册工厂、挂进 shell.overlay、渲染出文案与关闭按钮、点关闭 POST dismiss 并本地隐藏、React 缺失时降级 | test/client.test.mjs(vm + React/DOM shim 真实执行 bundle 并展开组件树) |
设置页:注册 settings.section、渲染全部字段、保存写出正确的路径操作与 revision、非法输入不写盘、恢复默认走 unset、Host 拒绝时展示原因 | 同上 |
设置:手写 schema 被官方 FileSettingsProvider 接受(注册 / describe() / toJSON()) | test/real-host.test.mjs(真实 dsh-settings-file) |
设置:真实 update() 即时生效、replace({}) 回退、非法写入被真实 provider 拒绝且不污染生效配置 | 同上 |
P6:真实 cordis 的 agent/request-error waterfall 上,被拦请求 park 住 → 谷期返回 { kind: 'retry' } → 重发的请求真的抵达适配器 | test/real-host.test.mjs(真 Context + 真 LlmRuntime) |
| P6:五种结算(谷期/放行/取消/abort/卸载)、辅助请求不 park、关掉开关回到 P2、重启兜底被对账认领 | test/host.integration.test.mjs + npm run accept 标准 9 |
设置页在真实桌面版里出现,且真的写进了 settings.yaml 并即时改变相位推演 | 在运行实例上实测(见「客户端半个必须声明 inject」「写入链路实测结论」) |
| 9 条验收标准 | npm run accept(真实插件代码 + 时钟覆盖把一天压进几毫秒) |
| 仍未验证 | 为什么 |
|---|---|
真实 agent/created 派发、session.deriveMessages()、session.requestHeader()?.config | 需要真实的 agent/session/goal 组合,离线搭不出来;只能读类型定义推断(/api/peak-brief.brief 那条路已经在真实实例上跑过一次真实模型调用) |
ctx.goals.disarm() / resume() 的权限策略 | 同上 |
| 真的拦下一次活的会话请求 | 会把这个会话自己锁死(被拦的请求没有模型回合可用,工具也跑不了),只有重启能解;故只在 test/real-host.test.mjs 的真实 waterfall 上验证过 |
| Host 半个的最新代码在你的运行实例里生效 | 桌面版只在启动时读 Host 代码;client.js 改完 Ctrl+R 即可,index.js 改完要重开 actdsh.exe |
| Client toast 在真实浏览器里的观感(CSS/portal/层级) | shim 验证的是逻辑与结构,不是像素 |
残余风险是有界的:插件装载、设置页注册与写入、核心拦截机制都已在真实运行时上跑通, 剩下的是具体服务契约(goal/agent/session)与"真拦一次活会话"这种需要你本人点头的操作。
「设置 → 峰谷调度」是一整页配置界面,可改:启用开关、提前量、时区、高峰时段、 高峰拦截模式、是否连辅助请求一起拦、被拦对话是否在谷期自动续跑、提示方式、按日手动覆盖。 另有"保存 / 恢复默认 / 关闭"与一条实时运行状态(当前相位、下次切换、已拦截次数)。
数据流(Host 与 Client 各一半,都不自己存配置):
Host ctx.settings.register('peak-brief', schema, { base: patch配置, applies: 'live' })
└─ scope.watch(next => applyConfig(next)) ← 写入后**即时生效**,无需重启
Client ctx.settingsScope.bind({ namespace: 'peak-brief' })
└─ slots.register({ name: 'settings.section', id, order, label }, PeakBriefSettings)
三个要点:
base(patch 层配置)→ 用户设置层。
所以设置页里"恢复默认"用的是 unset 全部字段(清空用户层),
结果回退到你在 patch 里写的值,而不是硬回内置默认。schema(merged),而我们的 schema 在遇到非法值(时区名、时段重叠、越界提前量……)
时抛错,那次写入就被拒绝,生效配置保持不变。ctx.inject(['settings'], …) 而不是把 settings 写进 inject:
cordis 的硬 inject 缺一个服务会让整个插件拒绝加载,而门控/简报/恢复都不依赖它
(这一点是实测出来的:直接访问 ctx.settings 会抛
cannot get property "settings" without inject)。inject = ['slots'](踩过的坑,附证据)设置页曾经完全不出现,而所有单元测试全绿。根因不在注册代码,而在启动顺序:
渲染器 apply: new SlotRegistry(ctx).install(...) ← `slots` 服务在这里才被提供
我们的 apply: 在它之前跑了 → ctx.get('slots') === undefined
→ 两处槽位注册整段被跳过(悄悄退化成 react-dom 自建容器)
ctx.get(name) 是可选查找(不等服务就绪)。插件 inject 为空时,apply 会抢在渲染器
之前跑;这是竞态,所以表现时而正常、时而消失。官方插件(settings-general 等)
全都声明了 inject = ['slots', 'locale', ...],就是把顺序交给 cordis 而不是交给运气。
修法:exports.inject = ['slots']。settingsScope 故意不声明——它可能永不可用,
硬声明会让 apply 永不执行,所以那条路继续走可选的 ctx.inject(['settingsScope'], …) 并自报。
/peak-allow)拦截点在 llm/stream waterfall 里:被拦的那一刻就没有模型回合了,所以任何"要模型去执行"
的逃生手段都可能不可用——/peak-allow 是 Host 的 commands 服务注册的斜杠命令,
commands 服务本身也可能不可用(实测运行实例里就报过 commands-service-unavailable)。
因此逃生路径必须只有 浏览器 → HTTP 这一条:拦截中的横幅(notice.kind === 'peak'
且 status.gate.blocking === true)直接带一个「放行 30 分钟」按钮,POST
/api/peak-brief.allow。没在拦的时候不显示这个按钮(免得把"高峰前简报"误当成拦截)。
实测(运行实例):POST /api/peak-brief.allow {minutes:30} → allow.active=true,
POST /api/peak-brief.allow-off → active=false。另有兜底:设置页里关掉「启用」并保存
同样能立刻停掉拦截,这条路也不需要模型回合。
客户端把关键结论 POST 回 /api/peak-brief.hello,Host 存进 state.notice
(GET /api/peak-brief.state 可读,客户端浮层也会显示)。当前会上报:
| 文本 | 含义 |
|---|---|
client apply 已执行(inject=[...] slots=可用/不可用) | bundle 真的在浏览器里跑了;同时暴露服务竞态 |
settings-section: 已提交注册(…) | 已调用 slots.inject('settings.section', …)(不等于注册成功) |
settings-section 终态:where=… spec=已声明/未声明 entries=N ids=… registered=是/否 | 确定信号:读槽位台账——我们有没有真的进 settings.section |
settings-section 渲染报错:… | 注册成功但组件渲染时抛错(走 slots.onEntryError) |
slots.inject 的延迟语义很关键:槽位被声明时回调才跑,而且它抛错不会被外面
try/catch 到(回调在声明者的 register() 里执行)。所以回调内部自带 try/catch,
另有一个 4 秒看门狗:槽位已声明却没进去就直连重试一次,仍失败则把台账写回后端。
实测确认(桌面版,运行实例):spec=已声明 entries=1 ids=dsh-peak-brief registered=是。
actdsh.exe 是 Electron 套壳,主进程 spawn dsh web。因此:
| 改了什么 | 生效方式 |
|---|---|
lib/client.js | 只需 Ctrl+R(Host 每次按内容重新提供 /plugins bundle,实测抓包核对过) |
lib/index.js 等 Host 半个 | 关窗(会连带 taskkill /T 结束整棵 dsh 进程树)后重开 actdsh.exe |
| 排障 | 窗口内 Alt 唤出菜单栏 → View → Toggle Developer Tools;Ctrl+R 刷新 |
设置页保存 → settings.yaml 落盘 → 生效配置即时改变 → 参与相位推演:
改「提前量」10 → 20 并保存
settings.yaml: leadMinutes: 20
GET /api/peak-brief.state: config.leadMinutes=20(无重启)
status.nextSwitch: 2026-09-21T00:50:00Z → 2026-09-21T00:40:00Z
最后一行是重点:提前量不是"存了个数",它真的把下次进入 lead 的时刻往前挪了 10 分钟。
外部挂载的插件解析不到裸包名(实测 @deepseek-ai/schemastery、
@deepseek-ai/cordis、schemastery 全部 ERR_MODULE_NOT_FOUND),所以不能用
schemastery 的 Schema。而 dsh-settings 只以两种方式碰 schema:
resolve(): const value = schema(mergeLayers(base, section)) // 可调用
describe(): registration.schema.toJSON() // 需要一个 toJSON
于是 lib/settings.js 给出一个可调用的函数 + toJSON(),两个调用点都满足。
这一点已用真实的 FileSettingsProvider 验证过:注册、describe()、写入、拒绝非法写入
四条路径都跑通了。
dsh-peak-brief/
package.json dsh.client.platform = web;main = lib/index.js
cordis.patch.yml 挂载行参考(真正生效的在 profile 的 patch 层)
data/2025.json 2026.json 节假日原始数据(来源可追溯)
scripts/build-holidays.mjs 由 data/ 生成 lib/holidays.js
scripts/acceptance.mjs 9 条验收标准的离线推演报告
test/real-host.test.mjs 真实 cordis + 真实 LlmRuntime + 真实 dsh-settings-file
test/client.test.mjs Client 半身:vm + React shim 真实执行并展开组件树
lib/holidays.js 内置兜底表(自动生成,勿手改)
lib/tz.js 时区/日期工具(零依赖,只用 Intl)
lib/calendar.js 峰谷日历:日型判定 + 覆盖 + 联网刷新 + 降级
lib/windows.js 相位状态机:off / lead / peak + 下次切换
lib/gate.js 门控决策 + 逃生窗口(纯逻辑,不自建请求)
lib/brief.js 简报:转录压缩、请求构造、严格校验、降级兜底
lib/brief-store.js 简报落盘(用完即弃的物理载体)
lib/resume.js 周期决策、目标授权、恢复消息构造
lib/settings.js 配置契约:默认值、规范化、校验、可调用 schema
lib/index.js Host 半身(ESM,无构建)
lib/client.js Client 半身(经典脚本 bundle,免构建)
test/ calendar / windows / host 集成 / smoke
npm run build:holidaysoverrides: { '2027-01-01': 'off', '2027-02-06': 'peak' }
(off = 当天全天闲时;peak = 当天按峰谷时段)| 路由 | 方法 | 说明 |
|---|---|---|
/api/peak-brief.state | GET | 插件状态 + 完整相位判定 + 门控状态;?at=<RFC3339> 可查询任意瞬间 |
/api/peak-brief.allow | POST | 逃生:{ minutes?, note? } 本次放行(缺省 30 分钟,且不超过本段高峰结束);顺手把等谷期的 park 结算成"立刻重发" |
/api/peak-brief.allow-off | POST | 立刻关闭逃生窗口,恢复拦截 |
/api/peak-brief.cancel-queue | POST | 取消推迟:{ sessionId? } 让等谷期的请求按拦截结束(省略则取消全部) |
/api/peak-brief.brief | POST | 生成简报:{ sessionId? }(只有一个活会话时可省略);落盘并返回简报 |
/api/peak-brief.debug-tick | POST | 推进一次周期检查:{ at? } 可先把时钟覆盖到某个瞬间,用于不等真时间就能推演 |
/api/peak-brief.debug-clock | POST | 装 / 卸时钟覆盖:{ at } 装入,{ clear: true } 卸下(默认不覆盖) |
/api/peak-brief.dismiss | POST | 关闭指定 seq 的通知(过期 seq 不会误关新通知) |
/api/peak-brief.hello | POST | 自检:推一条通知,验证 Host→Client 通路 |
# 查询任意瞬间的判定,不必等到明早 8:50
curl 'http://127.0.0.1:3080/api/peak-brief.state?at=2026-09-17T08:50:00%2B08:00'
注意:这些路由经
ctx.webServer.register注册,不经过 Web 应用的鉴权层 (与dsh-chat-forward、dsh-save-money同)。仅供本机使用。 卸载插件后这些路径会回落到应用的 401 鉴权响应。
npm run verify:package # 打包体检:pack → 解包 → 在解出来的副本里跑测试与验收
为什么需要它:files 漏一个目录,本地测试照样全绿 —— 因为本地测试读的是工作区,
而别人装的是 tarball。这个脚本把 npm 真正会发出的东西打出来、解到临时目录,
再在那个副本里跑完整测试与验收(不读工作区)。7 项检查:
| # | 检查 | 为什么重要 |
|---|---|---|
| 1 | npm pack 成功 | 清单本身合法 |
| 2 | tarball 含全部必需文件(lib/、scripts/、data/、cordis.patch.yml、LICENSE) | scripts/ 与 data/ 曾经漏掉:装完 npm run accept / build:holidays 直接不可用 |
| 3 | 解包成功 | 归档真的能装 |
| 4 | lib/ 只 import 相对路径与 node: 内置模块 | 零依赖是硬约束(外挂插件解析不了裸模块说明符) |
| 5 | 副本里 node --test 全通过 | 177/177,测的是发出去的那份 |
| 6 | 副本里 npm run accept 通过 | 9/9 验收 |
| 7 | scripts/build-holidays.mjs 可重跑且产出字节不变 | 抓到过真实漂移:头部原本写 生成时间:<now>,每次重跑都产生一条假 diff,"重跑再 diff"这个漂移检查因此完全失效;现已改为确定性的数据指纹 |
当前 tarball:32 个文件 / 约 111 KB(含 scripts/、data/、test/)。
private: true是故意的:它挡掉误发 npm(本插件目前只走 git 分发)。 若将来要发布到 npm / 插件市场,删掉这一行即可,其余字段(repository/homepage/bugs/keywords/exports["./client"])都已就位。
git clone https://github.com/rinttt233/dsh-peak-brief.git
# 然后把 cordis.patch.yml 的 name 指向 clone 出来的 lib/index.js(见下一节)
$DSH_HOME/profiles/web/cordis.patch.yml:
- insert:
- id: peak-brief
name: 'D:/deepseekharness/tmp/dsh-peak-brief/lib/index.js'
config:
leadMinutes: 10
notify: popup
绝对路径挂载之所以可行:@deepseek-ai/dsh-client-modules 的 nearestPackage()
会从入口文件向上找最近的 package.json,据此读 dsh.client.platform
并解析 exports["./client"]。无需 npm 安装,整个插件就是一个自包含文件夹。
node --check lib/*.js # 语法
node --test # 全部测试(单测 + Host 集成 + 烟测)
npm run accept # 9 条验收标准的离线推演报告(不需要等到明早 8:50)
# 非侵入式挂载校验(不碰正在用的 profile)
dsh --profile web --patch test/patch.probe.yml --dump-config
关于热重载,实测事实如下(不要相信"patch 层是 live 所以就全能热更"):
| 变更类型 | 是否即时生效 |
|---|---|
| profile patch 层(改配置 / 加减挂载行) | ✅ 即时生效,无需重启(patchReload: live) |
插件源码(lib/*.js 内容) | ❌ 不会生效,必须重启 DSH |
原因:patch 层重载会 dispose 并用新配置重新 apply(),但 Node 的 ESM 模块缓存
仍返回旧模块体(同一 file:// URL)。实测排除的绕法:
@deepseek-ai/cordis-plugin-hmr 实例 —— 未能接管hmr 行本身是 disabled: true判断当前跑的是哪份代码:GET /api/peak-brief.state 里的 build 字段。
calendar + windows + 单测与 Host 集成测试(只读,不拦截任何东西)gate:llm/stream 硬拦 + /peak-allow 逃生窗口brief:ctx.llm.stream 生成结构化简报并落盘resume:周期调度 + inject/followup + 目标 disarm/resume + 删文件 + 重启对账npm run accept 离线推演 9/9 通过(含斜杠命令逃生)agent/request-error + { kind: 'retry' }、
落盘兜底 + 重启对账、横幅「立即放行 / 取消推迟」agent/request-error waterfall 上的 park → 谷期 retry → 重发抵达适配器debug-tick 推演工作日 / 调休日 / 法定节假日)T-0 前在途的请求无法追回。user/message 受
model-visible ⟺ logged 约束,无法从会话记录抹除,只能之后被常规压缩带走。armed 是 process-local:每次 session-start 都会解除武装,重启后需重新武装。MIT