From b6601be653680d6960f7404e4c702c0f8151a4a6 Mon Sep 17 00:00:00 2001 From: lichunn <269031597@qq.com> Date: Sat, 17 May 2025 16:28:28 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat=EF=BC=9A=E9=87=8D=E6=9E=84AI?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controller/app-center/aiChat.ts | 38 +++- app/router/appCenter/base.ts | 1 + app/service/app-center/aiChat.ts | 293 ++++++++++++---------------- package.json | 8 +- 4 files changed, 164 insertions(+), 176 deletions(-) diff --git a/app/controller/app-center/aiChat.ts b/app/controller/app-center/aiChat.ts index 350af76..58a8d04 100644 --- a/app/controller/app-center/aiChat.ts +++ b/app/controller/app-center/aiChat.ts @@ -10,7 +10,6 @@ * */ import { Controller } from 'egg'; -import { E_FOUNDATION_MODEL } from '../../lib/enum'; export default class AiChatController extends Controller { public async aiChat() { @@ -20,11 +19,33 @@ export default class AiChatController extends Controller { if (!messages || !Array.isArray(messages)) { return this.ctx.helper.getResponseData('Not passing the correct message parameter'); } - const model = foundationModel?.model ?? E_FOUNDATION_MODEL.GPT_35_TURBO; - const token = foundationModel.token; - ctx.body = await ctx.service.appCenter.aiChat.getAnswerFromAi(messages, { model, token }); - } + const apiKey = foundationModel?.apiKey; + const baseUrl = foundationModel?.baseUrl; + const model = foundationModel?.model; + const streamStatus = foundationModel?.stream || false; + + if (streamStatus) { + ctx.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive' + }); + await ctx.service.appCenter.aiChat.getAnswerFromAi( + messages, + { apiKey, baseUrl, model, streamStatus }, + ctx.res + ); + return; + } + // 非流式模式 + ctx.body = await ctx.service.appCenter.aiChat.getAnswerFromAi(messages, { + apiKey, + baseUrl, + model, + streamStatus + }); + } public async uploadFile() { const { ctx } = this; @@ -33,4 +54,11 @@ export default class AiChatController extends Controller { const { model, token } = foundationModelObject.foundationModel; ctx.body = await ctx.service.appCenter.aiChat.getFileContentFromAi(fileStream, { model, token }); } + + public async search() { + const { ctx } = this; + const { content } = ctx.request.body; + + ctx.body = await ctx.service.appCenter.aiChat.search(content); + } } diff --git a/app/router/appCenter/base.ts b/app/router/appCenter/base.ts index 2b7c273..a4dc910 100644 --- a/app/router/appCenter/base.ts +++ b/app/router/appCenter/base.ts @@ -114,4 +114,5 @@ export default (app: Application) => { // AI大模型聊天接口 subRouter.post('/ai/chat', controller.appCenter.aiChat.aiChat); subRouter.post('/ai/files', controller.appCenter.aiChat.uploadFile); + subRouter.post('/ai/search', controller.appCenter.aiChat.search); }; diff --git a/app/service/app-center/aiChat.ts b/app/service/app-center/aiChat.ts index 8ed5207..18e4f15 100644 --- a/app/service/app-center/aiChat.ts +++ b/app/service/app-center/aiChat.ts @@ -10,14 +10,16 @@ * */ import { Service } from 'egg'; -import { E_FOUNDATION_MODEL } from '../../lib/enum'; import * as fs from 'fs'; import * as path from 'path'; +import OpenApi, * as $OpenApi from '@alicloud/openapi-client'; +import OpenApiUtil from '@alicloud/openapi-util'; +import * as $Util from '@alicloud/tea-util'; +import Credential, { Config } from '@alicloud/credentials'; const to = require('await-to-js').default; const OpenAI = require('openai'); - export type AiMessage = { role: string; // 角色 name?: string; // 名称 @@ -34,182 +36,47 @@ export default class AiChat extends Service { /** * 获取ai的答复 * - * 根据后续引进的大模型情况决定,是否通过重构来对不同大模型进行统一的适配 - * * @param messages * @param model * @return */ - async getAnswerFromAi(messages: Array, chatConfig: any) { - let res = await this.requestAnswerFromAi(messages, chatConfig); - let answerContent = ''; - let isFinish = res.choices[0].finish_reason; + async getAnswerFromAi(messages: Array, chatConfig: any, res: any = null) { + let result: any = null; - if (isFinish !== 'length') { - answerContent = res.choices[0]?.message.content; - } - - // 若内容过长被截断,继续回复 - while (isFinish === 'length') { - const prefix = res.choices[0].message.content; - answerContent += prefix; - messages.push({ - role: 'assistant', - content: prefix, - partial: true + try { + const openai = new OpenAI({ + apiKey: chatConfig.apiKey || process.env.OPEN_AI_API_KEY, + baseURL: chatConfig.baseUrl || process.env.OPEN_AI_BASE_URL }); - res = await this.requestAnswerFromAi(messages, chatConfig); - answerContent += res.choices[0].message.content; - isFinish = res.choices[0].finish_reason; - } - - const code = this.extractCode(answerContent); - const schema = this.extractSchemaCode(code); - const answer = { - role: res.choices[0].message.role, - content: answerContent - }; - const replyWithoutCode = this.removeCode(answerContent); - return this.ctx.helper.getResponseData({ - originalResponse: answer, - replyWithoutCode, - schema - }); - } + result = await openai.chat.completions.create({ + model: chatConfig.model || process.env.OPEN_AI_MODEL, + messages, + stream: chatConfig.streamStatus + }); - async requestAnswerFromAi(messages: Array, chatConfig: any) { - const { ctx } = this; - this.formatMessage(messages); - let res: any = null; - try { - // 根据大模型的不同匹配不同的配置 - const aiChatConfig = this.config.aiChat(messages, chatConfig.token); - const { httpRequestUrl, httpRequestOption } = aiChatConfig[chatConfig.model]; - this.ctx.logger.debug(httpRequestOption); - res = await ctx.curl(httpRequestUrl, httpRequestOption); + // 逐块发送数据到前端 + if (chatConfig.streamStatus) { + for await (const chunk of result) { + const content = chunk.choices[0]?.delta?.content || ''; + res.write(`data: ${JSON.stringify({ content })}\n\n`); // SSE 格式 + } + } else { + return result; + } } catch (e: any) { this.ctx.logger.debug(`调用AI大模型接口失败: ${(e as Error).message}`); return this.ctx.helper.getResponseData(`调用AI大模型接口失败: ${(e as Error).message}`); + } finally { + if (res) { + res.end(); // 关闭连接 + } } if (!res) { return this.ctx.helper.getResponseData(`调用AI大模型接口未返回正确数据.`); } - - // 适配文心一言的响应数据结构,文心的部分异常情况status也是200,需要转为400,以免前端无所适从 - if (res.data?.error_code) { - return this.ctx.helper.getResponseData(res.data?.error_msg); - } - - // 适配chatgpt的响应数据结构 - if (res.status !== 200) { - return this.ctx.helper.getResponseData(res.data?.error?.message, res.status); - } - - // 适配文心一言的响应数据结构 - if (chatConfig.model === E_FOUNDATION_MODEL.ERNIE_BOT_TURBO) { - return { - ...res.data, - choices: [ - { - message: { - role: 'assistant', - content: res.data.result - } - } - ] - }; - } - - return res.data; - } - - /** - * 提取回复中的代码 - * - * 暂且只满足回复中只包括一个代码块的场景 - * - * @param content ai回复的内容 - * @return 提取的文本 - */ - private extractCode(content: string) { - const { start, end } = this.getStartAndEnd(content); - if (start < 0 || end < 0) { - return ''; - } - return content.substring(start, end); - } - - /** - * 去除回复中的代码 - * - * 暂且只满足回复中只包括一个代码块的场景 - * - * @param content ai回复的内容 - * @return 去除代码后的回复内容 - */ - private removeCode(content: string) { - const { start, end } = this.getStartAndEnd(content); - if (start < 0 || end < 0) { - return content; - } - return content.substring(0, start) + '<代码在画布中展示>' + content.substring(end); - } - - private extractSchemaCode(content) { - const startMarker = /```json/; - const endMarker = /```/; - - const start = content.search(startMarker); - const end = content.slice(start + 7).search(endMarker) + start + 7; - - if (start >= 0 && end >= 0) { - return JSON.parse(content.substring(start + 7, end).trim()); - } - - return null; - } - - private getStartAndEnd(str: string) { - const start = str.search(/```|