仅 Windows 的进程控制插件:侧边栏按钮经宿主的优雅退出通道关闭 DSH;插件卡片可切换「应用窗口 / 普通标签页」——会改写桌面快捷方式并重启;另有退出前等待、进程退出码与强制结束三项高级设置。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 中文
DeepSeek Harness(DSH)插件,提供两项功能:
- 关闭进程:侧边栏页脚的
⏻按钮(二次确认)经 DSH 自身的优雅退出通道关闭本机dsh web进程,会话与设置先落盘。 - 启动方式切换:设置 → 插件页卡片上的切换按钮,在「应用窗口 / 普通标签页」之间切换下次启动方式。单次点击完成:保存设置 → 更新桌面快捷方式 → 重启 DSH。
平台支持
仅支持 Windows。package.json 声明 "os": ["win32"],其他平台无法安装。
应用窗口模式依赖桌面快捷方式(Windows Script Host 与 .lnk),当前没有等价实现。关闭进程功能本身不依赖平台(通过 ctx.appExit,不调用 shell),但为避免在 macOS/Linux 上安装后核心功能静默失效,本包明确声明不支持;后续如需支持,需补充 open/xdg-open、macOS 浏览器路径与 .desktop 等价实现。
环境要求
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows 10 或更高 |
| DSH | >=0.1.6-alpha.2 <0.1.7-0 || >=0.1.7-0 <0.2.0-0 || >=0.2.0-0 <0.3.0-0(见 package.json 的 engines.dsh) |
| Node.js | >=20 |
| Windows 脚本宿主 | 桌面快捷方式层需要 cscript.exe / wscript.exe。它可能被移除,或被杀毒软件、攻击面缩减(ASR)规则、组策略拦截——受管控的机器上这是真实情形,不是假设。遇到这种情况卡片会明确说明,而关闭进程、设置表单与模式切换本身不受影响。 |
这个 DSH 范围按小版本线逐段列出,并不是装饰:DSH 至今只发布过预发布版,而 semver 只允许预发布版进入「同一 major.minor.patch 元组上带有预发布比较符」的范围——所以 *、>=0.1.0 以及单写一个 >=0.1.6-alpha.2 都放不进任何一个真实存在的 DSH 版本。下界取第一个自带 @deepseek-ai/dsh-plugin-manager 的 DSH 版本:在那之前既没有 dsh plugin install,也没有 dsh.bundle 这一层,本包根本无法安装。实测于 0.1.6-alpha.2(全套测试)、0.1.7 线与 0.2.0-rc.2(桌面版);DSH 将来出现新的小版本线时,这里需要再加一段。
0.2.0-* 那一段是核对过实现的,不是照抄版本号:0.2.0-rc.2 里的 @deepseek-ai/dsh-host-webserver 仍是 extends Service 上的 webServer 服务,register({ kind, path, handler }) 的签名、重复路由抛错、返回注销函数、port getter 均未变,其 @deepseek-ai/schemastery 依赖(~3.18.4)也落在本包声明的 >=3.18.1 <4 内。
安装
你应该先cd到deepseek harness文件夹
下面三条命令完全等价,区别只在"如何调用 DSH":
# ① 已安装 dsh 命令(npm 全局安装或桌面版)
dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch#v1.1.10
# ② 从源码检出运行,且检出已构建(存在 apps/cli/lib/bin.js)
# 需在检出根目录执行
node apps\cli\lib\bin.js plugin --profile web add github:SanYe-SanJiu/dsh-power-switch#v1.1.10
# ③ 从源码检出运行,未构建或希望直接跑 TypeScript 源码
# 需在检出根目录执行;官方开发文档(docs/user/develop/basic/publish.md)
# 对源码检出的写法就是这条
pnpm dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch#v1.1.10
三种入口完全等价,本文其余命令都可照此替换(把 dsh 换成 node apps\cli\lib\bin.js 或 pnpm dsh)。
其它写法:
# 跟随分支:安装时解析当时的 main。pnpm 按 ref 缓存 git 安装,重复 add 也不保证刷新
<入口> plugin --profile web add github:SanYe-SanJiu/dsh-power-switch
# 从本地检出安装(用于开发;link: 保持指向检出目录,不复制)
<入口> plugin --profile web add link:<本包检出的绝对路径>
说明:
--profile为必填项;Web UI 对应的 profile 名为web(dsh web等价于dsh --profile web)。桌面版对应的 profile 名为desktop(见下文「桌面版」)。apps/cli/lib/bin.js是pnpm build的产物、不在仓库中,因此刚克隆的检出用 ③。add之后的参数原样转发给 pnpm,因此 npm 包名、github:owner/repo[#ref]、link:路径与 tarball URL 均可使用。相对路径(./x、../x、link:../x)以执行命令时的工作目录为基准解析,而非以 profile 目录为基准。- 安装完成后,DSH 会自动将该包加入 profile 的
dsh.profile.bundles(该包声明了dsh.bundle.patch),无需手工编辑 JSON。未声明 bundle patch 的包会收到"仅作为普通依赖安装"的提示。 - 仓库中包含构建产物
lib/,因此从 GitHub 安装不需要构建步骤,也不会触发 pnpm 的 allowBuilds 拦截。 - 发布遵循只加不改:修复以新版本号发布(
1.1.1、1.1.2…),已发布的 tag 绝不移动、删除后重指,因此钉住的安装不会在使用者脚下变化。 - 包自带展示元数据,格式按 DSH 0.1.7 的读法:
package.json.icon(清单相对路径的 SVG)与locale/<语言>.json里的meta块(插件列表显示的标题与简介)。两者都通过exports导出(./locale/*.json)以便解析器取到;更早的 DSH 版本会直接忽略这些字段。
安装失败时的两种情形
① ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION
profile 中存在发布未满 pnpm 等待期的插件版本时,pnpm 会在任何改动前校验整个锁文件,导致所有插件操作被拒绝(包括卸载其他插件)。可对本次命令放行:
dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch --config.minimum-release-age=0
必须使用连字符拼写:驼峰形式 --config.minimumReleaseAge=0 在 pnpm ≥ 12.3 中会被静默忽略,开关看似生效但报错不变。另不建议按提示执行 pnpm clean --lockfile,该命令会重新解析整个 profile,一并更换其他插件版本。
② 网络较慢
github: 安装需拉取整个仓库,可能超过 pnpm 默认 60 秒的抓取超时。追加第二个一次性开关:
dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch --config.minimum-release-age=0 --config.fetchTimeout=600000
安装完成后重启一次 DSH,打开 设置 → 插件:列表中会出现标签为「DSH 电源按钮」的卡片,侧边栏页脚会出现 ⏻。
安装校验与卸载
- 校验:profile 的
dsh.profile.bundles中包含本插件,且node_modules\dsh-power-switch为普通目录(本地link:安装为符号链接或 junction)。 - 卸载:
dsh plugin --profile web remove dsh-power-switch。桌面快捷方式不依赖包,因此无需先做任何还原——但若当前是应用窗口模式,想让原有启动方式立刻回来,就先在卡片里切回普通标签页再卸载。否则下一次双击图标就会自动完成:快捷方式所指的副本发现包已不在,会把原快捷方式放回去并弹窗说明。状态目录中的restore-shortcut.vbs是同一件事的手动入口。 - 切换回本地检出:先卸载,再执行
dsh plugin --profile web add link:<本包检出的绝对路径>;两步均建议附带上述一次性开关。 - 同一 profile 中不要同时安装 GitHub 版本与本地
link:版本:包名相同,后安装者会替换先安装者。
dsh plugin会写入$DSH_HOME/profiles/web/;若 dsh 运行于沙箱环境,请在普通终端中执行。
关闭进程
侧边栏 ⏻、卡片中的「关闭 DSH 进程」按钮,以及 POST /api/dsh-power-switch/shutdown 路由,三者行为一致。两个控件都会先弹出确认框,而确认框里同时提供重启选项(行为见重启):
- 先返回响应,再开始退出(页面因此能收到「已发出关闭请求」;超过 10 秒无响应则报告失败);
- 走优雅退出:会话与设置先落盘,插件树先卸载;
- 若优雅退出停滞,看门狗在超时后强制结束进程。
appExit 服务在宿主启动过程中逐步就绪。服务刚启动、该服务尚未注册时触发关闭,插件会退回 SIGTERM(实测日志为 no appExit service; sending SIGTERM to self)。如需确保优雅退出,可在启动数秒后再执行关闭。
应用窗口模式下,进程退出后页面自动关闭;普通标签页模式下,浏览器不允许页面关闭由用户打开的标签页,因此卡片提示按 Ctrl+W。这属于浏览器行为限制。
"进程退出"就是窗口关闭的时刻,这也正是 delayMs 看得见的原因:等待设为 10 秒时,窗口会停留约 10 秒,然后与它所属的进程一起关闭。两个电源控件都遵循这条规则;侧边栏按钮以前在宿主接受请求时就关窗口——那是另一个时刻,等待一旦真实生效,这个差别就立刻显得像故障。
重启
重启是两个电源控件弹出的确认框中的第三个选项(取消 / 重启 DSH / 关闭),也是"切换启动方式"这一动作的最后一步;当你只想替换掉即将被取代的那次启动时,它同样可以单独使用:
# 双击 scripts\restart-dsh-web.vbs,或直接运行脚本
node scripts\restart-dsh-web.mjs --delay-seconds 3
确认框里的选项与脚本都走 POST /api/dsh-power-switch/restart;确认框不携带模式,因此它按当前已配置的模式重启——只替换进程,不碰桌面快捷方式。
手动入口会在宿主实际服务的端口上停掉它,按记录的启动命令重新启动,等待该次运行输出的 token URL,然后证明本插件的浏览器半已进入新宿主提供的启动图。--port N 可覆盖它工作的端口、--cli <入口> 可在没有记录的机器上指定 CLI;--open / --app 可在之后打开窗口。
两个入口共用一条规则:替代进程必须先确认自己就位,宿主才被允许退出;确认不到时宿主继续服务,并在响应里说明原因——明确拒绝永远好过服务再也起不来。"记录的启动命令"指什么、以及哪些情形会直接拒绝,见启动方式一节,同一套规则适用于这个入口。
启动方式:应用窗口 / 普通标签页
| 应用窗口 | 普通标签页 | |
|---|---|---|
| 打开方式 | Chromium --app=(无地址栏与标签栏) |
默认浏览器的标签页 |
| 窗口尺寸 | 铺满所在屏幕的工作区 | 由浏览器决定 |
| 关闭进程后 | 页面自动关闭 | 需按 Ctrl+W |
窗口尺寸由页面设定,而不是由启动命令行设定——这是实测结论,不是偏好。Chromium 浏览器只要已有实例在运行(DSH 显示在屏幕上时必然如此),就会把 URL 交给那个实例,由它按自己记住的尺寸创建窗口,根本不会读交出 URL 的那条命令行的开关:在 2048×1152 的屏幕上,裸 --app=<url> 实测 1010×1084,而 --start-maximized、--window-size=1920,1080 --window-position=0,0、--kiosk、--start-fullscreen 实测都是同样大小的半窗。真正有效的是页面移动并调整自己的 app 窗口(同一组实测里 resizeTo 把 1010×1085 变成 1202×702)。
正因为如此,启动器把窗口开在本插件自己的页面上,而不是直接开在 DSH 上:/api/dsh-power-switch/app-window?next=<认证 URL> 的第一个动作就是把窗口铺满工作区,随后把自己替换成那个 URL。直接开 DSH 会让窗口以浏览器默认尺寸显示 DSH 启动所需的整段时间——也就是"先半屏、后跳满"那一下。用了交接页之后,窗口出现时就已经是填满的:在启动前开始、每 40ms 采一次尺寸的监视脚本,只观测到一个尺寸 2048×1104 于 0,0,且交接已经完成。该页面只接受带回环 http 且带 token 的 URL(浏览器会跟随它的重定向),使用者自己打开或安装的窗口与此无关;若浏览器拒绝了首次调整,client.js 会在 DSH 起来后再试一次。
铺满工作区已经是自动动作的极限:含任务栏的真全屏需要一次用户手势,浏览器不允许页面自作主张(实测:加载时 requestFullscreen() 被拒;真实点击后同一调用可到 2048×1152;F11 则由浏览器完成)。想要真全屏时在 app 窗口里按 F11 即可——本插件不会替你按键。
卡片上的切换按钮一次点击完成三件事:保存设置、更新桌面快捷方式、重启 DSH。设置写入 $DSH_HOME/settings.yaml 的 dsh-power-switch 段,同时更新组合入口,因此宿主的 settings 服务不可用时,重启后仍按所选方式打开。
两代设置模型都能用
DSH 0.1.7 更换了插件设置模型:ctx.settings 变成生成式表单服务(describe / update),不再有 register / installSection;同时 settings.yaml 退役——它会被一次性导入当前 profile 并改名。这两项变化都不会破坏本插件:
- 持久化路径是探测出来的,不靠版本号猜:宿主若仍有"注册命名空间"那套 API,就通过其写作用域写入;若只有新服务,则通过
update写入本插件自己的条目——该条目按"活动配置的形状"匹配,而不是猜一个 id; - 选择同时由插件自己记录在
launch-mode.txt,而所有运行在宿主之外的进程(桌面启动器、重启助手、监督进程)都读这份记录。因此即使宿主的设置文档已不存在,模式依然能跨冷启动保留——否则"切成应用窗口,下次启动却又是标签页"就会重现; - 唯一有差异的是设置界面:0.1.7 的表单由 Loader 条目声明的 schema 生成,而本插件未声明 schema,因此那里没有属于本插件的原生设置页。插件可配置的一切都在卡片上——启动方式开关与高级设置。
无法安全重启时会拒绝执行
重启助手必须先向宿主确认接管,宿主才会退出;确认未到达时,路由返回 500 并继续提供服务。三种拒绝原因分别对应卡片上的一条说明:
- 当前 DSH 不是以
dsh web命令行方式启动(例如桌面版),无法重放启动命令; - 重启助手未能启动;
- 重启助手已启动但未确认就位。
任一情形下,关闭进程功能不受影响,且设置已经保存。
桌面快捷方式
切换到应用窗口时接管已有的 DSH 快捷方式(不存在时新建 DSH 启动器),切回普通标签页时还原为原有启动方式。接管前的目标、参数、图标与描述记录在状态目录的 shortcut-backup.txt 中,可随时还原。
快捷方式指向的不是包内的启动器,而是状态目录里的副本 shortcut-launch.vbs——它每次启动都会写在那里,与上述记录和 restore-shortcut.vbs 放在一起。一个"插件一卸载就失效"的图标不值得放到桌面上,所以图标所指向的文件必须是卸载动不了的那个。双击它时:
- 插件还在——harness home 下的某个 profile 在其清单里仍列着它(
<profile>\package.json的dsh.profile.bundles,即宿主加载插件的那个列表),并且该 profile 里存在<profile>\node_modules\dsh-power-switch\scripts\launch-dsh.vbs。副本把本次启动收到的参数原样交给那个启动器、等待并回传其退出码。实际启动路径仍是launch-dsh.mjs(外壳launch-dsh.vbs),因此正常启动毫无变化;旧版本接管过的图标也会在下次启动时被改指到这个副本。 - 插件已不在——没有任何 profile 再列着它,于是通过
restore-shortcut.vbs重放记录:快捷方式回到本插件从未碰过它的样子(若是本插件新建的则删除),并弹出一个对话框说明。再双击一次,就按你原来的方式启动 DSH。
判定以清单为准,既不是"记录下来的那条路径",也不是"那个目录还在不在"——这两点都是实测出来的,不是假设。link: 安装被卸载时,清单会被改写,但 node_modules 里的 junction 会留下来;因此"启动器文件仍能解析"并不能证明插件还装着,把它当成证据正是"图标继续启动一个已卸载的插件、还原永远不发生"的成因。早期版本写下的记录路径有同样的失效方式,所以启动时会把它删掉。
本次启动究竟属于哪个 harness home,靠查而不是靠选定:环境变量 DSH_HOME、快捷方式携带的 --home、默认的 ~/.dsh 会逐个搜索,看谁的 profile 清单里仍列着本插件,找到的那个即为准,并且导出的也是它,这样外壳读到的状态目录与被执行的启动器属于同一个 home。因此"快捷方式建好之后 DSH_HOME 又变了"不会让已安装的插件看起来像被卸载,图标也就不会在插件还在时装作插件已消失。
这一层里没有任何写死的路径。shortcut-launch.vbs 用的是自身所在目录、上面那个 home、DSH 的 profiles 目录(CLI 解析 --profile 用的同一个常量)以及 <profile>\node_modules\<包名>——即 pnpm 放置直接依赖、且 DSH 明确说明"pnpm 管理的条目保持权威"的位置。restore-shortcut.vbs 用的是自身所在目录,其次 %DSH_HOME%,再其次 %USERPROFILE%\.dsh;它改写的 .lnk 就是记录里那一个。make-shortcut.vbs 向 shell 询问 Desktop 而不是自行拼接(因此重定向过的、非英文的桌面同样可用),写入的目标是 %SystemRoot%\System32\wscript.exe 并带回退的裸文件名。这些全部在运行时推导,任何用户名、盘符或目录下的安装行为一致。
这些对话框用使用者所用 Windows 的语言显示——因为它就是一个 Windows 对话框:脚本读取 HKCU\Control Panel\International\LocaleName,读不到就退回英文;DSH_POWER_SWITCH_LANG=zh|en 可覆盖两者。译文放在状态目录的 shortcut-messages.txt 里,而不是脚本内部,因为脚本存不下它:wscript 按 ANSI 读取 .vbs,中文写进去会变成乱码;而把脚本改成 UTF-16 又会让它在版本库里变成无法审阅的二进制。表里不重复英文:英文就是各个调用点里写着的文本,也是键或整个文件缺失时的回退。
需要这一层的原因:dsh web 将 URL 交给默认浏览器,因此始终打开标签页;它没有应用窗口选项,且该交接由使用清空环境变量的平台 opener 启动,插件无法拦截。因此「下次启动的窗口形态」只能由启动 dsh 的一方决定,即本包提供的启动器 scripts/launch-dsh.mjs(外壳 launch-dsh.vbs):读取设置中的模式 → 未运行服务时按记录的启动命令启动 → 等待宿主输出 token → 按模式打开窗口。
「按记录的启动命令」指插件运行时将自身的启动信息(process.execPath、argv、cwd)写入状态目录的 boot.json,启动器原样重放。早期实现为拼接 <检出目录>/apps/cli/lib/bin.js——该路径仅存在于 DSH 源码检出中,其他安装方式下必然失败。
冷启动无法自行得知的两项事实,一律推导或记录,绝不假设。端口取自宿主自己写下的记录:上次运行记录的认证 URL,其次为记录的启动命令中的 --port,最后才是文档中的默认值;等待宿主就绪信号时不限定端口,因此在非默认端口上服务的 DSH 会被正常启动并打开窗口,而不是在它从未使用过的端口上空等两分钟后失败。DSH home 作为 --home <目录> 写入快捷方式,并由外壳下传为 DSH_HOME(环境中已有该变量时以环境为准),因此从资源管理器双击快捷方式时,读取的状态目录与宿主一致——即使 DSH_HOME 只在该用户自己的终端里定义过。
启动失败会明确报出。外壳会等待启动器结束,退出码非 0 时弹出对话框,显示退出码、日志末尾与日志路径:快捷方式是被双击的,而静默退出的外壳与「快捷方式根本没接上」在现象上无法区分。
启动器也可直接在命令行使用:
node scripts/launch-dsh.mjs # 按设置中的模式
node scripts/launch-dsh.mjs --app # 本次强制应用窗口
node scripts/launch-dsh.mjs --tab # 本次强制普通标签页
node scripts/launch-dsh.mjs --cli <dsh CLI 入口路径> # 指定 CLI 入口
启动器不会重复启动实例:已有宿主在运行时仅打开窗口;端口有响应但无法获取有效 token 时拒绝启动并输出原因。
桌面版
DSH 的桌面版(DeepSeek Harness.exe,Electron 打包)用 --profile desktop,与本插件的 web profile 是两套独立安装。本插件可以装进桌面版 profile,且从 1.1.8 起关闭进程、从 1.1.9 起重启,在桌面版下都是真正可用的——但走的是与命令行不同的通道。
为什么通道必须不同。 桌面版的宿主进程是 Electron 主进程 fork 出来的子进程。它的 ctx.appExit 就是 DSH 的 shutdown(code),而实测 @deepseek-ai/dsh 0.2.x 的实现是:
shutdown(code){ return start(code, false) } // forceAfterDispose = false
// → completeOnce(code),即 process.exitCode = code
它只设置退出码、不调用 process.exit;只有 dispose 卡住超过 5 秒才会强制结束。命令行宿主能正常退出,是因为 CLI 主进程处理完就结束了;而 Electron 主进程的事件循环由 app 持有,设个退出码不会让它退出。结果是:路由回了成功、页面显示「正在关闭」、DSH 却还在跑。
桌面版自己的停止通道是 IPC。Electron 主进程发 { type: 'shutdown' },dsh-desktop-host 收到后执行 await shutdown.shutdown(0)、回报 { type: 'shutdown-complete' }、再 process.disconnect()。插件 1.1.6 曾先走这条通道(1.1.9 已移除,原因见下):isDesktopHost() 以「存在 process.send 且 connected === true」判定,requestDesktopShutdown() 发出上面那条消息;通道不可用时才退回 appExit → SIGTERM 的原有路径。
之所以用 IPC 通道而不是可执行文件名来判定:源码检出、打包安装、自定义安装的可执行文件名各不相同,而 IPC 通道是「这个进程是被 fork 出来的」这一事实本身,命令行宿主没有它。
但 IPC 通道不够,这是 1.1.8 修掉的问题。 { type: 'shutdown' } 最终仍然落到上面那个 shutdown() 上——设退出码、清掉自己的看门狗、然后保持存活。实测在打包的 0.2.0-rc.2 上:路由回 ok、页面显示「正在关闭」、DSH 还在跑。所以桌面版需要 DSH 树之外的终止者,插件现在的做法是:
- 起一个 PowerShell 启动器(
windowsHide,从创建那一刻起就没有窗口),它把助手交给 WMI 创建——这样助手不在应用的进程树里,不会随它一起被结束; - 助手先只读探针确认
process.ppid那个进程存在、且名字与本进程的可执行文件同名(DeepSeek Harness.exe→DeepSeek Harness);名字不对就不杀; - 确认后对主进程调终止。杀主进程而不是杀宿主:宿主是
fork出来的子进程,结束主进程一步关掉窗口、宿主和所有渲染进程,不留孤儿(实测:3 个子进程 + 主进程被杀,零残留)。反过来杀宿主,主进程会判定「子进程崩溃」并弹一个dsh desktop host stopped的模态框——正是这条路要避免的。
1.1.8 曾经在终止之前先发一条 IPC { type: 'shutdown' }。这一步在 1.1.9 被删掉了:主进程收到它就先把宿主停掉,助手往往还没开始跑,宿主就已经死了。结果是主进程把子进程退出判为失败、弹出崩溃框,而窗口还在——日志里表现为 desktop host: 之后一行都没有。先发 IPC 买到的是一段延迟,不是一次干净的退出。
实测取舍(同一台机器,重复多次):
| 做法 | 结果 |
|---|---|
Python TerminateProcess 打主进程 |
✅ 成功,无残留 |
PowerShell Process.Kill() 打主进程 |
✅ 成功,无残留 |
VBScript 经 WMI Win32_Process.Terminate 打主进程 |
✅ 成功,100 ms 内消失——助手现在走这条 |
taskkill /PID <主进程> /F |
❌ ERROR: Access denied |
| 杀光全部子进程 | ⚠️ 整个应用会退出,但主进程先弹一个崩溃框 |
| 向主进程发 IPC | ❌ 无退出通道(19 个通道里没有关闭/重启入口) |
taskkill 被拒与沙箱无关:打包的桌面版会装一条 dshMandatoryUpdatePolicy 限制辅助进程能动什么,而带着真实句柄的 TerminateProcess 不受它约束,PowerShell 的 Process.Kill() 走的正是这条。
助手为什么是 VBScript 而不是 PowerShell,这是 1.1.9 修掉的问题。 由 WMI 创建的进程会得到一个新控制台。powershell.exe 属 CONSOLE 子系统,于是重启过程中屏幕上会闪一个黑色命令行窗口、任务栏也会多出它——-WindowStyle Hidden 只是把一个已经创建出来的窗口隐藏掉,隐藏之前的那一帧仍然看得见。反过来,wscript.exe 属 GUI 子系统,根本不创建控制台,所以既没有可闪的窗口,也没有可隐藏的东西;助手要做的那些事(等待、查 pid、终止、重新启动)VBScript 全都能做,而且都不需要控制台。
也给 WMI 传过 CREATE_NO_WINDOW(Win32_ProcessStartup.CreateFlags = 0x08000000,这是唯一「连创建都不创建」的保证)。三种写法在本机都是 ReturnValue=21——参数被拒。所以绕开了它。
还有一个容易被忽略的连带问题:助手最初用 Start-Process 启动 DSH,新实例会继承助手的控制台,于是关掉那个黑色窗口就等于给所有 attach 到它的进程发 CTRL_CLOSE_EVENT,DSH 跟着退出。现在 DSH 也由助手经 WMI 启动,不继承任何控制台,也不属于任何 job 对象——关别的窗口不会带走它。
重启(1.1.9 起)。 复用的正是上面那个终止者:它不重放命令行,而是按固定顺序走完四步,最后用同一个可执行文件启动新实例。
| 步骤 | 为什么必须在这一步 |
|---|---|
| 1. 杀主进程 | 窗口在这里消失 |
| 2. 杀宿主 | 宿主是主进程 fork 出来的子进程,父进程死后它会成为孤儿并占着端口,新实例会撞上 |
| 3. 等 | Electron 退出时才释放单实例锁;起早了会被静默当成「已在运行」丢弃 |
| 4. 启动可执行文件 | 同一个文件、同一个工作目录 |
第 2、3 步之间是轮询而不是固定睡眠:进程有没有退出是可以检查的事实。第一版固定睡 1200 ms 再睡 2500 ms,每次重启约 4.5 秒;现在实测通常 1 秒左右完成。
第 4 步有守卫:可执行文件必须仍在磁盘上,否则宁可保持关闭也不半启动——这一点会明确告诉使用者,因为「重启失败」实际上就是「关闭」。
1.1.10 起,助手不再把「调用没报错」当成「启动成功」。 Shell.Run 不返回句柄,所以 1.1.9 写 helper: started 时,它只知道调用没有抛异常。现在启动后 1.5 秒数一次同名进程:数到就结束,数不到再启动一次,两次都数不到就写 the executable did not stay up。反过来,如果两次终止之后主进程或宿主仍然活着,助手不启动第二个实例并写明原因——那种情况下新实例会输掉单实例竞争、在一秒内自行退出,从外面看恰好就是「重启没反应」。
同一个版本还补了两处在别的机器上会出问题的地方:启动前设置工作目录(WScript.Shell 用脚本自己的工作目录,而 WMI 创建的助手在 System32,这一条在 1.1.9 里漏了),以及经 %SystemRoot% 解析 wscript.exe 而不是依赖 PATH。
为什么不重放启动命令行。 重启原本靠重放宿主自己的 argv,而桌面版的 argv 是 app.asar\dsh <profile> <runtime> ...——那是子进程的命令行,重放它只会得到一个没有 Electron 外壳的孤儿宿主,窗口永远不会出现。所以 looksLikeDshWeb() 在桌面版上判定「无法重放」是正确的;1.1.9 改变的是不重放也有办法重启,而不是放宽那条判断。
还有一处必须是异步的。 gracefulExit 若立刻 resolve,看门狗会把它读成「已经交接完毕」并撤离(graceful exit settled; the watchdog stands down),进程于是留在原地——这正是这条路曾经"报告成功却什么也没关"的机制。所以桌面分支返回的 promise 在进程真正消失前不会 resolve,撤离看门狗的强制退出路径一直挂着。上表里"杀不动"的情形也按此处理:不 resolve,交给看门狗升级。
| 功能 | 桌面版情形 |
|---|---|
| 关闭进程 | 可用:只读探针 → 终止主进程(见上)。任何一步失败,看门狗 5 秒后强制退出 |
| 重启 | 可用(1.1.9 起):同一个助手先杀主进程、再杀宿主、等单实例锁释放、重新启动可执行文件。不再重放命令行,也不再返回 unsupported-host |
| 启动方式切换 | 明确拒绝。桌面版本就是应用窗口,没有标签页形态可切;插件会返回 refused 并说明原因,不做任何改动 |
delayMs / exitCode |
无人读取。桌面版的退出由它的 Electron 主进程管理,这两个值只对从终端、脚本或服务管理器拉起的 DSH 有意义 |
hard |
强制结束路径:跳过优雅退出,直接走终止 |
| 关闭后的「还在运行」判定 | 探针请求插件自己的 /config 路由。桌面版页面来自 dsh-app://app/,由 Electron 主进程的协议处理器提供——即使宿主进程已退出它仍会应答,所以不能拿页面地址当存活依据 |
| 确认框 | 宿主不提供 Modal 时由插件自绘对话框,同样给出取消 / 重启 / 确认关闭三项,不再退回只有两个按钮的 window.confirm |
| 桌面快捷方式层 | 不会碰桌面版的图标。classifyShortcut 把目标指向 DeepSeek Harness.exe(或路径含 desktop-host、electron)的快捷方式判为 other 并排除,既不改写也不还原 |
安装到桌面版 profile:
dsh plugin --profile desktop add github:SanYe-SanJiu/dsh-power-switch#v1.1.10
装完需要重启桌面版。判断装没装对,看 %USERPROFILE%\.dsh\profiles\desktop\package.json 的 dsh.profile.bundles 里是否出现 dsh-power-switch。
升级注意:若不写 #vX.Y.Z,pnpm 会按分支 ref 缓存,重复 add 可能仍拿到旧版本。升级时请显式写上 tag,或先 dsh plugin --profile desktop remove dsh-power-switch 再安装。
一句话:桌面版的窗口自带 X,本插件在那里的意义是把关闭与重启做对(真正结束进程、真正替换掉它、给出正确的取消 / 重启 / 确认关闭三选一),而且全程不弹命令行窗口。从终端或脚本拉起 DSH 时,它还额外提供退出前等待与退出码。
状态与日志
运行时状态位于 $DSH_HOME/storages/dsh-power-switch/,不在包目录内:
| 文件 | 内容 |
|---|---|
restart-dsh.log |
插件、重启助手与监督进程共用的诊断日志 |
boot.json |
最近一次宿主的启动命令(启动器与监督进程使用) |
token-url.txt |
宿主记录的本次认证 URL(助手与启动器据此确认服务进程) |
launch-mode.txt |
下次启动应使用的模式,由插件自身记录。宿主设置文档已不存在时(0.1.7)冷启动读的就是它;设置文档有明确取值时以文档为准 |
node-path.txt |
宿主当前使用的 node.exe 路径;两个 .vbs 外壳读取该文件,因此经 nvm/fnm/volta 或应用商店安装的 Node 也可由快捷方式启动 |
dsh-web.<时间戳>.log |
由本插件启动的各次宿主的 stdout(包含该次运行的 token URL) |
shortcut-backup.txt |
接管前的原始快捷方式,用于还原 |
shortcut-launch.vbs |
被接管的快捷方式实际指向的启动器,每次启动刷新。仍有 profile 承载插件时,把启动交给该 profile 的包内启动器;不再有时依据记录还原原快捷方式。放在包外正是关键 |
shortcut-messages.txt |
两个 .vbs 辅助脚本的对话框文本(英文之外的语言),格式为 键.语言=文本。UTF-16:.vbs 按 ANSI 读取,中文写进脚本会变成乱码 |
restore-shortcut.vbs |
修复脚本的副本,每次启动时写入。双击后依据 shortcut-backup.txt 还原快捷方式并删除该记录;shortcut-launch.vbs 以 /quiet 调用它并自行报告结果 |
shortcut-result.txt |
最近一次快捷方式操作的原始结果 |
状态文件置于包外的原因:包可能位于只读存储中;且日志包含认证 token URL 与本机路径,写入包目录等同于写入版本库。
配置
卡片配置页,或 loader row 的 config::
| 键 | 默认值 | 说明 |
|---|---|---|
launchMode |
tab |
下次启动的窗口形态:tab / app |
delayMs |
1000 |
回完响应后等待多少毫秒才开始退出。响应本身需要 1200ms 送达浏览器,那就是下限:0 表示"响应一发出就走",小于 1200 也按 1200 等待 |
exitCode |
0 |
DSH 进程结束时返回给启动它的那一方的状态码——终端($LASTEXITCODE)、服务管理器、脚本。0 表示成功,因此非 0 是留给包装脚本用来区分"用户主动关闭"和"崩溃异常"的 |
hard |
false |
跳过优雅退出,直接结束进程 |
卡片默认只展开两个电源控件(关闭、重启)。启动方式开关与以上三项都在卡片底部一个默认收起的「高级设置」折叠区里。
这四项面向从本地构建启动 DSH 的情形:dsh web 命令行、源码检出,或由脚本 / 服务管理器拉起。桌面版(Electron)由它自己的主进程管理退出,启动方式切换会明确拒绝(见下文),delayMs 与 exitCode 也无人读取;只有 hard 在那里仍有意义——它是强制结束那条路径。
launchMode 由卡片切换;其余三项在高级设置块里修改,写入状态目录的 settings.json。该块存在的原因:在生成式设置表单的 DSH(0.1.7)上,未声明 schema 的插件拿不到任何表单,没有它这三项就只能手改 profile patch。若宿主仍提供"注册命名空间"那套设置 API,同一次保存也会写入设置文档,使两个界面不会各说各话;最终以记录值为准,删除 settings.json 即把决定权交回 loader row 的 config: 与设置文档。
卡片不再自己附带延迟:delayMs 管辖所有退出路径,包括卡片上的按钮。(它以前会附上 700ms"让界面更快响应"——这个数既不可能起作用(低于响应本身需要的 1200ms),又静默地压过了使用者刚保存的设置。)想要一次性延迟的调用方仍可在关闭请求体里带 delayMs,对该次请求优先于设置。勾选 hard 时,进程改为在同一个等待之后结束,而不是等 watchdog。
环境变量:
| 变量 | 说明 |
|---|---|
DSH_POWER_SWITCH_NO_WINDOW=1 |
本次启动不打开窗口 |
DSH_POWER_SWITCH_WINDOW_HANDLED=1 |
窗口已由其他进程负责;启动器与监督进程会设置该项,插件不再打开第二个窗口 |
DSH_POWER_SWITCH_CLI |
未记录启动命令时,指定 dsh 的 CLI 入口 |
DSH_POWER_SWITCH_PORT |
探活端口(默认 3080) |
DSH_POWER_SWITCH_LAUNCH_MODE、_DELAY、_HOST_LOG、_HANDSHAKE |
助手与监督进程之间的内部接口,无需设置 |
安全边界
六条路由(GET /config、GET /app-window、POST /shutdown、POST /restart、POST /shortcut、POST /settings)的共同要求是仅接受本机 loopback 请求:对端必须为 127.0.0.1/::1;出现任何转发头(forwarded、x-forwarded-for、x-real-ip、x-forwarded-host)一律拒绝。
写路由(shutdown/restart/shortcut/settings)额外要求 Origin 与 Host 完全一致。读路由(GET /config、GET /app-window)允许缺少 Origin(部分宿主自身发起的请求不带该头),但仍要求 loopback 并拒绝转发头;该差异在代码注释中说明。
POST /shortcut 是唯一会在包外产生文件的路由,其请求体只接受一个固定动作(scan / install / restore);目录、文件名、目标与参数均由宿主根据自身安装位置推导,客户端无法指定。
插件不读取凭据、不进行网络请求、不访问会话内容。
有两条边界应当明确写出,而不是等人踩到:
boot.json是一条信任边界。 重启链路会原样重放运行中宿主为自己记录的命令行——这正是设计本身,也是它从不重建安装路径的原因。因此,任何能写入状态目录的人都能让该命令以你的权限执行。这不是权限提升(settings.yaml、profile 的插件清单,以及宿主读取的任何其它文件同理),而这恰恰是状态目录位于用户配置文件内、绝不放进包目录的原因。请把"可写$DSH_HOME"视同"可以你的身份执行代码"。还有两个文件同属这条边界:shortcut-launch.vbs与restore-shortcut.vbs每次启动都从包内刷新,且本就设计为被执行(桌面快捷方式启动的就是前者),因此能写入状态目录的人可以在这两个名下放入别的东西——启动副本只会启动"某个 profile 解析出的那个启动器",修复副本只会依据记录字段改写.lnk,但仍应按"用户配置文件里的可执行文件"对待。- 带认证的
?token=…URL 是本地访问凭据。 它只写在两处,都在$DSH_HOME/storages/dsh-power-switch/下:宿主自身的 stdout 日志(dsh-web.<时间戳>.log,替代进程据此确认服务进程)与token-url.txt。任何会重复它的诊断行都脱敏为?token=***——包括桌面快捷方式写入%TEMP%的外壳日志。请勿将这两个文件贴到公开场合。
开发
npm run build # 将 src/ 复制为 lib/(宿主半产物)
node scripts/run-tests.mjs # tests/ 下的全部套件(node:test),其中一套会在真实的 Windows Script Host 上执行修复脚本
node scripts/verify-client-artifact.mjs # 经真实 HTTP 获取 client.js 并渲染两个视图
node scripts/verify-live.mjs # 对运行中的 DSH 做健康检查
client.js 为手写的 lazy-CJS factory 产物(window.__ModuleLoader__.load),无需构建步骤。tests/ 不随 npm 包发布,因此 npm test 需在检出目录中执行。测试套件:tests/host.test.mjs(请求围栏、参数解析、调度器、路由、重启用命令)、tests/host-wiring.test.mjs(插件装配与 ctx.appExit)、tests/client.test.mjs(卡片注册与交互)、tests/package.test.mjs(产物布局、src/lib 一致性、生成的监督进程源码编译)。
许可证
MIT
链接
同类插件
zhu1090093659/dsh-web#packages/dsh-task-board★ 10247
侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
zhu1090093659/dsh-web#packages/dsh-web-all★ 10247
DSH Web UI 插件与皮肤合集:任务看板、git 图、右侧面板、远程移动端 UI、桌宠、实时 token 统计与皮肤中心。
ccch1mneyyy/dsh-TUI★ 7252
Claude Code 风格全屏终端 UI:像素鲸鱼顶栏、实时工作状态行、思考流式展开。
MeteorNOX/DeepSeek-Balance-Whale-Widget★ 7046
右下角常驻的小鲸鱼挂件:余额、今日已用与每轮对话消耗(含峰谷价),余额预警与今日预算的泡泡内容都可编辑;泡泡点击序列模块化自定义,支持并列加权 A/B、随机台词与随机图片;内置 30+ 厂商模板(OpenAI / OpenRouter / Kimi / 硅基流动 / 方舟 / 智谱 / MiniMax 等),按模型查余额与订阅额度;另有任务结束音效、导入音频、自定义角色与资源管理。数据全在本机,无遥测。
omdsh-dev/DSH-better-sidebar★ 5011
侧边栏完整工作台:内置文件渲染编辑、终端、Git 与子代理,支持三方插件注册新 Tab。
Devin-AXIS/deepseek-design#deepseek-idesign★ 1778
可视化设计工作室,支持网站、App 原型、海报、信息卡、报告和杂志的模板创建、元素编辑、选区级 AI 草稿衔接与导出。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。