dsh-tg-bridge 1.3.4 → 1.4.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 +37 -18
- package/bridge-kick.mjs +51 -16
- package/cordis.patch.yml +6 -0
- package/package.json +23 -3
- package/plugin-bridge.mjs +83 -0
- package/setup.mjs +21 -2
package/README.md
CHANGED
|
@@ -23,9 +23,28 @@ blocks.
|
|
|
23
23
|
## Requirements
|
|
24
24
|
|
|
25
25
|
- Node.js ≥ 20
|
|
26
|
+
- DeepSeek Harness `0.2.1-rc.2` or newer in the `0.2.x` line (`>=0.2.1-rc.2 <0.3.0`)
|
|
26
27
|
- DeepSeek Harness installed (`~/.dsh/profiles/headless`)
|
|
27
28
|
- A Telegram bot token from [@BotFather](https://t.me/BotFather)
|
|
28
29
|
|
|
30
|
+
The supported harness range is declared twice: `engines.dsh` in `package.json`
|
|
31
|
+
(what npm and the market read) and `dsh.compatibility.dsh` (what DSH itself
|
|
32
|
+
reads before mounting the bundle). A future `0.3.0` needs an explicit bump:
|
|
33
|
+
the bridge relies on the `0.2.x` CLI flags and profile layout.
|
|
34
|
+
|
|
35
|
+
## Ecosystem
|
|
36
|
+
|
|
37
|
+
The Chinese DSH community maintains the largest plugin catalogs and the
|
|
38
|
+
in-app markets that read them; this bridge follows their packaging rules so it
|
|
39
|
+
can be listed next to them:
|
|
40
|
+
|
|
41
|
+
- [awesome-dsh-plugin](https://awesome-dsh-plugin.com) — bilingual (English /
|
|
42
|
+
中文) plugin list and the `dsh-plugin-catalog` npm package
|
|
43
|
+
- [dsh-market](https://github.com/dsh-market/dsh-market) — the Chinese-built
|
|
44
|
+
in-app market with one-click install and themes
|
|
45
|
+
- [dsh-find-plugin](https://github.com/awesome-dsh-plugin/dsh-find-plugin) —
|
|
46
|
+
finds a plugin from chat instead of browsing a catalog
|
|
47
|
+
|
|
29
48
|
## Languages
|
|
30
49
|
|
|
31
50
|
- Menus (`/` in chat): three sets, `ru` by default, plus `en` and `zh`.
|
|
@@ -39,26 +58,26 @@ blocks.
|
|
|
39
58
|
|
|
40
59
|
One command: your bot token and your numeric id. Nothing to edit by hand.
|
|
41
60
|
|
|
42
|
-
**
|
|
61
|
+
**Any platform with Node.js ≥ 20**
|
|
43
62
|
|
|
44
|
-
```
|
|
63
|
+
```bash
|
|
45
64
|
npx dsh-tg-bridge@latest init --token 123456789:AA... --user 123456789
|
|
46
65
|
npx dsh-tg-bridge@latest setup
|
|
47
66
|
```
|
|
48
67
|
|
|
49
|
-
|
|
68
|
+
`init` writes the token, your id and the working directory; `setup` registers
|
|
69
|
+
autostart and starts the bridge.
|
|
70
|
+
|
|
71
|
+
**As a DSH plugin** (the package carries a `dsh.bundle`; the catalog entry is
|
|
72
|
+
pending review):
|
|
50
73
|
|
|
51
74
|
```bash
|
|
52
|
-
|
|
53
|
-
bash -c "$(curl -fsSL https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.sh)"
|
|
75
|
+
dsh plugin --profile desktop add dsh-tg-bridge
|
|
54
76
|
```
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
$env:DSH_TG_TOKEN='123456789:AA...'; $env:DSH_TG_USER='123456789'
|
|
60
|
-
irm https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.ps1 | iex
|
|
61
|
-
```
|
|
78
|
+
The plugin starts one bridge when DSH boots and stops it on shutdown; if a
|
|
79
|
+
bridge is already running (for example from the scheduled task), it leaves it
|
|
80
|
+
alone. Run `npx dsh-tg-bridge@latest init ...` first so a config exists.
|
|
62
81
|
|
|
63
82
|
What `init` does:
|
|
64
83
|
|
|
@@ -90,17 +109,17 @@ in Russian, English and Chinese at once. The full settings table, permissions, c
|
|
|
90
109
|
common errors are in [docs/TELEGRAM-SETUP.md](docs/TELEGRAM-SETUP.md).
|
|
91
110
|
|
|
92
111
|
**Autostart** is installed by the setup: `launchd` on macOS, `systemd --user` on
|
|
93
|
-
Linux. On Windows `setup` registers the `DSH-TG-Bridge` task
|
|
94
|
-
|
|
112
|
+
Linux. On Windows `setup` registers the `DSH-TG-Bridge` task, which runs the
|
|
113
|
+
guarded supervisor (the same launcher on logon and every five minutes) so a
|
|
114
|
+
crashed bridge comes back without a second poller.
|
|
95
115
|
|
|
96
116
|
**Manual install** (if you want full control):
|
|
97
117
|
|
|
98
118
|
```bash
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
node cli.mjs setup # stop the old bridge and start the new one
|
|
119
|
+
npm install -g dsh-tg-bridge
|
|
120
|
+
dsh-tg init --token 123456789:AA... --user 123456789
|
|
121
|
+
dsh-tg check # report only, changes nothing
|
|
122
|
+
dsh-tg setup # stop the old bridge and start the new one
|
|
104
123
|
```
|
|
105
124
|
|
|
106
125
|
If the bridge is already installed, `setup` restarts it on the new code and reinstalls
|
package/bridge-kick.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Exit codes: 0 = nothing to do or started, 2 = start failed.
|
|
10
10
|
|
|
11
|
-
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
12
12
|
import { appendFileSync, existsSync, readFileSync, openSync, closeSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { join } from 'node:path';
|
|
@@ -21,25 +21,48 @@ const KICK_LOG = join(DIR, 'kick.log');
|
|
|
21
21
|
|
|
22
22
|
// Shared with setup.mjs: a restart holds this lock, so a tick that lands in the
|
|
23
23
|
// middle of one must not start a second poller.
|
|
24
|
+
//
|
|
25
|
+
// A live holder used to disable supervision forever: the old code started the
|
|
26
|
+
// bridge with spawnSync(), which blocks until the child EXITS — for a long-lived
|
|
27
|
+
// bridge the tick never returned, kept this lock for hours, and every later tick
|
|
28
|
+
// logged "restart in progress" and skipped (2026-10-24: supervisor dead from
|
|
29
|
+
// 18:57 while the bridge lived). Two fixes: spawn() + unref() below, and
|
|
30
|
+
// LOCK_MAX_AGE_MS here so even a genuinely wedged holder cannot wedge the guard.
|
|
24
31
|
const LOCK = join(DIR, 'restart.lock');
|
|
32
|
+
const LOCK_MAX_AGE_MS = 180_000;
|
|
33
|
+
|
|
25
34
|
function acquireLock(owner) {
|
|
26
35
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
27
36
|
try {
|
|
28
37
|
const fd = openSync(LOCK, 'wx');
|
|
29
38
|
writeFileSync(fd, JSON.stringify({ pid: process.pid, owner, ts: Date.now() }));
|
|
30
39
|
closeSync(fd);
|
|
31
|
-
|
|
40
|
+
// Remove only our own lock: after a stale-lock takeover the file belongs
|
|
41
|
+
// to the new holder, and a late exit must not steal it.
|
|
42
|
+
const release = () => {
|
|
43
|
+
try {
|
|
44
|
+
if (Number(JSON.parse(readFileSync(LOCK, 'utf8')).pid) === process.pid) unlinkSync(LOCK);
|
|
45
|
+
} catch {}
|
|
46
|
+
};
|
|
32
47
|
process.on('exit', release);
|
|
33
48
|
return release;
|
|
34
49
|
} catch (error) {
|
|
35
50
|
if (error.code !== 'EEXIST') return null;
|
|
36
51
|
let held = 0;
|
|
37
|
-
|
|
52
|
+
let heldTs = 0;
|
|
53
|
+
try {
|
|
54
|
+
const info = JSON.parse(readFileSync(LOCK, 'utf8'));
|
|
55
|
+
held = Number(info.pid) || 0;
|
|
56
|
+
heldTs = Number(info.ts) || 0;
|
|
57
|
+
} catch {}
|
|
58
|
+
const age = heldTs ? Date.now() - heldTs : Number.POSITIVE_INFINITY;
|
|
38
59
|
let alive = false;
|
|
39
60
|
if (held) {
|
|
40
61
|
try { process.kill(held, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
|
|
41
62
|
}
|
|
42
|
-
|
|
63
|
+
// Age wins over liveness: a holder that has not finished a restart in
|
|
64
|
+
// LOCK_MAX_AGE_MS is wedged, not busy.
|
|
65
|
+
if (alive && age < LOCK_MAX_AGE_MS) return null;
|
|
43
66
|
try { unlinkSync(LOCK); } catch {}
|
|
44
67
|
}
|
|
45
68
|
}
|
|
@@ -93,25 +116,37 @@ function main() {
|
|
|
93
116
|
const restartPending = existsSync(marker);
|
|
94
117
|
|
|
95
118
|
const filePid = pidFromFile();
|
|
119
|
+
// Cheap path first: a live pid file is authoritative for a healthy bridge, so
|
|
120
|
+
// the minute tick costs one kill(0) instead of a PowerShell process scan.
|
|
121
|
+
if (filePid && alive(filePid)) {
|
|
122
|
+
if (restartPending) log(`marker present but pid ${filePid} alive`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
// Only when the pid file is stale or missing: scan for a bridge that started
|
|
126
|
+
// without writing one.
|
|
96
127
|
const live = bridgePids();
|
|
97
|
-
if (live.length > 0
|
|
98
|
-
|
|
128
|
+
if (live.length > 0) {
|
|
129
|
+
log(`pid file stale (${filePid || 'missing'}), bridge ${live.join(',')} alive — not starting`);
|
|
99
130
|
return 0;
|
|
100
131
|
}
|
|
101
132
|
|
|
102
133
|
log(restartPending ? 'marker present and no bridge: starting' : 'no bridge: starting');
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
// spawn(), NOT spawnSync(): spawnSync waits for the child to exit, and this
|
|
135
|
+
// child is meant to outlive the tick.
|
|
136
|
+
let child;
|
|
137
|
+
try {
|
|
138
|
+
child = spawn(process.execPath, ['--dns-result-order=ipv4first', BRIDGE], {
|
|
139
|
+
cwd: DIR,
|
|
140
|
+
detached: true,
|
|
141
|
+
stdio: 'ignore',
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
});
|
|
144
|
+
} catch (error) {
|
|
145
|
+
log(`start failed: ${error.message}`);
|
|
111
146
|
return 2;
|
|
112
147
|
}
|
|
113
|
-
|
|
114
|
-
log(
|
|
148
|
+
child.unref();
|
|
149
|
+
log(`start issued pid=${child.pid ?? '?'}`);
|
|
115
150
|
return 0;
|
|
116
151
|
}
|
|
117
152
|
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# DSH bundle: load the Telegram bridge with the harness. The plugin starts one
|
|
2
|
+
# bridge process (skipped when a bridge is already running) and stops it on
|
|
3
|
+
# shutdown. Install with: dsh plugin --profile desktop add dsh-tg-bridge
|
|
4
|
+
- insert:
|
|
5
|
+
- id: tg-bridge
|
|
6
|
+
name: dsh-tg-bridge
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tg-bridge",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "bridge.mjs",
|
|
6
|
+
"main": "plugin-bridge.mjs",
|
|
7
7
|
"bin": {
|
|
8
8
|
"dsh-tg": "./cli.mjs"
|
|
9
9
|
},
|
|
@@ -16,7 +16,25 @@
|
|
|
16
16
|
"test": "node --test"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=20"
|
|
19
|
+
"node": ">=20",
|
|
20
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
21
|
+
},
|
|
22
|
+
"dsh": {
|
|
23
|
+
"manifestVersion": 1,
|
|
24
|
+
"bundle": {
|
|
25
|
+
"patch": "./cordis.patch.yml"
|
|
26
|
+
},
|
|
27
|
+
"compatibility": {
|
|
28
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0",
|
|
29
|
+
"profiles": [
|
|
30
|
+
"desktop",
|
|
31
|
+
"web"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/aleksvega/dsh-tg-bridge.git"
|
|
20
38
|
},
|
|
21
39
|
"keywords": [
|
|
22
40
|
"deepseek-harness",
|
|
@@ -31,6 +49,8 @@
|
|
|
31
49
|
"dependencies": {},
|
|
32
50
|
"files": [
|
|
33
51
|
"bridge.mjs",
|
|
52
|
+
"plugin-bridge.mjs",
|
|
53
|
+
"cordis.patch.yml",
|
|
34
54
|
"control.mjs",
|
|
35
55
|
"i18n.mjs",
|
|
36
56
|
"prices.mjs",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// DSH plugin entry: run the Telegram bridge alongside the harness.
|
|
2
|
+
//
|
|
3
|
+
// The bridge is a separate long-polling process, not a tool inside the agent,
|
|
4
|
+
// so the plugin starts it once when the host boots and stops it on shutdown.
|
|
5
|
+
// It never starts a second copy: a live `bridge.pid` wins. That keeps the
|
|
6
|
+
// scheduled-task install and this plugin from polling the same bot twice.
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { closeSync, existsSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
export const name = 'tg-bridge';
|
|
14
|
+
export const inject = [];
|
|
15
|
+
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const DSH_HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
18
|
+
const DIR = join(DSH_HOME, 'tg-bridge');
|
|
19
|
+
|
|
20
|
+
function alive(pid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return error.code === 'EPERM';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runningPid() {
|
|
30
|
+
try {
|
|
31
|
+
const pid = Number(readFileSync(join(DIR, 'bridge.pid'), 'utf8').trim());
|
|
32
|
+
return pid > 0 && alive(pid) ? pid : 0;
|
|
33
|
+
} catch {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function apply(ctx, config = {}) {
|
|
39
|
+
if (config.autoStart === false) return;
|
|
40
|
+
// Nothing to start until `dsh-tg init` wrote a token and a working directory.
|
|
41
|
+
if (!existsSync(join(DIR, 'config.json'))) return;
|
|
42
|
+
if (runningPid()) return;
|
|
43
|
+
|
|
44
|
+
// Two profiles loading at once must not both start a poller.
|
|
45
|
+
const lock = join(DIR, 'plugin-start.lock');
|
|
46
|
+
try {
|
|
47
|
+
const fd = openSync(lock, 'wx');
|
|
48
|
+
writeFileSync(fd, String(process.pid));
|
|
49
|
+
closeSync(fd);
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let child = null;
|
|
55
|
+
try {
|
|
56
|
+
child = spawn(process.execPath, [join(HERE, 'bridge.mjs')], {
|
|
57
|
+
cwd: DIR,
|
|
58
|
+
detached: true,
|
|
59
|
+
stdio: 'ignore',
|
|
60
|
+
windowsHide: true,
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
// Inside DSH desktop, process.execPath is Electron; this makes it run
|
|
64
|
+
// as Node without opening a window.
|
|
65
|
+
...(process.versions.electron ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
child.unref();
|
|
69
|
+
} catch {
|
|
70
|
+
child = null;
|
|
71
|
+
} finally {
|
|
72
|
+
try {
|
|
73
|
+
unlinkSync(lock);
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!child?.pid || typeof ctx.on !== 'function') return;
|
|
78
|
+
ctx.on('dispose', () => {
|
|
79
|
+
try {
|
|
80
|
+
if (alive(child.pid)) process.kill(child.pid);
|
|
81
|
+
} catch {}
|
|
82
|
+
});
|
|
83
|
+
}
|
package/setup.mjs
CHANGED
|
@@ -286,11 +286,30 @@ async function main() {
|
|
|
286
286
|
if (recentConflict) fail('a stale poller still owns getUpdates (409 in the last 90s) — kill it, then re-run setup');
|
|
287
287
|
else ok('no 409 conflicts in the last 90s');
|
|
288
288
|
|
|
289
|
+
// 4. Scheduled task: point it at the GUARDED HIDDEN launcher (wscript + bridge-kick).
|
|
290
|
+
// Two reasons it must be the guarded supervisor and not a direct node.exe start:
|
|
291
|
+
// (a) a console action on an Interactive-logon task pops a window at every logon
|
|
292
|
+
// and repetition — wscript with window style 0 never shows one;
|
|
293
|
+
// (b) starting bridge.mjs directly here races the one-minute tick: both see "no
|
|
294
|
+
// bridge" and both start a poller (409 getUpdates Conflict). Going through
|
|
295
|
+
// bridge-kick.mjs means every start happens under restart.lock after a check.
|
|
296
|
+
const vbsPath = join(BRIDGE_DIR, 'bridge-kick-hidden.vbs');
|
|
297
|
+
writeFileSync(
|
|
298
|
+
vbsPath,
|
|
299
|
+
[
|
|
300
|
+
"' hidden launcher: DSH bridge supervisor tick (no console window)",
|
|
301
|
+
'Set sh = CreateObject("WScript.Shell")',
|
|
302
|
+
`sh.CurrentDirectory = "${BRIDGE_DIR}"`,
|
|
303
|
+
`rc = sh.Run("""${NODE}"" ""${join(BRIDGE_DIR, 'bridge-kick.mjs')}""", 0, True)`,
|
|
304
|
+
'WScript.Quit rc',
|
|
305
|
+
'',
|
|
306
|
+
].join('\r\n'),
|
|
307
|
+
);
|
|
289
308
|
const task = run('powershell', [
|
|
290
309
|
'-NoProfile', '-NonInteractive', '-Command',
|
|
291
|
-
`$a = New-ScheduledTaskAction -Execute '
|
|
310
|
+
`$a = New-ScheduledTaskAction -Execute 'C:\\Windows\\System32\\wscript.exe' -Argument '//B //Nologo "${vbsPath}"'; Set-ScheduledTask -TaskName 'DSH-TG-Bridge' -Action $a | Out-Null; 'ok'`,
|
|
292
311
|
]);
|
|
293
|
-
if ((task.stdout || '').includes('ok')) ok('scheduled task DSH-TG-Bridge now
|
|
312
|
+
if ((task.stdout || '').includes('ok')) ok('scheduled task DSH-TG-Bridge now runs the guarded hidden supervisor');
|
|
294
313
|
else warn(`scheduled task not updated: ${(task.stderr || '').trim().slice(0, 160)}`);
|
|
295
314
|
|
|
296
315
|
// 5. Bot menu.
|