From 372b7d3a00c827d027495c3b7ab34f399f8a0679 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Sat, 3 Oct 2026 16:59:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AGC=20=E6=A0=8F?= =?UTF-8?q?=E7=9B=AE=E7=94=BB=E5=B8=83=E4=B8=8A=E4=BC=A0=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E8=90=BD=E3=80=8C=E5=BE=85=E5=BD=92=E7=B1=BB=E3=80=8D=EF=BC=88?= =?UTF-8?q?Issue=20359=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AGC Rust `upload_local_asset` 新增可选 `targetCategory`,登记改走 `register_local_asset_entry_with_category`(非法值失败关闭,不回退 kind 派生);`upload_local_asset_at` 委托新的 `upload_local_asset_at_with_category`,既有调用点与单测不动 - 前端 `uploadProjectAssetFilesAndReadSnapshot` 透传 `targetCategory`;栏目画布底部工具栏上传带上工具栏自己的栏目,与生成入口的 `targetCategory` 同一口径 - 新增 Rust 单测 `upload_registers_into_the_explicit_entry_category`(显式栏目 → character / 不传 → unclassified / 非法 version 失败关闭且不新增登记)与 appSurface 用例(在「角色与对象」上传图片,载荷必须带 `targetCategory: 'character'`) - 同步 PRD §3.10、`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md` §4 载荷表、V3 验收用例 S11a、decision-log 与 pitfalls --- .../src-tauri/src/assets.rs | 84 ++++++++++++++++++- .../src-tauri/src/commands/desktop.rs | 9 +- .../src/view/project-development/index.tsx | 16 +++- .../projectResourceLiveUpdateModel.ts | 14 +++- .../appSurface/project-development.suite.ts | 46 ++++++++++ ...AI游戏创作】项目开发工作台PRD-2026-07-20.md | 2 +- .../shared-memory/decision-log.md | 8 ++ docs/project-memory/shared-memory/pitfalls.md | 8 ++ ...C】栏目画布底部工具栏入口矩阵-2026-09-13.md | 2 +- ...用例】AGC资源工作台V3端到端验收-2026-09-11.md | 2 +- 10 files changed, 182 insertions(+), 9 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/assets.rs b/apps/ai-game-creator-shell/src-tauri/src/assets.rs index 214a360e5..c45f849b8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/assets.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/assets.rs @@ -567,6 +567,23 @@ pub(crate) fn upload_local_asset_at( file_name: &str, media_type: &str, bytes: &[u8], +) -> Result { + upload_local_asset_at_with_category(root, file_name, media_type, bytes, None) +} + +/// 带**显式入口栏目**的上传:只给 GUI 工具栏上传用(前端 `targetCategory`)。 +/// +/// 上传的 manifest `kind` 只由内容证据推导(图片 / 视频 / 代码 → `unclassified`), +/// 与栏目不是同一套词汇:在栏目画布里上传素材时,用户选定的栏目才是归属真相, +/// 否则素材会落进「待归类」、在上传它的那一栏里看不见。 +/// 取值校验与失败关闭沿用 [`register_local_asset_entry_with_category`]: +/// 非法值报错、绝不回退到 kind 派生;其它调用方继续走 [`upload_local_asset_at`]。 +pub(crate) fn upload_local_asset_at_with_category( + root: &Path, + file_name: &str, + media_type: &str, + bytes: &[u8], + target_category: Option<&str>, ) -> Result { if bytes.is_empty() { return Err("上传文件不能为空".to_string()); @@ -584,7 +601,7 @@ pub(crate) fn upload_local_asset_at( } crate::write_game_creator_private_file(&absolute_path, bytes, "上传文件")?; - register_local_asset_entry( + register_local_asset_entry_with_category( root, &relative_path, uploaded_asset_kind(file_name, media_type), @@ -602,6 +619,7 @@ pub(crate) fn upload_local_asset_at( generation_kind: None, reference_resource_ids: Vec::new(), }, + target_category, ) } @@ -2375,6 +2393,70 @@ mod tests { ); } + /// Issue 359:工具栏上传带上入口栏目,素材才不会落进「待归类」。 + /// + /// 上传的 manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都派生成 `unclassified`; + /// 在栏目画布(如「角色与对象」)里上传时栏目才是归属真相。不传时保持既有行为, + /// 非法值失败关闭且不留下半成品登记。 + #[test] + fn upload_registers_into_the_explicit_entry_category() { + fn upload_category(root: &Path, asset_id: &str) -> GameCreationAppAssetCategory { + read_existing_manifest_for_project(root) + .expect("read manifest") + .assets + .into_iter() + .find(|asset| asset.id == asset_id) + .expect("uploaded asset is registered") + .category + } + + let temporary = tempfile::tempdir().expect("tempdir"); + let root = temporary.path(); + crate::project::init_local_game_project_at(root, "upload-category-test", "上传入口栏目") + .expect("init project"); + + let uploaded = upload_local_asset_at_with_category( + root, + "hero.png", + "image/png", + b"png-bytes", + Some("character"), + ) + .expect("upload with an entry category"); + assert_eq!( + upload_category(root, &uploaded.id), + GameCreationAppAssetCategory::Character + ); + + let plain = upload_local_asset_at(root, "plain.png", "image/png", b"png-bytes") + .expect("upload without an entry category"); + assert_eq!( + upload_category(root, &plain.id), + GameCreationAppAssetCategory::Unclassified + ); + + // 非法入口栏目(栏目侧伪值 `version` 不在枚举里)失败关闭:不新增登记。 + let assets_before = read_existing_manifest_for_project(root) + .expect("read manifest") + .assets + .len(); + assert!(upload_local_asset_at_with_category( + root, + "bad.png", + "image/png", + b"png-bytes", + Some("version"), + ) + .is_err()); + assert_eq!( + read_existing_manifest_for_project(root) + .expect("read manifest") + .assets + .len(), + assets_before + ); + } + /// 画板导出推断出的 kind 必须已经是 canonical 值。 #[test] fn canvas_export_asset_kind_is_always_canonical() { diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs b/apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs index 57a598049..8b7da7ad0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs @@ -493,12 +493,19 @@ pub(crate) fn upload_local_asset( file_name: String, media_type: String, bytes: Vec, + target_category: Option, ) -> Result { let root = Path::new(project_path.trim()); enforce_project_permission_policy(root, "asset.upload")?; let _lock = acquire_project_write_lock(root, "asset.upload")?; advance_agent_runtime_project_revision_locked(root)?; - upload_local_asset_at(root, file_name.trim(), media_type.trim(), &bytes) + upload_local_asset_at_with_category( + root, + file_name.trim(), + media_type.trim(), + &bytes, + target_category.as_deref(), + ) } #[tauri::command] 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 df69d90a0..cfabbccf2 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 @@ -9062,9 +9062,15 @@ export default function ProjectDevelopmentView({ [selectResourceCanvasPage], ); - /** 工具栏「上传」:与资源面板上传同一条「上传 + 配对读清单」链路。 */ + /** + * 工具栏「上传」:与资源面板上传同一条「上传 + 配对读清单」链路。 + * + * 入口栏目随上传一起交给原生(`targetCategory`):上传的 kind 只由内容证据推导 + * (图片 / 视频 / 代码 → `unclassified`),不带上它,素材会落进「待归类」、 + * 在上传它的那一栏里看不见。取值就是工具栏自己的栏目,与生成入口同一口径。 + */ const uploadResourceCanvasToolbarFiles = useCallback( - async (files: readonly File[]) => { + async (files: readonly File[], targetCategory: ResourceCategory | null) => { const invoke = window.__TAURI__?.core?.invoke; if (!invoke) { setResourceWorkbenchNotice('上传素材需要在客户端内打开'); @@ -9088,6 +9094,7 @@ export default function ProjectDevelopmentView({ bytes: Array.from(new Uint8Array(await file.arrayBuffer())), })), ), + targetCategory, invoke, }); setResourceWorkbenchNotice(`已上传 ${uploadable.length} 个素材`); @@ -10548,7 +10555,10 @@ export default function ProjectDevelopmentView({ } onSelectAction={handleResourceBottomToolAction} onUploadFiles={(files) => { - void uploadResourceCanvasToolbarFiles(files); + void uploadResourceCanvasToolbarFiles( + files, + resourceCanvasBottomToolbarCategory, + ); }} uploading={resourceBottomToolbarUploading} /> diff --git a/apps/ai-game-creator-shell/src/view/project-development/projectResourceLiveUpdateModel.ts b/apps/ai-game-creator-shell/src/view/project-development/projectResourceLiveUpdateModel.ts index 79b322249..35b7163bd 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/projectResourceLiveUpdateModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/projectResourceLiveUpdateModel.ts @@ -1,4 +1,7 @@ -import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp'; +import type { + GameCreationAppManifest, + ProjectResourceCanvasCategory, +} from '../../../../../packages/shared/src/contracts/gameCreationApp'; export type ProjectManifestSnapshotSource = | 'initial' @@ -285,6 +288,14 @@ export async function uploadProjectAssetFilesAndReadSnapshot(input: { mediaType: string; bytes: number[]; }[]; + /** + * 入口栏目:栏目画布里上传时由调用方显式给出当前栏目,原生按它登记归类。 + * + * 上传的 manifest `kind` 只由内容证据推导(图片 / 视频 / 代码 → `unclassified`), + * 不传这一项时素材会落进「待归类」、在上传它的那一栏里看不见。 + * 判定与失败关闭都在原生:非法值报错,前端不做伪分类。 + */ + targetCategory?: ProjectResourceCanvasCategory | null; invoke(command: string, args: Record): Promise; }): Promise { for (const file of input.files) { @@ -293,6 +304,7 @@ export async function uploadProjectAssetFilesAndReadSnapshot(input: { fileName: file.fileName, mediaType: file.mediaType, bytes: file.bytes, + ...(input.targetCategory ? { targetCategory: input.targetCategory } : {}), }); } const fresh = await rereadAuthoritativeProjectManifestSnapshot({ diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index fd8e5bebf..b66ab4c34 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -8839,6 +8839,52 @@ export function registerProjectAgentStatusTests() { ); }, 20_000); + it('uploads toolbar files into the entry column so they stay visible where they were uploaded', async () => { + // Issue 359:图片只由内容证据派生成 `unclassified`,在「角色与对象」栏目里上传后 + // 会落进「待归类」、在上传它的那一栏里看不见。工具栏上传必须把当前栏目交给原生。 + const manifest = createGameCreationAppManifest( + 'workbench-bottom-toolbar-upload-category', + '底部工具栏上传归类项目', + ); + const { calls } = installResourceBookBottomToolbarInvoke(manifest); + + render( + React.createElement(ProjectDevelopmentView, { + projectName: manifest.name, + projectPath: '/tmp/workbench-bottom-toolbar-upload-category', + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + chat: React.createElement('div', null, '项目总控'), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + }), + ); + + await openResourceBookCategory('角色与对象'); + const uploadInput = screen.getByLabelText( + '上传素材文件', + ) as HTMLInputElement; + const file = new File(['png'], 'hero.png', { type: 'image/png' }); + if (typeof file.arrayBuffer !== 'function') { + Object.defineProperty(file, 'arrayBuffer', { + value: async () => new Uint8Array([112, 110, 103]).buffer, + }); + } + fireEvent.change(uploadInput, { target: { files: [file] } }); + + await waitFor(() => + expect(uploadCall(calls, 'hero.png')).toMatchObject({ + projectPath: '/tmp/workbench-bottom-toolbar-upload-category', + fileName: 'hero.png', + mediaType: 'image/png', + targetCategory: 'character', + bytes: [112, 110, 103], + }), + ); + }, 20_000); + it('keeps the bottom toolbar clear of the zoom dock and above the book scene', () => { const styles = readFileSync( repoPath('apps/ai-game-creator-shell/src/styles.css'), diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index 14b25c309..9e38ea713 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -164,7 +164,7 @@ - 位置与层级:画布左下角(`left: 14px; bottom: 14px; z-index: 40`)。右下角是既有的缩放 / 撤销 Dock(`right: 14px; bottom: 14px`),左下角是画布上唯一两者都不占的稳定空位。工具栏是管理区 `.game-resource-book-manager` 的**直接子节点**、与画本场景并列,不进带 `scale()` 的场景层;二级菜单与「入口不可用原因」都贴着工具栏上沿弹出,不做内嵌内容。 - 外壳用共享 chrome(`packages/image-canvas-react` 的 `CanvasToolbar / CanvasToolbarGroup / CanvasChromeButton`),样式落在 AGC 的 `resourceCanvasChrome.css`;共享包只承接通用表现,不含业务规则。 -- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。 +- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`,并带上当前栏目 `targetCategory`(上传的 manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都派生成 `unclassified`;不带入口栏目素材就落进「待归类」、在上传它的那一栏里看不见,Issue 359)。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。 - 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(位置与开合形态照抄网页端美术画布的任务侧栏的右上角锚点,颜色与外形仍走 AGC 平台 token;2026-09-21 由「左侧贴边 + 工具条入口」改为「右上角锚点 + 常驻开关」):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除)、折叠即整块让出画布、只留那一枚右上角开关(工具条上不再有重复入口),开合只走画布右上角那一枚「生成任务 · N」开关(两个页签下都在;**展开后开关让位、只留面板**,收起走面板头部 × 或点画布外部);锚点是画布那一格网格里的条目(不是写死 `top` 的绝对定位),工具条换行变高也不会压上去;每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布右上角锚点里(照抄美术画布那一处)、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。锚点按工作面分档:资源栏目画布与 UI 编辑器用画布顶边那一档,运行表现层下移让开右上角的版本入口;锚点是**画布那一格网格里的条目**(不是写死 `top` 的绝对定位),所以工具条换行变高也不会压上去。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。 - 面板形态:独立浮层(`ThemedModal`),**不在当前面板下面追加内容**;面板内不写功能说明或规则解释文案。**点「生成」即同步关闭面板**(不等 IPC、不等排队、不等生成),面板里**不出现**「排队中。」「正在生成。」「提交中…」这类阶段文案——阶段文案的唯一去处是任务侧栏与工具栏提示条。**只有「点击瞬间就失败」**(校验不过、权限拒绝、start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消请求(请求挂在任务与账本上,不挂在面板生命周期上)。 - 参数口径:比例 / 尺寸选项来自网页端美术画布的纯模型(`src/components/image-editor/ImageCanvasGenerationModel.ts`),并按本地 IPC 白名单收窄(本地通道明确拒绝 `4:3`);默认档 `1:1 · 1K`,生成 UI 设计图沿用网页端 UI 设计面板的默认 `16:9 · 1K`。本地 IPC 没有 `model` 入参,因此面板**不渲染模型选择器**(渲染一个改不了请求的控件就是假控件)。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 29d76aed1..4aa91f9be 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,13 @@ # 决策记录 +## 2026-10-03 AGC 栏目画布上传素材按入口栏目登记(Issue 359) + +- 背景:AGC 客户端在资源栏目子画布(「UI 交互 / 角色与对象 / 场景与环境 / 音频」)左下角工具栏点「上传」后,提示条给出「已上传 1 个素材」,但当前栏目计数不变(仍「0 项」)、素材出现在「待归类」,用户看到的是"上传成功了但它从这一页消失了"。原因是上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套,而 `upload_local_asset` 原先不接受入口栏目。 +- 决策:`upload_local_asset` 增加可选 `targetCategory`,Rust 走既有的 `register_local_asset_entry_with_category`(与生成入口 `start_local_project_asset_generation` 的 `targetCategory` **同一口径**:GUI 完成登记以入口栏目为准);前端 `uploadProjectAssetFilesAndReadSnapshot` 透传该字段,栏目画布工具栏上传取工具栏自己的栏目(`resourceCanvasBottomToolbarCategory`)。取值只接受共享分类枚举,非法值由原生失败关闭,前端不做伪分类。 +- 边界:不传 `targetCategory` 时保持既有 kind 派生行为——资源面板(`ResourceCanvasPanelView`,跨栏目列表而不是栏目工具)、UI 编辑器图片导入(`import_ui_editor_local_files`)、聊天附件上传都不变。`upload_local_asset_at` 签名保持不变(委托到新的 `upload_local_asset_at_with_category`),既有约 25 处调用点与 Rust 单测零改动。 +- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{assets.rs,commands/desktop.rs}`、`apps/ai-game-creator-shell/src/view/project-development/{projectResourceLiveUpdateModel.ts,index.tsx}`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、PRD §3.10、`docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md`(§4 载荷表)、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S11a)、`pitfalls.md`。 +- 验证:`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell assets::tests`(新增 `upload_registers_into_the_explicit_entry_category`:显式栏目 → `character`、不传 → `unclassified`、非法 `version` 失败关闭且不新增登记);`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`(196 passed / 9 skipped,含新增「uploads toolbar files into the entry column so they stay visible where they were uploaded」断言工具栏上传载荷带 `targetCategory: 'character'`);`npm run agc:typecheck`、`npm run check:encoding`、`git diff --check`。 + ## 2026-10-03 生成绑定不再经 prettier:ts-rs 原始输出即提交形态 - 背景:`scripts/check-generated-bindings.mjs` 对 AGC 的 `chat/generated` / `services/generated` 在重生成后会就地跑一遍 `npx prettier --write` 再比较。这会直接改写生成文件,还把「Rust 声明真的变了」与「prettier 版本 / 配置造成的格式漂移」混在同一条告警里——本次报出的 `ThreadRequestKind.ts` / `TurnCompletedStatus.ts`「内容变化」无法复现为语义变化(已提交内容与当前 Rust 枚举一致),prettier 归一化把格式差异也报成了「与 Rust 声明不一致」;生成物被仓库格式化工具二次改写后,重跑 `cargo test export_bindings` 也不再幂等。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 521c0a27b..02594596b 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -2,6 +2,14 @@ 这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。 +## 2026-10-03 AGC 栏目画布上传素材落「待归类」:kind 派生分类不等于入口栏目 + +- **现象**(Issue 359):在 AGC 资源栏目子画布(如「UI 交互」「角色与对象」)左下角工具栏点「上传」选图片 / 视频 / 代码类文件,提示条给出「已上传 1 个素材」,但当前栏目计数纹丝不动(仍「0 项」),素材出现在「待归类」。用户看到的是"上传成功了,可它就消失在这个页面里"。 +- **原因**:上传登记的 manifest `kind` 只由**内容证据**推导(`assets.rs::uploaded_asset_kind`:图片 / 视频 / 代码 → `unclassified`,音频 → `audio`,文档 / 字体 → `document`),kind 派生分类与栏目词汇(`ui-interaction` / `character` / `scene` / `audio`)不是同一套;`upload_local_asset` 原先不接受入口栏目,GUI 工具栏上传只能落 kind 派生分类。 +- **处理(现行口径)**:`upload_local_asset` 增加可选 `targetCategory`,Rust 走既有的 `register_local_asset_entry_with_category`(非法值失败关闭,不回退 kind 派生);栏目画布工具栏上传时取工具栏自己的栏目(`resourceCanvasBottomToolbarCategory`)。生成入口 `start_local_project_asset_generation` 的 `targetCategory` 是同一口径——GUI 完成登记以入口栏目为准。 +- **判据/取证**:`cargo test --locked ... --bin genarrative-ai-game-creator-shell upload_` 的 `assets::tests::upload_registers_into_the_explicit_entry_category`(显式栏目 → `character`;不传 → `unclassified`;非法 `version` 失败关闭且不新增登记);appSurface「uploads toolbar files into the entry column so they stay visible where they were uploaded」断言工具栏上传载荷带 `targetCategory: 'character'`。 +- **边界**:资源面板(跨栏目列表)、UI 编辑器图片导入、聊天附件上传都**不带**入口栏目,保持 kind 派生。任何新增的「某个栏目里的上传入口」都必须显式带上当前栏目,否则又会复现本坑。 + ## 2026-10-01 用户输错一次密码被当成"客户端出问题了"引导上报 - **现象**:登录页密码输错(或密码长度不合规)后弹出「发现问题」,报告面板「错误事件(2)」列出 `密码长度需要在 6 到 128 位之间 — auth · 1 次` 与 `手机号或密码错误 — auth · 1 次`,默认全选,与 react-render / 5xx / agent-runtime 终态失败视觉等价。 diff --git a/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md b/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md index 1de78d7c0..b7da13037 100644 --- a/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md +++ b/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md @@ -42,7 +42,7 @@ | 生成 UI 设计图 | 同上 | `kind: 'ui-design'`,默认 `16:9 · 1K`;前置同上 | | 生成背景音乐 | `start_local_project_asset_generation` | `kind: 'background-music'`、`idempotencyKey`;任务 id 即该次生成的 operation id | | 生成音效 | `start_local_project_asset_generation` | `kind: 'sound-effect'`、其余同上 | -| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes }` | +| 上传 | `upload_local_asset` | `{ projectPath, fileName, mediaType, bytes, targetCategory }`;`targetCategory` = 当前栏目(Issue 359) | `start_local_project_asset_generation` 的完整参数是 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath, idempotencyKey }`(Rust `src-tauri/src/asset_generation_tasks.rs`);图片类入口沿用 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }` 逐字不变,音频入口只带 `{ projectPath, projectId, taskId, kind, prompt, assetName, idempotencyKey }`(`idempotencyKey` = 该次生成的幂等键,任务 id 即 operation id;原生命令内部仍复用既有音频无源生成链路,不新增平台路由与请求体口径),**提交即返回**一条任务记录(`taskId / status / phaseDetail / assetId / error` 等);生成在 `tauri::async_runtime::spawn` 出来的后台任务里跑,账本落在项目内 `.agent/runtime/asset-generation-tasks/tasks.json`,进度由 `list_local_project_asset_generations` 读回。截图面板不再「等生成结束」,所以**点「生成」即同步关闭面板**(不等 IPC),面板内不出现阶段文案;只有「点击瞬间就失败」才带草稿重开(关闭 ≠ 取消)。 diff --git a/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md b/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md index 08df2cc5f..07b6c5b88 100644 --- a/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md +++ b/docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md @@ -57,7 +57,7 @@ | **S15** 删除 | 工具条 / 标签面板「删除」→ 确认弹窗 | 三分支:① 无引用→直接删;② 被引用未勾选→只删素材、版本保留**悬空绑定**、界面不合成幽灵资源卡;③ 勾选→素材与该批版本在**同一次 manifest 写入**内一起删 | L413(版本只追加的唯一例外)、L532–533;#309 C5 | 弹窗 `ariaLabel="确认删除资源"`;被引用时出现 `被 N 个游戏版本使用` + 版本列表 + checkbox `把相关游戏版本一并删除`;无引用时该 body 整块不渲染 | **只摘登记、不删磁盘文件**;读引用命令精确名是 `read_local_project_asset_references` | | **S15a** 替换素材(面板内选 + 画布点选目标) | 选中被**当前版本**绑定的素材 → 工具条「替换素材」→ 画布右上角出现非模态候选面板(面板内可筛可选)→ 直接在画布上点目标素材(或点面板里的候选)→ 面板「确认」 | 面板确认与画布点选是**同一条**写入链路(`replace_local_project_version_resource`,载荷逐字一致):改该版本绑定、不建新版本;画布点选只把目标落成面板的当前选择,确认后才写入 | PRD §5.3 / §7.8 第 8 条;#309「直接替换」口径 | 面板判据:`role="dialog"` 名为「选择替换素材」、挂 `.image-canvas-editor__project-asset-picker--floating`、**没有** `.platform-overlay` 遮罩;点画布候选后面板里该候选 `aria-selected="true"`、面板里的搜索词与分类筛选保持原样、零写入;非法目标(源素材本身 / 不在权威候选里 / 分类不同 / 未登记资源)在面板里出现 `role="alert"`(「替换素材与源素材相同」/「替换素材未登记或已被删除」/「替换素材不兼容:分类不同」)且零写入;面板关闭 = 卸载;Esc 只收面板(画布全局 Esc 的清选中不随之触发) | 点选只能点**当前画布上可见**的卡:目标在别的栏目时先切栏目(替换会话跨栏目存活,不因「收起资源」或切栏目结束);面板开着时空白处仍可框选 / 平移,卡片单击语义的恢复发生在面板关闭之后 | -| **S11a** 栏目画布底部工具栏 | 进「UI 交互 / 角色与对象 / 场景与环境 / 音频」任一栏目 → 点左下角工具栏里的入口生成 → 「收起资源」回总览看工具栏消失 | 工具栏只在矩阵四个栏目(功能画布)渲染;图片类入口走 `start_local_project_asset_generation`(提交即返回、生成在后台跑),音频入口走同一条命令的音频载荷,上传复用 `upload_local_asset`;生成 / 上传成功后走既有 manifest 刷新与资源定位 | PRD §3.10、§7.9 | DOM 判据 `[data-resource-bottom-toolbar=""]`(资源总览、「所有资源」展开态、文档、待归类、项目版本都**没有**这个节点);工具栏是 `.game-resource-book-manager` 的直接子节点(`closest('.game-resource-book-scene')` 为 `null`);每个入口一次 `start_local_project_asset_generation`:图片类载荷逐字为 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(`kind` 映射见 PRD §7.9 第 3 条;`taskId` 是前端每次提交新铸的本地任务 id),音频类载荷为 `{ projectPath, projectId, taskId, kind, prompt, assetName, idempotencyKey }`(`taskId` 即该次生成的 operation id,不发图片类那套比例 / 尺寸 / 参考 / 落点参数);该命令提交即返回,之后有 `list_local_project_asset_generations` 轮询与 `get_local_game_project_revision` + `get_local_game_manifest` 的配对读;点「生成」即把这次输入交给后台账本并**同步关闭**面板(不等 IPC、不等排队、不等生成,画布立即恢复可交互;面板 DOM 里没有阶段文案与「后台运行并关闭」这类在途按钮),关闭不等于取消,关闭后任务仍出现在「生成任务」面板(入口按钮 `aria-label="生成任务"`,非模态浮层、无 `aria-modal`)并显示后端 `phaseDetail`;第一条未终态时提交第二条 → 第二条显示「排队中。」且生成提交 IPC 次数仍为 1,第一条终态后自动补发(次数变 2);缺 `assets/art-spec.png` 时「生成图标素材 / 生成 UI 设计图」仍可点击(`aria-disabled="true"` 但**不是**原生 disabled)并给出含该路径的 `role="alert"` 原因、零生成请求;音频入口面板标题即「生成背景音乐」/「生成音效」且没有类型选择器 | ① 本地通道没有 `model` / `specType` / `replaceExisting` 入参:面板不渲染模型选择器,角色规范与自定义规范共用 `spec` 通道(靠 `assetName` 与提示词区分),「图标规范」在项目已有权威规范图时不再指向 `assets/art-spec.png`(否则会被 Rust 的防覆盖校验硬拒);② 本轮不做生成视频 / 宣发素材 / 生成游戏场景 / 选择工具 / 抓手工具;③ 既有「生成素材」浮层入口只保留生成视频,音频入口只在音频栏目工具栏出现 | +| **S11a** 栏目画布底部工具栏 | 进「UI 交互 / 角色与对象 / 场景与环境 / 音频」任一栏目 → 点左下角工具栏里的入口生成 → 「收起资源」回总览看工具栏消失 | 工具栏只在矩阵四个栏目(功能画布)渲染;图片类入口走 `start_local_project_asset_generation`(提交即返回、生成在后台跑),音频入口走同一条命令的音频载荷,上传复用 `upload_local_asset` 并带上当前栏目 `targetCategory`(素材落在上传它的那一栏,不落「待归类」);生成 / 上传成功后走既有 manifest 刷新与资源定位 | PRD §3.10、§7.9 | DOM 判据 `[data-resource-bottom-toolbar=""]`(资源总览、「所有资源」展开态、文档、待归类、项目版本都**没有**这个节点);工具栏是 `.game-resource-book-manager` 的直接子节点(`closest('.game-resource-book-scene')` 为 `null`);每个入口一次 `start_local_project_asset_generation`:图片类载荷逐字为 `{ projectPath, projectId, taskId, kind, prompt, aspectRatio, imageSize, assetName, outputPath }`(`kind` 映射见 PRD §7.9 第 3 条;`taskId` 是前端每次提交新铸的本地任务 id),音频类载荷为 `{ projectPath, projectId, taskId, kind, prompt, assetName, idempotencyKey }`(`taskId` 即该次生成的 operation id,不发图片类那套比例 / 尺寸 / 参考 / 落点参数);该命令提交即返回,之后有 `list_local_project_asset_generations` 轮询与 `get_local_game_project_revision` + `get_local_game_manifest` 的配对读;点「生成」即把这次输入交给后台账本并**同步关闭**面板(不等 IPC、不等排队、不等生成,画布立即恢复可交互;面板 DOM 里没有阶段文案与「后台运行并关闭」这类在途按钮),关闭不等于取消,关闭后任务仍出现在「生成任务」面板(入口按钮 `aria-label="生成任务"`,非模态浮层、无 `aria-modal`)并显示后端 `phaseDetail`;第一条未终态时提交第二条 → 第二条显示「排队中。」且生成提交 IPC 次数仍为 1,第一条终态后自动补发(次数变 2);缺 `assets/art-spec.png` 时「生成图标素材 / 生成 UI 设计图」仍可点击(`aria-disabled="true"` 但**不是**原生 disabled)并给出含该路径的 `role="alert"` 原因、零生成请求;音频入口面板标题即「生成背景音乐」/「生成音效」且没有类型选择器 | ① 本地通道没有 `model` / `specType` / `replaceExisting` 入参:面板不渲染模型选择器,角色规范与自定义规范共用 `spec` 通道(靠 `assetName` 与提示词区分),「图标规范」在项目已有权威规范图时不再指向 `assets/art-spec.png`(否则会被 Rust 的防覆盖校验硬拒);② 本轮不做生成视频 / 宣发素材 / 生成游戏场景 / 选择工具 / 抓手工具;③ 既有「生成素材」浮层入口只保留生成视频,音频入口只在音频栏目工具栏出现 | ### D 阶段 · 版本与运行 From 86e74867ae414f57912d6a859668cca1597cec36 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Sat, 3 Oct 2026 17:42:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?review=20=E5=8F=8D=E9=A6=88=EF=BC=9A?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=20targetCategory=20=E7=9A=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=8E=E6=B3=A8=E9=87=8A=E5=8F=A3=E5=BE=84=EF=BC=88?= =?UTF-8?q?#359=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实施计划合同补上「GUI 上传与图片类生成同一 targetCategory 口径,不传时仍按内容证据派生 kind」 - 栏目画布底部工具栏入口矩阵 §1 一句话补上「带当前栏目 targetCategory」 - 工具栏模型里上传入口的类型注释写明入口栏目与「待归类」的因果 - 修正占位落点 effect 里陈旧的示例:普通图片只在 Agent / Direct 不传 targetCategory 时才落「待归类」 --- .../resource-canvas/resourceCanvasBottomToolbarModel.ts | 8 +++++++- .../src/view/project-development/index.tsx | 5 +++-- .../【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md | 2 +- .../【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasBottomToolbarModel.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasBottomToolbarModel.ts index 429d83a34..9b848ccbc 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasBottomToolbarModel.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasBottomToolbarModel.ts @@ -99,7 +99,13 @@ export type ResourceCanvasAudioToolAction = audioKind: ResourceCanvasGenerationKind; }; -/** 上传入口:复用既有 `upload_local_asset`。 */ +/** + * 上传入口:复用既有 `upload_local_asset`。 + * + * 调用方按当前栏目带 `targetCategory`(见 `uploadResourceCanvasToolbarFiles`)——上传的 + * manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都会派生成 `unclassified`, + * 不带入口栏目素材就落「待归类」、在上传它的那一栏里看不见。 + */ export type ResourceCanvasUploadToolAction = ResourceCanvasBottomToolActionBase & { route: 'upload'; 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 cfabbccf2..50e0c00dc 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 @@ -3348,8 +3348,9 @@ export default function ProjectDevelopmentView({ * 两条判据缺一不可: * 1. 这张卡已经进了布局(`resourcePositionById`),否则 reconcile 还没补位、写坐标是空操作; * 2. 这张卡已经有正式归类(投影里的 `category`)——`commitPosition` 的 `section` 必须与该资源 - * 在 sidecar 里的 section 一致,否则会被静默跳过。生成结果的实际归类与入口栏目本来就可能不同 - * (普通图片落「待归类」、规范落「文档」),所以这里按**正式归类**写,而不是入口栏目。 + * 在 sidecar 里的 section 一致,否则会被静默跳过。生成结果的实际归类与入口栏目**不一定相同** + * (Agent / Direct 路径不传 `targetCategory`,普通图片仍落「待归类」),所以这里按**正式归类**写, + * 而不是入口栏目。 * * 写完撤掉占位:结果已经接管了它的位置。占位被用户先删掉时同样清掉这条意图(没有位置可接管)。 */ diff --git a/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md b/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md index b7da13037..ab8643343 100644 --- a/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md +++ b/docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md @@ -6,7 +6,7 @@ ## 1. 一句话 -功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类与音频入口都走本地 `start_local_project_asset_generation`(提交即返回、后台生成,见 2026-09-20 音频并入后台任务账本),上传复用 `upload_local_asset`;生成 / 上传成功后一律走既有 manifest 刷新与资源定位。 +功能画布 = 资源栏目页;栏目页左下角渲染一条由栏目 `category` 决定的工具栏,图片类与音频入口都走本地 `start_local_project_asset_generation`(提交即返回、后台生成,见 2026-09-20 音频并入后台任务账本),上传复用 `upload_local_asset`(带当前栏目 `targetCategory`,素材登记进上传它的那一栏);生成 / 上传成功后一律走既有 manifest 刷新与资源定位。 ## 2. 入口矩阵(事实源) diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 4faeabece..28eb88164 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -329,7 +329,7 @@ Rust 侧在 `server-rs/crates/shared-contracts` 维护唯一权威 `GameCreation - 参考选择范围为同一项目已登记图片,可跨栏目、多选,无规范前置时最多 5 张,有规范前置时最多 4 张用户参考(总计最多 5 张);复用资源引用选择组件,不允许文档、音视频、占位或跨项目素材。本地生成命令补最小引用 ID 参数并转换为当前账号绑定下的远端资源 ID,沿用图片生成 API 已有 `referenceImageSrcs`。需要规范图的普通图片请求合并并去重规范引用,总数不超过现有 API 限制;只接受单规范引用的图集操作不显示用户参考选择器,原生提交拒绝额外参考而非静默丢弃。不得降级成纯提示词。 - 占位由宿主按项目与独立草稿 ID 管理,提交后关联任务 ID;失败重试使用同一占位。切项目清理未提交草稿与界面位置,已提交任务继续沿用账本恢复,重开后不承诺恢复未持久化的占位位置。迟到结果先核对项目和任务归属;只有本会话仍存在的占位才应用最新位置。删除占位只隐藏展示,不取消后台任务或丢弃正式结果。 - 参考必须是原生可解码的栅格图片,SVG 不进入参考候选;原生在上传任何引用前预校验整组素材的归属、受控路径、文件及解码,失败不静默丢图。需要重新上传当前账号绑定的参考遵循 `asset.upload` 权限。manifest 读侧的引用形状检查不证明远端账号归属,实际生成始终通过当前账号 binding 解析,不凭历史来源 ID 发起请求。 -- 图片类 GUI 生成通过可选 `targetCategory` 在原生登记时写入入口栏目,使用既有 manifest `category` 字段及合法分类词表;不传时保留按 kind 派生的行为,Agent 不传。该值不改变远端生成内容及计费幂等槽,仅决定本地生成结果分类;重试保持原占位栏目。同路径重新生成时,主产物按本次入口栏目更新分类(包含覆盖此前手动分类),图集附属切片保持既有独立分类规则。 +- 图片类 GUI 生成通过可选 `targetCategory` 在原生登记时写入入口栏目,使用既有 manifest `category` 字段及合法分类词表;不传时保留按 kind 派生的行为,Agent 不传。该值不改变远端生成内容及计费幂等槽,仅决定本地生成结果分类;重试保持原占位栏目。同路径重新生成时,主产物按本次入口栏目更新分类(包含覆盖此前手动分类),图集附属切片保持既有独立分类规则。GUI 上传(栏目画布底部工具栏的 `upload_local_asset`)与图片类生成**同一口径**:带可选 `targetCategory` 在当前栏目登记,不传(资源面板、UI 编辑器导入、聊天附件)时保持按内容证据派生 kind 的行为。 - 生成面板打开后,占位和面板需处于当前画布标题栏与底部工具栏之间;面板复用公共外观,空间不足时面板内部滚动,提交按钮可达。音频/BGM 占位绑定原有 operation/idempotency 身份,进行中不允许换身份重复提交;失败可用原身份重试,成功结果与图片一样接管占位。关闭未提交浮层保留可继续编辑的草稿,删除占位才丢弃该草稿。 - 整理范围为当前栏目页全部资源;“所有资源”页为当前项目所有可展示资源,总览不新增整理行为。重排结果成为自动坐标,可撤销恢复原坐标与手动标记;历史仅保留当前会话,切项目清空。多选仅作用于当前画布可见选中资源,不携带筛选隐藏或跨栏目残留选择;取消手势恢复拖动前坐标,切项目清空选择。 - 当前素材名以现有正式命名链路为准:生成时 assetName 参与落盘名称,重命名更新文件名;卡片消费正式资源 label,不从临时输入或历史任务名覆盖后续重命名,不新增平行显示名持久化。若原有命名链路丢失 assetName,则修复原链路,而非只在卡片本地伪造。文档卡不显示任何正文摘要,但详情原文与 JSON 识别读取不变。