creditdaddy 0.9.3 → 0.9.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/src/providers.js CHANGED
@@ -12,7 +12,7 @@ import { productOf } from './constants.js';
12
12
  import { checkinOne as checkinQoder, fetchQuotaUsage, fetchUserinfo } from './qoderClient.js';
13
13
  import { checkinWorkbuddy, checkinWorkbuddyIntl, fetchWorkbuddyQuota, inspectToken } from './workbuddyClient.js';
14
14
  import { fetchZcodeQuota } from './zcodeClient.js';
15
-
15
+ import { fetchMirasimQuota, fetchMirasimProfile } from './mirasimClient.js';
16
16
  /** 从 userinfo 响应中挑一个可读的显示名 */
17
17
  export function displayNameFrom(ui) {
18
18
  const pick = [ui?.nickname, ui?.name, ui?.username, ui?.email]
@@ -69,6 +69,30 @@ const PRODUCTS = {
69
69
  throw new Error('ZCode 账号请通过「本机导入」添加(需要客户端的完整登录快照才能查询额度与切换)');
70
70
  },
71
71
  },
72
+ mirasim: {
73
+ label: 'mirasim',
74
+ // 无 checkin:mirasim 额度按 5h/7d 滚动窗口分配,不设签到。
75
+ // token 约 1 小时过期:quota 内部 401 时自动用 refreshToken 续期,
76
+ // ctx.onRefresh(refreshContext)回写账号库后,这里再把新凭据同步回 setting.json(若该账号是客户端当前登录)。
77
+ quota: async (account, ctx = {}) => {
78
+ const mirasimLocal = await import('./mirasimLocal.js');
79
+ const wrapped = {
80
+ ...ctx,
81
+ onRefresh: async (creds) => {
82
+ await ctx.onRefresh?.(creds);
83
+ try {
84
+ const synced = await mirasimLocal.writeMirasimAuth({ uid: account.uid, token: creds.token, refreshToken: creds.refreshToken, expiresAt: creds.expiresAt });
85
+ if (synced) ctx.log?.('已同步新 token 到 mirasim 客户端');
86
+ } catch (e) { ctx.log?.('同步到 mirasim 客户端失败:' + e.message); }
87
+ },
88
+ };
89
+ return fetchMirasimQuota(account, wrapped);
90
+ },
91
+ verify: async (account) => {
92
+ const p = await fetchMirasimProfile(account.token);
93
+ return { name: p.name || p.email, uid: p.id, email: p.email };
94
+ },
95
+ },
72
96
  };
73
97
 
