DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

Jeff1573 /

Jeff1573/dsh-plugin-scheduled-tasks

Verified

This plugin has no description yet.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@dcdb0be9

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 + 密码)从任何设备访问,见 反向代理部署

安装

前提:已经用得上 dsh(npx @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.yaml 的 allowBuilds 下再重跑。)

手工安装(没有 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.json 的 dsh.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.yaml 的 SCHEDULED_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.credentials 的 SCHEDULED_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 的参数:name、prompt、kind(daily / weekly / interval)、hour、minute、weekdays(0=周日)、minutes(≥5)、timeZone、cwd、权限预设。

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

设计与实现

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

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

组成

文件 作用
lib/index.js Host 半边:持久化、定时、建会话跑一轮、RPC
lib/clock.js 时区日历计算(纯函数,可脱离 harness 运行)
src/client/ 浏览器半边源码(React + slot 注册)
lib/client.js 构建产物,由 build.mjs 生成
build.mjs 复刻官方 clientBundle() 的输出契约
cordis.patch.yml bundle 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-tasks,authority: '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 / notifyTest。list 顺带返回 permissionPresets / agentPresets 目录和 failureCount,前端据此渲染下拉框与入口红点。

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

第三方插件无法注册自己的 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.action(kind: '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

—/ 5

No ratings yet

Verified DSH bundle

Commit dcdb0be9ba52

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout