dsh-remote-web 0.7.0-beta.5 → 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.
Files changed (3) hide show
  1. package/lib/client.js +192 -10
  2. package/lib/index.js +15 -0
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -834,22 +834,42 @@ window.__ModuleLoader__.load({
834
834
  * 其中 **带 aria-keyshortcuts / 文案或 aria-label 含「设置」** 的优先(语言无关 + 精确)。
835
835
  */
836
836
  function settingsTriggerCandidates() {
837
- var out = [];
837
+ var ranked = [];
838
838
  try {
839
+ /* 排名而不是「谁先扫到谁」(2026-10-11 业主实测「点入口弹出的是 token 用量小弹窗」):
840
+ ① 官方设置槽位里的按钮 = 确定是设置触发器(sidebar.settings / settings.launcher)
841
+ ② 带快捷键或文案含「设置」的弹层触发器
842
+ ③ 其它弹层触发器(最后兜底)
843
+ 并且**排除我们自己所在的底部槽**(sidebar.footer.action)—— token 用量、其它插件按钮
844
+ 都在那一块,它们同样带 aria-haspopup=dialog,正是「点错弹窗」的来源。
845
+ 只用 closest 判容器、不做后代选择器:任何 DOM 实现(含测试替身)都支持。 */
839
846
  var cands = document.querySelectorAll('button[aria-haspopup="dialog"], [aria-haspopup="dialog"]');
840
- var rest = [];
841
847
  for (var i = 0; i < cands.length; i++) {
842
848
  var el = cands[i];
843
849
  if (!el || el.id === NAV_ENTRY_ID) continue; // 排除我们自己的入口
844
850
  if (navInDialog(el)) continue; // 弹层内的(关闭键/栏目)一律不要
851
+ try { if (el.closest && el.closest('[data-slot="sidebar.footer.action"]')) continue; } catch (eF) { /* 忽略 */ }
845
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) { /* 拿不到几何信息就放行(老宿主/测试替身) */ }
861
+ var inSettingsSlot = false;
862
+ try {
863
+ inSettingsSlot = !!(el.closest && el.closest('[data-slot="sidebar.settings"], [data-slot="settings.launcher"]'));
864
+ } catch (e1) { inSettingsSlot = false; }
846
865
  var label = String(navAttrOf(el) || navCleanText(el) || "");
847
866
  var hasShortcut = !!(el.getAttribute && el.getAttribute("aria-keyshortcuts"));
848
- if (hasShortcut || /设置|Settings/i.test(label)) out.push(el);
849
- else rest.push(el);
867
+ var rank = inSettingsSlot ? 0 : ((hasShortcut || /设置|Settings/i.test(label)) ? 1 : 2);
868
+ ranked.push({ el: el, rank: rank, order: i });
850
869
  }
851
- return out.concat(rest); // 兜底也不能空着:实在认不出就逐个试(见 openRemoteSettings)
852
- } catch (e) { return out; }
870
+ ranked.sort(function (a, b) { return (a.rank - b.rank) || (a.order - b.order); });
871
+ return ranked.map(function (x) { return x.el; });
872
+ } catch (e) { return ranked.map(function (x) { return x.el; }); }
853
873
  }
854
874
  /** 官方「设置」触发器(首选候选,保持旧名字给测试与调用方用)。 */
