Bundle
dsh-power-controls
Settings > General 里的关闭/重启 DSH 按钮(不动第三方包)
- Source
- harmless0819-dev
- License
- MIT
- Updated
- Updated 2 days ago
Readme
# dsh-power-controls
设置 → 普通设置(General)里的两枚电源按钮。
| 按钮 | 做什么 |
| --- | --- |
| **重启 DSH** | 电源循环后端:杀掉当前 dsh web → 等 3080 释放 → 重新拉起 → 验证监听。页面断线几秒后自动刷新回来 |
| **关闭 DSH** | 只关后端。浏览器标签页会停在断线状态,需要双击桌面的 DSH Web 图标重新打开 |
## 为什么重启要绕一圈计划任务
第一版的重启是「spawn 一个 detached helper,然后自己 exit」。**这是错的**:dsh 退出时会回收自己的进程树,
helper 跟着一起死,于是后端再也起不来(用户看到的「关闭/重启后不会重新起来」就是这个)。
Windows 上没有可靠办法让子进程比父进程活得久(job object 说了算),所以重启改成把活交给**任务计划程序**:
```
点「重启 DSH」
└─ 后端: Register-ScheduledTask DSHPowerRestart + Start-ScheduledTask ← 确认成功后才 exit
└─ 计划任务(独立进程树): respawn-dsh-web.ps1
├─ 按端口找到旧的 dsh web 并停掉
├─ 等 3080 释放(最多 60 秒)
├─ start-dsh-web.ps1 -NoOpen 拉起新实例
└─ 验证端口,最多重试 3 次
```
关键点:**注册计划任务成功后才允许自己退出**。注册失败就返回 500 并继续活着,
否则用户会面对一个「点了按钮、后端没了、也没有人来拉」的死局。
## 文件
| 路径 | 作用 |
| --- | --- |
| `lib/index.js` | 后端:`/dsh-power/close`、`/dsh-power/restart`、`/dsh-power/ping`、`/dsh-power/report`、`/dsh-power/health`,以及 tapIndex 注入 |
| `lib/client.js` | 前端:往 `[data-slot="settings.general.item"]` 所在的 section 追加按钮行(**每次请求都从磁盘读,改完只要 F5**) |
| `test-harness.mjs` | 离线自检:mock 掉 `ctx.webServer`,直接驱动路由验证自动收尾规则(`node test-harness.mjs`) |
| `respawn-dsh-web.ps1` | 计划任务跑的电源循环脚本(ASCII-only,PS 5.1 按 ANSI 读无 BOM 的 .ps1) |
| `logs/dsh-power-restart.log` | 电源循环日志 |
| `logs/dsh-power-restart.{out,err}.log` | 新实例的 stdout/stderr |
## 自检(看不到浏览器时的验收通道)
```powershell
curl http://127.0.0.1:3080/dsh-power/health
```
返回 `lastReport` / `reportCount` / `clicks`:客户端注入成功会上报 `injected`,
点了按钮会上报 `click`,重连成功会上报 `reconnect`。看见 `injected` 就说明按钮已经画到设置页上了。
## 安装
```sh
# 从 GitHub(推荐)
dsh plugin --profile web add github:harmless0819-dev/dsh-power-controls
# 或从本地克隆
dsh plugin --profile web add link:<本仓库绝对路径>
```
> 装/卸这个插件会让 pnpm 重解析依赖树,而 **正在运行的 dsh web 锁着 lightningcss 原生模块**,
> 所以必须**先停后端再装**(我踩过:不停后端会 `拒绝访问 (os error 5)`)。
## 顺带:浏览器标签页图标(web icon)
同一个插件还负责把标签页 favicon 换成鲸鱼娘(图片由用户提供,128px 落在 `assets/web-icon.png`)。
**为什么不走皮肤**:皮肤中心的 `canServeSkinHooks()` 只放行内置皮肤和**字节校验过的官方市场皮肤**,
用户目录里的皮肤即使 manifest 合法,它的 `hooks.mjs` 也会被拒绝执行(原文:hooks only run for
built-in or byte-verified official-market skins)。所以 favicon 只能从**受信代码路径**注入。
两条腿走路,互为兜底:
| 层 | 做什么 |
| --- | --- |
| 服务端 `tapIndex` | 摘掉原有的 `<link rel="icon">`(stock 是 favicon.svg),在 `<head>` 里声明 `/dsh-power/web-icon.png` |
| 客户端 `applyFavicon()` | SPA 重渲染可能把 stock 图标放回来,所以每次 tick 重新确认我们的 link 是唯一候选 |
资源路由 `/dsh-power/web-icon.png` **每次请求从磁盘读**,换图不用重启。
## 自动收尾:关掉浏览器之后(默认开启)
关标签页**不会**停掉 dsh web —— 它是服务器,会话/正在跑的回合都靠它活着。但那会白占 ~0.5GB 内存。
所以页面会主动打招呼,**并且每次都报上自己的可见性**(`document.hidden`):
| 时机 | 请求 | 作用 |
| --- | --- | --- |
| 页面打开期间,每 3 秒 | `POST /dsh-power/alive { cid, hidden }` | 登记这个页面还活着、以及它是不是被切到后台了 |
| 可见性变化(切走 / 切回) | 同上,立即补一次 | 不用等下一个 3 秒 tick |
| 关闭 / 刷新 / 跳转 | `navigator.sendBeacon('/dsh-power/leaving', { cid, hidden })` | 明确道别(beacon 能在页面消失后送出,普通 fetch 会被丢) |
**「客户端消失」有两种,含义完全不同**:
| 现象 | 判定 | 处理 |
| --- | --- | --- |
| 收到 `pagehide` 道别 | `leaving` | 立刻从客户端表移除 |
| 12 秒(`CLIENT_STALE_MS`)没心跳,最后上报是**可见** | `stale-visible` | 移除(人走开了 / 断网 / 浏览器被杀) |
| 12 秒没心跳,最后上报是**隐藏** | 静默但保留 | **不算离开**:后台标签被浏览器节流是常态,页面很可能还开着 |
只有**客户端表彻底空了**才开始计时(默认 15 秒,`autoCloseMs` 可调,**设 0 = 关掉这个功能**):
1. **宽限期内页面回来了**(刷新、手快重开)→ 直接取消,什么都不做
2. 宽限到了但**还有回合在跑**(宿主 `ctx.get('agents')` 里 `status === 'running'`)→ 继续等,直到回合跑完
3. 没人看 + 没有回合在跑 → 退出进程把内存还回去,**并把原因写下来**
退出时日志里会写明原因,例如:
```
auto-close firing: reason=explicit-pagehide: last client p4x… (visible) said goodbye, no client for 16s …
auto-close firing: reason=silent-visible: last client p4x… (visible) went quiet, no client for 16s …
```
同一份记录还会落到 `D:\deepseek\logs\dsh-power-last-exit.json`,**由下一次启动的 `/dsh-power/health` 里的 `lastExit` 报出来**
—— 「上一次到底为什么退出」不用再靠猜。
### 怎么区分「用户没在看」和「用户把页面关了」
| 日志里看到 | 结论 |
| --- | --- |
| `page left (cid, visible/hidden)` | 明确的关闭 / 刷新 / 跳转 |
| `client cid went stale while visible` | 可见页面静默失联(断网、浏览器被杀) |
| `client cid went quiet while hidden … kept alive` | 后台标签被节流 —— 页面还在,只是没在被看 |
| 同一个 `cid` 隔一会儿又出现 | **同一个页面实例一直活着**(cid 每次页面加载重新铸,且不写入任何存储) |
| 出现新 `cid`,而旧 `cid` 之前没有 `page left` | 旧页面是没打招呼就没了(强杀 / 崩溃 / 信标丢包) |
自检通道:`/dsh-power/health` 的 `clients[].hidden` / `clients[].quiet`、`visibleClients`、
`autoClose.pendingReason` / `autoClose.lastRemoval`、`lastExit`、`logFile`、`runningTurns`;
日志写在 `D:\deepseek\logs\dsh-power-auto-close.log`。
## 离线自检
```powershell
node test-harness.mjs
```
Mocks `ctx.webServer` + `ctx.get('agents')`,直接驱动插件自己的路由,覆盖上面全部规则(可见性计数、
hidden 静默保留、visible 静默丢弃、道别移除、退出原因与 `last-exit.json` 的跨进程回报)。
退出路径跑在**子进程**里,所以它的 `process.exit` 不会杀掉测试本身。
测试用 `tickMs` / `staleMs` / `logFile` 三个配置缩短等待并把日志写进临时目录
(为了可测性而存在,生产环境不要设)。
Install
dsh plugin --profile web add github:harmless0819-dev/dsh-power-controls
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-power-controls from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.