dsh-im-union 0.3.1 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.en.md +3 -2
  3. package/README.md +3 -2
  4. package/client.js +2224 -1633
  5. package/dsh.plugin.json +1 -1
  6. package/lib/admin/api.js +2 -1
  7. package/lib/admin/channels.d.ts +27 -0
  8. package/lib/admin/channels.js +25 -0
  9. package/lib/channels/types.d.ts +4 -0
  10. package/lib/channels/weixin.d.ts +18 -1
  11. package/lib/channels/weixin.js +63 -9
  12. package/lib/client.js +2224 -1633
  13. package/lib/credentials/vault.d.ts +2 -2
  14. package/lib/credentials/vault.js +103 -5
  15. package/lib/engine/account-manager.d.ts +33 -2
  16. package/lib/engine/account-manager.js +74 -9
  17. package/lib/engine/commands.d.ts +18 -0
  18. package/lib/engine/commands.js +126 -13
  19. package/lib/engine/delivery-log.d.ts +2 -0
  20. package/lib/engine/delivery-log.js +12 -0
  21. package/lib/engine/gateway.d.ts +50 -1
  22. package/lib/engine/gateway.js +312 -45
  23. package/lib/engine/preset-supply.d.ts +44 -0
  24. package/lib/engine/preset-supply.js +114 -0
  25. package/lib/engine/relay.d.ts +25 -0
  26. package/lib/engine/relay.js +109 -0
  27. package/lib/engine/reply-sanitize.d.ts +25 -0
  28. package/lib/engine/reply-sanitize.js +68 -0
  29. package/lib/engine/router.d.ts +30 -2
  30. package/lib/engine/router.js +45 -4
  31. package/lib/engine/svg-render.d.ts +11 -0
  32. package/lib/engine/svg-render.js +57 -0
  33. package/lib/host-compat/session-api.d.ts +26 -1
  34. package/lib/host-compat/session-api.js +257 -56
  35. package/lib/host-compat/session-event-tap.d.ts +16 -0
  36. package/lib/host-compat/session-event-tap.js +45 -0
  37. package/lib/host-compat/settings-compat.d.ts +4 -1
  38. package/lib/host-compat/settings-compat.js +22 -7
  39. package/lib/index.d.ts +10 -4
  40. package/lib/index.js +201 -13
  41. package/lib/plugin-log.d.ts +12 -0
  42. package/lib/plugin-log.js +68 -0
  43. package/package.json +2 -2
  44. package/src/admin/api.ts +2 -1
  45. package/src/admin/channels.ts +48 -0
  46. package/src/channels/types.ts +4 -0
  47. package/src/channels/weixin.ts +72 -7
  48. package/src/credentials/vault.ts +102 -5
  49. package/src/engine/account-manager.ts +85 -10
  50. package/src/engine/commands.ts +133 -12
  51. package/src/engine/delivery-log.ts +11 -0
  52. package/src/engine/gateway.ts +342 -37
  53. package/src/engine/preset-supply.ts +129 -0
  54. package/src/engine/relay.ts +133 -0
  55. package/src/engine/reply-sanitize.ts +81 -0
  56. package/src/engine/router.ts +129 -78
  57. package/src/engine/svg-render.ts +71 -0
  58. package/src/host-compat/session-api.ts +664 -447
  59. package/src/host-compat/session-event-tap.ts +57 -0
  60. package/src/host-compat/settings-compat.ts +20 -6
  61. package/src/index.ts +186 -13
  62. package/src/plugin-log.ts +74 -0
