import { WORKS_SQUARE_CONFIG } from '../api/works-config'; import { proxyAwareFetch } from '../utils/proxy-fetch'; import { getValidWorksSquareAccessToken } from './works-square-session'; import { LEARNING_MATERIAL_MAX_FILE_BYTES, LEARNING_MATERIAL_MAX_FILES, LEARNING_MATERIAL_MAX_TOTAL_BYTES, type LearningGeneration, type LearningGenerationMaterialUpload, type LearningGenerationOptions, type LearningGenerationUploadRequest, } from '../../shared/learning'; type Dependencies = { fetchImpl?: typeof fetch; getAccessToken?: typeof getValidWorksSquareAccessToken; apiBaseUrl?: string; }; function record(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } function errorMessage(payload: unknown, fallback: string): string { const root = record(payload); const detail = root.detail; if (typeof detail === 'string' && detail.trim()) return detail; const detailRecord = record(detail); if (typeof detailRecord.message === 'string' && detailRecord.message.trim()) { return detailRecord.message; } if (typeof root.error === 'string' && root.error.trim()) return root.error; return fallback; } function validateOptions(value: LearningGenerationOptions): LearningGenerationOptions { const requirement = typeof value?.requirement === 'string' ? value.requirement.trim() : ''; if (!requirement) throw new Error('请填写课程需求'); const booleanKeys = [ 'enableWebSearch', 'enableImageGeneration', 'enableVideoGeneration', 'enableTTS', 'interactiveMode', 'taskEngineMode', ] as const; for (const key of booleanKeys) { if (typeof value[key] !== 'boolean') throw new Error('课程生成选项无效'); } return { requirement, enableWebSearch: value.enableWebSearch, enableImageGeneration: value.enableImageGeneration, enableVideoGeneration: value.enableVideoGeneration, enableTTS: value.enableTTS, interactiveMode: value.interactiveMode, taskEngineMode: value.taskEngineMode, }; } function safeFileName(value: string): string { const normalized = value.replace(/[\\/\0\r\n]/g, '_').trim().slice(0, 255); return normalized || 'material'; } function materialBytes(material: LearningGenerationMaterialUpload): Uint8Array { return material.bytes instanceof Uint8Array ? new Uint8Array(material.bytes) : new Uint8Array(material.bytes as ArrayBuffer); } function validateMaterials(materials: LearningGenerationMaterialUpload[]): Array<{ material: LearningGenerationMaterialUpload; bytes: Uint8Array; }> { if (!Array.isArray(materials)) throw new Error('课程材料格式无效'); if (materials.length > LEARNING_MATERIAL_MAX_FILES) { throw new Error(`课程材料最多 ${LEARNING_MATERIAL_MAX_FILES} 个`); } let totalBytes = 0; const validated = materials .map((material) => ({ material, bytes: materialBytes(material) })) .sort((left, right) => left.material.order - right.material.order); for (const { material, bytes } of validated) { if (!bytes.byteLength || bytes.byteLength !== material.size) throw new Error('课程材料内容不完整'); if (bytes.byteLength > LEARNING_MATERIAL_MAX_FILE_BYTES) throw new Error('单个课程材料不能超过 50 MiB'); totalBytes += bytes.byteLength; } if (totalBytes > LEARNING_MATERIAL_MAX_TOTAL_BYTES) throw new Error('课程材料总计不能超过 150 MiB'); return validated; } export function createLearningGenerationClient(dependencies: Dependencies = {}) { const fetchImpl = dependencies.fetchImpl ?? proxyAwareFetch; const getAccessToken = dependencies.getAccessToken ?? getValidWorksSquareAccessToken; const apiBaseUrl = (dependencies.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, ''); async function start(input: LearningGenerationUploadRequest): Promise { const options = validateOptions(input.options); const materials = validateMaterials(input.materials); const token = await getAccessToken({ fetchImpl }); if (!token) throw new Error('请先登录'); const form = materials.length > 0 ? new FormData() : null; if (form) { form.set('options', JSON.stringify(options)); for (const { material, bytes } of materials) { const blobBytes = new Uint8Array(bytes.byteLength); blobBytes.set(bytes); form.append( 'materials', new Blob([blobBytes.buffer], { type: material.mimeType || 'application/octet-stream' }), safeFileName(material.name), ); } } const request = (accessToken: string) => fetchImpl(`${apiBaseUrl}/api/learning/generations`, { method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, ...(form ? {} : { 'Content-Type': 'application/json' }), }, body: form ?? JSON.stringify(options), redirect: 'manual', }); let response = await request(token); if (response.status === 401) { await response.body?.cancel().catch(() => undefined); const refreshed = await getAccessToken({ fetchImpl, forceRefresh: true }); if (refreshed) response = await request(refreshed); } const payload = await response.json().catch(() => null); if (!response.ok || !payload) { throw new Error(errorMessage(payload, `课程任务创建失败(HTTP ${response.status || 502})`)); } const generation = record(payload); if (typeof generation.jobId !== 'string' || !generation.jobId) { throw new Error('学习服务没有返回任务编号'); } return generation as LearningGeneration; } return { start }; }