74 lines
3.4 KiB
TypeScript
74 lines
3.4 KiB
TypeScript
type Message = { id: string; role: string; parts?: unknown[] };
|
|
|
|
export function parseAppendedGenerationMessage(data: unknown): Message | null {
|
|
const message = typeof data === "string" ? JSON.parse(data) : data;
|
|
if (!message || typeof message !== "object") return null;
|
|
const candidate = message as Message;
|
|
return candidate.role === "assistant" && typeof candidate.id === "string" && Array.isArray(candidate.parts)
|
|
? candidate
|
|
: null;
|
|
}
|
|
|
|
export function appendGenerationMessage<T extends Message>(current: T[], appended: T): T[] {
|
|
const index = current.findIndex((message) => message.id === appended.id);
|
|
if (index < 0) return [...current, appended];
|
|
const next = [...current];
|
|
next[index] = appended;
|
|
return next;
|
|
}
|
|
|
|
export function messageText(message: Message) {
|
|
return (message.parts || []).flatMap((part) => {
|
|
const value = part as { type?: string; text?: string };
|
|
return value?.type === "text" && typeof value.text === "string" ? [value.text] : [];
|
|
}).join("\n\n");
|
|
}
|
|
|
|
export function mergeGenerationHistory<T extends Message>(current: T[], persisted: T[]): T[] {
|
|
if (!persisted.length) return current;
|
|
const localById = new Map(current.map((message) => [message.id, message]));
|
|
const persistedIds = new Set(persisted.map((message) => message.id));
|
|
const merged = persisted.map((message) => {
|
|
const local = localById.get(message.id);
|
|
// Polling must not replace a richer live chunk with an older database snapshot.
|
|
return local && messageText(local).length > messageText(message).length ? local : message;
|
|
}).concat(current.filter((message) => !persistedIds.has(message.id)));
|
|
return JSON.stringify(merged) === JSON.stringify(current) ? current : merged;
|
|
}
|
|
|
|
export function recoverReplacementRun(params: {
|
|
messages: Message[];
|
|
requestMessageId?: string | null;
|
|
userTurnCountAtStart: number;
|
|
requestedCardIds?: string[] | null;
|
|
expectedCount: number;
|
|
}) {
|
|
const requested = new Set(params.requestedCardIds || []);
|
|
const users = params.messages.filter((message) => message.role === "user");
|
|
const requestId = params.requestMessageId || users[params.userTurnCountAtStart]?.id;
|
|
const requestIndex = params.messages.findIndex((message) => message.id === requestId);
|
|
const completed = new Set<string>();
|
|
const failed = new Set<string>();
|
|
if (requestIndex < 0) return { completedCardIds: [], failedCardIds: [], done: false };
|
|
for (const message of params.messages.slice(requestIndex + 1)) {
|
|
if (message.role === "user") break;
|
|
if (message.role !== "assistant") continue;
|
|
const text = messageText(message);
|
|
const runId = text.match(/<!--\s*AGENT_REPLACEMENT_RUN_ID:([^>]+?)\s*-->/)?.[1]?.trim();
|
|
if (runId && runId !== requestId) continue;
|
|
const cardId = text.match(/<!--\s*AGENT_REPLACEMENT_CARD_ID:([^>]+?)\s*-->/)?.[1]?.trim();
|
|
if (!cardId || (requested.size > 0 && !requested.has(cardId))) continue;
|
|
// A start/partial message does not settle a card. Failure is terminal even with no image.
|
|
const failure = text.includes("生成失败") || text.includes("AGENT_REPLACEMENT_STATUS:failed");
|
|
if (!failure && !/!\[[^\]]*\]\(https?:\/\/[^)]+\)/.test(text)) continue;
|
|
completed.add(cardId);
|
|
if (failure) failed.add(cardId); else failed.delete(cardId);
|
|
}
|
|
const expectedCount = requested.size || params.expectedCount;
|
|
return {
|
|
completedCardIds: [...completed],
|
|
failedCardIds: [...failed],
|
|
done: expectedCount > 0 && completed.size >= expectedCount,
|
|
};
|
|
}
|