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 DesignGenerationTask, type DesignWorkspace, type DesignWorkspaceBootstrap, } from '../../shared/image-workspace'; type ImageWorkspaceEnvelope = { success?: boolean; status?: number; code?: string; error?: string; 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; constructor(status: number, code: string, message: string) { super(message); this.name = 'ImageWorkspaceApiError'; this.status = status; this.code = code; } } function createClientId(prefix: string): string { const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${prefix}-${id}`; } export function createImageWorkspaceTurnId(): string { return createClientId('turn'); } 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'; } 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, ); } if (!response.success || response.data === undefined) { throw new ImageWorkspaceApiError( response.status ?? 502, response.code ?? 'IMAGE_WORKSPACE_REQUEST_FAILED', response.error ?? 'AI 设计请求失败', ); } 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 fetchImageWorkspaceProject(workspaceId: string): Promise { return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}`, ); } export function sendImageWorkspaceMessage( workspaceId: string, expectedTurnRevision: number, message: string, clientTurnId = createImageWorkspaceTurnId(), attachmentAssetIds: string[] = [], ): Promise { return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/messages`, { method: 'POST', body: JSON.stringify({ clientTurnId, expectedTurnRevision, message: message.trim(), attachmentAssetIds, }), }, ); } export function confirmImageWorkspaceGeneration( workspaceId: string, expectedTurnRevision: number, quoteId: string, clientTurnId = createImageWorkspaceTurnId(), ): Promise { return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/quotes/${encodeURIComponent(quoteId)}/confirm`, { method: 'POST', body: JSON.stringify({ clientTurnId, expectedTurnRevision, }), }, ); } export function fetchImageWorkspaceTasks( workspaceId: string, ): Promise { return requestData( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/tasks`, ); } export async function openImageWorkspaceTaskEvents(workspaceId: string): Promise { await ensureHostApiToken(); return createHostEventSource( `${IMAGE_WORKSPACE_API_PATH}/workspaces/${encodeURIComponent(workspaceId)}/events`, ); } export async function resolveImageWorkspaceAssetUrl(contentPath: string): Promise { const token = await ensureHostApiToken(); const separator = contentPath.includes('?') ? '&' : '?'; return `${getHostApiBase()}${contentPath}${separator}token=${encodeURIComponent(token)}`; } const IMAGE_FILE_EXTENSIONS: Record = { 'image/gif': 'gif', 'image/jpeg': 'jpg', 'image/png': 'png', 'image/svg+xml': 'svg', 'image/webp': 'webp', }; function assetDownloadFileName(asset: DesignAsset): string { const safeAssetId = asset.assetId .replace(/[^a-zA-Z0-9_-]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 32) || 'image'; const extension = IMAGE_FILE_EXTENSIONS[asset.mimeType.toLowerCase()] ?? '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), }), }, ); }