@mrrisega/dsh-remote 0.6.13 → 0.6.15
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 +124 -14
- package/clients/dsh-remote/dsh-events.mjs +22 -2
- package/clients/dsh-remote/e2ee-client.mjs +97 -13
- package/clients/dsh-remote/e2ee-shim-script.js +31 -3
- package/clients/dsh-remote/mobile-adapter.mjs +283 -1
- package/clients/dsh-remote/test/boot-tip.test.mjs +158 -0
- package/clients/dsh-remote/test/dsh-events.test.mjs +89 -0
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +73 -3
- package/clients/dsh-remote/test/e2ee-client.test.mjs +42 -1
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +10 -1
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +18 -1
- package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +325 -0
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +3 -1
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +196 -0
- package/clients/dsh-remote/test/wechat-runtime.test.mjs +268 -1
- package/clients/dsh-remote/upstream-discovery.mjs +448 -0
- package/clients/dsh-remote/wechat-channel.mjs +86 -3
- package/clients/dsh-remote/wechat-runtime.mjs +253 -10
- package/dsh-setup.mjs +225 -23
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +31 -1
- package/packages/dsh-remote-web/lib/index.js +464 -15
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +124 -14
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +22 -2
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +97 -13
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +31 -3
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +283 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/upstream-discovery.mjs +448 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +86 -3
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +253 -10
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +225 -23
- package/packages/dsh-remote-web/test/connect-stuck-visibility.test.mjs +16 -1
- package/packages/dsh-remote-web/test/doctor-cli.test.mjs +117 -0
- package/packages/dsh-remote-web/test/linux-bridge.test.mjs +346 -0
- package/packages/dsh-remote-web/test/picker-pin.test.mjs +201 -0
- package/packages/dsh-remote-web/test/self-manage.test.mjs +12 -0
- package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +170 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* watcher 的上游端口发现回归 —— 复现 2026-09-23 两起真实事故的**用户可见形态**。
|
|
4
|
+
*
|
|
5
|
+
* 现场(用户报告,DSH Desktop 2.0.4 + 插件 0.6.10):
|
|
6
|
+
* ```
|
|
7
|
+
* 连接阶段: starting(正在启动 Bridge…)
|
|
8
|
+
* bridge 进程: 未运行 中继注册: 未注册 自动重试: 已尝试 6 次
|
|
9
|
+
* ```
|
|
10
|
+
* 面板永久停在 starting,设备从未登记。根因:**watcher 把上游写死 3080**,
|
|
11
|
+
* 而 DSH Desktop(Electron 壳 `dsh-plugin-desktop`)把 Web 服务放在 **43120**(占用则 +1)、
|
|
12
|
+
* 独立用户也可能 `dsh web --port 8090` / `--port 0`。探不到 → `checkUpstream()` 恒 false
|
|
13
|
+
* → **bridge 永远不被派生**,而面板只会说「正在启动 Bridge…」。
|
|
14
|
+
*
|
|
15
|
+
* 本用例锁的是**决策点**(不是实现细节):只要上游在**非默认端口**且应答像 dsh web,
|
|
16
|
+
* watcher 就必须走进「dsh web 在线,启动 bridge...」并带着**正确的**
|
|
17
|
+
* `DSH_BRIDGE_UPSTREAM` 去派生 bridge。
|
|
18
|
+
*
|
|
19
|
+
* 隔离手法与 runtime-provision / connect-loop 一致:临时 HOME + 假 launchctl/pgrep/ps/npx,
|
|
20
|
+
* 绝不用到本机真实服务;上游是**测试进程内**起的假 dsh web(真 dsh web 只在 3080,
|
|
21
|
+
* 那正是我们要避开的默认值)。
|
|
22
|
+
*/
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
26
|
+
import http from "node:http";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import test from "node:test";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const SETUP = path.join(HERE, "..", "..", "..", "dsh-setup.mjs");
|
|
34
|
+
|
|
35
|
+
/** 假 dsh web 的应答:带官方特征(身份校验靠它),并记录被访问过。 */
|
|
36
|
+
const DSH_HTML = "<!doctype html><html><head><title>DeepSeek Harness</title></head>"
|
|
37
|
+
+ "<body><script>__ModuleLoader__</script></body></html>";
|
|
38
|
+
|
|
39
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
40
|
+
|
|
41
|
+
/** 起一个假的上游(= 非默认端口上的 dsh web)。 */
|
|
42
|
+
async function startFakeDshWeb() {
|
|
43
|
+
const hits = [];
|
|
44
|
+
const srv = http.createServer((req, res) => {
|
|
45
|
+
hits.push(req.url);
|
|
46
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
47
|
+
res.end(DSH_HTML);
|
|
48
|
+
});
|
|
49
|
+
await new Promise((r) => srv.listen(0, "127.0.0.1", r));
|
|
50
|
+
return { srv, port: srv.address().port, hits, url: `http://127.0.0.1:${srv.address().port}` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 临时 relayDir + 一份"已登录"的配置(指向本地死端口,绝不外联)。 */
|
|
54
|
+
function makeEnv() {
|
|
55
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "dsh-upstream-port-"));
|
|
56
|
+
const home = path.join(root, "home");
|
|
57
|
+
const relayDir = path.join(root, "relay");
|
|
58
|
+
const bin = path.join(root, "bin");
|
|
59
|
+
for (const d of [home, relayDir, bin]) mkdirSync(d, { recursive: true });
|
|
60
|
+
|
|
61
|
+
// 假命令:让 watcher/插件的系统操作全部落空,绝不碰本机真实服务
|
|
62
|
+
const stub = (name, body) => {
|
|
63
|
+
const p = path.join(bin, name);
|
|
64
|
+
writeFileSync(p, `#!/bin/sh\n${body}\n`);
|
|
65
|
+
chmodSync(p, 0o755);
|
|
66
|
+
};
|
|
67
|
+
stub("launchctl", 'case "$1" in list) exit 0 ;; print) echo "could not find service" >&2; exit 113 ;; esac\nexit 0');
|
|
68
|
+
stub("pgrep", "exit 1");
|
|
69
|
+
stub("ps", 'case "$*" in *ppid*) echo 1 ;; *lstart*) echo "Mon Jan 1 00:00:00 2026" ;; esac\nexit 0');
|
|
70
|
+
stub("npx", "exit 127");
|
|
71
|
+
|
|
72
|
+
writeFileSync(path.join(relayDir, ".dsh-config.json"), JSON.stringify({
|
|
73
|
+
phone: "13800000000",
|
|
74
|
+
password: "pw",
|
|
75
|
+
device_id: "dev-aaaaaaaaaaaa",
|
|
76
|
+
tunnel_url: "ws://127.0.0.1:1",
|
|
77
|
+
api_url: "http://127.0.0.1:1"
|
|
78
|
+
}, null, 2), { mode: 0o600 });
|
|
79
|
+
|
|
80
|
+
return { root, home, relayDir, bin };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 起 watcher,返回 {proc, out(), stop()}。 */
|
|
84
|
+
function startWatcher(env, extraEnv = {}) {
|
|
85
|
+
let out = "";
|
|
86
|
+
const proc = spawn(process.execPath, [SETUP, "run"], {
|
|
87
|
+
cwd: env.root,
|
|
88
|
+
env: {
|
|
89
|
+
...process.env,
|
|
90
|
+
HOME: env.home,
|
|
91
|
+
DSH_RELAY_DIR: env.relayDir,
|
|
92
|
+
DSH_BRIDGE_CONFIG: path.join(env.relayDir, ".dsh-config.json"),
|
|
93
|
+
PATH: `${env.bin}:${process.env.PATH}`,
|
|
94
|
+
DSH_REMOTE_TELEMETRY: "0",
|
|
95
|
+
...extraEnv
|
|
96
|
+
},
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
98
|
+
});
|
|
99
|
+
proc.stdout.on("data", (d) => { out += String(d); });
|
|
100
|
+
proc.stderr.on("data", (d) => { out += String(d); });
|
|
101
|
+
return {
|
|
102
|
+
proc,
|
|
103
|
+
out: () => out,
|
|
104
|
+
async stop() {
|
|
105
|
+
try { proc.kill("SIGKILL"); } catch { /* ignore */ }
|
|
106
|
+
await sleep(120);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const waitFor = async (fn, timeoutMs = 12_000) => {
|
|
112
|
+
const t0 = Date.now();
|
|
113
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
114
|
+
if (fn()) return true;
|
|
115
|
+
await sleep(150);
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
test("★ 上游在非默认端口(DSH Desktop 场景):watcher 必须发现它并派生 bridge", async (t) => {
|
|
121
|
+
const env = makeEnv();
|
|
122
|
+
const upstream = await startFakeDshWeb();
|
|
123
|
+
let watcher = null;
|
|
124
|
+
try {
|
|
125
|
+
// 真实的 Desktop 情形:端口文件要么还没写、要么写着上一轮的旧端口
|
|
126
|
+
writeFileSync(path.join(env.relayDir, ".dsh-upstream"), "http://127.0.0.1:1\n", { mode: 0o600 });
|
|
127
|
+
// DSH_WEB_URL 是宿主真实端口的合法来源之一(Desktop 包就是用它把端口暴露给子进程的)
|
|
128
|
+
watcher = startWatcher(env, { DSH_WEB_URL: upstream.url });
|
|
129
|
+
|
|
130
|
+
const started = await waitFor(() => /dsh web 在线,启动 bridge/.test(watcher.out()));
|
|
131
|
+
assert.ok(
|
|
132
|
+
started,
|
|
133
|
+
`上游在 ${upstream.port}(非 3080)时 watcher 依然要启动 bridge。实际输出:\n${watcher.out().slice(0, 1200)}`
|
|
134
|
+
);
|
|
135
|
+
assert.ok(upstream.hits.length > 0, "watcher 必须真的探测了那个非默认端口");
|
|
136
|
+
assert.ok(
|
|
137
|
+
!/等待 dsh web(http:\/\/127\.0\.0\.1:3080)/.test(watcher.out()),
|
|
138
|
+
"日志不得再宣称自己在等 3080(用户实测就是被这句误导)"
|
|
139
|
+
);
|
|
140
|
+
// ★ 关键:派生出去的 bridge 必须拿到**被发现的那个端口**(否则它会自己去探 3080 并永久失败)
|
|
141
|
+
const gotBridgeUpstream = await waitFor(() => new RegExp(`上游地址已确定: http://127\\.0\\.0\\.1:${upstream.port}`).test(watcher.out()), 6000);
|
|
142
|
+
assert.ok(
|
|
143
|
+
gotBridgeUpstream,
|
|
144
|
+
`bridge 应被注入发现到的上游 ${upstream.url}。实际输出:\n${watcher.out().slice(0, 1200)}`
|
|
145
|
+
);
|
|
146
|
+
} finally {
|
|
147
|
+
if (watcher) await watcher.stop();
|
|
148
|
+
await new Promise((r) => upstream.srv.close(r));
|
|
149
|
+
rmSync(env.root, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("上游确实不在时:watcher 保持等待(不谎报在线,也不派生 bridge)", async (t) => {
|
|
154
|
+
const env = makeEnv();
|
|
155
|
+
// ⚠️ 必须用**显式** DSH_BRIDGE_UPSTREAM 钉死死端口:不加它时动态发现会去探 3080,
|
|
156
|
+
// 而开发机/CI 上真的可能有一个 dsh web 在 3080 —— 那样这条断言就变成看环境了。
|
|
157
|
+
// 显式值优先且不做探测(既有语义),于是"上游不可达"这件事在测试里是确定的。
|
|
158
|
+
let watcher = null;
|
|
159
|
+
try {
|
|
160
|
+
writeFileSync(path.join(env.relayDir, ".dsh-upstream"), "http://127.0.0.1:1\n", { mode: 0o600 });
|
|
161
|
+
watcher = startWatcher(env, { DSH_BRIDGE_UPSTREAM: "http://127.0.0.1:1" });
|
|
162
|
+
await sleep(3500);
|
|
163
|
+
const out = watcher.out();
|
|
164
|
+
assert.ok(!/dsh web 在线,启动 bridge/.test(out), `不得谎报在线:\n${out.slice(0, 600)}`);
|
|
165
|
+
assert.match(out, /等待 dsh web/, "应如实说明在等 dsh web");
|
|
166
|
+
} finally {
|
|
167
|
+
if (watcher) await watcher.stop();
|
|
168
|
+
rmSync(env.root, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|