|
|
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');
|
|
|
}); |