Compare commits

..

2 Commits

Author SHA1 Message Date
lhk229 dfb7617cbb 补充AGC工作目录创建测试
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m14s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 5m33s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m49s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 5m52s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m39s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m23s
Project CI / Frontend tests (pull_request) Failing after 2m46s
Project CI / Repository checks (pull_request) Failing after 3m29s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m24s
Project CI / Native shell tests (pull_request) Successful in 8m35s
Project CI / Backend tests (pull_request) Successful in 10m52s
覆盖子目录创建、重名和路径分隔符校验
2026-09-15 14:42:10 +08:00
lhk229 fa2a7caa95 新增AGC首页自定义工作目录
新增用户目录创建命令与首页接线

保持普通用户权限并避免ACL和UAC耦合

补充里程碑与实施计划
2026-09-15 14:38:04 +08:00
729 changed files with 11785 additions and 85631 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.
- 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 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.
- 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` | `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` |
| 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.
`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
@@ -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`. `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.
@@ -617,7 +617,6 @@ class GenarrativeExternalClient:
self,
reference_id: str,
icon_descriptions: list[str],
slice_mode: str,
**fields: Any,
) -> Any:
reference_id = normalize_optional_text(reference_id)
@@ -626,20 +625,6 @@ class GenarrativeExternalClient:
descriptions = [item.strip() for item in icon_descriptions if item.strip()]
if not descriptions:
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", "图标图集")
self._apply_canvas_session_fields(fields, label, 1024, 1024)
fields.setdefault("screenColor", "auto")
@@ -650,7 +635,6 @@ class GenarrativeExternalClient:
**fields,
"referenceId": reference_id,
"iconDescriptions": descriptions,
"sliceMode": slice_mode,
},
idempotency_key=idempotency_key,
)
@@ -895,7 +879,6 @@ def _self_test() -> None:
client.generate_icon_spritesheet(
"editor-resource-spec",
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
"connected-components",
canvasSession=session,
assetLabel="贪吃蛇透明图集",
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]["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")
+5 -19
View File
@@ -1,8 +1,8 @@
# 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.
# 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.
# 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.tiantoken.com/v1"
GENARRATIVE_LLM_BASE_URL="https://api.vectorengine.cn/v1"
GENARRATIVE_LLM_API_KEY=""
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_API_KEY="YOUR_DASHSCOPE_API_KEY"
# 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.
# VectorEngine LLM and GPT-image-2 / Gemini image generation config.
VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn"
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_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_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 与唯一的
# `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 进程必须使用同一个内部 Token。
# `npm run dev` 与 `npm run dev:api-server` 都会自动带起并验活唯一 worker,不要再开第二个终端重复启动。
-1
View File
@@ -164,7 +164,6 @@ module.exports = {
'server-rs/target-*',
'apps/desktop-shell/src-tauri/target',
'apps/ai-game-creator-shell/src/features/ui-editor/types/**',
'apps/ai-game-creator-shell/src/features/project-workspace/generated/**',
'target',
'src/main.tsx',
'src/App.tsx',
-3
View File
@@ -23,6 +23,3 @@
*.meta text
*.anim 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:
workflow_dispatch:
concurrency:
group: project-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
@@ -221,7 +217,8 @@ jobs:
- name: Run AI game creator shell agent-run 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:
name: AI game creator shell Rust crates
runs-on: genarrative-ci
@@ -262,15 +259,13 @@ jobs:
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
# 会在测试阶段自己 `Updating crates.io index`crates.io 一抖动整条 job 就红
# (见 #327 / PR #316 run 1950)。
# Cocos 插件也使用独立且未提交的锁文件,一并预热。
# 这些 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
# 解析,不再触碰 registry index。
for manifest_path in \
server-rs/crates/agent-runtime-core/Cargo.toml \
server-rs/crates/agent-runtime-orchestration/Cargo.toml \
plugins/agc-cocos-editor/native/cocos-editor-bridge/Cargo.toml; do
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
for attempt in $(seq 1 5); do
if cargo fetch \
--target x86_64-unknown-linux-gnu \
@@ -285,23 +280,6 @@ jobs:
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
run: npm run check:native-shells:agc-rust-crates
-12
View File
@@ -41,19 +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-package.json
/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
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/darwin-arm64/
/apps/ai-game-creator-shell/src-tauri/resources/codex/mac-native/darwin-x64/
/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/.llm-drafts/
/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
-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 -- "$@"
+1 -10
View File
@@ -1,14 +1,5 @@
{
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"overrides": [
{
"files": "packages/shared/src/contracts/generated/**/*.ts",
"options": {
"printWidth": 1000,
"singleQuote": false
}
}
]
"trailingComma": "all"
}
-16
View File
@@ -172,20 +172,6 @@ _Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
## 项目开发对话(DirectProject
**项目对话历史**:
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
**运行态事件**:
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
_Avoid_: 进度通知、快照轮询、第二套历史
**聊天投影**:
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
## Relationships
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**
@@ -220,5 +206,3 @@ _Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
- “入口闭环”曾可能只指内部 demo 或单个详情 CTA;已解析为 **正式作品入口闭环**,不新增独立专区或活动页。
- “创作编辑”曾可能指多步骤向导或完整编辑器;已解析为 **轻配置编辑流程**,使用单页表单 + 预览卡片完成保存草稿、发布和发布后跳转作品详情。
- “实施顺序”曾可能按 UI 或功能并行发散;已解析为契约/领域规则先行,再做后端存储/API,随后打通最小前端纵切,最后补投影体验与收口验证。
- “回合进度事件”曾同时指 Direct turn update 与 Thread Manager 运行态事件;已解析为 AGC 项目开发对话只保留 **运行态事件**
- “哪些消息可显示”曾可能由后端历史分页判断;已解析为可见性判断属于 **聊天投影**,后端只按原始条目分页,前端负责跳过不可显示条目并推进分页锚点。
@@ -38,7 +38,6 @@ vi.mock('../api/adminApiClient', () => ({
interface MockIntersectionObserverController {
enter: (target: Element) => void;
enterAll: (targets: Element[]) => void;
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) {
return observed.has(target);
},
@@ -773,10 +753,10 @@ test('后台素材查询为大量同时可见的缩略图持续错峰换签', as
const thumbnails = entries.map((entry) =>
thumbnailElementForLabel(entry.label),
);
for (const thumbnail of thumbnails) {
thumbnails.forEach((thumbnail) => {
expect(observer.isObserved(thumbnail)).toBe(true);
}
observer.enterAll(thumbnails);
observer.enter(thumbnail);
});
await act(async () => {
await Promise.resolve();
});
@@ -796,7 +776,7 @@ test('后台素材查询为大量同时可见的缩略图持续错峰换签', as
await vi.advanceTimersByTimeAsync(200);
});
expect(getAdminAssetReadUrl).toHaveBeenCalledTimes(105);
}, 10_000);
});
test('后台素材查询读取更多后为新进入可视区域的素材换签', async () => {
const observer = installIntersectionObserverMock();
@@ -2,8 +2,6 @@
"schemaVersion": "game-creator-config.v2",
"agentMode": "codex_app_server",
"llm": {
"customEnabled": false,
"visibleModels": [],
"apiKey": "",
"baseUrl": "https://dev.genarrative.world/gpt/v1",
"model": "gpt-6-astra",
@@ -15,7 +13,7 @@
"autoCompactTokenLimit": 64000,
"toolOutputTokenLimit": 12000,
"requestTimeoutMs": 180000,
"maxRetries": 10,
"maxRetries": 2,
"retryBackoffMs": 500
},
"agentLlm": {}
+1 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.67",
"version": "0.1.29",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -47,7 +47,6 @@
"@tauri-apps/plugin-dialog": "^2.7.2",
"@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-opener": "~2",
"@tauri-apps/plugin-updater": "2.11.0",
"@vitejs/plugin-react": "^5.0.4",
"focus-trap-react": "^12.0.3",
"lexical": "^0.47.0",
@@ -58,10 +57,7 @@
"react-colorful": "^5.8.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"react-window": "^1.8.11",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"three": "^0.184.0",
"vite": "^6.2.0",
"zustand": "^5.0.14"
},
@@ -73,8 +69,6 @@
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.184.1",
"@types/react-window": "^1.8.8",
"tailwindcss": "^4.1.14",
"typescript": "~5.8.2",
"vitest": "^0.34.6"
@@ -676,11 +676,11 @@ async function runSelfTest() {
designFoundationAssetCall?.arguments?.input?.outputPath ===
'assets/ui-prototype.png' &&
designFoundationAssetCall.arguments.input.aspectRatio === '16:9' &&
designFoundationAssetCall.arguments.input.assetKind === 'ui-design' &&
designFoundationAssetCall.arguments.input.assetKind === 'ui-prototype' &&
artAssetPlanAssetCall?.arguments?.input?.outputPath ===
'assets/art-spritesheet.png' &&
artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' &&
artAssetPlanAssetCall.arguments.input.assetKind === 'icon-spritesheet',
artAssetPlanAssetCall.arguments.input.assetKind === 'art-spritesheet',
'self-test-visual-assets-invalid',
);
@@ -70,7 +70,7 @@ const requiredFormalArtifactSpecs = [
{ path: 'game/balance.json', kind: 'json' },
{ path: 'assets/manifest.art.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' },
];
const editorImageArtifactSpecs = [
@@ -1,218 +0,0 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
generateUpdateManifest,
prepareReleaseVersion,
resolveReleaseContext,
runTauriBuild,
} from './build-release.mjs';
import { readReleaseDryRun, uploadReleaseArtifacts } from './release-oss.mjs';
import {
readUpdaterPubkey,
verifyUpdaterSignature,
} from './verify-updater-signature.mjs';
/**
* AGC macOS 渠道(dev-mac)发布入口:构建 universal 包 → 双架构 smoke → 生成 universal DMG
* → 生成渠道清单 latest.json → 用产物内烘焙的公钥验签 → 按 dry-run 决定是否上传 OSS。
*
* 边界:
* - Apple 签名与公证暂缺,产物为未签名 + 未公证(`--no-sign`),必须显式记录而非静默通过;
* - 更新包签名(TAURI_SIGNING_PRIVATE_KEYminisign)是硬需求:缺了客户端一律拒绝安装,
* 因此构建前要求凭据存在,构建后用内置公钥复核 `.sig` 才允许继续上传;
* - 未通过验签绝不写 OSS:上传顺序为更新包、签名、首装包,全部成功后才覆盖渠道清单指针。
*/
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = path.resolve(appRoot, '../..');
assert.equal(process.platform, 'darwin', '只能在 macOS Agent 执行');
assert.equal(
process.env.JENKINS_URL?.length > 0,
true,
'此入口仅用于 Jenkins 独立工作区',
);
assert.equal(
fs.realpathSync(process.env.WORKSPACE || '.'),
fs.realpathSync(repoRoot),
'必须在 Jenkins workspace 根目录执行',
);
const space = fs.statfsSync(repoRoot);
assert.ok(
space.bavail * space.bsize >= 8 * 1024 ** 3,
'构建前至少需要 8 GiB 可用空间;禁止自动清理开发缓存',
);
// 仅剥离 Apple 签名/公证变量:本节点没有证书,误用只会让构建失败;
// 更新包签名与 OSS 凭据必须保留,它们是本入口发布能力的组成部分。
for (const key of Object.keys(process.env)) {
if (/^APPLE_/u.test(key)) delete process.env[key];
}
assert.ok(
process.env.TAURI_SIGNING_PRIVATE_KEY?.length > 0 ||
process.env.TAURI_SIGNING_PRIVATE_KEY_PATH?.length > 0,
'缺少更新包签名私钥(TAURI_SIGNING_PRIVATE_KEY / _PATH):无签名的更新包会被客户端拒绝,禁止继续',
);
const bucket = process.env.AGC_OSS_BUCKET?.trim() || 'agc-dev';
const endpoint =
process.env.AGC_OSS_ENDPOINT?.trim() || 'oss-rg-china-mainland.aliyuncs.com';
if (!/^[a-z0-9][a-z0-9.-]{1,62}$/u.test(bucket) || /[\r\n\0]/u.test(endpoint)) {
throw new Error('OSS bucket 或 endpoint 配置无效');
}
process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
const dryRun = readReleaseDryRun();
process.env.CARGO_TARGET_DIR = path.join(appRoot, 'src-tauri/target');
const context = resolveReleaseContext(['--target=universal-apple-darwin']);
const version = await prepareReleaseVersion(context);
const args = [
'--target=universal-apple-darwin',
'--bundles',
'app',
'--ci',
'--no-sign',
// 基础配置已开启;这里显式声明,避免被其它配置来源关掉后静默失去更新能力。
'--config',
'{"bundle":{"createUpdaterArtifacts":true}}',
];
const command = (binary, argv, options = {}) =>
execFileSync(binary, argv, { cwd: repoRoot, stdio: 'inherit', ...options });
runTauriBuild(args, context);
const app = path.join(context.bundleRoot, 'macos/陶泥儿.app');
for (const architecture of ['arm64', 'x86_64']) {
command(process.execPath, [
path.join(appRoot, 'scripts/check-macos-bundle.mjs'),
app,
architecture,
'--universal',
]);
}
// DMG 放在 bundle 根目录下:渠道清单的首装包选择会扫描该目录,命名必须匹配 `_<version>_universal.dmg`。
const dmgDirectory = path.join(context.bundleRoot, 'macos');
fs.mkdirSync(dmgDirectory, { recursive: true });
const dmg = path.join(dmgDirectory, `陶泥儿_${version}_universal.dmg`);
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-ci-dmg-'));
try {
command('ditto', [app, path.join(stage, '陶泥儿.app')]);
fs.symlinkSync('/Applications', path.join(stage, 'Applications'));
command('hdiutil', [
'create',
'-volname',
'陶泥儿',
'-srcfolder',
stage,
'-format',
'UDZO',
dmg,
]);
command('hdiutil', ['verify', dmg]);
} finally {
fs.rmSync(stage, { recursive: true, force: true });
}
const release = await generateUpdateManifest(context);
assert.equal(
path.resolve(release.downloadArtifact),
path.resolve(dmg),
'首装包必须锁定本次生成的 universal DMG',
);
// 上传前门禁:用产物里烘焙的公钥复核更新包签名。验不过就停在这里,绝不写 OSS。
const signature = verifyUpdaterSignature({
artifactPath: release.artifact,
signaturePath: `${release.artifact}.sig`,
pubkey: readUpdaterPubkey(),
});
console.log(
`[agc-macos] 更新包签名校验通过:alg=${signature.algorithm}keyId=${signature.keyId}`,
);
const artifacts = path.join(repoRoot, 'artifacts');
// 只清理本 Job 的归档输出,不能把上次 DMG 当成本次成功产物。
fs.rmSync(artifacts, { recursive: true, force: true });
fs.mkdirSync(artifacts, { recursive: true });
const sha256 = (file) => {
const hash = createHash('sha256');
hash.update(fs.readFileSync(file));
return hash.digest('hex');
};
const dmgHash = sha256(dmg);
fs.writeFileSync(`${dmg}.sha256`, `${dmgHash} ${path.basename(dmg)}\n`);
const uploadPlan = uploadReleaseArtifacts(release, {
bucket,
endpoint,
binary: process.env.OSSUTIL_BIN?.trim() || 'ossutil',
accessKeyId: process.env.AGC_OSS_ACCESS_KEY_ID?.trim(),
accessKeySecret: process.env.AGC_OSS_ACCESS_KEY_SECRET,
dryRun,
});
const archived = [
dmg,
`${dmg}.sha256`,
release.manifestPath,
release.notesPath,
`${release.artifact}.sig`,
];
for (const file of archived) {
fs.copyFileSync(file, path.join(artifacts, path.basename(file)));
}
const commit = execFileSync('git', ['rev-parse', 'HEAD'], {
cwd: repoRoot,
encoding: 'utf8',
}).trim();
fs.writeFileSync(
path.join(artifacts, 'build-manifest.json'),
`${JSON.stringify(
{
version,
commit,
target: context.target,
channel: context.channel,
// Apple 签名与公证暂缺:显式记录为未验证项,不静默通过。
appleSigned: false,
notarized: false,
dryRun,
uploaded: !dryRun,
updaterSignature: {
algorithm: signature.algorithm,
keyId: signature.keyId,
verified: true,
},
oss: {
bucket,
endpoint,
latest: `oss://${bucket}/agc/${context.channel}/latest.json`,
objects: uploadPlan.map(({ destination }) => destination),
},
artifacts: {
updater: path.basename(release.artifact),
updaterSha256: sha256(release.artifact),
updaterBytes: fs.statSync(release.artifact).size,
updaterSignature: path.basename(`${release.artifact}.sig`),
firstInstall: path.basename(dmg),
firstInstallSha256: dmgHash,
manifest: 'latest.json',
},
smokes: ['arm64', 'x86_64'],
intelSmoke: process.arch === 'arm64' ? 'Rosetta' : 'native',
},
null,
2,
)}\n`,
);
console.log(
dryRun
? `[agc-macos] dry-run 完成${context.channel} 渠道产物与清单已生成未写入 OSS`
: `[agc-macos] ${context.channel} 渠道更新包签名首装包与清单已上传 OSS`,
);
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) {
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'),
[
'build',
'--features=cocos-editor-execute,unity-editor-execute',
'--features=cocos-editor-execute',
'--target',
'x86_64-pc-windows-msvc',
],
@@ -35,12 +35,6 @@ const windowsTauriConfig = JSON.parse(
'utf8',
),
);
const macosTauriConfig = JSON.parse(
fs.readFileSync(
new URL('../src-tauri/tauri.macos.conf.json', import.meta.url),
'utf8',
),
);
const cargoManifestSource = fs.readFileSync(
new URL('../src-tauri/Cargo.toml', import.meta.url),
'utf8',
@@ -127,10 +121,6 @@ const allowedUncalledTauriCommands = [
'open_game_creator_launcher_window',
'open_game_creator_workspace_window',
'read_direct_project_conversation',
// 项目定时快照上传只在 Rust 侧触发(周期定时器 / 工作区窗口关闭)与排障调用;
// 按产品口径不做客户端可见界面,因此同 `open_game_creator_*_window` 一样按 native-only 登记。
'read_local_project_snapshot_state',
'sync_local_project_snapshot',
'reset_design_agent_session',
'stop_local_game_preview_if_matches',
'start_game_creator_external_mcp',
@@ -143,6 +133,7 @@ const allowedUncalledTauriCommands = [
'reload_agc_plugin',
'call_agc_plugin',
'read_agc_plugin_panel',
'set_agc_plugin_project_path',
'set_agc_plugin_enabled',
];
const sourceExtensions = new Set([
@@ -1304,7 +1295,7 @@ for (const requiredSource of [
}
}
if (tauriConfig.productName !== '陶泥儿') {
if (tauriConfig.productName !== 'Genarrative AI Game Creator') {
throw new Error('AI game creator shell productName drifted');
}
@@ -1316,19 +1307,19 @@ const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent',
};
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':
'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':
'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':
'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':
'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':
'coding-agent/win-x64/codex-package.json',
'resources/codex/win-x64/NOTICE.md': 'coding-agent/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'coding-agent/win-x64/manifest.json',
'codex/win-x64/codex-package.json',
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
'resources/plugins': 'plugins',
};
assert.deepEqual(
@@ -1363,39 +1354,6 @@ if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
);
}
assert.deepEqual(
macosTauriConfig.bundle?.resources,
Object.fromEntries([
...['darwin-arm64', 'darwin-x64'].flatMap((arch) =>
[
'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/${arch}/${file}`,
`coding-agent/mac-native/${arch}/${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) {
throw new Error(
@@ -1760,7 +1718,7 @@ for (const snippet of [
'fn append_local_permission_log_at(',
'"command.auto"',
'GameCreationAppPermission::Auto',
'fn game_creator_bundled_codex_cli_path',
'GAME_CREATOR_BUNDLED_CODEX_CLI_RELATIVE_PATH',
'validate_game_creator_bundled_codex_cli',
'内置 Codex CLI 完整性校验失败',
]) {
@@ -1,250 +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] || '');
const architecture =
process.argv[3] || (process.arch === 'arm64' ? 'arm64' : 'x86_64');
assert.ok(
['arm64', 'x86_64'].includes(architecture),
'架构只接受 arm64 / x86_64',
);
const requireUniversal = process.argv.includes('--universal');
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) {
// 只强制被测应用切片;本机 Xcode 检查工具可能仅提供宿主架构。
const useSlice = command.startsWith(`${app}${path.sep}`);
const result = spawnSync(
useSlice ? '/usr/bin/arch' : command,
useSlice ? [`-${architecture}`, command, ...args] : args,
{
cwd: root,
env,
encoding: 'utf8',
timeout: 120_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 初始化超时')),
120_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 platform = architecture === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
const bundle = path.join(resources, 'coding-agent/mac-native', platform);
const executable = path.join(bundle, 'bin/codex');
const main = path.join(
app,
'Contents/MacOS/genarrative-ai-game-creator-shell',
);
const mainArchitectures = run('/usr/bin/lipo', ['-archs', main]);
assert.equal(mainArchitectures.status, 0);
assert.ok(mainArchitectures.stdout.split(/\s+/).includes(architecture));
if (requireUniversal) {
assert.deepEqual(mainArchitectures.stdout.trim().split(/\s+/).sort(), [
'arm64',
'x86_64',
]);
for (const platform of ['darwin-arm64', 'darwin-x64']) {
assert.ok(
fs.existsSync(
path.join(
resources,
'coding-agent/mac-native',
platform,
'manifest.json',
),
),
);
}
}
const manifest = JSON.parse(
fs.readFileSync(path.join(bundle, 'manifest.json'), 'utf8'),
);
assert.equal(manifest.schemaVersion, 'genarrative-codex-sidecar.v2');
assert.equal(manifest.platform, platform);
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(), architecture, 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 (${architecture}): 隔离安装包资源、架构、摘要、权限、正式 Codex 查找、app-server 握手及缺组件拒绝`,
);
console.log(
'未验证:GUI、真实登录/Provider 对话、Cocos macOS 原生桥接;插件 Node 仍为外部前提',
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}

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