import type { DesignAsset, DesignBrief, DesignCapabilities, DesignConfirmGenerationInput, DesignCreateWorkspaceInput, DesignGenerationQuote, DesignGenerationTask, DesignMessage, DesignRenameWorkspaceInput, DesignSubmitMessageInput, DesignWorkspace, DesignWorkspaceBootstrap, DesignWorkspaceSummary, } from '../../shared/image-workspace'; import { designAssetContentPath } from '../../shared/image-workspace'; import { WORKS_SQUARE_CONFIG } from '../api/works-config'; import { proxyAwareFetch } from '../utils/proxy-fetch'; import { getValidWorksSquareAccessToken } from '../services/works-square-session'; import { DesignWorkspaceModuleError, type DesignWorkspaceModule } from './module'; type ServerBrief = { version: number; status: DesignBrief['status']; medium: DesignBrief['medium']; summary: string; ready: boolean; missing_decision: string | null; }; type ServerQuote = { quote_id: string; status: DesignGenerationQuote['status']; medium: DesignGenerationQuote['medium']; brief_version: number; brief_summary: string; quoted_design_points: number; expires_at: string; }; type ServerMessage = { role: DesignMessage['role']; kind: DesignMessage['kind']; text: string; quick_replies: string[]; generation_quote: ServerQuote | null; turn_revision: number; created_at: string; }; type ServerWorkspaceSummary = { workspace_id: string; title: string; turn_revision: number; view_revision: number; phase: DesignWorkspaceSummary['phase']; brief: ServerBrief; updated_at: string; }; type ServerWorkspace = ServerWorkspaceSummary & { messages: ServerMessage[]; }; type ServerAsset = { asset_id: string; media_type: DesignAsset['mediaType']; mime_type: string; width: number; height: number; duration_milliseconds: number | null; created_at: string; }; type ServerTask = { task_id: string; workspace_id: string; medium: DesignGenerationTask['medium']; status: DesignGenerationTask['status']; brief_version: number; brief_summary: string; quote_id: string | null; quoted_design_points: number | null; failure_code: string | null; result_assets: ServerAsset[]; created_at: string; updated_at: string; }; type ServerErrorDetail = { code?: unknown; message?: unknown; }; type WorksSquareDesignWorkspaceOptions = { apiBaseUrl?: string; fetchImpl?: typeof fetch; }; function mapBrief(brief: ServerBrief): DesignBrief { return { version: brief.version, status: brief.status, medium: brief.medium, summary: brief.summary, ready: brief.ready, missingDecision: brief.missing_decision, }; } function mapQuote(quote: ServerQuote | null): DesignGenerationQuote | null { if (!quote) return null; return { quoteId: quote.quote_id, status: quote.status, medium: quote.medium, briefVersion: quote.brief_version, briefSummary: quote.brief_summary, quotedDesignPoints: quote.quoted_design_points, expiresAt: quote.expires_at, }; } function mapWorkspaceSummary(workspace: ServerWorkspaceSummary): DesignWorkspaceSummary { return { workspaceId: workspace.workspace_id, title: workspace.title, turnRevision: workspace.turn_revision, viewRevision: workspace.view_revision, phase: workspace.phase, brief: mapBrief(workspace.brief), updatedAt: workspace.updated_at, }; } function mapWorkspace(workspace: ServerWorkspace): DesignWorkspace { return { ...mapWorkspaceSummary(workspace), messages: workspace.messages.map((message, index) => ({ id: `${workspace.workspace_id}:${message.turn_revision}:${message.role}:${index}`, role: message.role, kind: message.kind, text: message.text, quickReplies: message.quick_replies, generationQuote: mapQuote(message.generation_quote), turnRevision: message.turn_revision, createdAt: message.created_at, })), }; } function mapTask(task: ServerTask): DesignGenerationTask { return { taskId: task.task_id, workspaceId: task.workspace_id, medium: task.medium, status: task.status, briefVersion: task.brief_version, briefSummary: task.brief_summary, quoteId: task.quote_id, quotedDesignPoints: task.quoted_design_points, failureCode: task.failure_code, resultAssets: task.result_assets.map((asset) => ({ assetId: asset.asset_id, workspaceId: task.workspace_id, mediaType: asset.media_type, mimeType: asset.mime_type, width: asset.width, height: asset.height, durationMilliseconds: asset.duration_milliseconds, createdAt: asset.created_at, contentPath: designAssetContentPath(task.workspace_id, asset.asset_id), })), createdAt: task.created_at, updatedAt: task.updated_at, }; } async function readPayload(response: Response): Promise { const text = await response.text(); if (!text.trim()) return null; try { return JSON.parse(text) as unknown; } catch { return text; } } function asErrorDetail(payload: unknown): ServerErrorDetail { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return {}; const detail = (payload as Record).detail; if (detail && typeof detail === 'object' && !Array.isArray(detail)) { return detail as ServerErrorDetail; } return payload as ServerErrorDetail; } function userFacingErrorMessage(code: string, fallback: string): string { const messages: Record = { workspace_not_found: '设计项目不存在或无权访问', workspace_revision_conflict: '设计项目已更新,请刷新后重试', idempotency_conflict: '这次操作与已经提交的请求冲突', reference_asset_invalid: '所选参考资产不可用', generation_quote_expired: '当前生成方案已过期,请让 Agent 重新确认', generation_quote_consumed: '当前生成方案已经确认过', generation_quote_invalid: '当前生成方案已失效,请让 Agent 重新确认', budget_denied: '当前设计点不足,无法开始生成', policy_blocked: '当前内容不符合创作安全规则', design_reasoner_unavailable: '设计 Agent 暂时不可用,请稍后重试', design_runtime_unavailable: 'AI 设计服务暂时不可用', design_production_unavailable: '当前生成能力暂时不可用', }; return messages[code] ?? fallback; } export class WorksSquareDesignWorkspace implements DesignWorkspaceModule { private readonly apiBaseUrl: string; private readonly fetchImpl: typeof fetch; constructor(options: WorksSquareDesignWorkspaceOptions = {}) { this.apiBaseUrl = (options.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, ''); this.fetchImpl = options.fetchImpl ?? proxyAwareFetch; } async bootstrap(): Promise { const [capabilities, workspaces] = await Promise.all([ this.getCapabilities(), this.requestJson('/api/design/workspaces?limit=100&offset=0'), ]); return { capabilities, workspaces: workspaces.map(mapWorkspaceSummary), }; } getCapabilities(): Promise { return this.requestJson('/api/design/capabilities'); } async createWorkspace(input: DesignCreateWorkspaceInput): Promise { const workspace = await this.requestJson('/api/design/workspaces', { method: 'POST', body: JSON.stringify({ client_workspace_id: input.clientWorkspaceId, title: input.title, }), }); return mapWorkspace(workspace); } async renameWorkspace(input: DesignRenameWorkspaceInput): Promise { const workspace = await this.requestJson( `/api/design/workspaces/${encodeURIComponent(input.workspaceId)}`, { method: 'PATCH', body: JSON.stringify({ title: input.title }), }, ); return mapWorkspace(workspace); } async getWorkspace(workspaceId: string): Promise { const workspace = await this.requestJson( `/api/design/workspaces/${encodeURIComponent(workspaceId)}`, ); return mapWorkspace(workspace); } async submitMessage(input: DesignSubmitMessageInput): Promise { const workspace = await this.requestJson( `/api/design/workspaces/${encodeURIComponent(input.workspaceId)}/turns`, { method: 'POST', body: JSON.stringify({ client_turn_id: input.clientTurnId, expected_turn_revision: input.expectedTurnRevision, message: input.message, attachment_asset_ids: input.attachmentAssetIds ?? [], action: null, }), }, ); return mapWorkspace(workspace); } async confirmGeneration(input: DesignConfirmGenerationInput): Promise { const workspace = await this.requestJson( `/api/design/workspaces/${encodeURIComponent(input.workspaceId)}/turns`, { method: 'POST', body: JSON.stringify({ client_turn_id: input.clientTurnId, expected_turn_revision: input.expectedTurnRevision, message: '确认生成', attachment_asset_ids: [], action: { type: 'confirm_generation', quote_id: input.quoteId, }, }), }, ); return mapWorkspace(workspace); } async listTasks(workspaceId: string): Promise { const tasks = await this.requestJson( `/api/design/workspaces/${encodeURIComponent(workspaceId)}/generation-tasks?limit=100&offset=0`, ); return tasks.map(mapTask); } openAssetContent(workspaceId: string, assetId: string, range?: string): Promise { return this.authorizedFetch( `/api/design/workspaces/${encodeURIComponent(workspaceId)}/assets/${encodeURIComponent(assetId)}/content`, range ? { headers: { Range: range } } : {}, ); } private async requestJson(path: string, init: RequestInit = {}): Promise { const response = await this.authorizedFetch(path, { ...init, headers: { Accept: 'application/json', ...(init.body ? { 'Content-Type': 'application/json' } : {}), ...(init.headers ?? {}), }, }); const payload = await readPayload(response); if (!response.ok) { const detail = asErrorDetail(payload); const code = typeof detail.code === 'string' ? detail.code : 'DESIGN_WORKSPACE_REQUEST_FAILED'; const fallback = typeof detail.message === 'string' ? detail.message : `AI 设计请求失败(${response.status})`; throw new DesignWorkspaceModuleError( response.status, code, userFacingErrorMessage(code, fallback), ); } return payload as T; } private async authorizedFetch(path: string, init: RequestInit = {}): Promise { let token = await getValidWorksSquareAccessToken({ fetchImpl: this.fetchImpl }); if (!token) { throw new DesignWorkspaceModuleError(401, 'AUTH_REQUIRED', '请先登录后再使用 AI 设计'); } let response = await this.fetchWithToken(path, token, init); if (response.status !== 401) return response; token = await getValidWorksSquareAccessToken({ fetchImpl: this.fetchImpl, forceRefresh: true, }); if (!token) { throw new DesignWorkspaceModuleError(401, 'AUTH_EXPIRED', '登录状态已失效,请重新登录'); } response = await this.fetchWithToken(path, token, init); return response; } private fetchWithToken(path: string, token: string, init: RequestInit): Promise { return this.fetchImpl(`${this.apiBaseUrl}${path}`, { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}`, }, }); } }