卡片浮层改为提交即关:快速编辑 / 生成动画只负责交任务
- 快速编辑与生成动画点提交当帧即关面板(不等 IPC、不留等待态),进度、阶段与结果只由「生成任务」侧栏承载,与图片类生成面板同一口径 - 受理之后的失败不再重开面板:侧栏那条收口为失败 + 提示条,草稿回落成「未完成编辑」等用户重开;只有点击瞬间就失败(名称预检、源素材不在画布、无客户端)留在面板里 - 派生请求身份改为按「入口 + 资源路径」记忆:重开面板再提交时提示词没变就沿用同一 operationId / 幂等键,命中同一 operation 账本;派生成成功后清除 - 删除 canDismissResourceCanvasQuickEdit 与宿主里只为它服务的两块面板状态 ref,clearResourceCanvasFocus 回到「清选中 + 关两块浮层」的直线逻辑 - 三条既有用例按新口径改写为「重开面板再重试,身份不变」;PRD §3.10 与 decision-log 同步
This commit is contained in:
+3
-15
@@ -48,9 +48,9 @@ export function isResourceCanvasPanTarget(
|
||||
if (!isResourceCanvasInteractionTarget(target)) return true;
|
||||
return Boolean(
|
||||
target.closest('.game-resource-card') &&
|
||||
!target.closest(
|
||||
'button:not(.game-resource-card-select), [role="button"], input, textarea, select, a, audio, video',
|
||||
),
|
||||
!target.closest(
|
||||
'button:not(.game-resource-card-select), [role="button"], input, textarea, select, a, audio, video',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,18 +85,6 @@ export function isResourceCanvasWheelOverlayTarget(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布浮层是否可以被清焦点顺手关掉。
|
||||
*
|
||||
* 对齐美术画布 `closeTransientEditorPanels()`:生成中的面板不关,否则用户看不到
|
||||
* 「修改中」状态,也拿不到失败后的重试入口。信息浮层是只读的,没有生成态。
|
||||
*/
|
||||
export function canDismissResourceCanvasQuickEdit(
|
||||
panel: { readonly status: string } | null | undefined,
|
||||
): boolean {
|
||||
return panel?.status !== 'generating';
|
||||
}
|
||||
|
||||
/** 资源画布上的 AGC 自有弹层:它们开着时画布浮层不参与「点外部关闭」。 */
|
||||
export type ResourceCanvasHostOverlayState = {
|
||||
readonly isResourcePanelOpen: boolean;
|
||||
|
||||
@@ -166,7 +166,6 @@ import {
|
||||
} from '../../features/resource-canvas/resourceCanvasBottomToolbarModel';
|
||||
import { ResourceCanvasBottomToolbarView } from '../../features/resource-canvas/ResourceCanvasBottomToolbarView';
|
||||
import {
|
||||
canDismissResourceCanvasQuickEdit,
|
||||
isResourceCanvasInteractionTarget,
|
||||
isResourceCanvasPanTarget,
|
||||
isResourceCanvasWheelOverlayTarget,
|
||||
@@ -573,6 +572,20 @@ function pendingResourceEditPhaseLabel(phase: string) {
|
||||
return RESOURCE_CANVAS_RESOURCE_EDIT_PHASE_LABELS[phase] ?? '等待恢复';
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片浮层的派生请求身份表键:**入口 + 资源路径**。
|
||||
*
|
||||
* 两块浮层都是「提交即关」,重试的唯一路径是重开面板再提交;身份不跟着资源走的话,重试会变成
|
||||
* 一条全新的 operation(原生账本认不出同一件事,还可能重复计费)。同一张素材上的两种入口
|
||||
* (快速编辑 / 生成动画)是两条不同的派生请求,所以键里带上入口。
|
||||
*/
|
||||
function resourceEditRequestKey(
|
||||
kind: 'quick-edit' | 'character-animation',
|
||||
resourcePath: string,
|
||||
): string {
|
||||
return `${kind}:${resourcePath}`;
|
||||
}
|
||||
|
||||
function pendingResourceEditCreatedAtLabel(createdAt: number) {
|
||||
if (!Number.isSafeInteger(createdAt) || createdAt < 0) return '时间未知';
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
@@ -2372,13 +2385,15 @@ export default function ProjectDevelopmentView({
|
||||
/** 「生成动画」的请求身份,与快速编辑同一套复用/重铸口径。 */
|
||||
const resourceCharacterAnimationRequestRef =
|
||||
useRef<ResourceDeriveRequest | null>(null);
|
||||
/** 快速编辑面板的最新状态:清焦点回调只在事件里读,不随面板状态反复重建。 */
|
||||
const quickEditPanelRef = useRef<QuickEditPanelState | null>(null);
|
||||
quickEditPanelRef.current = quickEditPanel;
|
||||
/** 「生成动画」面板的最新状态,读法同上。 */
|
||||
const characterAnimationPanelRef =
|
||||
useRef<CharacterAnimationPanelState | null>(null);
|
||||
characterAnimationPanelRef.current = characterAnimationPanel;
|
||||
/**
|
||||
* 资源路径 → 这块资源上最后一次派生请求的身份(按入口分键,见 `resourceEditRequestKey`)。
|
||||
*
|
||||
* 卡片浮层提交即关,用户重试只能「重开面板再提交」;身份不跟着资源走的话,那次重试就成了
|
||||
* 一条全新的 operation。派生成成功即清掉:下一次编辑本来就该是新的一笔。
|
||||
*/
|
||||
const resourceEditLastRequestsRef = useRef(
|
||||
new Map<string, ResourceDeriveRequest>(),
|
||||
);
|
||||
/**
|
||||
* 打开中的快速编辑面板,这一笔草稿挂在哪个资源路径下。
|
||||
*
|
||||
@@ -2425,23 +2440,16 @@ export default function ProjectDevelopmentView({
|
||||
* 清资源画布焦点:对齐美术画布 `clearCanvasFocus()`。
|
||||
*
|
||||
* 美术画布在画布空白 pointerdown 与 Escape 两处都会走这一步——清空选中让选中工具条
|
||||
* 收起,并关掉画布浮层(快速编辑与信息)。资源画布保持同一套时机:点在资源卡或浮层里
|
||||
* 不清,点画布空白、按 Esc 才清。生成中的快速编辑面板不关,与美术画布
|
||||
* `closeTransientEditorPanels()` 的 `status === 'generating'` 判断一致;信息浮层只读,
|
||||
* 没有生成态,随焦点一起收起。
|
||||
* 收起,并关掉画布浮层(快速编辑 / 生成动画 / 信息)。资源画布保持同一套时机:点在资源卡
|
||||
* 或浮层里不清,点画布空白、按 Esc 才清。
|
||||
*
|
||||
* 这里的浮层一律可收:卡片浮层只负责**提交任务**,提交当帧自己就关了(任务进「生成任务」
|
||||
* 侧栏跟踪),所以不存在「生成中必须留在画布上」的那一档。
|
||||
*/
|
||||
const clearResourceCanvasFocus = useCallback(() => {
|
||||
setSelectedResourceIds([]);
|
||||
setResourceInfoResourceId(null);
|
||||
if (!canDismissResourceCanvasQuickEdit(quickEditPanelRef.current)) {
|
||||
return;
|
||||
}
|
||||
closeResourceQuickEditPanel();
|
||||
// 「生成动画」与快速编辑同样是挂在资源卡旁的浮层,走同一套「点外部 / Esc 关闭」
|
||||
// 时机与「生成中不关」判据,否则按下 Esc 会只收起一个浮层。
|
||||
if (characterAnimationPanelRef.current?.status === 'generating') {
|
||||
return;
|
||||
}
|
||||
closeResourceCharacterAnimationPanel();
|
||||
}, [closeResourceCharacterAnimationPanel, closeResourceQuickEditPanel]);
|
||||
|
||||
@@ -7539,12 +7547,21 @@ export default function ProjectDevelopmentView({
|
||||
resourceQuickEditDraftPathRef.current = resource.path;
|
||||
setQuickEditPanel(restoredPanelDraft);
|
||||
setQuickEditReferences(remembered ? [...remembered.references] : []);
|
||||
resourceQuickEditRequestRef.current = {
|
||||
...createResourceEditRequestIdentity(restoredPanelDraft.prompt),
|
||||
sourceLayerId: layer.id,
|
||||
normalizedAssetId: null,
|
||||
expectedProjectRevision: null,
|
||||
};
|
||||
/*
|
||||
身份跟着**资源**走:上一次尝试失败后重开面板,提示词没变就沿用同一 operationId / 幂等键,
|
||||
重试命中同一 operation 账本(提交时由 `resolveResourceEditRequestIdentity` 再判一次)。
|
||||
*/
|
||||
const rememberedRequest = resourceEditLastRequestsRef.current.get(
|
||||
resourceEditRequestKey('quick-edit', resource.path),
|
||||
);
|
||||
resourceQuickEditRequestRef.current = rememberedRequest
|
||||
? { ...rememberedRequest, sourceLayerId: layer.id }
|
||||
: {
|
||||
...createResourceEditRequestIdentity(restoredPanelDraft.prompt),
|
||||
sourceLayerId: layer.id,
|
||||
normalizedAssetId: null,
|
||||
expectedProjectRevision: null,
|
||||
};
|
||||
setResourceWorkbenchNotice('');
|
||||
},
|
||||
[applyResourceCardFloatingPanelOpen, canvasResources],
|
||||
@@ -7798,30 +7815,36 @@ export default function ProjectDevelopmentView({
|
||||
const actionProject = { projectPath, projectId: manifest.projectId };
|
||||
const flowId = crypto.randomUUID();
|
||||
const sourceLayerId = layer.id;
|
||||
const previousRequest = resourceQuickEditRequestRef.current;
|
||||
const requestStoreKey = resourceEditRequestKey('quick-edit', resource.path);
|
||||
const previousRequest =
|
||||
resourceEditLastRequestsRef.current.get(requestStoreKey) ?? null;
|
||||
// 请求身份只对它铸造时的那句提示词有效:Rust 的 request_fingerprint 含 prompt
|
||||
// (resource_editor.rs:807-827),改了提示词还拿旧 operationId 重试会被判
|
||||
// 「operationId 或幂等键已绑定到不同资源编辑请求」(同文件 :5216-5225)。
|
||||
// 提示词没变时仍复用,失败重试照旧命中同一 operation 账本;
|
||||
// 正规化结果与 revision 是别的事实,换身份时原样保留。
|
||||
const request =
|
||||
previousRequest?.sourceLayerId === sourceLayerId
|
||||
? {
|
||||
...previousRequest,
|
||||
...resolveResourceEditRequestIdentity(previousRequest, prompt),
|
||||
}
|
||||
: {
|
||||
...createResourceEditRequestIdentity(prompt),
|
||||
sourceLayerId,
|
||||
normalizedAssetId: null,
|
||||
expectedProjectRevision: null,
|
||||
};
|
||||
const request = previousRequest
|
||||
? {
|
||||
...previousRequest,
|
||||
...resolveResourceEditRequestIdentity(previousRequest, prompt),
|
||||
}
|
||||
: {
|
||||
...createResourceEditRequestIdentity(prompt),
|
||||
sourceLayerId,
|
||||
normalizedAssetId: null,
|
||||
expectedProjectRevision: null,
|
||||
};
|
||||
resourceQuickEditRequestRef.current = request;
|
||||
setQuickEditPanel((current) =>
|
||||
current
|
||||
? { ...current, status: 'generating', errorMessage: undefined }
|
||||
: current,
|
||||
);
|
||||
resourceEditLastRequestsRef.current.set(requestStoreKey, request);
|
||||
/*
|
||||
提交即关面板:这块浮层只负责「把这次修改交出去」,交出去之后它的生命周期就结束了——
|
||||
进度、阶段与结果都在「生成任务」侧栏里跟,画布上不再挂一块等结果的板子。失败也不把面板
|
||||
拉回来:异步失败会回落成未完成草稿(恢复入口)+ 侧栏一条失败 + 提示条;只有**点击瞬间**
|
||||
就能判定的失败(名称预检、源素材不在画布上、没有客户端)才留在面板里。
|
||||
*/
|
||||
setQuickEditPanel(null);
|
||||
setQuickEditSourceLayer(null);
|
||||
resourceQuickEditDraftPathRef.current = null;
|
||||
// 提交当帧就上「生成任务」侧栏:原生账本要等 Rust 建好 ledger 才读得到,中间那段空白
|
||||
// 正是验收现场看到的「点了生成,生成任务里什么都没有」。
|
||||
beginResourceEditSubmission({
|
||||
@@ -7891,13 +7914,12 @@ export default function ProjectDevelopmentView({
|
||||
// 正式的可恢复事实已经由原 operation 账本接管,本地草稿再留着只会在恢复入口里
|
||||
// 多出一条重复条目。键是源素材路径,正规化换掉卡片 id 也不影响这一笔的归属。
|
||||
discardResourceQuickEditDraft(resource.path);
|
||||
resourceQuickEditDraftPathRef.current = null;
|
||||
// 这一笔已经落定:下一次编辑该是新的一笔,不再复用它的身份。
|
||||
resourceEditLastRequestsRef.current.delete(requestStoreKey);
|
||||
settleResourceEditSubmission(request.operationId, {
|
||||
status: 'completed',
|
||||
assetId: result.asset.id,
|
||||
});
|
||||
setQuickEditPanel(null);
|
||||
setQuickEditSourceLayer(null);
|
||||
resourceQuickEditRequestRef.current = null;
|
||||
setResourceWorkbenchNotice('快速编辑已产出新素材,正在同步资源与布局…');
|
||||
} catch (error) {
|
||||
@@ -7906,15 +7928,9 @@ export default function ProjectDevelopmentView({
|
||||
status: 'failed',
|
||||
error: message,
|
||||
});
|
||||
setQuickEditPanel((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
status: 'failed',
|
||||
errorMessage: message,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
resourceQuickEditRequestRef.current = null;
|
||||
// 面板已经关了:失败只能落在提示条与「生成任务」侧栏上,草稿留在原处等重开。
|
||||
setResourceWorkbenchNotice(`快速编辑失败:${message}`);
|
||||
}
|
||||
}, [
|
||||
beginResourceEditSubmission,
|
||||
@@ -7960,12 +7976,18 @@ export default function ProjectDevelopmentView({
|
||||
...panelDraft,
|
||||
resolution: RESOURCE_CHARACTER_ANIMATION_RESOLUTION,
|
||||
});
|
||||
resourceCharacterAnimationRequestRef.current = {
|
||||
...createResourceEditRequestIdentity(''),
|
||||
sourceLayerId: layer.id,
|
||||
normalizedAssetId: null,
|
||||
expectedProjectRevision: null,
|
||||
};
|
||||
// 与快速编辑同口径:失败后重开面板,提示词没变就沿用同一 operation 身份。
|
||||
const rememberedRequest = resourceEditLastRequestsRef.current.get(
|
||||
resourceEditRequestKey('character-animation', resource.path),
|
||||
);
|
||||
resourceCharacterAnimationRequestRef.current = rememberedRequest
|
||||
? { ...rememberedRequest, sourceLayerId: layer.id }
|
||||
: {
|
||||
...createResourceEditRequestIdentity(''),
|
||||
sourceLayerId: layer.id,
|
||||
normalizedAssetId: null,
|
||||
expectedProjectRevision: null,
|
||||
};
|
||||
setResourceWorkbenchNotice('');
|
||||
},
|
||||
[applyResourceCardFloatingPanelOpen, canvasResources],
|
||||
@@ -8064,27 +8086,30 @@ export default function ProjectDevelopmentView({
|
||||
const actionProject = { projectPath, projectId: manifest.projectId };
|
||||
const flowId = crypto.randomUUID();
|
||||
const sourceLayerId = layer.id;
|
||||
const previousRequest = resourceCharacterAnimationRequestRef.current;
|
||||
const requestStoreKey = resourceEditRequestKey(
|
||||
'character-animation',
|
||||
resource.path,
|
||||
);
|
||||
const previousRequest =
|
||||
resourceEditLastRequestsRef.current.get(requestStoreKey) ?? null;
|
||||
// 与快速编辑同一套口径:提示词没变就复用 operationId / 幂等键,失败重试命中同一
|
||||
// operation 账本;改了提示词必须重铸,否则 Rust 的 request_fingerprint 会判不一致。
|
||||
const request =
|
||||
previousRequest?.sourceLayerId === sourceLayerId
|
||||
? {
|
||||
...previousRequest,
|
||||
...resolveResourceEditRequestIdentity(previousRequest, prompt),
|
||||
}
|
||||
: {
|
||||
...createResourceEditRequestIdentity(prompt),
|
||||
sourceLayerId,
|
||||
normalizedAssetId: null,
|
||||
expectedProjectRevision: null,
|
||||
};
|
||||
const request = previousRequest
|
||||
? {
|
||||
...previousRequest,
|
||||
...resolveResourceEditRequestIdentity(previousRequest, prompt),
|
||||
}
|
||||
: {
|
||||
...createResourceEditRequestIdentity(prompt),
|
||||
sourceLayerId,
|
||||
normalizedAssetId: null,
|
||||
expectedProjectRevision: null,
|
||||
};
|
||||
resourceCharacterAnimationRequestRef.current = request;
|
||||
setCharacterAnimationPanel((current) =>
|
||||
current
|
||||
? { ...current, status: 'generating', errorMessage: undefined }
|
||||
: current,
|
||||
);
|
||||
resourceEditLastRequestsRef.current.set(requestStoreKey, request);
|
||||
// 与快速编辑同口径:提交即关面板,进度与结果都在「生成任务」侧栏里跟。
|
||||
setCharacterAnimationPanel(null);
|
||||
setCharacterAnimationSourceLayer(null);
|
||||
// 与快速编辑同口径:这一笔从按下按钮那一刻就进「生成任务」侧栏。
|
||||
beginResourceEditSubmission({
|
||||
operationId: request.operationId,
|
||||
@@ -8153,6 +8178,8 @@ export default function ProjectDevelopmentView({
|
||||
setCharacterAnimationPanel(null);
|
||||
setCharacterAnimationSourceLayer(null);
|
||||
resourceCharacterAnimationRequestRef.current = null;
|
||||
// 落定后不再复用这笔身份:下一次编辑是新的一笔。
|
||||
resourceEditLastRequestsRef.current.delete(requestStoreKey);
|
||||
settleResourceEditSubmission(request.operationId, {
|
||||
status: 'completed',
|
||||
assetId: result.asset.id,
|
||||
@@ -8164,15 +8191,9 @@ export default function ProjectDevelopmentView({
|
||||
status: 'failed',
|
||||
error: message,
|
||||
});
|
||||
setCharacterAnimationPanel((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
status: 'failed',
|
||||
errorMessage: message,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
resourceCharacterAnimationRequestRef.current = null;
|
||||
// 面板已经关了:失败落在提示条与「生成任务」侧栏上,重开面板再试命中同一 operation。
|
||||
setResourceWorkbenchNotice(`生成动画失败:${message}`);
|
||||
}
|
||||
}, [
|
||||
beginResourceEditSubmission,
|
||||
|
||||
@@ -841,8 +841,25 @@ describe('project resource live canvas integration', () => {
|
||||
'把角色头发设定改为红色',
|
||||
);
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
expect(await screen.findByRole('alert')).not.toBeNull();
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
// 提交即关面板:失败也不把面板拉回来,重试只能重开面板(任务与失败都在「生成任务」侧栏里)。
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull(),
|
||||
);
|
||||
fireEvent.click(await findResourceSelectButton('source-art.png'));
|
||||
const retryToolbar = await screen.findByRole('toolbar', {
|
||||
name: '图片工具栏',
|
||||
});
|
||||
fireEvent.click(
|
||||
within(retryToolbar).getByRole('button', { name: '快速编辑' }),
|
||||
);
|
||||
const retryPanel = await screen.findByRole('dialog', {
|
||||
name: '快速编辑图片',
|
||||
});
|
||||
// 草稿回填,提示词没变 → 沿用上一次那笔 operation 身份。
|
||||
expect(
|
||||
within(retryPanel).getByLabelText('快速编辑提示词').textContent,
|
||||
).toContain('把角色头发设定改为红色');
|
||||
fireEvent.click(within(retryPanel).getByRole('button', { name: '修改' }));
|
||||
|
||||
await waitFor(() => expect(deriveCalls).toHaveLength(2));
|
||||
expect(deriveCalls[0]?.operationId).toBe(deriveCalls[1]?.operationId);
|
||||
@@ -988,13 +1005,28 @@ describe('project resource live canvas integration', () => {
|
||||
'把角色头发改成红色',
|
||||
);
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
expect(await screen.findByRole('alert')).not.toBeNull();
|
||||
// 提交即关面板,重开面板继续改:草稿回填的是原文。
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull(),
|
||||
);
|
||||
fireEvent.click(await findResourceSelectButton('source-art.png'));
|
||||
const retryToolbar = await screen.findByRole('toolbar', {
|
||||
name: '图片工具栏',
|
||||
});
|
||||
fireEvent.click(
|
||||
within(retryToolbar).getByRole('button', { name: '快速编辑' }),
|
||||
);
|
||||
const retryPanel = await screen.findByRole('dialog', {
|
||||
name: '快速编辑图片',
|
||||
});
|
||||
|
||||
// 失败后提示词仍可改:润色一次,回填的是润色结果,而不是原文。
|
||||
fireEvent.click(within(panel).getByRole('button', { name: 'AI 润色' }));
|
||||
fireEvent.click(
|
||||
within(retryPanel).getByRole('button', { name: 'AI 润色' }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(panel).getByLabelText('快速编辑提示词').textContent,
|
||||
within(retryPanel).getByLabelText('快速编辑提示词').textContent,
|
||||
).toContain('亮红色');
|
||||
});
|
||||
expect(polishCalls[0]).toMatchObject({ prompt: '把角色头发改成红色' });
|
||||
@@ -1002,7 +1034,7 @@ describe('project resource live canvas integration', () => {
|
||||
'图片素材的快速编辑提示词',
|
||||
);
|
||||
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
fireEvent.click(within(retryPanel).getByRole('button', { name: '修改' }));
|
||||
await waitFor(() => expect(deriveCalls).toHaveLength(2));
|
||||
// Rust 的 request_fingerprint 含 prompt:提示词换过就必须换 operationId / 幂等键,
|
||||
// 否则后端会判「已绑定到不同资源编辑请求」。
|
||||
@@ -1031,17 +1063,32 @@ describe('project resource live canvas integration', () => {
|
||||
'把角色头发改成红色',
|
||||
);
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
expect(await screen.findByRole('alert')).not.toBeNull();
|
||||
// 提交即关面板:重开面板后提示词仍是原文(草稿回填),再改再润色都在这一份草稿上。
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull(),
|
||||
);
|
||||
fireEvent.click(await findResourceSelectButton('source-art.png'));
|
||||
const retryToolbar = await screen.findByRole('toolbar', {
|
||||
name: '图片工具栏',
|
||||
});
|
||||
fireEvent.click(
|
||||
within(retryToolbar).getByRole('button', { name: '快速编辑' }),
|
||||
);
|
||||
const retryPanel = await screen.findByRole('dialog', {
|
||||
name: '快速编辑图片',
|
||||
});
|
||||
|
||||
fireEvent.click(within(panel).getByRole('button', { name: 'AI 润色' }));
|
||||
fireEvent.click(
|
||||
within(retryPanel).getByRole('button', { name: 'AI 润色' }),
|
||||
);
|
||||
expect(
|
||||
await within(panel).findByText('AI 润色失败,可重试'),
|
||||
await within(retryPanel).findByText('AI 润色失败,可重试'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
within(panel).getByLabelText('快速编辑提示词').textContent,
|
||||
within(retryPanel).getByLabelText('快速编辑提示词').textContent,
|
||||
).toContain('把角色头发改成红色');
|
||||
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
fireEvent.click(within(retryPanel).getByRole('button', { name: '修改' }));
|
||||
await waitFor(() => expect(deriveCalls).toHaveLength(2));
|
||||
// 提示词没变,身份也不该变:重试仍然命中同一 operation 账本。
|
||||
expect(deriveCalls[1]?.operationId).toBe(deriveCalls[0]?.operationId);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
useImageCanvasFloatingOptionDismiss,
|
||||
} from '../../../src/components/image-editor/useImageCanvasFloatingOptionDismiss';
|
||||
import {
|
||||
canDismissResourceCanvasQuickEdit,
|
||||
isResourceCanvasHostOverlayOpen,
|
||||
isResourceCanvasInteractionTarget,
|
||||
isResourceCanvasPanTarget,
|
||||
@@ -134,15 +133,6 @@ describe('resourceCanvasFocusModel', () => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
test('快速编辑生成中不参与清焦点', () => {
|
||||
expect(canDismissResourceCanvasQuickEdit(null)).toBe(true);
|
||||
expect(canDismissResourceCanvasQuickEdit({ status: 'idle' })).toBe(true);
|
||||
expect(canDismissResourceCanvasQuickEdit({ status: 'failed' })).toBe(true);
|
||||
expect(canDismissResourceCanvasQuickEdit({ status: 'generating' })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('AGC 自有弹层开着时画布浮层不接管点外部关闭', () => {
|
||||
const closed = {
|
||||
isResourcePanelOpen: false,
|
||||
|
||||
@@ -561,7 +561,11 @@ describe('快速编辑草稿的保留与恢复', () => {
|
||||
'把夜色改成星空',
|
||||
);
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '修改' }));
|
||||
// 先正规化成正式素材、再派生失败:卡片已经重投影,面板仍停在失败态。
|
||||
// 提交即关面板:面板只负责把任务交出去,失败也不再把它拉回来(回落成草稿 + 提示条)。
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull(),
|
||||
);
|
||||
// 先正规化成正式素材、再派生失败:卡片已经重投影。
|
||||
await waitFor(() => expect(normalizeCalls).toHaveLength(1));
|
||||
await waitFor(() => expect(deriveCalls).toHaveLength(1));
|
||||
await waitFor(() =>
|
||||
@@ -576,9 +580,27 @@ describe('快速编辑草稿的保留与恢复', () => {
|
||||
'[data-resource-card-id="task:canvas-task:assets/generated/task-art.png"]',
|
||||
),
|
||||
).toBeNull();
|
||||
expect(await within(panel).findByRole('alert')).not.toBeNull();
|
||||
|
||||
await dismissPanel();
|
||||
// 失败可见:这一笔在「生成任务」侧栏里收口为失败并带上原因(面板已经关了,结果只在这里)。
|
||||
fireEvent.click(await screen.findByRole('button', { name: '生成任务' }));
|
||||
const taskSidebar = await screen.findByRole('region', { name: '生成任务' });
|
||||
expect(within(taskSidebar).getByText('失败')).not.toBeNull();
|
||||
expect(
|
||||
within(taskSidebar).getAllByText(/result-unknown: 测试网络中断/u).length,
|
||||
).toBeGreaterThan(0);
|
||||
// 草稿回到「未完成编辑」等重开。
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '管理未完成编辑 (1)' }),
|
||||
);
|
||||
expect(
|
||||
within(
|
||||
await screen.findByRole('dialog', { name: '未完成的资源编辑' }),
|
||||
).getByRole('button', { name: '继续编辑' }),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(
|
||||
within(
|
||||
screen.getByRole('dialog', { name: '未完成的资源编辑' }),
|
||||
).getByRole('button', { name: '关闭资源编辑恢复面板' }),
|
||||
);
|
||||
|
||||
// 旧卡片已经不在画布上:草稿按**路径**归属,新投影的正式素材卡照样命中,否则这一笔
|
||||
// 就再也点不到。
|
||||
|
||||
@@ -171,7 +171,8 @@
|
||||
- 前置规范图:`start_local_project_asset_generation` 的 `ui-prototype` 与 `art-spritesheet` 要求项目里已有登记并绑定当前账号的 `assets/art-spec.png`(`kind: icon-spec`、图片媒体类型、来源为画布)。**后台化没有改这条前置判据**:Rust 侧仍是同一口径,前端 `projectHasIconSpecReference` 仍与它逐字对齐,缺前置时对应入口**保持可点击**并给出可执行的原因与指路(仓库口径:视觉不可用但可点击说明原因,不用原生 `disabled`),原因文案包含 `assets/art-spec.png` 与「生成规范 → 图标规范」。
|
||||
- 权威规范图由工具栏自身可满足:「图标规范」入口在项目尚无权威规范图时按 `assets/art-spec.png` 落盘;已有权威规范图时**不再传 `outputPath`**(Rust 侧 `replace_existing` 固定为 `false`,指向已存在文件会被硬拒绝「outputPath 已存在,禁止静默覆盖」),改为生成一张新的普通图标规范资产。这条前端策略不扩接口。
|
||||
- 音频入口收敛:音频画布的「生成背景音乐 / 生成音效」由工具栏承载,既有「生成素材」浮层入口**只保留生成视频**(视频能力不删,只是不出现在工具栏里)。
|
||||
- 派生/修改类任务并入同一侧栏(2026-09-20):快速编辑、生成动画、视频 / 音效 / 背景音乐与抠图这类走**资源编辑链路**的任务也进「生成任务」侧栏。数据源 = 原生资源编辑账本 `list_pending_local_project_resource_edits` + **本会话的本地提交记录**,按 `operationId` 合并去重:本地那份负责「按下提交当帧可见」,原生那份负责「重开项目、别处提交也看得见,阶段文案以后端为首」。这类任务不写图片类生成账本(`.agent/runtime/asset-generation-tasks/tasks.json`),也不改原生资源编辑账本 schema。配套两条口径:提交期间该笔不再列进「管理未完成编辑」(失败后自动回落成未完成草稿,提示词不丢);同一张素材还有在途提交时,重开面板再提交会被拦下并给出原因,避免重复派生与重复计费。
|
||||
- 派生/修改类任务并入同一侧栏(2026-09-20):快速编辑、生成动画、视频 / 音效 / 背景音乐与抠图这类走**资源编辑链路**的任务也进「生成任务」侧栏。数据源 = 原生资源编辑账本 `list_pending_local_project_resource_edits` + **本会话的本地提交记录**,按 `operationId` 合并去重:本地那份负责「按下提交当帧可见」,原生那份负责「重开项目、别处提交也看得见,阶段文案以后端为首」。这类任务不写图片类生成账本(`.agent/runtime/asset-generation-tasks/tasks.json`),也不改原生资源编辑账本 schema。
|
||||
- 卡片浮层同一条「提交即关」(2026-09-21):快速编辑 / 生成动画这类挂在资源卡上的浮层只负责**把任务交出去**,点提交当帧即关面板(不等 IPC),进度、阶段与结果都在「生成任务」侧栏里跟;面板内不出现「修改中 / 生成中」这类等待态。只有**点击瞬间就失败**(名称预检、源素材不在画布上、没有客户端)留在面板里可见;**受理之后才失败**只把侧栏那条收口为失败 + 提示条,不重开面板,草稿回落成「未完成编辑」等用户重开。因此原有的「生成中的卡片浮层不参与清焦点」判据一并退役(浮层不会带着进行中的任务留在画布上)。身份跟着资源走:按「入口 + 资源路径」记住最后一次派生请求,重开面板再提交时提示词没变就沿用同一 `operationId` / 幂等键,命中同一 operation 账本;派生成成功即清掉。同一张素材仍有在途提交时,再提交会被拦下并给出原因。
|
||||
- 本轮明确不做:生成视频(保留在既有浮层入口)、宣发素材、生成游戏场景、画布级「选择工具 / 抓手工具」(AGC 现无画布级工具模式)。这五类都**不得**出现在工具栏里。
|
||||
- 本地 IPC 边界(前端只做本地收窄,不改接口):提示词上限 `32000` 字符、素材名上限 `120` 字符、比例白名单 `1:1 / 2:3 / 3:2 / 9:16 / 16:9`、尺寸白名单 `0.5K / 1K / 2K`。
|
||||
|
||||
|
||||
@@ -9041,3 +9041,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 理由:用户对「生成」的心智是「交出去就得能看见它在跑」,而不是必须区分两条账本;而防重复必须在**身份重铸之前**拦,等到原生账本判重时已经派生过一次。
|
||||
- 验证:新增模型用例 9 条、快速编辑两组宿主用例(提交当帧进侧栏并收口 / 在途重复提交被拦),两处新判据做过变异验证;定向 7 个文件 97 条全绿;`tests/appSurface.test.ts` 与改动前基线逐条一致(16 失败均为本分支既有)。
|
||||
- 待办:远程 CI 与真实客户端验收未跑;`resourceCanvasGenerationTasksSidebarDismiss` 等样式结构用例在本分支既有失败,与本批无关,另行跟踪。
|
||||
|
||||
## 2026-09-21 卡片浮层改为「提交即关」:快速编辑 / 生成动画只负责交任务
|
||||
|
||||
- 背景:客户端验收反馈——「生成动画」这类卡片浮层在失焦时不会收起,而且这块的关闭判据一直是东一处西一处拼的(点画布空白、换选中卡、Esc、点画布以外各有一条);动画面板在生成中还会被「生成中不关」判据锁在画布上。产品口径澄清:生成都归「生成任务」侧栏,**卡片浮层只用于提交任务,提交完生命周期就结束**。
|
||||
- 决策(提交即关):快速编辑 / 生成动画点提交当帧即关面板,不等 IPC、不留等待态;阶段与结果只由「生成任务」侧栏承载(本地提交记录 + 原生待办按 `operationId` 合并,见 2026-09-20 那条)。只有**点击瞬间就失败**留在面板里;**受理之后才失败**把侧栏收口为失败 + 提示条并回落草稿,不重开面板。
|
||||
- 决策(身份跟着资源走):按「入口 + 资源路径」记最后一次派生请求(`resourceEditRequestKey`)。重开面板再提交时提示词没变就沿用同一 `operationId` / 幂等键,重试仍命中同一 operation 账本;派生成成功即清掉,下一次编辑是新的一笔。原「按上次打开的 sourceLayerId 复用」在提交即关之后不再成立(面板重开时层 id 可能已被重投影换掉)。
|
||||
- 清理:`canDismissResourceCanvasQuickEdit`(生成中的浮层不参与清焦点)与宿主里两个只为它服务的面板状态 ref 一并删除;`clearResourceCanvasFocus` 恢复成「清选中 + 关两块浮层」的直线逻辑。
|
||||
- 影响面:`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`.../features/resource-canvas/resourceCanvasFocusModel.ts`、`tests/{projectResourceLiveIntegration,resourceCanvasQuickEditDraft,resourceCanvasFloatingDismiss}.test.tsx`、PRD §3.10。
|
||||
- 验证:定向 `projectResourceLiveIntegration`(32 条,三条断言面板留在失败态的用例按新口径改写为「重开面板再重试,身份不变」)、`resourceCanvasQuickEditDraft`(10 条)、`resourceCanvasFloatingDismiss`(18 条)全绿;`npm --prefix apps/ai-game-creator-shell run typecheck` 通过;`tests/appSurface.test.ts` 与改动前基线逐条一致(16 失败均为本分支既有)。
|
||||
|
||||
Reference in New Issue
Block a user