@goodandready/dsh-messenger-gateway 0.1.0 → 0.2.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,21 +1,241 @@
1
+ // dsh-messenger-gateway — browser (client) half.
2
+ // Settings card on Plugins → Plugin settings tab (settings.plugin.item).
3
+ // Fallback to sidebar settings.section when the slot is unavailable.
4
+
1
5
  window.__ModuleLoader__.load({
2
6
  id: '@goodandready/dsh-messenger-gateway',
3
7
  factory: (require) => {
4
8
  var module = { exports: {} }
5
9
  var exports = module.exports
10
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
6
11
  const React = require('react')
7
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
+ saving: 'Saving…',
56
+ showAdvanced: 'Advanced settings',
57
+ hideAdvanced: 'Hide advanced',
58
+ telegram: 'Telegram',
59
+ agent: 'Agent',
60
+ media: 'Media',
61
+ tts: 'Voice replies',
62
+ advanced: 'Telegram polling',
63
+ botToken: 'Bot token (write-only)',
64
+ botTokenHint: 'Leave empty to keep the current token',
65
+ allowedIds: 'Allowed user ids',
66
+ allowedIdsHint: 'Comma-separated. Empty = all users',
67
+ textFormat: 'Text format',
68
+ textFormatHtml: 'Markdown → Telegram HTML',
69
+ textFormatPlain: 'Plain text',
70
+ disableTelegram: 'Disable Telegram',
71
+ enableTelegram: 'Enable Telegram',
72
+ provider: 'Provider',
73
+ providerHint: 'Empty = DSH default',
74
+ model: 'Model',
75
+ modelHint: 'Empty = DSH default',
76
+ photoOnly: 'Photo without caption',
77
+ photoPrompt: 'Wait for question (recommended)',
78
+ photoRun: 'Reply immediately',
79
+ instruction: 'Extra agent instruction',
80
+ instructionHint: 'Prepended to each messenger message',
81
+ instructionPlaceholder: 'Empty = built-in relay (Russian, no reasoning)',
82
+ maxMessageLength: 'Max reply length (chars)',
83
+ turnTimeout: 'Agent turn timeout (sec)',
84
+ maxDocMb: 'Max document size (MB)',
85
+ maxImageMb: 'Max photo size (MB)',
86
+ maxTextKb: 'Max text from document (KB)',
87
+ ttsEnable: 'Voice replies via dsh-tts',
88
+ ttsMaxChars: 'Max spoken chars',
89
+ pollTimeout: 'pollTimeoutSeconds',
90
+ pollInterval: 'pollIntervalMs',
91
+ idleTimeout: 'idle timeout (sec)',
92
+ pairing: 'Pairing requests',
93
+ pairingEmpty: 'No pending codes',
94
+ pairingApprove: 'Approve',
95
+ pairingReject: 'Reject',
96
+ pairingRefresh: 'Refresh',
97
+ groupsEnable: 'Respond in groups',
98
+ groupMention: 'Groups require @mention / reply / command',
99
+ reactionsEnable: 'React while processing (👀)',
100
+ progressEnable: 'Progress message (thinking → delete → final)',
101
+ transport: 'Transport',
102
+ transportPoll: 'Long poll (getUpdates)',
103
+ transportWebhook: 'Webhook',
104
+ webhookUrl: 'Public webhook URL',
105
+ webhookUrlHint: 'Full HTTPS URL Telegram will POST to',
106
+ webhookSecret: 'Webhook secret (write-only)',
107
+ webhookSecretHint: 'Leave empty to keep current',
108
+ sessionScope: 'Group session scope',
109
+ sessionScopeChat: 'Shared per chat/topic',
110
+ sessionScopeUser: 'Per user (recommended)',
111
+ voiceMode: 'Voice replies mode',
112
+ voiceModeMirror: 'Mirror inbound voice',
113
+ voiceModeAlways: 'Always try TTS',
114
+ voiceModeOff: 'Off (unless /voice on)',
115
+ notifyBridge: 'Notify → Telegram home',
116
+ notifyBridgeHint: 'task_done/error from web sessions (not msgw-*)',
117
+ notifyHome: 'Notify home name',
118
+ }
119
+ const ru = {
120
+ title: 'Messenger gateway',
121
+ description: 'Telegram-бот: текст, голос, фото и документы.',
122
+ loading: 'Загрузка…',
123
+ save: 'Сохранить',
124
+ saving: 'Сохранение…',
125
+ showAdvanced: 'Расширенные настройки',
126
+ hideAdvanced: 'Скрыть расширенные',
127
+ telegram: 'Telegram',
128
+ agent: 'Агент',
129
+ media: 'Медиа',
130
+ tts: 'Голосовые ответы',
131
+ advanced: 'Polling Telegram',
132
+ botToken: 'Bot token (только запись)',
133
+ botTokenHint: 'Оставьте пустым, чтобы не менять',
134
+ allowedIds: 'Разрешённые user id',
135
+ allowedIdsHint: 'Через запятую. Пусто = все пользователи',
136
+ textFormat: 'Формат текста',
137
+ textFormatHtml: 'Markdown → Telegram HTML',
138
+ textFormatPlain: 'Простой текст',
139
+ disableTelegram: 'Выключить Telegram',
140
+ enableTelegram: 'Включить Telegram',
141
+ provider: 'Provider',
142
+ providerHint: 'Пусто = провайдер по умолчанию DSH',
143
+ model: 'Model',
144
+ modelHint: 'Пусто = модель по умолчанию',
145
+ photoOnly: 'Фото без подписи',
146
+ photoPrompt: 'Ждать вопрос (рекомендуется)',
147
+ photoRun: 'Отвечать сразу',
148
+ instruction: 'Доп. инструкция агенту',
149
+ instructionHint: 'Добавляется к каждому сообщению из мессенджера',
150
+ instructionPlaceholder: 'Пусто = встроенная инструкция (русский, без рассуждений)',
151
+ maxMessageLength: 'Макс. длина ответа (символов)',
152
+ turnTimeout: 'Таймаут хода агента (сек)',
153
+ maxDocMb: 'Макс. размер документа (МБ)',
154
+ maxImageMb: 'Макс. размер фото (МБ)',
155
+ maxTextKb: 'Макс. текст из документа (КБ)',
156
+ ttsEnable: 'Голосовые ответы (dsh-tts)',
157
+ ttsMaxChars: 'Макс. символов для озвучки',
158
+ pollTimeout: 'pollTimeoutSeconds',
159
+ pollInterval: 'pollIntervalMs',
160
+ idleTimeout: 'idle timeout (сек)',
161
+ pairing: 'Запросы сопряжения',
162
+ pairingEmpty: 'Нет ожидающих кодов',
163
+ pairingApprove: 'Одобрить',
164
+ pairingReject: 'Отклонить',
165
+ pairingRefresh: 'Обновить',
166
+ groupsEnable: 'Отвечать в группах',
167
+ groupMention: 'В группах только @mention / reply / команда',
168
+ reactionsEnable: 'Реакция 👀 пока думает',
169
+ progressEnable: 'Прогресс: думаю → удалить → финальный ответ',
170
+ transport: 'Транспорт',
171
+ transportPoll: 'Long poll (getUpdates)',
172
+ transportWebhook: 'Webhook',
173
+ webhookUrl: 'Публичный URL webhook',
174
+ webhookUrlHint: 'Полный HTTPS URL, куда Telegram шлёт обновления',
175
+ webhookSecret: 'Webhook secret (только запись)',
176
+ webhookSecretHint: 'Пусто = не менять',
177
+ sessionScope: 'Сессии в группах',
178
+ sessionScopeChat: 'Общая на чат/топик',
179
+ sessionScopeUser: 'На каждого user (реком.)',
180
+ voiceMode: 'Голосовые ответы',
181
+ voiceModeMirror: 'Как входящее (mirror)',
182
+ voiceModeAlways: 'Всегда TTS',
183
+ voiceModeOff: 'Выкл (кроме /voice on)',
184
+ notifyBridge: 'Notify → Telegram home',
185
+ notifyBridgeHint: 'task_done/error из web-сессий (не msgw-*)',
186
+ notifyHome: 'Имя home для notify',
187
+ }
188
+
189
+ function useActiveLocale(ctx) {
190
+ return React.useSyncExternalStore(
191
+ React.useMemo(() => (cb) => (ctx?.locale ? ctx.locale.subscribe(cb) : () => {}), [ctx]),
192
+ React.useCallback(() => {
193
+ if (ctx?.locale) {
194
+ const active = ctx.locale.getSnapshot().active
195
+ if (typeof active === 'string' && active) return active
196
+ }
197
+ return typeof navigator !== 'undefined' ? String(navigator.language || '').slice(0, 2) : 'en'
198
+ }, [ctx]),
199
+ )
200
+ }
201
+
202
+ function makeT(locale) {
203
+ const DICT = String(locale || '').startsWith('ru') ? ru : en
204
+ return (key) => DICT[key] || en[key] || key
205
+ }
206
+
8
207
  function idsToText(ids) { return (Array.isArray(ids) ? ids : []).map(String).filter(Boolean).join(', ') }
9
208
  function textToIds(text) {
10
209
  return String(text || '').split(/[\s,]+/).map((s) => s.trim()).filter(Boolean).map(Number).filter((n) => Number.isFinite(n))
11
210
  }
211
+ function bytesToMb(n) { return Math.round(Number(n || 0) / (1024 * 1024)) }
212
+ function mbToBytes(n) { return Math.max(1, Math.round(Number(n || 1))) * 1024 * 1024 }
213
+
214
+ function Field({ label, hint, children }) {
215
+ return React.createElement('div', { className: 'msgw-field' },
216
+ React.createElement('label', { className: 'msgw-label' }, label),
217
+ hint ? React.createElement('div', { className: 'msgw-hint' }, hint) : null,
218
+ children,
219
+ )
220
+ }
12
221
 
13
- function Section() {
222
+ function MessengerSettingsForm({ t }) {
14
223
  const [cfg, setCfg] = React.useState(null)
15
224
  const [token, setToken] = React.useState('')
225
+ const [webhookSecret, setWebhookSecret] = React.useState('')
16
226
  const [allowText, setAllowText] = React.useState('')
227
+ const [pending, setPending] = React.useState([])
17
228
  const [err, setErr] = React.useState('')
18
229
  const [busy, setBusy] = React.useState(false)
230
+ const [showAdvanced, setShowAdvanced] = React.useState(false)
231
+
232
+ const loadPairing = React.useCallback(async () => {
233
+ try {
234
+ const res = await fetch('/dsh-messenger-gateway/pairing', { credentials: 'same-origin' })
235
+ const data = await res.json()
236
+ if (res.ok && data.ok) setPending(Array.isArray(data.pending) ? data.pending : [])
237
+ } catch {}
238
+ }, [])
19
239
 
20
240
  const load = React.useCallback(async () => {
21
241
  const res = await fetch('/dsh-messenger-gateway/config', { credentials: 'same-origin' })
@@ -23,74 +243,279 @@ window.__ModuleLoader__.load({
23
243
  if (!res.ok || !data.ok) throw new Error(data.error || res.status)
24
244
  setCfg(data.config)
25
245
  setAllowText(idsToText(data.config?.telegram?.allowedUserIds))
26
- }, [])
246
+ await loadPairing()
247
+ }, [loadPairing])
27
248
 
28
249
  React.useEffect(() => { load().catch((e) => setErr(String(e.message || e))) }, [load])
29
250
 
30
- const save = async (patch) => {
251
+ const mergePatch = (base, patch) => ({
252
+ ...base,
253
+ ...patch,
254
+ telegram: { ...base.telegram, ...(patch.telegram || {}) },
255
+ tts: { ...base.tts, ...(patch.tts || {}) },
256
+ agent: { ...base.agent, ...(patch.agent || {}) },
257
+ media: { ...base.media, ...(patch.media || {}) },
258
+ })
259
+
260
+ const save = async (patch = {}) => {
31
261
  setBusy(true); setErr('')
32
262
  try {
33
- const body = {
34
- config: {
35
- ...cfg,
36
- ...patch,
37
- telegram: { ...cfg.telegram, ...(patch.telegram || {}) },
38
- tts: { ...cfg.tts, ...(patch.tts || {}) },
39
- },
40
- }
41
- if (token.trim()) body.config.telegram = { ...body.config.telegram, botToken: token.trim() }
42
- body.config.telegram.allowedUserIds = textToIds(allowText)
263
+ let next = mergePatch(cfg, patch)
264
+ if (token.trim()) next.telegram = { ...next.telegram, botToken: token.trim() }
265
+ if (webhookSecret.trim()) next.telegram = { ...next.telegram, webhookSecret: webhookSecret.trim() }
266
+ next.telegram.allowedUserIds = textToIds(allowText)
43
267
  const res = await fetch('/dsh-messenger-gateway/config', {
44
268
  method: 'PUT', credentials: 'same-origin',
45
- headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
269
+ headers: { 'Content-Type': 'application/json' },
270
+ body: JSON.stringify({ config: next }),
46
271
  })
47
272
  const data = await res.json()
48
273
  if (!res.ok || !data.ok) throw new Error(data.error || res.status)
49
- setCfg(data.config); setToken('')
274
+ setCfg(data.config); setToken(''); setWebhookSecret('')
50
275
  setAllowText(idsToText(data.config?.telegram?.allowedUserIds))
276
+ await loadPairing()
51
277
  } catch (e) { setErr(String(e.message || e)) } finally { setBusy(false) }
52
278
  }
53
279
 
54
- if (!cfg) return React.createElement('div', null, err || 'Loading…')
55
-
56
- return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 720 } },
57
- React.createElement('div', { style: { fontWeight: 600 } }, 'Messenger gateway'),
58
- React.createElement('div', { style: { fontSize: 12, opacity: 0.75 } }, 'Telegram: text, voice (dsh-voice), photos, documents. Optional voice replies via dsh-tts.'),
59
- React.createElement('label', null, 'Bot token (write-only)'),
60
- React.createElement('input', { type: 'password', value: token, placeholder: cfg.telegram?.botTokenConfigured ? '••••••••' : 'BotFather token', onChange: (e) => setToken(e.target.value) }),
61
- React.createElement('label', null, 'Allowed user ids (comma-separated, empty = all)'),
62
- React.createElement('textarea', { value: allowText, onChange: (e) => setAllowText(e.target.value), rows: 2 }),
63
- React.createElement('label', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
64
- React.createElement('input', { type: 'checkbox', checked: !!cfg.tts?.enabled, onChange: (e) => save({ tts: { enabled: e.target.checked } }) }),
65
- 'Speak agent replies (dsh-tts)',
280
+ if (!cfg) return React.createElement('div', null, err || t('loading'))
281
+
282
+ return React.createElement('div', { className: 'msgw-form' },
283
+ React.createElement('div', null,
284
+ React.createElement('div', { className: 'msgw-groupTitle' }, t('telegram')),
285
+ React.createElement(Field, { label: t('botToken'), hint: t('botTokenHint') },
286
+ React.createElement('input', { type: 'password', className: 'msgw-input', value: token, placeholder: cfg.telegram?.botTokenConfigured ? '••••••••' : 'BotFather', onChange: (e) => setToken(e.target.value) }),
287
+ ),
288
+ React.createElement(Field, { label: t('allowedIds'), hint: t('allowedIdsHint') },
289
+ React.createElement('textarea', { className: 'msgw-textarea', value: allowText, onChange: (e) => setAllowText(e.target.value), rows: 2 }),
290
+ ),
291
+ React.createElement(Field, { label: t('textFormat') },
292
+ React.createElement('select', { className: 'msgw-select', value: cfg.telegram?.textFormat || 'html', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, textFormat: e.target.value } }) },
293
+ React.createElement('option', { value: 'html' }, t('textFormatHtml')),
294
+ React.createElement('option', { value: 'plain' }, t('textFormatPlain')),
295
+ ),
296
+ ),
297
+ React.createElement('button', { type: 'button', className: 'msgw-ghost', disabled: busy, onClick: () => save({ telegram: { enabled: !cfg.telegram.enabled } }) }, cfg.telegram.enabled ? t('disableTelegram') : t('enableTelegram')),
298
+ ),
299
+
300
+ React.createElement('div', { className: 'msgw-section' },
301
+ React.createElement('div', { className: 'msgw-groupTitle' }, t('agent')),
302
+ React.createElement(Field, { label: t('provider'), hint: t('providerHint') },
303
+ React.createElement('input', { className: 'msgw-input', value: cfg.agent?.provider || '', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, provider: e.target.value } }) }),
304
+ ),
305
+ React.createElement(Field, { label: t('model'), hint: t('modelHint') },
306
+ React.createElement('input', { className: 'msgw-input', value: cfg.agent?.model || '', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, model: e.target.value } }) }),
307
+ ),
308
+ React.createElement(Field, { label: t('photoOnly') },
309
+ React.createElement('select', { className: 'msgw-select', value: cfg.agent?.photoOnlyMode || 'prompt', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, photoOnlyMode: e.target.value } }) },
310
+ React.createElement('option', { value: 'prompt' }, t('photoPrompt')),
311
+ React.createElement('option', { value: 'run' }, t('photoRun')),
312
+ ),
313
+ ),
314
+ React.createElement(Field, { label: t('sessionScope') },
315
+ React.createElement('select', { className: 'msgw-select', value: cfg.agent?.sessionScope || 'user', onChange: (e) => setCfg({ ...cfg, agent: { ...cfg.agent, sessionScope: e.target.value } }) },
316
+ React.createElement('option', { value: 'user' }, t('sessionScopeUser')),
317
+ React.createElement('option', { value: 'chat' }, t('sessionScopeChat')),
318
+ ),
319
+ ),
320
+ React.createElement(Field, { label: t('instruction'), hint: t('instructionHint') },
321
+ 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') }),
322
+ ),
323
+ React.createElement(Field, { label: t('maxMessageLength') },
324
+ 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 } }) }),
325
+ ),
326
+ React.createElement(Field, { label: t('turnTimeout') },
327
+ 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 } }) }),
328
+ ),
329
+ ),
330
+
331
+ React.createElement('div', { className: 'msgw-section' },
332
+ React.createElement('div', { className: 'msgw-groupTitle' }, t('media')),
333
+ React.createElement(Field, { label: t('maxDocMb') },
334
+ 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) } }) }),
335
+ ),
336
+ React.createElement(Field, { label: t('maxImageMb') },
337
+ 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) } }) }),
338
+ ),
339
+ React.createElement(Field, { label: t('maxTextKb') },
340
+ 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 } }) }),
341
+ ),
342
+ ),
343
+
344
+ React.createElement('div', { className: 'msgw-section' },
345
+ React.createElement('label', { className: 'msgw-check' },
346
+ React.createElement('input', { type: 'checkbox', checked: !!cfg.tts?.enabled, onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, enabled: e.target.checked } }) }),
347
+ t('ttsEnable'),
348
+ ),
349
+ cfg.tts?.enabled ? React.createElement(Field, { label: t('ttsMaxChars') },
350
+ 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 } }) }),
351
+ ) : null,
352
+ ),
353
+
354
+ React.createElement('div', { className: 'msgw-section' },
355
+ React.createElement('div', { className: 'msgw-groupTitle' }, t('pairing')),
356
+ React.createElement('button', { type: 'button', className: 'msgw-linkbtn', disabled: busy, onClick: () => loadPairing() }, t('pairingRefresh')),
357
+ !(pending && pending.length) ? React.createElement('div', { className: 'msgw-hint' }, t('pairingEmpty')) : pending.map((row) =>
358
+ React.createElement('div', { key: row.code, className: 'msgw-row' },
359
+ React.createElement('div', null, (row.username ? '@' + row.username + ' · ' : '') + 'id ' + row.userId + ' · ' + row.code),
360
+ React.createElement('div', { className: 'msgw-rowActions' },
361
+ React.createElement('button', {
362
+ type: 'button', className: 'msgw-mini', disabled: busy, onClick: async () => {
363
+ setBusy(true); setErr('')
364
+ try {
365
+ const res = await fetch('/dsh-messenger-gateway/pairing/approve', {
366
+ method: 'POST', credentials: 'same-origin',
367
+ headers: { 'Content-Type': 'application/json' },
368
+ body: JSON.stringify({ code: row.code }),
369
+ })
370
+ const data = await res.json()
371
+ if (!res.ok || !data.ok) throw new Error(data.error || res.status)
372
+ await load()
373
+ } catch (e) { setErr(String(e.message || e)) } finally { setBusy(false) }
374
+ },
375
+ }, t('pairingApprove')),
376
+ React.createElement('button', {
377
+ type: 'button', className: 'msgw-mini', disabled: busy, onClick: async () => {
378
+ setBusy(true); setErr('')
379
+ try {
380
+ const res = await fetch('/dsh-messenger-gateway/pairing/reject', {
381
+ method: 'POST', credentials: 'same-origin',
382
+ headers: { 'Content-Type': 'application/json' },
383
+ body: JSON.stringify({ code: row.code }),
384
+ })
385
+ const data = await res.json()
386
+ if (!res.ok || !data.ok) throw new Error(data.error || res.status)
387
+ await loadPairing()
388
+ } catch (e) { setErr(String(e.message || e)) } finally { setBusy(false) }
389
+ },
390
+ }, t('pairingReject')),
391
+ ),
392
+ ),
393
+ ),
66
394
  ),
