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 +10 -0
- package/bot-menu.mjs +64 -0
- package/bridge.mjs +14 -8
- package/control.mjs +22 -6
- package/docs/CHANGELOG.md +13 -0
- package/i18n.mjs +171 -0
- package/package.json +51 -50
- package/setup.mjs +1 -10
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"bridge-
|
|
17
|
-
"restart-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"install.
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
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 {
|
|
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')) {
|