@mrrisega/dsh-remote 0.5.0 → 0.6.0-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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.5.0",
3
+ "version": "0.6.0-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",
@@ -5,11 +5,12 @@
5
5
  // factory 内只能 require shell 种子模块(react / react/jsx-runtime 等)。
6
6
  //
7
7
  // 功能:
8
- // - settings.section:设置页「远程控制」栏目(位于「Agent 预设」下方,官方扩展点)
8
+ // - settings.section:设置页「远程访问」栏目(位于「Agent 预设」下方,官方扩展点)
9
9
  // - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
10
10
  // · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册 tabs
11
11
  // · 登录要求图形验证码;注册要求两次密码 + 图形验证码
12
- // · 远程地址展示 + bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
12
+ // · 📱 远程访问卡:一次性访问密钥(扫码/直接打开/复制 + 到期倒计时自动刷新)与已授权设备管理
13
+ // · bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
13
14
  // - 首次安装引导:设置页栏目旁小红点(localStorage dsh-remote-seen-dot 控制)
14
15
  // - shell.overlay:满意度弹窗(安装体验至少 1 小时后弹出,只弹一次)
15
16
  // 所有数据经同源 /dsh-remote/* 宿主路由读写(node 半提供)。
@@ -40,7 +41,7 @@ window.__ModuleLoader__.load({
40
41
  ".dru-settings-title{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,#e6edf3)}",
41
42
  ".dru-settings-sub{font-size:12px;color:var(--dsw-alias-label-tertiary,#8c959f);margin-top:2px}",
42
43
  ".dru-settings-body{display:flex;flex-direction:column;gap:14px}",
43
- // 首次安装引导小红点(挂在设置页「远程控制」导航栏目右上角)
44
+ // 首次安装引导小红点(挂在设置页「远程访问」导航栏目右上角)
44
45
  ".dru-reddot{position:absolute;top:9px;right:12px;width:7px;height:7px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2,#fff);pointer-events:none;z-index:1}",
45
46
  ".dru-card{background:#f6f8fa;border:1px solid #eaeef2;border-radius:10px;padding:14px 16px}",
46
47
  ".dru-card h3{margin:0 0 8px;font-size:13px;font-weight:700;color:#1f2328}",
@@ -129,10 +130,88 @@ window.__ModuleLoader__.load({
129
130
  ".dru-ver-badge-new{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
130
131
  ".dru-ver-badge-ok{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
131
132
  ".dru-up-log{margin-top:8px;background:#0d1117;color:#e6edf3;border-radius:8px;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:150px;overflow:auto}",
133
+ // ── 📱 远程访问(一次性访问密钥 + 已授权设备管理) ──
134
+ ".dru-access-flex{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-start;margin-top:10px}",
135
+ ".dru-access-col{flex:1;min-width:230px;display:flex;flex-direction:column;gap:8px}",
136
+ ".dru-qr-img{width:180px;height:180px;flex:none;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;object-fit:contain}",
137
+ ".dru-qr-ph{width:180px;height:180px;flex:none;border-radius:8px;border:1px dashed #d0d7de;background:#f6f8fa;color:#8c959f;font-size:12px;display:flex;align-items:center;justify-content:center;text-align:center;padding:10px;box-sizing:border-box}",
138
+ ".dru-url.big{font-size:13.5px;font-weight:600}",
139
+ ".dru-cd{font-size:12px;color:#9a6700;margin-top:2px}",
140
+ ".dru-cd-ok{color:#1a7f37}",
141
+ ".dru-key-note{font-size:12px;color:#57606a;line-height:1.6}",
142
+ ".dru-dev{border:1px solid #eaeef2;border-radius:8px;background:#ffffff;padding:9px 11px;margin-bottom:8px}",
143
+ ".dru-dev-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
144
+ ".dru-dev-name{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:130px}",
145
+ ".dru-dev-meta{font-size:11.5px;color:#57606a}",
146
+ ".dru-dev-sub{font-size:11px;color:#8c959f;margin-top:3px}",
147
+ ".dru-dev-tag{font-size:10.5px;color:#8c959f;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
148
+ ".dru-dev-tag-off{color:#cf222e;border-color:#ffb3b6;background:#fff0f1}",
149
+ // 左下角「远程访问」快捷小手机图标(安装后常驻;首次点击前带红点)
150
+ ".dru-fab{position:fixed;left:18px;bottom:18px;z-index:2147482100;width:46px;height:46px;border-radius:50%;background:#0969da;color:#fff;border:none;display:flex;align-items:center;justify-content:center;font-size:22px;line-height:1;box-shadow:0 6px 18px rgba(0,0,0,.28);cursor:pointer;font-family:inherit;padding:0}",
151
+ ".dru-fab:hover{background:#0860bd}",
152
+ ".dru-fab-dot{position:absolute;top:2px;right:2px;width:9px;height:9px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px #fff;pointer-events:none}",
132
153
  ].join("\n");
133
154
  document.head.appendChild(styleEl);
134
155
 
135
- // ── 首次安装引导小红点(设置页「远程控制」栏目,localStorage 控制) ────
156
+ // ── 左下角「远程访问」快捷小手机图标 ──
157
+ // 常驻于左下角:点击打开 设置页 → 「远程访问」栏目;首次点击前显示小红点(localStorage 一次)。
158
+ var FAB_SEEN_KEY = "dsh-remote-fab-seen";
159
+ function clickTextNav(text) {
160
+ try {
161
+ var nodes = document.querySelectorAll('button, [role="tab"], [class*="navCell"], [class*="nav"], [class*="sidebar"] a, a');
162
+ for (var i = 0; i < nodes.length; i++) {
163
+ var el = nodes[i];
164
+ var t = (el.textContent || "").trim();
165
+ if (t === text || t.indexOf(text) === 0) {
166
+ try { el.click(); return true; } catch (e) { /* 尝试下一个 */ }
167
+ }
168
+ }
169
+ } catch (e) { /* 忽略 */ }
170
+ return false;
171
+ }
172
+ function openRemoteSettings() {
173
+ if (clickTextNav("远程访问")) return; // 已在栏目内/可直接点到
174
+ if (clickTextNav("设置")) { /* 进入设置页后再轮询远程访问栏目 */ }
175
+ var tries = 0;
176
+ var iv = setInterval(function () {
177
+ if (clickTextNav("远程访问")) { clearInterval(iv); return; }
178
+ if (++tries > 40) clearInterval(iv);
179
+ }, 150);
180
+ if (typeof iv.unref === "function") iv.unref();
181
+ }
182
+ function ensureRemoteFab() {
183
+ try {
184
+ if (document.getElementById("dru-remote-fab")) return;
185
+ if (!document.body) { setTimeout(ensureRemoteFab, 300); return; }
186
+ var fab = document.createElement("button");
187
+ fab.id = "dru-remote-fab";
188
+ fab.className = "dru-fab";
189
+ fab.type = "button";
190
+ fab.setAttribute("aria-label", "远程访问");
191
+ fab.textContent = "📱";
192
+ var seen = false;
193
+ try { seen = !!localStorage.getItem(FAB_SEEN_KEY); } catch (e) {}
194
+ if (!seen) {
195
+ var dot = document.createElement("span");
196
+ dot.className = "dru-fab-dot";
197
+ fab.appendChild(dot);
198
+ }
199
+ fab.addEventListener("click", function () {
200
+ try { localStorage.setItem(FAB_SEEN_KEY, "1"); } catch (e) {}
201
+ var d = fab.querySelector(".dru-fab-dot");
202
+ if (d) d.remove();
203
+ openRemoteSettings();
204
+ });
205
+ document.body.appendChild(fab);
206
+ } catch (e) { /* 非关键 */ }
207
+ }
208
+ if (document.readyState === "loading") {
209
+ document.addEventListener("DOMContentLoaded", ensureRemoteFab);
210
+ } else {
211
+ setTimeout(ensureRemoteFab, 600);
212
+ }
213
+
214
+ // ── 首次安装引导小红点(设置页「远程访问」栏目,localStorage 控制) ────
136
215
  // 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
137
216
  // 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
138
217
  // 设置页由官方 shell 渲染(class 含 navCell 的导航按钮 + 栏目 label),
@@ -168,17 +247,17 @@ window.__ModuleLoader__.load({
168
247
  });
169
248
  } catch (e) {}
170
249
  }
171
- /** 扫描设置页导航,找到「远程控制」栏目按钮后注入红点。 */
250
+ /** 扫描设置页导航,找到「远程访问」栏目按钮后注入红点。 */
172
251
  function dotScan() {
173
252
  if (dotSeen()) return;
174
253
  var cells = document.querySelectorAll("button");
175
254
  for (var i = 0; i < cells.length; i++) {
176
255
  var c = cells[i];
177
- // 语义化匹配:设置页导航按钮(navCell)+ 栏目名「远程控制」。
256
+ // 语义化匹配:设置页导航按钮(navCell)+ 栏目名「远程访问」。
178
257
  // class 为 shell 的 hashed 类名,取子串匹配避免依赖具体 hash;
179
258
  // 若未来 hash 变化导致匹配失败,仅红点不显示,栏目本身不受影响。
180
259
  if (String(c.className || "").indexOf("navCell") === -1) continue;
181
- if ((c.textContent || "").indexOf("远程控制") === -1) continue;
260
+ if ((c.textContent || "").indexOf("远程访问") === -1) continue;
182
261
  dotInject(c);
183
262
  }
184
263
  }
@@ -524,7 +603,7 @@ window.__ModuleLoader__.load({
524
603
  var payload = {
525
604
  kind: "rating",
526
605
  category: "satisfaction",
527
- content: note.trim() || (rating >= 4 ? "对远程控制功能满意" : "对远程控制功能不太满意"),
606
+ content: note.trim() || (rating >= 4 ? "对远程访问功能满意" : "对远程访问功能不太满意"),
528
607
  rating: rating,
529
608
  recommend: recommend === true ? 1 : 0,
530
609
  };
@@ -547,7 +626,7 @@ window.__ModuleLoader__.load({
547
626
  step === "rate"
548
627
  ? h("div", null,
549
628
  h("div", { className: "dru-popup-icon" }, "😊"),
550
- h("div", { className: "dru-popup-title" }, "您对远程控制功能满意吗?"),
629
+ h("div", { className: "dru-popup-title" }, "您对远程访问功能满意吗?"),
551
630
  h("div", { className: "dru-popup-sub" }, "使用体验已满 1 小时,说说真实感受吧(1 分钟搞定)"),
552
631
  h("div", { className: "dru-popup-rate" },
553
632
  [ [5, "😄", "很满意"], [3, "😐", "一般"], [1, "😞", "不满意"] ].map(function (r) {
@@ -609,8 +688,8 @@ window.__ModuleLoader__.load({
609
688
  }
610
689
 
611
690
  // ── 图标 ────────────────────────────────────────────────────────────────
612
- // 通用「远程控制」图标(🖥 风格 emoji,与面板内既有 emoji 图标体系一致):
613
- // 栏目导航 label 与栏目头部均使用它。原侧边栏入口(电源图标按钮)已随入口迁移移除。
691
+ // 「远程访问」栏目图标沿用面板 emoji 图标体系:📱 更贴合“手机/另一台电脑远程访问”语义
692
+ // (原 🖥 已随入口迁移调整);栏目导航 label 与栏目头部均使用它。
614
693
 
615
694
  // ── 版本与更新卡片(自管理:市场没有更新按钮,这里提供在线一键更新/彻底卸载) ──
616
695
  // 数据来自 node 半新增的 /dsh-remote/self* 路由;逻辑均在插件 node 半实现,
@@ -807,6 +886,18 @@ window.__ModuleLoader__.load({
807
886
  var shArr = useState(""); var selfHost = shArr[0]; var setSelfHost = shArr[1];
808
887
  var lkArr = useState(""); var localKey = lkArr[0]; var setLocalKey = lkArr[1];
809
888
 
889
+ // ── 📱 远程访问卡(一次性访问密钥 + 已授权设备管理)——放在全部既有字段之后,保持既有 hook 序号 ──
890
+ var akeyArr = useState(null); var akey = akeyArr[0]; var setAkey = akeyArr[1]; // {url,key,expires_at,ttl_ms,qr_data_url}
891
+ var akeyBusyArr = useState(false); var akeyBusy = akeyBusyArr[0]; var setAkeyBusy = akeyBusyArr[1];
892
+ var akeyMsgArr = useState(null); var akeyMsg = akeyMsgArr[0]; var setAkeyMsg = akeyMsgArr[1]; // {kind,text}
893
+ var nowTickArr = useState(function () { return Date.now(); }); var nowTick = nowTickArr[0]; var setNowTick = nowTickArr[1];
894
+ var copiedKeyArr = useState(false); var copiedKey = copiedKeyArr[0]; var setCopiedKey = copiedKeyArr[1];
895
+ var devSessArr = useState(null); var devSessions = devSessArr[0]; var setDevSessions = devSessArr[1]; // null=未加载
896
+ var devOpenArr = useState(false); var devOpen = devOpenArr[0]; var setDevOpen = devOpenArr[1];
897
+ var devBusyArr = useState(""); var devBusy = devBusyArr[0]; var setDevBusy = devBusyArr[1];
898
+ var devMsgArr = useState(null); var devMsg = devMsgArr[0]; var setDevMsg = devMsgArr[1];
899
+ var armedDevArr = useState(null); var armedDev = armedDevArr[0]; var setArmedDev = armedDevArr[1]; // 待二次确认的 session id
900
+
810
901
  var refresh = useCallback(function () {
811
902
  setBusy("status");
812
903
  api("/dsh-remote/status").then(function (body) {
@@ -848,6 +939,167 @@ window.__ModuleLoader__.load({
848
939
  if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !lcap) loadCaptcha("login");
849
940
  }, [st, loggedIn, mode, authTab, lcap]);
850
941
 
942
+ // ── 📱 远程访问卡:常量 / 轮询 / 一次性访问密钥 / 已授权设备 ─────────────────
943
+ var KEY_AUTO_REFRESH_MS = 25000; // 停留栏目时约每 25s 自动轮换一把新的一次性密钥(防已用/过期)
944
+ var STATUS_POLL_MS = 5000; // 连接状态行轮询间隔
945
+
946
+ /** 轻量状态轮询:只更新 st,不改 mode(mode 由用户 Tab 选择决定,避免轮询把自建/云端来回切)。 */
947
+ function pollStatus() {
948
+ api("/dsh-remote/status").then(function (body) {
949
+ if (!body || !body.ok) return;
950
+ setSt(body);
951
+ }).catch(function () {});
952
+ }
953
+
954
+ // 时间工具(兼容 epoch 毫秒 / ISO 字符串 / 数字字符串)
955
+ function toMs(ts) {
956
+ if (ts === null || ts === undefined || ts === "") return 0;
957
+ var n = Number(ts);
958
+ if (String(ts).trim() !== "" && isFinite(n)) return n;
959
+ var d = new Date(ts);
960
+ return isNaN(d.getTime()) ? 0 : d.getTime();
961
+ }
962
+ function pad2(n) { return String(n).padStart(2, "0"); }
963
+ function fmtClock(ts) {
964
+ var ms = toMs(ts);
965
+ if (!ms) return "--:--:--";
966
+ var d = new Date(ms);
967
+ if (isNaN(d.getTime())) return "--:--:--";
968
+ return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
969
+ }
970
+ function fmtRemain(ms) {
971
+ var s = Math.max(0, Math.floor((ms || 0) / 1000));
972
+ var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
973
+ return (h > 0 ? pad2(h) + ":" : "") + pad2(m) + ":" + pad2(sec);
974
+ }
975
+ function fmtDT(ts) {
976
+ var ms = toMs(ts);
977
+ if (!ms) return "—";
978
+ var d = new Date(ms);
979
+ if (isNaN(d.getTime())) return "—";
980
+ return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + " " + pad2(d.getHours()) + ":" + pad2(d.getMinutes());
981
+ }
982
+
983
+ /** 创建(或刷新)一次性访问密钥:GET /dsh-remote/access-key(node 半转发企业端 /api/auth-key)。 */
984
+ function loadAccessKey() {
985
+ if (akeyBusy) return;
986
+ // 登录前不请求企业端(避免 401/404 噪音);由账号登录成功后触发
987
+ if (!(st && st.config && st.config.phone)) {
988
+ setAkeyMsg(null);
989
+ return;
990
+ }
991
+ setAkeyBusy(true);
992
+ api("/dsh-remote/access-key").then(function (b) {
993
+ if (!b || !b.ok) {
994
+ var why = (b && (b.error || (b.body && b.body.error))) || "未知错误";
995
+ setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:" + why });
996
+ return;
997
+ }
998
+ // 契约容错:url 必须有;qr_data_url 取不到时仍展示链接/复制/直接打开
999
+ if (!b.url) {
1000
+ setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:企业端未返回可用链接" });
1001
+ return;
1002
+ }
1003
+ setAkey({
1004
+ url: b.url || "",
1005
+ key: b.key != null ? b.key : null,
1006
+ expires_at: b.expires_at != null ? b.expires_at : null,
1007
+ ttl_ms: b.ttl_ms != null ? b.ttl_ms : null,
1008
+ qr_data_url: b.qr_data_url != null ? b.qr_data_url : null
1009
+ });
1010
+ setAkeyMsg(null);
1011
+ }).catch(function (e) {
1012
+ setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:" + e.message });
1013
+ }).finally(function () { setAkeyBusy(false); });
1014
+ }
1015
+
1016
+ var copyKeyUrl = function () {
1017
+ if (!(akey && akey.url)) return;
1018
+ try {
1019
+ navigator.clipboard.writeText(akey.url).then(function () {
1020
+ setCopiedKey(true);
1021
+ setTimeout(function () { setCopiedKey(false); }, 1500);
1022
+ });
1023
+ } catch (e) {}
1024
+ };
1025
+ /** 「直接打开」:浏览器新标签打开一次性访问地址(打开即扫码/点击进入)。 */
1026
+ var openKeyUrl = function () {
1027
+ if (!(akey && akey.url)) return;
1028
+ try { window.open(akey.url, "_blank", "noopener"); } catch (e) {}
1029
+ };
1030
+
1031
+ /** 加载已授权设备列表:GET /dsh-remote/mobile-sessions。 */
1032
+ function loadDevices() {
1033
+ if (devBusy !== "") return;
1034
+ setDevBusy("list");
1035
+ api("/dsh-remote/mobile-sessions").then(function (b) {
1036
+ if (!b || !b.ok) throw new Error((b && b.error) || "加载已授权设备失败");
1037
+ setDevSessions(Array.isArray(b.sessions) ? b.sessions : []);
1038
+ setDevMsg(null);
1039
+ }).catch(function (e) {
1040
+ setDevMsg({ kind: "err", text: "加载已授权设备失败:" + e.message });
1041
+ }).finally(function () { setDevBusy(""); });
1042
+ }
1043
+
1044
+ var toggleDevices = function () {
1045
+ var next = !devOpen;
1046
+ setDevOpen(next);
1047
+ if (next && devSessions === null && devBusy === "") loadDevices();
1048
+ if (!next) setArmedDev(null);
1049
+ };
1050
+
1051
+ /** 取消配对:先点一次进入确认态,再点一次才 POST revoke(同 SelfManageCard 二次确认风格)。 */
1052
+ var doRevokeDevice = function (id) {
1053
+ if (!id) return;
1054
+ if (armedDev !== id) { setArmedDev(id); return; }
1055
+ setDevBusy("revoke:" + id);
1056
+ post("/dsh-remote/mobile-sessions/revoke", { id: id }).then(function (b) {
1057
+ if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "取消失败");
1058
+ setArmedDev(null);
1059
+ setDevMsg({ kind: "ok", text: "已取消,对方需重新扫码/登录" });
1060
+ // 刷新列表(成功即重拉,行内状态随后由列表覆盖)
1061
+ api("/dsh-remote/mobile-sessions").then(function (lb) {
1062
+ if (lb && lb.ok) setDevSessions(Array.isArray(lb.sessions) ? lb.sessions : []);
1063
+ }).catch(function () {});
1064
+ }).catch(function (e) {
1065
+ setArmedDev(null);
1066
+ setDevMsg({ kind: "err", text: "取消配对失败:" + e.message });
1067
+ }).finally(function () { setDevBusy(""); });
1068
+ };
1069
+
1070
+ /** 升级/续费带登录态打开:先取一次性访问 url,再 window.open(node 半用账号 JWT 换 key)。 */
1071
+ var openUpgradeAuth = function () {
1072
+ setBusy("upgrade");
1073
+ api("/dsh-remote/access-key").then(function (b) {
1074
+ if (!b || !b.ok || !b.url) throw new Error((b && b.error) || "生成访问链接失败");
1075
+ try { window.open(b.url, "_blank", "noopener"); } catch (e2) {}
1076
+ setMsg("ok", "✅ 升级/续费页已在新标签页打开(带登录态)");
1077
+ }).catch(function (e) {
1078
+ setMsg("err", "打开升级/续费页失败:" + e.message);
1079
+ }).finally(function () { setBusy(""); });
1080
+ };
1081
+
1082
+ // 停留主视图(home)时:每 5s 轻量轮询连接状态 + 每秒刷新倒计时
1083
+ useEffect(function () {
1084
+ if (view !== "home") return;
1085
+ var pollIv = setInterval(function () { pollStatus(); }, STATUS_POLL_MS);
1086
+ var tickIv = setInterval(function () { setNowTick(Date.now()); }, 1000);
1087
+ return function () {
1088
+ clearInterval(pollIv);
1089
+ clearInterval(tickIv);
1090
+ };
1091
+ }, [view]);
1092
+
1093
+ // 已登录云端主视图停留期间:打开即取一把新 key + 已授权设备;之后每 ~25s 自动轮换
1094
+ // (未登录不轮询,避免 401 空转;离开栏目/切视图/退出登录即清理定时器)
1095
+ useEffect(function () {
1096
+ if (view !== "home" || mode !== "saas" || !loggedIn) return undefined;
1097
+ loadAccessKey();
1098
+ loadDevices();
1099
+ var rotateIv = setInterval(function () { loadAccessKey(); }, KEY_AUTO_REFRESH_MS);
1100
+ return function () { clearInterval(rotateIv); };
1101
+ }, [view, mode, loggedIn]);
1102
+
851
1103
  function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
852
1104
 
853
1105
  // ---------- 登录(密码/短信) ----------
@@ -948,11 +1200,6 @@ window.__ModuleLoader__.load({
948
1200
  .finally(function () { setBusy(""); });
949
1201
  };
950
1202
 
951
- var copyUrl = function () {
952
- if (!st) return;
953
- try { navigator.clipboard.writeText(st.remoteUrl).then(function () { setCopied(true); setTimeout(function () { setCopied(false); }, 1500); }); } catch (e) {}
954
- };
955
-
956
1203
  var saveLocal = function () {
957
1204
  if (!selfHost.trim() || !localKey.trim()) { setMsg("err", "请填写服务器地址与访问密钥"); return; }
958
1205
  setBusy("local");
@@ -991,12 +1238,6 @@ window.__ModuleLoader__.load({
991
1238
  if (!ts) return "—";
992
1239
  try { var d = new Date(Number(ts)); return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); } catch (e) { return "—"; }
993
1240
  }
994
- /** 推广/升级页地址(/app/promo),取不到时返回空(按钮隐藏)。 */
995
- function promoUrl() {
996
- var base = (pub && pub.app_url) || (st && st.remoteUrl) || "";
997
- if (!base) return "";
998
- return base.replace(/\/+$/, "") + "/promo";
999
- }
1000
1241
  function renderAccount() {
1001
1242
  var a = account;
1002
1243
  var plan = a ? a.plan : "free";
@@ -1004,7 +1245,6 @@ window.__ModuleLoader__.load({
1004
1245
  var isMember = plan === "pro" || plan === "pro_max";
1005
1246
  var endsAt = a && (a.plan_ends_at || a.trial_expires_at) ? Number(a.plan_ends_at || a.trial_expires_at) : 0;
1006
1247
  var quotaPct = quota && quota.limit_enabled ? quota.percent : null;
1007
- var promo = promoUrl();
1008
1248
  var planText;
1009
1249
  if (!isMember) planText = "免费额度: 带宽 ≈1Mbps" + (quotaPct !== null ? " · 本月流量已用 " + quotaPct + "%" : " · 本月流量限额 1GB");
1010
1250
  else if (source === "trial") planText = "试用 PRO 会员 · 到期 " + fmtDate(a.trial_expires_at);
@@ -1024,15 +1264,15 @@ window.__ModuleLoader__.load({
1024
1264
  h("span", null, planText)
1025
1265
  ),
1026
1266
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
1027
- promo && h("a", { className: "dru-btn dru-btn-primary", style: { textDecoration: "none", display: "inline-flex", alignItems: "center" }, href: promo, target: "_blank", rel: "noopener" },
1028
- !isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员"),
1267
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { display: "inline-flex", alignItems: "center" }, disabled: busy !== "", title: "升级/续费(带登录态打开)", onClick: openUpgradeAuth },
1268
+ busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
1029
1269
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友赚会员"),
1030
1270
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
1031
1271
  h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
1032
1272
  ),
1033
- endsAt && isMember ? h("div", { className: "dru-hint", style: { marginTop: 8 } },
1034
- "到期后如需继续使用会员权益,请在到期前续费。" + (promo ? "升级/续费入口在推广页。" : "")
1035
- ) : null
1273
+ h("div", { className: "dru-hint", style: { marginTop: 8 } },
1274
+ "升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
1275
+ (endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : ""))
1036
1276
  );
1037
1277
  }
1038
1278
 
@@ -1087,6 +1327,132 @@ window.__ModuleLoader__.load({
1087
1327
  );
1088
1328
  }
1089
1329
 
1330
+ // ---------- 📱 远程访问卡:一次性访问地址 / 二维码 / 状态行 ----------
1331
+ function renderAccessCard() {
1332
+ var hasKey = !!(akey && akey.url);
1333
+ var expMs = akey ? toMs(akey.expires_at) : 0;
1334
+ var remainMs = expMs ? expMs - nowTick : 0;
1335
+ var loggedInSaaS = !!(st && st.config && st.config.phone);
1336
+ var statusTxt = st === null
1337
+ ? "查询中…"
1338
+ : !loggedInSaaS
1339
+ ? "请先登录(下方账号卡片)后启用远程访问"
1340
+ : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
1341
+ var dotCls = "dru-dot " + (loggedInSaaS && serviceRunning ? "dru-dot-on" : "dru-dot-off");
1342
+ return card("📱 远程访问", [
1343
+ h("div", { className: "dru-status-line" },
1344
+ h("span", { className: dotCls }),
1345
+ h("span", null, statusTxt)
1346
+ ),
1347
+ akeyMsg ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } }, akeyMsg.text) : null,
1348
+ hasKey ? h("div", null, [
1349
+ h("div", { className: "dru-url big", style: { marginTop: 8 } },
1350
+ h("span", null, akey.url),
1351
+ h("button", { type: "button", onClick: copyKeyUrl }, copiedKey ? "已复制" : "复制")
1352
+ ),
1353
+ h("div", { className: "dru-access-flex" },
1354
+ h("div", { className: "dru-access-col", style: { alignItems: "center" } },
1355
+ akey.qr_data_url
1356
+ ? h("img", { className: "dru-qr-img", src: akey.qr_data_url, alt: "远程访问二维码" })
1357
+ : h("div", { className: "dru-qr-ph" }, "二维码生成中 / 暂不可用\n链接仍可复制或直接打开"),
1358
+ h("div", { className: "dru-cd" + (remainMs > 0 ? " dru-cd-ok" : "") },
1359
+ "有效至 " + fmtClock(akey.expires_at) + " · 剩余 " + fmtRemain(remainMs))
1360
+ ),
1361
+ h("div", { className: "dru-access-col" },
1362
+ h("div", { className: "dru-key-note" },
1363
+ "扫码即进入远程访问;每次生成的链接 30 分钟有效、访问一次后失效,停留栏目期间会自动更新。"),
1364
+ h("div", { className: "dru-actions", style: { marginTop: 2 } },
1365
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: akeyBusy, onClick: openKeyUrl }, "直接打开"),
1366
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: akeyBusy, onClick: loadAccessKey }, akeyBusy ? "生成中…" : "刷新二维码/访问链接")
1367
+ ),
1368
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
1369
+ !serviceRunning
1370
+ ? "本机 bridge 未运行:请先在下方「🖥 Bridge 服务」卡片启动,手机/另一台电脑才能连入本机。"
1371
+ : "手机上打开链接点「进入」即可像在本机一样使用 dsh web。")
1372
+ )
1373
+ )
1374
+ ]) : h("div", null, [
1375
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
1376
+ loggedInSaaS
1377
+ ? "正在生成一次性访问链接…(手机或另一台电脑扫码/打开即可进入)"
1378
+ : "登录下方「🔑 账号」卡片中的手机号账号后,即可生成一次性访问链接,让手机或另一台电脑远程使用同一份 dsh web。"),
1379
+ h("div", { className: "dru-actions", style: { marginTop: 8 } },
1380
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: akeyBusy, onClick: loadAccessKey },
1381
+ akeyBusy ? "生成中…" : "生成访问链接")
1382
+ )
1383
+ ])
1384
+ ]);
1385
+ }
1386
+
1387
+ // ---------- 📲 已授权设备卡(展开列表 + 二次确认取消配对) ----------
1388
+ function deviceLabel(s) {
1389
+ if (s && s.label) return String(s.label);
1390
+ var parts = [];
1391
+ if (s && s.os) parts.push(String(s.os));
1392
+ if (s && s.browser) parts.push(String(s.browser));
1393
+ return parts.length ? parts.join(" ") : "未知设备";
1394
+ }
1395
+ function deviceMeta(s) {
1396
+ var parts = [];
1397
+ if (s && s.os) parts.push(String(s.os));
1398
+ if (s && s.browser) parts.push(String(s.browser));
1399
+ return parts.join(" · ");
1400
+ }
1401
+ function renderDeviceList() {
1402
+ if (devSessions === null) {
1403
+ return h("div", { className: "dru-hint" }, devBusy === "list" ? "正在加载已授权设备…" : "加载已授权设备中,请稍候或点上方按钮重试");
1404
+ }
1405
+ if (devSessions.length === 0) {
1406
+ return h("div", { className: "dru-fb-empty" }, "暂无已授权设备(手机扫码后出现)");
1407
+ }
1408
+ return h("div", null, [
1409
+ devSessions.map(function (s) {
1410
+ var revoked = !!(s && s.revoked_at);
1411
+ return h("div", { key: s && s.id, className: "dru-dev" },
1412
+ h("div", { className: "dru-dev-top" },
1413
+ h("span", { className: "dru-dev-name" }, deviceLabel(s)),
1414
+ deviceMeta(s) ? h("span", { className: "dru-dev-meta" }, deviceMeta(s)) : null,
1415
+ revoked ? h("span", { className: "dru-dev-tag dru-dev-tag-off" }, "已取消配对") : null
1416
+ ),
1417
+ h("div", { className: "dru-dev-sub" },
1418
+ "首次配对 " + fmtDT(s && s.created_at) +
1419
+ (s && s.last_seen_at ? " · 最近活跃 " + fmtDT(s.last_seen_at) : "") +
1420
+ (revoked ? " · 取消于 " + fmtDT(s.revoked_at) : "")
1421
+ ),
1422
+ revoked ? null : h("div", { className: "dru-actions", style: { marginTop: 8 } },
1423
+ h("button", {
1424
+ type: "button",
1425
+ className: "dru-btn dru-btn-danger",
1426
+ disabled: devBusy !== "",
1427
+ onClick: function () { doRevokeDevice(s && s.id); }
1428
+ }, devBusy === "revoke:" + (s && s.id) ? "取消中…" : armedDev === (s && s.id) ? "⚠ 再点一次确认取消配对" : "取消配对")
1429
+ )
1430
+ );
1431
+ }),
1432
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
1433
+ "取消配对后,对方需重新扫码/登录才能再次远程访问本机。")
1434
+ ]);
1435
+ }
1436
+ function renderDevicesCard() {
1437
+ var count = devSessions === null ? null : devSessions.length;
1438
+ var btnLabel = devOpen
1439
+ ? "收起已授权设备列表"
1440
+ : "已授权设备 " + (count === null ? "…" : count) + "(点击展开管理)";
1441
+ return card("📲 已授权设备", [
1442
+ h("div", { className: "dru-actions", style: { marginTop: 2 } },
1443
+ h("button", {
1444
+ type: "button",
1445
+ className: "dru-btn dru-btn-ghost",
1446
+ style: { width: "100%" },
1447
+ disabled: devBusy !== "",
1448
+ onClick: toggleDevices
1449
+ }, devBusy === "list" ? "加载中…" : btnLabel)
1450
+ ),
1451
+ devOpen ? renderDeviceList() : null,
1452
+ devMsg ? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 6 } }, devMsg.text) : null
1453
+ ]);
1454
+ }
1455
+
1090
1456
  // ---------- 主视图 ----------
1091
1457
  function renderHome() {
1092
1458
  var isLocal = mode === "local";
@@ -1096,16 +1462,11 @@ window.__ModuleLoader__.load({
1096
1462
  h("div", { className: "dru-tab" + (!isLocal ? " active" : ""), onClick: function () { setMode("saas"); setMessage(null); } }, "☁️ 云端服务"),
1097
1463
  h("div", { className: "dru-tab" + (isLocal ? " active" : ""), onClick: function () { setMode("local"); setMessage(null); } }, "🖥 自建服务")
1098
1464
  ),
1099
- // 云端 tab:远程控制地址 + 账号(手机号登录,官方托管)
1465
+ // 云端 tab:📱 远程访问(一次性扫码访问 + 已授权设备)+ 账号(手机号登录,官方托管)
1100
1466
  !isLocal ? h("div", null,
1101
- card("📱 远程控制地址",
1102
- h("div", { className: "dru-url" },
1103
- h("span", null, st ? st.remoteUrl : "加载中…"),
1104
- h("button", { type: "button", onClick: copyUrl }, copied ? "已复制" : "复制")
1105
- ),
1106
- h("div", { className: "dru-hint", style: { marginTop: 6 } },
1107
- st && st.relayReachable === false ? "⚠ 云端服务不可达(地址可能不是最新)" : "手机浏览器打开此地址登录后即可远程控制本机。")
1108
- ),
1467
+ renderAccessCard(),
1468
+ // 已授权设备:仅登录后展示(登录前不显示,避免空列表/误导)
1469
+ loggedIn && st.config.phone ? renderDevicesCard() : null,
1109
1470
  card("🔑 账号",
1110
1471
  st === null
1111
1472
  ? h("div", { className: "dru-hint" }, "正在读取远控状态…")
@@ -1188,12 +1549,12 @@ window.__ModuleLoader__.load({
1188
1549
  )
1189
1550
  : null
1190
1551
  ),
1191
- // 关于 dsh-remote(开源项目说明卡片)
1552
+ // 关于 dsh-remote(v0.5+ 远程访问价值说明卡片)
1192
1553
  card("📖 关于 dsh-remote", [
1193
- h("div", { className: "dru-hint", style: { marginBottom: 6 } }, " 为什么推荐用 SaaS:不用自己买服务器、不用折腾部署,装好客户端就能用,最省心。"),
1194
- h("div", { className: "dru-hint", style: { marginBottom: 6 } }, " 会员费去向:付的是网络带宽/服务器成本,也是给开发者的合理支持,让项目持续维护。"),
1195
- h("div", { className: "dru-hint", style: { marginBottom: 6 } }, " 也可以自建:项目完全开源,有服务器可自行部署,流量走自己的服务器,闭环自控。"),
1196
- h("div", { className: "dru-hint" }, " 一句话总结:简单省心用 SaaS,技术玩家可自建。")
1554
+ h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
1555
+ h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
1556
+ h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
1557
+ h("div", { className: "dru-hint" }, "🛡 端到端流量保护:可选对通道做端到端加密保护,传输全程不暴露本机公网 IP(详见项目 README「安全」说明)。")
1197
1558
  ]),
1198
1559
  // 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
1199
1560
  h(SelfManageCard, null),
@@ -1201,12 +1562,12 @@ window.__ModuleLoader__.load({
1201
1562
  );
1202
1563
  }
1203
1564
 
1204
- return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程控制" },
1565
+ return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程访问" },
1205
1566
  h("div", { className: "dru-settings-head" },
1206
- h("span", { className: "dru-settings-icon" }, "🖥"),
1567
+ h("span", { className: "dru-settings-icon" }, "📱"),
1207
1568
  h("div", null,
1208
- h("h2", { className: "dru-settings-title" }, "远程控制"),
1209
- h("div", { className: "dru-settings-sub" }, "通过手机远程控制本机 dsh web")
1569
+ h("h2", { className: "dru-settings-title" }, "远程访问"),
1570
+ h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)")
1210
1571
  )
1211
1572
  ),
1212
1573
  h("div", { className: "dru-settings-body" },
@@ -1220,13 +1581,13 @@ window.__ModuleLoader__.load({
1220
1581
  function apply(ctx) {
1221
1582
  // 面板入口迁移:从侧边栏(sidebar.footer.action)移入「设置」页官方扩展点
1222
1583
  // settings.section(列表槽,由 ui-settings-general 在 sidebar.settings 下声明)。
1223
- // order 30 > Agent 预设(20),栏目落在「Agent 预设」下方;label 即栏目名(🖥 通用远程控制图标)。
1584
+ // order 30 > Agent 预设(20),栏目落在「Agent 预设」下方;label 即栏目名(📱 远程访问)。
1224
1585
  ctx.slots.inject("settings.section", function () {
1225
1586
  return ctx.slots.register({
1226
1587
  name: "settings.section",
1227
1588
  id: "dsh-remote",
1228
1589
  order: 30,
1229
- label: function () { return "🖥 远程控制"; }
1590
+ label: function () { return "📱 远程访问"; }
1230
1591
  }, RemoteControlSection);
1231
1592
  });
1232
1593
  ctx.slots.inject("shell.overlay", function () {