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 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 重启插件:在左侧边栏**账号头像/设置上方**放一个「⟳ 重启 DSH」按钮,**双击**即可整进程重启 DeepSeek Harness;当有 Agent 会话正在运行时,按钮自动置灰禁用,避免打断进行中的工作。
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
- - **双击**「⟳」按钮即可重启 DSH。
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("span", { key: "ic", style: { fontSize: 14 } }, "⟳"), h("span", { key: "tx", style: { fontSize: 12 } }, "重启 DSH")] : h("span", { style: { fontSize: 15 } }, "⟳"),
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.0",
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": {
@@ -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
  }