@mrrisega/dsh-remote 0.6.0-beta.11 → 0.6.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +0 -30
  2. package/clients/dsh-remote/dsh-bridge.mjs +5 -305
  3. package/package.json +2 -2
  4. package/packages/dsh-remote-web/lib/client.js +178 -467
  5. package/packages/dsh-remote-web/lib/index.js +4 -115
  6. package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +11 -81
  7. package/packages/dsh-remote-web/test/account-switch.test.mjs +1 -1
  8. package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -174
  9. package/packages/dsh-remote-web/test/settings-entry.test.mjs +5 -55
  10. package/packages/dsh-remote-web/test/uninstall-runtime.test.mjs +2 -2
  11. package/clients/dsh-remote/e2ee-client.mjs +0 -711
  12. package/clients/dsh-remote/e2ee-shim-script.js +0 -697
  13. package/clients/dsh-remote/e2ee-shim.mjs +0 -158
  14. package/clients/dsh-remote/mobile-adapter.mjs +0 -541
  15. package/clients/dsh-remote/test/bridge-html-inject.test.mjs +0 -110
  16. package/clients/dsh-remote/test/e2ee-bridge.test.mjs +0 -630
  17. package/clients/dsh-remote/test/e2ee-client.test.mjs +0 -365
  18. package/clients/dsh-remote/test/e2ee-shim.test.mjs +0 -507
  19. package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +0 -200
  20. package/clients/dsh-remote/test/lib-e2ee-shim.mjs +0 -59
  21. package/clients/dsh-remote/test/lib-native-e2ee.mjs +0 -92
  22. package/clients/dsh-remote/test/mobile-adapter.test.mjs +0 -124
  23. package/packages/dsh-remote-web/test/e2ee-state.test.mjs +0 -141
  24. package/packages/dsh-remote-web/test/feedback-account-mine.test.mjs +0 -70
  25. package/packages/dsh-remote-web/test/password-reset.test.mjs +0 -429
@@ -5,18 +5,13 @@
5
5
  // factory 内只能 require shell 种子模块(react / react/jsx-runtime 等)。
6
6
  //
7
7
  // 功能:
8
- // - 入口=官方「设置」页内的 settings.section「远程访问」栏目(位于「Agent 预设」下方,
9
- // 官方扩展点);不向官方侧边栏注入任何按钮,官方「设置」按钮原样、不被遮挡
8
+ // - settings.section:设置页「远程访问」栏目(位于「Agent 预设」下方,官方扩展点)
10
9
  // - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
11
10
  // · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册 tabs
12
- // · 🔒 修改密码(账号卡):已登录账号经 图形验证码+短信验证码 重置(/dsh-remote/password/reset 代理);
13
- // 成功后全部已授权设备/会话失效 → 本地登出并提示用新密码重新登录(覆盖本机 config 密码)
14
- // · 忘记密码(登录卡):未登录时登录表单内「忘记密码?」→ 同一套
15
- // 图形验证码+短信验证码 表单(手机号预填当前输入、可改),成功后本地登出并提示「请用新密码登录」
16
11
  // · 登录要求图形验证码;注册要求两次密码 + 图形验证码
17
12
  // · 📱 远程访问卡:一次性访问密钥(扫码/直接打开/复制 + 到期倒计时自动刷新)与已授权设备管理
18
13
  // · bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
19
- // - 首次安装引导:设置页「远程访问」栏目(官方导航 navCell)旁小红点(localStorage dsh-remote-seen-dot 控制)
14
+ // - 首次安装引导:设置页栏目旁小红点(localStorage dsh-remote-seen-dot 控制)
20
15
  // - shell.overlay:满意度弹窗(安装体验至少 1 小时后弹出,只弹一次)
21
16
  // 所有数据经同源 /dsh-remote/* 宿主路由读写(node 半提供)。
