feat: integrate learning module
This commit is contained in:
213
src/lib/learning.ts
Normal file
213
src/lib/learning.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user