fix: 修复 AI 设计确认生成任务链路

This commit is contained in:
2026-08-14 14:42:49 +08:00
parent 88f9ee8708
commit 22378efcee
6 changed files with 808 additions and 171 deletions

View File

@@ -212,13 +212,27 @@ type QueuedTaskEvent = {
type AgentRunEventWaiter = (run: ServerAgentRun) => void;
type AgentCommandWaiter = {
resolve(command: ServerAgentCommand): void;
reject(error: unknown): void;
timeout: ReturnType<typeof setTimeout>;
};
type AgentCommandChannel = {
socket: AgentWebSocket;
waiters: Map<string, AgentCommandWaiter>;
};
const AGENT_WEBSOCKET_OPEN = 1;
const AGENT_WEBSOCKET_PING_INTERVAL_MS = 20_000;
const AGENT_COMMAND_ACK_TIMEOUT_MS = 5_000;
const AGENT_RUN_INITIAL_POLL_INTERVAL_MS = 1_000;
const AGENT_RUN_MAX_POLL_INTERVAL_MS = 5_000;
const AGENT_RUN_TIMEOUT_MS = 10 * 60_000;
const DESIGN_EVENT_DELIVERY_BARRIER_TIMEOUT_MS = 1_000;
class AgentCommandTransportError extends Error {}
function mapBrief(brief: ServerBrief): DesignBrief {
const medium = brief.medium ?? null;
if (medium !== null && medium !== 'image' && medium !== 'video') {
@@ -710,6 +724,7 @@ function userFacingErrorMessage(code: string, fallback: string): string {
design_reasoner_unavailable: '设计 Agent 暂时不可用,请稍后重试',
design_runtime_unavailable: 'AI 设计服务暂时不可用',
design_production_unavailable: '当前生成能力暂时不可用',
agent_runtime_unavailable: 'AI 设计服务暂时不可用,请稍后重试',
agent_command_invalid: '设计请求内容无效,请检查后重试',
};
return messages[code] ?? fallback;
@@ -732,7 +747,8 @@ function agentRunErrorStatus(code: string): number {
}
if (code === 'design_reasoner_unavailable'
|| code === 'design_runtime_unavailable'
|| code === 'design_production_unavailable') {
|| code === 'design_production_unavailable'
|| code === 'agent_runtime_unavailable') {
return 503;
}
return 502;
@@ -754,6 +770,8 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
private readonly terminalAgentRuns = new Map<string, ServerAgentRun>();
private readonly agentRunEventWaiters = new Map<string, Set<AgentRunEventWaiter>>();
private readonly runStreamEndWaiters = new Map<string, Set<() => void>>();
private readonly agentCommandChannels = new Map<string, Set<AgentCommandChannel>>();
private nextAgentCommandRequestId = 0;
private eventSessionsEnabled = true;
constructor(options: WorksSquareDesignWorkspaceOptions = {}) {
@@ -905,28 +923,135 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
return this.getConversation(input.workspaceId, input.conversationId);
}
private submitTurnCommand(
private async submitTurnCommand(
sessionId: string,
input: AgentDesignTurnSubmission,
): Promise<ServerAgentCommand> {
const command = {
client_command_id: input.clientTurnId,
name: 'turn.submit',
input: {
expected_turn_revision: input.expectedTurnRevision,
message: input.message,
attachment_asset_ids: input.attachmentAssetIds,
action: input.action,
},
};
const channel = this.findAgentCommandChannel(sessionId);
if (channel) {
try {
return await this.submitTurnCommandOverWebSocket(channel, command);
} catch (error) {
if (!(error instanceof AgentCommandTransportError)) throw error;
}
}
return this.requestJson<ServerAgentCommand>(
`/api/agents/sessions/${encodeURIComponent(sessionId)}/commands`,
{
method: 'POST',
body: JSON.stringify({
client_command_id: input.clientTurnId,
name: 'turn.submit',
input: {
expected_turn_revision: input.expectedTurnRevision,
message: input.message,
attachment_asset_ids: input.attachmentAssetIds,
action: input.action,
},
}),
body: JSON.stringify(command),
},
);
}
private findAgentCommandChannel(sessionId: string): AgentCommandChannel | null {
const channels = this.agentCommandChannels.get(sessionId);
if (!channels) return null;
for (const channel of channels) {
if (channel.socket.readyState === AGENT_WEBSOCKET_OPEN) return channel;
}
return null;
}
private submitTurnCommandOverWebSocket(
channel: AgentCommandChannel,
command: Record<string, unknown>,
): Promise<ServerAgentCommand> {
const requestId = `design-command-${Date.now()}-${this.nextAgentCommandRequestId += 1}`;
return new Promise<ServerAgentCommand>((resolve, reject) => {
const timeout = setTimeout(() => {
channel.waiters.delete(requestId);
reject(new AgentCommandTransportError('Agent WebSocket command acknowledgement timed out'));
}, AGENT_COMMAND_ACK_TIMEOUT_MS);
channel.waiters.set(requestId, { resolve, reject, timeout });
try {
channel.socket.send(JSON.stringify({
type: 'command.submit',
request_id: requestId,
command,
}));
} catch {
clearTimeout(timeout);
channel.waiters.delete(requestId);
reject(new AgentCommandTransportError('Agent WebSocket command send failed'));
}
});
}
private handleAgentCommandFrame(channel: AgentCommandChannel, data: unknown): void {
if (typeof data !== 'string') return;
let frame: Record<string, unknown>;
try {
const parsed = JSON.parse(data) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return;
frame = parsed as Record<string, unknown>;
} catch {
return;
}
const requestId = frame.request_id;
if (typeof requestId !== 'string') return;
const waiter = channel.waiters.get(requestId);
if (!waiter) return;
if (frame.type === 'command.accepted') {
const command = frame.command;
if (!command || typeof command !== 'object' || Array.isArray(command)
|| typeof (command as Record<string, unknown>).run_id !== 'string') {
return;
}
clearTimeout(waiter.timeout);
channel.waiters.delete(requestId);
waiter.resolve(command as ServerAgentCommand);
return;
}
if (frame.type !== 'error') return;
const error = frame.error;
if (!error || typeof error !== 'object' || Array.isArray(error)) return;
const code = (error as Record<string, unknown>).code;
const message = (error as Record<string, unknown>).message;
if (typeof code !== 'string' || typeof message !== 'string') return;
clearTimeout(waiter.timeout);
channel.waiters.delete(requestId);
waiter.reject(new DesignWorkspaceModuleError(
agentRunErrorStatus(code),
code,
userFacingErrorMessage(code, message),
));
}
private registerAgentCommandChannel(
sessionId: string,
channel: AgentCommandChannel,
): void {
const channels = this.agentCommandChannels.get(sessionId) ?? new Set<AgentCommandChannel>();
channels.add(channel);
this.agentCommandChannels.set(sessionId, channels);
}
private unregisterAgentCommandChannel(
sessionId: string,
channel: AgentCommandChannel,
): void {
const channels = this.agentCommandChannels.get(sessionId);
channels?.delete(channel);
if (channels?.size === 0) this.agentCommandChannels.delete(sessionId);
for (const waiter of channel.waiters.values()) {
clearTimeout(waiter.timeout);
waiter.reject(new AgentCommandTransportError('Agent WebSocket connection closed'));
}
channel.waiters.clear();
}
private async waitForAgentRun(sessionId: string, runId: string): Promise<ServerAgentRun> {
const deadline = Date.now() + AGENT_RUN_TIMEOUT_MS;
const streamedRun = await this.waitForAgentRunEvent(sessionId, runId, deadline);
@@ -1091,6 +1216,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
}
const { socket } = connection;
const commandChannel: AgentCommandChannel = { socket, waiters: new Map() };
const queue = createTaskEventQueue();
let latestWorkspaceEventDelivery = Promise.resolve();
let didOpen = false;
@@ -1119,7 +1245,10 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
if (heartbeat !== null) clearInterval(heartbeat);
heartbeat = null;
unregister();
if (didOpen) this.unregisterRunEventStream(sessionId);
if (didOpen) {
this.unregisterAgentCommandChannel(sessionId, commandChannel);
this.unregisterRunEventStream(sessionId);
}
try {
await connection.dispose?.();
} catch {
@@ -1163,6 +1292,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
if (ending) return;
didOpen = true;
this.registerRunEventStream(sessionId);
this.registerAgentCommandChannel(sessionId, commandChannel);
heartbeat = setInterval(() => {
if (ending || socket.readyState !== AGENT_WEBSOCKET_OPEN) return;
try {
@@ -1178,6 +1308,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
};
socket.onmessage = ({ data }) => {
if (ending) return;
this.handleAgentCommandFrame(commandChannel, data);
const agentEvent = agentEventFromWebSocketFrame(data);
const event = normalizeWorkspaceEvent(
agentEvent,