package/client.js CHANGED
@@ -1,1633 +1,2224 @@
1
- /**
2
- * dsh-im-union 浏览器半(ModuleLoader 工厂,id 必须等于 npm 包名,否则整体静默失活)。
3
- *
4
- * 注册纪律:槽位注册必须发生在 client apply 期顶层——关在任何 inject 回调里
5
- * 是两代事故的根因。设置面代际差异在浏览器侧的体现:
6
- * - gen3(0.1.7+/0.2.x):settings.section 槽位挂面板;
7
- * - gen2(0.1.2~0.1.6):设置节由宿主半 installSection 出表单,client 只补状态卡;
8
- * - gen1(0.1.0/0.1.1):无设置页 → 自带浮窗兜底(本文件 overlayFallback)。
9
- * 宿主未声明的槽位会抛 SlotOwnershipError 并让整块 UI 消失——所有 renderSlot
10
- * 一律 try/catch,失败只当该槽位没有内容。
11
- *
12
- * 面板设计(v2,借鉴 dsh-im 的渠道卡视觉与 dsh-im-connect 的账号手风琴):
13
- * - 头部(品牌 + 副标语 + GitHub/刷新)→ 统计条 → 折叠诊断 → 渠道手风琴卡;
14
- * - 渠道卡 = 品牌色块 logo + 名称/标签 + 账号数 + 添加账号 + 展开箭头;
15
- * - 账号行 = 名称/内部键 + 状态徽章 + 检查/编辑/暂停/删除(两步确认)+ 白名单;
16
- * - 添加/编辑走同一弹窗:微信为扫码流(先建档后取码),其余渠道按 meta.fields
17
- * 出双列表单——secret 字段只写不读(留空=保持),plain 字段进 record.config
18
- * (修复旧版全塞 secrets 导致明文字段被 upsert 丢弃的问题)。
19
- * - 样式单 <style data-plugin> 注入;颜色全部走 var(--dsw-alias-*, fallback)
20
- * 消费链,暗色/浅色零分支,主题切换由宿主令牌接管。
21
- */
22
- window.__ModuleLoader__.load({
23
- id: 'dsh-im-union',
24
- factory: function (require) {
25
- // 插桩(真机诊断):宿主装载链哪一段没走到这里,日志即缺席——与 apply 入口日志
26
- // 成对,可二分「bundle 加载但 factory 未调」vs「factory 调了但 apply 未调」。
27
- console.info('[dsh-im-union] client factory invoked')
28
- var module = { exports: {} };
29
- var exports = module.exports;
30
- var React = require('react');
31
- var h = React.createElement;
32
- var useState = React.useState;
33
- var useEffect = React.useEffect;
34
- var useRef = React.useRef;
35
- var ReactDOM = null;
36
- try { ReactDOM = require('react-dom'); } catch (noReactDOM) { ReactDOM = null; }
37
- if (ReactDOM === null && typeof window !== 'undefined') ReactDOM = window.ReactDOM || null;
38
-
39
- var API_BASE = '/api/dsh-im-union';
40
- var REPO_URL = 'https://github.com/drscrewdriver/dsh-im-union';
41
- var STATE_POLL_MS = 60000;
42
- var ACCOUNTS_POLL_MS = 15000;
43
- var STYLE_ID = 'dsh-im-union-settings';
44
-
45
- // ── 语言词典(zh 真理源;其余语言缺键回落 zh,运行时字典与
46
- // locale/*.json 元数据两处分层,此处只放 client 侧 UI 文案)─────────────
47
- var LOCALES = {
48
- zh: {
49
- 'settings.title': 'IM 联合桥',
50
- 'panel.subtitle': '把多个 IM 平台接入本机 Harness:多账号独立会话、主动投递、跨宿主版本兼容。',
51
- 'panel.loading': '正在读取本机状态…',
52
- 'panel.error': '无法连接管理接口,请重启 Host 后刷新。',
53
- 'panel.refresh': '刷新',
54
- 'panel.github': 'GitHub',
55
- 'panel.debug': '运行时诊断',
56
- 'panel.generation': '设置面代际',
57
- 'panel.sessionBackend': '会话后端',
58
- 'panel.vault': '凭据存储',
59
- 'panel.uploads': '文件上传',
60
- 'panel.deliverables': '产物回传',
61
- 'panel.connector': 'AI Office Connector',
62
- 'panel.on': '已启用',
63
- 'panel.off': '未启用',
64
- 'panel.channels': '渠道实现度',
65
- 'panel.implemented': '已实现',
66
- 'panel.planned': '规划中',
67
- 'panel.stat.channels': '渠道',
68
- 'panel.stat.accounts': '账号',
69
- 'panel.stat.online': '在线',
70
- 'channel.feishu': '飞书',
71
- 'channel.weixin': '微信',
72
- 'channel.telegram': 'Telegram',
73
- 'channel.discord': 'Discord',
74
- 'channel.wecom': '企业微信',
75
- 'channel.dingtalk': '钉钉',
76
- 'channel.qq': 'QQ',
77
- 'channel.slack': 'Slack',
78
- 'channel.whatsapp': 'WhatsApp',
79
- 'channel.matrix': 'Matrix',
80
- 'channel.imessage': 'iMessage',
81
- 'channel.wecom-app': '企业微信(自建应用)',
82
- 'channel.email': '邮箱(IMAP/SMTP)',
83
- 'channel.dingtalk-notify': '钉钉通知(群机器人)',
84
- 'channel.qq-notify': 'QQ 通知(主动推送)',
85
- 'channel.tag.planned': '规划中',
86
- 'channel.tag.notify': '仅通知',
87
- 'channel.tag.macos': '仅 macOS',
88
- 'channel.count': '{n} 个账号',
89
- 'channel.emptyHint': '点击「添加账号」接入第一个机器人。',
90
- 'accounts.title': '账号管理',
91
- 'accounts.add': '添加账号',
92
- 'accounts.edit': '编辑',
93
- 'accounts.check': '检查连接',
94
- 'accounts.checking': '检查中…',
95
- 'accounts.pause': '暂停',
96
- 'accounts.resume': '恢复',
97
- 'accounts.remove': '删除',
98
- 'accounts.remove.confirm': '确认删除',
99
- 'accounts.state.connected': '已连接',
100
- 'accounts.state.disconnected': '未连接',
101
- 'accounts.state.paused': '已暂停',
102
- 'accounts.secret.missing': '凭据缺失:{keys}',
103
- 'accounts.alias': '别名(可选)',
104
- 'accounts.alias.placeholder': '给账号起个好认的名字',
105
- 'accounts.allowlist': '私聊白名单',
106
- 'accounts.allowlist.hint': '每行一个用户 ID;留空 = 拒绝所有私聊',
107
- 'accounts.allowlist.save': '保存白名单',
108
- 'accounts.saved': '已保存',
109
- 'accounts.saved.bindhint': '已保存(未连接):用「生成登录二维码」完成扫码授权后自动连接',
110
- 'accounts.removed': '已删除',
111
- 'accounts.qr.get': '生成登录二维码',
112
- 'accounts.qr.refresh': '重新获取二维码',
113
- 'accounts.qr.cancel': '取消扫码',
114
- 'accounts.qr.needid': '请先填写账号 ID 再获取二维码',
115
- 'accounts.qr.wait': '等待扫码…(用手机微信扫左侧二维码)',
116
- 'accounts.qr.scaned': '已扫码,请在手机上确认授权',
117
- 'accounts.qr.expired': '二维码已过期,请重新获取',
118
- 'accounts.qr.need_verifycode': '请输入手机上显示的验证码',
119
- 'accounts.qr.verify_code_blocked': '验证码错误次数过多,请重新获取二维码',
120
- 'accounts.qr.binded_redirect': '该 Bot 已绑定其它实例',
121
- 'accounts.qr.scaned_but_redirect': '已扫码,正在重定向等待确认',
122
- 'accounts.qr.code.submit': '提交验证码',
123
- 'accounts.qr.confirmed': '扫码绑定完成,凭据已保存并连接',
124
- 'accounts.qr.desc': '二维码由腾讯微信 iLink 服务签发。扫码确认后凭据只写入本机 Host,浏览器不会收到 bot_token。',
125
- 'modal.add.title': '添加{channel}账号',
126
- 'modal.edit.title': '编辑账号',
127
- 'modal.cancel': '取消',
128
- 'modal.save': '保存',
129
- 'modal.saving': '保存中…',
130
- 'modal.required': '* 为必填',
131
- 'modal.editSecretHint': '凭据留空表示保持现有值不变。',
132
- 'modal.autoId': '账号 ID 由系统自动生成,无需填写;别名用于列表展示。',
133
- 'modal.session.title': '会话偏好(新会话生效)',
134
- 'modal.session.hint': '仅对该账号新建的会话生效;已绑定的会话保持不变。',
135
- 'modal.session.none': '当前宿主无会话后端,会话偏好不可用。',
136
- 'access.title': '私聊准入',
137
- 'access.approved': '仅已批准用户',
138
- 'access.all': '所有私聊',
139
- 'access.all.tag': '私聊全开',
140
- 'access.hint': '「所有私聊」= 任何人都可私聊该机器人;白名单外的用户会收到未授权提示。',
141
- 'prefs.workspace': '工作区目录(cwd)',
142
- 'prefs.workspace.placeholder': '留空 = 宿主默认工作区',
143
- 'prefs.preset': 'Agent 预设',
144
- 'prefs.preset.default': '宿主默认',
145
- 'prefs.model': '模型',
146
- 'prefs.model.default': '宿主默认',
147
- 'prefs.effort': '推理等级',
148
- 'prefs.effort.default': '模型默认',
149
- 'catalog.loading': '目录读取中…',
150
- 'check.title': '诊断结果',
151
- 'check.passed': '通过',
152
- 'check.failed': '失败',
153
- 'check.unverified': '未验证',
154
- },
155
- en: {
156
- 'settings.title': 'IM Union Bridge',
157
- 'panel.subtitle': 'Connect IM platforms to your local Harness: multi-account sessions, proactive delivery, cross-version host support.',
158
- 'panel.loading': 'Loading local state…',
159
- 'panel.error': 'Management API unreachable. Restart the host and refresh.',
160
- 'panel.refresh': 'Refresh',
161
- 'panel.github': 'GitHub',
162
- 'panel.debug': 'Runtime diagnostics',
163
- 'panel.generation': 'Settings generation',
164
- 'panel.sessionBackend': 'Session backend',
165
- 'panel.vault': 'Credential vault',
166
- 'panel.uploads': 'File uploads',
167
- 'panel.deliverables': 'Deliverables',
168
- 'panel.connector': 'AI Office Connector',
169
- 'panel.on': 'enabled',
170
- 'panel.off': 'disabled',
171
- 'panel.channels': 'Channels',
172
- 'panel.implemented': 'implemented',
173
- 'panel.planned': 'planned',
174
- 'panel.stat.channels': 'Channels',
175
- 'panel.stat.accounts': 'Accounts',
176
- 'panel.stat.online': 'Online',
177
- 'channel.feishu': 'Feishu / Lark',
178
- 'channel.weixin': 'WeChat (iLink)',
179
- 'channel.telegram': 'Telegram',
180
- 'channel.discord': 'Discord',
181
- 'channel.wecom': 'WeCom',
182
- 'channel.dingtalk': 'DingTalk',
183
- 'channel.qq': 'QQ',
184
- 'channel.slack': 'Slack',
185
- 'channel.whatsapp': 'WhatsApp',
186
- 'channel.matrix': 'Matrix',
187
- 'channel.imessage': 'iMessage',
188
- 'channel.wecom-app': 'WeCom (self-built app)',
189
- 'channel.email': 'Email (IMAP/SMTP)',
190
- 'channel.dingtalk-notify': 'DingTalk notify (webhook)',
191
- 'channel.qq-notify': 'QQ notify (push)',
192
- 'channel.tag.planned': 'planned',
193
- 'channel.tag.notify': 'notify only',
194
- 'channel.tag.macos': 'macOS only',
195
- 'channel.count': '{n} accounts',
196
- 'channel.emptyHint': 'Click "Add account" to connect your first bot.',
197
- 'accounts.title': 'Accounts',
198
- 'accounts.add': 'Add account',
199
- 'accounts.edit': 'Edit',
200
- 'accounts.check': 'Check connection',
201
- 'accounts.checking': 'Checking…',
202
- 'accounts.pause': 'Pause',
203
- 'accounts.resume': 'Resume',
204
- 'accounts.remove': 'Remove',
205
- 'accounts.remove.confirm': 'Confirm remove',
206
- 'accounts.state.connected': 'Connected',
207
- 'accounts.state.disconnected': 'Disconnected',
208
- 'accounts.state.paused': 'Paused',
209
- 'accounts.secret.missing': 'Credential missing: {keys}',
210
- 'accounts.alias': 'Alias (optional)',
211
- 'accounts.alias.placeholder': 'A recognizable name for this account',
212
- 'accounts.allowlist': 'DM allowlist',
213
- 'accounts.allowlist.hint': 'One user ID per line; empty = reject all DMs',
214
- 'accounts.allowlist.save': 'Save allowlist',
215
- 'accounts.saved': 'Saved',
216
- 'accounts.saved.bindhint': 'Saved (disconnected): use "Get login QR code" to bind, it connects automatically',
217
- 'accounts.removed': 'Removed',
218
- 'accounts.qr.get': 'Get login QR code',
219
- 'accounts.qr.refresh': 'Refresh QR code',
220
- 'accounts.qr.cancel': 'Cancel',
221
- 'accounts.qr.needid': 'Fill the account ID first, then get the QR code',
222
- 'accounts.qr.wait': 'Waiting for scan… (scan with WeChat)',
223
- 'accounts.qr.scaned': 'Scanned — confirm on your phone',
224
- 'accounts.qr.expired': 'QR expired, fetch a new one',
225
- 'accounts.qr.need_verifycode': 'Enter the code shown on your phone',
226
- 'accounts.qr.verify_code_blocked': 'Too many wrong codes, fetch a new QR',
227
- 'accounts.qr.binded_redirect': 'This bot is already bound to another instance',
228
- 'accounts.qr.scaned_but_redirect': 'Scanned — redirecting, waiting for confirm',
229
- 'accounts.qr.code.submit': 'Submit code',
230
- 'accounts.qr.confirmed': 'QR binding done — credentials saved and connected',
231
- 'accounts.qr.desc': 'The QR code is signed by Tencent WeChat iLink. After scanning, credentials go straight into the local Host; the browser never sees the bot token.',
232
- 'modal.add.title': 'Add {channel} account',
233
- 'modal.edit.title': 'Edit account',
234
- 'modal.cancel': 'Cancel',
235
- 'modal.save': 'Save',
236
- 'modal.saving': 'Saving…',
237
- 'modal.required': '* required',
238
- 'modal.editSecretHint': 'Leave credentials blank to keep the current values.',
239
- 'modal.autoId': 'The account ID is generated automatically; the alias is for display only.',
240
- 'modal.session.title': 'Session preferences (new sessions only)',
241
- 'modal.session.hint': 'Applies to sessions this account creates later; existing sessions are untouched.',
242
- 'modal.session.none': 'No session backend on this host; session preferences are unavailable.',
243
- 'access.title': 'DM access',
244
- 'access.approved': 'Approved users only',
245
- 'access.all': 'Everyone',
246
- 'access.all.tag': 'open DM',
247
- 'access.hint': '"Everyone" lets anybody DM this bot; rejected users get a notice with their user ID.',
248
- 'prefs.workspace': 'Workspace directory (cwd)',
249
- 'prefs.workspace.placeholder': 'Empty = host default workspace',
250
- 'prefs.preset': 'Agent preset',
251
- 'prefs.preset.default': 'Host default',
252
- 'prefs.model': 'Model',
253
- 'prefs.model.default': 'Host default',
254
- 'prefs.effort': 'Reasoning effort',
255
- 'prefs.effort.default': 'Model default',
256
- 'catalog.loading': 'Loading catalog…',
257
- 'check.title': 'Diagnostics',
258
- 'check.passed': 'passed',
259
- 'check.failed': 'failed',
260
- 'check.unverified': 'unverified',
261
- },
262
- ja: { 'settings.title': 'IM Union', 'accounts.title': 'アカウント', 'accounts.add': '追加', 'accounts.edit': '編集', 'accounts.save': '保存', 'modal.cancel': 'キャンセル', 'accounts.check': '接続確認', 'accounts.pause': '一時停止', 'accounts.resume': '再開', 'accounts.remove': '削除', 'accounts.state.connected': '接続済', 'accounts.state.disconnected': '未接続', 'accounts.state.paused': '停止中', 'check.passed': '成功', 'check.failed': '失敗', 'check.unverified': '未検証' },
263
- ko: { 'settings.title': 'IM Union', 'accounts.title': '계정', 'accounts.add': '추가', 'accounts.edit': '편집', 'accounts.save': '저장', 'modal.cancel': '취소', 'accounts.check': '연결 확인', 'accounts.pause': '일시정지', 'accounts.resume': '재개', 'accounts.remove': '삭제', 'accounts.state.connected': '연결됨', 'accounts.state.disconnected': '미연결', 'accounts.state.paused': '정지됨', 'check.passed': '통과', 'check.failed': '실패', 'check.unverified': '미검증' },
264
- fr: { 'settings.title': 'IM Union', 'accounts.title': 'Comptes', 'accounts.add': 'Ajouter', 'accounts.edit': 'Modifier', 'accounts.save': 'Enregistrer', 'modal.cancel': 'Annuler', 'accounts.check': 'Vérifier', 'accounts.pause': 'Suspendre', 'accounts.resume': 'Reprendre', 'accounts.remove': 'Supprimer', 'accounts.state.connected': 'Connecté', 'accounts.state.disconnected': 'Déconnecté', 'accounts.state.paused': 'Suspendu', 'check.passed': 'réussi', 'check.failed': 'échoué', 'check.unverified': 'non vérifié' },
265
- de: { 'settings.title': 'IM Union', 'accounts.title': 'Konten', 'accounts.add': 'Hinzufügen', 'accounts.edit': 'Bearbeiten', 'accounts.save': 'Speichern', 'modal.cancel': 'Abbrechen', 'accounts.check': 'Verbindung prüfen', 'accounts.pause': 'Pausieren', 'accounts.resume': 'Fortsetzen', 'accounts.remove': 'Entfernen', 'accounts.state.connected': 'Verbunden', 'accounts.state.disconnected': 'Getrennt', 'accounts.state.paused': 'Pausiert', 'check.passed': 'ok', 'check.failed': 'fehlgeschlagen', 'check.unverified': 'ungeprüft' },
266
- it: { 'settings.title': 'IM Union', 'accounts.title': 'Account', 'accounts.add': 'Aggiungi', 'accounts.edit': 'Modifica', 'accounts.save': 'Salva', 'modal.cancel': 'Annulla', 'accounts.check': 'Controlla', 'accounts.pause': 'Pausa', 'accounts.resume': 'Riprendi', 'accounts.remove': 'Rimuovi', 'accounts.state.connected': 'Connesso', 'accounts.state.disconnected': 'Disconnesso', 'accounts.state.paused': 'In pausa', 'check.passed': 'ok', 'check.failed': 'fallito', 'check.unverified': 'non verificato' },
267
- ru: { 'settings.title': 'IM Union', 'accounts.title': 'Аккаунты', 'accounts.add': 'Добавить', 'accounts.edit': 'Изменить', 'accounts.save': 'Сохранить', 'modal.cancel': 'Отмена', 'accounts.check': 'Проверить', 'accounts.pause': 'Пауза', 'accounts.resume': 'Возобновить', 'accounts.remove': 'Удалить', 'accounts.state.connected': 'Подключено', 'accounts.state.disconnected': 'Отключено', 'accounts.state.paused': 'Пауза', 'check.passed': 'успех', 'check.failed': 'ошибка', 'check.unverified': 'не проверено' },
268
- es: { 'settings.title': 'IM Union', 'accounts.title': 'Cuentas', 'accounts.add': 'Añadir', 'accounts.edit': 'Editar', 'accounts.save': 'Guardar', 'modal.cancel': 'Cancelar', 'accounts.check': 'Comprobar', 'accounts.pause': 'Pausar', 'accounts.resume': 'Reanudar', 'accounts.remove': 'Eliminar', 'accounts.state.connected': 'Conectado', 'accounts.state.disconnected': 'Desconectado', 'accounts.state.paused': 'Pausado', 'check.passed': 'correcto', 'check.failed': 'fallido', 'check.unverified': 'sin verificar' },
269
- };
270
-
271
- // UI 语言:**取词时懒解析**,不在 factory 顶层冻结。真机实证:宿主初始 HTML
272
- // 是 <html lang="en">,i18n 启动后才改写为真实语言(如 zh-CN)——顶层冻结会
273
- // 把中文环境锁死在英文面。每处取词现场读 <html lang>;未知/缺空前缀保守归
274
- // zh(缺键再逐条回落 zh 词典)。
275
- var LANG_PREFIXES = ['zh', 'en', 'ja', 'ko', 'fr', 'de', 'it', 'ru', 'es'];
276
- function currentLang() {
277
- try {
278
- var docLang = String((typeof document !== 'undefined' && document.documentElement.lang) || '').toLowerCase();
279
- for (var i = 0; i < LANG_PREFIXES.length; i += 1) {
280
- if (docLang.indexOf(LANG_PREFIXES[i]) === 0) return LANG_PREFIXES[i];
281
- }
282
- } catch (noDoc) { /* SSR/老宿主 */ }
283
- return 'zh';
284
- }
285
- // 宿主词典层(apply 时捕获):宿主若提供逐键翻译则优先,miss 回本地词典。
286
- var hostDictionary = null;
287
- function t(key) {
288
- if (hostDictionary !== null && hostDictionary[key] !== undefined) return String(hostDictionary[key]);
289
- var dict = LOCALES[currentLang()] || LOCALES.zh;
290
- return (dict[key] !== undefined ? dict[key] : LOCALES.zh[key]) || key;
291
- }
292
- function pickBilingual(obj) {
293
- if (obj === null || obj === undefined) return '';
294
- if (typeof obj !== 'object') return String(obj);
295
- if (currentLang() === 'zh') return obj.zh || obj.en || '';
296
- return obj.en || obj.zh || '';
297
- }
298
- function channelName(channel) {
299
- var key = 'channel.' + channel.id;
300
- if (hostDictionary !== null && hostDictionary[key] !== undefined) return String(hostDictionary[key]);
301
- var dict = LOCALES[currentLang()] || LOCALES.zh;
302
- if (dict[key] !== undefined) return dict[key];
303
- if (LOCALES.zh[key] !== undefined) return LOCALES.zh[key];
304
- return pickBilingual(channel.label) || channel.id;
305
- }
306
- function format(key, params) {
307
- var text = t(key);
308
- for (var name in params) {
309
- if (Object.prototype.hasOwnProperty.call(params, name)) {
310
- text = text.split('{' + name + '}').join(String(params[name]));
311
- }
312
- }
313
- return text;
314
- }
315
-
316
- // ── 管理面 HTTP(写请求带客户端标识头;回包零凭据由宿主半保证)──────────
317
- function getJSON(path) {
318
- return fetch(API_BASE + path, {
319
- credentials: 'same-origin',
320
- headers: { 'x-dsh-im-union-client': '1' },
321
- }).then(function (response) {
322
- return response.json().then(function (json) {
323
- return { status: response.status, json: json };
324
- });
325
- });
326
- }
327
-
328
- function postJSON(path, body) {
329
- return fetch(API_BASE + path, {
330
- method: 'POST',
331
- credentials: 'same-origin',
332
- headers: { 'content-type': 'application/json', 'x-dsh-im-union-client': '1' },
333
- body: JSON.stringify(body || {}),
334
- }).then(function (response) {
335
- return response.json().then(function (json) {
336
- return { status: response.status, json: json };
337
- });
338
- });
339
- }
340
-
341
- function errorText(result) {
342
- return (result.json && result.json.error) || 'HTTP ' + result.status;
343
- }
344
- function messageText(error) {
345
- return String(error && error.message ? error.message : error);
346
- }
347
- function genAccountId(channelId) {
348
- return channelId + '-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
349
- }
350
-
351
- // ── 样式:单 <style data-plugin> 注入;颜色全部 var(--dsw-alias-*, fallback)
352
- // 消费链(本地 --diu 别名),暗色/浅色零分支——主题切换交给宿主令牌。──────
353
- var DIU_CSS = String.raw`
354
- .diu-page, .diu-modal, .diu-modalRoot {
355
- --diu-text: var(--dsw-alias-label-primary, #e8eaed);
356
- --diu-muted: var(--dsw-alias-label-tertiary, #9aa0a6);
357
- --diu-faint: var(--dsw-alias-label-caption, #81858c);
358
- --diu-line: var(--dsw-alias-border-l2, rgba(127,127,127,.24));
359
- --diu-line-strong: var(--dsw-alias-border-l3, rgba(127,127,127,.34));
360
- --diu-card: var(--dsw-alias-bg-layer-2, rgba(127,127,127,.06));
361
- --diu-sunken: var(--dsw-alias-bg-layer-1, rgba(127,127,127,.09));
362
- --diu-hover: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.1));
363
- --diu-accent: var(--dsw-alias-state-business-primary, #4b7cff);
364
- --diu-ok: var(--dsw-alias-state-success-primary, #22c55e);
365
- --diu-warn: var(--dsw-alias-state-warn-primary, var(--dsw-alias-state-warning-primary, #d97706));
366
- --diu-bad: var(--dsw-alias-state-error-primary, #d54941);
367
- --diu-shadow: var(--dsw-shadow-lv3, 0 0 1px rgba(0,0,0,.2), 0 12px 32px rgba(0,0,0,.18));
368
- --diu-mono: ui-monospace, SFMono-Regular, Consolas, "Courier New", monospace;
369
- }
370
- .diu-page { box-sizing: border-box; width: 100%; max-width: 980px; margin: 0 auto; padding: 4px 0 40px; color: var(--diu-text); font-size: 14px; container-type: inline-size; }
371
- .diu-page *, .diu-modalRoot * { box-sizing: border-box; }
372
- .diu-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
373
- .diu-title { margin: 0; font-size: 22px; line-height: 30px; font-weight: 650; letter-spacing: -.3px; }
374
- .diu-sub { margin: 6px 0 0; color: var(--diu-muted); font-size: 13px; line-height: 20px; }
375
- .diu-headActions { display: flex; gap: 8px; flex: none; padding-top: 2px; }
376
- .diu-iconBtn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 30px; padding: 0 10px; border: 1px solid var(--diu-line); border-radius: 8px; background: transparent; color: var(--diu-muted); font-size: 12px; font-family: inherit; cursor: pointer; transition: background .15s ease, color .15s ease, border-color .15s ease; }
377
- .diu-iconBtn:hover { background: var(--diu-hover); color: var(--diu-text); }
378
- .diu-iconBtn:focus-visible, .diu-btn:focus-visible, .diu-addBtn:focus-visible, .diu-platformHead:focus-visible { outline: 2px solid var(--diu-accent); outline-offset: 2px; }
379
- .diu-stats { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
380
- .diu-stat { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; background: color-mix(in srgb, var(--diu-text) 7%, transparent); color: var(--diu-muted); font-size: 12px; line-height: 20px; }
381
- .diu-stat strong { color: var(--diu-text); font-weight: 650; }
382
- .diu-banner { display: flex; align-items: flex-start; gap: 8px; margin-top: 12px; padding: 9px 12px; border-radius: 10px; border: 1px solid transparent; font-size: 13px; line-height: 19px; }
383
- .diu-banner-err { color: var(--diu-bad); border-color: color-mix(in srgb, var(--diu-bad) 38%, transparent); background: color-mix(in srgb, var(--diu-bad) 10%, transparent); }
384
- .diu-banner-ok { color: var(--diu-ok); border-color: color-mix(in srgb, var(--diu-ok) 35%, transparent); background: color-mix(in srgb, var(--diu-ok) 9%, transparent); }
385
- .diu-banner-info { color: var(--diu-accent); border-color: color-mix(in srgb, var(--diu-accent) 35%, transparent); background: color-mix(in srgb, var(--diu-accent) 9%, transparent); }
386
- .diu-bannerClose { margin-left: auto; flex: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 0; border-radius: 6px; background: transparent; color: inherit; opacity: .7; cursor: pointer; }
387
- .diu-bannerClose:hover { opacity: 1; background: color-mix(in srgb, currentColor 12%, transparent); }
388
- .diu-debug { margin-top: 14px; font-size: 12px; color: var(--diu-muted); }
389
- .diu-debug summary { cursor: pointer; user-select: none; list-style-position: inside; }
390
- .diu-debug summary:hover { color: var(--diu-text); }
391
- .diu-debugBody { margin-top: 8px; padding: 10px 12px; border: 1px solid var(--diu-line); border-radius: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 18px; }
392
- .diu-debugItem { display: flex; justify-content: space-between; gap: 10px; min-width: 0; }
393
- .diu-debugItem dt { color: var(--diu-faint); }
394
- .diu-debugItem dd { margin: 0; color: var(--diu-text); text-align: right; overflow-wrap: anywhere; }
395
- .diu-debugChannels { grid-column: 1 / -1; margin-top: 4px; padding-top: 8px; border-top: 1px solid var(--diu-line); display: flex; flex-wrap: wrap; gap: 6px; }
396
- .diu-platforms { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; }
397
- .diu-platform { border: 1px solid var(--diu-line); border-radius: 12px; background: var(--diu-card); overflow: hidden; transition: border-color .15s ease; }
398
- .diu-platform.open { border-color: var(--diu-line-strong); }
399
- .diu-platform.isPlanned { opacity: .62; }
400
- .diu-platformHead { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto auto; align-items: center; gap: 10px; width: 100%; min-height: 56px; padding: 8px 14px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
401
- .diu-platform:not(.open):not(.isPlanned) .diu-platformHead:hover { background: var(--diu-hover); }
402
- .diu-platformCopy { display: flex; align-items: center; gap: 8px; min-width: 0; }
403
- .diu-platformName { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
404
- .diu-count { color: var(--diu-faint); font-size: 12px; white-space: nowrap; }
405
- .diu-addBtn { display: inline-flex; align-items: center; gap: 4px; min-height: 28px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--diu-line); background: transparent; color: var(--diu-text); font-size: 12px; font-family: inherit; cursor: pointer; transition: border-color .15s ease, color .15s ease, background .15s ease; }
406
- .diu-addBtn:hover { border-color: color-mix(in srgb, var(--diu-accent) 55%, var(--diu-line)); color: var(--diu-accent); background: color-mix(in srgb, var(--diu-accent) 8%, transparent); }
407
- .diu-addBtn:disabled { opacity: .45; cursor: not-allowed; }
408
- .diu-caret { display: inline-flex; color: var(--diu-faint); transition: transform .16s ease; }
409
- .diu-platform.open .diu-caret { transform: rotate(90deg); }
410
- .diu-logo { display: grid; place-items: center; border-radius: 9px; flex: none; width: 30px; height: 30px; box-shadow: 0 1px 3px rgba(0,0,0,.12); }
411
- .diu-logo svg { display: block; }
412
- .diu-accountList { display: flex; flex-direction: column; gap: 8px; padding: 2px 12px 12px; }
413
- .diu-accountEmpty { padding: 6px 2px 4px; color: var(--diu-faint); font-size: 12px; }
414
- .diu-account { border: 1px solid var(--diu-line); border-radius: 10px; padding: 10px 12px; background: color-mix(in srgb, var(--diu-text) 3%, transparent); }
415
- .diu-accountMain { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
416
- .diu-accountName { font-weight: 600; font-size: 13px; }
417
- .diu-accountId { color: var(--diu-faint); font-size: 12px; font-family: var(--diu-mono); }
418
- .diu-badges { display: flex; gap: 6px; flex-wrap: wrap; }
419
- .diu-tag { display: inline-flex; align-items: center; gap: 5px; padding: 1px 8px; border-radius: 999px; border: 1px solid transparent; font-size: 11px; line-height: 18px; white-space: nowrap; }
420
- .diu-tag-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
421
- .diu-tagOk { color: var(--diu-ok); border-color: color-mix(in srgb, var(--diu-ok) 35%, transparent); background: color-mix(in srgb, var(--diu-ok) 9%, transparent); }
422
- .diu-tagWarn { color: var(--diu-warn); border-color: color-mix(in srgb, var(--diu-warn) 35%, transparent); background: color-mix(in srgb, var(--diu-warn) 9%, transparent); }
423
- .diu-tagBad { color: var(--diu-bad); border-color: color-mix(in srgb, var(--diu-bad) 35%, transparent); background: color-mix(in srgb, var(--diu-bad) 9%, transparent); }
424
- .diu-tagInfo { color: var(--diu-accent); border-color: color-mix(in srgb, var(--diu-accent) 35%, transparent); background: color-mix(in srgb, var(--diu-accent) 9%, transparent); }
425
- .diu-tagMuted { color: var(--diu-muted); border-color: var(--diu-line); background: transparent; }
426
- .diu-accountActions { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; }
427
- .diu-btn { display: inline-flex; align-items: center; gap: 5px; min-height: 28px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--diu-line); background: transparent; color: var(--diu-text); font-size: 12px; font-family: inherit; cursor: pointer; transition: background .15s ease, border-color .15s ease, color .15s ease; }
428
- .diu-btn:hover { background: var(--diu-hover); }
429
- .diu-btn:disabled { opacity: .5; cursor: not-allowed; }
430
- .diu-btnDanger { color: var(--diu-bad); }
431
- .diu-btnDanger:hover { background: color-mix(in srgb, var(--diu-bad) 10%, transparent); border-color: color-mix(in srgb, var(--diu-bad) 40%, var(--diu-line)); }
432
- .diu-btnPrimary { border-color: transparent; background: var(--diu-accent); color: #fff; }
433
- .diu-btnPrimary:hover { background: color-mix(in srgb, var(--diu-accent) 88%, #fff); }
434
- .diu-checkList { margin: 8px 0 0; padding: 8px 10px; border-radius: 8px; background: color-mix(in srgb, var(--diu-text) 4%, transparent); font-size: 12px; display: flex; flex-direction: column; gap: 4px; }
435
- .diu-checkItem { display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
436
- .diu-checkName { color: var(--diu-text); font-weight: 550; }
437
- .diu-checkHint { color: var(--diu-faint); }
438
- .diu-allow { margin-top: 8px; }
439
- .diu-allow > summary { cursor: pointer; user-select: none; font-size: 12px; color: var(--diu-muted); list-style-position: inside; }
440
- .diu-allow > summary:hover { color: var(--diu-text); }
441
- .diu-allowBody { margin-top: 6px; display: flex; flex-direction: column; gap: 6px; }
442
- .diu-hint { font-size: 12px; color: var(--diu-faint); line-height: 18px; }
443
- .diu-textarea { width: 100%; min-height: 56px; padding: 7px 9px; border-radius: 8px; border: 1px solid var(--diu-line); background: var(--diu-sunken); color: inherit; font: 12px/18px var(--diu-mono); resize: vertical; }
444
- .diu-textarea:focus, .diu-input:focus { outline: none; border-color: color-mix(in srgb, var(--diu-accent) 55%, var(--diu-line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--diu-accent) 16%, transparent); }
445
- .diu-modalRoot { position: fixed; inset: 0; z-index: 5000; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(0,0,0,.45); }
446
- .diu-modal { width: 580px; max-width: 100%; max-height: min(86vh, 780px); display: flex; flex-direction: column; border-radius: 14px; border: 1px solid var(--diu-line-strong); background: var(--dsw-alias-bg-layer-1, #232324); box-shadow: var(--diu-shadow); color: var(--diu-text); }
447
- .diu-modalHead { display: flex; align-items: center; gap: 10px; padding: 14px 16px 10px; }
448
- .diu-modalTitle { margin: 0; font-size: 16px; font-weight: 650; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
449
- .diu-closeBtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 8px; background: transparent; color: var(--diu-muted); cursor: pointer; }
450
- .diu-closeBtn:hover { background: var(--diu-hover); color: var(--diu-text); }
451
- .diu-modalBody { padding: 2px 16px 8px; overflow: auto; }
452
- .diu-qrBox { display: flex; gap: 14px; align-items: flex-start; padding: 12px; border: 1px solid var(--diu-line); border-radius: 10px; background: color-mix(in srgb, var(--diu-text) 3%, transparent); margin-bottom: 14px; }
453
- .diu-qrImg { width: 168px; height: 168px; flex: none; border-radius: 10px; background: #fff; padding: 8px; border: 1px solid var(--diu-line); }
454
- .diu-qrMeta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
455
- .diu-qrTitle { font-weight: 600; font-size: 13px; }
456
- .diu-qrStatus { font-size: 12px; color: var(--diu-muted); line-height: 18px; }
457
- .diu-qrActions { display: flex; gap: 6px; flex-wrap: wrap; }
458
- .diu-verifyRow { display: flex; gap: 6px; align-items: center; }
459
- .diu-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px; }
460
- .diu-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
461
- .diu-fieldWide { grid-column: 1 / -1; }
462
- .diu-label { display: flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 550; color: var(--diu-muted); }
463
- .diu-req { color: var(--diu-bad); }
464
- .diu-helpLink { margin-left: auto; font-size: 11px; font-weight: 400; color: var(--diu-accent); text-decoration: none; }
465
- .diu-helpLink:hover { text-decoration: underline; }
466
- .diu-input { height: 34px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--diu-line); background: var(--diu-sunken); color: inherit; font-size: 13px; font-family: inherit; min-width: 0; width: 100%; }
467
- .diu-input::placeholder, .diu-textarea::placeholder { color: var(--diu-faint); }
468
- .diu-modalNote { margin: 10px 0 0; font-size: 12px; color: var(--diu-faint); line-height: 18px; }
469
- .diu-sessionBlock { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--diu-line); }
470
- .diu-sessionTitle { font-weight: 600; font-size: 13px; margin-bottom: 10px; }
471
- select.diu-input { appearance: auto; }
472
- .diu-modalFoot { display: flex; align-items: center; gap: 8px; padding: 12px 16px 14px; border-top: 1px solid var(--diu-line); margin-top: 8px; }
473
- .diu-footMsg { flex: 1; min-width: 0; font-size: 12px; color: var(--diu-bad); line-height: 18px; overflow-wrap: anywhere; }
474
- .diu-footSpacer { flex: 1; }
475
- .diu-overlayFallback { position: fixed; right: 16px; bottom: 16px; z-index: 9999; width: 380px; max-height: 62vh; overflow: auto; border: 1px solid var(--diu-line); border-radius: 12px; background: var(--dsw-alias-bg-layer-1, #232324); box-shadow: var(--diu-shadow); padding: 12px; }
476
- @container (max-width: 640px) {
477
- .diu-fields { grid-template-columns: 1fr; }
478
- .diu-platformHead { grid-template-columns: auto minmax(0, 1fr) auto; }
479
- .diu-platformHead .diu-count, .diu-platformHead .diu-caret { display: none; }
480
- .diu-debugBody { grid-template-columns: 1fr; }
481
- .diu-qrBox { flex-direction: column; }
482
- }
483
- @media (prefers-reduced-motion: reduce) {
484
- .diu-caret, .diu-btn, .diu-addBtn, .diu-iconBtn { transition: none; }
485
- }
486
- `;
487
- var styleInstalled = false;
488
- function installStyles() {
489
- if (styleInstalled || typeof document === 'undefined') return;
490
- if (document.querySelector('style[data-plugin-css="' + STYLE_ID + '"]')) { styleInstalled = true; return; }
491
- var style = document.createElement('style');
492
- style.dataset.plugin = 'dsh-im-union';
493
- style.dataset.pluginCss = STYLE_ID;
494
- style.textContent = DIU_CSS;
495
- document.head.appendChild(style);
496
- styleInstalled = true;
497
- }
498
-
499
- // ── 品牌图标:净室简化图形(色块 chip + 单色/多色 glyph),非官方素材─────
500
- var CHIP_STYLES = {
501
- weixin: { background: '#07C160' },
502
- feishu: { background: '#ffffff', bordered: true },
503
- dingtalk: { background: '#1296DB' },
504
- wecom: { background: '#ffffff', bordered: true },
505
- telegram: { background: '#229ED9' },
506
- qq: { background: '#12B7F5' },
507
- slack: { background: '#ffffff', bordered: true },
508
- discord: { background: '#5865F2' },
509
- whatsapp: { background: '#25D366' },
510
- matrix: { background: '#0F1115' },
511
- imessage: { background: 'linear-gradient(180deg,#5BF675 0%,#28D944 55%,#0FBD2C 100%)' },
512
- 'wecom-app': { background: '#2B6DEB' },
513
- email: { background: '#5B7CFA' },
514
- 'dingtalk-notify': { background: '#1296DB' },
515
- 'qq-notify': { background: '#12B7F5' },
516
- };
517
-
518
- var GLYPHS = {
519
- weixin: function () {
520
- return [
521
- h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M10.4 3.2c-4.7 0-8.4 3-8.4 6.8 0 2.1 1.1 3.9 2.9 5.2l-.8 3.4 3.5-1.9c.9.3 1.8.4 2.8.4h.5a6.7 6.7 0 0 1-.3-2c0-3.7 3.4-6.7 7.7-6.7h.4c-.8-3-4.2-5.2-8.3-5.2zM7.7 8.2a1.05 1.05 0 1 1 0 2.1 1.05 1.05 0 0 1 0-2.1zm5.4 0a1.05 1.05 0 1 1 0 2.1 1.05 1.05 0 0 1 0-2.1z' }),
522
- h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M22 15.4c0-3-3-5.4-6.6-5.4s-6.6 2.4-6.6 5.4 3 5.4 6.6 5.4c.8 0 1.5-.1 2.2-.3l2.8 1.5-.7-2.6c1.4-1 2.3-2.4 2.3-4zm-8.9-1.4a.95.95 0 1 1 0 1.9.95.95 0 0 1 0-1.9zm4.6 0a.95.95 0 1 1 0 1.9.95.95 0 0 1 0-1.9z' }),
523
- ];
524
- },
525
- feishu: function () {
526
- return [
527
- h('path', { fill: '#00D6B9', d: 'M13.9 3.6 4.2 12.4c-.5.4-.2 1.2.4 1.2l6.1.2 4.9-8.7c.3-.5-.1-1.1-.6-1.1h-.5c-.2 0-.4.1-.6.2z' }),
528
- h('path', { fill: '#3370FF', d: 'M10.1 20.4l9.7-8.8c.5-.4.2-1.2-.4-1.2l-6.1-.2-4.9 8.7c-.3.5.1 1.1.6 1.1h.5c.2 0 .4-.1.6-.2z' }),
529
- h('circle', { cx: '16.6', cy: '16.4', r: '2.5', fill: '#00D6B9' }),
530
- ];
531
- },
532
- dingtalk: function () {
533
- return [
534
- h('path', { fill: '#fff', d: 'M20.8 4.3 3.6 11c-.7.3-.6 1.3.1 1.5l4.4 1.1 1.6 5c.2.6 1 .8 1.4.3l2.2-2.7 4.3 2.5c.5.3 1.1 0 1.3-.5l3-13c.1-.7-.5-1.2-1.1-.9z' }),
535
- ];
536
- },
537
- wecom: function () {
538
- return [
539
- h('circle', { cx: '12', cy: '5.8', r: '2.6', fill: '#FB5252' }),
540
- h('circle', { cx: '18.2', cy: '12', r: '2.6', fill: '#FFB43A' }),
541
- h('circle', { cx: '12', cy: '18.2', r: '2.6', fill: '#34C759' }),
542
- h('circle', { cx: '5.8', cy: '12', r: '2.6', fill: '#2B7FFF' }),
543
- ];
544
- },
545
- telegram: function () {
546
- return [
547
- h('path', { fill: '#fff', d: 'M20.7 4.2 3.5 10.9c-.8.3-.8 1.4 0 1.7l4.3 1.4 1.6 5c.2.7 1.1.9 1.6.3l2.2-2.5 4.2 2.4c.5.3 1.2 0 1.3-.6l2.8-13.2c.2-.8-.6-1.5-1.3-1.2z' }),
548
- h('path', { fill: '#229ED9', d: 'M8.6 13.6l8.9-5.6c.3-.2.6.2.3.4l-7.2 6.7-.2 2.6-1.8-4.1z' }),
549
- ];
550
- },
551
- qq: function () {
552
- return [
553
- h('path', { fill: '#fff', d: 'M12 2.6c-3.4 0-5.6 2.5-5.6 5.8 0 .9-.5 2-1 3.1-.6 1.4-1.3 3-1.3 4.5 0 1.5.8 2.5 1.8 2.5.6 0 1.1-.4 1.5-1 .3 1.4 1 2.8 2 3.8-1 .2-2 .8-2 1.5 0 .9 1.7 1.4 3.6 1.4h2c1.9 0 3.6-.5 3.6-1.4 0-.7-1-1.3-2-1.5 1-1 1.7-2.4 2-3.8.4.6.9 1 1.5 1 1 0 1.8-1 1.8-2.5 0-1.5-.7-3.1-1.3-4.5-.5-1.1-1-2.2-1-3.1 0-3.3-2.2-5.8-5.6-5.8z' }),
554
- h('ellipse', { cx: '12', cy: '14.8', rx: '3.6', ry: '4.8', fill: '#12B7F5' }),
555
- h('circle', { cx: '10.1', cy: '7.8', r: '1', fill: '#12B7F5' }),
556
- h('circle', { cx: '13.9', cy: '7.8', r: '1', fill: '#12B7F5' }),
557
- ];
558
- },
559
- slack: function () {
560
- return [
561
- h('rect', { x: '9.3', y: '3', width: '2.8', height: '8.4', rx: '1.4', fill: '#36C5F0' }),
562
- h('rect', { x: '12.7', y: '9.3', width: '8.4', height: '2.8', rx: '1.4', fill: '#2EB67D' }),
563
- h('rect', { x: '11.9', y: '12.7', width: '2.8', height: '8.4', rx: '1.4', fill: '#ECB22E' }),
564
- h('rect', { x: '2.9', y: '11.9', width: '8.4', height: '2.8', rx: '1.4', fill: '#E01E5A' }),
565
- ];
566
- },
567
- discord: function () {
568
- return [
569
- h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M19.3 5.6A16.5 16.5 0 0 0 15.6 4l-.5 1a13.4 13.4 0 0 0-6.2 0L8.4 4a16.5 16.5 0 0 0-3.7 1.6C2.3 9.3 1.6 12.9 2 16.4A16.9 16.9 0 0 0 7 19l1-1.6c-.6-.2-1.1-.5-1.6-.8l.4-.3a12.3 12.3 0 0 0 10.4 0l.4.3c-.5.3-1 .6-1.6.8l1 1.6a16.9 16.9 0 0 0 5-2.6c.4-4.1-.7-7.7-2.7-10.8zM9.1 14.2c-1 0-1.8-.9-1.8-2s.8-2 1.8-2 1.8.9 1.8 2-.8 2-1.8 2zm5.8 0c-1 0-1.8-.9-1.8-2s.8-2 1.8-2 1.8.9 1.8 2-.8 2-1.8 2z' }),
570
- ];
571
- },
572
- whatsapp: function () {
573
- return [
574
- h('path', { fill: '#fff', d: 'M12 2.8a9.2 9.2 0 0 0-7.9 13.9L2.8 21l4.5-1.2A9.2 9.2 0 1 0 12 2.8z' }),
575
- h('path', { fill: '#25D366', d: 'M9 8.1c.3-.3.8-.3 1.1 0l1.2 1.6c.2.3.2.7 0 1l-.8 1a8.6 8.6 0 0 0 3.8 3.8l1-.8c.3-.2.7-.2 1 0l1.6 1.2c.3.3.3.8 0 1.1-1.7 1.7-4.5 1-7.2-1.7S7.3 9.8 9 8.1z' }),
576
- ];
577
- },
578
- matrix: function () {
579
- return [
580
- h('path', { d: 'M8.6 4.5H5.4v15h3.2', stroke: '#fff', strokeWidth: 2, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' }),
581
- h('path', { d: 'M15.4 4.5h3.2v15h-3.2', stroke: '#fff', strokeWidth: 2, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' }),
582
- ];
583
- },
584
- imessage: function () {
585
- return [
586
- h('path', { fill: '#fff', d: 'M12 3.6c-4.8 0-8.6 3-8.6 6.7 0 2.1 1.2 3.9 3.1 5.2-.1 1.1-.6 2.4-1.7 3.4 2-.1 3.6-.9 4.7-1.8.8.2 1.6.3 2.5.3 4.8 0 8.6-3 8.6-6.7S16.8 3.6 12 3.6z' }),
587
- ];
588
- },
589
- 'wecom-app': function () {
590
- return [
591
- h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M9.4 6.6V6c0-.7.6-1.3 1.3-1.3h2.6c.7 0 1.3.6 1.3 1.3v.6h2.8c1 0 1.9.9 1.9 1.9v7.6c0 1-.9 1.9-1.9 1.9H6.6c-1 0-1.9-.9-1.9-1.9V8.5c0-1 .9-1.9 1.9-1.9h2.8zm1.4 0h2.4V6c0-.1-.1-.2-.2-.2h-2c-.1 0-.2.1-.2.2v.6zm-3.6 4.2h9.6v1.5H7.2v-1.5z' }),
592
- ];
593
- },
594
- email: function () {
595
- return [
596
- h('path', { fill: '#fff', d: 'M5 6.5h14a1.8 1.8 0 0 1 1.8 1.8v7.4a1.8 1.8 0 0 1-1.8 1.8H5a1.8 1.8 0 0 1-1.8-1.8V8.3A1.8 1.8 0 0 1 5 6.5z' }),
597
- h('path', { d: 'm4.6 8.4 7.4 5 7.4-5', stroke: '#5B7CFA', strokeWidth: 1.6, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' }),
598
- ];
599
- },
600
- 'dingtalk-notify': function () {
601
- return [
602
- h('path', { fill: '#fff', d: 'M12 3.2a5.4 5.4 0 0 0-5.4 5.4c0 3.1-.9 4.4-1.7 5.3-.4.4-.1 1.2.5 1.2h13.2c.6 0 .9-.8.5-1.2-.8-.9-1.7-2.2-1.7-5.3A5.4 5.4 0 0 0 12 3.2zm-2.2 13.6a2.2 2.2 0 0 0 4.4 0h-4.4z' }),
603
- ];
604
- },
605
- };
606
- GLYPHS['qq-notify'] = GLYPHS['dingtalk-notify'];
607
-
608
- function Logo(props) {
609
- var channelId = props.channel;
610
- var size = props.size || 30;
611
- var chip = CHIP_STYLES[channelId] || { background: 'var(--diu-hover)' };
612
- var style = { width: size, height: size, borderRadius: Math.round(size * 0.3), background: chip.background };
613
- if (chip.bordered) style.boxShadow = 'inset 0 0 0 1px var(--diu-line)';
614
- var draw = GLYPHS[channelId];
615
- var glyphSize = Math.round(size * 0.62);
616
- return h('span', { className: 'diu-logo', style: style, 'aria-hidden': 'true' },
617
- draw
618
- ? h('svg', { viewBox: '0 0 24 24', width: glyphSize, height: glyphSize, focusable: 'false' }, draw())
619
- : h('span', { style: { color: '#fff', fontSize: Math.round(size * 0.42), fontWeight: 700 } }, channelId.slice(0, 1).toUpperCase())
620
- );
621
- }
622
-
623
- // ── 小图标(stroke 类,currentColor)───────────────────────────────────
624
- function IconRefresh(props) {
625
- return h('svg', { viewBox: '0 0 24 24', width: props.size || 14, height: props.size || 14, 'aria-hidden': 'true', focusable: 'false' },
626
- h('path', { d: 'M20 12a8 8 0 1 1-2.9-6.2', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
627
- h('path', { d: 'M20.5 3.5v5h-5', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }),
628
- );
629
- }
630
- function IconChevron(props) {
631
- return h('svg', { viewBox: '0 0 24 24', width: props.size || 14, height: props.size || 14, 'aria-hidden': 'true', focusable: 'false' },
632
- h('path', { d: 'm9 6 6 6-6 6', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }),
633
- );
634
- }
635
- function IconPlus(props) {
636
- return h('svg', { viewBox: '0 0 24 24', width: props.size || 12, height: props.size || 12, 'aria-hidden': 'true', focusable: 'false' },
637
- h('path', { d: 'M12 5v14M5 12h14', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
638
- );
639
- }
640
- function IconClose(props) {
641
- return h('svg', { viewBox: '0 0 24 24', width: props.size || 14, height: props.size || 14, 'aria-hidden': 'true', focusable: 'false' },
642
- h('path', { d: 'M6 6l12 12M18 6 6 18', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
643
- );
644
- }
645
- function IconGitHub(props) {
646
- return h('svg', { viewBox: '0 0 24 24', width: props.size || 14, height: props.size || 14, 'aria-hidden': 'true', focusable: 'false' },
647
- h('path', { fill: 'currentColor', d: 'M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.87-.01-1.7-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.5-1.11-1.5-.91-.64.07-.63.07-.63 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.36 1.12 2.94.86.09-.67.35-1.12.63-1.38-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.7 0 0 .84-.28 2.75 1.05a9.36 9.36 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.4.2 2.44.1 2.7.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.8-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.6.69.49A10.02 10.02 0 0 0 22 12.25C22 6.58 17.52 2 12 2z' }),
648
- );
649
- }
650
-
651
- function Tag(props) {
652
- var cls = 'diu-tag diu-tag' + (props.tone || 'Muted');
653
- return h('span', { className: cls },
654
- props.dot !== false ? h('span', { className: 'diu-tag-dot' }) : null,
655
- props.children,
656
- );
657
- }
658
-
659
- // 弹窗 portal:宿主提供 react-dom 则挂 body(脱离设置对话框的层叠上下文),
660
- // 否则退化为面板内 fixed 层——两条路都保 Esc/背板点击可关。
661
- function Portal(props) {
662
- if (ReactDOM && typeof ReactDOM.createPortal === 'function' && typeof document !== 'undefined') {
663
- return ReactDOM.createPortal(props.children, document.body);
664
- }
665
- return h('div', { style: { position: 'fixed', inset: 0, zIndex: 5000 } }, props.children);
666
- }
667
-
668
- // ── 账号行:名称/内部键 + 状态徽章 + 操作(检查/编辑/暂停/两步删除)──────
669
- function AccountRow(props) {
670
- var row = props.row;
671
- var channel = props.channel;
672
- var busy = props.busy;
673
- var checks = props.checks || [];
674
- var checkTone = { passed: 'Ok', failed: 'Bad', unverified: 'Muted' };
675
-
676
- var pendingRemoveHook = useState(false);
677
- var pendingRemove = pendingRemoveHook[0];
678
- var setPendingRemove = pendingRemoveHook[1];
679
- var allowHook = useState(null);
680
- var allowDraft = allowHook[0];
681
- var setAllowDraft = allowHook[1];
682
-
683
- var missingSecrets = [];
684
- (row.secretFields || []).forEach(function (sf) {
685
- if (!sf.configured) missingSecrets.push(sf.key);
686
- });
687
- var badge = row.paused
688
- ? h(Tag, { tone: 'Muted' }, t('accounts.state.paused'))
689
- : row.connected
690
- ? h(Tag, { tone: 'Ok' }, t('accounts.state.connected'))
691
- : h(Tag, { tone: 'Warn' }, t('accounts.state.disconnected'));
692
- var allowValue = allowDraft !== null ? allowDraft : (row.dmAllowlist || []).join('\n');
693
-
694
- var actions = [
695
- h('button', { key: 'check', className: 'diu-btn', disabled: busy, onClick: function () { props.onCheck(row.accountId); } },
696
- busy ? t('accounts.checking') : t('accounts.check')),
697
- h('button', { key: 'edit', className: 'diu-btn', disabled: busy, onClick: function () { props.onEdit(row); } }, t('accounts.edit')),
698
- h('button', {
699
- key: 'pause', className: 'diu-btn', disabled: busy,
700
- onClick: function () { props.onPause(row.accountId, !row.paused); },
701
- }, row.paused ? t('accounts.resume') : t('accounts.pause')),
702
- ];
703
- if (pendingRemove) {
704
- actions.push(
705
- h('button', {
706
- key: 'removeYes', className: 'diu-btn diu-btnDanger', disabled: busy,
707
- onClick: function () { setPendingRemove(false); props.onRemove(row.accountId); },
708
- }, t('accounts.remove.confirm')),
709
- h('button', { key: 'removeNo', className: 'diu-btn', disabled: busy, onClick: function () { setPendingRemove(false); } }, t('modal.cancel')),
710
- );
711
- } else {
712
- actions.push(
713
- h('button', { key: 'remove', className: 'diu-btn diu-btnDanger', disabled: busy, onClick: function () { setPendingRemove(true); } }, t('accounts.remove')),
714
- );
715
- }
716
-
717
- var checkRows = null;
718
- if (checks.length > 0) {
719
- checkRows = h('div', { className: 'diu-checkList' },
720
- h('span', { className: 'diu-hint' }, t('check.title')),
721
- checks.map(function (c, i) {
722
- return h('div', { key: (c.name || 'check') + i, className: 'diu-checkItem' },
723
- h('span', { className: 'diu-checkName' }, c.name || '-'),
724
- h(Tag, { tone: checkTone[c.status] || 'Muted' }, t('check.' + c.status) || c.status),
725
- c.hint ? h('span', { className: 'diu-checkHint' }, c.hint) : null,
726
- );
727
- }),
728
- );
729
- }
730
-
731
- var allowBlock = channel.notifyOnly === true ? null : h(
732
- 'details',
733
- { className: 'diu-allow' },
734
- h('summary', null, t('accounts.allowlist') + '(' + (row.dmAllowlist || []).length + ')'),
735
- h('div', { className: 'diu-allowBody' },
736
- h('div', { className: 'diu-hint' }, t('accounts.allowlist.hint')),
737
- h('textarea', {
738
- className: 'diu-textarea', rows: 2, value: allowValue,
739
- onChange: function (event) { setAllowDraft(event.target.value); },
740
- }),
741
- h('div', null,
742
- h('button', {
743
- className: 'diu-btn', disabled: busy,
744
- onClick: function () {
745
- var ids = String(allowValue || '').split('\n').map(function (v) { return v.trim(); }).filter(function (v) { return v !== ''; });
746
- props.onSaveAllowlist(row.accountId, ids);
747
- },
748
- }, t('accounts.allowlist.save')),
749
- ),
750
- ),
751
- );
752
-
753
- return h('div', { className: 'diu-account' },
754
- h('div', { className: 'diu-accountMain' },
755
- h('span', { className: 'diu-accountName' }, row.alias || row.accountId),
756
- row.alias ? h('span', { className: 'diu-accountId' }, row.accountId) : null,
757
- h('span', { className: 'diu-badges' },
758
- badge,
759
- row.privateAccess === 'all' ? h(Tag, { tone: 'Info', dot: false }, t('access.all.tag')) : null,
760
- missingSecrets.length > 0
761
- ? h(Tag, { tone: 'Bad', dot: false }, format('accounts.secret.missing', { keys: missingSecrets.join(', ') }))
762
- : null,
763
- ),
764
- h('span', { className: 'diu-accountActions' }, actions),
765
- ),
766
- checkRows,
767
- allowBlock,
768
- );
769
- }
770
-
771
- // ── 渠道手风琴卡:头(logo/名称/标签/账号数/添加)+ 展开账号列表 ─────────
772
- function PlatformCard(props) {
773
- var channel = props.channel;
774
- var accounts = props.accounts || [];
775
- var open = props.open === true;
776
- var planned = channel.implemented !== true;
777
- var cls = 'diu-platform' + (open ? ' open' : '') + (planned ? ' isPlanned' : '');
778
- var tags = [];
779
- if (planned) tags.push(h(Tag, { key: 'planned', tone: 'Muted', dot: false }, t('channel.tag.planned')));
780
- if (channel.notifyOnly === true) tags.push(h(Tag, { key: 'notify', tone: 'Info', dot: false }, t('channel.tag.notify')));
781
- if (channel.hostRequirement === 'macos') tags.push(h(Tag, { key: 'macos', tone: 'Warn', dot: false }, t('channel.tag.macos')));
782
-
783
- return h('section', { className: cls },
784
- h('button', {
785
- className: 'diu-platformHead', 'aria-expanded': open ? 'true' : 'false',
786
- onClick: function () { props.onToggle(channel.id); },
787
- },
788
- h(Logo, { channel: channel.id, size: 30 }),
789
- h('span', { className: 'diu-platformCopy' },
790
- h('span', { className: 'diu-platformName' }, channelName(channel)),
791
- tags,
792
- ),
793
- h('span', { className: 'diu-count' }, format('channel.count', { n: accounts.length })),
794
- h('span', {
795
- className: 'diu-addBtn', role: 'button', 'aria-disabled': planned ? 'true' : 'false',
796
- onClick: function (event) {
797
- event.stopPropagation();
798
- if (!planned) props.onAdd(channel);
799
- },
800
- },
801
- h(IconPlus, null), t('accounts.add'),
802
- ),
803
- h('span', { className: 'diu-caret' }, h(IconChevron, null)),
804
- ),
805
- open && !planned
806
- ? accounts.length === 0
807
- ? h('div', { className: 'diu-accountList' }, h('div', { className: 'diu-accountEmpty' }, t('channel.emptyHint')))
808
- : h('div', { className: 'diu-accountList' }, accounts.map(function (row) {
809
- return h(AccountRow, {
810
- key: row.accountId, row: row, channel: channel, busy: props.busy,
811
- checks: props.checks[row.accountId] || [],
812
- onCheck: props.onCheck, onEdit: props.onEdit, onPause: props.onPause,
813
- onRemove: props.onRemove, onSaveAllowlist: props.onSaveAllowlist,
814
- });
815
- }))
816
- : null,
817
- );
818
- }
819
-
820
- // ── 添加/编辑弹窗:微信=扫码流(先建档后取码);其余=meta.fields 双列表单 ──
821
- function AccountModal(props) {
822
- var channel = props.channel;
823
- var account = props.account || null;
824
- var editing = account !== null;
825
- var isWeixin = channel.id === 'weixin';
826
- var fields = channel.fields || [];
827
-
828
- var aliasHook = useState(editing ? account.alias || '' : '');
829
- var alias = aliasHook[0];
830
- var setAlias = aliasHook[1];
831
- var valuesHook = useState(function () {
832
- var initial = {};
833
- for (var i = 0; i < fields.length; i += 1) {
834
- var f = fields[i];
835
- if (!f.secret && editing && account.config && typeof account.config[f.key] === 'string') {
836
- initial[f.key] = account.config[f.key];
837
- } else {
838
- initial[f.key] = '';
839
- }
840
- }
841
- return initial;
842
- });
843
- var values = valuesHook[0];
844
- var setValues = valuesHook[1];
845
- var qrHook = useState(null);
846
- var qr = qrHook[0];
847
- var setQr = qrHook[1];
848
- var busyHook = useState(false);
849
- var busy = busyHook[0];
850
- var setBusy = busyHook[1];
851
- var errHook = useState('');
852
- var err = errHook[0];
853
- var setErr = errHook[1];
854
- // 会话偏好(cwd/预设/模型/推理档)+ 私聊准入;模型编码 provider::model。
855
- var prefsHook = useState(function () {
856
- var s = (editing && account.session) || {};
857
- return {
858
- cwd: typeof s.cwd === 'string' ? s.cwd : '',
859
- agentPreset: typeof s.agentPreset === 'string' ? s.agentPreset : '',
860
- modelKey: typeof s.model === 'string' && s.model !== '' ? (typeof s.provider === 'string' ? s.provider : '') + '::' + s.model : '',
861
- reasoningEffort: typeof s.reasoningEffort === 'string' ? s.reasoningEffort : '',
862
- };
863
- });
864
- var prefs = prefsHook[0];
865
- var setPrefs = prefsHook[1];
866
- var accessHook = useState(editing && account.privateAccess === 'all' ? 'all' : 'approved');
867
- var privateAccess = accessHook[0];
868
- var setPrivateAccess = accessHook[1];
869
- var catalogHook = useState(null);
870
- var catalog = catalogHook[0];
871
- var setCatalog = catalogHook[1];
872
-
873
- // Esc 关闭(capture 注册避免被宿主对话框吞掉)。
874
- useEffect(function () {
875
- var handler = function (event) {
876
- if (event.key === 'Escape') { event.stopPropagation(); props.onClose(); }
877
- };
878
- document.addEventListener('keydown', handler, true);
879
- return function () { document.removeEventListener('keydown', handler, true); };
880
- }, [props.onClose]);
881
-
882
- // 偏好目录一次拉取(工作区 cwd 聚合/模型目录/预设清单);缺位=偏好区降级提示。
883
- useEffect(function () {
884
- var alive = true;
885
- getJSON('/session-catalog').then(function (result) {
886
- if (alive && result.status === 200 && result.json && result.json.ok) setCatalog(result.json);
887
- }).catch(function () { /* 目录不可用:模型/预设下拉保持空 */ });
888
- return function () { alive = false; };
889
- }, []);
890
-
891
- // 字段值归位:secret → secrets(留空=保持),plain → record.config;
892
- // 会话偏好进 record.session,准入档位进 record.privateAccess。
893
- function collect() {
894
- var config = {};
895
- var secrets = {};
896
- for (var i = 0; i < fields.length; i += 1) {
897
- var f = fields[i];
898
- var v = String(values[f.key] || '').trim();
899
- if (v === '') continue;
900
- if (f.secret) secrets[f.key] = v;
901
- else config[f.key] = v;
902
- }
903
- var session = {};
904
- var cwd = String(prefs.cwd || '').trim();
905
- var presetId = String(prefs.agentPreset || '').trim();
906
- if (cwd !== '') session.cwd = cwd;
907
- if (presetId !== '') session.agentPreset = presetId;
908
- if (prefs.modelKey !== '') {
909
- var split = prefs.modelKey.indexOf('::');
910
- var provider = split > 0 ? prefs.modelKey.slice(0, split) : '';
911
- var model = prefs.modelKey.slice(split + 2);
912
- if (provider !== '') session.provider = provider;
913
- if (model !== '') session.model = model;
914
- var effort = String(prefs.reasoningEffort || '').trim();
915
- if (effort !== '') session.reasoningEffort = effort;
916
- }
917
- return {
918
- record: {
919
- accountId: editing ? account.accountId : genAccountId(channel.id),
920
- channel: channel.id,
921
- alias: String(alias || '').trim(),
922
- config: config,
923
- privateAccess: privateAccess === 'all' ? 'all' : 'approved',
924
- session: session,
925
- },
926
- secrets: secrets,
927
- };
928
- }
929
-
930
- function requiredMissing() {
931
- for (var i = 0; i < fields.length; i += 1) {
932
- var f = fields[i];
933
- if (!f.required) continue;
934
- if (editing && f.secret) continue; // 编辑时空=保持现有凭据
935
- if (String(values[f.key] || '').trim() === '') return true;
936
- }
937
- return false;
938
- }
939
-
940
- function submit() {
941
- var payload = collect();
942
- // 微信扫码账号:表单存而连——凭据由扫码流程写入,保存时必须跳过
943
- // 适配器构建(否则 botToken 缺失工厂即抛 → 被当校验错 400,UI 假报)。
944
- payload.skipConnect = isWeixin && !payload.secrets.botToken;
945
- setBusy(true);
946
- setErr('');
947
- postJSON('/accounts/save', payload).then(function (result) {
948
- setBusy(false);
949
- if (result.status === 200) {
950
- props.onSaved(payload.skipConnect ? 'accounts.saved.bindhint' : 'accounts.saved');
951
- } else {
952
- setErr(errorText(result));
953
- }
954
- }).catch(function (error) {
955
- setBusy(false);
956
- setErr(messageText(error));
957
- });
958
- }
959
-
960
- // 扫码前先确保记录在库(新增=先建档 skipConnect;编辑=已在库):
961
- // qr/status confirmed 时按 accountId upsert,命中已有记录即保留别名/配置。
962
- function startQr() {
963
- setBusy(true);
964
- setErr('');
965
- var accountId = editing ? account.accountId : genAccountId(channel.id);
966
- var ensureSaved;
967
- if (editing) {
968
- ensureSaved = Promise.resolve();
969
- } else {
970
- var payload = collect();
971
- payload.record.accountId = accountId;
972
- payload.skipConnect = true;
973
- ensureSaved = postJSON('/accounts/save', payload).then(function (result) {
974
- if (result.status !== 200) throw new Error(errorText(result));
975
- });
976
- }
977
- ensureSaved.then(function () {
978
- return postJSON('/weixin/qr/start', {}).then(function (result) {
979
- setBusy(false);
980
- if (result.status === 200 && result.json && result.json.ok) {
981
- setQr({ qrcode: result.json.qrcode, qrDataUrl: result.json.qrDataUrl, accountId: accountId, status: 'wait' });
982
- } else {
983
- setErr(errorText(result));
984
- }
985
- });
986
- }).catch(function (error) {
987
- setBusy(false);
988
- setErr(messageText(error));
989
- });
990
- }
991
-
992
- // 状态驱动串行轮询:每次 setQr 产生新对象触发 effect 重跑下一拍;
993
- // 服务端每请求 hold 最长 ~35s,前端不并发叠轮(cleanup 置活旗防串台)。
994
- useEffect(function () {
995
- if (qr === null) return undefined;
996
- if (qr.status === 'confirmed' || qr.status === 'expired' || qr.status === 'need_verifycode' || qr.status === 'verify_code_blocked') return undefined;
997
- var alive = true;
998
- var payload = { qrcode: qr.qrcode, accountId: qr.accountId };
999
- if (typeof qr.verifyCode === 'string' && qr.verifyCode !== '') payload.verifyCode = qr.verifyCode;
1000
- postJSON('/weixin/qr/status', payload).then(function (result) {
1001
- if (!alive) return;
1002
- if (result.status === 200 && result.json && result.json.ok) {
1003
- var status = result.json.status;
1004
- if (status === 'confirmed') {
1005
- setQr(Object.assign({}, qr, { status: 'confirmed' }));
1006
- props.onSaved('accounts.qr.confirmed');
1007
- } else {
1008
- setQr(Object.assign({}, qr, { status: status }));
1009
- }
1010
- } else if (alive) {
1011
- setQr(Object.assign({}, qr, { status: 'expired' })); // 请求异常保守收轮:引导重新取码而非假进度
1012
- }
1013
- }).catch(function () {
1014
- if (alive) setQr(Object.assign({}, qr, { status: 'expired' }));
1015
- });
1016
- return function () { alive = false; };
1017
- }, [qr]);
1018
-
1019
- var qrBlock = null;
1020
- if (isWeixin) {
1021
- if (qr === null || qr.status === 'expired') {
1022
- qrBlock = h('div', { className: 'diu-qrBox' },
1023
- h('div', { className: 'diu-qrMeta' },
1024
- h('div', { className: 'diu-qrTitle' }, t('accounts.qr.get')),
1025
- h('div', { className: 'diu-qrStatus' }, t('accounts.qr.desc')),
1026
- h('div', { className: 'diu-qrActions' },
1027
- h('button', { className: 'diu-btn diu-btnPrimary', disabled: busy, onClick: startQr },
1028
- t(qr === null ? 'accounts.qr.get' : 'accounts.qr.refresh')),
1029
- ),
1030
- ),
1031
- );
1032
- } else if (qr.status === 'confirmed') {
1033
- qrBlock = h('div', { className: 'diu-banner diu-banner-ok', style: { margin: '0 0 14px' } }, t('accounts.qr.confirmed'));
1034
- } else {
1035
- var qrMeta = [h('div', { key: 'st', className: 'diu-qrStatus' }, t('accounts.qr.' + qr.status) || qr.status)];
1036
- if (qr.status === 'need_verifycode') {
1037
- qrMeta.push(
1038
- h('div', { key: 'vc', className: 'diu-verifyRow' },
1039
- h('input', {
1040
- className: 'diu-input', style: { maxWidth: 140 }, value: qr.verifyCode || '',
1041
- onChange: function (event) { setQr(Object.assign({}, qr, { verifyCode: event.target.value })); },
1042
- }),
1043
- h('button', {
1044
- className: 'diu-btn', onClick: function () { setQr(Object.assign({}, qr, { status: 'wait' })); },
1045
- }, t('accounts.qr.code.submit')),
1046
- ),
1047
- );
1048
- }
1049
- qrMeta.push(
1050
- h('div', { key: 'act', className: 'diu-qrActions' },
1051
- h('button', { className: 'diu-btn', disabled: busy, onClick: startQr }, t('accounts.qr.refresh')),
1052
- h('button', { className: 'diu-btn', onClick: function () { setQr(null); } }, t('accounts.qr.cancel')),
1053
- ),
1054
- );
1055
- qrBlock = h('div', { className: 'diu-qrBox' },
1056
- h('img', { className: 'diu-qrImg', src: qr.qrDataUrl, width: 168, height: 168, alt: 'login qr' }),
1057
- h('div', { className: 'diu-qrMeta' }, qrMeta),
1058
- );
1059
- }
1060
- }
1061
-
1062
- var fieldNodes = fields.map(function (f) {
1063
- var labelChildren = [
1064
- h('span', { key: 'label' }, pickBilingual(f.label) || f.key),
1065
- f.required ? h('span', { key: 'req', className: 'diu-req' }, '*') : null,
1066
- ];
1067
- if (f.helpUrl) {
1068
- labelChildren.push(h('a', {
1069
- key: 'help', className: 'diu-helpLink', href: f.helpUrl, target: '_blank', rel: 'noopener noreferrer',
1070
- }, currentLang() === 'zh' ? '帮助 ↗' : 'help ↗'));
1071
- }
1072
- return h('div', { key: f.key, className: 'diu-field' + (f.secret ? '' : ' diu-fieldPlain') },
1073
- h('span', { className: 'diu-label' }, labelChildren),
1074
- h('input', {
1075
- className: 'diu-input',
1076
- type: f.secret ? 'password' : 'text',
1077
- autoComplete: f.secret ? 'new-password' : 'off',
1078
- value: values[f.key] || '',
1079
- placeholder: editing && f.secret ? '••••••••' : '',
1080
- onChange: function (event) {
1081
- var next = Object.assign({}, values);
1082
- next[f.key] = event.target.value;
1083
- setValues(next);
1084
- },
1085
- }),
1086
- );
1087
- });
1088
-
1089
- var sessionSection = null;
1090
- if (channel.notifyOnly !== true) {
1091
- var backendNone = catalog !== null && catalog.backend === 'none';
1092
- var providerGroups = (catalog && catalog.providers) || [];
1093
- var presets = (catalog && catalog.presets) || [];
1094
- var workspaces = (catalog && catalog.workspaces) || [];
1095
- var selProvider = '';
1096
- var selModel = '';
1097
- if (prefs.modelKey !== '') {
1098
- var di = prefs.modelKey.indexOf('::');
1099
- selProvider = di > 0 ? prefs.modelKey.slice(0, di) : '';
1100
- selModel = prefs.modelKey.slice(di + 2);
1101
- }
1102
- var efforts = [];
1103
- providerGroups.forEach(function (group) {
1104
- if ((group.id || '') !== selProvider) return;
1105
- (group.models || []).forEach(function (m) {
1106
- if (m.id === selModel && m.reasoning && Array.isArray(m.reasoning.efforts)) efforts = m.reasoning.efforts;
1107
- });
1108
- });
1109
- var modelOptions = [h('option', { key: '_default', value: '' }, t('prefs.model.default'))];
1110
- providerGroups.forEach(function (group) {
1111
- var groupId = group.id || '';
1112
- modelOptions.push(
1113
- h('optgroup', { key: groupId || group.name, label: group.name || groupId },
1114
- (group.models || []).map(function (m) {
1115
- return h('option', { key: m.id, value: groupId + '::' + m.id }, m.name || m.id);
1116
- }),
1117
- ),
1118
- );
1119
- });
1120
- sessionSection = h('div', { className: 'diu-sessionBlock' },
1121
- h('div', { className: 'diu-sessionTitle' }, t('modal.session.title')),
1122
- backendNone
1123
- ? h('div', { className: 'diu-hint' }, t('modal.session.none'))
1124
- : h('div', { className: 'diu-fields' },
1125
- h('div', { className: 'diu-field' },
1126
- h('span', { className: 'diu-label' }, t('access.title')),
1127
- h('select', {
1128
- className: 'diu-input', value: privateAccess,
1129
- onChange: function (event) { setPrivateAccess(event.target.value === 'all' ? 'all' : 'approved'); },
1130
- },
1131
- h('option', { value: 'approved' }, t('access.approved')),
1132
- h('option', { value: 'all' }, t('access.all')),
1133
- ),
1134
- ),
1135
- h('div', { className: 'diu-field' },
1136
- h('span', { className: 'diu-label' }, t('prefs.preset')),
1137
- h('select', {
1138
- className: 'diu-input', value: prefs.agentPreset, disabled: catalog === null,
1139
- onChange: function (event) { setPrefs(Object.assign({}, prefs, { agentPreset: event.target.value })); },
1140
- },
1141
- h('option', { value: '' }, t('prefs.preset.default')),
1142
- presets.map(function (p) {
1143
- return h('option', { key: p.id, value: p.id }, p.name || p.id);
1144
- }),
1145
- ),
1146
- ),
1147
- h('div', { className: 'diu-field diu-fieldWide' },
1148
- h('span', { className: 'diu-label' }, t('prefs.workspace')),
1149
- h('input', {
1150
- className: 'diu-input', list: 'diu-cwd-options', value: prefs.cwd,
1151
- placeholder: t('prefs.workspace.placeholder'),
1152
- onChange: function (event) { setPrefs(Object.assign({}, prefs, { cwd: event.target.value })); },
1153
- }),
1154
- h('datalist', { id: 'diu-cwd-options' },
1155
- workspaces.map(function (w) { return h('option', { key: w, value: w }); }),
1156
- ),
1157
- ),
1158
- h('div', { className: 'diu-field' },
1159
- h('span', { className: 'diu-label' }, t('prefs.model')),
1160
- catalog === null
1161
- ? h('div', { className: 'diu-hint' }, t('catalog.loading'))
1162
- : h('select', {
1163
- className: 'diu-input', value: prefs.modelKey,
1164
- onChange: function (event) { setPrefs(Object.assign({}, prefs, { modelKey: event.target.value, reasoningEffort: '' })); },
1165
- }, modelOptions),
1166
- ),
1167
- efforts.length > 0
1168
- ? h('div', { className: 'diu-field' },
1169
- h('span', { className: 'diu-label' }, t('prefs.effort')),
1170
- h('select', {
1171
- className: 'diu-input', value: prefs.reasoningEffort,
1172
- onChange: function (event) { setPrefs(Object.assign({}, prefs, { reasoningEffort: event.target.value })); },
1173
- },
1174
- h('option', { value: '' }, t('prefs.effort.default')),
1175
- efforts.map(function (e) {
1176
- return h('option', { key: e.id, value: e.id }, e.name || e.id);
1177
- }),
1178
- ),
1179
- )
1180
- : null,
1181
- h('div', { className: 'diu-fieldWide diu-hint' }, t('modal.session.hint')),
1182
- ),
1183
- );
1184
- }
1185
-
1186
- var title = editing
1187
- ? t('modal.edit.title') + ':' + (account.alias || account.accountId)
1188
- : format('modal.add.title', { channel: channelName(channel) });
1189
-
1190
- return h(Portal, null,
1191
- h('div', {
1192
- className: 'diu-modalRoot',
1193
- onMouseDown: function (event) { if (event.target === event.currentTarget) props.onClose(); },
1194
- },
1195
- h('div', { className: 'diu-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': title },
1196
- h('div', { className: 'diu-modalHead' },
1197
- h(Logo, { channel: channel.id, size: 26 }),
1198
- h('h3', { className: 'diu-modalTitle' }, title),
1199
- h('button', { className: 'diu-closeBtn', onClick: props.onClose, 'aria-label': t('modal.cancel') }, h(IconClose, null)),
1200
- ),
1201
- h('div', { className: 'diu-modalBody' },
1202
- qrBlock,
1203
- h('div', { className: 'diu-fields' },
1204
- h('div', { className: 'diu-field diu-fieldWide' },
1205
- h('span', { className: 'diu-label' }, t('accounts.alias')),
1206
- h('input', {
1207
- className: 'diu-input', value: alias, placeholder: t('accounts.alias.placeholder'),
1208
- onChange: function (event) { setAlias(event.target.value); },
1209
- }),
1210
- ),
1211
- fieldNodes,
1212
- ),
1213
- sessionSection,
1214
- h('p', { className: 'diu-modalNote' },
1215
- t('modal.autoId'),
1216
- editing ? ' ' + t('modal.editSecretHint') : '',
1217
- ),
1218
- ),
1219
- h('div', { className: 'diu-modalFoot' },
1220
- h('span', { className: 'diu-footMsg' }, err),
1221
- busy ? h('span', { className: 'diu-hint' }, t('modal.saving')) : null,
1222
- h('button', { className: 'diu-btn', onClick: props.onClose }, t('modal.cancel')),
1223
- h('button', {
1224
- className: 'diu-btn diu-btnPrimary', disabled: busy || requiredMissing(), onClick: submit,
1225
- }, busy ? t('modal.saving') : t('modal.save')),
1226
- ),
1227
- ),
1228
- ),
1229
- );
1230
- }
1231
-
1232
- function PanelBody(props) {
1233
- var state = props.state;
1234
- var caps = state.capabilities || {};
1235
- var channels = state.channels || [];
1236
- var accounts = props.accounts || [];
1237
- var byChannel = {};
1238
- accounts.forEach(function (row) {
1239
- if (byChannel[row.channel] === undefined) byChannel[row.channel] = [];
1240
- byChannel[row.channel].push(row);
1241
- });
1242
- var online = accounts.filter(function (row) { return row.connected && !row.paused; }).length;
1243
- var implemented = channels.filter(function (c) { return c.implemented === true; });
1244
- // 已实现渠道在前(保持 meta 顺序),未实现垫底灰显。
1245
- var sorted = implemented.concat(channels.filter(function (c) { return c.implemented !== true; }));
1246
-
1247
- var noticeNode = null;
1248
- if (props.notice !== null && props.notice !== undefined) {
1249
- noticeNode = h('div', { className: 'diu-banner diu-banner-' + (props.notice.kind || 'info') },
1250
- h('span', { style: { flex: 1, minWidth: 0 } }, props.notice.text),
1251
- h('button', { className: 'diu-bannerClose', onClick: props.onDismissNotice, 'aria-label': t('modal.cancel') }, h(IconClose, null)),
1252
- );
1253
- }
1254
-
1255
- return h('div', { className: 'diu-page' },
1256
- h('div', { className: 'diu-head' },
1257
- h('div', null,
1258
- h('h2', { className: 'diu-title' }, t('settings.title')),
1259
- h('p', { className: 'diu-sub' }, t('panel.subtitle')),
1260
- ),
1261
- h('div', { className: 'diu-headActions' },
1262
- h('a', { className: 'diu-iconBtn', href: REPO_URL, target: '_blank', rel: 'noopener noreferrer', style: { textDecoration: 'none' } },
1263
- h(IconGitHub, null), t('panel.github')),
1264
- h('button', { className: 'diu-iconBtn', onClick: props.onRefresh }, h(IconRefresh, null), t('panel.refresh')),
1265
- ),
1266
- ),
1267
- h('div', { className: 'diu-stats' },
1268
- h('span', { className: 'diu-stat' }, t('panel.stat.channels'), h('strong', null, implemented.length + '/' + channels.length)),
1269
- h('span', { className: 'diu-stat' }, t('panel.stat.accounts'), h('strong', null, String(accounts.length))),
1270
- h('span', { className: 'diu-stat' }, t('panel.stat.online'), h('strong', null, String(online))),
1271
- ),
1272
- noticeNode,
1273
- h('details', { className: 'diu-debug' },
1274
- h('summary', null, t('panel.debug')),
1275
- h('dl', { className: 'diu-debugBody' },
1276
- h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.generation')), h('dd', null, String(caps.settingsGeneration || '-'))),
1277
- h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.sessionBackend')), h('dd', null, String(caps.sessionBackend || '-'))),
1278
- h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.vault')), h('dd', null, String(caps.vaultKind || '-'))),
1279
- h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.uploads')), h('dd', null, String(caps.uploadsMode || '-'))),
1280
- h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.deliverables')), h('dd', null, String(caps.deliverablesMode || '-'))),
1281
- h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.connector')), h('dd', null, caps.connectorEnabled ? t('panel.on') : t('panel.off'))),
1282
- h('div', { className: 'diu-debugChannels' },
1283
- channels.map(function (c) {
1284
- return h('span', { key: c.id, className: 'diu-stat' },
1285
- channelName(c), ' · ', c.implemented ? t('panel.implemented') : t('panel.planned'));
1286
- }),
1287
- ),
1288
- ),
1289
- ),
1290
- h('div', { className: 'diu-platforms' },
1291
- sorted.map(function (channel) {
1292
- return h(PlatformCard, {
1293
- key: channel.id,
1294
- channel: channel,
1295
- accounts: byChannel[channel.id] || [],
1296
- open: props.expanded[channel.id] === true,
1297
- busy: props.busy,
1298
- checks: props.checks,
1299
- onToggle: props.onToggle,
1300
- onAdd: props.onAdd,
1301
- onCheck: props.onCheck,
1302
- onEdit: props.onEdit,
1303
- onPause: props.onPause,
1304
- onRemove: props.onRemove,
1305
- onSaveAllowlist: props.onSaveAllowlist,
1306
- });
1307
- }),
1308
- ),
1309
- props.modal !== null && props.modal !== undefined
1310
- ? h(AccountModal, {
1311
- key: props.modal.key,
1312
- channel: props.modal.channel,
1313
- account: props.modal.account || null,
1314
- onClose: props.onCloseModal,
1315
- onSaved: props.onModalSaved,
1316
- })
1317
- : null,
1318
- );
1319
- }
1320
-
1321
- function UnionPanel() {
1322
- var stateHook = useState(null);
1323
- var state = stateHook[0];
1324
- var setState = stateHook[1];
1325
- var errorHook = useState(null);
1326
- var error = errorHook[0];
1327
- var setError = errorHook[1];
1328
- var accountsHook = useState(null);
1329
- var accounts = accountsHook[0];
1330
- var setAccounts = accountsHook[1];
1331
- var checksHook = useState({});
1332
- var checks = checksHook[0];
1333
- var setChecks = checksHook[1];
1334
- var noticeHook = useState(null);
1335
- var notice = noticeHook[0];
1336
- var setNotice = noticeHook[1];
1337
- var busyHook = useState(0);
1338
- var busy = busyHook[0];
1339
- var setBusy = busyHook[1];
1340
- var expandedHook = useState({});
1341
- var expanded = expandedHook[0];
1342
- var setExpanded = expandedHook[1];
1343
- var modalHook = useState(null);
1344
- var modal = modalHook[0];
1345
- var setModal = modalHook[1];
1346
- var didInitRef = useRef(false);
1347
- var mountedRef = useRef(true);
1348
-
1349
- useEffect(function () {
1350
- mountedRef.current = true;
1351
- return function () { mountedRef.current = false; };
1352
- }, []);
1353
-
1354
- function fetchAccounts(setter) {
1355
- return getJSON('/accounts').then(function (result) {
1356
- if (!mountedRef.current) return;
1357
- if (result.status === 200 && result.json && result.json.ok) {
1358
- var rows = result.json.accounts || [];
1359
- setter(rows);
1360
- // 首次拿到账号后展开有账号的渠道卡(之后尊重用户手动开合)。
1361
- if (!didInitRef.current) {
1362
- didInitRef.current = true;
1363
- var init = {};
1364
- rows.forEach(function (row) { init[row.channel] = true; });
1365
- setExpanded(init);
1366
- }
1367
- }
1368
- });
1369
- }
1370
-
1371
- useEffect(function () {
1372
- // 状态面慢轮询 + 账号面快轮询;后台标签页暂停,回前台立即补一拍。
1373
- var stateTimer = null;
1374
- var accountsTimer = null;
1375
- var tickState = function () {
1376
- if (document.hidden) return;
1377
- fetch(API_BASE + '/state', { credentials: 'same-origin', headers: { 'x-dsh-im-union-client': '1' } })
1378
- .then(function (response) {
1379
- if (!response.ok) throw new Error('HTTP ' + response.status);
1380
- return response.json();
1381
- })
1382
- .then(function (data) {
1383
- if (mountedRef.current) { setState(data); setError(null); }
1384
- })
1385
- .catch(function (err) {
1386
- if (mountedRef.current) setError(messageText(err));
1387
- });
1388
- };
1389
- var tickAccounts = function () {
1390
- if (document.hidden) return;
1391
- fetchAccounts(function (rows) { setAccounts(rows); }).catch(function () { /* 轮询失败静默,下一拍再试 */ });
1392
- };
1393
- tickState();
1394
- tickAccounts();
1395
- stateTimer = setInterval(tickState, STATE_POLL_MS);
1396
- accountsTimer = setInterval(tickAccounts, ACCOUNTS_POLL_MS);
1397
- var onVisible = function () {
1398
- if (!document.hidden) { tickState(); tickAccounts(); }
1399
- };
1400
- document.addEventListener('visibilitychange', onVisible);
1401
- return function () {
1402
- mountedRef.current = false;
1403
- clearInterval(stateTimer);
1404
- clearInterval(accountsTimer);
1405
- document.removeEventListener('visibilitychange', onVisible);
1406
- };
1407
- }, []);
1408
-
1409
- function withBusy(task) {
1410
- setBusy(function (n) { return n + 1; });
1411
- return Promise.resolve(task()).finally(function () {
1412
- setBusy(function (n) { return Math.max(0, n - 1); });
1413
- });
1414
- }
1415
-
1416
- function refreshAll() {
1417
- withBusy(function () {
1418
- var p1 = fetch(API_BASE + '/state', { credentials: 'same-origin', headers: { 'x-dsh-im-union-client': '1' } })
1419
- .then(function (response) { if (!response.ok) throw new Error('HTTP ' + response.status); return response.json(); })
1420
- .then(function (data) { setState(data); setError(null); })
1421
- .catch(function (err) { setError(messageText(err)); });
1422
- var p2 = fetchAccounts(function (rows) { setAccounts(rows); })
1423
- .catch(function (err) { setNotice({ kind: 'err', text: messageText(err) }); });
1424
- return Promise.all([p1, p2]);
1425
- });
1426
- }
1427
-
1428
- function doAction(path, body, doneNotice) {
1429
- withBusy(function () {
1430
- return postJSON(path, body).then(function (result) {
1431
- if (result.status === 200) {
1432
- if (doneNotice) setNotice({ kind: 'ok', text: t(doneNotice) });
1433
- return fetchAccounts(function (rows) { setAccounts(rows); });
1434
- }
1435
- setNotice({ kind: 'err', text: errorText(result) });
1436
- }).catch(function (err) {
1437
- setNotice({ kind: 'err', text: messageText(err) });
1438
- });
1439
- });
1440
- }
1441
-
1442
- function doCheck(accountId) {
1443
- withBusy(function () {
1444
- return postJSON('/accounts/check', { accountId: accountId }).then(function (result) {
1445
- if (result.status === 200 && result.json && result.json.ok) {
1446
- var next = Object.assign({}, checks);
1447
- next[accountId] = result.json.checks || [];
1448
- setChecks(next);
1449
- } else {
1450
- setNotice({ kind: 'err', text: errorText(result) });
1451
- }
1452
- }).catch(function (err) {
1453
- setNotice({ kind: 'err', text: messageText(err) });
1454
- });
1455
- });
1456
- }
1457
-
1458
- if (error !== null) {
1459
- return h('div', { className: 'diu-page' },
1460
- h('div', { className: 'diu-head' },
1461
- h('div', null,
1462
- h('h2', { className: 'diu-title' }, t('settings.title')),
1463
- h('p', { className: 'diu-sub' }, t('panel.subtitle')),
1464
- ),
1465
- h('div', { className: 'diu-headActions' },
1466
- h('button', { className: 'diu-iconBtn', onClick: refreshAll }, h(IconRefresh, null), t('panel.refresh')),
1467
- ),
1468
- ),
1469
- h('div', { className: 'diu-banner diu-banner-err' }, t('panel.error') + ' (' + error + ')'),
1470
- );
1471
- }
1472
- if (state === null) {
1473
- return h('div', { className: 'diu-page' }, h('p', { className: 'diu-hint' }, t('panel.loading')));
1474
- }
1475
-
1476
- function openAdd(channel) {
1477
- setModal({ key: 'add-' + channel.id + '-' + Date.now(), channel: channel, account: null });
1478
- }
1479
- function openEdit(row) {
1480
- var channel = null;
1481
- (state.channels || []).forEach(function (c) { if (c.id === row.channel) channel = c; });
1482
- if (channel === null) return;
1483
- setModal({ key: 'edit-' + row.accountId + '-' + Date.now(), channel: channel, account: row });
1484
- }
1485
- function closeModal() { setModal(null); }
1486
- function onModalSaved(noticeKey) {
1487
- setModal(null);
1488
- setNotice({ kind: 'ok', text: t(noticeKey) });
1489
- fetchAccounts(function (rows) { setAccounts(rows); }).catch(function () { /* 关弹窗后刷新失败留给轮询 */ });
1490
- }
1491
-
1492
- return h(PanelBody, {
1493
- state: state,
1494
- accounts: accounts || [],
1495
- checks: checks,
1496
- notice: notice,
1497
- busy: busy > 0,
1498
- expanded: expanded,
1499
- modal: modal,
1500
- onRefresh: refreshAll,
1501
- onDismissNotice: function () { setNotice(null); },
1502
- onToggle: function (channelId) {
1503
- setExpanded(function (prev) {
1504
- var next = Object.assign({}, prev);
1505
- next[channelId] = !prev[channelId];
1506
- return next;
1507
- });
1508
- },
1509
- onAdd: openAdd,
1510
- onEdit: openEdit,
1511
- onCloseModal: closeModal,
1512
- onModalSaved: onModalSaved,
1513
- onCheck: doCheck,
1514
- onPause: function (accountId, paused) {
1515
- doAction('/accounts/pause', { accountId: accountId, paused: paused }, paused ? 'accounts.saved' : 'accounts.saved');
1516
- },
1517
- onRemove: function (accountId) {
1518
- doAction('/accounts/remove', { accountId: accountId }, 'accounts.removed');
1519
- },
1520
- onSaveAllowlist: function (accountId, userIds) {
1521
- doAction('/accounts/allowlist', { accountId: accountId, userIds: userIds }, 'accounts.saved');
1522
- },
1523
- });
1524
- }
1525
-
1526
- // gen1 兜底:宿主没有设置页时挂一个最小浮窗(可关闭)。
1527
- function overlayFallback() {
1528
- try {
1529
- if (typeof document === 'undefined') return;
1530
- if (document.getElementById('dsh-im-union-overlay')) return;
1531
- installStyles();
1532
- var host = document.createElement('div');
1533
- host.id = 'dsh-im-union-overlay';
1534
- host.className = 'diu-overlayFallback';
1535
- document.body.appendChild(host);
1536
- var closeBar = document.createElement('button');
1537
- closeBar.textContent = '×';
1538
- closeBar.style.cssText = 'position:absolute;top:6px;right:8px;border:0;background:transparent;font-size:16px;cursor:pointer;opacity:.6';
1539
- closeBar.addEventListener('click', function () { host.remove(); });
1540
- host.style.position = 'fixed';
1541
- host.appendChild(closeBar);
1542
- var mount = document.createElement('div');
1543
- host.appendChild(mount);
1544
- if (window.ReactDOM && typeof window.ReactDOM.createRoot === 'function') {
1545
- window.ReactDOM.createRoot(mount).render(h(UnionPanel));
1546
- } else if (window.ReactDOM && typeof window.ReactDOM.render === 'function') {
1547
- window.ReactDOM.render(h(UnionPanel), mount);
1548
- }
1549
- } catch (error) {
1550
- console.warn('[dsh-im-union] 浮窗兜底失败:', error);
1551
- }
1552
- }
1553
-
1554
- var inject = ['slots', 'locale'];
1555
-
1556
- function apply(ctx) {
1557
- console.info('[dsh-im-union] apply invoked, slots:', typeof ctx && ctx.slots ? Object.keys(ctx.slots).join(',') : 'none')
1558
- installStyles();
1559
- // apply 期顶层注册——不许关进任何回调。
1560
- try {
1561
- if (ctx.locale && typeof ctx.locale.register === 'function') {
1562
- ctx.locale.register('im-union', LOCALES);
1563
- if (ctx.locale.dictionary) {
1564
- // 宿主词典作为最高优先查找层(逐键翻译,miss 回本地词典);
1565
- // 语言判定不走它——走 currentLang() 懒解析(见 LANG_PREFIXES 注释)。
1566
- hostDictionary = ctx.locale.dictionary;
1567
- }
1568
- }
1569
- } catch (error) {
1570
- console.warn('[dsh-im-union] 语言包注册失败(回落内置词典):', error);
1571
- }
1572
-
1573
- var registered = false;
1574
- // 契约实读(dsh-session-guard 同宿主先例,src/client/index.ts:147/159):
1575
- // inject(slotName, factory) 二参必须是工厂,工厂内调 register(desc, Component);
1576
- // 裸传对象会被宿主当工厂取 .call → `e.execute.call is not a function`。
1577
- // 三形依序:①settings.section 独立顶层项(IM 面板信息密度大,不挤家族票位;
1578
- // 顶层节 desc 形实读 guard 先例 index.ts:223-240:label 为惰性函数、带 priority)
1579
- // ②overlayFallback 兜底。
1580
- try {
1581
- if (typeof ctx.slots.inject === 'function' && typeof ctx.slots.register === 'function') {
1582
- ctx.slots.inject('settings.section', function () {
1583
- console.info('[dsh-im-union] settings.section factory fired')
1584
- return ctx.slots.register({
1585
- name: 'settings.section',
1586
- id: 'dsh-im-union',
1587
- order: 42,
1588
- priority: 5,
1589
- label: function () {
1590
- return t('settings.title')
1591
- },
1592
- inject: function () {
1593
- return {};
1594
- },
1595
- }, UnionPanel)
1596
- })
1597
- registered = true
1598
- console.info('[dsh-im-union] settings.section 独立顶层项已声明')
1599
- // bundle 详情页配置卡(Dsh 0.2.0:Plugins 详情页不自动渲染,须 key=包名 注册)。
1600
- try {
1601
- ctx.slots.inject('plugins.bundle.config', function () {
1602
- return ctx.slots.register(
1603
- {
1604
- name: 'plugins.bundle.config',
1605
- key: 'dsh-im-union',
1606
- inject: function () {
1607
- return {};
1608
- },
1609
- },
1610
- UnionPanel,
1611
- )
1612
- })
1613
- } catch (error) {
1614
- console.warn('[dsh-im-union] plugins.bundle.config 席位不可用:', error)
1615
- }
1616
- } else {
1617
- console.warn('[dsh-im-union] ctx.slots 无 inject/register 面(老线或壳缺):', typeof ctx.slots)
1618
- }
1619
- } catch (error) {
1620
- console.warn('[dsh-im-union] 槽位注册失败:', error)
1621
- }
1622
- if (!registered) {
1623
- // gen1 或槽位面异常 → 浮窗兜底,保证至少有一条可视管理入口。
1624
- overlayFallback();
1625
- }
1626
- }
1627
-
1628
- exports.name = 'dsh-im-union-client';
1629
- exports.inject = inject;
1630
- exports.apply = apply;
1631
- return module.exports;
1632
- },
1633
- });
1
+ /**
2
+ * dsh-im-union 浏览器半(ModuleLoader 工厂,id 必须等于 npm 包名,否则整体静默失活)。
3
+ *
4
+ * 注册纪律:槽位注册必须发生在 client apply 期顶层——关在任何 inject 回调里
5
+ * 是两代事故的根因。设置面代际差异在浏览器侧的体现:
6
+ * - gen3(0.1.7+/0.2.x):settings.section 槽位挂面板;
7
+ * - gen2(0.1.2~0.1.6):设置节由宿主半 installSection 出表单,client 只补状态卡;
8
+ * - gen1(0.1.0/0.1.1):无设置页 → 自带浮窗兜底(本文件 overlayFallback)。
9
+ * 宿主未声明的槽位会抛 SlotOwnershipError 并让整块 UI 消失——所有 renderSlot
10
+ * 一律 try/catch,失败只当该槽位没有内容。
11
+ *
12
+ * 面板设计(v2,借鉴 dsh-im 的渠道卡视觉与 dsh-im-connect 的账号手风琴):
13
+ * - 头部(品牌 + 副标语 + GitHub/刷新)→ 统计条 → 折叠诊断 → 渠道手风琴卡;
14
+ * - 渠道卡 = 品牌色块 logo + 名称/标签 + 账号数 + 添加账号 + 展开箭头;
15
+ * - 账号行 = 名称/内部键 + 状态徽章 + 检查/编辑/暂停/删除(两步确认)+ 白名单;
16
+ * - 添加/编辑走同一弹窗:微信为扫码流(先建档后取码),其余渠道按 meta.fields
17
+ * 出双列表单——secret 字段只写不读(留空=保持),plain 字段进 record.config
18
+ * (修复旧版全塞 secrets 导致明文字段被 upsert 丢弃的问题)。
19
+ * - 样式单 <style data-plugin> 注入;颜色全部走 var(--dsw-alias-*, fallback)
20
+ * 消费链,暗色/浅色零分支,主题切换由宿主令牌接管。
21
+ */
22
+ window.__ModuleLoader__.load({
23
+ id: 'dsh-im-union',
24
+ factory: function (require) {
25
+ // 插桩(真机诊断):宿主装载链哪一段没走到这里,日志即缺席——与 apply 入口日志
26
+ // 成对,可二分「bundle 加载但 factory 未调」vs「factory 调了但 apply 未调」。
27
+ console.info('[dsh-im-union] client factory invoked')
28
+ var module = { exports: {} };
29
+ var exports = module.exports;
30
+ var React = require('react');
31
+ var h = React.createElement;
32
+ var useState = React.useState;
33
+ var useEffect = React.useEffect;
34
+ var useRef = React.useRef;
35
+ var ReactDOM = null;
36
+ try { ReactDOM = require('react-dom'); } catch (noReactDOM) { ReactDOM = null; }
37
+ if (ReactDOM === null && typeof window !== 'undefined') ReactDOM = window.ReactDOM || null;
38
+
39
+ var API_BASE = '/api/dsh-im-union';
40
+ var REPO_URL = 'https://github.com/drscrewdriver/dsh-im-union';
41
+ var STATE_POLL_MS = 60000;
42
+ var ACCOUNTS_POLL_MS = 15000;
43
+ var STYLE_ID = 'dsh-im-union-settings';
44
+
45
+ // ── 语言词典(zh 真理源;其余语言缺键回落 zh,运行时字典与
46
+ // locale/*.json 元数据两处分层,此处只放 client 侧 UI 文案)─────────────
47
+ var LOCALES = {
48
+ zh: {
49
+ 'settings.title': 'IM 联合桥',
50
+ 'panel.subtitle': '把多个 IM 平台接入本机 Harness:多账号独立会话、主动投递、跨宿主版本兼容。',
51
+ 'panel.loading': '正在读取本机状态…',
52
+ 'panel.error': '无法连接管理接口,请重启 Host 后刷新。',
53
+ 'panel.refresh': '刷新',
54
+ 'panel.github': 'GitHub',
55
+ 'panel.debug': '运行时诊断',
56
+ 'panel.generation': '设置面代际',
57
+ 'panel.sessionBackend': '会话后端',
58
+ 'panel.vault': '凭据存储',
59
+ 'panel.uploads': '文件上传',
60
+ 'panel.deliverables': '产物回传',
61
+ 'panel.connector': 'AI Office Connector',
62
+ 'panel.on': '已启用',
63
+ 'panel.off': '未启用',
64
+ 'panel.channels': '渠道实现度',
65
+ 'panel.implemented': '已实现',
66
+ 'panel.planned': '规划中',
67
+ 'panel.stat.channels': '渠道',
68
+ 'panel.stat.accounts': '账号',
69
+ 'panel.stat.online': '在线',
70
+ 'channel.feishu': '飞书',
71
+ 'channel.weixin': '微信',
72
+ 'channel.telegram': 'Telegram',
73
+ 'channel.discord': 'Discord',
74
+ 'channel.wecom': '企业微信',
75
+ 'channel.dingtalk': '钉钉',
76
+ 'channel.qq': 'QQ',
77
+ 'channel.slack': 'Slack',
78
+ 'channel.whatsapp': 'WhatsApp',
79
+ 'channel.matrix': 'Matrix',
80
+ 'channel.imessage': 'iMessage',
81
+ 'channel.wecom-app': '企业微信(自建应用)',
82
+ 'channel.email': '邮箱(IMAP/SMTP)',
83
+ 'channel.dingtalk-notify': '钉钉通知(群机器人)',
84
+ 'channel.qq-notify': 'QQ 通知(主动推送)',
85
+ 'channel.tag.planned': '规划中',
86
+ 'channel.tag.notify': '仅通知',
87
+ 'channel.tag.macos': '仅 macOS',
88
+ 'channel.count': '{n} 个账号',
89
+ 'channel.emptyHint': '点击「添加账号」接入第一个机器人。',
90
+ 'accounts.title': '账号管理',
91
+ 'accounts.add': '添加账号',
92
+ 'accounts.edit': '编辑',
93
+ 'accounts.check': '检查连接',
94
+ 'accounts.checking': '检查中…',
95
+ 'accounts.pause': '暂停',
96
+ 'accounts.resume': '恢复',
97
+ 'accounts.remove': '删除',
98
+ 'accounts.remove.confirm': '确认删除',
99
+ 'accounts.state.connected': '已连接',
100
+ 'accounts.state.expired': '会话过期,需重新扫码',
101
+ 'accounts.send.failed': '最近发送失败',
102
+ 'rail.title': '频道', 'rail.search': '搜索会话', 'rail.loading': '加载中…', 'rail.empty': '暂无 IM 会话', 'rail.error': '加载失败,稍后重试', 'rail.detached': '已解绑', 'rail.tabTasks': '任务', 'rail.tabChannels': '频道', 'time.now': '刚刚', 'time.minutes': '{n} 分钟前', 'time.hours': '{n} 小时前', 'time.days': '{n} 天前',
103
+ 'accounts.state.disconnected': '未连接',
104
+ 'accounts.state.paused': '已暂停',
105
+ 'accounts.secret.missing': '凭据缺失:{keys}',
106
+ 'accounts.alias': '别名(可选)',
107
+ 'accounts.alias.placeholder': '给账号起个好认的名字',
108
+ 'accounts.allowlist': '私聊白名单',
109
+ 'accounts.allowlist.hint': '每行一个用户 ID;留空 = 拒绝所有私聊',
110
+ 'accounts.allowlist.save': '保存白名单',
111
+ 'accounts.prefs': '会话偏好',
112
+ 'accounts.saved': '已保存',
113
+ 'accounts.saved.bindhint': '已保存(未连接):用「生成登录二维码」完成扫码授权后自动连接',
114
+ 'accounts.removed': '已删除',
115
+ 'accounts.qr.get': '生成登录二维码',
116
+ 'accounts.qr.refresh': '重新获取二维码',
117
+ 'accounts.qr.cancel': '取消扫码',
118
+ 'accounts.qr.needid': '请先填写账号 ID 再获取二维码',
119
+ 'accounts.qr.wait': '等待扫码…(用手机微信扫左侧二维码)',
120
+ 'accounts.qr.scaned': '已扫码,请在手机上确认授权',
121
+ 'accounts.qr.expired': '二维码已过期,请重新获取',
122
+ 'accounts.qr.need_verifycode': '请输入手机上显示的验证码',
123
+ 'accounts.qr.verify_code_blocked': '验证码错误次数过多,请重新获取二维码',
124
+ 'accounts.qr.binded_redirect': '该 Bot 已绑定其它实例',
125
+ 'accounts.qr.scaned_but_redirect': '已扫码,正在重定向等待确认',
126
+ 'accounts.qr.code.submit': '提交验证码',
127
+ 'accounts.qr.confirmed': '扫码绑定完成,凭据已保存并连接',
128
+ 'accounts.qr.desc': '二维码由腾讯微信 iLink 服务签发。扫码确认后凭据只写入本机 Host,浏览器不会收到 bot_token。',
129
+ 'modal.add.title': '添加{channel}账号',
130
+ 'modal.edit.title': '编辑账号',
131
+ 'modal.cancel': '取消',
132
+ 'modal.save': '保存',
133
+ 'modal.saving': '保存中…',
134
+ 'modal.required': '* 为必填',
135
+ 'modal.editSecretHint': '凭据留空表示保持现有值不变。',
136
+ 'modal.autoId': '账号 ID 由系统自动生成,无需填写;别名用于列表展示。',
137
+ 'modal.session.title': '会话偏好(新会话生效)',
138
+ 'modal.session.hint': '工作区/模型/预设/权限对新会话生效;权限预设同时即时应用到该账号已活跃会话;命令执行开关即时生效。',
139
+ 'modal.session.none': '会话后端未就绪(宿主就绪后自动升级,无需重启);以下设置仍可编辑保存,命令执行/准入/工作区即时生效。',
140
+ 'access.title': '私聊准入',
141
+ 'access.approved': '仅已批准用户',
142
+ 'access.all': '所有私聊',
143
+ 'access.all.tag': '私聊全开',
144
+ 'access.hint': '「所有私聊」= 任何人都可私聊该机器人;白名单外的用户会收到未授权提示。',
145
+ 'prefs.workspace': '工作区目录(cwd)',
146
+ 'prefs.workspace.placeholder': '留空 = 宿主默认工作区',
147
+ 'prefs.preset': 'Agent 预设',
148
+ 'prefs.preset.default': '宿主默认',
149
+ 'prefs.model': '模型',
150
+ 'prefs.model.default': '宿主默认',
151
+ 'prefs.effort': '推理等级',
152
+ 'prefs.effort.default': '模型默认',
153
+ 'prefs.permission': '权限预设',
154
+ 'prefs.permission.default': '宿主默认',
155
+ 'exec.dm': '私聊命令执行',
156
+ 'exec.group': '群聊命令执行',
157
+ 'exec.on': '允许',
158
+ 'exec.off': '关闭',
159
+ 'catalog.loading': '目录读取中…',
160
+ 'check.title': '诊断结果',
161
+ 'check.passed': '通过',
162
+ 'check.failed': '失败',
163
+ 'check.unverified': '未验证',
164
+ 'whitelist.title': '模型白名单',
165
+ 'whitelist.placeholder': '一行一个 provider/model(留空=不限)',
166
+ 'whitelist.save': '保存白名单',
167
+ 'whitelist.saved': '已保存',
168
+ 'whitelist.hint': '设置后 /model 仅允许白名单内切换,/models 仅列出白名单内模型。',
169
+ 'relay.title': '抄送绑定',
170
+ 'relay.bind': '绑定会话',
171
+ 'relay.unbind': '解绑',
172
+ 'relay.doBind': '添加绑定',
173
+ 'relay.empty': '暂无绑定的会话',
174
+ 'relay.sessionId': '会话 ID',
175
+ 'relay.targetId': '推送目标(可选)',
176
+ },
177
+ en: {
178
+ 'settings.title': 'IM Union Bridge',
179
+ 'panel.subtitle': 'Connect IM platforms to your local Harness: multi-account sessions, proactive delivery, cross-version host support.',
180
+ 'panel.loading': 'Loading local state…',
181
+ 'panel.error': 'Management API unreachable. Restart the host and refresh.',
182
+ 'panel.refresh': 'Refresh',
183
+ 'panel.github': 'GitHub',
184
+ 'panel.debug': 'Runtime diagnostics',
185
+ 'panel.generation': 'Settings generation',
186
+ 'panel.sessionBackend': 'Session backend',
187
+ 'panel.vault': 'Credential vault',
188
+ 'panel.uploads': 'File uploads',
189
+ 'panel.deliverables': 'Deliverables',
190
+ 'panel.connector': 'AI Office Connector',
191
+ 'panel.on': 'enabled',
192
+ 'panel.off': 'disabled',
193
+ 'panel.channels': 'Channels',
194
+ 'panel.implemented': 'implemented',
195
+ 'panel.planned': 'planned',
196
+ 'panel.stat.channels': 'Channels',
197
+ 'panel.stat.accounts': 'Accounts',
198
+ 'panel.stat.online': 'Online',
199
+ 'channel.feishu': 'Feishu / Lark',
200
+ 'channel.weixin': 'WeChat (iLink)',
201
+ 'channel.telegram': 'Telegram',
202
+ 'channel.discord': 'Discord',
203
+ 'channel.wecom': 'WeCom',
204
+ 'channel.dingtalk': 'DingTalk',
205
+ 'channel.qq': 'QQ',
206
+ 'channel.slack': 'Slack',
207
+ 'channel.whatsapp': 'WhatsApp',
208
+ 'channel.matrix': 'Matrix',
209
+ 'channel.imessage': 'iMessage',
210
+ 'channel.wecom-app': 'WeCom (self-built app)',
211
+ 'channel.email': 'Email (IMAP/SMTP)',
212
+ 'channel.dingtalk-notify': 'DingTalk notify (webhook)',
213
+ 'channel.qq-notify': 'QQ notify (push)',
214
+ 'channel.tag.planned': 'planned',
215
+ 'channel.tag.notify': 'notify only',
216
+ 'channel.tag.macos': 'macOS only',
217
+ 'channel.count': '{n} accounts',
218
+ 'channel.emptyHint': 'Click "Add account" to connect your first bot.',
219
+ 'accounts.title': 'Accounts',
220
+ 'accounts.add': 'Add account',
221
+ 'accounts.edit': 'Edit',
222
+ 'accounts.check': 'Check connection',
223
+ 'accounts.checking': 'Checking…',
224
+ 'accounts.pause': 'Pause',
225
+ 'accounts.resume': 'Resume',
226
+ 'accounts.remove': 'Remove',
227
+ 'accounts.remove.confirm': 'Confirm remove',
228
+ 'accounts.state.connected': 'Connected',
229
+ 'accounts.state.expired': 'Session expired — re-scan QR',
230
+ 'accounts.send.failed': 'Last send failed',
231
+ 'rail.title': 'Channels', 'rail.search': 'Search sessions', 'rail.loading': 'Loading…', 'rail.empty': 'No IM sessions yet', 'rail.error': 'Load failed — retrying', 'rail.detached': 'unbound', 'time.now': 'just now', 'time.minutes': '{n}m ago', 'time.hours': '{n}h ago', 'time.days': '{n}d ago',
232
+ 'accounts.state.disconnected': 'Disconnected',
233
+ 'accounts.state.paused': 'Paused',
234
+ 'accounts.secret.missing': 'Credential missing: {keys}',
235
+ 'accounts.alias': 'Alias (optional)',
236
+ 'accounts.alias.placeholder': 'A recognizable name for this account',
237
+ 'accounts.allowlist': 'DM allowlist',
238
+ 'accounts.allowlist.hint': 'One user ID per line; empty = reject all DMs',
239
+ 'accounts.allowlist.save': 'Save allowlist',
240
+ 'accounts.prefs': 'Session prefs',
241
+ 'accounts.saved': 'Saved',
242
+ 'accounts.saved.bindhint': 'Saved (disconnected): use "Get login QR code" to bind, it connects automatically',
243
+ 'accounts.removed': 'Removed',
244
+ 'accounts.qr.get': 'Get login QR code',
245
+ 'accounts.qr.refresh': 'Refresh QR code',
246
+ 'accounts.qr.cancel': 'Cancel',
247
+ 'accounts.qr.needid': 'Fill the account ID first, then get the QR code',
248
+ 'accounts.qr.wait': 'Waiting for scan… (scan with WeChat)',
249
+ 'accounts.qr.scaned': 'Scanned — confirm on your phone',
250
+ 'accounts.qr.expired': 'QR expired, fetch a new one',
251
+ 'accounts.qr.need_verifycode': 'Enter the code shown on your phone',
252
+ 'accounts.qr.verify_code_blocked': 'Too many wrong codes, fetch a new QR',
253
+ 'accounts.qr.binded_redirect': 'This bot is already bound to another instance',
254
+ 'accounts.qr.scaned_but_redirect': 'Scanned — redirecting, waiting for confirm',
255
+ 'accounts.qr.code.submit': 'Submit code',
256
+ 'accounts.qr.confirmed': 'QR binding done — credentials saved and connected',
257
+ 'accounts.qr.desc': 'The QR code is signed by Tencent WeChat iLink. After scanning, credentials go straight into the local Host; the browser never sees the bot token.',
258
+ 'modal.add.title': 'Add {channel} account',
259
+ 'modal.edit.title': 'Edit account',
260
+ 'modal.cancel': 'Cancel',
261
+ 'modal.save': 'Save',
262
+ 'modal.saving': 'Saving…',
263
+ 'modal.required': '* required',
264
+ 'modal.editSecretHint': 'Leave credentials blank to keep the current values.',
265
+ 'modal.autoId': 'The account ID is generated automatically; the alias is for display only.',
266
+ 'modal.session.title': 'Session preferences (new sessions only)',
267
+ 'modal.session.hint': 'Workspace/model/preset/permission apply to sessions this account creates later; the permission preset also applies to this account\'s live sessions right away; command switches take effect immediately.',
268
+ 'modal.session.none': 'Session backend not ready yet (auto-upgrades once the host provides it — no restart needed); the settings below stay editable, and command switches / DM access / workspace apply immediately.',
269
+ 'access.title': 'DM access',
270
+ 'access.approved': 'Approved users only',
271
+ 'access.all': 'Everyone',
272
+ 'access.all.tag': 'open DM',
273
+ 'access.hint': '"Everyone" lets anybody DM this bot; rejected users get a notice with their user ID.',
274
+ 'prefs.workspace': 'Workspace directory (cwd)',
275
+ 'prefs.workspace.placeholder': 'Empty = host default workspace',
276
+ 'prefs.preset': 'Agent preset',
277
+ 'prefs.preset.default': 'Host default',
278
+ 'prefs.model': 'Model',
279
+ 'prefs.model.default': 'Host default',
280
+ 'prefs.effort': 'Reasoning effort',
281
+ 'prefs.effort.default': 'Model default',
282
+ 'prefs.permission': 'Permission preset',
283
+ 'prefs.permission.default': 'Host default',
284
+ 'exec.dm': 'DM commands',
285
+ 'exec.group': 'Group commands',
286
+ 'exec.on': 'Allow',
287
+ 'exec.off': 'Block',
288
+ 'catalog.loading': 'Loading catalog…',
289
+ 'check.title': 'Diagnostics',
290
+ 'check.passed': 'passed',
291
+ 'check.failed': 'failed',
292
+ 'check.unverified': 'unverified',
293
+ 'whitelist.title': 'Model Whitelist',
294
+ 'whitelist.placeholder': 'One provider/model per line (empty = no restriction)',
295
+ 'whitelist.save': 'Save Whitelist',
296
+ 'whitelist.saved': 'Saved',
297
+ 'whitelist.hint': 'When set, /model only allows switching within the whitelist; /models lists only whitelisted models.',
298
+ 'relay.title': 'Relay Binding',
299
+ 'relay.bind': 'Bind Session',
300
+ 'relay.unbind': 'Unbind',
301
+ 'relay.doBind': 'Add Binding',
302
+ 'relay.empty': 'No bound sessions',
303
+ 'relay.sessionId': 'Session ID',
304
+ 'relay.targetId': 'Push target (optional)',
305
+ },
306
+ ja: { 'settings.title': 'IM Union', 'accounts.title': 'アカウント', 'accounts.add': '追加', 'accounts.edit': '編集', 'accounts.save': '保存', 'modal.cancel': 'キャンセル', 'accounts.check': '接続確認', 'accounts.pause': '一時停止', 'accounts.resume': '再開', 'accounts.remove': '削除', 'accounts.state.connected': '接続済', 'accounts.state.expired': 'セッション失効・再スキャン', 'accounts.send.failed': '最終送信失敗', 'rail.title': 'チャンネル', 'rail.search': 'セッション検索', 'rail.loading': '読み込み中…', 'rail.empty': 'IMセッションなし', 'rail.error': '読込失敗・再試行', 'rail.detached': '解除済み', 'time.now': 'たった今', 'time.minutes': '{n}分前', 'time.hours': '{n}時間前', 'time.days': '{n}日前', 'accounts.state.disconnected': '未接続', 'accounts.state.paused': '停止中', 'check.passed': '成功', 'check.failed': '失敗', 'check.unverified': '未検証', 'prefs.permission': '権限プリセット', 'prefs.permission.default': 'ホスト既定', 'exec.dm': 'DMコマンド実行', 'exec.group': 'グループコマンド実行', 'exec.on': '許可', 'exec.off': '遮断', 'whitelist.title': 'Model Whitelist', 'whitelist.placeholder': 'One provider/model per line (empty = no restriction)', 'whitelist.save': 'Save Whitelist', 'whitelist.saved': 'Saved', 'whitelist.hint': 'When set, /model only allows switching within the whitelist; /models lists only whitelisted models.', 'relay.title': 'Relay Binding', 'relay.bind': 'Bind Session', 'relay.unbind': 'Unbind', 'relay.doBind': 'Add Binding', 'relay.empty': 'No bound sessions', 'relay.sessionId': 'Session ID', 'relay.targetId': 'Push target (optional)' },
307
+ ko: { 'settings.title': 'IM Union', 'accounts.title': '계정', 'accounts.add': '추가', 'accounts.edit': '편집', 'accounts.save': '저장', 'modal.cancel': '취소', 'accounts.check': '연결 확인', 'accounts.pause': '일시정지', 'accounts.resume': '재개', 'accounts.remove': '삭제', 'accounts.state.connected': '연결됨', 'accounts.state.expired': '세션 만료(재스캔 필요)', 'accounts.send.failed': '마지막 전송 실패', 'rail.title': '채널', 'rail.search': '세션 검색', 'rail.loading': '불러오는 중…', 'rail.empty': 'IM 세션 없음', 'rail.error': '로드 실패·재시도', 'rail.detached': '해제됨', 'time.now': '방금', 'time.minutes': '{n}분 전', 'time.hours': '{n}시간 전', 'time.days': '{n}일 전', 'accounts.state.disconnected': '미연결', 'accounts.state.paused': '정지됨', 'check.passed': '통과', 'check.failed': '실패', 'check.unverified': '미검증', 'prefs.permission': '권한 프리셋', 'prefs.permission.default': '호스트 기본', 'exec.dm': 'DM 명령 실행', 'exec.group': '그룹 명령 실행', 'exec.on': '허용', 'exec.off': '차단', 'whitelist.title': 'Model Whitelist', 'whitelist.placeholder': 'One provider/model per line (empty = no restriction)', 'whitelist.save': 'Save Whitelist', 'whitelist.saved': 'Saved', 'whitelist.hint': 'When set, /model only allows switching within the whitelist; /models lists only whitelisted models.', 'relay.title': 'Relay Binding', 'relay.bind': 'Bind Session', 'relay.unbind': 'Unbind', 'relay.doBind': 'Add Binding', 'relay.empty': 'No bound sessions', 'relay.sessionId': 'Session ID', 'relay.targetId': 'Push target (optional)' },
308
+ fr: { 'settings.title': 'IM Union', 'accounts.title': 'Comptes', 'accounts.add': 'Ajouter', 'accounts.edit': 'Modifier', 'accounts.save': 'Enregistrer', 'modal.cancel': 'Annuler', 'accounts.check': 'Vérifier', 'accounts.pause': 'Suspendre', 'accounts.resume': 'Reprendre', 'accounts.remove': 'Supprimer', 'accounts.state.connected': 'Connecté', 'accounts.state.expired': 'Session expirée — rescanner', 'accounts.send.failed': 'Dernier envoi échoué', 'rail.title': 'Canaux', 'rail.search': 'Rechercher', 'rail.loading': 'Chargement…', 'rail.empty': 'Aucune session IM', 'rail.error': 'Échec — nouvelle tentative', 'rail.detached': 'délié', 'time.now': "à l'instant", 'time.minutes': 'il y a {n} min', 'time.hours': 'il y a {n} h', 'time.days': 'il y a {n} j', 'accounts.state.disconnected': 'Déconnecté', 'accounts.state.paused': 'Suspendu', 'check.passed': 'réussi', 'check.failed': 'échoué', 'check.unverified': 'non vérifié', 'prefs.permission': 'Préréglage de permission', 'prefs.permission.default': 'Défaut hôte', 'exec.dm': 'Commandes en MP', 'exec.group': 'Commandes en groupe', 'exec.on': 'Autoriser', 'exec.off': 'Bloquer', 'whitelist.title': 'Model Whitelist', 'whitelist.placeholder': 'One provider/model per line (empty = no restriction)', 'whitelist.save': 'Save Whitelist', 'whitelist.saved': 'Saved', 'whitelist.hint': 'When set, /model only allows switching within the whitelist; /models lists only whitelisted models.', 'relay.title': 'Relay Binding', 'relay.bind': 'Bind Session', 'relay.unbind': 'Unbind', 'relay.doBind': 'Add Binding', 'relay.empty': 'No bound sessions', 'relay.sessionId': 'Session ID', 'relay.targetId': 'Push target (optional)' },
309
+ de: { 'settings.title': 'IM Union', 'accounts.title': 'Konten', 'accounts.add': 'Hinzufügen', 'accounts.edit': 'Bearbeiten', 'accounts.save': 'Speichern', 'modal.cancel': 'Abbrechen', 'accounts.check': 'Verbindung prüfen', 'accounts.pause': 'Pausieren', 'accounts.resume': 'Fortsetzen', 'accounts.remove': 'Entfernen', 'accounts.state.connected': 'Verbunden', 'accounts.state.expired': 'Sitzung abgelaufen — neu scannen', 'accounts.send.failed': 'Letzter Versand fehlgeschlagen', 'rail.title': 'Kanäle', 'rail.search': 'Sitzungen suchen', 'rail.loading': 'Lädt…', 'rail.empty': 'Keine IM-Sitzungen', 'rail.error': 'Fehlgeschlagen — erneut', 'rail.detached': 'gelöst', 'time.now': 'gerade eben', 'time.minutes': 'vor {n} Min.', 'time.hours': 'vor {n} Std.', 'time.days': 'vor {n} T.', 'accounts.state.disconnected': 'Getrennt', 'accounts.state.paused': 'Pausiert', 'check.passed': 'ok', 'check.failed': 'fehlgeschlagen', 'check.unverified': 'ungeprüft', 'prefs.permission': 'Berechtigungsprofil', 'prefs.permission.default': 'Host-Standard', 'exec.dm': 'DM-Befehle', 'exec.group': 'Gruppenbefehle', 'exec.on': 'Erlauben', 'exec.off': 'Blockieren', 'whitelist.title': 'Model Whitelist', 'whitelist.placeholder': 'One provider/model per line (empty = no restriction)', 'whitelist.save': 'Save Whitelist', 'whitelist.saved': 'Saved', 'whitelist.hint': 'When set, /model only allows switching within the whitelist; /models lists only whitelisted models.', 'relay.title': 'Relay Binding', 'relay.bind': 'Bind Session', 'relay.unbind': 'Unbind', 'relay.doBind': 'Add Binding', 'relay.empty': 'No bound sessions', 'relay.sessionId': 'Session ID', 'relay.targetId': 'Push target (optional)' },
310
+ it: { 'settings.title': 'IM Union', 'accounts.title': 'Account', 'accounts.add': 'Aggiungi', 'accounts.edit': 'Modifica', 'accounts.save': 'Salva', 'modal.cancel': 'Annulla', 'accounts.check': 'Controlla', 'accounts.pause': 'Pausa', 'accounts.resume': 'Riprendi', 'accounts.remove': 'Rimuovi', 'accounts.state.connected': 'Connesso', 'accounts.state.expired': 'Sessione scaduta — riscansionare', 'accounts.send.failed': 'Ultimo invio fallito', 'rail.title': 'Canali', 'rail.search': 'Cerca sessioni', 'rail.loading': 'Caricamento…', 'rail.empty': 'Nessuna sessione IM', 'rail.error': 'Caricamento non riuscito', 'rail.detached': 'slegata', 'time.now': 'adesso', 'time.minutes': '{n} min fa', 'time.hours': '{n} h fa', 'time.days': '{n} g fa', 'accounts.state.disconnected': 'Disconnesso', 'accounts.state.paused': 'In pausa', 'check.passed': 'ok', 'check.failed': 'fallito', 'check.unverified': 'non verificato', 'prefs.permission': 'Preset permessi', 'prefs.permission.default': 'Predefinito host', 'exec.dm': 'Comandi in DM', 'exec.group': 'Comandi in gruppo', 'exec.on': 'Consenti', 'exec.off': 'Blocca', 'whitelist.title': 'Model Whitelist', 'whitelist.placeholder': 'One provider/model per line (empty = no restriction)', 'whitelist.save': 'Save Whitelist', 'whitelist.saved': 'Saved', 'whitelist.hint': 'When set, /model only allows switching within the whitelist; /models lists only whitelisted models.', 'relay.title': 'Relay Binding', 'relay.bind': 'Bind Session', 'relay.unbind': 'Unbind', 'relay.doBind': 'Add Binding', 'relay.empty': 'No bound sessions', 'relay.sessionId': 'Session ID', 'relay.targetId': 'Push target (optional)' },
311
+ ru: { 'settings.title': 'IM Union', 'accounts.title': 'Аккаунты', 'accounts.add': 'Добавить', 'accounts.edit': 'Изменить', 'accounts.save': 'Сохранить', 'modal.cancel': 'Отмена', 'accounts.check': 'Проверить', 'accounts.pause': 'Пауза', 'accounts.resume': 'Возобновить', 'accounts.remove': 'Удалить', 'accounts.state.connected': 'Подключено', 'accounts.state.expired': 'Сессия истекла — пересканируйте', 'accounts.send.failed': 'Последняя отправка не удалась', 'rail.title': 'Каналы', 'rail.search': 'Поиск сеансов', 'rail.loading': 'Загрузка…', 'rail.empty': 'Нет IM-сеансов', 'rail.error': 'Сбой — повтор', 'rail.detached': 'отвязан', 'time.now': 'только что', 'time.minutes': '{n} мин назад', 'time.hours': '{n} ч назад', 'time.days': '{n} дн назад', 'accounts.state.disconnected': 'Отключено', 'accounts.state.paused': 'Пауза', 'check.passed': 'успех', 'check.failed': 'ошибка', 'check.unverified': 'не проверено', 'prefs.permission': 'Пресет разрешений', 'prefs.permission.default': 'По умолчанию хоста', 'exec.dm': 'Команды в ЛС', 'exec.group': 'Команды в группе', 'exec.on': 'Разрешить', 'exec.off': 'Запретить', 'whitelist.title': 'Model Whitelist', 'whitelist.placeholder': 'One provider/model per line (empty = no restriction)', 'whitelist.save': 'Save Whitelist', 'whitelist.saved': 'Saved', 'whitelist.hint': 'When set, /model only allows switching within the whitelist; /models lists only whitelisted models.', 'relay.title': 'Relay Binding', 'relay.bind': 'Bind Session', 'relay.unbind': 'Unbind', 'relay.doBind': 'Add Binding', 'relay.empty': 'No bound sessions', 'relay.sessionId': 'Session ID', 'relay.targetId': 'Push target (optional)' },
312
+ es: { 'settings.title': 'IM Union', 'accounts.title': 'Cuentas', 'accounts.add': 'Añadir', 'accounts.edit': 'Editar', 'accounts.save': 'Guardar', 'modal.cancel': 'Cancelar', 'accounts.check': 'Comprobar', 'accounts.pause': 'Pausar', 'accounts.resume': 'Reanudar', 'accounts.remove': 'Eliminar', 'accounts.state.connected': 'Conectado', 'accounts.state.expired': 'Sesión caducada — reescanear', 'accounts.send.failed': 'Último envío fallido', 'rail.title': 'Canales', 'rail.search': 'Buscar sesiones', 'rail.loading': 'Cargando…', 'rail.empty': 'Sin sesiones IM', 'rail.error': 'Error — reintentando', 'rail.detached': 'desvinculada', 'time.now': 'ahora mismo', 'time.minutes': 'hace {n} min', 'time.hours': 'hace {n} h', 'time.days': 'hace {n} d', 'accounts.state.disconnected': 'Desconectado', 'accounts.state.paused': 'Pausado', 'check.passed': 'correcto', 'check.failed': 'fallido', 'check.unverified': 'sin verificar', 'prefs.permission': 'Preajuste de permisos', 'prefs.permission.default': 'Predeterminado del host', 'exec.dm': 'Comandos por DM', 'exec.group': 'Comandos en grupo', 'exec.on': 'Permitir', 'exec.off': 'Bloquear', 'whitelist.title': 'Model Whitelist', 'whitelist.placeholder': 'One provider/model per line (empty = no restriction)', 'whitelist.save': 'Save Whitelist', 'whitelist.saved': 'Saved', 'whitelist.hint': 'When set, /model only allows switching within the whitelist; /models lists only whitelisted models.', 'relay.title': 'Relay Binding', 'relay.bind': 'Bind Session', 'relay.unbind': 'Unbind', 'relay.doBind': 'Add Binding', 'relay.empty': 'No bound sessions', 'relay.sessionId': 'Session ID', 'relay.targetId': 'Push target (optional)' },
313
+ };
314
+
315
+ // UI 语言:**取词时懒解析**,不在 factory 顶层冻结。真机实证:宿主初始 HTML
316
+ // 是 <html lang="en">,i18n 启动后才改写为真实语言(如 zh-CN)——顶层冻结会
317
+ // 把中文环境锁死在英文面。每处取词现场读 <html lang>;未知/缺空前缀保守归
318
+ // zh(缺键再逐条回落 zh 词典)。
319
+ var LANG_PREFIXES = ['zh', 'en', 'ja', 'ko', 'fr', 'de', 'it', 'ru', 'es'];
320
+ function currentLang() {
321
+ try {
322
+ var docLang = String((typeof document !== 'undefined' && document.documentElement.lang) || '').toLowerCase();
323
+ for (var i = 0; i < LANG_PREFIXES.length; i += 1) {
324
+ if (docLang.indexOf(LANG_PREFIXES[i]) === 0) return LANG_PREFIXES[i];
325
+ }
326
+ } catch (noDoc) { /* SSR/老宿主 */ }
327
+ return 'zh';
328
+ }
329
+ // 宿主词典层(apply 时捕获):宿主若提供逐键翻译则优先,miss 回本地词典。
330
+ var hostDictionary = null;
331
+ function t(key) {
332
+ if (hostDictionary !== null && hostDictionary[key] !== undefined) return String(hostDictionary[key]);
333
+ var dict = LOCALES[currentLang()] || LOCALES.zh;
334
+ return (dict[key] !== undefined ? dict[key] : LOCALES.zh[key]) || key;
335
+ }
336
+ function pickBilingual(obj) {
337
+ if (obj === null || obj === undefined) return '';
338
+ if (typeof obj !== 'object') return String(obj);
339
+ if (currentLang() === 'zh') return obj.zh || obj.en || '';
340
+ return obj.en || obj.zh || '';
341
+ }
342
+ function channelName(channel) {
343
+ var key = 'channel.' + channel.id;
344
+ if (hostDictionary !== null && hostDictionary[key] !== undefined) return String(hostDictionary[key]);
345
+ var dict = LOCALES[currentLang()] || LOCALES.zh;
346
+ if (dict[key] !== undefined) return dict[key];
347
+ if (LOCALES.zh[key] !== undefined) return LOCALES.zh[key];
348
+ return pickBilingual(channel.label) || channel.id;
349
+ }
350
+ function format(key, params) {
351
+ var text = t(key);
352
+ for (var name in params) {
353
+ if (Object.prototype.hasOwnProperty.call(params, name)) {
354
+ text = text.split('{' + name + '}').join(String(params[name]));
355
+ }
356
+ }
357
+ return text;
358
+ }
359
+
360
+ // ── 管理面 HTTP(写请求带客户端标识头;回包零凭据由宿主半保证)──────────
361
+ function getJSON(path) {
362
+ return fetch(API_BASE + path, {
363
+ credentials: 'same-origin',
364
+ headers: { 'x-dsh-im-union-client': '1' },
365
+ }).then(function (response) {
366
+ return response.json().then(function (json) {
367
+ return { status: response.status, json: json };
368
+ });
369
+ });
370
+ }
371
+
372
+ function postJSON(path, body) {
373
+ return fetch(API_BASE + path, {
374
+ method: 'POST',
375
+ credentials: 'same-origin',
376
+ headers: { 'content-type': 'application/json', 'x-dsh-im-union-client': '1' },
377
+ body: JSON.stringify(body || {}),
378
+ }).then(function (response) {
379
+ return response.json().then(function (json) {
380
+ return { status: response.status, json: json };
381
+ });
382
+ });
383
+ }
384
+
385
+ function errorText(result) {
386
+ return (result.json && result.json.error) || 'HTTP ' + result.status;
387
+ }
388
+ function messageText(error) {
389
+ return String(error && error.message ? error.message : error);
390
+ }
391
+ function genAccountId(channelId) {
392
+ return channelId + '-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
393
+ }
394
+
395
+ // ── 样式:单 <style data-plugin> 注入;颜色全部 var(--dsw-alias-*, fallback)
396
+ // 消费链(本地 --diu 别名),暗色/浅色零分支——主题切换交给宿主令牌。──────
397
+ var DIU_CSS = String.raw`
398
+ .diu-page, .diu-modal, .diu-modalRoot {
399
+ --diu-text: var(--dsw-alias-label-primary, #e8eaed);
400
+ --diu-muted: var(--dsw-alias-label-tertiary, #9aa0a6);
401
+ --diu-faint: var(--dsw-alias-label-caption, #81858c);
402
+ --diu-line: var(--dsw-alias-border-l2, rgba(127,127,127,.24));
403
+ --diu-line-strong: var(--dsw-alias-border-l3, rgba(127,127,127,.34));
404
+ --diu-card: var(--dsw-alias-bg-layer-2, rgba(127,127,127,.06));
405
+ --diu-sunken: var(--dsw-alias-bg-layer-1, rgba(127,127,127,.09));
406
+ --diu-hover: var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.1));
407
+ --diu-accent: var(--dsw-alias-state-business-primary, #4b7cff);
408
+ --diu-ok: var(--dsw-alias-state-success-primary, #22c55e);
409
+ --diu-warn: var(--dsw-alias-state-warn-primary, var(--dsw-alias-state-warning-primary, #d97706));
410
+ --diu-bad: var(--dsw-alias-state-error-primary, #d54941);
411
+ --diu-shadow: var(--dsw-shadow-lv3, 0 0 1px rgba(0,0,0,.2), 0 12px 32px rgba(0,0,0,.18));
412
+ --diu-mono: ui-monospace, SFMono-Regular, Consolas, "Courier New", monospace;
413
+ }
414
+ .diu-page { box-sizing: border-box; width: 100%; max-width: 980px; margin: 0 auto; padding: 4px 0 40px; color: var(--diu-text); font-size: 14px; container-type: inline-size; }
415
+ .diu-page *, .diu-modalRoot * { box-sizing: border-box; }
416
+ .diu-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
417
+ .diu-title { margin: 0; font-size: 22px; line-height: 30px; font-weight: 650; letter-spacing: -.3px; }
418
+ .diu-sub { margin: 6px 0 0; color: var(--diu-muted); font-size: 13px; line-height: 20px; }
419
+ .diu-headActions { display: flex; gap: 8px; flex: none; padding-top: 2px; }
420
+ .diu-iconBtn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 30px; padding: 0 10px; border: 1px solid var(--diu-line); border-radius: 8px; background: transparent; color: var(--diu-muted); font-size: 12px; font-family: inherit; cursor: pointer; transition: background .15s ease, color .15s ease, border-color .15s ease; }
421
+ .diu-iconBtn:hover { background: var(--diu-hover); color: var(--diu-text); }
422
+ .diu-iconBtn:focus-visible, .diu-btn:focus-visible, .diu-addBtn:focus-visible, .diu-platformHead:focus-visible { outline: 2px solid var(--diu-accent); outline-offset: 2px; }
423
+ .diu-stats { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
424
+ .diu-stat { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; background: color-mix(in srgb, var(--diu-text) 7%, transparent); color: var(--diu-muted); font-size: 12px; line-height: 20px; }
425
+ .diu-stat strong { color: var(--diu-text); font-weight: 650; }
426
+ .diu-banner { display: flex; align-items: flex-start; gap: 8px; margin-top: 12px; padding: 9px 12px; border-radius: 10px; border: 1px solid transparent; font-size: 13px; line-height: 19px; }
427
+ .diu-banner-err { color: var(--diu-bad); border-color: color-mix(in srgb, var(--diu-bad) 38%, transparent); background: color-mix(in srgb, var(--diu-bad) 10%, transparent); }
428
+ .diu-banner-ok { color: var(--diu-ok); border-color: color-mix(in srgb, var(--diu-ok) 35%, transparent); background: color-mix(in srgb, var(--diu-ok) 9%, transparent); }
429
+ .diu-banner-info { color: var(--diu-accent); border-color: color-mix(in srgb, var(--diu-accent) 35%, transparent); background: color-mix(in srgb, var(--diu-accent) 9%, transparent); }
430
+ .diu-bannerClose { margin-left: auto; flex: none; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border: 0; border-radius: 6px; background: transparent; color: inherit; opacity: .7; cursor: pointer; }
431
+ .diu-bannerClose:hover { opacity: 1; background: color-mix(in srgb, currentColor 12%, transparent); }
432
+ .diu-debug { margin-top: 14px; font-size: 12px; color: var(--diu-muted); }
433
+ .diu-debug summary { cursor: pointer; user-select: none; list-style-position: inside; }
434
+ .diu-debug summary:hover { color: var(--diu-text); }
435
+ .diu-debugBody { margin-top: 8px; padding: 10px 12px; border: 1px solid var(--diu-line); border-radius: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 18px; }
436
+ .diu-debugItem { display: flex; justify-content: space-between; gap: 10px; min-width: 0; }
437
+ .diu-debugItem dt { color: var(--diu-faint); }
438
+ .diu-debugItem dd { margin: 0; color: var(--diu-text); text-align: right; overflow-wrap: anywhere; }
439
+ .diu-debugChannels { grid-column: 1 / -1; margin-top: 4px; padding-top: 8px; border-top: 1px solid var(--diu-line); display: flex; flex-wrap: wrap; gap: 6px; }
440
+ .diu-platforms { display: flex; flex-direction: column; gap: 10px; margin-top: 16px; }
441
+ .diu-platform { border: 1px solid var(--diu-line); border-radius: 12px; background: var(--diu-card); overflow: hidden; transition: border-color .15s ease; }
442
+ .diu-platform.open { border-color: var(--diu-line-strong); }
443
+ .diu-platform.isPlanned { opacity: .62; }
444
+ .diu-platformHead { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto auto; align-items: center; gap: 10px; width: 100%; min-height: 56px; padding: 8px 14px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
445
+ .diu-platform:not(.open):not(.isPlanned) .diu-platformHead:hover { background: var(--diu-hover); }
446
+ .diu-platformCopy { display: flex; align-items: center; gap: 8px; min-width: 0; }
447
+ .diu-platformName { font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
448
+ .diu-count { color: var(--diu-faint); font-size: 12px; white-space: nowrap; }
449
+ .diu-addBtn { display: inline-flex; align-items: center; gap: 4px; min-height: 28px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--diu-line); background: transparent; color: var(--diu-text); font-size: 12px; font-family: inherit; cursor: pointer; transition: border-color .15s ease, color .15s ease, background .15s ease; }
450
+ .diu-addBtn:hover { border-color: color-mix(in srgb, var(--diu-accent) 55%, var(--diu-line)); color: var(--diu-accent); background: color-mix(in srgb, var(--diu-accent) 8%, transparent); }
451
+ .diu-addBtn:disabled { opacity: .45; cursor: not-allowed; }
452
+ .diu-caret { display: inline-flex; color: var(--diu-faint); transition: transform .16s ease; }
453
+ .diu-platform.open .diu-caret { transform: rotate(90deg); }
454
+ .diu-logo { display: grid; place-items: center; border-radius: 9px; flex: none; width: 30px; height: 30px; box-shadow: 0 1px 3px rgba(0,0,0,.12); }
455
+ .diu-logo svg { display: block; }
456
+ .diu-accountList { display: flex; flex-direction: column; gap: 8px; padding: 2px 12px 12px; }
457
+ .diu-accountEmpty { padding: 6px 2px 4px; color: var(--diu-faint); font-size: 12px; }
458
+ .diu-account { border: 1px solid var(--diu-line); border-radius: 10px; padding: 10px 12px; background: color-mix(in srgb, var(--diu-text) 3%, transparent); }
459
+ .diu-accountMain { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
460
+ .diu-accountName { font-weight: 600; font-size: 13px; }
461
+ .diu-accountId { color: var(--diu-faint); font-size: 12px; font-family: var(--diu-mono); }
462
+ .diu-badges { display: flex; gap: 6px; flex-wrap: wrap; }
463
+ .diu-tag { display: inline-flex; align-items: center; gap: 5px; padding: 1px 8px; border-radius: 999px; border: 1px solid transparent; font-size: 11px; line-height: 18px; white-space: nowrap; }
464
+ .diu-tag-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
465
+ .diu-tagOk { color: var(--diu-ok); border-color: color-mix(in srgb, var(--diu-ok) 35%, transparent); background: color-mix(in srgb, var(--diu-ok) 9%, transparent); }
466
+ .diu-tagWarn { color: var(--diu-warn); border-color: color-mix(in srgb, var(--diu-warn) 35%, transparent); background: color-mix(in srgb, var(--diu-warn) 9%, transparent); }
467
+ .diu-tagBad { color: var(--diu-bad); border-color: color-mix(in srgb, var(--diu-bad) 35%, transparent); background: color-mix(in srgb, var(--diu-bad) 9%, transparent); }
468
+ .diu-tagInfo { color: var(--diu-accent); border-color: color-mix(in srgb, var(--diu-accent) 35%, transparent); background: color-mix(in srgb, var(--diu-accent) 9%, transparent); }
469
+ .diu-tagMuted { color: var(--diu-muted); border-color: var(--diu-line); background: transparent; }
470
+ .diu-accountActions { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; }
471
+ .diu-btn { display: inline-flex; align-items: center; gap: 5px; min-height: 28px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--diu-line); background: transparent; color: var(--diu-text); font-size: 12px; font-family: inherit; cursor: pointer; transition: background .15s ease, border-color .15s ease, color .15s ease; }
472
+ .diu-btn:hover { background: var(--diu-hover); }
473
+ .diu-btn:disabled { opacity: .5; cursor: not-allowed; }
474
+ .diu-btnDanger { color: var(--diu-bad); }
475
+ .diu-btnDanger:hover { background: color-mix(in srgb, var(--diu-bad) 10%, transparent); border-color: color-mix(in srgb, var(--diu-bad) 40%, var(--diu-line)); }
476
+ .diu-btnPrimary { border-color: transparent; background: var(--diu-accent); color: #fff; }
477
+ .diu-btnPrimary:hover { background: color-mix(in srgb, var(--diu-accent) 88%, #fff); }
478
+ .diu-checkList { margin: 8px 0 0; padding: 8px 10px; border-radius: 8px; background: color-mix(in srgb, var(--diu-text) 4%, transparent); font-size: 12px; display: flex; flex-direction: column; gap: 4px; }
479
+ .diu-accountPrefs { margin-top: 6px; font-size: 12px; color: var(--diu-muted); line-height: 18px; min-width: 0; overflow-wrap: anywhere; }
480
+ .diu-accountPrefsLabel { color: var(--diu-faint); margin-right: 6px; }
481
+ .diu-checkItem { display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
482
+ .diu-checkName { color: var(--diu-text); font-weight: 550; }
483
+ .diu-checkHint { color: var(--diu-faint); }
484
+ .diu-allow { margin-top: 8px; }
485
+ .diu-allow > summary { cursor: pointer; user-select: none; font-size: 12px; color: var(--diu-muted); list-style-position: inside; }
486
+ .diu-allow > summary:hover { color: var(--diu-text); }
487
+ .diu-allowBody { margin-top: 6px; display: flex; flex-direction: column; gap: 6px; }
488
+ .diu-hint { font-size: 12px; color: var(--diu-faint); line-height: 18px; }
489
+ .diu-textarea { width: 100%; min-height: 56px; padding: 7px 9px; border-radius: 8px; border: 1px solid var(--diu-line); background: var(--diu-sunken); color: inherit; font: 12px/18px var(--diu-mono); resize: vertical; }
490
+ .diu-textarea:focus, .diu-input:focus { outline: none; border-color: color-mix(in srgb, var(--diu-accent) 55%, var(--diu-line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--diu-accent) 16%, transparent); }
491
+ .diu-modalRoot { position: fixed; inset: 0; z-index: 5000; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(0,0,0,.45); }
492
+ .diu-modal { width: 580px; max-width: 100%; max-height: min(86vh, 780px); display: flex; flex-direction: column; border-radius: 14px; border: 1px solid var(--diu-line-strong); background: var(--dsw-alias-bg-layer-1, #232324); box-shadow: var(--diu-shadow); color: var(--diu-text); }
493
+ .diu-modalHead { display: flex; align-items: center; gap: 10px; padding: 14px 16px 10px; }
494
+ .diu-modalTitle { margin: 0; font-size: 16px; font-weight: 650; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
495
+ .diu-closeBtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 8px; background: transparent; color: var(--diu-muted); cursor: pointer; }
496
+ .diu-closeBtn:hover { background: var(--diu-hover); color: var(--diu-text); }
497
+ .diu-modalBody { padding: 2px 16px 8px; overflow: auto; }
498
+ .diu-qrBox { display: flex; gap: 14px; align-items: flex-start; padding: 12px; border: 1px solid var(--diu-line); border-radius: 10px; background: color-mix(in srgb, var(--diu-text) 3%, transparent); margin-bottom: 14px; }
499
+ .diu-qrImg { width: 168px; height: 168px; flex: none; border-radius: 10px; background: #fff; padding: 8px; border: 1px solid var(--diu-line); }
500
+ .diu-qrMeta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
501
+ .diu-qrTitle { font-weight: 600; font-size: 13px; }
502
+ .diu-qrStatus { font-size: 12px; color: var(--diu-muted); line-height: 18px; }
503
+ .diu-qrActions { display: flex; gap: 6px; flex-wrap: wrap; }
504
+ .diu-verifyRow { display: flex; gap: 6px; align-items: center; }
505
+ .diu-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px; }
506
+ .diu-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
507
+ .diu-fieldWide { grid-column: 1 / -1; }
508
+ .diu-label { display: flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 550; color: var(--diu-muted); }
509
+ .diu-req { color: var(--diu-bad); }
510
+ .diu-helpLink { margin-left: auto; font-size: 11px; font-weight: 400; color: var(--diu-accent); text-decoration: none; }
511
+ .diu-helpLink:hover { text-decoration: underline; }
512
+ .diu-input { height: 34px; padding: 0 10px; border-radius: 8px; border: 1px solid var(--diu-line); background: var(--diu-sunken); color: inherit; font-size: 13px; font-family: inherit; min-width: 0; width: 100%; }
513
+ .diu-input::placeholder, .diu-textarea::placeholder { color: var(--diu-faint); }
514
+ .diu-modalNote { margin: 10px 0 0; font-size: 12px; color: var(--diu-faint); line-height: 18px; }
515
+ .diu-sessionBlock { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--diu-line); }
516
+ .diu-sessionTitle { font-weight: 600; font-size: 13px; margin-bottom: 10px; }
517
+ select.diu-input { appearance: auto; }
518
+ .diu-modalFoot { display: flex; align-items: center; gap: 8px; padding: 12px 16px 14px; border-top: 1px solid var(--diu-line); margin-top: 8px; }
519
+ .diu-footMsg { flex: 1; min-width: 0; font-size: 12px; color: var(--diu-bad); line-height: 18px; overflow-wrap: anywhere; }
520
+ .diu-footSpacer { flex: 1; }
521
+ .diu-overlayFallback { position: fixed; right: 16px; bottom: 16px; z-index: 9999; width: 380px; max-height: 62vh; overflow: auto; border: 1px solid var(--diu-line); border-radius: 12px; background: var(--dsw-alias-bg-layer-1, #232324); box-shadow: var(--diu-shadow); padding: 12px; }
522
+ .diu-whitelist-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,#ddd);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#222);font-size:12px;font-family:monospace;resize:vertical}
523
+ .diu-relay-dialog{padding:8px 12px;border-top:1px solid var(--dsw-alias-border-l2,#eee)}
524
+ .diu-relay-list{margin-bottom:8px}
525
+ .diu-relay-row{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px}
526
+ .diu-relay-sid{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#666)}
527
+ .diu-relay-form{display:flex;gap:6px;flex-wrap:wrap}
528
+ .diu-relay-form input{flex:1;min-width:120px;padding:5px 8px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,#ddd);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#222);font-size:12px}
529
+ .diu-btn-danger{background:var(--dsw-alias-state-error,#d32)}
530
+ @container (max-width: 640px) {
531
+ .diu-fields { grid-template-columns: 1fr; }
532
+ .diu-platformHead { grid-template-columns: auto minmax(0, 1fr) auto; }
533
+ .diu-platformHead .diu-count, .diu-platformHead .diu-caret { display: none; }
534
+ .diu-debugBody { grid-template-columns: 1fr; }
535
+ .diu-qrBox { flex-direction: column; }
536
+ }
537
+ @media (prefers-reduced-motion: reduce) {
538
+ .diu-caret, .diu-btn, .diu-addBtn, .diu-iconBtn { transition: none; }
539
+ }
540
+ `;
541
+ var styleInstalled = false;
542
+ function installStyles() {
543
+ if (styleInstalled || typeof document === 'undefined') return;
544
+ if (document.querySelector('style[data-plugin-css="' + STYLE_ID + '"]')) { styleInstalled = true; return; }
545
+ var style = document.createElement('style');
546
+ style.dataset.plugin = 'dsh-im-union';
547
+ style.dataset.pluginCss = STYLE_ID;
548
+ style.textContent = DIU_CSS;
549
+ document.head.appendChild(style);
550
+ styleInstalled = true;
551
+ }
552
+
553
+ // ── 品牌图标:净室简化图形(色块 chip + 单色/多色 glyph),非官方素材─────
554
+ var CHIP_STYLES = {
555
+ weixin: { background: '#07C160' },
556
+ feishu: { background: '#ffffff', bordered: true },
557
+ dingtalk: { background: '#1296DB' },
558
+ wecom: { background: '#ffffff', bordered: true },
559
+ telegram: { background: '#229ED9' },
560
+ qq: { background: '#12B7F5' },
561
+ slack: { background: '#ffffff', bordered: true },
562
+ discord: { background: '#5865F2' },
563
+ whatsapp: { background: '#25D366' },
564
+ matrix: { background: '#0F1115' },
565
+ imessage: { background: 'linear-gradient(180deg,#5BF675 0%,#28D944 55%,#0FBD2C 100%)' },
566
+ 'wecom-app': { background: '#2B6DEB' },
567
+ email: { background: '#5B7CFA' },
568
+ 'dingtalk-notify': { background: '#1296DB' },
569
+ 'qq-notify': { background: '#12B7F5' },
570
+ };
571
+
572
+ var GLYPHS = {
573
+ weixin: function () {
574
+ return [
575
+ h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M10.4 3.2c-4.7 0-8.4 3-8.4 6.8 0 2.1 1.1 3.9 2.9 5.2l-.8 3.4 3.5-1.9c.9.3 1.8.4 2.8.4h.5a6.7 6.7 0 0 1-.3-2c0-3.7 3.4-6.7 7.7-6.7h.4c-.8-3-4.2-5.2-8.3-5.2zM7.7 8.2a1.05 1.05 0 1 1 0 2.1 1.05 1.05 0 0 1 0-2.1zm5.4 0a1.05 1.05 0 1 1 0 2.1 1.05 1.05 0 0 1 0-2.1z' }),
576
+ h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M22 15.4c0-3-3-5.4-6.6-5.4s-6.6 2.4-6.6 5.4 3 5.4 6.6 5.4c.8 0 1.5-.1 2.2-.3l2.8 1.5-.7-2.6c1.4-1 2.3-2.4 2.3-4zm-8.9-1.4a.95.95 0 1 1 0 1.9.95.95 0 0 1 0-1.9zm4.6 0a.95.95 0 1 1 0 1.9.95.95 0 0 1 0-1.9z' }),
577
+ ];
578
+ },
579
+ feishu: function () {
580
+ return [
581
+ h('path', { fill: '#00D6B9', d: 'M13.9 3.6 4.2 12.4c-.5.4-.2 1.2.4 1.2l6.1.2 4.9-8.7c.3-.5-.1-1.1-.6-1.1h-.5c-.2 0-.4.1-.6.2z' }),
582
+ h('path', { fill: '#3370FF', d: 'M10.1 20.4l9.7-8.8c.5-.4.2-1.2-.4-1.2l-6.1-.2-4.9 8.7c-.3.5.1 1.1.6 1.1h.5c.2 0 .4-.1.6-.2z' }),
583
+ h('circle', { cx: '16.6', cy: '16.4', r: '2.5', fill: '#00D6B9' }),
584
+ ];
585
+ },
586
+ dingtalk: function () {
587
+ return [
588
+ h('path', { fill: '#fff', d: 'M20.8 4.3 3.6 11c-.7.3-.6 1.3.1 1.5l4.4 1.1 1.6 5c.2.6 1 .8 1.4.3l2.2-2.7 4.3 2.5c.5.3 1.1 0 1.3-.5l3-13c.1-.7-.5-1.2-1.1-.9z' }),
589
+ ];
590
+ },
591
+ wecom: function () {
592
+ return [
593
+ h('circle', { cx: '12', cy: '5.8', r: '2.6', fill: '#FB5252' }),
594
+ h('circle', { cx: '18.2', cy: '12', r: '2.6', fill: '#FFB43A' }),
595
+ h('circle', { cx: '12', cy: '18.2', r: '2.6', fill: '#34C759' }),
596
+ h('circle', { cx: '5.8', cy: '12', r: '2.6', fill: '#2B7FFF' }),
597
+ ];
598
+ },
599
+ telegram: function () {
600
+ return [
601
+ h('path', { fill: '#fff', d: 'M20.7 4.2 3.5 10.9c-.8.3-.8 1.4 0 1.7l4.3 1.4 1.6 5c.2.7 1.1.9 1.6.3l2.2-2.5 4.2 2.4c.5.3 1.2 0 1.3-.6l2.8-13.2c.2-.8-.6-1.5-1.3-1.2z' }),
602
+ h('path', { fill: '#229ED9', d: 'M8.6 13.6l8.9-5.6c.3-.2.6.2.3.4l-7.2 6.7-.2 2.6-1.8-4.1z' }),
603
+ ];
604
+ },
605
+ qq: function () {
606
+ return [
607
+ h('path', { fill: '#fff', d: 'M12 2.6c-3.4 0-5.6 2.5-5.6 5.8 0 .9-.5 2-1 3.1-.6 1.4-1.3 3-1.3 4.5 0 1.5.8 2.5 1.8 2.5.6 0 1.1-.4 1.5-1 .3 1.4 1 2.8 2 3.8-1 .2-2 .8-2 1.5 0 .9 1.7 1.4 3.6 1.4h2c1.9 0 3.6-.5 3.6-1.4 0-.7-1-1.3-2-1.5 1-1 1.7-2.4 2-3.8.4.6.9 1 1.5 1 1 0 1.8-1 1.8-2.5 0-1.5-.7-3.1-1.3-4.5-.5-1.1-1-2.2-1-3.1 0-3.3-2.2-5.8-5.6-5.8z' }),
608
+ h('ellipse', { cx: '12', cy: '14.8', rx: '3.6', ry: '4.8', fill: '#12B7F5' }),
609
+ h('circle', { cx: '10.1', cy: '7.8', r: '1', fill: '#12B7F5' }),
610
+ h('circle', { cx: '13.9', cy: '7.8', r: '1', fill: '#12B7F5' }),
611
+ ];
612
+ },
613
+ slack: function () {
614
+ return [
615
+ h('rect', { x: '9.3', y: '3', width: '2.8', height: '8.4', rx: '1.4', fill: '#36C5F0' }),
616
+ h('rect', { x: '12.7', y: '9.3', width: '8.4', height: '2.8', rx: '1.4', fill: '#2EB67D' }),
617
+ h('rect', { x: '11.9', y: '12.7', width: '2.8', height: '8.4', rx: '1.4', fill: '#ECB22E' }),
618
+ h('rect', { x: '2.9', y: '11.9', width: '8.4', height: '2.8', rx: '1.4', fill: '#E01E5A' }),
619
+ ];
620
+ },
621
+ discord: function () {
622
+ return [
623
+ h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M19.3 5.6A16.5 16.5 0 0 0 15.6 4l-.5 1a13.4 13.4 0 0 0-6.2 0L8.4 4a16.5 16.5 0 0 0-3.7 1.6C2.3 9.3 1.6 12.9 2 16.4A16.9 16.9 0 0 0 7 19l1-1.6c-.6-.2-1.1-.5-1.6-.8l.4-.3a12.3 12.3 0 0 0 10.4 0l.4.3c-.5.3-1 .6-1.6.8l1 1.6a16.9 16.9 0 0 0 5-2.6c.4-4.1-.7-7.7-2.7-10.8zM9.1 14.2c-1 0-1.8-.9-1.8-2s.8-2 1.8-2 1.8.9 1.8 2-.8 2-1.8 2zm5.8 0c-1 0-1.8-.9-1.8-2s.8-2 1.8-2 1.8.9 1.8 2-.8 2-1.8 2z' }),
624
+ ];
625
+ },
626
+ whatsapp: function () {
627
+ return [
628
+ h('path', { fill: '#fff', d: 'M12 2.8a9.2 9.2 0 0 0-7.9 13.9L2.8 21l4.5-1.2A9.2 9.2 0 1 0 12 2.8z' }),
629
+ h('path', { fill: '#25D366', d: 'M9 8.1c.3-.3.8-.3 1.1 0l1.2 1.6c.2.3.2.7 0 1l-.8 1a8.6 8.6 0 0 0 3.8 3.8l1-.8c.3-.2.7-.2 1 0l1.6 1.2c.3.3.3.8 0 1.1-1.7 1.7-4.5 1-7.2-1.7S7.3 9.8 9 8.1z' }),
630
+ ];
631
+ },
632
+ matrix: function () {
633
+ return [
634
+ h('path', { d: 'M8.6 4.5H5.4v15h3.2', stroke: '#fff', strokeWidth: 2, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' }),
635
+ h('path', { d: 'M15.4 4.5h3.2v15h-3.2', stroke: '#fff', strokeWidth: 2, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' }),
636
+ ];
637
+ },
638
+ imessage: function () {
639
+ return [
640
+ h('path', { fill: '#fff', d: 'M12 3.6c-4.8 0-8.6 3-8.6 6.7 0 2.1 1.2 3.9 3.1 5.2-.1 1.1-.6 2.4-1.7 3.4 2-.1 3.6-.9 4.7-1.8.8.2 1.6.3 2.5.3 4.8 0 8.6-3 8.6-6.7S16.8 3.6 12 3.6z' }),
641
+ ];
642
+ },
643
+ 'wecom-app': function () {
644
+ return [
645
+ h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M9.4 6.6V6c0-.7.6-1.3 1.3-1.3h2.6c.7 0 1.3.6 1.3 1.3v.6h2.8c1 0 1.9.9 1.9 1.9v7.6c0 1-.9 1.9-1.9 1.9H6.6c-1 0-1.9-.9-1.9-1.9V8.5c0-1 .9-1.9 1.9-1.9h2.8zm1.4 0h2.4V6c0-.1-.1-.2-.2-.2h-2c-.1 0-.2.1-.2.2v.6zm-3.6 4.2h9.6v1.5H7.2v-1.5z' }),
646
+ ];
647
+ },
648
+ email: function () {
649
+ return [
650
+ h('path', { fill: '#fff', d: 'M5 6.5h14a1.8 1.8 0 0 1 1.8 1.8v7.4a1.8 1.8 0 0 1-1.8 1.8H5a1.8 1.8 0 0 1-1.8-1.8V8.3A1.8 1.8 0 0 1 5 6.5z' }),
651
+ h('path', { d: 'm4.6 8.4 7.4 5 7.4-5', stroke: '#5B7CFA', strokeWidth: 1.6, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' }),
652
+ ];
653
+ },
654
+ 'dingtalk-notify': function () {
655
+ return [
656
+ h('path', { fill: '#fff', d: 'M12 3.2a5.4 5.4 0 0 0-5.4 5.4c0 3.1-.9 4.4-1.7 5.3-.4.4-.1 1.2.5 1.2h13.2c.6 0 .9-.8.5-1.2-.8-.9-1.7-2.2-1.7-5.3A5.4 5.4 0 0 0 12 3.2zm-2.2 13.6a2.2 2.2 0 0 0 4.4 0h-4.4z' }),
657
+ ];
658
+ },
659
+ };
660
+ GLYPHS['qq-notify'] = GLYPHS['dingtalk-notify'];
661
+
662
+ function Logo(props) {
663
+ var channelId = props.channel;
664
+ var size = props.size || 30;
665
+ var chip = CHIP_STYLES[channelId] || { background: 'var(--diu-hover)' };
666
+ var style = { width: size, height: size, borderRadius: Math.round(size * 0.3), background: chip.background };
667
+ if (chip.bordered) style.boxShadow = 'inset 0 0 0 1px var(--diu-line)';
668
+ var draw = GLYPHS[channelId];
669
+ var glyphSize = Math.round(size * 0.62);
670
+ return h('span', { className: 'diu-logo', style: style, 'aria-hidden': 'true' },
671
+ draw
672
+ ? h('svg', { viewBox: '0 0 24 24', width: glyphSize, height: glyphSize, focusable: 'false' }, draw())
673
+ : h('span', { style: { color: '#fff', fontSize: Math.round(size * 0.42), fontWeight: 700 } }, channelId.slice(0, 1).toUpperCase())
674
+ );
675
+ }
676
+
677
+ // ── 小图标(stroke 类,currentColor)───────────────────────────────────
678
+ function IconRefresh(props) {
679
+ return h('svg', { viewBox: '0 0 24 24', width: props.size || 14, height: props.size || 14, 'aria-hidden': 'true', focusable: 'false' },
680
+ h('path', { d: 'M20 12a8 8 0 1 1-2.9-6.2', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
681
+ h('path', { d: 'M20.5 3.5v5h-5', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }),
682
+ );
683
+ }
684
+ function IconChevron(props) {
685
+ return h('svg', { viewBox: '0 0 24 24', width: props.size || 14, height: props.size || 14, 'aria-hidden': 'true', focusable: 'false' },
686
+ h('path', { d: 'm9 6 6 6-6 6', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }),
687
+ );
688
+ }
689
+ function IconPlus(props) {
690
+ return h('svg', { viewBox: '0 0 24 24', width: props.size || 12, height: props.size || 12, 'aria-hidden': 'true', focusable: 'false' },
691
+ h('path', { d: 'M12 5v14M5 12h14', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
692
+ );
693
+ }
694
+ function IconClose(props) {
695
+ return h('svg', { viewBox: '0 0 24 24', width: props.size || 14, height: props.size || 14, 'aria-hidden': 'true', focusable: 'false' },
696
+ h('path', { d: 'M6 6l12 12M18 6 6 18', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
697
+ );
698
+ }
699
+ function IconGitHub(props) {
700
+ return h('svg', { viewBox: '0 0 24 24', width: props.size || 14, height: props.size || 14, 'aria-hidden': 'true', focusable: 'false' },
701
+ h('path', { fill: 'currentColor', d: 'M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.87-.01-1.7-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.5-1.11-1.5-.91-.64.07-.63.07-.63 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.36 1.12 2.94.86.09-.67.35-1.12.63-1.38-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.7 0 0 .84-.28 2.75 1.05a9.36 9.36 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.4.2 2.44.1 2.7.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.8-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.6.69.49A10.02 10.02 0 0 0 22 12.25C22 6.58 17.52 2 12 2z' }),
702
+ );
703
+ }
704
+
705
+ function Tag(props) {
706
+ var cls = 'diu-tag diu-tag' + (props.tone || 'Muted');
707
+ return h('span', { className: cls },
708
+ props.dot !== false ? h('span', { className: 'diu-tag-dot' }) : null,
709
+ props.children,
710
+ );
711
+ }
712
+
713
+ // 弹窗 portal:宿主提供 react-dom 则挂 body(脱离设置对话框的层叠上下文),
714
+ // 否则退化为面板内 fixed 层——两条路都保 Esc/背板点击可关。
715
+ function Portal(props) {
716
+ if (ReactDOM && typeof ReactDOM.createPortal === 'function' && typeof document !== 'undefined') {
717
+ return ReactDOM.createPortal(props.children, document.body);
718
+ }
719
+ return h('div', { style: { position: 'fixed', inset: 0, zIndex: 5000 } }, props.children);
720
+ }
721
+
722
+ // ── relay 抄送绑定区(Phase 8):notifyOnly 渠道账号把会话绑定到推送目标 ──
723
+ function RelayDialog(props) {
724
+ var sidHook = useState('');
725
+ var sid = sidHook[0];
726
+ var setSid = sidHook[1];
727
+ var tidHook = useState('');
728
+ var tid = tidHook[0];
729
+ var setTid = tidHook[1];
730
+ var listHook = useState([]);
731
+ var bindings = listHook[0];
732
+ var setBindings = listHook[1];
733
+ useEffect(function () {
734
+ getJSON('/relay-bindings').then(function (r) {
735
+ if (r.json && Array.isArray(r.json.bindings)) setBindings(r.json.bindings.filter(function (b) { return b.notifyAccountId === props.accountId; }));
736
+ }).catch(function () {});
737
+ }, [props.accountId]);
738
+ function doBind() {
739
+ if (!sid.trim()) return;
740
+ postJSON('/relay-bindings/bind', { sessionId: sid.trim(), notifyAccountId: props.accountId, targetId: tid.trim() || undefined }).then(function () {
741
+ setBindings(function (prev) { return prev.concat([{ sessionId: sid.trim(), notifyAccountId: props.accountId, boundAt: Date.now() }]); });
742
+ setSid('');
743
+ }).catch(function () {});
744
+ }
745
+ function doUnbind(sessionId) {
746
+ postJSON('/relay-bindings/unbind', { sessionId: sessionId }).then(function () {
747
+ setBindings(function (prev) { return prev.filter(function (b) { return b.sessionId !== sessionId; }); });
748
+ }).catch(function () {});
749
+ }
750
+ return h('div', { className: 'diu-relay-dialog' },
751
+ h('h4', null, t('relay.title') + ' \u2014 ' + props.accountId),
752
+ bindings.length > 0 ? h('div', { className: 'diu-relay-list' },
753
+ bindings.map(function (b) {
754
+ return h('div', { className: 'diu-relay-row', key: b.sessionId },
755
+ h('span', { className: 'diu-relay-sid' }, b.sessionId.length > 30 ? b.sessionId.slice(0, 30) + '…' : b.sessionId),
756
+ h('button', { className: 'diu-btn diu-btn-sm diu-btn-danger', onClick: function () { doUnbind(b.sessionId); } }, t('relay.unbind')),
757
+ );
758
+ })
759
+ ) : h('p', { className: 'diu-hint' }, t('relay.empty')),
760
+ h('div', { className: 'diu-relay-form' },
761
+ h('input', { placeholder: t('relay.sessionId'), value: sid, onInput: function (e) { setSid(e.target.value); } }),
762
+ h('input', { placeholder: t('relay.targetId'), value: tid, onInput: function (e) { setTid(e.target.value); } }),
763
+ h('button', { className: 'diu-btn diu-btn-primary', onClick: doBind }, t('relay.doBind')),
764
+ ),
765
+ );
766
+ }
767
+
768
+ // ── 账号行:名称/内部键 + 状态徽章 + 操作(检查/编辑/暂停/两步删除)──────
769
+ function AccountRow(props) {
770
+ var row = props.row;
771
+ var channel = props.channel;
772
+ var busy = props.busy;
773
+ var checks = props.checks || [];
774
+ var checkTone = { passed: 'Ok', failed: 'Bad', unverified: 'Muted' };
775
+
776
+ var pendingRemoveHook = useState(false);
777
+ var pendingRemove = pendingRemoveHook[0];
778
+ var setPendingRemove = pendingRemoveHook[1];
779
+ var allowHook = useState(null);
780
+ var allowDraft = allowHook[0];
781
+ var setAllowDraft = allowHook[1];
782
+ var relayHook = useState(false);
783
+ var relayOpen = relayHook[0];
784
+ var setRelayOpen = relayHook[1];
785
+
786
+ var missingSecrets = [];
787
+ (row.secretFields || []).forEach(function (sf) {
788
+ if (!sf.configured) missingSecrets.push(sf.key);
789
+ });
790
+ var badge = row.paused
791
+ ? h(Tag, { tone: 'Muted' }, t('accounts.state.paused'))
792
+ : row.sessionExpired === true
793
+ ? h(Tag, { tone: 'Bad' }, t('accounts.state.expired'))
794
+ : row.lastSendOk === false
795
+ ? h(Tag, { tone: 'Bad' }, t('accounts.send.failed'))
796
+ : row.connected
797
+ ? h(Tag, { tone: 'Ok' }, t('accounts.state.connected'))
798
+ : h(Tag, { tone: 'Warn' }, t('accounts.state.disconnected'));
799
+ var allowValue = allowDraft !== null ? allowDraft : (row.dmAllowlist || []).join('\n');
800
+ // 会话偏好摘要(可发现性):有值才显示,避免「新设置藏在弹窗里」。
801
+ var session0 = row.session || {};
802
+ var prefBits = [];
803
+ if (session0.cwd) {
804
+ var cwdParts = String(session0.cwd).split(/[\\/]/);
805
+ prefBits.push(cwdParts[cwdParts.length - 1] || session0.cwd);
806
+ }
807
+ if (session0.agentPreset) prefBits.push(session0.agentPreset);
808
+ if (session0.model) prefBits.push((session0.provider ? session0.provider + '/' : '') + session0.model + (session0.reasoningEffort ? '@' + session0.reasoningEffort : ''));
809
+ if (session0.permission) prefBits.push(t('prefs.permission') + ':' + session0.permission);
810
+ var exec0 = row.commandExec || {};
811
+ if (exec0.dm === false) prefBits.push(t('exec.dm') + ':' + t('exec.off'));
812
+ if (exec0.group === false) prefBits.push(t('exec.group') + ':' + t('exec.off'));
813
+
814
+ var actions = [
815
+ h('button', { key: 'check', className: 'diu-btn', disabled: busy, onClick: function () { props.onCheck(row.accountId); } },
816
+ busy ? t('accounts.checking') : t('accounts.check')),
817
+ h('button', { key: 'edit', className: 'diu-btn', disabled: busy, onClick: function () { props.onEdit(row); } }, t('accounts.edit')),
818
+ h('button', {
819
+ key: 'pause', className: 'diu-btn', disabled: busy,
820
+ onClick: function () { props.onPause(row.accountId, !row.paused); },
821
+ }, row.paused ? t('accounts.resume') : t('accounts.pause')),
822
+ ];
823
+ if (pendingRemove) {
824
+ actions.push(
825
+ h('button', {
826
+ key: 'removeYes', className: 'diu-btn diu-btnDanger', disabled: busy,
827
+ onClick: function () { setPendingRemove(false); props.onRemove(row.accountId); },
828
+ }, t('accounts.remove.confirm')),
829
+ h('button', { key: 'removeNo', className: 'diu-btn', disabled: busy, onClick: function () { setPendingRemove(false); } }, t('modal.cancel')),
830
+ );
831
+ } else {
832
+ actions.push(
833
+ h('button', { key: 'remove', className: 'diu-btn diu-btnDanger', disabled: busy, onClick: function () { setPendingRemove(true); } }, t('accounts.remove')),
834
+ );
835
+ }
836
+
837
+ var checkRows = null;
838
+ if (checks.length > 0) {
839
+ checkRows = h('div', { className: 'diu-checkList' },
840
+ h('span', { className: 'diu-hint' }, t('check.title')),
841
+ checks.map(function (c, i) {
842
+ return h('div', { key: (c.name || 'check') + i, className: 'diu-checkItem' },
843
+ h('span', { className: 'diu-checkName' }, c.name || '-'),
844
+ h(Tag, { tone: checkTone[c.status] || 'Muted' }, t('check.' + c.status) || c.status),
845
+ c.hint ? h('span', { className: 'diu-checkHint' }, c.hint) : null,
846
+ );
847
+ }),
848
+ );
849
+ }
850
+
851
+ var allowBlock = channel.notifyOnly === true ? null : h(
852
+ 'details',
853
+ { className: 'diu-allow' },
854
+ h('summary', null, t('accounts.allowlist') + '(' + (row.dmAllowlist || []).length + ')'),
855
+ h('div', { className: 'diu-allowBody' },
856
+ h('div', { className: 'diu-hint' }, t('accounts.allowlist.hint')),
857
+ h('textarea', {
858
+ className: 'diu-textarea', rows: 2, value: allowValue,
859
+ onChange: function (event) { setAllowDraft(event.target.value); },
860
+ }),
861
+ h('div', null,
862
+ h('button', {
863
+ className: 'diu-btn', disabled: busy,
864
+ onClick: function () {
865
+ var ids = String(allowValue || '').split('\n').map(function (v) { return v.trim(); }).filter(function (v) { return v !== ''; });
866
+ props.onSaveAllowlist(row.accountId, ids);
867
+ },
868
+ }, t('accounts.allowlist.save')),
869
+ ),
870
+ ),
871
+ );
872
+
873
+ return h('div', { className: 'diu-account' },
874
+ h('div', { className: 'diu-accountMain' },
875
+ h('span', { className: 'diu-accountName' }, row.alias || row.accountId),
876
+ row.alias ? h('span', { className: 'diu-accountId' }, row.accountId) : null,
877
+ h('span', { className: 'diu-badges' },
878
+ badge,
879
+ row.privateAccess === 'all' ? h(Tag, { tone: 'Info', dot: false }, t('access.all.tag')) : null,
880
+ missingSecrets.length > 0
881
+ ? h(Tag, { tone: 'Bad', dot: false }, format('accounts.secret.missing', { keys: missingSecrets.join(', ') }))
882
+ : null,
883
+ ),
884
+ h('span', { className: 'diu-accountActions' }, actions),
885
+ ),
886
+ checkRows,
887
+ prefBits.length > 0
888
+ ? h('div', { className: 'diu-accountPrefs' },
889
+ h('span', { className: 'diu-accountPrefsLabel' }, t('accounts.prefs')),
890
+ prefBits.join(' · '),
891
+ )
892
+ : null,
893
+ allowBlock,
894
+ channel.notifyOnly === true
895
+ ? h('div', { className: 'diu-relay' },
896
+ h('button', { className: 'diu-btn diu-btn-sm', onClick: function () { setRelayOpen(!relayOpen); } }, t('relay.bind')),
897
+ relayOpen ? h(RelayDialog, { accountId: row.accountId }) : null,
898
+ )
899
+ : null,
900
+ );
901
+ }
902
+
903
+ // ── 渠道手风琴卡:头(logo/名称/标签/账号数/添加)+ 展开账号列表 ─────────
904
+ function PlatformCard(props) {
905
+ var channel = props.channel;
906
+ var accounts = props.accounts || [];
907
+ var open = props.open === true;
908
+ var planned = channel.implemented !== true;
909
+ var cls = 'diu-platform' + (open ? ' open' : '') + (planned ? ' isPlanned' : '');
910
+ var tags = [];
911
+ if (planned) tags.push(h(Tag, { key: 'planned', tone: 'Muted', dot: false }, t('channel.tag.planned')));
912
+ if (channel.notifyOnly === true) tags.push(h(Tag, { key: 'notify', tone: 'Info', dot: false }, t('channel.tag.notify')));
913
+ if (channel.hostRequirement === 'macos') tags.push(h(Tag, { key: 'macos', tone: 'Warn', dot: false }, t('channel.tag.macos')));
914
+
915
+ return h('section', { className: cls },
916
+ h('button', {
917
+ className: 'diu-platformHead', 'aria-expanded': open ? 'true' : 'false',
918
+ onClick: function () { props.onToggle(channel.id); },
919
+ },
920
+ h(Logo, { channel: channel.id, size: 30 }),
921
+ h('span', { className: 'diu-platformCopy' },
922
+ h('span', { className: 'diu-platformName' }, channelName(channel)),
923
+ tags,
924
+ ),
925
+ h('span', { className: 'diu-count' }, format('channel.count', { n: accounts.length })),
926
+ h('span', {
927
+ className: 'diu-addBtn', role: 'button', 'aria-disabled': planned ? 'true' : 'false',
928
+ onClick: function (event) {
929
+ event.stopPropagation();
930
+ if (!planned) props.onAdd(channel);
931
+ },
932
+ },
933
+ h(IconPlus, null), t('accounts.add'),
934
+ ),
935
+ h('span', { className: 'diu-caret' }, h(IconChevron, null)),
936
+ ),
937
+ open && !planned
938
+ ? accounts.length === 0
939
+ ? h('div', { className: 'diu-accountList' }, h('div', { className: 'diu-accountEmpty' }, t('channel.emptyHint')))
940
+ : h('div', { className: 'diu-accountList' }, accounts.map(function (row) {
941
+ return h(AccountRow, {
942
+ key: row.accountId, row: row, channel: channel, busy: props.busy,
943
+ checks: props.checks[row.accountId] || [],
944
+ onCheck: props.onCheck, onEdit: props.onEdit, onPause: props.onPause,
945
+ onRemove: props.onRemove, onSaveAllowlist: props.onSaveAllowlist,
946
+ });
947
+ }))
948
+ : null,
949
+ );
950
+ }
951
+
952
+ // ── 添加/编辑弹窗:微信=扫码流(先建档后取码);其余=meta.fields 双列表单 ──
953
+ function AccountModal(props) {
954
+ var channel = props.channel;
955
+ var account = props.account || null;
956
+ var editing = account !== null;
957
+ var isWeixin = channel.id === 'weixin';
958
+ var fields = channel.fields || [];
959
+
960
+ var aliasHook = useState(editing ? account.alias || '' : '');
961
+ var alias = aliasHook[0];
962
+ var setAlias = aliasHook[1];
963
+ var valuesHook = useState(function () {
964
+ var initial = {};
965
+ for (var i = 0; i < fields.length; i += 1) {
966
+ var f = fields[i];
967
+ if (!f.secret && editing && account.config && typeof account.config[f.key] === 'string') {
968
+ initial[f.key] = account.config[f.key];
969
+ } else {
970
+ initial[f.key] = '';
971
+ }
972
+ }
973
+ return initial;
974
+ });
975
+ var values = valuesHook[0];
976
+ var setValues = valuesHook[1];
977
+ var qrHook = useState(null);
978
+ var qr = qrHook[0];
979
+ var setQr = qrHook[1];
980
+ var busyHook = useState(false);
981
+ var busy = busyHook[0];
982
+ var setBusy = busyHook[1];
983
+ var errHook = useState('');
984
+ var err = errHook[0];
985
+ var setErr = errHook[1];
986
+ // 会话偏好(cwd/预设/模型/推理档/权限)+ 私聊准入 + 命令执行开关;模型编码 provider::model。
987
+ var prefsHook = useState(function () {
988
+ var s = (editing && account.session) || {};
989
+ return {
990
+ cwd: typeof s.cwd === 'string' ? s.cwd : '',
991
+ agentPreset: typeof s.agentPreset === 'string' ? s.agentPreset : '',
992
+ modelKey: typeof s.model === 'string' && s.model !== '' ? (typeof s.provider === 'string' ? s.provider : '') + '::' + s.model : '',
993
+ reasoningEffort: typeof s.reasoningEffort === 'string' ? s.reasoningEffort : '',
994
+ permission: typeof s.permission === 'string' ? s.permission : '',
995
+ };
996
+ });
997
+ var prefs = prefsHook[0];
998
+ var setPrefs = prefsHook[1];
999
+ // 命令执行开关(缺键=开;插值即时生效,不依赖会话后端)。
1000
+ var execHook = useState(function () {
1001
+ var e = (editing && account.commandExec) || {};
1002
+ return { dm: e.dm !== false, group: e.group !== false };
1003
+ });
1004
+ var exec = execHook[0];
1005
+ var setExec = execHook[1];
1006
+ var accessHook = useState(editing && account.privateAccess === 'all' ? 'all' : 'approved');
1007
+ var privateAccess = accessHook[0];
1008
+ var setPrivateAccess = accessHook[1];
1009
+ var catalogHook = useState(null);
1010
+ var catalog = catalogHook[0];
1011
+ var setCatalog = catalogHook[1];
1012
+
1013
+ // Esc 关闭(capture 注册避免被宿主对话框吞掉)。
1014
+ useEffect(function () {
1015
+ var handler = function (event) {
1016
+ if (event.key === 'Escape') { event.stopPropagation(); props.onClose(); }
1017
+ };
1018
+ document.addEventListener('keydown', handler, true);
1019
+ return function () { document.removeEventListener('keydown', handler, true); };
1020
+ }, [props.onClose]);
1021
+
1022
+ // 偏好目录一次拉取(工作区 cwd 聚合/模型目录/预设清单);缺位=偏好区降级提示。
1023
+ useEffect(function () {
1024
+ var alive = true;
1025
+ getJSON('/session-catalog').then(function (result) {
1026
+ if (alive && result.status === 200 && result.json && result.json.ok) setCatalog(result.json);
1027
+ }).catch(function () { /* 目录不可用:模型/预设下拉保持空 */ });
1028
+ return function () { alive = false; };
1029
+ }, []);
1030
+
1031
+ // 字段值归位:secret → secrets(留空=保持),plain → record.config;
1032
+ // 会话偏好进 record.session,准入档位进 record.privateAccess。
1033
+ function collect() {
1034
+ var config = {};
1035
+ var secrets = {};
1036
+ for (var i = 0; i < fields.length; i += 1) {
1037
+ var f = fields[i];
1038
+ var v = String(values[f.key] || '').trim();
1039
+ if (v === '') continue;
1040
+ if (f.secret) secrets[f.key] = v;
1041
+ else config[f.key] = v;
1042
+ }
1043
+ var session = {};
1044
+ var cwd = String(prefs.cwd || '').trim();
1045
+ var presetId = String(prefs.agentPreset || '').trim();
1046
+ if (cwd !== '') session.cwd = cwd;
1047
+ if (presetId !== '') session.agentPreset = presetId;
1048
+ if (prefs.modelKey !== '') {
1049
+ var split = prefs.modelKey.indexOf('::');
1050
+ var provider = split > 0 ? prefs.modelKey.slice(0, split) : '';
1051
+ var model = prefs.modelKey.slice(split + 2);
1052
+ if (provider !== '') session.provider = provider;
1053
+ if (model !== '') session.model = model;
1054
+ var effort = String(prefs.reasoningEffort || '').trim();
1055
+ if (effort !== '') session.reasoningEffort = effort;
1056
+ }
1057
+ var permission = String(prefs.permission || '').trim();
1058
+ if (permission !== '') session.permission = permission;
1059
+ return {
1060
+ record: {
1061
+ accountId: editing ? account.accountId : genAccountId(channel.id),
1062
+ channel: channel.id,
1063
+ alias: String(alias || '').trim(),
1064
+ config: config,
1065
+ privateAccess: privateAccess === 'all' ? 'all' : 'approved',
1066
+ commandExec: { dm: exec.dm !== false, group: exec.group !== false },
1067
+ session: session,
1068
+ },
1069
+ secrets: secrets,
1070
+ };
1071
+ }
1072
+
1073
+ function requiredMissing() {
1074
+ for (var i = 0; i < fields.length; i += 1) {
1075
+ var f = fields[i];
1076
+ if (!f.required) continue;
1077
+ if (editing && f.secret) continue; // 编辑时空=保持现有凭据
1078
+ if (String(values[f.key] || '').trim() === '') return true;
1079
+ }
1080
+ return false;
1081
+ }
1082
+
1083
+ function submit() {
1084
+ var payload = collect();
1085
+ // 微信扫码账号:表单存而连——凭据由扫码流程写入,保存时必须跳过
1086
+ // 适配器构建(否则 botToken 缺失工厂即抛 → 被当校验错 400,UI 假报)。
1087
+ payload.skipConnect = isWeixin && !payload.secrets.botToken;
1088
+ setBusy(true);
1089
+ setErr('');
1090
+ postJSON('/accounts/save', payload).then(function (result) {
1091
+ setBusy(false);
1092
+ if (result.status === 200) {
1093
+ props.onSaved(payload.skipConnect ? 'accounts.saved.bindhint' : 'accounts.saved');
1094
+ } else {
1095
+ setErr(errorText(result));
1096
+ }
1097
+ }).catch(function (error) {
1098
+ setBusy(false);
1099
+ setErr(messageText(error));
1100
+ });
1101
+ }
1102
+
1103
+ // 扫码前先确保记录在库(新增=先建档 skipConnect;编辑=已在库):
1104
+ // qr/status confirmed 时按 accountId upsert,命中已有记录即保留别名/配置。
1105
+ function startQr() {
1106
+ setBusy(true);
1107
+ setErr('');
1108
+ var accountId = editing ? account.accountId : genAccountId(channel.id);
1109
+ var ensureSaved;
1110
+ if (editing) {
1111
+ ensureSaved = Promise.resolve();
1112
+ } else {
1113
+ var payload = collect();
1114
+ payload.record.accountId = accountId;
1115
+ payload.skipConnect = true;
1116
+ ensureSaved = postJSON('/accounts/save', payload).then(function (result) {
1117
+ if (result.status !== 200) throw new Error(errorText(result));
1118
+ });
1119
+ }
1120
+ ensureSaved.then(function () {
1121
+ return postJSON('/weixin/qr/start', {}).then(function (result) {
1122
+ setBusy(false);
1123
+ if (result.status === 200 && result.json && result.json.ok) {
1124
+ setQr({ qrcode: result.json.qrcode, qrDataUrl: result.json.qrDataUrl, accountId: accountId, status: 'wait' });
1125
+ } else {
1126
+ setErr(errorText(result));
1127
+ }
1128
+ });
1129
+ }).catch(function (error) {
1130
+ setBusy(false);
1131
+ setErr(messageText(error));
1132
+ });
1133
+ }
1134
+
1135
+ // 状态驱动串行轮询:每次 setQr 产生新对象触发 effect 重跑下一拍;
1136
+ // 服务端每请求 hold 最长 ~35s,前端不并发叠轮(cleanup 置活旗防串台)。
1137
+ useEffect(function () {
1138
+ if (qr === null) return undefined;
1139
+ if (qr.status === 'confirmed' || qr.status === 'expired' || qr.status === 'need_verifycode' || qr.status === 'verify_code_blocked') return undefined;
1140
+ var alive = true;
1141
+ var payload = { qrcode: qr.qrcode, accountId: qr.accountId };
1142
+ if (typeof qr.verifyCode === 'string' && qr.verifyCode !== '') payload.verifyCode = qr.verifyCode;
1143
+ postJSON('/weixin/qr/status', payload).then(function (result) {
1144
+ if (!alive) return;
1145
+ if (result.status === 200 && result.json && result.json.ok) {
1146
+ var status = result.json.status;
1147
+ if (status === 'confirmed') {
1148
+ setQr(Object.assign({}, qr, { status: 'confirmed' }));
1149
+ props.onSaved('accounts.qr.confirmed');
1150
+ } else {
1151
+ setQr(Object.assign({}, qr, { status: status }));
1152
+ }
1153
+ } else if (alive) {
1154
+ setQr(Object.assign({}, qr, { status: 'expired' })); // 请求异常保守收轮:引导重新取码而非假进度
1155
+ }
1156
+ }).catch(function () {
1157
+ if (alive) setQr(Object.assign({}, qr, { status: 'expired' }));
1158
+ });
1159
+ return function () { alive = false; };
1160
+ }, [qr]);
1161
+
1162
+ var qrBlock = null;
1163
+ if (isWeixin) {
1164
+ if (qr === null || qr.status === 'expired') {
1165
+ qrBlock = h('div', { className: 'diu-qrBox' },
1166
+ h('div', { className: 'diu-qrMeta' },
1167
+ h('div', { className: 'diu-qrTitle' }, t('accounts.qr.get')),
1168
+ h('div', { className: 'diu-qrStatus' }, t('accounts.qr.desc')),
1169
+ h('div', { className: 'diu-qrActions' },
1170
+ h('button', { className: 'diu-btn diu-btnPrimary', disabled: busy, onClick: startQr },
1171
+ t(qr === null ? 'accounts.qr.get' : 'accounts.qr.refresh')),
1172
+ ),
1173
+ ),
1174
+ );
1175
+ } else if (qr.status === 'confirmed') {
1176
+ qrBlock = h('div', { className: 'diu-banner diu-banner-ok', style: { margin: '0 0 14px' } }, t('accounts.qr.confirmed'));
1177
+ } else {
1178
+ var qrMeta = [h('div', { key: 'st', className: 'diu-qrStatus' }, t('accounts.qr.' + qr.status) || qr.status)];
1179
+ if (qr.status === 'need_verifycode') {
1180
+ qrMeta.push(
1181
+ h('div', { key: 'vc', className: 'diu-verifyRow' },
1182
+ h('input', {
1183
+ className: 'diu-input', style: { maxWidth: 140 }, value: qr.verifyCode || '',
1184
+ onChange: function (event) { setQr(Object.assign({}, qr, { verifyCode: event.target.value })); },
1185
+ }),
1186
+ h('button', {
1187
+ className: 'diu-btn', onClick: function () { setQr(Object.assign({}, qr, { status: 'wait' })); },
1188
+ }, t('accounts.qr.code.submit')),
1189
+ ),
1190
+ );
1191
+ }
1192
+ qrMeta.push(
1193
+ h('div', { key: 'act', className: 'diu-qrActions' },
1194
+ h('button', { className: 'diu-btn', disabled: busy, onClick: startQr }, t('accounts.qr.refresh')),
1195
+ h('button', { className: 'diu-btn', onClick: function () { setQr(null); } }, t('accounts.qr.cancel')),
1196
+ ),
1197
+ );
1198
+ qrBlock = h('div', { className: 'diu-qrBox' },
1199
+ h('img', { className: 'diu-qrImg', src: qr.qrDataUrl, width: 168, height: 168, alt: 'login qr' }),
1200
+ h('div', { className: 'diu-qrMeta' }, qrMeta),
1201
+ );
1202
+ }
1203
+ }
1204
+
1205
+ var fieldNodes = fields.map(function (f) {
1206
+ var labelChildren = [
1207
+ h('span', { key: 'label' }, pickBilingual(f.label) || f.key),
1208
+ f.required ? h('span', { key: 'req', className: 'diu-req' }, '*') : null,
1209
+ ];
1210
+ if (f.helpUrl) {
1211
+ labelChildren.push(h('a', {
1212
+ key: 'help', className: 'diu-helpLink', href: f.helpUrl, target: '_blank', rel: 'noopener noreferrer',
1213
+ }, currentLang() === 'zh' ? '帮助 ↗' : 'help ↗'));
1214
+ }
1215
+ return h('div', { key: f.key, className: 'diu-field' + (f.secret ? '' : ' diu-fieldPlain') },
1216
+ h('span', { className: 'diu-label' }, labelChildren),
1217
+ h('input', {
1218
+ className: 'diu-input',
1219
+ type: f.secret ? 'password' : 'text',
1220
+ autoComplete: f.secret ? 'new-password' : 'off',
1221
+ value: values[f.key] || '',
1222
+ placeholder: editing && f.secret ? '••••••••' : '',
1223
+ onChange: function (event) {
1224
+ var next = Object.assign({}, values);
1225
+ next[f.key] = event.target.value;
1226
+ setValues(next);
1227
+ },
1228
+ }),
1229
+ );
1230
+ });
1231
+
1232
+ var sessionSection = null;
1233
+ if (channel.notifyOnly !== true) {
1234
+ var backendNone = catalog !== null && catalog.backend === 'none';
1235
+ // 目录类下拉在「目录未达」或「后端未就绪」时禁用(数据源缺位如实降级),
1236
+ // 但整个设置面常显:命令执行/准入/工作区是本插件本地语义,始终可编辑。
1237
+ var catalogDown = catalog === null || backendNone;
1238
+ var providerGroups = (catalog && catalog.providers) || [];
1239
+ var presets = (catalog && catalog.presets) || [];
1240
+ var permissions = (catalog && catalog.permissions) || [];
1241
+ var workspaces = (catalog && catalog.workspaces) || [];
1242
+ var selProvider = '';
1243
+ var selModel = '';
1244
+ if (prefs.modelKey !== '') {
1245
+ var di = prefs.modelKey.indexOf('::');
1246
+ selProvider = di > 0 ? prefs.modelKey.slice(0, di) : '';
1247
+ selModel = prefs.modelKey.slice(di + 2);
1248
+ }
1249
+ var efforts = [];
1250
+ providerGroups.forEach(function (group) {
1251
+ if ((group.id || '') !== selProvider) return;
1252
+ (group.models || []).forEach(function (m) {
1253
+ if (m.id === selModel && m.reasoning && Array.isArray(m.reasoning.efforts)) efforts = m.reasoning.efforts;
1254
+ });
1255
+ });
1256
+ var modelOptions = [h('option', { key: '_default', value: '' }, t('prefs.model.default'))];
1257
+ providerGroups.forEach(function (group) {
1258
+ var groupId = group.id || '';
1259
+ modelOptions.push(
1260
+ h('optgroup', { key: groupId || group.name, label: group.name || groupId },
1261
+ (group.models || []).map(function (m) {
1262
+ return h('option', { key: m.id, value: groupId + '::' + m.id }, m.name || m.id);
1263
+ }),
1264
+ ),
1265
+ );
1266
+ });
1267
+ var permissionOptions = [h('option', { key: '_default', value: '' }, t('prefs.permission.default'))];
1268
+ permissions.forEach(function (p) {
1269
+ if (p && typeof p.id === 'string' && p.id !== '') {
1270
+ permissionOptions.push(h('option', { key: p.id, value: p.id }, p.name || p.id));
1271
+ }
1272
+ });
1273
+ var execSelect = function (key, label) {
1274
+ return h('div', { key: key, className: 'diu-field' },
1275
+ h('span', { className: 'diu-label' }, label),
1276
+ h('select', {
1277
+ className: 'diu-input', value: exec[key] === false ? 'off' : 'on',
1278
+ onChange: function (event) { setExec(Object.assign({}, exec, key === 'dm' ? { dm: event.target.value !== 'off' } : { group: event.target.value !== 'off' })); },
1279
+ },
1280
+ h('option', { value: 'on' }, t('exec.on')),
1281
+ h('option', { value: 'off' }, t('exec.off')),
1282
+ ),
1283
+ );
1284
+ };
1285
+ sessionSection = h('div', { className: 'diu-sessionBlock' },
1286
+ h('div', { className: 'diu-sessionTitle' }, t('modal.session.title')),
1287
+ backendNone ? h('div', { className: 'diu-hint' }, t('modal.session.none')) : null,
1288
+ h('div', { className: 'diu-fields' },
1289
+ h('div', { className: 'diu-field' },
1290
+ h('span', { className: 'diu-label' }, t('access.title')),
1291
+ h('select', {
1292
+ className: 'diu-input', value: privateAccess,
1293
+ onChange: function (event) { setPrivateAccess(event.target.value === 'all' ? 'all' : 'approved'); },
1294
+ },
1295
+ h('option', { value: 'approved' }, t('access.approved')),
1296
+ h('option', { value: 'all' }, t('access.all')),
1297
+ ),
1298
+ ),
1299
+ execSelect('dm', t('exec.dm')),
1300
+ execSelect('group', t('exec.group')),
1301
+ h('div', { className: 'diu-field' },
1302
+ h('span', { className: 'diu-label' }, t('prefs.preset')),
1303
+ catalog === null
1304
+ ? h('div', { className: 'diu-hint' }, t('catalog.loading'))
1305
+ : h('select', {
1306
+ className: 'diu-input', value: prefs.agentPreset, disabled: backendNone,
1307
+ onChange: function (event) { setPrefs(Object.assign({}, prefs, { agentPreset: event.target.value })); },
1308
+ },
1309
+ h('option', { value: '' }, t('prefs.preset.default')),
1310
+ presets.map(function (p) {
1311
+ return h('option', { key: p.id, value: p.id }, p.name || p.id);
1312
+ }),
1313
+ ),
1314
+ ),
1315
+ h('div', { className: 'diu-field diu-fieldWide' },
1316
+ h('span', { className: 'diu-label' }, t('prefs.workspace')),
1317
+ h('input', {
1318
+ className: 'diu-input', list: 'diu-cwd-options', value: prefs.cwd,
1319
+ placeholder: t('prefs.workspace.placeholder'),
1320
+ onChange: function (event) { setPrefs(Object.assign({}, prefs, { cwd: event.target.value })); },
1321
+ }),
1322
+ h('datalist', { id: 'diu-cwd-options' },
1323
+ workspaces.map(function (w) { return h('option', { key: w, value: w }); }),
1324
+ ),
1325
+ ),
1326
+ h('div', { className: 'diu-field' },
1327
+ h('span', { className: 'diu-label' }, t('prefs.model')),
1328
+ catalog === null
1329
+ ? h('div', { className: 'diu-hint' }, t('catalog.loading'))
1330
+ : h('select', {
1331
+ className: 'diu-input', value: prefs.modelKey, disabled: backendNone,
1332
+ onChange: function (event) { setPrefs(Object.assign({}, prefs, { modelKey: event.target.value, reasoningEffort: '' })); },
1333
+ }, modelOptions),
1334
+ ),
1335
+ efforts.length > 0
1336
+ ? h('div', { className: 'diu-field' },
1337
+ h('span', { className: 'diu-label' }, t('prefs.effort')),
1338
+ h('select', {
1339
+ className: 'diu-input', value: prefs.reasoningEffort,
1340
+ onChange: function (event) { setPrefs(Object.assign({}, prefs, { reasoningEffort: event.target.value })); },
1341
+ },
1342
+ h('option', { value: '' }, t('prefs.effort.default')),
1343
+ efforts.map(function (e) {
1344
+ return h('option', { key: e.id, value: e.id }, e.name || e.id);
1345
+ }),
1346
+ ),
1347
+ )
1348
+ : null,
1349
+ h('div', { className: 'diu-field' },
1350
+ h('span', { className: 'diu-label' }, t('prefs.permission')),
1351
+ catalogDown
1352
+ ? h('div', { className: 'diu-hint' }, backendNone ? t('modal.session.none') : t('catalog.loading'))
1353
+ : h('select', {
1354
+ className: 'diu-input', value: prefs.permission,
1355
+ onChange: function (event) { setPrefs(Object.assign({}, prefs, { permission: event.target.value })); },
1356
+ }, permissionOptions),
1357
+ ),
1358
+ h('div', { className: 'diu-fieldWide diu-hint' }, t('modal.session.hint')),
1359
+ ),
1360
+ );
1361
+ }
1362
+
1363
+ var title = editing
1364
+ ? t('modal.edit.title') + ':' + (account.alias || account.accountId)
1365
+ : format('modal.add.title', { channel: channelName(channel) });
1366
+
1367
+ return h(Portal, null,
1368
+ h('div', {
1369
+ className: 'diu-modalRoot',
1370
+ onMouseDown: function (event) { if (event.target === event.currentTarget) props.onClose(); },
1371
+ },
1372
+ h('div', { className: 'diu-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': title },
1373
+ h('div', { className: 'diu-modalHead' },
1374
+ h(Logo, { channel: channel.id, size: 26 }),
1375
+ h('h3', { className: 'diu-modalTitle' }, title),
1376
+ h('button', { className: 'diu-closeBtn', onClick: props.onClose, 'aria-label': t('modal.cancel') }, h(IconClose, null)),
1377
+ ),
1378
+ h('div', { className: 'diu-modalBody' },
1379
+ qrBlock,
1380
+ h('div', { className: 'diu-fields' },
1381
+ h('div', { className: 'diu-field diu-fieldWide' },
1382
+ h('span', { className: 'diu-label' }, t('accounts.alias')),
1383
+ h('input', {
1384
+ className: 'diu-input', value: alias, placeholder: t('accounts.alias.placeholder'),
1385
+ onChange: function (event) { setAlias(event.target.value); },
1386
+ }),
1387
+ ),
1388
+ fieldNodes,
1389
+ ),
1390
+ sessionSection,
1391
+ h('p', { className: 'diu-modalNote' },
1392
+ t('modal.autoId'),
1393
+ editing ? ' ' + t('modal.editSecretHint') : '',
1394
+ ),
1395
+ ),
1396
+ h('div', { className: 'diu-modalFoot' },
1397
+ h('span', { className: 'diu-footMsg' }, err),
1398
+ busy ? h('span', { className: 'diu-hint' }, t('modal.saving')) : null,
1399
+ h('button', { className: 'diu-btn', onClick: props.onClose }, t('modal.cancel')),
1400
+ h('button', {
1401
+ className: 'diu-btn diu-btnPrimary', disabled: busy || requiredMissing(), onClick: submit,
1402
+ }, busy ? t('modal.saving') : t('modal.save')),
1403
+ ),
1404
+ ),
1405
+ ),
1406
+ );
1407
+ }
1408
+
1409
+ function PanelBody(props) {
1410
+ var state = props.state;
1411
+ var caps = state.capabilities || {};
1412
+ var channels = state.channels || [];
1413
+ var accounts = props.accounts || [];
1414
+ var byChannel = {};
1415
+ accounts.forEach(function (row) {
1416
+ if (byChannel[row.channel] === undefined) byChannel[row.channel] = [];
1417
+ byChannel[row.channel].push(row);
1418
+ });
1419
+ var online = accounts.filter(function (row) { return row.connected && !row.paused; }).length;
1420
+ var implemented = channels.filter(function (c) { return c.implemented === true; });
1421
+ // 已实现渠道在前(保持 meta 顺序),未实现垫底灰显。
1422
+ var sorted = implemented.concat(channels.filter(function (c) { return c.implemented !== true; }));
1423
+
1424
+ // ── 模型白名单(Phase 7)──
1425
+ var wlHook = useState('');
1426
+ var wlText = wlHook[0];
1427
+ var setWlText = wlHook[1];
1428
+ var wlSaved = useState(false);
1429
+ var setWlSaved = wlSaved[1];
1430
+ useEffect(function () {
1431
+ getJSON('/model-whitelist').then(function (r) {
1432
+ if (r.json && Array.isArray(r.json.whitelist)) setWlText(r.json.whitelist.join('\n'));
1433
+ }).catch(function () {});
1434
+ }, []);
1435
+ function saveWhitelist() {
1436
+ var lines = wlText.split('\n').map(function (s) { return s.trim(); }).filter(function (s) { return s !== ''; });
1437
+ postJSON('/model-whitelist', { whitelist: lines }).then(function () {
1438
+ setWlSaved(true);
1439
+ setTimeout(function () { setWlSaved(false); }, 2000);
1440
+ }).catch(function () {});
1441
+ }
1442
+
1443
+ var noticeNode = null;
1444
+ if (props.notice !== null && props.notice !== undefined) {
1445
+ noticeNode = h('div', { className: 'diu-banner diu-banner-' + (props.notice.kind || 'info') },
1446
+ h('span', { style: { flex: 1, minWidth: 0 } }, props.notice.text),
1447
+ h('button', { className: 'diu-bannerClose', onClick: props.onDismissNotice, 'aria-label': t('modal.cancel') }, h(IconClose, null)),
1448
+ );
1449
+ }
1450
+
1451
+ return h('div', { className: 'diu-page' },
1452
+ h('div', { className: 'diu-head' },
1453
+ h('div', null,
1454
+ h('h2', { className: 'diu-title' }, t('settings.title')),
1455
+ h('p', { className: 'diu-sub' }, t('panel.subtitle')),
1456
+ ),
1457
+ h('div', { className: 'diu-headActions' },
1458
+ h('a', { className: 'diu-iconBtn', href: REPO_URL, target: '_blank', rel: 'noopener noreferrer', style: { textDecoration: 'none' } },
1459
+ h(IconGitHub, null), t('panel.github')),
1460
+ h('button', { className: 'diu-iconBtn', onClick: props.onRefresh }, h(IconRefresh, null), t('panel.refresh')),
1461
+ ),
1462
+ ),
1463
+ h('div', { className: 'diu-stats' },
1464
+ h('span', { className: 'diu-stat' }, t('panel.stat.channels'), h('strong', null, implemented.length + '/' + channels.length)),
1465
+ h('span', { className: 'diu-stat' }, t('panel.stat.accounts'), h('strong', null, String(accounts.length))),
1466
+ h('span', { className: 'diu-stat' }, t('panel.stat.online'), h('strong', null, String(online))),
1467
+ ),
1468
+ noticeNode,
1469
+ h('details', { className: 'diu-debug' },
1470
+ h('summary', null, t('panel.debug')),
1471
+ h('dl', { className: 'diu-debugBody' },
1472
+ h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.generation')), h('dd', null, String(caps.settingsGeneration || '-'))),
1473
+ h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.sessionBackend')), h('dd', null, String(caps.sessionBackend || '-'))),
1474
+ h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.vault')), h('dd', null, String(caps.vaultKind || '-'))),
1475
+ h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.uploads')), h('dd', null, String(caps.uploadsMode || '-'))),
1476
+ h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.deliverables')), h('dd', null, String(caps.deliverablesMode || '-'))),
1477
+ h('div', { className: 'diu-debugItem' }, h('dt', null, t('panel.connector')), h('dd', null, caps.connectorEnabled ? t('panel.on') : t('panel.off'))),
1478
+ h('div', { className: 'diu-debugChannels' },
1479
+ channels.map(function (c) {
1480
+ return h('span', { key: c.id, className: 'diu-stat' },
1481
+ channelName(c), ' · ', c.implemented ? t('panel.implemented') : t('panel.planned'));
1482
+ }),
1483
+ ),
1484
+ ),
1485
+ ),
1486
+ h('div', { className: 'diu-section' },
1487
+ h('h3', { className: 'diu-section-title' }, t('whitelist.title')),
1488
+ h('textarea', {
1489
+ className: 'diu-whitelist-textarea',
1490
+ rows: 5,
1491
+ placeholder: t('whitelist.placeholder'),
1492
+ value: wlText,
1493
+ onInput: function (e) { setWlText(e.target.value); },
1494
+ }),
1495
+ h('button', { className: 'diu-btn diu-btn-primary', onClick: saveWhitelist },
1496
+ wlSaved ? t('whitelist.saved') : t('whitelist.save')),
1497
+ h('p', { className: 'diu-hint' }, t('whitelist.hint')),
1498
+ ),
1499
+ h('div', { className: 'diu-platforms' },
1500
+ sorted.map(function (channel) {
1501
+ return h(PlatformCard, {
1502
+ key: channel.id,
1503
+ channel: channel,
1504
+ accounts: byChannel[channel.id] || [],
1505
+ open: props.expanded[channel.id] === true,
1506
+ busy: props.busy,
1507
+ checks: props.checks,
1508
+ onToggle: props.onToggle,
1509
+ onAdd: props.onAdd,
1510
+ onCheck: props.onCheck,
1511
+ onEdit: props.onEdit,
1512
+ onPause: props.onPause,
1513
+ onRemove: props.onRemove,
1514
+ onSaveAllowlist: props.onSaveAllowlist,
1515
+ });
1516
+ }),
1517
+ ),
1518
+ props.modal !== null && props.modal !== undefined
1519
+ ? h(AccountModal, {
1520
+ key: props.modal.key,
1521
+ channel: props.modal.channel,
1522
+ account: props.modal.account || null,
1523
+ onClose: props.onCloseModal,
1524
+ onSaved: props.onModalSaved,
1525
+ })
1526
+ : null,
1527
+ );
1528
+ }
1529
+
1530
+ function UnionPanel() {
1531
+ var stateHook = useState(null);
1532
+ var state = stateHook[0];
1533
+ var setState = stateHook[1];
1534
+ var errorHook = useState(null);
1535
+ var error = errorHook[0];
1536
+ var setError = errorHook[1];
1537
+ var accountsHook = useState(null);
1538
+ var accounts = accountsHook[0];
1539
+ var setAccounts = accountsHook[1];
1540
+ var checksHook = useState({});
1541
+ var checks = checksHook[0];
1542
+ var setChecks = checksHook[1];
1543
+ var noticeHook = useState(null);
1544
+ var notice = noticeHook[0];
1545
+ var setNotice = noticeHook[1];
1546
+ var busyHook = useState(0);
1547
+ var busy = busyHook[0];
1548
+ var setBusy = busyHook[1];
1549
+ var expandedHook = useState({});
1550
+ var expanded = expandedHook[0];
1551
+ var setExpanded = expandedHook[1];
1552
+ var modalHook = useState(null);
1553
+ var modal = modalHook[0];
1554
+ var setModal = modalHook[1];
1555
+ var didInitRef = useRef(false);
1556
+ var mountedRef = useRef(true);
1557
+
1558
+ useEffect(function () {
1559
+ mountedRef.current = true;
1560
+ return function () { mountedRef.current = false; };
1561
+ }, []);
1562
+
1563
+ function fetchAccounts(setter) {
1564
+ return getJSON('/accounts').then(function (result) {
1565
+ if (!mountedRef.current) return;
1566
+ if (result.status === 200 && result.json && result.json.ok) {
1567
+ var rows = result.json.accounts || [];
1568
+ setter(rows);
1569
+ // 首次拿到账号后展开有账号的渠道卡(之后尊重用户手动开合)。
1570
+ if (!didInitRef.current) {
1571
+ didInitRef.current = true;
1572
+ var init = {};
1573
+ rows.forEach(function (row) { init[row.channel] = true; });
1574
+ setExpanded(init);
1575
+ }
1576
+ }
1577
+ });
1578
+ }
1579
+
1580
+ useEffect(function () {
1581
+ // 状态面慢轮询 + 账号面快轮询;后台标签页暂停,回前台立即补一拍。
1582
+ var stateTimer = null;
1583
+ var accountsTimer = null;
1584
+ var tickState = function () {
1585
+ if (document.hidden) return;
1586
+ fetch(API_BASE + '/state', { credentials: 'same-origin', headers: { 'x-dsh-im-union-client': '1' } })
1587
+ .then(function (response) {
1588
+ if (!response.ok) throw new Error('HTTP ' + response.status);
1589
+ return response.json();
1590
+ })
1591
+ .then(function (data) {
1592
+ if (mountedRef.current) { setState(data); setError(null); }
1593
+ })
1594
+ .catch(function (err) {
1595
+ if (mountedRef.current) setError(messageText(err));
1596
+ });
1597
+ };
1598
+ var tickAccounts = function () {
1599
+ if (document.hidden) return;
1600
+ fetchAccounts(function (rows) { setAccounts(rows); }).catch(function () { /* 轮询失败静默,下一拍再试 */ });
1601
+ };
1602
+ tickState();
1603
+ tickAccounts();
1604
+ stateTimer = setInterval(tickState, STATE_POLL_MS);
1605
+ accountsTimer = setInterval(tickAccounts, ACCOUNTS_POLL_MS);
1606
+ var onVisible = function () {
1607
+ if (!document.hidden) { tickState(); tickAccounts(); }
1608
+ };
1609
+ document.addEventListener('visibilitychange', onVisible);
1610
+ return function () {
1611
+ mountedRef.current = false;
1612
+ clearInterval(stateTimer);
1613
+ clearInterval(accountsTimer);
1614
+ document.removeEventListener('visibilitychange', onVisible);
1615
+ };
1616
+ }, []);
1617
+
1618
+ function withBusy(task) {
1619
+ setBusy(function (n) { return n + 1; });
1620
+ return Promise.resolve(task()).finally(function () {
1621
+ setBusy(function (n) { return Math.max(0, n - 1); });
1622
+ });
1623
+ }
1624
+
1625
+ function refreshAll() {
1626
+ withBusy(function () {
1627
+ var p1 = fetch(API_BASE + '/state', { credentials: 'same-origin', headers: { 'x-dsh-im-union-client': '1' } })
1628
+ .then(function (response) { if (!response.ok) throw new Error('HTTP ' + response.status); return response.json(); })
1629
+ .then(function (data) { setState(data); setError(null); })
1630
+ .catch(function (err) { setError(messageText(err)); });
1631
+ var p2 = fetchAccounts(function (rows) { setAccounts(rows); })
1632
+ .catch(function (err) { setNotice({ kind: 'err', text: messageText(err) }); });
1633
+ return Promise.all([p1, p2]);
1634
+ });
1635
+ }
1636
+
1637
+ function doAction(path, body, doneNotice) {
1638
+ withBusy(function () {
1639
+ return postJSON(path, body).then(function (result) {
1640
+ if (result.status === 200) {
1641
+ if (doneNotice) setNotice({ kind: 'ok', text: t(doneNotice) });
1642
+ return fetchAccounts(function (rows) { setAccounts(rows); });
1643
+ }
1644
+ setNotice({ kind: 'err', text: errorText(result) });
1645
+ }).catch(function (err) {
1646
+ setNotice({ kind: 'err', text: messageText(err) });
1647
+ });
1648
+ });
1649
+ }
1650
+
1651
+ function doCheck(accountId) {
1652
+ withBusy(function () {
1653
+ return postJSON('/accounts/check', { accountId: accountId }).then(function (result) {
1654
+ if (result.status === 200 && result.json && result.json.ok) {
1655
+ var next = Object.assign({}, checks);
1656
+ next[accountId] = result.json.checks || [];
1657
+ setChecks(next);
1658
+ } else {
1659
+ setNotice({ kind: 'err', text: errorText(result) });
1660
+ }
1661
+ }).catch(function (err) {
1662
+ setNotice({ kind: 'err', text: messageText(err) });
1663
+ });
1664
+ });
1665
+ }
1666
+
1667
+ if (error !== null) {
1668
+ return h('div', { className: 'diu-page' },
1669
+ h('div', { className: 'diu-head' },
1670
+ h('div', null,
1671
+ h('h2', { className: 'diu-title' }, t('settings.title')),
1672
+ h('p', { className: 'diu-sub' }, t('panel.subtitle')),
1673
+ ),
1674
+ h('div', { className: 'diu-headActions' },
1675
+ h('button', { className: 'diu-iconBtn', onClick: refreshAll }, h(IconRefresh, null), t('panel.refresh')),
1676
+ ),
1677
+ ),
1678
+ h('div', { className: 'diu-banner diu-banner-err' }, t('panel.error') + ' (' + error + ')'),
1679
+ );
1680
+ }
1681
+ if (state === null) {
1682
+ return h('div', { className: 'diu-page' }, h('p', { className: 'diu-hint' }, t('panel.loading')));
1683
+ }
1684
+
1685
+ function openAdd(channel) {
1686
+ setModal({ key: 'add-' + channel.id + '-' + Date.now(), channel: channel, account: null });
1687
+ }
1688
+ function openEdit(row) {
1689
+ var channel = null;
1690
+ (state.channels || []).forEach(function (c) { if (c.id === row.channel) channel = c; });
1691
+ if (channel === null) return;
1692
+ setModal({ key: 'edit-' + row.accountId + '-' + Date.now(), channel: channel, account: row });
1693
+ }
1694
+ function closeModal() { setModal(null); }
1695
+ function onModalSaved(noticeKey) {
1696
+ setModal(null);
1697
+ setNotice({ kind: 'ok', text: t(noticeKey) });
1698
+ fetchAccounts(function (rows) { setAccounts(rows); }).catch(function () { /* 关弹窗后刷新失败留给轮询 */ });
1699
+ }
1700
+
1701
+ return h(PanelBody, {
1702
+ state: state,
1703
+ accounts: accounts || [],
1704
+ checks: checks,
1705
+ notice: notice,
1706
+ busy: busy > 0,
1707
+ expanded: expanded,
1708
+ modal: modal,
1709
+ onRefresh: refreshAll,
1710
+ onDismissNotice: function () { setNotice(null); },
1711
+ onToggle: function (channelId) {
1712
+ setExpanded(function (prev) {
1713
+ var next = Object.assign({}, prev);
1714
+ next[channelId] = !prev[channelId];
1715
+ return next;
1716
+ });
1717
+ },
1718
+ onAdd: openAdd,
1719
+ onEdit: openEdit,
1720
+ onCloseModal: closeModal,
1721
+ onModalSaved: onModalSaved,
1722
+ onCheck: doCheck,
1723
+ onPause: function (accountId, paused) {
1724
+ doAction('/accounts/pause', { accountId: accountId, paused: paused }, paused ? 'accounts.saved' : 'accounts.saved');
1725
+ },
1726
+ onRemove: function (accountId) {
1727
+ doAction('/accounts/remove', { accountId: accountId }, 'accounts.removed');
1728
+ },
1729
+ onSaveAllowlist: function (accountId, userIds) {
1730
+ doAction('/accounts/allowlist', { accountId: accountId, userIds: userIds }, 'accounts.saved');
1731
+ },
1732
+ });
1733
+ }
1734
+
1735
+ // gen1 兜底:宿主没有设置页时挂一个最小浮窗(可关闭)。
1736
+ function overlayFallback() {
1737
+ try {
1738
+ if (typeof document === 'undefined') return;
1739
+ if (document.getElementById('dsh-im-union-overlay')) return;
1740
+ installStyles();
1741
+ var host = document.createElement('div');
1742
+ host.id = 'dsh-im-union-overlay';
1743
+ host.className = 'diu-overlayFallback';
1744
+ document.body.appendChild(host);
1745
+ var closeBar = document.createElement('button');
1746
+ closeBar.textContent = '×';
1747
+ closeBar.style.cssText = 'position:absolute;top:6px;right:8px;border:0;background:transparent;font-size:16px;cursor:pointer;opacity:.6';
1748
+ closeBar.addEventListener('click', function () { host.remove(); });
1749
+ host.style.position = 'fixed';
1750
+ host.appendChild(closeBar);
1751
+ var mount = document.createElement('div');
1752
+ host.appendChild(mount);
1753
+ if (window.ReactDOM && typeof window.ReactDOM.createRoot === 'function') {
1754
+ window.ReactDOM.createRoot(mount).render(h(UnionPanel));
1755
+ } else if (window.ReactDOM && typeof window.ReactDOM.render === 'function') {
1756
+ window.ReactDOM.render(h(UnionPanel), mount);
1757
+ }
1758
+ } catch (error) {
1759
+ console.warn('[dsh-im-union] 浮窗兜底失败:', error);
1760
+ }
1761
+ }
1762
+
1763
+
1764
+ // ── 渠道视角(0.4.0):/channels 轮询 + 渠道 rail + 原生页签插入 ────────
1765
+ // 参考 dsh-im-connect ChannelRail/sync 形态。挂载降级链:①原生 tab registry
1766
+ // (宿主自带或 im-connect 壳的 __dshNativeTabs)插 channels 页签 → ②独立
1767
+ // sidebar.channels 槽 → ③仅日志(会话管理仍有 /sessions 命令面)。老线不 crash。
1768
+ function relativeTime(ts) {
1769
+ if (typeof ts !== 'number' || !(ts > 0)) return '';
1770
+ var delta = Date.now() - ts;
1771
+ if (delta < 60 * 1000) return t('time.now');
1772
+ if (delta < 60 * 60 * 1000) return t('time.minutes').replace('{n}', String(Math.floor(delta / 60000)));
1773
+ if (delta < 24 * 60 * 60 * 1000) return t('time.hours').replace('{n}', String(Math.floor(delta / 3600000)));
1774
+ return t('time.days').replace('{n}', String(Math.floor(delta / 86400000)));
1775
+ }
1776
+
1777
+ var railState = { groups: [], loaded: false, error: false };
1778
+ var railListeners = new Set();
1779
+ var railFailures = 0;
1780
+ var railTimer = null;
1781
+ function railNotify() { railListeners.forEach(function (fn) { try { fn(); } catch (_) {} }); }
1782
+ function railSetState(patch) {
1783
+ var changed = false;
1784
+ for (var key in patch) { if (railState[key] !== patch[key]) { railState[key] = patch[key]; changed = true; } }
1785
+ if (changed) railNotify();
1786
+ }
1787
+ function railTick() {
1788
+ if (document.hidden) return;
1789
+ getJSON('/channels').then(function (result) {
1790
+ railFailures = 0;
1791
+ railSetState({ groups: (result.json && result.json.groups) || [], loaded: true, error: false });
1792
+ }).catch(function () {
1793
+ railFailures += 1;
1794
+ if (railFailures >= 2) railSetState({ error: true });
1795
+ });
1796
+ }
1797
+ function startRailPolling() {
1798
+ if (railTimer !== null) return;
1799
+ var delay = Math.min(4000 * Math.pow(2, railFailures), 60000);
1800
+ railTimer = setTimeout(function () { railTimer = null; railTick(); startRailPolling(); }, delay);
1801
+ }
1802
+ document.addEventListener('visibilitychange', function () {
1803
+ if (!document.hidden) { railFailures = 0; if (railTimer !== null) { clearTimeout(railTimer); railTimer = null; } railTick(); startRailPolling(); }
1804
+ });
1805
+
1806
+ // Cordis 服务软读(dsh-im-connect 同款纪律):未 inject 的服务不能硬读 ctx[name],
1807
+ // 否则代理层直接抛且被 catch 吞掉——表现为「点击无反应」。
1808
+ function probeService(ctx, name) {
1809
+ if (!ctx) return undefined;
1810
+ var reflect = ctx.reflect;
1811
+ if (reflect && typeof reflect.get === 'function') {
1812
+ try {
1813
+ var found = reflect.get(name);
1814
+ if (found !== undefined) return found;
1815
+ } catch (_) { /* 未注入时不硬读 ctx[name],改走 get */ }
1816
+ }
1817
+ if (typeof ctx.get === 'function') {
1818
+ try {
1819
+ var viaGet = ctx.get(name);
1820
+ if (viaGet !== undefined) return viaGet;
1821
+ } catch (_) { return undefined; }
1822
+ }
1823
+ if (reflect && typeof reflect.get === 'function') return undefined;
1824
+ try { return ctx[name]; } catch (_) { return undefined; }
1825
+ }
1826
+
1827
+ // 打开会话(参考 openHostSession 链):uiWorkspace.openSession 优先;alpha.2+ 宿主
1828
+ // 有 sessions.retain 时 sessions.open 不再写 mainView,**禁止回退**(否则点了等于没开)。
1829
+ function openUnionSession(ctx, id) {
1830
+ if (!id) return false;
1831
+ try {
1832
+ var uiWorkspace = probeService(ctx, 'uiWorkspace');
1833
+ if (uiWorkspace && typeof uiWorkspace.openSession === 'function') {
1834
+ uiWorkspace.openSession(id);
1835
+ return true;
1836
+ }
1837
+ var sessionsSvc = probeService(ctx, 'sessions');
1838
+ if (sessionsSvc && typeof sessionsSvc.retain === 'function') return false;
1839
+ if (sessionsSvc && typeof sessionsSvc.open === 'function') {
1840
+ sessionsSvc.open(id);
1841
+ return true;
1842
+ }
1843
+ if (typeof window.__dshSessionsOpen === 'function') {
1844
+ window.__dshSessionsOpen(id);
1845
+ return true;
1846
+ }
1847
+ location.hash = '#/session/' + encodeURIComponent(id);
1848
+ return true;
1849
+ } catch (error) {
1850
+ console.warn('[dsh-im-union] 打开会话失败:', error);
1851
+ return false;
1852
+ }
1853
+ }
1854
+
1855
+ function ensureRailStyles() {
1856
+ if (document.getElementById('diu-rail-style')) return;
1857
+ var style = document.createElement('style');
1858
+ style.id = 'diu-rail-style';
1859
+ style.textContent =
1860
+ '.diu-rail{display:flex;flex-direction:column;gap:6px;padding:10px 12px;height:100%;overflow-y:auto;box-sizing:border-box}' +
1861
+ '.diu-rail-search{width:100%;box-sizing:border-box;padding:6px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,#ddd);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,#222);outline:none;font-size:12px}' +
1862
+ '.diu-rail-group-head{display:flex;justify-content:space-between;align-items:center;padding:4px 2px;font-size:11px;color:var(--dsw-alias-label-secondary,#888)}' +
1863
+ '.diu-rail-row{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary,#222)}' +
1864
+ '.diu-rail-row:hover{background:var(--dsw-alias-interactive-bg-hover,#f2f2f2)}' +
1865
+ '.diu-rail-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-border-l4,#ccc);flex:none}' +
1866
+ '.diu-rail-dot.on{background:var(--dsw-alias-state-success,#22a06b)}' +
1867
+ '.diu-rail-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
1868
+ '.diu-rail-time{font-size:11px;color:var(--dsw-alias-label-tertiary,#aaa);flex:none;margin-right:2px;text-align:right}' +
1869
+ '.diu-rail-empty{padding:14px 8px;font-size:12px;color:var(--dsw-alias-label-secondary,#888);text-align:center}' +
1870
+ // 侧栏 wrap:完全沿用 dsh-im-connect 既有 tab 样式(无框、下划线高亮),
1871
+ // 不自造带边框按钮(用户裁定:有现成样式参考,不要加框)。
1872
+ '.diu-wrap{display:flex;flex-direction:column;min-height:0;flex:1;height:100%;overflow:hidden}' +
1873
+ '.diu-official-tree{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}' +
1874
+ '.diu-tabs{display:flex;gap:18px;padding:4px 12px 0;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e5e5);flex:none}' +
1875
+ '.diu-tab{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,#888);padding:8px 4px;font-size:13px;cursor:pointer}' +
1876
+ // 点过的 tab 不留默认黑框(截图里「频道」被方框围住);键盘聚焦仍给焦点环。
1877
+ '.diu-tab:focus{outline:none}' +
1878
+ '.diu-tab:focus-visible{outline:2px solid var(--dsw-alias-border-brand,#888);outline-offset:2px;border-radius:4px}' +
1879
+ '.diu-tab.on{color:var(--dsw-alias-label-primary,#222);box-shadow:inset 0 -2px 0 currentColor}';
1880
+ document.head.appendChild(style);
1881
+ }
1882
+
1883
+ function ChannelRow(props) {
1884
+ var s = props.session;
1885
+ var chatId = String(s.sessionId).split(':').slice(4).join(':');
1886
+ var label = s.title || chatId || s.sessionId;
1887
+ return h('div', {
1888
+ className: 'diu-rail-row',
1889
+ onClick: function () { props.onOpen(s.sessionId); },
1890
+ },
1891
+ h('span', { className: 'diu-rail-dot' + (s.running ? ' on' : '') }),
1892
+ h('span', { className: 'diu-rail-label' }, label),
1893
+ h('span', { className: 'diu-rail-time' }, s.detached ? t('rail.detached') : relativeTime(s.lastOutAt)),
1894
+ );
1895
+ }
1896
+
1897
+ function ChannelRail(props) {
1898
+ var searchHook = useState('');
1899
+ var needle = searchHook[0].toLowerCase();
1900
+ var setNeedle = searchHook[1];
1901
+ var revHook = useState(0);
1902
+ var bump = revHook[1];
1903
+ var useEffect = React.useEffect;
1904
+ useEffect(function () {
1905
+ var fn = function () { bump(function (n) { return n + 1; }); };
1906
+ railListeners.add(fn);
1907
+ startRailPolling();
1908
+ return function () { railListeners.delete(fn); };
1909
+ }, []);
1910
+ ensureRailStyles();
1911
+ var groups = railState.groups;
1912
+ var sections = [];
1913
+ groups.forEach(function (group) {
1914
+ var sessions = (group.sessions || []).filter(function (s) {
1915
+ if (!needle) return true;
1916
+ return ((s.title || '') + ' ' + String(s.sessionId)).toLowerCase().indexOf(needle) >= 0;
1917
+ });
1918
+ if (sessions.length === 0) return;
1919
+ sessions = sessions.slice().sort(function (a, b) { return (b.lastOutAt || 0) - (a.lastOutAt || 0); });
1920
+ sections.push(h('div', { className: 'diu-rail-group', key: group.channel },
1921
+ h('div', { className: 'diu-rail-group-head' },
1922
+ h('span', { className: 'diu-rail-group-name' }, group.channel),
1923
+ h('span', { className: 'diu-rail-count' }, String(sessions.length))),
1924
+ sessions.map(function (s) { return h(ChannelRow, { key: s.sessionId, session: s, onOpen: props.onOpen }); })
1925
+ ));
1926
+ });
1927
+ var body;
1928
+ if (!railState.loaded && sections.length === 0) body = h('div', { className: 'diu-rail-empty' }, t('rail.loading'));
1929
+ else if (sections.length === 0) body = h('div', { className: 'diu-rail-empty' }, t('rail.empty'));
1930
+ else body = sections;
1931
+ return h('div', { className: 'diu-rail' },
1932
+ h('input', {
1933
+ className: 'diu-rail-search', placeholder: t('rail.search'), value: searchHook[0],
1934
+ onInput: function (e) { setNeedle(e.target.value); },
1935
+ }),
1936
+ body,
1937
+ railState.error ? h('div', { className: 'diu-rail-empty' }, t('rail.error')) : null,
1938
+ );
1939
+ }
1940
+
1941
+ // ── 侧栏 wrap(Phase 6):包裹宿主官方任务树,自建「任务/频道」两页签 ──
1942
+ // 形态照 dsh-im-connect 实战先例:直接替换 sidebar.workspaces 槽位的 component
1943
+ // 为自己的 Shell(任务页 = 渲染原官方组件,频道页 = 本插件 rail),并用
1944
+ // slots.subscribe + 重试环保持同步;检测到已被其它插件包裹则绝不插手(隔离优先)。
1945
+ class UnionSidebarBoundary extends React.Component {
1946
+ constructor(props) {
1947
+ super(props);
1948
+ this.state = { failed: false };
1949
+ }
1950
+ static getDerivedStateFromError() {
1951
+ return { failed: true };
1952
+ }
1953
+ componentDidCatch(error) {
1954
+ console.warn('[dsh-im-union] 侧栏 wrap 渲染失败,已回落宿主原生会话列表:', error);
1955
+ }
1956
+ render() {
1957
+ if (this.state.failed) {
1958
+ var Official = this.props.officialTree;
1959
+ return Official ? h(Official, this.props.hostProps) : null;
1960
+ }
1961
+ return this.props.children;
1962
+ }
1963
+ }
1964
+
1965
+ function readSidebarTab() {
1966
+ try {
1967
+ var v = localStorage.getItem('diu-sidebar-tab');
1968
+ return v === 'channels' ? 'channels' : 'tasks';
1969
+ } catch (_) { return 'tasks'; }
1970
+ }
1971
+
1972
+ // IM 会话判据(参考 isChannelSession):宿主 IM 约定全用 `im:` 前缀。
1973
+ function isChannelSession(id) {
1974
+ return String(id || '').indexOf('im:') === 0;
1975
+ }
1976
+
1977
+ // 任务树快照过滤(参考 filterTaskSessions):去掉 im: 会话,否则左栏「任务」页
1978
+ // 会把 IM 会话当普通会话显示成「未分组」。
1979
+ function filterTaskState(state) {
1980
+ var src = state || { ids: [], byId: {}, current: null };
1981
+ var srcIds = src.ids || [];
1982
+ var ids = [];
1983
+ for (var i = 0; i < srcIds.length; i++) {
1984
+ if (!isChannelSession(srcIds[i])) ids.push(srcIds[i]);
1985
+ }
1986
+ if (ids.length === srcIds.length) return src;
1987
+ var byId = {};
1988
+ for (var j = 0; j < ids.length; j++) {
1989
+ if (src.byId && src.byId[ids[j]]) byId[ids[j]] = src.byId[ids[j]];
1990
+ }
1991
+ return Object.assign({}, src, { ids: ids, byId: byId });
1992
+ }
1993
+
1994
+ function UnionSessionSwitcher(props) {
1995
+ var tabHook = useState(readSidebarTab);
1996
+ var tab = tabHook[0];
1997
+ var setTab = tabHook[1];
1998
+ var useEffect = React.useEffect;
1999
+ useEffect(function () {
2000
+ try { localStorage.setItem('diu-sidebar-tab', tab); } catch (_) {}
2001
+ }, [tab]);
2002
+ var Official = props.officialTree;
2003
+ // 打开会话(参考 openHostSession 链):props.openSession/open 优先,缺位退
2004
+ // probeService 软读;并跟随会话类型切页(im: → 频道、普通 → 任务)。
2005
+ function openSession(id) {
2006
+ if (!id) return;
2007
+ var hostOpen = (props && (props.openSession || props.open)) || null;
2008
+ var ok = false;
2009
+ if (typeof hostOpen === 'function') {
2010
+ try { hostOpen(id); ok = true; } catch (error) {
2011
+ console.warn('[dsh-im-union] props.openSession 失败,回退软读链:', error);
2012
+ }
2013
+ }
2014
+ if (!ok) ok = openUnionSession(props.__unionCtx || {}, id);
2015
+ if (ok) setTab(isChannelSession(id) ? 'channels' : 'tasks');
2016
+ else console.warn('[dsh-im-union] 会话打开面不可用(props.openSession / uiWorkspace / sessions.open 均缺位):', id);
2017
+ }
2018
+ // 任务树数据面替换(参考 useTaskSessions):官方树吃 props.useSessions,
2019
+ // 我们注入过滤版(排除 im: 会话)——否则「未分组」里会冒出 IM 会话。
2020
+ var rawUseSessions = props.useSessions;
2021
+ function useTaskSessions(selector, eq) {
2022
+ if (typeof rawUseSessions === 'function') {
2023
+ return rawUseSessions(function (state) {
2024
+ var filtered = filterTaskState(state);
2025
+ return typeof selector === 'function' ? selector(filtered) : filtered;
2026
+ }, eq);
2027
+ }
2028
+ var empty = { ids: [], byId: {}, current: null };
2029
+ return typeof selector === 'function' ? selector(empty) : empty;
2030
+ }
2031
+ var officialProps = Object.assign({}, props, { useSessions: useTaskSessions });
2032
+ var tabs = h('div', { className: 'diu-tabs', role: 'tablist', 'aria-label': t('rail.tabTasks') + '/' + t('rail.tabChannels') },
2033
+ h('button', {
2034
+ type: 'button', role: 'tab', 'aria-selected': tab === 'tasks',
2035
+ className: 'diu-tab' + (tab === 'tasks' ? ' on' : ''),
2036
+ onClick: function () { setTab('tasks'); },
2037
+ }, t('rail.tabTasks')),
2038
+ h('button', {
2039
+ type: 'button', role: 'tab', 'aria-selected': tab === 'channels',
2040
+ className: 'diu-tab' + (tab === 'channels' ? ' on' : ''),
2041
+ onClick: function () { setTab('channels'); },
2042
+ }, t('rail.tabChannels')),
2043
+ );
2044
+ var body;
2045
+ if (tab === 'tasks') {
2046
+ body = Official ? h('div', { className: 'diu-official-tree' }, h(Official, officialProps)) : null;
2047
+ } else {
2048
+ body = h(ChannelRail, { onOpen: openSession });
2049
+ }
2050
+ return h('div', { className: 'diu-wrap' }, tabs, body);
2051
+ }
2052
+
2053
+ function mountChannelRailSelf(ctx) {
2054
+ ensureRailStyles();
2055
+ startRailPolling();
2056
+ try {
2057
+ if (typeof ctx.slots.inject !== 'function') {
2058
+ console.info('[dsh-im-union] slots.inject 缺位,侧栏 wrap 不可用(设置面板仍可用)');
2059
+ return;
2060
+ }
2061
+ ctx.slots.inject('sidebar.workspaces', function () {
2062
+ var wrappedEntry = null;
2063
+ var originalComp = null;
2064
+ var ownedWrapper = null;
2065
+ var syncing = false;
2066
+ var unwrap = function () {
2067
+ try {
2068
+ if (wrappedEntry !== null && originalComp !== null && wrappedEntry.component === ownedWrapper) {
2069
+ wrappedEntry.component = originalComp;
2070
+ }
2071
+ } catch (_) {}
2072
+ wrappedEntry = null;
2073
+ originalComp = null;
2074
+ ownedWrapper = null;
2075
+ };
2076
+ var sync = function () {
2077
+ if (syncing) return;
2078
+ syncing = true;
2079
+ try {
2080
+ var read = ctx.slots.entriesOfSlot || ctx.slots.entries;
2081
+ var entries = (read && read.call(ctx.slots, 'sidebar.workspaces')) || [];
2082
+ var occupant = null;
2083
+ for (var i = 0; i < entries.length; i++) {
2084
+ if (entries[i] && entries[i].component) { occupant = entries[i]; break; }
2085
+ }
2086
+ if (wrappedEntry !== null && (occupant !== wrappedEntry || occupant.component !== ownedWrapper)) unwrap();
2087
+ if (!occupant) { unwrap(); return; }
2088
+ // 隔离优先:已被其它插件包裹/占用(im-connect、codex-ui、automation 等)→ 绝不插手
2089
+ var comp = occupant.component;
2090
+ if (comp.__unionWrapped) return;
2091
+ if (comp.__imConnectWrapped || comp.__dshNativeTabHost || comp.__dshAutomationWrapped) {
2092
+ if (wrappedEntry === null) {
2093
+ console.info('[dsh-im-union] sidebar.workspaces 已被其它插件接管,不插手(rail 在设置面板可用)');
2094
+ }
2095
+ return;
2096
+ }
2097
+ if (wrappedEntry !== null) return;
2098
+ originalComp = comp;
2099
+ var Official = originalComp;
2100
+ function UnionSidebarShell(innerProps) {
2101
+ return h(UnionSidebarBoundary, { officialTree: Official, hostProps: innerProps },
2102
+ h(UnionSessionSwitcher, Object.assign({}, innerProps, { officialTree: Official, __unionCtx: ctx })));
2103
+ }
2104
+ UnionSidebarShell.displayName = 'UnionSidebarShell';
2105
+ UnionSidebarShell.__unionWrapped = true;
2106
+ UnionSidebarShell.__dshNativeTabHost = true;
2107
+ UnionSidebarShell.__unionOriginal = Official;
2108
+ occupant.component = UnionSidebarShell;
2109
+ ownedWrapper = UnionSidebarShell;
2110
+ wrappedEntry = occupant;
2111
+ console.info('[dsh-im-union] sidebar.workspaces 已自持 wrap(任务/频道两页签)');
2112
+ } catch (error) {
2113
+ console.warn('[dsh-im-union] 侧栏 wrap 失败:', error);
2114
+ } finally {
2115
+ syncing = false;
2116
+ }
2117
+ };
2118
+ sync();
2119
+ var unsubWorkspaces = typeof ctx.slots.subscribe === 'function'
2120
+ ? ctx.slots.subscribe('sidebar.workspaces', sync)
2121
+ : function () {};
2122
+ var unsubSidebar = typeof ctx.slots.subscribe === 'function'
2123
+ ? ctx.slots.subscribe('sidebar', sync)
2124
+ : function () {};
2125
+ window.addEventListener('dsh-native-sidebar-change', sync);
2126
+ // 插件加载顺序不定:250ms × 40(10s 封顶)重试,包裹成功即停。
2127
+ var retries = 0;
2128
+ var timer = window.setInterval(function () {
2129
+ sync();
2130
+ if (wrappedEntry !== null || ++retries >= 40) window.clearInterval(timer);
2131
+ }, 250);
2132
+ return function () {
2133
+ window.clearInterval(timer);
2134
+ try { unsubWorkspaces(); unsubSidebar(); } catch (_) {}
2135
+ window.removeEventListener('dsh-native-sidebar-change', sync);
2136
+ unwrap();
2137
+ };
2138
+ });
2139
+ } catch (error) {
2140
+ console.warn('[dsh-im-union] sidebar.workspaces 注入失败:', error);
2141
+ }
2142
+ }
2143
+
2144
+ var inject = ['slots', 'locale', 'sessions', 'workspaces'];
2145
+
2146
+ function apply(ctx) {
2147
+ try { mountChannelRailSelf(ctx); } catch (error) { console.warn('[dsh-im-union] 渠道视角挂载失败:', error); }
2148
+ console.info('[dsh-im-union] apply invoked, slots:', typeof ctx && ctx.slots ? Object.keys(ctx.slots).join(',') : 'none')
2149
+ installStyles();
2150
+ // apply 期顶层注册——不许关进任何回调。
2151
+ try {
2152
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
2153
+ ctx.locale.register('im-union', LOCALES);
2154
+ if (ctx.locale.dictionary) {
2155
+ // 宿主词典作为最高优先查找层(逐键翻译,miss 回本地词典);
2156
+ // 语言判定不走它——走 currentLang() 懒解析(见 LANG_PREFIXES 注释)。
2157
+ hostDictionary = ctx.locale.dictionary;
2158
+ }
2159
+ }
2160
+ } catch (error) {
2161
+ console.warn('[dsh-im-union] 语言包注册失败(回落内置词典):', error);
2162
+ }
2163
+
2164
+ var registered = false;
2165
+ // 契约实读(dsh-session-guard 同宿主先例,src/client/index.ts:147/159):
2166
+ // inject(slotName, factory) 二参必须是工厂,工厂内调 register(desc, Component);
2167
+ // 裸传对象会被宿主当工厂取 .call → `e.execute.call is not a function`。
2168
+ // 三形依序:①settings.section 独立顶层项(IM 面板信息密度大,不挤家族票位;
2169
+ // 顶层节 desc 形实读 guard 先例 index.ts:223-240:label 为惰性函数、带 priority)
2170
+ // ②overlayFallback 兜底。
2171
+ try {
2172
+ if (typeof ctx.slots.inject === 'function' && typeof ctx.slots.register === 'function') {
2173
+ ctx.slots.inject('settings.section', function () {
2174
+ console.info('[dsh-im-union] settings.section factory fired')
2175
+ return ctx.slots.register({
2176
+ name: 'settings.section',
2177
+ id: 'dsh-im-union',
2178
+ order: 42,
2179
+ priority: 5,
2180
+ label: function () {
2181
+ return t('settings.title')
2182
+ },
2183
+ inject: function () {
2184
+ return {};
2185
+ },
2186
+ }, UnionPanel)
2187
+ })
2188
+ registered = true
2189
+ console.info('[dsh-im-union] settings.section 独立顶层项已声明')
2190
+ // bundle 详情页配置卡(Dsh 0.2.0:Plugins 详情页不自动渲染,须 key=包名 注册)。
2191
+ try {
2192
+ ctx.slots.inject('plugins.bundle.config', function () {
2193
+ return ctx.slots.register(
2194
+ {
2195
+ name: 'plugins.bundle.config',
2196
+ key: 'dsh-im-union',
2197
+ inject: function () {
2198
+ return {};
2199
+ },
2200
+ },
2201
+ UnionPanel,
2202
+ )
2203
+ })
2204
+ } catch (error) {
2205
+ console.warn('[dsh-im-union] plugins.bundle.config 席位不可用:', error)
2206
+ }
2207
+ } else {
2208
+ console.warn('[dsh-im-union] ctx.slots 无 inject/register 面(老线或壳缺):', typeof ctx.slots)
2209
+ }
2210
+ } catch (error) {
2211
+ console.warn('[dsh-im-union] 槽位注册失败:', error)
2212
+ }
2213
+ if (!registered) {
2214
+ // gen1 或槽位面异常 → 浮窗兜底,保证至少有一条可视管理入口。
2215
+ overlayFallback();
2216
+ }
2217
+ }
2218
+
2219
+ exports.name = 'dsh-im-union-client';
2220
+ exports.inject = inject;
2221
+ exports.apply = apply;
2222
+ return module.exports;
2223
+ },
2224
+ });