- Startseite
- Plugins
- Entwicklung & Plugin-Tools
- dsh-autostart
dsh-autostart
mandarin715/dsh-autostart
Autostart von DSH beim Windows-Start und Neustart des Dienstes per Klick in DSH. Ein residenter Supervisor, der den Host überlebt, verhindert einen Ausfall von DSH, falls der Neustart scheitert. DSH startet an eine verborgene, dem Supervisor gehörende Konsole angehängt, sodass kein Konsolenfenster erscheint. Mit Neustartschaltfläche, aktueller tokenisierter Zugriffs-URL und optionalem Hook nach dem Start.
Installation
dsh plugin --profile web add github:mandarin715/dsh-autostartREADME
dsh-autostart
English | 中文
Windows 专用的 DeepSeek Harness 插件:一键开机自启 + 一键重启,而且重启失败也不会把 DSH 关掉不回来 —— 全程没有控制台窗口。
⚠️ 免责声明(务必先读)
安装前请自行检验环境。因使用本插件导致对话历史丢失、任务中断或数据损坏的,概不负责。
重启会杀掉承载对话与 Agent 回合的 DSH 宿主进程:
- 有 Agent 在执行回合时重启 → 该回合被硬中断,结果可能不落盘
- 对话尚未写入
~/.dsh/sessions/→ 该段历史可能丢失且不可恢复 - 并行会话中正在运行的任务也会被中断
使用前请:① 自行确认环境(Windows 版本、Node、DSH 版本、安全软件是否拦注册表);② 重启前确认没有重要任务在跑;③ 重要会话先备份 ~/.dsh/sessions/;④ 自行评估是否启用开机自启(会写入 HKCU\...\Run)。
本软件按「原样」提供(MIT License,无任何担保)。
核心卖点(它到底解决什么)
重启一个程序,难的不是「关掉」,而是**「关掉之后还能回来」**。这个插件就是把这件事做成有保证的:
-
重启失败不会让 DSH 停摆。 放手让 DSH 退出之前,插件会 ① 确认「接管者」(看护进程)活着,② 把重启请求写进磁盘并读回来验证。任何一步不成立,它就拒绝这次重启、DSH 原地不动,并说明原因。这是一个真实发生过的故障:端口还没释放,旧逻辑探测一次就放弃,结果 DSH 关了、起不来。
-
全程没有黑窗口。 DSH 启动时依附在看护进程的隐藏控制台上,所以它派生的每条命令都共用这个隐藏控制台,而不再各自新建一个 Windows Terminal 窗口 —— 以前是「每用一次工具就弹一个黑框」。
-
接管者不是「看门狗」。 看护进程常驻,但只在「这次退出是你点的重启」时才拉起新实例。你自己关掉 DSH,它就安静收工 —— 一个你关不掉的进程,比一个偶尔不自动回来的服务更糟。
-
停用 / 卸载不会偷袭你正在用的 DSH。 「停用自启」只取消下次开机自启;当前这次开机里 DSH 照常运行,看护进程会在 DSH 自己退出后收工。
-
出问题有据可查。 每一步都写进
service.log;失败时它会说明原因,而不是静默不动。
要求
- Windows 10 / 11(需要能通过 PowerShell 调用
Win32_Process.Create—— 点重启而当时还没有看护进程在跑时,会刻意交由 WMI 服务创建一个,以免和宿主一起被杀) - Node.js ≥ 20(随 DSH 提供)
- DeepSeek Harness ≥
0.1.0-rc.6(实测于0.1.2-rc.1;重启所依赖的 Job Object 行为实测于0.1.5-rc.1) HKCU\...\Run可写(开机自启用);安全软件拦截注册表会导致「启用」失败
环境与重启
DSH 由一个常驻看护进程启动 —— 开机时由 bootstrap.vbs 跑
node service.js start --config <config.json>(start 是 supervise 的别名,后者才是真正的入口;
生成的 bootstrap.vbs 用的是 start)。活得比 DSH 久的是这个看护进程:它从子进程句柄上
得知 DSH 退出,并且只在「这次退出是你从设置页要的重启」时才拉起新实例。
只有当你点重启、而当时还没有看护进程在跑(例如 DSH 是你手动起来的)时,才需要从 DSH 内部先创建一个
看护进程。这是 WMI 服务唯一出场的地方:DSH 把自己的子进程放在一个 kill-on-close 的 Windows Job
Object 里,所以仅仅 detached 的进程会在 DSH 退出的瞬间被一起杀掉 —— 结果是 DSH 停摆、再也起不来。
交给 WMI 创建,看护进程才能出得了那个 job、活得比宿主久。一旦看护进程已经存在(登录后的常态,以及第一次
重启之后的每一次),就不再经过 WMI。
代价是:这样创建出来的进程不继承你的环境变量。因此:
- 看护进程的 config.json 路径由宿主用
--config <绝对路径>显式告知(而不是让它去按DSH_HOME推导 —— 那个变量在这里是缺失的); - 重启后的实例用 config.json 里记录的
dshHome重新断言DSH_HOME。
其余环境变量(自定义 PATH、DSH 读取的其他变量)不会被带到重启后的实例。如果你的
DSH 配置依赖环境变量,请留意这一点,并尽量让 command.execPath 是绝对路径。
安装
# 固定版本(推荐):装的是与作者本机一致、已验证过的那一版
dsh plugin --profile web add "github:Mandarin715/dsh-autostart#v0.2.1"
想要 main 上的最新修复(可能尚未验证):
dsh plugin --profile web add "github:Mandarin715/dsh-autostart"
重启 DSH 后,进入「设置 → 通用设置」,拉到最下方即可看到本插件的卡片。
使用
| 控件 | 作用 |
|---|---|
| 服务状态 | 实时探测端口,显示运行中/已停止 |
| 开机自启开关 | 启用时会写 ~/.dsh/dsh-autostart/config.json、生成 bootstrap.vbs,并写入注册表 HKCU\...\Run 的 DSH autostart 项 |
| 当前访问地址 | 从捕获的启动输出里解析出的最新带 token 地址,可一键复制 |
| 重启服务 | 二次确认后重启;DSH 会在数秒内恢复。它会先确保有看护进程在跑、由它把 DSH 拉回来;起步失败则当场拒绝这次重启 |
| 钩子脚本 | 可选。服务起来后会执行它,用于拉起你自己的依赖进程 |
页面完全打不开? 这张卡片只在 DSH 运行时才存在 —— 见下面的「服务没起来时怎么救」。
配置
在 profile 的 cordis.patch.yml 里:
- id: dsh-autostart
name: dsh-autostart
config:
hookScript: '' # 可选,服务起来后执行的脚本绝对路径
dshPort: 3080
exitDelayMs: 800 # 重启时旧宿主退出前的延时;上限 5000(见下方说明)
waitForExitMs: 30000 # 自常驻看护进程取代旧重启路径后已无读者 —— 保留只是为了让
# 已存在的 config.json 仍能加载
startTimeoutMs: 30000
openBrowserOnBoot: false # true = 开机时自动打开浏览器
blockWhenAgentsRunning: false # true = 有 Agent 在跑时拒绝重启
allowedHosts: [] # 经反代访问时填你的域名(必须显式填写),如 ['derp.example.com']
exitDelayMs的有效上限是 5000 毫秒。 这个延时只为让重启响应先刷出去、旧宿主再退出;而新拉起的 看护进程等这个宿主退出只等 30 秒(DEFAULT_TAKEOVER_EXIT_MS)。延时超过那个窗口,接管就会放弃并收工, 重启也就变成了关机 —— 所以超过 5000 的值在使用处被截断到 5000,而不是拒绝加载 (index.js的MAX_EXIT_DELAY_MS)。
allowedHosts必须由你显式填写(opt-in)。 默认是空数组,即不信任任何非回环的 Host。 如果你是通过反向代理访问 DSH(例如 frp + auth-proxy,它会把浏览器发来的原始 Host 转发过来), 那么请求里的 Host 是公网域名而不是127.0.0.1,写操作按钮(启用/停用自启、重启服务)会被守卫 拒绝并返回 403。把你的域名填进allowedHosts后这些按钮才可用;同源校验 (Origin 必须与 Host 完全一致)对这些条目仍然生效——可接受的写法见下。
allowedHosts 可接受的写法:裸主机名,或 host:port,例如 ['derp.example.com'] 或
['derp.example.com:8443']。不要带协议头或路径(https://derp.example.com/ 是错的)。
浏览器访问 https://derp.example.com 时 Host 不带端口,因此对白名单里的权威跳过本地 dshPort
校验——填进白名单本身就是显式授权,而 Origin 与 Host 的同源校验依然必须成立。回环地址仍保持严格端口校验。
改配置后要不要重新启用自启?——要,但原因不是 dispose。
hookScript/dshPort/startTimeoutMs这些字段是在**「启用自启」时快照进config.json** 的,而看护进程读的是config.json、不是插件的实时配置。所以改了它们**必须回设置页 重新点一次「启用自启」**才会生效(重复启用是幂等的)。真机实测过:改了hookScript却只重启服务,config.json里仍是旧值,钩子不会执行。 而allowedHosts只影响插件的路由守卫,重载即生效,与自启无关。waitForExitMs同样会被快照,但已经没有任何代码读它(见上方配置示例里的说明)。注册表项不会因为改配置而消失。 插件只在自己真的被卸载时(判据:本插件的
service.js已不存在)才移除DSH autostart;DSH 因重载或加载失败而拆解插件树时保留它。
和已有的开机自启项如何共存(重要)
本插件只管 DSH。如果你原本已经有自己的开机自启项(例如 HKCU\...\Run 里的 DSH Web、
DSH frpc、DSH authproxy),那么启用本插件的自启之后,会有两条自启项都想在登录时拉起 DSH。
- ✅ 不会互相覆盖:本插件只写、也只删自己那一条
DSH autostart,不碰你的条目(实测:启用后原有条目一字未动)。 - ⚠️ 但会重复:两者都靠"端口已在监听就跳过"去重,所以最终只会有一个 DSH;然而存在一个窄竞态 —— 两条几乎同时跑、都在对方绑定端口之前探测到"未监听",于是都去拉起,其中一个因端口被占用而失败退出 (无害,但会留下一次失败记录与可能的残留进程)。
做法 A:只留本插件(简单)
删掉你自己的 DSH 启动项:
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "DSH Web" /f
然后回设置页点一次「启用自启」。
⚠️ 不要顺手删掉
DSH frpc/DSH authproxy—— 本插件不管 frpc 和 auth-proxy。 手机远程若依赖它们,删掉就会失去自启。要合并请用做法 B。
做法 B:合并成「一条自启 + 一个钩子」(维护面最小)
钩子在开机与每次重启之后都会执行,所以把"确保其他进程在跑"的逻辑放进去,自启就只需要本插件这一条。
-
写一个钩子,例如
~/.dsh/hooks/after-service-up.ps1:# 服务起来后,确保其余依赖进程在跑(路径按你自己的改) & "$env:USERPROFILE\.dsh\scripts\start-frpc.ps1" & "$env:USERPROFILE\.dsh\scripts\start-authproxy.ps1" -
在 profile 的
cordis.patch.yml里指向它:- id: dsh-autostart name: dsh-autostart config: hookScript: 'C:\Users\<you>\.dsh\hooks\after-service-up.ps1' -
回设置页重新点一次「启用自启」(
hookScript是启用时快照进config.json的), 再删掉DSH Web/DSH frpc/DSH authproxy三条,只留插件的DSH autostart。
钩子失败不会阻断 DSH 启动(只记日志),所以钩子里各步骤可以独立失败。 确认钩子跑了没:看
~/.dsh/dsh-autostart/service.log里的hook exited code=…。
| 你原有的自启项 | 做法 A | 做法 B |
|---|---|---|
DSH Web(负责拉 DSH) | 删掉 | 删掉 |
DSH frpc / DSH authproxy | 保留 | 删掉,改由钩子负责 |
生成物位置
~/.dsh/dsh-autostart/
├── config.json # 记录真实启动命令,service.js 读取
├── bootstrap.vbs # 开机入口:拉起常驻看护进程(wscript 无窗口)
├── dsh-web-server.log # DSH stdout(含访问地址)
├── dsh-web-server.err.log
├── service.log # 看护进程日志,排查问题先看这里
├── supervise.pid # 看护进程自报的 pid(单实例守卫)
├── restart.request # 重启时写入,记的是看护进程看到的那个退出的 DSH
└── supervise.stop # 通知活着的看护进程在 DSH 消失后收工
后三个是临时状态文件:需要时创建、用完删除(全部尽力而为),不存在是正常的。但三者的清理时机并不一样:
supervise.pid 和 supervise.stop 在持有它们的看护进程处理到时就删掉;而 restart.request 只有在它记名的
那个 DSH 真的退出时才会被消费 —— 所以一次被拒绝、或始终没走到那次退出的重启,它写的请求可以一直留在盘上,
直到下一次匹配的退出把它取走。
服务没起来时怎么救
卡片在 DSH 的页面里 —— DSH 没起来时你打不开卡片,所以补救必须走命令行。按下面顺序来。
0) 先等几分钟 —— 看护进程会原地重试
DSH 是由常驻看护进程启动的 —— 开机自启那条入口指向的就是它,所以在该有 DSH 的时候它总在。 新实例没起来时,看护进程不会干等就放弃:
- 它会原地重试,间隔递增 1s → 1.5s → 2.25s → …,最多 5 次、预算 5 分钟;每次尝试还会为等端口
花掉
startTimeoutMs(默认 30 秒)。所以"预算 + 最后一次尝试自己的等待"才是最久要等的时间,而循环通常 比预算更早结束。前提是上一次那个子进程确实已经退出 —— 如果它还活着,它不会另起一个去抢同一个端口。 - 它不会再安排"以后某次":看护进程本身就活过这次尝试,重试就发生在那个已经在看着端口的进程里。
因此失败之后先等几分钟再动手 —— 预算 5 分钟,再加上最后一次尝试的 startTimeoutMs。日志里能看到它的动作:
spawned dsh pid=… (attempt 2/5) ← 正在重试
giving up: DSH did not come up after 5 attempt(s); supervisor exiting
那行 giving up 报的是实际尝试过的次数,不一定等于配置上限 5。
只有这些都失败,才按下面手动补救。
关于常驻进程(两条必须知道)
- 它和 DSH 同生共死。 DSH 是**附着(attached)**在它的隐藏控制台上启动的(正是这个共享让每条命令 不再弹窗),所以看护进程退出时 DSH 会一起结束。
- 停用自启并不会立刻停掉它。 停用只是取消开机自启 —— 当前这次开机里看护进程和你的 DSH 继续跑,
下次登录后不再出现。
- 卸载会另外写一个停止标记,所以活着的看护进程不会在 DSH 下次消失之后继续留守。
- 要立刻结束两者,直接关闭 DSH —— 子进程正常退出后,看护进程有意不留守。它只在你从设置页请求的 重启时才把 DSH 拉回来,它不是看门狗。
- CLI 里没有
stop模式(只有supervise和它的别名start),而且写停止标记也不会立刻停掉 看护进程:它只在子进程(DSH)退出之后才读那个标记。这个顺序是有意为之 —— 停用自启不该关掉你正在 用的 DSH。
1) 先看日志找原因
~/.dsh/dsh-autostart/service.log # 看护进程日志:先看这个
~/.dsh/dsh-autostart/dsh-web-server.err.log # DSH 自己的报错
常见三类:
WARN port 3080 did not come up in time→ DSH 被拉起了但没监听,去看dsh-web-server.err.loghook not found:/hook exited code=…→ 钩子问题,不会阻断 DSH 启动cannot read config→config.json缺失或损坏
2) 通用补救:手动执行一次「开机入口」
这一步等价于**"现在立刻跑一次开机自启",幂等(端口已在监听就跳过),而且无窗口**:
wscript.exe "$env:USERPROFILE\.dsh\dsh-autostart\bootstrap.vbs"
它执行 <node> <service.js> start --config <config.json>(start 是 supervise 的别名)。这条命令创建出的
就是常驻看护进程,它拥有一个隐藏控制台 —— DSH 附着在它上面启动,所以 DSH 没有可见窗口;DSH 的
stdout 写进 ~/.dsh/dsh-autostart/dsh-web-server.log —— 成功的话那里面就会出现新的带 token 地址。
看护进程会一直活到 DSH 结束,并始终持有 DSH。不要杀它:它是 DSH 的父进程,它退出 DSH 也会跟着结束。
也可以直接跑 CLI
node "<插件目录>\service.js" start --config "<config.json 的绝对路径>"
两个路径都必须是绝对路径(裸写 service.js 只有在你当前的工作目录恰好是插件目录时才能解析到)。它做的
和上面的 bootstrap.vbs 是同一件事,但它把 DSH 的命绑在你运行它的那个控制台上:DSH 是附着启动的,
所以关掉那个窗口就会终止 DSH;而且看护进程会占住前台直到 DSH 退出。用这种形式就把窗口一直留着;
否则优先用上面的 bootstrap.vbs —— 它给看护进程一个自己的隐藏控制台,并且立刻返回。
3) 如果连 config.json 都不存在
说明从未点过「启用自启」。那就先用你平时的方式把 DSH 起来,再到设置页启用一次自启。
4) 用你自己惯用的启动脚本也行 —— 但只有两个要点
要点是隐藏/后台启动,以及不要拿控制台窗口当服务的宿主:
- ✅
Start-Process … -WindowStyle Hidden,或wscript跑 VBS,把 DSH 拉成后台进程 - ❌ 不要用
npx @deepseek-ai/dsh web这种前台占用控制台的方式;更不要"先拿到带 token 的地址, 再把那个窗口关掉" —— 那个窗口就是 DSH 的控制台,关掉窗口 Windows 会终止 DSH,链接随之失效 (此时是"拒绝连接"而不是 401,再粘也没用)
作者自己的机器上这一步是
~/.dsh/scripts/start-dsh-web.ps1(它用Start-Process -WindowStyle Hidden启动,并把 stdout 写进~/.dsh/logs/dsh-web-server.log)。这是作者本地的脚本,不属于本插件; 你换成自己的等价脚本即可,或者直接用上面第 2) 步。
5) 别把两种情况搞混
| 现象 | 含义 | 做法 |
|---|---|---|
| 页面 401 Unauthorized | 服务在跑,只是这个浏览器没有会话 | 用「访问地址」里那条带 token 的链接进一次,不要重启服务 |
| 无法访问此网站 / 拒绝了连接 | 服务没起来 | 按上面 1)–4) 处理 |
关于那条带 token 的地址,三个事实:①只对本机有效(它是 127.0.0.1;手机走域名 + 密码,由反代代换);
②换来的会话 cookie 有 30 天有效期,关浏览器、重启 DSH 都不受影响;③但 token 本身每个 DSH 进程一个,
重启即作废 —— 所以要用当前这条(卡片显示的就是最新的),收藏一条长期用是无效的。
6) 补救成功的判据
- 端口 3080 处于 LISTENING
service.log里出现port 3080 is up- 设置页卡片的「服务」显示运行中
7) 如果最近升级或迁移过 DSH
config.json 记录的是当时的绝对路径(可能含 npx 缓存目录的哈希路径,如
…\_npx\<hash>\node_modules\@deepseek-ai\dsh\lib\bin.js)。升级/迁移后该路径可能已失效,于是
自启会静默失败(开机什么都没发生)。回设置页**重新点一次「启用自启」**让它重新捕获当前命令即可。
卸载
-
先在设置页停用开机自启(会删除注册表项)
这一步是必需的,不是可选的。原因有两条:①注册表项指向
~/.dsh/dsh-autostart/bootstrap.vbs, 而它与config.json都在插件目录之外,卸载不会删;②pnpm remove会保留node_modules/dsh-autostart这条符号链接,于是本插件的service.js依然可达 —— 而插件的清理 判据正是"service.js是否还存在"。所以只卸载而不先停用,会留下一条死条目(开机跑一次、 静默失败)。也可以卸载后手工删掉HKCU\...\Run里的DSH autostart。顺带一提:设置页的「设置 → 插件」没有本插件的卸载按钮(那一页只管理从市场安装的插件), 所以第 2 步的命令行是正常路径。
-
卸载插件:
dsh plugin --profile web remove dsh-autostart -
如需彻底清理,手动删除
~/.dsh/dsh-autostart/目录
为什么这样实现(踩坑记录)
- 为什么用
wscript.exe而不是powershell -WindowStyle Hidden:后者对长时间运行的脚本不可靠,会留下一个无法关闭的空控制台窗口。 - 为什么等端口用条件轮询而不是固定
Start-Sleep:固定等待曾让一次重启耗时 80 秒以上;条件轮询(waitForPort,间隔 250ms、受startTimeoutMs约束)把它压到数秒。看护进程不轮询自己持有的那个 DSH 进程 —— 它握有那个子进程的句柄,等的是句柄的exit事件。(没有句柄可握的地方仍然按 pid 轮询:接管时等旧 DSH 退出,以及等新看护进程认领supervise.pid。) - 为什么端口判定用 TCP 连接而不是
netstat/:port子串:子串匹配会命中TIME_WAIT与客户端连接,导致"其实没启动却报告已在运行"。 - 为什么用 Node 而不是 PowerShell 实现逻辑:PowerShell 脚本里的中文易出现编码问题,且受执行策略限制。
- 为什么必须写
--no-open:DSH0.1.2-rc.1每次启动生成新的 token,访问地址会打印在 stdout;插件把它捕获到日志并在设置页展示,因此开机时无需(也不应)弹出浏览器。
Ähnliche Plugins
deepseek-harness
deepseek-ai/deepseek-harness
dsh-market
dsh-market/dsh-market
dsh-web (dsh-plugin-manager)
zhu1090093659/dsh-web
dsh-web
zhu1090093659/dsh-web