diff --git a/server/db.js b/server/db.js
index 1fa596a..7a43a1b 100644
--- a/server/db.js
+++ b/server/db.js
@@ -2,7 +2,6 @@ const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
-// 确保数据目录存在
const dataDir = '/app/data';
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
@@ -10,12 +9,9 @@ if (!fs.existsSync(dataDir)) {
const dbPath = path.join(dataDir, 'xiaocai.db');
const db = new Database(dbPath);
-
-
-// 启用 WAL 模式提升并发
db.pragma('journal_mode = WAL');
-// 创建表
+// 创建基础表(如果不存在)
db.exec(`
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
@@ -27,7 +23,7 @@ db.exec(`
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_by TEXT NOT NULL,
- white_list TEXT DEFAULT '', -- 逗号分隔用户名,空表示所有人
+ white_list TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now','localtime'))
);
@@ -36,7 +32,7 @@ db.exec(`
room_id TEXT NOT NULL,
user TEXT NOT NULL,
text TEXT,
- attachments TEXT, -- JSON array of file paths
+ attachments TEXT,
timestamp TEXT DEFAULT (datetime('now','localtime')),
FOREIGN KEY(room_id) REFERENCES rooms(id)
);
@@ -45,13 +41,17 @@ db.exec(`
id TEXT PRIMARY KEY,
room_id TEXT NOT NULL,
item TEXT,
- amount REAL,
- method TEXT,
+ quantity INTEGER DEFAULT 1,
+ unit_price REAL DEFAULT 0,
+ amount REAL DEFAULT 0,
+ freight REAL DEFAULT 0,
+ payment_method TEXT DEFAULT '未指定',
+ invoice_type TEXT DEFAULT '无票',
status TEXT DEFAULT '待处理',
applicant TEXT,
+ remarks TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now','localtime')),
updated_at TEXT DEFAULT (datetime('now','localtime')),
- ai_notes TEXT,
FOREIGN KEY(room_id) REFERENCES rooms(id)
);
@@ -75,4 +75,21 @@ db.exec(`
);
`);
+// 为已有数据库添加可能缺失的列(安全迁移,忽略错误)
+const migrations = [
+ `ALTER TABLE purchases ADD COLUMN quantity INTEGER DEFAULT 1`,
+ `ALTER TABLE purchases ADD COLUMN unit_price REAL DEFAULT 0`,
+ `ALTER TABLE purchases ADD COLUMN freight REAL DEFAULT 0`,
+ `ALTER TABLE purchases ADD COLUMN invoice_type TEXT DEFAULT '无票'`,
+ `ALTER TABLE purchases ADD COLUMN payment_method TEXT DEFAULT '未指定'`,
+ `ALTER TABLE purchases ADD COLUMN remarks TEXT DEFAULT ''`,
+];
+for (const sql of migrations) {
+ try {
+ db.exec(sql);
+ } catch (e) {
+ // 列已存在或其他错误,忽略
+ }
+}
+
module.exports = db;
diff --git a/server/public/index.html b/server/public/index.html
index 848bd0f..437c29f 100644
--- a/server/public/index.html
+++ b/server/public/index.html
@@ -77,7 +77,7 @@
.purchase-item .item-main { display: flex; justify-content: space-between; align-items: baseline; }
.purchase-item .item-name { font-size: 16px; font-weight: 500; }
.purchase-item .item-amount { font-size: 16px; font-weight: 600; }
- .purchase-item .item-meta { display: flex; justify-content: space-between; margin-top: 4px; font-size: 13px; color: #888; }
+ .purchase-item .item-meta { display: flex; justify-content: space-between; margin-top: 4px; font-size: 13px; color: #888; flex-wrap: wrap; gap: 4px; }
.status-badge { background: #fef3c7; padding: 1px 6px; border-radius: 8px; font-size: 12px; }
.status-badge.done { background: #d1fae5; }
@@ -99,7 +99,6 @@
.version { text-align: center; font-size: 12px; color: #aaa; margin-top: 16px; }
- /* 全屏预览弹窗 */
.fullscreen-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.9); display: flex; align-items: center; justify-content: center;
@@ -111,6 +110,8 @@
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; }
+
+ .detail-row { margin-bottom: 8px; }
@@ -123,7 +124,7 @@
- v2.2
+ v2.4
@@ -332,14 +333,11 @@
function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; }
- // 双击全屏相关
function bindMessageEvents() {
- // 图片双击全屏
document.querySelectorAll('.msg-img').forEach(img => {
img.removeEventListener('dblclick', onImageDblClick);
img.addEventListener('dblclick', onImageDblClick);
});
- // 气泡双击全屏
document.querySelectorAll('.msg-bubble').forEach(bubble => {
bubble.removeEventListener('dblclick', onBubbleDblClick);
bubble.addEventListener('dblclick', onBubbleDblClick);
@@ -348,10 +346,9 @@
function onImageDblClick(e) {
e.stopPropagation();
- const imgSrc = e.target.src;
const overlay = document.createElement('div');
overlay.className = 'fullscreen-overlay';
- overlay.innerHTML = `
`;
+ overlay.innerHTML = `
`;
overlay.addEventListener('click', () => overlay.remove());
document.body.appendChild(overlay);
}
@@ -359,9 +356,7 @@
function onBubbleDblClick(e) {
e.stopPropagation();
const bubble = e.currentTarget;
- // 克隆气泡内容,去掉事件
const clone = bubble.cloneNode(true);
- // 移除可能有的链接行为
const overlay = document.createElement('div');
overlay.className = 'fullscreen-overlay';
const container = document.createElement('div');
@@ -488,12 +483,10 @@
}
}
- // 修正滑动手势方向
function initSwipeGestures() {
const chatPage = document.getElementById('chat-page');
const purchaseOverlay = document.getElementById('purchase-panel-overlay');
- let startX = 0;
- let startY = 0;
+ let startX = 0, startY = 0;
function handleTouchStart(e) {
startX = e.touches[0].clientX;
@@ -508,22 +501,12 @@
const diffY = endY - startY;
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
if (target === chatPage && !purchaseOverlay.classList.contains('hidden')) {
- // 采购清单打开时,任意水平滑动都关闭
- if (Math.abs(diffX) > 30) {
- closePurchasePanel();
- }
+ if (Math.abs(diffX) > 30) closePurchasePanel();
} else if (target === chatPage) {
- // 右滑返回列表,左滑打开采购清单
- if (diffX > 30) {
- showList();
- } else if (diffX < -30) {
- openPurchasePanel();
- }
+ if (diffX > 30) showList();
+ else if (diffX < -30) openPurchasePanel();
} else if (target === purchaseOverlay) {
- // 采购清单上任意滑动都关闭
- if (Math.abs(diffX) > 30) {
- closePurchasePanel();
- }
+ if (Math.abs(diffX) > 30) closePurchasePanel();
}
}
startX = 0; startY = 0;
@@ -681,7 +664,11 @@
html += `
${item.item}¥${item.amount}
-
${dateShort} ${timeShort}${item.applicant || ''} / ${item.method || ''}${item.status}
+
+ ${dateShort} ${timeShort}
+ ×${item.quantity} ${item.invoice_type !== '无票' ? '· ' + item.invoice_type : ''}
+ ${item.status}
+
`;
});
html += '';
@@ -710,7 +697,20 @@
let attachHtml = '';
if (p.attachments?.length) attachHtml = '' + p.attachments.map(a => `

`).join('') + '
';
const historyHtml = p.history?.map(h => `${h.timestamp} ${h.user}: ${h.action}`).join('') || '';
- document.getElementById('detail-content').innerHTML = `金额:¥${p.amount} | 方式:${p.method}
申请人:${p.applicant}
${attachHtml}操作历史
`;
+ document.getElementById('detail-content').innerHTML = `
+ 数量:${p.quantity}
+ 单价:¥${p.unit_price}
+ 邮费:¥${p.freight}
+ 总金额:¥${p.amount}
+ 付款方式:${p.payment_method || '未指定'}
+ 发票:${p.invoice_type || '无票'}
+ 状态:${p.status}
+ 申请人:${p.applicant}
+ 采购时间:${p.created_at}
+ 备注:${p.remarks || '无'}
+ ${attachHtml}
+ 操作历史
+ `;
document.getElementById('detail-modal').classList.remove('hidden');
}
function closeDetailModal() { document.getElementById('detail-modal').classList.add('hidden'); }
diff --git a/server/server.js b/server/server.js
index ea149ff..abe7e74 100644
--- a/server/server.js
+++ b/server/server.js
@@ -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);