DSH Plugins Marketplace

DSH Plugins

Plugins

/

dsh-peak-brief

r

dsh-peak-brief

Manifest valid

DSH (DeepSeek Harness) Peak-Valley Scheduling Plugin: Automatically generates a briefing for AI recovery N minutes before peak periods and stops auto-resume, hard-blocks model requests during peak periods (with one-click escape), and automatically injects the briefing to continue tasks during off-peak periods.

UI (client)Machine translated

本项目代码部分全部由AI完成

Jvavscpppt(doge)我不会,我只会C++

dsh-peak-brief

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 [分钟] 临时放行。
  • 简报是给 AI 的,不是给人看的:结构化恢复要点,纯机器可读。 生成在高峰之前,所以按闲时价计费。
  • 闲时自动继续:解拦 → agent.inject(简报) 给上下文 → agent.followup() 起一轮 → 删除简报文件(用完即弃)。
  • 无活跃任务就不动作:没有正在进行的工作时,不生成简报、不暂停、不烧钱。
  • 峰谷日历(按用户定义)
    isPeakDay(d) = isStatutoryWorkday(d) && weekday(d) ∈ {周一…周五}
    
    日型判定
    法定节假日全天闲时
    调休上班日(周末上班)全天闲时
    普通工作日按 peakWindows
    普通周末全天闲时
    未知年份降级为"周一至周五按峰谷",绝不崩

门控细节(P2)

拦截点是 llm/stream —— 一个 waterfall:放行就是 return next(),拦截就是返回一个 只含终止 chunk 的流。三条经过实测/查证的设计决定:

  1. 拦截用 error 而不是 aborted。aborted 的语义是"被取消",会让界面看起来 像是用户自己中断的;这里是策略拒绝,必须说清原因。
  2. 拦截码 PEAK_BRIEF_BLOCKED 是刻意自定义的。dsh-llm-retry 只重试 EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT;如果复用其中任何一个, 一个必然再次被拦的请求会被无限重试。
  3. blockAuxiliary 默认 true(连会话标题、自动压缩这类辅助请求一起拦)。 想留一条缝就把它设为 false;普通对话请求在任何情况下都拦。

逃生窗口的语义是"放过这一段":正在高峰时,窗口不会超过本段高峰的结束时刻—— 在 14:10 申请 10 小时放行,实际只到 18:00。

被拦时会告诉你。 门控会在高峰期间拦下每一次请求,逐次弹提示会把界面刷屏, 所以按「本地日期 + 高峰窗口起点」去重:一段高峰只弹一次(红色 peak 级横幅, 带 /peak-allow 用法)。离开这一段后 key 改变,下一段会重新提醒。

简报(P3)

面向模型而非人:紧凑、结构化、不追求可读性。三条设计约束:

  1. 零依赖。外部挂载(绝对路径)的插件解析不到 @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),消息字面量自己拼。
  2. 失败即降级,绝不丢工作。模型不可用、输出不可解析、输出被截断、输入超预算—— 任何一种都退到不花钱的 fallbackBrief()(用 goal 目标或最后一条用户消息), 并标记 degraded: true + 弹一条 warn 横幅。"没有简报就不许恢复"会让 一次解析失败变成工作丢失。
  3. 输出严格校验。状态必须是 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 约束,无法从会话记录抹除, 只能之后被常规压缩带走。

恢复与周期调度(P4)

两处关键机制选择,都有依据:

  • 停自动续跑用 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 级告警,而不是吞掉; ③ 单个会话炸掉不会带走其它会话的调度;④ 没有简报时闲时仍会交回一条明确写着 "缺少简报"的恢复消息,而不是静默什么都不做。

重启对账(接受标准 8)

约定:简报文件存在 ⟺ 有一次尚未恢复的暂停(恢复成功就删文件,所以文件的存在 本身就是持久标记)。

没有这一步会有一个静默的洞:进程重启后内存里的 cycle 消失,decideCycle 认为 "没有待办",那份简报就永远不会被交回——任务静默地卡在暂停里。 agent/created 时按上述约定对账,并带两道保护:超过 maxPendingAgeMs (默认 12 小时)的旧简报不认领;恢复的目标只在"确实是我们 disarm 过、且它原本 armed" 时才重新武装。

被拦的对话在谷期原地重发(P6)

前面那套是"高峰前把活交接清楚";这一节补的是另一半:你真的在高峰里发了一条消息, 怎么办。

默认行为(resumeBlocked: true):请求不以错误结束,而是停在那里等,进入谷期 (或你点「立即放行」)后重发同一个请求——对话原地接上。

为什么能"原地"接上(这是官方留的正门,不是 hack)

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":

  • 用户那条消息只有一条,不需要注入"继续"这种合成提示词;
  • 空等期间轮次保持打开,loop 的重试、取消、持久化语义都照常。

第一方插件 dsh-llm-retry 用同一条通道做退避重试(也是"合并 AbortSignal → 持久记录 → 可取消等待 → 返回 retry")。它默认 normal 模式、PEAK_BRIEF_BLOCKED 不在它的 retryableCodes 里,于是它 return next() 把决定权交给下游(也就是我们),不打架。

