dsh-tg-bridge 1.1.0 → 1.1.2

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/bridge.mjs CHANGED
@@ -800,7 +800,7 @@ async function startProgress(chatId, threadId, lang = 'ru') {
800
800
  // Stop is available for as long as the turn runs; the keyboard is cleared
801
801
  // when the status turns into the final line, so a stale button cannot be
802
802
  // pressed after the work is over.
803
- reply_markup: stopKeyboard(sessionKey(chatId, threadId)),
803
+ reply_markup: stopKeyboard(sessionKey(chatId, threadId), lang),
804
804
  ...(threadId > 0 ? { message_thread_id: threadId } : {}),
805
805
  });
806
806
  messageId = res.message_id;
@@ -878,8 +878,10 @@ async function startProgress(chatId, threadId, lang = 'ru') {
878
878
  if (result?.error) {
879
879
  // A failed turn leaves the status message as the error report and swaps
880
880
  // «Стоп» for «Повторить»: the person's next move is a retry, not a stop.
881
- const text = `⚠️ Ход не удался за ${Math.round((Date.now() - started) / 1000)} с\n${String(result.error).slice(0, 1200)}`;
882
- await edit(text, errorKeyboard(sessionKey(chatId, threadId)));
881
+ const failedFor = Math.round((Date.now() - started) / 1000);
882
+ const unit = lang === 'en' ? 's' : lang === 'zh' ? '秒' : 'с';
883
+ const text = `⚠️ ${t(lang, 'status.failed')} · ${failedFor} ${unit}\n${String(result.error).slice(0, 1200)}`;
884
+ await edit(text, errorKeyboard(sessionKey(chatId, threadId), lang));
883
885
  return true;
884
886
  }
885
887
  await edit(body(true), null);
@@ -1340,23 +1342,23 @@ const lastPrompt = new Map();
1340
1342
  const approvals = new Map();
1341
1343
  let approvalSeq = 0;
1342
1344
 
1343
- function stopKeyboard(key) {
1345
+ function stopKeyboard(key, lang = 'ru') {
1344
1346
  return {
1345
1347
  inline_keyboard: [
1346
1348
  [
1347
- { text: '⏹ Стоп', callback_data: 'stop' },
1348
- { text: '📄 Логи', callback_data: 'logs' },
1349
+ { text: `⏹ ${t(lang, 'btn.stop')}`, callback_data: 'stop' },
1350
+ { text: `📄 ${t(lang, 'btn.logs')}`, callback_data: 'logs' },
1349
1351
  ],
1350
1352
  ],
1351
1353
  };
1352
1354
  }
1353
1355
 
1354
- function errorKeyboard(key) {
1356
+ function errorKeyboard(key, lang = 'ru') {
1355
1357
  return {
1356
1358
  inline_keyboard: [
1357
1359
  [
1358
- { text: '🔁 Повторить', callback_data: 'retry' },
1359
- { text: '🩺 Диагноз', callback_data: 'doctor' },
1360
+ { text: `🔁 ${t(lang, 'btn.retry')}`, callback_data: 'retry' },
1361
+ { text: `🩺 ${t(lang, 'btn.doctor')}`, callback_data: 'doctor' },
1360
1362
  ],
1361
1363
  ],
1362
1364
  };
package/control.mjs CHANGED
@@ -346,7 +346,7 @@ const fmt = (rows) => rows.map((row) => ` ${row}`).join('\n');
346
346
  export const CONTROL_COMMANDS = [
347
347
  'status', 'models', 'model', 'sessions', 'resume', 'plugins', 'plugin',
348
348
  'skills', 'soul', 'restart', 'app', 'stop', 'new', 'seed', 'send', 'cd',
349
- 'prices', 'help', 'doctor', 'logs',
349
+ 'prices', 'help', 'doctor', 'logs', 'lang',
350
350
  ];
351
351
 
352
352
  /** One-line menu used when an unknown command arrives. */
@@ -356,6 +356,7 @@ export const CONTROL_COMMAND_HINT =
356
356
  'Модели: /models · /model · /prices\n' +
357
357
  'Расширения: /skills · /plugins · /plugin · /soul\n' +
358
358
  'Диагностика: /doctor · /logs\n' +
359
+ 'Язык: /lang en|ru|zh\n' +
359
360
  'Служебное: /app · /restart · /help';
360
361
 
361
362
  export async function handleControl(ctx) {
@@ -700,20 +701,10 @@ export async function handleControl(ctx) {
700
701
  }
701
702
 
702
703
  case '/help': {
703
- say(
704
- [
705
- 'Задачу можно писать обычным текстом. Каждая ветка форума — отдельная сессия.',
706
- '',
707
- 'Сессии: /new · /status · /sessions [n] · /resume <id>',
708
- 'Модель: /models · /model <id> [both]',
709
- 'Расширения: /plugins [headless] · /plugin add <pkg> · /plugin remove <name> · /plugin disable|enable <id>',
710
- 'Скиллы: /skills · /skills <name>',
711
- 'Персона: /soul · /soul <текст>',
712
- 'Служебное: /stop · /restart confirm · /app restart confirm',
713
- 'Темы: /seed <текст> — засеять пустую тему и закрепить сообщение',
714
- 'Команды можно писать и слитно: /apprestart.confirm = /app restart confirm',
715
- ].join('\n'),
716
- );
704
+ // One translated body keeps the help in step with the menu; the Russian
705
+ // text stays as the fallback when a key is missing.
706
+ const helpLang = normalizeLang(state.lang?.[key] || config.lang) || 'ru';
707
+ say(t(helpLang, 'help.body'));
717
708
  return true;
718
709
  }
719
710
 
package/i18n.mjs CHANGED
@@ -1,171 +1,186 @@
1
- // i18n — three locales for the user-visible surfaces.
2
- //
3
- // Scope: everything a person sees without asking for it — the Telegram command
4
- // menu, the live status message, turn errors and the everyday commands. Long
5
- // explanatory texts stay in the source language until they are translated
6
- // properly; a half-translated sentence is worse than a consistent one.
7
- //
8
- // Language resolution order: per-topic choice (/lang) -> config.lang -> the
9
- // sender's Telegram language_code -> 'ru'.
10
- import { readFileSync } from 'node:fs';
11
- import { join } from 'node:path';
12
-
13
- export const LANGS = ['en', 'ru', 'zh'];
14
-
15
- const DICT = {
16
- en: {
17
- 'status.working': 'Working',
18
- 'status.done': 'Done',
19
- 'status.failed': 'Turn failed',
20
- 'status.queued': 'queued',
21
- 'status.stop': 'Stop',
22
- 'status.cache': 'cache',
23
- 'status.steps': 'steps',
24
- 'status.thinking': 'waiting for the model',
25
- 'err.timeout': 'no activity for {min} min — turn interrupted',
26
- 'err.generic': 'something went wrong',
27
- 'msg.started': 'Telegram bridge for DSH is running (headless). Send a task; /help for commands.',
28
- 'msg.unknownCommand': 'Unknown command: {cmd}. Open /help — it lists everything this bot can do.',
29
- 'msg.langSet': 'Language set to English.',
30
- 'cmd.help.title': 'Commands',
31
- 'cmd.help.hint': 'Send any text to start a task. In a forum topic: /cd <project> binds the topic to that project.',
32
- 'cmd.cd.bound': 'Topic bound to project:\n{path}\n\nAll turns run from this directory.',
33
- 'cmd.cd.reset': 'Topic directory reset to the shared one.',
34
- 'cmd.cd.none': 'No such directory: {path}',
35
- 'cmd.new.done': 'New session in this topic. Context cleared.',
36
- 'cmd.stop.killed': 'Turn stopped.',
37
- 'cmd.stop.nothing': 'Nothing to stop.',
38
- 'menu.status': 'Sessions, model, active turns',
39
- 'menu.cd': 'Bind this topic to a project directory',
40
- 'menu.send': 'Send a file from the project into the chat',
41
- 'menu.prices': 'Model prices from the router',
42
- 'menu.stop': 'Stop the current turn',
43
- 'menu.new': 'New session in this topic',
44
- 'menu.seed': 'Seed an empty topic with text and pin it',
45
- 'menu.sessions': 'Recent sessions',
46
- 'menu.resume': 'Continue a session by id',
47
- 'menu.models': 'Available models',
48
- 'menu.model': 'Show or switch the model',
49
- 'menu.skills': 'Installed skills',
50
- 'menu.soul': 'Agent persona',
51
- 'menu.plugins': 'Installed plugins',
52
- 'menu.plugin': 'Enable or disable a plugin',
53
- 'menu.app': 'Desktop app control',
54
- 'menu.restart': 'Restart the agent',
55
- 'menu.help': 'All commands and hints',
56
- 'menu.lang': 'Interface language: en, ru, zh',
57
- },
58
- ru: {
59
- 'status.working': 'Работаю',
60
- 'status.done': 'Готово',
61
- 'status.failed': 'Ход не удался',
62
- 'status.queued': 'в очереди',
63
- 'status.stop': 'Стоп',
64
- 'status.cache': 'кэш',
65
- 'status.steps': 'шагов',
66
- 'status.thinking': 'жду модель',
67
- 'err.timeout': 'нет активности {min} мин — ход прерван',
68
- 'err.generic': 'что-то пошло не так',
69
- 'msg.started': 'Telegram-мост DSH запущен (headless). Пиши задачу; /help — команды.',
70
- 'msg.unknownCommand': 'Неизвестная команда: {cmd}. Открой /help — там всё, что умеет бот.',
71
- 'msg.langSet': 'Язык переключён на русский.',
72
- 'cmd.help.title': 'Команды',
73
- 'cmd.help.hint': 'Любой текст — задача агенту. В теме форума: /cd <проект> привяжет тему к проекту.',
74
- 'cmd.cd.bound': 'Тема привязана к проекту:\n{path}\n\nДальше все команды идут отсюда.',
75
- 'cmd.cd.reset': 'Каталог темы сброшен на общий.',
76
- 'cmd.cd.none': 'Нет такого каталога: {path}',
77
- 'cmd.new.done': 'Новая сессия в этой ветке. Контекст очищен.',
78
- 'cmd.stop.killed': 'Ход остановлен.',
79
- 'cmd.stop.nothing': 'Нечего останавливать.',
80
- 'menu.status': 'Сессии, модель, активные ходы',
81
- 'menu.cd': 'Привязать тему к каталогу проекта',
82
- 'menu.send': 'Прислать файл из проекта в чат',
83
- 'menu.prices': 'Цены моделей у роутера',
84
- 'menu.stop': 'Остановить текущий ход',
85
- 'menu.new': 'Новая сессия в этой теме',
86
- 'menu.seed': 'Засеять пустую тему текстом и закрепить',
87
- 'menu.sessions': 'Последние сессии',
88
- 'menu.resume': 'Продолжить сессию по id',
89
- 'menu.models': 'Доступные модели',
90
- 'menu.model': 'Показать или сменить модель',
91
- 'menu.skills': 'Установленные скиллы',
92
- 'menu.soul': 'Персона агента',
93
- 'menu.plugins': 'Установленные плагины',
94
- 'menu.plugin': 'Включить или выключить плагин',
95
- 'menu.app': 'Управление десктоп-приложением',
96
- 'menu.restart': 'Перезапустить агента',
97
- 'menu.help': 'Все команды и подсказки',
98
- 'menu.lang': 'Язык интерфейса: en, ru, zh',
99
- },
100
- zh: {
101
- 'status.working': '处理中',
102
- 'status.done': '完成',
103
- 'status.failed': '运行失败',
104
- 'status.queued': '排队中',
105
- 'status.stop': '停止',
106
- 'status.cache': '缓存',
107
- 'status.steps': '步',
108
- 'status.thinking': '等待模型',
109
- 'err.timeout': '{min} 分钟无活动 — 已中断',
110
- 'err.generic': '出现错误',
111
- 'msg.started': 'DSH 的 Telegram 桥已启动(headless)。直接发送任务;/help 查看命令。',
112
- 'msg.unknownCommand': '未知命令:{cmd}。打开 /help 查看机器人支持的全部命令。',
113
- 'msg.langSet': '界面语言已切换为中文。',
114
- 'cmd.help.title': '命令',
115
- 'cmd.help.hint': '发送任意文本即可开始任务。在论坛话题中:/cd <项目> 将该话题绑定到项目目录。',
116
- 'cmd.cd.bound': '话题已绑定到项目:\n{path}\n\n之后所有操作都在此目录执行。',
117
- 'cmd.cd.reset': '话题目录已恢复为默认。',
118
- 'cmd.cd.none': '目录不存在:{path}',
119
- 'cmd.new.done': '已在本话题开始新会话,上下文已清空。',
120
- 'cmd.stop.killed': '已停止当前任务。',
121
- 'cmd.stop.nothing': '没有可停止的任务。',
122
- 'menu.status': '会话、模型、进行中的任务',
123
- 'menu.cd': '将本话题绑定到项目目录',
124
- 'menu.send': '把项目中的文件发送到聊天',
125
- 'menu.prices': '路由器的模型价格',
126
- 'menu.stop': '停止当前任务',
127
- 'menu.new': '在本话题开始新会话',
128
- 'menu.seed': '用文本初始化空话题并置顶',
129
- 'menu.sessions': '最近的会话',
130
- 'menu.resume': '按 id 继续会话',
131
- 'menu.models': '可用模型',
132
- 'menu.model': '查看或切换模型',
133
- 'menu.skills': '已安装的技能',
134
- 'menu.soul': '代理人格设定',
135
- 'menu.plugins': '已安装的插件',
136
- 'menu.plugin': '启用或停用插件',
137
- 'menu.app': '桌面应用控制',
138
- 'menu.restart': '重启代理',
139
- 'menu.help': '全部命令与提示',
140
- 'menu.lang': '界面语言:en、ru、zh',
141
- },
142
- };
143
-
144
- /** Telegram sends zh-hans / zh-hans-cn / zh-CN; all of them mean the same locale. */
145
- export function normalizeLang(code) {
146
- const raw = String(code || '').toLowerCase();
147
- if (!raw) return '';
148
- if (raw.startsWith('zh')) return 'zh';
149
- if (raw.startsWith('en')) return 'en';
150
- if (raw.startsWith('ru')) return 'ru';
151
- return '';
152
- }
153
-
154
- export function t(lang, key, vars = {}) {
155
- const table = DICT[lang] || DICT.ru;
156
- const text = table[key] ?? DICT.ru[key] ?? key;
157
- return String(text).replace(/\{(\w+)\}/g, (_, name) => (vars[name] ?? `{${name}}`));
158
- }
159
-
160
- /** Menu rows for setMyCommands, translated per locale. */
161
- export function menuFor(lang, commands) {
162
- return commands.map((c) => ({ command: c.command, description: t(lang, `menu.${c.command}`) }));
163
- }
164
-
165
- /** Language for a message: topic choice, then config default, then Telegram. */
166
- export function resolveLang(state, key, msg, config = {}) {
167
- const chosen = state?.lang?.[key] || config.lang;
168
- // A foreign user must land in English, not Russian: Telegram reports the
169
- // sender's own locale, and everything outside ru/zh/en falls back to en.
170
- return normalizeLang(chosen) || normalizeLang(msg?.from?.language_code) || 'en';
171
- }
1
+ // i18n — three locales for the user-visible surfaces.
2
+ //
3
+ // Scope: everything a person sees without asking for it — the Telegram command
4
+ // menu, the live status message, turn errors and the everyday commands. Long
5
+ // explanatory texts stay in the source language until they are translated
6
+ // properly; a half-translated sentence is worse than a consistent one.
7
+ //
8
+ // Language resolution order: per-topic choice (/lang) -> config.lang -> the
9
+ // sender's Telegram language_code -> 'ru'.
10
+ import { readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ export const LANGS = ['en', 'ru', 'zh'];
14
+
15
+ const DICT = {
16
+ en: {
17
+ 'status.working': 'Working',
18
+ 'status.done': 'Done',
19
+ 'status.failed': 'Turn failed',
20
+ 'status.queued': 'queued',
21
+ 'status.stop': 'Stop',
22
+ 'help.body': "Write the task as plain text. Each forum topic is its own session:\n\nWork: /cd \u003cproject\u003e · /new · /stop · /send \u003cfile\u003e · /seed \u003ctext\u003e\nSessions: /status · /sessions [n] · /resume \u003cid\u003e\nModels: /models · /model \u003cid\u003e · /prices\nExtensions: /skills · /plugins · /plugin add|remove · /soul\nOther: /lang · /doctor · /logs [errors|N] · /app · /restart · /help",
23
+ 'btn.stop': 'Stop',
24
+ 'btn.logs': 'Logs',
25
+ 'btn.retry': 'Retry',
26
+ 'btn.doctor': 'Doctor',
27
+ 'status.cache': 'cache',
28
+ 'status.steps': 'steps',
29
+ 'status.thinking': 'waiting for the model',
30
+ 'err.timeout': 'no activity for {min} min — turn interrupted',
31
+ 'err.generic': 'something went wrong',
32
+ 'msg.started': 'Telegram bridge for DSH is running (headless). Send a task; /help for commands.',
33
+ 'msg.unknownCommand': 'Unknown command: {cmd}. Open /help — it lists everything this bot can do.',
34
+ 'msg.langSet': 'Language set to English.',
35
+ 'cmd.help.title': 'Commands',
36
+ 'cmd.help.hint': 'Send any text to start a task. In a forum topic: /cd <project> binds the topic to that project.',
37
+ 'cmd.cd.bound': 'Topic bound to project:\n{path}\n\nAll turns run from this directory.',
38
+ 'cmd.cd.reset': 'Topic directory reset to the shared one.',
39
+ 'cmd.cd.none': 'No such directory: {path}',
40
+ 'cmd.new.done': 'New session in this topic. Context cleared.',
41
+ 'cmd.stop.killed': 'Turn stopped.',
42
+ 'cmd.stop.nothing': 'Nothing to stop.',
43
+ 'menu.status': 'Sessions, model, active turns',
44
+ 'menu.cd': 'Bind this topic to a project directory',
45
+ 'menu.send': 'Send a file from the project into the chat',
46
+ 'menu.prices': 'Model prices from the router',
47
+ 'menu.stop': 'Stop the current turn',
48
+ 'menu.new': 'New session in this topic',
49
+ 'menu.seed': 'Seed an empty topic with text and pin it',
50
+ 'menu.sessions': 'Recent sessions',
51
+ 'menu.resume': 'Continue a session by id',
52
+ 'menu.models': 'Available models',
53
+ 'menu.model': 'Show or switch the model',
54
+ 'menu.skills': 'Installed skills',
55
+ 'menu.soul': 'Agent persona',
56
+ 'menu.plugins': 'Installed plugins',
57
+ 'menu.plugin': 'Enable or disable a plugin',
58
+ 'menu.app': 'Desktop app control',
59
+ 'menu.restart': 'Restart the agent',
60
+ 'menu.help': 'All commands and hints',
61
+ 'menu.lang': 'Interface language: en, ru, zh',
62
+ },
63
+ ru: {
64
+ 'status.working': 'Работаю',
65
+ 'status.done': 'Готово',
66
+ 'status.failed': 'Ход не удался',
67
+ 'status.queued': 'в очереди',
68
+ 'status.stop': 'Стоп',
69
+ 'help.body': "Задачу можно писать обычным текстом. Каждая ветка форума — отдельная сессия:\n\nРабота: /cd \u003cпроект\u003e · /new · /stop · /send \u003cфайл\u003e · /seed \u003cтекст\u003e\nСессии: /status · /sessions [n] · /resume \u003cid\u003e\nМодели: /models · /model \u003cid\u003e · /prices\nРасширения: /skills · /plugins · /plugin add|remove · /soul\nПрочее: /lang · /doctor · /logs [errors|N] · /app · /restart · /help",
70
+ 'btn.stop': 'Стоп',
71
+ 'btn.logs': 'Логи',
72
+ 'btn.retry': 'Повторить',
73
+ 'btn.doctor': 'Диагноз',
74
+ 'status.cache': 'кэш',
75
+ 'status.steps': 'шагов',
76
+ 'status.thinking': 'жду модель',
77
+ 'err.timeout': 'нет активности {min} мин — ход прерван',
78
+ 'err.generic': 'что-то пошло не так',
79
+ 'msg.started': 'Telegram-мост DSH запущен (headless). Пиши задачу; /help — команды.',
80
+ 'msg.unknownCommand': 'Неизвестная команда: {cmd}. Открой /help — там всё, что умеет бот.',
81
+ 'msg.langSet': 'Язык переключён на русский.',
82
+ 'cmd.help.title': 'Команды',
83
+ 'cmd.help.hint': 'Любой текст — задача агенту. В теме форума: /cd <проект> привяжет тему к проекту.',
84
+ 'cmd.cd.bound': 'Тема привязана к проекту:\n{path}\n\nДальше все команды идут отсюда.',
85
+ 'cmd.cd.reset': 'Каталог темы сброшен на общий.',
86
+ 'cmd.cd.none': 'Нет такого каталога: {path}',
87
+ 'cmd.new.done': 'Новая сессия в этой ветке. Контекст очищен.',
88
+ 'cmd.stop.killed': 'Ход остановлен.',
89
+ 'cmd.stop.nothing': 'Нечего останавливать.',
90
+ 'menu.status': 'Сессии, модель, активные ходы',
91
+ 'menu.cd': 'Привязать тему к каталогу проекта',
92
+ 'menu.send': 'Прислать файл из проекта в чат',
93
+ 'menu.prices': 'Цены моделей у роутера',
94
+ 'menu.stop': 'Остановить текущий ход',
95
+ 'menu.new': 'Новая сессия в этой теме',
96
+ 'menu.seed': 'Засеять пустую тему текстом и закрепить',
97
+ 'menu.sessions': 'Последние сессии',
98
+ 'menu.resume': 'Продолжить сессию по id',
99
+ 'menu.models': 'Доступные модели',
100
+ 'menu.model': 'Показать или сменить модель',
101
+ 'menu.skills': 'Установленные скиллы',
102
+ 'menu.soul': 'Персона агента',
103
+ 'menu.plugins': 'Установленные плагины',
104
+ 'menu.plugin': 'Включить или выключить плагин',
105
+ 'menu.app': 'Управление десктоп-приложением',
106
+ 'menu.restart': 'Перезапустить агента',
107
+ 'menu.help': 'Все команды и подсказки',
108
+ 'menu.lang': 'Язык интерфейса: en, ru, zh',
109
+ },
110
+ zh: {
111
+ 'status.working': '处理中',
112
+ 'status.done': '完成',
113
+ 'status.failed': '运行失败',
114
+ 'status.queued': '排队中',
115
+ 'status.stop': '停止',
116
+ 'help.body': "任务直接以文本发送。每个论坛话题是一个独立会话:\n\n工作: /cd \u003c项目\u003e · /new · /stop · /send \u003c文件\u003e · /seed \u003c文本\u003e\n会话: /status · /sessions [n] · /resume \u003cid\u003e\n模型: /models · /model \u003cid\u003e · /prices\n扩展: /skills · /plugins · /plugin add|remove · /soul\n其他: /lang · /doctor · /logs [errors|N] · /app · /restart · /help",
117
+ 'btn.stop': '停止',
118
+ 'btn.logs': '日志',
119
+ 'btn.retry': '重试',
120
+ 'btn.doctor': '诊断',
121
+ 'status.cache': '缓存',
122
+ 'status.steps': '步',
123
+ 'status.thinking': '等待模型',
124
+ 'err.timeout': '{min} 分钟无活动 — 已中断',
125
+ 'err.generic': '出现错误',
126
+ 'msg.started': 'DSH 的 Telegram 桥已启动(headless)。直接发送任务;/help 查看命令。',
127
+ 'msg.unknownCommand': '未知命令:{cmd}。打开 /help 查看机器人支持的全部命令。',
128
+ 'msg.langSet': '界面语言已切换为中文。',
129
+ 'cmd.help.title': '命令',
130
+ 'cmd.help.hint': '发送任意文本即可开始任务。在论坛话题中:/cd <项目> 将该话题绑定到项目目录。',
131
+ 'cmd.cd.bound': '话题已绑定到项目:\n{path}\n\n之后所有操作都在此目录执行。',
132
+ 'cmd.cd.reset': '话题目录已恢复为默认。',
133
+ 'cmd.cd.none': '目录不存在:{path}',
134
+ 'cmd.new.done': '已在本话题开始新会话,上下文已清空。',
135
+ 'cmd.stop.killed': '已停止当前任务。',
136
+ 'cmd.stop.nothing': '没有可停止的任务。',
137
+ 'menu.status': '会话、模型、进行中的任务',
138
+ 'menu.cd': '将本话题绑定到项目目录',
139
+ 'menu.send': '把项目中的文件发送到聊天',
140
+ 'menu.prices': '路由器的模型价格',
141
+ 'menu.stop': '停止当前任务',
142
+ 'menu.new': '在本话题开始新会话',
143
+ 'menu.seed': '用文本初始化空话题并置顶',
144
+ 'menu.sessions': '最近的会话',
145
+ 'menu.resume': '按 id 继续会话',
146
+ 'menu.models': '可用模型',
147
+ 'menu.model': '查看或切换模型',
148
+ 'menu.skills': '已安装的技能',
149
+ 'menu.soul': '代理人格设定',
150
+ 'menu.plugins': '已安装的插件',
151
+ 'menu.plugin': '启用或停用插件',
152
+ 'menu.app': '桌面应用控制',
153
+ 'menu.restart': '重启代理',
154
+ 'menu.help': '全部命令与提示',
155
+ 'menu.lang': '界面语言:en、ru、zh',
156
+ },
157
+ };
158
+
159
+ /** Telegram sends zh-hans / zh-hans-cn / zh-CN; all of them mean the same locale. */
160
+ export function normalizeLang(code) {
161
+ const raw = String(code || '').toLowerCase();
162
+ if (!raw) return '';
163
+ if (raw.startsWith('zh')) return 'zh';
164
+ if (raw.startsWith('en')) return 'en';
165
+ if (raw.startsWith('ru')) return 'ru';
166
+ return '';
167
+ }
168
+
169
+ export function t(lang, key, vars = {}) {
170
+ const table = DICT[lang] || DICT.ru;
171
+ const text = table[key] ?? DICT.ru[key] ?? key;
172
+ return String(text).replace(/\{(\w+)\}/g, (_, name) => (vars[name] ?? `{${name}}`));
173
+ }
174
+
175
+ /** Menu rows for setMyCommands, translated per locale. */
176
+ export function menuFor(lang, commands) {
177
+ return commands.map((c) => ({ command: c.command, description: t(lang, `menu.${c.command}`) }));
178
+ }
179
+
180
+ /** Language for a message: topic choice, then config default, then Telegram. */
181
+ export function resolveLang(state, key, msg, config = {}) {
182
+ const chosen = state?.lang?.[key] || config.lang;
183
+ // A foreign user must land in English, not Russian: Telegram reports the
184
+ // sender's own locale, and everything outside ru/zh/en falls back to en.
185
+ return normalizeLang(chosen) || normalizeLang(msg?.from?.language_code) || 'en';
186
+ }
package/package.json CHANGED
@@ -1,51 +1,50 @@
1
1
  {
2
- "name": "dsh-tg-bridge",
3
- "version": "1.1.0",
4
- "description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
5
- "type": "module",
6
- "main": "bridge.mjs",
7
- "bin": {
8
- "dsh-tg": "./cli.mjs"
9
- },
10
- "files": [
11
- "cli.mjs",
12
- "i18n.mjs",
13
- "bridge.mjs",
14
- "setup.mjs",
15
- "control.mjs",
16
- "bridge-kick.mjs",
17
- "bridge-restart-idle.mjs",
18
- "restart-after-send.mjs",
19
- "prices.mjs",
20
- "bot-menu.mjs",
21
- "transcribe.py",
22
- "patches/",
23
- "docs/",
24
- "config.example.json",
25
- "install.sh",
26
- "install.ps1",
27
- "README.md",
28
- "LICENSE"
29
- ],
30
- "scripts": {
31
- "start": "node bridge.mjs",
32
- "setup": "node setup.mjs",
33
- "check": "node setup.mjs --check",
34
- "menu": "node bot-menu.mjs",
35
- "doctor": "node bridge-kick.mjs"
36
- },
37
- "engines": {
38
- "node": ">=20"
39
- },
40
- "keywords": [
41
- "deepseek-harness",
42
- "dsh",
43
- "telegram",
44
- "bridge",
45
- "bot",
46
- "whisper",
47
- "voice"
48
- ],
49
- "license": "MIT",
50
- "dependencies": {}
51
- }
2
+ "name": "dsh-tg-bridge",
3
+ "version": "1.1.2",
4
+ "description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
5
+ "type": "module",
6
+ "main": "bridge.mjs",
7
+ "bin": {
8
+ "dsh-tg": "./cli.mjs"
9
+ },
10
+ "scripts": {
11
+ "start": "node bridge.mjs",
12
+ "setup": "node setup.mjs",
13
+ "check": "node setup.mjs --check",
14
+ "menu": "node bot-menu.mjs",
15
+ "doctor": "node bridge-kick.mjs"
16
+ },
17
+ "engines": {
18
+ "node": "\u003e=20"
19
+ },
20
+ "keywords": [
21
+ "deepseek-harness",
22
+ "dsh",
23
+ "telegram",
24
+ "bridge",
25
+ "bot",
26
+ "whisper",
27
+ "voice"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+
32
+ },
33
+ "files": [
34
+ "bridge.mjs",
35
+ "control.mjs",
36
+ "i18n.mjs",
37
+ "prices.mjs",
38
+ "setup.mjs",
39
+ "bot-menu.mjs",
40
+ "bridge-kick.mjs",
41
+ "bridge-restart-idle.mjs",
42
+ "restart-after-send.mjs",
43
+ "transcribe.py",
44
+ "cli.mjs",
45
+ "README.md",
46
+ "LICENSE",
47
+ "config.example.json",
48
+ "patches/"
49
+ ]
50
+ }