Spaces:
Running
Running
| 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 }; | |