/** * AI 试卷生成管线(入口模块) * * 本目录由三个子模块组成: * - parse.ts: Zod schema、JSON 解析、纯转换函数、提示词 * - request.ts: AI 请求构造与发送 * - structure.ts: 结构生成与预览/草稿转换 * * 本文件负责: * - 重新导出公共 API(schema、类型、函数) * - 编排高层流程(generateAiPreviewData / generateAiCreateDraftFromSource) */ import { z } from "zod" import { AiExamResponseSchema, AiQuestionSchema, } from "./parse" import { parseQuestionDetail, requestAiExamDraft, requestAiExamStructureDraft, validateExamSourceText, type SplitQuestionItem, } from "./request" import { buildPreviewPayload, mapWithConcurrency, previewToDraft, splitStructureItems, } from "./structure" // --------------------------------------------------------------------------- // Re-export public schemas & types // --------------------------------------------------------------------------- export { AiGeneratedStructureSchema, AiGeneratedStructureNodeSchema, AiInsertQuestionSchema, AiQuestionSchema, } from "./parse" export type { AiGeneratedQuestion, AiGeneratedStructureNode, AiPreviewData, AiPreviewQuestion, AiRewriteQuestionData, QuestionContentResult, } from "./parse" // --------------------------------------------------------------------------- // Re-export public functions // --------------------------------------------------------------------------- export { regenerateAiQuestionByInstruction } from "./request" // --------------------------------------------------------------------------- // High-level orchestration // --------------------------------------------------------------------------- export async function generateAiPreviewData(input: { title: string subject?: string grade?: string difficulty: number totalScore: number durationMin: number questionCount?: number sourceText: string aiProviderId?: string }) { const sourceValidation = await validateExamSourceText({ sourceText: input.sourceText, aiProviderId: input.aiProviderId, }) if (!sourceValidation.ok) { return { ok: false as const, message: sourceValidation.message } } const structureDraft = await requestAiExamStructureDraft(input) if (!structureDraft.ok) return structureDraft const splitItems = splitStructureItems(structureDraft.data) const limitedItems = typeof input.questionCount === "number" && input.questionCount > 0 ? splitItems.slice(0, input.questionCount) : splitItems if (limitedItems.length === 0) { return { ok: false as const, message: "AI returned no questions" } } const detailedQuestions = await mapWithConcurrency(limitedItems, 6, (item) => parseQuestionDetail({ item, subject: input.subject, grade: input.grade, difficulty: input.difficulty, aiProviderId: input.aiProviderId, })) const hasSectionStructure = limitedItems.some((item: SplitQuestionItem) => item.sectionIndex !== null) const aiParsed: z.infer = hasSectionStructure ? { title: structureDraft.data.title ?? input.title, sections: (() => { const sectionMap = new Map[] }>() limitedItems.forEach((item, index) => { if (item.sectionIndex === null) return const existed = sectionMap.get(item.sectionIndex) const question = detailedQuestions[index] if (existed) { existed.questions.push(question) return } sectionMap.set(item.sectionIndex, { title: item.sectionTitle || `Section ${item.sectionIndex + 1}`, questions: [question], }) }) return Array.from(sectionMap.entries()) .sort((a, b) => a[0] - b[0]) .map(([, section]) => section) })(), questions: undefined, } : { title: structureDraft.data.title ?? input.title, questions: detailedQuestions, sections: undefined, } const payload = buildPreviewPayload(aiParsed, input) return { ok: true as const, data: payload, rawOutput: structureDraft.rawOutput, } } export async function generateAiCreateDraftFromSource(input: { title: string subject?: string grade?: string difficulty: number totalScore: number durationMin: number questionCount?: number sourceText: string aiProviderId?: string }) { const preview = await generateAiPreviewData(input) if (!preview.ok) { return preview } const draft = previewToDraft(preview.data) return { ok: true as const, generated: draft.generated, structure: draft.structure, rawOutput: preview.rawOutput, } } export async function generateAiExamDraft(input: { title?: string subject?: string grade?: string difficulty?: number totalScore?: number durationMin?: number questionCount?: number sourceText: string aiProviderId?: string }) { return requestAiExamDraft(input) }