Skip to content
dsh.fish
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

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source