Compare commits
10
Commits
1889b3b71b
...
e9adbc90cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9adbc90cf | ||
|
|
c61d64a249 | ||
|
|
5f73c384cf | ||
|
|
fb07144ef3 | ||
|
|
742bb908a5 | ||
|
|
3ea58fcf3d | ||
|
|
3d7732474d | ||
|
|
4fe5abb74a | ||
|
|
cf63ed5e3f | ||
|
|
2feb6004b8 |
@@ -110,6 +110,14 @@
|
|||||||
overflow: auto; font-size: 16px; line-height: 1.5; color: #333;
|
overflow: auto; font-size: 16px; line-height: 1.5; color: #333;
|
||||||
}
|
}
|
||||||
.fullscreen-overlay .fullscreen-text img { max-width: 200px; border-radius: 8px; margin: 4px 0; }
|
.fullscreen-overlay .fullscreen-text img { max-width: 200px; border-radius: 8px; margin: 4px 0; }
|
||||||
|
.fullscreen-overlay .fullscreen-text table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.fullscreen-overlay .fullscreen-text th,
|
||||||
|
.fullscreen-overlay .fullscreen-text td {border: 1px solid #ddd;padding: 4px;text-align: left;font-size: 14px;}
|
||||||
|
.fullscreen-overlay .fullscreen-text th {background: #f0f0f0;}
|
||||||
|
|
||||||
.detail-row { margin-bottom: 8px; }
|
.detail-row { margin-bottom: 8px; }
|
||||||
</style>
|
</style>
|
||||||
@@ -368,6 +376,14 @@
|
|||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onImgThumbDblClick(img) {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'fullscreen-overlay';
|
||||||
|
overlay.innerHTML = `<img src="${img.src}" style="max-width:100%;max-height:100%;object-fit:contain" alt="">`;
|
||||||
|
overlay.addEventListener('click', () => overlay.remove());
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
function onBubbleDblClick(e) {
|
function onBubbleDblClick(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const bubble = e.currentTarget;
|
const bubble = e.currentTarget;
|
||||||
@@ -382,7 +398,7 @@
|
|||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 临时“输入中...”气泡
|
// 临时"输入中..."气泡
|
||||||
function insertTempBubble() {
|
function insertTempBubble() {
|
||||||
clearTempBubble();
|
clearTempBubble();
|
||||||
const tempId = 'temp_' + Date.now();
|
const tempId = 'temp_' + Date.now();
|
||||||
@@ -391,10 +407,30 @@
|
|||||||
messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(tempMsg));
|
messagesContainer.insertAdjacentHTML('beforeend', renderMessageHTML(tempMsg));
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
tempBubbleTimer = setTimeout(() => { clearTempBubble(); }, 8000);
|
tempBubbleTimer = setTimeout(() => { clearTempBubble(); }, 8000);
|
||||||
|
// 降级轮询:如果 WebSocket 消息丢失,定时从服务端拉取
|
||||||
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
|
let pollCount = 0;
|
||||||
|
pollTimer = setInterval(async () => {
|
||||||
|
pollCount++;
|
||||||
|
if (!currentRoom || pollCount > 5) { clearInterval(pollTimer); pollTimer = null; return; }
|
||||||
|
try {
|
||||||
|
const token = getToken();
|
||||||
|
const res = await fetch(API + `/api/rooms/${currentRoom}/messages`, { headers: { 'Authorization': `Bearer ${token}` } });
|
||||||
|
const msgs = await res.json();
|
||||||
|
const lastCacheId = messageCache.filter(m => !m.isTemp && !m.isPlaceholder).slice(-1)[0]?.id;
|
||||||
|
const newMsgs = msgs.filter(m => !lastCacheId || m.id > lastCacheId);
|
||||||
|
if (newMsgs.length) {
|
||||||
|
clearInterval(pollTimer); pollTimer = null;
|
||||||
|
clearTempBubble();
|
||||||
|
newMsgs.forEach(m => appendMessage(m));
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTempBubble() {
|
function clearTempBubble() {
|
||||||
if (tempBubbleTimer) { clearTimeout(tempBubbleTimer); tempBubbleTimer = null; }
|
if (tempBubbleTimer) { clearTimeout(tempBubbleTimer); tempBubbleTimer = null; }
|
||||||
|
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||||||
messageCache = messageCache.filter(m => !m.isTemp);
|
messageCache = messageCache.filter(m => !m.isTemp);
|
||||||
const tempEls = messagesContainer.querySelectorAll('[data-msg-id^="temp_"]');
|
const tempEls = messagesContainer.querySelectorAll('[data-msg-id^="temp_"]');
|
||||||
tempEls.forEach(el => el.remove());
|
tempEls.forEach(el => el.remove());
|
||||||
@@ -404,6 +440,7 @@
|
|||||||
let wsReconnectTimer = null;
|
let wsReconnectTimer = null;
|
||||||
let reconnectAttempts = 0;
|
let reconnectAttempts = 0;
|
||||||
const MAX_RECONNECT_DELAY = 30000;
|
const MAX_RECONNECT_DELAY = 30000;
|
||||||
|
let pollTimer = null; // 降级轮询定时器
|
||||||
|
|
||||||
function initWebSocket() {
|
function initWebSocket() {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
@@ -417,6 +454,7 @@
|
|||||||
};
|
};
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
|
console.log('📩 WS 收到:', data.type);
|
||||||
if (data.type === 'new_message') {
|
if (data.type === 'new_message') {
|
||||||
const msg = data.message;
|
const msg = data.message;
|
||||||
if (currentRoom === msg.room_id) {
|
if (currentRoom === msg.room_id) {
|
||||||
@@ -715,7 +753,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatMonth(dateStr) {
|
function formatMonth(dateStr) {
|
||||||
const parts = dateStr.split('/');
|
const clean = dateStr.replace(/-/g, '/');
|
||||||
|
const parts = clean.split('/');
|
||||||
if (parts.length >= 2) return `${parts[0]}年${parseInt(parts[1], 10)}月`;
|
if (parts.length >= 2) return `${parts[0]}年${parseInt(parts[1], 10)}月`;
|
||||||
return dateStr.substring(0,7);
|
return dateStr.substring(0,7);
|
||||||
}
|
}
|
||||||
@@ -733,7 +772,7 @@
|
|||||||
const p = await res.json();
|
const p = await res.json();
|
||||||
document.getElementById('detail-title').textContent = p.item;
|
document.getElementById('detail-title').textContent = p.item;
|
||||||
let attachHtml = '';
|
let attachHtml = '';
|
||||||
if (p.attachments?.length) attachHtml = '<div class="attachments">' + p.attachments.map(a => `<img class="img-thumb" src="${a.file_path}" alt="${a.file_path}">`).join('') + '</div>';
|
if (p.attachments?.length) attachHtml = '<div class="attachments">' + p.attachments.map(a => `<img class="img-thumb" src="${a.file_path}" alt="${a.file_path}" ondblclick="onImgThumbDblClick(this)">`).join('') + '</div>';
|
||||||
const historyHtml = p.history?.map(h => `<li><span class="history-time">${h.timestamp}</span> ${h.user}: ${h.action}</li>`).join('') || '';
|
const historyHtml = p.history?.map(h => `<li><span class="history-time">${h.timestamp}</span> ${h.user}: ${h.action}</li>`).join('') || '';
|
||||||
document.getElementById('detail-content').innerHTML = `
|
document.getElementById('detail-content').innerHTML = `
|
||||||
<div class="detail-row"><strong>数量:</strong>${p.quantity}</div>
|
<div class="detail-row"><strong>数量:</strong>${p.quantity}</div>
|
||||||
|
|||||||
+79
-17
@@ -27,6 +27,19 @@ const TIMEZONE = 'Asia/Shanghai';
|
|||||||
function timestamp() {
|
function timestamp() {
|
||||||
return new Date().toLocaleString('zh-CN', { timeZone: TIMEZONE, hour12: false });
|
return new Date().toLocaleString('zh-CN', { timeZone: TIMEZONE, hour12: false });
|
||||||
}
|
}
|
||||||
|
function normalizeTime(str) {
|
||||||
|
if (!str) return timestamp();
|
||||||
|
// 已经是标准格式 yyyy/M/d HH:mm:ss 直接返回
|
||||||
|
if (/\d{4}\/\d{1,2}\/\d{1,2} \d{2}:\d{2}:\d{2}/.test(str)) return str;
|
||||||
|
// 纯日期 yyyy-MM-dd 或 yyyy/MM/dd → 补充 00:00:00
|
||||||
|
const clean = str.replace(/-/g, '/');
|
||||||
|
if (/^\d{4}\/\d{1,2}\/\d{1,2}$/.test(clean)) return clean + ' 00:00:00';
|
||||||
|
// 带时间但分隔符是 - → 统一换成 /
|
||||||
|
const withSlash = str.replace(/-/g, '/');
|
||||||
|
if (/\d{4}\/\d{1,2}\/\d{1,2} \d{2}:\d{2}:\d{2}/.test(withSlash)) return withSlash;
|
||||||
|
// 无法识别,返回当前时间
|
||||||
|
return timestamp();
|
||||||
|
}
|
||||||
function currentTimeStr() { return timestamp(); }
|
function currentTimeStr() { return timestamp(); }
|
||||||
|
|
||||||
// 初始化用户
|
// 初始化用户
|
||||||
@@ -103,7 +116,7 @@ app.get('/api/rooms', auth, (req, res) => {
|
|||||||
rooms = db.prepare("SELECT * FROM rooms WHERE white_list = '' OR (',' || white_list || ',' LIKE ?)").all(`%,${username},%`);
|
rooms = db.prepare("SELECT * FROM rooms WHERE white_list = '' OR (',' || white_list || ',' LIKE ?)").all(`%,${username},%`);
|
||||||
}
|
}
|
||||||
const result = rooms.map(room => {
|
const result = rooms.map(room => {
|
||||||
const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY timestamp DESC LIMIT 1').get(room.id);
|
const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY id DESC LIMIT 1').get(room.id);
|
||||||
return {
|
return {
|
||||||
...room,
|
...room,
|
||||||
last_message: lastMsg ? lastMsg.text : '',
|
last_message: lastMsg ? lastMsg.text : '',
|
||||||
@@ -144,12 +157,13 @@ app.delete('/api/rooms/:roomId', auth, adminOnly, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/rooms/:roomId/messages', auth, (req, res) => {
|
app.get('/api/rooms/:roomId/messages', auth, (req, res) => {
|
||||||
const msgs = db.prepare('SELECT * FROM messages WHERE room_id = ? ORDER BY timestamp ASC').all(req.params.roomId);
|
const msgs = db.prepare('SELECT * FROM messages WHERE room_id = ? ORDER BY id ASC').all(req.params.roomId);
|
||||||
res.json(msgs);
|
res.json(msgs);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/rooms/:roomId/messages', auth, async (req, res) => {
|
app.post('/api/rooms/:roomId/messages', auth, async (req, res) => {
|
||||||
const { text, attachments } = req.body;
|
const { text, attachments } = req.body;
|
||||||
|
console.log('📨 收到消息, text:', (text||'').substring(0,30), 'attachments:', JSON.stringify(attachments));
|
||||||
const roomId = req.params.roomId;
|
const roomId = req.params.roomId;
|
||||||
const username = req.user.username;
|
const username = req.user.username;
|
||||||
const now = timestamp();
|
const now = timestamp();
|
||||||
@@ -159,7 +173,7 @@ app.post('/api/rooms/:roomId/messages', auth, async (req, res) => {
|
|||||||
);
|
);
|
||||||
const msg = { id: result.lastInsertRowid, room_id: roomId, user: username, text: text || '', attachments: attachments || [], timestamp: now };
|
const msg = { id: result.lastInsertRowid, room_id: roomId, user: username, text: text || '', attachments: attachments || [], timestamp: now };
|
||||||
|
|
||||||
const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY timestamp DESC LIMIT 1').get(roomId);
|
const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY id DESC LIMIT 1').get(roomId);
|
||||||
broadcastToRoomExcludeSelf(roomId, username, {
|
broadcastToRoomExcludeSelf(roomId, username, {
|
||||||
type: 'new_message', message: msg,
|
type: 'new_message', message: msg,
|
||||||
room_preview: { room_id: roomId, last_message: lastMsg ? lastMsg.text : '', last_time: lastMsg ? lastMsg.timestamp : '' }
|
room_preview: { room_id: roomId, last_message: lastMsg ? lastMsg.text : '', last_time: lastMsg ? lastMsg.timestamp : '' }
|
||||||
@@ -195,7 +209,7 @@ app.get('/api/rooms/:roomId/purchases', auth, (req, res) => {
|
|||||||
app.get('/api/purchases/:id', auth, (req, res) => {
|
app.get('/api/purchases/:id', auth, (req, res) => {
|
||||||
const pur = db.prepare('SELECT * FROM purchases WHERE id = ?').get(req.params.id);
|
const pur = db.prepare('SELECT * FROM purchases WHERE id = ?').get(req.params.id);
|
||||||
if (!pur) return res.status(404).json({ error: '未找到' });
|
if (!pur) return res.status(404).json({ error: '未找到' });
|
||||||
const history = db.prepare('SELECT * FROM purchase_history WHERE purchase_id = ? ORDER BY timestamp ASC').all(req.params.id);
|
const history = db.prepare('SELECT * FROM purchase_history WHERE purchase_id = ? ORDER BY id ASC').all(req.params.id);
|
||||||
const attachments = db.prepare('SELECT * FROM purchase_attachments WHERE purchase_id = ?').all(req.params.id);
|
const attachments = db.prepare('SELECT * FROM purchase_attachments WHERE purchase_id = ?').all(req.params.id);
|
||||||
res.json({ ...pur, history, attachments });
|
res.json({ ...pur, history, attachments });
|
||||||
});
|
});
|
||||||
@@ -273,7 +287,7 @@ function storeAndBroadcastText(roomId, user, text) {
|
|||||||
const now = timestamp();
|
const now = timestamp();
|
||||||
const result = db.prepare('INSERT INTO messages (room_id, user, text, timestamp) VALUES (?,?,?,?)').run(roomId, user, text, now);
|
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 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 timestamp DESC LIMIT 1').get(roomId);
|
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 : '' } });
|
broadcastToRoom(roomId, { type: 'new_message', message: msg, room_preview: { room_id: roomId, last_message: lastMsg ? lastMsg.text : '', last_time: lastMsg ? lastMsg.timestamp : '' } });
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
@@ -334,19 +348,37 @@ async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin) {
|
|||||||
- 删除所有金额为0的记录:delete_item="__AMOUNT_ZERO__"
|
- 删除所有金额为0的记录:delete_item="__AMOUNT_ZERO__"
|
||||||
- 删除物品名为空、null、undefined 的记录:delete_item="__NULL_NAME__"
|
- 删除物品名为空、null、undefined 的记录:delete_item="__NULL_NAME__"
|
||||||
- 不要再返回 action="ask" 来处理删除请求,只要确定是删除意图,就必须返回 action="delete" 并填好 delete_item。
|
- 不要再返回 action="ask" 来处理删除请求,只要确定是删除意图,就必须返回 action="delete" 并填好 delete_item。
|
||||||
- 若用户描述的是特定记录(如“名字叫XXX的”),则 delete_item 填那个物品名。
|
- 若用户描述的是特定记录(如"名字叫XXX的"),则 delete_item 填那个物品名。
|
||||||
|
|
||||||
|
⚠️ 删除规则补充(重要):
|
||||||
|
- 如果用户说"删除数量X的那条/那个记录",你必须同时返回 quantity 字段,值为 X。
|
||||||
|
- 如果用户提到"金额为Y的/价格Y的"来区分记录,你必须同时返回 amount 字段,值为 Y。
|
||||||
|
- 提供这些字段后,系统只会精确删除匹配的那一条,避免误删同名记录。
|
||||||
|
|
||||||
⚠️ 附件补充规则:
|
⚠️ 附件补充规则:
|
||||||
- 如果用户发送图片并明确说"这是XX的图片"或"作为附件存到XX采购里",
|
- 如果用户发送图片并明确说"这是XX的图片"或"作为附件存到XX采购里",
|
||||||
你只需返回 purchase_item 为 XX,**绝对不要**返回 quantity、unit_price、amount、freight、payment_method 等任何字段。
|
你只需返回 purchase_item 为 XX,**绝对不要**返回 quantity、unit_price、amount、freight、payment_method 等任何字段。
|
||||||
- 当消息中只有图片且文本明确指出"这是XX的图片"时,提取 purchase_item 为 XX,其余字段全部省略。
|
- 当消息中只有图片且文本明确指出"这是XX的图片"时,提取 purchase_item 为 XX,其余字段全部省略。
|
||||||
- 如果用户要求"把刚才/之前的图片作为附件存到XX采购",你可以返回 {"action":"purchase","purchase_item":"XX","use_recent_image":true},
|
- 如果用户要求"把刚才/之前的图片作为附件存到XX采购",或说"上面/前面/刚刚那张是XX的图片""那张图片存到XX"等引用历史上传图片的表述,你必须返回 {"action":"purchase","purchase_item":"XX","use_recent_image":true},
|
||||||
不提供任何数量、价格字段,系统会自动查找最近一张图片并关联。
|
不提供任何数量、价格字段,系统会自动查找最近一张图片并关联。
|
||||||
|
|
||||||
|
⚠️ 绝对禁止规则:
|
||||||
|
- 如果用户发送图片并明确说"这是XX的图片"或"作为附件存到XX",你**只能返回** purchase_item,
|
||||||
|
**绝对不允许返回** is_new、quantity、unit_price、amount 等任何其他字段。
|
||||||
|
- 纯图片+指定物品名的请求不是新建采购,系统会自动关联附件到已有记录。
|
||||||
|
|
||||||
⚠️ 采购规则:
|
⚠️ 采购规则:
|
||||||
|
- 只有用户明确表示"购买/采购/下单/付款/买了/花了"等消费意图时才返回 action="purchase"。
|
||||||
|
- 如果用户只是陈述事实或描述物品(如"前天deep"),没有明确采购意图,必须返回 action="ignore"。
|
||||||
|
- 如果信息不完整、无法确定具体物品名称,必须返回 action="ask" 追问,绝不允许猜测创建记录。
|
||||||
|
- 新建 vs 更新(极其重要):
|
||||||
|
* 如果用户提到"昨天/今天/刚刚/又/再/新/另一批/另外"等时间或重复购买词汇,必须返回 "is_new": true,系统会强制创建新记录。
|
||||||
|
* "纯图片+物品名"的附件请求**不是**新建,不要返回 is_new。
|
||||||
|
* 如果用户明确说"改一下/更新/修改/调整/更正"等词汇,才不提供 is_new 或设为 false,系统会尝试匹配已有记录更新。
|
||||||
|
* 当无法确定是新建还是更新时,默认视为新建(返回 is_new: true)。
|
||||||
- 用户补充信息时更新已有记录。更新时请返回完整字段值,包括未变化的字段。如果用户要求修改时间,请生成正确的created_time字段。
|
- 用户补充信息时更新已有记录。更新时请返回完整字段值,包括未变化的字段。如果用户要求修改时间,请生成正确的created_time字段。
|
||||||
- 总金额(amount)由系统自动按“数量×单价+邮费”计算,你无需提供该字段。
|
- 总金额(amount)由系统自动按"数量×单价+邮费"计算,你无需提供该字段。
|
||||||
- 如果无法确定 purchase_item(物品名称),必须 action="ask" 并追问“请提供物品名称”,绝不允许将 purchase_item 设为空字符串、null 或 undefined。`;
|
- 如果无法确定 purchase_item(物品名称),必须 action="ask" 并追问"请提供物品名称",绝不允许将 purchase_item 设为空字符串、null 或 undefined。`;
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
@@ -381,7 +413,13 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
|||||||
} else if (itemName === '__NULL_NAME__') {
|
} 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);
|
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 {
|
} else {
|
||||||
purchases = db.prepare('SELECT id, item, amount, payment_method, status, applicant, created_at FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`);
|
// 动态构建查询:根据 AI 提供的 quantity/amount 精确匹配
|
||||||
|
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) {
|
if (purchases.length === 0) {
|
||||||
const label = itemName === '__AMOUNT_ZERO__' ? '金额为0' : (itemName === '__NULL_NAME__' ? '名称为空' : `"${itemName}"`);
|
const label = itemName === '__AMOUNT_ZERO__' ? '金额为0' : (itemName === '__NULL_NAME__' ? '名称为空' : `"${itemName}"`);
|
||||||
@@ -405,11 +443,20 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (aiResult.action === 'purchase') {
|
if (aiResult.action === 'purchase') {
|
||||||
|
const item = aiResult.purchase_item;
|
||||||
|
// 兜底拦截:物品名为空/null/undefined/过短(<2字符) → 拒绝创建
|
||||||
|
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;
|
let usedRecentImage = false;
|
||||||
if (aiResult.use_recent_image) {
|
if (aiResult.use_recent_image) {
|
||||||
console.log('🔍 use_recent_image: 查找最近图片消息, roomId=', roomId);
|
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 timestamp DESC LIMIT 1").get(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);
|
||||||
console.log('🔍 最近消息:', recentMsg ? recentMsg.attachments : '(无)');
|
console.log('🔍 最近消息:', recentMsg ? recentMsg.attachments : '(无)');
|
||||||
if (recentMsg) {
|
if (recentMsg) {
|
||||||
try {
|
try {
|
||||||
@@ -418,9 +465,23 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
|||||||
} catch(e) { console.error('❌ 解析附件失败:', e); }
|
} catch(e) { console.error('❌ 解析附件失败:', e); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const createdTime = aiResult.created_time || now;
|
const createdTime = normalizeTime(aiResult.created_time);
|
||||||
let purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? AND status != ?').get(roomId, `%${aiResult.purchase_item}%`, '已完成');
|
// 新建 vs 更新:AI 标记 is_new 时强制新建,不尝试匹配已有记录
|
||||||
console.log('📦 purchase_item:', aiResult.purchase_item, purchase ? '(已有)' : '(新建)');
|
let isNew = aiResult.is_new === true;
|
||||||
|
let purchase;
|
||||||
|
// 兜底纠正:如果 AI 误将"纯附件请求"标记为 is_new,强制转为匹配已有记录
|
||||||
|
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('🛡️ 兜底纠正: is_new 但附件非空且已有同名记录,转为更新模式');
|
||||||
|
isNew = false;
|
||||||
|
purchase = existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isNew && !purchase) {
|
||||||
|
purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? AND status != ?').get(roomId, `%${aiResult.purchase_item}%`, '已完成');
|
||||||
|
}
|
||||||
|
console.log('📦 purchase_item:', aiResult.purchase_item, isNew ? '(强制新建)' : purchase ? '(匹配已有)' : '(未匹配-新建)');
|
||||||
let replyText = '';
|
let replyText = '';
|
||||||
const quantity = aiResult.quantity || 1;
|
const quantity = aiResult.quantity || 1;
|
||||||
const unitPrice = aiResult.unit_price || 0;
|
const unitPrice = aiResult.unit_price || 0;
|
||||||
@@ -483,11 +544,12 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
|||||||
console.log('📎 插入附件:', attachments.length, '个, purchase_id=', purchase.id);
|
console.log('📎 插入附件:', attachments.length, '个, purchase_id=', purchase.id);
|
||||||
const insertAttach = db.prepare('INSERT INTO purchase_attachments (purchase_id, file_path, uploaded_by, timestamp) VALUES (?,?,?,?)');
|
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); });
|
attachments.forEach(fp => { console.log(' 📎', fp); insertAttach.run(purchase.id, fp, username, now); });
|
||||||
if (usedRecentImage) {
|
|
||||||
replyText = `📎 已为「${aiResult.purchase_item}」关联 ${attachments.length} 个附件`;
|
|
||||||
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, `添加附件:${attachments.length} 个`, username, now);
|
db.prepare('INSERT INTO purchase_history (purchase_id, action, user, timestamp) VALUES (?,?,?,?)').run(purchase.id, `添加附件:${attachments.length} 个`, username, now);
|
||||||
console.log('✅ usedRecentImage 完成, replyText:', replyText);
|
// 如果字段没有变化,覆盖回复消息
|
||||||
|
if (replyText.includes('没有发生') || usedRecentImage) {
|
||||||
|
replyText = `📎 已为「${aiResult.purchase_item}」添加 ${attachments.length} 个附件`;
|
||||||
}
|
}
|
||||||
|
console.log('✅ 附件完成, replyText:', replyText);
|
||||||
} else if (usedRecentImage) {
|
} else if (usedRecentImage) {
|
||||||
replyText = `❌ 未找到最近的图片消息,请先发送图片再试。`;
|
replyText = `❌ 未找到最近的图片消息,请先发送图片再试。`;
|
||||||
console.log('⚠️ usedRecentImage 但附件为空');
|
console.log('⚠️ usedRecentImage 但附件为空');
|
||||||
|
|||||||
Reference in New Issue
Block a user