creditdaddy 1.5.0 → 1.5.2

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.
@@ -321,3 +321,90 @@ export async function fetchWorkbuddyQuota(account, { onRefresh } = {}) {
321
321
  const sum = (k) => Math.round(parts.reduce((s, p) => s + p[k], 0) * 100) / 100;
322
322
  return { total: sum('total'), used: sum('used'), remaining: sum('remaining'), parts, exceeded: false };
323
323
  }
324
+
325
+ // ─── Web 临时活动领取(额外补偿 / 首月赠礼)───
326
+ // 网页 usercenter 的活动端点(无 /v2 前缀,与桌面 /v2/billing/meter/* 并存于同一 APISIX 网关)。
327
+ // 事实源 = usercenter bundle(index-D7kQlGMt.js):cn.post("/billing/meter/compensation-status")
328
+ // / claim-compensation / check-gift-claimed / claim-gift;响应信封 { code, msg, data }。
329
+ // 实测 CD 存的 Keycloak Bearer JWT 直接可用(无需网页会话 cookie)——批量领取免逐个网页点击。
330
+ const WB_WEB_ACT = {
331
+ compensation: { label: '额外补偿', statusPath: '/billing/meter/compensation-status', claimPath: '/billing/meter/claim-compensation' },
332
+ gift: { label: '首月赠礼', statusPath: '/billing/meter/check-gift-claimed', claimPath: '/billing/meter/claim-gift' },
333
+ };
334
+ export const WB_ACTIVITY_KINDS = Object.keys(WB_WEB_ACT);
335
+
336
+ async function postWeb(account, path, token) {
337
+ // fetchJsonRace 统一出口:连接段限时 + 配置了出口代理时直连/代理双路兜底
338
+ const res = await fetchJsonRace(`https://${apiHost(account)}${path}`, {
339
+ method: 'POST',
340
+ headers: { ...headersFor(account, token), 'X-Client-Platform': 'web' },
341
+ body: '{}',
342
+ timeoutMs: FETCH_TIMEOUT_MS,
343
+ });
344
+ const text = await res.text();
345
+ let body = null;
346
+ try { body = text ? JSON.parse(text) : null; } catch {}
347
+ return { status: res.status, body, text };
348
+ }
349
+
350
+ /**
351
+ * 查某账号的临时活动状态(补偿 + 赠礼)。返回逐项 { kind, label, active, claimed, creditNum, endTime, claimable }。
352
+ * fail-soft:单项失败不阻断其余项,error 字段带原因。
353
+ */
354
+ export async function fetchWbActivities(account, { onRefresh } = {}) {
355
+ const out = [];
356
+ for (const [kind, spec] of Object.entries(WB_WEB_ACT)) {
357
+ try {
358
+ const r = await withToken(account, (t) => postWeb(account, spec.statusPath, t), onRefresh);
359
+ if (r.status === 401 || r.status === 403) { out.push({ kind, label: spec.label, error: `鉴权失败(HTTP ${r.status}),token 已失效` }); continue; }
360
+ if (r.status !== 200 || r.body?.code !== 0) { out.push({ kind, label: spec.label, error: r.body?.msg || `状态查询失败:HTTP ${r.status}` }); continue; }
361
+ const d = r.body.data || {};
362
+ const active = Boolean(d.active);
363
+ const claimed = Boolean(d.claimed);
364
+ out.push({
365
+ kind, label: spec.label,
366
+ active, claimed,
367
+ creditNum: Number(d.credit_num) || 0,
368
+ endTime: d.end_time || null,
369
+ claimable: active && !claimed,
370
+ });
371
+ } catch (e) {
372
+ out.push({ kind, label: spec.label, error: e?.message || '网络错误' });
373
+ }
374
+ }
375
+ return out;
376
+ }
377
+
378
+ /**
379
+ * 领取某临时活动(幂等:先查状态,未领且 active 才真领)。返回与签到一致的结果对象
380
+ * (status: checked-in / already / no-activity / failed,claimedAmount = 积分数)。
381
+ */
382
+ export async function claimWbActivity(account, kind, { onRefresh } = {}) {
383
+ const spec = WB_WEB_ACT[kind];
384
+ const label = account.name || account.id;
385
+ const base = { accountId: account.id, account: label, provider: account.provider, kind, activityLabel: spec?.label || kind };
386
+ if (!spec) return { ...base, status: 'failed', error: `未知活动 ${kind}` };
387
+ try {
388
+ // 1) 先查状态:已领 / 未激活则不发领取请求(幂等,多号批量也安全)
389
+ const st = await withToken(account, (t) => postWeb(account, spec.statusPath, t), onRefresh);
390
+ if (st.status === 401 || st.status === 403) return { ...base, status: 'failed', error: `鉴权失败(HTTP ${st.status}),token 已失效` };
391
+ if (st.status !== 200 || st.body?.code !== 0) return { ...base, status: 'failed', error: st.body?.msg || `状态查询失败:HTTP ${st.status}` };
392
+ const d = st.body.data || {};
393
+ const creditNum = Number(d.credit_num) || 0;
394
+ if (!d.active) return { ...base, status: 'no-activity', message: `${spec.label}:活动未开启`, claimedAmount: 0 };
395
+ if (d.claimed) return { ...base, status: 'already', message: `${spec.label}:已领取`, claimedAmount: creditNum };
396
+
397
+ // 2) 领取
398
+ const r = await withToken(account, (t) => postWeb(account, spec.claimPath, t), onRefresh);
399
+ if (r.status === 401 || r.status === 403) return { ...base, status: 'failed', error: `鉴权失败(HTTP ${r.status}),token 已失效` };
400
+ const code = r.body?.code ?? r.body?.error_code;
401
+ const msg = r.body?.msg ?? r.body?.message ?? '';
402
+ if (r.status >= 200 && r.status < 300 && (code === 0 || code === undefined)) {
403
+ return { ...base, status: 'checked-in', claimedAmount: creditNum, message: `${spec.label}领取成功` };
404
+ }
405
+ if (/已领取|already|repeat|重复/i.test(msg)) return { ...base, status: 'already', message: `${spec.label}:${msg}`, claimedAmount: 0 };
406
+ return { ...base, status: 'failed', error: `${spec.label}领取失败:HTTP ${r.status} ${msg || r.text.slice(0, 120)}`.trim() };
407
+ } catch (e) {
408
+ return { ...base, status: 'failed', error: e?.message || '网络错误' };
409
+ }
410
+ }
@@ -321,12 +321,22 @@ export async function fetchJsonRace(url, { method = 'GET', headers = {}, body =
321
321
  const order = useProxy
322
322
  ? (preferProxy || proxyFirst() ? ['viaProxy', 'direct'] : ['direct', 'viaProxy'])
323
323
  : ['direct'];
324
- let lastErr = null;
324
+ // 双路都失败时聚合错误再抛:只抛最后一条会把「直连网络抖动」伪装成「代理挂了」
325
+ // (或反之)——issue #31 面板只见 connect ECONNREFUSED 127.0.0.1:3213,看不到直连
326
+ // 那条路到底发生了什么,排查方向直接被带偏
327
+ const errs = [];
325
328
  for (const which of order) {
326
329
  const attempts = makeAttempt();
327
- try { return await attempts[which](); } catch (e) { lastErr = e; }
330
+ try { return await attempts[which](); } catch (e) { errs.push([which, e]); }
331
+ }
332
+ if (errs.length > 1) {
333
+ const detail = errs.map(([w, e]) => `${w === 'direct' ? '直连' : '代理'}: ${e?.message || e}`).join(';');
334
+ const last = errs[errs.length - 1][1];
335
+ // 在最后一条错误上改写 message(保留 err.code / instanceof,调用方判定不受影响)
336
+ last.message = detail;
337
+ throw last;
328
338
  }
329
- throw lastErr || new Error('fetch failed');
339
+ throw errs[0]?.[1] || new Error('fetch failed');
330
340
  }
331
341
 
332
342
  /** 领取失败码 → 中文提示(取自 zcode-switch i18n.rs) */