creditdaddy 1.3.2 → 1.4.1

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.
@@ -12,7 +12,20 @@
12
12
  */
13
13
 
14
14
  import { logger } from './logger.js';
15
+ import { t } from './i18n.js';
15
16
  import { fetchClaimPlans, claimPlan, fetchCaptchaConfig } from './zcodeClient.js';
17
+ import { withAccounts } from './store.js';
18
+
19
+ // 额满(1005)时消费服务端 nextAt(plan.ends_at):
20
+ // 下轮自动轮询(claimIntervalMin 分钟一触)在 nextAt 之前跳过该号,不做无谓抢。
21
+ // 进程内缓存 + 落盘 account.meta.zcodeClaimNextAt 双轨(重启保护)。
22
+ const snoozeUntil = new Map(); // accountId → ms 时间戳
23
+
24
+ function snoozeUntilMs(account) {
25
+ const meta = Number(new Date(account?.meta?.zcodeClaimNextAt || 0)) || 0;
26
+ const proc = snoozeUntil.get(account.id) || 0;
27
+ return Math.max(meta, proc);
28
+ }
16
29
 
17
30
  // 桌面版(Electron 主进程)注册的隐藏窗口验证码实现;NAS / CLI 为 null(走「需手动领取」分支)
18
31
  let captchaProvider = null;
