creditdaddy 1.1.1 → 1.2.0

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/constants.js CHANGED
@@ -1,95 +1,97 @@
1
- /**
2
- * CreditDaddy 常量 — 支持的产品线与 Qoder 官方 OpenAPI 端点(WorkBuddy 端点见 workbuddyClient.js)。
3
- *
4
- * 端点与请求头结构来自社区逆向成果(10router 项目实测可用):
5
- * openapi.qoder.sh 国际版:活动/签到、userinfo、配额、PAT 兑换
6
- * openapi.qoder.com.cn 国内版:同上
7
- *
8
- * ⚠️ 非官方接口,Qoder 调整服务端时可能失效。
9
- */
10
-
11
- import { readFileSync } from 'node:fs';
12
-
13
- export const OPENAPI_BASE = 'https://openapi.qoder.sh';
14
- export const CN_OPENAPI_BASE = 'https://openapi.qoder.com.cn';
15
-
16
- // 网页端(账号设置 → 用量明细)同源 API:逐资源包明细只在这里提供,且只认浏览器登录后的会话 Cookie。
17
- export const WEB_BASE = 'https://qoder.com';
18
- export const CN_WEB_BASE = 'https://qoder.cn';
19
-
20
- export const LOGIN_URL = 'https://qoder.com/device/selectAccounts';
21
- export const CN_LOGIN_URL = 'https://qoder.cn/device/selectAccounts';
22
-
23
- // 活动(签到)列表与领取
24
- export const CAMPAIGNS_PATH = '/sash/api/v1/me/campaigns?clientType=10';
25
- export const CAMPAIGN_CLAIM_PATH = (id) => `/sash/api/v1/me/campaigns/${encodeURIComponent(id)}/claim`;
26
-
27
- // 账号信息与配额
28
- export const USERINFO_PATH = '/api/v1/userinfo';
29
- export const QUOTA_USAGE_PATH = '/api/v2/quota/usage';
30
- // 网页端逐资源包用量明细(plan / 个人资源包 / 组织资源包,含每包到期时间)。
31
- // 仅接受浏览器登录的会话 Cookie(dt- / jt- token 一律 401),见 qoderClient.fetchUsageDetail。
32
- export const WEB_USAGE_PATH = '/api/v2/me/usages/big_model_credits';
33
-
34
- // PAT (pt-...) → 短期 job token (jt-...) 兑换(普通 JSON POST,无需 COSY 签名)
35
- export const JOB_TOKEN_EXCHANGE_PATH = '/api/v1/jobToken/exchange';
36
-
37
- // 账号的 provider = 产品 + 区域。新增产品时在此登记,并在 providers.js 挂上对应实现。
38
- export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw'];
39
- export const PROVIDER_LABEL = {
40
- qoder: 'Qoder 国际版',
41
- 'qoder-cn': 'Qoder 国内版',
42
- workbuddy: 'WorkBuddy 国内版',
43
- 'workbuddy-intl': 'WorkBuddy 国际版',
44
- zcode: 'ZCode',
45
- mirasim: 'mirasim',
46
- catpaw: '妙手',
47
- };
48
- /** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw) */
49
- export const productOf = (provider) => {
50
- const p = String(provider);
51
- if (p.startsWith('workbuddy')) return 'workbuddy';
52
- if (p.startsWith('zcode')) return 'zcode';
53
- if (p === 'mirasim') return 'mirasim';
54
- if (p === 'catpaw') return 'catpaw';
55
- return 'qoder';
56
- };
57
- /** provider 是否国内版 */
58
- export const isCnProvider = (provider) => provider === 'qoder-cn' || provider === 'workbuddy';
59
-
60
- /** Qoder 网页端同源 API 的 host(用量明细逐资源包接口在这里,只认浏览器会话 Cookie) */
61
- export function webBase(provider) {
62
- return provider === 'qoder-cn' ? CN_WEB_BASE : WEB_BASE;
63
- }
64
-
65
- /** 签到请求头(源自 10router qoderCheckin.js,clientType=10 与官方客户端一致) */
66
- export function buildQoderHeaders(token) {
67
- return {
68
- Authorization: `Bearer ${token}`,
69
- 'Cosy-ClientType': '10',
70
- 'Cosy-Version': '0.3.3',
71
- 'Cosy-MachineOS':
72
- process.platform === 'win32' ? 'windows'
73
- : process.platform === 'darwin' ? 'macos' : 'linux',
74
- 'User-Agent': 'Qoder',
75
- Accept: 'application/json',
76
- };
77
- }
78
-
79
- /** PAT 兑换请求头(源自 10router qoderModels.js,模拟官方 qodercli) */
80
- export function buildExchangeHeaders() {
81
- return {
82
- 'Content-Type': 'application/json',
83
- Accept: 'application/json',
84
- 'User-Agent': 'qodercli/1.0.0',
85
- 'Cosy-Version': '1.0.0',
86
- 'Cosy-ClientType': '5',
87
- };
88
- }
89
-
90
- export const FETCH_TIMEOUT_MS = 15000;
91
-
92
- // 版本号 / 项目主页以 package.json 为唯一来源(桌面版 / fpk 打包时都会一并拷入 package.json)
93
- const PKG = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
94
- export const APP_VERSION = PKG.version;
95
- export const PROJECT_URL = PKG.homepage || 'https://github.com/techysy/CreditDaddy';
1
+ /**
2
+ * CreditDaddy 常量 — 支持的产品线与 Qoder 官方 OpenAPI 端点(WorkBuddy 端点见 workbuddyClient.js)。
3
+ *
4
+ * 端点与请求头结构来自社区逆向成果(10router 项目实测可用):
5
+ * openapi.qoder.sh 国际版:活动/签到、userinfo、配额、PAT 兑换
6
+ * openapi.qoder.com.cn 国内版:同上
7
+ *
8
+ * ⚠️ 非官方接口,Qoder 调整服务端时可能失效。
9
+ */
10
+
11
+ import { readFileSync } from 'node:fs';
12
+
13
+ export const OPENAPI_BASE = 'https://openapi.qoder.sh';
14
+ export const CN_OPENAPI_BASE = 'https://openapi.qoder.com.cn';
15
+
16
+ // 网页端(账号设置 → 用量明细)同源 API:逐资源包明细只在这里提供,且只认浏览器登录后的会话 Cookie。
17
+ export const WEB_BASE = 'https://qoder.com';
18
+ export const CN_WEB_BASE = 'https://qoder.cn';
19
+
20
+ export const LOGIN_URL = 'https://qoder.com/device/selectAccounts';
21
+ export const CN_LOGIN_URL = 'https://qoder.cn/device/selectAccounts';
22
+
23
+ // 活动(签到)列表与领取
24
+ export const CAMPAIGNS_PATH = '/sash/api/v1/me/campaigns?clientType=10';
25
+ export const CAMPAIGN_CLAIM_PATH = (id) => `/sash/api/v1/me/campaigns/${encodeURIComponent(id)}/claim`;
26
+
27
+ // 账号信息与配额
28
+ export const USERINFO_PATH = '/api/v1/userinfo';
29
+ export const QUOTA_USAGE_PATH = '/api/v2/quota/usage';
30
+ // 网页端逐资源包用量明细(plan / 个人资源包 / 组织资源包,含每包到期时间)。
31
+ // 仅接受浏览器登录的会话 Cookie(dt- / jt- token 一律 401),见 qoderClient.fetchUsageDetail。
32
+ export const WEB_USAGE_PATH = '/api/v2/me/usages/big_model_credits';
33
+
34
+ // PAT (pt-...) → 短期 job token (jt-...) 兑换(普通 JSON POST,无需 COSY 签名)
35
+ export const JOB_TOKEN_EXCHANGE_PATH = '/api/v1/jobToken/exchange';
36
+
37
+ // 账号的 provider = 产品 + 区域。新增产品时在此登记,并在 providers.js 挂上对应实现。
38
+ export const PROVIDERS = ['qoder', 'qoder-cn', 'workbuddy', 'workbuddy-intl', 'zcode', 'mirasim', 'catpaw', 'trae'];
39
+ export const PROVIDER_LABEL = {
40
+ qoder: 'Qoder 国际版',
41
+ 'qoder-cn': 'Qoder 国内版',
42
+ workbuddy: 'WorkBuddy 国内版',
43
+ 'workbuddy-intl': 'WorkBuddy 国际版',
44
+ zcode: 'ZCode',
45
+ mirasim: 'mirasim',
46
+ catpaw: '妙手',
47
+ trae: 'Trae',
48
+ };
49
+ /** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw / trae) */
50
+ export const productOf = (provider) => {
51
+ const p = String(provider);
52
+ if (p.startsWith('workbuddy')) return 'workbuddy';
53
+ if (p.startsWith('zcode')) return 'zcode';
54
+ if (p === 'mirasim') return 'mirasim';
55
+ if (p === 'catpaw') return 'catpaw';
56
+ if (p.startsWith('trae')) return 'trae';
57
+ return 'qoder';
58
+ };
59
+ /** provider 是否国内版 */
60
+ export const isCnProvider = (provider) => provider === 'qoder-cn' || provider === 'workbuddy';
61
+
62
+ /** Qoder 网页端同源 API 的 host(用量明细逐资源包接口在这里,只认浏览器会话 Cookie) */
63
+ export function webBase(provider) {
64
+ return provider === 'qoder-cn' ? CN_WEB_BASE : WEB_BASE;
65
+ }
66
+
67
+ /** 签到请求头(源自 10router qoderCheckin.js,clientType=10 与官方客户端一致) */
68
+ export function buildQoderHeaders(token) {
69
+ return {
70
+ Authorization: `Bearer ${token}`,
71
+ 'Cosy-ClientType': '10',
72
+ 'Cosy-Version': '0.3.3',
73
+ 'Cosy-MachineOS':
74
+ process.platform === 'win32' ? 'windows'
75
+ : process.platform === 'darwin' ? 'macos' : 'linux',
76
+ 'User-Agent': 'Qoder',
77
+ Accept: 'application/json',
78
+ };
79
+ }
80
+
81
+ /** PAT 兑换请求头(源自 10router qoderModels.js,模拟官方 qodercli) */
82
+ export function buildExchangeHeaders() {
83
+ return {
84
+ 'Content-Type': 'application/json',
85
+ Accept: 'application/json',
86
+ 'User-Agent': 'qodercli/1.0.0',
87
+ 'Cosy-Version': '1.0.0',
88
+ 'Cosy-ClientType': '5',
89
+ };
90
+ }
91
+
92
+ export const FETCH_TIMEOUT_MS = 15000;
93
+
94
+ // 版本号 / 项目主页以 package.json 为唯一来源(桌面版 / fpk 打包时都会一并拷入 package.json)
95
+ const PKG = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
96
+ export const APP_VERSION = PKG.version;
97
+ export const PROJECT_URL = PKG.homepage || 'https://github.com/techysy/CreditDaddy';
package/src/daemon.js CHANGED
@@ -56,11 +56,12 @@ import { readWorkbuddySessions, writeWorkbuddySession, workbuddyAuthDir, current
56
56
  import { liveToAccount as zcodeLiveAccount, switchTo as zcodeSwitchTo, currentZcodeUid, currentZcodeIdentity, detectZcode, ensureVirtualDeviceMid, terminateZcode, zcodeRunning } from './zcodeLocal.js';
57
57
  import { liveToAccount as mirasimLiveAccount, switchTo as mirasimSwitchTo, currentMirasimUid, detectMirasim, terminateMirasim, mirasimRunning } from './mirasimLocal.js';
58
58
  import { liveToAccount as catpawLiveAccount, switchTo as catpawSwitchTo, currentCatpawToken, detectCatpaw, terminateCatpaw } from './catpawLocal.js';
59
- import { fetchClaimPlans, claimPlan, fetchCaptchaConfig, proxyFirst, setProxyFirst, proxyUrl, setProxyUrl, autoClaimEnabled, autoClaimUntil, setAutoClaimEnabled } from './zcodeClient.js';
59
+ import { liveToAccount as traeLiveAccount, detectTrae, switchTo as traeSwitchTo, snapshotLive as traeSnapshotLive } from './traeLocal.js';
60
+ import { fetchClaimPlans, claimPlan, fetchCaptchaConfig, proxyFirst, setProxyFirst, proxyUrl, setProxyUrl, autoClaimEnabled, autoClaimUntil, setAutoClaimEnabled, claimIntervalMin, setClaimIntervalMin, claimWindowMin, setClaimWindowMin } from './zcodeClient.js';
60
61
  import { exportAccounts, parseImport, TransferError } from './transfer.js';
61
62
  import { syncAccountsTo10r } from './tenrouterAccounts.js';
62
- import { runCheckinTick, getSchedulerInfo, dayKey, enableZcodeAutoClaimForOneHour, refreshZcodeScheduler, pollZcodeNow } from './checkin.js';
63
- import { detectQoderApps, readQoderAppAccounts, riskIdentityAvailable, riskIdentitySource } from './qoderApp.js';
63
+ import { runCheckinTick, getSchedulerInfo, dayKey, enableZcodeAutoClaimWindow, refreshZcodeScheduler, pollZcodeNow } from './checkin.js';
64
+ import { detectQoderApps, readQoderAppAccounts, riskIdentityAvailable, riskIdentitySource, switchTo as qoderSwitchTo, qoderRunning, terminateQoder } from './qoderApp.js';
64
65
  import { umidInfo, installUmid } from './qoderUmid.js';
65
66
  import * as tenrouter from './tenrouter.js';
66
67
  import * as zcodeGateway from './zcodeGateway.js';
@@ -68,7 +69,7 @@ import { startDeviceFlow, pollDeviceFlow, LOGIN_KINDS } from './authDevice.js';
68
69
  import { detectInstalls, scanLocalTokens, putCandidate, peekCandidate } from './localDetect.js';
69
70
  import { PROVIDER_LABEL, APP_VERSION, PROVIDERS, PROJECT_URL } from './constants.js';
70
71
 
71
- const PRODUCT_IDS = ['qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw'];
72
+ const PRODUCT_IDS = ['qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae'];
72
73
 
73
74
  /** 面板访问密码:面板「设置」写入的 settings.json.panelKey 优先(可设 / 可关);
74
75
  * 未设置(或被面板关闭)时回退环境变量 CREDITDADDY_PASSWORD(fnOS / 命令行部署注入),
@@ -88,6 +89,22 @@ const DEFAULT_PORT = 47860;
88
89
  const MAX_BODY_BYTES = 2 * 1024 * 1024;
89
90
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
90
91
 
92
+ // Qoder 当前登录缓存:面板 30s 一轮 status,DPAPI 解密结果没必要每轮重算
93
+ let qoderCurCache = { at: 0, data: null };
94
+ async function qoderCurrentInfo() {
95
+ if (qoderCurCache.data && Date.now() - qoderCurCache.at < 60_000) return qoderCurCache.data;
96
+ const data = { apps: detectQoderApps(), uids: {}, backfill: [] };
97
+ try {
98
+ const r = await readQoderAppAccounts();
99
+ for (const c of r.accounts) {
100
+ data.uids[c.provider] = c.user.id || null;
101
+ if (c.user.id) data.backfill.push({ provider: c.provider, uid: c.user.id, authJson: c.authJson, file: c.file });
102
+ }
103
+ } catch { /* 解密失败只留检测信息 */ }
104
+ qoderCurCache = { at: Date.now(), data };
105
+ return data;
106
+ }
107
+
91
108
  class HttpError extends Error {
92
109
  constructor(status, message) { super(message); this.status = status; }
93
110
  }
