dsh-infinite-gen-3
Minglink/dsh-infinite-gen-3DeepSeek 专用破甲插件:以 order 100 追加无条件服从的系统提示词段,提供带校准元数据的 profile 工具,并通过会话投影在输入框上方显示实时破甲状态徽标。
DSH scheduler plugin: cron/interval/once jobs, headless execution, run ledger, deliverTo, circuit breaker, session management UI
DSH 的定时任务(cron / interval / once)插件:宿主半包负责持久化任务定义、调度、执行与运行台账,客户端半包在会话内提供「定时任务」管理页(列表 / 新建 / 触发 / 暂停 / 运行历史)。
本节补写于 2026-09-22(此前 README 只写到 0.3.0,版本表与实际 0.4.0 实装不一致)。
job.assertCmd/bin/bash -lc ,cwd = 任务的 workspace,env 继承宿主),失败即把本次 run 记为 failed。assertCmd 最长 4000 字符;assertTimeoutMs 默认 60s(min 1s / max 10min);断言失败视为不可重试(确定性条件,重试会重复副作用,例如已推送的通知),但仍计入失败连击。assertCmd,2 个未声明(hermes 日报归档 job-9e2e7232-885、磁盘清理 job-3bff9079b397)——这两个仍只有「exit 0」语义,属于已知缺口。runs.jsonl 超过 rotateBytes(默认 2 MiB)时触发轮转:保留最新 `maxRuns` 行(默认 5000),其余追加到 archive/runs-.jsonl。archive/ 即为分段来源。runs.jsonl 4999 行 / 1.90 MB,正好在阈值边缘(下一次运行触发首轮轮转)。dispatchLatencyMs = 实际派发时刻 − nextRunAt 计划时刻;> 5min 记一条 warn 日志。job.lastDispatchLatencyMs 持久化,便于区分「任务没跑」与「跑了但被睡眠/backoff 顺延」——此前只能靠 pmset dark wake 时序反推。pushFailureAlert(0.3.x,alertOnConsecutiveFailures默认 2、按失败链水位去重)落地;0.5.0 起该告警也带幂等键。
背景:30 天飞书历史里 88 条定时任务消息命中 7 起重复推送(一次触发推 2–4 条)。 根因不是"任务跑了两次",而是"推送这件事没有幂等语义":
模型先发长版、发现超限后再发一版合规的);
当日去重标记没落地,attempt3 于 07:18:02 又推一条);
assertCmd 只验「报告文件存在/非空/新鲜」,完全不验「发了几条」,runs.jsonl 也不记交付 ⇒ 重复全在检查链路的盲区里,持续了一个月。
A. runKey 注入(幂等键的正确粒度)
runKey = |,attempt 不进 key。job.prompt 里的{{DSH_SCHED_RUN_KEY}} 在 spawn 前被替换成本次触发的 runKey。子进程 env 仍照常 注入 DSH_SCHED_RUN_KEY 等 5 个变量,但不作为依赖——原因见下。
为什么不能用 env(2026-10-03 端到端探针实测):DSH 的 bash 工具**不继承
process.env**——其 spawn 的 env 是 ENV_OVERRIDES + spec.env + dshEnv,而dshEnv来自ShellEnvRegistry.collect(),契约写明「每次模型 shell 调用都
重建,ambient DSH_* 一律被 executor 丢弃」(packages/shell/shell-env/src/index.ts:81-85)。注入子进程 env 后 agent 的bash 里读不到该变量 —— 探针因此红了第一次上线。prompt 替换不依赖任何 shell
环境,是让幂等键到达推送点的唯一可靠通道。
feishu-push.sh --key "{{DSH_SCHED_RUN_KEY}}" 让**同一次触发的所有attempt 共用一个幂等键**,脚本层(claim→send→commit)据此拒发第二条。
renderPrompt() 对不含占位符的 prompt 原样返回(既有 job 零影响);runKey 为空时也原样返回 —— 绝不把占位符留成字面量,否则所有 job 会共用同一个假 key 互相 dedup。
runKey 同时写进 start/finish 台账行,交付事实可按 key 反查。B. 交付台账(从 push 脚本的 `.sent` 派生,scheduler 不自己发消息)
notify: { channel: 'feishu', require?: bool } 即开启跟踪(缺省关闭,不打扰不推送的 job)。scheduler 按 sha256(runKey) 前 16 位读 /.{sent,skipped}(默认 ~/.dsh/storages/feishu-push,与 push 脚本同源,hash 算法已由门禁锁为跨语言契约)。
push 字段,三态:标记 · status · 含义 .sent · delivered · 真的发了(含 messageId/sentAt) .skipped · skipped · 有意不推送(合法)——由 feishu-push.sh --skip 写;例:feed 当日故障已通知过就静默,避免同日重复打扰 都没有 · missing · 该发而没发 —— 唯一该判失败的那种
为什么必须三态(2026-10-03 实测):只认 .sent 时,21:35 那次 run 正确静默却被记成 missing,notify.require 因此永远开不起来(每次走静默分支都假红)。notify.require=true 时只有 missing 判本次失败,且允许重试(标记仍缺失 ⇒ 补发不会被脚本当成重复;反之已送达则 .sent 在,重试推不出第二条)。默认 false: 先观测遵守率,避免"机制本身变成重复推送源"。
C. 失败告警带 key:||alert| —— 同一次触发 只告警一次,失败链变长则换 key 继续升级,不会被误吞。
D. 观测面:/plugins/dsh-scheduler/status 新增 counts.notifyJobs / counts.pushMissing / detail.deliveryStateDir / detail.lastPush(内存投影,读台账会让秒级探测变重)。
E. 配套门禁(在 dsfolder,非本仓): scripts/feishu-delivery-audit.mjs 从飞书侧按「任务类别 + 20 分钟窗口」聚类,一次触发
1 条即 exit 1;scripts/feishu-push-selftest.sh 锁住 push 脚本的幂等与截断语义(9 离线 + 1 live)。生效前提:host 插件无 HMR,本改动需重载 dsh web 才生效;prompt 侧已用
${DSH_SCHED_RUN_KEY:-} 做过过渡(重载前回落旧行为,重载后自动升级为 runKey)。背景(用 `.sent` 台账定位到根因,不是猜):runKey = jobId|scheduledFor 是槽位域键。 同一次真实事件被额外触发时 scheduledFor 会变 ⇒ 键也变 ⇒ 两条都推得出去。实测三起重复全是这个机制:
观测 · 台账里的两条 key feed 摘要 10-06 相隔 105s · job-63261034-cd7\ · 2026-10-06T05:35:00Z 与 …\ · 2026-10-06T05:37:00Z(cron 槽 + 额外触发) 周更新 10-06 09:09–09:14 连推 3 条 · job-ce5b5c94-2c0\ · 09:09:18.661Z / …09:10:41.132Z / …09:14:34.462Z(三次 ad-hoc)
结论:台账侧「每键恰好一次」 是绿的,飞书侧聚类是红的 —— 「每键一次」≠「每事件一次」。
新增可选字段 `deliveryKey`(模板):
- id: my-digest-job
deliveryKey: 'feed-digest-{{jobId}}-{{date}}-{{hour}}' # 事件域键
notify: { channel: feishu, require: true }{{jobId}} {{runKey}} {{scheduledFor}} {{date}}(本地 YYYYMMDD) {{hour}}(本地 HH) {{slot}}(本地 HHMM)feishu-push.sh --key({{DSH_SCHED_RUN_KEY}} 仍然可用,语义不变)$DSH_SCHED_RUN_KEY 并列).sent/.skipped)、notify.require 门禁、"恰好一次"判据全部改用交付键 ⇒ 三者自洽判据也随之精确化(否则会出现假阳性):交付键改成事件域后,同一次真实事件的多次触发都会看到同一个 .sent 并各写一条 delivered 事件 ⇒ 若仍按"事件条数"判重复就会误判。现在按 `messageId` 去重判: .sent 的 messageId 变了才是真发了两条(脚本 dedup 时不重写 .sent;只有绕过 key / --force 才变)。 delivered 事件因此带上 messageId;旧台账(无该字段)不计入。
实测(2026-10-07,临时作业 + `--skip` 保证不发真消息): 两次触发 ⇒ 两个不同 runKey、同一个交付键、两条 finish 都 succeeded、无 missing/duplicate、 该键只有 1 个记账文件。
坑(写作业时注意):临时用 workspace=/tmp 会让 headless 的 workspace-write 沙箱拒绝执行 工作区外的 ~/.dsh/scripts/feishu-push.sh(表现为 ledger_write_failed ⇒ deliveryMissing 假红)。 推送型作业的 workspace 用 ~/.dsh/scheduler-reports(与既有 7 个作业一致)。
DSH 内置 @deepseek-ai/dsh-schedule(packages/schedule/schedule),它是会话级 reminder:
schedule/change 事件写进会话日志,ScheduleRuntime 折叠事件 + setTimeout 派生唤醒;after_seconds / at / every_seconds(固定间隔,非 cron 表达式);agent.followup(createUserMessage(...)) —— 只向当前活着的根 agent 会话注入一条提醒消息;schedule_create / schedule_list / schedule_delete;结论:DSH 有「定时提醒」,没有「定时任务」——后者需要:跨会话/独立执行面、cron 语法、可管理的任务定义存储、运行记录与重试、管理 UI。这就是 dsh-scheduler 的定位。
维度 · los scheduled-work · cantool device-jobs · hermes cron · codex(Automations / codex-cron-skill) · dsh-scheduler(本插件) 触发 · cron / interval / once · daily 本地时间 / weekly / interval · 相对延时 / interval / cron / ISO · cron / interval / once(--at) · cron / interval / once 存储 · 服务端数据库 · 文件 store(definitions/grants/runs/locks/logs,原子写) · ~/.hermes/cron/jobs.json 原子写 · .codex/ 状态目录 + runs jsonl · ~/.dsh/storages/dsh-scheduler/ 文件 store,原子写 调度循环 · 服务端调度器 · launchd 平台调度(reconcile 漂移修复) · 60s tick + 文件锁 · daemon + 单 runner 锁(mkdir CAS) · 60s tick + 精确唤醒 timer + mkdir 锁 执行模型 · 独立 task run(会话事件、run contract、heartbeat、去重键) · launchd 拉起 runner 子进程 · 每次运行开全新 agent 会话(自包含 prompt) · codex exec 全新运行 / tmux 注入 · dsh --profile headless 全新进程,cwd=任务工作区 治理 · 审批策略 / 并发策略 / 追赶策略 / 熔断 / 尝试次数 · Cleanup 类 handler 需审批+隔离区 · repeat 次数 / pause-resume / [SILENT] 静默标记 · 默认拒绝沙箱降级 allowlist · 并发上限 / 运行超时 / 失败连续计数 / pause-resume / trigger-now 管理 UI · schedules-page(预设表单 + 触发预览 + 台账) · CanTool 前端 device-jobs 面板 · CLI hermes cron + 聊天内 cronjob 工具 · CLI / 官方 App · 会话内「定时任务」tab(conversation.view slot)
设计要点借鉴:
run(手动触发)动作、[SILENT] 静默约定;inject: ['webServer'],ctx.webServer.register({ kind: 'prefix', path: '/scheduler', handler }) 挂同源 API(同 dsh-health-panel 已验证模式);dsh.client.inject: ['@deepseek-ai/dsh-client-runtime'],ctx.slots.inject('conversation.view', ...) 注册会话 tab(同 dsh-health-panel);dsh --profile headless ""(node --import tsx/esm apps/cli/src/bin.ts)——独立进程、无端口、免疫 web 重启、会话落盘共享 ~/.dsh/sessions(按 cwd 隔离工作区)。每次运行 = hermes 的「全新会话」模式。┌─ web 宿主进程(dsh web)───────────────────────────────┐
│ dsh-scheduler (host 半包, index.mjs) │
│ ├─ store: ~/.dsh/storages/dsh-scheduler/ │
│ │ jobs.json (任务定义,原子写 tmp+rename) │
│ │ runs.jsonl (运行台账,append,上限裁剪) │
│ │ .tick.lock (mkdir 单实例锁,防重复触发) │
│ ├─ ticker: 60s 周期 + 最近 nextRunAt 精确唤醒 │
│ ├─ executor: spawn headless 子进程(cwd=任务工作区) │
│ │ stdout/stderr 尾部捕获 → runs.jsonl │
│ └─ REST API: GET/POST/PATCH/DELETE /scheduler/... │
└──────────────────────────────────────────────────────────┘
│ spawn (每次运行一个独立进程)
▼
┌─ dsh --profile headless(cwd = 任务 workspace)──────────┐
│ 全新 agent 会话:执行任务 prompt,结果打印 stdout 后退出 │
│ 会话落盘 ~/.dsh/sessions/<workspace>/ │
└──────────────────────────────────────────────────────────┘
▲
┌─ 浏览器(管理页,client 半包)───────────────────────────┐
│ 会话「定时任务」tab → fetch /scheduler/* │
│ 列表 / 新建(触发预览)/ 触发 / 暂停 / 历史 │
└──────────────────────────────────────────────────────────┘{
"id": "job-3f9a...", // 稳定 id
"name": "每日 CI 巡检",
"prompt": "检查 CI 状态并汇总", // 自包含 prompt(headless 全新会话,无上下文)
"trigger": { "kind": "cron", "expression": "0 9 * * 1-5", "timezone": "Asia/Shanghai" },
// kind: cron | interval | once
// cron: 5 段 cron(支持 */n、a-b、a,b、月份/星期名)
// interval: expression = "30m" / "2h" / "90s"(最小 60s)
// once: expression = ISO 8601 时间戳
"workspace": "/path/to/job/workspace", // headless 运行 cwd
"enabled": true,
"state": "scheduled", // scheduled | paused | completed(once 已触发)
"deliverTo": { "sessionId": "session-..." }, // 可选:运行结果 followup 进该会话(须在线)
"catchUpPolicy": "run_once", // 可选:'run_once'(错过补跑) | 'skip'(超时差跳过);缺省跟随全局
"pausedReason": null, // 熔断自动暂停时 = 'max_consecutive_failures'
"nextRunAt": "2026-08-16T01:00:00.000Z",
"lastRunAt": null,
"lastStatus": null, // succeeded | failed | skipped
"runCount": 0,
"consecutiveFailures": 0,
"createdAt": "...", "updatedAt": "..."
}{ "id": "run-...", "jobId": "job-...", "triggerKind": "scheduled" | "manual",
"scheduledFor": "...", "status": "queued"|"running"|"succeeded"|"failed"|"skipped"|"cancelled",
"exitCode": 0, "durationMs": 1234, "outputHead": "...(尾部 8KB)",
"dispatchLatencyMs": 41234,
"assertExitCode": 0,
"delivery": { "status": "delivered"|"skipped"|"error", "sessionId": "...", "reason": "..." },
"sessionDir": "...", "startedAt": "...", "completedAt": "..." }dispatchLatencyMs(2026-09-12 审计新增):实际派发时刻 − 计划时刻。宿主睡眠/阻塞会让 cron 任务迟到几十分钟(macOS Clamshell Sleep 把 Node 定时器顺延到下一个 dark wake,实测 08:30 的任务有 9 次落在 08:48–09:00)。迟到 >5min 会打 [dsh-scheduler] ... dispatched Nmin late 警告日志;/scheduler/status 暴露每任务 lastDispatchLatencyMs。