mslxdff 0.1.98 → 0.1.100

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.98",
3
+ "version": "0.1.100",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,8 @@
1
1
  // DeepSeek 账号池 + 登录(Android 协议)
2
2
  // 上游协议参考 iidamie/deepseek2api(GPL-3.0,协议事实)
3
+ // 并发闸门参考 Chat2API-WXS per-account guard:每 token 在途限 1,全忙 FIFO 排队,流结束释放
3
4
  import { compatFetch } from "../../compat.js";
5
+ import { envInt } from "../base.js";
4
6
  import { androidHeaders } from "./pow.js";
5
7
  import { DEEPSEEK_DEFAULT_BASE, DEEPSEEK_API_PREFIX } from "./pow.js";
6
8
  import { dsDebug } from "./debug.js";
@@ -56,7 +58,7 @@ export async function loginDeepseek({ loginValue, password, areaCode = "+86", fe
56
58
  }
57
59
 
58
60
  // 多账号轮换池:空闲最久优先 + 冷却 + onError;轮换语义对齐 NIyueeE/ds-free-api(选空闲最久者,最大化每次使用间隔)
59
- export function createAuthPool({ tokens = [], cooldownMs = DEFAULT_COOLDOWN_MS, clock = Date.now } = {}) {
61
+ export function createAuthPool({ tokens = [], cooldownMs = DEFAULT_COOLDOWN_MS, clock = Date.now, maxConcurrentPerToken: maxConcParam } = {}) {
60
62
  const list = [...new Set((tokens || []).map((t) => String(t).trim()).filter(Boolean))];
61
63
  const until = new Map(); // token → 冷却到期时刻
62
64
  const lastUsedAt = new Map(); // token → 最近一次被取用时刻(空闲最久排序依据)
@@ -107,5 +109,86 @@ export function createAuthPool({ tokens = [], cooldownMs = DEFAULT_COOLDOWN_MS,
107
109
  return token;
108
110
  }
109
111
 
110
- return { next, onError, available, requireToken, size: list.length, cooldownMs, keys: [...list] };
112
+ // ---------------------------------------------------------------------------
113
+ // 并发闸门:每 token 最多 maxConcurrentPerToken 个在途请求(默认 1 = 禁止并发)
114
+ // acquireSlot → { token, release } | null(超时);release 幂等,流式在 cleanup 链释放
115
+ // ---------------------------------------------------------------------------
116
+ const maxConcurrentPerToken = Math.max(1, Number(maxConcParam) > 0 ? Number(maxConcParam) : envInt("MSLXDFF_DEEPSEEK_MAX_CONCURRENT", 1));
117
+ const inFlight = new Map(); // token → 在途数
118
+ const waiters = []; // FIFO 等待队列:{ resolve, timer }
119
+
120
+ function inflightOf(token) {
121
+ return inFlight.get(token) || 0;
122
+ }
123
+
124
+ // 空闲(未冷却且在途未满)的号里选空闲最久者;无 → null
125
+ function nextIdle() {
126
+ let best = null;
127
+ let bestIdle = -1;
128
+ for (const token of list) {
129
+ if (isCooling(token) || inflightOf(token) >= maxConcurrentPerToken) continue;
130
+ const idle = clock() - (lastUsedAt.get(token) || 0);
131
+ if (idle > bestIdle) {
132
+ bestIdle = idle;
133
+ best = token;
134
+ }
135
+ }
136
+ return best;
137
+ }
138
+
139
+ function makeRelease(token) {
140
+ let released = false;
141
+ return () => {
142
+ if (released) return;
143
+ released = true;
144
+ inFlight.set(token, Math.max(0, inflightOf(token) - 1));
145
+ dsDebug("gate", { event: "release", tokenTail: String(token).slice(-6), inFlight: inflightOf(token) });
146
+ wakeNext();
147
+ };
148
+ }
149
+
150
+ // 有槽位释放 → 唤醒队首等待者重新竞争(重新走 nextIdle,天然跳过冷却/满载号)
151
+ function wakeNext() {
152
+ while (waiters.length) {
153
+ const w = waiters.shift();
154
+ clearTimeout(w.timer);
155
+ const token = nextIdle();
156
+ if (!token) { waiters.unshift(w); break; } // 没有可用槽位,塞回队首等下次 release
157
+ lastUsedAt.set(token, clock());
158
+ inFlight.set(token, inflightOf(token) + 1);
159
+ dsDebug("gate", { event: "wakeup", tokenTail: String(token).slice(-6), queue: waiters.length });
160
+ w.resolve({ token, release: makeRelease(token) });
161
+ }
162
+ }
163
+
164
+ async function acquireSlot({ timeoutMs } = {}) {
165
+ const timeout = Number(timeoutMs) > 0 ? Number(timeoutMs) : envInt("MSLXDFF_DEEPSEEK_QUEUE_TIMEOUT_MS", 30_000);
166
+ const token = nextIdle();
167
+ if (token) {
168
+ lastUsedAt.set(token, clock());
169
+ inFlight.set(token, inflightOf(token) + 1);
170
+ dsDebug("gate", { event: "acquire-direct", tokenTail: String(token).slice(-6), inFlight: inflightOf(token) });
171
+ return { token, release: makeRelease(token) };
172
+ }
173
+ if (!list.length) {
174
+ const err = new Error("缺少 DeepSeek 凭据,请先 -provider deepseek login 或设置 providerConfigs.deepseek.keys");
175
+ err._deepseekNoAuth = true;
176
+ throw err;
177
+ }
178
+ // 全忙:入 FIFO 队列等待 release 唤醒;超时返回 null。
179
+ // 定时器不 unref:有请求在排队 = 有未完成工作,事件循环必须等它(unref 会让进程提前退出)
180
+ return new Promise((resolve) => {
181
+ const timer = setTimeout(() => {
182
+ const i = waiters.indexOf(w);
183
+ if (i >= 0) waiters.splice(i, 1);
184
+ dsDebug("gate", { event: "acquire-timeout", queue: waiters.length });
185
+ resolve(null);
186
+ }, timeout);
187
+ const w = { resolve, timer };
188
+ waiters.push(w);
189
+ dsDebug("gate", { event: "enqueue", queue: waiters.length, timeoutMs: timeout });
190
+ });
191
+ }
192
+
193
+ return { next, onError, available, requireToken, acquireSlot, maxConcurrentPerToken, size: list.length, cooldownMs, keys: [...list] };
111
194
  }
@@ -183,17 +183,26 @@ export async function runDeepseekChat({ body, authPool, fetchImpl, dispatcher, b
183
183
 
184
184
  const maxAttempts = maxAuthRetries ?? Math.max(1, authPool.size);
185
185
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
186
- const token = authPool.requireToken();
186
+ // 并发闸门:每 token 在途限 1(MSLXDFF_DEEPSEEK_MAX_CONCURRENT),全忙排队,超时人话 429
187
+ const slot = await authPool.acquireSlot({});
188
+ if (!slot) {
189
+ throw upstreamError("DeepSeek: 所有账号都在忙(并发闸门排队超时),稍后重试或调大 MSLXDFF_DEEPSEEK_QUEUE_TIMEOUT_MS / MSLXDFF_DEEPSEEK_MAX_CONCURRENT", { status: 429 });
190
+ }
191
+ const { token, release } = slot;
187
192
  try {
188
193
  const out = prompt.length > threshold
189
194
  ? await runChunked({ token, flags, prompt, isStream, fetchImpl, dispatcher, baseUrl, connectTimeoutMs, threshold })
190
195
  : await runOnce({ token, flags, prompt, isStream, fetchImpl, dispatcher, baseUrl, connectTimeoutMs });
191
196
  if (out.kind === "stream") {
197
+ // 流式:槽位由 cleanup 链释放(buildOpenAiSseStream 的 done/error/cancel),此处不放
198
+ const prevCleanup = out.cleanup;
192
199
  return {
193
200
  kind: "stream",
194
201
  res: out.res,
195
202
  token,
196
- cleanup: () => deleteChatSession({ token: out.token, sessionId: out.sessionId, fetchImpl, dispatcher, baseUrl }),
203
+ cleanup: async () => {
204
+ try { await prevCleanup?.(); } finally { release(); }
205
+ },
197
206
  };
198
207
  }
199
208
  const sseText = await readBodyText(out.res);
@@ -209,11 +218,15 @@ export async function runDeepseekChat({ body, authPool, fetchImpl, dispatcher, b
209
218
  dsDump("aggregate", "EMPTY aggregate content! full sseText", sseText, 8000);
210
219
  }
211
220
  await deleteChatSession({ token: out.token, sessionId: out.sessionId, fetchImpl, dispatcher, baseUrl });
221
+ release(); // 聚合成功:槽位随会话删除一并释放
212
222
  return { kind: "json", data };
213
223
  } catch (err) {
224
+ release(); // 失败(含 rotateAuth 重试):立即释放,避免槽位泄漏
214
225
  dsError(`chat attempt=${attempt}`, err);
215
226
  if (err?._rotateAuth) authPool.onError(token, { cooldownMs: err._cooldownMs });
216
- if (!err?._rotateAuth || attempt >= maxAttempts - 1) throw err;
227
+ // 会话创建/删除阶段的 401/403 也属凭据被拒(session.js status),与 completion 阶段对齐
228
+ else if (err?.status === 401 || err?.status === 403) authPool.onError(token, { cooldownMs: COOLDOWN_PRESETS.default });
229
+ if (!(err?._rotateAuth || err?.status === 401 || err?.status === 403) || attempt >= maxAttempts - 1) throw err;
217
230
  }
218
231
  }
219
232
  throw upstreamError("DeepSeek: 所有账号均不可用", {});
@@ -30,7 +30,10 @@ export async function createChatSession({ token, fetchImpl, dispatcher, baseUrl
30
30
  const id = data?.data?.biz_data?.chat_session?.id ?? data?.data?.biz_data?.id;
31
31
  if (!res.ok || data?.data?.biz_code !== 0 || !id) {
32
32
  const msg = data?.data?.biz_msg || data?.msg || "响应缺少会话 id";
33
- throw new Error(`DeepSeek 会话创建失败: ${msg}${res.ok ? "" : ` (http ${res.status})`}`);
33
+ const err = new Error(`DeepSeek 会话创建失败: ${msg}${res.ok ? "" : ` (http ${res.status})`}`);
34
+ // 401/403 = 凭据被拒:带 status 供上层 rotateAuth(与 completion 阶段 classifyFailure 对齐)
35
+ if (res.status === 401 || res.status === 403) err.status = res.status;
36
+ throw err;
34
37
  }
35
38
  return id;
36
39
  }