@mrrisega/dsh-remote 0.6.18 → 0.7.0-beta.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/clients/dsh-remote/dsh-bridge.mjs +108 -12
- package/clients/dsh-remote/dsh-events.mjs +66 -8
- package/clients/dsh-remote/test/dsh-events.test.mjs +88 -0
- package/clients/dsh-remote/test/harness-cookie-authority.test.mjs +121 -0
- package/clients/dsh-remote/test/harness-cookie-selfheal.test.mjs +15 -7
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +17 -4
- package/clients/dsh-remote/wechat-runtime.mjs +13 -2
- package/dsh-setup.mjs +190 -18
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +62 -18
- package/packages/dsh-remote-web/lib/index.js +213 -19
- package/packages/dsh-remote-web/package.json +5 -2
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +108 -12
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +66 -8
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +13 -2
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +190 -18
- package/packages/dsh-remote-web/test/desktop-compat.test.mjs +269 -0
- package/packages/dsh-remote-web/test/diag-report.test.mjs +7 -1
- package/packages/dsh-remote-web/test/patch-activation.test.mjs +6 -1
- package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +13 -1
- package/packages/dsh-remote-web/test/windows-compat.test.mjs +33 -2
|
@@ -20,6 +20,12 @@ import { apply } from "../lib/index.js";
|
|
|
20
20
|
// ⚠️ 必须用 fileURLToPath:Windows 上 `new URL(...).pathname` 得到的是 "/D:/a/..."(带前导斜杠),
|
|
21
21
|
// path.join 之后会拼成 "D:\\D:\\a\\..." → ENOENT。CI 的 Windows 专项就死在这条上。
|
|
22
22
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
/**
|
|
24
|
+
* 版本号**从真实 manifest 派生**,不要写死。
|
|
25
|
+
* 写死会在每次发版时把这条无关用例弄红(2026-10-07 发 0.7.0 时实测踩到:断言里钉着 `/0\.6\./`),
|
|
26
|
+
* 而"按版本分桶"要的恰恰是**跟着真实版本走**。
|
|
27
|
+
*/
|
|
28
|
+
const PLUGIN_VERSION = JSON.parse(readFileSync(path.join(HERE, "..", "package.json"), "utf8")).version;
|
|
23
29
|
const readOrEmpty = async (p) => { try { return await readFile(p, "utf8"); } catch { return ""; } };
|
|
24
30
|
|
|
25
31
|
/** 假反馈服务:只收 /api/feedback 与 /api/feedback/:id/replies(与企业端契约同构)。 */
|
|
@@ -124,7 +130,7 @@ test("预览:把「将要发送的每一个字」摊开,且不含手机号
|
|
|
124
130
|
const b = await getJson(`${base}/dsh-remote/diag/bundle`);
|
|
125
131
|
assert.equal(b.ok, true);
|
|
126
132
|
assert.match(b.title, /\[诊断\]/);
|
|
127
|
-
assert.
|
|
133
|
+
assert.ok(b.title.includes(PLUGIN_VERSION), `标题要带插件版本(${PLUGIN_VERSION}),便于后台按版本分桶`);
|
|
128
134
|
const all = b.summary + "\n" + b.parts.join("\n");
|
|
129
135
|
for (const need of ["【阶段】", "【bridge】", "【上游】", "【配置目录】", env.relayDir]) {
|
|
130
136
|
assert.ok(all.includes(need), `预览必须包含 ${need}(这些正是 2026-09-23 那次反馈里缺失的证据)`);
|
|
@@ -178,7 +178,12 @@ test("回归护栏:plugin 子命令必须把激活结果 return 给 setup(
|
|
|
178
178
|
const call = src.match(/return convergePluginActivation\(profileDir[^\n]*/);
|
|
179
179
|
assert.ok(call, "pluginCmd 必须 return convergePluginActivation(...) 的结果");
|
|
180
180
|
// setup() 必须真的消费这个结果
|
|
181
|
-
|
|
181
|
+
// ⚠️ 2026-10-07 起参数从 `[]` 改为 `profileArgvOf(argv)`:桌面端要支持
|
|
182
|
+
// `--profile desktop`,而 setup() 原本**不把 --profile 透传给 pluginCmd**,
|
|
183
|
+
// 导致"指定 desktop profile"被静默忽略、插件装进 web profile,
|
|
184
|
+
// 桌面端用户装完看不到面板且极难自查。这里是那次修复的护栏。
|
|
185
|
+
assert.match(src, /pluginResult = await pluginCmd\(profileArgvOf\(argv\)\)/, "setup 应保存 pluginCmd 的返回值");
|
|
186
|
+
assert.match(src, /function profileArgvOf\(/, "必须有 profileArgvOf 把 --profile 挑出来");
|
|
182
187
|
assert.match(src, /pluginResult\.hotPatch/, "应据 hotPatch 判断是否走热挂载形态");
|
|
183
188
|
});
|
|
184
189
|
|
|
@@ -407,7 +407,7 @@ test("清理助手:护栏命中时绝不删 profile(relayDir 误配成 profi
|
|
|
407
407
|
test("安全:三处机密写入都要显式收紧 ACL(Windows 上 mode 0o600 无效)", () => {
|
|
408
408
|
for (const [label, src, callSites] of [
|
|
409
409
|
["安装器", SETUP_SRC, [/hardenFile\(CONFIG_PATH\)/]],
|
|
410
|
-
["插件半", INDEX_SRC, [/hardenFile\(configPathOf\(relayDir\)\)
|
|
410
|
+
["插件半", INDEX_SRC, [/hardenFile\(configPathOf\(relayDir\)\)/]],
|
|
411
411
|
["bridge", fs.readFileSync(path.join(HERE, "..", "..", "..", "clients", "dsh-remote", "dsh-bridge.mjs"), "utf8"), [/hardenFile\(CONFIG_PATH\)/]],
|
|
412
412
|
]) {
|
|
413
413
|
assert.ok(/function hardenFile\(file\)/.test(src), `${label} 应有 hardenFile`);
|
|
@@ -417,6 +417,18 @@ test("安全:三处机密写入都要显式收紧 ACL(Windows 上 mode 0o600
|
|
|
417
417
|
}
|
|
418
418
|
});
|
|
419
419
|
|
|
420
|
+
test("安全:Cookie 的每个写入点都必须紧跟着 hardenFile(按 authority 分文件后不得漏收)", () => {
|
|
421
|
+
// 0.7.0-beta.2 把 Cookie 从「一份共享文件」改成「本宿主专属 + 旧版镜像」两份(见 P0-2 事故根治)。
|
|
422
|
+
// 写点从 1 个变 2 个 —— 逐个按变量名断言,漏收的那一份在 Windows 上就是 ACL 敞开状态。
|
|
423
|
+
const mint = INDEX_SRC.slice(INDEX_SRC.indexOf("async function mintHarnessCookie"), INDEX_SRC.indexOf("function readHarnessCookie"));
|
|
424
|
+
assert.ok(mint.length > 0, "应能定位到 mintHarnessCookie(找不到说明函数被改名,请同步本用例)");
|
|
425
|
+
const written = [...mint.matchAll(/writeFileSync\((\w+),/g)].map((m) => m[1]);
|
|
426
|
+
assert.ok(written.length >= 2, `mintHarnessCookie 应写两份 Cookie(本宿主专属 + 旧版镜像),实际 ${written.length}`);
|
|
427
|
+
for (const v of written) {
|
|
428
|
+
assert.ok(new RegExp(`hardenFile\\(${v}\\)`).test(mint), `Cookie 写入点 ${v} 之后必须调用 hardenFile 收紧 ACL`);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
420
432
|
test("安全:hardenFile 在非 Windows 上只 chmod(不引入任何子进程,零副作用)", () => {
|
|
421
433
|
const src = SETUP_SRC.slice(SETUP_SRC.indexOf("function hardenFile(file)"), SETUP_SRC.indexOf("// ---------- 配置读写 ----------"));
|
|
422
434
|
const calls = [];
|
|
@@ -529,9 +529,10 @@ test("win32:登录任务经 wscript+VBS 隐藏启动(旧版会弹黑色空
|
|
|
529
529
|
SETUP_SRC.indexOf("function winHiddenLauncherPath()"),
|
|
530
530
|
SETUP_SRC.indexOf("function writeWindowsTask()"),
|
|
531
531
|
);
|
|
532
|
-
|
|
532
|
+
// SERVICE_NODE_ENV 是模块级常量:切片只含函数体,必须显式注入(真实 node 时为空对象)。
|
|
533
|
+
const make = new Function("CONFIG_DIR", "fs", "path", "SERVICE_NODE_ENV",
|
|
533
534
|
`${src}\nreturn { winHiddenLauncherPath, writeWinHiddenLauncher };`);
|
|
534
|
-
const { winHiddenLauncherPath, writeWinHiddenLauncher } = make(dir, nodeFs, path);
|
|
535
|
+
const { winHiddenLauncherPath, writeWinHiddenLauncher } = make(dir, nodeFs, path, {});
|
|
535
536
|
|
|
536
537
|
// 用「空格 + 中文 + 括号」的路径:Windows 上最常见的翻车形状
|
|
537
538
|
const node = "C:\\Program Files\\nodejs\\node.exe";
|
|
@@ -554,6 +555,36 @@ test("win32:登录任务经 wscript+VBS 隐藏启动(旧版会弹黑色空
|
|
|
554
555
|
assert.equal(value, `"${node}" "${setup}" run`, "VBS 字面量转义必须可往返(内嵌引号写成两个)");
|
|
555
556
|
assert.ok(value.includes("My App (x64)"), "含空格/括号的路径必须完整保留");
|
|
556
557
|
assert.ok(value.includes("张三"), "中文路径必须完整保留");
|
|
558
|
+
assert.ok(!/ELECTRON_RUN_AS_NODE/.test(vbs), "真实 node 时不得注入 Electron 变量(避免污染环境)");
|
|
559
|
+
} finally {
|
|
560
|
+
await rm(dir, { recursive: true, force: true });
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("win32:桌面端(Electron 解释器)自启动必须先设 ELECTRON_RUN_AS_NODE=1", async () => {
|
|
565
|
+
// 现场价值:DeepSeek Harness 桌面端宿主里 process.execPath 是 Electron 应用二进制。
|
|
566
|
+
// 不设这个变量,自启动拉起的不是 bridge 守护,而是**又开一个桌面端应用** ——
|
|
567
|
+
// 表现为「服务登记成功、进程秒退、bridge 永远起不来」,与 0.6.15 端口事故一样难排查。
|
|
568
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "dsh-win-electron-"));
|
|
569
|
+
try {
|
|
570
|
+
const src = SETUP_SRC.slice(
|
|
571
|
+
SETUP_SRC.indexOf("function winHiddenLauncherPath()"),
|
|
572
|
+
SETUP_SRC.indexOf("function writeWindowsTask()"),
|
|
573
|
+
);
|
|
574
|
+
const make = new Function("CONFIG_DIR", "fs", "path", "SERVICE_NODE_ENV",
|
|
575
|
+
`${src}\nreturn { writeWinHiddenLauncher };`);
|
|
576
|
+
const { writeWinHiddenLauncher } = make(dir, nodeFs, path, { ELECTRON_RUN_AS_NODE: "1" });
|
|
577
|
+
|
|
578
|
+
const node = "C:\\Program Files\\DeepSeek Harness\\DeepSeek Harness.exe";
|
|
579
|
+
const setup = path.join(dir, "dsh-setup.mjs");
|
|
580
|
+
const vbs = readFileSync(writeWinHiddenLauncher(node, setup), "utf8");
|
|
581
|
+
assert.match(vbs, /Environment\("PROCESS"\)\("ELECTRON_RUN_AS_NODE"\)\s*=\s*"1"/,
|
|
582
|
+
"必须先往进程环境写入 ELECTRON_RUN_AS_NODE=1,再 Run");
|
|
583
|
+
assert.ok(vbs.indexOf("ELECTRON_RUN_AS_NODE") < vbs.indexOf(".Run "),
|
|
584
|
+
"设置环境变量必须发生在 Run **之前**(否则白设)");
|
|
585
|
+
// 注入变量不得破坏原有的隐藏启动语义
|
|
586
|
+
const line = vbs.replace(/\r\n/g, "\n").split("\n").find((l) => l.includes(".Run "));
|
|
587
|
+
assert.ok(/,\s*0,\s*False\s*$/.test(line), `Run 仍必须是 (cmd, 0, False),实际:${line}`);
|
|
557
588
|
} finally {
|
|
558
589
|
await rm(dir, { recursive: true, force: true });
|
|
559
590
|
}
|