dsh-tg-bridge 1.3.3 → 1.3.5
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-kick.mjs +51 -16
- package/bridge.mjs +12 -4
- package/cli.mjs +4 -1
- package/control.mjs +41 -26
- package/package.json +1 -1
- package/prices.mjs +94 -22
- package/setup.mjs +21 -2
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/bridge.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
readdirSync, statSync, openSync, fsyncSync, closeSync, renameSync, writeFileSync as writeFd,
|
|
12
12
|
} from 'node:fs';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
|
-
import { join, basename } from 'node:path';
|
|
14
|
+
import { join, basename, isAbsolute } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
// Force IPv4 for every fetch. DNS returns an AAAA next to the A record, and
|
|
17
17
|
// Happy Eyeballs happily picks the IPv6 path, which here costs ~5 s or dies
|
|
@@ -25,7 +25,7 @@ net.setDefaultAutoSelectFamily(false);
|
|
|
25
25
|
import { noticeIsObsolete } from './queue-notice.mjs';
|
|
26
26
|
import { immediateKind } from './immediate.mjs';
|
|
27
27
|
import { writeSteerJob } from './steer-queue.mjs';
|
|
28
|
-
import { handleControl, CONTROL_COMMANDS } from './control.mjs';
|
|
28
|
+
import { handleControl, CONTROL_COMMANDS, modelOf } from './control.mjs';
|
|
29
29
|
import { renderPrices } from './prices.mjs';
|
|
30
30
|
import { t, resolveLang } from './i18n.mjs';
|
|
31
31
|
|
|
@@ -109,6 +109,11 @@ const turnStartedAt = new Map();
|
|
|
109
109
|
try {
|
|
110
110
|
Object.assign(config, JSON.parse(readFileSync(join(DIR, 'config.json'), 'utf8')));
|
|
111
111
|
} catch {}
|
|
112
|
+
// Old installs wrote these paths relative to the bridge directory, but turns
|
|
113
|
+
// run from the project cwd, where "./transcribe.py" does not exist. Resolve
|
|
114
|
+
// once at load so voice and vision work regardless of the topic's directory.
|
|
115
|
+
if (config.voice?.script && !isAbsolute(config.voice.script)) config.voice.script = join(DIR, config.voice.script);
|
|
116
|
+
if (config.vision?.patch && !isAbsolute(config.vision.patch)) config.vision.patch = join(DIR, config.vision.patch);
|
|
112
117
|
|
|
113
118
|
const state = (() => {
|
|
114
119
|
if (!existsSync(STATE)) return { offset: 0, sessions: {} };
|
|
@@ -1190,7 +1195,10 @@ ${t(lang, 'help.body')}`,
|
|
|
1190
1195
|
api: tg,
|
|
1191
1196
|
sendFile: (p, caption = '') => sendFile(chatId, threadId, p, caption),
|
|
1192
1197
|
seedTopic: ensureTopicSeed,
|
|
1193
|
-
prices: (filter) =>
|
|
1198
|
+
prices: (filter) => {
|
|
1199
|
+
const route = modelOf(config.profile || 'headless');
|
|
1200
|
+
return renderPrices(filter, { model: route.model, provider: route.provider, lang });
|
|
1201
|
+
},
|
|
1194
1202
|
reply: (out) => send(chatId, out, threadId),
|
|
1195
1203
|
stopTurn: (k) => {
|
|
1196
1204
|
const child = activeTurns.get(k);
|
|
@@ -1228,7 +1236,7 @@ ${t(lang, 'help.body')}`,
|
|
|
1228
1236
|
// Turn telemetry for the log: what the agent did, how much it cost, on which
|
|
1229
1237
|
// model. Without it /logs shows only "in" and "sent" — useless when asking
|
|
1230
1238
|
// "why did this take 9 minutes".
|
|
1231
|
-
const telemetry = { steps: 0, tools: 0, inputTokens: 0, cacheReadTokens: 0, model: config.
|
|
1239
|
+
const telemetry = { steps: 0, tools: 0, inputTokens: 0, cacheReadTokens: 0, model: modelOf(config.profile || 'headless').model };
|
|
1232
1240
|
const onEvent = (event) => {
|
|
1233
1241
|
try {
|
|
1234
1242
|
// The id of the session this process actually works in. A new topic has
|
package/cli.mjs
CHANGED
|
@@ -143,7 +143,10 @@ function writeConfig({ userId, cwd, voice }, dryRun) {
|
|
|
143
143
|
...example,
|
|
144
144
|
allowedUserIds: [Number(userId)],
|
|
145
145
|
cwd,
|
|
146
|
-
|
|
146
|
+
// Turns run from the user's project directory, so these must be absolute:
|
|
147
|
+
// a relative "./transcribe.py" silently disables voice and vision.
|
|
148
|
+
voice: { ...example.voice, enabled: Boolean(voice), script: join(RUNTIME, 'transcribe.py') },
|
|
149
|
+
vision: { ...example.vision, patch: join(RUNTIME, 'patches', 'vision.yml') },
|
|
147
150
|
};
|
|
148
151
|
delete config._comment;
|
|
149
152
|
if (dryRun) {
|
package/control.mjs
CHANGED
|
@@ -17,6 +17,7 @@ const PROFILES = join(DSH_HOME, 'profiles');
|
|
|
17
17
|
const APP = join(process.env.LOCALAPPDATA || '', 'Programs', 'DeepSeek Harness');
|
|
18
18
|
const EXE = join(APP, 'DeepSeek Harness.exe');
|
|
19
19
|
const DSH_CMD = join(APP, 'resources', 'runtime', 'cli', 'bin', 'dsh.cmd');
|
|
20
|
+
const CLI_JS = join(APP, 'resources', 'app.asar', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js');
|
|
20
21
|
|
|
21
22
|
/** Live DSH app PIDs (empty when the app is not running). */
|
|
22
23
|
function listPids() {
|
|
@@ -204,10 +205,16 @@ function removeBundle(profile, name) {
|
|
|
204
205
|
/** Run `dsh plugin ...` (which forwards to pnpm) and collect its output. */
|
|
205
206
|
function dshPlugin(profile, args, timeoutMs = 180000) {
|
|
206
207
|
return new Promise((resolve) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
// Spawning the .cmd shim with shell:false fails with EINVAL on modern Node.
|
|
209
|
+
// Run the real CLI entry through the Electron binary instead, like the
|
|
210
|
+
// bridge does for turns; fall back to a plain `dsh` on other platforms.
|
|
211
|
+
const useExe = process.platform === 'win32' && existsSync(EXE) && existsSync(join(APP, 'resources', 'app.asar'));
|
|
212
|
+
const child = useExe
|
|
213
|
+
? spawn(EXE, ['--expose-internals', CLI_JS, 'plugin', '--profile', profile, ...args], {
|
|
214
|
+
windowsHide: true,
|
|
215
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
|
216
|
+
})
|
|
217
|
+
: spawn('dsh', ['plugin', '--profile', profile, ...args], { windowsHide: true });
|
|
211
218
|
let out = '';
|
|
212
219
|
const timer = setTimeout(() => child.kill(), timeoutMs);
|
|
213
220
|
child.stdout.setEncoding('utf8');
|
|
@@ -350,7 +357,7 @@ export const CONTROL_COMMANDS = [
|
|
|
350
357
|
];
|
|
351
358
|
|
|
352
359
|
export async function handleControl(ctx) {
|
|
353
|
-
const { command, arg, send, reply, log, profile, state, key, config } = ctx;
|
|
360
|
+
const { command, arg, send, reply, log, profile, state, key, config, lang } = ctx;
|
|
354
361
|
const say = (text) => reply(text);
|
|
355
362
|
|
|
356
363
|
// One command for one job: `/plugins` is both the list and the way to change
|
|
@@ -369,30 +376,37 @@ export async function handleControl(ctx) {
|
|
|
369
376
|
|
|
370
377
|
switch (effective) {
|
|
371
378
|
case '/status': {
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
const deskPlugins = pluginsOf('desktop');
|
|
379
|
+
const activeProfile = config.profile || 'headless';
|
|
380
|
+
const head = modelOf(activeProfile);
|
|
375
381
|
const session = state.sessions[key] || '(ещё нет)';
|
|
376
382
|
const active = ctx.activeTurns ? ctx.activeTurns.size : 0;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
383
|
+
const lines = [
|
|
384
|
+
`Профиль: ${activeProfile} (${head.provider}/${head.model})`,
|
|
385
|
+
`Ветка: ${ctx.threadId || 'общая'}`,
|
|
386
|
+
`Сессия: ${session}`,
|
|
387
|
+
`Ходов в работе: ${active}`,
|
|
388
|
+
`Скиллы: ${skillsOf().length}`,
|
|
389
|
+
`DSH_HOME: ${DSH_HOME}`,
|
|
390
|
+
];
|
|
391
|
+
// The desktop profile only matters when this install actually has one.
|
|
392
|
+
if (existsSync(join(PROFILES, 'desktop', 'cordis.patch.yml'))) {
|
|
393
|
+
const desk = modelOf('desktop');
|
|
394
|
+
const deskPlugins = pluginsOf('desktop');
|
|
395
|
+
lines.splice(1, 0, `UI: desktop (${desk.provider}/${desk.model}) — плагинов ${deskPlugins.bundles.length}`);
|
|
396
|
+
}
|
|
397
|
+
say(lines.join('\n'));
|
|
388
398
|
return true;
|
|
389
399
|
}
|
|
390
400
|
|
|
391
401
|
case '/models': {
|
|
392
|
-
const
|
|
393
|
-
const
|
|
402
|
+
const activeProfile = config.profile || 'headless';
|
|
403
|
+
const head = modelOf(activeProfile);
|
|
404
|
+
const ids = modelsOf(activeProfile);
|
|
394
405
|
say(
|
|
395
|
-
[
|
|
406
|
+
[
|
|
407
|
+
ids.length > 0 ? `Модель сейчас: ${head.model}` : `В профиле ${activeProfile} нет списка моделей`,
|
|
408
|
+
...(ids.length > 0 ? ['Доступные:', ...ids.map((id) => ` ${id === head.model ? '*' : ' '} ${id}`)] : []),
|
|
409
|
+
].join('\n'),
|
|
396
410
|
);
|
|
397
411
|
return true;
|
|
398
412
|
}
|
|
@@ -403,18 +417,19 @@ export async function handleControl(ctx) {
|
|
|
403
417
|
say('Использование: /model <id> [both] (/models — список)');
|
|
404
418
|
return true;
|
|
405
419
|
}
|
|
406
|
-
|
|
407
|
-
|
|
420
|
+
const activeProfile = config.profile || 'headless';
|
|
421
|
+
if (!setModel(activeProfile, wanted)) {
|
|
422
|
+
say(`Не смог переключить ${activeProfile} на «${wanted}»: в профиле нет строки agent-default-model. /models — список.`);
|
|
408
423
|
return true;
|
|
409
424
|
}
|
|
410
425
|
const desktop = scope === 'both' ? setModel('desktop', wanted) : false;
|
|
411
|
-
say(`Модель
|
|
426
|
+
say(`Модель ${activeProfile} → ${wanted}${desktop ? ', desktop → ' + wanted : ''}. Действует со следующего хода.`);
|
|
412
427
|
return true;
|
|
413
428
|
}
|
|
414
429
|
|
|
415
430
|
case '/sessions': {
|
|
416
431
|
const limit = Number.parseInt(arg, 10) > 0 ? Math.min(Number.parseInt(arg, 10), 20) : 8;
|
|
417
|
-
const rows = recentSessions(config.cwd, limit);
|
|
432
|
+
const rows = recentSessions((state.cwd && state.cwd[key]) || config.cwd, limit);
|
|
418
433
|
if (rows.length === 0) {
|
|
419
434
|
say('Сессий не нашёл.');
|
|
420
435
|
return true;
|
package/package.json
CHANGED
package/prices.mjs
CHANGED
|
@@ -99,49 +99,121 @@ function usd(value) {
|
|
|
99
99
|
return `$${value < 0.01 ? value.toFixed(4) : value.toFixed(2)}`;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const WORDS = {
|
|
103
|
+
en: {
|
|
104
|
+
prices: 'Prices',
|
|
105
|
+
now: 'Current',
|
|
106
|
+
cheap: 'Cheapest with context >= 128k:',
|
|
107
|
+
none: 'No models matched the filter.',
|
|
108
|
+
fail: 'Could not fetch prices',
|
|
109
|
+
unknown: 'not found in the catalog',
|
|
110
|
+
},
|
|
111
|
+
ru: {
|
|
112
|
+
prices: 'Цены',
|
|
113
|
+
now: 'Сейчас',
|
|
114
|
+
cheap: 'Дешёвые модели с контекстом ≥128k:',
|
|
115
|
+
none: 'Ничего не нашлось по фильтру.',
|
|
116
|
+
fail: 'Не смог получить цены',
|
|
117
|
+
unknown: 'в каталоге не найден',
|
|
118
|
+
},
|
|
119
|
+
zh: {
|
|
120
|
+
prices: '价格',
|
|
121
|
+
now: '当前',
|
|
122
|
+
cheap: '上下文 ≥128k 的最便宜模型:',
|
|
123
|
+
none: '没有符合筛选条件的模型。',
|
|
124
|
+
fail: '无法获取价格',
|
|
125
|
+
unknown: '目录中未找到',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
102
129
|
/**
|
|
103
|
-
*
|
|
130
|
+
* Find a model by id. Providers prefix ids differently (`openai/gpt-4o`,
|
|
131
|
+
* `gpt-4o`, `unorouter/gpt-4o:free`), so exact match first, then the shortest
|
|
132
|
+
* id that contains the wanted token.
|
|
133
|
+
*/
|
|
134
|
+
export function findModel(models, wanted) {
|
|
135
|
+
const needle = String(wanted ?? '').trim().toLowerCase();
|
|
136
|
+
if (!needle) return null;
|
|
137
|
+
const exact = models.find((model) => model.id.toLowerCase() === needle);
|
|
138
|
+
if (exact) return exact;
|
|
139
|
+
const matches = models.filter((model) => model.id.toLowerCase().includes(needle));
|
|
140
|
+
matches.sort((a, b) => a.id.length - b.id.length);
|
|
141
|
+
return matches[0] || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Render `/prices [filter]` from every catalog we can reach, so the answer does
|
|
146
|
+
* not depend on one provider: the unorouter page carries live prices for its
|
|
147
|
+
* own routes, models.dev covers the rest of the market. Both are merged; the
|
|
148
|
+
* current model is looked up in the merged list.
|
|
104
149
|
* @param {string} filter optional text to match model ids
|
|
105
|
-
* @param {{ model?: string
|
|
150
|
+
* @param {{ model?: string, provider?: string, lang?: string }} options
|
|
106
151
|
*/
|
|
107
152
|
export async function renderPrices(filter = '', options = {}) {
|
|
153
|
+
const lang = WORDS[options.lang] ? options.lang : 'en';
|
|
154
|
+
const words = WORDS[lang];
|
|
155
|
+
const [page, dev] = await Promise.allSettled([
|
|
156
|
+
fetchText(MODELS_PAGE),
|
|
157
|
+
fetchText(MODELS_DEV, 30000),
|
|
158
|
+
]);
|
|
159
|
+
|
|
108
160
|
let models = [];
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
161
|
+
const sources = [];
|
|
162
|
+
if (page.status === 'fulfilled') {
|
|
163
|
+
const parsed = parseModelsPage(page.value);
|
|
164
|
+
if (parsed.length > 0) {
|
|
165
|
+
models.push(...parsed);
|
|
166
|
+
sources.push('unorouter');
|
|
167
|
+
}
|
|
115
168
|
}
|
|
116
|
-
if (
|
|
169
|
+
if (dev.status === 'fulfilled') {
|
|
117
170
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
const parsed = parseModelsDev(JSON.parse(dev.value));
|
|
172
|
+
if (parsed.length > 0) {
|
|
173
|
+
models.push(...parsed);
|
|
174
|
+
sources.push('models.dev');
|
|
175
|
+
}
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
if (models.length === 0) {
|
|
179
|
+
const reason = dev.status === 'rejected'
|
|
180
|
+
? dev.reason?.message
|
|
181
|
+
: page.status === 'rejected'
|
|
182
|
+
? page.reason?.message
|
|
183
|
+
: 'catalogs parsed to 0 models';
|
|
184
|
+
return `${words.fail}: ${reason}`;
|
|
123
185
|
}
|
|
124
186
|
|
|
187
|
+
// The live page wins on duplicate ids; models.dev fills the rest.
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
models = models.filter((model) => {
|
|
190
|
+
const id = model.id.toLowerCase();
|
|
191
|
+
if (seen.has(id)) return false;
|
|
192
|
+
seen.add(id);
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
|
|
125
196
|
const wanted = String(filter ?? '').trim().toLowerCase();
|
|
126
|
-
let rows = models.filter((m) => (m.context ?? 0) >= 128000 || m.context === null);
|
|
197
|
+
let rows = models.filter((m) => ((m.context ?? 0) >= 128000 || m.context === null) && m.inputPrice > 0);
|
|
127
198
|
if (wanted) rows = rows.filter((m) => m.id.toLowerCase().includes(wanted));
|
|
128
199
|
rows.sort((a, b) => a.inputPrice - b.inputPrice);
|
|
129
200
|
|
|
130
|
-
const lines = [
|
|
131
|
-
if (options.model) {
|
|
132
|
-
const current = models
|
|
201
|
+
const lines = [`${words.prices}: ${sources.join(' + ')}`, ''];
|
|
202
|
+
if (options.model && options.model !== '?') {
|
|
203
|
+
const current = findModel(models, options.model);
|
|
204
|
+
const name = options.provider ? `${options.provider}/${options.model}` : options.model;
|
|
133
205
|
lines.push(
|
|
134
206
|
current
|
|
135
|
-
?
|
|
136
|
-
:
|
|
207
|
+
? `${words.now}: ${name} — ${usd(current.inputPrice)} / ${usd(current.outputPrice)} per 1M`
|
|
208
|
+
: `${words.now}: ${name} (${words.unknown})`,
|
|
137
209
|
''
|
|
138
210
|
);
|
|
139
211
|
}
|
|
140
|
-
lines.push(
|
|
212
|
+
lines.push(words.cheap);
|
|
141
213
|
for (const model of rows.slice(0, 12)) {
|
|
142
214
|
const context = model.context ? `${Math.round(model.context / 1000)}k` : '?';
|
|
143
215
|
lines.push(`• ${model.id} — ${usd(model.inputPrice)} / ${usd(model.outputPrice)} · ${context}`);
|
|
144
216
|
}
|
|
145
|
-
if (rows.length === 0) lines.push(
|
|
217
|
+
if (rows.length === 0) lines.push(words.none);
|
|
146
218
|
return lines.join('\n');
|
|
147
219
|
}
|
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.
|