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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * 首批 source(issue #24「建议的首批接线」):
10
10
  * - `tokenharbor` :TH 免费额度网页读数(CD 已捕获的网页会话 → dashboard 解析);
11
- * - `minimax-code` :MiniMax Code 免费线算力币/签到额度——10Router 的 minimax-free
11
+ * - `minimax-free` :MiniMax Code 免费线算力币/签到额度——挂在 10Router 的 minimax-free 提供商(订阅线 minimax-code 是 10R 自己的直连线,两者不同卡)
12
12
  * 路由卡走 CD 本地网关,配额只有 CD 查得到,推数字正好补它的盲区。
13
13
  *
14
14
  * 降级语义(issue #24):会话失效 / 全部解析失败时改推 `message` 文字状态卡,
@@ -102,13 +102,62 @@ export function nextUtc8Midnight(now = new Date()) {
102
102
  return new Date((Math.floor(ms / dayMs) + 1) * dayMs - 8 * 3600_000).toISOString();
103
103
  }
104
104
 
105
+ /** 生成单个 MiniMax 账号的外配额卡 source id */
106
+ export function buildMiniMaxAccountSource(account) {
107
+ const rawId = account.uid || account.id || 'default';
108
+ const clean = String(rawId).toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 18);
109
+ return `minimax-free-${clean || 'acc'}`.slice(0, 32);
110
+ }
111
+
112
+ /**
113
+ * 组装单个 MiniMax 账号的单卡载荷(minimax-code 同款单值卡):
114
+ * 单账号独立一张卡,卡面副标题为账号名称(如「主」、「备」),
115
+ * 单行 Credits 储值余额,右侧带有只读连续签到徽章。
116
+ */
117
+ export function buildMiniMaxAccountPayload(account, quotaData, checkinData, { resetAt } = {}) {
118
+ const source = buildMiniMaxAccountSource(account);
119
+ const remaining = Number(quotaData?.remaining ?? quotaData?.total);
120
+ const label = String(account?.name || account?.uid || 'MiniMax').slice(0, 100);
121
+ const payload = {
122
+ provider: 'minimax-free',
123
+ label,
124
+ plan: quotaData?.plan || 'Free',
125
+ };
126
+
127
+ if (Number.isFinite(remaining) && !quotaData?.error) {
128
+ const row = {
129
+ name: 'Credits',
130
+ used: 0,
131
+ total: Math.max(remaining, 0) || 0.000001,
132
+ displayRemaining: true,
133
+ isCreditBalance: true,
134
+ recurring: true,
135
+ resetAt: resetAt || nextUtc8Midnight(),
136
+ };
137
+ if (checkinData) {
138
+ if (checkinData.checkedToday !== undefined) row.checkinToday = checkinData.checkedToday === true;
139
+ if (Number.isFinite(checkinData.streakDays) && checkinData.streakDays > 0) {
140
+ row.streakDays = checkinData.streakDays;
141
+ }
142
+ }
143
+ payload.quotas = { credits: row };
144
+ } else {
145
+ payload.message = quotaData?.error || t('MiniMax 账号额度暂不可查(凭据失效需在 CreditDaddy 重新登录,或官方接口异常稍后自动重试)');
146
+ }
147
+ assertScalarOnly(payload, source);
148
+ return { source, payload };
149
+ }
150
+
105
151
  /**
106
- * 组装 minimax-code 卡片载荷(issue #24 行字段建议):
152
+ * 组装 minimax-code 卡片载荷(历史兼容多行卡方法):
107
153
  * 每账号一行「存量」——`used=0 / total=剩余算力币 / displayRemaining=true`,
108
154
  * `resetAt` 给次日零点(免费线每日刷新口径)。全部查询失败时降级为文字状态卡。
155
+ * 每行可带只读签到状态(checkinToday / streakDays)——10R 白名单放行后卡面显示「今日已签」;
156
+ * 旧版 10R 无此字段自动忽略(向前兼容)。签到动作不进 10R(只读边界,方案 B)。
109
157
  * @param fetchQuota 注入的「account → 归一化额度」查询器(生产 = fetchMiniMaxQuota)
158
+ * @param fetchCheckin 注入的「account → 签到状态」查询器(生产 = fetchMiniMaxCheckinStatus;测试可注入)
110
159
  */
111
- export async function buildMiniMaxPayload(accounts, fetchQuota, { resetAt } = {}) {
160
+ export async function buildMiniMaxPayload(accounts, fetchQuota, { resetAt, fetchCheckin } = {}) {
112
161
  const rows = {};
113
162
  let failed = 0;
114
163
  for (const a of accounts.slice(0, MAX_ROWS)) {
@@ -117,24 +166,40 @@ export async function buildMiniMaxPayload(accounts, fetchQuota, { resetAt } = {}
117
166
  const remaining = Number(q?.remaining);
118
167
  if (!Number.isFinite(remaining) || q?.error) { failed++; continue; }
119
168
  const i = Object.keys(rows).length;
120
- rows[rowKey(i)] = {
169
+ const row = {
121
170
  name: (a.name || a.uid || 'MiniMax').slice(0, 80),
122
171
  used: 0,
123
172
  total: Math.max(remaining, 0) || 0.000001, // total 必须 >0(10Router positiveNumber);0 余额给最小正数让卡片显示「耗尽」
124
173
  displayRemaining: true,
174
+ // 算力币是储值余额不是订阅包:10R 按单值卡渲染(无 X/Y 进度条与重置倒计时,
175
+ // 与 minimax-code 订阅线 Credits 卡同款)。10R 旧版无此字段自动忽略。
176
+ isCreditBalance: true,
125
177
  recurring: true,
126
178
  resetAt: resetAt || nextUtc8Midnight(),
127
179
  };
180
+ // 签到状态(可选注入,fetch 失败 / 无注入则不带——字段缺席即「状态未知」,10R 不画徽章)
181
+ if (fetchCheckin) {
182
+ try {
183
+ const c = await fetchCheckin(a);
184
+ if (c) {
185
+ row.checkinToday = c.checkedToday === true;
186
+ if (Number.isFinite(c.streakDays) && c.streakDays > 0) row.streakDays = c.streakDays;
187
+ }
188
+ } catch { /* 状态未知就不带 */ }
189
+ }
190
+ rows[rowKey(i)] = row;
128
191
  }
129
- const payload = { provider: 'minimax-code', label: t('MiniMax Code 免费额度') };
192
+ // label 是数据标识不是 UI 文案——不随 CD 面板语言翻译(面板设英文时曾把
193
+ // label 推成英文,10R 卡面副标题跟着变英文)。显示侧翻译由 10R 负责。
194
+ const payload = { provider: 'minimax-free', label: 'MiniMax Code 免费额度' };
130
195
  if (Object.keys(rows).length) {
131
196
  payload.quotas = rows;
132
197
  payload.plan = 'Free';
133
- if (failed) payload.label = t('MiniMax Code 免费额度({n} 个账号查询失败)', { n: failed }).slice(0, 100);
198
+ if (failed) payload.label = `MiniMax Code 免费额度(${failed} 个账号查询失败)`.slice(0, 100);
134
199
  } else {
135
200
  payload.message = t('MiniMax 账号额度暂不可查(凭据失效需在 CreditDaddy 重新登录,或官方接口异常稍后自动重试)');
136
201
  }
137
- assertScalarOnly(payload, 'minimax-code');
202
+ assertScalarOnly(payload, 'minimax-free');
138
203
  return payload;
139
204
  }
140
205
 
@@ -215,6 +280,13 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
215
280
  if (hit) hit.meta = { ...(hit.meta || {}), lastProbe: { ok: Boolean(ok), status, error: ok ? null : String(error || '').slice(0, 200), at: new Date().toISOString() } };
216
281
  })).catch(() => {});
217
282
  };
