import { URL } from "url"; import { PassThrough } from "stream"; import http2 from "http2"; import path from "path"; import _ from "lodash"; import mime from "mime"; import FormData from "form-data"; 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 = "qwen"; // 最大重试次数 const MAX_RETRY_COUNT = 3; // 重试延迟 const RETRY_DELAY = 5000; // 伪装headers const FAKE_HEADERS = { Accept: "application/json, text/plain, */*", "Accept-Encoding": "gzip, deflate, br, zstd", "Accept-Language": "zh-CN,zh;q=0.9", "Cache-Control": "no-cache", Origin: "https://tongyi.aliyun.com", Pragma: "no-cache", "Sec-Ch-Ua": '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"Windows"', "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-site", Referer: "https://tongyi.aliyun.com/", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "X-Platform": "pc_tongyi", "X-Xsrf-Token": "48b9ee49-a184-45e2-9f67-fa87213edcdc", }; // 文件最大大小 const FILE_MAX_SIZE = 100 * 1024 * 1024; /** * 移除会话 * * 在对话流传输完毕后移除会话,避免创建的会话出现在用户的对话列表中 * * @param ticket tongyi_sso_ticket或login_aliyunid_ticket */ async function removeConversation(convId: string, ticket: string) { const result = await axios.post( `https://qianwen.biz.aliyun.com/dialog/session/delete`, { sessionId: convId, }, { headers: { Cookie: generateCookie(ticket), ...FAKE_HEADERS, }, timeout: 15000, validateStatus: () => true, } ); checkResult(result); } /** * 同步对话补全 * * @param model 模型名称 * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 * @param ticket tongyi_sso_ticket或login_aliyunid_ticket * @param refConvId 引用的会话ID * @param retryCount 重试次数 */ async function createCompletion( model = MODEL_NAME, messages: any[], ticket: string, refConvId = '', retryCount = 0 ) { let session: http2.ClientHttp2Session; return (async () => { logger.info(messages); // 提取引用文件URL并上传qwen获得引用的文件ID列表 const refFileUrls = extractRefFileUrls(messages); const refs = refFileUrls.length ? await Promise.all( refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket)) ) : []; // 如果引用对话ID不正确则重置引用 if (!/[0-9a-z]{32}/.test(refConvId)) refConvId = ''; // 请求流 const session: http2.ClientHttp2Session = await new Promise( (resolve, reject) => { const session = http2.connect("https://qianwen.biz.aliyun.com"); session.on("connect", () => resolve(session)); session.on("error", reject); } ); const [sessionId, parentMsgId = ''] = refConvId.split('-'); const req = session.request({ ":method": "POST", ":path": "/dialog/conversation", "Content-Type": "application/json", Cookie: generateCookie(ticket), ...FAKE_HEADERS, Accept: "text/event-stream", }); req.setTimeout(120000); req.write( JSON.stringify({ mode: "chat", model: "", action: "next", userAction: "chat", requestId: util.uuid(false), sessionId, sessionType: "text_chat", parentMsgId, params: { "fileUploadBatchId": util.uuid() }, contents: messagesPrepare(messages, refs, !!refConvId), }) ); req.setEncoding("utf8"); const streamStartTime = util.timestamp(); // 接收流为输出文本 const answer = await receiveStream(req); session.close(); logger.success( `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` ); // 异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略 removeConversation(answer.id, ticket).catch((err) => console.error(err)); return answer; })().catch((err) => { session && session.close(); if (retryCount < MAX_RETRY_COUNT) { logger.error(`Stream response error: ${err.message}`); logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); return (async () => { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return createCompletion(model, messages, ticket, refConvId, retryCount + 1); })(); } throw err; }); } /** * 流式对话补全 * * @param model 模型名称 * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 * @param ticket tongyi_sso_ticket或login_aliyunid_ticket * @param refConvId 引用的会话ID * @param retryCount 重试次数 */ async function createCompletionStream( model = MODEL_NAME, messages: any[], ticket: string, refConvId = '', retryCount = 0 ) { let session: http2.ClientHttp2Session; return (async () => { logger.info(messages); // 提取引用文件URL并上传qwen获得引用的文件ID列表 const refFileUrls = extractRefFileUrls(messages); const refs = refFileUrls.length ? await Promise.all( refFileUrls.map((fileUrl) => uploadFile(fileUrl, ticket)) ) : []; // 如果引用对话ID不正确则重置引用 if (!/[0-9a-z]{32}/.test(refConvId)) refConvId = '' // 请求流 session = await new Promise((resolve, reject) => { const session = http2.connect("https://qianwen.biz.aliyun.com"); session.on("connect", () => resolve(session)); session.on("error", reject); }); const [sessionId, parentMsgId = ''] = refConvId.split('-'); const req = session.request({ ":method": "POST", ":path": "/dialog/conversation", "Content-Type": "application/json", Cookie: generateCookie(ticket), ...FAKE_HEADERS, Accept: "text/event-stream", }); req.setTimeout(120000); req.write( JSON.stringify({ mode: "chat", model: "", action: "next", userAction: "chat", requestId: util.uuid(false), sessionId, sessionType: "text_chat", parentMsgId, params: { "fileUploadBatchId": util.uuid() }, contents: messagesPrepare(messages, refs, !!refConvId), }) ); req.setEncoding("utf8"); const streamStartTime = util.timestamp(); // 创建转换流将消息格式转换为gpt兼容格式 return createTransStream(req, (convId: string) => { // 关闭请求会话 session.close(); logger.success( `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` ); // 流传输结束后异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略 removeConversation(convId, ticket).catch((err) => console.error(err)); }); })().catch((err) => { session && session.close(); if (retryCount < MAX_RETRY_COUNT) { logger.error(`Stream response error: ${err.message}`); logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); return (async () => { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return createCompletionStream(model, messages, ticket, refConvId, retryCount + 1); })(); } throw err; }); } async function generateImages( model = MODEL_NAME, prompt: string, ticket: string, retryCount = 0 ) { let session: http2.ClientHttp2Session; return (async () => { const messages = [ { role: "user", content: prompt.indexOf('画') == -1 ? `请画:${prompt}` : prompt }, ]; // 请求流 const session: http2.ClientHttp2Session = await new Promise( (resolve, reject) => { const session = http2.connect("https://qianwen.biz.aliyun.com"); session.on("connect", () => resolve(session)); session.on("error", reject); } ); const req = session.request({ ":method": "POST", ":path": "/dialog/conversation", "Content-Type": "application/json", Cookie: generateCookie(ticket), ...FAKE_HEADERS, Accept: "text/event-stream", }); req.setTimeout(120000); req.write( JSON.stringify({ mode: "chat", model: "", action: "next", userAction: "chat", requestId: util.uuid(false), sessionId: "", sessionType: "text_chat", parentMsgId: "", params: { "fileUploadBatchId": util.uuid() }, contents: messagesPrepare(messages), }) ); req.setEncoding("utf8"); const streamStartTime = util.timestamp(); // 接收流为输出文本 const { convId, imageUrls } = await receiveImages(req); session.close(); logger.success( `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` ); // 异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略 removeConversation(convId, ticket).catch((err) => console.error(err)); return imageUrls; })().catch((err) => { session && session.close(); if (retryCount < MAX_RETRY_COUNT) { logger.error(`Stream response error: ${err.message}`); logger.warn(`Try again after ${RETRY_DELAY / 1000}s...`); return (async () => { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return generateImages(model, prompt, ticket, retryCount + 1); })(); } throw err; }); } /** * 提取消息中引用的文件URL * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 */ function extractRefFileUrls(messages: any[]) { const urls = []; // 如果没有消息,则返回[] if (!messages.length) { return urls; } // 只获取最新的消息 const lastMessage = messages[messages.length - 1]; if (_.isArray(lastMessage.content)) { lastMessage.content.forEach((v) => { if (!_.isObject(v) || !["file", "image_url"].includes(v["type"])) return; // glm-free-api支持格式 if ( v["type"] == "file" && _.isObject(v["file_url"]) && _.isString(v["file_url"]["url"]) ) urls.push(v["file_url"]["url"]); // 兼容gpt-4-vision-preview API格式 else if ( v["type"] == "image_url" && _.isObject(v["image_url"]) && _.isString(v["image_url"]["url"]) ) urls.push(v["image_url"]["url"]); }); } logger.info("本次请求上传:" + urls.length + "个文件"); return urls; } /** * 消息预处理 * * 由于接口只取第一条消息,此处会将多条消息合并为一条,实现多轮对话效果 * user:旧消息1 * assistant:旧消息2 * user:新消息 * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 * @param refs 参考文件列表 * @param isRefConv 是否为引用会话 */ function messagesPrepare(messages: any[], refs: any[] = [], isRefConv = false) { let content; if (isRefConv || 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 + `<|im_start|>${message.role || "user"}\n${v["text"] || ""}<|im_end|>\n`; }, content); } return (content += `<|im_start|>${message.role || "user"}\n${ message.content }<|im_end|>\n`); }, "").replace(/\!\[.*\]\(.+\)/g, ""); logger.info("\n对话合并:\n" + content); } return [ { content, contentType: "text", role: "user", }, ...refs ]; } /** * 检查请求结果 * * @param result 结果 */ function checkResult(result: AxiosResponse) { if (!result.data) return null; const { success, errorCode, errorMsg } = result.data; if (!_.isBoolean(success) || success) return result.data; throw new APIException( EX.API_REQUEST_FAILED, `[请求qwen失败]: ${errorCode}-${errorMsg}` ); } /** * 从流接收完整的消息内容 * * @param stream 消息流 */ async function receiveStream(stream: any): Promise { return new Promise((resolve, reject) => { // 消息初始化 const data = { id: "", model: MODEL_NAME, 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; if (event.data == "[DONE]") return; // 解析JSON const result = _.attempt(() => JSON.parse(event.data)); if (_.isError(result)) throw new Error(`Stream response invalid: ${event.data}`); if (!data.id && result.sessionId && result.msgId) data.id = `${result.sessionId}-${result.msgId}`; const text = (result.contents || []).reduce((str, part) => { const { contentType, role, content } = part; if (contentType != "text" && contentType != "text2image") return str; if (role != "assistant" && !_.isString(content)) return str; return str + content; }, ""); const exceptCharIndex = text.indexOf("�"); let chunk = text.substring( exceptCharIndex != -1 ? Math.min(data.choices[0].message.content.length, exceptCharIndex) : data.choices[0].message.content.length, exceptCharIndex == -1 ? text.length : exceptCharIndex ); if (chunk && result.contentType == "text2image") { chunk = chunk.replace( /https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi, (url) => { const urlObj = new URL(url); urlObj.search = ""; return urlObj.toString(); } ); } if (result.msgStatus != "finished") { if (result.contentType == "text") data.choices[0].message.content += chunk; } else { data.choices[0].message.content += chunk; if (!result.canShare) data.choices[0].message.content += "\n[内容由于不合规被停止生成,我们换个话题吧]"; if (result.errorCode) data.choices[0].message.content += `服务暂时不可用,第三方响应错误:${result.errorCode}`; 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)); stream.end(); }); } /** * 创建转换流 * * 将流格式转换为gpt兼容流格式 * * @param stream 消息流 * @param endCallback 传输结束回调 */ function createTransStream(stream: any, endCallback?: Function) { // 消息创建时间 const created = util.unixTimestamp(); // 创建转换流 const transStream = new PassThrough(); let content = ""; !transStream.closed && transStream.write( `data: ${JSON.stringify({ id: "", model: MODEL_NAME, 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; if (event.data == "[DONE]") return; // 解析JSON const result = _.attempt(() => JSON.parse(event.data)); if (_.isError(result)) throw new Error(`Stream response invalid: ${event.data}`); const text = (result.contents || []).reduce((str, part) => { const { contentType, role, content } = part; if (contentType != "text" && contentType != "text2image") return str; if (role != "assistant" && !_.isString(content)) return str; return str + content; }, ""); const exceptCharIndex = text.indexOf("�"); let chunk = text.substring( exceptCharIndex != -1 ? Math.min(content.length, exceptCharIndex) : content.length, exceptCharIndex == -1 ? text.length : exceptCharIndex ); if (chunk && result.contentType == "text2image") { chunk = chunk.replace( /https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi, (url) => { const urlObj = new URL(url); urlObj.search = ""; return urlObj.toString(); } ); } if (result.msgStatus != "finished") { if (chunk && result.contentType == "text") { content += chunk; const data = `data: ${JSON.stringify({ id: `${result.sessionId}-${result.msgId}`, model: MODEL_NAME, object: "chat.completion.chunk", choices: [ { index: 0, delta: { content: chunk }, finish_reason: null }, ], created, })}\n\n`; !transStream.closed && transStream.write(data); } } else { const delta = { content: chunk || "" }; if (!result.canShare) delta.content += "\n[内容由于不合规被停止生成,我们换个话题吧]"; if (result.errorCode) delta.content += `服务暂时不可用,第三方响应错误:${result.errorCode}`; const data = `data: ${JSON.stringify({ id: `${result.sessionId}-${result.msgId}`, model: MODEL_NAME, object: "chat.completion.chunk", choices: [ { index: 0, delta, finish_reason: "stop", }, ], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, created, })}\n\n`; !transStream.closed && transStream.write(data); !transStream.closed && transStream.end("data: [DONE]\n\n"); content = ""; endCallback && endCallback(result.sessionId); } // else // logger.warn(result.event, result); } catch (err) { logger.error(err); !transStream.closed && transStream.end("\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") ); stream.end(); return transStream; } /** * 从流接收图像 * * @param stream 消息流 */ async function receiveImages( stream: any ): Promise<{ convId: string; imageUrls: string[] }> { return new Promise((resolve, reject) => { let convId = ""; const imageUrls = []; const parser = createParser((event) => { try { if (event.type !== "event") return; if (event.data == "[DONE]") return; // 解析JSON const result = _.attempt(() => JSON.parse(event.data)); if (_.isError(result)) throw new Error(`Stream response invalid: ${event.data}`); if (!convId && result.sessionId) convId = result.sessionId; const text = (result.contents || []).reduce((str, part) => { const { role, content } = part; if (role != "assistant" && !_.isString(content)) return str; return str + content; }, ""); if (result.contentFrom == "text2image") { const urls = text.match( /https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\,]*)/gi ) || []; urls.forEach((url) => { const urlObj = new URL(url); urlObj.search = ""; const imageUrl = urlObj.toString(); if (imageUrls.indexOf(imageUrl) != -1) return; imageUrls.push(imageUrl); }); } if (result.msgStatus == "finished") { if (!result.canShare || imageUrls.length == 0) throw new APIException(EX.API_CONTENT_FILTERED); if (result.errorCode) throw new APIException( EX.API_REQUEST_FAILED, `服务暂时不可用,第三方响应错误:${result.errorCode}` ); } } 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({ convId, imageUrls })); stream.end(); }); } /** * 获取上传参数 * * @param ticket tongyi_sso_ticket或login_aliyunid_ticket */ async function acquireUploadParams(ticket: string) { const result = await axios.post( "https://qianwen.biz.aliyun.com/dialog/uploadToken", {}, { timeout: 15000, headers: { Cookie: generateCookie(ticket), ...FAKE_HEADERS, }, validateStatus: () => true, } ); const { data } = checkResult(result); return data; } /** * 预检查文件URL有效性 * * @param fileUrl 文件URL */ async function checkFileUrl(fileUrl: string) { if (util.isBASE64Data(fileUrl)) return; const result = await axios.head(fileUrl, { timeout: 15000, validateStatus: () => true, }); if (result.status >= 400) throw new APIException( EX.API_FILE_URL_INVALID, `File ${fileUrl} is not valid: [${result.status}] ${result.statusText}` ); // 检查文件大小 if (result.headers && result.headers["content-length"]) { const fileSize = parseInt(result.headers["content-length"], 10); if (fileSize > FILE_MAX_SIZE) throw new APIException( EX.API_FILE_EXECEEDS_SIZE, `File ${fileUrl} is not valid` ); } } /** * 上传文件 * * @param fileUrl 文件URL * @param ticket tongyi_sso_ticket或login_aliyunid_ticket */ async function uploadFile(fileUrl: string, ticket: string) { // 预检查远程文件URL可用性 await checkFileUrl(fileUrl); let filename, fileData, mimeType; // 如果是BASE64数据则直接转换为Buffer if (util.isBASE64Data(fileUrl)) { mimeType = util.extractBASE64DataFormat(fileUrl); const ext = mime.getExtension(mimeType); filename = `${util.uuid()}.${ext}`; fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), "base64"); } // 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存 else { filename = path.basename(fileUrl); ({ data: fileData } = await axios.get(fileUrl, { responseType: "arraybuffer", // 100M限制 maxContentLength: FILE_MAX_SIZE, // 60秒超时 timeout: 60000, })); } // 获取文件的MIME类型 mimeType = mimeType || mime.getType(filename); // 获取上传参数 const { accessId, policy, signature, dir } = await acquireUploadParams( ticket ); const formData = new FormData(); formData.append("OSSAccessKeyId", accessId); formData.append("policy", policy); formData.append("signature", signature); formData.append("key", `${dir}${filename}`); formData.append("dir", dir); formData.append("success_action_status", "200"); formData.append("file", fileData, { filename, contentType: mimeType, }); // 上传文件到OSS await axios.request({ method: "POST", url: "https://broadscope-dialogue.oss-cn-beijing.aliyuncs.com/", data: formData, // 100M限制 maxBodyLength: FILE_MAX_SIZE, // 60秒超时 timeout: 120000, headers: { ...FAKE_HEADERS, "X-Requested-With": "XMLHttpRequest" } }); const isImage = [ 'image/jpeg', 'image/jpg', 'image/tiff', 'image/png', 'image/bmp', 'image/gif', 'image/svg+xml', 'image/webp', 'image/ico', 'image/heic', 'image/heif', 'image/bmp', 'image/x-icon', 'image/vnd.microsoft.icon', 'image/x-png' ].includes(mimeType); if(isImage) { const result = await axios.post( "https://qianwen.biz.aliyun.com/dialog/downloadLink", { fileKey: filename, fileType: "image", dir }, { timeout: 15000, headers: { Cookie: generateCookie(ticket), ...FAKE_HEADERS, }, validateStatus: () => true, } ); const { data } = checkResult(result); return { role: "user", contentType: "image", content: data.url }; } else { let result = await axios.post( "https://qianwen.biz.aliyun.com/dialog/downloadLink/batch", { fileKeys: [filename], fileType: "file", dir }, { timeout: 15000, headers: { Cookie: generateCookie(ticket), ...FAKE_HEADERS, }, validateStatus: () => true, } ); const { data } = checkResult(result); if(!data.results[0] || !data.results[0].url) throw new Error(`文件上传失败:${data.results[0] ? data.results[0].errorMsg : '未知错误'}`); const url = data.results[0].url; const startTime = util.timestamp(); while(true) { result = await axios.post( "https://qianwen.biz.aliyun.com/dialog/secResult/batch", { urls: [url] }, { timeout: 15000, headers: { Cookie: generateCookie(ticket), ...FAKE_HEADERS, }, validateStatus: () => true, } ); const { data } = checkResult(result); if(data.pollEndFlag) { if(data.statusList[0] && data.statusList[0].status === 0) throw new Error(`文件处理失败:${data.statusList[0].errorMsg || '未知错误'}`); break; } if(util.timestamp() > startTime + 120000) throw new Error("文件处理超时:超出120秒"); } return { role: "user", contentType: "file", content: url, ext: { fileSize: fileData.byteLength } }; } } /** * Token切分 * * @param authorization 认证字符串 */ function tokenSplit(authorization: string) { return authorization.replace("Bearer ", "").split(","); } /** * 生成Cookies * * @param ticket tongyi_sso_ticket或login_aliyunid_ticket */ function generateCookie(ticket: string) { return [ `${ticket.length > 100 ? 'login_aliyunid_ticket' : 'tongyi_sso_ticket'}=${ticket}`, 'aliyun_choice=intl', "_samesite_flag_=true", `t=${util.uuid(false)}`, // `login_aliyunid_csrf=_csrf_tk_${util.generateRandomString({ charset: 'numeric', length: 15 })}`, // `cookie2=${util.uuid(false)}`, // `munb=22${util.generateRandomString({ charset: 'numeric', length: 11 })}`, // `csg=`, // `_tb_token_=${util.generateRandomString({ length: 10, capitalization: 'lowercase' })}`, // `cna=`, // `cnaui=`, // `atpsida=`, // `isg=`, // `tfstk=`, // `aui=`, // `sca=` ].join("; "); } /** * 获取Token存活状态 */ async function getTokenLiveStatus(ticket: string) { const result = await axios.post( "https://qianwen.biz.aliyun.com/dialog/session/list", {}, { headers: { Cookie: generateCookie(ticket), ...FAKE_HEADERS, }, timeout: 15000, validateStatus: () => true, } ); try { const { data } = checkResult(result); return _.isArray(data); } catch(err) { return false; } } export default { createCompletion, createCompletionStream, generateImages, getTokenLiveStatus, tokenSplit, };