跳到主要内容

dsh-restart

已验证

@zhengjunyao/dsh-restart · v0.2.1 · MIT · Web 界面

One-click restart for DeepSeek Harness: a web button (plus a dsh_restart agent tool) hands the relaunch to a detached helper that waits for the port to free, relaunches the exact same command, streams the new host's output, auto-reconnects the page — and

安装

dsh plugin add @zhengjunyao/dsh-restart

用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。

源码

标签

说明文档

@zhengjunyao/dsh-restart

English | 中文

给 DeepSeek Harness 装一个「重启」按钮:装完/更新插件后不用再回终端敲命令, 在 Web GUI 里点一下就把 DSH 重启了——页面自己重连、自己刷新;如果新进程起不来, 它会把报错直接显示出来(页内遮罩 + 一个独立端口的恢复控制台)。

Agent 不能自己把 DSH 重启掉。 dsh_restart 工具只提交一个「重启请求」, 必须在面板上亲手点「批准并重启」才会真的执行(见下面的「用户批准闸门」)。

为什么需要它

宿主侧的插件代码(lib/index.js、bundle 清单、dsh.client 清单)只有换一个 dsh web 进程才会生效,所以每次装插件都得手动重启一次。而这个「手动重启」本身 是最容易出事的环节:

  • 重启后进程起不来 → 浏览器一片空白,看不到任何原因;
  • 新进程崩溃/端口占用 → 只有终端能看见报错;
  • 重启期间页面直接失联 → 只能靠人手刷新。

dsh-restart 把这三件事都接住了。而在 agent 也能调工具之后,还多出第四件事: agent 顺手就把 DSH 重启了、你事后才知道——所以重启请求默认要你点确认。

