dsh-plugin-easy2restart 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +81 -2
- package/cordis.patch.yml +11 -0
- package/icon.svg +12 -0
- package/lib/client.js +206 -0
- package/lib/index.js +274 -0
- package/package.json +45 -3
- package/restart/restart.ps1 +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-plugin-easy2restart contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-easy2restart
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **DSH 重启插件:双击侧边栏重启按钮即可整体重启 DeepSeek Harness,会话运行时按钮置灰禁用。**
|
|
4
|
+
> **DSH plugin: double-click the sidebar restart button to fully relaunch DeepSeek Harness; it is greyed out while an agent session is running.**
|
|
5
|
+
|
|
6
|
+
DeepSeek Harness 重启插件:在左侧边栏**账号头像/设置上方**放一个「🔄 重启 DSH」按钮,**双击**即可整进程重启 DeepSeek Harness;当有 Agent 会话正在运行时,按钮自动置灰禁用,避免打断进行中的工作。
|
|
7
|
+
|
|
8
|
+
## 功能
|
|
9
|
+
|
|
10
|
+
- 侧边栏 `sidebar.footer.action` 位置注入重启按钮(账号头像/设置上方)。
|
|
11
|
+
- 双击按钮 → 宿主用 Windows 计划任务拉起一个**脱离 DSH 进程树**的 PowerShell 脚本,`taskkill /T` 强杀整个 DSH 进程树,随后重新启动 `DeepSeek Harness.exe`。
|
|
12
|
+
- 有 Agent 会话**运行中**时按钮置灰禁用(依据 DSH 会话状态 `running`)。
|
|
13
|
+
- 无会话运行时按钮可点;宿主管理保存一个「重启待办」,便于后续扩展「重启后回到原会话」等流程。
|
|
14
|
+
|
|
15
|
+
<img width="2558" height="1638" alt="dsh-pluin-restart-02" src="https://github.com/user-attachments/assets/1cd364ed-3048-4104-928c-46566ba9bcc6" />
|
|
16
|
+
|
|
17
|
+
<img width="2561" height="1638" alt="dsh-pluin-restart-01" src="https://github.com/user-attachments/assets/93e5d1e5-6647-4e03-9f8f-e491e3a4640d" />
|
|
18
|
+
|
|
19
|
+
## 依赖
|
|
20
|
+
|
|
21
|
+
- DeepSeek Harness 桌面版(0.2.0-rc 以上)
|
|
22
|
+
- Node.js ≥ 22
|
|
23
|
+
|
|
24
|
+
## 安装
|
|
25
|
+
|
|
26
|
+
> 前提:`dsh` 命令在 PATH 中(DeepSeek Harness 自带)。
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 用 link 方式装本地副本(推荐,便于维护/调试)
|
|
30
|
+
git clone https://github.com/SMPcosy/dsh-plugin-easy2restart
|
|
31
|
+
dsh plugin --profile desktop add link:<克隆或本目录的绝对路径>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
若发布到 registry:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dsh plugin --profile desktop add dsh-plugin-easy2restart
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
装完**重启 DeepSeek Harness**,侧边栏底部账号头像上方即出现「🔄」按钮。
|
|
41
|
+
|
|
42
|
+
## 使用
|
|
43
|
+
|
|
44
|
+
- **双击**「🔄」按钮即可重启 DSH。
|
|
45
|
+
- 当有会话正在运行时按钮为灰色(不可点),会话空闲时可正常重启。
|
|
46
|
+
- 重启由计划任务拉起,与 DSH 自身退出无关,即使 DSH 被强杀也能自动重新启动。
|
|
47
|
+
|
|
48
|
+
## 卸载
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
dsh plugin --profile desktop remove dsh-plugin-easy2restart
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
然后重启 DSH 使改动生效。
|
|
55
|
+
|
|
56
|
+
## 工作原理(简述)
|
|
57
|
+
|
|
58
|
+
- `lib/index.js`(宿主/Cordis 插件):注册 `/api/dsh-plugin-easy2restart/*` HTTP 路由;`/restart` 记录待办并调用 `scheduleRestart`,该函数用 `schtasks` 创建一个立即运行的重启任务,由计划任务服务启动(**脱离 DSH 进程树**),执行 `restart/restart.ps1`。
|
|
59
|
+
- `restart/restart.ps1`:`taskkill /PID <主进程> /T /F` 结束整个 DSH 进程树,等待退出后重新 `Start-Process DeepSeek Harness.exe`。
|
|
60
|
+
- `lib/client.js`(Web 客户端):注入 `sidebar.footer.action` 渲染按钮;用 `useSessionStatus` 判断是否有会话在运行以决定是否禁用。
|
|
61
|
+
|
|
62
|
+
## 目录结构
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
dsh-plugin-easy2restart/
|
|
66
|
+
├─ package.json # 插件清单(dsh.client / dsh.bundle 声明)
|
|
67
|
+
├─ cordis.patch.yml # bundle 挂载声明
|
|
68
|
+
├─ lib/
|
|
69
|
+
│ ├─ index.js # 宿主端:HTTP 路由 + 重启调度
|
|
70
|
+
│ └─ client.js # 客户端端:侧边栏按钮
|
|
71
|
+
└─ restart/
|
|
72
|
+
└─ restart.ps1 # 重启脚本(杀进程树 + 重启 exe)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 备注
|
|
76
|
+
|
|
77
|
+
- 这是改安装版主程序之外的一种插件方式:直接以本地 bundle 加入 profile,不修改 `app.asar`。
|
|
78
|
+
- 若希望「重启完成后自动回到原会话并填入『重启完成』」,可在 `/restart` 的待办(`~/.dsh/storages/dsh-plugin-easy2restart/pending.json`)基础上扩展(当前版本不自动填写输入框)。
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# dsh-plugin-easy2restart — DSH bundle patch
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# 将重启插件插入当前 profile 的配置树(宿主插件 + Web 客户端端)。
|
|
5
|
+
# 安装方式(无需手工改 profile):
|
|
6
|
+
# dsh plugin --profile desktop add link:<本目录绝对路径>
|
|
7
|
+
# 卸载:把本条目从 profile 的 cordis.patch.yml / bundle 列表移除后重启 DSH。
|
|
8
|
+
|
|
9
|
+
- insert:
|
|
10
|
+
- id: dsh-plugin-easy2restart
|
|
11
|
+
name: dsh-plugin-easy2restart
|
package/icon.svg
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#4f7cff"/>
|
|
5
|
+
<stop offset="1" stop-color="#6f5bff"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect x="2" y="2" width="44" height="44" rx="10" fill="url(#bg)"/>
|
|
9
|
+
<!-- 居中「🔄」循环箭头符号 -->
|
|
10
|
+
<text x="24" y="32" text-anchor="middle" dominant-baseline="central"
|
|
11
|
+
font-size="26" fill="#ffffff" font-family="'Segoe UI Symbol','Apple Color Emoji','Noto Color Emoji',sans-serif">🔄</text>
|
|
12
|
+
</svg>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-easy2restart — Web 客户端(客户端半侧)
|
|
3
|
+
*
|
|
4
|
+
* 挂载点:sidebar.footer.action(侧边栏底部、账号头像/设置上方)。
|
|
5
|
+
* 交互:
|
|
6
|
+
* - 双击按钮 => 调用宿主 /restart 记录「重启完成」待办并重启 DSH。
|
|
7
|
+
* - 有 Agent 会话运行中时按钮置灰禁用(通过宿主 /active 轮询判定)。
|
|
8
|
+
* - 重启后,DSH 恢复会话;本组件的「自动发送」交给宿主端在频道
|
|
9
|
+
* (apply 后检测 pending)尽力恢复会话并注入『重启完成』用户消息。
|
|
10
|
+
*
|
|
11
|
+
* 数据/动作走宿主 HTTP 路由:
|
|
12
|
+
* GET /api/dsh-plugin-easy2restart/active -> { active }
|
|
13
|
+
* GET /api/dsh-plugin-easy2restart/state -> { data: pending }
|
|
14
|
+
* POST /api/dsh-plugin-easy2restart/restart -> { sessionId, text }
|
|
15
|
+
* POST /api/dsh-plugin-easy2restart/clear
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
window.__ModuleLoader__.load({
|
|
19
|
+
id: "dsh-plugin-easy2restart",
|
|
20
|
+
factory: (require) => {
|
|
21
|
+
const module = { exports: {} };
|
|
22
|
+
const exports = module.exports;
|
|
23
|
+
|
|
24
|
+
const { createElement: h, useState, useEffect, useRef, useCallback } = require("react");
|
|
25
|
+
|
|
26
|
+
const NS = "dsh-plugin-easy2restart";
|
|
27
|
+
const API = "/api/dsh-plugin-easy2restart";
|
|
28
|
+
const POLL_MS = 1200;
|
|
29
|
+
|
|
30
|
+
const zh = {
|
|
31
|
+
tooltip: "重启 DSH",
|
|
32
|
+
tooltipDisabled: "有会话运行中,无法重启",
|
|
33
|
+
done: "重启完成",
|
|
34
|
+
restartHint: "双击重启",
|
|
35
|
+
unavailable: "宿主不可用",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function RestartButton(props) {
|
|
39
|
+
const { wide, useSessionStatus } = props || {};
|
|
40
|
+
// 诊断回写宿主(host 写入 diag.log)
|
|
41
|
+
function diag(msg) {
|
|
42
|
+
try {
|
|
43
|
+
fetch(API + "/diag", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "content-type": "application/json" },
|
|
46
|
+
body: JSON.stringify({ msg }),
|
|
47
|
+
}).catch(() => {});
|
|
48
|
+
} catch (e) {
|
|
49
|
+
/* 忽略 */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// 是否正在重启(防重复触发)
|
|
53
|
+
const [restarting, setRestarting] = useState(false);
|
|
54
|
+
// 宿主是否可及(/health 轮询)
|
|
55
|
+
const [alive, setAlive] = useState(true);
|
|
56
|
+
const busyRef = useRef(false);
|
|
57
|
+
|
|
58
|
+
// 是否有会话正在运行(禁用依据)。useSessionStatus 是 DSH 注入的快照 hook,
|
|
59
|
+
// snapshot 为 Map:sessionId -> status(含 running 字段)。若未见注入则视为无运行。
|
|
60
|
+
const running = (typeof useSessionStatus === "function" ? useSessionStatus : () => false)((snapshot) => {
|
|
61
|
+
if (!snapshot || typeof snapshot.values !== "function") return false;
|
|
62
|
+
for (const st of snapshot.values()) {
|
|
63
|
+
if (st && st.running) return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const fetchJson = useCallback(async (path, options) => {
|
|
69
|
+
const res = await fetch(`${API}${path}`, {
|
|
70
|
+
headers: { "content-type": "application/json" },
|
|
71
|
+
...(options || {}),
|
|
72
|
+
});
|
|
73
|
+
return res.json().catch(() => ({}));
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
// 轮询宿主存活性(存在 /health 路由 = alive)
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
let stop = false;
|
|
79
|
+
const tick = async () => {
|
|
80
|
+
if (stop) return;
|
|
81
|
+
try {
|
|
82
|
+
const r = await fetch(`${API}/health`);
|
|
83
|
+
if (!stop) setAlive(r.status >= 200 && r.status < 500);
|
|
84
|
+
} catch {
|
|
85
|
+
if (!stop) setAlive(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
tick();
|
|
89
|
+
const timer = window.setInterval(tick, POLL_MS);
|
|
90
|
+
return () => {
|
|
91
|
+
stop = true;
|
|
92
|
+
window.clearInterval(timer);
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
// 双击触发重启
|
|
97
|
+
const onDoubleClick = async () => {
|
|
98
|
+
if (busyRef.current || restarting || running) return;
|
|
99
|
+
busyRef.current = true;
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`${API}/restart`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "content-type": "application/json" },
|
|
104
|
+
body: JSON.stringify({ sessionId: null, text: zh.done }),
|
|
105
|
+
});
|
|
106
|
+
const j = await res.json().catch(() => ({}));
|
|
107
|
+
diag("double-click /restart status=" + res.status + " ok=" + !!j.ok);
|
|
108
|
+
if (j && j.ok) setRestarting(true);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
diag("double-click /restart fetch error: " + (e && e.message ? e.message : String(e)));
|
|
111
|
+
setAlive(false);
|
|
112
|
+
} finally {
|
|
113
|
+
busyRef.current = false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const disabled = running || restarting || !alive;
|
|
118
|
+
const label = running
|
|
119
|
+
? zh.tooltipDisabled
|
|
120
|
+
: restarting
|
|
121
|
+
? zh.done
|
|
122
|
+
: alive
|
|
123
|
+
? zh.tooltip
|
|
124
|
+
: zh.unavailable;
|
|
125
|
+
|
|
126
|
+
const style = {
|
|
127
|
+
boxSizing: "border-box",
|
|
128
|
+
width: wide ? "100%" : 30,
|
|
129
|
+
height: 30,
|
|
130
|
+
display: "inline-flex",
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
justifyContent: "center",
|
|
133
|
+
gap: 6,
|
|
134
|
+
border: "none",
|
|
135
|
+
borderRadius: 6,
|
|
136
|
+
background: "transparent",
|
|
137
|
+
color: disabled ? "var(--dsw-alias-label-dimmed, #9aa0a6)" : "var(--dsw-alias-label-secondary, #5f6368)",
|
|
138
|
+
cursor: disabled ? "default" : "pointer",
|
|
139
|
+
fontSize: 15,
|
|
140
|
+
opacity: disabled ? 0.45 : 1,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return h(
|
|
144
|
+
"button",
|
|
145
|
+
{
|
|
146
|
+
type: "button",
|
|
147
|
+
"aria-label": label,
|
|
148
|
+
title: `${label} — ${zh.restartHint}`,
|
|
149
|
+
style,
|
|
150
|
+
disabled,
|
|
151
|
+
onDoubleClick,
|
|
152
|
+
},
|
|
153
|
+
wide ? [h("span", { key: "ic", style: { fontSize: 14 } }, "🔄"), h("span", { key: "tx", style: { fontSize: 12 } }, "重启 DSH")] : h("span", { style: { fontSize: 15 } }, "🔄"),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
exports.name = NS;
|
|
158
|
+
exports.inject = ["slots", "locale"];
|
|
159
|
+
|
|
160
|
+
exports.apply = function apply(ctx) {
|
|
161
|
+
// 诊断回写宿主(host 写入 ~/.dsh/storages/dsh-plugin-easy2restart/diag.log),便于排查 client 加载链路。
|
|
162
|
+
function diag(msg) {
|
|
163
|
+
try {
|
|
164
|
+
fetch(API + "/diag", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "content-type": "application/json" },
|
|
167
|
+
body: JSON.stringify({ msg }),
|
|
168
|
+
}).catch(() => {});
|
|
169
|
+
} catch (e) {
|
|
170
|
+
/* 诊断失败忽略 */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
diag("apply-called slots=" + Boolean(ctx && ctx.slots) + " inject=" + Boolean(ctx && ctx.slots && ctx.slots.inject));
|
|
174
|
+
let t = function (key) {
|
|
175
|
+
return key;
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en: { tooltip: "Restart DSH", tooltipDisabled: "Session running; cannot restart", done: "Restart done", restartHint: "Double-click to restart" } }), NS + ": dict");
|
|
179
|
+
t = ctx.locale.bind(NS);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
/* 降级 */
|
|
182
|
+
}
|
|
183
|
+
const label = () => t("tooltip");
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
ctx.slots.inject("sidebar.footer.action", function () {
|
|
187
|
+
try {
|
|
188
|
+
const reg = ctx.slots.register(
|
|
189
|
+
{ name: "sidebar.footer.action", id: NS, order: -100, label, locale: NS },
|
|
190
|
+
RestartButton,
|
|
191
|
+
);
|
|
192
|
+
diag("slot registered sidebar.footer.action");
|
|
193
|
+
return reg;
|
|
194
|
+
} catch (e) {
|
|
195
|
+
diag("slot register fail: " + (e && e.message ? e.message : String(e)));
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
} catch (e) {
|
|
200
|
+
try { if (typeof console !== "undefined" && console && typeof console.warn === "function") console.warn("[" + NS + "] inject fail: " + (e && e.message)); } catch (e2) {}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return module.exports;
|
|
205
|
+
},
|
|
206
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-easy2restart — 宿主端(Cordis 插件)
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* 1. 注册 HTTP 路由,供 Web 客户端调用:
|
|
6
|
+
* GET /api/dsh-plugin-easy2restart/state -> 当前「待处理重启完成」标记(客户端重启后读取)
|
|
7
|
+
* POST /api/dsh-plugin-easy2restart/restart -> { sessionId, text } 记录标记并触发重启 DSH
|
|
8
|
+
* POST /api/dsh-plugin-easy2restart/clear -> 清除标记
|
|
9
|
+
* GET /api/dsh-plugin-easy2restart/health -> 探针
|
|
10
|
+
* 2. 重启:用 Windows 计划任务(schtasks)拉起脱离的 PowerShell 脚本,
|
|
11
|
+
* 强杀 DSH 进程树后重新启动 DeepSeek Harness.exe。
|
|
12
|
+
*
|
|
13
|
+
* 关键约束:宿主插件运行在 Electron 的 Node worker 进程里,
|
|
14
|
+
* process.ppid 是 Electron 主进程;完成后由计划任务(由 Task Scheduler 服务启动)
|
|
15
|
+
* 负责杀进程树并重启,因此不受 DSH 自身退出影响。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { execFile } from "node:child_process";
|
|
23
|
+
|
|
24
|
+
export const name = "dsh-plugin-easy2restart";
|
|
25
|
+
export const inject = ["webServer"];
|
|
26
|
+
|
|
27
|
+
const NS = "dsh-plugin-easy2restart";
|
|
28
|
+
const API_PREFIX = "/api/dsh-plugin-easy2restart";
|
|
29
|
+
|
|
30
|
+
/** 宿主加载诊断:每次启动模块加载/apply 都记一行,便于确认插件是否被 DSH 加载。 */
|
|
31
|
+
function bootMark(stage) {
|
|
32
|
+
try {
|
|
33
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
34
|
+
const file = path.join(home, "storages", NS, "host-boot.log");
|
|
35
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
36
|
+
fs.appendFileSync(file, `${new Date().toISOString()} pid=${process.pid} ${stage}\n`, "utf8");
|
|
37
|
+
} catch {
|
|
38
|
+
/* 诊断失败不阻塞插件 */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
bootMark("module-imported");
|
|
43
|
+
|
|
44
|
+
/** 客户端诊断回写:Web 客户端把加载/注册状态 POST 到此,宿主记录到文件,便于无浏览器控制台时排查。 */
|
|
45
|
+
function diagWrite(msg) {
|
|
46
|
+
try {
|
|
47
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
48
|
+
const file = path.join(home, "storages", NS, "diag.log");
|
|
49
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
50
|
+
fs.appendFileSync(file, `${new Date().toISOString()} ${msg}\n`, "utf8");
|
|
51
|
+
} catch {
|
|
52
|
+
/* 忽略 */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function defaultDataDir() {
|
|
57
|
+
const home = process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
58
|
+
return path.join(home, "storages", NS);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pendingFile() {
|
|
62
|
+
return path.join(defaultDataDir(), "pending.json");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readPending() {
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(pendingFile(), "utf8");
|
|
68
|
+
const obj = JSON.parse(raw);
|
|
69
|
+
if (obj && typeof obj.sessionId === "string") return obj;
|
|
70
|
+
return null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writePending(entry) {
|
|
77
|
+
try {
|
|
78
|
+
fs.mkdirSync(defaultDataDir(), { recursive: true });
|
|
79
|
+
fs.writeFileSync(pendingFile(), JSON.stringify(entry, null, 2), "utf8");
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clearPending() {
|
|
87
|
+
try {
|
|
88
|
+
fs.rmSync(pendingFile(), { force: true });
|
|
89
|
+
} catch {
|
|
90
|
+
/* 清理失败不阻塞 */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 用 Windows 计划任务拉起一个立即运行的重启任务(由 Task Scheduler 服务启动,
|
|
96
|
+
* 因而脱离 DSH 进程树)。随后 DSH 会随 taskkill /T 被强杀,脚本继续完成重启。
|
|
97
|
+
*/
|
|
98
|
+
function pluginRootDir() {
|
|
99
|
+
// 本文件位于 <root>/lib/index.js,插件根目录为其上一级
|
|
100
|
+
let dir = path.dirname(fileURLToPath(new URL(import.meta.url)));
|
|
101
|
+
if (path.basename(dir) === "lib") dir = path.dirname(dir);
|
|
102
|
+
return dir;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function scheduleRestart(mainPid, exePath) {
|
|
106
|
+
try {
|
|
107
|
+
const ps = path.join(pluginRootDir(), "restart", "restart.ps1");
|
|
108
|
+
const exe = exePath && exePath.length > 0 ? exePath : process.execPath; // DeepSeek Harness.exe
|
|
109
|
+
const mainId = typeof mainPid === "number" && mainPid > 0 ? mainPid : process.ppid;
|
|
110
|
+
const taskName = "DSHRestartPlugin_" + Date.now();
|
|
111
|
+
const action = `powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "${ps}" ${mainId} "${exe}"`;
|
|
112
|
+
diagWrite("scheduleRestart: ps=" + ps + " exe=" + exe + " mainId=" + mainId + " ppid=" + process.ppid);
|
|
113
|
+
diagWrite("action=" + action);
|
|
114
|
+
// /ST 00:00 仅作占位,/Run 会立即触发,无视时间。
|
|
115
|
+
execFile("schtasks", ["/Create", "/TN", taskName, "/TR", action, "/SC", "ONCE", "/ST", "00:00", "/F"], (err, stdout, stderr) => {
|
|
116
|
+
diagWrite("schtasks /Create result: err=" + (err ? (err.message || err.code) : "nil") + " out=" + String(stdout).trim() + " errOut=" + String(stderr).trim());
|
|
117
|
+
if (err) return;
|
|
118
|
+
execFile("schtasks", ["/Run", "/TN", taskName], (err2, out2, errOut2) => {
|
|
119
|
+
diagWrite("schtasks /Run result: err=" + (err2 ? (err2.message || err2.code) : "nil") + " out=" + String(out2).trim() + " errOut=" + String(errOut2).trim());
|
|
120
|
+
try {
|
|
121
|
+
execFile("schtasks", ["/Delete", "/TN", taskName, "/F"], (e3) => {
|
|
122
|
+
if (e3) diagWrite("schtasks /Delete err=" + (e3.message || e3.code));
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
/* 任务删除非关键 */
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
return true;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
diagWrite("scheduleRestart threw: " + (e && e.message ? e.message : String(e)));
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 判断是否存在正在运行的 Agent 会话(live agent > 0 即认为有活动会话)。 */
|
|
137
|
+
function hasActiveAgentSessions(ctx) {
|
|
138
|
+
try {
|
|
139
|
+
const agents = ctx && typeof ctx.get === "function" ? ctx.get("agents") : undefined;
|
|
140
|
+
if (agents && typeof agents.list === "function") {
|
|
141
|
+
const rows = agents.list();
|
|
142
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
/* 取不到则视为可重启(返回 false) */
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sendJson(res, status, payload) {
|
|
151
|
+
const body = JSON.stringify(payload, null, 2);
|
|
152
|
+
try {
|
|
153
|
+
res.writeHead(status, {
|
|
154
|
+
"content-type": "application/json; charset=utf-8",
|
|
155
|
+
"content-length": Buffer.byteLength(body),
|
|
156
|
+
"cache-control": "no-store",
|
|
157
|
+
});
|
|
158
|
+
res.end(body);
|
|
159
|
+
} catch {
|
|
160
|
+
/* 写响应失败忽略 */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readBody(req, limit = 256 * 1024) {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
let size = 0;
|
|
167
|
+
const chunks = [];
|
|
168
|
+
req.on("data", (c) => {
|
|
169
|
+
size += c.length;
|
|
170
|
+
if (size > limit) {
|
|
171
|
+
reject(new Error("request body too large"));
|
|
172
|
+
req.destroy();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
chunks.push(c);
|
|
176
|
+
});
|
|
177
|
+
req.on("end", () => {
|
|
178
|
+
try {
|
|
179
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"));
|
|
180
|
+
} catch (e) {
|
|
181
|
+
reject(e);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
req.on("error", (e) => reject(e));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function apply(ctx) {
|
|
189
|
+
bootMark("apply-called");
|
|
190
|
+
try {
|
|
191
|
+
const cm = typeof ctx.get === "function" ? ctx.get("clientModules") : undefined;
|
|
192
|
+
let pathRes = "(no clientModules)";
|
|
193
|
+
if (cm && typeof cm.clientPath === "function") {
|
|
194
|
+
pathRes = String(cm.clientPath(NS) ?? "(null)");
|
|
195
|
+
}
|
|
196
|
+
bootMark("clientPath=" + pathRes);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
bootMark("clientModules check fail: " + (e && e.message ? e.message : String(e)));
|
|
199
|
+
}
|
|
200
|
+
const api = {
|
|
201
|
+
readPending,
|
|
202
|
+
writePending,
|
|
203
|
+
clearPending,
|
|
204
|
+
scheduleRestart,
|
|
205
|
+
bootChannel: "apply",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
ctx.effect(() => {
|
|
209
|
+
if (!ctx.webServer || typeof ctx.webServer.register !== "function") return;
|
|
210
|
+
const route = (p, handler) =>
|
|
211
|
+
ctx.effect(
|
|
212
|
+
() => ctx.webServer.register({ kind: "exact", path: `${API_PREFIX}${p}`, handler }),
|
|
213
|
+
`${NS}: route ${p}`,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
route("/health", (req, res) => {
|
|
217
|
+
if (req.method !== "GET") return sendJson(res, 405, { ok: false, code: "METHOD_NOT_ALLOWED" });
|
|
218
|
+
return sendJson(res, 200, { ok: true, name: NS });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
route("/state", (req, res) => {
|
|
222
|
+
if (req.method !== "GET") return sendJson(res, 405, { ok: false, code: "METHOD_NOT_ALLOWED" });
|
|
223
|
+
return sendJson(res, 200, { ok: true, data: readPending() });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
route("/active", (req, res) => {
|
|
227
|
+
if (req.method !== "GET") return sendJson(res, 405, { ok: false, code: "METHOD_NOT_ALLOWED" });
|
|
228
|
+
return sendJson(res, 200, { ok: true, active: hasActiveAgentSessions(ctx) });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
route("/restart", async (req, res) => {
|
|
232
|
+
if (req.method !== "POST") return sendJson(res, 405, { ok: false, code: "METHOD_NOT_ALLOWED" });
|
|
233
|
+
let payload;
|
|
234
|
+
try {
|
|
235
|
+
payload = await readBody(req);
|
|
236
|
+
} catch {
|
|
237
|
+
return sendJson(res, 400, { ok: false, code: "BAD_BODY" });
|
|
238
|
+
}
|
|
239
|
+
const sessionId = typeof payload?.sessionId === "string" && payload.sessionId.length > 0 ? payload.sessionId : null;
|
|
240
|
+
const text = typeof payload?.text === "string" && payload.text.length > 0 ? payload.text : "重启完成";
|
|
241
|
+
writePending({ sessionId, text, at: new Date().toISOString() });
|
|
242
|
+
diagWrite("restart-request sessionId=" + sessionId);
|
|
243
|
+
const scheduled = scheduleRestart(process.ppid, process.execPath);
|
|
244
|
+
diagWrite("scheduleRestart returned " + scheduled);
|
|
245
|
+
return sendJson(res, 200, { ok: true, scheduled });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
route("/clear", async (req, res) => {
|
|
249
|
+
if (req.method !== "POST") return sendJson(res, 405, { ok: false, code: "METHOD_NOT_ALLOWED" });
|
|
250
|
+
try {
|
|
251
|
+
await readBody(req);
|
|
252
|
+
} catch {
|
|
253
|
+
/* 忽略请求体 */
|
|
254
|
+
}
|
|
255
|
+
clearPending();
|
|
256
|
+
return sendJson(res, 200, { ok: true });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
route("/diag", async (req, res) => {
|
|
260
|
+
if (req.method !== "POST") return sendJson(res, 405, { ok: false, code: "METHOD_NOT_ALLOWED" });
|
|
261
|
+
let payload;
|
|
262
|
+
try {
|
|
263
|
+
payload = await readBody(req);
|
|
264
|
+
} catch {
|
|
265
|
+
payload = {};
|
|
266
|
+
}
|
|
267
|
+
const m = typeof payload?.msg === "string" ? payload.msg : JSON.stringify(payload);
|
|
268
|
+
diagWrite("client: " + m);
|
|
269
|
+
return sendJson(res, 200, { ok: true });
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const __internals = { scheduleRestart, readPending, writePending, clearPending, hasActiveAgentSessions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-easy2restart",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "DSH plugin: double-click the sidebar restart button to fully relaunch DeepSeek Harness; it is greyed out while an agent session is running. (中文:DSH 重启插件:双击侧边栏重启按钮即可整体重启 DeepSeek Harness,会话运行时按钮置灰禁用。)",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"dsh",
|
|
9
|
+
"dsh-plugin",
|
|
10
|
+
"restart",
|
|
11
|
+
"relaunch"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"icon": "./icon.svg",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/SMPcosy/dsh-plugin-easy2restart.git"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./lib/index.js",
|
|
22
|
+
"./client": "./lib/client.js",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"cordis.patch.yml",
|
|
28
|
+
"restart",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"icon.svg"
|
|
32
|
+
],
|
|
33
|
+
"dsh": {
|
|
34
|
+
"bundle": {
|
|
35
|
+
"patch": "./cordis.patch.yml"
|
|
36
|
+
},
|
|
37
|
+
"client": {
|
|
38
|
+
"inject": [
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
40
|
+
"@deepseek-ai/dsh-client-locale"
|
|
41
|
+
],
|
|
42
|
+
"platform": "web"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=22"
|
|
47
|
+
}
|
|
6
48
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)][int]$MainPid,
|
|
3
|
+
[Parameter(Mandatory = $true)][string]$ExePath
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
# dsh-plugin-easy2restart 重启脚本
|
|
7
|
+
# 由 Windows 计划任务 (schtasks) 拉起,因此脱离 DSH 进程树,
|
|
8
|
+
# 即使 DSH 被 /T 强杀,本脚本仍能存活并完成重启。
|
|
9
|
+
|
|
10
|
+
# 1) 结束 DSH 主进程及其整个进程树
|
|
11
|
+
& taskkill /PID $MainPid /T /F 2>$null | Out-Null
|
|
12
|
+
|
|
13
|
+
# 2) 等多个含 DSH 名的进程都退出(最多约 20 秒)
|
|
14
|
+
$deadline = (Get-Date).AddSeconds(20)
|
|
15
|
+
do {
|
|
16
|
+
Start-Sleep -Milliseconds 400
|
|
17
|
+
$still = Get-Process -Name "DeepSeek Harness" -ErrorAction SilentlyContinue
|
|
18
|
+
} while ($still -and (Get-Date) -lt $deadline)
|
|
19
|
+
|
|
20
|
+
Start-Sleep -Milliseconds 800
|
|
21
|
+
|
|
22
|
+
# 3) 重新启动桌面应用(不等待它退出)
|
|
23
|
+
if (Test-Path -LiteralPath $ExePath) {
|
|
24
|
+
Start-Process -FilePath $ExePath -WorkingDirectory (Split-Path -Parent $ExePath)
|
|
25
|
+
}
|