diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 660426d17..b4902cae2 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -15,6 +15,19 @@ - 关联文档:相关 PRD、技术文档、提交或 Issue ``` +## 2026-09-11 冷启动首屏存在同一张卡被读两次(登记回声 + 取消/重扫路径):记录为后续项,本轮不修 + +- 背景:为解「从首页进项目 → 资源管理页首屏等图片」,本轮按用户批准做了 A(热预取不再按投影顺序盲取前 N,改为只预取几何上可见的卡)+ B(相交卡按「先视口内、再 160px `rootMargin` 圈」两档入队)。本条记录的是**做 A/B 时顺手发现、但属于另一条独立缺陷**的重复读;A/B 只改「取哪些、按什么顺序」,不碰它。 +- 现象:冷启动首屏同一张卡会被读两次。20 张登记卡(8 张视口外 + 4 张只在余量圈 + 8 张视口内)时 `read_local_project_media_preview` 共发 **15 次**:视口内 8 张各 1 次、视口外 0 次,余量圈那 4 张里 **3 张各 2 次、1 张 1 次**(distinct 12 + 重复 3)。 +- 两条对照证据(证明与 A/B 无关、且先于 A/B 存在): + 1. 把 `eagerPreviewLimit` 置 **0**(完全关掉热预取)→ 仍是 15 次、仍是那 3 张余量圈卡各读 2 次,只是顺序不同; + 2. 把 `useProjectResourceCardPreviews.ts` **整份换回 A/B 之前的 `HEAD` 版本** → 同样 3 张余量圈卡各读 2 次(顺序为登记顺序)。 +- 怀疑方向(未验证,本轮未定位):两条通路叠加 —— ①「登记即复核」的回声扫描:`observePreview` 每注册一张卡就跑一次兜底扫描,逐张注册会逐张放行一批;② `cancelQueuedVisiblePrefetches`(预取作用域变化时先下掉队列里的 `visible` 预取、再立刻重扫)与多延迟点兜底扫描(0 / 250 / 1000ms)叠加时,可能在前一次请求已完成之后又被判成"从未请求"(该 identity 在 `previews` 里没有状态)而重新入队。定位需要按 identity 打点 `requestPreview → 入队 → drain → publish` 的时序。 +- 未修原因:① 用户在赶 DDL,本轮时间窗只够 A+B;② **不是用户可见故障** —— 表现只是首屏多花一两个物理读取槽 / 多一次 IPC,图片照常出来;③ 它属于「可见性门禁 + 队列时序」这条更脆的通路,改它必须先把时序定位清楚,不能凭猜测顺手改。 +- 影响:首屏请求量比理论最小值多约 20%(上述夹具 15 次 vs 12 次);要再压首屏等待,必须先解开这条重复读。 +- 验证方式:本条目为**待查项,无代码改动**。修复时建议的断言:同一次冷启动首屏流程里,同一个 identity 的 `read_local_project_media_preview` **只允许发一次**(除非确实发生过 LRU 驱逐或显式重试)。 +- 关联:`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCardPreviews.ts`(`observePreview` 的注册回声扫描、`cancelQueuedVisiblePrefetches`、`RESOURCE_PREVIEW_VISIBLE_SWEEP_DELAYS_MS` 多延迟点兜底扫描)、`apps/ai-game-creator-shell/tests/useProjectResourceCardPreviews.test.ts`(`冷启动首屏的放行范围与放行顺序` 两条用例的夹具可直接复用:20 张卡 + `eagerPreviewLimit: 12`)。 + ## 2026-09-11 资源筛选改为按需弹出的独立浮层:区域与画布栏目共用一份状态,状态字段不做 - 背景:资源画布此前只有搜索框一条收窄路径。`bb4834520` 按用户要求移除了画布上的**常驻分类筛选条**(含画布标签 chip),随它一起摘掉的还有 `resourceCanvasCategoryFilters` / `resourceCanvasActiveTags` / `resourceTagLibrary` 三个只为常驻条存在的筛选轴,注释落在 `index.tsx` 的 `visibleResources` 上方。用户随后明确要的是**关键词 / 所在区域 / 状态 / 自定义标签**四个维度的检索,并给出原型:一个独立的「筛选」浮层、四字段竖向排布、右上角 `×` 关闭。因此本轮交付的是**按需弹出的浮层**,与已被移除的常驻条不是同一个东西;用户点名「切换区域的时候你也跟着切换区域,然后 filter 就好做了,只在当前区域 filter」。 @@ -22,7 +35,7 @@ 1. **查找素材**:与画布搜索浮层**共用同一份 `searchText` 状态**,两个入口读写同一个关键词,不新建第二套搜索;判据沿用既有四字段(label / path / mediaType / taskTitle)与大小写不敏感口径。 2. **所在区域**:取值 = 「全部区域」+ `PROJECT_RESOURCE_CANVAS_SECTIONS` 的 6 类资产分类与末尾独立「项目版本」栏;**区域不新增第二份 state** —— 下拉 `value` 由 `resourceBookState.view === 'child' ? resourceBookState.category : 'all'` 派生,`onChange` **只调既有 `openResourceBookChild`**(与栏目切换完全同一条路径,含 viewport 拟合)。因此滚轮 / 总览卡片 / 下一页切换栏目时区域显示自动跟随,**不需要对账 effect,也不可能双写漂移**。「全部区域」复用既有 `RESOURCE_BOOK_ALL_TARGET` 的「所有资源」分组网格,8 个取值全走同一个函数、**无特例分支**;又因为 `visibleResourcesByCategory` 由筛选后的 `visibleResources` 按 `category` 派生,「区域=具体栏目时只筛该栏目」与「区域=全部时筛全量」**共用同一份过滤实现**,不为前者另写一套裁集合逻辑。 3. **自定义标签**:从 manifest `assets[].tags` 派生,**多选 AND**(与既有 `assetTagsMatchSelection` 一致,PRD 无条款故不偏离);标签库只统计当前区域内资源,切区域后不残留别的区域的标签。 - **不提供「状态」字段**。三条证据:① `gameCreationApp.ts` 的 `GameCreationAppAssetManifestEntry` 字段集里**没有 status**;② `resourceProjectionModel.ts` 的 `ProjectResource` 字段集里**也没有 status**;③ 唯二候选都不是资源状态且恒单值 —— `GameCreationAppTaskStatus` 是**任务**状态、而投影只让 `status === 'completed'` 的任务产出资源;`ProjectAttachmentResult.status` 是**附件导入**状态、而投影先滤掉非 `imported` 的才产出资源。硬拿它们派生,取值永远只有一个,是**假筛选维度**。按用户原话「如果没有这个东西就删掉」与仓库「四不写」,**不实现、不留占位下拉**。 + **不提供「状态」字段**。三条证据:① `gameCreationApp.ts` 的 `GameCreationAppAssetManifestEntry` 字段集里**没有 status**;② `resourceProjectionModel.ts` 的 `ProjectResource` 字段集里**也没有 status**;③ 唯二候选都不是资源状态且恒单值 —— `GameCreationAppTaskStatus` 是**任务**状态、而投影只让 `status === 'completed'` 的任务产出资源;`ProjectAttachmentResult.status` 是**附件导入**状态、而投影先滤掉非 `imported` 的才产出资源。硬拿它们派生,取值永远只有一个,是**假筛选维度**。按用户原话「如果没有这个东西就删掉」与仓库「四不写」,**不实现、不留占位下拉**。 - 影响范围:`packages/shared/src/components/PlatformFilterPanel.tsx`(新增,仅表现)、`apps/ai-game-creator-shell/src/view/project-development/resourceCanvasFilterModel.ts`(新增纯函数:区域判据做恒等比较、关键词四字段、标签 AND、区域选项与显示名派生、标签库派生、筛选生效判据)、`apps/ai-game-creator-shell/src/view/project-development/ResourceFilterPanel.tsx`(新增 AGC 薄接线,领域规则留在这一层)、`apps/ai-game-creator-shell/src/styles.css`(`.game-resource-filter-panel` 右下角锚点,与搜索浮层同一 `bottom` / `z-index` 口径)。**`PlatformResourceFilterBar` 一字未改、其默认行为逐字不变**:它是**横向常驻条**且**正被主站 `src/components/image-editor/ImageCanvasProjectAssetPickerDialog.tsx` 在役使用**(参考图弹窗),把形态改成竖向会波及不在本任务范围的主站生成面,加 variant 又等于让一个组件背两种骨架,故新建外壳而非扩展它。区域显示名复用 `@` 面板那份唯一中文权威 `resourceReferenceCategoryLabel`,只为不在该 6 类表内的 `version` 单列固定显示名,不另建译名表。**跨端契约、sidecar schema、manifest 字段、后端接口均不变。** - 未落地(截至本条记录时):`index.tsx` 的 3 处接线(①`visibleResources` 内联过滤换成调用纯函数;②新增 `resourceFilterOpen` state + Dock 筛选按钮 + 浮层挂载,并把区域接到 `openResourceBookChild`;③新增 `activeTags` state)**尚未提交**——当时该文件正被另一条线(美术画布「生成动画」接线)占用。已落地的部分(外壳、纯函数、AGC 侧组件与样式)各自独立提交,接线完成后本条应补记。 - 验证方式:`packages/shared/src/components/PlatformFilterPanel.test.tsx` 覆盖 `dialog` 语义与关闭转发、字段逐个渲染、外壳不自带「状态」占位字段、`controlId` 字段名关联、宿主定位类注入且组件不自带 `absolute` / `fixed`;`apps/ai-game-creator-shell/tests/resourceCanvasFilterModel.test.ts` 覆盖区域选项顺序与逐个中文显示名、空筛选不重排、区域恒等比较、关键词四字段与空白归一、标签 AND 语义、无标签事实源资源的口径、三维度叠加、标签库计数与 `zh-CN` 排序、筛选生效判据;`apps/ai-game-creator-shell/tests/resourceFilterPanel.test.tsx` 覆盖三字段渲染且无「状态」占位、区域选项取值与显示名、关键词回显与回调、区域切换只回调、标签受控多选与 `aria-pressed`、空标签库隐藏该字段、关闭键 / `Escape` / 点外部 / 点触发按钮四种关闭口径、`Escape` 后焦点回触发按钮。**变异验证实测**:把区域判据改成恒 `true` → `4 failed`;去掉关键词判据 → `3 failed`;去掉标签判据 → `4 failed`;三处还原后复跑 `13 passed`。既有断言零放宽(`PlatformResourceFilterBar.test.tsx` 与主站弹窗测试一字未改,同跑 `9 passed`)。 @@ -57,8 +70,6 @@ - 验证方式:Rust 定向测试覆盖旧行投影(role/content 逐字节、无 `messageId` 时不带 `id`)、旧行+新行交替的混合文件按行顺序读取、`tool` 行被识别但不进上下文、非白名单异常行仍失败关闭、通用对话写入器产出的旧行形状落在白名单内、通用对话链跳过 DirectProject 行且坏行仍失败关闭、混合文件从两条链都能读(互不毒化)、显式 Codex 返回不再写 `project.jsonl`、专门 hint 与 `retryable=false` 落到诊断 sidecar。变异验证:去掉 legacy 兼容分支→投影与混合文件用例变红;把白名单放宽成「任意行都接受」→失败关闭用例变红。 - 关联文档:`docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md`、`docs/project-memory/shared-memory/pitfalls.md`。 - - - 背景:用户给出的目标样式截图里,这个面板标题是「编辑素材标签」、副标题是素材名、已有标签是自带删除按钮的胶囊 pill、输入框提示「新增标签,多个用逗号分隔」、底部只有「取消」与「保存标签」,**没有分类那一排**。而面板原实现同时承担 6 类 `category` 手动设置与 `assets[].tags` 编辑,PRD §5.3 也写着「用户可在「分类与标签」面板手动设置 `category`」。资源卡浮出工具条上只有这一个相关入口(`label="分类与标签"` → `setResourceClassificationAssetId`),不存在第二个「编辑素材标签」入口,所以两种读法只能二选一。截图里的标题/按钮文案在全仓(含 `docs/**`、`.codex/**`、各类型源码)检索均无命中,属仓库之外的来源,因此本次改动以用户截图为准、不宣称是 PRD 明文。 - 决策:**该面板只编辑 manifest `assets[].tags`,移除分类 chip 那一排。** 随之的事实是:**「用户手动设置 `category`」这项能力就此移除**,`category` 只由落盘值与 `assets[].kind` 派生加读时自愈决定。写入命令 `update_local_project_resource_classification` 的 `category` 是必填,前端读一次当前权威值并在保存时**原样回传**,因此「只改标签」不会顺带改动分类,Rust 侧与 manifest 字段构成都不改。面板标题改「编辑素材标签」、入口按钮 label 改「编辑标签」(否则工具条写着「分类与标签」却打开纯标签面板,属误导)。「删除资源」按钮截图未画但保留:`openDeleteResourceDialog` 只在这个面板里被调用,删掉会让用户失去唯一的资源删除入口。 - 影响范围:`apps/ai-game-creator-shell/src/view/project-development/ResourceClassificationPanel.tsx`(标题/副标题/标签状态由整段字符串改为字符串数组/pill 列表/底部按钮文案)、`index.tsx` 的入口按钮 label、`apps/ai-game-creator-shell/src/styles.css` 的标签 pill 选择器块;`packages/shared` 的标签归一化(`normalizeGameCreationAppAssetTags`)与写入契约不变。