creditdaddy 1.5.2 → 1.6.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/providers.js CHANGED
@@ -10,12 +10,13 @@
10
10
 
11
11
  import { productOf } from './constants.js';
12
12
  import { checkinOne as checkinQoder, fetchQuotaUsage, fetchUserinfo, fetchUsageDetail } from './qoderClient.js';
13
- import { checkinWorkbuddy, checkinWorkbuddyIntl, fetchWorkbuddyQuota, inspectToken } from './workbuddyClient.js';
13
+ import { checkinWorkbuddy, fetchWorkbuddyQuota, inspectToken } from './workbuddyClient.js';
14
14
  import { fetchZcodeQuota } from './zcodeClient.js';
15
15
  import { fetchMirasimQuota, fetchMirasimProfile } from './mirasimClient.js';
16
16
  import { fetchCatpawQuota, fetchCatpawProfile } from './catpawClient.js';
17
17
  import { fetchTraeQuota, checkinTrae, verifyTraeAccount } from './traeClient.js';
18
18
  import { fetchMiniMaxQuota, checkinMiniMax, verifyMiniMaxAccount } from './minimaxClient.js';
19
+ import { fetchDoubaoStatus, checkinDoubao } from './doubaoClient.js';
19
20
  /** 从 userinfo 响应中挑一个可读的显示名 */
