dsh-model 0.23.3 → 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.
@@ -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()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model",
3
- "version": "0.23.3",
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)