DeepSeek Harness plugin

dsh-plugin-scheduled-tasks

Scheduled tasks for DeepSeek Harness (dsh): at a daily / weekly / fixed-interval trigger, open a new session, send a prompt, and optionally push the result to a webhook.

Jump to install

Source facts

Repository
Jeff1573/dsh-plugin-scheduled-tasks
Latest update
Aug 19, 2026
Category
Memory
GitHub stars
0
Format
plugin
Catalog evidence
Upstream dsh.bundle evidence
Evidence path
package.json#dsh.bundle
Checked against
0.1.0-rc.8
Upstream check date
2026-08-20

This evidence comes from the upstream catalog. This site has not installed, run, or security-reviewed the plugin.

Install

Start with a prompt that asks an agent to review the GitHub repository and source. Switch to the command if you want to install it yourself.

Copy this prompt into DSH, Codex, or another agent and ask it to review the GitHub repository and source first.

Do not install or run any commands yet. Read this plugin's GitHub repository, README, and relevant source code. Then answer the questions below clearly and directly so I can decide whether it fits my needs:

1. What is this plugin, and what problem does it solve?
2. Who is it for, and what are its typical use cases?
3. How is it used after installation? Include one minimal example.
4. What known limitations or privacy, security, compatibility, or maintenance risks does it have?
5. Give a clear recommendation: recommend, conditionally recommend, or do not recommend, with reasons.

Distinguish statements documented by the repository, inferences from source code, and unknowns. If evidence is insufficient, say so explicitly. Do not guess or simply repeat the README.

GitHub: https://github.com/Jeff1573/dsh-plugin-scheduled-tasks
Plugin: dsh-plugin-scheduled-tasks
Author: Jeff1573

Check the source files

Read the README and other files from this plugin directory before installing.

File explorer3 files
README.mdSource · read only

dsh-plugin-scheduled-tasks

DeepSeek Harness 的定时任务插件。

到点自动新建一个会话并发起一轮对话——「每天九点帮我看一下 CI」这类事情不必有人守着。侧边栏底部有入口,可以增删改任务、暂停/启用、立即运行,并查看运行历史。

  • 三种触发规则:每天 / 每周几 / 固定间隔(最短 5 分钟),按 IANA 时区计算,正确处理夏令时跳变
  • 无人值守权限:附带一个「限定在工作区内、且不会停下来征求批准」的权限预设——dsh 出厂预设里没有这个组合
  • 完成通知:Bark / 企业微信群机器人 / ntfy / Server酱 / Telegram / 飞书 / 钉钉,或任意 webhook
  • 模型可以自己排任务scheduled_task_* 工具在每个会话里都能用
  • 点通知直接跳进那次会话,运行历史里也能点开
  • 一条命令装完即用dsh plugin --profile web add github:Jeff1573/dsh-plugin-scheduled-tasks,插件自带 bundle patch,不用手改配置
  • 可以放到反向代理后面(TLS + 密码)从任何设备访问,见 [反向代理部署](docs/reverse-proxy.md)

<!-- 建议在这里插入一张侧边栏面板的截图:这是访客第一眼最想看到的东西 -->

安装

前提:已经用得上 dshnpx @deepseek-ai/dsh web 或全局安装)。

一条命令(推荐)

dsh plugin --profile web add github:Jeff1573/dsh-plugin-scheduled-tasks

然后重启 dsh web 即可——不需要手改任何配置文件

dsh plugin 把参数转发给 profile 目录里的 pnpm,装完再按已安装状态调和 dsh.profile.bundles:凡是 manifest 里声明了 dsh.bundle.patch 的依赖都会自动加入层栈。本插件带着自己的 cordis.patch.yml,把注册自己那一行放在里面,所以装上就生效。

想跟某个分支或 tag,在后面加 #

dsh plugin --profile web add github:Jeff1573/dsh-plugin-scheduled-tasks#main

几点说明:

  • pnpm 必须在 PATH 上dsh plugin 只是个 pnpm 转发器,找不到 pnpm 会直接以 127 退出并提示;npm i -g pnpm 即可。
  • profile 不存在时会自动初始化,不用先手工建。
  • 本插件不发布到 npm registry,只从 git 安装。所以升级也走 git:dsh plugin --profile web update dsh-plugin-scheduled-tasks
  • 本插件没有 prepare 脚本lib/ 下的构建产物直接提交进仓库,所以 git 安装不需要任何构建步骤,也不会撞上 pnpm 的构建拦截。(其他需要在安装时构建的 git 插件会被 pnpm 拦下,那时 dsh 会提示你把它打印的 key 加到 profile 的 pnpm-workspace.yamlallowBuilds 下再重跑。)