283
+ const markRateLimited = (a, error) => {
284
+ if (noProbeMark || !a?.id) return;
285
+ import('./store.js').then(({ withAccounts }) => withAccounts((list) => {
286
+ const hit = list.find((x) => x.id === a.id);
287
+ if (hit) hit.meta = { ...(hit.meta || {}), lastProbe: { ok: false, status: 'dead', error: String(error || '').slice(0, 200), at: new Date().toISOString() } };
288
+ })).catch(() => {});
289
+ };
218
290
  // live 余额落库(面板 /quota 缓存路径的数据源):推送循环每轮 live 拉到的数字顺手写回
219
291
  // meta.quotaSnapshot + 各家明细字段,缓存随推送节奏(默认 60 分钟)保鲜。无条件落盘——
220
292
  // 回调路径(quota-refresh → onlySource)拿的也是真数据,没理由只落 lastProbe 不落数字。
@@ -232,8 +304,39 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
232
304
  return Number.isFinite(at) && (Date.now() - at) < DEAD_RETRY_MS;
233
305
  };
234
306
 
235
- // 清理历史遗留的 minimax-code 卡片(MiniMax 账号会话在 CD 自持,避免生成 0 行报错卡)
236
- if (!only) deleteExternalQuota({ endpoint, key, source: 'minimax-code', fetchImpl }).catch(() => {});
307
+ // 0. minimax-free(单账号单卡,minimax-code 同款单值卡:带签到徽章)
308
+ const mmAccounts = (accounts || []).filter((a) => a.provider === 'minimax' && (a.token || a.refreshToken));
309
+ if (mmAccounts.length) {
310
+ // 一次性清理:按账号单卡(6ebcc52)之前的版本把全部账号聚在 source「minimax-free」
311
+ // 一张多行卡上;升级用户的那张旧卡没人再推(也不该再推),不删会挂到 7 天 TTL 自然过期,
312
+ // 期间与新的 per-account 卡并存(主/备 + 老聚合卡三张,2026-10-21 实测)。有账号可推时
313
+ // 删一次旧 source,失败不阻断本轮推送(下轮还会再试)。
314
+ if (!only) {
315
+ const deleted = await deleteExternalQuota({ endpoint, key, source: 'minimax-free', fetchImpl });
316
+ if (deleted) logger.info('10R', '旧版聚合卡 minimax-free 已清理(推送改为按账号单卡)');
317
+ }
318
+ const { fetchMiniMaxQuota, fetchMiniMaxCheckinStatus } = await import('./minimaxClient.js');
319
+ for (const a of mmAccounts) {
320
+ const { source } = buildMiniMaxAccountPayload(a, null);
321
+ if (only && !only.includes(source)) continue;
322
+ const skipDead = !only && deadSkip(a);
323
+ let q = null;
324
+ let c = null;
325
+ if (!skipDead) {
326
+ try { q = await fetchMiniMaxQuota(a); } catch (e) { q = { error: e.message }; }
327
+ try { c = await fetchMiniMaxCheckinStatus(a); } catch {}
328
+ }
329
+ const { payload } = buildMiniMaxAccountPayload(a, q, c);
330
+ try {
331
+ const r = await pushOne({ endpoint, key, source, payload, fetchImpl, refreshUrl: await rurl(source) });
332
+ results.push({ source, status: 'ok', entries: r.entries });
333
+ logger.info('10R', '额度卡已推送:{source}({n} 行)', { source, n: r.entries ?? (Object.keys(payload.quotas || {}).length || 0) });
334
+ } catch (e) {
335
+ results.push({ source, status: 'failed', error: e.message });
336
+ logger.warn('10R', '额度卡推送失败 {source}:{err}', { source, err: e.message });
337
+ }
338
+ }
339
+ }
237
340
 
238
341
  // 1. tokenharbor(issue #24:TH 免费额度网页读数)
239
342
  const thAccounts = (accounts || []).filter((a) => a.provider === 'tokenharbor' && a.meta?.cookieSiteId);
@@ -249,6 +352,7 @@ export async function runQuotaPush({ endpoint, key, accounts, fetchImpl, onlySou
249
352
  }
250
353
  }
251
354
 
355
+
252
356
  // 2. xiaomi-mimo(issue #26:小米 MiMo 控制台充值余额推送)
253
357
  const mimoAccounts = (accounts || []).filter((a) => (a.provider === 'xiaomi-mimo' || a.meta?.cookieSiteId === 'xiaomi-mimo') && a.token);
254
358
  if (mimoAccounts.length) {