mslxdff 0.1.105 → 0.1.106

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.
@@ -1,194 +0,0 @@
1
- // DeepSeek 账号池 + 登录(Android 协议)
2
- // 上游协议参考 iidamie/deepseek2api(GPL-3.0,协议事实)
3
- // 并发闸门参考 Chat2API-WXS per-account guard:每 token 在途限 1,全忙 FIFO 排队,流结束释放
4
- import { compatFetch } from "../../compat.js";
5
- import { envInt } from "../base.js";
6
- import { androidHeaders } from "./pow.js";
7
- import { DEEPSEEK_DEFAULT_BASE, DEEPSEEK_API_PREFIX } from "./pow.js";
8
- import { dsDebug } from "./debug.js";
9
-
10
- export { androidHeaders };
11
-
12
- const DEFAULT_COOLDOWN_MS = 30_000;
13
- // 差异化冷却(防禁言体系):频率前兆 60s(TQZHR 重试间隔同量级)、禁言 5min;防同号轰炸拖成禁言
14
- export const COOLDOWN_PRESETS = Object.freeze({ default: DEFAULT_COOLDOWN_MS, frequency: 60_000, muted: 300_000 });
15
-
16
- function isEmail(v) {
17
- return String(v || "").includes("@");
18
- }
19
-
20
- export async function loginDeepseek({ loginValue, password, areaCode = "+86", fetchImpl, dispatcher, baseUrl = DEEPSEEK_DEFAULT_BASE, connectTimeoutMs = 30_000 } = {}) {
21
- const doFetch = fetchImpl || compatFetch;
22
- const email = isEmail(loginValue);
23
- const payload = {
24
- email: email ? String(loginValue).trim() : "",
25
- mobile: email ? "" : String(loginValue).trim(),
26
- area_code: email ? "" : areaCode,
27
- password: String(password ?? ""),
28
- device_id: "mslxdff",
29
- os: "android",
30
- };
31
- const url = `${String(baseUrl).replace(/\/+$/, "")}${DEEPSEEK_API_PREFIX}/users/login`;
32
- const controller = new AbortController();
33
- const timer = setTimeout(() => controller.abort(new Error("DeepSeek 登录超时")), connectTimeoutMs);
34
- let res;
35
- try {
36
- res = await doFetch(url, {
37
- method: "POST",
38
- headers: androidHeaders(null),
39
- body: JSON.stringify(payload),
40
- ...(dispatcher ? { dispatcher } : {}),
41
- signal: controller.signal,
42
- });
43
- } catch (err) {
44
- throw new Error(`DeepSeek 登录失败: ${String(err?.message || err)}`);
45
- } finally {
46
- clearTimeout(timer);
47
- }
48
-
49
- let data = null;
50
- try { data = await res.json(); } catch {}
51
- const biz = data?.data;
52
- const bizCode = biz?.biz_code ?? data?.code;
53
- if (!res.ok || bizCode !== 0 || !biz?.biz_data?.user?.token) {
54
- const msg = biz?.biz_msg || data?.msg || (biz?.biz_data?.user ? "响应缺少 token" : "登录响应格式错误");
55
- throw new Error(`DeepSeek 登录失败: ${msg}${res.ok ? "" : ` (http ${res.status})`}`);
56
- }
57
- return { token: biz.biz_data.user.token, userId: biz.biz_data.user.id };
58
- }
59
-
60
- // 多账号轮换池:空闲最久优先 + 冷却 + onError;轮换语义对齐 NIyueeE/ds-free-api(选空闲最久者,最大化每次使用间隔)
61
- export function createAuthPool({ tokens = [], cooldownMs = DEFAULT_COOLDOWN_MS, clock = Date.now, maxConcurrentPerToken: maxConcParam } = {}) {
62
- const list = [...new Set((tokens || []).map((t) => String(t).trim()).filter(Boolean))];
63
- const until = new Map(); // token → 冷却到期时刻
64
- const lastUsedAt = new Map(); // token → 最近一次被取用时刻(空闲最久排序依据)
65
-
66
- function isCooling(token) {
67
- const u = until.get(token);
68
- return u != null && clock() < u;
69
- }
70
-
71
- // 空闲最久优先:平局按 list 顺序(与 round-robin 常规场景兼容,间隔拉开时倾向复用久未用号)
72
- function next() {
73
- if (!list.length) return null;
74
- let best = null;
75
- let bestIdle = -1;
76
- for (const token of list) {
77
- if (isCooling(token)) continue;
78
- const idle = clock() - (lastUsedAt.get(token) || 0);
79
- if (idle > bestIdle) {
80
- bestIdle = idle;
81
- best = token;
82
- }
83
- }
84
- if (best) lastUsedAt.set(best, clock());
85
- return best;
86
- }
87
-
88
- // cooldownMsOverride 可覆盖池默认(frequency/muted 走差异化冷却)
89
- function onError(token, { cooldownMs: cooldownMsOverride } = {}) {
90
- if (!list.includes(token)) return;
91
- const ms = Number(cooldownMsOverride) > 0 ? Number(cooldownMsOverride) : cooldownMs;
92
- until.set(token, clock() + ms);
93
- dsDebug("auth", { event: "rotate", tokenTail: String(token).slice(-6), cooldownMs: ms, cooling: list.length - available(), total: list.length });
94
- }
95
-
96
- function available() {
97
- return list.filter((t) => !isCooling(t)).length;
98
- }
99
-
100
- function requireToken() {
101
- const token = next();
102
- if (!token) {
103
- const err = new Error(list.length
104
- ? `DeepSeek: 所有账号都在冷却中(${list.length} 个),稍后再试或 -provider deepseek login 追加`
105
- : "缺少 DeepSeek 凭据,请先 -provider deepseek login 或设置 providerConfigs.deepseek.keys");
106
- err._deepseekNoAuth = true;
107
- throw err;
108
- }
109
- return token;
110
- }
111
-
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] };
194
- }
@@ -1,158 +0,0 @@
1
- // DeepSeek bridge:OpenAI messages ↔ DeepSeek web 协议互转
2
- // prompt 构造参考 iidamie/deepseek2api messages_prepare(协议事实)
3
- // SSE 事件层(event/data 分块 + 流尾 flush)+ 调 sse-decoder.js 纯解码
4
- import { createDeepseekDeltaParser } from "./sse-decoder.js";
5
-
6
- const USER_TAG = "<|User|>";
7
- const ASSISTANT_OPEN = "<|Assistant|>";
8
- const ASSISTANT_CLOSE = "<|end▁of▁sentence|>";
9
-
10
- export function buildPrompt(messages) {
11
- const list = (messages || []).map((m) => {
12
- let text = "";
13
- if (Array.isArray(m?.content)) {
14
- text = m.content
15
- .filter((p) => p?.type === "text" && typeof p.text === "string")
16
- .map((p) => p.text)
17
- .join("\n");
18
- } else if (m?.content != null) {
19
- text = String(m.content);
20
- }
21
- return { role: String(m?.role || ""), text };
22
- });
23
- if (!list.length) return "";
24
-
25
- const merged = [list[0]];
26
- for (const msg of list.slice(1)) {
27
- if (msg.role && msg.role === merged[merged.length - 1].role) {
28
- merged[merged.length - 1].text += `\n\n${msg.text}`;
29
- } else {
30
- merged.push(msg);
31
- }
32
- }
33
-
34
- const parts = [];
35
- for (let idx = 0; idx < merged.length; idx++) {
36
- const { role, text } = merged[idx];
37
- if (role === "assistant") {
38
- parts.push(`${ASSISTANT_OPEN}${text}${ASSISTANT_CLOSE}`);
39
- } else if (role === "user" || role === "system") {
40
- parts.push(idx > 0 ? `${USER_TAG}${text}` : text);
41
- } else {
42
- parts.push(text);
43
- }
44
- }
45
- return parts.join("");
46
- }
47
-
48
- // 输入字符上限(ds-free-api 实测标定:default/vision≈1M tokens;expert≈64K tokens 与官方 API 一致)
49
- // 超限时上游 HTTP 200 + event:hint(input_exceeds_limit) + 立即 close;阈值取上限 75% 留余量
50
- const INPUT_CHAR_LIMITS = { expert: 163_840, default: 2_621_440, vision: 2_621_440 };
51
-
52
- export function promptThresholdFor(flags = {}) {
53
- const limit = flags?.expert ? INPUT_CHAR_LIMITS.expert : INPUT_CHAR_LIMITS.default;
54
- return Math.floor(limit * 0.75);
55
- }
56
-
57
- // 硬切(对齐 ds-free-api split_prompt_chunks:不感知标签边界,按字符数)
58
- export function splitPromptChunks(prompt, chunkSize) {
59
- const s = String(prompt ?? "");
60
- if (!chunkSize || chunkSize <= 0) return s ? [s] : [];
61
- const chunks = [];
62
- for (let i = 0; i < s.length; i += chunkSize) chunks.push(s.slice(i, i + chunkSize));
63
- return chunks;
64
- }
65
-
66
- export function mapModelToFlags(modelId) {
67
- const id = String(modelId || "");
68
- // 官网「专家模式」:completion body 显式 model_type:"expert"(真机抓包,model_type 取值 default/chat/reasoner/vision/expert)
69
- return {
70
- thinking: id.includes("reasoner"),
71
- search: id.includes("search"),
72
- expert: id.includes("expert"),
73
- };
74
- }
75
-
76
- export function buildUpstreamBody({ sessionId, prompt, thinking, search, expert, parentMessageId = null }) {
77
- const body = {
78
- chat_session_id: sessionId,
79
- parent_message_id: parentMessageId ?? null,
80
- prompt: String(prompt ?? ""),
81
- ref_file_ids: [],
82
- thinking_enabled: !!thinking,
83
- search_enabled: !!search,
84
- };
85
- // 非 expert 不传 model_type(v2.0.0 实测缺省走 session 的 default;expert 模式显式声明)
86
- if (expert) body.model_type = "expert";
87
- return body;
88
- }
89
-
90
- // SSE 事件层:喂入文本(可跨块断行),产出标准 delta 事件数组。
91
- // 上游形态(Android x-client-version 2.0.0 真机抓包):
92
- // event: ready / update_session / title / close
93
- // data: {"v":{"response":{fragments:[{type,content}]}}} → 快照(decoder 做 unseen 后缀去重)
94
- // data: {"p":"response/fragments/-1/content","o":"APPEND","v":"字"} → 定向增量
95
- // data: {"v":"字"} → 裸值:沿用 currentKind
96
- // data: {"p":"response/status","o":"SET","v":"FINISHED"} → 结束
97
- // startKind:thinking 请求首个 THINK fragment 可能以空对象 {} 下发(无 type),靠请求方兜底
98
- export function createDeepseekSseParser({ startKind = "content" } = {}) {
99
- let buffer = "";
100
- let closeSeen = false;
101
- const decoder = createDeepseekDeltaParser({ startKind });
102
-
103
- function processEventBlock(rawEvent, out) {
104
- let eventName = "";
105
- const dataLines = [];
106
- for (const line of rawEvent.split("\n")) {
107
- const trimmed = line.trim();
108
- if (!trimmed) continue;
109
- if (trimmed.startsWith("event:")) { eventName = trimmed.slice(6).trim(); continue; }
110
- if (trimmed.startsWith("data:")) dataLines.push(trimmed.slice(5).trim());
111
- }
112
- if (eventName === "close" && !closeSeen && !decoder._state.finished) {
113
- closeSeen = true;
114
- out.push({ finish: "stop" });
115
- return;
116
- }
117
- // 上游拒绝信号(真机抓包):event: hint + {"type":"error","content":"内容超长,请删减后再试","finish_reason":"input_exceeds_limit"}
118
- // 后跟 event: close,HTTP 仍是 200 —— 不透传就是"成功但空回复"
119
- if (eventName === "hint") {
120
- for (const dataStr of dataLines) {
121
- let d = null;
122
- try { d = JSON.parse(dataStr); } catch {}
123
- if (d?.type === "error") {
124
- out.push({ error: String(d.content || "上游拒绝"), finishReason: d.finish_reason || null, clearResponse: d.clear_response === true });
125
- return;
126
- }
127
- }
128
- return;
129
- }
130
- for (const dataStr of dataLines) {
131
- if (!dataStr || dataStr === "[DONE]") continue;
132
- for (const delta of decoder.consume(dataStr)) {
133
- if (delta.kind === "finish") out.push({ finish: "stop" });
134
- else if (delta.kind === "reasoning") out.push({ reasoning: delta.text });
135
- else out.push({ content: delta.text });
136
- }
137
- }
138
- }
139
-
140
- return function parse(chunk) {
141
- buffer += String(chunk ?? "");
142
- const out = [];
143
- let idx;
144
- while ((idx = buffer.indexOf("\n\n")) >= 0) {
145
- const rawEvent = buffer.slice(0, idx);
146
- buffer = buffer.slice(idx + 2);
147
- processEventBlock(rawEvent, out);
148
- }
149
- // 流尾 flush:仅当剩余 buffer 以换行结尾(data 行已完整终止)才处理,
150
- // 半截 JSON 保持缓冲等待下一块;否则 FINISHED/close 等尾部事件会永久卡在 buffer
151
- if (buffer.endsWith("\n") && buffer.trim()) {
152
- const rest = buffer;
153
- buffer = "";
154
- processEventBlock(rest, out);
155
- }
156
- return out;
157
- };
158
- }
@@ -1,233 +0,0 @@
1
- // DeepSeek completion 编排:token→session→pow→completion→(流|聚合)→delete
2
- // 超限分流:expert > 阈值 → 分块喂养 session(对齐 NIyueeE/ds-free-api 方案 B);其余超限人话报错
3
- // 风控语义参考 TQZHR/deepseek2api classifyProtocolResponse(MIT)
4
- import { androidHeaders, solveChallenge, netRetry, DEEPSEEK_DEFAULT_BASE, DEEPSEEK_API_PREFIX } from "./pow.js";
5
- import { COOLDOWN_PRESETS } from "./auth.js";
6
- import { buildPrompt, mapModelToFlags, createDeepseekSseParser, buildUpstreamBody, promptThresholdFor, splitPromptChunks } from "./bridge.js";
7
- import { createChatSession, deleteChatSession, feedChunkToSession } from "./session.js";
8
- import { dsDebug, dsDump, dsError } from "./debug.js";
9
-
10
- const CAPTCHA_PATTERN = /captcha|shumei|数美|验证码|风控|verification/i;
11
- // user is muted:账号被禁言/风控(上游对 completion 直接拒),切账号冷却,禁言期间不再打上游
12
- const MUTED_PATTERN = /user is muted|account is muted|\bmuted\b|禁言/i;
13
- // 「消息发送过于频繁」= 禁言前兆(TQZHR/deepseek2api 同款识别),命中即冷却换号,避免小病拖成禁言
14
- const FREQUENCY_PATTERN = /消息发送过于频繁[\s,,、::]*请稍后重试/;
15
-
16
- function classifyProtocolFailure(text, status) {
17
- const t = String(text || "");
18
- if (FREQUENCY_PATTERN.test(t)) {
19
- return upstreamError("DeepSeek 上游提示「消息发送过于频繁」:该账号触发频率风控(禁言前兆),已自动冷却并切换账号", { status, rotateAuth: true, cooldownMs: COOLDOWN_PRESETS.frequency });
20
- }
21
- if (MUTED_PATTERN.test(t)) {
22
- return upstreamError("DeepSeek 账号被禁言/风控(user is muted):已自动冷却该账号,请等待解除或 -provider deepseek login 追加账号", { status, rotateAuth: true, cooldownMs: COOLDOWN_PRESETS.muted });
23
- }
24
- return null;
25
- }
26
-
27
- function fmt(n) { return Number(n).toLocaleString("en-US"); }
28
-
29
- function upstreamError(message, { status, rotateAuth, cooldownMs } = {}) {
30
- const err = new Error(message);
31
- err.status = status;
32
- if (rotateAuth) err._rotateAuth = true;
33
- if (cooldownMs) err._cooldownMs = cooldownMs;
34
- return err;
35
- }
36
-
37
- function classifyFailure(status, bodyText) {
38
- const text = String(bodyText || "");
39
- const muted = classifyProtocolFailure(text, status);
40
- if (muted) return muted;
41
- if (CAPTCHA_PATTERN.test(text)) {
42
- return upstreamError("DeepSeek 触发上游风控/验证码,请稍后再试或换账号(-provider deepseek login 追加)", { status, rotateAuth: true });
43
- }
44
- if (status === 401 || status === 403) {
45
- return upstreamError(`DeepSeek 凭据被拒 (http ${status}),自动切换下一个账号或重新 login`, { status, rotateAuth: true });
46
- }
47
- if (status === 429) {
48
- return upstreamError("DeepSeek 请求过于频繁 (429),账号已冷却,稍后再试", { status, rotateAuth: true });
49
- }
50
- return upstreamError(`DeepSeek completion 失败 (http ${status}): ${text.slice(0, 300)}`, { status });
51
- }
52
-
53
- async function readBodyText(res) {
54
- try { return await res.text(); } catch { return ""; }
55
- }
56
-
57
- // 公共 completion 请求:netRetry + 非 SSE 检查 + INVALID_POW 一次重试(重解 challenge)
58
- async function fetchCompletionWithPow({ token, base, body, pow, fetchImpl, dispatcher, connectTimeoutMs }) {
59
- const url = `${base}${DEEPSEEK_API_PREFIX}/chat/completion`;
60
- for (let powAttempt = 0; ; powAttempt++) {
61
- let res;
62
- try {
63
- res = await netRetry(() => fetchImpl(url, {
64
- method: "POST",
65
- headers: androidHeaders(token, { "x-ds-pow-response": pow.header }),
66
- body: JSON.stringify(body),
67
- ...(dispatcher ? { dispatcher } : {}),
68
- }), { attempts: 2, delayMs: 400 });
69
- } catch (err) {
70
- dsError("completion", err);
71
- throw upstreamError(`DeepSeek completion 网络失败: ${String(err?.message || err)}`, { rotateAuth: false });
72
- }
73
- dsDebug("completion", { event: "response", status: res.status, contentType: res.headers.get("content-type"), powAttempt, promptLen: body.prompt.length });
74
-
75
- if (res.ok) {
76
- const contentType = res.headers.get("content-type") || "";
77
- if (!contentType.includes("text/event-stream")) {
78
- const text = await readBodyText(res);
79
- const muted = classifyProtocolFailure(text, res.status);
80
- if (muted) { dsDump("completion", "muted", text, 400); throw muted; }
81
- let payload = null;
82
- try { payload = JSON.parse(text); } catch {}
83
- const bizMsg = payload?.data?.biz_msg || payload?.msg || text.slice(0, 200);
84
- dsDump("completion", "non-sse body", text, 1200);
85
- throw upstreamError(`DeepSeek completion 返回非 SSE: ${bizMsg}`, { status: res.status });
86
- }
87
- return res;
88
- }
89
-
90
- const text = await readBodyText(res);
91
- dsDump("completion", `error body http=${res.status}`, text, 1200);
92
- if (/INVALID_POW_RESPONSE/i.test(text) && powAttempt < 1) {
93
- const retryPow = await solveChallenge({ token, fetchImpl, dispatcher, baseUrl: base, connectTimeoutMs });
94
- pow.header = retryPow.header;
95
- continue;
96
- }
97
- throw classifyFailure(res.status, text);
98
- }
99
- }
100
-
101
- // 单发路径(不超阈值)
102
- async function runOnce({ token, flags, prompt, isStream, fetchImpl, dispatcher, baseUrl, connectTimeoutMs }) {
103
- const base = String(baseUrl || DEEPSEEK_DEFAULT_BASE).replace(/\/+$/, "");
104
- const sessionId = await createChatSession({ token, fetchImpl, dispatcher, baseUrl: base, connectTimeoutMs });
105
- const pow = await solveChallenge({ token, fetchImpl, dispatcher, baseUrl: base, connectTimeoutMs });
106
- const completionBody = buildUpstreamBody({ sessionId, prompt, thinking: flags.thinking, search: flags.search, expert: flags.expert });
107
- dsDebug("completion", { event: "request", sessionId, thinking: flags.thinking, search: flags.search, expert: flags.expert, promptLen: prompt.length, promptHead: prompt.slice(0, 120), body: JSON.stringify(completionBody) });
108
- const res = await fetchCompletionWithPow({ token, base, body: completionBody, pow, fetchImpl, dispatcher, connectTimeoutMs });
109
- return { kind: isStream ? "stream" : "aggregate", res, sessionId, token };
110
- }
111
-
112
- // 分块喂养路径(expert 超阈值):共享 session,非末块只喂历史(ready→stop_stream→drain),末块正常生成
113
- async function runChunked({ token, flags, prompt, isStream, fetchImpl, dispatcher, baseUrl, connectTimeoutMs, threshold }) {
114
- const base = String(baseUrl || DEEPSEEK_DEFAULT_BASE).replace(/\/+$/, "");
115
- const chunks = splitPromptChunks(prompt, threshold);
116
- const sessionId = await createChatSession({ token, fetchImpl, dispatcher, baseUrl: base, connectTimeoutMs });
117
- dsDebug("chunked", { event: "start", sessionId, totalChunks: chunks.length, promptLen: prompt.length, threshold });
118
- let parentId = null;
119
- try {
120
- for (const chunk of chunks.slice(0, -1)) {
121
- const pow = await solveChallenge({ token, fetchImpl, dispatcher, baseUrl: base, connectTimeoutMs });
122
- const feedBody = buildUpstreamBody({ sessionId, prompt: chunk, thinking: false, search: false, expert: flags.expert, parentMessageId: parentId });
123
- dsDebug("chunked", { event: "feed", chunkLen: chunk.length, parentMessageId: parentId });
124
- const res = await fetchCompletionWithPow({ token, base, body: feedBody, pow, fetchImpl, dispatcher, connectTimeoutMs });
125
- parentId = await feedChunkToSession({ res, token, sessionId, fetchImpl, dispatcher, baseUrl, connectTimeoutMs });
126
- dsDebug("chunked", { event: "fed", parentMessageId: parentId });
127
- }
128
- const lastChunk = chunks[chunks.length - 1];
129
- const pow = await solveChallenge({ token, fetchImpl, dispatcher, baseUrl: base, connectTimeoutMs });
130
- const body = buildUpstreamBody({ sessionId, prompt: lastChunk, thinking: flags.thinking, search: flags.search, expert: flags.expert, parentMessageId: parentId });
131
- dsDebug("chunked", { event: "final", chunkLen: lastChunk.length, parentMessageId: parentId, thinking: flags.thinking, search: flags.search });
132
- const res = await fetchCompletionWithPow({ token, base, body, pow, fetchImpl, dispatcher, connectTimeoutMs });
133
- return { kind: isStream ? "stream" : "aggregate", res, sessionId, token };
134
- } catch (err) {
135
- dsError("chunked", err);
136
- await deleteChatSession({ token, sessionId, fetchImpl, dispatcher, baseUrl });
137
- throw err;
138
- }
139
- }
140
-
141
- export function aggregateFromText(sseText, model, { thinking = false } = {}) {
142
- const parse = createDeepseekSseParser({ startKind: thinking ? "reasoning" : "content" });
143
- let content = "";
144
- let reasoning = "";
145
- let finish = "stop";
146
- for (const ev of parse(String(sseText ?? ""))) {
147
- // 上游拒绝(hint error):人话抛出,绝不返回空 200;频率风控/禁言命中时冷却换号
148
- if (ev.error) {
149
- const rotate = FREQUENCY_PATTERN.test(String(ev.error)) || MUTED_PATTERN.test(String(ev.error));
150
- throw upstreamError(`DeepSeek 上游拒绝: ${ev.error}${ev.finishReason ? ` (${ev.finishReason})` : ""}`, { status: 502, rotateAuth: rotate });
151
- }
152
- if (ev.content) content += ev.content;
153
- if (ev.reasoning) reasoning += ev.reasoning;
154
- if (ev.finish) finish = ev.finish;
155
- }
156
- const message = { role: "assistant", content };
157
- if (reasoning) message.reasoning_content = reasoning;
158
- return {
159
- id: `chatcmpl-deepseek-${Date.now()}`,
160
- object: "chat.completion",
161
- created: Math.floor(Date.now() / 1000),
162
- model,
163
- choices: [{ index: 0, message, finish_reason: finish }],
164
- usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
165
- };
166
- }
167
-
168
- export async function runDeepseekChat({ body, authPool, fetchImpl, dispatcher, baseUrl, connectTimeoutMs = 30_000, maxAuthRetries } = {}) {
169
- const model = body?.model || "deepseek/chat";
170
- const rawId = String(model).includes("/") ? String(model).split("/").pop() : String(model);
171
- const flags = mapModelToFlags(rawId);
172
- const prompt = buildPrompt(body?.messages || []);
173
- const isStream = body?.stream === true;
174
- const threshold = promptThresholdFor(flags);
175
-
176
- // 超限分流(ds-free-api 实测标定:default/vision 2,621,440 / expert 163,840,阈值取 75%)
177
- if (prompt.length > threshold) {
178
- if (!flags.expert) {
179
- throw upstreamError(`DeepSeek ${model} 输入超长:${fmt(prompt.length)} 字符,超过 ${fmt(threshold)} 上限(网页通道 default 模型上限 2,621,440 字符)。请压缩对话历史(opencode /compact)或改用 deepseek-*-expert-free(自动分块)`, { status: 413 });
180
- }
181
- dsDebug("oversize", { event: "chunked-mode", model, promptLen: prompt.length, threshold });
182
- }
183
-
184
- const maxAttempts = maxAuthRetries ?? Math.max(1, authPool.size);
185
- for (let attempt = 0; attempt <= maxAttempts; attempt++) {
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;
192
- try {
193
- const out = prompt.length > threshold
194
- ? await runChunked({ token, flags, prompt, isStream, fetchImpl, dispatcher, baseUrl, connectTimeoutMs, threshold })
195
- : await runOnce({ token, flags, prompt, isStream, fetchImpl, dispatcher, baseUrl, connectTimeoutMs });
196
- if (out.kind === "stream") {
197
- // 流式:槽位由 cleanup 链释放(buildOpenAiSseStream 的 done/error/cancel),此处不放
198
- const prevCleanup = out.cleanup;
199
- return {
200
- kind: "stream",
201
- res: out.res,
202
- token,
203
- cleanup: async () => {
204
- try { await prevCleanup?.(); } finally { release(); }
205
- },
206
- };
207
- }
208
- const sseText = await readBodyText(out.res);
209
- let data;
210
- try {
211
- data = aggregateFromText(sseText, model, { thinking: flags.thinking });
212
- } catch (err) {
213
- await deleteChatSession({ token: out.token, sessionId: out.sessionId, fetchImpl, dispatcher, baseUrl });
214
- throw err;
215
- }
216
- dsDump("aggregate", `sseText model=${model}`, sseText, 4000);
217
- if (!data.choices[0].message.content && !data.choices[0].message.reasoning_content) {
218
- dsDump("aggregate", "EMPTY aggregate content! full sseText", sseText, 8000);
219
- }
220
- await deleteChatSession({ token: out.token, sessionId: out.sessionId, fetchImpl, dispatcher, baseUrl });
221
- release(); // 聚合成功:槽位随会话删除一并释放
222
- return { kind: "json", data };
223
- } catch (err) {
224
- release(); // 失败(含 rotateAuth 重试):立即释放,避免槽位泄漏
225
- dsError(`chat attempt=${attempt}`, err);
226
- if (err?._rotateAuth) authPool.onError(token, { cooldownMs: err._cooldownMs });
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;
230
- }
231
- }
232
- throw upstreamError("DeepSeek: 所有账号均不可用", {});
233
- }
@@ -1,44 +0,0 @@
1
- // DeepSeek 排障日志:落 ~/.config/mslxdff/deepseek-debug.log(5MB 轮转),定位空流/风控/协议异常
2
- // 解决后统一摘除调用点(用户授权全量日志)
3
- import { appendFileSync, statSync, unlinkSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { homedir } from "node:os";
6
-
7
- const DEBUG_FILE = process.env.MSLXDFF_DEEPSEEK_DEBUG_FILE
8
- || join(homedir(), ".config", "mslxdff", "deepseek-debug.log");
9
- const MAX_BYTES = 5 * 1024 * 1024;
10
-
11
- function rotateIfNeeded() {
12
- try {
13
- if (statSync(DEBUG_FILE).size > MAX_BYTES) unlinkSync(DEBUG_FILE);
14
- } catch {}
15
- }
16
-
17
- function write(line) {
18
- try {
19
- rotateIfNeeded();
20
- appendFileSync(DEBUG_FILE, line + "\n");
21
- } catch {}
22
- }
23
-
24
- function ts() {
25
- return new Date().toISOString().slice(11, 23);
26
- }
27
-
28
- // 结构化一行:dsDebug("completion", { model, status, ... })
29
- export function dsDebug(scope, fields = {}) {
30
- const pairs = Object.entries(fields).map(([k, v]) => `${k}=${typeof v === "string" ? JSON.stringify(v) : String(v)}`);
31
- write(`[${ts()}] [${scope}] ${pairs.join(" ")}`);
32
- }
33
-
34
- // 原始文本截断记录(SSE chunk / 聚合全文)
35
- export function dsDump(scope, label, text, limit = 2000) {
36
- const s = String(text ?? "");
37
- write(`[${ts()}] [${scope}] ${label} (${s.length}B) >>> ${s.slice(0, limit).replace(/\n/g, "\\n")}<<<`);
38
- }
39
-
40
- export function dsError(scope, err) {
41
- write(`[${ts()}] [${scope}] ERROR ${String(err?.stack || err?.message || err).slice(0, 800)}`);
42
- }
43
-
44
- export { DEBUG_FILE };