67
- cfg.tts?.enabled ? React.createElement('label', null,
68
- 'Max spoken chars',
69
- React.createElement('input', {
70
- type: 'number', min: 100, max: 8000,
71
- value: cfg.tts?.maxChars ?? 4000,
72
- onChange: (e) => setCfg({ ...cfg, tts: { ...cfg.tts, maxChars: Number(e.target.value) || 4000 } }),
73
- }),
395
+
396
+ React.createElement('div', { className: 'msgw-section' },
397
+ React.createElement('label', { className: 'msgw-check' },
398
+ React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.groupsEnabled !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupsEnabled: e.target.checked } }) }),
399
+ t('groupsEnable'),
400
+ ),
401
+ React.createElement('label', { className: 'msgw-check' },
402
+ React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.groupRequireMention !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, groupRequireMention: e.target.checked } }) }),
403
+ t('groupMention'),
404
+ ),
405
+ React.createElement('label', { className: 'msgw-check' },
406
+ React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.reactionsEnabled !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, reactionsEnabled: e.target.checked } }) }),
407
+ t('reactionsEnable'),
408
+ ),
409
+ React.createElement('label', { className: 'msgw-check' },
410
+ React.createElement('input', { type: 'checkbox', checked: cfg.telegram?.progressEnabled !== false, onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, progressEnabled: e.target.checked } }) }),
411
+ t('progressEnable'),
412
+ ),
413
+ React.createElement(Field, { label: t('voiceMode') },
414
+ React.createElement('select', { className: 'msgw-select', value: cfg.telegram?.voiceMode || 'mirror', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, voiceMode: e.target.value } }) },
415
+ React.createElement('option', { value: 'mirror' }, t('voiceModeMirror')),
416
+ React.createElement('option', { value: 'always' }, t('voiceModeAlways')),
417
+ React.createElement('option', { value: 'off' }, t('voiceModeOff')),
418
+ ),
419
+ ),
420
+ React.createElement('label', { className: 'msgw-check' },
421
+ 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 } } }) }),
422
+ t('notifyBridge'),
423
+ ),
424
+ React.createElement('div', { className: 'msgw-hint' }, t('notifyBridgeHint')),
425
+ React.createElement(Field, { label: t('notifyHome') },
426
+ 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 } } }) }),
427
+ ),
428
+ ),
429
+
430
+ React.createElement('button', { type: 'button', className: 'msgw-linkbtn', onClick: () => setShowAdvanced((v) => !v) }, showAdvanced ? t('hideAdvanced') : t('showAdvanced')),
431
+ showAdvanced ? React.createElement('div', { className: 'msgw-section' },
432
+ React.createElement('div', { className: 'msgw-groupTitle' }, t('advanced')),
433
+ React.createElement(Field, { label: t('transport') },
434
+ React.createElement('select', { className: 'msgw-select', value: cfg.telegram?.transport || 'poll', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, transport: e.target.value } }) },
435
+ React.createElement('option', { value: 'poll' }, t('transportPoll')),
436
+ React.createElement('option', { value: 'webhook' }, t('transportWebhook')),
437
+ ),
438
+ ),
439
+ React.createElement(Field, { label: t('webhookUrl'), hint: t('webhookUrlHint') },
440
+ React.createElement('input', { className: 'msgw-input', value: cfg.telegram?.webhookUrl || '', onChange: (e) => setCfg({ ...cfg, telegram: { ...cfg.telegram, webhookUrl: e.target.value } }) }),
441
+ ),
442
+ React.createElement(Field, { label: t('webhookSecret'), hint: t('webhookSecretHint') },
443
+ React.createElement('input', { type: 'password', className: 'msgw-input', value: webhookSecret, placeholder: cfg.telegram?.webhookSecretConfigured ? '••••••••' : '', onChange: (e) => setWebhookSecret(e.target.value) }),
444
+ ),
445
+ React.createElement(Field, { label: t('pollTimeout') },
446
+ 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 } }) }),
447
+ ),
448
+ React.createElement(Field, { label: t('pollInterval') },
449
+ 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 } }) }),
450
+ ),
451
+ React.createElement(Field, { label: t('idleTimeout') },
452
+ 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 } }) }),
453
+ ),
74
454
  ) : null,
