# @chenkai114/dsh-daemon 中文文档

规范地址：https://npmcn.com/package/%40chenkai114/dsh-daemon/

版本：0\.1\.29
npm 元数据更新时间：2026-10-29T07:11:38.207Z
中文译文更新时间：2026-11-09T12:16:29.569Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40chenkai114/dsh-daemon
原文对照：https://npmcn.com/package/%40chenkai114/dsh-daemon/?lang=original
项目主页：https://github.com/chenkai2/dsh-daemon#readme
源码仓库：https://github.com/chenkai2/dsh-daemon

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：254 kB，6 个文件
- 模块格式：CommonJS
- TypeScript 类型：未声明
- Node.js 要求：\>=18
- 运行依赖：0 个
- peer 依赖：@deepseek-ai/cordis \>=3，@deepseek-ai/dsh-tools \>=0\.1\.0-rc\.6
- 首次发布：2026-08-17T09:05:23.198Z
- 最新版本发布：2026-10-29T07:11:37.845Z
- 近 30 天下载量：3910（2026-10-10 至 2026-11-08，npm 官方下载统计）
- 已发布版本数：30（近 12 个月 30 个）

## 中文简介

dsh 守护进程:将DeepSeek Harness网络服务器(dsh web)注册为自动启动,自愈的背景服务(LaunchAgent / systemd / cron + 监督).

## 安装

```sh
npm install @chenkai114/dsh-daemon
```

也可以使用：`pnpm add @chenkai114/dsh-daemon`、`yarn add @chenkai114/dsh-daemon`、`bun add @chenkai114/dsh-daemon`

## README

## 版本对照（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

[中文](https://npmcn.com/package/%40chenkai114/dsh-daemon/README.md) | [English](https://npmcn.com/package/%40chenkai114/dsh-daemon/README.en.md)

将 **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](https://npmcn.com/package/%40chenkai114/dsh-daemon/CONTEXT.md)。

* * *

## 使用方式

### 方式 A —— 用 `dsh plugin` 安装（v0.1.9+，推荐）

1.  用官方插件管理器将包安装到 **web profile**（在 profile 目录内运行 pnpm，使 loader 能解析到它；仅全局安装不够——见下）：
    
    ```bash
    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_*` 工具即可供每个 agent 使用——直接让 agent 运行 `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` 反映全部状态

### 本地测试

```bash
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](https://github.com/chenkai2/dsh-daemon/issues/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](https://github.com/deepseek-ai/deepseek-harness/discussions/1564) 及 Culeot/dsh-no-console-flash 补丁。

* * *

## 许可证

MIT

## 常见问题

### @chenkai114/dsh-daemon 是什么？

dsh 守护进程:将DeepSeek Harness网络服务器\(dsh web\)注册为自动启动,自愈的背景服务\(LaunchAgent / systemd / cron \+ 监督\)\.

### 如何安装 @chenkai114/dsh-daemon？

在项目目录运行 npm install @chenkai114/dsh-daemon，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：@deepseek-ai/cordis（\>=3）、@deepseek-ai/dsh-tools（\>=0\.1\.0-rc\.6）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @chenkai114/dsh-daemon 支持 TypeScript 吗？

该包的 package\.json 没有声明类型入口。需要类型提示时，可以在 npm 上查找社区维护的 @types/chenkai114\_\_dsh-daemon 是否存在。

### @chenkai114/dsh-daemon 支持 ESM 还是 CommonJS？

以 CommonJS 形式发布，可以直接 require\(\)；在 ES Module 项目中也可以用 import 引入（Node\.js 会把 module\.exports 作为默认导出）。

### @chenkai114/dsh-daemon 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=18。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @chenkai114/dsh-daemon 可以商用吗？

@chenkai114/dsh-daemon 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @chenkai114/dsh-daemon 的下载量有多少？

根据 npm 官方下载统计，@chenkai114/dsh-daemon 在 2026/10/10 – 2026/11/08 这 30 天内被下载 3,910 次，日均约 130 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @chenkai114/dsh-daemon 还在维护吗？

最新版本 0\.1\.29 发布于 2026/10/29（11 天前）；至今共发布 30 个版本，其中过去 12 个月发布了 30 个；首次发布于 2026/08/17。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

