mslxdff 0.1.97 → 0.1.99
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 +1 -1
- package/src/chat-pipeline/serial-trial.js +1 -1
- package/src/cli/commands/model/picks.js +7 -2
- package/src/cli/commands/provider/bench-via.js +9 -0
- package/src/cli/commands/provider/bench.js +8 -0
- package/src/cli/commands/provider/deepseek-health.js +46 -0
- package/src/cli/commands/provider/deepseek-login.js +89 -0
- package/src/cli/commands/provider/index.js +4 -0
- package/src/cli/commands/provider/models.js +3 -0
- package/src/providers/deepseek/auth.js +111 -0
- package/src/providers/deepseek/bridge.js +158 -0
- package/src/providers/deepseek/chat.js +220 -0
- package/src/providers/deepseek/debug.js +44 -0
- package/src/providers/deepseek/hash-reference.js +117 -0
- package/src/providers/deepseek/hash.js +245 -0
- package/src/providers/deepseek/health.js +104 -0
- package/src/providers/deepseek/index.js +180 -0
- package/src/providers/deepseek/pow.js +99 -0
- package/src/providers/deepseek/session.js +114 -0
- package/src/providers/deepseek/sse-decoder.js +160 -0
- package/src/providers/deepseek.js +1 -0
- package/src/providers/registry.js +5 -0
- package/src/routes/hedge.js +3 -1
- package/src/runtime/providers-setup.js +2 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// DeepSeek health 探活(防禁言体系):对池内每个账号发最小真实请求,检测 muted/限频/凭据坏
|
|
2
|
+
// 语义对齐 NIyueeE/ds-free-api health_check(biz_code 非 0 = 异常;FINISHED/INCOMPLETE = 正常)
|
|
3
|
+
import { androidHeaders, solveChallenge, DEEPSEEK_DEFAULT_BASE, DEEPSEEK_API_PREFIX } from "./pow.js";
|
|
4
|
+
import { buildUpstreamBody } from "./bridge.js";
|
|
5
|
+
import { createChatSession } from "./session.js";
|
|
6
|
+
import { compatFetch } from "../../compat.js";
|
|
7
|
+
import { dsDebug } from "./debug.js";
|
|
8
|
+
|
|
9
|
+
const MUTED_RE = /user is muted|account is muted|\bmuted\b|禁言/i;
|
|
10
|
+
const FREQ_RE = /消息发送过于频繁[\s,,、::]*请稍后重试/;
|
|
11
|
+
const HEALTH_PROMPT = "只回复Hello, world!";
|
|
12
|
+
|
|
13
|
+
function tail(token) {
|
|
14
|
+
return String(token).slice(-6);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extractBizMsg(text) {
|
|
18
|
+
try {
|
|
19
|
+
const j = JSON.parse(text);
|
|
20
|
+
return j?.data?.biz_msg || j?.msg || "";
|
|
21
|
+
} catch {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 单账号探活:challenge → session → completion(SSE 判定)
|
|
27
|
+
async function probeToken({ token, fetchImpl, dispatcher, baseUrl, connectTimeoutMs, onError }) {
|
|
28
|
+
try {
|
|
29
|
+
const pow = await solveChallenge({ token, fetchImpl, dispatcher, baseUrl, connectTimeoutMs });
|
|
30
|
+
let sessionId = "";
|
|
31
|
+
try {
|
|
32
|
+
sessionId = await createChatSession({ token, fetchImpl, dispatcher, baseUrl, connectTimeoutMs });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const msg = String(err?.message || err);
|
|
35
|
+
if (/401|403|未登录/i.test(msg)) {
|
|
36
|
+
onError(token, {});
|
|
37
|
+
return { ok: false, detail: `凭据被拒(创建会话失败: ${msg.slice(0, 80)})` };
|
|
38
|
+
}
|
|
39
|
+
onError(token, {});
|
|
40
|
+
return { ok: false, detail: `创建会话失败: ${msg.slice(0, 120)}` };
|
|
41
|
+
}
|
|
42
|
+
const body = buildUpstreamBody({ sessionId, prompt: HEALTH_PROMPT, thinking: false, search: false, expert: false });
|
|
43
|
+
const url = `${baseUrl}${DEEPSEEK_API_PREFIX}/chat/completion`;
|
|
44
|
+
const res = await fetchImpl(url, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: androidHeaders(token, { "x-ds-pow-response": pow.header }),
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
49
|
+
});
|
|
50
|
+
const text = await res.text();
|
|
51
|
+
dsDebug("health", { event: "probe", tokenTail: tail(token), status: res.status, len: text.length });
|
|
52
|
+
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const bizMsg = extractBizMsg(text);
|
|
55
|
+
onError(token, {});
|
|
56
|
+
if (MUTED_RE.test(text)) return { ok: false, detail: "禁言(muted):已冷却 5 分钟,解封后再次探活自动恢复" };
|
|
57
|
+
if (res.status === 429 || FREQ_RE.test(text)) return { ok: false, detail: `触发频率风控(${bizMsg || `http ${res.status}`})` };
|
|
58
|
+
if (res.status === 401 || res.status === 403) return { ok: false, detail: `凭据被拒 (http ${res.status})` };
|
|
59
|
+
return { ok: false, detail: `HTTP ${res.status}: ${text.slice(0, 100)}` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const contentType = res.headers.get("content-type") || "";
|
|
63
|
+
if (!contentType.includes("text/event-stream")) {
|
|
64
|
+
const bizMsg = extractBizMsg(text);
|
|
65
|
+
try {
|
|
66
|
+
const bizCode = JSON.parse(text)?.data?.biz_code;
|
|
67
|
+
if (bizCode != null && bizCode !== 0) {
|
|
68
|
+
onError(token, {});
|
|
69
|
+
if (MUTED_RE.test(text)) return { ok: false, detail: "禁言(muted):已冷却 5 分钟,解封后再次探活自动恢复" };
|
|
70
|
+
return { ok: false, detail: `异常(biz_code=${bizCode} ${bizMsg})` };
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
return { ok: false, detail: `非 SSE 响应: ${text.slice(0, 100)}` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (/"biz_code"\s*:\s*[^0]/.test(text)) {
|
|
77
|
+
const m = text.match(/"biz_msg"\s*:\s*"([^"]+)"/);
|
|
78
|
+
onError(token, {});
|
|
79
|
+
const msg = m?.[1] || "";
|
|
80
|
+
if (MUTED_RE.test(msg)) return { ok: false, detail: "禁言(muted):已冷却 5 分钟,解封后再次探活自动恢复" };
|
|
81
|
+
return { ok: false, detail: `异常(biz_msg=${msg})` };
|
|
82
|
+
}
|
|
83
|
+
if (!/"FINISHED"|"INCOMPLETE"|APPEND/.test(text)) {
|
|
84
|
+
return { ok: false, detail: "SSE 未正常结束(无 ready/response 事件)" };
|
|
85
|
+
}
|
|
86
|
+
return { ok: true, detail: "健康" };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
dsDebug("health", { event: "error", tokenTail: tail(token), err: String(err?.message || err) });
|
|
89
|
+
// 网络异常不冷却(不误伤可用账号)
|
|
90
|
+
return { ok: false, detail: `网络失败: ${String(err?.message || err).slice(0, 100)}` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 对池内全部账号(绕过冷却——探活本来就是要测禁言状态)逐个探活,返回报告
|
|
95
|
+
export async function deepseekHealth({ authPool, fetchImpl, dispatcher, baseUrl = DEEPSEEK_DEFAULT_BASE, connectTimeoutMs = 30_000 } = {}) {
|
|
96
|
+
const doFetch = fetchImpl || compatFetch;
|
|
97
|
+
const report = [];
|
|
98
|
+
for (const token of authPool.keys || []) {
|
|
99
|
+
const r = await probeToken({ token, fetchImpl: doFetch, dispatcher, baseUrl: String(baseUrl).replace(/\/+$/, ""), connectTimeoutMs, onError: (t, opts) => authPool.onError(t, opts) });
|
|
100
|
+
report.push({ tokenTail: tail(token), ...r });
|
|
101
|
+
dsDebug("health", { event: "result", tokenTail: tail(token), ok: r.ok, detail: r.detail });
|
|
102
|
+
}
|
|
103
|
+
return report;
|
|
104
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// DeepSeek 供应商工厂:把 chat.deepseek.com 免费 web/移动端对话包装成 OpenAI 兼容模型
|
|
2
|
+
// 模型:deepseek/chat、deepseek/reasoner、deepseek/chat-search、deepseek/reasoner-search
|
|
3
|
+
import { collectApiKeysGeneric, envInt, getUndici } from "../base.js";
|
|
4
|
+
import { loadProviderKeys } from "../../state.js";
|
|
5
|
+
import { createAuthPool } from "./auth.js";
|
|
6
|
+
import { runDeepseekChat } from "./chat.js";
|
|
7
|
+
import { createDeepseekSseParser } from "./bridge.js";
|
|
8
|
+
import { DEEPSEEK_DEFAULT_BASE } from "./pow.js";
|
|
9
|
+
import { dsDebug, dsDump } from "./debug.js";
|
|
10
|
+
|
|
11
|
+
const { UndiciFetch } = getUndici();
|
|
12
|
+
|
|
13
|
+
// 对外 id 带 -free 后缀(ADR-0002 免费过滤 + 客户端一眼识别免费);上游 flags 由 bridge.mapModelToFlags 按 includes 判定,后缀不影响
|
|
14
|
+
// expert = 官网「专家模式」(completion body model_type:"expert",TQZHR 映射 deepseek-{chat,reasoner}-expert)
|
|
15
|
+
export const DEEPSEEK_MODELS = [
|
|
16
|
+
{ id: "deepseek-chat-free", object: "model", owned_by: "deepseek" },
|
|
17
|
+
{ id: "deepseek-reasoner-free", object: "model", owned_by: "deepseek" },
|
|
18
|
+
{ id: "deepseek-chat-search-free", object: "model", owned_by: "deepseek" },
|
|
19
|
+
{ id: "deepseek-reasoner-search-free", object: "model", owned_by: "deepseek" },
|
|
20
|
+
{ id: "deepseek-chat-expert-free", object: "model", owned_by: "deepseek" },
|
|
21
|
+
{ id: "deepseek-reasoner-expert-free", object: "model", owned_by: "deepseek" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function createDeepseekProvider({
|
|
25
|
+
id = "deepseek",
|
|
26
|
+
baseUrl,
|
|
27
|
+
apiKeys,
|
|
28
|
+
apiKey,
|
|
29
|
+
connectTimeoutMs = envInt("MSLXDFF_DEEPSEEK_TIMEOUT_MS", 30_000),
|
|
30
|
+
cooldownMs = envInt("MSLXDFF_DEEPSEEK_COOLDOWN_MS", 30_000),
|
|
31
|
+
fetchImpl,
|
|
32
|
+
file,
|
|
33
|
+
} = {}) {
|
|
34
|
+
const resolvedBase = String(baseUrl || DEEPSEEK_DEFAULT_BASE).trim().replace(/\/+$/, "");
|
|
35
|
+
if (!fetchImpl) fetchImpl = UndiciFetch;
|
|
36
|
+
|
|
37
|
+
const keys = collectApiKeysGeneric(id, apiKeys, apiKey, (pid) => loadProviderKeys(pid, file ? { file } : {}));
|
|
38
|
+
const authPool = createAuthPool({ tokens: keys, cooldownMs });
|
|
39
|
+
|
|
40
|
+
let modelsCache = null;
|
|
41
|
+
async function listModels() {
|
|
42
|
+
if (modelsCache) return modelsCache;
|
|
43
|
+
modelsCache = DEEPSEEK_MODELS.map((m) => ({ ...m, id: `${id}/${m.id}` }));
|
|
44
|
+
return modelsCache;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function chat(body) {
|
|
48
|
+
const out = await runDeepseekChat({
|
|
49
|
+
body,
|
|
50
|
+
authPool,
|
|
51
|
+
fetchImpl,
|
|
52
|
+
baseUrl: resolvedBase,
|
|
53
|
+
connectTimeoutMs,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (out.kind === "json") {
|
|
57
|
+
return new Response(JSON.stringify(out.data), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 流式:把上游 DeepSeek SSE 翻译为 OpenAI chat.completion.chunk 流,读完后无痕删会话
|
|
61
|
+
return new Response(buildOpenAiSseStream({ upstream: out.res, model: body?.model || "deepseek/chat", cleanup: out.cleanup, startKind: startKind(body) }), {
|
|
62
|
+
status: 200,
|
|
63
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function chatWithKeys(body, keys) {
|
|
68
|
+
const tmpPool = createAuthPool({ tokens: keys || [], cooldownMs });
|
|
69
|
+
const out = await runDeepseekChat({ body, authPool: tmpPool, fetchImpl, baseUrl: resolvedBase, connectTimeoutMs });
|
|
70
|
+
if (out.kind === "json") {
|
|
71
|
+
return new Response(JSON.stringify(out.data), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
72
|
+
}
|
|
73
|
+
return new Response(buildOpenAiSseStream({ upstream: out.res, model: body?.model || "deepseek/chat", cleanup: out.cleanup, startKind: startKind(body) }), {
|
|
74
|
+
status: 200,
|
|
75
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function close() {}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
chat,
|
|
84
|
+
chatWithKeys,
|
|
85
|
+
listModels,
|
|
86
|
+
preheat: async () => ({ ok: true }),
|
|
87
|
+
close,
|
|
88
|
+
keyRing: { available: () => authPool.available(), size: authPool.size, cooldownMs },
|
|
89
|
+
_authPool: authPool,
|
|
90
|
+
baseUrl: resolvedBase,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function startKind(body) {
|
|
95
|
+
const id = String(body?.model || "");
|
|
96
|
+
return id.includes("reasoner") ? "reasoning" : "content";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildOpenAiSseStream({ upstream, model, cleanup, startKind: initialKind = "content" }) {
|
|
100
|
+
const parse = createDeepseekSseParser({ startKind: initialKind });
|
|
101
|
+
const completionId = `chatcmpl-deepseek-${Date.now()}`;
|
|
102
|
+
const created = Math.floor(Date.now() / 1000);
|
|
103
|
+
const encoder = new TextEncoder();
|
|
104
|
+
let first = true;
|
|
105
|
+
let finished = false;
|
|
106
|
+
const stat = { chunks: 0, bytes: 0, content: 0, reasoning: 0, finishEv: 0, t0: Date.now() };
|
|
107
|
+
|
|
108
|
+
function frame(delta, finishReason = null) {
|
|
109
|
+
return `data: ${JSON.stringify({
|
|
110
|
+
id: completionId,
|
|
111
|
+
object: "chat.completion.chunk",
|
|
112
|
+
created,
|
|
113
|
+
model,
|
|
114
|
+
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
|
115
|
+
})}\n\n`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const reader = upstream.body.getReader();
|
|
119
|
+
const decoder = new TextDecoder();
|
|
120
|
+
|
|
121
|
+
return new ReadableStream({
|
|
122
|
+
async pull(controller) {
|
|
123
|
+
try {
|
|
124
|
+
const { done, value } = await reader.read();
|
|
125
|
+
if (done) {
|
|
126
|
+
dsDebug("sse-stream", { event: "upstream-done", model, ...stat, elapsedMs: Date.now() - stat.t0, EMPTY_STREAM: stat.chunks === 0 || (stat.content === 0 && stat.reasoning === 0) });
|
|
127
|
+
if (!finished) {
|
|
128
|
+
finished = true;
|
|
129
|
+
controller.enqueue(encoder.encode(frame({}, "stop")));
|
|
130
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
131
|
+
}
|
|
132
|
+
if (cleanup) { try { await cleanup(); } catch {} cleanup = null; }
|
|
133
|
+
controller.close();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
stat.chunks++;
|
|
137
|
+
stat.bytes += value?.byteLength || 0;
|
|
138
|
+
const text = decoder.decode(value, { stream: true });
|
|
139
|
+
dsDump("sse-stream", `upstream chunk #${stat.chunks} model=${model}`, text, 3000);
|
|
140
|
+
const events = parse(text);
|
|
141
|
+
dsDebug("sse-stream", { event: "parsed", chunkNo: stat.chunks, events: events.length, kinds: events.map((e) => (e.finish ? `finish:${e.finish}` : e.reasoning ? "reasoning" : e.content ? "content" : JSON.stringify(e).slice(0, 60))).join("|") });
|
|
142
|
+
for (const ev of events) {
|
|
143
|
+
if (first) {
|
|
144
|
+
first = false;
|
|
145
|
+
controller.enqueue(encoder.encode(frame({ role: "assistant", content: "" })));
|
|
146
|
+
}
|
|
147
|
+
// 上游拒绝(event:hint input_exceeds_limit / rate_limit_reached 等):透传为 OpenAI error 事件,绝不静默
|
|
148
|
+
if (ev.error && !finished) {
|
|
149
|
+
finished = true;
|
|
150
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: { message: ev.error, type: "upstream_error", ...(ev.finishReason ? { finish_reason: ev.finishReason } : {}) } })}\n\n`));
|
|
151
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
if (ev.reasoning) { stat.reasoning += ev.reasoning.length; controller.enqueue(encoder.encode(frame({ reasoning_content: ev.reasoning }))); }
|
|
155
|
+
if (ev.content) { stat.content += ev.content.length; controller.enqueue(encoder.encode(frame({ content: ev.content }))); }
|
|
156
|
+
if (ev.finish && !finished) {
|
|
157
|
+
stat.finishEv++;
|
|
158
|
+
finished = true;
|
|
159
|
+
controller.enqueue(encoder.encode(frame({}, ev.finish)));
|
|
160
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
dsDebug("sse-stream", { event: "reader-error", model, ...stat, error: String(err?.message || err) });
|
|
165
|
+
if (!finished) {
|
|
166
|
+
finished = true;
|
|
167
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: { message: String(err?.message || err), type: "upstream_error" } })}\n\n`));
|
|
168
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
169
|
+
}
|
|
170
|
+
if (cleanup) { try { await cleanup(); } catch {} cleanup = null; }
|
|
171
|
+
controller.close();
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
async cancel() {
|
|
175
|
+
dsDebug("sse-stream", { event: "downstream-cancel", model, ...stat });
|
|
176
|
+
try { await reader.cancel(); } catch {}
|
|
177
|
+
if (cleanup) { try { await cleanup(); } catch {} cleanup = null; }
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// DeepSeek PoW 编排:challenge 获取 + 求解 + x-ds-pow-response header 编码
|
|
2
|
+
// 上游协议参考 iidamie/deepseek2api(GPL-3.0,协议事实)与 TQZHR/deepseek2api(MIT)
|
|
3
|
+
import { deepSeekHashV1, findPowNonce } from "./hash.js";
|
|
4
|
+
import { compatFetch } from "../../compat.js";
|
|
5
|
+
|
|
6
|
+
export const DEEPSEEK_DEFAULT_BASE = "https://chat.deepseek.com";
|
|
7
|
+
export const DEEPSEEK_API_PREFIX = "/api/v0";
|
|
8
|
+
export const COMPLETION_TARGET_PATH = "/api/v0/chat/completion";
|
|
9
|
+
|
|
10
|
+
export function androidHeaders(token, extra = {}) {
|
|
11
|
+
return {
|
|
12
|
+
"User-Agent": "DeepSeek/1.0.13 Android/35",
|
|
13
|
+
Accept: "application/json",
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
"x-client-platform": "android",
|
|
16
|
+
"x-client-version": "2.0.0",
|
|
17
|
+
"x-client-locale": "zh_CN",
|
|
18
|
+
"accept-charset": "UTF-8",
|
|
19
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
20
|
+
...extra,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function apiError(payload, status) {
|
|
25
|
+
const bizMsg = payload?.data?.biz_msg || payload?.msg || payload?.error || "";
|
|
26
|
+
const code = payload?.data?.biz_code ?? payload?.code ?? status;
|
|
27
|
+
const err = new Error(`DeepSeek PoW 挑战获取失败${bizMsg ? `: ${bizMsg}` : ""}${code ? ` (code=${code})` : ""}`);
|
|
28
|
+
err.status = status;
|
|
29
|
+
err.bizCode = code;
|
|
30
|
+
return err;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 网络层抖动重试(换新连接):TUN 断流后 keepAlive 坏 socket 会 fetch failed/aborted,
|
|
34
|
+
// 重试即建新 socket。只重试"拿到响应前"的失败,不重试业务错(4xx/5xx 有响应)。
|
|
35
|
+
const NET_ERR_PATTERN = /fetch failed|aborted|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|UND_ERR|socket hang up|network/i;
|
|
36
|
+
export async function netRetry(fn, { attempts = 2, delayMs = 400 } = {}) {
|
|
37
|
+
for (let i = 0; ; i++) {
|
|
38
|
+
try {
|
|
39
|
+
return await fn();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const msg = `${err?.message || err} ${err?.cause?.code || err?.cause?.message || ""}`;
|
|
42
|
+
if (i >= attempts || !NET_ERR_PATTERN.test(msg)) throw err;
|
|
43
|
+
await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function fetchPowChallenge({ token, fetchImpl, dispatcher, baseUrl = DEEPSEEK_DEFAULT_BASE, connectTimeoutMs = 30_000 }) {
|
|
49
|
+
const doFetch = fetchImpl || compatFetch;
|
|
50
|
+
const url = `${String(baseUrl).replace(/\/+$/, "")}${DEEPSEEK_API_PREFIX}/chat/create_pow_challenge`;
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timer = setTimeout(() => controller.abort(new Error("DeepSeek PoW challenge timed out")), connectTimeoutMs);
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await netRetry(() => doFetch(url, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: androidHeaders(token),
|
|
58
|
+
body: JSON.stringify({ target_path: COMPLETION_TARGET_PATH }),
|
|
59
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
}), { attempts: 2, delayMs: 400 });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw new Error(`DeepSeek PoW 挑战请求失败: ${String(err?.message || err)}`);
|
|
64
|
+
} finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let payload = null;
|
|
69
|
+
try { payload = await res.json(); } catch {}
|
|
70
|
+
if (!res.ok || payload?.data?.biz_code !== 0 || !payload?.data?.biz_data?.challenge) {
|
|
71
|
+
throw apiError(payload, res.status);
|
|
72
|
+
}
|
|
73
|
+
return payload.data.biz_data.challenge;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildPowHeader(challenge, answer) {
|
|
77
|
+
const payload = {
|
|
78
|
+
algorithm: challenge.algorithm,
|
|
79
|
+
challenge: challenge.challenge,
|
|
80
|
+
salt: challenge.salt,
|
|
81
|
+
answer,
|
|
82
|
+
signature: challenge.signature,
|
|
83
|
+
target_path: challenge.target_path || COMPLETION_TARGET_PATH,
|
|
84
|
+
};
|
|
85
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 单次全编排:取 challenge → 穷举 nonce → 编 header。
|
|
89
|
+
// 返回 { challenge, answer, header },供 completion 请求直接使用。
|
|
90
|
+
export async function solveChallenge({ token, fetchImpl, dispatcher, baseUrl, connectTimeoutMs } = {}) {
|
|
91
|
+
const challenge = await fetchPowChallenge({ token, fetchImpl, dispatcher, baseUrl, connectTimeoutMs });
|
|
92
|
+
const expireAt = challenge.expire_at ?? challenge.expireAt;
|
|
93
|
+
const prefix = `${challenge.salt}_${expireAt}_`;
|
|
94
|
+
const answer = findPowNonce(prefix, challenge.challenge, challenge.difficulty);
|
|
95
|
+
if (answer < 0) {
|
|
96
|
+
throw new Error(`DeepSeek PoW 求解失败: difficulty=${challenge.difficulty} 空间内无解(challenge 不匹配)`);
|
|
97
|
+
}
|
|
98
|
+
return { challenge, answer, header: buildPowHeader(challenge, answer) };
|
|
99
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// DeepSeek 会话:无痕模式(每次 completion 前建、后删)
|
|
2
|
+
import { androidHeaders, netRetry } from "./pow.js";
|
|
3
|
+
import { DEEPSEEK_API_PREFIX } from "./pow.js";
|
|
4
|
+
import { dsDebug, dsDump, dsError } from "./debug.js";
|
|
5
|
+
|
|
6
|
+
export async function createChatSession({ token, fetchImpl, dispatcher, baseUrl = "https://chat.deepseek.com", connectTimeoutMs = 30_000 } = {}) {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const timer = setTimeout(() => controller.abort(new Error("DeepSeek 会话创建超时")), connectTimeoutMs);
|
|
9
|
+
let res;
|
|
10
|
+
try {
|
|
11
|
+
res = await netRetry(() => fetchImpl(`${String(baseUrl).replace(/\/+$/, "")}${DEEPSEEK_API_PREFIX}/chat_session/create`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: androidHeaders(token),
|
|
14
|
+
body: JSON.stringify({ agent: "chat" }),
|
|
15
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
16
|
+
signal: controller.signal,
|
|
17
|
+
}), { attempts: 2, delayMs: 400 });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
dsError("session", err);
|
|
20
|
+
throw new Error(`DeepSeek 会话创建失败: ${String(err?.message || err)}`);
|
|
21
|
+
} finally {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let data = null;
|
|
26
|
+
try { data = await res.json(); } catch {}
|
|
27
|
+
dsDebug("session", { event: "create", status: res.status, bizCode: data?.data?.biz_code });
|
|
28
|
+
if (data?.data?.biz_code !== 0) dsDump("session", "create body", JSON.stringify(data), 800);
|
|
29
|
+
// 新版协议(x-client-version 2.0.0)id 在 biz_data.chat_session.id;旧版在 biz_data.id
|
|
30
|
+
const id = data?.data?.biz_data?.chat_session?.id ?? data?.data?.biz_data?.id;
|
|
31
|
+
if (!res.ok || data?.data?.biz_code !== 0 || !id) {
|
|
32
|
+
const msg = data?.data?.biz_msg || data?.msg || "响应缺少会话 id";
|
|
33
|
+
throw new Error(`DeepSeek 会话创建失败: ${msg}${res.ok ? "" : ` (http ${res.status})`}`);
|
|
34
|
+
}
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function deleteChatSession({ token, sessionId, fetchImpl, dispatcher, baseUrl = "https://chat.deepseek.com", connectTimeoutMs = 15_000 } = {}) {
|
|
39
|
+
// 收尾动作:任何失败都静默(不抛),避免污染主流程
|
|
40
|
+
try {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(new Error("timeout")), connectTimeoutMs);
|
|
43
|
+
try {
|
|
44
|
+
await fetchImpl(`${String(baseUrl).replace(/\/+$/, "")}${DEEPSEEK_API_PREFIX}/chat_session/delete`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: androidHeaders(token),
|
|
47
|
+
body: JSON.stringify({ chat_session_id: sessionId }),
|
|
48
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 中止生成(官方协议:stop_stream 不需要 PoW header;message_id 取 ready 的 response_message_id)
|
|
58
|
+
// ds-free-api raw-api-reference §6
|
|
59
|
+
export async function stopDeepseekStream({ token, sessionId, messageId, fetchImpl, dispatcher, baseUrl = "https://chat.deepseek.com", connectTimeoutMs = 15_000 } = {}) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => controller.abort(new Error("DeepSeek stop_stream 超时")), connectTimeoutMs);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetchImpl(`${String(baseUrl).replace(/\/+$/, "")}${DEEPSEEK_API_PREFIX}/chat/stop_stream`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: androidHeaders(token),
|
|
66
|
+
body: JSON.stringify({ chat_session_id: sessionId, message_id: messageId }),
|
|
67
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
dsDebug("session", { event: "stop_stream", sessionId, messageId, status: res.status });
|
|
71
|
+
if (!res.ok) dsDump("session", `stop_stream non-ok http=${res.status}`, await res.text().catch(() => ""), 400);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
dsError("session", err);
|
|
74
|
+
throw err;
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 从 SSE 流读 event:ready 的 response_message_id(跨块缓冲)
|
|
81
|
+
function parseReadyMessageId(buf) {
|
|
82
|
+
const idx = buf.indexOf("event: ready");
|
|
83
|
+
if (idx < 0) return null;
|
|
84
|
+
const m = buf.slice(idx).match(/event: ready\s*\ndata: (\{[^\n]*\})/);
|
|
85
|
+
if (!m) return null;
|
|
86
|
+
try {
|
|
87
|
+
const j = JSON.parse(m[1]);
|
|
88
|
+
return j?.response_message_id != null ? Number(j.response_message_id) : null;
|
|
89
|
+
} catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 分块喂养:等 ready 拿 message_id → 等 update_session(上游把消息落库的信号;真机实测
|
|
93
|
+
// ready 后立刻 stop,下一块 parent_message_id 会报 biz_code:26 invalid message id)
|
|
94
|
+
// → stop_stream 中止生成 → drain 上游到关。update_session 未出现(流已关)也继续,宽松兜底。
|
|
95
|
+
export async function feedChunkToSession({ res, token, sessionId, fetchImpl, dispatcher, baseUrl, connectTimeoutMs = 30_000 } = {}) {
|
|
96
|
+
const reader = res.body.getReader();
|
|
97
|
+
const decoder = new TextDecoder();
|
|
98
|
+
let buf = "";
|
|
99
|
+
let messageId = null;
|
|
100
|
+
for (;;) {
|
|
101
|
+
const { done, value } = await reader.read();
|
|
102
|
+
if (done) break;
|
|
103
|
+
buf += decoder.decode(value, { stream: true });
|
|
104
|
+
if (messageId == null) messageId = parseReadyMessageId(buf);
|
|
105
|
+
if (messageId != null && buf.includes("event: update_session")) break;
|
|
106
|
+
}
|
|
107
|
+
if (messageId == null) throw new Error("DeepSeek 分块喂养失败:ready 事件缺少 response_message_id");
|
|
108
|
+
await stopDeepseekStream({ token, sessionId, messageId, fetchImpl, dispatcher, baseUrl, connectTimeoutMs });
|
|
109
|
+
for (;;) {
|
|
110
|
+
const { done } = await reader.read();
|
|
111
|
+
if (done) break;
|
|
112
|
+
}
|
|
113
|
+
return messageId;
|
|
114
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// DeepSeek SSE JSON-Patch decoder(对齐 TQZHR/deepseek2api MIT src/utils/deepseek-sse.js 范式)
|
|
2
|
+
// 纯解码层:consume(payloadText) → deltas[{kind,text,snapshot?}],kind ∈ content|reasoning|finish
|
|
3
|
+
// Android 通道特有补充:startKind(expert 空 THINK fragment 无 type 字段时兜底)+
|
|
4
|
+
// elapsed_secs SET 边界(THINK→RESPONSE 静默切换,新 fragment 首块带路径但无 o)。
|
|
5
|
+
const FRAGMENT_KIND_BY_TYPE = Object.freeze({
|
|
6
|
+
ANSWER: "content",
|
|
7
|
+
THINKING: "reasoning",
|
|
8
|
+
THINK: "reasoning",
|
|
9
|
+
RESPONSE: "content",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function resolveFragmentKind(type) {
|
|
13
|
+
return FRAGMENT_KIND_BY_TYPE[String(type || "").toUpperCase()] ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizePatchPath(basePath, path) {
|
|
17
|
+
const b = String(basePath || "").replace(/^\/+|\/+$/g, "");
|
|
18
|
+
const p = String(path || "").replace(/^\/+|\/+$/g, "");
|
|
19
|
+
if (!p) return b;
|
|
20
|
+
if (p === "response" || p.startsWith("response/")) return p;
|
|
21
|
+
return b ? `${b}/${p}` : p;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isSnapshotOperation(op) {
|
|
25
|
+
const t = String(op || "").toUpperCase();
|
|
26
|
+
return t === "SET" || t === "REPLACE";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// TQZHR takeUnseenSuffix:快照是某类全量文本,取相对已累积的未见过后缀
|
|
30
|
+
function unseenSuffix(previous, next) {
|
|
31
|
+
if (!next) return "";
|
|
32
|
+
if (!previous) return next;
|
|
33
|
+
if (next.startsWith(previous)) return next.slice(previous.length);
|
|
34
|
+
if (previous.endsWith(next)) return "";
|
|
35
|
+
const max = Math.min(previous.length, next.length);
|
|
36
|
+
for (let len = max; len > 0; len--) {
|
|
37
|
+
if (previous.endsWith(next.slice(0, len))) return next.slice(len);
|
|
38
|
+
}
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createDeepseekDeltaParser({ startKind = "content" } = {}) {
|
|
43
|
+
const state = { currentKind: startKind, finished: false };
|
|
44
|
+
const acc = { content: "", reasoning: "" };
|
|
45
|
+
|
|
46
|
+
function push(kind, text, snapshot, deltas) {
|
|
47
|
+
if (typeof text !== "string" || !text) return;
|
|
48
|
+
const unseen = snapshot ? unseenSuffix(acc[kind], text) : text;
|
|
49
|
+
if (!unseen) return;
|
|
50
|
+
acc[kind] += unseen;
|
|
51
|
+
deltas.push({ kind, text: unseen });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function appendFragmentDeltas(fragments, { snapshot = false } = {}, deltas) {
|
|
55
|
+
if (!Array.isArray(fragments)) return;
|
|
56
|
+
const grouped = new Map();
|
|
57
|
+
for (const frag of fragments) {
|
|
58
|
+
const kind = resolveFragmentKind(frag?.type);
|
|
59
|
+
// type 明确才更新 currentKind(空 fragment {} 不改变判定,Android expert 真机形态)
|
|
60
|
+
if (kind) state.currentKind = kind;
|
|
61
|
+
const content = typeof frag?.content === "string" && frag.content ? frag.content : "";
|
|
62
|
+
if (content) grouped.set(state.currentKind, (grouped.get(state.currentKind) ?? "") + content);
|
|
63
|
+
}
|
|
64
|
+
for (const [kind, text] of grouped) push(kind, text, snapshot, deltas);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function appendResponseSnapshot(response, deltas) {
|
|
68
|
+
if (Array.isArray(response?.fragments)) {
|
|
69
|
+
appendFragmentDeltas(response.fragments, { snapshot: true }, deltas);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// 旧版字段(web 1.x 通道)
|
|
73
|
+
push("reasoning", response?.thinking_content, true, deltas);
|
|
74
|
+
push("content", response?.content, true, deltas);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function decodePatch(payload, deltas, basePath = "") {
|
|
78
|
+
if (!payload || typeof payload !== "object") return;
|
|
79
|
+
const path = normalizePatchPath(basePath, payload.p ?? "");
|
|
80
|
+
const value = payload.v;
|
|
81
|
+
|
|
82
|
+
// 流结束信号(TQZHR 放在 completion-stream 层,我们在 decoder 内处理)
|
|
83
|
+
if (value === "FINISHED" && (path === "response/status" || /\/status$/.test(path))) {
|
|
84
|
+
if (!state.finished) {
|
|
85
|
+
state.finished = true;
|
|
86
|
+
deltas.push({ kind: "finish" });
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const response = value?.response ?? (!path ? payload.response : null);
|
|
92
|
+
if (response && typeof response === "object") {
|
|
93
|
+
appendResponseSnapshot(response, deltas);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (payload.o === "BATCH" && Array.isArray(value)) {
|
|
98
|
+
for (const op of value) decodePatch(op, deltas, path);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (path === "response" && Array.isArray(value)) {
|
|
103
|
+
for (const op of value) decodePatch(op, deltas, "response");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (path === "response/fragments" && Array.isArray(value)) {
|
|
108
|
+
appendFragmentDeltas(value, { snapshot: payload.o !== "APPEND" }, deltas);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (/^response\/fragments\/-?\d+$/.test(path) && value && typeof value === "object") {
|
|
113
|
+
appendFragmentDeltas([value], { snapshot: isSnapshotOperation(payload.o) }, deltas);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (/^response\/fragments\/-?\d+\/type$/.test(path)) {
|
|
118
|
+
state.currentKind = resolveFragmentKind(value) ?? state.currentKind;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (path === "response/thinking_content" && typeof value === "string") {
|
|
123
|
+
push("reasoning", value, isSnapshotOperation(payload.o), deltas);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (path === "response/content" && typeof value === "string") {
|
|
128
|
+
push("content", value, isSnapshotOperation(payload.o), deltas);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (/^response\/fragments\/-?\d+\/content$/.test(path) && typeof value === "string") {
|
|
133
|
+
push(state.currentKind, value, isSnapshotOperation(payload.o), deltas);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 裸 v:沿用 currentKind(流式优化,无 p)
|
|
138
|
+
if (!("p" in payload) && typeof value === "string") {
|
|
139
|
+
push(state.currentKind, value, false, deltas);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// elapsed_secs SET:Android 通道 THINK→RESPONSE 静默边界(单轮 THINK→RESPONSE 固定交替)
|
|
144
|
+
if (/^response\/fragments\/-?\d+\/elapsed_secs$/.test(path) && payload.o === "SET") {
|
|
145
|
+
if (state.currentKind === "reasoning") state.currentKind = "content";
|
|
146
|
+
}
|
|
147
|
+
// 其余(accumulated_token_usage / quasi_status / search_status 等)忽略
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
consume(payloadText) {
|
|
152
|
+
const deltas = [];
|
|
153
|
+
let payload = null;
|
|
154
|
+
try { payload = JSON.parse(String(payloadText ?? "")); } catch { return deltas; }
|
|
155
|
+
decodePatch(payload, deltas);
|
|
156
|
+
return deltas;
|
|
157
|
+
},
|
|
158
|
+
_state: state,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createDeepseekProvider, DEEPSEEK_MODELS } from "./deepseek/index.js";
|
|
@@ -15,6 +15,11 @@ export const customProviders = [
|
|
|
15
15
|
match: (id, baseUrl) => id === "cline" || id === "clinebot" || String(baseUrl).includes("cline.bot"),
|
|
16
16
|
load: () => import("./cline.js").then((m) => m.createClineProvider),
|
|
17
17
|
},
|
|
18
|
+
{
|
|
19
|
+
id: "deepseek",
|
|
20
|
+
match: (id, baseUrl) => id === "deepseek" || String(baseUrl).includes("chat.deepseek.com"),
|
|
21
|
+
load: () => import("./deepseek.js").then((m) => m.createDeepseekProvider),
|
|
22
|
+
},
|
|
18
23
|
];
|
|
19
24
|
|
|
20
25
|
// 供 bench/probe 等需要定制化解析模型列表的场景
|