75
- React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
76
- React.createElement('button', { disabled: busy, onClick: () => save({ telegram: { enabled: !cfg.telegram.enabled } }) }, cfg.telegram.enabled ? 'Disable Telegram' : 'Enable Telegram'),
77
- React.createElement('button', { disabled: busy, onClick: () => save({}) }, 'Save'),
455
+
456
+ err ? React.createElement('div', { className: 'msgw-err' }, err) : null,
457
+ React.createElement('div', { className: 'msgw-foot' },
458
+ React.createElement('button', { type: 'button', className: 'msgw-save', disabled: busy, onClick: () => save({}) }, busy ? t('saving') : t('save')),
459
+ ),
460
+ )
461
+ }
462
+
463
+ function PluginCard(props) {
464
+ const [open, setOpen] = React.useState(false)
465
+ const t = makeT(props.locale)
466
+ return React.createElement('li', { className: 'msgw-card' },
467
+ React.createElement('button', {
468
+ type: 'button',
469
+ className: 'msgw-head',
470
+ 'aria-expanded': open,
471
+ onClick: () => setOpen((v) => !v),
472
+ },
473
+ React.createElement('div', { className: 'msgw-headText' },
474
+ React.createElement('div', { className: 'msgw-title' }, t('title')),
475
+ React.createElement('div', { className: 'msgw-sub' }, t('description')),
476
+ ),
477
+ React.createElement('span', { className: 'msgw-chev' }, open ? '\u25B2' : '\u25BC'),
78
478
  ),
79
- err ? React.createElement('div', { style: { color: 'crimson', fontSize: 12 } }, err) : null,
479
+ open ? React.createElement('div', { className: 'msgw-body' },
480
+ React.createElement(MessengerSettingsForm, { t }),
481
+ ) : null,
80
482
  )
81
483
  }
