94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
// 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');
|
|
}
|
|
}
|