From bb5996240ac0cebfbc81cb361519b1a1bbfa5c51 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Thu, 18 Dec 2025 11:43:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E6=B7=BB=E5=8A=A0=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=20API=20=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /api/chat: 流式聊天接口,支持 Claude API 调用 - /api/conversations: 会话列表和创建接口 - /api/conversations/[id]: 单个会话详情和删除 - /api/models: 可用模型列表接口 - /api/settings: 用户设置读写接口 - /api/tools: 可用工具列表接口 --- src/app/api/chat/route.ts | 555 ++++++++++++++++++++++++ src/app/api/conversations/[id]/route.ts | 119 +++++ src/app/api/conversations/route.ts | 81 ++++ src/app/api/models/route.ts | 22 + src/app/api/settings/route.ts | 164 +++++++ src/app/api/tools/route.ts | 22 + 6 files changed, 963 insertions(+) create mode 100644 src/app/api/chat/route.ts create mode 100644 src/app/api/conversations/[id]/route.ts create mode 100644 src/app/api/conversations/route.ts create mode 100644 src/app/api/models/route.ts create mode 100644 src/app/api/settings/route.ts create mode 100644 src/app/api/tools/route.ts diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts new file mode 100644 index 0000000..224588c --- /dev/null +++ b/src/app/api/chat/route.ts @@ -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; + 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 = { + 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 }[] = []; + 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 = { + 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]); +} diff --git a/src/app/api/conversations/[id]/route.ts b/src/app/api/conversations/[id]/route.ts new file mode 100644 index 0000000..01af93b --- /dev/null +++ b/src/app/api/conversations/[id]/route.ts @@ -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 = { + 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 } + ); + } +} diff --git a/src/app/api/conversations/route.ts b/src/app/api/conversations/route.ts new file mode 100644 index 0000000..c56523a --- /dev/null +++ b/src/app/api/conversations/route.ts @@ -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 } + ); + } +} diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts new file mode 100644 index 0000000..985670e --- /dev/null +++ b/src/app/api/models/route.ts @@ -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 } + ); + } +} diff --git a/src/app/api/settings/route.ts b/src/app/api/settings/route.ts new file mode 100644 index 0000000..703b83a --- /dev/null +++ b/src/app/api/settings/route.ts @@ -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 = { + 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 } + ); + } +} diff --git a/src/app/api/tools/route.ts b/src/app/api/tools/route.ts new file mode 100644 index 0000000..db90356 --- /dev/null +++ b/src/app/api/tools/route.ts @@ -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 } + ); + } +}