能力

  • 一键重启:设置页「重启」卡片、左侧边栏「重启」入口(与其它插件入口并排), 点一下即完成交接。本人在面板上点击不需要二次确认——点击本身就是同意。
  • 用户批准闸门(默认开):agent 调 dsh_restart 不会重启任何东西, 只落一个待批准请求(~/.dsh/dsh-restart/pending-approval.json,默认 15 分钟有效)。 请求会同时出现在三处:
    • 设置页「重启」卡片里的红色确认卡:原因、发起方、提交时间、剩余有效时间(倒计时), 以及「批准并重启」/「拒绝,保持运行」两个按钮;
    • 侧边栏重启图标上的红点(不打开面板也看得见);
    • 一条 macOS 桌面通知。 在你点之前宿主不会有任何动作,超时未处理请求自动作废。批准是一次性消费: 请求先被删除再执行,所以重复点击、重放请求都不会重启两次。
  • 两种重启策略,自动识别:
    • launchd 托管(本机默认):宿主是 launchd 任务(com.dsh.web、KeepAlive)时, 自己 spawn 一个宿主会和 launchd 抢端口,所以改为助手 launchctl kickstart -k 让 launchd 自己重启,助手退居观察者(只等端口回来 + 跟随 plist 的 stdout/stderr 日志), 绝不重复拉起。
    • 自拉起:非托管宿主则由助手用完全相同的 argv/cwd/env 重新拉起, 先等端口真正释放(不是赌固定延时)再 spawn,不会撞 EADDRINUSE。
    • 助手本身是 detached 的零依赖纯 Node ESM(helper/restart-helper.mjs), 两种策略共用同一套日志/状态/控制台。
  • 自动重连:页面按配置的间隔探测 /api/dsh-restart/probe,新宿主一应答就自动 刷新(autoReload,默认开),载入新代码。
  • 报错直显:新进程的 stdout+stderr 实时流入 ~/.dsh/dsh-restart/logs/*.log; 助手把疑似报错的行(Error / EADDRINUSE / MODULE_NOT_FOUND / 调用栈 …)单独挑出来, 在页内遮罩里显示;DSH 已经完全挂掉时,助手自己的恢复控制台 (默认 http://127.0.0.1:3099,CORS 开放)仍然可用,能看到阶段、启动日志、 报错行、退出码,并可一键「重试启动」。
  • 失败自动重试:maxAttempts(默认 2)次内自动重试;都失败就停在那里等人工处理, 不会把端口/日志丢掉。
  • 就绪不等于端口通了:dsh web 会先绑端口、后加载插件树,所以「端口应答」远早于 「宿主真的起来了」。助手判定就绪要同时满足三条——端口应答、进程存活、启动输出里没有 boot 致命行——并先守住 readyConfirmMs(默认 4s);此后还会继续观察 bootWatchMs(默认 30s),把「已经报了就绪、几秒后却自己死掉」的启动(例如凭证写锁 等 30 秒才超时)重新判为失败,而不是留下一个假的成功。 launchd 托管的 observe 模式同规:先守 readyConfirmMs,并跟随托管方日志判断有没有 boot 致命行(宿主进程不在助手手里,日志就是存活性信号);托管路径刻意不做长观察—— 守住端口是托管方的职责。
  • Agent 工具:
    • dsh_restart_status(只读):宿主 pid/端口/版本/启动时长/启动命令、助手阶段与失败原因、 最近重启记录、上次启动日志里的疑似报错行,以及当前有没有待你确认的重启请求;
    • dsh_restart(提交重启请求):仍需传 confirm: true,但那只是调用方的自我声明, 不代表你同意、也不解锁任何东西——闸门开着时它只会提交请求并告诉你「去点确认」。
  • 旧标签页自愈:重启失败态只存在于页面(内存 + sessionStorage),所以宿主恢复后 (自己起来的、或 launchd 救回来的)残留的「启动失败」文案必须自己消失——页面在失败态 持续探测、标签页重新获得焦点时立刻复查、宿主一应答就丢掉持久化的失败记录,不需要用户刷新。
  • 401 原地换回登录:每次 dsh web 启动都换一个 launch token,旧标签页 URL 里的旧 token 在 cookie 失效时会被 401 拒。页面会 HEAD / 检出 401,并从免 cookie 的插件路由 取回本进程当前的 token,然后在原地(同一 authority、不跳转)发一次交换请求把 cookie 换回来,重新校验确认已认证后才让页面自己刷新回应用——一次重启结束时页面是自己回来的, 不用复制地址开新标签页。交换失败时才回落到可点击的**「用新 token 地址打开」**链接,所以 cookie 存不下也不会把页面带进宿主的纯文本 401 页。
  • 永久入口(可收藏):GET /api/dsh-restart/goto 永远 303 到本进程当前 token(相对地址)。 把它收藏起来就是「永远能进站的那条地址」——cookie 没了、停在纯文本 401 页、重启期间手刷过头, 点一下都能回来;401 卡片的主按钮与复制按钮给的就是它。
  • 等待期提示「请勿手动刷新」:重启窗口里自己按刷新会落进浏览器的 ERR_CONNECTION_REFUSED (那一刻页面已被浏览器错误页替换,插件救不了),所以提示写明「端口会先空约 3–5 秒,本页会自己回来」。
  • 重启历史:~/.dsh/dsh-restart/history.json 记录每次重启的时间、来源、原因、 新旧 pid 与日志路径。经你批准的重启记为 source: "user-approved"。

安装

  1. 安装插件(本地开发用 link:,发布版用 npm,也可以从 GitHub 装):

    dsh plugin --profile web add @zhengjunyao/dsh-restart   # npm
    dsh plugin --profile web add link:/path/to/dsh-restart  # 本地开发
    dsh plugin --profile web add github:zhengjy01/dsh-restart
    

    预期结果:命令返回成功,~/.dsh/profiles/web/package.json 的依赖里出现 @zhengjunyao/dsh-restart。

  2. 重启一次 DSH 让宿主侧代码加载(这一次是最后一次手动重启;装完以后再要重启, 直接点面板按钮即可)。

    预期结果:页面自动重连、自动刷新;左侧边栏出现重启图标,设置页出现「重启」卡片。

截图位 1|装完的样子 截:设置页「设置 → 插件配置 → Web 插件 → 重启」展开后的卡片(能看到宿主 pid、版本、 启动命令、重启记录、插件设置折叠区)。 范围:整个卡片,宽度约 680px;无需打码(里面是本机端口与 pid,不含 token)。 建议文件名:docs/images/dsh-restart-01-panel.png 补图后把本引用块替换成:![重启面板](docs/images/dsh-restart-01-panel.png)

  1. 验证能用:面板里能看到宿主信息与「上次启动日志」,说明宿主半与客户端半都加载成功。

截图位 2|侧边栏入口 + 并排的那一行 截:左侧边栏底部、与其它插件入口同一行的重启图标(悬停时 title 应显示「重启 DSH」)。 范围:只截那一行,别带整页;无需打码。 建议文件名:docs/images/dsh-restart-02-sidebar.png 补图后:![侧边栏入口](docs/images/dsh-restart-02-sidebar.png)

使用

你自己要重启

  1. 打开「设置 → 插件配置 → Web 插件 → 重启」(或点左侧边栏的重启图标)。
  2. 点「立即重启」:
    • 出现重启遮罩,显示阶段(下发指令 → 旧进程退出 → 新宿主启动 → 已就绪)与已等待时间;
    • 宿主退出(launchd 托管时由 launchctl kickstart -k 触发)、助手接管、新宿主起来后页面自动刷新;
    • 若新宿主起不来,遮罩里直接出现 Error: … 与日志尾部,可「复制报错」「让助手重试启动」 「打开恢复控制台」。
  3. 面板里还能看到:宿主信息、上次启动日志(含疑似报错行)、重启记录、插件设置。

Agent 请求重启时(批准闸门)

  1. 某个会话里的 agent 调用了 dsh_restart → 它不会重启,只提交请求; agent 的回复里会写明「已提交,请你去点确认」。
  2. 你会同时收到:macOS 桌面通知、侧边栏图标上的红点、面板里的红色确认卡。
  3. 打开面板,核对卡片上的原因与发起方,然后二选一:
    • 批准并重启:立刻按上面的流程重启,请求被消费掉(不会被重复执行);
    • 拒绝,保持运行:请求作废,宿主没有任何动作。
  4. 你也可以什么都不做:超过 approvalTtlMinutes(默认 15 分钟)请求自动作废。

截图位 3|批准确认卡(本功能的主角) 截:面板里红色边框的「有一个会话请求重启 DSH(尚未执行)」卡片,要能看清 原因、发起方、剩余时间倒计时、两个按钮。 范围:卡片本体,含右上角倒计时徽标。 打码:若原因/发起方里出现本机用户名或个人路径,用色块盖掉。 建议文件名:docs/images/dsh-restart-03-approval-card.png 补图后:![批准确认卡](docs/images/dsh-restart-03-approval-card.png)

截图位 4|侧边栏红点 + 桌面通知 截(可拼两张):①侧边栏重启图标右上角的红点;②macOS 通知中心里那条 「DSH:有一个重启请求,等你确认」。 打码:通知里的原因文本若含路径/home 用户名,请盖掉。 建议文件名:docs/images/dsh-restart-04-badge-and-notification.png 补图后:![红点与通知](docs/images/dsh-restart-04-badge-and-notification.png)

截图位 5|重启失败时的恢复控制台 截:新宿主起不来时的页内遮罩(显示 Error: … 与日志尾部),或 http://127.0.0.1:3099 的恢复控制台页面。 打码:日志里的本机绝对路径与用户名建议盖掉。 建议文件名:docs/images/dsh-restart-05-recovery-console.png 补图后:![恢复控制台](docs/images/dsh-restart-05-recovery-console.png)

配置

~/.dsh/dsh-restart.json(0600,首次加载时按插件行种子生成;面板可改):

键 默认 含义
enabled true 总开关(关闭后不挂载路由与工具)
announceToAgent true 在系统提示里公告插件能力
entry sidebar 入口位置:sidebar / ball / both / off
requireApproval true agent 发起的重启是否必须由你在面板点确认。关掉 = 恢复「confirm 即执行」的旧行为(不推荐;等于把闸门拆了)
approvalTtlMinutes 15 待批准请求的有效期(分钟,1–120)。超时自动作废
notifyOnApproval true 收到请求时发一条 macOS 桌面通知(仅 darwin)
restartMode auto auto(识别到 launchd 就交给它)/ launchd(强制,找不到任务则报错)/ helper(强制自拉起)
fallbackPort 3099 恢复控制台端口(被占用时自动 +1…+9)
bootTimeoutMs 120000 新宿主多久没应答算这次尝试失败
maxAttempts 2 单次重启请求的启动尝试次数
killGraceMs 6000 端口迟迟不释放时,助手 SIGKILL 旧进程前的宽限
portFreeTimeoutMs 25000 等旧进程释放端口的上限
lingerMs 4000 就绪后助手退出前保留控制台的时间
readyConfirmMs 4000 端口应答后、判定「已就绪」前必须守住的稳定时长(0 = 不守)
bootWatchMs 30000 已报就绪后继续观察新宿主、把「起来又死」改判为失败的时间窗(0 = 不守)
logLines 200 面板/接口返回的日志行数
autoReload true 新宿主应答后自动刷新页面
showOverlay true 重启时显示全屏遮罩
probeIntervalMs 1200 重连探测间隔
historyLimit 30 重启历史保留条数

HTTP 接口

全部 loopback-only(127.0.0.1 / ::1,同源),沿用其它 dsh-* 面板的守卫:

方法 路径 说明
GET /api/dsh-restart/status 宿主 + 助手 + 配置 + 历史 + 当前待批准请求
GET /api/dsh-restart/probe 极小存活探针(重连时高频轮询)
GET /api/dsh-restart/auth 本进程当前 launch token 地址(故意不要求 cookie,仍是 loopback-only)
GET /api/dsh-restart/goto 永久入口:303 到本进程当前 token(相对地址 ⇒ cookie 换回本 authority)。可收藏、不会过期
POST /api/dsh-restart/restart 交接重启,先回 202 再退出本进程(面板按钮走这条)
GET /api/dsh-restart/approval 当前待批准的重启请求 + 有效期 + 闸门开关
POST /api/dsh-restart/approval 你的决定:{ id, decision: "approve" | "deny" }。approve 会先消费请求再交接重启
GET /api/dsh-restart/logs 启动日志尾部 + 疑似报错行
GET /api/dsh-restart/history 重启记录
POST /api/dsh-restart/config 改配置 / reset: true 恢复默认
GET /api/dsh-restart/helper 经宿主读取助手实时状态
POST /api/dsh-restart/helper/retry 让失败的助手再试一次

工作方式

你自己点「立即重启」(本人点击即同意)
      │ POST /api/dsh-restart/restart
      ▼
  宿主(旧进程)──写 pending-spec.json──▶ 分离的助手进程(detached,零依赖)
      │ 回 202,延迟 ~0.7s 后 SIGTERM 自己                    │
      ▼                                                      │ 等端口释放
   进程退出 ─────────────────────────────────────────────────┤
                                                             ▼
                                        用完全相同的命令 spawn 新宿主
                                        (stdout/stderr → logs/<时间>-<pid>.log)
                                                             │
                     status.json ◀── 阶段/进度/报错 ──────────┤
                     http://127.0.0.1:3099 ◀── 恢复控制台 ───┘
                                                             │
   页面轮询 /probe ──▶ 新宿主应答 ──▶ location.reload() ◀─────┘

agent 调 dsh_restart(闸门开着)
      │ 只写 pending-approval.json(不重启)
      ▼
  面板红点 + 桌面通知 + 确认卡 ──你点「批准并重启」──▶ POST /api/dsh-restart/approval
                                                      │ 先消费请求(一次性)
                                                      ▼
                                              走上图那条重启路径(source=user-approved)

兼容性

  • 要求:DeepSeek Harness ≥ 0.1.5-rc.1(即 package.json 的 dsh.engines.dsh)。
  • 实测通过:0.1.5-rc.1 与 0.1.7-rc.2(macOS + Node 25.8.1;宿主半、客户端半、 真实重启全流程)。
  • 平台:只在 macOS 上实测过。macOS 且宿主由 launchd 托管时走 launchctl kickstart -k; 其它平台自动落到「分离助手自拉起」路径(代码里对 launchd 有平台守卫,Linux / Windows 未实测)。 桌面通知也只在 macOS 上发出(其它平台静默跳过,请求本身照样落盘)。
  • 兼容性判定也来自 peerDependencies 的 @deepseek-ai/dsh-* 范围并集(插件市场展示的是这一项)。 本版起在 peerDependencies 中显式声明兼容 DSH 0.2.0-rc.2(官方 DSH 包的版本范围已含 ^0.2.0-rc.2),在 0.2.0-rc.2 上不会再出现兼容告警;功能与行为无变化。

测试

pnpm test        # 275 项断言,八个套件
  • tests/smoke.mjs — 配置读写与钳制、历史、日志尾部与报错识别、启动签名、宿主信息;
  • tests/approval.mjs — 批准闸门:落盘权限 0600、新请求替换旧请求、id 不匹配不得执行、 批准一次性消费(重放不再触发)、拒绝无残留、过期请求绝不重启、空状态是 no-op;
  • tests/helper.mjs — 真的跑助手:崩溃路径(捕获退出码、stderr 报错行、控制台页面、 手动重试)、成功路径(等端口 → 拉起 → 就绪计时),以及就绪判定的三种边界: 宿主应答端口后才在插件上崩掉(不得报就绪,且要撤回已报的就绪状态、写入失败报告)、 应答端口后静默退出(没有任何报错行也要靠存活性判失败)、健康宿主打印错误形状的噪音 (不得误判为失败);
  • tests/routes.mjs — 合成的 req/res 打全部路由,含 loopback/跨站/方法守卫,以及 「connection 服务 → /auth 新 token 地址」的装配(无该服务时回落纯 origin);
  • tests/handoff.mjs — 端到端:假宿主进程 → POST 重启 → 旧进程真的退出 → 助手用相同命令拉起第二代 → 端口重新应答(新 pid)、restarted: true、历史落盘;
  • tests/launchd.mjs — launchd 识别(按 pid 匹配 launchctl list,因为 Node 会把 XPC_SERVICE_NAME 改写成 0)+ observe 模式(助手执行 kickCommand、跟随托管方日志、 绝不自己 spawn),以及托管宿主日志出现 boot 致命行时不得报就绪;
  • tests/selfheal.mjs — 用桩替换 sessionStorage/location/fetch,驱动真实的浏览器 bundle: 「构造失败态 → 宿主恢复 → 页面自愈」(挂载即清掉宿主已恢复的失败记录;失败的重启请求在 宿主应答后自动离开失败态并刷新)与「命中 401 → 取回当前 token 地址、且不刷新进 401 页」;
  • tests/client.mjs — 浏览器半的装配与渲染契约(入口、面板、遮罩挂在正确的槽位)。

边界

  • 只负责「重启」这一件事:不做插件安装、不做配置修复(那是 dsh-doctor 的领域)。
  • 重启一定会中断当前回合与连接——这是宿主进程被替换的必然结果;本插件保证的是 中断可见、可恢复、报错可读,而不是「不中断」。
  • 助手不会注册任何 OS 级后台服务;它就活一次重启,成功后就绪 + 保留控制台数秒即退出, 失败时留在原地等你处理(可随时 kill)。
  • 批准闸门挡住什么、不挡什么(诚实说明):它保证正当路径上 agent 无法自行重启—— 工具只提交请求,请求必须由你在面板点击才生效,且这一点击是唯一入口。 它不是对抗性沙箱:任何能在这台机器上执行 shell 的进程(包括 agent 自己) 理论上都可以绕开插件直接 launchctl kickstart -k com.dsh.web,插件不假装能阻止那种情况。 它解决的是「某个会话顺手把 DSH 重启掉、你事后才知道」,以及「重启没有留下任何人的许可痕迹」。
  • dsh_restart 仍需 confirm: true:那是调用方的自我声明,用来让 agent 先向你说明, 不构成你的许可;真正的许可是你在面板上的那一次点击(history.json 会记为 source: "user-approved")。

License

MIT