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/LICENSE +21 -21
- package/README.md +306 -289
- package/bin/creditdaddy.js +8 -4
- package/package.json +29 -28
- package/src/accounts.js +161 -161
- package/src/authDevice.js +205 -205
- package/src/catpawClient.js +103 -103
- package/src/catpawLocal.js +229 -229
- package/src/checkin.js +357 -340
- package/src/constants.js +97 -95
- package/src/daemon.js +130 -14
- package/src/localDetect.js +147 -147
- package/src/mirasimClient.js +189 -189
- package/src/panel.html +465 -142
- package/src/providers.js +190 -181
- package/src/qoderApp.js +93 -7
- package/src/qoderClient.js +237 -237
- package/src/qoderUmid.js +198 -198
- package/src/store.js +238 -238
- package/src/tenrouter.js +18 -8
- package/src/tenrouterAccounts.js +163 -163
- package/src/traeClient.js +258 -0
- package/src/traeLocal.js +337 -0
- package/src/transfer.js +160 -160
- package/src/usageSync.js +505 -505
- package/src/workbuddyLocal.js +133 -133
- package/src/zcodeAuth.js +131 -131
- package/src/zcodeAutoClaim.js +97 -97
- package/src/zcodeClient.js +33 -2
- package/src/zcodeGateway.js +26 -30
- package/src/zcodeLocal.js +334 -319
- package/src/zcrypto.js +160 -160
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (p.startsWith('
|
|
53
|
-
if (p
|
|
54
|
-
if (p === '
|
|
55
|
-
return '
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
'Cosy-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
'
|
|
85
|
-
|
|
86
|
-
'
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
355
|
-
|
|
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, {
|
|
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)
|
|
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 ? '
|
|
377
|
-
return json(res, 200, {
|
|
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 (
|
|
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)
|
|
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
|
}
|