| import { PassThrough } from "stream"; |
| import _ from "lodash"; |
| import AsyncLock from "async-lock"; |
| 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 logger from "@/lib/logger.ts"; |
| import util from "@/lib/util.ts"; |
|
|
| |
| const MODEL_NAME = "deepseek-chat"; |
| |
| const ACCESS_TOKEN_EXPIRES = 3600; |
| |
| const MAX_RETRY_COUNT = 3; |
| |
| const RETRY_DELAY = 5000; |
| |
| const FAKE_HEADERS = { |
| Accept: "*/*", |
| "Accept-Encoding": "gzip, deflate, br, zstd", |
| "Accept-Language": "zh-CN,zh;q=0.9", |
| Origin: "https://chat.deepseek.com", |
| Pragma: "no-cache", |
| Referer: "https://chat.deepseek.com/", |
| "Sec-Ch-Ua": |
| '"Chromium";v="124", "Google Chrome";v="124", "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/124.0.0.0 Safari/537.36", |
| "X-App-Version": "20240126.0", |
| }; |
| |
| const accessTokenMap = new Map(); |
| |
| const accessTokenRequestQueueMap: Record<string, Function[]> = {}; |
|
|
| |
| const chatLock = new AsyncLock(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function clearContext(model: string, refreshToken: string) { |
| const token = await acquireToken(refreshToken); |
| const result = await axios.post( |
| "https://chat.deepseek.com/api/v0/chat/clear_context", |
| { |
| model_class: model, |
| append_welcome_message: false |
| }, |
| { |
| headers: { |
| Authorization: `Bearer ${token}`, |
| ...FAKE_HEADERS, |
| }, |
| timeout: 15000, |
| validateStatus: () => true, |
| } |
| ); |
| checkResult(result, refreshToken); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async function createCompletion( |
| model = MODEL_NAME, |
| messages: any[], |
| refreshToken: string, |
| retryCount = 0 |
| ) { |
| return (async () => { |
| logger.info(messages); |
|
|
| |
| const result = await chatLock.acquire(refreshToken, async () => { |
| |
| await clearContext(model, refreshToken); |
| |
| const token = await acquireToken(refreshToken); |
| return await axios.post( |
| "https://chat.deepseek.com/api/v0/chat/completions", |
| { |
| message: messagesPrepare(messages), |
| stream: true, |
| model_preference: null, |
| model_class: model, |
| temperature: 0 |
| }, |
| { |
| headers: { |
| Authorization: `Bearer ${token}`, |
| ...FAKE_HEADERS |
| }, |
| |
| timeout: 120000, |
| validateStatus: () => true, |
| responseType: "stream", |
| } |
| ); |
| }); |
|
|
| 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); |
| 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, |
| retryCount + 1 |
| ); |
| })(); |
| } |
| throw err; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async function createCompletionStream( |
| model = MODEL_NAME, |
| messages: any[], |
| refreshToken: string, |
| retryCount = 0 |
| ) { |
| return (async () => { |
| logger.info(messages); |
|
|
| const result = await chatLock.acquire(refreshToken, async () => { |
| |
| await clearContext(model, refreshToken); |
| |
| const token = await acquireToken(refreshToken); |
| return await axios.post( |
| "https://chat.deepseek.com/api/v0/chat/completions", |
| { |
| message: messagesPrepare(messages), |
| stream: true, |
| model_preference: null, |
| model_class: model, |
| temperature: 0 |
| }, |
| { |
| headers: { |
| Authorization: `Bearer ${token}`, |
| ...FAKE_HEADERS |
| }, |
| |
| timeout: 120000, |
| validateStatus: () => true, |
| responseType: "stream", |
| } |
| ); |
| }); |
|
|
| 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(); |
| |
| return createTransStream(model, result.data, () => { |
| 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, |
| retryCount + 1 |
| ); |
| })(); |
| } |
| throw err; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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:" |
| ) |
| |
| .replace(/\!\[.+\]\(.+\)/g, ""); |
| logger.info("\n对话合并:\n" + content); |
| } |
| return content; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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}`); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| async function receiveStream(model: string, stream: any): Promise<any> { |
| 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") return; |
| |
| 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 || !result.choices[0].delta.content || result.choices[0].delta.content == ' ') |
| return; |
| data.choices[0].message.content += result.choices[0].delta.content; |
| if (result.choices && result.choices[0] && result.choices[0].finish_reason === "stop") |
| resolve(data); |
| } catch (err) { |
| logger.error(err); |
| reject(err); |
| } |
| }); |
| |
| stream.on("data", (buffer) => parser.feed(buffer.toString())); |
| stream.once("error", (err) => reject(err)); |
| stream.once("close", () => resolve(data)); |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function createTransStream(model: string, stream: any, endCallback?: Function) { |
| |
| 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") return; |
| |
| 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 || !result.choices[0].delta.content || result.choices[0].delta.content == ' ') |
| return; |
| result.model = model; |
| transStream.write(`data: ${JSON.stringify({ |
| id: result.id, |
| model: result.model, |
| object: "chat.completion.chunk", |
| choices: [ |
| { |
| index: 0, |
| delta: { role: "assistant", content: result.choices[0].delta.content }, |
| finish_reason: null, |
| }, |
| ], |
| created, |
| })}\n\n`); |
| if (result.choices && result.choices[0] && result.choices[0].finish_reason === "stop") { |
| transStream.write(`data: ${JSON.stringify({ |
| id: result.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"); |
| } |
| } catch (err) { |
| logger.error(err); |
| !transStream.closed && transStream.end("data: [DONE]\n\n"); |
| } |
| }); |
| |
| 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") |
| ); |
| return transStream; |
| } |
|
|
| |
| |
| |
| |
| |
| function tokenSplit(authorization: string) { |
| return authorization.replace("Bearer ", "").split(","); |
| } |
|
|
| |
| |
| |
| 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, |
| }, |
| timeout: 15000, |
| validateStatus: () => true, |
| } |
| ); |
| try { |
| const { token } = checkResult(result, refreshToken); |
| return !!token; |
| } |
| catch (err) { |
| return false; |
| } |
| } |
|
|
| export default { |
| createCompletion, |
| createCompletionStream, |
| getTokenLiveStatus, |
| tokenSplit, |
| }; |
|
|