修正美术包 API 指南、AGC 调用与切片用途判定 (#628)
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (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

AGC 美术包此前把内容、风格和排布要求放在未被提示词消费的元数据中,并要求恰好四张切片、按返回顺序分配用途。本 PR 完成以下三项修正:

- [x] API 文档:修正整个 `generationInputs` 的定位、保存与读取方式、接口保留/重建规则及已知消费者。任意元数据不会自动进入提示词,服务端行为和校验约束保持不变。
- [x] AGC 请求:美术包将原 brief 和内容、风格、排布要求写入实际消费的 `iconDescriptions`;普通图标入口继续原文单项透传。参考图、比例和尺寸使用正式参数,同步工具说明与随包 skill。
- [x] 客户端切片处理:美术包和直接生图工具不再暴露或发送 `sliceCount`,取消固定四片和按序赋义;按实际产物保存、登记和返回。切片目录按源图集隔离,文件名使用中性序号,普通清单指向真实总图。

四类素材需求只表达内容覆盖,不能证明四个连通区域或固定语义顺序。两个工具返回总图、完整切片路径及资源身份,预览逐项标注路径,未内嵌的图片列出路径供 Agent 查看。Agent 看图识别实体、状态和用途后再接入或处理;有效总图零切片时仍交付并保留告警。

本轮切片改造仅限客户端:服务端 API、请求侧 sliceCount 契约、切分算法和计费行为不变。不新增为数量不符找回、补切或凑数的工作流。保留文件完整性、平台身份、资源预算、事务与重生成恢复;兼容历史四用途产物和旧数量参数账本,旧请求正文、幂等键及 operation 不改写,候选身份不唯一时要求对账。完成结果快照补存切分声明,避免重放丢失响应字段。

验证:

- API 文档阶段:MCP 25 项、External editor API 30 项测试及 OpenAPI 约束等价检查通过。
- 本轮:317 项定向 Rust 测试通过(生成 118、Direct 运行时 93、工具桥 44、MCP 32、账本 6、提示词 24)。覆盖新请求完全省略字段、0/2/4/6 片、目录隔离、完整资源投影、动态事务回滚、旧请求和旧输出槽恢复;已受理任务恢复不新增生成 POST。
- cargo check、rustfmt、skill-pack:check、文档索引、变更文件编码及 git diff --check 通过。
- 未调用真实付费 Provider,未做客户端视觉试玩;PR 保持草稿,等待视觉及产品验收。

Closes #525

Reviewed-on: #628
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #628.
This commit is contained in:
2026-10-07 19:35:20 +08:00
committed by 孔令弘
parent 9cd290362c
commit 27e385785f
24 changed files with 2265 additions and 734 deletions
@@ -16,6 +16,7 @@ Connect to `https://www.genarrative.world/api/external/v1/mcp` using Streamable
- Upload local references using `prepare_asset_upload`: request a ticket, transfer the file from the client, then confirm the object. Confirmation does not create a canvas layer or a project/library record. Use the reference type accepted by the target tool; some operations require a registered resource or asset ID rather than an object key.
- Generation is paid and asynchronous. Keep one stable `idempotencyKey` per logical generation and retain the returned `operationId`. Call `check_generation` according to `pollAfterMs`; consume `result` only after `completed`, and report the safe error on `failed`. A polling timeout does not justify another generation.
- Read actual artifacts and warnings before claiming the requested deliverable is complete. Use project/library reads for complete persisted records, and `find_assets` with `action=get_download_url` for temporary media access.
- Treat `generationInputs` as generation context and provenance metadata, subject to each endpoint's preservation and rebuilding rules. Arbitrary fields, including `artSpec`, do not automatically enter the provider prompt or override request parameters. Put generation requirements in the endpoint's explicit inputs; see [Generation Inputs Metadata](references/requests-and-outputs.md#generation-inputs-metadata) for persistence, reads, and known consumers.
- Keep API Keys and temporary upload/download credentials out of chat, repository files, and logs. Business calls operate within the API Key's owner and scopes.
## Documentation Navigation
@@ -20,7 +20,7 @@ For generation, pass `projectId` with `canvasCompletion` when the result should
## Art Spec Routing
For a series of related art requests, an optional reusable spec can carry the shared requirements:
For a series of related art requests, an optional caller-defined spec can record the shared requirements. `artSpec` is an organizational convention inside `generationInputs`, not a server-defined generation parameter schema:
```json
{
@@ -35,7 +35,7 @@ For a series of related art requests, an optional reusable spec can carry the sh
}
```
Infer what is already clear and ask only for missing fields that block the selected endpoint. Reuse the current spec unless the user changes style, subject family, palette, format, or constraints. Store structured context under `generationInputs.artSpec` where supported and summarize it in the prompt when useful.
Infer what is already clear and ask only for missing fields that block the selected endpoint. Reuse the current spec unless the user changes style, subject family, palette, format, or constraints. Where the endpoint preserves custom metadata, `generationInputs.artSpec` can retain this context for later retrieval. To affect generation, always translate the relevant requirements into the endpoint's explicit inputs: image `prompt`, scene `sceneContent` / `stylePreset` / `customStyle`, or spritesheet `iconDescriptions`, plus the actual size and reference parameters. Neither `artSpec.references` nor `generationInputs.references` supplies reference media by itself. Scene and sound-effect generation rebuild their metadata and do not preserve an arbitrary `artSpec`; keep a caller-side copy when needed. See [Generation Inputs Metadata](requests-and-outputs.md#generation-inputs-metadata) for the rules applying to the entire `generationInputs` field.
## Intent Map
@@ -9,6 +9,7 @@ Use this reference to build generation payloads, carry canvas/library context, p
- [Polling State Machine](#polling-state-machine)
- [Canvas and Asset-Library Completion](#canvas-and-asset-library-completion)
- [Saving Existing Canvas Layout](#saving-existing-canvas-layout)
- [Generation Inputs Metadata](#generation-inputs-metadata)
- [Art Spec and Image Request](#art-spec-and-image-request)
- [Local Reference Requests](#local-reference-requests)
- [Compact Completed Result](#compact-completed-result)
@@ -135,7 +136,7 @@ Background removal preserves the source image dimensions. For normal canvas plac
Character animation accepts `assetFolderId` and `assetLabel` and persists the generated sequence. Consume the returned animation artifacts and persisted identities; do not synthesize a duplicate animation asset from the first frame. Use complete project/library records when complete persisted state is needed.
For the lower-level asset/resource creation endpoints, `generationInputs` is replayable request context rather than a media-runtime container. When `assetKind` is `character-animation`, the server rejects legacy runtime keys including `characterAnimation`, `frames`, `previewVideoPath`, `frameCount`, `fps`, and `durationSeconds`; send the formal sequence through `imageSequenceFrames` and `imageSequenceDurationMs`. Internal processing audit keys such as `screenColorHex`, `mattingProvider`, and `mattingModel` are removed before persistence.
For the lower-level asset/resource creation endpoints, `generationInputs` follows the metadata and media-runtime boundaries described in [Generation Inputs Metadata](#generation-inputs-metadata).
## Saving Existing Canvas Layout
@@ -146,15 +147,46 @@ For the lower-level asset/resource creation endpoints, `generationInputs` is rep
`edit_canvas/register_resource` registers existing media but does not create a canvas layer. `organize_asset_library/create_asset` creates metadata but does not upload or generate media. For generated media placement, prefer the generation tool's supported `canvasCompletion`; inspect returned identities before registering anything again.
## Generation Inputs Metadata
`generationInputs` is optional JSON generation context: an input snapshot, provenance, and supported application metadata. An object is the useful shape for named fields; accepting `JsonValue` does not promise lossless storage of every JSON value. The selected endpoint may sanitize, augment, or rebuild it. Arbitrary metadata is not automatically included in the provider prompt, used as generation parameters, or applied to a later request. Put requirements into the endpoint's explicit inputs, such as `prompt`, `iconDescriptions`, `sceneContent`, style/size options, and its actual reference-media fields.
When an operation persists project resources or library assets, their accepted metadata is saved with those records. Retrieve it through `GET /api/external/v1/editor/projects/{projectId}` (`project.resources[].generationInputs`) or `GET /api/external/v1/editor/assets/library` (`library.assets[].generationInputs`), subject to owner/scopes and record existence. MCP equivalents are `find_assets/get_project_resources` and `find_assets/list_library`. A compact generation result is not a complete metadata read. Metadata is not embedded in the image bytes, and uploading an image does not restore a previous record's metadata.
| Operation | Current handling of `generationInputs` |
| --- | --- |
| Create a project resource or library asset | Preserve accepted metadata after removing client-supplied `references` and internal audit keys. This registers a record; it does not execute a generation recipe. |
| Generate an image | Preserve custom object fields on the provider's original image record after sanitization; rebuild `references` from actual authorized reference inputs. Character transparency processing may create a separate derived record. |
| Generate a scene | Rebuild V2 `version`, `action=scene.generate`, `fields`, and `references` from normalized scene parameters. Only the exact client marker `source=ai-game-creator-client` is retained additionally; arbitrary custom fields such as `artSpec` are discarded. |
| Edit an image | Preserve accepted custom fields and rebuild source/auxiliary `references` from `sourceReferenceId` and the actual reference inputs. |
| Remove a background | Preserve accepted custom fields and rebuild `references` from the actual source. Processing audit metadata remains internal. This metadata does not configure the removal operation. |
| Generate an icon spritesheet or extract UI assets | Preserve accepted custom fields on the provider's original image record. Transparent sheets and slices have separate processing-stage/source metadata and do not automatically inherit all custom fields. Follow the returned source references to read the original context. |
| Generate a character animation | Preserve accepted generation context on the final sequence record; formal frames and sequence duration are separate media fields, not runtime data inside `generationInputs`. |
| Generate a video | Preserve accepted context; object metadata can also receive an added/updated duration display field from normalized request parameters. |
| Generate a sound effect | Rebuild `fields`, empty `references`, and `soundEffect` metadata from the actual generation. Only `source`, `conversationId`, and `toolCallMessageId` are copied from a caller-supplied object; arbitrary fields such as `artSpec` are discarded. |
| Generate background music | Preserve accepted context after sanitization; generation parameters come from the explicit request fields. |
Preservation does not mean every field is inert. Known consumers include:
- The canvas reads `fields` / `references` for input display. Recognized V2 `version`, `action`, and stable field/reference IDs support restoring supported generation panels; arbitrary metadata does not guarantee a UI display or a “modify” action.
- Icon spritesheet generation reads the saved reference spec's `fields` entry titled `游戏类型` to select genre-specific prompt text. This does not cause arbitrary fields in the current request to be interpreted as prompts.
- Integrated clients use recognized `source` markers for queue/idempotency namespaces and result projections. These are application markers, not authentication or model instructions.
Reference metadata does not grant access or select reference media. Image operations rebuild it from actual inputs and authorized records; direct resource/asset creation drops caller-supplied references. Supply the documented `referenceId`, `sourceReferenceId`, or media-reference fields. Do not assume other operations provide the same provenance rebuilding.
Persisted metadata is bounded to 64 KiB of serialized JSON and cannot contain inline media Data URLs. Top-level internal audit fields `screenColorHex`, `mattingProvider`, and `mattingModel` are stripped from client metadata and owner-facing reads; the server can store its own internal audit values. When `assetKind=character-animation`, legacy runtime keys such as `characterAnimation`, `frames`, `previewVideoPath`, `frameCount`, `fps`, and `durationSeconds` are rejected; use `imageSequenceFrames` and `imageSequenceDurationMs` for formal sequence data. Omitting metadata or sending null does not replace required request parameters; empty objects may normalize to null.
For reuse, read the saved record, recover the relevant requirements, and explicitly construct the next request. Keep the original complete request and idempotency key for retries: stored metadata, especially derived-asset metadata, is not a complete replayable HTTP payload.
## Art Spec and Image Request
Game scenes have a dedicated structured route: `POST /api/external/v1/editor/scenes/generations` with `sceneContent` and `stylePreset` (`customStyle` required when `stylePreset` is `custom`). The server assembles the full provider prompt; a caller-assembled `prompt` is not accepted. `kind: "scene"` and `assetKind: "scene"` remain invalid on generic image generation and return HTTP `400` before any generation job is queued.
When maintaining a reusable art spec, carry it in `generationInputs.artSpec` and reflect important constraints in the prompt. This is an example with both canvas and library destinations, not a requirement for every generation:
`generationInputs.artSpec` is an optional caller-defined metadata convention with no automatic prompt or parameter effect. In the generic image request below, the prompt repeats the desired style, palette, composition, and exclusions, while `aspectRatio` and `imageSize` set the actual format. The saved spec can help a caller construct later requests. This example uses both canvas and library destinations; neither the spec nor both destinations are required for every generation. Do not copy this metadata expectation to the scene route, which rebuilds its own context.
```json
{
"prompt": "一张横版幻想森林背景,适合游戏主视觉,无文字",
"prompt": "一张横版幻想森林背景,适合游戏主视觉,手绘游戏概念图风格,翡翠绿与金色光斑,中心留出角色站位,无文字、无 UI 按钮",
"aspectRatio": "16:9",
"imageSize": "1K",
"projectId": "<projectId>",
@@ -11,18 +11,17 @@
"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.parameters.brief": "面向当前游戏的简洁视觉需求",
"taonier_prepare_game_art.description": "创建或恢复当前 AGC 项目的陶泥儿标准游戏美术包,返回总图、实际切片与路径标注预览;需看图识别用途,内容要求不保证切片数量或顺序。默认复用有效美术包;根据当前对话需要选择 regenerate 重新生成。授权使用 AGC 客户端当前登录会话;遇到 401/403 时报告客户端登录或权限状态异常并停止。",
"taonier_prepare_game_art.parameters.brief": "面向当前游戏的简洁视觉需求,不超过 200 字符。写明主题、风格、玩家主体及状态、目标或收集物、障碍或场景元素、反馈特效等具体需要的素材。需求明确时列出各项素材的数量、状态,并要求独立排布、留出切分间距;数量未确定时不编造。工具会补齐沿用规范图和素材独立排布的通用要求;数量是生成目标,内容类别和数量均不保证实际切片数量或返回顺序,须看图确认用途",
"taonier_prepare_game_art.parameters.mode": "缺省安全复用有效美术包;Codex 仅在当前对话需要换一套或重新生成时使用 regenerate",
"agc_generate_image.description": "生成一张新图片:普通插画、角色立绘、统一视觉规范图、游戏 UI 设计图或透明游戏素材图集。仅在用户明确要求生成新图时调用。",
"agc_generate_image.parameters.prompt": "完整图片描述;普通图片、角色、规范图、UI 设计图或透明图集均可。kind=icon-spritesheet 时,去除首尾空白后的描述须为 1 到 200 个 Unicode 字符,保留内部换行并作为单条 iconDescriptions 原样提交;超限拒绝,不截断、不拆条,客户端不追加生图指令",
"agc_generate_image.parameters.prompt": "完整图片描述;普通图片、角色、规范图、UI 设计图或透明图集均可。kind=icon-spritesheet 时,需求明确则列出各项素材的数量、状态,并要求独立排布、留出切分间距;数量未确定时不编造。数量是生成目标,不保证实际切片数量或返回顺序,须看图确认用途。去除首尾空白后的图集描述须为 1 到 200 个 Unicode 字符,保留内部换行并作为单条 iconDescriptions 原样提交;超限拒绝,不截断、不拆条,客户端不追加生图指令",
"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",
@@ -6,7 +6,7 @@
"playtest.tetris": "完成合同要求 tetris-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,以及 gameplay={kind:'tetris',activePieceId,rotation,row,lockedPieces,lineClearChecks,clearedLines,occupiedCells};gameplay 可包含额外 telemetry 字段,但这些字段不能替代固定必填字段。界面必须提供 data-playtest-id=\"start\"、data-playtest-id=\"primary-action\" 与 data-playtest-id=\"restart\" 的真实控件;每个固定 data-playtest-id 在对应受控试玩步骤都必须恰好匹配一个可见且启用(disabled=false)的真实可点击 HTMLElement,同一固定值不得出现在多个控件上。primary-action 必须同步旋转同一 activePieceId,不能只推进 sequence、替换活动方块或更新装饰状态;start 后 2 秒观察内必须出现同一 activePieceId 的 row 下落或真实锁定;primary-action 后 3 秒内必须真实锁定方块,锁定后 activePieceId 必须变化、lockedPieces 恰好增加 1、lineClearChecks 推进,且 occupiedCells 与 clearedLines 必须体现新增方块或实际消行,不能只增加计数;restart 后 occupiedCells、lockedPieces、lineClearChecks 与 clearedLines 必须全部归零。初始状态必须是 ready 且 level 为正整数;start 后状态必须推进并进入 playing,并至少持续 2 秒保持 playing。primary-action 必须同步推进 sequence 与 rotation;动作后 phase 可为 playing、won 或 lost,若保持 playing 则继续观察最多 3 秒以取得锁定和消行检查证据。restart 后必须推进 sequence、恢复 ready 或 playing,并持续 3 秒稳定观察。全部观察期间 sequence 不得回退;若首轮进入 lost,必须按 generic-v1 的重开重试合同证明第二次能进入 won 或保持 playing,不能固定失败。",
"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.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。",
@@ -15,8 +15,8 @@
"scene_constraints": "必须是可见的真实图片产物,适合作为 Canvas 或 HTML 游戏场景底图;不得做成素材图集、完整游戏截图、海报或概念板;必须原创,不得复刻现有游戏场景、Logo、贴图、标志性布局或受保护视觉语言",
"spritesheet_subject": "{};使用项目原创命名和原创阵营设计",
"spritesheet_style": "清晰可切分的原创 Web 游戏素材图集;严格沿用当前规范图的轮廓、材质、色板与光照,素材类别以当前玩法合同为准",
"spritesheet_composition": "{} 游戏素材图集,按玩家主体及状态、目标或收集物、障碍或场景元素、反馈特效分区,留出清楚切分间距",
"spritesheet_constraints": "生成可直接用于游戏的真实透明图片,素材仅包含当前项目玩法合同需要的实体和反馈;角色轮廓、图标排布与配色采用项目原创设计",
"spritesheet_composition": "{} 游戏素材图集,覆盖当前玩法需要的玩家主体及状态、目标或收集物、障碍或场景元素、反馈特效;每类可含多个素材,各素材独立排布并留出清楚切分间距",
"spritesheet_constraints": "素材仅包含当前项目玩法合同需要的实体和反馈,可独立用于游戏;角色轮廓、图标排布与配色采用项目原创设计",
"image_generation": "根据用户需求生成一张全新的原创图片。主体、环境、风格、构图、光线和色彩以用户描述为准;不要生成素材图集、规范展板、完整游戏截图或文字说明。不得修改或复述为已有图片编辑。\n\n用户需求:{}",
"character_generation": "根据用户需求生成一张全新的原创角色形象或人物立绘。清楚表现角色外貌、服饰、姿势、表情、画风、构图和背景;只生成一张完整图片,不要生成图集、规范展板、完整游戏截图或文字说明。不得复刻现有作品角色或 Logo。\n\n用户需求:{}",
"publication_generation": "根据用户需求生成一张全新的原创游戏发布宣传图。突出主体、卖点、氛围、构图、色彩和适合发布展示的画面层次;只生成一张完整图片,不要生成素材图集、规范展板、完整游戏截图或文字说明。不得复刻现有作品角色或 Logo。\n\n用户需求:{}",
@@ -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.74",
"version": "2026-08-26.75",
"skills": [
{
"name": "agc-unity-editor",
@@ -101,7 +101,7 @@
"agents/openai.yaml",
"references/platform-art-contract.md"
],
"sha256": "dcf243784b7279fc4819140d746e607bde840a53bc419172d615c53364a2ecb4"
"sha256": "c0e4dc4ff9d23ed0c4832f9dc5e72926db2aee31318d807d2c35a0fd4e38f140"
},
{
"name": "agc-web-game-development",
@@ -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,12 +29,27 @@ 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.
For `taonier_prepare_game_art`, provide `brief` and optionally `mode`. Name the
game's theme, style, and needed player states, targets or collectibles,
obstacles or scene elements, and feedback effects in `brief`. The tool adds
shared requirements for visual-spec consistency and separated asset placement.
These content categories do not specify a slice count or returned ordering;
inspect the actual images before deciding how to use them.
For both a spritesheet `prompt` and a package `brief`, when the requirements
specify quantities, list each needed asset's quantity and state, and request
separate placement with clear spacing. For example: one idle player, one hit
player, one coin, and two obstacle variants, all separated. Do not invent
quantities when they are unspecified. Requested quantities guide generation;
they do not guarantee the actual slice count or semantic return order. Inspect
the returned images to verify the assets and their uses.
## Authorization boundary
If the tool returns `401` or `403`, stop the operation and report that login or permission state needs attention.
@@ -44,7 +59,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.
@@ -1,24 +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`. In both package `brief` and ordinary spritesheet `prompt`, include per-asset quantities and states when specified by the requirements, and request separate placement with clear spacing; do not invent unspecified quantities. These are generation targets, not guarantees of actual slice count or semantic order; inspect the returned images. 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(),
File diff suppressed because it is too large Load Diff
@@ -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,
))
}
@@ -4707,79 +4698,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]
@@ -6415,6 +6371,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 {
@@ -387,12 +387,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")
@@ -3090,14 +3084,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
@@ -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!(
@@ -4168,7 +4168,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)
@@ -4281,7 +4280,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);
}
}
@@ -2306,8 +2306,7 @@
"description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue",
"description": "可重放的生成输入。assetKind=character-animation 时不得包含 characterAnimation、frames、previewVideoPath、frameCount、fps 或 durationSeconds 等旧运行字段;正式媒体数据必须写入 imageSequenceFrames 和 imageSequenceDurationMs。服务端会移除 screenColorHex、mattingProvider、mattingModel 等内部处理审计字段。"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。本接口保存经清理的元数据,删除调用方 references 及顶层 screenColorHex、mattingProvider、mattingModel;不会执行生成配方。序列化后最多 64 KiB,禁止内联媒体 Data URL。assetKind=character-animation 时不得包含 characterAnimation、frames、previewVideoPath、frameCount、fps 或 durationSeconds 等旧运行字段;正式媒体数据使用 imageSequenceFrames 和 imageSequenceDurationMs。可通过素材库读取保存值。"
}
},
"additionalProperties": false
@@ -2404,8 +2403,7 @@
"description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue",
"description": "可重放的生成输入。assetKind=character-animation 时不得包含 characterAnimation、frames、previewVideoPath、frameCount、fps 或 durationSeconds 等旧运行字段;正式媒体数据必须写入 imageSequenceFrames 和 imageSequenceDurationMs。服务端会移除 screenColorHex、mattingProvider、mattingModel 等内部处理审计字段。"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。本接口保存经清理的元数据,删除调用方 references 及顶层 screenColorHex、mattingProvider、mattingModel;不会执行生成配方。序列化后最多 64 KiB,禁止内联媒体 Data URL。assetKind=character-animation 时不得包含 characterAnimation、frames、previewVideoPath、frameCount、fps 或 durationSeconds 等旧运行字段;正式媒体数据使用 imageSequenceFrames 和 imageSequenceDurationMs。可通过项目详情读取保存值。"
}
},
"additionalProperties": false
@@ -2797,7 +2795,7 @@
"description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "实际保存的生成上下文与来源元数据,接受任意 JSON 值,也可能为空;不是完整请求或自动执行指令。已按 owner 读取边界移除内联媒体与顶层内部审计字段;生成接口可能清理、补充或重建请求值,派生记录不保证继承原图自定义字段。fields/references 可供显示,识别的 V2 action/字段 ID 可供支持的面板恢复;参考规范图的“游戏类型”等已知字段也有后续消费者,不能视为全部无业务作用。任意 artSpec 等扩展字段不保证 UI 展示或自动复用。复用时由调用方将所需信息显式转换成新请求参数。通过项目详情的 project.resources 读取。"
},
"createdAt": {
"type": "string",
@@ -2948,7 +2946,7 @@
"description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "实际保存的生成上下文与来源元数据,接受任意 JSON 值,也可能为空;不是完整请求或自动执行指令。已按 owner 读取边界移除内联媒体与顶层内部审计字段;生成接口可能清理、补充或重建请求值,派生记录不保证继承原图自定义字段。fields/references 可供显示,识别的 V2 action/字段 ID 可供支持的面板恢复;参考规范图的“游戏类型”等已知字段也有后续消费者,不能视为全部无业务作用。任意 artSpec 等扩展字段不保证 UI 展示或自动复用。复用时由调用方将所需信息显式转换成新请求参数。通过素材库的 library.assets 读取。"
},
"createdAt": {
"type": "string",
@@ -3195,8 +3193,7 @@
"type": ["string", "null"]
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue",
"description": "场景配方由服务端重建;仅保留 source 精确等于 ai-game-creator-client 的客户端来源标记,用于选择 AGC 队列结果与幂等命名空间。调用方 fields、action 和引用 provenance 不会覆盖服务端配方。"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。本接口按 sceneContent、stylePreset、customStyle 和规范化尺寸 / 模型参数重建 V2 version/action/fields/references;仅额外保留 source 精确等于 ai-game-creator-client 的标记,用于 AGC 队列结果与幂等命名空间。调用方 fields、action、引用 provenance 和 artSpec 等其它扩展字段不会保留或覆盖服务端配方。"
},
"assetFolderId": {
"type": ["string", "null"]
@@ -3307,7 +3304,7 @@
"description": "生成产物分类。External v1 通用图片接口禁止使用 scene;结构化游戏场景必须使用主站场景专用契约。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。内容与构图要求写入 prompt,参考图写入 referenceImageSrcs,尺寸 / 风格使用正式参数。本接口清理内部审计字段并按真实且已鉴权的参考输入重建 references,其余自定义对象字段可保存到生成原图记录。角色透明化等派生记录可另建处理阶段和来源元数据;不保证最终派生图继承原图 artSpec。通过项目 / 素材库查询实际保存值。"
},
"assetFolderId": {
"type": ["string", "null"],
@@ -3405,7 +3402,7 @@
"description": "项目上下文。提供 targetLayerId 时必须同时提供非空 projectId,否则返回 400。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。编辑要求写入 prompt,原图使用 sourceReferenceId,辅助参考使用 referenceImageSrcs。本接口保留经清理的自定义字段,references 由已鉴权原图和实际辅助参考重建;不能通过本字段指定或伪造源图身份。通过项目 / 素材库查询实际保存值。"
},
"assetFolderId": {
"type": ["string", "null"]
@@ -3492,7 +3489,7 @@
"x-genarrative-media-family": "static-image"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。本字段不配置去背景算法或选取源图;来源由 sourceImageSrc/sourceResourceId 等正式参数确定。保留经清理的自定义字段,并从实际且已鉴权来源重建 references。服务端处理审计字段不向普通调用方返回。通过项目 / 素材库查询实际保存值。"
},
"assetFolderId": {
"type": ["string", "null"]
@@ -3701,7 +3698,7 @@
"type": ["string", "null"]
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。需生效的内容、构图和分隔要求写入 iconDescriptions,规范图使用 referenceId。本接口清理内部审计字段并重建 references,自定义字段可保存于生成原图;透明图集和切片另建处理阶段 / 来源元数据,不自动继承 artSpec。后续可按来源链查询原图记录。本接口会读取已保存参考规范图 fields 中标题为“游戏类型”的值来组装类型提示,但不会解析本次请求的任意元数据作为提示词。"
},
"assetFolderId": {
"type": ["string", "null"]
@@ -3764,7 +3761,7 @@
"type": ["string", "null"]
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。提取来源、标注与尺寸使用正式请求参数。本接口清理内部审计字段并按实际已鉴权参考重建 references,自定义字段可保存于生成原图;透明图集和切片另建处理阶段 / 来源元数据,不自动继承全部字段。需原始上下文时按来源链查询项目 / 素材库记录。"
},
"assetFolderId": {
"type": ["string", "null"]
@@ -4065,7 +4062,7 @@
"description": "带项目上下文生成时,服务端据此直接写入画布完成态并返回最新项目快照。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。动作、源角色和生成设置由本接口的正式参数决定。经清理的上下文可保存到最终动作序列记录;不得用 characterAnimation、frames、previewVideoPath、frameCount、fps 或 durationSeconds 等旧运行字段承载序列。正式输出帧与时长通过资源 / 素材的 imageSequenceFrames 和 imageSequenceDurationMs 读取。"
},
"sourceResourceId": {
"type": ["string", "null"]
@@ -4317,7 +4314,7 @@
"description": "带项目上下文生成时,服务端据此直接写入画布完成态并返回最新项目快照。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。视频内容与参考媒体使用 prompt 及本接口的正式参考参数。本接口保留经清理的上下文;对象元数据可根据规范化生成时长补充或更新 fields 中的时长展示项。通过项目 / 素材库查询实际保存值。"
},
"sourceResourceId": {
"type": ["string", "null"]
@@ -4493,7 +4490,7 @@
"description": "带项目上下文生成时,服务端据此直接写入画布完成态并返回最新项目快照。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。音效由 prompt、duration、loop、model 等正式参数决定。本接口根据实际生成重建 fields、空 references 和 soundEffect 元数据;只从调用方对象复制 source、conversationId、toolCallMessageId,artSpec 等其它字段不保留。不能通过本字段覆盖实际时长、Loop 或提示词。"
},
"assetFolderId": {
"type": ["string", "null"],
@@ -4533,7 +4530,7 @@
"description": "带项目上下文生成时,服务端据此直接写入画布完成态并返回最新项目快照。"
},
"generationInputs": {
"$ref": "#/components/schemas/JsonValue"
"description": "可选生成上下文与来源元数据,接受任意 JSON 值;有命名字段时使用对象。自定义字段(包括 artSpec)不会自动进入提示词或覆盖正式生成参数。保存、重建及已知字段消费规则依接口而定,不能视为完整 HTTP 重放载荷。音乐内容与是否纯音乐由 gptDescriptionPrompt、makeInstrumental 等正式参数决定。本接口保存经清理的上下文,不从自定义字段提取提示词或生成选项。通过项目 / 素材库查询实际保存值。"
},
"assetFolderId": {
"type": ["string", "null"],
+20 -5
View File
@@ -2,6 +2,14 @@
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
## 2026-10-05 generationInputs 的保存与复用不等于自动生效或完整透传
- **现象 / 根因**:调用方把构图等要求只写入 `generationInputs.artSpec`,但实际生图输入没有这些要求;把 `JsonValue` 和“可复用规范”误读为服务端会自动组装提示词、完整保留全部元数据或自动用于下一次生成。
- **现行边界**:整个 `generationInputs` 承载生成上下文、来源和应用元数据,保存规则取决于接口。场景与音效重建配方;图集原图可保存自定义字段,透明图集与切片另建处理阶段 / 来源元数据。已知字段仍有实际消费者,例如规范图的“游戏类型”、V2 面板恢复字段和客户端来源标记,不能把整个对象描述为无业务作用。
- **排查 / 使用**:同时核对正式请求字段、当前接口的重建 / 清理逻辑,以及最终资源 / 素材记录;重要要求必须进入 `prompt`、`iconDescriptions` 或场景结构化参数。项目与素材库读取可取得服务端实际保存的上下文,但它不等于完整 HTTP 重放载荷;重试仍保存原始请求和幂等键。
- **AGC 美术包**:核心图集新请求将原 brief 与共用的风格、内容和间距要求分别写入 `iconDescriptions`;普通图标入口仍原文单项透传。新增模板只影响新请求,旧请求继续按冻结正文和操作身份恢复,不改原 brief 的意图判据。真实 HTTP 载荷与旧请求恢复必须同时验证,不能只测试 `artSpec` 包含文案。
- **权威说明**:[External v1 OpenAPI](../../openapi/genarrative-external-v1.openapi.json) 与 [API 指南的 Generation Inputs Metadata](../../../.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md#generation-inputs-metadata)。本条记录现有行为,不引入 API、存储或 UI 行为变更。
## 2026-10-06 陶泥儿导出与发布媒体直传:幂等摘要、幂等账本、skill-pack 红与 zip 根入口
- **multipart 发布的幂等摘要必须纳入图片字节**:写路径改 `multipart/form-data` 后,`metadata` 文本 part 只描述槽位;若摘要只序列化元数据,同一 `Idempotency-Key` 换一张新图或换一个沿用 objectKey 会被判成同请求重放,服务端静默复用旧结果、新图丢失。现行口径:服务端 `publish_request_digest` = 规范化元数据 JSON + 每个 `cover`/`screenshot` part 的 SHA-256 + 槽位里的 objectKey;AGC 侧 `metadata_digest` 同样把图片原始字节喂进哈希。create / create version / update metadata 三条写路径共用;回归时「只换图不改文案」必须得到不同摘要。
@@ -173,7 +181,6 @@
- **处理(现行口径)**:`.project-chat-composer.is-direct-codex` 是单列网格(`gap: 8px`);**控件全部集中在唯一一行工具条**——左组 `@` / `+`,右组 模型 / 语音 / **「AI 润色 / 恢复原文」(紧贴发送左侧)** / 发送,六颗 `flex-wrap: nowrap` 平铺(润色钮由 `ResourceReferenceInput` 的 `actionsPortalTarget` portal 进右组发送钮之前的挂点,不再自成一行;它与发送钮同为圆角矩形按钮,相邻成组,DOM / Tab 顺序都是 模型 → 麦克风 → 润色 → 发送);状态/提示层只在有内容时占行(空输入态第二行为 0 高、行间距为 0,状态自己带 6px 上间距)。窄宽度用 `min-width` 兜(控件排 252px / 外壳 278px)+ 横向溢出可见:**不换行、不堆叠、不重叠**,发送钮仍可点击(夹具 `elementFromPoint` 命中)。模型钮封顶 160px 且名字走省略号(收缩由它承担),左组是固定 28px 动作钮、`flex: 0 0 auto` 不参与收缩。**模型失败提示后来改成走浮层**(见本文件顶部 2026-10-04 / 2026-10-05 两条):控件排里既不留常驻红字、也不留常驻状态文字——「控件排内独占一行」那次快照已退役,行高回到由模型触发钮决定。
- **判据/取证**:`apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts` 用层叠求值钉住单行工具条(240 / 280 / 296 / 320 / 360 / 438 / 900 七个宽度下 `flex-wrap` 都是 `nowrap`、控件排 `min-width: 252px`、外壳 `min-width: 278px`)、输入盒最高高度算式(12 盒内上内边距 + 160 编辑器 + 0 输入区行距 + 0 状态行 + 0 输入区下内边距 + 8 输入盒与工具栏行距 + **30 工具栏行高(模型触发钮 30px 撑起来,发送/语音是 28px)** + 12 盒内下内边距 = **222**),以及组件源码里润色钮渲染在工具条挂点(`actionsPortalTarget={controlsActionsSlot}`、挂点在控件排之后);`tests/appSurface/project-development.suite.ts` 钉住 composer 的 `gap: 8px`。真实渲染对照见 #600 PR:同一夹具在 495/438/360/296/280px 面板 × 空 / 长多行 / 队列+提示 四态下,改前 2~4 处相交(含文字被提示盖住),改后 0 处相交、0 处溢出。
- **关联**:`apps/ai-game-creator-shell/src/styles.css`(文件末尾「输入区工具栏与状态提示」区块)、`docs/【功能说明】AGC聊天AI润色与发送前提醒-2026-09-10.md`。
## 2026-10-03 AGC 随包 plugins 的 feature 档位必须与消费方一致,且门禁会因 build.rs 未重跑而假通过
- **现象**:Windows 本机 `npm run check:generated-bindings`(`npm run lint` 链内,`scripts/check-repository-ci.sh` 的 Repository checks 也走它)在 `build.rs:167:29` panic:`插件随包资源校验失败:随包插件存在未声明文件:.../src-tauri/resources/plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll(目标 x86_64-pc-windows-msvc 与当前 feature 组合不允许;请先执行随包资源准备步骤)`;树上换成 `agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe` 时报同一类错。反向还有更隐蔽的形态:门禁 2 秒就 exit 0 说「通过」,但 tree 上其实带着编辑器产物。
@@ -540,7 +547,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、公开契约)。
- 复用判据:凡是「平台产出 → 客户端校验后落盘」的链路,客户端门只能表达**安全 / 预算**约束,不得比平台的产品上限更严;两边上限要引同一个常量或同一份文档,改一边时必须同时改另一边,并补一条「上限之内必须能落盘」的回归用例。
@@ -5572,10 +5579,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)
@@ -6562,6 +6569,14 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- **判据/取证**:`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,否则重放会丢失服务端回显。
- 美术包仅有规范图和背景图时,`reuse-or-create` 必须继续补齐图集,不能把两图包返回为 completed。补齐优先恢复现有阶段请求;无请求才只读查找并按需生成,查询失败、身份冲突或账本损坏不能当作资源不存在而重发。补齐失败保留有效基础图,三张主图齐全且可验证后才交付;零切片告警不等于缺失总图。
- 规范图的原始生成资源 ID 与当前账号重新登记后的参考 ID 可以不同。整包完成和中断恢复只按阶段完成结果核对图集资源、对象、任务身份,不能额外要求请求参考 ID 等于规范图原始 ID;这会在素材已经保存后误报身份未绑定。规范图来源关系沿用现有绑定校验。
## 2026-10-05 在线游玩「一直黑屏」:加载面缺失 + 发行网关不压不发 ETag
- **现象**:用户反馈「进入游玩…加载有点慢,一直黑屏体验不好」。真实栈(真实发行包 + Chromium + 4 Mbps/100 ms 模拟链路)实测:网页游玩页点击「开始游戏」后 99.3% 像素亮度 < 24 的近黑面板 + 一行 `游戏正在启动…`,游戏画面 **3298 ms** 才出现;后台审核页点「试玩当前待审版本」后 iframe 直接以 `opacity:1` 出现、区域**全白空白** 4587 ms,页面**全程没有任何加载文案**。
@@ -542,7 +542,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`。
@@ -1495,7 +1495,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。
@@ -1628,20 +1628,36 @@ game-project/
## 2026-08-23 Direct Codex 美术包显式重生成与切片投影
- 客户端图集入口不暴露或发送 `sliceCount`;保留 `sliceMode` 与网格参数。服务端 API、OpenAPI 及服务端数量限制保持现状。四类内容只是生成需求,不能推导四个连通域、切片数量或返回次序对应的语义。
- 美术包和普通图集按实际返回的 0–256 张切片保存、登记与返回。有效透明总图没有独立切片时仍完成并返回切片告警;Agent 必须看图确认实体、状态、用途及缺失内容,再决定使用、进一步本地处理或补充生成。已有授权、付费幂等与资产登记规则继续适用,不能虚构切片、坐标或 Canvas 身份。
- 美术包 `reuse-or-create` 复用有效规范图和背景图,继续补齐缺失图集。补齐时若图集阶段有持久化请求,优先恢复原请求、原幂等键和操作身份,不以远端资源查找绕过账本;没有请求时才尝试只读恢复已有图集,确认没有可恢复产物后才发起图集生成。查询失败、账本损坏或身份冲突返回错误并保留现场,不把失败解释为不存在而新建请求,也不重生成已有效的前两张图。完整本地包仍按已有规则直接复用。
- 美术包成功结果统一要求规范图、背景图和总图通过已有文件、来源与登记校验,并返回这三张主图及实际可验证切片;缺失任一主图时返回工具错误,不返回 `completed`。有效总图没有独立切片仍为完成并附切片告警。补齐图集失败时保留已有规范图、背景图和可恢复请求,明确告知图集未完成及真实原因;游戏工程本身允许复用已有基础素材的验收规则不随之收紧。
- 整包重生成完成与中断恢复按阶段完成结果核对图集的资源、对象和任务身份,不再要求请求参考 ID 等于规范图最初的资源 ID;规范图在当前账号重新登记后可具有不同 ID。既有来源绑定、请求恢复和本轮替换记录校验继续适用,不增加新的参考图对账流程。
- 新切片使用源图集身份隔离的目录与中性序号,不再生成按 player 等用途命名的别名。普通图集的清单记录实际总图路径并保存在自己的目录;美术包公共清单与私有回执只证明来源、完整性和实际产物集合,不表达语义分类。历史四用途路径仅白名单兼容读取与恢复,文件名不能证明用途。
- 本地事务按固定合同文件与本次实际切片集合冻结快照、提交及恢复,保留摘要、真实 alpha、可见像素、唯一身份、项目归属、资源预算和 CAS 校验。重生成的旧快照包含实际旧切片;历史固定集合事务仍可恢复,不重写旧账本或请求体。普通调用升级前携带数量的活动请求必须在新 POST 前被定位并继续使用原身份;多个可能候选时失败关闭,不能任选或重复付费。
- 图集完成结果账本保留响应的 `sliceMode/gridX/gridY`,以支持同身份重放。历史账本已丢失切分声明且返回零切片时,声明无法用于证明切片方式;保留有效总图并明确告警,不因此重发生成。存在实际切片时仍要求切分声明一致。
- 两个工具都返回总图、完整切片路径、全部资源身份、告警以及有界预览。每张预览明确绑定路径,列出未内嵌预览的路径,Agent 使用现有图片查看能力继续检查。可验证的本地处理产物经现有导入工具登记;游戏语义绑定放在游戏工程中。
- 验收覆盖新 HTTP 请求完全省略数量字段、0/2/4/6 张切片、不同图集目录隔离、完整资源投影、旧账本零新增生成 POST、动态集合事务恢复与路径白名单。
- 美术包核心图集的新请求必须把 brief 原文与客户端的素材内容、风格和排布要求共同写入 `iconDescriptions`。内容覆盖当前玩法需要的玩家主体及状态、目标或收集物、障碍或场景元素、反馈特效;各素材独立排布并留出切分间距,沿用规范图的轮廓、材质、色板与光照。描述条数和内容类别不代表切片数量或返回顺序。规范图通过 `referenceId`、比例和尺寸通过正式参数传入;透明结果由服务端纯色底生成与抠图提供,不向模型追加直接生成透明背景的要求。`generationInputs.artSpec` 仅保留描述性上下文,不能作为要求已进入生成提示词的证据。
- Agent 在普通图集的 `prompt` 和美术包的 `brief` 中,需求明确时须列出各项素材的数量、状态及独立排布与留白要求;数量未确定时不编造。数量用于表达生成目标,不构成服务端切片数量保证,也不赋予返回顺序语义;生成后仍须看图确认实际切片内容与用途。工具参数说明与随包 skill 使用同一口径。
- 美术包 `brief` 的 Agent 工具说明简洁标注“不超过 200 字符”。本次仅补充说明,schema 的 4000 字符上限、请求组装、阶段校验和实际生成行为保持现状;删除旧 100 条描述输入方式并适当提高完整提示词字符上限的后续改造由 [issue #660](https://git.genarrative.world/git/GenarrativeAI/Genarrative/issues/660) 跟踪。
- 美术包工具继续只接收 `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)
+12 -4
View File
@@ -122,7 +122,9 @@ function validateExpectation(route) {
);
}
if (hasOwn(expect, 'upstreamPath')) {
fail(`${context} play_session_gateway 不做路径重写,不能配置 upstreamPath。`);
fail(
`${context} play_session_gateway 不做路径重写,不能配置 upstreamPath。`,
);
}
return;
}
@@ -288,7 +290,9 @@ function validateRustPlaySessionGatewayIsolation() {
const playSessionIndex = classifyBlock[1].indexOf(
'if is_play_session_proxy_path(path) {',
);
const genericApiIndex = classifyBlock[1].indexOf('path == "/api" || path.starts_with("/api/")');
const genericApiIndex = classifyBlock[1].indexOf(
'path == "/api" || path.starts_with("/api/")',
);
if (playSessionIndex < 0) {
fail(
'Pingora classify_path 缺少播放会话前缀判定(必须在通用 /api 分支之前命中)。',
@@ -320,7 +324,10 @@ function validateRustPlaySessionGatewayIsolation() {
function validateContentGatewayCookieIsolation() {
const clearCookieFragment = 'proxy_set_header Cookie "";';
const genericApiLocation = 'location ~ ^/api(?:/|$)';
const contentGatewayKinds = new Set(['release_gateway', 'play_session_gateway']);
const contentGatewayKinds = new Set([
'release_gateway',
'play_session_gateway',
]);
for (const route of matrix.routes) {
if (!contentGatewayKinds.has(route.expect?.kind)) {
@@ -346,7 +353,8 @@ function validateContentGatewayCookieIsolation() {
// 播放会话前缀必须排在自己的 `^~` 前缀 location 上,并且在模板里排在通用 `/api` location 之前;
// nginx 的 `^~` 前缀优先于正则 location,但顺序仍按任务要求固定,便于人工核对。
const playSessionLocation = 'location ^~ /api/game-distribution/play-sessions/';
const playSessionLocation =
'location ^~ /api/game-distribution/play-sessions/';
for (const environment of ['production', 'development']) {
const source = files[environment];
const playSessionIndex = source.indexOf(playSessionLocation);