@mrrisega/dsh-remote 0.7.12-beta.1 → 0.7.12-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/dingtalk-bot.mjs +1318 -0
- package/clients/dsh-remote/docs/feishu-bot-contract.md +177 -0
- package/clients/dsh-remote/dsh-bridge.mjs +398 -86
- package/clients/dsh-remote/feishu-bot.mjs +1668 -0
- package/clients/dsh-remote/mirror-diag-script.js +53 -14
- package/clients/dsh-remote/mobile-adapter.mjs +355 -40
- package/clients/dsh-remote/test/bridge-http-abort.test.mjs +143 -0
- package/clients/dsh-remote/test/bridge-slow-response.test.mjs +107 -0
- package/clients/dsh-remote/test/bridge-tunnel-receiver.test.mjs +135 -0
- package/clients/dsh-remote/test/dingtalk-bot.test.mjs +550 -0
- package/clients/dsh-remote/test/feishu-bot.test.mjs +839 -0
- package/clients/dsh-remote/test/mirror-diag.test.mjs +47 -2
- package/clients/dsh-remote/test/mobile-adapter-load-watch.test.mjs +208 -9
- package/dsh-setup.mjs +1 -1
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +332 -4
- package/packages/dsh-remote-web/lib/index.js +367 -21
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +7 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/im-format.mjs +372 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/upstream-discovery.mjs +495 -43
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wecom-bot.mjs +238 -12
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +77 -6
- package/packages/dsh-remote-web/test/im-bots.test.mjs +224 -2
- package/packages/dsh-remote-web/test/panel-ia.test.mjs +197 -32
|
@@ -152,6 +152,51 @@ test("适配层不在场(旧版本/宽屏/未注入)时:payload 形状不
|
|
|
152
152
|
const [payload] = env.payloads();
|
|
153
153
|
assert.ok(payload, "有失败资源时必须上报");
|
|
154
154
|
assert.equal("ui" in payload, false, "没有适配层诊断时不得新增字段(老服务端/老页面兼容)");
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// v19(2026-11-08)新增 b(批序号)/page_id(本页随机串):用于把同一页的多批上报关联起来。
|
|
156
|
+
// 两者都是**可选**字段,老服务端无视即可;除它们之外不得再新增字段。
|
|
157
|
+
assert.deepEqual(Object.keys(payload).sort(), ["at", "b", "boot_failed", "items", "kind", "page", "page_id", "ua", "v"],
|
|
158
|
+
"既有 payload 字段一个不多一个不少(v19 只允许新增 b/page_id)");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
163
|
+
// v19(2026-11-08):追加批 —— 同一页最多 3 批,避免"重试后的失败明细"丢掉
|
|
164
|
+
//
|
|
165
|
+
// 现场(dev-62ca4fd91b0b):首屏资源失败 4 项 + 自动重试 2 次,服务端只拿到第一批,
|
|
166
|
+
// 后续重试到底哪个 URL 还在失败无从判断。采集器改为按批上报(批间 ≥3s、每批 ≤20 项),
|
|
167
|
+
// 页内最多 3 批;成功页仍然**一个字节都不发**。
|
|
168
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
169
|
+
|
|
170
|
+
test("★ v19 追加批:同一页最多 3 批(b=1..3、page_id 一致、条目按批切分),第 4 批不再发", () => {
|
|
171
|
+
const env = runDiagScript();
|
|
172
|
+
// 第 1 批:3 条首屏失败
|
|
173
|
+
for (const u of ["/plugins/??a.js", "/assets/index.js", "/assets/vendor-x.js"]) {
|
|
174
|
+
env.fire("error", { target: { src: "https://n.risegao.cn:13443" + u, tagName: "SCRIPT" } });
|
|
175
|
+
}
|
|
176
|
+
env.runTimers(); // 走完 2500ms flush 延迟 → 第 1 批
|
|
177
|
+
// 第 2 批:自动重试后又失败 2 条(pagehide 兜底:跳过 3s 批间隔,模拟用户切走页面)
|
|
178
|
+
env.fire("error", { target: { src: "https://n.risegao.cn:13443/plugins/??a.js", tagName: "SCRIPT" } });
|
|
179
|
+
env.fire("error", { target: { src: "https://n.risegao.cn:13443/assets/index.js", tagName: "SCRIPT" } });
|
|
180
|
+
env.fire("pagehide", {});
|
|
181
|
+
// 第 3 批:再来 1 条
|
|
182
|
+
env.fire("error", { target: { src: "https://n.risegao.cn:13443/plugins/??a.js", tagName: "SCRIPT" } });
|
|
183
|
+
env.fire("pagehide", {});
|
|
184
|
+
// 第 4 条以后:超过 MAX_FLUSHES=3 → 一个字节都不再发
|
|
185
|
+
env.fire("error", { target: { src: "https://n.risegao.cn:13443/plugins/??a.js", tagName: "SCRIPT" } });
|
|
186
|
+
env.fire("pagehide", {});
|
|
187
|
+
|
|
188
|
+
const payloads = env.payloads();
|
|
189
|
+
assert.equal(payloads.length, 3, "同一页最多 3 批(第 4 批必须被丢弃)");
|
|
190
|
+
assert.deepEqual(payloads.map((p) => p.b), [1, 2, 3], "批序号必须递增且从 1 起");
|
|
191
|
+
assert.deepEqual(payloads.map((p) => p.items.length), [3, 2, 1], "每批只带尚未发出的条目");
|
|
192
|
+
const pageIds = new Set(payloads.map((p) => p.page_id));
|
|
193
|
+
assert.equal(pageIds.size, 1, "同一页的多批必须带同一个 page_id(服务端据此关联)");
|
|
194
|
+
assert.ok([...pageIds][0] && [...pageIds][0].length <= 64, "page_id 必须是非空、≤64 的随机串");
|
|
195
|
+
// 每批 ≤20 项(服务端上限)
|
|
196
|
+
for (const p of payloads) assert.ok(p.items.length <= 20, "单批不得超过服务端 20 项上限");
|
|
197
|
+
// 成功页零上报:没有任何失败项时,定时器/卸载都不发请求
|
|
198
|
+
const clean = runDiagScript();
|
|
199
|
+
clean.runTimers();
|
|
200
|
+
clean.fire("pagehide", {});
|
|
201
|
+
assert.equal(clean.payloads().length, 0, "成功页必须一个字节都不发(零流量)");
|
|
157
202
|
});
|
|
@@ -144,9 +144,25 @@ function harness(opts = {}) {
|
|
|
144
144
|
const fetches = [];
|
|
145
145
|
const signals = []; // 被包装的 fetch 收到的 init.signal(C4:在途请求取消的断言入口)
|
|
146
146
|
const winListeners = new Map();// window 上登记的事件监听(C4:资源 error 捕获靠它)
|
|
147
|
+
const docListeners = new Map();// document 上登记的事件监听(W3:visibilitychange)
|
|
147
148
|
let timerSeq = 0;
|
|
148
149
|
let intervalSeq = 0;
|
|
149
150
|
const clicks = { reload: 0, assign: [], clipboard: [] };
|
|
151
|
+
/* W3:可控时钟(模拟 iOS 冻结/恢复必须能推进时间)。
|
|
152
|
+
· wall = Date.now()(startedAt / hiddenMs 用它);
|
|
153
|
+
· perf = performance.now()(起点≈导航开始;perfOffsetMs 模拟"注入点落后导航开始"的 TTFB)。
|
|
154
|
+
默认固定基准值,保证用例可重复;advance() 同时推进两套时钟(挂起也是墙钟推进)。 */
|
|
155
|
+
const clock = {
|
|
156
|
+
wall: Number.isFinite(opts.wallNow) ? Number(opts.wallNow) : 1_700_000_000_000,
|
|
157
|
+
perf: Number.isFinite(opts.perfOffsetMs) ? Number(opts.perfOffsetMs) : 0,
|
|
158
|
+
};
|
|
159
|
+
const advance = (ms) => { const d = Math.max(0, Number(ms) || 0); clock.wall += d; clock.perf += d; return d; };
|
|
160
|
+
/* 注入体只用到 Date.now()(已 grep 确认无 new Date());仍给一个真 Date 的派生类兜底。 */
|
|
161
|
+
const RealDate = Date;
|
|
162
|
+
class FakeDate extends RealDate {
|
|
163
|
+
constructor(...args) { if (args.length === 0) super(clock.wall); else super(...args); }
|
|
164
|
+
static now() { return clock.wall; }
|
|
165
|
+
}
|
|
150
166
|
|
|
151
167
|
const html = new El("html");
|
|
152
168
|
/* hostFeature=true 模拟"官方文档已到、但界面还没挂载"的现场(HOSTISH 会读 outerHTML 找官方特征)。
|
|
@@ -174,7 +190,8 @@ function harness(opts = {}) {
|
|
|
174
190
|
const docQS = El.prototype.querySelector.bind(doc);
|
|
175
191
|
doc.querySelector = (sel) => (opts.officialReady ? new El("div") : docQS(sel));
|
|
176
192
|
doc.querySelectorAll = () => [];
|
|
177
|
-
doc.
|
|
193
|
+
doc.visibilityState = "visible";
|
|
194
|
+
doc.addEventListener = (type, fn) => { const arr = docListeners.get(type) || []; arr.push(fn); docListeners.set(type, arr); };
|
|
178
195
|
doc.removeEventListener = () => {};
|
|
179
196
|
|
|
180
197
|
const loc = {
|
|
@@ -221,8 +238,8 @@ function harness(opts = {}) {
|
|
|
221
238
|
MutationObserver: win.MutationObserver, ResizeObserver: win.ResizeObserver,
|
|
222
239
|
requestAnimationFrame: win.requestAnimationFrame,
|
|
223
240
|
AbortController, AbortSignal,
|
|
224
|
-
fetch: win.fetch, performance: { now: () =>
|
|
225
|
-
URL, Date, Math, JSON, Object, Array, String, Number, RegExp, Boolean, Promise, Error, isNaN, parseInt, parseFloat,
|
|
241
|
+
fetch: win.fetch, performance: { now: () => clock.perf },
|
|
242
|
+
URL, Date: FakeDate, Math, JSON, Object, Array, String, Number, RegExp, Boolean, Promise, Error, isNaN, parseInt, parseFloat,
|
|
226
243
|
HTMLElement: El,
|
|
227
244
|
});
|
|
228
245
|
ctx.globalThis = ctx;
|
|
@@ -234,7 +251,14 @@ function harness(opts = {}) {
|
|
|
234
251
|
const findWatchTimer = (ms) => timersByMs(ms)[0] || null;
|
|
235
252
|
return {
|
|
236
253
|
ctx, win, doc, body, timers, intervals, fetches, clicks, navigator, signals, store,
|
|
237
|
-
findBar, barBtn, findWatchTimer,
|
|
254
|
+
findBar, barBtn, findWatchTimer, clock, advance,
|
|
255
|
+
/** 触发 document 上的事件(W3:visibilitychange)。 */
|
|
256
|
+
fireDocumentEvent(type, ev) {
|
|
257
|
+
const arr = docListeners.get(type) || [];
|
|
258
|
+
for (const fn of arr) fn(ev);
|
|
259
|
+
},
|
|
260
|
+
/** 模拟一次挂起(墙钟/单调钟一起前进,定时器不跑 = iOS 冻结语义)。 */
|
|
261
|
+
suspend(ms) { advance(ms); return ms; },
|
|
238
262
|
countTimers: (ms) => timersByMs(ms).length,
|
|
239
263
|
/** 触发 window 上的事件(C4:资源 error 在**捕获阶段**被适配层接住)。 */
|
|
240
264
|
fireWindowEvent(type, ev) {
|
|
@@ -243,18 +267,31 @@ function harness(opts = {}) {
|
|
|
243
267
|
},
|
|
244
268
|
/** 触发**所有**同毫秒的 setTimeout(页面里同毫秒的定时器不止一个时用它)。 */
|
|
245
269
|
fireTimers(ms) {
|
|
270
|
+
advance(ms);
|
|
246
271
|
const list = timersByMs(ms);
|
|
247
272
|
assert.ok(list.length > 0, `应登记了 ${ms}ms 的定时器`);
|
|
248
273
|
for (const t of list) t.fn();
|
|
249
274
|
},
|
|
250
|
-
/** 触发某个已登记的 setTimeout
|
|
275
|
+
/** 触发某个已登记的 setTimeout 回调(用例手工推进时间轴;W3 起同时推进可控时钟)。 */
|
|
251
276
|
fireTimer(ms) {
|
|
277
|
+
advance(ms);
|
|
278
|
+
const t = findWatchTimer(ms);
|
|
279
|
+
assert.ok(t, `应登记了 ${ms}ms 的定时器`);
|
|
280
|
+
t.fn();
|
|
281
|
+
},
|
|
282
|
+
/** 触发某个已登记的 setTimeout,但**不推进**时钟(W3:精确构造"活跃时间刚好到 20s"的现场)。 */
|
|
283
|
+
fireTimerOnly(ms) {
|
|
252
284
|
const t = findWatchTimer(ms);
|
|
253
285
|
assert.ok(t, `应登记了 ${ms}ms 的定时器`);
|
|
254
286
|
t.fn();
|
|
255
287
|
},
|
|
256
|
-
/** 触发所有 setInterval(秒数刷新 /
|
|
257
|
-
tickIntervals(
|
|
288
|
+
/** 触发所有 setInterval(秒数刷新 / 自适应兜底);每跳推进 1s。 */
|
|
289
|
+
tickIntervals(seconds = 1) {
|
|
290
|
+
for (let i = 0; i < seconds; i++) {
|
|
291
|
+
advance(1000);
|
|
292
|
+
for (const fn of [...intervals.values()]) fn();
|
|
293
|
+
}
|
|
294
|
+
},
|
|
258
295
|
};
|
|
259
296
|
}
|
|
260
297
|
|
|
@@ -456,7 +493,12 @@ test("slow 时照旧上报 mirror_load 兜底样本:字段 ms/outcome/at 不
|
|
|
456
493
|
const posts = env.fetches.filter((f) => String(f.input).includes("/dsh-remote/telemetry/mirror-load"));
|
|
457
494
|
assert.equal(posts.length, 1, "slow 兜底样本每次页面只发一条(reported 一次语义)");
|
|
458
495
|
const body = JSON.parse(posts[0].init.body);
|
|
459
|
-
|
|
496
|
+
/* W3:上报体在 ms/outcome/at 之外**只增** active_ms / hidden_ms(服务端未升级时会静默丢弃,
|
|
497
|
+
不破坏旧契约);outcome 仍只允许 ok|slow(插件半会把非 slow 归一成 ok)。 */
|
|
498
|
+
assert.deepEqual(Object.keys(body).sort(), ["active_ms", "at", "hidden_ms", "ms", "outcome"],
|
|
499
|
+
"字段 = ms/outcome/at + active_ms/hidden_ms");
|
|
500
|
+
assert.equal(typeof body.active_ms, "number", "active_ms 必须是数字");
|
|
501
|
+
assert.equal(typeof body.hidden_ms, "number", "hidden_ms 必须是数字");
|
|
460
502
|
assert.equal(body.outcome, "slow", "看门狗兜底样本的 outcome 必须是 slow");
|
|
461
503
|
assert.ok(Number.isFinite(body.ms) && body.ms >= 0 && body.ms <= 600000, "ms 必须被钳制在 [0,600000]");
|
|
462
504
|
// 再触发一次:reported 已置位 → 不得重复上报
|
|
@@ -574,9 +616,13 @@ test("★ 文案分档:受保护 API 5xx(电脑端不可达)→「电脑
|
|
|
574
616
|
hostFeature: true,
|
|
575
617
|
fetch: () => Promise.resolve({ ok: false, status: 502, headers: { get: () => null } }),
|
|
576
618
|
}));
|
|
619
|
+
/* W3:10s 窗口按**活跃时间**算 —— 先把时钟推到活跃 19s,再制造 5xx,然后 tick 到 20s,
|
|
620
|
+
这样证据确实在"最近 10 秒活跃时间"内(旧 harness 不推进时间,这条断言是假成立的)。 */
|
|
621
|
+
env.advance(19000);
|
|
577
622
|
env.win.fetch("/api/session/list");
|
|
578
623
|
await new Promise((r) => setImmediate(r));
|
|
579
|
-
env.
|
|
624
|
+
env.advance(1000);
|
|
625
|
+
env.fireTimerOnly(20000);
|
|
580
626
|
assert.match(env.findBar().querySelector('[data-role="loadbar-txt"]').textContent,
|
|
581
627
|
/电脑端可能未连接(已等待 \d+ 秒)/, "5xx = 电脑端/隧道不可达,这时说「电脑端可能未连接」才是对的");
|
|
582
628
|
});
|
|
@@ -611,3 +657,156 @@ test("诊断出口:__dshMaDiag / __dshMaRetryStats 暴露 retry / scrim / comp
|
|
|
611
657
|
assert.equal(env.win.__dshMaDiag.compatOfficial.on, false, "锚点齐全的页面上不得误进 compat-official");
|
|
612
658
|
assert.equal(env.win.__dshMaRetryStats, env.win.__dshMaDiag.retry, "mirror-diag 直接读 __dshMaRetryStats");
|
|
613
659
|
});
|
|
660
|
+
|
|
661
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
662
|
+
// ⑧ W3(2026-11-08):active-time 口径 / 恢复即补偿 / 诊断串
|
|
663
|
+
//
|
|
664
|
+
// 现场:页面注入后活跃加载约 15s → 被 iOS 冻结 518s(零请求、定时器不跑)→ 恢复瞬间
|
|
665
|
+
// 20s 看门狗才执行,旧实现把墙钟 541.664s 当成加载耗时、提示条显示"已等待 533 秒"。
|
|
666
|
+
// 行为契约:冻结恢复后 ms 仍按 performance.now(541664),但 active_ms/hidden_ms 分开算,
|
|
667
|
+
// 提示条/诊断串的"等待"只用活跃时间;恢复要补判就绪 + 补扫启动失败 + 限量补偿 <link>。
|
|
668
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
669
|
+
|
|
670
|
+
test("★ W3 冻结 518s 后恢复:上报 ms=541664/slow、active≈23664、hidden=518000,复制行含「挂起 518 秒」", async () => {
|
|
671
|
+
/* perfOffsetMs=8664 模拟"注入脚本落后导航开始 8.664s"(本例 nginx HTML 上游 9.25s 的一部分),
|
|
672
|
+
于是同一个瞬间:performance.now()=541664,而 Date.now()-startedAt(墙钟等待)=533000。 */
|
|
673
|
+
const env = runScript(harness({ hostFeature: true, perfOffsetMs: 8664 }));
|
|
674
|
+
env.advance(15000); // ① 活跃加载 15s
|
|
675
|
+
env.doc.visibilityState = "hidden";
|
|
676
|
+
env.fireDocumentEvent("visibilitychange"); // ② 切走/锁屏(记录 hiddenAt)
|
|
677
|
+
env.suspend(518000); // ③ 冻结 518s:定时器一个都不跑
|
|
678
|
+
env.doc.visibilityState = "visible";
|
|
679
|
+
env.fireDocumentEvent("visibilitychange"); // ④ 恢复(结算隐藏时长 + 恢复补偿钩子)
|
|
680
|
+
env.fireWindowEvent("pageshow", { persisted: true }); // 同一次恢复的第二条路(事件顺序不确定)
|
|
681
|
+
await new Promise((r) => setImmediate(r));
|
|
682
|
+
|
|
683
|
+
const posts = env.fetches.filter((f) => String(f.input).includes("/dsh-remote/telemetry/mirror-load"));
|
|
684
|
+
assert.equal(posts.length, 1, "整页只上报一次(reported 一次性语义不变)");
|
|
685
|
+
const body = JSON.parse(posts[0].init.body);
|
|
686
|
+
assert.equal(body.outcome, "slow", "outcome 仍只允许 ok|slow");
|
|
687
|
+
assert.equal(body.ms, 541664, "ms = performance.now()(导航开始 → 恢复瞬间)");
|
|
688
|
+
assert.equal(body.hidden_ms, 518000, "hidden_ms = 挂起时长");
|
|
689
|
+
assert.ok(Math.abs(body.active_ms - 23664) <= 1000,
|
|
690
|
+
`active_ms ≈ 23664(541664-518000),实际 ${body.active_ms}`);
|
|
691
|
+
|
|
692
|
+
// 提示条/复制句只用活跃时间,并如实标注挂起
|
|
693
|
+
const barTxt = env.findBar().querySelector('[data-role="loadbar-txt"]').textContent;
|
|
694
|
+
assert.match(barTxt, /挂起 518 秒/, "提示条要说明「期间被系统挂起 518 秒」");
|
|
695
|
+
env.barBtn("loadbar-copy").click();
|
|
696
|
+
await new Promise((r) => setImmediate(r));
|
|
697
|
+
const line = env.clicks.clipboard[0];
|
|
698
|
+
assert.match(line, /等待 (23|24) 秒/, "等待秒数按活跃时间,不是墙钟 533 秒");
|
|
699
|
+
assert.match(line, /挂起 518 秒/);
|
|
700
|
+
assert.ok(line.length <= 400, "诊断串单行 ≤400 字符");
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test("★ W3 恢复补偿:只重建 <link>、每页 ≤2 次,绝不重建 <script>(防插件重复执行)", () => {
|
|
704
|
+
const env = runScript(harness());
|
|
705
|
+
const fireFail = (el) => env.fireWindowEvent("error", { target: el });
|
|
706
|
+
// 3 个 link + 2 个 script 失败(都算首屏关键路径)
|
|
707
|
+
const links = ["/plugins/??@a/x.js&rev=1", "/assets/a.css", "/assets/b.css"].map((u) => {
|
|
708
|
+
const el = new El("link"); el.href = u; env.body.appendChild(el); fireFail(el); return el;
|
|
709
|
+
});
|
|
710
|
+
const scripts = ["/assets/a.js", "/assets/b.js"].map((u) => {
|
|
711
|
+
const el = new El("script"); el.src = u; el.setAttribute("type", "module");
|
|
712
|
+
env.body.appendChild(el); fireFail(el); return el;
|
|
713
|
+
});
|
|
714
|
+
assert.equal(links.length + scripts.length, 5, "前提:5 次关键资源失败已登记");
|
|
715
|
+
|
|
716
|
+
const hides = (ms) => { env.doc.visibilityState = "hidden"; env.fireDocumentEvent("visibilitychange"); env.suspend(ms); env.doc.visibilityState = "visible"; env.fireDocumentEvent("visibilitychange"); };
|
|
717
|
+
hides(5000);
|
|
718
|
+
const retried = () => env.body.children.filter((c) => c.attrs && c.attrs["data-dsh-ma-retry"] === "1");
|
|
719
|
+
assert.equal(retried().length, 2, "首次恢复最多补偿 2 个");
|
|
720
|
+
assert.ok(retried().every((c) => c.tagName === "LINK"), "★ 只重建 <link>,不重建 <script>");
|
|
721
|
+
hides(5000);
|
|
722
|
+
assert.equal(retried().length, 2, "每页上限 2 次:再次恢复不得继续重建");
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("★ W3 恢复后已就绪:提示条收起、不再补报 ok(reported 一次性)", async () => {
|
|
726
|
+
const env = runScript(harness());
|
|
727
|
+
env.fireTimer(20000); // 先走完 20s:slow 已上报 + 提示条已出现
|
|
728
|
+
const posts = () => env.fetches.filter((f) => String(f.input).includes("/dsh-remote/telemetry/mirror-load"));
|
|
729
|
+
assert.equal(posts().length, 1, "前提:slow 样本已上报一次");
|
|
730
|
+
// 恢复时官方界面其实已经好了(模拟"冻结期间资源传完、回来才渲染出来")
|
|
731
|
+
env.doc.querySelector = (sel) => (/pI_x6G_frame|ma-frame|sidebar-collapsed|details-collapsed|rightbar-collapsed|composer-input|contenteditable|role="textbox"|textarea/.test(String(sel)) ? new El("div") : null);
|
|
732
|
+
env.doc.visibilityState = "hidden"; env.fireDocumentEvent("visibilitychange");
|
|
733
|
+
env.suspend(600000);
|
|
734
|
+
env.doc.visibilityState = "visible"; env.fireDocumentEvent("visibilitychange");
|
|
735
|
+
await new Promise((r) => setImmediate(r));
|
|
736
|
+
assert.equal(env.findBar().hidden, true, "认出官方界面后提示条必须收起");
|
|
737
|
+
assert.equal(posts().length, 1, "★ 不得补报第二条(ok)——reported 一次性语义不变");
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("★ W3 双保险:visibilitychange 与 1s 心跳不重复扣 hiddenMs", () => {
|
|
741
|
+
/* ① 可见性路径:hidden→518s→visible,hiddenMs 只能记一次 518s(不能变成 1036s) */
|
|
742
|
+
const env = runScript(harness({ hostFeature: true }));
|
|
743
|
+
env.advance(15000);
|
|
744
|
+
env.doc.visibilityState = "hidden"; env.fireDocumentEvent("visibilitychange");
|
|
745
|
+
env.suspend(518000);
|
|
746
|
+
env.doc.visibilityState = "visible"; env.fireDocumentEvent("visibilitychange");
|
|
747
|
+
const h1 = env.win.__dshMaDiag.load.hiddenMs;
|
|
748
|
+
assert.ok(h1 >= 517000 && h1 <= 519000, `hiddenMs 只记 518s 一次(实际 ${h1})`);
|
|
749
|
+
assert.ok(h1 < 600000, "绝不能重复扣成两倍");
|
|
750
|
+
|
|
751
|
+
/* ② 心跳路径:visibilitychange 没触发(进程被冻结)——靠 1s 心跳的 gap 兜底识别 */
|
|
752
|
+
const env2 = runScript(harness({ hostFeature: true }));
|
|
753
|
+
env2.tickIntervals(1); // 建立心跳基准
|
|
754
|
+
env2.advance(15000);
|
|
755
|
+
env2.suspend(518000);
|
|
756
|
+
env2.tickIntervals(1); // 恢复后第一跳:gap 巨大 → 记入 hidden
|
|
757
|
+
const h2 = env2.win.__dshMaDiag.load.hiddenMs;
|
|
758
|
+
assert.ok(h2 > 500000, `心跳兜底应识别出约 518s 挂起(实际 ${h2})`);
|
|
759
|
+
env2.doc.visibilityState = "visible";
|
|
760
|
+
env2.fireDocumentEvent("visibilitychange"); // 之后再来 visibilitychange 也不得重复扣
|
|
761
|
+
const h3 = env2.win.__dshMaDiag.load.hiddenMs;
|
|
762
|
+
assert.ok(h3 - h2 < 5000, `visibilitychange 不得重复扣(${h2} → ${h3})`);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("★ W3 两档文案:12s(活跃)出中性「正在准备界面…」,20s 才升级为可操作故障条", () => {
|
|
766
|
+
const env1 = runScript(harness());
|
|
767
|
+
env1.fireTimer(12000);
|
|
768
|
+
const bar1 = env1.findBar();
|
|
769
|
+
assert.ok(bar1 && bar1.hidden === false, "12s(活跃时间)就应出现中性进度提示(前 20s 不再空白)");
|
|
770
|
+
const txt1 = bar1.querySelector('[data-role="loadbar-txt"]').textContent;
|
|
771
|
+
assert.match(txt1, /正在准备界面…(已等待 \d+ 秒)/, "12s 阶段是中性「正在准备」,不是故障归因");
|
|
772
|
+
assert.ok(!/电脑端可能未连接|页面资源加载失败/.test(txt1), "12s 不得提前做故障归因");
|
|
773
|
+
|
|
774
|
+
const env2 = runScript(harness());
|
|
775
|
+
env2.fireTimer(20000);
|
|
776
|
+
const txt2 = env2.findBar().querySelector('[data-role="loadbar-txt"]').textContent;
|
|
777
|
+
assert.match(txt2, /网络较慢(已等待 20 秒)/, "20s 升级为既有故障分档(无失败证据 → 网络较慢)");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("★ W3 诊断串:page/res 拆分、exhausted、挂起时长、最后失败 URL/tag,且 ≤400 字符", async () => {
|
|
781
|
+
const env = runScript(harness({ hostFeature: true, perfOffsetMs: 8664 }));
|
|
782
|
+
// 同一个 URL 连续失败 3 次:两次就地重建后额度用尽(exhausted=true)
|
|
783
|
+
const el = failingScript("/plugins/??@deepseek-ai/x.js");
|
|
784
|
+
env.body.appendChild(el);
|
|
785
|
+
env.fireWindowEvent("error", { target: el }); // 失败 1 → 排 2s 重试
|
|
786
|
+
env.fireTimers(2000); // 重建 1(resRetries=1;同毫秒还有 E2EE 重绘,全部放行)
|
|
787
|
+
const clone1 = env.body.children.find((c) => c.attrs && c.attrs["data-dsh-ma-retry"] === "1");
|
|
788
|
+
clone1.src = clone1.getAttribute("src");
|
|
789
|
+
env.fireWindowEvent("error", { target: clone1 }); // 失败 2 → 排 4s 重试
|
|
790
|
+
env.fireTimers(4000); // 重建 2(resRetries=2)
|
|
791
|
+
const clones = env.body.children.filter((c) => c.attrs && c.attrs["data-dsh-ma-retry"] === "1");
|
|
792
|
+
const clone2 = clones[1];
|
|
793
|
+
clone2.src = clone2.getAttribute("src");
|
|
794
|
+
env.fireWindowEvent("error", { target: clone2 }); // 失败 3 → 额度用尽
|
|
795
|
+
assert.equal(env.win.__dshMaDiag.retry.res, 2, "前提:2 次就地重建");
|
|
796
|
+
// 挂起 518s 后恢复 → 出提示条
|
|
797
|
+
env.doc.visibilityState = "hidden"; env.fireDocumentEvent("visibilitychange");
|
|
798
|
+
env.suspend(518000);
|
|
799
|
+
env.doc.visibilityState = "visible"; env.fireDocumentEvent("visibilitychange");
|
|
800
|
+
await new Promise((r) => setImmediate(r));
|
|
801
|
+
env.barBtn("loadbar-copy").click();
|
|
802
|
+
await new Promise((r) => setImmediate(r));
|
|
803
|
+
const line = env.clicks.clipboard[0];
|
|
804
|
+
assert.match(line, /原因=resource/, "有资源失败 → resource");
|
|
805
|
+
assert.match(line, /首屏资源失败=3/, "失败条数");
|
|
806
|
+
assert.match(line, /link 0\/script 3/, "按 tag 拆分");
|
|
807
|
+
assert.match(line, /自动重试=页0\/元素2/, "页/元素拆分(不再只有和值 2)");
|
|
808
|
+
assert.match(line, /已用尽=是/, "exhausted 要带出去");
|
|
809
|
+
assert.match(line, /最后 .*plugins\/.* SCRIPT/i, "最后一次失败 URL+tag");
|
|
810
|
+
assert.match(line, /挂起 518 秒/, "挂起时长");
|
|
811
|
+
assert.ok(line.length <= 400, `单行 ≤400 字符(实际 ${line.length})`);
|
|
812
|
+
});
|
package/dsh-setup.mjs
CHANGED
|
@@ -149,7 +149,7 @@ function findDepWs(startDir) {
|
|
|
149
149
|
* 漏同步就会出现「入口脚本在、依赖不在」的半装运行时(插件半据此判定「运行环境已就绪」,
|
|
150
150
|
* 却永远起不来 bridge —— 2026-09-23 用户反馈的现场)。`src/channel-ref.mjs` 同理(本安装器也 import)。
|
|
151
151
|
*/
|
|
152
|
-
const RUNTIME_CLIENT_FILES = ["mobile-adapter.mjs", "dsh-bridge.mjs", "e2ee-shim.mjs", "e2ee-client.mjs", "e2ee-shim-script.js", "mobile-adapter.test.mjs", "wechat-channel.mjs", "dsh-events.mjs", "wechat-runtime.mjs", "wecom-bot.mjs", "src/lifecycle.mjs", "src/channel-ref.mjs"];
|
|
152
|
+
const RUNTIME_CLIENT_FILES = ["mobile-adapter.mjs", "dsh-bridge.mjs", "e2ee-shim.mjs", "e2ee-client.mjs", "e2ee-shim-script.js", "mobile-adapter.test.mjs", "wechat-channel.mjs", "dsh-events.mjs", "wechat-runtime.mjs", "wecom-bot.mjs", "im-format.mjs", "dingtalk-bot.mjs", "feishu-bot.mjs", "src/lifecycle.mjs", "src/channel-ref.mjs"];
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
155
|
* 把"客户端脚本"补齐到配置目录。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.7.12-beta.
|
|
3
|
+
"version": "0.7.12-beta.2",
|
|
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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"test:plugin": "DSH_PROFILE_DIR= DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 DSH_RELAY_DEFAULT_API=http://127.0.0.1:1 DSH_RELAY_BUNDLED_RUNTIME=/nonexistent-dsh-bundled NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
|
|
57
57
|
"_comment:test:bridge": "⚠️ 必须 --test-concurrency=1(与 test:plugin 对齐):tunnel-auth 有一条「bridge 5 秒内必须重连」的硬期限,并行跑时新增用例抢 CPU 会把它撞破 → 偶发假红(实测单独跑 2/2 通过、并发跑偶发 0/2)。假红比慢更贵。",
|
|
58
58
|
"test:bridge": "DSH_PROFILE_DIR= DSH_RELAY_DEFAULT_API=http://127.0.0.1:1 NODE_OPTIONS=\"--require $PWD/scripts/test-net-guard.cjs ${NODE_OPTIONS:-}\" node --test --test-concurrency=1 clients/dsh-remote/test/*.test.mjs",
|
|
59
|
-
"check": "node --check scripts/check-profile-activation.mjs && node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/wechat-channel.mjs && node --check clients/dsh-remote/wechat-runtime.mjs && node --check clients/dsh-remote/dsh-events.mjs && node --check clients/dsh-remote/agent/index.mjs && node --check clients/dsh-remote/agent/version.mjs && node --check clients/dsh-remote/agent/detect.mjs && node --check clients/dsh-remote/agent/permission-bridge.mjs && node --check clients/dsh-remote/agent/proc.mjs && node --check clients/dsh-remote/agent/events.mjs && node --check clients/dsh-remote/agent/codex.mjs && node --check clients/dsh-remote/agent/gemini.mjs && node --check clients/dsh-remote/agent/opencode.mjs && node --check clients/dsh-remote/agent/agent-e2e-codex.mjs && node --check clients/dsh-remote/agent/agent-e2e-real.mjs && node --check clients/dsh-remote/push-reporter.mjs && node --check clients/dsh-remote/decision-poller.mjs && node --check clients/dsh-remote/approval-coordinator.mjs && node --check clients/dsh-remote/wecom-bot.mjs && node --check clients/dsh-remote/im-format.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",
|
|
59
|
+
"check": "node --check scripts/check-profile-activation.mjs && node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/wechat-channel.mjs && node --check clients/dsh-remote/wechat-runtime.mjs && node --check clients/dsh-remote/dsh-events.mjs && node --check clients/dsh-remote/agent/index.mjs && node --check clients/dsh-remote/agent/version.mjs && node --check clients/dsh-remote/agent/detect.mjs && node --check clients/dsh-remote/agent/permission-bridge.mjs && node --check clients/dsh-remote/agent/proc.mjs && node --check clients/dsh-remote/agent/events.mjs && node --check clients/dsh-remote/agent/codex.mjs && node --check clients/dsh-remote/agent/gemini.mjs && node --check clients/dsh-remote/agent/opencode.mjs && node --check clients/dsh-remote/agent/agent-e2e-codex.mjs && node --check clients/dsh-remote/agent/agent-e2e-real.mjs && node --check clients/dsh-remote/push-reporter.mjs && node --check clients/dsh-remote/decision-poller.mjs && node --check clients/dsh-remote/approval-coordinator.mjs && node --check clients/dsh-remote/wecom-bot.mjs && node --check clients/dsh-remote/im-format.mjs && node --check clients/dsh-remote/dingtalk-bot.mjs && node --check clients/dsh-remote/feishu-bot.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",
|
|
60
60
|
"release:tarballs": "bash scripts/release-tarballs.sh",
|
|
61
61
|
"sync:local": "node scripts/sync-local.mjs"
|
|
62
62
|
},
|