@@ -350,9 +367,19 @@ async function handleApi(req, res, url) {
350
367
  if (p === '/api/zcode-gateway' && method === 'PUT') {
351
368
  const body = await readBody(req).catch(() => ({}));
352
369
  if (body?.enabled !== undefined) await zcodeGateway.setGatewayEnabled(body.enabled === true);
353
- if (body?.lan !== undefined) {
354
- await (await import('./store.js')).saveSettings({ zcodeGatewayLan: body.lan === true });
355
- logger.info('DAEMON', `ZCode 网关局域网访问:${body.lan === true ? '开(重启后绑定 0.0.0.0)' : '关(仅本机)'}`);
370
+ if (body?.lan !== undefined || body?.allow !== undefined) {
371
+ const patch = {};
372
+ if (body?.lan !== undefined) patch.zcodeGatewayLan = body.lan === true;
373
+ if (body?.allow !== undefined) {
374
+ if (!Array.isArray(body.allow)) return json(res, 400, { error: 'allow 需为字符串数组' });
375
+ const cleaned = [...new Set(body.allow.map((x) => String(x || '').trim()).filter(Boolean))];
376
+ if (cleaned.some((x) => /[^\w.*:-]/.test(x))) return json(res, 400, { error: '白名单条目只能是 IP / 主机名(支持 192.168.31.* 通配)' });
377
+ patch.zcodeGatewayAllow = cleaned;
378
+ }
379
+ await (await import('./store.js')).saveSettings(patch);
380
+ const lanNow = patch.zcodeGatewayLan ?? (await (await import('./store.js')).loadSettings()).zcodeGatewayLan === true;
381
+ const allowText = patch.zcodeGatewayAllow ? patch.zcodeGatewayAllow.join(', ') : '';
382
+ logger.info('DAEMON', `ZCode 网关局域网:${lanNow ? '开' : '关(仅本机)'}${allowText ? `,白名单:${allowText}` : ''}(绑定改动重启后生效)`);
356
383
  }
357
384
  return json(res, 200, await zcodeGateway.gatewayStatus());
358
385
  }
@@ -360,21 +387,33 @@ async function handleApi(req, res, url) {
360
387
  const u = proxyUrl();
361
388
  const masked = u ? (() => { try { const x = new URL(u); x.password = x.password ? '*'.repeat(4) : ''; return x.toString(); } catch { return '***'; } })() : null;
362
389
  const ac = autoClaimEnabled();
363
- return json(res, 200, { proxyFirst: proxyFirst(), proxyUrlMasked: masked, autoClaim: ac, autoClaimUntil: ac ? autoClaimUntil() : null, hasEnvProxy: Boolean(process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy) });
390
+ return json(res, 200, {
391
+ proxyFirst: proxyFirst(), proxyUrlMasked: masked,
392
+ autoClaim: ac, autoClaimUntil: ac ? autoClaimUntil() : null,
393
+ claimIntervalMin: claimIntervalMin(), claimWindowMin: claimWindowMin(),
394
+ hasEnvProxy: Boolean(process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy),
395
+ });
364
396
  }
365
397
  if (p === '/api/zcode/net' && method === 'PUT') {
366
398
  const body = await readBody(req).catch(() => ({}));
367
399
  try {
368
400
  if (body?.proxyFirst !== undefined) setProxyFirst(body.proxyFirst === true);
369
401
  if (body?.proxyUrl !== undefined) setProxyUrl(body.proxyUrl);
402
+ // 自动领取计划(轮询周期 / 运行时长)先落盘,再按新配置启动
403
+ if (body?.claimIntervalMin !== undefined) setClaimIntervalMin(body.claimIntervalMin);
404
+ if (body?.claimWindowMin !== undefined) setClaimWindowMin(body.claimWindowMin);
405
+ if (body?.claimIntervalMin !== undefined || body?.claimWindowMin !== undefined) refreshZcodeScheduler();
370
406
  if (body?.autoClaim !== undefined) {
371
- if (body.autoClaim === true) enableZcodeAutoClaimForOneHour(); // 开 = 开启 1 小时时段(2 分钟后首轮,之后每 ~2 分钟)
407
+ if (body.autoClaim === true) enableZcodeAutoClaimWindow(); // 开 = 按「自动领取计划」启动(2 分钟后首轮)
372
408
  else { setAutoClaimEnabled(false); refreshZcodeScheduler(); } // 关 = 立即停表
373
409
  }
374
410
  } catch (e) { return json(res, 400, { error: e.message }); }
375
411
  const ac = autoClaimEnabled();
376
- logger.info('DAEMON', `ZCode 出口:${proxyFirst() ? '代理优先' : '直连优先'},自动领取:${ac ? '开' : '关'},代理 ${proxyUrl() || '(环境变量/未设置)'}`);
377
- return json(res, 200, { proxyFirst: proxyFirst(), autoClaim: ac, autoClaimUntil: ac ? autoClaimUntil() : null });
412
+ logger.info('DAEMON', `ZCode 出口:${proxyFirst() ? '代理优先' : '直连优先'},自动领取:${ac ? `开(每 ${claimIntervalMin()} 分钟,${claimWindowMin() > 0 ? `运行 ${claimWindowMin()} 分钟` : '一直运行'})` : '关'},代理 ${proxyUrl() || '(环境变量/未设置)'}`);
413
+ return json(res, 200, {
414
+ proxyFirst: proxyFirst(), autoClaim: ac, autoClaimUntil: ac ? autoClaimUntil() : null,
415
+ claimIntervalMin: claimIntervalMin(), claimWindowMin: claimWindowMin(),
416
+ });
378
417
  }
379
418
 
380
419
  // 切换 WorkBuddy / ZCode 客户端当前登录账号
@@ -457,7 +496,55 @@ async function handleApi(req, res, url) {
457
496
  return json(res, e.catpawRunning ? 409 : 400, { error: e.message, code: e.catpawRunning ? 'CATPAW_RUNNING' : undefined });
458
497
  }
459
498
  }
460
- if (!target.provider.startsWith('workbuddy')) return json(res, 400, { error: '只有 WorkBuddy / ZCode / mirasim / 妙手 账号支持切换' });
499
+ if (target.provider === 'qoder' || target.provider === 'qoder-cn') {
500
+ // 防丢号:先把本机当前登录的最新 auth 快照回写账号库(客户端会静默刷新 token,旧快照可能已失效),再覆盖
501
+ try {
502
+ const lr = await readQoderAppAccounts();
503
+ const live = lr.accounts.find((c) => c.provider === target.provider);
504
+ if (live && live.user.id && String(live.user.id) !== String(target.uid)) {
505
+ await addAccount({
506
+ provider: live.provider, token: live.token, name: live.user.name || live.user.email,
507
+ uid: live.user.id, email: live.user.email, refreshToken: live.refreshToken, expiresAt: live.expiresAt,
508
+ source: 'local-app', meta: { qoderAuth: live.authJson, qoderAuthFile: live.file },
509
+ }, { trusted: true }).catch((e) => logger.warn('DAEMON', '同步 Qoder 当前登录失败:' + e.message));
510
+ }
511
+ } catch (e) { logger.warn('DAEMON', '读取 Qoder 当前登录失败:' + e.message); }
512
+ try {
513
+ // 强制切换:先关掉运行中的 Qoder,否则它退出时会把内存里的旧登录覆盖回文件
514
+ let closedClient = false;
515
+ if (body?.force === true) {
516
+ const t = terminateQoder();
517
+ closedClient = t.closed === true;
518
+ if (closedClient) logger.info('DAEMON', '已关闭 Qoder 客户端(强制切换)');
519
+ else if (t.running) logger.warn('DAEMON', '未能完全结束 Qoder 进程,继续强制切换');
520
+ }
521
+ const r = await qoderSwitchTo(target, { force: body?.force === true });
522
+ logger.info('DAEMON', r.alreadyActive ? `Qoder 当前已是 ${target.name || target.id}` : `Qoder 已切换到 ${target.name || target.id}(重新打开 Qoder 生效)`);
523
+ return json(res, 200, { ok: true, closedClient, ...r });
524
+ } catch (e) {
525
+ return json(res, e.qoderRunning ? 409 : 400, { error: e.message, code: e.qoderRunning ? 'QODER_RUNNING' : undefined });
526
+ }
527
+ }
528
+ if (target.provider === 'trae') {
529
+ // 防丢号:先把 Trae 当前登录同步进账号库并建快照,再覆盖成目标账号的登录态
530
+ try {
531
+ const live = await traeLiveAccount();
532
+ if (live && live.token !== target.token) {
533
+ await addAccount(live, { trusted: true }).catch((e) => logger.warn('DAEMON', '同步 Trae 当前登录失败:' + e.message));
534
+ traeSnapshotLive();
535
+ }
536
+ } catch (e) {}
537
+ try {
538
+ // 退进程交给 switchTo:它要先确认目标有快照才会动手,避免「白关一次 Trae」
539
+ const r = await traeSwitchTo(target, { force: body?.force === true });
540
+ if (r.closedClient) logger.info('DAEMON', '已关闭 Trae 客户端(强制切换)');
541
+ logger.info('DAEMON', r.alreadyActive ? `Trae 当前已是 ${target.name || target.id}` : `Trae 已切换到 ${target.name || target.id}(重新打开客户端生效)`);
542
+ return json(res, 200, { ok: true, ...r });
543
+ } catch (e) {
544
+ return json(res, e.traeRunning ? 409 : 400, { error: e.message, code: e.traeRunning ? 'TRAE_RUNNING' : undefined });
545
+ }
546
+ }
547
+ if (!target.provider.startsWith('workbuddy')) return json(res, 400, { error: '只有 WorkBuddy / ZCode / mirasim / 妙手 / Trae 账号支持切换' });
461
548
  // 先把客户端当前会话的最新 token 收回账号库,避免被覆盖后丢失
462
549
  const cur = readWorkbuddySessions().accounts.find((a) => a.current);
463
550
  if (cur && cur.uid !== target.uid) {
@@ -522,7 +609,7 @@ async function handleApi(req, res, url) {
522
609
 
523
610
  // ── 本机检测 / 凭据扫描 ──
524
611
  if (p === '/api/local/detect' && method === 'GET') {
525
- return json(res, 200, { apps: detectQoderApps(), workbuddyDir: workbuddyAuthDir(), zcode: detectZcode(), mirasim: detectMirasim(), catpaw: detectCatpaw(), legacy: detectInstalls() });
612
+ return json(res, 200, { apps: detectQoderApps(), workbuddyDir: workbuddyAuthDir(), zcode: detectZcode(), mirasim: detectMirasim(), catpaw: detectCatpaw(), trae: detectTrae(), legacy: detectInstalls() });
526
613
  }
527
614
  if (p === '/api/local/scan' && method === 'POST') {
528
615
  const existing = await loadAccounts();
@@ -548,6 +635,8 @@ async function handleApi(req, res, url) {
548
635
  addRecord({
549
636
  provider: c.provider, token: c.token, name: c.user.name || c.user.email, uid: c.user.id, email: c.user.email,
550
637
  refreshToken: c.refreshToken, expiresAt: c.expiresAt, source: 'local-app',
638
+ // 存整份解密后的 auth.v1.dat:切换时要原样写回(里面除 token 还有客户端读的其它字段)
639
+ meta: { qoderAuth: c.authJson, qoderAuthFile: c.file },
551
640
  }, { source: c.source });
552
641
  }
553
642
  // 2) WorkBuddy 客户端:当前会话 + 历史会话
@@ -575,7 +664,12 @@ async function handleApi(req, res, url) {
575
664
  const cp = await catpawLiveAccount();
576
665
  if (cp) addRecord(cp, { source: '妙手当前登录', current: true });
577
666
  } catch (e) { errors.push({ file: 'catpaw-moon/catx-credential.json', error: e.message }); }
578
- // 6) 旧版 VS Code 系 Qoder IDE / CLI:明文 token 扫描(归属需用户选择)
667
+ // 6) Trae 客户端:解密 <userData>\User\globalStorage\storage.json 的 tc 信封当前登录
668
+ try {
669
+ const tr = await traeLiveAccount();
670
+ if (tr) addRecord(tr, { source: 'Trae 当前登录', current: true });
671
+ } catch (e) { errors.push({ file: 'TRAE SOLO CN/User/globalStorage/storage.json', error: e.message }); }
672
+ // 7) 旧版 VS Code 系 Qoder IDE / CLI:明文 token 扫描(归属需用户选择)
579
673
  const det = detectInstalls();
580
674
  const dirs = det.ideDataDirs.filter(d => d.exists).map(d => d.path);
581
675
  if (det.cliDir.exists) dirs.push(det.cliDir.path);
@@ -598,6 +692,8 @@ async function handleApi(req, res, url) {
598
692
  { trusted: Boolean(cand.record) },
599
693
  );
600
694
  if (duplicate && !updated) return json(res, 409, { error: '该账号已存在,信息已是最新' });
695
+ // Trae 的登录态是 15 项文件快照而非单个凭据文件,导入时就建一次快照,否则该账号不可切换
696
+ if (account.provider === 'trae') traeSnapshotLive();
601
697
  logger.info('DAEMON', (updated ? '已用本机凭据更新:' : '从本机导入账号:') + (account.name || account.id));
602
698
  return json(res, updated ? 200 : 201, { account: publicAccount(account), updated });
603
699
  }
@@ -673,8 +769,24 @@ async function handleApi(req, res, url) {
673
769
  if (p === '/api/status' && method === 'GET') {
674
770
  const accounts = await loadAccounts();
675
771
  const state = await loadState();
772
+ // Qoder 当前登录(面板轮询每 30s 打一次,DPAPI 解密结果缓存 1 分钟)
773
+ const qoder = await qoderCurrentInfo();
774
+ // 旧账号没赶上「整份 auth 快照」的导入:本机 Qoder 有登录且账号库里有缺快照的同 uid 账号时,顺手回填(可逆、只在缺失时发生)
775
+ const needBackfill = qoder.backfill.length
776
+ && accounts.some((a) => (a.provider === 'qoder' || a.provider === 'qoder-cn')
777
+ && a.uid && !a.meta?.qoderAuth
778
+ && qoder.backfill.some((b) => b.provider === a.provider && String(b.uid) === String(a.uid)));
779
+ if (needBackfill) {
780
+ await withAccounts((list) => {
781
+ for (const b of qoder.backfill) {
782
+ const cur = list.find((a) => a.provider === b.provider && a.uid && b.uid && String(a.uid) === String(b.uid) && !a.meta?.qoderAuth);
783
+ if (cur) cur.meta = { ...(cur.meta || {}), qoderAuth: b.authJson, qoderAuthFile: b.file };
784
+ }
785
+ }).catch((e) => logger.warn('DAEMON', '回填 Qoder 登录快照失败:' + e.message));
786
+ }
676
787
  // 妙手凭据文件只存 token:当前登录按 token 对齐账号库,避免每轮状态都打网关查 uid
677
788
  const cpToken = currentCatpawToken();
789
+ const traeDet = detectTrae();
678
790
  return json(res, 200, {
679
791
  ok: true,
680
792
  app: 'CreditDaddy',
@@ -690,6 +802,8 @@ async function handleApi(req, res, url) {
690
802
  umid: umidInfo(),
691
803
  platform: process.platform,
692
804
  workbuddyCurrentUid: currentWorkbuddyUid(),
805
+ qoderCurrent: qoder.uids,
806
+ qoderClient: { installed: qoder.apps.some((x) => x.installed), signedIn: qoder.apps.some((x) => x.signedIn), running: qoderRunning() },
693
807
  zcodeCurrentUid: currentZcodeUid(),
694
808
  zcodeCurrentIdentity: currentZcodeIdentity(),
695
809
  zcodeClient: (() => { const d = detectZcode(); return { installed: d.exists, signedIn: d.signedIn, running: zcodeRunning() }; })(),
@@ -697,6 +811,8 @@ async function handleApi(req, res, url) {
697
811
  mirasimClient: (() => { const d = detectMirasim(); return { installed: d.clientInstalled, signedIn: d.signedIn, running: d.running }; })(),
698
812
  catpawCurrentUid: (cpToken && accounts.find((a) => a.provider === 'catpaw' && a.token === cpToken)?.uid) || null,
699
813
  catpawClient: (() => { const d = detectCatpaw(); return { installed: d.clientInstalled, signedIn: d.signedIn, running: d.running }; })(),
814
+ traeCurrentUid: traeDet.uid,
815
+ traeClient: { installed: traeDet.clientInstalled, signedIn: traeDet.signedIn, running: traeDet.running },
700
816
  keyRequired: Boolean(PANEL_KEY),
701
817
  });
702
818
  }