/** * 题目内容解析纯函数(共享层) * * 从 `unknown` 类型的题目内容中安全提取文本、选项、正确答案等。 * 所有函数均为纯函数,无副作用,便于单测。 * * 这些函数被 homework、error-book 等多个模块共享使用, * 避免各模块重复实现题目内容解析逻辑。 */ export type QuestionOption = { id: string text: string isCorrect?: boolean } export const isRecord = (v: unknown): v is Record => typeof v === "object" && v !== null export const getQuestionText = (content: unknown): string => { if (!isRecord(content)) return "" return typeof content.text === "string" ? content.text : "" } /** * 从题目内容中递归提取纯文本预览。 * * 支持以下内容格式: * - 字符串:直接返回(可截断) * - 数组:递归提取每个节点的 text 和 children,拼接为纯文本 * - 对象:提取 text 属性 * - 其他:返回 fallback * * @param content 题目内容(unknown 类型) * @param fallback 无法提取时的回退文本,默认空字符串 * @param maxLength 最大长度,0 表示不截断,默认 0 * @returns 提取的纯文本 */ export function extractQuestionPreview( content: unknown, fallback = "", maxLength = 0, ): string { const text = extractTextRecursive(content) if (text) { return maxLength > 0 ? text.slice(0, maxLength) : text } return fallback } function extractTextRecursive(content: unknown): string { if (typeof content === "string") return content if (Array.isArray(content)) { const parts: string[] = [] for (const node of content) { const text = extractTextRecursive(node) if (text) parts.push(text) } return parts.join("") } if (isRecord(content)) { if (typeof content.text === "string") return content.text if (Array.isArray(content.children)) { return extractTextRecursive(content.children) } } return "" } export const getOptions = (content: unknown): QuestionOption[] => { if (!isRecord(content)) return [] const raw = content.options if (!Array.isArray(raw)) return [] const out: QuestionOption[] = [] for (const item of raw) { if (!isRecord(item)) continue const id = typeof item.id === "string" ? item.id : "" const text = typeof item.text === "string" ? item.text : "" if (!id || !text) continue const isCorrect = item.isCorrect === true out.push({ id, text, isCorrect }) } return out } export const getChoiceCorrectIds = (content: unknown): string[] => { return getOptions(content) .filter((o): o is QuestionOption & { isCorrect: true } => o.isCorrect === true) .map((o) => o.id) } export const getJudgmentCorrectAnswer = (content: unknown): boolean | null => { if (!isRecord(content)) return null return typeof content.correctAnswer === "boolean" ? content.correctAnswer : null } export const getTextCorrectAnswers = (content: unknown): string[] => { if (!isRecord(content)) return [] const raw = content.correctAnswer if (typeof raw === "string") return [raw] if (Array.isArray(raw)) return raw.filter((x): x is string => typeof x === "string") return [] } export type QuestionType = "single_choice" | "multiple_choice" | "judgment" | "text" | string export const extractAnswerValue = (studentAnswer: unknown): unknown => { if (isRecord(studentAnswer) && "answer" in studentAnswer) return studentAnswer.answer return studentAnswer } /** * 从题目内容中提取正确答案,统一序列化为可存储的 JSON 格式。 * * - 选择题:返回正确选项 ID 数组 * - 判断题:返回布尔值 * - 填空题:返回正确答案字符串数组 * - 其他题型或无标准答案:返回 null * * @param questionType 题目类型 * @param content 题目内容(unknown 类型) * @returns 序列化后的正确答案,或 null */ export function extractCorrectAnswer( questionType: string, content: unknown, ): unknown { if (questionType === "single_choice" || questionType === "multiple_choice") { const ids = getChoiceCorrectIds(content) return ids.length > 0 ? ids : null } if (questionType === "judgment") { const answer = getJudgmentCorrectAnswer(content) return answer // 可能是 true/false/null } if (questionType === "text") { const answers = getTextCorrectAnswers(content) return answers.length > 0 ? answers : null } return null }