Merge 4993f6e: multi-turn conversation system
This commit is contained in:
@@ -43,15 +43,40 @@ export interface SaveGenerationRecordResult {
|
||||
// 单个 poller 内也会重复触发。这里做客户端幂等:in-flight 合流 + 成功后短期拦截,
|
||||
// 避免后端在缺少去重时插入重复记录。
|
||||
const inFlightSaves = new Map<string, Promise<SaveGenerationRecordResult>>();
|
||||
const recentlySavedAt = new Map<string, number>();
|
||||
const recentlySavedRecords = new Map<string, { savedAt: number; signature: string }>();
|
||||
const SAVE_DEDUPE_WINDOW_MS = 60_000;
|
||||
|
||||
function pruneRecentlySaved(now: number): void {
|
||||
for (const [id, savedAt] of recentlySavedAt) {
|
||||
if (now - savedAt > SAVE_DEDUPE_WINDOW_MS) recentlySavedAt.delete(id);
|
||||
for (const [id, record] of recentlySavedRecords) {
|
||||
if (now - record.savedAt > SAVE_DEDUPE_WINDOW_MS) recentlySavedRecords.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
function stableJsonStringify(value: unknown): string {
|
||||
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map(stableJsonStringify).join(",")}]`;
|
||||
const entries = Object.entries(value as Record<string, unknown>)
|
||||
.filter(([, entryValue]) => entryValue !== undefined)
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJsonStringify(entryValue)}`).join(",")}}`;
|
||||
}
|
||||
|
||||
function buildSaveSignature(input: SaveGenerationRecordInput): string {
|
||||
return stableJsonStringify({
|
||||
tool: input.tool,
|
||||
mode: input.mode,
|
||||
title: input.title,
|
||||
status: input.status,
|
||||
prompt: input.prompt,
|
||||
taskIds: input.taskIds,
|
||||
assets: input.assets,
|
||||
config: input.config,
|
||||
result: input.result,
|
||||
metadata: input.metadata,
|
||||
createdAt: input.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
function readPendingRecords(): SaveGenerationRecordInput[] {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(PENDING_RECORDS_KEY);
|
||||
@@ -78,26 +103,29 @@ export async function saveGenerationRecord(input: SaveGenerationRecordInput): Pr
|
||||
pruneRecentlySaved(now);
|
||||
|
||||
const recordId = input.clientRecordId;
|
||||
const signature = buildSaveSignature(input);
|
||||
if (recordId) {
|
||||
const inFlight = inFlightSaves.get(recordId);
|
||||
const saveKey = `${recordId}:${signature}`;
|
||||
const inFlight = inFlightSaves.get(saveKey);
|
||||
if (inFlight) return inFlight;
|
||||
const savedAt = recentlySavedAt.get(recordId);
|
||||
if (savedAt !== undefined && now - savedAt <= SAVE_DEDUPE_WINDOW_MS) {
|
||||
// 终态记录只需落库一次;窗口内的重复调用直接视为已保存。
|
||||
const savedRecord = recentlySavedRecords.get(recordId);
|
||||
if (savedRecord && savedRecord.signature === signature && now - savedRecord.savedAt <= SAVE_DEDUPE_WINDOW_MS) {
|
||||
// 相同 clientRecordId 且 payload 完全一致时才拦截;同一记录的多轮更新需要继续保存。
|
||||
return { source: "server", id: recordId };
|
||||
}
|
||||
}
|
||||
|
||||
const promise = saveGenerationRecordInternal(input);
|
||||
if (recordId) {
|
||||
inFlightSaves.set(recordId, promise);
|
||||
const saveKey = `${recordId}:${signature}`;
|
||||
inFlightSaves.set(saveKey, promise);
|
||||
void promise
|
||||
.then((result) => {
|
||||
if (result.source === "server") recentlySavedAt.set(recordId, Date.now());
|
||||
if (result.source === "server") recentlySavedRecords.set(recordId, { savedAt: Date.now(), signature });
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
inFlightSaves.delete(recordId);
|
||||
inFlightSaves.delete(saveKey);
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
|
||||
Reference in New Issue
Block a user