creditdaddy 1.1.1 → 1.3.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,99 @@
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', 'minimax'];
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
+ minimax: 'MiniMax',
49
+ };
50
+ /** provider → 产品线(qoder / workbuddy / zcode / mirasim / catpaw / trae / minimax) */
51
+ export const productOf = (provider) => {
52
+ const p = String(provider);
53
+ if (p.startsWith('workbuddy')) return 'workbuddy';
54
+ if (p.startsWith('zcode')) return 'zcode';
55
+ if (p === 'mirasim') return 'mirasim';
56
+ if (p === 'catpaw') return 'catpaw';
57
+ if (p.startsWith('trae')) return 'trae';
58
+ if (p === 'minimax') return 'minimax';
59
+ return 'qoder';
60
+ };
61
+ /** provider 是否国内版 */
62
+ export const isCnProvider = (provider) => provider === 'qoder-cn' || provider === 'workbuddy';
63
+
64
+ /** Qoder 网页端同源 API 的 host(用量明细逐资源包接口在这里,只认浏览器会话 Cookie) */
65
+ export function webBase(provider) {
66
+ return provider === 'qoder-cn' ? CN_WEB_BASE : WEB_BASE;
67
+ }
68
+
69
+ /** 签到请求头(源自 10router qoderCheckin.js,clientType=10 与官方客户端一致) */
70
+ export function buildQoderHeaders(token) {
71
+ return {
72
+ Authorization: `Bearer ${token}`,
73
+ 'Cosy-ClientType': '10',
74
+ 'Cosy-Version': '0.3.3',
75
+ 'Cosy-MachineOS':
76
+ process.platform === 'win32' ? 'windows'
77
+ : process.platform === 'darwin' ? 'macos' : 'linux',
78
+ 'User-Agent': 'Qoder',
79
+ Accept: 'application/json',
80
+ };
81
+ }
82
+
83
+ /** PAT 兑换请求头(源自 10router qoderModels.js,模拟官方 qodercli) */
84
+ export function buildExchangeHeaders() {
85
+ return {
86
+ 'Content-Type': 'application/json',
87
+ Accept: 'application/json',
88
+ 'User-Agent': 'qodercli/1.0.0',
89
+ 'Cosy-Version': '1.0.0',
90
+ 'Cosy-ClientType': '5',
91
+ };
92
+ }
93
+
94
+ export const FETCH_TIMEOUT_MS = 15000;
95
+
96
+ // 版本号 / 项目主页以 package.json 为唯一来源(桌面版 / fpk 打包时都会一并拷入 package.json)
97
+ const PKG = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
98
+ export const APP_VERSION = PKG.version;
99
+ export const PROJECT_URL = PKG.homepage || 'https://github.com/techysy/CreditDaddy';
package/src/daemon.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * POST /api/accounts/:id/zcode/claim ZCode 领取活动 {planId, captchaParam?, region?}
19
19
  * GET /api/zcode/captcha-config ZCode 领取验证码配置(sceneId / prefix / region)
20
20
  * POST /api/checkin 全部签到 {provider?, skipIfCheckedToday?}
21
- * POST /api/auth/device/start 发起浏览器登录 {provider: qoder / qoder-cn / workbuddy / workbuddy-intl / zcode-bigmodel / zcode-zai}
21
+ * POST /api/auth/device/start 发起浏览器登录 {provider: qoder / qoder-cn / workbuddy / workbuddy-intl / zcode-bigmodel / zcode-zai / minimax}
22
22
  * POST /api/auth/device/poll 轮询浏览器登录结果 {sessionId}
23
23
  * GET /api/local/detect 检测本机 Qoder 客户端 / IDE / CLI
24
24
  * POST /api/local/scan 读取本机已登录账号(解密客户端凭据 + 扫描旧版 IDE)
@@ -48,7 +48,7 @@ import path from 'node:path';
48
48
  import { fileURLToPath } from 'node:url';
49
49
  import { logger, getLogs } from './logger.js';
