Spaces:
Sleeping
Sleeping
| import { PassThrough } from "stream"; | |
| import _ from "lodash"; | |
| import axios, { AxiosResponse } from "axios"; | |
| 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 = './sha3_wasm_bg.7b9ca65ddd.wasm'; | |
| // access_token有效期 | |
| const ACCESS_TOKEN_EXPIRES = 3600; | |
| // 最大重试次数 | |
| const MAX_RETRY_COUNT = 3; | |
| // 重试延迟 | |
| const RETRY_DELAY = 5000; | |
| // 伪装headers | |
| const FAKE_HEADERS = { | |
| Accept: "*/*", | |
| "Accept-Encoding": "gzip, deflate, br, zstd", | |
| "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", | |
| Origin: "https://chat.deepseek.com", | |
| Pragma: "no-cache", | |
| Priority: "u=1, i", | |
| Referer: "https://chat.deepseek.com/", | |
| "Sec-Ch-Ua": | |
| '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', | |
| "Sec-Ch-Ua-Mobile": "?0", | |
| "Sec-Ch-Ua-Platform": '"Windows"', | |
| "Sec-Fetch-Dest": "empty", | |
| "Sec-Fetch-Mode": "cors", | |
| "Sec-Fetch-Site": "same-origin", | |
| "User-Agent": | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36", | |
| "X-App-Version": "20241129.1" | |
| }; | |
| const EVENT_COMMIT_ID = '41e9c7b1'; | |
| // 当前IP地址 | |
| let ipAddress = ''; | |
| // access_token映射 | |
| const accessTokenMap = new Map(); | |
| // access_token请求队列映射 | |
| const accessTokenRequestQueueMap: Record<string, Function[]> = {}; | |
| async function getIPAddress() { | |
| if (ipAddress) return ipAddress; | |
| const result = await axios.get('https://chat.deepseek.com/', { | |
| headers: { | |
| ...FAKE_HEADERS, | |
| Cookie: generateCookie() | |
| }, | |
| timeout: 15000, | |
| validateStatus: () => true, | |
| }); | |
| const ip = result.data.match(/<meta name="ip" content="([\d.]+)">/)?.[1]; | |
| if (!ip) throw new APIException(EX.API_REQUEST_FAILED, '获取IP地址失败'); | |
| logger.info(`当前IP地址: ${ip}`); | |
| ipAddress = ip; | |
| return ip; | |
| } | |
| /** | |
| * 请求access_token | |
| * | |
| * 使用refresh_token去刷新获得access_token | |
| * | |
| * @param refreshToken 用于刷新access_token的refresh_token | |
| */ | |
| async function requestToken(refreshToken: string) { | |
| if (accessTokenRequestQueueMap[refreshToken]) | |
| return new Promise((resolve) => | |
| accessTokenRequestQueueMap[refreshToken].push(resolve) | |
| ); | |
| accessTokenRequestQueueMap[refreshToken] = []; | |
| logger.info(`Refresh token: ${refreshToken}`); | |
| const result = await (async () => { | |
| const result = await axios.get( | |
| "https://chat.deepseek.com/api/v0/users/current", | |
| { | |
| headers: { | |
| Authorization: `Bearer ${refreshToken}`, | |
| ...FAKE_HEADERS, | |
| }, | |
| timeout: 15000, | |
| validateStatus: () => true, | |
| } | |
| ); | |
| const { token } = checkResult(result, refreshToken); | |
| return { | |
| accessToken: token, | |
| refreshToken: token, | |
| refreshTime: util.unixTimestamp() + ACCESS_TOKEN_EXPIRES, | |
| }; | |
| })() | |
| .then((result) => { | |
| if (accessTokenRequestQueueMap[refreshToken]) { | |
| accessTokenRequestQueueMap[refreshToken].forEach((resolve) => | |
| resolve(result) | |
| ); | |
| delete accessTokenRequestQueueMap[refreshToken]; | |
| } | |
| logger.success(`Refresh successful`); | |
| return result; | |
| }) | |
| .catch((err) => { | |
| if (accessTokenRequestQueueMap[refreshToken]) { | |
| accessTokenRequestQueueMap[refreshToken].forEach((resolve) => | |
| resolve(err) | |
| ); | |
| delete accessTokenRequestQueueMap[refreshToken]; | |
| } | |
| return err; | |
| }); | |
| if (_.isError(result)) throw result; | |
| return result; | |
| } | |
| /** | |
| * 获取缓存中的access_token | |
| * | |
| * 避免短时间大量刷新token,未加锁,如果有并发要求还需加锁 | |
| * | |
| * @param refreshToken 用于刷新access_token的refresh_token | |
| */ | |
| async function acquireToken(refreshToken: string): Promise<string> { | |
| let result = accessTokenMap.get(refreshToken); | |
| if (!result) { | |
| result = await requestToken(refreshToken); | |
| accessTokenMap.set(refreshToken, result); | |
| } | |
| if (util.unixTimestamp() > result.refreshTime) { | |
| result = await requestToken(refreshToken); | |
| accessTokenMap.set(refreshToken, result); | |
| } | |
| return result.accessToken; | |
| } | |
| /** | |
| * 生成cookie | |
| */ | |
| function generateCookie() { | |
| return `intercom-HWWAFSESTIME=${util.timestamp()}; HWWAFSESID=${util.generateRandomString({ | |
| charset: 'hex', | |
| length: 18 | |
| })}; Hm_lvt_${util.uuid(false)}=${util.unixTimestamp()},${util.unixTimestamp()},${util.unixTimestamp()}; Hm_lpvt_${util.uuid(false)}=${util.unixTimestamp()}; _frid=${util.uuid(false)}; _fr_ssid=${util.uuid(false)}; _fr_pvid=${util.uuid(false)}` | |
| } | |
| async function createSession(model: string, refreshToken: string): Promise<string> { | |
| const token = await acquireToken(refreshToken); | |
| const result = await axios.post( | |
| "https://chat.deepseek.com/api/v0/chat_session/create", | |
| { | |
| agent: "chat", | |
| }, | |
| { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| ...FAKE_HEADERS, | |
| }, | |
| timeout: 15000, | |
| validateStatus: () => true, | |
| } | |
| ); | |
| const { biz_data } = checkResult(result, refreshToken); | |
| if (!biz_data) | |
| throw new APIException(EX.API_REQUEST_FAILED, "创建会话失败,可能是账号或IP地址被封禁"); | |
| return biz_data.id; | |
| } | |
| /** | |
| * 碰撞challenge答案 | |
| * | |
| * 厂商这个反逆向的策略不错哦 | |
| * 相当于把计算量放在浏览器侧的话,用户分摊了这个计算量 | |
| * 但是如果逆向在服务器上算,那这个成本都在服务器集中,并发一高就GG | |
| */ | |
| async function answerChallenge(response: any, targetPath: string): 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 Buffer.from(JSON.stringify({ | |
| algorithm, | |
| challenge, | |
| salt, | |
| answer, | |
| signature, | |
| target_path: targetPath | |
| })).toString('base64'); | |
| } | |
| /** | |
| * 获取challenge响应 | |
| * | |
| * @param refreshToken 用于刷新access_token的refresh_token | |
| */ | |
| async function getChallengeResponse(refreshToken: string, targetPath: string) { | |
| const token = await acquireToken(refreshToken); | |
| const result = await axios.post('https://chat.deepseek.com/api/v0/chat/create_pow_challenge', { | |
| target_path: targetPath | |
| }, { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| ...FAKE_HEADERS, | |
| Cookie: generateCookie() | |
| }, | |
| timeout: 15000, | |
| validateStatus: () => true, | |
| }); | |
| const { biz_data: { challenge } } = checkResult(result, refreshToken); | |
| return challenge; | |
| } | |
| /** | |
| * 同步对话补全 | |
| * | |
| * @param model 模型名称 | |
| * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 | |
| * @param refreshToken 用于刷新access_token的refresh_token | |
| * @param refConvId 引用对话ID | |
| * @param retryCount 重试次数 | |
| */ | |
| async function createCompletion( | |
| model = MODEL_NAME, | |
| messages: any[], | |
| refreshToken: string, | |
| refConvId?: string, | |
| retryCount = 0 | |
| ) { | |
| return (async () => { | |
| logger.info(messages); | |
| // 如果引用对话ID不正确则重置引用 | |
| if (!/[0-9a-z\-]{36}@[0-9]+/.test(refConvId)) | |
| refConvId = null; | |
| // 消息预处理 | |
| const prompt = messagesPrepare(messages); | |
| // 解析引用对话ID | |
| const [refSessionId, refParentMsgId] = refConvId?.split('@') || []; | |
| // 创建会话 | |
| const sessionId = refSessionId || await createSession(model, refreshToken); | |
| // 请求流 | |
| const token = await acquireToken(refreshToken); | |
| const isSearchModel = model.includes('search') || prompt.includes('联网搜索'); | |
| const isThinkingModel = model.includes('think') || model.includes('r1') || prompt.includes('深度思考'); | |
| 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, '深度思考配额不足'); | |
| } | |
| } | |
| const challengeResponse = await getChallengeResponse(refreshToken, '/api/v0/chat/completion'); | |
| const challenge = await answerChallenge(challengeResponse, '/api/v0/chat/completion'); | |
| logger.info(`插冷鸡: ${challenge}`); | |
| const result = await axios.post( | |
| "https://chat.deepseek.com/api/v0/chat/completion", | |
| { | |
| chat_session_id: sessionId, | |
| parent_message_id: refParentMsgId || null, | |
| prompt, | |
| ref_file_ids: [], | |
| search_enabled: isSearchModel, | |
| thinking_enabled: isThinkingModel | |
| }, | |
| { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| ...FAKE_HEADERS, | |
| Cookie: generateCookie(), | |
| 'X-Ds-Pow-Response': challenge | |
| }, | |
| // 120秒超时 | |
| timeout: 120000, | |
| validateStatus: () => true, | |
| responseType: "stream", | |
| } | |
| ); | |
| // 发送事件,缓解被封号风险 | |
| await sendEvents(sessionId, refreshToken); | |
| if (result.headers["content-type"].indexOf("text/event-stream") == -1) { | |
| result.data.on("data", buffer => logger.error(buffer.toString())); | |
| throw new APIException( | |
| EX.API_REQUEST_FAILED, | |
| `Stream response Content-Type invalid: ${result.headers["content-type"]}` | |
| ); | |
| } | |
| const streamStartTime = util.timestamp(); | |
| // 接收流为输出文本 | |
| const answer = await receiveStream(model, result.data, sessionId); | |
| logger.success( | |
| `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` | |
| ); | |
| return answer; | |
| })().catch((err) => { | |
| if (retryCount < MAX_RETRY_COUNT) { | |
| logger.error(`Stream response error: ${err.stack}`); | |
| logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); | |
| return (async () => { | |
| await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); | |
| return createCompletion( | |
| model, | |
| messages, | |
| refreshToken, | |
| refConvId, | |
| retryCount + 1 | |
| ); | |
| })(); | |
| } | |
| throw err; | |
| }); | |
| } | |
| /** | |
| * 流式对话补全 | |
| * | |
| * @param model 模型名称 | |
| * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 | |
| * @param refreshToken 用于刷新access_token的refresh_token | |
| * @param refConvId 引用对话ID | |
| * @param retryCount 重试次数 | |
| */ | |
| async function createCompletionStream( | |
| model = MODEL_NAME, | |
| messages: any[], | |
| refreshToken: string, | |
| refConvId?: string, | |
| retryCount = 0 | |
| ) { | |
| return (async () => { | |
| logger.info(messages); | |
| // 如果引用对话ID不正确则重置引用 | |
| if (!/[0-9a-z\-]{36}@[0-9]+/.test(refConvId)) | |
| refConvId = null; | |
| // 消息预处理 | |
| const prompt = messagesPrepare(messages); | |
| // 解析引用对话ID | |
| const [refSessionId, refParentMsgId] = refConvId?.split('@') || []; | |
| const isSearchModel = model.includes('search') || prompt.includes('联网搜索'); | |
| const isThinkingModel = model.includes('think') || model.includes('r1') || prompt.includes('深度思考'); | |
| 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, '深度思考配额不足'); | |
| } | |
| } | |
| const challengeResponse = await getChallengeResponse(refreshToken, '/api/v0/chat/completion'); | |
| const challenge = await answerChallenge(challengeResponse, '/api/v0/chat/completion'); | |
| logger.info(`插冷鸡: ${challenge}`); | |
| // 创建会话 | |
| const sessionId = refSessionId || await createSession(model, refreshToken); | |
| // 请求流 | |
| const token = await acquireToken(refreshToken); | |
| const result = await axios.post( | |
| "https://chat.deepseek.com/api/v0/chat/completion", | |
| { | |
| chat_session_id: sessionId, | |
| parent_message_id: refParentMsgId || null, | |
| prompt, | |
| ref_file_ids: [], | |
| search_enabled: isSearchModel, | |
| thinking_enabled: isThinkingModel | |
| }, | |
| { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| ...FAKE_HEADERS, | |
| Cookie: generateCookie(), | |
| 'X-Ds-Pow-Response': challenge | |
| }, | |
| // 120秒超时 | |
| timeout: 120000, | |
| validateStatus: () => true, | |
| responseType: "stream", | |
| } | |
| ); | |
| // 发送事件,缓解被封号风险 | |
| await sendEvents(sessionId, refreshToken); | |
| if (result.headers["content-type"].indexOf("text/event-stream") == -1) { | |
| logger.error( | |
| `Invalid response Content-Type:`, | |
| result.headers["content-type"] | |
| ); | |
| result.data.on("data", buffer => logger.error(buffer.toString())); | |
| const transStream = new PassThrough(); | |
| transStream.end( | |
| `data: ${JSON.stringify({ | |
| id: "", | |
| model: MODEL_NAME, | |
| object: "chat.completion.chunk", | |
| choices: [ | |
| { | |
| index: 0, | |
| delta: { | |
| role: "assistant", | |
| content: "服务暂时不可用,第三方响应错误", | |
| }, | |
| finish_reason: "stop", | |
| }, | |
| ], | |
| usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, | |
| created: util.unixTimestamp(), | |
| })}\n\n` | |
| ); | |
| return transStream; | |
| } | |
| const streamStartTime = util.timestamp(); | |
| // 创建转换流将消息格式转换为gpt兼容格式 | |
| return createTransStream(model, result.data, sessionId, () => { | |
| logger.success( | |
| `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` | |
| ); | |
| }); | |
| })().catch((err) => { | |
| if (retryCount < MAX_RETRY_COUNT) { | |
| logger.error(`Stream response error: ${err.stack}`); | |
| logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); | |
| return (async () => { | |
| await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); | |
| return createCompletionStream( | |
| model, | |
| messages, | |
| refreshToken, | |
| refConvId, | |
| retryCount + 1 | |
| ); | |
| })(); | |
| } | |
| throw err; | |
| }); | |
| } | |
| /** | |
| * 消息预处理 | |
| * | |
| * 由于接口只取第一条消息,此处会将多条消息合并为一条,实现多轮对话效果 | |
| * | |
| * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 | |
| */ | |
| function messagesPrepare(messages: any[]) { | |
| let content; | |
| if (messages.length < 2) { | |
| content = messages.reduce((content, message) => { | |
| if (_.isArray(message.content)) { | |
| return ( | |
| message.content.reduce((_content, v) => { | |
| if (!_.isObject(v) || v["type"] != "text") return _content; | |
| return _content + (v["text"] || "") + "\n"; | |
| }, content) | |
| ); | |
| } | |
| return content + `${message.content}\n`; | |
| }, ""); | |
| logger.info("\n透传内容:\n" + content); | |
| } | |
| else { | |
| content = ( | |
| messages.reduce((content, message) => { | |
| if (_.isArray(message.content)) { | |
| return ( | |
| message.content.reduce((_content, v) => { | |
| if (!_.isObject(v) || v["type"] != "text") return _content; | |
| return _content + (`${message.role}:` + v["text"] || "") + "\n"; | |
| }, content) | |
| ); | |
| } | |
| return (content += `${message.role}:${message.content}\n`); | |
| }, "") + "assistant:" | |
| ) | |
| // 移除MD图像URL避免幻觉 | |
| .replace(/\!\[.+\]\(.+\)/g, ""); | |
| logger.info("\n对话合并:\n" + content); | |
| } | |
| return content; | |
| } | |
| /** | |
| * 检查请求结果 | |
| * | |
| * @param result 结果 | |
| * @param refreshToken 用于刷新access_token的refresh_token | |
| */ | |
| function checkResult(result: AxiosResponse, refreshToken: string) { | |
| if (!result.data) return null; | |
| const { code, data, msg } = result.data; | |
| if (!_.isFinite(code)) return result.data; | |
| if (code === 0) return data; | |
| if (code == 40003) accessTokenMap.delete(refreshToken); | |
| throw new APIException(EX.API_REQUEST_FAILED, `[请求deepseek失败]: ${msg}`); | |
| } | |
| /** | |
| * 从流接收完整的消息内容 | |
| * | |
| * @param model 模型名称 | |
| * @param stream 消息流 | |
| */ | |
| async function receiveStream(model: string, stream: any, refConvId?: string): Promise<any> { | |
| let thinking = false; | |
| const isSearchModel = model.includes('search'); | |
| const isThinkingModel = model.includes('think') || model.includes('r1'); | |
| const isSilentModel = model.includes('silent'); | |
| const isFoldModel = model.includes('fold'); | |
| logger.info(`模型: ${model}, 是否思考: ${isThinkingModel} 是否联网搜索: ${isSearchModel}, 是否静默思考: ${isSilentModel}, 是否折叠思考: ${isFoldModel}`); | |
| let refContent = ''; | |
| return new Promise((resolve, reject) => { | |
| // 消息初始化 | |
| const data = { | |
| id: "", | |
| model, | |
| object: "chat.completion", | |
| choices: [ | |
| { | |
| index: 0, | |
| message: { role: "assistant", content: "" }, | |
| finish_reason: "stop", | |
| }, | |
| ], | |
| usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, | |
| created: util.unixTimestamp(), | |
| }; | |
| const parser = createParser((event) => { | |
| try { | |
| if (event.type !== "event" || event.data.trim() == "[DONE]") return; | |
| // 解析JSON | |
| const result = _.attempt(() => JSON.parse(event.data)); | |
| if (_.isError(result)) | |
| throw new Error(`Stream response invalid: ${event.data}`); | |
| if (!result.choices || !result.choices[0] || !result.choices[0].delta) | |
| return; | |
| if (!data.id) | |
| data.id = `${refConvId}@${result.message_id}`; | |
| 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; | |
| } | |
| if (result.choices[0].delta.type === "thinking") { | |
| if (!thinking && isThinkingModel && !isSilentModel) { | |
| thinking = true; | |
| data.choices[0].message.content += isFoldModel ? "<details><summary>思考过程</summary><pre>" : "[思考开始]"; | |
| } | |
| if (isSilentModel) | |
| return; | |
| } | |
| else if (thinking && isThinkingModel && !isSilentModel) { | |
| thinking = false; | |
| data.choices[0].message.content += isFoldModel ? "</pre></details>" : "[思考结束]"; | |
| } | |
| 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}` : ''); | |
| resolve(data); | |
| } | |
| } catch (err) { | |
| logger.error(err); | |
| reject(err); | |
| } | |
| }); | |
| // 将流数据喂给SSE转换器 | |
| stream.on("data", (buffer) => parser.feed(buffer.toString())); | |
| stream.once("error", (err) => reject(err)); | |
| stream.once("close", () => resolve(data)); | |
| }); | |
| } | |
| /** | |
| * 创建转换流 | |
| * | |
| * 将流格式转换为gpt兼容流格式 | |
| * | |
| * @param model 模型名称 | |
| * @param stream 消息流 | |
| * @param endCallback 传输结束回调 | |
| */ | |
| function createTransStream(model: string, stream: any, refConvId: string, endCallback?: Function) { | |
| let thinking = false; | |
| const isSearchModel = model.includes('search'); | |
| const isThinkingModel = model.includes('think') || model.includes('r1'); | |
| const isSilentModel = model.includes('silent'); | |
| const isFoldModel = model.includes('fold'); | |
| logger.info(`模型: ${model}, 是否思考: ${isThinkingModel}, 是否联网搜索: ${isSearchModel}, 是否静默思考: ${isSilentModel}, 是否折叠思考: ${isFoldModel}`); | |
| // 消息创建时间 | |
| const created = util.unixTimestamp(); | |
| // 创建转换流 | |
| const transStream = new PassThrough(); | |
| !transStream.closed && | |
| transStream.write( | |
| `data: ${JSON.stringify({ | |
| id: "", | |
| model, | |
| object: "chat.completion.chunk", | |
| choices: [ | |
| { | |
| index: 0, | |
| delta: { role: "assistant", content: "" }, | |
| finish_reason: null, | |
| }, | |
| ], | |
| created, | |
| })}\n\n` | |
| ); | |
| const parser = createParser((event) => { | |
| try { | |
| if (event.type !== "event" || event.data.trim() == "[DONE]") return; | |
| // 解析JSON | |
| const result = _.attempt(() => JSON.parse(event.data)); | |
| if (_.isError(result)) | |
| throw new Error(`Stream response invalid: ${event.data}`); | |
| if (!result.choices || !result.choices[0] || !result.choices[0].delta) | |
| return; | |
| result.model = model; | |
| if (result.choices[0].delta.type === "search_result" && !isSilentModel) { | |
| const searchResults = result.choices[0]?.delta?.search_results || []; | |
| if (searchResults.length > 0) { | |
| const refContent = searchResults.map(item => `检索 ${item.title} - ${item.url}`).join('\n') + '\n\n'; | |
| transStream.write(`data: ${JSON.stringify({ | |
| id: `${refConvId}@${result.message_id}`, | |
| model: result.model, | |
| object: "chat.completion.chunk", | |
| choices: [ | |
| { | |
| index: 0, | |
| delta: { role: "assistant", content: refContent }, | |
| finish_reason: null, | |
| }, | |
| ], | |
| })}\n\n`); | |
| } | |
| return; | |
| } | |
| if (result.choices[0].delta.type === "thinking") { | |
| if (!thinking && isThinkingModel && !isSilentModel) { | |
| thinking = true; | |
| transStream.write(`data: ${JSON.stringify({ | |
| id: `${refConvId}@${result.message_id}`, | |
| model: result.model, | |
| object: "chat.completion.chunk", | |
| choices: [ | |
| { | |
| index: 0, | |
| delta: { role: "assistant", content: isFoldModel ? "<details><summary>思考过程</summary><pre>" : "[思考开始]" }, | |
| finish_reason: null, | |
| }, | |
| ], | |
| created, | |
| })}\n\n`); | |
| } | |
| if (isSilentModel) | |
| return; | |
| } | |
| else if (thinking && isThinkingModel && !isSilentModel) { | |
| thinking = false; | |
| transStream.write(`data: ${JSON.stringify({ | |
| id: `${refConvId}@${result.message_id}`, | |
| model: result.model, | |
| object: "chat.completion.chunk", | |
| choices: [ | |
| { | |
| index: 0, | |
| delta: { role: "assistant", content: isFoldModel ? "</pre></details>" : "[思考结束]" }, | |
| finish_reason: null, | |
| }, | |
| ], | |
| created, | |
| })}\n\n`); | |
| } | |
| if (!result.choices[0].delta.content) | |
| return; | |
| transStream.write(`data: ${JSON.stringify({ | |
| id: `${refConvId}@${result.message_id}`, | |
| model: result.model, | |
| object: "chat.completion.chunk", | |
| choices: [ | |
| { | |
| index: 0, | |
| delta: { role: "assistant", content: result.choices[0].delta.content.replace(/\[citation:\d+\]/g, '') }, | |
| finish_reason: null, | |
| }, | |
| ], | |
| created, | |
| })}\n\n`); | |
| if (result.choices && result.choices[0] && result.choices[0].finish_reason === "stop") { | |
| transStream.write(`data: ${JSON.stringify({ | |
| id: `${refConvId}@${result.message_id}`, | |
| model: result.model, | |
| object: "chat.completion.chunk", | |
| choices: [ | |
| { | |
| index: 0, | |
| delta: { role: "assistant", content: "" }, | |
| finish_reason: "stop" | |
| }, | |
| ], | |
| created, | |
| })}\n\n`); | |
| !transStream.closed && transStream.end("data: [DONE]\n\n"); | |
| endCallback && endCallback(); | |
| } | |
| } catch (err) { | |
| logger.error(err); | |
| !transStream.closed && transStream.end("data: [DONE]\n\n"); | |
| } | |
| }); | |
| // 将流数据喂给SSE转换器 | |
| stream.on("data", (buffer) => parser.feed(buffer.toString())); | |
| stream.once( | |
| "error", | |
| () => !transStream.closed && transStream.end("data: [DONE]\n\n") | |
| ); | |
| stream.once( | |
| "close", | |
| () => { | |
| !transStream.closed && transStream.end("data: [DONE]\n\n"); | |
| endCallback && endCallback(); | |
| } | |
| ); | |
| return transStream; | |
| } | |
| /** | |
| * Token切分 | |
| * | |
| * @param authorization 认证字符串 | |
| */ | |
| function tokenSplit(authorization: string) { | |
| return authorization.replace("Bearer ", "").split(","); | |
| } | |
| /** | |
| * 获取Token存活状态 | |
| */ | |
| async function getTokenLiveStatus(refreshToken: string) { | |
| const token = await acquireToken(refreshToken); | |
| const result = await axios.get( | |
| "https://chat.deepseek.com/api/v0/users/current", | |
| { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| ...FAKE_HEADERS, | |
| Cookie: generateCookie() | |
| }, | |
| timeout: 15000, | |
| validateStatus: () => true, | |
| } | |
| ); | |
| try { | |
| const { token } = checkResult(result, refreshToken); | |
| return !!token; | |
| } | |
| catch (err) { | |
| return false; | |
| } | |
| } | |
| async function sendEvents(refConvId: string, refreshToken: string) { | |
| try { | |
| const token = await acquireToken(refreshToken); | |
| const sessionId = `session_v0_${Math.random().toString(36).slice(2)}`; | |
| const timestamp = util.timestamp(); | |
| const fakeDuration1 = Math.floor(Math.random() * 1000); | |
| const fakeDuration2 = Math.floor(Math.random() * 1000); | |
| const fakeDuration3 = Math.floor(Math.random() * 1000); | |
| const ipAddress = await getIPAddress(); | |
| const response = await axios.post('https://chat.deepseek.com/api/v0/events', { | |
| "events": [ | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp, | |
| "event_name": "__reportEvent", | |
| "event_message": "调用上报事件接口", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "method": "post", | |
| "url": "/api/v0/events", | |
| "path": "/api/v0/events" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 100 + Math.floor(Math.random() * 1000), | |
| "event_name": "__reportEventOk", | |
| "event_message": "调用上报事件接口成功", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "method": "post", | |
| "url": "/api/v0/events", | |
| "path": "/api/v0/events", | |
| "logId": util.uuid(), | |
| "metricDuration": Math.floor(Math.random() * 1000), | |
| "status": "200" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 200 + Math.floor(Math.random() * 1000), | |
| "event_name": "createSessionAndStartCompletion", | |
| "event_message": "开始创建对话", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", | |
| "__referrer": "", | |
| "agentId": "chat", | |
| "thinkingEnabled": false | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 300 + Math.floor(Math.random() * 1000), | |
| "event_name": "__httpRequest", | |
| "event_message": "httpRequest POST /api/v0/chat_session/create", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", | |
| "__referrer": "", | |
| "url": "/api/v0/chat_session/create", | |
| "path": "/api/v0/chat_session/create", | |
| "method": "POST" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 400 + Math.floor(Math.random() * 1000), | |
| "event_name": "__httpResponse", | |
| "event_message": `httpResponse POST /api/v0/chat_session/create, ${Math.floor(Math.random() * 1000)}ms, reason: none`, | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "url": "/api/v0/chat_session/create", | |
| "path": "/api/v0/chat_session/create", | |
| "method": "POST", | |
| "metricDuration": Math.floor(Math.random() * 1000), | |
| "status": "200", | |
| "logId": util.uuid() | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 500 + Math.floor(Math.random() * 1000), | |
| "event_name": "__log", | |
| "event_message": "使用 buffer 模式", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 600 + Math.floor(Math.random() * 1000), | |
| "event_name": "chatCompletionApi", | |
| "event_message": "chatCompletionApi 被调用", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "scene": "completion", | |
| "chatSessionId": refConvId, | |
| "withFile": "false", | |
| "thinkingEnabled": "false" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 700 + Math.floor(Math.random() * 1000), | |
| "event_name": "__httpRequest", | |
| "event_message": "httpRequest POST /api/v0/chat/completion", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "url": "/api/v0/chat/completion", | |
| "path": "/api/v0/chat/completion", | |
| "method": "POST" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 800 + Math.floor(Math.random() * 1000), | |
| "event_name": "completionFirstChunkReceived", | |
| "event_message": "收到第一个 completion chunk(可以是空 chunk)", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "metricDuration": Math.floor(Math.random() * 1000), | |
| "logId": util.uuid() | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 900 + Math.floor(Math.random() * 1000), | |
| "event_name": "createSessionAndStartCompletion", | |
| "event_message": "创建会话并开始补全", | |
| "payload": { | |
| "__location": "https://chat.deepseek.com/", | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "agentId": "chat", | |
| "newSessionId": refConvId, | |
| "isCreateNewChat": "false", | |
| "thinkingEnabled": "false" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 1000 + Math.floor(Math.random() * 1000), | |
| "event_name": "routeChange", | |
| "event_message": `路由改变 => /a/chat/s/${refConvId}`, | |
| "payload": { | |
| "__location": `https://chat.deepseek.com/a/chat/s/${refConvId}`, | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "to": `/a/chat/s/${refConvId}`, | |
| "redirect": "false", | |
| "redirected": "false", | |
| "redirectReason": "", | |
| "redirectTo": "/", | |
| "hasToken": "true", | |
| "hasUserInfo": "true" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 1100 + Math.floor(Math.random() * 1000), | |
| "event_name": "__pageVisit", | |
| "event_message": `访问页面 [/a/chat/s/${refConvId}] [0]:${fakeDuration1}ms`, | |
| "payload": { | |
| "__location": `https://chat.deepseek.com/a/chat/s/${refConvId}`, | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "pathname": `/a/chat/s/${refConvId}`, | |
| "metricVisitIndex": 0, | |
| "metricDuration": fakeDuration1, | |
| "referrer": "none", | |
| "appTheme": "light" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 1200 + Math.floor(Math.random() * 1000), | |
| "event_name": "__tti", | |
| "event_message": `/a/chat/s/${refConvId} TTI 上报:${fakeDuration2}ms`, | |
| "payload": { | |
| "__location": `https://chat.deepseek.com/a/chat/s/${refConvId}`, | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "type": "warmStart", | |
| "referer": "", | |
| "metricDuration": fakeDuration2, | |
| "metricVisitIndex": 0, | |
| "metricDurationSinceMounted": 0, | |
| "hasError": "false" | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 1300 + Math.floor(Math.random() * 1000), | |
| "event_name": "__httpResponse", | |
| "event_message": `httpResponse POST /api/v0/chat/completion, ${fakeDuration3}ms, reason: none`, | |
| "payload": { | |
| "__location": `https://chat.deepseek.com/a/chat/s/${refConvId}`, | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "url": "/api/v0/chat/completion", | |
| "path": "/api/v0/chat/completion", | |
| "method": "POST", | |
| "metricDuration": fakeDuration3, | |
| "status": "200", | |
| "logId": util.uuid() | |
| }, | |
| "level": "info" | |
| }, | |
| { | |
| "session_id": sessionId, | |
| "client_timestamp_ms": timestamp + 1400 + Math.floor(Math.floor(Math.random() * 1000)), | |
| "event_name": "completionApiOk", | |
| "event_message": "完成响应,响应有正常的的 finish reason", | |
| "payload": { | |
| "__location": `https://chat.deepseek.com/a/chat/s/${refConvId}`, | |
| "__ip": ipAddress, | |
| "__region": "CN", | |
| "__pageVisibility": "true", | |
| "__nodeEnv": "production", | |
| "__deployEnv": "production", | |
| "__appVersion": FAKE_HEADERS["X-App-Version"], | |
| "__commitId": EVENT_COMMIT_ID, | |
| "__userAgent": FAKE_HEADERS["User-Agent"], | |
| "__referrer": "", | |
| "condition": "hasDone", | |
| "streamClosed": false, | |
| "scene": "completion", | |
| "chatSessionId": refConvId | |
| }, | |
| "level": "info" | |
| } | |
| ] | |
| }, { | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| ...FAKE_HEADERS, | |
| Referer: `https://chat.deepseek.com/a/chat/s/${refConvId}`, | |
| Cookie: generateCookie() | |
| }, | |
| validateStatus: () => true, | |
| }); | |
| checkResult(response, refreshToken); | |
| logger.info('发送事件成功'); | |
| } | |
| catch (err) { | |
| logger.error(err); | |
| } | |
| } | |
| /** | |
| * 获取深度思考配额 | |
| */ | |
| async function getThinkingQuota(refreshToken: string) { | |
| try { | |
| const response = await axios.get('https://chat.deepseek.com/api/v0/users/feature_quota', { | |
| headers: { | |
| Authorization: `Bearer ${refreshToken}`, | |
| ...FAKE_HEADERS, | |
| Cookie: generateCookie() | |
| }, | |
| timeout: 15000, | |
| validateStatus: () => true, | |
| }); | |
| const { biz_data } = checkResult(response, refreshToken); | |
| if (!biz_data) return 0; | |
| const { quota, used } = biz_data.thinking; | |
| if (!_.isFinite(quota) || !_.isFinite(used)) return 0; | |
| logger.info(`获取深度思考配额: ${quota}/${used}`); | |
| return quota - used; | |
| } | |
| catch (err) { | |
| logger.error('获取深度思考配额失败:', err); | |
| return 0; | |
| } | |
| } | |
| /** | |
| * 获取版本号 | |
| */ | |
| async function fetchAppVersion(): Promise<string> { | |
| try { | |
| logger.info('自动获取版本号'); | |
| const response = await axios.get('https://chat.deepseek.com/version.txt', { | |
| timeout: 5000, | |
| validateStatus: () => true, | |
| headers: { | |
| ...FAKE_HEADERS, | |
| Cookie: generateCookie() | |
| } | |
| }); | |
| if (response.status === 200 && response.data) { | |
| const version = response.data.toString().trim(); | |
| logger.info(`获取版本号: ${version}`); | |
| return version; | |
| } | |
| } catch (err) { | |
| logger.error('获取版本号失败:', err); | |
| } | |
| return "20241018.0"; | |
| } | |
| function autoUpdateAppVersion() { | |
| fetchAppVersion().then((version) => { | |
| FAKE_HEADERS["X-App-Version"] = version; | |
| }); | |
| } | |
| util.createCronJob('0 */10 * * * *', autoUpdateAppVersion).start(); | |
| getIPAddress().then(() => { | |
| autoUpdateAppVersion(); | |
| }).catch((err) => { | |
| logger.error('获取 IP 地址失败:', err); | |
| }); | |
| export default { | |
| createCompletion, | |
| createCompletionStream, | |
| getTokenLiveStatus, | |
| tokenSplit, | |
| fetchAppVersion, | |
| }; | |