@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/README.md +68 -19
- package/lib/adapters/index.js +12 -2
- package/lib/adapters/telegram.js +324 -47
- package/lib/client.js +471 -46
- package/lib/commands.js +30 -0
- package/lib/config.js +75 -0
- package/lib/documents.js +19 -5
- package/lib/gateway.js +476 -55
- package/lib/groups.js +72 -0
- package/lib/homes.js +58 -0
- package/lib/index.js +261 -39
- package/lib/messenger-api.js +5 -5
- package/lib/pairing.js +110 -0
- package/lib/stream.js +64 -0
- package/lib/telegram-format.js +39 -0
- package/lib/text.js +18 -1
- package/lib/topics.js +23 -4
- package/lib/tts.js +82 -2
- package/lib/voice-prefs.js +44 -0
- package/package.json +15 -9
- package/docs/architecture/2026-08-23-messenger-gateway-design.md +0 -30
- package/docs/deployment/0.1.0-install.md +0 -28
- package/docs/testing/0.1.0-smoke.md +0 -12
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
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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' },
|
|
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 || '
|
|
55
|
-
|
|
56
|
-
return React.createElement('div', {
|
|
57
|
-
React.createElement('div',
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
React.createElement('
|
|
70
|
-
type: '
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
})
|
package/lib/commands.js
ADDED
|
@@ -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
|
+
}
|