dsh-tg-bridge 1.5.3 → 1.5.4
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 +14 -3
- package/bridge.mjs +36 -8
- package/cli.mjs +13 -7
- package/config.example.json +5 -19
- package/package.json +2 -3
- package/prices.mjs +22 -22
- package/patches/model-deepseek-v4.1-flash_free.yml +0 -4
- package/patches/model-glm-5.3-flash_free.yml +0 -4
- package/patches/model-kimi-k3_free.yml +0 -4
- package/patches/vision.yml +0 -10
package/README.md
CHANGED
|
@@ -92,6 +92,15 @@ What `init` does:
|
|
|
92
92
|
`init` flags: `--cwd <path>` (default `~/Projects`), `--voice` (enable
|
|
93
93
|
voice transcription), `--dry-run` (show what would be written, change nothing).
|
|
94
94
|
|
|
95
|
+
Optional provider settings live in `config.json`:
|
|
96
|
+
|
|
97
|
+
| Key | Meaning |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `providerKeys` | extra env var names to pass to a turn, read from `~/.dsh/.credentials.yaml`; default `[]` |
|
|
100
|
+
| `pricesUrl` | public model page for `/prices`; default `""` uses models.dev only |
|
|
101
|
+
| `vision.patch` | overlay for photo turns; default `""` (photos are text-only until set) |
|
|
102
|
+
| `fallbackModels` | free-model fallbacks; default `[]` |
|
|
103
|
+
|
|
95
104
|
Get your id from [@userinfobot](https://t.me/userinfobot), the token from
|
|
96
105
|
[@BotFather](https://t.me/BotFather).
|
|
97
106
|
|
|
@@ -106,9 +115,11 @@ Three things without which the bot will stay silent in a group or work only half
|
|
|
106
115
|
3. **Pin Messages** permission for the bot. Only needed to pin the first
|
|
107
116
|
message of a topic; without it the bridge keeps working but will not pin.
|
|
108
117
|
|
|
109
|
-
You do not need to set up the command menu by hand:
|
|
110
|
-
in Russian, English and Chinese at once. The
|
|
111
|
-
|
|
118
|
+
You do not need to set up the command menu by hand: `init` and `setup` install it
|
|
119
|
+
in Russian, English and Chinese at once. The menu is not refreshed by a plain
|
|
120
|
+
bridge restart, so after adding commands run `dsh-tg menu`. The full settings
|
|
121
|
+
table, permissions, checks and common errors are in
|
|
122
|
+
[docs/TELEGRAM-SETUP.md](docs/TELEGRAM-SETUP.md).
|
|
112
123
|
|
|
113
124
|
**Autostart** is installed by the setup: `launchd` on macOS, `systemd --user` on
|
|
114
125
|
Linux. On Windows `setup` registers the `DSH-TG-Bridge` task, which runs the
|
package/bridge.mjs
CHANGED
|
@@ -259,6 +259,30 @@ function readSecret(name) {
|
|
|
259
259
|
return m ? m[1] : '';
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Env vars for the provider keys this install needs.
|
|
264
|
+
*
|
|
265
|
+
* A headless turn inherits the bridge's environment, and the credentials
|
|
266
|
+
* service only resolves providers the app knows about. Extra providers are
|
|
267
|
+
* therefore declared per install in `config.json` as
|
|
268
|
+
* `"providerKeys": ["MY_PROVIDER_API_KEY"]`, and each name is read from
|
|
269
|
+
* `~/.dsh/.credentials.yaml` under the same name. Nothing is hardcoded: an
|
|
270
|
+
* install that runs on the app's own providers sets an empty list and gets
|
|
271
|
+
* exactly the inherited environment.
|
|
272
|
+
*/
|
|
273
|
+
function providerKeys(cfg) {
|
|
274
|
+
const out = {};
|
|
275
|
+
const names = Array.isArray(cfg.providerKeys) ? cfg.providerKeys : [];
|
|
276
|
+
for (const name of names) {
|
|
277
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(String(name))) continue;
|
|
278
|
+
try {
|
|
279
|
+
const value = readSecret(name) || (process.env[name] || '').trim();
|
|
280
|
+
if (value) out[name] = value;
|
|
281
|
+
} catch {}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
262
286
|
/**
|
|
263
287
|
* The bot token can be stored under three names: the plain one that `dsh-tg
|
|
264
288
|
* init` writes, the suffixed one DSH generates when the secret is added through
|
|
@@ -740,13 +764,10 @@ function runTurn(task, sessionId, key = '', onEvent = null, extraArgs = [], cwd
|
|
|
740
764
|
DSH_TG_CHAT: String(key).split(':')[0] || '',
|
|
741
765
|
DSH_TG_THREAD: String(key).split(':')[1] || '0',
|
|
742
766
|
ELECTRON_RUN_AS_NODE: '1',
|
|
743
|
-
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
TAYNIK_API_KEY: readSecret('TAYNIK_API_KEY'),
|
|
748
|
-
NEURALDEEP_API_KEY: readSecret('NEURALDEEP_API_KEY'),
|
|
749
|
-
IKHDEV_PRO_API_KEY: readSecret('IKHDEV_PRO_API_KEY'),
|
|
767
|
+
// Provider keys the profile cannot resolve on its own. Configured per
|
|
768
|
+
// install (`providerKeys`, see config.example.json); a key listed in
|
|
769
|
+
// `~/.dsh/.credentials.yaml` is passed by its own name.
|
|
770
|
+
...providerKeys(config),
|
|
750
771
|
},
|
|
751
772
|
windowsHide: true,
|
|
752
773
|
});
|
|
@@ -1248,7 +1269,14 @@ ${t(lang, 'help.body')}`,
|
|
|
1248
1269
|
seedTopic: ensureTopicSeed,
|
|
1249
1270
|
prices: (filter) => {
|
|
1250
1271
|
const route = modelOf(config.profile || 'headless');
|
|
1251
|
-
|
|
1272
|
+
// A provider page is optional: without `pricesUrl` the answer comes from
|
|
1273
|
+
// the public models.dev catalog, so no install is tied to one vendor.
|
|
1274
|
+
return renderPrices(filter, {
|
|
1275
|
+
model: route.model,
|
|
1276
|
+
provider: route.provider,
|
|
1277
|
+
pageUrl: config.pricesUrl || '',
|
|
1278
|
+
lang,
|
|
1279
|
+
});
|
|
1252
1280
|
},
|
|
1253
1281
|
reply: (out) => send(chatId, out, threadId),
|
|
1254
1282
|
stopTurn: (k) => {
|
package/cli.mjs
CHANGED
|
@@ -37,15 +37,19 @@ const RUNTIME_FILES = [
|
|
|
37
37
|
*/
|
|
38
38
|
function syncRuntime() {
|
|
39
39
|
if (resolve(DIR) === resolve(RUNTIME)) return;
|
|
40
|
-
mkdirSync(join(RUNTIME, 'patches'), { recursive: true });
|
|
41
40
|
for (const file of RUNTIME_FILES) {
|
|
42
41
|
if (existsSync(join(DIR, file))) copyFileSync(join(DIR, file), join(RUNTIME, file));
|
|
43
42
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
43
|
+
// Model overlays ship in `patches/` of the package. A checkout without that
|
|
44
|
+
// directory is valid: fallbacks and vision are then simply not configured.
|
|
45
|
+
if (existsSync(join(DIR, 'patches'))) {
|
|
46
|
+
mkdirSync(join(RUNTIME, 'patches'), { recursive: true });
|
|
47
|
+
try {
|
|
48
|
+
for (const patch of readdirSync(join(DIR, 'patches'))) {
|
|
49
|
+
copyFileSync(join(DIR, 'patches', patch), join(RUNTIME, 'patches', patch));
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
49
53
|
console.log(` код: скопирован из ${DIR} в ${RUNTIME}`);
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -146,7 +150,9 @@ function writeConfig({ userId, cwd, voice }, dryRun) {
|
|
|
146
150
|
// Turns run from the user's project directory, so these must be absolute:
|
|
147
151
|
// a relative "./transcribe.py" silently disables voice and vision.
|
|
148
152
|
voice: { ...example.voice, enabled: Boolean(voice), script: join(RUNTIME, 'transcribe.py') },
|
|
149
|
-
|
|
153
|
+
// Vision is opt-in: without a patch the bridge logs that photos cannot be
|
|
154
|
+
// read instead of forcing one provider's model onto every install.
|
|
155
|
+
vision: { ...example.vision, patch: example.vision?.patch ? join(RUNTIME, 'patches', 'vision.yml') : '' },
|
|
150
156
|
};
|
|
151
157
|
delete config._comment;
|
|
152
158
|
if (dryRun) {
|
package/config.example.json
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
"allowedUserIds": [],
|
|
4
4
|
"profile": "headless",
|
|
5
5
|
"cwd": "",
|
|
6
|
+
"providerKeys": [],
|
|
7
|
+
"pricesUrl": "",
|
|
6
8
|
"turnIdleMs": 120000,
|
|
7
9
|
"turnTimeoutMs": 0,
|
|
8
10
|
"busyNoticeMs": 45000,
|
|
@@ -12,23 +14,7 @@
|
|
|
12
14
|
"pinTopicSeed": true,
|
|
13
15
|
"plainText": false,
|
|
14
16
|
"servers": {},
|
|
15
|
-
"fallbackModels": [
|
|
16
|
-
{
|
|
17
|
-
"model": "glm-5.3-flash:free",
|
|
18
|
-
"patch": "./patches/model-glm-5.3-flash_free.yml",
|
|
19
|
-
"free": true
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"model": "deepseek-v4.1-flash:free",
|
|
23
|
-
"patch": "./patches/model-deepseek-v4.1-flash_free.yml",
|
|
24
|
-
"free": true
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"model": "kimi-k3:free",
|
|
28
|
-
"patch": "./patches/model-kimi-k3_free.yml",
|
|
29
|
-
"free": true
|
|
30
|
-
}
|
|
31
|
-
],
|
|
17
|
+
"fallbackModels": [],
|
|
32
18
|
"voice": {
|
|
33
19
|
"enabled": false,
|
|
34
20
|
"python": "",
|
|
@@ -41,7 +27,7 @@
|
|
|
41
27
|
"echo": true
|
|
42
28
|
},
|
|
43
29
|
"vision": {
|
|
44
|
-
"patch": "
|
|
45
|
-
"model": "
|
|
30
|
+
"patch": "",
|
|
31
|
+
"model": ""
|
|
46
32
|
}
|
|
47
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tg-bridge",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
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": "plugin-bridge.mjs",
|
|
@@ -58,8 +58,7 @@
|
|
|
58
58
|
"README.md",
|
|
59
59
|
"LICENSE",
|
|
60
60
|
"config.example.json",
|
|
61
|
-
|
|
62
|
-
"queue-notice.mjs",
|
|
61
|
+
"queue-notice.mjs",
|
|
63
62
|
"immediate.mjs",
|
|
64
63
|
"say.mjs",
|
|
65
64
|
"steer-queue.mjs",
|
package/prices.mjs
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
// prices.mjs —
|
|
1
|
+
// prices.mjs — model prices without a provider-specific plugin.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// no uptime data but is stable JSON.
|
|
3
|
+
// `/prices` reads the public models.dev catalog by default. An install that
|
|
4
|
+
// wants live prices from its own provider can point `pricesUrl` at that
|
|
5
|
+
// provider's public model page; the parser is generic, so no vendor is baked
|
|
6
|
+
// into the code.
|
|
8
7
|
|
|
9
|
-
const MODELS_PAGE = 'https://unorouter.com/en/models';
|
|
10
8
|
const MODELS_DEV = 'https://models.dev/api.json';
|
|
11
9
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
12
10
|
|
|
@@ -128,7 +126,7 @@ const WORDS = {
|
|
|
128
126
|
|
|
129
127
|
/**
|
|
130
128
|
* Find a model by id. Providers prefix ids differently (`openai/gpt-4o`,
|
|
131
|
-
* `gpt-4o`, `
|
|
129
|
+
* `gpt-4o`, `vendor/gpt-4o:free`), so exact match first, then the shortest
|
|
132
130
|
* id that contains the wanted token.
|
|
133
131
|
*/
|
|
134
132
|
export function findModel(models, wanted) {
|
|
@@ -142,31 +140,33 @@ export function findModel(models, wanted) {
|
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
/**
|
|
145
|
-
* Render `/prices [filter]` from
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
143
|
+
* Render `/prices [filter]` from the public market catalog, plus an optional
|
|
144
|
+
* provider page configured as `pricesUrl`. Neither source is required: if one
|
|
145
|
+
* is down the other still answers, and with no `pricesUrl` only models.dev is
|
|
146
|
+
* queried.
|
|
149
147
|
* @param {string} filter optional text to match model ids
|
|
150
|
-
* @param {{ model?: string, provider?: string, lang?: string }} options
|
|
148
|
+
* @param {{ model?: string, provider?: string, pageUrl?: string, lang?: string }} options
|
|
151
149
|
*/
|
|
152
150
|
export async function renderPrices(filter = '', options = {}) {
|
|
153
151
|
const lang = WORDS[options.lang] ? options.lang : 'en';
|
|
154
152
|
const words = WORDS[lang];
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
const pageUrl = String(options.pageUrl || '').trim();
|
|
154
|
+
const jobs = [fetchText(MODELS_DEV, 30000)];
|
|
155
|
+
if (pageUrl) jobs.unshift(fetchText(pageUrl));
|
|
156
|
+
const results = await Promise.allSettled(jobs);
|
|
157
|
+
const page = pageUrl ? results[0] : null;
|
|
158
|
+
const dev = pageUrl ? results[1] : results[0];
|
|
159
159
|
|
|
160
160
|
let models = [];
|
|
161
161
|
const sources = [];
|
|
162
|
-
if (page
|
|
162
|
+
if (page?.status === 'fulfilled') {
|
|
163
163
|
const parsed = parseModelsPage(page.value);
|
|
164
164
|
if (parsed.length > 0) {
|
|
165
165
|
models.push(...parsed);
|
|
166
|
-
sources.push('
|
|
166
|
+
try { sources.push(new URL(pageUrl).hostname.replace(/^www\./i, '')); } catch { sources.push('provider'); }
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
-
if (dev
|
|
169
|
+
if (dev?.status === 'fulfilled') {
|
|
170
170
|
try {
|
|
171
171
|
const parsed = parseModelsDev(JSON.parse(dev.value));
|
|
172
172
|
if (parsed.length > 0) {
|
|
@@ -176,9 +176,9 @@ export async function renderPrices(filter = '', options = {}) {
|
|
|
176
176
|
} catch {}
|
|
177
177
|
}
|
|
178
178
|
if (models.length === 0) {
|
|
179
|
-
const reason = dev
|
|
179
|
+
const reason = dev?.status === 'rejected'
|
|
180
180
|
? dev.reason?.message
|
|
181
|
-
: page
|
|
181
|
+
: page?.status === 'rejected'
|
|
182
182
|
? page.reason?.message
|
|
183
183
|
: 'catalogs parsed to 0 models';
|
|
184
184
|
return `${words.fail}: ${reason}`;
|
package/patches/vision.yml
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# Vision overlay for photo turns.
|
|
2
|
-
#
|
|
3
|
-
# The default headless route (deepseek-v4.1-flash-1017) declares no image input,
|
|
4
|
-
# so `read_image` fails with "model does not declare image input". This overlay
|
|
5
|
-
# is applied ONLY to turns that carry a picture: it points the agent at a
|
|
6
|
-
# vision-capable model from the same provider, leaving text turns untouched.
|
|
7
|
-
- id: agent-default-model
|
|
8
|
-
config:
|
|
9
|
-
provider: unorouter
|
|
10
|
-
model: gemini-3.8-flash
|