82
484
 
83
485
  function apply(ctx) {
84
- ctx.slots.inject('settings.section', () => ctx.slots.register({
85
- name: 'settings.section',
86
- id: '@goodandready/dsh-messenger-gateway',
87
- order: 36,
88
- label: () => 'Messenger gateway',
89
- }, Section))
486
+ ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-messenger-gateway: dictionaries')
487
+ function useLocale() { return useActiveLocale(ctx) }
488
+
489
+ const tryPluginItem = () => {
490
+ try {
491
+ ctx.slots.inject('settings.plugin.item', () =>
492
+ ctx.slots.register({
493
+ name: 'settings.plugin.item',
494
+ key: NS,
495
+ locale: NS,
496
+ inject: () => ({ ctx }),
497
+ }, (props) => React.createElement(PluginCard, { ...props, locale: useLocale() })),
498
+ )
499
+ return true
500
+ } catch {
501
+ return false
502
+ }
503
+ }
504
+
505
+ if (!tryPluginItem()) {
506
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
507
+ name: 'settings.section',
508
+ id: NS,
509
+ order: 36,
510
+ label: () => makeT(useActiveLocale(ctx))('title'),
511
+ }, (props) => React.createElement('div', { style: { padding: 16 } },
512
+ React.createElement(MessengerSettingsForm, { t: makeT(useActiveLocale(ctx)) }),
513
+ )))
514
+ }
90
515
  }
