feat: 统一 AI 设计 Workspace 与生成任务链路
需求:以设计项目组织固定设计 Agent 对话、方向确认和图片视频任务。 实现:新增 Works Square 云端适配与开发态本地适配,统一 Host API、Quote 确认、任务轮询及私有媒体 Range 代理。
This commit is contained in:
@@ -363,48 +363,50 @@ describe('Sidebar project initialization flow', () => {
|
||||
expect(screen.queryByRole('button', { name: `配置项目 ${project.name}` })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches the sidebar to cloud projects and Agents inside the AI painting module', async () => {
|
||||
it('switches the sidebar to titled Design Workspaces inside the AI painting module', async () => {
|
||||
useAuthStore.setState({
|
||||
accessToken: 'cloud-token',
|
||||
expiresAt: Date.now() + 60_000,
|
||||
});
|
||||
const workspace = {
|
||||
activeProjectId: 'cloud-project',
|
||||
capabilities: {
|
||||
modes: [],
|
||||
models: [],
|
||||
aspectRatios: [],
|
||||
resolutions: [],
|
||||
outputCounts: [],
|
||||
maxReferenceImages: 0,
|
||||
referenceUpload: { enabled: false, acceptedMimeTypes: [] },
|
||||
const summary = {
|
||||
workspaceId: 'cloud-project',
|
||||
title: '云端概念设计',
|
||||
turnRevision: 0,
|
||||
viewRevision: 0,
|
||||
phase: 'shaping',
|
||||
brief: {
|
||||
version: 0,
|
||||
status: 'draft',
|
||||
medium: null,
|
||||
summary: '正在建立作品的视觉方向',
|
||||
ready: false,
|
||||
missingDecision: '作品形式',
|
||||
},
|
||||
projects: [{
|
||||
id: 'cloud-project',
|
||||
name: '云端概念设计',
|
||||
activeAgentId: 'cloud-agent',
|
||||
agents: [{ id: 'cloud-agent', name: '概念设计 Agent' }],
|
||||
messages: [],
|
||||
}],
|
||||
updatedAt: '2026-07-31T10:00:00Z',
|
||||
};
|
||||
const bootstrap = {
|
||||
capabilities: {
|
||||
conversation: true,
|
||||
generation: true,
|
||||
image: true,
|
||||
video: true,
|
||||
},
|
||||
workspaces: [summary],
|
||||
};
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/works/image-workspace') return { success: true, workspace };
|
||||
if (path === '/api/works/image-workspace/projects' && init?.method === 'POST') {
|
||||
return { success: true, workspace };
|
||||
if (path === '/api/works/image-workspace') {
|
||||
return { success: true, data: bootstrap };
|
||||
}
|
||||
if (path === '/api/works/image-workspace/projects/cloud-project/agents' && init?.method === 'POST') {
|
||||
if (path === '/api/works/image-workspace/workspaces/cloud-project' && !init?.method) {
|
||||
return { success: true, data: { ...summary, messages: [] } };
|
||||
}
|
||||
if (path === '/api/works/image-workspace/workspaces/cloud-project/tasks') {
|
||||
return { success: true, data: [] };
|
||||
}
|
||||
if (path === '/api/works/image-workspace/workspaces' && init?.method === 'POST') {
|
||||
return {
|
||||
success: true,
|
||||
workspace: {
|
||||
...workspace,
|
||||
projects: [{
|
||||
...workspace.projects[0],
|
||||
agents: [
|
||||
...workspace.projects[0].agents,
|
||||
{ id: 'cloud-agent-two', name: '新 Agent' },
|
||||
],
|
||||
}],
|
||||
},
|
||||
data: { ...summary, workspaceId: 'cloud-project-two', title: '角色设定', messages: [] },
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
@@ -413,7 +415,6 @@ describe('Sidebar project initialization flow', () => {
|
||||
render(<MemoryRouter initialEntries={['/image-canvas']}><Sidebar /></MemoryRouter>);
|
||||
|
||||
expect(await screen.findByTestId('sidebar-image-project-cloud-project')).toHaveTextContent('云端概念设计');
|
||||
expect(screen.getByText('概念设计 Agent')).toBeInTheDocument();
|
||||
const createImageProject = screen.getByTestId('sidebar-create-image-project');
|
||||
expect(createImageProject).toBeEnabled();
|
||||
expect(createImageProject).toHaveClass('bg-transparent', 'border-0', 'shadow-none');
|
||||
@@ -421,12 +422,10 @@ describe('Sidebar project initialization flow', () => {
|
||||
expect(screen.queryByText('项目路径')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('项目模板')).not.toBeInTheDocument();
|
||||
expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/opencode/projects')).toBe(false);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '为 云端概念设计 添加 Agent' }));
|
||||
expect(await screen.findByText('新 Agent')).toBeInTheDocument();
|
||||
expect(screen.queryByText(/添加 Agent/)).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('sidebar-create-image-project'));
|
||||
expect(screen.getByRole('dialog', { name: '新建项目' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog', { name: '新建设计项目' })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('项目名称')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('项目路径')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('项目模板')).not.toBeInTheDocument();
|
||||
@@ -434,55 +433,65 @@ describe('Sidebar project initialization flow', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建项目' }));
|
||||
|
||||
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith(
|
||||
'/api/works/image-workspace/projects',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'X-NianCode-Access-Token': 'cloud-token' },
|
||||
body: JSON.stringify({ name: '角色设定' }),
|
||||
},
|
||||
'/api/works/image-workspace/workspaces',
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
));
|
||||
const createCall = hostApiFetchMock.mock.calls.find(
|
||||
([path, init]) => path === '/api/works/image-workspace/workspaces'
|
||||
&& (init as RequestInit | undefined)?.method === 'POST',
|
||||
);
|
||||
expect(JSON.parse(String((createCall?.[1] as RequestInit).body))).toMatchObject({
|
||||
title: '角色设定',
|
||||
clientWorkspaceId: expect.stringMatching(/^workspace-/),
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps production project copy while the development adapter bypasses cloud login', async () => {
|
||||
it('keeps identical project copy when Main injects the local development adapter', async () => {
|
||||
window.electron.imageWorkspaceLocalDevelopment = true;
|
||||
const adapterProject = {
|
||||
id: 'local-project',
|
||||
name: '概念设计',
|
||||
activeAgentId: 'local-agent',
|
||||
agents: [{ id: 'local-agent', name: '视觉创作 Agent' }],
|
||||
messages: [],
|
||||
};
|
||||
let workspace = {
|
||||
activeProjectId: adapterProject.id as string | null,
|
||||
capabilities: {
|
||||
modes: [],
|
||||
models: [],
|
||||
aspectRatios: [],
|
||||
resolutions: [],
|
||||
outputCounts: [],
|
||||
maxReferenceImages: 0,
|
||||
referenceUpload: { enabled: false, acceptedMimeTypes: [] },
|
||||
const summary = {
|
||||
workspaceId: 'local-project',
|
||||
title: '概念设计',
|
||||
turnRevision: 0,
|
||||
viewRevision: 0,
|
||||
phase: 'shaping',
|
||||
brief: {
|
||||
version: 0,
|
||||
status: 'draft',
|
||||
medium: null,
|
||||
summary: '正在建立作品的视觉方向',
|
||||
ready: false,
|
||||
missingDecision: '作品形式',
|
||||
},
|
||||
projects: [adapterProject],
|
||||
updatedAt: '2026-07-31T10:00:00Z',
|
||||
};
|
||||
let bootstrap = {
|
||||
capabilities: {
|
||||
conversation: true,
|
||||
generation: true,
|
||||
image: true,
|
||||
video: true,
|
||||
},
|
||||
workspaces: [summary],
|
||||
};
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/works/image-workspace' && !init?.method) {
|
||||
return { success: true, workspace };
|
||||
return { success: true, data: bootstrap };
|
||||
}
|
||||
if (path === '/api/works/image-workspace/projects' && init?.method === 'POST') {
|
||||
const createdProject = {
|
||||
id: 'local-project-two',
|
||||
name: '角色设计',
|
||||
activeAgentId: 'local-agent-two',
|
||||
agents: [{ id: 'local-agent-two', name: '视觉创作 Agent' }],
|
||||
if (path === '/api/works/image-workspace/workspaces/local-project' && !init?.method) {
|
||||
return { success: true, data: { ...summary, messages: [] } };
|
||||
}
|
||||
if (path === '/api/works/image-workspace/workspaces/local-project/tasks') {
|
||||
return { success: true, data: [] };
|
||||
}
|
||||
if (path === '/api/works/image-workspace/workspaces' && init?.method === 'POST') {
|
||||
const created = {
|
||||
...summary,
|
||||
workspaceId: 'local-project-two',
|
||||
title: '角色设计',
|
||||
messages: [],
|
||||
};
|
||||
workspace = {
|
||||
...workspace,
|
||||
activeProjectId: createdProject.id,
|
||||
projects: [...workspace.projects, createdProject],
|
||||
};
|
||||
return { success: true, workspace };
|
||||
bootstrap = { ...bootstrap, workspaces: [created, ...bootstrap.workspaces] };
|
||||
return { success: true, data: created };
|
||||
}
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
@@ -490,7 +499,7 @@ describe('Sidebar project initialization flow', () => {
|
||||
const { container } = render(<MemoryRouter initialEntries={['/image-canvas']}><Sidebar /></MemoryRouter>);
|
||||
|
||||
expect(await screen.findByTestId('sidebar-image-project-local-project')).toHaveTextContent('概念设计');
|
||||
expect(screen.getByTestId('sidebar-create-image-project')).toHaveTextContent('新建项目');
|
||||
expect(screen.getByTestId('sidebar-create-image-project')).toHaveTextContent('新建设计项目');
|
||||
expect(screen.getByTestId('sidebar-image-projects')).toBeInTheDocument();
|
||||
expect(container).not.toHaveTextContent('本地开发');
|
||||
expect(container).not.toHaveTextContent('本地项目');
|
||||
@@ -498,15 +507,12 @@ describe('Sidebar project initialization flow', () => {
|
||||
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/image-workspace', {});
|
||||
|
||||
fireEvent.click(screen.getByTestId('sidebar-create-image-project'));
|
||||
expect(screen.getByRole('dialog', { name: '新建项目' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog', { name: '新建设计项目' })).toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('项目名称'), { target: { value: '角色设计' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建项目' }));
|
||||
await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith(
|
||||
'/api/works/image-workspace/projects',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: '角色设计' }),
|
||||
},
|
||||
'/api/works/image-workspace/workspaces',
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
));
|
||||
expect(await screen.findByText('角色设计')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user