@mrrisega/dsh-remote 0.6.0-beta.9 → 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.
@@ -5,13 +5,20 @@
5
5
  // factory 内只能 require shell 种子模块(react / react/jsx-runtime 等)。
6
6
  //
7
7
  // 功能:
8
- // - settings.section:设置页「远程访问」栏目(位于「Agent 预设」下方,官方扩展点)
8
+ // - 双入口(2026-09 恢复侧栏注入,与官方共存不遮挡):
9
+ // ① 官方「设置」→「远程访问」栏目(settings.section,位于「Agent 预设」下方,官方扩展点);
10
+ // ② 左侧官方「设置」按钮旁注入独立「📱 远程访问」快捷按钮(id=dru-nav-remote,非 clone,
11
+ // 插在其前方、同布局不覆盖官方按钮;点击直达该栏目,首次带引导红点)
9
12
  // - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
10
13
  // · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册 tabs
14
+ // · 🔒 修改密码(账号卡):已登录账号经 图形验证码+短信验证码 重置(/dsh-remote/password/reset 代理);
15
+ // 成功后全部已授权设备/会话失效 → 本地登出并提示用新密码重新登录(覆盖本机 config 密码)
16
+ // · 忘记密码(登录卡):未登录时登录表单内「忘记密码?」→ 同一套
17
+ // 图形验证码+短信验证码 表单(手机号预填当前输入、可改),成功后本地登出并提示「请用新密码登录」
11
18
  // · 登录要求图形验证码;注册要求两次密码 + 图形验证码
12
19
  // · 📱 远程访问卡:一次性访问密钥(扫码/直接打开/复制 + 到期倒计时自动刷新)与已授权设备管理
13
20
  // · bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
14
- // - 首次安装引导:设置页栏目旁小红点(localStorage dsh-remote-seen-dot 控制)
21
+ // - 首次安装引导:设置页「远程访问」栏目(官方导航 navCell)旁小红点(localStorage dsh-remote-seen-dot 控制)
15
22
  // - shell.overlay:满意度弹窗(安装体验至少 1 小时后弹出,只弹一次)
16
23
  // 所有数据经同源 /dsh-remote/* 宿主路由读写(node 半提供)。
