Compare commits
17 Commits
66b761314b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b72455062 | |||
| 526ad490f7 | |||
| 4993f6eeec | |||
| 79f220dbbf | |||
| c1c7cb3cc7 | |||
| b67f2e7601 | |||
| f056547160 | |||
| de3eb1d06a | |||
| f929be30ed | |||
| a2875738ce | |||
| 85adcdceef | |||
| ab99e3bf2f | |||
| e3b48e2614 | |||
| 5b316a2399 | |||
| 3f1954b38d | |||
| 96d335db8a | |||
| 307537a7ce |
@@ -0,0 +1 @@
|
||||
export * from "./aiGenerationClient.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./apiErrorUtils.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./generationRecordClient.ts";
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./serverConnection.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./taskSubscription.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./webGenerationGateway.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./toastStore.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ossAssets.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./workflows.ts";
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./EcommercePage.tsx";
|
||||
export * from "./EcommercePage.tsx";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
export * from "./ecommerceGenerationPersistence.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ecommerceImageValidation.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./ecommerceTemplates.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./workbenchDownload.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./useGenerationTasks.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./useTypewriter.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./backgroundTaskRunner.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./index.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./useAppStore.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./useGenerationStore.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./useProjectStore.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./useSessionStore.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./useTaskStore.ts";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2950,6 +2950,15 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] .clone-ai-result-stack > .clone-ai-node-label {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
z-index: 5;
|
||||
transform: translate(-50%, -100%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-clone-page[data-tool="clone"] .clone-ai-source-corner-action {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
@@ -7852,7 +7861,7 @@
|
||||
.product-set-preview-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100;
|
||||
z-index: 4000;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgb(17 24 39 / 58%);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./types.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./enterpriseVideoPolicy.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./happyHorseRouting.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./pixverseRouting.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./resolveVideoModel.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./taskLifecycle.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./translateTaskError.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./viduRouting.ts";
|
||||
Reference in New Issue
Block a user