图集切片模式改为必须显式声明并补齐决策要求 (#408)
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
## 背景 切图新增基于连通域的切分后,LLM 仍倾向显式传 `sliceMode=grid`:参数只存在于部分 LLM 可见面、带默认值、没有任何决策规则,生成结果也不回显生效模式。 ## 变更 - 平台:`/api/editor/icon-spritesheets/generations` 与 `/api/external/v1/editor/icon-spritesheets/generations` 把 `sliceMode` 改为必填并移除默认值;缺失、空白或未知取值在引用解析、定价与任何 provider / OSS 副作用之前返回 `400`,错误统一带 `field` 与决策要求。 - 契约:`grid` 必须同时提供 `gridX`/`gridY`,`connected-components` 不接受网格尺寸;`sliceCount` 只约束连通域切分,请求与响应的公开上限统一为 `256`;OpenAPI 去掉默认值并补必填与失败语义。 - AGC:MCP 工具说明去掉默认值并补决策要求,桥接层新增可测试的切分声明校验;原生工具 `canvas.asset_generate` 暴露 `sliceMode/gridX/gridY/sliceCount` 并要求图集显式声明;生成结果回显 `sliceMode/gridX/gridY` 与 `slicePaths`;严格图集在本地提交前校验平台回显与请求声明一致。 - 标准美术包:显式声明 `connected-components` 加 `sliceCount=4`,并在四张 canonical 切片用途映射前校验数量,禁止截断或错位。 - 前端与画板:画板 Agent 工具装配与画板提交计划显式声明连通域切分;前端类型要求显式 `sliceMode` 并在本地校验声明自洽。 - 文档与 Skill:主规范、OpenAPI、AGC Skill、外部编辑器 Skill、里程碑与实施计划、共享决策记录同步更新。 - 测试环境:测试构建对提权 Windows 主机上系统临时目录的所有者偏差做一次性所有者初始化重试,临时目录之外的越权所有者继续失败关闭。 ## 兼容性影响 省略 `sliceMode` 的旧调用方(含已发布但未更新的 AGC 客户端与第三方外部 API 调用方)会在图集生成上收到 `400`;这是本次"不允许默认值"的预期结果,仓库内自有调用方已全部改为显式声明。 ## 验证 - 平台:`slice_mode_must_be_declared_*` 与 OpenAPI 契约测试通过;全量 `cargo test -p api-server` 1043 通过 / 11 失败(`wallet_refund_outbox` 临时文件 `拒绝访问`,已在改动前基线复现,属本机环境)。 - AGC:`slice` 30、`spritesheet` 21、`direct_tools_mcp` 23、`agent_native_tools` 16、`canvas_generation_tests` 83、提示词上限与桥接门禁各 1 条、`cargo check --tests` 全部通过。 - 前端:182 条定向测试与 `typecheck` 通过。 - 门禁:`cargo fmt --check`(两个 workspace)、`check:encoding`、`check:doc-index`、`git diff --check` 通过。 - 未验证:真实 Provider 与浏览器试玩、确定性 e2e 车道;整机全量 AGC 单进程运行在本机受提权 shell 的所有者与时序问题影响,不作为门禁信号。 --------- Co-authored-by: kdletters <61648117+kdletters@users.noreply.github.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/408
This commit was merged in pull request #408.
This commit is contained in:
@@ -2749,6 +2749,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect.objectContaining({
|
||||
referenceId: 'resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮\n设置按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
|
||||
@@ -1080,6 +1080,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
referenceId: 'resource-icon-spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
iconDescriptions: ['返回按钮\n\n设置按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: 'auto',
|
||||
segModel: 'birefnet',
|
||||
|
||||
@@ -1136,6 +1136,9 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
|
||||
}
|
||||
: {}),
|
||||
iconDescriptions,
|
||||
// 切分模式没有默认值:画板链路按自由排布生成图标表,因此显式声明连通域切分;
|
||||
// 等分网格或固定槽位需求必须由调用方显式传 grid + gridX/gridY。
|
||||
sliceMode: 'connected-components',
|
||||
model: rememberImageModel,
|
||||
screenColor,
|
||||
segModel,
|
||||
|
||||
@@ -4249,6 +4249,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
'generated-character-drafts/editor/refs/icon-style.png',
|
||||
],
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1112,6 +1112,7 @@ describe('editorProjectClient', () => {
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: references.slice(0, 5),
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gpt-image-2',
|
||||
}),
|
||||
).rejects.toThrow('图标素材参考图最多允许 4 张');
|
||||
@@ -1162,6 +1163,29 @@ describe('editorProjectClient', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('requires an explicit slice declaration and rejects contradictory grid dimensions', async () => {
|
||||
requestJsonMock.mockResolvedValue({ queueState: { status: 'queued' } });
|
||||
|
||||
await expect(
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'grid',
|
||||
}),
|
||||
).rejects.toThrow('sliceMode=grid 必须同时提供 gridX 与 gridY');
|
||||
await expect(
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-icon-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
gridX: 2,
|
||||
gridY: 2,
|
||||
}),
|
||||
).rejects.toThrow('sliceMode=connected-components 不接受 gridX/gridY');
|
||||
|
||||
expect(requestJsonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects oversized stable video reference fields before submission', async () => {
|
||||
await expect(
|
||||
generateEditorVideo({
|
||||
@@ -1331,6 +1355,9 @@ describe('editorProjectClient', () => {
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
sliceMode: 'grid',
|
||||
gridX: 3,
|
||||
gridY: 2,
|
||||
assetLabel: '冒险游戏图标',
|
||||
});
|
||||
|
||||
@@ -1348,6 +1375,9 @@ describe('editorProjectClient', () => {
|
||||
referenceId: 'editor-resource-spec',
|
||||
referenceImageSrcs: ['/generated-images/editor/icon-ref.png'],
|
||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||
sliceMode: 'grid',
|
||||
gridX: 3,
|
||||
gridY: 2,
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
assetLabel: '冒险游戏图标',
|
||||
}),
|
||||
@@ -1365,6 +1395,7 @@ describe('editorProjectClient', () => {
|
||||
generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS + 1)],
|
||||
sliceMode: 'connected-components',
|
||||
}),
|
||||
).rejects.toThrow(`不能超过 ${EDITOR_ICON_DESCRIPTION_MAX_CHARS} 个字符`);
|
||||
|
||||
@@ -1377,6 +1408,7 @@ describe('editorProjectClient', () => {
|
||||
'图'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
|
||||
),
|
||||
],
|
||||
sliceMode: 'connected-components',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS} 个字符`,
|
||||
@@ -1388,6 +1420,7 @@ describe('editorProjectClient', () => {
|
||||
iconDescriptions: Array.from({ length: 8 }, () =>
|
||||
'😀'.repeat(EDITOR_ICON_DESCRIPTION_MAX_CHARS),
|
||||
),
|
||||
sliceMode: 'connected-components',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
`合计不能超过 ${EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES} 个 UTF-8 字节`,
|
||||
@@ -1412,6 +1445,7 @@ describe('editorProjectClient', () => {
|
||||
await generateEditorIconSpritesheet({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
segModel: 'anime-seg',
|
||||
@@ -1434,6 +1468,7 @@ describe('editorProjectClient', () => {
|
||||
body: JSON.stringify({
|
||||
referenceId: 'editor-resource-spec',
|
||||
iconDescriptions: ['返回按钮'],
|
||||
sliceMode: 'connected-components',
|
||||
model: 'gpt-image-2',
|
||||
screenColor: '#E6D8FF',
|
||||
segModel: 'anime-seg',
|
||||
|
||||
@@ -380,7 +380,8 @@ export type EditorIconSpritesheetGenerationInput = {
|
||||
referenceId: string;
|
||||
referenceImageSrcs?: string[];
|
||||
iconDescriptions: string[];
|
||||
sliceMode?: 'connected-components' | 'grid';
|
||||
/** 必填且没有默认值:必须显式声明连通域或网格切分。 */
|
||||
sliceMode: 'connected-components' | 'grid';
|
||||
gridX?: number;
|
||||
gridY?: number;
|
||||
model?: string;
|
||||
@@ -1300,6 +1301,16 @@ export async function generateEditorIconSpritesheet(
|
||||
input.iconDescriptions,
|
||||
);
|
||||
const model = input.model?.trim() || EDITOR_IMAGE_MODEL_NANOBANANA2;
|
||||
// 切分模式没有默认值:声明必须自洽,网格尺寸只能与 grid 同时提交。
|
||||
if (input.sliceMode === 'grid') {
|
||||
if (input.gridX === undefined || input.gridY === undefined) {
|
||||
throw new Error('sliceMode=grid 必须同时提供 gridX 与 gridY');
|
||||
}
|
||||
} else if (input.gridX !== undefined || input.gridY !== undefined) {
|
||||
throw new Error(
|
||||
'sliceMode=connected-components 不接受 gridX/gridY:网格尺寸只能与 grid 同时提交',
|
||||
);
|
||||
}
|
||||
assertStableEditorMediaReferences(input.referenceImageSrcs, '图标素材参考图');
|
||||
assertEditorReferenceLimit(
|
||||
input.referenceImageSrcs,
|
||||
@@ -1317,9 +1328,13 @@ export async function generateEditorIconSpritesheet(
|
||||
? { referenceImageSrcs: input.referenceImageSrcs }
|
||||
: {}),
|
||||
iconDescriptions,
|
||||
...(input.sliceMode ? { sliceMode: input.sliceMode } : {}),
|
||||
...(input.gridX !== undefined ? { gridX: input.gridX } : {}),
|
||||
...(input.gridY !== undefined ? { gridY: input.gridY } : {}),
|
||||
sliceMode: input.sliceMode,
|
||||
...(input.sliceMode === 'grid' && input.gridX !== undefined
|
||||
? { gridX: input.gridX }
|
||||
: {}),
|
||||
...(input.sliceMode === 'grid' && input.gridY !== undefined
|
||||
? { gridY: input.gridY }
|
||||
: {}),
|
||||
model,
|
||||
...(input.screenColor ? { screenColor: input.screenColor } : {}),
|
||||
...(input.segModel ? { segModel: input.segModel } : {}),
|
||||
|
||||
Reference in New Issue
Block a user