@goodandready/dsh-messenger-gateway 0.3.13 → 0.3.18

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,629 +1,1307 @@
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
-
15
- const css =
16
- '.msgw-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
17
- '.msgw-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
18
- '.msgw-headText{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}' +
19
- '.msgw-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
20
- '.msgw-sub{color:var(--dsw-alias-label-secondary);font-size:13px}' +
21
- '.msgw-chev{margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6}' +
22
- '.msgw-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}' +
23
- '.msgw-form{display:flex;flex-direction:column;max-width:760px}' +
24
- '.msgw-section{display:flex;flex-direction:column}' +
25
- '.msgw-groupTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;padding:12px 0 0}' +
26
- '.msgw-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}' +
27
- '.msgw-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}' +
28
- '.msgw-hint{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
29
- '.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-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font:inherit;font-size:13px}' +
30
- '.msgw-textarea{height:auto;min-height:72px;padding:8px 12px}' +
31
- '.msgw-check{display:flex;gap:8px;align-items:center;padding:12px 0;color:var(--dsw-alias-label-primary);font-size:13px}' +
32
- '.msgw-linkbtn{appearance:none;font:inherit;cursor:pointer;background:0 0;border:0;color:var(--dsw-alias-label-secondary);font-size:13px;padding:8px 0;text-align:left}' +
33
- '.msgw-err{color:var(--dsw-alias-state-error-primary);font-size:12px;padding:8px 0}' +
34
- '.msgw-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px}' +
35
- '.msgw-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font-size:13px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}' +
36
- '.msgw-save:disabled{opacity:.5;cursor:default}' +
37
- '.msgw-ghost{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 14px;font-size:13px;background:0 0;color:var(--dsw-alias-label-primary)}' +
38
- '.msgw-row{display:flex;gap:8px;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}' +
39
- '.msgw-rowActions{display:flex;gap:6px;flex-shrink:0}' +
40
- '.msgw-mini{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 10px;font-size:12px;background:0 0;color:var(--dsw-alias-label-primary)}'
41
- const tagId = 'dsh-messenger-gateway/settings-card.module.css'
42
- if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']')) {
43
- const tag = document.createElement('style')
44
- tag.dataset.plugin = NS
45
- tag.dataset.pluginCss = tagId
46
- tag.textContent = css
47
- document.head.appendChild(tag)
48
- }
49
-
50
- const en = {
51
- title: 'Messenger gateway',
52
- description: 'Telegram bot: text, voice, photos and documents.',
53
- loading: 'Loading…',
54
- save: 'Save',
55
- saved: 'Saved',
56
- saving: 'Saving…',
57
- showAdvanced: 'Advanced settings',
58
- hideAdvanced: 'Hide advanced',
59
- telegram: 'Telegram',
60
- agent: 'Agent',
61
- media: 'Media',
62
- tts: 'Voice replies',
63
- advanced: 'Telegram polling',
64
- botToken: 'Bot token (write-only)',
65
- botTokenHint: 'Leave empty to keep the current token',
66
- allowedIds: 'Allowed user ids',
67
- allowedIdsHint: 'Comma-separated. Empty = all users',
68
- textFormat: 'Text format',
69
- textFormatHtml: 'Markdown Telegram HTML',
70
- textFormatPlain: 'Plain text',
71
- disableTelegram: 'Disable Telegram',
72
- enableTelegram: 'Enable Telegram',
73
- provider: 'Provider',
74
- providerHint: 'Empty = DSH default',
75
- model: 'Model',
76
- modelHint: 'Empty = DSH default',
77
- photoOnly: 'Photo without caption',
78
- photoPrompt: 'Wait for question (recommended)',
79
- photoRun: 'Reply immediately',
80
- instruction: 'Extra agent instruction',
81
- instructionHint: 'Prepended to each messenger message',
82
- instructionPlaceholder: 'Empty = built-in relay (Russian, no reasoning)',
83
- maxMessageLength: 'Max reply length (chars)',
84
- turnTimeout: 'Agent turn timeout (sec)',
85
- maxDocMb: 'Max document size (MB)',
86
- maxImageMb: 'Max photo size (MB)',
87
- maxTextKb: 'Max text from document (KB)',
88
- ttsEnable: 'Voice replies via dsh-tts',
89
- ttsMaxChars: 'Max spoken chars',
90
- pollTimeout: 'pollTimeoutSeconds',
91
- pollInterval: 'pollIntervalMs',
92
- idleTimeout: 'idle timeout (sec)',
93
- pairing: 'Pairing requests',
94
- pairingEmpty: 'No pending codes',
95
- pairingApprove: 'Approve',
96
- pairingReject: 'Reject',
97
- pairingRefresh: 'Refresh',
98
- groupsEnable: 'Respond in groups',
99
- groupMention: 'Groups require @mention / reply / command',
100
- reactionsEnable: 'React while processing (👀)',
101
- progressEnable: 'Progress message (thinking delete final)',
102
- statusIndicator: 'Status indicator (Online/Offline)',
103
- statusIndicatorHint: 'Sets the bot short description; bots have no presence dot. Opt-in, visible to all users.',
104
- transport: 'Transport',
105
- transportPoll: 'Long poll (getUpdates)',
106
- transportWebhook: 'Webhook',
107
- webhookUrl: 'Public webhook URL',
108
- webhookUrlHint: 'Full HTTPS URL Telegram will POST to',
109
- webhookSecret: 'Webhook secret (write-only)',
110
- webhookSecretHint: 'Leave empty to keep current',
111
- sessionScope: 'Group session scope',
112
- sessionScopeChat: 'Shared per chat/topic',
113
- sessionScopeUser: 'Per user (recommended)',
114
- voiceMode: 'Voice replies mode',
115
- voiceModeMirror: 'Mirror inbound voice',
116
- voiceModeAlways: 'Always try TTS',
117
- voiceModeOff: 'Off (unless /voice on)',
118
- notifyBridge: 'Notify → Telegram home',
119
- notifyBridgeHint: 'task_done/error from web sessions (not msgw-*)',
120
- notifyHome: 'Notify home name',
121
- quickActions: 'Quick actions keyboard (/new, /stop, /voice, /status)',
122
- artifactPreviews: 'Render diagram and table previews',
123
- }
124
- const ru = {
125
- title: 'Messenger gateway',
126
- description: 'Telegram-бот: текст, голос, фото и документы.',
127
- loading: 'Загрузка…',
128
- save: 'Сохранить',
129
- saved: 'Сохранено',
130
- saving: 'Сохранение…',
131
- showAdvanced: 'Расширенные настройки',
132
- hideAdvanced: 'Скрыть расширенные',
133
- telegram: 'Telegram',
134
- agent: 'Агент',
135
- media: 'Медиа',
136
- tts: 'Голосовые ответы',
137
- advanced: 'Polling Telegram',
138
- botToken: 'Bot token (только запись)',
139
- botTokenHint: 'Оставьте пустым, чтобы не менять',
140
- allowedIds: 'Разрешённые user id',
141
- allowedIdsHint: 'Через запятую. Пусто = все пользователи',
142
- textFormat: 'Формат текста',
143
- textFormatHtml: 'Markdown Telegram HTML',
144
- textFormatPlain: 'Простой текст',
145
- disableTelegram: 'Выключить Telegram',
146
- enableTelegram: 'Включить Telegram',
147
- provider: 'Provider',
148
- providerHint: 'Пусто = провайдер по умолчанию DSH',
149
- model: 'Model',
150
- modelHint: 'Пусто = модель по умолчанию',
151
- photoOnly: 'Фото без подписи',
152
- photoPrompt: 'Ждать вопрос (рекомендуется)',
153
- photoRun: 'Отвечать сразу',
154
- instruction: 'Доп. инструкция агенту',
155
- instructionHint: 'Добавляется к каждому сообщению из мессенджера',
156
- instructionPlaceholder: 'Пусто = встроенная инструкция (русский, без рассуждений)',
157
- maxMessageLength: 'Макс. длина ответа (символов)',
158
- turnTimeout: 'Таймаут хода агента (сек)',
159
- maxDocMb: 'Макс. размер документа (МБ)',
160
- maxImageMb: 'Макс. размер фото (МБ)',
161
- maxTextKb: 'Макс. текст из документа (КБ)',
162
- ttsEnable: 'Голосовые ответы (dsh-tts)',
163
- ttsMaxChars: 'Макс. символов для озвучки',
164
- pollTimeout: 'pollTimeoutSeconds',
165
- pollInterval: 'pollIntervalMs',
166
- idleTimeout: 'idle timeout (сек)',
167
- pairing: 'Запросы сопряжения',
168
- pairingEmpty: 'Нет ожидающих кодов',
169
- pairingApprove: 'Одобрить',
170
- pairingReject: 'Отклонить',
171
- pairingRefresh: 'Обновить',
172
- groupsEnable: 'Отвечать в группах',
173
- groupMention: 'В группах только @mention / reply / команда',
174
- reactionsEnable: 'Реакция 👀 пока думает',
175
- progressEnable: 'Прогресс: думаю удалить → финальный ответ',
176
- statusIndicator: 'Индикатор статуса (Online/Offline)',
177
- statusIndicatorHint: 'Меняет short description бота (у ботов нет точки присутствия). Опцionalно, видно всем.',
178
- transport: 'Транспорт',
179
- transportPoll: 'Long poll (getUpdates)',
180
- transportWebhook: 'Webhook',
181
- webhookUrl: 'Публичный URL webhook',
182
- webhookUrlHint: 'Полный HTTPS URL, куда Telegram шлёт обновления',
183
- webhookSecret: 'Webhook secret (только запись)',
184
- webhookSecretHint: 'Пусто = не менять',
185
- sessionScope: 'Сессии в группах',
186
- sessionScopeChat: 'Общая на чат/топик',
187
- sessionScopeUser: 'На каждого user (реком.)',
188
- voiceMode: 'Голосовые ответы',
189
- voiceModeMirror: 'Как входящее (mirror)',
190
- voiceModeAlways: 'Всегда TTS',
191
- voiceModeOff: 'Выкл (кроме /voice on)',
192
- notifyBridge: 'Notify Telegram home',
193
- notifyBridgeHint: 'task_done/error из web-сессий (не msgw-*)',
194
- notifyHome: 'Имя home для notify',
195
- quickActions: 'Быстрая клавиатура (/new, /stop, /voice, /status)',
196
- artifactPreviews: 'Превью артефактов (диаграммы и таблицы)',
197
- }
198
-
199
- function useActiveLocale(ctx) {
200
- return React.useSyncExternalStore(
201
- React.useMemo(() => (cb) => (ctx?.locale ? ctx.locale.subscribe(cb) : () => {}), [ctx]),
202
- React.useCallback(() => {
203
- if (ctx?.locale) {
204
- const active = ctx.locale.getSnapshot().active
205
- if (typeof active === 'string' && active) return active
206
- }
207
- return typeof navigator !== 'undefined' ? String(navigator.language || '').slice(0, 2) : 'en'
208
- }, [ctx]),
209
- )
210
- }
211
-
212
- function makeT(locale) {
213
- const DICT = String(locale || '').startsWith('ru') ? ru : en
214
- return (key) => DICT[key] || en[key] || key
215
- }
216
-
217
- function idsToText(ids) { return (Array.isArray(ids) ? ids : []).map(String).filter(Boolean).join(', ') }
218
- function textToIds(text) {
219
- return String(text || '').split(/[\s,]+/).map((s) => s.trim()).filter(Boolean).map(Number).filter((n) => Number.isFinite(n))
220
- }
221
- function bytesToMb(n) { return Math.round(Number(n || 0) / (1024 * 1024)) }
222
- function mbToBytes(n) { return Math.max(1, Math.round(Number(n || 1))) * 1024 * 1024 }
223
-
224
- function Field({ label, hint, children }) {
225
- return React.createElement('div', { className: 'msgw-field' },
226
- React.createElement('label', { className: 'msgw-label' }, label),
227
- hint ? React.createElement('div', { className: 'msgw-hint' }, hint) : null,
228
- children,
229
- )
230
- }
231
-
232
- const SETTINGS_KEYS = ['enabled', 'telegram', 'agent', 'media', 'tts']
233
-
234
- function draftFromStored(s) {
235
- const src = (s && typeof s === 'object') ? s : {}
236
- const t = src.telegram || {}
237
- const a = src.agent || {}
238
- const m = src.media || {}
239
- const tts = src.tts || {}
240
- return {
241
- enabled: src.enabled !== false,
242
- telegram: {
243
- enabled: t.enabled === true,
244
- allowedUserIds: Array.isArray(t.allowedUserIds) ? t.allowedUserIds : [],
245
- pollTimeoutSeconds: Number(t.pollTimeoutSeconds) || 50,
246
- pollIntervalMs: Number(t.pollIntervalMs) || 500,
247
- commands: Array.isArray(t.commands) ? t.commands : [],
248
- textFormat: t.textFormat === 'plain' ? 'plain' : 'html',
249
- homeChatId: t.homeChatId != null ? t.homeChatId : '',
250
- homeThreadId: Number(t.homeThreadId) || 0,
251
- homes: Array.isArray(t.homes) ? t.homes : [],
252
- pairingEnabled: t.pairingEnabled !== false,
253
- streaming: t.streaming === true,
254
- streamEditIntervalMs: Number(t.streamEditIntervalMs) || 1200,
255
- progressEnabled: t.progressEnabled !== false,
256
- approvalsEnabled: t.approvalsEnabled !== false,
257
- groupsEnabled: t.groupsEnabled !== false,
258
- groupRequireMention: t.groupRequireMention !== false,
259
- reactionsEnabled: t.reactionsEnabled !== false,
260
- statusIndicator: t.statusIndicator === true,
261
- statusOnline: t.statusOnline || 'Online',
262
- statusOffline: t.statusOffline || 'Offline',
263
- transport: t.transport === 'webhook' ? 'webhook' : 'poll',
264
- webhookUrl: t.webhookUrl || '',
265
- webhookPath: t.webhookPath || '/dsh-messenger-gateway/telegram/webhook',
266
- voiceMode: t.voiceMode || 'mirror',
267
- quickActions: t.quickActions === true,
268
- artifactPreviews: t.artifactPreviews !== false,
269
- notifyBridge: t.notifyBridge || { enabled: false, events: ['task_done', 'error'], home: 'default', excludeSessionPrefixes: ['msgw-'] },
270
- alerts: t.alerts || { enabled: false, chatId: '', threadId: 0, home: '', events: ['error', 'pairing'] },
271
- botToken: t.botToken || '',
272
- webhookSecret: t.webhookSecret || '',
273
- },
274
- agent: {
275
- provider: a.provider || '',
276
- model: a.model || '',
277
- instructionPrefix: a.instructionPrefix || '',
278
- maxMessageLength: Number(a.maxMessageLength) || 4000,
279
- turnTimeoutMs: Number(a.turnTimeoutMs) || 600000,
280
- idleTimeoutMs: Number(a.idleTimeoutMs) || 3600000,
281
- photoOnlyMode: a.photoOnlyMode || 'prompt',
282
- sessionScope: a.sessionScope || 'user',
283
- },
284
- media: {
285
- maxDocBytes: Number(m.maxDocBytes) || 20971520,
286
- maxImageBytes: Number(m.maxImageBytes) || 20971520,
287
- maxTextInjectBytes: Number(m.maxTextInjectBytes) || 102400,
288
- },
289
- tts: {
290
- enabled: tts.enabled === true,
291
- maxChars: Number(tts.maxChars) || 4000,
292
- voiceSummary: tts.voiceSummary === true,
293
- },
294
- }
295
- }
296
-
297
- function MessengerSettingsForm({ t, ctx }) {
298
- const [token, setToken] = React.useState('')
299
- const [webhookSecret, setWebhookSecret] = React.useState('')
300
- const [allowText, setAllowText] = React.useState('')
301
- const [pending, setPending] = React.useState([])
302
- const [err, setErr] = React.useState('')
303
- const [msg, setMsg] = React.useState('')
304
- const [busy, setBusy] = React.useState(false)
305
- const [showAdvanced, setShowAdvanced] = React.useState(false)
306
-
307
- const scope = React.useMemo(
308
- () => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined),
309
- [ctx],
310
- )
311
-
312
- const snapshot = React.useSyncExternalStore(
313
- React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
314
- React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
315
- React.useCallback(() => ({ status: 'loading' }), []),
316
- )
317
-
318
- const snapStatus = (snapshot && snapshot.status) || 'loading'
319
- const stored = (snapshot && snapshot.value) || {}
320
-
321
- const [cfg, setCfg] = React.useState(null)
322
-
323
- React.useEffect(() => {
324
- if (snapStatus === 'ready' && cfg === null) {
325
- setCfg(draftFromStored(stored))
326
- setAllowText(idsToText(stored?.telegram?.allowedUserIds))
327
- }
328
- }, [snapStatus, stored, cfg])
329
-
330
- const loadPairing = React.useCallback(async () => {
331
- try {
332
- const res = await fetch('/dsh-messenger-gateway/pairing', { credentials: 'same-origin' })
333
- const data = await res.json()
334
- if (res.ok && data.ok) setPending(Array.isArray(data.pending) ? data.pending : [])
335
- } catch {}
336
- }, [])
337
-
338
- React.useEffect(() => {
339
- if (snapStatus === 'ready') loadPairing()
340
- }, [snapStatus, loadPairing])
341
-
342
- const mergePatch = (base, patch) => ({
343
- ...base,
344
- ...patch,
345
- telegram: { ...base.telegram, ...(patch.telegram || {}) },
346
- tts: { ...base.tts, ...(patch.tts || {}) },
347
- agent: { ...base.agent, ...(patch.agent || {}) },
348
- media: { ...base.media, ...(patch.media || {}) },
349
- })
350
-
351
- const save = async (patch = {}) => {
352
- if (!scope || !cfg) return
353
- setBusy(true); setErr(''); setMsg('')
354
- try {
355
- let next = mergePatch(cfg, patch)
356
- if (token.trim()) next.telegram = { ...next.telegram, botToken: token.trim() }
357
- if (webhookSecret.trim()) next.telegram = { ...next.telegram, webhookSecret: webhookSecret.trim() }
358
- next.telegram.allowedUserIds = textToIds(allowText)
359
-
360
- const broken = []
361
- for (const key of SETTINGS_KEYS) {
362
- if (next[key] !== undefined) {
363
- try {
364
- await scope.set(key, next[key])
365
- } catch (e) {
366
- broken.push(key + ': ' + (e && e.message || String(e)))
367
- }
368
- }
369
- }
370
- if (broken.length) {
371
- setErr('Save failed — ' + broken.join('; '))
372
- return
373
- }
374
- setCfg(next)
375
- setToken('')
376
- setWebhookSecret('')
377
- setMsg(t('saved'))
378
- setTimeout(() => setMsg(''), 3000)
379
- await loadPairing()
380
- } catch (e) { setErr(String(e.message || e)) } finally { setBusy(false) }
381
- }
382
-
383
- if (snapStatus === 'loading') {
384
- return React.createElement('div', { className: 'msgw-sub', style: { padding: '12px 0' } }, t('loading'))
385
- }
386
- if (snapStatus !== 'ready') {
387
- return React.createElement('div', { className: 'msgw-err', style: { padding: '12px 0' } },
388
- 'Settings unavailable (snapshot status: ' + snapStatus + '). Host namespace may be missing.')
389
- }
390
- if (!cfg) {
391
- return React.createElement('div', { className: 'msgw-sub', style: { padding: '12px 0' } }, t('loading'))
392
- }
393
-
394
- return React.createElement('div', { className: 'msgw-form' },
395
- React.createElement('div', null,
396
- React.createElement('div', { className: 'msgw-groupTitle' }, t('telegram')),
397
- React.createElement(Field, { label: t('botToken'), hint: t('botTokenHint') },
398
- React.createElement('input', { type: 'password', className: 'msgw-input', value: token, placeholder: cfg.telegram?.botTokenConfigured ? '••••••••' : 'BotFather', onChange: (e) => setToken(e.target.value) }),
399
- ),
400
- React.createElement(Field, { label: t('allowedIds'), hint: t('allowedIdsHint') },
401
- React.createElement('textarea', { className: 'msgw-textarea', value: allowText, onChange: (e) => setAllowText(e.target.value), rows: 2 }),
402
- ),
403
- React.createElement(Field, { label: t('textFormat') },
404
- React.createElement('select', { className: 'msgw-select', value: cfg.telegram?.textFormat || 'html', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, textFormat: e.target.value } }) },
405
- React.createElement('option', { value: 'html' }, t('textFormatHtml')),
406
- React.createElement('option', { value: 'plain' }, t('textFormatPlain')),
407
- ),
408
- ),
409
- React.createElement('button', { type: 'button', className: 'msgw-ghost', disabled: busy, onClick: () => save({ telegram: { enabled: !cfg.telegram.enabled } }) }, cfg.telegram.enabled ? t('disableTelegram') : t('enableTelegram')),
410
- ),
411
-
412
- React.createElement('div', { className: 'msgw-section' },
413
- React.createElement('div', { className: 'msgw-groupTitle' }, t('agent')),
414
- React.createElement(Field, { label: t('provider'), hint: t('providerHint') },
415
- React.createElement('input', { className: 'msgw-input', value: cfg.agent?.provider || '', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, provider: e.target.value } }) }),
416
- ),
417
- React.createElement(Field, { label: t('model'), hint: t('modelHint') },
418
- React.createElement('input', { className: 'msgw-input', value: cfg.agent?.model || '', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, model: e.target.value } }) }),
419
- ),
420
- React.createElement(Field, { label: t('photoOnly') },
421
- React.createElement('select', { className: 'msgw-select', value: cfg.agent?.photoOnlyMode || 'prompt', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, photoOnlyMode: e.target.value } }) },
422
- React.createElement('option', { value: 'prompt' }, t('photoPrompt')),
423
- React.createElement('option', { value: 'run' }, t('photoRun')),
424
- ),
425
- ),
426
- React.createElement(Field, { label: t('sessionScope') },
427
- React.createElement('select', { className: 'msgw-select', value: cfg.agent?.sessionScope || 'user', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, sessionScope: e.target.value } }) },
428
- React.createElement('option', { value: 'user' }, t('sessionScopeUser')),
429
- React.createElement('option', { value: 'chat' }, t('sessionScopeChat')),
430
- ),
431
- ),
432
- React.createElement(Field, { label: t('instruction'), hint: t('instructionHint') },
433
- React.createElement('textarea', { className: 'msgw-textarea', value: cfg.agent?.instructionPrefix || '', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, instructionPrefix: e.target.value } }), rows: 3, placeholder: t('instructionPlaceholder') }),
434
- ),
435
- React.createElement(Field, { label: t('maxMessageLength') },
436
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 500, max: 8000, value: cfg.agent?.maxMessageLength ?? 4000, onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, maxMessageLength: Number(e.target.value) || 4000 } }) }),
437
- ),
438
- React.createElement(Field, { label: t('turnTimeout') },
439
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 30, max: 3600, value: Math.round((cfg.agent?.turnTimeoutMs ?? 600000) / 1000), onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, turnTimeoutMs: (Number(e.target.value) || 600) * 1000 } }) }),
440
- ),
441
- ),
442
-
443
- React.createElement('div', { className: 'msgw-section' },
444
- React.createElement('div', { className: 'msgw-groupTitle' }, t('media')),
445
- React.createElement(Field, { label: t('maxDocMb') },
446
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 1, max: 50, value: bytesToMb(cfg.media?.maxDocBytes), onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxDocBytes: mbToBytes(e.target.value) } }) }),
447
- ),
448
- React.createElement(Field, { label: t('maxImageMb') },
449
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 1, max: 50, value: bytesToMb(cfg.media?.maxImageBytes), onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxImageBytes: mbToBytes(e.target.value) } }) }),
450
- ),
451
- React.createElement(Field, { label: t('maxTextKb') },
452
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 16, max: 512, value: Math.round((cfg.media?.maxTextInjectBytes ?? 102400) / 1024), onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxTextInjectBytes: (Number(e.target.value) || 100) * 1024 } }) }),
453
- ),
454
- ),
455
-
456
- React.createElement('div', { className: 'msgw-section' },
457
- React.createElement('label', { className: 'msgw-check' },
458
- React.createElement('input', { type: 'checkbox', checked: !!cfg.tts?.enabled, onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, enabled: e.target.checked } }) }),
459
- t('ttsEnable'),
460
- ),
461
- cfg.tts?.enabled ? React.createElement(Field, { label: t('ttsMaxChars') },
462
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 100, max: 8000, value: cfg.tts?.maxChars ?? 4000, onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, maxChars: Number(e.target.value) || 4000 } }) }),
463
- ) : null,
464
- ),
465
-
466
- React.createElement('div', { className: 'msgw-section' },
467
- React.createElement('div', { className: 'msgw-groupTitle' }, t('pairing')),
468
- React.createElement('button', { type: 'button', className: 'msgw-linkbtn', disabled: busy, onClick: () => loadPairing() }, t('pairingRefresh')),
469
- !(pending && pending.length) ? React.createElement('div', { className: 'msgw-hint' }, t('pairingEmpty')) : pending.map((row) =>
470
- React.createElement('div', { key: row.code, className: 'msgw-row' },
471
- React.createElement('div', null, (row.username ? '@' + row.username + ' · ' : '') + 'id ' + row.userId + ' · ' + row.code),
472
- React.createElement('div', { className: 'msgw-rowActions' },
473
- React.createElement('button', {
474
- type: 'button', className: 'msgw-mini', disabled: busy, onClick: async () => {
475
- setBusy(true); setErr('')
476
- try {
477
- const res = await fetch('/dsh-messenger-gateway/pairing/approve', {
478
- method: 'POST', credentials: 'same-origin',
479
- headers: { 'Content-Type': 'application/json' },
480
- body: JSON.stringify({ code: row.code }),
481
- })
482
- const data = await res.json()
483
- if (!res.ok || !data.ok) throw new Error(data.error || res.status)
484
- await load()
485
- } catch (e) { setErr(String(e.message || e)) } finally { setBusy(false) }
486
- },
487
- }, t('pairingApprove')),
488
- React.createElement('button', {
489
- type: 'button', className: 'msgw-mini', disabled: busy, onClick: async () => {
490
- setBusy(true); setErr('')
491
- try {
492
- const res = await fetch('/dsh-messenger-gateway/pairing/reject', {
493
- method: 'POST', credentials: 'same-origin',
494
- headers: { 'Content-Type': 'application/json' },
495
- body: JSON.stringify({ code: row.code }),
496
- })
497
- const data = await res.json()
498
- if (!res.ok || !data.ok) throw new Error(data.error || res.status)
499
- await loadPairing()
500
- } catch (e) { setErr(String(e.message || e)) } finally { setBusy(false) }
501
- },
502
- }, t('pairingReject')),
503
- ),
504
- ),
505
- ),
506
- ),
507
-
508
- React.createElement('div', { className: 'msgw-section' },
509
- React.createElement('label', { className: 'msgw-check' },
510
- React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.groupsEnabled !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupsEnabled: e.target.checked } }) }),
511
- t('groupsEnable'),
512
- ),
513
- React.createElement('label', { className: 'msgw-check' },
514
- React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.groupRequireMention !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupRequireMention: e.target.checked } }) }),
515
- t('groupMention'),
516
- ),
517
- React.createElement('label', { className: 'msgw-check' },
518
- React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.reactionsEnabled !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, reactionsEnabled: e.target.checked } }) }),
519
- t('reactionsEnable'),
520
- ),
521
- React.createElement('label', { className: 'msgw-check' },
522
- React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.progressEnabled !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, progressEnabled: e.target.checked } }) }),
523
- t('progressEnable'),
524
- ),
525
- React.createElement('label', { className: 'msgw-check' },
526
- React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.statusIndicator === true, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, statusIndicator: e.target.checked } }) }),
527
- t('statusIndicator'),
528
- ),
529
- React.createElement('div', { className: 'msgw-hint' }, t('statusIndicatorHint')),
530
- React.createElement('label', { className: 'msgw-check' },
531
- React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.quickActions !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, quickActions: e.target.checked } }) }),
532
- t('quickActions'),
533
- ),
534
- React.createElement('label', { className: 'msgw-check' },
535
- React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.artifactPreviews !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, artifactPreviews: e.target.checked } }) }),
536
- t('artifactPreviews'),
537
- ),
538
- React.createElement(Field, { label: t('voiceMode') },
539
- React.createElement('select', { className: 'msgw-select', value: cfg.telegram?.voiceMode || 'mirror', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, voiceMode: e.target.value } }) },
540
- React.createElement('option', { value: 'mirror' }, t('voiceModeMirror')),
541
- React.createElement('option', { value: 'always' }, t('voiceModeAlways')),
542
- React.createElement('option', { value: 'off' }, t('voiceModeOff')),
543
- ),
544
- ),
545
- React.createElement('label', { className: 'msgw-check' },
546
- React.createElement('input', { type: 'checkbox', checked: !!cfg.telegram?.notifyBridge?.enabled, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, notifyBridge: { ...(cfg.telegram.notifyBridge || {}), enabled: e.target.checked } } }) }),
547
- t('notifyBridge'),
548
- ),
549
- React.createElement('div', { className: 'msgw-hint' }, t('notifyBridgeHint')),
550
- React.createElement(Field, { label: t('notifyHome') },
551
- React.createElement('input', { className: 'msgw-input', value: cfg.telegram?.notifyBridge?.home || 'default', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, notifyBridge: { ...(cfg.telegram.notifyBridge || {}), home: e.target.value } } }) }),
552
- ),
553
- ),
554
-
555
- React.createElement('button', { type: 'button', className: 'msgw-linkbtn', onClick: () => setShowAdvanced((v) => !v) }, showAdvanced ? t('hideAdvanced') : t('showAdvanced')),
556
- showAdvanced ? React.createElement('div', { className: 'msgw-section' },
557
- React.createElement('div', { className: 'msgw-groupTitle' }, t('advanced')),
558
- React.createElement(Field, { label: t('transport') },
559
- React.createElement('select', { className: 'msgw-select', value: cfg.telegram?.transport || 'poll', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, transport: e.target.value } }) },
560
- React.createElement('option', { value: 'poll' }, t('transportPoll')),
561
- React.createElement('option', { value: 'webhook' }, t('transportWebhook')),
562
- ),
563
- ),
564
- React.createElement(Field, { label: t('webhookUrl'), hint: t('webhookUrlHint') },
565
- React.createElement('input', { className: 'msgw-input', value: cfg.telegram?.webhookUrl || '', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, webhookUrl: e.target.value } }) }),
566
- ),
567
- React.createElement(Field, { label: t('webhookSecret'), hint: t('webhookSecretHint') },
568
- React.createElement('input', { type: 'password', className: 'msgw-input', value: webhookSecret, placeholder: cfg.telegram?.webhookSecretConfigured ? '••••••••' : '', onChange: (e) => setWebhookSecret(e.target.value) }),
569
- ),
570
- React.createElement(Field, { label: t('pollTimeout') },
571
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 10, max: 60, value: cfg.telegram?.pollTimeoutSeconds ?? 50, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollTimeoutSeconds: Number(e.target.value) || 50 } }) }),
572
- ),
573
- React.createElement(Field, { label: t('pollInterval') },
574
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 100, max: 5000, value: cfg.telegram?.pollIntervalMs ?? 500, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollIntervalMs: Number(e.target.value) || 500 } }) }),
575
- ),
576
- React.createElement(Field, { label: t('idleTimeout') },
577
- React.createElement('input', { type: 'number', className: 'msgw-input', min: 300, max: 86400, value: Math.round((cfg.agent?.idleTimeoutMs ?? 3600000) / 1000), onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, idleTimeoutMs: (Number(e.target.value) || 3600) * 1000 } }) }),
578
- ),
579
- ) : null,
580
-
581
- err ? React.createElement('div', { className: 'msgw-err' }, err) : null,
582
- msg ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-success-primary, #10b981)', fontSize: '13px', padding: '8px 0' } }, msg) : null,
583
- React.createElement('div', { className: 'msgw-foot' },
584
- React.createElement('button', { type: 'button', className: 'msgw-save', disabled: busy, onClick: () => save({}) }, busy ? t('saving') : t('save')),
585
- ),
586
- )
587
- }
588
-
589
- function PluginCard(props) {
590
- const [open, setOpen] = React.useState(false)
591
- const t = makeT(props.locale)
592
- return React.createElement('li', { className: 'msgw-card' },
593
- React.createElement('button', {
594
- type: 'button',
595
- className: 'msgw-head',
596
- 'aria-expanded': open,
597
- onClick: () => setOpen((v) => !v),
598
- },
599
- React.createElement('div', { className: 'msgw-headText' },
600
- React.createElement('div', { className: 'msgw-title' }, t('title')),
601
- React.createElement('div', { className: 'msgw-sub' }, t('description')),
602
- ),
603
- React.createElement('span', { className: 'msgw-chev' }, open ? '\u25B2' : '\u25BC'),
604
- ),
605
- open ? React.createElement('div', { className: 'msgw-body' },
606
- React.createElement(MessengerSettingsForm, { t, ctx: props.ctx }),
607
- ) : null,
608
- )
609
- }
610
-
611
- function apply(ctx) {
612
- ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-messenger-gateway: dictionaries')
613
- function useLocale() { return useActiveLocale(ctx) }
614
-
615
- ctx.slots.inject('settings.plugin.item', () =>
616
- ctx.slots.register({
617
- name: 'settings.plugin.item',
618
- key: NS,
619
- locale: NS,
620
- inject: () => ({ ctx }),
621
- }, (props) => React.createElement(PluginCard, { ...props, ctx, locale: useLocale() })),
622
- )
623
- }
624
-
625
- exports.apply = apply
626
- exports.inject = ['slots', 'locale', 'settingsScope']
627
- return module.exports
628
- },
629
- })
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 ru = {
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: сессии, steer, топики форума, инлайн-кнопки и голосовые ответы.',
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: '⚙️ Расширенные настройки подключения и polling',
138
+ hideAdvanced: 'Скрыть расширенные настройки',
139
+ telegram: 'Настройки бота Telegram',
140
+ telegramSub: 'Ключи доступа, белый список пользователей и параметры доставки сообщений.',
141
+ agent: 'Поведение агента и рассуждения',
142
+ agentSub: 'Маршрутизация модели, дополнительные инструкции и таймаут хода.',
143
+ media: 'Ограничения медиа и документов',
144
+ mediaSub: 'Лимиты на извлечение текста и размер загружаемых файлов.',
145
+ tts: 'Голосовые ответы (dsh-tts)',
146
+ ttsSub: 'Синтез аудиоответов на голосовые сообщения в чате.',
147
+ advanced: 'Параметры соединения и polling',
148
+ botToken: 'Токен бота Telegram',
149
+ botTokenHint: 'Получен от @BotFather. Оставьте пустым, чтобы сохранить текущий.',
150
+ allowedIds: 'Разрешённые 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: 'Макс. размер документа (МБ)',
170
+ maxImageMb: 'Макс. размер фото (МБ)',
171
+ maxTextKb: 'Макс. объём текста из документа (КБ)',
172
+ ttsEnable: 'Включить голосовые ответы (требуется dsh-tts)',
173
+ ttsMaxChars: 'Макс. символов для озвучивания',
174
+ pollTimeout: 'Таймаут Long Poll (сек)',
175
+ pollInterval: 'Интервал Long Poll (мс)',
176
+ idleTimeout: 'Таймаут неактивного чата (сек)',
177
+ pairing: 'Запросы сопряжения и авторизация',
178
+ pairingSub: 'Подтверждение доступа новых пользователей при ограниченном списке.',
179
+ pairingEmpty: 'Нет ожидающих кодов сопряжения',
180
+ pairingApprove: 'Одобрить',
181
+ pairingReject: 'Отклонить',
182
+ pairingRefresh: 'Обновить список',
183
+ groupsEnable: 'Отвечать в группах и супергруппах',
184
+ groupMention: 'В группах отвечать только на @упоминание, reply или /команду',
185
+ reactionsEnable: 'Реакция 👀 пока агент думает',
186
+ progressEnable: 'Индикатор прогресса (Думаю → Удалить → Ответ)',
187
+ statusIndicator: 'Индикатор статуса (Online/Offline)',
188
+ statusIndicatorHint: 'Меняет краткое описание бота в Telegram (видно всем).',
189
+ transport: 'Режим транспорта',
190
+ transportPoll: 'Long poll (getUpdates)',
191
+ transportWebhook: 'Webhook (HTTPS)',
192
+ webhookUrl: 'Публичный HTTPS URL webhook',
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: 'Пересылка событий task_done/error из web-сессий в указанный канал/топик',
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('ru') ? ru : 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
+ }, [loadStatus, loadPairing])
500
+
501
+ const handleSmoke = async () => {
502
+ setBusy('smoke')
503
+ setErr('')
504
+ setMsg('')
505
+ setSmokeResult(null)
506
+ try {
507
+ const res = await fetch(`${ROUTE_PREFIX}/smoke`, {
508
+ method: 'POST',
509
+ credentials: 'same-origin',
510
+ headers: { 'Content-Type': 'application/json' },
511
+ body: JSON.stringify({ timeoutMs: 8000 }),
512
+ })
513
+ const data = await res.json().catch(() => ({}))
514
+ if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
515
+ setSmokeResult(data)
516
+ setMsg(t('diag.smoke_ok', { latency: data.latencyMs, username: data.botUsername, id: data.botId }))
517
+ await loadStatus()
518
+ } catch (e) {
519
+ setErr(t('diag.smoke_err', { error: String(e.message || e) }))
520
+ } finally {
521
+ setBusy('')
522
+ }
523
+ }
524
+
525
+ const mergePatch = (base, patch) => ({
526
+ ...base,
527
+ ...patch,
528
+ telegram: { ...base.telegram, ...(patch.telegram || {}) },
529
+ tts: { ...base.tts, ...(patch.tts || {}) },
530
+ agent: { ...base.agent, ...(patch.agent || {}) },
531
+ media: { ...base.media, ...(patch.media || {}) },
532
+ })
533
+
534
+ const save = async (patch = {}) => {
535
+ if (!scope || !cfg) return
536
+ setBusy('save')
537
+ setErr('')
538
+ setMsg('')
539
+ try {
540
+ let next = mergePatch(cfg, patch)
541
+ if (token.trim()) next.telegram = { ...next.telegram, botToken: token.trim() }
542
+ if (webhookSecret.trim()) next.telegram = { ...next.telegram, webhookSecret: webhookSecret.trim() }
543
+ next.telegram.allowedUserIds = textToIds(allowText)
544
+
545
+ const broken = []
546
+ for (const key of SETTINGS_KEYS) {
547
+ if (next[key] !== undefined) {
548
+ try {
549
+ await scope.set(key, next[key])
550
+ } catch (e) {
551
+ broken.push(key + ': ' + (e && e.message || String(e)))
552
+ }
553
+ }
554
+ }
555
+ if (broken.length) {
556
+ setErr('Save failed ' + broken.join('; '))
557
+ return
558
+ }
559
+ setCfg(next)
560
+ setToken('')
561
+ setWebhookSecret('')
562
+ setMsg(t('saved'))
563
+ setTimeout(() => setMsg(''), 3000)
564
+ await loadPairing()
565
+ await loadStatus()
566
+ } catch (e) {
567
+ setErr(String(e.message || e))
568
+ } finally {
569
+ setBusy('')
570
+ }
571
+ }
572
+
573
+ if (snapStatus === 'loading') {
574
+ return React.createElement('div', { className: 'msgw-page' }, t('settings.loading'))
575
+ }
576
+ if (snapStatus !== 'ready') {
577
+ return React.createElement('div', { className: 'msgw-page' },
578
+ React.createElement('div', { className: 'msgw-banner-warning' }, t('settings.unavailable'))
579
+ )
580
+ }
581
+ if (!cfg) {
582
+ return React.createElement('div', { className: 'msgw-page' }, t('settings.loading'))
583
+ }
584
+
585
+ const isRunning = Boolean(status?.running)
586
+ const botUsername = status?.botUsername || (status?.config?.telegram?.botUsername) || ''
587
+ const hasToken = Boolean(status?.config?.telegram?.botTokenConfigured || cfg.telegram?.botTokenConfigured || token.trim())
588
+ const transportMode = cfg.telegram?.transport || 'poll'
589
+ const pendingCount = pending.length
590
+
591
+ return React.createElement(
592
+ 'div',
593
+ { className: 'msgw-page' },
594
+
595
+ // Header with badges
596
+ React.createElement(
597
+ 'div',
598
+ { className: 'msgw-header' },
599
+ React.createElement(
600
+ 'div',
601
+ { className: 'msgw-page-title' },
602
+ `💬 ${t('header.title')}`,
603
+ React.createElement(
604
+ 'span',
605
+ { className: `msgw-badge ${isRunning ? 'msgw-badge-ok' : 'msgw-badge-bad'}` },
606
+ isRunning
607
+ ? (botUsername ? t('badge.online', { username: botUsername }) : t('badge.running'))
608
+ : t('badge.stopped')
609
+ ),
610
+ React.createElement(
611
+ 'span',
612
+ { className: `msgw-badge ${hasToken ? 'msgw-badge-ok' : 'msgw-badge-warn'}` },
613
+ hasToken ? t('badge.token_ok') : t('badge.token_missing')
614
+ ),
615
+ React.createElement(
616
+ 'span',
617
+ { className: 'msgw-badge msgw-badge-ok' },
618
+ t('badge.transport', { mode: transportMode })
619
+ ),
620
+ pendingCount > 0
621
+ ? React.createElement(
622
+ 'span',
623
+ { className: 'msgw-badge msgw-badge-warn' },
624
+ t('badge.pairing_count', { count: pendingCount })
625
+ )
626
+ : null
627
+ ),
628
+ React.createElement('div', { className: 'msgw-page-sub' }, t('header.sub'))
629
+ ),
630
+
631
+ // Global Alert banners
632
+ err ? React.createElement('div', { className: 'msgw-alert-err' }, err) : null,
633
+ msg ? React.createElement('div', { className: 'msgw-alert-ok' }, msg) : null,
634
+
635
+ // Card 1: Telegram Connection & Bot
636
+ React.createElement(
637
+ 'div',
638
+ { className: 'msgw-section-card' },
639
+ React.createElement(
640
+ 'div',
641
+ { className: 'msgw-section-title' },
642
+ React.createElement('span', null, `🤖 ${t('telegram')}`),
643
+ React.createElement(
644
+ 'button',
645
+ {
646
+ type: 'button',
647
+ className: `msgw-btn msgw-btn-mini ${cfg.telegram.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
648
+ disabled: !!busy,
649
+ onClick: () => save({ telegram: { enabled: !cfg.telegram.enabled } }),
650
+ },
651
+ cfg.telegram.enabled ? t('disableTelegram') : t('enableTelegram')
652
+ )
653
+ ),
654
+ React.createElement('div', { className: 'msgw-section-desc' }, t('telegramSub')),
655
+ React.createElement(Field, { label: t('botToken'), hint: t('botTokenHint') },
656
+ React.createElement('input', {
657
+ type: 'password',
658
+ className: 'msgw-input',
659
+ value: token,
660
+ placeholder: hasToken ? '••••••••••••••••••••' : '123456789:ABCdefGHI...',
661
+ onChange: (e) => setToken(e.target.value),
662
+ })
663
+ ),
664
+ React.createElement(Field, { label: t('allowedIds'), hint: t('allowedIdsHint') },
665
+ React.createElement('textarea', {
666
+ className: 'msgw-textarea',
667
+ value: allowText,
668
+ onChange: (e) => setAllowText(e.target.value),
669
+ rows: 2,
670
+ placeholder: '108191529, 987654321',
671
+ })
672
+ ),
673
+ React.createElement(Field, { label: t('textFormat') },
674
+ React.createElement(
675
+ 'select',
676
+ {
677
+ className: 'msgw-select',
678
+ value: cfg.telegram?.textFormat || 'html',
679
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, textFormat: e.target.value } }),
680
+ },
681
+ React.createElement('option', { value: 'html' }, t('textFormatHtml')),
682
+ React.createElement('option', { value: 'plain' }, t('textFormatPlain'))
683
+ )
684
+ ),
685
+ React.createElement('label', { className: 'msgw-check' },
686
+ React.createElement('input', {
687
+ type: 'checkbox',
688
+ checked: cfg.telegram?.groupsEnabled !== false,
689
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupsEnabled: e.target.checked } }),
690
+ }),
691
+ t('groupsEnable')
692
+ ),
693
+ React.createElement('label', { className: 'msgw-check' },
694
+ React.createElement('input', {
695
+ type: 'checkbox',
696
+ checked: cfg.telegram?.groupRequireMention !== false,
697
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupRequireMention: e.target.checked } }),
698
+ }),
699
+ t('groupMention')
700
+ ),
701
+ React.createElement('label', { className: 'msgw-check' },
702
+ React.createElement('input', {
703
+ type: 'checkbox',
704
+ checked: cfg.telegram?.reactionsEnabled !== false,
705
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, reactionsEnabled: e.target.checked } }),
706
+ }),
707
+ t('reactionsEnable')
708
+ ),
709
+ React.createElement('label', { className: 'msgw-check' },
710
+ React.createElement('input', {
711
+ type: 'checkbox',
712
+ checked: cfg.telegram?.progressEnabled !== false,
713
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, progressEnabled: e.target.checked } }),
714
+ }),
715
+ t('progressEnable')
716
+ ),
717
+ React.createElement('label', { className: 'msgw-check' },
718
+ React.createElement('input', {
719
+ type: 'checkbox',
720
+ checked: cfg.telegram?.quickActions !== false,
721
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, quickActions: e.target.checked } }),
722
+ }),
723
+ t('quickActions')
724
+ ),
725
+ React.createElement('label', { className: 'msgw-check' },
726
+ React.createElement('input', {
727
+ type: 'checkbox',
728
+ checked: cfg.telegram?.artifactPreviews !== false,
729
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, artifactPreviews: e.target.checked } }),
730
+ }),
731
+ t('artifactPreviews')
732
+ )
733
+ ),
734
+
735
+ // Card 2: Diagnostics & Telegram Smoke Test
736
+ React.createElement(
737
+ 'div',
738
+ { className: 'msgw-section-card' },
739
+ React.createElement('div', { className: 'msgw-section-title' }, t('diag.title')),
740
+ React.createElement('div', { className: 'msgw-section-desc' }, t('diag.desc')),
741
+ React.createElement(
742
+ 'div',
743
+ { className: 'msgw-row' },
744
+ React.createElement(
745
+ 'button',
746
+ {
747
+ type: 'button',
748
+ className: 'msgw-btn msgw-btn-primary',
749
+ disabled: !!busy || !hasToken,
750
+ onClick: handleSmoke,
751
+ },
752
+ busy === 'smoke' ? t('diag.smoke_testing') : t('diag.smoke_btn')
753
+ )
754
+ ),
755
+ smokeResult
756
+ ? React.createElement(
757
+ 'div',
758
+ { className: 'msgw-preview' },
759
+ `✅ Latency: ${smokeResult.latencyMs} ms | Bot: @${smokeResult.botUsername || 'unknown'} (ID: ${smokeResult.botId})`
760
+ )
761
+ : null
762
+ ),
763
+
764
+ // Card 3: Telemetry & Gateway Counters
765
+ status?.stats ? React.createElement(
766
+ 'div',
767
+ { className: 'msgw-section-card' },
768
+ React.createElement('div', { className: 'msgw-section-title' }, t('stats.title')),
769
+ React.createElement('div', { className: 'msgw-section-desc' }, t('stats.desc')),
770
+ React.createElement(
771
+ 'div',
772
+ { className: 'msgw-stat-grid' },
773
+ React.createElement(
774
+ 'div',
775
+ { className: 'msgw-stat-box' },
776
+ React.createElement('div', { className: 'msgw-stat-val' }, String(status.stats.sent || 0)),
777
+ React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.sent'))
778
+ ),
779
+ React.createElement(
780
+ 'div',
781
+ { className: 'msgw-stat-box' },
782
+ React.createElement('div', { className: 'msgw-stat-val' }, String(status.stats.errors || 0)),
783
+ React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.errors'))
784
+ ),
785
+ React.createElement(
786
+ 'div',
787
+ { className: 'msgw-stat-box' },
788
+ React.createElement('div', { className: 'msgw-stat-val' }, String(status.activeChats || 0)),
789
+ React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.active_chats'))
790
+ ),
791
+ React.createElement(
792
+ 'div',
793
+ { className: 'msgw-stat-box' },
794
+ React.createElement('div', { className: 'msgw-stat-val' }, `${status.uptimeSec || 0}s`),
795
+ React.createElement('div', { className: 'msgw-stat-lbl' }, t('stats.uptime'))
796
+ )
797
+ )
798
+ ) : null,
799
+
800
+ // Card 4: Agent & Turn Execution
801
+ React.createElement(
802
+ 'div',
803
+ { className: 'msgw-section-card' },
804
+ React.createElement('div', { className: 'msgw-section-title' }, `🧠 ${t('agent')}`),
805
+ React.createElement('div', { className: 'msgw-section-desc' }, t('agentSub')),
806
+ React.createElement(Field, { label: t('provider'), hint: t('providerHint') },
807
+ React.createElement('input', {
808
+ className: 'msgw-input',
809
+ value: cfg.agent?.provider || '',
810
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, provider: e.target.value } }),
811
+ })
812
+ ),
813
+ React.createElement(Field, { label: t('model'), hint: t('modelHint') },
814
+ React.createElement('input', {
815
+ className: 'msgw-input',
816
+ value: cfg.agent?.model || '',
817
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, model: e.target.value } }),
818
+ })
819
+ ),
820
+ React.createElement(Field, { label: t('photoOnly') },
821
+ React.createElement(
822
+ 'select',
823
+ {
824
+ className: 'msgw-select',
825
+ value: cfg.agent?.photoOnlyMode || 'prompt',
826
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, photoOnlyMode: e.target.value } }),
827
+ },
828
+ React.createElement('option', { value: 'prompt' }, t('photoPrompt')),
829
+ React.createElement('option', { value: 'run' }, t('photoRun'))
830
+ )
831
+ ),
832
+ React.createElement(Field, { label: t('sessionScope') },
833
+ React.createElement(
834
+ 'select',
835
+ {
836
+ className: 'msgw-select',
837
+ value: cfg.agent?.sessionScope || 'user',
838
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, sessionScope: e.target.value } }),
839
+ },
840
+ React.createElement('option', { value: 'user' }, t('sessionScopeUser')),
841
+ React.createElement('option', { value: 'chat' }, t('sessionScopeChat'))
842
+ )
843
+ ),
844
+ React.createElement(Field, { label: t('instruction'), hint: t('instructionHint') },
845
+ React.createElement('textarea', {
846
+ className: 'msgw-textarea',
847
+ value: cfg.agent?.instructionPrefix || '',
848
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, instructionPrefix: e.target.value } }),
849
+ rows: 3,
850
+ placeholder: t('instructionPlaceholder'),
851
+ })
852
+ ),
853
+ React.createElement(Field, { label: t('maxMessageLength') },
854
+ React.createElement('input', {
855
+ type: 'number',
856
+ className: 'msgw-input',
857
+ min: 500,
858
+ max: 8000,
859
+ value: cfg.agent?.maxMessageLength ?? 4000,
860
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, maxMessageLength: Number(e.target.value) || 4000 } }),
861
+ })
862
+ ),
863
+ React.createElement(Field, { label: t('turnTimeout') },
864
+ React.createElement('input', {
865
+ type: 'number',
866
+ className: 'msgw-input',
867
+ min: 30,
868
+ max: 3600,
869
+ value: Math.round((cfg.agent?.turnTimeoutMs ?? 600000) / 1000),
870
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, turnTimeoutMs: (Number(e.target.value) || 600) * 1000 } }),
871
+ })
872
+ )
873
+ ),
874
+
875
+ // Card 5: Media & TTS
876
+ React.createElement(
877
+ 'div',
878
+ { className: 'msgw-section-card' },
879
+ React.createElement('div', { className: 'msgw-section-title' }, `📎 ${t('media')}`),
880
+ React.createElement('div', { className: 'msgw-section-desc' }, t('mediaSub')),
881
+ React.createElement(Field, { label: t('maxDocMb') },
882
+ React.createElement('input', {
883
+ type: 'number',
884
+ className: 'msgw-input',
885
+ min: 1,
886
+ max: 50,
887
+ value: bytesToMb(cfg.media?.maxDocBytes),
888
+ onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxDocBytes: mbToBytes(e.target.value) } }),
889
+ })
890
+ ),
891
+ React.createElement(Field, { label: t('maxImageMb') },
892
+ React.createElement('input', {
893
+ type: 'number',
894
+ className: 'msgw-input',
895
+ min: 1,
896
+ max: 50,
897
+ value: bytesToMb(cfg.media?.maxImageBytes),
898
+ onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxImageBytes: mbToBytes(e.target.value) } }),
899
+ })
900
+ ),
901
+ React.createElement(Field, { label: t('maxTextKb') },
902
+ React.createElement('input', {
903
+ type: 'number',
904
+ className: 'msgw-input',
905
+ min: 16,
906
+ max: 512,
907
+ value: Math.round((cfg.media?.maxTextInjectBytes ?? 102400) / 1024),
908
+ onChange: (e) => setCfg({ ...cfg, media: { ...cfg.media, maxTextInjectBytes: (Number(e.target.value) || 100) * 1024 } }),
909
+ })
910
+ ),
911
+ React.createElement('div', { style: { height: '8px' } }),
912
+ React.createElement('div', { className: 'msgw-section-title' }, `🔊 ${t('tts')}`),
913
+ React.createElement('div', { className: 'msgw-section-desc' }, t('ttsSub')),
914
+ React.createElement('label', { className: 'msgw-check' },
915
+ React.createElement('input', {
916
+ type: 'checkbox',
917
+ checked: !!cfg.tts?.enabled,
918
+ onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, enabled: e.target.checked } }),
919
+ }),
920
+ t('ttsEnable')
921
+ ),
922
+ cfg.tts?.enabled ? React.createElement(Field, { label: t('ttsMaxChars') },
923
+ React.createElement('input', {
924
+ type: 'number',
925
+ className: 'msgw-input',
926
+ min: 100,
927
+ max: 8000,
928
+ value: cfg.tts?.maxChars ?? 4000,
929
+ onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, maxChars: Number(e.target.value) || 4000 } }),
930
+ })
931
+ ) : null,
932
+ React.createElement(Field, { label: t('voiceMode') },
933
+ React.createElement(
934
+ 'select',
935
+ {
936
+ className: 'msgw-select',
937
+ value: cfg.telegram?.voiceMode || 'mirror',
938
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, voiceMode: e.target.value } }),
939
+ },
940
+ React.createElement('option', { value: 'mirror' }, t('voiceModeMirror')),
941
+ React.createElement('option', { value: 'always' }, t('voiceModeAlways')),
942
+ React.createElement('option', { value: 'off' }, t('voiceModeOff'))
943
+ )
944
+ )
945
+ ),
946
+
947
+ // Card 6: Pairing Requests
948
+ React.createElement(
949
+ 'div',
950
+ { className: 'msgw-section-card' },
951
+ React.createElement(
952
+ 'div',
953
+ { className: 'msgw-section-title' },
954
+ React.createElement('span', null, `🔑 ${t('pairing')}`),
955
+ React.createElement(
956
+ 'button',
957
+ {
958
+ type: 'button',
959
+ className: 'msgw-btn msgw-btn-mini',
960
+ disabled: !!busy,
961
+ onClick: loadPairing,
962
+ },
963
+ t('pairingRefresh')
964
+ )
965
+ ),
966
+ React.createElement('div', { className: 'msgw-section-desc' }, t('pairingSub')),
967
+ !(pending && pending.length)
968
+ ? React.createElement('div', { className: 'msgw-hint' }, t('pairingEmpty'))
969
+ : React.createElement(
970
+ 'table',
971
+ { className: 'msgw-table' },
972
+ React.createElement(
973
+ 'tbody',
974
+ null,
975
+ pending.map((row) =>
976
+ React.createElement(
977
+ 'tr',
978
+ { key: row.code },
979
+ React.createElement('td', null, row.username ? `@${row.username}` : '—'),
980
+ React.createElement('td', null, `ID: ${row.userId}`),
981
+ React.createElement('td', { style: { fontFamily: 'monospace', fontWeight: 600 } }, row.code),
982
+ React.createElement(
983
+ 'td',
984
+ { style: { textAlign: 'right' } },
985
+ React.createElement(
986
+ 'button',
987
+ {
988
+ type: 'button',
989
+ className: 'msgw-btn msgw-btn-mini msgw-btn-primary',
990
+ style: { marginRight: '6px' },
991
+ disabled: !!busy,
992
+ onClick: async () => {
993
+ setBusy('approve')
994
+ setErr('')
995
+ try {
996
+ const res = await fetch(`${ROUTE_PREFIX}/pairing/approve`, {
997
+ method: 'POST',
998
+ credentials: 'same-origin',
999
+ headers: { 'Content-Type': 'application/json' },
1000
+ body: JSON.stringify({ code: row.code }),
1001
+ })
1002
+ const data = await res.json()
1003
+ if (!res.ok || !data.ok) throw new Error(data.error || res.status)
1004
+ await loadPairing()
1005
+ } catch (e) {
1006
+ setErr(String(e.message || e))
1007
+ } finally {
1008
+ setBusy('')
1009
+ }
1010
+ },
1011
+ },
1012
+ t('pairingApprove')
1013
+ ),
1014
+ React.createElement(
1015
+ 'button',
1016
+ {
1017
+ type: 'button',
1018
+ className: 'msgw-btn msgw-btn-mini msgw-btn-danger',
1019
+ disabled: !!busy,
1020
+ onClick: async () => {
1021
+ setBusy('reject')
1022
+ setErr('')
1023
+ try {
1024
+ const res = await fetch(`${ROUTE_PREFIX}/pairing/reject`, {
1025
+ method: 'POST',
1026
+ credentials: 'same-origin',
1027
+ headers: { 'Content-Type': 'application/json' },
1028
+ body: JSON.stringify({ code: row.code }),
1029
+ })
1030
+ const data = await res.json()
1031
+ if (!res.ok || !data.ok) throw new Error(data.error || res.status)
1032
+ await loadPairing()
1033
+ } catch (e) {
1034
+ setErr(String(e.message || e))
1035
+ } finally {
1036
+ setBusy('')
1037
+ }
1038
+ },
1039
+ },
1040
+ t('pairingReject')
1041
+ )
1042
+ )
1043
+ )
1044
+ )
1045
+ )
1046
+ )
1047
+ ),
1048
+
1049
+ // Card 7: Advanced Connection & Webhook
1050
+ React.createElement(
1051
+ 'div',
1052
+ { className: 'msgw-section-card' },
1053
+ React.createElement(
1054
+ 'button',
1055
+ {
1056
+ type: 'button',
1057
+ className: 'msgw-btn',
1058
+ style: { width: '100%', justifyContent: 'space-between' },
1059
+ onClick: () => setShowAdvanced((v) => !v),
1060
+ },
1061
+ React.createElement('span', null, showAdvanced ? t('hideAdvanced') : t('showAdvanced')),
1062
+ React.createElement('span', { style: { transform: showAdvanced ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
1063
+ React.createElement(Chevron)
1064
+ )
1065
+ ),
1066
+ showAdvanced ? React.createElement(
1067
+ 'div',
1068
+ { style: { display: 'flex', flexDirection: 'column', gap: '12px', marginTop: '12px' } },
1069
+ React.createElement(Field, { label: t('transport') },
1070
+ React.createElement(
1071
+ 'select',
1072
+ {
1073
+ className: 'msgw-select',
1074
+ value: cfg.telegram?.transport || 'poll',
1075
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, transport: e.target.value } }),
1076
+ },
1077
+ React.createElement('option', { value: 'poll' }, t('transportPoll')),
1078
+ React.createElement('option', { value: 'webhook' }, t('transportWebhook'))
1079
+ )
1080
+ ),
1081
+ React.createElement(Field, { label: t('webhookUrl'), hint: t('webhookUrlHint') },
1082
+ React.createElement('input', {
1083
+ className: 'msgw-input',
1084
+ value: cfg.telegram?.webhookUrl || '',
1085
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, webhookUrl: e.target.value } }),
1086
+ })
1087
+ ),
1088
+ React.createElement(Field, { label: t('webhookSecret'), hint: t('webhookSecretHint') },
1089
+ React.createElement('input', {
1090
+ type: 'password',
1091
+ className: 'msgw-input',
1092
+ value: webhookSecret,
1093
+ placeholder: cfg.telegram?.webhookSecretConfigured ? '••••••••••••••••' : '',
1094
+ onChange: (e) => setWebhookSecret(e.target.value),
1095
+ })
1096
+ ),
1097
+ React.createElement(Field, { label: t('pollTimeout') },
1098
+ React.createElement('input', {
1099
+ type: 'number',
1100
+ className: 'msgw-input',
1101
+ min: 10,
1102
+ max: 60,
1103
+ value: cfg.telegram?.pollTimeoutSeconds ?? 50,
1104
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollTimeoutSeconds: Number(e.target.value) || 50 } }),
1105
+ })
1106
+ ),
1107
+ React.createElement(Field, { label: t('pollInterval') },
1108
+ React.createElement('input', {
1109
+ type: 'number',
1110
+ className: 'msgw-input',
1111
+ min: 100,
1112
+ max: 5000,
1113
+ value: cfg.telegram?.pollIntervalMs ?? 500,
1114
+ onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, pollIntervalMs: Number(e.target.value) || 500 } }),
1115
+ })
1116
+ ),
1117
+ React.createElement(Field, { label: t('idleTimeout') },
1118
+ React.createElement('input', {
1119
+ type: 'number',
1120
+ className: 'msgw-input',
1121
+ min: 300,
1122
+ max: 86400,
1123
+ value: Math.round((cfg.agent?.idleTimeoutMs ?? 3600000) / 1000),
1124
+ onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, idleTimeoutMs: (Number(e.target.value) || 3600) * 1000 } }),
1125
+ })
1126
+ )
1127
+ ) : null
1128
+ ),
1129
+
1130
+ // Sticky Footer
1131
+ React.createElement(
1132
+ 'div',
1133
+ {
1134
+ className: 'msgw-foot',
1135
+ style: {
1136
+ display: 'flex',
1137
+ justifyContent: 'flex-end',
1138
+ gap: '10px',
1139
+ paddingTop: '12px',
1140
+ borderTop: '1px solid var(--dsw-alias-border-l2)',
1141
+ },
1142
+ },
1143
+ React.createElement(
1144
+ 'button',
1145
+ {
1146
+ type: 'button',
1147
+ className: 'msgw-btn msgw-btn-primary',
1148
+ disabled: !!busy,
1149
+ onClick: () => save({}),
1150
+ },
1151
+ busy === 'save' ? t('saving') : t('save')
1152
+ )
1153
+ )
1154
+ )
1155
+ }
1156
+
1157
+ function PluginCard(props) {
1158
+ const [open, setOpen] = React.useState(false)
1159
+ const t = props?.t || makeT(props?.locale)
1160
+ React.useEffect(() => {
1161
+ ensureCss()
1162
+ }, [])
1163
+
1164
+ return React.createElement(
1165
+ 'li',
1166
+ { className: 'msgw-card msgw-section-card', style: { listStyle: 'none', marginBottom: '12px' } },
1167
+ React.createElement(
1168
+ 'button',
1169
+ {
1170
+ type: 'button',
1171
+ className: 'msgw-head',
1172
+ style: {
1173
+ background: 'none',
1174
+ border: 'none',
1175
+ cursor: 'pointer',
1176
+ display: 'flex',
1177
+ alignItems: 'center',
1178
+ width: '100%',
1179
+ padding: 0,
1180
+ textAlign: 'left',
1181
+ },
1182
+ 'aria-expanded': open,
1183
+ onClick: () => setOpen((v) => !v),
1184
+ },
1185
+ React.createElement(
1186
+ 'div',
1187
+ { className: 'msgw-headText', style: { flex: 1 } },
1188
+ React.createElement('div', { className: 'msgw-title', style: { fontWeight: 600, fontSize: '15px' } }, t('title')),
1189
+ React.createElement('div', { className: 'msgw-sub', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('description'))
1190
+ ),
1191
+ React.createElement(
1192
+ 'span',
1193
+ { className: 'msgw-chev', style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
1194
+ React.createElement(Chevron)
1195
+ )
1196
+ ),
1197
+ open
1198
+ ? React.createElement(
1199
+ 'div',
1200
+ { className: 'msgw-body', style: { marginTop: '16px' } },
1201
+ React.createElement(
1202
+ ErrorBoundary,
1203
+ null,
1204
+ React.createElement(SettingsPage, { ...props, ctx: (props && props.ctx) || ctx, t })
1205
+ )
1206
+ )
1207
+ : null
1208
+ )
1209
+ }
1210
+
1211
+ function refreshMirrorUntilVisible(ctx) {
1212
+ const visible = () => {
1213
+ try {
1214
+ const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
1215
+ const view = s?.describe?.()?.getSnapshot?.()?.view
1216
+ return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
1217
+ } catch (_) {
1218
+ return false
1219
+ }
1220
+ }
1221
+ if (visible()) return () => {}
1222
+ let tries = 0
1223
+ const timer = setInterval(() => {
1224
+ if (visible() || tries >= 15) { clearInterval(timer); return }
1225
+ tries += 1
1226
+ try {
1227
+ const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
1228
+ s?.describe?.()?.load?.()
1229
+ } catch (_) {}
1230
+ }, 1000)
1231
+ return () => clearInterval(timer)
1232
+ }
1233
+
1234
+ function apply(ctx) {
1235
+ try {
1236
+ if (typeof ctx.effect === 'function') {
1237
+ ctx.effect(() => {
1238
+ try {
1239
+ const loc = ctx?.get?.('locale') || ctx?.locale
1240
+ if (loc?.register) {
1241
+ loc.register(NS, { en, ru })
1242
+ }
1243
+ } catch (taken) {
1244
+ console.warn('[dsh-messenger-gateway] словарь уже зарегистрирован:', taken && taken.message)
1245
+ }
1246
+ }, 'dsh-messenger-gateway: dictionaries')
1247
+ } else {
1248
+ const loc = ctx?.get?.('locale') || ctx?.locale
1249
+ if (loc?.register) {
1250
+ loc.register(NS, { en, ru })
1251
+ }
1252
+ }
1253
+ } catch (err) {
1254
+ console.warn('[dsh-messenger-gateway] ошибка регистрации словаря:', err && err.message)
1255
+ }
1256
+
1257
+ if (typeof ctx.effect === 'function') {
1258
+ ctx.effect(
1259
+ () => refreshMirrorUntilVisible(ctx),
1260
+ 'dsh-messenger-gateway: re-read the settings mirror until our namespace appears',
1261
+ )
1262
+ }
1263
+
1264
+ function registerSlotWhenReady(slotName, registerFn) {
1265
+ const slots = ctx?.get?.('slots') || ctx?.slots
1266
+ if (!slots) return
1267
+ if (typeof slots.inject === 'function') {
1268
+ try {
1269
+ slots.inject(slotName, () => {
1270
+ try {
1271
+ return registerFn()
1272
+ } catch (err) {
1273
+ console.warn('[dsh-messenger-gateway] Error registering slot ' + slotName + ':', err)
1274
+ }
1275
+ })
1276
+ return
1277
+ } catch (err) {
1278
+ console.warn('[dsh-messenger-gateway] Failed to inject slot ' + slotName + ':', err)
1279
+ }
1280
+ }
1281
+ if (typeof slots.register === 'function') {
1282
+ try {
1283
+ registerFn()
1284
+ } catch (err) {
1285
+ console.warn('[dsh-messenger-gateway] Failed direct registration for ' + slotName + ':', err)
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ registerSlotWhenReady('settings.plugin.item', () => {
1291
+ const slots = ctx?.get?.('slots') || ctx?.slots
1292
+ return slots.register(
1293
+ {
1294
+ name: 'settings.plugin.item',
1295
+ key: NS,
1296
+ locale: NS,
1297
+ inject: () => ({ ctx }),
1298
+ },
1299
+ (props) => React.createElement(ErrorBoundary, null, React.createElement(PluginCard, { ...props, ctx: (props && props.ctx) || ctx }))
1300
+ )
1301
+ })
1302
+ }
1303
+
1304
+ module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
1305
+ return module.exports
1306
+ },
1307
+ })