dsh-remote-web 0.7.10-beta.3 → 0.7.11-beta.3

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 + 分派条件),绝不留空面板。
@@ -579,6 +579,102 @@ 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
+ // 配置页的「为什么要填这些」折叠按钮 + 原生 select 统一外观(业主:别文字堆砌,多余信息收起来)
671
+ ".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}",
672
+ ".dru-ia-disclose:hover{color:var(--dru-accent)}",
673
+ ".dru-ia-disclose:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
674
+ "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}",
675
+ "@media (prefers-reduced-motion: reduce){",
676
+ " .dru-ia-scrim,.dru-ia-card,.dru-ia-btn,.dru-ia-seg-btn,.dru-ia-row{animation:none !important;transition:none !important}",
677
+ "}",
582
678
  ].join("\n");
583
679
  document.head.appendChild(styleEl);
584
680
 
@@ -3133,6 +3229,91 @@ window.__ModuleLoader__.load({
3133
3229
  // ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
3134
3230
  // 匿名装机统计的「面板打开」只上报一次/每页(node 半还会每进程去重)。
3135
3231
  var panelOpenedSent = false;
3232
+ // ══════════════════════════════════════════════════════════════════════
3233
+ // 🧭 面板 IA 基础件(2026-11-07 UX 重做):内联 SVG 图标 + 弹层(dialog)
3234
+ //
3235
+ // 为什么图标必须内联 SVG(业主原话「插件端全是文字堆砌,太密集」+ UX 规范):
3236
+ // ① emoji 是**彩色位图**,跟随不了宿主主题的 currentColor,深色/亮色下观感不稳定;
3237
+ // ② 不同系统 emoji 字形差异巨大(Windows/Mac/iOS),同一个面板会长得不一样;
3238
+ // ③ 16px 的 emoji 在密集列表里视觉重量过大,是"文字堆砌感"的来源之一。
3239
+ // 这里只放**本面板新增结构**用到的图标(旧卡片的 emoji 随重做一起不再出现在主界面)。
3240
+ // ══════════════════════════════════════════════════════════════════════
3241
+ var DRU_ICONS = {
3242
+ phone: '<rect x="5" y="2" width="14" height="20" rx="2.5"/><path d="M11 18h2"/>',
3243
+ 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"/>',
3244
+ devices: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
3245
+ 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"/>',
3246
+ x: '<path d="M18 6 6 18M6 6l12 12"/>',
3247
+ right: '<path d="m9 18 6-6-6-6"/>',
3248
+ left: '<path d="m15 18-6-6 6-6"/>',
3249
+ 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"/>',
3250
+ check: '<path d="M20 6 9 17l-5-5"/>',
3251
+ open: '<path d="M7 17 17 7M7 7h10v10"/>',
3252
+ key: '<circle cx="7.5" cy="15.5" r="3.5"/><path d="m21 2-9.6 9.6M15 7l3 3"/>',
3253
+ 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"/>',
3254
+ 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"/>',
3255
+ send: '<path d="m22 2-7 20-4-9-9-4 20-7z"/>',
3256
+ code: '<path d="m16 18 6-6-6-6M8 6l-6 6 6 6"/>',
3257
+ 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"/>',
3258
+ 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"/>',
3259
+ 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"/>',
3260
+ 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"/>'
3261
+ };
3262
+ /** 内联 SVG 图标(16px,currentColor)。名字不认识时退化成一个小圆点,绝不抛错。 */
3263
+ function druIcon(name, size) {
3264
+ var inner = DRU_ICONS[name] || '<circle cx="12" cy="12" r="3"/>';
3265
+ return h("svg", {
3266
+ width: size || 16, height: size || 16, viewBox: "0 0 24 24", fill: "none",
3267
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
3268
+ "aria-hidden": "true", focusable: "false",
3269
+ dangerouslySetInnerHTML: { __html: inner }
3270
+ });
3271
+ }
3272
+
3273
+ /**
3274
+ * 通用弹层(dialog)的**纯渲染函数**(不是组件:本仓库的面板只在少数 harness 里递归展开
3275
+ * 函数组件,把弹层做成"组件"会让大量既有用例看不到弹层内容 —— 而且那也不影响真实 React)。
3276
+ * 键盘(Esc / Tab 焦点陷阱)与"关闭后焦点回触发按钮"由 RemoteControlSection 里那条
3277
+ * `useEffect([iaModal])` 统一负责,见 openIAModal 上方注释。
3278
+ *
3279
+ * 交互契约(UX 检索:modal focus trap / Esc / focus return):
3280
+ * · role="dialog" + aria-modal + aria-labelledby(标题始终可见);
3281
+ * · Esc 关闭;点遮罩关闭;右上角 ✕ 关闭(三者同一动作);
3282
+ * · 关闭后焦点回到打开前的元素;
3283
+ * · 视觉:宽 480–560、圆角 12、边框 1px、表头/底部操作固定,正文超过 1.5 屏才滚动;
3284
+ * · prefers-reduced-motion 在样式表里统一关掉动效。
3285
+ */
3286
+ function iaDialog(props) {
3287
+ var p = props || {};
3288
+ var title = p.title || "";
3289
+ var sub = p.sub || "";
3290
+ var onClose = p.onClose;
3291
+ var labelledBy = "dru-ia-dialog-title";
3292
+ var bodyKids = Array.isArray(p.children) ? p.children : [p.children];
3293
+ var footKids = Array.isArray(p.foot) ? p.foot : [p.foot];
3294
+ var body = [].concat.apply(["div", { className: "dru-ia-dialog-body" }], bodyKids.filter(function (x) { return x !== null && x !== undefined && x !== false; }));
3295
+ var headKids = [
3296
+ h("div", { className: "dru-ia-dialog-title", id: labelledBy }, title,
3297
+ sub ? h("div", { className: "dru-ia-dialog-sub" }, sub) : null),
3298
+ h("button", { type: "button", className: "dru-ia-dialog-x", "aria-label": "关闭", onClick: onClose }, druIcon("x"))
3299
+ ];
3300
+ var kids = [
3301
+ h("div", { className: "dru-ia-dialog-head" }, headKids[0], headKids[1]),
3302
+ h.apply(null, body)
3303
+ ];
3304
+ if (p.foot) {
3305
+ kids.push(h.apply(null, [].concat.apply(["div", { className: "dru-ia-dialog-foot" }], footKids)));
3306
+ }
3307
+ return h("div", {
3308
+ className: "dru-ia-scrim",
3309
+ onMouseDown: function (e) { if (e && e.target === e.currentTarget && onClose) onClose(); }
3310
+ },
3311
+ h.apply(null, [].concat.apply(["div", {
3312
+ className: "dru-ia-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": labelledBy
3313
+ }], kids))
3314
+ );
3315
+ }
3316
+
3136
3317
  function RemoteControlSection(props) {
3137
3318
  // 🎨 主题(宿主深色/亮色):订阅外部存储 → 宿主切换时本组件自动重渲染,
3138
3319
  // 根节点上的 data-dru-theme 随之更新,整套 --dru-* 令牌实时切换。
@@ -3237,6 +3418,212 @@ window.__ModuleLoader__.load({
3237
3418
  var fixPromptArr = useState(null); var fixPrompt = fixPromptArr[0]; var setFixPrompt = fixPromptArr[1];
3238
3419
  var fixReportArr = useState(""); var fixReport = fixReportArr[0]; var setFixReport = fixReportArr[1];
3239
3420
 
3421
+ // ── 🧭 面板 IA 重做 + 通知通道(2026-11-07)────────────────────────────────
3422
+ // hook 序号铁律:全部追加在**最后一个既有 hook 之后**(见本组件顶部说明)。
3423
+ // · iaModal 当前打开的弹层(null / "mobile" / "channel" / "devices" / "account");
3424
+ // · iaChStep 通道弹层里的步骤(list / wecom / wechat / feishu / webhook);
3425
+ // · imBots* GET /dsh-remote/im-bots 的**脱敏**视图 + 表单态(secret 永不回显,
3426
+ // 所以输入框初值恒为空字符串 = "不修改")。
3427
+ var iaModalArr = useState(null); var iaModal = iaModalArr[0]; var setIaModal = iaModalArr[1];
3428
+ var iaChStepArr = useState("list"); var iaChStep = iaChStepArr[0]; var setIaChStep = iaChStepArr[1];
3429
+ var imBotsArr = useState(null); var imBots = imBotsArr[0]; var setImBots = imBotsArr[1];
3430
+ var imBotsBusyArr = useState(""); var imBotsBusy = imBotsBusyArr[0]; var setImBotsBusy = imBotsBusyArr[1];
3431
+ var imBotsMsgArr = useState(null); var imBotsMsg = imBotsMsgArr[0]; var setImBotsMsg = imBotsMsgArr[1];
3432
+ var imBotsFieldErrArr = useState({}); var imBotsFieldErr = imBotsFieldErrArr[0]; var setImBotsFieldErr = imBotsFieldErrArr[1];
3433
+ var imBotsBotIdArr = useState(""); var imBotsBotId = imBotsBotIdArr[0]; var setImBotsBotId = imBotsBotIdArr[1];
3434
+ var imBotsSecretArr = useState(""); var imBotsSecret = imBotsSecretArr[0]; var setImBotsSecret = imBotsSecretArr[1];
3435
+ var imBotsChatArr = useState(""); var imBotsChat = imBotsChatArr[0]; var setImBotsChat = imBotsChatArr[1];
3436
+ var imBotsUrlArr = useState(""); var imBotsUrl = imBotsUrlArr[0]; var setImBotsUrl = imBotsUrlArr[1];
3437
+ var imBotsShowSecretArr = useState(false); var imBotsShowSecret = imBotsShowSecretArr[0]; var setImBotsShowSecret = imBotsShowSecretArr[1];
3438
+ // 「转接到哪个会话」:空串 = 跟随当前活跃会话(桥侧默认)。title 一起存,用于回显与保存。
3439
+ var imBotsSessionIdArr = useState(""); var imBotsSessionId = imBotsSessionIdArr[0]; var setImBotsSessionId = imBotsSessionIdArr[1];
3440
+ var imBotsSessionTitleArr = useState(""); var imBotsSessionTitle = imBotsSessionTitleArr[0]; var setImBotsSessionTitle = imBotsSessionTitleArr[1];
3441
+ // 「为什么要填这些」默认折叠(业主:配置页别文字堆砌)
3442
+ var imBotsWhyArr = useState(false); var imBotsWhyOpen = imBotsWhyArr[0]; var setImBotsWhyOpen = imBotsWhyArr[1];
3443
+ // 从「飞书/钉钉即将支持 → 告诉我,优先支持」跳到反馈视图时带过去的一句引导(独立状态:
3444
+ // message 会被面板里各种动作清掉,跨视图的上下文不能押在它身上)。
3445
+ var iaFbHintArr = useState(""); var iaFbHint = iaFbHintArr[0]; var setIaFbHint = iaFbHintArr[1];
3446
+
3447
+ /** 读通道配置(GET 只回脱敏视图:botId/hasSecret/url/targetChatId + 桥写的状态)。 */
3448
+ function loadImBots() {
3449
+ return api("/dsh-remote/im-bots").then(function (b) {
3450
+ setImBots(b && typeof b === "object" ? b : null);
3451
+ return b;
3452
+ }).catch(function () {
3453
+ setImBots(null); // 读不到 = 未配置(入口卡显示「未配置」);进弹层点保存会拿到真实错误
3454
+ return null;
3455
+ });
3456
+ }
3457
+ // 挂载即读一次:入口格「通道」卡要显示当前状态;失败静默(下次打开弹层会重读)。
3458
+ useEffect(function () { loadImBots(); }, []);
3459
+
3460
+ // 弹层的键盘/焦点契约(UX 检索:modal focus trap / Esc / focus return):
3461
+ // · Esc 关闭;Tab / Shift+Tab 在弹层内循环(焦点绝不跑到背后的页面上);
3462
+ // · 关闭后焦点回到打开前的元素(document.activeElement 快照);
3463
+ // · 任何一步失败都吞掉 —— 弹层本身是纯 DOM,不依赖这些逻辑也能用。
3464
+ useEffect(function () {
3465
+ if (!iaModal) return undefined;
3466
+ var prev = null;
3467
+ try { prev = document.activeElement || null; } catch (e0) { prev = null; }
3468
+ var onKey = function (ev) {
3469
+ try {
3470
+ if (!ev) return;
3471
+ if (ev.key === "Escape" || ev.keyCode === 27) {
3472
+ if (ev.stopPropagation) ev.stopPropagation();
3473
+ setIaModal(null);
3474
+ return;
3475
+ }
3476
+ if (ev.key !== "Tab") return;
3477
+ var dlg = null;
3478
+ try { dlg = document.querySelector(".dru-ia-dialog"); } catch (e1) { dlg = null; }
3479
+ if (!dlg || typeof dlg.querySelectorAll !== "function") return;
3480
+ var list = dlg.querySelectorAll('button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
3481
+ var items = [];
3482
+ for (var i = 0; i < list.length; i++) {
3483
+ try { if (list[i] && list[i].offsetParent !== null) items.push(list[i]); } catch (e2) { /* 忽略 */ }
3484
+ }
3485
+ if (!items.length) return;
3486
+ var first = items[0], last = items[items.length - 1];
3487
+ var act = document.activeElement;
3488
+ if (ev.shiftKey) {
3489
+ if (act === first || !dlg.contains || !dlg.contains(act)) { ev.preventDefault(); try { last.focus(); } catch (e3) { /* 忽略 */ } }
3490
+ } else if (act === last) {
3491
+ ev.preventDefault();
3492
+ try { first.focus(); } catch (e4) { /* 忽略 */ }
3493
+ }
3494
+ } catch (e5) { /* 键盘逻辑失败不影响弹层使用 */ }
3495
+ };
3496
+ try { document.addEventListener("keydown", onKey, true); } catch (e6) { /* 忽略 */ }
3497
+ var t = null;
3498
+ try {
3499
+ t = setTimeout(function () {
3500
+ try {
3501
+ var dlg = document.querySelector(".dru-ia-dialog");
3502
+ if (!dlg || typeof dlg.querySelector !== "function") return;
3503
+ var el = dlg.querySelector(".dru-ia-dialog-body input, .dru-ia-dialog-body button, .dru-ia-dialog-x");
3504
+ if (el && el.focus) el.focus();
3505
+ } catch (e7) { /* 忽略 */ }
3506
+ }, 0);
3507
+ } catch (e8) { t = null; }
3508
+ return function () {
3509
+ try { if (t !== null) clearTimeout(t); } catch (e9) { /* 忽略 */ }
3510
+ try { document.removeEventListener("keydown", onKey, true); } catch (e10) { /* 忽略 */ }
3511
+ try { if (prev && prev.focus) prev.focus(); } catch (e11) { /* 忽略 */ }
3512
+ };
3513
+ }, [iaModal]);
3514
+
3515
+ /** 表单 → 契约 payload(空 secret 不传 = 保留已保存的 secret;空 targetChatId = 不设置)。 */
3516
+ function imBotsFormPayload() {
3517
+ var wecom = { enabled: true, botId: String(imBotsBotId || "").trim() };
3518
+ var sec = String(imBotsSecret || "").trim();
3519
+ if (sec) wecom.secret = sec;
3520
+ wecom.targetChatId = String(imBotsChat || "").trim();
3521
+ // 会话可选绑定:空串 = 跟随当前活跃会话(服务端会同时清掉 sessionTitle)
3522
+ wecom.sessionId = String(imBotsSessionId || "").trim();
3523
+ wecom.sessionTitle = wecom.sessionId ? String(imBotsSessionTitle || "").trim() : "";
3524
+ var u = String(imBotsUrl || "").trim();
3525
+ if (u) wecom.url = u;
3526
+ return wecom;
3527
+ }
3528
+ /** 节点半的稳定错误码 → 就近字段错误(有码就用码,没码就整体提示;绝不猜)。 */
3529
+ function imBotsFieldErrorsOf(code, message) {
3530
+ var e = {};
3531
+ var text = message || "";
3532
+ if (code === "invalid_bot_id") e.botId = text || "Bot ID 格式不对:应形如 aibxxxxxxxx";
3533
+ else if (code === "missing_secret") e.secret = text || "请填写 Secret";
3534
+ else if (code === "invalid_url") e.url = text || "服务地址必须以 http:// 或 https:// 开头";
3535
+ else if (code === "invalid_target_chat_id") e.chat = text || "接收会话 ID 太长";
3536
+ else if (code === "invalid_session_id") e.session = text || "会话 ID 太长";
3537
+ return e;
3538
+ }
3539
+ /** 保存(PUT);testAfter=true 时走 POST /test(= 保存后等桥连接,最多 10s)。 */
3540
+ function saveImBots(testAfter) {
3541
+ if (imBotsBusy !== "") return undefined;
3542
+ setImBotsBusy(testAfter ? "test" : "save");
3543
+ setImBotsMsg(null);
3544
+ setImBotsFieldErr({});
3545
+ var payload = { wecom: imBotsFormPayload() };
3546
+ var path = testAfter ? "/dsh-remote/im-bots/test" : "/dsh-remote/im-bots";
3547
+ var opts = {
3548
+ method: testAfter ? "POST" : "PUT",
3549
+ headers: { "content-type": "application/json" },
3550
+ body: JSON.stringify(payload)
3551
+ };
3552
+ return api(path, opts, testAfter ? 15000 : 10000).then(function (b) {
3553
+ if (testAfter && b && b.connected) {
3554
+ setImBotsMsg({ kind: "ok", text: "已连接:企业微信机器人可以接收消息了。" + (b.authenticated ? "" : "(通道已连通,鉴权稍后完成)") });
3555
+ } else {
3556
+ setImBotsMsg({ kind: "ok", text: testAfter ? "配置已保存。" : "已保存。" });
3557
+ }
3558
+ return loadImBots();
3559
+ }).catch(function (e) {
3560
+ var body = (e && e.body) || {};
3561
+ var code = String(body.error || "");
3562
+ var text = String(body.message || (e && e.message) || "操作失败");
3563
+ if (code === "missing_secret" || code === "invalid_bot_id" || code === "invalid_url" || code === "invalid_target_chat_id") {
3564
+ setImBotsFieldErr(imBotsFieldErrorsOf(code, text));
3565
+ setImBotsMsg({ kind: "err", text: text });
3566
+ } else if (code === "connect_failed") {
3567
+ setImBotsMsg({ kind: "err", text: "连接失败:" + (body.lastError || text) });
3568
+ } else if (code === "timeout") {
3569
+ setImBotsMsg({ kind: "warn", text: text });
3570
+ } else {
3571
+ setImBotsMsg({ kind: "err", text: text });
3572
+ }
3573
+ return null;
3574
+ }).finally(function () { setImBotsBusy(""); });
3575
+ }
3576
+ /** 「测试」= 用**已保存**配置重连并等结果(不重写配置;想改字段请先「保存」/「保存并连接」)。 */
3577
+ /** 「发送预览卡片」= 给绑定的企微会话发一张**演示用**审批卡(按钮不会真的执行)。
3578
+ * 触发方式:PUT 一个更大的 wecom.previewCardAt(桥监听这个时间戳);**必须带上表单里的
3579
+ * 全部既有字段**,否则 PUT 的覆盖写会把 botId/secret/会话绑定一起冲掉。
3580
+ * 空 secret 仍表示"不修改",所以只在用户填了新 Secret 时才提交 secret。 */
3581
+ function sendPreviewCard() {
3582
+ if (imBotsBusy !== "") return undefined;
3583
+ setImBotsBusy("preview");
3584
+ setImBotsMsg(null);
3585
+ setImBotsFieldErr({});
3586
+ var payload = { wecom: imBotsFormPayload() };
3587
+ // 桥只认"值变大":同一毫秒连点两次也要保证严格递增(否则第二次点了没反应,用户以为坏了)。
3588
+ var prevAt = Number((imBots && imBots.wecom && imBots.wecom.previewCardAt) || 0);
3589
+ payload.wecom.previewCardAt = Math.max(Date.now(), prevAt + 1);
3590
+ return api("/dsh-remote/im-bots", {
3591
+ method: "PUT",
3592
+ headers: { "content-type": "application/json" },
3593
+ body: JSON.stringify(payload)
3594
+ }, 10000).then(function () {
3595
+ setImBotsMsg({ kind: "ok", text: "已发送预览卡片,去企业微信看看" });
3596
+ return loadImBots(); // 拉回 status.wecom.lastPreviewCardAt → 显示「上次预览:HH:MM」
3597
+ }).catch(function (e) {
3598
+ var body = (e && e.body) || {};
3599
+ var code = String(body.error || "");
3600
+ var text = String(body.message || (e && e.message) || "发送失败");
3601
+ if (code === "invalid_preview_card_at") setImBotsMsg({ kind: "err", text: text });
3602
+ else setImBotsMsg({ kind: code === "timeout" ? "warn" : "err", text: "发送失败:" + text });
3603
+ return null;
3604
+ }).finally(function () { setImBotsBusy(""); });
3605
+ }
3606
+
3607
+ function testImBots() {
3608
+ if (imBotsBusy !== "") return undefined;
3609
+ setImBotsBusy("test");
3610
+ setImBotsMsg(null);
3611
+ return api("/dsh-remote/im-bots/test", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }, 15000)
3612
+ .then(function (b) {
3613
+ if (b && b.connected) setImBotsMsg({ kind: "ok", text: "已连接" + (b.authenticated ? "(鉴权通过)" : "") + "。" });
3614
+ else setImBotsMsg({ kind: "ok", text: "通道已就绪。" });
3615
+ return loadImBots();
3616
+ })
3617
+ .catch(function (e) {
3618
+ var body = (e && e.body) || {};
3619
+ var code = String(body.error || "");
3620
+ var text = String(body.message || (e && e.message) || "测试失败");
3621
+ setImBotsMsg({ kind: code === "timeout" ? "warn" : "err", text: code === "connect_failed" ? "连接失败:" + (body.lastError || text) : text });
3622
+ return null;
3623
+ })
3624
+ .finally(function () { setImBotsBusy(""); });
3625
+ }
3626
+
3240
3627
  var refresh = useCallback(function () {
3241
3628
  setBusy("status");
3242
3629
  var t0 = netNow(); // 📶 延迟采样点①:打开面板时这一次 /status(既有请求,不新增)
@@ -3674,8 +4061,11 @@ window.__ModuleLoader__.load({
3674
4061
  // ⚠️ 必须是无条件调用的 hook(hook 序号稳定),所以判断写在回调里,不能围着它加 if。
3675
4062
  // 同一帧的兜底还有底部的 view 分派:signed-out 时它**直接渲染 home**,不会先闪一帧空白。
3676
4063
  useEffect(function () {
3677
- if (view === "wechat" && !loggedIn) setView("home");
3678
- }, [view, loggedIn]);
4064
+ // IA 重做(2026-11-07):不再有 "wechat" 视图(微信通道搬进「通知与消息通道」弹层)——
4065
+ // 这里只做**视角兜底**:任何不在白名单里的 view(例如旧代码/外部残留)一律回 home,
4066
+ // 绝不让面板停在空视图上。保留这条 effect 也保证 hook 序号不变。
4067
+ if (view !== "home" && view !== "feedback" && view !== "invite") setView("home");
4068
+ }, [view]);
3679
4069
 
3680
4070
  /**
3681
4071
  * 连接阶段轮询:GET /dsh-remote/bridge-status(node 半在返回前自动补装运行环境 / 拉起 bridge /
@@ -4924,7 +5314,10 @@ window.__ModuleLoader__.load({
4924
5314
  // ---------- 反馈视图(独立页,带返回) ----------
4925
5315
  function renderFeedback() {
4926
5316
  return h("div", null,
4927
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回"),
5317
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setIaFbHint(""); setView("home"); } }, "← 返回"),
5318
+ // 带过来的上下文提示(例如从「飞书即将支持 → 告诉我,优先支持」过来时的那句引导);
5319
+ // 离开反馈视图时清空(它在 setView("home") 的返回按钮里一并清掉)。
5320
+ iaFbHint ? h("div", { className: "dru-msg dru-msg-ok" }, iaFbHint) : null,
4928
5321
  h(FeedbackCard, null)
4929
5322
  );
4930
5323
  }
@@ -5385,224 +5778,658 @@ window.__ModuleLoader__.load({
5385
5778
  ]);
5386
5779
  }
5387
5780
 
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
- };
5781
+ // ═════════════════════════════════════════════════════════════════════
5782
+ // 🧭 主面板 IA(2026-11-07 业主 UX 重做)
5783
+ //
5784
+ // 旧结构(被业主否掉):「顶部 3 个 tab(云端/自建/微信)」+ 一屏垂直堆叠的卡片 + 大段说明文字,
5785
+ // 且微信通道**登录后才出现** —— 用户原话:「顶部 3 个 tab 页,还要登录之后才有微信通道」
5786
+ // 「卡片式平铺堆叠,很糟糕」「全是文字堆砌,太密集,垂直往下垒」。
5787
+ // 新结构(本段实现):
5788
+ // · 一行状态头(连接状态 pill + 「打开手机端」主按钮);
5789
+ // · 2 列入口网格(≤3 行/卡):手机访问 / 通知与消息通道 / 已授权设备 / 账号;
5790
+ // · 一行文字入口(反馈 / 邀请 / 交流群 / 更多设置),高级设置默认折叠;
5791
+ // · 所有"进入细节"的操作一律走弹层(IADialog:Esc、焦点陷阱、关闭回焦点);
5792
+ // · 原来的「云端/自建」降级成**手机访问弹层里的分段控件**,不再占据顶部导航;
5793
+ // · 登录**不再隐藏任何功能**:未登录时网格与卡片照常显示,卡片文案写「登录后可同步」,
5794
+ // 点开进账号弹层登录,成功后在原地刷新(状态更新即重渲染,无需刷新页面)。
5795
+ // ═════════════════════════════════════════════════════════════════════
5796
+
5797
+ /** 弹层开关:同一时刻只开一个(真实产品里多弹层叠加只会让用户更迷路)。 */
5798
+ function openIAModal(name) {
5799
+ setMessage(null);
5800
+ setIaModal(name);
5801
+ if (name === "channel") {
5802
+ setIaChStep("list");
5803
+ setImBotsMsg(null);
5804
+ setImBotsFieldErr({});
5805
+ loadImBots().then(function (b) {
5806
+ var w = (b && b.wecom) || {};
5807
+ setImBotsBotId(String(w.botId || ""));
5808
+ setImBotsSecret("");
5809
+ setImBotsChat(String(w.targetChatId || ""));
5810
+ setImBotsSessionId(String(w.sessionId || ""));
5811
+ setImBotsSessionTitle(String(w.sessionTitle || ""));
5812
+ setImBotsWhyOpen(false);
5813
+ setImBotsUrl(String(w.url || ""));
5814
+ });
5815
+ }
5816
+ // 数据加载交给下面的 effect(按"弹层是否打开"触发):
5817
+ // 不在这里直接调 loadAccessKey/loadDevices —— 点击处理里同步改 busy 态会让弹层第一帧
5818
+ // 停在「生成中…」而不是空态,读屏/用例与真实用户看到的都应该是"空态 + 一个按钮"。
5408
5819
  }
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
- "微信机器人通道")
5820
+
5821
+ // 打开弹层时按需拉数据(幂等;失败由既有重试/红字逻辑兜底)。
5822
+ useEffect(function () {
5823
+ if (iaModal !== "mobile" || !loggedIn || akey) return;
5824
+ try { loadAccessKey(); } catch (e1) { /* 忽略 */ }
5825
+ }, [iaModal, loggedIn, akey]);
5826
+ useEffect(function () {
5827
+ if (iaModal !== "devices" || !loggedIn || devSessions !== null) return;
5828
+ try { loadDevices(); } catch (e2) { /* 忽略 */ }
5829
+ }, [iaModal, loggedIn, devSessions]);
5830
+ function closeIAModal() { setIaModal(null); }
5831
+
5832
+ /** 连接状态 → pill 语义(圆点 + 文案;不只靠颜色)。 */
5833
+ function iaConnState() {
5834
+ if (st === null) return { cls: "", text: "连接中…" };
5835
+ if (connInfo && connInfo.locked) return { cls: "is-warn", text: "本机已锁定" };
5836
+ if (!loggedIn) return { cls: "is-warn", text: "未登录" };
5837
+ if (connOnline) return { cls: "is-on", text: "已连接" };
5838
+ if (connPhase === "error") return { cls: "is-warn", text: "连接异常" };
5839
+ if (connPhase && connPhase !== "online") return { cls: "", text: "连接中…" };
5840
+ return { cls: "", text: serviceRunning ? "等待设备连接" : "未连接" };
5841
+ }
5842
+
5843
+ /** 入口卡(每卡 ≤3 行:标题 / 一行状态 / 操作)。 */
5844
+ function iaCard(icon, title, meta, actions) {
5845
+ return h("div", { className: "dru-ia-card" },
5846
+ h("div", { className: "dru-ia-card-top" },
5847
+ h("span", { className: "dru-ia-ico" }, druIcon(icon)),
5848
+ h("div", { className: "dru-ia-card-title" }, title)),
5849
+ h("div", { className: "dru-ia-card-meta" }, meta),
5850
+ h("div", { className: "dru-ia-card-actions" }, actions)
5851
+ );
5852
+ }
5853
+
5854
+ function renderIAHeader() {
5855
+ var cs = iaConnState();
5856
+ return h("div", { className: "dru-ia-head" },
5857
+ h("div", { className: "dru-ia-pill " + cs.cls, role: "status", "aria-live": "polite" },
5858
+ h("span", { className: "dru-ia-pill-dot", "aria-hidden": "true" }),
5859
+ cs.text),
5860
+ h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: function () { openIAModal("mobile"); } },
5861
+ druIcon("phone"), "打开手机端")
5862
+ );
5863
+ }
5864
+
5865
+ /** 入口网格:4 张卡(2×2),每卡只讲一件事。 */
5866
+ function renderIAGrid() {
5867
+ var w = (imBots && imBots.wecom) || {};
5868
+ var ws = (imBots && imBots.status) || {};
5869
+ var chMeta;
5870
+ if (imBots === null) chMeta = "企业微信机器人 · 读取中…";
5871
+ else if (ws.connected) chMeta = "企业微信机器人 · 已连接";
5872
+ else if (w.enabled || w.hasSecret) chMeta = "企业微信机器人 · 已配置(未连接)";
5873
+ else chMeta = "企业微信机器人 · 未配置";
5874
+
5875
+ var devCount = devSessions === null ? null : devSessions.length;
5876
+ var devMeta = !loggedIn
5877
+ ? "登录后可同步设备"
5878
+ : (devCount === null ? "尚未加载" : (devCount + " 台已授权设备"));
5879
+
5880
+ var acctMeta = st === null ? "读取中…" : (loggedIn ? (st.config && st.config.phone ? st.config.phone : "已登录") : "未登录(登录后可同步)");
5881
+
5882
+ var remain = 0;
5883
+ if (akey) { var expMs = toMs(akey.expires_at); if (expMs) remain = expMs - nowTick; }
5884
+ var mobileMeta = !loggedIn
5885
+ ? "登录后生成一次性访问码"
5886
+ : (akey && akey.url && remain > 0 ? ("访问码有效 · " + fmtRemain(remain) + "后过期") : "尚未生成访问码");
5887
+
5888
+ return h("div", { className: "dru-ia-grid" },
5889
+ iaCard("phone", "手机访问", mobileMeta,
5890
+ h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: function () { openIAModal("mobile"); } },
5891
+ loggedIn ? (akey && akey.url ? "查看访问码" : "生成访问码") : "登录并生成")),
5892
+ iaCard("bell", "通知与消息通道", chMeta,
5893
+ h("button", { type: "button", className: "dru-ia-btn", onClick: function () { openIAModal("channel"); } }, "设置")),
5894
+ iaCard("devices", "已授权设备", devMeta,
5895
+ h("button", { type: "button", className: "dru-ia-btn", onClick: function () { openIAModal("devices"); } }, "管理")),
5896
+ iaCard("user", "账号", acctMeta,
5897
+ h("button", { type: "button", className: "dru-ia-btn", onClick: function () { openIAModal("account"); } },
5898
+ loggedIn ? "我的信息" : "登录 / 注册"))
5899
+ );
5900
+ }
5901
+
5902
+ /** 一行文字入口:低频功能收在这里,主界面保持清爽。 */
5903
+ function renderIALinks() {
5904
+ var unread = annState.ok && annState.bubble && typeof annState.unread === "number" ? annState.unread : 0;
5905
+ return h("div", { className: "dru-ia-links" },
5906
+ h("button", { type: "button", className: "dru-ia-link", onClick: function () { setView("feedback"); } }, druIcon("chat"), "用户反馈"),
5907
+ h("button", { type: "button", className: "dru-ia-link", onClick: function () { setView("invite"); loadInvite(); } }, "邀请有礼"),
5908
+ community && community.qrcode
5909
+ ? h("button", { type: "button", className: "dru-ia-link", onClick: function () { setCommOpen(true); } }, "加入交流群")
5910
+ : null,
5911
+ h("button", {
5912
+ type: "button", className: "dru-ia-link", "aria-expanded": moreOpen ? "true" : "false",
5913
+ onClick: function () { setMoreOpen(!moreOpen); }
5914
+ }, moreOpen ? "收起高级设置" : "更多设置", druIcon(moreOpen ? "left" : "right")),
5915
+ unread > 0 ? h("span", { className: "dru-hint", style: { marginLeft: "auto" } }, "有 " + unread + " 条未读公告") : null
5916
+ );
5917
+ }
5918
+
5919
+ /** 后台服务卡(从旧 renderHome 搬出来;文案压缩,不再解释内部实现细节)。 */
5920
+ function renderBridgeCard() {
5921
+ return h("div", { className: "dru-card" },
5922
+ h("h3", null, "后台服务"),
5923
+ h("div", { className: "dru-status-line" },
5924
+ h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
5925
+ h("span", null, st ? serviceStateText : "查询中…"),
5926
+ launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
5927
+ ),
5928
+ connInfo && connPhase && connPhase !== "no_account"
5929
+ ? h("div", { className: "dru-hint", style: { marginTop: 6 } },
5930
+ "连接进度:" + (connInfo.locked ? "本机已被锁定(额度已满,升级或解绑后自动恢复)" : connText))
5931
+ : null,
5932
+ h("div", { className: "dru-actions", style: { marginTop: 10 } },
5933
+ !serviceRunning
5934
+ ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动后台服务")
5935
+ : h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止后台服务")
5936
+ ),
5937
+ // 自启动形态(平台差异要如实说:Windows 用任务计划程序、其它平台用 launchd/systemd 类服务)。
5938
+ h("div", { className: "dru-meta" }, st ? (st.service && (st.service.plistExists || st.service.serviceManager === "detached")
5939
+ ? (st.service.serviceManager === "detached"
5940
+ ? (st.service.autostartTask
5941
+ ? "自启动:已注册登录任务(Windows 任务计划程序 dsh-remote-bridge)"
5942
+ : "自启动:未注册登录任务(由面板在 dsh web 启动时自动拉起后台服务)")
5943
+ : "自启动服务已安装")
5944
+ : "自启动服务未安装(启动时自动创建)") : ""),
5945
+ // 设备 ID 属于"客服排查时要报的一行",留在高级设置里(主界面不露)。
5946
+ h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
5947
+ st && !serviceRuntimeReady
5948
+ ? h("div", { className: "dru-hint", style: { marginTop: 8 } }, "运行环境正在自动补装,装完会自动启动,不需要你操作。")
5949
+ : serviceCrashing
5950
+ ? h("div", { className: "dru-hint", style: { marginTop: 8 } }, "上次启动失败,已自动修复并重试;若持续失败请到「版本与更新」点一键更新。")
5951
+ : null,
5952
+ st && st.service && st.service.bindError
5953
+ ? (function (be) {
5954
+ var isLocked = be.code === "device_locked" || be.code === "device_limit_exceeded";
5955
+ var beLimit = (be.limit === 0 || be.limit) ? be.limit : null;
5956
+ const isBindCap = be.code === "device_bind_cap";
5957
+ return h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
5958
+ (isLocked ? "本机已被锁定:" : "设备注册失败:") + (be.message || "未说明原因"),
5959
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
5960
+ isLocked
5961
+ ? lockedPanelDetail(beLimit)
5962
+ : isBindCap
5963
+ ? "已绑定设备数达到上限(device_bind_cap):请在手机端「管理电脑」解绑不再使用的设备;解绑后本机会自动恢复(后台守护按指数退避重试,无需反复点按钮)。"
5964
+ : "请确认网络与账号状态后重试;仍未解决可点「彻底卸载」后重新安装。"));
5965
+ })(st.service.bindError)
5437
5966
  : null
5438
5967
  );
5439
5968
  }
