Compare commits
9
Commits
074e907cae
...
905973d739
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
905973d739 | ||
|
|
7cf008eac5 | ||
|
|
f3faa36e99 | ||
|
|
abf84e1081 | ||
|
|
6a768ef146 | ||
|
|
de2de85e58 | ||
|
|
886df10775 | ||
|
|
7247b1d832 | ||
|
|
b6f2c8dd42 |
+2
-2
@@ -48,8 +48,8 @@ async function callDeepSeek(messages, userId, temperature = 0.1, jsonMode = true
|
||||
console.log('缓存: hit=' + hit + ' miss=' + miss + ' total=' + total + ' 命中率=' + rate + '%');
|
||||
}
|
||||
// 输入消息大小日志
|
||||
var inputChars = JSON.stringify(messages).length;
|
||||
console.log('输入大小: ' + inputChars + ' 字符, ' + messages.length + ' 条消息');
|
||||
//var inputChars = JSON.stringify(messages).length;
|
||||
//console.log('输入大小: ' + inputChars + ' 字符, ' + messages.length + ' 条消息');
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
|
||||
+29
-10
@@ -1,18 +1,29 @@
|
||||
// AI Prompt 模板 — 模块化拼接
|
||||
function buildSystemPrompt(username, isAdmin) {
|
||||
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
||||
|
||||
// 稳定前缀放前面(提升缓存命中率)
|
||||
const role = '你是智能财务助手"小财"。结合对话历史理解用户意图,只返回JSON。';
|
||||
|
||||
const intent = [
|
||||
'意图分类:',
|
||||
'- 采购/付款/发票相关(新建):action="purchase",提取字段:purchase_item, quantity(默认1), unit_price(默认0), freight(默认0), payment_method(淘宝默认支付宝), invoice_type(默认无票), status, applicant, remarks, created_time',
|
||||
'- 查询汇总:action="query"',
|
||||
' 触发条件:当用户询问“买过什么/有没有买过/查一下/查查看/看看/搜索/找一下/列出/我买过/查查/搜搜”等查询历史采购记录的意图时,必须返回 action="query"。',
|
||||
' 示例:',
|
||||
' "查查看我买过记账本么?" → {"action":"query"}',
|
||||
' "我买过扩展坞吗?" → {"action":"query"}',
|
||||
' "绿联拓展坞有没有买过?" → {"action":"query"}',
|
||||
' 注意:即使物品名听起来奇怪或可能不存在,也绝对不能返回 ignore,必须返回 query 让系统去匹配。',
|
||||
'- 聊天:action="chat",reply简短回复',
|
||||
'- 删除指定物品:action="delete",delete_item为物品名',
|
||||
'- 清空所有采购数据:action="clear_all"',
|
||||
'- 忽略(仅当完全无关):action="ignore"'
|
||||
'- 忽略(仅当完全无关):action="ignore"',
|
||||
' 适用场景:纯闲聊(如“你好”“今天天气如何”)或与财务、采购完全无关的话题。',
|
||||
' 严禁将任何包含“买过”“查查看”“有没有”“找一下”等查询意图的句子判为 ignore,必须归为 query。'
|
||||
].join('\n');
|
||||
|
||||
const queryPriority = [
|
||||
'🔍 查询优先原则:',
|
||||
'- 用户任何以“查”“找”“看”“搜索”“有没有”“买过”开头的消息,无论是否完整或物品名是否已知,都必须返回 action="query",不得返回 ignore、chat 或 ask。',
|
||||
'- 系统会自行处理模糊匹配,你无需判断物品是否存在。'
|
||||
].join('\n');
|
||||
|
||||
const permission = [
|
||||
@@ -44,7 +55,7 @@ function buildSystemPrompt(username, isAdmin) {
|
||||
const modifyGuide = [
|
||||
'⚠️ 修改引导规则(极其重要!):',
|
||||
'- 聊天窗口仅用于**新建**采购记录和查询汇总。',
|
||||
'- 如果用户要求**修改/更正/调整/改一下/更新/纠正**已有采购记录 → 你必须返回 action="chat",reply引导用户去采购清单页面手动修改。',
|
||||
'- 如果用户要求**修改/更正/调整/改一下/更新/纠正**已有采购记录,或者说某商品已付款、已收到等变更已有采购记录的意图 → 你必须返回 action="chat",reply引导用户去采购清单页面手动修改。',
|
||||
' 示例回复:"请在采购清单中找到对应记录,点击进入详情页面直接修改。修改后点击保存即可。"',
|
||||
'- 如果用户要求修改,你绝不要尝试去匹配或更新已有记录,只能引导用户手动操作。'
|
||||
].join('\n');
|
||||
@@ -63,16 +74,24 @@ function buildSystemPrompt(username, isAdmin) {
|
||||
' * "发票到/全搞定/完结"等明确有发票或结束意图的 → "已完成"',
|
||||
' * 新建不指定 → "待付款"',
|
||||
'- 只有明确消费意图(购买/采购/下单/付款/买了/花了)才返回 action="purchase"',
|
||||
'- 纯陈述 → action="ignore"',
|
||||
'- 纯陈述且无查询意图 → action="ignore"',
|
||||
'- 若陈述中包含“查/看/找/搜索/有没有/买过”等查询关键词 → 必须 action="query"',
|
||||
'- 信息不完整 → action="ask" 追问',
|
||||
'- amount 由系统自动计算(quantity × unit_price + freight),AI 无需提供',
|
||||
'- 无法确定物品名 → action="ask" 追问'
|
||||
].join('\n');
|
||||
|
||||
// 变动的放最后
|
||||
const timeInfo = '当前服务器时间:' + now;
|
||||
|
||||
return [role, intent, permission, deleteRules, attachmentRules, prohibitionRules, modifyGuide, purchaseRules, timeInfo].join('\n');
|
||||
return [
|
||||
role,
|
||||
intent,
|
||||
queryPriority,
|
||||
permission,
|
||||
deleteRules,
|
||||
attachmentRules,
|
||||
prohibitionRules,
|
||||
modifyGuide,
|
||||
purchaseRules
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
module.exports = { buildSystemPrompt };
|
||||
|
||||
+39
-36
@@ -7,7 +7,7 @@ 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)); }
|
||||
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();
|
||||
@@ -36,26 +36,26 @@ function executePendingAction(roomId, username) {
|
||||
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);
|
||||
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 = '\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);
|
||||
const reply = '✅ 已清空当前房间的所有采购数据。';
|
||||
storeAndBroadcastText(roomId, '小财', 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; }
|
||||
} 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 || '\u8bf7\u63d0\u4f9b\u66f4\u591a\u4fe1\u606f\u3002';
|
||||
storeAndBroadcastText(roomId, '\u5c0f\u8d22', q); addToHistory(roomId, 'assistant', q); return;
|
||||
const q = aiResult.question || aiResult.reply || '请提供更多信息。';
|
||||
storeAndBroadcastText(roomId, '小财', q); addToHistory(roomId, 'assistant', q); return;
|
||||
}
|
||||
|
||||
if (aiResult.action === 'delete') {
|
||||
@@ -73,14 +73,14 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
||||
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');
|
||||
const label = itemName === '__AMOUNT_ZERO__' ? '金额为0' : (itemName === '__NULL_NAME__' ? '名称为空' : '"' + itemName + '"');
|
||||
storeAndBroadcastText(roomId, '小财', '没有找到与"' + label + '"相关的采购记录。');
|
||||
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);
|
||||
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;
|
||||
@@ -88,23 +88,22 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
||||
|
||||
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');
|
||||
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) { 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');
|
||||
storeAndBroadcastText(roomId, '小财', '请提供具体的物品名称。');
|
||||
addToHistory(roomId, 'assistant', '请提供具体的物品名称。');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,7 +114,7 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
||||
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)) {
|
||||
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) {}
|
||||
@@ -123,7 +122,6 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
||||
}
|
||||
|
||||
const createdTime = normalizeTime(aiResult.created_time);
|
||||
// AI 不负责更新 — 聊天永远新建,修改由用户从采购清单手动完成
|
||||
let purchase = null;
|
||||
|
||||
let replyText = '';
|
||||
@@ -132,13 +130,13 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
||||
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');
|
||||
// 金额为 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 || '无票';
|
||||
@@ -148,34 +146,39 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
||||
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;
|
||||
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';
|
||||
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 = '\u274c \u672a\u627e\u5230\u6700\u8fd1\u7684\u56fe\u7247\u6d88\u606f\uff0c\u8bf7\u5148\u53d1\u9001\u56fe\u7247\u518d\u8bd5\u3002';
|
||||
replyText = '❌ 未找到最近的图片消息,请先发送图片再试。';
|
||||
}
|
||||
|
||||
storeAndBroadcastText(roomId, '\u5c0f\u8d22', 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 = '\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); });
|
||||
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, '\u5c0f\u8d22', aiResult.reply || '\u597d\u7684\u3002');
|
||||
storeAndBroadcastText(roomId, '小财', aiResult.reply || '好的。');
|
||||
addToHistory(roomId, 'assistant', aiResult.reply);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,15 +88,15 @@ function replaceMessage(msgId, newMsg) {
|
||||
|
||||
function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; }
|
||||
|
||||
// 图片/气泡单击
|
||||
// 图片单击 / 气泡双击
|
||||
function bindMessageEvents() {
|
||||
document.querySelectorAll('.msg-img').forEach(img => {
|
||||
document.querySelectorAll('.msg-img').forEach(function(img) {
|
||||
img.removeEventListener('click', onImageClick);
|
||||
img.addEventListener('click', onImageClick);
|
||||
});
|
||||
document.querySelectorAll('.msg-bubble').forEach(bubble => {
|
||||
bubble.removeEventListener('click', onBubbleClick);
|
||||
bubble.addEventListener('click', onBubbleClick);
|
||||
document.querySelectorAll('.msg-bubble').forEach(function(bubble) {
|
||||
bubble.removeEventListener('dblclick', onBubbleClick);
|
||||
bubble.addEventListener('dblclick', onBubbleClick);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,6 +170,15 @@ function clearTempBubble() {
|
||||
tempEls.forEach(function(el) { el.remove(); });
|
||||
}
|
||||
|
||||
function showTempBubble(text) {
|
||||
clearTempBubble();
|
||||
var tempId = 'temp_' + Date.now();
|
||||
var tempMsg = { id: tempId, user: '小财', text: text, attachments: [], timestamp: '', isTemp: true };
|
||||
Store.messageCache.push(tempMsg);
|
||||
messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(tempMsg));
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 双指缩放 + 拖拽
|
||||
function initPinchZoom(img) {
|
||||
var scale = 1, startDist = 0, startScale = 1;
|
||||
|
||||
@@ -19,6 +19,8 @@ function initWebSocket() {
|
||||
const room = Store.rooms.find(r => r.id === data.room_preview.room_id);
|
||||
if (room) { room.last_message = data.room_preview.last_message; room.last_time = data.room_preview.last_time; renderChatList(); }
|
||||
}
|
||||
} else if (data.type === 'query_pending') {
|
||||
showTempBubble(data.text);
|
||||
} else if (data.type === 'purchase_updated') {
|
||||
if (Store.currentRoom) loadPurchases();
|
||||
updateSummaryPreview();
|
||||
|
||||
+29
-20
@@ -45,37 +45,46 @@ router.post('/:roomId/messages', authMiddleware, async (req, res) => {
|
||||
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, roomId);
|
||||
if (aiResponse && aiResponse.action !== 'ignore') {
|
||||
handleAIResult(aiResponse, roomId, username, text || '', attachments || []);
|
||||
if (aiResponse) {
|
||||
var responses = Array.isArray(aiResponse) ? aiResponse : [aiResponse];
|
||||
responses.forEach(function(r) {
|
||||
if (r.action !== 'ignore') handleAIResult(r, roomId, username, text || '', attachments || []);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('AI 分析失败:', e.message);
|
||||
var errText = '❌ ' + (e.message || 'AI 处理失败,请稍后重试');
|
||||
var result = db.prepare('INSERT INTO messages (room_id, user, text, timestamp) VALUES (?,?,?,?)').run(roomId, '小财', errText, timestamp());
|
||||
var msg = { id: result.lastInsertRowid, room_id: roomId, user: '小财', text: errText, attachments: [], timestamp: timestamp() };
|
||||
broadcastToRoom(roomId, { type: 'new_message', message: msg });
|
||||
var errResult = db.prepare('INSERT INTO messages (room_id, user, text, timestamp) VALUES (?,?,?,?)').run(roomId, '小财', errText, timestamp());
|
||||
var errMsg = { id: errResult.lastInsertRowid, room_id: roomId, user: '小财', text: errText, attachments: [], timestamp: timestamp() };
|
||||
broadcastToRoom(roomId, { type: 'new_message', message: errMsg });
|
||||
}
|
||||
});
|
||||
|
||||
async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin, roomId) {
|
||||
// 注入当前房间采购清单上下文
|
||||
const purchases = db.prepare("SELECT item, quantity, amount, status, created_at FROM purchases WHERE room_id = ? ORDER BY created_at DESC LIMIT 10").all(roomId);
|
||||
const purchaseContext = purchases.length
|
||||
? `\n📋 当前房间采购清单(最近10条):\n${purchases.map(p => `- ${p.item} | ×${p.quantity} | ¥${p.amount} | ${p.status} | ${p.created_at}`).join('\n')}`
|
||||
: '';
|
||||
const systemPrompt = buildSystemPrompt(username, isAdmin) + purchaseContext;
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...historyMessages.slice(-30),
|
||||
{ role: 'user', content: `${username}: ${text}` }
|
||||
var systemPrompt = buildSystemPrompt(username, isAdmin);
|
||||
var messages = [
|
||||
{ role: 'system', content: systemPrompt }
|
||||
];
|
||||
historyMessages.forEach(function(m) { messages.push(m); });
|
||||
messages.push({ role: 'system', content: '当前服务器时间:' + new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false }) });
|
||||
const content = await callDeepSeek(messages, username);
|
||||
console.log('🤖 AI 返回:', content);
|
||||
console.log('🤖 AI 返回:', content.substring(0, 200));
|
||||
try {
|
||||
const parsed = JSON.parse(content.replace(/```json|```/g, '').trim());
|
||||
normalize(parsed);
|
||||
const v = validate(parsed);
|
||||
if (!v.valid) console.warn('⚠️ AI 返回校验失败:', v.error);
|
||||
var cleaned = content.replace(/```json|```/g, '').trim();
|
||||
if (!cleaned) { console.warn('⚠️ AI 返回空内容'); return { action: 'ignore' }; }
|
||||
var parsed = JSON.parse(cleaned);
|
||||
// 支持批量:AI 可能返回数组
|
||||
if (Array.isArray(parsed)) {
|
||||
parsed.forEach(function(item, i) {
|
||||
normalize(item);
|
||||
var v = validate(item);
|
||||
if (!v.valid) console.warn('⚠️ AI 返回[' + i + ']校验失败:', v.error);
|
||||
});
|
||||
} else {
|
||||
normalize(parsed);
|
||||
var v = validate(parsed);
|
||||
if (!v.valid) console.warn('⚠️ AI 返回校验失败:', v.error);
|
||||
}
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
// AI 返回非 JSON(如纯文本回复图片消息)→ 降级为 chat
|
||||
|
||||
Reference in New Issue
Block a user