dsh-peak-brief
Manifest validDSH (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.
本项目代码部分全部由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/streamwaterfall 拦截一切模型请求(硬拦), 可用/peak-allow [分钟]临时放行。
- 简报是给 AI 的,不是给人看的:结构化恢复要点,纯机器可读。 生成在高峰之前,所以按闲时价计费。
- 闲时自动继续:解拦 →
agent.inject(简报)给上下文 →agent.followup()起一轮 → 删除简报文件(用完即弃)。 - 无活跃任务就不动作:没有正在进行的工作时,不生成简报、不暂停、不烧钱。
- 峰谷日历(按用户定义)
isPeakDay(d) = isStatutoryWorkday(d) && weekday(d) ∈ {周一…周五}日型 判定 法定节假日 全天闲时 调休上班日(周末上班) 全天闲时 普通工作日 按 peakWindows普通周末 全天闲时 未知年份 降级为"周一至周五按峰谷",绝不崩
门控细节(P2)
拦截点是 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 改变,下一段会重新提醒。
简报(P3)
面向模型而非人:紧凑、结构化、不追求可读性。三条设计约束:
- 零依赖。外部挂载(绝对路径)的插件解析不到
@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约束,无法从会话记录抹除, 只能之后被常规压缩带走。
恢复与周期调度(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 秒"仍在拦 | 结束(防御性,正常到不了) | 保留 |
两个容易写错的地方(都踩过,已由测试钉住)
- 判定"还拦不拦"不能用
gate.decide():它会累加blockedCount。每 5 秒轮询一次的话, 一次真实拦截会被记成几百次。所以另有一个纯函数gate.wouldBlock(), 并由test/gate.test.mjs的矩阵断言守着它与decide().block永远一致。 - 结算顺序必须是"先看还拦不拦,再看超时"。反过来的话,时钟一推过高峰结束就会被判成 "等待超时",本该重发的请求变成了失败——写验收标准 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)
三个要点:
- 解析顺序是 schema 默认 →
base(patch 层配置)→ 用户设置层。 所以设置页里"恢复默认"用的是unset全部字段(清空用户层), 结果回退到你在 patch 里写的值,而不是硬回内置默认。 - 非法配置存不进去,这是结构性保证而不是 UI 礼貌:dsh-settings 的写入路径会调用
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 分钟。
为什么 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.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"])都已就位。
从 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-errorwaterfall 上的 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
by CNyaotian-Lunar
DSH 插件:按 DeepSeek 峰谷分时计价,在高峰时段给 token 花销踩刹车(拦大任务 / 压缩输出上限 / 让模型少说废话),并提供一键放行口令
★ 0
MIT
JavaScript
Sep 25, 2026
dsh plugin --profile web add dsh-peak-gateby f20880479-lab
DeepSeek Harness 峰谷计费插件:高峰时段发送前确认(可勾选本次高峰段内不再提示),支持将消息排队到空闲时段(半价)自动发送;可拖拽悬浮队列窗口、调整发送顺序与时间。
★ 3
↓ 98/wk
MIT
JavaScript
Aug 26, 2026
dsh plugin --profile web add dsh-peak-gateby 534119219
DSH 峰谷提醒插件:DeepSeek 官方峰谷时段感知——高峰橙/低峰蓝贴边呼吸边框、流光效果、服务端消息推送提醒(自定义标题内容)。Peak/valley breathing border + push reminder for DSH.
★ 4
MIT
JavaScript
Aug 26, 2026
dsh plugin --profile web add chicheng-peakby GoldenShit233
错峰任务队列插件:高峰/任何时刻收到任务先确认细节并自动入队,空闲时段自动执行;右上角可拖拽悬浮面板(DeepSeek Harness web plugin)
★ 0
JavaScript
Aug 17, 2026
dsh plugin --profile web add offpeak-panelby csiroqa
DeepSeek Harness(DSH)定时任务 + 状态监控插件:按 cron 时间表自动触发 Agent 执行任务,/status 与设置页仪表盘查看系统与 harness 综合状态。Scheduled tasks (cron) + status monitoring plugin for DeepSeek Harness.
★ 3
MIT
TypeScript
Aug 23, 2026
dsh plugin --profile web add @dsh-external/dsh-scheduleby Leonx01
省钱闹钟:DeepSeek 峰谷计价低谷/高峰切换时,浏览器通知 + 国风提示音 + 页内 toast(DSH Web 插件 / dsh-plugin)
★ 0
MIT
JavaScript
Aug 20, 2026
dsh plugin --profile web add peak-valley-alarm