csdesign-pc-diy-store/lib/ai-assistant-openai.ts
2025-06-24 14:09:12 +08:00

360 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}