fix: 修复 AI 设计确认生成任务链路
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user