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

@@ -1,133 +1,119 @@
import { create } from 'zustand';
import {
addImageWorkspaceAgent,
confirmImageWorkspaceGeneration,
createImageWorkspaceProject,
fetchImageWorkspace,
fetchImageWorkspaceProject,
fetchImageWorkspaceTasks,
ImageWorkspaceApiError,
renameImageWorkspaceProject,
sendImageWorkspaceMessage,
uploadImageWorkspaceReference,
} from '@/lib/image-workspace';
import { useAuthStore } from '@/stores/auth';
import {
IMAGE_WORKSPACE_UNAVAILABLE_CODE,
type ImageWorkspaceProject,
type ImageWorkspaceReferenceUploadInput,
type ImageWorkspaceReferenceUploadResult,
type ImageWorkspaceSendMessageInput,
type ImageWorkspaceSnapshot,
type DesignGenerationTask,
type DesignWorkspace,
type DesignWorkspaceBootstrap,
type DesignWorkspaceSummary,
} from '../../shared/image-workspace';
export type ImageWorkspaceLoadStatus = 'idle' | 'loading' | 'ready' | 'unavailable' | 'error';
type ImageWorkspaceState = {
status: ImageWorkspaceLoadStatus;
snapshot: ImageWorkspaceSnapshot | null;
activeProjectId: string | null;
activeAgentIds: Record<string, string>;
bootstrap: DesignWorkspaceBootstrap | null;
activeWorkspaceId: string | null;
workspace: DesignWorkspace | null;
tasks: DesignGenerationTask[];
error: string | null;
load: () => Promise<ImageWorkspaceSnapshot | null>;
createProject: (name: string) => Promise<ImageWorkspaceSnapshot>;
addAgent: (projectId: string) => Promise<ImageWorkspaceSnapshot>;
sendMessage: (input: ImageWorkspaceSendMessageInput) => Promise<ImageWorkspaceSnapshot>;
uploadReference: (input: ImageWorkspaceReferenceUploadInput) => Promise<ImageWorkspaceReferenceUploadResult>;
selectProject: (projectId: string) => void;
selectAgent: (projectId: string, agentId: string) => void;
load: () => Promise<DesignWorkspaceBootstrap | null>;
createProject: (title: string) => Promise<DesignWorkspace>;
renameProject: (workspaceId: string, title: string) => Promise<DesignWorkspace>;
selectProject: (workspaceId: string) => Promise<void>;
refreshWorkspace: () => Promise<DesignWorkspace | null>;
refreshTasks: () => Promise<DesignGenerationTask[]>;
sendMessage: (message: string) => Promise<DesignWorkspace>;
confirmGeneration: (quoteId: string) => Promise<DesignWorkspace>;
reset: () => void;
};
let inFlightLoad: Promise<ImageWorkspaceSnapshot | null> | null = null;
function findProject(snapshot: ImageWorkspaceSnapshot, projectId: string | null): ImageWorkspaceProject | undefined {
return snapshot.projects.find((project) => project.id === projectId);
}
function resolveProjectId(snapshot: ImageWorkspaceSnapshot, currentProjectId: string | null): string | null {
if (findProject(snapshot, currentProjectId)) return currentProjectId;
if (findProject(snapshot, snapshot.activeProjectId ?? null)) return snapshot.activeProjectId ?? null;
return snapshot.projects[0]?.id ?? null;
}
function resolveAgentIds(
snapshot: ImageWorkspaceSnapshot,
currentAgentIds: Record<string, string>,
): Record<string, string> {
return Object.fromEntries(snapshot.projects.flatMap((project) => {
const currentAgentId = currentAgentIds[project.id];
let selectedAgentId: string | undefined;
if (currentAgentId && project.agents.some((agent) => agent.id === currentAgentId)) {
selectedAgentId = currentAgentId;
} else if (project.activeAgentId
&& project.agents.some((agent) => agent.id === project.activeAgentId)) {
selectedAgentId = project.activeAgentId;
} else {
selectedAgentId = project.agents[0]?.id;
}
return selectedAgentId ? [[project.id, selectedAgentId]] : [];
}));
}
let inFlightLoad: Promise<DesignWorkspaceBootstrap | null> | null = null;
function unavailable(error: unknown): boolean {
return error instanceof ImageWorkspaceApiError
&& (error.status === 501 || error.code === IMAGE_WORKSPACE_UNAVAILABLE_CODE);
}
async function getOptionalAccessToken(): Promise<string | null> {
return await useAuthStore.getState().getValidAccessToken();
function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
async function getRequiredAccessToken(): Promise<string> {
const accessToken = await useAuthStore.getState().getValidAccessToken();
if (!accessToken) throw new Error('请先登录后再使用创作空间');
return accessToken;
function toSummary(workspace: DesignWorkspace): DesignWorkspaceSummary {
const { messages: _messages, ...summary } = workspace;
return summary;
}
function usesAnonymousDevelopmentAdapter(): boolean {
return typeof window !== 'undefined'
&& window.electron?.imageWorkspaceLocalDevelopment === true;
function upsertSummary(
bootstrap: DesignWorkspaceBootstrap | null,
workspace: DesignWorkspace,
): DesignWorkspaceBootstrap | null {
if (!bootstrap) return null;
const summary = toSummary(workspace);
const workspaces = bootstrap.workspaces.filter(
(item) => item.workspaceId !== workspace.workspaceId,
);
return {
...bootstrap,
workspaces: [summary, ...workspaces],
};
}
export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) => {
const applySnapshot = (snapshot: ImageWorkspaceSnapshot) => {
const applyWorkspace = (workspace: DesignWorkspace): DesignWorkspace => {
set((state) => ({
status: 'ready',
snapshot,
activeProjectId: resolveProjectId(snapshot, state.activeProjectId),
activeAgentIds: resolveAgentIds(snapshot, state.activeAgentIds),
bootstrap: upsertSummary(state.bootstrap, workspace),
activeWorkspaceId: workspace.workspaceId,
workspace,
error: null,
}));
return snapshot;
return workspace;
};
const failLoad = (error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
set({
status: unavailable(error) ? 'unavailable' : 'error',
snapshot: null,
activeProjectId: null,
activeAgentIds: {},
error: message,
});
const loadWorkspace = async (workspaceId: string): Promise<void> => {
const [workspace, tasks] = await Promise.all([
fetchImageWorkspaceProject(workspaceId),
fetchImageWorkspaceTasks(workspaceId),
]);
set((state) => ({
status: 'ready',
bootstrap: upsertSummary(state.bootstrap, workspace),
activeWorkspaceId: workspaceId,
workspace,
tasks,
error: null,
}));
};
const mutate = async (
request: (accessToken: string | null) => Promise<ImageWorkspaceSnapshot>,
): Promise<ImageWorkspaceSnapshot> => {
try {
const accessToken = usesAnonymousDevelopmentAdapter()
? null
: await getRequiredAccessToken();
return applySnapshot(await request(accessToken));
} catch (error) {
set({ error: error instanceof Error ? error.message : String(error) });
throw error;
const recoverRevisionConflict = async (error: unknown): Promise<never> => {
if (error instanceof ImageWorkspaceApiError
&& error.code === 'workspace_revision_conflict') {
await get().refreshWorkspace().catch(() => null);
throw new ImageWorkspaceApiError(
409,
error.code,
'设计项目已更新,内容已刷新,请重新提交',
);
}
throw error;
};
return {
status: 'idle',
snapshot: null,
activeProjectId: null,
activeAgentIds: {},
bootstrap: null,
activeWorkspaceId: null,
workspace: null,
tasks: [],
error: null,
load: () => {
@@ -135,9 +121,32 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
set({ status: 'loading', error: null });
inFlightLoad = (async () => {
try {
return applySnapshot(await fetchImageWorkspace(await getOptionalAccessToken()));
const bootstrap = await fetchImageWorkspace();
const currentId = get().activeWorkspaceId;
const activeWorkspaceId = bootstrap.workspaces.some(
(workspace) => workspace.workspaceId === currentId,
)
? currentId
: bootstrap.workspaces[0]?.workspaceId ?? null;
set({
status: 'ready',
bootstrap,
activeWorkspaceId,
workspace: null,
tasks: [],
error: null,
});
if (activeWorkspaceId) await loadWorkspace(activeWorkspaceId);
return bootstrap;
} catch (error) {
failLoad(error);
set({
status: unavailable(error) ? 'unavailable' : 'error',
bootstrap: null,
activeWorkspaceId: null,
workspace: null,
tasks: [],
error: messageOf(error),
});
return null;
} finally {
inFlightLoad = null;
@@ -146,41 +155,108 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
return inFlightLoad;
},
createProject: (name) => mutate((accessToken) => createImageWorkspaceProject(accessToken, name)),
addAgent: (projectId) => mutate((accessToken) => addImageWorkspaceAgent(accessToken, projectId)),
sendMessage: (input) => mutate((accessToken) => sendImageWorkspaceMessage(accessToken, input)),
uploadReference: async (input) => {
createProject: async (title) => {
try {
const accessToken = usesAnonymousDevelopmentAdapter()
? null
: await getRequiredAccessToken();
const result = await uploadImageWorkspaceReference(accessToken, input);
applySnapshot(result.workspace);
return result;
const workspace = applyWorkspace(await createImageWorkspaceProject(title));
set({ tasks: [] });
return workspace;
} catch (error) {
set({ error: error instanceof Error ? error.message : String(error) });
set({ error: messageOf(error) });
throw error;
}
},
selectProject: (projectId) => {
if (!get().snapshot?.projects.some((project) => project.id === projectId)) return;
set({ activeProjectId: projectId });
renameProject: async (workspaceId, title) => {
try {
return applyWorkspace(await renameImageWorkspaceProject(workspaceId, title));
} catch (error) {
set({ error: messageOf(error) });
throw error;
}
},
selectAgent: (projectId, agentId) => {
const project = get().snapshot?.projects.find((item) => item.id === projectId);
if (!project?.agents.some((agent) => agent.id === agentId)) return;
set((state) => ({ activeAgentIds: { ...state.activeAgentIds, [projectId]: agentId } }));
selectProject: async (workspaceId) => {
if (!get().bootstrap?.workspaces.some((item) => item.workspaceId === workspaceId)) return;
set({
activeWorkspaceId: workspaceId,
workspace: null,
tasks: [],
error: null,
});
try {
await loadWorkspace(workspaceId);
} catch (error) {
set({ error: messageOf(error) });
}
},
refreshWorkspace: async () => {
const workspaceId = get().activeWorkspaceId;
if (!workspaceId) return null;
try {
return applyWorkspace(await fetchImageWorkspaceProject(workspaceId));
} catch (error) {
set({ error: messageOf(error) });
throw error;
}
},
refreshTasks: async () => {
const workspaceId = get().activeWorkspaceId;
if (!workspaceId) {
set({ tasks: [] });
return [];
}
try {
const tasks = await fetchImageWorkspaceTasks(workspaceId);
set({ tasks });
return tasks;
} catch (error) {
set({ error: messageOf(error) });
throw error;
}
},
sendMessage: async (message) => {
const workspace = get().workspace;
if (!workspace) throw new Error('请先选择设计项目');
try {
return applyWorkspace(await sendImageWorkspaceMessage(
workspace.workspaceId,
workspace.turnRevision,
message,
));
} catch (error) {
set({ error: messageOf(error) });
return await recoverRevisionConflict(error);
}
},
confirmGeneration: async (quoteId) => {
const workspace = get().workspace;
if (!workspace) throw new Error('请先选择设计项目');
try {
const updated = applyWorkspace(await confirmImageWorkspaceGeneration(
workspace.workspaceId,
workspace.turnRevision,
quoteId,
));
await get().refreshTasks().catch(() => []);
return updated;
} catch (error) {
set({ error: messageOf(error) });
return await recoverRevisionConflict(error);
}
},
reset: () => {
inFlightLoad = null;
set({
status: 'idle',
snapshot: null,
activeProjectId: null,
activeAgentIds: {},
bootstrap: null,
activeWorkspaceId: null,
workspace: null,
tasks: [],
error: null,
});
},