const { Student, Score, AttendanceModel, ClassModel, SubjectModel, User } = require('./models'); /** * 1. 定义工具描述 (Schema) - Gemini Format */ const mongoTools = [ { functionDeclarations: [ { name: "query_database", description: "查询学校数据库。⚠️字段名必须准确:班级字段名是 'className' (禁止用 'class'),学生姓名在Student表是 'name',在成绩表是 'studentName'。支持的集合: 'Student', 'Score', 'Attendance', 'Class'。", parameters: { type: "OBJECT", properties: { collection: { type: "STRING", description: "集合名称", enum: ["Student", "Score", "Attendance", "Class"] }, filter: { type: "OBJECT", description: "Mongoose查询条件JSON。例: {className:'一年级(1)班', name:'张三'}。注意:查询班级时必须使用 'className' (禁止用 'class') 字段。", }, limit: { type: "NUMBER", description: "默认 5" } }, required: ["collection", "filter"] } } ] } ]; /** * 转换器:将 Gemini 工具定义转换为 OpenAI/Doubao 工具定义 */ function getOpenAITools() { return mongoTools[0].functionDeclarations.map(tool => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.parameters } })); } /** * 2. 安全守门员 (Security Guardrail) */ function injectSecurityFilter(filter, user, role, schoolId) { const safeFilter = { ...filter, schoolId }; if (role === 'ADMIN' || role === 'PRINCIPAL') { return safeFilter; } if (role === 'TEACHER') { // 暂时不做强制过滤,依赖业务层逻辑 } if (role === 'STUDENT') { if (!safeFilter.studentNo && !safeFilter.name) { safeFilter.studentNo = user.studentNo; } } return safeFilter; } /** * 辅助:递归修正查询字段名 (Deep Normalization) */ function normalizeQueryFields(query, collection) { if (!query || typeof query !== 'object') return query; if (Array.isArray(query)) { return query.map(item => normalizeQueryFields(item, collection)); } const newQuery = {}; for (const key in query) { let newKey = key; // 1. 修正班级字段 if (key === 'class') newKey = 'className'; // 2. 修正名字字段 if (collection === 'Score' || collection === 'Attendance') { if (key === 'name') newKey = 'studentName'; } // 递归处理值 newQuery[newKey] = normalizeQueryFields(query[key], collection); } return newQuery; } /** * 🌟 核心增强:智能模糊查询构建器 * 解决 "四年级6班" 查不到 "四年级(6)班" 的问题 */ function buildFuzzyQuery(filter) { const fuzzyFilter = { ...filter }; // 辅助:转义正则特殊字符 const escapeRegExp = (string) => { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; // 1. 处理 className (最常见的问题) if (fuzzyFilter.className && typeof fuzzyFilter.className === 'string') { const rawClass = fuzzyFilter.className; // 提取年级和班级号 (例如: "四年级6班" -> grade="四年级", num="6") // 支持中文数字 (一二三) 和 阿拉伯数字 const match = rawClass.match(/^(.+?级).*?(\d+|[一二三四五六七八九十]+).*?班$/); if (match) { const gradePart = match[1]; // 四年级 const numPart = match[2]; // 6 或 六 // 构建宽容的正则:匹配年级 + 任意字符(如括号) + 数字 + 任意字符 + 班 // 这样 "四年级6班" 可以匹配 "四年级(6)班", "四年级(6)班", "四年级六班" fuzzyFilter.className = { $regex: new RegExp(`^${escapeRegExp(gradePart)}.*(${numPart}|${convertNum(numPart)}).*班$`), $options: 'i' }; } else { // 如果正则没解析出来,至少做个包含匹配,且忽略括号 // 比如搜 "1班",能匹配 "(1)班" fuzzyFilter.className = { $regex: escapeRegExp(rawClass).replace(/[\(\)()]/g, '.?'), $options: 'i' }; } } // 2. 处理姓名 (模糊匹配) const nameFields = ['name', 'studentName', 'teacherName']; nameFields.forEach(field => { if (fuzzyFilter[field] && typeof fuzzyFilter[field] === 'string') { // 支持只搜 "张三" 匹配 "张三丰" fuzzyFilter[field] = { $regex: escapeRegExp(fuzzyFilter[field]), $options: 'i' }; } }); // 3. 处理 $or 数组中的模糊匹配 (递归) if (fuzzyFilter.$or && Array.isArray(fuzzyFilter.$or)) { fuzzyFilter.$or = fuzzyFilter.$or.map(subFilter => buildFuzzyQuery(subFilter)); } return fuzzyFilter; } // 简单的数字互转辅助 function convertNum(n) { const map = {'1':'一','2':'二','3':'三','4':'四','5':'五','6':'六','7':'七','8':'八','9':'九','10':'十', '一':'1','二':'2','三':'3','四':'4','五':'5','六':'6','七':'7','八':'8','九':'9','十':'10'}; return map[n] || n; } /** * 3. 工具执行器 (Executor) */ async function executeMongoTool(functionCall, user, role, schoolId) { let args = functionCall.args; if (typeof functionCall.arguments === 'string') { try { args = JSON.parse(functionCall.arguments); } catch (e) { console.error("❌ [MCP ERROR] Invalid JSON arguments:", functionCall.arguments); return { error: "Invalid JSON arguments" }; } } const { collection, filter = {}, limit = 5 } = args || {}; // 1. 字段名修正 (Key Normalization) const normalizedFilter = normalizeQueryFields(filter, collection); // 2. 🛡️ 安全注入 let safeFilter = injectSecurityFilter(normalizedFilter, user, role, schoolId); // 3. 🧠 智能模糊处理 (Value Normalization) safeFilter = buildFuzzyQuery(safeFilter); const safeLimit = Math.min(Math.max(limit, 1), 20); // --- 🔍 MCP LOGGING --- console.log(`\n================= [MCP TOOL CALL] =================`); console.log(`🛠️ Tool: query_database`); console.log(`📂 Collection: ${collection}`); console.log(`📥 AI Params: ${JSON.stringify(filter)}`); console.log(`🔒 Safe Query: ${JSON.stringify(safeFilter)}`); console.log(`---------------------------------------------------`); try { let result = []; let fields = ""; let relationalInfo = ""; // 🔥 关系查询增强:Score 表没有 className,需要先查 Student 获取 studentNo if (collection === 'Score' && safeFilter.className) { console.log(`🔄 [Relational] Score query detected className filter. Searching Students first...`); // 构造 Student 查询条件 const studentQuery = { schoolId: safeFilter.schoolId, className: safeFilter.className // 这里已经是正则了 }; const studentsInClass = await Student.find(studentQuery).select('studentNo name'); const studentNos = studentsInClass.map(s => s.studentNo); if (studentNos.length === 0) { console.log(`⚠️ [Relational] No students found in class matching: ${JSON.stringify(safeFilter.className)}`); return { info: `未找到班级符合条件的学生(查询条件:${JSON.stringify(safeFilter.className)}),无法查询成绩。` }; } console.log(`✅ [Relational] Found ${studentNos.length} students. Translating to Score query...`); relationalInfo = `(已找到 ${studentNos.length} 名学生,正在查询他们的成绩)`; // 替换查询条件:移除 className,添加 studentNo $in delete safeFilter.className; safeFilter.studentNo = { $in: studentNos }; } switch (collection) { case "Student": fields = "name studentNo className gender flowerBalance seatNo -_id"; result = await Student.find(safeFilter).select(fields).limit(safeLimit).lean(); break; case "Score": fields = "studentName courseName score type examName -_id"; result = await Score.find(safeFilter).select(fields).sort({ _id: -1 }).limit(safeLimit).lean(); break; case "Attendance": fields = "studentName date status -_id"; result = await AttendanceModel.find(safeFilter).select(fields).sort({ date: -1 }).limit(safeLimit).lean(); break; case "Class": fields = "grade className teacherName studentCount -_id"; result = await ClassModel.find(safeFilter).select("grade className teacherName").limit(safeLimit).lean(); break; default: console.log(`❌ [MCP ERROR] Unknown collection: ${collection}`); return { error: "Unknown collection" }; } console.log(`✅ [MCP SUCCESS] Found ${result.length} records.`); if (result.length > 0) { console.log(`📄 Sample: ${JSON.stringify(result[0])}`); } else { console.log(`⚠️ No records found.`); } console.log(`===================================================\n`); if (result.length === 0) { let msg = "未找到符合条件的数据。"; if (relationalInfo) msg += " " + relationalInfo + " 但这些学生似乎还没有录入成绩。"; else msg += " 请尝试放宽查询条件,或确认班级/姓名格式(例如包含括号)。"; return { info: msg }; } return result; } catch (error) { console.error("❌ [MCP EXCEPTION]", error.message); return { error: "Database query failed", details: error.message }; } } module.exports = { mongoTools, getOpenAITools, executeMongoTool };