You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

141 lines
3.9 KiB

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 格式有误,请重试')
}
}