const SYSTEM_PROMPT = `你是一个专业的旅行规划助手。你需要根据用户的需求规划行程。 ## 重要:输出格式 你必须直接输出 JSON,不要包含任何 markdown 代码块标记(如 \`\`\`json),不要在 JSON 前后添加任何额外文字。 JSON 格式如下: { "thinking": "你的思考过程", "schemes": [ { "name": "方案名称", "route": "路线描述", "days": 4, "totalKm": 1200, "totalDriveTime": 12, "budget": "¥3000", "highlights": ["亮点1", "亮点2"], "points": [ { "name": "地点名称", "lat": 25.04, "lng": 102.71, "day": "Day 1", "badge": "START", "icon": "🏙️", "desc": "简短描述", "km": "0km", "driveTime": "—", "schedule": [{"time": "上午", "content": "行程"}], "foods": ["美食"], "hotel": "住宿", "tips": "注意事项" } ] } ] } 注意: 1. points 第一个用 START badge,中间用 D1/D2,最后一个用 END 2. lat/lng 必须是真实中国地理坐标 3. 至少 2 个方案,最多 3 个 4. 直接输出 JSON,不要其他内容` const QUICK_PLAN_PROMPT = `你是一个专业的旅行规划助手。根据用户指定的目的地和天数,快速生成 3 个不同的旅行方案。 ## 重要:输出格式 你必须直接输出 JSON,不要包含任何 markdown 代码块标记,不要在 JSON 前后添加任何额外文字。 JSON 格式如下: { "thinking": "你的思考过程", "schemes": [ { "name": "方案名称", "route": "路线描述,如:昆明→大理→丽江→香格里拉", "days": 5, "totalKm": 800, "totalDriveTime": 10, "budget": "¥3000", "highlights": ["亮点1", "亮点2", "亮点3"], "daysDetail": [ { "location": "当日所在地", "desc": "当日行程描述", "spots": ["景点1", "景点2", "景点3"] } ], "tips": "旅行贴士,如最佳季节、注意事项、必备物品等", "points": [ { "name": "地点名称", "lat": 25.04, "lng": 102.71, "day": "Day 1", "badge": "START", "icon": "🏙️", "desc": "该地点简短描述", "km": "0km", "driveTime": "—", "schedule": [{"time": "上午", "content": "行程安排"}], "foods": ["当地美食"], "hotel": "推荐住宿", "tips": "注意事项" } ] } ] } 要求: 1. 3 个方案风格不同(如:经典路线、深度游、小众路线) 2. lat/lng 必须是真实中国地理坐标 3. 行程安排要合理,每天 1-2 个地点 4. daysDetail 数组长度必须等于 days,每天一个对象 5. 直接输出 JSON,不要其他内容` const CUSTOM_PLAN_PROMPT = `你是一个专业的旅行规划助手。用户已经输入了他/她的行程安排,你需要: 1. 解析用户的行程安排 2. 评估行程是否合理(时间是否充裕、路线是否顺畅、景点搭配是否合理) 3. 如果不合理,给出优化建议和优化后的方案 4. 同时也生成一个按用户原始输入直接生成的方案 ## 重要:输出格式 你必须直接输出 JSON,不要包含任何 markdown 代码块标记,不要在 JSON 前后添加任何额外文字。 JSON 格式如下: { "thinking": "你的分析过程", "evaluation": { "isReasonable": true, "summary": "整体评价:行程基本合理/存在以下问题...", "suggestions": ["建议1:某天行程太紧,建议...", "建议2:A到B距离较远,建议..."] }, "optimizedScheme": { "name": "优化方案", "route": "优化后的路线", "days": 4, "totalKm": 800, "totalDriveTime": 10, "budget": "¥3000", "highlights": ["亮点1", "亮点2"], "points": [ { "name": "地点名称", "lat": 25.04, "lng": 102.71, "day": "Day 1", "badge": "START", "icon": "🏙️", "desc": "描述", "km": "0km", "driveTime": "—", "schedule": [{"time": "上午", "content": "行程"}], "foods": ["美食"], "hotel": "住宿", "tips": "注意事项" } ] }, "originalScheme": { "name": "原始方案(按你的输入生成)", "route": "原始路线", "days": 4, "totalKm": 900, "totalDriveTime": 12, "budget": "¥3500", "highlights": ["亮点1", "亮点2"], "points": [ { "name": "地点名称", "lat": 25.04, "lng": 102.71, "day": "Day 1", "badge": "START", "icon": "🏙️", "desc": "描述", "km": "0km", "driveTime": "—", "schedule": [{"time": "上午", "content": "行程"}], "foods": ["美食"], "hotel": "住宿", "tips": "注意事项" } ] } } 注意: 1. lat/lng 必须是真实中国地理坐标 2. optimizedScheme 和 originalScheme 都要包含完整的 points 数组 3. 即使行程合理也要生成两个方案(优化版和原版) 4. 直接输出 JSON,不要其他内容` export async function chatWithAI(userMessage, onThinking = null) { // Use Express API server directly (port 3001) instead of Vite proxy const apiUrl = import.meta.env.DEV ? 'http://localhost:3001/api/chat' : '/api/chat' const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: [ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: userMessage } ], stream: true }) }) if (!response.ok) { const error = await response.json().catch(() => ({})) throw new Error(error.error?.message || `API 请求失败: ${response.status}`) } const reader = response.body.getReader() const decoder = new TextDecoder() let fullContent = '' let reasoningContent = '' 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) { const trimmed = line.trim() if (!trimmed || !trimmed.startsWith('data:')) continue const data = trimmed.slice(5).trim() if (data === '[DONE]') break try { const parsed = JSON.parse(data) const delta = parsed.choices?.[0]?.delta || {} // Support both reasoning_content (thinking models) and content if (delta.reasoning_content) { reasoningContent += delta.reasoning_content if (onThinking) onThinking(reasoningContent) } const chunk = delta.content || '' if (chunk) { fullContent += chunk if (onThinking) onThinking(fullContent || reasoningContent) } } catch (e) { // Skip malformed JSON chunks } } } return parseAIResponse(fullContent || reasoningContent) } function parseAIResponse(content) { // Remove markdown code block markers if present let cleaned = content.trim() // Remove ```json ... ``` or ``` ... ``` const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/) if (codeBlockMatch) { cleaned = codeBlockMatch[1].trim() } // Remove any ... tags cleaned = cleaned.replace(/[\s\S]*?<\/thinking>/g, '').trim() // Try to find JSON object // Find the first { and last } to extract the JSON const firstBrace = cleaned.indexOf('{') const lastBrace = cleaned.lastIndexOf('}') if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) { throw new Error('AI 未返回有效的 JSON 数据') } const jsonStr = cleaned.substring(firstBrace, lastBrace + 1) try { const parsed = JSON.parse(jsonStr) // Validate required fields if (!parsed.schemes || !Array.isArray(parsed.schemes) || parsed.schemes.length === 0) { throw new Error('AI 返回结果缺少 schemes') } return parsed } catch (e) { if (e.message.includes('缺少 schemes')) throw e console.error('JSON parse error:', e) console.error('Raw content:', cleaned.substring(0, 200)) throw new Error('AI 返回的 JSON 格式有误,请重试') } } // Shared streaming function async function streamAIRequest(messages, onThinking = null) { const apiUrl = import.meta.env.DEV ? 'http://localhost:3001/api/chat' : '/api/chat' console.log('[streamAI] Fetching:', apiUrl) const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages, stream: true }) }) console.log('[streamAI] Response status:', response.status) if (!response.ok) { const error = await response.json().catch(() => ({})) throw new Error(error.error?.message || `API 请求失败: ${response.status}`) } const reader = response.body.getReader() const decoder = new TextDecoder() let fullContent = '' let reasoningContent = '' let buffer = '' let chunkCount = 0 let lastUpdate = Date.now() while (true) { const { done, value } = await reader.read() if (done) { console.log('[streamAI] Done, total chunks:', chunkCount) break } chunkCount++ buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { const trimmed = line.trim() if (!trimmed || !trimmed.startsWith('data:')) continue const data = trimmed.slice(5).trim() if (data === '[DONE]') break try { const parsed = JSON.parse(data) const delta = parsed.choices?.[0]?.delta || {} if (delta.reasoning_content) { reasoningContent += delta.reasoning_content if (onThinking) { onThinking(reasoningContent) if (Date.now() - lastUpdate > 2000) { console.log('[streamAI] reasoning_length:', reasoningContent.length) lastUpdate = Date.now() } } } const chunk = delta.content || '' if (chunk) fullContent += chunk } catch (e) { // Skip malformed JSON } } } console.log('[streamAI] fullContent:', fullContent.length, 'reasoningContent:', reasoningContent.length) return fullContent || reasoningContent } function parseJSONFromContent(content) { let cleaned = content.trim() const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/) if (codeBlockMatch) cleaned = codeBlockMatch[1].trim() cleaned = cleaned.replace(/[\s\S]*?<\/thinking>/g, '').trim() const firstBrace = cleaned.indexOf('{') const lastBrace = cleaned.lastIndexOf('}') if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) { throw new Error('AI 未返回有效的 JSON 数据') } const jsonStr = cleaned.substring(firstBrace, lastBrace + 1) try { return JSON.parse(jsonStr) } catch (e) { console.error('JSON parse error:', e) throw new Error('AI 返回的 JSON 格式有误,请重试') } } export async function quickPlan(userInput, onThinking = null) { const userMessage = `目的地:${userInput.destination} 出行天数:${userInput.days}天 出发日期:${userInput.startDate} 出行方式:${userInput.transport} 其他需求:${userInput.extraNeeds} 请为我生成 3 个不同风格的旅行方案。` const content = await streamAIRequest([ { role: 'system', content: QUICK_PLAN_PROMPT }, { role: 'user', content: userMessage } ], onThinking) const parsed = parseJSONFromContent(content) if (!parsed.schemes || !Array.isArray(parsed.schemes) || parsed.schemes.length === 0) { throw new Error('AI 返回结果缺少 schemes') } return parsed } export async function customPlan(userInput, onThinking = null) { const userMessage = `我的行程安排: ${userInput.itinerary} 出行方式:${userInput.transport} 同行人员:${userInput.companions} 请评估我的行程是否合理,并给出优化建议和两个方案(优化版和原版)。` const content = await streamAIRequest([ { role: 'system', content: CUSTOM_PLAN_PROMPT }, { role: 'user', content: userMessage } ], onThinking) const parsed = parseJSONFromContent(content) if (!parsed.evaluation) { throw new Error('AI 返回结果缺少 evaluation') } return parsed }