dsh-tg-bridge 1.0.2 → 1.1.1
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 +25 -17
- package/control.mjs +26 -20
- package/i18n.mjs +186 -0
- package/package.json +50 -50
- package/setup.mjs +1 -10
- package/docs/CHANGELOG.md +0 -32
- package/docs/RELEASE-CRITERIA.md +0 -23
- package/docs/ROADMAP.md +0 -51
- package/docs/STATE.md +0 -64
- package/docs/UX-ANALYSIS.md +0 -62
- package/docs/archive/DSH-ROADMAP-HANDOFF.md +0 -120
- package/docs/archive/PLAN.md +0 -119
- package/docs/archive/PROGRESS.md +0 -94
- package/install.ps1 +0 -36
- package/install.sh +0 -32
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');
|
|
@@ -799,7 +800,7 @@ async function startProgress(chatId, threadId) {
|
|
|
799
800
|
// Stop is available for as long as the turn runs; the keyboard is cleared
|
|
800
801
|
// when the status turns into the final line, so a stale button cannot be
|
|
801
802
|
// pressed after the work is over.
|
|
802
|
-
reply_markup: stopKeyboard(sessionKey(chatId, threadId)),
|
|
803
|
+
reply_markup: stopKeyboard(sessionKey(chatId, threadId), lang),
|
|
803
804
|
...(threadId > 0 ? { message_thread_id: threadId } : {}),
|
|
804
805
|
});
|
|
805
806
|
messageId = res.message_id;
|
|
@@ -877,8 +878,10 @@ async function startProgress(chatId, threadId) {
|
|
|
877
878
|
if (result?.error) {
|
|
878
879
|
// A failed turn leaves the status message as the error report and swaps
|
|
879
880
|
// «Стоп» for «Повторить»: the person's next move is a retry, not a stop.
|
|
880
|
-
const
|
|
881
|
-
|
|
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));
|
|
882
885
|
return true;
|
|
883
886
|
}
|
|
884
887
|
await edit(body(true), null);
|
|
@@ -927,6 +930,8 @@ async function handleMessage(msg) {
|
|
|
927
930
|
touchTopic(threadId);
|
|
928
931
|
|
|
929
932
|
const key = sessionKey(chatId, threadId);
|
|
933
|
+
// Language for everything this turn shows: topic choice, config, then Telegram.
|
|
934
|
+
const lang = resolveLang(state, key, msg, config);
|
|
930
935
|
const where = `${chatId}${threadId ? ` topic=${threadId}` : ''}`;
|
|
931
936
|
if (!config.allowedUserIds.includes(from) && !config.allowedUserIds.includes(chatId)) {
|
|
932
937
|
log(`ignored message from ${from} (chat ${chatId})`);
|
|
@@ -1086,7 +1091,9 @@ async function handleMessage(msg) {
|
|
|
1086
1091
|
log(`unknown command ${head} from ${from}`);
|
|
1087
1092
|
await send(
|
|
1088
1093
|
chatId,
|
|
1089
|
-
|
|
1094
|
+
`${t(lang, 'msg.unknownCommand', { cmd: head })}
|
|
1095
|
+
|
|
1096
|
+
${CONTROL_COMMAND_HINT}`,
|
|
1090
1097
|
threadId,
|
|
1091
1098
|
msg.message_id
|
|
1092
1099
|
);
|
|
@@ -1096,6 +1103,7 @@ async function handleMessage(msg) {
|
|
|
1096
1103
|
if (effective) {
|
|
1097
1104
|
const handled = await handleControl({
|
|
1098
1105
|
command: effective,
|
|
1106
|
+
lang,
|
|
1099
1107
|
arg: effectiveArg,
|
|
1100
1108
|
text,
|
|
1101
1109
|
chatId,
|
|
@@ -1144,7 +1152,7 @@ async function handleMessage(msg) {
|
|
|
1144
1152
|
|
|
1145
1153
|
// Live progress: one status message per turn, rewritten as the agent works.
|
|
1146
1154
|
// Without it a long turn and a hung turn looked exactly alike in the chat.
|
|
1147
|
-
const progress = await startProgress(chatId, threadId);
|
|
1155
|
+
const progress = await startProgress(chatId, threadId, lang);
|
|
1148
1156
|
// Turn telemetry for the log: what the agent did, how much it cost, on which
|
|
1149
1157
|
// model. Without it /logs shows only "in" and "sent" — useless when asking
|
|
1150
1158
|
// "why did this take 9 minutes".
|
|
@@ -1334,23 +1342,23 @@ const lastPrompt = new Map();
|
|
|
1334
1342
|
const approvals = new Map();
|
|
1335
1343
|
let approvalSeq = 0;
|
|
1336
1344
|
|
|
1337
|
-
function stopKeyboard(key) {
|
|
1345
|
+
function stopKeyboard(key, lang = 'ru') {
|
|
1338
1346
|
return {
|
|
1339
1347
|
inline_keyboard: [
|
|
1340
1348
|
[
|
|
1341
|
-
{ text:
|
|
1342
|
-
{ text:
|
|
1349
|
+
{ text: `⏹ ${t(lang, 'btn.stop')}`, callback_data: 'stop' },
|
|
1350
|
+
{ text: `📄 ${t(lang, 'btn.logs')}`, callback_data: 'logs' },
|
|
1343
1351
|
],
|
|
1344
1352
|
],
|
|
1345
1353
|
};
|
|
1346
1354
|
}
|
|
1347
1355
|
|
|
1348
|
-
function errorKeyboard(key) {
|
|
1356
|
+
function errorKeyboard(key, lang = 'ru') {
|
|
1349
1357
|
return {
|
|
1350
1358
|
inline_keyboard: [
|
|
1351
1359
|
[
|
|
1352
|
-
{ text:
|
|
1353
|
-
{ text:
|
|
1360
|
+
{ text: `🔁 ${t(lang, 'btn.retry')}`, callback_data: 'retry' },
|
|
1361
|
+
{ text: `🩺 ${t(lang, 'btn.doctor')}`, callback_data: 'doctor' },
|
|
1354
1362
|
],
|
|
1355
1363
|
],
|
|
1356
1364
|
};
|
|
@@ -1566,7 +1574,7 @@ async function main() {
|
|
|
1566
1574
|
botUsername = me.username || '';
|
|
1567
1575
|
log(`bot @${me.username}`);
|
|
1568
1576
|
for (const id of config.allowedUserIds) {
|
|
1569
|
-
try { await send(id,
|
|
1577
|
+
try { await send(id, `🤖 ${t(config.lang || 'ru', 'msg.started')}`); } catch {}
|
|
1570
1578
|
}
|
|
1571
1579
|
} catch (error) {
|
|
1572
1580
|
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,21 +684,26 @@ 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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
'Сессии: /new · /status · /sessions [n] · /resume <id>',
|
|
692
|
-
'Модель: /models · /model <id> [both]',
|
|
693
|
-
'Расширения: /plugins [headless] · /plugin add <pkg> · /plugin remove <name> · /plugin disable|enable <id>',
|
|
694
|
-
'Скиллы: /skills · /skills <name>',
|
|
695
|
-
'Персона: /soul · /soul <текст>',
|
|
696
|
-
'Служебное: /stop · /restart confirm · /app restart confirm',
|
|
697
|
-
'Темы: /seed <текст> — засеять пустую тему и закрепить сообщение',
|
|
698
|
-
'Команды можно писать и слитно: /apprestart.confirm = /app restart confirm',
|
|
699
|
-
].join('\n'),
|
|
700
|
-
);
|
|
703
|
+
// One translated body keeps the help in step with the menu; the Russian
|
|
704
|
+
// text stays as the fallback when a key is missing.
|
|
705
|
+
const helpLang = normalizeLang(state.lang?.[key] || config.lang) || 'ru';
|
|
706
|
+
say(t(helpLang, 'help.body'));
|
|
701
707
|
return true;
|
|
702
708
|
}
|
|
703
709
|
|
package/i18n.mjs
ADDED
|
@@ -0,0 +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
|
+
'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,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.1",
|
|
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
|
+
}
|