@goodandready/dsh-messenger-gateway 0.3.20 → 0.3.22

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
@@ -114,109 +114,183 @@ window.__ModuleLoader__.load({
114
114
  'stats.errors': 'Delivery Errors',
115
115
  'stats.uptime': 'Gateway Uptime',
116
116
  'stats.active_chats': 'Active Chats in Memory',
117
+ // Updater
118
+ 'updater.title': 'Plugin Self-Updater',
119
+ 'updater.desc': 'Check npmjs registry for updates and upgrade in-place.',
120
+ 'updater.current': 'Current version: {version}',
121
+ 'updater.checking': 'Checking for updates…',
122
+ 'updater.available': 'Update available: v{version}',
123
+ 'updater.upToDate': 'Up to date',
124
+ 'updater.btn_check': 'Check for Updates',
125
+ 'updater.btn_update': 'Update to v{version}',
126
+ 'updater.updating': 'Updating…',
127
+ 'updater.success': 'Updated to v{version}! Please restart DSH service if required.',
128
+ 'updater.failed': 'Update failed: {error}',
129
+ 'updater.check_failed': 'Update check failed: {error}',
130
+ // Discord
131
+ discord: 'Discord Gateway',
132
+ discordSub: 'Discord bot and outbound webhook delivery settings.',
133
+ enableDiscord: 'Enable Discord',
134
+ disableDiscord: 'Disable Discord',
135
+ discordToken: 'Discord Bot Token',
136
+ discordTokenHint: 'From Discord Developer Portal. Leave empty to keep existing.',
137
+ discordWebhook: 'Discord Webhook URL',
138
+ discordWebhookHint: 'Incoming webhook for sending messages/notifications.',
139
+ discordChannels: 'Allowed Discord Channel IDs',
140
+ discordChannelsHint: 'Comma-separated channel IDs. Leave empty to allow all configured.',
141
+ // Slack
142
+ slack: 'Slack Gateway',
143
+ slackSub: 'Slack bot and incoming webhook settings.',
144
+ enableSlack: 'Enable Slack',
145
+ disableSlack: 'Disable Slack',
146
+ slackToken: 'Slack Bot Token (xoxb-…)',
147
+ slackTokenHint: 'Bot User OAuth Token. Leave empty to keep existing.',
148
+ slackWebhook: 'Slack Webhook URL',
149
+ slackWebhookHint: 'Incoming webhook URL for channel notifications.',
150
+ slackDefaultChannel: 'Slack Default Channel',
151
+ slackDefaultChannelHint: 'Default channel or member ID for outbound messages (e.g. #general or C12345).',
152
+ // Cron badge
153
+ 'badge.cron_count': 'Cron: {count}',
117
154
  }
118
155
 
119
- const ru = {
156
+ const zh = {
120
157
  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)',
158
+ description: 'Telegram 机器人:文本、语音、图片、文档和交互式询问。',
159
+ 'settings.loading': '正在加载 Messenger gateway 设置…',
160
+ 'settings.retry': '重试',
161
+ 'settings.unavailable': '设置不可用(主机命名空间未就绪)。',
162
+ 'header.title': 'Telegram 与多消息平台网关',
163
+ 'header.sub': 'DeepSeek Harness 与 Telegram 之间的桥梁:会话、转向提示、论坛话题、内联按钮和可选语音。',
164
+ 'badge.online': 'Telegram 在线 (@{username})',
165
+ 'badge.running': '运行中',
166
+ 'badge.stopped': '已停止',
167
+ 'badge.token_ok': '令牌有效 ✓',
168
+ 'badge.token_missing': '缺少令牌',
169
+ 'badge.pairing_count': '配对: {count}',
170
+ 'badge.transport': '传输方式: {mode}',
171
+ save: '保存更改',
172
+ saved: '设置保存成功',
173
+ saving: '保存中…',
174
+ showAdvanced: '⚙️ 高级连接与轮询设置',
175
+ hideAdvanced: '隐藏高级设置',
176
+ telegram: 'Telegram 机器人设置',
177
+ telegramSub: '主机器人凭证、白名单与消息发送设置。',
178
+ agent: '智能体与推理行为',
179
+ agentSub: '模型路由、附加指令与轮次超时设置。',
180
+ media: '媒体与文件限制',
181
+ mediaSub: '文档提取与图片上传限制。',
182
+ tts: '语音回复 (dsh-tts)',
183
+ ttsSub: '为 Telegram 语音消息合成语音回复。',
184
+ advanced: '连接与轮询内部设置',
185
+ botToken: 'Telegram 机器人令牌',
186
+ botTokenHint: '从 @BotFather 获取。留空保持现有配置。',
187
+ allowedIds: '允许的 Telegram 用户 ID',
188
+ allowedIdsHint: '逗号分隔的用户 ID 列表。留空允许所有用户(启用配对请求)。',
189
+ textFormat: '文本格式化渲染',
190
+ textFormatHtml: 'Markdown → Telegram HTML (推荐)',
191
+ textFormatPlain: '纯文本 (无格式化)',
192
+ disableTelegram: '停用 Telegram 机器人',
193
+ enableTelegram: '启用 Telegram 机器人',
194
+ provider: '提供商覆盖',
195
+ providerHint: '留空使用 DSH 默认提供商',
196
+ model: '模型覆盖',
197
+ modelHint: '留空使用 DSH 默认模型',
198
+ photoOnly: '无附言图片处理方式',
199
+ photoPrompt: '等待问题 (推荐)',
200
+ photoRun: '立即回复',
201
+ instruction: '额外智能体指令',
202
+ instructionHint: '添加到每条来自网关的消息前',
203
+ instructionPlaceholder: '留空 = 内置指令',
204
+ maxMessageLength: '最大回复长度 (字符)',
205
+ turnTimeout: '智能体轮次超时 (秒)',
206
+ maxDocMb: '最大文档大小 (MB)',
207
+ maxImageMb: '最大图片大小 (MB)',
208
+ maxTextKb: '文档提取最大文本量 (KB)',
209
+ ttsEnable: '启用语音回复 (需 dsh-tts)',
210
+ ttsMaxChars: '每条语音消息最大字数',
211
+ pollTimeout: '长轮询超时 (秒)',
212
+ pollInterval: '长轮询间隔 (毫秒)',
213
+ idleTimeout: '空闲会话超时 (秒)',
214
+ pairing: '配对请求与授权',
215
+ pairingSub: '受限白名单下授权新用户。',
216
+ pairingEmpty: '暂无待处理配对码',
217
+ pairingApprove: '批准',
218
+ pairingReject: '拒绝',
219
+ pairingRefresh: '刷新列表',
220
+ groupsEnable: '在群组和超级群组中响应',
221
+ groupMention: '群组中需 @提及、回复或 /命令',
222
+ reactionsEnable: '处理中添加 👀 表情反应',
223
+ progressEnable: '进度状态提示 (思考中 → 删除 → 最终回复)',
224
+ statusIndicator: '机器人状态描述指示器 (Online/Offline)',
225
+ statusIndicatorHint: '通过 Telegram API 设置机器人简介(所有人可见)。',
226
+ transport: 'Telegram 传输模式',
227
+ transportPoll: '长轮询 (getUpdates)',
191
228
  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': 'Активных чатов в памяти',
229
+ webhookUrl: '公开 Webhook HTTPS 地址',
230
+ webhookUrlHint: 'Telegram 投递更新的公开端点',
231
+ webhookSecret: 'Webhook 密钥令牌',
232
+ webhookSecretHint: '留空保持当前配置的密钥',
233
+ sessionScope: '群组会话范围',
234
+ sessionScopeChat: '每个聊天/话题共享',
235
+ sessionScopeUser: '每用户独立会话 (推荐)',
236
+ voiceMode: '语音回复模式',
237
+ voiceModeMirror: '镜像输入语音 (仅语音输入时回复语音)',
238
+ voiceModeAlways: '始终合成语音回复',
239
+ voiceModeOff: '关闭 (除非通过 /voice 启用)',
240
+ notifyBridge: '通知网桥 → Telegram Home',
241
+ notifyBridgeHint: '将 Web 会话的 task_done/error 事件转发至指定频道/话题',
242
+ notifyHome: '目标频道名称 (Home)',
243
+ quickActions: '快捷操作键盘 (/new, /stop, /voice, /status)',
244
+ artifactPreviews: '在 Telegram 中预览图表与表格',
245
+ 'diag.title': '⚡ 诊断与连通性检查 (Smoke test)',
246
+ 'diag.desc': '检查 Telegram Bot API 连接、测量延迟并验证机器人名称。',
247
+ 'diag.smoke_btn': '测试 Telegram 连接 (Ping)',
248
+ 'diag.smoke_testing': '正在连接 Telegram API…',
249
+ 'diag.smoke_ok': 'Telegram API 连接成功!延迟: {latency} 毫秒 (@{username}, ID: {id})',
250
+ 'diag.smoke_err': '连通性检查失败: {error}',
251
+ 'stats.title': '📊 网关遥测与计数器',
252
+ 'stats.desc': '运行中网关进程的实时统计。',
253
+ 'stats.sent': '已发送消息',
254
+ 'stats.errors': '发送错误数',
255
+ 'stats.uptime': '运行时间',
256
+ 'stats.active_chats': '内存中活跃聊天数',
257
+ // Updater
258
+ 'updater.title': '插件自更新',
259
+ 'updater.desc': '从 npmjs 仓库检查更新并直接就地升级。',
260
+ 'updater.current': '当前版本: {version}',
261
+ 'updater.checking': '正在检查更新…',
262
+ 'updater.available': '发现新版本: v{version}',
263
+ 'updater.upToDate': '已是最新版本',
264
+ 'updater.btn_check': '检查更新',
265
+ 'updater.btn_update': '更新至 v{version}',
266
+ 'updater.updating': '正在更新…',
267
+ 'updater.success': '已更新至 v{version}!如需生效请重启 DSH 服务。',
268
+ 'updater.failed': '更新失败: {error}',
269
+ 'updater.check_failed': '检查更新失败: {error}',
270
+ // Discord
271
+ discord: 'Discord 网关',
272
+ discordSub: 'Discord 机器人与出站 Webhook 设置。',
273
+ enableDiscord: '启用 Discord',
274
+ disableDiscord: '停用 Discord',
275
+ discordToken: 'Discord 机器人令牌',
276
+ discordTokenHint: '从 Discord 开发者平台获取。留空保持现有配置。',
277
+ discordWebhook: 'Discord Webhook URL',
278
+ discordWebhookHint: '用于发送消息或通知的传入 Webhook。',
279
+ discordChannels: '允许的 Discord 频道 ID',
280
+ discordChannelsHint: '逗号分隔的频道 ID。留空允许所有已配置频道。',
281
+ // Slack
282
+ slack: 'Slack 网关',
283
+ slackSub: 'Slack 机器人与 Webhook 设置。',
284
+ enableSlack: '启用 Slack',
285
+ disableSlack: '停用 Slack',
286
+ slackToken: 'Slack 机器人令牌 (xoxb-…)',
287
+ slackTokenHint: 'Bot 用户 OAuth 令牌。留空保持现有配置。',
288
+ slackWebhook: 'Slack Webhook URL',
289
+ slackWebhookHint: '用于频道通知的传入 Webhook URL。',
290
+ slackDefaultChannel: 'Slack 默认频道',
291
+ slackDefaultChannelHint: '出站消息的默认频道或成员 ID (例如 #general 或 C12345)。',
292
+ // Cron badge
293
+ 'badge.cron_count': '定时任务: {count}',
220
294
  }
221
295
 
222
296
  function useActiveLocale(ctx) {
@@ -234,7 +308,7 @@ window.__ModuleLoader__.load({
234
308
  }
235
309
 
236
310
  function makeT(locale) {
237
- const DICT = String(locale || '').startsWith('ru') ? ru : en
311
+ const DICT = String(locale || '').startsWith('zh') ? zh : en
238
312
  return (key, params) => {
239
313
  let text = DICT[key] || en[key] || key
240
314
  if (params && typeof params === 'object') {
@@ -369,11 +443,13 @@ window.__ModuleLoader__.load({
369
443
  )
370
444
  }
371
445
 
372
- const SETTINGS_KEYS = ['enabled', 'telegram', 'agent', 'media', 'tts']
446
+ const SETTINGS_KEYS = ['enabled', 'telegram', 'discord', 'slack', 'agent', 'media', 'tts']
373
447
 
374
448
  function draftFromStored(s) {
375
449
  const src = (s && typeof s === 'object') ? s : {}
376
450
  const t = src.telegram || {}
451
+ const d = src.discord || {}
452
+ const sl = src.slack || {}
377
453
  const a = src.agent || {}
378
454
  const m = src.media || {}
379
455
  const tts = src.tts || {}
@@ -411,6 +487,18 @@ window.__ModuleLoader__.load({
411
487
  botToken: t.botToken || '',
412
488
  webhookSecret: t.webhookSecret || '',
413
489
  },
490
+ discord: {
491
+ enabled: d.enabled === true,
492
+ botToken: d.botToken || '',
493
+ webhookUrl: d.webhookUrl || '',
494
+ allowedChannelIds: Array.isArray(d.allowedChannelIds) ? d.allowedChannelIds : [],
495
+ },
496
+ slack: {
497
+ enabled: sl.enabled === true,
498
+ botToken: sl.botToken || '',
499
+ webhookUrl: sl.webhookUrl || '',
500
+ defaultChannel: sl.defaultChannel || '',
501
+ },
414
502
  agent: {
415
503
  provider: a.provider || '',
416
504
  model: a.model || '',
@@ -441,6 +529,18 @@ window.__ModuleLoader__.load({
441
529
  const [token, setToken] = React.useState('')
442
530
  const [webhookSecret, setWebhookSecret] = React.useState('')
443
531
  const [allowText, setAllowText] = React.useState('')
532
+ const [discordToken, setDiscordToken] = React.useState('')
533
+ const [slackToken, setSlackToken] = React.useState('')
534
+ const [discordChannelsText, setDiscordChannelsText] = React.useState('')
535
+ const [updateState, setUpdateState] = React.useState({
536
+ checking: false,
537
+ updating: false,
538
+ currentVersion: '',
539
+ latestVersion: '',
540
+ updateAvailable: false,
541
+ error: '',
542
+ notice: '',
543
+ })
444
544
  const [pending, setPending] = React.useState([])
445
545
  const [status, setStatus] = React.useState(null)
446
546
  const [smokeResult, setSmokeResult] = React.useState(null)
@@ -472,6 +572,7 @@ window.__ModuleLoader__.load({
472
572
  if (snapStatus === 'ready' && cfg === null) {
473
573
  setCfg(draftFromStored(stored))
474
574
  setAllowText(idsToText(stored?.telegram?.allowedUserIds))
575
+ setDiscordChannelsText(idsToText(stored?.discord?.allowedChannelIds))
475
576
  }
476
577
  }, [snapStatus, stored, cfg])
477
578
 
@@ -503,6 +604,61 @@ window.__ModuleLoader__.load({
503
604
  return () => clearInterval(timer)
504
605
  }, [loadStatus, loadPairing])
505
606
 
607
+ const loadUpdateStatus = React.useCallback(async () => {
608
+ setUpdateState((s) => ({ ...s, checking: true, error: '' }))
609
+ try {
610
+ const res = await fetch(`${ROUTE_PREFIX}/update`, { cache: 'no-store' })
611
+ const data = await res.json().catch(() => ({}))
612
+ if (res.ok && data.ok) {
613
+ setUpdateState((s) => ({
614
+ ...s,
615
+ checking: false,
616
+ currentVersion: data.currentVersion || '',
617
+ latestVersion: data.latestVersion || '',
618
+ updateAvailable: !!data.updateAvailable,
619
+ }))
620
+ } else {
621
+ setUpdateState((s) => ({ ...s, checking: false, error: data.error || `HTTP ${res.status}` }))
622
+ }
623
+ } catch (e) {
624
+ setUpdateState((s) => ({ ...s, checking: false, error: String(e.message || e) }))
625
+ }
626
+ }, [])
627
+
628
+ const handleTriggerUpdate = async () => {
629
+ if (updateState.updating) return
630
+ setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
631
+ try {
632
+ const res = await fetch(`${ROUTE_PREFIX}/update`, {
633
+ method: 'POST',
634
+ credentials: 'same-origin',
635
+ headers: {
636
+ 'Content-Type': 'application/json',
637
+ 'x-dsh-plugin-update': '1',
638
+ },
639
+ })
640
+ const data = await res.json().catch(() => ({}))
641
+ if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`)
642
+ setUpdateState((s) => ({
643
+ ...s,
644
+ updating: false,
645
+ updateAvailable: false,
646
+ currentVersion: data.updatedVersion || s.latestVersion,
647
+ notice: t('updater.success', { version: data.updatedVersion || s.latestVersion }),
648
+ }))
649
+ } catch (e) {
650
+ setUpdateState((s) => ({
651
+ ...s,
652
+ updating: false,
653
+ error: t('updater.failed', { error: String(e.message || e) }),
654
+ }))
655
+ }
656
+ }
657
+
658
+ React.useEffect(() => {
659
+ loadUpdateStatus()
660
+ }, [loadUpdateStatus])
661
+
506
662
  const handleSmoke = async () => {
507
663
  setBusy('smoke')
508
664
  setErr('')
@@ -531,6 +687,8 @@ window.__ModuleLoader__.load({
531
687
  ...base,
532
688
  ...patch,
533
689
  telegram: { ...base.telegram, ...(patch.telegram || {}) },
690
+ discord: { ...base.discord, ...(patch.discord || {}) },
691
+ slack: { ...base.slack, ...(patch.slack || {}) },
534
692
  tts: { ...base.tts, ...(patch.tts || {}) },
535
693
  agent: { ...base.agent, ...(patch.agent || {}) },
536
694
  media: { ...base.media, ...(patch.media || {}) },
@@ -546,6 +704,9 @@ window.__ModuleLoader__.load({
546
704
  if (token.trim()) next.telegram = { ...next.telegram, botToken: token.trim() }
547
705
  if (webhookSecret.trim()) next.telegram = { ...next.telegram, webhookSecret: webhookSecret.trim() }
548
706
  next.telegram.allowedUserIds = textToIds(allowText)
707
+ if (discordToken.trim()) next.discord = { ...next.discord, botToken: discordToken.trim() }
708
+ if (slackToken.trim()) next.slack = { ...next.slack, botToken: slackToken.trim() }
709
+ next.discord.allowedChannelIds = textToIds(discordChannelsText)
549
710
 
550
711
  const broken = []
551
712
  for (const key of SETTINGS_KEYS) {
@@ -564,6 +725,8 @@ window.__ModuleLoader__.load({
564
725
  setCfg(next)
565
726
  setToken('')
566
727
  setWebhookSecret('')
728
+ setDiscordToken('')
729
+ setSlackToken('')
567
730
  setMsg(t('saved'))
568
731
  setTimeout(() => setMsg(''), 3000)
569
732
  await loadPairing()
@@ -628,6 +791,13 @@ window.__ModuleLoader__.load({
628
791
  { className: 'msgw-badge msgw-badge-warn' },
629
792
  t('badge.pairing_count', { count: pendingCount })
630
793
  )
794
+ : null,
795
+ (status?.scheduledCount > 0)
796
+ ? React.createElement(
797
+ 'span',
798
+ { className: 'msgw-badge msgw-badge-ok' },
799
+ t('badge.cron_count', { count: status.scheduledCount })
800
+ )
631
801
  : null
632
802
  ),
633
803
  React.createElement('div', { className: 'msgw-page-sub' }, t('header.sub'))
@@ -637,6 +807,60 @@ window.__ModuleLoader__.load({
637
807
  err ? React.createElement('div', { className: 'msgw-alert-err' }, err) : null,
638
808
  msg ? React.createElement('div', { className: 'msgw-alert-ok' }, msg) : null,
639
809
 
810
+ // Self-Updater Card
811
+ React.createElement(
812
+ 'div',
813
+ { className: 'msgw-section-card' },
814
+ React.createElement(
815
+ 'div',
816
+ { className: 'msgw-section-title' },
817
+ React.createElement('span', null, `🚀 ${t('updater.title')}`),
818
+ React.createElement(
819
+ 'div',
820
+ { className: 'msgw-row' },
821
+ React.createElement(
822
+ 'button',
823
+ {
824
+ type: 'button',
825
+ className: 'msgw-btn msgw-btn-mini',
826
+ disabled: updateState.checking || updateState.updating,
827
+ onClick: loadUpdateStatus,
828
+ },
829
+ updateState.checking ? t('updater.checking') : t('updater.btn_check')
830
+ ),
831
+ updateState.updateAvailable
832
+ ? React.createElement(
833
+ 'button',
834
+ {
835
+ type: 'button',
836
+ className: 'msgw-btn msgw-btn-mini msgw-btn-primary',
837
+ disabled: updateState.updating,
838
+ onClick: handleTriggerUpdate,
839
+ },
840
+ updateState.updating ? t('updater.updating') : t('updater.btn_update', { version: updateState.latestVersion })
841
+ )
842
+ : null
843
+ )
844
+ ),
845
+ React.createElement('div', { className: 'msgw-section-desc' }, t('updater.desc')),
846
+ React.createElement(
847
+ 'div',
848
+ { className: 'msgw-row', style: { fontSize: '13px' } },
849
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } },
850
+ t('updater.current', { version: updateState.currentVersion || status?.version || '...' })
851
+ ),
852
+ updateState.checking
853
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-warn' }, t('updater.checking'))
854
+ : updateState.updateAvailable
855
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-warn' }, t('updater.available', { version: updateState.latestVersion }))
856
+ : updateState.currentVersion && !updateState.error
857
+ ? React.createElement('span', { className: 'msgw-badge msgw-badge-ok' }, `✓ ${t('updater.upToDate')}`)
858
+ : null
859
+ ),
860
+ updateState.notice ? React.createElement('div', { className: 'msgw-alert-ok' }, updateState.notice) : null,
861
+ updateState.error ? React.createElement('div', { className: 'msgw-alert-err' }, updateState.error) : null
862
+ ),
863
+
640
864
  // Card 1: Telegram Connection & Bot
641
865
  React.createElement(
642
866
  'div',
@@ -949,6 +1173,101 @@ window.__ModuleLoader__.load({
949
1173
  )
950
1174
  ),
951
1175
 
1176
+ // Card: Discord Gateway
1177
+ React.createElement(
1178
+ 'div',
1179
+ { className: 'msgw-section-card' },
1180
+ React.createElement(
1181
+ 'div',
1182
+ { className: 'msgw-section-title' },
1183
+ React.createElement('span', null, `🎮 ${t('discord')}`),
1184
+ React.createElement(
1185
+ 'button',
1186
+ {
1187
+ type: 'button',
1188
+ className: `msgw-btn msgw-btn-mini ${cfg.discord?.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
1189
+ disabled: !!busy,
1190
+ onClick: () => save({ discord: { enabled: !cfg.discord?.enabled } }),
1191
+ },
1192
+ cfg.discord?.enabled ? t('disableDiscord') : t('enableDiscord')
1193
+ )
1194
+ ),
1195
+ React.createElement('div', { className: 'msgw-section-desc' }, t('discordSub')),
1196
+ React.createElement(Field, { label: t('discordToken'), hint: t('discordTokenHint') },
1197
+ React.createElement('input', {
1198
+ type: 'password',
1199
+ className: 'msgw-input',
1200
+ value: discordToken,
1201
+ placeholder: (status?.config?.discord?.botTokenConfigured || cfg.discord?.botTokenConfigured) ? '••••••••••••••••' : 'Discord Bot Token',
1202
+ onChange: (e) => setDiscordToken(e.target.value),
1203
+ })
1204
+ ),
1205
+ React.createElement(Field, { label: t('discordWebhook'), hint: t('discordWebhookHint') },
1206
+ React.createElement('input', {
1207
+ className: 'msgw-input',
1208
+ value: cfg.discord?.webhookUrl || '',
1209
+ placeholder: 'https://discord.com/api/webhooks/...',
1210
+ onChange: (e) => setCfg({ ...cfg, discord: { ...cfg.discord, webhookUrl: e.target.value } }),
1211
+ })
1212
+ ),
1213
+ React.createElement(Field, { label: t('discordChannels'), hint: t('discordChannelsHint') },
1214
+ React.createElement('textarea', {
1215
+ className: 'msgw-textarea',
1216
+ value: discordChannelsText,
1217
+ onChange: (e) => setDiscordChannelsText(e.target.value),
1218
+ rows: 2,
1219
+ placeholder: '123456789012345678, 987654321098765432',
1220
+ })
1221
+ )
1222
+ ),
1223
+
1224
+ // Card: Slack Gateway
1225
+ React.createElement(
1226
+ 'div',
1227
+ { className: 'msgw-section-card' },
1228
+ React.createElement(
1229
+ 'div',
1230
+ { className: 'msgw-section-title' },
1231
+ React.createElement('span', null, `💼 ${t('slack')}`),
1232
+ React.createElement(
1233
+ 'button',
1234
+ {
1235
+ type: 'button',
1236
+ className: `msgw-btn msgw-btn-mini ${cfg.slack?.enabled ? 'msgw-btn-danger' : 'msgw-btn-primary'}`,
1237
+ disabled: !!busy,
1238
+ onClick: () => save({ slack: { enabled: !cfg.slack?.enabled } }),
1239
+ },
1240
+ cfg.slack?.enabled ? t('disableSlack') : t('enableSlack')
1241
+ )
1242
+ ),
1243
+ React.createElement('div', { className: 'msgw-section-desc' }, t('slackSub')),
1244
+ React.createElement(Field, { label: t('slackToken'), hint: t('slackTokenHint') },
1245
+ React.createElement('input', {
1246
+ type: 'password',
1247
+ className: 'msgw-input',
1248
+ value: slackToken,
1249
+ placeholder: (status?.config?.slack?.botTokenConfigured || cfg.slack?.botTokenConfigured) ? '••••••••••••••••' : 'xoxb-...',
1250
+ onChange: (e) => setSlackToken(e.target.value),
1251
+ })
1252
+ ),
1253
+ React.createElement(Field, { label: t('slackWebhook'), hint: t('slackWebhookHint') },
1254
+ React.createElement('input', {
1255
+ className: 'msgw-input',
1256
+ value: cfg.slack?.webhookUrl || '',
1257
+ placeholder: 'https://hooks.slack.com/services/...',
1258
+ onChange: (e) => setCfg({ ...cfg, slack: { ...cfg.slack, webhookUrl: e.target.value } }),
1259
+ })
1260
+ ),
1261
+ React.createElement(Field, { label: t('slackDefaultChannel'), hint: t('slackDefaultChannelHint') },
1262
+ React.createElement('input', {
1263
+ className: 'msgw-input',
1264
+ value: cfg.slack?.defaultChannel || '',
1265
+ placeholder: '#general or C12345678',
1266
+ onChange: (e) => setCfg({ ...cfg, slack: { ...cfg.slack, defaultChannel: e.target.value } }),
1267
+ })
1268
+ )
1269
+ ),
1270
+
952
1271
  // Card 6: Pairing Requests
953
1272
  React.createElement(
954
1273
  'div',
@@ -1243,20 +1562,20 @@ window.__ModuleLoader__.load({
1243
1562
  try {
1244
1563
  const loc = ctx?.get?.('locale') || ctx?.locale
1245
1564
  if (loc?.register) {
1246
- loc.register(NS, { en, ru })
1565
+ loc.register(NS, { en, zh })
1247
1566
  }
1248
1567
  } catch (taken) {
1249
- console.warn('[dsh-messenger-gateway] словарь уже зарегистрирован:', taken && taken.message)
1568
+ console.warn('[dsh-messenger-gateway] dictionary already registered:', taken && taken.message)
1250
1569
  }
1251
1570
  }, 'dsh-messenger-gateway: dictionaries')
1252
1571
  } else {
1253
1572
  const loc = ctx?.get?.('locale') || ctx?.locale
1254
1573
  if (loc?.register) {
1255
- loc.register(NS, { en, ru })
1574
+ loc.register(NS, { en, zh })
1256
1575
  }
1257
1576
  }
1258
1577
  } catch (err) {
1259
- console.warn('[dsh-messenger-gateway] ошибка регистрации словаря:', err && err.message)
1578
+ console.warn('[dsh-messenger-gateway] dictionary registration error:', err && err.message)
1260
1579
  }
1261
1580
 
1262
1581
  if (typeof ctx.effect === 'function') {