50
50
  import {
51
- loadAccounts, loadState, withAccounts, publicAccount, dataDir, loadSettings, saveSettings,
51
+ loadAccounts, loadState, withAccounts, publicAccount, dataDir, loadSettings, saveSettings, withSettings,
52
52
  } from './store.js';
53
53
  import { addAccount, importAccounts, refreshContext } from './accounts.js';
54
54
  import { productImpl } from './providers.js';
@@ -56,11 +56,14 @@ 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 { liveToAccount as minimaxLiveAccount, detectMiniMax, currentMiniMaxUid, currentMiniMaxToken, currentMiniMaxRecordKey } from './minimaxLocal.js';
61
+ import * as minimaxGateway from './minimaxGateway.js';
62
+ import { fetchClaimPlans, claimPlan, fetchCaptchaConfig, proxyFirst, setProxyFirst, proxyUrl, setProxyUrl, autoClaimEnabled, autoClaimUntil, setAutoClaimEnabled, claimIntervalMin, setClaimIntervalMin, claimWindowMin, setClaimWindowMin } from './zcodeClient.js';
60
63
  import { exportAccounts, parseImport, TransferError } from './transfer.js';
61
64
  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';
65
+ import { runCheckinTick, getSchedulerInfo, dayKey, enableZcodeAutoClaimWindow, refreshZcodeScheduler, pollZcodeNow } from './checkin.js';
66
+ import { detectQoderApps, readQoderAppAccounts, riskIdentityAvailable, riskIdentitySource, switchTo as qoderSwitchTo, qoderRunning, terminateQoder } from './qoderApp.js';
64
67
  import { umidInfo, installUmid } from './qoderUmid.js';
65
68
  import * as tenrouter from './tenrouter.js';
66
69
  import * as zcodeGateway from './zcodeGateway.js';
@@ -68,7 +71,7 @@ import { startDeviceFlow, pollDeviceFlow, LOGIN_KINDS } from './authDevice.js';
68
71
  import { detectInstalls, scanLocalTokens, putCandidate, peekCandidate } from './localDetect.js';
69
72
  import { PROVIDER_LABEL, APP_VERSION, PROVIDERS, PROJECT_URL } from './constants.js';
70
73
 
71
- const PRODUCT_IDS = ['qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw'];
74
+ const PRODUCT_IDS = ['qoder', 'workbuddy', 'zcode', 'mirasim', 'catpaw', 'trae', 'minimax'];
72
75
 
73
76
  /** 面板访问密码:面板「设置」写入的 settings.json.panelKey 优先(可设 / 可关);
74
77
  * 未设置(或被面板关闭)时回退环境变量 CREDITDADDY_PASSWORD(fnOS / 命令行部署注入),
@@ -88,6 +91,22 @@ const DEFAULT_PORT = 47860;
88
91
  const MAX_BODY_BYTES = 2 * 1024 * 1024;
89
92
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
90
93
 
94
+ // Qoder 当前登录缓存:面板 30s 一轮 status,DPAPI 解密结果没必要每轮重算
95
+ let qoderCurCache = { at: 0, data: null };
96
+ async function qoderCurrentInfo() {
97
+ if (qoderCurCache.data && Date.now() - qoderCurCache.at < 60_000) return qoderCurCache.data;
98
+ const data = { apps: detectQoderApps(), uids: {}, backfill: [] };
99
+ try {
100
+ const r = await readQoderAppAccounts();
101
+ for (const c of r.accounts) {
102
+ data.uids[c.provider] = c.user.id || null;
103
+ if (c.user.id) data.backfill.push({ provider: c.provider, uid: c.user.id, authJson: c.authJson, file: c.file });
104
+ }
105
+ } catch { /* 解密失败只留检测信息 */ }
106
+ qoderCurCache = { at: Date.now(), data };
107
+ return data;
108
+ }
109
+
91
110
  class HttpError extends Error {
92
111
  constructor(status, message) { super(message); this.status = status; }
93
112
  }
