dsh-tg-bridge 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
- - **Кнопки «Стоп»/«Повторить», approvals** — в планах v1.1 (см. `docs/ROADMAP.md`).
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
- **A. npm (рекомендуется)**
42
+ **Windows (PowerShell)**
43
43
 
44
- ```bash
45
- npx dsh-tg-bridge@latest init # создать config.json из шаблона
46
- npx dsh-tg-bridge@latest check # диагностика, ничего не меняет
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
- **B. Одной строкой (curl / PowerShell)**
49
+ **macOS / Linux**
51
50
 
52
51
  ```bash
53
- # macOS / Linux
54
- curl -fsSL https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.sh | bash
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
- # Windows
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
- **C. Вручную**
63
+ Что делает `init`:
63
64
 
64
- ```bash
65
- git clone https://github.com/aleksvega/dsh-tg-bridge.git ~/.dsh/tg-bridge
66
- cd ~/.dsh/tg-bridge
67
- cp config.example.json config.json
68
- node cli.mjs check
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
- Файл `~/.dsh/.credentials.yaml`, секция `refs`:
71
+ Флаги `init`: `--cwd <путь>` (по умолчанию `~/Projects`), `--voice` (включить
72
+ расшифровку голоса), `--dry-run` (показать, что будет записано, и ничего не менять).
75
73
 
76
- ```yaml
77
- refs:
78
- DSH_TELEGRAM_BOT_TOKEN: "123456789:AA..."
79
- ```
74
+ Id берётся у [@userinfobot](https://t.me/userinfobot), токен — у
75
+ [@BotFather](https://t.me/BotFather).
80
76
 
81
- Имя переменной может быть любым с префиксом `DSH_TELEGRAM_BOT_TOKEN_` —
82
- мост найдёт его сам.
77
+ **Автозапуск** установщик ставит сам: `launchd` на macOS, `systemd --user` на
78
+ Linux. На Windows `setup` регистрирует задачу `DSH-TG-Bridge` и минутный сторож
79
+ `DSH-Bridge-Kick`, который поднимает мост после падения.
83
80
 
84
- **3. Создать конфиг**
81
+ **Вручную** (если нужен полный контроль):
85
82
 
86
83
  ```bash
87
- cp config.example.json config.json
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
- Заполнить `allowedUserIds` — свой числовой id (узнать у
91
- [@userinfobot](https://t.me/userinfobot)). Без него мост не отвечает никому:
92
- это защита от чужих сообщений.
91
+ Если мост уже стоит, `setup` перезапускает его на новом коде и переустанавливает
92
+ меню команд.
93
93
 
94
- **4. Применить и проверить**
94
+ ## Если что-то не так
95
95
 
96
- ```bash
97
- node setup.mjs # остановит старый мост, поднимет новый, поставит меню
98
- node setup.mjs --check # только отчёт, ничего не меняет
99
- ```
96
+ Ответ на ошибку заканчивается кодом — по нему видно, что чинить:
100
97
 
101
- **5. Автозапуск (Windows)**
102
-
103
- ```powershell
104
- Register-ScheduledTask -TaskName "DSH-TG-Bridge" `
105
- -Action (New-ScheduledTaskAction -Execute "node" -Argument "bridge.mjs" -WorkingDirectory "$HOME\.dsh\tg-bridge") `
106
- -Trigger (New-ScheduledTaskTrigger -AtLogOn) -Force
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,16 @@ import { homedir } from 'node:os';
14
14
  const args = new Set(process.argv.slice(2));
15
15
 
16
16
  const token = (() => {
17
- const yaml = readFileSync(join(homedir(), '.dsh', '.credentials.yaml'), 'utf8');
18
- const m = /^\s*(DSH_TELEGRAM_BOT_TOKEN_[A-Z0-9_]+):\s*["']?([^"'\s]+)/m.exec(yaml);
19
- return m ? m[2] : '';
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
+ const m = /^\s*(DSH_TELEGRAM_BOT_TOKEN[A-Z0-9_]*):\s*["']?([^"'\s]+)/m.exec(yaml);
23
+ return m ? m[2] : '';
24
+ } catch {
25
+ return '';
26
+ }
20
27
  })();
21
28
 
22
29
  /** The real, implemented surface of dsh-tg-bridge — nothing aspirational. */
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 DIR = join(HOME, '.dsh', 'tg-bridge');
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(HOME, '.dsh', '.credentials.yaml');
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
- const token = readSecret('DSH_TELEGRAM_BOT_TOKEN_3278A97027814C501779F5E2');
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 in .credentials.yaml');
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 copy config.example.json -> config.json
5
- // npx dsh-tg setup install/restart the bridge (writes setup.log)
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
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 { dirname, join } from 'node:path';
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((await import('node:fs')).readFileSync(join(DIR, 'package.json'), 'utf8'));
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
- const res = spawnSync(process.execPath, [join(DIR, file), ...args], { stdio: 'inherit', cwd: DIR });
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 create config.json from the template
29
- dsh-tg setup install and restart the bridge (setup.log has every step)
30
- dsh-tg check run diagnostics without changing anything
31
- dsh-tg doctor supervisor report: pid, log tail, task state
32
- dsh-tg menu register /commands in the Telegram menu
33
- dsh-tg start run the bridge in the foreground
34
- dsh-tg version print the version
35
- dsh-tg help this text
36
-
37
- Docs: docs/STATE.md (current state), docs/ROADMAP.md (next steps),
38
- docs/UX-ANALYSIS.md (what is still missing vs a full UI).
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 target = join(DIR, 'config.json');
44
- if (existsSync(target)) { console.log('config.json already exists — left untouched'); break; }
45
- copyFileSync(join(DIR, 'config.example.json'), target);
46
- console.log(`created ${target}\nEdit allowedUserIds and cwd before the first start.`);
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
- case 'check': run('setup.mjs', ['--check', ...rest]); break;
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/package.json CHANGED
@@ -1,50 +1,48 @@
1
- {
2
- "name": "dsh-tg-bridge",
3
- "version": "1.1.2",
4
- "description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
5
- "type": "module",
6
- "main": "bridge.mjs",
7
- "bin": {
8
- "dsh-tg": "./cli.mjs"
9
- },
10
- "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
- }
1
+ {
2
+ "name": "dsh-tg-bridge",
3
+ "version": "1.2.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
+ "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
- "Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | Where-Object { $_.CommandLine -match 'bridge\\.mjs' } | Select-Object -ExpandProperty ProcessId",
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
- const m = /^\s*(DSH_TELEGRAM_BOT_TOKEN_[A-Z0-9_]+):\s*["']?([^"'\s]+)/m.exec(yaml);
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 '';