mslxdff 0.1.140 → 0.1.142
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/package.json
CHANGED
|
@@ -119,7 +119,37 @@ export async function handleClineLogin(id, sub) {
|
|
|
119
119
|
console.log(`✅ 登录成功! 账号: ${email}`);
|
|
120
120
|
console.log(`🔑 refreshToken: ${rt.slice(0, 8)}…${rt.slice(-8)} (${rt.length} 字符)`);
|
|
121
121
|
// 落盘到 state(供应商 id 恒为 cline;写盘前先跑一次性迁移,保证老用户不会再写出 clinebot)
|
|
122
|
-
|
|
122
|
+
// 同邮箱去重:refreshToken 每次 login 都换新串,裸串比对拦不住重复账号,必须按 email 归一。
|
|
123
|
+
// auths 存 uid=email 索引行(domain=cline.bot),keys 与 auths 双写;老 keys 无映射时用 refresh 反查兜底。
|
|
124
|
+
const { defaultStateFile, loadProviderKeys, saveProviderConfig, loadProviderConfig, loadProviderAuths } = await import("../../../state.js");
|
|
125
|
+
function normEmail(v) { return String(v || "").trim().toLowerCase(); }
|
|
126
|
+
function emailFromJson(j) {
|
|
127
|
+
try {
|
|
128
|
+
const d = j?.data && typeof j.data === "object" ? j.data : j;
|
|
129
|
+
const u = d?.userInfo && typeof d.userInfo === "object" ? d.userInfo : null;
|
|
130
|
+
const cand = u?.email || d?.email || d?.user?.email || u?.mail || d?.mail || "";
|
|
131
|
+
if (cand && String(cand).includes("@")) return normEmail(cand);
|
|
132
|
+
const m = String(JSON.stringify(j || {}).slice(0, 4000)).match(/[\w.+-]+@[\w-]+\.[\w.]+/);
|
|
133
|
+
return m ? normEmail(m[0]) : "";
|
|
134
|
+
} catch { return ""; }
|
|
135
|
+
}
|
|
136
|
+
async function resolveClineEmail(refreshToken) {
|
|
137
|
+
try {
|
|
138
|
+
const baseNoV1 = "https://api.cline.bot";
|
|
139
|
+
const res = await compatFetch(`${baseNoV1}/api/v1/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken, grantType: "refresh_token" }), ...extraOpts, signal: timeoutSignal(15000) });
|
|
140
|
+
if (!res.ok) return "";
|
|
141
|
+
return emailFromJson(await res.json().catch(() => null));
|
|
142
|
+
} catch { return ""; }
|
|
143
|
+
}
|
|
144
|
+
function upsertAuths(list, email, rt) {
|
|
145
|
+
const norm = normEmail(email);
|
|
146
|
+
if (!norm || norm === "unknown") return [...(list || [])];
|
|
147
|
+
const next = [...(list || [])];
|
|
148
|
+
const i = next.findIndex((a) => normEmail(a?.uid) === norm);
|
|
149
|
+
if (i >= 0) next[i] = { uid: next[i].uid, domain: "cline.bot", enterpriseId: "", refreshToken: rt };
|
|
150
|
+
else next.push({ uid: String(email).trim(), domain: "cline.bot", enterpriseId: "", refreshToken: rt });
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
123
153
|
try {
|
|
124
154
|
const { runStateMigrations } = await import("../../../state/migrations.js");
|
|
125
155
|
const mig = await runStateMigrations({ file: defaultStateFile() });
|
|
@@ -128,12 +158,51 @@ export async function handleClineLogin(id, sub) {
|
|
|
128
158
|
try {
|
|
129
159
|
const pid = "cline";
|
|
130
160
|
const cur = loadProviderKeys(pid);
|
|
161
|
+
const cfg = loadProviderConfig(pid) || { baseUrl: "", keys: [] };
|
|
162
|
+
const baseUrl = cfg.baseUrl || "https://api.cline.bot";
|
|
163
|
+
let auths = loadProviderAuths(pid) || [];
|
|
164
|
+
const norm = normEmail(email);
|
|
131
165
|
if (cur.includes(rt)) {
|
|
132
|
-
|
|
166
|
+
auths = upsertAuths(auths, email, rt);
|
|
167
|
+
saveProviderConfig(pid, { baseUrl, keys: cur, auths });
|
|
168
|
+
console.log(` ℹ️ ${pid} 已存在相同 token,已补邮箱映射(现 ${cur.length} 个账号)`);
|
|
169
|
+
} else if (norm && norm !== "unknown") {
|
|
170
|
+
const ai = auths.findIndex((a) => normEmail(a?.uid) === norm);
|
|
171
|
+
if (ai >= 0) {
|
|
172
|
+
const oldRt = String(auths[ai]?.refreshToken || "");
|
|
173
|
+
const nextKeys = [...cur];
|
|
174
|
+
const at = oldRt ? nextKeys.indexOf(oldRt) : -1;
|
|
175
|
+
if (at >= 0) nextKeys[at] = rt;
|
|
176
|
+
else { const dup = nextKeys.indexOf(rt); if (dup < 0) nextKeys.push(rt); }
|
|
177
|
+
auths = upsertAuths(auths, email, rt);
|
|
178
|
+
saveProviderConfig(pid, { baseUrl, keys: [...new Set(nextKeys.filter(Boolean))], auths });
|
|
179
|
+
console.log(` ℹ️ ${email} 已存在,token 已替换(仍 ${nextKeys.length} 个账号,未追加)`);
|
|
180
|
+
} else {
|
|
181
|
+
let dupIdx = -1;
|
|
182
|
+
const resolved = [];
|
|
183
|
+
for (let i = 0; i < cur.length; i++) {
|
|
184
|
+
const em = await resolveClineEmail(cur[i]);
|
|
185
|
+
resolved[i] = em;
|
|
186
|
+
if (em && em === norm) { dupIdx = i; break; }
|
|
187
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
188
|
+
}
|
|
189
|
+
if (dupIdx >= 0) {
|
|
190
|
+
const nextKeys = [...cur]; nextKeys[dupIdx] = rt;
|
|
191
|
+
for (let i = 0; i < cur.length; i++) if (resolved[i]) auths = upsertAuths(auths, resolved[i], i === dupIdx ? rt : cur[i]);
|
|
192
|
+
auths = upsertAuths(auths, email, rt);
|
|
193
|
+
saveProviderConfig(pid, { baseUrl, keys: [...new Set(nextKeys.filter(Boolean))], auths });
|
|
194
|
+
console.log(` ℹ️ ${email} 已存在(位置 ${dupIdx + 1}),token 已替换(仍 ${nextKeys.length} 个账号,未追加)`);
|
|
195
|
+
} else {
|
|
196
|
+
const nextKeys = [...new Set([...cur, rt].filter(Boolean))];
|
|
197
|
+
for (let i = 0; i < cur.length; i++) if (resolved[i]) auths = upsertAuths(auths, resolved[i], cur[i]);
|
|
198
|
+
auths = upsertAuths(auths, email, rt);
|
|
199
|
+
saveProviderConfig(pid, { baseUrl, keys: nextKeys, auths });
|
|
200
|
+
console.log(` ✅ 已写入 ${pid}(现 ${nextKeys.length} 个账号)`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
133
203
|
} else {
|
|
134
|
-
const cfg = loadProviderConfig(pid) || { baseUrl: "", keys: [] };
|
|
135
204
|
const nextKeys = [...new Set([...(cfg.keys || cur), rt].filter(Boolean))];
|
|
136
|
-
saveProviderConfig(pid, { baseUrl
|
|
205
|
+
saveProviderConfig(pid, { baseUrl, keys: nextKeys, auths });
|
|
137
206
|
console.log(` ✅ 已写入 ${pid}(现 ${nextKeys.length} 个账号)`);
|
|
138
207
|
}
|
|
139
208
|
} catch (e) {
|
|
@@ -144,6 +213,6 @@ export async function handleClineLogin(id, sub) {
|
|
|
144
213
|
console.log(" mslxdff -restart 重启网关使新账号生效");
|
|
145
214
|
console.log(" mslxdff -provider cline bench --json 测速 deepseek 是否 200");
|
|
146
215
|
console.log(" mslxdff -chat 直接对话,模型选 deepseek/deepseek-v4-flash");
|
|
147
|
-
console.log("\n多账号:重复 `mslxdff -provider cline login`
|
|
216
|
+
console.log("\n多账号:重复 `mslxdff -provider cline login` 追加(同邮箱自动替换不追加),二号自动做后备");
|
|
148
217
|
process.exit(0);
|
|
149
218
|
}
|
|
@@ -17,6 +17,14 @@ export async function handleProviderKeys(id, sub, rest, args) {
|
|
|
17
17
|
if (a) console.log(` [${i + 1}] user=${a.userName || "(未识别)"} uid=${a.userId ? `${a.userId.slice(0, 8)}…` : "(空,请删了重登回填)"} domain=${a.domainId ? a.domainId.slice(0, 8) + "…" : "(空)"} (${k.length} chars)`);
|
|
18
18
|
else console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`);
|
|
19
19
|
}
|
|
20
|
+
} else if (id === "cline") {
|
|
21
|
+
const { loadProviderAuths } = await import("../../../state.js");
|
|
22
|
+
const auths = loadProviderAuths(id) || [];
|
|
23
|
+
const byRt = new Map(auths.map((a) => [String(a?.refreshToken || ""), String(a?.uid || "")]));
|
|
24
|
+
keys.forEach((k, i) => {
|
|
25
|
+
const em = byRt.get(String(k || "")) || "";
|
|
26
|
+
console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)${em ? ` ${em}` : " (邮箱未映射,重登一次回填)"}`);
|
|
27
|
+
});
|
|
20
28
|
} else keys.forEach((k, i) => console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`));
|
|
21
29
|
console.log(` remove by: mslxdff -provider ${id} remove <seq> [seq...] | <key-value>`);
|
|
22
30
|
} else {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createKeyRing } from "../keyring.js";
|
|
2
|
-
import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProviderChatPath, saveProviderConfig } from "../../state.js";
|
|
2
|
+
import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProviderChatPath, saveProviderConfig, loadProviderAuths } from "../../state.js";
|
|
3
3
|
import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createChatRunner } from "../base.js";
|
|
4
4
|
import { compatFetch } from "../../compat.js";
|
|
5
5
|
import { joinModelId } from "../model-id.js";
|
|
@@ -68,8 +68,14 @@ export function createClineProvider({
|
|
|
68
68
|
const idx = cur.indexOf(oldRefreshToken);
|
|
69
69
|
if (idx >= 0) {
|
|
70
70
|
const next = [...cur]; next[idx] = newRefreshToken;
|
|
71
|
-
// 优先写 providerConfigs,兼容旧路径由 saveProviderConfig
|
|
72
|
-
|
|
71
|
+
// 优先写 providerConfigs,兼容旧路径由 saveProviderConfig 处理;auths 邮箱映射同步新串防漂移
|
|
72
|
+
let auths;
|
|
73
|
+
try {
|
|
74
|
+
const list = loadProviderAuths(id, file ? { file } : {});
|
|
75
|
+
const ai = list.findIndex((a) => String(a?.refreshToken || "") === String(oldRefreshToken || ""));
|
|
76
|
+
if (ai >= 0) { auths = [...list]; auths[ai] = { ...auths[ai], refreshToken: newRefreshToken }; }
|
|
77
|
+
} catch {}
|
|
78
|
+
saveProviderConfig(id, { baseUrl: resolvedBase, keys: next, ...(auths ? { auths } : {}) }, file ? { file } : {});
|
|
73
79
|
}
|
|
74
80
|
// 只回写 cline 自己(旧 id 轮换回写已随 id 统一删除,见 .scratch/cline-unify/SPEC.md §4.1)
|
|
75
81
|
} catch {}
|
package/src/providers/generic.js
CHANGED
|
@@ -4,6 +4,9 @@ import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createC
|
|
|
4
4
|
import { compatFetch } from "../compat.js";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
import { genId, opencodeUa, digestIdTail } from "../opencode-identity.js";
|
|
7
|
+
import { isResponsesModel } from "../upstream-responses.js";
|
|
8
|
+
import { createResponsesChannel, resolveResponsesPath, envSlug } from "./responses-channel.js";
|
|
9
|
+
import { resolveEngineMode } from "../upstream-engine/mode.js";
|
|
7
10
|
|
|
8
11
|
const { UndiciFetch } = getUndici();
|
|
9
12
|
|
|
@@ -41,6 +44,7 @@ export function createGenericProvider({
|
|
|
41
44
|
apiKey,
|
|
42
45
|
modelsPath,
|
|
43
46
|
chatPath,
|
|
47
|
+
responsesPath,
|
|
44
48
|
connectTimeoutMs = Number(process.env.MSLXDFF_GENERIC_TIMEOUT_MS) || 30_000,
|
|
45
49
|
cooldownMs = envInt("MSLXDFF_GENERIC_COOLDOWN_MS", 30_000),
|
|
46
50
|
retry = {
|
|
@@ -106,12 +110,29 @@ export function createGenericProvider({
|
|
|
106
110
|
});
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
// responses 类模型(muse-spark* 等)只在 /responses 挂载 → 单独通道,出参恒 chat 形状。
|
|
114
|
+
const responses = createResponsesChannel({
|
|
115
|
+
id,
|
|
116
|
+
url: joinUrl(resolvedBase, resolveResponsesPath(id, responsesPath)),
|
|
117
|
+
connectTimeoutMs,
|
|
118
|
+
retry,
|
|
119
|
+
cooldownMs,
|
|
120
|
+
// 供应商级 <ID>_SDK 未设置即继承全局总闸(MSLXDFF_UPSTREAM_ENGINE)
|
|
121
|
+
sdkEnabled: resolveEngineMode(process.env, `MSLXDFF_${envSlug(id)}_SDK`) === "sdk",
|
|
122
|
+
buildHeaders,
|
|
123
|
+
fetchImpl,
|
|
124
|
+
dispatcher,
|
|
125
|
+
});
|
|
126
|
+
|
|
109
127
|
async function chat(body, opts) {
|
|
128
|
+
const sourceKey = `MSLXDFF_${envSlug(id)}_KEY`;
|
|
129
|
+
if (isResponsesModel(body?.model)) return responses.run(body, ring, sourceKey, opts);
|
|
110
130
|
const { runChat } = scopedRunner(ring, opts);
|
|
111
|
-
return runChat(body, ring,
|
|
131
|
+
return runChat(body, ring, sourceKey);
|
|
112
132
|
}
|
|
113
133
|
async function chatWithKeys(body, keys, opts) {
|
|
114
134
|
const tmp = createKeyRing(keys, { cooldownMs });
|
|
135
|
+
if (isResponsesModel(body?.model)) return responses.run(body, tmp, "shared provider keys", opts);
|
|
115
136
|
const { runChat } = scopedRunner(tmp, opts);
|
|
116
137
|
return runChat(body, tmp, "shared provider keys");
|
|
117
138
|
}
|
|
@@ -131,5 +152,5 @@ export function createGenericProvider({
|
|
|
131
152
|
if (agent && typeof agent.close === "function") { try { await agent.close(); } catch {} }
|
|
132
153
|
}
|
|
133
154
|
|
|
134
|
-
return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase };
|
|
155
|
+
return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase, responsesUrl: responses.url };
|
|
135
156
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// 通用(带 key)供应商的 responses 通道:muse-spark* 这类模型上游只挂 /responses,
|
|
2
|
+
// 打同 host 的 /chat/completions 会 503 "Endpoint is unavailable"(ocgo 实测 2026-09-22)。
|
|
3
|
+
// 判定单一来源 = isResponsesModel(models.dev 模型级 provider.npm + muse-spark 前缀兜底),
|
|
4
|
+
// 与 GET /v1/models 的 capabilities.upstreamApi 同源,新模型无需改码。
|
|
5
|
+
//
|
|
6
|
+
// 契约:run() 的出参恒为 **chat 形状**(SSE 或 JSON),调用方无需再判分支。
|
|
7
|
+
// · 流式优先 @ai-sdk/openai 的 responses 适配器(含加密思考往返,ADR-0017 复用);
|
|
8
|
+
// · SDK 不可用 / 非流式 → 原生 fetch + chatToResponsesBody 正转换 + 反向整形。
|
|
9
|
+
// key 轮换、退避重试、_t 计时与 createChatRunner(base.js)同语义,避免两条通道行为漂移。
|
|
10
|
+
import { chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "../upstream-responses.js";
|
|
11
|
+
import { attemptOnceResponsesSdk } from "../upstream-engine/sdk/responses.js";
|
|
12
|
+
import { ENGINE_MARKER } from "../upstream-engine/sdk/chat.js";
|
|
13
|
+
import { sleep } from "./base.js";
|
|
14
|
+
|
|
15
|
+
/** 供应商 id → env 片段(`my-api` → `MY_API`),与 providerKeyEnv 同规则 */
|
|
16
|
+
export function envSlug(id) {
|
|
17
|
+
return String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* responses 端点路径:显式入参 > env `MSLXDFF_<ID>_RESPONSES_PATH` > 缺省 `/responses`。
|
|
22
|
+
* 归一为以 `/` 开头(joinUrl 会再拼 baseUrl)。
|
|
23
|
+
*/
|
|
24
|
+
export function resolveResponsesPath(id, responsesPath) {
|
|
25
|
+
const raw = responsesPath || process.env[`MSLXDFF_${envSlug(id)}_RESPONSES_PATH`] || "";
|
|
26
|
+
const s = String(raw).trim();
|
|
27
|
+
if (!s) return "/responses";
|
|
28
|
+
return s.startsWith("/") ? s : `/${s}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 建 responses 通道。
|
|
33
|
+
* @param {object} o
|
|
34
|
+
* @param {string} o.id 供应商 id(用于报错文案与 providerName)
|
|
35
|
+
* @param {string} o.url 已拼好的 responses 绝对 URL
|
|
36
|
+
* @param {number} o.connectTimeoutMs 单次尝试超时
|
|
37
|
+
* @param {object} o.retry 重试表(与 createChatRunner 同形状:network/429/50x → {attempts, delayMs})
|
|
38
|
+
* @param {number} o.cooldownMs 全 key 冷却时的人话报错文案用
|
|
39
|
+
* @param {boolean} o.sdkEnabled 是否允许走 AI SDK responses 适配器(流式路径)
|
|
40
|
+
* @param {Function} o.buildHeaders (body, key, opts) → headers
|
|
41
|
+
* @param {Function} o.fetchImpl 注入的 fetch(连接池/测试接缝)
|
|
42
|
+
* @param {object} [o.dispatcher] undici Agent
|
|
43
|
+
*/
|
|
44
|
+
export function createResponsesChannel({
|
|
45
|
+
id,
|
|
46
|
+
url,
|
|
47
|
+
connectTimeoutMs = 30_000,
|
|
48
|
+
retry = {},
|
|
49
|
+
cooldownMs = 30_000,
|
|
50
|
+
sdkEnabled = true,
|
|
51
|
+
buildHeaders,
|
|
52
|
+
fetchImpl,
|
|
53
|
+
dispatcher = null,
|
|
54
|
+
} = {}) {
|
|
55
|
+
async function attemptOnce(body, key, opts) {
|
|
56
|
+
const headers = buildHeaders ? buildHeaders(body, key, opts) : {};
|
|
57
|
+
const wantsStream = body?.stream !== false;
|
|
58
|
+
if (sdkEnabled && wantsStream) {
|
|
59
|
+
try {
|
|
60
|
+
const r = await attemptOnceResponsesSdk({
|
|
61
|
+
url, body, headers, providerName: id, marker: ENGINE_MARKER, fetchImpl,
|
|
62
|
+
});
|
|
63
|
+
if (r) return r; // 适配器已产出 chat SSE
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (!e || (!e._sdkLoadFailed && !e._sdkUnsupported)) throw e;
|
|
66
|
+
// SDK 装载失败 / URL 异形 → 落原生兜底(保底不变差)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
|
|
71
|
+
try {
|
|
72
|
+
const fetchOpts = { method: "POST", headers, body: JSON.stringify(chatToResponsesBody(body)), signal: controller.signal };
|
|
73
|
+
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
|
74
|
+
const res = await fetchImpl(url, fetchOpts);
|
|
75
|
+
if (!res.ok) return res;
|
|
76
|
+
if (wantsStream) return reshapeResponsesSse(res, body?.model);
|
|
77
|
+
const json = await res.json().catch(() => null);
|
|
78
|
+
return json ? toChatResponse(res, json) : res;
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** key 轮换 + 退避重试 + _t 计时;语义对齐 base.js 的 createChatRunner.runChat */
|
|
85
|
+
async function run(body, activeRing, sourceKey, opts) {
|
|
86
|
+
const t0 = performance.now();
|
|
87
|
+
const attempts = [];
|
|
88
|
+
let waitMs = 0;
|
|
89
|
+
const key = activeRing.next();
|
|
90
|
+
if (!key && activeRing.size > 0) {
|
|
91
|
+
const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
|
|
92
|
+
err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
for (let attempt = 0; ; attempt++) {
|
|
96
|
+
const t = performance.now();
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
result = await attemptOnce(body, key, opts);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
result = err;
|
|
102
|
+
}
|
|
103
|
+
attempts.push({ attempt, type: result instanceof Error ? "network" : `http${result?.status}`, ms: Math.round(performance.now() - t) });
|
|
104
|
+
if (result instanceof Error) {
|
|
105
|
+
const entry = retry?.network;
|
|
106
|
+
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
107
|
+
activeRing.onError(key);
|
|
108
|
+
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
109
|
+
throw result;
|
|
110
|
+
}
|
|
111
|
+
const entry = retry?.[result.status];
|
|
112
|
+
if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
|
|
113
|
+
if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
|
|
114
|
+
try { result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) }; } catch {}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { url, attemptOnce, run };
|
|
120
|
+
}
|