dsh-tg-bridge 1.1.2 → 1.2.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 +46 -45
- package/bot-menu.mjs +14 -9
- package/bridge.mjs +64 -4
- package/cli.mjs +185 -29
- package/control.mjs +22 -3
- package/package.json +48 -50
- package/setup.mjs +8 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Telegram-мост для [DeepSeek Harness](https://deepseek.com): управл
|
|
|
18
18
|
- **Fail-fast** — при исчерпанной квоте сразу переключается на free-модель.
|
|
19
19
|
- **`dsh-tg check`** — диагностика одной командой без изменений в системе.
|
|
20
20
|
- **Языки** — меню и служебные сообщения на `en`, `ru`, `zh`; `/lang` переключает язык темы, новый пользователь получает свой по `language_code` из Telegram (всё вне ru/zh → `en`).
|
|
21
|
-
- **Кнопки «Стоп»/«Повторить»,
|
|
21
|
+
- **Кнопки «Стоп»/«Повторить», подтверждения инструментов** — прямо в сообщении, без набора команд.
|
|
22
22
|
|
|
23
23
|
## Требования
|
|
24
24
|
|
|
@@ -37,74 +37,75 @@ Telegram-мост для [DeepSeek Harness](https://deepseek.com): управл
|
|
|
37
37
|
|
|
38
38
|
## Установка
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Одна команда: токен бота и свой числовой id. Руками править ничего не нужно.
|
|
41
41
|
|
|
42
|
-
**
|
|
42
|
+
**Windows (PowerShell)**
|
|
43
43
|
|
|
44
|
-
```
|
|
45
|
-
npx dsh-tg-bridge@latest init
|
|
46
|
-
npx dsh-tg-bridge@latest
|
|
47
|
-
npx dsh-tg-bridge@latest setup # установить и запустить мост
|
|
44
|
+
```powershell
|
|
45
|
+
npx dsh-tg-bridge@latest init --token 123456789:AA... --user 311217285
|
|
46
|
+
npx dsh-tg-bridge@latest setup
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
**
|
|
49
|
+
**macOS / Linux**
|
|
51
50
|
|
|
52
51
|
```bash
|
|
53
|
-
|
|
54
|
-
curl -fsSL https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.sh
|
|
52
|
+
DSH_TG_TOKEN=123456789:AA... DSH_TG_USER=311217285 \
|
|
53
|
+
bash -c "$(curl -fsSL https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.sh)"
|
|
55
54
|
```
|
|
56
55
|
|
|
56
|
+
**Windows одной строкой** (то же самое, ставит и запускает):
|
|
57
|
+
|
|
57
58
|
```powershell
|
|
58
|
-
|
|
59
|
+
$env:DSH_TG_TOKEN='123456789:AA...'; $env:DSH_TG_USER='311217285'
|
|
59
60
|
irm https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.ps1 | iex
|
|
60
61
|
```
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
Что делает `init`:
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
node cli.mjs setup
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
**2. Положить токен бота в хранилище DSH**
|
|
65
|
+
| Что | Куда |
|
|
66
|
+
|---|---|
|
|
67
|
+
| токен бота | `~/.dsh/.credentials.yaml` → `refs.DSH_TELEGRAM_BOT_TOKEN`; остальные ключи не трогает |
|
|
68
|
+
| твой id и рабочий каталог | `~/.dsh/tg-bridge/config.json`; существующий файл не перезаписывает |
|
|
69
|
+
| профиль `headless` | проверяет и подсказывает, как создать, если его нет |
|
|
73
70
|
|
|
74
|
-
|
|
71
|
+
Флаги `init`: `--cwd <путь>` (по умолчанию `~/Projects`), `--voice` (включить
|
|
72
|
+
расшифровку голоса), `--dry-run` (показать, что будет записано, и ничего не менять).
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
DSH_TELEGRAM_BOT_TOKEN: "123456789:AA..."
|
|
79
|
-
```
|
|
74
|
+
Id берётся у [@userinfobot](https://t.me/userinfobot), токен — у
|
|
75
|
+
[@BotFather](https://t.me/BotFather).
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
**Автозапуск** установщик ставит сам: `launchd` на macOS, `systemd --user` на
|
|
78
|
+
Linux. На Windows `setup` регистрирует задачу `DSH-TG-Bridge` и минутный сторож
|
|
79
|
+
`DSH-Bridge-Kick`, который поднимает мост после падения.
|
|
83
80
|
|
|
84
|
-
|
|
81
|
+
**Вручную** (если нужен полный контроль):
|
|
85
82
|
|
|
86
83
|
```bash
|
|
87
|
-
|
|
84
|
+
git clone https://github.com/aleksvega/dsh-tg-bridge.git ~/.dsh/tg-bridge
|
|
85
|
+
cd ~/.dsh/tg-bridge
|
|
86
|
+
node cli.mjs init --token 123456789:AA... --user 311217285
|
|
87
|
+
node cli.mjs check # отчёт, ничего не меняет
|
|
88
|
+
node cli.mjs setup # остановить старый мост и поднять новый
|
|
88
89
|
```
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
это защита от чужих сообщений.
|
|
91
|
+
Если мост уже стоит, `setup` перезапускает его на новом коде и переустанавливает
|
|
92
|
+
меню команд.
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
## Если что-то не так
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
node setup.mjs # остановит старый мост, поднимет новый, поставит меню
|
|
98
|
-
node setup.mjs --check # только отчёт, ничего не меняет
|
|
99
|
-
```
|
|
96
|
+
Ответ на ошибку заканчивается кодом — по нему видно, что чинить:
|
|
100
97
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
| Код | Что значит |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `INSUFFICIENT_QUOTA` | баланс провайдера пуст: пополни или оставь free-модели (`/prices`, `/models`) |
|
|
101
|
+
| `TG_CONFLICT` | того же бота опрашивает второй процесс: `/doctor`, затем `/restart` |
|
|
102
|
+
| `MISSING_CREDENTIAL` | в профиле нет ключа провайдера: добавь в приложении DSH |
|
|
103
|
+
| `NO_DSH_CLI` | не найден CLI DeepSeek Harness: проверь установку приложения |
|
|
104
|
+
| `NETWORK` | нет сети до провайдера: повтори, проверь VPN/DNS |
|
|
105
|
+
| `SANDBOX_DENY` | DSH запретил доступ к файлу или каталогу: проверь путь проекта и права |
|
|
106
|
+
| `UNKNOWN_MODEL` | модель из конфига недоступна: список рабочих в `/models` |
|
|
107
|
+
| `EMPTY_RESPONSE` | модель ничего не вернула: повтори или смени модель (`/model`) |
|
|
108
|
+
| `TURN_FAILED` | причина не распознана: `/doctor` и `/logs errors` |
|
|
108
109
|
|
|
109
110
|
## Проверка
|
|
110
111
|
|
package/bot-menu.mjs
CHANGED
|
@@ -14,9 +14,17 @@ import { homedir } from 'node:os';
|
|
|
14
14
|
const args = new Set(process.argv.slice(2));
|
|
15
15
|
|
|
16
16
|
const token = (() => {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const env = (process.env.DSH_TELEGRAM_BOT_TOKEN || '').trim();
|
|
18
|
+
if (env) return env;
|
|
19
|
+
try {
|
|
20
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
21
|
+
const yaml = readFileSync(join(home, '.credentials.yaml'), 'utf8');
|
|
22
|
+
// `dsh-tg init` writes the plain name; the DSH app adds a random suffix.
|
|
23
|
+
const m = /^\s*(DSH_TELEGRAM_BOT_TOKEN[A-Z0-9_]*):\s*["']?([^"'\s]+)/m.exec(yaml);
|
|
24
|
+
return m ? m[2] : '';
|
|
25
|
+
} catch {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
20
28
|
})();
|
|
21
29
|
|
|
22
30
|
/** The real, implemented surface of dsh-tg-bridge — nothing aspirational. */
|
|
@@ -34,8 +42,7 @@ const COMMANDS = [
|
|
|
34
42
|
{ command: 'model', description: 'Сменить модель: /model <id>' },
|
|
35
43
|
{ command: 'skills', description: 'Скиллы: список или текст одного' },
|
|
36
44
|
{ command: 'soul', description: 'Персона: показать или задать' },
|
|
37
|
-
{ command: 'plugins', description: '
|
|
38
|
-
{ command: 'plugin', description: 'add/remove/disable/enable плагина' },
|
|
45
|
+
{ command: 'plugins', description: 'Список и управление: add/remove/disable/enable' },
|
|
39
46
|
{ command: 'app', description: 'Перезапуск приложения: /app restart confirm' },
|
|
40
47
|
{ command: 'restart', description: 'Перезапуск моста: /restart confirm' },
|
|
41
48
|
{ command: 'doctor', description: 'Диагностика: мост, сеть, диск, каталог, сессия' },
|
|
@@ -59,8 +66,7 @@ const COMMANDS_EN = [
|
|
|
59
66
|
{ command: 'model', description: 'Show or switch the model' },
|
|
60
67
|
{ command: 'skills', description: 'Installed skills' },
|
|
61
68
|
{ command: 'soul', description: 'Agent persona' },
|
|
62
|
-
{ command: 'plugins', description: '
|
|
63
|
-
{ command: 'plugin', description: 'Enable or disable a plugin' },
|
|
69
|
+
{ command: 'plugins', description: 'List and manage: add/remove/disable/enable' },
|
|
64
70
|
{ command: 'app', description: 'Desktop app control' },
|
|
65
71
|
{ command: 'restart', description: 'Restart the bridge: /restart confirm' },
|
|
66
72
|
{ command: 'doctor', description: 'Diagnostics: bridge, network, disk, directory, session' },
|
|
@@ -84,8 +90,7 @@ const COMMANDS_ZH = [
|
|
|
84
90
|
{ command: 'model', description: '查看或切换模型' },
|
|
85
91
|
{ command: 'skills', description: '已安装的技能' },
|
|
86
92
|
{ command: 'soul', description: '代理人格设定' },
|
|
87
|
-
{ command: 'plugins', description: '
|
|
88
|
-
{ command: 'plugin', description: '启用或停用插件' },
|
|
93
|
+
{ command: 'plugins', description: '列表与管理:add/remove/disable/enable' },
|
|
89
94
|
{ command: 'app', description: '桌面应用控制' },
|
|
90
95
|
{ command: 'restart', description: '重启桥接:/restart confirm' },
|
|
91
96
|
{ command: 'doctor', description: '诊断:桥接、网络、磁盘、目录、会话' },
|
package/bridge.mjs
CHANGED
|
@@ -26,10 +26,13 @@ import { renderPrices } from './prices.mjs';
|
|
|
26
26
|
import { t, resolveLang } from './i18n.mjs';
|
|
27
27
|
|
|
28
28
|
const HOME = homedir();
|
|
29
|
-
const
|
|
29
|
+
const DSH_HOME = process.env.DSH_HOME || join(HOME, '.dsh');
|
|
30
|
+
// State, config and logs live here. DSH_TG_HOME lets a checkout run with its own
|
|
31
|
+
// directory instead of the default ~/.dsh/tg-bridge.
|
|
32
|
+
const DIR = process.env.DSH_TG_HOME || join(DSH_HOME, 'tg-bridge');
|
|
30
33
|
const STATE = join(DIR, 'state.json');
|
|
31
34
|
const LOG = join(DIR, 'bridge.log');
|
|
32
|
-
const CRED = join(
|
|
35
|
+
const CRED = join(DSH_HOME, '.credentials.yaml');
|
|
33
36
|
const PENDING_DIR = join(DIR, 'pending');
|
|
34
37
|
const DONE_FILE = join(DIR, 'done.json');
|
|
35
38
|
const RESTART_MARKER = join(DIR, 'restart.pending');
|
|
@@ -199,9 +202,31 @@ function readSecret(name) {
|
|
|
199
202
|
const m = yaml.match(re);
|
|
200
203
|
return m ? m[1] : '';
|
|
201
204
|
}
|
|
202
|
-
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The bot token can be stored under three names: the plain one that `dsh-tg
|
|
208
|
+
* init` writes, the suffixed one DSH generates when the secret is added through
|
|
209
|
+
* the app, or an env var. Checking only the suffixed name made a fresh install
|
|
210
|
+
* fail with "token not found" even though init had just written it.
|
|
211
|
+
*/
|
|
212
|
+
function findToken() {
|
|
213
|
+
const env = (process.env.DSH_TELEGRAM_BOT_TOKEN || '').trim();
|
|
214
|
+
if (env) return env;
|
|
215
|
+
for (const name of ['DSH_TELEGRAM_BOT_TOKEN', 'DSH_TELEGRAM_BOT_TOKEN_3278A97027814C501779F5E2']) {
|
|
216
|
+
try {
|
|
217
|
+
const value = readSecret(name);
|
|
218
|
+
if (value) return value;
|
|
219
|
+
} catch {}
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const m = readFileSync(CRED, 'utf8').match(/^\s*(DSH_TELEGRAM_BOT_TOKEN[A-Z0-9_]*):\s*["']?([^"'\s]+)/m);
|
|
223
|
+
if (m) return m[2];
|
|
224
|
+
} catch {}
|
|
225
|
+
return '';
|
|
226
|
+
}
|
|
227
|
+
const token = findToken();
|
|
203
228
|
if (!token) {
|
|
204
|
-
log('FATAL: bot token not found
|
|
229
|
+
log('FATAL: bot token not found — run `node cli.mjs init --token <t> --user <id>` or set DSH_TELEGRAM_BOT_TOKEN');
|
|
205
230
|
process.exit(1);
|
|
206
231
|
}
|
|
207
232
|
const api = (method) => `https://api.telegram.org/bot${token}/${method}`;
|
|
@@ -1233,6 +1258,9 @@ ${CONTROL_COMMAND_HINT}`,
|
|
|
1233
1258
|
}
|
|
1234
1259
|
stop.done = true;
|
|
1235
1260
|
await typing;
|
|
1261
|
+
// Name the cause once, in a short stable code, so a screenshot of the error is
|
|
1262
|
+
// enough to find the fix — and every code has a concrete next step, not "try again".
|
|
1263
|
+
if (result.error) result.error = explainFailure(result.error);
|
|
1236
1264
|
await progress.finish(result);
|
|
1237
1265
|
|
|
1238
1266
|
// Never move a topic onto a different session once it has one. A retry that
|
|
@@ -1265,6 +1293,38 @@ ${CONTROL_COMMAND_HINT}`,
|
|
|
1265
1293
|
await send(chatId, result.text || '(пустой ответ)', threadId);
|
|
1266
1294
|
}
|
|
1267
1295
|
|
|
1296
|
+
/**
|
|
1297
|
+
* Turn a raw CLI/provider error into `CODE` plus one actionable line.
|
|
1298
|
+
*
|
|
1299
|
+
* Raw text like `AUTH 403 insufficient_user_quota` means nothing to a person on
|
|
1300
|
+
* a phone; the code makes the failure greppable and the hint says what to do.
|
|
1301
|
+
*/
|
|
1302
|
+
function explainFailure(text) {
|
|
1303
|
+
const raw = String(text);
|
|
1304
|
+
const cases = [
|
|
1305
|
+
[/insufficient_user_quota|insufficient quota|AUTH\b|\b402\b|\bbalance\b|баланс|billing/i,
|
|
1306
|
+
'INSUFFICIENT_QUOTA', 'баланс провайдера пуст. Пополни счёт или оставь free-модели: /prices и /models.'],
|
|
1307
|
+
[/Conflict: terminated by other getUpdates|\b409\b/i,
|
|
1308
|
+
'TG_CONFLICT', 'того же бота опрашивает второй процесс. Останови лишний: /doctor, затем /restart.'],
|
|
1309
|
+
[/MISSING_CREDENTIAL|no credentials|not authenticated/i,
|
|
1310
|
+
'MISSING_CREDENTIAL', 'в профиле нет ключа провайдера. Добавь его в приложении DSH и повтори.'],
|
|
1311
|
+
[/ENOENT|not recognized|не найден.*dsh|dsh\.cmd/i,
|
|
1312
|
+
'NO_DSH_CLI', 'CLI DeepSeek Harness не найден. Проверь установку приложения (нужен dsh.exe в Programs).'],
|
|
1313
|
+
[/fetch failed|ETIMEDOUT|ECONNREFUSED|ENOTFOUND|socket hang up|network/i,
|
|
1314
|
+
'NETWORK', 'сеть до провайдера недоступна. Повтори через минуту; если повторяется — проверь VPN/DNS.'],
|
|
1315
|
+
[/EACCES|EPERM|access is denied|отказано в доступе|sandbox|permission denied/i,
|
|
1316
|
+
'SANDBOX_DENY', 'DSH запретил доступ к файлу или каталогу. Проверь путь проекта и права на него.'],
|
|
1317
|
+
[/UNKNOWN_MODEL|model not found|no such model/i,
|
|
1318
|
+
'UNKNOWN_MODEL', 'модель из конфига недоступна. Список рабочих: /models.'],
|
|
1319
|
+
[/empty response|EMPTY_RESPONSE|no output|пустой ответ/i,
|
|
1320
|
+
'EMPTY_RESPONSE', 'модель ничего не вернула. Повтори или смени модель: /model.'],
|
|
1321
|
+
];
|
|
1322
|
+
for (const [re, code, hint] of cases) {
|
|
1323
|
+
if (re.test(raw)) return `${raw}\n\n${code}: ${hint}`;
|
|
1324
|
+
}
|
|
1325
|
+
return `${raw}\n\nTURN_FAILED: повтори запрос; если ошибка та же — /doctor и /logs errors.`;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1268
1328
|
// Per-topic turn queues: one turn at a time inside a topic, different topics run
|
|
1269
1329
|
// in parallel. Bounded, because every turn is a full Electron process.
|
|
1270
1330
|
const queues = new Map();
|
package/cli.mjs
CHANGED
|
@@ -1,56 +1,212 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// dsh-tg — CLI entrypoint for the Telegram bridge of DeepSeek Harness.
|
|
3
3
|
//
|
|
4
|
-
// npx dsh-tg init
|
|
5
|
-
// npx dsh-tg setup
|
|
6
|
-
// npx dsh-tg check
|
|
7
|
-
// npx dsh-tg doctor
|
|
8
|
-
// npx dsh-tg menu
|
|
9
|
-
// npx dsh-tg start
|
|
4
|
+
// npx dsh-tg init --token <T> --user <ID> write config + credentials, no editing
|
|
5
|
+
// npx dsh-tg setup install/restart the bridge
|
|
6
|
+
// npx dsh-tg check diagnostics only, changes nothing
|
|
7
|
+
// npx dsh-tg doctor supervisor report (pid, log tail)
|
|
8
|
+
// npx dsh-tg menu register the bot command menu
|
|
9
|
+
// npx dsh-tg start run the bridge in the foreground
|
|
10
|
+
//
|
|
11
|
+
// The goal of `init` is a cold start with ONE command: the person pastes the
|
|
12
|
+
// bot token and their numeric id, and nothing else needs hand-editing.
|
|
10
13
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { copyFileSync, existsSync } from 'node:fs';
|
|
12
|
-
import {
|
|
14
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
17
|
import { fileURLToPath } from 'node:url';
|
|
14
18
|
|
|
15
19
|
const DIR = dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const pkg = JSON.parse(
|
|
20
|
+
const pkg = JSON.parse(readFileSync(join(DIR, 'package.json'), 'utf8'));
|
|
21
|
+
const DSH_HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
22
|
+
// setup.mjs, the watchdog and /doctor all work on this one directory, so the
|
|
23
|
+
// config must be written here and a checkout elsewhere must be mirrored here.
|
|
24
|
+
const RUNTIME = join(DSH_HOME, 'tg-bridge');
|
|
25
|
+
const RUNTIME_FILES = [
|
|
26
|
+
'bridge.mjs', 'control.mjs', 'i18n.mjs', 'bot-menu.mjs', 'prices.mjs',
|
|
27
|
+
'transcribe.py', 'setup.mjs', 'bridge-kick.mjs', 'bridge-restart-idle.mjs',
|
|
28
|
+
'restart-after-send.mjs', 'config.example.json',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Mirror this checkout into the runtime directory.
|
|
33
|
+
*
|
|
34
|
+
* Cloning anywhere except ~/.dsh/tg-bridge used to make `setup` answer "bridge
|
|
35
|
+
* not found", because every helper looks in the runtime directory. Files are
|
|
36
|
+
* copied, config.json is never overwritten: it holds the person's id and token.
|
|
37
|
+
*/
|
|
38
|
+
function syncRuntime() {
|
|
39
|
+
if (resolve(DIR) === resolve(RUNTIME)) return;
|
|
40
|
+
mkdirSync(join(RUNTIME, 'patches'), { recursive: true });
|
|
41
|
+
for (const file of RUNTIME_FILES) {
|
|
42
|
+
if (existsSync(join(DIR, file))) copyFileSync(join(DIR, file), join(RUNTIME, file));
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
for (const patch of readdirSync(join(DIR, 'patches'))) {
|
|
46
|
+
copyFileSync(join(DIR, 'patches', patch), join(RUNTIME, 'patches', patch));
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
console.log(` код: скопирован из ${DIR} в ${RUNTIME}`);
|
|
50
|
+
}
|
|
17
51
|
|
|
18
52
|
const [, , cmd = 'help', ...rest] = process.argv;
|
|
19
53
|
|
|
20
|
-
const run = (file, args) => {
|
|
21
|
-
|
|
54
|
+
const run = (file, args, { sync = false } = {}) => {
|
|
55
|
+
if (sync) syncRuntime();
|
|
56
|
+
const local = join(DIR, file);
|
|
57
|
+
const installed = join(RUNTIME, file);
|
|
58
|
+
// After a sync the installed copy is the freshest; otherwise run this checkout's
|
|
59
|
+
// own file, which reads state from RUNTIME through DSH_HOME.
|
|
60
|
+
const script = sync && resolve(RUNTIME) !== resolve(DIR) && existsSync(installed) ? installed : local;
|
|
61
|
+
const res = spawnSync(process.execPath, [script, ...args], { stdio: 'inherit', cwd: dirname(script) });
|
|
22
62
|
process.exit(res.status ?? 0);
|
|
23
63
|
};
|
|
24
64
|
|
|
65
|
+
/** `--flag value` reader over a flat argv list. */
|
|
66
|
+
function flag(name, fallback = '') {
|
|
67
|
+
const at = rest.indexOf(`--${name}`);
|
|
68
|
+
return at === -1 ? fallback : (rest[at + 1] ?? '');
|
|
69
|
+
}
|
|
70
|
+
|
|
25
71
|
const help = `
|
|
26
72
|
dsh-tg ${pkg.version} — Telegram bridge for DeepSeek Harness
|
|
27
73
|
|
|
28
|
-
dsh-tg init
|
|
29
|
-
dsh-tg setup
|
|
30
|
-
dsh-tg check
|
|
31
|
-
dsh-tg doctor
|
|
32
|
-
dsh-tg menu
|
|
33
|
-
dsh-tg start
|
|
34
|
-
dsh-tg version
|
|
35
|
-
dsh-tg help
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
dsh-tg init --token <bot-token> --user <your-id> настроить всё одной командой
|
|
75
|
+
dsh-tg setup установить и перезапустить мост (setup.log хранит шаги)
|
|
76
|
+
dsh-tg check диагностика, ничего не меняет
|
|
77
|
+
dsh-tg doctor отчёт супервизора: pid, хвост лога
|
|
78
|
+
dsh-tg menu зарегистрировать /команды в меню Telegram
|
|
79
|
+
dsh-tg start запустить мост на переднем плане
|
|
80
|
+
dsh-tg version версия
|
|
81
|
+
dsh-tg help этот текст
|
|
82
|
+
|
|
83
|
+
init:
|
|
84
|
+
--token <t> токен бота от @BotFather (обязательно)
|
|
85
|
+
--user <id> ваш числовой id от @userinfobot (обязательно)
|
|
86
|
+
--cwd <path> рабочий каталог (по умолчанию ~/Projects)
|
|
87
|
+
--voice включить голос (нужен python + faster-whisper)
|
|
88
|
+
--dry-run показать, что будет записано, ничего не менять
|
|
89
|
+
|
|
90
|
+
Docs: docs/STATE.md, docs/ROADMAP.md, docs/UX-ANALYSIS.md.
|
|
39
91
|
`;
|
|
40
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Write `DSH_TELEGRAM_BOT_TOKEN: "..."` into the credentials store without
|
|
95
|
+
* touching other secrets: DSH keeps every credential in this one file, so a
|
|
96
|
+
* careless rewrite would log the person out of everything else.
|
|
97
|
+
*
|
|
98
|
+
* Line-based on purpose: regex over the whole file either misses a `refs:`
|
|
99
|
+
* that sits mid-document or appends a second `refs:` key, and a duplicate key
|
|
100
|
+
* silently hides every credential above it.
|
|
101
|
+
*/
|
|
102
|
+
function writeCredential(token, dryRun) {
|
|
103
|
+
const file = join(DSH_HOME, '.credentials.yaml');
|
|
104
|
+
const line = ` DSH_TELEGRAM_BOT_TOKEN: "${token}"`;
|
|
105
|
+
let lines;
|
|
106
|
+
try {
|
|
107
|
+
lines = readFileSync(file, 'utf8').split(/\r?\n/);
|
|
108
|
+
} catch {
|
|
109
|
+
lines = [];
|
|
110
|
+
}
|
|
111
|
+
const existing = lines.findIndex((l) => /^\s*DSH_TELEGRAM_BOT_TOKEN:/.test(l));
|
|
112
|
+
if (existing !== -1) {
|
|
113
|
+
if (lines[existing] === line) {
|
|
114
|
+
console.log(' credentials: токен уже записан');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
lines[existing] = line;
|
|
118
|
+
console.log(` credentials: токен ${dryRun ? 'обновил бы' : 'обновлён'} в ${file}`);
|
|
119
|
+
} else {
|
|
120
|
+
const refs = lines.findIndex((l) => /^refs:\s*$/.test(l));
|
|
121
|
+
if (refs === -1) {
|
|
122
|
+
const tail = lines.join('\n').trimEnd();
|
|
123
|
+
lines = [...(tail ? [tail, ''] : []), 'refs:', line];
|
|
124
|
+
} else {
|
|
125
|
+
lines.splice(refs + 1, 0, line);
|
|
126
|
+
}
|
|
127
|
+
console.log(` credentials: токен ${dryRun ? 'записал бы' : 'записан'} в ${file}`);
|
|
128
|
+
}
|
|
129
|
+
if (dryRun) return;
|
|
130
|
+
mkdirSync(DSH_HOME, { recursive: true });
|
|
131
|
+
writeFileSync(file, `${lines.join('\n')}\n`, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Build config.json for this person: their id, their cwd, voice only if asked. */
|
|
135
|
+
function writeConfig({ userId, cwd, voice }, dryRun) {
|
|
136
|
+
const target = join(RUNTIME, 'config.json');
|
|
137
|
+
const example = JSON.parse(readFileSync(join(DIR, 'config.example.json'), 'utf8'));
|
|
138
|
+
if (existsSync(target)) {
|
|
139
|
+
console.log(' config.json: уже существует — не перезаписан');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const config = {
|
|
143
|
+
...example,
|
|
144
|
+
allowedUserIds: [Number(userId)],
|
|
145
|
+
cwd,
|
|
146
|
+
voice: { ...example.voice, enabled: Boolean(voice) },
|
|
147
|
+
};
|
|
148
|
+
delete config._comment;
|
|
149
|
+
if (dryRun) {
|
|
150
|
+
console.log(` config.json: создал бы ${target} (id ${userId}, cwd ${cwd}, voice ${voice ? 'on' : 'off'})`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
mkdirSync(RUNTIME, { recursive: true });
|
|
154
|
+
writeFileSync(target, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
155
|
+
console.log(` config.json: создан (id ${userId}, voice ${voice ? 'вкл' : 'выкл'})`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A missing headless profile cannot be invented: DSH ships its bundles. */
|
|
159
|
+
function checkProfile() {
|
|
160
|
+
const profile = join(DSH_HOME, 'profiles', 'headless');
|
|
161
|
+
if (existsSync(profile)) {
|
|
162
|
+
console.log(' профиль headless: на месте');
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
console.log(' профиль headless: НЕ НАЙДЕН');
|
|
166
|
+
console.log(' создай его один раз в приложении DSH: Settings → Profiles → headless,');
|
|
167
|
+
console.log(' либо скопируй шаблон из сборки: dsh --profile headless --dump-default-config');
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
41
171
|
switch (cmd) {
|
|
42
172
|
case 'init': {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
173
|
+
const token = flag('token');
|
|
174
|
+
const userId = flag('user');
|
|
175
|
+
const dryRun = rest.includes('--dry-run');
|
|
176
|
+
const voice = rest.includes('--voice');
|
|
177
|
+
const cwd = flag('cwd', join(homedir(), 'Projects'));
|
|
178
|
+
|
|
179
|
+
if (!token || !userId) {
|
|
180
|
+
console.error('init требует --token и --user\n');
|
|
181
|
+
console.log(help);
|
|
182
|
+
process.exit(2);
|
|
183
|
+
}
|
|
184
|
+
if (!/^\d+$/.test(userId)) {
|
|
185
|
+
console.error(`--user должен быть числом (получено "${userId}"). Узнать: @userinfobot`);
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
if (!/^\d{6,}:[A-Za-z0-9_-]{20,}$/.test(token)) {
|
|
189
|
+
console.error('--token не похож на токен бота (ожидается 123456789:AA…)');
|
|
190
|
+
process.exit(2);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(`dsh-tg init${dryRun ? ' (dry run)' : ''}\n`);
|
|
194
|
+
writeCredential(token, dryRun);
|
|
195
|
+
writeConfig({ userId, cwd, voice }, dryRun);
|
|
196
|
+
checkProfile();
|
|
197
|
+
console.log('\nДальше:');
|
|
198
|
+
console.log(' npx dsh-tg-bridge@latest check # проверить');
|
|
199
|
+
console.log(' npx dsh-tg-bridge@latest setup # поднять мост');
|
|
200
|
+
console.log(' в Telegram: /doctor');
|
|
47
201
|
break;
|
|
48
202
|
}
|
|
49
|
-
case 'setup': run('setup.mjs', rest); break;
|
|
50
|
-
|
|
203
|
+
case 'setup': run('setup.mjs', rest, { sync: true }); break;
|
|
204
|
+
// check must see the installed copy; if the runtime directory was never
|
|
205
|
+
// populated (custom clone path), mirror first so the report is not a lie.
|
|
206
|
+
case 'check': run('setup.mjs', ['--check', ...rest], { sync: !existsSync(join(RUNTIME, 'bridge.mjs')) }); break;
|
|
51
207
|
case 'doctor': run('bridge-kick.mjs', rest); break;
|
|
52
208
|
case 'menu': run('bot-menu.mjs', rest); break;
|
|
53
|
-
case 'start': run('bridge.mjs', rest); break;
|
|
209
|
+
case 'start': run('bridge.mjs', rest, { sync: true }); break;
|
|
54
210
|
case 'version': console.log(pkg.version); break;
|
|
55
211
|
case 'help': case '--help': case '-h': console.log(help); break;
|
|
56
212
|
default:
|
package/control.mjs
CHANGED
|
@@ -354,7 +354,8 @@ export const CONTROL_COMMAND_HINT =
|
|
|
354
354
|
'Работа: /cd · /new · /stop · /send · /seed\n' +
|
|
355
355
|
'Сессии: /status · /sessions · /resume\n' +
|
|
356
356
|
'Модели: /models · /model · /prices\n' +
|
|
357
|
-
'Расширения: /skills · /
|
|
357
|
+
'Расширения: /skills · /soul\n' +
|
|
358
|
+
'Плагины: /plugins [headless] — список; /plugins add|remove|disable|enable <что> — действия\n' +
|
|
358
359
|
'Диагностика: /doctor · /logs\n' +
|
|
359
360
|
'Язык: /lang en|ru|zh\n' +
|
|
360
361
|
'Служебное: /app · /restart · /help';
|
|
@@ -363,7 +364,21 @@ export async function handleControl(ctx) {
|
|
|
363
364
|
const { command, arg, send, reply, log, profile, state, key, config } = ctx;
|
|
364
365
|
const say = (text) => reply(text);
|
|
365
366
|
|
|
366
|
-
|
|
367
|
+
// One command for one job: `/plugins` is both the list and the way to change
|
|
368
|
+
// things. Two near-identical names (`/plugin` / `/plugins`) invited the exact
|
|
369
|
+
// mistake of typing the plural with an action. The singular form stays as an
|
|
370
|
+
// alias so old instructions and muscle memory keep working.
|
|
371
|
+
let effective = command;
|
|
372
|
+
if (effective === '/plugins') {
|
|
373
|
+
const [maybeAction] = String(arg || '').trim().split(/\s+/);
|
|
374
|
+
// `/plugins add|remove|disable|enable …` is the same work as `/plugin …`;
|
|
375
|
+
// the case bodies read `arg`, which already holds the action and its value.
|
|
376
|
+
if (['add', 'remove', 'disable', 'enable'].includes((maybeAction || '').toLowerCase())) {
|
|
377
|
+
effective = '/plugin';
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
switch (effective) {
|
|
367
382
|
case '/status': {
|
|
368
383
|
const head = modelOf('headless');
|
|
369
384
|
const desk = modelOf('desktop');
|
|
@@ -452,6 +467,10 @@ export async function handleControl(ctx) {
|
|
|
452
467
|
...lines,
|
|
453
468
|
orphan.length ? `\nУстановлены, но не в bundles:\n${fmt(orphan)}` : '',
|
|
454
469
|
`\nОтключены строки: ${disabled.length ? disabled.join(', ') : 'нет'}`,
|
|
470
|
+
// The actions live behind the same command, so the list must say how
|
|
471
|
+
// to reach them — otherwise people keep hunting for `/plugin`.
|
|
472
|
+
`\nИзменить: /plugins add <пакет> · remove <имя> · disable <id> · enable <id>`,
|
|
473
|
+
'Профиль: добавь headless или desktop в конце.',
|
|
455
474
|
]
|
|
456
475
|
.filter(Boolean)
|
|
457
476
|
.join('\n'),
|
|
@@ -464,7 +483,7 @@ export async function handleControl(ctx) {
|
|
|
464
483
|
const target = (rest[0] === 'headless' || rest[0] === 'desktop') && rest.length > 1 ? rest.shift() : 'desktop';
|
|
465
484
|
const value = rest.join(' ').trim();
|
|
466
485
|
if (!action || !value) {
|
|
467
|
-
say('Использование:\n/
|
|
486
|
+
say('Использование:\n/plugins add <npm|github|file:>\n/plugins remove <name>\n/plugins disable <id>\n/plugins enable <id>\nПрофиль: допиши headless или desktop.\n\n/plugin — то же самое, старая форма.');
|
|
468
487
|
return true;
|
|
469
488
|
}
|
|
470
489
|
if (action === 'disable' || action === 'enable') {
|
package/package.json
CHANGED
|
@@ -1,50 +1,48 @@
|
|
|
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.2.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": ">=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
|
+
"files": [
|
|
32
|
+
"bridge.mjs",
|
|
33
|
+
"control.mjs",
|
|
34
|
+
"i18n.mjs",
|
|
35
|
+
"prices.mjs",
|
|
36
|
+
"setup.mjs",
|
|
37
|
+
"bot-menu.mjs",
|
|
38
|
+
"bridge-kick.mjs",
|
|
39
|
+
"bridge-restart-idle.mjs",
|
|
40
|
+
"restart-after-send.mjs",
|
|
41
|
+
"transcribe.py",
|
|
42
|
+
"cli.mjs",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"config.example.json",
|
|
46
|
+
"patches/"
|
|
47
|
+
]
|
|
48
|
+
}
|
package/setup.mjs
CHANGED
|
@@ -124,9 +124,12 @@ function pidFilePid() {
|
|
|
124
124
|
function bridgePids() {
|
|
125
125
|
let pids = [];
|
|
126
126
|
try {
|
|
127
|
+
// Match the full script path, not just "bridge.mjs": a second install with
|
|
128
|
+
// its own DSH_HOME and token is a different bot and must not be killed.
|
|
129
|
+
const filter = `$_.CommandLine -and $_.CommandLine.Contains('${BRIDGE}')`;
|
|
127
130
|
const r = run('powershell', [
|
|
128
131
|
'-NoProfile', '-NonInteractive', '-Command',
|
|
129
|
-
|
|
132
|
+
`Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { ${filter} } | Select-Object -ExpandProperty ProcessId`,
|
|
130
133
|
]);
|
|
131
134
|
pids = (r.stdout || '')
|
|
132
135
|
.split(/\r?\n/)
|
|
@@ -141,9 +144,12 @@ function bridgePids() {
|
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
function token() {
|
|
147
|
+
const env = (process.env.DSH_TELEGRAM_BOT_TOKEN || '').trim();
|
|
148
|
+
if (env) return env;
|
|
144
149
|
try {
|
|
145
150
|
const yaml = readFileSync(CRED, 'utf8');
|
|
146
|
-
|
|
151
|
+
// `dsh-tg init` writes the plain name; the DSH app adds a random suffix.
|
|
152
|
+
const m = /^\s*(DSH_TELEGRAM_BOT_TOKEN[A-Z0-9_]*):\s*["']?([^"'\s]+)/m.exec(yaml);
|
|
147
153
|
return m ? m[2] : '';
|
|
148
154
|
} catch {
|
|
149
155
|
return '';
|