// Large-course mode API — regenerate a module and its dependent tail. // // POST /api/courses/:courseId/modules/:index/regenerate // // Re-runs the given module through the single-courseware agent, invalidating // and rebuilding every later module because their continuity input depends on // the target's actual output. Refused while any course generation is active. import { after, type NextRequest } from 'next/server'; import { apiError, apiSuccess, API_ERROR_CODES } from '@/lib/server/api-response'; import { buildRequestOrigin } from '@/lib/server/classroom-storage'; import { createLogger } from '@/lib/logger'; import { isValidCourseId, readCourseRecordReconciled } from '@/lib/course-framework/store'; import { isCourseGenerationRunning, runCourseGenerationJob } from '@/lib/course-framework/runner'; import { requireOpsAccess } from '@/lib/server/ops-access'; import { CourseMutationInProgressError, isCoursePublishing, } from '@/lib/course-framework/publish-state'; import { proxyLargeCourseControlToEngine } from '@/lib/makelore-runtime/ops-engine-control'; import { assertLearningCourseMutable, FrozenLearningCourseMutationError, } from '@/lib/makelore-course/immutability'; const log = createLogger('Module Regenerate API'); export async function POST( req: NextRequest, { params }: { params: Promise<{ courseId: string; index: string }> }, ) { const denied = await requireOpsAccess(req); if (denied) return denied; try { const { courseId, index: indexParam } = await params; const index = Number(indexParam); if (!isValidCourseId(courseId) || !Number.isInteger(index) || index < 1) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 400, 'Invalid course id or module index'); } const record = await readCourseRecordReconciled(courseId); if (!record) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 404, `Course not found: ${courseId}`); } const proxied = await proxyLargeCourseControlToEngine({ request: req, course: record, action: 'module-regenerate', moduleIndex: index, }); if (proxied) return proxied; await assertLearningCourseMutable(courseId, record); if (!record.framework) { return apiError( API_ERROR_CODES.INVALID_REQUEST, 409, 'Course framework not generated yet; cannot regenerate a module', ); } const moduleRec = record.modules.find((m) => m.index === index); if (!moduleRec) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 404, `Module ${index} not found`); } if ( isCourseGenerationRunning(courseId) || isCoursePublishing(courseId) || record.modules.some((module) => module.status === 'generating') ) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 409, '该课程已有生成任务正在运行'); } const baseUrl = buildRequestOrigin(req); const run = runCourseGenerationJob(courseId, baseUrl, { onlyModuleIndex: index }); after(() => run); return apiSuccess({ courseId, moduleIndex: index, status: 'generating', message: `Module ${index} and downstream regeneration started`, detailUrl: `${baseUrl}/api/courses/${courseId}`, }); } catch (error) { if (error instanceof FrozenLearningCourseMutationError) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 409, error.message); } if (error instanceof CourseMutationInProgressError) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 409, error.message); } log.error('Module regenerate failed:', error); return apiError(API_ERROR_CODES.INTERNAL_ERROR, 500, 'Failed to regenerate module'); } }