Spaces:
Running
Running
Upload 64 files
Browse files- ai-tools.js +55 -16
ai-tools.js
CHANGED
|
@@ -9,22 +9,22 @@ const mongoTools = [
|
|
| 9 |
functionDeclarations: [
|
| 10 |
{
|
| 11 |
name: "query_database",
|
| 12 |
-
description: "查询学校数据库
|
| 13 |
parameters: {
|
| 14 |
type: "OBJECT",
|
| 15 |
properties: {
|
| 16 |
collection: {
|
| 17 |
type: "STRING",
|
| 18 |
-
description: "
|
| 19 |
enum: ["Student", "Score", "Attendance", "Class"]
|
| 20 |
},
|
| 21 |
filter: {
|
| 22 |
type: "OBJECT",
|
| 23 |
-
description: "Mongoose
|
| 24 |
},
|
| 25 |
limit: {
|
| 26 |
type: "NUMBER",
|
| 27 |
-
description: "
|
| 28 |
}
|
| 29 |
},
|
| 30 |
required: ["collection", "filter"]
|
|
@@ -58,9 +58,9 @@ function injectSecurityFilter(filter, user, role, schoolId) {
|
|
| 58 |
return safeFilter;
|
| 59 |
}
|
| 60 |
|
|
|
|
| 61 |
if (role === 'TEACHER') {
|
| 62 |
-
|
| 63 |
-
// 实际逻辑可根据需求扩展
|
| 64 |
}
|
| 65 |
|
| 66 |
if (role === 'STUDENT') {
|
|
@@ -72,12 +72,41 @@ function injectSecurityFilter(filter, user, role, schoolId) {
|
|
| 72 |
return safeFilter;
|
| 73 |
}
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
/**
|
| 76 |
* 3. 工具执行器 (Executor)
|
| 77 |
*/
|
| 78 |
async function executeMongoTool(functionCall, user, role, schoolId) {
|
| 79 |
-
// 兼容 OpenAI 格式 (arguments 是字符串) 和 Gemini 格式 (args 是对象)
|
| 80 |
let args = functionCall.args;
|
|
|
|
| 81 |
if (typeof functionCall.arguments === 'string') {
|
| 82 |
try {
|
| 83 |
args = JSON.parse(functionCall.arguments);
|
|
@@ -89,17 +118,20 @@ async function executeMongoTool(functionCall, user, role, schoolId) {
|
|
| 89 |
|
| 90 |
const { collection, filter = {}, limit = 5 } = args || {};
|
| 91 |
|
|
|
|
|
|
|
|
|
|
| 92 |
// 🛡️ 安全注入
|
| 93 |
-
const safeFilter = injectSecurityFilter(
|
| 94 |
const safeLimit = Math.min(Math.max(limit, 1), 20);
|
| 95 |
|
| 96 |
-
// --- 🔍 MCP LOGGING
|
| 97 |
console.log(`\n================= [MCP TOOL CALL] =================`);
|
| 98 |
console.log(`🛠️ Tool: query_database`);
|
| 99 |
console.log(`📂 Collection: ${collection}`);
|
| 100 |
-
console.log(`📥 AI Params: ${JSON.stringify(filter)}`);
|
| 101 |
-
console.log(`
|
| 102 |
-
console.log(`
|
| 103 |
console.log(`---------------------------------------------------`);
|
| 104 |
|
| 105 |
try {
|
|
@@ -109,10 +141,17 @@ async function executeMongoTool(functionCall, user, role, schoolId) {
|
|
| 109 |
switch (collection) {
|
| 110 |
case "Student":
|
| 111 |
fields = "name studentNo className gender flowerBalance seatNo -_id";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
result = await Student.find(safeFilter).select(fields).limit(safeLimit).lean();
|
| 113 |
break;
|
| 114 |
case "Score":
|
| 115 |
fields = "studentName courseName score type examName -_id";
|
|
|
|
|
|
|
|
|
|
| 116 |
result = await Score.find(safeFilter).select(fields).sort({ _id: -1 }).limit(safeLimit).lean();
|
| 117 |
break;
|
| 118 |
case "Attendance":
|
|
@@ -125,24 +164,24 @@ async function executeMongoTool(functionCall, user, role, schoolId) {
|
|
| 125 |
break;
|
| 126 |
default:
|
| 127 |
console.log(`❌ [MCP ERROR] Unknown collection: ${collection}`);
|
| 128 |
-
console.log(`===================================================\n`);
|
| 129 |
return { error: "Unknown collection" };
|
| 130 |
}
|
| 131 |
|
| 132 |
console.log(`✅ [MCP SUCCESS] Found ${result.length} records.`);
|
| 133 |
if (result.length > 0) {
|
| 134 |
-
console.log(`📄 Sample
|
|
|
|
|
|
|
| 135 |
}
|
| 136 |
console.log(`===================================================\n`);
|
| 137 |
|
| 138 |
if (result.length === 0) {
|
| 139 |
-
return { info: "未找到符合条件的数据。" };
|
| 140 |
}
|
| 141 |
return result;
|
| 142 |
|
| 143 |
} catch (error) {
|
| 144 |
console.error("❌ [MCP EXCEPTION]", error.message);
|
| 145 |
-
console.log(`===================================================\n`);
|
| 146 |
return { error: "Database query failed", details: error.message };
|
| 147 |
}
|
| 148 |
}
|
|
|
|
| 9 |
functionDeclarations: [
|
| 10 |
{
|
| 11 |
name: "query_database",
|
| 12 |
+
description: "查询学校数据库。⚠️字段名必须准确:班级字段名是 'className' (禁止用 'class'),学生姓名在Student表是 'name',在成绩表是 'studentName'。支持的集合: 'Student', 'Score', 'Attendance', 'Class'。",
|
| 13 |
parameters: {
|
| 14 |
type: "OBJECT",
|
| 15 |
properties: {
|
| 16 |
collection: {
|
| 17 |
type: "STRING",
|
| 18 |
+
description: "集合名称",
|
| 19 |
enum: ["Student", "Score", "Attendance", "Class"]
|
| 20 |
},
|
| 21 |
filter: {
|
| 22 |
type: "OBJECT",
|
| 23 |
+
description: "Mongoose查询条件JSON。例: {className:'一年级(1)班', name:'张三'}。注意:查询班级时必须使用 'className' 字段。",
|
| 24 |
},
|
| 25 |
limit: {
|
| 26 |
type: "NUMBER",
|
| 27 |
+
description: "默认 5"
|
| 28 |
}
|
| 29 |
},
|
| 30 |
required: ["collection", "filter"]
|
|
|
|
| 58 |
return safeFilter;
|
| 59 |
}
|
| 60 |
|
| 61 |
+
// 老师只能看自己相关的班级逻辑可以在这里加强
|
| 62 |
if (role === 'TEACHER') {
|
| 63 |
+
// 暂时不做强制过滤,依赖业务层逻辑
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
if (role === 'STUDENT') {
|
|
|
|
| 72 |
return safeFilter;
|
| 73 |
}
|
| 74 |
|
| 75 |
+
/**
|
| 76 |
+
* 辅助:递归修正查询字段名 (Deep Normalization)
|
| 77 |
+
* AI 经常把 className 写成 class,把 studentName 写成 name,这里做统一修正
|
| 78 |
+
*/
|
| 79 |
+
function normalizeQueryFields(query, collection) {
|
| 80 |
+
if (!query || typeof query !== 'object') return query;
|
| 81 |
+
|
| 82 |
+
if (Array.isArray(query)) {
|
| 83 |
+
return query.map(item => normalizeQueryFields(item, collection));
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
const newQuery = {};
|
| 87 |
+
for (const key in query) {
|
| 88 |
+
let newKey = key;
|
| 89 |
+
|
| 90 |
+
// 1. 修正班级字段: class -> className
|
| 91 |
+
if (key === 'class') newKey = 'className';
|
| 92 |
+
|
| 93 |
+
// 2. 修正名字字段: Score表里叫 studentName, Student表里叫 name
|
| 94 |
+
if (collection === 'Score' || collection === 'Attendance') {
|
| 95 |
+
if (key === 'name') newKey = 'studentName';
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// 递归处理值 (例如 $or 数组内部的对象)
|
| 99 |
+
newQuery[newKey] = normalizeQueryFields(query[key], collection);
|
| 100 |
+
}
|
| 101 |
+
return newQuery;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
/**
|
| 105 |
* 3. 工具执行器 (Executor)
|
| 106 |
*/
|
| 107 |
async function executeMongoTool(functionCall, user, role, schoolId) {
|
|
|
|
| 108 |
let args = functionCall.args;
|
| 109 |
+
// 兼容 OpenAI 格式
|
| 110 |
if (typeof functionCall.arguments === 'string') {
|
| 111 |
try {
|
| 112 |
args = JSON.parse(functionCall.arguments);
|
|
|
|
| 118 |
|
| 119 |
const { collection, filter = {}, limit = 5 } = args || {};
|
| 120 |
|
| 121 |
+
// 🛠️ 关键修复:在注入安全字段前,先修正 AI 的字段命名错误
|
| 122 |
+
const normalizedFilter = normalizeQueryFields(filter, collection);
|
| 123 |
+
|
| 124 |
// 🛡️ 安全注入
|
| 125 |
+
const safeFilter = injectSecurityFilter(normalizedFilter, user, role, schoolId);
|
| 126 |
const safeLimit = Math.min(Math.max(limit, 1), 20);
|
| 127 |
|
| 128 |
+
// --- 🔍 MCP LOGGING ---
|
| 129 |
console.log(`\n================= [MCP TOOL CALL] =================`);
|
| 130 |
console.log(`🛠️ Tool: query_database`);
|
| 131 |
console.log(`📂 Collection: ${collection}`);
|
| 132 |
+
console.log(`📥 AI Params: ${JSON.stringify(filter)}`); // 原始
|
| 133 |
+
console.log(`🔧 Normalized: ${JSON.stringify(normalizedFilter)}`); // 修正后
|
| 134 |
+
console.log(`🔒 Safe Query: ${JSON.stringify(safeFilter)}`); // 最终
|
| 135 |
console.log(`---------------------------------------------------`);
|
| 136 |
|
| 137 |
try {
|
|
|
|
| 141 |
switch (collection) {
|
| 142 |
case "Student":
|
| 143 |
fields = "name studentNo className gender flowerBalance seatNo -_id";
|
| 144 |
+
// 模糊搜索支持
|
| 145 |
+
if (safeFilter.name && !safeFilter.name.$regex) {
|
| 146 |
+
safeFilter.name = { $regex: safeFilter.name, $options: 'i' };
|
| 147 |
+
}
|
| 148 |
result = await Student.find(safeFilter).select(fields).limit(safeLimit).lean();
|
| 149 |
break;
|
| 150 |
case "Score":
|
| 151 |
fields = "studentName courseName score type examName -_id";
|
| 152 |
+
if (safeFilter.studentName && !safeFilter.studentName.$regex) {
|
| 153 |
+
safeFilter.studentName = { $regex: safeFilter.studentName, $options: 'i' };
|
| 154 |
+
}
|
| 155 |
result = await Score.find(safeFilter).select(fields).sort({ _id: -1 }).limit(safeLimit).lean();
|
| 156 |
break;
|
| 157 |
case "Attendance":
|
|
|
|
| 164 |
break;
|
| 165 |
default:
|
| 166 |
console.log(`❌ [MCP ERROR] Unknown collection: ${collection}`);
|
|
|
|
| 167 |
return { error: "Unknown collection" };
|
| 168 |
}
|
| 169 |
|
| 170 |
console.log(`✅ [MCP SUCCESS] Found ${result.length} records.`);
|
| 171 |
if (result.length > 0) {
|
| 172 |
+
console.log(`📄 Sample: ${JSON.stringify(result[0])}`);
|
| 173 |
+
} else {
|
| 174 |
+
console.log(`⚠️ No records found. Hint: Check if schoolId matches.`);
|
| 175 |
}
|
| 176 |
console.log(`===================================================\n`);
|
| 177 |
|
| 178 |
if (result.length === 0) {
|
| 179 |
+
return { info: "未找到符合条件的数据。请确认查询条件是否准确(如姓名是否正确)。" };
|
| 180 |
}
|
| 181 |
return result;
|
| 182 |
|
| 183 |
} catch (error) {
|
| 184 |
console.error("❌ [MCP EXCEPTION]", error.message);
|
|
|
|
| 185 |
return { error: "Database query failed", details: error.message };
|
| 186 |
}
|
| 187 |
}
|