@goodandready/dsh-messenger-gateway 0.3.21 → 0.4.0

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