dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH WebUI 重启按钮(systemd 版):侧栏一键重启 dsh-web,WSL/Linux systemd 通道 + Windows 分支,/restart 命令,会话自动续接
English summary. A DeepSeek Harness WebUI restart button (systemd edition). Adds a sidebar‑footer restart trigger next to the remote‑web‑ui phone icon plus a
/restartcommand. Clicking schedulessystemctl --user restart dsh-web(~3s later, so the browser gets its 202 first), dups are refused (409), non‑loopback callers are refused (403), and a flag‑file + residual‑window design prevents restart loops. The reconnect UX belongs to the client runtime, not to this plugin: the ConnectionController's exponential backoff (500ms→10s) and the officialConnectionIndicator— rendered in the sidebar‑foot settings row — own the outage narration and the click‑to‑retry action. This plugin only consumesctx.connection.state, to reload the page once so the restarted host's client bundles actually load. Interrupted agent turns are auto‑resumed by the host half. Platform: WSL/Linux → systemctl --user (primary, tested); Windows → detached helper branch is present but unused on WSL.
GitHub topics(主代理创建仓库时使用): dsh · dsh-plugin · restart · systemd
双面插件(与 dsh-client-ui-aionui-panel 同构):
| 半边 | entry(exports) | 运行环境 | 职责 |
|---|---|---|---|
| host | "." → lib/index.js | node(宿主导进) | /api/restart-dsh 路由、/restart 命令、flag/resume 状态、restart-recover |
| client | "./client" → lib/client.js | 浏览器(WebUI) | 侧边栏 footer 重启按钮 + 二次确认 + 消费核心连接状态触发整页刷新 |
src/
index.ts # host apply():串联路由 + 命令 + boot 消费/resume
host/gate.ts # loopback fence(照抄 dsh-aionui-panel 的 gate)
host/restart.ts # 调度 + 延迟 spawn + 平台分支 + flag/单飞锁
host/recover.ts # agent/created -> lastTurnInterrupted -> followup continue
client/index.ts # client apply():locale + sidebar.footer.action 注入 + connection hooks
client/RestartButton.tsx # 按钮 + 确认对话框(内联样式,无需 CSS bundle)
client/api.ts # POST 助手(只发请求,不做重连探活)
内部依赖:官方 socket sidebar.footer.action(list/root additive,remote-web-ui 电话图标同座);客户端上下文与 slot 服务类型来自 @deepseek-ai/cordis 的 Context + dsh-client-ui-renderer 的声明合并。
| 关注点 | 归属 | 说明 |
|---|---|---|
| 断线检测 / 指数退避重试 | client runtime | ConnectionController(base 500ms → cap 10s,factor 2),插件不探测、不轮询 |
| 断线提示与「点击立即重连」 | ConnectionIndicator(dsh-client-ui-settings-general) | 渲染在侧边栏底部设置行:连接异常,点击立即重连 / 重新连接中 / 连接成功(2s),connecting 最少显示 800ms |
| 重启请求 / 单飞 / 排障文案 | 本插件 | 只负责 POST /api/restart-dsh、确认弹窗与 403/409/429/501 的结果呈现 |
| 重连后加载新的前端 bundle | 本插件 | 核心不会刷新页面(runtime 重连传输层但继续跑重启前的 JS),故订阅 ctx.connection.state,观察到真实断线→恢复后 location.reload() 一次 |
v0.1 里自研的
waitForReconnect()(每 350msfetch('/')+ 20s 超时 +sawDown启发式)与 busy/done 分阶文案已删除——它做的事核心做得更准(能区分「真的重连」和「重启前的旧进程还活着」),且sessionStorage兜底标记随之不再需要。
/restart)→ 二次确认:“进行中的 agent 任务将中断并自动续接。页面会自行重连,并在服务恢复后自动刷新。”POST /api/restart-dsh(或命令回调)。$DSH_HOME/dsh-restart-resume.json,写 flag $DSH_HOME/dsh-restart.flag,随即回 202 {scheduled:true, delayMs:3000}。systemctl --user restart dsh-web。服务重启;浏览器靠 ConnectionController 指数退避(500ms→10s)自动重连,无需手动刷新,核心指示器同步显示断线与恢复。location.reload() 一次(换掉重启前的前端 bundle);45s 内始终未恢复则弹超时提示并指向 systemctl --user status dsh-web。agent/created 监听、读取并立即消费 resume 列表——对每个最后 turn 被中断(存在未闭合 turn/start,或最近的 turn/end.reason.kind === 'interrupted')的会话,agent.followup("Continue.") 自动续接;idle/正常结束的会话绝不打扰。续接没有截止时间:会话什么时候被重新打开就什么时候续接(浏览器自动重连通常是秒级,用户稍后回来也可以)。| 平台 | 行为 | 代码位置 |
|---|---|---|
| WSL | process.platform === 'linux' → systemctl --user restart dsh-web(本机主场景,已实测) | restartArgv() / platformSupported() |
| Linux | 同上(systemd 用户态) | 同上 |
| Windows(原生 win32,非 WSL) | spawn(process.execPath, [helper, 'win-restart.mjs']) detached helper,重启可脱离当前进程存活(机器相关,WSL 场景不走此分支) | 同上 |
$DSH_HOME/dsh-restart.flag:spawn 前写入,boot 消费一次即删,绝不自行触发重启 → 残留 flag 无法造成二次重启。sec-fetch-site === 'cross-site' 或 Origin 不同源 → 403;X-Forwarded-For 永不被信任。$DSH_HOME → ~/.dsh)| 文件 | 用途 |
|---|---|
dsh-restart.flag | 一次性 boot token { reason, ts, sessionIds } |
dsh-restart-resume.json | 续接列表 { ts, reason, sessionIds } |
本目录即一个独立 Git + npm 包(社区格式 dsh-<feature>,systemd 版包名唯一且表意):
package.json 已补全:name: dsh-restart-systemd、version: 0.1.0、license: MIT、
repository(GitHub 占位 https://github.com/<your-org>/dsh-restart-systemd.git,主代理稍后创建)、
keywords(['dsh','dsh-plugin','deepseek-harness','restart','systemd','systemctl','wsl','webui'])、
files(lib, src, cordis.patch.yml, README.md, LICENSE)、dsh.bundle.patch ⇒ ./cordis.patch.yml。LICENSE 已就位(MIT,2026 contributors)。cordis.patch.yml 注入一行双面插件,安装后 host+client 同时挂载,无需手改 bundle:
- insert:
- id: ui-dsh-restart-systemd
name: dsh-restart-systemd
创建 GitHub 仓库时建议 topics:dsh、dsh-plugin、restart、systemd。
# 1) 构建(需 devDependencies)
cd dsh-restart-systemd
npm install
npm run build # tsc -b → lib/(host=lib/index.js,client=lib/client.js,内联样式无需 CSS bundle)
# 2) 装入 web profile(注册 bundle + 补丁插件行)
dsh plugin --profile web add /tmp/dsh-restart-systemd
# 或 pnpm add /tmp/dsh-restart-systemd + 手补 cordis.patch.yml 行到 profile bundle 列表
# 3) 无热重载,需重启生效
systemctl --user restart dsh-web
exports."." → lib/index.js,exports."./client" → lib/client.js;main/types 与之对应。
v0.2 起 devDependencies 对齐 DSH 0.1.7-rc.1(v0.1 编译于 0.1.0-rc.6,其 ConnectionHandle 还没有 state;dsh-client-runtime 包已被上游移除,客户端上下文改用 @deepseek-ai/cordis 的 Context,slot 服务由 dsh-client-ui-renderer 声明)。
对宿主的运行时要求很宽松:connection 服务是软读取(ctx.get('connection'),不写进 inject)。宿主没有该服务或版本更老时,重启按钮与 /restart 命令照常工作,只是不再自动整页刷新。
npm test # = build + 下面四套(共 87 项)
npm run test:client # 41 项:客户端 bundle 接线 / 刷新状态机 / 渲染与文案
npm run test:host # 25 项:续接判定全分支 / arm 端到端 / 无截止时间 / inject 声明
npm run test:wiring # 11 项:真 cordis 下的服务拓扑与 inject 解析
npm run test:cycle # 10 项:整个宿主链路(写快照 → boot 消费 → 续接)
后两套用真 cordis 加载真插件,并把 subprocess.spawn 打桩——因此不需要重启服务就能验证宿主半边,物理上也不可能触发重启。test:cycle 尤其关键:它按真实拓扑(agents 由兄弟插件提供)跑完整循环——重启写快照 → 下一个进程 boot 消费 flag/resume → arm() 装监听 → agent/created 触发 → 只有「最后 turn 被中断」的会话收到 Continue.。
对照实验:把同一套
test:cycle跑在修复前的构建上,快照是sessionIds: [](自动续接永远不可能触发);跑在修复后是["session-interrupted","session-clean"]。测试对 bug 敏感,不是空测试。
scripts/verify-client.mjs 在没有浏览器、也不重启线上服务的前提下验三件事:
lib/client/index.js,断言注册的 slot / locale / hooks.connectionState,以及运行时 require 只有 react、react-dom、react/jsx-runtime 三个 seed word;restartReloadDecision 的全部分支,重点是「观察到真实断线之前的 connected 绝不触发刷新」(宿主在 ~3s 调度延迟内仍由重启前的进程应答);t('restart.*') 键在中英两本字典里都存在、且没有多余键。scripts/verify-host.mjs 盯住续接判定这条最危险的逻辑(两个方向都会坏:太激进 = 给正常结束的会话发多余 turn;太保守 = 中断的 turn 永远不续接):
lastTurnInterrupted 全分支:未闭合 turn / interrupted / completed / aborted / blocked / error / max-tokens / 空日志 / 日志不可读(兜底为续接);arm() 端到端:只有「在快照里 且 最后 turn 确实被中断」的会话才会收到一条 Continue.,且该消息带本插件自有 source kind;正常结束或不在快照里的会话一条都不发;inject 必须声明 agents:这是让快照不为空的前提(见下)。这套用例是有来历的,三条都是实测事故:
- 升级到 0.1.7-rc.1 后
agent.session.events被snapshotEvents()取代,旧代码抛出的 TypeError 被try/catch吞掉,于是lastTurnInterrupted()对任何会话都返回 true。实测把旧实现对着新 Session 形状跑,4/4 个「干净结束」用例全判成需要续接——即会给快照里每个会话都灌一条Continue.。- 早先的实现以 boot 为基准给 60s 截止时间。实测本机一次重启:会话 18:40:58 被打断,直到 21:06 才被恢复,那时
pending已清空,续接完全没发生。现在改为「等会话被打开,没有截止时间」。- 最隐蔽的一条:
inject漏了agents。cordis 只给已声明依赖解析服务,而提供方(dsh-base 的agent行)与插件是根级兄弟、不是祖先,于是读ctx.agents抛cannot get property "agents" without inject,又被catch { return [] }吞成空数组——每次重启快照都是[],自动续接从头到尾就没工作过,日志里只留一句recorded 0 running agent(s),看着像"当时没任务在跑"。现在改为失败时logger.warn明说"本次不会续接任何会话",并打出live=N (id:status, …)让空快照一眼可辨。
线上手工验收:
journalctl --user -u dsh-web 出现 dsh-restart-systemd: ...。连接中断,正在重试,点击立即重连 → 重新连接中 → 连接成功),页面自行重连,随后自动整页刷新一次:
restart scheduled … delayMs=3000 → spawning restart for dsh-web … → 新 boot 的 consumed leftover restart flag。/restart 命令:聊天输入 /restart → 返回确认文本。recovery armed for N session(s) 与 resuming interrupted agent <id>,agent 自动继续。already-scheduled。curl -H 'Host: evil.example' -X POST http://127.0.0.1:3080/api/restart-dsh → 403 forbidden: loopback-only。systemctl --user status dsh-web 确认单元存活。dsh plugin --profile web remove ui-dsh-restart-systemd
# 或手删包 + bundle 行后
systemctl --user restart dsh-web
# 清理残留状态(随时可删)
rm -f ~/.dsh/dsh-restart.flag ~/.dsh/dsh-restart-resume.json
重启后按钮与命令即消失,不改动其它配置。
systemctl --user restart dsh-web,主目标已实测。restartArgv() 走 detached spawn(process.execPath, [helper])
win-restart.mjs(未随包附带;参考 anweat/dsh-restart 的 detached helper、
LnsiAxe/dsh-web-restart 的 WMI Win32_Process.Create、shaoyi1991 的 lsof 杀端口+spawn)。
路由、单飞、flag、loopback fence、restart-recover 均平台无关。WSL 本机场景不进入此分支。dsh-web(SYSTEMD_UNIT)。node … 手跑,spawn systemctl 会失败并记日志,不产生重启。completed/aborted 收尾,从而被跳过,所以不会打扰。仍在快照里但始终没被打开的会话(例如无头运行)只是留在内存里,不再写回磁盘:dsh-restart-resume.json 在读取时即被消费(删除),与 flag 同为一次性令牌。/plugins(官方 client-modules 拥有该前缀),仅注册 /api/restart-dsh,无 bundle 供数冲突。ConnectionIndicator 仅在 wide 时挂载(sidebar.toggle.badge 也会在断线时隐藏)。此时用户只能看到重启按钮自身的 spinner/禁用态;展开侧边栏即可看到核心的断线/重连提示。kind: 'plugin',v0.2 按 harness 设计改为本插件自有的 kind: 'dsh-restart-systemd'(MessageSourceMap 合并扩展)。任何按 source.kind === 'plugin' 过滤注入消息的插件(如 dsh-handoff-wall 的取材过滤)需同步加上这个新 kind,否则 Continue. 会被当作真实对话取走。ctx.systemPrompt.section({ name: 'plugin:dsh-restart-systemd', order: 215 }),把一段运维说明广播给每个会话、每一轮、以及每个子代理。删除理由有三:重启面是人的能力(侧边栏按钮 + /restart),而 dsh-commands 的契约是命令注册表不进任何模型请求("Command discovery, execution, and UI output add no model tokens"),所以那段文字买不到任何模型行为改变;它的体裁是 README(HTTP 202/409/403、3s 延迟、journalctl 排障),不是祈使句形式的行为指令,官方 prompt 段落(dsh-tool-jobs、dsh-tool-fs 等)全是后者;而且它的断言在 v0.2 修好之前一直是假的——它声称「重启前记录会话、重启后自动续接」,而 1e4206d 之前 inject 漏了 agents,快照恒为空、自动续接从未生效。能力发现走命令/工具描述,人类说明留在本 README。scripts/verify-host.mjs 用 !inject.includes('systemPrompt') 钉住这条。MIT — see LICENSE.