198 lines
12 KiB
JavaScript
198 lines
12 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) { 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 = '\u2705 \u5df2\u5220\u9664\u91c7\u8d2d\u8bb0\u5f55\uff1a' + itemName + '\uff08\u5171 ' + purchaseIds.length + ' \u6761\uff09';
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', 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 = '\u2705 \u5df2\u6e05\u7a7a\u5f53\u524d\u623f\u95f4\u7684\u6240\u6709\u91c7\u8d2d\u6570\u636e\u3002';
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', reply); addToHistory(roomId, 'assistant', reply);
|
|
}
|
|
broadcastToRoom(roomId, { type: 'purchase_updated' });
|
|
return true;
|
|
} catch (e) { storeAndBroadcastText(roomId, '\u5c0f\u8d22', '\u274c \u64cd\u4f5c\u6267\u884c\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002'); return false; }
|
|
}
|
|
|
|
function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
|
const now = timestamp();
|
|
|
|
if (aiResult.action === 'ask') {
|
|
const q = aiResult.question || aiResult.reply || '\u8bf7\u63d0\u4f9b\u66f4\u591a\u4fe1\u606f\u3002';
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', 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__' ? '\u91d1\u989d\u4e3a0' : (itemName === '__NULL_NAME__' ? '\u540d\u79f0\u4e3a\u7a7a' : '"' + itemName + '"');
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', '\u6ca1\u6709\u627e\u5230\u4e0e"' + label + '"\u76f8\u5173\u7684\u91c7\u8d2d\u8bb0\u5f55\u3002');
|
|
return;
|
|
}
|
|
let confirmText = '\u26a0\ufe0f \u5373\u5c06\u5220\u9664\u4ee5\u4e0b ' + purchases.length + ' \u6761\u91c7\u8d2d\u8bb0\u5f55\uff0c\u8bf7\u56de\u590d"\u786e\u8ba4"\u7ee7\u7eed\uff1a\n\n';
|
|
purchases.forEach(p => confirmText += '\u2022 ' + p.item + ' | ' + p.amount + ' | ' + p.status + ' | ' + p.applicant + ' | ' + p.created_at + '\n');
|
|
confirmText += '\n\u5982\u679c\u4e0d\u5220\u9664\uff0c\u8bf7\u5ffd\u7565\u6b64\u6d88\u606f\u3002';
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', 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, '\u5c0f\u8d22', '\u5f53\u524d\u623f\u95f4\u6ca1\u6709\u91c7\u8d2d\u8bb0\u5f55\uff0c\u65e0\u9700\u6e05\u7a7a\u3002'); return; }
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', '\u26a0\ufe0f \u5373\u5c06\u6e05\u7a7a\u5f53\u524d\u623f\u95f4\u7684 ' + count + ' \u6761\u91c7\u8d2d\u6570\u636e\uff0c\u8bf7\u56de\u590d"\u786e\u8ba4"\u7ee7\u7eed\uff0c\u5426\u5219\u5ffd\u7565\u3002');
|
|
addToHistory(roomId, 'assistant', '\u8bf7\u6c42\u6e05\u7a7a' + count + '\u6761\u8bb0\u5f55');
|
|
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) { aiResult.purchase_item = lastPurchase.item; }
|
|
}
|
|
const item = aiResult.purchase_item;
|
|
if (!item || item === 'null' || item === 'undefined' || item.trim().length < 2) {
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', '\u8bf7\u63d0\u4f9b\u5177\u4f53\u7684\u7269\u54c1\u540d\u79f0\u3002');
|
|
addToHistory(roomId, 'assistant', '\u8bf7\u63d0\u4f9b\u5177\u4f53\u7684\u7269\u54c1\u540d\u79f0\u3002');
|
|
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 && /(\u56fe\u7247|\u9644\u4ef6|\u52a0\u5230|\u5b58\u5165|\u5173\u8054|\u4f5c\u4e3a\u9644\u4ef6)/.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);
|
|
// AI 不负责更新 — 聊天永远新建,修改由用户从采购清单手动完成
|
|
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;
|
|
|
|
if (!purchase && amount === 0 && aiResult.quantity === undefined && aiResult.unit_price === undefined) {
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', '\u65e0\u6cd5\u521b\u5efa\u300c' + aiResult.purchase_item + '\u300d\uff1a\u7f3a\u5c11\u6570\u91cf\u548c\u4ef7\u683c\u4fe1\u606f\uff0c\u8bf7\u8865\u5145\u3002');
|
|
addToHistory(roomId, 'assistant', '\u7f3a\u5c11\u6570\u91cf\u548c\u4ef7\u683c');
|
|
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, '\u6dfb\u52a0\u9644\u4ef6\uff1a' + attachments.length + ' \u4e2a', username, now);
|
|
if (replyText.includes('\u6ca1\u6709\u53d1\u751f') || usedRecentImage) replyText = '\ud83d\udcce \u5df2\u4e3a\u300c' + aiResult.purchase_item + '\u300d\u6dfb\u52a0 ' + attachments.length + ' \u4e2a\u9644\u4ef6';
|
|
} else if (usedRecentImage) {
|
|
replyText = '\u274c \u672a\u627e\u5230\u6700\u8fd1\u7684\u56fe\u7247\u6d88\u606f\uff0c\u8bf7\u5148\u53d1\u9001\u56fe\u7247\u518d\u8bd5\u3002';
|
|
}
|
|
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', 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 = '\u6839\u636e\u4ee5\u4e0b\u91c7\u8d2d\u8bb0\u5f55\uff0c\u7528\u81ea\u7136\u8bed\u8a00\u56de\u7b54\u7528\u6237\u67e5\u8be2"' + originalText + '"' + '\u3002\u91c7\u8d2d\u8bb0\u5f55\uff1a\n' + (purchaseData || '\u6682\u65e0\u8bb0\u5f55');
|
|
callDeepSeekForSummary(summaryPrompt, username).then(reply => { storeAndBroadcastText(roomId, '小财', reply); addToHistory(roomId, 'assistant', reply); });
|
|
return;
|
|
}
|
|
|
|
if (aiResult.action === 'chat') {
|
|
storeAndBroadcastText(roomId, '\u5c0f\u8d22', aiResult.reply || '\u597d\u7684\u3002');
|
|
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
|
|
};
|