5440
- /** 微信机器人 tab 的内容:直接复用 WeChatBotSection(embedded=省掉外层 settings-section 壳)。 */
5441
- function renderWeChat() {
5969
+
5970
+ /** 「更多设置」:后台服务 / 版本与更新 / 关于。低频但必须有出口,默认折叠。 */
5971
+ function renderMoreSettings() {
5972
+ return h("div", { className: "dru-ia" },
5973
+ renderBridgeCard(),
5974
+ h(SelfManageCard, null),
5975
+ h("div", { className: "dru-card" },
5976
+ h("h3", null, "关于 dsh-remote"),
5977
+ h("div", { className: "dru-hint" }, "用手机或另一台电脑随时接管这台电脑上的 DeepSeek Harness:装上即得专属加密地址,免公网 IP、免内网穿透,全程加密。"),
5978
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
5979
+ "匿名装机统计:只上报「装机 / 连接是否成功」这类事件(安装开始与失败原因、后台服务是否注册成功、是否首次远程打通)——"
5980
+ + "不含任何账号、手机号、会话或文件内容、主机名、路径、设备指纹与 IP;标识是本机随机 ID(重装即变)。"
5981
+ + "用环境变量 DSH_REMOTE_TELEMETRY=0 可完全关闭(不生成 ID、不发任何请求)。"),
5982
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
5983
+ "端到端加密:手机与电脑之间的消息内容用账号密码派生的密钥加密,密钥与密码不落服务端(中继只可见路径 / 大小 / 时间)。")
5984
+ )
5985
+ );
5986
+ }
5987
+
5988
+ // ── 弹层内容 ──────────────────────────────────────────────────────────
5989
+ /** 自建服务表单(从旧 home 的自建卡搬出来,放进「手机访问」弹层的分段里)。 */
5990
+ function renderSelfHostBody() {
5991
+ if (st && st.config && st.config.mode === "local") {
5992
+ return h("div", null,
5993
+ h("div", { className: "dru-ia-state is-ok" }, druIcon("check"), "已连接 " + (st.config.selfHostUrl || "自建服务")),
5994
+ h("div", { className: "dru-hint", style: { marginTop: 8 } }, "当前使用自建服务(本地认证,无需手机号账号)。切回云端:切到「云端服务」分段用手机号登录即可。")
5995
+ );
5996
+ }
5442
5997
  return h("div", null,
5443
- renderMainTabs(),
5444
- h(WeChatBotSection, { embedded: true })
5998
+ field("服务器地址", input({ value: selfHost, placeholder: "my.example.com:13443", onChange: function (e) { setSelfHost(e.target.value); } })),
5999
+ field("访问密钥", input({ type: "password", value: localKey, placeholder: "自建服务的访问密钥", autoComplete: "off", onChange: function (e) { setLocalKey(e.target.value); } })),
6000
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%", marginTop: 8 }, disabled: busy !== "", onClick: saveLocal }, busy === "local" ? "保存中…" : "切换到自建服务"),
6001
+ h("div", { className: "dru-hint", style: { marginTop: 8 } }, "服务器地址由你的自建部署决定(不是上面的云端地址),填地址与访问密钥即可。")
5445
6002
  );
5446
6003
  }
