Compare commits

...
9 Commits
Author SHA1 Message Date
kicer 074e907cae 修改缓存命中逻辑 2026-07-29 08:16:57 +08:00
kicer 0e17abd653 对话时候严格要求json输出 2026-07-29 08:12:37 +08:00
kicer c90718c6de 关闭思考,温度0.1 2026-07-29 08:10:09 +08:00
kicer 467d879dc3 ds api加上错误时的回复 2026-07-29 08:03:15 +08:00
kicer 416b173183 ds api请求时加上user_id 2026-07-29 08:00:51 +08:00
kicer e08c0cdff0 修改prompt 2026-07-29 07:46:57 +08:00
kicer 1f6bc4789b v2.8.1, 基本版本功能完成 2026-07-29 07:27:20 +08:00
kicer 300930cebd 附件图片单击后打开 2026-07-29 07:11:31 +08:00
kicer 56cc1f6e2e fix css 2026-07-28 20:44:04 +08:00
8 changed files with 79 additions and 37 deletions
+48 -11
View File
@@ -1,18 +1,55 @@
// DeepSeek API 客户端 // DeepSeek API 客户端
const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || ''; const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || '';
async function callDeepSeek(messages, temperature = 0.1) { function sanitizeUserId(username) {
console.log('🤖 调用 DeepSeek...'); return username.replace(/[^a-zA-Z0-9\-_]/g, '_').substring(0, 64) || 'anon';
const res = await fetch('https://api.deepseek.com/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_API_KEY}` },
body: JSON.stringify({ model: 'deepseek-v4-flash', messages, temperature, stream: false })
});
const data = await res.json();
if (!data.choices || !data.choices[0]) {
console.error('🤖 DeepSeek 返回异常:', JSON.stringify(data));
throw new Error('DeepSeek API 返回异常: ' + (data.error?.message || JSON.stringify(data)));
} }
function getErrorMsg(status, data) {
var msg = data && data.error && data.error.message ? data.error.message : '';
switch (status) {
case 400: return 'AI 请求格式错误,请联系管理员(400)' + (msg ? '' + msg : '');
case 401: return 'AI 认证失败,请检查 API Key(401)' + (msg ? '' + msg : '');
case 402: return 'AI 账户余额不足,请联系管理员充值(402)';
case 422: return 'AI 参数错误(422)' + (msg ? '' + msg : '');
case 429: return 'AI 请求太频繁,请稍后重试(429)';
case 500: return 'AI 服务器故障,请稍后重试(500)';
case 503: return 'AI 服务器繁忙,请稍后重试(503)';
default: return 'AI 响应异常(' + status + ')' + (msg ? '' + msg : '');
}
}
async function callDeepSeek(messages, userId, temperature = 0.1, jsonMode = true) {
console.log('调用 DeepSeek...');
var body = { model: 'deepseek-v4-flash', messages: messages, temperature: temperature, stream: false, thinking: { type: 'disabled' } };
if (jsonMode) body.response_format = { type: 'json_object' };
if (userId) body.user_id = sanitizeUserId(userId);
var res = await fetch('https://api.deepseek.com/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + DEEPSEEK_API_KEY },
body: JSON.stringify(body)
});
var data = await res.json();
if (!res.ok) {
var errMsg = getErrorMsg(res.status, data);
console.error('DeepSeek API 错误:', errMsg);
throw new Error(errMsg);
}
if (!data.choices || !data.choices[0]) {
console.error('DeepSeek 返回异常:', JSON.stringify(data));
throw new Error('AI 返回数据异常');
}
// 缓存命中日志
if (data.usage) {
var hit = data.usage.prompt_cache_hit_tokens || 0;
var miss = data.usage.prompt_cache_miss_tokens || 0;
var total = data.usage.prompt_tokens || 0;
var rate = total > 0 ? (hit / total * 100).toFixed(1) : 0;
console.log('缓存: hit=' + hit + ' miss=' + miss + ' total=' + total + ' 命中率=' + rate + '%');
}
// 输入消息大小日志
var inputChars = JSON.stringify(messages).length;
console.log('输入大小: ' + inputChars + ' 字符, ' + messages.length + ' 条消息');
return data.choices[0].message.content; return data.choices[0].message.content;
} }
+9 -5
View File
@@ -2,7 +2,8 @@
function buildSystemPrompt(username, isAdmin) { function buildSystemPrompt(username, isAdmin) {
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false }); const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
const role = '你是智能财务助手"小财"。当前服务器时间:' + now + '。结合对话历史理解用户意图,只返回JSON。'; // 稳定前缀放前面(提升缓存命中率)
const role = '你是智能财务助手"小财"。结合对话历史理解用户意图,只返回JSON。';
const intent = [ const intent = [
'意图分类:', '意图分类:',
@@ -57,9 +58,9 @@ function buildSystemPrompt(username, isAdmin) {
' * "已收货" — 已收到货物', ' * "已收货" — 已收到货物',
' * "已完成" — 发票已收,全部结束', ' * "已完成" — 发票已收,全部结束',
'- 状态映射:', '- 状态映射:',
' * "付了/已付/付款了" → "已付款"', ' * "付了/已付/付款了/买了/花了"等明确已付款完成意图的 → "已付款"',
' * "到了/收到货了" → "已收货"', ' * "到了/收到货了"等明确已收到货意图的 → "已收货"',
' * "发票到/全搞定/完结" → "已完成"', ' * "发票到/全搞定/完结"等明确有发票或结束意图的 → "已完成"',
' * 新建不指定 → "待付款"', ' * 新建不指定 → "待付款"',
'- 只有明确消费意图(购买/采购/下单/付款/买了/花了)才返回 action="purchase"', '- 只有明确消费意图(购买/采购/下单/付款/买了/花了)才返回 action="purchase"',
'- 纯陈述 → action="ignore"', '- 纯陈述 → action="ignore"',
@@ -68,7 +69,10 @@ function buildSystemPrompt(username, isAdmin) {
'- 无法确定物品名 → action="ask" 追问' '- 无法确定物品名 → action="ask" 追问'
].join('\n'); ].join('\n');
return [role, intent, permission, deleteRules, attachmentRules, prohibitionRules, modifyGuide, purchaseRules].join('\n'); // 变动的放最后
const timeInfo = '当前服务器时间:' + now;
return [role, intent, permission, deleteRules, attachmentRules, prohibitionRules, modifyGuide, purchaseRules, timeInfo].join('\n');
} }
module.exports = { buildSystemPrompt }; module.exports = { buildSystemPrompt };
+4 -4
View File
@@ -170,7 +170,7 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
if (aiResult.action === 'query') { 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 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'); 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).then(reply => { storeAndBroadcastText(roomId, '\u5c0f\u8d22', reply); addToHistory(roomId, 'assistant', reply); }); callDeepSeekForSummary(summaryPrompt, username).then(reply => { storeAndBroadcastText(roomId, '小财', reply); addToHistory(roomId, 'assistant', reply); });
return; return;
} }
@@ -181,13 +181,13 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
} }
} }
async function callDeepSeekForSummary(prompt) { async function callDeepSeekForSummary(prompt, userId) {
const { callDeepSeek } = require('./ai/client'); const { callDeepSeek } = require('./ai/client');
const messages = [ const messages = [
{ role: 'system', content: '\u4f60\u662f\u4e00\u4e2a\u8d22\u52a1\u52a9\u624b\uff0c\u8bf7\u6839\u636e\u91c7\u8d2d\u8bb0\u5f55\u751f\u6210\u7b80\u6d01\u56de\u590d\u3002' }, { role: 'system', content: '你是一个财务助手,请根据采购记录生成简洁回复。' },
{ role: 'user', content: prompt } { role: 'user', content: prompt }
]; ];
return callDeepSeek(messages, 0.3); return callDeepSeek(messages, userId, 0.3, false);
} }
module.exports = { module.exports = {
+6 -11
View File
@@ -1,20 +1,18 @@
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; height: 100vh; display: flex; justify-content: center; align-items: center; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; height: 100vh; display: flex; justify-content: center; align-items: center; }
.app { width: 100%; max-width: 420px; height: 100vh; background: #fff; display: flex; flex-direction: column; box-shadow: 0 0 20px rgba(0,0,0,0.1); position: relative; } .app { width: 100%; height: 100vh; background: #fff; display: flex; flex-direction: column; box-shadow: 0 0 20px rgba(0,0,0,0.1); position: relative; }
.hidden { display: none !important; } .hidden { display: none !important; }
.header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 0 16px; display: flex; justify-content: space-between; align-items: center; min-height: 48px; } .header { background: #2a5298; color: #fff; padding: 0 16px; display: flex; justify-content: space-between; align-items: center; min-height: 48px; }
.header-left { display: flex; align-items: center; gap: 8px; cursor: pointer; } .header-left { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.header-left h2 { font-size: 18px; font-weight: 500; } .header-left h2 { font-size: 18px; font-weight: 500; }
.icon-btn { background: none; border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 4px; border-radius: 50%; } .icon-btn { background: none; border: none; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 4px; border-radius: 50%; }
.icon-btn svg { width: 22px; height: 22px; stroke: #fff; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } .icon-btn svg { width: 22px; height: 22px; stroke: #fff; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.chat-item:nth-child(even) {background-color: #e8f0fe;}
.chat-list { flex: 1; overflow-y: auto; padding-bottom: 8px; } .chat-list { flex: 1; overflow-y: auto; padding-bottom: 8px; }
.chat-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; cursor: pointer; } .chat-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; cursor: pointer; }
.chat-item:active { background: #f9f9f9; } .chat-item:active { background: #f9f9f9; }
.avatar { width: 44px; height: 44px; border-radius: 50%; background: #e0e0e0; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; color: #555; flex-shrink: 0; } .avatar { width: 44px; height: 44px; border-radius: 50%; background: #2a5298; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; color: #fff; flex-shrink: 0; }
.chat-info { flex: 1; min-width: 0; } .chat-info { flex: 1; min-width: 0; }
.chat-name { font-size: 16px; font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .chat-name { font-size: 16px; font-weight: 500; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.last-msg { font-size: 14px; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .last-msg { font-size: 14px; color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -23,13 +21,11 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
.room-actions { display: flex; gap: 4px; margin-top: 4px; } .room-actions { display: flex; gap: 4px; margin-top: 4px; }
.edit-room-btn { background: none; border: none; cursor: pointer; display: flex; align-items: center; padding: 2px; } .edit-room-btn { background: none; border: none; cursor: pointer; display: flex; align-items: center; padding: 2px; }
.edit-room-btn svg { width: 16px; height: 16px; stroke: #888; fill: none; stroke-width: 2; } .edit-room-btn svg { width: 16px; height: 16px; stroke: #888; fill: none; stroke-width: 2; }
.summary-card { background: linear-gradient(135deg, #e8f0fe 0%, #d4e4fc 100%); margin: 8px; border-radius: 12px; padding: 14px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border: 1px solid #b8d4f8; } .summary-card { background: linear-gradient(135deg, #e8f0fe 0%, #d4e4fc 100%); margin: 8px; border-radius: 12px; padding: 14px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border: 1px solid #b8d4f8; }
.summary-card h4 { font-size: 16px; font-weight: 500; } .summary-card h4 { font-size: 16px; font-weight: 500; }
.summary-card .summary-preview { font-size: 14px; color: #555; } .summary-card .summary-preview { font-size: 14px; color: #555; }
.chat-window { display: flex; flex-direction: column; height: 100%; } .chat-window { display: flex; flex-direction: column; height: 100%; }
.chat-header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 0 16px; display: flex; align-items: center; min-height: 48px; } .chat-header { background: #2a5298; color: #fff; padding: 0 16px; display: flex; align-items: center; min-height: 48px; }
.header .back-btn { margin-right: 12px; } .header .back-btn { margin-right: 12px; }
.chat-header .back-btn { margin-right: 12px; } .chat-header .back-btn { margin-right: 12px; }
.chat-title { font-size: 17px; font-weight: 500; flex: 1; } .chat-title { font-size: 17px; font-weight: 500; flex: 1; }
@@ -48,7 +44,6 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
.msg-bubble th { background: #f0f0f0; } .msg-bubble th { background: #f0f0f0; }
.msg-bubble pre { background: #f0f0f0; padding: 8px; border-radius: 4px; overflow-x: auto; } .msg-bubble pre { background: #f0f0f0; padding: 8px; border-radius: 4px; overflow-x: auto; }
.msg-bubble code { background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-size: 14px; } .msg-bubble code { background: #f0f0f0; padding: 2px 4px; border-radius: 3px; font-size: 14px; }
.input-area { padding: 8px 12px; border-top: 1px solid #eee; display: flex; align-items: center; background: #fff; gap: 8px; } .input-area { padding: 8px 12px; border-top: 1px solid #eee; display: flex; align-items: center; background: #fff; gap: 8px; }
.input-area textarea { flex: 1; border: 1px solid #ddd; border-radius: 12px; padding: 10px 12px; font-size: 16px; outline: none; resize: none; min-height: 40px; max-height: 120px; overflow-y: hidden; line-height: 1.4; } .input-area textarea { flex: 1; border: 1px solid #ddd; border-radius: 12px; padding: 10px 12px; font-size: 16px; outline: none; resize: none; min-height: 40px; max-height: 120px; overflow-y: hidden; line-height: 1.4; }
.file-upload-btn { background: none; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 4px; } .file-upload-btn { background: none; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 4px; }
@@ -120,7 +115,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
/* 可编辑详情弹窗 */ /* 可编辑详情弹窗 */
.detail-modal { max-height: 90vh; overflow-y: auto; padding: 0; } .detail-modal { max-height: 90vh; overflow-y: auto; padding: 0; }
.detail-modal input, .detail-modal select, .detail-modal textarea { margin: 0; } .detail-modal input, .detail-modal select, .detail-modal textarea { margin: 0; }
.detail-header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; padding: 8px 12px 8px 16px; display: flex; align-items: center; gap: 8px; position: sticky; top: 0; z-index: 1; } .detail-header { background: #2a5298; color: #fff; padding: 8px 12px 8px 16px; display: flex; align-items: center; gap: 8px; position: sticky; top: 0; z-index: 1; }
.detail-header .icon-btn { padding:0; margin-right:0; flex-shrink: 0; margin-left: auto; } .detail-header .icon-btn { padding:0; margin-right:0; flex-shrink: 0; margin-left: auto; }
.detail-header .icon-btn svg { stroke: #fff; } .detail-header .icon-btn svg { stroke: #fff; }
.modal .detail-title-input { flex: 1; font-size: 18px; font-weight: 600; color: #fff; background: transparent; border: none; outline: none; padding: 4px 0; min-width: 0; } .modal .detail-title-input { flex: 1; font-size: 18px; font-weight: 600; color: #fff; background: transparent; border: none; outline: none; padding: 4px 0; min-width: 0; }
+1 -1
View File
@@ -237,7 +237,7 @@
<script> <script>
// 版本号 // 版本号
(function() { (function() {
document.getElementById('version-text').textContent = 'v2.8-260728'; document.getElementById('version-text').textContent = 'v2.8.2-260729';
})(); })();
document.addEventListener('input', function(e) { document.addEventListener('input', function(e) {
if (e.target.id === 'msg-input' || e.target.id === 'purchase-msg-input') { if (e.target.id === 'msg-input' || e.target.id === 'purchase-msg-input') {
+1 -1
View File
@@ -110,7 +110,7 @@ function onImageClick(e) {
initPinchZoom(overlay.querySelector('img')); initPinchZoom(overlay.querySelector('img'));
} }
function onImgThumbDblClick(img) { function onImgThumbClick(img) {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'fullscreen-overlay'; overlay.className = 'fullscreen-overlay';
overlay.innerHTML = '<img src="' + img.src + '" style="max-width:100%;max-height:100%;object-fit:contain;transition: transform 0.1s; transform-origin: 0 0;" alt="">'; overlay.innerHTML = '<img src="' + img.src + '" style="max-width:100%;max-height:100%;object-fit:contain;transition: transform 0.1s; transform-origin: 0 0;" alt="">';
+1 -1
View File
@@ -93,7 +93,7 @@ function renderEditAttachments(attachments) {
} }
container.innerHTML = attachments.map(function(a) { container.innerHTML = attachments.map(function(a) {
return '<div class="attach-thumb-wrap">' + return '<div class="attach-thumb-wrap">' +
'<img class="attach-thumb" src="' + a.file_path + '" ondblclick="onImgThumbDblClick(this)">' + '<img class="attach-thumb" src="' + a.file_path + '" onclick="onImgThumbClick(this)">' +
'<button class="attach-del-btn" onclick="deleteAttachment(event, \'' + encodeURIComponent(a.file_path) + '\')">' + '<button class="attach-del-btn" onclick="deleteAttachment(event, \'' + encodeURIComponent(a.file_path) + '\')">' +
'<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' + '<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' +
'</button>' + '</button>' +
+9 -3
View File
@@ -1,7 +1,7 @@
// 消息路由 + AI 分析 // 消息路由 + AI 分析
const express = require('express'); const express = require('express');
const { authMiddleware } = require('../middleware/auth'); const { authMiddleware } = require('../middleware/auth');
const { broadcastToRoomExcludeSelf } = require('../ws'); const { broadcastToRoomExcludeSelf, broadcastToRoom } = require('../ws');
const { buildSystemPrompt } = require('../ai/prompt'); const { buildSystemPrompt } = require('../ai/prompt');
const { validate, normalize } = require('../ai/validator'); const { validate, normalize } = require('../ai/validator');
const { callDeepSeek } = require('../ai/client'); const { callDeepSeek } = require('../ai/client');
@@ -48,7 +48,13 @@ router.post('/:roomId/messages', authMiddleware, async (req, res) => {
if (aiResponse && aiResponse.action !== 'ignore') { if (aiResponse && aiResponse.action !== 'ignore') {
handleAIResult(aiResponse, roomId, username, text || '', attachments || []); handleAIResult(aiResponse, roomId, username, text || '', attachments || []);
} }
} catch (e) { console.error('AI 分析失败:', e); } } 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 });
}
}); });
async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin, roomId) { async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin, roomId) {
@@ -63,7 +69,7 @@ async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin, roo
...historyMessages.slice(-30), ...historyMessages.slice(-30),
{ role: 'user', content: `${username}: ${text}` } { role: 'user', content: `${username}: ${text}` }
]; ];
const content = await callDeepSeek(messages, 0.1); const content = await callDeepSeek(messages, username);
console.log('🤖 AI 返回:', content); console.log('🤖 AI 返回:', content);
try { try {
const parsed = JSON.parse(content.replace(/```json|```/g, '').trim()); const parsed = JSON.parse(content.replace(/```json|```/g, '').trim());