实现了AI助手
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk";
|
||||
import {
|
||||
Message,
|
||||
MessageParam,
|
||||
Tool
|
||||
} from "@anthropic-ai/sdk/resources/messages/messages.mjs";
|
||||
import { ComponentService } from '@/lib/services/component-service';
|
||||
import { ConversationService } from '@/lib/services/conversation-service';
|
||||
|
||||
|
||||
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
const ANTHROPIC_BASE_URL = process.env.ANTHROPIC_BASE_URL;
|
||||
if (!ANTHROPIC_API_KEY) {
|
||||
throw new Error("ANTHROPIC_API_KEY is not set");
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class AIClient {
|
||||
private anthropic: Anthropic;
|
||||
private tools: Tool[] = [
|
||||
{
|
||||
name: "query-components",
|
||||
description: "查询PC配件信息,支持按类型、品牌、价格范围、关键词等条件搜索。单次最多返回5个配件。",
|
||||
input_schema: {
|
||||
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"
|
||||
},
|
||||
}
|
||||
}
|
||||
}, {
|
||||
name: "show-components",
|
||||
description: "根据提供的配件ID列表,向用户以卡片形式展示一个或多个具体型号的配件。如果你提到了某些特定配件,请用此工具更好地向用户展示。",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
component_ids: {
|
||||
type: "array",
|
||||
description: "一个包含一个或多个要展示的配件ID的数组。这些ID来自`query-components`工具的查询结果。",
|
||||
items: {
|
||||
type: "string",
|
||||
description: "单个配件的唯一ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["component_ids"]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.anthropic = new Anthropic({
|
||||
apiKey: ANTHROPIC_API_KEY,
|
||||
baseURL: ANTHROPIC_BASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询配件
|
||||
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];
|
||||
// 添加新的用户消息
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
let maxIterations = 20;
|
||||
let currentIteration = 0;
|
||||
|
||||
while (currentIteration < maxIterations) {
|
||||
currentIteration++;
|
||||
console.log("Send Req", messages);
|
||||
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: ["claude-sonnet-4-20250514", "claude-3-5-haiku-20241022"][1],
|
||||
max_tokens: 2048,
|
||||
messages,
|
||||
tools: this.tools,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
let tempMsg: Message | null = null
|
||||
|
||||
// 处理流式响应
|
||||
for await (const chunk of response) {
|
||||
console.log(chunk);
|
||||
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
tempMsg = {
|
||||
id: chunk.message.id,
|
||||
type: chunk.message.type,
|
||||
role: chunk.message.role,
|
||||
content: [],
|
||||
model: chunk.message.model,
|
||||
usage: chunk.message.usage,
|
||||
stop_reason: chunk.message.stop_reason,
|
||||
stop_sequence: chunk.message.stop_sequence,
|
||||
}
|
||||
break
|
||||
|
||||
case "message_delta":
|
||||
tempMsg = {
|
||||
...tempMsg || {},
|
||||
stop_reason: chunk.delta.stop_reason,
|
||||
stop_sequence: chunk.delta.stop_sequence,
|
||||
// @ts-ignore
|
||||
usage: chunk.usage,
|
||||
}
|
||||
break
|
||||
|
||||
case "message_stop":
|
||||
break
|
||||
|
||||
case "content_block_start":
|
||||
const contentBlock = chunk.content_block;
|
||||
switch (contentBlock.type) {
|
||||
case "text":
|
||||
tempMsg?.content.push({
|
||||
type: contentBlock.type,
|
||||
text: contentBlock.text,
|
||||
citations: contentBlock.citations || [],
|
||||
})
|
||||
yield contentBlock.text
|
||||
break
|
||||
case "tool_use":
|
||||
tempMsg?.content.push({
|
||||
type: contentBlock.type,
|
||||
id: contentBlock.id,
|
||||
name: contentBlock.name,
|
||||
input: "",
|
||||
});
|
||||
yield `\n\n**使用工具**: ${contentBlock.name}\n\n\`\`\`\n`
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case "content_block_delta":
|
||||
const contentBlockDelta = chunk.delta;
|
||||
const targetContent = tempMsg?.content[chunk.index];
|
||||
switch (contentBlockDelta.type) {
|
||||
case "text_delta":
|
||||
if (targetContent && targetContent.type === "text") {
|
||||
targetContent.text += contentBlockDelta.text;
|
||||
yield contentBlockDelta.text;
|
||||
} else console.error("文本块不匹配,无法追加内容", targetContent);
|
||||
break
|
||||
case "input_json_delta":
|
||||
if (targetContent && targetContent.type === "tool_use") {
|
||||
targetContent.input += contentBlockDelta.partial_json || "";
|
||||
yield contentBlockDelta.partial_json || "";
|
||||
} else console.error("工具调用块不匹配,无法追加输入", targetContent);
|
||||
break
|
||||
}
|
||||
break
|
||||
|
||||
case "content_block_stop":
|
||||
let type = tempMsg?.content[chunk.index].type;
|
||||
if (type === "tool_use") {
|
||||
yield `\n\`\`\``;
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!tempMsg) {
|
||||
console.error("没有接收到有效的消息内容");
|
||||
yield "\n\n❌ 抱歉,处理您的请求时没有返回有效的消息内容。";
|
||||
break;
|
||||
}
|
||||
|
||||
messages.push({
|
||||
role: tempMsg.role,
|
||||
content: tempMsg.content.map(c => {
|
||||
if (c.type === "text") {
|
||||
return {
|
||||
type: c.type,
|
||||
text: c.text,
|
||||
};
|
||||
} else if (c.type === "tool_use") {
|
||||
return {
|
||||
id: c.id,
|
||||
type: c.type,
|
||||
name: c.name,
|
||||
input: JSON.parse(c.input as string)
|
||||
};
|
||||
} else return c;
|
||||
})
|
||||
});
|
||||
|
||||
if (!tempMsg.content.find(c => c.type === "tool_use")) {
|
||||
break
|
||||
}
|
||||
|
||||
let tempUserMsg: MessageParam = {
|
||||
role: "user",
|
||||
content: []
|
||||
}
|
||||
|
||||
let needRerun = false;
|
||||
for (const toolUse of tempMsg.content.filter(c => c.type === "tool_use")) {
|
||||
|
||||
let toolArgs = JSON.parse(toolUse.input as string || "{}");
|
||||
console.log(toolArgs);
|
||||
|
||||
switch (toolUse.name) {
|
||||
case "query-components":
|
||||
// @ts-ignore
|
||||
tempUserMsg.content.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: toolUse.id,
|
||||
content: await this.queryComponents(toolArgs)
|
||||
});
|
||||
needRerun = true;
|
||||
yield 'next_block'
|
||||
break
|
||||
case "show-components":
|
||||
let components = await ComponentService.getComponentsByIds(toolArgs.component_ids)
|
||||
|
||||
// @ts-ignore
|
||||
tempUserMsg.content.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: toolUse.id,
|
||||
content: `成功向用户展示了 ${JSON.stringify(
|
||||
(await ComponentService.getComponentsByIds(toolArgs.component_ids)).map((c, index) => (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,
|
||||
} || { id: toolArgs.component_ids[index].id, name: "未知配件" })
|
||||
)
|
||||
)}`
|
||||
});
|
||||
|
||||
needRerun = true;
|
||||
yield `show_card: ${JSON.stringify(components)}`;
|
||||
yield 'next_block'
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`未知工具调用: ${toolUse.name}`);
|
||||
|
||||
}
|
||||
|
||||
// 继续处理下一个工具调用
|
||||
}
|
||||
|
||||
// 将工具调用结果添加到消息列表中
|
||||
messages.push(tempUserMsg);
|
||||
if (!needRerun) break
|
||||
|
||||
} if (currentIteration >= maxIterations) {
|
||||
yield "\n\n⚠️ 达到最大处理轮次,对话结束。";
|
||||
}
|
||||
|
||||
// 保存更新后的对话到数据库
|
||||
if (currentConversationId) {
|
||||
await ConversationService.updateConversationMessages(currentConversationId, userId, messages);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理查询时发生错误:', error);
|
||||
yield `\n\n❌ 抱歉,处理您的请求时遇到了问题: ${error instanceof Error ? error.message : '未知错误'}`;
|
||||
|
||||
// 即使出错也要保存对话
|
||||
if (currentConversationId) {
|
||||
try {
|
||||
await ConversationService.updateConversationMessages(currentConversationId, userId, messages);
|
||||
} 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+37
@@ -1,5 +1,7 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret-here'
|
||||
|
||||
@@ -22,3 +24,38 @@ export function verifyToken(token: string): any {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUser(request: NextRequest) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization')
|
||||
const token = authHeader?.replace('Bearer ', '')
|
||||
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
|
||||
const decoded = verifyToken(token)
|
||||
if (!decoded || !decoded.userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
address: true,
|
||||
isAdmin: true,
|
||||
createdAt: true
|
||||
}
|
||||
})
|
||||
|
||||
return user
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,159 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export interface ComponentQueryParams {
|
||||
type?: string
|
||||
typeId?: string // 直接使用typeId
|
||||
brand?: string
|
||||
minPrice?: number
|
||||
maxPrice?: number
|
||||
search?: string
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface ComponentQueryResult {
|
||||
components: Array<{
|
||||
id: string
|
||||
name: string
|
||||
brand: string
|
||||
model: string
|
||||
price: number
|
||||
description: string | null
|
||||
stock: number
|
||||
type: string
|
||||
specifications: string | null
|
||||
imageUrl: string | null
|
||||
}>
|
||||
pagination: {
|
||||
page: number
|
||||
limit: number
|
||||
total: number
|
||||
totalPages: number
|
||||
hasNext: boolean
|
||||
hasPrev: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentService {
|
||||
/**
|
||||
* 查询组件列表
|
||||
*/
|
||||
static async queryComponents(params: ComponentQueryParams): Promise<ComponentQueryResult> {
|
||||
const {
|
||||
type,
|
||||
typeId,
|
||||
brand,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
search,
|
||||
page = 1,
|
||||
limit = 12
|
||||
} = params
|
||||
|
||||
const where: any = {}
|
||||
|
||||
// 优先使用typeId,如果没有但有type,则查找typeId
|
||||
if (typeId) {
|
||||
where.componentTypeId = typeId
|
||||
} else if (type) {
|
||||
const componentType = await prisma.componentType.findFirst({
|
||||
where: {
|
||||
name: {
|
||||
contains: type,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
}
|
||||
})
|
||||
if (componentType) {
|
||||
where.componentTypeId = componentType.id
|
||||
}
|
||||
}
|
||||
|
||||
if (brand) {
|
||||
where.brand = {
|
||||
contains: brand,
|
||||
mode: 'insensitive'
|
||||
}
|
||||
}
|
||||
|
||||
if (minPrice || maxPrice) {
|
||||
where.price = {}
|
||||
if (minPrice) where.price.gte = minPrice
|
||||
if (maxPrice) where.price.lte = maxPrice
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ brand: { contains: search, mode: 'insensitive' } },
|
||||
{ model: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } }
|
||||
]
|
||||
}
|
||||
|
||||
const [components, total] = await Promise.all([
|
||||
prisma.component.findMany({
|
||||
where,
|
||||
include: {
|
||||
componentType: true
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
}),
|
||||
prisma.component.count({ where })
|
||||
])
|
||||
|
||||
const totalPages = Math.ceil(total / limit)
|
||||
|
||||
return {
|
||||
components: components.map(comp => ({
|
||||
id: comp.id,
|
||||
name: comp.name,
|
||||
brand: comp.brand,
|
||||
model: comp.model,
|
||||
price: comp.price,
|
||||
description: comp.description,
|
||||
stock: comp.stock,
|
||||
type: comp.componentType.name,
|
||||
specifications: comp.specifications,
|
||||
imageUrl: comp.imageUrl
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取单个组件
|
||||
*/
|
||||
static async getComponentById(id: string) {
|
||||
return await prisma.component.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
componentType: true
|
||||
}
|
||||
})
|
||||
}
|
||||
static async getComponentsByIds(componentIds: string[]) {
|
||||
return await Promise.all(componentIds.map(id => ComponentService.getComponentById(id)))
|
||||
}
|
||||
/**
|
||||
* 获取所有组件类型
|
||||
*/
|
||||
static async getComponentTypes() {
|
||||
return await prisma.componentType.findMany({
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import type { MessageParam } from '@anthropic-ai/sdk/resources'
|
||||
|
||||
export interface ConversationData {
|
||||
id: string
|
||||
title: string | null
|
||||
messages: MessageParam[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export class ConversationService {
|
||||
/**
|
||||
* 创建新对话
|
||||
*/
|
||||
static async createConversation(userId: string, initialMessage: string): Promise<string> {
|
||||
// 生成对话标题(取用户消息的前30个字符)
|
||||
const title = initialMessage.length > 30
|
||||
? initialMessage.substring(0, 30) + '...'
|
||||
: initialMessage
|
||||
|
||||
const systemMessage: MessageParam = {
|
||||
role: "user",
|
||||
content: `你是PC DIY商店的专业AI助手。你的主要职责是:
|
||||
|
||||
1. 帮助用户查找和推荐PC配件
|
||||
2. 提供配件的价格、性能和兼容性信息
|
||||
3. 根据用户预算和需求推荐最适合的配件
|
||||
4. 解答关于PC装机的技术问题`
|
||||
}
|
||||
|
||||
const assistantGreeting: MessageParam = {
|
||||
role: "assistant",
|
||||
content: "您好!我是PC DIY商店的AI助手,很高兴为您服务。请告诉我您需要什么帮助?"
|
||||
}
|
||||
|
||||
const userMessage: MessageParam = {
|
||||
role: "user",
|
||||
content: initialMessage
|
||||
}
|
||||
|
||||
const initialMessages = [systemMessage, assistantGreeting, userMessage]
|
||||
|
||||
const conversation = await prisma.conversation.create({
|
||||
data: {
|
||||
userId,
|
||||
title,
|
||||
messages: JSON.stringify(initialMessages)
|
||||
}
|
||||
})
|
||||
|
||||
return conversation.id
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话
|
||||
*/
|
||||
static async getConversation(conversationId: string, userId: string): Promise<ConversationData | null> {
|
||||
const conversation = await prisma.conversation.findFirst({
|
||||
where: {
|
||||
id: conversationId,
|
||||
userId
|
||||
}
|
||||
})
|
||||
|
||||
if (!conversation) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: conversation.id,
|
||||
title: conversation.title,
|
||||
messages: JSON.parse(conversation.messages) as MessageParam[],
|
||||
createdAt: conversation.createdAt,
|
||||
updatedAt: conversation.updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话消息
|
||||
*/
|
||||
static async updateConversationMessages(
|
||||
conversationId: string,
|
||||
userId: string,
|
||||
messages: MessageParam[]
|
||||
): Promise<void> {
|
||||
await prisma.conversation.updateMany({
|
||||
where: {
|
||||
id: conversationId,
|
||||
userId
|
||||
},
|
||||
data: {
|
||||
messages: JSON.stringify(messages),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的对话列表
|
||||
*/
|
||||
static async getUserConversations(userId: string, page = 1, limit = 20) {
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
const [conversations, total] = await Promise.all([
|
||||
prisma.conversation.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
skip,
|
||||
take: limit
|
||||
}),
|
||||
prisma.conversation.count({
|
||||
where: { userId }
|
||||
})
|
||||
])
|
||||
|
||||
return {
|
||||
conversations,
|
||||
total,
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除对话
|
||||
*/
|
||||
static async deleteConversation(conversationId: string, userId: string): Promise<boolean> {
|
||||
const result = await prisma.conversation.deleteMany({
|
||||
where: {
|
||||
id: conversationId,
|
||||
userId
|
||||
}
|
||||
})
|
||||
|
||||
return result.count > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话标题
|
||||
*/
|
||||
static async updateConversationTitle(
|
||||
conversationId: string,
|
||||
userId: string,
|
||||
title: string
|
||||
): Promise<boolean> {
|
||||
const result = await prisma.conversation.updateMany({
|
||||
where: {
|
||||
id: conversationId,
|
||||
userId
|
||||
},
|
||||
data: { title }
|
||||
})
|
||||
|
||||
return result.count > 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user