feat: productionize learning engine and classroom
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
isCourseGenerationRunning,
|
||||
} from '@/lib/course-framework/runner';
|
||||
import { requireOpsAccess } from '@/lib/server/ops-access';
|
||||
import { proxyLargeCourseControlToEngine } from '@/lib/makelore-runtime/ops-engine-control';
|
||||
|
||||
const log = createLogger('Course Cancel API');
|
||||
|
||||
@@ -22,7 +23,7 @@ export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ courseId: string }> },
|
||||
) {
|
||||
const denied = requireOpsAccess(request);
|
||||
const denied = await requireOpsAccess(request);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
@@ -34,6 +35,12 @@ export async function POST(
|
||||
if (!record) {
|
||||
return apiError(API_ERROR_CODES.INVALID_REQUEST, 404, `Course not found: ${courseId}`);
|
||||
}
|
||||
const proxied = await proxyLargeCourseControlToEngine({
|
||||
request,
|
||||
course: record,
|
||||
action: 'cancel',
|
||||
});
|
||||
if (proxied) return proxied;
|
||||
if (!isCourseGenerationRunning(courseId)) {
|
||||
return apiError(API_ERROR_CODES.INVALID_REQUEST, 409, 'Course generation is not running');
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ 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('Framework Regenerate API');
|
||||
|
||||
@@ -28,7 +33,7 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ courseId: string }> },
|
||||
) {
|
||||
const denied = requireOpsAccess(req);
|
||||
const denied = await requireOpsAccess(req);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
@@ -40,6 +45,13 @@ export async function POST(
|
||||
if (!record) {
|
||||
return apiError(API_ERROR_CODES.INVALID_REQUEST, 404, `Course not found: ${courseId}`);
|
||||
}
|
||||
const proxied = await proxyLargeCourseControlToEngine({
|
||||
request: req,
|
||||
course: record,
|
||||
action: 'framework-regenerate',
|
||||
});
|
||||
if (proxied) return proxied;
|
||||
await assertLearningCourseMutable(courseId, record);
|
||||
if (
|
||||
isCourseGenerationRunning(courseId) ||
|
||||
isCoursePublishing(courseId) ||
|
||||
@@ -59,6 +71,9 @@ export async function POST(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ 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');
|
||||
|
||||
@@ -24,7 +29,7 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ courseId: string; index: string }> },
|
||||
) {
|
||||
const denied = requireOpsAccess(req);
|
||||
const denied = await requireOpsAccess(req);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
@@ -37,6 +42,14 @@ export async function POST(
|
||||
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,
|
||||
@@ -68,6 +81,9 @@ export async function POST(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ import {
|
||||
import { COURSEWARE_PUBLISH_TOKEN_ENV, isPublishTokenConfigured } from '@/lib/courseware-repo';
|
||||
import type { CourseManifestRecord } from '@/lib/course-manifest-repo/types';
|
||||
import type { CoursePublicationReceipt } from '@/lib/course-framework/types';
|
||||
import { requireOpsAccess } from '@/lib/server/ops-access';
|
||||
import {
|
||||
configuredWorksSquareApiOrigin,
|
||||
requireOpsAccess,
|
||||
} from '@/lib/server/ops-access';
|
||||
import { buildRequestOrigin } from '@/lib/server/classroom-storage';
|
||||
import { isCourseGenerationRunning } from '@/lib/course-framework/runner';
|
||||
import {
|
||||
@@ -49,6 +52,7 @@ import { publishCourseToServer } from '@/lib/server/course-publish-transport';
|
||||
import { commitRemoteCoursePublish } from '@/lib/server/course-publish-transaction';
|
||||
|
||||
const log = createLogger('Course Publish API');
|
||||
export const maxDuration = 900;
|
||||
|
||||
function buildPublicationReceipt(
|
||||
snapshot: CoursePublishSnapshot,
|
||||
@@ -112,7 +116,7 @@ export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ courseId: string }> },
|
||||
) {
|
||||
const denied = requireOpsAccess(request);
|
||||
const denied = await requireOpsAccess(request);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
@@ -120,6 +124,64 @@ export async function POST(
|
||||
if (!isValidCourseId(courseId)) {
|
||||
return apiError(API_ERROR_CODES.INVALID_REQUEST, 400, `Invalid course id: ${courseId}`);
|
||||
}
|
||||
const worksOrigin = configuredWorksSquareApiOrigin();
|
||||
if (worksOrigin) {
|
||||
const authorization = request.headers.get('authorization') ?? '';
|
||||
const finalizeResponse = await fetch(
|
||||
`${worksOrigin}/api/admin/learning/generations/${encodeURIComponent(courseId)}/finalize`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { Authorization: authorization, Accept: 'application/json' },
|
||||
redirect: 'error',
|
||||
cache: 'no-store',
|
||||
signal: AbortSignal.timeout(10 * 60_000),
|
||||
},
|
||||
);
|
||||
const finalized = (await finalizeResponse.json().catch(() => null)) as {
|
||||
id?: unknown;
|
||||
status?: unknown;
|
||||
} | null;
|
||||
if (!finalizeResponse.ok || typeof finalized?.id !== 'string') {
|
||||
return apiError(
|
||||
API_ERROR_CODES.UPSTREAM_ERROR,
|
||||
finalizeResponse.status >= 400 && finalizeResponse.status < 500
|
||||
? finalizeResponse.status
|
||||
: 502,
|
||||
'Works could not finalize the aggregate learning course',
|
||||
);
|
||||
}
|
||||
const publishResponse = await fetch(
|
||||
`${worksOrigin}/api/admin/learning/courses/${encodeURIComponent(finalized.id)}/publish`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { Authorization: authorization, Accept: 'application/json' },
|
||||
redirect: 'error',
|
||||
cache: 'no-store',
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
},
|
||||
);
|
||||
const published = (await publishResponse.json().catch(() => null)) as {
|
||||
id?: unknown;
|
||||
status?: unknown;
|
||||
} | null;
|
||||
if (!publishResponse.ok || typeof published?.id !== 'string') {
|
||||
return apiError(
|
||||
API_ERROR_CODES.UPSTREAM_ERROR,
|
||||
publishResponse.status >= 400 && publishResponse.status < 500
|
||||
? publishResponse.status
|
||||
: 502,
|
||||
'Works registered the course package but could not publish it',
|
||||
);
|
||||
}
|
||||
await updateCourseRecord(courseId, {
|
||||
externalPublication: {
|
||||
courseId: published.id,
|
||||
status: published.status === 'published' ? 'published' : 'ready',
|
||||
publishedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
return apiSuccess({ record: published, aggregate: true });
|
||||
}
|
||||
if (!isPublishTokenConfigured()) {
|
||||
return apiError(
|
||||
API_ERROR_CODES.INTERNAL_ERROR,
|
||||
|
||||
@@ -21,6 +21,11 @@ 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 Resume API');
|
||||
|
||||
@@ -28,7 +33,7 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ courseId: string }> },
|
||||
) {
|
||||
const denied = requireOpsAccess(req);
|
||||
const denied = await requireOpsAccess(req);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
@@ -40,6 +45,13 @@ export async function POST(
|
||||
if (!record) {
|
||||
return apiError(API_ERROR_CODES.INVALID_REQUEST, 404, `Course not found: ${courseId}`);
|
||||
}
|
||||
const proxied = await proxyLargeCourseControlToEngine({
|
||||
request: req,
|
||||
course: record,
|
||||
action: 'resume',
|
||||
});
|
||||
if (proxied) return proxied;
|
||||
await assertLearningCourseMutable(courseId, record);
|
||||
if (record.status === 'completed') {
|
||||
return apiError(API_ERROR_CODES.INVALID_REQUEST, 409, 'Course is already completed');
|
||||
}
|
||||
@@ -61,6 +73,9 @@ export async function POST(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ courseId: string }> },
|
||||
) {
|
||||
const denied = requireOpsAccess(request);
|
||||
const denied = await requireOpsAccess(request);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
@@ -46,6 +46,10 @@ export async function GET(
|
||||
for (const moduleRec of record.modules) {
|
||||
let published = false;
|
||||
let publishedCoursewareId: string | undefined;
|
||||
if (record.externalPublication?.status === 'published') {
|
||||
published = true;
|
||||
publishedCoursewareId = `${record.externalPublication.courseId}_m${moduleRec.index}`;
|
||||
}
|
||||
if (moduleRec.classroomId) {
|
||||
const coursewareId = courseModuleCoursewareId(courseId, moduleRec.index);
|
||||
const receipt = record.publication?.modules.find(
|
||||
@@ -57,7 +61,7 @@ export async function GET(
|
||||
const currentSourceRevisionHash = currentClassroom
|
||||
? computeClassroomSourceRevisionHash(currentClassroom)
|
||||
: undefined;
|
||||
published = Boolean(
|
||||
published = published || Boolean(
|
||||
receipt &&
|
||||
receipt.coursewareId === coursewareId &&
|
||||
receipt.sourceClassroomId === moduleRec.classroomId &&
|
||||
|
||||
@@ -21,6 +21,11 @@ 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');
|
||||
|
||||
@@ -28,7 +33,7 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ courseId: string }> },
|
||||
) {
|
||||
const denied = requireOpsAccess(req);
|
||||
const denied = await requireOpsAccess(req);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
@@ -40,6 +45,13 @@ export async function POST(
|
||||
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, '课程框架尚未生成,请等待框架生成完成');
|
||||
}
|
||||
@@ -70,6 +82,9 @@ export async function POST(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -16,12 +16,15 @@ import { createLogger } from '@/lib/logger';
|
||||
import type { CourseCreateInput } from '@/lib/course-framework/types';
|
||||
import { createCourse, runCourseFrameworkGeneration } from '@/lib/course-framework/runner';
|
||||
import { listCourseRecords, readCourseRecordReconciled } from '@/lib/course-framework/store';
|
||||
import { requireOpsAccess } from '@/lib/server/ops-access';
|
||||
import {
|
||||
configuredWorksSquareApiOrigin,
|
||||
requireOpsAccess,
|
||||
} from '@/lib/server/ops-access';
|
||||
|
||||
const log = createLogger('Courses API');
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const denied = requireOpsAccess(req);
|
||||
const denied = await requireOpsAccess(req);
|
||||
if (denied) return denied;
|
||||
|
||||
let requirementSnippet: string | undefined;
|
||||
@@ -39,6 +42,8 @@ export async function POST(req: NextRequest) {
|
||||
? { enableVideoGeneration: rawBody.enableVideoGeneration }
|
||||
: {}),
|
||||
...(rawBody.enableTTS != null ? { enableTTS: rawBody.enableTTS } : {}),
|
||||
...(rawBody.interactiveMode != null ? { interactiveMode: rawBody.interactiveMode } : {}),
|
||||
...(rawBody.taskEngineMode != null ? { taskEngineMode: rawBody.taskEngineMode } : {}),
|
||||
...(rawBody.pdfContent ? { pdfContent: rawBody.pdfContent } : {}),
|
||||
};
|
||||
const { requirement } = body;
|
||||
@@ -51,6 +56,70 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const worksOrigin = configuredWorksSquareApiOrigin();
|
||||
if (worksOrigin) {
|
||||
const upstream = await fetch(`${worksOrigin}/api/admin/learning/generations`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: req.headers.get('authorization') ?? '',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
requirement: body.requirement,
|
||||
enableWebSearch: body.enableWebSearch ?? false,
|
||||
enableImageGeneration: body.enableImageGeneration ?? false,
|
||||
enableVideoGeneration: body.enableVideoGeneration ?? false,
|
||||
enableTTS: body.enableTTS ?? true,
|
||||
interactiveMode: body.interactiveMode ?? true,
|
||||
taskEngineMode: body.taskEngineMode ?? false,
|
||||
}),
|
||||
redirect: 'error',
|
||||
cache: 'no-store',
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const result = (await upstream.json().catch(() => null)) as {
|
||||
jobId?: unknown;
|
||||
job_id?: unknown;
|
||||
status?: unknown;
|
||||
message?: unknown;
|
||||
} | null;
|
||||
if (!upstream.ok) {
|
||||
return apiError(
|
||||
API_ERROR_CODES.UPSTREAM_ERROR,
|
||||
upstream.status >= 400 && upstream.status < 500 ? upstream.status : 502,
|
||||
'Works rejected the large-course generation request',
|
||||
);
|
||||
}
|
||||
const remoteJobId =
|
||||
typeof result?.jobId === 'string'
|
||||
? result.jobId
|
||||
: typeof result?.job_id === 'string'
|
||||
? result.job_id
|
||||
: '';
|
||||
if (!remoteJobId) {
|
||||
return apiError(
|
||||
API_ERROR_CODES.UPSTREAM_ERROR,
|
||||
502,
|
||||
'Works returned an invalid generation response',
|
||||
);
|
||||
}
|
||||
return apiSuccess(
|
||||
{
|
||||
courseId: remoteJobId,
|
||||
jobId: remoteJobId,
|
||||
status: typeof result?.status === 'string' ? result.status : 'queued',
|
||||
message:
|
||||
typeof result?.message === 'string'
|
||||
? result.message
|
||||
: 'Course framework generation started',
|
||||
detailUrl: `${buildRequestOrigin(req)}/api/courses/${remoteJobId}`,
|
||||
pollIntervalMs: 3000,
|
||||
},
|
||||
202,
|
||||
);
|
||||
}
|
||||
|
||||
const courseId = nanoid(10);
|
||||
const baseUrl = buildRequestOrigin(req);
|
||||
await createCourse(courseId, body);
|
||||
@@ -85,7 +154,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const denied = requireOpsAccess(request);
|
||||
const denied = await requireOpsAccess(request);
|
||||
if (denied) return denied;
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user