fix: stabilize ai design history switching

This commit is contained in:
2026-08-19 10:31:36 +08:00
parent abece81fdb
commit bf0b805d9f
14 changed files with 573 additions and 48 deletions

View File

@@ -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),
});
}

View File

@@ -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>;

View File

@@ -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,