@mrrisega/dsh-remote 0.6.16 → 0.6.18-beta.1
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 +34 -0
- package/clients/dsh-remote/e2ee-shim-script.js +168 -8
- package/clients/dsh-remote/mobile-adapter.mjs +497 -28
- package/clients/dsh-remote/test/bridge-html-inject.test.mjs +33 -1
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +168 -4
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +95 -6
- package/clients/dsh-remote/wechat-runtime.mjs +25 -2
- package/docs/telemetry.md +7 -1
- package/dsh-setup.mjs +152 -3
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +781 -50
- package/packages/dsh-remote-web/lib/index.js +342 -6
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +34 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +168 -8
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +497 -28
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +25 -2
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +152 -3
- package/packages/dsh-remote-web/test/account-switch-device.test.mjs +20 -2
- package/packages/dsh-remote-web/test/announcements-proxy.test.mjs +310 -0
- package/packages/dsh-remote-web/test/announcements-ui.test.mjs +524 -0
- package/packages/dsh-remote-web/test/connect-stuck-visibility.test.mjs +4 -1
- package/packages/dsh-remote-web/test/diag-report.test.mjs +4 -1
- package/packages/dsh-remote-web/test/doctor-cli.test.mjs +17 -1
- package/packages/dsh-remote-web/test/harness-restart.test.mjs +5 -1
- package/packages/dsh-remote-web/test/installer-cli.test.mjs +1 -1
- package/packages/dsh-remote-web/test/launchd-domain.test.mjs +26 -0
- package/packages/dsh-remote-web/test/picker-pin.test.mjs +32 -2
- package/packages/dsh-remote-web/test/plugin-link.test.mjs +3 -1
- package/packages/dsh-remote-web/test/plugin-manifest.test.mjs +2 -2
- package/packages/dsh-remote-web/test/runtime-bundle.test.mjs +4 -1
- package/packages/dsh-remote-web/test/telemetry.test.mjs +29 -10
- package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +3 -3
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +8 -3
- package/packages/dsh-remote-web/test/windows-compat.test.mjs +7 -4
|
@@ -764,6 +764,7 @@ async function doHttp(method, path, reqHeaders, body, isB64) {
|
|
|
764
764
|
if (m.injected) {
|
|
765
765
|
console.log(`[bridge] mobile-adapter 注入 ${path}: ${(buf.length / 1024).toFixed(0)}KB → ${(m.buf.length / 1024).toFixed(0)}KB`);
|
|
766
766
|
buf = m.buf;
|
|
767
|
+
armMirrorLoadWatch(path);
|
|
767
768
|
}
|
|
768
769
|
// E2EE 镜像页加密 shim(Phase-4):仅桥端 e2ee 启用时注入(e2ee.enabled=false → 零注入零行为)
|
|
769
770
|
if (e2ee.enabled) {
|
|
@@ -830,10 +831,39 @@ export function e2eeHttpResponseFrame(id, sessId, respEnv) {
|
|
|
830
831
|
};
|
|
831
832
|
}
|
|
832
833
|
|
|
834
|
+
/**
|
|
835
|
+
* 「镜像页交付了、但页面再也没回来」的看门狗(2026-09-25)。
|
|
836
|
+
*
|
|
837
|
+
* 【为什么需要】首屏提示层被整块删除后,页面上不再有任何东西能告诉用户"没加载完"
|
|
838
|
+
* (那正是业主的要求:不许再有会盖住界面的层)。但**服务端能看见一个更硬的信号**:
|
|
839
|
+
* 我们把镜像页 HTML 交付给手机后,官方前端一定会在几秒内发一串请求(模块、会话、mux…)。
|
|
840
|
+
* 若 60 秒内一个都没有,那这个页面几乎必然没起来 —— 这时在**日志**里留一条带时间戳的结论,
|
|
841
|
+
* 用户「复制诊断信息」或 `dsh-remote doctor` 时就能看到,不用再靠猜。
|
|
842
|
+
*
|
|
843
|
+
* 代价与边界:
|
|
844
|
+
* · 只是**一次**待办标记 + 一个 unref 定时器,没有额外请求、没有额外文件;
|
|
845
|
+
* · 60 秒很宽松(正常加载 1~3 秒就回来一串请求);用户"打开就关掉"最多多一行日志;
|
|
846
|
+
* · 只记日志,不做任何用户可见动作(不注入、不弹层、不改状态)。
|
|
847
|
+
*/
|
|
848
|
+
let mirrorLoadWatch = null;
|
|
849
|
+
function armMirrorLoadWatch(path) {
|
|
850
|
+
mirrorLoadWatch = { at: Date.now(), path };
|
|
851
|
+
const timer = setTimeout(() => {
|
|
852
|
+
if (!mirrorLoadWatch) return;
|
|
853
|
+
const waited = Math.round((Date.now() - mirrorLoadWatch.at) / 1000);
|
|
854
|
+
console.error(`[bridge] ⚠️ 镜像页疑似没加载起来:交付 ${mirrorLoadWatch.path} 后 ${waited} 秒内没有任何后续请求`
|
|
855
|
+
+ `(正常 1~3 秒就会有一串)。手机侧请刷新一次;仍不行把这条日志发给客服。`);
|
|
856
|
+
mirrorLoadWatch = null;
|
|
857
|
+
}, 60 * 1000);
|
|
858
|
+
timer.unref?.();
|
|
859
|
+
}
|
|
860
|
+
|
|
833
861
|
export async function handleHttpFrame(dchOrSend, frame) {
|
|
834
862
|
const send = toSender(dchOrSend);
|
|
835
863
|
const { id, method = "GET", path = "/", headers = {}, body, bodyBase64: isB64 } = frame;
|
|
836
864
|
const t0 = Date.now();
|
|
865
|
+
// 页面还活着:任何后续请求都撤掉「镜像页加载」看门狗(见 armMirrorLoadWatch)
|
|
866
|
+
if (mirrorLoadWatch) { mirrorLoadWatch = null; }
|
|
837
867
|
// 桌面授权引导(方案A):POST /_e2ee/intro(device/channel 形态归一)—— 本地应答,不连上游
|
|
838
868
|
if (method === "POST" && path === "/_e2ee/intro") {
|
|
839
869
|
return answerIntro(send, id);
|
|
@@ -1675,6 +1705,10 @@ async function main() {
|
|
|
1675
1705
|
return runTunnel();
|
|
1676
1706
|
}
|
|
1677
1707
|
|
|
1708
|
+
// test hooks:镜像页加载看门狗的内部状态(给用例断言"后续请求会撤掉它",见 test/bridge-html-inject.test.mjs)
|
|
1709
|
+
export function __mirrorLoadWatchState() { return mirrorLoadWatch ? { ...mirrorLoadWatch } : null; }
|
|
1710
|
+
export function __armMirrorLoadWatch(path) { armMirrorLoadWatch(path); }
|
|
1711
|
+
|
|
1678
1712
|
// 直接运行(node dsh-bridge.mjs)时启动服务;被测试 import 时只导出协议函数。
|
|
1679
1713
|
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
1680
1714
|
if (isMain) {
|
|
@@ -277,6 +277,46 @@
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
/* ---- 上游路径 / channel|device 形态归一(与 router resolveRoute / native wcUpstreamPathOf 同构) ---- */
|
|
280
|
+
/**
|
|
281
|
+
* 从镜像页路径取出 deviceId:`/remote/<deviceId>/…` → deviceId(其余形态返回 "")。
|
|
282
|
+
* 与 mobile-adapter.mjs 的 deviceIdOfPath() 同一套判据;这里不查任何 cookie。
|
|
283
|
+
*/
|
|
284
|
+
function seDeviceIdOfPath(pathname) {
|
|
285
|
+
try {
|
|
286
|
+
var parts = String(pathname || "").split("/").filter(Boolean);
|
|
287
|
+
if (parts[0] !== "remote" || !parts[1]) return "";
|
|
288
|
+
var id = decodeURIComponent(parts[1]);
|
|
289
|
+
return /^[A-Za-z0-9._-]{1,64}$/.test(id) ? id : "";
|
|
290
|
+
} catch (e) { return ""; }
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* 把**根路径**的受保护请求改写成"设备作用域"形态:`/api/x` → `/remote/<deviceId>/api/x`。
|
|
294
|
+
*
|
|
295
|
+
* 为什么必须(2026-09-26 线上事故,nginx access/error 日志为证):
|
|
296
|
+
* 官方客户端把 mux WS 的地址固定按 **`location.origin`** 拼成 `/api/remote.mux`
|
|
297
|
+
* (@deepseek-ai/dsh-api-gateway/lib/client.js:544 的 remoteStreamUrl())—— 它**永远**不带设备前缀,
|
|
298
|
+
* 于是中继只能靠 **`dsh_device` cookie** 来判"这是哪台设备"(relay-router/src/index.mjs:626-627
|
|
299
|
+
* 的根路径兜底)。该 cookie 是会过期的:拿不到就 resolveRoute → null → upgrade 直接
|
|
300
|
+
* `rejectUpgrade(socket, 404)`(:1294),nginx 侧只看到 `009`/`404 9` 这种截断响应。
|
|
301
|
+
* 而设备作用域形态 `/remote/<deviceId>/api/remote.mux` 由**路径本身**携带设备身份,
|
|
302
|
+
* 不依赖任何 cookie(同 rc 的测试就是这么连的:relay-router/test/router-e2e.test.mjs:267)。
|
|
303
|
+
*
|
|
304
|
+
* 安全性:中继对两种形态会归一到**同一个上游路径**(resolveRoute 的 device 分支取 m[2]),
|
|
305
|
+
* bridge/宿主看到的 path/label 与改写前逐字节一致 —— 只是"设备凭据从 cookie 挪到了 URL"。
|
|
306
|
+
* 保守性:只有在**页面路径里能读到 deviceId**(`/remote/<id>/…`,也就是 2026-09-19 起的标准入口)
|
|
307
|
+
* 且该路径确实是根形态的受保护前缀时才改写;读不到(老入口/根路径打开)就原样不动,
|
|
308
|
+
* 行为与今天完全一致 —— 宁可退化,也绝不猜设备。
|
|
309
|
+
*/
|
|
310
|
+
function seScopeToDevice(fullPath, deviceId) {
|
|
311
|
+
if (!deviceId) return String(fullPath || "");
|
|
312
|
+
var p = String(fullPath || "");
|
|
313
|
+
if (p.indexOf("/remote/") === 0) return p; // 已经是设备/通道形态 → 不动
|
|
314
|
+
var qIdx = p.indexOf("?");
|
|
315
|
+
var path = qIdx === -1 ? p : p.slice(0, qIdx);
|
|
316
|
+
var qs = qIdx === -1 ? "" : p.slice(qIdx);
|
|
317
|
+
if (!seIsProtectedPath(path)) return p; // 只改写受保护前缀(与加密策略同源)
|
|
318
|
+
return "/remote/" + encodeURIComponent(deviceId) + path + qs;
|
|
319
|
+
}
|
|
280
320
|
function seUpstreamPathOf(fullPath) {
|
|
281
321
|
var p = String(fullPath || "");
|
|
282
322
|
var query = "";
|
|
@@ -384,6 +424,58 @@
|
|
|
384
424
|
if (typeof console !== "undefined" && console.warn) console.warn("[dsh-e2ee-shim]", message);
|
|
385
425
|
}
|
|
386
426
|
|
|
427
|
+
/* ---- 会话失效自愈(2026-09-26 线上事故:unknown_session 502 + 刷新也好不了) -------------
|
|
428
|
+
链路:手机镜像页 → 加密信封 → 中继透明转发 → 桥端 e2ee-client 查会话表 → 查不到就回
|
|
429
|
+
**明文** 502 + `x-dsh-e2ee-error: unknown_session`(桥端会话表是进程内存 + 24h TTL + CAP=32 LRU,
|
|
430
|
+
桥重启/被挤掉之后就没了)。而手机侧靠 **localStorage 持久交接单**长期复用同一个 sessId
|
|
431
|
+
(见 _boot 的"记住本机"分支)→ 每个受保护路径全 502,且**刷新只会把同一张失效交接单再装回去**。
|
|
432
|
+
修法(只做两件事,绝不静默降级):
|
|
433
|
+
① 收到该头 → 立刻清掉 sessionStorage/localStorage 的交接单 → 下一次进入镜像页时
|
|
434
|
+
"没有交接单"→ 走正常流程回设备列表重新解锁;
|
|
435
|
+
② 通过 seStaleSessionHook 把**明确原因**交给 DOM 层写进徽标(需要重新解锁),
|
|
436
|
+
不再让用户看到"加密好好的,但什么都点不动"。
|
|
437
|
+
⚠️ 不改 SE_PROTECTED_PREFIXES:把 /sidebar 移出白名单会让 fs.read/write、pty.*、git.commit
|
|
438
|
+
明文过境(见 :39 的策略表),那是安全红线。 */
|
|
439
|
+
var seStaleSessionHook = null;
|
|
440
|
+
var seLastStaleCode = "";
|
|
441
|
+
/** 清掉本机保存的 E2EE 交接单(sessionStorage 一次性 + localStorage 持久)。node 测试无 storage → 静默跳过。 */
|
|
442
|
+
function seForgetHandoverStores() {
|
|
443
|
+
try { if (typeof sessionStorage !== "undefined" && sessionStorage && sessionStorage.removeItem) sessionStorage.removeItem(DSH_E2EE_HANDOVER_KEY); } catch (e) { /* ignore */ }
|
|
444
|
+
try { if (typeof localStorage !== "undefined" && localStorage && localStorage.removeItem) localStorage.removeItem(DSH_E2EE_HANDOVER_KEY); } catch (e) { /* ignore */ }
|
|
445
|
+
}
|
|
446
|
+
/** 读响应头里的桥端 E2EE 错误码(非信封响应才有;取不到返回 "")。 */
|
|
447
|
+
function seE2eeErrorOf(res) {
|
|
448
|
+
try { return String((res && res.headers && res.headers.get && res.headers.get("x-dsh-e2ee-error")) || ""); } catch (e) { return ""; }
|
|
449
|
+
}
|
|
450
|
+
/** 收到带 x-dsh-e2ee-error 的响应 → 清失效会话 + 上报(同一个码只报一次,避免刷屏)。 */
|
|
451
|
+
function seNoteE2eeError(res) {
|
|
452
|
+
var code = seE2eeErrorOf(res);
|
|
453
|
+
if (!code) return "";
|
|
454
|
+
seForgetHandoverStores();
|
|
455
|
+
if (code !== seLastStaleCode) {
|
|
456
|
+
seLastStaleCode = code;
|
|
457
|
+
try { if (typeof seStaleSessionHook === "function") seStaleSessionHook(code); } catch (e) { /* ignore */ }
|
|
458
|
+
seNotifyFail("加密会话已失效(" + code + "):本机保存的会话已不可用,需要重新解锁。");
|
|
459
|
+
}
|
|
460
|
+
return code;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* ---- 数据 WS 握手失败上报(2026-09-26:线上 mux 握手 404/009 → 用户只看到"什么都没发生") ----
|
|
464
|
+
mux WS 是镜像页与电脑端之间**唯一**的实时数据通道;它连不上时页面不报错,只是所有数据都停住。
|
|
465
|
+
这里只做**可观测**:把"没连上"作为明确原因交给徽标(区分"会话未建立/设备离线/网络中断"),
|
|
466
|
+
绝不在这里做重连 —— 官方客户端自己会重连,我们再加一层只会变成"静默无限重连"。 */
|
|
467
|
+
var seWsFailHook = null;
|
|
468
|
+
var seWsFailCount = 0;
|
|
469
|
+
var seWsFailReported = false;
|
|
470
|
+
function seNoteWsHandshakeFail(label, detail) {
|
|
471
|
+
seWsFailCount++;
|
|
472
|
+
if (seWsFailReported) return;
|
|
473
|
+
seWsFailReported = true;
|
|
474
|
+
try { if (typeof seWsFailHook === "function") seWsFailHook(String(label || ""), String(detail || ""), seWsFailCount); } catch (e) { /* ignore */ }
|
|
475
|
+
}
|
|
476
|
+
/** 握手成功后复位(下一次再断时还能再报一次;不清 count,用于展示"重试了几次")。 */
|
|
477
|
+
function seNoteWsOpened() { seWsFailReported = false; }
|
|
478
|
+
|
|
387
479
|
/* ---- HTTP 信封传输包装(与 native e2eeFetchEnveloped 同构;输入/输出为 fetch 语义) ---- */
|
|
388
480
|
var SE_OUTER_STRIP = {
|
|
389
481
|
"host": 1, "origin": 1, "referer": 1, "cookie": 1, "connection": 1, "upgrade": 1,
|
|
@@ -464,7 +556,9 @@
|
|
|
464
556
|
signal: req.signal
|
|
465
557
|
});
|
|
466
558
|
} catch (e) { throw e; }
|
|
467
|
-
|
|
559
|
+
/* router/桥端明文错误页 → 原样透传(零行为),但**先看一眼**是不是桥端在说"会话我不认识"
|
|
560
|
+
(x-dsh-e2ee-error)——是的话清掉本机失效交接单并给出明确原因(见 seNoteE2eeError)。 */
|
|
561
|
+
if (!seIsEnvelopeResponse(res)) { seNoteE2eeError(res); return res; }
|
|
468
562
|
var envResp = null;
|
|
469
563
|
try { envResp = await res.json(); } catch (e) {
|
|
470
564
|
seNotifyFail("响应信封非法");
|
|
@@ -506,10 +600,18 @@
|
|
|
506
600
|
this._recvQ = Promise.resolve();
|
|
507
601
|
this.binaryType = "blob";
|
|
508
602
|
var inner = this._inner;
|
|
509
|
-
|
|
510
|
-
inner.
|
|
603
|
+
this._opened = false; // 是否**曾经**握手成功(区分"没连上"与"连上后断开")
|
|
604
|
+
inner.onopen = function (ev) { self._state = 1; self._opened = true; seNoteWsOpened(); if (typeof self._onopen === "function") self._onopen(ev || { type: "open", target: self }); };
|
|
605
|
+
inner.onerror = function (ev) {
|
|
606
|
+
/* 握手上就失败(error 早于任何 open)→ 让用户知道"加密数据通道没建立"。
|
|
607
|
+
这是 2026-09-26 事故里最缺的一条信号:mux 被 404 掐掉时页面**一声不响**,
|
|
608
|
+
用户只看到所有数据都停住。 */
|
|
609
|
+
if (!self._opened) seNoteWsHandshakeFail(self._meta && self._meta.wsLabel, "error");
|
|
610
|
+
if (typeof self._onerror === "function") self._onerror(ev || { type: "error", target: self });
|
|
611
|
+
};
|
|
511
612
|
inner.onclose = function (ev) {
|
|
512
613
|
self._state = 3;
|
|
614
|
+
if (!self._opened) seNoteWsHandshakeFail(self._meta && self._meta.wsLabel, (ev && (ev.code || ev.reason)) || "close");
|
|
513
615
|
if (typeof self._onclose === "function") self._onclose(ev || { type: "close", target: self });
|
|
514
616
|
};
|
|
515
617
|
inner.onmessage = function (ev) { self._enqueueInbound(ev); };
|
|
@@ -608,14 +710,28 @@
|
|
|
608
710
|
/** 生成 window.WebSocket 的替换构造函数:命中策略才套 SeE2eeWs,否则原样放行。 */
|
|
609
711
|
function seMakeWsCtor(NativeWS, ctx) {
|
|
610
712
|
function SeWs(url, protocols) {
|
|
611
|
-
var full = "", should = false;
|
|
713
|
+
var full = "", should = false, scoped = String(url);
|
|
612
714
|
try {
|
|
613
715
|
var base = ctx.origin || (typeof window !== "undefined" ? window.location.href : undefined);
|
|
614
716
|
var uo = new URL(url, base);
|
|
615
|
-
if (seSameWebContext(uo, ctx.origin)) {
|
|
717
|
+
if (seSameWebContext(uo, ctx.origin)) {
|
|
718
|
+
/* ★ 设备作用域改写(2026-09-26 事故):官方客户端把 mux WS 固定拼成**根路径**
|
|
719
|
+
`/api/remote.mux`(dsh-api-gateway/lib/client.js:544 用 location.origin 拼),
|
|
720
|
+
于是中继只能靠 `dsh_device` cookie 判设备;cookie 一过期/缺失就是 404(upgrade 被
|
|
721
|
+
rejectUpgrade 掐掉,nginx 只看到 009)→ E2EE 会话永远建不起来 → 之后每个受保护请求
|
|
722
|
+
全是 unknown_session 502。镜像页的标准入口是 `/remote/<deviceId>/`(2026-09-19 起),
|
|
723
|
+
deviceId 就在地址里 —— 拿它把 WS 改写成 `/remote/<deviceId>/api/remote.mux`,
|
|
724
|
+
设备身份由路径携带,不再依赖任何 cookie(中继会把两种形态归一到同一上游路径)。
|
|
725
|
+
⚠️ 只改 WS:HTTP 那条链路影响面大得多,且线上证据显示它当时是通的
|
|
726
|
+
(502 是桥端回的**信封响应**,说明 HTTP 请求已经到达该设备)。 */
|
|
727
|
+
scoped = seScopeToDevice(uo.pathname + uo.search, ctx.deviceId);
|
|
728
|
+
var so = scoped === (uo.pathname + uo.search) ? uo : new URL(scoped, base);
|
|
729
|
+
full = so.pathname + so.search;
|
|
730
|
+
should = seShouldEncryptWs(full);
|
|
731
|
+
}
|
|
616
732
|
} catch (e) { should = false; }
|
|
617
733
|
if (!should) return new NativeWS(url, protocols);
|
|
618
|
-
var target = seBuildWsTarget(
|
|
734
|
+
var target = seBuildWsTarget(scoped, ctx.sess.sessId, seRandomHex(8), base);
|
|
619
735
|
if (!target) return new NativeWS(url, protocols);
|
|
620
736
|
return new SeE2eeWs(NativeWS, target.href, protocols, { sess: ctx.sess, wsLabel: target.wsLabel });
|
|
621
737
|
}
|
|
@@ -731,6 +847,33 @@
|
|
|
731
847
|
function _reportFail(message) {
|
|
732
848
|
_badge("err", "无法解密", String(message || "会话异常"));
|
|
733
849
|
}
|
|
850
|
+
/* 桥端回了 x-dsh-e2ee-error(unknown_session / not_verified / auth_failed …):
|
|
851
|
+
会话已经**不可恢复**,本机交接单已由 seNoteE2eeError 清掉 —— 这里只负责把"为什么"
|
|
852
|
+
说清楚(用户看到的必须是"需要重新解锁",而不是一个说不清的 502 或一句"已失效")。 */
|
|
853
|
+
var _STALE_HINT = {
|
|
854
|
+
unknown_session: "电脑端已不认识这个加密会话(通常是电脑端重启过,或会话太多被挤掉)。",
|
|
855
|
+
not_verified: "这个加密会话还没完成校验。",
|
|
856
|
+
auth_failed: "解密校验失败:密码与电脑端不一致,或传输被篡改。",
|
|
857
|
+
replay: "检测到重放。",
|
|
858
|
+
bad_envelope: "响应信封非法。",
|
|
859
|
+
bad_plain: "响应明文非法。"
|
|
860
|
+
};
|
|
861
|
+
function _reportStaleSession(code) {
|
|
862
|
+
var hint = _STALE_HINT[String(code || "")] || ("电脑端返回:" + String(code || "") + "。");
|
|
863
|
+
_badge("warn", "加密会话已失效,需要重新解锁", hint + " 已清除本机保存的会话;返回设备列表重新点选设备解锁即可恢复加密。");
|
|
864
|
+
}
|
|
865
|
+
/* 加密数据 WS(mux)**握手就没成功**:官方客户端只会安静地重连,页面不报错、数据全停。
|
|
866
|
+
这里把原因分档说清楚(会话/设备/网络),但**不做任何重连**(重连是官方的事,
|
|
867
|
+
我们再叠一层就变成"静默无限重连"——正是这次要修掉的形态)。 */
|
|
868
|
+
function _reportWsFail(label, detail, count) {
|
|
869
|
+
var n = Number(count) || 1;
|
|
870
|
+
var why = String(detail || "") === "error"
|
|
871
|
+
? "电脑端可能未连接(设备离线)、网络中断,或加密会话已失效。"
|
|
872
|
+
: ("连接被关闭(" + String(detail || "close") + "),多半是握手被拒或被中继/代理掐断。");
|
|
873
|
+
_badge("warn", "加密数据通道未建立(第 " + n + " 次)",
|
|
874
|
+
"与电脑端的加密 WebSocket 没连上:" + why + " 弱网时官方客户端会自动重连,稍等即可;"
|
|
875
|
+
+ "一直这样请返回设备列表重新解锁(会自动重建加密通道)。");
|
|
876
|
+
}
|
|
734
877
|
|
|
735
878
|
/* ---- 安装 fetch/WS 加密补丁(有会话才安装) ---- */
|
|
736
879
|
var _installed = false;
|
|
@@ -738,7 +881,12 @@
|
|
|
738
881
|
if (_installed) return;
|
|
739
882
|
_installed = true;
|
|
740
883
|
seFailureHook = _reportFail;
|
|
741
|
-
|
|
884
|
+
seStaleSessionHook = _reportStaleSession;
|
|
885
|
+
seWsFailHook = _reportWsFail;
|
|
886
|
+
/* deviceId 取自**页面路径**(`/remote/<deviceId>/…`,2026-09-19 起的标准入口):
|
|
887
|
+
WS 侧用它把根路径的 mux 改写成设备作用域形态,从而不再依赖会过期的 dsh_device cookie
|
|
888
|
+
(见 seScopeToDevice 的长注释与 2026-09-26 的 nginx 日志证据)。读不到就留空 = 不改写。 */
|
|
889
|
+
var ctx = { sess: sess, origin: window.location.origin, deviceId: seDeviceIdOfPath(window.location.pathname) };
|
|
742
890
|
try {
|
|
743
891
|
if (window.fetch) {
|
|
744
892
|
var origFetch = window.fetch.bind(window);
|
|
@@ -772,7 +920,19 @@
|
|
|
772
920
|
try { lsSess = seSessionOfHandover(lsRaw); } catch (e) { lsSess = null; }
|
|
773
921
|
if (lsSess) {
|
|
774
922
|
_install(lsSess);
|
|
775
|
-
|
|
923
|
+
/* ⚠️ 0.6.18 起这句文案不再无条件说"加密好好的"(2026-09-26 事故的教训):
|
|
924
|
+
从 localStorage 恢复的会话**没法在本机验证**桥端是否还认它(桥端会话表是进程内存 +
|
|
925
|
+
24h TTL + CAP=32 LRU,桥重启/被挤掉之后这张交接单就是死票)。真实校验只能等
|
|
926
|
+
第一个受保护请求 —— 那一步由 seNoteE2eeError 兜底:一旦桥端回 unknown_session,
|
|
927
|
+
会话被清掉、徽标转 warn 并明说"需要重新解锁"。
|
|
928
|
+
这里**刻意仍用 ok 态**(而不是一刷新就报 ⚠):99% 的刷新会话是有效的,
|
|
929
|
+
照 R2 的建议无条件降级会让每次刷新都出现一个假告警 —— 本仓库一贯的取向是
|
|
930
|
+
"宁可漏报,也不要误报吓人"(见 mobile-adapter 的 load-warn 看门狗注释)。
|
|
931
|
+
折中做法:文案如实说明"恢复但未验证",并把出口写清楚。 */
|
|
932
|
+
_badge("ok", "端到端加密已开启(本机记忆恢复)",
|
|
933
|
+
"刷新后已自动恢复端到端加密(记住本机),密钥仅存内存。"
|
|
934
|
+
+ "注意:这次恢复的会话要到第一个数据请求才能确认电脑端仍认可它 —— "
|
|
935
|
+
+ "若稍后提示「加密会话已失效」,说明电脑端已重启或会话被挤掉,请返回设备列表重新解锁。");
|
|
776
936
|
return;
|
|
777
937
|
}
|
|
778
938
|
}
|