Merge branch 'master' into rm/design-v2
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
This commit is contained in:
@@ -32,6 +32,7 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
|
||||
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
|
||||
- Icon spritesheet generation accepts `sliceMode="connected-components"` (default alpha-connectivity detection) or `sliceMode="grid"`. Grid mode requires `gridX` and `gridY` (1-32); use `sliceCount` only to constrain connected-component output.
|
||||
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
|
||||
- Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record.
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Background removal | `/api/external/v1/editor/images/background-removals` | `sourceImageSrc` | `projectId`, `sourceResourceId`, `targetLayerId`, static-image `assetKind`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceMode`, `gridX`, `gridY`, `sliceCount`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
@@ -94,7 +94,7 @@ For image edit/redraw, confirming an upload is not sufficient: create a project
|
||||
|
||||
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
|
||||
|
||||
`sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
|
||||
`sliceMode` controls atlas splitting. Use `"connected-components"` (default) to detect independent opaque regions by alpha connectivity, or `"grid"` with positive `gridX` and `gridY` values (maximum 32 each). `sliceCount` optionally constrains the connected-component result.
|
||||
|
||||
## Common Values
|
||||
|
||||
|
||||
@@ -79,9 +79,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
|
||||
|
||||
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
|
||||
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For a fixed four-category game contract it may send `sliceMode: "grid"`; for free-form assets use `sliceMode: "connected-components"` (the default).
|
||||
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. When using the fixed four-category contract, require response `sliceMode: "grid"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
|
||||
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
|
||||
|
||||
|
||||
@@ -881,12 +881,14 @@ def _self_test() -> None:
|
||||
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
|
||||
canvasSession=session,
|
||||
assetLabel="贪吃蛇透明图集",
|
||||
sliceMode="connected-components",
|
||||
referenceId="must-not-override-explicit-reference",
|
||||
iconDescriptions=["不得覆盖显式图标描述"],
|
||||
)
|
||||
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["screenColor"] == "auto"
|
||||
assert calls[0]["body"]["sliceMode"] == "connected-components"
|
||||
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
|
||||
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
|
||||
print("self-test ok")
|
||||
|
||||
+10
-5
@@ -1,8 +1,8 @@
|
||||
# Server-side OpenAI-compatible LLM endpoint base URL.
|
||||
LLM_BASE_URL="https://api.vectorengine.cn/v1"
|
||||
LLM_BASE_URL="https://api.tiantoken.com/v1"
|
||||
|
||||
# Server-side API key used by the local Vite proxy.
|
||||
# Recommended: set `LLM_API_KEY` locally, or use `VECTOR_ENGINE_API_KEY`
|
||||
# Recommended: set `LLM_API_KEY` locally, or use `TIANTOKEN_API_KEY`
|
||||
# through the Rust api-server proxy.
|
||||
# Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy,
|
||||
# but it should not be relied on by browser code.
|
||||
@@ -122,7 +122,7 @@ WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY=""
|
||||
# Model name for chat completions.
|
||||
VITE_LLM_MODEL="gpt-5.4-mini"
|
||||
GENARRATIVE_LLM_PROVIDER="openai-compatible"
|
||||
GENARRATIVE_LLM_BASE_URL="https://api.vectorengine.cn/v1"
|
||||
GENARRATIVE_LLM_BASE_URL="https://api.tiantoken.com/v1"
|
||||
GENARRATIVE_LLM_API_KEY=""
|
||||
GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
|
||||
|
||||
@@ -130,10 +130,15 @@ GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
|
||||
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
|
||||
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
|
||||
|
||||
# VectorEngine LLM and GPT-image-2 / Gemini image generation config.
|
||||
# Tiantoken LLM and GPT-image-2 / Gemini image generation config.
|
||||
TIANTOKEN_BASE_URL="https://api.tiantoken.com"
|
||||
TIANTOKEN_API_KEY=""
|
||||
TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS="1000000"
|
||||
|
||||
# VectorEngine is retained for Suno audio generation only.
|
||||
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn"
|
||||
VECTOR_ENGINE_API_KEY=""
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000"
|
||||
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS="180000"
|
||||
|
||||
# ElevenLabs editor sound-effect generation is server-side only.
|
||||
ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -74,7 +74,7 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
|
||||
三个接口:**对上**承顶层系统范围表并跑循环覆盖检查;**对内**地图↔职责↔依赖
|
||||
三方一致、主数据归属唯一;**对下**目录映射 + MVP 闭环喂系统文档站。
|
||||
|
||||
## 四、怎么写(模板即流程,十二节按序)
|
||||
## 四、怎么写(模板参考结构,建议按此组织)
|
||||
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/architecture.md)
|
||||
|
||||
### 1. 架构定位与目标
|
||||
@@ -85,7 +85,7 @@ description: 写游戏策划案(GDD)系统架构时使用。在顶层设计
|
||||
→ 没有变更记录的架构文档,第二轮迭代就会变成黑箱。
|
||||
|
||||
### 2. 系统地图
|
||||
Sxx 编号清单(核心系统 2~12 个)+ 支撑层(存档/UI,不拥有核心规则)。
|
||||
Sxx 编号清单(核心系统通常 1-5 个,有明确要求可超出 5 个)+ 支撑层(存档/UI,不拥有核心规则)。
|
||||
P0 段五列表:
|
||||
| 系统 | 目的 | 输入 | 输出 | P0 原因 |
|
||||
→ 每行 P0 原因必须答"删了它,__ 塌";答不出的降级或合并。
|
||||
|
||||
@@ -69,7 +69,7 @@ description: 写游戏策划案(GDD)概念层时使用。把一句话游戏
|
||||
记住三个接口:**对内**锚点仲裁一切;**对下**张力变取舍表、定稿变硬约束;
|
||||
**对上**边界画线防止越层。九节不是清单,是一台咬合的机器。
|
||||
|
||||
## 四、怎么写(模板即流程,九节按序)
|
||||
## 四、怎么写(模板参考结构,建议按此组织)
|
||||
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/concept-design.md)
|
||||
|
||||
### 1. 一句话概念
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
---
|
||||
name: game-gdd-system-doc
|
||||
description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二节同构骨架、
|
||||
description: 写单个系统的设计文档(Sxx)时的总纲——通用纪律、十二类常见内容、
|
||||
红线与分析文档格式。每类系统的专属写法与模板在 modules/system-types/ 下对应目录的 SKILL.md
|
||||
与对应模块的模板.md 里,按需取用。
|
||||
---
|
||||
@@ -26,9 +26,9 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
|
||||
1. 架构已定稿:找到本系统的 Sxx 编号、职责表行、依赖方向——这是合同。
|
||||
2. 在 01~12 文件夹里选最接近的系统类型(可组合,如"钓鱼"=05 采集+06 战斗
|
||||
的判定部分),读取对应的 `SKILL.md` 与 `模板.md`。
|
||||
3. 该文件夹标注"必读例子"的,先读例子全文做密度锚。
|
||||
3. 该文件夹标注"参考例子"的,可先读例子了解写法。
|
||||
|
||||
## 三、十二节总览:写什么、为什么、怎么咬合
|
||||
## 三、常见内容总览:写什么、为什么、怎么咬合
|
||||
|
||||
系统文档回答四个问题:
|
||||
**这个系统为什么存在(1~2)→ 玩家怎么用它(3~5)→ 它怎么运转(6~8)→
|
||||
@@ -52,7 +52,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
|
||||
咬合:**对上**服从架构三条合同(编号/职责/依赖);**对内**状态与接口不越
|
||||
职责边界;**对下**第 7 节交接喂 TDD。
|
||||
|
||||
## 四、十二节通用写法
|
||||
## 四、常见内容的参考写法
|
||||
(各系统类型的特殊写法见对应文件夹 SKILL.md;纯净模板在其 模板.md)
|
||||
|
||||
1 系统目的:若删除它,__ 会塌——一句话说不出 = 该系统不该存在。
|
||||
@@ -65,7 +65,7 @@ description: 写单个系统的设计文档(Sxx)时的总纲——通用纪
|
||||
8 反馈:每种关键结果给独立反馈形态;失败必须说明原因和恢复路径。
|
||||
9 内部循环:动词链;可拆单次/区域/长期三层。
|
||||
10 输入输出与依赖:引用具名系统与具名数据,禁泛称"资源"。
|
||||
11 边界与非目标:照该类型 skill 的"三不"写全;必含"字段数值归 TDD"一条。
|
||||
11 边界与非目标:参考该类型 skill 的“三不”说明边界;建议说明字段与数值的交接边界。
|
||||
12 开放问题:结构级才留;手感数值类标"待原型验证"。
|
||||
|
||||
## 五、分析文档(全局一份,按层分节)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -80,7 +80,7 @@ description: 写游戏策划案(GDD)顶层设计时使用。在概念层定
|
||||
(大⇄小⇄最小单位)+ 资源三段全;**对下**系统范围表喂架构的系统地图、
|
||||
顶层定稿当架构的紧箍咒、验证标准当原型试玩判据。
|
||||
|
||||
## 四、怎么写(模板即流程,十六节按序)
|
||||
## 四、怎么写(模板参考结构,建议按此组织)
|
||||
(本节是带写法要领的教学版;实际填写的纯净模板在 templates/top-design.md)
|
||||
|
||||
### 1. 顶层定位与规模锚点
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
|
||||
你是游戏策划协作 Agent,与用户持续协作完成游戏设计。像普通策划同事一样交流,使用工作区文件工具读写资料;所有文件路径使用相对路径。根据当前对话、阶段上下文和已有文档决定下一步行动。修改文件后,简要说明修改内容和相对路径。对不确定内容区分用户确认、Agent 建议和待原型验证事项;不要把建议写成用户已确认的决定。
|
||||
|
||||
优先完成能够依据已有信息推进的工作,不要为每个设计空白都询问用户。局部、可逆的问题可以先提出合理方案并标为暂定。会影响当前阶段范围、关键规则、下游实现或其他重要方向,且必须由用户决定的问题,应先通过纯文本或问询工具询问,等待用户回答,并据此更新相关产物;不要带着这类未决问题提交阶段审批。
|
||||
优先完成能够依据已有信息推进的工作,不要为每个设计空白都询问用户。局部、可逆的问题可以先提出合理方案并标为暂定。会影响当前阶段范围、关键规则、下游实现或其他重要方向,且必须由用户决定的问题,应先通过纯文本或问询工具询问,等待用户回答。决定稳定后,再更新受影响的正式产物和必要的过程记录;不要带着这类未决问题提交阶段审批。
|
||||
|
||||
分析阶段优先记录当前目标、上层约束、候选方案、取舍、用户已确认或 Agent 暂定的边界,以及必须检查的验收项。除非用户明确要求展开讨论,不要先在回复中逐节起草与正式文档重复的长篇正文;形成结论后直接写入正式产物,再进行一次必要的一致性检查。文件操作前只需说明简短计划、目标文件和主要变化。
|
||||
|
||||
正式策划文档在文档头部写明版本标记,例如“版本:v1”。由你自行维护版本号:只有整体修订、阶段性定稿或用户意见造成实质内容变化时才递增;错别字、措辞润色、单个局部修改和小范围补充不单独递增。
|
||||
|
||||
阶段审批是每个阶段的最终检查,表示本阶段产物已经完成,无未决内容,交给用户做最终检阅,不承担问询功能。提交前,解决所有影响本阶段完成的关键问题,或明确说明它们不阻塞本阶段交付,并更新相关产物。可以保留不阻塞当前阶段的后续事项和待原型验证项。
|
||||
|
||||
过程文档用于记录关键依据、决定和待办,不要求实时完整,也不应重复正式设计文档。阶段内优先完成主要设计内容;只有稳定且影响后续工作的决定才需要同步到多个过程文档。阶段提交前,补齐影响验收的关键记录。
|
||||
|
||||
阶段获批后,产物中已经采用的方案作为后续工作的依据,并保留原有决策来源。除非用户主动质疑或出现新的约束冲突,不要反复要求确认历史暂定决定。
|
||||
|
||||
用户说“继续”时,继续推进当前阶段最有价值的工作。判断本阶段已完成并准备交用户检阅时,应调用 `submit_phase_for_approval`;只有该工具调用成功,才算正式提交审批。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{"type":"function","function":{"name":"get_workflow_status","description":"读取当前策划工作流状态,只返回阶段列表、当前阶段、已批准阶段和待审批阶段;不推进阶段、不提交审批、不修改文件。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"list_resources","description":"列出固定资源的逻辑目录、资源 ID、标题和简介。资源是只读的随包文档;不要猜测物理路径。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"read_resource","description":"读取一份固定资源文档全文。每次读取一个 resource_id;资源只读。读到未实现占位文档时由你自行判断和处理。","parameters":{"type":"object","properties":{"resource_id":{"type":"string"}},"required":["resource_id"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件,优先用于已有文件的小范围修订。先读文件,以唯一且非空的 old_text 精确匹配并替换为 new_text;new_text 为空可删除片段,保留原文并追加可插入。匹配失败不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["path","old_text","new_text"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件。使用 old_text/new_text,或使用 edits 一次进行多个独立替换;每个 old_text 必须非空且在原文件中唯一,匹配失败、重复或范围重叠时不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["old_text","new_text"],"additionalProperties":false}}},"required":["path"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"delete_path","description":"谨慎使用;永久删除工作区内的文件或目录;目录会连同全部内容递归删除,不备份。先确认目标及删除范围。path 使用相对路径,不能删除工作区根目录,也不能经过链接。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"list_dir","description":"列出工作目录内的文件和目录。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"read_file","description":"读取工作目录内的 UTF-8 文本文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
|
||||
|
||||
@@ -19,6 +19,12 @@ 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.
|
||||
|
||||
When `agc_generate_image` is used with `kind="art-spritesheet"`, pass
|
||||
`sliceMode="connected-components"` (the default alpha-connectivity splitter)
|
||||
or `sliceMode="grid"` with `gridX` and `gridY` (1-32 each). The selected mode is carried
|
||||
through the client request and returned result; do not infer it from the number
|
||||
of slices.
|
||||
|
||||
## Authorization boundary
|
||||
|
||||
`agc_tools` is an AGC client-owned bridge to the AGC backend. In the normal client build it uses the current client login session and account routes; the user and model never need to provide, configure, paste, create, or rotate an API Key, Token, Cookie, URL, or `.env` value. If the tool returns `401` or `403`, report only that the AGC client login or permission state is unavailable, stop the operation, and do not ask the user for credentials or expose an internal URL.
|
||||
|
||||
+1
@@ -15,6 +15,7 @@
|
||||
- On timeout or uncertain delivery, reuse the recorded operation; never create a replacement request.
|
||||
- `postprocess-failed-source-preserved` means the complete provider source remains usable, but the requested transparent derivative is absent.
|
||||
- `sliceWarning` means the complete transparent sheet remains usable, but individual slices are absent.
|
||||
- For direct `agc_generate_image` spritesheet requests, `sliceMode="connected-components"` selects alpha-connectivity detection and `sliceMode="grid"` uses the caller-provided `gridX` and `gridY` (1-32 each). The client preserves the selected mode and grid dimensions in the request identity and result metadata.
|
||||
- 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. `slicePaths` contains only slices that the client downloaded, validated, and registered with their platform source identities.
|
||||
- `resources` contains only safe registered identity fields: local asset/path/kind/media type, Canvas project/resource/asset/task IDs, and reference resource IDs. It never exposes prompts, models, provider routes, absolute paths, URLs, tokens, cookies, or API keys.
|
||||
|
||||
@@ -28,6 +28,7 @@ mod prompt;
|
||||
mod runtime_actions;
|
||||
mod runtime_adapter;
|
||||
mod runtime_driver;
|
||||
mod runtime_error;
|
||||
mod runtime_protocol;
|
||||
mod runtime_state;
|
||||
mod runtime_tools;
|
||||
@@ -56,6 +57,7 @@ pub(crate) use prompt::*;
|
||||
pub(crate) use runtime_actions::*;
|
||||
pub(crate) use runtime_adapter::*;
|
||||
pub(crate) use runtime_driver::*;
|
||||
pub(crate) use runtime_error::*;
|
||||
pub(crate) use runtime_protocol::*;
|
||||
pub(crate) use runtime_state::*;
|
||||
pub(crate) use runtime_tools::*;
|
||||
|
||||
@@ -271,6 +271,51 @@ fn game_creator_codex_app_server_error_kind(kind: &str) -> platform_llm::LlmErro
|
||||
))
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_error_kind_with_machine_detail(
|
||||
kind: &str,
|
||||
error: &serde_json::Value,
|
||||
) -> platform_llm::LlmError {
|
||||
let mut fields = Vec::new();
|
||||
if let Some(object) = error.as_object() {
|
||||
if let Some(code) = object.get("code").and_then(serde_json::Value::as_str) {
|
||||
if !code.is_empty()
|
||||
&& code.len() <= 80
|
||||
&& code
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
|
||||
{
|
||||
fields.push(format!("code={code}"));
|
||||
}
|
||||
}
|
||||
let keys = object
|
||||
.keys()
|
||||
.filter(|key| {
|
||||
matches!(
|
||||
key.as_str(),
|
||||
"httpConnectionFailed"
|
||||
| "responseStreamConnectionFailed"
|
||||
| "responseStreamDisconnected"
|
||||
| "responseTooManyFailedAttempts"
|
||||
| "activeTurnNotSteerable"
|
||||
| "codexErrorInfo"
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if !keys.is_empty() {
|
||||
fields.push(format!("fields={}", keys.join(",")));
|
||||
}
|
||||
}
|
||||
let suffix = if fields.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" detail={}", fields.join(" "))
|
||||
};
|
||||
platform_llm::LlmError::InvalidRequest(format!(
|
||||
"{GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX}{kind}{suffix}"
|
||||
))
|
||||
}
|
||||
|
||||
fn game_creator_codex_app_server_error_http_status(
|
||||
info: &serde_json::Value,
|
||||
field: &str,
|
||||
@@ -425,7 +470,7 @@ fn game_creator_codex_app_server_failed_turn_error(
|
||||
return game_creator_codex_app_server_error_kind("unauthorized");
|
||||
}
|
||||
let Some(info) = error.get("codexErrorInfo").filter(|info| !info.is_null()) else {
|
||||
return game_creator_codex_app_server_error_kind("other");
|
||||
return game_creator_codex_app_server_error_kind_with_machine_detail("other", error);
|
||||
};
|
||||
if let Some(kind) = info.as_str() {
|
||||
return match kind {
|
||||
@@ -449,8 +494,8 @@ fn game_creator_codex_app_server_failed_turn_error(
|
||||
game_creator_codex_app_server_error_kind("thread-rollback-failed")
|
||||
}
|
||||
"sandboxError" => game_creator_codex_app_server_error_kind("sandbox-error"),
|
||||
"other" => game_creator_codex_app_server_error_kind("other"),
|
||||
_ => game_creator_codex_app_server_error_kind("other"),
|
||||
"other" => game_creator_codex_app_server_error_kind_with_machine_detail("other", error),
|
||||
_ => game_creator_codex_app_server_error_kind_with_machine_detail("other", error),
|
||||
};
|
||||
}
|
||||
for field in [
|
||||
@@ -466,7 +511,7 @@ fn game_creator_codex_app_server_failed_turn_error(
|
||||
if info.get("activeTurnNotSteerable").is_some() {
|
||||
return game_creator_codex_app_server_error_kind("active-turn-not-steerable");
|
||||
}
|
||||
game_creator_codex_app_server_error_kind("other")
|
||||
game_creator_codex_app_server_error_kind_with_machine_detail("other", error)
|
||||
}
|
||||
|
||||
async fn isolate_game_creator_codex_app_server_terminal_unknown(
|
||||
|
||||
@@ -264,22 +264,45 @@ pub(crate) fn execute_design_file_tool(
|
||||
}
|
||||
"patch_file" => {
|
||||
let relative = required_tool_path(args)?;
|
||||
let old = args
|
||||
.get("old_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 old_text")?;
|
||||
let new = args
|
||||
.get("new_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 new_text")?;
|
||||
if old.is_empty() {
|
||||
return Err("old_text 不能为空".to_string());
|
||||
}
|
||||
let edits = if let Some(items) = args.get("edits").and_then(Value::as_array) {
|
||||
if items.is_empty() {
|
||||
return Err("edits 不能为空".to_string());
|
||||
}
|
||||
items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
let old = item
|
||||
.get("old_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| format!("edits[{index}].old_text 必须是字符串"))?;
|
||||
let new = item
|
||||
.get("new_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| format!("edits[{index}].new_text 必须是字符串"))?;
|
||||
if old.is_empty() {
|
||||
return Err(format!("edits[{index}].old_text 不能为空"));
|
||||
}
|
||||
Ok((old.to_string(), new.to_string()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?
|
||||
} else {
|
||||
let old = args
|
||||
.get("old_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 old_text")?;
|
||||
let new = args
|
||||
.get("new_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 new_text")?;
|
||||
if old.is_empty() {
|
||||
return Err("old_text 不能为空".to_string());
|
||||
}
|
||||
vec![(old.to_string(), new.to_string())]
|
||||
};
|
||||
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
||||
if !path.is_file() {
|
||||
return Ok(Value::String(format!(
|
||||
"局部修改失败:文件不存在:{display}"
|
||||
)));
|
||||
return Err(format!("文件不存在:{display}"));
|
||||
}
|
||||
let content =
|
||||
fs::read_to_string(&path).map_err(|error| format!("读取失败:{error}"))?;
|
||||
@@ -288,20 +311,52 @@ pub(crate) fn execute_design_file_tool(
|
||||
} else {
|
||||
"\n"
|
||||
};
|
||||
let old = old.replace("\r\n", "\n").replace('\n', newline);
|
||||
let new = new.replace("\r\n", "\n").replace('\n', newline);
|
||||
let count = content.matches(&old).count();
|
||||
if count != 1 {
|
||||
return Err(format!(
|
||||
"原文匹配 {count} 处,需要唯一匹配;请重新读取文件并扩大匹配范围"
|
||||
));
|
||||
let normalized = edits
|
||||
.into_iter()
|
||||
.map(|(old, new)| {
|
||||
(
|
||||
old.replace("\r\n", "\n").replace('\n', newline),
|
||||
new.replace("\r\n", "\n").replace('\n', newline),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = Vec::new();
|
||||
for (index, (old, new)) in normalized.iter().enumerate() {
|
||||
let count = content.matches(old).count();
|
||||
if count == 0 {
|
||||
return Err(format!("edits[{index}] 原文未找到:{display}"));
|
||||
}
|
||||
if count != 1 {
|
||||
return Err(format!(
|
||||
"edits[{index}] 原文匹配 {count} 处,必须唯一:{display}"
|
||||
));
|
||||
}
|
||||
let start = content.find(old).expect("count checked");
|
||||
let end = start + old.len();
|
||||
if let Some((other_index, _other_start, _other_end)) = matches
|
||||
.iter()
|
||||
.find(|(_, other_start, other_end)| start < *other_end && *other_start < end)
|
||||
{
|
||||
return Err(format!(
|
||||
"edits[{index}] 与 edits[{other_index}] 修改范围重叠:{display}"
|
||||
));
|
||||
}
|
||||
matches.push((index, start, end));
|
||||
let _ = new;
|
||||
}
|
||||
crate::write_game_creator_private_file(
|
||||
&path,
|
||||
content.replacen(&old, &new, 1).as_bytes(),
|
||||
"策划工作区文件",
|
||||
)?;
|
||||
Ok(Value::String(format!("已局部修改 {display}")))
|
||||
let mut updated = content.clone();
|
||||
for (index, start, end) in matches.into_iter().rev() {
|
||||
let (_, new) = &normalized[index];
|
||||
updated.replace_range(start..end, new);
|
||||
}
|
||||
if updated == content {
|
||||
return Err(format!("没有产生修改:{display}"));
|
||||
}
|
||||
crate::write_game_creator_private_file(&path, updated.as_bytes(), "策划工作区文件")?;
|
||||
Ok(Value::String(format!(
|
||||
"已局部修改 {display}({} 处)",
|
||||
normalized.len()
|
||||
)))
|
||||
}
|
||||
"delete_path" => {
|
||||
let relative = required_tool_path(args)?;
|
||||
@@ -645,6 +700,29 @@ mod tests {
|
||||
)
|
||||
.expect("patch");
|
||||
assert!(patched.as_str().unwrap().contains("已局部修改"));
|
||||
execute_design_file_tool(
|
||||
root,
|
||||
"write_file",
|
||||
&json!({"path":"notes/multi.md","content":"甲\n乙\n丙"}),
|
||||
)
|
||||
.expect("write multi");
|
||||
let multi = execute_design_file_tool(
|
||||
root,
|
||||
"patch_file",
|
||||
&json!({
|
||||
"path":"notes/multi.md",
|
||||
"edits":[
|
||||
{"old_text":"甲","new_text":"一"},
|
||||
{"old_text":"丙","new_text":"三"}
|
||||
]
|
||||
}),
|
||||
)
|
||||
.expect("multi patch");
|
||||
assert!(multi.as_str().unwrap().contains("2 处"));
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("design_artifacts/notes/multi.md")).expect("read multi"),
|
||||
"一\n乙\n三"
|
||||
);
|
||||
execute_design_file_tool(root, "delete_path", &json!({"path":"notes"}))
|
||||
.expect("delete dir");
|
||||
assert!(!root.join("design_artifacts/notes").exists());
|
||||
|
||||
@@ -549,6 +549,7 @@ fn extract_mcp_arguments(root: &Path, tool: &str, arguments: &Value) -> Value {
|
||||
}
|
||||
"agc_generate_image" => {
|
||||
copy_string(object, "kind", &mut out);
|
||||
copy_string(object, "sliceMode", &mut out);
|
||||
copy_string(object, "aspectRatio", &mut out);
|
||||
copy_string(object, "imageSize", &mut out);
|
||||
copy_string(object, "assetName", &mut out);
|
||||
|
||||
@@ -1905,7 +1905,11 @@ fn direct_codex_error_is_mud_points_insufficient(error: &str) -> bool {
|
||||
|| normalized.contains("insufficient-mud-points")
|
||||
}
|
||||
|
||||
fn record_direct_codex_turn_failure(root: &Path, failure: DirectCodexTurnFailure) -> String {
|
||||
fn record_direct_codex_turn_failure(
|
||||
root: &Path,
|
||||
failure: DirectCodexTurnFailure,
|
||||
client_turn_id: Option<&str>,
|
||||
) -> String {
|
||||
let summary = direct_codex_failure_public_summary(&failure.error)
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| redact_agent_runtime_error(root, &failure.error, 320));
|
||||
@@ -1942,18 +1946,53 @@ fn record_direct_codex_turn_failure(root: &Path, failure: DirectCodexTurnFailure
|
||||
} else {
|
||||
"未能保存项目诊断"
|
||||
};
|
||||
format!(
|
||||
"direct-codex-failure:v1 stage={} retryable={} summary={};建议:{};{}",
|
||||
let error_code = classify_direct_codex_error(&failure.error);
|
||||
let unified_detail_ref = persist_agent_runtime_error(
|
||||
root,
|
||||
client_turn_id,
|
||||
"direct-codex",
|
||||
failure.stage.id(),
|
||||
error_code,
|
||||
retryable,
|
||||
&summary,
|
||||
recovery_hint,
|
||||
&failure.error,
|
||||
None,
|
||||
serde_json::json!({
|
||||
"legacyDiagnosticWritten": diagnostic_written,
|
||||
}),
|
||||
)
|
||||
.ok()
|
||||
.map(|event| event.detail_ref);
|
||||
format!(
|
||||
"direct-codex-failure:v2 stage={} code={} retryable={} summary={};建议:{};{}{}",
|
||||
failure.stage.id(),
|
||||
error_code,
|
||||
retryable,
|
||||
diagnostic["summary"]
|
||||
.as_str()
|
||||
.unwrap_or("未提供可安全展示的详细原因"),
|
||||
recovery_hint,
|
||||
diagnostics_suffix,
|
||||
unified_detail_ref
|
||||
.map(|path| format!(";详情:{path}"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn persist_direct_codex_failure_context(
|
||||
root: &Path,
|
||||
client_turn_id: &str,
|
||||
error: &str,
|
||||
) -> Result<(), String> {
|
||||
let item = direct_project_local_message_item(
|
||||
"assistant",
|
||||
error,
|
||||
Some(&format!("direct-codex:{client_turn_id}:failure")),
|
||||
)?;
|
||||
append_direct_project_history_item_at(root, &item)
|
||||
}
|
||||
|
||||
fn direct_taonier_art_generation_runtime_context(
|
||||
root: &Path,
|
||||
output_path: &str,
|
||||
@@ -2269,9 +2308,19 @@ fn direct_registered_taonier_slice_paths(root: &Path) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn direct_game_sources_referenced_taonier_assets(root: &Path) -> Vec<String> {
|
||||
let sources = direct_codex_game_outputs(root)
|
||||
let mut source_paths = direct_codex_game_outputs(root)
|
||||
.into_iter()
|
||||
.filter_map(|(relative_path, _, _)| std::fs::read_to_string(root.join(relative_path)).ok())
|
||||
.map(|(relative_path, _, _)| relative_path)
|
||||
.collect::<Vec<_>>();
|
||||
// npm/Phaser projects put the actual scene and loader code below `game/src`.
|
||||
// Keep the canonical output list for manifest projection, but scan the
|
||||
// complete bounded source list for the asset reference contract.
|
||||
source_paths.extend(direct_npm_source_paths(root));
|
||||
source_paths.sort();
|
||||
source_paths.dedup();
|
||||
let sources = source_paths
|
||||
.into_iter()
|
||||
.filter_map(|relative_path| std::fs::read_to_string(root.join(relative_path)).ok())
|
||||
.collect::<Vec<_>>();
|
||||
let mut available_paths = Vec::new();
|
||||
if direct_taonier_art_base_is_valid(root) {
|
||||
@@ -2284,6 +2333,28 @@ fn direct_game_sources_referenced_taonier_assets(root: &Path) -> Vec<String> {
|
||||
available_paths.push(DIRECT_CODEX_SPRITESHEET_ASSET_PATH.to_string());
|
||||
}
|
||||
available_paths.extend(direct_registered_taonier_slice_paths(root));
|
||||
// A project may have a valid, client-registered art-spritesheet at a
|
||||
// project-specific path (for example a generated building sheet). The
|
||||
// fixed canonical package paths above are compatibility candidates only;
|
||||
// the manifest is the authority for additional runtime image identities.
|
||||
if let Ok(manifest) = read_manifest_for_project(root) {
|
||||
available_paths.extend(
|
||||
manifest
|
||||
.assets
|
||||
.into_iter()
|
||||
.filter(|asset| {
|
||||
matches!(
|
||||
asset.kind.as_str(),
|
||||
"art-spritesheet" | "art-spritesheet-slice" | "game-background"
|
||||
) && asset.media_type == "image/png"
|
||||
&& asset.source.kind == GameCreationAppAssetSourceKind::Canvas
|
||||
&& asset.local_path.starts_with("assets/")
|
||||
})
|
||||
.map(|asset| asset.local_path),
|
||||
);
|
||||
}
|
||||
available_paths.sort();
|
||||
available_paths.dedup();
|
||||
available_paths
|
||||
.into_iter()
|
||||
.filter(|path| sources.iter().any(|source| source.contains(path.as_str())))
|
||||
@@ -2783,6 +2854,9 @@ async fn generate_direct_taonier_art_asset_at(
|
||||
asset_label: asset_label.to_string(),
|
||||
replace_existing: root.join(output_path).is_file(),
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let runtime_context =
|
||||
direct_taonier_art_generation_runtime_context(root, output_path, asset_kind)?;
|
||||
@@ -4084,7 +4158,17 @@ async fn run_direct_game_creator_turn_at_with_creation_type_and_emitter(
|
||||
{
|
||||
Ok(reply) => Ok(reply),
|
||||
Err(failure) => {
|
||||
let error = record_direct_codex_turn_failure(root, failure);
|
||||
let error = record_direct_codex_turn_failure(
|
||||
root,
|
||||
failure,
|
||||
turn_emitter.map(|emitter| emitter.turn_id()),
|
||||
);
|
||||
if let Some(emitter) = turn_emitter {
|
||||
// Persist the safe terminal projection so the next DirectProject
|
||||
// turn can answer a diagnostic question from evidence instead of
|
||||
// guessing or starting another playtest.
|
||||
let _ = persist_direct_codex_failure_context(root, emitter.turn_id(), &error);
|
||||
}
|
||||
if let Some(emitter) = turn_emitter {
|
||||
emitter.emit("failed", Some("none"), None);
|
||||
}
|
||||
@@ -6617,26 +6701,27 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn direct_failure_diagnostic_is_redacted_and_persisted_with_a_stable_stage() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
init_local_game_project_at(root.path(), "direct-diagnostic", "直连诊断")
|
||||
.expect("init project");
|
||||
let parent = tempfile::tempdir().expect("temp dir");
|
||||
let root = parent.path().join("project");
|
||||
init_local_game_project_at(&root, "direct-diagnostic", "直连诊断").expect("init project");
|
||||
let error = record_direct_codex_turn_failure(
|
||||
root.path(),
|
||||
&root,
|
||||
DirectCodexTurnFailure::new(
|
||||
DirectCodexFailureStage::ArtPreparation,
|
||||
"读取陶泥儿画布资源失败:https://provider.example/private?token=secret C:\\Users\\private\\project authorization=Bearer secret",
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(error
|
||||
.starts_with("direct-codex-failure:v1 stage=art-preparation retryable=true summary="));
|
||||
.starts_with("direct-codex-failure:v2 stage=art-preparation code=runtime-failure retryable=true summary="));
|
||||
assert!(error.contains("<redacted-url>"), "{error}");
|
||||
assert!(error.contains("<absolute-path>"), "{error}");
|
||||
assert!(!error.contains("authorization=Bearer secret"), "{error}");
|
||||
assert!(!error.contains("?token=secret"), "{error}");
|
||||
assert!(!error.contains("provider.example"), "{error}");
|
||||
|
||||
let diagnostics = root.path().join(".agent/runtime/direct-codex-diagnostics");
|
||||
let diagnostics = root.join(".agent/runtime/direct-codex-diagnostics");
|
||||
let entries = std::fs::read_dir(&diagnostics)
|
||||
.expect("diagnostic directory")
|
||||
.filter_map(Result::ok)
|
||||
@@ -6655,25 +6740,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn direct_failure_diagnostic_marks_project_history_shape_failure_as_not_retryable() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
init_local_game_project_at(root.path(), "direct-diagnostic", "直连诊断")
|
||||
.expect("init project");
|
||||
let parent = tempfile::tempdir().expect("temp dir");
|
||||
let root = parent.path().join("project");
|
||||
init_local_game_project_at(&root, "direct-diagnostic", "直连诊断").expect("init project");
|
||||
let history_path = root
|
||||
.path()
|
||||
.join(".agent/conversations/project.jsonl")
|
||||
.display()
|
||||
.to_string();
|
||||
let error = record_direct_codex_turn_failure(
|
||||
root.path(),
|
||||
&root,
|
||||
DirectCodexTurnFailure::new(
|
||||
DirectCodexFailureStage::CodeGeneration,
|
||||
format!("DirectProject 历史记录类型无效:{history_path}"),
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(
|
||||
error.starts_with(
|
||||
"direct-codex-failure:v1 stage=code-generation retryable=false summary="
|
||||
"direct-codex-failure:v2 stage=code-generation code=runtime-failure retryable=false summary="
|
||||
),
|
||||
"{error}"
|
||||
);
|
||||
@@ -6683,9 +6768,9 @@ mod tests {
|
||||
),
|
||||
"{error}"
|
||||
);
|
||||
assert!(error.ends_with("已保存脱敏项目诊断"), "{error}");
|
||||
assert!(error.contains("已保存脱敏项目诊断"), "{error}");
|
||||
|
||||
let diagnostics = root.path().join(".agent/runtime/direct-codex-diagnostics");
|
||||
let diagnostics = root.join(".agent/runtime/direct-codex-diagnostics");
|
||||
let entries = std::fs::read_dir(&diagnostics)
|
||||
.expect("diagnostic directory")
|
||||
.filter_map(Result::ok)
|
||||
@@ -6699,19 +6784,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn direct_failure_diagnostic_marks_ambiguous_canvas_identity_as_not_retryable() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
init_local_game_project_at(root.path(), "direct-diagnostic", "直连诊断")
|
||||
.expect("init project");
|
||||
let parent = tempfile::tempdir().expect("temp dir");
|
||||
let root = parent.path().join("project");
|
||||
init_local_game_project_at(&root, "direct-diagnostic", "直连诊断").expect("init project");
|
||||
let error = record_direct_codex_turn_failure(
|
||||
root.path(),
|
||||
&root,
|
||||
DirectCodexTurnFailure::new(
|
||||
DirectCodexFailureStage::ArtPreparation,
|
||||
"陶泥儿画布存在多个同源核心图集,身份不唯一,已拒绝恢复",
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(
|
||||
error.contains("stage=art-preparation retryable=false"),
|
||||
error.contains("stage=art-preparation code=runtime-failure retryable=false"),
|
||||
"{error}"
|
||||
);
|
||||
assert!(error.contains("历史画布资源不满足安全恢复条件"), "{error}");
|
||||
@@ -6719,15 +6805,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn direct_failure_diagnostic_keeps_private_credential_storage_failure_actionable() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
init_local_game_project_at(root.path(), "direct-diagnostic", "直连诊断")
|
||||
.expect("init project");
|
||||
let parent = tempfile::tempdir().expect("temp dir");
|
||||
let root = parent.path().join("project");
|
||||
init_local_game_project_at(&root, "direct-diagnostic", "直连诊断").expect("init project");
|
||||
let error = record_direct_codex_turn_failure(
|
||||
root.path(),
|
||||
&root,
|
||||
DirectCodexTurnFailure::new(
|
||||
DirectCodexFailureStage::ArtPreparation,
|
||||
"private-external-editor-credential-storage-preparation-failed: 本机开发者凭据存储目录未安全初始化;未创建远端凭据",
|
||||
),
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(
|
||||
@@ -7827,6 +7914,40 @@ mod tests {
|
||||
.any(|warning| warning.contains("不得猜测切片")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_completion_scans_npm_scene_modules_for_registered_asset_references() {
|
||||
let parent = tempfile::tempdir().expect("temp dir");
|
||||
let root = parent.path().join("project");
|
||||
init_local_game_project_at(&root, "direct-src-runtime", "源码模块素材引用")
|
||||
.expect("init project");
|
||||
register_direct_taonier_art_package_fixture(&root);
|
||||
register_direct_taonier_art_slice_entries_fixture(&root);
|
||||
std::fs::write(
|
||||
root.join("game/package.json"),
|
||||
"{\"scripts\":{\"build\":\"vite build\"}}",
|
||||
)
|
||||
.expect("package");
|
||||
std::fs::write(root.join("game/index.html"), "<!doctype html>").expect("index");
|
||||
std::fs::write(root.join("game/style.css"), "body {}").expect("style");
|
||||
std::fs::write(root.join("game/game.js"), "import './src/scene.js';").expect("entry");
|
||||
std::fs::create_dir_all(root.join("game/src")).expect("src dir");
|
||||
std::fs::write(
|
||||
root.join("game/src/scene.js"),
|
||||
"const player = new Image(); player.src = '/assets/art-spritesheet-slices/player.png';",
|
||||
)
|
||||
.expect("scene");
|
||||
std::fs::write(
|
||||
root.join("assets/art-spritesheet-slices/player.png"),
|
||||
tiny_opaque_png(),
|
||||
)
|
||||
.expect("slice");
|
||||
|
||||
assert_eq!(
|
||||
direct_game_sources_referenced_taonier_assets(&root),
|
||||
vec!["assets/art-spritesheet-slices/player.png".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_output_sync_accepts_trusted_spec_and_background_without_a_historical_spritesheet() {
|
||||
let root = tempfile::tempdir().expect("temp dir");
|
||||
|
||||
@@ -1078,7 +1078,9 @@ fn bridge_attempt(arguments: &Value) -> Result<usize, String> {
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| "工具参数 attempt 必须是 1 到 3 的整数".to_string())?;
|
||||
if !(1..=3).contains(&attempt) {
|
||||
return Err("工具参数 attempt 必须是 1 到 3 的整数".to_string());
|
||||
return Err(format!(
|
||||
"playtest-attempt-limit-exceeded: 本轮试玩最多 3 次,收到 attempt={attempt};请结束试玩并基于最近一次浏览器证据报告结果"
|
||||
));
|
||||
}
|
||||
Ok(attempt as usize)
|
||||
}
|
||||
@@ -2104,6 +2106,9 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
|
||||
"imageSize",
|
||||
"assetName",
|
||||
"outputPath",
|
||||
"sliceMode",
|
||||
"gridX",
|
||||
"gridY",
|
||||
],
|
||||
)?;
|
||||
enforce_project_permission_policy(&state.root, "canvas.asset_generate")?;
|
||||
@@ -2145,6 +2150,44 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| "AI 生成图片".to_string());
|
||||
let output_path = bridge_optional_bounded_string(arguments, "outputPath", 512)?;
|
||||
let slice_mode = arguments
|
||||
.get("sliceMode")
|
||||
.map(|_| bridge_bounded_string(arguments, "sliceMode", 32))
|
||||
.transpose()?;
|
||||
if slice_mode
|
||||
.as_deref()
|
||||
.is_some_and(|mode| !matches!(mode, "connected-components" | "grid"))
|
||||
{
|
||||
return Err("工具参数 sliceMode 只允许 connected-components 或 grid".to_string());
|
||||
}
|
||||
let grid_x = arguments
|
||||
.get("gridX")
|
||||
.map(|_| {
|
||||
arguments
|
||||
.get("gridX")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as u32)
|
||||
.ok_or_else(|| "工具参数 gridX 必须是整数".to_string())
|
||||
})
|
||||
.transpose()?;
|
||||
let grid_y = arguments
|
||||
.get("gridY")
|
||||
.map(|_| {
|
||||
arguments
|
||||
.get("gridY")
|
||||
.and_then(Value::as_u64)
|
||||
.map(|value| value as u32)
|
||||
.ok_or_else(|| "工具参数 gridY 必须是整数".to_string())
|
||||
})
|
||||
.transpose()?;
|
||||
if slice_mode.as_deref() == Some("grid") && (grid_x.is_none() || grid_y.is_none()) {
|
||||
return Err("grid 模式必须同时提供 gridX 与 gridY".to_string());
|
||||
}
|
||||
if grid_x.is_some_and(|value| !(1..=32).contains(&value))
|
||||
|| grid_y.is_some_and(|value| !(1..=32).contains(&value))
|
||||
{
|
||||
return Err("工具参数 gridX/gridY 必须在 1 到 32 之间".to_string());
|
||||
}
|
||||
let options = PlatformArtAssetGenerationOptions {
|
||||
output_path,
|
||||
aspect_ratio,
|
||||
@@ -2153,6 +2196,9 @@ async fn bridge_generate_image(state: &DirectToolBridgeState, arguments: &Value)
|
||||
asset_label: asset_name.clone(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode,
|
||||
grid_x,
|
||||
grid_y,
|
||||
};
|
||||
let _generation_guard = state.image_generation_gate.lock().await;
|
||||
let generated = with_direct_editor_api_credentials(
|
||||
@@ -2564,6 +2610,30 @@ async fn handle_direct_tool_bridge(
|
||||
}
|
||||
_ => bridge_tool_result("未知或未审核的客户端工具".to_string(), Vec::new(), true),
|
||||
};
|
||||
if result.get("isError").and_then(Value::as_bool) == Some(true) {
|
||||
let message = result
|
||||
.pointer("/content/0/text")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("客户端工具执行失败");
|
||||
let code = if message.contains("playtest-attempt-limit-exceeded") {
|
||||
"playtest-attempt-limit-exceeded"
|
||||
} else {
|
||||
"tool-error"
|
||||
};
|
||||
let _ = persist_agent_runtime_error(
|
||||
&state.root,
|
||||
None,
|
||||
"agc-tools",
|
||||
"tool-execution",
|
||||
code,
|
||||
true,
|
||||
message,
|
||||
"查看项目错误诊断后处理",
|
||||
message,
|
||||
None,
|
||||
serde_json::json!({"tool": request.tool}),
|
||||
);
|
||||
}
|
||||
Json(result)
|
||||
}
|
||||
|
||||
|
||||
@@ -244,6 +244,24 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
||||
"type": "string",
|
||||
"maxLength": 512,
|
||||
"description": "可选项目相对输出路径,必须位于 assets/ 且不能覆盖已有文件"
|
||||
},
|
||||
"sliceMode": {
|
||||
"type": "string",
|
||||
"enum": ["connected-components", "grid"],
|
||||
"default": "connected-components",
|
||||
"description": "仅 kind=art-spritesheet 生效:connected-components 按透明像素连通域切分,grid 按 gridX×gridY 网格切分"
|
||||
},
|
||||
"gridX": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32,
|
||||
"description": "grid 模式横向网格数量"
|
||||
},
|
||||
"gridY": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 32,
|
||||
"description": "grid 模式纵向网格数量"
|
||||
}
|
||||
},
|
||||
"required": ["prompt"],
|
||||
@@ -880,7 +898,9 @@ fn tool_attempt(arguments: &Value) -> Result<usize, String> {
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| "工具参数 attempt 必须是 1 到 3 的整数".to_string())?;
|
||||
if !(1..=3).contains(&attempt) {
|
||||
return Err("工具参数 attempt 必须是 1 到 3 的整数".to_string());
|
||||
return Err(format!(
|
||||
"playtest-attempt-limit-exceeded: 本轮试玩最多 3 次,收到 attempt={attempt};请结束试玩并基于最近一次浏览器证据报告结果"
|
||||
));
|
||||
}
|
||||
Ok(attempt as usize)
|
||||
}
|
||||
@@ -1014,6 +1034,9 @@ async fn call_agc_generate_image(arguments: &Value) -> Value {
|
||||
"imageSize",
|
||||
"assetName",
|
||||
"outputPath",
|
||||
"sliceMode",
|
||||
"gridX",
|
||||
"gridY",
|
||||
],
|
||||
) {
|
||||
return mcp_tool_result(error, Vec::new(), true);
|
||||
@@ -1041,6 +1064,7 @@ async fn call_agc_generate_image(arguments: &Value) -> Value {
|
||||
("imageSize", 4),
|
||||
("assetName", DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS),
|
||||
("outputPath", 512),
|
||||
("sliceMode", 32),
|
||||
] {
|
||||
if arguments.get(field).is_some() {
|
||||
if let Err(error) = bounded_tool_string(arguments, field, max_chars) {
|
||||
@@ -2226,6 +2250,10 @@ mod tests {
|
||||
assert!(image_tool["description"]
|
||||
.as_str()
|
||||
.is_some_and(|description| description.contains("不是本工具的限制")));
|
||||
assert_eq!(
|
||||
image_tool["inputSchema"]["properties"]["sliceMode"]["enum"],
|
||||
json!(["connected-components", "grid"])
|
||||
);
|
||||
let edit_tool = specs["tools"]
|
||||
.as_array()
|
||||
.expect("tool array")
|
||||
|
||||
@@ -413,6 +413,9 @@ pub(crate) struct PlatformArtAssetGenerationOptions {
|
||||
pub(crate) asset_label: String,
|
||||
pub(crate) replace_existing: bool,
|
||||
pub(crate) slice_count: Option<usize>,
|
||||
pub(crate) slice_mode: Option<String>,
|
||||
pub(crate) grid_x: Option<u32>,
|
||||
pub(crate) grid_y: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for PlatformArtAssetGenerationOptions {
|
||||
@@ -425,6 +428,9 @@ impl Default for PlatformArtAssetGenerationOptions {
|
||||
asset_label: "AI 游戏首版美术素材".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1574,7 +1580,7 @@ pub(in crate::agent) struct PreparedPlatformArtAssetGeneration {
|
||||
warning: Option<String>,
|
||||
slice_warning: Option<String>,
|
||||
slices: Vec<PreparedPlatformArtAssetSlice>,
|
||||
spritesheet_slice_layout: Option<String>,
|
||||
spritesheet_slice_mode: Option<String>,
|
||||
generation_route: String,
|
||||
generation_kind: String,
|
||||
reference_resource_ids: Vec<String>,
|
||||
@@ -2213,11 +2219,8 @@ pub(crate) async fn generate_platform_art_asset_with_required_slices_at(
|
||||
/// 而任何输入不同(提示词、输出路径、比例、尺寸、类型、标签、严格切片)都是另一个
|
||||
/// 动作,必须各自独立成槽,才能在同一项目里同时在途。
|
||||
///
|
||||
/// **字段集合与取值方式必须与升级前逐字节一致**:升级前遗留账本里持久化的
|
||||
/// `actionFingerprint` 就是这个材料的历史哈希,改动材料会让旧账本无法按精确动作被
|
||||
/// 识别与迁移(见 `adopt_legacy_standalone_platform_art_generation_runtime_state_at`)。
|
||||
/// 已知边界:`slice_count` 不进身份(与升级前一致),仅切片数不同的两条图集请求仍落到
|
||||
/// 同一槽,第二条在账本请求正文校验处失败关闭,不会二次 POST。
|
||||
/// 升级前遗留账本仍由旧材料函数定位;新请求把显式切分模式纳入身份,避免同一图集
|
||||
/// 请求在网格与连通域之间误复用。`slice_count` 继续保持历史兼容语义,不进身份。
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StandalonePlatformArtGenerationFingerprintMaterial<'a> {
|
||||
@@ -2229,6 +2232,9 @@ struct StandalonePlatformArtGenerationFingerprintMaterial<'a> {
|
||||
asset_label: &'a str,
|
||||
replace_existing: bool,
|
||||
require_slices: bool,
|
||||
slice_mode: Option<&'a str>,
|
||||
grid_x: Option<u32>,
|
||||
grid_y: Option<u32>,
|
||||
}
|
||||
|
||||
/// 把输出路径收口成稳定的旧槽材料:空路径与未指定路径都落到 `(automatic-output)`,
|
||||
@@ -2265,6 +2271,9 @@ fn standalone_platform_art_generation_runtime_context(
|
||||
asset_label: &options.asset_label,
|
||||
replace_existing: options.replace_existing,
|
||||
require_slices,
|
||||
slice_mode: options.slice_mode.as_deref(),
|
||||
grid_x: options.grid_x,
|
||||
grid_y: options.grid_y,
|
||||
})
|
||||
.map_err(|error| format!("序列化 standalone 图片生成动作身份失败:{error}"))?;
|
||||
let action_fingerprint = format!("{:x}", Sha256::digest(&identity_bytes));
|
||||
@@ -2811,6 +2820,9 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
"referenceId": reference_id,
|
||||
"iconDescriptions": canonical_art_spritesheet_icon_descriptions(&generation_prompt),
|
||||
"sliceCount": options.slice_count,
|
||||
"sliceMode": options.slice_mode,
|
||||
"gridX": options.grid_x,
|
||||
"gridY": options.grid_y,
|
||||
"screenColor": "auto",
|
||||
"aspectRatio": options.aspect_ratio,
|
||||
"imageSize": options.image_size,
|
||||
@@ -3106,8 +3118,8 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
Vec::new()
|
||||
};
|
||||
let warning = platform_art_generation_warning(generated);
|
||||
let spritesheet_slice_layout = if is_canonical_art_spritesheet {
|
||||
json_string_field(generated, "sliceLayout")
|
||||
let spritesheet_slice_mode = if is_canonical_art_spritesheet {
|
||||
json_string_field(generated, "sliceMode")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -3168,7 +3180,7 @@ pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at
|
||||
warning,
|
||||
slice_warning,
|
||||
slices,
|
||||
spritesheet_slice_layout,
|
||||
spritesheet_slice_mode,
|
||||
generation_route,
|
||||
generation_kind,
|
||||
reference_resource_ids,
|
||||
@@ -6508,7 +6520,7 @@ fn validate_strict_platform_art_spritesheet_contract(
|
||||
task_id: Option<&str>,
|
||||
generation_route: &str,
|
||||
generation_kind: &str,
|
||||
spritesheet_slice_layout: Option<&str>,
|
||||
spritesheet_slice_mode: Option<&str>,
|
||||
reference_resource_ids: &[String],
|
||||
has_transparent_pixels: bool,
|
||||
has_visible_pixels: bool,
|
||||
@@ -6545,7 +6557,7 @@ fn validate_strict_platform_art_spritesheet_contract(
|
||||
{
|
||||
return Err("strict spritesheet 图集生成 route/kind 与严格图集合同不一致".to_string());
|
||||
}
|
||||
let _requested_slice_layout = spritesheet_slice_layout;
|
||||
let _requested_slice_mode = spritesheet_slice_mode;
|
||||
if reference_resource_ids.len() != 1
|
||||
|| reference_resource_ids[0].trim().is_empty()
|
||||
|| reference_resource_ids[0].trim() == resource_id
|
||||
@@ -7307,7 +7319,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
|
||||
warning,
|
||||
mut slice_warning,
|
||||
slices,
|
||||
spritesheet_slice_layout,
|
||||
spritesheet_slice_mode,
|
||||
generation_route,
|
||||
generation_kind,
|
||||
reference_resource_ids,
|
||||
@@ -7326,7 +7338,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
|
||||
task_id.as_deref(),
|
||||
&generation_route,
|
||||
&generation_kind,
|
||||
spritesheet_slice_layout.as_deref(),
|
||||
spritesheet_slice_mode.as_deref(),
|
||||
&reference_resource_ids,
|
||||
spritesheet_has_transparent_pixels,
|
||||
spritesheet_has_visible_pixels,
|
||||
@@ -7901,8 +7913,8 @@ mod canvas_generation_tests {
|
||||
let body = serde_json::json!({
|
||||
"error": {
|
||||
"code": "invalid-request",
|
||||
"field": "sliceLayout",
|
||||
"message": "只支持 grid-2x2;operationId=private-operation-id;api_key=private-key",
|
||||
"field": "sliceMode",
|
||||
"message": "只支持 grid;operationId=private-operation-id;api_key=private-key",
|
||||
},
|
||||
"details": {
|
||||
"path": "C:\\Users\\private\\secret.json",
|
||||
@@ -7911,8 +7923,8 @@ mod canvas_generation_tests {
|
||||
.to_string();
|
||||
let summary = summarize_external_http_error_body(&body).expect("summary");
|
||||
assert!(summary.contains("code=invalid-request"), "{summary}");
|
||||
assert!(summary.contains("field=sliceLayout"), "{summary}");
|
||||
assert!(summary.contains("只支持 grid-2x2"), "{summary}");
|
||||
assert!(summary.contains("field=sliceMode"), "{summary}");
|
||||
assert!(summary.contains("只支持 grid"), "{summary}");
|
||||
assert!(!summary.contains("private-operation-id"), "{summary}");
|
||||
assert!(!summary.contains("private-key"), "{summary}");
|
||||
assert!(!summary.contains("C:\\Users\\private"), "{summary}");
|
||||
@@ -8312,6 +8324,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "手工背景".to_string(),
|
||||
replace_existing: true,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let ordinary =
|
||||
standalone_platform_art_generation_runtime_context("完整生成提示词", &options, false)
|
||||
@@ -9826,7 +9841,7 @@ mod canvas_generation_tests {
|
||||
Some("spritesheet-task"),
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
"icon-spritesheet",
|
||||
Some("grid-2x2"),
|
||||
Some("grid"),
|
||||
&["art-spec-resource".to_string()],
|
||||
true,
|
||||
true,
|
||||
@@ -10325,6 +10340,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "整包规范图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let prompt = "生成同一套整包美术";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -11220,6 +11238,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "整包背景图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let prompt = "保持同一个生成提示词";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -11681,6 +11702,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "游戏统一视觉规范图".to_string(),
|
||||
replace_existing: false,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
};
|
||||
let prompt = "恢复已受理视觉规范图";
|
||||
let generation_prompt = build_platform_art_asset_prompt(prompt, &[], &options);
|
||||
@@ -12287,6 +12311,9 @@ mod canvas_generation_tests {
|
||||
asset_label: "游戏首版核心美术素材".to_string(),
|
||||
replace_existing: true,
|
||||
slice_count: None,
|
||||
slice_mode: None,
|
||||
grid_x: None,
|
||||
grid_y: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12317,7 +12344,7 @@ mod canvas_generation_tests {
|
||||
warning: None,
|
||||
slice_warning: None,
|
||||
slices: Vec::new(),
|
||||
spritesheet_slice_layout: Some("grid-2x2".to_string()),
|
||||
spritesheet_slice_mode: Some("grid".to_string()),
|
||||
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
|
||||
generation_kind: "icon-spritesheet".to_string(),
|
||||
reference_resource_ids: vec!["art-spec-resource".to_string()],
|
||||
@@ -12636,7 +12663,7 @@ mod canvas_generation_tests {
|
||||
warning: None,
|
||||
slice_warning: None,
|
||||
slices,
|
||||
spritesheet_slice_layout: Some("grid-2x2".to_string()),
|
||||
spritesheet_slice_mode: Some("grid".to_string()),
|
||||
generation_route: "/api/external/v1/editor/icon-spritesheets/generations".to_string(),
|
||||
generation_kind: "icon-spritesheet".to_string(),
|
||||
reference_resource_ids: vec!["art-spec-resource".to_string()],
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Shared, project-bound error events for Agent Runtime and DirectProject.
|
||||
//!
|
||||
//! Every caller supplies a safe public summary and a private detail. This
|
||||
//! module is the only persistence boundary for the latter: it redacts project
|
||||
//! paths and credentials before writing a bounded diagnostic sidecar.
|
||||
|
||||
use super::{redact_agent_runtime_error, write_agent_runtime_json_sidecar_with_max_bytes};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_SCHEMA_VERSION: &str = "agent-runtime-error.v1";
|
||||
pub(crate) const AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS: usize = 8 * 1024;
|
||||
|
||||
static ERROR_EVENT_SEQUENCE: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub(crate) struct AgentRuntimeErrorEvent {
|
||||
pub schema_version: &'static str,
|
||||
pub event_id: String,
|
||||
pub client_turn_id: Option<String>,
|
||||
pub source: String,
|
||||
pub stage: String,
|
||||
pub code: String,
|
||||
pub retryable: bool,
|
||||
pub occurred_at_unix_nanos: String,
|
||||
pub elapsed_ms: Option<u64>,
|
||||
pub public_text: String,
|
||||
pub recovery_hint: String,
|
||||
pub detail_ref: String,
|
||||
pub persistence_failed: bool,
|
||||
pub metadata: Value,
|
||||
}
|
||||
|
||||
pub(crate) fn persist_agent_runtime_error(
|
||||
root: &Path,
|
||||
client_turn_id: Option<&str>,
|
||||
source: &str,
|
||||
stage: &str,
|
||||
code: &str,
|
||||
retryable: bool,
|
||||
public_text: &str,
|
||||
recovery_hint: &str,
|
||||
detail: &str,
|
||||
elapsed_ms: Option<u64>,
|
||||
metadata: Value,
|
||||
) -> Result<AgentRuntimeErrorEvent, String> {
|
||||
let occurred_at_unix_nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|error| format!("读取错误事件时间失败:{error}"))?
|
||||
.as_nanos();
|
||||
let sequence = ERROR_EVENT_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
||||
let event_id = format!("error-{occurred_at_unix_nanos}-{sequence}");
|
||||
let detail_ref = format!(".agent/runtime/errors/{event_id}.json");
|
||||
let safe_detail =
|
||||
redact_agent_runtime_error(root, detail, AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS);
|
||||
let diagnostic = serde_json::json!({
|
||||
"schemaVersion": AGENT_RUNTIME_ERROR_SCHEMA_VERSION,
|
||||
"eventId": event_id,
|
||||
"clientTurnId": client_turn_id,
|
||||
"source": source,
|
||||
"stage": stage,
|
||||
"code": code,
|
||||
"retryable": retryable,
|
||||
"occurredAtUnixNanos": occurred_at_unix_nanos.to_string(),
|
||||
"elapsedMs": elapsed_ms,
|
||||
"publicText": public_text,
|
||||
"recoveryHint": recovery_hint,
|
||||
"detail": safe_detail,
|
||||
"metadata": metadata,
|
||||
});
|
||||
write_agent_runtime_json_sidecar_with_max_bytes(
|
||||
root,
|
||||
&detail_ref,
|
||||
"统一 Agent Runtime 错误诊断",
|
||||
&diagnostic,
|
||||
16 * 1024,
|
||||
)?;
|
||||
Ok(AgentRuntimeErrorEvent {
|
||||
schema_version: AGENT_RUNTIME_ERROR_SCHEMA_VERSION,
|
||||
event_id,
|
||||
client_turn_id: client_turn_id.map(str::to_string),
|
||||
source: source.to_string(),
|
||||
stage: stage.to_string(),
|
||||
code: code.to_string(),
|
||||
retryable,
|
||||
occurred_at_unix_nanos: occurred_at_unix_nanos.to_string(),
|
||||
elapsed_ms,
|
||||
public_text: public_text.to_string(),
|
||||
recovery_hint: recovery_hint.to_string(),
|
||||
detail_ref,
|
||||
persistence_failed: false,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn classify_direct_codex_error(error: &str) -> &'static str {
|
||||
let normalized = error.to_ascii_lowercase();
|
||||
if normalized.contains("等待 turn/completed 超时") {
|
||||
"turn-idle-timeout"
|
||||
} else if normalized.contains("达到 directproject 硬上限") {
|
||||
"turn-hard-timeout"
|
||||
} else if normalized.contains("transport closed") || normalized.contains("连接已关闭") {
|
||||
"transport-closed"
|
||||
} else if normalized.contains("playtest-attempt-limit-exceeded") {
|
||||
"playtest-attempt-limit-exceeded"
|
||||
} else if (normalized.contains("tool") || normalized.contains("工具"))
|
||||
&& normalized.contains("参数")
|
||||
{
|
||||
"tool-invalid-arguments"
|
||||
} else if normalized.contains("codex app-server-error:other") {
|
||||
"app-server-other"
|
||||
} else {
|
||||
"runtime-failure"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn error_event_is_bounded_and_redacts_private_detail() {
|
||||
let parent = tempfile::tempdir().expect("temp root");
|
||||
let root = parent.path().join("project");
|
||||
crate::project::init_local_game_project_at(&root, "runtime-error", "错误事件")
|
||||
.expect("init project");
|
||||
let event = persist_agent_runtime_error(
|
||||
&root,
|
||||
Some("turn-123"),
|
||||
"direct-codex",
|
||||
"code-generation",
|
||||
"turn-idle-timeout",
|
||||
true,
|
||||
"本轮没有收到完成事件",
|
||||
"查看诊断后重试",
|
||||
"C:\\Users\\private\\project https://provider.example/a?token=secret",
|
||||
Some(1200),
|
||||
serde_json::json!({"lastEvent":"item/started"}),
|
||||
)
|
||||
.expect("persist event");
|
||||
assert_eq!(event.code, "turn-idle-timeout");
|
||||
let path = root.join(&event.detail_ref);
|
||||
let text = std::fs::read_to_string(path).expect("diagnostic");
|
||||
assert!(text.contains("<absolute-path>"));
|
||||
assert!(text.contains("<redacted-url>"));
|
||||
assert!(!text.contains("token=secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timeout_and_tool_errors_have_distinct_codes() {
|
||||
assert_eq!(
|
||||
classify_direct_codex_error("等待 turn/completed 超时"),
|
||||
"turn-idle-timeout"
|
||||
);
|
||||
assert_eq!(
|
||||
classify_direct_codex_error("达到 DirectProject 硬上限"),
|
||||
"turn-hard-timeout"
|
||||
);
|
||||
assert_eq!(
|
||||
classify_direct_codex_error("工具参数 attempt 必须是 1 到 3 的整数"),
|
||||
"tool-invalid-arguments"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,26 @@ pub(crate) fn append_game_creator_agent_runtime_terminal_public_message_at(
|
||||
error: &str,
|
||||
) -> Result<(), String> {
|
||||
let content = game_creator_agent_runtime_failure_conversation_message(&state.agent_id, error);
|
||||
// Keep the existing conversation projection, but also persist one common
|
||||
// bounded diagnostic event for every Agent Runtime terminal failure. This
|
||||
// makes non-DirectProject failures observable through the same detail API.
|
||||
let _ = persist_agent_runtime_error(
|
||||
root,
|
||||
Some(&state.run_id),
|
||||
"agent-runtime",
|
||||
&state.phase,
|
||||
"agent-runtime-terminal",
|
||||
false,
|
||||
&content,
|
||||
"查看项目错误诊断后处理",
|
||||
error,
|
||||
None,
|
||||
serde_json::json!({
|
||||
"agentId": state.agent_id,
|
||||
"sessionId": state.session_id,
|
||||
"runId": state.run_id,
|
||||
}),
|
||||
);
|
||||
let status = if state.phase == "budget-exhausted" {
|
||||
"budget-exhausted"
|
||||
} else if state.phase == "needs-reconciliation" {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user