dsh-model 0.23.2 → 0.23.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.en.md CHANGED
@@ -163,6 +163,7 @@ See the [design doc](docs/DESIGN.md) (Chinese).
163
163
  - Meant for personal use: one account of your own per provider; do not use it to serve others.
164
164
  - Using subscription and WorkBuddy credentials outside the official clients may break the providers' terms and put the account at risk. Decide for yourself.
165
165
  - Windows 10 / 11 (x64, ARM64), macOS and Linux, Node ≥ 20.
166
+ - Downloading the engine needs GitHub: setup uses the system proxy (Windows, macOS) or `HTTPS_PROXY` automatically; if it still cannot connect it says why, and you can pass `--proxy <url>` or download the file yourself and set `DSH_MODEL_ENGINE_ARCHIVE`.
166
167
  - Windows: the background services start at sign-in with no window. If an antivirus blocks them (the program is gone or will not start), trust `%USERPROFILE%\.dsh-model` and run `dsh-model repair`.
167
168
 
168
169
  ## License
package/README.md CHANGED
@@ -163,6 +163,7 @@ dsh · 编辑器 · 脚本 ──Bearer key──▶ 引擎 127.0.0.1:8317/v1(
163
163
  - 适合个人使用:每家用自己的一个账号,不要拿来对外提供服务。
164
164
  - 订阅和 WorkBuddy 的凭据在官方客户端之外使用,可能不符合服务商的条款,账号有风险,请自行判断。
165
165
  - 支持 Windows 10 / 11(x64、ARM64)、macOS、Linux,需要 Node ≥ 20。
166
+ - 下载引擎要连 GitHub:setup 会自动用上系统代理(Windows、macOS)或 `HTTPS_PROXY`;连不上时会说明原因,可以用 `--proxy <地址>` 指定代理,或手动下载后设置 `DSH_MODEL_ENGINE_ARCHIVE`。
166
167
  - Windows:后台服务登录后自动运行、不弹窗口。如果被杀毒软件拦下(程序不见了或启动不了),把 `%USERPROFILE%\.dsh-model` 加入信任,再执行 `dsh-model repair`。
167
168
 
168
169
  ## License
@@ -50,31 +50,35 @@ function appDataRoots(home, platform = process.platform, env = process.env) {
50
50
  return [...new Set([env.APPDATA, join(home, 'AppData', 'Roaming')].filter((v) => Boolean(v)))];
51
51
  return [env.XDG_CONFIG_HOME || join(home, '.config')];
52
52
  }
53
- /** 找本机的 Trae 登录:先桌面 App,再 CLI。找不到返回 null(不报错:没装 Trae 就不显示) */
53
+ /**
54
+ * 找本机的 Trae 登录。可能有好几份(Trae CN、TRAE SOLO CN、CLI…),常有一份早就过期没人用了
55
+ * (2026-10-29 实测:先读到的 TRAE SOLO CN 已过期 7 天,正在用的 Trae CN 是新的,签到因此失败)。
56
+ * 所以全读一遍再挑:没过期的优先,其中桌面 App(带设备 id)优先,再按到期最晚。找不到返回 null(没装 Trae 就不显示)。
57
+ */
54
58
  export async function findTraeCredential(home = homedir()) {
55
59
  // 测试环境不看本机真装了什么(与不发现桌面 App 同一个开关)
56
60
  if (process.env.DSH_MODEL_NO_APP_DISCOVERY && home === homedir())
57
61
  return null;
62
+ const found = [];
58
63
  for (const root of appDataRoots(home)) {
59
64
  for (const app of APPS) {
60
65
  const file = join(root, app, 'User', 'globalStorage', 'storage.json');
61
- let storage;
62
66
  try {
63
- storage = JSON.parse(await readFile(file, 'utf8'));
67
+ const storage = JSON.parse(await readFile(file, 'utf8'));
68
+ const raw = storage[AUTH_KEY];
69
+ if (typeof raw !== 'string' || !raw.trim())
70
+ continue;
71
+ const text = raw.trim().startsWith('{') ? raw : decryptStorageValue(raw);
72
+ const auth = JSON.parse(text);
73
+ const token = auth.token || auth.accessToken;
74
+ if (!token)
75
+ continue;
76
+ const dc = Object.keys(storage).find((k) => k.startsWith(DEVICE_PREFIX));
77
+ found.push({ token, deviceId: dc ? dc.slice(DEVICE_PREFIX.length) : String(storage['telemetry.devDeviceId'] ?? ''), source: app });
64
78
  }
65
79
  catch {
66
- continue;
80
+ // 没装、读不了或解不开:看下一个
67
81
  }
68
- const raw = storage[AUTH_KEY];
69
- if (typeof raw !== 'string' || !raw.trim())
70
- continue;
71
- const text = raw.trim().startsWith('{') ? raw : decryptStorageValue(raw);
72
- const auth = JSON.parse(text);
73
- const token = auth.token || auth.accessToken;
74
- if (!token)
75
- continue;
76
- const dc = Object.keys(storage).find((k) => k.startsWith(DEVICE_PREFIX));
77
- return { token, deviceId: dc ? dc.slice(DEVICE_PREFIX.length) : String(storage['telemetry.devDeviceId'] ?? ''), source: app };
78
82
  }
79
83
  }
80
84
  for (const dir of ['.trae-cn', '.trae']) {
@@ -82,13 +86,30 @@ export async function findTraeCredential(home = homedir()) {
82
86
  const text = (await readFile(join(home, dir, 'trae-jwt-token'), 'utf8')).trim();
83
87
  const token = text.startsWith('{') ? String(JSON.parse(text).token ?? '') : text;
84
88
  if (token.split('.').length === 3)
85
- return { token, deviceId: '', source: `~/${dir}` };
89
+ found.push({ token, deviceId: '', source: `~/${dir}` });
86
90
  }
87
91
  catch {
88
92
  // 没有
89
93
  }
90
94
  }
91
- return null;
95
+ return pickTraeCredential(found);
96
+ }
97
+ /** 几份登录里挑一份:没过期的优先 → 带设备 id 的(桌面 App)优先 → 到期最晚的。导出给测试 */
98
+ export function pickTraeCredential(list, now = Date.now()) {
99
+ const exp = (t) => {
100
+ const d = tokenDaysLeft(t, now);
101
+ return d === null ? Number.POSITIVE_INFINITY : d;
102
+ };
103
+ const ranked = [...list].sort((a, b) => {
104
+ const live = Number(exp(b.token) >= 0) - Number(exp(a.token) >= 0);
105
+ if (live)
106
+ return live;
107
+ const desktop = Number(Boolean(b.deviceId)) - Number(Boolean(a.deviceId));
108
+ if (desktop)
109
+ return desktop;
110
+ return exp(b.token) - exp(a.token);
111
+ });
112
+ return ranked[0] ?? null;
92
113
  }
93
114
  /** 令牌还剩几天(读 JWT 的 exp);读不出来返回 null */
94
115
  export function tokenDaysLeft(token, now = Date.now()) {
@@ -45,9 +45,9 @@ async function upgrade(ctx, requested) {
45
45
  let asset = version === manifest.version ? manifest.assets[target] : undefined;
46
46
  if (!asset) {
47
47
  warn(L(`v${version} 不在本版 dsh-model 的锁定清单里,校验值取自上游发布的 checksums.txt`, `v${version} is not pinned by this dsh-model; using the checksum from the upstream checksums.txt`));
48
- asset = await fetchUpstreamAsset(manifest, version, target);
48
+ asset = await fetchUpstreamAsset(manifest, version, target, { proxy: all.config.proxy });
49
49
  }
50
- await installVersion(ctx, version, asset);
50
+ await installVersion(ctx, version, asset, { proxy: all.config.proxy });
51
51
  ok(L(`v${version} 已下载并校验`, `v${version} downloaded and verified`));
52
52
  await trialRun(ctx, all, versionBinary(ctx, version));
53
53
  ok(L('试运行通过', 'Trial run passed'));
@@ -164,10 +164,10 @@ export async function ensureEngine(ctx, all, opts = {}) {
164
164
  const manifest = await loadManifest();
165
165
  const target = platformTarget();
166
166
  const version = config.engine.version ?? manifest.version;
167
- const asset = version === manifest.version ? manifest.assets[target] : await fetchUpstreamAsset(manifest, version, target);
167
+ const asset = version === manifest.version ? manifest.assets[target] : await fetchUpstreamAsset(manifest, version, target, { proxy: config.proxy });
168
168
  if (!asset)
169
169
  throw new DshModelError('unsupported_platform', L(`引擎没有 ${target} 的发布包`, `No engine build for ${target}`));
170
- const installed = await installVersion(ctx, version, asset);
170
+ const installed = await installVersion(ctx, version, asset, { proxy: config.proxy });
171
171
  if ((await currentVersion(ctx)) !== version)
172
172
  await activateVersion(ctx, version);
173
173
  config.engine.version = version;
@@ -3,13 +3,12 @@
3
3
  // 只信任仓库里 engine-manifest.json 的 sha256;校验不过直接失败。
4
4
  // 版本目录 engine/versions/<v>/,current 软链指向在用版本,升级与回滚只切软链。
5
5
  // Windows:发布包是 zip、程序叫 cli-proxy-api.exe;建软链要管理员,所以 current 用目录联接(junction),不需要权限。
6
- import { createWriteStream } from 'node:fs';
7
6
  import { chmod, mkdir, readFile, readlink, rename, rm, symlink, unlink } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
8
  import { join, resolve } from 'node:path';
9
- import { Readable } from 'node:stream';
10
- import { pipeline } from 'node:stream/promises';
11
- import { DshModelError } from '../errors.js';
9
+ import { DshModelError, isDshModelError } from '../errors.js';
12
10
  import { L } from '../i18n.js';
11
+ import { downloadFile, downloadText } from '../util/download.js';
13
12
  import { run } from '../util/exec.js';
14
13
  import { ensureDir, exists, sha256 } from '../util/fs.js';
15
14
  import { PKG_ROOT } from '../util/pkg.js';
@@ -44,13 +43,19 @@ export async function currentVersion(ctx) {
44
43
  }
45
44
  }
46
45
  /** 从上游发布的 checksums.txt 取某版本的资产(engine upgrade <ver> 指定了清单外版本时用) */
47
- export async function fetchUpstreamAsset(manifest, version, target) {
46
+ export async function fetchUpstreamAsset(manifest, version, target, opts = {}) {
48
47
  const base = `https://github.com/${manifest.repo}/releases/download/v${version}`;
49
48
  const file = assetFileName(version, target);
50
- const res = await fetch(`${base}/checksums.txt`);
51
- if (!res.ok)
52
- throw new DshModelError('engine_version_not_found', L(`找不到引擎版本 v${version}(HTTP ${res.status})`, `Engine version v${version} not found (HTTP ${res.status})`));
53
- const line = (await res.text()).split('\n').find((l) => l.trim().endsWith(file));
49
+ let text;
50
+ try {
51
+ text = await downloadText(`${base}/checksums.txt`, join(tmpdir(), `dsh-model-checksums-${process.pid}-${Date.now()}.txt`), opts);
52
+ }
53
+ catch (error) {
54
+ if (isDshModelError(error) && error.code === 'download_not_found')
55
+ throw new DshModelError('engine_version_not_found', L(`找不到引擎版本 v${version}`, `Engine version v${version} not found`));
56
+ throw error;
57
+ }
58
+ const line = text.split('\n').find((l) => l.trim().endsWith(file));
54
59
  const sha = line?.trim().split(/\s+/)[0];
55
60
  if (!sha || !/^[0-9a-f]{64}$/.test(sha))
56
61
  throw new DshModelError('engine_checksum_missing', L(`v${version} 的 checksums.txt 里没有 ${file}`, `${file} is missing from v${version} checksums.txt`));
@@ -60,7 +65,7 @@ export async function fetchUpstreamAsset(manifest, version, target) {
60
65
  * 安装某版本到 versions/<v>/(已装就跳过)。返回是否真的装了。
61
66
  * DSH_MODEL_ENGINE_ARCHIVE 可指向本地 tar.gz(离线 / 测试),同样要过 sha256 校验。
62
67
  */
63
- export async function installVersion(ctx, version, asset) {
68
+ export async function installVersion(ctx, version, asset, opts = {}) {
64
69
  const bin = versionBinary(ctx, version);
65
70
  if (await exists(bin))
66
71
  return false;
@@ -75,10 +80,14 @@ export async function installVersion(ctx, version, asset) {
75
80
  data = await readFile(local);
76
81
  }
77
82
  else {
78
- const res = await fetch(asset.url);
79
- if (!res.ok || !res.body)
80
- throw new DshModelError('engine_download_failed', L(`下载引擎失败:HTTP ${res.status}`, `Engine download failed: HTTP ${res.status}`), asset.url);
81
- await pipeline(Readable.fromWeb(res.body), createWriteStream(archive, { mode: 0o600 }));
83
+ try {
84
+ await downloadFile(asset.url, archive, opts);
85
+ }
86
+ catch (error) {
87
+ if (isDshModelError(error))
88
+ throw new DshModelError('engine_download_failed', L(`下载引擎失败:${error.message.replace(/^下载失败:|^Download failed: /, '')}`, `Engine download failed: ${error.message.replace(/^下载失败:|^Download failed: /, '')}`), error.hint);
89
+ throw error;
90
+ }
82
91
  data = await readFile(archive);
83
92
  }
84
93
  const actual = sha256(data);
@@ -0,0 +1,69 @@
1
+ // util/download.ts — 下载文件(引擎发布包、checksums.txt)
2
+ //
3
+ // Node 的 fetch 不走系统代理,也不认 HTTPS_PROXY。国内直连 GitHub 的下载地址经常不通,
4
+ // Windows 上 setup 只报一句 "fetch failed"(实测)。所以设了代理时改用系统自带的 curl 走代理
5
+ // (macOS、Linux 都有;Windows 10 1803 起自带 curl.exe),没有 curl 再退回 fetch。
6
+ // 下载的内容一律由调用方按清单里的 sha256 校验,走哪条路都一样可信。
7
+ import { createWriteStream } from 'node:fs';
8
+ import { readFile, rm } from 'node:fs/promises';
9
+ import { Readable } from 'node:stream';
10
+ import { pipeline } from 'node:stream/promises';
11
+ import { DshModelError } from '../errors.js';
12
+ import { L } from '../i18n.js';
13
+ import { run, which } from './exec.js';
14
+ import { redactProxy } from './proxy.js';
15
+ /** fetch 抛的错里真正的原因("fetch failed" 的 cause:ENOTFOUND、ECONNRESET、UND_ERR_CONNECT_TIMEOUT…) */
16
+ export function fetchCause(error) {
17
+ const e = error;
18
+ const cause = e?.cause?.code ?? e?.cause?.message;
19
+ return cause ? `${e.message ?? 'fetch failed'}(${cause})` : String(e?.message ?? error);
20
+ }
21
+ /** 连不上时怎么办:开代理、指定代理,或手动下载后用本地文件 */
22
+ export function downloadHint(url, proxy) {
23
+ return proxy
24
+ ? L(`经代理 ${redactProxy(proxy)} 也没下载成功:确认代理软件开着、能打开 GitHub;或换一个代理(dsh-model setup --proxy <地址>)。也可以手动下载 ${url},把环境变量 DSH_MODEL_ENGINE_ARCHIVE 设成下载好的文件路径,再执行 dsh-model setup`, `Still failed through the proxy ${redactProxy(proxy)}: check the proxy app is running and can reach GitHub, or use another one (dsh-model setup --proxy <url>). You can also download ${url} yourself, set the environment variable DSH_MODEL_ENGINE_ARCHIVE to the file's path, and run dsh-model setup again`)
25
+ : L(`连不上 GitHub(下载引擎要用)。用代理的话:打开代理软件的"系统代理",或执行 dsh-model setup --proxy http://127.0.0.1:7890(换成你的代理端口)。也可以手动下载 ${url},把环境变量 DSH_MODEL_ENGINE_ARCHIVE 设成下载好的文件路径,再执行 dsh-model setup`, `Cannot reach GitHub (needed to download the engine). If you use a proxy: turn on its "system proxy", or run dsh-model setup --proxy http://127.0.0.1:7890 (your proxy's port). You can also download ${url} yourself, set the environment variable DSH_MODEL_ENGINE_ARCHIVE to the file's path, and run dsh-model setup again`);
26
+ }
27
+ /** 下载到 dest。HTTP 404 抛 code=download_not_found,其余失败抛 download_failed(带原因和建议) */
28
+ export async function downloadFile(url, dest, opts = {}) {
29
+ const curl = opts.proxy ? await which('curl') : null;
30
+ if (opts.proxy && curl) {
31
+ // --noproxy '':只用这里指定的代理,不受环境里 NO_PROXY 的影响
32
+ const r = await run(curl, ['-fsSL', '--retry', '2', '--connect-timeout', '20', '--proxy', opts.proxy, '--noproxy', '', '-o', dest, url], { timeoutMs: 10 * 60_000 });
33
+ if (r.code === 0)
34
+ return;
35
+ await rm(dest, { force: true });
36
+ // curl -f:HTTP 4xx/5xx 退出码 22,stderr 里有 "The requested URL returned error: 404"
37
+ if (/\b404\b/.test(r.stderr))
38
+ throw new DshModelError('download_not_found', L(`没有这个文件:${url}`, `No such file: ${url}`));
39
+ throw new DshModelError('download_failed', L(`下载失败:${r.stderr.trim() || `curl 退出码 ${r.code}`}`, `Download failed: ${r.stderr.trim() || `curl exit code ${r.code}`}`), downloadHint(url, opts.proxy));
40
+ }
41
+ let res;
42
+ try {
43
+ res = await fetch(url, { signal: AbortSignal.timeout(10 * 60_000) });
44
+ }
45
+ catch (error) {
46
+ throw new DshModelError('download_failed', L(`下载失败:${fetchCause(error)}`, `Download failed: ${fetchCause(error)}`), downloadHint(url, opts.proxy));
47
+ }
48
+ if (res.status === 404)
49
+ throw new DshModelError('download_not_found', L(`没有这个文件:${url}`, `No such file: ${url}`));
50
+ if (!res.ok || !res.body)
51
+ throw new DshModelError('download_failed', L(`下载失败:HTTP ${res.status}`, `Download failed: HTTP ${res.status}`), downloadHint(url, opts.proxy));
52
+ try {
53
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(dest, { mode: 0o600 }));
54
+ }
55
+ catch (error) {
56
+ await rm(dest, { force: true });
57
+ throw new DshModelError('download_failed', L(`下载中断:${fetchCause(error)}`, `Download interrupted: ${fetchCause(error)}`), downloadHint(url, opts.proxy));
58
+ }
59
+ }
60
+ /** 下载一个小文本文件(checksums.txt) */
61
+ export async function downloadText(url, tmpFile, opts = {}) {
62
+ try {
63
+ await downloadFile(url, tmpFile, opts);
64
+ return await readFile(tmpFile, 'utf8');
65
+ }
66
+ finally {
67
+ await rm(tmpFile, { force: true });
68
+ }
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "description": "Daily check-in credits claimed for you (Trae, Qoder, WorkBuddy China), one OpenAI-compatible address that relays every model, and free or subscription model sources (SiliconFlow, Gemini, Codex, Kimi, Ollama…). For DeepSeek Harness (dsh) and any OpenAI-compatible tool. 每天自动签到领积分 · 一个地址中转所有模型 · 免费和订阅模型随你接。",
5
5
  "keywords": [
6
6
  "siliconflow",
package/plugin/client.js CHANGED
@@ -1053,7 +1053,9 @@ window.__ModuleLoader__.load({
1053
1053
  h('div', { style: { ...S.line, marginTop: 6 } },
1054
1054
  st.qoder.configured
1055
1055
  ? h('button', { type: 'button', style: S.btn, onClick: () => { if (window.confirm(L('删除 Qoder 的令牌?之后不再给 Qoder 签到。', 'Remove the Qoder token? Qoder will no longer be checked in.'))) void api('DELETE', '/checkin/qoder').then(setSt, (e) => setErr(e.message)) } }, L('删除 Qoder 令牌', 'Remove Qoder token'))
1056
- : h('button', { type: 'button', style: S.btn, onClick: () => setQoder(true) }, L('添加 Qoder', 'Add Qoder')))) : null,
1056
+ : h('button', { type: 'button', style: S.btn, onClick: () => setQoder(true) }, L('添加 Qoder', 'Add Qoder'))),
1057
+ // Trae 读本机 Trae 的登录:这台没装(比如 VPS)就不会出现,在装了 Trae 的电脑上签就行(每个账号一天一次)
1058
+ !st.available.some((a) => a.id === 'trae') ? h('div', { style: { ...S.meta, marginTop: 6 } }, L('Trae:这台电脑上没装 Trae,所以不签。在装了 Trae 的电脑上会自动签(一个账号一天签一次就够)。', 'Trae: not installed on this machine, so it is skipped. A computer with Trae installed checks it in automatically (once a day per account is enough).')) : null) : null,
1057
1059
  !open && !st.qoder.configured ? h('div', { style: { ...S.meta, marginTop: 4 } }, h('button', { type: 'button', style: { ...S.btn, padding: '2px 8px' }, onClick: () => setQoder(true) }, L('+ 添加 Qoder', '+ Add Qoder'))) : null,
1058
1060
  err ? h('div', { style: { fontSize: 12, color: C.err, marginTop: 6 } }, err) : null),
1059
1061
  qoder ? h(QoderDialog, { onDone: () => { setQoder(false); void refresh() }, onClose: () => setQoder(false) }) : null)