stud-manager / ai-tools.js
dvc890's picture
Upload 64 files
9d69f4f verified
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 };