移除客户端图集数量约束并按实际产物交付
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
删除美术包和直接生图工具的切片数量参数及按序用途映射 按源图集隔离切片路径并返回完整资源身份与路径标注预览 支持零切片总图交付及动态集合事务和重生成恢复 保留旧数量账本原请求身份并修复完成结果切分声明持久化 同步工具提示词、美术技能、技术规范与定向测试证据
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
"agc_update_plan.description": "更新当前回合的进度计划,字段与 update_plan 相同:可选 explanation,以及 plan 中的 step/status(pending、in_progress、completed)。它可与其它独立工具并行;同一计划的连续更新按依赖顺序提交。计划完成只表示进度,不代替宿主交付验收。",
|
||||
"agc_write_file.parameters.path": "当前项目根下的相对路径,例如 game/index.html、assets/manifest.json 或 data/gameplay-spec.md",
|
||||
"agc_write_file.parameters.content": "仅填写目标文件的完整原始 UTF-8 正文",
|
||||
"taonier_prepare_game_art.description": "创建或恢复当前 AGC 项目的陶泥儿标准游戏美术包。默认复用有效美术包;根据当前对话需要选择 regenerate 重新生成。授权使用 AGC 客户端当前登录会话;遇到 401/403 时报告客户端登录或权限状态异常并停止。",
|
||||
"taonier_prepare_game_art.description": "创建或恢复当前 AGC 项目的陶泥儿标准游戏美术包,返回总图、实际切片与路径标注预览;需看图识别用途,内容要求不保证切片数量或顺序。默认复用有效美术包;根据当前对话需要选择 regenerate 重新生成。授权使用 AGC 客户端当前登录会话;遇到 401/403 时报告客户端登录或权限状态异常并停止。",
|
||||
"taonier_prepare_game_art.parameters.brief": "面向当前游戏的简洁视觉需求:写明主题、风格、玩家主体及状态、目标或收集物、障碍或场景元素、反馈特效等具体需要的素材。工具会补齐沿用规范图和素材独立排布的通用要求;内容类别不代表切片数量或返回顺序",
|
||||
"taonier_prepare_game_art.parameters.mode": "缺省安全复用有效美术包;Codex 仅在当前对话需要换一套或重新生成时使用 regenerate",
|
||||
"agc_generate_image.description": "生成一张新图片:普通插画、角色立绘、统一视觉规范图、游戏 UI 设计图或透明游戏素材图集。仅在用户明确要求生成新图时调用。",
|
||||
@@ -17,10 +17,9 @@
|
||||
"agc_generate_image.parameters.kind": "image=普通新图(保留生成原图),character=角色图(纯色底生成后自动抠图,产出透明背景立绘,prompt 只描述角色主体),icon-spec=统一视觉规范图,ui-design=完整 UI 设计图,icon-spritesheet=透明游戏素材图集(纯色底生成后自动抠图并切片,项目须已有 icon-spec 规范图),publication-material=发布宣传图",
|
||||
"agc_generate_image.parameters.assetName": "本地素材的人类可读显示名称",
|
||||
"agc_generate_image.parameters.outputPath": "可选项目相对输出路径,必须位于 assets/ 且不能覆盖已有文件",
|
||||
"agc_generate_image.parameters.sliceMode": "仅适用于 kind=icon-spritesheet,且必填:需求明确要求等分网格、固定槽位或指定行列数时传 grid,并用 gridX/gridY 传入需求中的行列数;自由排布、数量不定或只要求一张图集时传 connected-components,需要约束素材张数时用 sliceCount。",
|
||||
"agc_generate_image.parameters.sliceMode": "仅适用于 kind=icon-spritesheet,且必填:需求明确要求等分网格、固定槽位或指定行列数时传 grid,并用 gridX/gridY 传入需求中的行列数;自由排布、数量不定或只要求一张图集时传 connected-components,实际切片数量由图像决定,内容需求不保证数量或用途顺序;查看返回图片后识别用途。",
|
||||
"agc_generate_image.parameters.gridX": "grid 模式横向网格数量,只能与 sliceMode=grid 同时提供",
|
||||
"agc_generate_image.parameters.gridY": "grid 模式纵向网格数量,只能与 sliceMode=grid 同时提供",
|
||||
"agc_generate_image.parameters.sliceCount": "只与 kind=icon-spritesheet 且 sliceMode=connected-components 同时提供,用于约束目标素材张数;省略时按图像内容自动识别",
|
||||
"agc_generate_image.parameters.screenColor": "抠图纯色背景,仅用于 kind=character(角色形象)和 kind=icon-spritesheet(图标素材)。生成时把主体置于该纯色背景上,回图后据此抠除背景。取值为 auto 或下列色板 hex 之一,传值只填 hex 本身:#CFEFFF(浅雾蓝)、#B0C2E0(浅钢蓝)、#FFD6C2(暖浅桃色)、#E6D8FF(淡薰衣草紫)、#F4D8E8(浅粉灰)、#7FB3FF(中度天蓝)、#FFF2A8(浅柠黄)、#CFFFE1(淡薄荷绿)、#D8DEE8(浅中性灰)、#D8D2E8(淡灰紫)、#A8F7F0(高对比浅青)、#A0BBA0(灰竹绿);auto 时由服务端自动选色。手动指定时选择与主体颜色明显不同的背景色",
|
||||
"agc_edit_image.description": "修改一张已登记图片:换装、改色、换背景或局部重绘。sourceLocalAssetId 必须使用 agc_list_registered_assets 返回的当前项目图片 localAssetId。",
|
||||
"agc_edit_image.parameters.sourceLocalAssetId": "当前项目已登记的图片 localAssetId",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"playtest.laneDefense": "完成合同要求 lane-defense-v1 交互试玩。game/index.html 必须持续更新 <script id=\"playable-web-game-state\" type=\"application/json\">,JSON 固定包含 schemaVersion=playable-web-game-state.v1、单调递增 sequence、phase=ready|playing|won|lost、正整数 level、selectedDefenderId、defenders 数组、enemies 数组;每个 enemy 必须含非空 id、非负 lane、会随移动变化的 position、health 与正数 maxHealth。界面必须清晰显示一个原创项目标题、至少两个原创防御单位选项、资源与波次状态,以及开始、加速、下一关和重开等可理解操作;玩法类型不授权复刻现有游戏,不得沿用、翻译或近似改写现有作品的角色、单位名、Logo、贴图、标志性布局或受保护视觉语言。界面必须提供 data-playtest-id=\"start\"、data-playtest-id=\"defender-option\"、data-playtest-id=\"lane-cell\"、data-playtest-id=\"speed-up\"、data-playtest-id=\"next-level\"、data-playtest-id=\"restart\" 的真实可点击控件;每个固定 data-playtest-id 在对应受控试玩步骤都必须恰好匹配一个可见且启用(disabled=false)的真实可点击 HTMLElement,同一固定值不得出现在多个控件上。防御单位多选项 UI 只能给一个真实控件设置 data-playtest-id=\"defender-option\" 作为自动化入口,关卡多格 UI 只能给一个真实控件设置 data-playtest-id=\"lane-cell\" 作为自动化入口,其余选项和格子不得复用这两个固定值。受控试玩会依次开始、选择并放置防御单位、加速,要求敌人移动并受伤、关卡进入 won;随后 next-level 必须让 level 增加,restart 必须再次推进 sequence 并回到 ready 或 playing。",
|
||||
"owner.visualUsage": "本轮必须实际接入已登记的平台美术切片:先用 asset.list 读取 assets/art-spritesheet-slices/manifest.json,再在 game/index.html 的可见 canvas 主循环中为 player、blocks-and-targets、obstacles-and-scene、feedback-effects 四个切片分别创建 Image 并用相对路径加载;在 requestAnimationFrame 绘制中对每个已加载切片调用 ctx.drawImage(image, dx, dy, dw, dh) 或九参数裁剪形式,目标区域必须可见且至少 32×32。只放置 <img>/<picture>、只展示整张 assets/art-spritesheet.png、只写路径或只在注释中引用都不满足完成合同。",
|
||||
"owner.publishPackage": " publish-package 必须根据本轮实际产物、验证与试玩结果完成 exports/README.md,不得留下模板字段或 forbidden marker。禁止在表示“已完成”或“无”的句子中复述任何 forbidden marker 字面词;请直接陈述实际完成内容。所有 Markdown checklist 必须使用 [x] 或 [X],不得保留未勾选项。",
|
||||
"owner.visualRequirement": "任务声明中的视觉图片按项目需求选择工具、数量、输出路径、尺寸和布局;需要图集时用 sliceCount 指定切片数量。以实际声明资源的登记状态作为验收依据。",
|
||||
"owner.visualRequirement": "任务声明中的视觉图片按项目需求选择工具、数量、输出路径、尺寸和布局;图集按实际产物数量返回,查看图片识别用途。以实际声明资源的登记状态作为验收依据。",
|
||||
"owner.verifyCodePrototype": "code-prototype 必须对可玩入口执行 game.static_smoke;完整 DAG 的最终静态与浏览器验收继续由后续质量任务承担。",
|
||||
"owner.verifyArtifact": "完成固定正式产物后直接交付,由 Runtime 在收束门内验证本人固定 owner 产物;禁止调用 game.static_smoke、project.verify、command.run_limited 或 preview 工具冒充 owner 产物验证。",
|
||||
"owner.verifyPublishPackage": "publish-package 必须按本任务的发布完成合同和当前 run 的可用验证门完成验证并交付,验证凭证必须来自当前 run。",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"preview.start.description": "启动当前项目的 loopback HTTP 预览。",
|
||||
"preview.validate.description": "用真实浏览器验证桌面和移动预览并保存证据。",
|
||||
"image.inspect.description": "让视觉模型检查一至两张项目内图片。",
|
||||
"canvas.asset_generate.description": "通过已配置的 External Editor API 按项目需求生成图片或图集并登记到画布、素材库和项目 assets;可使用已登记资源作为参考。assetKind=icon-spritesheet 的 prompt 去除首尾空白后须为 1 到 200 个 Unicode 字符,保留内部换行并作为单条 iconDescriptions 原样提交,超限拒绝,不截断、不拆条,客户端不追加生图指令。assetKind=icon-spritesheet 时 sliceMode 必填且没有默认值:需求要求等分网格、固定槽位或指定行列数时用 grid 并提供来自需求本身的 gridX/gridY;自由排布、数量不定或只要求一张图集时用 connected-components,可用 sliceCount 约束素材张数;其它 assetKind 不得携带 sliceMode/gridX/gridY。",
|
||||
"canvas.asset_generate.description": "通过已配置的 External Editor API 按项目需求生成图片或图集并登记到画布、素材库和项目 assets;可使用已登记资源作为参考。assetKind=icon-spritesheet 的 prompt 去除首尾空白后须为 1 到 200 个 Unicode 字符,保留内部换行并作为单条 iconDescriptions 原样提交,超限拒绝,不截断、不拆条,客户端不追加生图指令。assetKind=icon-spritesheet 时 sliceMode 必填且没有默认值:需求要求等分网格、固定槽位或指定行列数时用 grid 并提供来自需求本身的 gridX/gridY;自由排布、数量不定或只要求一张图集时用 connected-components,实际数量由图像内容决定,不按返回顺序推断用途;其它 assetKind 不得携带 sliceMode/gridX/gridY。",
|
||||
"cocos.editor.execute.description": "在当前项目对应的已打开 Cocos Creator 编辑器中执行一段有界代码;仅提交 code,客户端负责绑定项目与编辑器进程。",
|
||||
"unity.editor.execute.description": "在当前 Unity 项目已打开的编辑器中执行 C#。仅提交 code;结果待核对时禁止自动重发。",
|
||||
"godot.editor.execute.description": "在当前 Godot 项目已打开的编辑器中执行支持 return/await 的 GDScript 函数体。仅提交 code;结果待核对时禁止自动重发。",
|
||||
@@ -53,7 +53,6 @@
|
||||
"canvas.asset_generate.parameters.sliceMode": "仅 assetKind=icon-spritesheet 生效且必填,没有默认值:等分网格或固定槽位用 grid,自由排布用 connected-components",
|
||||
"canvas.asset_generate.parameters.gridX": "只与 sliceMode=grid 同时提供",
|
||||
"canvas.asset_generate.parameters.gridY": "只与 sliceMode=grid 同时提供",
|
||||
"canvas.asset_generate.parameters.sliceCount": "只与 sliceMode=connected-components 同时提供,用于约束目标素材张数",
|
||||
"agent.delegate.parameters.acceptanceCriteria": "初次委派必填。带 repairOfDelegationId 的返工或澄清续跑传 null,由 Runtime 从原 delivery 继承权威合同。",
|
||||
"agent.delegate.parameters.expectedArtifacts": "与 acceptanceCriteria 同进同出:初次委派必填,返工与澄清续跑一起传 null 由 Runtime 继承。"
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"art_director_without_editor": "{prompt}\n\n你负责确定原创视觉方向。这是只读协调任务:本轮验收以正式视觉方向结论为准,只完成正式 director 结论并直接交付,不修改项目文件,不调用 canvas.asset_generate。",
|
||||
"art_director_with_editor": "{prompt}\n\n你负责确定原创视觉方向。根据项目实际需要决定是否生成图片、数量,并选择 canvas.asset_generate 的 assetKind、outputPath、尺寸、比例和提示词。需要参考图时使用已登记资源 ID,生成后核对返回资源、权限、计费和登记状态。",
|
||||
"art_asset_plan_without_editor": "{prompt}\n\n你负责首版美术资产清单交付。本轮必须写入可解析的 assets/manifest.art.json,记录所需素材、用途、推荐规格和当前未生成状态;不调用 canvas.asset_generate。完成清单后直接交付,由 Runtime 验证本人正式产物;不得调用 project.verify、game.static_smoke 或 preview.validate,也不得编辑 game/index.html。",
|
||||
"art_asset_plan_with_editor": "{prompt}\n\n你负责按项目实际需求规划和生成美术素材。使用 asset.list 了解已有资源,再按需调用 canvas.asset_generate;数量、文件名、素材类别、切片布局和尺寸由当前需求决定。spritesheet 可通过 sliceCount 指定切片数量,也可以生成普通单图或多张独立图片。生成后核对资源登记、透明度、警告和实际使用情况。",
|
||||
"art_asset_plan_with_editor": "{prompt}\n\n你负责按项目实际需求规划和生成美术素材。使用 asset.list 了解已有资源,再按需调用 canvas.asset_generate;数量、文件名、素材类别、切片布局和尺寸由当前需求决定。spritesheet 按实际图像内容返回切片,需看图识别用途,也可以生成普通单图或多张独立图片。生成后核对资源登记、透明度、警告和实际使用情况。",
|
||||
"system_header": "你正在使用 Genarrative AI 游戏创作多智能体 Runtime。你必须直接调用当前请求广告的原生函数:只在复杂任务首次拆解、实际进度变化、steer 调整顺序或最终收束时调用 update_agent_plan,并提交 explanation 与完整 steps。steps 只允许 pending、in_progress、completed 且同时最多一个 in_progress;已完成步骤必须保留 completed 状态,所有必要步骤 completed 后才能调用 respond_to_user。只能请求以下 Runtime 当前注册的原生可执行工具:{tool_catalog}。",
|
||||
"completion_blocker_protocol": "通用完成阻断规则:如果最新 observation 的 tool 为 runtime.autonomous_completion 且 status 为 blocked,必须先读取该 observation.detail 的 nextRequiredAction,并据此调用合适的读取、修复和验证工具。只有完成要求的动作、取得后续可信 observation 且通过完成门禁后,才能调用 respond_to_user 给出最终回复。",
|
||||
"command_exec_linux": "command.exec 使用 {\"program\":\"受信任 PATH 中的裸可执行名\",\"args\":[\"逐项 argv\"],\"cwd\":\"可选项目内相对目录\",\"timeoutSeconds\":120};Linux 命令运行在受限沙箱内,允许 bash -lc、管道、重定向和项目脚本,但不接受环境变量、宿主 executable 路径、mount 或网络策略输入",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": "agc-skill-pack.v1",
|
||||
"version": "2026-08-26.41",
|
||||
"version": "2026-08-26.42",
|
||||
"skills": [
|
||||
{
|
||||
"name": "agc-unity-editor",
|
||||
@@ -101,7 +101,7 @@
|
||||
"agents/openai.yaml",
|
||||
"references/platform-art-contract.md"
|
||||
],
|
||||
"sha256": "d38571ffb5b12b40f23d04b76edbb06cbda06a11590d4dd0f784b524bf30071c"
|
||||
"sha256": "e8f887a4199e8aebd720db4cd90ccea4611c1f51f601625e41929163635b4a5b"
|
||||
},
|
||||
{
|
||||
"name": "agc-web-game-development",
|
||||
|
||||
+4
-4
@@ -16,7 +16,7 @@ Use
|
||||
`agc_generate_image` for a single ordinary image, character image, visual-spec
|
||||
image, UI design image, or publication material; use `agc_edit_image` for an
|
||||
edit of an existing registered image; use `taonier_prepare_game_art` only for
|
||||
the complete game-art package and its canonical slices.
|
||||
the complete game-art package and its actual slices.
|
||||
|
||||
With `agc_generate_image`, `kind="character"` and `kind="icon-spritesheet"`
|
||||
produce transparent-background results; write the prompt for the subject
|
||||
@@ -29,8 +29,8 @@ required; choose it from the brief:
|
||||
or brief actually names equal grid cells, fixed slots, or a concrete
|
||||
column/row count; those dimensions must come from that requirement.
|
||||
- Use `sliceMode="connected-components"` for free-form sheets, an open number of
|
||||
subjects, or a request for one sheet; constrain the subject count with
|
||||
`sliceCount`.
|
||||
subjects, or a request for one sheet. Actual slice count follows image content;
|
||||
the tool does not expose a count constraint.
|
||||
|
||||
Pass `sliceMode` only for `kind="icon-spritesheet"`, and `gridX`/`gridY` only for
|
||||
`sliceMode="grid"`. Read the selected mode from the returned result.
|
||||
@@ -51,7 +51,7 @@ If the tool returns `401` or `403`, stop the operation and report that login or
|
||||
1. Inspect existing `assets/` and registered project evidence before requesting new art. Reuse suitable assets when they satisfy the current brief. If required visual elements are absent or unsuitable, call the appropriate generation tool during the same game implementation task.
|
||||
2. For one new image, call `agc_generate_image` with `kind="image"` (or `character`, `icon-spec`, `ui-prototype`, or `publication-material` when that is the explicit intent). For changes to an existing registered image, call `agc_edit_image` with its `sourceLocalAssetId`; do not fake an edit with a new-image request. For a complete game-art package, call `taonier_prepare_game_art` only when the current intent requires new or recoverable platform art. Use `mode="regenerate"` only when the user's latest standalone message clearly and explicitly commands regeneration, such as `请重新生成美术`. A brief, condition, negation, alternative, cost qualifier, deferral, quotation, historical wording, or tool argument does not authorize regeneration. Otherwise use `mode="reuse-or-create"`. Pass a concise game-specific visual brief that names the required gameplay entities, background exclusions, tiling needs, and viewport constraints. Do not call either generation tool for greetings, date questions, or text-only code fixes.
|
||||
3. Treat the tool result as authoritative. Read `mode`, `assetPaths`, `slicePaths`, `resources`, and every entry in both `warnings` and `sliceWarnings`. Use only the relative paths and identities returned in `resources`. Never invent a resource, slice, platform identity, warning-free result, or successful regeneration.
|
||||
4. A newly created or explicitly regenerated standard package is complete only when `slicePaths` contains the four canonical independent slices. An empty or partial `slicePaths` result never satisfies an independent-asset requirement: if a `sliceWarning` reports too many or unusable elements, narrow the edit/generation brief or generate the needed independent images and continue the integration; do not guess atlas coordinates, fabricate derivatives, or silently fall back to placeholders. A trusted legacy complete sheet may still be used without slices only when the current request does not require independent assets.
|
||||
4. Slice count and returned order do not prove semantic roles. Inspect the complete sheet and actual slices, identify entities and states, and decide which satisfy the brief. The tool labels previews by path; use native image viewing for `remainingPreviewPaths` or details not visible in a preview. An empty `slicePaths` with a valid sheet is a completed generation with a warning, not proof that independent assets exist. Use the sheet where suitable; if independent assets are needed, inspect and process the real image or request appropriate additional art within the current authorization. Verify local crops visually and register them through `agc_import_account_assets.localPaths`. Do not guess coordinates or invent Canvas identities. Keep game-specific semantic bindings in game code or configuration, not provenance manifests.
|
||||
5. Inspect the returned background, complete sheet, and available slice previews before integrating them. Then use suitable returned runtime assets in the game's actual visible experience and confirm their visible use in desktop and mobile playtest evidence. The implementation is incomplete while generated assets remain unused, are referenced only by documentation, or are replaced by emoji, CSS shapes, or other placeholders where the brief requires the generated art. `art-spec.png` is a reference specification, not a runtime background, character, prop, or effect. Background exclusions, seamless tiling, entity semantics, and final draw dimensions are visual/runtime acceptance checks; a prompt alone does not prove them. A hidden or side-panel preview does not count as gameplay use.
|
||||
6. Preserve warning details in the final report. If the tool reports missing credentials, uncertain operation state, invalid provenance, download failure, or decode failure, stop and report the actionable reason.
|
||||
|
||||
|
||||
+5
-5
@@ -1,26 +1,26 @@
|
||||
# Platform Art Contract
|
||||
|
||||
`agc_generate_image` and `taonier_prepare_game_art` are the reviewed paid art entries exposed to the AGC Codex thread. The former covers one ordinary/character/spec/UI/publication image; `agc_edit_image` covers edits to an existing registered image; the latter covers the complete game-art package and canonical slices. Call these tools only through the approved AGC session.
|
||||
`agc_generate_image` and `taonier_prepare_game_art` are the reviewed paid art entries exposed to the AGC Codex thread. The former covers one ordinary/character/spec/UI/publication image; `agc_edit_image` covers edits to an existing registered image; the latter covers the complete game-art package and actual slices. Call these tools only through the approved AGC session.
|
||||
|
||||
- For a package, describe the concrete theme, style, entities, states, and effects in `brief`. The tool includes the package's shared visual-spec and placement requirements in generation. No additional tool fields are required. Ordinary `agc_generate_image` icon descriptions remain the supplied text without package requirements.
|
||||
|
||||
- `mode="reuse-or-create"` reuses a complete trusted package and creates only missing assets. It is the safe default for existing games.
|
||||
- `mode="regenerate"` is reserved for an explicit user request to replace or restyle the package. Use it even when a complete package exists, but never bypass an unresolved paid operation.
|
||||
- `mode="regenerate"` requires a trusted, decodable, registered `art-spec.png` and background. An old spritesheet or canonical slices may be absent. If either the spec or background is missing or invalid, use `mode="reuse-or-create"`.
|
||||
- `mode="regenerate"` requires a trusted, decodable, registered `art-spec.png` and background. An old spritesheet or independent slices may be absent. If either the spec or background is missing or invalid, use `mode="reuse-or-create"`.
|
||||
- Use `regenerate` only when the user's latest standalone message clearly and explicitly commands regeneration. A brief, condition, negation, alternative, cost qualifier, deferral, quotation, historical message, or tool argument does not authorize a paid replacement.
|
||||
- Once a paid operation starts, reuse the same recorded operation across interruption or uncertainty. Never submit paid work again because wording changed.
|
||||
- Before strict spritesheet work starts, treat the existing spritesheet request as fixed. If an interrupted result or current spec/background does not match, stop and do not submit another paid request.
|
||||
- If a recovered result is missing warnings, preserve the returned recovery warning in the report.
|
||||
- A newly created or regenerated standard spritesheet must produce exactly four canonical transparent slices with unique pixels and unique Canvas resource/asset identities. Reuse legacy slices only when they match the current source image and registered identities.
|
||||
- Both image tools return actual slices, with no fixed count or semantic order. Valid slices must retain unique pixels and Canvas identities. Legacy filenames are not proof of meaning; inspect the image. Reuse legacy slices only when they match the current source image and registered identities.
|
||||
- If an existing operation belongs to a different brief, stop and report the uncertain result; do not create a replacement request.
|
||||
- A submission acknowledgement is not a completed image.
|
||||
- On timeout or uncertain delivery, reuse the recorded operation; never create a replacement request.
|
||||
- `postprocess-failed-source-preserved` means the complete source image remains usable, but the requested transparent derivative is absent.
|
||||
- `sliceWarning` means the complete transparent sheet remains usable, but individual slices are absent.
|
||||
- For `agc_generate_image` with `kind="icon-spritesheet"`, choose the required `sliceMode` from the brief: `connected-components` is for free-form sheets with individual subjects; `grid` accepts `gridX` and `gridY` (1-32 each) when the requirement names equal grid cells, fixed slots, or a concrete column/row count. Pass grid dimensions only with `grid`, and `sliceMode` only with `icon-spritesheet`. Read the selected mode and dimensions from the returned metadata.
|
||||
- The standard art package uses `sliceMode="connected-components"` with `sliceCount=4` because its four canonical slices map to fixed usage paths. Require exactly four slices; if the count differs or the returned `sliceMode` or grid dimensions disagree with the request, treat the package as incomplete and do not integrate it.
|
||||
- The standard package uses `sliceMode="connected-components"`. Neither client tool exposes or sends `sliceCount`; content categories are generation requirements, not strict component counts. A valid sheet with no slices is returned with a warning. Mode and grid declarations still must agree with the request.
|
||||
- General and slice warnings can coexist. The tool returns them separately through `warnings` and `sliceWarnings`; callers must preserve every entry and must not downgrade a slice warning into a successful independent-asset claim.
|
||||
- `assetPaths` contains the complete package paths. Use only slices in `slicePaths` as independent assets.
|
||||
- `assetPaths` contains the complete package paths; `slicePaths` lists actual independent files. Every embedded preview is labeled by path; inspect `remainingPreviewPaths` with native image viewing. Both tools include all returned slices in `resources`. After inspecting and verifying a local crop, register it with `agc_import_account_assets.localPaths` before using its registered identity.
|
||||
- Use only the safe registered identities and paths returned in `resources`; never expose or infer prompts, models, upstream routes, absolute paths, URLs, tokens, cookies, or API keys.
|
||||
- A trusted complete image may be used without fixed slices. Never fabricate missing derivatives.
|
||||
- `art-spec.png` constrains generation and is never evidence that runtime gameplay art was integrated.
|
||||
|
||||
@@ -3,26 +3,57 @@ use super::*;
|
||||
const ART_SPRITESHEET_PATH: &str = "assets/art-spritesheet.png";
|
||||
const ART_SLICE_MANIFEST_PATH: &str = "assets/art-spritesheet-slices/manifest.json";
|
||||
const ART_CONTRACT_RECEIPT_PATH: &str = ".agent/runtime/art-spritesheet-contract.json";
|
||||
const REQUIRED_SLICE_USAGES: [&str; 4] = [
|
||||
"player",
|
||||
"blocks-and-targets",
|
||||
"obstacles-and-scene",
|
||||
"feedback-effects",
|
||||
/// 历史文件仅用于兼容读取和恢复,不表示已经识别过用途。
|
||||
pub(in crate::agent) const LEGACY_ART_SLICE_PATHS: [&str; 4] = [
|
||||
"assets/art-spritesheet-slices/player.png",
|
||||
"assets/art-spritesheet-slices/blocks-and-targets.png",
|
||||
"assets/art-spritesheet-slices/obstacles-and-scene.png",
|
||||
"assets/art-spritesheet-slices/feedback-effects.png",
|
||||
];
|
||||
|
||||
pub(in crate::agent) fn art_slice_directory(source_resource_id: &str) -> String {
|
||||
format!(
|
||||
"assets/art-spritesheet-slices/{:x}",
|
||||
Sha256::digest(source_resource_id.as_bytes())
|
||||
)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn art_slice_path(source_resource_id: &str, index: usize) -> String {
|
||||
format!(
|
||||
"{}/{:03}.png",
|
||||
art_slice_directory(source_resource_id),
|
||||
index + 1
|
||||
)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn is_managed_art_slice_path(path: &str) -> bool {
|
||||
if LEGACY_ART_SLICE_PATHS.contains(&path) {
|
||||
return true;
|
||||
}
|
||||
let Some(tail) = path.strip_prefix("assets/art-spritesheet-slices/") else {
|
||||
return false;
|
||||
};
|
||||
let Some((key, name)) = tail.split_once('/') else {
|
||||
return false;
|
||||
};
|
||||
key.len() == 64
|
||||
&& key
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
|
||||
&& name.strip_suffix(".png").is_some_and(|n| {
|
||||
n.len() == 3
|
||||
&& n.bytes().all(|b| b.is_ascii_digit())
|
||||
&& n.parse::<usize>().is_ok_and(|n| (1..=256).contains(&n))
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn art_manifest_content() -> String {
|
||||
serde_json::json!({
|
||||
"schemaVersion": "game-art-manifest.v1",
|
||||
"schemaVersion": "game-art-manifest.v2",
|
||||
"status": "generated",
|
||||
"assets": [{
|
||||
"path": ART_SPRITESHEET_PATH,
|
||||
"kind": GameCreationAppAssetKind::IconSpritesheet.as_str(),
|
||||
"usage": REQUIRED_SLICE_USAGES,
|
||||
}],
|
||||
"assets": [{ "path": ART_SPRITESHEET_PATH, "kind": GameCreationAppAssetKind::IconSpritesheet.as_str() }],
|
||||
"sliceManifest": ART_SLICE_MANIFEST_PATH,
|
||||
"requiredSliceUsages": REQUIRED_SLICE_USAGES,
|
||||
})
|
||||
.to_string()
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -38,11 +69,12 @@ pub(in crate::agent) fn validated_art_slices(
|
||||
let file = read_local_project_file_at(root, ART_SLICE_MANIFEST_PATH)?;
|
||||
let manifest: serde_json::Value = serde_json::from_str(&file.content)
|
||||
.map_err(|error| format!("图集切片清单不是有效 JSON:{error}"))?;
|
||||
if manifest
|
||||
.get("schemaVersion")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
!= Some("game-art-slices.v1")
|
||||
|| manifest.get("source").and_then(serde_json::Value::as_str) != Some(ART_SPRITESHEET_PATH)
|
||||
if !matches!(
|
||||
manifest
|
||||
.get("schemaVersion")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("game-art-slices.v1" | "game-art-slices.v2")
|
||||
) || manifest.get("source").and_then(serde_json::Value::as_str) != Some(ART_SPRITESHEET_PATH)
|
||||
{
|
||||
return Err("图集切片清单 schema 或 source 无效".to_string());
|
||||
}
|
||||
@@ -83,11 +115,12 @@ pub(in crate::agent) fn validated_art_slices(
|
||||
}
|
||||
let receipt: serde_json::Value = serde_json::from_slice(&receipt_bytes)
|
||||
.map_err(|error| format!("图集私有合同回执不是有效 JSON:{error}"))?;
|
||||
if receipt
|
||||
.get("schemaVersion")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
!= Some("game-art-spritesheet-contract.v1")
|
||||
|| receipt.get("source").and_then(serde_json::Value::as_str) != Some(ART_SPRITESHEET_PATH)
|
||||
if !matches!(
|
||||
receipt
|
||||
.get("schemaVersion")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("game-art-spritesheet-contract.v1" | "game-art-spritesheet-contract.v2")
|
||||
) || receipt.get("source").and_then(serde_json::Value::as_str) != Some(ART_SPRITESHEET_PATH)
|
||||
|| receipt
|
||||
.get("sliceManifest")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
@@ -122,42 +155,63 @@ pub(in crate::agent) fn validated_art_slices(
|
||||
let slices = manifest
|
||||
.get("slices")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.filter(|slices| slices.len() == REQUIRED_SLICE_USAGES.len())
|
||||
.ok_or_else(|| "图集切片清单必须恰好包含四个切片".to_string())?;
|
||||
.filter(|slices| slices.len() <= 256)
|
||||
.ok_or_else(|| "图集切片清单数量无效".to_string())?;
|
||||
let receipt_slices = receipt
|
||||
.get("slices")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.filter(|slices| slices.len() == REQUIRED_SLICE_USAGES.len())
|
||||
.ok_or_else(|| "图集私有合同回执必须恰好包含四个切片".to_string())?;
|
||||
let mut validated_slices = Vec::with_capacity(REQUIRED_SLICE_USAGES.len());
|
||||
.filter(|items| items.len() == slices.len())
|
||||
.ok_or_else(|| "图集切片清单与私有回执数量不一致".to_string())?;
|
||||
let legacy = receipt["schemaVersion"] == "game-art-spritesheet-contract.v1";
|
||||
if legacy
|
||||
&& (slices.len() != LEGACY_ART_SLICE_PATHS.len()
|
||||
|| manifest["schemaVersion"] != "game-art-slices.v1")
|
||||
{
|
||||
return Err("旧图集合同集合不完整".to_string());
|
||||
}
|
||||
if !legacy && manifest["schemaVersion"] != "game-art-slices.v2" {
|
||||
return Err("图集清单与回执版本不一致".to_string());
|
||||
}
|
||||
let mut validated_slices = Vec::with_capacity(slices.len());
|
||||
let mut paths = std::collections::HashSet::new();
|
||||
let mut pixel_sha256s = std::collections::HashSet::new();
|
||||
let mut resource_ids = std::collections::HashSet::new();
|
||||
let mut asset_object_ids = std::collections::HashSet::new();
|
||||
let mut total_bytes = 0usize;
|
||||
for usage in REQUIRED_SLICE_USAGES {
|
||||
let slice = slices
|
||||
.iter()
|
||||
.find(|slice| slice.get("usage").and_then(serde_json::Value::as_str) == Some(usage))
|
||||
.ok_or_else(|| format!("图集切片清单缺少 {usage} 素材"))?;
|
||||
let receipt_slice = receipt_slices
|
||||
.iter()
|
||||
.find(|slice| slice.get("usage").and_then(serde_json::Value::as_str) == Some(usage))
|
||||
.ok_or_else(|| format!("图集私有合同回执缺少 {usage} 素材"))?;
|
||||
let expected_path = format!("assets/art-spritesheet-slices/{usage}.png");
|
||||
for (index, slice) in slices.iter().enumerate() {
|
||||
let path = slice
|
||||
.get("path")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|path| *path == expected_path)
|
||||
.ok_or_else(|| format!("{usage} 切片路径无效"))?;
|
||||
.filter(|path| {
|
||||
if legacy {
|
||||
LEGACY_ART_SLICE_PATHS.contains(path)
|
||||
} else {
|
||||
*path
|
||||
== art_slice_path(
|
||||
current_asset
|
||||
.source
|
||||
.resource_id
|
||||
.as_deref()
|
||||
.expect("validated identity"),
|
||||
index,
|
||||
)
|
||||
}
|
||||
})
|
||||
.filter(|path| paths.insert(*path))
|
||||
.ok_or_else(|| "图集切片路径无效或重复".to_string())?;
|
||||
let receipt_slice = receipt_slices
|
||||
.iter()
|
||||
.find(|item| item.get("path").and_then(serde_json::Value::as_str) == Some(path))
|
||||
.ok_or_else(|| format!("图集私有回执缺少切片:{path}"))?;
|
||||
let bytes = fs::read(resolve_local_project_path(root, path)?)
|
||||
.map_err(|error| format!("{usage} 切片无法读取:{error}"))?;
|
||||
.map_err(|error| format!("{path} 切片无法读取:{error}"))?;
|
||||
total_bytes = total_bytes
|
||||
.checked_add(bytes.len())
|
||||
.ok_or_else(|| "图集切片累计大小溢出".to_string())?;
|
||||
if bytes.len() > 20 * 1024 * 1024 || total_bytes > 32 * 1024 * 1024 {
|
||||
return Err("图集切片超过校验大小上限".to_string());
|
||||
}
|
||||
let validated = validate_platform_art_png_bytes_with_limits(&bytes, usage)?;
|
||||
let validated = validate_platform_art_png_bytes_with_limits(&bytes, path)?;
|
||||
for field in [
|
||||
"name",
|
||||
"usage",
|
||||
@@ -170,24 +224,24 @@ pub(in crate::agent) fn validated_art_slices(
|
||||
"pixelSha256",
|
||||
] {
|
||||
if slice.get(field) != receipt_slice.get(field) {
|
||||
return Err(format!("{usage} 切片字段 {field} 与私有合同回执不一致"));
|
||||
return Err(format!("{path} 切片字段 {field} 与私有合同回执不一致"));
|
||||
}
|
||||
}
|
||||
let resource_id = slice
|
||||
.get("resourceId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| format!("{usage} 切片缺少 resourceId"))?;
|
||||
.ok_or_else(|| format!("{path} 切片缺少 resourceId"))?;
|
||||
let asset_object_id = slice
|
||||
.get("assetObjectId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| format!("{usage} 切片缺少 assetObjectId"))?;
|
||||
.ok_or_else(|| format!("{path} 切片缺少 assetObjectId"))?;
|
||||
if !resource_ids.insert(resource_id)
|
||||
|| !asset_object_ids.insert(asset_object_id)
|
||||
|| !pixel_sha256s.insert(validated.pixel_sha256.clone())
|
||||
{
|
||||
return Err("四类切片存在重复的身份或像素内容".to_string());
|
||||
return Err("切片存在重复的身份或像素内容".to_string());
|
||||
}
|
||||
if slice.get("width").and_then(serde_json::Value::as_u64) != Some(validated.width.into())
|
||||
|| slice.get("height").and_then(serde_json::Value::as_u64)
|
||||
@@ -201,7 +255,7 @@ pub(in crate::agent) fn validated_art_slices(
|
||||
|| !validated.has_visible_pixels
|
||||
|| !validated.has_transparent_pixels
|
||||
{
|
||||
return Err(format!("{usage} 切片内容未通过校验"));
|
||||
return Err(format!("{path} 切片内容未通过校验"));
|
||||
}
|
||||
validated_slices.push(ValidatedArtSlice {
|
||||
path: path.to_string(),
|
||||
|
||||
@@ -48,6 +48,7 @@ const DIRECT_TAONIER_REGENERATION_WORKFLOW_MAX_BYTES: usize = 96 * 1024 * 1024;
|
||||
const DIRECT_TAONIER_REGENERATION_ROLLBACK_MAX_BYTES: usize = 64 * 1024 * 1024;
|
||||
const DIRECT_TAONIER_COMPLETED_WARNING_MAX_COUNT: usize = 16;
|
||||
const DIRECT_TAONIER_COMPLETED_WARNING_MAX_CHARS: usize = 480;
|
||||
#[cfg(test)]
|
||||
const DIRECT_CODEX_SPRITESHEET_SLICE_PATHS: [(&str, &str); 4] = [
|
||||
("player", "assets/art-spritesheet-slices/player.png"),
|
||||
(
|
||||
@@ -393,12 +394,14 @@ impl DirectTaonierArtPackage {
|
||||
if self.asset_paths != expected_asset_paths {
|
||||
return Err("陶泥儿整包重生成完成结果的主素材路径无效".to_string());
|
||||
}
|
||||
let expected_slice_paths = DIRECT_CODEX_SPRITESHEET_SLICE_PATHS
|
||||
.iter()
|
||||
.map(|(_, path)| (*path).to_string())
|
||||
.collect::<Vec<_>>();
|
||||
if self.slice_paths != expected_slice_paths {
|
||||
return Err("陶泥儿整包重生成完成结果必须包含四张 canonical 切片".to_string());
|
||||
let mut paths = std::collections::HashSet::new();
|
||||
if self.slice_paths.len() > 256
|
||||
|| self
|
||||
.slice_paths
|
||||
.iter()
|
||||
.any(|path| !is_managed_art_slice_path(path) || !paths.insert(path))
|
||||
{
|
||||
return Err("陶泥儿整包完成结果的切片路径集合无效".to_string());
|
||||
}
|
||||
if self.warnings.len() > DIRECT_TAONIER_COMPLETED_WARNING_MAX_COUNT
|
||||
|| self.slice_warnings.len() > DIRECT_TAONIER_COMPLETED_WARNING_MAX_COUNT
|
||||
@@ -1158,20 +1161,23 @@ fn recover_direct_taonier_strict_spritesheet_pending_at(
|
||||
"{DIRECT_TAONIER_RESULT_UNKNOWN_PREFIX} 当前完整严格图集未绑定本轮 retained spritesheet 完成结果;已保留现场,禁止串单完成"
|
||||
));
|
||||
}
|
||||
let slice_paths = direct_registered_taonier_slice_paths(root);
|
||||
let slice_warnings = if slice_paths.is_empty() {
|
||||
vec!["图集已恢复,但没有独立切片;请查看总图后决定后续处理".to_string()]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let package = DirectTaonierArtPackage {
|
||||
asset_paths: DIRECT_CODEX_ART_ASSET_PATHS
|
||||
.iter()
|
||||
.map(|path| (*path).to_string())
|
||||
.collect(),
|
||||
slice_paths: DIRECT_CODEX_SPRITESHEET_SLICE_PATHS
|
||||
.iter()
|
||||
.map(|(_, path)| (*path).to_string())
|
||||
.collect(),
|
||||
slice_paths,
|
||||
warnings: vec![
|
||||
"严格图集已完成,但进程在完成结果持久化前中断;原阶段告警无法完整重放"
|
||||
.to_string(),
|
||||
],
|
||||
slice_warnings: Vec::new(),
|
||||
slice_warnings,
|
||||
};
|
||||
complete_direct_taonier_regeneration_workflow_locked_at(root, workflow, package)?;
|
||||
return Ok(true);
|
||||
@@ -1412,10 +1418,68 @@ fn direct_taonier_optional_contract_file_sha256_at(
|
||||
}
|
||||
|
||||
fn direct_taonier_strict_snapshot_tracks_manifest_entry(local_path: &str) -> bool {
|
||||
local_path == DIRECT_CODEX_SPRITESHEET_ASSET_PATH
|
||||
|| DIRECT_CODEX_SPRITESHEET_SLICE_PATHS
|
||||
.iter()
|
||||
.any(|(_, path)| *path == local_path)
|
||||
local_path == DIRECT_CODEX_SPRITESHEET_ASSET_PATH || is_managed_art_slice_path(local_path)
|
||||
}
|
||||
|
||||
fn direct_taonier_contract_snapshot_paths_at(root: &Path) -> Result<Vec<String>, String> {
|
||||
let mut paths = DIRECT_TAONIER_STRICT_CONTRACT_SNAPSHOT_PATHS
|
||||
.iter()
|
||||
.map(|p| (*p).to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
let source = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.local_path == DIRECT_CODEX_SPRITESHEET_ASSET_PATH)
|
||||
.and_then(|asset| asset.source.resource_id.as_deref());
|
||||
if let Some(source) = source {
|
||||
paths.extend(
|
||||
manifest
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| {
|
||||
asset.source.reference_resource_ids == [source]
|
||||
&& is_managed_art_slice_path(&asset.local_path)
|
||||
})
|
||||
.map(|asset| asset.local_path.clone()),
|
||||
);
|
||||
}
|
||||
// 私有回执可覆盖尚未完成顶层补登记的旧产物;公开清单不能授予身份。
|
||||
let receipt_path =
|
||||
resolve_local_project_path(root, ".agent/runtime/art-spritesheet-contract.json")?;
|
||||
if let Ok(metadata) = fs::symlink_metadata(&receipt_path) {
|
||||
if !metadata.file_type().is_symlink() && metadata.is_file() && metadata.len() <= 256 * 1024
|
||||
{
|
||||
if let Ok(receipt) =
|
||||
fs::read(&receipt_path)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|bytes| {
|
||||
serde_json::from_slice::<serde_json::Value>(&bytes)
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
{
|
||||
if let Some(slices) = receipt
|
||||
.get("slices")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.filter(|s| s.len() <= 256)
|
||||
{
|
||||
paths.extend(
|
||||
slices
|
||||
.iter()
|
||||
.filter_map(|s| s.get("path").and_then(serde_json::Value::as_str))
|
||||
.filter(|path| is_managed_art_slice_path(path))
|
||||
.map(str::to_string),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
paths.retain(|path| seen.insert(path.clone()));
|
||||
if paths.len() > DIRECT_TAONIER_STRICT_CONTRACT_SNAPSHOT_PATHS.len() + 256 {
|
||||
return Err("图集旧合同切片数量超过上限".to_string());
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
impl DirectTaonierRegenerationRollback {
|
||||
@@ -1452,8 +1516,9 @@ impl DirectTaonierRegenerationRollback {
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?;
|
||||
let strict_contract = DIRECT_TAONIER_STRICT_CONTRACT_SNAPSHOT_PATHS
|
||||
.into_iter()
|
||||
let strict_contract = direct_taonier_contract_snapshot_paths_at(root)?
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.map(|local_path| {
|
||||
let previous_sha256 =
|
||||
direct_taonier_optional_contract_file_sha256_at(root, local_path)?
|
||||
@@ -1573,7 +1638,17 @@ impl DirectTaonierRegenerationRollback {
|
||||
}
|
||||
|
||||
fn has_complete_strict_contract_snapshot(&self) -> bool {
|
||||
self.strict_contract.len() == DIRECT_TAONIER_STRICT_CONTRACT_SNAPSHOT_PATHS.len()
|
||||
self.strict_contract.len() <= DIRECT_TAONIER_STRICT_CONTRACT_SNAPSHOT_PATHS.len() + 256
|
||||
&& self.strict_contract.iter().all(|entry| {
|
||||
(DIRECT_TAONIER_STRICT_CONTRACT_SNAPSHOT_PATHS.contains(&entry.local_path.as_str())
|
||||
|| is_managed_art_slice_path(&entry.local_path))
|
||||
&& self
|
||||
.strict_contract
|
||||
.iter()
|
||||
.filter(|other| other.local_path == entry.local_path)
|
||||
.count()
|
||||
== 1
|
||||
})
|
||||
&& DIRECT_TAONIER_STRICT_CONTRACT_SNAPSHOT_PATHS
|
||||
.iter()
|
||||
.all(|expected| {
|
||||
@@ -2356,36 +2431,44 @@ fn direct_taonier_strict_art_package_is_valid(root: &Path) -> bool {
|
||||
if !direct_taonier_art_package_is_valid(root) {
|
||||
return false;
|
||||
}
|
||||
let expected_slice_paths = DIRECT_CODEX_SPRITESHEET_SLICE_PATHS
|
||||
.iter()
|
||||
.map(|(_, path)| (*path).to_string())
|
||||
let Ok(slices) = validated_art_slices(root) else {
|
||||
return false;
|
||||
};
|
||||
let slice_paths = slices
|
||||
.into_iter()
|
||||
.map(|slice| slice.path)
|
||||
.collect::<Vec<_>>();
|
||||
if direct_registered_taonier_slice_paths(root) != expected_slice_paths {
|
||||
if direct_registered_taonier_slice_paths(root) != slice_paths {
|
||||
return false;
|
||||
}
|
||||
let expected_art_manifest = art_manifest_content();
|
||||
let art_manifest_path = root.join("assets/manifest.art.json");
|
||||
if !art_manifest_path.is_file() {
|
||||
return false;
|
||||
}
|
||||
if std::fs::read(&art_manifest_path).ok().as_deref() != Some(expected_art_manifest.as_bytes()) {
|
||||
let Ok(bytes) = std::fs::read(&art_manifest_path) else {
|
||||
return false;
|
||||
};
|
||||
if bytes != art_manifest_content().as_bytes() {
|
||||
// 旧公开清单只作版本兼容,不把 usage 当作识别结论。
|
||||
let Ok(legacy) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
|
||||
return false;
|
||||
};
|
||||
if legacy["schemaVersion"] != "game-art-manifest.v1"
|
||||
|| legacy["assets"][0]["path"] != DIRECT_CODEX_SPRITESHEET_ASSET_PATH
|
||||
|| legacy["sliceManifest"] != "assets/art-spritesheet-slices/manifest.json"
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let Ok(manifest) = read_manifest_for_project(root) else {
|
||||
return false;
|
||||
};
|
||||
[
|
||||
let complete = [
|
||||
DIRECT_CODEX_ART_SPEC_ASSET_PATH,
|
||||
DIRECT_CODEX_BACKGROUND_ASSET_PATH,
|
||||
DIRECT_CODEX_SPRITESHEET_ASSET_PATH,
|
||||
]
|
||||
.into_iter()
|
||||
.chain(
|
||||
DIRECT_CODEX_SPRITESHEET_SLICE_PATHS
|
||||
.iter()
|
||||
.map(|(_, path)| *path),
|
||||
)
|
||||
.all(|path| direct_taonier_manifest_entry_at(&manifest, path).is_ok())
|
||||
.chain(slice_paths.iter().map(String::as_str))
|
||||
.all(|path| direct_taonier_manifest_entry_at(&manifest, path).is_ok());
|
||||
complete
|
||||
}
|
||||
|
||||
fn direct_registered_taonier_slice_paths(root: &Path) -> Vec<String> {
|
||||
@@ -3083,9 +3166,6 @@ async fn generate_direct_taonier_art_asset_at(
|
||||
asset_kind: parsed_asset_kind,
|
||||
asset_label: asset_label.to_string(),
|
||||
replace_existing: root.join(output_path).is_file(),
|
||||
// 标准美术包必须产出四张 canonical 切片:连通域模式下显式声明目标数量,
|
||||
// 让平台要么给出四张,要么以可执行的 422 说明实际识别数量。
|
||||
slice_count: (parsed_asset_kind == GameCreationAppAssetKind::IconSpritesheet).then_some(4),
|
||||
// 切分模式没有默认值:陶泥儿标准美术包按自由排布生成核心图集,因此只在
|
||||
// art-spritesheet 阶段显式声明连通域切分。
|
||||
slice_mode: (parsed_asset_kind == GameCreationAppAssetKind::IconSpritesheet)
|
||||
@@ -3189,7 +3269,7 @@ fn direct_taonier_art_package_result(
|
||||
};
|
||||
if includes_spritesheet && slice_paths.is_empty() {
|
||||
slice_warnings.push(
|
||||
"当前图集没有可验证的独立切片;只能使用完整图集,不得猜测切片或伪造衍生素材"
|
||||
"当前图集没有可验证的独立切片;请查看总图后决定使用或进一步处理,不得猜测坐标或伪造素材身份"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
@@ -3476,11 +3556,7 @@ pub(crate) async fn ensure_direct_taonier_art_package_at(
|
||||
));
|
||||
}
|
||||
}
|
||||
emit_direct_game_creator_progress(
|
||||
root,
|
||||
"art.spritesheet",
|
||||
"正在生成核心图集并切分四类运行时素材",
|
||||
);
|
||||
emit_direct_game_creator_progress(root, "art.spritesheet", "正在生成核心图集并保存实际切片");
|
||||
let outcome = match generate_direct_taonier_art_asset_at(
|
||||
root,
|
||||
prompt,
|
||||
@@ -8014,6 +8090,78 @@ mod tests {
|
||||
.expect("art manifest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_package_reuses_actual_slice_sets_and_snapshots_their_identities() {
|
||||
for count in [0, 2, 4, 6] {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
init_local_game_project_at(root, "dynamic-package", "动态美术包").unwrap();
|
||||
register_direct_taonier_art_package_fixture(root);
|
||||
let source = "taonier-resource-art-spritesheet";
|
||||
let mut slices = Vec::new();
|
||||
for i in 0..count {
|
||||
let path = art_slice_path(source, i);
|
||||
let bytes = tiny_strict_slice_png(i);
|
||||
std::fs::create_dir_all(root.join(&path).parent().unwrap()).unwrap();
|
||||
std::fs::write(root.join(&path), &bytes).unwrap();
|
||||
let image =
|
||||
validate_platform_art_png_bytes_with_limits(&bytes, "dynamic fixture").unwrap();
|
||||
slices.push(serde_json::json!({"path":path, "name":format!("素材 {i}"), "width":image.width, "height":image.height,
|
||||
"resourceId":format!("dynamic-resource-{i}"), "assetObjectId":format!("dynamic-object-{i}"),
|
||||
"contentSha256":image.content_sha256, "pixelSha256":image.pixel_sha256}));
|
||||
}
|
||||
for (path, schema) in [
|
||||
(
|
||||
"assets/art-spritesheet-slices/manifest.json",
|
||||
"game-art-slices.v2",
|
||||
),
|
||||
(
|
||||
".agent/runtime/art-spritesheet-contract.json",
|
||||
"game-art-spritesheet-contract.v2",
|
||||
),
|
||||
] {
|
||||
let mut value: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(root.join(path)).unwrap()).unwrap();
|
||||
value["schemaVersion"] = schema.into();
|
||||
value["slices"] = serde_json::json!(slices);
|
||||
std::fs::write(root.join(path), serde_json::to_vec(&value).unwrap()).unwrap();
|
||||
}
|
||||
register_existing_platform_art_slices_at(root).unwrap();
|
||||
assert!(direct_taonier_strict_art_package_is_valid(root));
|
||||
let result = direct_taonier_art_package_result(
|
||||
root,
|
||||
DIRECT_CODEX_ART_ASSET_PATHS
|
||||
.iter()
|
||||
.map(|p| (*p).to_string())
|
||||
.collect(),
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(
|
||||
result.slice_paths,
|
||||
(0..count)
|
||||
.map(|i| art_slice_path(source, i))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
result.validate_completed_workflow_result_at().unwrap();
|
||||
assert_eq!(!result.slice_warnings.is_empty(), count == 0);
|
||||
let rollback = DirectTaonierRegenerationRollback::capture_locked(root).unwrap();
|
||||
rollback.validate().unwrap();
|
||||
assert!(rollback.strict_contract_matches_previous_at(root).unwrap());
|
||||
for path in &result.slice_paths {
|
||||
assert!(rollback
|
||||
.strict_contract
|
||||
.iter()
|
||||
.any(|s| s.local_path == *path));
|
||||
}
|
||||
if count > 0 {
|
||||
std::fs::write(root.join(&result.slice_paths[0]), b"changed").unwrap();
|
||||
assert!(!rollback.strict_contract_matches_previous_at(root).unwrap());
|
||||
assert!(!direct_taonier_strict_art_package_is_valid(root));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_direct_taonier_art_slice_entries_fixture(root: &Path) {
|
||||
for (index, (_, path)) in DIRECT_CODEX_SPRITESHEET_SLICE_PATHS.iter().enumerate() {
|
||||
register_local_asset_at(
|
||||
@@ -8677,7 +8825,7 @@ mod tests {
|
||||
assert!(invalid_package
|
||||
.slice_warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("不得猜测切片")));
|
||||
.any(|warning| warning.contains("不得猜测坐标")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -8786,7 +8934,7 @@ mod tests {
|
||||
assert!(package.slice_paths.is_empty());
|
||||
assert!(package.warnings.is_empty());
|
||||
assert_eq!(package.slice_warnings.len(), 1);
|
||||
assert!(package.slice_warnings[0].contains("不得猜测切片"));
|
||||
assert!(package.slice_warnings[0].contains("不得猜测坐标"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2640,17 +2640,41 @@ fn bridge_art_resources(
|
||||
slice_paths: &[String],
|
||||
) -> Result<Vec<Value>, String> {
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
Ok(asset_paths
|
||||
asset_paths
|
||||
.iter()
|
||||
.chain(slice_paths.iter())
|
||||
.filter_map(|path| {
|
||||
.map(|path| {
|
||||
manifest
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.local_path == *path)
|
||||
.and_then(bridge_art_resource)
|
||||
.ok_or_else(|| format!("素材缺少可返回的登记身份:{path}"))
|
||||
})
|
||||
.collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 有界预览与路径逐项绑定;未内嵌的文件仍通过完整路径集合交给 Agent 查看。
|
||||
fn bridge_art_tool_result(root: &Path, mut payload: Value, paths: &[String]) -> Value {
|
||||
let mut previews = Vec::new();
|
||||
let mut remaining = Vec::new();
|
||||
for path in paths {
|
||||
if previews.len() < 7 {
|
||||
if let Ok(data) = bridge_png_content(root, &root.join(path)) {
|
||||
previews.push((path.clone(), data));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
remaining.push(path.clone());
|
||||
}
|
||||
payload["previewPaths"] = json!(previews.iter().map(|(p, _)| p).collect::<Vec<_>>());
|
||||
payload["remainingPreviewPaths"] = json!(remaining);
|
||||
let mut content = vec![json!({"type":"text", "text":payload.to_string()})];
|
||||
for (path, data) in previews {
|
||||
content.push(json!({"type":"text", "text":format!("图片预览:{path}")}));
|
||||
content.push(bridge_tool_image_block(data));
|
||||
}
|
||||
json!({"content":content})
|
||||
}
|
||||
|
||||
async fn bridge_prepare_game_art_validated(
|
||||
@@ -2665,14 +2689,14 @@ async fn bridge_prepare_game_art_validated(
|
||||
.map_err(|cause| PrepareGameArtError::PackageGenerationFailed { cause })?;
|
||||
let resources = bridge_art_resources(root, &package.asset_paths, &package.slice_paths)
|
||||
.map_err(|cause| PrepareGameArtError::ArtResourcesUnreadable { cause })?;
|
||||
let images = package
|
||||
let paths = package
|
||||
.asset_paths
|
||||
.iter()
|
||||
.chain(package.slice_paths.iter())
|
||||
.take(7)
|
||||
.filter_map(|relative| bridge_png_content(root, &root.join(relative)).ok())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
Ok(bridge_tool_result(
|
||||
Ok(bridge_art_tool_result(
|
||||
root,
|
||||
json!({
|
||||
"status": "completed",
|
||||
"mode": mode.as_str(),
|
||||
@@ -2681,11 +2705,10 @@ async fn bridge_prepare_game_art_validated(
|
||||
"warnings": bounded_remote_warnings(&package.warnings),
|
||||
"sliceWarnings": bounded_remote_warnings(&package.slice_warnings),
|
||||
"resources": resources,
|
||||
"derivativePolicy": "新建或重生成的标准图集必须包含四张 canonical 切片;旧可信图集缺少切片时只能按 warning 使用完整图集,不得伪造衍生素材",
|
||||
"next": "读取这些相对路径并把合适素材接入核心游戏画面"
|
||||
})
|
||||
.to_string(),
|
||||
images,
|
||||
"derivativePolicy": "内容要求不保证切片数量或用途顺序;查看总图及切片后识别用途。零切片时保留总图与告警;真实本地处理产物通过 agc_import_account_assets.localPaths 登记,不能伪造平台身份",
|
||||
"next": "查看预览及 remainingPreviewPaths 的图片,确认实体、状态和缺失内容后再接入游戏"
|
||||
}),
|
||||
&paths,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -2809,8 +2832,6 @@ pub(in crate::agent) const GENERATE_IMAGE_IMAGE_SIZES: &[&str] = &["0.5K", "1K",
|
||||
pub(in crate::agent) const GENERATE_IMAGE_SLICE_MODES: &[&str] = &["connected-components", "grid"];
|
||||
pub(in crate::agent) const GENERATE_IMAGE_GRID_AXIS_MIN: u32 = 1;
|
||||
pub(in crate::agent) const GENERATE_IMAGE_GRID_AXIS_MAX: u32 = 32;
|
||||
pub(in crate::agent) const GENERATE_IMAGE_SLICE_COUNT_MIN: u64 = 1;
|
||||
pub(in crate::agent) const GENERATE_IMAGE_SLICE_COUNT_MAX: u64 = 256;
|
||||
|
||||
/// `agc_generate_image` 允许的入参字段。
|
||||
const GENERATE_IMAGE_ARGUMENTS: &[&str] = &[
|
||||
@@ -2823,7 +2844,6 @@ const GENERATE_IMAGE_ARGUMENTS: &[&str] = &[
|
||||
"sliceMode",
|
||||
"gridX",
|
||||
"gridY",
|
||||
"sliceCount",
|
||||
"screenColor",
|
||||
];
|
||||
|
||||
@@ -2964,18 +2984,14 @@ fn validate_generate_image_slice_declaration(
|
||||
slice_mode: Option<&str>,
|
||||
grid_x: Option<u32>,
|
||||
grid_y: Option<u32>,
|
||||
slice_count: Option<usize>,
|
||||
) -> Result<(), GenerateImageError> {
|
||||
if kind == GameCreationAppAssetKind::IconSpritesheet {
|
||||
if slice_mode.is_none() {
|
||||
return Err(GenerateImageError::SpritesheetSliceModeMissing);
|
||||
}
|
||||
if slice_mode == Some("grid") && slice_count.is_some() {
|
||||
return Err(GenerateImageError::SpritesheetSliceCountNotAllowed);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if slice_mode.is_some() || grid_x.is_some() || grid_y.is_some() || slice_count.is_some() {
|
||||
if slice_mode.is_some() || grid_x.is_some() || grid_y.is_some() {
|
||||
return Err(GenerateImageError::SliceDeclarationNotForKind { kind });
|
||||
}
|
||||
Ok(())
|
||||
@@ -3033,7 +3049,6 @@ pub(in crate::agent) struct GenerateImageInput {
|
||||
pub(in crate::agent) slice_mode: Option<String>,
|
||||
pub(in crate::agent) grid_x: Option<u32>,
|
||||
pub(in crate::agent) grid_y: Option<u32>,
|
||||
pub(in crate::agent) slice_count: Option<usize>,
|
||||
pub(in crate::agent) screen_color: Option<String>,
|
||||
}
|
||||
|
||||
@@ -3080,29 +3095,7 @@ pub(in crate::agent) fn generate_image_input(
|
||||
return Err(GenerateImageError::GridOutOfRange { axis, got });
|
||||
}
|
||||
}
|
||||
let slice_count = arguments
|
||||
.get("sliceCount")
|
||||
.filter(|value| !value.is_null())
|
||||
.map(|value| {
|
||||
value
|
||||
.as_u64()
|
||||
.filter(|count| {
|
||||
(GENERATE_IMAGE_SLICE_COUNT_MIN..=GENERATE_IMAGE_SLICE_COUNT_MAX)
|
||||
.contains(count)
|
||||
})
|
||||
.map(|count| count as usize)
|
||||
.ok_or_else(|| GenerateImageError::SliceCountInvalid {
|
||||
got: not_text_repr(value),
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
validate_generate_image_slice_declaration(
|
||||
kind,
|
||||
slice_mode.as_deref(),
|
||||
grid_x,
|
||||
grid_y,
|
||||
slice_count,
|
||||
)?;
|
||||
validate_generate_image_slice_declaration(kind, slice_mode.as_deref(), grid_x, grid_y)?;
|
||||
let screen_color = normalize_generate_image_screen_color(arguments, kind)?;
|
||||
Ok(GenerateImageInput {
|
||||
prompt,
|
||||
@@ -3114,7 +3107,6 @@ pub(in crate::agent) fn generate_image_input(
|
||||
slice_mode,
|
||||
grid_x,
|
||||
grid_y,
|
||||
slice_count,
|
||||
screen_color,
|
||||
})
|
||||
}
|
||||
@@ -3136,7 +3128,6 @@ async fn bridge_generate_image(
|
||||
slice_mode,
|
||||
grid_x,
|
||||
grid_y,
|
||||
slice_count,
|
||||
screen_color,
|
||||
} = generate_image_input(arguments)?;
|
||||
let options = PlatformArtAssetGenerationOptions {
|
||||
@@ -3146,7 +3137,6 @@ async fn bridge_generate_image(
|
||||
asset_kind: kind,
|
||||
asset_label: asset_name.clone(),
|
||||
replace_existing: false,
|
||||
slice_count,
|
||||
slice_mode,
|
||||
grid_x,
|
||||
grid_y,
|
||||
@@ -3174,17 +3164,22 @@ async fn bridge_generate_image(
|
||||
.await
|
||||
.map_err(|message| GenerateImageError::PlatformArtRequestFailed { message })?;
|
||||
emit_game_creator_manifest_invalidated(&state.root, "direct-codex-art");
|
||||
let slice_paths = generated
|
||||
.slices
|
||||
.iter()
|
||||
.map(|slice| slice.local_path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let resources = bridge_art_resources(
|
||||
&state.root,
|
||||
std::slice::from_ref(&generated.asset.local_path),
|
||||
&[],
|
||||
&slice_paths,
|
||||
)
|
||||
.map_err(|message| GenerateImageError::ArtResourcesUnreadable { message })?;
|
||||
let images = bridge_png_content(&state.root, &state.root.join(&generated.asset.local_path))
|
||||
.ok()
|
||||
.into_iter()
|
||||
let paths = std::iter::once(generated.asset.local_path.clone())
|
||||
.chain(slice_paths.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(bridge_tool_result(
|
||||
Ok(bridge_art_tool_result(
|
||||
&state.root,
|
||||
json!({
|
||||
"status": "completed",
|
||||
"kind": kind,
|
||||
@@ -3208,14 +3203,10 @@ async fn bridge_generate_image(
|
||||
"sliceMode": generated.slice_mode,
|
||||
"gridX": generated.grid_x,
|
||||
"gridY": generated.grid_y,
|
||||
"slicePaths": generated
|
||||
.slices
|
||||
.iter()
|
||||
.map(|slice| slice.local_path.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
.to_string(),
|
||||
images,
|
||||
"slicePaths": slice_paths,
|
||||
"next": "查看总图和实际切片,使用图片查看能力检查 remainingPreviewPaths;按内容识别用途,不按返回顺序或文件名赋义。",
|
||||
}),
|
||||
&paths,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -4681,79 +4672,44 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn generate_image_slice_declaration_is_explicit_and_self_consistent() {
|
||||
let missing = validate_generate_image_slice_declaration(
|
||||
assert!(validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
.expect_err("icon-spritesheet without sliceMode must fail closed")
|
||||
.to_user_msg();
|
||||
assert!(missing.contains("没有默认值"), "{missing}");
|
||||
assert!(missing.contains("connected-components"), "{missing}");
|
||||
|
||||
.is_err());
|
||||
assert!(validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
Some("connected-components"),
|
||||
None,
|
||||
None,
|
||||
Some(4),
|
||||
None
|
||||
)
|
||||
.is_ok());
|
||||
assert!(validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
Some("grid"),
|
||||
Some(3),
|
||||
Some(2),
|
||||
None,
|
||||
Some(2)
|
||||
)
|
||||
.is_ok());
|
||||
let grid_with_count = validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
Some("grid"),
|
||||
Some(2),
|
||||
Some(2),
|
||||
Some(4),
|
||||
)
|
||||
.expect_err("grid mode must not carry sliceCount")
|
||||
.to_user_msg();
|
||||
assert!(grid_with_count.contains("gridX×gridY"), "{grid_with_count}");
|
||||
|
||||
let wrong_kind = validate_generate_image_slice_declaration(
|
||||
assert!(validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::Image,
|
||||
Some("connected-components"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
.expect_err("slice declaration must stay scoped to icon-spritesheet")
|
||||
.to_user_msg();
|
||||
assert!(
|
||||
wrong_kind.contains("仅对 kind=icon-spritesheet 生效"),
|
||||
"{wrong_kind}"
|
||||
);
|
||||
let wrong_kind_count = validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::Image,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(8),
|
||||
)
|
||||
.expect_err("sliceCount-only violation must be rejected")
|
||||
.to_user_msg();
|
||||
assert!(
|
||||
wrong_kind_count.contains("sliceCount"),
|
||||
"sliceCount-only violation must name sliceCount: {wrong_kind_count}"
|
||||
);
|
||||
.is_err());
|
||||
assert!(validate_generate_image_slice_declaration(
|
||||
GameCreationAppAssetKind::Image,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
.is_ok());
|
||||
for kind in ["image", "icon-spritesheet"] {
|
||||
assert!(generate_image_input(&json!({"prompt":"素材", "kind":kind, "sliceMode":"connected-components", "sliceCount":4})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6389,6 +6345,53 @@ mod tests {
|
||||
turn_ids
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn art_result_returns_all_resources_and_labels_bounded_previews() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
init_local_game_project_at(root, "art-result", "图集返回").unwrap();
|
||||
let mut paths = vec!["assets/sheet.png".to_string()];
|
||||
paths.extend((0..8).map(|i| art_slice_path("source", i)));
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
std::fs::create_dir_all(root.join(path).parent().unwrap()).unwrap();
|
||||
image::RgbaImage::from_pixel(2, 2, image::Rgba([i as u8, 20, 30, 128]))
|
||||
.save(root.join(path))
|
||||
.unwrap();
|
||||
import_canvas_asset_at(
|
||||
root,
|
||||
path,
|
||||
GameCreationAppAssetKind::Icon,
|
||||
"image/png",
|
||||
"canvas",
|
||||
Some(format!("resource-{i}")),
|
||||
Some(format!("object-{i}")),
|
||||
Some("task".into()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let resources = bridge_art_resources(root, &paths[..1], &paths[1..]).unwrap();
|
||||
assert_eq!(resources.len(), paths.len());
|
||||
let response = bridge_art_tool_result(
|
||||
root,
|
||||
json!({"resources":resources, "slicePaths":paths[1..]}),
|
||||
&paths,
|
||||
);
|
||||
let payload: Value =
|
||||
serde_json::from_str(response["content"][0]["text"].as_str().unwrap()).unwrap();
|
||||
assert_eq!(payload["previewPaths"], json!(paths[..7]));
|
||||
assert_eq!(payload["remainingPreviewPaths"], json!(paths[7..]));
|
||||
assert_eq!(payload["resources"].as_array().unwrap().len(), 9);
|
||||
for (i, path) in paths.iter().take(7).enumerate() {
|
||||
assert!(response["content"][1 + i * 2]["text"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains(path));
|
||||
assert_eq!(response["content"][2 + i * 2]["type"], "image");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_art_resource_exposes_only_the_safe_identity_projection() {
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
|
||||
@@ -346,12 +346,6 @@ fn direct_tools_mcp_specs_for_plugins(
|
||||
"maximum": 32,
|
||||
"description": prompt_text!("directTools.agc_generate_image.parameters.gridY")
|
||||
},
|
||||
"sliceCount": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 256,
|
||||
"description": prompt_text!("directTools.agc_generate_image.parameters.sliceCount")
|
||||
},
|
||||
"screenColor": {
|
||||
"type": "string",
|
||||
"description": prompt_text!("directTools.agc_generate_image.parameters.screenColor")
|
||||
@@ -2849,14 +2843,9 @@ mod tests {
|
||||
image_tool["inputSchema"]["properties"]["sliceMode"]["enum"],
|
||||
json!(["connected-components", "grid"])
|
||||
);
|
||||
assert_eq!(
|
||||
image_tool["inputSchema"]["properties"]["sliceCount"]["minimum"],
|
||||
json!(1)
|
||||
);
|
||||
assert_eq!(
|
||||
image_tool["inputSchema"]["properties"]["sliceCount"]["maximum"],
|
||||
json!(256)
|
||||
);
|
||||
assert!(image_tool["inputSchema"]["properties"]
|
||||
.get("sliceCount")
|
||||
.is_none());
|
||||
assert!(
|
||||
image_tool["inputSchema"]["properties"]["screenColor"]["description"]
|
||||
.as_str()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+41
-1
@@ -901,7 +901,10 @@ fn durable_legacy_generation_result(
|
||||
result: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let mut durable = durable_legacy_generation_object(result);
|
||||
for field in ["spritesheetWidth", "spritesheetHeight"] {
|
||||
if let Some(mode) = json_string_field(result, "sliceMode") {
|
||||
durable.insert("sliceMode".into(), serde_json::Value::String(mode));
|
||||
}
|
||||
for field in ["spritesheetWidth", "spritesheetHeight", "gridX", "gridY"] {
|
||||
if let Some(value) = result.get(field).and_then(serde_json::Value::as_u64) {
|
||||
durable.insert(field.to_string(), serde_json::Value::from(value));
|
||||
}
|
||||
@@ -1115,6 +1118,43 @@ pub(in crate::agent) fn remove_platform_art_generation_runtime_state_at(
|
||||
/// 原样保留(不迁移、不删除、不阻塞),由对应动作自己的请求迁移。
|
||||
///
|
||||
/// 返回 `Ok(false)` 表示没有需要迁移的旧账本。任何身份无法安全解释的情形都失败关闭。
|
||||
/// 只读匹配旧输出槽,返回其原身份;不改变冻结请求或已受理操作。
|
||||
pub(super) fn matching_legacy_standalone_platform_art_context_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
legacy_run_id: &str,
|
||||
candidates: &[PlatformArtGenerationRuntimeContext],
|
||||
) -> Result<Option<PlatformArtGenerationRuntimeContext>, String> {
|
||||
let path = platform_art_generation_runtime_relative_path(agent_id, legacy_run_id);
|
||||
let Some(state) =
|
||||
read_agent_runtime_json_sidecar_with_max_bytes::<PlatformArtGenerationRuntimeState>(
|
||||
root,
|
||||
&path,
|
||||
"External Editor 生成账本",
|
||||
PLATFORM_ART_GENERATION_RUNTIME_MAX_BYTES,
|
||||
)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(candidate) = candidates.iter().find(|candidate| {
|
||||
candidate.agent_id == state.agent_id
|
||||
&& candidate.action_fingerprint == state.action_fingerprint
|
||||
}) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let identity = format!("{agent_id}:{legacy_run_id}");
|
||||
let context = PlatformArtGenerationRuntimeContext {
|
||||
run_id: legacy_run_id.to_string(),
|
||||
task_id: identity.clone(),
|
||||
session_id: identity.clone(),
|
||||
action_id: identity,
|
||||
..candidate.clone()
|
||||
};
|
||||
read_platform_art_generation_runtime_state(root, &context)?
|
||||
.ok_or_else(|| "旧输出槽账本在读取期间消失,已拒绝创建新请求".to_string())?;
|
||||
Ok(Some(context))
|
||||
}
|
||||
|
||||
pub(super) fn adopt_legacy_standalone_platform_art_generation_runtime_state_at(
|
||||
root: &Path,
|
||||
context: &PlatformArtGenerationRuntimeContext,
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
use crate::agent::direct_tool_bridge::{
|
||||
GENERATE_IMAGE_ASPECT_RATIOS, GENERATE_IMAGE_GRID_AXIS_MAX, GENERATE_IMAGE_GRID_AXIS_MIN,
|
||||
GENERATE_IMAGE_IMAGE_SIZES, GENERATE_IMAGE_SLICE_COUNT_MAX, GENERATE_IMAGE_SLICE_COUNT_MIN,
|
||||
GENERATE_IMAGE_SLICE_MODES,
|
||||
GENERATE_IMAGE_IMAGE_SIZES, GENERATE_IMAGE_SLICE_MODES,
|
||||
};
|
||||
use crate::agent::generation::PLATFORM_ART_ASSET_GENERATION_KINDS;
|
||||
use crate::agent::tool::error::{ProjectPermissionRejection, ToolArgumentsRejection, ToolFailure};
|
||||
@@ -48,9 +47,7 @@ pub(crate) enum GenerateImageError {
|
||||
GridAxisNotInteger { axis: &'static str },
|
||||
GridAxesMissing,
|
||||
GridOutOfRange { axis: &'static str, got: u32 },
|
||||
SliceCountInvalid { got: String },
|
||||
SpritesheetSliceModeMissing,
|
||||
SpritesheetSliceCountNotAllowed,
|
||||
SliceDeclarationNotForKind { kind: GameCreationAppAssetKind },
|
||||
ScreenColorNotForKind { kind: GameCreationAppAssetKind },
|
||||
ScreenColorMalformed { got: String },
|
||||
@@ -171,21 +168,12 @@ impl ToolFailure for GenerateImageError {
|
||||
"生成图片失败:{axis}={got} 超出范围,gridX/gridY 必须在 {GENERATE_IMAGE_GRID_AXIS_MIN} 到 {GENERATE_IMAGE_GRID_AXIS_MAX} 之间。"
|
||||
)
|
||||
}
|
||||
Self::SliceCountInvalid { got } => {
|
||||
format!(
|
||||
"生成图片失败:sliceCount「{got}」非法,必须是 {GENERATE_IMAGE_SLICE_COUNT_MIN} 到 {GENERATE_IMAGE_SLICE_COUNT_MAX} 的整数。"
|
||||
)
|
||||
}
|
||||
Self::SpritesheetSliceModeMissing => format!(
|
||||
"生成图片失败:kind={} 必须显式声明 sliceMode,没有默认值:需求要求等分网格、固定槽位或指定行列数时传 sliceMode=grid 并提供 gridX/gridY;自由排布、数量不定或只要求一张图集时传 sliceMode=connected-components。",
|
||||
GameCreationAppAssetKind::IconSpritesheet
|
||||
),
|
||||
Self::SpritesheetSliceCountNotAllowed => {
|
||||
"生成图片失败:sliceMode=grid 的素材张数由 gridX×gridY 决定,不接受 sliceCount。"
|
||||
.to_string()
|
||||
}
|
||||
Self::SliceDeclarationNotForKind { kind } => format!(
|
||||
"生成图片失败:sliceMode/gridX/gridY/sliceCount 仅对 kind={} 生效,当前 kind={kind}。",
|
||||
"生成图片失败:sliceMode/gridX/gridY 仅对 kind={} 生效,当前 kind={kind}。",
|
||||
GameCreationAppAssetKind::IconSpritesheet
|
||||
),
|
||||
Self::ScreenColorNotForKind { kind } => format!(
|
||||
|
||||
@@ -4153,7 +4153,6 @@ pub(crate) fn prepare_local_project_asset_generation(
|
||||
)?
|
||||
.unwrap_or_else(|| LOCAL_PROJECT_ASSET_DEFAULT_ASSET_NAME.to_string()),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
// 切分模式没有默认值:GUI 快速编辑只按自由排布生成图集,因此仅在 icon-spritesheet
|
||||
// 时显式声明连通域切分;等分网格或固定槽位需求由外部 API 显式传 grid + gridX/gridY。
|
||||
slice_mode: (asset_kind == GameCreationAppAssetKind::IconSpritesheet)
|
||||
@@ -4266,7 +4265,6 @@ mod local_project_asset_generation_tests {
|
||||
assert_eq!(request.prompt, "像素月光厨房主角");
|
||||
assert_eq!(request.options.asset_kind.as_str(), kind);
|
||||
assert!(!request.options.replace_existing);
|
||||
assert_eq!(request.options.slice_count, None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# AGC 图集实际产物与语义识别实施计划
|
||||
|
||||
- Version: 1
|
||||
- Status: Implemented, Awaiting Acceptance
|
||||
- Date: 2026-10-05
|
||||
- Parent Spec: [里程碑](./【里程碑】AGC图集实际产物与语义识别-2026-10-05.md)
|
||||
|
||||
## 顺序与边界
|
||||
|
||||
1. 删除 AGC schema、解析、错误枚举、options 及新请求中的数量参数;旧身份材料仅用于活动账本查找。
|
||||
2. 按源资源摘要与序号保存切片,普通清单与总图正确关联;严格事务快照与恢复白名单支持实际集合,旧固定合同只读兼容。
|
||||
3. 修正完成结果白名单以保留模式和网格参数,历史无声明零片结果返回告警;修改美术包回执、复用、重生成快照和结果投影,接受带告警的零切片;保留原完整性、身份、CAS 和预算限制。
|
||||
4. 返回完整资源身份及路径标注预览,同步提示词、随包 skill、主规范和共享记忆。
|
||||
5. 跑生成、事务恢复、Direct 重生成与工具契约相关测试;Rust check,prompt/skill 检查,编码和文档索引检查。
|
||||
|
||||
按实现、定向验证、边界复核分段检查,每段约 30 分钟;仅影响上述验收的问题扩大修改。失败时保留旧账本和事务现场,不调用付费 Provider 试错。回滚为本轮客户端代码与说明变更;已落盘的新集合需由支持动态合同的版本读取,不能降级为旧四片判断。
|
||||
|
||||
## 验证证据
|
||||
|
||||
- `cargo test --locked --offline --bin genarrative-ai-game-creator-shell canvas_generation_tests`:118 项通过。
|
||||
- 同一测试可执行文件的 Direct runtime、tool bridge、MCP、external_generation_state_tests 定向分组:93/44/32/6 项通过。
|
||||
- `cargo test --locked --offline --test runtime_prompt_bundle_build --test prompt_source_boundaries`:24 项通过。
|
||||
- `cargo check --locked --offline --bin genarrative-ai-game-creator-shell`、`cargo fmt --check` 通过。
|
||||
- `npm run skill-pack:check --prefix apps/ai-game-creator-shell`、`npm run check:doc-index`、按变更列表运行 `npm run check:encoding`、`git diff --check` 通过。
|
||||
- 回环 HTTP 测试在允许监听端口的环境运行;真实付费 Provider 与视觉试玩未运行。通用 skill validator 缺 PyYAML,使用仓库 skill 包检查及既有回归替代。
|
||||
|
||||
具体条款与证据见对应里程碑。
|
||||
@@ -0,0 +1,36 @@
|
||||
# AGC 图集实际产物与语义识别
|
||||
|
||||
- Version: 1
|
||||
- Status: Implemented, Awaiting Acceptance
|
||||
- Date: 2026-10-05
|
||||
- Parent Spec: [AGC 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#2026-08-23-direct-codex-美术包显式重生成与切片投影)
|
||||
|
||||
## 范围与评审
|
||||
|
||||
用户已确认客户端范围并指示开工;按已讨论方案复核入口、持久化、恢复和 Agent 说明后实施。此前生成需求入参工作已有定向验证,用户指示继续第三项;本轮不改变其请求内容合同。
|
||||
|
||||
必须移除客户端数量参数及按顺序赋义,按实际产物返回完整身份和可查看图片。风险项为动态集合事务、旧活动账本恢复与零切片告警;可选的自动语义分类、补切工作流不实施。服务端 API 与 OpenAPI 不变。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 美术包及普通新请求完全不含 sliceCount;工具不接受该参数,保留模式和网格参数。
|
||||
2. 0/2/4/6 个有效切片均按实际结果保存;零片保留有效总图并告警,身份或文件不可信仍拒绝。
|
||||
3. 中性路径按源图隔离,无按序用途字段;两个工具提供全部资源身份及可定位预览。
|
||||
4. Agent 说明明确内容要求不是严格数量或用途保证,先看图再处理;本地产物通过既有工具登记。
|
||||
5. 完成账本保留切分声明,历史零切片账本缺少声明时带告警保留总图;事务与重生成恢复核对动态集合;旧固定事务、旧用途文件和旧已付费请求继续可恢复且不增加生成 POST。
|
||||
6. 定向测试、Rust 检查、提示词与 skill 包、文档索引、编码及 diff 检查通过;真实 Provider 未验证部分明确记录。
|
||||
|
||||
## 验收证据
|
||||
|
||||
| 条款 | 结果与证据 |
|
||||
| --- | --- |
|
||||
| 1 新请求与工具合同 | HTTP 夹具断言普通/美术包新请求完全不含 sliceCount;MCP schema 和解析拒绝旧工具参数,模式及网格字段保留 |
|
||||
| 2 实际数量与零片 | `spritesheet_commits_actual_slice_sets_and_isolates_ordinary_outputs` 覆盖普通与严格提交的 0/2/4/6 片;零片总图、告警及回执可读;已有透明度、身份、像素和预算测试继续通过 |
|
||||
| 3 路径与投影 | 同一夹具验证目录隔离、真实总图路径及无用途别名;`art_result_returns_all_resources_and_labels_bounded_previews` 验证九项身份完整、七张有界预览逐项标注及剩余路径 |
|
||||
| 4 Agent 说明 | 工具与随包 skill 改为看图识别、按实际内容处理;skill-pack:check 与提示词测试通过 |
|
||||
| 5 恢复与兼容 | 新旧请求三种状态及旧输出槽 HTTP 夹具保留原正文/幂等身份,已受理任务无新生成 POST;动态事务中断恢复、路径白名单、原固定合同与 Direct 重生成回归通过;动态包复用和快照漂移检测覆盖 0/2/4/6 片 |
|
||||
| 6 工程检查 | 生成组 118、Direct 运行时 93、工具桥 44、MCP 32、账本 6、提示词 24,共 317 项 Rust 测试通过;cargo check、rustfmt、skill 包、文档索引、变更文件编码及 diff 检查通过 |
|
||||
|
||||
使用本地 HTTP 夹具验证生产入口,没有调用真实付费 Provider,也没有客户端视觉试玩;这些测试不证明素材语义正确。通用 skill quick_validate 因本机缺少 PyYAML 无法运行,已使用仓库 skill 包完整性检查和提示词/随包 skill 回归替代,未安装依赖。原无参数全仓编码命令受沙箱子进程限制,按真实变更文件列表重跑通过。
|
||||
|
||||
实现等待视觉与产品验收,PR 保持草稿。本轮服务端与 OpenAPI 无修改。
|
||||
@@ -30,4 +30,4 @@
|
||||
| 提示词与普通构建 | `runtime_prompt_bundle_build` 18 项、`prompt_source_boundaries` 6 项及 `cargo check --locked --offline --bin genarrative-ai-game-creator-shell` 通过 |
|
||||
| 文档与资源 | skill-pack:check、check:doc-index、变更文件编码、rustfmt 和差异检查通过 |
|
||||
|
||||
总计 53 项 Rust 测试通过。本地 HTTP 夹具需要监听回环端口,沙箱限制后已在获准的沙箱外环境重跑通过。真实客户端 / 付费 Provider 整包生成未执行;这些证据证明请求传输与恢复,不保证生成图片的视觉质量。固定四片、按序分配用途及 brief 长度不匹配仍属后续工作。本里程碑等待验收,不推进第三项。
|
||||
总计 53 项 Rust 测试通过。本地 HTTP 夹具需要监听回环端口,沙箱限制后已在获准的沙箱外环境重跑通过。真实客户端 / 付费 Provider 整包生成未执行;这些证据证明请求传输与恢复,不保证生成图片的视觉质量。后续客户端切片工作由独立规范覆盖。
|
||||
|
||||
@@ -394,7 +394,7 @@ Copy Artifact 插件在**非 SYSTEM 认证**下按「认证用户」判权:只
|
||||
## 同一条链路两处上限不一致:平台合法产出被客户端整条丢弃
|
||||
|
||||
- 现象:客户端报「生成素材失败:platform-generation-result-unknown: 异步生成完成结果无法绑定到 operationId:External Editor 旧同步结果的图集切片超过 64 个」,而平台侧这次生成**其实已经成功并切完图**(任务账本耗时正常、`assetId` 为空、没有任何素材落盘,付费产物被丢)。
|
||||
- 成因:图集切片上限在链路里存在两份字面量——平台切分、Agent 工具 schema `sliceCount` 与持久化产物批次都是 256,客户端结果绑定门写着 64(`agent/generation/{canvas_generation.rs,external_generation_state.rs}`)。自动切分(`connected-components` + `sliceCount=null`)切出 65~256 片是合法产出,客户端比平台更严就会把结果整条判失败。
|
||||
- 成因:图集切片上限在链路里存在两份字面量——平台切分与持久化产物批次都是 256,客户端结果绑定门写着 64(`agent/generation/{canvas_generation.rs,external_generation_state.rs}`)。自动切分(`connected-components` 且省略数量参数)切出 65~256 片是合法产出,客户端比平台更严就会把结果整条判失败。
|
||||
- 处理:客户端门统一到 `PLATFORM_ART_SPRITESHEET_MAX_SLICES = 256`,判据与文案各只留一份(数字由常量插值),并在注释里点名三处同值权威(平台切分常量、工具 schema、公开契约)。
|
||||
- 复用判据:凡是「平台产出 → 客户端校验后落盘」的链路,客户端门只能表达**安全 / 预算**约束,不得比平台的产品上限更严;两边上限要引同一个常量或同一份文档,改一边时必须同时改另一边,并补一条「上限之内必须能落盘」的回归用例。
|
||||
|
||||
@@ -5426,10 +5426,10 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- 处理:显式重做使用 `mode=regenerate`,普通请求使用 `reuse-or-create`。模式由 Codex 根据当前用户请求通过审核工具显式选择;客户端不再使用 Unicode NFKC、关键词、否定词表或独立确认句式判断业务意图。旧文本授权规则已被 2026-09-03 MCP 决策替代,相关无调用实现于 2026-09-23 删除。工具桥绑定活动客户端回合与稳定 `clientTurnId`,冻结首次 `brief` 摘要;缺少活动回合或摘要冲突仍拒绝。项目权限、账号、计费、幂等、锁与未知结果恢复合同继续有效。
|
||||
- 幂等与恢复:同一调用完成回包丢失只从 `completed` 持久结果等值重放,不能因重试再次扣费。App 必须在 Direct 调用前落盘原始 User 消息和回合 ID,Tauri 必须在成功返回前幂等落盘同 ID assistant 终态;同进程重复水合若命中“回合仍在运行”,只能显示瞬时占用提示,不得以稳定 assistant messageId 写成终态并抢占原执行的成功回复。恢复扫描与启动前置恢复必须发现 `resetting / compensating / anchored in-progress` 并在专用锁内恢复,重开项目只续跑真正未回答的原身份。整条付费链必须持有专用跨进程执行锁;换新回合时先持久化 `resetting` 再清理旧阶段账本,不得通过删除 workflow 留出无主窗口。崩溃补偿只恢复旧文件并清 replacement CAS 锚点,已 `prepared / accepted` 阶段账本、原 `Idempotency-Key / operationId` 必须保留,同冻结意图续跑复用旧请求;未知账本在文件 mutation 前失败关闭。只有没有任何阶段账本和替换锚点的孤立 workflow 空壳可原子接管;旧 schema 和其余冲突失败关闭。遇到 prompt 或当前 art-spec 身份不一致的未决账本必须保留原 operation 并返回对账错误。
|
||||
- 执行边界(2026-09-24 校准):DirectProject 的 cwd 与 AGC 业务身份根是用户选择的 canonical 项目根;其原生 OS 路径字节与权威 manifest `projectId` 经域标签和独立长度前缀编码后绑定连接池和 thread 身份。旧的 `game/` 唯一可写根、禁止全部网络 / 命令 / MCP 的描述已失效;也不能把后来的“完整访问”描述理解为绕过当前宿主门禁。当前 thread 使用 `sandbox=read-only`、`approvalPolicy=untrusted`,turn 使用 `sandboxPolicy.type=readOnly`;原生命令按逐次审批与宿主执行许可处理,客户端 MCP 仍校验项目绑定、业务权限和副作用许可。具体边界以[主实施计划“宿主验收与执行许可合同”](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#宿主验收与执行许可合同)及当前实现为准,Provider 凭据保持隔离。
|
||||
- 资源投影:标准图集首次创建和重生成都要求四张透明、可见、像素及平台身份唯一的 canonical 切片;工具只回传通过私有回执、公开清单、源图和顶层登记交叉验证的 `slicePaths` 与安全 `resources`。部分/opaque/重复/缺回执切片必须告警,不能把公开清单或顶层自述身份当作 Canvas 权威。
|
||||
- 资源投影:标准图集首次创建和重生成均按实际切片集合提交,保留透明、可见、像素及平台身份唯一的校验;有效总图零切片时仍返回成功与告警;工具只回传通过私有回执、公开清单、源图和顶层登记交叉验证的 `slicePaths` 与安全 `resources`。部分/opaque/重复/缺回执切片必须告警,不能把公开清单或顶层自述身份当作 Canvas 权威。
|
||||
- 同进程恢复补充:命中“同一 stable turn 仍在运行”后除禁止写 assistant 终态外,还必须删除当前 App 实例的恢复 claim。这样原调用随后成功时显式刷新能读取其终态,随后失败时也能按相同 `clientTurnId` 再次续跑;不要靠重载 WebView 清理进程内 claim,也不要用无界定时轮询制造并发调用。
|
||||
- 严格图集崩溃补充:规范图和背景图的两文件 rollback 不覆盖严格图集事务已经整体修改的 `.agent/manifest.json`、私有回执、公开清单、主图集、四切片和切片清单。必须在严格调用前持久化 pending 及九项旧合同身份;重启恢复先对账底层严格事务,完整新合同直接收口完成,完整旧合同才补偿前两阶段,混合或漂移状态失败关闭。不要在严格提交成功后局部恢复前两张图。
|
||||
- 部分旧包补充:rollback 的规范图/背景图必须保存旧字节与旧 manifest entry,不能把这两项缺失隐式当成空内容;显式 `regenerate` 因此只在这两项可信可回滚时开放。历史主图集、私有回执、公开清单或 canonical 切片可以缺失,但八个严格路径与受管顶层 asset identity 必须逐项冻结其真实 `Present/Some` 或 `Missing/None` 状态,补偿也必须恢复相同存在性。不要因为旧美术包缺切片而阻断重生成,也不要把本轮新建的严格文件误记成旧文件。
|
||||
- 严格图集崩溃补充:规范图和背景图的两文件 rollback 不覆盖严格图集事务已经整体修改的 `.agent/manifest.json`、私有回执、公开清单、主图集、实际切片和切片清单。必须在严格调用前持久化 pending 及完整旧合同身份;重启恢复先对账底层严格事务,完整新合同直接收口完成,完整旧合同才补偿前两阶段,混合或漂移状态失败关闭。不要在严格提交成功后局部恢复前两张图。
|
||||
- 部分旧包补充:rollback 的规范图/背景图必须保存旧字节与旧 manifest entry,不能把这两项缺失隐式当成空内容;显式 `regenerate` 因此只在这两项可信可回滚时开放。历史主图集、私有回执、公开清单或 canonical 切片可以缺失,但固定合同文件、实际切片与受管顶层 asset identity 必须逐项冻结其真实 `Present/Some` 或 `Missing/None` 状态,补偿也必须恢复相同存在性。不要因为旧美术包缺切片而阻断重生成,也不要把本轮新建的严格文件误记成旧文件。
|
||||
- 对话扫描与 claim 补充:历史中出现 `User A / User B / Assistant B` 时,B 已回答不代表 A 已回答,扫描必须继续寻找 A。成功 Direct 回复在 Rust 返回前已经落盘,前端冗余 append 失败不能据此重跑;普通错误回复的显式落盘失败时,恢复 claim 要保持到 React fallback writer 的同一 messageId append 明确收敛。writer 成功或明确失败后才释放;失败路径要停止该消息的自动迟到重试,再由显式重新加载对话复用原 stable turn。终态后及时删除 claim,避免 Set 无界增长。
|
||||
|
||||
## Native shell CI 不能在测试阶段重新解析 Cargo registry(2026-08-26)
|
||||
@@ -6415,3 +6415,9 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **别踩**:不要写成裸前缀正则(`^/pay`)——它会吞掉 `/payment/x`、`/paycheckout/x` 这类同名邻居;也不要把深链塞进精确 allowlist 的 alternatives 里(`pay` 的 alternatives 只匹配 `/pay`)。
|
||||
- **判据/取证**:`node --test scripts/check-nginx-spa-routes.test.mjs`(正/反用例,含「写回精确匹配即红」)、`npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`、`cargo test -p pingora-gateway -- pay_checkout_deep_link matches_nginx_route_parity_matrix`;线上复验 `curl -s -o /dev/null -w '%{http_code}' https://<平台域名>/pay/<checkoutToken>` → 200 且正文与 `/` 同一份 `index.html`。
|
||||
- **关联**:`scripts/check-nginx-spa-routes.mjs`、`deploy/pingora/nginx-route-parity.matrix.json`、`server-rs/crates/pingora-gateway/src/main.rs`、`server-rs/crates/api-server/src/payment.rs`、`deploy/nginx/genarrative.conf`。
|
||||
|
||||
## AGC 图集数量与返回次序不表达用途(2026-10-05)
|
||||
|
||||
- 图集请求中的 sliceCount 不影响模型提示词,只会约束服务端后处理;客户端不再暴露或发送它,服务端 API 仍保留现状。四类内容需求不等于四个连通域,也不能按返回次序或历史 player 等文件名分配用途。
|
||||
- 新切片按源资源摘要隔离、以中性序号保存;普通清单记录实际总图路径,美术包回执按实际集合验证。工具返回全部身份与路径标注预览,Agent 看图识别后使用或处理;零片仍交付有效总图与告警。
|
||||
- 动态集合必须同时进入事务 journal、重生成快照、完整性检查和工具投影。旧数量请求需先定位原动作槽并使用原请求体、幂等键和 operation,候选不唯一则对账;不能删掉指纹字段后直接发起新付费请求。完成结果要保留 sliceMode/gridX/gridY,否则重放会丢失服务端回显。
|
||||
|
||||
@@ -500,7 +500,7 @@ App 界面测试中,关闭 Agent 弹窗后的迟到读取用例先等待「刷
|
||||
|
||||
已有持久生成账本的 Provider 待执行动作恢复时,若动作省略了旧视觉 Agent 自动补齐的参数,只在 Agent、动作身份、生成种类和冻结提示词均匹配旧合同后补齐缺省参数;显式参数不得被覆盖。新请求继续按当前自由图片合同执行,不能重新引入固定视觉产物门禁。恢复复用原 operation 与幂等账本,不因默认值变化重复提交已受理请求。
|
||||
|
||||
单 HTML 测试须在项目初始化前准备 HTML;npm 项目的预览与导出测试须准备构建目录。图片测试按现行数量和布局合同验证资源、透明度、引用和持久恢复,不继续要求固定四切片。
|
||||
单 HTML 测试须在项目初始化前准备 HTML;npm 项目的预览与导出测试须准备构建目录。图片测试按现行数量和布局合同验证资源、透明度、引用和持久恢复,不要求固定切片数量。
|
||||
|
||||
本地 Provider smoke 在启动 Agent 前准备已有的 `game/index.html`,按 JSON Generator 的单 HTML 项目合同验证生成、资源引用与浏览器预览。Agent 子进程失败时,诊断必须包含退出码、终止信号及有长度上限的 stderr/stdout 尾部,避免编译 warning 淹没实际错误。定位此阶段失败时单独运行 `npm run ai-game-creator-shell:agent-run:smoke`。
|
||||
|
||||
@@ -1453,7 +1453,7 @@ game-project/
|
||||
- 2026-07-27 补充 tool-plan 成功响应交接的内容边界:Provider 的自然语言计划叙述,以及结构化 arguments 中 `body / code / content / css / html / newText / oldText / patch / script / text` 等源码内容字段,只检查真实密钥 token 形状、凭据头标记和不安全控制字符;仅仅提及 `.env` 或 `game-creator.config` 不能阻断已经计费的安全响应。结构化输入中的敏感 JSON key、非内容字段中的配置痕迹或绝对路径、真实 token、容量、thinking、身份、顺序和账本完整性门禁仍失败关闭。成功 handoff 失败进入 reconciliation 时,Runtime 额外只持久化受控 `failureKind`、脱敏错误 SHA-256 和字符数,不保存 Provider 正文、function arguments、密钥或绝对路径。定向回归覆盖叙述/源码字段放行、`.env.local` 路径和真实 token 拒绝、全部 tool-plan handoff 回归及诊断零正文。
|
||||
- 自然语言 interaction 的 `resume` 只代表“继续当前未完成 Runtime”。宿主在进入 interaction 前已确认当前 Session 没有可 steer、pending 或 running 的 Runtime 时,模型返回的自然语言 `resume` 必须规范化为 `execute`,基于会话历史新建 run;显式恢复入口(`--agent-resume`)仍只执行恢复扫描且无任务时不新建,避免“那就继续修复”被反复吞成空恢复。
|
||||
- `prepared` 发布后,创建阶段锚定的事务目录句柄和 identity 必须由 live rollback 对象一直持有到 `committed` 清理或 rollback 结束,提交和回滚不得按 `PathBuf` 重新接受替换目录;live commit 在发布 `committed` 前后都必须验证 retained handle 仍对应权威 pathname,身份漂移不得降级为提交成功 warning。Unix 清理先把权威叶子通过 no-replace rename 原子隔离为固定 retired 目录,复核 retained inode 后再清空和删除,删除前还需再次复核;进程若在隔离后退出,下次持项目写锁恢复先幂等清理 retired 目录。恢复在首个 canonical 写入前一次性冻结九路径全部前态,晚序普通文件内容变化也必须触发 CAS 冲突并逆序回滚早序安装。历史 `.previous / .replacement` 若没有 durable journal,只允许通过锚定父目录识别后进入 reconciliation;不得凭 pathname 自动 hard-link、move、恢复 canonical 或删除残留。
|
||||
- 图集本地提交以主图 staging 为线性化前置:任何新主图先写随机私有 staging 文件,替换时保留 previous,canonical 主图完整安装后才写四切片、公开清单、私有回执和项目资产登记。进程若在 backup/install 窗口退出,同一 accepted External generation 恢复先识别唯一同 suffix 的 previous/replacement 对并恢复旧主图,再按远端结果完成替换;若 canonical 已等于远端摘要,则不再要求替换授权,直接补齐其余合同。成功后清理主图、四切片、公开清单、私有回执和项目 manifest 的全部遗留 staging/backup。首次生成也禁止直接流式写 canonical 路径,避免部分 PNG 被误认为已安装结果。
|
||||
- 图集本地提交以主图 staging 为线性化前置:任何新主图先写随机私有 staging 文件,替换时保留 previous,canonical 主图完整安装后才写实际切片、公开清单、私有回执和项目资产登记。进程若在 backup/install 窗口退出,同一 accepted External generation 恢复先识别唯一同 suffix 的 previous/replacement 对并恢复旧主图,再按远端结果完成替换;若 canonical 已等于远端摘要,则不再要求替换授权,直接补齐其余合同。成功后清理主图、实际切片、公开清单、私有回执和项目 manifest 的全部遗留 staging/backup。首次生成也禁止直接流式写 canonical 路径,避免部分 PNG 被误认为已安装结果。
|
||||
- ESM 投影中的顶层 function/class/variable 声明必须直接使用 Oxc statement span 提取,不能用首个分号或换行截断箭头函数、多行 initializer 或多 declarator;对象、数组、默认值与 rest 解构声明必须递归收集全部 binding,并保证同一声明只投影一次。import symbol 与 importer 自有 root binding 分开保存,即使名称仅大小写不同也不得在组合前折叠;投影根分配 canonical 名时必须避让 importer 与 origin 的全部非 import semantic binding,不能被嵌套局部捕获。模块组合必须按依赖深度迭代到稳定闭包,把被导出函数继续依赖的 imported origin 带入最终 consumer 单元;循环 ESM 以原始声明 identity 去重回流,不能不断生成重命名副本,并以最终 span replacement 后的单 unit `2 MiB`、累计投影处理 `32 MiB` 为失败关闭上限。固定字符串 dynamic import 同样由下游实际使用的 export 反向驱动加载;未使用 export、未调用嵌套函数和恒假分支中的 dynamic source 不得进入模块单元,同名 dynamic export 不能反向选择未引用的本地声明。只有被选声明中的 `await import` 解构、namespace member 或 `.then(...)` 静态 binding 才能进入组合投影;callback 参数、解构 alias 和 namespace member 必须按 semantic symbol span 改接到投影根,dynamic object shorthand 改名时还必须显式保留原属性键,不能靠追加同名文本跨过局部遮蔽。span replacement 完成后还要对完整组合 unit 重跑 parser 与 semantic,启发式扫描只能在原始源码完成 AST 解析和掩码后再统一小写。匿名 default function / arrow 必须在原始 AST 中以 collision-safe synthetic binding 注册可外调 root span,使 wrapper 内 imported member 的传递依赖继续传播;synthetic binding 必须避开用户真实根名,改名只能更新 default target,不能污染同名命名导出。namespace 经过 renamed re-export 时,同时保留 importer 使用的 member 名和最终 origin export 名:前者定位 importer member span,后者选择 origin declaration,不能混用。named import、namespace import 和 namespace 解构 alias 的成员调用必须保留完整静态成员路径,并按调用 span 排除恒假分支后再把 demand 传播到上游;对象 / class 直接成员、对象解构 alias、实例 alias 与下游 wrapper 都使用同一条可达性链。对象 method shorthand、函数表达式值、箭头函数值以及 class function-valued field 必须按精确函数 span 注册成员根,只加载被实际调用成员中的 dynamic dependency,不能因声明写法遗漏,也不能把属性内未调用的嵌套函数升级为根。
|
||||
- 被选 export root 的直接顶层 assignment 必须与声明一起进入 projection,并沿 RHS semantic dependency 继续闭包,覆盖声明后 live binding 初始化、导出对象成员安装与 class prototype 安装。assignment target 必须解析到相同 root symbol;函数体内、嵌套控制流或其它 root 的写入不随文本同名混入。importer 外部调用被选 export 时,assignment RHS 的 function / arrow、对象成员安装与 prototype 成员安装必须成为对应 root / member 的 projected reachability root,且 class static 与 instance 成员严格分离。全部依赖声明先完成投影,再按原始源码位置输出延后的初始化写入,避免 projection traversal 引入 TDZ;组合结果继续受 canonical 重命名、循环 identity 去重及既有体积门禁约束。
|
||||
- 顶层 object / array destructuring assignment 写入 exported live binding 时,projection 必须按 assignment target 内实际解析到的 root symbol 收集整条写入,并从最终一次写入中精确选取对应对象属性或数组槽位的 function / arrow 作为外部 callable root;更早写入和其它解构槽位不得反向加载 dynamic dependency。
|
||||
@@ -1586,23 +1586,31 @@ game-project/
|
||||
|
||||
## 2026-08-23 Direct Codex 美术包显式重生成与切片投影
|
||||
|
||||
- 客户端图集入口不暴露或发送 `sliceCount`;保留 `sliceMode` 与网格参数。服务端 API、OpenAPI 及服务端数量限制保持现状。四类内容只是生成需求,不能推导四个连通域、切片数量或返回次序对应的语义。
|
||||
- 美术包和普通图集按实际返回的 0–256 张切片保存、登记与返回。有效透明总图没有独立切片时仍完成并返回切片告警;Agent 必须看图确认实体、状态、用途及缺失内容,再决定使用、进一步本地处理或补充生成。已有授权、付费幂等与资产登记规则继续适用,不能虚构切片、坐标或 Canvas 身份。
|
||||
- 新切片使用源图集身份隔离的目录与中性序号,不再生成按 player 等用途命名的别名。普通图集的清单记录实际总图路径并保存在自己的目录;美术包公共清单与私有回执只证明来源、完整性和实际产物集合,不表达语义分类。历史四用途路径仅白名单兼容读取与恢复,文件名不能证明用途。
|
||||
- 本地事务按固定合同文件与本次实际切片集合冻结快照、提交及恢复,保留摘要、真实 alpha、可见像素、唯一身份、项目归属、资源预算和 CAS 校验。重生成的旧快照包含实际旧切片;历史固定集合事务仍可恢复,不重写旧账本或请求体。普通调用升级前携带数量的活动请求必须在新 POST 前被定位并继续使用原身份;多个可能候选时失败关闭,不能任选或重复付费。
|
||||
- 图集完成结果账本保留响应的 `sliceMode/gridX/gridY`,以支持同身份重放。历史账本已丢失切分声明且返回零切片时,声明无法用于证明切片方式;保留有效总图并明确告警,不因此重发生成。存在实际切片时仍要求切分声明一致。
|
||||
- 两个工具都返回总图、完整切片路径、全部资源身份、告警以及有界预览。每张预览明确绑定路径,列出未内嵌预览的路径,Agent 使用现有图片查看能力继续检查。可验证的本地处理产物经现有导入工具登记;游戏语义绑定放在游戏工程中。
|
||||
- 验收覆盖新 HTTP 请求完全省略数量字段、0/2/4/6 张切片、不同图集目录隔离、完整资源投影、旧账本零新增生成 POST、动态集合事务恢复与路径白名单;真实 Provider 视觉质量独立验收。
|
||||
|
||||
- 美术包核心图集的新请求必须把 brief 原文与客户端的素材内容、风格和排布要求共同写入 `iconDescriptions`。内容覆盖当前玩法需要的玩家主体及状态、目标或收集物、障碍或场景元素、反馈特效;各素材独立排布并留出切分间距,沿用规范图的轮廓、材质、色板与光照。描述条数和内容类别不代表切片数量或返回顺序。规范图通过 `referenceId`、比例和尺寸通过正式参数传入;透明结果由服务端纯色底生成与抠图提供,不向模型追加直接生成透明背景的要求。`generationInputs.artSpec` 仅保留描述性上下文,不能作为要求已进入生成提示词的证据。
|
||||
- 美术包工具继续只接收 `brief` 与 `mode`,Agent 在 brief 中提供具体主题、风格、实体和反馈需求;客户端补齐通用要求。普通画布及 `agc_generate_image` 图标入口仍将 trim 后原文作为唯一描述项。美术包新增要求使用独立描述项并校验当前 API 的每条 200 字符、合计 2000 字符 / 6144 UTF-8 字节及 100 条上限,不挤占或截断原文;本次不扩展已有 brief 长度合同。只在没有冻结请求的新提交路径组装要求,已有 `prepared / accepted / legacy-completed` 请求按原请求体、幂等键与操作身份恢复,原 brief 的意图比较不受新增模板影响。验证必须覆盖实际 HTTP 请求、普通入口原文、描述边界及旧请求恢复,不以元数据或提示词文本存在代替传输证据。
|
||||
- 请求要求的自动化证据由 `art_package_spritesheet_posts_requirements_but_ordinary_icons_keep_original_text`、`art_package_spritesheet_recovers_old_requests_without_injecting_new_requirements` 与描述边界用例提供:生产生成入口经过本地 HTTP 夹具,核对真实 POST、原请求字节、幂等键及重复恢复零新增生成 POST。真实 Provider 的视觉效果不由请求夹具替代。
|
||||
- 标准美术包在客户端将规范图、背景图和主图集统一保存为 PNG:下载仍校验来源、声明类型与文件签名,随后按真实内容接受 PNG/JPEG/WebP,在已有 20 MiB、4096 像素单边和 64 MiB 解码内存限制内完整解码。有效 PNG 原样保留,JPEG/WebP 编码成 PNG,最终内容也不得超过 20 MiB;本地媒体类型固定为 `image/png`。转码不补造透明度,主图集与独立切片继续执行真实 alpha、可见像素、尺寸和唯一性合同;平台独立切片仍须为 PNG。此行为仅属于美术包,普通图片工具的指定扩展名合同不变。
|
||||
- 美术包的转码在项目提交锁和本地写入之前完成,文件摘要、已安装结果识别、替换恢复和 manifest 登记均使用最终 PNG。转码失败保留原生成账本与平台身份,同冻结意图重试重新读取已有结果,不提交新的付费生成;不改变请求快照、幂等身份、固定资源路径或旧 PNG 包的复用方式,无数据迁移。验收覆盖 JPEG/WebP 转码、PNG 字节不变、损坏/超限拒绝、真实透明度以及已有结果重复恢复零生成 POST;客户端真实 Provider 的整包验证单独记录。
|
||||
- 转码自动化验收由 `canvas_generation_tests` 的格式/边界用例和 `retained_runtime_generation_retries_a_completed_stage_without_posting_again` 本地 HTTP 夹具覆盖:PNG/JPEG/WebP 均先下载损坏内容,再从同一已完成账本恢复两次,核对最终 PNG、稳定本地 asset ID、Canvas 来源身份、账本保留/清理及零生成 POST;替换与补偿继续由现有图集事务和 Direct 重生成用例覆盖。真实 Provider 的客户端整包效果不由这些夹具替代。
|
||||
- `agc_tools.taonier_prepare_game_art` 的请求模式固定为 `reuse-or-create | regenerate`。缺省使用 `reuse-or-create`,完整且可信的本地包继续零付费复用;只有用户显式要求重做、替换或切换视觉风格时使用 `regenerate`,并绕过完整包短路,按规范图、背景图、透明图集顺序生成和原位替换。`regenerate` 的旧包前置门只要求规范图和背景图已经可下载、可解码、来源一致且存在可信 manifest 登记,使两项旧字节与登记可以完整 rollback;历史主图集、私有回执、公开清单或 canonical 切片可以缺失。客户端必须把八个严格路径的实际存在性和摘要,以及其中受管顶层 asset identity,逐项冻结为 `Present/Some` 或 `Missing/None`,不能把缺失状态伪造成空文件或虚假登记。规范图或背景图任一缺失或身份无效时才失败关闭并提示先用 `reuse-or-create` 修复基础素材。
|
||||
- `agc_tools.taonier_prepare_game_art` 的请求模式固定为 `reuse-or-create | regenerate`。缺省使用 `reuse-or-create`,完整且可信的本地包继续零付费复用;只有用户显式要求重做、替换或切换视觉风格时使用 `regenerate`,并绕过完整包短路,按规范图、背景图、透明图集顺序生成和原位替换。`regenerate` 的旧包前置门只要求规范图和背景图已经可下载、可解码、来源一致且存在可信 manifest 登记,使两项旧字节与登记可以完整 rollback;历史主图集、私有回执、公开清单或 canonical 切片可以缺失。客户端必须把固定合同文件与实际旧切片的存在性和摘要,以及其中受管顶层 asset identity,逐项冻结为 `Present/Some` 或 `Missing/None`,不能把缺失状态伪造成空文件或虚假登记。规范图或背景图任一缺失或身份无效时才失败关闭并提示先用 `reuse-or-create` 修复基础素材。
|
||||
- 显式重生成不放宽 External Editor 幂等与未知态边界。固定阶段已有 `prepared / accepted` 账本时,本次生成 prompt 必须与账本冻结 prompt 一致才可恢复;不一致返回 `platform-generation-result-unknown` 并保留原 `Idempotency-Key / operationId` 对账,禁止把旧结果解释为新意图,也禁止另起付费 POST。
|
||||
- 整包重生成在首个付费阶段前建立客户端私有 v4 workflow,状态固定为 `resetting / in-progress / compensating / completed`,并同时绑定意图摘要和客户端稳定 `clientTurnId`。专用 `direct-codex-art` 跨进程执行锁覆盖整个付费重生成生命周期,但不持有通用项目写锁等待网络。规范图和背景图替换后立即持久化旧字节、旧 manifest entry 与本轮双 CAS 锚点;任一后续阶段失败时进入 `compensating`,可在进程重启后继续恢复旧文件及旧登记。已成功阶段的生成账本继续保留;`completed` 持久化经脱敏和数量 / 长度限制的完整工具结果,同一 `clientTurnId` 回包丢失时必须等值重放且零新 POST。新的显式用户回合先持久化目标回合所有的 `resetting` workflow,再清理上一轮三阶段账本并转回 `in-progress`,任一崩溃点都不得出现无 workflow 窗口。只有尚无任何阶段账本且无替换锚点的孤立 `in-progress` 空壳允许被新回合原子接管;其余身份冲突、未知版本以及缺少新恢复字段的旧 v2/v3 workflow 均失败关闭,不能用 serde 缺省值把旧状态升级成可执行状态。
|
||||
- 恢复扫描必须把 `resetting`、`compensating` 和仍带替换锚点的 `in-progress` 识别为可恢复状态,并在 Direct app-server 启动前持有同一专用执行锁完成阶段清理、补偿和中性化。补偿只恢复旧文件并清除本地 replacement CAS 锚点;已 `prepared / accepted` 的阶段账本、原 `Idempotency-Key` 与 `operationId` 必须保留,同冻结意图续跑复用原请求身份,未知账本在文件 mutation 前失败关闭。冻结意图一致但进程 invocation 已变化时允许安全接管本轮;`completed` 则以外层原始 `clientTurnId` 为权威,忽略模型重采样 brief 并等值回放。客户端必须在启动 Direct Codex 前幂等落盘原始 User 消息与稳定回合 ID;最终 assistant 回复必须在 Tauri 成功返回和 `completed` 事件前,以同一稳定回合 ID 幂等写入项目主对话,重启后项目对话只续跑真正未回答的原始回合,不能生成新身份或重复应用已完成代码修改。
|
||||
- workflow 在调用严格图集事务前必须先持久化 `strictSpritesheetPending`,并冻结严格事务覆盖的九项旧合同身份:`.agent/manifest.json` 中受管 asset identity、客户端私有回执、公开 `assets/manifest.art.json`、主图集、四张 canonical 切片和公开切片清单;旧路径允许按真实状态冻结为缺失。异步 Provider 返回终态后,客户端必须先把脱敏且可恢复的完成结果绑定到原 retained stage ledger,再允许本地严格事务提交。恢复在同一项目写锁内完成底层严格事务对账与 workflow CAS;若九项新合同与当前规范图身份完整一致、规范图/背景图替换锚点属于本轮,且私有回执的 resource/asset/task identity 与本轮 retained spritesheet 完成结果一致,才保留整组新结果并补写 `completed`。若九项仍逐项精确等于冻结的旧合同,严格合同判定、写入 `compensating`、恢复规范图/背景图与登记、回读验证和清除锚点必须全部位于同一项目锁内;`compensating` 重启也必须重新验证旧合同。任一文件存在性、摘要、顶层 asset identity、retained result 或 CAS 处于第三种状态时进入本地 reconciliation,保留 workflow、阶段账本和文件现场,禁止制造新旧混合包或重新付费。恢复若只能证明完整新合同而无法重建中断前尚未持久化的阶段告警,完成结果必须追加明确恢复告警,不能用空 warning 集合伪装为原阶段没有告警。
|
||||
- 工具完成结果同时返回主包 `assetPaths`、实际成功持久化的 `slicePaths`、安全身份投影 `resources`,并把普通 `warnings` 与 `sliceWarnings` 分开。每张本地切片都以真实 Canvas `resourceId / assetObjectId / taskId` 和源图集 `sourceResourceId` 登记为顶层 manifest asset;同路径替换保留本地 asset ID。严格图集事务继续覆盖主图、四张 canonical 切片、公开切片清单、私有回执和 `.agent/manifest.json`,失败时整组恢复。旧项目缺顶层切片登记时只能由客户端私有回执授权补登记;可编辑的公开切片清单不能单独成为 `.agent` Canvas 身份来源。
|
||||
- workflow 在调用严格图集事务前必须先持久化 `strictSpritesheetPending`,并冻结严格事务覆盖的旧合同身份:`.agent/manifest.json` 中受管 asset identity、客户端私有回执、公开 `assets/manifest.art.json`、主图集、实际切片和公开切片清单;旧路径允许按真实状态冻结为缺失。异步 Provider 返回终态后,客户端必须先把脱敏且可恢复的完成结果绑定到原 retained stage ledger,再允许本地严格事务提交。恢复在同一项目写锁内完成底层严格事务对账与 workflow CAS;若完整新合同与当前规范图身份完整一致、规范图/背景图替换锚点属于本轮,且私有回执的 resource/asset/task identity 与本轮 retained spritesheet 完成结果一致,才保留整组新结果并补写 `completed`。若旧合同仍逐项精确等于冻结的旧合同,严格合同判定、写入 `compensating`、恢复规范图/背景图与登记、回读验证和清除锚点必须全部位于同一项目锁内;`compensating` 重启也必须重新验证旧合同。任一文件存在性、摘要、顶层 asset identity、retained result 或 CAS 处于第三种状态时进入本地 reconciliation,保留 workflow、阶段账本和文件现场,禁止制造新旧混合包或重新付费。恢复若只能证明完整新合同而无法重建中断前尚未持久化的阶段告警,完成结果必须追加明确恢复告警,不能用空 warning 集合伪装为原阶段没有告警。
|
||||
- 工具完成结果同时返回主包 `assetPaths`、实际成功持久化的 `slicePaths`、安全身份投影 `resources`,并把普通 `warnings` 与 `sliceWarnings` 分开。每张本地切片都以真实 Canvas `resourceId / assetObjectId / taskId` 和源图集 `sourceResourceId` 登记为顶层 manifest asset;同路径替换保留本地 asset ID。严格图集事务继续覆盖主图、实际切片、公开切片清单、私有回执和 `.agent/manifest.json`,失败时整组恢复。旧项目缺顶层切片登记时只能由客户端私有回执授权补登记;可编辑的公开切片清单不能单独成为 `.agent` Canvas 身份来源。
|
||||
- `regenerate` 的模式选择遵循 2026-09-03 MCP 能力边界:Codex 根据当前用户请求,经审核后的工具显式选择 `mode=regenerate`;客户端不再通过自然语言关键词、Unicode 归一化、否定词表或独立确认句式判断高层业务意图。工具桥继续校验项目权限,将操作绑定活动客户端回合与稳定 `clientTurnId`、冻结首次 `brief` 摘要,串行处理同一重生成动作,并在同回合等值重试时返回已完成结果;缺少活动回合或摘要冲突仍拒绝。账号、计费、幂等账本、锁、付费结果未知与恢复合同继续有效。2026-09-23 已删除无调用的旧文本判断函数,不恢复该旧语义门禁。同一进程重复水合相同 `clientTurnId` 时,“回合仍在运行”只属于瞬时占用状态,前端不得以稳定 assistant messageId 将其写成终态;原执行的成功回复仍由 Tauri 在返回前持久化。DirectProject 的 cwd 和 AGC 项目身份根使用用户选择的 canonical 项目根;其原生 OS 路径字节与权威 manifest `projectId` 通过域标签和独立长度前缀编码后绑定连接池及 thread 身份,项目被替换时不能复用旧连接。进程 sandbox 与文件、命令、权限请求的批准规则按本文件后续“DirectProject Codex 完整访问覆盖”;客户端 MCP 仍保持项目绑定和业务权限校验。`resources` 只返回本地 asset/path/kind/media type、Canvas project/resource/asset/task ID 与 reference resource IDs,不返回 prompt、model、provider route、绝对路径、URL、Token、Cookie 或 API Key。客户端付费资源生成(图片、视频、角色动画、音效、背景音乐)统一调用站内 `/api/editor/...` 路由并复用平台登录态,不走 External v1;External v1 只保留给外部开发者模式和历史账本重放兼容。
|
||||
- 成功响应中的 `warnings / sliceWarnings` 与错误响应采用同一脱敏边界:逐条移除宿主绝对路径、凭据与 URL,并设置固定长度上限;非阻断告警不成为绕开错误分支隐私保护的旁路。
|
||||
- Direct 同进程重复水合若收到“同一 stable turn 仍在运行”,必须释放当前 App 实例的恢复 claim;该结果不落 assistant 终态,后续显式刷新对话可按原 `clientTurnId` 再次读取已落盘回复或续跑,不要求重载整个 WebView,也不启动无界自动轮询。
|
||||
- 对话恢复从新到旧扫描全部合法 Direct User 回合;较新的 User 已有稳定 assistant 时必须继续寻找更早未回答回合,不能提前结束扫描。普通成功回复或普通错误回复若终态 assistant 持久化失败,同样必须释放当前 App 实例的恢复 claim,使后续显式重新加载对话时能以原稳定 `clientTurnId` 重试;claim 只表示当前实例内正在恢复,不能成为磁盘终态的替代品。
|
||||
- Direct 的运行态素材验收不再把 `assets/art-spec.png` 当作背景、角色、道具或反馈;规范图只作为派生 reference。标准核心图集无论首次创建还是显式重生成,都必须原子取得恰好四张 canonical 独立切片后才算本次生成成功;每张切片必须有真实 alpha、可见像素、唯一规范像素内容及唯一 Canvas `resourceId / assetObjectId`。旧项目只在私有回执与公开清单、当前源图和顶层登记完全一致时投影四条 `slicePaths`;部分、opaque、重复或缺回执状态只返回 warning,不得猜测或伪造衍生素材。
|
||||
- Direct 的运行态素材验收不再把 `assets/art-spec.png` 当作背景、角色、道具或反馈;规范图只作为派生 reference。标准核心图集无论首次创建还是显式重生成,都按实际产物原子提交,零切片也保留有效总图和告警;每张切片必须有真实 alpha、可见像素、唯一规范像素内容及唯一 Canvas `resourceId / assetObjectId`。旧项目只在私有回执与公开清单、当前源图和顶层登记完全一致时投影实际 `slicePaths`;部分、opaque、重复或缺回执状态只返回 warning,不得猜测或伪造衍生素材。
|
||||
- 机器门只证明 PNG、真实 alpha、非空可见像素、切片像素唯一、稳定平台身份、顶层登记及源码/双视口实际渲染。背景是否混入实体、地面是否无缝、素材语义是否匹配、最终绘制尺寸是否满足玩法仍由 Codex 检查工具图片和 desktop/mobile 试玩截图;prompt 约束本身不算通过证据。
|
||||
|
||||
## DirectProject 工具权限现行覆盖(2026-08-24)
|
||||
|
||||
Reference in New Issue
Block a user