Files
Agent-System/web/src/engine/messageIdentity.ts
T

125 lines
3.9 KiB
TypeScript

import type { AssistantMessageStatus, ChatMessage } from '../types/workbench';
import {
isErrorTerminalChatStatus,
isInflightChatStatus,
isSuccessTerminalChatStatus,
} from '../lib/squadCompletionObservation';
/** 收集消息的所有可对账身份键 */
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));
}
function isTerminalMessageStatus(status: string | undefined): boolean {
if (!status) return false;
return (
isSuccessTerminalChatStatus(status) ||
isErrorTerminalChatStatus(status) ||
status === 'cancelled'
);
}
/**
* Prefer history terminal status when live is still inflight.
* Otherwise a completed task_status from chat window refresh is masked by a
* stale pending/streaming_status bubble (SSE done lost / reconnect gap).
*/
export function resolveMergedMessageStatus(
historyStatus: string | undefined,
liveStatus: string | undefined,
): AssistantMessageStatus | undefined {
const live = liveStatus || '';
if (isInflightChatStatus(live) && isTerminalMessageStatus(historyStatus)) {
return historyStatus as AssistantMessageStatus | undefined;
}
return (liveStatus ?? historyStatus) as AssistantMessageStatus | undefined;
}
/** 用 live 流状态覆盖历史快照中的同一条消息 */
export function mergeLiveOverHistory(
history: ChatMessage,
live: ChatMessage,
): ChatMessage {
if (!isSameMessage(history, live)) {
return history;
}
const preferHistoryTerminal =
isInflightChatStatus(live.status || '') && isTerminalMessageStatus(history.status);
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: preferHistoryTerminal
? undefined
: live.livePhaseLabel ?? history.livePhaseLabel,
taskDurationMs: live.taskDurationMs ?? history.taskDurationMs,
phaseSnapshot: live.phaseSnapshot ?? history.phaseSnapshot,
status: resolveMergedMessageStatus(history.status, live.status),
};
}