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

快速重启 zhuifengqug/dsh-quick-restart

DeepSeek Harness
zhuifengqug48 天前有提交已提供中文文档扫描:含敏感能力

一键重启 DSH 进程,让新装插件与配置立即生效

27.1/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

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

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add zhuifengqug/dsh-quick-restart
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
dsh 原生插件 · other
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 48 天前
档位由哪些信号合成

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件
查看扫描证据(共 92 处,展示前 92 条)
  • 读取本地文件
    lib/flow.js:49
    entries = readdirSync(directory);
  • 读取本地文件
    lib/flow.js:58
    parsed = JSON.parse(readFileSync(file, 'utf8'));
  • 写入本地文件
    lib/log.js:11
    mkdirSync(dirname(path), {
  • 读取本地文件
    lib/log.js:14
    if (!statSync(path, {
  • 写入本地文件
    lib/log.js:16
    })) writeFileSync(path, '', 'utf8');
  • 写入本地文件
    lib/log.js:26
    appendFileSync(this.path, `${text}\n`, 'utf8');
  • 读取本地文件
    lib/log.js:31
    const text = readFileSync(this.path, 'utf8');
  • 读取本地文件
    lib/log.js:39
    const info = statSync(this.path, {
  • 读取本地文件
    lib/log.js:43
    const text = readFileSync(this.path, 'utf8');
  • 写入本地文件
    lib/log.js:44
    writeFileSync(this.path, `…(日志超过上限,已截断,保留最近部分)\n${text.slice(-TRUNCATE_TO_BYTES)}`, 'utf8');
  • 执行外部命令来自配置/变量
    lib/win.js:22
    execFile(exe, [
  • 读取本地文件
    lib/win.js:52
    if (!existsSync(candidate)) continue;
  • 执行外部命令来自配置/变量
    lib/win.js:55
    execFileSync(candidate, [
  • 写入本地文件
    lib/helper.js:99
    mkdirSync(runDir, {
  • 写入本地文件
    lib/helper.js:103
    copyFileSync(join(input.pluginRoot, 'helper', 'quick-restart-helper.mjs'), helperPath);
  • 写入本地文件
    lib/helper.js:112
    writeFileSync(specPath, JSON.stringify(spec, null, 2), 'utf8');
  • 写入本地文件
    lib/helper.js:114
    writeFileSync(launchScriptPath, renderWmiLaunchScript(specPath, pidFilePath), 'utf8');
  • 删除本地文件
    lib/helper.js:169
    rmSync(staged.pidFilePath, {
  • 执行外部命令来自配置/变量
    lib/helper.js:173
    execFile(powerShellPath, [
  • 读取本地文件
    lib/helper.js:194
    pidText = readFileSync(staged.pidFilePath, 'utf8').replace(/^ FEFF/, '').trim();
  • 读取本地文件
    lib/recipe.js:15
    const text = readFileSync(path, 'utf8');
  • 读取本地文件
    lib/recipe.js:98
    if (!existsSync(managedStatePath)) continue;
  • 读取本地文件
    lib/recipe.js:154
    const cliLooksLikeDsh = typeof ownCli === 'string' && ownCli.length > 2 && existsSync(ownCli) && /(^|[ /])bin js$|dsh/i.test(ownCli);
  • 读取本地文件
    lib/state.js:162
    text = readFileSync(this.path, 'utf8');
  • 写入本地文件
    lib/state.js:177
    mkdirSync(dirname(this.path), {
  • 写入本地文件
    lib/state.js:181
    writeFileSync(temporary, JSON.stringify(sanitizeRecord(record), null, 2), 'utf8');
  • 删除本地文件
    lib/state.js:183
    rmSync(this.path, {
  • 写入本地文件
    lib/state.js:187
    renameSync(temporary, this.path);
  • 删除本地文件
    lib/state.js:191
    rmSync(this.path, {
  • 写入本地文件
    lib/state.js:198
    mkdirSync(dirname(path), {
  • 写入本地文件
    lib/state.js:203
    writeFileSync(path, JSON.stringify(holder, null, 2), {
  • 删除本地文件
    lib/state.js:228
    rmSync(path, {
  • 删除本地文件
    lib/state.js:252
    rmSync(path, {
  • 读取本地文件
    lib/state.js:263
    const parsed = JSON.parse(readFileSync(path, 'utf8'));
  • 删除本地文件
    lib/state.js:273
    rmSync(path, {
  • 读取本地文件
    helper/quick-restart-helper.mjs:41
    spec = JSON.parse(readFileSync(specPath, 'utf8'))
  • 写入本地文件
    helper/quick-restart-helper.mjs:65
    appendFileSync(logPath, `${line}\n`, 'utf8')
  • 读取本地文件
    helper/quick-restart-helper.mjs:75
    return JSON.parse(readFileSync(statePath, 'utf8'))
  • 写入本地文件
    helper/quick-restart-helper.mjs:86
    mkdirSync(dirname(statePath), { recursive: true })
  • 写入本地文件
    helper/quick-restart-helper.mjs:88
    writeFileSync(temporary, JSON.stringify(next, null, 2), 'utf8')
  • 删除本地文件
    helper/quick-restart-helper.mjs:89
    rmSync(statePath, { force: true })
  • 写入本地文件
    helper/quick-restart-helper.mjs:90
    renameSync(temporary, statePath)
  • 执行外部命令来自配置/变量
    helper/quick-restart-helper.mjs:135
    const child = spawn(powerShell(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded], {
  • 执行外部命令来自配置/变量
    helper/quick-restart-helper.mjs:162
    const child = spawn(powerShell(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded], {
  • 删除本地文件递归
    helper/quick-restart-helper.mjs:211
    rmSync(runDir, { recursive: true, force: true })
  • 执行外部命令来自配置/变量
    helper/quick-restart-helper.mjs:255
    const child = spawn(cmd, ['/c', 'start', '', program, ...args], {
  • 读取本地文件
    lib/helper/quick-restart-helper.mjs:41
    spec = JSON.parse(readFileSync(specPath, 'utf8'))
  • 写入本地文件
    lib/helper/quick-restart-helper.mjs:65
    appendFileSync(logPath, `${line}\n`, 'utf8')
  • 读取本地文件
    lib/helper/quick-restart-helper.mjs:75
    return JSON.parse(readFileSync(statePath, 'utf8'))
  • 写入本地文件
    lib/helper/quick-restart-helper.mjs:86
    mkdirSync(dirname(statePath), { recursive: true })
  • 写入本地文件
    lib/helper/quick-restart-helper.mjs:88
    writeFileSync(temporary, JSON.stringify(next, null, 2), 'utf8')
  • 删除本地文件
    lib/helper/quick-restart-helper.mjs:89
    rmSync(statePath, { force: true })
  • 写入本地文件
    lib/helper/quick-restart-helper.mjs:90
    renameSync(temporary, statePath)
  • 执行外部命令来自配置/变量
    lib/helper/quick-restart-helper.mjs:135
    const child = spawn(powerShell(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded], {
  • 执行外部命令来自配置/变量
    lib/helper/quick-restart-helper.mjs:162
    const child = spawn(powerShell(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded], {
  • 删除本地文件递归
    lib/helper/quick-restart-helper.mjs:211
    rmSync(runDir, { recursive: true, force: true })
  • 执行外部命令来自配置/变量
    lib/helper/quick-restart-helper.mjs:255
    const child = spawn(cmd, ['/c', 'start', '', program, ...args], {
  • 读取本地文件
    src/flow.ts:181
    entries = readdirSync(directory)
  • 读取本地文件
    src/flow.ts:190
    parsed = JSON.parse(readFileSync(file, 'utf8')) as Partial<RestartRecord>
  • 写入本地文件
    src/helper.ts:186
    mkdirSync(runDir, { recursive: true })
  • 写入本地文件
    src/helper.ts:188
    copyFileSync(join(input.pluginRoot, 'helper', 'quick-restart-helper.mjs'), helperPath)
  • 写入本地文件
    src/helper.ts:197
    writeFileSync(specPath, JSON.stringify(spec, null, 2), 'utf8')
  • 写入本地文件
    src/helper.ts:199
    writeFileSync(launchScriptPath, renderWmiLaunchScript(specPath, pidFilePath), 'utf8')
  • 删除本地文件
    src/helper.ts:282
    rmSync(staged.pidFilePath, { force: true })
  • 执行外部命令来自配置/变量
    src/helper.ts:284
    execFile(
  • 读取本地文件
    src/helper.ts:298
    pidText = readFileSync(staged.pidFilePath, 'utf8').replace(/^ FEFF/, '').trim()
  • 写入本地文件
    src/log.ts:27
    mkdirSync(dirname(path), { recursive: true })
  • 读取本地文件
    src/log.ts:28
    if (!statSync(path, { throwIfNoEntry: false })) writeFileSync(path, '', 'utf8')
  • 写入本地文件
    src/log.ts:28
    if (!statSync(path, { throwIfNoEntry: false })) writeFileSync(path, '', 'utf8')
  • 写入本地文件
    src/log.ts:44
    appendFileSync(this.path, `${text}\n`, 'utf8')
  • 读取本地文件
    src/log.ts:53
    const text = readFileSync(this.path, 'utf8')
  • 读取本地文件
    src/log.ts:62
    const info = statSync(this.path, { throwIfNoEntry: false })
  • 读取本地文件
    src/log.ts:64
    const text = readFileSync(this.path, 'utf8')
  • 写入本地文件
    src/log.ts:65
    writeFileSync(this.path, `…(日志超过上限,已截断,保留最近部分)\n${text.slice(-TRUNCATE_TO_BYTES)}`, 'utf8')
  • 读取本地文件
    src/recipe.ts:91
    const text = readFileSync(path, 'utf8')
  • 读取本地文件
    src/recipe.ts:220
    if (!existsSync(managedStatePath)) continue
  • 读取本地文件
    src/recipe.ts:282
    && existsSync(ownCli)
  • 读取本地文件
    src/state.ts:207
    text = readFileSync(this.path, 'utf8')
  • 写入本地文件
    src/state.ts:224
    mkdirSync(dirname(this.path), { recursive: true })
  • 写入本地文件
    src/state.ts:226
    writeFileSync(temporary, JSON.stringify(sanitizeRecord(record), null, 2), 'utf8')
  • 删除本地文件
    src/state.ts:228
    rmSync(this.path, { force: true })
  • 写入本地文件
    src/state.ts:232
    renameSync(temporary, this.path)
  • 删除本地文件
    src/state.ts:238
    rmSync(this.path, { force: true })
  • 写入本地文件
    src/state.ts:275
    mkdirSync(dirname(path), { recursive: true })
  • 写入本地文件
    src/state.ts:278
    writeFileSync(path, JSON.stringify(holder, null, 2), { encoding: 'utf8', flag: 'wx' })
  • 删除本地文件
    src/state.ts:294
    rmSync(path, { force: true })
  • 删除本地文件
    src/state.ts:308
    rmSync(path, { force: true })
  • 读取本地文件
    src/state.ts:316
    const parsed = JSON.parse(readFileSync(path, 'utf8')) as LockHolder
  • 删除本地文件
    src/state.ts:328
    rmSync(path, { force: true })
  • 执行外部命令来自配置/变量
    src/win.ts:65
    execFile(
  • 读取本地文件
    src/win.ts:89
    if (!existsSync(candidate)) continue
  • 执行外部命令来自配置/变量
    src/win.ts:92
    execFileSync(candidate, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', 'exit 0'], {
扫描于 2026/11/3(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

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

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

为 DeepSeek Harness 提供快速重启命令的插件

README

由 DeepSeek 最新模型翻译生成
dsh-quick-restart 在 DSH Web GUI 里点一下,就重启整个 DSH 运行进程——让新安装 / 更新 / 改过配置的插件与配置立刻生效。 不是刷新浏览器、不是只重载本插件、也不新建第二个实例。 - 入口:侧栏「设置」旁边的 快速重启 按钮(另有设置页左侧导航里的 快速重启 DSH 整页) - 重启期间 Edge 窗口保持打开,服务恢复后由 DSH 自己的连接控制器自动重连 - 有任务在跑时先等任务结束,超时只提示;强制重启必须由用户明确确认 一、为什么必须有一个「外部助手」 插件跑在 DSH 进程内部,一旦 DSH 退出就什么都不剩了,没有组件还能启动新实例。 所以本插件只负责妥善收尾并把接力棒交出去:一个不依赖 DSH 存活的 detached 助手负责「等旧实例退出 → 等端口释放 → 起新实例 → 健康检查 → 写回结果 → 自删退出」。 关键约束:Job Object 本机 DSH 由 D:\dsh\dshstart\Start-DSH-GUI.ps1 托管,该脚本用 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE(0x2000) 创建 Job Object,脚本结束时关闭 Job 句柄, Job 内所有进程一起被终止。
展开完整文档(README 剩余 1 节)
dsh-quick-restart

在 DSH Web GUI 里点一下,就重启整个 DSH 运行进程——让新安装 / 更新 / 改过配置的插件与配置立刻生效。
不是刷新浏览器、不是只重载本插件、也不新建第二个实例。

- 入口:侧栏「设置」旁边的 快速重启 按钮(另有设置页左侧导航里的 快速重启 DSH 整页)
- 重启期间 Edge 窗口保持打开,服务恢复后由 DSH 自己的连接控制器自动重连
- 有任务在跑时先等任务结束,超时只提示;强制重启必须由用户明确确认

一、为什么必须有一个「外部助手」

插件跑在 DSH 进程内部,一旦 DSH 退出就什么都不剩了,没有组件还能启动新实例。
所以本插件只负责妥善收尾并把接力棒交出去:一个不依赖 DSH 存活的 detached 助手负责「等旧实例退出 → 等端口释放 → 起新实例 → 健康检查 → 写回结果 → 自删退出」。

关键约束:Job Object

本机 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.exit

DSH 没有「重启进程」接口,也没有全局 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 没有「停止接收新任务」的全局开关。能用的官方局部闸门 + 等待语义:

1. ctx.goals.disarm(agent) —— 撤销自动续跑授权(否则 goal 会在 turn 结束后自己再起一轮,永远等不到静止);
2. ctx.subagents.drainContinuableDescendants(agents) —— 关闭可续跑子代理接纳;
3. 等 agent.whenIdle() + 对账 ctx.jobs 注册表,上限 gracefulShutdownTimeoutMs;
4. 超时 → 进入 confirming-force-restart,只提示、不自动强杀;
5. 用户明确确认后: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 的补丁文件)。
手工安装就是把下面三件事做完:

1. <DSH_HOME>/profiles/web/package.json → "dsh-quick-restart": "link:<插件目录绝对路径>" (发布到 npm / GitHub 后也可写成版本号或 github:zhuifengqug/dsh-quick-restart)
2. <DSH_HOME>/profiles/web/cordis.patch.yml → - insert: { id: dsh-quick-restart, name: dsh-quick-restart }
(即仓库根目录的 cordis.patch.yml)
3. <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 被复用)。

六、GUI 侧

- sidebar.footer.action:侧栏常驻按钮,窄栏只留图标但保留 aria-label / title
- settings.section:设置页整页(同一套组件)
- 与宿主通信走 DSH 自己认证过的 Connection RPC 通道 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)。

三道防线(都别再退回去):
1. client/client.js 导出的 inject 必须包含 connection;
2. 取服务用 ctx.get('connection') 兜底 —— ctx.get() 不走守卫,注入声明被漏掉也能用;
3. scripts/build.mjs 的 client inject guard:扫描 ctx.<名> 访问,未声明且未用
ctx.get('名') 取的一律构建失败。

新实例还会在状态目录里反查自己的「出生记录」(newInstance.pid === 自己),所以即使浏览器整页重载,面板上依然能看到「本实例由快速重启启动(旧实例 PID …)」。
注意:这条判据不能要求记录已经是 succeeded——新实例加载时助手那侧通常还停在 waiting-for-ready,所以做成「加载时查一次 + 读状态时按 5s 节流补查」。

七、已知边界(做不到的部分)

1. 没有全局 drain 闸门:等待任务期间 DSH 仍可能接到新任务(例如用户手动发消息)。
我们只能等,不能冻结;因此等待是有上限的。
2. 工作流 ctx.workflowEngine 没有 list():只能靠 workflow/start|end 事件计数,进程外无法枚举正在跑的 workflow run。
3. schedule 定时任务没有独立可观测面;它触发时走 agent turn,已被 agent/status 覆盖。
4. 非本机 subagent provider 的运行只能靠事件计数,无法直接查询。
5. 环境变量按名字过滤:名字像凭据的变量不会随重启传递(只报告名字)。
依赖环境变量注入的供应商密钥请改写到 $DSH_HOME/.credentials.yaml 或设置页。
6. 强制重启只能作用于当前实例:DSH 自身的 cancel/kill 接口;不按进程名批量杀进程。
7. ctx.appExit 缺失的组合里会降级为 process.exit(0)(有 libuv 断言风险,已在 GUI 明示)。

八、验证记录(本机 Windows 11 26200 / DSH 0.1.5-rc.1)

- Job Object 逃逸(合成 Job + 对照组):CONTROL-assigned 被杀;Node detached 子进程被杀; WMI 子进程存活 ✅。三次独立实验,仅 WMI 稳定存活。
- 单元测试:node --test test/unit.test.mjs test/state.test.mjs → 17 pass / 0 fail。
- 端到端重启(隔离实例,端口 3081,独立 DSH_HOME),用插件自己的 lib/helper.js 生产代码:
- 助手由 WMI 创建,父进程 = WmiPrvSE.exe,escapedJob = true
- 杀掉旧实例 → 启动器 Job 关闭 → 助手存活并接管
- 端口释放 → 拉起新实例 → 健康检查 HTTP 401 + 端口所有者 PID 指纹匹配 → state = succeeded
- 新实例 PID 与状态文件记录一致;助手自行退出,运行目录已清理
- 客户端半体(无头 Edge + CDP 真实驱动 GUI):
- 修好前(inject: ['slots']):面板能打开,但「立即重启」disabled = true,面板上写着 connection-unavailable —— 与用户报的现象一致,已复现并定位。
- 修好后:按钮 disabled = false,面板显示 当前状态:idle、 启动方式:直接重放当前命令行 · 服务端口 3081,设置项中文标签正常。
- 在真实 GUI 上点「立即重启」→ 确认:preparing-helper → 助手由 WMI 启动(父进程 = WmiPrvSE.exe)→ ctx.appExit(0) 优雅退出 → 新实例起来 → 页面短暂显示「DSH 已重新连接。」→ 新实例 state = succeeded, health.readyAt 与端口所有者 PID 都对得上。旧 PID 50460 → 新 PID 49296。
- 新实例面板显示「本实例由「快速重启」启动(旧实例 PID 50460,… UTC)」。
- RPC 通道:无 Cookie 时返回 401(已注册且受 DSH 鉴权保护);带 DSH 的会话 Cookie 调用 status 返回 ok:true 且 capabilities.canStart = true。
- 客户端 bundle 热更新:改 lib/client.js 后,宿主自动重算 combo rev (三个不同的 rev 依次出现),浏览器刷新页面即生效,不需要重启 DSH。
- 主实例(3080)全程未重启、未被影响(结束时仍 HTTP 200,PID 39244 存活)。
预览分享卡片长什么样 ↓zhuifengqug/dsh-quick-restart
分享 · DeepSeek Harness Hub
zhuifengqug/dsh-quick-restart

一键重启 DSH 进程,让新装插件与配置立即生效

这是你的仓库?认领条目

回到顶部