Bundle
dsh-web-close
DSH Web 界面的半透明红色关闭按钮:SVG 图标、可拖动、记忆位置,二次确认后优雅关闭当前 DSH 进程并关闭本页
- Source
- Aclguh
- License
- MIT
- Updated
- Updated 4 days ago
Readme
# dsh-web-close —— DSH Web 关闭按钮
DSH(DeepSeek Harness)Web 界面上的常驻关闭按钮:半透明红色、SVG 手绘电源图标、
可拖动并记忆位置,点击后弹出视口居中的二次确认(左「取消」灰 / 右「确认关闭」红),
确认后**优雅关闭**当前正在运行的 DSH 进程,并尝试关掉当前这个 DSH 页面
(只关本页,不会关掉浏览器或其他窗口)。
标准 DSH 插件包,通过 `dsh plugin` 安装与卸载,随 `dsh web` 每次启动自动加载。
## 特性
<<<<<<< HEAD
- **常驻自启**:标准 DSH bundle 插件,装一次,之后每次启动都在
- **半透明红外观**:`rgba(220,38,38,.42)` + 毛玻璃 + 红色描边光晕,悬停加深
- **SVG 手绘图标**:电源符号、警告三角、叉号全部为内联 SVG,**不含任何图片资源**
- **可拖动 + 位置记忆**:默认落在视口 `top: 1234px / left: 306px`
(等价于 `inset: 1234px auto auto 306px`);拖到哪就在哪,写入 `localStorage`,
刷新或重开浏览器后原样恢复;按当前视口夹取,窗口变小也不会跑到屏幕外
- **二次确认弹窗**:视口居中,左「取消」灰、右「确认关闭」红,并显示将要关闭的 PID 与地址
- **优雅关闭**:不是 `kill -9`,而是先卸载整棵插件树再退出(见下)
- **顺便关掉本页面**:关闭请求被接受后自动 `window.close()` 当前标签页;
浏览器出于安全策略拒绝时,弹窗里会出现「关闭本页」按钮,退而把本页置为 `about:blank`。
全程只作用于当前这一个标签页,其他窗口与标签页不受影响
- **跨平台、零依赖**:宿主侧只用 Node 内置模块,浏览器侧是纯原生 DOM;
=======
- **常驻自启**:标准 DSH bundle 插件,装一次,之后每次启动都在
- **半透明红外观**:`rgba(220,38,38,.42)` + 毛玻璃 + 红色描边光晕,悬停加深
- **SVG 手绘图标**:电源符号、警告三角、叉号全部为内联 SVG,**不含任何图片资源**
- **可拖动 + 位置记忆**:拖到哪就在哪,写入 `localStorage`,刷新或重开浏览器后原样恢复;
按当前视口夹取,窗口变小也不会跑到屏幕外
- **二次确认弹窗**:视口居中,左「取消」灰、右「确认关闭」红,并显示将要关闭的 PID 与地址
- **优雅关闭**:不是 `kill -9`,而是先卸载整棵插件树再退出(见下)
- **跨平台、零依赖**:宿主侧只用 Node 内置模块,浏览器侧是纯原生 DOM;
>>>>>>> e0f3bb049138f465ae5c900a80e6f292f6f4dfb3
Windows / macOS / Linux 行为一致,没有 PowerShell、没有构建步骤、没有运行时依赖
## 安装:从 GitHub 克隆安装
```sh
git clone https://github.com/Aclguh/dsh-web-close.git
cd dsh-web-close
dsh plugin --profile web add link:.
```
要点:
- `link:.` 表示链接**当前目录**——仓库根目录本身就是插件包。
- ⚠️ 不要写成 `link:.\dsh-web-close` 这类带子目录的路径,那样会装成普通依赖而不是插件,
重启后按钮不会出现。
- 成功后 `dsh plugin` 会自动把 `dsh-web-close` 写进 profile 的 `dsh.profile.bundles`,
不需要手工改配置。
- **装完必须重启 `dsh web`**,然后刷新浏览器。
- 如果之后移动了源码目录,要重新执行一次 `add link:<新路径>`;若提示冲突,
先 `dsh plugin --profile web remove dsh-web-close` 再重新 add。
## 卸载
```sh
dsh plugin --profile web remove dsh-web-close
```
然后确认 profile 的 `dsh.profile.bundles` 里不再有 `"dsh-web-close"`(没有就无需处理),
最后重启 `dsh web`。
## 环境要求
- 已安装 DSH,且能正常执行 `dsh web`
- `pnpm`(`dsh plugin` 会把参数转发给 pnpm;缺失时先 `npm install -g pnpm`)
- 本插件只对 **web** profile 有意义(它依赖 DSH 的 Web 服务器服务)
- 没有其他依赖:不需要额外 npm 包、不需要构建工具、不需要任何 API Key
### 给 AI 助手的安装说明
如果想让另一个 DSH / AI 助手代装,把下面这段直接发给它:
```text
请帮我安装 DSH 插件 dsh-web-close,来源是 GitHub 仓库 Aclguh/dsh-web-close。
步骤:
1. 确保 pnpm 可用(没有就先执行 npm install -g pnpm)
2. git clone https://github.com/Aclguh/dsh-web-close.git
3. 进入克隆出来的 dsh-web-close 目录,执行:
dsh plugin --profile web add link:.
(仓库根目录就是插件包,不要写成 link:.\dsh-web-close 这种带子目录的路径)
4. 重启 dsh web,然后刷新浏览器
安装后请验证:
- dsh --profile web --dump-config 的输出里能看到 - id: dsh-web-close
- 浏览器里出现半透明红色圆形按钮(默认在视口 top 1234px / left 306px)
- curl http://127.0.0.1:3080/dsh-web-close/info.json 返回含 "ok":true 的 JSON
```
## 它是怎么关闭 DSH 的
插件运行在 DSH 宿主进程内部,也就是承载 Web 服务与 Agent 的那个 node 进程本身。
因此它不需要去别的进程里"找到它再杀掉",而是调用 DSH 自己的有界优雅关闭:
| 顺序 | 路径 | 行为 |
| --- | --- | --- |
| 1 | `ctx.get('appExit')(0)` | DSH 启动器通过 `provideCmdline()` 提供的退出请求 → `shutdown.shutdown(0)`:先 dispose 整棵插件树(所有插件的 disposer 都会执行),再设 `exitCode = 0` 让 Node 自然退出 |
| 2 | `process.emit('SIGINT')` | 命中 DSH 自己注册的 `process.on('SIGINT')` → `shutdown.interrupt(130)`,同样先 dispose 再退出 |
| 3 | `process.exit(0)` | 兜底强制退出 |
`appExit` 是 `ctx.provide()` 提供的裸值,不在 Service 目录里;因此本插件**刻意不把它写进
`inject`**(那样会有让整行永远停在 `waiting for appExit` 的风险),而是运行时用 `ctx.get()`
取值,并带上表里的降级链。
> ⚠️ Web 服务与 Agent 宿主是**同一个** node 进程,所以关闭的是整个 DSH,当前所有会话都会中断——
> 这正是二次确认存在的原因。
<<<<<<< HEAD
关闭请求被接受后,浏览器侧还会做最后一件事:关掉**当前这一个**标签页。
它只调 `window.close()`,不碰 `window.opener`、不碰其他窗口;浏览器按安全策略拒绝时,
退化为把本页替换成 `about:blank`。
=======
>>>>>>> e0f3bb049138f465ae5c900a80e6f292f6f4dfb3
## 目录结构
```text
dsh-web-close/
├── package.json # 包声明;dsh.bundle.patch 指向 cordis.patch.yml
├── cordis.patch.yml # bundle patch:把 id/name 为 dsh-web-close 的一行插入配置树
├── lib/
│ ├── index.js # 宿主半边:HTTP 路由 + tapIndex 注入 <script>
│ └── widget.js # 浏览器半边:原生 DOM 的按钮与确认弹窗
├── README.md
└── LICENSE
```
## 验证
**配置层**——确认插件行被组合进 profile:
```sh
dsh --profile web --dump-config
```
输出中应能看到:
```yaml
# == dsh-web-close
- id: dsh-web-close
name: dsh-web-close
```
**服务层**——需要 DSH 正在运行。端口按你自己的替换(DSH 默认 `3080`):
```sh
curl http://127.0.0.1:3080/dsh-web-close/info.json
# {"ok":true,"pid":12345,"url":"http://127.0.0.1:3080","profile":null,"uptimeSec":438,"inProcess":true}
curl -I http://127.0.0.1:3080/dsh-web-close/widget.js
# HTTP/1.1 200 OK
# Content-Type: application/javascript; charset=utf-8
```
**界面层**——重启并刷新后,页面里出现半透明红色圆形按钮
(默认在视口 `top 1234px / left 306px`)。
> DSH 的 HTML 路由自身带访问鉴权(直接 curl 根路径会得到 `401`),但插件注册的同源路由不受影响,
> 所以上面两条 curl 可以直接用。
## 使用
- **拖动**:按住按钮拖到任意位置。位移超过 4px 才判定为拖动,因此不会误触发点击。
- **位置记忆**:松手即写入 `localStorage`,键名 `dsh-web-close-pos`(结构 `{ v: 2, left, top }`)。
想恢复默认位置(`top 1234 / left 306`),在浏览器控制台执行:
```js
localStorage.removeItem('dsh-web-close-pos')
```
然后刷新页面。位置按当前视口夹取,所以窗口比 `1234px` 矮时按钮会被收到可视区域内。
- **关闭**:点击按钮 → 弹窗显示目标进程 `node.exe · PID … · http://127.0.0.1:…` →
点「确认关闭」。约 0.9 秒后 DSH 开始退出,随后自动尝试关闭本页面。
若浏览器拒绝脚本关闭(只允许关闭脚本打开的窗口),弹窗会给出「关闭本页」按钮兜底。
- 点「取消」、点弹窗外的遮罩、或按 `Esc` 都会关闭弹窗,不会执行任何操作。
## 路由
宿主半边在 DSH 的 Web 服务器上注册三个同源路由:
| 路由 | 方法 | 说明 |
| --- | --- | --- |
| `/dsh-web-close/widget.js` | GET | 浏览器半边脚本。**每次请求都重新读盘**,所以改完 `lib/widget.js` 只需刷新页面 |
| `/dsh-web-close/info.json` | GET | 返回将被动到的进程:`process.pid` 与浏览器自己的 Host 头 |
| `/dsh-web-close/shutdown` | POST | 接受关闭请求,延迟 0.9 秒后触发优雅关闭(其他方法返回 `405`) |
`widget.js` 由 `ctx.webServer.tapIndex()` 以 `<script defer>` 注入 `index.html`,并带重复注入保护。
`/shutdown` 是用 `fetch(..., { keepalive: true })` 发出的,所以即使标签页随后立刻被关掉,
请求也一定能送达。
## 常见问题
- **按钮不出现**
1. 确认 `dsh plugin --profile web add` 成功,且 `dsh --profile web --dump-config` 里能看到
`- id: dsh-web-close`;
2. 确认**重启过** `dsh web`——插件只在启动时加载,热改配置不会生效;
3. 重启后按 `Ctrl/Cmd + Shift + R` 强制刷新,避开浏览器缓存。
- **改了默认位置后按钮没动**:`localStorage` 里存的是旧结构(`v: 1`)坐标,会被自动作废并回落新默认位置;
若想彻底清干净,执行 `localStorage.removeItem('dsh-web-close-pos')` 再刷新。
- **拖动后位置没保留**:无痕/隐私模式下 `localStorage` 不可用,这时只在本次会话内记忆位置。
- **点了「确认关闭」但页面没自动关闭**:浏览器的安全策略只允许脚本关闭"由脚本打开的"窗口,
普通标签页会被忽略。这时弹窗会出现「关闭本页」按钮(置为空白页),或直接按 `Ctrl / ⌘ + W`。
DSH 进程本身不受影响,已经退出或正在退出。
- **点了「确认关闭」但没退出**:查看 `dsh web` 所在终端的输出,关闭时会打印一行
`[dsh-web-close] graceful shutdown requested via appExit`(或 `via SIGINT`)。
- **端口不是 3080**:把 curl 命令换成你自己的端口即可;界面里显示的地址来自浏览器实际请求,
始终是正确的。
- **改了代码不生效**:改 `lib/widget.js` 只需刷新页面(路由每次重新读盘);
改 `lib/index.js`(宿主半边)需要重启 `dsh web`。
- **会和别的插件冲突吗**:本插件只占用 `/dsh-web-close/*` 三个路由和一个
`id: dsh-web-close` 的配置行。与同生态插件的 `tapIndex` 注入是叠加关系,
例如可与 `dsh-whale-widget` 同时使用,两个 `<script>` 会并存于页面底部。
## 许可证
本项目基于 **MIT License** 开源,详见 [LICENSE](LICENSE)。
Install
dsh plugin --profile web add github:Aclguh/dsh-web-close
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-web-close from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.