import { AppError } from '@/lib/error-model'; import { createHostEventSource, ensureHostApiToken, getHostApiBase, hostApiFetch, } from '@/lib/host-api'; import { IMAGE_WORKSPACE_API_PATH, IMAGE_WORKSPACE_UNAVAILABLE_CODE, IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE, designAssetDownloadPath, type DesignAsset, type DesignAssetSaveResult, type DesignCommandFailureOutcome, type DesignCommandInput, type DesignCommandResult, type DesignDeleteWorkspaceResult, type DesignWorkspace, type DesignWorkspaceBootstrap, } from '../../shared/image-workspace'; type ImageWorkspaceEnvelope = { success?: boolean; status?: number; code?: string; error?: string; commandOutcome?: DesignCommandFailureOutcome; data?: T; }; export const IMAGE_WORKSPACE_CREATE_PROJECT_EVENT = 'niancode:image-workspace:create-project'; export class ImageWorkspaceApiError extends Error { readonly status: number; readonly code: string; readonly commandOutcome?: DesignCommandFailureOutcome; constructor( status: number, code: string, message: string, commandOutcome?: DesignCommandFailureOutcome, ) { super(message); this.name = 'ImageWorkspaceApiError'; this.status = status; this.code = code; this.commandOutcome = commandOutcome; } } function createClientId(prefix: string): string { const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${prefix}-${id}`; } export function createImageWorkspaceOperationId(): string { return createClientId('design-operation'); } function getErrorStatus(error: unknown): number { if (error instanceof AppError && typeof error.details?.status === 'number') { return error.details.status; } return 502; } function getErrorCode(error: unknown, status: number): string { if (error instanceof AppError && typeof error.details?.backendCode === 'string') { return error.details.backendCode; } if (status === 501) return IMAGE_WORKSPACE_UNAVAILABLE_CODE; return 'IMAGE_WORKSPACE_REQUEST_FAILED'; } function getCommandOutcome(error: unknown): DesignCommandFailureOutcome | undefined { const outcome = error instanceof AppError ? error.details?.commandOutcome : undefined; return outcome === 'definitive_failure' || outcome === 'unknown' ? outcome : undefined; } async function requestData(path: string, init: RequestInit = {}): Promise { let response: ImageWorkspaceEnvelope; try { response = await hostApiFetch>(path, init); } catch (error) { const status = getErrorStatus(error); throw new ImageWorkspaceApiError( status, getErrorCode(error, status), error instanceof Error ? error.message : IMAGE_WORKSPACE_UNAVAILABLE_MESSAGE, getCommandOutcome(error), ); } if (!response.success || response.data === undefined) { throw new ImageWorkspaceApiError( response.status ?? 502, response.code ?? 'IMAGE_WORKSPACE_REQUEST_FAILED', response.error ?? 'AI 设计请求失败', response.commandOutcome, ); } return response.data; } export function fetchImageWorkspace(): Promise { return requestData(IMAGE_WORKSPACE_API_PATH); } export function createImageWorkspaceProject( title: string, clientWorkspaceId = createClientId('workspace'), ): Promise { return requestData(`${IMAGE_WORKSPACE_API_PATH}/workspaces`, { method: 'POST', body: JSON.stringify({ clientWorkspaceId, title: title.trim() }), }); } export function renameImageWorkspaceProject( workspaceId: string, title: string, ): Promise { return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}`, { method: 'PATCH', body: JSON.stringify({ title: title.trim() }) }, ); } export function deleteImageWorkspaceProject( workspaceId: string, ): Promise { return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}`, { method: 'DELETE' }, ); } export function fetchImageWorkspaceProject(workspaceId: string): Promise { return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}`, ); } export function submitImageWorkspaceCommand( command: DesignCommandInput, ): Promise { const { workspaceId, ...body } = command; return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/commands`, { method: 'POST', body: JSON.stringify(body) }, ); } export async function openImageWorkspaceEvents( workspaceId: string, sessionId: string, afterEventId?: string, ): Promise { await ensureHostApiToken(); const query = new URLSearchParams({ sessionId }); if (afterEventId) query.set('afterEventId', afterEventId); return createHostEventSource( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/events?${query}`, ); } export async function resolveImageWorkspaceAssetUrl(contentPath: string): Promise { const token = await ensureHostApiToken(); const separator = contentPath.includes('?') ? '&' : '?'; return `${getHostApiBase()}${contentPath}${separator}token=${encodeURIComponent(token)}`; } const ASSET_FILE_EXTENSIONS: Record = { 'image/gif': 'gif', 'image/jpeg': 'jpg', 'image/png': 'png', 'image/svg+xml': 'svg', 'image/webp': 'webp', 'video/mp4': 'mp4', 'video/quicktime': 'mov', 'video/webm': 'webm', }; function assetDownloadFileName(asset: DesignAsset): string { const safeAssetId = asset.assetId .replace(/[^a-zA-Z0-9_-]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 32) || 'image'; const extension = ASSET_FILE_EXTENSIONS[asset.mimeType.toLowerCase()] ?? (asset.mediaType === 'video' ? 'mp4' : 'png'); return `Makelore-AI-Design-${safeAssetId}.${extension}`; } export function saveImageWorkspaceAsset(asset: DesignAsset): Promise { return requestData( designAssetDownloadPath(asset.workspaceId, asset.assetId), { method: 'POST', body: JSON.stringify({ defaultFileName: assetDownloadFileName(asset) }), }, ); } const DESIGN_IMAGE_UPLOAD_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']); const MAX_DESIGN_IMAGE_UPLOAD_BYTES = 10 * 1024 * 1024; async function fileToBase64(file: File): Promise { const bytes = new Uint8Array(await file.arrayBuffer()); let encoded = ''; for (let offset = 0; offset < bytes.length; offset += 48 * 1024) { const chunk = bytes.subarray(offset, offset + 48 * 1024); let binary = ''; for (let index = 0; index < chunk.length; index += 0x8000) { binary += String.fromCharCode(...chunk.subarray(index, index + 0x8000)); } encoded += btoa(binary); } return encoded; } export async function uploadImageWorkspaceAsset( workspaceId: string, file: File, ): Promise { if (!DESIGN_IMAGE_UPLOAD_MIME_TYPES.has(file.type)) { throw new ImageWorkspaceApiError( 400, 'design_asset_upload_type_invalid', '请选择 JPEG、PNG 或 WebP 图片', ); } if (file.size <= 0 || file.size > MAX_DESIGN_IMAGE_UPLOAD_BYTES) { throw new ImageWorkspaceApiError( 413, 'design_asset_upload_too_large', '图片大小需在 10 MB 以内', ); } return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/assets`, { method: 'POST', body: JSON.stringify({ fileName: file.name, mimeType: file.type, dataBase64: await fileToBase64(file), }), }, ); }