feat: 统一 AI 设计 Workspace 与生成任务链路

需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。

实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
This commit is contained in:
2026-07-31 13:56:55 +08:00
parent 80e8386fa6
commit 3d9dd14918
22 changed files with 2506 additions and 1707 deletions

View File

@@ -0,0 +1,376 @@
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<unknown> {
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<string, unknown>).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<string, string> = {
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<DesignWorkspaceBootstrap> {
const [capabilities, workspaces] = await Promise.all([
this.getCapabilities(),
this.requestJson<ServerWorkspaceSummary[]>('/api/design/workspaces?limit=100&offset=0'),
]);
return {
capabilities,
workspaces: workspaces.map(mapWorkspaceSummary),
};
}
getCapabilities(): Promise<DesignCapabilities> {
return this.requestJson<DesignCapabilities>('/api/design/capabilities');
}
async createWorkspace(input: DesignCreateWorkspaceInput): Promise<DesignWorkspace> {
const workspace = await this.requestJson<ServerWorkspace>('/api/design/workspaces', {
method: 'POST',
body: JSON.stringify({
client_workspace_id: input.clientWorkspaceId,
title: input.title,
}),
});
return mapWorkspace(workspace);
}
async renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace> {
const workspace = await this.requestJson<ServerWorkspace>(
`/api/design/workspaces/${encodeURIComponent(input.workspaceId)}`,
{
method: 'PATCH',
body: JSON.stringify({ title: input.title }),
},
);
return mapWorkspace(workspace);
}
async getWorkspace(workspaceId: string): Promise<DesignWorkspace> {
const workspace = await this.requestJson<ServerWorkspace>(
`/api/design/workspaces/${encodeURIComponent(workspaceId)}`,
);
return mapWorkspace(workspace);
}
async submitMessage(input: DesignSubmitMessageInput): Promise<DesignWorkspace> {
const workspace = await this.requestJson<ServerWorkspace>(
`/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<DesignWorkspace> {
const workspace = await this.requestJson<ServerWorkspace>(
`/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<DesignGenerationTask[]> {
const tasks = await this.requestJson<ServerTask[]>(
`/api/design/workspaces/${encodeURIComponent(workspaceId)}/generation-tasks?limit=100&offset=0`,
);
return tasks.map(mapTask);
}
openAssetContent(workspaceId: string, assetId: string, range?: string): Promise<Response> {
return this.authorizedFetch(
`/api/design/workspaces/${encodeURIComponent(workspaceId)}/assets/${encodeURIComponent(assetId)}/content`,
range ? { headers: { Range: range } } : {},
);
}
private async requestJson<T>(path: string, init: RequestInit = {}): Promise<T> {
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<Response> {
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<Response> {
return this.fetchImpl(`${this.apiBaseUrl}${path}`, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${token}`,
},
});
}
}