@@ -350,31 +369,76 @@ async function handleApi(req, res, url) {
350
369
  if (p === '/api/zcode-gateway' && method === 'PUT') {
351
370
  const body = await readBody(req).catch(() => ({}));
352
371
  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)' : '关(仅本机)'}`);
372
+ if (body?.lan !== undefined || body?.allow !== undefined) {
373
+ const patch = {};
374
+ if (body?.lan !== undefined) patch.zcodeGatewayLan = body.lan === true;
375
+ if (body?.allow !== undefined) {
376
+ if (!Array.isArray(body.allow)) return json(res, 400, { error: 'allow 需为字符串数组' });
377
+ const cleaned = [...new Set(body.allow.map((x) => String(x || '').trim()).filter(Boolean))];
378
+ if (cleaned.some((x) => /[^\w.*:-]/.test(x))) return json(res, 400, { error: '白名单条目只能是 IP / 主机名(支持 192.168.31.* 通配)' });
379
+ patch.zcodeGatewayAllow = cleaned;
380
+ }
381
+ await (await import('./store.js')).saveSettings(patch);
382
+ const lanNow = patch.zcodeGatewayLan ?? (await (await import('./store.js')).loadSettings()).zcodeGatewayLan === true;
383
+ const allowText = patch.zcodeGatewayAllow ? patch.zcodeGatewayAllow.join(', ') : '';
384
+ logger.info('DAEMON', `ZCode 网关局域网:${lanNow ? '开' : '关(仅本机)'}${allowText ? `,白名单:${allowText}` : ''}(绑定改动重启后生效)`);
356
385
  }
357
386
  return json(res, 200, await zcodeGateway.gatewayStatus());
358
387
  }
388
+ // MiniMax 本地网关控制
389
+ if (p === '/api/minimax-gateway' && method === 'GET') {
390
+ return json(res, 200, await minimaxGateway.gatewayStatus());
391
+ }
392
+ if (p === '/api/minimax-gateway' && method === 'PUT') {
393
+ const body = await readBody(req).catch(() => ({}));
394
+ if (body?.enabled !== undefined) await minimaxGateway.setGatewayEnabled(body.enabled === true);
395
+ if (body?.lan !== undefined || body?.allow !== undefined) {
396
+ const patch = {};
397
+ if (body?.lan !== undefined) patch.minimaxGatewayLan = body.lan === true;
398
+ if (body?.allow !== undefined) {
399
+ if (!Array.isArray(body.allow)) return json(res, 400, { error: 'allow 需为字符串数组' });
400
+ const cleaned = [...new Set(body.allow.map((x) => String(x || '').trim()).filter(Boolean))];
401
+ if (cleaned.some((x) => /[^\w.*:-]/.test(x))) return json(res, 400, { error: '白名单条目只能是 IP / 主机名(支持 192.168.31.* 通配)' });
402
+ patch.minimaxGatewayAllow = cleaned;
403
+ }
404
+ await (await import('./store.js')).saveSettings(patch);
405
+ const lanNow = patch.minimaxGatewayLan ?? (await (await import('./store.js')).loadSettings()).minimaxGatewayLan === true;
406
+ const allowText = patch.minimaxGatewayAllow ? patch.minimaxGatewayAllow.join(', ') : '';
407
+ logger.info('DAEMON', `MiniMax 网关局域网:${lanNow ? '开' : '关(仅本机)'}${allowText ? `,白名单:${allowText}` : ''}`);
408
+ }
409
+ return json(res, 200, await minimaxGateway.gatewayStatus());
410
+ }
359
411
  if (p === '/api/zcode/net' && method === 'GET') {
360
412
  const u = proxyUrl();
361
413
  const masked = u ? (() => { try { const x = new URL(u); x.password = x.password ? '*'.repeat(4) : ''; return x.toString(); } catch { return '***'; } })() : null;
362
414
  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) });
