Skip to content

Commit

Permalink
处理官方最新的插冷鸡方案
Browse files Browse the repository at this point in the history
  • Loading branch information
Vinlic committed Dec 23, 2024
1 parent 1648f3b commit d88025c
Show file tree
Hide file tree
Showing 6 changed files with 1,723 additions and 64 deletions.
Binary file added 35144cac553a7a2a.wasm
Binary file not shown.
31 changes: 0 additions & 31 deletions challenge-worker.js

This file was deleted.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "deepseek-free-api",
"version": "0.0.10",
"version": "0.0.13",
"description": "DeepSeek Free API Server",
"type": "module",
"main": "dist/index.js",
Expand Down Expand Up @@ -43,7 +43,7 @@
"yaml": "^2.3.4"
},
"devDependencies": {
"@types/lodash": "^4.14.202",
"@types/lodash": "^4.17.13",
"@types/mime": "^3.0.4",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
Expand Down
65 changes: 34 additions & 31 deletions src/api/controllers/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import path from 'path';
import APIException from "@/lib/exceptions/APIException.ts";
import EX from "@/api/consts/exceptions.ts";
import { createParser } from "eventsource-parser";
import { DeepSeekHash } from "@/lib/challenge.ts";
import logger from "@/lib/logger.ts";
import util from "@/lib/util.ts";

// 模型名称
const MODEL_NAME = "deepseek-chat";
// 插冷鸡WASM文件路径
const WASM_PATH = './35144cac553a7a2a.wasm';
// access_token有效期
const ACCESS_TOKEN_EXPIRES = 3600;
// 最大重试次数
Expand Down Expand Up @@ -199,30 +202,18 @@ async function createSession(model: string, refreshToken: string): Promise<strin
* 相当于把计算量放在浏览器侧的话,用户分摊了这个计算量
* 但是如果逆向在服务器上算,那这个成本都在服务器集中,并发一高就GG
*/
function answerChallenge(response: any): Promise<any> {
return new Promise((resolve, reject) => {
const worker = getWorker();
if (!worker) {
reject(new Error('No available workers'));
return;
}

worker.once('message', (result) => {
releaseWorker(worker);
if (result.error) {
reject(new Error(result.error));
} else {
resolve(result);
}
});

worker.once('error', (error) => {
releaseWorker(worker);
reject(error);
});

worker.postMessage(response);
});
async function answerChallenge(response: any): Promise<any> {
const { algorithm, challenge, salt, difficulty, expire_at, signature } = response;
const deepSeekHash = new DeepSeekHash();
await deepSeekHash.init(WASM_PATH);
const answer = deepSeekHash.calculateHash(algorithm, challenge, salt, difficulty, expire_at);
return {
algorithm,
challenge,
salt,
answer,
signature
}
}

/**
Expand All @@ -236,7 +227,7 @@ async function getChallengeResponse(refreshToken: string) {
headers: {
Authorization: `Bearer ${token}`,
...FAKE_HEADERS,
Cookie: generateCookie()
Cookie: generateCookie()
},
timeout: 15000,
validateStatus: () => true,
Expand Down Expand Up @@ -281,13 +272,19 @@ async function createCompletion(

const isSearchModel = model.includes('search') || prompt.includes('联网搜索');
const isThinkingModel = model.includes('think') || model.includes('r1') || prompt.includes('深度思考');

let challenge;

if(isSearchModel && isThinkingModel)
throw new APIException(EX.API_REQUEST_FAILED, '深度思考和联网搜索不能同时使用');

if (isThinkingModel) {
const thinkingQuota = await getThinkingQuota(refreshToken);
if (thinkingQuota <= 0) {
throw new APIException(EX.API_REQUEST_FAILED, '深度思考配额不足');
}
}

let challenge;
if (isSearchModel || isThinkingModel) {
const challengeResponse = await getChallengeResponse(refreshToken);
challenge = await answerChallenge(challengeResponse);
logger.info(`插冷鸡: ${JSON.stringify(challenge)}`);
Expand Down Expand Up @@ -387,12 +384,18 @@ async function createCompletionStream(
const isSearchModel = model.includes('search') || prompt.includes('联网搜索');
const isThinkingModel = model.includes('think') || model.includes('r1') || prompt.includes('深度思考');

let challenge;
if(isSearchModel && isThinkingModel)
throw new APIException(EX.API_REQUEST_FAILED, '深度思考和联网搜索不能同时使用');

if (isThinkingModel) {
const thinkingQuota = await getThinkingQuota(refreshToken);
if (thinkingQuota <= 0) {
throw new APIException(EX.API_REQUEST_FAILED, '深度思考配额不足');
}
}

let challenge;
if (isSearchModel || isThinkingModel) {
const challengeResponse = await getChallengeResponse(refreshToken);
challenge = await answerChallenge(challengeResponse);
logger.info(`插冷鸡: ${JSON.stringify(challenge)}`);
Expand Down Expand Up @@ -584,7 +587,7 @@ async function receiveStream(model: string, stream: any, refConvId?: string): Pr
return;
if (!data.id)
data.id = `${refConvId}@${result.message_id}`;
if(result.choices[0].delta.type === "search_result" && !isSilentModel) {
if (result.choices[0].delta.type === "search_result" && !isSilentModel) {
const searchResults = result.choices[0]?.delta?.search_results || [];
refContent += searchResults.map(item => `${item.title} - ${item.url}`).join('\n');
return;
Expand All @@ -601,7 +604,7 @@ async function receiveStream(model: string, stream: any, refConvId?: string): Pr
thinking = false;
data.choices[0].message.content += isFoldModel ? "</pre></details>" : "[思考结束]";
}
if(result.choices[0].delta.content)
if (result.choices[0].delta.content)
data.choices[0].message.content += result.choices[0].delta.content;
if (result.choices && result.choices[0] && result.choices[0].finish_reason === "stop") {
data.choices[0].message.content = data.choices[0].message.content.replace(/^\n+/, '').replace(/\[citation:\d+\]/g, '') + (refContent ? `\n\n搜索结果来自:\n${refContent}` : '');
Expand Down Expand Up @@ -721,7 +724,7 @@ function createTransStream(model: string, stream: any, refConvId: string, endCal
})}\n\n`);
}

if(!result.choices[0].delta.content)
if (!result.choices[0].delta.content)
return;

transStream.write(`data: ${JSON.stringify({
Expand Down
135 changes: 135 additions & 0 deletions src/lib/challenge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import fs from 'fs-extra';

export class DeepSeekHash {
private wasmInstance: any;
private offset: number = 0;
private cachedUint8Memory: Uint8Array | null = null;
private cachedTextEncoder: TextEncoder = new TextEncoder();

// 编码字符串到 WASM 内存
private encodeString(
text: string,
allocate: (size: number, align: number) => number,
reallocate?: (ptr: number, oldSize: number, newSize: number, align: number) => number
): number {
// 简单情况:当没有 reallocate 函数时,直接编码整个字符串
if (!reallocate) {
const encoded = this.cachedTextEncoder.encode(text);
const ptr = allocate(encoded.length, 1) >>> 0;
const memory = this.getCachedUint8Memory();
memory.subarray(ptr, ptr + encoded.length).set(encoded);
this.offset = encoded.length;
return ptr;
}

// 复杂情况:分两步处理 ASCII 和非 ASCII 字符
const strLength = text.length;
let ptr = allocate(strLength, 1) >>> 0;
const memory = this.getCachedUint8Memory();
let asciiLength = 0;

// 首先尝试 ASCII 编码
for (; asciiLength < strLength; asciiLength++) {
const charCode = text.charCodeAt(asciiLength);
if (charCode > 127) break;
memory[ptr + asciiLength] = charCode;
}

// 如果存在非 ASCII 字符,需要重新分配空间并处理
if (asciiLength !== strLength) {
if (asciiLength > 0) {
text = text.slice(asciiLength);
}

// 为非 ASCII 字符重新分配空间(每个字符最多需要 3 字节)
ptr = reallocate(ptr, strLength, asciiLength + text.length * 3, 1) >>> 0;

// 使用 encodeInto 处理剩余的非 ASCII 字符
const result = this.cachedTextEncoder.encodeInto(
text,
this.getCachedUint8Memory().subarray(ptr + asciiLength, ptr + asciiLength + text.length * 3)
);
asciiLength += result.written;

// 最终调整内存大小
ptr = reallocate(ptr, asciiLength + text.length * 3, asciiLength, 1) >>> 0;
}

this.offset = asciiLength;
return ptr;
}

// 获取 WASM 内存视图
private getCachedUint8Memory(): Uint8Array {
if (this.cachedUint8Memory === null || this.cachedUint8Memory.byteLength === 0) {
this.cachedUint8Memory = new Uint8Array(this.wasmInstance.memory.buffer);
}
return this.cachedUint8Memory;
}

// DeepSeekHash 计算函数
public calculateHash(
algorithm: string,
challenge: string,
salt: string,
difficulty: number,
expireAt: number
): number | undefined {
if (algorithm !== 'DeepSeekHashV1') {
throw new Error('Unsupported algorithm: ' + algorithm);
}

// 拼接前缀
const prefix = `${salt}_${expireAt}_`;

try {
// 分配栈空间
const retptr = this.wasmInstance.__wbindgen_add_to_stack_pointer(-16);

// 获取编码后的指针和长度
const ptr0 = this.encodeString(
challenge,
this.wasmInstance.__wbindgen_export_0,
this.wasmInstance.__wbindgen_export_1
);
const len0 = this.offset;

const ptr1 = this.encodeString(
prefix,
this.wasmInstance.__wbindgen_export_0,
this.wasmInstance.__wbindgen_export_1
);
const len1 = this.offset;

// 传入正确的长度参数
this.wasmInstance.wasm_solve(retptr, ptr0, len0, ptr1, len1, difficulty);

// 获取返回结果
const dataView = new DataView(this.wasmInstance.memory.buffer);
const status = dataView.getInt32(retptr + 0, true);
const value = dataView.getFloat64(retptr + 8, true);

// 如果求解失败,返回 undefined
if (status === 0)
return undefined;

return value;

} finally {
// 释放栈空间
this.wasmInstance.__wbindgen_add_to_stack_pointer(16);
}
}

// 初始化 WASM 模块
public async init(wasmPath: string): Promise<any> {
const imports = { wbg: {} };
const wasmBuffer = await fs.readFile(wasmPath);
const { instance } = await WebAssembly.instantiate(wasmBuffer, imports);
this.wasmInstance = instance.exports;
return this.wasmInstance;
}
}

// 导出类
export default DeepSeekHash;
Loading

0 comments on commit d88025c

Please sign in to comment.