dsh-remote-web 0.7.10-beta.3 → 0.7.12-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -14,15 +14,15 @@
14
14
  // 旧代码用「文本包含『设置』」去找锚点 → 按钮被塞进设置面板栏目列表最上面(用户反馈的
15
15
  // 「植入错了地方」)。现在找不到锚点就**不注入**,绝不将就一个错误的落点。
16
16
  // - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
17
- // · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册 tabs
17
+ // · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册(在「账号」弹层的分段里)
18
18
  // · 🔒 修改密码(账号卡):已登录账号经 图形验证码+短信验证码 重置(/dsh-remote/password/reset 代理);
19
19
  // 成功后全部已授权设备/会话失效 → 本地登出并提示用新密码重新登录(覆盖本机 config 密码)
20
20
  // · 忘记密码(登录卡):未登录时登录表单内「忘记密码?」→ 同一套
21
21
  // 图形验证码+短信验证码 表单(手机号预填当前输入、可改),成功后本地登出并提示「请用新密码登录」
22
22
  // · 登录要求图形验证码;注册要求两次密码 + 图形验证码
23
23
  // · 📱 远程访问卡:一次性访问密钥(扫码/直接打开/复制 + 到期倒计时自动刷新)与已授权设备管理
24
- // · 🤖 微信机器人通道:与「☁️ 云端服务 / 🖥 自建服务」并列的**第三个 tab**(条形 tab 由 home 与
25
- // wechat 两个视图共用,所以切过去还能切回来);不再是独立的 settings.section 栏目 ——
24
+ // · 🤖 微信机器人通道:「通知与消息通道」弹层里的一个平台入口(2026-11-07 IA 重做;
25
+ // 顶部 3 tab 已删除,云端/自建降级为「手机访问」弹层内的分段;不再是独立的 settings.section 栏目 ——
26
26
  // 用户约定:「不要给它单独弄一个菜单,直接放到面板里面」(0.6.11)
27
27
  // 🔒 登录门(用户约定):微信通道要求**已注册并登录**,未登录不渲染这个 tab;若会话在
28
28
  // tab 开着的时候过期,view 复位回 home(兜底 effect + 分派条件),绝不留空面板。
@@ -30,7 +30,7 @@
30
30
  // 排在「连接微信机器人」按钮之前 —— 它是绑定的理由,又不把按钮挤出首屏。
31
31
  // ✨ 动效引导:未绑定时「连接」按钮做很慢的呼吸(.dru-wx-attn),
32
32
  // prefers-reduced-motion:reduce 下关闭;状态从不靠动效单独表达。
33
- // · bridge 状态与启停开关 + 关于 dsh-remote 说明卡片
33
+ // · bridge 状态与启停开关 + 关于 dsh-remote-web 说明卡片
34
34
  // · 🧰 账号卡「更多」:只收纳(用户关掉邀请引导之后的)「升级 / 带新用户」两个入口 ——
35
35
  // 主界面只剩一个入口,减少营销露出;**档位规格数字(Mbps / GB / 价格)一律不展示**
36
36
  // (0.6.11 用户约定:「插件面板里面不要展示『更多』里的 PRO 版本流量带宽」)
