Merge remote-tracking branch 'origin/pr-536'

This commit is contained in:
ddCat
2026-09-18 23:31:56 +08:00
3 changed files with 50 additions and 1 deletions

View File

@@ -0,0 +1,14 @@
export function createCreationSubmitGate() {
let locked = false;
return {
tryAcquire() {
if (locked) return false;
locked = true;
return true;
},
release() {
locked = false;
},
};
}

View File

@@ -33,6 +33,7 @@ import { defaultCreationMode, modeLabels, type CreationConversation, type Creati
import { attachCreationTaskContexts, completedCreationGenerationTask, conversationTimestamp, creationShotRail, creationVideoShotOrdinal, isImageAttachment, isVideoAttachment, materializeCreationTaskResults, newConversation, newMessage, reconcileCreationTaskMessages } from "./creation-conversations";
import { CreationComposer, CreationEmptySuggest, CreationFeaturedWorks, CreationHistoryDrawer, CreationMessageView, CreationModeTabs, CreationWorkspaceToolbar, creationAssetCategoryLabels } from "./creation-workspace";
import { CreationAgentEntry } from "./creation-agent-entry";
import { createCreationSubmitGate } from "./creation-submit-gate";
const AssetLibraryPickerModal = lazy(() => import("@/components/assets/asset-library-picker-modal").then((module) => ({ default: module.AssetLibraryPickerModal })));
const loadCreationRuntime = () => import("./creation-runtime");
@@ -113,6 +114,7 @@ export default function CreatePage() {
const taskSyncWarningRef = useRef(false);
const activeGenerationTaskIdsRef = useRef(new Set<string>());
const retryPreparingRef = useRef(new Set<string>());
const submitGateRef = useRef(createCreationSubmitGate());
const pendingRetryRef = useRef<{ context: CreationRetryContext; lockKey: string } | null>(null);
const [retrySequence, setRetrySequence] = useState(0);
const [composerPreferencesInitialized, setComposerPreferencesInitialized] = useState(false);
@@ -527,24 +529,33 @@ export default function CreatePage() {
const releaseRetryLock = () => {
if (retryLockKey) retryPreparingRef.current.delete(retryLockKey);
};
if (!submitGateRef.current.tryAcquire()) {
releaseRetryLock();
return;
}
const releaseSubmitGate = () => submitGateRef.current.release();
const text = prompt.trim();
if (!text || busy || !activeConversation) {
releaseRetryLock();
releaseSubmitGate();
return;
}
if (!selectedModel) {
toast.warning(`请先在设置中配置${modeLabels[mode]}模型`);
releaseRetryLock();
releaseSubmitGate();
return;
}
if (mode === "video" && !videoDurationAllowed(videoProfile, Number(seconds))) {
toast.error("当前模型不支持所选视频时长,请重新选择");
releaseRetryLock();
releaseSubmitGate();
return;
}
if (attachments.length > maxReferences) {
toast.warning("参考内容正在按当前模型能力调整,请稍后重试");
releaseRetryLock();
releaseSubmitGate();
return;
}
const settings = { ratio, seconds, quality, videoQuality, count };
@@ -559,7 +570,15 @@ export default function CreatePage() {
characterCount: 0,
});
const skillReferences = references.flatMap((reference) => (reference.skill ? [reference.skill] : []));
const runtime = await loadCreationRuntime();
let runtime: CreationRuntime;
try {
runtime = await loadCreationRuntime();
} catch (error) {
toast.error(error instanceof Error ? error.message : "生成运行时加载失败");
releaseRetryLock();
releaseSubmitGate();
return;
}
let skillExecution: Awaited<ReturnType<typeof runtime.skillRuntime.prepare>>;
try {
skillExecution = await runtime.skillRuntime.prepare({
@@ -571,6 +590,7 @@ export default function CreatePage() {
} catch (error) {
toast.error(error instanceof Error ? error.message : "技能上下文加载失败");
releaseRetryLock();
releaseSubmitGate();
return;
}
const expandedPrompt = skillExecution.prompt;
@@ -719,6 +739,7 @@ export default function CreatePage() {
for (const taskId of boundTaskIds) activeGenerationTaskIdsRef.current.delete(taskId);
requestLifecycle.release();
releaseRetryLock();
releaseSubmitGate();
if (abortRef.current === controller) {
abortRef.current = null;
setBusy(false);

View File

@@ -0,0 +1,14 @@
import { expect, test } from "bun:test";
import { createCreationSubmitGate } from "../src/pages/create/creation-submit-gate";
test("creation submit gate rejects a second submit until the first submit releases it", () => {
const gate = createCreationSubmitGate();
expect(gate.tryAcquire()).toBe(true);
expect(gate.tryAcquire()).toBe(false);
gate.release();
expect(gate.tryAcquire()).toBe(true);
});