Files
ai-xiaocai/server/handlers.js
2026-07-29 10:47:49 +08:00

201 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// AI 结果处理 + 共享状态
const { v4: uuidv4 } = require('uuid');
const db = require('./db');
const { timestamp, normalizeTime } = require('./utils');
const { broadcastToRoom } = require('./ws');
// 对话历史(内存)
const conversationHistory = new Map();
function getHistory(roomId) { if (!conversationHistory.has(roomId)) conversationHistory.set(roomId, []); return conversationHistory.get(roomId); }
function addToHistory(roomId, role, content) { var h = getHistory(roomId); h.push({ role: role, content: content }); if (h.length > 60) conversationHistory.set(roomId, h.slice(-30)); }
// 待处理操作
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 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;
}
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; }
}
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 {
let query = 'SELECT id, item, amount, payment_method, status, applicant, created_at FROM purchases WHERE room_id = ? AND item LIKE ?';
const params = [roomId, '%' + itemName + '%'];
if (aiResult.quantity !== undefined) { query += ' AND quantity = ?'; params.push(aiResult.quantity); }
if (aiResult.amount !== undefined) { query += ' AND amount = ?'; params.push(aiResult.amount); }
purchases = db.prepare(query).all(...params);
}
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') {
if (aiResult.use_recent_image && (!aiResult.purchase_item || aiResult.purchase_item === 'null')) {
const lastPurchase = db.prepare("SELECT item FROM purchases WHERE room_id = ? AND item IS NOT NULL AND item != '' ORDER BY created_at DESC LIMIT 1").get(roomId);
if (lastPurchase) { aiResult.purchase_item = lastPurchase.item; }
}
const item = aiResult.purchase_item;
if (!item || item === 'null' || item === 'undefined' || item.trim().length < 2) {
storeAndBroadcastText(roomId, '小财', '请提供具体的物品名称。');
addToHistory(roomId, 'assistant', '请提供具体的物品名称。');
return;
}
let usedRecentImage = false;
if (aiResult.use_recent_image) {
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);
if (recentMsg) {
try { const files = JSON.parse(recentMsg.attachments); if (files.length) { attachments = files; usedRecentImage = true; } } catch(e) {}
}
}
if (!usedRecentImage && !attachments?.length && /(图片|附件|加到|存入|关联|作为附件)/.test(originalText)) {
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);
if (recentMsg) {
try { const files = JSON.parse(recentMsg.attachments); if (files.length) { attachments = files; usedRecentImage = true; } } catch(e) {}
}
}
const createdTime = normalizeTime(aiResult.created_time);
let purchase = null;
let replyText = '';
const quantity = aiResult.quantity || 1;
const unitPrice = aiResult.unit_price || 0;
const freight = aiResult.freight || 0;
const amount = quantity * unitPrice + freight;
// 金额为 0 → AI 判断错误,拒绝创建
if (!purchase && amount === 0) {
storeAndBroadcastText(roomId, '小财', '无法创建「' + aiResult.purchase_item + '」:金额为 0请补充价格信息。');
addToHistory(roomId, 'assistant', '金额为0拒绝');
return;
}
const id = uuidv4();
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 };
if (attachments?.length) {
const insertAttach = db.prepare('INSERT INTO purchase_attachments (purchase_id, file_path, uploaded_by, timestamp) VALUES (?,?,?,?)');
attachments.forEach(fp => insertAttach.run(purchase.id, fp, username, now));
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, '添加附件:' + attachments.length + ' 个', username, now);
if (usedRecentImage) replyText = '📎 已为「' + aiResult.purchase_item + '」添加 ' + attachments.length + ' 个附件';
} else if (usedRecentImage) {
replyText = '❌ 未找到最近的图片消息,请先发送图片再试。';
}
storeAndBroadcastText(roomId, '小财', replyText);
addToHistory(roomId, 'assistant', replyText);
broadcastToRoom(roomId, { type: 'purchase_updated' });
return;
}
if (aiResult.action === 'query') {
broadcastToRoom(roomId, { type: 'query_pending', text: '🔍 正在查询采购数据,请稍候...' });
const purchaseData = db.prepare('SELECT item, amount, payment_method, status, created_at FROM purchases WHERE room_id = ? ORDER BY created_at DESC').all(roomId).map(p => p.created_at + ' ' + p.item + ' ¥' + p.amount + ' ' + (p.payment_method||'') + ' ' + p.status).join('\n');
const summaryPrompt = '根据以下采购记录,用自然语言回答用户查询"' + originalText + '"。采购记录:\n' + (purchaseData || '暂无记录');
callDeepSeekForSummary(summaryPrompt, username).then(function(reply) {
console.log('Query汇总回复:', reply.substring(0, 200));
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, userId) {
const { callDeepSeek } = require('./ai/client');
const messages = [
{ role: 'system', content: '你是一个财务助手,请根据采购记录生成简洁回复。' },
{ role: 'user', content: prompt }
];
return callDeepSeek(messages, userId, 0.3, false);
}
module.exports = {
getHistory, addToHistory,
hasPendingAction, setPendingAction, clearPendingAction,
handleAIResult, executePendingAction, storeAndBroadcastText
};