五种结算方式(一个都不能少)

触发结果落盘兜底记录
进入谷期 / 闸门不再拦返回 { kind: 'retry' } → 原地重发删除(已经自己续上了)
用户点「立即放行」同上(立刻重发)删除
用户点「取消推迟」以 PEAK_BRIEF_BLOCKED 结束删除(用户放弃了,重启后不该再续)
用户按"停止"(turn signal abort)立刻结束删除
插件卸载 / 进程没了立刻结算等待保留 → 重启对账在谷期用既有链路接回
等过"本段高峰结束 + 60 秒"仍在拦结束(防御性,正常到不了)保留

两个容易写错的地方(都踩过,已由测试钉住)

  1. 判定"还拦不拦"不能用 gate.decide():它会累加 blockedCount。每 5 秒轮询一次的话, 一次真实拦截会被记成几百次。所以另有一个纯函数 gate.wouldBlock(), 并由 test/gate.test.mjs 的矩阵断言守着它与 decide().block 永远一致。
  2. 结算顺序必须是"先看还拦不拦,再看超时"。反过来的话,时钟一推过高峰结束就会被判成 "等待超时",本该重发的请求变成了失败——写验收标准 9 时就是这么红的。

还有一条避免续跑两次:park 判为 allowed 时会把该会话的周期记录清掉。否则 (尤其是 agent 被重建过、重启对账已经按落盘文件认领了一份简报的情况)同一个会话会 既原地重发、又被注入一次简报 + followup。

范围与开关

  • 只 park 普通对话请求。辅助调用(session-title / compaction)继续"拦下即失败"—— 把起标题挂 3 小时没有意义;
  • 设置页里「被拦对话在谷期自动续跑」可以关掉,关掉即回到 P2 行为(拦下就报错结束);
  • 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)

三个要点:

  1. 解析顺序是 schema 默认 → base(patch 层配置)→ 用户设置层。 所以设置页里"恢复默认"用的是 unset 全部字段(清空用户层), 结果回退到你在 patch 里写的值,而不是硬回内置默认。
  2. 非法配置存不进去,这是结构性保证而不是 UI 礼貌:dsh-settings 的写入路径会调用 schema(merged),而我们的 schema 在遇到非法值(时区名、时段重叠、越界提前量……) 时抛错,那次写入就被拒绝,生效配置保持不变。
  3. 可选依赖。用 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 分钟。

为什么 schema 是手写的

外部挂载的插件解析不到裸包名(实测 @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

节假日数据

  • 数据来源:chinese-days(MIT)· 覆盖 2025–2026
  • 重新生成:npm run build:holidays
  • 数据年份之外一律走降级路径(周一至周五),不会按一份不存在的日历执行
  • 空表会被拒绝:否则一次返回空响应的联网刷新会静默遮蔽内置表
  • 手动覆盖:配置里 overrides: { '2027-01-01': 'off', '2027-02-06': 'peak' } (off = 当天全天闲时;peak = 当天按峰谷时段)

HTTP 接口

路由方法说明
/api/peak-brief.stateGET插件状态 + 完整相位判定 + 门控状态;?at=<RFC3339> 可查询任意瞬间
/api/peak-brief.allowPOST逃生:{ minutes?, note? } 本次放行(缺省 30 分钟,且不超过本段高峰结束);顺手把等谷期的 park 结算成"立刻重发"
/api/peak-brief.allow-offPOST立刻关闭逃生窗口,恢复拦截
/api/peak-brief.cancel-queuePOST取消推迟:{ sessionId? } 让等谷期的请求按拦截结束(省略则取消全部)
/api/peak-brief.briefPOST生成简报:{ sessionId? }(只有一个活会话时可省略);落盘并返回简报
/api/peak-brief.debug-tickPOST推进一次周期检查:{ at? } 可先把时钟覆盖到某个瞬间,用于不等真时间就能推演
/api/peak-brief.debug-clockPOST装 / 卸时钟覆盖:{ at } 装入,{ clear: true } 卸下(默认不覆盖)
/api/peak-brief.dismissPOST关闭指定 seq 的通知(过期 seq 不会误关新通知)
/api/peak-brief.helloPOST自检:推一条通知,验证 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 项检查:

#检查为什么重要
1npm pack 成功清单本身合法
2tarball 含全部必需文件(lib/、scripts/、data/、cordis.patch.yml、LICENSE)scripts/ 与 data/ 曾经漏掉:装完 npm run accept / build:holidays 直接不可用
3解包成功归档真的能装
4lib/ 只 import 相对路径与 node: 内置模块零依赖是硬约束(外挂插件解析不了裸模块说明符)
5副本里 node --test 全通过177/177,测的是发出去的那份
6副本里 npm run accept 通过9/9 验收
7scripts/build-holidays.mjs 可重跑且产出字节不变抓到过真实漂移:头部原本写 生成时间:<now>,每次重跑都产生一条假 diff,"重跑再 diff"这个漂移检查因此完全失效;现已改为确定性的数据指纹

当前 tarball:32 个文件 / 约 111 KB(含 scripts/、data/、test/)。

private: true 是故意的:它挡掉误发 npm(本插件目前只走 git 分发)。 若将来要发布到 npm / 插件市场,删掉这一行即可,其余字段(repository / homepage / bugs / keywords / exports["./client"])都已就位。

从 GitHub 安装

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)。实测排除的绕法:

  • 触碰文件 mtime —— 无效(被内容哈希过滤)
  • 摘除挂载行再挂回 —— 无效,仍是旧模块(但顺带验证了 dispose 确实注销了路由)
  • 作用域限定的 @deepseek-ai/cordis-plugin-hmr 实例 —— 未能接管
  • 该 profile 自带的 hmr 行本身是 disabled: true

