// 消息路由 + AI 分析 const express = require('express'); const { authMiddleware } = require('../middleware/auth'); const { broadcastToRoomExcludeSelf, broadcastToRoom } = require('../ws'); const { buildSystemPrompt } = require('../ai/prompt'); const { validate, normalize } = require('../ai/validator'); const { callDeepSeek } = require('../ai/client'); const { TOOLS } = require('../ai/tools'); const { getHistory, addToHistory, hasPendingAction, handleAIResult, executePendingAction } = require('../handlers'); const db = require('../db'); const { timestamp } = require('../utils'); const router = express.Router(); router.get('/:roomId/messages', authMiddleware, (req, res) => { const msgs = db.prepare('SELECT * FROM messages WHERE room_id = ? ORDER BY id ASC').all(req.params.roomId); res.json(msgs); }); router.post('/:roomId/messages', authMiddleware, async (req, res) => { const { text, attachments } = req.body; console.log('📨 收到消息, text:', (text||'').substring(0,30), 'attachments:', JSON.stringify(attachments)); const roomId = req.params.roomId; const username = req.user.username; const now = timestamp(); const result = db.prepare('INSERT INTO messages (room_id, user, text, attachments, timestamp) VALUES (?,?,?,?,?)').run( roomId, username, text || '', attachments ? JSON.stringify(attachments) : null, 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 id DESC LIMIT 1').get(roomId); broadcastToRoomExcludeSelf(roomId, username, { type: 'new_message', message: msg, 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; } try { 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) { var responses = Array.isArray(aiResponse) ? aiResponse : [aiResponse]; var allIgnore = responses.every(function(r) { return r.action === 'ignore'; }); responses.forEach(function(r) { if (r.action !== 'ignore') handleAIResult(r, roomId, username, text || '', attachments || []); }); if (allIgnore) broadcastToRoom(roomId, { type: 'clear_temp' }); } } catch (e) { console.error('AI 分析失败:', e.message); var errText = '❌ ' + (e.message || 'AI 处理失败,请稍后重试'); 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) { 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 message = await callDeepSeek(messages, username, 0.1, true, TOOLS); // 1) 工具调用路径:模型调用了 create_purchase / query_purchases / reply_chat / ignore if (message && Array.isArray(message.tool_calls) && message.tool_calls.length) { message.tool_calls.forEach(function(tc, i) { const fn = tc.function || {}; console.log('🤖 AI 工具调用[' + i + ']: ' + (fn.name || '?') + ' args=' + (fn.arguments || '').substring(0, 300)); }); const results = []; message.tool_calls.forEach(function(tc) { const fn = tc.function || {}; const name = fn.name || ''; let args = {}; try { args = JSON.parse(fn.arguments || '{}'); } catch(e) { console.warn('⚠️ 工具参数解析失败:', name, (fn.arguments || '').substring(0, 100)); args = {}; } if (name === 'create_purchase') { // 工具参数已是结构化对象,补上 action 字段以复用现有 handleAIResult 的 purchase 分支 results.push(Object.assign({ action: 'purchase' }, args)); } else if (name === 'query_purchases') { // 复用现有 query 分支(本地查库 + 二次汇总),不依赖模型返回结果 results.push({ action: 'query', keywords: args.keywords || '' }); } else if (name === 'reply_chat') { results.push({ action: 'chat', reply: args.reply || '好的。' }); } else { // ignore 或未知工具 → 忽略 console.warn('⚠️ 未识别的工具名:', name); results.push({ action: 'ignore' }); } }); return results; } // 2) 纯文本/JSON 路径:query / delete / clear_all 等按旧逻辑解析 const content = (message && typeof message === 'object') ? (message.content || '') : (message || ''); console.log('🤖 AI 返回:', content.substring(0, 200)); try { 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 console.warn('⚠️ AI 返回非 JSON,降级为 chat:', content.substring(0, 50)); return { action: 'chat', reply: content }; } } module.exports = router;