130 lines
5.0 KiB
TypeScript
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),
|
|
);
|
|
}
|
|
}
|