dsh-remote-web 0.7.10-beta.10 → 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 +1048 -223
- package/lib/index.js +299 -2
- package/package.json +1 -1
- package/runtime/clients/dsh-remote/dsh-bridge.mjs +99 -6
- package/runtime/clients/dsh-remote/wecom-bot.mjs +2444 -0
- package/runtime/dsh-setup.mjs +18 -3
package/lib/client.js
CHANGED
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
// 旧代码用「文本包含『设置』」去找锚点 → 按钮被塞进设置面板栏目列表最上面(用户反馈的
|
|
15
15
|
// 「植入错了地方」)。现在找不到锚点就**不注入**,绝不将就一个错误的落点。
|
|
16
16
|
// - 栏目内联渲染配置面板(浅色高对比 UI,遵循主流登录体验)
|
|
17
|
-
// · 登录态:已登录显示账号 + 退出登录;未登录显示
|
|
17
|
+
// · 登录态:已登录显示账号 + 退出登录;未登录显示 登录/注册(在「账号」弹层的分段里)
|
|
18
18
|
// · 🔒 修改密码(账号卡):已登录账号经 图形验证码+短信验证码 重置(/dsh-remote/password/reset 代理);
|
|
19
19
|
// 成功后全部已授权设备/会话失效 → 本地登出并提示用新密码重新登录(覆盖本机 config 密码)
|
|
20
20
|
// · 忘记密码(登录卡):未登录时登录表单内「忘记密码?」→ 同一套
|
|
21
21
|
// 图形验证码+短信验证码 表单(手机号预填当前输入、可改),成功后本地登出并提示「请用新密码登录」
|
|
22
22
|
// · 登录要求图形验证码;注册要求两次密码 + 图形验证码
|
|
23
23
|
// · 📱 远程访问卡:一次性访问密钥(扫码/直接打开/复制 + 到期倒计时自动刷新)与已授权设备管理
|
|
24
|
-
// · 🤖
|
|
25
|
-
//
|
|
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
|
-
|
|
3678
|
-
|
|
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
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
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
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
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
|
-
|
|
5441
|
-
|
|
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
|
-
|
|
5444
|
-
|
|
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
|
-
|
|
5448
|
-
|
|
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
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
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
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
h("div",
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
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
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
:
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
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
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
h(
|
|
5593
|
-
h("div", { className: "dru-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
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 分派:
|
|
5951
|
-
|
|
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
|
|
5983
|
-
//
|
|
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
|
-
//
|
|
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 都有该槽,
|