feat(ai): V2 深度增强 — SSE 流式/全局助手/内容安全/多角色覆盖

对标 Khanmigo/Duolingo Max/Squirrel AI/Century Tech 实现:

- SSE 流式响应:createAiChatCompletionStream AsyncGenerator + /api/ai/chat/stream SSE 端点 + useAiChatStream hook(AbortController 停止生成 + localStorage 持久化)

- Markdown 渲染:AiMarkdownRenderer(react-markdown + remark-gfm + 代码块/表格/列表 + hover 复制按钮)

- 全局 AI 助手:AiAssistantWidget 浮动按钮 + Sheet 侧抽屉 + usePathname 路由推断上下文(7 类场景系统提示)+ dashboard layout 全局注入 AiClientProvider

- 内容安全:content-safety.ts 多层过滤(输入/输出安全过滤 + 每日限制 student 50/teacher 200/parent 30/admin 500 + 学生苏格拉底模式),COPPA/FERPA K12 合规

- 多角色 AI 覆盖:家长端 AiChildSummary(学情摘要)+ 管理员端 AiUsageDashboard(使用监控)+ 学生端 AiStudyPath(个性化学习路径)

- i18n 修复:8 处错误键引用 + zh-CN/en ai.json 全面扩展

- 架构文档 004/005 同步更新
This commit is contained in:
SpecialX
2026-06-23 01:34:37 +08:00
parent a60105455e
commit 4da9194a5e
27 changed files with 3522 additions and 172 deletions

View File