5447
- function renderHome() {
5448
- var isLocal = mode === "local";
6004
+
6005
+ /** 账号弹层内容:已登录 → 复用既有 renderAccount();未登录 → 登录/注册表单(原样搬过来)。 */
6006
+ function renderAccountBody() {
6007
+ if (st === null) return h("div", { className: "dru-hint" }, "正在读取远控状态…");
6008
+ if (loggedIn && st.config && st.config.phone) return renderAccount();
5449
6009
  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
- )
6010
+ h("div", { className: "dru-ia-seg", role: "tablist", "aria-label": "登录或注册" },
6011
+ h("button", {
6012
+ type: "button", role: "tab", "aria-selected": authTab === "login" ? "true" : "false",
6013
+ className: "dru-ia-seg-btn" + (authTab === "login" ? " is-active" : ""),
6014
+ onClick: function () { setAuthTab("login"); setPwdOpen(false); setMessage(null); }
6015
+ }, "登录"),
6016
+ h("button", {
6017
+ type: "button", role: "tab", "aria-selected": authTab === "register" ? "true" : "false",
6018
+ className: "dru-ia-seg-btn" + (authTab === "register" ? " is-active" : ""),
6019
+ onClick: function () { setAuthTab("register"); setPwdOpen(false); setMessage(null); }
6020
+ }, "注册")
5524
6021
  ),
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 } },
5541
- !serviceRunning
5542
- ? 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" ? "停止中…" : "停止后台服务")
6022
+ authTab === "login"
6023
+ ? (pwdOpen
6024
+ ? renderResetPwdForm(true)
6025
+ : h("div", null,
6026
+ field("手机号", input({ type: "tel", value: phone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setPhone(e.target.value); } })),
6027
+ field("密码", input({ type: "password", value: pass, placeholder: "密码", autoComplete: "current-password", onChange: function (e) { setPass(e.target.value); } })),
6028
+ field("验证码", h("div", { className: "dru-captcha" },
6029
+ input({ value: lcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setLcapTxt(e.target.value); } }),
6030
+ 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 },
6031
+ lcap && lcap.svg && lcap.svg.indexOf("<svg") !== 0 ? lcap.svg : null)
6032
+ )),
6033
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doLogin }, busy === "login" ? "登录中…" : "登录"),
6034
+ h("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 8 } },
6035
+ h("button", { type: "button", className: "dru-linkbtn", disabled: busy !== "", onClick: togglePwdForm }, "忘记密码?"))
6036
+ ))
6037
+ : h("div", null,
6038
+ field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
6039
+ field("密码", input({ type: "password", value: rpass, placeholder: "至少 8 位", autoComplete: "new-password", onChange: function (e) { setRpass(e.target.value); } })),
6040
+ field("确认密码", input({ type: "password", value: rpass2, placeholder: "再次输入密码", autoComplete: "new-password", onChange: function (e) { setRpass2(e.target.value); } })),
6041
+ field("邀请码(选填)", input({ value: rInvite, placeholder: "邀请你的人的邀请码,如 A8K2M4XQ", autoComplete: "off", onChange: function (e) { setRInvite(e.target.value); } })),
6042
+ rcap && field("图形验证码", h("div", { className: "dru-captcha" },
6043
+ input({ value: rcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRcapTxt(e.target.value); } }),
6044
+ h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("register"); }, dangerouslySetInnerHTML: rcap.svg && rcap.svg.indexOf("<svg") === 0 ? { __html: rcap.svg } : void 0 },
6045
+ rcap.svg && rcap.svg.indexOf("<svg") !== 0 ? rcap.svg : null)
6046
+ )),
6047
+ field("短信验证码", h("div", { className: "dru-captcha" },
6048
+ input({ value: rsms, placeholder: "6 位验证码", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRsms(e.target.value); } }),
6049
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { flex: "none", padding: "0 14px" }, disabled: busy !== "", onClick: sendRegSms }, rsmsBtn)
6050
+ )),
6051
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doRegister }, busy === "register" ? "注册中…" : "注册")
6052
+ ),
6053
+ message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
6054
+ );
6055
+ }
6056
+
6057
+ /** 手机访问弹层:二维码/链接/倒计时(复用既有 renderAccessCard)+ 云端/自建分段。 */
6058
+ function renderMobileModal() {
6059
+ if (iaModal !== "mobile") return null;
6060
+ var isLocal = mode === "local";
6061
+ return iaDialog(Object.assign({}, {
6062
+ title: "手机访问",
6063
+ sub: "手机扫码或在浏览器里打开链接,即可远程使用这台电脑上的 DSH",
6064
+ onClose: closeIAModal,
6065
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "完成")
6066
+ }, { children: [h("div", { className: "dru-ia-seg", role: "tablist", "aria-label": "接入方式" },
6067
+ h("button", {
6068
+ type: "button", role: "tab", "aria-selected": !isLocal ? "true" : "false",
6069
+ className: "dru-ia-seg-btn" + (!isLocal ? " is-active" : ""),
6070
+ onClick: function () { setMode("saas"); setMessage(null); }
6071
+ }, "云端服务"),
6072
+ h("button", {
6073
+ type: "button", role: "tab", "aria-selected": isLocal ? "true" : "false",
6074
+ className: "dru-ia-seg-btn" + (isLocal ? " is-active" : ""),
6075
+ onClick: function () { setMode("local"); setMessage(null); }
6076
+ }, "自建服务")
6077
+ ),
6078
+ isLocal ? renderSelfHostBody() : renderAccessCard(),
6079
+ // 已授权设备概览(与入口网格里的设备卡同源):扫码/发链接之后「哪台手机已授权」是同一件事的下半段。
6080
+ (!isLocal && loggedIn)
6081
+ ? h("div", null,
6082
+ h("div", { className: "dru-ia-card-actions", style: { marginTop: 4 } },
6083
+ h("button", {
6084
+ type: "button", className: "dru-ia-btn",
6085
+ onClick: function () { setIaModal("devices"); }
6086
+ }, "已授权设备 " + (devSessions === null ? "…" : devSessions.length) + "(管理)")),
6087
+ devMsg ? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 8 } }, devMsg.text) : null)
6088
+ : null] }));
6089
+ }
6090
+
6091
+ /** 通道列表行(弹层第一步)。 */
6092
+ function iaChannelRow(icon, title, sub, go, goText) {
6093
+ return h("button", { type: "button", className: "dru-ia-row", onClick: go },
6094
+ h("span", { className: "dru-ia-ico" }, druIcon(icon)),
6095
+ h("span", { className: "dru-ia-row-main" },
6096
+ h("span", { className: "dru-ia-row-title" }, title),
6097
+ h("span", { className: "dru-ia-row-sub" }, sub)),
6098
+ h("span", { className: "dru-ia-row-go" }, goText, druIcon("right"))
6099
+ );
6100
+ }
6101
+
6102
+ /** 统一的表单字段结构(label + 控件 + 错误位;错误就近显示)。 */
6103
+ function iaField(label, htmlFor, control, errText, hintText, extraHintId) {
6104
+ // 同时带 dru-field 语义类名(任务里约定的字段结构名),样式仍由 .dru-ia-field 承担。
6105
+ return h("div", { className: "dru-ia-field dru-field" },
6106
+ h("label", { className: "dru-ia-label", htmlFor: htmlFor }, label),
6107
+ control,
6108
+ errText ? h("div", { id: extraHintId || undefined, className: "dru-ia-err", role: "alert" }, errText) : null,
6109
+ hintText ? h("div", { className: "dru-ia-hint" }, hintText) : null
6110
+ );
6111
+ }
6112
+ /** 会话 id 的短形态(下拉里标题可能重复,短 id 帮用户区分)。 */
6113
+ function iaShortId(id) {
6114
+ var s2 = String(id || "");
6115
+ return s2.length > 8 ? (s2.slice(0, 8) + "…") : s2;
6116
+ }
6117
+ /** 会话标题截断(业主:下拉文案别撑爆一行)。 */
6118
+ function iaTruncTitle(t) {
6119
+ var s2 = String(t || "").trim();
6120
+ if (!s2) return "未命名会话";
6121
+ return s2.length > 24 ? (s2.slice(0, 24) + "…") : s2;
6122
+ }
6123
+ /** 时间戳 → 本地 HH:MM(拿不到就空串,绝不显示 Invalid Date)。 */
6124
+ function iaHHMM(ms) {
6125
+ var n = Number(ms);
6126
+ if (!Number.isFinite(n) || n <= 0) return "";
6127
+ try {
6128
+ var d = new Date(n);
6129
+ var p = function (v) { return String(v).padStart(2, "0"); };
6130
+ return p(d.getHours()) + ":" + p(d.getMinutes());
6131
+ } catch (e) { return ""; }
6132
+ }
6133
+ /** 当前实际转发目标的一句话(绑定会话:<title or 跟随活跃>)。 */
6134
+ function iaBoundSessionText() {
6135
+ var ws = (imBots && imBots.status) || {};
6136
+ var list = Array.isArray(ws.sessions) ? ws.sessions : [];
6137
+ var bound = String(ws.boundSessionId || "");
6138
+ if (bound) {
6139
+ var hit = null;
6140
+ for (var i = 0; i < list.length; i++) { if (list[i] && list[i].id === bound) { hit = list[i]; break; } }
6141
+ return iaTruncTitle((hit && hit.title) || bound);
6142
+ }
6143
+ var activeTitle = String(ws.activeSessionTitle || "");
6144
+ return activeTitle ? ("跟随活跃(" + iaTruncTitle(activeTitle) + ")") : "跟随活跃会话";
6145
+ }
6146
+
6147
+ /** 企业微信配置步骤:会话绑定 / Bot ID / Secret(不回显)/ 接收会话 ID / 保存并连接 / 测试。 */
6148
+ function renderWecomStep(close) {
6149
+ var w = (imBots && imBots.wecom) || {};
6150
+ var ws = (imBots && imBots.status) || {};
6151
+ var fieldErr = imBotsFieldErr || {};
6152
+ var sessions = Array.isArray(ws.sessions) ? ws.sessions : [];
6153
+ var stateLine = null;
6154
+ if (imBotsBusy === "test") {
6155
+ stateLine = h("div", { className: "dru-ia-state", role: "status", "aria-live": "polite" }, h("span", { className: "dru-spin" }), "正在连接企业微信…(最多 10 秒)");
6156
+ } else if (imBotsMsg) {
6157
+ stateLine = h("div", {
6158
+ className: "dru-ia-state " + (imBotsMsg.kind === "ok" ? "is-ok" : imBotsMsg.kind === "warn" ? "is-warn" : "is-err"),
6159
+ role: "status", "aria-live": "polite"
6160
+ }, imBotsMsg.text);
6161
+ } else if (ws.connected) {
6162
+ stateLine = h("div", { className: "dru-ia-state is-ok", role: "status" }, druIcon("check"), "已连接" + (ws.authenticated ? "(鉴权通过)" : ""));
6163
+ } else if (w.enabled || w.hasSecret) {
6164
+ stateLine = h("div", { className: "dru-ia-state", role: "status" }, "已保存,等待连接…" + (ws.lastError ? ":" + ws.lastError : ""));
6165
+ }
6166
+
6167
+ // 会话下拉:默认「跟随当前活跃会话」+ 状态文件里的会话(只读桥写的数据,不自己拉会话列表)
6168
+ var sessionOptions = [h("option", { value: "" }, "跟随当前活跃会话(默认)")];
6169
+ for (var i = 0; i < sessions.length; i++) {
6170
+ var it = sessions[i] || {};
6171
+ if (!it.id) continue;
6172
+ sessionOptions.push(h("option", { value: String(it.id) },
6173
+ iaTruncTitle(it.title) + " · " + iaShortId(it.id)));
6174
+ }
6175
+ var sessionSelect = h("select", {
6176
+ id: "dru-wecom-session", className: "dru-ia-input", value: String(imBotsSessionId || ""),
6177
+ "aria-invalid": fieldErr.session ? "true" : "false",
6178
+ "aria-describedby": fieldErr.session ? "dru-wecom-session-err" : undefined,
6179
+ onChange: function (e) {
6180
+ var sid = String(e.target.value || "");
6181
+ var title = "";
6182
+ for (var k = 0; k < sessions.length; k++) {
6183
+ if (sessions[k] && String(sessions[k].id) === sid) { title = String(sessions[k].title || ""); break; }
6184
+ }
6185
+ setImBotsSessionId(sid);
6186
+ setImBotsSessionTitle(sid ? title : "");
6187
+ }
6188
+ }, sessionOptions);
6189
+
6190
+ return iaDialog(Object.assign({}, {
6191
+ title: "企业微信智能机器人",
6192
+ sub: "填 Bot ID 与 Secret 即可;Secret 只保存在本机,不会回显",
6193
+ onClose: close,
6194
+ foot: [
6195
+ h("button", {
6196
+ type: "button", className: "dru-ia-btn is-primary", disabled: imBotsBusy !== "",
6197
+ onClick: function () { saveImBots(true); }
6198
+ }, imBotsBusy === "test" ? "连接中…" : "保存并连接"),
6199
+ h("button", { type: "button", className: "dru-ia-btn", disabled: imBotsBusy !== "", onClick: function () { saveImBots(false); } },
6200
+ imBotsBusy === "save" ? "保存中…" : "仅保存"),
6201
+ h("button", { type: "button", className: "dru-ia-btn is-ghost", disabled: imBotsBusy !== "", onClick: function () { testImBots(); } }, "测试"),
6202
+ // 次要动作:发一张演示卡(按钮不会真的执行),不抢主操作。
6203
+ h("button", {
6204
+ type: "button", className: "dru-ia-btn is-ghost", disabled: imBotsBusy !== "",
6205
+ onClick: function () { sendPreviewCard(); }
6206
+ }, imBotsBusy === "preview" ? "发送中…" : "发送预览卡片")
6207
+ ]
6208
+ }, { children: [
6209
+ // 当前实际转发目标:一行说清楚(状态不只靠颜色/图标)
6210
+ h("div", { className: "dru-ia-state" }, druIcon("chat"), "绑定会话:" + iaBoundSessionText()),
6211
+ // 没有接收方时如实提示(不拦着点:桥会记日志跳过,用户自己能看到结果)
6212
+ (!ws.boundChatId && !String(imBotsChat || "").trim() && !String(w.targetChatId || "").trim())
6213
+ ? h("div", { className: "dru-ia-hint" }, "还没有接收方:先在企微里给机器人发一句话完成绑定(现在点也能发,桥会跳过)。")
6214
+ : null,
6215
+ iaHHMM(ws.lastPreviewCardAt)
6216
+ ? h("div", { className: "dru-ia-hint" }, "上次预览:" + iaHHMM(ws.lastPreviewCardAt))
6217
+ : null,
6218
+ iaField("转接到哪个会话", "dru-wecom-session", sessionSelect,
6219
+ fieldErr.session || "",
6220
+ sessions.length
6221
+ ? "给机器人发消息时,转发到这个会话;默认跟随当前活跃会话。"
6222
+ : "还没有会话,去创建一个任务后这里会出现。",
6223
+ "dru-wecom-session-err"),
6224
+ iaField("Bot ID", "dru-wecom-bot",
6225
+ h("input", {
6226
+ id: "dru-wecom-bot", className: "dru-ia-input", type: "text", value: imBotsBotId,
6227
+ placeholder: "aibxxxxxxxx", autoComplete: "off", spellCheck: false,
6228
+ "aria-invalid": fieldErr.botId ? "true" : "false",
6229
+ "aria-describedby": fieldErr.botId ? "dru-wecom-bot-err" : undefined,
6230
+ onChange: function (e) { setImBotsBotId(e.target.value); }
6231
+ }),
6232
+ fieldErr.botId || "", null, "dru-wecom-bot-err"),
6233
+ iaField("Secret", "dru-wecom-secret",
6234
+ h("div", { className: "dru-ia-input-row" },
6235
+ h("input", {
6236
+ id: "dru-wecom-secret", className: "dru-ia-input",
6237
+ type: imBotsShowSecret ? "text" : "password", value: imBotsSecret,
6238
+ placeholder: w.hasSecret ? "已保存(留空表示不修改)" : "粘贴机器人 Secret",
6239
+ autoComplete: "new-password", spellCheck: false,
6240
+ "aria-invalid": fieldErr.secret ? "true" : "false",
6241
+ "aria-describedby": fieldErr.secret ? "dru-wecom-secret-err" : undefined,
6242
+ onChange: function (e) { setImBotsSecret(e.target.value); }
6243
+ }),
6244
+ h("button", {
6245
+ type: "button", className: "dru-ia-iconbtn",
6246
+ "aria-label": imBotsShowSecret ? "隐藏 Secret" : "显示 Secret",
6247
+ onClick: function () { setImBotsShowSecret(!imBotsShowSecret); }
6248
+ }, druIcon(imBotsShowSecret ? "eyeoff" : "eye"))
5544
6249
  ),
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)
6250
+ fieldErr.secret || "", "只保存在本机;已保存的 Secret 永远不回显。", "dru-wecom-secret-err"),
6251
+ iaField("接收会话 ID(可选)", "dru-wecom-chat",
6252
+ h("input", {
6253
+ id: "dru-wecom-chat", className: "dru-ia-input", type: "text", value: imBotsChat,
6254
+ placeholder: "留空即可", autoComplete: "off", spellCheck: false,
6255
+ "aria-invalid": fieldErr.chat ? "true" : "false",
6256
+ onChange: function (e) { setImBotsChat(e.target.value); }
6257
+ }),
6258
+ fieldErr.chat || "", "不知道填什么就留空——你在企业微信里给机器人发一句话后会自动绑定。"),
6259
+ // 高级项收进折叠:主表单只留 4 个字段(一屏一主操作)
6260
+ h("div", { className: "dru-ia-field" },
6261
+ h("button", {
6262
+ type: "button", className: "dru-ia-disclose", "aria-expanded": imBotsWhyOpen ? "true" : "false",
6263
+ onClick: function () { setImBotsWhyOpen(!imBotsWhyOpen); }
6264
+ }, "为什么要填这些", druIcon(imBotsWhyOpen ? "left" : "right")),
6265
+ imBotsWhyOpen
6266
+ ? h("div", { className: "dru-ia-hint", style: { padding: "8px 2px 0" } },
6267
+ "Bot ID/Secret 来自企业微信「智能机器人」页,机器人靠它连接;接收会话 ID 与转接会话决定消息发到哪个对话;服务地址仅私有化部署才需要改。",
6268
+ h("div", { style: { marginTop: 8 } },
6269
+ h("label", { className: "dru-ia-label", htmlFor: "dru-wecom-url" }, "服务地址(可选,高级)"),
6270
+ h("input", {
6271
+ id: "dru-wecom-url", className: "dru-ia-input", type: "url", value: imBotsUrl,
6272
+ placeholder: "默认使用企业微信官方接口,一般无需填写", autoComplete: "off", spellCheck: false,
6273
+ "aria-invalid": fieldErr.url ? "true" : "false",
6274
+ onChange: function (e) { setImBotsUrl(e.target.value); }
6275
+ }),
6276
+ fieldErr.url ? h("div", { className: "dru-ia-err", role: "alert" }, fieldErr.url) : null))
5578
6277
  : null
5579
6278
  ),
5580
- // 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
5581
- 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)。
5598
- h("div", { className: "dru-hint", style: { marginTop: 6 } },
5599
- "📊 匿名装机统计:只上报「装机/连接是否成功」这类事件(安装开始与失败原因、后台服务是否注册成功、是否首次远程打通)——"
5600
- + "不含任何账号、手机号、会话或文件内容、主机名、路径、设备指纹与 IP;标识是本机随机 ID(重装即变)。"
5601
- + "用环境变量 DSH_REMOTE_TELEMETRY=0 可完全关闭(不生成 ID、不发任何请求)。")
5602
- // ⚠️ 「💬 加入交流群」按钮已搬到「🔑 账号」卡的功能区(与「💬 用户反馈」并列)——
5603
- // 那里才是"有事想找人问"时会看的地方;这里只剩阅读性内容。
5604
- ]),
5605
- message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text)
6279
+ stateLine
6280
+ ] }));
6281
+ }
6282
+
6283
+ /** 「微信机器人(扫码绑定)」步骤:复用既有 WeChatBotSection;未登录时给登录入口(不再隐藏功能)。 */
6284
+ function renderWechatStep(close) {
6285
+ return iaDialog(Object.assign({}, {
6286
+ title: "微信机器人(扫码绑定)",
6287
+ sub: "扫码绑定后,任务完成/出错/需要拍板时会推到你的微信",
6288
+ onClose: close,
6289
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: function () { setIaChStep("list"); } }, "返回平台列表")
6290
+ }, { children: [loggedIn
6291
+ ? h(WeChatBotSection, { embedded: true })
6292
+ : h("div", { className: "dru-ia-empty" },
6293
+ druIcon("user", 24),
6294
+ h("div", { className: "dru-ia-empty-title" }, "登录后可绑定微信机器人"),
6295
+ h("div", null, "微信通道需要账号身份(用来把你的电脑和微信对上)。登录后回到这里即可扫码绑定。"),
6296
+ h("button", {
6297
+ type: "button", className: "dru-ia-btn is-primary",
6298
+ onClick: function () { setIaModal("account"); setIaChStep("list"); }
6299
+ }, "去登录"))] }));
6300
+ }
6301
+
6302
+ /** 还没支持的平台:只给**诚实**的空状态(说明 + 反馈入口),绝不放假表单/假入口。 */
6303
+ function renderComingSoonStep(close, kind) {
6304
+ var meta = {
6305
+ feishu: {
6306
+ name: "飞书机器人",
6307
+ why: "飞书官方长连接接入需要企业管理员配置应用(应用凭证 + 机器人能力),接入方式与企业微信类似。"
6308
+ },
6309
+ dingtalk: {
6310
+ name: "钉钉机器人",
6311
+ why: "钉钉应用机器人需要企业管理员创建应用并开通机器人权限,接入方式与企业微信类似。"
6312
+ },
6313
+ webhook: {
6314
+ name: "高级 · Webhook",
6315
+ why: "Webhook 需要你自行提供公网 HTTPS 端点并校验签名;我们会把它作为同一条通道的高级选项。"
6316
+ }
6317
+ }[kind] || { name: "这个平台", why: "接入方式还在评估中。" };
6318
+ return iaDialog(Object.assign({}, {
6319
+ title: meta.name,
6320
+ sub: "即将支持",
6321
+ onClose: close,
6322
+ foot: [
6323
+ h("button", {
6324
+ type: "button", className: "dru-ia-btn is-primary",
6325
+ // 复用现有反馈通道:关掉弹层 → 打开反馈视图并带一句引导(不新增后端接口)
6326
+ onClick: function () {
6327
+ setIaModal(null); setIaChStep("list");
6328
+ setIaFbHint("在下面写一句「想要 " + meta.name + "」,我们会按需求排期。");
6329
+ setView("feedback");
6330
+ }
6331
+ }, "告诉我,优先支持"),
6332
+ h("button", { type: "button", className: "dru-ia-btn", onClick: function () { setIaChStep("list"); } }, "返回平台列表")
6333
+ ]
6334
+ }, { children: [
6335
+ h("div", { className: "dru-ia-empty" },
6336
+ druIcon("bell", 24),
6337
+ h("div", { className: "dru-ia-empty-title" }, meta.name + " 即将支持"),
6338
+ h("div", null, meta.why),
6339
+ h("div", { className: "dru-ia-hint", style: { marginTop: 4 } }, "现在可以先用「企业微信智能机器人」。"))
6340
+ ] }));
6341
+ }
6342
+
6343
+ /** 通道弹层:平台列表 → 配置步骤(一级一屏,不做长表单堆叠)。 */
6344
+ function renderChannelModal() {
6345
+ if (iaModal !== "channel") return null;
6346
+ if (iaChStep === "wecom") return renderWecomStep(closeIAModal);
6347
+ if (iaChStep === "wechat") return renderWechatStep(closeIAModal);
6348
+ if (iaChStep === "feishu" || iaChStep === "dingtalk" || iaChStep === "webhook") return renderComingSoonStep(closeIAModal, iaChStep);
6349
+ var w = (imBots && imBots.wecom) || {};
6350
+ var ws = (imBots && imBots.status) || {};
6351
+ var wecomSub = ws.connected
6352
+ ? "已连接 · 推送消息到企业微信"
6353
+ : (w.enabled || w.hasSecret ? "已配置,等待连接" : "未配置 · 填 Bot ID 与 Secret 即可");
6354
+ return iaDialog(Object.assign({}, {
6355
+ title: "通知与消息通道",
6356
+ sub: "选一个平台,让电脑端在任务完成、出错或需要拍板时主动通知你",
6357
+ onClose: closeIAModal,
6358
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "关闭")
6359
+ }, { children: [h("div", { className: "dru-ia-list" },
6360
+ iaChannelRow("chat", "企业微信智能机器人", wecomSub, function () { setIaChStep("wecom"); setImBotsMsg(null); setImBotsFieldErr({}); }, "配置"),
6361
+ iaChannelRow("chat", "微信机器人(扫码绑定)", "扫码绑定 · 在微信里收通知 / 回数字拍板", function () { setIaChStep("wechat"); }, "打开"),
6362
+ iaChannelRow("send", "飞书机器人", "即将支持 · 需要企业管理员配置应用", function () { setIaChStep("feishu"); }, "查看"),
6363
+ iaChannelRow("send", "钉钉机器人", "即将支持 · 需要企业管理员创建应用", function () { setIaChStep("dingtalk"); }, "查看"),
6364
+ iaChannelRow("code", "高级 · Webhook", "高级用法 · 自备公网端点", function () { setIaChStep("webhook"); }, "查看")
6365
+ )] }));
6366
+ }
6367
+
6368
+ /** 已授权设备弹层:复用既有设备列表(渲染逻辑一行未改,只是从卡片挪进弹层)。 */
6369
+ function renderDevicesModal() {
6370
+ if (iaModal !== "devices") return null;
6371
+ return iaDialog(Object.assign({}, {
6372
+ title: "已授权设备",
6373
+ sub: "这些手机/电脑已经可以远程访问本机;撤销后需要重新授权",
6374
+ onClose: closeIAModal,
6375
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "完成")
6376
+ }, { children: [!loggedIn
6377
+ ? h("div", { className: "dru-ia-empty" },
6378
+ druIcon("devices", 24),
6379
+ h("div", { className: "dru-ia-empty-title" }, "登录后可查看已授权设备"),
6380
+ h("div", null, "设备列表跟账号走:登录后这里会显示每一台已授权的手机/电脑。"),
6381
+ h("button", { type: "button", className: "dru-ia-btn is-primary", onClick: function () { setIaModal("account"); } }, "去登录"))
6382
+ : h("div", null,
6383
+ h("div", { className: "dru-ia-card-actions", style: { marginBottom: 8 } },
6384
+ h("button", {
6385
+ type: "button", className: "dru-ia-btn", disabled: devBusy !== "",
6386
+ onClick: function () { try { loadDevices(); } catch (e1) { /* 忽略 */ } }
6387
+ }, "已授权设备 " + (devSessions === null ? "…" : devSessions.length) + "(刷新)")),
6388
+ devSessions === null ? h("div", { className: "dru-hint" }, "正在读取设备列表…") : renderDeviceList(),
6389
+ devMsg
6390
+ ? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 6 } },
6391
+ devMsg.text,
6392
+ devMsg.retry
6393
+ ? h("button", { type: "button", className: "dru-linkbtn", style: { marginLeft: 8 }, disabled: devBusy !== "", onClick: devMsg.retry }, devBusy === "list" ? "重试中…" : "立即重试")
6394
+ : null)
6395
+ : null)] }));
6396
+ }
6397
+
6398
+ /** 账号弹层:登录不再隐藏任何功能 —— 未登录只是卡片写「登录后可同步」,点进来登录即可。 */
6399
+ function renderAccountModal() {
6400
+ if (iaModal !== "account") return null;
6401
+ return iaDialog(Object.assign({}, {
6402
+ title: "账号",
6403
+ sub: "登录后可同步设备、生成访问码;账号只用于你自己的设备之间同步",
6404
+ onClose: closeIAModal,
6405
+ foot: h("button", { type: "button", className: "dru-ia-btn", onClick: closeIAModal }, "完成")
6406
+ }, { children: [renderAccountBody()] }));
6407
+ }
6408
+
6409
+ function renderIAModals() {
6410
+ return h("div", null,
6411
+ renderMobileModal(),
6412
+ renderChannelModal(),
6413
+ renderDevicesModal(),
6414
+ renderAccountModal()
6415
+ );
6416
+ }
6417
+
6418
+ /**
6419
+ * 主面板:状态头 + 入口网格 + 一行文字入口(+ 折叠的高级设置)。
6420
+ * 「不做整页滚动」:这里最多两屏;细节一律进弹层(弹层内部才允许滚动)。
6421
+ */
6422
+ function renderHome() {
6423
+ return h("div", { className: "dru-ia" },
6424
+ renderSetupAlert(),
6425
+ message ? h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text) : null,
6426
+ renderIAHeader(),
6427
+ renderAnnouncementsCard(),
6428
+ renderIAGrid(),
6429
+ renderIALinks(),
6430
+ // 高级设置默认折叠:**节点始终在 DOM 里**(<details> 的等价语义),
6431
+ // 但折叠时用 hidden 完全收起 —— 主面板保持清爽,键盘/读屏也不会读到隐藏内容。
6432
+ h("div", { hidden: moreOpen ? undefined : "" , "aria-hidden": moreOpen ? undefined : "true" }, renderMoreSettings())
5606
6433
  );
