Compare commits
18 Commits
3f1954b38d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b72455062 | |||
| 526ad490f7 | |||
| 4993f6eeec | |||
| 79f220dbbf | |||
| c1c7cb3cc7 | |||
| b67f2e7601 | |||
| f056547160 | |||
| de3eb1d06a | |||
| f929be30ed | |||
| a2875738ce | |||
| 85adcdceef | |||
| 66b761314b | |||
| ab99e3bf2f | |||
| e3b48e2614 | |||
| 9a9c7eb86d | |||
| 5b316a2399 | |||
| 48262d6233 | |||
| 062c8b3445 |
@@ -0,0 +1,43 @@
|
|||||||
|
# 自动检测文本文件并统一换行符
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# 源码强制使用 LF(跨平台一致)
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.mjs text eol=lf
|
||||||
|
*.cjs text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.svg text eol=lf
|
||||||
|
|
||||||
|
# 配置类(统一 LF)
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.toml text eol=lf
|
||||||
|
*.conf text eol=lf
|
||||||
|
|
||||||
|
# Windows 专用脚本保持 CRLF
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# 二进制文件,不做换行符转换
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.webp binary
|
||||||
|
*.ico binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.otf binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.pdf binary
|
||||||
|
*.zip binary
|
||||||
|
*.gz binary
|
||||||
@@ -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 合流 + 成功后短期拦截,
|
// 单个 poller 内也会重复触发。这里做客户端幂等:in-flight 合流 + 成功后短期拦截,
|
||||||
// 避免后端在缺少去重时插入重复记录。
|
// 避免后端在缺少去重时插入重复记录。
|
||||||
const inFlightSaves = new Map<string, Promise<SaveGenerationRecordResult>>();
|
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;
|
const SAVE_DEDUPE_WINDOW_MS = 60_000;
|
||||||
|
|
||||||
function pruneRecentlySaved(now: number): void {
|
function pruneRecentlySaved(now: number): void {
|
||||||
for (const [id, savedAt] of recentlySavedAt) {
|
for (const [id, record] of recentlySavedRecords) {
|
||||||
if (now - savedAt > SAVE_DEDUPE_WINDOW_MS) recentlySavedAt.delete(id);
|
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[] {
|
function readPendingRecords(): SaveGenerationRecordInput[] {
|
||||||
try {
|
try {
|
||||||
const raw = window.localStorage.getItem(PENDING_RECORDS_KEY);
|
const raw = window.localStorage.getItem(PENDING_RECORDS_KEY);
|
||||||
@@ -78,26 +103,29 @@ export async function saveGenerationRecord(input: SaveGenerationRecordInput): Pr
|
|||||||
pruneRecentlySaved(now);
|
pruneRecentlySaved(now);
|
||||||
|
|
||||||
const recordId = input.clientRecordId;
|
const recordId = input.clientRecordId;
|
||||||
|
const signature = buildSaveSignature(input);
|
||||||
if (recordId) {
|
if (recordId) {
|
||||||
const inFlight = inFlightSaves.get(recordId);
|
const saveKey = `${recordId}:${signature}`;
|
||||||
|
const inFlight = inFlightSaves.get(saveKey);
|
||||||
if (inFlight) return inFlight;
|
if (inFlight) return inFlight;
|
||||||
const savedAt = recentlySavedAt.get(recordId);
|
const savedRecord = recentlySavedRecords.get(recordId);
|
||||||
if (savedAt !== undefined && now - savedAt <= SAVE_DEDUPE_WINDOW_MS) {
|
if (savedRecord && savedRecord.signature === signature && now - savedRecord.savedAt <= SAVE_DEDUPE_WINDOW_MS) {
|
||||||
// 终态记录只需落库一次;窗口内的重复调用直接视为已保存。
|
// 相同 clientRecordId 且 payload 完全一致时才拦截;同一记录的多轮更新需要继续保存。
|
||||||
return { source: "server", id: recordId };
|
return { source: "server", id: recordId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = saveGenerationRecordInternal(input);
|
const promise = saveGenerationRecordInternal(input);
|
||||||
if (recordId) {
|
if (recordId) {
|
||||||
inFlightSaves.set(recordId, promise);
|
const saveKey = `${recordId}:${signature}`;
|
||||||
|
inFlightSaves.set(saveKey, promise);
|
||||||
void promise
|
void promise
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.source === "server") recentlySavedAt.set(recordId, Date.now());
|
if (result.source === "server") recentlySavedRecords.set(recordId, { savedAt: Date.now(), signature });
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
inFlightSaves.delete(recordId);
|
inFlightSaves.delete(saveKey);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return promise;
|
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";
|
||||||
@@ -381,108 +381,6 @@ export default function EcommerceClonePanel({
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{cloneOutput === "hot" ? (
|
|
||||||
<section className="clone-ai-replicate-panel" aria-label="爆款图复刻设置">
|
|
||||||
<div className="clone-ai-dynamic-head">
|
|
||||||
<strong>爆款图参考设置</strong>
|
|
||||||
<span>随生成模式切换</span>
|
|
||||||
</div>
|
|
||||||
<div className="clone-ai-replicate-section">
|
|
||||||
<span className="clone-ai-replicate-title">参考内容</span>
|
|
||||||
<div className="clone-ai-replicate-tabs" role="tablist" aria-label="参考内容来源">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cloneReferenceMode === "upload" ? "is-active" : ""}
|
|
||||||
aria-selected={cloneReferenceMode === "upload"}
|
|
||||||
onClick={() => setCloneReferenceMode("upload")}
|
|
||||||
>
|
|
||||||
上传参考图
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cloneReferenceMode === "link" ? "is-active" : ""}
|
|
||||||
aria-selected={cloneReferenceMode === "link"}
|
|
||||||
onClick={() => setCloneReferenceMode("link")}
|
|
||||||
>
|
|
||||||
导入链接
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{cloneReferenceMode === "upload" ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`clone-ai-replicate-upload${isCloneReferenceDragging ? " is-dragging" : ""}${cloneReferenceImages.length ? " has-files" : ""}`}
|
|
||||||
onClick={() => cloneReferenceInputRef.current?.click()}
|
|
||||||
onDragOver={handleCloneReferenceDragOver}
|
|
||||||
onDragLeave={handleCloneReferenceDragLeave}
|
|
||||||
onDrop={handleCloneReferenceDrop}
|
|
||||||
>
|
|
||||||
{cloneReferenceImages.length ? (
|
|
||||||
<>
|
|
||||||
<div className="clone-ai-replicate-files">
|
|
||||||
{cloneReferenceImages.map((item) => (
|
|
||||||
<figure
|
|
||||||
key={item.id}
|
|
||||||
className="clone-ai-replicate-file"
|
|
||||||
onMouseEnter={(e) => handleFileMouseEnter(item.src, e)}
|
|
||||||
onMouseLeave={handleFileMouseLeave}
|
|
||||||
>
|
|
||||||
<img src={item.src} alt="" />
|
|
||||||
</figure>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<span className="clone-ai-replicate-add-more">
|
|
||||||
<CloudUploadOutlined />
|
|
||||||
点击继续上传文件
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
<CloudUploadOutlined />
|
|
||||||
<span className="clone-ai-replicate-upload-text">拖拽或点击上传参考图</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<em>{cloneReferenceImages.length ? `已选 ${cloneReferenceImages.length}/${maxCloneReferenceImages}` : `最多 ${maxCloneReferenceImages} 张`}</em>
|
|
||||||
{isCloneReferenceDragging ? (
|
|
||||||
<div className="clone-ai-replicate-upload-overlay">
|
|
||||||
<CloudUploadOutlined />
|
|
||||||
<span>释放文件以上传</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<label className="clone-ai-replicate-link">
|
|
||||||
<input placeholder="粘贴商品图或详情页链接" />
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
ref={cloneReferenceInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/jpeg,image/png,image/webp"
|
|
||||||
multiple
|
|
||||||
onChange={handleCloneReferenceUpload}
|
|
||||||
aria-label="上传参考图片"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="clone-ai-replicate-section">
|
|
||||||
<span className="clone-ai-replicate-title">复刻程度</span>
|
|
||||||
<div className="clone-ai-replicate-levels" role="toolbar" aria-label="复刻程度">
|
|
||||||
{cloneReplicateLevelOptions.map((option) => (
|
|
||||||
<button
|
|
||||||
key={option.key}
|
|
||||||
type="button"
|
|
||||||
className={cloneReplicateLevel === option.key ? "is-active" : ""}
|
|
||||||
aria-pressed={cloneReplicateLevel === option.key}
|
|
||||||
onClick={() => setCloneReplicateLevel(option.key)}
|
|
||||||
>
|
|
||||||
<strong>{option.title}</strong>
|
|
||||||
<span>{option.desc}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{cloneOutput === "set" ? (
|
{cloneOutput === "set" ? (
|
||||||
<section className="clone-ai-count-panel" aria-label="套图图片数量">
|
<section className="clone-ai-count-panel" aria-label="套图图片数量">
|
||||||
<div className="clone-ai-dynamic-head">
|
<div className="clone-ai-dynamic-head">
|
||||||
|
|||||||
@@ -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";
|
||||||
+2161
-10
File diff suppressed because it is too large
Load Diff
@@ -7861,7 +7861,7 @@
|
|||||||
.product-set-preview-backdrop {
|
.product-set-preview-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 100;
|
z-index: 4000;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: rgb(17 24 39 / 58%);
|
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