dsh-tg-bridge 1.2.8 → 1.2.10
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/bridge.mjs +13 -24
- package/immediate.mjs +56 -0
- package/package.json +6 -3
- package/queue-notice.mjs +10 -0
package/bridge.mjs
CHANGED
|
@@ -22,6 +22,8 @@ import dns from 'node:dns';
|
|
|
22
22
|
import net from 'node:net';
|
|
23
23
|
dns.setDefaultResultOrder('ipv4first');
|
|
24
24
|
net.setDefaultAutoSelectFamily(false);
|
|
25
|
+
import { noticeIsObsolete } from './queue-notice.mjs';
|
|
26
|
+
import { immediateKind } from './immediate.mjs';
|
|
25
27
|
import { handleControl, CONTROL_COMMANDS } from './control.mjs';
|
|
26
28
|
import { renderPrices } from './prices.mjs';
|
|
27
29
|
import { t, resolveLang } from './i18n.mjs';
|
|
@@ -473,33 +475,13 @@ function commandOf(text) {
|
|
|
473
475
|
return `/${match[1].toLowerCase().replace(/[.-]+/g, '')}`;
|
|
474
476
|
}
|
|
475
477
|
|
|
476
|
-
/**
|
|
477
|
-
* Commands that must run *now*, not behind the turn they are about.
|
|
478
|
-
*
|
|
479
|
-
* Everything in a topic used to be queued, so `/stop` waited for the very turn
|
|
480
|
-
* it was supposed to kill — on 2026-10-24 a 26-minute turn swallowed `/stop`
|
|
481
|
-
* and the person got "ход уже идёт" instead of an interruption. These few
|
|
482
|
-
* commands touch no turn state, so they can run concurrently with one.
|
|
483
|
-
*/
|
|
484
|
-
const IMMEDIATE_COMMANDS = new Set([
|
|
485
|
-
'stop',
|
|
486
|
-
'logs',
|
|
487
|
-
'status',
|
|
488
|
-
'help',
|
|
489
|
-
'doctor',
|
|
490
|
-
'restart',
|
|
491
|
-
]);
|
|
492
|
-
|
|
493
478
|
/** Canonical immediate command name for a message, or '' when it must queue. */
|
|
494
479
|
function immediateCommandOf(text) {
|
|
495
480
|
const trimmed = String(text ?? '').trim();
|
|
496
481
|
if (!trimmed.startsWith('/')) return '';
|
|
497
|
-
const head = trimmed.split(/\s+/)
|
|
482
|
+
const [head, ...rest] = trimmed.split(/\s+/);
|
|
498
483
|
const collapsed = commandOf(head).replace(/^\//, '');
|
|
499
|
-
|
|
500
|
-
return 'restart';
|
|
501
|
-
}
|
|
502
|
-
return IMMEDIATE_COMMANDS.has(collapsed) ? collapsed : '';
|
|
484
|
+
return immediateKind(collapsed, rest);
|
|
503
485
|
}
|
|
504
486
|
|
|
505
487
|
/**
|
|
@@ -1751,6 +1733,10 @@ async function main() {
|
|
|
1751
1733
|
// handled inline and must stay silent about it.
|
|
1752
1734
|
msg.text || msg.caption
|
|
1753
1735
|
? async () => {
|
|
1736
|
+
// Which turn was running when this message was queued: if that
|
|
1737
|
+
// value changes, the queued turn has started and the notice is
|
|
1738
|
+
// already obsolete.
|
|
1739
|
+
const turnWhenQueued = turnStartedAt.get(queueKey);
|
|
1754
1740
|
await new Promise((resolve) => setTimeout(resolve, Number(config.busyNoticeMs ?? 45000)));
|
|
1755
1741
|
if (!queues.has(queueKey)) return; // already finished
|
|
1756
1742
|
// One notice per topic: a second queued message must not add a
|
|
@@ -1767,8 +1753,11 @@ async function main() {
|
|
|
1767
1753
|
thread,
|
|
1768
1754
|
);
|
|
1769
1755
|
if (messageId) busyNotices.set(queueKey, { chatId: msg.chat.id, messageId });
|
|
1770
|
-
// The turn may have started while the notice was in flight
|
|
1771
|
-
|
|
1756
|
+
// The queued turn may have started while the notice was in flight;
|
|
1757
|
+
// in that case nothing else will delete it until the turn ends.
|
|
1758
|
+
if (noticeIsObsolete(turnWhenQueued, turnStartedAt.get(queueKey), queues.has(queueKey))) {
|
|
1759
|
+
await clearBusyNotice(queueKey);
|
|
1760
|
+
}
|
|
1772
1761
|
}
|
|
1773
1762
|
: undefined,
|
|
1774
1763
|
);
|
package/immediate.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Which control commands may run while a turn is still working?
|
|
2
|
+
//
|
|
3
|
+
// Everything in a topic used to be queued, so `/stop` waited for the very turn
|
|
4
|
+
// it was supposed to kill. The rule here is not "important commands", it is
|
|
5
|
+
// "commands that neither read nor write the state of the running turn".
|
|
6
|
+
//
|
|
7
|
+
// Groups:
|
|
8
|
+
// * always immediate — read-only reports and turn control;
|
|
9
|
+
// * immediate only without arguments — the bare form reports, the form with
|
|
10
|
+
// an argument changes state (`/soul <text>`, `/lang en`);
|
|
11
|
+
// * `/plugins` without an action — the list; with add/remove/disable/enable
|
|
12
|
+
// it installs plugins, which must queue like any other mutation;
|
|
13
|
+
// * everything else queues, because it changes the session the turn is using
|
|
14
|
+
// (`/model`, `/new`, `/resume`, `/cd`, `/send`, `/seed`, `/plugin`).
|
|
15
|
+
|
|
16
|
+
const PLUGIN_ACTIONS = new Set(['add', 'remove', 'disable', 'enable']);
|
|
17
|
+
|
|
18
|
+
const ALWAYS = new Set([
|
|
19
|
+
// Turn control.
|
|
20
|
+
'stop',
|
|
21
|
+
'restart',
|
|
22
|
+
// Reports: they read profiles, sessions, skills and the price table, but
|
|
23
|
+
// touch neither the queue nor the session of the running turn.
|
|
24
|
+
'status',
|
|
25
|
+
'logs',
|
|
26
|
+
'help',
|
|
27
|
+
'doctor',
|
|
28
|
+
'models',
|
|
29
|
+
'sessions',
|
|
30
|
+
'skills',
|
|
31
|
+
'prices',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const BARE_ONLY = new Set([
|
|
35
|
+
'soul', // `/soul` reads AGENTS.md, `/soul <text>` rewrites it
|
|
36
|
+
'lang', // `/lang` reports the current language, `/lang en` switches it
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Canonical immediate command name, or '' when the message must queue.
|
|
41
|
+
* `name` is the command without the slash and without a bot mention,
|
|
42
|
+
* `args` is everything after it, already split.
|
|
43
|
+
*/
|
|
44
|
+
export function immediateKind(name, args = []) {
|
|
45
|
+
const command = String(name ?? '').toLowerCase();
|
|
46
|
+
if (command === 'apprestart' || command === 'apprestartconfirm' || command === 'restartconfirm') {
|
|
47
|
+
return 'restart';
|
|
48
|
+
}
|
|
49
|
+
if (ALWAYS.has(command)) return command;
|
|
50
|
+
if (command === 'plugins') {
|
|
51
|
+
const [action = ''] = args;
|
|
52
|
+
return PLUGIN_ACTIONS.has(String(action).toLowerCase()) ? '' : 'plugins';
|
|
53
|
+
}
|
|
54
|
+
if (BARE_ONLY.has(command) && args.length === 0) return command;
|
|
55
|
+
return '';
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tg-bridge",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
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
6
|
"main": "bridge.mjs",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"setup": "node setup.mjs",
|
|
13
13
|
"check": "node setup.mjs --check",
|
|
14
14
|
"menu": "node bot-menu.mjs",
|
|
15
|
-
"doctor": "node bridge-kick.mjs"
|
|
15
|
+
"doctor": "node bridge-kick.mjs",
|
|
16
|
+
"test": "node --test"
|
|
16
17
|
},
|
|
17
18
|
"engines": {
|
|
18
19
|
"node": ">=20"
|
|
@@ -43,6 +44,8 @@
|
|
|
43
44
|
"README.md",
|
|
44
45
|
"LICENSE",
|
|
45
46
|
"config.example.json",
|
|
46
|
-
"patches/"
|
|
47
|
+
"patches/",
|
|
48
|
+
"queue-notice.mjs",
|
|
49
|
+
"immediate.mjs"
|
|
47
50
|
]
|
|
48
51
|
}
|
package/queue-notice.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// When is a "your message is queued" notice already obsolete?
|
|
2
|
+
//
|
|
3
|
+
// It was written for a specific running turn (`turnWhenQueued`). It must go as
|
|
4
|
+
// soon as that turn is no longer the one in front: either the queue drained, or
|
|
5
|
+
// the queued turn started (its start time differs). Kept in its own module so
|
|
6
|
+
// the rule can be tested without starting the bridge or talking to Telegram.
|
|
7
|
+
|
|
8
|
+
export function noticeIsObsolete(turnWhenQueued, turnNow, stillQueued) {
|
|
9
|
+
return !stillQueued || turnNow !== turnWhenQueued;
|
|
10
|
+
}
|