impl client message id and client retry
This commit is contained in:
+4
@@ -139,6 +139,7 @@ describe('useEditorAgentConversation', () => {
|
||||
expect(client.sendMessage).toHaveBeenCalledWith(
|
||||
'conversation-1',
|
||||
expect.objectContaining({
|
||||
clientMessageId: expect.stringMatching(/^editor-agent-/u),
|
||||
text: '把这个角色改成像素风',
|
||||
attachments: [],
|
||||
}),
|
||||
@@ -155,6 +156,9 @@ describe('useEditorAgentConversation', () => {
|
||||
'把这个角色改成像素风',
|
||||
'我来处理',
|
||||
]);
|
||||
expect(result.current.messages[0]?.clientMessageId).toMatch(
|
||||
/^editor-agent-/u,
|
||||
);
|
||||
expect(result.current.messages[1]?.toolCall).toEqual(
|
||||
expect.objectContaining({
|
||||
toolName: 'generate_image',
|
||||
|
||||
@@ -83,13 +83,23 @@ function isAbortError(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
function createEditorAgentClientMessageId() {
|
||||
const randomId =
|
||||
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
return `editor-agent-${randomId}`;
|
||||
}
|
||||
|
||||
function createLocalUserMessage(params: {
|
||||
id: number;
|
||||
clientMessageId: string;
|
||||
text: string;
|
||||
attachments: EditorAgentAttachmentRef[];
|
||||
}): EditorAgentMessage {
|
||||
return {
|
||||
id: params.id,
|
||||
clientMessageId: params.clientMessageId,
|
||||
role: 'user',
|
||||
text: params.text,
|
||||
attachments: params.attachments,
|
||||
@@ -359,12 +369,14 @@ export function useEditorAgentConversation({
|
||||
return;
|
||||
}
|
||||
const conversationId = await ensureConversationForSend();
|
||||
const clientMessageId = createEditorAgentClientMessageId();
|
||||
const abortController = new AbortController();
|
||||
activeRequestAbortControllerRef.current = abortController;
|
||||
setErrorMessage(null);
|
||||
setIsWaiting(true);
|
||||
const optimisticMessage = createLocalUserMessage({
|
||||
id: -1,
|
||||
clientMessageId,
|
||||
text,
|
||||
attachments,
|
||||
});
|
||||
@@ -377,6 +389,7 @@ export function useEditorAgentConversation({
|
||||
const response = await client.sendMessage(
|
||||
conversationId,
|
||||
{
|
||||
clientMessageId,
|
||||
text,
|
||||
attachments,
|
||||
},
|
||||
|
||||
@@ -599,6 +599,60 @@ describe('apiClient', () => {
|
||||
expect(result).toEqual({ value: 42 });
|
||||
});
|
||||
|
||||
it('reuses the exact post body and request id for an enabled unsafe transport retry', async () => {
|
||||
setStoredAccessToken('editor-agent-token', { emit: false });
|
||||
fetchMock
|
||||
.mockRejectedValueOnce(new TypeError('network unavailable'))
|
||||
.mockResolvedValueOnce(
|
||||
createResponseMock({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
ok: true,
|
||||
data: { deltaMessages: [] },
|
||||
error: null,
|
||||
meta: { apiVersion: '2026-06-16' },
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
const body = JSON.stringify({
|
||||
clientMessageId: 'client-message-1',
|
||||
text: '生成一张图',
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
await requestJson(
|
||||
'/api/editor/agent-conversations/conversation-1/messages',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
},
|
||||
'发送画布 Agent 消息失败',
|
||||
{
|
||||
authImpact: 'local',
|
||||
retry: {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 1,
|
||||
maxDelayMs: 1,
|
||||
retryUnsafeMethods: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
const firstInit = fetchMock.mock.calls[0]?.[1] as RequestInit;
|
||||
const secondInit = fetchMock.mock.calls[1]?.[1] as RequestInit;
|
||||
expect(firstInit.body).toBe(body);
|
||||
expect(secondInit.body).toBe(body);
|
||||
expect((firstInit.headers as Record<string, string>)['x-request-id']).toBe(
|
||||
'web-11111111-2222-3333-4444-555555555555',
|
||||
);
|
||||
expect((secondInit.headers as Record<string, string>)['x-request-id']).toBe(
|
||||
(firstInit.headers as Record<string, string>)['x-request-id'],
|
||||
);
|
||||
});
|
||||
|
||||
it('aborts requests when timeoutMs is reached', async () => {
|
||||
setStoredAccessToken('timeout-token', { emit: false });
|
||||
fetchMock.mockImplementation(
|
||||
|
||||
@@ -123,6 +123,7 @@ describe('editorAgentClient', () => {
|
||||
requestJsonMock.mockResolvedValueOnce(responseBody);
|
||||
|
||||
const result = await sendEditorAgentMessage('conversation-1', {
|
||||
clientMessageId: 'client-message-1',
|
||||
text: '帮我把角色改成像素风',
|
||||
attachments: [],
|
||||
});
|
||||
@@ -134,6 +135,7 @@ describe('editorAgentClient', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
clientMessageId: 'client-message-1',
|
||||
text: '帮我把角色改成像素风',
|
||||
attachments: [],
|
||||
}),
|
||||
@@ -142,6 +144,12 @@ describe('editorAgentClient', () => {
|
||||
expect.objectContaining({
|
||||
timeoutMs: 1_200_000,
|
||||
authImpact: 'local',
|
||||
retry: {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 250,
|
||||
maxDelayMs: 250,
|
||||
retryUnsafeMethods: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,12 @@ import { requestJson } from '../apiClient';
|
||||
const EDITOR_PROJECT_AGENT_CONVERSATION_API_BASE = '/api/editor/projects';
|
||||
const EDITOR_AGENT_CONVERSATION_API_BASE = '/api/editor/agent-conversations';
|
||||
const EDITOR_AGENT_MESSAGE_TIMEOUT_MS = 1_200_000;
|
||||
const EDITOR_AGENT_MESSAGE_RETRY = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 250,
|
||||
maxDelayMs: 250,
|
||||
retryUnsafeMethods: true,
|
||||
} as const;
|
||||
|
||||
export type SendEditorAgentMessageOptions = {
|
||||
signal?: AbortSignal;
|
||||
@@ -116,6 +122,7 @@ export async function sendEditorAgentMessage(
|
||||
{
|
||||
timeoutMs: EDITOR_AGENT_MESSAGE_TIMEOUT_MS,
|
||||
authImpact: 'local',
|
||||
retry: EDITOR_AGENT_MESSAGE_RETRY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user