@mrrisega/dsh-remote 0.7.0-beta.8 → 0.7.1-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 +17 -17
- package/clients/dsh-remote/dsh-events.mjs +6 -6
- package/clients/dsh-remote/e2ee-client.mjs +7 -7
- package/clients/dsh-remote/e2ee-shim-script.js +9 -9
- package/clients/dsh-remote/mobile-adapter.mjs +294 -43
- package/clients/dsh-remote/test/bridge-gzip.test.mjs +1 -1
- package/clients/dsh-remote/test/device-registration-report.test.mjs +1 -1
- package/clients/dsh-remote/test/dsh-events.test.mjs +10 -10
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +4 -4
- package/clients/dsh-remote/test/e2ee-client.test.mjs +7 -7
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +1 -1
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +1 -1
- package/clients/dsh-remote/test/harness-cookie-authority.test.mjs +4 -4
- package/clients/dsh-remote/test/machine-fingerprint.test.mjs +2 -2
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +6 -6
- package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +1 -1
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +5 -5
- package/clients/dsh-remote/test/mobile-adapter-settings.test.mjs +352 -0
- package/clients/dsh-remote/test/mobile-adapter.test.mjs +35 -0
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +1 -1
- package/clients/dsh-remote/test/wechat-channel.test.mjs +5 -5
- package/clients/dsh-remote/test/wechat-runtime.test.mjs +26 -26
- package/clients/dsh-remote/test/wechat-tier-source.test.mjs +2 -2
- package/clients/dsh-remote/wechat-channel.mjs +9 -9
- package/clients/dsh-remote/wechat-runtime.mjs +38 -38
- package/docs/telemetry.md +3 -3
- package/dsh-setup.mjs +27 -27
- package/package.json +3 -3
- package/packages/dsh-remote-web/lib/client.js +56 -56
- package/packages/dsh-remote-web/lib/fix-prompt.js +1 -1
- package/packages/dsh-remote-web/lib/index.js +380 -59
- package/packages/dsh-remote-web/lib/tarball-update.js +1 -1
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +17 -17
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +6 -6
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +7 -7
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +9 -9
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +294 -43
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +9 -9
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +38 -38
- package/packages/dsh-remote-web/runtime/clients/dsh-web/native.html +5 -5
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +27 -27
- package/packages/dsh-remote-web/test/account-switch-device.test.mjs +2 -2
- package/packages/dsh-remote-web/test/activation-single-point.test.mjs +1 -1
- package/packages/dsh-remote-web/test/announcements-ui.test.mjs +1 -1
- package/packages/dsh-remote-web/test/connect-loop.test.mjs +1 -1
- package/packages/dsh-remote-web/test/connect-ui.test.mjs +2 -2
- package/packages/dsh-remote-web/test/desktop-compat.test.mjs +4 -4
- package/packages/dsh-remote-web/test/diag-report.test.mjs +1 -1
- package/packages/dsh-remote-web/test/fix-prompt.test.mjs +2 -2
- package/packages/dsh-remote-web/test/harness-restart.test.mjs +1 -1
- package/packages/dsh-remote-web/test/install-profile-targets.test.mjs +3 -3
- package/packages/dsh-remote-web/test/installer-cli.test.mjs +1 -1
- package/packages/dsh-remote-web/test/launchd-domain.test.mjs +2 -2
- package/packages/dsh-remote-web/test/patch-activation.test.mjs +2 -2
- package/packages/dsh-remote-web/test/picker-pin.test.mjs +1 -1
- package/packages/dsh-remote-web/test/plugin-manifest.test.mjs +2 -2
- package/packages/dsh-remote-web/test/quota-absent.test.mjs +4 -4
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -2
- package/packages/dsh-remote-web/test/selfheal-restart-guard.test.mjs +420 -0
- package/packages/dsh-remote-web/test/settings-entry-placement.test.mjs +6 -6
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +2 -2
- package/packages/dsh-remote-web/test/settings-open-path.test.mjs +4 -4
- package/packages/dsh-remote-web/test/setup-output.test.mjs +11 -11
- package/packages/dsh-remote-web/test/tarball-update.test.mjs +1 -1
- package/packages/dsh-remote-web/test/telemetry.test.mjs +4 -4
- package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +2 -2
- package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +1 -1
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +7 -7
- package/packages/dsh-remote-web/test/windows-compat.test.mjs +5 -5
- package/scripts/test-net-guard.cjs +1 -1
|
@@ -147,7 +147,7 @@ function unaryArgsProblem(method, args) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
150
|
+
* 一元方法的默认应答(真值约定),让 happy-path 用例不必逐个配桩。
|
|
151
151
|
* 需要精确控制的用例用 `api.setRpcFor()` 覆盖。
|
|
152
152
|
*/
|
|
153
153
|
function defaultRpcFor(method, body) {
|
|
@@ -331,7 +331,7 @@ async function startFakeDsh(options = {}) {
|
|
|
331
331
|
const stream = { streamId: msg.streamId, endpoint: msg.endpoint, payload: msg.payload, closed: false };
|
|
332
332
|
// 镜像真 gateway 的 payload 校验(dsh-api-gateway/lib/index.js:929):
|
|
333
333
|
// 非 $events 的流必须恰好带一个 plain-object 的 args 字段 —— 本地假服务端若放松这条,
|
|
334
|
-
//
|
|
334
|
+
// 就会漏掉真实线上会拒的形状(这个坑正是靠真机探测发现的)。
|
|
335
335
|
if (!isPlainArgsPayload(msg.payload)) {
|
|
336
336
|
api.fail(conn, msg.streamId, {
|
|
337
337
|
code: "gateway/internal",
|
|
@@ -815,7 +815,7 @@ test("follow 的会话 id:原样为主、alternative 兜底一次;agent-busy
|
|
|
815
815
|
assert.equal(toDurableSessionId("session-9b4f"), "session-9b4f");
|
|
816
816
|
|
|
817
817
|
const faults = [];
|
|
818
|
-
//
|
|
818
|
+
// 真机约定:子代理会话的真实 id 是裸 uuid,`session-<uuid>` 不存在(实测)。
|
|
819
819
|
// 这个假服务端只认「原样 id」,对另一种形式回 session/not-found。
|
|
820
820
|
const api = await startFakeDsh({
|
|
821
821
|
onOpen(server, conn, stream) {
|
|
@@ -847,7 +847,7 @@ test("follow 的会话 id:原样为主、alternative 兜底一次;agent-busy
|
|
|
847
847
|
sub.on("fault", (fault) => faults.push(fault));
|
|
848
848
|
await start(sub);
|
|
849
849
|
|
|
850
|
-
// 事件里报裸 uuid(= 真机 api-session/status
|
|
850
|
+
// 事件里报裸 uuid(= 真机 api-session/status 的约定)→ 主形式必须**原样**,不加前缀
|
|
851
851
|
const reportedId = "9b4f9e73-8912-49ca-98ba-23da9c34ab47";
|
|
852
852
|
globalThis.__acceptedId = `session-${reportedId}`; // 服务端只认带前缀的那种
|
|
853
853
|
api.pushEmit("api-session/status", [reportedId, true]);
|
|
@@ -1997,17 +1997,17 @@ test("一元:本地校验(空 text / 缺 sessionId / 坏 mode…)直接类
|
|
|
1997
1997
|
}
|
|
1998
1998
|
});
|
|
1999
1999
|
|
|
2000
|
-
// ═══════════════════ 2026-09-25
|
|
2000
|
+
// ═══════════════════ 2026-09-25 故障:完成推送静默消失(用户实测)═══════════════════
|
|
2001
2001
|
//
|
|
2002
2002
|
// 现场:任务跑完了,微信**一条推送都没有**,bridge 日志里也**没有任何报错**。
|
|
2003
|
-
//
|
|
2003
|
+
// 活体探测(以微信通道同款方式订阅本机 dsh web)显示:
|
|
2004
2004
|
// · 新起的订阅器 163ms 就发现并 follow 了正在跑的会话 —— 机制本身是好的;
|
|
2005
2005
|
// · 而当时在跑的 bridge 进程到 3080 只有 2 条 WS(mux + control)、**没有 follow 流**,
|
|
2006
2006
|
// 可它明明有一个正在跑长任务的会话。
|
|
2007
2007
|
// 根因就在下面这条:周期性发现被"当前一条 follow 都没有"这个条件**关掉了自己**——
|
|
2008
2008
|
// 丢一次 status:true 边沿就永久失去发现能力,而 turn/end 只在 follow 流上才有。
|
|
2009
2009
|
|
|
2010
|
-
test("★
|
|
2010
|
+
test("★ 回归锁:一条 follow 都没有时,周期性发现**仍然要跑**(丢一次边沿不能永久失聪)", async () => {
|
|
2011
2011
|
const api = await startFakeDsh({ listItems: [] }); // 连上时没有任何会话在跑
|
|
2012
2012
|
const sub = createEventSubscriber({
|
|
2013
2013
|
upstream: api.base,
|
|
@@ -2087,18 +2087,18 @@ test("★ 一次 follow 失败不该把这条会话**永久**钉死:下一轮
|
|
|
2087
2087
|
});
|
|
2088
2088
|
|
|
2089
2089
|
// ============================================================
|
|
2090
|
-
// 上游是**函数**:宿主换端口要能自己跟上(2026-10-09
|
|
2090
|
+
// 上游是**函数**:宿主换端口要能自己跟上(2026-10-09 真机故障回归)
|
|
2091
2091
|
// ============================================================
|
|
2092
2092
|
|
|
2093
2093
|
/**
|
|
2094
|
-
*
|
|
2094
|
+
* 【故障原貌】bridge 起来时宿主在 3080,随后宿主换端口(DSH Desktop 每次启动都是随机端口)。
|
|
2095
2095
|
* 隧道转发侧每次都读最新 UPSTREAM,所以一切正常;**唯独事件订阅器抱着启动那一刻的字符串**,
|
|
2096
2096
|
* 于是永久 `connect ECONNREFUSED 127.0.0.1:3080` —— 手机能开页面、面板显示 online,
|
|
2097
2097
|
* 但任务跑完**一条微信推送都没有**。迷惑性极强,最后靠 bridge 日志里刷屏的那一行才定位。
|
|
2098
2098
|
*
|
|
2099
2099
|
* 修法:`upstream` 允许传函数,每次连接/RPC 重新求值。这条用例就是那件事的锁。
|
|
2100
2100
|
*/
|
|
2101
|
-
test("★
|
|
2101
|
+
test("★ 回归锁:upstream 传函数时,宿主换端口后订阅器必须自己跟过去(不得永久打旧端口)", async () => {
|
|
2102
2102
|
const a = await startFakeDsh();
|
|
2103
2103
|
const b = await startFakeDsh();
|
|
2104
2104
|
let current = a.base;
|
|
@@ -310,7 +310,7 @@ test("开关与回退:enabled bridge 上报 caps + 状态文件;off/401 明文
|
|
|
310
310
|
assert.match(await r.text(), /⚠/);
|
|
311
311
|
});
|
|
312
312
|
|
|
313
|
-
test("E2EE 解锁握手(device 形态)+ channel
|
|
313
|
+
test("E2EE 解锁握手(device 形态)+ channel 形态可开,探测通过后会话可用", async () => {
|
|
314
314
|
const { mk, params } = await phonePrepare();
|
|
315
315
|
const { ctrl, client, sessId } = await unlock(`${routerBase}/remote/${DEV_ON}/_e2ee/ctrl`, { mk, params });
|
|
316
316
|
assert.equal(sessId.length, 32);
|
|
@@ -379,7 +379,7 @@ test("E2EE HTTP:信封请求解封→上游→封回往返一致;篡改密文
|
|
|
379
379
|
});
|
|
380
380
|
|
|
381
381
|
test("★ 0.6.15 三层 base64 降为两层:声明过的客户端收到二进制明文,协商头不转发上游", async () => {
|
|
382
|
-
//
|
|
382
|
+
// 用户实测(dev-1014f947f8ab,Ubuntu 上的 dsh + Windows 访问,首屏要等几分钟):
|
|
383
383
|
// 同一条响应被 base64 编了三层(1.333³≈2.37×)。第一层是"gzip 正文进 JSON",
|
|
384
384
|
// 现在由客户端声明 x-dsh-e2ee-want: bin 换成二进制框架;老客户端不声明 → 形态不变。
|
|
385
385
|
const u = globalThis.__unlocked;
|
|
@@ -492,7 +492,7 @@ test("E2EE WS 数据流:&e2ee=&w= 标记 → 桥剥除标记连上游,逐消息
|
|
|
492
492
|
// (而非测试内复制)跑真实解锁 + HTTP 信封 + WS 数据流,验证手机端与桥端互通。
|
|
493
493
|
// ============================================================
|
|
494
494
|
|
|
495
|
-
/** 手机端(浏览器 WebCrypto)解锁:hello→ack→deriveShk
|
|
495
|
+
/** 手机端(浏览器 WebCrypto)解锁:hello→ack→deriveShk→探测;密码错 → bad_key。 */
|
|
496
496
|
function wcUnlock(ctrlWsUrl, { mkBytes, params }) {
|
|
497
497
|
return new Promise((resolve, reject) => {
|
|
498
498
|
const sessId = wc.wcNewSessId();
|
|
@@ -649,7 +649,7 @@ test("Phase-4 镜像页 shim:注入+交接单重建+HTTP 信封/WS 真实互通;
|
|
|
649
649
|
const params = await fetchE2eeParams(apiBase, signJwt({ sub: "42", phone: "13811110001", plan: "free" }));
|
|
650
650
|
assert.equal(params.enabled, true);
|
|
651
651
|
const mk = new Uint8Array(deriveMasterKey(PASSWORD, params.salt, params.kdf));
|
|
652
|
-
// 手机端(浏览器 WebCrypto)解锁 →
|
|
652
|
+
// 手机端(浏览器 WebCrypto)解锁 → 已与桥端探测互认的会话
|
|
653
653
|
const { client, sessId } = await wcUnlock(`${routerBase}/remote/${DEV_ON}/_e2ee/ctrl`, { mkBytes: mk, params });
|
|
654
654
|
assert.equal(sessId.length, 32);
|
|
655
655
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - MK 派生向量与 enterprise 参考实现(e2ee.js deriveE2eeMasterKey)一致(固定口令/盐 → 固定 hex);
|
|
5
5
|
* - 信封加解密往返 / 篡改拒绝 / nonce 重放拒绝 / ws 计数重放拒绝;
|
|
6
6
|
* - 参数端点容错(非 200 / 无 e2ee / enabled=false → disabled 不回退抛错);
|
|
7
|
-
* - 桥端控制通道握手(hello→ack
|
|
7
|
+
* - 桥端控制通道握手(hello→ack、探测好/坏密钥)、http 明文编解码、ws 标签解析。
|
|
8
8
|
*
|
|
9
9
|
* 用法: node --test test/e2ee-client.test.mjs
|
|
10
10
|
*/
|
|
@@ -216,7 +216,7 @@ function flipB64(b64) {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
test("★ 0.6.15 二进制明文框架:正文不再 base64(省掉三层里的第一层),且旧 JSON 形态仍能解", () => {
|
|
219
|
-
//
|
|
219
|
+
// 用户实测"打开慢几分钟"的直接原因之一:同一条响应被 base64 编了三层(1.333³≈2.37×)。
|
|
220
220
|
// 第一层就是"gzip 正文塞进 JSON 明文"。二进制框架把它去掉,且必须**两种形态都能解**
|
|
221
221
|
// (老客户端只发旧形态,新客户端声明后收新形态)。
|
|
222
222
|
const body = Buffer.from("首屏聚合包".repeat(500), "utf8");
|
|
@@ -323,7 +323,7 @@ test("信封标记头:封包/识别/解析一致;content-type 与 x-dsh-e2ee 双
|
|
|
323
323
|
assert.equal(parseEnvelopeMarker({}), null);
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
-
// ---------- 桥端服务:握手 /
|
|
326
|
+
// ---------- 桥端服务:握手 / 探测 / 开关 ----------
|
|
327
327
|
|
|
328
328
|
test("E2eeService:disabled 状态 caps 为空;enabled 后 caps=['e2ee-v2']", () => {
|
|
329
329
|
const off = new E2eeService({ enabled: false, reason: "server_disabled" });
|
|
@@ -345,7 +345,7 @@ test("E2eeService.introGrant:启用时一次性下发派生 MK(32B b64url);禁
|
|
|
345
345
|
assert.equal(g.epoch, 3);
|
|
346
346
|
assert.equal(typeof g.mk, "string");
|
|
347
347
|
assert.equal(Buffer.from(g.mk, "base64url").length, 32);
|
|
348
|
-
assert.equal(Buffer.from(g.mk, "base64url").toString("hex"), mk.toString("hex"), "引导下发的 MK 应与桥端派生 MK 一致(
|
|
348
|
+
assert.equal(Buffer.from(g.mk, "base64url").toString("hex"), mk.toString("hex"), "引导下发的 MK 应与桥端派生 MK 一致(否则手机无法通过探测)");
|
|
349
349
|
});
|
|
350
350
|
|
|
351
351
|
test("E2eeService.init:服务端 enabled=false/401/无密码/禁用开关 → 明文回退原因", async () => {
|
|
@@ -387,7 +387,7 @@ test("握手:hello→ack(b 随机 32B);salt/profile 不符拒绝;disabled 服务
|
|
|
387
387
|
assert.throws(() => off.handleHello({ v: 2, type: "e2ee-hello", role: "phone", s: newSessId(), a: randomB64(32), salt: SALT_B64 }), (e) => e.code === "disabled");
|
|
388
388
|
});
|
|
389
389
|
|
|
390
|
-
test("
|
|
390
|
+
test("探测:正确 MK 通过并置 verified;错误 MK → bad_key;未解锁会话被拒", () => {
|
|
391
391
|
const mk = deriveMasterKey("unit-password", SALT_B64);
|
|
392
392
|
const svc = new E2eeService({ enabled: true, mk, salt: SALT_B64, profile: "pbkdf2-sha256-600k" });
|
|
393
393
|
const sessId = newSessId();
|
|
@@ -398,7 +398,7 @@ test("探针:正确 MK 通过并置 verified;错误 MK → bad_key;未解锁会
|
|
|
398
398
|
return ack.b;
|
|
399
399
|
})();
|
|
400
400
|
const client = new E2eeSession({ sessId, ...deriveShk(mk, aB64, bB64), profile: "pbkdf2-sha256-600k" });
|
|
401
|
-
//
|
|
401
|
+
// 探测(明文 {p,t,c})
|
|
402
402
|
const probe = client.seal({ kind: "ctrl", dir: "p2b", counter: 0, data: JSON.stringify({ p: "dsh-e2ee-probe-v1", t: Date.now(), c: 0 }) });
|
|
403
403
|
const { reply, session } = svc.handleProbe(sessId, probe);
|
|
404
404
|
assert.equal(reply.k, "ctrl");
|
|
@@ -407,7 +407,7 @@ test("探针:正确 MK 通过并置 verified;错误 MK → bad_key;未解锁会
|
|
|
407
407
|
// bridge 回 probe-ok → 手机可开
|
|
408
408
|
const okPlain = client.open({ kind: "ctrl", dir: "b2p", env: reply, counter: reply.c });
|
|
409
409
|
assert.equal(JSON.parse(okPlain.data.toString("utf8")).p, "dsh-e2ee-probe-ok");
|
|
410
|
-
//
|
|
410
|
+
// 重放同一探测 → 计数重放拒绝
|
|
411
411
|
assert.throws(() => svc.handleProbe(sessId, probe), (e) => e.code === "replay" || e.code === "bad_counter");
|
|
412
412
|
// 错误 MK(错误口令)的手机 → bad_key
|
|
413
413
|
const wrongMk = deriveMasterKey("wrong-password", SALT_B64);
|
|
@@ -566,7 +566,7 @@ test("fetch 包装:真实字节往返(shim 密封 → 桥端打开 → 封回
|
|
|
566
566
|
// ─────────────── 2026-09-27:P0-1 收尾(设备作用域必须在"没有会话"时也生效) ───────────────
|
|
567
567
|
|
|
568
568
|
test("★ WS 设备作用域:不改写就走放行分支时,也必须用**改写后**的 URL", () => {
|
|
569
|
-
//
|
|
569
|
+
// 故障现场:镜像页**没有会话**(未解锁)→ 不加密 → 走放行分支。
|
|
570
570
|
// 官方客户端把 mux 拼成根路径 `/api/remote.mux`,中继只认会过期的 dsh_device cookie → 404
|
|
571
571
|
// (rejectUpgrade 写状态行后立刻 destroy → nginx 只看到 009)→ e2ee-hello 送不到 →
|
|
572
572
|
// 会话永远建不起来 → 每个受保护请求 unknown_session 502。死循环。
|
|
@@ -123,7 +123,7 @@ test("信封逐字段一致:http 请求 / http 响应 / w(文本·二进制)/ ct
|
|
|
123
123
|
else assert.equal(Buffer.from(back.data).toString("utf8"), data);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// ctrl
|
|
126
|
+
// ctrl 探测
|
|
127
127
|
const probePlain = JSON.stringify({ p: "dsh-e2ee-probe-v1", t: 1700000000000, c: 0 });
|
|
128
128
|
const envCN = nodeS.seal({ kind: "ctrl", dir: "p2b", counter: 0, data: probePlain, nonce: NONCE_12() });
|
|
129
129
|
const envCW = await wcS.seal({ kind: "ctrl", dir: "p2b", counter: 0, data: probePlain, nonceBytes: NONCE_12() });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Cookie **按 authority 隔离**回归(2026-10-09
|
|
3
|
+
* Cookie **按 authority 隔离**回归(2026-10-09 故障根治)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 【故障原貌】`~/.dsh-remote/` 只有一份 `.harness-cookie.json`,而 Web 端(3080)与桌面端
|
|
6
6
|
* (每次启动随机端口)是**两个宿主进程**,各自的插件都往里写自己 authority 的 Cookie →
|
|
7
7
|
* 互相覆盖。桥撞 401 后重读磁盘拿到的是**另一个宿主刚写的** Cookie(与失败的不是同一个)
|
|
8
8
|
* → 重试 → 还是 401 → 再读 → 又被覆盖回去……**乒乓,永不自愈**,手机端持续白页。
|
|
@@ -69,9 +69,9 @@ test("本宿主专属文件优先:`.harness-cookie.<authority>.json` 收到的
|
|
|
69
69
|
assert.equal(r.cookie, "dsh-auth-OWN");
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
test("★
|
|
72
|
+
test("★ 回归锁:旧版共享文件里是**另一个宿主**的 Cookie → 一个字节都不许发出去", async () => {
|
|
73
73
|
clearCookies();
|
|
74
|
-
//
|
|
74
|
+
// 这正是故障现场:共享文件被另一宿主(3080)覆盖过
|
|
75
75
|
writeDoc(LEGACY, { authority: "127.0.0.1:3080", cookie: "dsh-auth-FOREIGN", mintedAt: Date.now() });
|
|
76
76
|
const r = await ask();
|
|
77
77
|
assert.equal(r.status, 200);
|
|
@@ -113,8 +113,8 @@ test("机器指纹:darwin/linux 两条老分支没有被改坏", () => {
|
|
|
113
113
|
assert.equal(linux.fn(), "0123456789abcdef", "linux 仍读 /etc/machine-id(并去掉换行)");
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
test("★回归:子进程一律用绝对路径优先,不赌 PATH(launchd 的 PATH 不含 /usr/sbin
|
|
117
|
-
//
|
|
116
|
+
test("★回归:子进程一律用绝对路径优先,不赌 PATH(launchd 的 PATH 不含 /usr/sbin 的真实故障)", () => {
|
|
117
|
+
// 故障(2026-09-22 真机日志):bridge 由 launchd 拉起,plist 给的 PATH 是
|
|
118
118
|
// /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin —— 没有 /usr/sbin,而 ioreg 在那里。
|
|
119
119
|
// 日志里出现 `/bin/sh: ioreg: command not found`,指纹**静默**退化成 hostname 哈希:
|
|
120
120
|
// 用户改一次主机名就被服务端当成新设备。这类"PATH 假设"必须用绝对路径堵死。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* mobile-adapter
|
|
3
|
+
* mobile-adapter 手机端两个真实故障的回归测试(假 DOM + 事件传播模型 + vm 跑真实注入脚本)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 故障 1(2026-09-19,微信内置浏览器实测反馈)
|
|
6
6
|
* 「用微信语音转文字输入时,它在自动整理文字的过程中会直接触发输入框的发送」——半截话被发出。
|
|
7
7
|
* 取证:官方输入区是 Lexical contenteditable(根元素带 data-composer-input / role=textbox /
|
|
8
8
|
* aria-multiline / data-lexical-editor),官方键位图自带的 IME 守卫只看
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* (isComposing / keyCode 229 / compositionend 后 60ms 内)一律 stopPropagation;
|
|
14
14
|
* ② ≤820px 时输入框里的 Enter **只换行不发送**,发送只由官方发送按钮负责。
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* 故障 2(2026-09-19,Windows 电脑通道实测反馈)
|
|
17
17
|
* 「展开左边抽屉,无法选择历史会话,点任何东西都没有反应;界面右边有一个白色的框占了点位置」。
|
|
18
18
|
* 根因:`isDrawer()` 只查 `r.right > 8`,而**向右滑出屏外的第三列**右边坐标是很大的正数
|
|
19
19
|
* (真机实测 left=400 / right=759 / width=358)→ 完全看不见的详情列被判成"展开的详情列" →
|
|
@@ -915,7 +915,7 @@ test("★ 迭代速度护栏:适配层测试的假定时器**必须 unref**(
|
|
|
915
915
|
});
|
|
916
916
|
|
|
917
917
|
test("★ 护栏:解析期绝不许往页面塞东西(0.6.14 首屏提示层就是这么做、然后永久盖住 UI 的)", () => {
|
|
918
|
-
//
|
|
918
|
+
// 故障 3(2026-09-25,用户实测):「界面都已经加载完了…但这个遮罩一直不消失」,
|
|
919
919
|
// 截图里官方 UI 就在 88% 不透明的全屏层下面透出来,秒表走到 187 秒 —— 刷新也一样。
|
|
920
920
|
//
|
|
921
921
|
// 根因是**设计错误**:那个提示层是 `position:fixed; inset:0; z-index:2147483000` 的全屏层,
|
|
@@ -923,7 +923,7 @@ test("★ 护栏:解析期绝不许往页面塞东西(0.6.14 首屏提示层
|
|
|
923
923
|
// 两组信号全不命中 → 层永远留着。当时我写的"pointer-events:none 所以不会挡点击"是自我安慰:
|
|
924
924
|
// **盖住视线和进不去对用户没区别**。
|
|
925
925
|
//
|
|
926
|
-
//
|
|
926
|
+
// 用户决定:直接删掉。这条护栏守两件事:
|
|
927
927
|
// ① `dsh-ma-boot` 不许回来;
|
|
928
928
|
// ② **解析期**(主机门之前)不许有任何 `appendChild` —— 页面还没确认是官方 dsh web,
|
|
929
929
|
// 更不该有任何覆盖层;所有注入节点都必须在 boot() 里、过了主机门之后才做。
|
|
@@ -1058,7 +1058,7 @@ test("★ 官方右栏里有内容时:适配层**不插手**(交还官方 op
|
|
|
1058
1058
|
assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false, "★ 更不得为它开遮罩");
|
|
1059
1059
|
});
|
|
1060
1060
|
|
|
1061
|
-
test("★ 2026-09-19
|
|
1061
|
+
test("★ 2026-09-19 白框故障的**结构性**回归锁:适配层任何情况下都不滑出第三列", () => {
|
|
1062
1062
|
const { js } = injected();
|
|
1063
1063
|
// 当年白框的三种触发现场各来一遍,断言"我们什么都不做" ⇒ 白框在结构上不可能再出现
|
|
1064
1064
|
for (const opts of [
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* mobile-adapter **图片上传兼容**契约测试(假 DOM + vm 执行真实注入脚本)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 用户反馈(2026-09):「手机端无法识别图片」——选了照片发出去,模型看不到图 / 被当成普通文件。
|
|
6
6
|
*
|
|
7
7
|
* 根因(官方 dsh web 客户端 `dsh-client-ui-conversation` 的 createDrafts):
|
|
8
8
|
* 只有 `file.type ∈ {image/png,image/jpeg,image/webp,image/gif}` 才走**图片**通道
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* mobile-adapter **运行时**契约测试(假 DOM + vm 执行真实注入脚本)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 故障背景(2026-09-15 生产反馈):
|
|
6
6
|
* 手机端用户打开后**整屏被阴影覆盖、点哪都没反应**,只有左侧栏(z-index 300,在遮罩之上)能点。
|
|
7
7
|
* 根因是适配层自己创建的全屏 scrim(div.dsh-ma-scrim, z-index 290):
|
|
8
8
|
* · 判定「侧栏是否展开」用的是 `!frame.hasAttribute("data-sidebar-collapsed")`,
|
|
@@ -274,7 +274,7 @@ test("运行时:判定为展开时不再隐藏汉堡按钮(避免用户没
|
|
|
274
274
|
const ham = findHamburger(env);
|
|
275
275
|
assert.ok(ham, "汉堡按钮应存在");
|
|
276
276
|
assert.notEqual(ham.style.display, "none",
|
|
277
|
-
"抽屉展开时也不应把汉堡隐藏 ——
|
|
277
|
+
"抽屉展开时也不应把汉堡隐藏 —— 判定一旦出错,用户会连入口都没有(故障现场就是这个死局)");
|
|
278
278
|
});
|
|
279
279
|
|
|
280
280
|
test("运行时:找不到官方 toggle 时,点遮罩必须能自救(摘掉遮挡类)", () => {
|
|
@@ -537,11 +537,11 @@ test("网络延迟分档:500ms 以内=顺畅、1000ms 以内=正常(远程
|
|
|
537
537
|
});
|
|
538
538
|
|
|
539
539
|
test("★ 亚毫秒样本(本机/回环自测)也必须出数 —— 不得永远停在「测速中…」", async () => {
|
|
540
|
-
//
|
|
540
|
+
// 用户 2026-10-09 实测反馈:「小悬浮窗一直提示『测速中,等待下一次网络请求后开始测速』」。
|
|
541
541
|
// 根因:旧实现是 `if (r < 1) return;`(注释写"亚毫秒=同帧缓存命中"),但**本机/回环**访问
|
|
542
542
|
// (桌面端自测、在同一台机器上打开镜像页)一次真实往返往往就是 < 1ms —— 于是**每一个**样本
|
|
543
543
|
// 都被丢掉,界面永远停在空态。面板侧 chip 从来没这个毛病(`v <= 0` + `Math.max(1, …)`),
|
|
544
|
-
//
|
|
544
|
+
// 这条用例把两侧约定钉在一起。
|
|
545
545
|
const env = harness({ width: 390, fetchImpl: async () => ({ status: 200, ok: true }) }); // 零延迟 = 亚毫秒
|
|
546
546
|
runAdapter(env);
|
|
547
547
|
const rtt = findFab(env).querySelector('[data-role="rtt"]');
|
|
@@ -580,7 +580,7 @@ test("★ 悬浮窗被官方 SPA 重渲染重建后,已积累的样本仍要
|
|
|
580
580
|
});
|
|
581
581
|
|
|
582
582
|
test("★ 采样器由 boot 安装(不依赖菜单)+ 空态说得清原因 + 被顶掉能自愈重挂", async () => {
|
|
583
|
-
// 2026-10-10
|
|
583
|
+
// 2026-10-10 二修:用户第二次反馈「镜像页一直测速中」,而同一时段镜像页打了 69 次
|
|
584
584
|
// /api/agentPresets/list 这类**完全合格**的请求 —— 说明真根因是**采样没接上**
|
|
585
585
|
// (旧实现把包装写在 buildRemoteMenu 内部的一次性分支里),而不是样本被过滤。
|
|
586
586
|
const env = harness({
|