@@ -579,6 +579,122 @@ window.__ModuleLoader__.load({
579
579
  // 气泡独占一行,绝不把按钮挤到屏幕外(320px 下也不横向滚动)
580
580
  " .dru-tip{flex:1 1 100%;padding:9px 40px 9px 10px;font-size:12px}",
581
581
  "}",
582
+ // ══════════════════════════════════════════════════════════════════════
583
+ // 🧭 面板信息架构(2026-11-07 业主 UX 重做):状态头 + 2 列入口网格 + 弹层
584
+ //
585
+ // 为什么重做(业主原话):「整个交互都是卡片式平铺堆叠,很糟糕。别人产品很清爽,
586
+ // 是弹出式设计,或者是有结构的」「插件端全是文字堆砌,太密集,布局垂直往下垒」。
587
+ // 结构:主面板只留 ①状态头 ②入口网格(每卡 ≤3 行)③一行文字入口;
588
+ // 具体操作全部进弹层(role=dialog,宽 480–560,Esc 关闭,焦点陷阱,关闭回焦点)。
589
+ // 视觉纪律(UX 数据库检索后的落地):字号 15/600 标题、13/600 卡片标题、13 正文、12 辅助;
590
+ // 行高 1.5;8 网格(卡片内 16);圆角 12;边框 1px;hover 150ms;尊重 reduced-motion;
591
+ // 图标一律内联 SVG 16px currentColor(**不用 emoji 当图标**);状态不只靠颜色(圆点+文案)。
592
+ // 颜色全部走宿主主题变量(--dsw-alias-*,缺失时回落本插件的 --dru-*),不硬编码深浅色。
593
+ ".dru-ia{display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:1.5}",
594
+ ".dru-ia-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 16px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2,var(--dru-border));background:var(--dsw-alias-bg-layer-2,var(--dru-surface-2))}",
595
+ ".dru-ia-pill{display:inline-flex;align-items:center;gap:6px;min-height:24px;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;white-space:nowrap;color:var(--dsw-alias-label-secondary,var(--dru-fg-muted));background:var(--dru-surface-3);border:1px solid var(--dru-border-soft)}",
596
+ ".dru-ia-pill-dot{width:7px;height:7px;border-radius:50%;background:var(--dru-dot-off);flex:none}",
597
+ ".dru-ia-pill.is-on{color:var(--dru-success);border-color:var(--dru-success-border)}",
598
+ ".dru-ia-pill.is-on .dru-ia-pill-dot{background:var(--dru-success);box-shadow:0 0 6px var(--dru-success-glow)}",
599
+ ".dru-ia-pill.is-warn{color:var(--dru-warn-fg)}",
600
+ ".dru-ia-pill.is-warn .dru-ia-pill-dot{background:var(--dru-warn-border-2)}",
601
+ ".dru-ia-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}",
602
+ "@media (max-width:520px){.dru-ia-grid{grid-template-columns:minmax(0,1fr)}}",
603
+ ".dru-ia-card{display:flex;flex-direction:column;gap:8px;min-width:0;padding:16px;border-radius:12px;border:1px solid var(--dsw-alias-border-l2,var(--dru-border));background:var(--dsw-alias-bg-layer-2,var(--dru-surface-2));transition:border-color .15s ease,box-shadow .15s ease}",
604
+ ".dru-ia-card:hover{border-color:var(--dru-accent);box-shadow:0 2px 10px var(--dru-focus-halo-3)}",
605
+ ".dru-ia-card-top{display:flex;align-items:center;gap:8px;min-width:0}",
606
+ ".dru-ia-ico{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;flex:none;border-radius:8px;background:var(--dru-accent-tint);color:var(--dru-accent-fg)}",
607
+ ".dru-ia-card-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,var(--dru-fg));min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
608
+ ".dru-ia-card-meta{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted));min-height:18px}",
609
+ ".dru-ia-card-actions{margin-top:auto;display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
610
+ ".dru-ia-links{display:flex;flex-wrap:wrap;align-items:center;gap:4px 14px;padding:2px 4px}",
611
+ ".dru-ia-link{display:inline-flex;align-items:center;gap:4px;min-height:32px;padding:0;border:0;background:none;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted));font:inherit;font-size:12px;cursor:pointer;border-radius:6px}",
612
+ ".dru-ia-link:hover{color:var(--dru-accent)}",
613
+ ".dru-ia-link:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
614
+ ".dru-ia-link .dru-reddot{position:static;width:6px;height:6px;box-shadow:none}",
615
+ // ── 弹层(dialog):宽 480–560、表头/底部操作固定、正文超过 1.5 屏才滚动 ──────
616
+ ".dru-ia-scrim{position:fixed;inset:0;z-index:2147482000;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box;background:var(--dru-overlay);animation:dru-ia-fade .15s ease-out}",
617
+ ".dru-ia-dialog{display:flex;flex-direction:column;width:min(520px,calc(100vw - 48px));max-height:min(78vh,640px);box-sizing:border-box;border-radius:12px;border:1px solid var(--dsw-alias-border-l2,var(--dru-border));background:var(--dsw-alias-bg-layer-2,var(--dru-surface-2));color:var(--dsw-alias-label-primary,var(--dru-fg));box-shadow:0 24px 64px var(--dru-shadow);overflow:hidden;font-size:13px;line-height:1.5}",
618
+ ".dru-ia-dialog-head{display:flex;align-items:flex-start;gap:10px;padding:16px;border-bottom:1px solid var(--dru-border-soft);flex:none}",
619
+ ".dru-ia-dialog-title{opacity:1;font-size:15px;font-weight:600;line-height:1.4;min-width:0;flex:1}",
620
+ ".dru-ia-dialog-sub{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted));margin-top:2px;font-weight:400}",
621
+ ".dru-ia-dialog-x{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;border-radius:8px;border:1px solid transparent;background:none;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted));cursor:pointer}",
622
+ ".dru-ia-dialog-x:hover{background:var(--dru-hover);color:var(--dsw-alias-label-primary,var(--dru-fg))}",
623
+ ".dru-ia-dialog-x:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
624
+ ".dru-ia-dialog-body{padding:16px;overflow-y:auto;min-height:0;display:flex;flex-direction:column;gap:12px}",
625
+ ".dru-ia-dialog-foot{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--dru-border-soft);flex:none;background:var(--dsw-alias-bg-layer-2,var(--dru-surface-2))}",
626
+ ".dru-ia-dialog-foot .dru-hint{margin-right:auto}",
627
+ "@keyframes dru-ia-fade{from{opacity:0}to{opacity:1}}",
628
+ // 分段控件(segmented):把原来顶部的 3 个 tab 降级成弹层内的一段
629
+ ".dru-ia-seg{display:flex;gap:2px;padding:3px;border-radius:10px;background:var(--dru-surface-3);border:1px solid var(--dru-border-soft)}",
630
+ ".dru-ia-seg-btn{flex:1;min-height:32px;padding:0 10px;border:0;border-radius:8px;background:none;color:var(--dsw-alias-label-secondary,var(--dru-fg-muted));font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;transition:background .15s ease,color .15s ease}",
631
+ ".dru-ia-seg-btn:hover{color:var(--dsw-alias-label-primary,var(--dru-fg))}",
632
+ ".dru-ia-seg-btn.is-active{background:var(--dsw-alias-bg-layer-3,var(--dru-surface-2));color:var(--dsw-alias-label-primary,var(--dru-fg));box-shadow:0 1px 2px var(--dru-shadow)}",
633
+ ".dru-ia-seg-btn:focus-visible{outline:2px solid var(--dru-focus);outline-offset:1px}",
634
+ // 平台列表(通道弹层第一步)
635
+ ".dru-ia-list{display:flex;flex-direction:column;gap:8px}",
636
+ ".dru-ia-row{display:flex;align-items:center;gap:12px;width:100%;min-height:52px;padding:10px 12px;box-sizing:border-box;border-radius:10px;border:1px solid var(--dsw-alias-border-l2,var(--dru-border));background:var(--dsw-alias-bg-layer-2,var(--dru-surface-2));color:inherit;font:inherit;text-align:left;cursor:pointer;transition:border-color .15s ease,background .15s ease}",
637
+ ".dru-ia-row:hover{border-color:var(--dru-accent);background:var(--dru-hover)}",
638
+ ".dru-ia-row:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
639
+ ".dru-ia-row-main{flex:1;min-width:0}",
640
+ ".dru-ia-row-title{font-size:13px;font-weight:600;line-height:1.4}",
641
+ ".dru-ia-row-sub{font-size:12px;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted));line-height:1.5}",
642
+ ".dru-ia-row-go{display:inline-flex;align-items:center;gap:4px;flex:none;font-size:12px;color:var(--dru-accent)}",
643
+ // 表单(label 可见、错误就近、按钮 ≥32)
644
+ ".dru-ia-field{display:flex;flex-direction:column;gap:6px}",
645
+ ".dru-ia-label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,var(--dru-fg-muted))}",
646
+ ".dru-ia-input{width:100%;min-height:34px;box-sizing:border-box;padding:6px 10px;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dsw-alias-bg-layer-1,var(--dru-surface));color:var(--dsw-alias-label-primary,var(--dru-fg));font:inherit;font-size:13px}",
647
+ ".dru-ia-input:focus{outline:2px solid var(--dru-focus);outline-offset:1px;border-color:var(--dru-accent)}",
648
+ ".dru-ia-input[aria-invalid=\"true\"]{border-color:var(--dru-danger)}",
649
+ ".dru-ia-input-row{display:flex;gap:8px}",
650
+ ".dru-ia-input-row .dru-ia-input{flex:1;min-width:0}",
651
+ ".dru-ia-err{font-size:12px;line-height:1.5;color:var(--dru-danger)}",
652
+ ".dru-ia-hint{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted))}",
653
+ ".dru-ia-state{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:8px;font-size:12px;line-height:1.5;background:var(--dru-surface-3)}",
654
+ ".dru-ia-state.is-ok{color:var(--dru-success);background:var(--dru-success-soft)}",
655
+ ".dru-ia-state.is-err{color:var(--dru-danger);background:var(--dru-danger-soft)}",
656
+ ".dru-ia-state.is-warn{color:var(--dru-warn-fg);background:var(--dru-warn-soft)}",
657
+ ".dru-ia-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:28px 12px;text-align:center;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted))}",
658
+ ".dru-ia-empty-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,var(--dru-fg))}",
659
+ ".dru-ia-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;padding:0 14px;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dsw-alias-label-primary,var(--dru-fg));font:inherit;font-size:13px;font-weight:600;cursor:pointer;transition:background .15s ease,border-color .15s ease}",
660
+ ".dru-ia-btn:hover{border-color:var(--dru-accent);background:var(--dru-hover)}",
661
+ ".dru-ia-btn:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
662
+ ".dru-ia-btn[disabled]{opacity:.55;cursor:not-allowed}",
663
+ ".dru-ia-btn.is-primary{border-color:var(--dru-accent-fill);background:var(--dru-accent-fill);color:var(--dru-on-accent)}",
664
+ ".dru-ia-btn.is-primary:hover{background:var(--dru-accent);border-color:var(--dru-accent)}",
665
+ ".dru-ia-btn.is-ghost{border-color:transparent;background:none;color:var(--dru-accent)}",
666
+ ".dru-ia-btn.is-danger{border-color:var(--dru-danger-border);color:var(--dru-danger);background:none}",
667
+ ".dru-ia-iconbtn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dsw-alias-label-secondary,var(--dru-fg-muted));cursor:pointer}",
668
+ ".dru-ia-iconbtn:hover{color:var(--dru-accent);border-color:var(--dru-accent)}",
669
+ ".dru-ia-iconbtn:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
670
+ // 会员档位条 + 「会员功能」徽章(2026-11-07 桥侧会员判定;只做展示,不做门禁)
671
+ ".dru-ia-tier{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-radius:8px;font-size:12px;line-height:1.5;background:var(--dru-surface-3);color:var(--dsw-alias-label-secondary,var(--dru-fg-muted));border:1px solid var(--dru-border-soft)}",
672
+ ".dru-ia-tier.is-member{color:var(--dru-accent-fg);background:var(--dru-accent-tint);border-color:var(--dru-accent-tint-border)}",
673
+ ".dru-ia-tier-name{font-weight:600;min-width:0}",
674
+ ".dru-ia-tier-link{flex:none;font-size:12px;color:var(--dru-accent);text-decoration:none}",
675
+ ".dru-ia-tier-link:hover{text-decoration:underline}",
676
+ ".dru-ia-badge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:999px;font-size:11px;font-weight:600;line-height:1.4;color:var(--dru-accent-fg);background:var(--dru-accent-tint);border:1px solid var(--dru-accent-tint-border);cursor:help}",
677
+ "select.dru-ia-input[disabled]{opacity:.6;cursor:not-allowed;background:var(--dru-surface-3)}",
678
+ // A3/A6(2026-11-07 业主逐条 UI 修正):状态头右侧按钮组 / 后台服务横向紧凑布局 / 卸载反馈单选行
679
+ ".dru-ia-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}",
680
+ ".dru-bridge-keep{display:flex;align-items:center;gap:6px;margin:8px 0 10px;padding:7px 10px;border-radius:8px;font-size:12px;line-height:1.5;color:var(--dru-warn-fg);background:var(--dru-warn-soft);border:1px solid var(--dru-warn-border)}",
681
+ ".dru-bridge-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 16px;align-items:center}",
682
+ "@media (max-width:520px){.dru-bridge-grid{grid-template-columns:minmax(0,1fr)}}",
683
+ ".dru-bridge-cell{min-width:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted))}",
684
+ ".dru-bridge-cell .dru-status-line{margin:0}",
685
+ ".dru-bridge-cell-action{display:flex;justify-content:flex-end}",
686
+ "@media (max-width:520px){.dru-bridge-cell-action{justify-content:flex-start}}",
687
+ ".dru-actions-wrap{flex-wrap:wrap}",
688
+ ".dru-ia-row-compact{min-height:40px;padding:6px 10px}",
689
+ ".dru-ia-row-compact input[type=radio]{flex:none;margin:0}",
690
+ // 配置页的「为什么要填这些」折叠按钮 + 原生 select 统一外观(业主:别文字堆砌,多余信息收起来)
691
+ ".dru-ia-disclose{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:32px;padding:0;border:0;background:none;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted));font:inherit;font-size:12px;cursor:pointer;text-align:left}",
692
+ ".dru-ia-disclose:hover{color:var(--dru-accent)}",
693
+ ".dru-ia-disclose:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
694
+ "select.dru-ia-input{appearance:none;-webkit-appearance:none;padding-right:28px;background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-position:calc(100% - 15px) 50%,calc(100% - 10px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}",
695
+ "@media (prefers-reduced-motion: reduce){",
696
+ " .dru-ia-scrim,.dru-ia-card,.dru-ia-btn,.dru-ia-seg-btn,.dru-ia-row{animation:none !important;transition:none !important}",
697
+ "}",
582
698
  ].join("\n");
583
699
  document.head.appendChild(styleEl);
584
700
 
@@ -1860,7 +1976,7 @@ window.__ModuleLoader__.load({
1860
1976
  return {
1861
1977
  bucket: "permission",
1862
1978
  title: "没有写入权限(装不进运行目录)",
1863
- hint: "点「一键修复」重装到可写目录;若装了安全软件(如杀毒/EDR),请允许 dsh-remote 写入用户目录后重试。",
1979
+ hint: "点「一键修复」重装到可写目录;若装了安全软件(如杀毒/EDR),请允许 dsh-remote-web 写入用户目录后重试。",
1864
1980
  repair: true, retry: true
1865
1981
  };
1866
1982
  }
@@ -1894,7 +2010,7 @@ window.__ModuleLoader__.load({
1894
2010
  return {
1895
2011
  bucket: "permission",
1896
2012
  title: "没有写入权限(装不进运行目录)",
1897
- hint: "点「一键修复」重装到可写目录;若装了安全软件(如杀毒/EDR),请允许 dsh-remote 写入用户目录后重试。",
2013
+ hint: "点「一键修复」重装到可写目录;若装了安全软件(如杀毒/EDR),请允许 dsh-remote-web 写入用户目录后重试。",
1898
2014
  repair: true, retry: true
1899
2015
  };
1900
2016
  }
@@ -2708,6 +2824,16 @@ window.__ModuleLoader__.load({
2708
2824
  // 最近一次拉到的日志尾巴(**与 log 分开存**的原因见 applyUpdLog 收尾分支:
2709
2825
  // log 只服务于"正在更新"的黑框,收尾即清空;lastLog 留给用户主动回看)。
2710
2826
  var lastLogArr = useState(""); var lastLog = lastLogArr[0]; var setLastLog = lastLogArr[1];
2827
+ // A7(2026-11-07 业主):彻底卸载前先做一次**挽留反馈**("为什么要卸载?" + 建议),
2828
+ // 提交走现有反馈通道(POST /dsh-remote/feedback,kind=uninstall),然后再继续卸载。
2829
+ var unrOpenArr = useState(false); var unrOpen = unrOpenArr[0]; var setUnrOpen = unrOpenArr[1];
2830
+ var unrReasonArr = useState(""); var unrReason = unrReasonArr[0]; var setUnrReason = unrReasonArr[1];
2831
+ var unrNoteArr = useState(""); var unrNote = unrNoteArr[0]; var setUnrNote = unrNoteArr[1];
2832
+ var unrBusyArr = useState(false); var unrBusy = unrBusyArr[0]; var setUnrBusy = unrBusyArr[1];
2833
+ var unrErrArr = useState(null); var unrErr = unrErrArr[0]; var setUnrErr = unrErrArr[1];
2834
+ // 2026-11-07 业主:版本卡里给出可复制的安装/更新命令(npm 包名保持不变:@mrrisega/dsh-remote)。
2835
+ var INSTALL_CMD = "npx @mrrisega/dsh-remote@latest";
2836
+ var instCopyArr = useState(false); var instCopied = instCopyArr[0]; var setInstCopied = instCopyArr[1];
2711
2837
 
2712
2838
  var loadVer = useCallback(function () {
2713
2839
  api("/dsh-remote/self").then(function (b) {
@@ -2875,8 +3001,94 @@ window.__ModuleLoader__.load({
2875
3001
  }).finally(function () { setCxBusy(false); });
2876
3002
  };
2877
3003
 
3004
+ /** A7 弹层:为什么要卸载?+ 原因单选 + 建议 + 「仍要卸载 / 我再想想」。 */
3005
+ function renderUninstallFeedback() {
3006
+ var close = function () { if (!unrBusy) setUnrOpen(false); };
3007
+ return iaDialog(Object.assign({
3008
+ title: "卸载前,想听听你的想法",
3009
+ sub: "提交后会继续卸载;也可以点「我再想想」退出",
3010
+ onClose: close,
3011
+ foot: [
3012
+ h("button", { type: "button", className: "dru-ia-btn", disabled: unrBusy, onClick: close }, "我再想想"),
3013
+ h("button", {
3014
+ type: "button", className: "dru-ia-btn is-danger", disabled: unrBusy,
3015
+ onClick: function () { submitUninstallFeedback(true); }
3016
+ }, unrBusy ? "提交中…" : "仍要卸载")
3017
+ ],
3018
+ children: [
3019
+ h("div", { className: "dru-ia-field dru-field" },
3020
+ h("div", { className: "dru-ia-label" }, "为什么要卸载?"),
3021
+ h("div", { className: "dru-ia-list" }, UNINSTALL_REASONS.map(function (r) {
3022
+ return h("label", { key: r.id, className: "dru-ia-row dru-ia-row-compact", style: { cursor: "pointer" } },
3023
+ h("input", {
3024
+ type: "radio", name: "dru-uninstall-reason", value: r.id,
3025
+ checked: unrReason === r.id, disabled: unrBusy,
3026
+ onChange: function () { setUnrReason(r.id); setUnrErr(null); }
3027
+ }),
3028
+ h("span", { className: "dru-ia-row-main" }, h("span", { className: "dru-ia-row-title" }, r.label)));
3029
+ })),
3030
+ unrErr ? h("div", { className: "dru-ia-err", role: "alert" }, unrErr) : null),
3031
+ h("div", { className: "dru-ia-field dru-field" },
3032
+ h("label", { className: "dru-ia-label", htmlFor: "dru-uninstall-note" }, "还有什么想告诉我们的?(可选)"),
3033
+ h("textarea", {
3034
+ id: "dru-uninstall-note", className: "dru-ia-input", rows: 3, value: unrNote,
3035
+ placeholder: "比如卡在哪一步、希望我们怎么改", disabled: unrBusy,
3036
+ onChange: function (e) { setUnrNote(e.target.value); }
3037
+ })),
3038
+ // 后果说明(业主 A7:卸载前讲清楚会发生什么;也让"不再保留 bridge/配置"的文案有落点)
3039
+ h("div", { className: "dru-ia-hint" },
3040
+ "仍要卸载会:① 停止并移除 bridge 自启动服务;② 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销。")
3041
+ ] }));
3042
+ }
3043
+ // Esc 关闭(弹层键盘契约;与面板里 iaDialog 的交互保持一致)
3044
+ useEffect(function () {
3045
+ if (!unrOpen) return undefined;
3046
+ var onKey = function (ev) {
3047
+ try {
3048
+ if (!ev) return;
3049
+ if (ev.key === "Escape" || ev.keyCode === 27) { if (!unrBusy) setUnrOpen(false); }
3050
+ } catch (e) { /* 忽略 */ }
3051
+ };
3052
+ try { document.addEventListener("keydown", onKey, true); } catch (e) { /* 忽略 */ }
3053
+ return function () { try { document.removeEventListener("keydown", onKey, true); } catch (e) { /* 忽略 */ } };
3054
+ }, [unrOpen, unrBusy]);
3055
+
3056
+ /** 挽留反馈的选项(业主:几个原因单选 + 可填建议)。 */
3057
+ var UNINSTALL_REASONS = [
3058
+ { id: "unstable", label: "功能不好用 / 不稳定" },
3059
+ { id: "complex", label: "太复杂,看不懂怎么用" },
3060
+ { id: "alternative", label: "已经有别的替代工具" },
3061
+ { id: "resource", label: "占用资源 / 影响电脑运行" },
3062
+ { id: "temporary", label: "只是临时不用了,以后可能再装" },
3063
+ { id: "other", label: "其它原因" }
3064
+ ];
3065
+ /** A7:点「彻底卸载」→ 先弹挽留反馈(不直接卸载;确认语义由这个弹层承担)。 */
2878
3066
  var doUninstall = function () {
2879
- if (!armed) { setArmed(true); return; }
3067
+ setUnrReason("");
3068
+ setUnrNote("");
3069
+ setUnrErr(null);
3070
+ setUnrOpen(true);
3071
+ };
3072
+ /** 提交挽留反馈(失败也继续卸载:反馈是"尽量收集",不能反过来卡住用户)。 */
3073
+ var submitUninstallFeedback = function (thenUninstall) {
3074
+ if (unrBusy) return;
3075
+ var reason = String(unrReason || "");
3076
+ if (!reason) { setUnrErr("请选择一个卸载原因(必选)"); return; }
3077
+ var hit = null;
3078
+ for (var i = 0; i < UNINSTALL_REASONS.length; i++) { if (UNINSTALL_REASONS[i].id === reason) { hit = UNINSTALL_REASONS[i]; break; } }
3079
+ var content = "[卸载反馈] 原因:" + ((hit && hit.label) || reason)
3080
+ + (unrNote.trim() ? ("\n建议:" + unrNote.trim()) : "");
3081
+ setUnrBusy(true);
3082
+ setUnrErr(null);
3083
+ post("/dsh-remote/feedback", { kind: "uninstall", category: "uninstall", content: content })
3084
+ .catch(function () { /* 反馈失败静默:不能因此拦住用户卸载 */ })
3085
+ .finally(function () {
3086
+ setUnrBusy(false);
3087
+ setUnrOpen(false);
3088
+ if (thenUninstall) runUninstall();
3089
+ });
3090
+ };
3091
+ var runUninstall = function () {
2880
3092
  setUnBusy(true);
2881
3093
  setSelfMsg(null);
2882
3094
  // 卸载:服务端现在"先卸插件、先回响应、慢活交子进程",正常应在 1 秒内返回;
@@ -2940,6 +3152,26 @@ window.__ModuleLoader__.load({
2940
3152
  ? h("span", { className: "dru-ver-badge dru-ver-badge-new" }, "发现新版本 v" + chk.latest)
2941
3153
  : chk && !outdated ? h("span", { className: "dru-ver-badge dru-ver-badge-ok" }, "已是最新版本") : null
2942
3154
  ),
3155
+ // 安装 / 更新命令(可复制;复用账号卡那套 .dru-copy-* 样式与反馈)
3156
+ h("div", { className: "dru-copy-row", style: { marginTop: 8 } },
3157
+ h("div", { className: "dru-copy-label" }, "安装 / 更新命令"),
3158
+ h("div", { className: "dru-copy-box" },
3159
+ h("span", { className: "dru-code-val" }, INSTALL_CMD),
3160
+ h("button", {
3161
+ type: "button",
3162
+ className: "dru-copy-btn" + (instCopied ? " copied" : ""),
3163
+ "aria-label": "复制安装命令",
3164
+ onClick: function () {
3165
+ copyText(INSTALL_CMD).then(function (done) {
3166
+ if (!done) { setSelfMsg({ kind: "err", text: "复制失败:请手动选中上面的命令复制(浏览器可能限制了剪贴板权限)" }); return; }
3167
+ setInstCopied(true);
3168
+ try { setTimeout(function () { setInstCopied(false); }, 1600); } catch (e) { /* 忽略 */ }
3169
+ });
3170
+ }
3171
+ }, instCopied ? "✅ 已复制" : "复制命令")
3172
+ ),
3173
+ h("div", { className: "dru-hint", style: { marginTop: 6 } }, "已安装的更新:重跑这条命令即可。")
3174
+ ),
2943
3175
  h("div", { className: "dru-meta" },
2944
3176
  runtimeReady === false
2945
3177
  ? "⚠ 后台服务运行环境缺失(点下方「一键更新」会自动补齐并启动,不用手动装)"
@@ -3007,7 +3239,7 @@ window.__ModuleLoader__.load({
3007
3239
  style: { marginLeft: "auto" },
3008
3240
  disabled: unBusy || updating || upBusy,
3009
3241
  onClick: doUninstall
3010
- }, unBusy ? "卸载中…" : armed ? "⚠ 再点一次确认彻底卸载" : "彻底卸载")
3242
+ }, unBusy ? "卸载中…" : "彻底卸载")
3011
3243
  ),
3012
3244
  cxMsg ? h("div", { className: "dru-msg dru-msg-" + cxMsg.kind }, cxMsg.text) : null,
3013
3245
  updated
@@ -3041,8 +3273,10 @@ window.__ModuleLoader__.load({
3041
3273
  : null,
3042
3274
  selfMsg ? h("div", { className: "dru-msg dru-msg-" + selfMsg.kind }, selfMsg.text) : null,
3043
3275
  h("div", { className: "dru-hint", style: { marginTop: 8 } },
3044
- armed ? "⚠ 再次点击后即开始彻底卸载:① 移除 dsh web 配置中的插件引用与本地文件;② 停止并移除 bridge 自启动服务(即让「后台服务」不再开机自启:macOS com.dshremote.bridge / Linux dsh-bridge / Windows 任务计划程序 dsh-remote-bridge)并结束残留进程;③ 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销,如需再次使用:在插件市场搜索「dsh-remote-web」重新安装,或执行 npx @mrrisega/dsh-remote。" :
3045
- "检测新版、一键在线更新、彻底卸载都在本卡片完成;更新卡住时会出现「取消并重试」。")
3276
+ "", // A7:二次确认已改成"挽留反馈弹层"(见 renderUninstallFeedback),这里不再有 armed 文案
3277
+ "检测新版、一键在线更新、彻底卸载都在本卡片完成;更新卡住时会出现「取消并重试」。"
3278
+ + "彻底卸载前会先询问一句卸载原因(可跳过反馈直接卸载)。"),
3279
+ unrOpen ? renderUninstallFeedback() : null
3046
3280
  );
3047
3281
  }
3048
3282
 
@@ -3133,6 +3367,91 @@ window.__ModuleLoader__.load({
3133
3367
  // ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
3134
3368
  // 匿名装机统计的「面板打开」只上报一次/每页(node 半还会每进程去重)。
3135
3369
  var panelOpenedSent = false;
3370
+ // ══════════════════════════════════════════════════════════════════════
3371
+ // 🧭 面板 IA 基础件(2026-11-07 UX 重做):内联 SVG 图标 + 弹层(dialog)
3372
+ //
3373
+ // 为什么图标必须内联 SVG(业主原话「插件端全是文字堆砌,太密集」+ UX 规范):
3374
+ // ① emoji 是**彩色位图**,跟随不了宿主主题的 currentColor,深色/亮色下观感不稳定;
3375
+ // ② 不同系统 emoji 字形差异巨大(Windows/Mac/iOS),同一个面板会长得不一样;
3376
+ // ③ 16px 的 emoji 在密集列表里视觉重量过大,是"文字堆砌感"的来源之一。
3377
+ // 这里只放**本面板新增结构**用到的图标(旧卡片的 emoji 随重做一起不再出现在主界面)。
3378
+ // ══════════════════════════════════════════════════════════════════════
3379
+ var DRU_ICONS = {
3380
+ phone: '<rect x="5" y="2" width="14" height="20" rx="2.5"/><path d="M11 18h2"/>',
3381
+ bell: '<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/>',
3382
+ devices: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
3383
+ user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
3384
+ x: '<path d="M18 6 6 18M6 6l12 12"/>',
3385
+ right: '<path d="m9 18 6-6-6-6"/>',
3386
+ left: '<path d="m15 18-6-6 6-6"/>',
3387
+ copy: '<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
3388
+ check: '<path d="M20 6 9 17l-5-5"/>',
3389
+ open: '<path d="M7 17 17 7M7 7h10v10"/>',
3390
+ key: '<circle cx="7.5" cy="15.5" r="3.5"/><path d="m21 2-9.6 9.6M15 7l3 3"/>',
3391
+ qr: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM20 14h1M20 20h1M17 20h1"/>',
3392
+ chat: '<path d="M21 11.5a8.4 8.4 0 0 1-8.5 8.5 9 9 0 0 1-3.4-.7L3 21l1.7-5.1A8.4 8.4 0 0 1 4 11.5 8.5 8.5 0 0 1 12.5 3 8.4 8.4 0 0 1 21 11.5z"/>',
3393
+ send: '<path d="m22 2-7 20-4-9-9-4 20-7z"/>',
3394
+ code: '<path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/>',
3395
+ link: '<path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/>',
3396
+ eye: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/>',
3397
+ eyeoff: '<path d="M17.9 17.9A10.6 10.6 0 0 1 12 19c-6.5 0-10-7-10-7a18 18 0 0 1 4.2-4.9M9.9 4.2A10.6 10.6 0 0 1 12 4c6.5 0 10 7 10 7a18 18 0 0 1-2.2 3.2M2 2l20 20"/>',
3398
+ alert: '<path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/>'
3399
+ };
3400
+ /** 内联 SVG 图标(16px,currentColor)。名字不认识时退化成一个小圆点,绝不抛错。 */
3401
+ function druIcon(name, size) {
3402
+ var inner = DRU_ICONS[name] || '<circle cx="12" cy="12" r="3"/>';
3403
+ return h("svg", {
3404
+ width: size || 16, height: size || 16, viewBox: "0 0 24 24", fill: "none",
3405
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
3406
+ "aria-hidden": "true", focusable: "false",
3407
+ dangerouslySetInnerHTML: { __html: inner }
3408
+ });
3409
+ }
3410
+
3411
+ /**
3412
+ * 通用弹层(dialog)的**纯渲染函数**(不是组件:本仓库的面板只在少数 harness 里递归展开
3413
+ * 函数组件,把弹层做成"组件"会让大量既有用例看不到弹层内容 —— 而且那也不影响真实 React)。
3414
+ * 键盘(Esc / Tab 焦点陷阱)与"关闭后焦点回触发按钮"由 RemoteControlSection 里那条
3415
+ * `useEffect([iaModal])` 统一负责,见 openIAModal 上方注释。
3416
+ *
3417
+ * 交互契约(UX 检索:modal focus trap / Esc / focus return):
3418
+ * · role="dialog" + aria-modal + aria-labelledby(标题始终可见);
3419
+ * · Esc 关闭;点遮罩关闭;右上角 ✕ 关闭(三者同一动作);
3420
+ * · 关闭后焦点回到打开前的元素;
3421
+ * · 视觉:宽 480–560、圆角 12、边框 1px、表头/底部操作固定,正文超过 1.5 屏才滚动;
3422
+ * · prefers-reduced-motion 在样式表里统一关掉动效。
3423
+ */
3424
+ function iaDialog(props) {
3425
+ var p = props || {};
3426
+ var title = p.title || "";
3427
+ var sub = p.sub || "";
3428
+ var onClose = p.onClose;
3429
+ var labelledBy = "dru-ia-dialog-title";
3430
+ var bodyKids = Array.isArray(p.children) ? p.children : [p.children];
3431
+ var footKids = Array.isArray(p.foot) ? p.foot : [p.foot];
3432
+ var body = [].concat.apply(["div", { className: "dru-ia-dialog-body" }], bodyKids.filter(function (x) { return x !== null && x !== undefined && x !== false; }));
3433
+ var headKids = [
3434
+ h("div", { className: "dru-ia-dialog-title", id: labelledBy }, title,
3435
+ sub ? h("div", { className: "dru-ia-dialog-sub" }, sub) : null),
3436
+ h("button", { type: "button", className: "dru-ia-dialog-x", "aria-label": "关闭", onClick: onClose }, druIcon("x"))
3437
+ ];
3438
+ var kids = [
3439
+ h("div", { className: "dru-ia-dialog-head" }, headKids[0], headKids[1]),
3440
+ h.apply(null, body)
3441
+ ];
3442
+ if (p.foot) {
3443
+ kids.push(h.apply(null, [].concat.apply(["div", { className: "dru-ia-dialog-foot" }], footKids)));
3444
+ }
3445
+ return h("div", {
3446
+ className: "dru-ia-scrim",
3447
+ onMouseDown: function (e) { if (e && e.target === e.currentTarget && onClose) onClose(); }
3448
+ },
3449
+ h.apply(null, [].concat.apply(["div", {
3450
+ className: "dru-ia-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": labelledBy
3451
+ }], kids))
3452
+ );
3453
+ }
3454
+
3136
3455
  function RemoteControlSection(props) {
3137
3456
  // 🎨 主题(宿主深色/亮色):订阅外部存储 → 宿主切换时本组件自动重渲染,
3138
3457
  // 根节点上的 data-dru-theme 随之更新,整套 --dru-* 令牌实时切换。
@@ -3237,8 +3556,221 @@ window.__ModuleLoader__.load({
3237
3556
  var fixPromptArr = useState(null); var fixPrompt = fixPromptArr[0]; var setFixPrompt = fixPromptArr[1];
3238
3557
  var fixReportArr = useState(""); var fixReport = fixReportArr[0]; var setFixReport = fixReportArr[1];
3239
3558
 
3240
- var refresh = useCallback(function () {
3241
- setBusy("status");
3559
+ // ── 🧭 面板 IA 重做 + 通知通道(2026-11-07)────────────────────────────────
3560
+ // hook 序号铁律:全部追加在**最后一个既有 hook 之后**(见本组件顶部说明)。
3561
+ // · iaModal 当前打开的弹层(null / "mobile" / "channel" / "devices" / "account");
3562
+ // · iaChStep 通道弹层里的步骤(list / wecom / wechat / feishu / webhook);
3563
+ // · imBots* GET /dsh-remote/im-bots 的**脱敏**视图 + 表单态(secret 永不回显,
3564
+ // 所以输入框初值恒为空字符串 = "不修改")。
3565
+ var iaModalArr = useState(null); var iaModal = iaModalArr[0]; var setIaModal = iaModalArr[1];
3566
+ var iaChStepArr = useState("list"); var iaChStep = iaChStepArr[0]; var setIaChStep = iaChStepArr[1];
3567
+ var imBotsArr = useState(null); var imBots = imBotsArr[0]; var setImBots = imBotsArr[1];
3568
+ var imBotsBusyArr = useState(""); var imBotsBusy = imBotsBusyArr[0]; var setImBotsBusy = imBotsBusyArr[1];
3569
+ var imBotsMsgArr = useState(null); var imBotsMsg = imBotsMsgArr[0]; var setImBotsMsg = imBotsMsgArr[1];
3570
+ var imBotsFieldErrArr = useState({}); var imBotsFieldErr = imBotsFieldErrArr[0]; var setImBotsFieldErr = imBotsFieldErrArr[1];
3571
+ var imBotsBotIdArr = useState(""); var imBotsBotId = imBotsBotIdArr[0]; var setImBotsBotId = imBotsBotIdArr[1];
3572
+ var imBotsSecretArr = useState(""); var imBotsSecret = imBotsSecretArr[0]; var setImBotsSecret = imBotsSecretArr[1];
3573
+ var imBotsChatArr = useState(""); var imBotsChat = imBotsChatArr[0]; var setImBotsChat = imBotsChatArr[1];
3574
+ var imBotsUrlArr = useState(""); var imBotsUrl = imBotsUrlArr[0]; var setImBotsUrl = imBotsUrlArr[1];
3575
+ var imBotsShowSecretArr = useState(false); var imBotsShowSecret = imBotsShowSecretArr[0]; var setImBotsShowSecret = imBotsShowSecretArr[1];
3576
+ // 「转接到哪个会话」:空串 = 跟随当前活跃会话(桥侧默认)。title 一起存,用于回显与保存。
3577
+ var imBotsSessionIdArr = useState(""); var imBotsSessionId = imBotsSessionIdArr[0]; var setImBotsSessionId = imBotsSessionIdArr[1];
3578
+ var imBotsSessionTitleArr = useState(""); var imBotsSessionTitle = imBotsSessionTitleArr[0]; var setImBotsSessionTitle = imBotsSessionTitleArr[1];
3579
+ // 「为什么要填这些」默认折叠(业主:配置页别文字堆砌)
3580
+ var imBotsWhyArr = useState(false); var imBotsWhyOpen = imBotsWhyArr[0]; var setImBotsWhyOpen = imBotsWhyArr[1];
3581
+ // 从「飞书/钉钉即将支持 → 告诉我,优先支持」跳到反馈视图时带过去的一句引导(独立状态:
3582
+ // message 会被面板里各种动作清掉,跨视图的上下文不能押在它身上)。
3583
+ var iaFbHintArr = useState(""); var iaFbHint = iaFbHintArr[0]; var setIaFbHint = iaFbHintArr[1];
3584
+ // 未登录守卫:为什么弹登录(A4/A5)+ 登录后自动回到哪个流程("手机访问"登录完继续生成访问码)。
3585
+ var iaLoginWhyArr = useState(""); var iaLoginWhy = iaLoginWhyArr[0]; var setIaLoginWhy = iaLoginWhyArr[1];
3586
+ var iaAfterLoginArr = useState(""); var iaAfterLogin = iaAfterLoginArr[0]; var setIaAfterLogin = iaAfterLoginArr[1];
3587
+
3588
+ /** 读通道配置(GET 只回脱敏视图:botId/hasSecret/url/targetChatId + 桥写的状态)。 */
3589
+ function loadImBots() {
3590
+ return api("/dsh-remote/im-bots").then(function (b) {
3591
+ setImBots(b && typeof b === "object" ? b : null);
3592
+ return b;
3593
+ }).catch(function () {
3594
+ setImBots(null); // 读不到 = 未配置(入口卡显示「未配置」);进弹层点保存会拿到真实错误
3595
+ return null;
3596
+ });
3597
+ }
3598
+ // 挂载即读一次:入口格「通道」卡要显示当前状态;失败静默(下次打开弹层会重读)。
3599
+ useEffect(function () { loadImBots(); }, []);
3600
+
3601
+ // 弹层的键盘/焦点契约(UX 检索:modal focus trap / Esc / focus return):
3602
+ // · Esc 关闭;Tab / Shift+Tab 在弹层内循环(焦点绝不跑到背后的页面上);
3603
+ // · 关闭后焦点回到打开前的元素(document.activeElement 快照);
3604
+ // · 任何一步失败都吞掉 —— 弹层本身是纯 DOM,不依赖这些逻辑也能用。
3605
+ useEffect(function () {
3606
+ if (!iaModal) return undefined;
3607
+ var prev = null;
3608
+ try { prev = document.activeElement || null; } catch (e0) { prev = null; }
3609
+ var onKey = function (ev) {
3610
+ try {
3611
+ if (!ev) return;
3612
+ if (ev.key === "Escape" || ev.keyCode === 27) {
3613
+ if (ev.stopPropagation) ev.stopPropagation();
3614
+ setIaModal(null);
3615
+ return;
3616
+ }
3617
+ if (ev.key !== "Tab") return;
3618
+ var dlg = null;
3619
+ try { dlg = document.querySelector(".dru-ia-dialog"); } catch (e1) { dlg = null; }
3620
+ if (!dlg || typeof dlg.querySelectorAll !== "function") return;
3621
+ var list = dlg.querySelectorAll('button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
3622
+ var items = [];
3623
+ for (var i = 0; i < list.length; i++) {
3624
+ try { if (list[i] && list[i].offsetParent !== null) items.push(list[i]); } catch (e2) { /* 忽略 */ }
3625
+ }
3626
+ if (!items.length) return;
3627
+ var first = items[0], last = items[items.length - 1];
3628
+ var act = document.activeElement;
3629
+ if (ev.shiftKey) {
3630
+ if (act === first || !dlg.contains || !dlg.contains(act)) { ev.preventDefault(); try { last.focus(); } catch (e3) { /* 忽略 */ } }
3631
+ } else if (act === last) {
3632
+ ev.preventDefault();
3633
+ try { first.focus(); } catch (e4) { /* 忽略 */ }
3634
+ }
3635
+ } catch (e5) { /* 键盘逻辑失败不影响弹层使用 */ }
3636
+ };
3637
+ try { document.addEventListener("keydown", onKey, true); } catch (e6) { /* 忽略 */ }
3638
+ var t = null;
3639
+ try {
3640
+ t = setTimeout(function () {
3641
+ try {
3642
+ var dlg = document.querySelector(".dru-ia-dialog");
3643
+ if (!dlg || typeof dlg.querySelector !== "function") return;
3644
+ var el = dlg.querySelector(".dru-ia-dialog-body input, .dru-ia-dialog-body button, .dru-ia-dialog-x");
3645
+ if (el && el.focus) el.focus();
3646
+ } catch (e7) { /* 忽略 */ }
3647
+ }, 0);
3648
+ } catch (e8) { t = null; }
3649
+ return function () {
3650
+ try { if (t !== null) clearTimeout(t); } catch (e9) { /* 忽略 */ }
3651
+ try { document.removeEventListener("keydown", onKey, true); } catch (e10) { /* 忽略 */ }
3652
+ try { if (prev && prev.focus) prev.focus(); } catch (e11) { /* 忽略 */ }
3653
+ };
3654
+ }, [iaModal]);
3655
+
3656
+ /** 表单 → 契约 payload(空 secret 不传 = 保留已保存的 secret;空 targetChatId = 不设置)。 */
3657
+ function imBotsFormPayload() {
3658
+ var wecom = { enabled: true, botId: String(imBotsBotId || "").trim() };
3659
+ var sec = String(imBotsSecret || "").trim();
3660
+ if (sec) wecom.secret = sec;
3661
+ wecom.targetChatId = String(imBotsChat || "").trim();
3662
+ // 会话可选绑定:空串 = 跟随当前活跃会话(服务端会同时清掉 sessionTitle)
3663
+ wecom.sessionId = String(imBotsSessionId || "").trim();
3664
+ wecom.sessionTitle = wecom.sessionId ? String(imBotsSessionTitle || "").trim() : "";
3665
+ var u = String(imBotsUrl || "").trim();
3666
+ if (u) wecom.url = u;
3667
+ return wecom;
3668
+ }
3669
+ /** 节点半的稳定错误码 → 就近字段错误(有码就用码,没码就整体提示;绝不猜)。 */
3670
+ function imBotsFieldErrorsOf(code, message) {
3671
+ var e = {};
3672
+ var text = message || "";
3673
+ if (code === "invalid_bot_id") e.botId = text || "Bot ID 格式不对:应形如 aibxxxxxxxx";
3674
+ else if (code === "missing_secret") e.secret = text || "请填写 Secret";
3675
+ else if (code === "invalid_url") e.url = text || "服务地址必须以 http:// 或 https:// 开头";
3676
+ else if (code === "invalid_target_chat_id") e.chat = text || "接收会话 ID 太长";
3677
+ else if (code === "invalid_session_id") e.session = text || "会话 ID 太长";
3678
+ return e;
3679
+ }
3680
+ /** 保存(PUT);testAfter=true 时走 POST /test(= 保存后等桥连接,最多 10s)。 */
3681
+ function saveImBots(testAfter) {
3682
+ if (imBotsBusy !== "") return undefined;
3683
+ setImBotsBusy(testAfter ? "test" : "save");
3684
+ setImBotsMsg(null);
3685
+ setImBotsFieldErr({});
3686
+ var payload = { wecom: imBotsFormPayload() };
3687
+ var path = testAfter ? "/dsh-remote/im-bots/test" : "/dsh-remote/im-bots";
3688
+ var opts = {
3689
+ method: testAfter ? "POST" : "PUT",
3690
+ headers: { "content-type": "application/json" },
3691
+ body: JSON.stringify(payload)
3692
+ };
3693
+ return api(path, opts, testAfter ? 15000 : 10000).then(function (b) {
3694
+ if (testAfter && b && b.connected) {
3695
+ setImBotsMsg({ kind: "ok", text: "已连接:企业微信机器人可以接收消息了。" + (b.authenticated ? "" : "(通道已连通,鉴权稍后完成)") });
3696
+ } else {
3697
+ setImBotsMsg({ kind: "ok", text: testAfter ? "配置已保存。" : "已保存。" });
3698
+ }
3699
+ return loadImBots();
3700
+ }).catch(function (e) {
3701
+ var body = (e && e.body) || {};
3702
+ var code = String(body.error || "");
3703
+ var text = String(body.message || (e && e.message) || "操作失败");
3704
+ if (code === "missing_secret" || code === "invalid_bot_id" || code === "invalid_url" || code === "invalid_target_chat_id") {
3705
+ setImBotsFieldErr(imBotsFieldErrorsOf(code, text));
3706
+ setImBotsMsg({ kind: "err", text: text });
3707
+ } else if (code === "connect_failed") {
3708
+ setImBotsMsg({ kind: "err", text: "连接失败:" + (body.lastError || text) });
3709
+ } else if (code === "timeout") {
3710
+ setImBotsMsg({ kind: "warn", text: text });
3711
+ } else {
3712
+ setImBotsMsg({ kind: "err", text: text });
3713
+ }
3714
+ return null;
3715
+ }).finally(function () { setImBotsBusy(""); });
3716
+ }
3717
+ /** 「测试」= 用**已保存**配置重连并等结果(不重写配置;想改字段请先「保存」/「保存并连接」)。 */
3718
+ /** 「发送预览卡片」= 给绑定的企微会话发一张**演示用**审批卡(按钮不会真的执行)。
3719
+ * 触发方式:PUT 一个更大的 wecom.previewCardAt(桥监听这个时间戳);**必须带上表单里的
3720
+ * 全部既有字段**,否则 PUT 的覆盖写会把 botId/secret/会话绑定一起冲掉。
3721
+ * 空 secret 仍表示"不修改",所以只在用户填了新 Secret 时才提交 secret。 */
3722
+ function sendPreviewCard() {
3723
+ if (imBotsBusy !== "") return undefined;
3724
+ setImBotsBusy("preview");
3725
+ setImBotsMsg(null);
3726
+ setImBotsFieldErr({});
3727
+ var payload = { wecom: imBotsFormPayload() };
3728
+ // 桥只认"值变大":同一毫秒连点两次也要保证严格递增(否则第二次点了没反应,用户以为坏了)。
3729
+ var prevAt = Number((imBots && imBots.wecom && imBots.wecom.previewCardAt) || 0);
3730
+ payload.wecom.previewCardAt = Math.max(Date.now(), prevAt + 1);
3731
+ return api("/dsh-remote/im-bots", {
3732
+ method: "PUT",
3733
+ headers: { "content-type": "application/json" },
3734
+ body: JSON.stringify(payload)
3735
+ }, 10000).then(function () {
3736
+ setImBotsMsg({ kind: "ok", text: "已发送预览卡片,去企业微信看看" });
3737
+ return loadImBots(); // 拉回 status.wecom.lastPreviewCardAt → 显示「上次预览:HH:MM」
3738
+ }).catch(function (e) {
3739
+ var body = (e && e.body) || {};
3740
+ var code = String(body.error || "");
3741
+ var text = String(body.message || (e && e.message) || "发送失败");
3742
+ if (code === "invalid_preview_card_at") setImBotsMsg({ kind: "err", text: text });
3743
+ else setImBotsMsg({ kind: code === "timeout" ? "warn" : "err", text: "发送失败:" + text });
3744
+ return null;
3745
+ }).finally(function () { setImBotsBusy(""); });
3746
+ }
3747
+
3748
+ function testImBots() {
3749
+ if (imBotsBusy !== "") return undefined;
3750
+ setImBotsBusy("test");
3751
+ setImBotsMsg(null);
3752
+ return api("/dsh-remote/im-bots/test", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }, 15000)
3753
+ .then(function (b) {
3754
+ if (b && b.connected) setImBotsMsg({ kind: "ok", text: "已连接" + (b.authenticated ? "(鉴权通过)" : "") + "。" });
3755
+ else setImBotsMsg({ kind: "ok", text: "通道已就绪。" });
3756
+ return loadImBots();
3757
+ })
3758
+ .catch(function (e) {
3759
+ var body = (e && e.body) || {};
3760
+ var code = String(body.error || "");
3761
+ var text = String(body.message || (e && e.message) || "测试失败");
3762
+ setImBotsMsg({ kind: code === "timeout" ? "warn" : "err", text: code === "connect_failed" ? "连接失败:" + (body.lastError || text) : text });
3763
+ return null;
3764
+ })
3765
+ .finally(function () { setImBotsBusy(""); });
3766
+ }
3767
+
3768
+ /**
3769
+ * 拉全量状态。quiet=true:**不占用忙碌态**(用于"登录后异步补状态"——
3770
+ * 业主实测登录 7~8 秒的体验里,按钮一直转圈是主因之一;这里让按钮只反映登录请求本身)。
3771
+ */
3772
+ var refresh = useCallback(function (quiet) {
3773
+ if (!quiet) setBusy("status");
3242
3774
  var t0 = netNow(); // 📶 延迟采样点①:打开面板时这一次 /status(既有请求,不新增)
3243
3775
  api("/dsh-remote/status").then(function (body) {
3244
3776
  noteRtt(netNow() - t0); // 先记样本再 setSt —— 这一次的延迟当轮就能显示出来
@@ -3246,9 +3778,14 @@ window.__ModuleLoader__.load({
3246
3778
  setMode(body && body.config && body.config.mode === "local" ? "local" : "saas");
3247
3779
  setMessage(null);
3248
3780
  }).catch(function (e) { setMessage({ kind: "err", text: "读取状态失败: " + e.message }); })
3249
- .finally(function () { setBusy(""); });
3781
+ .finally(function () { if (!quiet) setBusy(""); });
3250
3782
  }, []);
3251
3783
 
3784
+ /** 后台静默补状态(不置忙碌、失败静默)——登录成功后用它把衍生信息补上。 */
3785
+ var refreshQuiet = function () {
3786
+ try { refresh(true); } catch (e) { /* 忽略 */ }
3787
+ };
3788
+
3252
3789
  function loadCaptcha(kind) {
3253
3790
  var setCap = kind === "login" ? setLcap : setRcap;
3254
3791
  setCap({ id: null, svg: '<span class="dru-hint">加载中</span>' });
@@ -3674,8 +4211,11 @@ window.__ModuleLoader__.load({
3674
4211
  // ⚠️ 必须是无条件调用的 hook(hook 序号稳定),所以判断写在回调里,不能围着它加 if。
3675
4212
  // 同一帧的兜底还有底部的 view 分派:signed-out 时它**直接渲染 home**,不会先闪一帧空白。
3676
4213
  useEffect(function () {
3677
- if (view === "wechat" && !loggedIn) setView("home");
3678
- }, [view, loggedIn]);
4214
+ // IA 重做(2026-11-07):不再有 "wechat" 视图(微信通道搬进「通知与消息通道」弹层)——
4215
+ // 这里只做**视角兜底**:任何不在白名单里的 view(例如旧代码/外部残留)一律回 home,
4216
+ // 绝不让面板停在空视图上。保留这条 effect 也保证 hook 序号不变。
4217
+ if (view !== "home" && view !== "feedback" && view !== "invite") setView("home");
4218
+ }, [view]);
3679
4219
 
3680
4220
  /**
3681
4221
  * 连接阶段轮询:GET /dsh-remote/bridge-status(node 半在返回前自动补装运行环境 / 拉起 bridge /
@@ -3870,8 +4410,25 @@ window.__ModuleLoader__.load({
3870
4410
  * 于是"服务没重启成功"和"一切正常"在界面上长得一模一样。
3871
4411
  */
3872
4412
  function applyAccountSaved(cfg, okText) {
3873
- setSt(cfg);
3874
4413
  var br = cfg && cfg.bridgeRestart;
4414
+ // B2/B3:登录响应已改成"立即返回"(statusPending=true,只回最小 config 回显)——
4415
+ // 合并进现有 st(不覆盖 service/connect 等已渲染数据),其余信息后台静默补。
4416
+ if (cfg && cfg.statusPending) {
4417
+ setSt(function (prev) {
4418
+ var base = prev && typeof prev === "object" ? prev : {};
4419
+ return Object.assign({}, base, { ok: true, config: Object.assign({}, (base && base.config) || {}, cfg.config || {}) });
4420
+ });
4421
+ if (br && br.ok === false && br.status !== "running" && br.status !== "provisioning" && br.status !== "skipped") {
4422
+ setMsg("warn", okText + ";但后台服务未能自动重启:"
4423
+ + ((br.detail || br.status) || "未知原因")
4424
+ + " —— 新账号可能收不到这台设备,请点下方「重启服务」或重启一次 dsh web。");
4425
+ } else {
4426
+ setMsg("ok", okText + "(正在准备远程服务…)");
4427
+ }
4428
+ refreshQuiet();
4429
+ return;
4430
+ }
4431
+ setSt(cfg);
3875
4432
  if (br && br.ok === false && br.status !== "running" && br.status !== "provisioning" && br.status !== "skipped") {
3876
4433
  setMsg("warn", okText + ";但后台服务未能自动重启:"
3877
4434
  + ((br.detail || br.status) || "未知原因")
@@ -3887,12 +4444,28 @@ window.__ModuleLoader__.load({
3887
4444
  post("/dsh-remote/login", { phone: phone.trim(), password: pass, captcha_id: lcap.id, captcha_answer: lcapTxt.trim() })
3888
4445
  .then(function (body) {
3889
4446
  if (body.ok || body.status === 200) {
4447
+ // B3(2026-11-07):**登录请求成功即切已登录 UI**,不再等 /config(它要保存配对、
4448
+ // 重启 bridge、拉衍生信息)。按钮忙碌态只覆盖真正的登录请求。
4449
+ setBusy("");
4450
+ setSt(function (prev) {
4451
+ var base = prev && typeof prev === "object" ? prev : {};
4452
+ return Object.assign({}, base, {
4453
+ ok: true,
4454
+ config: Object.assign({}, (base && base.config) || {}, {
4455
+ phone: phone.trim(), hasPhone: true, hasPassword: true
4456
+ })
4457
+ });
4458
+ });
4459
+ setMsg("ok", "登录成功,正在准备远程服务…");
3890
4460
  return post("/dsh-remote/config", { phone: phone.trim(), password: pass }).then(function (cfg) {
3891
4461
  // 切换连接账号(手机号与之前不同):清除旧账号留下的反馈线程凭据
3892
4462
  if (prevPhone !== phone.trim()) fbClearThreads();
3893
4463
  setPwdOpen(false); // 登录成功(可能从「忘记密码」返回)→ 收起重置表单
3894
4464
  setPass(""); setLcapTxt(""); setLcap(null);
3895
4465
  applyAccountSaved(cfg, "✅ 登录成功,账号已保存");
4466
+ }).catch(function (e) {
4467
+ // 账号已登录成功,只是本机配对保存失败:如实提示,不影响已登录 UI
4468
+ setMsg("warn", "登录成功,但本机配对信息保存失败:" + (e && e.message ? e.message : e) + "(可重试或重启面板)");
3896
4469
  });
3897
4470
  }
3898
4471
  var relayBody = body.body || {};
@@ -3943,10 +4516,24 @@ window.__ModuleLoader__.load({
3943
4516
  post("/dsh-remote/register", regPayload)
3944
4517
  .then(function (body) {
3945
4518
  if (body.ok || body.status === 201 || (body.body && body.body.token)) {
4519
+ // B3:注册成功即切已登录 UI(与登录同一策略),/config 异步完成配对保存。
4520
+ setBusy("");
4521
+ setSt(function (prev) {
4522
+ var base = prev && typeof prev === "object" ? prev : {};
4523
+ return Object.assign({}, base, {
4524
+ ok: true,
4525
+ config: Object.assign({}, (base && base.config) || {}, {
4526
+ phone: rphone.trim(), hasPhone: true, hasPassword: true
4527
+ })
4528
+ });
4529
+ });
4530
+ setMsg("ok", "注册成功,正在准备远程服务…");
3946
4531
  return post("/dsh-remote/config", { phone: rphone.trim(), password: rpass }).then(function (cfg) {
3947
4532
  setPwdOpen(false);
3948
4533
  setRphone(""); setRpass(""); setRpass2(""); setRsms(""); setRcap(null); setRcapTxt(""); setRInvite("");
3949
4534
  applyAccountSaved(cfg, "✅ 注册成功,已自动登录");
4535
+ }).catch(function (e) {
4536
+ setMsg("warn", "注册成功,但本机配对信息保存失败:" + (e && e.message ? e.message : e));
3950
4537
  });
3951
4538
  }
3952
4539
  var b = body.body || {};
@@ -4153,8 +4740,18 @@ window.__ModuleLoader__.load({
4153
4740
  // 切换到自建服务(连接账号上下文变为本地无账号):清除 SaaS 账号的反馈线程凭据
4154
4741
  fbClearThreads();
4155
4742
  setPwdOpen(false);
4156
- setSt(body); setSelfHost(""); setLocalKey("");
4157
- setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,后台服务已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
4743
+ // B2:登录/配对响应现在是精简的(statusPending)→ 合并最小 config 回显,其余静默补
4744
+ if (body && body.ok && body.statusPending) {
4745
+ setSt(function (prev) {
4746
+ var base = prev && typeof prev === "object" ? prev : {};
4747
+ return Object.assign({}, base, { ok: true, config: Object.assign({}, (base && base.config) || {}, body.config || {}) });
4748
+ });
4749
+ refreshQuiet();
4750
+ } else {
4751
+ setSt(body);
4752
+ }
4753
+ setSelfHost(""); setLocalKey("");
4754
+ setMsg(body && body.ok ? "ok" : "err", body && body.ok ? "✅ 已切换到自建服务,后台服务已重启" : ((body && (body.error || (body.body && body.body.error))) || "保存失败"));
4158
4755
  })
4159
4756
  .catch(function (e) { setMsg("err", "保存失败: " + e.message); })
4160
4757
  .finally(function () { setBusy(""); });
@@ -4528,15 +5125,8 @@ window.__ModuleLoader__.load({
4528
5125
  // 免费用户这里什么都不显示 —— 把「免费档」当标签挂在脸上,正是要减少的营销露出。
4529
5126
  isMember ? h("div", { className: "plan" }, plan === "pro_max" ? "Pro Max" : "PRO") : null
4530
5127
  ),
4531
- // 2026-10-17(业主):退出登录从按钮行挪到**账号行右侧** —— 它属于"账号维护",
4532
- // 与账号信息同一行动线;主按钮行也因此少一个按钮。
4533
- h("button", {
4534
- type: "button",
4535
- className: "dru-user-logout",
4536
- disabled: busy !== "",
4537
- title: "退出登录(电脑端后台服务与手机端连接不受影响)",
4538
- onClick: function () { doLogout(false); }
4539
- }, "退出登录")
5128
+ // A3(2026-11-07):退出登录统一放到**状态头右侧**(与「登录 / 注册」同一位置),
5129
+ // 账号弹层里不再重复一个同义按钮。
4540
5130
  ),
4541
5131
  // 额度状态:不限量说明 / 剩余额度百分比 / 会员到期日。
4542
5132
  // 首屏只说「还剩多少」,不提档位名、不写带宽数值(数值在下面「更多」里)。
@@ -4544,58 +5134,33 @@ window.__ModuleLoader__.load({
4544
5134
  h("span", { className: "dru-dot " + (isMember ? "dru-dot-on" : "dru-dot-off") }),
4545
5135
  h("span", null, planText)
4546
5136
  ),
4547
- h("div", { className: "dru-actions", style: { marginTop: 10 } },
4548
- // 主行动:免费=升级入口,会员=续费/转正(账号维护,永远在正面第一位)
5137
+ // A2(2026-11-07 业主:账号弹层不要折叠):弹层空间足够 → 所有功能**直接平铺**,
5138
+ // 不再有「更多」折叠体;一屏一个主操作(升级/续费仍是唯一 primary)。
5139
+ h("div", { className: "dru-actions dru-actions-wrap", style: { marginTop: 10 } },
4549
5140
  h("button", {
4550
5141
  type: "button", className: "dru-btn dru-btn-primary",
4551
5142
  style: { display: "inline-flex", alignItems: "center" }, disabled: busy !== "",
4552
5143
  onClick: openUpgradeAuth
4553
5144
  },
4554
- busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
4555
- // 次要:用户反馈("联系我们"的高频入口,留在正面)
4556
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
4557
- // 「更多」变成同一行里的普通按钮(原来是一条整宽披露条,视觉重量和按钮完全不匹配)。
4558
- // 无障碍不变:id / aria-expanded / aria-controls 保持原样(用例按它们定位)。
4559
- h("button", {
4560
- type: "button",
4561
- id: MKT_MORE_TOGGLE_ID,
4562
- className: "dru-btn dru-btn-ghost dru-disclose-inline",
4563
- "aria-expanded": moreOpen ? "true" : "false",
4564
- "aria-controls": MKT_MORE_BODY_ID,
4565
- onClick: function () { setMoreOpen(!moreOpen); }
4566
- },
4567
- h("span", null, "更多"),
4568
- h("span", { className: "dru-disclose-caret", "aria-hidden": "true" }, moreOpen ? "▲" : "▼")
4569
- )
5145
+ busy === "upgrade" ? "生成链接中…" : (!isMember ? "升级 PRO" : source === "trial" ? "转正式 PRO" : "续费会员")),
5146
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "用户反馈"),
5147
+ invOff ? null : h("button", {
5148
+ type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "",
5149
+ title: invTipRule ? "带 " + invTipRule.n + " 位新用户可得 " + invTipRule.days + " 天 PRO 会员" : "带新用户换会员",
5150
+ onClick: function () { setView("invite"); loadInvite(); }
5151
+ }, "带新用户换会员"),
5152
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "修改密码"),
5153
+ community && community.qrcode
5154
+ ? h("button", {
5155
+ type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "",
5156
+ onClick: function () { setCommOpen(true); }
5157
+ }, "加入交流群")
5158
+ : null
4570
5159
  ),
4571
5160
  // 🎁 邀请引导气泡:紧跟按钮行(不遮挡按钮)。点 ✕ 只做"不再提示",不再改变入口位置。
4572
5161
  renderInviteTip(),
4573
- // 🧰 「更多」折叠体:带新用户 / 修改密码 / 加入交流群 + 修改密码表单
4574
- h("div", {
4575
- id: MKT_MORE_BODY_ID,
4576
- className: "dru-more-body",
4577
- hidden: !moreOpen
4578
- },
4579
- h("div", { className: "dru-more-actions" },
4580
- invOff ? null : h("button", {
4581
- type: "button",
4582
- className: "dru-btn dru-btn-ghost",
4583
- disabled: busy !== "",
4584
- title: invTipRule ? "带 " + invTipRule.n + " 位新用户可得 " + invTipRule.days + " 天 PRO 会员" : "带新用户换会员",
4585
- onClick: function () { setView("invite"); loadInvite(); }
4586
- }, "🎁 带新用户换会员"),
4587
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
4588
- community && community.qrcode
4589
- ? h("button", {
4590
- type: "button",
4591
- className: "dru-btn dru-btn-ghost",
4592
- disabled: busy !== "",
4593
- onClick: function () { setCommOpen(true); }
4594
- }, "💬 加入交流群")
4595
- : null
4596
- ),
4597
- pwdOpen ? renderResetPwdForm(false) : null
4598
- ),
5162
+ // 修改密码表单直接展开在平铺按钮下方(不再藏在折叠体里)
5163
+ pwdOpen ? renderResetPwdForm(false) : null,
4599
5164
  );
4600
5165
  }
4601
5166
 
@@ -4924,7 +5489,10 @@ window.__ModuleLoader__.load({
4924
5489
  // ---------- 反馈视图(独立页,带返回) ----------
4925
5490
  function renderFeedback() {
4926
5491
  return h("div", null,
4927
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回"),
5492
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setIaFbHint(""); setView("home"); } }, "← 返回"),
5493
+ // 带过来的上下文提示(例如从「飞书即将支持 → 告诉我,优先支持」过来时的那句引导);
5494
+ // 离开反馈视图时清空(它在 setView("home") 的返回按钮里一并清掉)。
5495
+ iaFbHint ? h("div", { className: "dru-msg dru-msg-ok" }, iaFbHint) : null,
4928
5496
  h(FeedbackCard, null)
4929
5497
  );
4930
5498
  }
@@ -5206,7 +5774,9 @@ window.__ModuleLoader__.load({
5206
5774
  h("span", { className: "dru-dot " + (e.kind === "ok" ? "dru-dot-on" : "dru-dot-off") }),
5207
5775
  h("span", null, e.text));
5208
5776
  }
5209
- return card("📱 远程访问", [
5777
+ // A1(2026-11-07 业主截图指出):正文里别再重复「远程访问」标题 —— 设置页顶部已有标题,
5778
+ // 弹层标题也已经写了「手机访问」;这里只保留内容卡片。
5779
+ return h("div", { className: "dru-card" }, [
5210
5780
  // 【0.7.10】激活引导置顶:二维码链路的**收尾**(见 renderActivationGuide 的长注释)。
5211
5781
  // 放在状态 chip 之前 = 「设备连上后第一屏看到的就是拿手机做什么」,而不是先读一堆技术状态。
5212
5782
  renderActivationGuide(),
@@ -5385,227 +5955,743 @@ window.__ModuleLoader__.load({
5385
5955
  ]);
5386
5956
  }
5387
5957
 
5388
- // ---------- 主视图 ----------
5389
- /**
5390
- * 主 Tab 的公共属性:视觉态(.active)与语义态(aria-selected)**同源**,不会说一套做一套;
5391
- * tabIndex=0 + Enter/Space → 键盘可操作(主 Tab 是面板的导航,不能只能点鼠标)。
5392
- * ⚠️ 与既有 tab 条(登录/注册、反馈 tab)保持同一形态:可点的 <div>,不改成 <button>
5393
- * (那些 tab 已经是这样;换形态会让同一种控件在面板里出现两种外观与焦点行为)。
5394
- * 无障碍:文字标签**始终在**(选中态另有 border + 底色),不靠颜色单独表意。
5395
- */
5396
- function mainTabProps(active, onPick) {
5397
- return {
5398
- className: "dru-tab" + (active ? " active" : ""),
5399
- role: "tab",
5400
- "aria-selected": active ? "true" : "false",
5401
- tabIndex: 0,
5402
- onClick: onPick,
5403
- onKeyDown: function (e) {
5404
- var k = e && e.key;
5405
- if (k === "Enter" || k === " " || k === "Spacebar") { e.preventDefault(); onPick(); }
5406
- }
5407
- };
5958
+ // ═════════════════════════════════════════════════════════════════════
5959
+ // 🧭 主面板 IA(2026-11-07 业主 UX 重做)
5960
+ //
5961
+ // 旧结构(被业主否掉):「顶部 3 个 tab(云端/自建/微信)」+ 一屏垂直堆叠的卡片 + 大段说明文字,
5962
+ // 且微信通道**登录后才出现** —— 用户原话:「顶部 3 个 tab 页,还要登录之后才有微信通道」
5963
+ // 「卡片式平铺堆叠,很糟糕」「全是文字堆砌,太密集,垂直往下垒」。
5964
+ // 新结构(本段实现):
5965
+ // · 一行状态头(连接状态 pill + 「打开手机端」主按钮);
5966
+ // · 2 列入口网格(≤3 行/卡):手机访问 / 通知与消息通道 / 已授权设备 / 账号;
5967
+ // · 一行文字入口(反馈 / 邀请 / 交流群 / 更多设置),高级设置默认折叠;
5968
+ // · 所有"进入细节"的操作一律走弹层(IADialog:Esc、焦点陷阱、关闭回焦点);
5969
+ // · 原来的「云端/自建」降级成**手机访问弹层里的分段控件**,不再占据顶部导航;
5970
+ // · 登录**不再隐藏任何功能**:未登录时网格与卡片照常显示,卡片文案写「登录后可同步」,
5971
+ // 点开进账号弹层登录,成功后在原地刷新(状态更新即重渲染,无需刷新页面)。
5972
+ // ═════════════════════════════════════════════════════════════════════
5973
+
5974
+ /** 弹层开关:同一时刻只开一个(真实产品里多弹层叠加只会让用户更迷路)。 */
5975
+ function openIAModal(name, why) {
5976
+ setMessage(null);
5977
+ setIaLoginWhy(why ? String(why) : "");
5978
+ setIaModal(name);
5979
+ if (name === "channel") {
5980
+ setIaChStep("list");
5981
+ setImBotsMsg(null);
5982
+ setImBotsFieldErr({});
5983
+ loadImBots().then(function (b) {
5984
+ var w = (b && b.wecom) || {};
5985
+ setImBotsBotId(String(w.botId || ""));
5986
+ setImBotsSecret("");
5987
+ setImBotsChat(String(w.targetChatId || ""));
5988
+ setImBotsSessionId(String(w.sessionId || ""));
5989
+ setImBotsSessionTitle(String(w.sessionTitle || ""));
5990
+ setImBotsWhyOpen(false);
5991
+ setImBotsUrl(String(w.url || ""));
5992
+ });
5993
+ }
5994
+ // 数据加载交给下面的 effect(按"弹层是否打开"触发):
5995
+ // 不在这里直接调 loadAccessKey/loadDevices —— 点击处理里同步改 busy 态会让弹层第一帧
5996
+ // 停在「生成中…」而不是空态,读屏/用例与真实用户看到的都应该是"空态 + 一个按钮"。
5408
5997
  }
5409
- /**
5410
- * 主 Tab 条(☁️ 云端服务 / 🖥 自建服务 / 💬 微信机器人通道)。
5411
- *
5412
- * ⚠️ 必须抽成函数、由 **home 与 wechat 两个视图共用**:这条 strip 原来长在 renderHome() 里,
5413
- * 而切到微信 tab 后 renderHome() 不再渲染 —— 只长在 renderHome() 里就等于「进了微信页
5414
- * 就再也回不去」(面板只有一个 settings.section 栏目,没有别的返回入口)。
5415
- *
5416
- * 选中态:模式两个 tab 只在 view !== "wechat" 时选中;微信 tab 只在 view === "wechat" 时选中。
5417
- * 点模式 tab 时一并把 view **复位成 home**(否则从微信页点「自建服务」会切了 mode 却留在微信页)。
5418
- *
5419
- * 🔒 登录门(用户约定:微信通道**要求已注册并登录**):未登录时**不渲染**这第三个 tab。
5420
- * 登录态来自 st.config(phone / hasLocalKey),登录成功后 st 更新即重新渲染 ——
5421
- * tab 当场出现,**不需要刷新页面**;登出/会话过期则当场消失(view 的兜底见 RemoteControlSection
5422
- * 里那条复位 effect 与底部的 view 分派)。除登录态外,不为任何其它理由隐藏它。
5423
- */
5424
- function renderMainTabs() {
5425
- var isLocal = mode === "local";
5426
- var onWeChat = view === "wechat";
5427
- return h("div", { className: "dru-tabs", style: { marginTop: 4 }, role: "tablist", "aria-label": "远程访问功能切换" },
5428
- h("div", mainTabProps(!onWeChat && !isLocal, function () { setMode("saas"); setView("home"); setMessage(null); }), "☁️ 云端服务"),
5429
- h("div", mainTabProps(!onWeChat && isLocal, function () { setMode("local"); setView("home"); setMessage(null); }), "🖥 自建服务"),
5430
- // 微信绿泡泡:纯装饰(可读名字来自同 tab 的文字标签),所以 aria-hidden;
5431
- // U+FE0E(变体选择符-15)强迫 💬 走**文字字形**,否则系统彩色 emoji 会忽略 color(见 .dru-wx-ico)。
5432
- // 🔒 只有已登录才渲染这个 tab(见上面的登录门说明)。
5433
- loggedIn
5434
- ? h("div", mainTabProps(onWeChat, function () { setView("wechat"); setMessage(null); }),
5435
- h("span", { className: "dru-wx-ico", "aria-hidden": "true" }, "💬\uFE0E"),
5436
- "微信机器人通道")
5437
- : null
5998
+
5999
+ // 打开弹层时按需拉数据(幂等;失败由既有重试/红字逻辑兜底)。
6000
+ // 注:这里**不再**额外拉 access-key / 设备列表 —— 面板已有「已登录 home 停留即取码」的
6001
+ // 既有 effect(含 25s 轮换),弹层打开时那份数据本就已经在页面上。重复拉会让"一次登录"
6002
+ // 打两次 /access-key(生产里浪费签发额度,也和既有回归用例的期望冲突)。
6003
+ function closeIAModal() { setIaModal(null); }
6004
+
6005
+ /** A4/A5:未登录时不要静默失败 —— 直接弹登录弹层,并说明"登录后才能用手机访问";
6006
+ * 登录成功后自动回到「手机访问」流程继续生成访问码(iaAfterLogin)。 */
6007
+ function openMobileAccess() {
6008
+ if (!loggedIn) {
6009
+ setIaAfterLogin("mobile");
6010
+ openIAModal("account", "登录后才能用手机访问");
6011
+ return;
6012
+ }
6013
+ openIAModal("mobile");
6014
+ }
6015
+
6016
+ // 登录成功回跳:把"刚才想做的事"接上(目前只有手机访问这一个意图)。
6017
+ useEffect(function () {
6018
+ if (!loggedIn || iaAfterLogin !== "mobile") return;
6019
+ setIaAfterLogin("");
6020
+ try { openIAModal("mobile"); } catch (e) { /* 忽略 */ }
6021
+ }, [loggedIn, iaAfterLogin]);
6022
+
6023
+ /** 连接状态 → pill 语义(圆点 + 文案;不只靠颜色)。 */
6024
+ function iaConnState() {
6025
+ if (st === null) return { cls: "", text: "连接中…" };
6026
+ if (connInfo && connInfo.locked) return { cls: "is-warn", text: "本机已锁定" };
6027
+ if (!loggedIn) return { cls: "is-warn", text: "未登录" };
6028
+ if (connOnline) return { cls: "is-on", text: "已连接" };
6029
+ if (connPhase === "error") return { cls: "is-warn", text: "连接异常" };
6030
+ if (connPhase && connPhase !== "online") return { cls: "", text: "连接中…" };
6031
+ return { cls: "", text: serviceRunning ? "等待设备连接" : "未连接" };
6032
+ }
6033
+
6034
+ /** 入口卡(每卡 ≤3 行:标题 / 一行状态 / 操作)。 */
6035
+ function iaCard(icon, title, meta, actions) {
6036
+ return h("div", { className: "dru-ia-card" },
6037
+ h("div", { className: "dru-ia-card-top" },
6038
+ h("span", { className: "dru-ia-ico" }, druIcon(icon)),
6039
+ h("div", { className: "dru-ia-card-title" }, title)),
6040
+ h("div", { className: "dru-ia-card-meta" }, meta),
6041
+ h("div", { className: "dru-ia-card-actions" }, actions)
5438
6042
  );
5439
6043
  }
5440
- /** 微信机器人 tab 的内容:直接复用 WeChatBotSection(embedded=省掉外层 settings-section 壳)。 */
5441
- function renderWeChat() {
5442
- return h("div", null,
5443
- renderMainTabs(),
5444
- h(WeChatBotSection, { embedded: true })
6044
+
6045
+ function renderIAHeader() {
6046
+ var cs = iaConnState();
6047
+ return h("div", { className: "dru-ia-head" },
6048
+ h("div", { className: "dru-ia-pill " + cs.cls, role: "status", "aria-live": "polite" },
6049
+ h("span", { className: "dru-ia-pill-dot", "aria-hidden": "true" }),
6050
+ cs.text),
6051
+ h("div", { className: "dru-ia-head-actions" },
6052
+ // A3:登录/退出登录**同一位置**(状态头右侧;账号弹层里不再放退出按钮)。
6053
+ // 未登录显示「登录 / 注册」入口,点击弹账号弹层。
6054
+ loggedIn
6055
+ ? h("button", {
6056
+ type: "button", className: "dru-ia-btn", disabled: busy !== "",
6057
+ title: "退出登录(电脑端后台服务与手机端连接不受影响)",
6058
+ onClick: function () { doLogout(false); }
6059
+ }, "退出登录")
6060
+ : h("button", {
6061
+ type: "button", className: "dru-ia-btn",
6062
+ onClick: function () { openIAModal("account"); }
6063
+ }, "登录 / 注册"),
6064
+ h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: openMobileAccess },
6065
+ druIcon("phone"), "打开手机端")
6066
+ )
5445
6067
  );
5446
6068
  }
5447
- function renderHome() {
5448
- var isLocal = mode === "local";
5449
- return h("div", null,
5450
- // 连接模式主 Tab(云端服务 / 自建服务 / 微信机器人通道;最后一个是 0.6.11 从独立栏目搬进来的)
5451
- renderMainTabs(),
5452
- // 云端 tab:📱 远程访问(一次性扫码访问 + 已授权设备)+ 账号(手机号登录,官方托管)
5453
- !isLocal ? h("div", null,
5454
- renderAccessCard(),
5455
- // 已授权设备:仅登录后展示(登录前不显示,避免空列表/误导)
5456
- loggedIn && st.config.phone ? renderDevicesCard() : null,
5457
- // 📣 公告(0.6.18):插在「🔑 账号」之前 —— 未读公告与"远程访问/账号"同属首屏要紧信息;
5458
- // 没有可显示的内容时整卡不存在(见 renderAnnouncementsCard 的 fail closed 说明)。
5459
- renderAnnouncementsCard(),
5460
- card("🔑 账号",
5461
- st === null
5462
- ? h("div", { className: "dru-hint" }, "正在读取远控状态…")
5463
- : loggedIn && st.config.phone
5464
- ? renderAccount()
5465
- : h("div", null,
5466
- h("div", { className: "dru-tabs" },
5467
- h("div", { className: "dru-tab" + (authTab === "login" ? " active" : ""), onClick: function () { setAuthTab("login"); setPwdOpen(false); setMessage(null); } }, "登录"),
5468
- h("div", { className: "dru-tab" + (authTab === "register" ? " active" : ""), onClick: function () { setAuthTab("register"); setPwdOpen(false); setMessage(null); } }, "注册")
5469
- ),
5470
- authTab === "login"
5471
- ? (pwdOpen
5472
- // 「忘记密码」展开:与账号卡「修改密码」共用同一重置表单(手机号可编辑、预填登录输入)
5473
- ? renderResetPwdForm(true)
5474
- : h("div", null,
5475
- field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
5476
- field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
5477
- field("验证码", h("div", { className: "dru-captcha" },
5478
- input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
5479
- 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 },
5480
- lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
5481
- )),
5482
- h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "login" ? "登录中…" : "登录"),
5483
- // 忘记密码:小字入口,点击展开短信验证码重置表单(不抢占任何官方按钮)
5484
- h("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 } },
5485
- h("button", { type: "button", className: "dru-linkbtn", disabled: busy !== "", onClick: togglePwdForm }, "忘记密码?"))
5486
- )
5487
- )
5488
- : h("div", null,
5489
- field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
5490
- field("密码", input({ type: "password", value: rpass, placeholder: "至少 8 位", autoComplete: "new-password", onChange: function (e) { setRpass(e.target.value); } })),
5491
- field("确认密码", input({ type: "password", value: rpass2, placeholder: "再次输入密码", autoComplete: "new-password", onChange: function (e) { setRpass2(e.target.value); } })),
5492
- field("邀请码(选填)", input({ value: rInvite, placeholder: "邀请你的人的邀请码,如 A8K2M4XQ", autoComplete: "off", onChange: function (e) { setRInvite(e.target.value); } })),
5493
- rcap && field("图形验证码", h("div", { className: "dru-captcha" },
5494
- input({ value: rcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRcapTxt(e.target.value); } }),
5495
- h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("register"); }, dangerouslySetInnerHTML: rcap.svg && rcap.svg.indexOf("<svg") === 0 ? { __html: rcap.svg } : void 0 },
5496
- rcap.svg && rcap.svg.indexOf("<svg") !== 0 ? rcap.svg : null)
5497
- )),
5498
- field("短信验证码", h("div", { className: "dru-captcha" },
5499
- input({ value: rsms, placeholder: "6 位验证码", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRsms(e.target.value); } }),
5500
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: sendRegSms }, rsmsBtn)
5501
- )),
5502
- h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doRegister }, busy === "register" ? "注册中…" : "注册")
5503
- ),
5504
- message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
5505
- )
5506
- )
5507
- ) : card("🔌 自建服务",
5508
- st && st.config && st.config.mode === "local"
5509
- ? h("div", null,
5510
- h("div", { className: "dru-status-line" },
5511
- h("span", { className: "dru-dot dru-dot-on" }),
5512
- h("span", null, "已连接 " + (st.config.selfHostUrl || "自建服务"))
5513
- ),
5514
- h("div", { className: "dru-hint", style: { marginTop: 6 } },
5515
- "当前使用自建服务(本地认证,无需手机号账号)。切换到云端:在上方「云端服务」标签用手机号账号登录即可。")
5516
- )
5517
- : h("div", null,
5518
- field("服务器地址", input({ value: selfHost, placeholder: "my.example.com:13443", onChange: function (e) { setSelfHost(e.target.value); } })),
5519
- field("访问密钥", input({ type: "password", value: localKey, placeholder: "自建服务的访问密钥", autoComplete: "off", onChange: function (e) { setLocalKey(e.target.value); } })),
5520
- h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: saveLocal }, busy === "local" ? "保存中…" : "切换到自建服务"),
5521
- h("div", { className: "dru-hint", style: { marginTop: 6 } },
5522
- "服务器地址由你自行部署决定:填写你自建的 dsh-remote 服务地址与访问密钥即可(不是上面的云端地址)。部署方法见 README「自建部署」一节;切回云端随时可登录恢复。")
5523
- )
5524
- ),
5525
- // 后台服务(原「Bridge 服务」:面向非技术用户统一叫「后台服务」,卡内配一句人话解释)
5526
- card("🖥 后台服务",
5527
- h("div", { className: "dru-status-line" },
5528
- h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
5529
- h("span", null, st ? serviceStateText : "查询中…"),
5530
- launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
5531
- ),
5532
- h("div", { className: "dru-hint", style: { marginTop: 6 } },
5533
- "「后台服务」=装在这台电脑上、负责把手机和电脑连起来的小程序(内部名 bridge)。面板会自动安装、自动启动,一般不需要你手动操作;只有状态一直停在「已停止」时才需要点下面的按钮。"),
5534
- // 自动连接进度(登录后由面板 2.5s 短轮询自动推进):让用户在 bridge 卡也能一眼看到
5535
- // 「到底走到哪一步了」——进程在跑≠能用,注册到中继才算。
5536
- connInfo && connPhase && connPhase !== "no_account"
5537
- ? h("div", { className: "dru-hint", style: { marginTop: 6 } },
5538
- "自动连接进度:" + (connInfo.locked ? "本机已被锁定(当前套餐额度已满,升级或解绑后自动恢复)" : connText))
5539
- : null,
5540
- h("div", { className: "dru-actions", style: { marginTop: 10 } },
6069
+
6070
+ /** 入口网格:4 张卡(2×2),每卡只讲一件事。 */
6071
+ function renderIAGrid() {
6072
+ var w = (imBots && imBots.wecom) || {};
6073
+ var ws = (imBots && imBots.status) || {};
6074
+ var chMeta;
6075
+ if (imBots === null) chMeta = "企业微信机器人 · 读取中…";
6076
+ else if (ws.connected) chMeta = "企业微信机器人 · 已连接";
6077
+ else if (w.enabled || w.hasSecret) chMeta = "企业微信机器人 · 已配置(未连接)";
6078
+ else chMeta = "企业微信机器人 · 未配置";
6079
+
6080
+ var devCount = devSessions === null ? null : devSessions.length;
6081
+ var devMeta = !loggedIn
6082
+ ? "登录后可同步设备"
6083
+ : (devCount === null ? "尚未加载" : (devCount + " 台已授权设备"));
6084
+
6085
+ var acctMeta = st === null ? "读取中…" : (loggedIn ? (st.config && st.config.phone ? st.config.phone : "已登录") : "未登录(登录后可同步)");
6086
+
6087
+ var remain = 0;
6088
+ if (akey) { var expMs = toMs(akey.expires_at); if (expMs) remain = expMs - nowTick; }
6089
+ var mobileMeta = !loggedIn
6090
+ ? "登录后即可生成一次性访问码"
6091
+ : (akey && akey.url && remain > 0 ? ("访问码有效 · " + fmtRemain(remain) + "后过期") : "尚未生成访问码");
6092
+
6093
+ return h("div", { className: "dru-ia-grid" },
6094
+ iaCard("phone", "手机访问", mobileMeta,
6095
+ h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: openMobileAccess },
6096
+ loggedIn ? (akey && akey.url ? "查看访问码" : "生成访问码") : "登录并生成访问码")),
6097
+ iaCard("bell", "通知与消息通道", chMeta,
6098
+ h("button", { type: "button", className: "dru-ia-btn", onClick: function () { openIAModal("channel"); } }, "设置")),
6099
+ iaCard("devices", "已授权设备", devMeta,
6100
+ h("button", { type: "button", className: "dru-ia-btn", onClick: function () { openIAModal("devices"); } }, "管理")),
6101
+ iaCard("user", "账号", acctMeta,
6102
+ h("button", { type: "button", className: "dru-ia-btn", onClick: function () { openIAModal("account"); } },
6103
+ loggedIn ? "我的信息" : "登录 / 注册"))
6104
+ );
6105
+ }
6106
+
6107
+ /** 一行文字入口:低频功能收在这里,主界面保持清爽。 */
6108
+ function renderIALinks() {
6109
+ var unread = annState.ok && annState.bubble && typeof annState.unread === "number" ? annState.unread : 0;
6110
+ return h("div", { className: "dru-ia-links" },
6111
+ h("button", { type: "button", className: "dru-ia-link", onClick: function () { setView("feedback"); } }, druIcon("chat"), "用户反馈"),
6112
+ h("button", { type: "button", className: "dru-ia-link", onClick: function () { setView("invite"); loadInvite(); } }, "邀请有礼"),
6113
+ community && community.qrcode
6114
+ ? h("button", { type: "button", className: "dru-ia-link", onClick: function () { setCommOpen(true); } }, "加入交流群")
6115
+ : null,
6116
+ h("button", {
6117
+ type: "button", className: "dru-ia-link", "aria-expanded": moreOpen ? "true" : "false",
6118
+ onClick: function () { setMoreOpen(!moreOpen); }
6119
+ }, moreOpen ? "收起高级设置" : "更多设置", druIcon(moreOpen ? "left" : "right")),
6120
+ unread > 0 ? h("span", { className: "dru-hint", style: { marginLeft: "auto" } }, "有 " + unread + " 条未读公告") : null
6121
+ );
6122
+ }
6123
+
6124
+ /** 后台服务卡(从旧 renderHome 搬出来;文案压缩,不再解释内部实现细节)。 */
6125
+ function renderBridgeCard() {
6126
+ var autostartText = st ? (st.service && (st.service.plistExists || st.service.serviceManager === "detached")
6127
+ ? (st.service.serviceManager === "detached"
6128
+ ? (st.service.autostartTask
6129
+ ? "自启动:已注册登录任务(Windows 任务计划程序 dsh-remote-bridge)"
6130
+ : "自启动:未注册(由面板在 dsh web 启动时自动拉起)")
6131
+ : "自启动服务已安装")
6132
+ : "自启动服务未安装(启动时自动创建)") : "";
6133
+ var deviceText = st && st.config && st.config.deviceId ? ("设备 ID:" + st.config.deviceId) : "设备 ID:生成中";
6134
+ return h("div", { className: "dru-card" },
6135
+ h("h3", null, "后台服务"),
6136
+ // A6:业主点名要这句提示 —— 显眼但不刺眼(浅色提示条 + 图标,不占满屏、不闪)
6137
+ h("div", { className: "dru-bridge-keep", role: "note" },
6138
+ druIcon("alert", 14),
6139
+ h("span", null, "保持在线,才能用手机端远程控制这台电脑,请不要关闭。")),
6140
+ // A6:横向紧凑布局(2 列网格):状态 / 启停按钮 / 自启动 / 设备 ID 不再一列排队
6141
+ h("div", { className: "dru-bridge-grid" },
6142
+ h("div", { className: "dru-bridge-cell" },
6143
+ h("div", { className: "dru-status-line" },
6144
+ h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
6145
+ h("span", null, st ? serviceStateText : "查询中…"),
6146
+ launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null)),
6147
+ h("div", { className: "dru-bridge-cell dru-bridge-cell-action" },
5541
6148
  !serviceRunning
5542
6149
  ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动后台服务")
5543
- : h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止后台服务")
5544
- ),
5545
- h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
5546
- h("div", { className: "dru-meta" }, st ? (st.service && (st.service.plistExists || st.service.serviceManager === "detached")
5547
- ? (st.service.serviceManager === "detached"
5548
- ? (st.service.autostartTask
5549
- ? "自启动:已注册登录任务(任务计划程序 dsh-remote-bridge)"
5550
- : "自启动:未注册登录任务(由面板在 dsh web 启动时自动拉起后台服务)")
5551
- : "自启动服务已安装")
5552
- : "自启动服务未安装(启动时自动创建)") : ""),
5553
- // 0.6.2:把「运行环境缺失 / launchd 崩溃循环」如实告诉用户,而不是显示「运行中」
5554
- st && !serviceRuntimeReady
5555
- ? h("div", { className: "dru-hint", style: { marginTop: 8 } },
5556
- "🛠 在插件市场搜索「dsh-remote-web」装上的只是「面板插件」,这台电脑上的后台服务需要额外补装——已自动在后台安装,装完会自动启动,不用做任何操作。若超过几分钟仍未变成「运行中」,点上方版本卡的「一键更新」手动补全即可。")
5557
- : serviceCrashing
5558
- ? h("div", { className: "dru-hint", style: { marginTop: 8 } },
5559
- "⚠️ 上一次启动失败(自启动入口失效),已自动清理失效自启动项并重新补装运行环境。稍候会自动恢复;仍失败请点「一键更新」。")
5560
- : null,
5561
- st && st.service && st.service.bindError
5562
- ? (function (be) {
5563
- var isLocked = be.code === "device_locked" || be.code === "device_limit_exceeded";
5564
- var isBindCap = be.code === "device_bind_cap";
5565
- var beLimit = (be.limit === 0 || be.limit) ? be.limit : null;
5566
- return h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
5567
- (isLocked ? "🔒 本机已被锁定:" : "⚠️ 设备注册失败:") + (be.message || "未说明原因"),
5568
- h("div", { className: "dru-hint", style: { marginTop: 4 } },
5569
- isLocked
5570
- // 契约 §8.1 的新语义:不是"解绑旧设备才能登录",而是"超额锁定 → 升级或解绑,本机自动解锁"
5571
- ? lockedPanelDetail(beLimit)
5572
- : isBindCap
5573
- // device_bind_cap = 防滥用硬上限(bridge 退出码 5):守护按指数退避重试,不是无限重试
5574
- ? "已绑定设备数达到上限:请在手机端「管理电脑」解绑不再使用的设备;解绑后本机会自动恢复(后台守护会按指数退避重试,无需反复点按钮)。"
5575
- : "请确认网络与账号状态后重试;仍未解决可点下方「彻底卸载」后重新安装。")
5576
- );
5577
- })(st.service.bindError)
5578
- : null
6150
+ : h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止后台服务")),
6151
+ h("div", { className: "dru-bridge-cell dru-meta" }, autostartText),
6152
+ h("div", { className: "dru-bridge-cell dru-meta" }, deviceText)
5579
6153
  ),
5580
- // 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
6154
+ connInfo && connPhase && connPhase !== "no_account"
6155
+ ? h("div", { className: "dru-hint", style: { marginTop: 8 } },
6156
+ "连接进度:" + (connInfo.locked ? "本机已被锁定(额度已满,升级或解绑后自动恢复)" : connText))
6157
+ : null,
6158
+ st && !serviceRuntimeReady
6159
+ ? h("div", { className: "dru-hint", style: { marginTop: 8 } }, "运行环境正在自动补装,装完会自动启动,不需要你操作。")
6160
+ : serviceCrashing
6161
+ ? h("div", { className: "dru-hint", style: { marginTop: 8 } }, "上次启动失败,已自动修复并重试;若持续失败请到「版本与更新」点一键更新。")
6162
+ : null,
6163
+ st && st.service && st.service.bindError
6164
+ ? (function (be) {
6165
+ var isLocked = be.code === "device_locked" || be.code === "device_limit_exceeded";
6166
+ var beLimit = (be.limit === 0 || be.limit) ? be.limit : null;
6167
+ var isBindCap = be.code === "device_bind_cap";
6168
+ return h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
6169
+ (isLocked ? "本机已被锁定:" : "设备注册失败:") + (be.message || "未说明原因"),
6170
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
6171
+ isLocked
6172
+ ? lockedPanelDetail(beLimit)
6173
+ : isBindCap
6174
+ ? "已绑定设备数达到上限(device_bind_cap):请在手机端「管理电脑」解绑不再使用的设备;解绑后本机会自动恢复(后台守护按指数退避重试,无需反复点按钮)。"
6175
+ : "请确认网络与账号状态后重试;仍未解决可点「彻底卸载」后重新安装。"));
6176
+ })(st.service.bindError)
6177
+ : null
6178
+ );
6179
+ }
6180
+
6181
+ /** 「更多设置」:后台服务 / 版本与更新 / 关于。低频但必须有出口,默认折叠。 */
6182
+ function renderMoreSettings() {
6183
+ return h("div", { className: "dru-ia" },
6184
+ renderBridgeCard(),
5581
6185
  h(SelfManageCard, null),
5582
- // 📖 关于 dsh-remote(**挪到最底**,0.6.18 面板重排):
5583
- // 原来它夹在「🖥 后台服务」与「🔄 版本与更新」之间,六段技术说明把
5584
- // "需要你动一下"(版本/重启)和"日常会用的入口"(账号/微信/公告)全挤到了它上面 ——
5585
- // 用户为了找版本卡要先滚过一整屏说明文字。关于是**背景阅读**,天然属于最后一屏。
5586
- // ⚠️ 整块必须留在 renderHome 的返回树里,**不能**塞进 SelfManageCard 的返回树 ——
5587
- // 那会让它跟着那个组件的状态生命周期一起消失/重建。
5588
- card("📖 关于 dsh-remote", [
5589
- // 一句话定位(面向非技术用户先讲清楚"这是什么"),下面 4 条为既有技术说明(文案受源码契约锁定)
5590
- h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 一句话:用手机随时接管这台电脑上的 DeepSeek Harness——装上即得专属加密地址,人在外面也能全功能操作(免内网穿透、免公网 IP、全程加密)。"),
5591
- h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
5592
- h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
5593
- h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表。"),
5594
- h("div", { className: "dru-hint" }, "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。"),
5595
- // 匿名装机统计(隐私披露):开源项目必须把「采什么 / 不采什么 / 怎么关」写在用户看得到的地方。
5596
- // 只保留结论式的说明句:不再单列「隐私说明: README… · docs/telemetry.md」那一行文档链接
5597
- // (面板里堆文档链接既占地方又不像产品文案;完整字段清单在仓库 docs/telemetry.md)。
6186
+ h("div", { className: "dru-card" },
6187
+ h("h3", null, "关于 dsh-remote-web"),
6188
+ h("div", { className: "dru-hint" }, "用手机或另一台电脑随时接管这台电脑上的 DeepSeek Harness:装上即得专属加密地址,免公网 IP、免内网穿透,全程加密。"),
5598
6189
  h("div", { className: "dru-hint", style: { marginTop: 6 } },
5599
- "📊 匿名装机统计:只上报「装机/连接是否成功」这类事件(安装开始与失败原因、后台服务是否注册成功、是否首次远程打通)——"
6190
+ "匿名装机统计:只上报「装机 / 连接是否成功」这类事件(安装开始与失败原因、后台服务是否注册成功、是否首次远程打通)——"
5600
6191
  + "不含任何账号、手机号、会话或文件内容、主机名、路径、设备指纹与 IP;标识是本机随机 ID(重装即变)。"
5601
- + "用环境变量 DSH_REMOTE_TELEMETRY=0 可完全关闭(不生成 ID、不发任何请求)。")
5602
- // ⚠️ 「💬 加入交流群」按钮已搬到「🔑 账号」卡的功能区(与「💬 用户反馈」并列)——
5603
- // 那里才是"有事想找人问"时会看的地方;这里只剩阅读性内容。
5604
- ]),
6192
+ + "用环境变量 DSH_REMOTE_TELEMETRY=0 可完全关闭(不生成 ID、不发任何请求)。"),
6193
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
6194
+ "端到端加密:手机与电脑之间的消息内容用账号密码派生的密钥加密,密钥与密码不落服务端(中继只可见路径 / 大小 / 时间)。")
6195
+ )
6196
+ );
6197
+ }
6198
+
6199
+ // ── 弹层内容 ──────────────────────────────────────────────────────────
6200
+ /** 自建服务表单(从旧 home 的自建卡搬出来,放进「手机访问」弹层的分段里)。 */
6201
+ function renderSelfHostBody() {
6202
+ if (st && st.config && st.config.mode === "local") {
6203
+ return h("div", null,
6204
+ h("div", { className: "dru-ia-state is-ok" }, druIcon("check"), "已连接 " + (st.config.selfHostUrl || "自建服务")),
6205
+ h("div", { className: "dru-hint", style: { marginTop: 8 } }, "当前使用自建服务(本地认证,无需手机号账号)。切回云端:切到「云端服务」分段用手机号登录即可。")
6206
+ );
6207
+ }
6208
+ return h("div", null,
6209
+ field("服务器地址", input({ value: selfHost, placeholder: "my.example.com:13443", onChange: function (e) { setSelfHost(e.target.value); } })),
6210
+ field("访问密钥", input({ type: "password", value: localKey, placeholder: "自建服务的访问密钥", autoComplete: "off", onChange: function (e) { setLocalKey(e.target.value); } })),
6211
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%", marginTop: 8 }, disabled: busy !== "", onClick: saveLocal }, busy === "local" ? "保存中…" : "切换到自建服务"),
6212
+ h("div", { className: "dru-hint", style: { marginTop: 8 } }, "服务器地址由你的自建部署决定(不是上面的云端地址),填地址与访问密钥即可。")
6213
+ );
6214
+ }
6215
+
6216
+ /** 账号弹层内容:已登录 → 复用既有 renderAccount();未登录 → 登录/注册表单(原样搬过来)。 */
6217
+ function renderAccountBody() {
6218
+ if (st === null) return h("div", { className: "dru-hint" }, "正在读取远控状态…");
6219
+ if (loggedIn && st.config && st.config.phone) return renderAccount();
6220
+ return h("div", null,
6221
+ h("div", { className: "dru-ia-seg", role: "tablist", "aria-label": "登录或注册" },
6222
+ h("button", {
6223
+ type: "button", role: "tab", "aria-selected": authTab === "login" ? "true" : "false",
6224
+ className: "dru-ia-seg-btn" + (authTab === "login" ? " is-active" : ""),
6225
+ onClick: function () { setAuthTab("login"); setPwdOpen(false); setMessage(null); }
6226
+ }, "登录"),
6227
+ h("button", {
6228
+ type: "button", role: "tab", "aria-selected": authTab === "register" ? "true" : "false",
6229
+ className: "dru-ia-seg-btn" + (authTab === "register" ? " is-active" : ""),
6230
+ onClick: function () { setAuthTab("register"); setPwdOpen(false); setMessage(null); }
6231
+ }, "注册")
6232
+ ),
6233
+ authTab === "login"
6234
+ ? (pwdOpen
6235
+ ? renderResetPwdForm(true)
6236
+ : h("div", null,
6237
+ field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
6238
+ field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
6239
+ field("验证码", h("div", { className: "dru-captcha" },
6240
+ input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
6241
+ 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 },
6242
+ lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
6243
+ )),
6244
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "login" ? "登录中…" : "登录"),
6245
+ h("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 } },
6246
+ h("button", { type: "button", className: "dru-linkbtn", disabled: busy !== "", onClick: togglePwdForm }, "忘记密码?"))
6247
+ ))
6248
+ : h("div", null,
6249
+ field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
6250
+ field("密码", input({ type: "password", value: rpass, placeholder: "至少 8 位", autoComplete: "new-password", onChange: function (e) { setRpass(e.target.value); } })),
6251
+ field("确认密码", input({ type: "password", value: rpass2, placeholder: "再次输入密码", autoComplete: "new-password", onChange: function (e) { setRpass2(e.target.value); } })),
6252
+ field("邀请码(选填)", input({ value: rInvite, placeholder: "邀请你的人的邀请码,如 A8K2M4XQ", autoComplete: "off", onChange: function (e) { setRInvite(e.target.value); } })),
6253
+ rcap && field("图形验证码", h("div", { className: "dru-captcha" },
6254
+ input({ value: rcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRcapTxt(e.target.value); } }),
6255
+ h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("register"); }, dangerouslySetInnerHTML: rcap.svg && rcap.svg.indexOf("<svg") === 0 ? { __html: rcap.svg } : void 0 },
6256
+ rcap.svg && rcap.svg.indexOf("<svg") !== 0 ? rcap.svg : null)
6257
+ )),
6258
+ field("短信验证码", h("div", { className: "dru-captcha" },
6259
+ input({ value: rsms, placeholder: "6 位验证码", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRsms(e.target.value); } }),
6260
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: sendRegSms }, rsmsBtn)
6261
+ )),
6262
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doRegister }, busy === "register" ? "注册中…" : "注册")
6263
+ ),
5605
6264
  message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
5606
6265
  );
5607
6266
  }
5608
6267
 
6268
+ /** 手机访问弹层:二维码/链接/倒计时(复用既有 renderAccessCard)+ 云端/自建分段。 */
6269
+ function renderMobileModal() {
6270
+ if (iaModal !== "mobile") return null;
6271
+ var isLocal = mode === "local";
6272
+ return iaDialog(Object.assign({}, {
6273
+ title: "手机访问",
6274
+ sub: "手机扫码或在浏览器里打开链接,即可远程使用这台电脑上的 DSH",
6275
+ onClose: closeIAModal,
6276
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "完成")
6277
+ }, { children: [h("div", { className: "dru-ia-seg", role: "tablist", "aria-label": "接入方式" },
6278
+ h("button", {
6279
+ type: "button", role: "tab", "aria-selected": !isLocal ? "true" : "false",
6280
+ className: "dru-ia-seg-btn" + (!isLocal ? " is-active" : ""),
6281
+ onClick: function () { setMode("saas"); setMessage(null); }
6282
+ }, "云端服务"),
6283
+ h("button", {
6284
+ type: "button", role: "tab", "aria-selected": isLocal ? "true" : "false",
6285
+ className: "dru-ia-seg-btn" + (isLocal ? " is-active" : ""),
6286
+ onClick: function () { setMode("local"); setMessage(null); }
6287
+ }, "自建服务")
6288
+ ),
6289
+ isLocal ? renderSelfHostBody() : renderAccessCard(),
6290
+ // 已授权设备概览(与入口网格里的设备卡同源):扫码/发链接之后「哪台手机已授权」是同一件事的下半段。
6291
+ (!isLocal && loggedIn)
6292
+ ? h("div", null,
6293
+ h("div", { className: "dru-ia-card-actions", style: { marginTop: 4 } },
6294
+ h("button", {
6295
+ type: "button", className: "dru-ia-btn",
6296
+ onClick: function () { setIaModal("devices"); }
6297
+ }, "已授权设备 " + (devSessions === null ? "…" : devSessions.length) + "(管理)")),
6298
+ devMsg ? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 8 } }, devMsg.text) : null)
6299
+ : null] }));
6300
+ }
6301
+
6302
+ /** 通道列表行(弹层第一步)。 */
6303
+ function iaChannelRow(icon, title, sub, go, goText) {
6304
+ return h("button", { type: "button", className: "dru-ia-row", onClick: go },
6305
+ h("span", { className: "dru-ia-ico" }, druIcon(icon)),
6306
+ h("span", { className: "dru-ia-row-main" },
6307
+ h("span", { className: "dru-ia-row-title" }, title),
6308
+ h("span", { className: "dru-ia-row-sub" }, sub)),
6309
+ h("span", { className: "dru-ia-row-go" }, goText, druIcon("right"))
6310
+ );
6311
+ }
6312
+
6313
+ /** 企微通道档位条(2026-11-07 桥侧会员判定):取不到 tier 就**不渲染**,绝不猜。 */
6314
+ function iaTierBar(ws) {
6315
+ try {
6316
+ var tier = String((ws && ws.tier) || "");
6317
+ if (tier !== "free" && tier !== "pro" && tier !== "pro_max") return null;
6318
+ if (tier === "free") {
6319
+ var remaining = (typeof ws.remainingMessages === "number" && isFinite(ws.remainingMessages))
6320
+ ? ws.remainingMessages : null;
6321
+ var perMonth = Number(ws.messagesPerMonth) > 0 ? Number(ws.messagesPerMonth) : 0;
6322
+ var used = ws.usage && typeof ws.usage.messages === "number" ? ws.usage.messages : null;
6323
+ // 只有「桥明确给了余量」或「每月上限+本月已用」才显示数字;否则不瞎猜。
6324
+ if (remaining === null && perMonth && used !== null) remaining = Math.max(0, perMonth - used);
6325
+ var txt = "免费档";
6326
+ if (remaining !== null) txt += " · 本月剩余 " + remaining + " 条指令";
6327
+ if (perMonth) txt += "(每月 " + perMonth + " 条)";
6328
+ return h("div", { className: "dru-ia-tier", role: "status" },
6329
+ h("span", { className: "dru-ia-tier-name" }, txt),
6330
+ h("a", {
6331
+ className: "dru-ia-tier-link", href: "/app/promo", target: "_blank", rel: "noopener"
6332
+ }, "升级 Pro 不限量"));
6333
+ }
6334
+ return h("div", { className: "dru-ia-tier is-member", role: "status" },
6335
+ h("span", { className: "dru-ia-tier-name" }, "会员档 · 指令不限"));
6336
+ } catch (e) { return null; }
6337
+ }
6338
+
6339
+ /** 统一的表单字段结构(label + 控件 + 错误位;错误就近显示)。 */
6340
+ function iaField(label, htmlFor, control, errText, hintText, extraHintId) {
6341
+ // 同时带 dru-field 语义类名(任务里约定的字段结构名),样式仍由 .dru-ia-field 承担。
6342
+ return h("div", { className: "dru-ia-field dru-field" },
6343
+ h("label", { className: "dru-ia-label", htmlFor: htmlFor }, label),
6344
+ control,
6345
+ errText ? h("div", { id: extraHintId || undefined, className: "dru-ia-err", role: "alert" }, errText) : null,
6346
+ hintText ? h("div", { className: "dru-ia-hint" }, hintText) : null
6347
+ );
6348
+ }
6349
+ /** 会话 id 的短形态(下拉里标题可能重复,短 id 帮用户区分)。 */
6350
+ function iaShortId(id) {
6351
+ var s2 = String(id || "");
6352
+ return s2.length > 8 ? (s2.slice(0, 8) + "…") : s2;
6353
+ }
6354
+ /** 会话标题截断(业主:下拉文案别撑爆一行)。 */
6355
+ function iaTruncTitle(t) {
6356
+ var s2 = String(t || "").trim();
6357
+ if (!s2) return "未命名会话";
6358
+ return s2.length > 24 ? (s2.slice(0, 24) + "…") : s2;
6359
+ }
6360
+ /** 时间戳 → 本地 HH:MM(拿不到就空串,绝不显示 Invalid Date)。 */
6361
+ function iaHHMM(ms) {
6362
+ var n = Number(ms);
6363
+ if (!Number.isFinite(n) || n <= 0) return "";
6364
+ try {
6365
+ var d = new Date(n);
6366
+ var p = function (v) { return String(v).padStart(2, "0"); };
6367
+ return p(d.getHours()) + ":" + p(d.getMinutes());
6368
+ } catch (e) { return ""; }
6369
+ }
6370
+ /** 当前实际转发目标的一句话(绑定会话:<title or 跟随活跃>)。 */
6371
+ function iaBoundSessionText() {
6372
+ var ws = (imBots && imBots.status) || {};
6373
+ var list = Array.isArray(ws.sessions) ? ws.sessions : [];
6374
+ var bound = String(ws.boundSessionId || "");
6375
+ if (bound) {
6376
+ var hit = null;
6377
+ for (var i = 0; i < list.length; i++) { if (list[i] && list[i].id === bound) { hit = list[i]; break; } }
6378
+ return iaTruncTitle((hit && hit.title) || bound);
6379
+ }
6380
+ var activeTitle = String(ws.activeSessionTitle || "");
6381
+ return activeTitle ? ("跟随活跃(" + iaTruncTitle(activeTitle) + ")") : "跟随活跃会话";
6382
+ }
6383
+
6384
+ /** 企业微信配置步骤:会话绑定 / Bot ID / Secret(不回显)/ 接收会话 ID / 保存并连接 / 测试。 */
6385
+ function renderWecomStep(close) {
6386
+ var w = (imBots && imBots.wecom) || {};
6387
+ var ws = (imBots && imBots.status) || {};
6388
+ var fieldErr = imBotsFieldErr || {};
6389
+ var sessions = Array.isArray(ws.sessions) ? ws.sessions : [];
6390
+ var stateLine = null;
6391
+ if (imBotsBusy === "test") {
6392
+ stateLine = h("div", { className: "dru-ia-state", role: "status", "aria-live": "polite" }, h("span", { className: "dru-spin" }), "正在连接企业微信…(最多 10 秒)");
6393
+ } else if (imBotsMsg) {
6394
+ stateLine = h("div", {
6395
+ className: "dru-ia-state " + (imBotsMsg.kind === "ok" ? "is-ok" : imBotsMsg.kind === "warn" ? "is-warn" : "is-err"),
6396
+ role: "status", "aria-live": "polite"
6397
+ }, imBotsMsg.text);
6398
+ } else if (ws.connected) {
6399
+ stateLine = h("div", { className: "dru-ia-state is-ok", role: "status" }, druIcon("check"), "已连接" + (ws.authenticated ? "(鉴权通过)" : ""));
6400
+ } else if (w.enabled || w.hasSecret) {
6401
+ stateLine = h("div", { className: "dru-ia-state", role: "status" }, "已保存,等待连接…" + (ws.lastError ? ":" + ws.lastError : ""));
6402
+ }
6403
+
6404
+ // 会话下拉:默认「跟随当前活跃会话」+ 状态文件里的会话(只读桥写的数据,不自己拉会话列表)
6405
+ var sessionOptions = [h("option", { value: "" }, "跟随当前活跃会话(默认)")];
6406
+ for (var i = 0; i < sessions.length; i++) {
6407
+ var it = sessions[i] || {};
6408
+ if (!it.id) continue;
6409
+ sessionOptions.push(h("option", { value: String(it.id) },
6410
+ iaTruncTitle(it.title) + " · " + iaShortId(it.id)));
6411
+ }
6412
+ // 免费档:桥会忽略 sessionId(bindingGated=true)→ 下拉置灰 + 「会员功能」徽章 + 悬停说明。
6413
+ var sessionGated = !!ws.bindingGated;
6414
+ var sessionIgnored = sessionGated && !!String(imBotsSessionId || "").trim();
6415
+ var sessionSelect = h("select", {
6416
+ id: "dru-wecom-session", className: "dru-ia-input", value: sessionGated ? "" : String(imBotsSessionId || ""),
6417
+ disabled: sessionGated ? true : undefined,
6418
+ "aria-disabled": sessionGated ? "true" : undefined,
6419
+ title: sessionGated ? "升级后可固定转接会话" : undefined,
6420
+ "aria-invalid": fieldErr.session ? "true" : "false",
6421
+ "aria-describedby": fieldErr.session ? "dru-wecom-session-err" : undefined,
6422
+ onChange: function (e) {
6423
+ var sid = String(e.target.value || "");
6424
+ var title = "";
6425
+ for (var k = 0; k < sessions.length; k++) {
6426
+ if (sessions[k] && String(sessions[k].id) === sid) { title = String(sessions[k].title || ""); break; }
6427
+ }
6428
+ setImBotsSessionId(sid);
6429
+ setImBotsSessionTitle(sid ? title : "");
6430
+ }
6431
+ }, sessionOptions);
6432
+
6433
+ return iaDialog(Object.assign({}, {
6434
+ title: "企业微信智能机器人",
6435
+ sub: "填 Bot ID 与 Secret 即可;Secret 只保存在本机,不会回显",
6436
+ onClose: close,
6437
+ foot: [
6438
+ h("button", {
6439
+ type: "button", className: "dru-ia-btn is-primary", disabled: imBotsBusy !== "",
6440
+ onClick: function () { saveImBots(true); }
6441
+ }, imBotsBusy === "test" ? "连接中…" : "保存并连接"),
6442
+ h("button", { type: "button", className: "dru-ia-btn", disabled: imBotsBusy !== "", onClick: function () { saveImBots(false); } },
6443
+ imBotsBusy === "save" ? "保存中…" : "仅保存"),
6444
+ h("button", { type: "button", className: "dru-ia-btn is-ghost", disabled: imBotsBusy !== "", onClick: function () { testImBots(); } }, "测试"),
6445
+ // 次要动作:发一张演示卡(按钮不会真的执行),不抢主操作。
6446
+ h("button", {
6447
+ type: "button", className: "dru-ia-btn is-ghost", disabled: imBotsBusy !== "",
6448
+ onClick: function () { sendPreviewCard(); }
6449
+ }, imBotsBusy === "preview" ? "发送中…" : "发送预览卡片")
6450
+ ]
6451
+ }, { children: [
6452
+ // 2026-11-07:档位条(免费档显示本月剩余指令 + 升级入口;会员档显示不限量;取不到不渲染)
6453
+ iaTierBar(ws),
6454
+ // 当前实际转发目标:一行说清楚(状态不只靠颜色/图标)
6455
+ h("div", { className: "dru-ia-state" }, druIcon("chat"), "绑定会话:" + iaBoundSessionText()),
6456
+ // 没有接收方时如实提示(不拦着点:桥会记日志跳过,用户自己能看到结果)
6457
+ (!ws.boundChatId && !String(imBotsChat || "").trim() && !String(w.targetChatId || "").trim())
6458
+ ? h("div", { className: "dru-ia-hint" }, "还没有接收方:先在企微里给机器人发一句话完成绑定(现在点也能发,桥会跳过)。")
6459
+ : null,
6460
+ iaHHMM(ws.lastPreviewCardAt)
6461
+ ? h("div", { className: "dru-ia-hint" }, "上次预览:" + iaHHMM(ws.lastPreviewCardAt))
6462
+ : null,
6463
+ iaField(
6464
+ sessionGated
6465
+ ? h("span", { style: { display: "inline-flex", alignItems: "center", gap: 6 } },
6466
+ "转接到哪个会话",
6467
+ h("span", { className: "dru-ia-badge", title: "升级后可固定转接会话" }, "会员功能"))
6468
+ : "转接到哪个会话",
6469
+ "dru-wecom-session", sessionSelect,
6470
+ fieldErr.session || "",
6471
+ sessionGated
6472
+ ? (sessionIgnored
6473
+ ? "当前跟随活跃会话(免费档不能固定转接:升级后可固定转接会话)。"
6474
+ : "免费档不能固定转接会话:当前跟随活跃会话,升级后可固定。")
6475
+ : (sessions.length
6476
+ ? "给机器人发消息时,转发到这个会话;默认跟随当前活跃会话。"
6477
+ : "还没有会话,去创建一个任务后这里会出现。"),
6478
+ "dru-wecom-session-err"),
6479
+ iaField("Bot ID", "dru-wecom-bot",
6480
+ h("input", {
6481
+ id: "dru-wecom-bot", className: "dru-ia-input", type: "text", value: imBotsBotId,
6482
+ placeholder: "aibxxxxxxxx", autoComplete: "off", spellCheck: false,
6483
+ "aria-invalid": fieldErr.botId ? "true" : "false",
6484
+ "aria-describedby": fieldErr.botId ? "dru-wecom-bot-err" : undefined,
6485
+ onChange: function (e) { setImBotsBotId(e.target.value); }
6486
+ }),
6487
+ fieldErr.botId || "", null, "dru-wecom-bot-err"),
6488
+ iaField("Secret", "dru-wecom-secret",
6489
+ h("div", { className: "dru-ia-input-row" },
6490
+ h("input", {
6491
+ id: "dru-wecom-secret", className: "dru-ia-input",
6492
+ type: imBotsShowSecret ? "text" : "password", value: imBotsSecret,
6493
+ placeholder: w.hasSecret ? "已保存(留空表示不修改)" : "粘贴机器人 Secret",
6494
+ autoComplete: "new-password", spellCheck: false,
6495
+ "aria-invalid": fieldErr.secret ? "true" : "false",
6496
+ "aria-describedby": fieldErr.secret ? "dru-wecom-secret-err" : undefined,
6497
+ onChange: function (e) { setImBotsSecret(e.target.value); }
6498
+ }),
6499
+ h("button", {
6500
+ type: "button", className: "dru-ia-iconbtn",
6501
+ "aria-label": imBotsShowSecret ? "隐藏 Secret" : "显示 Secret",
6502
+ onClick: function () { setImBotsShowSecret(!imBotsShowSecret); }
6503
+ }, druIcon(imBotsShowSecret ? "eyeoff" : "eye"))
6504
+ ),
6505
+ fieldErr.secret || "", "只保存在本机;已保存的 Secret 永远不回显。", "dru-wecom-secret-err"),
6506
+ iaField("接收会话 ID(可选)", "dru-wecom-chat",
6507
+ h("input", {
6508
+ id: "dru-wecom-chat", className: "dru-ia-input", type: "text", value: imBotsChat,
6509
+ placeholder: "留空即可", autoComplete: "off", spellCheck: false,
6510
+ "aria-invalid": fieldErr.chat ? "true" : "false",
6511
+ onChange: function (e) { setImBotsChat(e.target.value); }
6512
+ }),
6513
+ fieldErr.chat || "", "不知道填什么就留空——你在企业微信里给机器人发一句话后会自动绑定。"),
6514
+ // 高级项收进折叠:主表单只留 4 个字段(一屏一主操作)
6515
+ h("div", { className: "dru-ia-field" },
6516
+ h("button", {
6517
+ type: "button", className: "dru-ia-disclose", "aria-expanded": imBotsWhyOpen ? "true" : "false",
6518
+ onClick: function () { setImBotsWhyOpen(!imBotsWhyOpen); }
6519
+ }, "为什么要填这些", druIcon(imBotsWhyOpen ? "left" : "right")),
6520
+ imBotsWhyOpen
6521
+ ? h("div", { className: "dru-ia-hint", style: { padding: "8px 2px 0" } },
6522
+ "Bot ID/Secret 来自企业微信「智能机器人」页,机器人靠它连接;接收会话 ID 与转接会话决定消息发到哪个对话;服务地址仅私有化部署才需要改。",
6523
+ h("div", { style: { marginTop: 8 } },
6524
+ h("label", { className: "dru-ia-label", htmlFor: "dru-wecom-url" }, "服务地址(可选,高级)"),
6525
+ h("input", {
6526
+ id: "dru-wecom-url", className: "dru-ia-input", type: "url", value: imBotsUrl,
6527
+ placeholder: "默认使用企业微信官方接口,一般无需填写", autoComplete: "off", spellCheck: false,
6528
+ "aria-invalid": fieldErr.url ? "true" : "false",
6529
+ onChange: function (e) { setImBotsUrl(e.target.value); }
6530
+ }),
6531
+ fieldErr.url ? h("div", { className: "dru-ia-err", role: "alert" }, fieldErr.url) : null))
6532
+ : null
6533
+ ),
6534
+ stateLine
6535
+ ] }));
6536
+ }
6537
+
6538
+ /** 「微信机器人(扫码绑定)」步骤:复用既有 WeChatBotSection;未登录时给登录入口(不再隐藏功能)。 */
6539
+ function renderWechatStep(close) {
6540
+ return iaDialog(Object.assign({}, {
6541
+ title: "微信机器人(扫码绑定)",
6542
+ sub: "扫码绑定后,任务完成/出错/需要拍板时会推到你的微信",
6543
+ onClose: close,
6544
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: function () { setIaChStep("list"); } }, "返回平台列表")
6545
+ }, { children: [loggedIn
6546
+ ? h(WeChatBotSection, { embedded: true })
6547
+ : h("div", { className: "dru-ia-empty" },
6548
+ druIcon("user", 24),
6549
+ h("div", { className: "dru-ia-empty-title" }, "登录后可绑定微信机器人"),
6550
+ h("div", null, "微信通道需要账号身份(用来把你的电脑和微信对上)。登录后回到这里即可扫码绑定。"),
6551
+ h("button", {
6552
+ type: "button", className: "dru-ia-btn is-primary",
6553
+ onClick: function () { setIaModal("account"); setIaChStep("list"); }
6554
+ }, "去登录"))] }));
6555
+ }
6556
+
6557
+ /** 还没支持的平台:只给**诚实**的空状态(说明 + 反馈入口),绝不放假表单/假入口。 */
6558
+ function renderComingSoonStep(close, kind) {
6559
+ var meta = {
6560
+ feishu: {
6561
+ name: "飞书机器人",
6562
+ why: "飞书官方长连接接入需要企业管理员配置应用(应用凭证 + 机器人能力),接入方式与企业微信类似。"
6563
+ },
6564
+ dingtalk: {
6565
+ name: "钉钉机器人",
6566
+ why: "钉钉应用机器人需要企业管理员创建应用并开通机器人权限,接入方式与企业微信类似。"
6567
+ },
6568
+ webhook: {
6569
+ name: "高级 · Webhook",
6570
+ why: "Webhook 需要你自行提供公网 HTTPS 端点并校验签名;我们会把它作为同一条通道的高级选项。"
6571
+ }
6572
+ }[kind] || { name: "这个平台", why: "接入方式还在评估中。" };
6573
+ return iaDialog(Object.assign({}, {
6574
+ title: meta.name,
6575
+ sub: "即将支持",
6576
+ onClose: close,
6577
+ foot: [
6578
+ h("button", {
6579
+ type: "button", className: "dru-ia-btn is-primary",
6580
+ // 复用现有反馈通道:关掉弹层 → 打开反馈视图并带一句引导(不新增后端接口)
6581
+ onClick: function () {
6582
+ setIaModal(null); setIaChStep("list");
6583
+ setIaFbHint("在下面写一句「想要 " + meta.name + "」,我们会按需求排期。");
6584
+ setView("feedback");
6585
+ }
6586
+ }, "告诉我,优先支持"),
6587
+ h("button", { type: "button", className: "dru-ia-btn", onClick: function () { setIaChStep("list"); } }, "返回平台列表")
6588
+ ]
6589
+ }, { children: [
6590
+ h("div", { className: "dru-ia-empty" },
6591
+ druIcon("bell", 24),
6592
+ h("div", { className: "dru-ia-empty-title" }, meta.name + " 即将支持"),
6593
+ h("div", null, meta.why),
6594
+ h("div", { className: "dru-ia-hint", style: { marginTop: 4 } }, "现在可以先用「企业微信智能机器人」。"))
6595
+ ] }));
6596
+ }
6597
+
6598
+ /** 通道弹层:平台列表 → 配置步骤(一级一屏,不做长表单堆叠)。 */
6599
+ function renderChannelModal() {
6600
+ if (iaModal !== "channel") return null;
6601
+ if (iaChStep === "wecom") return renderWecomStep(closeIAModal);
6602
+ if (iaChStep === "wechat") return renderWechatStep(closeIAModal);
6603
+ if (iaChStep === "feishu" || iaChStep === "dingtalk" || iaChStep === "webhook") return renderComingSoonStep(closeIAModal, iaChStep);
6604
+ var w = (imBots && imBots.wecom) || {};
6605
+ var ws = (imBots && imBots.status) || {};
6606
+ var wecomSub = ws.connected
6607
+ ? "已连接 · 推送消息到企业微信"
6608
+ : (w.enabled || w.hasSecret ? "已配置,等待连接" : "未配置 · 填 Bot ID 与 Secret 即可");
6609
+ return iaDialog(Object.assign({}, {
6610
+ title: "通知与消息通道",
6611
+ sub: "选一个平台,让电脑端在任务完成、出错或需要拍板时主动通知你",
6612
+ onClose: closeIAModal,
6613
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "关闭")
6614
+ }, { children: [h("div", { className: "dru-ia-list" },
6615
+ iaChannelRow("chat", "企业微信智能机器人", wecomSub, function () { setIaChStep("wecom"); setImBotsMsg(null); setImBotsFieldErr({}); }, "配置"),
6616
+ iaChannelRow("chat", "微信机器人(扫码绑定)", "扫码绑定 · 在微信里收通知 / 回数字拍板", function () { setIaChStep("wechat"); }, "打开"),
6617
+ iaChannelRow("send", "飞书机器人", "即将支持 · 需要企业管理员配置应用", function () { setIaChStep("feishu"); }, "查看"),
6618
+ iaChannelRow("send", "钉钉机器人", "即将支持 · 需要企业管理员创建应用", function () { setIaChStep("dingtalk"); }, "查看"),
6619
+ iaChannelRow("code", "高级 · Webhook", "高级用法 · 自备公网端点", function () { setIaChStep("webhook"); }, "查看")
6620
+ )] }));
6621
+ }
6622
+
6623
+ /** 已授权设备弹层:复用既有设备列表(渲染逻辑一行未改,只是从卡片挪进弹层)。 */
6624
+ function renderDevicesModal() {
6625
+ if (iaModal !== "devices") return null;
6626
+ return iaDialog(Object.assign({}, {
6627
+ title: "已授权设备",
6628
+ sub: "这些手机/电脑已经可以远程访问本机;撤销后需要重新授权",
6629
+ onClose: closeIAModal,
6630
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "完成")
6631
+ }, { children: [!loggedIn
6632
+ ? h("div", { className: "dru-ia-empty" },
6633
+ druIcon("devices", 24),
6634
+ h("div", { className: "dru-ia-empty-title" }, "登录后可查看已授权设备"),
6635
+ h("div", null, "设备列表跟账号走:登录后这里会显示每一台已授权的手机/电脑。"),
6636
+ h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: function () { setIaModal("account"); } }, "去登录"))
6637
+ : h("div", null,
6638
+ h("div", { className: "dru-ia-card-actions", style: { marginBottom: 8 } },
6639
+ h("button", {
6640
+ type: "button", className: "dru-ia-btn", disabled: devBusy !== "",
6641
+ onClick: function () { try { loadDevices(); } catch (e1) { /* 忽略 */ } }
6642
+ }, "已授权设备 " + (devSessions === null ? "…" : devSessions.length) + "(刷新)")),
6643
+ devSessions === null ? h("div", { className: "dru-hint" }, "正在读取设备列表…") : renderDeviceList(),
6644
+ devMsg
6645
+ ? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 6 } },
6646
+ devMsg.text,
6647
+ devMsg.retry
6648
+ ? h("button", { type: "button", className: "dru-linkbtn", style: { marginLeft: 8 }, disabled: devBusy !== "", onClick: devMsg.retry }, devBusy === "list" ? "重试中…" : "立即重试")
6649
+ : null)
6650
+ : null)] }));
6651
+ }
6652
+
6653
+ /** 账号弹层:登录不再隐藏任何功能 —— 未登录只是卡片写「登录后可同步」,点进来登录即可。 */
6654
+ function renderAccountModal() {
6655
+ if (iaModal !== "account") return null;
6656
+ return iaDialog(Object.assign({}, {
6657
+ title: "账号",
6658
+ sub: "登录后可同步设备、生成访问码;账号只用于你自己的设备之间同步",
6659
+ onClose: closeIAModal,
6660
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "完成")
6661
+ }, { children: [
6662
+ // A4:从「打开手机端 / 生成访问码」被守卫弹过来时,说明一句"为什么"(不留静默跳转)
6663
+ iaLoginWhy ? h("div", { className: "dru-ia-state is-warn", role: "status" }, iaLoginWhy) : null,
6664
+ renderAccountBody()
6665
+ ] }));
6666
+ }
6667
+
6668
+ function renderIAModals() {
6669
+ return h("div", null,
6670
+ renderMobileModal(),
6671
+ renderChannelModal(),
6672
+ renderDevicesModal(),
6673
+ renderAccountModal()
6674
+ );
6675
+ }
6676
+
6677
+ /**
6678
+ * 主面板:状态头 + 入口网格 + 一行文字入口(+ 折叠的高级设置)。
6679
+ * 「不做整页滚动」:这里最多两屏;细节一律进弹层(弹层内部才允许滚动)。
6680
+ */
6681
+ function renderHome() {
6682
+ return h("div", { className: "dru-ia" },
6683
+ renderSetupAlert(),
6684
+ message ? h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text) : null,
6685
+ renderIAHeader(),
6686
+ renderAnnouncementsCard(),
6687
+ renderIAGrid(),
6688
+ renderIALinks(),
6689
+ // 高级设置默认折叠:**节点始终在 DOM 里**(<details> 的等价语义),
6690
+ // 但折叠时用 hidden 完全收起 —— 主面板保持清爽,键盘/读屏也不会读到隐藏内容。
6691
+ h("div", { hidden: moreOpen ? undefined : "" , "aria-hidden": moreOpen ? undefined : "true" }, renderMoreSettings())
6692
+ );
6693
+ }
6694
+
5609
6695
  // 【自动化】待重启时自动倒计时重启(见 autoRestartCancelled 说明);倒计时只在面板可见且无其他操作时推进。