@@ -28,11 +41,18 @@ const grantsText = (plan) => (plan.grants && plan.grants.length ? plan.grants.jo
28
41
  */
29
42
  export async function zcodeAutoClaim(account) {
30
43
  const label = account.name || account.uid || account.id;
44
+ {
45
+ const until = snoozeUntilMs(account);
46
+ if (until && until > Date.now()) {
47
+ logger.debug('ZCODE-CLAIM', '{label} 名额已领完,跳过直到 {until}(服务端计划结束点)', { label, until: new Date(until).toISOString() });
48
+ return null;
49
+ }
50
+ }
31
51
  let plans;
32
52
  try {
33
53
  plans = (await fetchClaimPlans(account)).plans;
34
54
  } catch (e) {
35
- logger.debug('ZCODE-CLAIM', `${label} 活动查询失败:${e.message}`);
55
+ logger.debug('ZCODE-CLAIM', '{label} 活动查询失败:{err}', { label, err: e.message });
36
56
  return null;
37
57
  }
38
58
  if (!plans.length) return null;
@@ -41,6 +61,7 @@ export async function zcodeAutoClaim(account) {
41
61
  let anySuccess = false;
42
62
  let needManual = false;
43
63
 
64
+ let snoozeAt = 0;
44
65
  for (const plan of plans) {
45
66
  const planName = plan.name || plan.planId;
46
67
  // 第 2 步:先试不带验证码
@@ -48,17 +69,20 @@ export async function zcodeAutoClaim(account) {
48
69
  const r = await claimPlan(account, plan.planId, {});
49
70
  claims.push({ plan: planName, via: 'direct', ok: true });
50
71
  anySuccess = true;
51
- logger.info('ZCODE-CLAIM', `${label} 已领取「${planName}」(免验证码)${grantsText(plan) ? ':' + grantsText(plan) : ''}`);
72
+ logger.info('ZCODE-CLAIM', '{label} 已领取「{plan}」(免验证码){grants}', { label, plan: planName, grants: grantsText(plan) ? ':' + grantsText(plan) : '' });
52
73
  continue;
53
74
  } catch (e) {
54
75
  if (e.code === 1003) {
55
76
  claims.push({ plan: planName, ok: true, already: true });
56
- logger.info('ZCODE-CLAIM', `${label}「${planName}」已领取过`);
77
+ logger.info('ZCODE-CLAIM', '{label}「{plan}」已领取过', { label, plan: planName });
57
78
  continue;
58
79
  }
80
+ if (e.code === 1005 && e.nextAt) {
81
+ snoozeAt = Math.max(snoozeAt, Number(new Date(e.nextAt)) || 0);
82
+ }
59
83
  if (e.code !== 3007 && e.code !== 3001) {
60
84
  claims.push({ plan: planName, ok: false, message: e.message });
61
- logger.warn('ZCODE-CLAIM', `${label} 领取「${planName}」失败:${e.message}`);
85
+ logger.warn('ZCODE-CLAIM', '{label} 领取「{plan}」失败:{err}', { label, plan: planName, err: e.message });
62
86
  continue;
63
87
  }
64
88
  }
@@ -66,7 +90,7 @@ export async function zcodeAutoClaim(account) {
66
90
  if (!captchaProvider) {
67
91
  needManual = true;
68
92
  claims.push({ plan: planName, ok: false, needManual: true });
69
- logger.info('ZCODE-CLAIM', `${label}「${planName}」需要验证码,本环境无法自动通过,请在面板手动领取`);
93
+ logger.info('ZCODE-CLAIM', '{label}「{plan}」需要验证码,本环境无法自动通过,请在面板手动领取', { label, plan: planName });
70
94
  continue;
71
95
  }
72
96
  try {
@@ -76,13 +100,29 @@ export async function zcodeAutoClaim(account) {
76
100
  await claimPlan(account, plan.planId, { captchaParam, region });
77
101
  claims.push({ plan: planName, via: 'captcha', ok: true });
78
102
  anySuccess = true;
79
- logger.info('ZCODE-CLAIM', `${label} 已领取「${planName}」(静默验证码)${grantsText(plan) ? ':' + grantsText(plan) : ''}`);
103
+ logger.info('ZCODE-CLAIM', '{label} 已领取「{plan}」(静默验证码){grants}', { label, plan: planName, grants: grantsText(plan) ? ':' + grantsText(plan) : '' });
80
104
  } catch (e2) {
105
+ if (e2.code === 1005 && e2.nextAt) {
106
+ snoozeAt = Math.max(snoozeAt, Number(new Date(e2.nextAt)) || 0);
107
+ }
81
108
  needManual = true;
82
109
  claims.push({ plan: planName, ok: false, needManual: true, message: e2.message });
83
- logger.warn('ZCODE-CLAIM', `${label}「${planName}」验证码未完成:${e2.message}(可在面板手动领取)`);
110
+ logger.warn('ZCODE-CLAIM', '{label}「{plan}」验证码未完成:{err}(可在面板手动领取)', { label, plan: planName, err: e2.message });
84
111
  }
85
112
  }
113
+ // 1005 名额已满 → 按服务端 ends_at 将本账号挂进跳过窗口,下一轮自动轮询在该时间点前不再空打
114
+ if (snoozeAt > 0) {
115
+ snoozeUntil.set(account.id, snoozeAt);
116
+ try {
117
+ await withAccounts((list) => {
118
+ const cur = list.find((a) => a.id === account.id);
119
+ if (cur) {
120
+ cur.meta = cur.meta && typeof cur.meta === 'object' && !Array.isArray(cur.meta) ? cur.meta : {};
121
+ cur.meta.zcodeClaimNextAt = new Date(snoozeAt).toISOString();
122
+ }
123
+ });
124
+ } catch {}
125
+ }
86
126
 
87
127
  const names = claims.filter((c) => c.ok && !c.already).map((c) => c.plan);
88
128
  const status = names.length ? 'checked-in'
@@ -293,7 +293,7 @@ function combineSignals({ wallSignal, connectSignal, external }) {
293
293
  * 跑几分钟,不能因为「连接快、生成慢」被误杀。
294
294
  * 不传 connectMs 时退回单段 timeoutMs(旧行为,所有既有调用方不受影响)。
295
295
  */
296
- export async function fetchJsonRace(url, { method = 'GET', headers = {}, body = null, timeoutMs = 15000, connectMs = null, signal = null } = {}) {
296
+ export async function fetchJsonRace(url, { method = 'GET', headers = {}, body = null, timeoutMs = 15000, connectMs = null, signal = null, preferProxy = false, redirect = 'manual' } = {}) {
297
297
  const useProxy = Boolean(effectiveProxy()) && !proxyBypass(url);
298
298
  const twoPhase = Number.isFinite(connectMs) && connectMs > 0 && connectMs < timeoutMs;
299
299
  // 超时信号必须每次尝试各建一个:共用一个的话,代理优先模式下第一条路吃完 timeoutMs,
@@ -302,7 +302,8 @@ export async function fetchJsonRace(url, { method = 'GET', headers = {}, body =
302
302
  const wallSignal = AbortSignal.timeout(timeoutMs);
303
303
  const connectSignal = twoPhase ? AbortSignal.timeout(connectMs) : null;
304
304
  const { signal: abortSignal, releaseConnect } = combineSignals({ wallSignal, connectSignal, external: signal });
305
- const base = { method, headers: { ...headers }, ...(body != null ? { body: typeof body === 'string' ? body : JSON.stringify(body) } : {}), signal: abortSignal };
305
+ // redirect 默认 manual:follow 的 301/302 会把 POST 转 GET 丢 body、307 会把大 body 重放。
306
+ const base = { method, headers: { ...headers }, ...(body != null ? { body: typeof body === 'string' ? body : JSON.stringify(body) } : {}), signal: abortSignal, redirect };
306
307
  return {
307
308
  // 响应头到达 = 连接段成功:摘掉 connectSignal 转发,此后只有墙钟/外部取消能中断流
308
309
  direct: async () => { const r = await fetch(url, base); releaseConnect(); return r; },
@@ -315,7 +316,11 @@ export async function fetchJsonRace(url, { method = 'GET', headers = {}, body =
315
316
  },
316
317
  };
317
318
  };
318
- const order = useProxy ? (proxyFirst() ? ['viaProxy', 'direct'] : ['direct', 'viaProxy']) : ['direct'];
319
+ // preferProxy:该域名按 IP 识别地区(如 qoder.com 国际版),配置了代理就该优先走代理、
320
+ // 直连只作兜底——直连「成功返回 403 风控页」也算成功响应,走默认顺序永远不会轮到代理
321
+ const order = useProxy
322
+ ? (preferProxy || proxyFirst() ? ['viaProxy', 'direct'] : ['direct', 'viaProxy'])
323
+ : ['direct'];
319
324
  let lastErr = null;
320
325
  for (const which of order) {
321
326
  const attempts = makeAttempt();
@@ -39,7 +39,8 @@ export function getZcodeCompletionProvider() { return completionProvider; }
39
39
  import { loadAccounts, loadSettings, saveSettings, loadState, withState } from './store.js';
40
40
  import crypto from 'node:crypto';
41
41
  import os from 'node:os';
42
- import { logger, summarizeAttempts } from './logger.js';
42
+ import { logger, summarizeAttempts } from './logger.js';
43
+ import { t } from './i18n.js';
43
44
 
44
45
  const PLAN_MESSAGES_URL = 'https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages';
45
46
  const CAPTCHA_CACHE_TTL_MS = 30_000;
@@ -108,7 +109,7 @@ function isDead(account) {
108
109
  if (fp === credFingerprint(account)) return true;
109
110
  dead.delete(account.id);
110
111
  cooling.delete(account.id);
111
- logger.info('ZCODE-GW', `${account.name || account.uid || account.id} 凭据已更新,解除拉黑`);
112
+ logger.info('ZCODE-GW', '{label} 凭据已更新,解除拉黑', { label: account.name || account.uid || account.id });
112
113
  return false;
113
114
  }
114
115
 
@@ -368,7 +369,13 @@ export function remoteAllowed(remote, allowList) {
368
369
  return (Array.isArray(allowList) ? allowList : []).some((entry) => {
369
370
  const e = String(entry || '').trim();
370
371
  if (!e) return false;
371
- if (e.endsWith('*')) return host.toLowerCase().startsWith(e.slice(0, -1).toLowerCase());
372
+ if (e.endsWith('*')) {
373
+ // 通配要求带点段尾:防止 192.168.31.1* 这类把 .100-.199 整段一起放行的怪手。
374
+ // 要扩整段子网,把写法统一为 192.168.31.*(带尾句点)。
375
+ const prefix = e.slice(0, -1);
376
+ if (!prefix.endsWith('.')) return false;
377
+ return host.toLowerCase().startsWith(prefix.toLowerCase());
378
+ }
372
379
  return host.toLowerCase() === e.toLowerCase();
373
380
  });
374
381
  }
@@ -487,7 +494,7 @@ export async function handleGateway(req, res) {
487
494
  if (isAuthError(200, text)) {
488
495
  markDead(account);
489
496
  attempted.push({ account: label, ok: false, error: 'JWT 已失效,账号拉黑' });
490
- logger.warn('ZCODE-GW', `${label} JWT 失效(200 业务码)`);
497
+ logger.warn('ZCODE-GW', '{label} JWT 失效(200 业务码)', { label });
491
498
  continue;
492
499
  }
493
500
  if (isExhausted(200, text)) {
@@ -509,20 +516,20 @@ export async function handleGateway(req, res) {
509
516
  }
510
517
  continue;
511
518
  }
512
- logger.warn('ZCODE-GW', `${label} 上游 200 业务错误:${formatUpstreamError(200, text)} (${text.slice(0, 80)})`);
519
+ logger.warn('ZCODE-GW', '{label} 上游 200 业务错误:{err} ({snippet})', { label, err: formatUpstreamError(200, text), snippet: text.slice(0, 80) });
513
520
  res.writeHead(502, { 'Content-Type': 'application/json' });
514
521
  res.end(text);
515
522
  return true;
516
523
  }
517
524
  // 真 Message(非流式)——原样转发
518
- logger.debug('ZCODE-GW', `${label} 补全成功 (${upstream.status})`);
525
+ logger.debug('ZCODE-GW', '{label} 补全成功 ({status})', { label, status: upstream.status });
519
526
  stats.lastAccount = label;
520
527
  cooling.delete(account.id);
521
528
  res.writeHead(upstream.status, { 'Content-Type': ct || 'application/json', 'Cache-Control': 'no-cache' });
522
529
  res.end(text);
523
530
  return true;
524
531
  }
525
- logger.debug('ZCODE-GW', `${label} 补全成功 (${upstream.status})`);
532
+ logger.debug('ZCODE-GW', '{label} 补全成功 ({status})', { label, status: upstream.status });
526
533
  stats.lastAccount = label;
527
534
  cooling.delete(account.id);
528
535
  // 注意:undici 已自动解压上游 gzip——body 是明文,绝不能再带 content-encoding 头
@@ -543,7 +550,7 @@ export async function handleGateway(req, res) {
543
550
  }
544
551
  } catch (e) {
545
552
  // 客户端主动断开(点停止/换一句)就会在这里结束——属正常收尾,不是故障,降为 debug
546
- logger.debug('ZCODE-GW', `流式转发中断:${e.message}`);
553
+ logger.debug('ZCODE-GW', '流式转发中断:{err}', { err: e.message });
547
554
  }
548
555
  res.end();
549
556
  return true;
@@ -554,9 +561,9 @@ export async function handleGateway(req, res) {
554
561
  if (isCaptchaError(upstream.status, text)) {
555
562
  invalidateCaptcha();
556
563
  attempted.push({ account: label, ok: false, error: `验证码被拒(${upstream.status})`, captcha: true });
557
- logger.warn('ZCODE-GW', `${label} 验证码被拒 (${formatUpstreamError(upstream.status, text)})`);
564
+ logger.warn('ZCODE-GW', '{label} 验证码被拒 ({err})', { label, err: formatUpstreamError(upstream.status, text) });
558
565
  try { captcha = await ensureCaptcha(true); } catch (e) {
559
- logger.warn('ZCODE-GW', `验证码重解失败:${e.message}`);
566
+ logger.warn('ZCODE-GW', '验证码重解失败:{err}', { err: e.message });
560
567
  captcha = null;
561
568
  }
562
569
  continue;
@@ -564,31 +571,31 @@ export async function handleGateway(req, res) {
564
571
  if (isAuthError(upstream.status, text)) {
565
572
  markDead(account);
566
573
  attempted.push({ account: label, ok: false, error: 'JWT 已失效,账号拉黑(重新登录后再导入)' });
567
- logger.debug('ZCODE-GW', `${label} JWT 失效,拉黑`);
574
+ logger.debug('ZCODE-GW', '{label} JWT 失效,拉黑', { label });
568
575
  continue;
569
576
  }
570
577
  if (isExhausted(upstream.status, text)) {
571
578
  markCooling(account.id, 30 * 60_000);
572
579
  attempted.push({ account: label, ok: false, error: '额度不足/无资源包' });
573
- logger.debug('ZCODE-GW', `${label} 额度不足 (${upstream.status})`);
580
+ logger.debug('ZCODE-GW', '{label} 额度不足 ({status})', { label, status: upstream.status });
574
581
  continue;
575
582
  }
576
583
  if (upstream.status === 429) {
577
584
  markCooling(account.id);
578
585
  attempted.push({ account: label, ok: false, error: '429 限流,冷却 5 分钟' });
579
- logger.debug('ZCODE-GW', `${label} 429 限流,冷却 5 分钟`);
586
+ logger.debug('ZCODE-GW', '{label} 429 限流,冷却 5 分钟', { label });
580
587
  continue;
581
588
  }
582
589
  // 其他错误:友好化展示 + 原样透传给客户端
583
590
  const errorSummary = formatUpstreamError(upstream.status, text);
584
- logger.warn('ZCODE-GW', `${label} 上游错误 ${upstream.status} — ${errorSummary}`);
591
+ logger.warn('ZCODE-GW', '{label} 上游错误 {status} — {summary}', { label, status: upstream.status, summary: errorSummary });
585
592
  res.writeHead(upstream.status, { 'Content-Type': 'application/json' });
586
593
  res.end(text);
587
594
  return true;
588
595
  }
589
596
 
590
597
  const failedDetail = summarizeAttempts(attempted);
591
- logger.warn('ZCODE-GW', `全部 ${attempted.length} 次尝试失败${failedDetail ? ':' + failedDetail : ''}(下一请求自动重试)`);
598
+ logger.warn('ZCODE-GW', '全部 {n} 次尝试失败{detail}(下一请求自动重试)', { n: attempted.length, detail: failedDetail ? ':' + failedDetail : '' });
592
599
  res.writeHead(502, { 'Content-Type': 'application/json' });
593
600
  res.end(JSON.stringify({ error: 'ZCode 网关:所有账号尝试均失败', attempts: attempted }));
594
601
  return true;