mslxdff 0.1.104 → 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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/auto.js +3 -3
  3. package/src/chat/engine.js +2 -2
  4. package/src/chat/gateway.js +6 -2
  5. package/src/chat/orchestrator.js +7 -0
  6. package/src/chat/prompt.js +2 -2
  7. package/src/chat/repl.js +11 -3
  8. package/src/chat/terminal.js +40 -0
  9. package/src/chat-pipeline/index.js +14 -3
  10. package/src/chat-pipeline/policy.js +3 -0
  11. package/src/chat-pipeline/serial-trial.js +15 -5
  12. package/src/cli/commands/provider/bench-via.js +0 -9
  13. package/src/cli/commands/provider/bench.js +0 -8
  14. package/src/cli/commands/provider/index.js +0 -4
  15. package/src/cli/commands/provider/models.js +0 -3
  16. package/src/cli/commands/use-group.js +56 -0
  17. package/src/cli/help.js +2 -0
  18. package/src/cli/index.js +2 -0
  19. package/src/providers/registry.js +0 -5
  20. package/src/routes/hedge.js +1 -2
  21. package/src/runtime/providers-setup.js +1 -2
  22. package/src/state/facade.js +1 -0
  23. package/src/state/schemas/use-group.js +56 -0
  24. package/src/cli/commands/provider/deepseek-health.js +0 -46
  25. package/src/cli/commands/provider/deepseek-login.js +0 -89
  26. package/src/providers/deepseek/auth.js +0 -194
  27. package/src/providers/deepseek/bridge.js +0 -158
  28. package/src/providers/deepseek/chat.js +0 -233
  29. package/src/providers/deepseek/debug.js +0 -44
  30. package/src/providers/deepseek/hash-reference.js +0 -117
  31. package/src/providers/deepseek/hash.js +0 -245
  32. package/src/providers/deepseek/health.js +0 -104
  33. package/src/providers/deepseek/index.js +0 -180
  34. package/src/providers/deepseek/pow.js +0 -99
  35. package/src/providers/deepseek/session.js +0 -117
  36. package/src/providers/deepseek/sse-decoder.js +0 -160
  37. package/src/providers/deepseek.js +0 -1
@@ -1,89 +0,0 @@
1
- // -provider deepseek login:把 chat.deepseek.com 凭据落盘 providerConfigs.deepseek.keys
2
- // 三种用法:
3
- // login --token <userToken> 浏览器贴 token(F12 → Application → Local Storage → userToken)
4
- // login <email|mobile> <password> 账密登录换 token(Android 协议)
5
- // login 打印用法引导
6
- export async function handleDeepseekLogin(id, sub, rest) {
7
- if (id !== "deepseek" && id !== "ds") return false;
8
- if (sub !== "login" && sub !== "auth") return false;
9
-
10
- const args = rest || [];
11
- const tokenFlagIdx = args.findIndex((a) => a === "--token" || a === "-token" || a === "token");
12
-
13
- if (tokenFlagIdx < 0 && args.filter((a) => !String(a).startsWith("-")).length < 2) {
14
- printUsage();
15
- process.exit(0);
16
- }
17
-
18
- const { loadProviderKeys, saveProviderConfig, loadProviderConfig } = await import("../../../state.js");
19
-
20
- let token = null;
21
- let label = "";
22
-
23
- if (tokenFlagIdx >= 0) {
24
- token = String(args[tokenFlagIdx + 1] || "").trim();
25
- if (!token) {
26
- console.error("❌ --token 后面要贴 userToken 值");
27
- printUsage();
28
- process.exit(1);
29
- }
30
- label = "userToken(浏览器)";
31
- } else {
32
- const [loginValue, password] = args.filter((a) => !String(a).startsWith("-"));
33
- console.log("🚀 DeepSeek 账密登录中(Android 协议)...");
34
- try {
35
- const { loginDeepseek } = await import("../../../providers/deepseek/auth.js");
36
- const out = await loginDeepseek({ loginValue, password });
37
- token = out.token;
38
- label = `账密(${String(loginValue).slice(0, 3)}***)`;
39
- console.log(`✅ 登录成功,拿到 token`);
40
- } catch (e) {
41
- console.error(`❌ ${e.message}`);
42
- console.error(`\n换浏览器贴 token 方式:chat.deepseek.com 登录后 F12 → Application → Local Storage → 复制 userToken →`);
43
- console.error(` mslxdff -provider deepseek login --token <userToken>`);
44
- process.exit(1);
45
- }
46
- }
47
-
48
- if (!token) {
49
- console.error("❌ 未拿到 token");
50
- process.exit(1);
51
- }
52
-
53
- const cur = loadProviderKeys("deepseek");
54
- if (cur.includes(token)) {
55
- console.log(`ℹ️ 该 token 已存在(现 ${cur.length} 个账号),跳过`);
56
- } else {
57
- const cfg = loadProviderConfig("deepseek") || {};
58
- const nextKeys = [...new Set([...(cfg.keys || cur), token].filter(Boolean))];
59
- saveProviderConfig("deepseek", { ...cfg, keys: nextKeys });
60
- console.log(`✅ 已写入 deepseek(现 ${nextKeys.length} 个账号)· 来源: ${label}`);
61
- }
62
-
63
- console.log("=".repeat(60));
64
- console.log("\n下一步:");
65
- console.log(" mslxdff -provider deepseek allowAny on 放行模型(默认 allowlist 空=全 blocked)");
66
- console.log(" mslxdff -provider deepseek models 查看支持的模型");
67
- console.log(" mslxdff -restart 重启网关生效");
68
- console.log("\n用法:model 字段填 deepseek/deepseek-chat 或 deepseek/deepseek-reasoner");
69
- console.log(" (-search 后缀开启联网搜索:deepseek/deepseek-chat-search)");
70
- console.log("\n多账号:重复 login 追加,自动轮换 + 401/429/风控切号");
71
- console.log("注意:单账号同时仅 1 路输出;触发验证码时换号或稍后再试");
72
- process.exit(0);
73
- }
74
-
75
- function printUsage() {
76
- console.log("DeepSeek 登录(两种方式任选):");
77
- console.log("");
78
- console.log(" 方式 1(推荐,免密码):浏览器贴 token");
79
- console.log(" 1. 浏览器登录 https://chat.deepseek.com");
80
- console.log(" 2. F12 → Application → Local Storage → https://chat.deepseek.com");
81
- console.log(" 3. 找到 userToken,复制值");
82
- console.log(" 4. mslxdff -provider deepseek login --token <userToken>");
83
- console.log("");
84
- console.log(" 方式 2:账号密码(邮箱或手机号)");
85
- console.log(" mslxdff -provider deepseek login you@example.com yourpassword");
86
- console.log(" mslxdff -provider deepseek login 13800138000 yourpassword");
87
- console.log("");
88
- console.log(" 登录后:mslxdff -provider deepseek allowAny on && mslxdff -restart");
89
- }
@@ -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
- }