From 84b8f5130bf716217793a7cba624fc380a6e6f44 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Fri, 11 Sep 2026 01:47:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B5=84=E6=BA=90=E6=80=BB?= =?UTF-8?q?=E8=A7=88=E5=8F=AA=E7=94=BB=E5=BD=93=E5=89=8D=E6=A0=8F=E7=9B=AE?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9A=E6=8B=86?= =?UTF-8?q?=E5=BC=80=E8=BA=AB=E4=BB=BD=E5=8F=A3=E5=BE=84=E4=B8=8E=E7=83=AD?= =?UTF-8?q?=E9=A2=84=E5=8F=96=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前一次改动(60d8b8fbb)把 `useProjectResourceCardPreviews` 的 `resources` 从全量投影收窄成当前分页栏目,造成产品回归:该入参不只喂热预取,它同时决定返回的 `identityByResourceId`,而 `renderResourceBookCard` 在身份缺失时 `return null`,于是资源总览(main 态)只有当前分页栏目画出真实卡片,其余栏目只剩栏目标题栏加空的层叠占位。 - 拆成两个入参:`resources` 恢复全量资源投影语义(决定 `identityByResourceId`、缓存与卡片挂载),新增 `eagerResources` 只用于 `eagerPreviewLimit` 计数的热预取,缺省回退 `resources`。两个入参的语义写进 Hook 参数注释,调用方 `index.tsx` 在传参处注明「收窄 `resources` 会让其它栏目只剩标题栏」。 - 纠正前一次提交的说法:**identity map 必须保持全量**,不能跟着热预取一起收窄。热预取可以只喂当前栏目(上一个提交的意图保留、`A → B → 回 A` 会重读 A 可见卡的已知代价也保留),但身份 / 渲染口径收窄会让总览其它栏目不再挂载卡片——技术方案要求「主画布展示各子画布的缩略入口,内部按资源类型显示有限层叠卡片……主画布预览和子画布展开态复用同一套卡片视觉」,这是产品回归而非可接受的行为变更。 - 不使用「身份未知时用 idle 占位兜底渲染」这条备选:它会让总览卡片失去真实预览,观感更差。 - `pitfalls.md` 追加同题排障口径,含通用规则:凡 Hook 入参同时参与「身份 / 缓存 key」与「调度 / 预热」,就不能为优化调度去收窄它,应先确认它还有没有别的消费者。 - 回归用例 `appSurface > uses one full-page canvas per resource section with dependency-only guide lines` 已自行变绿(未改该断言)。验证:`npm run test -- apps/ai-game-creator-shell/tests` 1171 passed / 4 skipped / 0 failed;`appSurface.test.ts` + `useProjectResourceCardPreviews.test.ts` 415 passed;`src/components/image-editor` 1385 passed;typecheck exit 0;check:encoding 4374 文件;`git diff --check` 干净。 --- .../src/view/project-development/index.tsx | 8 ++++++- .../useProjectResourceCardPreviews.ts | 21 ++++++++++++++++++- docs/project-memory/shared-memory/pitfalls.md | 9 ++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) 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 f86ba9696..d5c7766a5 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 @@ -1860,7 +1860,13 @@ export default function ProjectDevelopmentView({ projectPath, projectId: manifest.projectId, mode: sortMode, - resources: activePageResources, + // `resources` 决定 `identityByResourceId`,而 `renderResourceBookCard` 在身份缺失时 + // 直接不挂载卡片。总览(main 态)要同时画出每个栏目的层叠卡片,所以这里必须保持 + // 全量投影;收窄它会让除当前分页栏目以外的栏目只剩栏目标题栏。 + resources: canvasResources, + // 真正要收窄的是热预取:只让当前分页栏目先占读取槽。传全量时前 12 张可能落在 + // 用户看不到的栏目,看得见的卡片反而要排队。 + eagerResources: activePageResources, canvasRef: resourceCanvasRef, // 资源卡片渲染在资源画本场景里,可见性判定要用真正包含它们的容器。 intersectionRootRef: resourceBookManagerRef, diff --git a/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts b/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts index f6e38cb0f..bef0eac06 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts @@ -201,7 +201,22 @@ export function useProjectResourceCardPreviews(input: { projectPath: string; projectId: string; mode: ProjectResourceCanvasLayoutMode; + /** + * 身份与渲染口径,**必须传全量资源投影**。 + * + * 这里同时决定 `identityByResourceId`,而调用方在身份缺失时会直接不挂载卡片 + * (`ProjectDevelopmentView` 的 `renderResourceBookCard` 对未知身份 `return null`)。 + * 因此把本参数收窄成「当前分页栏目」会让总览里其它栏目只剩栏目标题栏和空的缩略卡片。 + * 需要缩小预热范围请用 [`eagerResources`],不要改这里。 + */ resources: ProjectResource[]; + /** + * 只用于热预取(`eagerPreviewLimit` 计数的那个集合),不影响身份 / 缓存 / 渲染。 + * + * 缺省回退到 `resources`。资源画布传「当前分页栏目」,避免用户看不到的栏目先占满 + * 全局物理读取槽;传全量时前若干张可能落在别的栏目,看得见的卡片反而要排队。 + */ + eagerResources?: ProjectResource[]; canvasRef: React.RefObject; // 可见性判定容器。卡片渲染在资源画本场景里时,传真正包含卡片的容器; // 缺省回退到 canvasRef。 @@ -670,8 +685,11 @@ export function useProjectResourceCardPreviews(input: { Math.floor(input.eagerPreviewLimit ?? 0), ), ); + // 只预热 `eagerResources`(缺省回退 `resources`):身份与缓存仍然覆盖全量投影, + // 这里收窄的仅仅是「谁先占读取槽」。 + const eagerResources = input.eagerResources ?? input.resources; let requested = 0; - for (const resource of input.resources) { + for (const resource of eagerResources) { if (requested >= eagerPreviewLimit) { break; } @@ -690,6 +708,7 @@ export function useProjectResourceCardPreviews(input: { }, [ identityByResourceId, input.eagerPreviewLimit, + input.eagerResources, input.resources, requestPreview, ]); diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 4398f77c8..e9a2c8f9c 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -197,6 +197,15 @@ - 验证:先让旧 scope 占满 3 个 permit,再连续执行 A → B → A、mode、Hook 和多窗口切换;断言原生活动峰值始终 `<= 3`,旧等待任务不打开文件,旧在途任务在最近检查点释放,新 scope 随后启动,取消任务不编码 data URL / 不创建 Blob URL,最终活动 request / scope registry 为零。阻塞读取进入后主动 abort 外层 future,必须证明旧 blocking 任务仍占 permit、仍可按 scope 取消且新请求不能提前启动。另分别证明 seen request `8192` 项的硬上限、cancelled scope 超预算时不淘汰活动记录,以及任一活动 scope 结束后非活动 tombstone 立即收敛到 `1024` 项预算。前端再断言旧 `then / catch / finally` 和取消 ACK 均不写新 scope;内部取消类别只允许精确匹配,不能因真实错误正文恰好包含该标识而静默吞错。 - 关联:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts`、`apps/ai-game-creator-shell/src-tauri/src/resource_preview_scheduler.rs`、`apps/ai-game-creator-shell/src-tauri/src/resource_inspect.rs`、`apps/ai-game-creator-shell/src-tauri/src/image_inspect.rs`。 +## 预览 Hook 的资源入参收窄会连带收窄 identity map,进而让总览其它栏目不画卡片(2026-09-11) + +- 现象:资源总览(`main` 态)里只有「当前分页栏目」那一个栏目画出真实卡片,其余栏目只剩栏目标题栏加空的层叠占位。插桩实测:`main` 态下 5 个 `.game-resource-book-scene-card`,只有当前分页栏目的那个带 `.game-resource-card` 子节点,其余全部 0 子节点;把入参换回全量投影即整体恢复。 +- 原因:`useProjectResourceCardPreviews` 的 `resources` 入参**不只喂热预取**,它同时决定返回的 `identityByResourceId`;而 `ProjectDevelopmentView` 的 `renderResourceBookCard` 在身份缺失时直接 `return null`。所以「把 `resources` 换成当前栏目」看起来只是缩小预热范围,实际把身份 map 一起收窄了,总览里不在当前栏目的卡片就此不再挂载。技术方案明确要求「主画布展示各子画布的缩略入口,内部按资源类型显示有限层叠卡片……主画布预览和子画布展开态复用同一套卡片视觉」,因此这是产品回归,不是可接受的行为变更。 +- 处理:**把「身份 / 渲染口径」与「热预取口径」拆成两个入参**——`resources` 保持全量资源投影不变(决定 `identityByResourceId`、缓存与卡片挂载),新增 `eagerResources` 只用于 `eagerPreviewLimit` 计数的热预取,缺省回退 `resources`。两个入参的语义写进 `useProjectResourceCardPreviews` 的参数注释,调用方 `index.tsx` 也在传参处注明「收窄 `resources` 会让其它栏目只剩标题栏」。**不要**改用「身份未知时用 idle 占位兜底渲染」:那会让总览卡片失去真实预览,观感更差。 +- 通用规则:凡 Hook 的某个入参同时参与「身份 / 缓存 key」与「调度 / 预热」,就不能为了优化调度去收窄它。先确认这个入参还有没有别的消费者,再决定是拆参数还是加参数;同理适用于今后任何「只预热一部分」的优化。 +- 验证:`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts` 的 `uses one full-page canvas per resource section with dependency-only guide lines` 会断言非当前栏目的 `.game-resource-book-scene-card` 内存在 `.game-resource-card`;`tests/useProjectResourceCardPreviews.test.ts` 覆盖 `eagerResources` 缺省回退到 `resources` 的语义。 +- 关联:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts`、`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 + ## 依赖线与资源卡不在同一 transform 层时会在滚动和缩放中分离 - 现象:静止时依赖线似乎对齐,触摸板缩放或连续滚动后线段会追赶、漂移或忽隐忽现;某一资源分区的长线还可能出现在相邻分区。