Files
makelore/electron/image-workspace/works-square-workspace.ts
brother7 3d9dd14918 feat: 统一 AI 设计 Workspace 与生成任务链路
需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。

实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
2026-07-31 13:56:55 +08:00

377 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}`,
},
});
}
}