fix: stabilize ai design history switching
This commit is contained in:
@@ -18,7 +18,10 @@ import {
|
||||
type DesignRenameWorkspaceInput,
|
||||
type DesignSubmitMessageInput,
|
||||
} from '../../../shared/image-workspace';
|
||||
import { DesignWorkspaceModuleError } from '../../image-workspace/module';
|
||||
import {
|
||||
DesignWorkspaceModuleError,
|
||||
type DesignWorkspaceEventSubscription,
|
||||
} from '../../image-workspace/module';
|
||||
import type { HostApiContext } from '../context';
|
||||
import {
|
||||
flushStreamingHeaders,
|
||||
@@ -328,19 +331,25 @@ async function relayWorkspaceEvents(
|
||||
}
|
||||
const header = req.headers['last-event-id'];
|
||||
const afterEventId = Array.isArray(header) ? header[0] : header;
|
||||
const subscription = await ctx.imageWorkspace.openWorkspaceEvents({
|
||||
workspaceId,
|
||||
conversationId,
|
||||
...(afterEventId ? { afterEventId } : {}),
|
||||
});
|
||||
let subscription: DesignWorkspaceEventSubscription | null = null;
|
||||
let closed = false;
|
||||
const close = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
subscription.close();
|
||||
subscription?.close();
|
||||
};
|
||||
res.once('close', close);
|
||||
try {
|
||||
const opened = await ctx.imageWorkspace.openWorkspaceEvents({
|
||||
workspaceId,
|
||||
conversationId,
|
||||
...(afterEventId ? { afterEventId } : {}),
|
||||
});
|
||||
if (closed) {
|
||||
opened.close();
|
||||
return;
|
||||
}
|
||||
subscription = opened;
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
@@ -456,7 +465,13 @@ export async function handleImageWorkspaceRoutes(
|
||||
&& segments[0] === 'workspaces'
|
||||
&& segments[2] === 'conversations'
|
||||
&& req.method === 'GET') {
|
||||
sendData(res, await ctx.imageWorkspace.getConversation(segments[1], segments[3]));
|
||||
const before = url.searchParams.get('before');
|
||||
sendData(
|
||||
res,
|
||||
before === null
|
||||
? await ctx.imageWorkspace.getConversation(segments[1], segments[3])
|
||||
: await ctx.imageWorkspace.getConversation(segments[1], segments[3], before),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -257,6 +257,7 @@ const LOCAL_CAPABILITIES: DesignCapabilities = {
|
||||
image: true,
|
||||
video: true,
|
||||
};
|
||||
const LOCAL_CONVERSATION_MESSAGE_PAGE_SIZE = 10;
|
||||
|
||||
type PersistedWorkspace = DesignWorkspaceSummary & {
|
||||
clientWorkspaceId: string;
|
||||
@@ -542,9 +543,13 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
async getConversation(
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
before?: string,
|
||||
): Promise<DesignConversation> {
|
||||
const state = await this.load();
|
||||
return clone(this.requireConversation(state, workspaceId, conversationId));
|
||||
return this.conversationView(
|
||||
this.requireConversation(state, workspaceId, conversationId),
|
||||
before,
|
||||
);
|
||||
}
|
||||
|
||||
renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace> {
|
||||
@@ -1081,14 +1086,34 @@ export class LocalImageWorkspace implements DesignWorkspaceModule {
|
||||
});
|
||||
}
|
||||
|
||||
private conversationView(conversation: PersistedConversation): DesignConversation {
|
||||
private conversationView(
|
||||
conversation: PersistedConversation,
|
||||
before?: string,
|
||||
): DesignConversation {
|
||||
const {
|
||||
clientConversationId: _clientId,
|
||||
requestHashes: _requestHashes,
|
||||
...view
|
||||
} = conversation;
|
||||
const endIndex = before === undefined
|
||||
? conversation.messages.length
|
||||
: conversation.messages.findIndex((message) => message.id === before);
|
||||
if (endIndex < 0) {
|
||||
throw new LocalImageWorkspaceError(
|
||||
422,
|
||||
'conversation_history_cursor_invalid',
|
||||
'历史会话分页位置已失效,请重新打开会话',
|
||||
);
|
||||
}
|
||||
const startIndex = Math.max(0, endIndex - LOCAL_CONVERSATION_MESSAGE_PAGE_SIZE);
|
||||
const messages = conversation.messages.slice(startIndex, endIndex);
|
||||
return clone({
|
||||
...view,
|
||||
messages,
|
||||
messagePage: {
|
||||
hasOlder: startIndex > 0,
|
||||
nextBefore: startIndex > 0 ? messages[0]?.id ?? null : null,
|
||||
},
|
||||
latestMessagePreview: latestMessagePreview(conversation.messages),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ export interface DesignWorkspaceModule {
|
||||
renameWorkspace(input: DesignRenameWorkspaceInput): Promise<DesignWorkspace>;
|
||||
getWorkspace(workspaceId: string): Promise<DesignWorkspace>;
|
||||
createConversation(input: DesignCreateConversationInput): Promise<DesignConversation>;
|
||||
getConversation(workspaceId: string, conversationId: string): Promise<DesignConversation>;
|
||||
getConversation(
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
before?: string,
|
||||
): Promise<DesignConversation>;
|
||||
submitMessage(input: DesignSubmitMessageInput): Promise<DesignConversation>;
|
||||
updateGenerationQuote(input: DesignGenerationQuoteUpdateInput): Promise<DesignGenerationQuote>;
|
||||
confirmGeneration(input: DesignConfirmGenerationInput): Promise<DesignConversation>;
|
||||
|
||||
@@ -86,6 +86,7 @@ type ServerQuote = {
|
||||
};
|
||||
|
||||
type ServerMessage = {
|
||||
message_id?: string;
|
||||
role: DesignMessage['role'];
|
||||
kind: DesignMessage['kind'];
|
||||
text: string;
|
||||
@@ -119,6 +120,10 @@ type ServerConversationSummary = {
|
||||
|
||||
type ServerConversation = ServerConversationSummary & {
|
||||
messages: ServerMessage[];
|
||||
message_page?: {
|
||||
has_older: boolean;
|
||||
next_before: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type ServerWorkspace = ServerWorkspaceSummary & {
|
||||
@@ -264,6 +269,7 @@ type AgentCommandChannel = {
|
||||
};
|
||||
|
||||
const AGENT_WEBSOCKET_OPEN = 1;
|
||||
const AGENT_WEBSOCKET_OPEN_TIMEOUT_MS = 10_000;
|
||||
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;
|
||||
@@ -393,7 +399,8 @@ function mapConversationSummary(
|
||||
|
||||
function mapConversation(conversation: ServerConversation): DesignConversation {
|
||||
const messages = conversation.messages.map((message, index) => ({
|
||||
id: `${conversation.conversation_id}:${message.turn_revision}:${message.role}:${index}`,
|
||||
id: message.message_id
|
||||
?? `${conversation.conversation_id}:${message.turn_revision}:${message.role}:${index}`,
|
||||
role: message.role,
|
||||
kind: message.kind,
|
||||
text: message.text,
|
||||
@@ -408,6 +415,12 @@ function mapConversation(conversation: ServerConversation): DesignConversation {
|
||||
?? normalizeMessagePreview(conversation.latest_message_preview)
|
||||
?? normalizeMessagePreview(conversation.brief.summary),
|
||||
messages,
|
||||
messagePage: conversation.message_page
|
||||
? {
|
||||
hasOlder: conversation.message_page.has_older,
|
||||
nextBefore: conversation.message_page.next_before,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -935,9 +948,11 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
async getConversation(
|
||||
workspaceId: string,
|
||||
conversationId: string,
|
||||
before?: string,
|
||||
): Promise<DesignConversation> {
|
||||
const query = before ? `?before=${encodeURIComponent(before)}` : '';
|
||||
const conversation = await this.requestJson<ServerConversation>(
|
||||
`/api/design/workspaces/${encodeURIComponent(workspaceId)}/conversations/${encodeURIComponent(conversationId)}`,
|
||||
`/api/design/workspaces/${encodeURIComponent(workspaceId)}/conversations/${encodeURIComponent(conversationId)}${query}`,
|
||||
);
|
||||
this.rememberConversation(conversation);
|
||||
return mapConversation(conversation);
|
||||
@@ -1328,6 +1343,7 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
let ending = false;
|
||||
let settled = false;
|
||||
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
||||
let openedTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let unregister = () => undefined;
|
||||
let resolveOpened: () => void = () => undefined;
|
||||
let rejectOpened: (error: unknown) => void = () => undefined;
|
||||
@@ -1349,6 +1365,8 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
socket.onclose = null;
|
||||
if (heartbeat !== null) clearInterval(heartbeat);
|
||||
heartbeat = null;
|
||||
if (openedTimeout !== null) clearTimeout(openedTimeout);
|
||||
openedTimeout = null;
|
||||
unregister();
|
||||
if (didOpen) {
|
||||
this.unregisterAgentCommandChannel(sessionId, commandChannel);
|
||||
@@ -1450,6 +1468,19 @@ export class WorksSquareDesignWorkspace implements DesignWorkspaceModule {
|
||||
input.conversationId,
|
||||
close,
|
||||
);
|
||||
openedTimeout = setTimeout(() => {
|
||||
if (ending || didOpen) return;
|
||||
beginEnd(new DesignWorkspaceModuleError(
|
||||
504,
|
||||
'DESIGN_EVENT_STREAM_TIMEOUT',
|
||||
'AI 设计任务状态连接超时,请稍后重试',
|
||||
));
|
||||
try {
|
||||
socket.close(1000, 'Timed out opening design event stream');
|
||||
} catch {
|
||||
// settle below remains authoritative when the transport cannot close.
|
||||
}
|
||||
}, AGENT_WEBSOCKET_OPEN_TIMEOUT_MS);
|
||||
await opened;
|
||||
return {
|
||||
events: queue.events,
|
||||
|
||||
Reference in New Issue
Block a user