feat: 流式展示设计 Agent 对话
需求:设计 Agent 对话与确认生成的回复需要实时展示。 实现:统一通过 Agent Gateway 提交 Turn,接收并去重 assistant delta,以 canonical Workspace 收口,并修复跨项目旧请求回写竞态。
This commit is contained in:
@@ -147,8 +147,123 @@ describe('Works Square AI design adapter', () => {
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
it('submits a conversation turn through the persistent Agent Gateway Session', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
session_id: 'session-one',
|
||||
status: 'active',
|
||||
}, 201))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
run_id: 'run-one',
|
||||
status: 'queued',
|
||||
error: null,
|
||||
}, 202))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
run_id: 'run-one',
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
}))
|
||||
.mockResolvedValueOnce(jsonResponse(serverWorkspace));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
clientInstanceId: 'installation-one',
|
||||
});
|
||||
|
||||
await expect(adapter.submitMessage({
|
||||
workspaceId: 'workspace-one',
|
||||
clientTurnId: 'turn-two',
|
||||
expectedTurnRevision: 1,
|
||||
message: '做一张保护海洋的公益海报',
|
||||
attachmentAssetIds: ['asset-reference'],
|
||||
})).resolves.toMatchObject({
|
||||
workspaceId: 'workspace-one',
|
||||
turnRevision: 1,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://square.example/api/agents/sessions/session-one/commands',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_command_id: 'turn-two',
|
||||
name: 'turn.submit',
|
||||
input: {
|
||||
expected_turn_revision: 1,
|
||||
message: '做一张保护海洋的公益海报',
|
||||
attachment_asset_ids: ['asset-reference'],
|
||||
action: null,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'https://square.example/api/agents/sessions/session-one/runs/run-one',
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'https://square.example/api/design/workspaces/workspace-one',
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('maps an invalid Runtime command to a user input error', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
session_id: 'session-one',
|
||||
status: 'active',
|
||||
}, 201))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
run_id: 'run-invalid',
|
||||
status: 'queued',
|
||||
error: null,
|
||||
}, 202))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
run_id: 'run-invalid',
|
||||
status: 'failed',
|
||||
error: {
|
||||
code: 'agent_command_invalid',
|
||||
message: 'private validation detail',
|
||||
retryable: false,
|
||||
},
|
||||
}));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
await expect(adapter.submitMessage({
|
||||
workspaceId: 'workspace-one',
|
||||
clientTurnId: 'turn-invalid',
|
||||
expectedTurnRevision: 1,
|
||||
message: 'invalid',
|
||||
})).rejects.toMatchObject({
|
||||
status: 422,
|
||||
code: 'agent_command_invalid',
|
||||
message: '设计请求内容无效,请检查后重试',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps task creation behind structured Quote confirmation', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse(serverWorkspace));
|
||||
const fetchMock = vi.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
session_id: 'session-one',
|
||||
status: 'active',
|
||||
}, 201))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
run_id: 'run-confirm',
|
||||
status: 'queued',
|
||||
error: null,
|
||||
}, 202))
|
||||
.mockResolvedValueOnce(jsonResponse({
|
||||
run_id: 'run-confirm',
|
||||
status: 'succeeded',
|
||||
error: null,
|
||||
}))
|
||||
.mockResolvedValueOnce(jsonResponse(serverWorkspace));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example/',
|
||||
fetchImpl: fetchMock,
|
||||
@@ -162,19 +277,33 @@ describe('Works Square AI design adapter', () => {
|
||||
});
|
||||
|
||||
expect(workspace.workspaceId).toBe('workspace-one');
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://square.example/api/design/workspaces/workspace-one/turns',
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://square.example/api/agents/sessions/session-one/commands',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_turn_id: 'turn-two',
|
||||
expected_turn_revision: 1,
|
||||
message: '确认生成',
|
||||
attachment_asset_ids: [],
|
||||
action: { type: 'confirm_generation', quote_id: 'quote-one' },
|
||||
client_command_id: 'turn-two',
|
||||
name: 'turn.submit',
|
||||
input: {
|
||||
expected_turn_revision: 1,
|
||||
message: '确认生成',
|
||||
attachment_asset_ids: [],
|
||||
action: { type: 'confirm_generation', quote_id: 'quote-one' },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'https://square.example/api/agents/sessions/session-one/runs/run-confirm',
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'https://square.example/api/design/workspaces/workspace-one',
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('maps stable generation tasks and private asset relay paths', async () => {
|
||||
@@ -278,13 +407,32 @@ describe('Works Square AI design adapter', () => {
|
||||
type: 'design.workspace.updated',
|
||||
schema_version: 1,
|
||||
payload: {
|
||||
workspace: {
|
||||
workspace_id: 'workspace-one',
|
||||
view_revision: 2,
|
||||
},
|
||||
workspace: serverWorkspace,
|
||||
generation_tasks: [snapshotTask],
|
||||
},
|
||||
};
|
||||
const assistantDeltaEvent = {
|
||||
session_id: 'session-one',
|
||||
sequence: 2,
|
||||
runtime: 'design',
|
||||
type: 'design.assistant.delta',
|
||||
schema_version: 1,
|
||||
payload: {
|
||||
workspace_id: 'workspace-one',
|
||||
client_turn_id: 'turn-two',
|
||||
turn_revision: 2,
|
||||
chunk_index: 0,
|
||||
delta: '方向已经明确',
|
||||
},
|
||||
};
|
||||
const malformedDeltaEvent = {
|
||||
...assistantDeltaEvent,
|
||||
sequence: 99,
|
||||
payload: {
|
||||
...assistantDeltaEvent.payload,
|
||||
chunk_index: -1,
|
||||
},
|
||||
};
|
||||
const taskEvent = {
|
||||
session_id: 'session-one',
|
||||
sequence: 3,
|
||||
@@ -336,6 +484,8 @@ describe('Works Square AI design adapter', () => {
|
||||
{
|
||||
frames: [
|
||||
{ type: 'event', event: snapshotEvent },
|
||||
{ type: 'event', event: malformedDeltaEvent },
|
||||
{ type: 'event', event: assistantDeltaEvent },
|
||||
{ type: 'event', event: taskEvent },
|
||||
],
|
||||
closeCode: 1000,
|
||||
@@ -367,12 +517,28 @@ describe('Works Square AI design adapter', () => {
|
||||
type: 'design.generation_tasks.snapshot',
|
||||
workspaceId: 'workspace-one',
|
||||
workspaceViewRevision: 2,
|
||||
workspace: expect.objectContaining({
|
||||
workspaceId: 'workspace-one',
|
||||
title: serverWorkspace.title,
|
||||
messages: [expect.objectContaining({
|
||||
text: serverWorkspace.messages[0].text,
|
||||
})],
|
||||
}),
|
||||
generationTasks: [expect.objectContaining({
|
||||
taskId: 'task-snapshot',
|
||||
medium: 'image',
|
||||
status: 'running',
|
||||
})],
|
||||
},
|
||||
{
|
||||
id: 'session-one:2',
|
||||
type: 'design.assistant.delta',
|
||||
workspaceId: 'workspace-one',
|
||||
clientTurnId: 'turn-two',
|
||||
turnRevision: 2,
|
||||
chunkIndex: 0,
|
||||
delta: '方向已经明确',
|
||||
},
|
||||
{
|
||||
id: 'session-one:3',
|
||||
type: 'design.generation_task.updated',
|
||||
|
||||
Reference in New Issue
Block a user