const express = require('express'); const http = require('http'); const { WebSocketServer } = require('ws'); const jwt = require('jsonwebtoken'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { v4: uuidv4 } = require('uuid'); const db = require('./db'); const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server }); // 环境变量 const USERS = {}; (process.env.USERS || '').split(',').forEach(u => { const [name, pass] = u.split(':'); if (name) USERS[name] = pass; }); const ADMINS = (process.env.ADMINS || '').split(','); const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY; const JWT_SECRET = process.env.JWT_SECRET || 'xiaocai-secret-' + Date.now(); // 时区 const TIMEZONE = 'Asia/Shanghai'; function timestamp() { return new Date().toLocaleString('zh-CN', { timeZone: TIMEZONE, hour12: false }); } function currentTimeStr() { return timestamp(); } // 初始化用户 const insertUser = db.prepare('INSERT OR IGNORE INTO users (username, password, is_admin) VALUES (?, ?, ?)'); for (const [username, password] of Object.entries(USERS)) { const isAdmin = ADMINS.includes(username) ? 1 : 0; insertUser.run(username, password, isAdmin); } // 上传配置 const uploadsDir = '/app/data/uploads'; if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true }); const storage = multer.diskStorage({ destination: uploadsDir, filename: (req, file, cb) => { const ext = path.extname(file.originalname) || '.jpg'; const safeName = Date.now() + '-' + Math.random().toString(36).substring(2, 8) + ext; cb(null, safeName); } }); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { const allowed = ['image/jpeg','image/png','image/gif']; cb(null, allowed.includes(file.mimetype)); } }); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/uploads', express.static(uploadsDir)); // 登录 app.post('/api/login', (req, res) => { const { username, password } = req.body; if (USERS[username] && USERS[username] === password) { const token = jwt.sign({ username, isAdmin: ADMINS.includes(username) }, JWT_SECRET, { expiresIn: '7d' }); return res.json({ token, username, isAdmin: ADMINS.includes(username) }); } res.status(401).json({ error: '用户名或密码错误' }); }); const auth = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) return res.status(401).json({ error: '未登录' }); const token = authHeader.split(' ')[1]; try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; next(); } catch (e) { res.status(401).json({ error: '登录已过期' }); } }; const adminOnly = (req, res, next) => { if (!req.user.isAdmin) return res.status(403).json({ error: '仅管理员可执行此操作' }); next(); }; 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 username = req.user.username; const isAdmin = req.user.isAdmin; let rooms; if (isAdmin) { rooms = db.prepare('SELECT * FROM rooms').all(); } else { rooms = db.prepare("SELECT * FROM rooms WHERE white_list = '' OR (',' || white_list || ',' LIKE ?)").all(`%,${username},%`); } const result = rooms.map(room => { const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY id DESC LIMIT 1').get(room.id); return { ...room, last_message: lastMsg ? lastMsg.text : '', last_time: lastMsg ? lastMsg.timestamp : '' }; }); res.json(result); }); app.post('/api/rooms', auth, adminOnly, (req, res) => { const { name, whiteList } = req.body; const id = uuidv4(); const now = timestamp(); db.prepare('INSERT INTO rooms (id, name, created_by, white_list, created_at) VALUES (?,?,?,?,?)').run(id, name, req.user.username, whiteList || '', now); broadcast({ type: 'room_created', room: { id, name, white_list: whiteList || '' } }); res.json({ id, name }); }); app.put('/api/rooms/:roomId', auth, adminOnly, (req, res) => { const { name, whiteList } = req.body; const roomId = req.params.roomId; const room = db.prepare('SELECT * FROM rooms WHERE id = ?').get(roomId); if (!room) return res.status(404).json({ error: '群聊不存在' }); db.prepare('UPDATE rooms SET name = ?, white_list = ? WHERE id = ?').run(name, whiteList || '', roomId); broadcast({ type: 'room_updated', room: { id: roomId, name, white_list: whiteList || '' } }); res.json({ success: true }); }); app.delete('/api/rooms/:roomId', auth, adminOnly, (req, res) => { const roomId = req.params.roomId; db.prepare('DELETE FROM messages WHERE room_id = ?').run(roomId); 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); db.prepare('DELETE FROM rooms WHERE id = ?').run(roomId); broadcast({ type: 'room_deleted', roomId }); res.json({ success: true }); }); app.get('/api/rooms/:roomId/messages', auth, (req, res) => { const msgs = db.prepare('SELECT * FROM messages WHERE room_id = ? ORDER BY id ASC').all(req.params.roomId); res.json(msgs); }); app.post('/api/rooms/:roomId/messages', auth, async (req, res) => { const { text, attachments } = req.body; const roomId = req.params.roomId; const username = req.user.username; const now = timestamp(); const result = db.prepare('INSERT INTO messages (room_id, user, text, attachments, timestamp) VALUES (?,?,?,?,?)').run( roomId, username, text || '', attachments ? JSON.stringify(attachments) : null, now ); const msg = { id: result.lastInsertRowid, room_id: roomId, user: username, text: text || '', attachments: attachments || [], timestamp: now }; const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY id DESC LIMIT 1').get(roomId); broadcastToRoomExcludeSelf(roomId, username, { type: 'new_message', message: msg, room_preview: { room_id: roomId, last_message: lastMsg ? lastMsg.text : '', last_time: lastMsg ? lastMsg.timestamp : '' } }); 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 })); const aiResponse = await analyzeWithDeepSeek(text || '', historyMessages, username, req.user.isAdmin); if (aiResponse && aiResponse.action !== 'ignore') { handleAIResult(aiResponse, roomId, username, text || '', attachments || []); } } catch (e) { console.error('AI 分析失败:', e); } }); app.post('/api/upload', auth, upload.single('file'), (req, res) => { if (!req.file) return res.status(400).json({ error: '请上传图片' }); res.json({ path: '/uploads/' + req.file.filename }); }); app.get('/api/rooms/:roomId/purchases', auth, (req, res) => { const purchases = db.prepare('SELECT * FROM purchases WHERE room_id = ? ORDER BY created_at DESC').all(req.params.roomId); res.json(purchases); }); app.get('/api/purchases/:id', auth, (req, res) => { const pur = db.prepare('SELECT * FROM purchases WHERE id = ?').get(req.params.id); if (!pur) return res.status(404).json({ error: '未找到' }); const history = db.prepare('SELECT * FROM purchase_history WHERE purchase_id = ? ORDER BY id ASC').all(req.params.id); const attachments = db.prepare('SELECT * FROM purchase_attachments WHERE purchase_id = ?').all(req.params.id); res.json({ ...pur, history, attachments }); }); app.get('/api/summary', auth, (req, res) => { const username = req.user.username; const isAdmin = req.user.isAdmin; let rooms; if (isAdmin) { rooms = db.prepare('SELECT id, name FROM rooms').all(); } else { // 使用与群聊列表完全相同的白名单过滤方式 rooms = db.prepare("SELECT id, name FROM rooms WHERE white_list = '' OR (',' || white_list || ',' LIKE ?)").all(`%,${username},%`) } const summary = rooms.map(room => { const stats = db.prepare(`SELECT COUNT(*) as count, SUM(amount) as total FROM purchases WHERE room_id = ?`).get(room.id); return { room_id: room.id, room_name: room.name, purchase_count: stats.count || 0, total_amount: stats.total || 0 }; }); res.json(summary); }); app.get('/api/rooms/:roomId/purchases/export', auth, (req, res) => { const purchases = db.prepare('SELECT * FROM purchases WHERE room_id = ?').all(req.params.roomId); let csv = '时间,事项,数量,单价,金额,邮费,付款方式,发票类型,状态,申请人,备注\n'; purchases.forEach(p => { csv += `${p.created_at},"${p.item||''}",${p.quantity},${p.unit_price},${p.amount},${p.freight},"${p.payment_method||''}","${p.invoice_type||''}","${p.status}","${p.applicant}","${p.remarks||''}"\n`; }); res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Disposition', `attachment; filename="purchases-${req.params.roomId}.csv"`); res.send('\uFEFF' + csv); }); // WebSocket const clients = new Map(); wss.on('connection', (ws, req) => { console.log('✅ WebSocket 客户端已连接'); const url = new URL(req.url, 'http://localhost'); const token = url.searchParams.get('token'); if (!token) return ws.close(); let username; try { const decoded = jwt.verify(token, JWT_SECRET); username = decoded.username; } catch (e) { return ws.close(); } ws.username = username; ws.isAdmin = ADMINS.includes(username); clients.set(ws, { username, roomId: null, isAdmin: ws.isAdmin }); ws.on('message', (data) => { try { const msg = JSON.parse(data); if (msg.type === 'join') { ws.roomId = msg.roomId; clients.set(ws, { username, roomId: msg.roomId, isAdmin: ws.isAdmin }); } } catch (e) {} }); ws.on('close', () => clients.delete(ws)); }); 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) => { if (info.roomId === roomId && ws.readyState === 1) ws.send(message); }); } function broadcast(data) { const message = JSON.stringify(data); clients.forEach((ws) => { if (ws.readyState === 1) ws.send(message); }); } function storeAndBroadcastText(roomId, user, text) { const now = timestamp(); const result = db.prepare('INSERT INTO messages (room_id, user, text, timestamp) VALUES (?,?,?,?)').run(roomId, user, text, now); const msg = { id: result.lastInsertRowid, room_id: roomId, user, text, attachments: [], timestamp: now }; const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY id DESC LIMIT 1').get(roomId); broadcastToRoom(roomId, { type: 'new_message', message: msg, room_preview: { room_id: roomId, last_message: lastMsg ? lastMsg.text : '', last_time: lastMsg ? lastMsg.timestamp : '' } }); return msg; } // 待处理操作 const pendingActions = new Map(); 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 = timestamp(); try { if (action.type === 'delete') { const { itemName, purchaseIds } = action.data; const delA = db.prepare('DELETE FROM purchase_attachments WHERE purchase_id = ?'); const delH = db.prepare('DELETE FROM purchase_history WHERE purchase_id = ?'); const delP = db.prepare('DELETE FROM purchases WHERE id = ?'); for (const id of purchaseIds) { delA.run(id); delH.run(id); delP.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) { storeAndBroadcastText(roomId, '小财', '❌ 操作执行失败,请重试。'); return false; } } // 对话历史 const conversationHistory = new Map(); function getHistory(roomId) { if (!conversationHistory.has(roomId)) conversationHistory.set(roomId, []); return conversationHistory.get(roomId); } function addToHistory(roomId, role, content) { const history = getHistory(roomId); history.push({ role, content }); if (history.length > 60) conversationHistory.set(roomId, history.slice(-40)); } // AI 分析(明确权限不足时返回 chat 而非 ignore) async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin) { const systemPrompt = `你是智能财务助手“小财”。当前服务器时间:${currentTimeStr()}。结合对话历史理解用户意图,只返回JSON。 意图分类: - 采购/付款/发票相关:action="purchase",提取字段:purchase_item, quantity(默认1), unit_price(默认0), amount(默认0), freight(默认0), payment_method(淘宝默认支付宝), invoice_type(默认无票), status, applicant, remarks, created_time - 查询汇总:action="query" - 聊天:action="chat",reply简短回复 - 删除指定物品:action="delete",delete_item为物品名 - 清空所有采购数据:action="clear_all" - 忽略(仅当完全无关):action="ignore" ⚠️ 重要权限规则: - ⚠️ 当前用户${username},管理员状态:${isAdmin}。仅管理员可执行删除/清空操作。 - 如果用户要求删除或清空,且 isAdmin 为 false,你必须返回 action="chat" 并说明“仅管理员可操作”。 - 如果 isAdmin 为 true,正常返回 delete 或 clear_all。 - 如果用户要求“删除金额为0的记录”或“删除没有名字的记录”等无法用具体物品名描述的删除请求,你可以使用以下特殊 delete_item 值: - 删除所有金额为0的记录:delete_item="__AMOUNT_ZERO__" - 删除物品名为空、null、undefined 的记录:delete_item="__NULL_NAME__" - 不要再返回 action="ask" 来处理删除请求,只要确定是删除意图,就必须返回 action="delete" 并填好 delete_item。 - 若用户描述的是特定记录(如“名字叫XXX的”),则 delete_item 填那个物品名。 ⚠️ 附件补充规则: - 如果用户发送图片并明确说"这是XX的图片"或"作为附件存到XX采购里", 你只需返回 purchase_item 为 XX,**绝对不要**返回 quantity、unit_price、amount、freight、payment_method 等任何字段。 - 当消息中只有图片且文本明确指出"这是XX的图片"时,提取 purchase_item 为 XX,其余字段全部省略。 - 如果用户要求"把刚才/之前的图片作为附件存到XX采购",你可以返回 {"action":"purchase","purchase_item":"XX","use_recent_image":true}, 不提供任何数量、价格字段,系统会自动查找最近一张图片并关联。 ⚠️ 采购规则: - 新建 vs 更新(极其重要): * 如果用户提到"昨天/今天/刚刚/又/再/新/另一批/另外"等时间或重复购买词汇,必须返回 "is_new": true,系统会强制创建新记录。 * 如果用户明确说"改一下/更新/修改/调整/更正"等词汇,才不提供 is_new 或设为 false,系统会尝试匹配已有记录更新。 * 当无法确定是新建还是更新时,默认视为新建(返回 is_new: true)。 - 用户补充信息时更新已有记录。更新时请返回完整字段值,包括未变化的字段。如果用户要求修改时间,请生成正确的created_time字段。 - 总金额(amount)由系统自动按"数量×单价+邮费"计算,你无需提供该字段。 - 如果无法确定 purchase_item(物品名称),必须 action="ask" 并追问"请提供物品名称",绝不允许将 purchase_item 设为空字符串、null 或 undefined。`; const messages = [ { role: 'system', content: systemPrompt }, ...historyMessages.slice(-30), { role: 'user', content: `${username}: ${text}` } ]; console.log('🤖 调用 DeepSeek...'); const res = await fetch('https://api.deepseek.com/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_API_KEY}` }, body: JSON.stringify({ model: 'deepseek-v4-flash', messages, temperature: 0.1, stream: false }) }); const data = await res.json(); if (!data.choices || !data.choices[0]) { console.error('🤖 DeepSeek 返回异常:', JSON.stringify(data)); throw new Error('DeepSeek API 返回异常: ' + (data.error?.message || JSON.stringify(data))); } const content = data.choices[0].message.content; console.log('🤖 AI 返回:', content); return JSON.parse(content.replace(/```json|```/g, '').trim()); } function handleAIResult(aiResult, roomId, username, originalText, attachments) { const now = timestamp(); if (aiResult.action === 'ask') { const q = aiResult.question || aiResult.reply || '请提供更多信息。'; storeAndBroadcastText(roomId, '小财', q); addToHistory(roomId, 'assistant', q); return; } if (aiResult.action === 'delete') { const itemName = aiResult.delete_item; let purchases; if (itemName === '__AMOUNT_ZERO__') { purchases = db.prepare('SELECT id, item, amount, payment_method, status, applicant, created_at FROM purchases WHERE room_id = ? AND amount = 0').all(roomId); } else if (itemName === '__NULL_NAME__') { purchases = db.prepare("SELECT id, item, amount, payment_method, status, applicant, created_at FROM purchases WHERE room_id = ? AND (item IS NULL OR item = '' OR item = 'null' OR item = 'undefined')").all(roomId); } else { purchases = db.prepare('SELECT id, item, amount, payment_method, status, applicant, created_at FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`); } if (purchases.length === 0) { const label = itemName === '__AMOUNT_ZERO__' ? '金额为0' : (itemName === '__NULL_NAME__' ? '名称为空' : `"${itemName}"`); storeAndBroadcastText(roomId, '小财', `没有找到与"${label}"相关的采购记录。`); return; } 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') { const count = db.prepare('SELECT COUNT(*) as count FROM purchases WHERE room_id = ?').get(roomId).count; if (count === 0) { storeAndBroadcastText(roomId, '小财', '当前房间没有采购记录,无需清空。'); return; } storeAndBroadcastText(roomId, '小财', `⚠️ 即将清空当前房间的 ${count} 条采购数据,请回复“确认”继续,否则忽略。`); addToHistory(roomId, 'assistant', `请求清空${count}条记录`); setPendingAction(roomId, username, { type: 'clear', data: {} }); return; } if (aiResult.action === 'purchase') { // 引用最近图片:用户要求把之前的图片关联到某采购 let usedRecentImage = false; if (aiResult.use_recent_image) { console.log('🔍 use_recent_image: 查找最近图片消息, roomId=', roomId); const recentMsg = db.prepare("SELECT attachments FROM messages WHERE room_id = ? AND attachments IS NOT NULL AND attachments != '' AND attachments != '[]' ORDER BY id DESC LIMIT 1").get(roomId); console.log('🔍 最近消息:', recentMsg ? recentMsg.attachments : '(无)'); if (recentMsg) { try { const files = JSON.parse(recentMsg.attachments); if (files.length) { attachments = files; usedRecentImage = true; console.log('✅ 引用图片:', files.length, '个'); } } catch(e) { console.error('❌ 解析附件失败:', e); } } } const createdTime = aiResult.created_time || now; // 新建 vs 更新:AI 标记 is_new 时强制新建,不尝试匹配已有记录 const isNew = aiResult.is_new === true; let purchase; if (!isNew) { purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? AND status != ?').get(roomId, `%${aiResult.purchase_item}%`, '已完成'); } console.log('📦 purchase_item:', aiResult.purchase_item, isNew ? '(强制新建)' : purchase ? '(匹配已有)' : '(未匹配-新建)'); let replyText = ''; const quantity = aiResult.quantity || 1; const unitPrice = aiResult.unit_price || 0; const freight = aiResult.freight || 0; const amount = quantity * unitPrice + freight; if (!purchase) { const id = uuidv4(); const item = aiResult.purchase_item; const paymentMethod = aiResult.payment_method || (aiResult.method === '淘宝' ? '支付宝' : (aiResult.method || '未指定')); const invoiceType = aiResult.invoice_type || '无票'; const status = aiResult.status || '待处理'; const applicant = aiResult.applicant || username; const remarks = aiResult.remarks || ''; db.prepare(`INSERT INTO purchases (id, room_id, item, quantity, unit_price, amount, freight, payment_method, invoice_type, status, applicant, remarks, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run( id, roomId, item, quantity, unitPrice, amount, freight, paymentMethod, invoiceType, status, applicant, remarks, createdTime ); db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(id, `创建采购:${item},数量${quantity},金额¥${amount},状态${status}`, '小财', now); replyText = `✅ 已记录采购:${item},数量 ${quantity},金额 ¥${amount},状态 ${status}`; purchase = { id }; } else { const old = purchase; const newVals = { quantity: aiResult.quantity ?? old.quantity, unit_price: aiResult.unit_price ?? old.unit_price, freight: aiResult.freight ?? old.freight, payment_method: aiResult.payment_method || (aiResult.method === '淘宝' ? '支付宝' : (aiResult.method || old.payment_method)), invoice_type: aiResult.invoice_type || old.invoice_type, status: aiResult.status || old.status, applicant: aiResult.applicant || old.applicant, remarks: aiResult.remarks !== undefined ? aiResult.remarks : old.remarks, created_at: aiResult.created_time || old.created_at }; if (aiResult.amount !== undefined && aiResult.amount !== null) { // 始终根据数量、单价、邮费重新计算总金额,确保准确 // newVals.amount = aiResult.amount; newVals.amount = newVals.quantity * newVals.unit_price + newVals.freight; } else { newVals.amount = newVals.unit_price * newVals.quantity + newVals.freight; } const historyChanges = []; let changed = false; for (const [field, newVal] of Object.entries(newVals)) { if (String(newVal) !== String(old[field])) { changed = true; historyChanges.push(`${field}: ${old[field]} → ${newVal}`); db.prepare(`UPDATE purchases SET ${field} = ? WHERE id = ?`).run(newVal, purchase.id); } } if (changed) { db.prepare('UPDATE purchases SET updated_at = ? WHERE id = ?').run(now, purchase.id); db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, `更新:${historyChanges.join(';')}`, username, now); replyText = `🔄 已更新「${aiResult.purchase_item}」:${historyChanges.join(',')}`; } else { replyText = `ℹ️ 采购「${aiResult.purchase_item}」的信息没有发生变化。`; } } if (attachments?.length) { console.log('📎 插入附件:', attachments.length, '个, purchase_id=', purchase.id); const insertAttach = db.prepare('INSERT INTO purchase_attachments (purchase_id, file_path, uploaded_by, timestamp) VALUES (?,?,?,?)'); attachments.forEach(fp => { console.log(' 📎', fp); insertAttach.run(purchase.id, fp, username, now); }); if (usedRecentImage) { replyText = `📎 已为「${aiResult.purchase_item}」关联 ${attachments.length} 个附件`; db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, `添加附件:${attachments.length} 个`, username, now); console.log('✅ usedRecentImage 完成, replyText:', replyText); } } else if (usedRecentImage) { replyText = `❌ 未找到最近的图片消息,请先发送图片再试。`; console.log('⚠️ usedRecentImage 但附件为空'); } storeAndBroadcastText(roomId, '小财', replyText); addToHistory(roomId, 'assistant', replyText); broadcastToRoom(roomId, { type: 'purchase_updated' }); return; } if (aiResult.action === 'query') { const purchaseData = db.prepare('SELECT item, amount, payment_method, status FROM purchases WHERE room_id = ? ORDER BY created_at DESC').all(roomId).map(p => `${p.item} ¥${p.amount} ${p.payment_method||''} ${p.status}`).join('\n'); const summaryPrompt = `根据以下采购记录,用自然语言回答用户查询“${originalText}”。采购记录:\n${purchaseData || '暂无记录'}`; callDeepSeekForSummary(summaryPrompt).then(reply => { storeAndBroadcastText(roomId, '小财', reply); addToHistory(roomId, 'assistant', reply); }); return; } if (aiResult.action === 'chat') { storeAndBroadcastText(roomId, '小财', aiResult.reply || '好的。'); addToHistory(roomId, 'assistant', aiResult.reply); return; } } async function callDeepSeekForSummary(prompt) { const res = await fetch('https://api.deepseek.com/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_API_KEY}` }, body: JSON.stringify({ model: 'deepseek-v4-flash', messages: [ { role: 'system', content: '你是一个财务助手,请根据采购记录生成简洁回复。' }, { role: 'user', content: prompt } ], temperature: 0.3, stream: false }) }); const data = await res.json(); return data.choices[0].message.content; } server.listen(process.env.PORT || 3000, () => console.log(`Server running on port ${process.env.PORT || 3000}`));