17
24
  window.__ModuleLoader__.load({
@@ -61,6 +68,8 @@ window.__ModuleLoader__.load({
61
68
  ".dru-btn-ghost:hover:not(:disabled){background:#f6f8fa}",
62
69
  ".dru-btn-danger{background:#ffffff;color:#cf222e;border-color:#cf222e}",
63
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}",
64
73
  ".dru-tabs{display:flex;gap:8px;margin-bottom:12px}",
65
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}",
66
75
  ".dru-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
@@ -149,175 +158,14 @@ window.__ModuleLoader__.load({
149
158
  // Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
150
159
  ".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:#57606a}",
151
160
  ".dru-e2ee-line.ok{color:#1a7f37}",
152
- // 左下角「远程访问」快捷小手机图标(安装后常驻;首次点击前带红点)
153
- ".dru-fab{position:fixed;left:18px;bottom:18px;z-index:2147482100;width:46px;height:46px;border-radius:50%;background:#0969da;color:#fff;border:none;display:flex;align-items:center;justify-content:center;font-size:22px;line-height:1;box-shadow:0 6px 18px rgba(0,0,0,.28);cursor:pointer;font-family:inherit;padding:0}",
154
- ".dru-fab:hover{background:#0860bd}",
155
- ".dru-fab-dot{position:absolute;top:2px;right:2px;width:9px;height:9px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px #fff;pointer-events:none}",
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}",
156
166
  ].join("\n");
157
167
  document.head.appendChild(styleEl);
158
168
 
159
- // ── 侧边栏「远程访问」快捷入口(挂到官方设置按钮上方一行,用 dsh 自身的挂载位) ──
160
- // 不用 fixed 悬浮层(会遮挡官方按钮);改为在左侧主菜单的「设置」按钮上方克隆一行同款导航项:
161
- // 点击 = 打开 设置页 → 「远程访问」栏目;首次点击前该入口右上角带小红点(localStorage 一次)。
162
- // 导航命中策略(兼容宿主差异):候选 = [class*=navCell] / [role=tab] / [role=menuitem] /
163
- // [aria-label](纯图标项常只有 aria-label 可读);文本优先取 aria-label,再取 textContent,
164
- // 因此 emoji/图标前缀(如 🖥/📱)不影响子串命中。命中「远程访问」栏目项后打 data-dru-remote
165
- // 标记(同一设置页生命周期内二次命中直接走标记,无需重扫)。始终排除本插件注入的侧栏项
166
- // (id=dru-nav-remote),避免点到自己造成递归。
167
- var NAV_SEEN_KEY = "dsh-remote-nav-seen";
168
- var NAV_ENTRY_ID = "dru-nav-remote";
169
- var NAV_MARK = "data-dru-remote";
170
- var NAV_CAND_SEL = '[class*="navCell"], [role="tab"], [role="menuitem"], [aria-label]';
171
- var NAV_ANY_SEL = 'button, a, [role="button"], [class*="nav"] button, [class*="sidebar"] a';
172
-
173
- /** 元素导航文本:优先 aria-label(纯图标导航常见),再拼 textContent;去空白差异。 */
174
- function navTextOf(el) {
175
- var t = "";
176
- try {
177
- var al = el.getAttribute && el.getAttribute("aria-label");
178
- if (al) t += " " + al;
179
- } catch (e) {}
180
- try { if (el.textContent) t += " " + el.textContent; } catch (e2) {}
181
- return t.replace(/\s+/g, " ").trim();
182
- }
183
- /** 候选是否“可点导航项”:按钮/链接/tab/menuitem/navCell(排除无交互的装饰容器)。 */
184
- function isNavClickable(el) {
185
- try {
186
- var tag = String(el.tagName || "").toLowerCase();
187
- if (tag === "button" || tag === "a") return true;
188
- var role = el.getAttribute && el.getAttribute("role") || "";
189
- if (role === "tab" || role === "menuitem" || role === "button" || role === "link") return true;
190
- return String(el.className || "").indexOf("navCell") !== -1;
191
- } catch (e) { return false; }
192
- }
193
- /** 在候选列表里找文本含 token 的导航项并返回(命中「远程访问」时打标记)。 */
194
- function pickNavHit(nodes, token, excludeId, preferMarked) {
195
- try {
196
- for (var i = 0; i < nodes.length; i++) {
197
- var el = nodes[i];
198
- if (excludeId && el.id === excludeId) continue; // 排除注入项自身,防递归
199
- if (!isNavClickable(el)) continue;
200
- var marked = !!el.getAttribute && el.getAttribute(NAV_MARK) === "1";
201
- if (preferMarked && !marked) continue;
202
- if (!preferMarked && marked) continue;
203
- if (navTextOf(el).indexOf(token) === -1) continue;
204
- if (token === "远程访问") { try { el.setAttribute(NAV_MARK, "1"); } catch (e) {} }
205
- return el;
206
- }
207
- } catch (e) { /* 忽略 */ }
208
- return null;
209
- }
210
- /**
211
- * 点击“文本含 token”的导航项:① 已打 data-dru-remote 标记的(远程栏目)优先;
212
- * ② [class*=navCell]/[role=tab]/[role=menuitem]/[aria-label] 语义候选;
213
- * ③ 任意按钮/链接兜底。全部失败返回 false(调用方决定是否退化进「设置」)。
214
- */
215
- function clickNavToken(token, excludeId) {
216
- try {
217
- if (token === "远程访问") {
218
- var all = document.querySelectorAll(NAV_CAND_SEL + ", " + NAV_ANY_SEL);
219
- var hit = pickNavHit(all, token, excludeId, true);
220
- if (hit) { try { hit.click(); } catch (e) {} return true; }
221
- }
222
- var sem = document.querySelectorAll(NAV_CAND_SEL);
223
- var hit2 = pickNavHit(sem, token, excludeId, false);
224
- if (hit2) { try { hit2.click(); } catch (e) {} return true; }
225
- var any = document.querySelectorAll(NAV_ANY_SEL);
226
- var hit3 = pickNavHit(any, token, excludeId, false);
227
- if (hit3) { try { hit3.click(); } catch (e) {} return true; }
228
- } catch (e) { /* 忽略 */ }
229
- return false;
230
- }
231
- /** 「远程访问」栏目内容区是否已在 DOM(设置页已选中该栏目)。 */
232
- function remoteSectionVisible() {
233
- try {
234
- var el = document.querySelector(".dru-settings-section");
235
- return !!el && (!document.body || document.body.contains(el));
236
- } catch (e) { return false; }
237
- }
238
- /**
239
- * 打开设置页「远程访问」栏目(侧栏注入项/面板内“去设置”等共用入口):
240
- * 已在栏目内容区 → 不动;否则先找栏目项直接点;点不到 → 点「设置」进设置页,
241
- * 再轮询(栏目项异步挂载)补点「远程访问」。全程排除注入项自身防递归。
242
- */
243
- function openRemoteSettings() {
244
- if (remoteSectionVisible()) return;
245
- if (clickNavToken("远程访问", NAV_ENTRY_ID)) return;
246
- clickNavToken("设置", NAV_ENTRY_ID); // 进设置页(导航文字可能不同名/纯图标,交给命中器)
247
- var tries = 0;
248
- var iv = setInterval(function () {
249
- if (remoteSectionVisible() || clickNavToken("远程访问", NAV_ENTRY_ID)) { clearInterval(iv); return; }
250
- if (++tries > 40) clearInterval(iv);
251
- }, 150);
252
- if (typeof iv.unref === "function") iv.unref();
253
- }
254
- // 暴露给宿主/自动化(同一入口,避免重复实现;测试沙箱经此驱动点击流)
255
- try { window.__dshRemoteNav = { open: openRemoteSettings }; } catch (e) {}
256
-
257
- function injectSidebarRemoteEntry() {
258
- try {
259
- if (document.getElementById(NAV_ENTRY_ID)) return;
260
- if (!document.body) { setTimeout(injectSidebarRemoteEntry, 300); return; }
261
- var tries = 0;
262
- var iv = setInterval(function () {
263
- try {
264
- if (document.getElementById(NAV_ENTRY_ID)) { clearInterval(iv); return; }
265
- // 在左侧主导航找「设置」按钮:语义候选优先,文本开头为“设置”或 aria-label 恰为“设置”
266
- // (兼容文字不同名/带 emoji 图标的情况:能读到的标识仍是“设置”)。
267
- var nodes = document.querySelectorAll(NAV_CAND_SEL + ", " + NAV_ANY_SEL);
268
- var settingsBtn = null;
269
- for (var i = 0; i < nodes.length; i++) {
270
- var el = nodes[i];
271
- if (el.id === NAV_ENTRY_ID) continue;
272
- if (!isNavClickable(el)) continue;
273
- var label = (el.getAttribute && el.getAttribute("aria-label")) || "";
274
- var t = (el.textContent || "").trim();
275
- var hit = label === "设置" || label.indexOf("设置") === 0 ||
276
- t === "设置" || t.indexOf("设置") === 0 || t.indexOf("设置 ") === 0;
277
- if (!hit) continue;
278
- settingsBtn = el;
279
- break;
280
- }
281
- if (!settingsBtn || !settingsBtn.parentNode) {
282
- if (++tries > 80) clearInterval(iv);
283
- return;
284
- }
285
- var entry = settingsBtn.cloneNode(false);
286
- entry.id = NAV_ENTRY_ID;
287
- entry.removeAttribute("data-view");
288
- entry.removeAttribute("href");
289
- entry.textContent = "";
290
- entry.setAttribute("role", "button");
291
- entry.setAttribute("tabindex", "0");
292
- entry.setAttribute("aria-label", "远程访问");
293
- entry.textContent = "📱 远程访问";
294
- var seen = false;
295
- try { seen = !!localStorage.getItem(NAV_SEEN_KEY); } catch (e) {}
296
- if (!seen) {
297
- var dot = document.createElement("span");
298
- dot.style.cssText = "position:absolute;top:4px;right:10px;width:8px;height:8px;border-radius:50%;background:#e5484d;pointer-events:none";
299
- entry.style.position = "relative";
300
- entry.appendChild(dot);
301
- }
302
- entry.addEventListener("click", function () {
303
- try { localStorage.setItem(NAV_SEEN_KEY, "1"); } catch (e) {}
304
- var d = entry.querySelector("span[style]");
305
- if (d && /background:#e5484d/.test(d.getAttribute("style") || "")) d.remove();
306
- openRemoteSettings();
307
- });
308
- settingsBtn.parentNode.insertBefore(entry, settingsBtn); // 设置在入口下方
309
- clearInterval(iv);
310
- } catch (e) { /* 忽略单次失败,继续重试 */ }
311
- }, 300);
312
- if (typeof iv.unref === "function") iv.unref();
313
- } catch (e) { /* 非关键 */ }
314
- }
315
- if (document.readyState === "loading") {
316
- document.addEventListener("DOMContentLoaded", injectSidebarRemoteEntry);
317
- } else {
318
- setTimeout(injectSidebarRemoteEntry, 600);
319
- }
320
-
321
169
  // ── 首次安装引导小红点(设置页「远程访问」栏目,localStorage 控制) ────
322
170
  // 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
323
171
  // 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
@@ -381,6 +229,104 @@ window.__ModuleLoader__.load({
381
229
  dotScan();
382
230
  }
383
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
+
384
330
  // ── 宿主 API ────────────────────────────────────────────────────────────
385
331
  function api(path, options) {
386
332
  return fetch(path, options).then(function (res) {
@@ -419,6 +365,10 @@ window.__ModuleLoader__.load({
419
365
  var FB_THREADS_KEY = "dsh-feedback-threads";
420
366
  var FB_POPUP_KEY = "dsh-feedback-popup";
421
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"); }
422
372
  var FB_CATEGORIES = [
423
373
  { value: "feature", label: "功能类" },
424
374
  { value: "bug", label: "Bug 类" },
@@ -445,10 +395,17 @@ window.__ModuleLoader__.load({
445
395
  try { localStorage.removeItem(FB_THREADS_KEY); } catch (e) {}
446
396
  }
447
397
  function fbPopState() {
448
- try { return JSON.parse(localStorage.getItem(FB_POPUP_KEY) || "null"); } catch (e) { return null; }
398
+ try { return JSON.parse(localStorage.getItem(FB_POPUP_KEY + ":" + fbPopScope) || "null"); } catch (e) { return null; }
449
399
  }
450
400
  function fbSavePopState(s) {
451
- try { localStorage.setItem(FB_POPUP_KEY, JSON.stringify(s)); } catch (e) {}
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) {}
452
409
  }
453
410
 
454
411
  /** 同源反馈代理调用(浏览器 → /dsh-remote/feedback/* → 独立反馈服务)。 */
@@ -574,7 +531,7 @@ window.__ModuleLoader__.load({
574
531
  category: cat,
575
532
  title: fbTitle.trim(),
576
533
  content: fbContent.trim(),
577
- contact: contact.trim() || (cfg && cfg.phone ? cfg.phone : ""),
534
+ contact: contact.trim() || ((cfg && cfg.phone && cfg.phone.indexOf("*") === -1) ? cfg.phone : ""),
578
535
  };
579
536
  fbApi("/feedback", { method: "POST", body: JSON.stringify(payload) })
580
537
  .then(function (b) {
@@ -829,6 +786,7 @@ window.__ModuleLoader__.load({
829
786
  .catch(function (e) {
830
787
  var errBody = e.body && e.body.error;
831
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: "你已评价过,感谢支持(同一账号/设备只收集一次)" }); }
832
790
  else setMessage({ kind: "err", text: "提交失败:" + ((errBody && errBody.message) || e.message) + "(可稍后再试)" });
833
791
  })
834
792
  .finally(function () { setBusy(""); });
@@ -1133,6 +1091,14 @@ window.__ModuleLoader__.load({
1133
1091
  var armedDevArr = useState(null); var armedDev = armedDevArr[0]; var setArmedDev = armedDevArr[1]; // 待二次确认的 session id(取消配对)
1134
1092
  var armedDelArr = useState(null); var armedDel = armedDelArr[0]; var setArmedDel = armedDelArr[1]; // 待二次确认的 session id(删除记录)
1135
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)
1136
1102
 
1137
1103
  var refresh = useCallback(function () {
1138
1104
  setBusy("status");
@@ -1172,15 +1138,17 @@ window.__ModuleLoader__.load({
1172
1138
 
1173
1139
  useEffect(function () { refresh(); }, [refresh]);
1174
1140
  useEffect(function () {
1175
- if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !lcap) loadCaptcha("login");
1176
- }, [st, loggedIn, mode, authTab, lcap]);
1141
+ // 展开「忘记密码」重置表单时不用预载登录表单验证码(收起的 reset 表单用独立 pwdCap)
1142
+ if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !pwdOpen && !lcap) loadCaptcha("login");
1143
+ }, [st, loggedIn, mode, authTab, pwdOpen, lcap]);
1177
1144
 
1178
1145
  // ── 📱 远程访问卡:常量 / 轮询 / 一次性访问密钥 / 已授权设备 ─────────────────
1179
1146
  var KEY_AUTO_REFRESH_MS = 25000; // 停留栏目时约每 25s 自动轮换一把新的一次性密钥(防已用/过期)
1180
- var STATUS_POLL_MS = 5000; // 连接状态行轮询间隔
1147
+ var STATUS_POLL_MS = 30000; // 连接状态行轮询间隔(审计降频:原 5s→30s;页面隐藏时暂停)
1181
1148
 
1182
1149
  /** 轻量状态轮询:只更新 st,不改 mode(mode 由用户 Tab 选择决定,避免轮询把自建/云端来回切)。 */
1183
1150
  function pollStatus() {
1151
+ if (document.hidden) return; // 页面隐藏时不空转(审计:降低请求频率)
1184
1152
  api("/dsh-remote/status").then(function (body) {
1185
1153
  if (!body || !body.ok) return;
1186
1154
  setSt(body);
@@ -1362,14 +1330,17 @@ window.__ModuleLoader__.load({
1362
1330
  }).finally(function () { setBusy(""); });
1363
1331
  };
1364
1332
 
1365
- // 停留主视图(home)时:每 5s 轻量轮询连接状态 + 每秒刷新倒计时
1333
+ // 停留主视图(home)时:轮询连接状态(15s,审计降频) + 每秒刷新倒计时;隐藏暂停、回前台立即刷新
1366
1334
  useEffect(function () {
1367
1335
  if (view !== "home") return;
1368
1336
  var pollIv = setInterval(function () { pollStatus(); }, STATUS_POLL_MS);
1369
1337
  var tickIv = setInterval(function () { setNowTick(Date.now()); }, 1000);
1338
+ var visFn = function () { if (!document.hidden) pollStatus(); };
1339
+ document.addEventListener("visibilitychange", visFn);
1370
1340
  return function () {
1371
1341
  clearInterval(pollIv);
1372
1342
  clearInterval(tickIv);
1343
+ document.removeEventListener("visibilitychange", visFn);
1373
1344
  };
1374
1345
  }, [view]);
1375
1346
 
@@ -1397,6 +1368,7 @@ window.__ModuleLoader__.load({
1397
1368
  return post("/dsh-remote/config", { phone: phone.trim(), password: pass }).then(function (cfg) {
1398
1369
  // 切换连接账号(手机号与之前不同):清除旧账号留下的反馈线程凭据
1399
1370
  if (prevPhone !== phone.trim()) fbClearThreads();
1371
+ setPwdOpen(false); // 登录成功(可能从「忘记密码」返回)→ 收起重置表单
1400
1372
  setSt(cfg); setPass(""); setLcapTxt(""); setLcap(null);
1401
1373
  setMsg("ok", "✅ 登录成功,账号已保存");
1402
1374
  });
@@ -1447,6 +1419,7 @@ window.__ModuleLoader__.load({
1447
1419
  .then(function (body) {
1448
1420
  if (body.ok || body.status === 201 || (body.body && body.body.token)) {
1449
1421
  return post("/dsh-remote/config", { phone: rphone.trim(), password: rpass }).then(function (cfg) {
1422
+ setPwdOpen(false);
1450
1423
  setSt(cfg);
1451
1424
  setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRcap(null); setRcapTxt(""); setRInvite("");
1452
1425
  setMsg("ok", "✅ 注册成功,已自动登录");
@@ -1468,12 +1441,115 @@ window.__ModuleLoader__.load({
1468
1441
  // 退出登录:清除本机保存的用户反馈线程凭据(thread_token 见 FB_THREADS_KEY),
1469
1442
  // 反馈历史保留在服务端(按账号校验),账号身份变化后本机不再可见旧线程。
1470
1443
  fbClearThreads();
1444
+ setPwdOpen(false); // 退出登录后回到登录卡,收起重置/修改密码表单
1471
1445
  if (clearForm) { setPhone(""); setPass(""); setLcapTxt(""); setLcap(null); setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRsmsBtn("获取验证码"); setRcapTxt(""); setRcap(null); }
1472
1446
  setMessage(null);
1473
1447
  }).catch(function (e) { setMsg("err", "退出失败: " + e.message); })
1474
1448
  .finally(function () { setBusy(""); });
1475
1449
  };
1476
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
+
1477
1553
  var toggleBridge = function (start) {
1478
1554
  setBusy(start ? "start" : "stop");
1479
1555
  post(start ? "/dsh-remote/start" : "/dsh-remote/stop")
@@ -1492,6 +1568,7 @@ window.__ModuleLoader__.load({
1492
1568
  .then(function (body) {
1493
1569
  // 切换到自建服务(连接账号上下文变为本地无账号):清除 SaaS 账号的反馈线程凭据
1494
1570
  fbClearThreads();
1571
+ setPwdOpen(false);
1495
1572
  setSt(body); setSelfHost(""); setLocalKey("");
1496
1573
  setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,bridge 已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
1497
1574
  })
@@ -1553,11 +1630,53 @@ window.__ModuleLoader__.load({
1553
1630
  busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
1554
1631
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友赚会员"),
1555
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 ? "收起修改密码" : "🔒 修改密码"),
1556
1634
  h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
1557
1635
  ),
1558
1636
  h("div", { className: "dru-hint", style: { marginTop: 8 } },
1559
1637
  "升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
1560
- (endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : ""))
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
1561
1680
  );
1562
1681
  }
1563
1682
 
@@ -1789,19 +1908,26 @@ window.__ModuleLoader__.load({
1789
1908
  ? renderAccount()
1790
1909
  : h("div", null,
1791
1910
  h("div", { className: "dru-tabs" },
1792
- h("div", { className: "dru-tab" + (authTab === "login" ? " active" : ""), onClick: function () { setAuthTab("login"); setMessage(null); } }, "登录"),
1793
- h("div", { className: "dru-tab" + (authTab === "register" ? " active" : ""), onClick: function () { setAuthTab("register"); setMessage(null); } }, "注册")
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); } }, "注册")
1794
1913
  ),
1795
1914
  authTab === "login"
1796
- ? h("div", null,
1797
- field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
1798
- field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
1799
- field("验证码", h("div", { className: "dru-captcha" },
1800
- input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
1801
- 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 },
1802
- lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
1803
- )),
1804
- h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "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
+ )
1805
1931
  )
1806
1932
  : h("div", null,
1807
1933
  field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
@@ -1911,10 +2037,18 @@ window.__ModuleLoader__.load({
1911
2037
 
1912
2038
  // 首次安装引导小红点(localStorage dsh-remote-seen-dot;点击后不再显示)
1913
2039
  dotWatch();
2040
+ // 侧栏「远程访问」快捷入口(2026-09 恢复注入:与官方「设置」按钮共存不遮挡)
2041
+ navEnsureStart();
1914
2042
 
1915
2043
  // 满意度弹窗调度:首次观察到 bridge 运行(即“安装完成并体验”)后约 10 分钟弹出;
1916
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) {}
1917
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 });
1918
2052
  if (body && body.service && body.service.running) {
1919
2053
  fbEnsureFirstSeen();
1920
2054
  fbMaybeOpenPopup();
@@ -1925,6 +2059,7 @@ window.__ModuleLoader__.load({
1925
2059
  if (!s || s.state !== "pending") { clearInterval(popupTimer); return; }
1926
2060
  if (!fbFirstSeenAt) {
1927
2061
  api("/dsh-remote/status").then(function (body) {
2062
+ if (body && body.config) setFbPopScope(body.config.phone || body.config.deviceId || "anon");
1928
2063
  if (body && body.service && body.service.running) {
1929
2064
  fbEnsureFirstSeen();
1930
2065
  fbMaybeOpenPopup();