22
17
  window.__ModuleLoader__.load({
@@ -66,8 +61,6 @@ window.__ModuleLoader__.load({
66
61
  ".dru-btn-ghost:hover:not(:disabled){background:#f6f8fa}",
67
62
  ".dru-btn-danger{background:#ffffff;color:#cf222e;border-color:#cf222e}",
68
63
  ".dru-btn-danger:hover:not(:disabled){background:#fff0f1}",
69
- ".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}",
70
- ".dru-linkbtn:hover{text-decoration:underline}",
71
64
  ".dru-tabs{display:flex;gap:8px;margin-bottom:12px}",
72
65
  ".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}",
73
66
  ".dru-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
@@ -153,12 +146,97 @@ window.__ModuleLoader__.load({
153
146
  ".dru-dev-sub{font-size:11px;color:#8c959f;margin-top:3px}",
154
147
  ".dru-dev-tag{font-size:10.5px;color:#8c959f;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
155
148
  ".dru-dev-tag-off{color:#cf222e;border-color:#ffb3b6;background:#fff0f1}",
156
- // Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
157
- ".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:#57606a}",
158
- ".dru-e2ee-line.ok{color:#1a7f37}",
149
+ // 左下角「远程访问」快捷小手机图标(安装后常驻;首次点击前带红点)
150
+ ".dru-fab{position:fixed;left:18px;bottom:18px;z-index:2147482100;width:46px;height:46px;border-radius:50%;background:#0969da;color:#fff;border:none;display:flex;align-items:center;justify-content:center;font-size:22px;line-height:1;box-shadow:0 6px 18px rgba(0,0,0,.28);cursor:pointer;font-family:inherit;padding:0}",
151
+ ".dru-fab:hover{background:#0860bd}",
152
+ ".dru-fab-dot{position:absolute;top:2px;right:2px;width:9px;height:9px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px #fff;pointer-events:none}",
159
153
  ].join("\n");
160
154
  document.head.appendChild(styleEl);
161
155
 
156
+ // ── 侧边栏「远程访问」快捷入口(挂到官方设置按钮上方一行,用 dsh 自身的挂载位) ──
157
+ // 不用 fixed 悬浮层(会遮挡官方按钮);改为在左侧主菜单的「设置」按钮上方克隆一行同款导航项:
158
+ // 点击 = 打开 设置页 → 「远程访问」栏目;首次点击前该入口右上角带小红点(localStorage 一次)。
159
+ var NAV_SEEN_KEY = "dsh-remote-nav-seen";
160
+ function clickTextNav(text) {
161
+ try {
162
+ var nodes = document.querySelectorAll('button, [role="tab"], [role="menuitem"], [class*="navCell"], [class*="nav"], [class*="sidebar"] a, a');
163
+ for (var i = 0; i < nodes.length; i++) {
164
+ var el = nodes[i];
165
+ var t = (el.textContent || "").trim();
166
+ if (t === text || t.indexOf(text) === 0) {
167
+ try { el.click(); return true; } catch (e) { /* 尝试下一个 */ }
168
+ }
169
+ }
170
+ } catch (e) { /* 忽略 */ }
171
+ return false;
172
+ }
173
+ function openRemoteSettings() {
174
+ if (clickTextNav("远程访问")) return; // 已在栏目内/可直接点到
175
+ if (clickTextNav("设置")) { /* 进入设置页后再轮询远程访问栏目 */ }
176
+ var tries = 0;
177
+ var iv = setInterval(function () {
178
+ if (clickTextNav("远程访问")) { clearInterval(iv); return; }
179
+ if (++tries > 40) clearInterval(iv);
180
+ }, 150);
181
+ if (typeof iv.unref === "function") iv.unref();
182
+ }
183
+ function injectSidebarRemoteEntry() {
184
+ try {
185
+ if (document.getElementById("dru-nav-remote")) return;
186
+ if (!document.body) { setTimeout(injectSidebarRemoteEntry, 300); return; }
187
+ var tries = 0;
188
+ var iv = setInterval(function () {
189
+ try {
190
+ if (document.getElementById("dru-nav-remote")) { clearInterval(iv); return; }
191
+ // 只在「设置页已打开/未打开都能出现」的左侧主导航找「设置」按钮;
192
+ // 取文本以“设置”开头且最可能是菜单项(避免命中标题/弹层里的“设置”文字)
193
+ var nodes = document.querySelectorAll('button, [role="menuitem"], a, [class*="navCell"]');
194
+ var settingsBtn = null;
195
+ for (var i = 0; i < nodes.length; i++) {
196
+ var el = nodes[i];
197
+ var t = (el.textContent || "").trim();
198
+ if (t.indexOf("设置") === 0 || t.indexOf("设置 ") === 0 || t === "设置") { settingsBtn = el; break; }
199
+ }
200
+ if (!settingsBtn || !settingsBtn.parentNode) {
201
+ if (++tries > 80) clearInterval(iv);
202
+ return;
203
+ }
204
+ var entry = settingsBtn.cloneNode(false);
205
+ entry.id = "dru-nav-remote";
206
+ entry.removeAttribute("data-view");
207
+ entry.removeAttribute("href");
208
+ entry.textContent = "";
209
+ entry.setAttribute("role", "button");
210
+ entry.setAttribute("tabindex", "0");
211
+ entry.setAttribute("aria-label", "远程访问");
212
+ entry.textContent = "📱 远程访问";
213
+ var seen = false;
214
+ try { seen = !!localStorage.getItem(NAV_SEEN_KEY); } catch (e) {}
215
+ if (!seen) {
216
+ var dot = document.createElement("span");
217
+ dot.style.cssText = "position:absolute;top:4px;right:10px;width:8px;height:8px;border-radius:50%;background:#e5484d;pointer-events:none";
218
+ entry.style.position = "relative";
219
+ entry.appendChild(dot);
220
+ }
221
+ entry.addEventListener("click", function () {
222
+ try { localStorage.setItem(NAV_SEEN_KEY, "1"); } catch (e) {}
223
+ var d = entry.querySelector("span[style]");
224
+ if (d && /background:#e5484d/.test(d.getAttribute("style") || "")) d.remove();
225
+ openRemoteSettings();
226
+ });
227
+ settingsBtn.parentNode.insertBefore(entry, settingsBtn); // 设置在入口下方
228
+ clearInterval(iv);
229
+ } catch (e) { /* 忽略单次失败,继续重试 */ }
230
+ }, 300);
231
+ if (typeof iv.unref === "function") iv.unref();
232
+ } catch (e) { /* 非关键 */ }
233
+ }
234
+ if (document.readyState === "loading") {
235
+ document.addEventListener("DOMContentLoaded", injectSidebarRemoteEntry);
236
+ } else {
237
+ setTimeout(injectSidebarRemoteEntry, 600);
238
+ }
239
+
162
240
  // ── 首次安装引导小红点(设置页「远程访问」栏目,localStorage 控制) ────
163
241
  // 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
164
242
  // 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
@@ -242,21 +320,7 @@ window.__ModuleLoader__.load({
242
320
  return api(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(data || {}) });
243
321
  };
244
322
 
245
- /** 一次性访问 url → 升级/续费页 url(带同一 auth,进入后即登录态到 /app/promo)。 */
246
- function promoUrlOf(u) {
247
- var s = String(u || "");
248
- if (/\/app\/\?auth=/i.test(s)) return s.replace(/\/app\/\?auth=/i, "/app/promo?auth=");
249
- try {
250
- var m = /auth=([^&#]+)/.exec(s);
251
- var origin = s.split("/app")[0];
252
- if (m && origin) return origin + "/app/promo?auth=" + m[1];
253
- } catch (e) { /* fallthrough */ }
254
- return s;
255
- }
256
-
257
323
  // ── 用户反馈模块:本地状态(thread 令牌 / 弹窗节流) ───────────────────
258
- // 「我的反馈」双数据源:本地 dsh-feedback-threads(thread_token 凭据,匿名/本机未同步行)
259
- // + 服务端 /api/feedback/mine(登录态账号历史,见 FeedbackCard loadMine/fbRows,合并去重展示)。
260
324
  var FB_THREADS_KEY = "dsh-feedback-threads";
261
325
  var FB_POPUP_KEY = "dsh-feedback-popup";
262
326
  var FB_POPUP_DELAY_MS = 60 * 60 * 1000; // 安装/体验后至少 1 小时才弹窗(只弹一次)
@@ -322,17 +386,9 @@ window.__ModuleLoader__.load({
322
386
  var threadDataArr = useState(null); var threadData = threadDataArr[0]; var setThreadData = threadDataArr[1];
323
387
  var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
324
388
  var msgArr = useState(null); var fbMsg = msgArr[0]; var setFbMsg = msgArr[1];
325
- // 账号历史(服务端 /feedback/mine 聚合列表):null=未加载;登录态(SaaS 账号已配)打开「我的反馈」时拉取
326
- var mineListArr = useState(null); var mineList = mineListArr[0]; var setMineList = mineListArr[1];
327
- var mineErrArr = useState(false); var mineErr = mineErrArr[0]; var setMineErr = mineErrArr[1];
328
- var mineBusyArr = useState(false); var mineBusy = mineBusyArr[0]; var setMineBusy = mineBusyArr[1];
329
389
 
330
390
  function setMsg(kind, text) { setFbMsg({ kind: kind, text: text }); }
331
391
 
332
- // 登录态(SaaS 且账号已配)→ 打开「我的反馈」拉账号历史(/mine 需要 JWT,节点半在登录态自动附加);
333
- // 自建/未登录(cfg.phone 为空)→ 仍走本地 thread_token,不发 /mine。
334
- var fbAccount = !!(cfg && cfg.phone && fbAuth === "account");
335
-
336
392
  var loadCfg = useCallback(function () {
337
393
  api("/dsh-remote/feedback-config").then(function (b) {
338
394
  setCfg(b);
@@ -343,68 +399,18 @@ window.__ModuleLoader__.load({
343
399
  }).catch(function (e) { setCfg({ reachable: false }); setMsg("warn", "反馈服务未连接:" + e.message); });
344
400
  }, []);
345
401
 
346
- /** 服务端账号历史行(mine 列表含该 id)→ 单条打开/回复走服务端(节点半自动附 JWT),无需本地 thread_token。 */
347
- function fbRowIsAccount(id) {
348
- return !!(mineList && mineList.some(function (x) { return x.id === id; }));
349
- }
350
-
351
402
  function loadThread(id) {
352
403
  var t = threads.filter(function (x) { return x.id === id; })[0];
353
- // 账号历史行:不带 thread_token(登录态由节点半自动附账号 JWT,服务端按账号归属放行);
354
- // 匿名/纯本地行:仍按原 thread_token 逻辑(带本地凭据访问)。
355
- var accountRow = fbRowIsAccount(id);
356
- if (!accountRow && !t) return;
404
+ if (!t) return;
357
405
  setBusy("thread:" + id);
358
- fbApi("/feedback/" + id, accountRow ? {} : { token: t.token }).then(function (b) {
406
+ fbApi("/feedback/" + id, { token: t.token }).then(function (b) {
359
407
  setThreadData(b.feedback);
360
408
  }).catch(function (e) {
361
- if (e && e.status === 401) setMsg("err", "登录已过期,请退出后重新登录后再查看该反馈");
362
- else setMsg("err", "加载反馈详情失败:" + (e && e.message));
409
+ setMsg("err", "加载反馈详情失败:" + e.message);
363
410
  }).finally(function () { setBusy(""); });
364
411
  }
365
412
 
366
- /** 登录态拉取账号历史(/feedback/mine,翻页参数可用;节点半在登录态自动附 JWT)。 */
367
- function loadMine() {
368
- if (!fbAccount) return;
369
- setMineBusy(true); setMineErr(false);
370
- fbApi("/feedback/mine?page=1&page_size=20").then(function (b) {
371
- setMineList((b && b.items) || []);
372
- }).catch(function (e) {
373
- setMineErr(true);
374
- var eb = e && e.body && e.body.error;
375
- setMsg("err", e && e.status === 401
376
- ? "账号登录已过期,请退出后重新登录,再查看账号全部历史"
377
- : "账号历史拉取失败:" + ((eb && eb.message) || (e && e.message)) + "(本机记录仍可查看)");
378
- }).finally(function () { setMineBusy(false); });
379
- }
380
-
381
- /** 合并展示列表:账号历史(服务端 mine,新→旧)在前;本机 thread_token 行去重后保留
382
- * (匿名/本机新增但服务端未聚合的行仍可见,打开继续走本地凭据)。 */
383
- function fbRows() {
384
- var out = [];
385
- var seen = {};
386
- (mineList || []).forEach(function (it) {
387
- seen[it.id] = true;
388
- out.push({
389
- id: it.id, acct: true,
390
- status: it.status || "", category: it.category || "", title: it.title || "",
391
- created_at: it.created_at || 0, reply_count: it.reply_count || 0,
392
- });
393
- });
394
- threads.slice().reverse().forEach(function (t) {
395
- if (seen[t.id]) return;
396
- seen[t.id] = true;
397
- out.push({ id: t.id, acct: false, status: "", category: "", title: "", created_at: 0, reply_count: 0 });
398
- });
399
- return out;
400
- }
401
-
402
413
  useEffect(function () { loadCfg(); }, [loadCfg]);
403
- // 登录态打开「我的反馈」→ 拉账号历史;仅在未加载且非错误态自动触发(失败后点 tab 重试,不无限重试)
404
- useEffect(function () {
405
- if (tab === "mine" && fbAccount && mineList === null && !mineErr && !mineBusy) loadMine();
406
- // eslint-disable-next-line react-hooks/exhaustive-deps
407
- }, [tab, fbAccount, mineList === null, mineErr, mineBusy]);
408
414
 
409
415
  var doSubmit = function () {
410
416
  if (!fbContent.trim()) { setMsg("err", "请填写反馈内容"); return; }
@@ -421,7 +427,6 @@ window.__ModuleLoader__.load({
421
427
  .then(function (b) {
422
428
  fbRememberThread(b.feedback.id, b.thread_token);
423
429
  setThreads(fbLoadThreads());
424
- if (fbAccount) setMineList(null); // 账号态提交成功 → 回「我的反馈」时重新拉取(含本条)
425
430
  setFbTitle(""); setFbContent(""); setContact("");
426
431
  setMsg("ok", "✅ 反馈已提交,可在「我的反馈」查看回复");
427
432
  setTab("mine");
@@ -439,15 +444,9 @@ window.__ModuleLoader__.load({
439
444
  var text = input ? input.value.trim() : "";
440
445
  if (!text) { setMsg("err", "请填写回复内容"); return; }
441
446
  var t = threads.filter(function (x) { return x.id === id; })[0];
442
- var accountRow = fbRowIsAccount(id);
443
- // 账号历史行:服务端回复(节点半自动附 JWT,无需本地 thread_token);本地/匿名行:带 thread_token 回复
444
- var replyOpts = { method: "POST", body: JSON.stringify({ content: text }) };
445
- if (!accountRow) {
446
- if (!t) return;
447
- replyOpts.token = t.token;
448
- }
447
+ if (!t) return;
449
448
  setBusy("reply:" + id);
450
- fbApi("/feedback/" + id + "/replies", replyOpts)
449
+ fbApi("/feedback/" + id + "/replies", { method: "POST", body: JSON.stringify({ content: text }), token: t.token })
451
450
  .then(function () {
452
451
  if (input) input.value = "";
453
452
  loadThread(id);
@@ -455,8 +454,7 @@ window.__ModuleLoader__.load({
455
454
  })
456
455
  .catch(function (e) {
457
456
  var errBody = e.body && e.body.error;
458
- if (e && e.status === 401) setMsg("err", "登录已过期,请重新登录后再回复");
459
- else if (errBody && errBody.code === "rate_limited") setMsg("err", "今天回复次数已达上限,请明天再试");
457
+ if (errBody && errBody.code === "rate_limited") setMsg("err", "今天回复次数已达上限,请明天再试");
460
458
  else setMsg("err", "回复失败:" + (errBody && errBody.message ? errBody.message : e.message));
461
459
  })
462
460
  .finally(function () { setBusy(""); });
@@ -468,83 +466,11 @@ window.__ModuleLoader__.load({
468
466
  if (id !== openId) loadThread(id);
469
467
  };
470
468
 
471
- /** 「我的反馈」tab 内容:账号历史行直接用服务端字段渲染(状态/类别/标题/时间/回复数),
472
- * 并保留本机未同步的 thread_token 行;单条展开/回复由 loadThread/doReply 分流。 */
473
- function renderMine() {
474
- var rows = fbRows();
475
- if (rows.length === 0) {
476
- var emptyText = fbAccount
477
- ? (mineBusy ? "正在加载账号历史…" : (mineErr ? "账号历史加载失败,点上方「我的反馈」tab 重试" : "暂无反馈,提交第一条?"))
478
- : "还没有提交过反馈。" + (cfg && cfg.reachable === false ? "(反馈服务未连接)" : "登录手机号账号后,可在任意设备查看账号全部历史。");
479
- return h("div", null,
480
- h("div", { className: "dru-fb-empty" }, emptyText),
481
- fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
482
- );
483
- }
484
- return h("div", null,
485
- rows.map(function (r) {
486
- var open = openId === r.id;
487
- var td = open ? threadData : null;
488
- var st = td ? td.status : r.status;
489
- var cat = td ? td.category : r.category;
490
- var titleTxt = td ? (td.title || "") : r.title;
491
- var ts = td ? td.created_at : r.created_at;
492
- var catLbl = (function () {
493
- var c = FB_CATEGORIES.filter(function (x) { return x.value === cat; })[0];
494
- return c ? c.label : (cat || "反馈");
495
- })();
496
- return h("div", { key: r.id, className: "dru-fb-item" },
497
- h("div", { className: "dru-fb-item-head" },
498
- fbStatusBadge(st || "open"),
499
- h("span", { className: "dru-fb-cat" }, catLbl),
500
- h("span", { className: "dru-fb-item-title" }, titleTxt || ("反馈 #" + r.id.slice(-6))),
501
- h("span", { className: "dru-fb-item-time" },
502
- (ts ? new Date(ts).toLocaleString() : "") +
503
- (r.acct && !open && r.reply_count > 0 ? " · " + r.reply_count + " 条回复" : ""))
504
- ),
505
- open && td
506
- ? h("div", null,
507
- h("div", { className: "dru-fb-item-content" }, td.content),
508
- td.replies.map(function (reply) {
509
- return h("div", { key: reply.id, className: "dru-fb-reply" },
510
- h("div", { className: "dru-fb-reply-row" },
511
- h("span", { className: "dru-fb-reply-who" + (reply.author === "user" ? " user" : "") }, reply.author === "admin" ? "管理员回复" : "我"),
512
- h("span", { className: "dru-fb-reply-text" }, reply.content)
513
- ),
514
- h("div", { className: "dru-fb-item-time" }, new Date(reply.created_at).toLocaleString())
515
- );
516
- }),
517
- h("div", { className: "dru-fb-reply" },
518
- h("textarea", { id: "dru-fb-reply-" + r.id, className: "dru-fb-reply-input", placeholder: "回复管理员…", maxLength: 2000 }),
519
- h("div", { style: { textAlign: "right", marginTop: 6 } },
520
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { doReply(r.id); } }, busy === "reply:" + r.id ? "发送中…" : "回复")
521
- )
522
- ),
523
- h("div", { className: "dru-fb-item-time", style: { textAlign: "right", marginTop: 8 } },
524
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { loadThread(r.id); } }, "↻ 刷新"),
525
- " ",
526
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, onClick: function () { setOpenId(""); setThreadData(null); } }, "收起")
527
- )
528
- )
529
- : h("div", { className: "dru-fb-item-time", style: { textAlign: "right" } },
530
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { openThread(r.id); } }, open ? "收起" : "查看 / 回复"))
531
- );
532
- }),
533
- mineBusy ? h("div", { className: "dru-hint", style: { marginTop: 6 } }, "账号历史同步中…") : null,
534
- fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
535
- );
536
- }
537
-
538
- var mineRows = fbRows(); // 「我的反馈」展示行数:账号历史 + 本机未同步线程(合并去重后)
539
469
  return h("div", { className: "dru-card" },
540
470
  h("h3", null, "💬 用户反馈"),
541
471
  h("div", { className: "dru-fb-tabs" },
542
472
  h("div", { className: "dru-fb-tab" + (tab === "submit" ? " active" : ""), onClick: function () { setTab("submit"); setFbMsg(null); } }, "提交反馈"),
543
- h("div", { className: "dru-fb-tab" + (tab === "mine" ? " active" : ""), onClick: function () {
544
- setTab("mine"); setFbMsg(null); setThreads(fbLoadThreads());
545
- // 登录态(账号已配)打开「我的反馈」→ 拉取账号历史(合并展示,换设备/重装也能看到)
546
- if (fbAccount && mineList === null) loadMine();
547
- } }, "我的反馈" + (mineRows.length ? "(" + mineRows.length + ")" : ""))
473
+ h("div", { className: "dru-fb-tab" + (tab === "mine" ? " active" : ""), onClick: function () { setTab("mine"); setFbMsg(null); setThreads(fbLoadThreads()); } }, "我的反馈" + (threads.length ? "(" + threads.length + ")" : ""))
548
474
  ),
549
475
  tab === "submit"
550
476
  ? h("div", null,
@@ -572,7 +498,52 @@ window.__ModuleLoader__.load({
572
498
  h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doSubmit }, busy === "submit" ? "提交中…" : "提交反馈"),
573
499
  fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
574
500
  )
575
- : renderMine()
501
+ : h("div", null,
502
+ threads.length === 0
503
+ ? h("div", { className: "dru-fb-empty" }, "还没有提交过反馈。" + (cfg && cfg.reachable === false ? "(反馈服务未连接)" : ""))
504
+ : threads.slice().reverse().map(function (t) {
505
+ var open = openId === t.id;
506
+ var td = open ? threadData : null;
507
+ return h("div", { key: t.id, className: "dru-fb-item" },
508
+ h("div", { className: "dru-fb-item-head" },
509
+ fbStatusBadge(td ? td.status : "open"),
510
+ h("span", { className: "dru-fb-cat" }, (function () {
511
+ var c = FB_CATEGORIES.filter(function (x) { return x.value === (td ? td.category : ""); })[0];
512
+ return c ? c.label : (td ? td.category : "反馈");
513
+ })()),
514
+ h("span", { className: "dru-fb-item-title" }, td ? (td.title || "反馈") : "反馈 #" + t.id.slice(-6)),
515
+ h("span", { className: "dru-fb-item-time" }, td ? new Date(td.created_at).toLocaleString() : "")
516
+ ),
517
+ open && td
518
+ ? h("div", null,
519
+ h("div", { className: "dru-fb-item-content" }, td.content),
520
+ td.replies.map(function (r) {
521
+ return h("div", { key: r.id, className: "dru-fb-reply" },
522
+ h("div", { className: "dru-fb-reply-row" },
523
+ h("span", { className: "dru-fb-reply-who" + (r.author === "user" ? " user" : "") }, r.author === "admin" ? "管理员回复" : "我"),
524
+ h("span", { className: "dru-fb-reply-text" }, r.content)
525
+ ),
526
+ h("div", { className: "dru-fb-item-time" }, new Date(r.created_at).toLocaleString())
527
+ );
528
+ }),
529
+ h("div", { className: "dru-fb-reply" },
530
+ h("textarea", { id: "dru-fb-reply-" + t.id, className: "dru-fb-reply-input", placeholder: "回复管理员…", maxLength: 2000 }),
531
+ h("div", { style: { textAlign: "right", marginTop: 6 } },
532
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { doReply(t.id); } }, busy === "reply:" + t.id ? "发送中…" : "回复")
533
+ )
534
+ ),
535
+ h("div", { className: "dru-fb-item-time", style: { textAlign: "right", marginTop: 8 } },
536
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { loadThread(t.id); } }, "↻ 刷新"),
537
+ " ",
538
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, onClick: function () { setOpenId(""); setThreadData(null); } }, "收起")
539
+ )
540
+ )
541
+ : h("div", { className: "dru-fb-item-time", style: { textAlign: "right" } },
542
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { padding: "2px 10px", fontSize: 12 }, disabled: busy !== "", onClick: function () { openThread(t.id); } }, open ? "收起" : "查看 / 回复"))
543
+ );
544
+ }),
545
+ fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
546
+ )
576
547
  );
577
548
  }
578
549
 
@@ -900,30 +871,10 @@ window.__ModuleLoader__.load({
900
871
  selfMsg ? h("div", { className: "dru-msg dru-msg-" + selfMsg.kind }, selfMsg.text) : null,
901
872
  h("div", { className: "dru-hint", style: { marginTop: 8 } },
902
873
  armed ? "⚠ 再次点击后即开始彻底卸载:① 移除 dsh web 配置中的插件引用与本地文件;② 停止并移除 bridge 自启动服务(macOS com.dshremote.bridge / Linux dsh-bridge)并结束残留进程;③ 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销,如需再次使用请在插件市场重新安装。" :
903
- "检测新版、一键在线更新、彻底卸载都在本卡片完成。")
874
+ "插件市场没有更新/卸载按钮(dsh 官方市场暂不提供),本卡片即官方管理入口:检测新版、一键在线更新、彻底卸载都在这里完成。")
904
875
  );
905
876
  }
906
877
 
907
- // ── 端到端加密(E2EE,Phase-5):bridge 状态 → 可读文案映射 ─────────────
908
- // bridge 启停时把开关结果写入 <relayDir>/.e2ee-state.json({enabled,reason,profile,epoch,caps},
909
- // 见 clients/dsh-remote/e2ee-client.mjs),node 半随 /dsh-remote/status 以 service.e2ee 下发;
910
- // 此处只做“可读文案”映射(协议 docs/e2ee-protocol.md §2.3/§7.3)。
911
- var E2EE_DISABLED_COPY = {
912
- server_disabled: "端到端加密暂不可用(当前为普通安全连接 HTTPS)",
913
- params_unreachable: "当前为普通安全连接(HTTPS)",
914
- disabled_by_config: "当前为普通安全连接(HTTPS)",
915
- derive_failed: "账号密码已变更,需在「🔑 账号」重新登录后恢复端到端加密(当前为普通安全连接(HTTPS))",
916
- };
917
- var E2EE_DISABLED_FALLBACK = "当前为普通安全连接(HTTPS)";
918
- /** 把 service.e2ee 归一化为可读状态行;e2ee 缺失/旧 host 未下发 → null(不打扰)。 */
919
- function describeE2ee(e2ee) {
920
- if (!e2ee || typeof e2ee !== "object") return null;
921
- if (e2ee.enabled === true) return { kind: "ok", text: "🔒 端到端加密已启用(手机解锁后生效)" };
922
- var reason = typeof e2ee.reason === "string" && e2ee.reason ? e2ee.reason : "";
923
- var text = E2EE_DISABLED_COPY[reason] || E2EE_DISABLED_FALLBACK; // reason 未知 → 兜底
924
- return { kind: "off", text: text };
925
- }
926
-
927
878
  // ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
928
879
  function RemoteControlSection(props) {
929
880
 
@@ -971,17 +922,7 @@ window.__ModuleLoader__.load({
971
922
  var devOpenArr = useState(false); var devOpen = devOpenArr[0]; var setDevOpen = devOpenArr[1];
972
923
  var devBusyArr = useState(""); var devBusy = devBusyArr[0]; var setDevBusy = devBusyArr[1];
973
924
  var devMsgArr = useState(null); var devMsg = devMsgArr[0]; var setDevMsg = devMsgArr[1];
974
- var armedDevArr = useState(null); var armedDev = armedDevArr[0]; var setArmedDev = armedDevArr[1]; // 待二次确认的 session id(取消配对)
975
- var armedDelArr = useState(null); var armedDel = armedDelArr[0]; var setArmedDel = armedDelArr[1]; // 待二次确认的 session id(删除记录)
976
- var purgeArmedArr = useState(false); var purgeArmed = purgeArmedArr[0]; var setPurgeArmed = purgeArmedArr[1]; // 清理已解绑二次确认
977
- // ── 🔒 修改密码(已登录 SaaS,短信重置)状态:放在全部既有字段之后,保持既有 hook 序号 ──
978
- // 成功后企业端使该账号全部授权设备/会话失效(含 E2EE 派生口令)→ 本地登出、用新密码重新登录。
979
- var pwdOpenArr = useState(false); var pwdOpen = pwdOpenArr[0]; var setPwdOpen = pwdOpenArr[1]; // 小表单展开
980
- var pwdCapArr = useState(null); var pwdCap = pwdCapArr[0]; var setPwdCap = pwdCapArr[1]; // {id,svg} 图形验证码
981
- var pwdCapTxtArr = useState(""); var pwdCapTxt = pwdCapTxtArr[0]; var setPwdCapTxt = pwdCapTxtArr[1];
982
- var pwdSmsArr = useState(""); var pwdSms = pwdSmsArr[0]; var setPwdSms = pwdSmsArr[1];
983
- var pwdSmsBtnArr = useState("获取验证码"); var pwdSmsBtn = pwdSmsBtnArr[0]; var setPwdSmsBtn = pwdSmsBtnArr[1];
984
- var pwdNewArr = useState(""); var pwdNew = pwdNewArr[0]; var setPwdNew = pwdNewArr[1]; // 新密码(≥8)
925
+ var armedDevArr = useState(null); var armedDev = armedDevArr[0]; var setArmedDev = armedDevArr[1]; // 待二次确认的 session id
985
926
 
986
927
  var refresh = useCallback(function () {
987
928
  setBusy("status");
@@ -1021,9 +962,8 @@ window.__ModuleLoader__.load({
1021
962
 
1022
963
  useEffect(function () { refresh(); }, [refresh]);
1023
964
  useEffect(function () {
1024
- // 展开「忘记密码」重置表单时不用预载登录表单验证码(收起的 reset 表单用独立 pwdCap)
1025
- if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !pwdOpen && !lcap) loadCaptcha("login");
1026
- }, [st, loggedIn, mode, authTab, pwdOpen, lcap]);
965
+ if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !lcap) loadCaptcha("login");
966
+ }, [st, loggedIn, mode, authTab, lcap]);
1027
967
 
1028
968
  // ── 📱 远程访问卡:常量 / 轮询 / 一次性访问密钥 / 已授权设备 ─────────────────
1029
969
  var KEY_AUTO_REFRESH_MS = 25000; // 停留栏目时约每 25s 自动轮换一把新的一次性密钥(防已用/过期)
@@ -1126,86 +1066,39 @@ window.__ModuleLoader__.load({
1126
1066
  setDevMsg({ kind: "err", text: "加载已授权设备失败:" + e.message });
1127
1067
  }).finally(function () { setDevBusy(""); });
1128
1068
  }
1129
- /** 操作(取消配对/删除记录/清理已解绑)成功后静默重拉列表,覆盖行内状态。 */
1130
- function refreshDeviceList() {
1131
- api("/dsh-remote/mobile-sessions").then(function (b) {
1132
- if (b && b.ok) setDevSessions(Array.isArray(b.sessions) ? b.sessions : []);
1133
- }).catch(function () {});
1134
- }
1135
1069
 
1136
1070
  var toggleDevices = function () {
1137
1071
  var next = !devOpen;
1138
1072
  setDevOpen(next);
1139
1073
  if (next && devSessions === null && devBusy === "") loadDevices();
1140
- if (!next) { setArmedDev(null); setArmedDel(null); setPurgeArmed(false); }
1074
+ if (!next) setArmedDev(null);
1141
1075
  };
1142
1076
 
1143
1077
  /** 取消配对:先点一次进入确认态,再点一次才 POST revoke(同 SelfManageCard 二次确认风格)。 */
1144
1078
  var doRevokeDevice = function (id) {
1145
1079
  if (!id) return;
1146
- if (armedDev !== id) { setArmedDev(id); setArmedDel(null); setPurgeArmed(false); return; }
1080
+ if (armedDev !== id) { setArmedDev(id); return; }
1147
1081
  setDevBusy("revoke:" + id);
1148
1082
  post("/dsh-remote/mobile-sessions/revoke", { id: id }).then(function (b) {
1149
1083
  if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "取消失败");
1150
1084
  setArmedDev(null);
1151
1085
  setDevMsg({ kind: "ok", text: "已取消,对方需重新扫码/登录" });
1152
- refreshDeviceList();
1086
+ // 刷新列表(成功即重拉,行内状态随后由列表覆盖)
1087
+ api("/dsh-remote/mobile-sessions").then(function (lb) {
1088
+ if (lb && lb.ok) setDevSessions(Array.isArray(lb.sessions) ? lb.sessions : []);
1089
+ }).catch(function () {});
1153
1090
  }).catch(function (e) {
1154
1091
  setArmedDev(null);
1155
1092
  setDevMsg({ kind: "err", text: "取消配对失败:" + e.message });
1156
1093
  }).finally(function () { setDevBusy(""); });
1157
1094
  };
1158
1095
 
1159
- /**
1160
- * 删除设备记录:任意行(含已取消/历史)都可用,整行删除并拉黑 jti——
1161
- * DELETE /dsh-remote/mobile-sessions/delete(body {id} → 企业端 DELETE /api/mobile-sessions/:id)。
1162
- * 先点一次进入确认态,再点一次才发请求。
1163
- */
1164
- var doDeleteDevice = function (id) {
1165
- if (!id) return;
1166
- if (armedDel !== id) { setArmedDel(id); setArmedDev(null); setPurgeArmed(false); return; }
1167
- setDevBusy("delete:" + id);
1168
- api("/dsh-remote/mobile-sessions/delete", {
1169
- method: "DELETE",
1170
- headers: { "content-type": "application/json" },
1171
- body: JSON.stringify({ id: id })
1172
- }).then(function (b) {
1173
- if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "删除失败");
1174
- setArmedDel(null);
1175
- setDevMsg({ kind: "ok", text: "已删除该设备的记录" });
1176
- refreshDeviceList();
1177
- }).catch(function (e) {
1178
- setArmedDel(null);
1179
- setDevMsg({ kind: "err", text: "删除记录失败:" + e.message });
1180
- }).finally(function () { setDevBusy(""); });
1181
- };
1182
-
1183
- /**
1184
- * 清理已解绑:删除本人全部 revoked 行——POST /dsh-remote/mobile-sessions/purge(企业端 purge)。
1185
- * 先点一次进入确认态,再点一次才发请求;成功后刷新列表并提示清理条数。
1186
- */
1187
- var doPurgeDevices = function () {
1188
- if (!purgeArmed) { setPurgeArmed(true); setArmedDev(null); setArmedDel(null); return; }
1189
- setDevBusy("purge");
1190
- post("/dsh-remote/mobile-sessions/purge", {}).then(function (b) {
1191
- if (!b || !b.ok) throw new Error((b && (b.error || (b.body && b.body.error))) || "清理失败");
1192
- setPurgeArmed(false);
1193
- var n = b.removed != null ? Number(b.removed) : 0;
1194
- setDevMsg({ kind: "ok", text: n > 0 ? ("已清理 " + n + " 条已解绑记录") : "已清理全部已解绑记录" });
1195
- refreshDeviceList();
1196
- }).catch(function (e) {
1197
- setPurgeArmed(false);
1198
- setDevMsg({ kind: "err", text: "清理失败:" + e.message });
1199
- }).finally(function () { setDevBusy(""); });
1200
- };
1201
-
1202
- /** 升级/续费带登录态打开:取一次性访问 url,改写为 /app/promo?auth=… 后在手机端进入续费页。 */
1096
+ /** 升级/续费带登录态打开:先取一次性访问 url,再 window.open(node 半用账号 JWT 换 key)。 */
1203
1097
  var openUpgradeAuth = function () {
1204
1098
  setBusy("upgrade");
1205
1099
  api("/dsh-remote/access-key").then(function (b) {
1206
1100
  if (!b || !b.ok || !b.url) throw new Error((b && b.error) || "生成访问链接失败");
1207
- var promo = promoUrlOf(b.url);
1208
- try { window.open(promo, "_blank", "noopener"); } catch (e2) {}
1101
+ try { window.open(b.url, "_blank", "noopener"); } catch (e2) {}
1209
1102
  setMsg("ok", "✅ 升级/续费页已在新标签页打开(带登录态)");
1210
1103
  }).catch(function (e) {
1211
1104
  setMsg("err", "打开升级/续费页失败:" + e.message);
@@ -1247,7 +1140,6 @@ window.__ModuleLoader__.load({
1247
1140
  return post("/dsh-remote/config", { phone: phone.trim(), password: pass }).then(function (cfg) {
1248
1141
  // 切换连接账号(手机号与之前不同):清除旧账号留下的反馈线程凭据
1249
1142
  if (prevPhone !== phone.trim()) fbClearThreads();
1250
- setPwdOpen(false); // 登录成功(可能从「忘记密码」返回)→ 收起重置表单
1251
1143
  setSt(cfg); setPass(""); setLcapTxt(""); setLcap(null);
1252
1144
  setMsg("ok", "✅ 登录成功,账号已保存");
1253
1145
  });
@@ -1298,7 +1190,6 @@ window.__ModuleLoader__.load({
1298
1190
  .then(function (body) {
1299
1191
  if (body.ok || body.status === 201 || (body.body && body.body.token)) {
1300
1192
  return post("/dsh-remote/config", { phone: rphone.trim(), password: rpass }).then(function (cfg) {
1301
- setPwdOpen(false);
1302
1193
  setSt(cfg);
1303
1194
  setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRcap(null); setRcapTxt(""); setRInvite("");
1304
1195
  setMsg("ok", "✅ 注册成功,已自动登录");
@@ -1315,118 +1206,15 @@ window.__ModuleLoader__.load({
1315
1206
  setBusy("logout");
1316
1207
  post("/dsh-remote/logout").then(function (body) {
1317
1208
  setSt(body); setAccount(null); setQuota(null);
1318
- // 清空一次性访问密钥/二维码/授权设备等本地状态(退出后不得残留可见)
1319
- setAkey(null); setAkeyMsg(null);
1320
1209
  // 退出登录:清除本机保存的用户反馈线程凭据(thread_token 见 FB_THREADS_KEY),
1321
1210
  // 反馈历史保留在服务端(按账号校验),账号身份变化后本机不再可见旧线程。
1322
1211
  fbClearThreads();
1323
- setPwdOpen(false); // 退出登录后回到登录卡,收起重置/修改密码表单
1324
1212
  if (clearForm) { setPhone(""); setPass(""); setLcapTxt(""); setLcap(null); setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRsmsBtn("获取验证码"); setRcapTxt(""); setRcap(null); }
1325
1213
  setMessage(null);
1326
1214
  }).catch(function (e) { setMsg("err", "退出失败: " + e.message); })
1327
1215
  .finally(function () { setBusy(""); });
1328
1216
  };
1329
1217
 
1330
- // ---------- 🔒 修改密码 / 忘记密码(共用:短信验证码重置;企业端公开 POST /api/password/reset) ----------
1331
- // 图形验证码 + 短信验证码复用既有 /dsh-remote/captcha、/dsh-remote/sms-code 防刷路径;
1332
- // 新密码 ≥8 位。成功后企业端使全部授权设备/会话失效(含 E2EE 派生口令)→ 本地登出,
1333
- // 提示用新密码重新登录(登录成功会把新密码写回本地 config,覆盖旧口令并重建 E2EE 派生)。
1334
- // 两处入口共用同一组字段/函数(pwdOpen/pwdCap/pwdCapTxt/pwdSms/pwdSmsBtn/pwdNew 与
1335
- // togglePwdForm/sendPwdSms/doResetPwd/renderResetPwdForm,避免重复实现):
1336
- // - 账号卡「🔒 修改密码」:fromLogin=false,手机号以当前登录账号为准(只读);
1337
- // - 登录卡「忘记密码?」:fromLogin=true,手机号预填登录输入、可改;成功后请用新密码登录。
1338
- var PWD_RESET_WARN = "修改后所有已授权设备/会话将失效,需重新登录与解锁";
1339
-
1340
- function pwdLoadCaptcha() {
1341
- setPwdCap({ id: null, svg: '<span class="dru-hint">加载中</span>' });
1342
- fetch("/dsh-remote/captcha").then(function (r) {
1343
- return r.text().then(function (text) {
1344
- if (!r.ok) throw new Error("http " + r.status);
1345
- var body = null; try { body = JSON.parse(text); } catch (e) { body = null; }
1346
- if (body && body.captcha_id) setPwdCap({ id: body.captcha_id, svg: body.svg || "" });
1347
- else if (body && body.svg) setPwdCap({ id: null, svg: body.svg });
1348
- else setPwdCap({ id: null, svg: null });
1349
- });
1350
- }).catch(function () { setPwdCap({ id: null, svg: null }); });
1351
- }
1352
- /** 展开/收起重置密码表单(账号卡「修改密码」与登录卡「忘记密码」共用开关)。 */
1353
- var togglePwdForm = function () {
1354
- var next = !pwdOpen;
1355
- setPwdOpen(next);
1356
- setMessage(null);
1357
- if (next && !pwdCap) pwdLoadCaptcha();
1358
- };
1359
- /** 发送短信验证码(图形验证码防刷;captcha_invalid → 重载验证码提示重试)。ph 由调用方给出:
1360
- * 账号卡=当前登录手机号;登录卡「忘记密码」=表单输入手机号。 */
1361
- var sendPwdSms = function (ph) {
1362
- ph = String(ph || "").trim();
1363
- if (!/^1\d{10}$/.test(ph)) { setMsg("err", "请输入正确的手机号"); return; }
1364
- if (!pwdCap || !pwdCap.id || !pwdCapTxt.trim()) { setMsg("err", "请输入图中验证码(点击图片可刷新)"); return; }
1365
- setBusy("pwd-sms");
1366
- post("/dsh-remote/sms-code", { phone: ph, captcha_id: pwdCap.id, captcha_answer: pwdCapTxt.trim() })
1367
- .then(function (body) {
1368
- var testCode = body && (body.test_code || (body.body && body.body.test_code));
1369
- setPwdCap(null); setPwdCapTxt(""); // 验证码一次性:发送后失效
1370
- if (testCode) setMsg("ok", "验证码已发送(测试码: " + testCode + ")");
1371
- else setMsg("ok", "验证码已发送");
1372
- var s = 60; setPwdSmsBtn(s + "s");
1373
- var t = setInterval(function () { s--; if (s <= 0) { clearInterval(t); setPwdSmsBtn("获取验证码"); } else setPwdSmsBtn(s + "s"); }, 1000);
1374
- })
1375
- .catch(function (e) {
1376
- var relayError = e.body && e.body.body && e.body.body.error;
1377
- if (relayError && relayError.code === "captcha_invalid") {
1378
- setPwdCapTxt(""); pwdLoadCaptcha();
1379
- setMsg("err", "发送次数较多,请输入图中验证码后重试");
1380
- } else setMsg("err", "发送失败: " + e.message);
1381
- })
1382
- .finally(function () { setBusy(""); });
1383
- };
1384
- /**
1385
- * 确认重置密码:POST /dsh-remote/password/reset → 成功后本地登出,提示用新密码登录。
1386
- * @param {string} ph - 手机号(账号卡=当前登录账号;登录卡「忘记密码」=表单输入)。
1387
- * @param {boolean} fromLogin - true=登录卡「忘记密码」;false=账号卡「修改密码」。
1388
- */
1389
- var doResetPwd = function (ph, fromLogin) {
1390
- ph = String(ph || "").trim();
1391
- if (!/^1\d{10}$/.test(ph)) { setMsg("err", fromLogin ? "请输入正确的手机号" : "请先登录手机号账号(修改密码以当前账号为准)"); return; }
1392
- if (!pwdSms.trim()) { setMsg("err", "请填写短信验证码"); return; }
1393
- if (pwdNew.length < 8) { setMsg("err", "新密码至少 8 位"); return; }
1394
- setBusy("pwd-reset");
1395
- post("/dsh-remote/password/reset", { phone: ph, sms_code: pwdSms.trim(), new_password: pwdNew })
1396
- .then(function (body) {
1397
- var ok = !!(body && (body.ok === true || (body.body && body.body.ok === true)));
1398
- if (!ok) {
1399
- var relayBody = (body && body.body) || body || {};
1400
- var errText = (relayBody.error && (relayBody.error.message || relayBody.error))
1401
- || relayBody.detail || ("修改失败(" + ((body && body.status) || "?") + ")");
1402
- setMsg("err", (fromLogin ? "重置失败:" : "修改失败:") + String(errText));
1403
- setPwdSms(""); // 短信验证码一次性:失败后需重新获取
1404
- return undefined;
1405
- }
1406
- // 成功:全部授权设备/会话已失效(含 E2EE)→ 本地登出清掉 config 旧口令与本地凭据,
1407
- // 提示用新密码重新登录(登录会把新口令写入 config 并重启 bridge / 重建 E2EE)。
1408
- setBusy("pwd-logout");
1409
- return post("/dsh-remote/logout").then(function (lb) {
1410
- setSt(lb); setAccount(null); setQuota(null);
1411
- setAkey(null); setAkeyMsg(null);
1412
- fbClearThreads();
1413
- setPwdOpen(false); setPwdSms(""); setPwdNew(""); setPwdCap(null); setPwdCapTxt(""); setPwdSmsBtn("获取验证码");
1414
- if (fromLogin) {
1415
- // 「忘记密码」成功:本机登出完成 → 回到登录表单提示用新密码登录(手机号保留,便于直接重登)
1416
- setPass(""); setLcapTxt(""); setLcap(null); loadCaptcha("login");
1417
- setMsg("ok", "✅ 密码已重置成功:" + PWD_RESET_WARN + ",本机已退出登录。请用新密码登录。");
1418
- } else {
1419
- setMsg("ok", "✅ 密码已修改成功:所有已授权设备与会话已失效,本机已退出登录。" +
1420
- "请在下方账号卡用「新密码」重新登录——登录会更新本机保存的密码并重新启用端到端加密(E2EE)。");
1421
- }
1422
- }).catch(function (e) {
1423
- setMsg("err", "密码已" + (fromLogin ? "重置" : "修改") + "成功,但本机退出登录失败:" + e.message + "(建议手动「退出登录」后用新密码重新登录,以更新本机配置密码)");
1424
- });
1425
- })
1426
- .catch(function (e) { setMsg("err", (fromLogin ? "重置失败: " : "修改失败: ") + e.message); })
1427
- .finally(function () { setBusy(""); });
1428
- };
1429
-
1430
1218
  var toggleBridge = function (start) {
1431
1219
  setBusy(start ? "start" : "stop");
1432
1220
  post(start ? "/dsh-remote/start" : "/dsh-remote/stop")
@@ -1445,7 +1233,6 @@ window.__ModuleLoader__.load({
1445
1233
  .then(function (body) {
1446
1234
  // 切换到自建服务(连接账号上下文变为本地无账号):清除 SaaS 账号的反馈线程凭据
1447
1235
  fbClearThreads();
1448
- setPwdOpen(false);
1449
1236
  setSt(body); setSelfHost(""); setLocalKey("");
1450
1237
  setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,bridge 已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
1451
1238
  })
@@ -1507,50 +1294,11 @@ window.__ModuleLoader__.load({
1507
1294
  busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
1508
1295
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友赚会员"),
1509
1296
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
1510
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
1511
1297
  h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
1512
1298
  ),
1513
1299
  h("div", { className: "dru-hint", style: { marginTop: 8 } },
1514
1300
  "升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
1515
- (endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : "")),
1516
- pwdOpen ? renderResetPwdForm(false) : null
1517
- );
1518
- }
1519
-
1520
- // ---------- 🔒 修改密码 / 忘记密码 共用重置表单(图形验证码 + 短信验证码 + 新密码≥8) ----------
1521
- // fromLogin=false:账号卡「修改密码」手机号以当前登录账号为准(只读);
1522
- // fromLogin=true:登录卡「忘记密码」手机号预填登录输入、可改(成功后返回登录表单请用新密码登录)。
1523
- function renderResetPwdForm(fromLogin) {
1524
- var ph = fromLogin ? phone : ((st && st.config && st.config.phone) || "");
1525
- return h("div", { className: "dru-card", style: { marginTop: 10, border: "1px dashed #d0d7de" } },
1526
- h("h3", null, fromLogin ? "忘记密码(短信重置)" : "🔒 修改密码(短信验证)"),
1527
- h("div", { className: "dru-hint", style: { marginBottom: 8 } },
1528
- fromLogin
1529
- ? "手机号预填当前输入,可改为其它账号;" + PWD_RESET_WARN + "。"
1530
- : PWD_RESET_WARN + "(E2EE 用新密码重新派生)。"),
1531
- field("手机号", fromLogin
1532
- ? input({ type: "tel", value: ph, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })
1533
- : h("input", { className: "dru-input", type: "tel", value: ph, disabled: true, readOnly: true, title: "以当前登录账号为准" })),
1534
- field("图形验证码", h("div", { className: "dru-captcha" },
1535
- input({ value: pwdCapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setPwdCapTxt(e.target.value); } }),
1536
- h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { pwdLoadCaptcha(); }, dangerouslySetInnerHTML: pwdCap && pwdCap.svg && pwdCap.svg.indexOf("<svg") === 0 ? { __html: pwdCap.svg } : void 0 },
1537
- pwdCap && pwdCap.svg && pwdCap.svg.indexOf("<svg") !== 0 ? pwdCap.svg : null)
1538
- )),
1539
- field("短信验证码", h("div", { className: "dru-captcha" },
1540
- input({ value: pwdSms, placeholder: "6 位验证码", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setPwdSms(e.target.value); } }),
1541
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: function () { sendPwdSms(ph); } }, pwdSmsBtn)
1542
- )),
1543
- field("新密码(至少 8 位)", input({ type: "password", value: pwdNew, minLength: 8, autoComplete: "new-password", placeholder: "至少 8 位", onChange: function (e) { setPwdNew(e.target.value); } })),
1544
- h("div", { className: "dru-hint", style: { marginBottom: 8 } },
1545
- fromLogin
1546
- ? "重置成功后本机会退出登录(如有已保存的旧账号),请用新密码登录。"
1547
- : "修改成功后,本机保存的旧密码会被清除并退出登录;请用新密码重新登录(登录会更新本机配置密码并重新启用远程访问与端到端加密)。"),
1548
- h("button", { type: "button", className: "dru-btn dru-btn-danger", style: { width: "100%" }, disabled: busy !== "", onClick: function () { doResetPwd(ph, fromLogin); } },
1549
- busy === "pwd-reset" ? "提交中…" : busy === "pwd-logout" ? "已重置,正在退出本地登录…" : (fromLogin ? "确认重置密码" : "确认修改密码")),
1550
- fromLogin
1551
- ? h("div", { style: { textAlign: "right", marginTop: 6 } },
1552
- h("button", { type: "button", className: "dru-linkbtn", onClick: function () { togglePwdForm(); } }, "← 返回登录"))
1553
- : null
1301
+ (endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : ""))
1554
1302
  );
1555
1303
  }
1556
1304
 
@@ -1617,23 +1365,11 @@ window.__ModuleLoader__.load({
1617
1365
  ? "请先登录(下方账号卡片)后启用远程访问"
1618
1366
  : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
1619
1367
  var dotCls = "dru-dot " + (loggedInSaaS && serviceRunning ? "dru-dot-on" : "dru-dot-off");
1620
- // Phase-5:端到端加密(E2EE)状态行 —— 未登录/旧 host 未下发 e2ee 一律不渲染
1621
- // (桌面宽屏与手机镜像共用同一面板:纯文字状态行、不弹层不打扰);启用=绿点绿字(🔒已启用),
1622
- // 未启用=灰字 + 中性原因文案(回退普通 HTTPS 连接,不宣称“灰度等待”)。
1623
- function renderE2eeBadge() {
1624
- if (!loggedInSaaS) return null;
1625
- var e = describeE2ee(st && st.service && st.service.e2ee);
1626
- if (!e) return null;
1627
- return h("div", { className: "dru-e2ee-line" + (e.kind === "ok" ? " ok" : "") },
1628
- h("span", { className: "dru-dot " + (e.kind === "ok" ? "dru-dot-on" : "dru-dot-off") }),
1629
- h("span", null, e.text));
1630
- }
1631
1368
  return card("📱 远程访问", [
1632
1369
  h("div", { className: "dru-status-line" },
1633
1370
  h("span", { className: dotCls }),
1634
1371
  h("span", null, statusTxt)
1635
1372
  ),
1636
- renderE2eeBadge(),
1637
1373
  akeyMsg ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } }, akeyMsg.text) : null,
1638
1374
  hasKey ? h("div", null, [
1639
1375
  h("div", { className: "dru-url big", style: { marginTop: 8 } },
@@ -1650,15 +1386,15 @@ window.__ModuleLoader__.load({
1650
1386
  ),
1651
1387
  h("div", { className: "dru-access-col" },
1652
1388
  h("div", { className: "dru-key-note" },
1653
- "扫码即进入,30 分钟有效、用一次即失效。"),
1389
+ "扫码即进入远程访问;每次生成的链接 30 分钟有效、访问一次后失效,停留栏目期间会自动更新。"),
1654
1390
  h("div", { className: "dru-actions", style: { marginTop: 2 } },
1655
1391
  h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: akeyBusy, onClick: openKeyUrl }, "直接打开"),
1656
1392
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: akeyBusy, onClick: loadAccessKey }, akeyBusy ? "生成中…" : "刷新二维码/访问链接")
1657
1393
  ),
1658
1394
  h("div", { className: "dru-hint", style: { marginTop: 4 } },
1659
1395
  !serviceRunning
1660
- ? "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。"
1661
- : "打开链接/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。")
1396
+ ? "本机 bridge 未运行:请先在下方「🖥 Bridge 服务」卡片启动,手机/另一台电脑才能连入本机。"
1397
+ : "手机上打开链接点「进入」即可像在本机一样使用 dsh web。")
1662
1398
  )
1663
1399
  )
1664
1400
  ]) : h("div", null, [
@@ -1674,7 +1410,7 @@ window.__ModuleLoader__.load({
1674
1410
  ]);
1675
1411
  }
1676
1412
 
1677
- // ---------- 📲 已授权设备卡(展开列表 + 行内 取消配对/删除记录 + 底部 清理已解绑) ----------
1413
+ // ---------- 📲 已授权设备卡(展开列表 + 二次确认取消配对) ----------
1678
1414
  function deviceLabel(s) {
1679
1415
  if (s && s.label) return String(s.label);
1680
1416
  var parts = [];
@@ -1695,12 +1431,10 @@ window.__ModuleLoader__.load({
1695
1431
  if (devSessions.length === 0) {
1696
1432
  return h("div", { className: "dru-fb-empty" }, "暂无已授权设备(手机扫码后出现)");
1697
1433
  }
1698
- var revokedCount = devSessions.filter(function (s) { return !!(s && s.revoked_at); }).length;
1699
1434
  return h("div", null, [
1700
1435
  devSessions.map(function (s) {
1701
- var id = s && s.id;
1702
1436
  var revoked = !!(s && s.revoked_at);
1703
- return h("div", { key: id, className: "dru-dev" },
1437
+ return h("div", { key: s && s.id, className: "dru-dev" },
1704
1438
  h("div", { className: "dru-dev-top" },
1705
1439
  h("span", { className: "dru-dev-name" }, deviceLabel(s)),
1706
1440
  deviceMeta(s) ? h("span", { className: "dru-dev-meta" }, deviceMeta(s)) : null,
@@ -1711,34 +1445,18 @@ window.__ModuleLoader__.load({
1711
1445
  (s && s.last_seen_at ? " · 最近活跃 " + fmtDT(s.last_seen_at) : "") +
1712
1446
  (revoked ? " · 取消于 " + fmtDT(s.revoked_at) : "")
1713
1447
  ),
1714
- h("div", { className: "dru-actions", style: { marginTop: 8 } },
1715
- revoked ? null : h("button", {
1716
- type: "button",
1717
- className: "dru-btn dru-btn-danger",
1718
- disabled: devBusy !== "",
1719
- onClick: function () { doRevokeDevice(id); }
1720
- }, devBusy === "revoke:" + id ? "取消中…" : armedDev === id ? "⚠ 再点一次确认取消配对" : "取消配对"),
1448
+ revoked ? null : h("div", { className: "dru-actions", style: { marginTop: 8 } },
1721
1449
  h("button", {
1722
1450
  type: "button",
1723
- className: "dru-btn dru-btn-ghost",
1724
- style: { color: "#cf222e", borderColor: "#cf222e" },
1451
+ className: "dru-btn dru-btn-danger",
1725
1452
  disabled: devBusy !== "",
1726
- onClick: function () { doDeleteDevice(id); }
1727
- }, devBusy === "delete:" + id ? "删除中…" : armedDel === id ? "⚠ 再点一次确认删除记录" : "删除记录")
1453
+ onClick: function () { doRevokeDevice(s && s.id); }
1454
+ }, devBusy === "revoke:" + (s && s.id) ? "取消中…" : armedDev === (s && s.id) ? "⚠ 再点一次确认取消配对" : "取消配对")
1728
1455
  )
1729
1456
  );
1730
1457
  }),
1731
1458
  h("div", { className: "dru-hint", style: { marginTop: 4 } },
1732
- "取消配对后,对方需重新扫码/登录才能再次远程访问本机。"),
1733
- h("div", { className: "dru-actions", style: { marginTop: 8, borderTop: "1px dashed #eaeef2", paddingTop: 8 } },
1734
- h("button", {
1735
- type: "button",
1736
- className: "dru-btn dru-btn-ghost",
1737
- disabled: devBusy !== "" || revokedCount === 0,
1738
- onClick: doPurgeDevices,
1739
- title: revokedCount > 0 ? ("清理 " + revokedCount + " 条已解绑记录") : "没有已解绑记录"
1740
- }, devBusy === "purge" ? "清理中…" : purgeArmed ? "⚠ 再点一次确认清理已解绑" : "清理已解绑" + (revokedCount > 0 ? "(" + revokedCount + ")" : ""))
1741
- )
1459
+ "取消配对后,对方需重新扫码/登录才能再次远程访问本机。")
1742
1460
  ]);
1743
1461
  }
1744
1462
  function renderDevicesCard() {
@@ -1782,26 +1500,19 @@ window.__ModuleLoader__.load({
1782
1500
  ? renderAccount()
1783
1501
  : h("div", null,
1784
1502
  h("div", { className: "dru-tabs" },
1785
- h("div", { className: "dru-tab" + (authTab === "login" ? " active" : ""), onClick: function () { setAuthTab("login"); setPwdOpen(false); setMessage(null); } }, "登录"),
1786
- h("div", { className: "dru-tab" + (authTab === "register" ? " active" : ""), onClick: function () { setAuthTab("register"); setPwdOpen(false); setMessage(null); } }, "注册")
1503
+ h("div", { className: "dru-tab" + (authTab === "login" ? " active" : ""), onClick: function () { setAuthTab("login"); setMessage(null); } }, "登录"),
1504
+ h("div", { className: "dru-tab" + (authTab === "register" ? " active" : ""), onClick: function () { setAuthTab("register"); setMessage(null); } }, "注册")
1787
1505
  ),
1788
1506
  authTab === "login"
1789
- ? (pwdOpen
1790
- // 「忘记密码」展开:与账号卡「修改密码」共用同一重置表单(手机号可编辑、预填登录输入)
1791
- ? renderResetPwdForm(true)
1792
- : h("div", null,
1793
- field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
1794
- field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
1795
- field("验证码", h("div", { className: "dru-captcha" },
1796
- input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
1797
- 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 },
1798
- lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
1799
- )),
1800
- h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "login" ? "登录中…" : "登录"),
1801
- // 忘记密码:小字入口,点击展开短信验证码重置表单(不抢占任何官方按钮)
1802
- h("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 } },
1803
- h("button", { type: "button", className: "dru-linkbtn", disabled: busy !== "", onClick: togglePwdForm }, "忘记密码?"))
1804
- )
1507
+ ? h("div", null,
1508
+ field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
1509
+ field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
1510
+ field("验证码", h("div", { className: "dru-captcha" },
1511
+ input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
1512
+ 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 },
1513
+ lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
1514
+ )),
1515
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "login" ? "登录中…" : "登录")
1805
1516
  )
1806
1517
  : h("div", null,
1807
1518
  field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
@@ -1869,7 +1580,7 @@ window.__ModuleLoader__.load({
1869
1580
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
1870
1581
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
1871
1582
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
1872
- h("div", { className: "dru-hint" }, "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。")
1583
+ h("div", { className: "dru-hint" }, "🛡 端到端流量保护:可选对通道做端到端加密保护,传输全程不暴露本机公网 IP(详见项目 README「安全」说明)。")
1873
1584
  ]),
1874
1585
  // 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
1875
1586
  h(SelfManageCard, null),