完善字段信息

This commit is contained in:
2026-07-22 12:03:30 +08:00
parent 4ff7a490d9
commit a8bb6c2fcd
3 changed files with 144 additions and 95 deletions

View File

@@ -22,11 +22,14 @@ 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 (?, ?, ?)');
@@ -113,7 +116,6 @@ app.post('/api/rooms', auth, adminOnly, (req, res) => {
res.json({ id, name });
});
// 更新群聊信息(管理员)
app.put('/api/rooms/:roomId', auth, adminOnly, (req, res) => {
const { name, whiteList } = req.body;
const roomId = req.params.roomId;
@@ -124,7 +126,6 @@ app.put('/api/rooms/:roomId', auth, adminOnly, (req, res) => {
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);
@@ -141,7 +142,6 @@ app.get('/api/rooms/:roomId/messages', auth, (req, res) => {
res.json(msgs);
});
// 发送消息(统一入口,异步触发 AI
app.post('/api/rooms/:roomId/messages', auth, async (req, res) => {
const { text, attachments } = req.body;
const roomId = req.params.roomId;
@@ -161,22 +161,15 @@ app.post('/api/rooms/:roomId/messages', auth, async (req, res) => {
};
const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY timestamp DESC LIMIT 1').get(roomId);
const preview = {
room_id: roomId,
last_message: lastMsg ? lastMsg.text : '',
last_time: lastMsg ? lastMsg.timestamp : ''
};
broadcastToRoomExcludeSelf(roomId, username, {
type: 'new_message',
message: msg,
room_preview: preview
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;
@@ -228,9 +221,9 @@ app.get('/api/summary', auth, (req, res) => {
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';
let csv = '时间,事项,数量,单价,金额,邮费,付款方式,发票类型,状态,申请人,备注\n';
purchases.forEach(p => {
csv += `${p.created_at},"${p.item||''}",${p.amount},"${p.method||''}","${p.status}","${p.applicant}"\n`;
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"`);
@@ -310,7 +303,7 @@ function storeAndBroadcastText(roomId, user, text) {
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); }
@@ -343,40 +336,30 @@ function executePendingAction(roomId, username) {
} catch (e) { console.error('执行待处理操作失败:', 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));
}
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 函数 ----------
// AI 分析
async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin) {
const systemPrompt = `你是智能财务助手“小财”。结合对话历史理解用户意图只返回JSON。
const systemPrompt = `你是智能财务助手“小财”。当前服务器时间:${currentTimeStr()}结合对话历史理解用户意图只返回JSON。
意图分类:
- 采购/付款/发票相关action="purchase",提取purchase_item, amount, method, status, applicant, created_time(可选根据聊天中的时间推断格式yyyy/MM/dd HH:mm:ss)
- 采购/付款/发票相关action="purchase",提取以下字段(所有字段可选,缺失则不返回):
purchase_item (物品名称), quantity (数量默认1), unit_price (单价默认0), amount (总金额默认0),
freight (邮费默认0), payment_method (付款方式:支付宝/微信/对公转账/现金等,淘宝购买默认支付宝),
invoice_type (发票类型:增票/普票/无票,默认无票), status (待采购/已采购/发票已收), applicant (申请人),
remarks (备注,重要信息如“下次换商家”“缺少配件”等), created_time (如果用户提到具体时间则按yyyy/MM/dd HH:mm:ss格式输出)
- 查询汇总action="query"
- 聊天action="chat"reply简短回复
- 删除指定物品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字段。`;
const messages = [
{ role: 'system', content: systemPrompt },
@@ -405,10 +388,9 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
return;
}
// 删除指定物品(管理员权限)
if (aiResult.action === 'delete') {
const itemName = aiResult.delete_item;
const purchases = db.prepare('SELECT id, item, amount, method, status, applicant, created_at FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`);
const 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) {
storeAndBroadcastText(roomId, '小财', `没有找到与“${itemName}”相关的采购记录。`);
addToHistory(roomId, 'assistant', `没有找到与“${itemName}”相关的采购记录。`);
@@ -421,14 +403,10 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
confirmText += `\n如果不删除,请忽略此消息。`;
storeAndBroadcastText(roomId, '小财', confirmText);
addToHistory(roomId, 'assistant', confirmText);
setPendingAction(roomId, username, {
type: 'delete',
data: { itemName, purchaseIds: purchases.map(p => p.id) }
});
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) {
@@ -447,25 +425,79 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
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}%`, '已完成');
let replyText = '';
// 计算总金额:优先使用 AI 提供的 amount否则根据单价*数量+邮费计算
const quantity = aiResult.quantity || 1;
const unitPrice = aiResult.unit_price || 0;
const freight = aiResult.freight || 0;
let amount;
if (aiResult.amount !== undefined && aiResult.amount !== null) {
amount = aiResult.amount;
} else {
amount = unitPrice * quantity + freight;
}
if (!purchase) {
const id = uuidv4();
db.prepare('INSERT INTO purchases (id, room_id, item, amount, method, status, applicant, created_at) VALUES (?,?,?,?,?,?,?,?)').run(
id, roomId, aiResult.purchase_item, aiResult.amount, aiResult.method, aiResult.status || '待处理', aiResult.applicant || username, createdTime
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, 'AI 创建采购条目', '小财', now);
replyText = `✅ 已记录采购:${aiResult.purchase_item},金额 ¥${aiResult.amount},状态 ${aiResult.status || '待处理'}`;
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 {
db.prepare('UPDATE purchases SET amount = ?, method = ?, status = ?, updated_at = ? WHERE id = ?').run(
aiResult.amount, aiResult.method, aiResult.status, now, purchase.id
);
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, `更新采购信息(金额:${aiResult.amount},状态:${aiResult.status}`, username, now);
replyText = `🔄 已更新采购「${aiResult.purchase_item}」:金额 ¥${aiResult.amount},状态 ${aiResult.status}`;
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
};
// 重新计算金额,除非 AI 明确给了 amount
if (aiResult.amount !== undefined && aiResult.amount !== null) {
newVals.amount = aiResult.amount;
} 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) {
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));
}
storeAndBroadcastText(roomId, '小财', replyText);
addToHistory(roomId, 'assistant', replyText);
broadcastToRoom(roomId, { type: 'purchase_updated' });
@@ -473,8 +505,8 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
}
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');
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);