From 3cca4fb99c2e46bb37270ba2f79c79ea3fd7a3e9 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Sun, 4 Oct 2026 13:11:37 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=89=88=E6=9C=AC=E5=8D=A1?= =?UTF-8?q?=E6=8C=89=E5=86=85=E9=83=A8=E7=89=88=E6=9C=AC=E5=BA=8F=E6=95=B0?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=EF=BC=8C=E4=BF=AE=E6=AD=A3=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E8=A2=AB=E6=8E=92=E5=88=B0=E6=9C=80=E5=90=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 根因:版本投影的显示顺序走依赖视图、同层按 opaque 资源 id 做字符串排序,而 `version:initial-*` 的字符串序大于 `version:agent-*`,于是初始版本被排到最后(4 个版本读作 2/3/4/1,既与卡面「版本 N」序号冲突,也与「最新版本 = 发布标签」冲突;已落盘坐标 agent-22(0,0) agent-42(228,0) agent-46(0,168) initial-13(228,168) 为证) - 改法:`ProjectResource` / `ResourceCanvasItem` 增加 `orderIndex`(= 内部版本序数,仅项目版本资源携带),新增 `compareResourceOrder`(先比序键、再回落 opaque id)并替换依赖视图 4 处 `compareStableIdentifier`;类型视图在 mediaType 之后插入序键比较(顺带修掉 ≥10 个版本时「版本 10」被字符串序排到「版本 2」之前的错位) - 兼容性:不带 `orderIndex` 的其它资源排序结果与落盘坐标逐值不变 - 验证:`npx vitest run apps/ai-game-creator-shell/tests` 199 文件 / 1935 passed / 0 failed;`npm run agc:typecheck` exit 0;`npm run check:encoding` passed;`git diff --check` 干净;真机 CDP 用 Vite 现网真实模块 + 真实 manifest 复算落位:修复前 2→3→4→5→1,修复后 1→2→3→4→5 --- .../resourceCanvasLayoutModel.ts | 40 +++++++++- .../resourceProjectionModel.ts | 11 +++ .../tests/resourceCanvasLayoutModel.test.ts | 76 +++++++++++++++++++ 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts index 97ba9a92b..3c2b47a49 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceCanvasLayoutModel.ts @@ -114,6 +114,11 @@ export type ResourceCanvasItem = { label: string; mediaType: string; dependencyDepth: number; + /** + * 栏目内显式次序键(可选)。同层落位先按它升序,缺省时退回 opaque `id`: + * 资源 id 只是标识,只有自带线性序的资源(如「项目版本」的 `版本 N`)才给这个键。 + */ + orderIndex?: number; cardSize?: ResourceCanvasCardSize; }; @@ -408,6 +413,24 @@ function compareStableIdentifier(left: string, right: string) { return left < right ? -1 : left > right ? 1 : 0; } +/** + * 同层内的落位次序:先按资源自带的显式次序键 `orderIndex`,缺省视为同一序,再退回 opaque id。 + * + * 资源 id 只是标识(`version:{versionId}` 里的 versionId 是 `initial-*` / `agent-*`), + * 拿它当次序会让「项目版本」栏目排成 `agent-*` 在前、`initial-*` 在后 —— 与卡面 + * `版本 N`(内部版本序数)不一致,也看不出哪张是最新版本。只有声明了显式次序的资源 + * (当前就是项目版本)受影响,其余资源一律保持原有的 id 序,落位逐值不变。 + */ +function compareResourceOrder( + resourceById: ReadonlyMap, + leftId: string, + rightId: string, +) { + const leftOrder = resourceById.get(leftId)?.orderIndex ?? 0; + const rightOrder = resourceById.get(rightId)?.orderIndex ?? 0; + return leftOrder - rightOrder || compareStableIdentifier(leftId, rightId); +} + function compareResources( mode: ProjectResourceCanvasLayoutMode, left: ResourceCanvasItem, @@ -423,6 +446,9 @@ function compareResources( return ( compareStableIdentifier(left.subtype, right.subtype) || compareStableIdentifier(left.mediaType, right.mediaType) || + // 自带显式次序的资源(项目版本)先按序数:卡面 `版本 N` 按字符串比会在 10 版之后错位 + // (`版本 10` 排在 `版本 2` 前)。没有序键的资源一律退回原有的标签序。 + (left.orderIndex ?? 0) - (right.orderIndex ?? 0) || left.label.localeCompare(right.label, 'zh-CN') || compareStableIdentifier(left.id, right.id) ); @@ -817,7 +843,11 @@ function sortDependencyBucket( } return ( left.previousRank - right.previousRank || - compareStableIdentifier(left.minimumResourceId, right.minimumResourceId) + compareResourceOrder( + resourceById, + left.minimumResourceId, + right.minimumResourceId, + ) ); }); resourceIds.splice( @@ -828,7 +858,7 @@ function sortDependencyBucket( (left, right) => (rankByResourceId.get(left) ?? 0) - (rankByResourceId.get(right) ?? 0) || - compareStableIdentifier(left, right), + compareResourceOrder(resourceById, left, right), ), ), ); @@ -977,7 +1007,7 @@ function dependencyAutomaticPositions( .filter((component) => !component.related) .flatMap((component) => component.resourceIds) .filter((resourceId) => automaticIds.has(resourceId)) - .sort(compareStableIdentifier); + .sort((left, right) => compareResourceOrder(resourceById, left, right)); const clusters: DependencyCluster[] = []; for (const component of relatedComponents) { @@ -1058,7 +1088,9 @@ function dependencyAutomaticPositions( idsByDepth.set(resource.dependencyDepth, ids); } for (const [depth, resourceIds] of idsByDepth) { - resourceIds.sort(compareStableIdentifier); + resourceIds.sort((left, right) => + compareResourceOrder(resourceById, left, right), + ); const grid = dependencyLayerGrid(resourceIds, cardSizeByResourceId); gridByBucket.set(resourceIds, grid); // 层内 rank 用"网格行号",同一行的相邻卡共享纵向位置,跨层中位数对齐仍然成立。 diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts index b30c063d6..4b110f41e 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceProjectionModel.ts @@ -49,6 +49,15 @@ export type ProjectResource = { referenceResourceIds: string[]; dependencies: string[]; dependencyDepth: number; + /** + * 栏目内显式次序键:同类同层资源按它升序落位,缺省时退回 opaque 资源 id。 + * + * 「项目版本」卡必须带它:卡面 `版本 N` 就是 AGC 内部版本序数(= `manifest.versions` + * 的下标 + 1,也是发布面板取的标签),而版本资源 id 是 `version:{versionId}`, + * versionId 又是 `initial-*` / `agent-*` 这类 opaque 标识。按 id 排会把初始版本 + * (`initial-*` 字符串序大于 `agent-*`)排到最后,与卡面序号和「最新版本」都对不上。 + */ + orderIndex?: number; content?: string; version?: ProjectVersionResourceSummary; /** @@ -530,6 +539,8 @@ export function projectResourcesFromReadModels( referenceResourceIds: [], dependencies: [], dependencyDepth: 0, + // 显示次序 = `manifest.versions` 下标 = 卡面 `版本 N` 的序号 = 发布面板取的标签。 + orderIndex: index, version, }); } diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasLayoutModel.test.ts b/apps/ai-game-creator-shell/tests/resourceCanvasLayoutModel.test.ts index 1310098b7..e445ec2e9 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasLayoutModel.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceCanvasLayoutModel.test.ts @@ -372,6 +372,82 @@ describe('resource canvas layout model', () => { ]); }); + it('项目版本卡按内部版本序数落位,而不是按 opaque 资源 id', () => { + // 版本资源 id 里的 versionId 是 opaque 标识:`initial-13` 的字符串序大于 `agent-22`。 + // 没有显式次序键时,依赖视图会把初始版本排到最后 —— 卡面 `版本 1` 落在最右, + // 与「最新内部版本 = 版本 N」以及发布面板取的标签(`versions.length`)都对不上。 + const rows = [ + ['initial-13', 0], + ['agent-22', 1], + ['agent-42', 2], + ['agent-46', 3], + ['agent-69', 4], + ] as const; + + const withOrderIndex = reconcileResourceCanvasLayout( + createEmptyResourceCanvasLayout('project-version-order', 'dependency'), + rows.map(([versionId, orderIndex]) => ({ + ...resource(`version:${versionId}`, 'version'), + subtype: 'project-version', + mediaType: '正式项目版本', + label: `版本 ${orderIndex + 1}`, + orderIndex, + })), + ).layout; + + // 阅读次序 = 先上后下、先左后右;它必须与 `版本 N` 的序号一致。 + expect( + [...withOrderIndex.positions] + .sort((left, right) => left.y - right.y || left.x - right.x) + .map((position) => position.resourceId), + ).toEqual(rows.map(([versionId]) => `version:${versionId}`)); + + // 对照组:不带显式次序键时落位退回 opaque id,`initial-*` 排到最后 —— 这就是要修的观感。 + const withoutOrderIndex = reconcileResourceCanvasLayout( + createEmptyResourceCanvasLayout('project-version-order', 'dependency'), + rows.map(([versionId, orderIndex]) => ({ + ...resource(`version:${versionId}`, 'version'), + subtype: 'project-version', + mediaType: '正式项目版本', + label: `版本 ${orderIndex + 1}`, + })), + ).layout; + + expect( + [...withoutOrderIndex.positions] + .sort((left, right) => left.y - right.y || left.x - right.x) + .map((position) => position.resourceId), + ).toEqual([ + 'version:agent-22', + 'version:agent-42', + 'version:agent-46', + 'version:agent-69', + 'version:initial-13', + ]); + }); + + it('类型视图的项目版本也按序数排,不按 `版本 N` 的字符串序', () => { + // 10 版之后 `版本 10` 的字符串序小于 `版本 2`,按 label 比会错位;显式序键不会。 + const indexes = Array.from({ length: 10 }, (_unused, index) => index); + + const typeLayout = reconcileResourceCanvasLayout( + createEmptyResourceCanvasLayout('project-version-type-order', 'type'), + indexes.map((index) => ({ + ...resource(`version:agent-${index + 1}`, 'version'), + subtype: 'project-version', + mediaType: '正式项目版本', + label: `版本 ${index + 1}`, + orderIndex: index, + })), + ).layout; + + expect( + [...typeLayout.positions] + .sort((left, right) => left.y - right.y || left.x - right.x) + .map((position) => position.resourceId), + ).toEqual(indexes.map((index) => `version:agent-${index + 1}`)); + }); + it('keeps connected art cards close while placing separate components before isolates', () => { const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-clusters', 'dependency'),