音频提交上下文按任务 id 各自留存,重叠提交不再互相覆盖
- `resourceGenerationAudioSubmissionRef` 从单槽改成按任务 id 索引的 Map(键是 task id,值里 不再重复存 taskId) - 提交写入改成 `set(task.taskId, …)`,收口读取改成 `get(settlement.taskId)` + `delete`,切项目 改成 `clear()` - 原先两条音频提交重叠时,后一次提交会覆盖前一条的提交上下文,前一条「后端从未受理」的即时 失败因此找不到自己的恢复上下文,只走通用失败提示、丢掉「面板连原草稿带回来」 - 新增宿主用例「两条音频提交重叠:第一条未被受理时,它的面板仍会被带回来」:给测试宿主加 `holdFirstAssetGenerationStart` 开关,把第一次派发挂在受理前,复现两条提交重叠
This commit is contained in:
@@ -2080,18 +2080,26 @@ export default function ProjectDevelopmentView({
|
||||
new Map<string, ResourceEditRequestIdentity>(),
|
||||
);
|
||||
/**
|
||||
* 音频提交上下文:这次提交属于哪张占位。
|
||||
* 音频提交上下文:这次提交属于哪张占位。按**任务 id** 各自一条。
|
||||
*
|
||||
* 只有一件事要用它——**后端从未受理**的即时失败要把面板连原草稿、原请求身份带回来;受理
|
||||
* 之后才失败的收口只看账本,不需要这条本地上下文。`dispatchedImmediately` 沿用图片类口径:
|
||||
* 排在队列后面才派发的任务即使失败也不弹面板打断用户。
|
||||
*
|
||||
* 用 Map 而不是单槽:队列串行派发,第二张占位可以在第一条还在途时提交(它排在本地队列里),
|
||||
* 单槽会被后一次提交覆盖,于是前一条「从未受理」的即时失败找不到自己的恢复上下文,只剩通用
|
||||
* 失败提示、丢掉把面板连草稿带回来这一步。
|
||||
*/
|
||||
const resourceGenerationAudioSubmissionRef = useRef<{
|
||||
taskId: string;
|
||||
draftId: string;
|
||||
kind: ResourceCanvasGenerationKind;
|
||||
dispatchedImmediately: boolean;
|
||||
} | null>(null);
|
||||
const resourceGenerationAudioSubmissionRef = useRef(
|
||||
new Map<
|
||||
string,
|
||||
{
|
||||
draftId: string;
|
||||
kind: ResourceCanvasGenerationKind;
|
||||
dispatchedImmediately: boolean;
|
||||
}
|
||||
>(),
|
||||
);
|
||||
/** 用户主动收起浮层时留下的可再编辑草稿(按草稿 ID 记,提交成功后清掉)。 */
|
||||
const resourceGenerationDraftRef = useRef(
|
||||
new Map<
|
||||
@@ -7860,12 +7868,11 @@ export default function ProjectDevelopmentView({
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
prompt: input.prompt,
|
||||
});
|
||||
resourceGenerationAudioSubmissionRef.current = {
|
||||
taskId: task.taskId,
|
||||
resourceGenerationAudioSubmissionRef.current.set(task.taskId, {
|
||||
draftId,
|
||||
kind: input.kind,
|
||||
dispatchedImmediately,
|
||||
};
|
||||
});
|
||||
resourceGenerationDraftRef.current.set(draftId, {
|
||||
kind: input.kind,
|
||||
prompt: input.prompt,
|
||||
@@ -7959,9 +7966,11 @@ export default function ProjectDevelopmentView({
|
||||
// 账本已经把结果写在它自己的项目里,这里不再动当前项目的状态。
|
||||
return;
|
||||
}
|
||||
const audioSubmission = resourceGenerationAudioSubmissionRef.current;
|
||||
if (audioSubmission?.taskId === settlement.taskId) {
|
||||
resourceGenerationAudioSubmissionRef.current = null;
|
||||
const audioSubmission = resourceGenerationAudioSubmissionRef.current.get(
|
||||
settlement.taskId,
|
||||
);
|
||||
if (audioSubmission) {
|
||||
resourceGenerationAudioSubmissionRef.current.delete(settlement.taskId);
|
||||
if (
|
||||
settlement.status === 'failed' &&
|
||||
settlement.record === null &&
|
||||
@@ -8249,7 +8258,7 @@ export default function ProjectDevelopmentView({
|
||||
setResourceAssetGenerationPanel(null);
|
||||
// 收起时的草稿与音频提交身份同样是本项目内的记忆,换项目一并作废。
|
||||
resourceGenerationAudioRequestRef.current.clear();
|
||||
resourceGenerationAudioSubmissionRef.current = null;
|
||||
resourceGenerationAudioSubmissionRef.current.clear();
|
||||
resourceGenerationDraftRef.current.clear();
|
||||
resourceAssetGenerationDraftRef.current.clear();
|
||||
void (async () => {
|
||||
|
||||
@@ -102,6 +102,13 @@ function installHostTauri(options: {
|
||||
* 所以「未受理」与「已受理」两条路能在同一条用例里对照。
|
||||
*/
|
||||
assetGenerationStartError?: string;
|
||||
/**
|
||||
* 让**第一次** `start_local_project_asset_generation` 挂在半空(受理前)。
|
||||
*
|
||||
* 用来复现「两条音频提交重叠」:第一条还在等受理时提交第二条,第二条会排在本地队列里。
|
||||
* 用例用返回的 `releaseHeldAssetGenerationStart()` 决定第一条什么时候收到结果。
|
||||
*/
|
||||
holdFirstAssetGenerationStart?: boolean;
|
||||
}) {
|
||||
const layoutWrites: LayoutWrite[] = [];
|
||||
const deriveCalls: Array<Record<string, unknown>> = [];
|
||||
@@ -114,6 +121,7 @@ function installHostTauri(options: {
|
||||
const assetGenerationStartError = {
|
||||
current: options.assetGenerationStartError,
|
||||
};
|
||||
let releaseHeldStart: (() => void) | null = null;
|
||||
const positions: ProjectResourceCanvasPosition[] = [];
|
||||
|
||||
const invoke = vi.fn(
|
||||
@@ -192,7 +200,13 @@ function installHostTauri(options: {
|
||||
}
|
||||
if (command === 'start_local_project_asset_generation') {
|
||||
const startArgs = args ?? {};
|
||||
const isFirstStart = assetGenerationStarts.length === 0;
|
||||
assetGenerationStarts.push(structuredClone(startArgs));
|
||||
if (options.holdFirstAssetGenerationStart && isFirstStart) {
|
||||
await new Promise<void>((resolve) => {
|
||||
releaseHeldStart = resolve;
|
||||
});
|
||||
}
|
||||
if (assetGenerationStartError.current) {
|
||||
throw new Error(assetGenerationStartError.current);
|
||||
}
|
||||
@@ -257,6 +271,7 @@ function installHostTauri(options: {
|
||||
deriveCalls,
|
||||
assetGenerationStarts,
|
||||
assetGenerationStartError,
|
||||
releaseHeldAssetGenerationStart: () => releaseHeldStart?.(),
|
||||
unexpectedCommands,
|
||||
/**
|
||||
* 后台任务收口为完成:账本里那条记录变成 `completed` + `assetId`,素材同时进清单。
|
||||
@@ -672,6 +687,47 @@ describe('画布生成入口的宿主生命周期', () => {
|
||||
await waitFor(() => expect(floatingPanel()).toBeNull());
|
||||
}, 20_000);
|
||||
|
||||
test('两条音频提交重叠:第一条未被受理时,它的面板仍会被带回来', async () => {
|
||||
const tauri = installHostTauri({
|
||||
assets: [seedBgmAsset('seed-bgm')],
|
||||
assetGenerationStartError: '项目权限策略拒绝执行:asset.register',
|
||||
holdFirstAssetGenerationStart: true,
|
||||
});
|
||||
render(<HostWorkbench assets={[seedBgmAsset('seed-bgm')]} />);
|
||||
await settle();
|
||||
|
||||
// 第一条:背景音乐。派发挂在「受理」上,队列还没拿到它的终局。
|
||||
const { draftId } = await openBgmEntry();
|
||||
fireEvent.click(panelSubmitButton('生成背景音乐'));
|
||||
await settle();
|
||||
expect(tauri.assetGenerationStarts).toHaveLength(1);
|
||||
|
||||
// 第二条:音效(另一张占位)。第一条还在途,所以它排在本地队列里,还没有发 IPC。
|
||||
await openAudioTool('生成音效');
|
||||
fireEvent.change(
|
||||
floatingPanel()?.querySelector('textarea') as HTMLTextAreaElement,
|
||||
{ target: { value: '木门缓慢推开的吱呀声' } },
|
||||
);
|
||||
fireEvent.click(panelSubmitButton('生成音效'));
|
||||
await settle();
|
||||
expect(tauri.assetGenerationStarts).toHaveLength(1);
|
||||
|
||||
/*
|
||||
放开第一条:它这次从未被受理。提交上下文按**任务 id** 各自留存,所以后来的第二条不许把
|
||||
第一条的恢复上下文挤掉——面板必须连原草稿回到第一张占位。
|
||||
*/
|
||||
await act(async () => {
|
||||
tauri.releaseHeldAssetGenerationStart();
|
||||
await Promise.resolve();
|
||||
});
|
||||
await waitFor(() => expect(floatingPanel()).not.toBeNull());
|
||||
expect(floatingPanelPrompt()?.value ?? '').toBe('一段平静的夜晚钢琴曲');
|
||||
expect(
|
||||
placeholderElement(draftId)?.dataset
|
||||
.resourceCanvasGenerationPlaceholderStatus,
|
||||
).toBe('failed');
|
||||
}, 20_000);
|
||||
|
||||
test('失败后用同一份请求重试:复用同一个 operationId 与幂等键', async () => {
|
||||
const tauri = installHostTauri({
|
||||
assets: [seedBgmAsset('seed-bgm')],
|
||||
|
||||
Reference in New Issue
Block a user