Files
openmaic/OpenMAIC/app/api/generate-classroom/route.ts

130 lines
5.0 KiB
TypeScript

import { after, type NextRequest } from 'next/server';
import { nanoid } from 'nanoid';
import { apiError, apiSuccess } from '@/lib/server/api-response';
import { type GenerateClassroomInput } from '@/lib/server/classroom-generation';
import { runClassroomGenerationJob } from '@/lib/server/classroom-job-runner';
import { createClassroomGenerationJob } from '@/lib/server/classroom-job-store';
import { buildRequestOrigin } from '@/lib/server/classroom-storage';
import { createLogger } from '@/lib/logger';
import {
authorizeClassroomJobCreation,
authorizeClassroomJobList,
classroomJobAccessError,
} from '@/lib/server/authz/classroom-job-access';
import { resolveIdentityEnforcementMode } from '@/lib/server/authz/identity-enforcement';
const log = createLogger('GenerateClassroom API');
export const maxDuration = 30;
export async function POST(req: NextRequest) {
let requirementSnippet: string | undefined;
try {
const rawBody = (await req.json()) as Partial<GenerateClassroomInput>;
requirementSnippet = rawBody.requirement?.substring(0, 60);
const body: GenerateClassroomInput = {
requirement: rawBody.requirement || '',
...(rawBody.pdfContent ? { pdfContent: rawBody.pdfContent } : {}),
...(rawBody.enableWebSearch != null ? { enableWebSearch: rawBody.enableWebSearch } : {}),
...(rawBody.webSearchProviderId ? { webSearchProviderId: rawBody.webSearchProviderId } : {}),
...(rawBody.webSearchApiKey ? { webSearchApiKey: rawBody.webSearchApiKey } : {}),
...(rawBody.webSearchModelId ? { webSearchModelId: rawBody.webSearchModelId } : {}),
...(rawBody.baiduSubSources ? { baiduSubSources: rawBody.baiduSubSources } : {}),
...(rawBody.enableImageGeneration != null
? { enableImageGeneration: rawBody.enableImageGeneration }
: {}),
...(rawBody.enableVideoGeneration != null
? { enableVideoGeneration: rawBody.enableVideoGeneration }
: {}),
...(rawBody.enableTTS != null ? { enableTTS: rawBody.enableTTS } : {}),
...(rawBody.interactiveMode != null ? { interactiveMode: rawBody.interactiveMode } : {}),
...(rawBody.taskEngineMode != null ? { taskEngineMode: rawBody.taskEngineMode } : {}),
};
const { requirement } = body;
if (!requirement) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'Missing required field: requirement');
}
const creationAccess = await authorizeClassroomJobCreation(req);
if (!creationAccess.allowed) return classroomJobAccessError(creationAccess);
const baseUrl = buildRequestOrigin(req);
const jobId = nanoid(10);
const job = await createClassroomGenerationJob(jobId, body, creationAccess.ownership);
const pollUrl = `${baseUrl}/api/generate-classroom/${jobId}`;
after(() => runClassroomGenerationJob(jobId, body, baseUrl));
return apiSuccess(
{
jobId,
status: job.status,
step: job.step,
message: job.message,
pollUrl,
pollIntervalMs: 5000,
},
202,
);
} catch (error) {
log.error(
`Classroom generation job creation failed [requirement="${requirementSnippet ?? 'unknown'}..."]:`,
error,
);
return apiError(
'INTERNAL_ERROR',
500,
'Failed to create classroom generation job',
error instanceof Error ? error.message : 'Unknown error',
);
}
}
/**
* GET /api/generate-classroom?limit=N — list recent background generation
* jobs (newest first) for the ops task panel.
*/
export async function GET(request: NextRequest) {
try {
const limitParam = Number(new URL(request.url).searchParams.get('limit') ?? 20);
const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(limitParam, 50) : 20;
const { listClassroomGenerationJobs } = await import('@/lib/server/classroom-job-store');
const enforcementMode = resolveIdentityEnforcementMode();
const jobs = await listClassroomGenerationJobs(
enforcementMode === 'required' ? Number.MAX_SAFE_INTEGER : limit,
);
const listAccess = await authorizeClassroomJobList(request, jobs, {
mode: enforcementMode,
});
if (!listAccess.allowed) return classroomJobAccessError(listAccess);
return apiSuccess({
items: listAccess.jobs.slice(0, limit).map((job) => ({
jobId: job.id,
status: job.status,
step: job.step,
progress: job.progress,
message: job.message,
createdAt: job.createdAt,
updatedAt: job.updatedAt,
completedAt: job.completedAt,
requirement: job.inputSummary.requirementPreview,
scenesGenerated: job.scenesGenerated,
totalScenes: job.totalScenes,
result: job.result,
error: job.error,
done: job.status === 'succeeded' || job.status === 'failed' || job.status === 'cancelled',
})),
});
} catch (error) {
log.error('Classroom job listing failed:', error);
return apiError(
'INTERNAL_ERROR',
500,
'Failed to list classroom generation jobs',
error instanceof Error ? error.message : String(error),
);
}
}