@mrrisega/dsh-remote 0.7.2-beta.2 → 0.7.3-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.
|
@@ -554,6 +554,40 @@ 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-18 真机事故)=====
|
|
558
|
+
事故:iOS 16.1 / 17.3 的 Safari 打不开镜像页(缺 Promise.withResolvers / AbortSignal.any /
|
|
559
|
+
甚至不支持新语法)。补不了的 API 已在注入体最前面尽量补齐;补不了的(语法级)用这条**顶部轻量条**
|
|
560
|
+
明确告诉用户"升级系统或换浏览器",而不是让他干等。
|
|
561
|
+
安全边界:只占顶部一条、限高可滚、可关闭、不覆盖整屏、z-index 低于 FAB(400)——
|
|
562
|
+
和「加载卡住」底部条同一套安全方向:判断错最坏只是多一条可关的提示,绝不遮挡主界面。 */
|
|
563
|
+
div.dsh-ma-compatbar {
|
|
564
|
+
position: fixed; top: 0; left: 0; right: 0; z-index: 398;
|
|
565
|
+
box-sizing: border-box;
|
|
566
|
+
padding: calc(10px + env(safe-area-inset-top, 0px)) 14px 10px;
|
|
567
|
+
max-height: min(42vh, 300px); overflow: auto;
|
|
568
|
+
background: #fff8e6; color: #6b4a00;
|
|
569
|
+
border-bottom: 1px solid rgba(154, 103, 0, .35);
|
|
570
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, .12);
|
|
571
|
+
font: 400 12.5px/1.65 -apple-system, system-ui, sans-serif;
|
|
572
|
+
text-align: left;
|
|
573
|
+
}
|
|
574
|
+
div.dsh-ma-compatbar[hidden] { display: none !important; }
|
|
575
|
+
div.dsh-ma-compatbar .dsh-ma-compatbar-title { font-weight: 700; font-size: 13.5px; margin-bottom: 4px; }
|
|
576
|
+
div.dsh-ma-compatbar .dsh-ma-compatbar-detail { color: #7a5a10; word-break: break-word; }
|
|
577
|
+
div.dsh-ma-compatbar .dsh-ma-compatbar-hint { margin-top: 4px; color: #7a5a10; }
|
|
578
|
+
div.dsh-ma-compatbar .dsh-ma-compatbar-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
|
579
|
+
div.dsh-ma-compatbar button {
|
|
580
|
+
min-height: 40px; padding: 8px 12px; border-radius: 8px;
|
|
581
|
+
border: .5px solid rgba(154, 103, 0, .45); background: #fffdf5; color: #6b4a00;
|
|
582
|
+
font: 600 12.5px/1 -apple-system, system-ui, sans-serif; cursor: pointer;
|
|
583
|
+
}
|
|
584
|
+
div.dsh-ma-compatbar button.primary { background: #9a6700; border-color: #9a6700; color: #fff; }
|
|
585
|
+
div.dsh-ma-compatbar button.dsh-ma-compatbar-x {
|
|
586
|
+
position: absolute; top: calc(6px + env(safe-area-inset-top, 0px)); right: 8px;
|
|
587
|
+
min-height: 32px; min-width: 36px; padding: 4px 8px; border: none; background: transparent;
|
|
588
|
+
color: #7a5a10; font-size: 14px;
|
|
589
|
+
}
|
|
590
|
+
|
|
557
591
|
/* ===== 加载疑似卡住时的**可操作**底部提示条(2026-10-17 事故新增) =====
|
|
558
592
|
事故现场:用户本机 ~/.dsh-remote(bridge 运行时 + 账号配对信息/设备密钥)被误删 → bridge 起不来、
|
|
559
593
|
手机端镜像页第一次加载**干等 47 秒**才报错;页面上只有悬浮菜单里一行"已等 N 秒…复制给客服",
|
|
@@ -663,6 +697,112 @@ const FAB_PHONE_SVG =
|
|
|
663
697
|
const SCRIPT = `(() => {
|
|
664
698
|
"use strict";
|
|
665
699
|
|
|
700
|
+
/* ===DSH-MA-COMPAT-START===
|
|
701
|
+
旧浏览器兼容层(2026-10-18 真机事故)。
|
|
702
|
+
事故:iPhone iOS 16.1 / 17.3 的 Safari 打开镜像页永远卡住、出不来对话界面 ——
|
|
703
|
+
诊断上报抓到真实错误(mirror_diag):
|
|
704
|
+
· TypeError: Promise.withResolvers is not a function (Safari 17.4+ 才有)
|
|
705
|
+
· AbortSignal.any is not a function (Safari 17.4+ 才有)
|
|
706
|
+
· SyntaxError: Unexpected token '{'(iOS 16.1 连新语法都解析不了 → 只能提示升级)
|
|
707
|
+
DSH web 前端按现代浏览器构建;镜像页在**它的模块执行之前**补齐缺失的运行时 API,
|
|
708
|
+
能让 iOS 17.x / 老 Safari 直接可用;补不了的(语法级不支持)明确提示用户升级/换浏览器。
|
|
709
|
+
注入体纪律:本段不得出现反引号;只用 ES5 语法(老 Safari 也要能解析这段)。 */
|
|
710
|
+
|
|
711
|
+
// ① 运行时 API 补齐(全部有 typeof 守卫,绝不覆盖已有实现)
|
|
712
|
+
(function () {
|
|
713
|
+
try {
|
|
714
|
+
if (typeof Promise !== "undefined" && typeof Promise.withResolvers !== "function") {
|
|
715
|
+
Object.defineProperty(Promise, "withResolvers", {
|
|
716
|
+
configurable: true, writable: true,
|
|
717
|
+
value: function () {
|
|
718
|
+
var resolve, reject;
|
|
719
|
+
var promise = new Promise(function (res, rej) { resolve = res; reject = rej; });
|
|
720
|
+
return { promise: promise, resolve: resolve, reject: reject };
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
} catch (e) { /* 补齐失败不影响页面 */ }
|
|
725
|
+
try {
|
|
726
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.any !== "function") {
|
|
727
|
+
AbortSignal.any = function (signals) {
|
|
728
|
+
var ctrl = new AbortController();
|
|
729
|
+
var list = [];
|
|
730
|
+
try { list = Array.prototype.slice.call(signals || []); } catch (e2) { list = []; }
|
|
731
|
+
for (var i = 0; i < list.length; i++) {
|
|
732
|
+
try { if (list[i] && list[i].aborted) { ctrl.abort(list[i].reason); return ctrl.signal; } } catch (e3) { /* 忽略 */ }
|
|
733
|
+
}
|
|
734
|
+
function onAbort(e) {
|
|
735
|
+
var sig = e && e.target;
|
|
736
|
+
try { ctrl.abort(sig && sig.reason); } catch (e4) { try { ctrl.abort(); } catch (e5) { /* 忽略 */ } }
|
|
737
|
+
}
|
|
738
|
+
for (var j = 0; j < list.length; j++) {
|
|
739
|
+
try {
|
|
740
|
+
if (list[j] && typeof list[j].addEventListener === "function") {
|
|
741
|
+
list[j].addEventListener("abort", onAbort, { once: true });
|
|
742
|
+
}
|
|
743
|
+
} catch (e6) { /* 忽略 */ }
|
|
744
|
+
}
|
|
745
|
+
return ctrl.signal;
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
} catch (e) { /* 同上 */ }
|
|
749
|
+
try {
|
|
750
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout !== "function") {
|
|
751
|
+
AbortSignal.timeout = function (ms) {
|
|
752
|
+
var ctrl = new AbortController();
|
|
753
|
+
setTimeout(function () {
|
|
754
|
+
try { ctrl.abort(new Error("TimeoutError")); } catch (e2) { try { ctrl.abort(); } catch (e3) { /* 忽略 */ } }
|
|
755
|
+
}, Number(ms) || 0);
|
|
756
|
+
return ctrl.signal;
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
} catch (e) { /* 同上 */ }
|
|
760
|
+
try {
|
|
761
|
+
if (typeof Array.prototype.findLast !== "function") {
|
|
762
|
+
Array.prototype.findLast = function (fn, thisArg) {
|
|
763
|
+
for (var i = this.length - 1; i >= 0; i--) {
|
|
764
|
+
if (fn.call(thisArg, this[i], i, this)) return this[i];
|
|
765
|
+
}
|
|
766
|
+
return undefined;
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
if (typeof Array.prototype.findLastIndex !== "function") {
|
|
770
|
+
Array.prototype.findLastIndex = function (fn, thisArg) {
|
|
771
|
+
for (var i = this.length - 1; i >= 0; i--) {
|
|
772
|
+
if (fn.call(thisArg, this[i], i, this)) return i;
|
|
773
|
+
}
|
|
774
|
+
return -1;
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (typeof Array.prototype.toSorted !== "function") {
|
|
778
|
+
Array.prototype.toSorted = function (cmp) { return this.slice().sort(cmp); };
|
|
779
|
+
}
|
|
780
|
+
if (typeof Array.prototype.toReversed !== "function") {
|
|
781
|
+
Array.prototype.toReversed = function () { return this.slice().reverse(); };
|
|
782
|
+
}
|
|
783
|
+
if (typeof Object.hasOwn !== "function") {
|
|
784
|
+
Object.hasOwn = function (o, k) { return Object.prototype.hasOwnProperty.call(o, k); };
|
|
785
|
+
}
|
|
786
|
+
} catch (e) { /* 同上 */ }
|
|
787
|
+
})();
|
|
788
|
+
|
|
789
|
+
// ② 探测"补不了的":语法级不支持(老 Safari 解析不到新版构建)→ 只能提示升级/换浏览器
|
|
790
|
+
var maCompat = (function () {
|
|
791
|
+
var why = "";
|
|
792
|
+
try {
|
|
793
|
+
// 静态块(Safari 16.4+)。CSP 禁 eval 时抛的是 EvalError 而非 SyntaxError —— 不能误判
|
|
794
|
+
new Function("class __DshCompatProbe { static { this.ok = 1; } }");
|
|
795
|
+
} catch (e) {
|
|
796
|
+
if (e && e.name === "SyntaxError") why = "系统版本过旧(不支持现代 JavaScript 语法)";
|
|
797
|
+
}
|
|
798
|
+
var missing = [];
|
|
799
|
+
try { if (typeof Promise.withResolvers !== "function") missing.push("Promise.withResolvers"); } catch (e2) { /* 忽略 */ }
|
|
800
|
+
try { if (typeof AbortSignal !== "undefined" && typeof AbortSignal.any !== "function") missing.push("AbortSignal.any"); } catch (e3) { /* 忽略 */ }
|
|
801
|
+
if (!why && missing.length) why = "缺少浏览器能力:" + missing.join(" / ");
|
|
802
|
+
return { ok: !why, why: why };
|
|
803
|
+
})();
|
|
804
|
+
/* ===DSH-MA-COMPAT-END=== */
|
|
805
|
+
|
|
666
806
|
/* ===== 【0.6.16 已删除】首屏加载提示层(0.6.14 引入,0.6.15 仍在)=====
|
|
667
807
|
为什么删掉(用户实测,2026-09-25):「界面都已经加载完了,十几秒就把插件的接口加载好了,
|
|
668
808
|
但这个遮罩一直不消失」—— 截图里官方 UI 就在下面透出来,秒表走到 187 秒。
|
|
@@ -738,10 +878,121 @@ const SCRIPT = `(() => {
|
|
|
738
878
|
⚠️ 12 秒是"开始提示"的阈值,不是"判定失败":限速放开后正常首屏 1~3 秒,
|
|
739
879
|
12 秒既能及时帮到用户,也不至于刚打开就吓人(宁可晚一点,也别误报)。
|
|
740
880
|
⚠️ 注入体是模板字符串:本段注释/文案**一律用「」**,不得出现反引号。 */
|
|
881
|
+
try {
|
|
882
|
+
maWireCompatErrors();
|
|
883
|
+
if (maCompat && maCompat.ok === false) {
|
|
884
|
+
var maCompatKick = function () { setTimeout(function () { maShowCompatBar(); }, 800); };
|
|
885
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", maCompatKick);
|
|
886
|
+
else maCompatKick();
|
|
887
|
+
}
|
|
888
|
+
} catch (e) { /* 兼容提示失败绝不影响主功能 */ }
|
|
889
|
+
|
|
741
890
|
var maLoadWatch = { startedAt: Date.now(), slow: false, wired: false, bound: false, warnEl: null, warnTxt: null,
|
|
742
891
|
reported: false, reportMs: 0,
|
|
743
892
|
// 2026-10-17:底部可操作提示条(barEl = 懒创建的节点;barTimer = 秒数刷新)
|
|
744
893
|
barEl: null, barTxt: null, barTimer: 0, dismissed: false };
|
|
894
|
+
/* ===== 旧浏览器兼容提示条(2026-10-18)=====
|
|
895
|
+
只有两种触发:① 注入体最前面的 maCompat 探测到"补不了的"(语法级不支持);
|
|
896
|
+
② 页面脚本报出 SyntaxError / is not a function 这类典型兼容错误且界面还没就绪。
|
|
897
|
+
只显示一条、可关闭、不遮挡整屏;提供「返回设备列表」与「复制诊断信息」两个出口。 */
|
|
898
|
+
var maCompatBar = { el: null, txt: null, detail: null, dismissed: false, wired: false };
|
|
899
|
+
function maCompatReason(msg) {
|
|
900
|
+
var why = "";
|
|
901
|
+
try { why = String(msg ? msg : (maCompat && maCompat.why) || "").slice(0, 200); } catch (e) { why = ""; }
|
|
902
|
+
return why;
|
|
903
|
+
}
|
|
904
|
+
function maMountCompatBar(reason) {
|
|
905
|
+
try {
|
|
906
|
+
if (maCompatBar.dismissed) return;
|
|
907
|
+
if (maCompatBar.el && maCompatBar.el.parentNode) { maCompatRender(reason); return; }
|
|
908
|
+
if (!document.body) return;
|
|
909
|
+
var bar = document.createElement("div");
|
|
910
|
+
bar.className = "dsh-ma-compatbar";
|
|
911
|
+
bar.setAttribute("role", "alert");
|
|
912
|
+
|
|
913
|
+
var title = document.createElement("div");
|
|
914
|
+
title.className = "dsh-ma-compatbar-title";
|
|
915
|
+
title.textContent = "浏览器版本过旧,远程界面可能无法打开";
|
|
916
|
+
|
|
917
|
+
var detail = document.createElement("div");
|
|
918
|
+
detail.className = "dsh-ma-compatbar-detail";
|
|
919
|
+
|
|
920
|
+
var hint = document.createElement("div");
|
|
921
|
+
hint.className = "dsh-ma-compatbar-hint";
|
|
922
|
+
hint.textContent = "建议:把系统/浏览器升级到最新(iOS 17.4 及以上),或改用 Chrome、电脑端访问。";
|
|
923
|
+
|
|
924
|
+
var row = document.createElement("div");
|
|
925
|
+
row.className = "dsh-ma-compatbar-row";
|
|
926
|
+
var back = document.createElement("button");
|
|
927
|
+
back.className = "primary";
|
|
928
|
+
back.textContent = "返回设备列表";
|
|
929
|
+
back.addEventListener("click", function () {
|
|
930
|
+
try { location.href = "/app/"; } catch (e) { try { location.assign("/app/"); } catch (e2) { /* 忽略 */ } }
|
|
931
|
+
});
|
|
932
|
+
var copy = document.createElement("button");
|
|
933
|
+
copy.textContent = "复制诊断信息";
|
|
934
|
+
copy.addEventListener("click", function () {
|
|
935
|
+
var line = "";
|
|
936
|
+
try {
|
|
937
|
+
var ua = String((navigator && navigator.userAgent) || "").slice(0, 120);
|
|
938
|
+
line = "dsh-remote 浏览器兼容问题:缺少能力 = " + maCompatReason(reason) + ";UA=" + ua + ";页面=" + String(location.href || "").slice(0, 120);
|
|
939
|
+
} catch (e) { line = "dsh-remote 浏览器兼容问题(无法读取 UA)"; }
|
|
940
|
+
try {
|
|
941
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
942
|
+
navigator.clipboard.writeText(line).then(function () { hint.textContent = "已复制诊断信息,可直接发给客服。"; }, function () { hint.textContent = line; });
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
} catch (e2) { /* 落到下面 */ }
|
|
946
|
+
hint.textContent = line;
|
|
947
|
+
});
|
|
948
|
+
var x = document.createElement("button");
|
|
949
|
+
x.className = "dsh-ma-compatbar-x";
|
|
950
|
+
x.setAttribute("aria-label", "关闭提示");
|
|
951
|
+
x.textContent = "✕";
|
|
952
|
+
x.addEventListener("click", function () { maCompatBar.dismissed = true; try { bar.hidden = true; } catch (e) { /* 忽略 */ } });
|
|
953
|
+
|
|
954
|
+
row.appendChild(back);
|
|
955
|
+
row.appendChild(copy);
|
|
956
|
+
bar.appendChild(x);
|
|
957
|
+
bar.appendChild(title);
|
|
958
|
+
bar.appendChild(detail);
|
|
959
|
+
bar.appendChild(hint);
|
|
960
|
+
bar.appendChild(row);
|
|
961
|
+
document.body.appendChild(bar);
|
|
962
|
+
maCompatBar.el = bar; maCompatBar.txt = title; maCompatBar.detail = detail;
|
|
963
|
+
maCompatRender(reason);
|
|
964
|
+
} catch (e) { /* 提示失败绝不影响主功能 */ }
|
|
965
|
+
}
|
|
966
|
+
function maCompatRender(reason) {
|
|
967
|
+
try {
|
|
968
|
+
if (!maCompatBar.el) return;
|
|
969
|
+
var why = maCompatReason(reason);
|
|
970
|
+
if (why && maCompatBar.detail) maCompatBar.detail.textContent = "检测到:" + why;
|
|
971
|
+
maCompatBar.el.hidden = !!maCompatBar.dismissed;
|
|
972
|
+
} catch (e) { /* 忽略 */ }
|
|
973
|
+
}
|
|
974
|
+
/** 显示兼容提示(带就绪保护:界面已经能用了就不再打扰)。 */
|
|
975
|
+
function maShowCompatBar(reason) {
|
|
976
|
+
try {
|
|
977
|
+
if (maCompatBar.dismissed) return;
|
|
978
|
+
if (maLoadReadyNow()) return; // 页面其实正常 → 不吓人
|
|
979
|
+
maMountCompatBar(reason);
|
|
980
|
+
} catch (e) { /* 忽略 */ }
|
|
981
|
+
}
|
|
982
|
+
/** 页面脚本报出"典型兼容错误"时也要提示(有些内联报错早于我们的探测)。 */
|
|
983
|
+
function maWireCompatErrors() {
|
|
984
|
+
try {
|
|
985
|
+
window.addEventListener("error", function (e) {
|
|
986
|
+
try {
|
|
987
|
+
var msg = String((e && e.message) || "");
|
|
988
|
+
if (!/SyntaxError|Unexpected token|is not a function|Cannot read propert/i.test(msg)) return;
|
|
989
|
+
if (!/(withResolvers|AbortSignal|Promise|assets\\/|plugins\\/)/i.test(msg + " " + String((e.target && (e.target.src || e.target.href)) || ""))) return;
|
|
990
|
+
maShowCompatBar(msg.slice(0, 200));
|
|
991
|
+
} catch (e2) { /* 忽略 */ }
|
|
992
|
+
}, true);
|
|
993
|
+
} catch (e) { /* 忽略 */ }
|
|
994
|
+
}
|
|
995
|
+
|
|
745
996
|
/** 看门狗阈值(毫秒):12 秒仍认不出官方界面就开始提示。测试按常量断言,不要散落魔法数。 */
|
|
746
997
|
var MA_LOAD_WATCH_MS = 12 * 1000;
|
|
747
998
|
/** 官方界面是否已经可用(只看最强信号;只用于决定"要不要多显示一行字")。 */
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 镜像页「旧浏览器兼容层」(2026-10-18 真机事故)。
|
|
4
|
+
*
|
|
5
|
+
* 事故:用户在 iPhone(iOS 16.1 / 17.3)打开镜像页永远卡住、出不来对话界面。
|
|
6
|
+
* mirror_diag 上报抓到真实错误:
|
|
7
|
+
* · TypeError: Promise.withResolvers is not a function (Safari 17.4+ 才有)
|
|
8
|
+
* · AbortSignal.any is not a function (Safari 17.4+ 才有)
|
|
9
|
+
* · SyntaxError: Unexpected token '{'(iOS 16.1 连新语法都不支持 → 只能提示升级)
|
|
10
|
+
*
|
|
11
|
+
* 本文件锁三件事:
|
|
12
|
+
* ① 注入体最前面**真的补齐**了这两个运行时 API(vm 里删掉原生实现后跑,断言可用);
|
|
13
|
+
* ② 语法级不支持 / 典型兼容报错时,会显示**顶部轻量提示条**(可关闭、非全屏、z-index 低于 FAB);
|
|
14
|
+
* ③ 提示条只在"界面没就绪"时出现,绝不打扰正常页面。
|
|
15
|
+
*/
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
import vm from "node:vm";
|
|
19
|
+
import { injectMobileAdapter } from "../mobile-adapter.mjs";
|
|
20
|
+
|
|
21
|
+
/** 取注入脚本原文。 */
|
|
22
|
+
function adapterScript() {
|
|
23
|
+
const { html } = injectMobileAdapter("<!doctype html><html><head></head><body></body></html>");
|
|
24
|
+
const m = /<script id="[^"]*-js"[^>]*>([\s\S]*?)<\/script>/.exec(html);
|
|
25
|
+
assert.ok(m, "应能从注入结果里取出脚本");
|
|
26
|
+
return m[1];
|
|
27
|
+
}
|
|
28
|
+
function adapterCss() {
|
|
29
|
+
const { html } = injectMobileAdapter("<!doctype html><html><head></head><body></body></html>");
|
|
30
|
+
const m = /<style id="[^"]*-css"[^>]*>([\s\S]*?)<\/style>/.exec(html);
|
|
31
|
+
assert.ok(m, "应能从注入结果里取出样式");
|
|
32
|
+
return m[1];
|
|
33
|
+
}
|
|
34
|
+
/** 只取兼容层代码段(有 marker,便于独立执行)。 */
|
|
35
|
+
function compatBlock() {
|
|
36
|
+
const src = adapterScript();
|
|
37
|
+
const i = src.indexOf("===DSH-MA-COMPAT-START===");
|
|
38
|
+
const j = src.indexOf("===DSH-MA-COMPAT-END===");
|
|
39
|
+
assert.ok(i > 0 && j > i, "兼容层应有 START/END 标记");
|
|
40
|
+
const bodyStart = src.indexOf("*/", i) + 2; // START 注释结束
|
|
41
|
+
const bodyEnd = src.lastIndexOf("/*", j); // END 注释开始(不要把它带进来)
|
|
42
|
+
return src.slice(bodyStart, bodyEnd);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("★ 补齐 Promise.withResolvers 与 AbortSignal.any(老 Safari 缺这两项 → 页面永远起不来)", async () => {
|
|
46
|
+
// 造一个"老 Safari"环境:独立 fake realm —— 不污染宿主全局(Promise 无 withResolvers、
|
|
47
|
+
// AbortSignal 无 any;AbortController/Signal 行为与浏览器一致:abort 会派发 abort 事件)。
|
|
48
|
+
const listeners = new WeakMap();
|
|
49
|
+
class FakeAbortSignal {
|
|
50
|
+
constructor() { this.aborted = false; this.reason = undefined; }
|
|
51
|
+
addEventListener(type, fn) {
|
|
52
|
+
if (type !== "abort") return;
|
|
53
|
+
const arr = listeners.get(this) || [];
|
|
54
|
+
arr.push(fn);
|
|
55
|
+
listeners.set(this, arr);
|
|
56
|
+
}
|
|
57
|
+
removeEventListener() {}
|
|
58
|
+
}
|
|
59
|
+
class FakeAbortController {
|
|
60
|
+
constructor() { this.signal = new FakeAbortSignal(); }
|
|
61
|
+
abort(reason) {
|
|
62
|
+
const sig = this.signal;
|
|
63
|
+
if (sig.aborted) return;
|
|
64
|
+
sig.aborted = true;
|
|
65
|
+
sig.reason = reason;
|
|
66
|
+
for (const fn of listeners.get(sig) || []) {
|
|
67
|
+
try { fn({ target: sig }); } catch (e) { /* 忽略 */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const sandbox = { console, setTimeout, clearTimeout, AbortSignal: FakeAbortSignal, AbortController: FakeAbortController, Array, Object };
|
|
72
|
+
sandbox.globalThis = sandbox;
|
|
73
|
+
// 老 Safari:没有 withResolvers。用包装函数暴露 Promise 构造器(不带静态 withResolvers)
|
|
74
|
+
sandbox.Promise = function PromiseCompat(executor) { return new Promise(executor); };
|
|
75
|
+
|
|
76
|
+
vm.runInNewContext(compatBlock(), sandbox, { timeout: 5000 });
|
|
77
|
+
|
|
78
|
+
// ① withResolvers:结构正确、能 resolve
|
|
79
|
+
assert.equal(typeof sandbox.Promise.withResolvers, "function", "必须补出 Promise.withResolvers");
|
|
80
|
+
const wr = sandbox.Promise.withResolvers();
|
|
81
|
+
assert.equal(typeof wr.resolve, "function");
|
|
82
|
+
assert.equal(typeof wr.reject, "function");
|
|
83
|
+
assert.ok(wr.promise instanceof Promise);
|
|
84
|
+
let got = null;
|
|
85
|
+
wr.promise.then((v) => { got = v; });
|
|
86
|
+
wr.resolve(42);
|
|
87
|
+
await new Promise((r) => setImmediate(r));
|
|
88
|
+
assert.equal(got, 42, "补齐后的 promise 必须真的能用");
|
|
89
|
+
|
|
90
|
+
// ② AbortSignal.any:任一 signal 中断 → 合并 signal 跟着中断
|
|
91
|
+
assert.equal(typeof sandbox.AbortSignal.any, "function", "必须补出 AbortSignal.any");
|
|
92
|
+
const c1 = new sandbox.AbortController();
|
|
93
|
+
const c2 = new sandbox.AbortController();
|
|
94
|
+
const combined = sandbox.AbortSignal.any([c1.signal, c2.signal]);
|
|
95
|
+
assert.equal(combined.aborted, false);
|
|
96
|
+
c2.abort();
|
|
97
|
+
assert.equal(combined.aborted, true, "其中一个 abort → 合并 signal 必须 aborted");
|
|
98
|
+
|
|
99
|
+
// ③ 已经 aborted 的输入 → 立即返回 aborted 的 signal
|
|
100
|
+
const c3 = new sandbox.AbortController();
|
|
101
|
+
c3.abort();
|
|
102
|
+
assert.equal(sandbox.AbortSignal.any([c3.signal]).aborted, true);
|
|
103
|
+
|
|
104
|
+
// ④ 常用 ES2023 方法兜底(这些也在老 Safari 上缺失)
|
|
105
|
+
assert.equal(typeof sandbox.Array.prototype.findLast, "function");
|
|
106
|
+
assert.equal(typeof sandbox.Array.prototype.toSorted, "function");
|
|
107
|
+
assert.equal(typeof sandbox.Object.hasOwn, "function");
|
|
108
|
+
assert.equal(vm.runInNewContext("[1,2,3].findLast(function(n){return n<3})", sandbox), 2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("★ 语法级不支持(老 Safari 解析不了新语法)→ 探测为不兼容并给出原因", () => {
|
|
112
|
+
const sandbox = { console, setTimeout, clearTimeout };
|
|
113
|
+
sandbox.globalThis = sandbox;
|
|
114
|
+
sandbox.Promise = Promise;
|
|
115
|
+
sandbox.AbortSignal = AbortSignal;
|
|
116
|
+
// 模拟"新语法解析失败":Function 构造器抛 SyntaxError(CSP 场景抛 EvalError 不应误判 —— 见下一条)
|
|
117
|
+
sandbox.Function = function () { const e = new SyntaxError("Unexpected token '{'"); throw e; };
|
|
118
|
+
vm.runInNewContext(compatBlock(), sandbox, { timeout: 5000 });
|
|
119
|
+
const compat = vm.runInNewContext("maCompat", sandbox);
|
|
120
|
+
assert.equal(compat.ok, false, "语法不支持必须判为不兼容");
|
|
121
|
+
assert.match(compat.why, /系统版本过旧/, "要给出人能看懂的原因(系统版本过旧)");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("★ CSP 禁止 eval 抛 EvalError 时**不得**误判为老浏览器(否则正常用户会被吓到)", () => {
|
|
125
|
+
const sandbox = { console, setTimeout, clearTimeout };
|
|
126
|
+
sandbox.globalThis = sandbox;
|
|
127
|
+
sandbox.Promise = Promise;
|
|
128
|
+
sandbox.AbortSignal = AbortSignal;
|
|
129
|
+
// 补上 polyfill,确保唯一失败点是 EvalError
|
|
130
|
+
sandbox.Function = function () { const e = new Error("Blocked by CSP"); e.name = "EvalError"; throw e; };
|
|
131
|
+
// 让 withResolvers 存在,避免被当成缺失
|
|
132
|
+
sandbox.Promise = Object.assign(Promise, { withResolvers: Promise.withResolvers || function () { let a, b; const p = new Promise((r, j) => { a = r; b = j; }); return { promise: p, resolve: a, reject: b }; } });
|
|
133
|
+
vm.runInNewContext(compatBlock(), sandbox, { timeout: 5000 });
|
|
134
|
+
const compat = vm.runInNewContext("maCompat", sandbox);
|
|
135
|
+
assert.equal(typeof compat, "object");
|
|
136
|
+
// EvalError 不应产生"系统版本过旧"
|
|
137
|
+
assert.ok(!/系统版本过旧/.test(compat.why || ""), `CSP 的 EvalError 不得被判成语法不支持,实际 why=${compat.why}`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("★ 兼容提示条:顶部轻量条(非全屏、可关闭、z-index 低于 FAB),两个出口按钮齐全", () => {
|
|
141
|
+
const src = adapterScript();
|
|
142
|
+
const css = adapterCss();
|
|
143
|
+
assert.match(src, /dsh-ma-compatbar/, "注入体必须有兼容提示条");
|
|
144
|
+
assert.match(css, /div\.dsh-ma-compatbar\s*\{/, "必须有对应样式");
|
|
145
|
+
// 顶部轻量条:fixed top,限高;不得全屏
|
|
146
|
+
const barCss = css.slice(css.indexOf("div.dsh-ma-compatbar {"), css.indexOf("div.dsh-ma-compatbar[hidden]"));
|
|
147
|
+
assert.match(barCss, /position:\s*fixed/, "提示条应为浮层");
|
|
148
|
+
assert.match(barCss, /top:\s*0/, "应贴顶部(不挡对话区操作)");
|
|
149
|
+
assert.match(barCss, /max-height:/, "必须限高,不能占满屏");
|
|
150
|
+
assert.doesNotMatch(barCss, /(height|inset):\s*(100vh|0;)/, "不得写成全屏覆盖层(历史事故:全屏层曾永久盖住界面)");
|
|
151
|
+
// z-index 低于 FAB(400),保证悬浮菜单永远可点
|
|
152
|
+
const z = /z-index:\s*(\d+)/.exec(barCss);
|
|
153
|
+
assert.ok(z && Number(z[1]) < 400, `z-index 必须低于 FAB 400,实际 ${z && z[1]}`);
|
|
154
|
+
// 关闭按钮 + 两个出口
|
|
155
|
+
assert.match(src, /dsh-ma-compatbar-x/, "必须有 ✕ 关闭");
|
|
156
|
+
assert.match(src, /返回设备列表/, "必须有「返回设备列表」出口");
|
|
157
|
+
assert.match(src, /复制诊断信息/, "必须有「复制诊断信息」出口");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("★ 提示只在界面没就绪时出现(正常页面绝不被这条打扰)", () => {
|
|
161
|
+
const src = adapterScript();
|
|
162
|
+
const fn = src.slice(src.indexOf("function maShowCompatBar("), src.indexOf("/** 页面脚本报出"));
|
|
163
|
+
assert.match(fn, /maLoadReadyNow\(\)/, "必须先判断页面已就绪,就绪则不提示");
|
|
164
|
+
assert.match(fn, /maLoadReadyNow\(\)\) return/, "就绪时直接 return");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("★ 注入体完整性:无 </script>、可编译(老浏览器兜底代码不能自己把文档截断)", () => {
|
|
168
|
+
const src = adapterScript();
|
|
169
|
+
assert.ok(!/<\/script/i.test(src), "注入体不得含 </script>");
|
|
170
|
+
assert.doesNotThrow(() => new vm.Script(src), "注入体必须能被解析(否则页面直接白屏)");
|
|
171
|
+
// 兼容层必须出现在脚本最前面(要在应用模块执行前补齐 API)
|
|
172
|
+
const compatIdx = src.indexOf("===DSH-MA-COMPAT-START===");
|
|
173
|
+
const firstAppRef = src.indexOf("__DSH_TRANSPORT__");
|
|
174
|
+
assert.ok(compatIdx > 0 && compatIdx < firstAppRef, "兼容层必须排在宿主相关逻辑之前(模块执行前)");
|
|
175
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3-beta.1",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-web",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3-beta.1",
|
|
4
4
|
"description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -554,6 +554,40 @@ 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-18 真机事故)=====
|
|
558
|
+
事故:iOS 16.1 / 17.3 的 Safari 打不开镜像页(缺 Promise.withResolvers / AbortSignal.any /
|
|
559
|
+
甚至不支持新语法)。补不了的 API 已在注入体最前面尽量补齐;补不了的(语法级)用这条**顶部轻量条**
|
|
560
|
+
明确告诉用户"升级系统或换浏览器",而不是让他干等。
|
|
561
|
+
安全边界:只占顶部一条、限高可滚、可关闭、不覆盖整屏、z-index 低于 FAB(400)——
|
|
562
|
+
和「加载卡住」底部条同一套安全方向:判断错最坏只是多一条可关的提示,绝不遮挡主界面。 */
|
|
563
|
+
div.dsh-ma-compatbar {
|
|
564
|
+
position: fixed; top: 0; left: 0; right: 0; z-index: 398;
|
|
565
|
+
box-sizing: border-box;
|
|
566
|
+
padding: calc(10px + env(safe-area-inset-top, 0px)) 14px 10px;
|
|
567
|
+
max-height: min(42vh, 300px); overflow: auto;
|
|
568
|
+
background: #fff8e6; color: #6b4a00;
|
|
569
|
+
border-bottom: 1px solid rgba(154, 103, 0, .35);
|
|
570
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, .12);
|
|
571
|
+
font: 400 12.5px/1.65 -apple-system, system-ui, sans-serif;
|
|
572
|
+
text-align: left;
|
|
573
|
+
}
|
|
574
|
+
div.dsh-ma-compatbar[hidden] { display: none !important; }
|
|
575
|
+
div.dsh-ma-compatbar .dsh-ma-compatbar-title { font-weight: 700; font-size: 13.5px; margin-bottom: 4px; }
|
|
576
|
+
div.dsh-ma-compatbar .dsh-ma-compatbar-detail { color: #7a5a10; word-break: break-word; }
|
|
577
|
+
div.dsh-ma-compatbar .dsh-ma-compatbar-hint { margin-top: 4px; color: #7a5a10; }
|
|
578
|
+
div.dsh-ma-compatbar .dsh-ma-compatbar-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
|
579
|
+
div.dsh-ma-compatbar button {
|
|
580
|
+
min-height: 40px; padding: 8px 12px; border-radius: 8px;
|
|
581
|
+
border: .5px solid rgba(154, 103, 0, .45); background: #fffdf5; color: #6b4a00;
|
|
582
|
+
font: 600 12.5px/1 -apple-system, system-ui, sans-serif; cursor: pointer;
|
|
583
|
+
}
|
|
584
|
+
div.dsh-ma-compatbar button.primary { background: #9a6700; border-color: #9a6700; color: #fff; }
|
|
585
|
+
div.dsh-ma-compatbar button.dsh-ma-compatbar-x {
|
|
586
|
+
position: absolute; top: calc(6px + env(safe-area-inset-top, 0px)); right: 8px;
|
|
587
|
+
min-height: 32px; min-width: 36px; padding: 4px 8px; border: none; background: transparent;
|
|
588
|
+
color: #7a5a10; font-size: 14px;
|
|
589
|
+
}
|
|
590
|
+
|
|
557
591
|
/* ===== 加载疑似卡住时的**可操作**底部提示条(2026-10-17 事故新增) =====
|
|
558
592
|
事故现场:用户本机 ~/.dsh-remote(bridge 运行时 + 账号配对信息/设备密钥)被误删 → bridge 起不来、
|
|
559
593
|
手机端镜像页第一次加载**干等 47 秒**才报错;页面上只有悬浮菜单里一行"已等 N 秒…复制给客服",
|
|
@@ -663,6 +697,112 @@ const FAB_PHONE_SVG =
|
|
|
663
697
|
const SCRIPT = `(() => {
|
|
664
698
|
"use strict";
|
|
665
699
|
|
|
700
|
+
/* ===DSH-MA-COMPAT-START===
|
|
701
|
+
旧浏览器兼容层(2026-10-18 真机事故)。
|
|
702
|
+
事故:iPhone iOS 16.1 / 17.3 的 Safari 打开镜像页永远卡住、出不来对话界面 ——
|
|
703
|
+
诊断上报抓到真实错误(mirror_diag):
|
|
704
|
+
· TypeError: Promise.withResolvers is not a function (Safari 17.4+ 才有)
|
|
705
|
+
· AbortSignal.any is not a function (Safari 17.4+ 才有)
|
|
706
|
+
· SyntaxError: Unexpected token '{'(iOS 16.1 连新语法都解析不了 → 只能提示升级)
|
|
707
|
+
DSH web 前端按现代浏览器构建;镜像页在**它的模块执行之前**补齐缺失的运行时 API,
|
|
708
|
+
能让 iOS 17.x / 老 Safari 直接可用;补不了的(语法级不支持)明确提示用户升级/换浏览器。
|
|
709
|
+
注入体纪律:本段不得出现反引号;只用 ES5 语法(老 Safari 也要能解析这段)。 */
|
|
710
|
+
|
|
711
|
+
// ① 运行时 API 补齐(全部有 typeof 守卫,绝不覆盖已有实现)
|
|
712
|
+
(function () {
|
|
713
|
+
try {
|
|
714
|
+
if (typeof Promise !== "undefined" && typeof Promise.withResolvers !== "function") {
|
|
715
|
+
Object.defineProperty(Promise, "withResolvers", {
|
|
716
|
+
configurable: true, writable: true,
|
|
717
|
+
value: function () {
|
|
718
|
+
var resolve, reject;
|
|
719
|
+
var promise = new Promise(function (res, rej) { resolve = res; reject = rej; });
|
|
720
|
+
return { promise: promise, resolve: resolve, reject: reject };
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
} catch (e) { /* 补齐失败不影响页面 */ }
|
|
725
|
+
try {
|
|
726
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.any !== "function") {
|
|
727
|
+
AbortSignal.any = function (signals) {
|
|
728
|
+
var ctrl = new AbortController();
|
|
729
|
+
var list = [];
|
|
730
|
+
try { list = Array.prototype.slice.call(signals || []); } catch (e2) { list = []; }
|
|
731
|
+
for (var i = 0; i < list.length; i++) {
|
|
732
|
+
try { if (list[i] && list[i].aborted) { ctrl.abort(list[i].reason); return ctrl.signal; } } catch (e3) { /* 忽略 */ }
|
|
733
|
+
}
|
|
734
|
+
function onAbort(e) {
|
|
735
|
+
var sig = e && e.target;
|
|
736
|
+
try { ctrl.abort(sig && sig.reason); } catch (e4) { try { ctrl.abort(); } catch (e5) { /* 忽略 */ } }
|
|
737
|
+
}
|
|
738
|
+
for (var j = 0; j < list.length; j++) {
|
|
739
|
+
try {
|
|
740
|
+
if (list[j] && typeof list[j].addEventListener === "function") {
|
|
741
|
+
list[j].addEventListener("abort", onAbort, { once: true });
|
|
742
|
+
}
|
|
743
|
+
} catch (e6) { /* 忽略 */ }
|
|
744
|
+
}
|
|
745
|
+
return ctrl.signal;
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
} catch (e) { /* 同上 */ }
|
|
749
|
+
try {
|
|
750
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout !== "function") {
|
|
751
|
+
AbortSignal.timeout = function (ms) {
|
|
752
|
+
var ctrl = new AbortController();
|
|
753
|
+
setTimeout(function () {
|
|
754
|
+
try { ctrl.abort(new Error("TimeoutError")); } catch (e2) { try { ctrl.abort(); } catch (e3) { /* 忽略 */ } }
|
|
755
|
+
}, Number(ms) || 0);
|
|
756
|
+
return ctrl.signal;
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
} catch (e) { /* 同上 */ }
|
|
760
|
+
try {
|
|
761
|
+
if (typeof Array.prototype.findLast !== "function") {
|
|
762
|
+
Array.prototype.findLast = function (fn, thisArg) {
|
|
763
|
+
for (var i = this.length - 1; i >= 0; i--) {
|
|
764
|
+
if (fn.call(thisArg, this[i], i, this)) return this[i];
|
|
765
|
+
}
|
|
766
|
+
return undefined;
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
if (typeof Array.prototype.findLastIndex !== "function") {
|
|
770
|
+
Array.prototype.findLastIndex = function (fn, thisArg) {
|
|
771
|
+
for (var i = this.length - 1; i >= 0; i--) {
|
|
772
|
+
if (fn.call(thisArg, this[i], i, this)) return i;
|
|
773
|
+
}
|
|
774
|
+
return -1;
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (typeof Array.prototype.toSorted !== "function") {
|
|
778
|
+
Array.prototype.toSorted = function (cmp) { return this.slice().sort(cmp); };
|
|
779
|
+
}
|
|
780
|
+
if (typeof Array.prototype.toReversed !== "function") {
|
|
781
|
+
Array.prototype.toReversed = function () { return this.slice().reverse(); };
|
|
782
|
+
}
|
|
783
|
+
if (typeof Object.hasOwn !== "function") {
|
|
784
|
+
Object.hasOwn = function (o, k) { return Object.prototype.hasOwnProperty.call(o, k); };
|
|
785
|
+
}
|
|
786
|
+
} catch (e) { /* 同上 */ }
|
|
787
|
+
})();
|
|
788
|
+
|
|
789
|
+
// ② 探测"补不了的":语法级不支持(老 Safari 解析不到新版构建)→ 只能提示升级/换浏览器
|
|
790
|
+
var maCompat = (function () {
|
|
791
|
+
var why = "";
|
|
792
|
+
try {
|
|
793
|
+
// 静态块(Safari 16.4+)。CSP 禁 eval 时抛的是 EvalError 而非 SyntaxError —— 不能误判
|
|
794
|
+
new Function("class __DshCompatProbe { static { this.ok = 1; } }");
|
|
795
|
+
} catch (e) {
|
|
796
|
+
if (e && e.name === "SyntaxError") why = "系统版本过旧(不支持现代 JavaScript 语法)";
|
|
797
|
+
}
|
|
798
|
+
var missing = [];
|
|
799
|
+
try { if (typeof Promise.withResolvers !== "function") missing.push("Promise.withResolvers"); } catch (e2) { /* 忽略 */ }
|
|
800
|
+
try { if (typeof AbortSignal !== "undefined" && typeof AbortSignal.any !== "function") missing.push("AbortSignal.any"); } catch (e3) { /* 忽略 */ }
|
|
801
|
+
if (!why && missing.length) why = "缺少浏览器能力:" + missing.join(" / ");
|
|
802
|
+
return { ok: !why, why: why };
|
|
803
|
+
})();
|
|
804
|
+
/* ===DSH-MA-COMPAT-END=== */
|
|
805
|
+
|
|
666
806
|
/* ===== 【0.6.16 已删除】首屏加载提示层(0.6.14 引入,0.6.15 仍在)=====
|
|
667
807
|
为什么删掉(用户实测,2026-09-25):「界面都已经加载完了,十几秒就把插件的接口加载好了,
|
|
668
808
|
但这个遮罩一直不消失」—— 截图里官方 UI 就在下面透出来,秒表走到 187 秒。
|
|
@@ -738,10 +878,121 @@ const SCRIPT = `(() => {
|
|
|
738
878
|
⚠️ 12 秒是"开始提示"的阈值,不是"判定失败":限速放开后正常首屏 1~3 秒,
|
|
739
879
|
12 秒既能及时帮到用户,也不至于刚打开就吓人(宁可晚一点,也别误报)。
|
|
740
880
|
⚠️ 注入体是模板字符串:本段注释/文案**一律用「」**,不得出现反引号。 */
|
|
881
|
+
try {
|
|
882
|
+
maWireCompatErrors();
|
|
883
|
+
if (maCompat && maCompat.ok === false) {
|
|
884
|
+
var maCompatKick = function () { setTimeout(function () { maShowCompatBar(); }, 800); };
|
|
885
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", maCompatKick);
|
|
886
|
+
else maCompatKick();
|
|
887
|
+
}
|
|
888
|
+
} catch (e) { /* 兼容提示失败绝不影响主功能 */ }
|
|
889
|
+
|
|
741
890
|
var maLoadWatch = { startedAt: Date.now(), slow: false, wired: false, bound: false, warnEl: null, warnTxt: null,
|
|
742
891
|
reported: false, reportMs: 0,
|
|
743
892
|
// 2026-10-17:底部可操作提示条(barEl = 懒创建的节点;barTimer = 秒数刷新)
|
|
744
893
|
barEl: null, barTxt: null, barTimer: 0, dismissed: false };
|
|
894
|
+
/* ===== 旧浏览器兼容提示条(2026-10-18)=====
|
|
895
|
+
只有两种触发:① 注入体最前面的 maCompat 探测到"补不了的"(语法级不支持);
|
|
896
|
+
② 页面脚本报出 SyntaxError / is not a function 这类典型兼容错误且界面还没就绪。
|
|
897
|
+
只显示一条、可关闭、不遮挡整屏;提供「返回设备列表」与「复制诊断信息」两个出口。 */
|
|
898
|
+
var maCompatBar = { el: null, txt: null, detail: null, dismissed: false, wired: false };
|
|
899
|
+
function maCompatReason(msg) {
|
|
900
|
+
var why = "";
|
|
901
|
+
try { why = String(msg ? msg : (maCompat && maCompat.why) || "").slice(0, 200); } catch (e) { why = ""; }
|
|
902
|
+
return why;
|
|
903
|
+
}
|
|
904
|
+
function maMountCompatBar(reason) {
|
|
905
|
+
try {
|
|
906
|
+
if (maCompatBar.dismissed) return;
|
|
907
|
+
if (maCompatBar.el && maCompatBar.el.parentNode) { maCompatRender(reason); return; }
|
|
908
|
+
if (!document.body) return;
|
|
909
|
+
var bar = document.createElement("div");
|
|
910
|
+
bar.className = "dsh-ma-compatbar";
|
|
911
|
+
bar.setAttribute("role", "alert");
|
|
912
|
+
|
|
913
|
+
var title = document.createElement("div");
|
|
914
|
+
title.className = "dsh-ma-compatbar-title";
|
|
915
|
+
title.textContent = "浏览器版本过旧,远程界面可能无法打开";
|
|
916
|
+
|
|
917
|
+
var detail = document.createElement("div");
|
|
918
|
+
detail.className = "dsh-ma-compatbar-detail";
|
|
919
|
+
|
|
920
|
+
var hint = document.createElement("div");
|
|
921
|
+
hint.className = "dsh-ma-compatbar-hint";
|
|
922
|
+
hint.textContent = "建议:把系统/浏览器升级到最新(iOS 17.4 及以上),或改用 Chrome、电脑端访问。";
|
|
923
|
+
|
|
924
|
+
var row = document.createElement("div");
|
|
925
|
+
row.className = "dsh-ma-compatbar-row";
|
|
926
|
+
var back = document.createElement("button");
|
|
927
|
+
back.className = "primary";
|
|
928
|
+
back.textContent = "返回设备列表";
|
|
929
|
+
back.addEventListener("click", function () {
|
|
930
|
+
try { location.href = "/app/"; } catch (e) { try { location.assign("/app/"); } catch (e2) { /* 忽略 */ } }
|
|
931
|
+
});
|
|
932
|
+
var copy = document.createElement("button");
|
|
933
|
+
copy.textContent = "复制诊断信息";
|
|
934
|
+
copy.addEventListener("click", function () {
|
|
935
|
+
var line = "";
|
|
936
|
+
try {
|
|
937
|
+
var ua = String((navigator && navigator.userAgent) || "").slice(0, 120);
|
|
938
|
+
line = "dsh-remote 浏览器兼容问题:缺少能力 = " + maCompatReason(reason) + ";UA=" + ua + ";页面=" + String(location.href || "").slice(0, 120);
|
|
939
|
+
} catch (e) { line = "dsh-remote 浏览器兼容问题(无法读取 UA)"; }
|
|
940
|
+
try {
|
|
941
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
942
|
+
navigator.clipboard.writeText(line).then(function () { hint.textContent = "已复制诊断信息,可直接发给客服。"; }, function () { hint.textContent = line; });
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
} catch (e2) { /* 落到下面 */ }
|
|
946
|
+
hint.textContent = line;
|
|
947
|
+
});
|
|
948
|
+
var x = document.createElement("button");
|
|
949
|
+
x.className = "dsh-ma-compatbar-x";
|
|
950
|
+
x.setAttribute("aria-label", "关闭提示");
|
|
951
|
+
x.textContent = "✕";
|
|
952
|
+
x.addEventListener("click", function () { maCompatBar.dismissed = true; try { bar.hidden = true; } catch (e) { /* 忽略 */ } });
|
|
953
|
+
|
|
954
|
+
row.appendChild(back);
|
|
955
|
+
row.appendChild(copy);
|
|
956
|
+
bar.appendChild(x);
|
|
957
|
+
bar.appendChild(title);
|
|
958
|
+
bar.appendChild(detail);
|
|
959
|
+
bar.appendChild(hint);
|
|
960
|
+
bar.appendChild(row);
|
|
961
|
+
document.body.appendChild(bar);
|
|
962
|
+
maCompatBar.el = bar; maCompatBar.txt = title; maCompatBar.detail = detail;
|
|
963
|
+
maCompatRender(reason);
|
|
964
|
+
} catch (e) { /* 提示失败绝不影响主功能 */ }
|
|
965
|
+
}
|
|
966
|
+
function maCompatRender(reason) {
|
|
967
|
+
try {
|
|
968
|
+
if (!maCompatBar.el) return;
|
|
969
|
+
var why = maCompatReason(reason);
|
|
970
|
+
if (why && maCompatBar.detail) maCompatBar.detail.textContent = "检测到:" + why;
|
|
971
|
+
maCompatBar.el.hidden = !!maCompatBar.dismissed;
|
|
972
|
+
} catch (e) { /* 忽略 */ }
|
|
973
|
+
}
|
|
974
|
+
/** 显示兼容提示(带就绪保护:界面已经能用了就不再打扰)。 */
|
|
975
|
+
function maShowCompatBar(reason) {
|
|
976
|
+
try {
|
|
977
|
+
if (maCompatBar.dismissed) return;
|
|
978
|
+
if (maLoadReadyNow()) return; // 页面其实正常 → 不吓人
|
|
979
|
+
maMountCompatBar(reason);
|
|
980
|
+
} catch (e) { /* 忽略 */ }
|
|
981
|
+
}
|
|
982
|
+
/** 页面脚本报出"典型兼容错误"时也要提示(有些内联报错早于我们的探测)。 */
|
|
983
|
+
function maWireCompatErrors() {
|
|
984
|
+
try {
|
|
985
|
+
window.addEventListener("error", function (e) {
|
|
986
|
+
try {
|
|
987
|
+
var msg = String((e && e.message) || "");
|
|
988
|
+
if (!/SyntaxError|Unexpected token|is not a function|Cannot read propert/i.test(msg)) return;
|
|
989
|
+
if (!/(withResolvers|AbortSignal|Promise|assets\\/|plugins\\/)/i.test(msg + " " + String((e.target && (e.target.src || e.target.href)) || ""))) return;
|
|
990
|
+
maShowCompatBar(msg.slice(0, 200));
|
|
991
|
+
} catch (e2) { /* 忽略 */ }
|
|
992
|
+
}, true);
|
|
993
|
+
} catch (e) { /* 忽略 */ }
|
|
994
|
+
}
|
|
995
|
+
|
|
745
996
|
/** 看门狗阈值(毫秒):12 秒仍认不出官方界面就开始提示。测试按常量断言,不要散落魔法数。 */
|
|
746
997
|
var MA_LOAD_WATCH_MS = 12 * 1000;
|
|
747
998
|
/** 官方界面是否已经可用(只看最强信号;只用于决定"要不要多显示一行字")。 */
|