Files
Agent-System/web/src/engine/messageIdentity.ts
T
weichenandCursor 9daf3b7a10 fix(web): replay task phases after refresh and keep interrupted resumable
Progress lived only in the open SSE stream, and interrupted history was shown as completed so the card could not be continued.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 06:21:09 +00:00

88 lines
2.5 KiB
TypeScript

import type { ChatMessage } from '../types/workbench';
/** 收集消息的所有可对账身份键 */
export function getMessageKeys(m: ChatMessage): string[] {
const keys: string[] = [];
if (m.clientTurnId) {
keys.push(`turn:${m.clientTurnId}:${m.role}`);
}
if (m.clientMessageId) {
keys.push(`client:${m.clientMessageId}`);
}
keys.push(`id:${m.id}`);
if (m.serverId) {
keys.push(`server:${m.serverId}`);
}
return keys;
}
/** 判断两条消息是否代表同一条物理消息 */
export function isSameMessage(a: ChatMessage, b: ChatMessage): boolean {
if (a.sessionId !== b.sessionId || a.role !== b.role) {
return false;
}
if (a.id && b.id && a.id === b.id) {
return true;
}
if (a.clientTurnId && b.clientTurnId && a.clientTurnId === b.clientTurnId) {
return true;
}
if (a.serverId && b.serverId && a.serverId === b.serverId) {
return true;
}
if (a.clientMessageId && b.clientMessageId && a.clientMessageId === b.clientMessageId) {
return true;
}
if (a.content === b.content && a.timestamp === b.timestamp) {
return true;
}
return false;
}
/** 构建历史 bucket 中已有消息的身份键集合 */
export function buildExistingMessageKeySet(messages: ChatMessage[]): Set<string> {
const keys = new Set<string>();
for (const m of messages) {
for (const k of getMessageKeys(m)) {
keys.add(k);
}
}
return keys;
}
/** live 消息是否已在历史中(按身份对账,而非仅 m.id) */
export function isLiveMessageInHistory(
live: ChatMessage,
historyMessages: ChatMessage[],
): boolean {
return historyMessages.some((h) => isSameMessage(live, h));
}
/** 过滤出尚未出现在历史中的 live 消息 */
export function filterPendingLiveMessages(
liveMessages: ChatMessage[],
historyMessages: ChatMessage[],
): ChatMessage[] {
return liveMessages.filter((m) => !isLiveMessageInHistory(m, historyMessages));
}
/** 用 live 流状态覆盖历史快照中的同一条消息 */
export function mergeLiveOverHistory(
history: ChatMessage,
live: ChatMessage,
): ChatMessage {
if (!isSameMessage(history, live)) {
return history;
}
return {
...history,
...live,
id: history.id,
serverId: live.serverId ?? history.serverId,
clientTurnId: live.clientTurnId ?? history.clientTurnId,
clientMessageId: live.clientMessageId ?? history.clientMessageId,
phaseEvents: live.phaseEvents?.length ? live.phaseEvents : history.phaseEvents,
livePhaseLabel: live.livePhaseLabel ?? history.livePhaseLabel,
};
}