修复资源总览只画当前栏目卡片的问题:拆开身份口径与热预取口径

前一次改动(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` 干净。
This commit is contained in:
2026-09-11 01:47:04 +08:00
parent 7fa61231ae
commit 84b8f5130b
3 changed files with 36 additions and 2 deletions
@@ -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,
@@ -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<HTMLDivElement | null>;
// 可见性判定容器。卡片渲染在资源画本场景里时,传真正包含卡片的容器;
// 缺省回退到 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,
]);
@@ -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 层时会在滚动和缩放中分离
- 现象:静止时依赖线似乎对齐,触摸板缩放或连续滚动后线段会追赶、漂移或忽隐忽现;某一资源分区的长线还可能出现在相邻分区。