dsh-plugin-easy2restart 0.1.0 → 0.1.2
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 +20 -20
- package/README.md +3 -3
- package/cordis.patch.yml +10 -10
- package/icon.png +0 -0
- package/lib/client.js +4 -1
- package/lib/index.js +273 -273
- package/package.json +4 -2
- package/restart/restart.ps1 +24 -24
package/LICENSE
CHANGED
|
@@ -1,21 +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
|
|
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
21
|
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **DSH 重启插件:双击侧边栏重启按钮即可整体重启 DeepSeek Harness,会话运行时按钮置灰禁用。**
|
|
4
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
5
|
|
|
6
|
-
DeepSeek Harness
|
|
6
|
+
DeepSeek Harness 重启插件:在左侧边栏**账号头像/设置上方**放一个「🔄 重启 DSH」按钮,**双击**即可整进程重启 DeepSeek Harness;当有 Agent 会话正在运行时,按钮自动置灰禁用,避免打断进行中的工作。
|
|
7
7
|
|
|
8
8
|
## 功能
|
|
9
9
|
|
|
@@ -37,11 +37,11 @@ dsh plugin --profile desktop add link:<克隆或本目录的绝对路径>
|
|
|
37
37
|
dsh plugin --profile desktop add dsh-plugin-easy2restart
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
装完**重启 DeepSeek Harness
|
|
40
|
+
装完**重启 DeepSeek Harness**,侧边栏底部账号头像上方即出现「🔄」按钮。
|
|
41
41
|
|
|
42
42
|
## 使用
|
|
43
43
|
|
|
44
|
-
-
|
|
44
|
+
- **双击**「🔄」按钮即可重启 DSH。
|
|
45
45
|
- 当有会话正在运行时按钮为灰色(不可点),会话空闲时可正常重启。
|
|
46
46
|
- 重启由计划任务拉起,与 DSH 自身退出无关,即使 DSH 被强杀也能自动重新启动。
|
|
47
47
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,11 +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
|
|
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
11
|
name: dsh-plugin-easy2restart
|
package/icon.png
ADDED
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -27,6 +27,9 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
const API = "/api/dsh-plugin-easy2restart";
|
|
28
28
|
const POLL_MS = 1200;
|
|
29
29
|
|
|
30
|
+
// 按钮图标:与插件列表共用同一张 ⟳ 图标(内联 data URI,随位置缩放)
|
|
31
|
+
const ICON = "data:image/png;base64,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";
|
|
32
|
+
|
|
30
33
|
const zh = {
|
|
31
34
|
tooltip: "重启 DSH",
|
|
32
35
|
tooltipDisabled: "有会话运行中,无法重启",
|
|
@@ -150,7 +153,7 @@ window.__ModuleLoader__.load({
|
|
|
150
153
|
disabled,
|
|
151
154
|
onDoubleClick,
|
|
152
155
|
},
|
|
153
|
-
wide ? [h("
|
|
156
|
+
wide ? [h("img", { key: "ic", src: ICON, alt: "", "aria-hidden": "true", style: { width: 14, height: 14, display: "block", objectFit: "contain" } }), h("span", { key: "tx", style: { fontSize: 12 } }, "重启 DSH")] : h("img", { src: ICON, alt: "", "aria-hidden": "true", style: { width: 16, height: 16, display: "block", objectFit: "contain" } }),
|
|
154
157
|
);
|
|
155
158
|
}
|
|
156
159
|
|
package/lib/index.js
CHANGED
|
@@ -1,274 +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
|
-
|
|
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
274
|
export const __internals = { scheduleRestart, readPending, writePending, clearPending, hasActiveAgentSessions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-easy2restart",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"keywords": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"relaunch"
|
|
12
12
|
],
|
|
13
13
|
"license": "MIT",
|
|
14
|
+
"icon": "./icon.png",
|
|
14
15
|
"main": "lib/index.js",
|
|
15
16
|
"repository": {
|
|
16
17
|
"type": "git",
|
|
@@ -26,7 +27,8 @@
|
|
|
26
27
|
"cordis.patch.yml",
|
|
27
28
|
"restart",
|
|
28
29
|
"README.md",
|
|
29
|
-
"LICENSE"
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"icon.png"
|
|
30
32
|
],
|
|
31
33
|
"dsh": {
|
|
32
34
|
"bundle": {
|
package/restart/restart.ps1
CHANGED
|
@@ -1,25 +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)
|
|
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
25
|
}
|