@@ -9,6 +9,8 @@ import {
QUESTION_VARIANT_SYSTEM_PROMPT,
SIMILAR_QUESTION_SYSTEM_PROMPT,
WEAKNESS_ANALYSIS_SYSTEM_PROMPT,
CHILD_SUMMARY_SYSTEM_PROMPT,
STUDY_PATH_SYSTEM_PROMPT,
} from "./prompt-templates"
import { withAiTracking } from "./usage-tracker"
import {
@@ -17,6 +19,8 @@ import {
QuestionVariantResultSchema,
SimilarQuestionListSchema,
WeaknessAnalysisResultSchema,
ChildSummaryResultSchema,
StudyPathResultSchema,
} from "../schema"
import type {
AiChatMessage,
@@ -33,6 +37,10 @@ import type {
SimilarQuestionResult,
WeaknessAnalysisInput,
WeaknessAnalysisResult,
ChildSummaryInput,
ChildSummaryResult,
StudyPathInput,
StudyPathResult,
} from "../types"
// ---------------------------------------------------------------------------
@@ -320,6 +328,76 @@ export class DefaultAiService implements AiService {
return { result: validated.data }
})
}
async generateChildSummary(input: ChildSummaryInput): Promise<ChildSummaryResult> {
return withAiTracking(this.userId, "weakness_analysis", undefined, async () => {
const userLines = [
`Student ID: ${input.studentId}`,
input.studentName ? `Student Name: ${input.studentName}` : "",
input.grade ? `Grade: ${input.grade}` : "",
input.recentGrades && input.recentGrades.length > 0
? `Recent Grades:\n${JSON.stringify(input.recentGrades, null, 2)}`
: "",
input.attendanceRate !== undefined
? `Attendance Rate: ${(input.attendanceRate * 100).toFixed(1)}%`
: "",
input.errorBookSummary
? `Error Book Summary:\n${JSON.stringify(input.errorBookSummary, null, 2)}`
: "",
input.homeworkCompletionRate !== undefined
? `Homework Completion Rate: ${(input.homeworkCompletionRate * 100).toFixed(1)}%`
: "",
].filter((line) => line.length > 0)
const { content } = await callAi(
buildChatMessages(CHILD_SUMMARY_SYSTEM_PROMPT, userLines.join("\n\n")),
{ temperature: 0.4, maxTokens: 2000 }
)
const parsed = extractJson(content)
const validated = ChildSummaryResultSchema.safeParse(parsed)
if (!validated.success) {
return {
result: {
overallAssessment: "Unable to generate summary at this time.",
strengths: [],
areasForImprovement: [],
familyTutoringSuggestions: [],
nextSteps: [],
},
}
}
return { result: validated.data }
})
}
async recommendStudyPath(input: StudyPathInput): Promise<StudyPathResult> {
return withAiTracking(this.userId, "weakness_analysis", undefined, async () => {
const userLines = [
`Student ID: ${input.studentId}`,
input.subject ? `Subject: ${input.subject}` : "",
input.currentMastery && input.currentMastery.length > 0
? `Current Mastery:\n${JSON.stringify(input.currentMastery, null, 2)}`
: "",
input.learningGoal ? `Learning Goal: ${input.learningGoal}` : "",
].filter((line) => line.length > 0)
const { content } = await callAi(
buildChatMessages(STUDY_PATH_SYSTEM_PROMPT, userLines.join("\n\n")),
{ temperature: 0.5, maxTokens: 2000 }
)
const parsed = extractJson(content)
const validated = StudyPathResultSchema.safeParse(parsed)
if (!validated.success) {
return {
result: {
currentLevel: "Analysis unavailable",
learningPath: [],
summary: "Unable to generate learning path at this time.",
motivation: "Keep learning!",
},
}
}
return { result: validated.data }
})
}
}
/**

View File

@@ -0,0 +1,173 @@
import "server-only"
/**
* AI 内容安全过滤
*
* 多层防护:
* 1. 输入过滤:检查用户输入是否包含不当内容
* 2. 输出过滤:检查 AI 回复是否包含不当内容
* 3. 每日限制:按用户 + 日期计数
*
* 参考 Khanmigo 的多层 moderation 模式。
*/
// ---------------------------------------------------------------------------
// 不当内容关键词(基础过滤,生产环境应接入专业 Moderation API
// ---------------------------------------------------------------------------
const BLOCKED_INPUT_PATTERNS: readonly RegExp[] = [
/\b(violence|kill|murder|suicide|self[- ]?harm|cut myself)\b/i,
/\b(porn|sex|nude|nsfw|explicit)\b/i,
/\b(drug|cocaine|heroin|weed|marijuana)\b/i,
/\b(hack|exploit|malware|virus|phishing)\b/i,
// PII 请求
/\b(your (password|credit card|ssn|social security|bank account))\b/i,
/\b(home address|phone number|real name)\b/i,
]
const BLOCKED_OUTPUT_PATTERNS: readonly RegExp[] = [
/\b(violence|kill|murder|suicide|self[- ]?harm)\b/i,
/\b(porn|sex|nude|nsfw|explicit)\b/i,
/\b(drug|cocaine|heroin)\b/i,
]
const STUDENT_BLOCKED_PATTERNS: readonly RegExp[] = [
// 学生侧额外限制:禁止直接给出作业答案
/\b(here is the (complete )?answer|the answer is:?)\b/i,
]
// ---------------------------------------------------------------------------
// 输入过滤
// ---------------------------------------------------------------------------
export type SafetyFilterResult = {
blocked: boolean
reason?: string
}
export const filterUserInput = (
content: string,
options?: { isStudent?: boolean }
): SafetyFilterResult => {
const text = String(content ?? "")
for (const pattern of BLOCKED_INPUT_PATTERNS) {
if (pattern.test(text)) {
return {
blocked: true,
reason: "Input contains inappropriate content",
}
}
}
if (options?.isStudent) {
// 学生侧额外检查
for (const pattern of STUDENT_BLOCKED_PATTERNS) {
if (pattern.test(text)) {
return {
blocked: true,
reason: "Student input blocked by safety filter",
}
}
}
}
return { blocked: false }
}
// ---------------------------------------------------------------------------
// 输出过滤
// ---------------------------------------------------------------------------
export const filterAiOutput = (
content: string,
options?: { isStudent?: boolean }
): SafetyFilterResult => {
const text = String(content ?? "")
for (const pattern of BLOCKED_OUTPUT_PATTERNS) {
if (pattern.test(text)) {
return {
blocked: true,
reason: "AI output contains inappropriate content",
}
}
}
if (options?.isStudent) {
for (const pattern of STUDENT_BLOCKED_PATTERNS) {
if (pattern.test(text)) {
return {
blocked: true,
reason: "AI output blocked for student safety",
}
}
}
}
return { blocked: false }
}
// ---------------------------------------------------------------------------
// 每日限制
// ---------------------------------------------------------------------------
const DAILY_LIMITS: Record<string, number> = {
student: 50,
teacher: 200,
parent: 30,
admin: 500,
}
export const getDailyLimit = (role: string): number => {
return DAILY_LIMITS[role] ?? 50
}
/**
* 检查用户今日 AI 使用次数
*
* 生产环境应接入 Redis 或数据库计数器。
* 当前实现为内存映射(单实例场景),多实例需替换为 Redis。
*/
const dailyUsageMap = new Map<string, { date: string; count: number }>()
export const checkDailyLimit = (userId: string, role: string): SafetyFilterResult => {
const today = new Date().toISOString().slice(0, 10)
const key = `${userId}:${today}`
const limit = getDailyLimit(role)
const current = dailyUsageMap.get(key)
if (!current) {
return { blocked: false }
}
if (current.count >= limit) {
return {
blocked: true,
reason: `Daily limit reached (${current.count}/${limit})`,
}
}
return { blocked: false }
}
export const incrementDailyUsage = (userId: string): void => {
const today = new Date().toISOString().slice(0, 10)
const key = `${userId}:${today}`
const current = dailyUsageMap.get(key)
if (current && current.date === today) {
current.count += 1
} else {
dailyUsageMap.set(key, { date: today, count: 1 })
}
// 清理过期条目(防止内存泄漏)
if (dailyUsageMap.size > 10000) {
for (const [k, v] of dailyUsageMap.entries()) {
if (v.date !== today) {
dailyUsageMap.delete(k)
}
}
}
}

