const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const axios = require('axios'); const cors = require('cors'); const app = express(); const server = http.createServer(app); app.use(cors({ origin: "http://127.0.0.1:5500", methods: ["GET", "POST"], credentials: true })); const io = new Server(server, { cors: { origin: "http://127.0.0.1:5500", methods: ["GET", "POST"], credentials: true } }); const AI_API_URL = 'https://api.siliconflow.cn/v1/chat/completions'; const AI_API_KEY = 'sk-xnnehzodtycretktyjuxyvfqfulfomvhytroftenszqnwxyt'; const AI_MODEL = 'Qwen/QwQ-32B'; const onlineUsers = new Map(); // 定义最大重试次数 const MAX_RETRIES = 3; app.use(express.json()); app.post('/generate-strategy', async (req, res) => { try { const { destination, travelDate, travelDays, budget, preferences, specialNeed } = req.body; const prompt = ` 请严格按照以下格式生成${travelDays}天的${destination}旅游攻略(出行日期:${travelDate},人均预算${budget}元): 1. 攻略标题:包含目的地、天数、出发日期; 2. 每日行程:每天1个标题+3-4个时间段的行程(格式:时间-地点:描述,地点需含景点/餐厅,描述需含亮点); 3. 预算总结:分“门票”“餐饮”“住宿”3类,最后算总计(单位:元); 4. 兴趣偏好:优先满足${preferences.join('、')}; 5. 特殊需求:${specialNeed || '无'}。 示例格式: 标题:北京2日游攻略(2025-01-01出发) 第1天:历史文化之旅 09:00-12:00 故宫:明清皇家宫殿,必看太和殿、乾清宫,建议租讲解器 12:30-14:00 四季民福(故宫店):北京烤鸭招牌店,可远眺故宫 14:30-17:30 景山公园:登顶万春亭,俯瞰故宫全景 第2天:自然休闲之旅 09:00-12:00 颐和园:皇家园林,乘船游昆明湖,逛长廊 12:30-14:00 苏州街:颐和园周边小吃街,尝豌豆黄、艾窝窝 14:30-17:30 圆明园:遗址公园,感受历史,建议看西洋楼遗址 预算总结: 门票:故宫60 + 景山2 + 颐和园30 + 圆明园25 = 117元 餐饮:午餐80*2 + 小吃30 = 190元 住宿:经济型酒店300元/晚*1晚 = 300元 总计:117+190+300=607元 `; let aiResponse; let retryCount = 0; let finalError = null; // 增加重试逻辑,且动态调整超时时间 while (retryCount <= MAX_RETRIES) { try { // 动态超时:基础60秒,每次重试加20秒 const dynamicTimeout = 60000 + retryCount * 20000; aiResponse = await axios.post( AI_API_URL, { model: AI_MODEL, messages: [{ role: "user", content: prompt }], temperature: 0.7 }, { headers: { 'Authorization': `Bearer ${AI_API_KEY}`, 'Content-Type': 'application/json' }, timeout: dynamicTimeout } ); break; // 成功获取响应,跳出重试循环 } catch (aiError) { retryCount++; finalError = aiError; // 每次重试前等待1-3秒(随机) await new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 1000)); console.warn(`等待 ${(Math.random() * 2000 + 1000)/1000} 秒后,进行第 ${retryCount} 次重试,错误:`, aiError.response?.data || aiError.message); } } if (!aiResponse) { // 根据错误类型返回不同提示 if (finalError.code === 'ECONNABORTED') { return res.status(504).json({ code: 504, error: 'AI接口响应超时,可能是服务繁忙,请稍后再试' }); } else { console.error('调用硅基流动AI接口多次失败,详细错误:', finalError.response?.data || finalError.message); return res.status(500).json({ code: 500, error: '调用AI服务多次失败,可能是接口配置或网络问题,请检查后重试' }); } } const rawContent = aiResponse.data.choices[0].message.content; const strategy = parseAiStrategy(rawContent, destination, travelDate, travelDays, budget); res.json({ code: 200, data: strategy }); } catch (error) { console.error('攻略生成失败:', error.response?.data || error.message); res.status(500).json({ code: 500, error: '攻略生成失败,请检查AI服务或参数后重试' }); } }); function parseAiStrategy(rawContent, destination, travelDate, travelDays, budget) { const titleMatch = rawContent.match(/标题:(.+)/); const title = titleMatch ? titleMatch[1] : `${destination}${travelDays}日游攻略(${travelDate}出发)`; const days = []; for (let i = 1; i <= travelDays; i++) { const dayMatch = rawContent.match(new RegExp(`第${i}天:(.+?)(?=第${i+1}天|预算总结)`, 's')); if (dayMatch) { const dayContent = dayMatch[1]; const scheduleMatches = dayContent.match(/(\d{2}:\d{2}-\d{2}:\d{2})\s+(.+?)\:(.+?)(?=\d{2}:\d{2}|$)/g) || []; const schedule = scheduleMatches.map(item => { const [time, location, description] = item.split(/\s+|\:/).filter(Boolean); return { time, location, description: description.trim() }; }); days.push({ title: `第${i}天:${dayMatch[1].split('\n')[0].trim()}`, schedule }); } } const budgetSummary = { 门票: 0, 餐饮: 0, 住宿: 0 }; const ticketMatch = rawContent.match(/门票:.*?= (\d+)元/); const foodMatch = rawContent.match(/餐饮:.*?= (\d+)元/); const hotelMatch = rawContent.match(/住宿:.*?= (\d+)元/); const totalMatch = rawContent.match(/总计:(\d+)元/); budgetSummary.门票 = ticketMatch ? parseInt(ticketMatch[1]) : Math.floor(budget * 0.2); budgetSummary.餐饮 = foodMatch ? parseInt(foodMatch[1]) : Math.floor(budget * 0.3); budgetSummary.住宿 = hotelMatch ? parseInt(hotelMatch[1]) : Math.floor(budget * 0.4); const totalBudget = totalMatch ? parseInt(totalMatch[1]) : budget; return { id: Date.now().toString(), title, days: days.length ? days : [{ title: `第1天:${destination}核心游`, schedule: [{ time: '09:00-17:00', location: destination, description: `建议游览${destination}核心景点,结合${preferences[0] || '自然风光'}体验` }] }], budgetSummary, totalBudget }; } io.on('connection', (socket) => { console.log('新客户端连接:', socket.id); socket.on('login', (username) => { onlineUsers.set(socket.id, username); console.log(`${username} 已登录`); socket.emit('messageReceived', { sender: '系统', content: `欢迎来到AI聊天!我是硅基流动AI助手,有什么可以帮助你的吗?` }); }); socket.on('chatMessage', async (data) => { const sender = onlineUsers.get(socket.id); if (!sender) { socket.emit('messageReceived', { sender: '系统', content: '请先登录' }); return; } console.log(`收到消息: ${sender} -> ${data.recipient}: ${data.message}`); socket.emit('messageReceived', { sender: sender, content: data.message }); if (data.recipient === '硅基流动AI助手') { try { const aiRes = await axios.post( AI_API_URL, { model: AI_MODEL, messages: [{ role: "user", content: data.message }] }, { headers: { 'Authorization': `Bearer ${AI_API_KEY}`, 'Content-Type': 'application/json' } } ); socket.emit('messageReceived', { sender: '硅基流动AI助手', content: aiRes.data.choices[0].message.content }); } catch (err) { console.error('AI聊天调用错误:', err); socket.emit('messageReceived', { sender: '系统', content: '抱歉,AI聊天服务暂时无法使用' }); } } }); socket.on('disconnect', () => { const username = onlineUsers.get(socket.id); if (username) { console.log(`${username} 已断开连接`); onlineUsers.delete(socket.id); } console.log('客户端断开连接:', socket.id); }); }); const PORT = 3005; server.listen(PORT, () => { console.log(`服务器运行在 http://localhost:${PORT}`); console.log('攻略生成接口:POST http://localhost:3005/generate-strategy'); });