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; 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), ); } }