🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

守护进程 chenkai2/dsh-daemon

DeepSeek Harnessv0.1.29
chenkai25 天前有提交已提供中文文档扫描:未发现敏感能力

dsh 守护进程:把 DeepSeek Harness web 服务(dsh web)注册成开机自启、自动恢复的后台服务(LaunchAgent / systemd / cron + 看门狗)

37.4/100
综合分
8
GitHub 星标
1,297
npm 周下载
—/5
用户评分

数据截至 2026/10/30 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add @chenkai114/dsh-daemon
npm 源 · @chenkai114/dsh-daemon 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · tool
装得上吗
本站已在 dsh 0.2.1-rc.5 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 5 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包@chenkai114/dsh-daemon @ 0.1.29
✓Node 引擎要求 >=18 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/11/1 08:20:13

安全扫描自动 · 静态分析源码
v2.7.0
🟢 扫描:未发现敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。

扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-tools@deepseek-ai/cordis
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 注册开机自启服务
  • 崩溃自动恢复
  • 后台常驻运行
v0.1.292026-10-29
· 为什么 更新逻辑一直只比较 daemon 自己的版本(isNewer + sameMajor)。一个装着旧 dsh 的客户端,会把「要求更新 dsh 才能跑的 daemon」自动装进来,然后 crash-loop 并连带把 dsh web 拖下水。0.2.x 那次是靠「同时兼容两种 shell 形状」扛过去的,不是门禁保证的。 · 兼容门禁(默认保守) · 每版在 package.json 声明支持的 dsh 区间:"dsh": { "requires": { "dsh": ">=0.1.2-alpha.1 = > <= < = 用空格 AND 组合;^ ~ || 或看不懂的写法一律按「无法判定」处理,不猜 · 三种情况都跳过自动更新并把原因写进 status:incompatible / unknown-dsh / unverified-compat · 人工覆盖:dsh-daemon update --apply(或工具的 apply:true)视为强制,会照装并在日志里写明「兼容性不满足但被强制」 · 更新失败可诊断 · 单次 fetch 10s x1 → 15s x2(间隔 2s) · 失败分类:timeout after 15s / DNS lookup failed (ENOTFOUND) / connection failed (ECONNREFUSED) / registry HTTP 5xx,不再一律 registry unreachable · status 增加 Last successful registry check · 测试 新增 test/update-gate.test.js:区间解析器 18 个边界用例(prerelease 排序、非法区间返回「无法判定」)+ 模板接线 + 真实生成一次 watchdog 并 node --check。
v0.1.282026-10-28
· 修什么 来自一次真实故障:8 核机器 load average 100+、swap 用满时 /health 偶尔要 4.6s 才回,而 watchdog 单次预算 5s、失败阈值 3 次 —— 健康的 dsh web 半小时内被杀三次;第三次之后 pid 文件落在一个已死的进程上,真正服务的是未托管实例,于是每几分钟再拉起一个注定 EADDRINUSE 的新实例。 · 单次 /health 预算 5s → 20s,失败阈值 3 → 5:约 50 秒真实超时才重启 · tick 串行 + launch() 单飞:不再同一秒拉起两个 dsh web · 收养未托管实例:pid 文件指向死进程但端口上已有健康实例时,纠正 pid 文件并记日志,不再重复拉起 · 停进程前校验身份:只对确实是 dsh web(或真占着本端口)的 PID 发信号,PID 复用不误杀(#3 问题 1) · 拒绝非 JS 入口:dsh.ps1/.cmd/pnpm sh 未解析成 lib/bin.js 时明确报错(#3 N1 兜底) · status 诚实:pid 文件与真实监听者不一致时直接点出来 · 合并 PR #2(@QiE2035):pnpm sh / .cmd shim 解析到真实 lib/bin.js,并在 pnpm store GC/重装后自愈 DSH_BIN · 新增 test/restart-loop.test.js:真实生成的 watchdog + 假 dsh web 复刻收养与慢 /health 两个现场 升级后 watchdog 会按 GEN_VERSION 自动重写;同 major 版本会自动更新,或手动 dsh-daemon update --apply 后 dsh-daemon restart。
v0.1.272026-10-16
· 变更 1. 健康检查间隔 30s → 10s watchdog 的 /health 轮询默认间隔改为 10 秒(DSH_DAEMON_HEALTH_INTERVAL 仍可覆盖,支持 ms/s/m)。 失败阈值仍是连续 3 次,因此「进程活着、超过宽限期仍未健康」的重启判定从约 90 秒缩短到约 30 秒; 睡眠唤醒判定(90 秒 tick gap)与最小重启间隔(15 秒)都大于 tick,不会误判也不会热重启。 2. 启动失败可查明细:dsh-daemon logs dsh web 没起来时,现在像 systemctl 一样直接告诉你去哪看明细: · 新增 dsh-daemon logs [-n N] [--web | --watchdog](watchdog 的 --logs 模式,bash/cmd 包装脚本同步生成): 默认打印 dsh-web.log 与 watchdog.log 各 50 行,先给诊断头(/health 状态、托管 PID 是否存活、 本次启动多久、是否处于暂停状态),末尾给出 tail -f 实时跟踪命令; · 所有失败路径都打印入口:dsh-daemon start / restart 失败、status 不健康、 spawn 失败、进程消失重启、失败阈值重启,均输出 Details: dsh-daemon logs -n 100 (raw log: …/dsh-web.log); 生成的 watchdog 自身日志行也带 (details: dsh-daemon logs -n 100),只看 watchdog.log 也能找到出口; · 新增工具 dsh_daemon_logs(参数 lines、source),沙箱/无终端场景拿到同样内容。 · 测试 · 新增 test/logs-command.test.js:用真实生成的 watchdog + 秒退的假 dsh 覆盖 10s 默认 tick 固化、失败提示、--logs 两种筛选与未知参数、CLI 转发、工具输出(26 项断言); 该用例以动态沙箱方式加载插件,不依赖 @deepseek-ai/dsh-tools,发布流水线零额外依赖; · npm test 与 npm run test:ci 全绿(version-gate / token-auth / registration-drift / startup-grace / logs-command)。 · 升级 自动更新(默认 6 小时一次)会把包升到 0.1.27,插件下次启动时按 GEN_VERSION 自动重新生成 watchdog 与 CLI;要立刻生效可运行一次 dsh_daemon_reinstall(仅重启 watchdog,不动 dsh web)。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
版本对照(dsh-daemon ↔ dsh) | dsh-daemon | 支撑的 dsh | 说明 | | 0.1.29 | 0.1.x 与 0.2.x | 自动更新加 dsh 兼容门禁:每版在 package.json 声明 dsh.requires.dsh,更新前比对客户端已装 dsh 版本,不满足/未声明/读不到就跳过自动升级(dsh-daemon update --apply 可人工强制);更新检查改为 2 次尝试 + 15s 预算,错误细分为 timeout / DNS / HTTP / 连接失败,status 显示上次成功检查时间 | | 0.1.28 | 0.1.x 与 0.2.x | 修「健康检查误杀 + 重启风暴」:/health 单次预算 5s → 20s、失败阈值 3 → 5(~50s 真超时才重启);tick 串行(tickBusy)+ launch() 单飞,杜绝同一秒拉起两个 dsh web(EADDRINUSE);收养未托管但正在服务的 dsh web(纠正 pid 文件)而不是再拉一个;停进程前校验「这个 PID 真的是 dsh web」(PID 复用不误杀);status 报出 pid 文件与实际监听者不一致;非 JS 入口(dsh.ps1/.cmd shim)拒绝启动并明确报错 |
展开完整文档(README 剩余 1 节)
版本对照(dsh-daemon ↔ dsh)

| dsh-daemon | 支撑的 dsh | 说明 |
| 0.1.29 | 0.1.x 与 0.2.x | 自动更新加 dsh 兼容门禁:每版在 package.json 声明 dsh.requires.dsh,更新前比对客户端已装 dsh 版本,不满足/未声明/读不到就跳过自动升级(dsh-daemon update --apply 可人工强制);更新检查改为 2 次尝试 + 15s 预算,错误细分为 timeout / DNS / HTTP / 连接失败,status 显示上次成功检查时间 |
| 0.1.28 | 0.1.x 与 0.2.x | 修「健康检查误杀 + 重启风暴」:/health 单次预算 5s → 20s、失败阈值 3 → 5(~50s 真超时才重启);tick 串行(tickBusy)+ launch() 单飞,杜绝同一秒拉起两个 dsh web(EADDRINUSE);收养未托管但正在服务的 dsh web(纠正 pid 文件)而不是再拉一个;停进程前校验「这个 PID 真的是 dsh web」(PID 复用不误杀);status 报出 pid 文件与实际监听者不一致;非 JS 入口(dsh.ps1/.cmd shim)拒绝启动并明确报错 |
| 0.1.27 | 0.1.x 与 0.2.x | 健康检查间隔默认 30s → 10s(自愈更快);新增 dsh-daemon logs 与 dsh_daemon_logs:dsh web 启动失败时像 systemctl 一样打印明细入口(web + watchdog 日志尾部、/health、PID、启动时长) |
| 0.1.26 | 0.1.x 与 0.2.x | 启动宽限期:dsh web 启动慢(大插件集、冷缓存)时不再被 watchdog 按 90 秒判死反复重启,宽限期内不计健康失败(DSH_DAEMON_STARTUP_GRACE,默认 5 分钟);停旧进程改为先 SIGTERM 再 SIGKILL 并等端口真正释放(修 EADDRINUSE 连锁失败);status / start 如实报「仍在启动」 |
| 0.1.25 | 0.1.x 与 0.2.x | 适配 0.2.x 的 shell 服务改名:run(spec) → execute(spec) + 句柄 result()(0.2.1 上 reinstall 曾报 shell.run is not a function),并兼容两种结果形状 |
| 0.1.24 | 0.1.x(0.2.x 部分路径不可用) | OIDC 发布流水线;未适配 0.2.x shell API |
| 0.1.23 | ≥ 0.1.2-alpha.1(token 认证起) | 注册漂移自愈、status 如实上报注册状态、拒绝易失 DSH_HOME |
| 0.1.21–0.1.22 | 同上 | 单实例守卫校验 cmdline、dsh web 显式传 cwd |
| 0.1.20 | 同上 | DSH_DAEMON_TRUSTED_HOST:反代场景下给 dsh web 追 --trusted-host |
| 0.1.19 | 同上 | 自动开浏览器补齐 token 认证的入场 cookie |
| ≤ 0.1.18 | ≥ 0.1.0-rc.8(--no-open 起) | 更早的 dsh 没有 --no-open,watchdog 启动时会跳过该参数 |

两个门限常量取自代码(lib/index.js):DSH_NO_OPEN_MIN = 0.1.0-rc.8、DSH_TOKEN_AUTH_MIN = 0.1.2-alpha.1。
dsh 侧与 daemon 交互的接口只有三处:web 启动参数、/health 探活、以及注入的 shell/fs 服务 —— 后者是 0.2.x 唯一改过的地方。

dsh-daemon

| English

将 DeepSeek Harness 网络服务(dsh web)注册为自启动、自愈的后台服务。

安装后,dsh web 将:

- 登录时自动启动(LaunchAgent RunAtLoad / systemd WantedBy=default.target / cron @reboot)
- 睡眠唤醒后自动重启
- 自愈:watchdog 每 10 秒(可配置)健康检查一次 http://127.0.0.1:<port>/health,连续 3 次失败后重启服务
- 慢启动安全:刚拉起的 dsh web 处在启动宽限期(DSH_DAEMON_STARTUP_GRACE,默认 5 分钟)内时,健康检查失败不计入重启阈值,进程也不会被杀
- 不依赖当前会话:watchdog 是独立生成的脚本,而非内存中的插件

安装/卸载永远不会触碰当前正在运行的会话。

账号对应关系(npm scope / GitHub 账号)见 CONTEXT.md。

使用方式

方式 A —— 用 dsh plugin 安装(v0.1.9+,推荐)

1. 用官方插件管理器将包安装到 web profile(在 profile 目录内运行 pnpm,使 loader 能解析到它;仅全局安装不够——见下):

dsh plugin --profile web add @chenkai114/dsh-daemon

(需要 PATH 上有 pnpm——用 corepack enable 一次性启用。)

为什么不能只 npm install -g?loader 以 Node ESM 解析导入 name: 行,解析锚点是 profile 目录(~/.dsh/profiles/web/);全局 node_modules 不在该解析链上(NODE_PATH 对 ESM 无效)。profile 自己的 node_modules——由 pnpm 管理——才是包可达的原因。
2. 重启 dsh web。包声明了 dsh.bundle manifest,dsh plugin add 会把它自动加入 dsh.profile.bundles,启动时作为 bundle 层挂载——不需要(也不应该)再手动往 ~/.dsh/profiles/web/cordis.patch.yml 里 insert 同一行,否则会触发 duplicate loader entry id: dsh-daemon 启动失败。

七个 dsh_daemon_ 工具即可供每个智能体使用——直接让智能体运行 dsh_daemon_install。

以后升级:dsh plugin --profile web update @chenkai114/dsh-daemon(并重启)。

⚠️ 从 v0.1.8 及更早版本升级:如果你之前按旧文档在 ~/.dsh/profiles/web/cordis.patch.yml 里手动加过 - insert: dsh-daemon 行,升级后必须删掉那一行(保留文件里其他内容),否则 bundle 层 + 手动层会插入同一个 id: dsh-daemon 两次,dsh web 启动时报 duplicate loader entry id。删除后重启即可。

权限说明:daemon 管理用户级系统服务(LaunchAgent plist、$DSH_HOME 下的状态文件),因此插件对其文件与命令操作请求 danger-full-access。若部署拒绝提权,工具会以沙箱拒绝失败。

方式 B —— 动态 Cordis 插件(无需安装)

将 lib/index.js 的内容粘贴到 cordis_define 的 code.host 字段并运行。这正是插件在真实会话中开发与验证的方式:沙箱提供 harness 全局,文件以 return plugin; 结尾。

端口

默认端口为当前监听的 webServer 端口(通常 3080),其次 DSH_WEB_PORT,再其次工具显式 port 参数。更换端口后运行 dsh_daemon_reinstall。

架构

守护进程是一个 watchdog 监督者,由三部分组成。

1. 平台注册

一个按用户注册的服务,负责在登录时启动 watchdog 并保持其存活:

| 平台 | 机制 |
| macOS | LaunchAgent ~/Library/LaunchAgents/com.deepseek-ai.dsh-watchdog.plist —— ProgramArguments=[node, watchdog.js]、RunAtLoad、KeepAlive{SuccessfulExit:false}、ThrottleInterval=10,环境变量携带 DSH_WEB_PORT 与 DSH_HOME。通过 launchctl load -w 加载。 |
| Linux | systemd 用户单元 ~/.config/systemd/user/dsh-watchdog.service —— Type=simple、Restart=always、RestartSec=10、StartLimitIntervalSec=0;通过 systemctl --user enable --now 启用。systemd 不可用时回退为 cron @reboot 条目。 |
| Windows | VBS 启动器 + 计划任务 —— 任务 DshWatchdog(XML 在 $DSH_HOME/daemon/dsh-watchdog-task.xml,UTF-16LE)在登录时运行 wscript.exe //B dsh-watchdog.vbs;VBS 设置 DSH_WEB_PORT/DSH_HOME 并以隐藏窗口启动 node watchdog.js。RestartOnFailure PT1M/999,MultipleInstancesPolicy=IgnoreNew。通过 schtasks /Create 注册。 |

Windows 支持按 macOS/Linux 的同等行为实现(插件的 shell 层在 win32 下切换到 PowerShell,即 DSH 的 shell 执行器),但尚未在真实 Windows 机器上验证。

2. watchdog 循环

生成的独立脚本 $DSH_HOME/daemon/watchdog.js(零依赖,任意 Node ≥ 18 可运行,无需会话):

- 将自身 PID 写入 .dsh-watchdog.pid;SIGINT / SIGTERM / SIGHUP 清理后退出;单实例锁拒绝重复的 watchdog
- 启动时若 http://127.0.0.1:<port>/health 不健康,则拉起 web 服务(node <dsh> web --port <port>,分离运行,输出到 logs/dsh-web.log)
- 之后每 10 秒(可通过 DSH_DAEMON_HEALTH_INTERVAL 配置):
- 存在 .daemon-stopped(用户暂停监控)或 .daemon-restart.lock 较新(< 120 秒,重启进行中)时跳过;
- tick 间隔超过 90 秒(睡眠唤醒)时重启服务;
- 服务仍在启动宽限期内(.dsh-web.started 距今 < DSH_DAEMON_STARTUP_GRACE,默认 5 分钟)且进程存活时,只记录「仍在启动」,不计失败、不重启——大插件集/冷缓存的 dsh web 可能几十秒到几分钟才对外提供 /health,旧的「90 秒判死」会让它永远起不来;
- 进程已消失(崩溃、激活期 EADDRINUSE、OOM)时立刻重启,不等满 3 次失败;
- 连续 3 次健康检查失败后重启服务(进程存活但超过宽限期仍未健康);
- 停旧进程先发 SIGTERM,等待最多 8 秒(dsh web 会优雅退出并释放监听端口),仍不退再 SIGKILL;随后等端口真正空闲(最多 5 秒)才拉起新进程,避免新进程在插件激活期报 EADDRINUSE 而秒退;
- .daemon-installed 标记消失(已卸载)时退出;
- 日志写入 logs/watchdog.log(5 MB × 3 轮转)。

3. daemon 感知的 start / stop

- dsh_daemon_stop 写入 .daemon-stopped(watchdog 不再重启服务),并停止正在运行的托管服务。
- dsh_daemon_start 清除该标记、确保 watchdog 运行,并在服务不健康时拉起它。

工具

插件仅包含 Host 侧,注册 8 个可供模型调用的工具:

| 工具 | 功能 |
| dsh_daemon_install | 生成 watchdog.js 与状态文件,写入 LaunchAgent plist(或 systemd 单元 / cron 条目、Windows 的 VBS + 计划任务),立即启动 watchdog。可选 port 参数。 |
| dsh_daemon_uninstall | 停止 watchdog,卸载并删除平台注册,清除全部状态文件。 |
| dsh_daemon_reinstall | 先卸载再安装(升级 dsh 或更换端口后使用;同时按当前自动更新配置重新生成 watchdog)。 |
| dsh_daemon_status | 安装时间、端口、本地/最新版本、更新状态、watchdog PID/存活、手动停止标记、服务健康、最近日志。服务不健康时额外打印 Details: 行指向 dsh_daemon_logs。 |
| dsh_daemon_logs | dsh web 启动失败的明细入口:web 服务 stdout/stderr 日志尾部 + watchdog 日志尾部,附当前 /health、web PID 与本次启动时长。可选 lines(默认 50)与 source(all/web/watchdog)。终端等价命令 dsh-daemon logs。 |
| dsh_daemon_start | 清除停止标记,确保 watchdog 运行,服务不健康时拉起。 |
| dsh_daemon_stop | 写入停止标记(watchdog 不再重启),停止托管服务(若有)。绝不触碰当前会话。 |
| dsh_daemon_update | 检查新版本(默认 apply: false)或下载并应用(apply: true)。也是大版本变更的人工入口。 |

命令行(dsh-daemon)

dsh_daemon_install 还会在 node 的 bin 目录(PATH 内)生成一个轻量 dsh-daemon 命令,无需打开 GUI 即可在终端控制 daemon:

| 命令 | 功能 |
| dsh-daemon status | 与 GUI 工具相同的状态。 |
| dsh-daemon restart | 立即重启 dsh web(杀掉端口上的进程并拉起新进程,无需等待健康循环),返回前已验证健康。 |
| dsh-daemon start | 清除停止标记;watchdog 缺失时启动;web 不健康时拉起。 |
| dsh-daemon stop | 写入停止标记并杀掉 web 服务(包括手动启动的)。 |
| dsh-daemon logs | 查看明细:web 日志与 watchdog 日志尾部(-n 100 指定行数,--web / --watchdog 只看一个)。dsh web 启动失败时用这个命令。 |
| dsh-daemon update | 检查 registry(加 --apply 下载并应用)。 |
| dsh-daemon install / uninstall / reinstall | 注册类操作,由插件通过其 /dsh-daemon/command 路由执行——需要 dsh web 处于运行状态(上面的监督类命令通过 watchdog 脚本独立工作)。 |
| dsh-daemon help | 用法说明。 |

restart/stop 会中断所有打开的会话,与手动 pkill 效果相同——若直接拉起失败,watchdog 会在下一个健康周期重新拉起 web 服务。

启动失败的明细入口

像 systemctl 在服务启动失败时打印「用 journalctl 看明细」一样,daemon 的每条失败信息都指向同一个命令:

$ dsh-daemon restart
dsh-daemon: restarting dsh web on port 3080...
launched dsh web (PID 51234)
❌ dsh web exited during startup — last log lines:
| Error: cannot find module @acme/plugin-x
| plugin activation failed for row "plugin-x"
Details: dsh-daemon logs -n 100 (raw log: ~/.dsh/daemon/logs/dsh-web.log)

$ dsh-daemon logs -n 50
dsh-daemon logs — dsh web startup diagnostics port 3080 | /health ❌ not serving | web pid 51234 (not running) | last launch 3s ago web log: ~/.dsh/daemon/logs/dsh-web.log (272 bytes, updated ...) watchdog log: ~/.dsh/daemon/logs/watchdog.log (975 bytes, updated ...)

--- dsh-web.log (last 50 lines, ~/.dsh/daemon/logs/dsh-web.log) ---
| Error: cannot find module @acme/plugin-x
| plugin activation failed for row "plugin-x"

--- watchdog.log (last 50 lines, ~/.dsh/daemon/logs/watchdog.log) ---
| [2026-10-16T12:23:42.653Z] [watchdog] web server not healthy on port 3080 at startup, launching... (details: dsh-daemon logs -n 100)

- dsh-daemon logs 默认打印两个日志各 50 行,末尾给出 tail -f 实时跟踪命令;
- 第一次诊断时,先回答三个关键问题:/health 是否在服务、托管的 web PID 是否存活、本次启动多久(还在启动宽限期内就不算失败);
- 沙箱/无终端场景可用工具 dsh_daemon_logs(参数 lines、source)拿到同样的内容;
- 日志文件位置固定:$DSH_HOME/daemon/logs/dsh-web.log(web 的 stdout/stderr,启动崩溃的原因就在这里)与 $DSH_HOME/daemon/logs/watchdog.log(watchdog 的决策过程)。

状态文件($DSH_HOME/daemon/,$DSH_HOME 默认为 ~/.dsh)

daemon/
├── watchdog.js # 生成的 watchdog 脚本(独立、零依赖) ├── .daemon-installed # 安装时间戳标记 ├── .daemon-port # 被监督的端口 ├── .daemon-stopped # 暂停标记:watchdog 不再重启服务 ├── .daemon-restart.lock # 重启进行中标记(TTL 120 秒) ├── .dsh-watchdog.pid # watchdog PID ├── .dsh-web.pid # 托管 web 服务 PID ├── .daemon-update.lock # 更新进行中锁(并发保护) ├── .daemon-update-pending # 已下载待重启生效的更新 ├── .daemon-update-check.json # 最近一次更新检查结果(状态显示用) ├── dsh-watchdog.vbs # Windows:隐藏的 wscript 启动器 ├── dsh-watchdog-task.xml # Windows:计划任务 XML(UTF-16LE) └── logs/ ├── watchdog.log # watchdog 日志(5 MB × 3 轮转) └── dsh-web.log # watchdog 拉起的 web 服务输出

自动更新

watchdog 启动时及每 6 小时检查 npm registry,并用 pnpm 更新 profile 目录中的 @chenkai114/dsh-daemon:

- 版本策略:同 major 版本(0.1.3 → 0.1.4、0.2.x → 0.2.y)自动更新;major 变更(0.x → 1.x、1.x → 2.x、…)仅提示,需人工执行 dsh_daemon_update 工具。
- dsh 兼容门禁:除 daemon 自身版本外,还要求新版本声明的 dsh.requires.dsh 与本机已装 dsh 版本相符;不满足、未声明或读不到本机 dsh 版本时只提示、不自动安装(dsh-daemon update --apply 可强制)。
- 更新模式(DSH_DAEMON_UPDATE_MODE):
- restart(默认):下载完成后,watchdog 每 30 秒轮询插件的 /dsh-daemon/activity 端点(进行中的回合 + 后台任务),仅在安静窗口后重启 dsh web——进行中的对话或任务会推迟重启直到结束。端点不可达(插件未挂载)时,仍会在 DSH_DAEMON_DEFER_MAX 之后重启。完全无人值守。
- download:新包安装到 profile 并写入待生效标记;下次自然重启 dsh web 时生效。绝不中断任何会话,把「何时生效」的控制权留给用户。
- 失败安全:registry 不可达、pnpm 失败或更新后版本不一致只会写日志行与检查状态;旧包保持安装(pnpm store 保留旧版本,dsh plugin --profile web add @chenkai114/dsh-daemon@<旧版> 可回滚)。

配置在 dsh_daemon_install/reinstall 时捕获并嵌入生成的 watchdog 脚本:

| 环境变量 | 默认值 | 含义 |
| DSH_DAEMON_AUTO_UPDATE | 1 | 0 关闭检查 |
| DSH_DAEMON_UPDATE_INTERVAL | 6h | 检查间隔(ms/s/m/h/d) |
| DSH_DAEMON_UPDATE_MODE | restart | restart 或 download |
| DSH_DAEMON_QUIET_WINDOW | 5m | restart 模式重启前所需的安静时间 |
| DSH_DAEMON_DEFER_MAX | 15m | 活动端点不可达时最多等待多久再重启 |
| DSH_DAEMON_NPM_REGISTRY | https://registry.npmjs.org | 检查与 pnpm 更新所用的 registry |
| DSH_DAEMON_PROFILE | web | 存放插件的 profile 目录 |
| DSH_DAEMON_HEALTH_INTERVAL | 10s | watchdog 循环的健康检查间隔(ms/s/m;宽限期外连续 3 次失败触发重启,默认 tick 下约 30 秒) |
| DSH_DAEMON_STARTUP_GRACE | 5m | dsh web 启动宽限期(ms/s/m):这段时间内的健康检查失败不计入重启阈值,也不会杀进程。插件多、机器冷启动慢时保持默认;确定自己的 dsh web 十几秒就能起来时可以调小(如 90s)以更快发现真正的卡死 |
| DSH_DAEMON_OPEN_BROWSER | 1 | 0 时即使检测到新版 dsh 的启动 token 也不自动弹浏览器(URL 仍写入 ~/.dsh/daemon/.web-auth-url 与 watchdog 日志,可人工访问) |
| DSH_DAEMON_CLI_DIR | node bin 目录 | 生成的 dsh-daemon CLI 写入目录(测试/沙箱安装时指向临时目录,避免污染真实 PATH) |
| DSH_DAEMON_NO_SYSTEM | 未设置 | 1 时跳过系统级注册(launchd/schtasks/systemd)——测试/沙箱安装不触碰宿主系统服务,watchdog 仍直接启动 |
| DSH_DAEMON_TRUSTED_HOST | 未设置 | 反向代理场景:安装/重装时把 Host(可逗号分隔)固化进生成的 watchdog,dsh web 启动时自动追加 --trusted-host;不设置而由反向代理进来时,dsh 的 /api 会 403。之所以固化进脚本,是因为插件自启的 watchdog 拿不到 plist/unit 的环境变量 |
| DSH_DAEMON_ALLOW_EPHEMERAL_HOME | 未设置 | 设为 1 时,允许把注册装到临时 DSH_HOME(/tmp、/var/folders)上;默认拒绝,改用 ~/.dsh,避免临时目录被清理后 LaunchAgent/systemd 注册永久失效(DSH_DAEMON_NO_SYSTEM=1 的测试不受影响) |
| DSH_DAEMON_TRUSTED_HOST | 未设置 | 逗号分隔的 --trusted-host 列表(如 dsh.example.com,10.0.0.5:8080)。经反向代理(nginx)访问时,Host 头是公网域名,/api 的信任围栏会返回 403——设此项让 dsh web 信任这些 Host |

自动更新逻辑位于生成的 watchdog.js 中;升级到含新更新逻辑的版本后,运行一次 dsh_daemon_reinstall 重新生成。

验证记录

以下全部针对真实插件代码端到端验证过:

- 安装 → plutil -lint 通过,launchctl list 显示该 agent,watchdog 日志 watchdog started (PID …, port 3080) / web server already healthy on port 3080
- 在空端口上,watchdog 启动时拉起真实 dsh web --port <port>(新端口健康 OK)
- 自愈:SIGKILL 托管服务后 → health check failed (1/3 → 2/3 → 3/3) → failure threshold reached, restarting web server → 新进程返回 200
- 启动宽限期(node test/startup-grace.test.js,真实生成的 watchdog + 假 dsh):8 秒才健康的慢启动在 20 秒宽限内只记录「仍在启动」,不重启也不杀进程;宽限期结束(2 秒)后先 SIGTERM 再拉起新进程,每次重启都有对应的优雅停止(端口先释放)
- 启动失败可诊断(node test/logs-command.test.js,真实生成的 watchdog + 秒退的假 dsh):默认 tick 固化为 INTERVAL_MS = 10000;--start 失败时打印 Details: dsh-daemon logs -n 100;dsh-daemon logs / --logs / dsh_daemon_logs 三条入口都能看到 dsh web 的崩溃原因与 watchdog 决策日志
- launchd KeepAlive:SIGKILL watchdog 后约 11 秒内被 launchd 重启
- 单实例保护:重复运行 watchdog.js 立即退出
- stop 写入暂停标记并只杀托管服务;start 清除;uninstall 移除 launchd 注册、plist、状态文件并释放端口;status 反映全部状态

本地测试

node test/harness.js dsh_daemon_status # 静态包模式
DYNAMIC=1 node test/harness.js dsh_daemon_status # 动态沙箱模式
node test/startup-grace.test.js # 启动宽限期:慢启动不被杀、超时才重启、优雅停止
node test/logs-command.test.js # 10s tick 默认值 + dsh-daemon logs / 失败明细入口
node test/restart-loop.test.js # 收养未托管实例 + 慢 /health 不再误杀
node test/update-gate.test.js # dsh 兼容区间门禁 + 更新失败分类

测试驱动运行真实插件代码(真实 bash/fs),并真实调用工具。

v0.1.29 — 自动更新加 dsh 兼容门禁,更新失败可诊断

背景:更新逻辑一直只比较 daemon 自己的版本(isNewer + sameMajor)。
一个装着旧 dsh 的客户端,会把「要求更新 dsh 才能跑的 daemon」自动装进来,然后 crash-loop 并连带把 dsh web 拖下水——之前 0.2.x 那次是靠「同时兼容两种 shell 形状」侥幸扛过去的,不是门禁保证的。

兼容门禁(默认保守,宁可不动):

- 每个发布版本在 package.json 声明自己支持的 dsh 区间:
"dsh": { "requires": { "dsh": ">=0.1.2-alpha.1 <0.3.0" } };
- 更新检查直接读 registry /latest 返回的 manifest 里的这个字段,与本机已装
dsh 版本比对(区间语法只支持 >= > <= < = 用空格 AND 组合;不支持 ^ ~ ||,
遇到看不懂的区间一律按「无法判定」处理,不猜);
- 三种情况都跳过自动更新并把原因写进 status: incompatible(区间不满足,显示要求版本与当前版本)、 unknown-dsh(读不到本机 dsh 版本)、 unverified-compat(新版本没声明区间);
- 人工覆盖:dsh-daemon update --apply / dsh_daemon_update 的 apply:true 视为强制,会照装并在日志里写明「兼容性不满足但被强制」。

错误细分 + 重试:单次 fetch 从 10s 一次改为 15s × 2 次(间隔 2s),失败按 timeout after 15s / DNS lookup failed (ENOTFOUND) / connection failed (ECONNREFUSED) / registry HTTP 5xx 分别记录, status 里额外显示 Last successful registry check。这修的是之前「离线、DNS 抖动、5xx 全都显示 registry unreachable」的问题。

新增 test/update-gate.test.js:区间解析器 18 个边界用例(含 prerelease 排序、非法区间返回「无法判定」)+ 模板接线 + 真实生成一次 watchdog 并 node --check。

v0.1.28 — 健康检查不再误杀,重启风暴收口

一次真实故障(8 核机器 load average 100+、swap 用满):/health 偶尔要 4.6 秒才回,watchdog 的单次预算是 5 秒、失败阈值 3 次 —— 于是健康的 dsh web 在半小时内被杀了三次;第三次之后 pid 文件落在一个已经死掉的进程上,真正在服务的是另一个未托管实例,watchdog 既认不出它、也管不到它,只能每隔几分钟再拉起一个注定 EADDRINUSE 的新实例,把本已紧张的 CPU 再踩一脚。

- 单次 /health 预算 5s → 20s,失败阈值 3 → 5:约 50 秒真实超时才重启,瞬时抖动与 GC/换页停顿不再被判死(DSH_DAEMON_HEALTH_INTERVAL 不受影响)。
- tick 串行:setInterval 的异步体可以重叠,探测变慢后两轮同时看到「进程没了」就各自拉起一次 —— 这正是同一秒出现两个 dsh web 的来源。现在用 tickBusy 互斥,探测再慢也不会叠加。
- launch() 单飞:tick、CLI、更新器同时要求启动时共享同一个 in-flight promise,不会再出现「赢家服务、输家被记进 pid 文件」。
- 收养未托管实例:pid 文件指向死进程、而端口上已有健康的 dsh web 时,把 pid 文件纠正为真实监听者并记日志(adopted the running dsh web),不再拉起第二个;端口被非 dsh 进程占用时明确记录并拒绝盲拉。
- 停进程前校验身份:只对「确实是 dsh web」或「真的占着本端口的」PID 发信号,PID 复用不再误杀无关进程(issue #3 问题 1)。
- status 诚实:pid 文件与实际监听者不一致时直接点出来,并给出 dsh-daemon restart 的修法。
- 拒绝非 JS 入口:dsh.ps1/dsh.cmd/pnpm sh 之类的 shim 若没解析成 lib/bin.js,直接报错而不是让 node 去解析 PowerShell(issue #3 N1 的兜底),不再产出「日志显示启动成功、实际永远起不来」的循环。
- 合并 PR #2(@QiE2035):pnpm sh / .cmd shim 解析到真实 lib/bin.js,并在 pnpm store GC / 重装后通过 shim 自愈 DSH_BIN。

新增 test/restart-loop.test.js:用真实生成的 watchdog + 假 dsh web 复刻两个现场 —— ①pid 文件指向死进程时收养真实监听者且不重启;②单次 /health 慢到 6 秒仍不触发重启。

v0.1.27 — 健康检查 10s,启动失败可查明细(dsh-daemon logs)

更快的自愈:watchdog 的健康检查默认间隔从 30 秒改为 10 秒(DSH_DAEMON_HEALTH_INTERVAL,仍可用 ms/s/m 覆盖)。失败阈值仍是连续 3 次,所以「进程活着但超过宽限期仍未健康」的重启判定从约 90 秒缩短到约 30 秒;睡眠唤醒判定(90 秒 tick gap)与最小重启间隔(15 秒)保持大于 tick,不会误判也不会热重启。

启动失败有明细入口:dsh web 没起来时,此前只能从 ~/.dsh/daemon/logs/ 里自己找文件。现在:

- 新增 dsh-daemon logs [-n N] [--web | --watchdog](watchdog 的 --logs 模式,bash/cmd 包装脚本同步生成):默认打印 web 与 watchdog 两个日志各 50 行,先给一段诊断头(/health 状态、托管 PID 是否存活、本次启动多久、是否处于暂停),末尾给出 tail -f 实时跟踪命令;
- 所有失败路径都像 systemctl 一样打印入口:dsh-daemon start / restart 失败时输出 Details: dsh-daemon logs -n 100 (raw log: …/dsh-web.log),status 不健康时同样打印,生成的 watchdog 写日志时也带上 (details: dsh-daemon logs -n 100),因此只看 watchdog.log 也能找到出口;
- 新增工具 dsh_daemon_logs(参数 lines、source),沙箱/无终端场景拿到同样的内容;
- 回归测试 test/logs-command.test.js 用真实生成的 watchdog + 秒退的假 dsh 覆盖:10s 默认 tick、失败提示、--logs 两种筛选、未知参数、CLI 转发与工具输出。

生成物变更需要重新生成:升级后 watchdog 会按 GEN_VERSION 自动重写;若版本未变,运行一次 dsh_daemon_reinstall。

v0.1.19 — dsh web token 授权适配(?token= 启动令牌种子)

dsh ≥ 0.1.2-alpha.1(harness commit 3e24087bfa)起 dsh web 启动时生成进程内随机 token 并打印 dsh web: http://127.0.0.1:<port>/?token=...:浏览器访问该 URL 一次后种下 30 天有效的 host-only Cookie(签名密钥持久化,跨重启有效),直接访问 3080 无 Cookie → 401。此前 watchdog 用 --no-open 拉起,用户浏览器从未种过 Cookie,daemon 托管的 web 直接 401。

适配:watchdog 拉起 web 后从 dsh-web.log(web stdout 的重定向目标)提取本次运行的 token URL,然后:
- 检测到 ?token=(新版 dsh)→ 打开默认浏览器访问一次,种下 Cookie(与手动执行 dsh web 行为一致);无 GUI 环境打开失败时,URL 已落盘,可人工访问;
- 未检测到(旧版 dsh)→ 保持现状(--no-open,绝不弹浏览器);
- token URL 始终写入 ~/.dsh/daemon/.web-auth-url(0600,每次 launch 覆盖),dsh-daemon status 会显示当前有效 URL;
- DSH_DAEMON_OPEN_BROWSER=0 关闭自动弹窗(仅落盘 + 日志);
- 提取锚定 launch 前的文件偏移:POSIX 追加日志不会误取旧 run 的失效 token(永远取本次运行新增段里最后一条 dsh web: 行;win32 Start-Process 覆盖语义下自动回退读整文件,dsh-web.log.1 是上一进程的过期 URL,从不读取);
- 探测跨版本稳定:dsh web: http://... 这行自最老版本就打印,新旧唯一差异是 URL 是否带 ?token=,故以 ?token= 有无为判据,对未来版本成立;
- 反向保险:新版 dsh 的 token 行迟迟不出现时继续等待重试(快轮询 15s@250ms 后接慢轮询 75s@5s),绝不按「无 token」当旧版跳过;超时仅告警,下次 launch 重试;
- 弹窗节流:token 每次启动必变,但 30 天 Cookie 跨重启有效(签名密钥持久),因此本次 URL 与上次记录相同时不再弹(.web-auth-url 仍刷新供人工访问);SSH_CONNECTION/SSH_TTY 非空时抑制弹窗(远程会话不弹别人桌面),只落盘+日志;
- 版本门控照搬 --no-open 模式(DSH_TOKEN_AUTH_MIN = 0.1.2-alpha.1,模块级函数经 toString() 内联进 watchdog,每次 launch 运行时重判),仅在确定旧版时跳过轮询;URL 行探测是主判据,与版本无关地可靠。

v0.1.18 — Windows 黑框修复:隐藏控制台而非无控制台;start 等待健康

Windows 上 watchdog 用 CP.spawn(..., { detached: true }) 拉起 dsh web、pnpm、netstat 等子进程时,Node 默认给 detached 子进程分配独立控制台窗口(watchdog 本身由 VBS/计划任务隐藏启动、无控制台),于是每次拉起/重启都会闪出黑框。v0.1.17 的 --no-open 修复让重启循环消失后,黑框成了最显眼的问题。

机制选择(deepseek-harness discussion #1564 / #810):不能给 dsh web 用 windowsHide(CREATE_NO_WINDOW)——无控制台的宿主会让它每次 spawn 的子进程都新建一个可见控制台,而且 CREATE_NO_WINDOW 会让 Windows ACL 沙箱的受限令牌子进程直接 0xC0000142(DLL 初始化失败)。正确做法是给 dsh web 一个隐藏的控制台(STARTF_USESHOWWINDOW + SW_HIDE,保持 dwCreationFlags=0,Windows 上经 Start-Process -WindowStyle Hidden 实现,与 dsh-daemon start 直启路径一致):dsh web 自身无可见窗口,它的控制台子进程又继承这个隐藏控制台,层层都不再闪框。

- win32 上 watchdog 改经 powershell.exe -Command "Start-Process -FilePath <node> -ArgumentList ... -WindowStyle Hidden -RedirectStandardOutput <web.log> -PassThru" 拉起 dsh web(wrapper 本身 windowsHide,短命且普通令牌,安全),PID 由 wrapper 写入 pidfile、watchdog 轮询确认; ⚠️ wrapper 不能带 detached: true——Node 在 Windows 上把它映射成 DETACHED_PROCESS,会让 Start-Process 整条命令卡死(PID 不写、子进程不起,实测复现);Start-Process 的子进程本就独立存活,wrapper 无需脱离; Start-Process 的重定向是覆盖语义,因此每次拉起前先把旧 dsh-web.log 轮转为 dsh-web.log.1(保留上一代崩溃现场),新日志有界(当前 + 上一代,不无限增长);
- watchdog 其余短命子进程(自spawn、pnpm、空闲重启 waiter、netstat/lsof)保留 windowsHide: true——普通令牌下安全,且与讨论中 subprocess-local 的处理一致;
- dsh-daemon start 现在像 restart 一样在 launch 后轮询等待健康(最多约 13s)再返回——此前 dsh web 要十几秒才起来,start 立即返回导致紧随的 status 显示 unhealthy,用户常误以为失败又重复 start(重复 start 会按 PID 文件杀掉上一个还在启动的实例);
- 模板断言:spawn 点必须带 windowsHide,且 win32 拉起必须走 Start-Process -WindowStyle Hidden(防回归)。

注:dsh 内部组件(dsh-sandbox-windows-acl 两处 spawn dwFlags:256→257 + wShowWindow:0;dsh-subprocess-local 加 windowsHide:true)是 #1564 讨论里的另一层补丁,针对 dsh 本体、不属于本仓库;升级 dsh 后需重新应用(社区补丁脚本 Culeot/dsh-no-console-flash 幂等可重跑)。

v0.1.17 — 按 dsh 版本决定是否传 --no-open

v0.1.16 起 watchdog 用 dsh web --port <port> --no-open 拉起服务,但 --no-open 是 @deepseek-ai/dsh 0.1.0-rc.8(dsh-web-app 0.1.0-rc.8,同期加入默认弹浏览器行为)才支持的参数:旧版 CLI 直接报 unknown option '--no-open' 退出,导致 watchdog 陷入「拉起即死 → 健康检查失败 → 再拉起」的死循环,web 永远起不来。

- watchdog 每次 launch 时读取 dsh 包 package.json 的版本做 semver 比较,≥ 0.1.0-rc.8 才追加 --no-open;版本未知/读不到时保守跳过——服务照常启动,且 rc.8 之前的 dsh 本来就不弹浏览器,零损失;
- dsh-daemon start 的直启命令(Windows Start-Process / Unix nohup)同步按版本决定是否带 --no-open;
- 门控函数是模块级单一实现,watchdog 通过 Function.prototype.toString() 内联同一份代码——升级/降级 dsh 无需重装 daemon,每次拉起都会重新判定;
- 新增 test/version-gate.test.js 单元测试(npm test 一并运行)。

v0.1.16 — 健康检查、浏览器弹窗与环境变量转发

- /health 路由:watchdog 每 30s 检查 http://127.0.0.1:<port>/health,但 deepseek-harness 的 web server 没有该路由(未知路径 404),导致 web 明明在跑却永远报 unhealthy。插件现在自己注册 /health,返回 200 {"ok":true}——插件在线即 web 在线,检测可靠。
- --no-open:daemon 托管的 web 重启(自动更新、自愈)不再自动弹浏览器 tab;手动 dsh web 仍保持默认打开。
- 环境变量转发:dsh-daemon install/uninstall/reinstall 通过 /dsh-daemon/command 路由在 web 进程里执行,之前 shell 里的 DSH_DAEMON_ 变量到不了插件。现在 CLI wrapper 收集当前 shell 的全部 DSH_DAEMON_* 并随请求转发,因此 DSH_DAEMON_UPDATE_INTERVAL=1m dsh-daemon reinstall 能正确配置 watchdog。

v0.1.15 — 测试/沙箱安装不再污染宿主系统

测试 harness 用临时 HOME 跑 install 时会污染真实环境,两个开关封堵:

- DSH_DAEMON_CLI_DIR:覆盖生成的 dsh-daemon CLI 的写入目录(默认 node bin),测试指向临时目录,不再覆盖真实 PATH 里的 wrapper;
- DSH_DAEMON_NO_SYSTEM:1 时跳过系统级注册(launchd/schtasks/systemd),防止测试 install 按 label 抢注系统服务、让真实 daemon 失效。harness 默认同时设置两者。

v0.1.14 — restart 成为默认自动更新模式

DSH_DAEMON_UPDATE_MODE 的默认值从 download 改为 restart:未显式设置时,更新下载后 watchdog 会在 web 空闲时自动重启生效(完全无人值守,绝不打断进行中的会话)。需要手动控制生效时机时显式设为 download。

v0.1.13 — 自动更新后自动重新生成 watchdog

此前自动更新只刷新 npm 包,已生成的 watchdog.js(安装时的一次性产物)不会自动用上新版的生成逻辑——需要手动 dsh_daemon_reinstall。v0.1.13 起:

- 生成 watchdog 时把插件版本嵌入脚本(GEN_VERSION 常量);
- 插件每次启动时比对 GEN_VERSION 与当前安装版本,不一致(自动更新后、或手动升级包后)即自动重新生成 watchdog.js 与 CLI wrapper 并重启 watchdog 进程;
- 因此自动更新(download 模式用户重启 web / restart 模式自动重启 web)或手动升级后重启 dsh web,watchdog 都会自动跟上新版,无需手动 dsh_daemon_reinstall;
- 测试/开发加载可通过 DSH_DAEMON_AUTOREGEN=0 跳过该同步。

v0.1.12 — Windows 弹窗回归修复

v0.1.11 给 watchdog 的 launch() 等 spawn 加了 windowsHide: true(对应 Windows CREATE_NO_WINDOW)。副作用是 dsh web 进程失去控制台句柄,此后 web 内部任何子进程(git、工具执行等)在 Windows 上都会新建可见控制台窗口 → 运行期频繁弹窗(issue #1)。

v0.1.12 移除全部 4 处 windowsHide,恢复 v0.1.10 的模型:watchdog 由 VBS shell.Run ..., 0(SW_HIDE)启动时自带隐藏控制台,web 继承它,web 的子进程再继承 → 整条链不弹窗(此行为已在 v0.1.10 实测)。

注意:安装/执行插件命令时若仍有弹窗(DSH 沙箱/子进程路径,非本插件 watchdog),那是 deepseek-harness 自身的 Windows 控制台处理问题,与本插件无关——见 discussion #1564 及 Culeot/dsh-no-console-flash 补丁。

许可证

MIT
预览分享卡片长什么样 ↓chenkai2/dsh-daemon
分享 · DeepSeek Harness Hub
chenkai2/dsh-daemon

dsh 守护进程:把 DeepSeek Harness web 服务(dsh web)注册成开机自启、自动恢复的后台服务(LaunchAgent / systemd / cron + 看门狗)

这是你的仓库?认领条目

回到顶部