bark-notify
Verified@lijixu/bark-notify · v1.0.0 · MIT
DSH bundle: push Bark notifications to iPhone and Apple Watch when an agent finishes a run, needs approval, or errors out.
Install
dsh plugin add @lijixu/bark-notify Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
@lijixu/bark-notify
当前语言:简体中文 | English
DSH 插件:Agent 运行结束、需要人工授权、或运行出错时,通过 Bark 推送到 iPhone / Apple Watch。
Bark 的通知会由 iPhone 自动镜像到配对的 Apple Watch,所以只需在 iPhone 上装好 Bark App。
通知时机
| 事件 | 触发点 | 通知标题 |
|---|---|---|
| Agent 运行结束 | agent/status 从 running 落到 idle,且本轮 turn/end 原因为 completed,settleMs 后仍空闲 |
✅ 任务完成 |
| 需要授权 | approval/request(工具调用等待人工批准) |
🔔 需要授权 |
| 运行出错 | agent/error(某一轮 / 某一步出错) |
⚠️ 运行出错 |
| 运行被中止 | 本轮 turn/end 原因为 aborted,且 notifyOnAbort: true |
⏹️ 已中止 |
被中止的运行不会被当成「任务完成」。 点「停止」时 dsh-agent-loop 只会记下 turn/end { reason: { kind: 'aborted', reason: { kind: 'user' } } },不会发出 agent/error,而 agent/status 同样只是从 running 落到 idle —— 光看状态是分不出「跑完了」和「被停了」的。因此插件额外监听 session/event,读出本轮 turn/end 的原因再决定推不推:
本轮 turn/end 原因 |
插件行为 |
|---|---|
aborted(手动停止 / 上级 Agent 取消 / hook 取消 / 会话关闭) |
默认不推送;notifyOnAbort: true 时改推 ⏹️ 已中止(正文写明是谁中止的) |
error(本轮以失败收尾) |
只推 ⚠️ 运行出错,不再补一条 ✅ 任务完成 |
其他(completed、max-tokens 等正常收尾) |
推 ✅ 任务完成 |
没有 turn/end(被唤醒但无活可干) |
不推送 |
插件只做「旁观者」:授权事件始终调用 next() 交回给原有的审批 UI,不会抢占或改变授权结果。
安装
已发布到 npm,一条命令即可(也可以从源码目录或 GitHub 装,见「打包与发布」):
plugin_manager: { action: "install_bundle", target: "@lijixu/bark-notify" }
安装会在当前 profile 的 dsh.profile.bundles 末尾加入本 bundle,并写入 bark-notify 行;改动对所有会话生效,重启后依然存在。
⚠️ 改了插件代码后必须重启 DSH。 DSH 按「模块代」缓存已加载的插件:重新安装同一个 bundle 只会重新应用行与配置,不会替换已加载的 JavaScript 模块(
plugin_manager会明确返回restart-required)。
打包与发布
一个 DSH bundle 就是一个普通 npm 包,只要求 package.json 里有 dsh.bundle.patch 指向它的 patch 文件(见 package.json)。cordis.patch.yml 里的 id / name / config 就是安装后写进 profile 的那一行,plugin.js 是真正被加载的入口(main / exports)。本仓库零依赖、零转译、无构建步骤,所以「打包」= 把文件放进包里 —— npm pack 出来的 tarball 就是可分发的成品(9 个文件、约 17 kB)。
当前状态
| 项 | 值 |
|---|---|
| npm 包名 | @lijixu/bark-notify |
| GitHub 仓库 | https://github.com/xuliji/bark-notify |
| 发布可见性 | 公开(publishConfig.access = "public";scoped 包默认是私有的,免费账号发不了) |
| 三处名字 | package.json 的 name、cordis.patch.yml 的 name:、本文档的示例 —— 必须完全一致,否则 profile 里那一行会指向不存在的包 |
GitHub 用户名是
xuliji,npm 用户名是lijixu,两者不是同一个,改配置时别混。
首次发布(手动)
cd /Users/lijixu/Documents/bark-notify
npm test # 15 条 node:test 用例
npm pack --dry-run # 确认包内容:9 个文件,test/ 不会进包
npm publish # publishConfig 已声明 public,不需要再加 --access public
后续发版
npm version patch # 或 minor / major;会顺带打 git tag 并提交
git push --follow-tags
npm publish
DSH 侧升级:再跑一次 install_bundle(或在 Plugin Manager 里更新该 bundle),然后重启 DSH。
托管到 GitHub
仓库推上去(远端还没建的话,先在 GitHub 上建一个空的 bark-notify):
cd /Users/lijixu/Documents/bark-notify
git add -A
git commit -m "feat: 发布为 @lijixu/bark-notify,修复中止误报完成"
git branch -M main
git remote add origin https://github.com/xuliji/bark-notify.git # 已有 origin 则用 git remote set-url
git push -u origin main
仓库里的 .github/workflows/ci.yml 会在 push / PR 时跑 npm test 与 npm pack --dry-run(Node 20 + 22)。自动发布按你的选择没加,npm 发布保持在终端手动执行 —— 这样第一个版本不用碰 npm 的 token / 2FA bypass 新规。
安装来源(install_bundle 的 target 支持哪些写法)
target 会被 dsh-plugin-manager 先解析成 install spec,支持以下形式:
| 形式 | 例子 |
|---|---|
| npm 包名 | @lijixu/bark-notify、@lijixu/[email protected] |
绝对路径 / file: / link: |
/Users/lijixu/Documents/bark-notify |
| GitHub 简写 / git URL | github:xuliji/bark-notify、git+https://github.com/xuliji/bark-notify.git |
| 仓库 URL | https://github.com/xuliji/bark-notify |
| tarball(本地或远程) | …/lijixu-bark-notify-1.0.0.tgz、https://…/x.tgz |
(相对路径会被拒绝:Host 的工作目录和你在浏览器里输入的位置没有关系。)
三个坑
- 🔑 不要把真实 Bark Key 写进仓库里的
cordis.patch.yml。 那个文件会随 npm 包公开分发出去。仓库里的deviceKey保持空占位符,真 Key 写在 profile 的 patch 层(~/.dsh/profiles/desktop/cordis.patch.yml,不在 git 里、升级 bundle 也不会丢),它的优先级高于 bundle 层。 - 改了
plugin.js必须重启 DSH。 DSH 按「模块代」缓存已加载的插件:重新安装同一个 bundle 只会重新应用行与配置,不会替换已加载的 JavaScript 模块(plugin_manager会明确返回restart-required)。发新版本时记得 bumpversion。 - 不要把
@deepseek-ai/dsh*写进peerDependencies。 DSH 会拿这些 peer 对运行时版本做 semver 校验,不匹配就拦下安装并要求对包名@版本逐条授予版本豁免;豁免本身带风险。本插件保持零依赖,正是为了在任何 dsh 版本上都能直接安装。
配置
安装时的默认配置(本 bundle 的 cordis.patch.yml)里 deviceKey 是空占位符,必须填上你的 Bark Key 才会真正联网推送。
推荐做法是在 profile 的 patch 层(~/.dsh/profiles/desktop/cordis.patch.yml)追加一条同 id 覆盖,它会覆盖 bundle 层的配置,真 Key 不会进 git、升级本 bundle 时也不会丢:
- id: bark-notify
name: '@lijixu/bark-notify'
config:
deviceKey: '你的 Bark Key' # Bark App 首页 https://api.day.app/xxxxx 中的 xxxxx
# 其余为可选覆盖
# serverUrl: https://api.day.app
# notifyOnFinish: true
# notifyOnApproval: true
# notifyOnError: true
# notifyOnAbort: false
# includeSubagents: false
# settleMs: 1500
# timeoutMs: 10000
# group: DeepSeek Harness
# sound: ''
# level: active
# icon: ''
# url: ''
| 字段 | 默认值 | 说明 |
|---|---|---|
serverUrl |
https://api.day.app |
Bark 服务器地址;自建 bark-server 改成自己的域名 |
deviceKey |
空 | 必填,Bark 设备 Key;为空时只记录日志、不联网 |
notifyOnFinish |
true |
Agent 运行结束是否推送 |
notifyOnApproval |
true |
需要授权是否推送 |
notifyOnError |
true |
运行出错是否推送 |
notifyOnAbort |
false |
运行被中止(含手动停止)是否推送;关闭时中止不打扰,打开时改推 ⏹️ 已中止 |
includeSubagents |
false |
是否也为子 Agent(subagent)推送 |
settleMs |
1500 |
空闲后延迟多久才推送,跳过「刚结束又立刻开始」的抖动;设为 0 立即推送 |
timeoutMs |
10000 |
单次推送请求超时 |
group |
DeepSeek Harness |
Bark 通知分组 |
sound |
空 | Bark 铃声名,空为系统默认 |
level |
active |
中断级别:active / timeSensitive / passive / critical |
icon |
空 | 通知图标 URL |
url |
空 | 点击通知打开的 URL |
配置是每次推送时读取的:改完 YAML 重启 DSH 生效;插件自身不校验字段名,未知键会被忽略,字段类型不对则回退默认值。
推送内容示例
✅ 任务完成
Agent 本轮运行已结束
重构登录模块(7dbd389b)
🔔 需要授权
工具:bash
原因:在工作区外写入文件
重构登录模块(7dbd389b)
⚠️ 运行出错
第 3 轮 / 第 2 步出错
ENOENT: no such file or directory, open 'src/app.ts'
重构登录模块(7dbd389b)
⏹️ 已中止
运行已被手动中止
重构登录模块(7dbd389b)
会话标题取自可选的 sessionTitle 服务;服务缺失时退化为「会话 + 短 ID」。
实现要点(维护者必读)
- 「跑完了」和「被停了」要靠日志区分:
agent/status只有idle/running两个状态,中止和正常结束在这里完全一样。真正的原因是turn/end的reason(由dsh-agent-loop在turn()的finally里 append)。插件监听session/event(post-commit、append 期间同步回调,因此一定早于随后的idle)把原因按 Session 记进WeakMap,在agent/status落到idle时消费一次:只有本次idle真的有过turn/end才谈得上推送,所以「被唤醒但没有活可干」的空转不会误报完成。 - 子 Agent 判定必须用
header.origin === 'subagent',不能只看header.parentSession:用户手动 fork 会话时SessionController.fork()也会写parentSession(但不写origin、不写delegationDepth)。只按parentSession判断会把用户自己 fork 出来的会话当成子 Agent,默认配置下就再也收不到它的通知了。 - 授权监听必须
prepend:approval/request的瀑布链第一个监听器是 Host 启动时注册的「终止应答者」,它拿到人工决定后直接返回、不再调用next()。因此排在链尾的监听器永远不会被执行,必须ctx.on('approval/request', handler, { prepend: true })才能观察到请求,并原样调用next()把决定权交回去。 - 零依赖:profile 里安装的 bundle 是软链到本目录、按真实路径解析的,
@deepseek-ai/*这类只存在于 dsh 安装目录里的包解析不到(实测 import 失败)。所以本插件不 import 任何模块,resolveSettings()自己做默认值与校验;这也意味着它不会在 Plugin Manager 里投影出配置表单,配置请按上表写 YAML。 - Bark HTTP API v2:
POST {serverUrl}/push,JSON 体{ device_key, title, body, group?, sound?, level?, icon?, url? },返回code: 200视为成功;空字符串字段不会出现在请求体里。 - HTTP 用进程内
fetch;请求带超时,并随插件卸载一起中止。 - 监听器都注册在插件上下文里,卸载时自动注销;Agent 被 dispose 时清掉它的去抖定时器与状态,卸载时清空全部并把在途请求中止。
- 子 Agent 默认不推送,避免一次任务产生大量通知。
- 通知推送失败只记 warn 日志,不会影响 Agent 运行或授权流程。
目录结构
plugin.js # 插件实现(零依赖,ESM)
cordis.patch.yml # bundle 默认配置
locale/{en,zh}.json # Plugin Manager 里展示的名称与描述
icon.svg # 图标
test/plugin.test.js # node:test 用例(不发布)