dsh-tg-bridge 1.2.5 → 1.2.7

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
@@ -74,6 +74,21 @@ irm https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.ps1 |
74
74
  Id берётся у [@userinfobot](https://t.me/userinfobot), токен — у
75
75
  [@BotFather](https://t.me/BotFather).
76
76
 
77
+ ### Настройки бота в Telegram
78
+
79
+ Три вещи, без которых бот будет молчать в группе или работать наполовину:
80
+
81
+ 1. **Privacy mode → Disable** (`/setprivacy` у BotFather). Иначе бот видит
82
+ только команды, а обычный текст в теме — нет.
83
+ 2. **Темы включены** в настройках группы (Edit → Topics). Без них вся работа
84
+ идёт в один чат, отдельные сессии на тему не создаются.
85
+ 3. **Право Pin Messages** для бота. Нужно только для закрепления первого
86
+ сообщения темы; без него мост продолжит работать, но пин не поставит.
87
+
88
+ Меню команд настраивать руками не нужно: мост ставит его сам при старте, сразу
89
+ на русском, английском и китайском. Полная таблица настроек, права, проверка и
90
+ типичные ошибки — [docs/TELEGRAM-SETUP.md](docs/TELEGRAM-SETUP.md).
91
+
77
92
  **Автозапуск** установщик ставит сам: `launchd` на macOS, `systemd --user` на
78
93
  Linux. На Windows `setup` регистрирует задачу `DSH-TG-Bridge` и минутный сторож
79
94
  `DSH-Bridge-Kick`, который поднимает мост после падения.
@@ -153,6 +168,7 @@ Telegram → bridge.mjs → dsh --profile headless --json → ответ
153
168
 
154
169
  ## Документация
155
170
 
171
+ - [docs/TELEGRAM-SETUP.md](docs/TELEGRAM-SETUP.md) — настройки BotFather, группы и права бота
156
172
  - [docs/STATE.md](docs/STATE.md) — что работает, известные проблемы
157
173
  - [docs/ROADMAP.md](docs/ROADMAP.md) — что дальше
158
174
  - [docs/UX-ANALYSIS.md](docs/UX-ANALYSIS.md) — разбор интерактива
package/bridge.mjs CHANGED
@@ -22,7 +22,7 @@ import dns from 'node:dns';
22
22
  import net from 'node:net';
23
23
  dns.setDefaultResultOrder('ipv4first');
24
24
  net.setDefaultAutoSelectFamily(false);
25
- import { handleControl, CONTROL_COMMANDS, CONTROL_COMMAND_HINT } from './control.mjs';
25
+ import { handleControl, CONTROL_COMMANDS } from './control.mjs';
26
26
  import { renderPrices } from './prices.mjs';
27
27
  import { t, resolveLang } from './i18n.mjs';
28
28
 
@@ -676,14 +676,14 @@ async function ensureTopicSeed(chatId, threadId, text) {
676
676
  * reads as a log of what was started instead of a wall of prompts. The full
677
677
  * text lives in the topic itself; this is only the pointer.
678
678
  */
679
- async function logTopicToGeneral(chatId, threadId, text) {
679
+ async function logTopicToGeneral(chatId, threadId, text, lang = 'ru') {
680
680
  if (config.topicLog === false) return;
681
681
  const head = String(text ?? '').replace(/\s+/g, ' ').trim();
682
682
  const preview = head.length > 90 ? `${head.slice(0, 90)}…` : head;
683
683
  try {
684
684
  await tg('sendMessage', {
685
685
  chat_id: chatId,
686
- text: `📌 Новая тема: ${preview}\n→ работа идёт в теме, здесь — лог.`,
686
+ text: t(lang, 'msg.topicLog', { preview }),
687
687
  disable_web_page_preview: true,
688
688
  });
689
689
  log(`topic ${threadId} logged to General`);
@@ -845,10 +845,13 @@ async function startProgress(chatId, threadId, lang = 'ru') {
845
845
  // Three recent steps is enough to see the shape of the work; six lines of
846
846
  // shell commands read as noise on a phone.
847
847
  const tail = steps.slice(-3).map((line) => `• ${line}`).join('\n');
848
- const more = steps.length > 3 ? `\n…и ещё ${steps.length - 3}` : '';
848
+ const more = steps.length > 3 ? `\n${t(lang, 'status.more', { n: steps.length - 3 })}` : '';
849
849
  // The cache line belongs to the outcome, not to every intermediate edit.
850
850
  const cache = final && usage.cacheReadTokens
851
- ? `\n🧠 кэш: ${Math.round((usage.cacheReadTokens / Math.max(1, usage.cacheReadTokens + (usage.inputTokens ?? 0))) * 100)}% (${Math.round(usage.cacheReadTokens / 1000)}k из кэша)`
851
+ ? `\n${t(lang, 'status.cacheLine', {
852
+ pct: Math.round((usage.cacheReadTokens / Math.max(1, usage.cacheReadTokens + (usage.inputTokens ?? 0))) * 100),
853
+ k: Math.round(usage.cacheReadTokens / 1000),
854
+ })}`
852
855
  : '';
853
856
  return (tail ? `${head}\n${tail}${more}${cache}` : `${head}${cache}`);
854
857
  };
@@ -858,10 +861,6 @@ async function startProgress(chatId, threadId, lang = 'ru') {
858
861
  chat_id: chatId,
859
862
  text: body(),
860
863
  disable_web_page_preview: true,
861
- // Stop is available for as long as the turn runs; the keyboard is cleared
862
- // when the status turns into the final line, so a stale button cannot be
863
- // pressed after the work is over.
864
- reply_markup: stopKeyboard(sessionKey(chatId, threadId), lang),
865
864
  ...(threadId > 0 ? { message_thread_id: threadId } : {}),
866
865
  });
867
866
  messageId = res.message_id;
@@ -1013,7 +1012,7 @@ async function handleMessage(msg) {
1013
1012
  try {
1014
1013
  const res = await tg('sendMessage', {
1015
1014
  chat_id: chatId,
1016
- text: '🎤 Расшифровываю…',
1015
+ text: t(lang, 'msg.transcribing'),
1017
1016
  disable_web_page_preview: true,
1018
1017
  ...(threadId > 0 ? { message_thread_id: threadId } : {}),
1019
1018
  ...(msg.message_id ? { reply_to_message_id: msg.message_id } : {}),
@@ -1113,7 +1112,7 @@ async function handleMessage(msg) {
1113
1112
  const seed = await ensureTopicSeed(chatId, threadId, text);
1114
1113
  if (seed) {
1115
1114
  log(`topic ${threadId} seeded with message=${seed}`);
1116
- await logTopicToGeneral(chatId, threadId, text);
1115
+ await logTopicToGeneral(chatId, threadId, text, lang);
1117
1116
  }
1118
1117
  }
1119
1118
 
@@ -1154,7 +1153,7 @@ async function handleMessage(msg) {
1154
1153
  chatId,
1155
1154
  `${t(lang, 'msg.unknownCommand', { cmd: head })}
1156
1155
 
1157
- ${CONTROL_COMMAND_HINT}`,
1156
+ ${t(lang, 'help.body')}`,
1158
1157
  threadId,
1159
1158
  msg.message_id
1160
1159
  );
@@ -1444,17 +1443,6 @@ const lastPrompt = new Map();
1444
1443
  const approvals = new Map();
1445
1444
  let approvalSeq = 0;
1446
1445
 
1447
- function stopKeyboard(key, lang = 'ru') {
1448
- return {
1449
- inline_keyboard: [
1450
- [
1451
- { text: `⏹ ${t(lang, 'btn.stop')}`, callback_data: 'stop' },
1452
- { text: `📄 ${t(lang, 'btn.logs')}`, callback_data: 'logs' },
1453
- ],
1454
- ],
1455
- };
1456
- }
1457
-
1458
1446
  function errorKeyboard(key, lang = 'ru') {
1459
1447
  return {
1460
1448
  inline_keyboard: [
@@ -1682,9 +1670,10 @@ async function main() {
1682
1670
  const notice = JSON.parse(readFileSync(RESTART_NOTICE, 'utf8'));
1683
1671
  unlinkSync(RESTART_NOTICE);
1684
1672
  if (Date.now() - Number(notice.at || 0) < 10 * 60 * 1000) {
1673
+ const lang = resolveLang(state, sessionKey(notice.chatId, notice.threadId || 0), null, config);
1685
1674
  await send(
1686
1675
  notice.chatId,
1687
- `✅ Мост перезапущен — на связи (pid ${process.pid}).\nТема и сессия прежние, можно продолжать.`,
1676
+ `✅ ${t(lang, 'msg.restarted', { pid: process.pid })}`,
1688
1677
  notice.threadId || 0,
1689
1678
  );
1690
1679
  }
@@ -1741,10 +1730,12 @@ async function main() {
1741
1730
  if (!queues.has(queueKey)) return; // already finished
1742
1731
  const started = turnStartedAt.get(queueKey);
1743
1732
  const mins = started ? Math.max(1, Math.round((Date.now() - started) / 60000)) : 0;
1744
- const howLong = mins > 0 ? `идёт уже ${mins} мин` : 'ещё идёт';
1733
+ const notice = mins > 0
1734
+ ? t(lang, 'msg.busy.since', { min: mins })
1735
+ : t(lang, 'msg.busy.now');
1745
1736
  await send(
1746
1737
  msg.chat.id,
1747
- `⏳ Предыдущий ход ${howLong} — твоё сообщение в очереди.\nЕсли это затянулось, /stop прервёт ход, и я сразу возьмусь за твоим.`,
1738
+ `⏳ ${notice}`,
1748
1739
  thread,
1749
1740
  );
1750
1741
  }
package/control.mjs CHANGED
@@ -349,17 +349,6 @@ export const CONTROL_COMMANDS = [
349
349
  'prices', 'help', 'doctor', 'logs', 'lang',
350
350
  ];
351
351
 
352
- /** One-line menu used when an unknown command arrives. */
353
- export const CONTROL_COMMAND_HINT =
354
- 'Работа: /cd · /new · /stop · /send · /seed\n' +
355
- 'Сессии: /status · /sessions · /resume\n' +
356
- 'Модели: /models · /model · /prices\n' +
357
- 'Расширения: /skills · /soul\n' +
358
- 'Плагины: /plugins [headless] — список; /plugins add|remove|disable|enable <что> — действия\n' +
359
- 'Диагностика: /doctor · /logs\n' +
360
- 'Язык: /lang en|ru|zh\n' +
361
- 'Служебное: /app · /restart · /help';
362
-
363
352
  export async function handleControl(ctx) {
364
353
  const { command, arg, send, reply, log, profile, state, key, config } = ctx;
365
354
  const say = (text) => reply(text);
@@ -549,10 +538,10 @@ export async function handleControl(ctx) {
549
538
 
550
539
  case '/restart': {
551
540
  if (arg !== 'confirm') {
552
- say('Перезапуск моста: /restart confirm. Приложение DSH: /app restart confirm.');
541
+ say(t(ctx.lang || 'ru', 'cmd.restart.prompt'));
553
542
  return true;
554
543
  }
555
- say('Перезапускаю мост…');
544
+ say(t(ctx.lang || 'ru', 'cmd.restart.started'));
556
545
  const bridge = fileURLToPath(new URL('./bridge.mjs', import.meta.url));
557
546
  // Tell the replacement process where to confirm, so the chat does not go
558
547
  // silent after "Перезапускаю мост…" (it did, twice on 2026-10-24).
package/i18n.mjs CHANGED
@@ -25,11 +25,20 @@ const DICT = {
25
25
  'btn.retry': 'Retry',
26
26
  'btn.doctor': 'Doctor',
27
27
  'status.cache': 'cache',
28
+ 'status.more': '…and {n} more',
29
+ 'status.cacheLine': '🧠 cache: {pct}% ({k}k from cache)',
30
+ 'msg.topicLog': '📌 New topic: {preview}\n→ work happens in the topic, this is the log.',
31
+ 'msg.transcribing': '🎤 Transcribing…',
28
32
  'status.steps': 'steps',
29
33
  'status.thinking': 'waiting for the model',
30
34
  'err.timeout': 'no activity for {min} min — turn interrupted',
31
35
  'err.generic': 'something went wrong',
36
+ 'msg.busy.since': '⏳ The previous turn has been running for {min} min — your message is queued.\nIf it stalls, /stop interrupts it and I take yours right away.',
37
+ 'msg.busy.now': '⏳ The previous turn is still running — your message is queued.\nIf it stalls, /stop interrupts it and I take yours right away.',
32
38
  'msg.started': 'Telegram bridge for DSH is running (headless). Send a task; /help for commands.',
39
+ 'msg.restarted': 'Bridge restarted — back online (pid {pid}).\nThe topic and session are the same; keep going.',
40
+ 'cmd.restart.prompt': 'To restart the bridge: /restart confirm. For the DSH app: /app restart confirm.',
41
+ 'cmd.restart.started': 'Restarting the bridge…',
33
42
  'msg.unknownCommand': 'Unknown command: {cmd}. Open /help — it lists everything this bot can do.',
34
43
  'msg.langSet': 'Language set to English.',
35
44
  'cmd.help.title': 'Commands',
@@ -72,11 +81,20 @@ const DICT = {
72
81
  'btn.retry': 'Повторить',
73
82
  'btn.doctor': 'Диагноз',
74
83
  'status.cache': 'кэш',
84
+ 'status.more': '…и ещё {n}',
85
+ 'status.cacheLine': '🧠 кэш: {pct}% ({k}k из кэша)',
86
+ 'msg.topicLog': '📌 Новая тема: {preview}\n→ работа идёт в теме, здесь — лог.',
87
+ 'msg.transcribing': '🎤 Расшифровываю…',
75
88
  'status.steps': 'шагов',
76
89
  'status.thinking': 'жду модель',
77
90
  'err.timeout': 'нет активности {min} мин — ход прерван',
78
91
  'err.generic': 'что-то пошло не так',
92
+ 'msg.busy.since': '⏳ Предыдущий ход идёт уже {min} мин — твоё сообщение в очереди.\nЕсли это затянулось, /stop прервёт ход, и я сразу возьмусь за твоим.',
93
+ 'msg.busy.now': '⏳ Предыдущий ход ещё идёт — твоё сообщение в очереди.\nЕсли это затянулось, /stop прервёт ход, и я сразу возьмусь за твоим.',
79
94
  'msg.started': 'Telegram-мост DSH запущен (headless). Пиши задачу; /help — команды.',
95
+ 'msg.restarted': 'Мост перезапущен — на связи (pid {pid}).\nТема и сессия прежние, можно продолжать.',
96
+ 'cmd.restart.prompt': 'Перезапуск моста: /restart confirm. Приложение DSH: /app restart confirm.',
97
+ 'cmd.restart.started': 'Перезапускаю мост…',
80
98
  'msg.unknownCommand': 'Неизвестная команда: {cmd}. Открой /help — там всё, что умеет бот.',
81
99
  'msg.langSet': 'Язык переключён на русский.',
82
100
  'cmd.help.title': 'Команды',
@@ -119,11 +137,20 @@ const DICT = {
119
137
  'btn.retry': '重试',
120
138
  'btn.doctor': '诊断',
121
139
  'status.cache': '缓存',
140
+ 'status.more': '…还有 {n} 条',
141
+ 'status.cacheLine': '🧠 缓存:{pct}%({k}k 来自缓存)',
142
+ 'msg.topicLog': '📌 新主题:{preview}\n→ 工作在主题里进行,这里是日志。',
143
+ 'msg.transcribing': '🎤 正在转写…',
122
144
  'status.steps': '步',
123
145
  'status.thinking': '等待模型',
124
146
  'err.timeout': '{min} 分钟无活动 — 已中断',
125
147
  'err.generic': '出现错误',
148
+ 'msg.busy.since': '⏳ 上一轮已运行 {min} 分钟 —— 你的消息在队列中。\n如果太久,/stop 会中断它,我马上处理你的消息。',
149
+ 'msg.busy.now': '⏳ 上一轮还在运行 —— 你的消息在队列中。\n如果太久,/stop 会中断它,我马上处理你的消息。',
126
150
  'msg.started': 'DSH 的 Telegram 桥已启动(headless)。直接发送任务;/help 查看命令。',
151
+ 'msg.restarted': '桥已重启,连接正常(pid {pid})。\n主题和会话不变,可以继续。',
152
+ 'cmd.restart.prompt': '重启桥:/restart confirm。重启 DSH 应用:/app restart confirm。',
153
+ 'cmd.restart.started': '正在重启桥…',
127
154
  'msg.unknownCommand': '未知命令:{cmd}。打开 /help 查看机器人支持的全部命令。',
128
155
  'msg.langSet': '界面语言已切换为中文。',
129
156
  'cmd.help.title': '命令',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tg-bridge",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
5
5
  "type": "module",
6
6
  "main": "bridge.mjs",