Compare commits

..

1 Commits

Author SHA1 Message Date
kdletters b79ee96f7e 补齐容器预览的 AGC Router 密钥说明
- deploy/container/api-server.env.example 增加 AGC 官方 LLM Router 段,列明容器预览必须由 .env.secrets.local 提供的三个密钥
- 【开发运维】Jenkins容器预览部署控制面技术方案 补充预览 secrets 必含密钥与缺失时的失败表现
- 【开发运维】本地开发验证与生产运维 补充容器与 Jenkins 预览路径同样受 Router 启动校验约束
- project-memory pitfalls 补充容器预览缺少 Router 密钥时的排障特征
2026-09-14 19:52:27 +08:00
758 changed files with 12185 additions and 96629 deletions
@@ -32,7 +32,6 @@ 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. - 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. - 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. - 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 requires an explicit `sliceMode` and has no default. Use `sliceMode="grid"` with the `gridX` and `gridY` the requirement actually names (1-32 each) only for equal grid cells or fixed slots; use `sliceMode="connected-components"` for free-form sheets or an open number of subjects, and constrain the count with `sliceCount` instead of inventing grid dimensions. `connected-components` must not carry `gridX`/`gridY`; an omitted, contradictory, or misapplied declaration returns 400 before billing.
- 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. - 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. - 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 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` | | 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` | | 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` | `sliceMode`, `gridX`, `gridY`, `sliceCount`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `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` | | 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` | | 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` | | 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. 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.
`sliceMode` is required and has no default, so every request must state it. Use `"connected-components"` to detect independent opaque regions by alpha connectivity, or `"grid"` with positive `gridX` and `gridY` values (maximum 32 each) only when the requirement names equal grid cells or fixed slots; the dimensions must come from that requirement. `connected-components` must not carry `gridX`/`gridY`, and `sliceCount` constrains the connected-component result instead of expressing a grid. Omitting `sliceMode`, or contradicting the declared mode with grid dimensions, returns 400 before pricing, enqueueing, or any provider call. `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.
## Common Values ## 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. 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`. 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`. `sliceMode` is required and has no default: send `sliceMode: "grid"` with `gridX`/`gridY` only when the requirement itself fixes the slots or names the column/row count, and otherwise send `sliceMode: "connected-components"` (with `sliceCount` when a subject count must be constrained); never invent a grid to express "kinds of assets", and never send `gridX`/`gridY` with `connected-components`. 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.
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 the requirement fixes grid slots, require the response `sliceMode` to match the declared `grid` request and exactly `gridX × gridY` slices before registering the local runtime sheet; a connected-components request is instead judged by its own `sliceCount` or by the requirement, and 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. 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.
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG. Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
@@ -617,7 +617,6 @@ class GenarrativeExternalClient:
self, self,
reference_id: str, reference_id: str,
icon_descriptions: list[str], icon_descriptions: list[str],
slice_mode: str,
**fields: Any, **fields: Any,
) -> Any: ) -> Any:
reference_id = normalize_optional_text(reference_id) reference_id = normalize_optional_text(reference_id)
@@ -626,20 +625,6 @@ class GenarrativeExternalClient:
descriptions = [item.strip() for item in icon_descriptions if item.strip()] descriptions = [item.strip() for item in icon_descriptions if item.strip()]
if not descriptions: if not descriptions:
raise GenarrativeApiError("icon_descriptions must contain at least one non-empty item") raise GenarrativeApiError("icon_descriptions must contain at least one non-empty item")
slice_mode = normalize_optional_text(slice_mode)
if slice_mode not in ("connected-components", "grid"):
raise GenarrativeApiError(
"slice_mode must be declared explicitly as 'connected-components' or 'grid'; the API has no default"
)
grid_x = fields.get("gridX")
grid_y = fields.get("gridY")
if slice_mode == "grid":
if grid_x is None or grid_y is None:
raise GenarrativeApiError("slice_mode='grid' requires both gridX and gridY")
elif grid_x is not None or grid_y is not None:
raise GenarrativeApiError(
"slice_mode='connected-components' must not carry gridX/gridY"
)
label = fields.get("assetLabel", "图标图集") label = fields.get("assetLabel", "图标图集")
self._apply_canvas_session_fields(fields, label, 1024, 1024) self._apply_canvas_session_fields(fields, label, 1024, 1024)
fields.setdefault("screenColor", "auto") fields.setdefault("screenColor", "auto")
@@ -650,7 +635,6 @@ class GenarrativeExternalClient:
**fields, **fields,
"referenceId": reference_id, "referenceId": reference_id,
"iconDescriptions": descriptions, "iconDescriptions": descriptions,
"sliceMode": slice_mode,
}, },
idempotency_key=idempotency_key, idempotency_key=idempotency_key,
) )
@@ -895,7 +879,6 @@ def _self_test() -> None:
client.generate_icon_spritesheet( client.generate_icon_spritesheet(
"editor-resource-spec", "editor-resource-spec",
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"], ["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
"connected-components",
canvasSession=session, canvasSession=session,
assetLabel="贪吃蛇透明图集", assetLabel="贪吃蛇透明图集",
referenceId="must-not-override-explicit-reference", referenceId="must-not-override-explicit-reference",
@@ -904,7 +887,6 @@ def _self_test() -> None:
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations" assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
assert calls[0]["body"]["referenceId"] == "editor-resource-spec" assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
assert calls[0]["body"]["screenColor"] == "auto" assert calls[0]["body"]["screenColor"] == "auto"
assert calls[0]["body"]["sliceMode"] == "connected-components"
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上" assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo" assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
print("self-test ok") print("self-test ok")
+5 -19
View File
@@ -1,8 +1,8 @@
# Server-side OpenAI-compatible LLM endpoint base URL. # Server-side OpenAI-compatible LLM endpoint base URL.
LLM_BASE_URL="https://api.tiantoken.com/v1" LLM_BASE_URL="https://api.vectorengine.cn/v1"
# Server-side API key used by the local Vite proxy. # Server-side API key used by the local Vite proxy.
# Recommended: set `LLM_API_KEY` locally, or use `TIANTOKEN_API_KEY` # Recommended: set `LLM_API_KEY` locally, or use `VECTOR_ENGINE_API_KEY`
# through the Rust api-server proxy. # through the Rust api-server proxy.
# Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy, # Legacy compatibility: `VITE_LLM_API_KEY` is still supported by the proxy,
# but it should not be relied on by browser code. # 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. # Model name for chat completions.
VITE_LLM_MODEL="gpt-5.4-mini" VITE_LLM_MODEL="gpt-5.4-mini"
GENARRATIVE_LLM_PROVIDER="openai-compatible" GENARRATIVE_LLM_PROVIDER="openai-compatible"
GENARRATIVE_LLM_BASE_URL="https://api.tiantoken.com/v1" GENARRATIVE_LLM_BASE_URL="https://api.vectorengine.cn/v1"
GENARRATIVE_LLM_API_KEY="" GENARRATIVE_LLM_API_KEY=""
GENARRATIVE_LLM_MODEL="gpt-5.4-mini" GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
@@ -130,15 +130,10 @@ GENARRATIVE_LLM_MODEL="gpt-5.4-mini"
DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1" DASHSCOPE_BASE_URL="https://dashscope.aliyuncs.com/api/v1"
DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY" DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY"
# Tiantoken LLM and GPT-image-2 / Gemini image generation config. # VectorEngine 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_BASE_URL="https://api.vectorengine.cn"
VECTOR_ENGINE_API_KEY="" VECTOR_ENGINE_API_KEY=""
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS="180000" VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000"
# ElevenLabs editor sound-effect generation is server-side only. # ElevenLabs editor sound-effect generation is server-side only.
ELEVENLABS_BASE_URL="https://api.elevenlabs.io" ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
@@ -158,15 +153,6 @@ ALIYUN_OSS_POST_EXPIRE_SECONDS="600"
ALIYUN_OSS_POST_MAX_SIZE_BYTES="20971520" ALIYUN_OSS_POST_MAX_SIZE_BYTES="20971520"
ALIYUN_OSS_SUCCESS_ACTION_STATUS="200" ALIYUN_OSS_SUCCESS_ACTION_STATUS="200"
# AGC 项目定时快照上传目标。对象只落在服务端私有前缀
# `agc/project-snapshots/v1/{user}/{project}/` 下,客户端直传票据不覆盖该前缀。
# bucket 与凭据可以与资源 bucket 分离;凭据未设置时回退使用 ALIYUN_OSS_ACCESS_KEY_*
# 但 bucket / endpoint 默认指向 AGC 发行 bucket,需要该凭据具备目标 bucket 的 PutObject 权限。
GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_BUCKET="agc-dev"
GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_ENDPOINT="oss-rg-china-mainland.aliyuncs.com"
GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_ACCESS_KEY_ID=""
GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_ACCESS_KEY_SECRET=""
# BgFilter 受限资源 worker。父 api-server / external-generation-worker 与唯一的 # BgFilter 受限资源 worker。父 api-server / external-generation-worker 与唯一的
# `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 进程必须使用同一个内部 Token。 # `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 进程必须使用同一个内部 Token。
# `npm run dev` 与 `npm run dev:api-server` 都会自动带起并验活唯一 worker,不要再开第二个终端重复启动。 # `npm run dev` 与 `npm run dev:api-server` 都会自动带起并验活唯一 worker,不要再开第二个终端重复启动。
-1
View File
@@ -164,7 +164,6 @@ module.exports = {
'server-rs/target-*', 'server-rs/target-*',
'apps/desktop-shell/src-tauri/target', 'apps/desktop-shell/src-tauri/target',
'apps/ai-game-creator-shell/src/features/ui-editor/types/**', 'apps/ai-game-creator-shell/src/features/ui-editor/types/**',
'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
'target', 'target',
'src/main.tsx', 'src/main.tsx',
'src/App.tsx', 'src/App.tsx',
-3
View File
@@ -23,6 +23,3 @@
*.meta text *.meta text
*.anim text *.anim text
*.controller text *.controller text
# Rust ts-rs 生成的共享契约:保留在仓库中供 TS 消费,但不作为手写源文件统计。
packages/shared/src/contracts/generated/** linguist-generated=true
+4 -26
View File
@@ -7,10 +7,6 @@ on:
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
concurrency:
group: project-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions: permissions:
contents: read contents: read
@@ -221,7 +217,8 @@ jobs:
- name: Run AI game creator shell agent-run smoke - name: Run AI game creator shell agent-run smoke
run: npm run check:native-shells:agc-rust-smoke run: npm run check:native-shells:agc-rust-smoke
# AGC 壳依赖的共享 / 平台和编辑器插件 crate 各自预热独立 manifest,再运行对应测试。 # AGC 壳依赖的共享 / 平台 crate 测试用的是 server-rs workspace 与两个无锁独立 crate
# 的 manifest,属另一套依赖图,因此单独一个 job 预热、单独跑。
ai-game-creator-shell-rust-crates: ai-game-creator-shell-rust-crates:
name: AI game creator shell Rust crates name: AI game creator shell Rust crates
runs-on: genarrative-ci runs-on: genarrative-ci
@@ -262,15 +259,13 @@ jobs:
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试 # `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
# 会在测试阶段自己 `Updating crates.io index`crates.io 一抖动整条 job 就红 # 会在测试阶段自己 `Updating crates.io index`crates.io 一抖动整条 job 就红
# (见 #327 / PR #316 run 1950)。 # (见 #327 / PR #316 run 1950)。
# Cocos 插件也使用独立且未提交的锁文件,一并预热。 # 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
# 这些 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内, # 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本 # 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
# 解析,不再触碰 registry index。 # 解析,不再触碰 registry index。
for manifest_path in \ for manifest_path in \
server-rs/crates/agent-runtime-core/Cargo.toml \ server-rs/crates/agent-runtime-core/Cargo.toml \
server-rs/crates/agent-runtime-orchestration/Cargo.toml \ server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml; do
for attempt in $(seq 1 5); do for attempt in $(seq 1 5); do
if cargo fetch \ if cargo fetch \
--target x86_64-unknown-linux-gnu \ --target x86_64-unknown-linux-gnu \
@@ -285,23 +280,6 @@ jobs:
done done
done done
- name: Prepare Unity plugin Rust dependencies
shell: bash
run: |
set -euo pipefail
for attempt in $(seq 1 5); do
if cargo fetch --locked \
--target x86_64-unknown-linux-gnu \
--manifest-path plugins/agc-unity-editor/native/unity-editor-bridge/Cargo.toml; then
break
fi
if [[ "${attempt}" -eq 5 ]]; then
echo 'Unity plugin Cargo dependency fetch failed after 5 attempts.' >&2
exit 1
fi
sleep $((attempt * 2))
done
- name: Run AI game creator shell shared crate gates - name: Run AI game creator shell shared crate gates
run: npm run check:native-shells:agc-rust-crates run: npm run check:native-shells:agc-rust-crates
-10
View File
@@ -41,17 +41,7 @@ temp*build*/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/ /apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json /apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
/apps/ai-game-creator-shell/src-tauri/resources/plugins/ /apps/ai-game-creator-shell/src-tauri/resources/plugins/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/bin/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-path/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-resources/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/codex-package.json
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/manifest.json
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/NOTICE.md
/plugins/agc-cocos-editor/native/payload/ /plugins/agc-cocos-editor/native/payload/
/plugins/agc-unity-editor/dotnet/**/bin/
/plugins/agc-unity-editor/dotnet/**/obj/
/plugins/agc-unity-editor/dotnet/publish/
/plugins/agc-unity-editor/dotnet/native-build/
/apps/ai-game-creator-shell/logs/ /apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/.llm-drafts/ /apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json /apps/ai-game-creator-shell/game-creator.config.local.json
-3
View File
@@ -1,4 +1 @@
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
# 子进程(npm、lint-staged、测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
npm run format:staged npm run format:staged
-3
View File
@@ -1,4 +1 @@
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
npm run check:pre-push-master -- "$@" npm run check:pre-push-master -- "$@"
+1 -10
View File
@@ -1,14 +1,5 @@
{ {
"singleQuote": true, "singleQuote": true,
"semi": true, "semi": true,
"trailingComma": "all", "trailingComma": "all"
"overrides": [
{
"files": "packages/shared/src/contracts/generated/**/*.ts",
"options": {
"printWidth": 1000,
"singleQuote": false
}
}
]
} }
-16
View File
@@ -172,20 +172,6 @@ _Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。 Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI _Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
## 项目开发对话(DirectProject
**项目对话历史**:
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
**运行态事件**:
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
_Avoid_: 进度通知、快照轮询、第二套历史
**聊天投影**:
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
## Relationships ## Relationships
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发** - 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**
@@ -220,5 +206,3 @@ _Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。 - “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。 - “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。 - “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
- “回合进度事件”曾同时指 Direct turn update 与 Thread Manager 运行态事件;已解析为 AGC 项目开发对话只保留 **运行态事件**
- “哪些消息可显示”曾可能由后端历史分页判断;已解析为可见性判断属于 **聊天投影**,后端只按原始条目分页,前端负责跳过不可显示条目并推进分页锚点。
@@ -38,7 +38,6 @@ vi.mock('../api/adminApiClient', () => ({
interface MockIntersectionObserverController { interface MockIntersectionObserverController {
enter: (target: Element) => void; enter: (target: Element) => void;
enterAll: (targets: Element[]) => void;
isObserved: (target: Element) => boolean; isObserved: (target: Element) => boolean;
} }
@@ -107,25 +106,6 @@ function installIntersectionObserverMock(): MockIntersectionObserverController {
); );
}); });
}, },
enterAll(targets) {
act(() => {
for (const target of targets) {
const record = observed.get(target);
if (!record) {
throw new Error('目标缩略图尚未进入 IntersectionObserver');
}
record.callback(
[
{
isIntersecting: true,
target,
} as IntersectionObserverEntry,
],
record.observer,
);
}
});
},
isObserved(target) { isObserved(target) {
return observed.has(target); return observed.has(target);
}, },
@@ -773,10 +753,10 @@ test('后台素材查询为大量同时可见的缩略图持续错峰换签', as
const thumbnails = entries.map((entry) => const thumbnails = entries.map((entry) =>
thumbnailElementForLabel(entry.label), thumbnailElementForLabel(entry.label),
); );
for (const thumbnail of thumbnails) { thumbnails.forEach((thumbnail) => {
expect(observer.isObserved(thumbnail)).toBe(true); expect(observer.isObserved(thumbnail)).toBe(true);
} observer.enter(thumbnail);
observer.enterAll(thumbnails); });
await act(async () => { await act(async () => {
await Promise.resolve(); await Promise.resolve();
}); });
@@ -796,7 +776,7 @@ test('后台素材查询为大量同时可见的缩略图持续错峰换签', as
await vi.advanceTimersByTimeAsync(200); await vi.advanceTimersByTimeAsync(200);
}); });
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(105); expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(105);
}, 10_000); });
test('后台素材查询读取更多后为新进入可视区域的素材换签', async () => { test('后台素材查询读取更多后为新进入可视区域的素材换签', async () => {
const observer = installIntersectionObserverMock(); const observer = installIntersectionObserverMock();
@@ -2,8 +2,6 @@
"schemaVersion": "game-creator-config.v2", "schemaVersion": "game-creator-config.v2",
"agentMode": "codex_app_server", "agentMode": "codex_app_server",
"llm": { "llm": {
"customEnabled": false,
"visibleModels": [],
"apiKey": "", "apiKey": "",
"baseUrl": "https://dev.genarrative.world/gpt/v1", "baseUrl": "https://dev.genarrative.world/gpt/v1",
"model": "gpt-6-astra", "model": "gpt-6-astra",
@@ -15,7 +13,7 @@
"autoCompactTokenLimit": 64000, "autoCompactTokenLimit": 64000,
"toolOutputTokenLimit": 12000, "toolOutputTokenLimit": 12000,
"requestTimeoutMs": 180000, "requestTimeoutMs": 180000,
"maxRetries": 10, "maxRetries": 2,
"retryBackoffMs": 500 "retryBackoffMs": 500
}, },
"agentLlm": {} "agentLlm": {}
+1 -7
View File
@@ -1,7 +1,7 @@
{ {
"name": "@genarrative/ai-game-creator-shell", "name": "@genarrative/ai-game-creator-shell",
"private": true, "private": true,
"version": "0.1.67", "version": "0.1.29",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/start-tauri-dev.mjs", "dev": "node scripts/start-tauri-dev.mjs",
@@ -47,7 +47,6 @@
"@tauri-apps/plugin-dialog": "^2.7.2", "@tauri-apps/plugin-dialog": "^2.7.2",
"@tauri-apps/plugin-http": "^2.5.9", "@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-opener": "~2", "@tauri-apps/plugin-opener": "~2",
"@tauri-apps/plugin-updater": "2.11.0",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"focus-trap-react": "^12.0.3", "focus-trap-react": "^12.0.3",
"lexical": "^0.47.0", "lexical": "^0.47.0",
@@ -58,10 +57,7 @@
"react-colorful": "^5.8.0", "react-colorful": "^5.8.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-window": "^1.8.11",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"three": "^0.184.0",
"vite": "^6.2.0", "vite": "^6.2.0",
"zustand": "^5.0.14" "zustand": "^5.0.14"
}, },
@@ -73,8 +69,6 @@
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/three": "^0.184.1",
"@types/react-window": "^1.8.8",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"typescript": "~5.8.2", "typescript": "~5.8.2",
"vitest": "^0.34.6" "vitest": "^0.34.6"
@@ -676,11 +676,11 @@ async function runSelfTest() {
designFoundationAssetCall?.arguments?.input?.outputPath === designFoundationAssetCall?.arguments?.input?.outputPath ===
'assets/ui-prototype.png' && 'assets/ui-prototype.png' &&
designFoundationAssetCall.arguments.input.aspectRatio === '16:9' && designFoundationAssetCall.arguments.input.aspectRatio === '16:9' &&
designFoundationAssetCall.arguments.input.assetKind === 'ui-design' && designFoundationAssetCall.arguments.input.assetKind === 'ui-prototype' &&
artAssetPlanAssetCall?.arguments?.input?.outputPath === artAssetPlanAssetCall?.arguments?.input?.outputPath ===
'assets/art-spritesheet.png' && 'assets/art-spritesheet.png' &&
artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' && artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' &&
artAssetPlanAssetCall.arguments.input.assetKind === 'icon-spritesheet', artAssetPlanAssetCall.arguments.input.assetKind === 'art-spritesheet',
'self-test-visual-assets-invalid', 'self-test-visual-assets-invalid',
); );
@@ -70,7 +70,7 @@ const requiredFormalArtifactSpecs = [
{ path: 'game/balance.json', kind: 'json' }, { path: 'game/balance.json', kind: 'json' },
{ path: 'assets/manifest.art.json', kind: 'json' }, { path: 'assets/manifest.art.json', kind: 'json' },
{ path: 'assets/manifest.audio.json', kind: 'json' }, { path: 'assets/manifest.audio.json', kind: 'json' },
{ path: 'game/index.html', kind: 'file' }, { path: 'game/index.html', kind: 'game-entry' },
{ path: 'exports/README.md', kind: 'file' }, { path: 'exports/README.md', kind: 'file' },
]; ];
const editorImageArtifactSpecs = [ const editorImageArtifactSpecs = [
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -19,6 +19,6 @@ export function withDefaultCargoFeatures(argv, features) {
export function defaultEditorFeatures(target) { export function defaultEditorFeatures(target) {
return target === 'win32' || target.includes('windows') return target === 'win32' || target.includes('windows')
? ['cocos-editor-execute', 'unity-editor-execute'] ? ['cocos-editor-execute']
: []; : [];
} }
@@ -9,7 +9,7 @@ test('Windows release includes the same editor feature as development', () => {
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'), buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
[ [
'build', 'build',
'--features=cocos-editor-execute,unity-editor-execute', '--features=cocos-editor-execute',
'--target', '--target',
'x86_64-pc-windows-msvc', 'x86_64-pc-windows-msvc',
], ],
@@ -35,12 +35,6 @@ const windowsTauriConfig = JSON.parse(
'utf8', 'utf8',
), ),
); );
const macosTauriConfig = JSON.parse(
fs.readFileSync(
new URL('../src-tauri/tauri.macos.conf.json', import.meta.url),
'utf8',
),
);
const cargoManifestSource = fs.readFileSync( const cargoManifestSource = fs.readFileSync(
new URL('../src-tauri/Cargo.toml', import.meta.url), new URL('../src-tauri/Cargo.toml', import.meta.url),
'utf8', 'utf8',
@@ -119,18 +113,9 @@ const allowedUncalledTauriCommands = [
'chat_with_game_creator_agent', 'chat_with_game_creator_agent',
'check_ui_editor_font_glyph_coverage', 'check_ui_editor_font_glyph_coverage',
'create_ui_design_resource', 'create_ui_design_resource',
// 图片类生成的同步变体:GUI 已改为 `start_local_project_asset_generation` + 项目内任务账本
// (提交即返回、后台生成)。这条命令**没有生产调用方**,只有 Rust 集成测试
// `src/tests/project.rs`)与 `commands.rs` 单测在调;待后续批次删除,或改为转调
// `start_local_project_asset_generation`。
'generate_local_project_asset',
'open_game_creator_launcher_window', 'open_game_creator_launcher_window',
'open_game_creator_workspace_window', 'open_game_creator_workspace_window',
'read_direct_project_conversation', 'read_direct_project_conversation',
// 项目定时快照上传只在 Rust 侧触发(周期定时器 / 工作区窗口关闭)与排障调用;
// 按产品口径不做客户端可见界面,因此同 `open_game_creator_*_window` 一样按 native-only 登记。
'read_local_project_snapshot_state',
'sync_local_project_snapshot',
'reset_design_agent_session', 'reset_design_agent_session',
'stop_local_game_preview_if_matches', 'stop_local_game_preview_if_matches',
'start_game_creator_external_mcp', 'start_game_creator_external_mcp',
@@ -143,6 +128,7 @@ const allowedUncalledTauriCommands = [
'reload_agc_plugin', 'reload_agc_plugin',
'call_agc_plugin', 'call_agc_plugin',
'read_agc_plugin_panel', 'read_agc_plugin_panel',
'set_agc_plugin_project_path',
'set_agc_plugin_enabled', 'set_agc_plugin_enabled',
]; ];
const sourceExtensions = new Set([ const sourceExtensions = new Set([
@@ -1304,7 +1290,7 @@ for (const requiredSource of [
} }
} }
if (tauriConfig.productName !== '陶泥儿') { if (tauriConfig.productName !== 'Genarrative AI Game Creator') {
throw new Error('AI game creator shell productName drifted'); throw new Error('AI game creator shell productName drifted');
} }
@@ -1316,19 +1302,19 @@ const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent', 'design-agent': 'design-agent',
}; };
const expectedBundledWindowsResources = { const expectedBundledWindowsResources = {
'resources/codex/win-x64/bin/codex.exe': 'coding-agent/win-x64/bin/codex.exe', 'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
'resources/codex/win-x64/bin/codex-code-mode-host.exe': 'resources/codex/win-x64/bin/codex-code-mode-host.exe':
'coding-agent/win-x64/bin/codex-code-mode-host.exe', 'codex/win-x64/bin/codex-code-mode-host.exe',
'resources/codex/win-x64/codex-path/rg.exe': 'resources/codex/win-x64/codex-path/rg.exe':
'coding-agent/win-x64/codex-path/rg.exe', 'codex/win-x64/codex-path/rg.exe',
'resources/codex/win-x64/codex-resources/codex-command-runner.exe': 'resources/codex/win-x64/codex-resources/codex-command-runner.exe':
'coding-agent/win-x64/codex-resources/codex-command-runner.exe', 'codex/win-x64/codex-resources/codex-command-runner.exe',
'resources/codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe': 'resources/codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe':
'coding-agent/win-x64/codex-resources/codex-windows-sandbox-setup.exe', 'codex/win-x64/codex-resources/codex-windows-sandbox-setup.exe',
'resources/codex/win-x64/codex-package.json': 'resources/codex/win-x64/codex-package.json':
'coding-agent/win-x64/codex-package.json', 'codex/win-x64/codex-package.json',
'resources/codex/win-x64/NOTICE.md': 'coding-agent/win-x64/NOTICE.md', 'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'coding-agent/win-x64/manifest.json', 'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
'resources/plugins': 'plugins', 'resources/plugins': 'plugins',
}; };
assert.deepEqual( assert.deepEqual(
@@ -1363,37 +1349,6 @@ if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory', 'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
); );
} }
assert.deepEqual(
macosTauriConfig.bundle?.resources,
Object.fromEntries([
...[
'bin/codex',
'bin/codex-code-mode-host',
'codex-path/rg',
'codex-resources/zsh/bin/zsh',
'codex-package.json',
'NOTICE.md',
'manifest.json',
].map((file) => [
`resources/codex/mac-native/${file}`,
`coding-agent/mac-native/${file}`,
]),
['resources/plugins', 'plugins'],
]),
'macOS must bundle the complete native Codex layout and plugin workspace',
);
assert.deepEqual(
macosTauriConfig.plugins?.updater?.endpoints,
[
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/dev-mac/latest.json',
],
'macOS local builds must not use the Windows update channel',
);
assert.equal(
macosTauriConfig.bundle?.macOS?.minimumSystemVersion,
'15.0',
'macOS deployment baseline must cover the bundled native zsh requirement',
);
if (tauriConfig.app?.withGlobalTauri !== true) { if (tauriConfig.app?.withGlobalTauri !== true) {
throw new Error( throw new Error(
@@ -1758,7 +1713,7 @@ for (const snippet of [
'fn append_local_permission_log_at(', 'fn append_local_permission_log_at(',
'"command.auto"', '"command.auto"',
'GameCreationAppPermission::Auto', 'GameCreationAppPermission::Auto',
'fn game_creator_bundled_codex_cli_path', 'GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH',
'validate_game_creator_bundled_codex_cli', 'validate_game_creator_bundled_codex_cli',
'内置 Codex CLI 完整性校验失败', '内置 Codex CLI 完整性校验失败',
]) { ]) {
@@ -1,222 +0,0 @@
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
// 只操作临时复制品;不启动 GUI、不读取开发机凭据、不访问 Provider。
assert.equal(process.platform, 'darwin', '此验证必须在 macOS 执行');
const source = path.resolve(process.argv[2] || '');
assert.ok(
source.endsWith('.app') && fs.statSync(source).isDirectory(),
'请传入 .app 绝对路径',
);
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'agc-macos-bundle-')),
);
const app = path.join(root, '陶泥儿 隔离测试.app');
const home = path.join(root, 'home');
const config = path.join(root, 'config');
const tmp = path.join(root, 'tmp');
const codexHome = path.join(root, 'codex-home');
for (const directory of [home, config, tmp, codexHome]) {
fs.mkdirSync(directory, { mode: 0o700 });
}
const env = {
HOME: home,
PATH: '/usr/bin:/bin',
TMPDIR: tmp,
CODEX_HOME: codexHome,
};
function run(command, args) {
const result = spawnSync(command, args, {
cwd: root,
env,
encoding: 'utf8',
timeout: 30_000,
maxBuffer: 1024 * 1024,
});
assert.ifError(result.error);
return result;
}
async function hashFile(file) {
const hash = createHash('sha256');
for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
return hash.digest('hex');
}
async function handshake(executable) {
const child = spawn(executable, ['app-server'], {
cwd: root,
env,
stdio: ['pipe', 'pipe', 'pipe'],
});
let buffered = '';
let stderrBytes = 0;
try {
await new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error('app-server 初始化超时')),
15_000,
);
const finish = (error) => {
clearTimeout(timer);
if (error) reject(error);
else resolve();
};
child.on('error', finish);
child.on('exit', (code) =>
finish(new Error(`app-server 提前退出 ${code}`)),
);
child.stderr.on('data', (chunk) => {
stderrBytes += chunk.length;
if (stderrBytes > 1024 * 1024)
finish(new Error('app-server stderr 超限'));
});
child.stdout.on('data', (chunk) => {
buffered += chunk.toString('utf8');
if (buffered.length > 1024 * 1024)
return finish(new Error('app-server stdout 超限'));
let end;
while ((end = buffered.indexOf('\n')) >= 0) {
const line = buffered.slice(0, end);
buffered = buffered.slice(end + 1);
try {
const message = JSON.parse(line);
if (message.id !== 1) continue;
assert.ok(message.result?.userAgent, '初始化必须返回真实服务身份');
assert.equal(message.error, undefined);
child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
finish();
} catch (error) {
finish(error);
}
}
});
child.stdin.on('error', finish);
child.stdin.write(
`${JSON.stringify({
id: 1,
method: 'initialize',
params: {
clientInfo: {
name: 'agc_bundle_smoke',
title: 'AGC bundle smoke',
version: '1',
},
capabilities: { experimentalApi: true },
},
})}\n`,
);
});
} finally {
if (child.exitCode === null && child.signalCode === null) {
await new Promise((resolve) => {
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
child.once('exit', () => {
clearTimeout(timer);
resolve();
});
child.kill('SIGTERM');
});
}
}
}
try {
fs.cpSync(source, app, { recursive: true });
const resources = path.join(app, 'Contents/Resources');
const bundle = path.join(resources, 'coding-agent/mac-native');
const executable = path.join(bundle, 'bin/codex');
const main = path.join(
app,
'Contents/MacOS/genarrative-ai-game-creator-shell',
);
const manifest = JSON.parse(
fs.readFileSync(path.join(bundle, 'manifest.json'), 'utf8'),
);
assert.equal(manifest.schemaVersion, 'genarrative-codex-sidecar.v2');
assert.equal(
manifest.platform,
process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64',
);
assert.equal(manifest.version, 'codex-cli 0.147.0');
const components = [
'bin/codex',
'bin/codex-code-mode-host',
'codex-path/rg',
'codex-resources/zsh/bin/zsh',
'codex-package.json',
];
assert.deepEqual(Object.keys(manifest.files).sort(), [...components].sort());
for (const component of components) {
const file = path.join(bundle, component);
assert.equal(await hashFile(file), manifest.files[component], component);
if (component !== 'codex-package.json') {
fs.accessSync(file, fs.constants.X_OK);
const arch = run('/usr/bin/lipo', ['-archs', file]);
assert.equal(arch.status, 0, component);
assert.equal(
arch.stdout.trim(),
process.arch === 'arm64' ? 'arm64' : 'x86_64',
component,
);
}
}
assert.ok(fs.existsSync(path.join(bundle, 'NOTICE.md')));
const plugin = path.join(resources, 'plugins/agc-cocos-editor');
for (const file of [
'plugin.json',
'src/entry.mjs',
'panels/cocos-editor.html',
]) {
assert.ok(fs.existsSync(path.join(plugin, file)), file);
}
const packageFiles = fs.readdirSync(resources, { recursive: true });
assert.ok(
!packageFiles.some((file) =>
/(^|\/)(\.env[^/]*|auth\.json|node_modules|target|\.git)(\/|$)|\.(exe|dll)$/.test(
file,
),
),
);
assert.equal(run(executable, ['--version']).stdout.trim(), manifest.version);
assert.equal(
run(path.join(bundle, 'codex-path/rg'), ['--version']).status,
0,
);
assert.equal(
run(path.join(bundle, 'codex-resources/zsh/bin/zsh'), ['--version']).status,
0,
);
// 使用正式 AGC 查找/校验入口,而非只证明 sidecar 可以独立执行。
const status = run(main, ['--config-dir', config, '--llm-status']);
const statusText = `${status.stdout}\n${status.stderr}`;
assert.ok(!statusText.includes('Codex CLI 未安装'), statusText);
assert.ok(
statusText.includes('authentication-required'),
'隔离账号应仅被登录门禁拒绝',
);
await handshake(executable);
// 临时复制品缺少辅助程序时,正式入口必须拒绝内置程序;PATH 无全局 Codex 可兜底。
fs.renameSync(
path.join(bundle, 'bin/codex-code-mode-host'),
path.join(root, 'saved-code-mode-host'),
);
const broken = run(main, ['--config-dir', config, '--llm-status']);
assert.notEqual(broken.status, 0);
assert.match(`${broken.stdout}\n${broken.stderr}`, /Codex CLI 未安装/);
console.log(
'PASS: 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝',
);
console.log(
'未验证:GUI、真实登录/Provider 对话、Cocos macOS 原生桥接;插件 Node 仍为外部前提',
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
@@ -290,7 +290,7 @@ function deterministicArtManifest(
{ {
id: 'garden-guardians-spritesheet', id: 'garden-guardians-spritesheet',
path: 'assets/art-spritesheet.png', path: 'assets/art-spritesheet.png',
kind: 'icon-spritesheet', kind: 'art-spritesheet',
usage: ['defenders', 'enemies', 'battlefield-ui'], usage: ['defenders', 'enemies', 'battlefield-ui'],
source: 'canvas', source: 'canvas',
status: 'ready', status: 'ready',
@@ -710,7 +710,7 @@ function canvasAssetCall(agentId) {
outputPath: 'assets/ui-prototype.png', outputPath: 'assets/ui-prototype.png',
aspectRatio: '16:9', aspectRatio: '16:9',
imageSize: '2K', imageSize: '2K',
assetKind: 'ui-design', assetKind: 'ui-prototype',
assetLabel: '游戏横屏界面原型图', assetLabel: '游戏横屏界面原型图',
replaceExisting: false, replaceExisting: false,
}); });
@@ -721,10 +721,9 @@ function canvasAssetCall(agentId) {
outputPath: 'assets/art-spritesheet.png', outputPath: 'assets/art-spritesheet.png',
aspectRatio: '1:1', aspectRatio: '1:1',
imageSize: '1K', imageSize: '1K',
assetKind: 'icon-spritesheet', assetKind: 'art-spritesheet',
assetLabel: '游戏首版核心美术素材', assetLabel: '游戏首版核心美术素材',
replaceExisting: false, replaceExisting: false,
sliceMode: 'connected-components',
}); });
} }
@@ -830,12 +829,12 @@ function missingGeneratedVisualAssetObservation(context, agentId) {
}, },
'design-foundation': { 'design-foundation': {
path: 'assets/ui-prototype.png', path: 'assets/ui-prototype.png',
kind: 'ui-design', kind: 'ui-prototype',
summary: '策划界面原型图尚未按正式视觉流程生成并登记,不能完成任务', summary: '策划界面原型图尚未按正式视觉流程生成并登记,不能完成任务',
}, },
'art-asset-plan': { 'art-asset-plan': {
path: 'assets/art-spritesheet.png', path: 'assets/art-spritesheet.png',
kind: 'icon-spritesheet', kind: 'art-spritesheet',
summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务', summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务',
}, },
}[agentId]; }[agentId];
@@ -2667,7 +2666,7 @@ function createDeterministicCanvasFixture(apiKey) {
asset: { asset: {
assetId: `asset-${sliceImageId}`, assetId: `asset-${sliceImageId}`,
assetObjectId: sliceAssetObjectId, assetObjectId: sliceAssetObjectId,
assetKind: 'icon', assetKind: 'art-spritesheet-slice',
projectId, projectId,
taskId, taskId,
}, },
@@ -2692,7 +2691,7 @@ function createDeterministicCanvasFixture(apiKey) {
'deterministic spritesheet fixture', 'deterministic spritesheet fixture',
model: 'deterministic-canvas-v1', model: 'deterministic-canvas-v1',
provider: 'deterministic-loopback', provider: 'deterministic-loopback',
sliceMode: 'connected-components', sliceLayout: 'grid-2x2',
spritesheetResource: { spritesheetResource: {
resourceId, resourceId,
projectId, projectId,
@@ -2707,7 +2706,7 @@ function createDeterministicCanvasFixture(apiKey) {
spritesheetAsset: { spritesheetAsset: {
assetId: `asset-${imageId}`, assetId: `asset-${imageId}`,
assetObjectId, assetObjectId,
assetKind: 'icon-spritesheet', assetKind: 'art-spritesheet',
projectId, projectId,
taskId, taskId,
}, },
@@ -2749,7 +2748,7 @@ function createDeterministicCanvasFixture(apiKey) {
const assetObjectId = `asset-object-${imageId}`; const assetObjectId = `asset-object-${imageId}`;
const resourceId = `resource-${imageId}`; const resourceId = `resource-${imageId}`;
const assetKind = const assetKind =
typeof body?.assetKind === 'string' ? body.assetKind : 'image'; typeof body?.assetKind === 'string' ? body.assetKind : 'game-art';
images.set(imageId, { images.set(imageId, {
...image, ...image,
objectKey, objectKey,

Some files were not shown because too many files have changed in this diff Show More