dsh-codearts 0.5.2 → 1.0.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/lib/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { C as isBenefitModel, D as codeartsAuthPath, E as codeartsAccountsPath, O as startLogin, S as filterEnabledModels, T as CodeArtsCredentialStore, _ as pickDailyWelfareCampaign, a as SNAP_HOST, b as CodeArtsCatalog, c as claimDailyWelfare, d as fetchModels, f as fetchWelfareCampaigns, g as maxTokensCeilingFor, h as isWelfareClaimedStatus, i as refreshCatalog, l as degradeTokenCeiling, m as isParamInvalidFrame, n as codeartsUsagePath, o as canonicalModel, p as isConcurrencyCap, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as fetchCallerIdentity, v as prepareChatBody, w as CodeArtsAccountStore, x as FALLBACK_CODEARTS_MODELS, y as signedGet } from "./usage-B4vQb1sP.mjs";
1
+ import { A as isBenefitModel, C as upstreamErrorMessage, D as creditRatioLabel, E as clearBenefitBlocked, F as startLogin, M as CodeArtsCredentialStore, N as codeartsAccountsPath, O as filterEnabledModels, P as codeartsAuthPath, S as signedGet, T as FALLBACK_CODEARTS_MODELS, _ as isWelfareClaimedStatus, a as SNAP_HOST, b as prepareChatBody, c as claimDailyWelfare, d as fetchModels, f as fetchWelfareCampaigns, g as isTpmLimit, h as isQueueableRate, i as refreshCatalog, j as CodeArtsAccountStore, k as isBenefitBlocked, l as degradeTokenCeiling, m as isParamInvalidFrame, n as codeartsUsagePath, o as canonicalModel, p as isConcurrencyCap, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as fetchCallerIdentity, v as maxTokensCeilingFor, w as CodeArtsCatalog, x as rateLimitFrame, y as pickDailyWelfareCampaign } from "./usage-BEoMNRwo.mjs";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { createServer } from "node:http";
@@ -314,8 +314,9 @@ function safeJson(text) {
314
314
  * 而上游对同一账号的并发是有成本/风险的。
315
315
  */
316
316
  var CodeArtsCreditCache = class {
317
- state;
318
- inflight;
317
+ /** 按账号分槽:多账号下每个号有自己的余额,不能共用一个快照。 */
318
+ states = /* @__PURE__ */ new Map();
319
+ inflights = /* @__PURE__ */ new Map();
319
320
  ttlMs;
320
321
  failureTtlMs;
321
322
  load;
@@ -326,37 +327,46 @@ var CodeArtsCreditCache = class {
326
327
  this.load = options.load ?? fetchCredits;
327
328
  this.now = options.now ?? (() => Date.now());
328
329
  }
329
- /** 当前缓存内容(不触发请求),供只想读快照的场景使用。 */
330
- peek() {
331
- return this.state;
330
+ /** 某账号的缓存内容(不触发请求),供只想读快照的场景使用。 */
331
+ peek(accountId) {
332
+ return this.states.get(accountId);
332
333
  }
333
- /** 丢掉缓存,下次 get 会重新拉(重新登录/添加账号后调用)。 */
334
- invalidate() {
335
- this.state = void 0;
336
- this.inflight = void 0;
334
+ /** 丢掉缓存,下次 get 会重新拉(重新登录/添加账号后调用)。省略账号则清空全部。 */
335
+ invalidate(accountId) {
336
+ if (accountId === void 0) {
337
+ this.states.clear();
338
+ this.inflights.clear();
339
+ return;
340
+ }
341
+ this.states.delete(accountId);
342
+ this.inflights.delete(accountId);
337
343
  }
338
344
  /**
339
- * 取一次积分。命中缓存直接返回;否则合并成一次上游请求。
345
+ * 取一次某账号的积分。命中缓存直接返回;否则合并成一次上游请求。
340
346
  * **不抛异常**:失败以 `phase: 'error'` 返回,调用方(状态路由)照样能渲染。
341
347
  */
342
- async get(credential, signal) {
343
- const cached = this.state;
348
+ async get(accountId, credential, signal) {
349
+ const cached = this.states.get(accountId);
344
350
  if (cached !== void 0) {
345
351
  const ttl = cached.phase === "ok" ? this.ttlMs : this.failureTtlMs;
346
352
  if (this.now() - cached.at < ttl) return cached;
347
353
  }
348
- return this.inflight ??= this.run(credential, signal).finally(() => {
349
- this.inflight = void 0;
354
+ const existing = this.inflights.get(accountId);
355
+ if (existing !== void 0) return existing;
356
+ const inflight = this.run(accountId, credential, signal).finally(() => {
357
+ this.inflights.delete(accountId);
350
358
  });
359
+ this.inflights.set(accountId, inflight);
360
+ return inflight;
351
361
  }
352
- async run(credential, signal) {
362
+ async run(accountId, credential, signal) {
353
363
  try {
354
364
  const state = {
355
365
  phase: "ok",
356
366
  value: await this.load(credential, signal),
357
367
  at: this.now()
358
368
  };
359
- this.state = state;
369
+ this.states.set(accountId, state);
360
370
  return state;
361
371
  } catch (error) {
362
372
  const state = {
@@ -364,7 +374,7 @@ var CodeArtsCreditCache = class {
364
374
  message: error instanceof Error ? error.message : String(error),
365
375
  at: this.now()
366
376
  };
367
- this.state = state;
377
+ this.states.set(accountId, state);
368
378
  return state;
369
379
  }
370
380
  }
@@ -380,7 +390,8 @@ var CodeArtsCreditCache = class {
380
390
  * models and points pi-ai at the shim's OpenAI-compatible endpoint.
381
391
  *
382
392
  * MVP simplifications vs dsh-codebuddy-cli:
383
- * - no billing rate in the model name (per-request cost not exposed by the API)
393
+ * - only the default-slot credit multiplier goes into the model name (`(x0.18)`);
394
+ * per-request cost is not modelled (subscription quota, cost reported as zero)
384
395
  * - no per-model reasoning ladder (models are surfaced as plain chat models)
385
396
  *
386
397
  * @module dsh-codearts/adapter
@@ -655,21 +666,6 @@ const SERVER_COOLDOWN_MS = 3e4;
655
666
  /** 零散错误累计到阈值后冷却该账号。 */
656
667
  const ERROR_COOLDOWN_MS = 6e4;
657
668
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
658
- /**
659
- * 在**同一个账号**上重发,直到不再撞上限或预算耗尽。
660
- *
661
- * 只在没有别的账号可选时使用 —— 等 52s 是最后手段,能换号就换号。
662
- * 命中上限不甩给 pi-ai:429 会被重试 5 次,每次再去要一个会话,越试越满。
663
- */
664
- async function openStream(credential, prepared, signal) {
665
- const until = Date.now() + CAP_WAIT_LIMIT;
666
- for (;;) {
667
- const result = await chatStream(credential, prepared, signal);
668
- if (result.ok) return result;
669
- if (!(isConcurrencyCap(result) && !signal.aborted && Date.now() + CAP_RETRY_GAP <= until)) return result;
670
- await sleep(CAP_RETRY_GAP);
671
- }
672
- }
673
669
  function createCodeArtsShim(options) {
674
670
  const { store, catalog } = options;
675
671
  const logger = options.logger;
@@ -775,6 +771,21 @@ function createCodeArtsShim(options) {
775
771
  const reader = result.stream.getReader();
776
772
  const first = await reader.read();
777
773
  const head = first.done ? void 0 : first.value;
774
+ const rate = head === void 0 ? void 0 : rateLimitFrame(new TextDecoder().decode(head));
775
+ if (rate !== void 0) {
776
+ reader.releaseLock();
777
+ await result.stream.cancel().catch(() => {});
778
+ logger?.warn(`dsh-codearts: ${prepared.model} 首帧被判限流(${rate.code}),进入本地排队`);
779
+ return {
780
+ prepared,
781
+ result: {
782
+ ok: false,
783
+ kind: "soft_rate",
784
+ status: 429,
785
+ message: `${rate.code}: ${rate.message}`
786
+ }
787
+ };
788
+ }
778
789
  const shouldHeal = heal < MAX_CEILING_HEALS && head !== void 0 && isParamInvalidFrame(new TextDecoder().decode(head));
779
790
  reader.releaseLock();
780
791
  if (!shouldHeal) return {
@@ -795,6 +806,39 @@ function createCodeArtsShim(options) {
795
806
  logger?.warn(`dsh-codearts: ${prepared.model} 被判参数非法,max_tokens 降档到 ${maxTokensCeilingFor(prepared.model)} 后重发`);
796
807
  }
797
808
  }
809
+ /**
810
+ * 在**同一个账号**上重发,直到不再被限流或预算耗尽。
811
+ *
812
+ * 并发槽位是账号级的:有别的号就该换号,等 52s 是最后手段。
813
+ * TPM 是模型级配额:换号只是换个账号撞同一堵墙,只能原地等窗口滑过去 ——
814
+ * 间隔逐次翻倍(3s → 6s → 12s → 24s,封顶 30s),避免在配额已经满的时候
815
+ * 把重试本身也打成一波流量。
816
+ *
817
+ * 命中限流不甩给 pi-ai:429 会被重试 5 次,每次再撞一次限流,越试越满。
818
+ */
819
+ async function openStream(credential, raw, contextWindow, signal) {
820
+ const started = Date.now();
821
+ const until = started + CAP_WAIT_LIMIT;
822
+ let gap = CAP_RETRY_GAP;
823
+ for (;;) {
824
+ const opened = await openWithCeilingHeal(credential, raw, contextWindow, signal);
825
+ const waitedMs = Date.now() - started;
826
+ if (opened.result.ok || signal.aborted) return {
827
+ ...opened,
828
+ waitedMs
829
+ };
830
+ if (!isQueueableRate(opened.result)) return {
831
+ ...opened,
832
+ waitedMs
833
+ };
834
+ if (isTpmLimit(opened.result)) gap = Math.min(gap * 2, 3e4);
835
+ if (Date.now() + gap > until) return {
836
+ ...opened,
837
+ waitedMs
838
+ };
839
+ await sleep(gap);
840
+ }
841
+ }
798
842
  /** 先交回降档判定读过的那一帧(它是流的开头,丢了就少一段),再接着读剩下的。 */
799
843
  async function* drainFrames(reader, head) {
800
844
  if (head !== void 0) yield head;
@@ -838,6 +882,8 @@ function createCodeArtsShim(options) {
838
882
  pool.sync(ids);
839
883
  const tried = /* @__PURE__ */ new Set();
840
884
  let cappedAccount;
885
+ let tpmAccount;
886
+ let queueWaitedMs = 0;
841
887
  let lastKind;
842
888
  let lastStatus = 0;
843
889
  let lastMessage = "";
@@ -874,6 +920,10 @@ function createCodeArtsShim(options) {
874
920
  cappedAccount = id;
875
921
  continue;
876
922
  }
923
+ if (isTpmLimit(result)) {
924
+ tpmAccount = id;
925
+ break;
926
+ }
877
927
  if (result.kind === "session_dead") {
878
928
  pool.disable(id, result.message);
879
929
  continue;
@@ -889,39 +939,41 @@ function createCodeArtsShim(options) {
889
939
  }
890
940
  }
891
941
  if (controller.signal.aborted) return;
892
- if (cappedAccount !== void 0) {
893
- if (pool.acquire(cappedAccount)) {
894
- const prepared = prepareChatBody(raw, contextWindow);
895
- try {
896
- const credential = await store.resolve(cappedAccount);
897
- if (credential !== void 0) {
898
- const result = await openStream(credential, prepared, controller.signal);
899
- if (controller.signal.aborted) {
900
- if (result.ok) await result.stream.cancel().catch(() => {});
901
- return;
902
- }
903
- if (result.ok) {
904
- pool.noteSuccess(cappedAccount);
905
- await pipeToResponse(res, result.stream, controller.signal, {
906
- accountId: cappedAccount,
907
- model: prepared.model
908
- });
909
- return;
910
- }
911
- lastKind = result.kind;
912
- lastStatus = result.status;
913
- lastMessage = result.message;
942
+ const queueAccount = cappedAccount ?? tpmAccount;
943
+ if (queueAccount !== void 0) {
944
+ if (pool.acquire(queueAccount)) try {
945
+ const credential = await store.resolve(queueAccount);
946
+ if (credential !== void 0) {
947
+ const opened = await openStream(credential, raw, contextWindow, controller.signal);
948
+ const { result } = opened;
949
+ queueWaitedMs = opened.waitedMs;
950
+ if (controller.signal.aborted) {
951
+ if (result.ok) await result.stream.cancel().catch(() => {});
952
+ return;
914
953
  }
915
- } finally {
916
- pool.release(cappedAccount);
954
+ if (result.ok) {
955
+ pool.noteSuccess(queueAccount);
956
+ await pipeToResponse(res, result.stream, controller.signal, {
957
+ accountId: queueAccount,
958
+ model: opened.prepared.model
959
+ }, opened.head);
960
+ return;
961
+ }
962
+ lastKind = result.kind;
963
+ lastStatus = result.status;
964
+ lastMessage = result.message;
917
965
  }
966
+ } finally {
967
+ pool.release(queueAccount);
918
968
  }
919
969
  }
920
970
  if (lastKind === void 0) {
921
971
  writeOpenAIError(res, 429, "soft_rate", "codearts 当前没有可用账号(全部被禁用或冷却中),请稍后重试");
922
972
  return;
923
973
  }
924
- writeOpenAIError(res, KIND_STATUS[lastKind], lastKind, `codearts upstream ${lastKind} (http ${lastStatus}): ${lastMessage.slice(0, 400)}`);
974
+ const explanation = upstreamErrorMessage(lastMessage, lastStatus);
975
+ const waited = queueWaitedMs >= 1e3 ? `(本地已排队重试 ${Math.round(queueWaitedMs / 1e3)} 秒)` : "";
976
+ writeOpenAIError(res, KIND_STATUS[lastKind], lastKind, `${explanation}${waited}`);
925
977
  }
926
978
  /**
927
979
  * 记下一帧里的上游流内错误。判据宽松(帧文本含 "error" 才解析),解析不出来就忽略 ——
@@ -1014,11 +1066,13 @@ const CODEARTS_LOGIN_STATUS_PATH = "/api/codearts/login/status";
1014
1066
  const CODEARTS_ACCOUNTS_PATH = "/api/codearts/accounts";
1015
1067
  /** POST /api/codearts/claim — 领一次福利中心的每日签到积分(幂等由上游按天去重)。 */
1016
1068
  const CODEARTS_CLAIM_PATH = "/api/codearts/claim";
1069
+ /** POST /api/codearts/refresh-models — 立刻重拉一次模型目录,并清掉「福利今日不可用」标记。 */
1070
+ const CODEARTS_REFRESH_MODELS_PATH = "/api/codearts/refresh-models";
1017
1071
  /**
1018
1072
  * 由完整目录 + 已存 enabled 计算 selection。
1019
1073
  * - enabled 为空/未存 => 全部勾选(restricted=false)
1020
1074
  * - enabled 为非空列表 => 仅勾选其中的(restricted=true),缺的按未勾选
1021
- * 福利模型用 catalog 的 benefit 标记(这里靠 model.free 字段,由调用方注入)。
1075
+ * 福利模型用 catalog 的 benefit 标记(这里靠 model.benefit 字段,由调用方注入)。
1022
1076
  */
1023
1077
  function selectionOf(models, enabled, writable) {
1024
1078
  const allowed = new Set(enabled ?? []);
@@ -1027,13 +1081,46 @@ function selectionOf(models, enabled, writable) {
1027
1081
  choices: models.map((m) => ({
1028
1082
  id: m.id,
1029
1083
  name: m.name,
1030
- free: m.free,
1084
+ benefit: m.benefit,
1085
+ blocked: m.blocked,
1086
+ ratio: m.ratio,
1031
1087
  enabled: restricted ? allowed.has(m.id) : true
1032
1088
  })),
1033
1089
  restricted,
1034
1090
  writable
1035
1091
  };
1036
1092
  }
1093
+ /** 目录 -> 勾选 UI 的选项(福利标记、今日不可用标记与倍率都在这里算)。 */
1094
+ function choicesOf(deps) {
1095
+ return deps.models().map((m) => {
1096
+ const benefit = isBenefitModel(m.id);
1097
+ const ratio = creditRatioLabel(m.ratio, benefit);
1098
+ return {
1099
+ id: m.id,
1100
+ name: m.name,
1101
+ benefit,
1102
+ blocked: isBenefitBlocked(m.id),
1103
+ ...ratio === void 0 ? {} : { ratio }
1104
+ };
1105
+ });
1106
+ }
1107
+ /**
1108
+ * 把逐账号的领取结果汇总成一条:HTTP 层只看 ok,卡片看 results 逐号展示。
1109
+ * 有一个号失败就算整批 ok=false,但别的号领到的结果照样在 results 里。
1110
+ */
1111
+ function summarizeClaims(results) {
1112
+ const failed = results.find((result) => result.state === "failed");
1113
+ const claimed = results.filter((result) => result.claimed);
1114
+ const messages = [...new Set(results.map((result) => result.message).filter((message) => message !== ""))];
1115
+ return {
1116
+ ok: results.length > 0 && failed === void 0,
1117
+ claimed: claimed.length > 0,
1118
+ state: claimed.length > 0 ? "claimed" : failed?.state ?? results[0]?.state ?? "failed",
1119
+ amount: claimed.reduce((sum, result) => sum + result.amount, 0),
1120
+ message: results.length === 1 ? results[0].message : messages.join("; ").slice(0, 300),
1121
+ results
1122
+ };
1123
+ }
1037
1124
  function json(res, status, body) {
1038
1125
  const payload = JSON.stringify(body);
1039
1126
  res.writeHead(status, {
@@ -1125,7 +1212,9 @@ function usageOf(deps) {
1125
1212
  return {
1126
1213
  today: snapshot?.today ?? noTotals,
1127
1214
  allTime: snapshot?.allTime ?? noTotals,
1215
+ last30: snapshot?.last30 ?? noTotals,
1128
1216
  byModelToday: snapshot?.byModelToday ?? [],
1217
+ byAccountToday: snapshot?.byAccountToday ?? {},
1129
1218
  days: snapshot?.days ?? 0,
1130
1219
  ...snapshot?.lastAt === void 0 ? {} : { lastAt: snapshot.lastAt },
1131
1220
  slots: slotsOf(deps)
@@ -1140,8 +1229,13 @@ function usageOf(deps) {
1140
1229
  async function creditsOf(deps) {
1141
1230
  if (deps.credits === void 0) return {};
1142
1231
  try {
1143
- const state = await deps.credits();
1144
- return state.phase === "ok" ? { credits: state.value } : { creditsError: state.message };
1232
+ const entries = await deps.credits();
1233
+ const primary = entries.find((entry) => entry.credits !== void 0) ?? entries[0];
1234
+ return {
1235
+ ...primary?.credits === void 0 ? {} : { credits: primary.credits },
1236
+ ...primary?.credits === void 0 && primary?.creditsError !== void 0 ? { creditsError: primary.creditsError } : {},
1237
+ ...entries.length === 0 ? {} : { creditsByAccount: entries }
1238
+ };
1145
1239
  } catch (error) {
1146
1240
  return { creditsError: safeMessage(error) };
1147
1241
  }
@@ -1225,11 +1319,7 @@ function modelsSelectionHandler(deps) {
1225
1319
  return;
1226
1320
  }
1227
1321
  if (req.method === "GET") {
1228
- const all = deps.models().map((m) => ({
1229
- id: m.id,
1230
- name: m.name,
1231
- free: isBenefitModel(m.id)
1232
- }));
1322
+ const all = choicesOf(deps);
1233
1323
  const writable = deps.setEnabledModels !== void 0;
1234
1324
  json(res, 200, { selection: selectionOf(all, deps.enabledModels?.(), writable) });
1235
1325
  return;
@@ -1254,11 +1344,7 @@ function modelsSelectionHandler(deps) {
1254
1344
  json(res, 403, { error: "selection-not-writable" });
1255
1345
  return;
1256
1346
  }
1257
- json(res, 200, { selection: selectionOf(deps.models().map((m) => ({
1258
- id: m.id,
1259
- name: m.name,
1260
- free: isBenefitModel(m.id)
1261
- })), deps.enabledModels?.(), true) });
1347
+ json(res, 200, { selection: selectionOf(choicesOf(deps), deps.enabledModels?.(), true) });
1262
1348
  } catch (error) {
1263
1349
  json(res, 500, { error: safeMessage(error) });
1264
1350
  }
@@ -1361,8 +1447,17 @@ function claimHandler(deps) {
1361
1447
  json(res, 403, { error: "request-not-trusted" });
1362
1448
  return;
1363
1449
  }
1450
+ let accountId;
1451
+ const type = req.headers["content-type"];
1452
+ if (typeof type === "string" && type.trim().toLowerCase().startsWith("application/json")) try {
1453
+ const body = JSON.parse(await readBody(req));
1454
+ if (typeof body?.id === "string" && body.id.trim() !== "") accountId = body.id.trim();
1455
+ } catch {
1456
+ json(res, 400, { error: "expected {\"id\"?: string}" });
1457
+ return;
1458
+ }
1364
1459
  try {
1365
- const result = await deps.claim();
1460
+ const result = await deps.claim(accountId);
1366
1461
  json(res, result.ok ? 200 : 500, result);
1367
1462
  } catch (error) {
1368
1463
  json(res, 500, {
@@ -1375,6 +1470,43 @@ function claimHandler(deps) {
1375
1470
  }
1376
1471
  };
1377
1472
  }
1473
+ /**
1474
+ * POST /api/codearts/refresh-models — 立刻重拉目录,并清掉「福利今日不可用」标记。
1475
+ *
1476
+ * 清标记放在拉目录**之前**:福利回来时这是唯一的自救出口,不能让用户等到明天。
1477
+ * 代价是「今天确实领完了」的那种缺失也会被一并清掉,再点一次就重新记上。
1478
+ */
1479
+ function refreshModelsHandler(deps) {
1480
+ return async (req, res) => {
1481
+ if (req.method !== "POST") {
1482
+ json(res, 405, { error: "method not allowed" });
1483
+ return;
1484
+ }
1485
+ if (!loopbackRequest(req)) {
1486
+ json(res, 403, { error: "request-not-trusted" });
1487
+ return;
1488
+ }
1489
+ if (deps.refreshModels === void 0) {
1490
+ json(res, 404, { error: "refresh not available" });
1491
+ return;
1492
+ }
1493
+ try {
1494
+ clearBenefitBlocked();
1495
+ const counted = await deps.refreshModels();
1496
+ const selection = selectionOf(choicesOf(deps), deps.enabledModels?.(), deps.setEnabledModels !== void 0);
1497
+ json(res, 200, {
1498
+ ok: true,
1499
+ ...counted,
1500
+ selection
1501
+ });
1502
+ } catch (error) {
1503
+ json(res, 500, {
1504
+ ok: false,
1505
+ message: safeMessage(error)
1506
+ });
1507
+ }
1508
+ };
1509
+ }
1378
1510
  /** Mount the status / models / token routes. */
1379
1511
  function registerCodeArtsStatusRoute(ctx, deps) {
1380
1512
  ctx.effect(() => {
@@ -1421,6 +1553,11 @@ function registerCodeArtsStatusRoute(ctx, deps) {
1421
1553
  path: CODEARTS_CLAIM_PATH,
1422
1554
  handler: claimHandler(deps)
1423
1555
  });
1556
+ const disposeRefresh = ctx.webServer.register({
1557
+ kind: "exact",
1558
+ path: CODEARTS_REFRESH_MODELS_PATH,
1559
+ handler: refreshModelsHandler(deps)
1560
+ });
1424
1561
  return () => {
1425
1562
  disposeStatus();
1426
1563
  disposeModels();
@@ -1430,6 +1567,7 @@ function registerCodeArtsStatusRoute(ctx, deps) {
1430
1567
  disposeLoginStatus();
1431
1568
  disposeAccounts();
1432
1569
  disposeClaim();
1570
+ disposeRefresh();
1433
1571
  for (const s of activeLogins.values()) s.close();
1434
1572
  activeLogins.clear();
1435
1573
  };
@@ -1606,38 +1744,50 @@ function apply(ctx, rawConfig) {
1606
1744
  /** /status 是卡片与对话底部都会轮询的接口,积分慢请求不能把它拖成转圈。 */
1607
1745
  const CREDIT_TIMEOUT_MS = 8e3;
1608
1746
  /**
1609
- * 拉一次积分。
1747
+ * 拉一次积分(每个账号各自一份)。
1610
1748
  *
1611
- * 积分是**账号级**属性,不随请求轮转:这里挑第一个没用坏的账号,够覆盖正常用法
1612
- * (多账号下的积分差异不在本插件关心范围内)。
1749
+ * 积分是**账号级**属性:多账号下每个号的余额、到期、签到状态都不同,所以逐号
1750
+ * 并发拉、逐号缓存(缓存键就是账号 id)。一个号失败只影响它自己那一份。
1613
1751
  */
1614
1752
  const loadCredits = async (signal) => {
1615
1753
  const ids = await store.ids();
1616
- if (ids.length === 0) return {
1617
- phase: "error",
1618
- message: "not signed in",
1619
- at: Date.now()
1620
- };
1754
+ if (ids.length === 0) return [];
1621
1755
  const controller = new AbortController();
1622
1756
  const timer = setTimeout(() => controller.abort(), CREDIT_TIMEOUT_MS);
1623
1757
  timer.unref?.();
1624
1758
  signal?.addEventListener("abort", () => controller.abort(), { once: true });
1625
1759
  try {
1626
- let last;
1627
- for (const id of ids) {
1760
+ return await Promise.all(ids.map(async (id) => {
1628
1761
  const stored = await store.credential(id);
1629
- if (stored === void 0 || stored.stale === true) continue;
1630
- const resolved = await store.resolve(id);
1631
- if (resolved === void 0) continue;
1632
- const state = await credits.get(resolved, controller.signal);
1633
- if (state.phase === "ok") return state;
1634
- last = state;
1635
- }
1636
- return last ?? {
1637
- phase: "error",
1638
- message: "no usable credential",
1639
- at: Date.now()
1640
- };
1762
+ if (stored === void 0) return {
1763
+ id,
1764
+ creditsError: "account no longer stored"
1765
+ };
1766
+ if (stored.stale === true) return {
1767
+ id,
1768
+ creditsError: "account needs sign-in"
1769
+ };
1770
+ try {
1771
+ const resolved = await store.resolve(id);
1772
+ if (resolved === void 0) return {
1773
+ id,
1774
+ creditsError: "no usable credential"
1775
+ };
1776
+ const state = await credits.get(id, resolved, controller.signal);
1777
+ return state.phase === "ok" ? {
1778
+ id,
1779
+ credits: state.value
1780
+ } : {
1781
+ id,
1782
+ creditsError: state.message
1783
+ };
1784
+ } catch (error) {
1785
+ return {
1786
+ id,
1787
+ creditsError: error instanceof Error ? error.message : String(error)
1788
+ };
1789
+ }
1790
+ }));
1641
1791
  } finally {
1642
1792
  clearTimeout(timer);
1643
1793
  }
@@ -1650,6 +1800,8 @@ function apply(ctx, rawConfig) {
1650
1800
  await settings.update(settingsCompatNamespace(ctx), { enabledModels: [...ids] });
1651
1801
  return true;
1652
1802
  };
1803
+ /** 手动「刷新模型列表」的预算:三路来源各打一次,比每小时的后台刷新更该给足时间。 */
1804
+ const MANUAL_REFRESH_MS = 6e4;
1653
1805
  ctx.inject(["webServer"], (webCtx) => registerCodeArtsStatusRoute(webCtx, {
1654
1806
  store,
1655
1807
  models: () => catalog.current(),
@@ -1658,40 +1810,71 @@ function apply(ctx, rawConfig) {
1658
1810
  pool: () => pool.list(),
1659
1811
  usage: () => usage.snapshot(),
1660
1812
  credits: (signal) => loadCredits(signal),
1661
- claim: async () => {
1662
- try {
1663
- const ids = await store.ids();
1664
- for (const id of ids) {
1665
- const stored = await store.credential(id);
1666
- if (stored === void 0 || stored.stale === true) continue;
1667
- const resolved = await store.resolve(id);
1668
- if (resolved === void 0) continue;
1669
- const outcome = await claimDailyWelfare(resolved, AbortSignal.timeout(2e4));
1670
- if (outcome.state === "claimed") credits.invalidate();
1671
- return {
1672
- ok: outcome.state !== "failed",
1673
- claimed: outcome.state === "claimed",
1674
- state: outcome.state,
1675
- amount: outcome.amount,
1676
- message: outcome.message
1677
- };
1813
+ claim: async (accountId) => {
1814
+ const ids = accountId === void 0 ? await store.ids() : [accountId];
1815
+ if (ids.length === 0) return {
1816
+ ok: false,
1817
+ claimed: false,
1818
+ state: "failed",
1819
+ amount: 0,
1820
+ message: "no usable credential"
1821
+ };
1822
+ const results = [];
1823
+ for (const id of ids) try {
1824
+ const stored = await store.credential(id);
1825
+ if (stored === void 0 || stored.stale === true) {
1826
+ results.push({
1827
+ id,
1828
+ ok: false,
1829
+ claimed: false,
1830
+ state: "failed",
1831
+ amount: 0,
1832
+ message: stored === void 0 ? "account not found" : "account needs sign-in"
1833
+ });
1834
+ continue;
1678
1835
  }
1679
- return {
1680
- ok: false,
1681
- claimed: false,
1682
- state: "failed",
1683
- amount: 0,
1684
- message: "no usable credential"
1685
- };
1836
+ const resolved = await store.resolve(id);
1837
+ if (resolved === void 0) {
1838
+ results.push({
1839
+ id,
1840
+ ok: false,
1841
+ claimed: false,
1842
+ state: "failed",
1843
+ amount: 0,
1844
+ message: "no usable credential"
1845
+ });
1846
+ continue;
1847
+ }
1848
+ const outcome = await claimDailyWelfare(resolved, AbortSignal.timeout(2e4));
1849
+ if (outcome.state === "claimed") credits.invalidate(id);
1850
+ results.push({
1851
+ id,
1852
+ ok: outcome.state !== "failed",
1853
+ claimed: outcome.state === "claimed",
1854
+ state: outcome.state,
1855
+ amount: outcome.amount,
1856
+ message: outcome.message
1857
+ });
1686
1858
  } catch (error) {
1687
- return {
1859
+ results.push({
1860
+ id,
1688
1861
  ok: false,
1689
1862
  claimed: false,
1690
1863
  state: "failed",
1691
1864
  amount: 0,
1692
1865
  message: error instanceof Error ? error.message : String(error)
1693
- };
1866
+ });
1694
1867
  }
1868
+ return summarizeClaims(results);
1869
+ },
1870
+ refreshModels: async () => {
1871
+ const outcome = await refreshCatalog(store, catalog, AbortSignal.timeout(MANUAL_REFRESH_MS));
1872
+ const models = catalog.current();
1873
+ return {
1874
+ models: models.length,
1875
+ benefit: models.filter((model) => isBenefitModel(model.id)).length,
1876
+ refreshed: outcome?.anySucceeded === true
1877
+ };
1695
1878
  }
1696
1879
  }));
1697
1880
  installSettingsSectionCompat(ctx, CODEARTS_SETTINGS_NS, Config, config, {