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,不要其他内容` export async function chatWithAI(userMessage, onThinking = null) { const response = await fetch('/api/chat', { 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 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 chunk = parsed.choices?.[0]?.delta?.content || '' if (chunk) { fullContent += chunk if (onThinking) onThinking(fullContent) } } catch (e) { // Skip malformed JSON chunks } } } return parseAIResponse(fullContent) } 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 格式有误,请重试') } }