@mrrisega/dsh-remote 0.7.1 → 0.7.2-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/dsh-bridge.mjs +16 -0
- package/clients/dsh-remote/mirror-diag-script.js +154 -0
- package/clients/dsh-remote/mirror-diag.mjs +106 -0
- package/clients/dsh-remote/mobile-adapter.mjs +236 -22
- package/clients/dsh-remote/test/bridge-html-inject.test.mjs +46 -10
- package/clients/dsh-remote/test/bridge-upstream-refresh.test.mjs +40 -0
- package/clients/dsh-remote/test/mirror-diag.test.mjs +82 -0
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +31 -11
- package/clients/dsh-remote/test/mobile-adapter-load-watch.test.mjs +408 -0
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +152 -155
- package/packages/dsh-remote-web/lib/index.js +396 -14
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +16 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mirror-diag-script.js +154 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mirror-diag.mjs +106 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +236 -22
- package/packages/dsh-remote-web/test/announcements-ui.test.mjs +6 -3
- package/packages/dsh-remote-web/test/desktop-compat.test.mjs +12 -6
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +116 -3
- package/packages/dsh-remote-web/test/repair-backup.test.mjs +599 -0
|
@@ -76,6 +76,7 @@ import { promisify } from "node:util";
|
|
|
76
76
|
import { gzip as gzipCb } from "node:zlib";
|
|
77
77
|
// 移动端适配层(经隧道访问的官方 dsh web 窄屏注入;DSH_MOBILE_ADAPTER=0 可关闭,默认开启)
|
|
78
78
|
import { maybeInjectMobileAdapter } from "./mobile-adapter.mjs";
|
|
79
|
+
import { maybeInjectMirrorDiag } from "./mirror-diag.mjs";
|
|
79
80
|
import { discoverUpstream, resolveUpstreamHint, FALLBACK_UPSTREAM } from "./upstream-discovery.mjs";
|
|
80
81
|
// 镜像页 E2EE 加密 shim(Phase-4):text/html 注入;DSH_E2EE_SHIM=0 可关闭,叠加 e2ee.enabled 灰度门
|
|
81
82
|
import { maybeInjectE2eeShim } from "./e2ee-shim.mjs";
|
|
@@ -862,6 +863,15 @@ async function doHttp(method, path, reqHeaders, body, isB64) {
|
|
|
862
863
|
buf = m.buf;
|
|
863
864
|
armMirrorLoadWatch(path);
|
|
864
865
|
}
|
|
866
|
+
// 镜像页加载故障采集器(2026-10-17,**与 e2ee 无关**):出问题的用户往往没开 E2EE,
|
|
867
|
+
// 所以无条件注入。只在失败时上报(见 mirror-diag-script.js),零 DOM 改动。
|
|
868
|
+
{
|
|
869
|
+
const d = maybeInjectMirrorDiag({ buf, contentType });
|
|
870
|
+
if (d.injected) {
|
|
871
|
+
console.log(`[bridge] mirror-diag 注入 ${path}: ${(buf.length / 1024).toFixed(0)}KB → ${(d.buf.length / 1024).toFixed(0)}KB`);
|
|
872
|
+
buf = d.buf;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
865
875
|
// E2EE 镜像页加密 shim(Phase-4):仅桥端 e2ee 启用时注入(e2ee.enabled=false → 零注入零行为)
|
|
866
876
|
if (e2ee.enabled) {
|
|
867
877
|
const s = maybeInjectE2eeShim({ buf, contentType });
|
|
@@ -1235,6 +1245,12 @@ export async function handleWsOpen(dchOrSend, frame) {
|
|
|
1235
1245
|
console.log(`[bridge] ws-error ${path} (id=${id}): ${e.message || ""}`);
|
|
1236
1246
|
if (!session.opened) {
|
|
1237
1247
|
wsSessions.delete(id);
|
|
1248
|
+
// ★ 上游可能已经换端口(桌面端 App 重启会换本地端口,生产日志里抓到过
|
|
1249
|
+
// `upstream_ws_open_failed ... ECONNREFUSED 127.0.0.1:19387`)。
|
|
1250
|
+
// HTTP 失败路径早就会触发重新发现,**WS 失败路径之前漏了** —— 而镜像页常常
|
|
1251
|
+
// 先开 WS(控制通道/数据通道),于是端口变了以后一直 502、页面白屏,直到用户
|
|
1252
|
+
// 手动重开。这里补上:本次失败照旧回 502,但立即触发重新发现,下一次重试就命中新端口。
|
|
1253
|
+
void refreshUpstream("ws-open 上游连接失败");
|
|
1238
1254
|
send({ id, type: "ws-open", ok: false, code: 502, reason: String(e.message || "ws error") });
|
|
1239
1255
|
}
|
|
1240
1256
|
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-mirror-diag — 镜像页「加载故障」采集器(由 bridge 注入到 text/html 的 </head> 前)。
|
|
3
|
+
*
|
|
4
|
+
* 由来(2026-10-17):用户反馈「装了 UI 插件后打不开界面 / Failed to load plugins」,
|
|
5
|
+
* 但本地与账号环境都复现不出来。生产日志里只看到 `/plugins/??…` 的 499 中断,
|
|
6
|
+
* 拿不到浏览器侧的真实失败项(哪个 URL、多少毫秒、什么错误)。这个采集器就是
|
|
7
|
+
* **让下一次故障自己带证据回来**。
|
|
8
|
+
*
|
|
9
|
+
* 设计约束(都是硬要求):
|
|
10
|
+
* 1. **绝不打扰用户**:不改 DOM、不加 UI、不阻塞渲染;任何异常都在内部吞掉。
|
|
11
|
+
* 2. **只在出错时上报**:没有失败资源/脚本错误 → 一个字节都不发(零流量)。
|
|
12
|
+
* 3. **不泄密**:只上报**路径**(剥掉 query/hash)、状态码、耗时、字节数、错误消息前 500 字;
|
|
13
|
+
* 不上报页面内容、请求体、cookie、token、用户信息。
|
|
14
|
+
* 4. **可关**:`?mirror_diag=0` 或 bridge 环境变量 `DSH_MIRROR_DIAG=0` → 完全不注入。
|
|
15
|
+
* 5. **慢请求也记**:`/plugins/`、`/assets/` 加载超过 8s 记一条(这正是"插件聚合包被
|
|
16
|
+
* 限速拖死 → 浏览器 499"的现场)。
|
|
17
|
+
*
|
|
18
|
+
* 上报地址:`/relay-api/api/mirror-diag`(企业端,需登录态;token 从 localStorage.dsh_token 取)。
|
|
19
|
+
*/
|
|
20
|
+
(function () {
|
|
21
|
+
"use strict";
|
|
22
|
+
var W = window, D = document;
|
|
23
|
+
var VERSION = 1;
|
|
24
|
+
var SLOW_MS = 8000;
|
|
25
|
+
var MAX_ITEMS = 20;
|
|
26
|
+
var FLUSH_DELAY = 2500;
|
|
27
|
+
var ENDPOINT = "/relay-api/api/mirror-diag";
|
|
28
|
+
var items = [];
|
|
29
|
+
var bootFailed = [];
|
|
30
|
+
var sent = false;
|
|
31
|
+
var timer = null;
|
|
32
|
+
|
|
33
|
+
function now() { return Date.now(); }
|
|
34
|
+
function clip(s, n) { s = String(s == null ? "" : s); return s.length > n ? s.slice(0, n) : s; }
|
|
35
|
+
/** 只保留同源路径(剥 query/hash),跨源整条丢弃 —— 避免把第三方 URL 带出去。 */
|
|
36
|
+
function pathOf(u) {
|
|
37
|
+
try {
|
|
38
|
+
var x = new URL(String(u || ""), W.location.href);
|
|
39
|
+
if (x.origin !== W.location.origin) return null;
|
|
40
|
+
return clip(x.pathname, 300);
|
|
41
|
+
} catch (e) { return null; }
|
|
42
|
+
}
|
|
43
|
+
function add(item) {
|
|
44
|
+
if (!item || items.length >= MAX_ITEMS || sent) return;
|
|
45
|
+
items.push(item);
|
|
46
|
+
if (timer === null) timer = setTimeout(flush, FLUSH_DELAY);
|
|
47
|
+
}
|
|
48
|
+
function interesting(p) {
|
|
49
|
+
return !!p && (p.indexOf("/plugins/") === 0 || p.indexOf("/assets/") === 0
|
|
50
|
+
|| p.indexOf("/remote/") === 0 || p.indexOf("/api/") === 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ① 资源加载失败(script/link/img/fetch 的 error 事件在捕获阶段拿到 target)
|
|
54
|
+
W.addEventListener("error", function (e) {
|
|
55
|
+
try {
|
|
56
|
+
var t = e && e.target;
|
|
57
|
+
if (t && t !== W && (t.src || t.href)) {
|
|
58
|
+
var p = pathOf(t.src || t.href);
|
|
59
|
+
if (interesting(p)) add({ t: "res", url: p, tag: String(t.tagName || "").toLowerCase() });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (e && e.message) add({ t: "err", msg: clip(e.message, 500), src: pathOf(e.filename) });
|
|
63
|
+
} catch (x) { /* 采集器自身绝不抛 */ }
|
|
64
|
+
}, true);
|
|
65
|
+
|
|
66
|
+
// ② 未处理异常 / Promise 拒绝
|
|
67
|
+
W.addEventListener("unhandledrejection", function (e) {
|
|
68
|
+
try {
|
|
69
|
+
var r = e && e.reason;
|
|
70
|
+
var msg = r && (r.message || r.stack) ? (r.message || "") + " " + (r.stack || "") : String(r);
|
|
71
|
+
add({ t: "rej", msg: clip(msg, 500) });
|
|
72
|
+
} catch (x) {}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ③ 慢资源(插件聚合包/前端主干)—— 用 Resource Timing,不额外发请求
|
|
76
|
+
try {
|
|
77
|
+
if (W.PerformanceObserver) {
|
|
78
|
+
var po = new W.PerformanceObserver(function (list) {
|
|
79
|
+
try {
|
|
80
|
+
list.getEntries().forEach(function (en) {
|
|
81
|
+
var p = pathOf(en.name);
|
|
82
|
+
if (!interesting(p)) return;
|
|
83
|
+
if (en.duration >= SLOW_MS) {
|
|
84
|
+
add({ t: "slow", url: p, ms: Math.round(en.duration), bytes: en.transferSize || 0 });
|
|
85
|
+
} else if (en.transferSize === 0 && en.duration > 0 && en.responseEnd > 0) {
|
|
86
|
+
// transferSize 为 0 且非缓存命中 → 常见于"请求被中断/取消"
|
|
87
|
+
add({ t: "zero", url: p, ms: Math.round(en.duration) });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
} catch (x) {}
|
|
91
|
+
});
|
|
92
|
+
po.observe({ type: "resource", buffered: true });
|
|
93
|
+
}
|
|
94
|
+
} catch (x) {}
|
|
95
|
+
|
|
96
|
+
// ④ 首页启动失败文案(`Failed to load plugins` / `did not activate`)—— 抓具体插件名
|
|
97
|
+
function scanBootFailure() {
|
|
98
|
+
try {
|
|
99
|
+
var txt = (D.body && D.body.innerText) || "";
|
|
100
|
+
if (txt.indexOf("Failed to load plugins") < 0 && txt.indexOf("did not activate") < 0) return;
|
|
101
|
+
var lines = txt.split("\n").map(function (s) { return s.trim(); }).filter(Boolean);
|
|
102
|
+
for (var i = 0; i < lines.length; i++) {
|
|
103
|
+
if (/import failed|did not activate|pending \(waiting|: [a-z]+ failed/i.test(lines[i])) {
|
|
104
|
+
if (bootFailed.length < 10) bootFailed.push(clip(lines[i], 200));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (bootFailed.length === 0) bootFailed.push("Failed to load plugins(无明细文本)");
|
|
108
|
+
if (timer === null) timer = setTimeout(flush, 300);
|
|
109
|
+
} catch (x) {}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
var mo = new W.MutationObserver(scanBootFailure);
|
|
113
|
+
if (D.documentElement) mo.observe(D.documentElement, { childList: true, subtree: true, characterData: true });
|
|
114
|
+
setTimeout(scanBootFailure, 6000);
|
|
115
|
+
setTimeout(scanBootFailure, 15000);
|
|
116
|
+
} catch (x) {}
|
|
117
|
+
|
|
118
|
+
function token() {
|
|
119
|
+
try {
|
|
120
|
+
var t = W.localStorage && W.localStorage.getItem("dsh_token");
|
|
121
|
+
if (t) return t;
|
|
122
|
+
var m = D.cookie.match(/(?:^|;\s*)dsh_token=([^;]+)/);
|
|
123
|
+
return m ? decodeURIComponent(m[1]) : "";
|
|
124
|
+
} catch (e) { return ""; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function flush() {
|
|
128
|
+
timer = null;
|
|
129
|
+
if (sent || (items.length === 0 && bootFailed.length === 0)) return;
|
|
130
|
+
sent = true;
|
|
131
|
+
var tk = token();
|
|
132
|
+
var payload = {
|
|
133
|
+
v: VERSION,
|
|
134
|
+
kind: bootFailed.length ? "boot" : "runtime",
|
|
135
|
+
page: clip(W.location.pathname, 200),
|
|
136
|
+
ua: clip(W.navigator && W.navigator.userAgent, 300),
|
|
137
|
+
items: items.slice(0, MAX_ITEMS),
|
|
138
|
+
boot_failed: bootFailed.slice(0, 10),
|
|
139
|
+
at: now()
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
fetch(ENDPOINT, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
credentials: "same-origin",
|
|
145
|
+
headers: { "content-type": "application/json", ...(tk ? { authorization: "Bearer " + tk } : {}) },
|
|
146
|
+
body: JSON.stringify(payload),
|
|
147
|
+
keepalive: true
|
|
148
|
+
}).catch(function () { /* 上报失败静默:绝不影响页面 */ });
|
|
149
|
+
} catch (e) { /* ignore */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 页面卸载前兜底发一次(keepalive 允许在卸载后继续)
|
|
153
|
+
W.addEventListener("pagehide", function () { try { if (!sent) flush(); } catch (x) {} });
|
|
154
|
+
})();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mirror-diag.mjs — bridge 侧注入的「镜像页加载故障采集器」。
|
|
3
|
+
*
|
|
4
|
+
* 注入目标:经隧道回传的**官方 dsh web 文档**(与 mobile-adapter / e2ee-shim 同一注入管线、
|
|
5
|
+
* 同一宿主特征门),在 `</head>` 前插入 `<script id="dsh-mirror-diag-js">`。
|
|
6
|
+
*
|
|
7
|
+
* 为什么需要它(2026-10-17):用户反馈「装了 UI 插件后打不开界面 / Failed to load plugins」,
|
|
8
|
+
* 本地复现不出来。生产日志只能看到 `/plugins/??…` 出现 499(客户端中断),拿不到浏览器侧
|
|
9
|
+
* 到底哪个资源失败、耗时多少、什么错误。采集器让下一次故障**自己带证据回来**。
|
|
10
|
+
*
|
|
11
|
+
* 与 e2ee 的关系:**刻意不依赖** e2ee.enabled —— 出问题的用户往往没开 E2EE,
|
|
12
|
+
* 只有无条件注入才能覆盖他们。脚本本身零 DOM 改动、只在失败时上报(见 mirror-diag-script.js)。
|
|
13
|
+
*
|
|
14
|
+
* 开关:
|
|
15
|
+
* - `DSH_MIRROR_DIAG=0` → 完全不注入(逃生口);
|
|
16
|
+
* - `?mirror_diag=0` → 页面侧由脚本自身判定(见 mirror-diag-script.js 的注释)。
|
|
17
|
+
*/
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const THIS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
export const DIAG_ID = "dsh-mirror-diag";
|
|
24
|
+
export const DIAG_SCRIPT_PATH = path.join(THIS_DIR, "mirror-diag-script.js");
|
|
25
|
+
|
|
26
|
+
/** 与 mobile-adapter / e2ee-shim 同源的「官方 dsh web 文档」特征门。 */
|
|
27
|
+
const HOST_FEATURE_RE =
|
|
28
|
+
/(__ModuleLoader__|@deepseek-ai\/dsh-client-modules|@deepseek-ai\/dsh-client-connection|<title>\s*DeepSeek Harness)/i;
|
|
29
|
+
|
|
30
|
+
/** 采集器源码:只在首次注入时读盘并缓存(读失败 → 永不注入,绝不影响主链路)。 */
|
|
31
|
+
let scriptCache = null;
|
|
32
|
+
function diagScriptText() {
|
|
33
|
+
if (scriptCache !== null) return scriptCache;
|
|
34
|
+
try {
|
|
35
|
+
scriptCache = fs.readFileSync(DIAG_SCRIPT_PATH, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
scriptCache = "";
|
|
38
|
+
}
|
|
39
|
+
return scriptCache;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `DSH_MIRROR_DIAG=0` 关闭注入(默认开启);测试里可用同样的环境变量。 */
|
|
43
|
+
export function mirrorDiagEnabled() {
|
|
44
|
+
const v = process.env.DSH_MIRROR_DIAG;
|
|
45
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 注入门:text/html + 宿主特征 + 未注入过。 */
|
|
49
|
+
export function shouldInjectMirrorDiag({ contentType, html }) {
|
|
50
|
+
const ct = String(contentType || "").toLowerCase();
|
|
51
|
+
if (!ct.includes("text/html")) return false;
|
|
52
|
+
const text = String(html || "");
|
|
53
|
+
if (text.length === 0) return false;
|
|
54
|
+
if (!text.toLowerCase().includes("</head>")) return false;
|
|
55
|
+
if (text.includes(`data-${DIAG_ID}`)) return false; // 幂等:重复注入直接跳过
|
|
56
|
+
return HOST_FEATURE_RE.test(text);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 纯函数:把采集器注入到上游 HTML 的 `</head>` 之前。
|
|
61
|
+
* @param {string} html
|
|
62
|
+
* @returns {{html: string, injected: boolean}}
|
|
63
|
+
*/
|
|
64
|
+
export function injectMirrorDiag(html) {
|
|
65
|
+
const src = String(html);
|
|
66
|
+
if (src.includes(`data-${DIAG_ID}`)) return { html: src, injected: false };
|
|
67
|
+
// ★ 注入点必须是 <head> **开头**,不能是 </head> 前:
|
|
68
|
+
// 官方 dsh web 的插件/主干 <script> 就在 <head> 里,注入到 </head> 前会**晚于**
|
|
69
|
+
// 那些脚本执行 —— 启动期错误(本次要抓的正是这类)就漏了。
|
|
70
|
+
// 用字符扫描而不是正则,避免 HTML 里的属性引号/大小写差异把注入点算错。
|
|
71
|
+
const low = src.toLowerCase();
|
|
72
|
+
const headIdx = low.indexOf("<head");
|
|
73
|
+
let pos = -1;
|
|
74
|
+
if (headIdx !== -1) {
|
|
75
|
+
const gt = low.indexOf(">", headIdx);
|
|
76
|
+
pos = gt === -1 ? headIdx : gt + 1;
|
|
77
|
+
} else {
|
|
78
|
+
pos = low.indexOf("</head>"); // 兜底:没有 <head> 时退化为 </head> 前
|
|
79
|
+
}
|
|
80
|
+
if (pos === -1) return { html: src, injected: false };
|
|
81
|
+
const script = diagScriptText();
|
|
82
|
+
if (!script) return { html: src, injected: false };
|
|
83
|
+
// 注入体不能含 </script>(会把文档提前截断)—— 采集器源码里本来就没有,这里再守一道。
|
|
84
|
+
if (/<\/script/i.test(script)) {
|
|
85
|
+
console.warn(`[${DIAG_ID}] 采集器源码含 </script>,拒绝注入`);
|
|
86
|
+
return { html: src, injected: false };
|
|
87
|
+
}
|
|
88
|
+
const block = `<script id="${DIAG_ID}-js" data-${DIAG_ID}>${script}</script>`;
|
|
89
|
+
return { html: src.slice(0, pos) + block + src.slice(pos), injected: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {{buf: Buffer, contentType: string}} input
|
|
94
|
+
* @returns {{buf: Buffer, injected: boolean}}
|
|
95
|
+
*/
|
|
96
|
+
export function maybeInjectMirrorDiag({ buf, contentType }) {
|
|
97
|
+
if (!mirrorDiagEnabled() || !Buffer.isBuffer(buf) || buf.length === 0) {
|
|
98
|
+
return { buf, injected: false };
|
|
99
|
+
}
|
|
100
|
+
const text = buf.toString("utf8");
|
|
101
|
+
if (!shouldInjectMirrorDiag({ contentType, html: text })) return { buf, injected: false };
|
|
102
|
+
const out = injectMirrorDiag(text);
|
|
103
|
+
return { buf: Buffer.from(out.html, "utf8"), injected: out.injected };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default { injectMirrorDiag, maybeInjectMirrorDiag, shouldInjectMirrorDiag, mirrorDiagEnabled };
|
|
@@ -554,6 +554,67 @@ div.dsh-ma-menu .dsh-ma-load-warn[hidden] { display: none !important; }
|
|
|
554
554
|
div.dsh-ma-menu .dsh-ma-load-warn .ico { flex: none; }
|
|
555
555
|
div.dsh-ma-menu .dsh-ma-load-warn .txt { min-width: 0; overflow-wrap: anywhere; }
|
|
556
556
|
|
|
557
|
+
/* ===== 加载疑似卡住时的**可操作**底部提示条(2026-10-17 事故新增) =====
|
|
558
|
+
事故现场:用户本机 ~/.dsh-remote(bridge 运行时 + 账号配对信息/设备密钥)被误删 → bridge 起不来、
|
|
559
|
+
手机端镜像页第一次加载**干等 47 秒**才报错;页面上只有悬浮菜单里一行"已等 N 秒…复制给客服",
|
|
560
|
+
没有任何可点的操作。这里补一条**底部轻量提示条**(不是全屏层、不遮挡主界面):
|
|
561
|
+
· 只在 12 秒看门狗判定 slow 之后才**懒创建**(解析期一个节点都不往页面塞);
|
|
562
|
+
· 只占视口底部一条、高度受限(max-height),浮层外的区域照常可点 → 用户还能继续操作;
|
|
563
|
+
· 三个按钮:重试 / 返回设备列表 / 复制诊断信息,外加 ✕ 关闭;关掉后本页不再弹;
|
|
564
|
+
· 认出官方界面后自动收起(判断错了也不会一直挂着)——失败方向仍然安全:
|
|
565
|
+
最坏只是底部多一条**可关闭**的提示,而不是"永远盖住整片界面"。
|
|
566
|
+
动画只在「支持 prefers-reduced-motion 且用户没要求减少动效」时挂上:
|
|
567
|
+
老浏览器不认识这个媒体查询 → 不匹配 → 无动画(无需用 matchMedia 判断,也不会有兼容异常)。 */
|
|
568
|
+
div.dsh-ma-loadbar {
|
|
569
|
+
position: fixed;
|
|
570
|
+
left: max(10px, env(safe-area-inset-left));
|
|
571
|
+
right: max(10px, env(safe-area-inset-right));
|
|
572
|
+
bottom: calc(10px + env(safe-area-inset-bottom));
|
|
573
|
+
/* 低于 FAB(400) 与菜单(401):提示条不得压住"自救出口"(悬浮按钮永远点得到) */
|
|
574
|
+
z-index: 399;
|
|
575
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
|
576
|
+
box-sizing: border-box;
|
|
577
|
+
width: auto;
|
|
578
|
+
max-height: min(38vh, 260px);
|
|
579
|
+
overflow-y: auto;
|
|
580
|
+
-webkit-overflow-scrolling: touch;
|
|
581
|
+
padding: 10px 12px;
|
|
582
|
+
border-radius: 12px;
|
|
583
|
+
background: var(--dsw-alias-bg-elevated, #fff);
|
|
584
|
+
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.3));
|
|
585
|
+
box-shadow: 0 6px 24px rgba(0,0,0,.24);
|
|
586
|
+
color: var(--dsw-alias-label-primary, #111);
|
|
587
|
+
font: 400 13px/1.5 -apple-system, system-ui, sans-serif;
|
|
588
|
+
}
|
|
589
|
+
/* ⚠️ 必须显式写 [hidden]:UA 的 [hidden]{display:none} 会被上面的 display:flex 盖过(本仓踩过)。 */
|
|
590
|
+
div.dsh-ma-loadbar[hidden] { display: none !important; }
|
|
591
|
+
div.dsh-ma-loadbar .dsh-ma-loadbar-txt { flex: 1 1 160px; min-width: 0; overflow-wrap: anywhere; }
|
|
592
|
+
div.dsh-ma-loadbar .dsh-ma-loadbar-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
593
|
+
div.dsh-ma-loadbar button {
|
|
594
|
+
flex: none; padding: 8px 12px; border-radius: 9px; cursor: pointer;
|
|
595
|
+
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.35));
|
|
596
|
+
background: var(--dsw-alias-button-elevated-fill, rgba(127,127,127,.08));
|
|
597
|
+
color: var(--dsw-alias-label-primary, #111);
|
|
598
|
+
font: 600 13px/1 -apple-system, system-ui, sans-serif;
|
|
599
|
+
touch-action: manipulation; -webkit-tap-highlight-color: transparent;
|
|
600
|
+
}
|
|
601
|
+
div.dsh-ma-loadbar button:active { transform: scale(.98); }
|
|
602
|
+
div.dsh-ma-loadbar button.dsh-ma-loadbar-close {
|
|
603
|
+
padding: 6px 9px; border-color: transparent; background: transparent;
|
|
604
|
+
color: var(--dsw-alias-label-tertiary, #6b7280);
|
|
605
|
+
}
|
|
606
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
607
|
+
div.dsh-ma-loadbar { animation: dsh-ma-loadbar-in .18s ease-out; }
|
|
608
|
+
@keyframes dsh-ma-loadbar-in {
|
|
609
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
610
|
+
to { opacity: 1; transform: none; }
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/* 双保险:显式要求减少动效时一律无动画(老浏览器不匹配上面的 no-preference → 天然无动画) */
|
|
614
|
+
@media (prefers-reduced-motion: reduce) {
|
|
615
|
+
div.dsh-ma-loadbar { animation: none !important; transition: none !important; }
|
|
616
|
+
}
|
|
617
|
+
|
|
557
618
|
/* 会话过期引导条:镜像页在会话过期后只会"连不上",这里给一句能照做的话 + 一个出口。
|
|
558
619
|
非阻塞(顶部细条),点关闭即消失;不拦截任何请求。 */
|
|
559
620
|
div.dsh-ma-sessionbar {
|
|
@@ -656,21 +717,33 @@ const SCRIPT = `(() => {
|
|
|
656
717
|
}
|
|
657
718
|
})();
|
|
658
719
|
|
|
659
|
-
/* ===== 「加载疑似没完成」看门狗(2026-09-25
|
|
660
|
-
|
|
661
|
-
(用户实测:秒表走到 187
|
|
720
|
+
/* ===== 「加载疑似没完成」看门狗(2026-09-25 起;2026-10-17 提速并改成**可操作**提示)=====
|
|
721
|
+
历史:0.6.14 的首屏提示层是全屏覆盖 + 启发式退出 —— 判断错一次就把**已经加载完**的界面永久盖住
|
|
722
|
+
(用户实测:秒表走到 187 秒,刷新也一样)。所以 0.6.16 换了安全的失败方向:只置 slow 标记,
|
|
723
|
+
在**已有的**悬浮菜单里多一行字。这个方向本身是对的,但**提示太晚、且没有操作入口**——
|
|
724
|
+
2026-10-17 真实事故:用户本机 ~/.dsh-remote(运行时 + 账号配对信息/设备密钥)被误删后,
|
|
725
|
+
手机端镜像页第一次加载**干等 47 秒**才报错,页面上只有一行"已等 N 秒…复制给客服",
|
|
726
|
+
用户完全不知道该重新安装还是重新登录。
|
|
662
727
|
|
|
663
|
-
|
|
728
|
+
现在(2026-10-17):
|
|
729
|
+
· 解析期仍然**不创建任何 DOM**(只有几个变量 + 一个定时器)—— 页面还没过主机门,
|
|
664
730
|
更不该有任何覆盖层;
|
|
665
|
-
·
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
·
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
731
|
+
· **12 秒**后仍认不出官方界面 → 置 slow + 显示**底部轻量提示条**(懒创建):
|
|
732
|
+
文案「电脑端可能未连接 / 页面加载较慢」+ 已等待秒数;
|
|
733
|
+
按钮:重试(location.reload)/ 返回设备列表(/app/)/ 复制诊断信息 / ✕ 关闭;
|
|
734
|
+
· 提示条**不是全屏层**:只占底部一条、高度受限、浮层外照常可点、可关闭,
|
|
735
|
+
关掉后本页不再弹;认出官方界面后自动收起 —— 失败方向仍然安全;
|
|
736
|
+
· 菜单里那一行字**保留**(被动展开,不打扰,也仍可复制诊断话术)。
|
|
737
|
+
|
|
738
|
+
⚠️ 12 秒是"开始提示"的阈值,不是"判定失败":限速放开后正常首屏 1~3 秒,
|
|
739
|
+
12 秒既能及时帮到用户,也不至于刚打开就吓人(宁可晚一点,也别误报)。
|
|
740
|
+
⚠️ 注入体是模板字符串:本段注释/文案**一律用「」**,不得出现反引号。 */
|
|
672
741
|
var maLoadWatch = { startedAt: Date.now(), slow: false, wired: false, bound: false, warnEl: null, warnTxt: null,
|
|
673
|
-
reported: false, reportMs: 0
|
|
742
|
+
reported: false, reportMs: 0,
|
|
743
|
+
// 2026-10-17:底部可操作提示条(barEl = 懒创建的节点;barTimer = 秒数刷新)
|
|
744
|
+
barEl: null, barTxt: null, barTimer: 0, dismissed: false };
|
|
745
|
+
/** 看门狗阈值(毫秒):12 秒仍认不出官方界面就开始提示。测试按常量断言,不要散落魔法数。 */
|
|
746
|
+
var MA_LOAD_WATCH_MS = 12 * 1000;
|
|
674
747
|
/** 官方界面是否已经可用(只看最强信号;只用于决定"要不要多显示一行字")。 */
|
|
675
748
|
function maLoadReadyNow() {
|
|
676
749
|
try {
|
|
@@ -718,8 +791,8 @@ const SCRIPT = `(() => {
|
|
|
718
791
|
* ② **全程静默**:任何异常都吞掉,失败绝不影响页面,也绝不做重试;
|
|
719
792
|
* ③ 只在**已有的**同一批请求之外发这一次(一次 POST,几十字节),不是测速、不轮询。
|
|
720
793
|
* body = { ms, outcome, at }:ms=耗时毫秒(截到 [0,600000],与契约一致);outcome ∈ {ok,slow}
|
|
721
|
-
* (slow =
|
|
722
|
-
*/
|
|
794
|
+
* (slow = 12 秒看门狗兜底,是"删失下限",后台只把 ok 计入 P50/P90);at=事件发生的 epoch ms。
|
|
795
|
+
* ⚠️ 2026-10-17:阈值 25s→12s,**字段与一次语义一律不变**(后台契约不动)。 */
|
|
723
796
|
function maReportMirrorLoad(outcome) {
|
|
724
797
|
if (maLoadWatch.reported) return;
|
|
725
798
|
maLoadWatch.reported = true;
|
|
@@ -748,10 +821,140 @@ const SCRIPT = `(() => {
|
|
|
748
821
|
try { ua = String((navigator && navigator.userAgent) || "").slice(0, 120); } catch (e) { /* 忽略 */ }
|
|
749
822
|
return "dsh-remote 手机端加载疑似卡住:等待 " + maLoadWaitedSec() + " 秒后仍未出现对话界面。UA=" + ua;
|
|
750
823
|
}
|
|
824
|
+
|
|
825
|
+
/* ===== 底部可操作提示条(2026-10-17,见上方看门狗注释)=====
|
|
826
|
+
纪律:
|
|
827
|
+
· **懒创建**:只有 slow 且用户没关过时才创建节点;
|
|
828
|
+
· **绝不阻塞**:只占底部一条(CSS 限高),浮层外照常可点;按钮动作全部就地完成;
|
|
829
|
+
· **任何异常都吞掉**:提示层坏掉也绝不影响页面(每个入口都有 try/catch);
|
|
830
|
+
· **只提示一次**:用户点 ✕ 后 dismissed=true,本页不再弹(不骚扰)。 */
|
|
831
|
+
/** 提示条主文案:电脑端未连接/加载慢 + 已等待秒数(秒数由 barTimer 每秒刷新)。 */
|
|
832
|
+
function maLoadBarText() {
|
|
833
|
+
return "电脑端可能未连接 / 页面加载较慢(已等待 " + maLoadWaitedSec() + " 秒)";
|
|
834
|
+
}
|
|
835
|
+
/** 收起提示条并停掉秒数刷新(不改变 dismissed:用于"认出界面后自动收起"这类临时收起)。 */
|
|
836
|
+
function maLoadHideBar() {
|
|
837
|
+
try { if (maLoadWatch.barEl) maLoadWatch.barEl.hidden = true; } catch (e) { /* 忽略 */ }
|
|
838
|
+
try {
|
|
839
|
+
if (maLoadWatch.barTimer) { clearInterval(maLoadWatch.barTimer); maLoadWatch.barTimer = 0; }
|
|
840
|
+
} catch (e2) { /* 忽略 */ }
|
|
841
|
+
}
|
|
842
|
+
/** 用户点 ✕:本页不再弹(dismissed 是**本页**语义;刷新页面会重新开始一次提示机会)。 */
|
|
843
|
+
function maLoadDismissBar() {
|
|
844
|
+
try {
|
|
845
|
+
maLoadWatch.dismissed = true;
|
|
846
|
+
maLoadHideBar();
|
|
847
|
+
maLoadRenderWarn(); // 菜单里那一行也一起收起(关闭 = 本页只提示一次)
|
|
848
|
+
} catch (e) { /* 忽略 */ }
|
|
849
|
+
}
|
|
850
|
+
/** 导航(返回设备列表):assign 失败退回 href。 */
|
|
851
|
+
function maLoadGoto(to) {
|
|
852
|
+
try { window.location.assign(to); return; } catch (e) { /* 继续 */ }
|
|
853
|
+
try { window.location.href = to; } catch (e2) { /* 忽略 */ }
|
|
854
|
+
}
|
|
855
|
+
/** 复制诊断信息(沿用 maLoadCopyLine:含 UA 与耗时);剪贴板不可用时把整句显示在提示条上让用户截图。 */
|
|
856
|
+
function maLoadCopyDiag() {
|
|
857
|
+
var line = maLoadCopyLine();
|
|
858
|
+
var show = function (t) {
|
|
859
|
+
try { if (maLoadWatch.barTxt) maLoadWatch.barTxt.textContent = t; } catch (e) { /* 忽略 */ }
|
|
860
|
+
};
|
|
861
|
+
try {
|
|
862
|
+
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
863
|
+
var p = navigator.clipboard.writeText(line);
|
|
864
|
+
if (p && typeof p.then === "function") {
|
|
865
|
+
p.then(function () { show("已复制诊断信息(含等待时长与机型),可直接发给客服。"); },
|
|
866
|
+
function () { show(line); });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} catch (e) { /* 落到显示 */ }
|
|
871
|
+
show(line);
|
|
872
|
+
}
|
|
873
|
+
/** 懒创建提示条(只在 slow 之后调用;解析期绝不碰 DOM)。 */
|
|
874
|
+
function maLoadMountBar() {
|
|
875
|
+
try {
|
|
876
|
+
if (maLoadWatch.barEl || maLoadWatch.dismissed) return maLoadWatch.barEl;
|
|
877
|
+
var body = document.body || document.documentElement;
|
|
878
|
+
if (!body || !body.appendChild) return null;
|
|
879
|
+
var bar = document.createElement("div");
|
|
880
|
+
bar.className = "dsh-ma-loadbar";
|
|
881
|
+
/* role=status + aria-live=polite:读屏软件会把"加载较慢"念出来,但不会抢焦点;
|
|
882
|
+
它**不是** dialog/alert,不抢键盘、不锁滚动。 */
|
|
883
|
+
bar.setAttribute("role", "status");
|
|
884
|
+
bar.setAttribute("aria-live", "polite");
|
|
885
|
+
bar.innerHTML =
|
|
886
|
+
'<div class="dsh-ma-loadbar-txt" data-role="loadbar-txt"></div>' +
|
|
887
|
+
'<div class="dsh-ma-loadbar-actions">' +
|
|
888
|
+
'<button type="button" class="dsh-ma-loadbar-btn" data-role="loadbar-retry">重试</button>' +
|
|
889
|
+
'<button type="button" class="dsh-ma-loadbar-btn" data-role="loadbar-back">返回设备列表</button>' +
|
|
890
|
+
'<button type="button" class="dsh-ma-loadbar-btn" data-role="loadbar-copy">复制诊断信息</button>' +
|
|
891
|
+
'</div>' +
|
|
892
|
+
'<button type="button" class="dsh-ma-loadbar-close" data-role="loadbar-close" aria-label="关闭提示">✕</button>';
|
|
893
|
+
bar.hidden = true;
|
|
894
|
+
maLoadWatch.barEl = bar;
|
|
895
|
+
maLoadWatch.barTxt = bar.querySelector('[data-role="loadbar-txt"]');
|
|
896
|
+
bar.addEventListener("click", function (ev) {
|
|
897
|
+
// 事件委托:真实浏览器里目标可能是按钮内的文字节点;从 target 往上找到 [data-role]
|
|
898
|
+
try {
|
|
899
|
+
if (ev && typeof ev.preventDefault === "function") ev.preventDefault();
|
|
900
|
+
} catch (e0) { /* 忽略 */ }
|
|
901
|
+
try {
|
|
902
|
+
var t = ev && ev.target ? ev.target : null;
|
|
903
|
+
var role = "";
|
|
904
|
+
var guard = 0;
|
|
905
|
+
while (t && t !== bar && guard < 16 && !role) {
|
|
906
|
+
guard += 1;
|
|
907
|
+
try {
|
|
908
|
+
if (t.getAttribute) role = String(t.getAttribute("data-role") || "");
|
|
909
|
+
} catch (e1) { role = ""; }
|
|
910
|
+
t = t.parentNode || null;
|
|
911
|
+
}
|
|
912
|
+
if (role === "loadbar-retry") {
|
|
913
|
+
try { window.location.reload(); } catch (e2) { /* 忽略 */ }
|
|
914
|
+
} else if (role === "loadbar-back") {
|
|
915
|
+
maLoadGoto("/app/"); // 绝对路径:镜像页当前路径可能是 /remote/<deviceId>/…
|
|
916
|
+
} else if (role === "loadbar-copy") {
|
|
917
|
+
maLoadCopyDiag();
|
|
918
|
+
} else if (role === "loadbar-close") {
|
|
919
|
+
maLoadDismissBar();
|
|
920
|
+
}
|
|
921
|
+
} catch (e3) { /* 提示条失败绝不影响页面 */ }
|
|
922
|
+
});
|
|
923
|
+
body.appendChild(bar);
|
|
924
|
+
return bar;
|
|
925
|
+
} catch (e) { return null; }
|
|
926
|
+
}
|
|
927
|
+
/** 按当前状态渲染提示条(slow + 未关闭 → 显示;否则收起)。 */
|
|
928
|
+
function maLoadRenderBar() {
|
|
929
|
+
try {
|
|
930
|
+
if (!maLoadWatch.slow || maLoadWatch.dismissed) { maLoadHideBar(); return; }
|
|
931
|
+
var bar = maLoadWatch.barEl || maLoadMountBar();
|
|
932
|
+
if (!bar) return;
|
|
933
|
+
if (maLoadWatch.barTxt) maLoadWatch.barTxt.textContent = maLoadBarText();
|
|
934
|
+
bar.hidden = false;
|
|
935
|
+
if (!maLoadWatch.barTimer) {
|
|
936
|
+
// 每秒刷新「已等待 N 秒」;同时复核"是不是其实早就好了"——好了就自动收起(不长期挂着)。
|
|
937
|
+
maLoadWatch.barTimer = setInterval(function () {
|
|
938
|
+
try {
|
|
939
|
+
if (!maLoadWatch.slow || maLoadWatch.dismissed) { maLoadHideBar(); return; }
|
|
940
|
+
if (maLoadReadyNow()) {
|
|
941
|
+
maLoadWatch.slow = false;
|
|
942
|
+
maLoadHideBar();
|
|
943
|
+
maLoadRenderWarn(); // 菜单那一行也一起收起
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (maLoadWatch.barTxt) maLoadWatch.barTxt.textContent = maLoadBarText();
|
|
947
|
+
} catch (e) { /* 忽略 */ }
|
|
948
|
+
}, 1000);
|
|
949
|
+
try { if (maLoadWatch.barTimer && maLoadWatch.barTimer.unref) maLoadWatch.barTimer.unref(); } catch (e2) { /* 忽略 */ }
|
|
950
|
+
}
|
|
951
|
+
} catch (e) { /* 提示条失败绝不影响页面 */ }
|
|
952
|
+
}
|
|
751
953
|
function maLoadRenderWarn() {
|
|
752
954
|
try {
|
|
753
955
|
if (!maLoadWatch.warnEl) return;
|
|
754
|
-
|
|
956
|
+
// 用户点过 ✕ = 本页不想再被这事打扰:菜单里那一行也一起收起("只提示一次")
|
|
957
|
+
if (!maLoadWatch.slow || maLoadWatch.dismissed) { maLoadWatch.warnEl.hidden = true; return; }
|
|
755
958
|
if (maLoadWatch.warnTxt) {
|
|
756
959
|
maLoadWatch.warnTxt.textContent = "本次页面可能没加载完整(已等 " + maLoadWaitedSec()
|
|
757
960
|
+ " 秒)。点这里复制一句话发给客服,或点下方「返回设备列表」重来。";
|
|
@@ -785,18 +988,18 @@ const SCRIPT = `(() => {
|
|
|
785
988
|
}
|
|
786
989
|
} catch (e) { /* 忽略 */ }
|
|
787
990
|
}
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
// 必然是"≥25 秒",这是**删失下限**而不是真实耗时,后台只把它单列,不混进 P50/P90。
|
|
991
|
+
// 12 秒后仍认不出官方界面 → 置慢标记 + 懒创建底部提示条(**只在这时才碰 DOM**)。
|
|
992
|
+
// 同一处顺手把 mirror_load 的**兜底样本**发出去(outcome="slow"):12 秒没就绪的页面上报的
|
|
993
|
+
// 必然是"≥12 秒",这是**删失下限**而不是真实耗时,后台只把它单列,不混进 P50/P90。
|
|
792
994
|
setTimeout(function () {
|
|
793
995
|
try {
|
|
794
996
|
if (maLoadReadyNow()) return;
|
|
795
997
|
maLoadWatch.slow = true;
|
|
796
998
|
if (maLoadWatch.wired) maLoadRenderWarn();
|
|
999
|
+
maLoadRenderBar(); // 底部可操作提示条(重试 / 返回设备列表 / 复制诊断 / 关闭)
|
|
797
1000
|
maReportMirrorLoad("slow");
|
|
798
1001
|
} catch (e) { /* 忽略 */ }
|
|
799
|
-
},
|
|
1002
|
+
}, MA_LOAD_WATCH_MS);
|
|
800
1003
|
|
|
801
1004
|
try {
|
|
802
1005
|
const HOST_RE = new RegExp(${JSON.stringify(RUNTIME_HOST_RE.source)});
|
|
@@ -2511,13 +2714,24 @@ const SCRIPT = `(() => {
|
|
|
2511
2714
|
maObserver = null;
|
|
2512
2715
|
try { if (maInterval) clearInterval(maInterval); } catch (eD2) { /* 忽略 */ }
|
|
2513
2716
|
maInterval = 0;
|
|
2717
|
+
// 加载提示条的秒数刷新也一起停掉(页面卸载/进 bfcache 后不留悬挂回调)
|
|
2718
|
+
try { if (maLoadWatch.barTimer) { clearInterval(maLoadWatch.barTimer); maLoadWatch.barTimer = 0; } } catch (eD3) { /* 忽略 */ }
|
|
2514
2719
|
};
|
|
2515
2720
|
try { window.addEventListener("pagehide", maTeardown); } catch (ePh) { /* 忽略 */ }
|
|
2516
2721
|
try { window.addEventListener("unload", maTeardown); } catch (eUl) { /* 忽略 */ }
|
|
2517
2722
|
/* 手机上"切到别的 App 再切回来"走的常是 bfcache:pagehide(persisted=true) → pageshow(persisted=true)。
|
|
2518
|
-
DOM 原样保留但计时器/观察者已断 —— 这里重新武装,否则回到页面后看门狗就永久失灵了。
|
|
2723
|
+
DOM 原样保留但计时器/观察者已断 —— 这里重新武装,否则回到页面后看门狗就永久失灵了。
|
|
2724
|
+
2026-10-17:加载提示条的秒数刷新也在 maTeardown 里停了,回到前台要按现状重新挂上
|
|
2725
|
+
(否则用户切回来看的是"冻结的已等待 N 秒")。 */
|
|
2519
2726
|
try {
|
|
2520
|
-
window.addEventListener("pageshow", (ev) => {
|
|
2727
|
+
window.addEventListener("pageshow", (ev) => {
|
|
2728
|
+
try {
|
|
2729
|
+
if (ev && ev.persisted) {
|
|
2730
|
+
maArm();
|
|
2731
|
+
if (maLoadWatch.slow && !maLoadWatch.dismissed) maLoadRenderBar();
|
|
2732
|
+
}
|
|
2733
|
+
} catch (ePs) { /* 忽略 */ }
|
|
2734
|
+
});
|
|
2521
2735
|
} catch (ePs2) { /* 忽略 */ }
|
|
2522
2736
|
} catch (e) { if (window.console) console.warn("[dsh-mobile-adapter]", e && e.message); }
|
|
2523
2737
|
})();`;
|