415
+ return json(res, 200, {
416
+ proxyFirst: proxyFirst(), proxyUrlMasked: masked,
417
+ autoClaim: ac, autoClaimUntil: ac ? autoClaimUntil() : null,
418
+ claimIntervalMin: claimIntervalMin(), claimWindowMin: claimWindowMin(),
419
+ hasEnvProxy: Boolean(process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy),
420
+ });
364
421
  }
365
422
  if (p === '/api/zcode/net' && method === 'PUT') {
366
423
  const body = await readBody(req).catch(() => ({}));
367
424
  try {
368
425
  if (body?.proxyFirst !== undefined) setProxyFirst(body.proxyFirst === true);
369
426
  if (body?.proxyUrl !== undefined) setProxyUrl(body.proxyUrl);
427
+ // 自动领取计划(轮询周期 / 运行时长)先落盘,再按新配置启动
428
+ if (body?.claimIntervalMin !== undefined) setClaimIntervalMin(body.claimIntervalMin);
429
+ if (body?.claimWindowMin !== undefined) setClaimWindowMin(body.claimWindowMin);
430
+ if (body?.claimIntervalMin !== undefined || body?.claimWindowMin !== undefined) refreshZcodeScheduler();
370
431
  if (body?.autoClaim !== undefined) {
371
- if (body.autoClaim === true) enableZcodeAutoClaimForOneHour(); // 开 = 开启 1 小时时段(2 分钟后首轮,之后每 ~2 分钟)
432
+ if (body.autoClaim === true) enableZcodeAutoClaimWindow(); // 开 = 按「自动领取计划」启动(2 分钟后首轮)
372
433
  else { setAutoClaimEnabled(false); refreshZcodeScheduler(); } // 关 = 立即停表
373
434
  }
374
435
  } catch (e) { return json(res, 400, { error: e.message }); }
375
436
  const ac = autoClaimEnabled();
376
- logger.info('DAEMON', `ZCode 出口:${proxyFirst() ? '代理优先' : '直连优先'},自动领取:${ac ? '开' : '关'},代理 ${proxyUrl() || '(环境变量/未设置)'}`);
377
- return json(res, 200, { proxyFirst: proxyFirst(), autoClaim: ac, autoClaimUntil: ac ? autoClaimUntil() : null });
437
+ logger.info('DAEMON', `ZCode 出口:${proxyFirst() ? '代理优先' : '直连优先'},自动领取:${ac ? `开(每 ${claimIntervalMin()} 分钟,${claimWindowMin() > 0 ? `运行 ${claimWindowMin()} 分钟` : '一直运行'})` : '关'},代理 ${proxyUrl() || '(环境变量/未设置)'}`);
438
+ return json(res, 200, {
439
+ proxyFirst: proxyFirst(), autoClaim: ac, autoClaimUntil: ac ? autoClaimUntil() : null,
440
+ claimIntervalMin: claimIntervalMin(), claimWindowMin: claimWindowMin(),
441
+ });
378
442
  }
379
443
 
380
444
  // 切换 WorkBuddy / ZCode 客户端当前登录账号
@@ -457,7 +521,55 @@ async function handleApi(req, res, url) {
457
521
  return json(res, e.catpawRunning ? 409 : 400, { error: e.message, code: e.catpawRunning ? 'CATPAW_RUNNING' : undefined });
458
522
  }
459
523
  }
