dsh-remote-ui 0.6.0

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/lib/client.js ADDED
@@ -0,0 +1,2078 @@
1
+ // dsh-remote-web — browser half(手写 bundle,无需构建)
2
+ //
3
+ // 格式遵循 dsh 浏览器插件约定(双半插件,bundle 手写无构建):
4
+ // window.__ModuleLoader__.load({ id: <包名>, factory: (require) => {...} })
5
+ // factory 内只能 require shell 种子模块(react / react/jsx-runtime 等)。
6
+ //
7
+ // 功能:
8
+ // - 双入口(2026-09 恢复侧栏注入,与官方共存不遮挡):
9
+ // ① 官方「设置」→「远程访问」栏目(settings.section,位于「Agent 预设」下方,官方扩展点);
10
+ // ② 左侧官方「设置」按钮旁注入独立「📱 远程访问」快捷按钮(id=dru-nav-remote,非 clone,
11
+ // 插在其前方、同布局不覆盖官方按钮;点击直达该栏目,首次带引导红点)
12
+ // - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
13
+ // · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册 tabs
14
+ // · 🔒 修改密码(账号卡):已登录账号经 图形验证码+短信验证码 重置(/dsh-remote/password/reset 代理);
15
+ // 成功后全部已授权设备/会话失效 → 本地登出并提示用新密码重新登录(覆盖本机 config 密码)
16
+ // · 忘记密码(登录卡):未登录时登录表单内「忘记密码?」→ 同一套
17
+ // 图形验证码+短信验证码 表单(手机号预填当前输入、可改),成功后本地登出并提示「请用新密码登录」
18
+ // · 登录要求图形验证码;注册要求两次密码 + 图形验证码
19
+ // · 📱 远程访问卡:一次性访问密钥(扫码/直接打开/复制 + 到期倒计时自动刷新)与已授权设备管理
20
+ // · bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
21
+ // - 首次安装引导:设置页「远程访问」栏目(官方导航 navCell)旁小红点(localStorage dsh-remote-seen-dot 控制)
22
+ // - shell.overlay:满意度弹窗(安装体验至少 1 小时后弹出,只弹一次)
23
+ // 所有数据经同源 /dsh-remote/* 宿主路由读写(node 半提供)。
24
+ window.__ModuleLoader__.load({
25
+ id: "dsh-remote-ui",
26
+ factory: (require) => {
27
+ var module = { exports: {} };
28
+ var exports = module.exports;
29
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
30
+
31
+ var react = require("react");
32
+ var h = react.createElement;
33
+ var useState = react.useState;
34
+ var useEffect = react.useEffect;
35
+ var useCallback = react.useCallback;
36
+ var useSyncExternalStore = react.useSyncExternalStore;
37
+
38
+ // ── 样式(浅色高对比,data-plugin 便于 HMR 清理) ───────────────────────
39
+ // 面板主体内嵌于 DSH Web「设置」页的 settings.section 栏目(官方扩展点),
40
+ // 不再使用侧边栏入口与全窗浮动层,故不再需要 .dru-entry/.dru-backdrop/.dru-panel。
41
+ var styleEl = document.createElement("style");
42
+ styleEl.setAttribute("data-plugin", "dsh-remote-ui");
43
+ styleEl.textContent = [
44
+ // 设置页栏目容器(nav 选中后渲染在 settings.section 内容区)
45
+ ".dru-settings-section{max-width:720px;display:flex;flex-direction:column;gap:14px;padding-top:2px}",
46
+ ".dru-settings-head{display:flex;align-items:center;gap:10px;padding:6px 2px 2px}",
47
+ ".dru-settings-icon{font-size:24px;line-height:1;flex:none}",
48
+ ".dru-settings-title{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,#e6edf3)}",
49
+ ".dru-settings-sub{font-size:12px;color:var(--dsw-alias-label-tertiary,#8c959f);margin-top:2px}",
50
+ ".dru-settings-body{display:flex;flex-direction:column;gap:14px}",
51
+ // 首次安装引导小红点(挂在设置页「远程访问」导航栏目右上角)
52
+ ".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}",
53
+ ".dru-card{background:#f6f8fa;border:1px solid #eaeef2;border-radius:10px;padding:14px 16px}",
54
+ ".dru-card h3{margin:0 0 8px;font-size:13px;font-weight:700;color:#1f2328}",
55
+ ".dru-url{background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:9px 11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;word-break:break-all;display:flex;align-items:center;justify-content:space-between;gap:8px;color:#1f2328}",
56
+ ".dru-url button{flex:none;border:1px solid #d0d7de;background:#ffffff;color:#0969da;border-radius:6px;padding:3px 8px;font-size:12px;cursor:pointer}",
57
+ ".dru-url button:hover{background:#f6f8fa}",
58
+ ".dru-field{margin-bottom:11px}",
59
+ ".dru-field > label{display:block;font-size:12.5px;font-weight:600;color:#1f2328;margin-bottom:5px}",
60
+ ".dru-input{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
61
+ ".dru-input:focus{outline:none;border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
62
+ ".dru-actions{display:flex;gap:8px;flex-wrap:wrap}",
63
+ ".dru-btn{padding:8px 14px;border-radius:8px;border:1px solid transparent;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit}",
64
+ ".dru-btn:disabled{opacity:.55;cursor:default}",
65
+ ".dru-btn-primary{background:#0969da;color:#ffffff;border-color:#0969da}",
66
+ ".dru-btn-primary:hover:not(:disabled){background:#0860bd}",
67
+ ".dru-btn-ghost{background:#ffffff;color:#1f2328;border-color:#d0d7de}",
68
+ ".dru-btn-ghost:hover:not(:disabled){background:#f6f8fa}",
69
+ ".dru-btn-danger{background:#ffffff;color:#cf222e;border-color:#cf222e}",
70
+ ".dru-btn-danger:hover:not(:disabled){background:#fff0f1}",
71
+ ".dru-linkbtn{display:inline-block;padding:0;border:none;background:none;color:#0969da;font-size:12.5px;line-height:1.6;cursor:pointer;font-family:inherit;text-decoration:none}",
72
+ ".dru-linkbtn:hover{text-decoration:underline}",
73
+ ".dru-tabs{display:flex;gap:8px;margin-bottom:12px}",
74
+ ".dru-tab{flex:1;padding:7px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de;user-select:none}",
75
+ ".dru-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
76
+ ".dru-captcha{display:flex;align-items:stretch;gap:8px}",
77
+ ".dru-captcha .dru-input{flex:1;min-width:0}",
78
+ ".dru-captcha-box{width:118px;height:42px;flex:none;border-radius:8px;border:1px solid #d0d7de;cursor:pointer;background:#f6f8fa;display:flex;align-items:center;justify-content:center;font-size:12px;color:#57606a;overflow:hidden}",
79
+ ".dru-captcha-box svg{display:block;width:100%;height:100%}",
80
+ ".dru-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}",
81
+ ".dru-avatar{width:40px;height:40px;border-radius:50%;background:#0969da;color:#ffffff;display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:700;flex-shrink:0}",
82
+ ".dru-user .mail{font-size:14px;font-weight:600;color:#1f2328;word-break:break-all}",
83
+ ".dru-user .plan{font-size:12px;color:#57606a;margin-top:2px}",
84
+ ".dru-status-line{display:flex;align-items:center;gap:8px;font-size:13px;color:#1f2328}",
85
+ ".dru-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex:none}",
86
+ ".dru-dot-on{background:#1a7f37;box-shadow:0 0 6px rgba(26,127,55,.6)}",
87
+ ".dru-dot-off{background:#8c959f}",
88
+ ".dru-meta{font-size:12px;color:#57606a;margin-top:6px;word-break:break-all}",
89
+ ".dru-msg{font-size:12.5px;min-height:18px;margin-top:8px}",
90
+ ".dru-msg-ok{color:#1a7f37}",
91
+ ".dru-msg-err{color:#cf222e}",
92
+ ".dru-msg-warn{color:#9a6700}",
93
+ ".dru-hint{font-size:12px;color:#57606a;line-height:1.6}",
94
+ // ── 用户反馈模块 ──
95
+ ".dru-fb-tabs{display:flex;gap:8px;margin-bottom:10px}",
96
+ ".dru-fb-tab{flex:1;padding:6px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;color:#57606a;background:#eaeef2;border:1px solid #d0d7de;user-select:none}",
97
+ ".dru-fb-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
98
+ ".dru-fb-select{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
99
+ ".dru-fb-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit;resize:vertical;min-height:64px}",
100
+ ".dru-fb-textarea:focus{outline:none;border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
101
+ ".dru-fb-item{border:1px solid #eaeef2;border-radius:8px;background:#ffffff;padding:10px 12px;margin-bottom:8px}",
102
+ ".dru-fb-item-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap}",
103
+ ".dru-fb-badge{font-size:11px;border-radius:999px;padding:1px 8px;flex:none}",
104
+ ".dru-fb-badge-open{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
105
+ ".dru-fb-badge-processing{color:#0969da;background:#ddf4ff;border:1px solid #b6e3ff}",
106
+ ".dru-fb-badge-done{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
107
+ ".dru-fb-cat{font-size:11px;border-radius:999px;padding:1px 8px;flex:none;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de}",
108
+ ".dru-fb-item-title{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:120px}",
109
+ ".dru-fb-item-content{font-size:12.5px;color:#57606a;white-space:pre-wrap;word-break:break-word;margin:4px 0}",
110
+ ".dru-fb-item-time{font-size:11px;color:#8c959f}",
111
+ ".dru-fb-reply{border-top:1px dashed #eaeef2;margin-top:8px;padding-top:8px}",
112
+ ".dru-fb-reply-row{display:flex;gap:6px;align-items:flex-start;margin-bottom:6px}",
113
+ ".dru-fb-reply-who{font-size:12px;font-weight:600;color:#0969da;flex:none;width:76px}",
114
+ ".dru-fb-reply-who.user{color:#57606a}",
115
+ ".dru-fb-reply-text{font-size:12.5px;color:#1f2328;white-space:pre-wrap;word-break:break-word;flex:1}",
116
+ ".dru-fb-reply-input{width:100%;box-sizing:border-box;padding:7px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;font-size:12.5px;font-family:inherit;resize:vertical;min-height:44px}",
117
+ ".dru-fb-empty{font-size:12.5px;color:#8c959f;text-align:center;padding:14px 0}",
118
+ // ── 满意度弹窗(1 小时体验后,只弹一次) ──
119
+ ".dru-popup{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:2147482000;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}",
120
+ ".dru-popup-card{width:min(400px,calc(100vw - 48px));background:#ffffff;color:#1f2328;border:1px solid #d0d7de;border-radius:12px;box-shadow:0 24px 64px rgba(0,0,0,.45);font-size:14px;line-height:1.5;font-family:var(--dsw-font-family,-apple-system,'PingFang SC','Microsoft YaHei',sans-serif);overflow:hidden}",
121
+ ".dru-popup-body{padding:22px 22px 16px;text-align:center}",
122
+ ".dru-popup-icon{font-size:34px;margin-bottom:8px}",
123
+ ".dru-popup-title{font-size:16px;font-weight:700;color:#1f2328;margin-bottom:6px}",
124
+ ".dru-popup-sub{font-size:12.5px;color:#57606a;margin-bottom:16px}",
125
+ ".dru-popup-rate{display:flex;gap:10px;justify-content:center;margin-bottom:14px}",
126
+ ".dru-popup-rate button{flex:1;max-width:96px;border:1px solid #d0d7de;background:#f6f8fa;border-radius:10px;padding:12px 6px;font-size:20px;cursor:pointer;font-family:inherit}",
127
+ ".dru-popup-rate button:hover{border-color:#0969da;background:#ddf4ff}",
128
+ ".dru-popup-rate button.sel{border-color:#0969da;background:#ddf4ff;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
129
+ ".dru-popup-rate button .lbl{display:block;font-size:11px;color:#57606a;margin-top:4px;font-weight:600}",
130
+ ".dru-popup-textarea{width:100%;box-sizing:border-box;padding:9px 11px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13px;font-family:inherit;resize:vertical;min-height:56px;text-align:left}",
131
+ ".dru-popup-invite{background:#f6f8fa;border:1px dashed #d0d7de;border-radius:8px;padding:12px;font-size:13px;color:#57606a;text-align:center;margin-bottom:12px}",
132
+ ".dru-popup-actions{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:14px}",
133
+ ".dru-popup-foot{display:flex;justify-content:space-between;padding:10px 22px;border-top:1px solid #eaeef2;font-size:12px;color:#8c959f}",
134
+ ".dru-popup-foot button{border:none;background:none;color:#57606a;cursor:pointer;font-size:12px;font-family:inherit;padding:4px 6px}",
135
+ ".dru-popup-foot button:hover{color:#0969da}",
136
+ ".dru-popup .dru-msg{text-align:left}",
137
+ // ── 自管理:版本与更新(插件面板内提供在线更新/彻底卸载,市场无更新按钮) ──
138
+ ".dru-ver-badge{display:inline-block;font-size:11px;border-radius:999px;padding:1px 8px;margin-left:6px;vertical-align:1px}",
139
+ ".dru-ver-badge-new{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
140
+ ".dru-ver-badge-ok{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
141
+ ".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}",
142
+ // ── 📱 远程访问(一次性访问密钥 + 已授权设备管理) ──
143
+ ".dru-access-flex{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-start;margin-top:10px}",
144
+ ".dru-access-col{flex:1;min-width:230px;display:flex;flex-direction:column;gap:8px}",
145
+ ".dru-qr-img{width:180px;height:180px;flex:none;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;object-fit:contain}",
146
+ ".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}",
147
+ ".dru-url.big{font-size:13.5px;font-weight:600}",
148
+ ".dru-cd{font-size:12px;color:#9a6700;margin-top:2px}",
149
+ ".dru-cd-ok{color:#1a7f37}",
150
+ ".dru-key-note{font-size:12px;color:#57606a;line-height:1.6}",
151
+ ".dru-dev{border:1px solid #eaeef2;border-radius:8px;background:#ffffff;padding:9px 11px;margin-bottom:8px}",
152
+ ".dru-dev-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
153
+ ".dru-dev-name{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:130px}",
154
+ ".dru-dev-meta{font-size:11.5px;color:#57606a}",
155
+ ".dru-dev-sub{font-size:11px;color:#8c959f;margin-top:3px}",
156
+ ".dru-dev-tag{font-size:10.5px;color:#8c959f;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
157
+ ".dru-dev-tag-off{color:#cf222e;border-color:#ffb3b6;background:#fff0f1}",
158
+ // Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
159
+ ".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:#57606a}",
160
+ ".dru-e2ee-line.ok{color:#1a7f37}",
161
+ // 侧栏「远程访问」快捷按钮(2026-09 恢复注入,与官方「设置」按钮共存不遮挡):
162
+ // 独立 button(非 clone),插在官方「设置」按钮之前,同源同布局不覆盖官方热区。
163
+ ".dru-nav-remote{display:inline-flex;align-items:center;gap:6px;height:30px;margin:0 0 2px;padding:0 10px;border:1px solid #d0d7de;border-radius:8px;background:#ffffff;color:#0969da;font:500 12.5px/1 inherit;cursor:pointer;white-space:nowrap;user-select:none}",
164
+ ".dru-nav-remote:hover:not(:disabled){background:#f0f6ff;border-color:#0969da}",
165
+ ".dru-nav-remote-dot{position:absolute;top:-2px;right:-2px;width:8px;height:8px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px #fff;pointer-events:none}",
166
+ ].join("\n");
167
+ document.head.appendChild(styleEl);
168
+
169
+ // ── 首次安装引导小红点(设置页「远程访问」栏目,localStorage 控制) ────
170
+ // 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
171
+ // 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
172
+ // 设置页由官方 shell 渲染(class 含 navCell 的导航按钮 + 栏目 label),
173
+ // 注入采用 DOM 兜底:MutationObserver 监听设置页打开,把红点挂到栏目按钮右上角。
174
+ var DOT_SEEN_KEY = "dsh-remote-seen-dot";
175
+ function dotSeen() {
176
+ try { return localStorage.getItem(DOT_SEEN_KEY) === "1"; } catch (e) { return true; }
177
+ }
178
+ function dotMarkSeen() {
179
+ try { localStorage.setItem(DOT_SEEN_KEY, "1"); } catch (e) {}
180
+ try {
181
+ var dot = document.querySelector(".dru-reddot");
182
+ if (dot && dot.parentNode) dot.parentNode.removeChild(dot);
183
+ } catch (e) {}
184
+ if (window.__dshRemoteDotObs) {
185
+ try { window.__dshRemoteDotObs.disconnect(); } catch (e) {}
186
+ window.__dshRemoteDotObs = null;
187
+ }
188
+ window.__dshRemoteDotWatch = false;
189
+ }
190
+ /** 给单个设置页导航栏目按钮挂红点(幂等)。 */
191
+ function dotInject(cell) {
192
+ try {
193
+ if (cell.querySelector(".dru-reddot")) return;
194
+ cell.style.position = "relative";
195
+ var dot = document.createElement("span");
196
+ dot.className = "dru-reddot";
197
+ dot.setAttribute("aria-hidden", "true");
198
+ cell.appendChild(dot);
199
+ cell.addEventListener("click", function onDotCellClick() {
200
+ dotMarkSeen();
201
+ cell.removeEventListener("click", onDotCellClick);
202
+ });
203
+ } catch (e) {}
204
+ }
205
+ /** 扫描设置页导航,找到「远程访问」栏目按钮后注入红点。 */
206
+ function dotScan() {
207
+ if (dotSeen()) return;
208
+ var cells = document.querySelectorAll("button");
209
+ for (var i = 0; i < cells.length; i++) {
210
+ var c = cells[i];
211
+ // 语义化匹配:设置页导航按钮(navCell)+ 栏目名「远程访问」。
212
+ // class 为 shell 的 hashed 类名,取子串匹配避免依赖具体 hash;
213
+ // 若未来 hash 变化导致匹配失败,仅红点不显示,栏目本身不受影响。
214
+ if (String(c.className || "").indexOf("navCell") === -1) continue;
215
+ if ((c.textContent || "").indexOf("远程访问") === -1) continue;
216
+ dotInject(c);
217
+ }
218
+ }
219
+ /** 开始监听设置页打开(首次安装期间有效;点击后自动停止)。 */
220
+ function dotWatch() {
221
+ if (dotSeen()) return;
222
+ if (window.__dshRemoteDotWatch) return;
223
+ window.__dshRemoteDotWatch = true;
224
+ try {
225
+ var obs = new MutationObserver(function () { dotScan(); });
226
+ obs.observe(document.body, { childList: true, subtree: true });
227
+ window.__dshRemoteDotObs = obs;
228
+ } catch (e) {}
229
+ dotScan();
230
+ }
231
+
232
+ // ── 侧栏「远程访问」快捷按钮(2026-09 恢复注入:与官方「设置」按钮共存、不遮挡) ──
233
+ // 用户要求:左侧官方「设置」按钮旁保留「远程访问」入口。实现为独立 <button id=dru-nav-remote>
234
+ // (绝不 clone 官方按钮 → 不继承官方事件委托/点击热区),插在官方「设置」按钮之前;
235
+ // 点击 → 打开官方设置页「远程访问」栏目。全程不点工作区会话内容:
236
+ // - 候选一律限定为官方导航(class 含 navCell / 位于导航容器内)+ 短文本/aria 精确匹配;
237
+ // - 命中排除自身与“菜单/更多”语义节点;
238
+ // - 栏目未开时先点官方「设置」,再轮询(≤3s)补点「远程访问」栏目项。
239
+ var NAV_ENTRY_ID = "dru-nav-remote";
240
+ var NAV_TEXT_MAX = 10;
241
+ var NAV_POLL_MAX = 20;
242
+ var NAV_BAD_RE = /(^|[^a-z0-9])(menu|context|more|kebab|dropdown|popover|toolbar)($|[^a-z0-9])/i;
243
+ function navCleanText(el) { try { return (el.textContent || "").replace(/\s+/g, " ").trim(); } catch (e) { return ""; } }
244
+ function navAttrOf(el) {
245
+ try { return String(el.getAttribute("aria-label") || el.getAttribute("title") || "").trim(); } catch (e) { return ""; }
246
+ }
247
+ function navCellLike(el) {
248
+ try {
249
+ var cls = String(el.className || "");
250
+ if (cls.indexOf("navCell") !== -1) return true;
251
+ return !!el.closest('[role="navigation"], nav, [class*="sidebar" i], [class*="sidenav" i], [class*="appnav" i], [class*="navRail" i]');
252
+ } catch (e) { return false; }
253
+ }
254
+ function pickNavByToken(token) {
255
+ try {
256
+ var els = document.querySelectorAll("button, [role='tab'], [role='menuitem'], [role='button'], a, [aria-label]");
257
+ for (var i = 0; i < els.length; i++) {
258
+ var el = els[i];
259
+ if (!el || el.id === NAV_ENTRY_ID) continue; // 排除注入按钮自身(防递归)
260
+ if (!navCellLike(el)) continue;
261
+ if (NAV_BAD_RE.test(String(el.className || ""))) continue;
262
+ var t = navCleanText(el);
263
+ if (t && !/\r|\n/.test(t) && t.length <= NAV_TEXT_MAX &&
264
+ (t === token || t.indexOf(token) === 0 || t.indexOf(token) !== -1)) return el;
265
+ var at = navAttrOf(el);
266
+ if (at && (at === token || at.indexOf(token) === 0)) return el;
267
+ }
268
+ } catch (e) { /* 忽略 */ }
269
+ return null;
270
+ }
271
+ /** 打开「远程访问」设置栏目:栏目已渲染直接点;否则先点官方「设置」再 ≤3s 轮询补点。 */
272
+ function openRemoteSettings() {
273
+ try {
274
+ var cell = pickNavByToken("远程访问");
275
+ if (cell) { cell.click(); return; }
276
+ var st = pickNavByToken("设置");
277
+ if (!st) return;
278
+ st.click();
279
+ var tries = 0;
280
+ var iv = setInterval(function () {
281
+ tries++;
282
+ var c2 = pickNavByToken("远程访问");
283
+ if (c2) { clearInterval(iv); c2.click(); }
284
+ else if (tries >= NAV_POLL_MAX) clearInterval(iv);
285
+ }, 150);
286
+ } catch (e) { /* 忽略 */ }
287
+ }
288
+ /** 注入侧栏按钮到官方「设置」按钮之前(独立 button;首次未见过引导红点)。 */
289
+ function mountNavEntry(host) {
290
+ try {
291
+ if (document.getElementById(NAV_ENTRY_ID)) return;
292
+ var wrap = host && host.parentNode;
293
+ if (!wrap || !wrap.insertBefore || !wrap.contains) return;
294
+ var btn = document.createElement("button");
295
+ btn.type = "button";
296
+ btn.id = NAV_ENTRY_ID;
297
+ btn.className = "dru-nav-remote";
298
+ btn.setAttribute("data-dru-remote", "1");
299
+ btn.title = "打开「远程访问」设置(快捷入口)";
300
+ btn.style.position = "relative";
301
+ btn.textContent = "📱 远程访问";
302
+ btn.addEventListener("click", function (e) {
303
+ try { e.stopPropagation(); e.preventDefault(); } catch (err) {}
304
+ dotMarkSeen(); // 用过快捷入口=已了解入口位置 → 关闭首次红点(含官方栏目红点)
305
+ openRemoteSettings();
306
+ });
307
+ wrap.insertBefore(btn, host);
308
+ if (!dotSeen()) {
309
+ var d = document.createElement("span");
310
+ d.className = "dru-nav-remote-dot";
311
+ btn.appendChild(d);
312
+ }
313
+ } catch (e) { /* 忽略 */ }
314
+ }
315
+ /** 常驻确保:官方「设置」按钮出现后在它前面挂一次入口(去重;2s 心跳,成本极低)。 */
316
+ function navEnsureStart() {
317
+ if (window.__dshRemoteNavStarted) return;
318
+ window.__dshRemoteNavStarted = true;
319
+ try { window.__dshRemoteNav = { open: openRemoteSettings }; } catch (e) {}
320
+ var iv = setInterval(function () {
321
+ try {
322
+ if (document.getElementById(NAV_ENTRY_ID)) return; // 已注入
323
+ var host = pickNavByToken("设置");
324
+ if (host) mountNavEntry(host);
325
+ } catch (e) { /* 忽略 */ }
326
+ }, 2000);
327
+ try { if (iv && typeof iv.unref === "function") iv.unref(); } catch (e) {} // node 测试环境不阻塞退出
328
+ }
329
+
330
+ // ── 宿主 API ────────────────────────────────────────────────────────────
331
+ function api(path, options) {
332
+ return fetch(path, options).then(function (res) {
333
+ return res.text().then(function (text) {
334
+ var body = null;
335
+ try { body = JSON.parse(text); } catch (e) { body = null; }
336
+ if (!res.ok) {
337
+ var err = (body && (body.error || (body.body && body.body.error && (body.body.error.message || body.body.error)))) || ("HTTP " + res.status);
338
+ var e = new Error(typeof err === "string" ? err : JSON.stringify(err));
339
+ e.status = res.status; e.body = body;
340
+ throw e;
341
+ }
342
+ return body;
343
+ });
344
+ });
345
+ }
346
+ var post = function (path, data) {
347
+ return api(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(data || {}) });
348
+ };
349
+
350
+ /** 一次性访问 url → 升级/续费页 url(带同一 auth,进入后即登录态到 /app/promo)。 */
351
+ function promoUrlOf(u) {
352
+ var s = String(u || "");
353
+ if (/\/app\/\?auth=/i.test(s)) return s.replace(/\/app\/\?auth=/i, "/app/promo?auth=");
354
+ try {
355
+ var m = /auth=([^&#]+)/.exec(s);
356
+ var origin = s.split("/app")[0];
357
+ if (m && origin) return origin + "/app/promo?auth=" + m[1];
358
+ } catch (e) { /* fallthrough */ }
359
+ return s;
360
+ }
361
+
362
+ // ── 用户反馈模块:本地状态(thread 令牌 / 弹窗节流) ───────────────────
363
+ // 「我的反馈」双数据源:本地 dsh-feedback-threads(thread_token 凭据,匿名/本机未同步行)
364
+ // + 服务端 /api/feedback/mine(登录态账号历史,见 FeedbackCard loadMine/fbRows,合并去重展示)。
365
+ var FB_THREADS_KEY = "dsh-feedback-threads";
366
+ var FB_POPUP_KEY = "dsh-feedback-popup";
367
+ var FB_POPUP_DELAY_MS = 60 * 60 * 1000; // 安装/体验后至少 1 小时才弹窗(只弹一次)
368
+ // 评价去重(2026-09):弹窗状态按「账号(掩码手机号)或设备」分作用域 —— 换账号/换机不重复打扰;
369
+ // 服务端另有 user_id/device_id 409 already_rated 兜底(同一账号/设备只收集一次 rating)。
370
+ var fbPopScope = "anon";
371
+ function setFbPopScope(scope) { fbPopScope = String(scope || "anon"); }
372
+ var FB_CATEGORIES = [
373
+ { value: "feature", label: "功能类" },
374
+ { value: "bug", label: "Bug 类" },
375
+ { value: "requirement", label: "需求类" },
376
+ { value: "other", label: "其他" },
377
+ ];
378
+ var FB_STATUS_LABEL = { open: "待处理", processing: "处理中", done: "已解决" };
379
+
380
+ function fbLoadThreads() {
381
+ try { return JSON.parse(localStorage.getItem(FB_THREADS_KEY) || "[]"); } catch (e) { return []; }
382
+ }
383
+ function fbSaveThreads(list) {
384
+ try { localStorage.setItem(FB_THREADS_KEY, JSON.stringify(list.slice(-20))); } catch (e) {}
385
+ }
386
+ function fbRememberThread(id, token) {
387
+ var list = fbLoadThreads().filter(function (t) { return t.id !== id; });
388
+ list.push({ id: id, token: token, at: Date.now() });
389
+ fbSaveThreads(list);
390
+ }
391
+ /** 清除本地保存的用户反馈线程凭据(thread_token 只存于此,见 FB_THREADS_KEY)。
392
+ * 退出登录 / 切换连接账号时调用:反馈历史保留在服务端(按账号校验),
393
+ * 账号身份变化后本机不再持有旧线程的访问凭据。 */
394
+ function fbClearThreads() {
395
+ try { localStorage.removeItem(FB_THREADS_KEY); } catch (e) {}
396
+ }
397
+ function fbPopState() {
398
+ try { return JSON.parse(localStorage.getItem(FB_POPUP_KEY + ":" + fbPopScope) || "null"); } catch (e) { return null; }
399
+ }
400
+ function fbSavePopState(s) {
401
+ try { localStorage.setItem(FB_POPUP_KEY + ":" + fbPopScope, JSON.stringify(s)); } catch (e) {}
402
+ }
403
+ /** 作用域清理:旧版无作用域键也一并视为已完成,避免老用户再次被打扰。 */
404
+ function fbClearPopScope(scopes) {
405
+ try {
406
+ localStorage.removeItem(FB_POPUP_KEY); // 兼容旧键(无后缀)
407
+ if (Array.isArray(scopes)) scopes.forEach(function (sc) { localStorage.removeItem(FB_POPUP_KEY + ":" + sc); });
408
+ } catch (e) {}
409
+ }
410
+
411
+ /** 同源反馈代理调用(浏览器 → /dsh-remote/feedback/* → 独立反馈服务)。 */
412
+ function fbApi(path, options) {
413
+ var opts = Object.assign({}, options || {});
414
+ var headers = Object.assign({}, opts.headers || {});
415
+ if (opts.token) headers.authorization = "Bearer " + opts.token;
416
+ if (opts.body !== undefined) headers["content-type"] = "application/json";
417
+ return api("/dsh-remote/feedback/api" + path, Object.assign({}, opts, { headers: headers }));
418
+ }
419
+
420
+ function fbStatusBadge(status) {
421
+ return h("span", { className: "dru-fb-badge dru-fb-badge-" + (status || "open") },
422
+ FB_STATUS_LABEL[status] || status || "待处理");
423
+ }
424
+
425
+ /** 面板内的“用户反馈”卡片(提交 + 我的反馈 双 tab)。
426
+ * 匿名(自建/未登录)不要求任何验证码:仅填写内容即可提交,
427
+ * 服务端只记录 IP / 浏览器标识,防刷由服务端限流兜底。 */
428
+ function FeedbackCard(props) {
429
+ var cfgArr = useState(null); var cfg = cfgArr[0]; var setCfg = cfgArr[1];
430
+ var fbAuthArr = useState("anonymous"); var fbAuth = fbAuthArr[0]; var setFbAuth = fbAuthArr[1];
431
+ var tabArr = useState("submit"); var tab = tabArr[0]; var setTab = tabArr[1];
432
+ var catArr = useState("bug"); var cat = catArr[0]; var setCat = catArr[1];
433
+ var titleArr = useState(""); var fbTitle = titleArr[0]; var setFbTitle = titleArr[1];
434
+ var contentArr = useState(""); var fbContent = contentArr[0]; var setFbContent = contentArr[1];
435
+ var contactArr = useState(""); var contact = contactArr[0]; var setContact = contactArr[1];
436
+ var threadsArr = useState(fbLoadThreads()); var threads = threadsArr[0]; var setThreads = threadsArr[1];
437
+ var openIdArr = useState(""); var openId = openIdArr[0]; var setOpenId = openIdArr[1];
438
+ var threadDataArr = useState(null); var threadData = threadDataArr[0]; var setThreadData = threadDataArr[1];
439
+ var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
440
+ var msgArr = useState(null); var fbMsg = msgArr[0]; var setFbMsg = msgArr[1];
441
+ // 账号历史(服务端 /feedback/mine 聚合列表):null=未加载;登录态(SaaS 账号已配)打开「我的反馈」时拉取
442
+ var mineListArr = useState(null); var mineList = mineListArr[0]; var setMineList = mineListArr[1];
443
+ var mineErrArr = useState(false); var mineErr = mineErrArr[0]; var setMineErr = mineErrArr[1];
444
+ var mineBusyArr = useState(false); var mineBusy = mineBusyArr[0]; var setMineBusy = mineBusyArr[1];
445
+
446
+ function setMsg(kind, text) { setFbMsg({ kind: kind, text: text }); }
447
+
448
+ // 登录态(SaaS 且账号已配)→ 打开「我的反馈」拉账号历史(/mine 需要 JWT,节点半在登录态自动附加);
449
+ // 自建/未登录(cfg.phone 为空)→ 仍走本地 thread_token,不发 /mine。
450
+ var fbAccount = !!(cfg && cfg.phone && fbAuth === "account");
451
+
452
+ var loadCfg = useCallback(function () {
453
+ api("/dsh-remote/feedback-config").then(function (b) {
454
+ setCfg(b);
455
+ // 登录态(节点半自动附加 JWT)→ 提交自动带上账号身份;
456
+ // 匿名(自建/未登录)→ 不要求任何验证码,服务端只记录 IP / 浏览器标识。
457
+ setFbAuth(b.auth === "account" ? "account" : "anonymous");
458
+ if (!b.reachable) setMsg("warn", "反馈服务未连接,请检查网络或 feedback_url 配置");
459
+ }).catch(function (e) { setCfg({ reachable: false }); setMsg("warn", "反馈服务未连接:" + e.message); });
460
+ }, []);
461
+
462
+ /** 服务端账号历史行(mine 列表含该 id)→ 单条打开/回复走服务端(节点半自动附 JWT),无需本地 thread_token。 */
463
+ function fbRowIsAccount(id) {
464
+ return !!(mineList && mineList.some(function (x) { return x.id === id; }));
465
+ }
466
+
467
+ function loadThread(id) {
468
+ var t = threads.filter(function (x) { return x.id === id; })[0];
469
+ // 账号历史行:不带 thread_token(登录态由节点半自动附账号 JWT,服务端按账号归属放行);
470
+ // 匿名/纯本地行:仍按原 thread_token 逻辑(带本地凭据访问)。
471
+ var accountRow = fbRowIsAccount(id);
472
+ if (!accountRow && !t) return;
473
+ setBusy("thread:" + id);
474
+ fbApi("/feedback/" + id, accountRow ? {} : { token: t.token }).then(function (b) {
475
+ setThreadData(b.feedback);
476
+ }).catch(function (e) {
477
+ if (e && e.status === 401) setMsg("err", "登录已过期,请退出后重新登录后再查看该反馈");
478
+ else setMsg("err", "加载反馈详情失败:" + (e && e.message));
479
+ }).finally(function () { setBusy(""); });
480
+ }
481
+
482
+ /** 登录态拉取账号历史(/feedback/mine,翻页参数可用;节点半在登录态自动附 JWT)。 */
483
+ function loadMine() {
484
+ if (!fbAccount) return;
485
+ setMineBusy(true); setMineErr(false);
486
+ fbApi("/feedback/mine?page=1&page_size=20").then(function (b) {
487
+ setMineList((b && b.items) || []);
488
+ }).catch(function (e) {
489
+ setMineErr(true);
490
+ var eb = e && e.body && e.body.error;
491
+ setMsg("err", e && e.status === 401
492
+ ? "账号登录已过期,请退出后重新登录,再查看账号全部历史"
493
+ : "账号历史拉取失败:" + ((eb && eb.message) || (e && e.message)) + "(本机记录仍可查看)");
494
+ }).finally(function () { setMineBusy(false); });
495
+ }
496
+
497
+ /** 合并展示列表:账号历史(服务端 mine,新→旧)在前;本机 thread_token 行去重后保留
498
+ * (匿名/本机新增但服务端未聚合的行仍可见,打开继续走本地凭据)。 */
499
+ function fbRows() {
500
+ var out = [];
501
+ var seen = {};
502
+ (mineList || []).forEach(function (it) {
503
+ seen[it.id] = true;
504
+ out.push({
505
+ id: it.id, acct: true,
506
+ status: it.status || "", category: it.category || "", title: it.title || "",
507
+ created_at: it.created_at || 0, reply_count: it.reply_count || 0,
508
+ });
509
+ });
510
+ threads.slice().reverse().forEach(function (t) {
511
+ if (seen[t.id]) return;
512
+ seen[t.id] = true;
513
+ out.push({ id: t.id, acct: false, status: "", category: "", title: "", created_at: 0, reply_count: 0 });
514
+ });
515
+ return out;
516
+ }
517
+
518
+ useEffect(function () { loadCfg(); }, [loadCfg]);
519
+ // 登录态打开「我的反馈」→ 拉账号历史;仅在未加载且非错误态自动触发(失败后点 tab 重试,不无限重试)
520
+ useEffect(function () {
521
+ if (tab === "mine" && fbAccount && mineList === null && !mineErr && !mineBusy) loadMine();
522
+ // eslint-disable-next-line react-hooks/exhaustive-deps
523
+ }, [tab, fbAccount, mineList === null, mineErr, mineBusy]);
524
+
525
+ var doSubmit = function () {
526
+ if (!fbContent.trim()) { setMsg("err", "请填写反馈内容"); return; }
527
+ if (fbContent.length > 2000) { setMsg("err", "内容不能超过 2000 字"); return; }
528
+ setBusy("submit");
529
+ var payload = {
530
+ kind: "feedback",
531
+ category: cat,
532
+ title: fbTitle.trim(),
533
+ content: fbContent.trim(),
534
+ contact: contact.trim() || ((cfg && cfg.phone && cfg.phone.indexOf("*") === -1) ? cfg.phone : ""),
535
+ };
536
+ fbApi("/feedback", { method: "POST", body: JSON.stringify(payload) })
537
+ .then(function (b) {
538
+ fbRememberThread(b.feedback.id, b.thread_token);
539
+ setThreads(fbLoadThreads());
540
+ if (fbAccount) setMineList(null); // 账号态提交成功 → 回「我的反馈」时重新拉取(含本条)
541
+ setFbTitle(""); setFbContent(""); setContact("");
542
+ setMsg("ok", "✅ 反馈已提交,可在「我的反馈」查看回复");
543
+ setTab("mine");
544
+ })
545
+ .catch(function (e) {
546
+ var errBody = e.body && e.body.error;
547
+ if (errBody && errBody.code === "rate_limited") setMsg("err", "今天提交次数已达上限,请明天再试");
548
+ else setMsg("err", "提交失败:" + (errBody && errBody.message ? errBody.message : e.message));
549
+ })
550
+ .finally(function () { setBusy(""); });
551
+ };
552
+
553
+ var doReply = function (id) {
554
+ var input = document.getElementById("dru-fb-reply-" + id);
555
+ var text = input ? input.value.trim() : "";
556
+ if (!text) { setMsg("err", "请填写回复内容"); return; }
557
+ var t = threads.filter(function (x) { return x.id === id; })[0];
558
+ var accountRow = fbRowIsAccount(id);
559
+ // 账号历史行:服务端回复(节点半自动附 JWT,无需本地 thread_token);本地/匿名行:带 thread_token 回复
560
+ var replyOpts = { method: "POST", body: JSON.stringify({ content: text }) };
561
+ if (!accountRow) {
562
+ if (!t) return;
563
+ replyOpts.token = t.token;
564
+ }
565
+ setBusy("reply:" + id);
566
+ fbApi("/feedback/" + id + "/replies", replyOpts)
567
+ .then(function () {
568
+ if (input) input.value = "";
569
+ loadThread(id);
570
+ setMsg("ok", "✅ 已回复");
571
+ })
572
+ .catch(function (e) {
573
+ var errBody = e.body && e.body.error;
574
+ if (e && e.status === 401) setMsg("err", "登录已过期,请重新登录后再回复");
575
+ else if (errBody && errBody.code === "rate_limited") setMsg("err", "今天回复次数已达上限,请明天再试");
576
+ else setMsg("err", "回复失败:" + (errBody && errBody.message ? errBody.message : e.message));
577
+ })
578
+ .finally(function () { setBusy(""); });
579
+ };
580
+
581
+ var openThread = function (id) {
582
+ setOpenId(id === openId ? "" : id);
583
+ setThreadData(null);
584
+ if (id !== openId) loadThread(id);
585
+ };
586
+
587
+ /** 「我的反馈」tab 内容:账号历史行直接用服务端字段渲染(状态/类别/标题/时间/回复数),
588
+ * 并保留本机未同步的 thread_token 行;单条展开/回复由 loadThread/doReply 分流。 */
589
+ function renderMine() {
590
+ var rows = fbRows();
591
+ if (rows.length === 0) {
592
+ var emptyText = fbAccount
593
+ ? (mineBusy ? "正在加载账号历史…" : (mineErr ? "账号历史加载失败,点上方「我的反馈」tab 重试" : "暂无反馈,提交第一条?"))
594
+ : "还没有提交过反馈。" + (cfg && cfg.reachable === false ? "(反馈服务未连接)" : "登录手机号账号后,可在任意设备查看账号全部历史。");
595
+ return h("div", null,
596
+ h("div", { className: "dru-fb-empty" }, emptyText),
597
+ fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
598
+ );
599
+ }
600
+ return h("div", null,
601
+ rows.map(function (r) {
602
+ var open = openId === r.id;
603
+ var td = open ? threadData : null;
604
+ var st = td ? td.status : r.status;
605
+ var cat = td ? td.category : r.category;
606
+ var titleTxt = td ? (td.title || "") : r.title;
607
+ var ts = td ? td.created_at : r.created_at;
608
+ var catLbl = (function () {
609
+ var c = FB_CATEGORIES.filter(function (x) { return x.value === cat; })[0];
610
+ return c ? c.label : (cat || "反馈");
611
+ })();
612
+ return h("div", { key: r.id, className: "dru-fb-item" },
613
+ h("div", { className: "dru-fb-item-head" },
614
+ fbStatusBadge(st || "open"),
615
+ h("span", { className: "dru-fb-cat" }, catLbl),
616
+ h("span", { className: "dru-fb-item-title" }, titleTxt || ("反馈 #" + r.id.slice(-6))),
617
+ h("span", { className: "dru-fb-item-time" },
618
+ (ts ? new Date(ts).toLocaleString() : "") +
619
+ (r.acct && !open && r.reply_count > 0 ? " · " + r.reply_count + " 条回复" : ""))
620
+ ),
621
+ open && td
622
+ ? h("div", null,
623
+ h("div", { className: "dru-fb-item-content" }, td.content),
624
+ td.replies.map(function (reply) {
625
+ return h("div", { key: reply.id, className: "dru-fb-reply" },
626
+ h("div", { className: "dru-fb-reply-row" },
627
+ h("span", { className: "dru-fb-reply-who" + (reply.author === "user" ? " user" : "") }, reply.author === "admin" ? "管理员回复" : "我"),
628
+ h("span", { className: "dru-fb-reply-text" }, reply.content)
629
+ ),
630
+ h("div", { className: "dru-fb-item-time" }, new Date(reply.created_at).toLocaleString())
631
+ );
632
+ }),
633
+ h("div", { className: "dru-fb-reply" },
634
+ h("textarea", { id: "dru-fb-reply-" + r.id, className: "dru-fb-reply-input", placeholder: "回复管理员…", maxLength: 2000 }),
635
+ h("div", { style: { textAlign: "right", marginTop: 6 } },
636
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { doReply(r.id); } }, busy === "reply:" + r.id ? "发送中…" : "回复")
637
+ )
638
+ ),
639
+ h("div", { className: "dru-fb-item-time", style: { textAlign: "right", marginTop: 8 } },
640
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { loadThread(r.id); } }, "↻ 刷新"),
641
+ " ",
642
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, onClick: function () { setOpenId(""); setThreadData(null); } }, "收起")
643
+ )
644
+ )
645
+ : h("div", { className: "dru-fb-item-time", style: { textAlign: "right" } },
646
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { openThread(r.id); } }, open ? "收起" : "查看 / 回复"))
647
+ );
648
+ }),
649
+ mineBusy ? h("div", { className: "dru-hint", style: { marginTop: 6 } }, "账号历史同步中…") : null,
650
+ fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
651
+ );
652
+ }
653
+
654
+ var mineRows = fbRows(); // 「我的反馈」展示行数:账号历史 + 本机未同步线程(合并去重后)
655
+ return h("div", { className: "dru-card" },
656
+ h("h3", null, "💬 用户反馈"),
657
+ h("div", { className: "dru-fb-tabs" },
658
+ h("div", { className: "dru-fb-tab" + (tab === "submit" ? " active" : ""), onClick: function () { setTab("submit"); setFbMsg(null); } }, "提交反馈"),
659
+ h("div", { className: "dru-fb-tab" + (tab === "mine" ? " active" : ""), onClick: function () {
660
+ setTab("mine"); setFbMsg(null); setThreads(fbLoadThreads());
661
+ // 登录态(账号已配)打开「我的反馈」→ 拉取账号历史(合并展示,换设备/重装也能看到)
662
+ if (fbAccount && mineList === null) loadMine();
663
+ } }, "我的反馈" + (mineRows.length ? "(" + mineRows.length + ")" : ""))
664
+ ),
665
+ tab === "submit"
666
+ ? h("div", null,
667
+ h("div", { className: "dru-field" },
668
+ h("label", null, "类别"),
669
+ h("select", { className: "dru-fb-select", value: cat, onChange: function (e) { setCat(e.target.value); } },
670
+ FB_CATEGORIES.map(function (c) { return h("option", { key: c.value, value: c.value }, c.label); }))
671
+ ),
672
+ h("div", { className: "dru-field" },
673
+ h("label", null, "标题(可选)"),
674
+ h("input", { className: "dru-input", type: "text", value: fbTitle, maxLength: 120, placeholder: "一句话描述", onChange: function (e) { setFbTitle(e.target.value); } })
675
+ ),
676
+ h("div", { className: "dru-field" },
677
+ h("label", null, "内容"),
678
+ h("textarea", { className: "dru-fb-textarea", value: fbContent, maxLength: 2000, placeholder: "请描述遇到的问题或功能想法…", onChange: function (e) { setFbContent(e.target.value); } })
679
+ ),
680
+ h("div", { className: "dru-field" },
681
+ h("label", null, "联系方式(可选)"),
682
+ h("input", { className: "dru-input", type: "text", value: contact, maxLength: 120, placeholder: cfg && cfg.phone ? "已登录手机号:" + cfg.phone : "邮箱/手机号,便于我们跟进", onChange: function (e) { setContact(e.target.value); } })
683
+ ),
684
+ h("div", { className: "dru-hint", style: { marginBottom: 10 } },
685
+ fbAuth === "account"
686
+ ? "已登录账号,提交反馈会带上你的身份信息。"
687
+ : "匿名反馈:无需输入任何验证码,上报仅记录 IP 与浏览器标识。"),
688
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doSubmit }, busy === "submit" ? "提交中…" : "提交反馈"),
689
+ fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
690
+ )
691
+ : renderMine()
692
+ );
693
+ }
694
+
695
+ // ── 满意度弹窗(安装体验约 10 分钟后弹出) ──────────────────────────────
696
+
697
+ /** 模块级弹窗开关(与面板独立)。 */
698
+ var popupOpen = false;
699
+ var popupListeners = new Set();
700
+ function setPopupOpen(v) { popupOpen = !!v; popupListeners.forEach(function (l) { l(); }); }
701
+ function subscribePopup(cb) { popupListeners.add(cb); return function () { popupListeners.delete(cb); }; }
702
+ function usePopupOpen() { return useSyncExternalStore(subscribePopup, function () { return popupOpen; }); }
703
+
704
+ /** 首次安装时间戳(首次观察到 bridge 运行时记下,便于“先用满 1 小时再评价”)。 */
705
+ var fbFirstSeenAt = null;
706
+ function fbEnsureFirstSeen() {
707
+ var s = fbPopState();
708
+ if (s && s.state === "pending" && s.firstSeen) { fbFirstSeenAt = s.firstSeen; return; }
709
+ if (fbFirstSeenAt) return;
710
+ fbFirstSeenAt = Date.now();
711
+ fbSavePopState({ state: "pending", firstSeen: fbFirstSeenAt, nextAt: s && s.nextAt ? s.nextAt : 0 });
712
+ }
713
+ /** 弹窗触发检查:到点且未完成/未推迟。 */
714
+ function fbMaybeOpenPopup() {
715
+ var s = fbPopState();
716
+ if (!s || s.state !== "pending" || popupOpen) return;
717
+ if (!fbFirstSeenAt) return;
718
+ if (Date.now() - fbFirstSeenAt < FB_POPUP_DELAY_MS) return;
719
+ if (s.nextAt && Date.now() < s.nextAt) return;
720
+ setPopupOpen(true);
721
+ }
722
+ function fbPopupLater() {
723
+ // 只弹一次:关闭即视为本轮安装的评价流程结束,不再自动重复弹出
724
+ fbSavePopState({ state: "done", firstSeen: fbFirstSeenAt || Date.now(), nextAt: 0 });
725
+ setPopupOpen(false);
726
+ }
727
+ function fbPopupDone() {
728
+ fbSavePopState({ state: "done", firstSeen: fbFirstSeenAt || Date.now(), nextAt: 0 });
729
+ setPopupOpen(false);
730
+ }
731
+
732
+ function FeedbackPopup() {
733
+ var open = usePopupOpen();
734
+ var stepArr = useState("rate"); var step = stepArr[0]; var setStep = stepArr[1];
735
+ var ratingArr = useState(null); var rating = ratingArr[0]; var setRating = ratingArr[1];
736
+ var noteArr = useState(""); var note = noteArr[0]; var setNote = noteArr[1];
737
+ var recommendArr = useState(null); var recommend = recommendArr[0]; var setRecommend = recommendArr[1];
738
+ var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
739
+ var msgArr = useState(null); var message = msgArr[0]; var setMessage = msgArr[1];
740
+ var fbAuthArr = useState("anonymous"); var fbAuth = fbAuthArr[0]; var setFbAuth = fbAuthArr[1];
741
+ // 邀请链接(已登录用户推荐成功后展示)
742
+ var inviteArr = useState(null); var popupInvite = inviteArr[0]; var setPopupInvite = inviteArr[1];
743
+ var inviteCopiedArr = useState(false); var inviteCopied = inviteCopiedArr[0]; var setInviteCopied = inviteCopiedArr[1];
744
+
745
+ /** 预取邀请信息(已登录且有邀请码时生成专属链接)。 */
746
+ function loadPopupInvite() {
747
+ if (popupInvite) return;
748
+ api("/dsh-remote/account").then(function (b) {
749
+ var acct = (b && b.ok && b.account) ? b.account : null;
750
+ if (!acct || !acct.invite_code) { setPopupInvite({ code: "" }); return; }
751
+ api("/dsh-remote/remote-url").then(function (u) {
752
+ var base = (u && u.remoteUrl) || "https://n.risegao.cn:13443/app/";
753
+ setPopupInvite({ code: acct.invite_code, link: base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(acct.invite_code) });
754
+ }).catch(function () { setPopupInvite({ code: acct.invite_code }); });
755
+ }).catch(function () { setPopupInvite({ code: "" }); });
756
+ }
757
+
758
+ useEffect(function () {
759
+ if (!open) return;
760
+ // 登录态(节点半自动附加 JWT)→ 提交自动带上账号身份;
761
+ // 匿名(自建/未登录)→ 不要求任何验证码,服务端只记录 IP / 浏览器标识。
762
+ api("/dsh-remote/feedback-config").then(function (b) {
763
+ setFbAuth(b.auth === "account" ? "account" : "anonymous");
764
+ }).catch(function () { setFbAuth("anonymous"); });
765
+ // eslint-disable-next-line react-hooks/exhaustive-deps
766
+ }, [open]);
767
+
768
+ if (!open) return null;
769
+
770
+ var doSubmit = function () {
771
+ if (rating === null) { setMessage({ kind: "err", text: "请先选择满意度" }); return; }
772
+ if (step === "rate") { setStep("recommend"); return; }
773
+ setBusy("submit");
774
+ var payload = {
775
+ kind: "rating",
776
+ category: "satisfaction",
777
+ content: note.trim() || (rating >= 4 ? "对远程访问功能满意" : "对远程访问功能不太满意"),
778
+ rating: rating,
779
+ recommend: recommend === true ? 1 : 0,
780
+ };
781
+ fbApi("/feedback", { method: "POST", body: JSON.stringify(payload) })
782
+ .then(function (b) {
783
+ fbRememberThread(b.feedback.id, b.thread_token);
784
+ fbPopupDone();
785
+ })
786
+ .catch(function (e) {
787
+ var errBody = e.body && e.body.error;
788
+ if (errBody && errBody.code === "rate_limited") { fbPopupLater(); setMessage({ kind: "err", text: "今日提交已达上限,明天再来吧" }); }
789
+ else if (errBody && errBody.code === "already_rated") { fbPopupDone(); setMessage({ kind: "ok", text: "你已评价过,感谢支持(同一账号/设备只收集一次)" }); }
790
+ else setMessage({ kind: "err", text: "提交失败:" + ((errBody && errBody.message) || e.message) + "(可稍后再试)" });
791
+ })
792
+ .finally(function () { setBusy(""); });
793
+ };
794
+
795
+ return h("div", { className: "dru-popup", onMouseDown: function (e) { if (e.target === e.currentTarget) fbPopupLater(); } },
796
+ h("div", { className: "dru-popup-card", role: "dialog", "aria-label": "用户反馈" },
797
+ h("div", { className: "dru-popup-body" },
798
+ step === "rate"
799
+ ? h("div", null,
800
+ h("div", { className: "dru-popup-icon" }, "😊"),
801
+ h("div", { className: "dru-popup-title" }, "您对远程访问功能满意吗?"),
802
+ h("div", { className: "dru-popup-sub" }, "使用体验已满 1 小时,说说真实感受吧(1 分钟搞定)"),
803
+ h("div", { className: "dru-popup-rate" },
804
+ [ [5, "😄", "很满意"], [3, "😐", "一般"], [1, "😞", "不满意"] ].map(function (r) {
805
+ return h("button", { key: r[0], type: "button", className: rating === r[0] ? "sel" : "", onClick: function () { setRating(r[0]); setMessage(null); } },
806
+ h("span", null, r[1]), h("span", { className: "lbl" }, r[2]));
807
+ })
808
+ ),
809
+ h("textarea", { className: "dru-popup-textarea", value: note, maxLength: 2000, placeholder: "说说想法或遇到的问题(可选)…", onChange: function (e) { setNote(e.target.value); } }),
810
+ h("div", { className: "dru-hint", style: { marginTop: 10 } },
811
+ fbAuth === "account"
812
+ ? "已登录账号,本次评价会带上你的身份信息。"
813
+ : "匿名评价:只需填上面内容,无需任何验证码;上报会记录 IP 与浏览器标识。"),
814
+ message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text),
815
+ h("div", { className: "dru-popup-actions" },
816
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: doSubmit }, "下一步")
817
+ )
818
+ )
819
+ : h("div", null,
820
+ h("div", { className: "dru-popup-icon" }, "🤝"),
821
+ h("div", { className: "dru-popup-title" }, "愿意推荐给朋友吗?"),
822
+ h("div", { className: "dru-popup-sub" }, "邀请好友一起使用,双方都能获得更好的体验"),
823
+ recommend === true
824
+ ? h("div", { className: "dru-popup-invite" },
825
+ h("div", null, "🎉 感谢推荐!"),
826
+ popupInvite && popupInvite.code && popupInvite.link
827
+ ? h("div", null,
828
+ h("div", { className: "dru-url", style: { marginTop: 10, textAlign: "left" } },
829
+ h("span", null, popupInvite.link),
830
+ h("button", { type: "button", onClick: function () {
831
+ try { navigator.clipboard.writeText(popupInvite.link).then(function () { setInviteCopied(true); setTimeout(function () { setInviteCopied(false); }, 1500); }); } catch (e) {}
832
+ } }, inviteCopied ? "已复制" : "复制邀请链接")
833
+ ),
834
+ h("div", { style: { marginTop: 8, fontSize: 12 } }, "把链接发给好友,注册时自动带上你的邀请码。")
835
+ )
836
+ : h("div", { style: { marginTop: 6, fontSize: 12 } },
837
+ popupInvite && popupInvite.code
838
+ ? "邀请码 " + popupInvite.code + " 已生成:在设置面板 →「🎯 邀请好友赚会员」中复制邀请链接。"
839
+ : "登录手机号账号后,在设置面板 →「🎯 邀请好友赚会员」中获取专属邀请链接。")
840
+ )
841
+ : h("div", null,
842
+ h("div", { className: "dru-popup-actions" },
843
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: function () { setRecommend(true); loadPopupInvite(); } }, "愿意推荐"),
844
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: function () { setRecommend(false); } }, "暂时不了")
845
+ ),
846
+ h("div", { className: "dru-hint", style: { marginTop: 10 } }, "推荐成功可获得专属邀请链接")
847
+ ),
848
+ message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text),
849
+ recommend !== null && h("div", { className: "dru-popup-actions" },
850
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: doSubmit }, busy === "submit" ? "提交中…" : "完成")
851
+ )
852
+ )
853
+ ),
854
+ h("div", { className: "dru-popup-foot" },
855
+ h("button", { type: "button", title: "关闭本轮评价,之后不再自动弹出", onClick: fbPopupLater }, "暂不评价"),
856
+ h("button", { type: "button", onClick: fbPopupDone }, "不再提示")
857
+ )
858
+ )
859
+ );
860
+ }
861
+
862
+ // ── 图标 ────────────────────────────────────────────────────────────────
863
+ // 「远程访问」栏目图标沿用面板 emoji 图标体系:📱 更贴合“手机/另一台电脑远程访问”语义
864
+ // (原 🖥 已随入口迁移调整);栏目导航 label 与栏目头部均使用它。
865
+
866
+ // ── 版本与更新卡片(自管理:市场没有更新按钮,这里提供在线一键更新/彻底卸载) ──
867
+ // 数据来自 node 半新增的 /dsh-remote/self* 路由;逻辑均在插件 node 半实现,
868
+ // 因此无论插件从「插件市场」还是 npx 安装,界面与行为完全一致。
869
+ function SelfManageCard() {
870
+ var verArr = useState(null); var ver = verArr[0]; var setVer = verArr[1]; // {version, runtimeReady}
871
+ var chkArr = useState(null); var chk = chkArr[0]; var setChk = chkArr[1]; // {current, latest, outdated}
872
+ var chkBusyArr = useState(false); var chkBusy = chkBusyArr[0]; var setChkBusy = chkBusyArr[1];
873
+ var upBusyArr = useState(false); var upBusy = upBusyArr[0]; var setUpBusy = upBusyArr[1];
874
+ var unBusyArr = useState(false); var unBusy = unBusyArr[0]; var setUnBusy = unBusyArr[1];
875
+ var logArr = useState(""); var log = logArr[0]; var setLog = logArr[1]; // 更新日志尾部
876
+ var updArr = useState(false); var updating = updArr[0]; var setUpdating = updArr[1]; // 更新任务是否仍在跑
877
+ var doneArr = useState(false); var updated = doneArr[0]; var setUpdated = doneArr[1]; // 本轮已更新完成(提示重启)
878
+ var armArr = useState(false); var armed = armArr[0]; var setArmed = armArr[1]; // 彻底卸载二次确认
879
+ var msgArr = useState(null); var selfMsg = msgArr[0]; var setSelfMsg = msgArr[1]; // {kind, text}
880
+
881
+ var loadVer = useCallback(function () {
882
+ api("/dsh-remote/self").then(function (b) {
883
+ if (b && b.ok) setVer(b);
884
+ }).catch(function () {});
885
+ }, []);
886
+
887
+ var doCheck = useCallback(function () {
888
+ setChkBusy(true);
889
+ api("/dsh-remote/self/update-check").then(function (b) {
890
+ if (b && b.ok) setChk(b);
891
+ else setSelfMsg({ kind: "err", text: "检查更新失败:" + ((b && (b.error || b.detail)) || "未知错误") });
892
+ }).catch(function (e) {
893
+ setSelfMsg({ kind: "err", text: "无法连接更新服务:" + e.message });
894
+ }).finally(function () { setChkBusy(false); });
895
+ }, []);
896
+
897
+ // 轮询更新日志:点击一键更新后,每 2s 拉一次日志;直到 running=false 视为完成
898
+ var logTimer = useCallback(function (force) {
899
+ if (!force && updating) return;
900
+ api("/dsh-remote/self/update-log").then(function (b) {
901
+ if (b && b.ok) {
902
+ setLog(b.log || "");
903
+ if (!b.running) {
904
+ setUpdating(false);
905
+ setUpdated(true);
906
+ loadVer();
907
+ return;
908
+ }
909
+ }
910
+ setUpdating(true);
911
+ }).catch(function () { setUpdating(false); });
912
+ }, [updating, loadVer]);
913
+
914
+ useEffect(function () { loadVer(); doCheck(); }, [loadVer, doCheck]);
915
+ useEffect(function () {
916
+ if (!updating) return;
917
+ var iv = setInterval(function () {
918
+ api("/dsh-remote/self/update-log").then(function (b) {
919
+ if (!b || !b.ok) return;
920
+ setLog(b.log || "");
921
+ if (!b.running) {
922
+ clearInterval(iv);
923
+ setUpdating(false);
924
+ setUpdated(true);
925
+ loadVer();
926
+ doCheck();
927
+ }
928
+ }).catch(function () {});
929
+ }, 2000);
930
+ return function () { clearInterval(iv); };
931
+ }, [updating, loadVer, doCheck]);
932
+
933
+ var doUpdate = function () {
934
+ setUpBusy(true);
935
+ setUpdated(false);
936
+ setSelfMsg(null);
937
+ post("/dsh-remote/self/update", {}).then(function (b) {
938
+ if (b && b.ok) {
939
+ setSelfMsg({ kind: "ok", text: "更新已在后台开始,正在下载安装…(本页会实时显示进度日志)" });
940
+ setUpdating(true);
941
+ logTimer(true);
942
+ } else {
943
+ var detail = String((b && (b.detail || b.error)) || "更新启动失败");
944
+ // 另一种常见情况:node 半返回 ok:false + “已有更新在进行中” → 转为跟踪进度而非报错
945
+ api("/dsh-remote/self/update-log").then(function (lb) {
946
+ if (lb && lb.ok && lb.running) {
947
+ setSelfMsg({ kind: "ok", text: "检测到已有一次更新正在进行,正在跟踪进度…" });
948
+ setUpdating(true);
949
+ logTimer(true);
950
+ } else {
951
+ setSelfMsg({ kind: "err", text: detail });
952
+ }
953
+ }).catch(function () { setSelfMsg({ kind: "err", text: detail }); });
954
+ }
955
+ }).catch(function (e) {
956
+ setSelfMsg({ kind: "err", text: "更新失败:" + e.message });
957
+ }).finally(function () { setUpBusy(false); });
958
+ };
959
+
960
+ var doUninstall = function () {
961
+ if (!armed) { setArmed(true); return; }
962
+ setUnBusy(true);
963
+ setSelfMsg(null);
964
+ post("/dsh-remote/self/uninstall", {}).then(function (b) {
965
+ if (b && b.ok) {
966
+ setArmed(false);
967
+ // 优先展示服务端 detail(含 bridge 自启动/配置目录的逐项清理结果与重启提示);
968
+ // 兜底文案同样说明 bridge 自启动服务与本地配置目录会一并移除/清空
969
+ var detail = b && b.detail ? String(b.detail) : "";
970
+ setSelfMsg({ kind: "ok", text: detail || "已彻底卸载:插件引用、bridge 自启动服务与本地配置目录(账号/密钥/运行时等)已一并移除并清空。请重启 dsh web 后完全生效(本栏目将消失);如需再次使用,在插件市场重新安装即可。" });
971
+ } else {
972
+ setArmed(false);
973
+ setSelfMsg({ kind: "err", text: "卸载失败:" + ((b && (b.error || b.detail)) || "未知错误") });
974
+ }
975
+ }).catch(function (e) {
976
+ setArmed(false);
977
+ setSelfMsg({ kind: "err", text: "卸载失败:" + e.message });
978
+ }).finally(function () { setUnBusy(false); });
979
+ };
980
+
981
+ var outdated = !!(chk && chk.outdated && chk.latest && chk.latest !== chk.current);
982
+ var currentV = (ver && ver.version) || (chk && chk.current) || "…";
983
+ var runtimeReady = ver ? !!ver.runtimeReady : null;
984
+
985
+ return h("div", { className: "dru-card", style: { marginTop: 2 } },
986
+ h("h3", null, "🔄 版本与更新"),
987
+ h("div", { className: "dru-status-line" },
988
+ h("span", null, "插件版本 v" + currentV),
989
+ chk === null && chkBusy ? h("span", { className: "dru-meta", style: { margin: 0 } }, "(检查新版本中…)") : null,
990
+ chk && outdated
991
+ ? h("span", { className: "dru-ver-badge dru-ver-badge-new" }, "发现新版本 v" + chk.latest)
992
+ : chk && !outdated ? h("span", { className: "dru-ver-badge dru-ver-badge-ok" }, "已是最新版本") : null
993
+ ),
994
+ h("div", { className: "dru-meta" },
995
+ runtimeReady === false ? "⚠ 桌面运行环境缺失(点击下方「一键更新」会自动补全并启动)" : runtimeReady === true ? "桌面运行环境正常" : "读取运行环境中…"
996
+ ),
997
+ h("div", { className: "dru-actions", style: { marginTop: 10 } },
998
+ outdated
999
+ ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
1000
+ upBusy ? "更新启动中…" : updating ? "正在更新…" : "一键更新到 v" + chk.latest)
1001
+ : h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
1002
+ updating ? "正在更新…" : (ver && !ver.runtimeReady) ? "安装并启动(一键修复)" : "重新检查 / 修复"),
1003
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: chkBusy || updating || upBusy, onClick: doCheck }, chkBusy ? "检查中…" : "检查更新"),
1004
+ h("button", {
1005
+ type: "button",
1006
+ className: "dru-btn dru-btn-danger",
1007
+ style: { marginLeft: "auto" },
1008
+ disabled: unBusy || updating || upBusy,
1009
+ onClick: doUninstall
1010
+ }, unBusy ? "卸载中…" : armed ? "⚠ 再点一次确认彻底卸载" : "彻底卸载")
1011
+ ),
1012
+ updated
1013
+ ? h("div", { className: "dru-msg dru-msg-ok" },
1014
+ "✅ 更新已完成,最新代码已就位。请", h("strong", null, "重启 dsh web"), "后生效;桌面 bridge 会随系统自启自动运行新版本。")
1015
+ : null,
1016
+ log ? h("div", { className: "dru-up-log", title: "更新日志(尾部)" }, log) : null,
1017
+ selfMsg ? h("div", { className: "dru-msg dru-msg-" + selfMsg.kind }, selfMsg.text) : null,
1018
+ h("div", { className: "dru-hint", style: { marginTop: 8 } },
1019
+ armed ? "⚠ 再次点击后即开始彻底卸载:① 移除 dsh web 配置中的插件引用与本地文件;② 停止并移除 bridge 自启动服务(macOS com.dshremote.bridge / Linux dsh-bridge)并结束残留进程;③ 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销,如需再次使用请在插件市场重新安装。" :
1020
+ "检测新版、一键在线更新、彻底卸载都在本卡片完成。")
1021
+ );
1022
+ }
1023
+
1024
+ // ── 端到端加密(E2EE,Phase-5):bridge 状态 → 可读文案映射 ─────────────
1025
+ // bridge 启停时把开关结果写入 <relayDir>/.e2ee-state.json({enabled,reason,profile,epoch,caps},
1026
+ // 见 clients/dsh-remote/e2ee-client.mjs),node 半随 /dsh-remote/status 以 service.e2ee 下发;
1027
+ // 此处只做“可读文案”映射(协议 docs/e2ee-protocol.md §2.3/§7.3)。
1028
+ var E2EE_DISABLED_COPY = {
1029
+ server_disabled: "端到端加密暂不可用(当前为普通安全连接 HTTPS)",
1030
+ params_unreachable: "当前为普通安全连接(HTTPS)",
1031
+ disabled_by_config: "当前为普通安全连接(HTTPS)",
1032
+ derive_failed: "账号密码已变更,需在「🔑 账号」重新登录后恢复端到端加密(当前为普通安全连接(HTTPS))",
1033
+ };
1034
+ var E2EE_DISABLED_FALLBACK = "当前为普通安全连接(HTTPS)";
1035
+ /** 把 service.e2ee 归一化为可读状态行;e2ee 缺失/旧 host 未下发 → null(不打扰)。 */
1036
+ function describeE2ee(e2ee) {
1037
+ if (!e2ee || typeof e2ee !== "object") return null;
1038
+ if (e2ee.enabled === true) return { kind: "ok", text: "🔒 端到端加密已启用(手机解锁后生效)" };
1039
+ var reason = typeof e2ee.reason === "string" && e2ee.reason ? e2ee.reason : "";
1040
+ var text = E2EE_DISABLED_COPY[reason] || E2EE_DISABLED_FALLBACK; // reason 未知 → 兜底
1041
+ return { kind: "off", text: text };
1042
+ }
1043
+
1044
+ // ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
1045
+ function RemoteControlSection(props) {
1046
+
1047
+ var statusArr = useState(null); var st = statusArr[0]; var setSt = statusArr[1];
1048
+ var modeArr = useState("saas"); var mode = modeArr[0]; var setMode = modeArr[1]; // saas | local
1049
+ var viewArr = useState("home"); var view = viewArr[0]; var setView = viewArr[1]; // home | feedback | invite
1050
+ var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
1051
+ var msgArr = useState(null); var message = msgArr[0]; var setMessage = msgArr[1];
1052
+ var copiedArr = useState(false); var copied = copiedArr[0]; var setCopied = copiedArr[1];
1053
+
1054
+ // 我的信息(个人中心)数据
1055
+ var acctArr = useState(null); var account = acctArr[0]; var setAccount = acctArr[1];
1056
+ var quotaArr = useState(null); var quota = quotaArr[0]; var setQuota = quotaArr[1];
1057
+ var inviteArr = useState(null); var inviteData = inviteArr[0]; var setInviteData = inviteArr[1];
1058
+ var pubArr = useState(null); var pub = pubArr[0]; var setPub = pubArr[1];
1059
+
1060
+ // 登录表单字段
1061
+ var phoneArr = useState(""); var phone = phoneArr[0]; var setPhone = phoneArr[1];
1062
+ var passArr = useState(""); var pass = passArr[0]; var setPass = passArr[1];
1063
+ var lcapArr = useState(null); var lcap = lcapArr[0]; var setLcap = lcapArr[1]; // {id, svg}
1064
+ var lcapTxtArr = useState(""); var lcapTxt = lcapTxtArr[0]; var setLcapTxt = lcapTxtArr[1];
1065
+ // 注册表单字段
1066
+ var rphoneArr = useState(""); var rphone = rphoneArr[0]; var setRphone = rphoneArr[1];
1067
+ var rpassArr = useState(""); var rpass = rpassArr[0]; var setRpass = rpassArr[1];
1068
+ var rpass2Arr = useState(""); var rpass2 = rpass2Arr[0]; var setRpass2 = rpass2Arr[1];
1069
+ var rsmsArr = useState(""); var rsms = rsmsArr[0]; var setRsms = rsmsArr[1];
1070
+ var rsmsBtnArr = useState("获取验证码"); var rsmsBtn = rsmsBtnArr[0]; var setRsmsBtn = rsmsBtnArr[1];
1071
+ var rcapArr = useState(null); var rcap = rcapArr[0]; var setRcap = rcapArr[1];
1072
+ var rcapTxtArr = useState(""); var rcapTxt = rcapTxtArr[0]; var setRcapTxt = rcapTxtArr[1];
1073
+ var rInviteArr = useState(""); var rInvite = rInviteArr[0]; var setRInvite = rInviteArr[1];
1074
+ // 注册 tab(登录/注册)
1075
+ var authTabArr = useState("login"); var authTab = authTabArr[0]; var setAuthTab = authTabArr[1];
1076
+
1077
+ // 自建服务表单字段
1078
+ var shArr = useState(""); var selfHost = shArr[0]; var setSelfHost = shArr[1];
1079
+ var lkArr = useState(""); var localKey = lkArr[0]; var setLocalKey = lkArr[1];
1080
+
1081
+ // ── 📱 远程访问卡(一次性访问密钥 + 已授权设备管理)——放在全部既有字段之后,保持既有 hook 序号 ──
1082
+ var akeyArr = useState(null); var akey = akeyArr[0]; var setAkey = akeyArr[1]; // {url,key,expires_at,ttl_ms,qr_data_url}
1083
+ var akeyBusyArr = useState(false); var akeyBusy = akeyBusyArr[0]; var setAkeyBusy = akeyBusyArr[1];
1084
+ var akeyMsgArr = useState(null); var akeyMsg = akeyMsgArr[0]; var setAkeyMsg = akeyMsgArr[1]; // {kind,text}
1085
+ var nowTickArr = useState(function () { return Date.now(); }); var nowTick = nowTickArr[0]; var setNowTick = nowTickArr[1];
1086
+ var copiedKeyArr = useState(false); var copiedKey = copiedKeyArr[0]; var setCopiedKey = copiedKeyArr[1];
1087
+ var devSessArr = useState(null); var devSessions = devSessArr[0]; var setDevSessions = devSessArr[1]; // null=未加载
1088
+ var devOpenArr = useState(false); var devOpen = devOpenArr[0]; var setDevOpen = devOpenArr[1];
1089
+ var devBusyArr = useState(""); var devBusy = devBusyArr[0]; var setDevBusy = devBusyArr[1];
1090
+ var devMsgArr = useState(null); var devMsg = devMsgArr[0]; var setDevMsg = devMsgArr[1];
1091
+ var armedDevArr = useState(null); var armedDev = armedDevArr[0]; var setArmedDev = armedDevArr[1]; // 待二次确认的 session id(取消配对)
1092
+ var armedDelArr = useState(null); var armedDel = armedDelArr[0]; var setArmedDel = armedDelArr[1]; // 待二次确认的 session id(删除记录)
1093
+ var purgeArmedArr = useState(false); var purgeArmed = purgeArmedArr[0]; var setPurgeArmed = purgeArmedArr[1]; // 清理已解绑二次确认
1094
+ // ── 🔒 修改密码(已登录 SaaS,短信重置)状态:放在全部既有字段之后,保持既有 hook 序号 ──
1095
+ // 成功后企业端使该账号全部授权设备/会话失效(含 E2EE 派生口令)→ 本地登出、用新密码重新登录。
1096
+ var pwdOpenArr = useState(false); var pwdOpen = pwdOpenArr[0]; var setPwdOpen = pwdOpenArr[1]; // 小表单展开
1097
+ var pwdCapArr = useState(null); var pwdCap = pwdCapArr[0]; var setPwdCap = pwdCapArr[1]; // {id,svg} 图形验证码
1098
+ var pwdCapTxtArr = useState(""); var pwdCapTxt = pwdCapTxtArr[0]; var setPwdCapTxt = pwdCapTxtArr[1];
1099
+ var pwdSmsArr = useState(""); var pwdSms = pwdSmsArr[0]; var setPwdSms = pwdSmsArr[1];
1100
+ var pwdSmsBtnArr = useState("获取验证码"); var pwdSmsBtn = pwdSmsBtnArr[0]; var setPwdSmsBtn = pwdSmsBtnArr[1];
1101
+ var pwdNewArr = useState(""); var pwdNew = pwdNewArr[0]; var setPwdNew = pwdNewArr[1]; // 新密码(≥8)
1102
+
1103
+ var refresh = useCallback(function () {
1104
+ setBusy("status");
1105
+ api("/dsh-remote/status").then(function (body) {
1106
+ setSt(body);
1107
+ setMode(body && body.config && body.config.mode === "local" ? "local" : "saas");
1108
+ setMessage(null);
1109
+ }).catch(function (e) { setMessage({ kind: "err", text: "读取状态失败: " + e.message }); })
1110
+ .finally(function () { setBusy(""); });
1111
+ }, []);
1112
+
1113
+ function loadCaptcha(kind) {
1114
+ var setCap = kind === "login" ? setLcap : setRcap;
1115
+ setCap({ id: null, svg: '<span class="dru-hint">加载中</span>' });
1116
+ fetch("/dsh-remote/captcha").then(function (r) {
1117
+ return r.text().then(function (text) {
1118
+ if (!r.ok) throw new Error("http " + r.status);
1119
+ var body = null; try { body = JSON.parse(text); } catch (e) { body = null; }
1120
+ if (body && body.captcha_id) setCap({ id: body.captcha_id, svg: body.svg || "" });
1121
+ else if (body && body.svg) setCap({ id: null, svg: body.svg });
1122
+ else setCap({ id: null, svg: null });
1123
+ });
1124
+ }).catch(function () { setCap({ id: null, svg: null }); });
1125
+ }
1126
+
1127
+ var loggedIn = !!(st && st.config && (st.config.phone || st.config.hasLocalKey));
1128
+ var serviceRunning = !!(st && st.service && st.service.running);
1129
+ var launchdPid = st && st.service && st.service.launchd && st.service.launchd.pid;
1130
+
1131
+ // 已登录(SaaS)→ 拉我的信息/配额/公共配置
1132
+ useEffect(function () {
1133
+ if (!(st && st.config && st.config.phone)) return;
1134
+ api("/dsh-remote/account").then(function (b) { if (b && b.ok) setAccount(b.account); }).catch(function () {});
1135
+ api("/dsh-remote/quota").then(function (b) { if (b && b.ok) setQuota(b.quota); }).catch(function () {});
1136
+ api("/dsh-remote/remote-url").then(function (b) { if (b && b.ok && b.publicConfig) setPub(b.publicConfig); }).catch(function () {});
1137
+ }, [st && st.config && st.config.phone]);
1138
+
1139
+ useEffect(function () { refresh(); }, [refresh]);
1140
+ useEffect(function () {
1141
+ // 展开「忘记密码」重置表单时不用预载登录表单验证码(收起的 reset 表单用独立 pwdCap)
1142
+ if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !pwdOpen && !lcap) loadCaptcha("login");
1143
+ }, [st, loggedIn, mode, authTab, pwdOpen, lcap]);
1144
+
1145
+ // ── 📱 远程访问卡:常量 / 轮询 / 一次性访问密钥 / 已授权设备 ─────────────────
1146
+ var KEY_AUTO_REFRESH_MS = 25000; // 停留栏目时约每 25s 自动轮换一把新的一次性密钥(防已用/过期)
1147
+ var STATUS_POLL_MS = 30000; // 连接状态行轮询间隔(审计降频:原 5s→30s;页面隐藏时暂停)
1148
+
1149
+ /** 轻量状态轮询:只更新 st,不改 mode(mode 由用户 Tab 选择决定,避免轮询把自建/云端来回切)。 */
1150
+ function pollStatus() {
1151
+ if (document.hidden) return; // 页面隐藏时不空转(审计:降低请求频率)
1152
+ api("/dsh-remote/status").then(function (body) {
1153
+ if (!body || !body.ok) return;
1154
+ setSt(body);
1155
+ }).catch(function () {});
1156
+ }
1157
+
1158
+ // 时间工具(兼容 epoch 毫秒 / ISO 字符串 / 数字字符串)
1159
+ function toMs(ts) {
1160
+ if (ts === null || ts === undefined || ts === "") return 0;
1161
+ var n = Number(ts);
1162
+ if (String(ts).trim() !== "" && isFinite(n)) return n;
1163
+ var d = new Date(ts);
1164
+ return isNaN(d.getTime()) ? 0 : d.getTime();
1165
+ }
1166
+ function pad2(n) { return String(n).padStart(2, "0"); }
1167
+ function fmtClock(ts) {
1168
+ var ms = toMs(ts);
1169
+ if (!ms) return "--:--:--";
1170
+ var d = new Date(ms);
1171
+ if (isNaN(d.getTime())) return "--:--:--";
1172
+ return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
1173
+ }
1174
+ function fmtRemain(ms) {
1175
+ var s = Math.max(0, Math.floor((ms || 0) / 1000));
1176
+ var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
1177
+ return (h > 0 ? pad2(h) + ":" : "") + pad2(m) + ":" + pad2(sec);
1178
+ }
1179
+ function fmtDT(ts) {
1180
+ var ms = toMs(ts);
1181
+ if (!ms) return "—";
1182
+ var d = new Date(ms);
1183
+ if (isNaN(d.getTime())) return "—";
1184
+ return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + " " + pad2(d.getHours()) + ":" + pad2(d.getMinutes());
1185
+ }
1186
+
1187
+ /** 创建(或刷新)一次性访问密钥:GET /dsh-remote/access-key(node 半转发企业端 /api/auth-key)。 */
1188
+ function loadAccessKey() {
1189
+ if (akeyBusy) return;
1190
+ // 登录前不请求企业端(避免 401/404 噪音);由账号登录成功后触发
1191
+ if (!(st && st.config && st.config.phone)) {
1192
+ setAkeyMsg(null);
1193
+ return;
1194
+ }
1195
+ setAkeyBusy(true);
1196
+ api("/dsh-remote/access-key").then(function (b) {
1197
+ if (!b || !b.ok) {
1198
+ var why = (b && (b.error || (b.body && b.body.error))) || "未知错误";
1199
+ setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:" + why });
1200
+ return;
1201
+ }
1202
+ // 契约容错:url 必须有;qr_data_url 取不到时仍展示链接/复制/直接打开
1203
+ if (!b.url) {
1204
+ setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:企业端未返回可用链接" });
1205
+ return;
1206
+ }
1207
+ setAkey({
1208
+ url: b.url || "",
1209
+ key: b.key != null ? b.key : null,
1210
+ expires_at: b.expires_at != null ? b.expires_at : null,
1211
+ ttl_ms: b.ttl_ms != null ? b.ttl_ms : null,
1212
+ qr_data_url: b.qr_data_url != null ? b.qr_data_url : null
1213
+ });
1214
+ setAkeyMsg(null);
1215
+ }).catch(function (e) {
1216
+ setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:" + e.message });
1217
+ }).finally(function () { setAkeyBusy(false); });
1218
+ }
1219
+
1220
+ var copyKeyUrl = function () {
1221
+ if (!(akey && akey.url)) return;
1222
+ try {
1223
+ navigator.clipboard.writeText(akey.url).then(function () {
1224
+ setCopiedKey(true);
1225
+ setTimeout(function () { setCopiedKey(false); }, 1500);
1226
+ });
1227
+ } catch (e) {}
1228
+ };
1229
+ /** 「直接打开」:浏览器新标签打开一次性访问地址(打开即扫码/点击进入)。 */
1230
+ var openKeyUrl = function () {
1231
+ if (!(akey && akey.url)) return;
1232
+ try { window.open(akey.url, "_blank", "noopener"); } catch (e) {}
1233
+ };
1234
+
1235
+ /** 加载已授权设备列表:GET /dsh-remote/mobile-sessions。 */
1236
+ function loadDevices() {
1237
+ if (devBusy !== "") return;
1238
+ setDevBusy("list");
1239
+ api("/dsh-remote/mobile-sessions").then(function (b) {
1240
+ if (!b || !b.ok) throw new Error((b && b.error) || "加载已授权设备失败");
1241
+ setDevSessions(Array.isArray(b.sessions) ? b.sessions : []);
1242
+ setDevMsg(null);
1243
+ }).catch(function (e) {
1244
+ setDevMsg({ kind: "err", text: "加载已授权设备失败:" + e.message });
1245
+ }).finally(function () { setDevBusy(""); });
1246
+ }
1247
+ /** 操作(取消配对/删除记录/清理已解绑)成功后静默重拉列表,覆盖行内状态。 */
1248
+ function refreshDeviceList() {
1249
+ api("/dsh-remote/mobile-sessions").then(function (b) {
1250
+ if (b && b.ok) setDevSessions(Array.isArray(b.sessions) ? b.sessions : []);
1251
+ }).catch(function () {});
1252
+ }
1253
+
1254
+ var toggleDevices = function () {
1255
+ var next = !devOpen;
1256
+ setDevOpen(next);
1257
+ if (next && devSessions === null && devBusy === "") loadDevices();
1258
+ if (!next) { setArmedDev(null); setArmedDel(null); setPurgeArmed(false); }
1259
+ };
1260
+
1261
+ /** 取消配对:先点一次进入确认态,再点一次才 POST revoke(同 SelfManageCard 二次确认风格)。 */
1262
+ var doRevokeDevice = function (id) {
1263
+ if (!id) return;
1264
+ if (armedDev !== id) { setArmedDev(id); setArmedDel(null); setPurgeArmed(false); return; }
1265
+ setDevBusy("revoke:" + id);
1266
+ post("/dsh-remote/mobile-sessions/revoke", { id: id }).then(function (b) {
1267
+ if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "取消失败");
1268
+ setArmedDev(null);
1269
+ setDevMsg({ kind: "ok", text: "已取消,对方需重新扫码/登录" });
1270
+ refreshDeviceList();
1271
+ }).catch(function (e) {
1272
+ setArmedDev(null);
1273
+ setDevMsg({ kind: "err", text: "取消配对失败:" + e.message });
1274
+ }).finally(function () { setDevBusy(""); });
1275
+ };
1276
+
1277
+ /**
1278
+ * 删除设备记录:任意行(含已取消/历史)都可用,整行删除并拉黑 jti——
1279
+ * DELETE /dsh-remote/mobile-sessions/delete(body {id} → 企业端 DELETE /api/mobile-sessions/:id)。
1280
+ * 先点一次进入确认态,再点一次才发请求。
1281
+ */
1282
+ var doDeleteDevice = function (id) {
1283
+ if (!id) return;
1284
+ if (armedDel !== id) { setArmedDel(id); setArmedDev(null); setPurgeArmed(false); return; }
1285
+ setDevBusy("delete:" + id);
1286
+ api("/dsh-remote/mobile-sessions/delete", {
1287
+ method: "DELETE",
1288
+ headers: { "content-type": "application/json" },
1289
+ body: JSON.stringify({ id: id })
1290
+ }).then(function (b) {
1291
+ if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "删除失败");
1292
+ setArmedDel(null);
1293
+ setDevMsg({ kind: "ok", text: "已删除该设备的记录" });
1294
+ refreshDeviceList();
1295
+ }).catch(function (e) {
1296
+ setArmedDel(null);
1297
+ setDevMsg({ kind: "err", text: "删除记录失败:" + e.message });
1298
+ }).finally(function () { setDevBusy(""); });
1299
+ };
1300
+
1301
+ /**
1302
+ * 清理已解绑:删除本人全部 revoked 行——POST /dsh-remote/mobile-sessions/purge(企业端 purge)。
1303
+ * 先点一次进入确认态,再点一次才发请求;成功后刷新列表并提示清理条数。
1304
+ */
1305
+ var doPurgeDevices = function () {
1306
+ if (!purgeArmed) { setPurgeArmed(true); setArmedDev(null); setArmedDel(null); return; }
1307
+ setDevBusy("purge");
1308
+ post("/dsh-remote/mobile-sessions/purge", {}).then(function (b) {
1309
+ if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "清理失败");
1310
+ setPurgeArmed(false);
1311
+ var n = b.removed != null ? Number(b.removed) : 0;
1312
+ setDevMsg({ kind: "ok", text: n > 0 ? ("已清理 " + n + " 条已解绑记录") : "已清理全部已解绑记录" });
1313
+ refreshDeviceList();
1314
+ }).catch(function (e) {
1315
+ setPurgeArmed(false);
1316
+ setDevMsg({ kind: "err", text: "清理失败:" + e.message });
1317
+ }).finally(function () { setDevBusy(""); });
1318
+ };
1319
+
1320
+ /** 升级/续费带登录态打开:取一次性访问 url,改写为 /app/promo?auth=… 后在手机端进入续费页。 */
1321
+ var openUpgradeAuth = function () {
1322
+ setBusy("upgrade");
1323
+ api("/dsh-remote/access-key").then(function (b) {
1324
+ if (!b || !b.ok || !b.url) throw new Error((b && b.error) || "生成访问链接失败");
1325
+ var promo = promoUrlOf(b.url);
1326
+ try { window.open(promo, "_blank", "noopener"); } catch (e2) {}
1327
+ setMsg("ok", "✅ 升级/续费页已在新标签页打开(带登录态)");
1328
+ }).catch(function (e) {
1329
+ setMsg("err", "打开升级/续费页失败:" + e.message);
1330
+ }).finally(function () { setBusy(""); });
1331
+ };
1332
+
1333
+ // 停留主视图(home)时:轮询连接状态(15s,审计降频) + 每秒刷新倒计时;隐藏暂停、回前台立即刷新
1334
+ useEffect(function () {
1335
+ if (view !== "home") return;
1336
+ var pollIv = setInterval(function () { pollStatus(); }, STATUS_POLL_MS);
1337
+ var tickIv = setInterval(function () { setNowTick(Date.now()); }, 1000);
1338
+ var visFn = function () { if (!document.hidden) pollStatus(); };
1339
+ document.addEventListener("visibilitychange", visFn);
1340
+ return function () {
1341
+ clearInterval(pollIv);
1342
+ clearInterval(tickIv);
1343
+ document.removeEventListener("visibilitychange", visFn);
1344
+ };
1345
+ }, [view]);
1346
+
1347
+ // 已登录云端主视图停留期间:打开即取一把新 key + 已授权设备;之后每 ~25s 自动轮换
1348
+ // (未登录不轮询,避免 401 空转;离开栏目/切视图/退出登录即清理定时器)
1349
+ useEffect(function () {
1350
+ if (view !== "home" || mode !== "saas" || !loggedIn) return undefined;
1351
+ loadAccessKey();
1352
+ loadDevices();
1353
+ var rotateIv = setInterval(function () { loadAccessKey(); }, KEY_AUTO_REFRESH_MS);
1354
+ return function () { clearInterval(rotateIv); };
1355
+ }, [view, mode, loggedIn]);
1356
+
1357
+ function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
1358
+
1359
+ // ---------- 登录(密码/短信) ----------
1360
+ var doLogin = function () {
1361
+ if (!phone.trim() || !pass) { setMsg("err", "请填写手机号与密码"); return; }
1362
+ if (!lcap || !lcap.id || !lcapTxt.trim()) { setMsg("err", "请输入图中验证码(点击图片可刷新)"); if (!lcap) loadCaptcha("login"); return; }
1363
+ var prevPhone = (st && st.config && st.config.phone) || "";
1364
+ setBusy("login");
1365
+ post("/dsh-remote/login", { phone: phone.trim(), password: pass, captcha_id: lcap.id, captcha_answer: lcapTxt.trim() })
1366
+ .then(function (body) {
1367
+ if (body.ok || body.status === 200) {
1368
+ return post("/dsh-remote/config", { phone: phone.trim(), password: pass }).then(function (cfg) {
1369
+ // 切换连接账号(手机号与之前不同):清除旧账号留下的反馈线程凭据
1370
+ if (prevPhone !== phone.trim()) fbClearThreads();
1371
+ setPwdOpen(false); // 登录成功(可能从「忘记密码」返回)→ 收起重置表单
1372
+ setSt(cfg); setPass(""); setLcapTxt(""); setLcap(null);
1373
+ setMsg("ok", "✅ 登录成功,账号已保存");
1374
+ });
1375
+ }
1376
+ var relayBody = body.body || {};
1377
+ var errText = (relayBody.error && relayBody.error.message) || body.error || ("登录失败(" + (body.status || "?") + ")");
1378
+ setMsg("err", String(errText));
1379
+ setLcapTxt(""); setLcap(null); loadCaptcha("login");
1380
+ })
1381
+ .catch(function (e) { setMsg("err", "登录失败: " + e.message); })
1382
+ .finally(function () { setBusy(""); });
1383
+ };
1384
+
1385
+ var sendRegSms = function () {
1386
+ if (!/^1\d{10}$/.test(rphone.trim())) { setMsg("err", "请输入正确的手机号"); return; }
1387
+ if (rcap && (!rcap.id || !rcapTxt.trim())) { setMsg("err", "请输入图中验证码(点击图片可刷新)"); return; }
1388
+ setBusy("sms");
1389
+ post("/dsh-remote/sms-code", { phone: rphone.trim(), ...(rcap ? { captcha_id: rcap.id, captcha_answer: rcapTxt.trim() } : {}) })
1390
+ .then(function (body) {
1391
+ var testCode = body && (body.test_code || (body.body && body.body.test_code));
1392
+ setRcap(null); setRcapTxt("");
1393
+ if (testCode) setMsg("ok", "验证码已发送(测试码: " + testCode + ")");
1394
+ else setMsg("ok", "验证码已发送");
1395
+ var s = 60; setRsmsBtn(s + "s");
1396
+ var t = setInterval(function () { s--; if (s <= 0) { clearInterval(t); setRsmsBtn("获取验证码"); } else setRsmsBtn(s + "s"); }, 1000);
1397
+ })
1398
+ .catch(function (e) {
1399
+ var relayError = e.body && e.body.body && e.body.body.error;
1400
+ if (relayError && relayError.code === "captcha_invalid") {
1401
+ setRcapTxt(""); loadCaptcha("register");
1402
+ setMsg("err", "发送次数较多,请输入图中验证码后重试");
1403
+ } else setMsg("err", "发送失败: " + e.message);
1404
+ })
1405
+ .finally(function () { setBusy(""); });
1406
+ };
1407
+
1408
+ var doRegister = function () {
1409
+ if (!/^1\d{10}$/.test(rphone.trim())) { setMsg("err", "请输入正确的手机号"); return; }
1410
+ if (!rsms.trim()) { setMsg("err", "请填写短信验证码"); return; }
1411
+ if (rpass.length < 8) { setMsg("err", "密码至少 8 位"); return; }
1412
+ if (rpass !== rpass2) { setMsg("err", "两次输入的密码不一致"); return; }
1413
+ setBusy("register");
1414
+ var regPayload = { phone: rphone.trim(), sms_code: rsms.trim(), password: rpass };
1415
+ var invite = rInvite.trim().toUpperCase();
1416
+ if (invite) regPayload.invite_code = invite;
1417
+ if (rcap) { regPayload.captcha_id = rcap.id; regPayload.captcha_answer = rcapTxt.trim(); }
1418
+ post("/dsh-remote/register", regPayload)
1419
+ .then(function (body) {
1420
+ if (body.ok || body.status === 201 || (body.body && body.body.token)) {
1421
+ return post("/dsh-remote/config", { phone: rphone.trim(), password: rpass }).then(function (cfg) {
1422
+ setPwdOpen(false);
1423
+ setSt(cfg);
1424
+ setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRcap(null); setRcapTxt(""); setRInvite("");
1425
+ setMsg("ok", "✅ 注册成功,已自动登录");
1426
+ });
1427
+ }
1428
+ var b = body.body || {};
1429
+ setMsg("err", (b.error && b.error.message) || "注册失败(" + (body.status || "?") + ")");
1430
+ })
1431
+ .catch(function (e) { setMsg("err", "注册失败: " + e.message); })
1432
+ .finally(function () { setBusy(""); });
1433
+ };
1434
+
1435
+ var doLogout = function (clearForm) {
1436
+ setBusy("logout");
1437
+ post("/dsh-remote/logout").then(function (body) {
1438
+ setSt(body); setAccount(null); setQuota(null);
1439
+ // 清空一次性访问密钥/二维码/授权设备等本地状态(退出后不得残留可见)
1440
+ setAkey(null); setAkeyMsg(null);
1441
+ // 退出登录:清除本机保存的用户反馈线程凭据(thread_token 见 FB_THREADS_KEY),
1442
+ // 反馈历史保留在服务端(按账号校验),账号身份变化后本机不再可见旧线程。
1443
+ fbClearThreads();
1444
+ setPwdOpen(false); // 退出登录后回到登录卡,收起重置/修改密码表单
1445
+ if (clearForm) { setPhone(""); setPass(""); setLcapTxt(""); setLcap(null); setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRsmsBtn("获取验证码"); setRcapTxt(""); setRcap(null); }
1446
+ setMessage(null);
1447
+ }).catch(function (e) { setMsg("err", "退出失败: " + e.message); })
1448
+ .finally(function () { setBusy(""); });
1449
+ };
1450
+
1451
+ // ---------- 🔒 修改密码 / 忘记密码(共用:短信验证码重置;企业端公开 POST /api/password/reset) ----------
1452
+ // 图形验证码 + 短信验证码复用既有 /dsh-remote/captcha、/dsh-remote/sms-code 防刷路径;
1453
+ // 新密码 ≥8 位。成功后企业端使全部授权设备/会话失效(含 E2EE 派生口令)→ 本地登出,
1454
+ // 提示用新密码重新登录(登录成功会把新密码写回本地 config,覆盖旧口令并重建 E2EE 派生)。
1455
+ // 两处入口共用同一组字段/函数(pwdOpen/pwdCap/pwdCapTxt/pwdSms/pwdSmsBtn/pwdNew 与
1456
+ // togglePwdForm/sendPwdSms/doResetPwd/renderResetPwdForm,避免重复实现):
1457
+ // - 账号卡「🔒 修改密码」:fromLogin=false,手机号以当前登录账号为准(只读);
1458
+ // - 登录卡「忘记密码?」:fromLogin=true,手机号预填登录输入、可改;成功后请用新密码登录。
1459
+ var PWD_RESET_WARN = "修改后所有已授权设备/会话将失效,需重新登录与解锁";
1460
+
1461
+ function pwdLoadCaptcha() {
1462
+ setPwdCap({ id: null, svg: '<span class="dru-hint">加载中</span>' });
1463
+ fetch("/dsh-remote/captcha").then(function (r) {
1464
+ return r.text().then(function (text) {
1465
+ if (!r.ok) throw new Error("http " + r.status);
1466
+ var body = null; try { body = JSON.parse(text); } catch (e) { body = null; }
1467
+ if (body && body.captcha_id) setPwdCap({ id: body.captcha_id, svg: body.svg || "" });
1468
+ else if (body && body.svg) setPwdCap({ id: null, svg: body.svg });
1469
+ else setPwdCap({ id: null, svg: null });
1470
+ });
1471
+ }).catch(function () { setPwdCap({ id: null, svg: null }); });
1472
+ }
1473
+ /** 展开/收起重置密码表单(账号卡「修改密码」与登录卡「忘记密码」共用开关)。 */
1474
+ var togglePwdForm = function () {
1475
+ var next = !pwdOpen;
1476
+ setPwdOpen(next);
1477
+ setMessage(null);
1478
+ if (next && !pwdCap) pwdLoadCaptcha();
1479
+ };
1480
+ /** 发送短信验证码(图形验证码防刷;captcha_invalid → 重载验证码提示重试)。ph 由调用方给出:
1481
+ * 账号卡=当前登录手机号;登录卡「忘记密码」=表单输入手机号。 */
1482
+ var sendPwdSms = function (ph) {
1483
+ ph = String(ph || "").trim();
1484
+ // 账号卡改密(隐私契约):phone 为空 = 由服务端取本机账号真号发码;仅当显式填了非法号才报错
1485
+ if (ph && !/^1\d{10}$/.test(ph)) { setMsg("err", "请输入正确的手机号"); return; }
1486
+ if (!pwdCap || !pwdCap.id || !pwdCapTxt.trim()) { setMsg("err", "请输入图中验证码(点击图片可刷新)"); return; }
1487
+ setBusy("pwd-sms");
1488
+ post("/dsh-remote/sms-code", { phone: ph, captcha_id: pwdCap.id, captcha_answer: pwdCapTxt.trim() })
1489
+ .then(function (body) {
1490
+ var testCode = body && (body.test_code || (body.body && body.body.test_code));
1491
+ setPwdCap(null); setPwdCapTxt(""); // 验证码一次性:发送后失效
1492
+ if (testCode) setMsg("ok", "验证码已发送(测试码: " + testCode + ")");
1493
+ else setMsg("ok", "验证码已发送");
1494
+ var s = 60; setPwdSmsBtn(s + "s");
1495
+ var t = setInterval(function () { s--; if (s <= 0) { clearInterval(t); setPwdSmsBtn("获取验证码"); } else setPwdSmsBtn(s + "s"); }, 1000);
1496
+ })
1497
+ .catch(function (e) {
1498
+ var relayError = e.body && e.body.body && e.body.body.error;
1499
+ if (relayError && relayError.code === "captcha_invalid") {
1500
+ setPwdCapTxt(""); pwdLoadCaptcha();
1501
+ setMsg("err", "发送次数较多,请输入图中验证码后重试");
1502
+ } else setMsg("err", "发送失败: " + e.message);
1503
+ })
1504
+ .finally(function () { setBusy(""); });
1505
+ };
1506
+ /**
1507
+ * 确认重置密码:POST /dsh-remote/password/reset → 成功后本地登出,提示用新密码登录。
1508
+ * @param {string} ph - 手机号(账号卡=当前登录账号;登录卡「忘记密码」=表单输入)。
1509
+ * @param {boolean} fromLogin - true=登录卡「忘记密码」;false=账号卡「修改密码」。
1510
+ */
1511
+ var doResetPwd = function (ph, fromLogin) {
1512
+ ph = String(ph || "").trim();
1513
+ if (ph && !/^1\d{10}$/.test(ph)) { setMsg("err", "请输入正确的手机号"); return; }
1514
+ if (!ph && fromLogin) { setMsg("err", "请输入正确的手机号"); return; } // 登录卡忘记密码必须显式填号
1515
+ if (!pwdSms.trim()) { setMsg("err", "请填写短信验证码"); return; }
1516
+ if (pwdNew.length < 8) { setMsg("err", "新密码至少 8 位"); return; }
1517
+ setBusy("pwd-reset");
1518
+ post("/dsh-remote/password/reset", { phone: ph, sms_code: pwdSms.trim(), new_password: pwdNew })
1519
+ .then(function (body) {
1520
+ var ok = !!(body && (body.ok === true || (body.body && body.body.ok === true)));
1521
+ if (!ok) {
1522
+ var relayBody = (body && body.body) || body || {};
1523
+ var errText = (relayBody.error && (relayBody.error.message || relayBody.error))
1524
+ || relayBody.detail || ("修改失败(" + ((body && body.status) || "?") + ")");
1525
+ setMsg("err", (fromLogin ? "重置失败:" : "修改失败:") + String(errText));
1526
+ setPwdSms(""); // 短信验证码一次性:失败后需重新获取
1527
+ return undefined;
1528
+ }
1529
+ // 成功:全部授权设备/会话已失效(含 E2EE)→ 本地登出清掉 config 旧口令与本地凭据,
1530
+ // 提示用新密码重新登录(登录会把新口令写入 config 并重启 bridge / 重建 E2EE)。
1531
+ setBusy("pwd-logout");
1532
+ return post("/dsh-remote/logout").then(function (lb) {
1533
+ setSt(lb); setAccount(null); setQuota(null);
1534
+ setAkey(null); setAkeyMsg(null);
1535
+ fbClearThreads();
1536
+ setPwdOpen(false); setPwdSms(""); setPwdNew(""); setPwdCap(null); setPwdCapTxt(""); setPwdSmsBtn("获取验证码");
1537
+ if (fromLogin) {
1538
+ // 「忘记密码」成功:本机登出完成 → 回到登录表单提示用新密码登录(手机号保留,便于直接重登)
1539
+ setPass(""); setLcapTxt(""); setLcap(null); loadCaptcha("login");
1540
+ setMsg("ok", "✅ 密码已重置成功:" + PWD_RESET_WARN + ",本机已退出登录。请用新密码登录。");
1541
+ } else {
1542
+ setMsg("ok", "✅ 密码已修改成功:所有已授权设备与会话已失效,本机已退出登录。" +
1543
+ "请在下方账号卡用「新密码」重新登录——登录会更新本机保存的密码并重新启用端到端加密(E2EE)。");
1544
+ }
1545
+ }).catch(function (e) {
1546
+ setMsg("err", "密码已" + (fromLogin ? "重置" : "修改") + "成功,但本机退出登录失败:" + e.message + "(建议手动「退出登录」后用新密码重新登录,以更新本机配置密码)");
1547
+ });
1548
+ })
1549
+ .catch(function (e) { setMsg("err", (fromLogin ? "重置失败: " : "修改失败: ") + e.message); })
1550
+ .finally(function () { setBusy(""); });
1551
+ };
1552
+
1553
+ var toggleBridge = function (start) {
1554
+ setBusy(start ? "start" : "stop");
1555
+ post(start ? "/dsh-remote/start" : "/dsh-remote/stop")
1556
+ .then(function (body) {
1557
+ setSt(body);
1558
+ setMsg(body.ok ? "ok" : "err", body.ok ? (start ? "✅ bridge 已启动" + (body.pid ? " (pid=" + body.pid + ")" : "") : "bridge 已停止") : (body.detail || body.status || "操作失败"));
1559
+ })
1560
+ .catch(function (e) { setMsg("err", (start ? "启动" : "停止") + "失败: " + e.message); })
1561
+ .finally(function () { setBusy(""); });
1562
+ };
1563
+
1564
+ var saveLocal = function () {
1565
+ if (!selfHost.trim() || !localKey.trim()) { setMsg("err", "请填写服务器地址与访问密钥"); return; }
1566
+ setBusy("local");
1567
+ post("/dsh-remote/config", { mode: "local", selfHostUrl: selfHost.trim(), localKey: localKey.trim() })
1568
+ .then(function (body) {
1569
+ // 切换到自建服务(连接账号上下文变为本地无账号):清除 SaaS 账号的反馈线程凭据
1570
+ fbClearThreads();
1571
+ setPwdOpen(false);
1572
+ setSt(body); setSelfHost(""); setLocalKey("");
1573
+ setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,bridge 已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
1574
+ })
1575
+ .catch(function (e) { setMsg("err", "保存失败: " + e.message); })
1576
+ .finally(function () { setBusy(""); });
1577
+ };
1578
+
1579
+ var loadInvite = function () {
1580
+ setBusy("invite");
1581
+ api("/dsh-remote/remote-url").then(function (b) {
1582
+ var pubBody = (b && b.publicConfig) || {};
1583
+ setPub(pubBody);
1584
+ }).catch(function () {});
1585
+ api("/dsh-remote/invite-records").then(function (b) {
1586
+ if (b && b.ok) setInviteData({ records: b.records || [], rewards: b.rewards || [] });
1587
+ else setInviteData({ records: [], rewards: [] });
1588
+ }).catch(function () { setInviteData({ records: [], rewards: [] }); })
1589
+ .finally(function () { setBusy(""); });
1590
+ };
1591
+
1592
+ var field = function (label, inputEl) {
1593
+ return h("div", { className: "dru-field" }, h("label", null, label), inputEl);
1594
+ };
1595
+ var input = function (attrs) { return h("input", Object.assign({ className: "dru-input", type: "text" }, attrs)); };
1596
+ var card = function (title, children) { return h("div", { className: "dru-card" }, title ? h("h3", null, title) : null, children); };
1597
+
1598
+ // ---------- 我的信息(个人中心) ----------
1599
+ function fmtDate(ts) {
1600
+ if (!ts) return "—";
1601
+ 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 "—"; }
1602
+ }
1603
+ function renderAccount() {
1604
+ var a = account;
1605
+ var plan = a ? a.plan : "free";
1606
+ var source = a ? (a.plan_source || "plan") : "plan";
1607
+ var isMember = plan === "pro" || plan === "pro_max";
1608
+ var endsAt = a && (a.plan_ends_at || a.trial_expires_at) ? Number(a.plan_ends_at || a.trial_expires_at) : 0;
1609
+ var quotaPct = quota && quota.limit_enabled ? quota.percent : null;
1610
+ var planText;
1611
+ if (!isMember) planText = "免费额度: 带宽 ≈1Mbps" + (quotaPct !== null ? " · 本月流量已用 " + quotaPct + "%" : " · 本月流量限额 1GB");
1612
+ else if (source === "trial") planText = "试用 PRO 会员 · 到期 " + fmtDate(a.trial_expires_at);
1613
+ else if (endsAt) planText = plan === "pro_max" ? "Pro Max 会员 · 到期 " + fmtDate(endsAt) : "PRO 会员 · 到期 " + fmtDate(endsAt);
1614
+ else planText = plan === "pro_max" ? "Pro Max 长期会员" : "PRO 长期会员";
1615
+ return h("div", null,
1616
+ h("div", { className: "dru-user" },
1617
+ h("div", { className: "dru-avatar" }, (st.config.phone || "D").charAt(0).toUpperCase()),
1618
+ h("div", null,
1619
+ h("div", { className: "mail" }, st.config.phone),
1620
+ h("div", { className: "plan" }, plan === "pro_max" ? "Pro Max 会员" : plan === "pro" ? "PRO 会员" : "免费用户")
1621
+ )
1622
+ ),
1623
+ // 套餐状态(免费额度 / 会员到期日)
1624
+ h("div", { className: "dru-status-line", style: { marginTop: 10 } },
1625
+ h("span", { className: "dru-dot " + (isMember ? "dru-dot-on" : "dru-dot-off") }),
1626
+ h("span", null, planText)
1627
+ ),
1628
+ h("div", { className: "dru-actions", style: { marginTop: 10 } },
1629
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { display: "inline-flex", alignItems: "center" }, disabled: busy !== "", title: "升级/续费(带登录态打开)", onClick: openUpgradeAuth },
1630
+ busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
1631
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友赚会员"),
1632
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
1633
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
1634
+ h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
1635
+ ),
1636
+ h("div", { className: "dru-hint", style: { marginTop: 8 } },
1637
+ "升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
1638
+ (endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : "")),
1639
+ pwdOpen ? renderResetPwdForm(false) : null
1640
+ );
1641
+ }
1642
+
1643
+ // ---------- 🔒 修改密码 / 忘记密码 共用重置表单(图形验证码 + 短信验证码 + 新密码≥8) ----------
1644
+ // fromLogin=false:账号卡「修改密码」手机号以当前登录账号为准(只读);
1645
+ // fromLogin=true:登录卡「忘记密码」手机号预填登录输入、可改(成功后返回登录表单请用新密码登录)。
1646
+ function renderResetPwdForm(fromLogin) {
1647
+ // 隐私审计(2026-09):账号卡「修改密码」手机号只展示掩码;发送/重置时由服务端取本机账号真实号,
1648
+ // 浏览器端不再出现明文手机号。登录卡「忘记密码」为用户自行输入,原样使用。
1649
+ var curPhone = ((st && st.config && st.config.phone) || "");
1650
+ var ph = fromLogin ? phone : "";
1651
+ return h("div", { className: "dru-card", style: { marginTop: 10, border: "1px dashed #d0d7de" } },
1652
+ h("h3", null, fromLogin ? "忘记密码(短信重置)" : "🔒 修改密码(短信验证)"),
1653
+ h("div", { className: "dru-hint", style: { marginBottom: 8 } },
1654
+ fromLogin
1655
+ ? "手机号预填当前输入,可改为其它账号;" + PWD_RESET_WARN + "。"
1656
+ : PWD_RESET_WARN + "(E2EE 用新密码重新派生)。"),
1657
+ field("手机号", fromLogin
1658
+ ? input({ type: "tel", value: ph, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })
1659
+ : h("input", { className: "dru-input", type: "tel", value: curPhone, disabled: true, readOnly: true, title: "以当前登录账号为准" })),
1660
+ field("图形验证码", h("div", { className: "dru-captcha" },
1661
+ input({ value: pwdCapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setPwdCapTxt(e.target.value); } }),
1662
+ h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { pwdLoadCaptcha(); }, dangerouslySetInnerHTML: pwdCap && pwdCap.svg && pwdCap.svg.indexOf("<svg") === 0 ? { __html: pwdCap.svg } : void 0 },
1663
+ pwdCap && pwdCap.svg && pwdCap.svg.indexOf("<svg") !== 0 ? pwdCap.svg : null)
1664
+ )),
1665
+ field("短信验证码", h("div", { className: "dru-captcha" },
1666
+ input({ value: pwdSms, placeholder: "6 位验证码", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setPwdSms(e.target.value); } }),
1667
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: function () { sendPwdSms(ph); } }, pwdSmsBtn)
1668
+ )),
1669
+ field("新密码(至少 8 位)", input({ type: "password", value: pwdNew, minLength: 8, autoComplete: "new-password", placeholder: "至少 8 位", onChange: function (e) { setPwdNew(e.target.value); } })),
1670
+ h("div", { className: "dru-hint", style: { marginBottom: 8 } },
1671
+ fromLogin
1672
+ ? "重置成功后本机会退出登录(如有已保存的旧账号),请用新密码登录。"
1673
+ : "修改成功后,本机保存的旧密码会被清除并退出登录;请用新密码重新登录(登录会更新本机配置密码并重新启用远程访问与端到端加密)。"),
1674
+ h("button", { type: "button", className: "dru-btn dru-btn-danger", style: { width: "100%" }, disabled: busy !== "", onClick: function () { doResetPwd(ph, fromLogin); } },
1675
+ busy === "pwd-reset" ? "提交中…" : busy === "pwd-logout" ? "已重置,正在退出本地登录…" : (fromLogin ? "确认重置密码" : "确认修改密码")),
1676
+ fromLogin
1677
+ ? h("div", { style: { textAlign: "right", marginTop: 6 } },
1678
+ h("button", { type: "button", className: "dru-linkbtn", onClick: function () { togglePwdForm(); } }, "← 返回登录"))
1679
+ : null
1680
+ );
1681
+ }
1682
+
1683
+ // ---------- 邀请视图 ----------
1684
+ function renderInvite() {
1685
+ var rule = (pub && pub.invite_rule) || { n: 3, days: 15 };
1686
+ var code = (account && account.invite_code) || "";
1687
+ var base = (st && st.remoteUrl) || (pub && pub.app_url) || "https://n.risegao.cn:13443/app/";
1688
+ var link = base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(code);
1689
+ var copyInvite = function () {
1690
+ try { navigator.clipboard.writeText(link).then(function () { setCopied(true); setTimeout(function () { setCopied(false); }, 1500); }); } catch (e) {}
1691
+ };
1692
+ return h("div", null,
1693
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回"),
1694
+ card("🎯 邀请好友赚会员", [
1695
+ h("div", { className: "dru-hint", style: { marginBottom: 8 } },
1696
+ "每邀请 " + rule.n + " 位好友安装并注册,你获得 " + rule.days + " 天 PRO 会员(好友完成设备安装后计入有效邀请)。"),
1697
+ code ? h("div", null,
1698
+ h("div", { className: "dru-hint" }, "我的邀请码: " + code),
1699
+ h("div", { className: "dru-url" },
1700
+ h("span", null, link),
1701
+ h("button", { type: "button", onClick: copyInvite }, copied ? "已复制" : "复制链接")
1702
+ ),
1703
+ h("div", { className: "dru-hint", style: { marginTop: 8 } },
1704
+ "把链接发给好友,或把邀请码「" + code + "」告诉他们,注册时填写即可。")
1705
+ ) : h("div", null,
1706
+ h("div", { className: "dru-hint" }, "需要手机号账号登录后才能生成专属邀请链接。"),
1707
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%", marginTop: 10 }, disabled: busy !== "", onClick: function () {
1708
+ setBusy("invite-refresh");
1709
+ api("/dsh-remote/account").then(function (b) {
1710
+ if (b && b.ok && b.account && b.account.invite_code) { setAccount(b.account); setMsg("ok", "邀请码已生成"); }
1711
+ else setMsg("err", "获取邀请码失败,请稍后重试或先登录手机号账号");
1712
+ }).catch(function (e) { setMsg("err", "获取邀请码失败:" + e.message); })
1713
+ .finally(function () { setBusy(""); });
1714
+ } }, busy === "invite-refresh" ? "生成中…" : "获取邀请码")
1715
+ ),
1716
+ h("div", { className: "dru-hint", style: { marginTop: 10 } }, "有效邀请记录(登录可见):")
1717
+ ]),
1718
+ code ? card("我的邀请记录",
1719
+ (inviteData && inviteData.records && inviteData.records.length)
1720
+ ? h("div", { className: "dru-status-line" }, "已邀请 " + inviteData.records.length + " 人(有效绑定), 获得奖励 " + ((inviteData.rewards || []).length) + " 次")
1721
+ : h("div", { className: "dru-hint" }, "暂无有效邀请记录。邀请好友安装并注册,完成设备绑定后即计入。")
1722
+ ) : null
1723
+ );
1724
+ }
1725
+
1726
+ // ---------- 反馈视图(独立页,带返回) ----------
1727
+ function renderFeedback() {
1728
+ return h("div", null,
1729
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回"),
1730
+ h(FeedbackCard, null)
1731
+ );
1732
+ }
1733
+
1734
+ // ---------- 📱 远程访问卡:一次性访问地址 / 二维码 / 状态行 ----------
1735
+ function renderAccessCard() {
1736
+ var hasKey = !!(akey && akey.url);
1737
+ var expMs = akey ? toMs(akey.expires_at) : 0;
1738
+ var remainMs = expMs ? expMs - nowTick : 0;
1739
+ var loggedInSaaS = !!(st && st.config && st.config.phone);
1740
+ var statusTxt = st === null
1741
+ ? "查询中…"
1742
+ : !loggedInSaaS
1743
+ ? "请先登录(下方账号卡片)后启用远程访问"
1744
+ : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
1745
+ var dotCls = "dru-dot " + (loggedInSaaS && serviceRunning ? "dru-dot-on" : "dru-dot-off");
1746
+ // Phase-5:端到端加密(E2EE)状态行 —— 未登录/旧 host 未下发 e2ee 一律不渲染
1747
+ // (桌面宽屏与手机镜像共用同一面板:纯文字状态行、不弹层不打扰);启用=绿点绿字(🔒已启用),
1748
+ // 未启用=灰字 + 中性原因文案(回退普通 HTTPS 连接,不宣称“灰度等待”)。
1749
+ function renderE2eeBadge() {
1750
+ if (!loggedInSaaS) return null;
1751
+ var e = describeE2ee(st && st.service && st.service.e2ee);
1752
+ if (!e) return null;
1753
+ return h("div", { className: "dru-e2ee-line" + (e.kind === "ok" ? " ok" : "") },
1754
+ h("span", { className: "dru-dot " + (e.kind === "ok" ? "dru-dot-on" : "dru-dot-off") }),
1755
+ h("span", null, e.text));
1756
+ }
1757
+ return card("📱 远程访问", [
1758
+ h("div", { className: "dru-status-line" },
1759
+ h("span", { className: dotCls }),
1760
+ h("span", null, statusTxt)
1761
+ ),
1762
+ renderE2eeBadge(),
1763
+ akeyMsg ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } }, akeyMsg.text) : null,
1764
+ hasKey ? h("div", null, [
1765
+ h("div", { className: "dru-url big", style: { marginTop: 8 } },
1766
+ h("span", null, akey.url),
1767
+ h("button", { type: "button", onClick: copyKeyUrl }, copiedKey ? "已复制" : "复制")
1768
+ ),
1769
+ h("div", { className: "dru-access-flex" },
1770
+ h("div", { className: "dru-access-col", style: { alignItems: "center" } },
1771
+ akey.qr_data_url
1772
+ ? h("img", { className: "dru-qr-img", src: akey.qr_data_url, alt: "远程访问二维码" })
1773
+ : h("div", { className: "dru-qr-ph" }, "二维码生成中 / 暂不可用\n链接仍可复制或直接打开"),
1774
+ h("div", { className: "dru-cd" + (remainMs > 0 ? " dru-cd-ok" : "") },
1775
+ "有效至 " + fmtClock(akey.expires_at) + " · 剩余 " + fmtRemain(remainMs))
1776
+ ),
1777
+ h("div", { className: "dru-access-col" },
1778
+ h("div", { className: "dru-key-note" },
1779
+ "扫码即进入,30 分钟有效、用一次即失效。"),
1780
+ h("div", { className: "dru-actions", style: { marginTop: 2 } },
1781
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: akeyBusy, onClick: openKeyUrl }, "直接打开"),
1782
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: akeyBusy, onClick: loadAccessKey }, akeyBusy ? "生成中…" : "刷新二维码/访问链接")
1783
+ ),
1784
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
1785
+ !serviceRunning
1786
+ ? "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。"
1787
+ : "打开链接/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。")
1788
+ )
1789
+ )
1790
+ ]) : h("div", null, [
1791
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
1792
+ loggedInSaaS
1793
+ ? "正在生成一次性访问链接…(手机或另一台电脑扫码/打开即可进入)"
1794
+ : "登录下方「🔑 账号」卡片中的手机号账号后,即可生成一次性访问链接,让手机或另一台电脑远程使用同一份 dsh web。"),
1795
+ h("div", { className: "dru-actions", style: { marginTop: 8 } },
1796
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: akeyBusy, onClick: loadAccessKey },
1797
+ akeyBusy ? "生成中…" : "生成访问链接")
1798
+ )
1799
+ ])
1800
+ ]);
1801
+ }
1802
+
1803
+ // ---------- 📲 已授权设备卡(展开列表 + 行内 取消配对/删除记录 + 底部 清理已解绑) ----------
1804
+ function deviceLabel(s) {
1805
+ if (s && s.label) return String(s.label);
1806
+ var parts = [];
1807
+ if (s && s.os) parts.push(String(s.os));
1808
+ if (s && s.browser) parts.push(String(s.browser));
1809
+ return parts.length ? parts.join(" ") : "未知设备";
1810
+ }
1811
+ function deviceMeta(s) {
1812
+ var parts = [];
1813
+ if (s && s.os) parts.push(String(s.os));
1814
+ if (s && s.browser) parts.push(String(s.browser));
1815
+ return parts.join(" · ");
1816
+ }
1817
+ function renderDeviceList() {
1818
+ if (devSessions === null) {
1819
+ return h("div", { className: "dru-hint" }, devBusy === "list" ? "正在加载已授权设备…" : "加载已授权设备中,请稍候或点上方按钮重试");
1820
+ }
1821
+ if (devSessions.length === 0) {
1822
+ return h("div", { className: "dru-fb-empty" }, "暂无已授权设备(手机扫码后出现)");
1823
+ }
1824
+ var revokedCount = devSessions.filter(function (s) { return !!(s && s.revoked_at); }).length;
1825
+ return h("div", null, [
1826
+ devSessions.map(function (s) {
1827
+ var id = s && s.id;
1828
+ var revoked = !!(s && s.revoked_at);
1829
+ return h("div", { key: id, className: "dru-dev" },
1830
+ h("div", { className: "dru-dev-top" },
1831
+ h("span", { className: "dru-dev-name" }, deviceLabel(s)),
1832
+ deviceMeta(s) ? h("span", { className: "dru-dev-meta" }, deviceMeta(s)) : null,
1833
+ revoked ? h("span", { className: "dru-dev-tag dru-dev-tag-off" }, "已取消配对") : null
1834
+ ),
1835
+ h("div", { className: "dru-dev-sub" },
1836
+ "首次配对 " + fmtDT(s && s.created_at) +
1837
+ (s && s.last_seen_at ? " · 最近活跃 " + fmtDT(s.last_seen_at) : "") +
1838
+ (revoked ? " · 取消于 " + fmtDT(s.revoked_at) : "")
1839
+ ),
1840
+ h("div", { className: "dru-actions", style: { marginTop: 8 } },
1841
+ revoked ? null : h("button", {
1842
+ type: "button",
1843
+ className: "dru-btn dru-btn-danger",
1844
+ disabled: devBusy !== "",
1845
+ onClick: function () { doRevokeDevice(id); }
1846
+ }, devBusy === "revoke:" + id ? "取消中…" : armedDev === id ? "⚠ 再点一次确认取消配对" : "取消配对"),
1847
+ h("button", {
1848
+ type: "button",
1849
+ className: "dru-btn dru-btn-ghost",
1850
+ style: { color: "#cf222e", borderColor: "#cf222e" },
1851
+ disabled: devBusy !== "",
1852
+ onClick: function () { doDeleteDevice(id); }
1853
+ }, devBusy === "delete:" + id ? "删除中…" : armedDel === id ? "⚠ 再点一次确认删除记录" : "删除记录")
1854
+ )
1855
+ );
1856
+ }),
1857
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
1858
+ "取消配对后,对方需重新扫码/登录才能再次远程访问本机。"),
1859
+ h("div", { className: "dru-actions", style: { marginTop: 8, borderTop: "1px dashed #eaeef2", paddingTop: 8 } },
1860
+ h("button", {
1861
+ type: "button",
1862
+ className: "dru-btn dru-btn-ghost",
1863
+ disabled: devBusy !== "" || revokedCount === 0,
1864
+ onClick: doPurgeDevices,
1865
+ title: revokedCount > 0 ? ("清理 " + revokedCount + " 条已解绑记录") : "没有已解绑记录"
1866
+ }, devBusy === "purge" ? "清理中…" : purgeArmed ? "⚠ 再点一次确认清理已解绑" : "清理已解绑" + (revokedCount > 0 ? "(" + revokedCount + ")" : ""))
1867
+ )
1868
+ ]);
1869
+ }
1870
+ function renderDevicesCard() {
1871
+ var count = devSessions === null ? null : devSessions.length;
1872
+ var btnLabel = devOpen
1873
+ ? "收起已授权设备列表"
1874
+ : "已授权设备 " + (count === null ? "…" : count) + "(点击展开管理)";
1875
+ return card("📲 已授权设备", [
1876
+ h("div", { className: "dru-actions", style: { marginTop: 2 } },
1877
+ h("button", {
1878
+ type: "button",
1879
+ className: "dru-btn dru-btn-ghost",
1880
+ style: { width: "100%" },
1881
+ disabled: devBusy !== "",
1882
+ onClick: toggleDevices
1883
+ }, devBusy === "list" ? "加载中…" : btnLabel)
1884
+ ),
1885
+ devOpen ? renderDeviceList() : null,
1886
+ devMsg ? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 6 } }, devMsg.text) : null
1887
+ ]);
1888
+ }
1889
+
1890
+ // ---------- 主视图 ----------
1891
+ function renderHome() {
1892
+ var isLocal = mode === "local";
1893
+ return h("div", null,
1894
+ // 连接模式主 Tab(云端服务 / 自建服务,二选一)
1895
+ h("div", { className: "dru-tabs", style: { marginTop: 4 } },
1896
+ h("div", { className: "dru-tab" + (!isLocal ? " active" : ""), onClick: function () { setMode("saas"); setMessage(null); } }, "☁️ 云端服务"),
1897
+ h("div", { className: "dru-tab" + (isLocal ? " active" : ""), onClick: function () { setMode("local"); setMessage(null); } }, "🖥 自建服务")
1898
+ ),
1899
+ // 云端 tab:📱 远程访问(一次性扫码访问 + 已授权设备)+ 账号(手机号登录,官方托管)
1900
+ !isLocal ? h("div", null,
1901
+ renderAccessCard(),
1902
+ // 已授权设备:仅登录后展示(登录前不显示,避免空列表/误导)
1903
+ loggedIn && st.config.phone ? renderDevicesCard() : null,
1904
+ card("🔑 账号",
1905
+ st === null
1906
+ ? h("div", { className: "dru-hint" }, "正在读取远控状态…")
1907
+ : loggedIn && st.config.phone
1908
+ ? renderAccount()
1909
+ : h("div", null,
1910
+ h("div", { className: "dru-tabs" },
1911
+ h("div", { className: "dru-tab" + (authTab === "login" ? " active" : ""), onClick: function () { setAuthTab("login"); setPwdOpen(false); setMessage(null); } }, "登录"),
1912
+ h("div", { className: "dru-tab" + (authTab === "register" ? " active" : ""), onClick: function () { setAuthTab("register"); setPwdOpen(false); setMessage(null); } }, "注册")
1913
+ ),
1914
+ authTab === "login"
1915
+ ? (pwdOpen
1916
+ // 「忘记密码」展开:与账号卡「修改密码」共用同一重置表单(手机号可编辑、预填登录输入)
1917
+ ? renderResetPwdForm(true)
1918
+ : h("div", null,
1919
+ field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
1920
+ field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
1921
+ field("验证码", h("div", { className: "dru-captcha" },
1922
+ input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
1923
+ h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("login"); }, dangerouslySetInnerHTML: lcap && lcap.svg && lcap.svg.indexOf("<svg") === 0 ? { __html: lcap.svg } : void 0 },
1924
+ lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
1925
+ )),
1926
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "login" ? "登录中…" : "登录"),
1927
+ // 忘记密码:小字入口,点击展开短信验证码重置表单(不抢占任何官方按钮)
1928
+ h("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 } },
1929
+ h("button", { type: "button", className: "dru-linkbtn", disabled: busy !== "", onClick: togglePwdForm }, "忘记密码?"))
1930
+ )
1931
+ )
1932
+ : h("div", null,
1933
+ field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
1934
+ field("密码", input({ type: "password", value: rpass, placeholder: "至少 8 位", autoComplete: "new-password", onChange: function (e) { setRpass(e.target.value); } })),
1935
+ field("确认密码", input({ type: "password", value: rpass2, placeholder: "再次输入密码", autoComplete: "new-password", onChange: function (e) { setRpass2(e.target.value); } })),
1936
+ field("邀请码(选填)", input({ value: rInvite, placeholder: "好友的邀请码,如 A8K2M4XQ", autoComplete: "off", onChange: function (e) { setRInvite(e.target.value); } })),
1937
+ rcap && field("图形验证码", h("div", { className: "dru-captcha" },
1938
+ input({ value: rcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRcapTxt(e.target.value); } }),
1939
+ h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("register"); }, dangerouslySetInnerHTML: rcap.svg && rcap.svg.indexOf("<svg") === 0 ? { __html: rcap.svg } : void 0 },
1940
+ rcap.svg && rcap.svg.indexOf("<svg") !== 0 ? rcap.svg : null)
1941
+ )),
1942
+ field("短信验证码", h("div", { className: "dru-captcha" },
1943
+ input({ value: rsms, placeholder: "6 位验证码", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRsms(e.target.value); } }),
1944
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: sendRegSms }, rsmsBtn)
1945
+ )),
1946
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doRegister }, busy === "register" ? "注册中…" : "注册")
1947
+ ),
1948
+ message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
1949
+ )
1950
+ )
1951
+ ) : card("🔌 自建服务",
1952
+ st && st.config && st.config.mode === "local"
1953
+ ? h("div", null,
1954
+ h("div", { className: "dru-status-line" },
1955
+ h("span", { className: "dru-dot dru-dot-on" }),
1956
+ h("span", null, "已连接 " + (st.config.selfHostUrl || "自建服务"))
1957
+ ),
1958
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
1959
+ "当前使用自建服务(本地认证,无需手机号账号)。切换到云端:在上方「云端服务」标签用手机号账号登录即可。")
1960
+ )
1961
+ : h("div", null,
1962
+ field("服务器地址", input({ value: selfHost, placeholder: "my.example.com:13443", onChange: function (e) { setSelfHost(e.target.value); } })),
1963
+ field("访问密钥", input({ type: "password", value: localKey, placeholder: "自建服务的访问密钥", autoComplete: "off", onChange: function (e) { setLocalKey(e.target.value); } })),
1964
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: saveLocal }, busy === "local" ? "保存中…" : "切换到自建服务"),
1965
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
1966
+ "服务器地址由你自行部署决定:填写你自建的 dsh-remote 服务地址与访问密钥即可(不是上面的云端地址)。部署方法见 README「自建部署」一节;切回云端随时可登录恢复。")
1967
+ )
1968
+ ),
1969
+ // Bridge 状态
1970
+ card("🖥 Bridge 服务",
1971
+ h("div", { className: "dru-status-line" },
1972
+ h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
1973
+ h("span", null, st ? (serviceRunning ? "运行中" : "已停止") : "查询中…"),
1974
+ launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
1975
+ ),
1976
+ h("div", { className: "dru-actions", style: { marginTop: 10 } },
1977
+ !serviceRunning
1978
+ ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动 bridge")
1979
+ : h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止 bridge")
1980
+ ),
1981
+ h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
1982
+ h("div", { className: "dru-meta" }, st ? (st.service && st.service.plistExists ? "自启动服务已安装" : "自启动服务未安装(启动时自动创建)") : ""),
1983
+ st && st.service && st.service.bindError
1984
+ ? h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
1985
+ "⚠️ 设备注册失败:" + (st.service.bindError.message || "未说明原因"),
1986
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
1987
+ st.service.bindError.code === "device_limit_exceeded"
1988
+ ? "已达本套餐设备数上限。若是同一台电脑重装,稍等片刻会自动顶替旧设备;仍未恢复请在手机端「设备管理」解绑旧设备(免费用户每月可解绑 3 次)后,回到这里点「启动 bridge」。"
1989
+ : "请确认网络与账号状态后重试;仍未解决可点下方「彻底卸载」后重新安装。")
1990
+ )
1991
+ : null
1992
+ ),
1993
+ // 关于 dsh-remote(v0.5+ 远程访问价值说明卡片)
1994
+ card("📖 关于 dsh-remote", [
1995
+ h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
1996
+ h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
1997
+ h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
1998
+ h("div", { className: "dru-hint" }, "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。")
1999
+ ]),
2000
+ // 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
2001
+ h(SelfManageCard, null),
2002
+ message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
2003
+ );
2004
+ }
2005
+
2006
+ return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程访问" },
2007
+ h("div", { className: "dru-settings-head" },
2008
+ h("span", { className: "dru-settings-icon" }, "📱"),
2009
+ h("div", null,
2010
+ h("h2", { className: "dru-settings-title" }, "远程访问"),
2011
+ h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)")
2012
+ )
2013
+ ),
2014
+ h("div", { className: "dru-settings-body" },
2015
+ view === "feedback" ? renderFeedback() : view === "invite" ? renderInvite() : renderHome()
2016
+ )
2017
+ );
2018
+ }
2019
+
2020
+ // ── 插件入口 ─────────────────────────────────────────────────────────────
2021
+ var inject = ["slots"];
2022
+ function apply(ctx) {
2023
+ // 面板入口迁移:从侧边栏(sidebar.footer.action)移入「设置」页官方扩展点
2024
+ // settings.section(列表槽,由 ui-settings-general 在 sidebar.settings 下声明)。
2025
+ // order 30 > Agent 预设(20),栏目落在「Agent 预设」下方;label 即栏目名(📱 远程访问)。
2026
+ ctx.slots.inject("settings.section", function () {
2027
+ return ctx.slots.register({
2028
+ name: "settings.section",
2029
+ id: "dsh-remote",
2030
+ order: 30,
2031
+ label: function () { return "📱 远程访问"; }
2032
+ }, RemoteControlSection);
2033
+ });
2034
+ ctx.slots.inject("shell.overlay", function () {
2035
+ return ctx.slots.register({ name: "shell.overlay", id: "dsh-feedback-popup", order: 90 }, FeedbackPopup);
2036
+ });
2037
+
2038
+ // 首次安装引导小红点(localStorage dsh-remote-seen-dot;点击后不再显示)
2039
+ dotWatch();
2040
+ // 侧栏「远程访问」快捷入口(2026-09 恢复注入:与官方「设置」按钮共存不遮挡)
2041
+ navEnsureStart();
2042
+
2043
+ // 满意度弹窗调度:首次观察到 bridge 运行(即“安装完成并体验”)后约 10 分钟弹出;
2044
+ // 每 60 秒复查一次,避免 dsh web 启动晚于到点时间。
2045
+ // 弹窗状态按「账号/设备」分作用域(见 fbPopScope);旧版无后缀键视为已完成并迁移,不再打扰老用户。
2046
+ var legacyPopState = null;
2047
+ try { legacyPopState = JSON.parse(localStorage.getItem(FB_POPUP_KEY) || "null"); } catch (e) { legacyPopState = null; }
2048
+ try { localStorage.removeItem(FB_POPUP_KEY); } catch (e) {}
2049
+ api("/dsh-remote/status").then(function (body) {
2050
+ if (body && body.config) setFbPopScope(body.config.phone || body.config.deviceId || "anon");
2051
+ if (legacyPopState && legacyPopState.state === "done") fbSavePopState({ state: "done", firstSeen: legacyPopState.firstSeen || Date.now(), nextAt: 0 });
2052
+ if (body && body.service && body.service.running) {
2053
+ fbEnsureFirstSeen();
2054
+ fbMaybeOpenPopup();
2055
+ }
2056
+ }).catch(function () {});
2057
+ var popupTimer = setInterval(function () {
2058
+ var s = fbPopState();
2059
+ if (!s || s.state !== "pending") { clearInterval(popupTimer); return; }
2060
+ if (!fbFirstSeenAt) {
2061
+ api("/dsh-remote/status").then(function (body) {
2062
+ if (body && body.config) setFbPopScope(body.config.phone || body.config.deviceId || "anon");
2063
+ if (body && body.service && body.service.running) {
2064
+ fbEnsureFirstSeen();
2065
+ fbMaybeOpenPopup();
2066
+ }
2067
+ }).catch(function () {});
2068
+ return;
2069
+ }
2070
+ fbMaybeOpenPopup();
2071
+ }, 60 * 1000);
2072
+ }
2073
+
2074
+ exports.inject = inject;
2075
+ exports.apply = apply;
2076
+ return module.exports;
2077
+ },
2078
+ });