feat(learning): replace courses with project catalog

This commit is contained in:
2026-08-20 00:08:04 +08:00
parent 2cb8a7aef4
commit 38db1589e2
56 changed files with 1741 additions and 8725 deletions

View File

@@ -1,169 +1,47 @@
/** Stable boundary between Makelore, downloaded course packages and Works Square. */
/** Stable project-catalog boundary between Makelore and Works Square. */
export const LEARNING_API_PATH = '/api/works/learning';
export const LEARNING_GENERATION_CONTRACT_VERSION = 2;
export const LEARNING_MATERIAL_MAX_FILES = 5;
export const LEARNING_MATERIAL_MAX_FILE_BYTES = 50 * 1024 * 1024;
export const LEARNING_MATERIAL_MAX_TOTAL_BYTES = 150 * 1024 * 1024;
/** Main rejects larger course archives before downloading any bytes. */
/** Main rejects larger project archives before writing any bytes. */
export const LEARNING_ARCHIVE_MAX_BYTES = 512 * 1024 * 1024;
export const LEARNING_MEDIA_MAX_BYTES = 10 * 1024 * 1024;
export type LearningGenerationOptions = {
requirement: string;
enableWebSearch: boolean;
enableImageGeneration: boolean;
enableVideoGeneration: boolean;
enableTTS: boolean;
interactiveMode: boolean;
taskEngineMode: boolean;
export type LearningProjectImage = {
url: string;
alt: string;
width?: number;
height?: number;
};
export type LearningGenerationMaterialRef = {
export type LearningProjectSummary = {
id: string;
name: string;
mimeType: string;
size: number;
lastModified: number;
order: number;
};
/** IPC-only upload shape. File bytes are never persisted in Renderer state. */
export type LearningGenerationMaterialUpload = LearningGenerationMaterialRef & {
bytes: Uint8Array;
};
export type LearningGenerationUploadRequest = {
options: LearningGenerationOptions;
materials: LearningGenerationMaterialUpload[];
};
export type LearningRuntimeCapability =
| 'quiz-grade'
| 'pbl/v2/task/update'
| 'pbl/v2/open-task'
| 'pbl/v2/simulator'
| 'pbl/v2/instructor'
| 'pbl/v2/evaluate';
export type LearningRuntimeBridgeRequest = {
requestId: string;
courseId: string;
contentHash: string;
capability: LearningRuntimeCapability;
method: 'POST';
context: {
moduleId: string | null;
moduleContentHash: string;
anchor: {
sceneId: string;
sceneOrder?: number;
};
};
body: unknown;
};
export type LearningRuntimeBridgeEvent =
| { requestId: string; type: 'start'; status: number; contentType: string }
| { requestId: string; type: 'chunk'; chunk: Uint8Array }
| { requestId: string; type: 'end' }
| { requestId: string; type: 'error'; code: string; message: string };
export type LearningSceneKind = 'slide' | 'interactive' | 'quiz' | 'pbl';
export type LearningCourseStatus = 'generating' | 'ready' | 'published' | 'failed' | 'archived';
export type LearningSingleCourseCapabilities = {
modular?: false;
sceneKinds: LearningSceneKind[];
hasAudio: boolean;
hasWhiteboard: boolean;
hasAgent: boolean;
};
export type LearningLargeCourseCapabilities = {
modular: true;
orderedModules: true;
productionStagePerModule: true;
};
export type LearningCourseCapabilities = LearningSingleCourseCapabilities | LearningLargeCourseCapabilities;
export type LearningCourseModule = {
moduleId: string;
title: string;
summary: string | null;
sceneCount: number;
contentHash: string;
};
export type LearningCourse = {
id: string;
origin: 'user_single' | 'ops_large';
status: LearningCourseStatus;
title: string;
summary: string | null;
language: string | null;
contentHash: string;
archiveSha256: string;
summary: string;
cover: LearningProjectImage;
tags: string[];
version: string | null;
archiveBytes: number;
formatVersion: number;
minPlayerVersion: string;
sceneCount: number;
/** Ordered learning units inside one product-level course. */
modules?: LearningCourseModule[];
capabilities: LearningCourseCapabilities;
createdAt: string;
publishedAt: string | null;
};
export type LearningProgress = {
courseId: string;
/** Aggregate course hash used by Works ownership and entitlement checks. */
contentHash: string;
moduleId?: string | null;
moduleContentHash?: string | null;
sceneOrder: number;
actionIndex: number | null;
positionMs: number | null;
completed: boolean;
publishedAt: string;
updatedAt: string;
};
export type LearningProgressWrite = Pick<
LearningProgress,
'contentHash' | 'moduleId' | 'sceneOrder' | 'actionIndex' | 'positionMs' | 'completed'
>;
export type InstalledLearningCourse = {
schemaVersion: 1;
course: LearningCourse;
installedAt: string;
export type LearningProjectDetail = LearningProjectSummary & {
readmeMarkdown: string;
archiveFileName: string;
archiveSha256: string;
};
export type LearningClassroomPayload = {
courseId: string;
/** Aggregate package identity used to address immutable local assets. */
courseContentHash: string;
/** Aggregate hash kept as a compatibility alias for existing Stage bridges. */
contentHash: string;
moduleId: string | null;
/** Selected frozen module hash; equals courseContentHash for legacy single-classroom packages. */
moduleContentHash: string;
modules: LearningCourseModule[];
classroom: { stage: Record<string, unknown>; scenes: unknown[] };
export type LearningProjectPage = {
items: LearningProjectSummary[];
nextCursor: string | null;
total?: number;
};
export type LearningGeneration = {
contractVersion?: number;
jobId: string;
status: string;
mode?: 'single' | 'large';
step: string | null;
progress: number | null;
message: string | null;
scenesGenerated: number | null;
totalScenes: number | null;
courseId: string | null;
error: string | null;
done: boolean;
export type LearningProjectListQuery = {
cursor?: string;
limit?: number;
};
export type LearningProjectDownloadResult = {
status: 'saved' | 'cancelled';
};