dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
在 DSH Web GUI 里点一下,就重启整个 DSH 运行进程——让新安装 / 更新 / 改过配置的插件与配置立刻生效。 不是刷新浏览器、不是只重载本插件、也不新建第二个实例。
插件跑在 DSH 进程内部,一旦 DSH 退出就什么都不剩了,没有组件还能启动新实例。 所以本插件只负责妥善收尾并把接力棒交出去:一个不依赖 DSH 存活的 detached 助手 负责「等旧实例退出 → 等端口释放 → 起新实例 → 健康检查 → 写回结果 → 自删退出」。
本机 DSH 由 D:\dsh\dshstart\Start-DSH-GUI.ps1 托管,该脚本用
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE(0x2000) 创建 Job Object,脚本结束时关闭 Job 句柄,
Job 内所有进程一起被终止。带对照组的实测结论(三步验证,详见「验证记录」):
| 派生方式 | 是否继承 Job | Job 关闭后 |
|---|---|---|
Node spawn({detached:true}) | 是 | 被杀 |
Start-Process(同控制台) | 是 | 被杀 |
explorer.exe / ShellExecute | 否 | 存活 |
WMI Win32_Process.Create | 否(父进程 = WmiPrvSE.exe) | 存活 |
因此助手只用 WMI 创建,并且创建后硬核验父进程必须是 WmiPrvSE.exe:
核验不过就中止重启(DSH 保持原样),绝不会出现「DSH 停了但没人拉起」。
CreateFlags 必须是合法的 DETACHED_PROCESS(0x08)(助手不分配控制台 ⇒ 不弹黑窗);
CREATE_NO_WINDOW(0x08000000) 不在 Win32_ProcessStartup.CreateFlags 的合法取值里,
WMI 会直接返回 InvalidParameter(21)。
WMI 创建的进程继承的是 WmiPrvSE 的环境,不是 DSH 的。所以宿主机把 DSH 环境按变量名过滤后随
spec 传给助手:名字看起来像凭据的(*_KEY / *TOKEN* / *SECRET* / PASSWORD / COOKIE /
AUTHORIZATION …)一律不传,只把变量名报告给用户,值永不进入文件、命令行或日志。
ctx.appExit,不是 process.exitDSH 没有「重启进程」接口,也没有全局 drain / quiesce 闸门。官方唯一的整机停止接口是
ctx.appExit(code):它接到 CLI 的 process-shutdown,dispose 整棵 fiber 树
(agent 取消 + 等空闲、jobs teardown、关监听 socket)→ 设 process.exitCode → Node 自然退出,
5 秒兜底强退。仓库 Agent Note 明确记录过:Windows 上在 Undici 请求后立刻 process.exit()
会触发 libuv 断言(nodejs/node#56645),所以本插件绝不自己调 process.exit();
只有 ctx.appExit 缺失时才降级,并把降级如实写进日志与 GUI 状态。
DSH 没有「停止接收新任务」的全局开关。能用的官方局部闸门 + 等待语义:
ctx.goals.disarm(agent) —— 撤销自动续跑授权(否则 goal 会在 turn 结束后自己再起一轮,永远等不到静止);ctx.subagents.drainContinuableDescendants(agents) —— 关闭可续跑子代理接纳;agent.whenIdle() + 对账 ctx.jobs 注册表,上限 gracefulShutdownTimeoutMs;confirming-force-restart,只提示、不自动强杀;agent.cancel({kind:'hook'}) + ctx.jobs.kill(id, ownerAgent),再走 appExit。agent/status(idle⇄running,覆盖模型请求 / 流式返回 / 工具调用 / 重试恢复) +
subagent/start|end + workflow/start|end +
ctx.jobs 注册表。注意 jobs.list(caller) 只返回「无主 + 该 caller 的」job,
所以对账时必须对每个 agent 各取一次再并集,否则会漏掉 bash / pwsh / 子代理产生的有主后台任务。
本插件以 profile bundle 的形式安装(package.json 里的 dsh.bundle.patch
指向仓库自带的 cordis.patch.yml,安装器据此把插件插进 profile 的补丁文件)。
手工安装就是把下面三件事做完:
<DSH_HOME>/profiles/web/package.json → "dsh-quick-restart": "link:<插件目录绝对路径>"
(发布到 npm / GitHub 后也可写成版本号或 github:zhuifengqug/dsh-quick-restart)<DSH_HOME>/profiles/web/cordis.patch.yml → - insert: { id: dsh-quick-restart, name: dsh-quick-restart }
(即仓库根目录的 cordis.patch.yml)<DSH_HOME>/profiles/web/node_modules/dsh-quick-restart → 指向插件目录(junction/symlink)装完需要手动重启一次 DSH 才会加载(这正是本插件要解决的问题,所以第一次得手动一次)。 之后就可以一直用 GUI 里的按钮。
⚠️ 不要用
dsh web --patch <外部文件>的方式挂载本插件:client 模块扫描以补丁文件所在目录为 resolution base,插件会加载到宿主半体但客户端半体不会进 boot graph(实测 404)。
npm run typecheck # tsc --noEmit(仓库自带 tsc:node <repo>/node_modules/typescript/bin/tsc)
npm run build # node:module.stripTypeScriptTypes 剥离类型 + 拷贝客户端包与助手
npm test # 单元测试(20 项:脱敏/配置/配方/状态机/锁/出生记录)
npm run test:launch # 助手启动路径回归测试(假新实例,不碰真 DSH;Windows)
npm run e2e # 端到端重启(需要先起一个可重启的测试实例)
src/ 是唯一权威源码,lib/ 是可删除重建的产物。构建期守卫会拒绝不可擦除语法、
拒绝 export default apply、拒绝客户端访问未声明的服务、并强制助手只 import node: 内建模块。
start 直接起进程,不要写 .cmd 再 start 它老实现把命令行写进 launch.cmd 再 cmd /c start "" launch.cmd,那等价于
cmd /K launch.cmd:cmd 是逐行读取脚本的,脚本必须活到新 DSH 退出为止。
可助手在健康检查一通过就清理了运行目录,DSH 退出时 cmd 读不到收尾的 exit,
窗口会永远停在提示符上,并且锁住工作目录(真机实测:DSH_HOME\profiles\web
被它锁死,目录改名/删除全部 EBUSY)。
现在改成直接 spawn(cmd, ['/c','start','',program,...args], { cwd }):
工作目录由 spawn 的 cwd 给(不需要 cd /d),程序退出窗口就关闭,
运行目录可以立刻清理。npm run test:launch 钉住这两条。
idle → checking → waiting-for-tasks → confirming-force-restart → preparing-helper
→ stopping → starting → waiting-for-ready → succeeded | failed
迁移是表驱动的(src/state.ts 的 TRANSITIONS),非法迁移直接拒绝,不用布尔量拼状态。
旧实例只写到 stopping;starting 之后由助手续写;新实例的插件加载时读同一份记录回传 GUI。
状态文件:<DSH_HOME>/quick-restart/state/<port>-<pid>.json(多实例隔离 + 并发锁 .lock)。
内容只有路径、PID、状态、超时、健康检查结果与失败原因;写盘前统一过 sanitizeRecord,
任何字段都先经 redact()。支持过期清理、异常中断恢复(标 failed 可重试)、陈旧锁抢占
(持锁进程已死或 PID 被复用)。
sidebar.footer.action:侧栏常驻按钮,窄栏只留图标但保留 aria-label / titlesettings.section:设置页整页(同一套组件)ctx.connection.rpc.handle('/quick-restart', …)
—— 不自己注册裸 HTTP 路由,因此不会绕过 /api 的信任围栏;通道注册是调用方 fiber 的 effect,
卸载/热重载自动撤销HEAD /favicon.svg(静态文件,不受鉴权影响)做只读探活;
恢复后回读真实结果。不自己实现重连:DSH 的 ConnectionController 本来就会无上限指数退避
(500ms ×2 → 10s 上限 + 抖动),并有 connection/reset 事件在重连后重新拉取设置与会话inject: ['slots', 'connection']浏览器侧 cordis 的服务守卫对未声明的服务会抛
cannot get property "connection" without inject。如果 client 半体只写了 ['slots'],
ctx.connection 的访问会被这层守卫拒绝;一旦代码里用 try/catch 吞掉,症状就是
「面板能打开,但『立即重启』永远灰着」(面板上只有一行 connection-unavailable)。
三道防线(都别再退回去):
client/client.js 导出的 inject 必须包含 connection;ctx.get('connection') 兜底 —— ctx.get() 不走守卫,注入声明被漏掉也能用;scripts/build.mjs 的 client inject guard:扫描 ctx.<名> 访问,未声明且未用
ctx.get('名') 取的一律构建失败。新实例还会在状态目录里反查自己的「出生记录」(newInstance.pid === 自己),
所以即使浏览器整页重载,面板上依然能看到「本实例由快速重启启动(旧实例 PID …)」。
注意:这条判据不能要求记录已经是 succeeded——新实例加载时助手那侧通常还停在
waiting-for-ready,所以做成「加载时查一次 + 读状态时按 5s 节流补查」。
ctx.workflowEngine 没有 list():只能靠 workflow/start|end 事件计数,
进程外无法枚举正在跑的 workflow run。agent/status 覆盖。$DSH_HOME/.credentials.yaml 或设置页。ctx.appExit 缺失的组合里会降级为 process.exit(0)(有 libuv 断言风险,已在 GUI 明示)。CONTROL-assigned 被杀;Node detached 子进程被杀;
WMI 子进程存活 ✅。三次独立实验,仅 WMI 稳定存活。node --test test/unit.test.mjs test/state.test.mjs → 17 pass / 0 fail。lib/helper.js 生产代码:
父进程 = WmiPrvSE.exe,escapedJob = truestate = succeededinject: ['slots']):面板能打开,但「立即重启」disabled = true,
面板上写着 connection-unavailable —— 与用户报的现象一致,已复现并定位。disabled = false,面板显示 当前状态:idle、
启动方式:直接重放当前命令行 · 服务端口 3081,设置项中文标签正常。preparing-helper → 助手由 WMI 启动
(父进程 = WmiPrvSE.exe)→ ctx.appExit(0) 优雅退出 → 新实例起来 →
页面短暂显示「DSH 已重新连接。」→ 新实例 state = succeeded,
health.readyAt 与端口所有者 PID 都对得上。旧 PID 50460 → 新 PID 49296。status 返回 ok:true 且 capabilities.canStart = true。lib/client.js 后,宿主自动重算 combo rev
(三个不同的 rev 依次出现),浏览器刷新页面即生效,不需要重启 DSH。