Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd94db73c9 |
@@ -5,7 +5,6 @@
|
|||||||
## 目录约定
|
## 目录约定
|
||||||
|
|
||||||
- `.codex/skills/` 是项目专属 skill 根目录。每个 skill 以目录中的 `SKILL.md` 为入口,配套的参考资料和脚本放在同一目录下。
|
- `.codex/skills/` 是项目专属 skill 根目录。每个 skill 以目录中的 `SKILL.md` 为入口,配套的参考资料和脚本放在同一目录下。
|
||||||
- `spec-driven-development` 负责跨模块、公开契约、SpacetimeDB、AGC/Runtime 和复杂 UI 任务的规范先行与逐里程碑验收;具体规则以 `docs/` 下的 SDD 工作流和模板为准。
|
|
||||||
- `.codex/plugins/` 保存随仓库分发的项目插件资源及其参考资料。当前的 `game-studio` 插件提供浏览器游戏设计、原型、2D/3D 技术栈、素材管线和 playtest 工作流;是否启用遵循当前 Codex 的插件加载机制,不依赖旧工具的环境变量或个人配置脚本。
|
- `.codex/plugins/` 保存随仓库分发的项目插件资源及其参考资料。当前的 `game-studio` 插件提供浏览器游戏设计、原型、2D/3D 技术栈、素材管线和 playtest 工作流;是否启用遵循当前 Codex 的插件加载机制,不依赖旧工具的环境变量或个人配置脚本。
|
||||||
- `.codex/hooks/`、`.codex/environments/` 等目录保存项目工具链所需的 hooks 和环境模板;它们不替代项目代码中的运行时配置。
|
- `.codex/hooks/`、`.codex/environments/` 等目录保存项目工具链所需的 hooks 和环境模板;它们不替代项目代码中的运行时配置。
|
||||||
- 长期有效的产品、架构、接口、排障和协作知识统一放在 `docs/` 与 `docs/project-memory/`,不复制到本目录。
|
- 长期有效的产品、架构、接口、排障和协作知识统一放在 `docs/` 与 `docs/project-memory/`,不复制到本目录。
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
name: spec-driven-development
|
|
||||||
description: 在 Genarrative 中处理跨模块功能、公开 API/DTO、SpacetimeDB schema、AGC/Runtime 或复杂 UI 状态链路时,按主规范、里程碑规范、单里程碑实现计划和验收证据推进;小型局部修复不触发。
|
|
||||||
license: MIT
|
|
||||||
metadata:
|
|
||||||
codex:
|
|
||||||
tags: [SDD, 规范驱动开发, 主规范, 里程碑, 验收]
|
|
||||||
---
|
|
||||||
|
|
||||||
# SDD 规范驱动开发
|
|
||||||
|
|
||||||
本 skill 把 Genarrative 的复杂任务路由到规范先行、单里程碑交付的工作流。权威规则和模板分别见:
|
|
||||||
|
|
||||||
- `docs/【协作规范】规范驱动开发工作流-2026-09-12.md`
|
|
||||||
- `docs/project-memory/shared-memory/【模板】规范驱动开发主规范与里程碑模板-2026-09-12.md`
|
|
||||||
|
|
||||||
## 触发条件
|
|
||||||
|
|
||||||
使用本 skill 的任务包括:
|
|
||||||
|
|
||||||
- 跨前端、`api-server`、`platform-*`、`spacetime-client`、`spacetime-module` 或 `module-*` 的功能。
|
|
||||||
- `/api/external/v1`、共享 DTO、OpenAPI、SpacetimeDB schema、迁移或跨版本重放合同变化。
|
|
||||||
- AGC / DirectProject、Agent Runtime、工具白名单、审批、持久化恢复或资源工作流变化。
|
|
||||||
- 复杂 UI 状态链路、入口/页面生命周期变化,或需要多个独立验收面的功能。
|
|
||||||
|
|
||||||
文案、单文件局部修复、无行为变化重构和一次性诊断继续使用轻量流程;执行中一旦触及公开行为或跨模块合同,切换到本 skill。
|
|
||||||
|
|
||||||
## 执行要求
|
|
||||||
|
|
||||||
1. 检查工作树,读取当前专题、源码、测试、契约和历史决策;写清交付结果、验收判据和不做项。
|
|
||||||
2. 找到或更新唯一主规范。主规范描述行为合同、边界、非目标、兼容/迁移和证据,不绑定实现细节。
|
|
||||||
3. 在 `docs/project-memory/plans/` 创建一个或多个 `【里程碑】中文标题-YYYY-MM-DD.md`,填写 `Version`、`Status`、`Date`、`Parent Spec`、范围、依赖和验收标准;评审通过前不写业务代码。
|
|
||||||
4. 每次只选择一个已评审里程碑,创建对应的 `【实施计划】中文标题-YYYY-MM-DD.md`,明确修改边界、顺序、验证命令、风险和回滚点。
|
|
||||||
5. 发现行为需要变化时,严格按 `主规范 → 尚未实现的里程碑规范 → 当前实现计划 → 代码与测试` 更新。
|
|
||||||
6. 完成一个里程碑后,提供主规范/里程碑逐条对照、自动化验证、必要运行时 smoke、边界验证和未验证项;未验收不得推进下一个里程碑。
|
|
||||||
7. 全部验收通过后,将持久结论合并回主规范,删除已完成的临时计划,并检查文档、契约、测试和提交边界一致。
|
|
||||||
|
|
||||||
## 约束
|
|
||||||
|
|
||||||
- 里程碑和实现计划是开发期协调文件,完成或取消后删除;阶段编号和临时名称不得进入产品代码、用户文档、测试名称或提交标题。
|
|
||||||
- 活动计划可以提交以便团队同步,但不得把一次性实现步骤写进长期规范。
|
|
||||||
- 继续遵守现有 API、SpacetimeDB、AGC、编码和中文文档约束;本 skill 不替代专题门禁。
|
|
||||||
- 文档任务至少运行 `npm run check:doc-index`、`npm run check:encoding` 和 `git diff --check`;代码任务再运行范围匹配的测试、类型检查、schema/OpenAPI 或运行时 smoke。
|
|
||||||
+5
-19
@@ -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,不要再开第二个终端重复启动。
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
+52
-338
@@ -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
|
||||||
|
|
||||||
@@ -31,28 +27,9 @@ env:
|
|||||||
RUSTC_WRAPPER: ''
|
RUSTC_WRAPPER: ''
|
||||||
CARGO_BUILD_RUSTC_WRAPPER: ''
|
CARGO_BUILD_RUSTC_WRAPPER: ''
|
||||||
|
|
||||||
# job 声明顺序就是 runner 领取顺序,因此把最长尾的客户端 Rust 门禁排在前面,
|
|
||||||
# 让它在最少的等待下占用并发槽位;其余 job 按时长递减排列。
|
|
||||||
#
|
|
||||||
# 客户端(微信壳 / Expo 移动壳 / Tauri 桌面壳 / AI 游戏创作壳)门禁原先全部串在
|
|
||||||
# `Native shell tests` 一个 job 里,实测 18 分 37 秒。现在按门禁组拆成一个分组一个 job:
|
|
||||||
# `Native shell tests`(契约 + H5 / 微信 / 移动 / 桌面壳门禁 + 发布构建 smoke)、
|
|
||||||
# `AI game creator shell web tests`(typecheck + 壳内测试)、`AI game creator shell
|
|
||||||
# Rust shard 1/4` 到 `4/4`(AGC 壳 bin 单测按名单分 4 片)、`AI game creator shell
|
|
||||||
# Rust smoke`(agent-run smoke)与 `AI game creator shell Rust crates`(AGC 壳依赖的
|
|
||||||
# 共享 / 平台 crate 测试)。各自的命令与拆分前逐一对应,本地
|
|
||||||
# `npm run check:native-shells` 仍是同一条串行序列。
|
|
||||||
#
|
|
||||||
# AGC 壳的 bin 单测(2466 条)按名单分 4 片、一片一个 job:片内保持 `--test-threads=1`
|
|
||||||
# (当年线程并行会互相干扰的是进程内后台锁与异步终态),片与片之间靠 job 级并发摊开。
|
|
||||||
# 不要改回「一个 job 里多进程并行这几片」:同一容器内它们会争抢共享 HOME、target 与固定
|
|
||||||
# 临时路径,实测比整套串行还慢。每个分片 job 都会自校验「片并集等于全集且互斥」。
|
|
||||||
jobs:
|
jobs:
|
||||||
# AGC 壳自身的 Rust bin 单测分片,4 片各自独立 job 并发执行、片内仍保持
|
repository-checks:
|
||||||
# `--test-threads=1`。这里不装 npm 依赖:壳 Rust 门禁只用 cargo 与 node 内建模块,
|
name: Repository checks
|
||||||
# 也只需要 AGC 壳自己那份锁定依赖。
|
|
||||||
ai-game-creator-shell-rust-shard-1:
|
|
||||||
name: AI game creator shell Rust shard 1/4
|
|
||||||
runs-on: genarrative-ci
|
runs-on: genarrative-ci
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout full history from Gitea
|
- name: Checkout full history from Gitea
|
||||||
@@ -64,228 +41,76 @@ jobs:
|
|||||||
- name: Validate preinstalled CI job image and sandbox
|
- name: Validate preinstalled CI job image and sandbox
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||||
|
|
||||||
- name: Prepare AI game creator shell Rust dependencies
|
- name: Resolve comparison base
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# AGC 壳有独立 Cargo.lock,其 path 依赖已含 platform-llm、platform-agent、
|
base_ref="$(node -e '
|
||||||
# agent-runtime-core 与 shared-contracts,因此只锁这一份 manifest 就能覆盖壳测试
|
const fs = require("node:fs");
|
||||||
# 与 smoke 的全部第三方依赖;server-rs 那次预热归 crate 级 job,不在这里重复。
|
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||||||
for attempt in $(seq 1 5); do
|
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
|
||||||
if cargo fetch --locked \
|
')"
|
||||||
--target x86_64-unknown-linux-gnu \
|
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
|
||||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
|
||||||
break
|
echo "comparison base commit is unavailable: ${base_ref}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
|
||||||
fi
|
fi
|
||||||
if [[ "${attempt}" -eq 5 ]]; then
|
resolved_base_ref="$(git rev-parse --verify "${base_ref}^{commit}" 2>/dev/null || true)"
|
||||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
head_ref="$(git rev-parse HEAD)"
|
||||||
|
if [[ "${resolved_base_ref}" == "${head_ref}" ]]; then
|
||||||
|
resolved_base_ref="$(git rev-parse --verify HEAD^ 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
if [[ -z "${resolved_base_ref}" ]]; then
|
||||||
|
echo 'comparison base must resolve to a commit distinct from HEAD.' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sleep $((attempt * 2))
|
base_ref="${resolved_base_ref}"
|
||||||
done
|
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
|
||||||
|
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
|
||||||
|
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
- name: Run AI game creator shell Rust shard 1/4
|
- name: Install npm dependencies
|
||||||
run: npm run check:native-shells:agc-rust-shard-1
|
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||||
|
|
||||||
ai-game-creator-shell-rust-shard-2:
|
- name: Run repository checks
|
||||||
name: AI game creator shell Rust shard 2/4
|
run: npm run check:repository-ci
|
||||||
|
|
||||||
|
frontend-tests:
|
||||||
|
name: Frontend tests
|
||||||
runs-on: genarrative-ci
|
runs-on: genarrative-ci
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout full history from Gitea
|
- name: Checkout source from Gitea
|
||||||
env:
|
env:
|
||||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
|
||||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
||||||
run: genarrative-gitea-checkout
|
run: genarrative-gitea-checkout
|
||||||
|
|
||||||
- name: Validate preinstalled CI job image and sandbox
|
- name: Validate preinstalled CI job image and sandbox
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
||||||
|
|
||||||
- name: Prepare AI game creator shell Rust dependencies
|
- name: Install npm dependencies
|
||||||
shell: bash
|
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
for attempt in $(seq 1 5); do
|
|
||||||
if cargo fetch --locked \
|
|
||||||
--target x86_64-unknown-linux-gnu \
|
|
||||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ "${attempt}" -eq 5 ]]; then
|
|
||||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep $((attempt * 2))
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Run AI game creator shell Rust shard 2/4
|
- name: Run frontend and script tests
|
||||||
run: npm run check:native-shells:agc-rust-shard-2
|
run: npm run test
|
||||||
|
|
||||||
ai-game-creator-shell-rust-shard-3:
|
- name: Run BgFilter worker smoke harness tests
|
||||||
name: AI game creator shell Rust shard 3/4
|
run: npm run bgfilter-worker:smoke-test
|
||||||
runs-on: genarrative-ci
|
|
||||||
steps:
|
|
||||||
- name: Checkout full history from Gitea
|
|
||||||
env:
|
|
||||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
|
||||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: genarrative-gitea-checkout
|
|
||||||
|
|
||||||
- name: Validate preinstalled CI job image and sandbox
|
- name: Validate production health patrol behavior
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
run: npm run check:production-health-patrol
|
||||||
|
|
||||||
- name: Prepare AI game creator shell Rust dependencies
|
- name: Validate production API release behavior
|
||||||
shell: bash
|
run: npm run check:production-api-release
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
for attempt in $(seq 1 5); do
|
|
||||||
if cargo fetch --locked \
|
|
||||||
--target x86_64-unknown-linux-gnu \
|
|
||||||
--manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ "${attempt}" -eq 5 ]]; then
|
|
||||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep $((attempt * 2))
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Run AI game creator shell Rust shard 3/4
|
- name: Validate production API deploy behavior
|
||||||
run: npm run check:native-shells:agc-rust-shard-3
|
run: npm run check:production-api-deploy
|
||||||
|
|
||||||
ai-game-creator-shell-rust-shard-4:
|
|
||||||
name: AI game creator shell Rust shard 4/4
|
|
||||||
runs-on: genarrative-ci
|
|
||||||
steps:
|
|
||||||
- name: Checkout full history from Gitea
|
|
||||||
env:
|
|
||||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
|
||||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: genarrative-gitea-checkout
|
|
||||||
|
|
||||||
- name: Validate preinstalled CI job image and sandbox
|
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
|
||||||
|
|
||||||
- name: Prepare AI game creator shell 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 apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ "${attempt}" -eq 5 ]]; then
|
|
||||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep $((attempt * 2))
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Run AI game creator shell Rust shard 4/4
|
|
||||||
run: npm run check:native-shells:agc-rust-shard-4
|
|
||||||
|
|
||||||
# agent-run smoke 会 spawn `cargo run`(走壳自己的 manifest),同样不装 npm 依赖,
|
|
||||||
# 单独一个 job,免得把已经压到 4 分钟级的片 job 拖长。
|
|
||||||
ai-game-creator-shell-rust-smoke:
|
|
||||||
name: AI game creator shell Rust smoke
|
|
||||||
runs-on: genarrative-ci
|
|
||||||
steps:
|
|
||||||
- name: Checkout full history from Gitea
|
|
||||||
env:
|
|
||||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
|
||||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: genarrative-gitea-checkout
|
|
||||||
|
|
||||||
- name: Validate preinstalled CI job image and sandbox
|
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
|
||||||
|
|
||||||
- name: Prepare AI game creator shell 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 apps/ai-game-creator-shell/src-tauri/Cargo.toml; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ "${attempt}" -eq 5 ]]; then
|
|
||||||
echo 'AI game creator shell Cargo dependency fetch failed after 5 attempts.' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep $((attempt * 2))
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Run AI game creator shell agent-run smoke
|
|
||||||
run: npm run check:native-shells:agc-rust-smoke
|
|
||||||
|
|
||||||
# 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
|
|
||||||
steps:
|
|
||||||
- name: Checkout full history from Gitea
|
|
||||||
env:
|
|
||||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
|
||||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: genarrative-gitea-checkout
|
|
||||||
|
|
||||||
- name: Validate preinstalled CI job image and sandbox
|
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
|
||||||
|
|
||||||
- name: Prepare server-rs 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 server-rs/Cargo.toml; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ "${attempt}" -eq 5 ]]; then
|
|
||||||
echo 'server-rs Cargo dependency fetch failed after 5 attempts.' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep $((attempt * 2))
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Prepare standalone Rust crate dependencies
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
# agent-runtime-core / agent-runtime-orchestration 被 server-rs/Cargo.toml 的
|
|
||||||
# exclude 排除,不参与上面的 workspace 锁文件,因此上面那次锁定 fetch 覆盖不到它们;
|
|
||||||
# 而 `npm run ai-game-creator-shell:check:rust:crates` 会用
|
|
||||||
# `cargo test --manifest-path` 单独跑这两个 crate。不在这里预热的话,这两条测试
|
|
||||||
# 会在测试阶段自己 `Updating crates.io index`,crates.io 一抖动整条 job 就红
|
|
||||||
# (见 #327 / PR #316 run 1950)。
|
|
||||||
# 两个 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; do
|
|
||||||
for attempt in $(seq 1 5); do
|
|
||||||
if cargo fetch \
|
|
||||||
--target x86_64-unknown-linux-gnu \
|
|
||||||
--manifest-path "${manifest_path}"; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [[ "${attempt}" -eq 5 ]]; then
|
|
||||||
echo "standalone crate dependency fetch failed after 5 attempts: ${manifest_path}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep $((attempt * 2))
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Run AI game creator shell shared crate gates
|
|
||||||
run: npm run check:native-shells:agc-rust-crates
|
|
||||||
|
|
||||||
backend-tests:
|
backend-tests:
|
||||||
name: Backend tests
|
name: Backend tests
|
||||||
@@ -369,8 +194,6 @@ jobs:
|
|||||||
- name: Check SpacetimeDB module
|
- name: Check SpacetimeDB module
|
||||||
run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml
|
run: cargo check --locked -p spacetime-module --manifest-path server-rs/Cargo.toml
|
||||||
|
|
||||||
# 客户端的壳级与契约门禁:静态契约断言、H5 / 微信 / 移动 / 桌面壳运行时门禁,
|
|
||||||
# 以及依赖发布产物的构建 smoke。
|
|
||||||
native-shell-tests:
|
native-shell-tests:
|
||||||
name: Native shell tests
|
name: Native shell tests
|
||||||
runs-on: genarrative-ci
|
runs-on: genarrative-ci
|
||||||
@@ -392,6 +215,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
for manifest_path in \
|
for manifest_path in \
|
||||||
|
server-rs/Cargo.toml \
|
||||||
apps/desktop-shell/src-tauri/Cargo.toml \
|
apps/desktop-shell/src-tauri/Cargo.toml \
|
||||||
apps/ai-game-creator-shell/src-tauri/Cargo.toml; do
|
apps/ai-game-creator-shell/src-tauri/Cargo.toml; do
|
||||||
for attempt in $(seq 1 5); do
|
for attempt in $(seq 1 5); do
|
||||||
@@ -408,118 +232,8 @@ jobs:
|
|||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Run native shell contract gates
|
|
||||||
run: npm run check:native-shells:contract
|
|
||||||
|
|
||||||
- name: Run native shell gates
|
- name: Run native shell gates
|
||||||
run: npm run check:native-shells:shells
|
run: npm run check:native-shells
|
||||||
|
|
||||||
- name: Run native shell release build smoke
|
|
||||||
run: npm run check:native-shells:release
|
|
||||||
|
|
||||||
- name: Ensure native lockfiles are unchanged
|
- name: Ensure native lockfiles are unchanged
|
||||||
run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock apps/ai-game-creator-shell/src-tauri/Cargo.lock
|
run: git diff --exit-code -- apps/desktop-shell/src-tauri/Cargo.lock apps/ai-game-creator-shell/src-tauri/Cargo.lock
|
||||||
|
|
||||||
frontend-tests:
|
|
||||||
name: Frontend tests
|
|
||||||
runs-on: genarrative-ci
|
|
||||||
steps:
|
|
||||||
- name: Checkout source from Gitea
|
|
||||||
env:
|
|
||||||
GENARRATIVE_GITEA_FETCH_DEPTH: '1'
|
|
||||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: genarrative-gitea-checkout
|
|
||||||
|
|
||||||
- name: Validate preinstalled CI job image and sandbox
|
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
|
||||||
|
|
||||||
- name: Install npm dependencies
|
|
||||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
|
||||||
|
|
||||||
- name: Run frontend and script tests
|
|
||||||
run: npm run test
|
|
||||||
|
|
||||||
- name: Run BgFilter worker smoke harness tests
|
|
||||||
run: npm run bgfilter-worker:smoke-test
|
|
||||||
|
|
||||||
- name: Validate production health patrol behavior
|
|
||||||
run: npm run check:production-health-patrol
|
|
||||||
|
|
||||||
- name: Validate production API release behavior
|
|
||||||
run: npm run check:production-api-release
|
|
||||||
|
|
||||||
- name: Validate production API deploy behavior
|
|
||||||
run: npm run check:production-api-deploy
|
|
||||||
|
|
||||||
repository-checks:
|
|
||||||
name: Repository checks
|
|
||||||
runs-on: genarrative-ci
|
|
||||||
steps:
|
|
||||||
- name: Checkout full history from Gitea
|
|
||||||
env:
|
|
||||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
|
||||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: genarrative-gitea-checkout
|
|
||||||
|
|
||||||
- name: Validate preinstalled CI job image and sandbox
|
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
|
||||||
|
|
||||||
- name: Resolve comparison base
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
base_ref="$(node -e '
|
|
||||||
const fs = require("node:fs");
|
|
||||||
const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
|
||||||
process.stdout.write(event.pull_request?.base?.sha ?? event.before ?? "");
|
|
||||||
')"
|
|
||||||
if [[ -n "${base_ref}" && ! "${base_ref}" =~ ^0+$ ]]; then
|
|
||||||
git cat-file -e "${base_ref}^{commit}" 2>/dev/null || {
|
|
||||||
echo "comparison base commit is unavailable: ${base_ref}" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
else
|
|
||||||
base_ref="$(git merge-base HEAD origin/master 2>/dev/null || git rev-parse HEAD)"
|
|
||||||
fi
|
|
||||||
resolved_base_ref="$(git rev-parse --verify "${base_ref}^{commit}" 2>/dev/null || true)"
|
|
||||||
head_ref="$(git rev-parse HEAD)"
|
|
||||||
if [[ "${resolved_base_ref}" == "${head_ref}" ]]; then
|
|
||||||
resolved_base_ref="$(git rev-parse --verify HEAD^ 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
if [[ -z "${resolved_base_ref}" ]]; then
|
|
||||||
echo 'comparison base must resolve to a commit distinct from HEAD.' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
base_ref="${resolved_base_ref}"
|
|
||||||
if [[ "${GITHUB_EVENT_NAME:-}" == 'pull_request' ]] \
|
|
||||||
&& ! git merge-base --is-ancestor "${base_ref}" HEAD; then
|
|
||||||
echo 'pull request head does not contain the latest base commit; update the branch and rerun CI.' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "SPACETIME_SCHEMA_BASE_REF=${base_ref}" >> "${GITHUB_ENV}"
|
|
||||||
|
|
||||||
- name: Install npm dependencies
|
|
||||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
|
||||||
|
|
||||||
- name: Run repository checks
|
|
||||||
run: npm run check:repository-ci
|
|
||||||
|
|
||||||
# 客户端的 AI 游戏创作壳前端门禁:typecheck 与壳内测试,不触碰 Cargo。
|
|
||||||
ai-game-creator-shell-web-tests:
|
|
||||||
name: AI game creator shell web tests
|
|
||||||
runs-on: genarrative-ci
|
|
||||||
steps:
|
|
||||||
- name: Checkout full history from Gitea
|
|
||||||
env:
|
|
||||||
GENARRATIVE_GITEA_FETCH_DEPTH: '0'
|
|
||||||
GENARRATIVE_GITEA_TOKEN: ${{ github.token }}
|
|
||||||
run: genarrative-gitea-checkout
|
|
||||||
|
|
||||||
- name: Validate preinstalled CI job image and sandbox
|
|
||||||
run: GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1 bash scripts/check-gitea-ci-job-image.sh
|
|
||||||
|
|
||||||
- name: Install npm dependencies
|
|
||||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
|
||||||
|
|
||||||
- name: Run AI game creator shell web gates
|
|
||||||
run: npm run check:native-shells:agc-web
|
|
||||||
|
|||||||
@@ -40,14 +40,6 @@ temp*build*/
|
|||||||
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-path/
|
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-path/
|
||||||
/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/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/
|
|
||||||
/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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -- "$@"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
3. [`docs/project-memory/README.md`](docs/project-memory/README.md)、[`project-overview.md`](docs/project-memory/shared-memory/project-overview.md)、[`team-conventions.md`](docs/project-memory/shared-memory/team-conventions.md)、[`development-workflow.md`](docs/project-memory/shared-memory/development-workflow.md)。
|
3. [`docs/project-memory/README.md`](docs/project-memory/README.md)、[`project-overview.md`](docs/project-memory/shared-memory/project-overview.md)、[`team-conventions.md`](docs/project-memory/shared-memory/team-conventions.md)、[`development-workflow.md`](docs/project-memory/shared-memory/development-workflow.md)。
|
||||||
4. 与任务相关的 [`decision-log.md`](docs/project-memory/shared-memory/decision-log.md)、[`pitfalls.md`](docs/project-memory/shared-memory/pitfalls.md)、[`docs/README.md`](docs/README.md) 和当前专题文档。
|
4. 与任务相关的 [`decision-log.md`](docs/project-memory/shared-memory/decision-log.md)、[`pitfalls.md`](docs/project-memory/shared-memory/pitfalls.md)、[`docs/README.md`](docs/README.md) 和当前专题文档。
|
||||||
- 落地工程修改前,先确认是否已有足够具体的 PRD、技术方案或当前融合文档;文档仍存在编码级歧义时,先补文档再编码。
|
- 落地工程修改前,先确认是否已有足够具体的 PRD、技术方案或当前融合文档;文档仍存在编码级歧义时,先补文档再编码。
|
||||||
- 跨模块功能、公开 API/DTO、SpacetimeDB schema、AGC/Runtime 或复杂 UI 状态链路必须按 [`docs/【协作规范】规范驱动开发工作流-2026-09-12.md`](docs/【协作规范】规范驱动开发工作流-2026-09-12.md) 先完成主规范、里程碑规范和单里程碑实现计划;评审与验收门禁未通过前不得进入下一里程碑。局部修复、文案和无行为变化重构继续走轻量流程。
|
|
||||||
- 本仓库的本地 RAG 位于 [`scripts/rag/`](scripts/rag/);RAG 只作为候选上下文,不替代打开源文件核对。默认不安装 RAG 运行时依赖,需要启用时必须先询问用户,并只安装到 gitignored 的 `.rag/runtime/`。
|
- 本仓库的本地 RAG 位于 [`scripts/rag/`](scripts/rag/);RAG 只作为候选上下文,不替代打开源文件核对。默认不安装 RAG 运行时依赖,需要启用时必须先询问用户,并只安装到 gitignored 的 `.rag/runtime/`。
|
||||||
|
|
||||||
## 绝对约束
|
## 绝对约束
|
||||||
|
|||||||
-24
@@ -172,28 +172,6 @@ _Avoid_: 多步骤向导、完整规则编辑器、拖拽编辑器
|
|||||||
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
|
Bark Battle 平台作品闭环按契约与领域规则、后端存储/API、最小前端纵切、投影体验、收口验证的顺序推进。
|
||||||
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
|
_Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
|
||||||
|
|
||||||
## 项目开发对话(DirectProject)
|
|
||||||
|
|
||||||
**DirectProject 专属聊天模块**:
|
|
||||||
AGC 普通项目聊天的独立容器,拥有 DirectProject 的聊天状态、运行态订阅、历史读取、发送队列、附件和中止交互,并把聊天投影交给专属表现层渲染;它不承接 Supervisor、Design Agent 或 Planning V2 的运行态。
|
|
||||||
_Avoid_: 把 DirectProject 作为项目总控聊天的一个布尔分支、把四种 Agent 会话抽象成同一事实源
|
|
||||||
|
|
||||||
**项目工作台布局**:
|
|
||||||
承载本地项目的资源工作区、项目级工具和独立聊天产品路径的外层界面;布局拥有跨面板的账户/钱包入口,聊天模块只负责项目对话,不嵌套账户展示。
|
|
||||||
_Avoid_: 把钱包入口塞进聊天设置、让聊天组件拥有工作台级账户状态
|
|
||||||
|
|
||||||
**项目对话历史**:
|
|
||||||
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
|
|
||||||
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
|
|
||||||
|
|
||||||
**运行态事件**:
|
|
||||||
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
|
|
||||||
_Avoid_: 进度通知、快照轮询、第二套历史
|
|
||||||
|
|
||||||
**聊天投影**:
|
|
||||||
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
|
|
||||||
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
|
|
||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
|
|
||||||
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**。
|
- 一个 **汪汪声浪大作战** 单局包含多个 **有效声浪触发**。
|
||||||
@@ -228,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();
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
|
|
||||||
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
|
|
||||||
# “构建 -> 监听 -> 再构建”的自触发循环。
|
|
||||||
resources/plugins/
|
|
||||||
@@ -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 +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.12",
|
||||||
"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"
|
||||||
|
|||||||
@@ -371,15 +371,7 @@ async function runMissingConfigWizard(setActiveChild, explicitConfigDir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function secureWindowsPrivateRuntimePath(
|
async function secureWindowsPrivateRuntimePath(targetPath, options) {
|
||||||
targetPath,
|
|
||||||
options,
|
|
||||||
secureWindowsPath = null,
|
|
||||||
) {
|
|
||||||
if (secureWindowsPath) {
|
|
||||||
await secureWindowsPath(targetPath, options);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { secureWindowsGameCreatorPathForCurrentUser } = await import(
|
const { secureWindowsGameCreatorPathForCurrentUser } = await import(
|
||||||
'./game-creator-config-wizard.mjs'
|
'./game-creator-config-wizard.mjs'
|
||||||
);
|
);
|
||||||
@@ -391,7 +383,6 @@ async function copyPrivateRuntimeConfigEntry(
|
|||||||
runtimeConfigDir,
|
runtimeConfigDir,
|
||||||
fileName,
|
fileName,
|
||||||
required,
|
required,
|
||||||
secureWindowsPath = null,
|
|
||||||
) {
|
) {
|
||||||
const sourcePath = path.join(sourceConfigDir, fileName);
|
const sourcePath = path.join(sourceConfigDir, fileName);
|
||||||
const sourceMetadata = await lstat(sourcePath).catch((error) => {
|
const sourceMetadata = await lstat(sourcePath).catch((error) => {
|
||||||
@@ -411,11 +402,9 @@ async function copyPrivateRuntimeConfigEntry(
|
|||||||
const sourceBytes = await readFile(sourcePath);
|
const sourceBytes = await readFile(sourcePath);
|
||||||
const destinationFile = await open(destinationPath, 'wx', 0o600);
|
const destinationFile = await open(destinationPath, 'wx', 0o600);
|
||||||
try {
|
try {
|
||||||
await secureWindowsPrivateRuntimePath(
|
await secureWindowsPrivateRuntimePath(destinationPath, {
|
||||||
destinationPath,
|
isDirectory: false,
|
||||||
{ isDirectory: false },
|
});
|
||||||
secureWindowsPath,
|
|
||||||
);
|
|
||||||
await destinationFile.writeFile(sourceBytes);
|
await destinationFile.writeFile(sourceBytes);
|
||||||
await destinationFile.sync();
|
await destinationFile.sync();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -448,11 +437,7 @@ async function copyPrivateRuntimeConfigEntry(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareSwarmTestRuntimeConfig(
|
export async function prepareSwarmTestRuntimeConfig(sourceConfigDir, tempRoot) {
|
||||||
sourceConfigDir,
|
|
||||||
tempRoot,
|
|
||||||
{ secureWindowsPath = null } = {},
|
|
||||||
) {
|
|
||||||
if (!path.isAbsolute(sourceConfigDir)) {
|
if (!path.isAbsolute(sourceConfigDir)) {
|
||||||
throw new Error('配置来源目录必须是绝对路径');
|
throw new Error('配置来源目录必须是绝对路径');
|
||||||
}
|
}
|
||||||
@@ -472,11 +457,9 @@ export async function prepareSwarmTestRuntimeConfig(
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
await secureWindowsPrivateRuntimePath(
|
await secureWindowsPrivateRuntimePath(runtimeConfigDir, {
|
||||||
runtimeConfigDir,
|
isDirectory: true,
|
||||||
{ isDirectory: true },
|
});
|
||||||
secureWindowsPath,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await chmod(runtimeConfigDir, 0o700);
|
await chmod(runtimeConfigDir, 0o700);
|
||||||
}
|
}
|
||||||
@@ -494,14 +477,12 @@ export async function prepareSwarmTestRuntimeConfig(
|
|||||||
runtimeConfigDir,
|
runtimeConfigDir,
|
||||||
configFileName,
|
configFileName,
|
||||||
true,
|
true,
|
||||||
secureWindowsPath,
|
|
||||||
);
|
);
|
||||||
await copyPrivateRuntimeConfigEntry(
|
await copyPrivateRuntimeConfigEntry(
|
||||||
canonicalSourceConfigDir,
|
canonicalSourceConfigDir,
|
||||||
runtimeConfigDir,
|
runtimeConfigDir,
|
||||||
localConfigFileName,
|
localConfigFileName,
|
||||||
false,
|
false,
|
||||||
secureWindowsPath,
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
path: await realpath(runtimeConfigDir),
|
path: await realpath(runtimeConfigDir),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
|||||||
/** 默认桌面能力;显式 feature 参数优先,不把应用参数当 Cargo 参数。 */
|
|
||||||
export function withDefaultCargoFeatures(argv, features) {
|
|
||||||
const separator = argv.indexOf('--');
|
|
||||||
const cargoArgs = separator < 0 ? argv : argv.slice(0, separator);
|
|
||||||
if (
|
|
||||||
!features.length ||
|
|
||||||
cargoArgs.some(
|
|
||||||
(value) =>
|
|
||||||
value === '--features' ||
|
|
||||||
value === '-f' ||
|
|
||||||
value.startsWith('--features=') ||
|
|
||||||
/^-f.+/u.test(value),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return argv;
|
|
||||||
}
|
|
||||||
return [`--features=${features.join(',')}`, ...argv];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultEditorFeatures(target) {
|
|
||||||
return target === 'win32' || target.includes('windows')
|
|
||||||
? ['cocos-editor-execute']
|
|
||||||
: [];
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import { test } from 'node:test';
|
|
||||||
|
|
||||||
import { buildTauriBuildArguments } from './build-release.mjs';
|
|
||||||
import { withDefaultCargoFeatures } from './cargo-features.mjs';
|
|
||||||
|
|
||||||
test('Windows release includes the same editor feature as development', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
|
|
||||||
[
|
|
||||||
'build',
|
|
||||||
'--features=cocos-editor-execute',
|
|
||||||
'--target',
|
|
||||||
'x86_64-pc-windows-msvc',
|
|
||||||
],
|
|
||||||
);
|
|
||||||
assert.deepEqual(
|
|
||||||
buildTauriBuildArguments(
|
|
||||||
['--no-bundle'],
|
|
||||||
'x86_64-pc-windows-msvc',
|
|
||||||
'linux',
|
|
||||||
),
|
|
||||||
['build', '--no-bundle'],
|
|
||||||
);
|
|
||||||
assert.deepEqual(
|
|
||||||
buildTauriBuildArguments(['--target=aarch64-apple-darwin']),
|
|
||||||
['build', '--target=aarch64-apple-darwin'],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('explicit Cargo features override defaults in every supported spelling', () => {
|
|
||||||
for (const args of [
|
|
||||||
['--features', 'custom'],
|
|
||||||
['--features=custom'],
|
|
||||||
['-f', 'custom'],
|
|
||||||
['-fcustom'],
|
|
||||||
]) {
|
|
||||||
assert.deepEqual(
|
|
||||||
withDefaultCargoFeatures(args, ['cocos-editor-execute']),
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert.deepEqual(
|
|
||||||
withDefaultCargoFeatures(
|
|
||||||
['--', '--features=app'],
|
|
||||||
['cocos-editor-execute'],
|
|
||||||
),
|
|
||||||
['--features=cocos-editor-execute', '--', '--features=app'],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -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',
|
||||||
@@ -114,37 +108,15 @@ const rustSharedContractSource = fs.readFileSync(
|
|||||||
);
|
);
|
||||||
const allowedUncalledTauriCommands = [
|
const allowedUncalledTauriCommands = [
|
||||||
'append_direct_project_conversation_message',
|
'append_direct_project_conversation_message',
|
||||||
// TODO: Remove the retired binding command after the legacy runtime path is removed.
|
|
||||||
'bind_components',
|
|
||||||
'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',
|
|
||||||
'stop_local_game_preview_if_matches',
|
'stop_local_game_preview_if_matches',
|
||||||
'start_game_creator_external_mcp',
|
'start_game_creator_external_mcp',
|
||||||
'stop_game_creator_external_mcp',
|
'stop_game_creator_external_mcp',
|
||||||
'list_agc_plugins',
|
|
||||||
'list_agc_extensions',
|
|
||||||
'refresh_agc_plugins',
|
|
||||||
'start_agc_plugin',
|
|
||||||
'stop_agc_plugin',
|
|
||||||
'reload_agc_plugin',
|
|
||||||
'call_agc_plugin',
|
|
||||||
'read_agc_plugin_panel',
|
|
||||||
'set_agc_plugin_project_path',
|
|
||||||
'set_agc_plugin_enabled',
|
|
||||||
];
|
];
|
||||||
const sourceExtensions = new Set([
|
const sourceExtensions = new Set([
|
||||||
'.json',
|
'.json',
|
||||||
@@ -1305,7 +1277,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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,88 +1285,36 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
|
|||||||
throw new Error('AI game creator shell identifier drifted');
|
throw new Error('AI game creator shell identifier drifted');
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedBundledDesignAgentResources = {
|
const expectedBundledCodexResources = {
|
||||||
'design-agent': 'design-agent',
|
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
|
||||||
};
|
|
||||||
const expectedBundledWindowsResources = {
|
|
||||||
'resources/codex/win-x64/bin/codex.exe': 'coding-agent/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',
|
|
||||||
};
|
};
|
||||||
assert.deepEqual(
|
if (tauriConfig.bundle?.resources !== undefined) {
|
||||||
tauriConfig.bundle?.resources,
|
|
||||||
expectedBundledDesignAgentResources,
|
|
||||||
'AI game creator shell base Tauri config must bundle the design-agent resource pack',
|
|
||||||
);
|
|
||||||
for (const key of Object.keys(tauriConfig.bundle?.resources ?? {})) {
|
|
||||||
if (String(key).includes('codex')) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
|
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
windowsTauriConfig.bundle?.resources,
|
windowsTauriConfig.bundle?.resources,
|
||||||
expectedBundledWindowsResources,
|
expectedBundledCodexResources,
|
||||||
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set and Cocos bridge payload directory',
|
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set',
|
||||||
);
|
);
|
||||||
if (
|
|
||||||
Object.prototype.hasOwnProperty.call(
|
|
||||||
windowsTauriConfig.bundle?.resources ?? {},
|
|
||||||
'design-agent',
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'design-agent resource pack must not be mixed into the Windows Codex sidecar bundle',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
|
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'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(
|
||||||
@@ -1759,7 +1679,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 });
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user