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.
lvgl/node/server.js

220 lines
8.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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