855
875
  function pickSettingsTrigger() {
@@ -887,6 +907,134 @@ window.__ModuleLoader__.load({
887
907
  } catch (e) { /* 忽略 */ }
888
908
  return null;
889
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
+
890
1038
  /**
891
1039
  * 打开「远程访问」设置栏目。
892
1040
  * 栏目已渲染 → 直接点;否则**依次试每个"设置"候选**:点一下 → 轮询 ≤3s 看栏目出没出来 →
@@ -896,13 +1044,15 @@ window.__ModuleLoader__.load({
896
1044
  */
897
1045
  function openRemoteSettings() {
898
1046
  try {
1047
+ navTrace("open:enter");
899
1048
  var cell = pickNavByToken("远程访问", { insideDialog: true });
900
- if (cell) { cell.click(); return; }
1049
+ if (cell) { navTrace("open:section-already-visible"); cell.click(); return; }
901
1050
  var cands = settingsTriggerCandidates();
902
- if (!cands.length) return;
1051
+ navTrace("open:cands=" + cands.length);
1052
+ if (!cands.length) { navTrace("open:no-sidebar-cands→shortcut"); return openSettingsViaShortcut(); }
903
1053
  var idx = 0;
904
1054
  var tryNext = function () {
905
- if (idx >= cands.length) return;
1055
+ if (idx >= cands.length) { navTrace("open:sidebar-cands-exhausted→shortcut"); return openSettingsViaShortcut(); } // 侧栏里找不到触发器 → 走账号菜单
906
1056
  var el = cands[idx++];
907
1057
  try { el.click(); } catch (eClick) { return tryNext(); }
908
1058
  var tries = 0;
@@ -923,6 +1073,21 @@ window.__ModuleLoader__.load({
923
1073
  */
924
1074
  var remoteNavOpen = null;
925
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
+
926
1091
  /* 📱 侧栏底部的「远程访问」入口(2026-10-10 三修:改用**官方槽位**)。====================
927
1092
  前两次都栽在"自己找 DOM 落点"上:
928
1093
  · 一修前:按文案包含「设置」找锚点 → 命中的是设置面板里的「通用设置」→ 按钮跑到设置面板顶部;
@@ -957,10 +1122,23 @@ window.__ModuleLoader__.load({
957
1122
  ]);
958
1123
  }
959
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
+ 失败的可见后果只是"弹窗里没有这一项",侧栏那个官方槽位入口仍然在。 */
960
1137
  function navEnsureStart() {
961
1138
  if (window.__dshRemoteNavStarted) return;
962
1139
  window.__dshRemoteNavStarted = true;
963
1140
  try { window.__dshRemoteNav = { open: openRemoteSettings }; } catch (e) {}
1141
+ navTrace("adapter-ready");
964
1142
  // ⚠️ 这里**不再**做 DOM 注入(见 RemoteNavEntry 的注释):入口由官方槽位渲染。
965
1143
  // 保留本函数只为实现"继续暴露 window.__dshRemoteNav"(老调用方与诊断仍在用)。
966
1144
  try { if (typeof openRemoteSettings === "function") remoteNavOpen = openRemoteSettings; } catch (e) { /* 忽略 */ }
@@ -2718,7 +2896,11 @@ window.__ModuleLoader__.load({
2718
2896
  // 在那边**做不到也不该做**(强行退出会弹原生致命错误)。面板据此:
2719
2897
  // ① 不自动倒计时重启;② 保留提醒但改文案,引导"退出并重开桌面应用"。
2720
2898
  // 旧版 host 的响应里没有该字段 → false,行为与改动前完全一致。
2721
- var desktopHost = !!(st && st.desktopHost);
2899
+ // 🔴 2026-10-11 修:node 半把该字段下在 **connect** 里(`connect.desktopHost`),
2900
+ // 这里以前读顶层 `st.desktopHost` → **永远 false** → 桌面端被当成 Web 版:
2901
+ // 面板照样摆「立即重启」按钮、照样走倒计时自动重启那条路(业主实测"装插件时窗口被突兀关掉")。
2902
+ // 兼容两种位置,桌面端的"不自动重启/只提示用户自己重启"这才真正生效。
2903
+ var desktopHost = !!(st && (st.desktopHost || (st.connect && st.connect.desktopHost)));
2722
2904
  // kind === "refresh" = 插件文件在运行中被改写(装完/更新完),只需刷新页面;
2723
2905
  // 其余情况(极为罕见)才提重启 dsh web。
2724
2906
  var isRefreshOnly = restartPending && restartInfo.kind === "refresh";
package/lib/index.js CHANGED
@@ -6626,6 +6626,21 @@ function registerRoutes(ctx, relayDir) {
6626
6626
  handler: async (_req, res) => {
6627
6627
  // 首次安装/在线更新后重启 DeepSeek harness:插件本体与浏览器半在进程启动时装载,
6628
6628
  // 不重启则面板入口与 /dsh-remote/* 路由都不会出现。只重启承载本插件的进程。
6629
+ //
6630
+ // 🔴 2026-10-11 业主口径:**桌面端绝不代为重启**。
6631
+ // "自动重启会很突兀地把窗口给关掉,关没了……我们应该提示用户自己重启,不要由我们来重启。"
6632
+ // 桌面端 Host 由 Electron 托管,重启 = 强杀承载窗口的进程(还会弹原生致命错误)。
6633
+ // 这里做成**硬拒绝**(不只是前端藏按钮):任何调用方都拿不到"被重启"的结果,
6634
+ // 只会收到一句明确的"请手动重启",前端据此展示提示。
6635
+ if (IS_ELECTRON_HOST) {
6636
+ sendJson(res, 200, {
6637
+ ...(await composeStatus(relayDir)),
6638
+ ok: false,
6639
+ status: "manual-restart-required",
6640
+ detail: "桌面端不代为重启:请手动退出并重新打开 DeepSeek Harness(自动重启会突兀地关掉窗口)",
6641
+ });
6642
+ return;
6643
+ }
6629
6644
  const r = await restartHarness(relayDir);
6630
6645
  const payload = { ...(await composeStatus(relayDir)), ...r };
6631
6646
  if (!r.ok) payload.error = r.detail || r.status || "重启失败";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.7.0-beta.5",
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",