diff --git a/src/api/consts/exceptions.ts b/src/api/consts/exceptions.ts index 812e463..30e8dcf 100644 --- a/src/api/consts/exceptions.ts +++ b/src/api/consts/exceptions.ts @@ -5,5 +5,6 @@ export default { API_TOKEN_EXPIRES: [-2002, 'Token已失效'], API_FILE_URL_INVALID: [-2003, '远程文件URL非法'], API_FILE_EXECEEDS_SIZE: [-2004, '远程文件超出大小'], - API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'] + API_CHAT_STREAM_PUSHING: [-2005, '已有对话流正在输出'], + API_CONTENT_FILTERED: [-2006, '内容由于合规问题已被阻止生成'] } \ No newline at end of file diff --git a/src/api/controllers/chat.ts b/src/api/controllers/chat.ts index 0f4e458..6ca82e5 100644 --- a/src/api/controllers/chat.ts +++ b/src/api/controllers/chat.ts @@ -1,20 +1,20 @@ import { PassThrough } from "stream"; -import path from 'path'; -import _ from 'lodash'; -import mime from 'mime'; +import path from "path"; +import _ from "lodash"; +import mime from "mime"; import FormData from "form-data"; -import axios, { AxiosResponse } from 'axios'; +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'; +import { createParser } from "eventsource-parser"; +import logger from "@/lib/logger.ts"; +import util from "@/lib/util.ts"; // 模型名称 -const MODEL_NAME = 'glm'; +const MODEL_NAME = "glm"; // 默认的智能体ID,GLM4 -const DEFAULT_ASSISTANT_ID = '65940acff94777010aa6b796'; +const DEFAULT_ASSISTANT_ID = "65940acff94777010aa6b796"; // access_token有效期 const ACCESS_TOKEN_EXPIRES = 3600; // 最大重试次数 @@ -23,15 +23,17 @@ const MAX_RETRY_COUNT = 3; const RETRY_DELAY = 5000; // 伪装headers const FAKE_HEADERS = { - 'Accept': '*/*', - 'App-Name': 'chatglm', - 'Platform': 'pc', - 'Origin': 'https://chatglm.cn', - '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"', - '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', - 'Version': '0.0.1' + Accept: "*/*", + "App-Name": "chatglm", + Platform: "pc", + Origin: "https://chatglm.cn", + "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"', + "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", + Version: "0.0.1", }; // 文件最大大小 const FILE_MAX_SIZE = 100 * 1024 * 1024; @@ -42,61 +44,70 @@ const accessTokenRequestQueueMap: Record = {}; /** * 请求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)); + return new Promise((resolve) => + accessTokenRequestQueueMap[refreshToken].push(resolve) + ); accessTokenRequestQueueMap[refreshToken] = []; logger.info(`Refresh token: ${refreshToken}`); const result = await (async () => { - const result = await axios.post('https://chatglm.cn/chatglm/backend-api/v1/user/refresh', {}, { - headers: { - Authorization: `Bearer ${refreshToken}`, - Referer: 'https://chatglm.cn/main/alltoolsdetail', - 'X-Device-Id': util.uuid(false), - 'X-Request-Id': util.uuid(false), - ...FAKE_HEADERS - }, - timeout: 15000, - validateStatus: () => true - }); + const result = await axios.post( + "https://chatglm.cn/chatglm/backend-api/v1/user/refresh", + {}, + { + headers: { + Authorization: `Bearer ${refreshToken}`, + Referer: "https://chatglm.cn/main/alltoolsdetail", + "X-Device-Id": util.uuid(false), + "X-Request-Id": util.uuid(false), + ...FAKE_HEADERS, + }, + timeout: 15000, + validateStatus: () => true, + } + ); const { result: _result } = checkResult(result, refreshToken); const { accessToken } = _result; return { accessToken, refreshToken, - refreshTime: util.unixTimestamp() + ACCESS_TOKEN_EXPIRES - } + refreshTime: util.unixTimestamp() + ACCESS_TOKEN_EXPIRES, + }; })() - .then(result => { - if(accessTokenRequestQueueMap[refreshToken]) { - accessTokenRequestQueueMap[refreshToken].forEach(resolve => resolve(result)); + .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)); + .catch((err) => { + if (accessTokenRequestQueueMap[refreshToken]) { + accessTokenRequestQueueMap[refreshToken].forEach((resolve) => + resolve(err) + ); delete accessTokenRequestQueueMap[refreshToken]; } return err; }); - if(_.isError(result)) - throw result; + if (_.isError(result)) throw result; return result; } /** * 获取缓存中的access_token - * + * * 避免短时间大量刷新token,未加锁,如果有并发要求还需加锁 - * + * * @param refreshToken 用于刷新access_token的refresh_token */ async function acquireToken(refreshToken: string): Promise { @@ -114,199 +125,346 @@ async function acquireToken(refreshToken: string): Promise { /** * 移除会话 - * + * * 在对话流传输完毕后移除会话,避免创建的会话出现在用户的对话列表中 - * + * * @param refreshToken 用于刷新access_token的refresh_token */ -async function removeConversation(convId: string, refreshToken: string, assistantId = DEFAULT_ASSISTANT_ID) { +async function removeConversation( + convId: string, + refreshToken: string, + assistantId = DEFAULT_ASSISTANT_ID +) { const token = await acquireToken(refreshToken); - const result = await axios.post('https://chatglm.cn/chatglm/backend-api/assistant/conversation/delete', { - assistant_id: assistantId, - conversation_id: convId - }, { - headers: { - Authorization: `Bearer ${token}`, - Referer: `https://chatglm.cn/main/alltoolsdetail`, - 'X-Device-Id': util.uuid(false), - 'X-Request-Id': util.uuid(false), - ...FAKE_HEADERS + const result = await axios.post( + "https://chatglm.cn/chatglm/backend-api/assistant/conversation/delete", + { + assistant_id: assistantId, + conversation_id: convId, }, - timeout: 15000, - validateStatus: () => true - }); + { + headers: { + Authorization: `Bearer ${token}`, + Referer: `https://chatglm.cn/main/alltoolsdetail`, + "X-Device-Id": util.uuid(false), + "X-Request-Id": util.uuid(false), + ...FAKE_HEADERS, + }, + timeout: 15000, + validateStatus: () => true, + } + ); checkResult(result, refreshToken); } /** * 同步对话补全 - * + * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 * @param refreshToken 用于刷新access_token的refresh_token * @param assistantId 智能体ID,默认使用GLM4原版 * @param retryCount 重试次数 */ -async function createCompletion(messages: any[], refreshToken: string, assistantId = DEFAULT_ASSISTANT_ID, retryCount = 0) { +async function createCompletion( + messages: any[], + refreshToken: string, + assistantId = DEFAULT_ASSISTANT_ID, + retryCount = 0 +) { return (async () => { logger.info(messages); // 提取引用文件URL并上传获得引用的文件ID列表 const refFileUrls = extractRefFileUrls(messages); - const refs = refFileUrls.length ? await Promise.all(refFileUrls.map(fileUrl => uploadFile(fileUrl, refreshToken))) : []; + const refs = refFileUrls.length + ? await Promise.all( + refFileUrls.map((fileUrl) => uploadFile(fileUrl, refreshToken)) + ) + : []; // 请求流 const token = await acquireToken(refreshToken); - const result = await axios.post('https://chatglm.cn/chatglm/backend-api/assistant/stream', { - assistant_id: assistantId, - conversation_id: '', - messages: messagesPrepare(messages, refs), - meta_data: { - channel: '', - draft_id: '', - input_question_type: 'xxxx', - is_test: false - } - }, { - headers: { - Authorization: `Bearer ${token}`, - Referer: assistantId == DEFAULT_ASSISTANT_ID ? 'https://chatglm.cn/main/alltoolsdetail' : `https://chatglm.cn/main/gdetail/${assistantId}`, - 'X-Device-Id': util.uuid(false), - 'X-Request-Id': util.uuid(false), - ...FAKE_HEADERS + const result = await axios.post( + "https://chatglm.cn/chatglm/backend-api/assistant/stream", + { + assistant_id: assistantId, + conversation_id: "", + messages: messagesPrepare(messages, refs), + meta_data: { + channel: "", + draft_id: "", + input_question_type: "xxxx", + is_test: false, + }, }, - // 120秒超时 - timeout: 120000, - validateStatus: () => true, - responseType: 'stream' - }); + { + headers: { + Authorization: `Bearer ${token}`, + Referer: + assistantId == DEFAULT_ASSISTANT_ID + ? "https://chatglm.cn/main/alltoolsdetail" + : `https://chatglm.cn/main/gdetail/${assistantId}`, + "X-Device-Id": util.uuid(false), + "X-Request-Id": util.uuid(false), + ...FAKE_HEADERS, + }, + // 120秒超时 + timeout: 120000, + validateStatus: () => true, + responseType: "stream", + } + ); - if(result.headers['content-type'].indexOf('text/event-stream') == -1) - throw new APIException(EX.API_REQUEST_FAILED, `Stream response Content-Type invalid: ${result.headers['content-type']}`); + if (result.headers["content-type"].indexOf("text/event-stream") == -1) + throw new APIException( + EX.API_REQUEST_FAILED, + `Stream response Content-Type invalid: ${result.headers["content-type"]}` + ); const streamStartTime = util.timestamp(); // 接收流为输出文本 const answer = await receiveStream(result.data); - logger.success(`Stream has completed transfer ${util.timestamp() - streamStartTime}ms`); + logger.success( + `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` + ); // 异步移除会话 - removeConversation(answer.id, refreshToken, assistantId) - .catch(err => console.error(err)); + removeConversation(answer.id, refreshToken, assistantId).catch((err) => + console.error(err) + ); 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(messages, refreshToken, assistantId, retryCount + 1); - })(); - } - throw err; - }); + })().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( + messages, + refreshToken, + assistantId, + retryCount + 1 + ); + })(); + } + throw err; + }); } /** * 流式对话补全 - * + * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 * @param refreshToken 用于刷新access_token的refresh_token * @param assistantId 智能体ID,默认使用GLM4原版 * @param retryCount 重试次数 */ -async function createCompletionStream(messages: any[], refreshToken: string, assistantId = DEFAULT_ASSISTANT_ID, retryCount = 0) { +async function createCompletionStream( + messages: any[], + refreshToken: string, + assistantId = DEFAULT_ASSISTANT_ID, + retryCount = 0 +) { return (async () => { logger.info(messages); // 提取引用文件URL并上传获得引用的文件ID列表 const refFileUrls = extractRefFileUrls(messages); - const refs = refFileUrls.length ? await Promise.all(refFileUrls.map(fileUrl => uploadFile(fileUrl, refreshToken))) : []; + const refs = refFileUrls.length + ? await Promise.all( + refFileUrls.map((fileUrl) => uploadFile(fileUrl, refreshToken)) + ) + : []; // 请求流 const token = await acquireToken(refreshToken); - const result = await axios.post(`https://chatglm.cn/chatglm/backend-api/assistant/stream`, { - assistant_id: assistantId, - conversation_id: '', - messages: messagesPrepare(messages, refs), - meta_data: { - channel: '', - draft_id: '', - input_question_type: 'xxxx', - is_test: false - } - }, { - headers: { - Authorization: `Bearer ${token}`, - Referer: assistantId == DEFAULT_ASSISTANT_ID ? 'https://chatglm.cn/main/alltoolsdetail' : `https://chatglm.cn/main/gdetail/${assistantId}`, - 'X-Device-Id': util.uuid(false), - 'X-Request-Id': util.uuid(false), - ...FAKE_HEADERS + const result = await axios.post( + `https://chatglm.cn/chatglm/backend-api/assistant/stream`, + { + assistant_id: assistantId, + conversation_id: "", + messages: messagesPrepare(messages, refs), + meta_data: { + channel: "", + draft_id: "", + input_question_type: "xxxx", + is_test: false, + }, }, - // 120秒超时 - timeout: 120000, - validateStatus: () => true, - responseType: 'stream' - }); + { + headers: { + Authorization: `Bearer ${token}`, + Referer: + assistantId == DEFAULT_ASSISTANT_ID + ? "https://chatglm.cn/main/alltoolsdetail" + : `https://chatglm.cn/main/gdetail/${assistantId}`, + "X-Device-Id": util.uuid(false), + "X-Request-Id": util.uuid(false), + ...FAKE_HEADERS, + }, + // 120秒超时 + 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']); + if (result.headers["content-type"].indexOf("text/event-stream") == -1) { + logger.error( + `Invalid response Content-Type:`, + result.headers["content-type"] + ); 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`); + 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(result.data, (convId: string) => { - logger.success(`Stream has completed transfer ${util.timestamp() - streamStartTime}ms`); + logger.success( + `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` + ); // 流传输结束后异步移除会话 - removeConversation(convId, refreshToken, assistantId) - .catch(err => console.error(err)); + removeConversation(convId, refreshToken, assistantId).catch((err) => + console.error(err) + ); }); - })() - .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(messages, refreshToken, assistantId, retryCount + 1); - })(); + })().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( + messages, + refreshToken, + assistantId, + retryCount + 1 + ); + })(); + } + throw err; + }); +} + +async function generateImages( + model = MODEL_NAME, + prompt: string, + refreshToken: string, + retryCount = 0 +) { + return (async () => { + const messages = [{ role: "user", content: prompt }]; + // 官方AI绘图的智能体ID + const assistantId = "65a232c082ff90a2ad2f15e2"; + // 请求流 + const token = await acquireToken(refreshToken); + const result = await axios.post( + "https://chatglm.cn/chatglm/backend-api/assistant/stream", + { + assistant_id: assistantId, + conversation_id: "", + messages: messagesPrepare(messages, []), + meta_data: { + channel: "", + draft_id: "", + input_question_type: "xxxx", + is_test: false, + }, + }, + { + headers: { + Authorization: `Bearer ${token}`, + Referer: `https://chatglm.cn/main/gdetail/${assistantId}`, + "X-Device-Id": util.uuid(false), + "X-Request-Id": util.uuid(false), + ...FAKE_HEADERS, + }, + // 120秒超时 + timeout: 120000, + validateStatus: () => true, + responseType: "stream", } - throw err; - }); + ); + + if (result.headers["content-type"].indexOf("text/event-stream") == -1) + throw new APIException( + EX.API_REQUEST_FAILED, + `Stream response Content-Type invalid: ${result.headers["content-type"]}` + ); + + const streamStartTime = util.timestamp(); + // 接收流为输出文本 + const { convId, imageUrls } = await receiveImages(result.data); + logger.success( + `Stream has completed transfer ${util.timestamp() - streamStartTime}ms` + ); + + // 异步移除会话,如果消息不合规,此操作可能会抛出数据库错误异常,请忽略 + removeConversation(convId, refreshToken, assistantId).catch((err) => + console.error(err) + ); + + return imageUrls; + })().catch((err) => { + 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, refreshToken, retryCount + 1); + })(); + } + throw err; + }); } /** * 提取消息中引用的文件URL - * + * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 */ function extractRefFileUrls(messages: any[]) { return messages.reduce((urls, message) => { if (_.isArray(message.content)) { - message.content.forEach(v => { - if (!_.isObject(v) || !['file', 'image_url'].includes(v['type'])) + message.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']); + 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']); + else if ( + v["type"] == "image_url" && + _.isObject(v["image_url"]) && + _.isString(v["image_url"]["url"]) + ) + urls.push(v["image_url"]["url"]); }); } return urls; @@ -315,74 +473,94 @@ function extractRefFileUrls(messages: any[]) { /** * 消息预处理 - * + * * 由于接口只取第一条消息,此处会将多条消息合并为一条,实现多轮对话效果 * 使用”你“这个角色回复”我“这个角色,以第一人称对话\n * 我:旧消息1 * 你:旧消息2 * 我:新消息 - * + * * @param messages 参考gpt系列消息格式,多轮对话请完整提供上下文 */ function messagesPrepare(messages: any[], refs: any[]) { - const 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'] || ''); - }, content) + '\n'; - } - return content += `${message.role.replace('sytstem', '<|sytstem|>').replace('assistant', '<|assistant|>').replace('user', '<|user|>')}\n${message.content}\n`; - }, '') + '<|assistant|>\n'; - const fileRefs = refs.filter(ref => !ref.width && !ref.height); - const imageRefs = refs.filter(ref => ref.width || ref.height).map(ref => { - ref.image_url = ref.file_url; - return ref; - }); + const 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"] || ""); + }, content) + "\n" + ); + } + return (content += `${message.role + .replace("sytstem", "<|sytstem|>") + .replace("assistant", "<|assistant|>") + .replace("user", "<|user|>")}\n${message.content}\n`); + }, "") + "<|assistant|>\n"; + const fileRefs = refs.filter((ref) => !ref.width && !ref.height); + const imageRefs = refs + .filter((ref) => ref.width || ref.height) + .map((ref) => { + ref.image_url = ref.file_url; + return ref; + }); return [ { - role: 'user', + role: "user", content: [ - { type: 'text', text: content.replace(/\!\[.+\]\(.+\)/g, '') }, - ...(fileRefs.length == 0 ? [] : [{ - type: 'file', - file: fileRefs - }]), - ...(imageRefs.length == 0 ? [] : [{ - type: 'image', - image: imageRefs - }]) - ] - } - ] + { type: "text", text: content.replace(/\!\[.+\]\(.+\)/g, "") }, + ...(fileRefs.length == 0 + ? [] + : [ + { + type: "file", + file: fileRefs, + }, + ]), + ...(imageRefs.length == 0 + ? [] + : [ + { + type: "image", + image: imageRefs, + }, + ]), + ], + }, + ]; } /** * 预检查文件URL有效性 - * + * * @param fileUrl 文件URL */ async function checkFileUrl(fileUrl: string) { - if (util.isBASE64Data(fileUrl)) - return; + if (util.isBASE64Data(fileUrl)) return; const result = await axios.head(fileUrl, { timeout: 15000, - validateStatus: () => true + validateStatus: () => true, }); if (result.status >= 400) - throw new APIException(EX.API_FILE_URL_INVALID, `File ${fileUrl} is not valid: [${result.status}] ${result.statusText}`); + 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 (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`); + throw new APIException( + EX.API_FILE_EXECEEDS_SIZE, + `File ${fileUrl} is not valid` + ); } } /** * 上传文件 - * + * * @param fileUrl 文件URL * @param refreshToken 用于刷新access_token的refresh_token */ @@ -396,17 +574,17 @@ async function uploadFile(fileUrl: string, refreshToken: string) { mimeType = util.extractBASE64DataFormat(fileUrl); const ext = mime.getExtension(mimeType); filename = `${util.uuid()}.${ext}`; - fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), 'base64'); + fileData = Buffer.from(util.removeBASE64DataHeader(fileUrl), "base64"); } // 下载文件到内存,如果您的服务器内存很小,建议考虑改造为流直传到下一个接口上,避免停留占用内存 else { filename = path.basename(fileUrl); ({ data: fileData } = await axios.get(fileUrl, { - responseType: 'arraybuffer', + responseType: "arraybuffer", // 100M限制 maxContentLength: FILE_MAX_SIZE, // 60秒超时 - timeout: 60000 + timeout: 60000, })); } @@ -414,16 +592,16 @@ async function uploadFile(fileUrl: string, refreshToken: string) { mimeType = mimeType || mime.getType(filename); const formData = new FormData(); - formData.append('file', fileData, { + formData.append("file", fileData, { filename, - contentType: mimeType + contentType: mimeType, }); // 上传文件到目标OSS const token = await acquireToken(refreshToken); let result = await axios.request({ - method: 'POST', - url: 'https://chatglm.cn/chatglm/backend-api/assistant/file_upload', + method: "POST", + url: "https://chatglm.cn/chatglm/backend-api/assistant/file_upload", data: formData, // 100M限制 maxBodyLength: FILE_MAX_SIZE, @@ -433,9 +611,9 @@ async function uploadFile(fileUrl: string, refreshToken: string) { Authorization: `Bearer ${token}`, Referer: `https://chatglm.cn/`, ...FAKE_HEADERS, - ...formData.getHeaders() + ...formData.getHeaders(), }, - validateStatus: () => true + validateStatus: () => true, }); const { result: uploadResult } = checkResult(result, refreshToken); @@ -444,142 +622,174 @@ async function uploadFile(fileUrl: string, refreshToken: string) { /** * 检查请求结果 - * + * * @param result 结果 */ function checkResult(result: AxiosResponse, refreshToken: string) { - if (!result.data) - return null; + if (!result.data) return null; const { code, status, message } = result.data; - if (!_.isFinite(code) && !_.isFinite(status)) - return result.data; - if (code === 0 || status === 0) - return result.data; - if (code == 401) - accessTokenMap.delete(refreshToken); + if (!_.isFinite(code) && !_.isFinite(status)) return result.data; + if (code === 0 || status === 0) return result.data; + if (code == 401) accessTokenMap.delete(refreshToken); throw new APIException(EX.API_REQUEST_FAILED, `[请求glm失败]: ${message}`); } /** * 从流接收完整的消息内容 - * + * * @param stream 消息流 */ async function receiveStream(stream: any): Promise { return new Promise((resolve, reject) => { // 消息初始化 const data = { - id: '', + id: "", model: MODEL_NAME, - object: 'chat.completion', + object: "chat.completion", choices: [ - { index: 0, message: { role: 'assistant', content: '' }, finish_reason: 'stop' } + { + index: 0, + message: { role: "assistant", content: "" }, + finish_reason: "stop", + }, ], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - created: util.unixTimestamp() + created: util.unixTimestamp(), }; let toolCall = false; let codeGenerating = false; let textChunkLength = 0; - let codeTemp = ''; - let lastExecutionOutput = ''; + let codeTemp = ""; + let lastExecutionOutput = ""; let textOffset = 0; - const parser = createParser(event => { + const parser = createParser((event) => { try { if (event.type !== "event") return; // 解析JSON const result = _.attempt(() => JSON.parse(event.data)); if (_.isError(result)) throw new Error(`Stream response invalid: ${event.data}`); - if(!data.id && result.conversation_id) + if (!data.id && result.conversation_id) data.id = result.conversation_id; - if(result.status != 'finish') { + if (result.status != "finish") { const text = result.parts.reduce((str, part) => { const { status, content, meta_data } = part; - if(!_.isArray(content)) - return str; + if (!_.isArray(content)) return str; const partText = content.reduce((innerStr, value) => { - const { status: partStatus, type, text, image, code, content } = value; - if(partStatus == 'init' && textChunkLength > 0) { + const { + status: partStatus, + type, + text, + image, + code, + content, + } = value; + if (partStatus == "init" && textChunkLength > 0) { textOffset += textChunkLength + 1; textChunkLength = 0; - innerStr += '\n'; + innerStr += "\n"; } - if(type == 'text') { - if(toolCall) { - innerStr += '\n'; + if (type == "text") { + if (toolCall) { + innerStr += "\n"; textOffset++; toolCall = false; } - if(partStatus == 'finish') - textChunkLength = text.length; + if (partStatus == "finish") textChunkLength = text.length; return innerStr + text; - } - else if(type == 'quote_result' && status == 'finish' && meta_data && _.isArray(meta_data.metadata_list)) { - const searchText = meta_data.metadata_list.reduce((meta, v) => meta + `检索 ${v.title}(${v.url}) ...`, '') + '\n'; + } else if ( + type == "quote_result" && + status == "finish" && + meta_data && + _.isArray(meta_data.metadata_list) + ) { + const searchText = + meta_data.metadata_list.reduce( + (meta, v) => meta + `检索 ${v.title}(${v.url}) ...`, + "" + ) + "\n"; textOffset += searchText.length; toolCall = true; return innerStr + searchText; - } - else if(type == 'image' && _.isArray(image) && status == 'finish') { - const imageText = image.reduce((imgs, v) => imgs + (/^(http|https):\/\//.test(v.image_url) ? `![图像](${v.image_url || ''})` : ''), '') + '\n'; + } else if ( + type == "image" && + _.isArray(image) && + status == "finish" + ) { + const imageText = + image.reduce( + (imgs, v) => + imgs + + (/^(http|https):\/\//.test(v.image_url) + ? `![图像](${v.image_url || ""})` + : ""), + "" + ) + "\n"; textOffset += imageText.length; toolCall = true; return innerStr + imageText; - } - else if(type == 'code' && partStatus == 'init') { - let codeHead = ''; - if(!codeGenerating) { + } else if (type == "code" && partStatus == "init") { + let codeHead = ""; + if (!codeGenerating) { codeGenerating = true; - codeHead = '```python\n'; + codeHead = "```python\n"; } const chunk = code.substring(codeTemp.length, code.length); codeTemp += chunk; textOffset += codeHead.length + chunk.length; return innerStr + codeHead + chunk; - } - else if(type == 'code' && partStatus == 'finish' && codeGenerating) { - const codeFooter = '\n```\n'; + } else if ( + type == "code" && + partStatus == "finish" && + codeGenerating + ) { + const codeFooter = "\n```\n"; codeGenerating = false; - codeTemp = ''; + codeTemp = ""; textOffset += codeFooter.length; return innerStr + codeFooter; - } - else if(type == 'execution_output' && _.isString(content) && partStatus == 'done' && lastExecutionOutput != content) { + } else if ( + type == "execution_output" && + _.isString(content) && + partStatus == "done" && + lastExecutionOutput != content + ) { lastExecutionOutput = content; - const _content = content.replace(/^\n/, ''); + const _content = content.replace(/^\n/, ""); textOffset += _content.length + 1; - return innerStr + _content + '\n'; + return innerStr + _content + "\n"; } return innerStr; - }, ''); + }, ""); return str + partText; - }, ''); - const chunk = text.substring(data.choices[0].message.content.length - textOffset, text.length); + }, ""); + const chunk = text.substring( + data.choices[0].message.content.length - textOffset, + text.length + ); data.choices[0].message.content += chunk; - } - else { - data.choices[0].message.content = data.choices[0].message.content.replace(/【\d+†source】/g, ''); + } else { + data.choices[0].message.content = + data.choices[0].message.content.replace(/【\d+†source】/g, ""); resolve(data); } - } - catch (err) { + } catch (err) { logger.error(err); reject(err); } }); // 将流数据喂给SSE转换器 - stream.on("data", buffer => parser.feed(buffer.toString())); - stream.once("error", err => reject(err)); + stream.on("data", (buffer) => parser.feed(buffer.toString())); + stream.once("error", (err) => reject(err)); stream.once("close", () => resolve(data)); }); } /** * 创建转换流 - * + * * 将流格式转换为gpt兼容流格式 - * + * * @param stream 消息流 * @param endCallback 传输结束回调 */ @@ -588,154 +798,261 @@ function createTransStream(stream: any, endCallback?: Function) { const created = util.unixTimestamp(); // 创建转换流 const transStream = new PassThrough(); - let content = ''; + let content = ""; let toolCall = false; let codeGenerating = false; let textChunkLength = 0; - let codeTemp = ''; - let lastExecutionOutput = ''; + let codeTemp = ""; + let lastExecutionOutput = ""; let textOffset = 0; - !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 => { + !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; // 解析JSON const result = _.attempt(() => JSON.parse(event.data)); if (_.isError(result)) throw new Error(`Stream response invalid: ${event.data}`); - if(result.status != 'finish' && result.status != 'intervene') { + if (result.status != "finish" && result.status != "intervene") { const text = result.parts.reduce((str, part) => { const { status, content, meta_data } = part; - if(!_.isArray(content)) - return str; + if (!_.isArray(content)) return str; const partText = content.reduce((innerStr, value) => { - const { status: partStatus, type, text, image, code, content } = value; - if(partStatus == 'init' && textChunkLength > 0) { + const { + status: partStatus, + type, + text, + image, + code, + content, + } = value; + if (partStatus == "init" && textChunkLength > 0) { textOffset += textChunkLength + 1; textChunkLength = 0; - innerStr += '\n'; + innerStr += "\n"; } - if(type == 'text') { - if(toolCall) { - innerStr += '\n'; + if (type == "text") { + if (toolCall) { + innerStr += "\n"; textOffset++; toolCall = false; } - if(partStatus == 'finish') - textChunkLength = text.length; + if (partStatus == "finish") textChunkLength = text.length; return innerStr + text; - } - else if(type == 'quote_result' && status == 'finish' && meta_data && _.isArray(meta_data.metadata_list)) { - const searchText = meta_data.metadata_list.reduce((meta, v) => meta + `检索 ${v.title}(${v.url}) ...`, '') + '\n'; + } else if ( + type == "quote_result" && + status == "finish" && + meta_data && + _.isArray(meta_data.metadata_list) + ) { + const searchText = + meta_data.metadata_list.reduce( + (meta, v) => meta + `检索 ${v.title}(${v.url}) ...`, + "" + ) + "\n"; textOffset += searchText.length; toolCall = true; return innerStr + searchText; - } - else if(type == 'image' && _.isArray(image) && status == 'finish') { - const imageText = image.reduce((imgs, v) => imgs + (/^(http|https):\/\//.test(v.image_url) ? `![图像](${v.image_url || ''})` : ''), '') + '\n'; + } else if ( + type == "image" && + _.isArray(image) && + status == "finish" + ) { + const imageText = + image.reduce( + (imgs, v) => + imgs + + (/^(http|https):\/\//.test(v.image_url) + ? `![图像](${v.image_url || ""})` + : ""), + "" + ) + "\n"; textOffset += imageText.length; toolCall = true; return innerStr + imageText; - } - else if(type == 'code' && partStatus == 'init') { - let codeHead = ''; - if(!codeGenerating) { + } else if (type == "code" && partStatus == "init") { + let codeHead = ""; + if (!codeGenerating) { codeGenerating = true; - codeHead = '```python\n'; + codeHead = "```python\n"; } const chunk = code.substring(codeTemp.length, code.length); codeTemp += chunk; textOffset += codeHead.length + chunk.length; return innerStr + codeHead + chunk; - } - else if(type == 'code' && partStatus == 'finish' && codeGenerating) { - const codeFooter = '\n```\n'; + } else if ( + type == "code" && + partStatus == "finish" && + codeGenerating + ) { + const codeFooter = "\n```\n"; codeGenerating = false; - codeTemp = ''; + codeTemp = ""; textOffset += codeFooter.length; return innerStr + codeFooter; - } - else if(type == 'execution_output' && _.isString(content) && partStatus == 'done' && lastExecutionOutput != content) { + } else if ( + type == "execution_output" && + _.isString(content) && + partStatus == "done" && + lastExecutionOutput != content + ) { lastExecutionOutput = content; textOffset += content.length + 1; - return innerStr + content + '\n'; + return innerStr + content + "\n"; } return innerStr; - }, ''); + }, ""); return str + partText; - }, ''); + }, ""); const chunk = text.substring(content.length - textOffset, text.length); - if(chunk) { + if (chunk) { content += chunk; const data = `data: ${JSON.stringify({ id: result.conversation_id, model: MODEL_NAME, - object: 'chat.completion.chunk', + object: "chat.completion.chunk", choices: [ - { index: 0, delta: { content: chunk }, finish_reason: null } + { index: 0, delta: { content: chunk }, finish_reason: null }, ], - created + created, })}\n\n`; !transStream.closed && transStream.write(data); } - } - else { + } else { const data = `data: ${JSON.stringify({ id: result.conversation_id, model: MODEL_NAME, - object: 'chat.completion.chunk', + object: "chat.completion.chunk", choices: [ { index: 0, - delta: result.status == 'intervene' && result.last_error && result.last_error.intervene_text ? { content: `\n\n${result.last_error.intervene_text}` } : {}, - finish_reason: 'stop' - } + delta: + result.status == "intervene" && + result.last_error && + result.last_error.intervene_text + ? { content: `\n\n${result.last_error.intervene_text}` } + : {}, + finish_reason: "stop", + }, ], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - created + created, })}\n\n`; !transStream.closed && transStream.write(data); - !transStream.closed && transStream.end('data: [DONE]\n\n'); - content = ''; + !transStream.closed && transStream.end("data: [DONE]\n\n"); + content = ""; endCallback && endCallback(result.conversation_id); } - } - catch (err) { + } catch (err) { logger.error(err); - !transStream.closed && transStream.end('\n\n'); + !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.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; } +/** + * 从流接收图像 + * + * @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; + // 解析JSON + const result = _.attempt(() => JSON.parse(event.data)); + if (_.isError(result)) + throw new Error(`Stream response invalid: ${event.data}`); + if (!convId && result.conversation_id) + convId = result.conversation_id; + if(result.status == "intervene") + throw new APIException(EX.API_CONTENT_FILTERED); + if (result.status != "finish") { + result.parts.forEach(part => { + const { content } = part; + if (!_.isArray(content)) return; + content.forEach(value => { + const { + status: partStatus, + type, + image + } = value; + if ( + type == "image" && + _.isArray(image) && + partStatus == "finish" + ) { + image.forEach((value) => { + if (!/^(http|https):\/\//.test(value.image_url) || imageUrls.indexOf(value.image_url) != -1) + return; + imageUrls.push(value.image_url); + }); + } + }); + }); + } + } 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 + })); + }); +} + /** * Token切分 - * + * * @param authorization 认证字符串 */ function tokenSplit(authorization: string) { - return authorization.replace('Bearer ', '').split(','); + return authorization.replace("Bearer ", "").split(","); } /** * 备用生成cookie - * + * * 暂时还不需要 - * - * @param refreshToken - * @param token + * + * @param refreshToken + * @param token */ function generateCookie(refreshToken: string, token: string) { const timestamp = util.unixTimestamp(); @@ -745,14 +1062,15 @@ function generateCookie(refreshToken: string, token: string) { // chatglm_user_id: '', _ga_PMD05MS2V9: `GS1.1.${gsTimestamp}.18.0.${gsTimestamp}.0.0.0`, chatglm_token: token, - chatglm_token_expires: util.getDateString('yyyy-MM-dd HH:mm:ss'), - abtestid: 'a', + chatglm_token_expires: util.getDateString("yyyy-MM-dd HH:mm:ss"), + abtestid: "a", // acw_tc: '' - } + }; } export default { createCompletion, createCompletionStream, - tokenSplit + generateImages, + tokenSplit, }; diff --git a/src/api/routes/images.ts b/src/api/routes/images.ts new file mode 100644 index 0000000..ba67cbf --- /dev/null +++ b/src/api/routes/images.ts @@ -0,0 +1,39 @@ +import _ from "lodash"; + +import Request from "@/lib/request/Request.ts"; +import chat from "@/api/controllers/chat.ts"; +import util from "@/lib/util.ts"; + +export default { + prefix: "/v1/images", + + post: { + "/generations": async (request: Request) => { + request + .validate("body.prompt", _.isString) + .validate("headers.authorization", _.isString); + // refresh_token切分 + const tokens = chat.tokenSplit(request.headers.authorization); + // 随机挑选一个refresh_token + const token = _.sample(tokens); + const prompt = request.body.prompt; + const responseFormat = _.defaultTo(request.body.response_format, "url"); + const model = request.body.model; + const imageUrls = await chat.generateImages(model, prompt, token); + let data = []; + if (responseFormat == "b64_json") { + data = ( + await Promise.all(imageUrls.map((url) => util.fetchFileBASE64(url))) + ).map((b64) => ({ b64_json: b64 })); + } else { + data = imageUrls.map((url) => ({ + url, + })); + } + return { + created: util.unixTimestamp(), + data, + }; + }, + }, +}; diff --git a/src/api/routes/index.ts b/src/api/routes/index.ts index 2e44386..814a995 100644 --- a/src/api/routes/index.ts +++ b/src/api/routes/index.ts @@ -1,7 +1,9 @@ import chat from "./chat.ts"; +import images from "./images.ts"; import ping from "./ping.ts"; export default [ chat, + images, ping ]; \ No newline at end of file diff --git a/src/lib/util.ts b/src/lib/util.ts index c15706c..0f3fd16 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -1,258 +1,307 @@ -import os from 'os'; -import path from 'path'; -import crypto from 'crypto'; -import { Readable, Writable } from 'stream'; +import os from "os"; +import path from "path"; +import crypto from "crypto"; +import { Readable, Writable } from "stream"; -import 'colors'; -import mime from 'mime'; -import fs from 'fs-extra'; -import { v1 as uuid } from 'uuid'; -import { format as dateFormat } from 'date-fns'; -import CRC32 from 'crc-32'; -import randomstring from 'randomstring'; -import _ from 'lodash'; -import { CronJob } from 'cron'; +import "colors"; +import mime from "mime"; +import axios from "axios"; +import fs from "fs-extra"; +import { v1 as uuid } from "uuid"; +import { format as dateFormat } from "date-fns"; +import CRC32 from "crc-32"; +import randomstring from "randomstring"; +import _ from "lodash"; +import { CronJob } from "cron"; -import HTTP_STATUS_CODE from './http-status-codes.ts'; +import HTTP_STATUS_CODE from "./http-status-codes.ts"; const autoIdMap = new Map(); const util = { + is2DArrays(value: any) { + return ( + _.isArray(value) && + (!value[0] || (_.isArray(value[0]) && _.isArray(value[value.length - 1]))) + ); + }, - is2DArrays(value: any) { - return _.isArray(value) && (!value[0] || (_.isArray(value[0]) && _.isArray(value[value.length - 1]))); - }, + uuid: (separator = true) => (separator ? uuid() : uuid().replace(/\-/g, "")), - uuid: (separator = true) => separator ? uuid() : uuid().replace(/\-/g, ""), + autoId: (prefix = "") => { + let index = autoIdMap.get(prefix); + if (index > 999999) index = 0; //超过最大数字则重置为0 + autoIdMap.set(prefix, (index || 0) + 1); + return `${prefix}${index || 1}`; + }, - autoId: (prefix = '') => { - let index = autoIdMap.get(prefix); - if(index > 999999) index = 0; //超过最大数字则重置为0 - autoIdMap.set(prefix, (index || 0) + 1); - return `${prefix}${index || 1}`; - }, + ignoreJSONParse(value: string) { + const result = _.attempt(() => JSON.parse(value)); + if (_.isError(result)) return null; + return result; + }, - ignoreJSONParse(value: string) { - const result = _.attempt(() => JSON.parse(value)); - if(_.isError(result)) - return null; - return result; - }, + generateRandomString(options: any): string { + return randomstring.generate(options); + }, - generateRandomString(options: any): string { - return randomstring.generate(options); - }, + getResponseContentType(value: any): string | null { + return value.headers + ? value.headers["content-type"] || value.headers["Content-Type"] + : null; + }, - getResponseContentType(value: any): string | null { - return value.headers ? (value.headers["content-type"] || value.headers["Content-Type"]) : null; - }, + mimeToExtension(value: string) { + let extension = mime.getExtension(value); + if (extension == "mpga") return "mp3"; + return extension; + }, - mimeToExtension(value: string) { - let extension = mime.getExtension(value); - if(extension == "mpga") - return "mp3"; - return extension; - }, + extractURLExtension(value: string) { + const extname = path.extname(new URL(value).pathname); + return extname.substring(1).toLowerCase(); + }, - extractURLExtension(value: string) { - const extname = path.extname(new URL(value).pathname); - return extname.substring(1).toLowerCase(); - }, + createCronJob(cronPatterns: any, callback?: Function) { + if (!_.isFunction(callback)) + throw new Error("callback must be an Function"); + return new CronJob( + cronPatterns, + () => callback(), + null, + false, + "Asia/Shanghai" + ); + }, - createCronJob(cronPatterns: any, callback?: Function) { - if(!_.isFunction(callback)) throw new Error("callback must be an Function"); - return new CronJob(cronPatterns, () => callback(), null, false, "Asia/Shanghai"); - }, + getDateString(format = "yyyy-MM-dd", date = new Date()) { + return dateFormat(date, format); + }, - getDateString(format = "yyyy-MM-dd", date = new Date()) { - return dateFormat(date, format); - }, + getIPAddressesByIPv4(): string[] { + const interfaces = os.networkInterfaces(); + const addresses = []; + for (let name in interfaces) { + const networks = interfaces[name]; + const results = networks.filter( + (network) => + network.family === "IPv4" && + network.address !== "127.0.0.1" && + !network.internal + ); + if (results[0] && results[0].address) addresses.push(results[0].address); + } + return addresses; + }, - getIPAddressesByIPv4(): string[] { - const interfaces = os.networkInterfaces(); - const addresses = []; - for (let name in interfaces) { - const networks = interfaces[name]; - const results = networks.filter(network => network.family === "IPv4" && network.address !== "127.0.0.1" && !network.internal); - if (results[0] && results[0].address) - addresses.push(results[0].address); - } - return addresses; - }, + getMACAddressesByIPv4(): string[] { + const interfaces = os.networkInterfaces(); + const addresses = []; + for (let name in interfaces) { + const networks = interfaces[name]; + const results = networks.filter( + (network) => + network.family === "IPv4" && + network.address !== "127.0.0.1" && + !network.internal + ); + if (results[0] && results[0].mac) addresses.push(results[0].mac); + } + return addresses; + }, - getMACAddressesByIPv4(): string[] { - const interfaces = os.networkInterfaces(); - const addresses = []; - for (let name in interfaces) { - const networks = interfaces[name]; - const results = networks.filter(network => network.family === "IPv4" && network.address !== "127.0.0.1" && !network.internal); - if (results[0] && results[0].mac) - addresses.push(results[0].mac); - } - return addresses; - }, + generateSSEData(event?: string, data?: string, retry?: number) { + return `event: ${event || "message"}\ndata: ${(data || "") + .replace(/\n/g, "\\n") + .replace(/\s/g, "\\s")}\nretry: ${retry || 3000}\n\n`; + }, - generateSSEData(event?: string, data?: string, retry?: number) { - return `event: ${event || "message"}\ndata: ${(data || "").replace(/\n/g, "\\n").replace(/\s/g, "\\s")}\nretry: ${retry || 3000}\n\n`; - }, + buildDataBASE64(type, ext, buffer) { + return `data:${type}/${ext.replace("jpg", "jpeg")};base64,${buffer.toString( + "base64" + )}`; + }, - buildDataBASE64(type, ext, buffer) { - return `data:${type}/${ext.replace("jpg", "jpeg")};base64,${buffer.toString("base64")}`; - }, + isLinux() { + return os.platform() !== "win32"; + }, - isLinux() { - return os.platform() !== "win32"; - }, - - isIPAddress(value) { - return _.isString(value) && (/^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/.test(value) || /\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*/.test(value)); - }, + isIPAddress(value) { + return ( + _.isString(value) && + (/^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/.test( + value + ) || + /\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*/.test( + value + )) + ); + }, - isPort(value) { - return _.isNumber(value) && value > 0 && value < 65536; - }, + isPort(value) { + return _.isNumber(value) && value > 0 && value < 65536; + }, - isReadStream(value): boolean { - return value && (value instanceof Readable || "readable" in value || value.readable); - }, + isReadStream(value): boolean { + return ( + value && + (value instanceof Readable || "readable" in value || value.readable) + ); + }, - isWriteStream(value): boolean { - return value && (value instanceof Writable || "writable" in value || value.writable); - }, + isWriteStream(value): boolean { + return ( + value && + (value instanceof Writable || "writable" in value || value.writable) + ); + }, - isHttpStatusCode(value) { - return _.isNumber(value) && Object.values(HTTP_STATUS_CODE).includes(value); - }, + isHttpStatusCode(value) { + return _.isNumber(value) && Object.values(HTTP_STATUS_CODE).includes(value); + }, - isURL(value) { - return !_.isUndefined(value) && /^(http|https)/.test(value); - }, + isURL(value) { + return !_.isUndefined(value) && /^(http|https)/.test(value); + }, - isSrc(value) { - return !_.isUndefined(value) && /^\/.+\.[0-9a-zA-Z]+(\?.+)?$/.test(value); - }, + isSrc(value) { + return !_.isUndefined(value) && /^\/.+\.[0-9a-zA-Z]+(\?.+)?$/.test(value); + }, - isBASE64(value) { - return !_.isUndefined(value) && /^[a-zA-Z0-9\/\+]+(=?)+$/.test(value); - }, + isBASE64(value) { + return !_.isUndefined(value) && /^[a-zA-Z0-9\/\+]+(=?)+$/.test(value); + }, - isBASE64Data(value) { - return /^data:/.test(value); - }, + isBASE64Data(value) { + return /^data:/.test(value); + }, - extractBASE64DataFormat(value): string | null { - const match = value.trim().match(/^data:(.+);base64,/); - if(!match) return null; - return match[1]; - }, + extractBASE64DataFormat(value): string | null { + const match = value.trim().match(/^data:(.+);base64,/); + if (!match) return null; + return match[1]; + }, - removeBASE64DataHeader(value): string { - return value.replace(/^data:(.+);base64,/, ""); - }, + removeBASE64DataHeader(value): string { + return value.replace(/^data:(.+);base64,/, ""); + }, - isDataString(value): boolean { - return /^(base64|json):/.test(value); - }, + isDataString(value): boolean { + return /^(base64|json):/.test(value); + }, - isStringNumber(value) { - return _.isFinite(Number(value)); - }, + isStringNumber(value) { + return _.isFinite(Number(value)); + }, - isUnixTimestamp(value) { - return /^[0-9]{10}$/.test(`${value}`); - }, + isUnixTimestamp(value) { + return /^[0-9]{10}$/.test(`${value}`); + }, - isTimestamp(value) { - return /^[0-9]{13}$/.test(`${value}`); - }, + isTimestamp(value) { + return /^[0-9]{13}$/.test(`${value}`); + }, - isEmail(value) { - return /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/.test(value); - }, + isEmail(value) { + return /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/.test( + value + ); + }, - isAsyncFunction(value) { - return Object.prototype.toString.call(value) === "[object AsyncFunction]"; - }, + isAsyncFunction(value) { + return Object.prototype.toString.call(value) === "[object AsyncFunction]"; + }, - async isAPNG(filePath) { - let head; - const readStream = fs.createReadStream(filePath, { start: 37, end: 40 }); - const readPromise = new Promise((resolve, reject) => { - readStream.once("end", resolve); - readStream.once("error", reject); - }); - readStream.once("data", data => head = data); - await readPromise; - return head.compare(Buffer.from([0x61, 0x63, 0x54, 0x4c])) === 0; - }, + async isAPNG(filePath) { + let head; + const readStream = fs.createReadStream(filePath, { start: 37, end: 40 }); + const readPromise = new Promise((resolve, reject) => { + readStream.once("end", resolve); + readStream.once("error", reject); + }); + readStream.once("data", (data) => (head = data)); + await readPromise; + return head.compare(Buffer.from([0x61, 0x63, 0x54, 0x4c])) === 0; + }, - unixTimestamp() { - return parseInt(`${Date.now() / 1000}`); - }, + unixTimestamp() { + return parseInt(`${Date.now() / 1000}`); + }, - timestamp() { - return Date.now(); - }, + timestamp() { + return Date.now(); + }, - urlJoin(...values) { - let url = ""; - for (let i = 0; i < values.length; i++) - url += `${i > 0 ? "/" : ""}${values[i].replace(/^\/*/, "").replace(/\/*$/, "")}`; - return url; - }, + urlJoin(...values) { + let url = ""; + for (let i = 0; i < values.length; i++) + url += `${i > 0 ? "/" : ""}${values[i] + .replace(/^\/*/, "") + .replace(/\/*$/, "")}`; + return url; + }, - millisecondsToHmss(milliseconds) { - if (_.isString(milliseconds)) return milliseconds; - milliseconds = parseInt(milliseconds); - const sec = Math.floor(milliseconds / 1000); - const hours = Math.floor(sec / 3600); - const minutes = Math.floor((sec - hours * 3600) / 60); - const seconds = sec - hours * 3600 - minutes * 60; - const ms = milliseconds % 60000 - seconds * 1000; - return `${hours > 9 ? hours : "0" + hours}:${minutes > 9 ? minutes : "0" + minutes}:${seconds > 9 ? seconds : "0" + seconds}.${ms}`; - }, + millisecondsToHmss(milliseconds) { + if (_.isString(milliseconds)) return milliseconds; + milliseconds = parseInt(milliseconds); + const sec = Math.floor(milliseconds / 1000); + const hours = Math.floor(sec / 3600); + const minutes = Math.floor((sec - hours * 3600) / 60); + const seconds = sec - hours * 3600 - minutes * 60; + const ms = (milliseconds % 60000) - seconds * 1000; + return `${hours > 9 ? hours : "0" + hours}:${ + minutes > 9 ? minutes : "0" + minutes + }:${seconds > 9 ? seconds : "0" + seconds}.${ms}`; + }, - millisecondsToTimeString(milliseconds) { - if(milliseconds < 1000) - return `${milliseconds}ms`; - if(milliseconds < 60000) - return `${parseFloat((milliseconds / 1000).toFixed(2))}s`; - return `${Math.floor(milliseconds / 1000 / 60)}m${Math.floor(milliseconds / 1000 % 60)}s`; - }, + millisecondsToTimeString(milliseconds) { + if (milliseconds < 1000) return `${milliseconds}ms`; + if (milliseconds < 60000) + return `${parseFloat((milliseconds / 1000).toFixed(2))}s`; + return `${Math.floor(milliseconds / 1000 / 60)}m${Math.floor( + (milliseconds / 1000) % 60 + )}s`; + }, - rgbToHex(r, g, b): string { - return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); - }, + rgbToHex(r, g, b): string { + return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + }, - hexToRgb(hex) { - const value = parseInt(hex.replace(/^#/, ""), 16); - return [(value >> 16) & 255, (value >> 8) & 255, value & 255]; - }, + hexToRgb(hex) { + const value = parseInt(hex.replace(/^#/, ""), 16); + return [(value >> 16) & 255, (value >> 8) & 255, value & 255]; + }, - md5(value) { - return crypto.createHash("md5").update(value).digest("hex"); - }, + md5(value) { + return crypto.createHash("md5").update(value).digest("hex"); + }, - crc32(value) { - return _.isBuffer(value) ? CRC32.buf(value) : CRC32.str(value); - }, + crc32(value) { + return _.isBuffer(value) ? CRC32.buf(value) : CRC32.str(value); + }, - arrayParse(value): any[] { - return _.isArray(value) ? value : [value]; - }, + arrayParse(value): any[] { + return _.isArray(value) ? value : [value]; + }, - booleanParse(value) { - return value === "true" || value === true ? true : false - }, + booleanParse(value) { + return value === "true" || value === true ? true : false; + }, - encodeBASE64(value) { - return Buffer.from(value).toString("base64"); - }, + encodeBASE64(value) { + return Buffer.from(value).toString("base64"); + }, - decodeBASE64(value) { - return Buffer.from(value, "base64").toString(); - }, + decodeBASE64(value) { + return Buffer.from(value, "base64").toString(); + }, + async fetchFileBASE64(url: string) { + const result = await axios.get(url, { + responseType: "arraybuffer", + }); + return result.data.toString("base64"); + }, }; -export default util; \ No newline at end of file +export default util;