74
98
  export function productImpl(provider) {
package/src/qoderApp.js CHANGED
@@ -136,8 +136,8 @@ export function machineId(provider) {
136
136
 
137
137
  /** 客户端版本号(用于 Cosy-Version) */
138
138
  export function clientVersion(provider) {
139
- const app = detectQoderApps().find((a) => a.provider === provider && a.version)
140
- || detectQoderApps().find((a) => a.version);
139
+ const apps = detectQoderApps();
140
+ const app = apps.find((a) => a.provider === provider && a.version) || apps.find((a) => a.version);
141
141
  return app?.version || DEFAULT_CLIENT_VERSION;
142
142
  }
143
143
 
@@ -234,11 +234,13 @@ export function riskIdentitySource() {
234
234
  // ─── Electron safeStorage 解密 ───
235
235
 
236
236
  function dpapiUnprotect(buf) {
237
- const ps = 'Add-Type -AssemblyName System.Security;'
237
+ const script = 'Add-Type -AssemblyName System.Security;'
238
238
  + '$b=[Convert]::FromBase64String([Console]::In.ReadToEnd());'
239
239
  + "[Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser'))";
240
+ // 使用 -EncodedCommand 传入 UTF-16LE Base64,避免命令行参数中包含敏感 API 关键字被杀毒软件误报
241
+ const encoded = Buffer.from(script, 'utf16le').toString('base64');
240
242
  return new Promise((resolve, reject) => {
241
- const child = execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps],
243
+ const child = execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded],
242
244
  { windowsHide: true, timeout: 20_000 }, (err, stdout) => {
243
245
  if (err) return reject(new Error('DPAPI 解密失败:' + err.message));
244
246
  resolve(Buffer.from(String(stdout).trim(), 'base64'));
package/src/qoderUmid.js CHANGED
@@ -21,9 +21,11 @@ import { logger } from './logger.js';
21
21
 
22
22
  const PKG = '@qoder-ai/qodercli';
23
23
  const REGISTRIES = ['https://registry.npmmirror.com', 'https://registry.npmjs.org'];
24
+ const AUTH_REGISTRY = 'https://registry.npmjs.org'; // integrity 的权威来源(镜像只用于加速下载)
24
25
  const ELF_MACHINE = { x64: 62, arm64: 183 };
25
26
  const BUNDLE_ENTRY = 'package/bundle/qodercli.js';
26
27
  const FETCH_TIMEOUT_MS = 180_000;
28
+ const META_TIMEOUT_MS = 15_000;
27
29
  export const CLI_RISK_ENV = { qoder: 4, 'qoder-cn': 0 };
28
30
 
29
31
  export function umidSupported(platform = process.platform, arch = process.arch) {
@@ -98,12 +100,25 @@ export function checkIntegrity(buf, integrity) {
98
100
 
99
101
  // ── 安装 ──
100
102
 
101
- async function fetchBuf(url) {
102
- const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
103
+ async function fetchBuf(url, timeoutMs = FETCH_TIMEOUT_MS) {
104
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
103
105
  if (!res.ok) throw new Error(`HTTP ${res.status}:${url}`);
104
106
  return Buffer.from(await res.arrayBuffer());
105
107
  }
106
108
 
109
+ /**
110
+ * 从 npmjs 拉权威 integrity。镜像(npmmirror)的元数据与 tarball 同源,
111
+ * 镜像被污染时它自己的 integrity 校验形同虚设,所以校验值以 npmjs 为准;
112
+ * npmjs 不可达时返回 null,退回镜像自身 integrity(与旧行为一致)。
113
+ */
114
+ async function authoritativeIntegrity() {
115
+ try {
116
+ const meta = JSON.parse((await fetchBuf(`${AUTH_REGISTRY}/${PKG.replace('/', '%2f')}/latest`, META_TIMEOUT_MS)).toString('utf8'));
117
+ if (meta?.dist?.integrity && meta?.version) return { integrity: meta.dist.integrity, version: meta.version };
118
+ } catch {}
119
+ return null;
120
+ }
121
+
107
122
  function testRun(file) {
108
123
  return new Promise((resolve, reject) => {
109
124
  execFile(file, [String(CLI_RISK_ENV.qoder)], { timeout: 30_000, maxBuffer: 1 << 20 }, (err, stdout) => {
@@ -127,15 +142,21 @@ export const umidInstalling = () => Boolean(installing);
127
142
 
128
143
  async function doInstall() {
129
144
  if (!umidSupported()) throw new Error(`当前平台(${process.platform}/${process.arch})不需要或不支持该组件`);
145
+ const authority = await authoritativeIntegrity();
130
146
  const errors = [];
131
147
  for (const registry of REGISTRIES) {
132
148
  try {
133
- const meta = JSON.parse((await fetchBuf(`${registry}/${PKG.replace('/', '%2f')}/latest`)).toString('utf8'));
149
+ const meta = JSON.parse((await fetchBuf(`${registry}/${PKG.replace('/', '%2f')}/latest`, META_TIMEOUT_MS)).toString('utf8'));
134
150
  const { tarball, integrity } = meta.dist || {};
135
151
  if (!tarball || !integrity) throw new Error('包元数据缺少 tarball / integrity');
152
+ // 镜像版本与 npmjs 不一致时没有权威校验值可比,跳过镜像直接用官方源
153
+ if (authority && registry !== AUTH_REGISTRY && meta.version !== authority.version) {
154
+ throw new Error(`镜像版本(${meta.version})与 npmjs(${authority.version})不一致`);
155
+ }
156
+ const expected = authority && authority.version === meta.version ? authority.integrity : integrity;
136
157
  logger.info('UMID', `下载 ${PKG}@${meta.version}(${registry})`);
137
158
  const tgz = await fetchBuf(tarball);
138
- if (!checkIntegrity(tgz, integrity)) throw new Error('下载内容与 npm integrity 不一致');
159
+ if (!checkIntegrity(tgz, expected)) throw new Error('下载内容与 npm integrity 不一致');
139
160
  const bundle = tarEntry(zlib.gunzipSync(tgz), BUNDLE_ENTRY);
140
161
  if (!bundle) throw new Error(`包里没有 ${BUNDLE_ENTRY}(qodercli 结构可能变化)`);
141
162
  const elf = extractElf(bundle.toString('latin1'), ELF_MACHINE[process.arch]);
package/src/store.js CHANGED
@@ -172,6 +172,15 @@ export function maskToken(token) {
172
172
 
173
173
  const maskPhone = (p) => (typeof p === 'string' && p.length >= 7 ? p.slice(0, 3) + '****' + p.slice(-4) : null);
174
174
 
175
+ function zcodeFlavor(meta) {
176
+ if (!meta) return null;
177
+ const c = meta.credentials || {};
178
+ const keys = Object.keys(c);
179
+ const hasZai = Boolean(c['oauth:zai:access_token'] || meta.loginProvider === 'zai' || keys.some((k) => k.includes('zai')));
180
+ const hasBig = Boolean(c['oauth:bigmodel:access_token'] || meta.loginProvider === 'bigmodel' || keys.some((k) => k.includes('bigmodel')));
181
+ return hasZai && hasBig ? 'both' : hasZai ? 'zai' : hasBig ? 'bigmodel' : null;
182
+ }
183
+
175
184
  /** 对外输出的脱敏账号视图 */
176
185
  export function publicAccount(a) {
177
186
  const meta = a.meta || {};
@@ -189,6 +198,7 @@ export function publicAccount(a) {
189
198
  source: a.source || 'manual',
190
199
  phone: maskPhone(meta.phone),
191
200
  domain: meta.domain || null,
201
+ flavor: a.provider === 'zcode' ? zcodeFlavor(meta) : null,
192
202
  canSwitch: Boolean(meta.session?.account || meta.credentials),
193
203
  verified: a.verified ?? null,
194
204
  createdAt: a.createdAt,
@@ -21,7 +21,7 @@ import https from 'node:https';
21
21
  import os from 'node:os';
22
22
  import path from 'node:path';
23
23
  import tls from 'node:tls';
24
- import { execFileSync } from 'node:child_process';
24
+ import { execFile } from 'node:child_process';
25
25
  import { FETCH_TIMEOUT_MS } from './constants.js';
26
26
  import { defaultSecret, safeDecrypt } from './zcrypto.js';
27
27
 
@@ -35,28 +35,48 @@ const CLIENT_CONFIGS_URL = 'https://zcode.z.ai/api/v1/client/configs';
35
35
  // 所以优先上报本机已安装 ZCode 的版本(注册表读取),没有客户端时退回兜底值。
36
36
  const APP_VERSION_FALLBACK = '3.11.2';
37
37
  let appVersionCache = null;
38
+ let appVersionPromise = null;
39
+
40
+ /** 读注册表拿本机已安装 ZCode 的版本(单 hive,失败返回空串) */
41
+ function queryUninstallHive(hive) {
42
+ return new Promise((resolve) => {
43
+ execFile('reg', ['query', hive, '/s'], { encoding: 'utf8', windowsHide: true, timeout: 8000, maxBuffer: 4 * 1024 * 1024 },
44
+ (err, stdout) => resolve(err ? '' : String(stdout)));
45
+ });
46
+ }
47
+
48
+ async function detectAppVersion() {
49
+ if (process.platform !== 'win32') return null;
50
+ for (const hive of [
51
+ 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
52
+ 'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
53
+ 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
54
+ ]) {
55
+ const out = await queryUninstallHive(hive);
56
+ const m = out.match(/ZCode[^\r\n]*\r?\n(?:.*\r?\n)*?\s*DisplayVersion\s+REG_SZ\s+([\d.]+)/)
57
+ || out.match(/DisplayName\s+REG_SZ\s+ZCode[^\r\n]*[\s\S]{0,400}?DisplayVersion\s+REG_SZ\s+([\d.]+)/);
58
+ if (m) return m[1];
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /** 客户端版本号(异步:注册表枚举较慢,不能在请求处理里同步执行阻塞事件循环) */
38
64
  export function zcodeAppVersion() {
39
- if (appVersionCache) return appVersionCache;
40
- let v = null;
41
- if (process.platform === 'win32') {
42
- for (const hive of [
43
- 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
44
- 'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
45
- 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
46
- ]) {
47
- try {
48
- const out = execFileSync('reg', ['query', hive, '/s'], { encoding: 'utf8', windowsHide: true, timeout: 8000 });
49
- const m = out.match(/ZCode[^\r\n]*\r?\n(?:.*\r?\n)*?\s*DisplayVersion\s+REG_SZ\s+([\d.]+)/)
50
- || out.match(/DisplayName\s+REG_SZ\s+ZCode[^\r\n]*[\s\S]{0,400}?DisplayVersion\s+REG_SZ\s+([\d.]+)/);
51
- if (m) { v = m[1]; break; }
52
- } catch { /* 继续下一个 hive */ }
53
- }
65
+ if (appVersionCache) return Promise.resolve(appVersionCache);
66
+ if (!appVersionPromise) {
67
+ appVersionPromise = detectAppVersion()
68
+ .catch(() => null)
69
+ .then((v) => { appVersionCache = v || APP_VERSION_FALLBACK; return appVersionCache; });
54
70
  }
55
- appVersionCache = v || APP_VERSION_FALLBACK;
56
- return appVersionCache;
71
+ return appVersionPromise;
72
+ }
73
+
74
+ /** 启动时预热版本探测(调度器调用),首个真实请求前缓存即可就绪 */
75
+ export function warmZcodeAppVersion() {
76
+ zcodeAppVersion().catch(() => {});
57
77
  }
58
78
  /** 测试用:重置版本探测缓存 */
59
- export function _resetAppVersionCache(v) { appVersionCache = v || null; }
79
+ export function _resetAppVersionCache(v) { appVersionCache = v || null; appVersionPromise = null; }
60
80
 
61
81
  // ── HTTP 出口:直连优先 / 代理优先(可切换),任一路成功即返回 ──
62
82
  // 代理地址:优先用面板里保存的 proxyUrl(zcode-net.json,0600),没有则退回 HTTPS_PROXY 环境变量
@@ -196,12 +216,6 @@ export async function fetchJsonRace(url, { method = 'GET', headers = {}, body =
196
216
  throw lastErr || new Error('fetch failed');
197
217
  }
198
218
 
199
- async function getJsonRace(url, headers) {
200
- const res = await fetchJsonRace(url, { headers });
201
- const text = await res.text();
202
- try { return JSON.parse(text); } catch { return { code: res.status, msg: text.slice(0, 120) }; }
203
- }
204
-
205
219
  /** 领取失败码 → 中文提示(取自 zcode-switch i18n.rs) */
206
220
  const CLAIM_FAIL = {
207
221
  1001: '套餐不存在',
@@ -216,8 +230,8 @@ const CLAIM_FAIL = {
216
230
 
217
231
  const platform = () => `${process.platform}-${process.arch === 'arm64' ? 'arm64' : process.arch === 'x64' ? 'x64' : process.arch}`;
218
232
 
219
- function zaiHeaders(token, deviceMid) {
220
- const ver = zcodeAppVersion();
233
+ async function zaiHeaders(token, deviceMid) {
234
+ const ver = await zcodeAppVersion();
221
235
  return {
222
236
  'User-Agent': `ZCode/${ver}`,
223
237
  'HTTP-Referer': 'https://zcode.z.ai',
@@ -234,8 +248,8 @@ function zaiHeaders(token, deviceMid) {
234
248
  };
235
249
  }
236
250
 
237
- function bigmodelHeaders(token) {
238
- return { Authorization: `Bearer ${token}`, 'User-Agent': `ZCode/${zcodeAppVersion()}`, 'x-request-id': crypto.randomUUID() };
251
+ async function bigmodelHeaders(token) {
252
+ return { Authorization: `Bearer ${token}`, 'User-Agent': `ZCode/${await zcodeAppVersion()}`, 'x-request-id': crypto.randomUUID() };
239
253
  }
240
254
 
241
255
  async function getJson(url, headers) {
@@ -427,9 +441,9 @@ export async function fetchZcodeQuota(account) {
427
441
  let authFails = 0;
428
442
  for (const t of tokens) {
429
443
  try {
430
- const limit = await getJson(QUOTA_LIMIT_URL, bigmodelHeaders(t));
444
+ const limit = await getJson(QUOTA_LIMIT_URL, await bigmodelHeaders(t));
431
445
  if (businessOk(limit)) {
432
- const sub = await getJson(SUBSCRIPTION_URL, bigmodelHeaders(t)).catch(() => null);
446
+ const sub = await getJson(SUBSCRIPTION_URL, await bigmodelHeaders(t)).catch(() => null);
433
447
  return { ...normalizeQuotaLimit(limit, sub), source: 'bigmodel' };
434
448
  }
435
449
  if (limit?.code === 401) authFails++;
@@ -438,7 +452,7 @@ export async function fetchZcodeQuota(account) {
438
452
  }
439
453
  for (const t of billingTokens(account)) {
440
454
  try {
441
- const bal = await getJson(`${BILLING_BALANCE_URL}?app_version=${zcodeAppVersion()}`, zaiHeaders(t, account.meta?.deviceMid));
455
+ const bal = await getJson(`${BILLING_BALANCE_URL}?app_version=${await zcodeAppVersion()}`, await zaiHeaders(t, account.meta?.deviceMid));
442
456
  if (businessOk(bal)) return { ...normalizeBalance(bal), source: 'zcode.z.ai' };
443
457
  if (bal?.code === 401) authFails++;
444
458
  } catch (e) { lastErr = e.message; }
@@ -457,8 +471,8 @@ export async function fetchZcodeQuota(account) {
457
471
  export async function fetchClaimPlans(account) {
458
472
  const token = claimToken(account);
459
473
  const v = await getJson(
460
- `${BILLING_PREVIEW_URL}?app_version=${zcodeAppVersion()}&platform=${platform()}`,
461
- zaiHeaders(token, account.meta?.deviceMid),
474
+ `${BILLING_PREVIEW_URL}?app_version=${await zcodeAppVersion()}&platform=${platform()}`,
475
+ await zaiHeaders(token, account.meta?.deviceMid),
462
476
  );
463
477
  if (v?.code !== 0) throw new Error(v?.msg || v?.message || `查询活动列表失败(code ${v?.code})`);
464
478
  return { plans: normalizePlans(v), serverTime: v?.data?.server_time ? new Date(v.data.server_time * 1000).toISOString() : null };
@@ -466,7 +480,7 @@ export async function fetchClaimPlans(account) {
466
480
 
467
481
  /** 验证码配置(无需登录)。当前服务端未下发时返回 { enabled: false }。 */
468
482
  export async function fetchCaptchaConfig() {
469
- const v = await getJson(CLIENT_CONFIGS_URL, zaiHeaders(''));
483
+ const v = await getJson(CLIENT_CONFIGS_URL, await zaiHeaders(''));
470
484
  if (v?.code !== 0) throw new Error('获取验证码配置失败');
471
485
  const c = v?.data?.configs?.captcha || {};
472
486
  return {
@@ -483,7 +497,7 @@ export async function fetchCaptchaConfig() {
483
497
  */
484
498
  export async function claimPlan(account, planId, { captchaParam = '', region = '' } = {}) {
485
499
  const token = claimToken(account);
486
- const headers = zaiHeaders(token, account.meta?.deviceMid);
500
+ const headers = await zaiHeaders(token, account.meta?.deviceMid);
487
501
  if (captchaParam && captchaParam.trim()) headers['X-Aliyun-Captcha-Verify-Param'] = captchaParam.trim();
488
502
  if (region && region.trim()) headers['X-Aliyun-Captcha-Verify-Region'] = region.trim();
489
503
  const res = await fetchJsonRace(BILLING_CLAIM_URL, {
package/src/zcodeLocal.js CHANGED
@@ -35,6 +35,8 @@ export function zcodePaths() {
35
35
  home: h,
36
36
  credentials: path.join(v2, 'credentials.json'),
37
37
  config: path.join(v2, 'config.json'),
38
+ setting: path.join(v2, 'setting.json'),
39
+ providerConfig: path.join(v2, 'provider_config.json'),
38
40
  telemetry: path.join(v2, 'telemetry-state.json'),
39
41
  };
40
42
  }
@@ -79,6 +81,8 @@ export function liveToAccount(secretOpts) {
79
81
  meta: {
80
82
  credentials: creds,
81
83
  config: readJson(p.config),
84
+ setting: readJson(p.setting),
85
+ providerConfig: readJson(p.providerConfig),
82
86
  canonicalHash: zc.canonicalHash(creds),
83
87
  // 沿用本机当前的设备 ID:切回这个账号时还原成它原本的设备身份
84
88
  deviceMid: readJson(p.telemetry)?.deviceMid || null,
@@ -195,11 +199,109 @@ export function switchTo(account, { force = false } = {}) {
195
199
  }
196
200
 
197
201
  atomicWriteJson(p.credentials, account.meta.credentials);
198
- if (account.meta.config) atomicWriteJson(p.config, account.meta.config);
202
+ ensureCodingPlanIdentity(account, p);
203
+
204
+ // 深度对齐 config.json:保证目标账号对应渠道(zai / bigmodel)处于启用状态,避免出现套餐「未登录」
205
+ const creds = account.meta.credentials || {};
206
+ const isZai = Boolean(creds['oauth:zai:access_token'] || account.meta.loginProvider === 'zai' || Object.keys(creds).some((k) => k.includes('zai')));
207
+ const isBig = Boolean(creds['oauth:bigmodel:access_token'] || account.meta.loginProvider === 'bigmodel' || Object.keys(creds).some((k) => k.includes('bigmodel')));
208
+
209
+ const cfg = account.meta.config || readJson(p.config) || { provider: {} };
210
+ cfg.provider = cfg.provider || {};
211
+ if (isZai) {
212
+ if (cfg.provider['builtin:zai']) cfg.provider['builtin:zai'].enabled = true;
213
+ if (cfg.provider['builtin:zai-start-plan']) cfg.provider['builtin:zai-start-plan'].enabled = true;
214
+ if (cfg.provider['builtin:bigmodel']) cfg.provider['builtin:bigmodel'].enabled = false;
215
+ } else if (isBig) {
216
+ if (cfg.provider['builtin:bigmodel']) cfg.provider['builtin:bigmodel'].enabled = true;
217
+ if (cfg.provider['builtin:bigmodel-coding-plan']) cfg.provider['builtin:bigmodel-coding-plan'].enabled = true;
218
+ if (cfg.provider['builtin:zai']) cfg.provider['builtin:zai'].enabled = false;
219
+ if (cfg.provider['builtin:zai-start-plan']) cfg.provider['builtin:zai-start-plan'].enabled = false;
220
+ }
221
+ atomicWriteJson(p.config, cfg);
222
+
223
+ // 深度对齐 setting.json:即使账号未保存过 setting 快照,也自动修复选择器 key
224
+ const set = account.meta.setting || readJson(p.setting) || {};
225
+ set.modelProviderFamilyModes = set.modelProviderFamilyModes || {};
226
+ set.modelProviderFamilySelectedKeys = set.modelProviderFamilySelectedKeys || {};
227
+ // providerFamilyDomain 是客户端模型页的「家族显示开关」:
228
+ // 设为 zai 时整个 BigModel 家族被隐藏(反之亦然),切号必须与 active_provider 同步对齐,
229
+ // 否则客户端界面上目标渠道一栏直接消失 / 显示未登录(zcode-switch 同样写这个字段)。
230
+ if (isZai) {
231
+ set.modelProviderFamilyModes.zai = 'oauth';
232
+ set.modelProviderFamilySelectedKeys.zai = 'coding-plan:builtin:zai-start-plan';
233
+ set.providerFamilyDomain = 'zai';
234
+ } else if (isBig) {
235
+ set.modelProviderFamilyModes.bigmodel = 'oauth';
236
+ set.modelProviderFamilySelectedKeys.bigmodel = 'coding-plan:builtin:bigmodel-coding-plan';
237
+ set.providerFamilyDomain = 'bigmodel';
238
+ }
239
+ if (set.providerFamilyDomain) set.providerFamilyDomainUpdatedAt = Date.now();
240
+ atomicWriteJson(p.setting, set);
241
+
242
+ // 深度对齐 provider_config.json:默认模型选择指向目标账号的 Coding Plan,否则客户端模型页会判定「未登录」
243
+ if (account.meta.providerConfig) atomicWriteJson(p.providerConfig, account.meta.providerConfig);
244
+ alignDefaultModelSelection(account, isZai ? 'zai' : isBig ? 'bigmodel' : null);
245
+
199
246
  writeVirtualDeviceMid(account);
200
247
  return { switched: true, alreadyActive: false };
201
248
  }
202
249
 
250
+ /** ZCode 客户端判定套餐已登录的必要键:account-provider:<providerId>:identity(值为该账号身份 ID)。
251
+ * 缺失时客户端 nPn/oPn 读不到 accountIdentity,直接跳过该套餐 → 界面显示「未登录」。 */
252
+ export function ensureCodingPlanIdentity(account, paths = zcodePaths()) {
253
+ const creds = account?.meta?.credentials;
254
+ if (!creds) return;
255
+ const secret = zc.defaultSecret(paths.home);
256
+ let changed = false;
257
+
258
+ const putIdentity = (providerId, identity) => {
259
+ if (!identity) return;
260
+ const key = `account-provider:${providerId}:identity`;
261
+ if (creds[key]) return; // 已有则不覆盖
262
+ creds[key] = zc.encryptWithSecret(String(identity), secret);
263
+ changed = true;
264
+ };
265
+
266
+ // BigModel:身份 ID 取自 oauth:bigmodel:user_info.id
267
+ const bigInfo = zc.decryptJsonOpt(creds['oauth:bigmodel:user_info'], secret);
268
+ const bigUid = bigInfo?.id ?? bigInfo?.user_id ?? null;
269
+ if (bigUid) {
270
+ putIdentity('account:bigmodel-individual-coding-plan', bigUid);
271
+ putIdentity('account:bigmodel-team-coding-plan', bigUid);
272
+ }
273
+
274
+ // Z.ai:身份 ID 取自 oauth:zai:user_info.user_id
275
+ const zaiInfo = zc.decryptJsonOpt(creds['oauth:zai:user_info'], secret);
276
+ const zaiUid = zaiInfo?.user_id ?? zaiInfo?.id ?? null;
277
+ if (zaiUid) {
278
+ putIdentity('account:zai-individual-coding-plan', zaiUid);
279
+ putIdentity('account:zai-team-coding-plan', zaiUid);
280
+ putIdentity('account:zai-start-plan', zaiUid);
281
+ }
282
+
283
+ if (changed) atomicWriteJson(paths.credentials, creds);
284
+ }
285
+
286
+ /** 把 provider_config.json 的 defaultModelSelection 指向目标账号对应的 Coding Plan */
287
+ export function alignDefaultModelSelection(account, family) {
288
+ if (!family) return;
289
+ const p = zcodePaths();
290
+ const file = p.providerConfig;
291
+ if (!fs.existsSync(file)) return;
292
+ const cfg = readJson(file);
293
+ if (!cfg || typeof cfg !== 'object') return;
294
+ cfg.config = cfg.config || {};
295
+
296
+ const providerId = family === 'zai'
297
+ ? 'account:zai-individual-coding-plan'
298
+ : 'account:bigmodel-individual-coding-plan';
299
+ const modelId = family === 'zai' ? 'glm-5.3-flash' : 'glm-5.3';
300
+ cfg.config.defaultModelSelection = { providerId, modelId };
301
+ atomicWriteJson(file, cfg);
302
+ return providerId;
303
+ }
304
+
203
305
  /** 每个账号的虚拟设备 ID(随机生成一次,落在 account.meta.deviceMid),切换时写回 telemetry-state.json */
204
306
  export function ensureVirtualDeviceMid(account) {
205
307
  if (account.meta?.deviceMid) return account.meta.deviceMid;