5607
6434
  }
5608
6435
 
@@ -5668,6 +6495,7 @@ window.__ModuleLoader__.load({
5668
6495
  };
5669
6496
 
5670
6497
  /** 交流群弹窗(复用满意度弹窗的 dru-popup 视觉;点遮罩/关闭即收起)。 */
6498
+
5671
6499
  function renderCommunityModal() {
5672
6500
  if (!commOpen || !(community && community.qrcode)) return null;
5673
6501
  var close = function () { setCommOpen(false); };
@@ -5864,7 +6692,7 @@ window.__ModuleLoader__.load({
5864
6692
 
5865
6693
  return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程访问", "data-dru-theme": theme },
5866
6694
  h("div", { className: "dru-settings-head" },
5867
- h("span", { className: "dru-settings-icon" }, "📱"),
6695
+ h("span", { className: "dru-settings-icon" }, druIcon("phone", 20)),
5868
6696
  h("div", null,
5869
6697
  h("h2", { className: "dru-settings-title" }, "远程访问"),
5870
6698
  h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)"),
@@ -5947,16 +6775,13 @@ window.__ModuleLoader__.load({
5947
6775
  )
5948
6776
  )
5949
6777
  : 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()
6778
+ // view 分派:feedback | invite | home("wechat" 视图已不存在 —— 微信通道在通道弹层里)
6779
+ view === "feedback" ? renderFeedback()
5957
6780
  : view === "invite" ? renderInvite()
5958
6781
  : renderHome()
5959
6782
  ),
6783
+ // 🧭 弹层统一挂在面板根上(fixed 定位,不占布局):任何视图下都能开、都能 Esc 关闭。
6784
+ renderIAModals(),
5960
6785
  // 2026-10-17(业主):删除「重启 DeepSeek Harness」整段说明 ——
5961
6786
  // 桌面端运行环境由应用本身托管、插件安装/更新自动热加载,刷新页面即可;
5962
6787
  // 只有非桌面端(自建 dsh web)才保留重启入口,且文案只讲这一种场景。
@@ -5979,8 +6804,8 @@ window.__ModuleLoader__.load({
5979
6804
  // ══ 🤖 微信机器人(「📱 远程访问」面板里的第三个 tab:微信机器人通道) ═════════
5980
6805
  // 【0.6.11 用户约定】「不要给它单独弄一个菜单,直接放到面板里面。在面板里单开一个
5981
6806
  // table 页,命名为『微信机器人通道』,并增加上微信的绿泡泡小图标,点开后可以进行绑定设置。」
5982
- // → 不再注册第二个 settings.section 栏目(只有 📱 远程访问 一个),本组件由 renderWeChat()
5983
- // 以 embedded=true 作为**子组件**渲染(见 renderMainTabs / renderWeChat)。
6807
+ // → 不再注册第二个 settings.section 栏目(只有「远程访问」一个),本组件由通道弹层的
6808
+ // 「微信机器人(扫码绑定)」步骤以 embedded=true 作为**子组件**渲染(见 renderWechatStep)。
5984
6809
  // 数据全部来自同源 /dsh-remote/wechat/*(宿主半边代理 bridge 的控制面)。
5985
6810
  //
5986
6811
  // ⚠️ 令牌铁律:本组件**只**渲染 bridge 已脱敏的字段;不请求、不拼接、不缓存任何 token
@@ -6621,7 +7446,7 @@ window.__ModuleLoader__.load({
6621
7446
  // order 30 > Agent 预设(20),栏目落在「Agent 预设」下方;label 即栏目名(📱 远程访问)。
6622
7447
  // 【0.6.11】本插件在这个槽里**只有一个栏目**(📱 远程访问):🤖 微信机器人原先以 order 31
6623
7448
  // 注册成第二个栏目,用户约定「不要给它单独弄一个菜单,直接放到面板里面」→ 已改成面板里的
6624
- // 第三个 tab(renderMainTabs / renderWeChat)。不要再往这个槽里加第二个 register。
7449
+ // 通道弹层里的微信步骤(renderWechatStep)。不要再往这个槽里加第二个 register。
6625
7450
  /* 📱 侧栏底部的快捷入口 —— 官方列表槽 `sidebar.footer.action`(2026-10-10 三修)。
6626
7451
  这个槽由 `@deepseek-ai/dsh-client-ui-sidebar` 声明,渲染在侧栏底部的 footerActions 里
6627
7452
  (在「设置」那一行的上方、与「意见反馈/登录/更多」同属底部区域)。0.1.x 与 0.2 都有该槽,