View File

@@ -152,3 +152,72 @@ export const JSON_REPAIR_SYSTEM_PROMPT = [
"Do not use placeholders such as ... or [...].",
"Return JSON only without markdown.",
].join("\n")
// ---------------------------------------------------------------------------
// 通用聊天(全局 AI 助手)
// ---------------------------------------------------------------------------
export const CHAT_SYSTEM_PROMPT = [
"You are a helpful K12 education assistant for the Next_Edu school management system.",
"You assist teachers, students, parents, and administrators with their daily tasks.",
"Respond in the user's language (Chinese by default).",
"Use Markdown formatting for structured content (lists, tables, code blocks).",
"Be concise, accurate, and pedagogically sound.",
].join("\n")
// ---------------------------------------------------------------------------
// 家长学情摘要
// ---------------------------------------------------------------------------
export const CHILD_SUMMARY_SYSTEM_PROMPT = [
"You are an expert K12 family education advisor.",
"Analyze the student's learning data and generate a summary for parents.",
"Return JSON only without markdown.",
"Output schema:",
"{",
' "overallAssessment": "brief overall assessment in parent-friendly language",',
' "strengths": ["strength 1", "strength 2"],',
' "areasForImprovement": ["area 1", "area 2"],',
' "familyTutoringSuggestions": ["suggestion 1", "suggestion 2"],',
' "nextSteps": ["actionable next step 1", "actionable next step 2"]',
"}",
"Rules:",
"- Use encouraging and constructive tone.",
"- Focus on actionable advice parents can follow at home.",
"- Avoid educational jargon; use plain language.",
"- Consider cultural sensitivity in family education.",
"- If data is limited, provide general guidance.",
"Never output placeholders.",
].join("\n")
// ---------------------------------------------------------------------------
// 学习路径推荐
// ---------------------------------------------------------------------------
export const STUDY_PATH_SYSTEM_PROMPT = [
"You are an expert K12 adaptive learning path designer.",
"Based on the student's current mastery levels, recommend a personalized learning path.",
"Return JSON only without markdown.",
"Output schema:",
"{",
' "currentLevel": "brief description of current level",',
' "learningPath": [',
" {",
' "step": 1,',
' "knowledgePoint": "knowledge point name",',
' "status": "mastered | in_progress | needs_work",',
' "recommendedAction": "specific action to take",',
' "estimatedTime": "15 min"',
" }",
" ],",
' "summary": "brief summary of the learning path",',
' "motivation": "encouraging message for the student"',
"}",
"Rules:",
"- Order learning path from foundational to advanced.",
"- Prioritize weak areas (mastery < 2) first.",
"- Include 3-7 steps in the learning path.",
"- estimatedTime should be realistic (5-30 min per step).",
"- motivation should be age-appropriate and encouraging.",
"Never output placeholders.",
].join("\n")