feat: integrate learning module
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-16 20:52:16 +08:00
parent 26b52d76e3
commit 01bee3188b
107 changed files with 6318 additions and 12063 deletions

213
src/lib/learning.ts Normal file
View File

@@ -0,0 +1,213 @@
import { AppError } from '@/lib/error-model';
import { invokeIpc } from '@/lib/api-client';
import { hostApiFetch } from '@/lib/host-api';
import {
LEARNING_API_PATH,
type LearningCourse,
type InstalledLearningCourse,
type LearningProgress,
type LearningProgressWrite,
type LearningGeneration,
type LearningGenerationMaterialUpload,
type LearningGenerationOptions,
type LearningGenerationUploadRequest,
type LearningClassroomPayload,
type LearningRuntimeBridgeEvent,
type LearningRuntimeBridgeRequest,
} from '../../shared/learning';
type Envelope<T> = {
success?: boolean;
status?: number;
code?: string;
error?: string;
data?: T;
};
export class LearningApiError extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
) {
super(message);
this.name = 'LearningApiError';
}
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
let envelope: Envelope<T>;
try {
envelope = await hostApiFetch<Envelope<T>>(path, init);
} catch (error) {
const status = error instanceof AppError && typeof error.details?.status === 'number'
? error.details.status
: 502;
throw new LearningApiError(status, 'LEARNING_REQUEST_FAILED', error instanceof Error ? error.message : '学习服务请求失败');
}
if (!envelope.success || envelope.data === undefined) {
throw new LearningApiError(
envelope.status ?? 502,
envelope.code ?? 'LEARNING_REQUEST_FAILED',
envelope.error ?? '学习服务请求失败',
);
}
return envelope.data;
}
export function fetchLearningCourses(): Promise<LearningCourse[]> {
return request(`${LEARNING_API_PATH}/courses`);
}
export function fetchMyLearningCourses(): Promise<LearningCourse[]> {
return request(`${LEARNING_API_PATH}/courses/mine`);
}
export function fetchLearningProgress(): Promise<LearningProgress[]> {
return request(`${LEARNING_API_PATH}/progress`);
}
export const DEFAULT_LEARNING_GENERATION_OPTIONS: Omit<LearningGenerationOptions, 'requirement'> = {
enableWebSearch: false,
enableImageGeneration: false,
enableVideoGeneration: false,
enableTTS: true,
interactiveMode: true,
taskEngineMode: false,
};
function normalizeGenerationOptions(
input: string | LearningGenerationOptions,
): LearningGenerationOptions {
return typeof input === 'string'
? { requirement: input, ...DEFAULT_LEARNING_GENERATION_OPTIONS }
: input;
}
export function startLearningGeneration(
input: string | LearningGenerationOptions,
materials: LearningGenerationMaterialUpload[] = [],
): Promise<LearningGeneration> {
const options = normalizeGenerationOptions(input);
if (materials.length > 0) {
const request: LearningGenerationUploadRequest = { options, materials };
return invokeIpc<LearningGeneration>('learning:startGeneration', request);
}
return request(`${LEARNING_API_PATH}/generations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(options),
});
}
export function fetchLearningGeneration(jobId: string): Promise<LearningGeneration> {
return request(`${LEARNING_API_PATH}/generations/${encodeURIComponent(jobId)}`);
}
export function cancelLearningGeneration(jobId: string): Promise<LearningGeneration> {
return request(`${LEARNING_API_PATH}/generations/${encodeURIComponent(jobId)}/cancel`, {
method: 'POST',
});
}
export function resumeLearningGeneration(jobId: string): Promise<LearningGeneration> {
return request(`${LEARNING_API_PATH}/generations/${encodeURIComponent(jobId)}/resume`, {
method: 'POST',
});
}
export function finalizeLearningGeneration(jobId: string): Promise<LearningCourse> {
return request(`${LEARNING_API_PATH}/generations/${encodeURIComponent(jobId)}/finalize`, {
method: 'POST',
});
}
export const LEARNING_LIBRARY_CHANGED_EVENT = 'makelore:learning-library-changed';
export function saveLearningProgress(
courseId: string,
progress: LearningProgressWrite,
): Promise<LearningProgress> {
const payload: LearningProgressWrite = {
contentHash: progress.contentHash,
...(progress.moduleId ? { moduleId: progress.moduleId } : {}),
sceneOrder: progress.sceneOrder,
actionIndex: progress.actionIndex,
positionMs: progress.positionMs,
completed: progress.completed,
};
return request(`${LEARNING_API_PATH}/courses/${encodeURIComponent(courseId)}/progress`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
export async function downloadLearningCourse(courseId: string): Promise<InstalledLearningCourse> {
const record = await invokeIpc<InstalledLearningCourse>('learning:download', courseId);
if (typeof window !== 'undefined') {
window.dispatchEvent(new Event(LEARNING_LIBRARY_CHANGED_EVENT));
}
return record;
}
export function listInstalledLearningCourses(): Promise<InstalledLearningCourse[]> {
return invokeIpc<InstalledLearningCourse[]>('learning:listInstalled');
}
export type { LearningClassroomPayload } from '../../shared/learning';
export function readLearningClassroom(courseId: string, moduleId?: string): Promise<LearningClassroomPayload> {
return invokeIpc<LearningClassroomPayload>('learning:readClassroom', courseId, moduleId);
}
export function getLearningPlayerUrl(): Promise<string> {
return invokeIpc<string>('learning:playerUrl');
}
export type LearningAgentRequest = {
courseId: string;
contentHash: string;
message: string;
history?: Array<{ role: 'user' | 'assistant'; content: string }>;
anchor?: {
sceneId?: string;
sceneOrder?: number;
sceneTitle?: string;
actionIndex?: number;
moduleId?: string | null;
moduleContentHash?: string;
};
};
export function askLearningAgent(request: LearningAgentRequest): Promise<{ text: string }> {
return invokeIpc<{ text: string }>('learning:agentAsk', request);
}
export function resetLearningAgent(courseId?: string, contentHash?: string): Promise<void> {
return invokeIpc<void>('learning:agentReset', courseId, contentHash);
}
export function transcribeLearningSpeech(request: {
audio: Uint8Array;
fileName: string;
mimeType: string;
language?: string;
}): Promise<{ text: string }> {
return invokeIpc<{ text: string }>('learning:transcribe', request);
}
export async function streamLearningRuntime(
request: LearningRuntimeBridgeRequest,
onEvent: (event: LearningRuntimeBridgeEvent) => void,
): Promise<void> {
const unsubscribe = window.electron.ipcRenderer.on('learning:runtime-event', (payload) => {
const event = payload as LearningRuntimeBridgeEvent;
if (event?.requestId === request.requestId) onEvent(event);
});
try {
await invokeIpc<void>('learning:runtimeRequest', request);
} finally {
if (typeof unsubscribe === 'function') unsubscribe();
}
}