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

400 lines
16 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 { 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);
}
}