91
516
 
92
517
  exports.apply = apply
93
- exports.inject = ['slots']
518
+ exports.inject = ['slots', 'locale']
94
519
  return module.exports
95
520
  },
96
- })
521
+ })
@@ -0,0 +1,30 @@
1
+ export const DEFAULT_TELEGRAM_COMMANDS = [
2
+ { command: 'start', description: 'Подключение и приветствие' },
3
+ { command: 'help', description: 'Список команд' },
4
+ { command: 'new', description: 'Новая сессия агента' },
5
+ { command: 'whoami', description: 'Показать ваш Telegram id' },
6
+ { command: 'stop', description: 'Прервать текущий ответ' },
7
+ { command: 'pair', description: 'Одобрить код сопряжения: /pair CODE' },
8
+ { command: 'sethome', description: 'Home: /sethome [name]' },
9
+ { command: 'home', description: 'Список home-каналов' },
10
+ { command: 'model', description: 'Показать или сменить модель' },
11
+ { command: 'status', description: 'Статус шлюза' },
12
+ { command: 'voice', description: 'Голосовые ответы: /voice on|off|status' },
13
+ ]
14
+
15
+ export function normalizeTelegramCommands(commands) {
16
+ const source = Array.isArray(commands) && commands.length ? commands : DEFAULT_TELEGRAM_COMMANDS
17
+ const out = []
18
+ const seen = new Set()
19
+ for (const raw of source) {
20
+ if (!raw || typeof raw !== 'object') continue
21
+ let command = String(raw.command || '').trim().replace(/^\//, '')
22
+ const description = String(raw.description || '').trim()
23
+ if (!command || !description) continue
24
+ command = command.slice(0, 32).toLowerCase()
25
+ if (seen.has(command)) continue
26
+ seen.add(command)
27
+ out.push({ command, description: description.slice(0, 256) })
28
+ }
29
+ return out.length ? out : [...DEFAULT_TELEGRAM_COMMANDS]
30
+ }