20
21
  export function displayNameFrom(ui) {
21
22
  const pick = [ui?.nickname, ui?.name, ui?.username, ui?.email]
@@ -124,8 +125,9 @@ const PRODUCTS = {
124
125
  },
125
126
  workbuddy: {
126
127
  label: 'WorkBuddy',
127
- // 国内版走 billing daily-checkin 签到;国际版没有签到接口,走「活跃领取」(免费档对话探测)
128
- checkin: (account, ctx) => (account.provider === 'workbuddy-intl' ? checkinWorkbuddyIntl(account, ctx) : checkinWorkbuddy(account, ctx)),
128
+ // 国内版走 billing daily-checkin 签到;国际版无签到动作(原「活跃领取」线已删——
129
+ // 逐日主动打上游对话接口博资格的机制整体下线,workbuddy-intl 产品线本身保留,额度查看照常)
130
+ checkin: (account, ctx) => checkinWorkbuddy(account, ctx),
129
131
  quota: (account, ctx) => fetchWorkbuddyQuota(account, ctx),
130
132
  verify: async (account) => {
131
133
  const info = inspectToken(account.token);
@@ -194,6 +196,40 @@ const PRODUCTS = {
194
196
  quota: (account, ctx = {}) => fetchMiniMaxQuota(account, wrapMiniMaxRefresh(account, ctx)),
195
197
  verify: (account) => verifyMiniMaxAccount(account),
196
198
  },
199
+ doubao: {
200
+ label: '豆包',
201
+ // 网页会话捕获线(issue #32):凭据是 doubao.com 会话 cookie(meta.cookieSiteSession.cookie)。
202
+ // 每日签到 +200 额度(服务端幂等);「额度」= 活动累计获得点数(progress.total_amount),
203
+ // 与其他产品线的套餐/积分口径不同,面板以独立产品页呈现。
204
+ checkin: (account) => checkinDoubao(account),
205
+ quota: async (account) => {
206
+ const cookie = account.meta?.cookieSiteSession?.cookie;
207
+ const st = await fetchDoubaoStatus(cookie);
208
+ if (!st.auth) {
209
+ const e = new Error('豆包会话已失效,请重新登录捕获');
210
+ e.auth = true;
211
+ throw e;
212
+ }
213
+ return {
214
+ total: st.totalAmount,
215
+ used: 0,
216
+ remaining: st.totalAmount,
217
+ exceeded: false,
218
+ plan: null,
219
+ planExpiresAt: null,
220
+ parts: st.checkin ? [{
221
+ name: '每日签到',
222
+ total: st.checkin.rewardAmount,
223
+ used: 0,
224
+ remaining: st.checkin.checkedIn ? st.checkin.rewardAmount : 0,
225
+ expiresAt: null,
226
+ recurring: true,
227
+ unit: '额度',
228
+ }] : [],
229
+ empty: st.totalAmount === 0 && !st.checkin,
230
+ };
231
+ },
232
+ },
197
233
  };
198
234
 
199
235
  /** 包裹 MiniMax 的 ctx.onRefresh:回写账号库后,安全同步新凭据回本机 auth.json(仅本机导入账号且客户端未运行时) */
package/src/traeClient.js CHANGED
@@ -52,9 +52,12 @@ function seeded(uid, salt, n) {
52
52
  return out.slice(0, n);
53
53
  }
54
54
 
55
- export function traeDevice(uid, storedDeviceId) {
56
- const d = seeded(uid, 'devid', 15).map((b) => String(48 + (b % 10)));
57
- const m = seeded(uid, 'market', 16);
55
+ export function traeDevice(uid, storedDeviceId, attempt = 0) {
56
+ // attempt 参数:9074 重试时轮换派生设备号(issue #31 报告者实测——派生号被服务端一律拒 9074、
57
+ // 同一时刻真机号可领,说明服务端大概率只认登录时注册的设备号,换号重试才有机会过)。
58
+ // attempt=0 为默认指纹(跨天稳定恒定);attempt>0 派生结果互异但同样确定(同 attempt 重放一致)。
59
+ const d = seeded(`${uid}`, `devid:${attempt}`, 15).map((b) => String(48 + (b % 10)));
60
+ const m = seeded(`${uid}`, `market:${attempt}`, 16);
58
61
  m[6] = (m[6] & 0x0f) | 0x40;
59
62
  m[8] = (m[8] & 0x3f) | 0x80;
60
63
  const h = m.map((b) => b.toString(16).padStart(2, '0')).join('');
@@ -102,12 +105,12 @@ function findField(obj, key, depth = 0) {
102
105
  return undefined;
103
106
  }
104
107
 
105
- /** 统一 POST → { status, body };401 / code 1001 抛 auth 错误(凭据失效) */
106
- async function post(url, account, body = {}) {
108
+ /** 统一 POST → { status, body };401 / code 1001 抛 auth 错误(凭据失效)。attempt 透传给设备派生(9074 轮换重试用) */
109
+ async function post(url, account, body = {}, attempt = 0) {
107
110
  const jwt = account.token;
108
111
  if (!jwt) throw new Error('账号缺少 Trae 登录 token(请到 Trae 客户端登录后用「本机导入」同步)');
109
112
  const uid = account.uid || uidFromJwt(jwt);
110
- const dev = traeDevice(uid || 'anonymous', account.meta?.deviceId);
113
+ const dev = traeDevice(uid || 'anonymous', account.meta?.deviceId, attempt);
111
114
  const res = await fetchJsonRace(url, {
112
115
  method: 'POST',
113
116
  headers: traeHeaders(jwt, dev),
@@ -230,10 +233,12 @@ export async function checkinTrae(account, ctx = {}) {
230
233
  if (/已经签到|已签到|already.*sign/i.test(String(r.message || ''))) {
231
234
  return { ...base, status: 'already', message: withCode(r.message || '今天已签到', r.code), uid };
232
235
  }
233
- // 9074 实测文案「当前参与用户太多,请稍后再试」= 服务端瞬时限流(issue #31 面板实测),
234
- // 不是设备限领——退避重试而非直接放弃;重试耗尽归 failed(不写签到 memo,下一轮还试)
236
+ // 9074 实测两副面孔(issue #31):面板文案「当前参与用户太多」像瞬时限流,但报告者对照
237
+ // 实验显示派生设备号被服务端一律拒 9074、同一时刻真机号可领——服务端大概率只认登录时
238
+ // 注册的设备号。因此重试时**轮换派生设备号**(attempt 递增),退避 + 换号双管齐下;
239
+ // 重试耗尽归 failed(不写签到 memo,下一轮还试)
235
240
  if (r.code === 9074) {
236
- return finalizeClaim(await claimWithRetry(ctx, () => post(CLAIM_URL, account)), base, uid, TRAE_DEFAULT_DAILY_CREDITS);
241
+ return finalizeClaim(await claimWithRetry(ctx, (i) => post(CLAIM_URL, account, {}, i)), base, uid, TRAE_DEFAULT_DAILY_CREDITS);
237
242
  }
238
243
  return { ...base, status: 'failed', message: withCode(r.message || `code ${r.code}`, r.code), uid };
239
244
  }
@@ -246,17 +251,20 @@ export async function checkinTrae(account, ctx = {}) {
246
251
  // claim 接口有时只返回 { code: 0, message: "success" } 而不包含 credits 字段,作为 fallback 避免 +0
247
252
  const expectedCredits = Number(findField(r.body, 'credits') ?? findField(r.body, 'extra_credits')) || TRAE_DEFAULT_DAILY_CREDITS;
248
253
 
249
- return finalizeClaim(await claimWithRetry(ctx, () => post(CLAIM_URL, account)), base, uid, expectedCredits);
254
+ return finalizeClaim(await claimWithRetry(ctx, (i) => post(CLAIM_URL, account, {}, i)), base, uid, expectedCredits);
250
255
  }
251
256
 
252
- /** 9074 瞬时限流退避重试:3 次,间隔 3s/8s/15s;始终失败返回最后一次响应(由 finalizeClaim 归 failed) */
253
- async function claimWithRetry(ctx, attempt) {
257
+ /**
258
+ * claim 重试:间隔 3s/8s/15s,每次重试换一个派生设备号(attempt 1/2/3)。
259
+ * 首次尝试用默认指纹(attempt 0),始终失败返回最后一次响应(由 finalizeClaim 归 failed)。
260
+ */
261
+ async function claimWithRetry(ctx, attemptFn) {
254
262
  const backoff = [0, 3000, 8000, 15000];
255
- let res = await attempt();
263
+ let res = await attemptFn(0);
256
264
  for (let i = 1; i < backoff.length && res.code === 9074; i++) {
257
- ctx.log?.(`Trae 服务端限流(code 9074),${backoff[i] / 1000}s 后重试(${i}/${backoff.length - 1})`);
265
+ ctx.log?.(`Trae 服务端拒绝(code 9074),${backoff[i] / 1000}s 后换设备号重试(${i}/${backoff.length - 1})`);
258
266
  await new Promise((ok) => setTimeout(ok, backoff[i]));
259
- res = await attempt();
267
+ res = await attemptFn(i);
260
268
  }
261
269
  return res;
262
270
  }
package/src/traeLocal.js CHANGED
@@ -16,8 +16,12 @@ import crypto from 'node:crypto';
16
16
  import { execFileSync } from 'node:child_process';
17
17
  import { dataDir } from './store.js';
18
18
 
19
- const APP_NAMES = ['TRAE SOLO CN', 'Trae CN', 'TRAE SOLO', 'Trae'];
20
- const EXE_NAMES = ['TRAE SOLO CN.exe', 'Trae CN.exe', 'TRAE SOLO.exe', 'Trae.exe'];
19
+ // 探测顺序(2026-10-21 TraeWork→TRAE 迁移后定版):新 TRAE CN 优先、国际版次之,
20
+ // SOLO 两个旧变体垫底——SOLO CN 客户端已停发(官方数据迁移页引导全员升新 TRAE),
21
+ // 但迁移期仍有未升级用户,保留兜底;已卸载残留的旧目录读到陈旧凭据的危害大于漏检,
22
+ // 故不再排最前。TRAE_HOME 环境变量可显式指定目录(traeDataDir)。
23
+ const APP_NAMES = ['Trae CN', 'Trae', 'TRAE SOLO CN', 'TRAE SOLO'];
24
+ const EXE_NAMES = ['Trae CN.exe', 'Trae.exe', 'TRAE SOLO CN.exe', 'TRAE SOLO.exe'];
21
25
  const AUTH_KEY = 'iCubeAuthInfo://icube.cloudide';
22
26
  const DC_PREFIX = 'iCubeAuthInfo://icube-dc:';
23
27
  const HEADER = Buffer.from([116, 99, 5, 16, 0, 0]);
@@ -98,7 +102,12 @@ export function readLiveAuth() {
98
102
  if (!storage) return null;
99
103
  const auth = tcDecrypt(storage[AUTH_KEY]);
100
104
  if (!auth || typeof auth.token !== 'string') return null;
101
- return { dataDir: dir, deviceId: deviceIdOf(storage), auth };
105
+ // 国际版防御:信封里带登录 host(api.trae.cn = 国内 / *.trae.ai = 国际,实测 TraeWork→TRAE
106
+ // 迁移后的国际版目录 host 为 growsg-normal.trae.ai)。CD 的 Trae 线(额度/签到/网关)全部
107
+ // 打 api.trae.cn,国际版 JWT 不被认——标记 isIntl 让上层明确提示,而不是导入后对着空额度卡困惑。
108
+ const host = String(auth.host || '');
109
+ const isIntl = Boolean(host) && !/(^|\.)trae\.cn$/.test(new URL(host).hostname || '');
110
+ return { dataDir: dir, deviceId: deviceIdOf(storage), auth, isIntl, host: host || null };
102
111
  }
103
112
 
104
113
  const decodeTasklist = (buf) => {
@@ -157,7 +166,7 @@ export function detectTrae() {
157
166
  export async function liveToAccount() {
158
167
  const live = readLiveAuth();
159
168
  if (!live) return null;
160
- const { auth, deviceId } = live;
169
+ const { auth, deviceId, isIntl } = live;
161
170
  const account = {
162
171
  provider: 'trae',
163
172
  token: auth.token,
@@ -174,6 +183,7 @@ export async function liveToAccount() {
174
183
  deviceId: deviceId || undefined,
175
184
  host: auth.host || undefined,
176
185
  region: auth.userRegion?.region || undefined,
186
+ isIntl: isIntl || undefined,
177
187
  avatarUrl: auth.account?.avatar_url || undefined,
178
188
  capturedAt: new Date().toISOString(),
179
189
  },
package/src/usageSync.js CHANGED
@@ -451,7 +451,7 @@ function collectMimo(files, now = Date.now()) {
451
451
  function collectMirasim(files, endpoint, now = Date.now()) {
452
452
  const out = [];
453
453
  const seen = new Set();
454
- let selfHosted = 0, unsettled = 0;
454
+ let selfHosted = 0, relayed = 0, unsettled = 0;
455
455
  for (const f of files) {
456
456
  for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
457
457
  if (!line.trim()) continue;
@@ -460,6 +460,14 @@ function collectMirasim(files, endpoint, now = Date.now()) {
460
460
  if (!e || typeof e !== 'object') continue;
461
461
  if (e.id) { if (seen.has(e.id)) continue; seen.add(e.id); }
462
462
  if (isSelfHostedUpstream(e.upstreamHost, endpoint)) { selfHosted++; continue; }
463
+ // 只同步走 mirasim 官方中继(relay.mirasim.ai)的调用(2026-10-21 用户拍板):
464
+ // mirasim IDE 里直连上游的调用,上游就是本机 10Router——10R 已原生记账(落在
465
+ // codebuddy-cn 等连接名下),CD 再导一遍就是同一批调用记两笔(10R 用量表里
466
+ // glm-5.3-flashx 出现 codebuddy-cn 3,800 + mirasim-anthropic 2,917 两行重复)。
467
+ // viaRelay=true 的行走 mirasim 官方云中继,10R 没见过,才是 CD 该补的账。
468
+ // direct 行 upstreamHost 全为 null 无从判上游,viaRelay 就是现成的分界(实测
469
+ // relay 行全部是 claude/kimi 家族、direct 行全部是 glm/deepseek/gemini,零交叉)。
470
+ if (!e.viaRelay) { relayed++; continue; }
463
471
  if (!((e.input || 0) + (e.output || 0) + (e.cacheRead || 0) + (e.cacheWrite || 0))) continue;
464
472
  // 刚发生的调用可能还在补字段(durationMs / token 等),定型前先不发
465
473
  const tsMs = Date.parse(e.ts);
@@ -467,7 +475,7 @@ function collectMirasim(files, endpoint, now = Date.now()) {
467
475
  out.push(convertMirasimRow(e));
468
476
  }
469
477
  }
470
- return { entries: out, notes: (selfHosted ? [`跳过 ${selfHosted} 行经 10Router 中转的调用(已在 10Router 记账)`] : []).concat(unsettled ? [`暂不同步 ${unsettled} 行 1 小时内的新调用(等记录定型)`] : []) };
478
+ return { entries: out, notes: (selfHosted ? [`跳过 ${selfHosted} 行经 10Router 中转的调用(已在 10Router 记账)`] : []).concat(relayed ? [`跳过 ${relayed} 行直连上游的调用(上游为本机 10Router,已在 10Router 记账)`] : []).concat(unsettled ? [`暂不同步 ${unsettled} 行 1 小时内的新调用(等记录定型)`] : []) };
471
479
  }
472
480
 
473
481
  /** 读取某来源的全部行:{ entries, notes, files, sentTotals? };来源不存在返回 entries=[] */
@@ -94,51 +94,6 @@ async function post(account, path, token) {
94
94
  return { status: res.status, body, text };
95
95
  }
96
96
 
97
- // 国际版「活跃领取」探测:官方规则为「当天有 ≥1 次有效对话请求即视为活跃用户,发放每日赠送积分」。
98
- // 没有签到接口,所以每天发一条免费档模型(rateMultiplier 0,~0 消耗)的极短流式请求。
99
- // 对齐 10router src/sse/services/codebuddyCheckin.js 的 intl 探测(stream 网关的系统提示与 typed blocks 是必需的,否则 11101)。
100
- const INTL_PROBE_MODEL = 'hy4-preview';
101
- async function postIntlProbe(account, token) {
102
- const res = await fetchJsonRace(`https://${apiHost(account)}/v2/chat/completions`, {
103
- method: 'POST',
104
- headers: { ...headersFor(account, token), 'Content-Type': 'application/json', Accept: 'text/event-stream' },
105
- body: JSON.stringify({
106
- model: INTL_PROBE_MODEL,
107
- stream: true,
108
- max_tokens: 16,
109
- messages: [
110
- { role: 'system', content: 'You are CodeBuddy Code.' },
111
- { role: 'user', content: [{ type: 'text', text: 'hi' }] },
112
- ],
113
- }),
114
- timeoutMs: Math.max(FETCH_TIMEOUT_MS, 25_000),
115
- });
116
- // 2xx = 会话建立成功,把 SSE 流排干(最多几个 token)
117
- const text = await res.text().catch(() => '');
118
- return { status: res.status, text };
119
- }
120
-
121
- /**
122
- * 国际版「活跃领取」:一次免费档对话请求使账号成为当日活跃用户。
123
- * 返回与每日签到一致的结果对象(session-ok → checked-in,claimedAmount 0 表示额度随官方结算发放)。
124
- */
125
- export async function checkinWorkbuddyIntl(account, { onRefresh } = {}) {
126
- const label = account.name || account.id;
127
- const base = { accountId: account.id, account: label, provider: account.provider };
128
- try {
129
- const r = await withToken(account, (t) => postIntlProbe(account, t), onRefresh);
130
- if (r.status === 401) return { ...base, status: 'failed', error: '鉴权失败(401),token 已失效,请在客户端重新登录后重新导入' };
131
- if (r.status === 429) return { ...base, status: 'no-activity', message: '今日额度已耗尽,活跃请求暂不可达(等官方发放后再试)', claimedAmount: 0 };
132
- if (r.status >= 200 && r.status < 300) {
133
- return { ...base, uid: account.uid, status: 'checked-in', claimedAmount: 0, message: '已发送活跃请求(保持活跃,活跃不等于必有奖励)' };
134
- }
135
- const m = (() => { try { return JSON.parse(r.text)?.error?.data?.msg || ''; } catch { return ''; } })();
136
- return { ...base, status: 'failed', error: `活跃请求失败:HTTP ${r.status} ${m || r.text.slice(0, 120)}`.trim() };
137
- } catch (e) {
138
- return { ...base, status: 'failed', error: e?.message || '网络错误' };
139
- }
140
- }
141
-
142
97
  export function isExpired(account, now = Date.now()) {
143
98
  const exp = account.expiresAt || inspectToken(account.token).expiresAt;
144
99
  return Boolean(exp) && new Date(exp).getTime() - EXPIRY_SKEW_MS <= now;