460
- if (!target.provider.startsWith('workbuddy')) return json(res, 400, { error: '只有 WorkBuddy / ZCode / mirasim / 妙手 账号支持切换' });
524
+ if (target.provider === 'qoder' || target.provider === 'qoder-cn') {
525
+ // 防丢号:先把本机当前登录的最新 auth 快照回写账号库(客户端会静默刷新 token,旧快照可能已失效),再覆盖
526
+ try {
527
+ const lr = await readQoderAppAccounts();
528
+ const live = lr.accounts.find((c) => c.provider === target.provider);
529
+ if (live && live.user.id && String(live.user.id) !== String(target.uid)) {
530
+ await addAccount({
531
+ provider: live.provider, token: live.token, name: live.user.name || live.user.email,
532
+ uid: live.user.id, email: live.user.email, refreshToken: live.refreshToken, expiresAt: live.expiresAt,
533
+ source: 'local-app', meta: { qoderAuth: live.authJson, qoderAuthFile: live.file },
534
+ }, { trusted: true }).catch((e) => logger.warn('DAEMON', '同步 Qoder 当前登录失败:' + e.message));
535
+ }
536
+ } catch (e) { logger.warn('DAEMON', '读取 Qoder 当前登录失败:' + e.message); }
537
+ try {
538
+ // 强制切换:先关掉运行中的 Qoder,否则它退出时会把内存里的旧登录覆盖回文件
539
+ let closedClient = false;
540
+ if (body?.force === true) {
541
+ const t = terminateQoder();
542
+ closedClient = t.closed === true;
543
+ if (closedClient) logger.info('DAEMON', '已关闭 Qoder 客户端(强制切换)');
544
+ else if (t.running) logger.warn('DAEMON', '未能完全结束 Qoder 进程,继续强制切换');
545
+ }
546
+ const r = await qoderSwitchTo(target, { force: body?.force === true });
547
+ logger.info('DAEMON', r.alreadyActive ? `Qoder 当前已是 ${target.name || target.id}` : `Qoder 已切换到 ${target.name || target.id}(重新打开 Qoder 生效)`);
548
+ return json(res, 200, { ok: true, closedClient, ...r });
549
+ } catch (e) {
550
+ return json(res, e.qoderRunning ? 409 : 400, { error: e.message, code: e.qoderRunning ? 'QODER_RUNNING' : undefined });
551
+ }
552
+ }
553
+ if (target.provider === 'trae') {
554
+ // 防丢号:先把 Trae 当前登录同步进账号库并建快照,再覆盖成目标账号的登录态
555
+ try {
556
+ const live = await traeLiveAccount();
557
+ if (live && live.token !== target.token) {
558
+ await addAccount(live, { trusted: true }).catch((e) => logger.warn('DAEMON', '同步 Trae 当前登录失败:' + e.message));
559
+ traeSnapshotLive();
560
+ }
561
+ } catch (e) {}
562
+ try {
563
+ // 退进程交给 switchTo:它要先确认目标有快照才会动手,避免「白关一次 Trae」
564
+ const r = await traeSwitchTo(target, { force: body?.force === true });
565
+ if (r.closedClient) logger.info('DAEMON', '已关闭 Trae 客户端(强制切换)');
566
+ logger.info('DAEMON', r.alreadyActive ? `Trae 当前已是 ${target.name || target.id}` : `Trae 已切换到 ${target.name || target.id}(重新打开客户端生效)`);
567
+ return json(res, 200, { ok: true, ...r });
568
+ } catch (e) {
569
+ return json(res, e.traeRunning ? 409 : 400, { error: e.message, code: e.traeRunning ? 'TRAE_RUNNING' : undefined });
570
+ }
571
+ }
572
+ if (!target.provider.startsWith('workbuddy')) return json(res, 400, { error: '只有 WorkBuddy / ZCode / mirasim / 妙手 / Trae 账号支持切换' });
461
573
  // 先把客户端当前会话的最新 token 收回账号库,避免被覆盖后丢失
462
574
  const cur = readWorkbuddySessions().accounts.find((a) => a.current);
463
575
  if (cur && cur.uid !== target.uid) {
@@ -511,8 +623,12 @@ async function handleApi(req, res, url) {
511
623
  });
