@kesike/dsh-exit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/README.zh-CN.md +120 -0
- package/cordis.patch.yml +6 -0
- package/docs/dsh-exit-landscape.webp +0 -0
- package/docs/dsh-exit-real.png +0 -0
- package/lib/client.js +222 -0
- package/lib/index.js +93 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# dsh-exit
|
|
4
|
+
|
|
5
|
+
**A focused exit control for the DeepSeek Harness web interface.**
|
|
6
|
+
|
|
7
|
+
[](https://npmjs.com/package/@deepseek-ai/dsh)
|
|
8
|
+
[](https://www.npmjs.com/package/@kesike/dsh-exit)
|
|
9
|
+
[](https://github.com/KeS1Ke/dsh-exit)
|
|
10
|
+
[](package.json)
|
|
11
|
+
[](package.json)
|
|
12
|
+
|
|
13
|
+
[简体中文](README.zh-CN.md)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
> [!IMPORTANT]
|
|
18
|
+
> Confirming the action terminates the dsh host process and releases its ports. The host terminal window and the current browser tab are intentionally left open.
|
|
19
|
+
|
|
20
|
+
## ✨ What it does
|
|
21
|
+
|
|
22
|
+
`dsh-exit` adds a compact, floating **Exit** button to the lower-right corner of the entire dsh web interface.
|
|
23
|
+
|
|
24
|
+
- Opens a confirmation modal before doing anything destructive.
|
|
25
|
+
- Calls the typed `dshExit/exit` Remote method only after confirmation.
|
|
26
|
+
- Gives the gateway a short acknowledgement window, then ends the host process.
|
|
27
|
+
- Releases every port held by that process, including the usual `127.0.0.1:3080`.
|
|
28
|
+
- Keeps the terminal window and browser tab available for the next action.
|
|
29
|
+
- Uses the existing dsh design tokens and the Lucide `power` icon.
|
|
30
|
+
|
|
31
|
+
## 🎬 Animated exit flow
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img src="docs/dsh-exit-landscape.webp" alt="Landscape animated dsh-exit flow generated by Archify" width="720">
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
## 📸 Real dsh UI
|
|
38
|
+
|
|
39
|
+
The capture below comes from a running dsh Web profile with `dsh-exit` installed. The red power button is the actual plugin control rendered at the lower-right corner.
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<img src="docs/dsh-exit-real.png" alt="Running dsh Web UI with the dsh-exit power button in the lower-right corner" width="720">
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
## 🧱 What ships
|
|
46
|
+
|
|
47
|
+
| Layer | File | Responsibility |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| Host | [`lib/index.js`](lib/index.js) | Registers the `dshExit` Cordis service and the typed `exit()` Remote method. |
|
|
50
|
+
| Client | [`lib/client.js`](lib/client.js) | Self-contained browser bundle: button, CSS, modal, keyboard handling, and Remote mounting. |
|
|
51
|
+
| Bundle patch | [`cordis.patch.yml`](cordis.patch.yml) | Inserts the plugin row into the dsh profile. |
|
|
52
|
+
| Package metadata | [`package.json`](package.json) | Declares the host entry, web client, and bundle patch. |
|
|
53
|
+
| Animated flow | [`docs/dsh-exit-landscape.webp`](docs/dsh-exit-landscape.webp) | Landscape Archify trace animation rendered inline as animated WebP. |
|
|
54
|
+
| Real UI capture | [`docs/dsh-exit-real.png`](docs/dsh-exit-real.png) | Running dsh Web profile with the actual button visible. |
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## 🚀 Install from npm
|
|
58
|
+
|
|
59
|
+
Install the published package directly into the dsh Web profile:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
dsh plugin --profile web add @kesike/dsh-exit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Restart the profile after installation. The dsh host loader resolves the bare runtime imports, so this package intentionally declares no runtime dependencies. To update or remove it:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
dsh plugin --profile web update @kesike/dsh-exit
|
|
69
|
+
dsh plugin --profile web remove @kesike/dsh-exit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 🖥️ UI behavior
|
|
73
|
+
|
|
74
|
+
| Interaction | Result |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| Click the floating power button | Opens the confirmation modal. |
|
|
77
|
+
| Click **Cancel**, press **Esc**, or click outside | Closes the modal without changing the host. |
|
|
78
|
+
| Click **Confirm exit** | Disables duplicate actions and invokes the host Remote method. |
|
|
79
|
+
| Remote call fails | Restores the controls and shows an inline error message. |
|
|
80
|
+
| Remote call succeeds | Closes the modal; the host exits shortly afterward. |
|
|
81
|
+
|
|
82
|
+
## 🔌 Remote contract
|
|
83
|
+
|
|
84
|
+
The host advertises a strict, parameterless Remote method:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
namespace: dshExit
|
|
88
|
+
method: exit
|
|
89
|
+
result: true
|
|
90
|
+
gateway: /api/dshExit/exit
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The client-side dsh gateway returns the standard envelope `{ ok: true, value: true }` on success. The service guards repeated calls with an `exiting` flag and schedules `process.exit(0)` after the response has a chance to settle.
|
|
94
|
+
|
|
95
|
+
## 🧪 Development checks
|
|
96
|
+
|
|
97
|
+
Run the lightweight syntax checks from the project root:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
node --check lib/index.js
|
|
101
|
+
node --check lib/client.js
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 🗺️ Quick reference
|
|
105
|
+
|
|
106
|
+
- **Target:** DeepSeek Harness web profile
|
|
107
|
+
- **Install mode:** npm package
|
|
108
|
+
- **Surface:** lower-right floating control
|
|
109
|
+
- **Action:** terminate the dsh host process
|
|
110
|
+
- **Default port mentioned by dsh:** `127.0.0.1:3080`
|
|
111
|
+
- **Repository topics:** `dsh` · `deepseek-harness` · `plugin` · `javascript` · `web`
|
|
112
|
+
|
|
113
|
+
The power icon comes from [Lucide](https://lucide.dev/icons/power) and is used under its ISC license.
|
|
114
|
+
|
|
115
|
+
<div align="center">
|
|
116
|
+
|
|
117
|
+
<sub>Small surface, explicit confirmation, predictable exit behavior.</sub>
|
|
118
|
+
|
|
119
|
+
</div>
|
|
120
|
+
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# dsh-exit
|
|
4
|
+
|
|
5
|
+
**为 DeepSeek Harness Web 界面提供一个明确、可确认的退出控制。**
|
|
6
|
+
|
|
7
|
+
[](https://npmjs.com/package/@deepseek-ai/dsh)
|
|
8
|
+
[](https://www.npmjs.com/package/@kesike/dsh-exit)
|
|
9
|
+
[](https://github.com/KeS1Ke/dsh-exit)
|
|
10
|
+
[](package.json)
|
|
11
|
+
[](package.json)
|
|
12
|
+
|
|
13
|
+
**简体中文** | [English](README.md)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
> [!IMPORTANT]
|
|
18
|
+
> 确认退出后会结束 dsh 宿主进程并释放其占用的端口;宿主终端窗口和当前浏览器标签页会被刻意保留。
|
|
19
|
+
|
|
20
|
+
## ✨ 功能概览
|
|
21
|
+
|
|
22
|
+
`dsh-exit` 在 dsh Web 界面右下角增加一个紧凑的悬浮**退出**按钮。
|
|
23
|
+
|
|
24
|
+
- 先弹出确认弹窗,确认前不会执行退出动作。
|
|
25
|
+
- 确认后调用类型化的 `dshExit/exit` Remote 方法。
|
|
26
|
+
- 先返回调用应答,再等待短暂窗口结束宿主进程。
|
|
27
|
+
- 释放该进程占用的全部端口,包括常见的 `127.0.0.1:3080`。
|
|
28
|
+
- 保留宿主终端窗口和浏览器标签页,方便后续操作。
|
|
29
|
+
- 使用 dsh 现有设计令牌与 Lucide `power` 电源图标。
|
|
30
|
+
|
|
31
|
+
## 🎬 动态退出流程
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img src="docs/dsh-exit-landscape.webp" alt="由 Archify 生成的横版 dsh-exit 动态流程图" width="720">
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
## 📸 真实 dsh 界面
|
|
38
|
+
|
|
39
|
+
下图来自已经安装 `dsh-exit` 的真实 dsh Web profile;右下角的红色电源按钮就是插件实际渲染的控件。
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<img src="docs/dsh-exit-real.png" alt="运行中的 dsh Web 界面与右下角 dsh-exit 电源按钮" width="720">
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
## 🧱 项目组成
|
|
46
|
+
|
|
47
|
+
| 层次 | 文件 | 职责 |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| 宿主侧 | [`lib/index.js`](lib/index.js) | 注册 `dshExit` Cordis 服务和类型化 `exit()` Remote 方法。 |
|
|
50
|
+
| 客户端 | [`lib/client.js`](lib/client.js) | 自包含浏览器 bundle:按钮、样式、弹窗、键盘交互和 Remote 挂载。 |
|
|
51
|
+
| Bundle 补丁 | [`cordis.patch.yml`](cordis.patch.yml) | 向 dsh profile 插入插件行。 |
|
|
52
|
+
| 包元数据 | [`package.json`](package.json) | 声明宿主入口、Web 客户端和 Bundle 补丁。 |
|
|
53
|
+
| 动态流程 | [`docs/dsh-exit-landscape.webp`](docs/dsh-exit-landscape.webp) | 横版 Archify trace 动画,以动画 WebP 直接内嵌。 |
|
|
54
|
+
| 真实界面截图 | [`docs/dsh-exit-real.png`](docs/dsh-exit-real.png) | 已运行 dsh Web profile 中的实际按钮。 |
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## 🚀 从 npm 安装
|
|
58
|
+
|
|
59
|
+
直接将 npm 包加入 dsh 的 Web profile:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
dsh plugin --profile web add @kesike/dsh-exit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
安装后重启 profile。裸导入由 dsh 宿主加载器解析,因此本包不声明运行时依赖。更新或移除:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
dsh plugin --profile web update @kesike/dsh-exit
|
|
69
|
+
dsh plugin --profile web remove @kesike/dsh-exit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 🖥️ 交互说明
|
|
73
|
+
|
|
74
|
+
| 操作 | 结果 |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| 点击悬浮电源按钮 | 打开确认弹窗。 |
|
|
77
|
+
| 点击**取消**、按 `Esc` 或点击弹窗外部 | 关闭弹窗,不改变宿主状态。 |
|
|
78
|
+
| 点击**确认退出** | 禁用重复操作并调用宿主 Remote 方法。 |
|
|
79
|
+
| Remote 调用失败 | 恢复控件并显示行内错误信息。 |
|
|
80
|
+
| Remote 调用成功 | 关闭弹窗,宿主随后退出。 |
|
|
81
|
+
|
|
82
|
+
## 🔌 Remote 接口
|
|
83
|
+
|
|
84
|
+
宿主侧公开一个严格校验、无参数的 Remote 方法:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
namespace: dshExit
|
|
88
|
+
method: exit
|
|
89
|
+
result: true
|
|
90
|
+
gateway: /api/dshExit/exit
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
dsh 客户端网关成功时返回标准 envelope:`{ ok: true, value: true }`。服务端通过 `exiting` 标志防止重复调用,并在应答有机会完成后延迟执行 `process.exit(0)`。
|
|
94
|
+
|
|
95
|
+
## 🧪 开发检查
|
|
96
|
+
|
|
97
|
+
在项目根目录执行轻量语法检查:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
node --check lib/index.js
|
|
101
|
+
node --check lib/client.js
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 🗺️ 快速信息
|
|
105
|
+
|
|
106
|
+
- **目标:** DeepSeek Harness Web profile
|
|
107
|
+
- **安装方式:** npm 包
|
|
108
|
+
- **界面位置:** 右下角悬浮控件
|
|
109
|
+
- **执行动作:** 结束 dsh 宿主进程
|
|
110
|
+
- **dsh 常见端口:** `127.0.0.1:3080`
|
|
111
|
+
- **仓库主题:** `dsh` · `deepseek-harness` · `plugin` · `javascript` · `web`
|
|
112
|
+
|
|
113
|
+
电源图标来自 [Lucide](https://lucide.dev/icons/power),遵循其 ISC 许可使用。
|
|
114
|
+
|
|
115
|
+
<div align="center">
|
|
116
|
+
|
|
117
|
+
<sub>界面小而专注,退出动作明确且可预期。</sub>
|
|
118
|
+
|
|
119
|
+
</div>
|
|
120
|
+
|
package/cordis.patch.yml
ADDED
|
Binary file
|
|
Binary file
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//#region dsh-exit 浏览器客户端 bundle
|
|
2
|
+
/**
|
|
3
|
+
* dsh-exit 客户端:
|
|
4
|
+
* - 在整个界面右下角挂载紧凑的退出按钮
|
|
5
|
+
* - 退出按钮首次点击弹确认弹窗;确认后调用 remote.dshExit.exit(),
|
|
6
|
+
* 服务端结束宿主进程并释放端口,不操作终端窗口或浏览器标签页
|
|
7
|
+
*
|
|
8
|
+
* 无外部依赖:全部逻辑在本工厂内完成,不 require 任何模块。
|
|
9
|
+
*/
|
|
10
|
+
window.__ModuleLoader__.load({
|
|
11
|
+
id: "@kesike/dsh-exit",
|
|
12
|
+
factory: () => {
|
|
13
|
+
var module = { exports: {} };
|
|
14
|
+
var exports = module.exports;
|
|
15
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
16
|
+
|
|
17
|
+
/** 与 archive-manager「删除会话」相同的错误红(设计令牌)。 */
|
|
18
|
+
const ERROR_RED = "var(--dsw-alias-state-error-primary)";
|
|
19
|
+
|
|
20
|
+
/** Lucide power 图标(ISC 许可,免费),16px 线性风格贴合现有按钮。 */
|
|
21
|
+
const ICON_SVG = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>';
|
|
22
|
+
|
|
23
|
+
const CSS_TEXT = [
|
|
24
|
+
".dshExitBtn{position:fixed;right:12px;bottom:12px;left:auto;z-index:2147482000;box-sizing:border-box;width:32px;height:32px;padding:0;display:grid;place-items:center;cursor:pointer;border:1px solid " + ERROR_RED + ";border-radius:50%;background:var(--dsw-alias-button-elevated-fill);color:" + ERROR_RED + ";box-shadow:0 2px 8px rgba(0,0,0,.24);transition:background .12s ease,transform .12s ease}",
|
|
25
|
+
".dshExitBtn svg{width:16px;height:16px;flex:none}",
|
|
26
|
+
".dshExitBtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);transform:translateY(-1px)}",
|
|
27
|
+
".dshExitBtn:focus-visible{outline:2px solid " + ERROR_RED + ";outline-offset:2px}",
|
|
28
|
+
".dshExitBtn:disabled{cursor:default;opacity:.7}",
|
|
29
|
+
".dshExitOverlay{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;background:rgba(15,20,25,.45);animation:dshExitFade .12s ease-out}",
|
|
30
|
+
"@keyframes dshExitFade{from{opacity:0}}",
|
|
31
|
+
".dshExitModal{box-sizing:border-box;width:min(380px,calc(100vw - 48px));border-radius:16px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);box-shadow:0 24px 64px rgba(0,0,0,.35);padding:24px;display:flex;flex-direction:column;gap:12px;animation:dshExitPop .14s ease-out}",
|
|
32
|
+
"@keyframes dshExitPop{from{opacity:0;transform:translateY(6px) scale(.98)}}",
|
|
33
|
+
".dshExitTitle{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
34
|
+
".dshExitBody{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
|
|
35
|
+
".dshExitError{margin:0;min-height:0;font-size:12px;line-height:18px;color:" + ERROR_RED + "}",
|
|
36
|
+
".dshExitActions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}",
|
|
37
|
+
".dshExitCancel{cursor:pointer;height:32px;padding:0 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}",
|
|
38
|
+
".dshExitCancel:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
39
|
+
".dshExitCancel:disabled{cursor:default;opacity:.6}",
|
|
40
|
+
".dshExitConfirm{cursor:pointer;height:32px;padding:0 14px;border-radius:10px;border:none;background:" + ERROR_RED + ";color:#fff;font-size:13px;font-weight:600}",
|
|
41
|
+
".dshExitConfirm:hover:not(:disabled){filter:brightness(1.08)}",
|
|
42
|
+
".dshExitConfirm:disabled{cursor:default;opacity:.6}"
|
|
43
|
+
].join("\n");
|
|
44
|
+
|
|
45
|
+
/** 客户端严格编解码 shim:与服务端 exitedSchema 对应。 */
|
|
46
|
+
const exitedClientSchema = {
|
|
47
|
+
parse(value) {
|
|
48
|
+
if (value !== true) throw new TypeError("exit must return true");
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** 客户端 Remote 描述符:挂载后 ctx.get("remote.dshExit") 可用。 */
|
|
54
|
+
const EXIT_REMOTE = {
|
|
55
|
+
package: "@kesike/dsh-exit",
|
|
56
|
+
descriptors: [
|
|
57
|
+
{
|
|
58
|
+
id: "@kesike/dsh-exit#dshExit/exit",
|
|
59
|
+
service: "dshExit",
|
|
60
|
+
namespace: "dshExit",
|
|
61
|
+
method: "exit",
|
|
62
|
+
invocation: { kind: "direct" },
|
|
63
|
+
parameters: [],
|
|
64
|
+
result: {
|
|
65
|
+
mode: "strict",
|
|
66
|
+
typeSymbol: "@kesike/dsh-exit/types#Exited",
|
|
67
|
+
schema: exitedClientSchema
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let exiting = false;
|
|
74
|
+
let exitBtnEl = null;
|
|
75
|
+
let overlayEl = null;
|
|
76
|
+
let escHandler = null;
|
|
77
|
+
|
|
78
|
+
function injectCss() {
|
|
79
|
+
if (document.querySelector("style[data-plugin-css='dsh-exit']")) return;
|
|
80
|
+
const tag = document.createElement("style");
|
|
81
|
+
tag.dataset.pluginCss = "dsh-exit";
|
|
82
|
+
tag.textContent = CSS_TEXT;
|
|
83
|
+
document.head.appendChild(tag);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createExitButton() {
|
|
87
|
+
const exitBtn = document.createElement("button");
|
|
88
|
+
exitBtn.className = "dshExitBtn";
|
|
89
|
+
exitBtn.type = "button";
|
|
90
|
+
exitBtn.disabled = false;
|
|
91
|
+
exitBtn.setAttribute("aria-label", "退出");
|
|
92
|
+
exitBtn.title = "退出 DeepSeek Harness";
|
|
93
|
+
exitBtn.dataset.dshExitButton = "true";
|
|
94
|
+
exitBtn.innerHTML = ICON_SVG;
|
|
95
|
+
return exitBtn;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function mount(ctx) {
|
|
99
|
+
const existing = document.querySelector("[data-dsh-exit-button='true']");
|
|
100
|
+
if (existing) {
|
|
101
|
+
exitBtnEl = existing;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!document.body) return;
|
|
105
|
+
const exitBtn = createExitButton();
|
|
106
|
+
exitBtn.addEventListener("click", (event) => {
|
|
107
|
+
event.stopPropagation();
|
|
108
|
+
openModal(ctx, exitBtn);
|
|
109
|
+
});
|
|
110
|
+
document.body.appendChild(exitBtn);
|
|
111
|
+
exitBtnEl = exitBtn;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function closeModal() {
|
|
115
|
+
if (escHandler) {
|
|
116
|
+
document.removeEventListener("keydown", escHandler);
|
|
117
|
+
escHandler = null;
|
|
118
|
+
}
|
|
119
|
+
if (overlayEl) {
|
|
120
|
+
overlayEl.remove();
|
|
121
|
+
overlayEl = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function openModal(ctx, exitBtn) {
|
|
126
|
+
closeModal();
|
|
127
|
+
overlayEl = document.createElement("div");
|
|
128
|
+
overlayEl.className = "dshExitOverlay";
|
|
129
|
+
const panel = document.createElement("div");
|
|
130
|
+
panel.className = "dshExitModal";
|
|
131
|
+
panel.setAttribute("role", "dialog");
|
|
132
|
+
panel.setAttribute("aria-modal", "true");
|
|
133
|
+
panel.setAttribute("aria-label", "退出 DeepSeek Harness");
|
|
134
|
+
const title = document.createElement("h3");
|
|
135
|
+
title.className = "dshExitTitle";
|
|
136
|
+
title.textContent = "退出 DeepSeek Harness?";
|
|
137
|
+
const body = document.createElement("p");
|
|
138
|
+
body.className = "dshExitBody";
|
|
139
|
+
body.textContent = "退出后将结束所有运行中的会话并停止后台服务,3080 端口会随之释放。";
|
|
140
|
+
const errorEl = document.createElement("p");
|
|
141
|
+
errorEl.className = "dshExitError";
|
|
142
|
+
const actions = document.createElement("div");
|
|
143
|
+
actions.className = "dshExitActions";
|
|
144
|
+
const cancel = document.createElement("button");
|
|
145
|
+
cancel.type = "button";
|
|
146
|
+
cancel.className = "dshExitCancel";
|
|
147
|
+
cancel.textContent = "取消";
|
|
148
|
+
const confirm = document.createElement("button");
|
|
149
|
+
confirm.type = "button";
|
|
150
|
+
confirm.className = "dshExitConfirm";
|
|
151
|
+
confirm.textContent = "确认退出";
|
|
152
|
+
actions.append(cancel, confirm);
|
|
153
|
+
panel.append(title, body, errorEl, actions);
|
|
154
|
+
overlayEl.appendChild(panel);
|
|
155
|
+
|
|
156
|
+
escHandler = (event) => {
|
|
157
|
+
if (event.key === "Escape" && !confirm.disabled) closeModal();
|
|
158
|
+
};
|
|
159
|
+
document.addEventListener("keydown", escHandler);
|
|
160
|
+
overlayEl.addEventListener("click", (event) => {
|
|
161
|
+
if (event.target === overlayEl && !confirm.disabled) closeModal();
|
|
162
|
+
});
|
|
163
|
+
cancel.addEventListener("click", () => {
|
|
164
|
+
if (!confirm.disabled) closeModal();
|
|
165
|
+
});
|
|
166
|
+
confirm.addEventListener("click", () => {
|
|
167
|
+
exiting = true;
|
|
168
|
+
confirm.disabled = true;
|
|
169
|
+
cancel.disabled = true;
|
|
170
|
+
errorEl.textContent = "";
|
|
171
|
+
exitBtn.disabled = true;
|
|
172
|
+
exitBtn.setAttribute("aria-label", "正在退出…");
|
|
173
|
+
Promise.resolve()
|
|
174
|
+
.then(() => {
|
|
175
|
+
const service = ctx.get("remote.dshExit");
|
|
176
|
+
if (service === undefined) throw new Error("dsh-exit 服务不可用(宿主未注册?)");
|
|
177
|
+
return service.exit();
|
|
178
|
+
})
|
|
179
|
+
.then((result) => {
|
|
180
|
+
if (!result || result.ok !== true) {
|
|
181
|
+
throw new Error((result && result.error && result.error.message) || "退出调用失败");
|
|
182
|
+
}
|
|
183
|
+
closeModal();
|
|
184
|
+
})
|
|
185
|
+
.catch((reason) => {
|
|
186
|
+
exiting = false;
|
|
187
|
+
confirm.disabled = false;
|
|
188
|
+
cancel.disabled = false;
|
|
189
|
+
exitBtn.disabled = false;
|
|
190
|
+
exitBtn.setAttribute("aria-label", "退出");
|
|
191
|
+
errorEl.textContent = "退出失败:" + (reason instanceof Error ? reason.message : String(reason));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
document.body.appendChild(overlayEl);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function apply(ctx) {
|
|
198
|
+
injectCss();
|
|
199
|
+
mount(ctx);
|
|
200
|
+
const observer = new MutationObserver(() => {
|
|
201
|
+
if (!exiting) mount(ctx);
|
|
202
|
+
});
|
|
203
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
204
|
+
let disposeRemote = () => {};
|
|
205
|
+
const remote = ctx.get("remote");
|
|
206
|
+
if (remote !== undefined) disposeRemote = await remote.$mount(EXIT_REMOTE);
|
|
207
|
+
return async () => {
|
|
208
|
+
observer.disconnect();
|
|
209
|
+
closeModal();
|
|
210
|
+
if (exitBtnEl) {
|
|
211
|
+
exitBtnEl.remove();
|
|
212
|
+
exitBtnEl = null;
|
|
213
|
+
}
|
|
214
|
+
disposeRemote();
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
exports.apply = apply;
|
|
219
|
+
return module.exports;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
//#endregion
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//#region dsh-exit 宿主侧
|
|
2
|
+
/**
|
|
3
|
+
* dsh-exit:把「退出 dsh 宿主并释放端口」暴露给浏览器的一个宿主服务。
|
|
4
|
+
*
|
|
5
|
+
* 远程面:`remote.dshExit.exit()`(Typert Remote,经 /api/dshExit/exit 网关)。
|
|
6
|
+
* 调用后:先应答,再延迟 400ms 结束宿主进程——端口随进程退出释放;
|
|
7
|
+
* 不操作宿主终端窗口或系统托盘。
|
|
8
|
+
*/
|
|
9
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
10
|
+
|
|
11
|
+
const exitedSchema = {
|
|
12
|
+
parse(value) {
|
|
13
|
+
if (value !== true) throw new TypeError(`exit must return true, got ${String(value)}`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Host 严格描述符。网关优先读 typert.local,避免生产环境 SRC 扫描 404。 */
|
|
19
|
+
const EXIT_INVOCATIONS = [
|
|
20
|
+
{
|
|
21
|
+
id: "@kesike/dsh-exit#dshExit/exit",
|
|
22
|
+
service: "dshExit",
|
|
23
|
+
namespace: "dshExit",
|
|
24
|
+
method: "exit",
|
|
25
|
+
invocation: { kind: "direct" },
|
|
26
|
+
parameters: [],
|
|
27
|
+
result: {
|
|
28
|
+
mode: "strict",
|
|
29
|
+
typeSymbol: "@kesike/dsh-exit/types#Exited",
|
|
30
|
+
schema: exitedSchema
|
|
31
|
+
},
|
|
32
|
+
sourceLocation: { file: "@kesike/dsh-exit/lib/index.js", line: 1, column: 1 }
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const EXIT_TYPERT = {
|
|
37
|
+
package: "@kesike/dsh-exit",
|
|
38
|
+
face: "host",
|
|
39
|
+
schemas: [],
|
|
40
|
+
model: { services: [], events: [], objects: [] },
|
|
41
|
+
invocations: EXIT_INVOCATIONS
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 模拟 TS 装饰器管线 `@Remote(method)`:`Remote` 返回标准方法装饰器,
|
|
46
|
+
* 这里构造一个 addInitializer 立即以 `this` = instance 执行的装饰器上下文。
|
|
47
|
+
*/
|
|
48
|
+
function markRemoteMethod(instance, method) {
|
|
49
|
+
const context = {
|
|
50
|
+
private: false,
|
|
51
|
+
static: false,
|
|
52
|
+
name: method,
|
|
53
|
+
addInitializer(fn) {
|
|
54
|
+
fn.call(instance);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
Remote(method)(void 0, context);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class DshExitService extends TypertRemoteService {
|
|
61
|
+
static name = "dshExit";
|
|
62
|
+
static inject = ["typert"];
|
|
63
|
+
exiting = false;
|
|
64
|
+
|
|
65
|
+
constructor(ctx) {
|
|
66
|
+
super(ctx, "dshExit");
|
|
67
|
+
markRemoteMethod(this, "exit");
|
|
68
|
+
const existing = ctx.get("typert");
|
|
69
|
+
if (existing !== undefined) {
|
|
70
|
+
existing.register(EXIT_TYPERT);
|
|
71
|
+
} else {
|
|
72
|
+
ctx.inject(["typert"], (typertCtx) => {
|
|
73
|
+
typertCtx.typert.register(EXIT_TYPERT);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 结束宿主并释放端口;不触碰宿主终端窗口。 */
|
|
79
|
+
async exit() {
|
|
80
|
+
if (this.exiting) return true;
|
|
81
|
+
this.exiting = true;
|
|
82
|
+
this.ctx.logger?.info?.("dsh-exit: exit requested from the web UI");
|
|
83
|
+
setTimeout(() => process.exit(0), 400);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function apply(ctx) {
|
|
89
|
+
ctx.plugin(DshExitService);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { apply };
|
|
93
|
+
//#endregion
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kesike/dsh-exit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "在 dsh web 界面提供退出按钮:确认后结束宿主进程并释放端口",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dsh",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"plugin",
|
|
11
|
+
"web",
|
|
12
|
+
"exit"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/KeS1Ke/dsh-exit#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/KeS1Ke/dsh-exit.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/KeS1Ke/dsh-exit/issues"
|
|
21
|
+
},
|
|
22
|
+
"main": "lib/index.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./lib/index.js",
|
|
25
|
+
"./client": "./lib/client.js",
|
|
26
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.19.0 || >=24.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib",
|
|
34
|
+
"cordis.patch.yml",
|
|
35
|
+
"README.md",
|
|
36
|
+
"README.zh-CN.md",
|
|
37
|
+
"docs/dsh-exit-landscape.webp",
|
|
38
|
+
"docs/dsh-exit-real.png"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
},
|
|
44
|
+
"dsh": {
|
|
45
|
+
"bundle": {
|
|
46
|
+
"patch": "./cordis.patch.yml"
|
|
47
|
+
},
|
|
48
|
+
"client": {
|
|
49
|
+
"platform": "web"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|