From 324573938526a1db9b6ccb86ada19bcc34398e39 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Fri, 11 Sep 2026 20:03:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=94=B9=E6=88=90=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E6=9B=BF=E6=8D=A2=EF=BC=9A=E4=B8=8D=E5=88=87=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E3=80=81=E5=B0=BA=E5=AF=B8=E8=A7=84=E6=A0=BC=E9=99=8D?= =?UTF-8?q?=E7=BA=A7=E4=B8=BA=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 口径跟进:前端从「版本级替换」改成「直接替换」——成功后**不再切换版本**(没有新版本可切),只重读 manifest;「当前使用」高亮会随新绑定自动移动(改的仍是同一个源版本) - resourceVersionReplacementModel:兼容性判据的展示口径收窄为「硬门禁只有分类与类型」,尺寸规格不再产生禁用原因,改为非阻断提示「格式与源素材不同」;新增 resourceReplacementWarning 与 resourceReplacementAssetHints;类型去掉 parentVersionId、加上 warning - index.tsx:confirmResourceVersionReplacement 去掉 selectActiveVersion 调用,失败仍保留弹窗与选择并显示原因(不切版本、不动高亮、不提示成功);新增 hint 映射并传给弹窗 - ImageCanvasProjectAssetPickerDialog:以可选 prop assetHints 扩展(默认 undefined → 不渲染),用于展示"可选但有差异"的提示;与 assetBlockedReasons 的区别是提示不改变可点性,默认行为仍逐字不变 - 失败文案补一条:版本写入边界拒绝(「不可修改、删除或重排」)翻成「替换被项目版本写入边界拒绝,请刷新项目后重试」,不让用户看到内部口径原文 - 用例改造:模型 9 条(尺寸规格不再禁用、提示表只收可选候选、提示不覆盖真正的不兼容原因、边界拒绝文案)、真链路 4 条(候选弹窗禁用硬门禁项 + 格式提示仍可选、写入载荷精确、成功后不切版本且不重载预览、失败保留弹窗且不重读 manifest、候选读取失败不弹空壳弹窗) - 验证:两个前端用例文件 13 passed / 0 failed;npm run ai-game-creator-shell:typecheck(含 check-config)exit 0 --- .../resourceVersionReplacementModel.ts | 60 ++++++++++++--- .../src/view/project-development/index.tsx | 17 +++-- .../tests/resourceVersionReplacement.test.tsx | 73 ++++++++++++------- .../resourceVersionReplacementModel.test.ts | 66 ++++++++++++----- .../ImageCanvasProjectAssetPickerDialog.tsx | 30 +++++++- 5 files changed, 181 insertions(+), 65 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceVersionReplacementModel.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceVersionReplacementModel.ts index 018a378e4..316d481ea 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceVersionReplacementModel.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceVersionReplacementModel.ts @@ -6,13 +6,16 @@ import { } from './resourceCanvasToolbarModel'; /** - * 「替换素材」的展示层口径:类型、禁用原因与失败文案。 + * 「替换素材」的展示层口径:类型、禁用原因、提示与失败文案。 * - * 三项兼容性的**判据在 Rust**(`project/version_resource_replacement.rs`),前端只做呈现, - * 不在这里重算判据 —— 否则同一个规则会出现两份实现,迟早分叉。 + * 兼容性判据在 Rust(`project/version_resource_replacement.rs`),前端只做呈现,不在这里重算 + * 判据 —— 否则同一个规则会出现两份实现,迟早分叉。当前形态是**直接替换**:改 manifest 里该 + * 版本的绑定,不产生新版本。 */ -/** PRD §5.3 的三项兼容性;三项必须同时为 `true` 才能创建下一版本。 */ +/** + * 兼容性三项。只有前两项是硬门禁(不满足则候选不可选);`sizeSpecEqual` 只作提示。 + */ export type ProjectVersionResourceCompatibility = { categoryEqual: boolean; subtypeEqual: boolean; @@ -24,6 +27,8 @@ export type LocalProjectVersionReplacementCandidate = { compatible: boolean; compatibility: ProjectVersionResourceCompatibility; blockedReason: string | null; + /** 非阻断提示(当前只有"格式与源素材不同")。 */ + warning: string | null; }; export type ReadLocalProjectVersionReplacementCandidatesResult = { @@ -32,26 +37,27 @@ export type ReadLocalProjectVersionReplacementCandidatesResult = { candidates: LocalProjectVersionReplacementCandidate[]; }; -/** PRD §5.3 的 `ProjectVersionResourceReplacement`。 */ +/** 这一次替换的记录:哪个版本的哪个绑定指向改成了哪个素材(没有新版本,也就没有 parentVersionId)。 */ export type ProjectVersionResourceReplacement = { - sourceVersionId: string; + versionId: string; sourceResourceId: string; replacementResourceId: string; compatibility: ProjectVersionResourceCompatibility; + warning: string | null; }; export type ReplaceLocalProjectVersionResourceResult = { versionId: string; - parentVersionId: string; committedProjectRevision: number; replacement: ProjectVersionResourceReplacement; }; /** - * 三项兼容性对应的中文原因,按 PRD §5.3 的字段顺序取第一条不等的维度。 + * 候选不可选的原因:**只看硬门禁**(分类 / 类型)。 * * 后端已经给出 `blockedReason`;这里只在它缺失(旧后端 / 手工构造的候选)时按同一顺序派生, - * 不让界面出现"不可选但不说原因"的条目。 + * 不让界面出现"不可选但不说原因"的条目。尺寸规格不参与该判据:它今天不完整,用不完整的判据 + * 拒绝会误伤 `png ↔ webp` 这类正常替换。 */ export function resourceReplacementBlockedReason( candidate: LocalProjectVersionReplacementCandidate, @@ -61,10 +67,26 @@ export function resourceReplacementBlockedReason( if (reason) return reason; if (!candidate.compatibility.categoryEqual) return '分类不同'; if (!candidate.compatibility.subtypeEqual) return '类型不同'; - if (!candidate.compatibility.sizeSpecEqual) return '尺寸规格不同'; return '替换兼容性未通过'; } +/** 候选的非阻断提示:可选但仍值得说一声(当前只有尺寸规格/格式差异)。 */ +export function resourceReplacementWarning( + candidate: LocalProjectVersionReplacementCandidate, +): string | null { + const warning = candidate.warning?.trim(); + if (warning) return warning; + if ( + candidate.compatible && + candidate.compatibility.categoryEqual && + candidate.compatibility.subtypeEqual && + !candidate.compatibility.sizeSpecEqual + ) { + return '格式与源素材不同'; + } + return null; +} + /** * 候选 → 禁用原因表,喂给弹窗的 `assetBlockedReasons`。 * @@ -81,6 +103,18 @@ export function resourceReplacementBlockedReasons( return reasons; } +/** 候选 → 提示表,喂给弹窗的 `assetHints`(默认不渲染,只有传了才显示)。 */ +export function resourceReplacementAssetHints( + candidates: readonly LocalProjectVersionReplacementCandidate[], +): Record { + const hints: Record = {}; + for (const candidate of candidates) { + const warning = resourceReplacementWarning(candidate); + if (warning) hints[candidate.resourceId] = warning; + } + return hints; +} + /** * 候选 → 弹窗素材。 * @@ -154,7 +188,11 @@ export function resourceVersionReplacementErrorMessage(error: unknown): string { if (message.includes('替换素材与源素材相同')) { return '替换素材与源素材相同'; } - if (message.includes('需要在客户端内执行')) { + if (message.includes('不可修改、删除或重排')) { + // 版本记录写入边界的拒绝:直接替换只允许改一个版本的绑定,走到这里说明写入被守门挡下了。 + return '替换被项目版本写入边界拒绝,请刷新项目后重试'; + } + if (message.includes('必须在客户端内执行')) { return '替换素材需要在客户端内执行'; } return message; diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 3a8dcc622..b6819c01b 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -138,6 +138,7 @@ import { } from '../../features/resource-canvas/resourceCanvasVersionBindingModel'; import { type LocalProjectVersionReplacementCandidate, + resourceReplacementAssetHints, resourceReplacementBlockedReasons, resourceReplacementPickerAssets, resourceVersionReplacementErrorMessage, @@ -5072,11 +5073,10 @@ export default function ProjectDevelopmentView({ setResourceReplacementError(null); }, []); /** - * 确认替换:CAS 写入「新版本 + 新绑定」,成功后重读 manifest 并把当前版本切到新版本。 + * 确认替换:直接改该版本的绑定(不建新版本),成功后重读 manifest。 * - * 失败一律保留弹窗与选择并把原因显示在弹窗里:不切版本、不动高亮、不提示成功。 - * 按 PRD §3.2 末条,新版本不自动被运行中的预览消费;入口只在资源视图出现, - * 因此这里的 `selectActiveVersion` 不会顺手重载运行画面。 + * 失败一律保留弹窗与选择并把原因显示在弹窗里:不动版本、不动高亮、不提示成功。 + * 成功后**不切换版本**——没有新版本可切;「当前使用」高亮会随新绑定自动移动(同一个源版本)。 */ const confirmResourceVersionReplacement = useCallback( async (assetIds: string[]) => { @@ -5112,7 +5112,6 @@ export default function ProjectDevelopmentView({ setResourceReplacementOpen(false); setResourceReplacementSource(null); setResourceReplacementCandidates([]); - selectActiveVersion(result.versionId); await reloadManifestAfterAssetCommand( result.committedProjectRevision, `version-resource-replacement:${result.versionId}`, @@ -5131,7 +5130,6 @@ export default function ProjectDevelopmentView({ reloadManifestAfterAssetCommand, resourceReplacementLoading, resourceReplacementSource, - selectActiveVersion, ], ); const resourceReplacementPickerEntries = useMemo( @@ -5143,6 +5141,10 @@ export default function ProjectDevelopmentView({ () => resourceReplacementBlockedReasons(resourceReplacementCandidates), [resourceReplacementCandidates], ); + const resourceReplacementHintMap = useMemo( + () => resourceReplacementAssetHints(resourceReplacementCandidates), + [resourceReplacementCandidates], + ); function showRunView() { if (!runAvailable || uiEditorRoute) { @@ -6588,7 +6590,7 @@ export default function ProjectDevelopmentView({ /> ) : null} {/* - 版本级资源替换:复用美术画布的参考图弹窗(单选 + 禁用原因 + 失败原因三个 opt-in prop)。 + 版本级资源替换(直接替换):复用美术画布的参考图弹窗(单选 + 禁用原因 + 提示 + 失败原因)。 缩略图走 `renderAssetMedia` 的类型占位:AGC 的素材预览要经带 scope 的原生读取器拿 Blob URL, 弹窗里没有同步 `src`,直接给 `` 会挂破图。 */} @@ -6599,6 +6601,7 @@ export default function ProjectDevelopmentView({ singleSelect selectionNoun="替换素材" assetBlockedReasons={resourceReplacementBlockedReasonMap} + assetHints={resourceReplacementHintMap} errorMessage={resourceReplacementError} renderAssetMedia={(asset) => (