feat(api): 添加后端 API 路由
- /api/chat: 流式聊天接口,支持 Claude API 调用 - /api/conversations: 会话列表和创建接口 - /api/conversations/[id]: 单个会话详情和删除 - /api/models: 可用模型列表接口 - /api/settings: 用户设置读写接口 - /api/tools: 可用工具列表接口
This commit is contained in:
parent
ab9dd5aff8
commit
bb5996240a
555
src/app/api/chat/route.ts
Normal file
555
src/app/api/chat/route.ts
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/drizzle/db';
|
||||||
|
import { conversations, messages, userSettings } from '@/drizzle/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { executeTool } from '@/services/tools';
|
||||||
|
|
||||||
|
interface ChatRequest {
|
||||||
|
conversationId: string;
|
||||||
|
message: string;
|
||||||
|
model?: string;
|
||||||
|
tools?: string[];
|
||||||
|
enableThinking?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息内容块类型
|
||||||
|
interface ContentBlock {
|
||||||
|
type: 'text' | 'tool_use' | 'tool_result' | 'thinking';
|
||||||
|
text?: string;
|
||||||
|
thinking?: string;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
input?: Record<string, unknown>;
|
||||||
|
tool_use_id?: string;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 消息类型
|
||||||
|
interface APIMessage {
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string | ContentBlock[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认系统提示词 - 用于生成更详细、更有结构的回复
|
||||||
|
const DEFAULT_SYSTEM_PROMPT = `你是一个专业、友好的 AI 助手。请遵循以下规则来回复用户:
|
||||||
|
|
||||||
|
## 📅 当前时间
|
||||||
|
**重要**:今天的日期是 {{CURRENT_DATE}}。当你需要搜索实时信息或提及日期时,请使用这个准确的日期。
|
||||||
|
|
||||||
|
## 🎯 核心原则:专注回答最新问题
|
||||||
|
|
||||||
|
**重要**:你必须只回答用户最新(最后一条)的问题。请遵循以下规则:
|
||||||
|
|
||||||
|
1. **只回答最新问题**:无论对话历史中有多少问题,你只需要回答用户最新发送的那一条消息
|
||||||
|
2. **智能判断相关性**:
|
||||||
|
- 如果最新问题与之前的对话主题相关(如追问、补充),可以参考历史上下文
|
||||||
|
- 如果最新问题是一个全新的、无关的话题,直接回答,不要提及之前的对话
|
||||||
|
3. **绝不重复回答**:已经回答过的问题不要再次回答,即使用户没有明确表示已收到答案
|
||||||
|
4. **一个问题一个回答**:每次只专注于一个问题,不要试图同时回答多个问题
|
||||||
|
|
||||||
|
## 回复风格
|
||||||
|
- 使用中文回复,除非用户明确要求其他语言
|
||||||
|
- 回复要详细、有深度,不要过于简短
|
||||||
|
- 语气友好、专业,像一个耐心的老师
|
||||||
|
|
||||||
|
## 格式规范
|
||||||
|
- 使用 Markdown 格式化回复
|
||||||
|
- 对于代码,使用代码块并标明语言,例如 \`\`\`python
|
||||||
|
- 使用标题(##、###)来组织长回复
|
||||||
|
- 使用列表(有序或无序)来列举要点
|
||||||
|
- 使用粗体或斜体强调重要内容
|
||||||
|
|
||||||
|
## 回答结构
|
||||||
|
当回答技术问题时,请按以下结构:
|
||||||
|
1. **简要概述**:先给出简洁的答案
|
||||||
|
2. **详细解释**:深入解释原理或概念
|
||||||
|
3. **代码示例**:如果适用,提供完整、可运行的代码
|
||||||
|
4. **注意事项**:指出常见错误或最佳实践
|
||||||
|
5. **延伸阅读**:如果有相关主题,可以简要提及
|
||||||
|
|
||||||
|
## 代码规范
|
||||||
|
- 代码要完整、可运行,不要省略关键部分
|
||||||
|
- 添加适当的注释解释关键逻辑
|
||||||
|
- 使用有意义的变量名
|
||||||
|
- 如果代码较长,先展示完整代码,再逐段解释
|
||||||
|
|
||||||
|
## 工具使用
|
||||||
|
- 当需要查询实时信息时,请使用 web_search 工具
|
||||||
|
- 当需要执行代码验证结果时,请使用 code_execution 工具
|
||||||
|
- 当需要获取网页内容时,请使用 web_fetch 工具
|
||||||
|
- **工具调用原则**:只为最新问题调用必要的工具,不要为历史问题调用工具
|
||||||
|
|
||||||
|
## 工具结果总结(重要)
|
||||||
|
使用工具后,请按以下方式总结和回答:
|
||||||
|
1. **直接给出答案**:不要重复展示工具返回的原始数据,直接用自然语言总结关键信息
|
||||||
|
2. **引用来源**:在回答末尾用"来源"或"参考"标注信息来源网站
|
||||||
|
3. **格式示例**:
|
||||||
|
- ✅ 正确:"今天北京天气晴朗,气温15°C。(来源:weather.com)"
|
||||||
|
- ❌ 错误:把整个搜索结果复制出来
|
||||||
|
4. **代码执行结果**:直接展示输出结果,说明代码运行是否成功
|
||||||
|
|
||||||
|
## 特别注意
|
||||||
|
- 如果问题不明确,先确认理解是否正确
|
||||||
|
- 如果有多种方案,说明各自的优缺点
|
||||||
|
- 承认不确定的地方,不要编造信息`;
|
||||||
|
|
||||||
|
// POST /api/chat - 发送消息并获取 AI 回复
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body: ChatRequest = await request.json();
|
||||||
|
const { conversationId, message, model, tools, enableThinking } = body;
|
||||||
|
|
||||||
|
// 获取用户设置
|
||||||
|
const settings = await db.query.userSettings.findFirst({
|
||||||
|
where: eq(userSettings.id, 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings?.cchApiKey) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Please configure your CCH API key in settings' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对话信息
|
||||||
|
const conversation = await db.query.conversations.findFirst({
|
||||||
|
where: eq(conversations.conversationId, conversationId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Conversation not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对话历史消息
|
||||||
|
const historyMessages = await db.query.messages.findMany({
|
||||||
|
where: eq(messages.conversationId, conversationId),
|
||||||
|
orderBy: (messages, { asc }) => [asc(messages.createdAt)],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存用户消息
|
||||||
|
const userMessageId = nanoid();
|
||||||
|
await db.insert(messages).values({
|
||||||
|
messageId: userMessageId,
|
||||||
|
conversationId,
|
||||||
|
role: 'user',
|
||||||
|
content: message,
|
||||||
|
status: 'completed',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 构建消息历史
|
||||||
|
const messageHistory: APIMessage[] = historyMessages.map((msg) => ({
|
||||||
|
role: msg.role as 'user' | 'assistant',
|
||||||
|
content: msg.content,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 添加当前用户消息
|
||||||
|
messageHistory.push({
|
||||||
|
role: 'user',
|
||||||
|
content: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 准备 AI 消息 ID
|
||||||
|
const assistantMessageId = nanoid();
|
||||||
|
|
||||||
|
// 创建 SSE 响应
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
try {
|
||||||
|
// 调用 CCH API
|
||||||
|
const cchUrl = settings.cchUrl || 'http://localhost:13500';
|
||||||
|
const useThinking = enableThinking ?? conversation.enableThinking;
|
||||||
|
const useModel = model || conversation.model;
|
||||||
|
|
||||||
|
// 构建工具定义(如果需要)
|
||||||
|
const toolDefinitions = buildToolDefinitions(tools || (conversation.tools as string[]) || []);
|
||||||
|
|
||||||
|
// 获取系统提示词(优先级:对话级别 > 全局设置 > 默认)
|
||||||
|
const baseSystemPrompt = conversation.systemPrompt || settings.systemPrompt || DEFAULT_SYSTEM_PROMPT;
|
||||||
|
|
||||||
|
// 替换日期占位符为当前日期
|
||||||
|
const currentDate = new Date().toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'long',
|
||||||
|
});
|
||||||
|
const systemPrompt = baseSystemPrompt.replace('{{CURRENT_DATE}}', currentDate);
|
||||||
|
|
||||||
|
// 获取温度参数(优先级:对话级别 > 全局设置 > 默认 0.7)
|
||||||
|
const temperature = parseFloat(conversation.temperature || settings.temperature || '0.7');
|
||||||
|
|
||||||
|
// 工具执行循环
|
||||||
|
let currentMessages = [...messageHistory];
|
||||||
|
let fullContent = '';
|
||||||
|
let thinkingContent = '';
|
||||||
|
let totalInputTokens = 0;
|
||||||
|
let totalOutputTokens = 0;
|
||||||
|
let loopCount = 0;
|
||||||
|
const maxLoops = 10; // 防止无限循环
|
||||||
|
let hasToolResults = false; // 标记是否有工具结果
|
||||||
|
|
||||||
|
while (loopCount < maxLoops) {
|
||||||
|
loopCount++;
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
const requestBody: Record<string, unknown> = {
|
||||||
|
model: useModel,
|
||||||
|
max_tokens: 8192,
|
||||||
|
stream: true,
|
||||||
|
system: systemPrompt,
|
||||||
|
messages: currentMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加工具
|
||||||
|
if (toolDefinitions.length > 0) {
|
||||||
|
requestBody.tools = toolDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加思考模式
|
||||||
|
// 重要:thinking 模式和工具调用的 tool_result 不兼容
|
||||||
|
// 当消息中包含 tool_result 时,不能启用 thinking 模式
|
||||||
|
if (useThinking && !hasToolResults) {
|
||||||
|
requestBody.thinking = {
|
||||||
|
type: 'enabled',
|
||||||
|
budget_tokens: 4096,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
requestBody.temperature = temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${cchUrl}/v1/messages`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': settings.cchApiKey!,
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`CCH API error: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
throw new Error('No response body');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集当前轮次的内容
|
||||||
|
let currentTextContent = '';
|
||||||
|
let currentThinkingContent = '';
|
||||||
|
const toolCalls: { id: string; name: string; input: Record<string, unknown> }[] = [];
|
||||||
|
let currentToolUse: { id: string; name: string; inputJson: string } | null = null;
|
||||||
|
let stopReason: string | null = null;
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
// 处理流式响应
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() || '';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
const data = line.slice(6);
|
||||||
|
if (data === '[DONE]') continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(data);
|
||||||
|
|
||||||
|
if (event.type === 'content_block_delta') {
|
||||||
|
const delta = event.delta;
|
||||||
|
|
||||||
|
if (delta.type === 'thinking_delta') {
|
||||||
|
currentThinkingContent += delta.thinking || '';
|
||||||
|
thinkingContent += delta.thinking || '';
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'thinking',
|
||||||
|
content: delta.thinking || '',
|
||||||
|
})}\n\n`));
|
||||||
|
} else if (delta.type === 'text_delta') {
|
||||||
|
currentTextContent += delta.text || '';
|
||||||
|
fullContent += delta.text || '';
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'text',
|
||||||
|
content: delta.text || '',
|
||||||
|
})}\n\n`));
|
||||||
|
} else if (delta.type === 'input_json_delta') {
|
||||||
|
if (currentToolUse) {
|
||||||
|
currentToolUse.inputJson += delta.partial_json || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.type === 'message_delta') {
|
||||||
|
if (event.usage) {
|
||||||
|
totalOutputTokens += event.usage.output_tokens || 0;
|
||||||
|
}
|
||||||
|
if (event.delta?.stop_reason) {
|
||||||
|
stopReason = event.delta.stop_reason;
|
||||||
|
}
|
||||||
|
} else if (event.type === 'message_start') {
|
||||||
|
if (event.message?.usage) {
|
||||||
|
totalInputTokens += event.message.usage.input_tokens || 0;
|
||||||
|
}
|
||||||
|
} else if (event.type === 'content_block_start') {
|
||||||
|
if (event.content_block?.type === 'tool_use') {
|
||||||
|
currentToolUse = {
|
||||||
|
id: event.content_block.id,
|
||||||
|
name: event.content_block.name,
|
||||||
|
inputJson: '',
|
||||||
|
};
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'tool_use_start',
|
||||||
|
id: event.content_block.id,
|
||||||
|
name: event.content_block.name,
|
||||||
|
})}\n\n`));
|
||||||
|
}
|
||||||
|
} else if (event.type === 'content_block_stop') {
|
||||||
|
if (currentToolUse) {
|
||||||
|
try {
|
||||||
|
const toolInput = JSON.parse(currentToolUse.inputJson || '{}');
|
||||||
|
toolCalls.push({
|
||||||
|
id: currentToolUse.id,
|
||||||
|
name: currentToolUse.name,
|
||||||
|
input: toolInput,
|
||||||
|
});
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'tool_use_complete',
|
||||||
|
id: currentToolUse.id,
|
||||||
|
name: currentToolUse.name,
|
||||||
|
input: toolInput,
|
||||||
|
})}\n\n`));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse tool input:', e);
|
||||||
|
}
|
||||||
|
currentToolUse = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Parse error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要执行工具
|
||||||
|
if (stopReason === 'tool_use' && toolCalls.length > 0) {
|
||||||
|
// 构建助手消息的内容块
|
||||||
|
const assistantContent: ContentBlock[] = [];
|
||||||
|
|
||||||
|
// 如果有 thinking 内容,需要先添加(Claude API 要求)
|
||||||
|
if (currentThinkingContent) {
|
||||||
|
assistantContent.push({
|
||||||
|
type: 'thinking',
|
||||||
|
thinking: currentThinkingContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTextContent) {
|
||||||
|
assistantContent.push({
|
||||||
|
type: 'text',
|
||||||
|
text: currentTextContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加工具调用
|
||||||
|
for (const tc of toolCalls) {
|
||||||
|
assistantContent.push({
|
||||||
|
type: 'tool_use',
|
||||||
|
id: tc.id,
|
||||||
|
name: tc.name,
|
||||||
|
input: tc.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将助手消息添加到历史
|
||||||
|
currentMessages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: assistantContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行所有工具并收集结果
|
||||||
|
const toolResults: ContentBlock[] = [];
|
||||||
|
|
||||||
|
for (const tc of toolCalls) {
|
||||||
|
// 发送工具执行开始事件
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'tool_execution_start',
|
||||||
|
id: tc.id,
|
||||||
|
name: tc.name,
|
||||||
|
})}\n\n`));
|
||||||
|
|
||||||
|
// 执行工具
|
||||||
|
const result = await executeTool(tc.name, tc.input);
|
||||||
|
|
||||||
|
// 发送工具执行结果事件(使用简短版本)
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'tool_execution_result',
|
||||||
|
id: tc.id,
|
||||||
|
name: tc.name,
|
||||||
|
success: result.success,
|
||||||
|
result: result.displayResult,
|
||||||
|
})}\n\n`));
|
||||||
|
|
||||||
|
// 将简短的工具结果显示给用户
|
||||||
|
const toolDisplayText = `\n\n${result.displayResult}\n\n`;
|
||||||
|
fullContent += toolDisplayText;
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'text',
|
||||||
|
content: toolDisplayText,
|
||||||
|
})}\n\n`));
|
||||||
|
|
||||||
|
// 将完整的工具结果发送给 AI
|
||||||
|
toolResults.push({
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: tc.id,
|
||||||
|
content: result.fullResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将工具结果添加到消息历史
|
||||||
|
currentMessages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: toolResults,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标记已有工具结果,下一轮请求需要禁用 thinking 模式
|
||||||
|
hasToolResults = true;
|
||||||
|
|
||||||
|
// 继续循环,让 AI 基于工具结果继续回复
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有工具调用或停止原因不是 tool_use,则结束循环
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存 AI 回复到数据库
|
||||||
|
await db.insert(messages).values({
|
||||||
|
messageId: assistantMessageId,
|
||||||
|
conversationId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: fullContent,
|
||||||
|
thinkingContent: thinkingContent || null,
|
||||||
|
inputTokens: totalInputTokens,
|
||||||
|
outputTokens: totalOutputTokens,
|
||||||
|
status: 'completed',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新对话信息
|
||||||
|
await db
|
||||||
|
.update(conversations)
|
||||||
|
.set({
|
||||||
|
messageCount: (conversation.messageCount || 0) + 2,
|
||||||
|
totalTokens: (conversation.totalTokens || 0) + totalInputTokens + totalOutputTokens,
|
||||||
|
lastMessageAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
title: (conversation.messageCount || 0) === 0
|
||||||
|
? message.slice(0, 50) + (message.length > 50 ? '...' : '')
|
||||||
|
: conversation.title,
|
||||||
|
})
|
||||||
|
.where(eq(conversations.conversationId, conversationId));
|
||||||
|
|
||||||
|
// 发送完成事件
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'done',
|
||||||
|
messageId: assistantMessageId,
|
||||||
|
inputTokens: totalInputTokens,
|
||||||
|
outputTokens: totalOutputTokens,
|
||||||
|
})}\n\n`));
|
||||||
|
|
||||||
|
controller.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Stream error:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
error: errorMessage,
|
||||||
|
})}\n\n`));
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chat API error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to process chat request' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建工具定义
|
||||||
|
function buildToolDefinitions(toolIds: string[]) {
|
||||||
|
const toolMap: Record<string, object> = {
|
||||||
|
web_search: {
|
||||||
|
name: 'web_search',
|
||||||
|
description: '搜索互联网获取最新信息。当用户询问时事、新闻、天气、实时数据等需要最新信息的问题时,请使用此工具。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: '搜索查询关键词',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
code_execution: {
|
||||||
|
name: 'code_execution',
|
||||||
|
description: '执行代码并返回结果。支持 Python、JavaScript、TypeScript、Java、C、C++、Go、Rust 等多种语言。当需要验证代码、进行计算或演示代码运行结果时,请使用此工具。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
code: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要执行的代码',
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: 'string',
|
||||||
|
description: '编程语言 (python, javascript, typescript, java, c, cpp, go, rust, ruby, php 等)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['code', 'language'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
web_fetch: {
|
||||||
|
name: 'web_fetch',
|
||||||
|
description: '获取指定 URL 的网页内容。当用户提供了具体的网址并想了解该页面的内容时,请使用此工具。',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要获取内容的完整 URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['url'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return toolIds
|
||||||
|
.filter((id) => toolMap[id])
|
||||||
|
.map((id) => toolMap[id]);
|
||||||
|
}
|
||||||
119
src/app/api/conversations/[id]/route.ts
Normal file
119
src/app/api/conversations/[id]/route.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/drizzle/db';
|
||||||
|
import { conversations, messages } from '@/drizzle/schema';
|
||||||
|
import { eq, asc } from 'drizzle-orm';
|
||||||
|
|
||||||
|
interface RouteParams {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/conversations/[id] - 获取单个对话及其消息
|
||||||
|
export async function GET(request: Request, { params }: RouteParams) {
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
const conversation = await db.query.conversations.findFirst({
|
||||||
|
where: eq(conversations.conversationId, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Conversation not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageList = await db.query.messages.findMany({
|
||||||
|
where: eq(messages.conversationId, id),
|
||||||
|
orderBy: [asc(messages.createdAt)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
...conversation,
|
||||||
|
messages: messageList,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get conversation:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to get conversation' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/conversations/[id] - 更新对话
|
||||||
|
export async function PUT(request: Request, { params }: RouteParams) {
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
const body = await request.json();
|
||||||
|
const { title, isPinned, isArchived } = body;
|
||||||
|
|
||||||
|
const updateData: Record<string, unknown> = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (title !== undefined) {
|
||||||
|
updateData.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPinned !== undefined) {
|
||||||
|
updateData.isPinned = isPinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArchived !== undefined) {
|
||||||
|
updateData.isArchived = isArchived;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(conversations)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(conversations.conversationId, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Conversation not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(updated);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update conversation:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to update conversation' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/conversations/[id] - 删除对话
|
||||||
|
export async function DELETE(request: Request, { params }: RouteParams) {
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
// 先删除相关消息
|
||||||
|
await db.delete(messages).where(eq(messages.conversationId, id));
|
||||||
|
|
||||||
|
// 再删除对话
|
||||||
|
const [deleted] = await db
|
||||||
|
.delete(conversations)
|
||||||
|
.where(eq(conversations.conversationId, id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Conversation not found' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete conversation:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to delete conversation' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/app/api/conversations/route.ts
Normal file
81
src/app/api/conversations/route.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/drizzle/db';
|
||||||
|
import { conversations, messages } from '@/drizzle/schema';
|
||||||
|
import { desc, eq } from 'drizzle-orm';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
// GET /api/conversations - 获取对话列表
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const conversationList = await db.query.conversations.findMany({
|
||||||
|
where: eq(conversations.isArchived, false),
|
||||||
|
orderBy: [desc(conversations.lastMessageAt)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(conversationList);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get conversations:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to get conversations' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/conversations - 创建新对话
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { title, model, tools, enableThinking } = body;
|
||||||
|
|
||||||
|
const conversationId = nanoid();
|
||||||
|
|
||||||
|
const [newConversation] = await db
|
||||||
|
.insert(conversations)
|
||||||
|
.values({
|
||||||
|
conversationId,
|
||||||
|
title: title || '新对话',
|
||||||
|
model: model || 'claude-sonnet-4-20250514',
|
||||||
|
tools: tools || [],
|
||||||
|
enableThinking: enableThinking || false,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return NextResponse.json(newConversation, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create conversation:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to create conversation' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/conversations - 批量删除对话
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { conversationIds } = body;
|
||||||
|
|
||||||
|
if (!conversationIds || !Array.isArray(conversationIds)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'conversationIds array is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除相关消息
|
||||||
|
for (const id of conversationIds) {
|
||||||
|
await db.delete(messages).where(eq(messages.conversationId, id));
|
||||||
|
await db.delete(conversations).where(eq(conversations.conversationId, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete conversations:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to delete conversations' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/api/models/route.ts
Normal file
22
src/app/api/models/route.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/drizzle/db';
|
||||||
|
import { models } from '@/drizzle/schema';
|
||||||
|
import { eq, asc } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// GET /api/models - 获取所有模型列表
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const modelList = await db.query.models.findMany({
|
||||||
|
where: eq(models.isEnabled, true),
|
||||||
|
orderBy: [asc(models.sortOrder)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(modelList);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get models:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to get models' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
164
src/app/api/settings/route.ts
Normal file
164
src/app/api/settings/route.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/drizzle/db';
|
||||||
|
import { userSettings } from '@/drizzle/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// GET /api/settings - 获取用户设置
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const settings = await db.query.userSettings.findFirst({
|
||||||
|
where: eq(userSettings.id, 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
// 如果没有设置,返回默认值
|
||||||
|
return NextResponse.json({
|
||||||
|
cchUrl: process.env.CCH_DEFAULT_URL || 'http://localhost:13500',
|
||||||
|
cchApiKeyConfigured: false,
|
||||||
|
defaultModel: 'claude-sonnet-4-20250514',
|
||||||
|
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
||||||
|
systemPrompt: '',
|
||||||
|
temperature: '0.7',
|
||||||
|
theme: 'light',
|
||||||
|
language: 'zh-CN',
|
||||||
|
enableThinking: false,
|
||||||
|
saveChatHistory: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不返回 API Key 本身,只返回是否已配置
|
||||||
|
return NextResponse.json({
|
||||||
|
cchUrl: settings.cchUrl,
|
||||||
|
cchApiKeyConfigured: settings.cchApiKeyConfigured,
|
||||||
|
defaultModel: settings.defaultModel,
|
||||||
|
defaultTools: settings.defaultTools,
|
||||||
|
systemPrompt: settings.systemPrompt || '',
|
||||||
|
temperature: settings.temperature || '0.7',
|
||||||
|
theme: settings.theme,
|
||||||
|
language: settings.language,
|
||||||
|
enableThinking: settings.enableThinking,
|
||||||
|
saveChatHistory: settings.saveChatHistory,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get settings:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to get settings' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/settings - 更新用户设置
|
||||||
|
export async function PUT(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const {
|
||||||
|
cchUrl,
|
||||||
|
cchApiKey,
|
||||||
|
defaultModel,
|
||||||
|
defaultTools,
|
||||||
|
systemPrompt,
|
||||||
|
temperature,
|
||||||
|
theme,
|
||||||
|
language,
|
||||||
|
enableThinking,
|
||||||
|
saveChatHistory,
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
// 构建更新对象
|
||||||
|
const updateData: Record<string, unknown> = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cchUrl !== undefined) {
|
||||||
|
updateData.cchUrl = cchUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提供了 API Key,更新它
|
||||||
|
if (cchApiKey !== undefined) {
|
||||||
|
if (cchApiKey === '') {
|
||||||
|
// 清除 API Key
|
||||||
|
updateData.cchApiKey = null;
|
||||||
|
updateData.cchApiKeyConfigured = false;
|
||||||
|
} else {
|
||||||
|
updateData.cchApiKey = cchApiKey;
|
||||||
|
updateData.cchApiKeyConfigured = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultModel !== undefined) {
|
||||||
|
updateData.defaultModel = defaultModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultTools !== undefined) {
|
||||||
|
updateData.defaultTools = defaultTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemPrompt !== undefined) {
|
||||||
|
updateData.systemPrompt = systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temperature !== undefined) {
|
||||||
|
updateData.temperature = temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme !== undefined) {
|
||||||
|
updateData.theme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (language !== undefined) {
|
||||||
|
updateData.language = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableThinking !== undefined) {
|
||||||
|
updateData.enableThinking = enableThinking;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveChatHistory !== undefined) {
|
||||||
|
updateData.saveChatHistory = saveChatHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否存在设置记录
|
||||||
|
const existing = await db.query.userSettings.findFirst({
|
||||||
|
where: eq(userSettings.id, 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
// 创建新的设置记录
|
||||||
|
await db.insert(userSettings).values({
|
||||||
|
id: 1,
|
||||||
|
...updateData,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 更新现有记录
|
||||||
|
await db
|
||||||
|
.update(userSettings)
|
||||||
|
.set(updateData)
|
||||||
|
.where(eq(userSettings.id, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回更新后的设置(不包含 API Key)
|
||||||
|
const updatedSettings = await db.query.userSettings.findFirst({
|
||||||
|
where: eq(userSettings.id, 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
cchUrl: updatedSettings?.cchUrl,
|
||||||
|
cchApiKeyConfigured: updatedSettings?.cchApiKeyConfigured,
|
||||||
|
defaultModel: updatedSettings?.defaultModel,
|
||||||
|
defaultTools: updatedSettings?.defaultTools,
|
||||||
|
systemPrompt: updatedSettings?.systemPrompt || '',
|
||||||
|
temperature: updatedSettings?.temperature || '0.7',
|
||||||
|
theme: updatedSettings?.theme,
|
||||||
|
language: updatedSettings?.language,
|
||||||
|
enableThinking: updatedSettings?.enableThinking,
|
||||||
|
saveChatHistory: updatedSettings?.saveChatHistory,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update settings:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to update settings' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/api/tools/route.ts
Normal file
22
src/app/api/tools/route.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/drizzle/db';
|
||||||
|
import { tools } from '@/drizzle/schema';
|
||||||
|
import { eq, asc } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// GET /api/tools - 获取所有工具列表
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const toolList = await db.query.tools.findMany({
|
||||||
|
where: eq(tools.isEnabled, true),
|
||||||
|
orderBy: [asc(tools.sortOrder)],
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(toolList);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get tools:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to get tools' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user