diff --git a/server/public/index.html b/server/public/index.html index b0809f8..96d8dc8 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -192,7 +192,7 @@ let currentRoom = null; let rooms = []; let pendingUploads = []; - const sentMsgIds = new Set(); // 已发送成功的消息 ID 集合,防止重复 + const sentMsgIds = new Set(); // 本地已处理的消息 ID,防止广播重复 function getToken() { return localStorage.getItem('token'); } function saveAuth(data) { @@ -303,8 +303,7 @@ const data = JSON.parse(e.data); if (data.type === 'new_message') { const msg = data.message; - // 去重:如果消息 ID 已在发送确认集合中,忽略广播 - if (sentMsgIds.has(msg.id)) return; + // 发送者本人不会收到自己的广播(已由后端过滤),因此直接添加 if (currentRoom === msg.room_id) { appendMessage(msg); } @@ -483,7 +482,7 @@ body: JSON.stringify({ text: text || '', attachments: pendingUploads }) }); const newMsg = await res.json(); - sentMsgIds.add(newMsg.id); // 记录已发送,避免广播重复 + // 不记录 sentMsgIds,因为后端已经过滤,我们直接 append appendMessage(newMsg); input.value = ''; pendingUploads = []; @@ -492,13 +491,12 @@ } } - // 选图后自动压缩上传,然后调用 sendMessage,确保占位 + // 选图后自动压缩上传,显示占位,然后发送 async function handleFileSelect(event) { const files = event.target.files; if (!files.length) return; const token = getToken(); - // 立即显示占位消息 const placeholderId = 'placeholder_' + Date.now(); const placeholderMsg = { id: placeholderId, @@ -529,11 +527,11 @@ const data = await res.json(); if (data.path) pendingUploads.push(data.path); } - // 上传完毕,移除占位并发送正式消息 + // 移除占位 window._messagesCache = window._messagesCache.filter(m => m.id !== placeholderId); - sendMessage(); // 会自动带上 pendingUploads + // 发送正式消息 + sendMessage(); } catch(e) { - // 失败则移除占位 window._messagesCache = window._messagesCache.filter(m => m.id !== placeholderId); renderMessages(window._messagesCache); alert('图片发送失败,请重试'); diff --git a/server/server.js b/server/server.js index 8eda128..a87205c 100644 --- a/server/server.js +++ b/server/server.js @@ -42,7 +42,7 @@ const storage = multer.diskStorage({ }); const upload = multer({ storage, - limits: { fileSize: 5 * 1024 * 1024 }, // 5MB + limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const allowed = ['image/jpeg','image/png','image/gif']; cb(null, allowed.includes(file.mimetype)); @@ -80,7 +80,6 @@ app.get('/api/user', auth, (req, res) => { res.json({ username: req.user.username, isAdmin: req.user.isAdmin }); }); -// 群聊列表(带最后消息) app.get('/api/rooms', auth, (req, res) => { const rooms = db.prepare('SELECT * FROM rooms').all(); const result = rooms.map(room => { @@ -126,19 +125,26 @@ app.post('/api/rooms/:roomId/messages', auth, (req, res) => { attachments: attachments || [], timestamp }; + // 房间最后消息预览 const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY timestamp DESC LIMIT 1').get(roomId); - broadcastToRoom(roomId, { + const preview = { + room_id: roomId, + last_message: lastMsg ? lastMsg.text : '', + last_time: lastMsg ? lastMsg.timestamp : '' + }; + + // 广播给房间内其他成员,但不包括发送者自己 + broadcastToRoomExcludeSelf(roomId, username, { type: 'new_message', message: msg, - room_preview: { - room_id: roomId, - last_message: lastMsg ? lastMsg.text : '', - last_time: lastMsg ? lastMsg.timestamp : '' - } + room_preview: preview }); + // 加入对话历史 addToHistory(roomId, 'user', `${username}: ${text || '图片'}`); + + // 返回消息给发送者 res.json(msg); }); @@ -186,7 +192,8 @@ app.get('/api/rooms/:roomId/purchases/export', auth, (req, res) => { }); // WebSocket 管理 -const clients = new Map(); +const clients = new Map(); // ws -> { username, roomId } + wss.on('connection', (ws, req) => { console.log('✅ WebSocket 客户端已连接'); const url = new URL(req.url, 'http://localhost'); @@ -220,6 +227,17 @@ wss.on('connection', (ws, req) => { }); }); +// 广播给房间内所有成员,排除指定用户 +function broadcastToRoomExcludeSelf(roomId, excludeUsername, data) { + const message = JSON.stringify(data); + clients.forEach((info, ws) => { + if (info.roomId === roomId && info.username !== excludeUsername && ws.readyState === 1) { + ws.send(message); + } + }); +} + +// 广播给房间内所有人 function broadcastToRoom(roomId, data) { const message = JSON.stringify(data); clients.forEach((info, ws) => { @@ -229,6 +247,7 @@ function broadcastToRoom(roomId, data) { }); } +// 全局广播 function broadcast(data) { const message = JSON.stringify(data); clients.forEach((ws) => { @@ -236,7 +255,7 @@ function broadcast(data) { }); } -// 文本消息存储并广播(用于 AI 回复等内部消息) +// 内部消息存储并广播(用于 AI 回复等) function storeAndBroadcastText(roomId, user, text) { const timestamp = new Date().toLocaleString('zh-CN', { hour12: false }); const result = db.prepare('INSERT INTO messages (room_id, user, text, timestamp) VALUES (?,?,?,?)').run(roomId, user, text, timestamp); @@ -266,7 +285,7 @@ function addToHistory(roomId, role, content) { if (history.length > 60) conversationHistory.set(roomId, history.slice(-40)); } -// AI 处理相关函数(保持不变) +// AI 处理(保持原逻辑) async function analyzeWithDeepSeek(text, historyMessages, username) { const systemPrompt = `你是智能财务助手“小财”。结合对话历史理解用户意图。只返回JSON。