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/bin.mjs +9 -21
- package/lib/client/index.d.cts +25 -6
- package/lib/client/index.js +817 -387
- package/lib/index.d.mts +32 -23
- package/lib/index.mjs +305 -122
- package/lib/{usage-B4vQb1sP.mjs → usage-BEoMNRwo.mjs} +227 -45
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
318
|
-
|
|
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.
|
|
330
|
+
/** 某账号的缓存内容(不触发请求),供只想读快照的场景使用。 */
|
|
331
|
+
peek(accountId) {
|
|
332
|
+
return this.states.get(accountId);
|
|
332
333
|
}
|
|
333
|
-
/** 丢掉缓存,下次 get
|
|
334
|
-
invalidate() {
|
|
335
|
-
|
|
336
|
-
|
|
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.
|
|
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
|
-
|
|
349
|
-
|
|
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.
|
|
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.
|
|
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
|
-
* -
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
916
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
1144
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
if (
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
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
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
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, {
|