stud-manager / ai-context.js
dvc890's picture
Upload 67 files
8d16fdf verified
const {
User, Student, Score, AttendanceModel, ClassModel,
Course, ConfigModel
} = require('./models');
/**
* 格式化当前日期
*/
const getCurrentDateInfo = () => {
const now = new Date();
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}${days[now.getDay()]}`;
};
/**
* 辅助函数:解析学年前缀
* 例如 "2023-2024学年 第二学期" -> "2023-2024学年"
*/
const getSchoolYearPrefix = (semester) => {
if (!semester) return null;
const match = semester.match(/^(.+?学年)/);
return match ? match[1] : null;
};
/**
* 构建学生画像上下文 (学生视角)
*/
async function buildStudentContext(username, schoolId) {
const student = await Student.findOne({
$or: [{ studentNo: username }, { name: username }],
schoolId
});
if (!student) return "无法找到该学生的详细档案。";
// 获取当前学期配置
const config = await ConfigModel.findOne({ key: 'main' });
const currentSemester = config ? config.semester : null;
// 逻辑:尝试拉取本学年的所有数据 (第一学期 + 第二学期)
const schoolYearPrefix = getSchoolYearPrefix(currentSemester);
const scoreQuery = {
studentNo: student.studentNo,
schoolId
};
if (schoolYearPrefix) {
// 匹配该学年开头的所有学期
scoreQuery.semester = { $regex: new RegExp('^' + schoolYearPrefix) };
} else if (currentSemester) {
scoreQuery.semester = currentSemester;
}
// 获取成绩 (稍微放宽限制以容纳整年数据)
const recentScores = await Score.find(scoreQuery).sort({ semester: -1, _id: -1 }).limit(100);
// 获取考勤概况
const attendanceStats = await AttendanceModel.aggregate([
{ $match: { studentId: student._id.toString() } },
{ $group: { _id: "$status", count: { $sum: 1 } } }
]);
const absentCount = attendanceStats.find(a => a._id === 'Absent')?.count || 0;
const leaveCount = attendanceStats.find(a => a._id === 'Leave')?.count || 0;
let prompt = `
### 当前用户身份:学生 (个人视图)
- **姓名**: ${student.name}
- **班级**: ${student.className}
- **学号**: ${student.studentNo}
- **积分(小红花)**: ${student.flowerBalance} 🌺
- **当前学期**: ${currentSemester || '全部'}
### 个人学习数据
`;
if (recentScores.length > 0) {
// 格式化输出,带上学期标识
const scoreList = recentScores.map(s => {
const semShort = s.semester ? s.semester.replace(schoolYearPrefix || '', '').trim() : '';
return `${s.courseName}: ${s.score} (${semShort} ${s.examName||s.type})`;
}).join('\n');
prompt += `#### 成绩记录 (本学年):\n${scoreList}\n`;
} else {
prompt += `- **近期成绩**: 暂无记录\n`;
}
if (absentCount > 0 || leaveCount > 0) {
prompt += `- **考勤**: 缺勤 ${absentCount} 次,请假 ${leaveCount} 次。\n`;
} else {
prompt += `- **考勤**: 全勤。\n`;
}
return prompt;
}
/**
* 构建教师画像上下文 (严格权限版 + 全量数据)
*/
async function buildTeacherContext(username, schoolId) {
const user = await User.findOne({ username, schoolId });
if (!user) return "无法找到该教师档案。";
const homeroomClass = user.homeroomClass;
// 1. 查找任教课程信息 (确定科任权限)
const courses = await Course.find({
schoolId,
$or: [{ teacherId: user._id }, { teacherName: user.trueName || user.username }]
});
// 2. 确定有权限的班级列表
const authorizedClasses = new Set();
const teachingSubjectsMap = {}; // Map<ClassName, Set<SubjectName>>
// 班主任权限:拥有该班级所有数据权限
if (homeroomClass) {
authorizedClasses.add(homeroomClass);
}
// 科任权限:拥有特定班级的特定科目权限
courses.forEach(c => {
if (c.className) {
authorizedClasses.add(c.className);
if (!teachingSubjectsMap[c.className]) teachingSubjectsMap[c.className] = new Set();
teachingSubjectsMap[c.className].add(c.courseName);
}
});
const classList = Array.from(authorizedClasses);
if (classList.length === 0) {
return `### 当前用户身份:教师 (${user.trueName})\n目前系统显示您未绑定任何班级。请告知用户去“班级管理”或“课程安排”进行绑定。`;
}
// 3. 全量拉取相关班级的学生
const students = await Student.find({
schoolId,
className: { $in: classList },
status: 'Enrolled'
}).sort({ seatNo: 1, studentNo: 1 }); // 按座号排序
if (students.length === 0) {
return `### 当前用户身份:教师\n管理班级: [${classList.join(', ')}]\n但系统未在这些班级找到学生档案。`;
}
const studentNos = students.map(s => s.studentNo);
// 4. 拉取这些学生的成绩 (限制为本学年:包含第一学期和第二学期)
const config = await ConfigModel.findOne({ key: 'main' });
const currentSemester = config ? config.semester : null;
const schoolYearPrefix = getSchoolYearPrefix(currentSemester);
const scoreQuery = {
schoolId,
studentNo: { $in: studentNos },
status: 'Normal'
};
// 关键修复:使用正则匹配整个学年 (例如 "2024-2025学年")
if (schoolYearPrefix) {
scoreQuery.semester = { $regex: new RegExp('^' + schoolYearPrefix) };
} else if (currentSemester) {
// 如果无法解析年份,回退到当前学期
scoreQuery.semester = currentSemester;
}
const allScores = await Score.find(scoreQuery);
// 5. 构建 Prompt
let prompt = `
### 当前用户身份:教师 (${user.trueName || username})
### 权限范围 (严格遵守)
你只能回答下列班级的数据。如果用户询问其他班级(例如用户只教一年级,却问四年级),请礼貌拒绝,说明权限不足。
管理班级: [${classList.join(', ')}]
数据范围: ${schoolYearPrefix ? schoolYearPrefix + " (全学年)" : (currentSemester || '所有历史')}
### 详细班级数据
`;
for (const cls of classList) {
const isClassHomeroom = cls === homeroomClass;
const subjects = teachingSubjectsMap[cls] ? Array.from(teachingSubjectsMap[cls]) : [];
const roleText = isClassHomeroom ? "班主任 (全科权限)" : `任课老师 (科目: ${subjects.join(', ')})`;
prompt += `\n#### 🏫 ${cls} [${roleText}]\n`;
const clsStudents = students.filter(s => s.className === cls);
if (clsStudents.length === 0) {
prompt += "(暂无学生)\n";
continue;
}
prompt += `学生总数: ${clsStudents.length}人\n名单及成绩 (格式: [学期]科目:分数):\n`;
for (const s of clsStudents) {
let sScores = allScores.filter(sc => sc.studentNo === s.studentNo);
// 如果不是班主任,仅展示自己教的科目成绩
if (!isClassHomeroom && subjects.length > 0) {
sScores = sScores.filter(sc => subjects.includes(sc.courseName));
}
// 格式: 张三(01号): [一]语文:90, [二]语文:85...
const scoreStr = sScores.length > 0
? sScores.map(sc => {
// 简化学期显示,例如 "第一学期" -> "一", "第二学期" -> "二"
let semLabel = "";
if (sc.semester && schoolYearPrefix) {
if (sc.semester.includes("第一")) semLabel = "[上]";
else if (sc.semester.includes("第二")) semLabel = "[下]";
}
return `${semLabel}${sc.courseName}:${sc.score}`;
}).join(', ')
: (isClassHomeroom ? "暂无本学年成绩" : "无本科目成绩");
prompt += `- ${s.name} (${s.seatNo ? s.seatNo+'号' : '无座号'}): ${scoreStr}\n`;
}
}
return prompt;
}
/**
* 构建管理员/校长画像上下文
*/
async function buildAdminContext(role, schoolId) {
return `### 当前用户身份:${role === 'PRINCIPAL' ? '校长' : '超级管理员'}\n你拥有最高权限,但请注意保护隐私。`;
}
/**
* 主入口
*/
async function buildUserContext(username, role, schoolId) {
try {
const dateStr = getCurrentDateInfo();
let roleContext = "";
if (role === 'STUDENT') {
roleContext = await buildStudentContext(username, schoolId);
} else if (role === 'TEACHER') {
roleContext = await buildTeacherContext(username, schoolId);
} else if (role === 'ADMIN' || role === 'PRINCIPAL') {
roleContext = await buildAdminContext(role, schoolId);
}
return `
---
【系统注入上下文】
当前时间: ${dateStr}
以下是当前用户权限范围内的 **全量真实数据**。
${roleContext}
【AI 行为准则】
1. **角色设定**: 你是学校的AI智能助手。你的职责是协助查询校内信息,同时也乐于回答通用的百科/日常问题。
2. **数据查询规则**:
- 当用户询问 **学校内部数据** (如: 学生成绩、考勤、班级名单、老师信息) 时,**必须且只能** 使用上述提供的【系统注入上下文】。
- 如果用户询问的 **校内数据** 不在上下文中 (例如: 用户只教一年级,却问三年级的数据),请明确告知“无权限查看该数据”。
3. **通用问答规则**:
- 当用户询问 **非校内数据** (如: 天气、历史、写代码、翻译、闲聊) 时,请忽略权限限制,利用你的通用知识库或联网搜索功能正常回答。不要因为上下文中没有天气数据就拒绝回答。
4. **联网搜索**: 如果用户开启了联网搜索,积极搜索最新信息回答通用问题。
---
`;
} catch (e) {
console.error("Context build failed:", e);
return "";
}
}
module.exports = { buildUserContext };