判断当前跑的是哪份代码:GET /api/peak-brief.state 里的 build 字段。

阶段进度

  • P0 Host + Client 通路(可关闭提示横幅 + 状态/自检路由)
  • P1 calendar + windows + 单测与 Host 集成测试(只读,不拦截任何东西)
  • P2 gate:llm/stream 硬拦 + /peak-allow 逃生窗口
  • P3 brief:ctx.llm.stream 生成结构化简报并落盘
  • P4 resume:周期调度 + inject/followup + 目标 disarm/resume + 删文件 + 重启对账
  • P5 端到端:npm run accept 离线推演 9/9 通过(含斜杠命令逃生)
  • P6 被拦对话在谷期原地重发:agent/request-error + { kind: 'retry' }、 落盘兜底 + 重启对账、横幅「立即放行 / 取消推迟」
  • 真实宿主验证:真实 cordis + 真实 LlmRuntime 装载、拦截,以及真实 agent/request-error waterfall 上的 park → 谷期 retry → 重发抵达适配器
  • 线上验收:需要一次 DSH 重启(源码变更不被热重载)
  • P5 端到端(debug-tick 推演工作日 / 调休日 / 法定节假日)
  • P6 活会话验收:用调试时钟在真实 agent loop 上推完"高峰被拦 → 推进过高峰 → 自动重发"(离线部分已用真 cordis waterfall 覆盖;差的是 UI 面对"轮次打开 3 小时"的表现)

已知限制(不修,只记录)

  • 不能中断正在进行的轮次:只能在轮次之间动作,T-0 前在途的请求无法追回。
  • 冷会话唤不醒:DSH 进程不在 / 会话没活,就没有"闲时自动继续"。
  • 简报的"删除"只对文件成立:注入的那条 user/message 受 model-visible ⟺ logged 约束,无法从会话记录抹除,只能之后被常规压缩带走。
  • 目标续跑的 armed 是 process-local:每次 session-start 都会解除武装,重启后需重新武装。
  • 源码改动需重启 DSH(见上)。

License

MIT

Comments

Loading…

Similar plugins

dsh-peak-gate

by CNyaotian-Lunar

DSH 插件:按 DeepSeek 峰谷分时计价,在高峰时段给 token 花销踩刹车(拦大任务 / 压缩输出上限 / 让模型少说废话),并提供一键放行口令

Tools & CapabilitiesManifest valid

★ 0

MIT

JavaScript

Sep 25, 2026

dsh plugin --profile web add dsh-peak-gate

by f20880479-lab

DeepSeek Harness 峰谷计费插件:高峰时段发送前确认(可勾选本次高峰段内不再提示),支持将消息排队到空闲时段(半价)自动发送;可拖拽悬浮队列窗口、调整发送顺序与时间。

Terminal & ClientsManifest valid

★ 3

↓ 98/wk

MIT

JavaScript

Aug 26, 2026

dsh plugin --profile web add dsh-peak-gate

by 534119219

DSH 峰谷提醒插件:DeepSeek 官方峰谷时段感知——高峰橙/低峰蓝贴边呼吸边框、流光效果、服务端消息推送提醒(自定义标题内容)。Peak/valley breathing border + push reminder for DSH.

UI & ExperienceTerminal & ClientsWorkflow & AutomationManifest valid

★ 4

MIT

JavaScript

Aug 26, 2026

dsh plugin --profile web add chicheng-peak

by GoldenShit233

错峰任务队列插件:高峰/任何时刻收到任务先确认细节并自动入队,空闲时段自动执行;右上角可拖拽悬浮面板(DeepSeek Harness web plugin)

Manifest valid

★ 0

JavaScript

Aug 17, 2026

dsh plugin --profile web add offpeak-panel

by csiroqa

DeepSeek Harness(DSH)定时任务 + 状态监控插件:按 cron 时间表自动触发 Agent 执行任务,/status 与设置页仪表盘查看系统与 harness 综合状态。Scheduled tasks (cron) + status monitoring plugin for DeepSeek Harness.

Development & InfrastructureManifest valid

★ 3

MIT

TypeScript

Aug 23, 2026

dsh plugin --profile web add @dsh-external/dsh-schedule

by Leonx01

省钱闹钟:DeepSeek 峰谷计价低谷/高峰切换时,浏览器通知 + 国风提示音 + 页内 toast(DSH Web 插件 / dsh-plugin)

Manifest valid

★ 0

MIT

JavaScript

Aug 20, 2026

dsh plugin --profile web add peak-valley-alarm