fix: 修复设计 Agent 慢响应超时

问题:客户端每 250ms 轮询 Run,并在 120 秒固定超时,导致服务端日志刷屏且慢响应被提前判定失败。

修复:将轮询改为 1 秒起步、最高 5 秒的指数退避,并将等待窗口调整为 10 分钟;新增 150 秒慢 Run 回归测试。
This commit is contained in:
2026-08-03 11:47:27 +08:00
parent f9b2616d14
commit 20c567fe5f
2 changed files with 59 additions and 3 deletions

View File

@@ -190,8 +190,9 @@ type TaskEventQueue = {
const AGENT_WEBSOCKET_OPEN = 1; const AGENT_WEBSOCKET_OPEN = 1;
const AGENT_WEBSOCKET_PING_INTERVAL_MS = 20_000; const AGENT_WEBSOCKET_PING_INTERVAL_MS = 20_000;
const AGENT_RUN_POLL_INTERVAL_MS = 250; const AGENT_RUN_INITIAL_POLL_INTERVAL_MS = 1_000;
const AGENT_RUN_TIMEOUT_MS = 120_000; const AGENT_RUN_MAX_POLL_INTERVAL_MS = 5_000;
const AGENT_RUN_TIMEOUT_MS = 10 * 60_000;
function mapBrief(brief: ServerBrief): DesignBrief { function mapBrief(brief: ServerBrief): DesignBrief {
return { return {
@@ -737,6 +738,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
private async waitForAgentRun(sessionId: string, runId: string): Promise<ServerAgentRun> { private async waitForAgentRun(sessionId: string, runId: string): Promise<ServerAgentRun> {
const deadline = Date.now() + AGENT_RUN_TIMEOUT_MS; const deadline = Date.now() + AGENT_RUN_TIMEOUT_MS;
let pollInterval = AGENT_RUN_INITIAL_POLL_INTERVAL_MS;
while (true) { while (true) {
const run = await this.requestJson<ServerAgentRun>( const run = await this.requestJson<ServerAgentRun>(
`/api/agents/sessions/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(runId)}`, `/api/agents/sessions/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(runId)}`,
@@ -751,7 +753,8 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
'设计 Agent 响应超时,请稍后重试', '设计 Agent 响应超时,请稍后重试',
); );
} }
await new Promise<void>((resolve) => setTimeout(resolve, AGENT_RUN_POLL_INTERVAL_MS)); await new Promise<void>((resolve) => setTimeout(resolve, pollInterval));
pollInterval = Math.min(pollInterval * 2, AGENT_RUN_MAX_POLL_INTERVAL_MS);
} }
} }

View File

@@ -210,6 +210,59 @@ describe('Works Square AI design adapter', () => {
); );
}); });
it('waits for a slow design run without flooding the run endpoint', async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-08-03T02:00:00Z'));
const startedAt = Date.now();
let runPolls = 0;
const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
const url = String(input);
if (url.endsWith('/api/agents/sessions')) {
return jsonResponse({ session_id: 'session-slow', status: 'active' }, 201);
}
if (url.endsWith('/api/agents/sessions/session-slow/commands')) {
return jsonResponse({ run_id: 'run-slow', status: 'queued', error: null }, 202);
}
if (url.endsWith('/api/agents/sessions/session-slow/runs/run-slow')) {
runPolls += 1;
return jsonResponse({
run_id: 'run-slow',
status: Date.now() - startedAt >= 150_000 ? 'succeeded' : 'running',
error: null,
});
}
if (url.endsWith('/api/design/workspaces/workspace-one')) {
return jsonResponse(serverWorkspace);
}
throw new Error(`Unexpected request: ${url} ${init?.method ?? 'GET'}`);
});
const adapter = new WorksSquareDesignWorkspace({
apiBaseUrl: 'https://square.example',
fetchImpl: fetchMock,
});
try {
const outcomePromise = adapter.submitMessage({
workspaceId: 'workspace-one',
clientTurnId: 'turn-slow',
expectedTurnRevision: 1,
message: '整理一个复杂的品牌设计方向',
}).then(
(value) => ({ value, error: null }),
(error: unknown) => ({ value: null, error }),
);
await vi.advanceTimersByTimeAsync(160_000);
const outcome = await outcomePromise;
expect(outcome.error).toBeNull();
expect(outcome.value).toMatchObject({ workspaceId: 'workspace-one' });
expect(runPolls).toBeLessThanOrEqual(40);
} finally {
vi.useRealTimers();
}
});
it('maps an invalid Runtime command to a user input error', async () => { it('maps an invalid Runtime command to a user input error', async () => {
const fetchMock = vi.fn<typeof fetch>() const fetchMock = vi.fn<typeof fetch>()
.mockResolvedValueOnce(jsonResponse({ .mockResolvedValueOnce(jsonResponse({