@mrrisega/dsh-remote 0.7.0-beta.6 → 0.7.0-beta.7
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/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +170 -4
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/test/desktop-compat.test.mjs +4 -1
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +2 -1
- package/packages/dsh-remote-web/test/settings-open-path.test.mjs +102 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.7.0-beta.
|
|
3
|
+
"version": "0.7.0-beta.7",
|
|
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",
|
|
@@ -848,8 +848,16 @@ window.__ModuleLoader__.load({
|
|
|
848
848
|
var el = cands[i];
|
|
849
849
|
if (!el || el.id === NAV_ENTRY_ID) continue; // 排除我们自己的入口
|
|
850
850
|
if (navInDialog(el)) continue; // 弹层内的(关闭键/栏目)一律不要
|
|
851
|
-
try { if (el.closest && el.closest('[data-slot="sidebar.footer.action"]')) continue; } catch (
|
|
851
|
+
try { if (el.closest && el.closest('[data-slot="sidebar.footer.action"]')) continue; } catch (eF) { /* 忽略 */ }
|
|
852
852
|
if (NAV_BAD_RE.test(String(el.className || ""))) continue;
|
|
853
|
+
/* 🔴 2026-10-11 业主实测「点入口弹出的是本轮用量」:那次弹窗锚在**右上角**,
|
|
854
|
+
说明候选里混进了**顶栏**的按钮(用量/上下文那些同样带 aria-haspopup="dialog")。
|
|
855
|
+
这里加几何闸门:只认**侧栏范围内**的候选(左侧窄条),顶栏/内容区的按钮一律不碰。 */
|
|
856
|
+
try {
|
|
857
|
+
var r0 = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
858
|
+
if (r0 && (r0.width === 0 || r0.height === 0)) continue; // 不可见
|
|
859
|
+
if (r0 && r0.left > 360) continue; // 明显在右侧(顶栏/内容区)
|
|
860
|
+
} catch (e0) { /* 拿不到几何信息就放行(老宿主/测试替身) */ }
|
|
853
861
|
var inSettingsSlot = false;
|
|
854
862
|
try {
|
|
855
863
|
inSettingsSlot = !!(el.closest && el.closest('[data-slot="sidebar.settings"], [data-slot="settings.launcher"]'));
|
|
@@ -899,6 +907,134 @@ window.__ModuleLoader__.load({
|
|
|
899
907
|
} catch (e) { /* 忽略 */ }
|
|
900
908
|
return null;
|
|
901
909
|
}
|
|
910
|
+
/**
|
|
911
|
+
* 从**账号菜单**里进设置(桌面端的「设置」就是账号菜单里的一项)。
|
|
912
|
+
*
|
|
913
|
+
* 为什么需要:桌面端侧栏底部那颗"齿轮"在 Electron 渲染器里不一定存在 —— 业主截图里
|
|
914
|
+
* 底部只有「远程访问」+ 账号(高昂),「设置」在**账号菜单**里。所以当侧栏候选全部落空时,
|
|
915
|
+
* 必须能"打开账号菜单 → 点设置 → 再点我们的栏目",否则入口点下去就是没反应。
|
|
916
|
+
*/
|
|
917
|
+
/** 设置面板是否已经打开(可见的 dialog 且含「通用设置」——它就是设置面板的标志)。 */
|
|
918
|
+
function settingsDialogOpen() {
|
|
919
|
+
try {
|
|
920
|
+
var ds = document.querySelectorAll('[role="dialog"], [aria-modal="true"]');
|
|
921
|
+
for (var i = 0; i < ds.length; i++) {
|
|
922
|
+
var el = ds[i];
|
|
923
|
+
if (!el.getBoundingClientRect || el.getBoundingClientRect().width === 0) continue;
|
|
924
|
+
var t = String(el.textContent || "");
|
|
925
|
+
if (t.indexOf("通用设置") !== -1 || t.indexOf("General") !== -1) return true;
|
|
926
|
+
}
|
|
927
|
+
} catch (e) { /* 忽略 */ }
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* 这一行是不是账号菜单里的「设置」?
|
|
933
|
+
* ⚠️ **只认"以设置/Settings 开头"** —— 绝不能加"以设置结尾":账号菜单在桌面端真实文案是
|
|
934
|
+
* 「设置 ⌥ ⌘ ,」(带快捷键),而以"设置"结尾的还有「**通用设置**」,
|
|
935
|
+
* 那会让我们把设置面板当成账号菜单、把「通用设置」当成「设置」行(2026-10-11 两个远程访问的根因)。
|
|
936
|
+
*/
|
|
937
|
+
function menuRowIsSettings(el) {
|
|
938
|
+
try {
|
|
939
|
+
var t = String(el.textContent || "").replace(/\s+/g, "").trim();
|
|
940
|
+
if (!t || t.indexOf("远程访问") !== -1) return false;
|
|
941
|
+
return /^(设置|Settings)/i.test(t);
|
|
942
|
+
} catch (e) { return false; }
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** 账号菜单的触发器:桌面端是左下角那颗"头像 chip"(aria-label="账号菜单")。 */
|
|
946
|
+
function accountMenuTrigger() {
|
|
947
|
+
try {
|
|
948
|
+
var all = document.querySelectorAll('[aria-haspopup="menu"], button, [role="button"]');
|
|
949
|
+
var best = null;
|
|
950
|
+
for (var i = 0; i < all.length; i++) {
|
|
951
|
+
var el = all[i];
|
|
952
|
+
if (!el || el.id === NAV_ENTRY_ID) continue;
|
|
953
|
+
var aria = String((el.getAttribute && el.getAttribute("aria-label")) || "");
|
|
954
|
+
if (/账号菜单|Account menu/i.test(aria)) return el;
|
|
955
|
+
var r = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
956
|
+
if (!best && r && r.width > 0 && r.left < 360 && r.top > 300
|
|
957
|
+
&& String((el.getAttribute && el.getAttribute("aria-haspopup")) || "") === "menu") best = el;
|
|
958
|
+
}
|
|
959
|
+
return best;
|
|
960
|
+
} catch (e) { return null; }
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* 兜底:从**账号菜单**进设置(桌面端的「设置」就在账号菜单里)。
|
|
965
|
+
* 这是唯一在真机上验证过能打开设置面板的路径 —— 但账号菜单会闪现一下,所以:
|
|
966
|
+
* · 只在侧栏候选与快捷键都失败后才用(见 openRemoteSettings);
|
|
967
|
+
* · 轮询间隔压到 40ms,菜单可见时间≈一两帧,尽量不打扰。
|
|
968
|
+
*/
|
|
969
|
+
function openSettingsViaAccountMenu() {
|
|
970
|
+
try {
|
|
971
|
+
var trigger = accountMenuTrigger();
|
|
972
|
+
navTrace("menu:trigger=" + (trigger ? "found" : "none"));
|
|
973
|
+
if (!trigger) return;
|
|
974
|
+
try { trigger.click(); } catch (e) { return; }
|
|
975
|
+
var tries = 0;
|
|
976
|
+
var iv = setInterval(function () {
|
|
977
|
+
tries += 1;
|
|
978
|
+
var menus = document.querySelectorAll('[role="menu"], [role="listbox"], [role="dialog"]');
|
|
979
|
+
for (var i = 0; i < menus.length; i++) {
|
|
980
|
+
var m = menus[i];
|
|
981
|
+
if (!m.getBoundingClientRect || m.getBoundingClientRect().width === 0) continue;
|
|
982
|
+
if (String(m.textContent || "").indexOf("通用设置") !== -1) continue; // 那是设置面板,不是账号菜单
|
|
983
|
+
var items = m.querySelectorAll ? m.querySelectorAll('button, [role="menuitem"]') : [];
|
|
984
|
+
for (var k = 0; k < items.length; k++) {
|
|
985
|
+
if (!menuRowIsSettings(items[k])) continue;
|
|
986
|
+
navTrace("menu:settings-row-clicked");
|
|
987
|
+
clearInterval(iv);
|
|
988
|
+
try { items[k].click(); } catch (e2) { /* 忽略 */ }
|
|
989
|
+
var tries2 = 0;
|
|
990
|
+
var iv2 = setInterval(function () {
|
|
991
|
+
tries2 += 1;
|
|
992
|
+
var cell = pickNavByToken("远程访问", { insideDialog: true });
|
|
993
|
+
if (cell) { navTrace("menu:section-clicked"); clearInterval(iv2); cell.click(); }
|
|
994
|
+
else if (tries2 >= NAV_POLL_MAX) { navTrace("menu:section-timeout"); clearInterval(iv2); }
|
|
995
|
+
}, 120);
|
|
996
|
+
try { if (iv2 && typeof iv2.unref === "function") iv2.unref(); } catch (e3) { /* 忽略 */ }
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (tries >= 30) { navTrace("menu:timeout"); clearInterval(iv); } // 30×40ms≈1.2s
|
|
1001
|
+
}, 40);
|
|
1002
|
+
try { if (iv && typeof iv.unref === "function") iv.unref(); } catch (e4) { /* 忽略 */ }
|
|
1003
|
+
} catch (e) { /* 忽略 */ }
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* 用**官方设置快捷键**(⌥⌘, / Alt+Meta+,)打开设置面板。
|
|
1008
|
+
*
|
|
1009
|
+
* 为什么用快捷键而不是"点账号菜单里的设置"(2026-10-11 业主反馈):
|
|
1010
|
+
* 桌面端侧栏没有设置齿轮,只能从账号菜单进 —— 但那会让**账号菜单先弹出来再关掉**,
|
|
1011
|
+
* 业主的原话是「会触发一个小弹窗……有一瞬间会闪出一下,体验不好」。
|
|
1012
|
+
* 实测(探针在桌面渲染器里验证):向 document 派发一次
|
|
1013
|
+
* `keydown {key:",", metaKey:true, altKey:true}` 就能**直接**打开设置面板,没有任何中间弹层。
|
|
1014
|
+
* 快捷键取自设置行自带的 `aria-keyshortcuts="Alt+Meta+,"`(宿主自己的约定)。
|
|
1015
|
+
*/
|
|
1016
|
+
function openSettingsViaShortcut() {
|
|
1017
|
+
try {
|
|
1018
|
+
navTrace("open:shortcut-dispatch");
|
|
1019
|
+
var ev = new KeyboardEvent("keydown", {
|
|
1020
|
+
key: ",", code: "Comma", metaKey: true, altKey: true, bubbles: true, cancelable: true
|
|
1021
|
+
});
|
|
1022
|
+
document.dispatchEvent(ev);
|
|
1023
|
+
var tries = 0;
|
|
1024
|
+
var iv = setInterval(function () {
|
|
1025
|
+
tries += 1;
|
|
1026
|
+
var cell = pickNavByToken("远程访问", { insideDialog: true });
|
|
1027
|
+
if (cell) { navTrace("open:shortcut-section-clicked"); clearInterval(iv); cell.click(); return; }
|
|
1028
|
+
if (tries >= 6) { // ~0.6s 还没开 → 桌面端快捷键可能是**原生菜单加速键**(合成事件打不动)
|
|
1029
|
+
clearInterval(iv);
|
|
1030
|
+
if (!settingsDialogOpen()) { navTrace("open:shortcut-no-effect→account-menu"); openSettingsViaAccountMenu(); }
|
|
1031
|
+
else navTrace("open:shortcut-dialog-but-no-section");
|
|
1032
|
+
}
|
|
1033
|
+
}, 100);
|
|
1034
|
+
try { if (iv && typeof iv.unref === "function") iv.unref(); } catch (eV) { /* 忽略 */ }
|
|
1035
|
+
} catch (e) { navTrace("open:shortcut-error:" + (e && e.message)); }
|
|
1036
|
+
}
|
|
1037
|
+
|
|
902
1038
|
/**
|
|
903
1039
|
* 打开「远程访问」设置栏目。
|
|
904
1040
|
* 栏目已渲染 → 直接点;否则**依次试每个"设置"候选**:点一下 → 轮询 ≤3s 看栏目出没出来 →
|
|
@@ -908,13 +1044,15 @@ window.__ModuleLoader__.load({
|
|
|
908
1044
|
*/
|
|
909
1045
|
function openRemoteSettings() {
|
|
910
1046
|
try {
|
|
1047
|
+
navTrace("open:enter");
|
|
911
1048
|
var cell = pickNavByToken("远程访问", { insideDialog: true });
|
|
912
|
-
if (cell) { cell.click(); return; }
|
|
1049
|
+
if (cell) { navTrace("open:section-already-visible"); cell.click(); return; }
|
|
913
1050
|
var cands = settingsTriggerCandidates();
|
|
914
|
-
|
|
1051
|
+
navTrace("open:cands=" + cands.length);
|
|
1052
|
+
if (!cands.length) { navTrace("open:no-sidebar-cands→shortcut"); return openSettingsViaShortcut(); }
|
|
915
1053
|
var idx = 0;
|
|
916
1054
|
var tryNext = function () {
|
|
917
|
-
if (idx >= cands.length) return;
|
|
1055
|
+
if (idx >= cands.length) { navTrace("open:sidebar-cands-exhausted→shortcut"); return openSettingsViaShortcut(); } // 侧栏里找不到触发器 → 走账号菜单
|
|
918
1056
|
var el = cands[idx++];
|
|
919
1057
|
try { el.click(); } catch (eClick) { return tryNext(); }
|
|
920
1058
|
var tries = 0;
|
|
@@ -935,6 +1073,21 @@ window.__ModuleLoader__.load({
|
|
|
935
1073
|
*/
|
|
936
1074
|
var remoteNavOpen = null;
|
|
937
1075
|
|
|
1076
|
+
/**
|
|
1077
|
+
* 入口点击链路的**现场追踪**(有界 40 条,挂在 `window.__druNavTrace`)。
|
|
1078
|
+
* 为什么值得常驻:这类"点了没反应"的问题跨了"侧栏候选 → 账号菜单 → 设置面板"三步,
|
|
1079
|
+
* 光看界面分不清卡在哪一步(本次排查就是靠它定位到「设置 ⌥⌘,」文案不匹配)。
|
|
1080
|
+
* 只写内存、不含隐私,出问题时让用户执行 `copy(window.__druNavTrace)` 即可。
|
|
1081
|
+
*/
|
|
1082
|
+
function navTrace(step) {
|
|
1083
|
+
try {
|
|
1084
|
+
var w = window;
|
|
1085
|
+
if (!w.__druNavTrace) w.__druNavTrace = [];
|
|
1086
|
+
w.__druNavTrace.push(String(step));
|
|
1087
|
+
if (w.__druNavTrace.length > 40) w.__druNavTrace.shift();
|
|
1088
|
+
} catch (e) { /* 忽略 */ }
|
|
1089
|
+
}
|
|
1090
|
+
|
|
938
1091
|
/* 📱 侧栏底部的「远程访问」入口(2026-10-10 三修:改用**官方槽位**)。====================
|
|
939
1092
|
前两次都栽在"自己找 DOM 落点"上:
|
|
940
1093
|
· 一修前:按文案包含「设置」找锚点 → 命中的是设置面板里的「通用设置」→ 按钮跑到设置面板顶部;
|
|
@@ -969,10 +1122,23 @@ window.__ModuleLoader__.load({
|
|
|
969
1122
|
]);
|
|
970
1123
|
}
|
|
971
1124
|
|
|
1125
|
+
/* 📱 账号菜单里的入口(2026-10-11 业主指定位置:「放在设置那一行的上面,小弹窗里面」)。
|
|
1126
|
+
────────────────────────────────────────────────────────────────────────────
|
|
1127
|
+
那个「小弹窗」= 桌面端自带的**账号菜单**(设置 / 意见反馈 / 登录 / 更多),由
|
|
1128
|
+
`@deepseek-ai/dsh-client-ui-settings-account` 渲染(它只在桌面渲染器里注册,且有
|
|
1129
|
+
`dshDesktop` 判断)。它**没有给插件留任何菜单槽位**(该包只注册 shell.overlay /
|
|
1130
|
+
settings.models.sign-in / shell.quota-notice),所以放进它只能做 DOM 插入。
|
|
1131
|
+
因此这里的写法刻意保守:
|
|
1132
|
+
· 只在"确实是账号菜单"(含「意见反馈」这一独有文案)的浮层里动手,别的浮层一律不碰;
|
|
1133
|
+
· 幂等(插过就跳过);找不到「设置」那一行就**什么都不做**(绝不猜位置);
|
|
1134
|
+
· 只用自己创建的 button(**不 cloneNode** —— 克隆会继承官方事件委托,历史事故);
|
|
1135
|
+
· 整段 try/catch,任何异常都不影响面板本身。
|
|
1136
|
+
失败的可见后果只是"弹窗里没有这一项",侧栏那个官方槽位入口仍然在。 */
|
|
972
1137
|
function navEnsureStart() {
|
|
973
1138
|
if (window.__dshRemoteNavStarted) return;
|
|
974
1139
|
window.__dshRemoteNavStarted = true;
|
|
975
1140
|
try { window.__dshRemoteNav = { open: openRemoteSettings }; } catch (e) {}
|
|
1141
|
+
navTrace("adapter-ready");
|
|
976
1142
|
// ⚠️ 这里**不再**做 DOM 注入(见 RemoteNavEntry 的注释):入口由官方槽位渲染。
|
|
977
1143
|
// 保留本函数只为实现"继续暴露 window.__dshRemoteNav"(老调用方与诊断仍在用)。
|
|
978
1144
|
try { if (typeof openRemoteSettings === "function") remoteNavOpen = openRemoteSettings; } catch (e) { /* 忽略 */ }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-web",
|
|
3
|
-
"version": "0.7.0-beta.
|
|
3
|
+
"version": "0.7.0-beta.7",
|
|
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",
|
|
@@ -159,7 +159,10 @@ test("桌面端:状态接口必须下发宿主形态,面板才能自适应",
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
test("桌面端:面板不得自动倒计时重启,且不摆点了必然失败的重启按钮", () => {
|
|
162
|
-
|
|
162
|
+
// ⚠️ 2026-10-11 修:node 半把这个字段下在 **connect** 里(`connect.desktopHost`),
|
|
163
|
+
// 只读顶层 `st.desktopHost` 会**永远 false** → 桌面端被当成 Web 版(照样摆重启按钮、走自动倒计时)。
|
|
164
|
+
assert.match(CLIENT_SRC, /var desktopHost = !!\(st && \(st\.desktopHost \|\| \(st\.connect && st\.connect\.desktopHost\)\)\)/,
|
|
165
|
+
"面板必须同时读顶层与 connect.desktopHost(只读顶层等于门禁失效)");
|
|
163
166
|
assert.match(CLIENT_SRC, /isRestartHard \|\| desktopHost \|\| busy !== ""/,
|
|
164
167
|
"自动倒计时重启必须在桌面端短路(Host 由 Electron 托管,不能替用户重启)");
|
|
165
168
|
assert.match(CLIENT_SRC, /\(desktopHost && !isRefreshOnly\) \? null : h\("button"/,
|
|
@@ -347,7 +347,8 @@ test("源码约束:侧栏注入「远程访问」按钮与官方「设置」
|
|
|
347
347
|
assert.match(SOURCE, /data-dru-remote/);
|
|
348
348
|
assert.match(SOURCE, /NAV_ENTRY_ID/);
|
|
349
349
|
// 安全设计:绝不克隆官方按钮;候选限定 navCell/导航容器 + 短文本;排除自身;排除菜单语义
|
|
350
|
-
|
|
350
|
+
// 只禁止**调用**(注释里说明"为什么不克隆"是允许的;2026-10-11 因注释里出现该词误报过)
|
|
351
|
+
assert.doesNotMatch(SOURCE, /\.cloneNode\s*\(/);
|
|
351
352
|
assert.match(SOURCE, /navCell/);
|
|
352
353
|
assert.match(SOURCE, /NAV_TEXT_MAX/);
|
|
353
354
|
assert.match(SOURCE, /el\.id === NAV_ENTRY_ID/);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「点入口怎么打开设置」契约测试(2026-10-11 业主反馈后的最终形态)。
|
|
3
|
+
*
|
|
4
|
+
* 背景(业主原话):
|
|
5
|
+
* ① 「账号菜单里面的那个删掉。只保留官方槽位的那个。」
|
|
6
|
+
* ② 「设置界面出现了两个远程访问菜单。」—— 因为账号菜单注入的判据里加了"以设置结尾",
|
|
7
|
+
* 而「通用设置」正好以"设置"结尾 → 注入行被插进了**设置面板**里,多出一个"远程访问"。
|
|
8
|
+
* ③ 「点击……会触发一个小弹窗,同时也会把设置反馈的弹窗呼出来,有一瞬间会闪出一下,体验不好。」
|
|
9
|
+
* —— 那是"点账号菜单 → 再点设置"这条路带来的中间弹层。
|
|
10
|
+
*
|
|
11
|
+
* 现在的契约:
|
|
12
|
+
* · **不再往账号菜单里插任何东西**(dru-menu-row / accountMenuInjectTick 一律不得回来);
|
|
13
|
+
* · 侧栏入口在桌面端用**官方设置快捷键**(⌥⌘, / Alt+Meta+,)直接打开设置面板,
|
|
14
|
+
* 不产生任何中间弹层;打开后自动点中我们的栏目;
|
|
15
|
+
* · 面板里只能有**一个**「远程访问」栏目。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { test } from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
const SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "lib", "client.js"), "utf8");
|
|
25
|
+
|
|
26
|
+
/** 把快捷键打开逻辑切出来,在最小环境里跑一遍。 */
|
|
27
|
+
function loadShortcut({ sectionFound = true } = {}) {
|
|
28
|
+
const start = SOURCE.indexOf("function openSettingsViaShortcut()");
|
|
29
|
+
assert.ok(start > 0, "应能定位 openSettingsViaShortcut");
|
|
30
|
+
const end = SOURCE.indexOf("/**\n * 打开「远程访问」设置栏目。", start);
|
|
31
|
+
assert.ok(end > start, "应能定位函数结尾");
|
|
32
|
+
const src = SOURCE.slice(start, end);
|
|
33
|
+
|
|
34
|
+
const dispatched = [];
|
|
35
|
+
const clicked = [];
|
|
36
|
+
class KeyboardEvent {
|
|
37
|
+
constructor(type, init) { this.type = type; Object.assign(this, init); }
|
|
38
|
+
}
|
|
39
|
+
const document = { dispatchEvent: (e) => { dispatched.push(e); return true; } };
|
|
40
|
+
const pickNavByToken = () => (sectionFound ? { click: () => clicked.push("远程访问") } : null);
|
|
41
|
+
const fn = new Function(
|
|
42
|
+
"document", "KeyboardEvent", "navTrace", "pickNavByToken", "NAV_POLL_MAX", "setInterval", "clearInterval",
|
|
43
|
+
`${src}; return openSettingsViaShortcut;`
|
|
44
|
+
)(document, KeyboardEvent, () => {}, pickNavByToken, 3, (f) => { f(); return 1; }, () => {});
|
|
45
|
+
return { run: fn, dispatched, clicked };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("★ 用官方设置快捷键(⌥⌘,)直接打开:不经过任何中间弹层", () => {
|
|
49
|
+
const { run, dispatched } = loadShortcut();
|
|
50
|
+
run();
|
|
51
|
+
assert.equal(dispatched.length, 1, "应恰好派发一次快捷键事件");
|
|
52
|
+
const ev = dispatched[0];
|
|
53
|
+
assert.equal(ev.type, "keydown");
|
|
54
|
+
assert.equal(ev.key, ",");
|
|
55
|
+
assert.equal(ev.code, "Comma");
|
|
56
|
+
assert.equal(ev.metaKey, true, "必须带 Meta(⌘)");
|
|
57
|
+
assert.equal(ev.altKey, true, "必须带 Alt(⌥)—— 与设置行自带的 aria-keyshortcuts=\"Alt+Meta+,\" 一致");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("★ 设置面板打开后自动点中「远程访问」栏目", () => {
|
|
61
|
+
const { run, clicked } = loadShortcut({ sectionFound: true });
|
|
62
|
+
run();
|
|
63
|
+
assert.deepEqual(clicked, ["远程访问"], "面板开了要顺手切到我们的栏目");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("★ 不再往账号菜单里插任何东西(业主明确要求删掉)", () => {
|
|
67
|
+
for (const banned of ["dru-menu-row", "accountMenuInjectTick", "MENU_ENTRY_SIG"]) {
|
|
68
|
+
assert.ok(!SOURCE.includes(banned), `不得再往账号菜单里插条目:${banned}(已按业主要求删除)`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("★ 打开设置的顺序:侧栏候选 → 快捷键(验证) → 账号菜单(兜底)", () => {
|
|
73
|
+
// 桌面端的 ⌥⌘, 是**原生菜单加速键**,合成 DOM 事件打不动它(真机实测"点了没反应");
|
|
74
|
+
// 而"设置"只存在于账号菜单里 → 必须保留这条兜底,但只能在前面都失败后才走(它会闪一下)。
|
|
75
|
+
assert.match(SOURCE, /open:shortcut-no-effect→account-menu/, "快捷键无效时必须转投账号菜单兜底");
|
|
76
|
+
const shortcutAt = SOURCE.indexOf("function openSettingsViaShortcut()");
|
|
77
|
+
const menuAt = SOURCE.indexOf("function openSettingsViaAccountMenu()");
|
|
78
|
+
assert.ok(shortcutAt > 0 && menuAt > 0, "两条路径都要在");
|
|
79
|
+
// 菜单兜底里必须带上"不是设置面板"的排除(否则会把设置面板当成账号菜单)
|
|
80
|
+
assert.match(SOURCE, /indexOf\("通用设置"\) !== -1\) continue;/, "识别账号菜单时必须排除设置面板本身");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("★ 「设置」行判据:认「设置 ⌥ ⌘ ,」,绝不认「通用设置」", () => {
|
|
84
|
+
const start = SOURCE.indexOf("function menuRowIsSettings(");
|
|
85
|
+
assert.ok(start > 0, "应有 menuRowIsSettings");
|
|
86
|
+
const end = SOURCE.indexOf("function accountMenuTrigger()", start);
|
|
87
|
+
const fn = new Function(`${SOURCE.slice(start, end)}; return menuRowIsSettings;`)();
|
|
88
|
+
const mk = (t) => ({ textContent: t });
|
|
89
|
+
assert.equal(fn(mk("设置 ⌥ ⌘ ,")), true, "真实文案带快捷键,必须认得");
|
|
90
|
+
assert.equal(fn(mk("设置")), true, "纯文案也要认得");
|
|
91
|
+
assert.equal(fn(mk("Settings")), true, "英文界面");
|
|
92
|
+
assert.equal(fn(mk("通用设置")), false, "★ 绝不能认「通用设置」——2026-10-11 两个『远程访问』就是它造成的");
|
|
93
|
+
assert.equal(fn(mk("📱 远程访问")), false, "不能认我们自己的入口");
|
|
94
|
+
assert.equal(fn(mk("意见反馈")), false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("★ 反复出现的坑:判据不得用「以设置结尾」(会命中「通用设置」)", () => {
|
|
98
|
+
// 这次设置面板里冒出两个「远程访问」,根因就是结尾匹配把「通用设置」当成了「设置」行。
|
|
99
|
+
// 直接把这种判据钉死:源码里不得再出现「以设置/Settings 结尾」的正则片段。
|
|
100
|
+
assert.ok(!SOURCE.includes("(设置|Settings)$"), "不得再用「以设置结尾」的判据");
|
|
101
|
+
assert.ok(!SOURCE.includes("(设置|Settings)/$"), "不得再用「以设置结尾」的判据(另一种写法)");
|
|
102
|
+
});
|