512
624
  if (!bound) {
513
625
  // 登录窗口可能先于设备码入库关闭:暂存待绑定会话,addAccount 补全 uid 后自动挂上
514
- const pending = (await loadSettings()).pendingQoderWebSessions || {};
515
- await saveSettings({ pendingQoderWebSessions: { ...pending, [probe.user_id]: { kind, cookie, capturedAt: now } } });
626
+ await withSettings((settings) => {
627
+ settings.pendingQoderWebSessions = {
628
+ ...(settings.pendingQoderWebSessions || {}),
629
+ [probe.user_id]: { kind, cookie, capturedAt: now },
630
+ };
631
+ });
516
632
  logger.info('DAEMON', `收到 Qoder 网页会话(uid ${probe.user_id.slice(0, 8)}…),暂无匹配账号,已暂存待绑定`);
517
633
  } else {
518
634
  logger.info('DAEMON', `已绑定 Qoder 网页会话到 ${bound} 个账号(逐资源包明细可用)`);
@@ -522,7 +638,7 @@ async function handleApi(req, res, url) {
522
638
 
523
639
  // ── 本机检测 / 凭据扫描 ──
524
640
  if (p === '/api/local/detect' && method === 'GET') {
525
- return json(res, 200, { apps: detectQoderApps(), workbuddyDir: workbuddyAuthDir(), zcode: detectZcode(), mirasim: detectMirasim(), catpaw: detectCatpaw(), legacy: detectInstalls() });
641
+ return json(res, 200, { apps: detectQoderApps(), workbuddyDir: workbuddyAuthDir(), zcode: detectZcode(), mirasim: detectMirasim(), catpaw: detectCatpaw(), trae: detectTrae(), minimax: detectMiniMax(), legacy: detectInstalls() });
526
642
  }
527
643
  if (p === '/api/local/scan' && method === 'POST') {
528
644
  const existing = await loadAccounts();
@@ -548,6 +664,8 @@ async function handleApi(req, res, url) {
548
664
  addRecord({
549
665
  provider: c.provider, token: c.token, name: c.user.name || c.user.email, uid: c.user.id, email: c.user.email,
550
666
  refreshToken: c.refreshToken, expiresAt: c.expiresAt, source: 'local-app',
667
+ // 存整份解密后的 auth.v1.dat:切换时要原样写回(里面除 token 还有客户端读的其它字段)
668
+ meta: { qoderAuth: c.authJson, qoderAuthFile: c.file },
551
669
  }, { source: c.source });
552
670
  }
553
671
  // 2) WorkBuddy 客户端:当前会话 + 历史会话
@@ -575,7 +693,17 @@ async function handleApi(req, res, url) {
575
693
  const cp = await catpawLiveAccount();
576
694
  if (cp) addRecord(cp, { source: '妙手当前登录', current: true });
577
695
  } catch (e) { errors.push({ file: 'catpaw-moon/catx-credential.json', error: e.message }); }
578
- // 6) 旧版 VS Code 系 Qoder IDE / CLI:明文 token 扫描(归属需用户选择)
696
+ // 6) Trae 客户端:解密 <userData>\User\globalStorage\storage.json 的 tc 信封当前登录
697
+ try {
698
+ const tr = await traeLiveAccount();
699
+ if (tr) addRecord(tr, { source: 'Trae 当前登录', current: true });
700
+ } catch (e) { errors.push({ file: 'TRAE SOLO CN/User/globalStorage/storage.json', error: e.message }); }
701
+ // 7) MiniMax 客户端:读取 ~/.minimax 凭据当前登录
702
+ try {
703
+ const mm = await minimaxLiveAccount();
704
+ if (mm) addRecord(mm, { source: 'MiniMax 当前登录', current: true });
705
+ } catch (e) { errors.push({ file: '~/.minimax/auth/.../auth.json', error: e.message }); }
706
+ // 8) 旧版 VS Code 系 Qoder IDE / CLI:明文 token 扫描(归属需用户选择)
579
707
  const det = detectInstalls();
580
708
  const dirs = det.ideDataDirs.filter(d => d.exists).map(d => d.path);
581
709
  if (det.cliDir.exists) dirs.push(det.cliDir.path);
@@ -598,6 +726,8 @@ async function handleApi(req, res, url) {
598
726
  { trusted: Boolean(cand.record) },
599
727
  );
600
728
  if (duplicate && !updated) return json(res, 409, { error: '该账号已存在,信息已是最新' });
729
+ // Trae 的登录态是 15 项文件快照而非单个凭据文件,导入时就建一次快照,否则该账号不可切换
730
+ if (account.provider === 'trae') traeSnapshotLive();
601
731
  logger.info('DAEMON', (updated ? '已用本机凭据更新:' : '从本机导入账号:') + (account.name || account.id));
602
732
  return json(res, updated ? 200 : 201, { account: publicAccount(account), updated });
603
733
  }
@@ -664,17 +794,45 @@ async function handleApi(req, res, url) {
664
794
  } catch (e) { return json(res, 500, { error: e.message }); }
665
795
  }
666
796
 
667
- // 日志
797
+ // 日志(tag 过滤:?tag=MINIMAX-GW / ZCODE-GW 取网关调用日志,与运行日志分开展示)
668
798
  if (p === '/api/logs' && method === 'GET') {
669
- return json(res, 200, { logs: getLogs(200) });
799
+ const tag = url.searchParams.get('tag') || null;
800
+ return json(res, 200, { logs: getLogs(200, tag) });
670
801
  }
671
802
 
672
803
  // 状态
673
804
  if (p === '/api/status' && method === 'GET') {
674
805
  const accounts = await loadAccounts();
675
806
  const state = await loadState();
807
+ // Qoder 当前登录(面板轮询每 30s 打一次,DPAPI 解密结果缓存 1 分钟)
808
+ const qoder = await qoderCurrentInfo();
809
+ // 旧账号没赶上「整份 auth 快照」的导入:本机 Qoder 有登录且账号库里有缺快照的同 uid 账号时,顺手回填(可逆、只在缺失时发生)
810
+ const needBackfill = qoder.backfill.length
811
+ && accounts.some((a) => (a.provider === 'qoder' || a.provider === 'qoder-cn')
812
+ && a.uid && !a.meta?.qoderAuth
813
+ && qoder.backfill.some((b) => b.provider === a.provider && String(b.uid) === String(a.uid)));
814
+ if (needBackfill) {
815
+ await withAccounts((list) => {
816
+ for (const b of qoder.backfill) {
817
+ const cur = list.find((a) => a.provider === b.provider && a.uid && b.uid && String(a.uid) === String(b.uid) && !a.meta?.qoderAuth);
818
+ if (cur) cur.meta = { ...(cur.meta || {}), qoderAuth: b.authJson, qoderAuthFile: b.file };
819
+ }
820
+ }).catch((e) => logger.warn('DAEMON', '回填 Qoder 登录快照失败:' + e.message));
821
+ }
676
822
  // 妙手凭据文件只存 token:当前登录按 token 对齐账号库,避免每轮状态都打网关查 uid
677
823
  const cpToken = currentCatpawToken();
824
+ const mmToken = currentMiniMaxToken();
825
+ const mmRecKey = currentMiniMaxRecordKey();
826
+ // 1) token match;2) record-key match (token 轮换时仍稳定);3) uid-cache fallback
827
+ let mmLiveUid = null;
828
+ if (mmToken) {
829
+ mmLiveUid = accounts.find((a) => a.provider === 'minimax' && a.token === mmToken)?.uid;
830
+ }
831
+ if (!mmLiveUid && mmRecKey) {
832
+ mmLiveUid = accounts.find((a) => a.provider === 'minimax' && a.meta?.authRecordKey === mmRecKey)?.uid;
833
+ }
834
+ if (!mmLiveUid) mmLiveUid = currentMiniMaxUid();
835
+ const traeDet = detectTrae();
678
836
  return json(res, 200, {
679
837
  ok: true,
680
838
  app: 'CreditDaddy',
@@ -684,12 +842,20 @@ async function handleApi(req, res, url) {
684
842
  dataDir: dataDir(),
685
843
  today: dayKey(),
686
844
  todayDone: state?.qoderDailyDone || {},
845
+ todayByProduct: {
846
+ qoder: dayKey(Date.now(), 'qoder'),
847
+ workbuddy: dayKey(Date.now(), 'workbuddy'),
848
+ trae: dayKey(Date.now(), 'trae'),
849
+ minimax: dayKey(Date.now(), 'minimax'),
850
+ },
687
851
  scheduler: getSchedulerInfo(),
688
852
  riskIdentity: riskIdentityAvailable(),
689
853
  riskSource: riskIdentitySource(),
690
854
  umid: umidInfo(),
691
855
  platform: process.platform,
692
856
  workbuddyCurrentUid: currentWorkbuddyUid(),
857
+ qoderCurrent: qoder.uids,
858
+ qoderClient: { installed: qoder.apps.some((x) => x.installed), signedIn: qoder.apps.some((x) => x.signedIn), running: qoderRunning() },
693
859
  zcodeCurrentUid: currentZcodeUid(),
694
860
  zcodeCurrentIdentity: currentZcodeIdentity(),
695
861
  zcodeClient: (() => { const d = detectZcode(); return { installed: d.exists, signedIn: d.signedIn, running: zcodeRunning() }; })(),
@@ -697,6 +863,10 @@ async function handleApi(req, res, url) {
697
863
  mirasimClient: (() => { const d = detectMirasim(); return { installed: d.clientInstalled, signedIn: d.signedIn, running: d.running }; })(),
698
864
  catpawCurrentUid: (cpToken && accounts.find((a) => a.provider === 'catpaw' && a.token === cpToken)?.uid) || null,
699
865
  catpawClient: (() => { const d = detectCatpaw(); return { installed: d.clientInstalled, signedIn: d.signedIn, running: d.running }; })(),
866
+ traeCurrentUid: traeDet.uid,
867
+ traeClient: { installed: traeDet.clientInstalled, signedIn: traeDet.signedIn, running: traeDet.running },
868
+ minimaxCurrentUid: mmLiveUid,
869
+ minimaxClient: (() => { const d = detectMiniMax(); return { installed: d.installed, signedIn: d.signedIn, running: d.running }; })(),
700
870
  keyRequired: Boolean(PANEL_KEY),
701
871
  });
702
872
  }
@@ -768,6 +938,10 @@ export async function startDaemon(port = DEFAULT_PORT, host = '127.0.0.1') {
768
938
  if (url.pathname === '/gateway/v1/messages' || url.pathname === '/v1/messages') {
769
939
  return await zcodeGateway.handleGateway(req, res);
770
940
  }
941
+ // MiniMax Code 本地 Anthropic 兼容网关
942
+ if (url.pathname === '/gateway/minimax/v1/messages') {
943
+ return await minimaxGateway.handleGateway(req, res);
944
+ }
771
945
  if (url.pathname.startsWith('/api/')) {
772
946
  return await handleApi(req, res, url);
773
947
  }
@@ -786,7 +960,7 @@ export async function startDaemon(port = DEFAULT_PORT, host = '127.0.0.1') {
786
960
  }
787
961
  });
788
962
 
789
- return new Promise((resolve) => {
963
+ return new Promise((resolve, reject) => {
790
964
  let attempts = 0;
791
965
  const bind = (p) => {
792
966
  // 失败重试时必须摘掉上一次的 listening 监听,否则成功后会重复触发
@@ -798,7 +972,11 @@ export async function startDaemon(port = DEFAULT_PORT, host = '127.0.0.1') {
798
972
  server.close();
799
973
  setTimeout(() => bind(p + 1), 300);
800
974
  } else {
801
- logger.error('DAEMON', `监听失败:${err?.message || err}`);
975
+ // 兜底失败必须 reject:否则这个 promise 永远挂着,调用方的 startScheduler() 不会跑,
976
+ // 进程既不监听也不退出,只在日志里留一行,看起来像「启动了但没反应」
977
+ const msg = err?.message || String(err);
978
+ logger.error('DAEMON', `监听失败:${msg}`);
979
+ reject(new Error(`无法监听 ${host}:${p}:${msg}`));
802
980
  }
803
981
  };
804
982
  const onListening = () => {