278 lines
16 KiB
JavaScript
278 lines
16 KiB
JavaScript
// 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) { const h = getHistory(roomId); h.push({ role, content }); if (h.length > 60) conversationHistory.set(roomId, h.slice(-40)); }
|
||
|
||
// 待处理操作
|
||
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); }
|
||
console.log('🔍 删除查询:', query, params);
|
||
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') {
|
||
// use_recent_image 但缺 purchase_item → 从房间最近一条采购推断
|
||
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) {
|
||
console.log('🛡️ use_recent_image 缺 purchase_item,推断为:', lastPurchase.item);
|
||
aiResult.purchase_item = lastPurchase.item;
|
||
}
|
||
}
|
||
const item = aiResult.purchase_item;
|
||
if (!item || item === 'null' || item === 'undefined' || item.trim().length < 2) {
|
||
console.log('🚫 拒绝无效 purchase_item:', JSON.stringify(item));
|
||
storeAndBroadcastText(roomId, '小财', '请提供具体的物品名称。');
|
||
addToHistory(roomId, 'assistant', '请提供具体的物品名称。');
|
||
return;
|
||
}
|
||
console.log('🛒 purchase 分支, item:', item, 'attachments:', JSON.stringify(attachments));
|
||
|
||
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);
|
||
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); }
|
||
}
|
||
}
|
||
// 安全网:消息提到图片/附件但没带附件 → AI 忘设 use_recent_image,自动补
|
||
if (!usedRecentImage && !attachments?.length && /(图片|附件|加到|存入|关联|作为附件)/.test(originalText)) {
|
||
console.log('🔍 安全网: 文本提及附件但无附件,自动尝试 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; console.log('✅ 安全网引用图片:', files.length, '个'); }
|
||
} catch(e) {}
|
||
}
|
||
}
|
||
|
||
const createdTime = normalizeTime(aiResult.created_time);
|
||
let isNew = aiResult.is_new === true;
|
||
let purchase;
|
||
if (isNew && attachments?.length > 0) {
|
||
const existing = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? ORDER BY created_at DESC LIMIT 1').get(roomId, `%${aiResult.purchase_item}%`);
|
||
if (existing) { console.log('🛡️ 兜底纠正: 转为更新模式'); isNew = false; purchase = existing; }
|
||
}
|
||
if (!isNew && !purchase) {
|
||
purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? ORDER BY created_at DESC LIMIT 1').get(roomId, `%${aiResult.purchase_item}%`);
|
||
// 全名匹配失败 → 去掉括号内容后用剩余关键词回退匹配
|
||
if (!purchase) {
|
||
const keyword = aiResult.purchase_item.replace(/[((].*[))]/g, '').trim();
|
||
if (keyword.length >= 2) {
|
||
console.log('🔍 全名不匹配,尝试关键词:', keyword);
|
||
purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? ORDER BY created_at DESC LIMIT 1').get(roomId, `%${keyword}%`);
|
||
}
|
||
}
|
||
// 所有 LIKE 失败 → 回退到房间内最近一条采购
|
||
if (!purchase) {
|
||
console.log('🔍 LIKE 全失败,回退到最近记录');
|
||
purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? ORDER BY created_at DESC LIMIT 1').get(roomId);
|
||
}
|
||
}
|
||
console.log('📦 purchase_item:', aiResult.purchase_item, isNew ? '(强制新建)' : purchase ? '(匹配已有)' : '(未匹配-新建)');
|
||
|
||
// is_new 未明确设为 false、且所有回退匹配均失败 → 视为新建
|
||
if (!isNew && !purchase) {
|
||
console.log('📦 未匹配到已有记录,转为新建模式');
|
||
isNew = true;
|
||
}
|
||
|
||
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 && aiResult.quantity === undefined && aiResult.unit_price === undefined) {
|
||
console.log('🚫 拒绝新建 amount=0 记录, item:', aiResult.purchase_item);
|
||
storeAndBroadcastText(roomId, '小财', `无法创建「${aiResult.purchase_item}」:缺少数量和价格信息,请补充。`);
|
||
addToHistory(roomId, 'assistant', '缺少数量和价格');
|
||
return;
|
||
}
|
||
|
||
if (!purchase) {
|
||
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 };
|
||
} 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
|
||
};
|
||
newVals.amount = newVals.quantity * newVals.unit_price + 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); });
|
||
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, `添加附件:${attachments.length} 个`, username, now);
|
||
if (replyText.includes('没有发生') || usedRecentImage) {
|
||
replyText = `📎 已为「${aiResult.purchase_item}」添加 ${attachments.length} 个附件`;
|
||
}
|
||
console.log('✅ 附件完成, replyText:', replyText);
|
||
} else if (usedRecentImage) {
|
||
replyText = '❌ 未找到最近的图片消息,请先发送图片再试。';
|
||
}
|
||
|
||
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 { callDeepSeek } = require('./ai/client');
|
||
const messages = [
|
||
{ role: 'system', content: '你是一个财务助手,请根据采购记录生成简洁回复。' },
|
||
{ role: 'user', content: prompt }
|
||
];
|
||
return callDeepSeek(messages, 0.3);
|
||
}
|
||
|
||
module.exports = {
|
||
getHistory, addToHistory,
|
||
hasPendingAction, setPendingAction, clearPendingAction,
|
||
handleAIResult, executePendingAction, storeAndBroadcastText
|
||
};
|