mslxdff 0.1.97 → 0.1.98
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,220 @@
|
|
|
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
|
+
const token = authPool.requireToken();
|
|
187
|
+
try {
|
|
188
|
+
const out = prompt.length > threshold
|
|
189
|
+
? await runChunked({ token, flags, prompt, isStream, fetchImpl, dispatcher, baseUrl, connectTimeoutMs, threshold })
|
|
190
|
+
: await runOnce({ token, flags, prompt, isStream, fetchImpl, dispatcher, baseUrl, connectTimeoutMs });
|
|
191
|
+
if (out.kind === "stream") {
|
|
192
|
+
return {
|
|
193
|
+
kind: "stream",
|
|
194
|
+
res: out.res,
|
|
195
|
+
token,
|
|
196
|
+
cleanup: () => deleteChatSession({ token: out.token, sessionId: out.sessionId, fetchImpl, dispatcher, baseUrl }),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const sseText = await readBodyText(out.res);
|
|
200
|
+
let data;
|
|
201
|
+
try {
|
|
202
|
+
data = aggregateFromText(sseText, model, { thinking: flags.thinking });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
await deleteChatSession({ token: out.token, sessionId: out.sessionId, fetchImpl, dispatcher, baseUrl });
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
dsDump("aggregate", `sseText model=${model}`, sseText, 4000);
|
|
208
|
+
if (!data.choices[0].message.content && !data.choices[0].message.reasoning_content) {
|
|
209
|
+
dsDump("aggregate", "EMPTY aggregate content! full sseText", sseText, 8000);
|
|
210
|
+
}
|
|
211
|
+
await deleteChatSession({ token: out.token, sessionId: out.sessionId, fetchImpl, dispatcher, baseUrl });
|
|
212
|
+
return { kind: "json", data };
|
|
213
|
+
} catch (err) {
|
|
214
|
+
dsError(`chat attempt=${attempt}`, err);
|
|
215
|
+
if (err?._rotateAuth) authPool.onError(token, { cooldownMs: err._cooldownMs });
|
|
216
|
+
if (!err?._rotateAuth || attempt >= maxAttempts - 1) throw err;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
throw upstreamError("DeepSeek: 所有账号均不可用", {});
|
|
220
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// 可读 FIPS 202 参考实现(BigInt):24 轮 = 标准 SHA3-256,仅测试对拍用。
|
|
2
|
+
// 源自 diegosouzapw/OmniRoute(MIT)clean-room NIST FIPS 202 port。
|
|
3
|
+
// 运行时求解用 hash.js 的 Uint32 优化版(keccakP1600Uint32)。
|
|
4
|
+
|
|
5
|
+
const LANE_MASK = (1n << 64n) - 1n;
|
|
6
|
+
const SHA3_256_RATE_BYTES = 136;
|
|
7
|
+
const SHA3_DOMAIN_SUFFIX = 0x06;
|
|
8
|
+
const SHA3_256_OUTPUT_BYTES = 32;
|
|
9
|
+
|
|
10
|
+
const ROTATION_OFFSETS = [
|
|
11
|
+
0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const ROUND_CONSTANTS = [
|
|
15
|
+
0x0000000000000001n,
|
|
16
|
+
0x0000000000008082n,
|
|
17
|
+
0x800000000000808an,
|
|
18
|
+
0x8000000080008000n,
|
|
19
|
+
0x000000000000808bn,
|
|
20
|
+
0x0000000080000001n,
|
|
21
|
+
0x8000000080008081n,
|
|
22
|
+
0x8000000000008009n,
|
|
23
|
+
0x000000000000008an,
|
|
24
|
+
0x0000000000000088n,
|
|
25
|
+
0x0000000080008009n,
|
|
26
|
+
0x000000008000000an,
|
|
27
|
+
0x000000008000808bn,
|
|
28
|
+
0x800000000000008bn,
|
|
29
|
+
0x8000000000008089n,
|
|
30
|
+
0x8000000000008003n,
|
|
31
|
+
0x8000000000008002n,
|
|
32
|
+
0x8000000000000080n,
|
|
33
|
+
0x000000000000800an,
|
|
34
|
+
0x800000008000000an,
|
|
35
|
+
0x8000000080008081n,
|
|
36
|
+
0x8000000000008080n,
|
|
37
|
+
0x0000000080000001n,
|
|
38
|
+
0x8000000080008008n,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function rotateLeft64(value, amount) {
|
|
42
|
+
if (amount === 0) return value;
|
|
43
|
+
const shift = BigInt(amount);
|
|
44
|
+
return ((value << shift) | (value >> (64n - shift))) & LANE_MASK;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function keccakP1600Reference(state, roundCount) {
|
|
48
|
+
const columnParity = new Array(5).fill(0n);
|
|
49
|
+
const thetaMix = new Array(5).fill(0n);
|
|
50
|
+
const rhoPiState = new Array(25).fill(0n);
|
|
51
|
+
const firstRound = ROUND_CONSTANTS.length - roundCount;
|
|
52
|
+
|
|
53
|
+
for (let round = firstRound; round < ROUND_CONSTANTS.length; round++) {
|
|
54
|
+
for (let x = 0; x < 5; x++) {
|
|
55
|
+
columnParity[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20];
|
|
56
|
+
}
|
|
57
|
+
for (let x = 0; x < 5; x++) {
|
|
58
|
+
thetaMix[x] = columnParity[(x + 4) % 5] ^ rotateLeft64(columnParity[(x + 1) % 5], 1);
|
|
59
|
+
}
|
|
60
|
+
for (let y = 0; y < 5; y++) {
|
|
61
|
+
for (let x = 0; x < 5; x++) state[x + 5 * y] ^= thetaMix[x];
|
|
62
|
+
}
|
|
63
|
+
for (let y = 0; y < 5; y++) {
|
|
64
|
+
for (let x = 0; x < 5; x++) {
|
|
65
|
+
const destinationX = y;
|
|
66
|
+
const destinationY = (2 * x + 3 * y) % 5;
|
|
67
|
+
const lane = x + 5 * y;
|
|
68
|
+
rhoPiState[destinationX + 5 * destinationY] = rotateLeft64(state[lane], ROTATION_OFFSETS[lane]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (let y = 0; y < 5; y++) {
|
|
72
|
+
const row = 5 * y;
|
|
73
|
+
for (let x = 0; x < 5; x++) {
|
|
74
|
+
state[x + row] = rhoPiState[x + row] ^ (~rhoPiState[((x + 1) % 5) + row] & LANE_MASK & rhoPiState[((x + 2) % 5) + row]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
state[0] ^= ROUND_CONSTANTS[round];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function absorbReferenceBlock(state, block, roundCount) {
|
|
82
|
+
for (let index = 0; index < SHA3_256_RATE_BYTES; index++) {
|
|
83
|
+
const lane = Math.floor(index / 8);
|
|
84
|
+
const shift = BigInt((index % 8) * 8);
|
|
85
|
+
state[lane] ^= BigInt(block[index]) << shift;
|
|
86
|
+
}
|
|
87
|
+
keccakP1600Reference(state, roundCount);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sha3_256ReferenceWithRoundCount(input, roundCount) {
|
|
91
|
+
const bytes = new TextEncoder().encode(input);
|
|
92
|
+
const state = new Array(25).fill(0n);
|
|
93
|
+
let offset = 0;
|
|
94
|
+
|
|
95
|
+
while (offset + SHA3_256_RATE_BYTES <= bytes.length) {
|
|
96
|
+
absorbReferenceBlock(state, bytes.subarray(offset, offset + SHA3_256_RATE_BYTES), roundCount);
|
|
97
|
+
offset += SHA3_256_RATE_BYTES;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const finalBlock = new Uint8Array(SHA3_256_RATE_BYTES);
|
|
101
|
+
finalBlock.set(bytes.subarray(offset));
|
|
102
|
+
finalBlock[bytes.length - offset] ^= SHA3_DOMAIN_SUFFIX;
|
|
103
|
+
finalBlock[SHA3_256_RATE_BYTES - 1] ^= 0x80;
|
|
104
|
+
absorbReferenceBlock(state, finalBlock, roundCount);
|
|
105
|
+
|
|
106
|
+
const output = new Uint8Array(SHA3_256_OUTPUT_BYTES);
|
|
107
|
+
for (let index = 0; index < output.length; index++) {
|
|
108
|
+
const lane = Math.floor(index / 8);
|
|
109
|
+
const shift = BigInt((index % 8) * 8);
|
|
110
|
+
output[index] = Number((state[lane] >> shift) & 0xffn);
|
|
111
|
+
}
|
|
112
|
+
return Array.from(output, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function sha3_256Fips202Reference(input) {
|
|
116
|
+
return sha3_256ReferenceWithRoundCount(input, 24);
|
|
117
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// DeepSeekHashV1 + KECCAK-p[1600] sponge primitives (Uint32 optimized).
|
|
2
|
+
// Clean-room port of diegosouzapw/OmniRoute (MIT) open-sse/lib/deepseek-pow-hash.js,
|
|
3
|
+
// itself derived from NIST FIPS 202: DeepSeekHashV1 is SHA3-256 with the final
|
|
4
|
+
// Keccak round dropped (KECCAK-p[1600, 23]), verified by differential testing
|
|
5
|
+
// against the official sha3_wasm_bg.wasm black box.
|
|
6
|
+
// 可读 BigInt 参考实现拆在 hash-reference.js(仅测试对拍用)。
|
|
7
|
+
|
|
8
|
+
const LANE_MASK = (1n << 64n) - 1n;
|
|
9
|
+
const SHA3_256_RATE_BYTES = 136;
|
|
10
|
+
const SHA3_DOMAIN_SUFFIX = 0x06;
|
|
11
|
+
const SHA3_256_OUTPUT_BYTES = 32;
|
|
12
|
+
|
|
13
|
+
const ROTATION_OFFSETS = [
|
|
14
|
+
0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const ROUND_CONSTANTS = [
|
|
18
|
+
0x0000000000000001n,
|
|
19
|
+
0x0000000000008082n,
|
|
20
|
+
0x800000000000808an,
|
|
21
|
+
0x8000000080008000n,
|
|
22
|
+
0x000000000000808bn,
|
|
23
|
+
0x0000000080000001n,
|
|
24
|
+
0x8000000080008081n,
|
|
25
|
+
0x8000000000008009n,
|
|
26
|
+
0x000000000000008an,
|
|
27
|
+
0x0000000000000088n,
|
|
28
|
+
0x0000000080008009n,
|
|
29
|
+
0x000000008000000an,
|
|
30
|
+
0x000000008000808bn,
|
|
31
|
+
0x800000000000008bn,
|
|
32
|
+
0x8000000000008089n,
|
|
33
|
+
0x8000000000008003n,
|
|
34
|
+
0x8000000000008002n,
|
|
35
|
+
0x8000000000000080n,
|
|
36
|
+
0x000000000000800an,
|
|
37
|
+
0x800000008000000an,
|
|
38
|
+
0x8000000080008081n,
|
|
39
|
+
0x8000000000008080n,
|
|
40
|
+
0x0000000080000001n,
|
|
41
|
+
0x8000000080008008n,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export { sha3_256Fips202Reference } from "./hash-reference.js";
|
|
45
|
+
|
|
46
|
+
const DEEPSEEK_HASH_ROUNDS = 23;
|
|
47
|
+
const DIGEST_HEX_PATTERN = /^[a-f0-9]{64}$/i;
|
|
48
|
+
const ROUND_CONSTANTS_LOW = Uint32Array.from(ROUND_CONSTANTS, (value) => Number(value & 0xffffffffn));
|
|
49
|
+
const ROUND_CONSTANTS_HIGH = Uint32Array.from(ROUND_CONSTANTS, (value) => Number((value >> 32n) & 0xffffffffn));
|
|
50
|
+
const RHO_PI_DESTINATION_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => {
|
|
51
|
+
const x = lane % 5;
|
|
52
|
+
const y = Math.floor(lane / 5);
|
|
53
|
+
return 2 * (y + 5 * ((2 * x + 3 * y) % 5));
|
|
54
|
+
});
|
|
55
|
+
const CHI_NEXT_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => {
|
|
56
|
+
const x = lane % 5;
|
|
57
|
+
const row = lane - x;
|
|
58
|
+
return 2 * (row + ((x + 1) % 5));
|
|
59
|
+
});
|
|
60
|
+
const CHI_NEXT_NEXT_WORDS = Uint8Array.from({ length: 25 }, (_, lane) => {
|
|
61
|
+
const x = lane % 5;
|
|
62
|
+
const row = lane - x;
|
|
63
|
+
return 2 * (row + ((x + 2) % 5));
|
|
64
|
+
});
|
|
65
|
+
const HEX_DIGITS = "0123456789abcdef";
|
|
66
|
+
|
|
67
|
+
export const MAX_POW_DIFFICULTY = 250_000;
|
|
68
|
+
|
|
69
|
+
function keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount) {
|
|
70
|
+
const firstRound = ROUND_CONSTANTS.length - roundCount;
|
|
71
|
+
|
|
72
|
+
for (let round = firstRound; round < ROUND_CONSTANTS.length; round++) {
|
|
73
|
+
for (let x = 0; x < 5; x++) {
|
|
74
|
+
const word = 2 * x;
|
|
75
|
+
columnParity[word] = state[word] ^ state[word + 10] ^ state[word + 20] ^ state[word + 30] ^ state[word + 40];
|
|
76
|
+
columnParity[word + 1] = state[word + 1] ^ state[word + 11] ^ state[word + 21] ^ state[word + 31] ^ state[word + 41];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (let x = 0; x < 5; x++) {
|
|
80
|
+
const previous = 2 * ((x + 4) % 5);
|
|
81
|
+
const next = 2 * ((x + 1) % 5);
|
|
82
|
+
const rotatedLow = (columnParity[next] << 1) | (columnParity[next + 1] >>> 31);
|
|
83
|
+
const rotatedHigh = (columnParity[next + 1] << 1) | (columnParity[next] >>> 31);
|
|
84
|
+
thetaMix[2 * x] = columnParity[previous] ^ rotatedLow;
|
|
85
|
+
thetaMix[2 * x + 1] = columnParity[previous + 1] ^ rotatedHigh;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (let x = 0; x < 5; x++) {
|
|
89
|
+
const word = 2 * x;
|
|
90
|
+
const low = thetaMix[word];
|
|
91
|
+
const high = thetaMix[word + 1];
|
|
92
|
+
state[word] ^= low;
|
|
93
|
+
state[word + 1] ^= high;
|
|
94
|
+
state[word + 10] ^= low;
|
|
95
|
+
state[word + 11] ^= high;
|
|
96
|
+
state[word + 20] ^= low;
|
|
97
|
+
state[word + 21] ^= high;
|
|
98
|
+
state[word + 30] ^= low;
|
|
99
|
+
state[word + 31] ^= high;
|
|
100
|
+
state[word + 40] ^= low;
|
|
101
|
+
state[word + 41] ^= high;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
rhoPiState[0] = state[0];
|
|
105
|
+
rhoPiState[1] = state[1];
|
|
106
|
+
for (let lane = 1; lane < 25; lane++) {
|
|
107
|
+
const source = 2 * lane;
|
|
108
|
+
const destination = RHO_PI_DESTINATION_WORDS[lane];
|
|
109
|
+
const amount = ROTATION_OFFSETS[lane];
|
|
110
|
+
const low = state[source];
|
|
111
|
+
const high = state[source + 1];
|
|
112
|
+
|
|
113
|
+
if (amount < 32) {
|
|
114
|
+
rhoPiState[destination] = (low << amount) | (high >>> (32 - amount));
|
|
115
|
+
rhoPiState[destination + 1] = (high << amount) | (low >>> (32 - amount));
|
|
116
|
+
} else {
|
|
117
|
+
const reduced = amount - 32;
|
|
118
|
+
rhoPiState[destination] = (high << reduced) | (low >>> (32 - reduced));
|
|
119
|
+
rhoPiState[destination + 1] = (low << reduced) | (high >>> (32 - reduced));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (let lane = 0; lane < 25; lane++) {
|
|
124
|
+
const word = 2 * lane;
|
|
125
|
+
const nextWord = CHI_NEXT_WORDS[lane];
|
|
126
|
+
const nextNextWord = CHI_NEXT_NEXT_WORDS[lane];
|
|
127
|
+
state[word] = rhoPiState[word] ^ (~rhoPiState[nextWord] & rhoPiState[nextNextWord]);
|
|
128
|
+
state[word + 1] = rhoPiState[word + 1] ^ (~rhoPiState[nextWord + 1] & rhoPiState[nextNextWord + 1]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
state[0] ^= ROUND_CONSTANTS_LOW[round];
|
|
132
|
+
state[1] ^= ROUND_CONSTANTS_HIGH[round];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function absorbFullUint32Block(state, bytes, offset, rhoPiState, columnParity, thetaMix, roundCount) {
|
|
137
|
+
for (let index = 0; index < SHA3_256_RATE_BYTES; index++) {
|
|
138
|
+
const word = index >>> 2;
|
|
139
|
+
state[word] ^= bytes[offset + index] << ((index & 3) * 8);
|
|
140
|
+
}
|
|
141
|
+
keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function digestStateToHex(state) {
|
|
145
|
+
let digest = "";
|
|
146
|
+
for (let index = 0; index < SHA3_256_OUTPUT_BYTES; index++) {
|
|
147
|
+
const byte = (state[index >>> 2] >>> ((index & 3) * 8)) & 0xff;
|
|
148
|
+
digest += HEX_DIGITS[byte >>> 4] + HEX_DIGITS[byte & 0x0f];
|
|
149
|
+
}
|
|
150
|
+
return digest;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sha3_256Uint32WithRoundCount(input, roundCount) {
|
|
154
|
+
const bytes = new TextEncoder().encode(input);
|
|
155
|
+
const state = new Uint32Array(50);
|
|
156
|
+
const rhoPiState = new Uint32Array(50);
|
|
157
|
+
const columnParity = new Uint32Array(10);
|
|
158
|
+
const thetaMix = new Uint32Array(10);
|
|
159
|
+
let offset = 0;
|
|
160
|
+
|
|
161
|
+
while (offset + SHA3_256_RATE_BYTES <= bytes.length) {
|
|
162
|
+
absorbFullUint32Block(state, bytes, offset, rhoPiState, columnParity, thetaMix, roundCount);
|
|
163
|
+
offset += SHA3_256_RATE_BYTES;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const remaining = bytes.length - offset;
|
|
167
|
+
for (let index = 0; index < remaining; index++) {
|
|
168
|
+
state[index >>> 2] ^= bytes[offset + index] << ((index & 3) * 8);
|
|
169
|
+
}
|
|
170
|
+
state[remaining >>> 2] ^= SHA3_DOMAIN_SUFFIX << ((remaining & 3) * 8);
|
|
171
|
+
state[(SHA3_256_RATE_BYTES - 1) >>> 2] ^= 0x80 << 24;
|
|
172
|
+
keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, roundCount);
|
|
173
|
+
return digestStateToHex(state);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function deepSeekHashV1(input) {
|
|
177
|
+
return sha3_256Uint32WithRoundCount(input, DEEPSEEK_HASH_ROUNDS);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseDigestWords(digestHex) {
|
|
181
|
+
const words = new Uint32Array(SHA3_256_OUTPUT_BYTES / 4);
|
|
182
|
+
for (let index = 0; index < SHA3_256_OUTPUT_BYTES; index++) {
|
|
183
|
+
const byte = Number.parseInt(digestHex.slice(index * 2, index * 2 + 2), 16);
|
|
184
|
+
words[index >>> 2] |= byte << ((index & 3) * 8);
|
|
185
|
+
}
|
|
186
|
+
return words;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function findPowNonce(prefix, challenge, difficulty) {
|
|
190
|
+
if (typeof prefix !== "string") throw new TypeError("DeepSeek PoW prefix must be a string");
|
|
191
|
+
if (!DIGEST_HEX_PATTERN.test(challenge)) {
|
|
192
|
+
throw new TypeError("DeepSeek PoW challenge must be a 64-character hex digest");
|
|
193
|
+
}
|
|
194
|
+
if (!Number.isSafeInteger(difficulty) || difficulty < 1 || difficulty > MAX_POW_DIFFICULTY) {
|
|
195
|
+
throw new RangeError(`DeepSeek PoW difficulty must be an integer from 1 to ${MAX_POW_DIFFICULTY}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const prefixBytes = new TextEncoder().encode(prefix);
|
|
199
|
+
const baseState = new Uint32Array(50);
|
|
200
|
+
const rhoPiState = new Uint32Array(50);
|
|
201
|
+
const columnParity = new Uint32Array(10);
|
|
202
|
+
const thetaMix = new Uint32Array(10);
|
|
203
|
+
let prefixOffset = 0;
|
|
204
|
+
|
|
205
|
+
while (prefixOffset + SHA3_256_RATE_BYTES <= prefixBytes.length) {
|
|
206
|
+
absorbFullUint32Block(baseState, prefixBytes, prefixOffset, rhoPiState, columnParity, thetaMix, DEEPSEEK_HASH_ROUNDS);
|
|
207
|
+
prefixOffset += SHA3_256_RATE_BYTES;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const tailLength = prefixBytes.length - prefixOffset;
|
|
211
|
+
const tailWords = new Uint32Array(Math.ceil(tailLength / 4));
|
|
212
|
+
for (let index = 0; index < tailLength; index++) {
|
|
213
|
+
tailWords[index >>> 2] ^= prefixBytes[prefixOffset + index] << ((index & 3) * 8);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const targetWords = parseDigestWords(challenge.toLowerCase());
|
|
217
|
+
const state = new Uint32Array(50);
|
|
218
|
+
|
|
219
|
+
nonceLoop: for (let nonce = 0; nonce < difficulty; nonce++) {
|
|
220
|
+
state.set(baseState);
|
|
221
|
+
for (let word = 0; word < tailWords.length; word++) state[word] ^= tailWords[word];
|
|
222
|
+
|
|
223
|
+
let position = tailLength;
|
|
224
|
+
const nonceText = String(nonce);
|
|
225
|
+
for (let index = 0; index < nonceText.length; index++) {
|
|
226
|
+
state[position >>> 2] ^= nonceText.charCodeAt(index) << ((position & 3) * 8);
|
|
227
|
+
position += 1;
|
|
228
|
+
if (position === SHA3_256_RATE_BYTES) {
|
|
229
|
+
keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, DEEPSEEK_HASH_ROUNDS);
|
|
230
|
+
position = 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
state[position >>> 2] ^= SHA3_DOMAIN_SUFFIX << ((position & 3) * 8);
|
|
235
|
+
state[(SHA3_256_RATE_BYTES - 1) >>> 2] ^= 0x80 << 24;
|
|
236
|
+
keccakP1600Uint32(state, rhoPiState, columnParity, thetaMix, DEEPSEEK_HASH_ROUNDS);
|
|
237
|
+
|
|
238
|
+
for (let word = 0; word < targetWords.length; word++) {
|
|
239
|
+
if (state[word] !== targetWords[word]) continue nonceLoop;
|
|
240
|
+
}
|
|
241
|
+
return nonce;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return -1;
|
|
245
|
+
}
|