/** * 题目内容类型定义与类型守卫。 * * 题目 content 存储为 JSON(unknown),实际结构为: * - { text: string } — 简答题/判断题 * - { text: string, options: QuestionOption[] } — 选择题 * - { text: string, options: QuestionOption[], answer?: string, explanation?: string } — 完整结构 * * 所有从 unknown 到具体类型的转换必须使用本文件的类型守卫函数, * 禁止使用 `as` 断言。 */ /** 选择题选项 */ export interface QuestionOption { id: string text: string isCorrect: boolean } /** 题目内容结构化类型 */ export interface QuestionContent { text: string options?: QuestionOption[] answer?: string explanation?: string } /** 类型守卫:判断值是否为字符串 */ function isString(value: unknown): value is string { return typeof value === "string" } /** 类型守卫:判断值是否为非 null 对象 */ function isNonNullObject(value: unknown): value is Record { return typeof value === "object" && value !== null } /** 类型守卫:判断值是否为 QuestionOption */ function isQuestionOption(value: unknown): value is QuestionOption { if (!isNonNullObject(value)) return false const id = value.id const text = value.text const isCorrect = value.isCorrect return ( (isString(id) || id === undefined) && isString(text) && typeof isCorrect === "boolean" ) } /** 类型守卫:判断值是否为 QuestionOption 数组 */ function isQuestionOptionArray(value: unknown): value is QuestionOption[] { return Array.isArray(value) && value.every(isQuestionOption) } /** * 从 unknown 安全解析为 QuestionContent。 * * 处理以下情况: * - 字符串:转为 { text: string } * - 对象:提取 text/options/answer/explanation 字段 * - null/undefined:返回 { text: "" } * - 其他:JSON.stringify 后转为 { text } * * @example * const content = parseQuestionContent(question.content) * console.log(content.text) // 题干文本 * console.log(content.options) // 选项列表(选择题) */ export function parseQuestionContent(raw: unknown): QuestionContent { if (isString(raw)) { return { text: raw } } if (isNonNullObject(raw)) { const text = isString(raw.text) ? raw.text : "" const optionsRaw = raw.options const options = isQuestionOptionArray(optionsRaw) ? optionsRaw.map((opt) => ({ id: opt.id ?? opt.text, text: opt.text, isCorrect: opt.isCorrect, })) : undefined const answer = isString(raw.answer) ? raw.answer : undefined const explanation = isString(raw.explanation) ? raw.explanation : undefined return { text, options, answer, explanation } } if (raw == null) { return { text: "" } } try { return { text: JSON.stringify(raw) } } catch { return { text: "" } } } /** * 从 QuestionContent 提取纯文本预览(截断到指定长度)。 * * @param raw - 原始 content(unknown) * @param maxLength - 最大长度,默认 80 */ export function getQuestionPreview(raw: unknown, maxLength = 80): string { const content = parseQuestionContent(raw) return content.text.slice(0, maxLength) } /** * 从 QuestionContent 提取选项列表(用于表单回填)。 * * @returns 选项数组,无选项时返回 undefined */ export function getQuestionOptions(raw: unknown): QuestionOption[] | undefined { const content = parseQuestionContent(raw) return content.options } /** * 从 QuestionContent 提取纯文本(用于 AI 变体生成等场景)。 */ export function getQuestionText(raw: unknown): string { return parseQuestionContent(raw).text }