// Large-course mode API — confirm the framework and start module generation. // // POST /api/courses/:courseId/start // // The two-layer flow is deliberately gated: creating a course only generates // the framework (status `framework_ready`). This endpoint is the operator's // explicit confirmation after reviewing the framework — only then does the // Layer-2 single-courseware agent start generating each module one by one. 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, runCourseModuleGeneration, } 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('Course Start API'); export async function POST( req: NextRequest, { params }: { params: Promise<{ courseId: string }> }, ) { const denied = await requireOpsAccess(req); if (denied) return denied; try { const { courseId } = await params; if (!isValidCourseId(courseId)) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 400, `Invalid course id: ${courseId}`); } 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: 'start', }); if (proxied) return proxied; await assertLearningCourseMutable(courseId, record); if (!record.framework) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 409, '课程框架尚未生成,请等待框架生成完成'); } if (record.status !== 'framework_ready') { return apiError( API_ERROR_CODES.INVALID_REQUEST, 409, `当前状态(${record.status})不允许启动模块生成`, ); } if ( isCourseGenerationRunning(courseId) || isCoursePublishing(courseId) || record.modules.some((m) => m.status === 'generating') ) { return apiError(API_ERROR_CODES.INVALID_REQUEST, 409, '该课程已有生成任务正在运行'); } const baseUrl = buildRequestOrigin(req); const run = runCourseModuleGeneration(courseId, baseUrl); after(() => run); log.info(`Course ${courseId}: framework confirmed by operator — starting module generation`); return apiSuccess({ courseId, status: 'generating', message: '模块生成已开始', 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('Course start failed:', error); return apiError(API_ERROR_CODES.INTERNAL_ERROR, 500, 'Failed to start module generation'); } }