Files
openmaic/OpenMAIC/app/api/courses/[courseId]/start/route.ts

95 lines
3.5 KiB
TypeScript

// 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');
}
}