@goodandready/dsh-messenger-gateway 0.3.22 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,1631 +1,1698 @@
1
- // dsh-messenger-gateway — browser (client) half.
2
- // Settings card on Plugins → Plugin settings tab (settings.plugin.item).
3
- // Config managed via reactive ctx.settingsScope snapshot.
4
-
5
- window.__ModuleLoader__.load({
6
- id: '@goodandready/dsh-messenger-gateway',
7
- factory: (require) => {
8
- var module = { exports: {} }
9
- var exports = module.exports
10
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
11
- const React = require('react')
12
-
13
- const NS = 'dsh-messenger-gateway'
14
- const ROUTE_PREFIX = '/dsh-messenger-gateway'
15
-
16
- const en = {
17
- title: 'Messenger gateway',
18
- description: 'Telegram bot: text, voice, photos, documents, and interactive asks.',
19
- 'settings.loading': 'Loading Messenger gateway settings…',
20
- 'settings.retry': 'Retry',
21
- 'settings.unavailable': 'Settings unavailable (host namespace is not ready).',
22
- 'header.title': 'Telegram & Multi-Messenger Gateway',
23
- 'header.sub': 'Bridge between DeepSeek Harness and Telegram: sessions, steer, forum topics, inline buttons, and optional voice notes.',
24
- 'badge.online': 'Telegram online (@{username})',
25
- 'badge.running': 'Running',
26
- 'badge.stopped': 'Stopped',
27
- 'badge.token_ok': 'Token ✓',
28
- 'badge.token_missing': 'Token missing',
29
- 'badge.pairing_count': 'Pairing: {count}',
30
- 'badge.transport': 'Transport: {mode}',
31
- save: 'Save Changes',
32
- saved: 'Settings saved successfully',
33
- saving: 'Saving…',
34
- showAdvanced: '⚙️ Advanced connection & polling settings',
35
- hideAdvanced: 'Hide advanced settings',
36
- telegram: 'Telegram Bot Configuration',
37
- telegramSub: 'Main bot credentials, whitelist, and message delivery settings.',
38
- agent: 'Agent & Reasoning Execution',
39
- agentSub: 'Model routing, instruction overrides, and turn timeout control.',
40
- media: 'Media & File Limits',
41
- mediaSub: 'Document extraction and image upload limits.',
42
- tts: 'Voice Replies (dsh-tts)',
43
- ttsSub: 'Synthesize spoken replies for Telegram voice messages.',
44
- advanced: 'Connection & Polling Internals',
45
- botToken: 'Telegram Bot Token',
46
- botTokenHint: 'Obtained via @BotFather. Leave empty to keep existing configured token.',
47
- allowedIds: 'Allowed Telegram User IDs',
48
- allowedIdsHint: 'Comma-separated list of user IDs. Leave empty to allow all users (pairing requests enabled).',
49
- textFormat: 'Text Format Rendering',
50
- textFormatHtml: 'Markdown → Telegram HTML (recommended)',
51
- textFormatPlain: 'Plain text (no formatting)',
52
- disableTelegram: 'Disable Telegram Bot',
53
- enableTelegram: 'Enable Telegram Bot',
54
- provider: 'Provider Override',
55
- providerHint: 'Leave empty to use DSH default provider',
56
- model: 'Model Override',
57
- modelHint: 'Leave empty to use DSH default model',
58
- photoOnly: 'Photo without Caption Behavior',
59
- photoPrompt: 'Wait for question (recommended)',
60
- photoRun: 'Reply immediately',
61
- instruction: 'Extra Agent Instruction',
62
- instructionHint: 'Prepended to each inbound messenger message',
63
- instructionPlaceholder: 'Empty = built-in relay instruction (Russian, concise)',
64
- maxMessageLength: 'Max Reply Length (characters)',
65
- turnTimeout: 'Agent Turn Timeout (seconds)',
66
- maxDocMb: 'Max Document Size (MB)',
67
- maxImageMb: 'Max Photo Size (MB)',
68
- maxTextKb: 'Max Extracted Text from Document (KB)',
69
- ttsEnable: 'Enable Voice Replies (requires dsh-tts)',
70
- ttsMaxChars: 'Max Spoken Characters per Voice Note',
71
- pollTimeout: 'Long Poll Timeout (seconds)',
72
- pollInterval: 'Long Poll Interval (ms)',
73
- idleTimeout: 'Idle Chat Timeout (seconds)',
74
- pairing: 'Pairing Requests & Authorization',
75
- pairingSub: 'Authorize new users when whitelist is restricted.',
76
- pairingEmpty: 'No pending pairing codes',
77
- pairingApprove: 'Approve',
78
- pairingReject: 'Reject',
79
- pairingRefresh: 'Refresh List',
80
- groupsEnable: 'Respond in Telegram Groups & Supergroups',
81
- groupMention: 'Require @mention, reply, or /command in groups',
82
- reactionsEnable: 'React with 👀 while processing turn',
83
- progressEnable: 'Progress status message (Thinking → Delete → Final)',
84
- statusIndicator: 'Bot Status Description Indicator (Online/Offline)',
85
- statusIndicatorHint: 'Sets bot short description via Telegram API (visible to all users).',
86
- transport: 'Telegram Transport Mode',
87
- transportPoll: 'Long poll (getUpdates)',
88
- transportWebhook: 'Webhook (HTTPS)',
89
- webhookUrl: 'Public Webhook HTTPS URL',
90
- webhookUrlHint: 'Public endpoint where Telegram sends updates',
91
- webhookSecret: 'Webhook Secret Token',
92
- webhookSecretHint: 'Leave empty to keep current configured secret',
93
- sessionScope: 'Group Session Scope',
94
- sessionScopeChat: 'Shared per chat/topic',
95
- sessionScopeUser: 'Per-user session (recommended)',
96
- voiceMode: 'Voice Reply Mode',
97
- voiceModeMirror: 'Mirror inbound voice (reply voice only if input was voice)',
98
- voiceModeAlways: 'Always synthesize voice note',
99
- voiceModeOff: 'Off (unless explicitly toggled via /voice)',
100
- notifyBridge: 'Notify Bridge → Telegram Home',
101
- notifyBridgeHint: 'Forward task_done/error events from web sessions into designated Telegram home',
102
- notifyHome: 'Notify Target Home Name',
103
- quickActions: 'Quick Actions Keyboard (/new, /stop, /voice, /status)',
104
- artifactPreviews: 'Render diagram and table previews in Telegram',
105
- 'diag.title': '⚡ Diagnostics & Telegram Smoke Test',
106
- 'diag.desc': 'Test connection to Telegram Bot API, measure network latency, and verify bot identity.',
107
- 'diag.smoke_btn': 'Run Telegram Smoke Test (Ping)',
108
- 'diag.smoke_testing': 'Testing Telegram API…',
109
- 'diag.smoke_ok': 'Telegram API OK! Latency: {latency} ms (@{username}, ID: {id})',
110
- 'diag.smoke_err': 'Smoke test failed: {error}',
111
- 'stats.title': '📊 Gateway Telemetry & Session Counters',
112
- 'stats.desc': 'Real-time counters from the running gateway process.',
113
- 'stats.sent': 'Messages Delivered',
114
- 'stats.errors': 'Delivery Errors',
115
- 'stats.uptime': 'Gateway Uptime',
116
- 'stats.active_chats': 'Active Chats in Memory',
117
- // Updater
118
- 'updater.title': 'Plugin Self-Updater',
119
- 'updater.desc': 'Check npmjs registry for updates and upgrade in-place.',
120
- 'updater.current': 'Current version: {version}',
121
- 'updater.checking': 'Checking for updates…',
122
- 'updater.available': 'Update available: v{version}',
123
- 'updater.upToDate': 'Up to date',
124
- 'updater.btn_check': 'Check for Updates',
125
- 'updater.btn_update': 'Update to v{version}',
126
- 'updater.updating': 'Updating…',
127
- 'updater.success': 'Updated to v{version}! Please restart DSH service if required.',
128
- 'updater.failed': 'Update failed: {error}',
129
- 'updater.check_failed': 'Update check failed: {error}',
130
- // Discord
131
- discord: 'Discord Gateway',
132
- discordSub: 'Discord bot and outbound webhook delivery settings.',
133
- enableDiscord: 'Enable Discord',
134
- disableDiscord: 'Disable Discord',
135
- discordToken: 'Discord Bot Token',
136
- discordTokenHint: 'From Discord Developer Portal. Leave empty to keep existing.',
137
- discordWebhook: 'Discord Webhook URL',
138
- discordWebhookHint: 'Incoming webhook for sending messages/notifications.',
139
- discordChannels: 'Allowed Discord Channel IDs',
140
- discordChannelsHint: 'Comma-separated channel IDs. Leave empty to allow all configured.',
141
- // Slack
142
- slack: 'Slack Gateway',
143
- slackSub: 'Slack bot and incoming webhook settings.',
144
- enableSlack: 'Enable Slack',
145
- disableSlack: 'Disable Slack',
146
- slackToken: 'Slack Bot Token (xoxb-…)',
147
- slackTokenHint: 'Bot User OAuth Token. Leave empty to keep existing.',
148
- slackWebhook: 'Slack Webhook URL',
149
- slackWebhookHint: 'Incoming webhook URL for channel notifications.',
150
- slackDefaultChannel: 'Slack Default Channel',
151
- slackDefaultChannelHint: 'Default channel or member ID for outbound messages (e.g. #general or C12345).',
152
- // Cron badge
153
- 'badge.cron_count': 'Cron: {count}',
154
- }
155
-
156
- const zh = {
157
- title: 'Messenger gateway',
158
- description: 'Telegram 机器人:文本、语音、图片、文档和交互式询问。',
159
- 'settings.loading': '正在加载 Messenger gateway 设置…',
160
- 'settings.retry': '重试',
161
- 'settings.unavailable': '设置不可用(主机命名空间未就绪)。',
162
- 'header.title': 'Telegram 与多消息平台网关',
163
- 'header.sub': 'DeepSeek Harness 与 Telegram 之间的桥梁:会话、转向提示、论坛话题、内联按钮和可选语音。',
164
- 'badge.online': 'Telegram 在线 (@{username})',
165
- 'badge.running': '运行中',
166
- 'badge.stopped': '已停止',
167
- 'badge.token_ok': '令牌有效 ✓',
168
- 'badge.token_missing': '缺少令牌',
169
- 'badge.pairing_count': '配对: {count}',
170
- 'badge.transport': '传输方式: {mode}',
171
- save: '保存更改',
172
- saved: '设置保存成功',
173
- saving: '保存中…',
174
- showAdvanced: '⚙️ 高级连接与轮询设置',
175
- hideAdvanced: '隐藏高级设置',
176
- telegram: 'Telegram 机器人设置',
177
- telegramSub: '主机器人凭证、白名单与消息发送设置。',
178
- agent: '智能体与推理行为',
179
- agentSub: '模型路由、附加指令与轮次超时设置。',
180
- media: '媒体与文件限制',
181
- mediaSub: '文档提取与图片上传限制。',
182
- tts: '语音回复 (dsh-tts)',
183
- ttsSub: '为 Telegram 语音消息合成语音回复。',
184
- advanced: '连接与轮询内部设置',
185
- botToken: 'Telegram 机器人令牌',
186
- botTokenHint: '从 @BotFather 获取。留空保持现有配置。',
187
- allowedIds: '允许的 Telegram 用户 ID',
188
- allowedIdsHint: '逗号分隔的用户 ID 列表。留空允许所有用户(启用配对请求)。',
189
- textFormat: '文本格式化渲染',
190
- textFormatHtml: 'Markdown → Telegram HTML (推荐)',
191
- textFormatPlain: '纯文本 (无格式化)',
192
- disableTelegram: '停用 Telegram 机器人',
193
- enableTelegram: '启用 Telegram 机器人',
194
- provider: '提供商覆盖',
195
- providerHint: '留空使用 DSH 默认提供商',
196
- model: '模型覆盖',
197
- modelHint: '留空使用 DSH 默认模型',
198
- photoOnly: '无附言图片处理方式',
199
- photoPrompt: '等待问题 (推荐)',
200
- photoRun: '立即回复',
201
- instruction: '额外智能体指令',
202
- instructionHint: '添加到每条来自网关的消息前',
203
- instructionPlaceholder: '留空 = 内置指令',
204
- maxMessageLength: '最大回复长度 (字符)',
205
- turnTimeout: '智能体轮次超时 (秒)',
206
- maxDocMb: '最大文档大小 (MB)',
207
- maxImageMb: '最大图片大小 (MB)',
208
- maxTextKb: '文档提取最大文本量 (KB)',
209
- ttsEnable: '启用语音回复 (需 dsh-tts)',
210
- ttsMaxChars: '每条语音消息最大字数',
211
- pollTimeout: '长轮询超时 (秒)',
212
- pollInterval: '长轮询间隔 (毫秒)',
213
- idleTimeout: '空闲会话超时 (秒)',
214
- pairing: '配对请求与授权',
215
- pairingSub: '受限白名单下授权新用户。',
216
- pairingEmpty: '暂无待处理配对码',
217
- pairingApprove: '批准',
218
- pairingReject: '拒绝',
219
- pairingRefresh: '刷新列表',
220
- groupsEnable: '在群组和超级群组中响应',
221
- groupMention: '群组中需 @提及、回复或 /命令',
222
- reactionsEnable: '处理中添加 👀 表情反应',
223
- progressEnable: '进度状态提示 (思考中 → 删除 → 最终回复)',
224
- statusIndicator: '机器人状态描述指示器 (Online/Offline)',
225
- statusIndicatorHint: '通过 Telegram API 设置机器人简介(所有人可见)。',
226
- transport: 'Telegram 传输模式',
227
- transportPoll: '长轮询 (getUpdates)',
228
- transportWebhook: 'Webhook (HTTPS)',
229
- webhookUrl: '公开 Webhook HTTPS 地址',
230
- webhookUrlHint: 'Telegram 投递更新的公开端点',
231
- webhookSecret: 'Webhook 密钥令牌',
232
- webhookSecretHint: '留空保持当前配置的密钥',
233
- sessionScope: '群组会话范围',
234
- sessionScopeChat: '每个聊天/话题共享',
235
- sessionScopeUser: '每用户独立会话 (推荐)',
236
- voiceMode: '语音回复模式',
237
- voiceModeMirror: '镜像输入语音 (仅语音输入时回复语音)',
238
- voiceModeAlways: '始终合成语音回复',
239
- voiceModeOff: '关闭 (除非通过 /voice 启用)',
240
- notifyBridge: '通知网桥 → Telegram Home',
241
- notifyBridgeHint: '将 Web 会话的 task_done/error 事件转发至指定频道/话题',
242
- notifyHome: '目标频道名称 (Home)',
243
- quickActions: '快捷操作键盘 (/new, /stop, /voice, /status)',
244
- artifactPreviews: '在 Telegram 中预览图表与表格',
245
- 'diag.title': '⚡ 诊断与连通性检查 (Smoke test)',
246
- 'diag.desc': '检查 Telegram Bot API 连接、测量延迟并验证机器人名称。',
247
- 'diag.smoke_btn': '测试 Telegram 连接 (Ping)',
248
- 'diag.smoke_testing': '正在连接 Telegram API…',
249
- 'diag.smoke_ok': 'Telegram API 连接成功!延迟: {latency} 毫秒 (@{username}, ID: {id})',
250
- 'diag.smoke_err': '连通性检查失败: {error}',
251
- 'stats.title': '📊 网关遥测与计数器',
252
- 'stats.desc': '运行中网关进程的实时统计。',
253
- 'stats.sent': '已发送消息',
254
- 'stats.errors': '发送错误数',
255
- 'stats.uptime': '运行时间',
256
- 'stats.active_chats': '内存中活跃聊天数',
257
- // Updater
258
- 'updater.title': '插件自更新',
259
- 'updater.desc': '从 npmjs 仓库检查更新并直接就地升级。',
260
- 'updater.current': '当前版本: {version}',
261
- 'updater.checking': '正在检查更新…',
262
- 'updater.available': '发现新版本: v{version}',
263
- 'updater.upToDate': '已是最新版本',
264
- 'updater.btn_check': '检查更新',
265
- 'updater.btn_update': '更新至 v{version}',
266
- 'updater.updating': '正在更新…',
267
- 'updater.success': '已更新至 v{version}!如需生效请重启 DSH 服务。',
268
- 'updater.failed': '更新失败: {error}',
269
- 'updater.check_failed': '检查更新失败: {error}',
270
- // Discord
271
- discord: 'Discord 网关',
272
- discordSub: 'Discord 机器人与出站 Webhook 设置。',
273
- enableDiscord: '启用 Discord',
274
- disableDiscord: '停用 Discord',
275
- discordToken: 'Discord 机器人令牌',
276
- discordTokenHint: '从 Discord 开发者平台获取。留空保持现有配置。',
277
- discordWebhook: 'Discord Webhook URL',
278
- discordWebhookHint: '用于发送消息或通知的传入 Webhook。',
279
- discordChannels: '允许的 Discord 频道 ID',
280
- discordChannelsHint: '逗号分隔的频道 ID。留空允许所有已配置频道。',
281
- // Slack
282
- slack: 'Slack 网关',
283
- slackSub: 'Slack 机器人与 Webhook 设置。',
284
- enableSlack: '启用 Slack',
285
- disableSlack: '停用 Slack',
286
- slackToken: 'Slack 机器人令牌 (xoxb-…)',
287
- slackTokenHint: 'Bot 用户 OAuth 令牌。留空保持现有配置。',
288
- slackWebhook: 'Slack Webhook URL',
289
- slackWebhookHint: '用于频道通知的传入 Webhook URL。',
290
- slackDefaultChannel: 'Slack 默认频道',
291
- slackDefaultChannelHint: '出站消息的默认频道或成员 ID (例如 #general 或 C12345)。',
292
- // Cron badge
293
- 'badge.cron_count': '定时任务: {count}',
294
- }
295
-
296
- function useActiveLocale(ctx) {
297
- const loc = ctx?.get?.('locale') || ctx?.locale
298
- return React.useSyncExternalStore(
299
- React.useMemo(() => (cb) => (loc ? loc.subscribe(cb) : () => {}), [loc]),
300
- React.useCallback(() => {
301
- if (loc) {
302
- const active = loc.getSnapshot?.()?.active
303
- if (typeof active === 'string' && active) return active
304
- }
305
- return typeof navigator !== 'undefined' ? String(navigator.language || '').slice(0, 2) : 'en'
306
- }, [loc]),
307
- )
308
- }
309
-
310
- function makeT(locale) {
311
- const DICT = String(locale || '').startsWith('zh') ? zh : en
312
- return (key, params) => {
313
- let text = DICT[key] || en[key] || key
314
- if (params && typeof params === 'object') {
315
- for (const k of Object.keys(params)) {
316
- text = text.replace(new RegExp('\\{' + k + '\\}', 'g'), String(params[k]))
317
- }
318
- }
319
- return text
320
- }
321
- }
322
-
323
- function ensureCss() {
324
- if (typeof document === 'undefined') return
325
- const id = 'dsh-messenger-gateway-full-css'
326
- if (document.getElementById(id)) return
327
- const style = document.createElement('style')
328
- style.id = id
329
- style.dataset.dshPlugin = NS
330
- style.textContent = `
331
- .msgw-page{display:flex;flex-direction:column;gap:16px;color:var(--dsw-alias-label-primary);font-family:inherit}
332
- .msgw-header{display:flex;flex-direction:column;gap:4px;padding-bottom:12px;border-bottom:1px solid var(--dsw-alias-border-l2)}
333
- .msgw-page-title{font-size:18px;font-weight:700;display:flex;align-items:center;gap:10px;flex-wrap:wrap;color:var(--dsw-alias-label-primary)}
334
- .msgw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
335
-
336
- .msgw-badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:12px;display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
337
- .msgw-badge-ok{background:rgba(16,185,129,0.15);color:var(--dsw-alias-state-success-primary, #10b981);border:1px solid rgba(16,185,129,0.3)}
338
- .msgw-badge-warn{background:rgba(245,158,11,0.15);color:var(--dsw-alias-state-warning-primary, #f59e0b);border:1px solid rgba(245,158,11,0.3)}
339
- .msgw-badge-bad{background:rgba(239,68,68,0.15);color:var(--dsw-alias-state-error-primary, #ef4444);border:1px solid rgba(239,68,68,0.3)}
340
-
341
- .msgw-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:12px}
342
- .msgw-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
343
- .msgw-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4;margin-top:-6px}
344
-
345
- .msgw-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
346
- .msgw-field{display:flex;flex-direction:column;gap:6px}
347
- .msgw-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
348
- .msgw-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.3}
349
-
350
- .msgw-input,.msgw-select,.msgw-textarea{width:100%;box-sizing:border-box;height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font:inherit;font-size:13px;outline:none;transition:border-color .15s}
351
- .msgw-input:focus,.msgw-select:focus,.msgw-textarea:focus{border-color:var(--dsw-alias-label-primary)}
352
- .msgw-textarea{height:auto;min-height:72px;padding:8px 12px}
353
- .msgw-check{display:flex;gap:8px;align-items:center;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}
354
-
355
- .msgw-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
356
- .msgw-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
357
- .msgw-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
358
- .msgw-btn-primary:hover:not(:disabled){opacity:0.88}
359
- .msgw-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
360
- .msgw-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.5)}
361
- .msgw-btn-mini{padding:4px 10px;font-size:12px;border-radius:6px}
362
- .msgw-btn:disabled{opacity:0.5;cursor:not-allowed}
363
-
364
- .msgw-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px;border:1px solid rgba(16,185,129,0.25)}
365
- .msgw-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px;border:1px solid rgba(239,68,68,0.25)}
366
- .msgw-banner-warning{padding:12px 16px;border-radius:8px;background:rgba(245,158,11,0.12);border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
367
-
368
- .msgw-stat-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:10px}
369
- .msgw-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
370
- .msgw-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
371
- .msgw-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
372
-
373
- .msgw-table{width:100%;border-collapse:collapse;margin-top:4px}
374
- .msgw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
375
- .msgw-table td{padding:8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
376
- .msgw-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
377
- .msgw-preview{padding:12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2)}
378
- `
379
- document.head.appendChild(style)
380
- }
381
-
382
- function createErrorBoundary() {
383
- if (!React || typeof React.Component !== 'function') {
384
- return function NoopBoundary(props) { return props?.children || null }
385
- }
386
- return class ErrorBoundary extends React.Component {
387
- constructor(props) {
388
- super(props)
389
- this.state = { hasError: false, error: null }
390
- }
391
- static getDerivedStateFromError(error) {
392
- return { hasError: true, error }
393
- }
394
- componentDidCatch(error, errorInfo) {
395
- console.error('[dsh-messenger-gateway] React UI Error:', error, errorInfo)
396
- }
397
- render() {
398
- if (this.state.hasError) {
399
- return React.createElement(
400
- 'div',
401
- { className: 'msgw-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
402
- React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ Messenger Gateway UI Error:'),
403
- React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
404
- React.createElement(
405
- 'button',
406
- {
407
- type: 'button',
408
- className: 'msgw-btn msgw-btn-mini',
409
- style: { marginTop: '10px' },
410
- onClick: () => this.setState({ hasError: false, error: null }),
411
- },
412
- 'Retry'
413
- )
414
- )
415
- }
416
- return this.props?.children || null
417
- }
418
- }
419
- }
420
- const ErrorBoundary = createErrorBoundary()
421
-
422
- function FallbackChevron() {
423
- return React.createElement(
424
- 'svg',
425
- { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
426
- React.createElement('polyline', { points: '6 9 12 15 18 9' })
427
- )
428
- }
429
- const Chevron = FallbackChevron
430
-
431
- function idsToText(ids) { return (Array.isArray(ids) ? ids : []).map(String).filter(Boolean).join(', ') }
432
- function textToIds(text) {
433
- return String(text || '').split(/[\s,]+/).map((s) => s.trim()).filter(Boolean).map(Number).filter((n) => Number.isFinite(n))
434
- }
435
- function bytesToMb(n) { return Math.round(Number(n || 0) / (1024 * 1024)) }
436
- function mbToBytes(n) { return Math.max(1, Math.round(Number(n || 1))) * 1024 * 1024 }
437
-
438
- function Field({ label, hint, children }) {
439
- return React.createElement('div', { className: 'msgw-field' },
440
- React.createElement('label', { className: 'msgw-label' }, label),
441
- hint ? React.createElement('div', { className: 'msgw-hint' }, hint) : null,
442
- children,
443
- )
444
- }
445
-
446
- const SETTINGS_KEYS = ['enabled', 'telegram', 'discord', 'slack', 'agent', 'media', 'tts']
447
-
448
- function draftFromStored(s) {
449
- const src = (s && typeof s === 'object') ? s : {}
450
- const t = src.telegram || {}
451
- const d = src.discord || {}
452
- const sl = src.slack || {}
453
- const a = src.agent || {}
454
- const m = src.media || {}
455
- const tts = src.tts || {}
456
- return {
457
- enabled: src.enabled !== false,
458
- telegram: {
459
- enabled: t.enabled === true,
460
- allowedUserIds: Array.isArray(t.allowedUserIds) ? t.allowedUserIds : [],
461
- pollTimeoutSeconds: Number(t.pollTimeoutSeconds) || 50,
462
- pollIntervalMs: Number(t.pollIntervalMs) || 500,
463
- commands: Array.isArray(t.commands) ? t.commands : [],
464
- textFormat: t.textFormat === 'plain' ? 'plain' : 'html',
465
- homeChatId: t.homeChatId != null ? t.homeChatId : '',
466
- homeThreadId: Number(t.homeThreadId) || 0,
467
- homes: Array.isArray(t.homes) ? t.homes : [],
468
- pairingEnabled: t.pairingEnabled !== false,
469
- streaming: t.streaming === true,
470
- streamEditIntervalMs: Number(t.streamEditIntervalMs) || 1200,
471
- progressEnabled: t.progressEnabled !== false,
472
- approvalsEnabled: t.approvalsEnabled !== false,
473
- groupsEnabled: t.groupsEnabled !== false,
474
- groupRequireMention: t.groupRequireMention !== false,
475
- reactionsEnabled: t.reactionsEnabled !== false,
476
- statusIndicator: t.statusIndicator === true,
477
- statusOnline: t.statusOnline || 'Online',
478
- statusOffline: t.statusOffline || 'Offline',
479
- transport: t.transport === 'webhook' ? 'webhook' : 'poll',
480
- webhookUrl: t.webhookUrl || '',
481
- webhookPath: t.webhookPath || '/dsh-messenger-gateway/telegram/webhook',
482
- voiceMode: t.voiceMode || 'mirror',
483
- quickActions: t.quickActions === true,
484
- artifactPreviews: t.artifactPreviews !== false,
485
- notifyBridge: t.notifyBridge || { enabled: false, events: ['task_done', 'error'], home: 'default', excludeSessionPrefixes: ['msgw-'] },
486
- alerts: t.alerts || { enabled: false, chatId: '', threadId: 0, home: '', events: ['error', 'pairing'] },
487
- botToken: t.botToken || '',
488
- webhookSecret: t.webhookSecret || '',
489
- },
490
- discord: {
491
- enabled: d.enabled === true,
492
- botToken: d.botToken || '',
493
- webhookUrl: d.webhookUrl || '',
494
- allowedChannelIds: Array.isArray(d.allowedChannelIds) ? d.allowedChannelIds : [],
495
- },
496
- slack: {
497
- enabled: sl.enabled === true,
498
- botToken: sl.botToken || '',
499
- webhookUrl: sl.webhookUrl || '',
500
- defaultChannel: sl.defaultChannel || '',
501
- },
502
- agent: {
503
- provider: a.provider || '',
504
- model: a.model || '',
505
- instructionPrefix: a.instructionPrefix || '',
506
- maxMessageLength: Number(a.maxMessageLength) || 4000,
507
- turnTimeoutMs: Number(a.turnTimeoutMs) || 600000,
508
- idleTimeoutMs: Number(a.idleTimeoutMs) || 3600000,
509
- photoOnlyMode: a.photoOnlyMode || 'prompt',
510
- sessionScope: a.sessionScope || 'user',
511
- },
512
- media: {
513
- maxDocBytes: Number(m.maxDocBytes) || 20971520,
514
- maxImageBytes: Number(m.maxImageBytes) || 20971520,
515
- maxTextInjectBytes: Number(m.maxTextInjectBytes) || 102400,
516
- },
517
- tts: {
518
- enabled: tts.enabled === true,
519
- maxChars: Number(tts.maxChars) || 4000,
520
- voiceSummary: tts.voiceSummary === true,
521
- },
522
- }
523
- }
524
-
525
- function SettingsPage(props) {
526
- const ctx = props?.ctx
527
- const t = props?.t || makeT(props?.locale)
528
-
529
- const [token, setToken] = React.useState('')
530
- const [webhookSecret, setWebhookSecret] = React.useState('')
531
- const [allowText, setAllowText] = React.useState('')
532
- const [discordToken, setDiscordToken] = React.useState('')
533
- const [slackToken, setSlackToken] = React.useState('')
534
- const [discordChannelsText, setDiscordChannelsText] = React.useState('')
535
- const [updateState, setUpdateState] = React.useState({
536
- checking: false,
537
- updating: false,
538
- currentVersion: '',
539
- latestVersion: '',
540
- updateAvailable: false,
541
- error: '',
542
- notice: '',
543
- })
544
- const [pending, setPending] = React.useState([])
545
- const [status, setStatus] = React.useState(null)
546
- const [smokeResult, setSmokeResult] = React.useState(null)
547
- const [err, setErr] = React.useState('')
548
- const [msg, setMsg] = React.useState('')
549
- const [busy, setBusy] = React.useState('')
550
- const [showAdvanced, setShowAdvanced] = React.useState(false)
551
-
552
- React.useEffect(() => {
553
- ensureCss()
554
- }, [])
555
-
556
- const scope = React.useMemo(() => {
557
- const settingsScope = ctx?.get?.('settingsScope') || ctx?.settingsScope
558
- return settingsScope ? settingsScope.bind({ namespace: NS }) : undefined
559
- }, [ctx])
560
-
561
- const snapshot = React.useSyncExternalStore(
562
- React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
563
- React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
564
- React.useCallback(() => ({ status: 'loading' }), []),
565
- )
566
-
567
- const snapStatus = (snapshot && snapshot.status) || 'loading'
568
- const stored = (snapshot && snapshot.value) || {}
569
- const [cfg, setCfg] = React.useState(null)
570
-
571
- React.useEffect(() => {
572
- if (snapStatus === 'ready' && cfg === null) {
573
- setCfg(draftFromStored(stored))
574
- setAllowText(idsToText(stored?.telegram?.allowedUserIds))
575
- setDiscordChannelsText(idsToText(stored?.discord?.allowedChannelIds))
576
- }
577
- }, [snapStatus, stored, cfg])
578
-
579
- const loadStatus = React.useCallback(async () => {
580
- try {
581
- const res = await fetch(`${ROUTE_PREFIX}/status`, { cache: 'no-store' })
582
- const data = await res.json().catch(() => ({}))
583
- if (res.ok && data.ok) {
584
- setStatus(data)
585
- }
586
- } catch (_) {}
587
- }, [])
588
-
589
- const loadPairing = React.useCallback(async () => {
590
- try {
591
- const res = await fetch(`${ROUTE_PREFIX}/pairing`, { credentials: 'same-origin', cache: 'no-store' })
592
- const data = await res.json().catch(() => ({}))
593
- if (res.ok && data.ok) setPending(Array.isArray(data.pending) ? data.pending : [])
594
- } catch (_) {}
595
- }, [])
596
-
597
- React.useEffect(() => {
598
- loadStatus()
599
- loadPairing()
600
- const timer = setInterval(() => {
601
- loadStatus()
602
- loadPairing()
603
- }, 10000)
604
- return () => clearInterval(timer)
605
- }, [loadStatus, loadPairing])
606
-
607
- const loadUpdateStatus = React.useCallback(async () => {
608
- setUpdateState((s) => ({ ...s, checking: true, error: '' }))
609
- try {
610
- const res = await fetch(`${ROUTE_PREFIX}/update`, { cache: 'no-store' })
611
- const data = await res.json().catch(() => ({}))
612
- if (res.ok && data.ok) {
613
- setUpdateState((s) => ({
614
- ...s,
615
- checking: false,
616
- currentVersion: data.currentVersion || '',
617
- latestVersion: data.latestVersion || '',
618
- updateAvailable: !!data.updateAvailable,
619
- }))
620
- } else {
621
- setUpdateState((s) => ({ ...s, checking: false, error: data.error || `HTTP ${res.status}` }))
622
- }
623
- } catch (e) {
624
- setUpdateState((s) => ({ ...s, checking: false, error: String(e.message || e) }))
625
- }
626
- }, [])
627
-
628
- const handleTriggerUpdate = async () => {
629
- if (updateState.updating) return
630
- setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
631
- try {
632
- const res = await fetch(`${ROUTE_PREFIX}/update`, {
633
- method: 'POST',
634
- credentials: 'same-origin',
635
- headers: {
636
- 'Content-Type': 'application/json',
637
- 'x-dsh-plugin-update': '1',
638
- },
639
- })
640
- const data = await res.json().catch(() => ({}))
641
- if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
642
- setUpdateState((s) => ({
643
- ...s,
644
- updating: false,
645
- updateAvailable: false,
646
- currentVersion: data.updatedVersion || s.latestVersion,
647
- notice: t('updater.success', { version: data.updatedVersion || s.latestVersion }),
648
- }))
649
- } catch (e) {
650
- setUpdateState((s) => ({
651
- ...s,
652
- updating: false,
653
- error: t('updater.failed', { error: String(e.message || e) }),
654
- }))
655
- }
656
- }
657
-
658
- React.useEffect(() => {
659
- loadUpdateStatus()
660
- }, [loadUpdateStatus])
661
-
662
- const handleSmoke = async () => {
663
- setBusy('smoke')
664
- setErr('')
665
- setMsg('')
666
- setSmokeResult(null)
667
- try {
668
- const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
669
- method: 'POST',
670
- credentials: 'same-origin',
671
- headers: { 'Content-Type': 'application/json' },
672
- body: JSON.stringify({ timeoutMs: 8000 }),
673
- })
674
- const data = await res.json().catch(() => ({}))
675
- if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
676
- setSmokeResult(data)
677
- setMsg(t('diag.smoke_ok', { latency: data.latencyMs, username: data.botUsername, id: data.botId }))
678
- await loadStatus()
679
- } catch (e) {
680
- setErr(t('diag.smoke_err', { error: String(e.message || e) }))
681
- } finally {
682
- setBusy('')
683
- }
684
- }
685
-
686
- const mergePatch = (base, patch) => ({
687
- ...base,
688
- ...patch,
689
- telegram: { ...base.telegram, ...(patch.telegram || {}) },
690
- discord: { ...base.discord, ...(patch.discord || {}) },
691
- slack: { ...base.slack, ...(patch.slack || {}) },
692
- tts: { ...base.tts, ...(patch.tts || {}) },
693
- agent: { ...base.agent, ...(patch.agent || {}) },
694
- media: { ...base.media, ...(patch.media || {}) },
695
- })
696
-
697
- const save = async (patch = {}) => {
698
- if (!scope || !cfg) return
699
- setBusy('save')
700
- setErr('')
701
- setMsg('')
702
- try {
703
- let next = mergePatch(cfg, patch)
704
- if (token.trim()) next.telegram = { ...next.telegram, botToken: token.trim() }
705
- if (webhookSecret.trim()) next.telegram = { ...next.telegram, webhookSecret: webhookSecret.trim() }
706
- next.telegram.allowedUserIds = textToIds(allowText)
707
- if (discordToken.trim()) next.discord = { ...next.discord, botToken: discordToken.trim() }
708
- if (slackToken.trim()) next.slack = { ...next.slack, botToken: slackToken.trim() }
709
- next.discord.allowedChannelIds = textToIds(discordChannelsText)
710
-
711
- const broken = []
712
- for (const key of SETTINGS_KEYS) {
713
- if (next[key] !== undefined) {
714
- try {
715
- await scope.set(key, next[key])
716
- } catch (e) {
717
- broken.push(key + ': ' + (e && e.message || String(e)))
718
- }
719
- }
720
- }
721
- if (broken.length) {
722
- setErr('Save failed — ' + broken.join('; '))
723
- return
724
- }
725
- setCfg(next)
726
- setToken('')
727
- setWebhookSecret('')
728
- setDiscordToken('')
729
- setSlackToken('')
730
- setMsg(t('saved'))
731
- setTimeout(() => setMsg(''), 3000)
732
- await loadPairing()
733
- await loadStatus()
734
- } catch (e) {
735
- setErr(String(e.message || e))
736
- } finally {
737
- setBusy('')
738
- }
739
- }
740
-
741
- if (snapStatus === 'loading') {
742
- return React.createElement('div', { className: 'msgw-page' }, t('settings.loading'))
743
- }
744
- if (snapStatus !== 'ready') {
745
- return React.createElement('div', { className: 'msgw-page' },
746
- React.createElement('div', { className: 'msgw-banner-warning' }, t('settings.unavailable'))
747
- )
748
- }
749
- if (!cfg) {
750
- return React.createElement('div', { className: 'msgw-page' }, t('settings.loading'))
751
- }
752
-
753
- const isRunning = Boolean(status?.running)
754
- const botUsername = status?.botUsername || (status?.config?.telegram?.botUsername) || ''
755
- const hasToken = Boolean(status?.config?.telegram?.botTokenConfigured || cfg.telegram?.botTokenConfigured || token.trim())
756
- const transportMode = cfg.telegram?.transport || 'poll'
757
- const pendingCount = pending.length
758
-
759
- return React.createElement(
760
- 'div',
761
- { className: 'msgw-page' },
762
-
763
- // Header with badges
764
- React.createElement(
765
- 'div',
766
- { className: 'msgw-header' },
767
- React.createElement(
768
- 'div',
769
- { className: 'msgw-page-title' },
770
- `💬 ${t('header.title')}`,
771
- React.createElement(
772
- 'span',
773
- { className: `msgw-badge ${isRunning ? 'msgw-badge-ok' : 'msgw-badge-bad'}` },
774
- isRunning
775
- ? (botUsername ? t('badge.online', { username: botUsername }) : t('badge.running'))
776
- : t('badge.stopped')
777
- ),
778
- React.createElement(
779
- 'span',
780
- { className: `msgw-badge ${hasToken ? 'msgw-badge-ok' : 'msgw-badge-warn'}` },
781
- hasToken ? t('badge.token_ok') : t('badge.token_missing')
782
- ),
783
- React.createElement(
784
- 'span',
785
- { className: 'msgw-badge msgw-badge-ok' },
786
- t('badge.transport', { mode: transportMode })
787
- ),
788
- pendingCount > 0
789
- ? React.createElement(
790
- 'span',
791
- { className: 'msgw-badge msgw-badge-warn' },
792
- t('badge.pairing_count', { count: pendingCount })
793
- )
794
- : null,
795
- (status?.scheduledCount > 0)
796
- ? React.createElement(
797
- 'span',
798
- { className: 'msgw-badge msgw-badge-ok' },
799
- t('badge.cron_count', { count: status.scheduledCount })
800
- )
801
- : null
802
- ),
803
- React.createElement('div', { className: 'msgw-page-sub' }, t('header.sub'))
804
- ),
805
-
806
- // Global Alert banners
807
- err ? React.createElement('div', { className: 'msgw-alert-err' }, err) : null,
808
- msg ? React.createElement('div', { className: 'msgw-alert-ok' }, msg) : null,
809
-
810
- // Self-Updater Card
811
- React.createElement(
812
- 'div',
813
- { className: 'msgw-section-card' },
814
- React.createElement(
815
- 'div',
816
- { className: 'msgw-section-title' },
817
- React.createElement('span', null, `🚀 ${t('updater.title')}`),
818
- React.createElement(
819
- 'div',
820
- { className: 'msgw-row' },
821
- React.createElement(
822
- 'button',
823
- {
824
- type: 'button',
825
- className: 'msgw-btn msgw-btn-mini',
826
- disabled: updateState.checking || updateState.updating,
827
- onClick: loadUpdateStatus,
828
- },
829
- updateState.checking ? t('updater.checking') : t('updater.btn_check')
830
- ),
831
- updateState.updateAvailable
832
- ? React.createElement(
833
- 'button',
834
- {
835
- type: 'button',
836
- className: 'msgw-btn msgw-btn-mini msgw-btn-primary',
837
- disabled: updateState.updating,
838
- onClick: handleTriggerUpdate,
839
- },
840
- updateState.updating ? t('updater.updating') : t('updater.btn_update', { version: updateState.latestVersion })
841
- )
842
- : null
843
- )
844
- ),
845
- React.createElement('div', { className: 'msgw-section-desc' }, t('updater.desc')),
846
- React.createElement(
847
- 'div',
848
- { className: 'msgw-row', style: { fontSize: '13px' } },
849
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } },
850
- t('updater.current', { version: updateState.currentVersion || status?.version || '...' })
851
- ),
852
- updateState.checking
853
- ? React.createElement('span', { className: 'msgw-badge msgw-badge-warn' }, t('updater.checking'))
854
- : updateState.updateAvailable
855
- ? React.createElement('span', { className: 'msgw-badge msgw-badge-warn' }, t('updater.available', { version: updateState.latestVersion }))
856
- : updateState.currentVersion && !updateState.error
857
- ? React.createElement('span', { className: 'msgw-badge msgw-badge-ok' }, `✓ ${t('updater.upToDate')}`)
858
- : null
859
- ),
860
- updateState.notice ? React.createElement('div', { className: 'msgw-alert-ok' }, updateState.notice) : null,
861
- updateState.error ? React.createElement('div', { className: 'msgw-alert-err' }, updateState.error) : null
862
- ),
863
-
864
- // Card 1: Telegram Connection & Bot
865
- React.createElement(
866
- 'div',
867
- { className: 'msgw-section-card' },
868
- React.createElement(
869
- 'div',
870
- { className: 'msgw-section-title' },
871
- React.createElement('span', null, `🤖 ${t('telegram')}`),
872
- React.createElement(
873
- 'button',
874
- {
875
- type: 'button',
876
- className: `msgw-btn msgw-btn-mini ${cfg.telegram.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
877
- disabled: !!busy,
878
- onClick: () => save({ telegram: { enabled: !cfg.telegram.enabled } }),
879
- },
880
- cfg.telegram.enabled ? t('disableTelegram') : t('enableTelegram')
881
- )
882
- ),
883
- React.createElement('div', { className: 'msgw-section-desc' }, t('telegramSub')),
884
- React.createElement(Field, { label: t('botToken'), hint: t('botTokenHint') },
885
- React.createElement('input', {
886
- type: 'password',
887
- className: 'msgw-input',
888
- value: token,
889
- placeholder: hasToken ? '••••••••••••••••••••' : '123456789:ABCdefGHI...',
890
- onChange: (e) => setToken(e.target.value),
891
- })
892
- ),
893
- React.createElement(Field, { label: t('allowedIds'), hint: t('allowedIdsHint') },
894
- React.createElement('textarea', {
895
- className: 'msgw-textarea',
896
- value: allowText,
897
- onChange: (e) => setAllowText(e.target.value),
898
- rows: 2,
899
- placeholder: '108191529, 987654321',
900
- })
901
- ),
902
- React.createElement(Field, { label: t('textFormat') },
903
- React.createElement(
904
- 'select',
905
- {
906
- className: 'msgw-select',
907
- value: cfg.telegram?.textFormat || 'html',
908
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, textFormat: e.target.value } }),
909
- },
910
- React.createElement('option', { value: 'html' }, t('textFormatHtml')),
911
- React.createElement('option', { value: 'plain' }, t('textFormatPlain'))
912
- )
913
- ),
914
- React.createElement('label', { className: 'msgw-check' },
915
- React.createElement('input', {
916
- type: 'checkbox',
917
- checked: cfg.telegram?.groupsEnabled !== false,
918
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupsEnabled: e.target.checked } }),
919
- }),
920
- t('groupsEnable')
921
- ),
922
- React.createElement('label', { className: 'msgw-check' },
923
- React.createElement('input', {
924
- type: 'checkbox',
925
- checked: cfg.telegram?.groupRequireMention !== false,
926
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupRequireMention: e.target.checked } }),
927
- }),
928
- t('groupMention')
929
- ),
930
- React.createElement('label', { className: 'msgw-check' },
931
- React.createElement('input', {
932
- type: 'checkbox',
933
- checked: cfg.telegram?.reactionsEnabled !== false,
934
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, reactionsEnabled: e.target.checked } }),
935
- }),
936
- t('reactionsEnable')
937
- ),
938
- React.createElement('label', { className: 'msgw-check' },
939
- React.createElement('input', {
940
- type: 'checkbox',
941
- checked: cfg.telegram?.progressEnabled !== false,
942
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, progressEnabled: e.target.checked } }),
943
- }),
944
- t('progressEnable')
945
- ),
946
- React.createElement('label', { className: 'msgw-check' },
947
- React.createElement('input', {
948
- type: 'checkbox',
949
- checked: cfg.telegram?.quickActions !== false,
950
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, quickActions: e.target.checked } }),
951
- }),
952
- t('quickActions')
953
- ),
954
- React.createElement('label', { className: 'msgw-check' },
955
- React.createElement('input', {
956
- type: 'checkbox',
957
- checked: cfg.telegram?.artifactPreviews !== false,
958
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, artifactPreviews: e.target.checked } }),
959
- }),
960
- t('artifactPreviews')
961
- )
962
- ),
963
-
964
- // Card 2: Diagnostics & Telegram Smoke Test
965
- React.createElement(
966
- 'div',
967
- { className: 'msgw-section-card' },
968
- React.createElement('div', { className: 'msgw-section-title' }, t('diag.title')),
969
- React.createElement('div', { className: 'msgw-section-desc' }, t('diag.desc')),
970
- React.createElement(
971
- 'div',
972
- { className: 'msgw-row' },
973
- React.createElement(
974
- 'button',
975
- {
976
- type: 'button',
977
- className: 'msgw-btn msgw-btn-primary',
978
- disabled: !!busy || !hasToken,
979
- onClick: handleSmoke,
980
- },
981
- busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
982
- )
983
- ),
984
- smokeResult
985
- ? React.createElement(
986
- 'div',
987
- { className: 'msgw-preview' },
988
- `✅ Latency: ${smokeResult.latencyMs} ms | Bot: @${smokeResult.botUsername || 'unknown'} (ID: ${smokeResult.botId})`
989
- )
990
- : null
991
- ),
992
-
993
- // Card 3: Telemetry & Gateway Counters
994
- status?.stats ? React.createElement(
995
- 'div',
996
- { className: 'msgw-section-card' },
997
- React.createElement('div', { className: 'msgw-section-title' }, t('stats.title')),
998
- React.createElement('div', { className: 'msgw-section-desc' }, t('stats.desc')),
999
- React.createElement(
1000
- 'div',
1001
- { className: 'msgw-stat-grid' },
1002
- React.createElement(
1003
- 'div',
1004
- { className: 'msgw-stat-box' },
1005
- React.createElement('div', { className: 'msgw-stat-val' }, String(status.stats.sent || 0)),
1006
- React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.sent'))
1007
- ),
1008
- React.createElement(
1009
- 'div',
1010
- { className: 'msgw-stat-box' },
1011
- React.createElement('div', { className: 'msgw-stat-val' }, String(status.stats.errors || 0)),
1012
- React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.errors'))
1013
- ),
1014
- React.createElement(
1015
- 'div',
1016
- { className: 'msgw-stat-box' },
1017
- React.createElement('div', { className: 'msgw-stat-val' }, String(status.activeChats || 0)),
1018
- React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.active_chats'))
1019
- ),
1020
- React.createElement(
1021
- 'div',
1022
- { className: 'msgw-stat-box' },
1023
- React.createElement('div', { className: 'msgw-stat-val' }, `${status.uptimeSec || 0}s`),
1024
- React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.uptime'))
1025
- )
1026
- )
1027
- ) : null,
1028
-
1029
- // Card 4: Agent & Turn Execution
1030
- React.createElement(
1031
- 'div',
1032
- { className: 'msgw-section-card' },
1033
- React.createElement('div', { className: 'msgw-section-title' }, `🧠 ${t('agent')}`),
1034
- React.createElement('div', { className: 'msgw-section-desc' }, t('agentSub')),
1035
- React.createElement(Field, { label: t('provider'), hint: t('providerHint') },
1036
- React.createElement('input', {
1037
- className: 'msgw-input',
1038
- value: cfg.agent?.provider || '',
1039
- onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, provider: e.target.value } }),
1040
- })
1041
- ),
1042
- React.createElement(Field, { label: t('model'), hint: t('modelHint') },
1043
- React.createElement('input', {
1044
- className: 'msgw-input',
1045
- value: cfg.agent?.model || '',
1046
- onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, model: e.target.value } }),
1047
- })
1048
- ),
1049
- React.createElement(Field, { label: t('photoOnly') },
1050
- React.createElement(
1051
- 'select',
1052
- {
1053
- className: 'msgw-select',
1054
- value: cfg.agent?.photoOnlyMode || 'prompt',
1055
- onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, photoOnlyMode: e.target.value } }),
1056
- },
1057
- React.createElement('option', { value: 'prompt' }, t('photoPrompt')),
1058
- React.createElement('option', { value: 'run' }, t('photoRun'))
1059
- )
1060
- ),
1061
- React.createElement(Field, { label: t('sessionScope') },
1062
- React.createElement(
1063
- 'select',
1064
- {
1065
- className: 'msgw-select',
1066
- value: cfg.agent?.sessionScope || 'user',
1067
- onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, sessionScope: e.target.value } }),
1068
- },
1069
- React.createElement('option', { value: 'user' }, t('sessionScopeUser')),
1070
- React.createElement('option', { value: 'chat' }, t('sessionScopeChat'))
1071
- )
1072
- ),
1073
- React.createElement(Field, { label: t('instruction'), hint: t('instructionHint') },
1074
- React.createElement('textarea', {
1075
- className: 'msgw-textarea',
1076
- value: cfg.agent?.instructionPrefix || '',
1077
- onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, instructionPrefix: e.target.value } }),
1078
- rows: 3,
1079
- placeholder: t('instructionPlaceholder'),
1080
- })
1081
- ),
1082
- React.createElement(Field, { label: t('maxMessageLength') },
1083
- React.createElement('input', {
1084
- type: 'number',
1085
- className: 'msgw-input',
1086
- min: 500,
1087
- max: 8000,
1088
- value: cfg.agent?.maxMessageLength ?? 4000,
1089
- onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, maxMessageLength: Number(e.target.value) || 4000 } }),
1090
- })
1091
- ),
1092
- React.createElement(Field, { label: t('turnTimeout') },
1093
- React.createElement('input', {
1094
- type: 'number',
1095
- className: 'msgw-input',
1096
- min: 30,
1097
- max: 3600,
1098
- value: Math.round((cfg.agent?.turnTimeoutMs ?? 600000) / 1000),
1099
- onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, turnTimeoutMs: (Number(e.target.value) || 600) * 1000 } }),
1100
- })
1101
- )
1102
- ),
1103
-
1104
- // Card 5: Media & TTS
1105
- React.createElement(
1106
- 'div',
1107
- { className: 'msgw-section-card' },
1108
- React.createElement('div', { className: 'msgw-section-title' }, `📎 ${t('media')}`),
1109
- React.createElement('div', { className: 'msgw-section-desc' }, t('mediaSub')),
1110
- React.createElement(Field, { label: t('maxDocMb') },
1111
- React.createElement('input', {
1112
- type: 'number',
1113
- className: 'msgw-input',
1114
- min: 1,
1115
- max: 50,
1116
- value: bytesToMb(cfg.media?.maxDocBytes),
1117
- onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxDocBytes: mbToBytes(e.target.value) } }),
1118
- })
1119
- ),
1120
- React.createElement(Field, { label: t('maxImageMb') },
1121
- React.createElement('input', {
1122
- type: 'number',
1123
- className: 'msgw-input',
1124
- min: 1,
1125
- max: 50,
1126
- value: bytesToMb(cfg.media?.maxImageBytes),
1127
- onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxImageBytes: mbToBytes(e.target.value) } }),
1128
- })
1129
- ),
1130
- React.createElement(Field, { label: t('maxTextKb') },
1131
- React.createElement('input', {
1132
- type: 'number',
1133
- className: 'msgw-input',
1134
- min: 16,
1135
- max: 512,
1136
- value: Math.round((cfg.media?.maxTextInjectBytes ?? 102400) / 1024),
1137
- onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxTextInjectBytes: (Number(e.target.value) || 100) * 1024 } }),
1138
- })
1139
- ),
1140
- React.createElement('div', { style: { height: '8px' } }),
1141
- React.createElement('div', { className: 'msgw-section-title' }, `🔊 ${t('tts')}`),
1142
- React.createElement('div', { className: 'msgw-section-desc' }, t('ttsSub')),
1143
- React.createElement('label', { className: 'msgw-check' },
1144
- React.createElement('input', {
1145
- type: 'checkbox',
1146
- checked: !!cfg.tts?.enabled,
1147
- onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, enabled: e.target.checked } }),
1148
- }),
1149
- t('ttsEnable')
1150
- ),
1151
- cfg.tts?.enabled ? React.createElement(Field, { label: t('ttsMaxChars') },
1152
- React.createElement('input', {
1153
- type: 'number',
1154
- className: 'msgw-input',
1155
- min: 100,
1156
- max: 8000,
1157
- value: cfg.tts?.maxChars ?? 4000,
1158
- onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, maxChars: Number(e.target.value) || 4000 } }),
1159
- })
1160
- ) : null,
1161
- React.createElement(Field, { label: t('voiceMode') },
1162
- React.createElement(
1163
- 'select',
1164
- {
1165
- className: 'msgw-select',
1166
- value: cfg.telegram?.voiceMode || 'mirror',
1167
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, voiceMode: e.target.value } }),
1168
- },
1169
- React.createElement('option', { value: 'mirror' }, t('voiceModeMirror')),
1170
- React.createElement('option', { value: 'always' }, t('voiceModeAlways')),
1171
- React.createElement('option', { value: 'off' }, t('voiceModeOff'))
1172
- )
1173
- )
1174
- ),
1175
-
1176
- // Card: Discord Gateway
1177
- React.createElement(
1178
- 'div',
1179
- { className: 'msgw-section-card' },
1180
- React.createElement(
1181
- 'div',
1182
- { className: 'msgw-section-title' },
1183
- React.createElement('span', null, `🎮 ${t('discord')}`),
1184
- React.createElement(
1185
- 'button',
1186
- {
1187
- type: 'button',
1188
- className: `msgw-btn msgw-btn-mini ${cfg.discord?.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
1189
- disabled: !!busy,
1190
- onClick: () => save({ discord: { enabled: !cfg.discord?.enabled } }),
1191
- },
1192
- cfg.discord?.enabled ? t('disableDiscord') : t('enableDiscord')
1193
- )
1194
- ),
1195
- React.createElement('div', { className: 'msgw-section-desc' }, t('discordSub')),
1196
- React.createElement(Field, { label: t('discordToken'), hint: t('discordTokenHint') },
1197
- React.createElement('input', {
1198
- type: 'password',
1199
- className: 'msgw-input',
1200
- value: discordToken,
1201
- placeholder: (status?.config?.discord?.botTokenConfigured || cfg.discord?.botTokenConfigured) ? '••••••••••••••••' : 'Discord Bot Token',
1202
- onChange: (e) => setDiscordToken(e.target.value),
1203
- })
1204
- ),
1205
- React.createElement(Field, { label: t('discordWebhook'), hint: t('discordWebhookHint') },
1206
- React.createElement('input', {
1207
- className: 'msgw-input',
1208
- value: cfg.discord?.webhookUrl || '',
1209
- placeholder: 'https://discord.com/api/webhooks/...',
1210
- onChange: (e) => setCfg({ ...cfg, discord: { ...cfg.discord, webhookUrl: e.target.value } }),
1211
- })
1212
- ),
1213
- React.createElement(Field, { label: t('discordChannels'), hint: t('discordChannelsHint') },
1214
- React.createElement('textarea', {
1215
- className: 'msgw-textarea',
1216
- value: discordChannelsText,
1217
- onChange: (e) => setDiscordChannelsText(e.target.value),
1218
- rows: 2,
1219
- placeholder: '123456789012345678, 987654321098765432',
1220
- })
1221
- )
1222
- ),
1223
-
1224
- // Card: Slack Gateway
1225
- React.createElement(
1226
- 'div',
1227
- { className: 'msgw-section-card' },
1228
- React.createElement(
1229
- 'div',
1230
- { className: 'msgw-section-title' },
1231
- React.createElement('span', null, `💼 ${t('slack')}`),
1232
- React.createElement(
1233
- 'button',
1234
- {
1235
- type: 'button',
1236
- className: `msgw-btn msgw-btn-mini ${cfg.slack?.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
1237
- disabled: !!busy,
1238
- onClick: () => save({ slack: { enabled: !cfg.slack?.enabled } }),
1239
- },
1240
- cfg.slack?.enabled ? t('disableSlack') : t('enableSlack')
1241
- )
1242
- ),
1243
- React.createElement('div', { className: 'msgw-section-desc' }, t('slackSub')),
1244
- React.createElement(Field, { label: t('slackToken'), hint: t('slackTokenHint') },
1245
- React.createElement('input', {
1246
- type: 'password',
1247
- className: 'msgw-input',
1248
- value: slackToken,
1249
- placeholder: (status?.config?.slack?.botTokenConfigured || cfg.slack?.botTokenConfigured) ? '••••••••••••••••' : 'xoxb-...',
1250
- onChange: (e) => setSlackToken(e.target.value),
1251
- })
1252
- ),
1253
- React.createElement(Field, { label: t('slackWebhook'), hint: t('slackWebhookHint') },
1254
- React.createElement('input', {
1255
- className: 'msgw-input',
1256
- value: cfg.slack?.webhookUrl || '',
1257
- placeholder: 'https://hooks.slack.com/services/...',
1258
- onChange: (e) => setCfg({ ...cfg, slack: { ...cfg.slack, webhookUrl: e.target.value } }),
1259
- })
1260
- ),
1261
- React.createElement(Field, { label: t('slackDefaultChannel'), hint: t('slackDefaultChannelHint') },
1262
- React.createElement('input', {
1263
- className: 'msgw-input',
1264
- value: cfg.slack?.defaultChannel || '',
1265
- placeholder: '#general or C12345678',
1266
- onChange: (e) => setCfg({ ...cfg, slack: { ...cfg.slack, defaultChannel: e.target.value } }),
1267
- })
1268
- )
1269
- ),
1270
-
1271
- // Card 6: Pairing Requests
1272
- React.createElement(
1273
- 'div',
1274
- { className: 'msgw-section-card' },
1275
- React.createElement(
1276
- 'div',
1277
- { className: 'msgw-section-title' },
1278
- React.createElement('span', null, `🔑 ${t('pairing')}`),
1279
- React.createElement(
1280
- 'button',
1281
- {
1282
- type: 'button',
1283
- className: 'msgw-btn msgw-btn-mini',
1284
- disabled: !!busy,
1285
- onClick: loadPairing,
1286
- },
1287
- t('pairingRefresh')
1288
- )
1289
- ),
1290
- React.createElement('div', { className: 'msgw-section-desc' }, t('pairingSub')),
1291
- !(pending && pending.length)
1292
- ? React.createElement('div', { className: 'msgw-hint' }, t('pairingEmpty'))
1293
- : React.createElement(
1294
- 'table',
1295
- { className: 'msgw-table' },
1296
- React.createElement(
1297
- 'tbody',
1298
- null,
1299
- pending.map((row) =>
1300
- React.createElement(
1301
- 'tr',
1302
- { key: row.code },
1303
- React.createElement('td', null, row.username ? `@${row.username}` : '—'),
1304
- React.createElement('td', null, `ID: ${row.userId}`),
1305
- React.createElement('td', { style: { fontFamily: 'monospace', fontWeight: 600 } }, row.code),
1306
- React.createElement(
1307
- 'td',
1308
- { style: { textAlign: 'right' } },
1309
- React.createElement(
1310
- 'button',
1311
- {
1312
- type: 'button',
1313
- className: 'msgw-btn msgw-btn-mini msgw-btn-primary',
1314
- style: { marginRight: '6px' },
1315
- disabled: !!busy,
1316
- onClick: async () => {
1317
- setBusy('approve')
1318
- setErr('')
1319
- try {
1320
- const res = await fetch(`${ROUTE_PREFIX}/pairing/approve`, {
1321
- method: 'POST',
1322
- credentials: 'same-origin',
1323
- headers: { 'Content-Type': 'application/json' },
1324
- body: JSON.stringify({ code: row.code }),
1325
- })
1326
- const data = await res.json()
1327
- if (!res.ok || !data.ok) throw new Error(data.error || res.status)
1328
- await loadPairing()
1329
- } catch (e) {
1330
- setErr(String(e.message || e))
1331
- } finally {
1332
- setBusy('')
1333
- }
1334
- },
1335
- },
1336
- t('pairingApprove')
1337
- ),
1338
- React.createElement(
1339
- 'button',
1340
- {
1341
- type: 'button',
1342
- className: 'msgw-btn msgw-btn-mini msgw-btn-danger',
1343
- disabled: !!busy,
1344
- onClick: async () => {
1345
- setBusy('reject')
1346
- setErr('')
1347
- try {
1348
- const res = await fetch(`${ROUTE_PREFIX}/pairing/reject`, {
1349
- method: 'POST',
1350
- credentials: 'same-origin',
1351
- headers: { 'Content-Type': 'application/json' },
1352
- body: JSON.stringify({ code: row.code }),
1353
- })
1354
- const data = await res.json()
1355
- if (!res.ok || !data.ok) throw new Error(data.error || res.status)
1356
- await loadPairing()
1357
- } catch (e) {
1358
- setErr(String(e.message || e))
1359
- } finally {
1360
- setBusy('')
1361
- }
1362
- },
1363
- },
1364
- t('pairingReject')
1365
- )
1366
- )
1367
- )
1368
- )
1369
- )
1370
- )
1371
- ),
1372
-
1373
- // Card 7: Advanced Connection & Webhook
1374
- React.createElement(
1375
- 'div',
1376
- { className: 'msgw-section-card' },
1377
- React.createElement(
1378
- 'button',
1379
- {
1380
- type: 'button',
1381
- className: 'msgw-btn',
1382
- style: { width: '100%', justifyContent: 'space-between' },
1383
- onClick: () => setShowAdvanced((v) => !v),
1384
- },
1385
- React.createElement('span', null, showAdvanced ? t('hideAdvanced') : t('showAdvanced')),
1386
- React.createElement('span', { style: { transform: showAdvanced ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
1387
- React.createElement(Chevron)
1388
- )
1389
- ),
1390
- showAdvanced ? React.createElement(
1391
- 'div',
1392
- { style: { display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '12px' } },
1393
- React.createElement(Field, { label: t('transport') },
1394
- React.createElement(
1395
- 'select',
1396
- {
1397
- className: 'msgw-select',
1398
- value: cfg.telegram?.transport || 'poll',
1399
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, transport: e.target.value } }),
1400
- },
1401
- React.createElement('option', { value: 'poll' }, t('transportPoll')),
1402
- React.createElement('option', { value: 'webhook' }, t('transportWebhook'))
1403
- )
1404
- ),
1405
- React.createElement(Field, { label: t('webhookUrl'), hint: t('webhookUrlHint') },
1406
- React.createElement('input', {
1407
- className: 'msgw-input',
1408
- value: cfg.telegram?.webhookUrl || '',
1409
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, webhookUrl: e.target.value } }),
1410
- })
1411
- ),
1412
- React.createElement(Field, { label: t('webhookSecret'), hint: t('webhookSecretHint') },
1413
- React.createElement('input', {
1414
- type: 'password',
1415
- className: 'msgw-input',
1416
- value: webhookSecret,
1417
- placeholder: cfg.telegram?.webhookSecretConfigured ? '••••••••••••••••' : '',
1418
- onChange: (e) => setWebhookSecret(e.target.value),
1419
- })
1420
- ),
1421
- React.createElement(Field, { label: t('pollTimeout') },
1422
- React.createElement('input', {
1423
- type: 'number',
1424
- className: 'msgw-input',
1425
- min: 10,
1426
- max: 60,
1427
- value: cfg.telegram?.pollTimeoutSeconds ?? 50,
1428
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollTimeoutSeconds: Number(e.target.value) || 50 } }),
1429
- })
1430
- ),
1431
- React.createElement(Field, { label: t('pollInterval') },
1432
- React.createElement('input', {
1433
- type: 'number',
1434
- className: 'msgw-input',
1435
- min: 100,
1436
- max: 5000,
1437
- value: cfg.telegram?.pollIntervalMs ?? 500,
1438
- onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollIntervalMs: Number(e.target.value) || 500 } }),
1439
- })
1440
- ),
1441
- React.createElement(Field, { label: t('idleTimeout') },
1442
- React.createElement('input', {
1443
- type: 'number',
1444
- className: 'msgw-input',
1445
- min: 300,
1446
- max: 86400,
1447
- value: Math.round((cfg.agent?.idleTimeoutMs ?? 3600000) / 1000),
1448
- onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, idleTimeoutMs: (Number(e.target.value) || 3600) * 1000 } }),
1449
- })
1450
- )
1451
- ) : null
1452
- ),
1453
-
1454
- // Sticky Footer
1455
- React.createElement(
1456
- 'div',
1457
- {
1458
- className: 'msgw-foot',
1459
- style: {
1460
- display: 'flex',
1461
- justifyContent: 'flex-end',
1462
- gap: '10px',
1463
- paddingTop: '12px',
1464
- borderTop: '1px solid var(--dsw-alias-border-l2)',
1465
- },
1466
- },
1467
- React.createElement(
1468
- 'button',
1469
- {
1470
- type: 'button',
1471
- className: 'msgw-btn msgw-btn-primary',
1472
- disabled: !!busy,
1473
- onClick: () => save({}),
1474
- },
1475
- busy === 'save' ? t('saving') : t('save')
1476
- )
1477
- )
1478
- )
1479
- }
1480
-
1481
- function PluginCard(props) {
1482
- const [open, setOpen] = React.useState(false)
1483
- const t = props?.t || makeT(props?.locale)
1484
- React.useEffect(() => {
1485
- ensureCss()
1486
- }, [])
1487
-
1488
- return React.createElement(
1489
- 'li',
1490
- { className: 'msgw-card msgw-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
1491
- React.createElement(
1492
- 'button',
1493
- {
1494
- type: 'button',
1495
- className: 'msgw-head',
1496
- style: {
1497
- background: 'none',
1498
- border: 'none',
1499
- cursor: 'pointer',
1500
- display: 'flex',
1501
- alignItems: 'center',
1502
- width: '100%',
1503
- padding: 0,
1504
- textAlign: 'left',
1505
- },
1506
- 'aria-expanded': open,
1507
- onClick: () => setOpen((v) => !v),
1508
- },
1509
- React.createElement(
1510
- 'div',
1511
- { className: 'msgw-headText', style: { flex: 1 } },
1512
- React.createElement('div', { className: 'msgw-title', style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
1513
- React.createElement('div', { className: 'msgw-sub', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('description'))
1514
- ),
1515
- React.createElement(
1516
- 'span',
1517
- { className: 'msgw-chev', style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
1518
- React.createElement(Chevron)
1519
- )
1520
- ),
1521
- open
1522
- ? React.createElement(
1523
- 'div',
1524
- { className: 'msgw-body', style: { marginTop: '16px' } },
1525
- React.createElement(
1526
- ErrorBoundary,
1527
- null,
1528
- React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx, t })
1529
- )
1530
- )
1531
- : null
1532
- )
1533
- }
1534
-
1535
- function refreshMirrorUntilVisible(ctx) {
1536
- const visible = () => {
1537
- try {
1538
- const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
1539
- const view = s?.describe?.()?.getSnapshot?.()?.view
1540
- return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
1541
- } catch (_) {
1542
- return false
1543
- }
1544
- }
1545
- if (visible()) return () => {}
1546
- let tries = 0
1547
- const timer = setInterval(() => {
1548
- if (visible() || tries >= 15) { clearInterval(timer); return }
1549
- tries += 1
1550
- try {
1551
- const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
1552
- s?.describe?.()?.load?.()
1553
- } catch (_) {}
1554
- }, 1000)
1555
- return () => clearInterval(timer)
1556
- }
1557
-
1558
- function apply(ctx) {
1559
- try {
1560
- if (typeof ctx.effect === 'function') {
1561
- ctx.effect(() => {
1562
- try {
1563
- const loc = ctx?.get?.('locale') || ctx?.locale
1564
- if (loc?.register) {
1565
- loc.register(NS, { en, zh })
1566
- }
1567
- } catch (taken) {
1568
- console.warn('[dsh-messenger-gateway] dictionary already registered:', taken && taken.message)
1569
- }
1570
- }, 'dsh-messenger-gateway: dictionaries')
1571
- } else {
1572
- const loc = ctx?.get?.('locale') || ctx?.locale
1573
- if (loc?.register) {
1574
- loc.register(NS, { en, zh })
1575
- }
1576
- }
1577
- } catch (err) {
1578
- console.warn('[dsh-messenger-gateway] dictionary registration error:', err && err.message)
1579
- }
1580
-
1581
- if (typeof ctx.effect === 'function') {
1582
- ctx.effect(
1583
- () => refreshMirrorUntilVisible(ctx),
1584
- 'dsh-messenger-gateway: re-read the settings mirror until our namespace appears',
1585
- )
1586
- }
1587
-
1588
- function registerSlotWhenReady(slotName, registerFn) {
1589
- const slots = ctx?.get?.('slots') || ctx?.slots
1590
- if (!slots) return
1591
- if (typeof slots.inject === 'function') {
1592
- try {
1593
- slots.inject(slotName, () => {
1594
- try {
1595
- return registerFn()
1596
- } catch (err) {
1597
- console.warn('[dsh-messenger-gateway] Error registering slot ' + slotName + ':', err)
1598
- }
1599
- })
1600
- return
1601
- } catch (err) {
1602
- console.warn('[dsh-messenger-gateway] Failed to inject slot ' + slotName + ':', err)
1603
- }
1604
- }
1605
- if (typeof slots.register === 'function') {
1606
- try {
1607
- registerFn()
1608
- } catch (err) {
1609
- console.warn('[dsh-messenger-gateway] Failed direct registration for ' + slotName + ':', err)
1610
- }
1611
- }
1612
- }
1613
-
1614
- registerSlotWhenReady('settings.plugin.item', () => {
1615
- const slots = ctx?.get?.('slots') || ctx?.slots
1616
- return slots.register(
1617
- {
1618
- name: 'settings.plugin.item',
1619
- key: NS,
1620
- locale: NS,
1621
- inject: () => ({ ctx }),
1622
- },
1623
- (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
1624
- )
1625
- })
1626
- }
1627
-
1628
- module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
1629
- return module.exports
1630
- },
1631
- })
1
+ // dsh-messenger-gateway — browser (client) half.
2
+ // Settings card on Plugins → Plugin settings tab (settings.plugin.item).
3
+ // Config managed via reactive ctx.settingsScope snapshot.
4
+
5
+ window.__ModuleLoader__.load({
6
+ id: '@goodandready/dsh-messenger-gateway',
7
+ factory: (require) => {
8
+ var module = { exports: {} }
9
+ var exports = module.exports
10
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
11
+ const React = require('react')
12
+
13
+ const NS = 'dsh-messenger-gateway'
14
+ const ROUTE_PREFIX = '/dsh-messenger-gateway'
15
+ // Plugins page row seat: key = '<package name>#<row id>', row id as cordis.patch.yml declares it.
16
+ const PKG = '@goodandready/dsh-messenger-gateway'
17
+ const ROW_ID = 'dsh-messenger-gateway'
18
+ const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
19
+
20
+ const en = {
21
+ title: 'Messenger gateway',
22
+ description: 'Telegram bot: text, voice, photos, documents, and interactive asks.',
23
+ 'settings.loading': 'Loading Messenger gateway settings…',
24
+ 'settings.retry': 'Retry',
25
+ 'settings.unavailable': 'Settings unavailable (host namespace is not ready).',
26
+ 'header.title': 'Telegram & Multi-Messenger Gateway',
27
+ 'header.sub': 'Bridge between DeepSeek Harness and Telegram: sessions, steer, forum topics, inline buttons, and optional voice notes.',
28
+ 'badge.online': 'Telegram online (@{username})',
29
+ 'badge.running': 'Running',
30
+ 'badge.stopped': 'Stopped',
31
+ 'badge.token_ok': 'Token ✓',
32
+ 'badge.token_missing': 'Token missing',
33
+ 'badge.pairing_count': 'Pairing: {count}',
34
+ 'badge.transport': 'Transport: {mode}',
35
+ save: 'Save Changes',
36
+ saved: 'Settings saved successfully',
37
+ saving: 'Saving…',
38
+ showAdvanced: '⚙️ Advanced connection & polling settings',
39
+ hideAdvanced: 'Hide advanced settings',
40
+ telegram: 'Telegram Bot Configuration',
41
+ telegramSub: 'Main bot credentials, whitelist, and message delivery settings.',
42
+ agent: 'Agent & Reasoning Execution',
43
+ agentSub: 'Model routing, instruction overrides, and turn timeout control.',
44
+ media: 'Media & File Limits',
45
+ mediaSub: 'Document extraction and image upload limits.',
46
+ tts: 'Voice Replies (dsh-tts)',
47
+ ttsSub: 'Synthesize spoken replies for Telegram voice messages.',
48
+ advanced: 'Connection & Polling Internals',
49
+ botToken: 'Telegram Bot Token',
50
+ botTokenHint: 'Obtained via @BotFather. Leave empty to keep existing configured token.',
51
+ allowedIds: 'Allowed Telegram User IDs',
52
+ allowedIdsHint: 'Comma-separated list of user IDs. Leave empty to allow all users (pairing requests enabled).',
53
+ textFormat: 'Text Format Rendering',
54
+ textFormatHtml: 'Markdown → Telegram HTML (recommended)',
55
+ textFormatPlain: 'Plain text (no formatting)',
56
+ disableTelegram: 'Disable Telegram Bot',
57
+ enableTelegram: 'Enable Telegram Bot',
58
+ provider: 'Provider Override',
59
+ providerHint: 'Leave empty to use DSH default provider',
60
+ model: 'Model Override',
61
+ modelHint: 'Leave empty to use DSH default model',
62
+ photoOnly: 'Photo without Caption Behavior',
63
+ photoPrompt: 'Wait for question (recommended)',
64
+ photoRun: 'Reply immediately',
65
+ instruction: 'Extra Agent Instruction',
66
+ instructionHint: 'Prepended to each inbound messenger message',
67
+ instructionPlaceholder: 'Empty = built-in relay instruction (Russian, concise)',
68
+ maxMessageLength: 'Max Reply Length (characters)',
69
+ turnTimeout: 'Agent Turn Timeout (seconds)',
70
+ maxDocMb: 'Max Document Size (MB)',
71
+ maxImageMb: 'Max Photo Size (MB)',
72
+ maxTextKb: 'Max Extracted Text from Document (KB)',
73
+ ttsEnable: 'Enable Voice Replies (requires dsh-tts)',
74
+ ttsMaxChars: 'Max Spoken Characters per Voice Note',
75
+ pollTimeout: 'Long Poll Timeout (seconds)',
76
+ pollInterval: 'Long Poll Interval (ms)',
77
+ idleTimeout: 'Idle Chat Timeout (seconds)',
78
+ pairing: 'Pairing Requests & Authorization',
79
+ pairingSub: 'Authorize new users when whitelist is restricted.',
80
+ pairingEmpty: 'No pending pairing codes',
81
+ pairingApprove: 'Approve',
82
+ pairingReject: 'Reject',
83
+ pairingRefresh: 'Refresh List',
84
+ groupsEnable: 'Respond in Telegram Groups & Supergroups',
85
+ groupMention: 'Require @mention, reply, or /command in groups',
86
+ reactionsEnable: 'React with 👀 while processing turn',
87
+ progressEnable: 'Progress status message (Thinking → Delete → Final)',
88
+ statusIndicator: 'Bot Status Description Indicator (Online/Offline)',
89
+ statusIndicatorHint: 'Sets bot short description via Telegram API (visible to all users).',
90
+ transport: 'Telegram Transport Mode',
91
+ transportPoll: 'Long poll (getUpdates)',
92
+ transportWebhook: 'Webhook (HTTPS)',
93
+ webhookUrl: 'Public Webhook HTTPS URL',
94
+ webhookUrlHint: 'Public endpoint where Telegram sends updates',
95
+ webhookSecret: 'Webhook Secret Token',
96
+ webhookSecretHint: 'Leave empty to keep current configured secret',
97
+ webhookSecretConfigured: 'Secret set',
98
+ webhookSecretRequired: 'Secret required',
99
+ sessionScope: 'Group Session Scope',
100
+ sessionScopeChat: 'Shared per chat/topic',
101
+ sessionScopeUser: 'Per-user session (recommended)',
102
+ voiceMode: 'Voice Reply Mode',
103
+ voiceModeMirror: 'Mirror inbound voice (reply voice only if input was voice)',
104
+ voiceModeAlways: 'Always synthesize voice note',
105
+ voiceModeOff: 'Off (unless explicitly toggled via /voice)',
106
+ notifyBridge: 'Notify Bridge → Telegram Home',
107
+ notifyBridgeHint: 'Forward task_done/error events from web sessions into designated Telegram home',
108
+ notifyHome: 'Notify Target Home Name',
109
+ quickActions: 'Quick Actions Keyboard (/new, /stop, /voice, /status)',
110
+ artifactPreviews: 'Render diagram and table previews in Telegram',
111
+ 'diag.title': '⚡ Diagnostics & Telegram Smoke Test',
112
+ 'diag.desc': 'Test connection to Telegram Bot API, measure network latency, and verify bot identity.',
113
+ 'diag.smoke_btn': 'Run Telegram Smoke Test (Ping)',
114
+ 'diag.smoke_testing': 'Testing Telegram API…',
115
+ 'diag.smoke_ok': 'Telegram API OK! Latency: {latency} ms (@{username}, ID: {id})',
116
+ 'diag.smoke_err': 'Smoke test failed: {error}',
117
+ 'stats.title': '📊 Gateway Telemetry & Session Counters',
118
+ 'stats.desc': 'Real-time counters from the running gateway process.',
119
+ 'stats.sent': 'Messages Delivered',
120
+ 'stats.errors': 'Delivery Errors',
121
+ 'stats.uptime': 'Gateway Uptime',
122
+ 'stats.active_chats': 'Active Chats in Memory',
123
+ // Updater
124
+ 'updater.title': 'Plugin Self-Updater',
125
+ 'updater.desc': 'Check npmjs registry for updates and upgrade in-place.',
126
+ 'updater.current': 'Current version: {version}',
127
+ 'updater.checking': 'Checking for updates…',
128
+ 'updater.available': 'Update available: v{version}',
129
+ 'updater.upToDate': 'Up to date',
130
+ 'updater.btn_check': 'Check for Updates',
131
+ 'updater.btn_update': 'Update to v{version}',
132
+ 'updater.updating': 'Updating…',
133
+ 'updater.success': 'Updated to v{version}! Please restart DSH service if required.',
134
+ 'updater.failed': 'Update failed: {error}',
135
+ 'updater.check_failed': 'Update check failed: {error}',
136
+ // Discord
137
+ discord: 'Discord Gateway',
138
+ discordSub: 'Discord bot and outbound webhook delivery settings.',
139
+ enableDiscord: 'Enable Discord',
140
+ disableDiscord: 'Disable Discord',
141
+ discordToken: 'Discord Bot Token',
142
+ discordTokenHint: 'From Discord Developer Portal. Leave empty to keep existing.',
143
+ discordWebhook: 'Discord Webhook URL',
144
+ discordWebhookHint: 'Incoming webhook for sending messages/notifications.',
145
+ discordChannels: 'Allowed Discord Channel IDs',
146
+ discordChannelsHint: 'Comma-separated channel IDs. Leave empty to allow all configured.',
147
+ // Slack
148
+ slack: 'Slack Gateway',
149
+ slackSub: 'Slack bot and incoming webhook settings.',
150
+ enableSlack: 'Enable Slack',
151
+ disableSlack: 'Disable Slack',
152
+ slackToken: 'Slack Bot Token (xoxb-…)',
153
+ slackTokenHint: 'Bot User OAuth Token. Leave empty to keep existing.',
154
+ slackWebhook: 'Slack Webhook URL',
155
+ slackWebhookHint: 'Incoming webhook URL for channel notifications.',
156
+ slackDefaultChannel: 'Slack Default Channel',
157
+ slackDefaultChannelHint: 'Default channel or member ID for outbound messages (e.g. #general or C12345).',
158
+ // Cron badge
159
+ 'badge.cron_count': 'Cron: {count}',
160
+ 'badge.api_degraded': 'Telegram API degraded ({count})',
161
+ }
162
+
163
+ const zh = {
164
+ title: 'Messenger gateway',
165
+ description: 'Telegram 机器人:文本、语音、图片、文档和交互式询问。',
166
+ 'settings.loading': '正在加载 Messenger gateway 设置…',
167
+ 'settings.retry': '重试',
168
+ 'settings.unavailable': '设置不可用(主机命名空间未就绪)。',
169
+ 'header.title': 'Telegram 与多消息平台网关',
170
+ 'header.sub': 'DeepSeek Harness 与 Telegram 之间的桥梁:会话、转向提示、论坛话题、内联按钮和可选语音。',
171
+ 'badge.online': 'Telegram 在线 (@{username})',
172
+ 'badge.running': '运行中',
173
+ 'badge.stopped': '已停止',
174
+ 'badge.token_ok': '令牌有效 ✓',
175
+ 'badge.token_missing': '缺少令牌',
176
+ 'badge.pairing_count': '配对: {count}',
177
+ 'badge.transport': '传输方式: {mode}',
178
+ save: '保存更改',
179
+ saved: '设置保存成功',
180
+ saving: '保存中…',
181
+ showAdvanced: '⚙️ 高级连接与轮询设置',
182
+ hideAdvanced: '隐藏高级设置',
183
+ telegram: 'Telegram 机器人设置',
184
+ telegramSub: '主机器人凭证、白名单与消息发送设置。',
185
+ agent: '智能体与推理行为',
186
+ agentSub: '模型路由、附加指令与轮次超时设置。',
187
+ media: '媒体与文件限制',
188
+ mediaSub: '文档提取与图片上传限制。',
189
+ tts: '语音回复 (dsh-tts)',
190
+ ttsSub: '为 Telegram 语音消息合成语音回复。',
191
+ advanced: '连接与轮询内部设置',
192
+ botToken: 'Telegram 机器人令牌',
193
+ botTokenHint: '从 @BotFather 获取。留空保持现有配置。',
194
+ allowedIds: '允许的 Telegram 用户 ID',
195
+ allowedIdsHint: '逗号分隔的用户 ID 列表。留空允许所有用户(启用配对请求)。',
196
+ textFormat: '文本格式化渲染',
197
+ textFormatHtml: 'Markdown → Telegram HTML (推荐)',
198
+ textFormatPlain: '纯文本 (无格式化)',
199
+ disableTelegram: '停用 Telegram 机器人',
200
+ enableTelegram: '启用 Telegram 机器人',
201
+ provider: '提供商覆盖',
202
+ providerHint: '留空使用 DSH 默认提供商',
203
+ model: '模型覆盖',
204
+ modelHint: '留空使用 DSH 默认模型',
205
+ photoOnly: '无附言图片处理方式',
206
+ photoPrompt: '等待问题 (推荐)',
207
+ photoRun: '立即回复',
208
+ instruction: '额外智能体指令',
209
+ instructionHint: '添加到每条来自网关的消息前',
210
+ instructionPlaceholder: '留空 = 内置指令',
211
+ maxMessageLength: '最大回复长度 (字符)',
212
+ turnTimeout: '智能体轮次超时 (秒)',
213
+ maxDocMb: '最大文档大小 (MB)',
214
+ maxImageMb: '最大图片大小 (MB)',
215
+ maxTextKb: '文档提取最大文本量 (KB)',
216
+ ttsEnable: '启用语音回复 (需 dsh-tts)',
217
+ ttsMaxChars: '每条语音消息最大字数',
218
+ pollTimeout: '长轮询超时 (秒)',
219
+ pollInterval: '长轮询间隔 (毫秒)',
220
+ idleTimeout: '空闲会话超时 (秒)',
221
+ pairing: '配对请求与授权',
222
+ pairingSub: '受限白名单下授权新用户。',
223
+ pairingEmpty: '暂无待处理配对码',
224
+ pairingApprove: '批准',
225
+ pairingReject: '拒绝',
226
+ pairingRefresh: '刷新列表',
227
+ groupsEnable: '在群组和超级群组中响应',
228
+ groupMention: '群组中需 @提及、回复或 /命令',
229
+ reactionsEnable: '处理中添加 👀 表情反应',
230
+ progressEnable: '进度状态提示 (思考中 → 删除 → 最终回复)',
231
+ statusIndicator: '机器人状态描述指示器 (Online/Offline)',
232
+ statusIndicatorHint: '通过 Telegram API 设置机器人简介(所有人可见)。',
233
+ transport: 'Telegram 传输模式',
234
+ transportPoll: '长轮询 (getUpdates)',
235
+ transportWebhook: 'Webhook (HTTPS)',
236
+ webhookUrl: '公开 Webhook HTTPS 地址',
237
+ webhookUrlHint: 'Telegram 投递更新的公开端点',
238
+ webhookSecret: 'Webhook 密钥令牌',
239
+ webhookSecretHint: '留空保持当前配置的密钥',
240
+ webhookSecretConfigured: '密钥已设置',
241
+ webhookSecretRequired: '必须设置密钥',
242
+ sessionScope: '群组会话范围',
243
+ sessionScopeChat: '每个聊天/话题共享',
244
+ sessionScopeUser: '每用户独立会话 (推荐)',
245
+ voiceMode: '语音回复模式',
246
+ voiceModeMirror: '镜像输入语音 (仅语音输入时回复语音)',
247
+ voiceModeAlways: '始终合成语音回复',
248
+ voiceModeOff: '关闭 (除非通过 /voice 启用)',
249
+ notifyBridge: '通知网桥 → Telegram Home',
250
+ notifyBridgeHint: '将 Web 会话的 task_done/error 事件转发至指定频道/话题',
251
+ notifyHome: '目标频道名称 (Home)',
252
+ quickActions: '快捷操作键盘 (/new, /stop, /voice, /status)',
253
+ artifactPreviews: '在 Telegram 中预览图表与表格',
254
+ 'diag.title': '⚡ 诊断与连通性检查 (Smoke test)',
255
+ 'diag.desc': '检查 Telegram Bot API 连接、测量延迟并验证机器人名称。',
256
+ 'diag.smoke_btn': '测试 Telegram 连接 (Ping)',
257
+ 'diag.smoke_testing': '正在连接 Telegram API…',
258
+ 'diag.smoke_ok': 'Telegram API 连接成功!延迟: {latency} 毫秒 (@{username}, ID: {id})',
259
+ 'diag.smoke_err': '连通性检查失败: {error}',
260
+ 'stats.title': '📊 网关遥测与计数器',
261
+ 'stats.desc': '运行中网关进程的实时统计。',
262
+ 'stats.sent': '已发送消息',
263
+ 'stats.errors': '发送错误数',
264
+ 'stats.uptime': '运行时间',
265
+ 'stats.active_chats': '内存中活跃聊天数',
266
+ // Updater
267
+ 'updater.title': '插件自更新',
268
+ 'updater.desc': '从 npmjs 仓库检查更新并直接就地升级。',
269
+ 'updater.current': '当前版本: {version}',
270
+ 'updater.checking': '正在检查更新…',
271
+ 'updater.available': '发现新版本: v{version}',
272
+ 'updater.upToDate': '已是最新版本',
273
+ 'updater.btn_check': '检查更新',
274
+ 'updater.btn_update': '更新至 v{version}',
275
+ 'updater.updating': '正在更新…',
276
+ 'updater.success': '已更新至 v{version}!如需生效请重启 DSH 服务。',
277
+ 'updater.failed': '更新失败: {error}',
278
+ 'updater.check_failed': '检查更新失败: {error}',
279
+ // Discord
280
+ discord: 'Discord 网关',
281
+ discordSub: 'Discord 机器人与出站 Webhook 设置。',
282
+ enableDiscord: '启用 Discord',
283
+ disableDiscord: '停用 Discord',
284
+ discordToken: 'Discord 机器人令牌',
285
+ discordTokenHint: '从 Discord 开发者平台获取。留空保持现有配置。',
286
+ discordWebhook: 'Discord Webhook URL',
287
+ discordWebhookHint: '用于发送消息或通知的传入 Webhook。',
288
+ discordChannels: '允许的 Discord 频道 ID',
289
+ discordChannelsHint: '逗号分隔的频道 ID。留空允许所有已配置频道。',
290
+ // Slack
291
+ slack: 'Slack 网关',
292
+ slackSub: 'Slack 机器人与 Webhook 设置。',
293
+ enableSlack: '启用 Slack',
294
+ disableSlack: '停用 Slack',
295
+ slackToken: 'Slack 机器人令牌 (xoxb-…)',
296
+ slackTokenHint: 'Bot 用户 OAuth 令牌。留空保持现有配置。',
297
+ slackWebhook: 'Slack Webhook URL',
298
+ slackWebhookHint: '用于频道通知的传入 Webhook URL。',
299
+ slackDefaultChannel: 'Slack 默认频道',
300
+ slackDefaultChannelHint: '出站消息的默认频道或成员 ID (例如 #general 或 C12345)。',
301
+ // Cron badge
302
+ 'badge.cron_count': '定时任务: {count}',
303
+ 'badge.api_degraded': 'Telegram API 异常 ({count} 次连续错误)',
304
+ }
305
+
306
+ function useActiveLocale(ctx) {
307
+ const loc = ctx?.get?.('locale') || ctx?.locale
308
+ return React.useSyncExternalStore(
309
+ React.useMemo(() => (cb) => (loc ? loc.subscribe(cb) : () => {}), [loc]),
310
+ React.useCallback(() => {
311
+ if (loc) {
312
+ const active = loc.getSnapshot?.()?.active
313
+ if (typeof active === 'string' && active) return active
314
+ }
315
+ return typeof navigator !== 'undefined' ? String(navigator.language || '').slice(0, 2) : 'en'
316
+ }, [loc]),
317
+ )
318
+ }
319
+
320
+ function makeT(locale) {
321
+ const DICT = String(locale || '').startsWith('zh') ? zh : en
322
+ return (key, params) => {
323
+ let text = DICT[key] || en[key] || key
324
+ if (params && typeof params === 'object') {
325
+ for (const k of Object.keys(params)) {
326
+ text = text.replace(new RegExp('\\{' + k + '\\}', 'g'), String(params[k]))
327
+ }
328
+ }
329
+ return text
330
+ }
331
+ }
332
+
333
+ function ensureCss() {
334
+ if (typeof document === 'undefined') return
335
+ const id = 'dsh-messenger-gateway-full-css'
336
+ if (document.getElementById(id)) return
337
+ const style = document.createElement('style')
338
+ style.id = id
339
+ style.dataset.dshPlugin = NS
340
+ style.textContent = `
341
+ .msgw-page{display:flex;flex-direction:column;gap:16px;color:var(--dsw-alias-label-primary);font-family:inherit}
342
+ .msgw-page-seat{padding:0}
343
+ .msgw-header{display:flex;flex-direction:column;gap:4px;padding-bottom:12px;border-bottom:1px solid var(--dsw-alias-border-l2)}
344
+ .msgw-page-title{font-size:18px;font-weight:700;display:flex;align-items:center;gap:10px;flex-wrap:wrap;color:var(--dsw-alias-label-primary)}
345
+ .msgw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
346
+
347
+ .msgw-badge{font-size:11px;font-weight:600;padding:2px 8px;border-radius:12px;display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
348
+ .msgw-badge-ok{background:var(--dsw-alias-state-success-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-border, var(--dsw-alias-border-l2))}
349
+ .msgw-badge-warn{background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-border, var(--dsw-alias-border-l2))}
350
+ .msgw-badge-bad{background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-border, var(--dsw-alias-border-l2))}
351
+
352
+ .msgw-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:12px}
353
+ .msgw-section-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
354
+ .msgw-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4;margin-top:-6px}
355
+
356
+ .msgw-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
357
+ .msgw-field{display:flex;flex-direction:column;gap:6px}
358
+ .msgw-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
359
+ .msgw-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.3}
360
+
361
+ .msgw-input,.msgw-select,.msgw-textarea{width:100%;box-sizing:border-box;height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font:inherit;font-size:13px;outline:none;transition:border-color .15s}
362
+ .msgw-input:focus,.msgw-select:focus,.msgw-textarea:focus{border-color:var(--dsw-alias-label-primary)}
363
+ .msgw-textarea{height:auto;min-height:72px;padding:8px 12px}
364
+ .msgw-check{display:flex;gap:8px;align-items:center;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}
365
+
366
+ .msgw-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
367
+ .msgw-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
368
+ .msgw-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
369
+ .msgw-btn-primary:hover:not(:disabled){opacity:0.88}
370
+ .msgw-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-border, var(--dsw-alias-border-l2))}
371
+ .msgw-btn-danger:hover:not(:disabled){background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-hover));border-color:var(--dsw-alias-state-error-primary)}
372
+ .msgw-btn-mini{padding:4px 10px;font-size:12px;border-radius:6px}
373
+ .msgw-btn:disabled{opacity:0.5;cursor:not-allowed}
374
+
375
+ .msgw-alert-ok{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-success-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-success-primary);font-size:13px;border:1px solid var(--dsw-alias-state-success-border, var(--dsw-alias-border-l2))}
376
+ .msgw-alert-err{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-error-primary);font-size:13px;border:1px solid var(--dsw-alias-state-error-border, var(--dsw-alias-border-l2))}
377
+ .msgw-banner-warning{padding:12px 16px;border-radius:8px;background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-state-warning-border, var(--dsw-alias-state-warning-primary));color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
378
+
379
+ .msgw-stat-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:10px}
380
+ .msgw-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
381
+ .msgw-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
382
+ .msgw-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
383
+
384
+ .msgw-table{width:100%;border-collapse:collapse;margin-top:4px}
385
+ .msgw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
386
+ .msgw-table td{padding:8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
387
+ .msgw-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
388
+ .msgw-preview{padding:12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2)}
389
+ `
390
+ document.head.appendChild(style)
391
+ }
392
+
393
+ function createErrorBoundary() {
394
+ if (!React || typeof React.Component !== 'function') {
395
+ return function NoopBoundary(props) { return props?.children || null }
396
+ }
397
+ return class ErrorBoundary extends React.Component {
398
+ constructor(props) {
399
+ super(props)
400
+ this.state = { hasError: false, error: null }
401
+ }
402
+ static getDerivedStateFromError(error) {
403
+ return { hasError: true, error }
404
+ }
405
+ componentDidCatch(error, errorInfo) {
406
+ console.error('[dsh-messenger-gateway] React UI Error:', error, errorInfo)
407
+ }
408
+ render() {
409
+ if (this.state.hasError) {
410
+ return React.createElement(
411
+ 'div',
412
+ { className: 'msgw-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
413
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ Messenger Gateway UI Error:'),
414
+ React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
415
+ React.createElement(
416
+ 'button',
417
+ {
418
+ type: 'button',
419
+ className: 'msgw-btn msgw-btn-mini',
420
+ style: { marginTop: '10px' },
421
+ onClick: () => this.setState({ hasError: false, error: null }),
422
+ },
423
+ 'Retry'
424
+ )
425
+ )
426
+ }
427
+ return this.props?.children || null
428
+ }
429
+ }
430
+ }
431
+ const ErrorBoundary = createErrorBoundary()
432
+
433
+ function FallbackChevron() {
434
+ return React.createElement(
435
+ 'svg',
436
+ { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
437
+ React.createElement('polyline', { points: '6 9 12 15 18 9' })
438
+ )
439
+ }
440
+ const Chevron = FallbackChevron
441
+
442
+ function idsToText(ids) { return (Array.isArray(ids) ? ids : []).map(String).filter(Boolean).join(', ') }
443
+ function textToIds(text) {
444
+ return String(text || '').split(/[\s,]+/).map((s) => s.trim()).filter(Boolean).map(Number).filter((n) => Number.isFinite(n))
445
+ }
446
+ function bytesToMb(n) { return Math.round(Number(n || 0) / (1024 * 1024)) }
447
+ function mbToBytes(n) { return Math.max(1, Math.round(Number(n || 1))) * 1024 * 1024 }
448
+
449
+ function Field({ label, hint, children }) {
450
+ return React.createElement('div', { className: 'msgw-field' },
451
+ React.createElement('label', { className: 'msgw-label' }, label),
452
+ hint ? React.createElement('div', { className: 'msgw-hint' }, hint) : null,
453
+ children,
454
+ )
455
+ }
456
+
457
+ const SETTINGS_KEYS = ['enabled', 'telegram', 'discord', 'slack', 'agent', 'media', 'tts']
458
+
459
+ function draftFromStored(s) {
460
+ const src = (s && typeof s === 'object') ? s : {}
461
+ const t = src.telegram || {}
462
+ const d = src.discord || {}
463
+ const sl = src.slack || {}
464
+ const a = src.agent || {}
465
+ const m = src.media || {}
466
+ const tts = src.tts || {}
467
+ return {
468
+ enabled: src.enabled !== false,
469
+ telegram: {
470
+ enabled: t.enabled === true,
471
+ allowedUserIds: Array.isArray(t.allowedUserIds) ? t.allowedUserIds : [],
472
+ pollTimeoutSeconds: Number(t.pollTimeoutSeconds) || 50,
473
+ pollIntervalMs: Number(t.pollIntervalMs) || 500,
474
+ commands: Array.isArray(t.commands) ? t.commands : [],
475
+ textFormat: t.textFormat === 'plain' ? 'plain' : 'html',
476
+ homeChatId: t.homeChatId != null ? t.homeChatId : '',
477
+ homeThreadId: Number(t.homeThreadId) || 0,
478
+ homes: Array.isArray(t.homes) ? t.homes : [],
479
+ pairingEnabled: t.pairingEnabled !== false,
480
+ streaming: t.streaming === true,
481
+ streamEditIntervalMs: Number(t.streamEditIntervalMs) || 1200,
482
+ progressEnabled: t.progressEnabled !== false,
483
+ approvalsEnabled: t.approvalsEnabled !== false,
484
+ groupsEnabled: t.groupsEnabled !== false,
485
+ groupRequireMention: t.groupRequireMention !== false,
486
+ reactionsEnabled: t.reactionsEnabled !== false,
487
+ statusIndicator: t.statusIndicator === true,
488
+ statusOnline: t.statusOnline || 'Online',
489
+ statusOffline: t.statusOffline || 'Offline',
490
+ transport: t.transport === 'webhook' ? 'webhook' : 'poll',
491
+ webhookUrl: t.webhookUrl || '',
492
+ webhookPath: t.webhookPath || '/dsh-messenger-gateway/telegram/webhook',
493
+ voiceMode: t.voiceMode || 'mirror',
494
+ quickActions: t.quickActions === true,
495
+ artifactPreviews: t.artifactPreviews !== false,
496
+ notifyBridge: t.notifyBridge || { enabled: false, events: ['task_done', 'error'], home: 'default', excludeSessionPrefixes: ['msgw-'] },
497
+ alerts: t.alerts || { enabled: false, chatId: '', threadId: 0, home: '', events: ['error', 'pairing'] },
498
+ botToken: t.botToken || '',
499
+ webhookSecret: t.webhookSecret || '',
500
+ },
501
+ discord: {
502
+ enabled: d.enabled === true,
503
+ botToken: d.botToken || '',
504
+ webhookUrl: d.webhookUrl || '',
505
+ allowedChannelIds: Array.isArray(d.allowedChannelIds) ? d.allowedChannelIds : [],
506
+ },
507
+ slack: {
508
+ enabled: sl.enabled === true,
509
+ botToken: sl.botToken || '',
510
+ webhookUrl: sl.webhookUrl || '',
511
+ defaultChannel: sl.defaultChannel || '',
512
+ },
513
+ agent: {
514
+ provider: a.provider || '',
515
+ model: a.model || '',
516
+ instructionPrefix: a.instructionPrefix || '',
517
+ maxMessageLength: Number(a.maxMessageLength) || 4000,
518
+ turnTimeoutMs: Number(a.turnTimeoutMs) || 600000,
519
+ idleTimeoutMs: Number(a.idleTimeoutMs) || 3600000,
520
+ photoOnlyMode: a.photoOnlyMode || 'prompt',
521
+ sessionScope: a.sessionScope || 'user',
522
+ },
523
+ media: {
524
+ maxDocBytes: Number(m.maxDocBytes) || 20971520,
525
+ maxImageBytes: Number(m.maxImageBytes) || 20971520,
526
+ maxTextInjectBytes: Number(m.maxTextInjectBytes) || 102400,
527
+ },
528
+ tts: {
529
+ enabled: tts.enabled === true,
530
+ maxChars: Number(tts.maxChars) || 4000,
531
+ voiceSummary: tts.voiceSummary === true,
532
+ },
533
+ }
534
+ }
535
+
536
+ function SettingsPage(props) {
537
+ const ctx = props?.ctx
538
+ const t = props?.t || makeT(props?.locale)
539
+
540
+ const [token, setToken] = React.useState('')
541
+ const [webhookSecret, setWebhookSecret] = React.useState('')
542
+ const [allowText, setAllowText] = React.useState('')
543
+ const [discordToken, setDiscordToken] = React.useState('')
544
+ const [slackToken, setSlackToken] = React.useState('')
545
+ const [discordChannelsText, setDiscordChannelsText] = React.useState('')
546
+ const [updateState, setUpdateState] = React.useState({
547
+ checking: false,
548
+ updating: false,
549
+ currentVersion: '',
550
+ latestVersion: '',
551
+ updateAvailable: false,
552
+ error: '',
553
+ notice: '',
554
+ })
555
+ const [pending, setPending] = React.useState([])
556
+ const [status, setStatus] = React.useState(null)
557
+ const [smokeResult, setSmokeResult] = React.useState(null)
558
+ const [err, setErr] = React.useState('')
559
+ const [msg, setMsg] = React.useState('')
560
+ const [busy, setBusy] = React.useState('')
561
+ const [showAdvanced, setShowAdvanced] = React.useState(false)
562
+
563
+ React.useEffect(() => {
564
+ ensureCss()
565
+ }, [])
566
+
567
+ const scope = React.useMemo(() => {
568
+ const settingsScope = ctx?.get?.('settingsScope') || ctx?.settingsScope
569
+ return settingsScope ? settingsScope.bind({ namespace: NS }) : undefined
570
+ }, [ctx])
571
+
572
+ const snapshot = React.useSyncExternalStore(
573
+ React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
574
+ React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
575
+ React.useCallback(() => ({ status: 'loading' }), []),
576
+ )
577
+
578
+ const snapStatus = (snapshot && snapshot.status) || 'loading'
579
+ const stored = (snapshot && snapshot.value) || {}
580
+ const [cfg, setCfg] = React.useState(null)
581
+
582
+ React.useEffect(() => {
583
+ if (snapStatus === 'ready' && cfg === null) {
584
+ setCfg(draftFromStored(stored))
585
+ setAllowText(idsToText(stored?.telegram?.allowedUserIds))
586
+ setDiscordChannelsText(idsToText(stored?.discord?.allowedChannelIds))
587
+ }
588
+ }, [snapStatus, stored, cfg])
589
+
590
+ const loadStatus = React.useCallback(async () => {
591
+ try {
592
+ const res = await fetch(`${ROUTE_PREFIX}/status`, { cache: 'no-store' })
593
+ const data = await res.json().catch(() => ({}))
594
+ if (res.ok && data.ok) {
595
+ setStatus(data)
596
+ }
597
+ } catch (err) {
598
+ console.debug?.('[dsh-messenger-gateway] client status poll error:', err?.message || err)
599
+ }
600
+ }, [])
601
+
602
+ const loadPairing = React.useCallback(async () => {
603
+ try {
604
+ const res = await fetch(`${ROUTE_PREFIX}/pairing`, { credentials: 'same-origin', cache: 'no-store' })
605
+ const data = await res.json().catch(() => ({}))
606
+ if (res.ok && data.ok) setPending(Array.isArray(data.pending) ? data.pending : [])
607
+ } catch (err) {
608
+ console.debug?.('[dsh-messenger-gateway] client pairing poll error:', err?.message || err)
609
+ }
610
+ }, [])
611
+
612
+ React.useEffect(() => {
613
+ loadStatus()
614
+ loadPairing()
615
+ const timer = setInterval(() => {
616
+ loadStatus()
617
+ loadPairing()
618
+ }, 10000)
619
+ return () => clearInterval(timer)
620
+ }, [loadStatus, loadPairing])
621
+
622
+ const loadUpdateStatus = React.useCallback(async () => {
623
+ setUpdateState((s) => ({ ...s, checking: true, error: '' }))
624
+ try {
625
+ const res = await fetch(`${ROUTE_PREFIX}/update`, { cache: 'no-store' })
626
+ const data = await res.json().catch(() => ({}))
627
+ if (res.ok && data.ok) {
628
+ setUpdateState((s) => ({
629
+ ...s,
630
+ checking: false,
631
+ currentVersion: data.currentVersion || '',
632
+ latestVersion: data.latestVersion || '',
633
+ updateAvailable: !!data.updateAvailable,
634
+ }))
635
+ } else {
636
+ setUpdateState((s) => ({ ...s, checking: false, error: data.error || `HTTP ${res.status}` }))
637
+ }
638
+ } catch (e) {
639
+ setUpdateState((s) => ({ ...s, checking: false, error: String(e.message || e) }))
640
+ }
641
+ }, [])
642
+
643
+ const handleTriggerUpdate = async () => {
644
+ if (updateState.updating) return
645
+ setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
646
+ try {
647
+ const res = await fetch(`${ROUTE_PREFIX}/update`, {
648
+ method: 'POST',
649
+ credentials: 'same-origin',
650
+ headers: {
651
+ 'Content-Type': 'application/json',
652
+ 'x-dsh-plugin-update': '1',
653
+ },
654
+ })
655
+ const data = await res.json().catch(() => ({}))
656
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
657
+ setUpdateState((s) => ({
658
+ ...s,
659
+ updating: false,
660
+ updateAvailable: false,
661
+ currentVersion: data.updatedVersion || s.latestVersion,
662
+ notice: t('updater.success', { version: data.updatedVersion || s.latestVersion }),
663
+ }))
664
+ } catch (e) {
665
+ setUpdateState((s) => ({
666
+ ...s,
667
+ updating: false,
668
+ error: t('updater.failed', { error: String(e.message || e) }),
669
+ }))
670
+ }
671
+ }
672
+
673
+ React.useEffect(() => {
674
+ loadUpdateStatus()
675
+ }, [loadUpdateStatus])
676
+
677
+ const handleSmoke = async () => {
678
+ setBusy('smoke')
679
+ setErr('')
680
+ setMsg('')
681
+ setSmokeResult(null)
682
+ try {
683
+ const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
684
+ method: 'POST',
685
+ credentials: 'same-origin',
686
+ headers: { 'Content-Type': 'application/json' },
687
+ body: JSON.stringify({ timeoutMs: 8000 }),
688
+ })
689
+ const data = await res.json().catch(() => ({}))
690
+ if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
691
+ setSmokeResult(data)
692
+ setMsg(t('diag.smoke_ok', { latency: data.latencyMs, username: data.botUsername, id: data.botId }))
693
+ await loadStatus()
694
+ } catch (e) {
695
+ setErr(t('diag.smoke_err', { error: String(e.message || e) }))
696
+ } finally {
697
+ setBusy('')
698
+ }
699
+ }
700
+
701
+ const mergePatch = (base, patch) => ({
702
+ ...base,
703
+ ...patch,
704
+ telegram: { ...base.telegram, ...(patch.telegram || {}) },
705
+ discord: { ...base.discord, ...(patch.discord || {}) },
706
+ slack: { ...base.slack, ...(patch.slack || {}) },
707
+ tts: { ...base.tts, ...(patch.tts || {}) },
708
+ agent: { ...base.agent, ...(patch.agent || {}) },
709
+ media: { ...base.media, ...(patch.media || {}) },
710
+ })
711
+
712
+ const save = async (patch = {}) => {
713
+ if (!scope || !cfg) return
714
+ setBusy('save')
715
+ setErr('')
716
+ setMsg('')
717
+ try {
718
+ let next = mergePatch(cfg, patch)
719
+ if (token.trim()) next.telegram = { ...next.telegram, botToken: token.trim() }
720
+ if (webhookSecret.trim()) next.telegram = { ...next.telegram, webhookSecret: webhookSecret.trim() }
721
+ next.telegram.allowedUserIds = textToIds(allowText)
722
+ if (discordToken.trim()) next.discord = { ...next.discord, botToken: discordToken.trim() }
723
+ if (slackToken.trim()) next.slack = { ...next.slack, botToken: slackToken.trim() }
724
+ next.discord.allowedChannelIds = textToIds(discordChannelsText)
725
+
726
+ const broken = []
727
+ for (const key of SETTINGS_KEYS) {
728
+ if (next[key] !== undefined) {
729
+ try {
730
+ await scope.set(key, next[key])
731
+ } catch (e) {
732
+ broken.push(key + ': ' + (e && e.message || String(e)))
733
+ }
734
+ }
735
+ }
736
+ if (broken.length) {
737
+ setErr('Save failed — ' + broken.join('; '))
738
+ return
739
+ }
740
+ setCfg(next)
741
+ setToken('')
742
+ setWebhookSecret('')
743
+ setDiscordToken('')
744
+ setSlackToken('')
745
+ setMsg(t('saved'))
746
+ setTimeout(() => setMsg(''), 3000)
747
+ await loadPairing()
748
+ await loadStatus()
749
+ } catch (e) {
750
+ setErr(String(e.message || e))
751
+ } finally {
752
+ setBusy('')
753
+ }
754
+ }
755
+
756
+ if (snapStatus === 'loading') {
757
+ return React.createElement('div', { className: 'msgw-page' }, t('settings.loading'))
758
+ }
759
+ if (snapStatus !== 'ready') {
760
+ return React.createElement('div', { className: 'msgw-page' },
761
+ React.createElement('div', { className: 'msgw-banner-warning' }, t('settings.unavailable'))
762
+ )
763
+ }
764
+ if (!cfg) {
765
+ return React.createElement('div', { className: 'msgw-page' }, t('settings.loading'))
766
+ }
767
+
768
+ const isRunning = Boolean(status?.running)
769
+ const botUsername = status?.botUsername || (status?.config?.telegram?.botUsername) || ''
770
+ const hasToken = Boolean(status?.config?.telegram?.botTokenConfigured || cfg.telegram?.botTokenConfigured || token.trim())
771
+ const transportMode = cfg.telegram?.transport || 'poll'
772
+ const pendingCount = pending.length
773
+
774
+ return React.createElement(
775
+ 'div',
776
+ { className: 'msgw-page' },
777
+
778
+ // Header with badges
779
+ React.createElement(
780
+ 'div',
781
+ { className: 'msgw-header' },
782
+ React.createElement(
783
+ 'div',
784
+ { className: 'msgw-page-title' },
785
+ `💬 ${t('header.title')}`,
786
+ React.createElement(
787
+ 'span',
788
+ { className: `msgw-badge ${isRunning ? 'msgw-badge-ok' : 'msgw-badge-bad'}` },
789
+ isRunning
790
+ ? (botUsername ? t('badge.online', { username: botUsername }) : t('badge.running'))
791
+ : t('badge.stopped')
792
+ ),
793
+ React.createElement(
794
+ 'span',
795
+ { className: `msgw-badge ${hasToken ? 'msgw-badge-ok' : 'msgw-badge-warn'}` },
796
+ hasToken ? t('badge.token_ok') : t('badge.token_missing')
797
+ ),
798
+ React.createElement(
799
+ 'span',
800
+ { className: 'msgw-badge msgw-badge-ok' },
801
+ t('badge.transport', { mode: transportMode })
802
+ ),
803
+ pendingCount > 0
804
+ ? React.createElement(
805
+ 'span',
806
+ { className: 'msgw-badge msgw-badge-warn' },
807
+ t('badge.pairing_count', { count: pendingCount })
808
+ )
809
+ : null,
810
+ (status?.scheduledCount > 0)
811
+ ? React.createElement(
812
+ 'span',
813
+ { className: 'msgw-badge msgw-badge-ok' },
814
+ t('badge.cron_count', { count: status.scheduledCount })
815
+ )
816
+ : null,
817
+ status?.apiHealth?.degraded
818
+ ? React.createElement(
819
+ 'span',
820
+ { className: 'msgw-badge msgw-badge-bad' },
821
+ t('badge.api_degraded', { count: status.apiHealth.consecutiveFailures })
822
+ )
823
+ : null
824
+ ),
825
+ React.createElement('div', { className: 'msgw-page-sub' }, t('header.sub'))
826
+ ),
827
+
828
+ // Global Alert banners
829
+ err ? React.createElement('div', { className: 'msgw-alert-err' }, err) : null,
830
+ msg ? React.createElement('div', { className: 'msgw-alert-ok' }, msg) : null,
831
+
832
+ // Self-Updater Card
833
+ React.createElement(
834
+ 'div',
835
+ { className: 'msgw-section-card' },
836
+ React.createElement(
837
+ 'div',
838
+ { className: 'msgw-section-title' },
839
+ React.createElement('span', null, `🚀 ${t('updater.title')}`),
840
+ React.createElement(
841
+ 'div',
842
+ { className: 'msgw-row' },
843
+ React.createElement(
844
+ 'button',
845
+ {
846
+ type: 'button',
847
+ className: 'msgw-btn msgw-btn-mini',
848
+ disabled: updateState.checking || updateState.updating,
849
+ onClick: loadUpdateStatus,
850
+ },
851
+ updateState.checking ? t('updater.checking') : t('updater.btn_check')
852
+ ),
853
+ updateState.updateAvailable
854
+ ? React.createElement(
855
+ 'button',
856
+ {
857
+ type: 'button',
858
+ className: 'msgw-btn msgw-btn-mini msgw-btn-primary',
859
+ disabled: updateState.updating,
860
+ onClick: handleTriggerUpdate,
861
+ },
862
+ updateState.updating ? t('updater.updating') : t('updater.btn_update', { version: updateState.latestVersion })
863
+ )
864
+ : null
865
+ )
866
+ ),
867
+ React.createElement('div', { className: 'msgw-section-desc' }, t('updater.desc')),
868
+ React.createElement(
869
+ 'div',
870
+ { className: 'msgw-row', style: { fontSize: '13px' } },
871
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } },
872
+ t('updater.current', { version: updateState.currentVersion || status?.version || '...' })
873
+ ),
874
+ updateState.checking
875
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-warn' }, t('updater.checking'))
876
+ : updateState.updateAvailable
877
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-warn' }, t('updater.available', { version: updateState.latestVersion }))
878
+ : updateState.currentVersion && !updateState.error
879
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-ok' }, `✓ ${t('updater.upToDate')}`)
880
+ : null
881
+ ),
882
+ updateState.notice ? React.createElement('div', { className: 'msgw-alert-ok' }, updateState.notice) : null,
883
+ updateState.error ? React.createElement('div', { className: 'msgw-alert-err' }, updateState.error) : null
884
+ ),
885
+
886
+ // Card 1: Telegram Connection & Bot
887
+ React.createElement(
888
+ 'div',
889
+ { className: 'msgw-section-card' },
890
+ React.createElement(
891
+ 'div',
892
+ { className: 'msgw-section-title' },
893
+ React.createElement('span', null, `🤖 ${t('telegram')}`),
894
+ React.createElement(
895
+ 'button',
896
+ {
897
+ type: 'button',
898
+ className: `msgw-btn msgw-btn-mini ${cfg.telegram.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
899
+ disabled: !!busy,
900
+ onClick: () => save({ telegram: { enabled: !cfg.telegram.enabled } }),
901
+ },
902
+ cfg.telegram.enabled ? t('disableTelegram') : t('enableTelegram')
903
+ )
904
+ ),
905
+ React.createElement('div', { className: 'msgw-section-desc' }, t('telegramSub')),
906
+ React.createElement(Field, { label: t('botToken'), hint: t('botTokenHint') },
907
+ React.createElement('input', {
908
+ type: 'password',
909
+ className: 'msgw-input',
910
+ value: token,
911
+ placeholder: hasToken ? '••••••••••••••••••••' : '123456789:ABCdefGHI...',
912
+ onChange: (e) => setToken(e.target.value),
913
+ })
914
+ ),
915
+ React.createElement(Field, { label: t('allowedIds'), hint: t('allowedIdsHint') },
916
+ React.createElement('textarea', {
917
+ className: 'msgw-textarea',
918
+ value: allowText,
919
+ onChange: (e) => setAllowText(e.target.value),
920
+ rows: 2,
921
+ placeholder: '108191529, 987654321',
922
+ })
923
+ ),
924
+ React.createElement(Field, { label: t('textFormat') },
925
+ React.createElement(
926
+ 'select',
927
+ {
928
+ className: 'msgw-select',
929
+ value: cfg.telegram?.textFormat || 'html',
930
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, textFormat: e.target.value } }),
931
+ },
932
+ React.createElement('option', { value: 'html' }, t('textFormatHtml')),
933
+ React.createElement('option', { value: 'plain' }, t('textFormatPlain'))
934
+ )
935
+ ),
936
+ React.createElement('label', { className: 'msgw-check' },
937
+ React.createElement('input', {
938
+ type: 'checkbox',
939
+ checked: cfg.telegram?.groupsEnabled !== false,
940
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupsEnabled: e.target.checked } }),
941
+ }),
942
+ t('groupsEnable')
943
+ ),
944
+ React.createElement('label', { className: 'msgw-check' },
945
+ React.createElement('input', {
946
+ type: 'checkbox',
947
+ checked: cfg.telegram?.groupRequireMention !== false,
948
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupRequireMention: e.target.checked } }),
949
+ }),
950
+ t('groupMention')
951
+ ),
952
+ React.createElement('label', { className: 'msgw-check' },
953
+ React.createElement('input', {
954
+ type: 'checkbox',
955
+ checked: cfg.telegram?.reactionsEnabled !== false,
956
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, reactionsEnabled: e.target.checked } }),
957
+ }),
958
+ t('reactionsEnable')
959
+ ),
960
+ React.createElement('label', { className: 'msgw-check' },
961
+ React.createElement('input', {
962
+ type: 'checkbox',
963
+ checked: cfg.telegram?.progressEnabled !== false,
964
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, progressEnabled: e.target.checked } }),
965
+ }),
966
+ t('progressEnable')
967
+ ),
968
+ React.createElement('label', { className: 'msgw-check' },
969
+ React.createElement('input', {
970
+ type: 'checkbox',
971
+ checked: cfg.telegram?.quickActions !== false,
972
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, quickActions: e.target.checked } }),
973
+ }),
974
+ t('quickActions')
975
+ ),
976
+ React.createElement('label', { className: 'msgw-check' },
977
+ React.createElement('input', {
978
+ type: 'checkbox',
979
+ checked: cfg.telegram?.artifactPreviews !== false,
980
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, artifactPreviews: e.target.checked } }),
981
+ }),
982
+ t('artifactPreviews')
983
+ )
984
+ ),
985
+
986
+ // Card 2: Diagnostics & Telegram Smoke Test
987
+ React.createElement(
988
+ 'div',
989
+ { className: 'msgw-section-card' },
990
+ React.createElement('div', { className: 'msgw-section-title' }, t('diag.title')),
991
+ React.createElement('div', { className: 'msgw-section-desc' }, t('diag.desc')),
992
+ React.createElement(
993
+ 'div',
994
+ { className: 'msgw-row' },
995
+ React.createElement(
996
+ 'button',
997
+ {
998
+ type: 'button',
999
+ className: 'msgw-btn msgw-btn-primary',
1000
+ disabled: !!busy || !hasToken,
1001
+ onClick: handleSmoke,
1002
+ },
1003
+ busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
1004
+ )
1005
+ ),
1006
+ smokeResult
1007
+ ? React.createElement(
1008
+ 'div',
1009
+ { className: 'msgw-preview' },
1010
+ `✅ Latency: ${smokeResult.latencyMs} ms | Bot: @${smokeResult.botUsername || 'unknown'} (ID: ${smokeResult.botId})`
1011
+ )
1012
+ : null
1013
+ ),
1014
+
1015
+ // Card 3: Telemetry & Gateway Counters
1016
+ status?.stats ? React.createElement(
1017
+ 'div',
1018
+ { className: 'msgw-section-card' },
1019
+ React.createElement('div', { className: 'msgw-section-title' }, t('stats.title')),
1020
+ React.createElement('div', { className: 'msgw-section-desc' }, t('stats.desc')),
1021
+ React.createElement(
1022
+ 'div',
1023
+ { className: 'msgw-stat-grid' },
1024
+ React.createElement(
1025
+ 'div',
1026
+ { className: 'msgw-stat-box' },
1027
+ React.createElement('div', { className: 'msgw-stat-val' }, String(status.stats.sent || 0)),
1028
+ React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.sent'))
1029
+ ),
1030
+ React.createElement(
1031
+ 'div',
1032
+ { className: 'msgw-stat-box' },
1033
+ React.createElement('div', { className: 'msgw-stat-val' }, String(status.stats.errors || 0)),
1034
+ React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.errors'))
1035
+ ),
1036
+ React.createElement(
1037
+ 'div',
1038
+ { className: 'msgw-stat-box' },
1039
+ React.createElement('div', { className: 'msgw-stat-val' }, String(status.activeChats || 0)),
1040
+ React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.active_chats'))
1041
+ ),
1042
+ React.createElement(
1043
+ 'div',
1044
+ { className: 'msgw-stat-box' },
1045
+ React.createElement('div', { className: 'msgw-stat-val' }, `${status.uptimeSec || 0}s`),
1046
+ React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.uptime'))
1047
+ )
1048
+ )
1049
+ ) : null,
1050
+
1051
+ // Card 4: Agent & Turn Execution
1052
+ React.createElement(
1053
+ 'div',
1054
+ { className: 'msgw-section-card' },
1055
+ React.createElement('div', { className: 'msgw-section-title' }, `🧠 ${t('agent')}`),
1056
+ React.createElement('div', { className: 'msgw-section-desc' }, t('agentSub')),
1057
+ React.createElement(Field, { label: t('provider'), hint: t('providerHint') },
1058
+ React.createElement('input', {
1059
+ className: 'msgw-input',
1060
+ value: cfg.agent?.provider || '',
1061
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, provider: e.target.value } }),
1062
+ })
1063
+ ),
1064
+ React.createElement(Field, { label: t('model'), hint: t('modelHint') },
1065
+ React.createElement('input', {
1066
+ className: 'msgw-input',
1067
+ value: cfg.agent?.model || '',
1068
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, model: e.target.value } }),
1069
+ })
1070
+ ),
1071
+ React.createElement(Field, { label: t('photoOnly') },
1072
+ React.createElement(
1073
+ 'select',
1074
+ {
1075
+ className: 'msgw-select',
1076
+ value: cfg.agent?.photoOnlyMode || 'prompt',
1077
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, photoOnlyMode: e.target.value } }),
1078
+ },
1079
+ React.createElement('option', { value: 'prompt' }, t('photoPrompt')),
1080
+ React.createElement('option', { value: 'run' }, t('photoRun'))
1081
+ )
1082
+ ),
1083
+ React.createElement(Field, { label: t('sessionScope') },
1084
+ React.createElement(
1085
+ 'select',
1086
+ {
1087
+ className: 'msgw-select',
1088
+ value: cfg.agent?.sessionScope || 'user',
1089
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, sessionScope: e.target.value } }),
1090
+ },
1091
+ React.createElement('option', { value: 'user' }, t('sessionScopeUser')),
1092
+ React.createElement('option', { value: 'chat' }, t('sessionScopeChat'))
1093
+ )
1094
+ ),
1095
+ React.createElement(Field, { label: t('instruction'), hint: t('instructionHint') },
1096
+ React.createElement('textarea', {
1097
+ className: 'msgw-textarea',
1098
+ value: cfg.agent?.instructionPrefix || '',
1099
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, instructionPrefix: e.target.value } }),
1100
+ rows: 3,
1101
+ placeholder: t('instructionPlaceholder'),
1102
+ })
1103
+ ),
1104
+ React.createElement(Field, { label: t('maxMessageLength') },
1105
+ React.createElement('input', {
1106
+ type: 'number',
1107
+ className: 'msgw-input',
1108
+ min: 500,
1109
+ max: 8000,
1110
+ value: cfg.agent?.maxMessageLength ?? 4000,
1111
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, maxMessageLength: Number(e.target.value) || 4000 } }),
1112
+ })
1113
+ ),
1114
+ React.createElement(Field, { label: t('turnTimeout') },
1115
+ React.createElement('input', {
1116
+ type: 'number',
1117
+ className: 'msgw-input',
1118
+ min: 30,
1119
+ max: 3600,
1120
+ value: Math.round((cfg.agent?.turnTimeoutMs ?? 600000) / 1000),
1121
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, turnTimeoutMs: (Number(e.target.value) || 600) * 1000 } }),
1122
+ })
1123
+ )
1124
+ ),
1125
+
1126
+ // Card 5: Media & TTS
1127
+ React.createElement(
1128
+ 'div',
1129
+ { className: 'msgw-section-card' },
1130
+ React.createElement('div', { className: 'msgw-section-title' }, `📎 ${t('media')}`),
1131
+ React.createElement('div', { className: 'msgw-section-desc' }, t('mediaSub')),
1132
+ React.createElement(Field, { label: t('maxDocMb') },
1133
+ React.createElement('input', {
1134
+ type: 'number',
1135
+ className: 'msgw-input',
1136
+ min: 1,
1137
+ max: 50,
1138
+ value: bytesToMb(cfg.media?.maxDocBytes),
1139
+ onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxDocBytes: mbToBytes(e.target.value) } }),
1140
+ })
1141
+ ),
1142
+ React.createElement(Field, { label: t('maxImageMb') },
1143
+ React.createElement('input', {
1144
+ type: 'number',
1145
+ className: 'msgw-input',
1146
+ min: 1,
1147
+ max: 50,
1148
+ value: bytesToMb(cfg.media?.maxImageBytes),
1149
+ onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxImageBytes: mbToBytes(e.target.value) } }),
1150
+ })
1151
+ ),
1152
+ React.createElement(Field, { label: t('maxTextKb') },
1153
+ React.createElement('input', {
1154
+ type: 'number',
1155
+ className: 'msgw-input',
1156
+ min: 16,
1157
+ max: 512,
1158
+ value: Math.round((cfg.media?.maxTextInjectBytes ?? 102400) / 1024),
1159
+ onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxTextInjectBytes: (Number(e.target.value) || 100) * 1024 } }),
1160
+ })
1161
+ ),
1162
+ React.createElement('div', { style: { height: '8px' } }),
1163
+ React.createElement('div', { className: 'msgw-section-title' }, `🔊 ${t('tts')}`),
1164
+ React.createElement('div', { className: 'msgw-section-desc' }, t('ttsSub')),
1165
+ React.createElement('label', { className: 'msgw-check' },
1166
+ React.createElement('input', {
1167
+ type: 'checkbox',
1168
+ checked: !!cfg.tts?.enabled,
1169
+ onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, enabled: e.target.checked } }),
1170
+ }),
1171
+ t('ttsEnable')
1172
+ ),
1173
+ cfg.tts?.enabled ? React.createElement(Field, { label: t('ttsMaxChars') },
1174
+ React.createElement('input', {
1175
+ type: 'number',
1176
+ className: 'msgw-input',
1177
+ min: 100,
1178
+ max: 8000,
1179
+ value: cfg.tts?.maxChars ?? 4000,
1180
+ onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, maxChars: Number(e.target.value) || 4000 } }),
1181
+ })
1182
+ ) : null,
1183
+ React.createElement(Field, { label: t('voiceMode') },
1184
+ React.createElement(
1185
+ 'select',
1186
+ {
1187
+ className: 'msgw-select',
1188
+ value: cfg.telegram?.voiceMode || 'mirror',
1189
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, voiceMode: e.target.value } }),
1190
+ },
1191
+ React.createElement('option', { value: 'mirror' }, t('voiceModeMirror')),
1192
+ React.createElement('option', { value: 'always' }, t('voiceModeAlways')),
1193
+ React.createElement('option', { value: 'off' }, t('voiceModeOff'))
1194
+ )
1195
+ )
1196
+ ),
1197
+
1198
+ // Card: Discord Gateway
1199
+ React.createElement(
1200
+ 'div',
1201
+ { className: 'msgw-section-card' },
1202
+ React.createElement(
1203
+ 'div',
1204
+ { className: 'msgw-section-title' },
1205
+ React.createElement('span', null, `🎮 ${t('discord')}`),
1206
+ React.createElement(
1207
+ 'button',
1208
+ {
1209
+ type: 'button',
1210
+ className: `msgw-btn msgw-btn-mini ${cfg.discord?.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
1211
+ disabled: !!busy,
1212
+ onClick: () => save({ discord: { enabled: !cfg.discord?.enabled } }),
1213
+ },
1214
+ cfg.discord?.enabled ? t('disableDiscord') : t('enableDiscord')
1215
+ )
1216
+ ),
1217
+ React.createElement('div', { className: 'msgw-section-desc' }, t('discordSub')),
1218
+ React.createElement(Field, { label: t('discordToken'), hint: t('discordTokenHint') },
1219
+ React.createElement('input', {
1220
+ type: 'password',
1221
+ className: 'msgw-input',
1222
+ value: discordToken,
1223
+ placeholder: (status?.config?.discord?.botTokenConfigured || cfg.discord?.botTokenConfigured) ? '••••••••••••••••' : 'Discord Bot Token',
1224
+ onChange: (e) => setDiscordToken(e.target.value),
1225
+ })
1226
+ ),
1227
+ React.createElement(Field, { label: t('discordWebhook'), hint: t('discordWebhookHint') },
1228
+ React.createElement('input', {
1229
+ className: 'msgw-input',
1230
+ value: cfg.discord?.webhookUrl || '',
1231
+ placeholder: 'https://discord.com/api/webhooks/...',
1232
+ onChange: (e) => setCfg({ ...cfg, discord: { ...cfg.discord, webhookUrl: e.target.value } }),
1233
+ })
1234
+ ),
1235
+ React.createElement(Field, { label: t('discordChannels'), hint: t('discordChannelsHint') },
1236
+ React.createElement('textarea', {
1237
+ className: 'msgw-textarea',
1238
+ value: discordChannelsText,
1239
+ onChange: (e) => setDiscordChannelsText(e.target.value),
1240
+ rows: 2,
1241
+ placeholder: '123456789012345678, 987654321098765432',
1242
+ })
1243
+ )
1244
+ ),
1245
+
1246
+ // Card: Slack Gateway
1247
+ React.createElement(
1248
+ 'div',
1249
+ { className: 'msgw-section-card' },
1250
+ React.createElement(
1251
+ 'div',
1252
+ { className: 'msgw-section-title' },
1253
+ React.createElement('span', null, `💼 ${t('slack')}`),
1254
+ React.createElement(
1255
+ 'button',
1256
+ {
1257
+ type: 'button',
1258
+ className: `msgw-btn msgw-btn-mini ${cfg.slack?.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
1259
+ disabled: !!busy,
1260
+ onClick: () => save({ slack: { enabled: !cfg.slack?.enabled } }),
1261
+ },
1262
+ cfg.slack?.enabled ? t('disableSlack') : t('enableSlack')
1263
+ )
1264
+ ),
1265
+ React.createElement('div', { className: 'msgw-section-desc' }, t('slackSub')),
1266
+ React.createElement(Field, { label: t('slackToken'), hint: t('slackTokenHint') },
1267
+ React.createElement('input', {
1268
+ type: 'password',
1269
+ className: 'msgw-input',
1270
+ value: slackToken,
1271
+ placeholder: (status?.config?.slack?.botTokenConfigured || cfg.slack?.botTokenConfigured) ? '••••••••••••••••' : 'xoxb-...',
1272
+ onChange: (e) => setSlackToken(e.target.value),
1273
+ })
1274
+ ),
1275
+ React.createElement(Field, { label: t('slackWebhook'), hint: t('slackWebhookHint') },
1276
+ React.createElement('input', {
1277
+ className: 'msgw-input',
1278
+ value: cfg.slack?.webhookUrl || '',
1279
+ placeholder: 'https://hooks.slack.com/services/...',
1280
+ onChange: (e) => setCfg({ ...cfg, slack: { ...cfg.slack, webhookUrl: e.target.value } }),
1281
+ })
1282
+ ),
1283
+ React.createElement(Field, { label: t('slackDefaultChannel'), hint: t('slackDefaultChannelHint') },
1284
+ React.createElement('input', {
1285
+ className: 'msgw-input',
1286
+ value: cfg.slack?.defaultChannel || '',
1287
+ placeholder: '#general or C12345678',
1288
+ onChange: (e) => setCfg({ ...cfg, slack: { ...cfg.slack, defaultChannel: e.target.value } }),
1289
+ })
1290
+ )
1291
+ ),
1292
+
1293
+ // Card 6: Pairing Requests
1294
+ React.createElement(
1295
+ 'div',
1296
+ { className: 'msgw-section-card' },
1297
+ React.createElement(
1298
+ 'div',
1299
+ { className: 'msgw-section-title' },
1300
+ React.createElement('span', null, `🔑 ${t('pairing')}`),
1301
+ React.createElement(
1302
+ 'button',
1303
+ {
1304
+ type: 'button',
1305
+ className: 'msgw-btn msgw-btn-mini',
1306
+ disabled: !!busy,
1307
+ onClick: loadPairing,
1308
+ },
1309
+ t('pairingRefresh')
1310
+ )
1311
+ ),
1312
+ React.createElement('div', { className: 'msgw-section-desc' }, t('pairingSub')),
1313
+ !(pending && pending.length)
1314
+ ? React.createElement('div', { className: 'msgw-hint' }, t('pairingEmpty'))
1315
+ : React.createElement(
1316
+ 'table',
1317
+ { className: 'msgw-table' },
1318
+ React.createElement(
1319
+ 'tbody',
1320
+ null,
1321
+ pending.map((row) =>
1322
+ React.createElement(
1323
+ 'tr',
1324
+ { key: row.code },
1325
+ React.createElement('td', null, row.username ? `@${row.username}` : '—'),
1326
+ React.createElement('td', null, `ID: ${row.userId}`),
1327
+ React.createElement('td', { style: { fontFamily: 'monospace', fontWeight: 600 } }, row.code),
1328
+ React.createElement(
1329
+ 'td',
1330
+ { style: { textAlign: 'right' } },
1331
+ React.createElement(
1332
+ 'button',
1333
+ {
1334
+ type: 'button',
1335
+ className: 'msgw-btn msgw-btn-mini msgw-btn-primary',
1336
+ style: { marginRight: '6px' },
1337
+ disabled: !!busy,
1338
+ onClick: async () => {
1339
+ setBusy('approve')
1340
+ setErr('')
1341
+ try {
1342
+ const res = await fetch(`${ROUTE_PREFIX}/pairing/approve`, {
1343
+ method: 'POST',
1344
+ credentials: 'same-origin',
1345
+ headers: { 'Content-Type': 'application/json' },
1346
+ body: JSON.stringify({ code: row.code }),
1347
+ })
1348
+ const data = await res.json()
1349
+ if (!res.ok || !data.ok) throw new Error(data.error || res.status)
1350
+ await loadPairing()
1351
+ } catch (e) {
1352
+ setErr(String(e.message || e))
1353
+ } finally {
1354
+ setBusy('')
1355
+ }
1356
+ },
1357
+ },
1358
+ t('pairingApprove')
1359
+ ),
1360
+ React.createElement(
1361
+ 'button',
1362
+ {
1363
+ type: 'button',
1364
+ className: 'msgw-btn msgw-btn-mini msgw-btn-danger',
1365
+ disabled: !!busy,
1366
+ onClick: async () => {
1367
+ setBusy('reject')
1368
+ setErr('')
1369
+ try {
1370
+ const res = await fetch(`${ROUTE_PREFIX}/pairing/reject`, {
1371
+ method: 'POST',
1372
+ credentials: 'same-origin',
1373
+ headers: { 'Content-Type': 'application/json' },
1374
+ body: JSON.stringify({ code: row.code }),
1375
+ })
1376
+ const data = await res.json()
1377
+ if (!res.ok || !data.ok) throw new Error(data.error || res.status)
1378
+ await loadPairing()
1379
+ } catch (e) {
1380
+ setErr(String(e.message || e))
1381
+ } finally {
1382
+ setBusy('')
1383
+ }
1384
+ },
1385
+ },
1386
+ t('pairingReject')
1387
+ )
1388
+ )
1389
+ )
1390
+ )
1391
+ )
1392
+ )
1393
+ ),
1394
+
1395
+ // Card 7: Advanced Connection & Webhook
1396
+ React.createElement(
1397
+ 'div',
1398
+ { className: 'msgw-section-card' },
1399
+ React.createElement(
1400
+ 'button',
1401
+ {
1402
+ type: 'button',
1403
+ className: 'msgw-btn',
1404
+ style: { width: '100%', justifyContent: 'space-between' },
1405
+ onClick: () => setShowAdvanced((v) => !v),
1406
+ },
1407
+ React.createElement('span', null, showAdvanced ? t('hideAdvanced') : t('showAdvanced')),
1408
+ React.createElement('span', { style: { transform: showAdvanced ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
1409
+ React.createElement(Chevron)
1410
+ )
1411
+ ),
1412
+ showAdvanced ? React.createElement(
1413
+ 'div',
1414
+ { style: { display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '12px' } },
1415
+ React.createElement(Field, { label: t('transport') },
1416
+ React.createElement(
1417
+ 'select',
1418
+ {
1419
+ className: 'msgw-select',
1420
+ value: cfg.telegram?.transport || 'poll',
1421
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, transport: e.target.value } }),
1422
+ },
1423
+ React.createElement('option', { value: 'poll' }, t('transportPoll')),
1424
+ React.createElement('option', { value: 'webhook' }, t('transportWebhook'))
1425
+ )
1426
+ ),
1427
+ React.createElement(Field, { label: t('webhookUrl'), hint: t('webhookUrlHint') },
1428
+ React.createElement('input', {
1429
+ className: 'msgw-input',
1430
+ value: cfg.telegram?.webhookUrl || '',
1431
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, webhookUrl: e.target.value } }),
1432
+ })
1433
+ ),
1434
+ React.createElement(Field, {
1435
+ label: React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '8px' } },
1436
+ t('webhookSecret'),
1437
+ cfg.telegram?.webhookSecretConfigured
1438
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-ok' }, t('webhookSecretConfigured'))
1439
+ : (cfg.telegram?.transport === 'webhook'
1440
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-bad' }, t('webhookSecretRequired'))
1441
+ : null)
1442
+ ),
1443
+ hint: t('webhookSecretHint')
1444
+ },
1445
+ React.createElement('input', {
1446
+ type: 'password',
1447
+ className: 'msgw-input',
1448
+ value: webhookSecret,
1449
+ placeholder: cfg.telegram?.webhookSecretConfigured ? '••••••••••••••••' : '',
1450
+ onChange: (e) => setWebhookSecret(e.target.value),
1451
+ })
1452
+ ),
1453
+ React.createElement(Field, { label: t('pollTimeout') },
1454
+ React.createElement('input', {
1455
+ type: 'number',
1456
+ className: 'msgw-input',
1457
+ min: 10,
1458
+ max: 60,
1459
+ value: cfg.telegram?.pollTimeoutSeconds ?? 50,
1460
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollTimeoutSeconds: Number(e.target.value) || 50 } }),
1461
+ })
1462
+ ),
1463
+ React.createElement(Field, { label: t('pollInterval') },
1464
+ React.createElement('input', {
1465
+ type: 'number',
1466
+ className: 'msgw-input',
1467
+ min: 100,
1468
+ max: 5000,
1469
+ value: cfg.telegram?.pollIntervalMs ?? 500,
1470
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollIntervalMs: Number(e.target.value) || 500 } }),
1471
+ })
1472
+ ),
1473
+ React.createElement(Field, { label: t('idleTimeout') },
1474
+ React.createElement('input', {
1475
+ type: 'number',
1476
+ className: 'msgw-input',
1477
+ min: 300,
1478
+ max: 86400,
1479
+ value: Math.round((cfg.agent?.idleTimeoutMs ?? 3600000) / 1000),
1480
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, idleTimeoutMs: (Number(e.target.value) || 3600) * 1000 } }),
1481
+ })
1482
+ )
1483
+ ) : null
1484
+ ),
1485
+
1486
+ // Sticky Footer
1487
+ React.createElement(
1488
+ 'div',
1489
+ {
1490
+ className: 'msgw-foot',
1491
+ style: {
1492
+ display: 'flex',
1493
+ justifyContent: 'flex-end',
1494
+ gap: '10px',
1495
+ paddingTop: '12px',
1496
+ borderTop: '1px solid var(--dsw-alias-border-l2)',
1497
+ },
1498
+ },
1499
+ React.createElement(
1500
+ 'button',
1501
+ {
1502
+ type: 'button',
1503
+ className: 'msgw-btn msgw-btn-primary',
1504
+ disabled: !!busy,
1505
+ onClick: () => save({}),
1506
+ },
1507
+ busy === 'save' ? t('saving') : t('save')
1508
+ )
1509
+ )
1510
+ )
1511
+ }
1512
+
1513
+ function PluginCard(props) {
1514
+ const [open, setOpen] = React.useState(false)
1515
+ const t = props?.t || makeT(props?.locale)
1516
+ React.useEffect(() => {
1517
+ ensureCss()
1518
+ }, [])
1519
+
1520
+ // Plugins page row seat ('plugins.row.config'): the host page draws the title,
1521
+ // icon and crumb and provides its own padding, so view 'page' renders the form
1522
+ // bare (no card chrome, which would double the border and clip the left edge)
1523
+ // and view 'summary' renders the one-line state under the row title.
1524
+ if (props && props.view === 'summary') {
1525
+ return React.createElement('div', { className: 'msgw-sub' }, t('description'))
1526
+ }
1527
+ if (props && props.view === 'page') {
1528
+ return React.createElement(
1529
+ 'div',
1530
+ { className: 'msgw-page-seat' },
1531
+ React.createElement(ErrorBoundary, null,
1532
+ React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx, t })
1533
+ )
1534
+ )
1535
+ }
1536
+
1537
+ return React.createElement(
1538
+ 'li',
1539
+ { className: 'msgw-card msgw-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
1540
+ React.createElement(
1541
+ 'button',
1542
+ {
1543
+ type: 'button',
1544
+ className: 'msgw-head',
1545
+ style: {
1546
+ background: 'none',
1547
+ border: 'none',
1548
+ cursor: 'pointer',
1549
+ display: 'flex',
1550
+ alignItems: 'center',
1551
+ width: '100%',
1552
+ padding: 0,
1553
+ textAlign: 'left',
1554
+ },
1555
+ 'aria-expanded': open,
1556
+ onClick: () => setOpen((v) => !v),
1557
+ },
1558
+ React.createElement(
1559
+ 'div',
1560
+ { className: 'msgw-headText', style: { flex: 1 } },
1561
+ React.createElement('div', { className: 'msgw-title', style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
1562
+ React.createElement('div', { className: 'msgw-sub', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('description'))
1563
+ ),
1564
+ React.createElement(
1565
+ 'span',
1566
+ { className: 'msgw-chev', style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
1567
+ React.createElement(Chevron)
1568
+ )
1569
+ ),
1570
+ open
1571
+ ? React.createElement(
1572
+ 'div',
1573
+ { className: 'msgw-body', style: { marginTop: '16px' } },
1574
+ React.createElement(
1575
+ ErrorBoundary,
1576
+ null,
1577
+ React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx, t })
1578
+ )
1579
+ )
1580
+ : null
1581
+ )
1582
+ }
1583
+
1584
+ function refreshMirrorUntilVisible(ctx) {
1585
+ const visible = () => {
1586
+ try {
1587
+ const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
1588
+ const view = s?.describe?.()?.getSnapshot?.()?.view
1589
+ return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
1590
+ } catch (_) {
1591
+ return false
1592
+ }
1593
+ }
1594
+ if (visible()) return () => {}
1595
+ let tries = 0
1596
+ const timer = setInterval(() => {
1597
+ if (visible() || tries >= 15) { clearInterval(timer); return }
1598
+ tries += 1
1599
+ try {
1600
+ const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
1601
+ s?.describe?.()?.load?.()
1602
+ } catch (_) {
1603
+ /* safe fallback if settingsScope is not yet attached to container */
1604
+ }
1605
+ }, 1000)
1606
+ return () => clearInterval(timer)
1607
+ }
1608
+
1609
+ function apply(ctx) {
1610
+ try {
1611
+ if (typeof ctx.effect === 'function') {
1612
+ ctx.effect(() => {
1613
+ try {
1614
+ const loc = ctx?.get?.('locale') || ctx?.locale
1615
+ if (loc?.register) {
1616
+ loc.register(NS, { en, zh })
1617
+ }
1618
+ } catch (taken) {
1619
+ console.warn('[dsh-messenger-gateway] dictionary already registered:', taken && taken.message)
1620
+ }
1621
+ }, 'dsh-messenger-gateway: dictionaries')
1622
+ } else {
1623
+ const loc = ctx?.get?.('locale') || ctx?.locale
1624
+ if (loc?.register) {
1625
+ loc.register(NS, { en, zh })
1626
+ }
1627
+ }
1628
+ } catch (err) {
1629
+ console.warn('[dsh-messenger-gateway] dictionary registration error:', err && err.message)
1630
+ }
1631
+
1632
+ if (typeof ctx.effect === 'function') {
1633
+ ctx.effect(
1634
+ () => refreshMirrorUntilVisible(ctx),
1635
+ 'dsh-messenger-gateway: re-read the settings mirror until our namespace appears',
1636
+ )
1637
+ }
1638
+
1639
+ function registerSlotWhenReady(slotName, registerFn) {
1640
+ const slots = ctx?.get?.('slots') || ctx?.slots
1641
+ if (!slots) return
1642
+ if (typeof slots.inject === 'function') {
1643
+ try {
1644
+ slots.inject(slotName, () => {
1645
+ try {
1646
+ return registerFn()
1647
+ } catch (err) {
1648
+ console.warn('[dsh-messenger-gateway] Error registering slot ' + slotName + ':', err)
1649
+ }
1650
+ })
1651
+ return
1652
+ } catch (err) {
1653
+ console.warn('[dsh-messenger-gateway] Failed to inject slot ' + slotName + ':', err)
1654
+ }
1655
+ }
1656
+ if (typeof slots.register === 'function') {
1657
+ try {
1658
+ registerFn()
1659
+ } catch (err) {
1660
+ console.warn('[dsh-messenger-gateway] Failed direct registration for ' + slotName + ':', err)
1661
+ }
1662
+ }
1663
+ }
1664
+
1665
+ // Row seat on the Plugins page (current core): the plugin's row gains a
1666
+ // configure control whose page is the settings form. Registered first; the
1667
+ // legacy seats below stay as fallbacks so settings never become unreachable.
1668
+ registerSlotWhenReady('plugins.row.config', () => {
1669
+ const slots = ctx?.get?.('slots') || ctx?.slots
1670
+ return slots.register(
1671
+ {
1672
+ name: 'plugins.row.config',
1673
+ key: ROW_CONFIG_KEY,
1674
+ locale: NS,
1675
+ inject: () => ({ ctx }),
1676
+ },
1677
+ (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
1678
+ )
1679
+ })
1680
+
1681
+ registerSlotWhenReady('settings.plugin.item', () => {
1682
+ const slots = ctx?.get?.('slots') || ctx?.slots
1683
+ return slots.register(
1684
+ {
1685
+ name: 'settings.plugin.item',
1686
+ key: NS,
1687
+ locale: NS,
1688
+ inject: () => ({ ctx }),
1689
+ },
1690
+ (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
1691
+ )
1692
+ })
1693
+ }
1694
+
1695
+ module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
1696
+ return module.exports
1697
+ },
1698
+ })