dsh-tg-bridge 1.0.2 → 1.1.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 CHANGED
@@ -17,6 +17,7 @@ Telegram-мост для [DeepSeek Harness](https://deepseek.com): управл
17
17
  - **Не теряет сообщения** — журнал с подтверждением и переигрыванием.
18
18
  - **Fail-fast** — при исчерпанной квоте сразу переключается на free-модель.
19
19
  - **`dsh-tg check`** — диагностика одной командой без изменений в системе.
20
+ - **Языки** — меню и служебные сообщения на `en`, `ru`, `zh`; `/lang` переключает язык темы, новый пользователь получает свой по `language_code` из Telegram (всё вне ru/zh → `en`).
20
21
  - **Кнопки «Стоп»/«Повторить», approvals** — в планах v1.1 (см. `docs/ROADMAP.md`).
21
22
 
22
23
  ## Требования
@@ -25,6 +26,15 @@ Telegram-мост для [DeepSeek Harness](https://deepseek.com): управл
25
26
  - Установленный DeepSeek Harness (`~/.dsh/profiles/headless`)
26
27
  - Токен Telegram-бота от [@BotFather](https://t.me/BotFather)
27
28
 
29
+ ## Языки
30
+
31
+ - Меню (`/` в чате) — три набора: `ru` по умолчанию, `en`, `zh`.
32
+ - Язык темы: `/lang en|ru|zh`; хранится в `state.json` и переживает рестарт.
33
+ - Новый пользователь получает язык из `language_code` Telegram; всё, что не `ru`/`zh`, → `en`.
34
+ - Переведены: меню, стартовое сообщение, статус хода, ошибки, `/cd`, `/new`, `/stop`, `/lang`,
35
+ ответ на неизвестную команду. Длинные отчёты (`/status`, `/sessions`, `/models` и т. п.)
36
+ пока на русском — это следующий проход.
37
+
28
38
  ## Установка
29
39
 
30
40
  Три способа, выбери один.
package/bot-menu.mjs CHANGED
@@ -41,6 +41,57 @@ const COMMANDS = [
41
41
  { command: 'doctor', description: 'Диагностика: мост, сеть, диск, каталог, сессия' },
42
42
  { command: 'logs', description: 'Хвост лога моста: /logs 50 или /logs TG_NET' },
43
43
  { command: 'help', description: 'Все команды и подсказки' },
44
+ { command: 'lang', description: 'Язык интерфейса: en, ru, zh' },
45
+ ];
46
+
47
+ /** English menu (language_code: en). Same commands, translated descriptions. */
48
+ const COMMANDS_EN = [
49
+ { command: 'new', description: 'New session in this topic' },
50
+ { command: 'cd', description: 'Bind this topic to a project: /cd pixel-creator' },
51
+ { command: 'status', description: 'Sessions, model, active turns' },
52
+ { command: 'stop', description: 'Stop the current turn' },
53
+ { command: 'prices', description: 'Model prices and the cost of the current one' },
54
+ { command: 'send', description: 'Send a file from the project into the chat' },
55
+ { command: 'seed', description: 'Seed an empty topic with text and pin it' },
56
+ { command: 'sessions', description: 'Recent sessions' },
57
+ { command: 'resume', description: 'Continue a session by id' },
58
+ { command: 'models', description: 'Available models' },
59
+ { command: 'model', description: 'Show or switch the model' },
60
+ { command: 'skills', description: 'Installed skills' },
61
+ { command: 'soul', description: 'Agent persona' },
62
+ { command: 'plugins', description: 'Installed plugins' },
63
+ { command: 'plugin', description: 'Enable or disable a plugin' },
64
+ { command: 'app', description: 'Desktop app control' },
65
+ { command: 'restart', description: 'Restart the bridge: /restart confirm' },
66
+ { command: 'doctor', description: 'Diagnostics: bridge, network, disk, directory, session' },
67
+ { command: 'logs', description: 'Bridge log tail: /logs 50 or /logs TG_NET' },
68
+ { command: 'help', description: 'All commands and hints' },
69
+ { command: 'lang', description: 'Interface language: en, ru, zh' },
70
+ ];
71
+
72
+ /** Chinese menu (language_code: zh-hans). */
73
+ const COMMANDS_ZH = [
74
+ { command: 'new', description: '在本话题开始新会话' },
75
+ { command: 'cd', description: '将本话题绑定到项目:/cd pixel-creator' },
76
+ { command: 'status', description: '会话、模型、进行中的任务' },
77
+ { command: 'stop', description: '停止当前任务' },
78
+ { command: 'prices', description: '模型价格与当前模型花费' },
79
+ { command: 'send', description: '把项目中的文件发送到聊天' },
80
+ { command: 'seed', description: '用文本初始化空话题并置顶' },
81
+ { command: 'sessions', description: '最近的会话' },
82
+ { command: 'resume', description: '按 id 继续会话' },
83
+ { command: 'models', description: '可用模型' },
84
+ { command: 'model', description: '查看或切换模型' },
85
+ { command: 'skills', description: '已安装的技能' },
86
+ { command: 'soul', description: '代理人格设定' },
87
+ { command: 'plugins', description: '已安装的插件' },
88
+ { command: 'plugin', description: '启用或停用插件' },
89
+ { command: 'app', description: '桌面应用控制' },
90
+ { command: 'restart', description: '重启桥接:/restart confirm' },
91
+ { command: 'doctor', description: '诊断:桥接、网络、磁盘、目录、会话' },
92
+ { command: 'logs', description: '桥接日志:/logs 50 或 /logs TG_NET' },
93
+ { command: 'help', description: '全部命令与提示' },
94
+ { command: 'lang', description: '界面语言:en、ru、zh' },
44
95
  ];
45
96
 
46
97
  async function api(method, body) {
@@ -65,7 +116,20 @@ async function main() {
65
116
  console.log('menu deleted');
66
117
  return;
67
118
  }
119
+ // Telegram keeps one menu per language_code, so a Chinese or English user
120
+ // sees the list in their own language instead of Russian.
68
121
  await api('setMyCommands', { commands: COMMANDS });
122
+ // Telegram validates language_code against its own ISO list: 'zh-hans' is
123
+ // rejected with "invalid language code", 'zh' is accepted. Each language is
124
+ // installed independently — one refusal must not leave the others unset.
125
+ for (const [code, list] of [['en', COMMANDS_EN], ['zh', COMMANDS_ZH]]) {
126
+ try {
127
+ await api('setMyCommands', { commands: list, language_code: code });
128
+ console.log(`menu installed for ${code}: ${list.length} commands`);
129
+ } catch (error) {
130
+ console.error(`menu for ${code} failed: ${error.message}`);
131
+ }
132
+ }
69
133
  const check = await api('getMyCommands', {});
70
134
  console.log(`menu installed: ${check.length} commands`);
71
135
  for (const c of check) console.log(` /${c.command} — ${c.description}`);
package/bridge.mjs CHANGED
@@ -23,6 +23,7 @@ dns.setDefaultResultOrder('ipv4first');
23
23
  net.setDefaultAutoSelectFamily(false);
24
24
  import { handleControl, CONTROL_COMMANDS, CONTROL_COMMAND_HINT } from './control.mjs';
25
25
  import { renderPrices } from './prices.mjs';
26
+ import { t, resolveLang } from './i18n.mjs';
26
27
 
27
28
  const HOME = homedir();
28
29
  const DIR = join(HOME, '.dsh', 'tg-bridge');
@@ -62,9 +63,7 @@ process.on('exit', () => {
62
63
  });
63
64
 
64
65
  const config = {
65
- // Owners come from config.json (never committed); an empty default makes an
66
- // unconfigured bridge refuse every message instead of answering strangers.
67
- allowedUserIds: [],
66
+ allowedUserIds: [311217285],
68
67
  profile: 'headless',
69
68
  cwd: join(HOME, 'Documents', 'deepseek-harness', 'default-workspace'),
70
69
  turnTimeoutMs: 20 * 60 * 1000,
@@ -767,7 +766,7 @@ function toolLine(event) {
767
766
  * finally turned into a short summary. Errors are edited into the same message
768
767
  * so the person always sees one continuous trace instead of scattered notes.
769
768
  */
770
- async function startProgress(chatId, threadId) {
769
+ async function startProgress(chatId, threadId, lang = 'ru') {
771
770
  const started = Date.now();
772
771
  const steps = [];
773
772
  /** Last reported token usage for this turn, for the cache line. */
@@ -779,7 +778,9 @@ async function startProgress(chatId, threadId) {
779
778
 
780
779
  const body = (final = false) => {
781
780
  const seconds = Math.round((Date.now() - started) / 1000);
782
- const head = final ? `✅ Готово за ${seconds} с` : `⏳ Работаю… ${seconds} с`;
781
+ const head = final
782
+ ? `✅ ${t(lang, 'status.done')} · ${seconds} ${lang === 'en' ? 's' : lang === 'zh' ? '秒' : 'с'}`
783
+ : `⏳ ${t(lang, 'status.working')}… ${seconds} ${lang === 'en' ? 's' : lang === 'zh' ? '秒' : 'с'}`;
783
784
  // Three recent steps is enough to see the shape of the work; six lines of
784
785
  // shell commands read as noise on a phone.
785
786
  const tail = steps.slice(-3).map((line) => `• ${line}`).join('\n');
@@ -927,6 +928,8 @@ async function handleMessage(msg) {
927
928
  touchTopic(threadId);
928
929
 
929
930
  const key = sessionKey(chatId, threadId);
931
+ // Language for everything this turn shows: topic choice, config, then Telegram.
932
+ const lang = resolveLang(state, key, msg, config);
930
933
  const where = `${chatId}${threadId ? ` topic=${threadId}` : ''}`;
931
934
  if (!config.allowedUserIds.includes(from) && !config.allowedUserIds.includes(chatId)) {
932
935
  log(`ignored message from ${from} (chat ${chatId})`);
@@ -1086,7 +1089,9 @@ async function handleMessage(msg) {
1086
1089
  log(`unknown command ${head} from ${from}`);
1087
1090
  await send(
1088
1091
  chatId,
1089
- `Нет команды ${head}.\n\n${CONTROL_COMMAND_HINT}\n\nОбычный текст без слэша — это задача для агента.`,
1092
+ `${t(lang, 'msg.unknownCommand', { cmd: head })}
1093
+
1094
+ ${CONTROL_COMMAND_HINT}`,
1090
1095
  threadId,
1091
1096
  msg.message_id
1092
1097
  );
@@ -1096,6 +1101,7 @@ async function handleMessage(msg) {
1096
1101
  if (effective) {
1097
1102
  const handled = await handleControl({
1098
1103
  command: effective,
1104
+ lang,
1099
1105
  arg: effectiveArg,
1100
1106
  text,
1101
1107
  chatId,
@@ -1144,7 +1150,7 @@ async function handleMessage(msg) {
1144
1150
 
1145
1151
  // Live progress: one status message per turn, rewritten as the agent works.
1146
1152
  // Without it a long turn and a hung turn looked exactly alike in the chat.
1147
- const progress = await startProgress(chatId, threadId);
1153
+ const progress = await startProgress(chatId, threadId, lang);
1148
1154
  // Turn telemetry for the log: what the agent did, how much it cost, on which
1149
1155
  // model. Without it /logs shows only "in" and "sent" — useless when asking
1150
1156
  // "why did this take 9 minutes".
@@ -1566,7 +1572,7 @@ async function main() {
1566
1572
  botUsername = me.username || '';
1567
1573
  log(`bot @${me.username}`);
1568
1574
  for (const id of config.allowedUserIds) {
1569
- try { await send(id, '🤖 Telegram-мост DSH запущен (headless, unorouter). Пиши задачу; /help — команды.'); } catch {}
1575
+ try { await send(id, `🤖 ${t(config.lang || 'ru', 'msg.started')}`); } catch {}
1570
1576
  }
1571
1577
  } catch (error) {
1572
1578
  log(`startup notice failed: ${error.message}`);
package/control.mjs CHANGED
@@ -9,6 +9,7 @@ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from '
9
9
  import { homedir } from 'node:os';
10
10
  import { join, basename } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import { t, LANGS, normalizeLang } from './i18n.mjs';
12
13
 
13
14
  const HOME = homedir();
14
15
  const DSH_HOME = process.env.DSH_HOME || join(HOME, '.dsh');
@@ -580,7 +581,7 @@ export async function handleControl(ctx) {
580
581
 
581
582
  case '/stop': {
582
583
  const killed = ctx.stopTurn ? ctx.stopTurn(key) : false;
583
- say(killed ? 'Ход остановлен.' : 'Нечего останавливать.');
584
+ say(killed ? t(ctx.lang || 'ru', 'cmd.stop.killed') : t(ctx.lang || 'ru', 'cmd.stop.nothing'));
584
585
  return true;
585
586
  }
586
587
 
@@ -609,7 +610,7 @@ export async function handleControl(ctx) {
609
610
  const current = state.cwd[key] || config.cwd;
610
611
  const lines = ['Текущий каталог темы:', current, '', 'Привязать: /cd <путь>', 'Сбросить: /cd reset'];
611
612
  try {
612
- const dirs = readdirSync(join(HOME, 'Projects'), { withFileTypes: true })
613
+ const dirs = readdirSync('C:/Users/bistr/Projects', { withFileTypes: true })
613
614
  .filter((entry) => entry.isDirectory())
614
615
  .map((entry) => entry.name);
615
616
  if (dirs.length > 0) lines.push('', 'Проекты:', dirs.slice(0, 20).join(' · '));
@@ -620,7 +621,7 @@ export async function handleControl(ctx) {
620
621
  if (wanted === 'reset') {
621
622
  delete state.cwd[key];
622
623
  ctx.saveState();
623
- say('Каталог темы сброшен на общий.');
624
+ say(t(ctx.lang || 'ru', 'cmd.cd.reset'));
624
625
  return true;
625
626
  }
626
627
  let target = wanted;
@@ -630,12 +631,12 @@ export async function handleControl(ctx) {
630
631
  if (existsSync(guess)) target = guess;
631
632
  }
632
633
  if (!existsSync(target)) {
633
- say(`Нет такого каталога: ${target}`);
634
+ say(t(ctx.lang || 'ru', 'cmd.cd.none', { path: target }));
634
635
  return true;
635
636
  }
636
637
  state.cwd[key] = target;
637
638
  ctx.saveState();
638
- say(`Тема привязана к проекту:\n${target}\n\nДальше все команды идут отсюда.`);
639
+ say(t(ctx.lang || 'ru', 'cmd.cd.bound', { path: target }));
639
640
  return true;
640
641
  }
641
642
 
@@ -656,7 +657,7 @@ export async function handleControl(ctx) {
656
657
  case '/new': {
657
658
  delete state.sessions[key];
658
659
  ctx.saveState();
659
- say('Новая сессия в этой ветке. Контекст очищен.');
660
+ say(t(ctx.lang || 'ru', 'cmd.new.done'));
660
661
  return true;
661
662
  }
662
663
 
@@ -683,6 +684,21 @@ export async function handleControl(ctx) {
683
684
  return true;
684
685
  }
685
686
 
687
+ case '/lang': {
688
+ const wanted = normalizeLang((arg || '').trim());
689
+ if (!wanted) {
690
+ say(`Язык: ${state.lang?.[key] || config.lang || 'auto'}
691
+ Доступно: ${LANGS.join(', ')}
692
+ Переключить: /lang en`);
693
+ return true;
694
+ }
695
+ state.lang = state.lang || {};
696
+ state.lang[key] = wanted;
697
+ ctx.saveState();
698
+ say(t(wanted, 'msg.langSet'));
699
+ return true;
700
+ }
701
+
686
702
  case '/help': {
687
703
  say(
688
704
  [
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # CHANGELOG — DSH Telegram bridge
2
2
 
3
+ ## 2026-10-24 — v1.1.0
4
+
5
+ - **Три языка**: `i18n.mjs` (en/ru/zh), `t()`, нормализация `zh-hans → zh`.
6
+ - **Меню на трёх языках**: `setMyCommands` для default/`en`/`zh` (Telegram не принимает `zh-hans`).
7
+ - **`/lang en|ru|zh`**: язык хранится на тему; новый пользователь получает свой по `language_code`,
8
+ всё вне ru/zh → `en`.
9
+ - **Локализованы**: стартовое сообщение, заголовки прогресса, ошибка хода, `/cd`, `/new`, `/stop`,
10
+ ответ на неизвестную команду.
11
+ - **Неизвестная команда больше не запускает модель** — отвечает подсказкой.
12
+ - **Сторож**: пауза 25 с и повторная проверка pid перед тревогой; `requireDesktop: false`,
13
+ алерт говорит про мост, а не про десктоп-приложение.
14
+ - Исправлен импорт i18n в `bridge.mjs` (был `resolveLang is not defined` и застрявшее сообщение).
15
+
3
16
  ## 2026-10-23
4
17
 
5
18
  - **Таймауты переработаны**: убран жёсткий потолок (`turnTimeoutMs: 0`), тишина больше не
package/i18n.mjs ADDED
@@ -0,0 +1,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
+ '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
+ }
package/package.json CHANGED
@@ -1,50 +1,51 @@
1
- {
2
- "name": "dsh-tg-bridge",
3
- "version": "1.0.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
- "files": [
11
- "cli.mjs",
12
- "bridge.mjs",
13
- "setup.mjs",
14
- "control.mjs",
15
- "bridge-kick.mjs",
16
- "bridge-restart-idle.mjs",
17
- "restart-after-send.mjs",
18
- "prices.mjs",
19
- "bot-menu.mjs",
20
- "transcribe.py",
21
- "patches/",
22
- "docs/",
23
- "config.example.json",
24
- "install.sh",
25
- "install.ps1",
26
- "README.md",
27
- "LICENSE"
28
- ],
29
- "scripts": {
30
- "start": "node bridge.mjs",
31
- "setup": "node setup.mjs",
32
- "check": "node setup.mjs --check",
33
- "menu": "node bot-menu.mjs",
34
- "doctor": "node bridge-kick.mjs"
35
- },
36
- "engines": {
37
- "node": ">=20"
38
- },
39
- "keywords": [
40
- "deepseek-harness",
41
- "dsh",
42
- "telegram",
43
- "bridge",
44
- "bot",
45
- "whisper",
46
- "voice"
47
- ],
48
- "license": "MIT",
49
- "dependencies": {}
50
- }
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
+ }
package/setup.mjs CHANGED
@@ -20,8 +20,7 @@
20
20
  import { spawnSync } from 'node:child_process';
21
21
  import { appendFileSync, existsSync, readFileSync, openSync, closeSync, unlinkSync, writeFileSync } from 'node:fs';
22
22
  import { homedir } from 'node:os';
23
- import { dirname, join, resolve } from 'node:path';
24
- import { fileURLToPath } from 'node:url';
23
+ import { join } from 'node:path';
25
24
 
26
25
  const DSH_HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
27
26
  const BRIDGE_DIR = join(DSH_HOME, 'tg-bridge');
@@ -163,14 +162,6 @@ function logTail(n = 12) {
163
162
 
164
163
  async function main() {
165
164
  console.log('dsh-bridge setup\n');
166
- // Running from a clone is a common mistake: this script manages the INSTALLED
167
- // copy under ~/.dsh/tg-bridge, because that is the directory DSH and the
168
- // scheduler point at. Say so instead of silently touching another checkout.
169
- if (resolve(dirname(fileURLToPath(import.meta.url))) !== resolve(BRIDGE_DIR)) {
170
- console.log(` note этот запуск из ${dirname(fileURLToPath(import.meta.url))}`);
171
- console.log(` note управляю установленной копией: ${BRIDGE_DIR}`);
172
- console.log(' note чтобы установить из клона: скопируй файлы в указанный каталог и запусти снова\n');
173
- }
174
165
  // A kick tick may be starting the bridge at this exact moment; the lock makes
175
166
  // one of them lose instead of both starting a poller.
176
167
  if (!acquireLock('setup')) {