|
|
|
|
|
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 <thinking>...</thinking> tags
|
|
|
|
|
|
cleaned = cleaned.replace(/<thinking>[\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 格式有误,请重试')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|