手工安装(没有 pnpm 时)

仓库必须克隆到 ~/.dsh/profiles/ 目录树里面。 Node 会把 symlink 解析到真实路径再向上查找 node_modules,只有位于 profiles 树内才能共享 harness 那一份 @deepseek-ai/* 单例;放在别处会加载到重复实例,插件起不来。

mkdir -p ~/.dsh/profiles/plugins
git clone https://github.com/Jeff1573/dsh-plugin-scheduled-tasks.git \
          ~/.dsh/profiles/plugins/scheduled-tasks

mkdir -p ~/.dsh/profiles/web/node_modules
ln -sfn ~/.dsh/profiles/plugins/scheduled-tasks \
        ~/.dsh/profiles/web/node_modules/dsh-plugin-scheduled-tasks

然后手工做 dsh plugin 本来会替你做的那件事:把包名加进 ~/.dsh/profiles/web/package.jsondsh.profile.bundles追加在官方 bundle 之后,层的顺序就是数组顺序):

{
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "@deepseek-ai/dsh-web-app",
        "dsh-plugin-scheduled-tasks"
      ]
    }
  }
}

插件自带的 patch 层会接手注册,所以不要再往 cordis.patch.yml 里写 insert——两处都写会让插件在合成树里出现两次,而 --dump-config 不会报这个错。

(建议)无人值守权限预设

理由见下文[无人值守的权限](#无人值守的权限)。这一项不由插件自带:patch 的 config 是整体替换,插件擅自重述 permission 那一行会覆盖掉别的层定义的预设,而放宽部署的权限表属于运维决策,不该由某个插件的安装动作代劳。

所以这一段要你自己加到 ~/.dsh/profiles/web/cordis.patch.yml——注意出厂那三个必须原样重述:

- id: permission
  name: '@deepseek-ai/dsh-permission-presets'
  config:
    presets:
      read-only:
        sandbox: read-only
        approval: ask
      workspace-write:
        sandbox: workspace-write
        approval: ask
      danger-full-access:
        sandbox: danger-full-access
        approval: never
      scheduled:
        sandbox: workspace-write
        approval: never
        name: 定时任务
        description: 限定在工作区内,且不会停下来征求批准——供无人值守的定时运行使用。

确认与重启

dsh --profile web --dump-config | grep -A1 scheduled-tasks

只读、不 boot。应当恰好输出一条 id: scheduled-tasks;输出两条说明既进了 bundle 层又留着手写的 insert,去掉后者。确认无误后重启 dsh web——插件集合变更需要重启,包元数据缓存不过期。

从早期的手工安装迁移

0.1.0 之前本插件不带 bundle patch,装法是往 profile 的 cordis.patch.yml 里手写一行 insert。现在插件会自注册,那一行必须删掉,否则插件加载两次(两套定时器、两个侧边栏入口)。

cordis.patch.yml 里这一段删除:

- insert:
    - id: scheduled-tasks
      name: 'dsh-plugin-scheduled-tasks'

permission 那一段(如果加过)保留不动。然后按上面的「确认与重启」核对只剩一条。

卸载

dsh plugin --profile web remove dsh-plugin-scheduled-tasks

dsh plugin 的调和是双向的:依赖没了,它也会从 dsh.profile.bundles 里退出。手工安装的话,把包名从 bundles 里删掉、移除 symlink 即可。两种方式都需要重启。

任务数据留在 ~/.dsh/storages/scheduled_tasks.json,删掉该文件即彻底清除;webhook 地址在 ~/.dsh/.credentials.yamlSCHEDULED_TASKS_NOTIFY_URL

使用

重启后侧边栏底部会出现「定时任务」入口,点开就是面板。

建第一个任务

面板右下角「新建任务」。必填只有三项:

字段说明
触发时间「每天」/「每周」/「间隔」三选一。前两者填时:分,按表单下方显示的时区计算;「间隔」填分钟数(≥5)
任务名称列表和通知里显示的名字
你希望 dsh 做什么?到点后新建一个会话,把这段内容作为第一条消息发出

点「创建」即生效,列表里每行会显示下一次触发时间。

高级选项

同一个表单里展开「高级选项」:

  • 工作目录 —— 留空则用默认 cwd
  • 权限预设 —— 无人值守建议选 scheduled;留空跟随全局默认,而全局默认通常是会发问的,定时运行会卡住直到超时
  • Agent 预设 —— 留空跟随全局默认
  • 运行超时 —— 默认 30 分钟,到点 agent.cancel() 并记为「超时」
  • 失败后 5 分钟自动重试一次 —— 只重试一次,不串联
  • 进程重启后,若已错过触发则补跑一次 —— 默认关闭
  • 完成后通知 / 附带模型回复摘要 —— 可覆盖全局设置

运行与查看

列表每行有「立即运行 / 编辑 / 暂停 / 删除」:

  • 立即运行 会等到会话创建完成就跳进那个对话,可以看着它流式生成
  • 定时触发 的会话会在生成过程中自己出现在侧边栏,但不会抢占你正在看的界面
  • 历史 N 展开该任务的运行记录,每条都能点开对应会话;状态分 成功 / 失败 / 超时 / 已中断
  • 入口上带红点表示最近有失败

编辑复用新建表单,只是标题变成「编辑自动化任务」。编辑正在运行中的任务不会打断那一次运行,它跑完后按新记录重排。

完成通知

面板里「通知设置」,勾选「启用完成通知」后选通道:

通道怎么填
Bark(iOS 推送)地址填 App 给的 https://api.day.app/<KEY>,自架的 Bark 服务器同样可以
企业微信群机器人群里添加「群机器人」,把它给的 Webhook 地址粘进来。官方接口、无需 OAuth,但个人用要先注册企业才能建群
自定义模板地址填服务端点,下面粘贴它要的 JSON body,用 {{占位符}} 取值。界面里有 ntfy / Server酱 / Telegram / 飞书 / 钉钉的现成模板可一键套用
自定义 JSON webhook固定结构 {source, task, taskId, status, at, host, sessionId, detail?, reply?},发给你自己写的接收端

可用占位符:task taskId status statusText at atIso host sessionId detail reply

其余几项:

  • 默认时机 —— 「每次运行结束都通知」或「仅失败或超时时通知」,单个任务可在自己的高级选项里覆盖为 always / failure / never
  • Web 界面地址(可选) —— 填了之后推送会带上 <base>/?dsst-session=<id>,点通知直接跳到那次运行的会话。地址必须是收通知的设备能访问到的(内网 IP、内网穿透域名等),127.0.0.1 只在本机有效。
  • 默认附带模型回复摘要 —— 默认关闭,开启后只发前 200 字。通知本身必然要把任务名、时间、状态、主机名发出这台机器;回复内容属于额外外发,所以做成显式开关。
  • 发送测试通知 —— 会立刻向已保存的地址真实发送一条消息。

几条约束:

  • 地址即密钥。企业微信把 robot key 放在 query string 里,所以 URL 整体当凭据处理:存 ctx.credentialsSCHEDULED_TASKS_NOTIFY_URL(落在 ~/.dsh/.credentials.yaml,权限 600),不进任务表,也永不回传给浏览器——前端只拿到「是否已配置」和 origin。表单里留空表示「保持不变」。
  • 公网必须 https;明文 http 只允许发往本机或内网(127/10/192.168/172.16-31、localhost*.local),这样自架的 Bark/ntfy 在局域网里能直接用,而带 key 的地址永远不会明文走公网。
  • 通知失败不会把成功的运行记成失败:投递错误写在该次运行记录的 notifyError 上,仅此而已。
  • 企业微信群机器人有频率限制(每分钟约 20 条),高频间隔任务请酌情用「仅失败时通知」。
  • 个人微信没有官方发送接口。 网上看着能用的那些库走的是逆向出来的网页/桌面协议,违反服务条款且会导致封号,所以这里不提供这种渠道。想在微信里收,走企业微信(可转发到个人微信)或 Server酱 一类的服务号中转(用「自定义模板」通道)。纯个人用其实 Bark / ntfy 更顺手,而且可以自架。

在对话里排任务

scheduled_task_list / scheduled_task_create / scheduled_task_delete 注册在根 context 上,所以每个会话都能用。「每天九点帮我看一下 CI」可以在对话里直接说,不必打开面板。

scheduled_task_create 的参数:namepromptkinddaily / weekly / interval)、hourminuteweekdays(0=周日)、minutes(≥5)、timeZonecwd、权限预设。

注意定时任务跑起来的会话同样能看到这三个工具,也就是说一个定时任务可以再排新的定时任务。

设计与实现

为什么不用 @deepseek-ai/dsh-schedule

dsh-schedule 是 *session-local reminder*:只能在一个已经活着的会话里给它自己的 agent 排后续消息(deliveryMode 是写死的 'session-local'),冷会话不触发,也不支持日历/cron 语义。本插件要的是「无人值守、到点新建会话」,两者不是同一件事,所以自己拥有触发器并调用 ctx.agents.create()

组成

文件作用
lib/index.jsHost 半边:持久化、定时、建会话跑一轮、RPC
lib/clock.js时区日历计算(纯函数,可脱离 harness 运行)
src/client/浏览器半边源码(React + slot 注册)
lib/client.js构建产物,由 build.mjs 生成
build.mjs复刻官方 clientBundle() 的输出契约
cordis.patch.ymlbundle patch 层:dsh.bundle.patch 指向它,装上即自注册

执行链路

沿用 dsh-headless 的 run 链,并补上 Web 场景所需的两步(参考 dsh-host-apiproxy):

ctx.agents.withoutInitiator(() => ctx.agents.create({ sessionId, meta:{cwd, agentPreset}, agentOptions, setup }))
  → agent.whenIdle()
  → agent.followup(createUserMessage(...))
  → agent.whenIdle()
  → ctx.sessions.flush(agent.session)
  → workspace.attachSession(sessionId)     // 让会话出现在侧边栏
  → handle.dispose()                        // 日志已落盘,用户点开走 resume

withoutInitiator 是必须的:定时唤醒不属于任何 agent,否则会误继承当时恰好活着的那一个。

无人值守的权限

这是本插件最要紧的一处设计。

dsh 出厂的权限预设只有 read-only(ask) / workspace-write(ask) / danger-full-access(never)——没有「受限但不发问」的组合。定时任务要么卡在没人回答的审批上,要么开全权限。

所以安装步骤 3 里额外定义了一个 scheduled 预设:sandbox: workspace-write + approval: never,即限定在工作区根目录与 /tmp,且不停下来问人。

应用方式是建完 agent、发 prompt 之前调用 ctx.permissionPresets.set(agent.session, preset)——第一个工具调用就必须已经在这个界限内。会话日志里能看到两段:先是用户默认被 pin 下来,然后切到任务自己的预设。

即便如此,配了会发问的预设仍可能卡住,所以每个任务都有运行超时(默认 30 分钟),到点 agent.cancel() 并把这次记为 timeout。没有超时的话,卡住的任务会永远占着 running,之后每一次到期都被跳过。

定时

ctx.timeout 只用于「睡到下一个检查点」。超过 Node 定时器上限(约 24.8 天)的等待分段进行,每次唤醒都重读墙钟再决定是否真的到点——所以主机休眠或系统时钟被步进都不会漏触发或重复触发。

日历计算在 lib/clock.js,三种触发规则:

  • daily / weekly:给定 {hour, minute, timeZone}(weekly 另加 weekdays,0=周日)求下一个严格晚于 now 的时刻。DST 春季跳变里不存在的钟点(例如 America/New_York 的 02:30)落到跳变后的第一个瞬间;秋季重叠取较早的那次。
  • interval锚定在任务创建时刻,而不是上一次唤醒。09:02 创建、周期 15 分,就永远落在 :02/:17/:32/:47,任何一次运行拖多久都不会让相位漂移。错过的周期直接坍缩,不会堆积补跑。

previousOccurrence() 支撑「补跑」:进程重启时,若某任务上一次应触发的时刻已过且没有对应的运行记录,就补跑一次(latest-only,沿用 dsh-schedule 的先例)。默认关闭——无人值守的任务在启动时突然自己跑起来,应该是一个选择而不是一个意外。

存储

ctx.storageDomain,domain 名 scheduled_tasks,落在 $DSH_HOME/storages/scheduled_tasks.json。 注意存储层的 UNIT_NAME_RE/^[a-z][a-z0-9_]*$/ —— 下划线,不能用中划线。

前后端通信

ctx.connection.rpc,通道 /rpc-scheduled-tasksauthority: 'trusted-host'

不能用 loopback:loopback 通道的可信列表是写死的空表,只接受 loopback 的 Host 头,套在反向代理后面 Host 是域名,每个调用都会 403。trusted-host 是出厂 /api 通道用的同一道围栏——由部署方用 dsh web --trusted-host <domain> 声明自己的 authority,跨站/来源检查照旧生效。 endpoint 在 URL 路径里,body 是 {type:'client-request', rpcId, method, payload}

curl -sX POST http://127.0.0.1:3080/rpc-scheduled-tasks/list \
  -H 'content-type: application/json' \
  -d '{"type":"client-request","rpcId":"1","method":"list","payload":{}}'

endpoint:list / create / update / delete / toggle / runNow / notifyGet / notifySet / notifyTestlist 顺带返回 permissionPresets / agentPresets 目录和 failureCount,前端据此渲染下拉框与入口红点。

update 只移动表单里的六个字段(名称、指令、时/分、时区、工作目录);enabledcreatedAt 和运行记录属于任务自身的生命周期,不随编辑变化。改完必定重排定时器——编辑很可能挪动了触发时刻,旧的等待一定是过期的。

第三方插件无法注册自己的 Host→Client 推送事件(API_REMOTE_FORWARDED_EVENTS 是写死的白名单,里面没有任何 session / workspace 事件),所以面板打开时 5 秒轮询、关闭时 30 秒轮询。

对话的即时呈现:定时运行的会话完全在宿主侧创建,浏览器不会收到任何通知——不做处理的话,新会话要刷新页面才看得见。两条路径分别处理:

  • 「立即运行」runNow 端点会等到会话创建完成(而不是整轮跑完)才返回 startedSessionId,前端拿到后立刻刷新会话列表并跳进那个对话。这是用户手势,抢占当前视图是合理的。
  • 定时触发list 里带有 activeRuns(进行中的运行及其会话 id),轮询发现新条目就刷新侧边栏——对话在生成过程中就会出现在列表里,但不会抢占你正在看的界面;任务跑完的那次轮询会再刷一次。refresh() 不在 feature 包可用的公开契约上(上游把它留在具体类上),因此是探测调用而非直接依赖:将来某个版本移除它,面板照常工作,侧边栏退回到「刷新页面才更新」。运行记录也可以点击,走公开的 sessions.open(id) 直接跳过去。

通知的实现

引擎就是一次 webhook POST,各渠道只是 body 模板——把插件绑死在某家的消息 API 上,对方一改就废,而「模板 + URL」能扛过去。

插值发生在解析后的 JSON 树上,不是对模板文本做字符串替换——所以模型回复里带引号或换行也不会撑破 body。模板在保存时就要求能 JSON.parse 且顶层是对象。

「附带模型回复摘要」是三态而不是布尔:任务记录里那个字段总是被显式写入,布尔值就永远不会「缺省」,全局开关会变成死代码——早期版本正是这个 bug。存量的布尔值按「未选择」读,即跟随全局。

UI

注册在 sidebar.footer.actionkind: 'list',additive,不会顶掉任何现有占位者)。组件来自 @deepseek-ai/dsh-client-ui-primitives;该包没有 Select 和 TimePicker,所以「每天」目前是固定文案,时间用原生 <input type="time">。颜色只用 --dsw-alias-* 语义 token。

编辑表单按任务自己的时区回填(不采用当前浏览器的时区,否则从另一台机器改个提示词就会悄悄挪动触发时刻)。

开发

npm install             # 只装 esbuild(peerDependencies 由 harness 提供,别装到本地)
node build.mjs          # 重建 lib/client.js

⚠️ 装依赖务必用 npm install --omit=peer(或就是 npm install,因为 peer 都在 peerDependencies 里)。如果 @deepseek-ai/* 被装进本包的 node_modules,插件会拿到与 harness 不同的模块实例,服务注入会失效。

lib/client.js 有意提交进仓库:客户端模块注册表是直接从磁盘读这个文件的,没有它就是 404,克隆下来必须能直接用。.map 则被忽略。

客户端有 HMR:产物 hash 变化会被 host 轮询到并推给浏览器,刷新页面即可。host 半边没有 HMR(web 组合里被上游显式禁用),改 lib/index.js 要重启 dsh web

lib/clock.js 是纯函数,可以直接 node --input-type=module -e "import {nextFireAt} from './lib/clock.js'; ..." 验证。

已知限制

  • 无人值守的权限scheduled 预设不发问,但它在工作区内是可写的——定时任务会真实改文件、跑命令。要更严就选 read-only,代价是任何写操作都会卡在审批上直到超时。
  • 自动重试只有一次,且不串联:结构性失败不会变成无限重跑。
  • 通知只有一个全局目标地址和一个模板,不能按任务分发到不同的群/设备。
  • 没有通知的重发或队列:一次 POST 失败就只记录,不重试。
  • 触发规则支持每天 / 每周几 / 固定间隔,没有 cron 表达式——全仓库没有 cron 解析器,要支持得自己引库。
  • 间隔最短 5 分钟。
  • 补跑是 latest-only 且默认关闭:错过 5 次也只补 1 次。
  • 同一任务的上一次运行未结束时,新的到期会被跳过(不排队)。
  • 面板只在打开时轮询,关闭后不刷新。

License

MIT