360 lines
14 KiB
TypeScript
360 lines
14 KiB
TypeScript
import OpenAI from 'openai';
|
||
import { ComponentService } from '@/lib/services/component-service';
|
||
import { ConversationService } from '@/lib/services/conversation-service';
|
||
import { Component } from "@prisma/client";
|
||
|
||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||
const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL;
|
||
|
||
if (!OPENAI_API_KEY) {
|
||
throw new Error("OPENAI_API_KEY is not set");
|
||
}
|
||
|
||
// 定义消息类型(兼容OpenAI格式)
|
||
type MessageParam = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
||
type FunctionCall = OpenAI.Chat.Completions.ChatCompletionMessageToolCall;
|
||
type Message = OpenAI.Chat.Completions.ChatCompletionMessage;
|
||
|
||
|
||
|
||
export class AIClient {
|
||
private openai: OpenAI;
|
||
private tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
|
||
{
|
||
type: "function",
|
||
function: {
|
||
name: "query-components",
|
||
description: "查询PC配件信息,支持按类型、品牌、价格范围、关键词等条件搜索。单次最多返回5个配件。",
|
||
parameters: {
|
||
type: "object",
|
||
properties: {
|
||
type: {
|
||
type: "string",
|
||
enum: ["CPU", "内存", "硬盘", "主板", "显卡", "机箱"],
|
||
description: "配件类型"
|
||
},
|
||
brand: {
|
||
type: "string",
|
||
description: "品牌名称"
|
||
},
|
||
minPrice: {
|
||
type: "number",
|
||
description: "最低价格"
|
||
},
|
||
maxPrice: {
|
||
type: "number",
|
||
description: "最高价格"
|
||
},
|
||
search: {
|
||
type: "string",
|
||
description: "搜索关键词,会在名称、品牌、型号、描述中搜索"
|
||
},
|
||
page: {
|
||
type: "integer",
|
||
description: "页码,默认为1"
|
||
},
|
||
}
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: "function",
|
||
function: {
|
||
name: "show-components",
|
||
description: "根据提供的配件ID列表,向用户以卡片形式展示一个或多个具体型号的配件。如果你提到了某些特定配件,请用此工具更好地向用户展示。",
|
||
parameters: {
|
||
type: "object",
|
||
properties: {
|
||
component_ids: {
|
||
type: "array",
|
||
description: "一个包含一个或多个要展示的配件ID的数组。这些ID来自`query-components`工具的查询结果。",
|
||
items: {
|
||
type: "string",
|
||
description: "单个配件的唯一ID"
|
||
}
|
||
}
|
||
},
|
||
required: ["component_ids"]
|
||
}
|
||
}
|
||
}
|
||
];
|
||
|
||
constructor() {
|
||
this.openai = new OpenAI({
|
||
apiKey: OPENAI_API_KEY,
|
||
baseURL: OPENAI_BASE_URL || "https://api.openai.com/v1"
|
||
});
|
||
}
|
||
|
||
// 查询配件
|
||
private async queryComponents(args: any): Promise<string> {
|
||
try {
|
||
const result = await ComponentService.queryComponents({
|
||
type: args.type,
|
||
brand: args.brand,
|
||
minPrice: args.minPrice,
|
||
maxPrice: args.maxPrice,
|
||
search: args.search,
|
||
page: args.page || 1,
|
||
limit: 5
|
||
})
|
||
|
||
if (result.components.length === 0) {
|
||
return "未找到符合条件的配件"
|
||
}
|
||
|
||
// 返回格式化的结果
|
||
return JSON.stringify(result.components.map(c => ({
|
||
id: c.id,
|
||
name: c.name,
|
||
brand: c.brand,
|
||
model: c.model,
|
||
price: c.price,
|
||
description: c.description,
|
||
stock: c.stock,
|
||
specifications: c.specifications,
|
||
})))
|
||
|
||
} catch (error) {
|
||
console.error('查询配件失败:', error)
|
||
return `查询配件时发生错误: ${error instanceof Error ? error.message : '未知错误'}`
|
||
}
|
||
}
|
||
|
||
|
||
async *processQuery(
|
||
query: string,
|
||
userId: string,
|
||
conversationId?: string
|
||
): AsyncGenerator<string, void, unknown> {
|
||
let messages: MessageParam[];
|
||
let currentConversationId = conversationId;
|
||
|
||
// 如果提供了对话ID,加载现有对话
|
||
if (conversationId) {
|
||
const conversation = await ConversationService.getConversation(conversationId, userId);
|
||
if (conversation) {
|
||
messages = [...conversation.messages as MessageParam[]];
|
||
// 添加新的用户消息
|
||
messages.push({
|
||
role: "user",
|
||
content: query
|
||
});
|
||
} else {
|
||
throw new Error('对话不存在或无权限访问');
|
||
}
|
||
} else {
|
||
// 创建新对话
|
||
currentConversationId = await ConversationService.createConversation(userId, query);
|
||
|
||
// 发送新对话ID给前端
|
||
yield `conversation_id:${currentConversationId}`;
|
||
|
||
// 获取新创建的对话消息
|
||
const conversation = await ConversationService.getConversation(currentConversationId, userId);
|
||
if (!conversation) {
|
||
throw new Error('创建对话失败');
|
||
}
|
||
messages = conversation.messages as MessageParam[];
|
||
}
|
||
|
||
try {
|
||
let maxIterations = 10; // 防止无限循环
|
||
let currentIteration = 0;
|
||
|
||
while (currentIteration < maxIterations) {
|
||
currentIteration++;
|
||
console.log("Send Req", messages);
|
||
|
||
const stream = await this.openai.chat.completions.create({
|
||
model: ["web-rev-claude-sonnet-4-20250514", "gemini-2.5-flash"][1],
|
||
max_tokens: 2048,
|
||
messages,
|
||
tools: this.tools,
|
||
stream: true,
|
||
});
|
||
|
||
let assistantMessage: MessageParam = {
|
||
role: "assistant",
|
||
content: "",
|
||
tool_calls: []
|
||
};
|
||
|
||
// 处理流式响应
|
||
for await (const chunk of stream) {
|
||
const delta = chunk.choices[0]?.delta;
|
||
if (!delta) continue;
|
||
|
||
// 处理文本内容
|
||
if (delta.content) {
|
||
assistantMessage.content += delta.content;
|
||
yield delta.content;
|
||
}
|
||
|
||
// 处理工具调用
|
||
if (delta.tool_calls) {
|
||
for (const toolCall of delta.tool_calls) {
|
||
const index = toolCall.index;
|
||
|
||
// 初始化工具调用
|
||
if (!assistantMessage.tool_calls) {
|
||
assistantMessage.tool_calls = [];
|
||
}
|
||
|
||
if (!assistantMessage.tool_calls[index]) {
|
||
assistantMessage.tool_calls[index] = {
|
||
id: toolCall.id || "",
|
||
type: "function",
|
||
function: {
|
||
name: toolCall.function?.name || "",
|
||
arguments: ""
|
||
}
|
||
};
|
||
yield `\n\n**使用工具**: ${toolCall.function?.name}\n\n\`\`\`\n`;
|
||
}
|
||
|
||
// 累积工具调用参数
|
||
if (toolCall.function?.arguments) {
|
||
assistantMessage.tool_calls[index].function.arguments += toolCall.function.arguments;
|
||
yield toolCall.function.arguments;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果有工具调用,结束代码块
|
||
if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
|
||
yield `\n\`\`\``;
|
||
}
|
||
|
||
// 将助手消息添加到对话中
|
||
messages.push(assistantMessage);
|
||
|
||
// 如果没有工具调用,结束循环
|
||
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
||
break;
|
||
}
|
||
|
||
// 处理工具调用
|
||
let needRerun = false;
|
||
for (const toolCall of assistantMessage.tool_calls) {
|
||
if (!toolCall.function.name) continue;
|
||
|
||
let toolArgs: any = {};
|
||
try {
|
||
toolArgs = JSON.parse(toolCall.function.arguments);
|
||
} catch (error) {
|
||
console.error('解析工具参数失败:', error);
|
||
continue;
|
||
}
|
||
|
||
let toolResult = "";
|
||
|
||
switch (toolCall.function.name) {
|
||
case "query-components":
|
||
toolResult = await this.queryComponents(toolArgs);
|
||
needRerun = true;
|
||
yield 'next_block';
|
||
break;
|
||
|
||
case "show-components":
|
||
const components = await ComponentService.getComponentsByIds(toolArgs.component_ids);
|
||
toolResult = `成功向用户展示了 ${components.map(c => c?.id).join(',')}`;
|
||
needRerun = true;
|
||
yield `show_card: ${JSON.stringify(components)}`;
|
||
break;
|
||
|
||
default:
|
||
console.error(`未知工具调用: ${toolCall.function.name}`);
|
||
toolResult = `未知工具: ${toolCall.function.name}`;
|
||
}
|
||
|
||
// 添加工具调用结果消息
|
||
messages.push({
|
||
role: "tool",
|
||
content: toolResult,
|
||
tool_call_id: toolCall.id
|
||
});
|
||
}
|
||
|
||
if (!needRerun) break;
|
||
}
|
||
|
||
if (currentIteration >= maxIterations) {
|
||
yield "\n\n⚠️ 达到最大处理轮次,对话结束。";
|
||
} // 保存更新后的对话到数据库
|
||
if (currentConversationId) {
|
||
// 转换消息格式以兼容现有的ConversationService
|
||
const compatibleMessages = messages.map((msg): any => {
|
||
if (msg.role === "tool") {
|
||
// 将工具消息转换为用户消息格式以保持兼容性
|
||
return {
|
||
role: "user",
|
||
content: `Tool result: ${msg.content}`
|
||
};
|
||
}
|
||
return {
|
||
role: msg.role,
|
||
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
|
||
};
|
||
});
|
||
await ConversationService.updateConversationMessages(currentConversationId, userId, compatibleMessages);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('处理查询时发生错误:', error);
|
||
yield `\n\n❌ 抱歉,处理您的请求时遇到了问题: ${error instanceof Error ? error.message : '未知错误'}`; // 即使出错也要保存对话
|
||
if (currentConversationId) {
|
||
try {
|
||
const compatibleMessages = messages.map((msg): any => {
|
||
if (msg.role === "tool") {
|
||
return {
|
||
role: "user",
|
||
content: `Tool result: ${msg.content}`
|
||
};
|
||
}
|
||
return {
|
||
role: msg.role,
|
||
content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
|
||
};
|
||
});
|
||
await ConversationService.updateConversationMessages(currentConversationId, userId, compatibleMessages);
|
||
} catch (saveError) {
|
||
console.error('保存对话失败:', saveError);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户的对话历史
|
||
*/
|
||
async getUserConversations(userId: string, page = 1, limit = 20) {
|
||
return await ConversationService.getUserConversations(userId, page, limit);
|
||
}
|
||
|
||
/**
|
||
* 获取特定对话的详情
|
||
*/
|
||
async getConversation(conversationId: string, userId: string) {
|
||
return await ConversationService.getConversation(conversationId, userId);
|
||
}
|
||
|
||
/**
|
||
* 删除对话
|
||
*/
|
||
async deleteConversation(conversationId: string, userId: string) {
|
||
return await ConversationService.deleteConversation(conversationId, userId);
|
||
}
|
||
|
||
/**
|
||
* 更新对话标题
|
||
*/
|
||
async updateConversationTitle(conversationId: string, userId: string, title: string) {
|
||
return await ConversationService.updateConversationTitle(conversationId, userId, title);
|
||
}
|
||
|
||
}
|
||
|