@mrrisega/dsh-remote 0.6.7-beta.3 → 0.6.7
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 +16 -0
- package/clients/dsh-remote/e2ee-shim-script.js +18 -1
- package/clients/dsh-remote/mobile-adapter.mjs +487 -170
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +91 -34
- package/dsh-setup.mjs +59 -6
- package/package.json +6 -5
- package/packages/dsh-remote-web/lib/client.js +22 -4
- package/packages/dsh-remote-web/lib/index.js +161 -40
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/test/account-switch-device.test.mjs +307 -0
- package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +68 -1
- package/packages/dsh-remote-web/test/windows-compat.test.mjs +18 -0
- package/scripts/test-net-guard.cjs +49 -0
|
@@ -91,8 +91,16 @@ class El {
|
|
|
91
91
|
get isConnected() { return true; }
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
94
|
+
/**
|
|
95
|
+
* 搭一个最小 dsh web 环境:frame(带官方 data 属性)内含 sidebar/center 两列 + 可选官方 toggle。
|
|
96
|
+
*
|
|
97
|
+
* `variant: "rc2"` 复刻**官方 0.1.5-rc.2 起**的 DOM:第三列由 `details` 改名 `rightbar`
|
|
98
|
+
* (`data-details-collapsed` → `data-rightbar-collapsed`、`pI_x6G_detailsCol` → `pI_x6G_rightbarCol`),
|
|
99
|
+
* 且 `overlayLayer`(官方 shell.overlay 宿主,`inset:0` 全屏、`data-shell-overlay`)留在 frame 末尾、
|
|
100
|
+
* 折叠时**没有拖拽把手**——这正是 2026-09-19 线上"整屏遮罩"的现场结构。
|
|
101
|
+
*/
|
|
102
|
+
function harness({ collapsedAttr = undefined, detailsAttr = undefined, sidebarRect = null, width = 390, withToggle = true,
|
|
103
|
+
variant = "rc1", rightbarAttr = undefined, overlayRect = null, rightbarRect = null } = {}) {
|
|
96
104
|
const html = new El("html");
|
|
97
105
|
// HOSTISH 门禁会读 documentElement.outerHTML 找官方特征(__ModuleLoader__ / DeepSeek Harness)
|
|
98
106
|
html.outerHTML = '<html><head><script>window.__ModuleLoader__ = { mode: "queue" }</script>'
|
|
@@ -107,6 +115,17 @@ function harness({ collapsedAttr = undefined, detailsAttr = undefined, sidebarRe
|
|
|
107
115
|
if (sidebarRect) sidebar._rect = sidebarRect;
|
|
108
116
|
frame.appendChild(sidebar);
|
|
109
117
|
frame.appendChild(center);
|
|
118
|
+
let rightbar = null;
|
|
119
|
+
let overlay = null;
|
|
120
|
+
if (variant === "rc2") {
|
|
121
|
+
if (rightbarAttr !== undefined) frame.setAttribute("data-rightbar-collapsed", rightbarAttr);
|
|
122
|
+
rightbar = new El("div", { class: "pI_x6G_rightbarCol" });
|
|
123
|
+
if (rightbarRect) rightbar._rect = rightbarRect;
|
|
124
|
+
frame.appendChild(rightbar);
|
|
125
|
+
overlay = new El("div", { class: "pI_x6G_overlayLayer", "data-shell-overlay": "" });
|
|
126
|
+
if (overlayRect) overlay._rect = overlayRect;
|
|
127
|
+
frame.appendChild(overlay);
|
|
128
|
+
}
|
|
110
129
|
const toggle = new El("button");
|
|
111
130
|
toggle.setAttribute("aria-label", "打开侧边栏");
|
|
112
131
|
|
|
@@ -127,7 +146,7 @@ function harness({ collapsedAttr = undefined, detailsAttr = undefined, sidebarRe
|
|
|
127
146
|
addEventListener() {},
|
|
128
147
|
querySelector(sel) {
|
|
129
148
|
const s2 = String(sel);
|
|
130
|
-
if (s2.includes(".pI_x6G_frame") ||
|
|
149
|
+
if (s2.includes(".pI_x6G_frame") || /\[data-(sidebar|details|rightbar)-collapsed\]/.test(s2) || s2.includes("[data-shell-overlay]")) return frame;
|
|
131
150
|
if (s2.includes("aria-label=")) return withToggle ? toggle : null;
|
|
132
151
|
if (s2.includes(".hHd-Xa_toggle")) return withToggle ? toggle : null;
|
|
133
152
|
if (s2.includes("hHd-Xa")) return sidebar; // 子列识别用(真实页面里这是侧栏)
|
|
@@ -154,12 +173,15 @@ function harness({ collapsedAttr = undefined, detailsAttr = undefined, sidebarRe
|
|
|
154
173
|
MutationObserver: class { observe() {} disconnect() {} },
|
|
155
174
|
ResizeObserver: class { observe() {} disconnect() {} },
|
|
156
175
|
};
|
|
176
|
+
// 真实浏览器里 window.location 必然存在(注入层用 location.assign/href 跳转),假 DOM 也要有
|
|
177
|
+
const loc = { href: "https://n.risegao.cn:13443/remote/dev-x/", host: "n.risegao.cn", pathname: "/remote/dev-x/", origin: "https://n.risegao.cn:13443" };
|
|
178
|
+
win.location = loc;
|
|
157
179
|
const ctx = vm.createContext({
|
|
158
180
|
window: win, document: doc, console, setTimeout: win.setTimeout, clearTimeout: win.clearTimeout,
|
|
159
181
|
setInterval: () => 0, clearInterval() {}, MutationObserver: win.MutationObserver,
|
|
160
182
|
ResizeObserver: win.ResizeObserver, requestAnimationFrame: win.requestAnimationFrame,
|
|
161
183
|
navigator: { userAgent: "iPhone Safari", maxTouchPoints: 5, platform: "iPhone" },
|
|
162
|
-
location:
|
|
184
|
+
location: loc,
|
|
163
185
|
localStorage: win.localStorage, URL, Date, Math, JSON, Object, Array, String, Number, RegExp, Boolean, Promise, Error, isNaN, parseInt, parseFloat,
|
|
164
186
|
HTMLElement: El, // 脚本用 `c instanceof HTMLElement` 过滤 frame 的子列
|
|
165
187
|
});
|
|
@@ -170,7 +192,7 @@ function harness({ collapsedAttr = undefined, detailsAttr = undefined, sidebarRe
|
|
|
170
192
|
ctx.console = dbgConsole;
|
|
171
193
|
ctx.globalThis = ctx;
|
|
172
194
|
ctx.self = win;
|
|
173
|
-
return { ctx, html, body, frame, sidebar, toggle, win };
|
|
195
|
+
return { ctx, html, body, frame, sidebar, rightbar, overlay, toggle, win };
|
|
174
196
|
}
|
|
175
197
|
|
|
176
198
|
const ADAPTER_DEBUG = process.env.DSH_ADAPTER_DEBUG === "1";
|
|
@@ -260,48 +282,83 @@ test("运行时:能拿到官方 toggle 时,点遮罩仍走官方 toggle(
|
|
|
260
282
|
assert.ok(env.toggle._clicked >= 1, "应点击官方 toggle");
|
|
261
283
|
});
|
|
262
284
|
|
|
263
|
-
// ──
|
|
285
|
+
// ── 远程控制悬浮按钮(2026-09-19:字号 + 加密小锁 + 返回设备列表,三合一) ──────
|
|
286
|
+
//
|
|
287
|
+
// 用户要求:「把字体的菜单和加密小锁的菜单合并成一个小悬浮按钮;点击后可以弹菜单调节字体,
|
|
288
|
+
// 也可以返回设备列表,让用户主动返回回去;手机端要有,电脑端也可以有。」
|
|
264
289
|
|
|
265
|
-
const
|
|
266
|
-
const
|
|
290
|
+
const findFab = (env) => env.body.children.find((c) => c._cls.has("dsh-ma-fab"));
|
|
291
|
+
const findMenu = (env) => env.body.children.find((c) => c._cls.has("dsh-ma-menu"));
|
|
267
292
|
|
|
268
|
-
test("
|
|
293
|
+
test("悬浮菜单:按钮与菜单都创建,菜单默认收起", () => {
|
|
269
294
|
const env = runAdapter(harness({}));
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
assert.ok(
|
|
273
|
-
assert.ok(
|
|
274
|
-
assert.equal(
|
|
275
|
-
assert.ok((panel.querySelector ? panel : {}).querySelector === undefined || true);
|
|
295
|
+
const fab = findFab(env);
|
|
296
|
+
const menu = findMenu(env);
|
|
297
|
+
assert.ok(fab, "应有悬浮按钮");
|
|
298
|
+
assert.ok(menu, "应有菜单面板");
|
|
299
|
+
assert.equal(menu.hidden, true, "菜单默认收起(点按钮才展开)");
|
|
276
300
|
});
|
|
277
301
|
|
|
278
|
-
test("
|
|
302
|
+
test("悬浮菜单:点按钮开合菜单", () => {
|
|
279
303
|
const env = runAdapter(harness({}));
|
|
280
|
-
|
|
281
|
-
|
|
304
|
+
const fab = findFab(env);
|
|
305
|
+
const menu = findMenu(env);
|
|
306
|
+
fab.dispatch("click");
|
|
307
|
+
assert.equal(menu.hidden, false, "点按钮应展开菜单");
|
|
308
|
+
fab.dispatch("click");
|
|
309
|
+
assert.equal(menu.hidden, true, "再点应收起");
|
|
282
310
|
});
|
|
283
311
|
|
|
284
|
-
test("
|
|
312
|
+
test("悬浮菜单:字号档位仍生效(属性/正文变量/持久化)", () => {
|
|
285
313
|
const env = runAdapter(harness({}));
|
|
286
|
-
const
|
|
287
|
-
const range =
|
|
288
|
-
assert.ok(range, "
|
|
289
|
-
// 打到「大」(level 2)
|
|
314
|
+
const menu = findMenu(env);
|
|
315
|
+
const range = menu.querySelector('input[type="range"]');
|
|
316
|
+
assert.ok(range, "菜单里应有字号滑块");
|
|
290
317
|
range.value = "2";
|
|
291
318
|
range.dispatch("input");
|
|
292
319
|
assert.equal(env.html.getAttribute("data-dsh-ma-fs"), "2", "应写入缩放属性(CSS 据此放大字号)");
|
|
293
|
-
assert.equal(env.win.localStorage.getItem("dsh-ma-font-scale"), "2", "
|
|
294
|
-
const contentVar = env.html.
|
|
295
|
-
assert.ok(contentVar, "应同步官方正文变量 --dsh-content-font-size
|
|
320
|
+
assert.equal(env.win.localStorage.getItem("dsh-ma-font-scale"), "2", "应持久化");
|
|
321
|
+
const contentVar = (env.html._cssVars || {})["--dsh-content-font-size"];
|
|
322
|
+
assert.ok(contentVar, "应同步官方正文变量 --dsh-content-font-size");
|
|
323
|
+
range.value = "0";
|
|
324
|
+
range.dispatch("input");
|
|
325
|
+
assert.equal(env.html.getAttribute("data-dsh-ma-fs"), null, "回到「最小」应移除缩放属性");
|
|
296
326
|
});
|
|
297
327
|
|
|
298
|
-
test("
|
|
328
|
+
test("悬浮菜单:必须有「返回设备列表」,且回到 /app/(用户在镜像页的唯一主动退路)", () => {
|
|
299
329
|
const env = runAdapter(harness({}));
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
330
|
+
const menu = findMenu(env);
|
|
331
|
+
const back = menu.querySelector(".dsh-ma-menu-back");
|
|
332
|
+
assert.ok(back, "菜单里必须有返回设备列表按钮");
|
|
333
|
+
back.dispatch("click");
|
|
334
|
+
assert.equal(env.ctx.location.href, "/app/", "点击后应跳到 APP 外壳(绝对路径,不能依赖当前镜像路径)");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("悬浮菜单:E2EE 状态可合并(shim 在场时隐藏独立药丸并读它的状态)", () => {
|
|
338
|
+
const env = harness({});
|
|
339
|
+
env.win.__dshE2eeBadge = { state: () => ({ mode: "warn", text: "未加密(刷新后需重新解锁)" }) };
|
|
340
|
+
runAdapter(env);
|
|
341
|
+
assert.equal(env.html.getAttribute("data-dsh-ma-merged-e2ee"), "1",
|
|
342
|
+
"shim 在场时应挂牌合并(CSS 据此隐藏官方那颗独立药丸)");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("多端适配:悬浮按钮与字号样式的 CSS 必须在 @media(max-width:820px) 之外(电脑端也要有)", () => {
|
|
346
|
+
const { html } = injectMobileAdapter("<!doctype html><html><head></head><body></body></html>");
|
|
347
|
+
const css = /<style id="[^"]*-css"[^>]*>([\s\S]*?)<\/style>/.exec(html)[1];
|
|
348
|
+
// 只保留 @media 之外的规则(做一次括号配平剥离),再断言关键选择器还在
|
|
349
|
+
let out = "", i = 0, depth = 0, inMedia = false;
|
|
350
|
+
while (i < css.length) {
|
|
351
|
+
if (css.startsWith("@media", i)) {
|
|
352
|
+
const open = css.indexOf("{", i);
|
|
353
|
+
let d = 1, j = open + 1;
|
|
354
|
+
while (j < css.length && d > 0) { if (css[j] === "{") d++; else if (css[j] === "}") d--; j++; }
|
|
355
|
+
i = j;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
out += css[i++];
|
|
359
|
+
}
|
|
360
|
+
assert.match(out, /button\.dsh-ma-fab\s*\{/, "悬浮按钮样式必须在媒体查询之外(电脑端可见)");
|
|
361
|
+
assert.match(out, /div\.dsh-ma-menu\s*\{/, "菜单样式必须在媒体查询之外");
|
|
362
|
+
assert.match(out, /html\[data-dsh-ma-fs="3"\]/, "字号档位样式也应在媒体查询之外(电脑端同样生效)");
|
|
363
|
+
assert.match(out, /z-index:\s*40[01]/, "悬浮按钮/菜单层级要高于遮罩(290),遮罩异常时仍是自救出口");
|
|
307
364
|
});
|
package/dsh-setup.mjs
CHANGED
|
@@ -863,11 +863,37 @@ dsh-remote:独立的本地设置页已移除。
|
|
|
863
863
|
`);
|
|
864
864
|
}
|
|
865
865
|
|
|
866
|
+
/**
|
|
867
|
+
* 账号指纹:账号 / 模式 / 设备身份任一变化都意味着"必须用新凭据重新登记"。
|
|
868
|
+
* 为什么需要:bridge 的 DSH_BRIDGE_PHONE/PASSWORD 是**进程启动时**从环境变量固化的,
|
|
869
|
+
* 配置改了并不会影响已在跑的进程 —— 这是"切换账号后设备仍留在旧账号"的根因之一。
|
|
870
|
+
*/
|
|
871
|
+
function accountFingerprint(cfg) {
|
|
872
|
+
const c = cfg || {};
|
|
873
|
+
return [c.local_key ? "local" : "saas", c.phone || c.email || "", String(c.device_id || ""), String(c.local_key || "")].join("|");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* 服务端登录限流的"最早可重试时刻"(bridge 在收到 429 时落盘)。
|
|
878
|
+
* 为什么需要:限流窗口是 15 分钟,而 watcher 每 10 秒就会重新拉起 bridge ——
|
|
879
|
+
* 不读这个文件就是 90 次空撞(日志刷屏、还可能拖长窗口)。它只是**退避提示**,绝不阻止正常启动。
|
|
880
|
+
*/
|
|
881
|
+
const LOGIN_RATELIMIT_FILE = ".dsh-login-ratelimited";
|
|
882
|
+
function loginRateLimitUntil() {
|
|
883
|
+
const f = path.join(CONFIG_DIR, LOGIN_RATELIMIT_FILE);
|
|
884
|
+
try {
|
|
885
|
+
const until = Number(String(fs.readFileSync(f, "utf8")).trim());
|
|
886
|
+
if (Number.isFinite(until) && until > Date.now()) return until;
|
|
887
|
+
fs.rmSync(f, { force: true }); // 已过期:清掉,恢复正常启动
|
|
888
|
+
} catch { /* 无文件 = 未限流 */ }
|
|
889
|
+
return 0;
|
|
890
|
+
}
|
|
891
|
+
|
|
866
892
|
// ---------- run:前台跑 bridge(带配置 + 自启动 watcher) ----------
|
|
867
893
|
async function runBridge() {
|
|
868
|
-
//
|
|
869
|
-
// 两个 bridge 会抢同一个设备登记 →
|
|
870
|
-
|
|
894
|
+
// 去重(所有平台):自启动服务、登录任务、插件半的脱离进程兜底都可能拉起 watcher,
|
|
895
|
+
// 两个 bridge 会抢同一个设备登记 → 以 pid 文件为准,已有守护在跑就直接退出。
|
|
896
|
+
{
|
|
871
897
|
const other = readPidFile(WATCHER_PID_FILE);
|
|
872
898
|
if (other && other !== process.pid && pidAlive(other)) {
|
|
873
899
|
console.log(`[dsh-remote] 已有一个 bridge 守护在运行(pid=${other}),本进程直接退出,避免重复实例。`);
|
|
@@ -876,6 +902,10 @@ async function runBridge() {
|
|
|
876
902
|
}
|
|
877
903
|
let cfg = loadConfig();
|
|
878
904
|
let warnedNoLogin = false;
|
|
905
|
+
/** 当前 bridge 子进程使用的账号指纹(账号/模式/设备身份任一变化 → 重启子进程)。 */
|
|
906
|
+
let bridgeAccountFp = "";
|
|
907
|
+
/** 限流提示只打一次,避免刷屏。 */
|
|
908
|
+
let warnedRateLimit = false;
|
|
879
909
|
|
|
880
910
|
// watcher:检测 dsh web(127.0.0.1:3080)存活,存活才启动 bridge
|
|
881
911
|
const checkUpstream = () => new Promise((resolve) => {
|
|
@@ -917,6 +947,28 @@ async function runBridge() {
|
|
|
917
947
|
}
|
|
918
948
|
const apiUrl = saas ? (cfg.api_url || DEFAULT_API) : "";
|
|
919
949
|
|
|
950
|
+
// 账号/模式/设备身份变了 → 必须重启 bridge(它的凭据是启动时从环境变量固化的)。
|
|
951
|
+
// 没有这一条时:面板切了账号、旧 bridge 却继续用旧账号跑,新账号的设备列表里看不到这台机器
|
|
952
|
+
//(2026-09-19 实测事故)。
|
|
953
|
+
const fp = accountFingerprint(cfg);
|
|
954
|
+
if (!childStopped(bridgeProc) && bridgeAccountFp && bridgeAccountFp !== fp) {
|
|
955
|
+
console.log("[dsh-remote] 账号配置已变化 → 重启 bridge 让新账号生效...");
|
|
956
|
+
try { bridgeProc.kill(); } catch { /* 已退出 */ }
|
|
957
|
+
bridgeAccountFp = "";
|
|
958
|
+
return; // 下一轮(10s 内)用新配置重新拉起
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// 服务端登录限流窗口内不反复拉起 bridge(凭据没错,改了也没用)
|
|
962
|
+
const rlUntil = loginRateLimitUntil();
|
|
963
|
+
if (rlUntil > Date.now()) {
|
|
964
|
+
if (!warnedRateLimit) {
|
|
965
|
+
warnedRateLimit = true;
|
|
966
|
+
console.log(`[dsh-remote] 登录被服务端限流,约 ${Math.ceil((rlUntil - Date.now()) / 1000)} 秒后自动重试(凭据无需修改)。`);
|
|
967
|
+
}
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
warnedRateLimit = false;
|
|
971
|
+
|
|
920
972
|
const alive = await checkUpstream();
|
|
921
973
|
if (alive && childStopped(bridgeProc)) {
|
|
922
974
|
starting = true;
|
|
@@ -944,7 +996,8 @@ async function runBridge() {
|
|
|
944
996
|
});
|
|
945
997
|
// Windows:落 pid 文件,供插件半发现进程(Windows 没有 pgrep/ps)。
|
|
946
998
|
// 插件半启动 bridge 走的就是本函数,所以这里写一次两边都覆盖到。
|
|
947
|
-
|
|
999
|
+
writePidFile(BRIDGE_PID_FILE, bridgeProc.pid); // 供插件半发现进程(Windows 无 pgrep;POSIX 用于脱离进程兜底的去重)
|
|
1000
|
+
bridgeAccountFp = fp; // 记下这个子进程用的是哪套凭据,配置一变就重启它
|
|
948
1001
|
setTimeout(() => { starting = false; }, 5000);
|
|
949
1002
|
} else if (!alive && bridgeProc && bridgeProc.exitCode === null) {
|
|
950
1003
|
console.log("[dsh-remote] dsh web 离线,停止 bridge...");
|
|
@@ -952,8 +1005,8 @@ async function runBridge() {
|
|
|
952
1005
|
}
|
|
953
1006
|
};
|
|
954
1007
|
|
|
955
|
-
//
|
|
956
|
-
|
|
1008
|
+
// watcher 自身的 pid 也要落盘(插件半据此判断"守护在跑"并避免重复拉起)
|
|
1009
|
+
{
|
|
957
1010
|
writePidFile(WATCHER_PID_FILE, process.pid);
|
|
958
1011
|
const cleanup = () => { removePidFile(WATCHER_PID_FILE); removePidFile(BRIDGE_PID_FILE); };
|
|
959
1012
|
process.on("exit", cleanup);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.7
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"clients/dsh-remote",
|
|
40
40
|
"packages/dsh-remote-web",
|
|
41
41
|
"docs/self-hosting.md",
|
|
42
|
-
"docs/telemetry.md"
|
|
42
|
+
"docs/telemetry.md",
|
|
43
|
+
"scripts/test-net-guard.cjs"
|
|
43
44
|
],
|
|
44
45
|
"workspaces": [
|
|
45
46
|
"packages/*",
|
|
@@ -50,10 +51,10 @@
|
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
53
|
"test": "npm run test:router && npm run test:plugin && npm run test:bridge",
|
|
53
|
-
"test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/presence-report.test.mjs packages/relay-router/test/logger.test.mjs",
|
|
54
|
+
"test:router": "NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/native-app-entry.test.mjs packages/relay-router/test/presence-report.test.mjs packages/relay-router/test/logger.test.mjs",
|
|
54
55
|
"_comment:test:plugin": "DSH_RELAY_SKIP_SERVICE=1 = 测试隔离:插件加载时不做真实系统操作(npx 补装 / launchctl / 重启 harness);需要真实分支的用例自行 unset(见 account-switch / runtime-provision / harness-restart 用例注释)",
|
|
55
|
-
"test:plugin": "DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
|
|
56
|
-
"test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
|
|
56
|
+
"test:plugin": "DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
|
|
57
|
+
"test:bridge": "NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test clients/dsh-remote/test/*.test.mjs",
|
|
57
58
|
"check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && node --check packages/relay-router/src/index.mjs && node --check packages/relay-router/src/logger.mjs && bash -n deploy/install-open.sh && grep -q 'command -v setsid' deploy/install-open.sh && grep -q 'command -v ss' deploy/install-open.sh",
|
|
58
59
|
"release:tarballs": "bash scripts/release-tarballs.sh"
|
|
59
60
|
},
|
|
@@ -1727,6 +1727,25 @@ window.__ModuleLoader__.load({
|
|
|
1727
1727
|
// ---------- 登录(密码/短信) ----------
|
|
1728
1728
|
var doLogin = function () {
|
|
1729
1729
|
if (!phone.trim() || !pass) { setMsg("err", "请填写手机号与密码"); return; }
|
|
1730
|
+
/**
|
|
1731
|
+
* 账号落盘(POST /dsh-remote/config)后的统一提示。
|
|
1732
|
+
*
|
|
1733
|
+
* 为什么单独抽出来:这次"换账号后设备不出现"的事故就是**静默失败**造成的 ——
|
|
1734
|
+
* /config 会顺带重启 bridge(让新账号生效),但其返回的 bridgeRestart 前端从来不看,
|
|
1735
|
+
* 于是"服务没重启成功"和"一切正常"在界面上长得一模一样。
|
|
1736
|
+
*/
|
|
1737
|
+
function applyAccountSaved(cfg, okText) {
|
|
1738
|
+
setSt(cfg);
|
|
1739
|
+
var br = cfg && cfg.bridgeRestart;
|
|
1740
|
+
if (br && br.ok === false && br.status !== "running" && br.status !== "provisioning" && br.status !== "skipped") {
|
|
1741
|
+
setMsg("warn", okText + ";但后台服务(bridge)未能自动重启:"
|
|
1742
|
+
+ ((br.detail || br.status) || "未知原因")
|
|
1743
|
+
+ " —— 新账号可能收不到这台设备,请点下方「重启服务」或重启一次 dsh web。");
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
setMsg("ok", okText);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1730
1749
|
if (!lcap || !lcap.id || !lcapTxt.trim()) { setMsg("err", "请输入图中验证码(点击图片可刷新)"); if (!lcap) loadCaptcha("login"); return; }
|
|
1731
1750
|
var prevPhone = (st && st.config && st.config.phone) || "";
|
|
1732
1751
|
setBusy("login");
|
|
@@ -1737,8 +1756,8 @@ window.__ModuleLoader__.load({
|
|
|
1737
1756
|
// 切换连接账号(手机号与之前不同):清除旧账号留下的反馈线程凭据
|
|
1738
1757
|
if (prevPhone !== phone.trim()) fbClearThreads();
|
|
1739
1758
|
setPwdOpen(false); // 登录成功(可能从「忘记密码」返回)→ 收起重置表单
|
|
1740
|
-
|
|
1741
|
-
|
|
1759
|
+
setPass(""); setLcapTxt(""); setLcap(null);
|
|
1760
|
+
applyAccountSaved(cfg, "✅ 登录成功,账号已保存");
|
|
1742
1761
|
});
|
|
1743
1762
|
}
|
|
1744
1763
|
var relayBody = body.body || {};
|
|
@@ -1791,9 +1810,8 @@ window.__ModuleLoader__.load({
|
|
|
1791
1810
|
if (body.ok || body.status === 201 || (body.body && body.body.token)) {
|
|
1792
1811
|
return post("/dsh-remote/config", { phone: rphone.trim(), password: rpass }).then(function (cfg) {
|
|
1793
1812
|
setPwdOpen(false);
|
|
1794
|
-
setSt(cfg);
|
|
1795
1813
|
setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRcap(null); setRcapTxt(""); setRInvite("");
|
|
1796
|
-
|
|
1814
|
+
applyAccountSaved(cfg, "✅ 注册成功,已自动登录");
|
|
1797
1815
|
});
|
|
1798
1816
|
}
|
|
1799
1817
|
var b = body.body || {};
|