5610
6696
  // ⚠️ isRefreshOnly(插件更新后只需刷新)时**不**自动重启:刷新是零风险动作,重启 dsh web 会打断
5611
6697
  // 用户正在进行的会话,不能替他做这个决定 —— 让他点「刷新页面」即可。
@@ -5668,6 +6754,7 @@ window.__ModuleLoader__.load({
5668
6754
  };
5669
6755
 
5670
6756
  /** 交流群弹窗(复用满意度弹窗的 dru-popup 视觉;点遮罩/关闭即收起)。 */
6757
+
5671
6758
  function renderCommunityModal() {
5672
6759
  if (!commOpen || !(community && community.qrcode)) return null;
5673
6760
  var close = function () { setCommOpen(false); };
@@ -5864,10 +6951,11 @@ window.__ModuleLoader__.load({
5864
6951
 
5865
6952
  return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程访问", "data-dru-theme": theme },
5866
6953
  h("div", { className: "dru-settings-head" },
5867
- h("span", { className: "dru-settings-icon" }, "📱"),
6954
+ h("span", { className: "dru-settings-icon" }, druIcon("phone", 20)),
5868
6955
  h("div", null,
5869
- h("h2", { className: "dru-settings-title" }, "远程访问"),
5870
- h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)"),
6956
+ // 2026-11-07 业主:「我的插件名字叫 dsh-remote-web,这个是全名」→ 面板标题统一成品牌全名。
6957
+ h("h2", { className: "dru-settings-title" }, "dsh-remote-web"),
6958
+ h("div", { className: "dru-settings-sub" }, "远程访问:通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)"),
5871
6959
  h("div", { className: "dru-settings-sub" }, "装上即得专属加密地址:人在外面也能用手机全功能接管这台电脑上的 DeepSeek Harness(免内网穿透、免公网 IP、全程加密)。")
5872
6960
  )
5873
6961
  ),
@@ -5947,16 +7035,13 @@ window.__ModuleLoader__.load({
5947
7035
  )
5948
7036
  )
5949
7037
  : null,
5950
- // view 分派:wechat(第三个主 tab,内容自带 tab 条)| feedback | invite | home
5951
- // 🔒 微信通道要求已登录:若 view 还停在 "wechat" 而登录态已失效(会话中途过期),
5952
- // 这里**直接回落到 home**(渲染账号/登录卡)—— 绝不留一个空面板;上一条 effect
5953
- // 会把 view 正式复位成 home,两者一致。
5954
- view === "wechat" && loggedIn ? renderWeChat()
5955
- : view === "wechat" ? renderHome()
5956
- : view === "feedback" ? renderFeedback()
7038
+ // view 分派:feedback | invite | home("wechat" 视图已不存在 —— 微信通道在通道弹层里)
7039
+ view === "feedback" ? renderFeedback()
5957
7040
  : view === "invite" ? renderInvite()
5958
7041
  : renderHome()
5959
7042
  ),
7043
+ // 🧭 弹层统一挂在面板根上(fixed 定位,不占布局):任何视图下都能开、都能 Esc 关闭。
7044
+ renderIAModals(),
5960
7045
  // 2026-10-17(业主):删除「重启 DeepSeek Harness」整段说明 ——
5961
7046
  // 桌面端运行环境由应用本身托管、插件安装/更新自动热加载,刷新页面即可;
5962
7047
  // 只有非桌面端(自建 dsh web)才保留重启入口,且文案只讲这一种场景。
@@ -5979,8 +7064,8 @@ window.__ModuleLoader__.load({
5979
7064
  // ══ 🤖 微信机器人(「📱 远程访问」面板里的第三个 tab:微信机器人通道) ═════════
5980
7065
  // 【0.6.11 用户约定】「不要给它单独弄一个菜单,直接放到面板里面。在面板里单开一个
5981
7066
  // table 页,命名为『微信机器人通道』,并增加上微信的绿泡泡小图标,点开后可以进行绑定设置。」
5982
- // → 不再注册第二个 settings.section 栏目(只有 📱 远程访问 一个),本组件由 renderWeChat()
5983
- // 以 embedded=true 作为**子组件**渲染(见 renderMainTabs / renderWeChat)。
7067
+ // → 不再注册第二个 settings.section 栏目(只有「远程访问」一个),本组件由通道弹层的
7068
+ // 「微信机器人(扫码绑定)」步骤以 embedded=true 作为**子组件**渲染(见 renderWechatStep)。
5984
7069
  // 数据全部来自同源 /dsh-remote/wechat/*(宿主半边代理 bridge 的控制面)。
5985
7070
  //
5986
7071
  // ⚠️ 令牌铁律:本组件**只**渲染 bridge 已脱敏的字段;不请求、不拼接、不缓存任何 token
@@ -6621,7 +7706,7 @@ window.__ModuleLoader__.load({
6621
7706
  // order 30 > Agent 预设(20),栏目落在「Agent 预设」下方;label 即栏目名(📱 远程访问)。
6622
7707
  // 【0.6.11】本插件在这个槽里**只有一个栏目**(📱 远程访问):🤖 微信机器人原先以 order 31
6623
7708
  // 注册成第二个栏目,用户约定「不要给它单独弄一个菜单,直接放到面板里面」→ 已改成面板里的
6624
- // 第三个 tab(renderMainTabs / renderWeChat)。不要再往这个槽里加第二个 register。
7709
+ // 通道弹层里的微信步骤(renderWechatStep)。不要再往这个槽里加第二个 register。
6625
7710
  /* 📱 侧栏底部的快捷入口 —— 官方列表槽 `sidebar.footer.action`(2026-10-10 三修)。
6626
7711
  这个槽由 `@deepseek-ai/dsh-client-ui-sidebar` 声明,渲染在侧栏底部的 footerActions 里
6627
7712
  (在「设置」那一行的上方、与「意见反馈/登录/更多」同属底部区域)。0.1.x 与 0.2 都有该槽,