feat: 完善图像工作区与创作工具体验
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-16 14:08:02 +08:00
parent bfcb88cfef
commit 26b52d76e3
92 changed files with 16678 additions and 2975 deletions

View File

@@ -36,6 +36,49 @@ const serverConversation = {
medium: 'image',
brief_version: 1,
brief_summary: '???????????',
final_prompt: '??????????????',
prompt_mode: 'guided',
generation_parameters: {
model: 'image-model-one',
resolution: '1024x1024',
aspect_ratio: '1:1',
duration_seconds: null,
},
generation_options: {
models: [{
value: 'image-model-one',
label: '??????',
default: true,
disabled: false,
multiplier: 1,
}],
resolutions: [{
value: '1024x1024',
label: '1024 × 1024',
default: true,
disabled: false,
multiplier: 1,
}],
durations: [],
aspect_ratios: [{
value: '1:1',
label: '1:1',
default: true,
disabled: false,
multiplier: 1,
}, {
value: '16:9',
label: '16:9',
default: false,
disabled: false,
multiplier: 1,
}],
},
pricing: {
schema: 'design-pricing-v2',
amount: 1,
rounding: 'ceil',
},
quoted_design_points: 1,
expires_at: '2026-07-31T11:00:00Z',
},
@@ -55,6 +98,16 @@ const serverWorkspace = {
updated_at: '2026-07-31T10:00:00Z',
};
const quoteConfirmationInput = {
finalPrompt: '??????????????',
generationParameters: {
model: 'image-model-one',
resolution: '1024x1024',
aspectRatio: '1:1',
durationSeconds: null,
},
};
function jsonResponse(payload: unknown, status = 200): Response {
return new Response(JSON.stringify(payload), {
status,
@@ -196,6 +249,156 @@ describe('Works Square AI design adapter', () => {
))).toBe(true);
});
it('returns an explicit deletion result when the Workspace API responds with JSON', async () => {
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse({
workspace_id: 'workspace/one',
deleted: true,
}));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
await expect(adapter.deleteWorkspace('workspace/one')).resolves.toEqual({
workspaceId: 'workspace/one',
deleted: true,
});
expect(fetchMock).toHaveBeenCalledWith(
'https://square.example/api/design/workspaces/workspace%2Fone',
expect.objectContaining({
method: 'DELETE',
headers: expect.objectContaining({
Accept: 'application/json',
Authorization: 'Bearer access-one',
}),
}),
);
});
it('accepts a 204 deletion and clears only the deleted Workspace event/session caches', async () => {
let deleted = false;
let targetConversationReads = 0;
const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
const url = String(input);
if (url.endsWith('/api/design/workspaces/workspace-one') && init?.method === 'DELETE') {
deleted = true;
return new Response(null, { status: 204 });
}
if (url.endsWith('/workspaces/workspace-one/conversations/conversation-one')) {
targetConversationReads += 1;
if (deleted) {
return jsonResponse({
detail: { code: 'workspace_not_found', message: 'deleted' },
}, 404);
}
return jsonResponse(serverConversation);
}
if (url.endsWith('/sessions/session-one/stream-tickets')) {
return jsonResponse({
stream_url: '/api/agents/sessions/session-one/ws?ticket=ticket-one',
});
}
if (url.endsWith('/workspaces/workspace-two/conversations/conversation-two')) {
return jsonResponse({
...serverConversation,
workspace_id: 'workspace-two',
conversation_id: 'conversation-two',
agent_session_id: 'session-two',
});
}
if (url.endsWith('/sessions/session-two/stream-tickets')) {
return jsonResponse({
stream_url: '/api/agents/sessions/session-two/ws?ticket=ticket-two',
});
}
throw new Error(`Unexpected request: ${url}`);
});
const { sockets, webSocketFactory } = scriptedSockets([{ open: true }, { open: true }]);
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const target = await adapter.openWorkspaceEvents({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
});
const keeper = await adapter.openWorkspaceEvents({
workspaceId: 'workspace-two',
conversationId: 'conversation-two',
});
await expect(adapter.deleteWorkspace('workspace-one')).resolves.toEqual({
workspaceId: 'workspace-one',
deleted: true,
});
expect(sockets.map((socket) => socket.readyState)).toEqual([3, 1]);
await expect(adapter.openWorkspaceEvents({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
})).rejects.toMatchObject({
status: 404,
code: 'workspace_not_found',
message: '设计项目不存在或无权访问',
});
expect(targetConversationReads).toBe(2);
expect(fetchMock.mock.calls.filter(([, init]) => init?.method === 'DELETE'))
.toEqual([expect.arrayContaining([
'https://square.example/api/design/workspaces/workspace-one',
])]);
target.close();
keeper.close();
});
it('preserves Workspace event/session caches when deletion fails', async () => {
let ticketSequence = 0;
const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
const url = String(input);
if (url.endsWith('/workspaces/workspace-one/conversations/conversation-one')) {
return jsonResponse(serverConversation);
}
if (url.endsWith('/sessions/session-one/stream-tickets')) {
ticketSequence += 1;
return jsonResponse({
stream_url: `/api/agents/sessions/session-one/ws?ticket=ticket-${ticketSequence}`,
});
}
if (url.endsWith('/api/design/workspaces/workspace-one') && init?.method === 'DELETE') {
return jsonResponse({
detail: { code: 'workspace_unavailable', message: 'private detail' },
}, 503);
}
throw new Error(`Unexpected request: ${url}`);
});
const { sockets, webSocketFactory } = scriptedSockets([{ open: true }, { open: true }]);
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
webSocketFactory,
});
const first = await adapter.openWorkspaceEvents({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
});
await expect(adapter.deleteWorkspace('workspace-one')).rejects.toMatchObject({
status: 503,
code: 'workspace_unavailable',
});
const second = await adapter.openWorkspaceEvents({
workspaceId: 'workspace-one',
conversationId: 'conversation-one',
});
expect(fetchMock.mock.calls.filter(([url]) => (
String(url).endsWith('/workspaces/workspace-one/conversations/conversation-one')
))).toHaveLength(1);
expect(sockets.map((socket) => socket.readyState)).toEqual([1, 1]);
first.close();
second.close();
});
it('lists, creates, and reads Conversations through the Workspace API', async () => {
const secondConversation = {
...serverConversation,
@@ -348,6 +551,47 @@ describe('Works Square AI design adapter', () => {
expect(commandBody.input).not.toHaveProperty('conversation_id');
});
it('re-quotes a generation Quote through the cloud PATCH contract and maps every option', async () => {
const quote = serverConversation.messages[0].generation_quote;
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse(quote));
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
await expect(adapter.updateGenerationQuote({
workspaceId: 'workspace-one',
quoteId: 'quote-one',
finalPrompt: '用户最新编辑后的完整提示词',
generationParameters: {
model: 'image-model-one',
resolution: '1024x1024',
aspectRatio: '16:9',
durationSeconds: null,
},
})).resolves.toMatchObject({
quoteId: 'quote-one',
finalPrompt: '??????????????',
generationOptions: {
models: [{ value: 'image-model-one', label: '??????', disabled: false }],
},
});
expect(fetchMock).toHaveBeenCalledWith(
'https://square.example/api/design/workspaces/workspace-one/generation-quotes/quote-one',
expect.objectContaining({
method: 'PATCH',
body: JSON.stringify({
final_prompt: '用户最新编辑后的完整提示词',
model: 'image-model-one',
resolution: '1024x1024',
aspect_ratio: '16:9',
duration_seconds: null,
}),
}),
);
});
it('uploads a local reference image as multipart data and maps the returned Asset', async () => {
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse({
asset_id: 'asset-uploaded',
@@ -1008,6 +1252,7 @@ describe('Works Square AI design adapter', () => {
clientTurnId: 'turn-confirm-live',
expectedTurnRevision: 1,
quoteId: 'quote-one',
...quoteConfirmationInput,
})).resolves.toMatchObject({ turnRevision: 2 });
await consume;
@@ -1025,7 +1270,17 @@ describe('Works Square AI design adapter', () => {
client_command_id: 'turn-confirm-live',
name: 'turn.submit',
input: {
action: { type: 'confirm_generation', quote_id: 'quote-one' },
action: {
type: 'confirm_generation',
quote_id: 'quote-one',
final_prompt: quoteConfirmationInput.finalPrompt,
aspect_ratio: '1:1',
generation_parameters: {
model: 'image-model-one',
resolution: '1024x1024',
duration_seconds: null,
},
},
},
},
});
@@ -1089,6 +1344,7 @@ describe('Works Square AI design adapter', () => {
clientTurnId: 'turn-unavailable',
expectedTurnRevision: 1,
quoteId: 'quote-one',
...quoteConfirmationInput,
})).rejects.toMatchObject({
status: 503,
code: 'agent_runtime_unavailable',
@@ -1256,6 +1512,7 @@ describe('Works Square AI design adapter', () => {
clientTurnId: 'turn-two',
expectedTurnRevision: 1,
quoteId: 'quote-one',
...quoteConfirmationInput,
});
expect(workspace.workspaceId).toBe('workspace-one');
@@ -1271,7 +1528,17 @@ describe('Works Square AI design adapter', () => {
expected_turn_revision: 1,
message: '确认生成',
attachment_asset_ids: [],
action: { type: 'confirm_generation', quote_id: 'quote-one' },
action: {
type: 'confirm_generation',
quote_id: 'quote-one',
final_prompt: quoteConfirmationInput.finalPrompt,
aspect_ratio: '1:1',
generation_parameters: {
model: 'image-model-one',
resolution: '1024x1024',
duration_seconds: null,
},
},
},
}),
}),