feat: 统一 AI 设计 Workspace 与生成任务链路
需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。 实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
This commit is contained in:
376
electron/image-workspace/works-square-workspace.ts
Normal file
376
electron/image-workspace/works-square-workspace.ts
Normal 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}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user