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