From d2d8b9325a104a191b9d97cfb4a62a865310499a Mon Sep 17 00:00:00 2001 From: kicer Date: Wed, 22 Jul 2026 10:55:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=93=8D=E4=BD=9C=E5=8A=A0?= =?UTF-8?q?=E5=85=A5=E7=AE=A1=E7=90=86=E5=91=98=E6=9D=83=E9=99=90=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/server.js | 135 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 34 deletions(-) diff --git a/server/server.js b/server/server.js index e7689cb..52b57c7 100644 --- a/server/server.js +++ b/server/server.js @@ -76,11 +76,8 @@ const auth = (req, res, next) => { } }; -// 管理员权限检查中间件 const adminOnly = (req, res, next) => { - if (!req.user.isAdmin) { - return res.status(403).json({ error: '仅管理员可执行此操作' }); - } + if (!req.user.isAdmin) return res.status(403).json({ error: '仅管理员可执行此操作' }); next(); }; @@ -150,6 +147,12 @@ app.post('/api/rooms/:roomId/messages', auth, async (req, res) => { addToHistory(roomId, 'user', `${username}: ${text || '图片'}`); res.json(msg); + // 检查是否有待确认操作且用户回复了“确认” + if (text && text.trim() === '确认' && hasPendingAction(roomId, username)) { + executePendingAction(roomId, username); + return; + } + try { const history = getHistory(roomId); const historyMessages = history.map(e => ({ role: e.role, content: e.content })); @@ -205,7 +208,6 @@ app.get('/api/rooms/:roomId/purchases/export', auth, (req, res) => { res.send('\uFEFF' + csv); }); -// 清空所有采购数据(管理员) app.post('/api/rooms/:roomId/purchases/clear', auth, adminOnly, (req, res) => { const roomId = req.params.roomId; db.prepare('DELETE FROM purchase_attachments WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); @@ -217,7 +219,6 @@ app.post('/api/rooms/:roomId/purchases/clear', auth, adminOnly, (req, res) => { // WebSocket 管理 const clients = new Map(); - wss.on('connection', (ws, req) => { console.log('✅ WebSocket 客户端已连接'); const url = new URL(req.url, 'http://localhost'); @@ -234,7 +235,7 @@ wss.on('connection', (ws, req) => { ws.username = username; ws.isAdmin = ADMINS.includes(username); - clients.set(ws, { username, roomId: null, isAdmin: ADMINS.includes(username) }); + clients.set(ws, { username, roomId: null, isAdmin: ws.isAdmin }); ws.on('message', (data) => { try { @@ -293,6 +294,59 @@ function storeAndBroadcastText(roomId, user, text) { return msg; } +// ---------- 待处理操作管理 ---------- +const pendingActions = new Map(); // key: `${roomId}:${username}`, value: { type, data } + +function hasPendingAction(roomId, username) { + return pendingActions.has(`${roomId}:${username}`); +} + +function setPendingAction(roomId, username, action) { + pendingActions.set(`${roomId}:${username}`, action); +} + +function clearPendingAction(roomId, username) { + pendingActions.delete(`${roomId}:${username}`); +} + +function executePendingAction(roomId, username) { + const action = pendingActions.get(`${roomId}:${username}`); + if (!action) return false; + clearPendingAction(roomId, username); + + const now = new Date().toLocaleString('zh-CN', { hour12: false }); + try { + if (action.type === 'delete') { + const { itemName, purchaseIds } = action.data; + const deleteAttachments = db.prepare('DELETE FROM purchase_attachments WHERE purchase_id = ?'); + const deleteHistory = db.prepare('DELETE FROM purchase_history WHERE purchase_id = ?'); + const deletePurchase = db.prepare('DELETE FROM purchases WHERE id = ?'); + for (const id of purchaseIds) { + deleteAttachments.run(id); + deleteHistory.run(id); + deletePurchase.run(id); + } + const reply = `✅ 已删除采购记录:${itemName}(共 ${purchaseIds.length} 条)`; + storeAndBroadcastText(roomId, '小财', reply); + addToHistory(roomId, 'assistant', reply); + } else if (action.type === 'clear') { + db.prepare('DELETE FROM purchase_attachments WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); + db.prepare('DELETE FROM purchase_history WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); + db.prepare('DELETE FROM purchases WHERE room_id = ?').run(roomId); + const reply = '✅ 已清空当前房间的所有采购数据。'; + storeAndBroadcastText(roomId, '小财', reply); + addToHistory(roomId, 'assistant', reply); + } + broadcastToRoom(roomId, { type: 'purchase_updated' }); + return true; + } catch (e) { + console.error('执行待处理操作失败:', e); + storeAndBroadcastText(roomId, '小财', '❌ 操作执行失败,请重试。'); + return false; + } +} + +// 对话历史 const conversationHistory = new Map(); function getHistory(roomId) { if (!conversationHistory.has(roomId)) conversationHistory.set(roomId, []); @@ -311,16 +365,20 @@ async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin) { - 采购/付款/发票相关:action="purchase",提取purchase_item, amount, method, status, applicant, created_time - 查询汇总:action="query" - 聊天:action="chat",reply简短回复 -- 删除指定物品:action="delete",delete_item物品名 -- 清空所有采购数据(仅管理员):action="clear_all",注意:只有管理员可以请求清空,如果不是管理员但要求清空,应拒绝并告知只有管理员可操作。 +- 删除指定物品:action="delete",delete_item为物品名 +- 清空所有采购数据:action="clear_all" - 忽略:action="ignore" +重要权限规则: +- 删除和清空操作只有管理员可以执行。当前用户${username}的管理员状态:${isAdmin}。 +- 如果用户要求删除或清空,但当前用户不是管理员,你必须直接回复拒绝,返回action="chat",reply说明需要管理员权限。 +- 如果用户是管理员,返回delete或clear_all action。 + 采购规则: - 金额缺失=0,方式缺失="未指定",申请人缺失="${username}"。 - 状态:已付→已采购,发票→发票已收,否则待采购。 -- 时间提取:如果用户提到具体时间,设置created_time字段,格式yyyy/MM/dd HH:mm:ss。 - -当前用户${username},管理员状态:${isAdmin}。`; +- 用户补充信息时更新已有记录。 +- 时间提取:如果用户提到具体时间,设置created_time字段,格式yyyy/MM/dd HH:mm:ss。`; const messages = [ { role: 'system', content: systemPrompt }, @@ -348,40 +406,47 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) { addToHistory(roomId, 'assistant', aiResult.question); return; } + + // 删除指定物品(管理员权限) if (aiResult.action === 'delete') { const itemName = aiResult.delete_item; - const purchases = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`); + const purchases = db.prepare('SELECT id, item, amount, method, status, applicant, created_at FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`); if (purchases.length === 0) { storeAndBroadcastText(roomId, '小财', `没有找到与“${itemName}”相关的采购记录。`); addToHistory(roomId, 'assistant', `没有找到与“${itemName}”相关的采购记录。`); return; } - const deleteAttachments = db.prepare('DELETE FROM purchase_attachments WHERE purchase_id = ?'); - const deleteHistory = db.prepare('DELETE FROM purchase_history WHERE purchase_id = ?'); - const deletePurchase = db.prepare('DELETE FROM purchases WHERE id = ?'); - for (const p of purchases) { - deleteAttachments.run(p.id); - deleteHistory.run(p.id); - deletePurchase.run(p.id); - } - const reply = `已删除采购记录:${itemName}(共 ${purchases.length} 条)`; - storeAndBroadcastText(roomId, '小财', reply); - addToHistory(roomId, 'assistant', reply); - broadcastToRoom(roomId, { type: 'purchase_updated' }); + // 构建确认消息 + let confirmText = `⚠️ 即将删除以下 ${purchases.length} 条采购记录,请回复“确认”继续:\n\n`; + purchases.forEach(p => { + confirmText += `• ${p.item} | ¥${p.amount} | ${p.status} | ${p.applicant} | ${p.created_at}\n`; + }); + confirmText += `\n如果不删除,请忽略此消息。`; + storeAndBroadcastText(roomId, '小财', confirmText); + addToHistory(roomId, 'assistant', confirmText); + // 存储待处理操作 + setPendingAction(roomId, username, { + type: 'delete', + data: { itemName, purchaseIds: purchases.map(p => p.id) } + }); return; } + + // 清空所有采购数据(管理员权限) if (aiResult.action === 'clear_all') { - // 管理员才可以清空,但已经由 AI 判断了权限,这里再检查一次 - // 清空所有采购数据(当前房间) - db.prepare('DELETE FROM purchase_attachments WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); - db.prepare('DELETE FROM purchase_history WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); - db.prepare('DELETE FROM purchases WHERE room_id = ?').run(roomId); - const reply = '已清空当前房间的所有采购数据。'; - storeAndBroadcastText(roomId, '小财', reply); - addToHistory(roomId, 'assistant', reply); - broadcastToRoom(roomId, { type: 'purchase_updated' }); + const count = db.prepare('SELECT COUNT(*) as count FROM purchases WHERE room_id = ?').get(roomId).count; + if (count === 0) { + storeAndBroadcastText(roomId, '小财', '当前房间没有采购记录,无需清空。'); + addToHistory(roomId, 'assistant', '当前房间没有采购记录,无需清空。'); + return; + } + const confirmText = `⚠️ 即将清空当前房间的 ${count} 条采购数据,请回复“确认”继续,否则忽略。`; + storeAndBroadcastText(roomId, '小财', confirmText); + addToHistory(roomId, 'assistant', confirmText); + setPendingAction(roomId, username, { type: 'clear', data: {} }); return; } + if (aiResult.action === 'purchase') { const createdTime = aiResult.created_time || now; let purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? AND status != ?').get(roomId, `%${aiResult.purchase_item}%`, '已完成'); @@ -410,6 +475,7 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) { broadcastToRoom(roomId, { type: 'purchase_updated' }); return; } + if (aiResult.action === 'query') { const purchaseData = db.prepare('SELECT item, amount, method, status FROM purchases WHERE room_id = ? ORDER BY created_at DESC').all(roomId) .map(p => `${p.item} ¥${p.amount} ${p.status}`).join('\n'); @@ -420,6 +486,7 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) { }); return; } + if (aiResult.action === 'chat') { const reply = aiResult.reply || '好的,有什么需要帮助的吗?'; storeAndBroadcastText(roomId, '小财', reply);