From 2f092c35669718fa966b5adcd4f919a48a85a917 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 15 Jul 2026 03:04:41 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E7=B4=A0=E6=9D=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=B1=82=E7=BA=A7=E4=B8=8E=E6=88=90=E6=9C=AC=E5=BD=92?= =?UTF-8?q?=E5=9B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后台素材查询按任务分页并将中间产物逐行收纳为子项 生成原始产物沿用真实素材类型并承载任务生成成本 抠图、拆分和逐帧后处理记录实际生成器且阶段成本归零 补齐历史类型映射、角色动作链路、测试与文档 --- apps/admin-web/src/api/adminApiTypes.ts | 4 + .../pages/AdminEditorAssetQueryPage.test.tsx | 72 +++- .../src/pages/AdminEditorAssetQueryPage.tsx | 257 +++++++++---- apps/admin-web/src/styles/admin.css | 63 +++- .../shared-memory/decision-log.md | 2 +- docs/project-memory/shared-memory/pitfalls.md | 4 +- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 2 +- ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 2 +- server-rs/crates/api-server/src/admin.rs | 267 +++++++++++++- .../src/character_animation_assets.rs | 57 +-- .../crates/api-server/src/editor_project.rs | 328 ++++++++++++----- .../crates/shared-contracts/src/admin.rs | 4 + .../src/editor_project_storage.rs | 339 ++++++++++++++++-- .../image-editor/ImageCanvasEditorModel.ts | 1 - .../image-editor/ImageCanvasEditorTypes.ts | 1 - .../ImageCanvasWorldView.test.tsx | 4 +- .../image-editor/ImageCanvasWorldView.tsx | 5 - ...anvasGenerationSubmissionWorkflow.test.tsx | 4 +- 18 files changed, 1161 insertions(+), 255 deletions(-) diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index b21413bde..8841b9ae0 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -467,6 +467,10 @@ export interface AdminEditorAssetPayload { generationCostMudPoints: number; createdAt: string; updatedAt: string; + generator: string; + taskGenerator: string; + taskCostMudPoints: number; + children: AdminEditorAssetPayload[]; } export interface AdminEditorAssetListResponse { diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx index ddbc7a502..9d360b81c 100644 --- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx @@ -148,6 +148,10 @@ const generatedAsset: AdminEditorAssetPayload = { generationCostMudPoints: 12, createdAt: '2026-07-04T10:00:00Z', updatedAt: '2026-07-04T10:00:00Z', + generator: '角色抠图', + taskGenerator: '角色生成', + taskCostMudPoints: 12, + children: [], }; beforeEach(() => { @@ -245,17 +249,45 @@ test('后台素材查询不展示分类筛选和分类列', async () => { expect(screen.queryByRole('columnheader', { name: '分类' })).toBeNull(); }); -test('后台素材查询将中间产物标记为不单独计费', async () => { +test('后台素材查询将中间产物按真实类型逐行收纳在最终产物下', async () => { + const providerSource: AdminEditorAssetPayload = { + ...generatedAsset, + assetId: 'asset-source', + label: '角色形象生成原图', + objectKey: + 'generated-character-drafts/editor/character-images/provider-source-task-1.png', + imageSrc: + '/generated-character-drafts/editor/character-images/provider-source-task-1.png', + assetKind: 'character', + generator: '角色生图', + taskGenerator: '角色生成', + generationCostMudPoints: 12, + taskCostMudPoints: 12, + children: [], + }; + const postprocessOutput: AdminEditorAssetPayload = { + ...generatedAsset, + assetId: 'asset-postprocess', + label: '角色抠图阶段产物', + objectKey: + 'generated-character-drafts/editor/character-images/background-removal-task-1.png', + imageSrc: + '/generated-character-drafts/editor/character-images/background-removal-task-1.png', + assetKind: 'character', + generator: '角色抠图', + taskGenerator: '角色生成', + generationCostMudPoints: 0, + taskCostMudPoints: 0, + children: [], + }; vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({ entries: [ { ...generatedAsset, - assetId: 'asset-source', - label: '角色形象原图', - assetKind: 'editor_green_screen_source', generationCostMudPoints: 0, + taskCostMudPoints: 12, + children: [providerSource, postprocessOutput], }, - generatedAsset, ], nextCursor: null, }); @@ -264,19 +296,31 @@ test('后台素材查询将中间产物标记为不单独计费', async () => { , ); - const sourceRow = (await screen.findByText('角色形象原图')).closest('tr'); - const finalRow = screen.getByText('角色形象 1').closest('tr'); - expect(sourceRow).not.toBeNull(); + const finalRow = (await screen.findByText('角色形象 1')).closest('tr'); expect(finalRow).not.toBeNull(); - expect(within(sourceRow!).getByText('不单独计费')).toBeTruthy(); expect(within(finalRow!).getByText('12 泥点')).toBeTruthy(); + expect(screen.queryByText('角色形象生成原图')).toBeNull(); + expect(screen.queryByText('角色抠图阶段产物')).toBeNull(); + + fireEvent.click( + within(finalRow!).getByRole('button', { name: '展开中间产物' }), + ); + + const sourceRow = (await screen.findByText('角色形象生成原图')).closest('tr'); + const postprocessRow = screen.getByText('角色抠图阶段产物').closest('tr'); + expect(sourceRow).not.toBeNull(); + expect(postprocessRow).not.toBeNull(); + expect(within(sourceRow!).getByText('角色生图')).toBeTruthy(); + expect(within(sourceRow!).getByText('12 泥点')).toBeTruthy(); + expect(within(postprocessRow!).getByText('角色抠图')).toBeTruthy(); + expect( + within(postprocessRow!).getByText('本阶段不额外扣费'), + ).toBeTruthy(); fireEvent.click(within(sourceRow!).getByRole('button', { name: '详情' })); - expect( - within(await screen.findByRole('dialog', { name: '素材详情' })).getByText( - '不单独计费', - ), - ).toBeTruthy(); + const detail = await screen.findByRole('dialog', { name: '素材详情' }); + expect(within(detail).getByText('角色生图')).toBeTruthy(); + expect(within(detail).getByText('12 泥点')).toBeTruthy(); }); test('后台素材查询缩略图使用 objectKey 换签后展示', async () => { diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx index d8276fe8f..e23a00aae 100644 --- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx @@ -1,5 +1,5 @@ -import { Eye, FileText, RefreshCcw, X } from 'lucide-react'; -import type { ReactNode } from 'react'; +import { ChevronDown, ChevronRight, Eye, FileText, RefreshCcw, X } from 'lucide-react'; +import { Fragment, type ReactNode } from 'react'; import { useCallback, useEffect, useState } from 'react'; import { @@ -24,10 +24,6 @@ const ADMIN_ASSET_READ_EXPIRE_SECONDS = 300; const ADMIN_ASSET_READ_DISPATCH_SPACING_MS = 40; const ADMIN_ASSET_READ_RETRY_DELAYS_MS = [400, 1_200, 3_000] as const; const ADMIN_ASSET_THUMBNAIL_ROOT_MARGIN = '240px 0px'; -const NON_BILLED_INTERMEDIATE_ASSET_KINDS = new Set([ - 'editor_green_screen_source', - 'editor_image_edit_source', -]); const AUDIO_ASSET_COVER_SRC = `${import.meta.env.DEV ? import.meta.env.BASE_URL : '/'}creation-home/audio-asset-cover.png`; let adminAssetReadDispatchTail = Promise.resolve(); @@ -48,6 +44,9 @@ export function AdminEditorAssetQueryPage({ useState(null); const [previewEntry, setPreviewEntry] = useState(null); + const [expandedTaskIds, setExpandedTaskIds] = useState>( + () => new Set(), + ); const [promptPreview, setPromptPreview] = useState<{ title: string; prompt: string; @@ -153,7 +152,7 @@ export function AdminEditorAssetQueryPage({