diff --git a/.codex/README.md b/.codex/README.md new file mode 100644 index 000000000..4ea0ba9a9 --- /dev/null +++ b/.codex/README.md @@ -0,0 +1,16 @@ +# Genarrative Codex 项目工具 + +`.codex/` 是仓库级 Codex 工具目录,保存项目共享的 skills、插件资源、hooks 和相关配置模板。它只描述如何协作和加载工具,不承载项目业务知识。 + +## 目录约定 + +- `.codex/skills/` 是项目专属 skill 根目录。每个 skill 以目录中的 `SKILL.md` 为入口,配套的参考资料和脚本放在同一目录下。 +- `.codex/plugins/` 保存随仓库分发的项目插件资源及其参考资料。当前的 `game-studio` 插件提供浏览器游戏设计、原型、2D/3D 技术栈、素材管线和 playtest 工作流;是否启用遵循当前 Codex 的插件加载机制,不依赖旧工具的环境变量或个人配置脚本。 +- `.codex/hooks/`、`.codex/environments/` 等目录保存项目工具链所需的 hooks 和环境模板;它们不替代项目代码中的运行时配置。 +- 长期有效的产品、架构、接口、排障和协作知识统一放在 `docs/` 与 `docs/project-memory/`,不复制到本目录。 + +## 使用边界 + +进入仓库后先读根目录 `AGENTS.md`,再按任务路由读取对应 skill。SpacetimeDB 的通用概念、Rust 服务端、CLI、TypeScript 客户端和 MCP 用法由已安装的官方插件提供;项目约束和入口由 `.codex/skills/genarrative-spacetimedb/SKILL.md` 统一编排。 + +个人 `~/.codex` 配置、凭据、会话、环境变量和本地路径不得复制到仓库。若本目录内容与当前代码或最新 `docs/` 冲突,以代码和最新文档为准,并修正过期工具说明。 diff --git a/.hermes/plugins/game-studio/.codex-plugin/plugin.json b/.codex/plugins/game-studio/.codex-plugin/plugin.json similarity index 100% rename from .hermes/plugins/game-studio/.codex-plugin/plugin.json rename to .codex/plugins/game-studio/.codex-plugin/plugin.json diff --git a/.hermes/plugins/game-studio/__init__.py b/.codex/plugins/game-studio/__init__.py similarity index 76% rename from .hermes/plugins/game-studio/__init__.py rename to .codex/plugins/game-studio/__init__.py index bcf37e070..b7ea43159 100644 --- a/.hermes/plugins/game-studio/__init__.py +++ b/.codex/plugins/game-studio/__init__.py @@ -1,8 +1,8 @@ -"""Hermes wrapper for the OpenAI Codex Game Studio plugin. +"""Metadata bridge for the bundled Codex Game Studio project plugin. -This plugin was imported from a Codex curated plugin cache. It exposes the -plugin's bundled SKILL.md files as Hermes plugin skills using qualified names -like `game-studio:phaser-2d-game`. +The manifest and bundled ``SKILL.md`` files are the source of truth. This +module only exposes their descriptions to a compatible plugin host and keeps +the project resource bundle free of runtime-specific behavior. """ from __future__ import annotations diff --git a/.hermes/plugins/game-studio/assets/app-icon.png b/.codex/plugins/game-studio/assets/app-icon.png similarity index 100% rename from .hermes/plugins/game-studio/assets/app-icon.png rename to .codex/plugins/game-studio/assets/app-icon.png diff --git a/.hermes/plugins/game-studio/assets/game-studio.svg b/.codex/plugins/game-studio/assets/game-studio.svg similarity index 100% rename from .hermes/plugins/game-studio/assets/game-studio.svg rename to .codex/plugins/game-studio/assets/game-studio.svg diff --git a/.hermes/plugins/game-studio/plugin.yaml b/.codex/plugins/game-studio/plugin.yaml similarity index 100% rename from .hermes/plugins/game-studio/plugin.yaml rename to .codex/plugins/game-studio/plugin.yaml diff --git a/.hermes/plugins/game-studio/references/alternative-3d-engines.md b/.codex/plugins/game-studio/references/alternative-3d-engines.md similarity index 100% rename from .hermes/plugins/game-studio/references/alternative-3d-engines.md rename to .codex/plugins/game-studio/references/alternative-3d-engines.md diff --git a/.hermes/plugins/game-studio/references/engine-selection.md b/.codex/plugins/game-studio/references/engine-selection.md similarity index 100% rename from .hermes/plugins/game-studio/references/engine-selection.md rename to .codex/plugins/game-studio/references/engine-selection.md diff --git a/.hermes/plugins/game-studio/references/frontend-prompts.md b/.codex/plugins/game-studio/references/frontend-prompts.md similarity index 100% rename from .hermes/plugins/game-studio/references/frontend-prompts.md rename to .codex/plugins/game-studio/references/frontend-prompts.md diff --git a/.hermes/plugins/game-studio/references/gltf-loading-starter.md b/.codex/plugins/game-studio/references/gltf-loading-starter.md similarity index 100% rename from .hermes/plugins/game-studio/references/gltf-loading-starter.md rename to .codex/plugins/game-studio/references/gltf-loading-starter.md diff --git a/.hermes/plugins/game-studio/references/phaser-architecture.md b/.codex/plugins/game-studio/references/phaser-architecture.md similarity index 100% rename from .hermes/plugins/game-studio/references/phaser-architecture.md rename to .codex/plugins/game-studio/references/phaser-architecture.md diff --git a/.hermes/plugins/game-studio/references/playtest-checklist.md b/.codex/plugins/game-studio/references/playtest-checklist.md similarity index 100% rename from .hermes/plugins/game-studio/references/playtest-checklist.md rename to .codex/plugins/game-studio/references/playtest-checklist.md diff --git a/.hermes/plugins/game-studio/references/rapier-integration-starter.md b/.codex/plugins/game-studio/references/rapier-integration-starter.md similarity index 100% rename from .hermes/plugins/game-studio/references/rapier-integration-starter.md rename to .codex/plugins/game-studio/references/rapier-integration-starter.md diff --git a/.hermes/plugins/game-studio/references/react-three-fiber-stack.md b/.codex/plugins/game-studio/references/react-three-fiber-stack.md similarity index 100% rename from .hermes/plugins/game-studio/references/react-three-fiber-stack.md rename to .codex/plugins/game-studio/references/react-three-fiber-stack.md diff --git a/.hermes/plugins/game-studio/references/react-three-fiber-starter.md b/.codex/plugins/game-studio/references/react-three-fiber-starter.md similarity index 100% rename from .hermes/plugins/game-studio/references/react-three-fiber-starter.md rename to .codex/plugins/game-studio/references/react-three-fiber-starter.md diff --git a/.hermes/plugins/game-studio/references/sprite-pipeline.md b/.codex/plugins/game-studio/references/sprite-pipeline.md similarity index 100% rename from .hermes/plugins/game-studio/references/sprite-pipeline.md rename to .codex/plugins/game-studio/references/sprite-pipeline.md diff --git a/.hermes/plugins/game-studio/references/three-hud-layout-patterns.md b/.codex/plugins/game-studio/references/three-hud-layout-patterns.md similarity index 100% rename from .hermes/plugins/game-studio/references/three-hud-layout-patterns.md rename to .codex/plugins/game-studio/references/three-hud-layout-patterns.md diff --git a/.hermes/plugins/game-studio/references/three-webgl-architecture.md b/.codex/plugins/game-studio/references/three-webgl-architecture.md similarity index 100% rename from .hermes/plugins/game-studio/references/three-webgl-architecture.md rename to .codex/plugins/game-studio/references/three-webgl-architecture.md diff --git a/.hermes/plugins/game-studio/references/threejs-stack.md b/.codex/plugins/game-studio/references/threejs-stack.md similarity index 100% rename from .hermes/plugins/game-studio/references/threejs-stack.md rename to .codex/plugins/game-studio/references/threejs-stack.md diff --git a/.hermes/plugins/game-studio/references/threejs-vanilla-starter.md b/.codex/plugins/game-studio/references/threejs-vanilla-starter.md similarity index 100% rename from .hermes/plugins/game-studio/references/threejs-vanilla-starter.md rename to .codex/plugins/game-studio/references/threejs-vanilla-starter.md diff --git a/.hermes/plugins/game-studio/references/web-3d-asset-pipeline.md b/.codex/plugins/game-studio/references/web-3d-asset-pipeline.md similarity index 100% rename from .hermes/plugins/game-studio/references/web-3d-asset-pipeline.md rename to .codex/plugins/game-studio/references/web-3d-asset-pipeline.md diff --git a/.hermes/plugins/game-studio/references/webgl-debugging-and-performance.md b/.codex/plugins/game-studio/references/webgl-debugging-and-performance.md similarity index 100% rename from .hermes/plugins/game-studio/references/webgl-debugging-and-performance.md rename to .codex/plugins/game-studio/references/webgl-debugging-and-performance.md diff --git a/.hermes/plugins/game-studio/scripts/build_sprite_edit_canvas.py b/.codex/plugins/game-studio/scripts/build_sprite_edit_canvas.py similarity index 100% rename from .hermes/plugins/game-studio/scripts/build_sprite_edit_canvas.py rename to .codex/plugins/game-studio/scripts/build_sprite_edit_canvas.py diff --git a/.hermes/plugins/game-studio/scripts/normalize_sprite_strip.py b/.codex/plugins/game-studio/scripts/normalize_sprite_strip.py similarity index 100% rename from .hermes/plugins/game-studio/scripts/normalize_sprite_strip.py rename to .codex/plugins/game-studio/scripts/normalize_sprite_strip.py diff --git a/.hermes/plugins/game-studio/scripts/render_sprite_preview_sheet.py b/.codex/plugins/game-studio/scripts/render_sprite_preview_sheet.py similarity index 100% rename from .hermes/plugins/game-studio/scripts/render_sprite_preview_sheet.py rename to .codex/plugins/game-studio/scripts/render_sprite_preview_sheet.py diff --git a/.hermes/plugins/game-studio/skills/game-playtest/SKILL.md b/.codex/plugins/game-studio/skills/game-playtest/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/game-playtest/SKILL.md rename to .codex/plugins/game-studio/skills/game-playtest/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/game-playtest/agents/openai.yaml b/.codex/plugins/game-studio/skills/game-playtest/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/game-playtest/agents/openai.yaml rename to .codex/plugins/game-studio/skills/game-playtest/agents/openai.yaml diff --git a/.hermes/plugins/game-studio/skills/game-studio/SKILL.md b/.codex/plugins/game-studio/skills/game-studio/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/game-studio/SKILL.md rename to .codex/plugins/game-studio/skills/game-studio/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/game-studio/agents/openai.yaml b/.codex/plugins/game-studio/skills/game-studio/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/game-studio/agents/openai.yaml rename to .codex/plugins/game-studio/skills/game-studio/agents/openai.yaml diff --git a/.hermes/plugins/game-studio/skills/game-ui-frontend/SKILL.md b/.codex/plugins/game-studio/skills/game-ui-frontend/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/game-ui-frontend/SKILL.md rename to .codex/plugins/game-studio/skills/game-ui-frontend/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/game-ui-frontend/agents/openai.yaml b/.codex/plugins/game-studio/skills/game-ui-frontend/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/game-ui-frontend/agents/openai.yaml rename to .codex/plugins/game-studio/skills/game-ui-frontend/agents/openai.yaml diff --git a/.hermes/plugins/game-studio/skills/phaser-2d-game/SKILL.md b/.codex/plugins/game-studio/skills/phaser-2d-game/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/phaser-2d-game/SKILL.md rename to .codex/plugins/game-studio/skills/phaser-2d-game/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/phaser-2d-game/agents/openai.yaml b/.codex/plugins/game-studio/skills/phaser-2d-game/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/phaser-2d-game/agents/openai.yaml rename to .codex/plugins/game-studio/skills/phaser-2d-game/agents/openai.yaml diff --git a/.hermes/plugins/game-studio/skills/react-three-fiber-game/SKILL.md b/.codex/plugins/game-studio/skills/react-three-fiber-game/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/react-three-fiber-game/SKILL.md rename to .codex/plugins/game-studio/skills/react-three-fiber-game/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/react-three-fiber-game/agents/openai.yaml b/.codex/plugins/game-studio/skills/react-three-fiber-game/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/react-three-fiber-game/agents/openai.yaml rename to .codex/plugins/game-studio/skills/react-three-fiber-game/agents/openai.yaml diff --git a/.hermes/plugins/game-studio/skills/sprite-pipeline/SKILL.md b/.codex/plugins/game-studio/skills/sprite-pipeline/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/sprite-pipeline/SKILL.md rename to .codex/plugins/game-studio/skills/sprite-pipeline/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/sprite-pipeline/agents/openai.yaml b/.codex/plugins/game-studio/skills/sprite-pipeline/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/sprite-pipeline/agents/openai.yaml rename to .codex/plugins/game-studio/skills/sprite-pipeline/agents/openai.yaml diff --git a/.hermes/plugins/game-studio/skills/three-webgl-game/SKILL.md b/.codex/plugins/game-studio/skills/three-webgl-game/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/three-webgl-game/SKILL.md rename to .codex/plugins/game-studio/skills/three-webgl-game/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/three-webgl-game/agents/openai.yaml b/.codex/plugins/game-studio/skills/three-webgl-game/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/three-webgl-game/agents/openai.yaml rename to .codex/plugins/game-studio/skills/three-webgl-game/agents/openai.yaml diff --git a/.hermes/plugins/game-studio/skills/web-3d-asset-pipeline/SKILL.md b/.codex/plugins/game-studio/skills/web-3d-asset-pipeline/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/web-3d-asset-pipeline/SKILL.md rename to .codex/plugins/game-studio/skills/web-3d-asset-pipeline/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/web-3d-asset-pipeline/agents/openai.yaml b/.codex/plugins/game-studio/skills/web-3d-asset-pipeline/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/web-3d-asset-pipeline/agents/openai.yaml rename to .codex/plugins/game-studio/skills/web-3d-asset-pipeline/agents/openai.yaml diff --git a/.hermes/plugins/game-studio/skills/web-game-foundations/SKILL.md b/.codex/plugins/game-studio/skills/web-game-foundations/SKILL.md similarity index 100% rename from .hermes/plugins/game-studio/skills/web-game-foundations/SKILL.md rename to .codex/plugins/game-studio/skills/web-game-foundations/SKILL.md diff --git a/.hermes/plugins/game-studio/skills/web-game-foundations/agents/openai.yaml b/.codex/plugins/game-studio/skills/web-game-foundations/agents/openai.yaml similarity index 100% rename from .hermes/plugins/game-studio/skills/web-game-foundations/agents/openai.yaml rename to .codex/plugins/game-studio/skills/web-game-foundations/agents/openai.yaml diff --git a/.codex/skills/README.md b/.codex/skills/README.md new file mode 100644 index 000000000..407ed1cd6 --- /dev/null +++ b/.codex/skills/README.md @@ -0,0 +1,7 @@ +# Genarrative 项目 skills + +`.codex/skills/` 是仓库级、可通过 Git 共享的项目专属 skill 根目录。每个目录的 `SKILL.md` 是唯一入口;较长的参考资料、示例和脚本放在该 skill 的 `references/`、`assets/` 或 `scripts/` 下。 + +项目 skill 负责把当前仓库的目录、契约、验证门禁和历史约束串起来,不重复维护通用框架知识。涉及 SpacetimeDB 时,先读 `genarrative-spacetimedb/SKILL.md`,由它路由到已安装的官方 SpacetimeDB 插件 skills。 + +长期项目知识放在 `docs/` 与 `docs/project-memory/`;不要把个人配置、密钥、会话、缓存或临时计划写入本目录。发现 skill 与代码或最新文档不一致时,按当前实现更新 skill,并同步必要的项目文档。 diff --git a/.hermes/skills/behavior-driven-development/SKILL.md b/.codex/skills/behavior-driven-development/SKILL.md similarity index 97% rename from .hermes/skills/behavior-driven-development/SKILL.md rename to .codex/skills/behavior-driven-development/SKILL.md index 5b3e4e674..f2e94eb80 100644 --- a/.hermes/skills/behavior-driven-development/SKILL.md +++ b/.codex/skills/behavior-driven-development/SKILL.md @@ -1,11 +1,9 @@ --- name: behavior-driven-development description: 在 Genarrative 中需要用 BDD/行为驱动方式把 PRD、用户故事、验收标准转成可执行场景、Gherkin 用例、测试计划或 TDD 落地顺序时使用。 -version: 1.0.0 -author: Hermes Agent license: MIT metadata: - hermes: + codex: tags: [BDD, Gherkin, 验收标准, 用户故事, 测试, Genarrative] related_skills: [writing-plans, test-driven-development, systematic-debugging, requesting-code-review] --- @@ -280,11 +278,11 @@ fn anonymous_user_cannot_publish_generated_draft() { | 产物类型 | 推荐路径 | 适用场景 | | --- | --- | --- | -| 实施前分析 / 临时计划 | 当前任务说明或 `.tmp/-bdd-scenarios.md` | 某次 Hermes 开发任务前,用于澄清行为、拆测试、辅助实现;不作为长期产品依据。 | +| 实施前分析 / 临时计划 | 当前任务说明或 `.tmp/-bdd-scenarios.md` | 某次 Codex 开发任务前,用于澄清行为、拆测试、辅助实现;不作为长期产品依据。 | | 正式产品验收 / PRD 场景 | 当前 `docs/` 融合文档,必要时新增 `docs/【产品验收】<功能名>BDD场景-YYYY-MM-DD.md` | 产品、测试、开发都需要长期参考的验收标准、用户故事、功能边界。 | | 技术/API/领域行为场景 | 当前 `docs/` 融合文档,必要时新增 `docs/【技术验收】<功能名>BDD场景-YYYY-MM-DD.md` | 后端 API、领域规则、状态机、SpacetimeDB reducer/table、SSE/异步任务、埋点副作用。 | | 自动化 Gherkin feature 文件 | `tests/features/*.feature` 或 `e2e/features/*.feature` | 项目已接入 Cucumber/Playwright BDD 等 Gherkin runner 时。未接入前不要随意新建测试 runner 目录。 | -| 稳定流程或团队经验 | `docs/project-memory/shared-memory/` 或 `.hermes/skills/` | 不是某个功能验收,而是长期可复用的团队流程、坑点、执行规范。 | +| 稳定流程或团队经验 | `docs/project-memory/shared-memory/` 或 `.codex/skills/` | 不是某个功能验收,而是长期可复用的团队流程、坑点、执行规范。 | 默认规则: @@ -311,7 +309,7 @@ e2e/features/invite-code.feature - 实施计划:当前任务上下文或 `.tmp/.md` - 产品/验收文档:当前 `docs/` 融合文档,必要时新增 `docs/【产品验收】中文标题-YYYY-MM-DD.md` - 技术设计:当前 `docs/` 融合文档,必要时新增 `docs/【技术方案】中文标题-YYYY-MM-DD.md` -- 共享经验或稳定流程:`docs/project-memory/shared-memory/` 或 `.hermes/skills/` +- 共享经验或稳定流程:`docs/project-memory/shared-memory/` 或 `.codex/skills/` BDD 文档建议包含: diff --git a/.hermes/skills/genarrative-admin-backoffice/SKILL.md b/.codex/skills/genarrative-admin-backoffice/SKILL.md similarity index 97% rename from .hermes/skills/genarrative-admin-backoffice/SKILL.md rename to .codex/skills/genarrative-admin-backoffice/SKILL.md index 24adcbf38..5c826f743 100644 --- a/.hermes/skills/genarrative-admin-backoffice/SKILL.md +++ b/.codex/skills/genarrative-admin-backoffice/SKILL.md @@ -1,12 +1,9 @@ --- name: genarrative-admin-backoffice -short_description: 在 Genarrative/陶泥儿后台新增或修改管理页、后台只读/写接口、导出能力时使用。 description: 在 Genarrative/陶泥儿后台新增或修改管理页、后台 BFF 接口、shared-contracts/admin DTO、admin-web 路由导航、Excel/表格导出与验证发布时使用。 -version: 1.0.0 -author: Hermes Agent license: MIT metadata: - hermes: + codex: tags: [Genarrative, 陶泥儿后台, admin-web, 后台接口, Excel导出, Rust, Axum, SpacetimeDB] related_skills: [genarrative-play-type-integration] --- @@ -168,7 +165,7 @@ npm run dev - 本地和人工排障不再使用 `spacetime --root-dir`。如果看到 `bin/current/spacetimedb-cli` 缺失类错误,优先确认是否仍在运行旧脚本或旧发布包;本地开发应使用 `npm run dev` / `npm run dev:spacetime`,通过项目脚本和 `--data-dir` 隔离 SpacetimeDB 数据目录,不再把用户级 SpacetimeDB 安装同步到项目目录。 - `scripts/dev.mjs` 默认 `api timeout: 600s`. 合并 master 后首次 Rust 依赖/工作区重编译可能超过默认等待窗口,导致完整 `npm run dev` 在 api-server 就绪前超时并回收 SpacetimeDB。先让 Rust 编译完成,或临时用 `npm run dev:api-server -- --api-timeout-seconds 900` 预热 api-server 编译;之后再重新跑完整 `npm run dev`。 -- 用户贴出的 Hermes background watch 通知可能来自已退出的旧 session。先用 `process poll` 查该 session 状态,再判断是否需要处理;不要把旧失败误判成当前服务失败。 +- 用户贴出的 Codex background watch 通知可能来自已退出的旧 session。先用 `process poll` 查该 session 状态,再判断是否需要处理;不要把旧失败误判成当前服务失败。 ## 测试与验证 diff --git a/.hermes/skills/genarrative-admin-backoffice/references/admin-database-table-query-2026-05-08.md b/.codex/skills/genarrative-admin-backoffice/references/admin-database-table-query-2026-05-08.md similarity index 100% rename from .hermes/skills/genarrative-admin-backoffice/references/admin-database-table-query-2026-05-08.md rename to .codex/skills/genarrative-admin-backoffice/references/admin-database-table-query-2026-05-08.md diff --git a/.hermes/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md b/.codex/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md similarity index 100% rename from .hermes/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md rename to .codex/skills/genarrative-admin-backoffice/references/admin-tracking-events-export-2026-05-07.md diff --git a/.hermes/skills/genarrative-admin-backoffice/references/daily-login-auth-closure.md b/.codex/skills/genarrative-admin-backoffice/references/daily-login-auth-closure.md similarity index 100% rename from .hermes/skills/genarrative-admin-backoffice/references/daily-login-auth-closure.md rename to .codex/skills/genarrative-admin-backoffice/references/daily-login-auth-closure.md diff --git a/.hermes/skills/genarrative-admin-backoffice/references/daily-login-tracking-trigger-points.md b/.codex/skills/genarrative-admin-backoffice/references/daily-login-tracking-trigger-points.md similarity index 100% rename from .hermes/skills/genarrative-admin-backoffice/references/daily-login-tracking-trigger-points.md rename to .codex/skills/genarrative-admin-backoffice/references/daily-login-tracking-trigger-points.md diff --git a/.hermes/skills/genarrative-admin-backoffice/references/dev-rust-stack-startup-2026-05-08.md b/.codex/skills/genarrative-admin-backoffice/references/dev-rust-stack-startup-2026-05-08.md similarity index 100% rename from .hermes/skills/genarrative-admin-backoffice/references/dev-rust-stack-startup-2026-05-08.md rename to .codex/skills/genarrative-admin-backoffice/references/dev-rust-stack-startup-2026-05-08.md diff --git a/.hermes/skills/genarrative-admin-backoffice/references/private-table-sql-token-refresh.md b/.codex/skills/genarrative-admin-backoffice/references/private-table-sql-token-refresh.md similarity index 100% rename from .hermes/skills/genarrative-admin-backoffice/references/private-table-sql-token-refresh.md rename to .codex/skills/genarrative-admin-backoffice/references/private-table-sql-token-refresh.md diff --git a/.hermes/skills/genarrative-admin-backoffice/references/spacetimedb-http-sql-sats-display.md b/.codex/skills/genarrative-admin-backoffice/references/spacetimedb-http-sql-sats-display.md similarity index 100% rename from .hermes/skills/genarrative-admin-backoffice/references/spacetimedb-http-sql-sats-display.md rename to .codex/skills/genarrative-admin-backoffice/references/spacetimedb-http-sql-sats-display.md diff --git a/.hermes/skills/genarrative-auth-session-flow/SKILL.md b/.codex/skills/genarrative-auth-session-flow/SKILL.md similarity index 99% rename from .hermes/skills/genarrative-auth-session-flow/SKILL.md rename to .codex/skills/genarrative-auth-session-flow/SKILL.md index 4644306fd..7e49913a1 100644 --- a/.hermes/skills/genarrative-auth-session-flow/SKILL.md +++ b/.codex/skills/genarrative-auth-session-flow/SKILL.md @@ -1,11 +1,9 @@ --- name: genarrative-auth-session-flow description: 在 Genarrative 中排查或修改登录、access token、refresh cookie、AuthGate 会话恢复、登录态刷新、认证埋点链路时使用。 -version: 1.0.0 -author: Hermes Agent license: MIT metadata: - hermes: + codex: tags: [Genarrative, auth, session, cookie, refresh-token, AuthGate, tracking] related_skills: [systematic-debugging, test-driven-development, genarrative-profile-features] --- diff --git a/.hermes/skills/genarrative-auth-session-flow/references/session-restore-daily-login-tracking-2026-05-08.md b/.codex/skills/genarrative-auth-session-flow/references/session-restore-daily-login-tracking-2026-05-08.md similarity index 100% rename from .hermes/skills/genarrative-auth-session-flow/references/session-restore-daily-login-tracking-2026-05-08.md rename to .codex/skills/genarrative-auth-session-flow/references/session-restore-daily-login-tracking-2026-05-08.md diff --git a/.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md b/.codex/skills/genarrative-dev-stack-port-routing/SKILL.md similarity index 98% rename from .hermes/skills/genarrative-dev-stack-port-routing/SKILL.md rename to .codex/skills/genarrative-dev-stack-port-routing/SKILL.md index cde9e6977..0a0a6540c 100644 --- a/.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md +++ b/.codex/skills/genarrative-dev-stack-port-routing/SKILL.md @@ -1,12 +1,9 @@ --- name: genarrative-dev-stack-port-routing -short_description: 修改 Genarrative 本地 dev 启动端口、代理目标、端口冲突处理时使用。 description: 在 Genarrative 中修改 npm run dev / dev:spacetime / dev:api-server / dev:bgfilter-worker / dev:web / dev:admin-web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、Rust 进程环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。 -version: 1.2.0 -author: Hermes Agent license: MIT metadata: - hermes: + codex: tags: [Genarrative, dev-stack, 端口探测, Vite, api-server, SpacetimeDB, npm-run-dev] related_skills: [genarrative-admin-backoffice] --- diff --git a/.codex/skills/genarrative-play-type-integration/SKILL.md b/.codex/skills/genarrative-play-type-integration/SKILL.md index 63d14982e..9ba349906 100644 --- a/.codex/skills/genarrative-play-type-integration/SKILL.md +++ b/.codex/skills/genarrative-play-type-integration/SKILL.md @@ -3,7 +3,7 @@ name: genarrative-play-type-integration description: 在 Genarrative 中新增或补齐一个创作入口/玩法类型时,按入口配置、前端分流、契约、后端接口、工作台、独立生成页、结果页、发布、统一作品详情、正式 runtime、公开 read model、基础统计与作品架/广场的顺序接入。 license: MIT metadata: - author: Hermes Agent + author: Genarrative Team version: "1.0" --- @@ -90,13 +90,14 @@ metadata: 12. **旧数据策略**:旧草稿、旧发布配置、旧分享码是迁移、降级展示、重新生成,还是明确不兼容。 - `AGENTS.md` -- `.hermes/shared-memory/` +- `docs/project-memory/shared-memory/` - `CONTEXT.md` - `docs/README.md` - `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` - 相关玩法 PRD 或设计文档 +- `.codex/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md`(涉及正式 runtime 埋点时) -如果文档不能精确指导字段、契约、资产槽位、生成流程和恢复语义,先补文档再编码。新增长期约定时同步 `.hermes/shared-memory/`。 +如果文档不能精确指导字段、契约、资产槽位、生成流程和恢复语义,先补文档再编码。新增长期约定时同步 `docs/project-memory/shared-memory/`。 ### 2. 定玩法边界 diff --git a/.hermes/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md b/.codex/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md similarity index 100% rename from .hermes/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md rename to .codex/skills/genarrative-play-type-integration/references/genarrative-analytics-tracking-runtime.md diff --git a/.hermes/skills/genarrative-profile-features/SKILL.md b/.codex/skills/genarrative-profile-features/SKILL.md similarity index 99% rename from .hermes/skills/genarrative-profile-features/SKILL.md rename to .codex/skills/genarrative-profile-features/SKILL.md index 5dd6c87b5..81de55e44 100644 --- a/.hermes/skills/genarrative-profile-features/SKILL.md +++ b/.codex/skills/genarrative-profile-features/SKILL.md @@ -1,11 +1,9 @@ --- name: genarrative-profile-features description: 在 Genarrative “我的”页签新增或修改个人中心入口、独立 profile 路由、反馈/记录/设置类页面时使用。 -version: 1.0.0 -author: Hermes Agent license: MIT metadata: - hermes: + codex: tags: [Genarrative, profile, 我的页签, 前端, 路由, 反馈] related_skills: [writing-plans, test-driven-development] --- diff --git a/.hermes/skills/genarrative-profile-features/references/profile-feedback-entry-2026-05-08.md b/.codex/skills/genarrative-profile-features/references/profile-feedback-entry-2026-05-08.md similarity index 100% rename from .hermes/skills/genarrative-profile-features/references/profile-feedback-entry-2026-05-08.md rename to .codex/skills/genarrative-profile-features/references/profile-feedback-entry-2026-05-08.md diff --git a/.hermes/skills/genarrative-profile-invite-flow/SKILL.md b/.codex/skills/genarrative-profile-invite-flow/SKILL.md similarity index 99% rename from .hermes/skills/genarrative-profile-invite-flow/SKILL.md rename to .codex/skills/genarrative-profile-invite-flow/SKILL.md index 784998a10..eb2483590 100644 --- a/.hermes/skills/genarrative-profile-invite-flow/SKILL.md +++ b/.codex/skills/genarrative-profile-invite-flow/SKILL.md @@ -1,11 +1,9 @@ --- name: genarrative-profile-invite-flow description: 在 Genarrative 中排查或修改邀请码、邀请好友、首次登录后填写邀请码、我的页签邀请码兑换链路时使用。 -version: 1.0.0 -author: Hermes Agent license: MIT metadata: - hermes: + codex: tags: [Genarrative, 邀请码, referral, auth, profile, query-params, 前端] related_skills: [] --- diff --git a/.hermes/skills/genarrative-profile-invite-flow/references/query-invite-code-flow-2026-05-07.md b/.codex/skills/genarrative-profile-invite-flow/references/query-invite-code-flow-2026-05-07.md similarity index 100% rename from .hermes/skills/genarrative-profile-invite-flow/references/query-invite-code-flow-2026-05-07.md rename to .codex/skills/genarrative-profile-invite-flow/references/query-invite-code-flow-2026-05-07.md diff --git a/.codex/skills/genarrative-spacetimedb/SKILL.md b/.codex/skills/genarrative-spacetimedb/SKILL.md new file mode 100644 index 000000000..d6a9a3b4d --- /dev/null +++ b/.codex/skills/genarrative-spacetimedb/SKILL.md @@ -0,0 +1,126 @@ +--- +name: genarrative-spacetimedb +description: Genarrative 的 SpacetimeDB 项目适配规范。用于涉及 SpacetimeDB 架构、Rust module、schema、migration、reducer、procedure、view、绑定生成、CLI、MCP、发布、调试或运行时核验的任务。 +--- + +# Genarrative SpacetimeDB 项目指导 + +本 skill 只保存 Genarrative 的项目约束和操作边界;SpacetimeDB 的通用 API、语言 SDK 和 CLI 手册由已安装的官方插件提供。项目规则覆盖插件示例中的默认值或与本仓库冲突的建议。 + +## 官方插件依赖 + +开始 SpacetimeDB 任务时,按任务范围读取官方插件 skill: + +- `spacetimedb:concepts`:核心语义、表、reducer、procedure、view、订阅和身份。 +- `spacetimedb:rust-server`:Rust module、表属性、访问器、迁移兼容性和 SDK API。 +- `spacetimedb:cli`:初始化、构建、发布、生成绑定、SQL、调用、日志和 server 管理。 +- `spacetimedb:typescript-client`:前端生成绑定、订阅和 TypeScript 客户端 SDK;其它语言客户端按需读取插件对应 skill。 +- `spacetimedb:mcp`:通过已连接的 MCP 操作运行中的数据库;没有 MCP 工具时使用 CLI 等价命令。 + +如果当前环境尚未安装插件,使用: + +```bash +codex plugin marketplace add clockworklabs/SpacetimeDB --sparse .agents --sparse codex-plugin +codex plugin add spacetimedb\@spacetimedb-plugins +``` + +插件不可用时,以当前源码、`docs/`、生成绑定和仓库脚本为准,不凭记忆发明 SpacetimeDB API。 + +## 架构边界 + +Genarrative 的唯一有效后端路线是: + +```text +server-rs + Axum + SpacetimeDB +``` + +- `module-*`:领域模型、命令、应用规则、领域事件和领域错误;不得直接依赖 Axum、SpacetimeDB table/reducer/procedure、`spacetime-client`、外部平台或文件系统。 +- `spacetime-module`:SpacetimeDB 表、reducer、procedure、view、migration、事务 adapter 和 row mapper。 +- `spacetime-client`:后端访问 SpacetimeDB 的 typed facade;其它后端 crate 不直接创建第二套访问路径。 +- `api-server`:HTTP、SSE、BFF 和外部副作用编排。 +- `platform-*`:OSS、LLM、认证、语音等外部平台能力。 +- `shared-contracts` / `packages/shared`:前后端 DTO、公开契约和无业务真相的共享 TypeScript 代码。 +- 前端只负责表现、交互、临时 UI 状态和后端结果渲染,不绕过 BFF/投影直接读取私有表或推导正式业务状态。 + +SpacetimeDB 是数据和事务层,不替代 `api-server` BFF、`spacetime-client` facade 或公开 read model。插件提供的“SpacetimeDB 可替代传统服务端”通用描述不能改变本项目边界。 + +## 语义与安全不变量 + +- Reducer 是原子事务写路径,不向调用者返回业务数据;读取通过订阅、read model、view 或 BFF。 +- Reducer 必须确定性执行:不得访问文件系统、网络、系统时钟或外部随机源;使用 `ctx.timestamp`、`ctx.rng()` / `ctx.random()` 等 SpacetimeDB 能力。 +- 授权使用上下文中的 `ctx.sender()`(或当前语言对应 API),不信任调用参数传入的身份。 +- Auto-increment ID 不是排序依据;需要顺序时使用时间戳或显式序列字段。 +- Private table 是后端事实;用户可见状态通过 BFF、投影或明确的 public table/view 暴露。公共表仍只能由 reducer/procedure 写入。 +- Procedure 在 2.8 已稳定,可使用显式事务和 `ctx.http`;Genarrative 默认仍把外部 provider 协议放在 `platform-*`,把编排放在 `api-server`,除非当前架构明确要求 module procedure。 +- Event table 必须显式订阅,按插入事件消费;不要依赖其持久化行或 `OnUpdate`。需要更新回调时使用持久表或带主键的 procedural view。 +- Standalone MCP 是 operator/developer 集成面,不是 BFF、facade 或公开 read model 的替代品。MCP/SQL/CLI 的写入都必须有明确授权;日常 smoke 优先只读。 + +## Schema 与迁移 + +修改现有 SpacetimeDB persistent table 时: + +1. 新字段只能追加到 Rust 表结构体末尾,并设置明确的 `#[default(...)]`。 +2. 删除、改名、重排、改类型或破坏性约束变更前,必须先询问用户并确认迁移计划。 +3. 同步更新 `server-rs/crates/spacetime-module/src/migration.rs`、后端架构文档中的表目录、生成绑定和相关契约/测试。 +4. 运行: + + ```bash + npm run spacetime:generate + npm run check:spacetime-schema + ``` + +Event table 的较宽松自动迁移规则不适用于 persistent table,不能借此绕过上述门禁。以当前源码和 `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 为 schema 真相。 + +## CLI、目标 server 与本地开发 + +- 优先使用仓库 wrapper:`npm run dev:spacetime`、`npm run dev:api-server`、`npm run spacetime:generate`。 +- 直接使用 CLI 时始终显式传 `--server` 或 `--server-url`;不要依赖默认云端目标或个人 CLI 默认 server。 +- 不新增 `maincloud` / `MAINCLOUD` 命令、环境变量、脚本或文档;历史残留只按历史处理。 +- 人工命令、本地联调、排障步骤和文档示例禁止使用 `spacetime --root-dir`;本地数据隔离使用项目脚本或 `--data-dir`。 +- `spacetime publish` 的 `--delete-data=always` 只在明确授权的破坏性操作中使用;schema 冲突优先按项目脚本和受控迁移流程处理。 +- 项目 SpacetimeDB crate、SDK、CLI/standalone 和生成 bindings 按 `2.8.3` 对齐;官方发行资产、Rust crates 和容器镜像使用 `v2.8.3` 版本标签,仓库额外固定 CLI commit `8e410d2842147bd8e5a32a9589cc00c19f7478e2`。升级时核对 Cargo 精确 pin、实际 CLI 和运行中服务二进制,不把本地 CLI 重装当作仓库升级。 + +本地开发默认由项目启动器管理端口;实际监听地址以 `.app/dev-stack.json` 和启动日志为准,不能从文档默认端口推断当前目标。发布后确认 api-server 使用的是同一 database、server 和 token。 + +## MCP 与运行时核验 + +如果当前会话暴露 SpacetimeDB MCP 工具,读取运行中的数据库优先使用 typed MCP:先 `list_databases` / `get_schema`,再做只读 SQL 或 `ping`;调用 reducer 或 SQL 写入前确认目标、身份和授权。没有 MCP 工具时使用显式目标的 CLI。2.8 standalone 的 MCP HTTP endpoint 是 `POST /v1/database/{name_or_identity}/mcp`,提供 `ping`、`get_schema`、`sql`、`call`;升级 smoke 在隔离数据库中只做 `initialize`、`tools/list`、`ping`、`get_schema`,除非写入明确属于任务范围。 + +排查“服务健康但业务不可用”时按顺序核对: + +1. SpacetimeDB standalone 是否运行(本地优先 `npm run dev:spacetime`,主机侧核对 systemd)。 +2. module 是否发布到 api-server 实际使用的同一个 server/database。 +3. 生成绑定是否来自当前 module。 +4. api-server 的 database、server URL 和 token 是否一致。 +5. reducer/procedure 是否真正被调用;区分超时、权限、schema 不存在和业务错误。 +6. `/healthz` / `/readyz` 通过但业务仍失败时,继续检查 API 日志和公开路由,不把健康检查当作业务成功证明。 + +主机升级需核对运行中进程而非只看 PATH: + +```bash +type -a spacetime +spacetime --version +pid="$(systemctl show spacetimedb.service -p MainPID --value)" +readlink -f "/proc/${pid}/exe" +"/proc/${pid}/exe" --version +curl -fsS http://127.0.0.1:3101/v1/ping +``` + +## 修改后的最小验证 + +按范围执行定向测试/类型检查,并至少运行: + +```bash +npm run check:encoding +git diff --check +``` + +涉及 schema 时追加 `npm run spacetime:generate` 和 `npm run check:spacetime-schema`;涉及 API 时按当前后端文档启动 `npm run dev:api-server` 并检查 `/healthz`。无法运行的验证要在交付说明中标记为未验证并说明原因。 + +## 参考入口 + +- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` +- `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md` +- `server-rs/README.md` +- `scripts/check-spacetime-schema-guard.mjs` +- `scripts/check-server-rs-ddd-boundaries.mjs` diff --git a/.codex/skills/spacetimedb-cli/SKILL.md b/.codex/skills/spacetimedb-cli/SKILL.md deleted file mode 100644 index 73dac7168..000000000 --- a/.codex/skills/spacetimedb-cli/SKILL.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -name: spacetimedb-cli -description: SpacetimeDB 2.7 CLI reference for Genarrative. Use for spacetime build, publish, generate, call, sql, logs, server management, local dev, explicit server targeting, version checks, and remote runtime verification. ---- - -# SpacetimeDB CLI - -Use this skill when working with the `spacetime` CLI in Genarrative. Prefer repository scripts when they exist, and keep every operation pinned to an explicit target server or local process. - -## Genarrative Rules - -- Do not rely on the default SpacetimeDB cloud target. Pass `--server` or `--server-url` explicitly in scripts, docs, smoke tests, and manual troubleshooting. -- Do not introduce `maincloud` / `MAINCLOUD` commands, env vars, or docs. Treat old references as historical residue. -- Do not use `spacetime --root-dir` in manual commands or docs. Use project scripts, `--data-dir`, explicit `--server`, or the configured running service. -- For repository version upgrades, update `server-rs/Cargo.toml` exact pins, regenerate bindings, and verify the actual CLI/runtime version. Do not treat a local CLI reinstall as a repo upgrade. -- For host upgrades, verify the running service binary, not just shell PATH: `systemctl show ... MainPID` -> `/proc/$pid/exe --version` -> `/v1/ping`. - -## Core Commands - -```bash -# Build module -spacetime build -spacetime build --debug - -# Publish to an explicit server -spacetime publish my-database --server http://127.0.0.1:3101 --yes=migrate,break-clients - -# Destructive publish only when explicitly intended -spacetime publish my-database --server http://127.0.0.1:3101 --delete-data=always --yes=delete-data,migrate - -# Delete data only for breaking schema conflicts -spacetime publish my-database --server http://127.0.0.1:3101 --delete-data=on-conflict --yes=migrate - -# Generate bindings -spacetime generate --lang typescript|csharp|rust|unrealcpp --out-dir ./bindings --module-path ./server -``` - -## Genarrative Local Workflow - -```bash -# Prefer project wrappers -npm run dev:spacetime -npm run dev:api-server -npm run spacetime:generate - -# Query local database -spacetime sql my-db --server http://127.0.0.1:3101 "SELECT * FROM players" - -# Logs -spacetime logs my-db --server http://127.0.0.1:3101 -f -``` - -## Database Interaction - -```bash -# SQL / describe -spacetime sql my-db --server http://127.0.0.1:3101 "SELECT * FROM users" -spacetime describe my-db --server http://127.0.0.1:3101 --json -spacetime describe my-db table users --server http://127.0.0.1:3101 --json - -# Reducer/procedure calls. Arguments are positional JSON values. -spacetime call --server http://127.0.0.1:3101 my-db my_reducer '"value"' '123' - -# 2.5+ accepts hex strings for Identity arguments without full JSON tuple syntax. -spacetime call --server http://127.0.0.1:3101 my-db reducer_needing_identity 0xabc123... - -# Subscribe from CLI -spacetime subscribe my-db "SELECT * FROM users" --num-updates 10 --server http://127.0.0.1:3101 -``` - -## Standalone MCP Endpoint (2.7) - -SpacetimeDB 2.7 standalone exposes an authenticated JSON-RPC MCP endpoint at -`POST /v1/database/{name_or_identity}/mcp`. It advertises `ping`, `get_schema`, -`sql`, and `call`. The SQL and reducer tools execute with the bearer token's -identity, so keep routine smoke checks read-only. - -```bash -curl -fsS \ - -H "Authorization: Bearer ${SPACETIME_TOKEN}" \ - -H 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"genarrative-smoke","version":"1.0.0"}}}' \ - http://127.0.0.1:3101/v1/database/my-db/mcp - -curl -fsS \ - -H "Authorization: Bearer ${SPACETIME_TOKEN}" \ - -H 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ping","arguments":{"message":"genarrative"}}}' \ - http://127.0.0.1:3101/v1/database/my-db/mcp -``` - -For repository upgrade validation, also call `tools/list` and the read-only -`get_schema` tool against an isolated local database. Do not use `sql` or `call` -for writes unless that mutation is explicitly in scope. - -## Server & Auth - -```bash -spacetime server list -spacetime server add local --url http://localhost:3000 --default -spacetime server add genarrative-dev --url http://127.0.0.1:3101 -spacetime server ping genarrative-dev - -spacetime login -spacetime login --token -spacetime login show -spacetime logout -``` - -## Version & Runtime Verification - -```bash -# CLI resolution can be misleading; compare all candidates when diagnosing. -type -a spacetime -spacetime --version -spacetime version list - -# Verify a systemd service binary actually changed. -pid="$(systemctl show spacetimedb.service -p MainPID --value)" -readlink -f "/proc/${pid}/exe" -"/proc/${pid}/exe" --version -curl -fsS http://127.0.0.1:3101/v1/ping -``` - -## Flags - -| Flag | Description | -|------|-------------| -| `--server`, `-s` | Target server nickname, host, or URL | -| `--yes`, `-y` | Non-interactive prompt skipping; in 2.6+ use scoped values | -| `--delete-data`, `-c` | Publish data policy: `always`, `on-conflict`, or `never` | -| `--module-path`, `-p` | Module project path | -| `--bin-path`, `-b` | Publish/generate from compiled wasm | -| `--no-config` | Ignore `spacetime.json` | -| `--env` | Select config file layering environment | - -## Troubleshooting - -### Not Logged In - -```bash -spacetime login -``` - -### Server Not Responding - -```bash -spacetime server ping -curl -fsS http://127.0.0.1:3101/v1/ping -``` - -For local Genarrative work, start SpacetimeDB first with `npm run dev:spacetime`, then start `npm run dev:api-server`. - -### Schema Conflict - -```bash -spacetime publish my-db --server http://127.0.0.1:3101 --delete-data=on-conflict --yes=migrate -``` - -Use `--delete-data=always` only with explicit approval. - -### Version Mismatch - -```bash -rg -n 'spacetimedb' server-rs/Cargo.toml -spacetime --version -spacetime version list -pid="$(systemctl show spacetimedb.service -p MainPID --value)" -"/proc/${pid}/exe" --version -``` - -## Notes - -- Procedure calls remain stable in 2.7; module HTTP handlers/webhooks and RLS capabilities still require their documented gates. -- 2.5 fixed `publish --delete-data` config fallback; 2.6 kept that behavior and improved CLI binary distribution; 2.7 adds `spacetime sql --format json` and database `lock` / `unlock`. -- The official 2.7.0 Linux release archives and container image currently use the `v2.7.0-hotfix3` asset tag while binaries report `2.7.0`; keep the asset tag distinct from the runtime version check. -- Do not assume `spacetime version install 2.7.0` selected hotfix3: stale updater metadata can install bare-tag commit `a08663c7...`. For the current release, verify CLI commit `d220349a...` and use the official hotfix3 archive or repository provision flow when it differs. -- Genarrative scripts should pass `--server` or `--server-url` explicitly instead of relying on CLI defaults. diff --git a/.codex/skills/spacetimedb-concepts/SKILL.md b/.codex/skills/spacetimedb-concepts/SKILL.md deleted file mode 100644 index e671603dd..000000000 --- a/.codex/skills/spacetimedb-concepts/SKILL.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -name: spacetimedb-concepts -description: Understand SpacetimeDB 2.7 architecture, reducer/procedure/table/view semantics, schema evolution, subscriptions, identity, and Genarrative-specific backend boundaries. Use when designing or reviewing SpacetimeDB-backed features. ---- - -# SpacetimeDB Core Concepts - -SpacetimeDB is a relational database that also executes application logic in uploaded modules. In Genarrative, it is the data and transaction layer behind `server-rs + Axum + SpacetimeDB`, not a replacement for the `api-server` BFF or external platform adapters. - -## Genarrative Boundaries - -- Domain rules live in `module-*`. -- SpacetimeDB tables, reducers, procedures, migrations, row mappers, and read models live in `spacetime-module`. -- Backend access goes through `spacetime-client` facades. -- HTTP/SSE/BFF and external orchestration stay in `api-server`. -- External side effects stay in `platform-*`. -- Frontend renders backend truth and must not bypass BFF/projections to invent formal business state. - -## Critical Rules - -1. **Reducers are transactional**: they do not return data to callers. Read through subscriptions, read models, views, or BFF endpoints. -2. **Reducers are deterministic**: no filesystem, network, wall-clock, or external RNG. Use `ctx.timestamp`, `ctx.rng()` / `ctx.random()`, and tables. -3. **Procedures are stable in 2.7**: they can use explicit transactions and outgoing HTTP via `ctx.http`. -4. **Identity comes from context**: use `ctx.sender()` or language equivalent for authorization. Never trust identity passed as an argument. -5. **Auto-increment IDs are not ordering guarantees**: gaps are normal. Use timestamps or explicit sequence columns for ordering. -6. **Schema changes need migration discipline**: existing Genarrative table fields must be appended with defaults; update migration code, table catalog, generated bindings, and run `npm run check:spacetime-schema`. - -## Tables - -- Private tables are the default; only reducers/procedures and database owners can access them. -- Public tables are exposed to clients through subscriptions. Writes still go through reducers/procedures. -- Organize data by access pattern when bandwidth or update frequency differs. -- Existing persistent tables in Genarrative are conservative: no rename, delete, reorder, or type changes without a user-approved migration plan. - -## Reducers - -Reducers are deterministic transactional functions. They are the primary client-invoked mutation path. - -- No global mutable state. -- No filesystem, network, timers, or non-deterministic RNG. -- Return `Result<(), String>` for expected sender-visible errors. -- Use `ctx.sender()` for authorization. -- Store persistent state in tables. - -## Procedures - -Procedures are stable in 2.7. They can be scheduled, can open explicit transactions with `with_tx` / `try_with_tx`, and can use outgoing HTTP (`ctx.http`). - -Genarrative default: keep external provider protocols in `platform-*` and orchestration in `api-server` unless a task explicitly moves a workflow into a module procedure. - -Module HTTP handlers/webhooks and RLS `client_visibility_filter` remain subject to their documented gates in 2.7. - -## Views - -Views expose computed read-only data. SpacetimeDB 2.7 supports primary keys on procedural views in Rust, TypeScript, C#, and C++. Clients can receive update events when subscribed to such views with primary keys. Ensure the view never returns duplicate primary keys, because that can fail view refresh and roll back the triggering transaction. - -## Event Tables - -Event tables broadcast reducer/procedure-specific facts to subscribers and must be subscribed explicitly. They are excluded from `subscribe_to_all_tables()`. - -Since 2.6, event tables support broader layout-altering automigrations, including column removal, reordering, and type changes that regular tables reject. This relaxed migration behavior is for event-only tables, not persistent tables. - -Event-table primary keys and constraints are transaction-scoped. They can reject duplicate event rows within one transaction, but event rows are not retained in client cache, so clients observe event tables through insert callbacks only. Do not design Genarrative event tables around `OnUpdate` / `on_update` / `onUpdate`; use a persistent table or a primary-keyed procedural view when update callbacks are required. - -Official 2.4.1 through 2.7 release notes document primary-key-backed update callbacks for procedural views, not event tables. - -## Subscriptions - -1. Subscribe to SQL queries or generated table/query builders. -2. Receive initial matching rows. -3. Receive updates when subscribed rows change. -4. Render from subscribed data, not reducer return values. - -Best practices: - -- Group subscriptions by lifetime. -- Subscribe to new data before unsubscribing old data during transitions. -- Avoid overlapping queries that duplicate row delivery. -- Use indexes for subscribed filters. - -## Standalone MCP - -SpacetimeDB 2.7 standalone exposes `POST /v1/database/{name_or_identity}/mcp` -using MCP JSON-RPC protocol `2025-06-18`. Its tools are `ping`, `get_schema`, -`sql`, and `call`; SQL and reducer calls run with the authenticated caller's -identity. In Genarrative this is an operator/developer integration surface, not -a replacement for `api-server` BFF routes, `spacetime-client` facades, or public -read models. Upgrade smoke should use an isolated local database and restrict -itself to `initialize`, `tools/list`, `ping`, and `get_schema` unless writes are -explicitly intended. - -## 2.2.0 to 2.7.0 Delta - -Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then: - -- **2.2.0**: v3 WebSocket transport and TS SDK default, safer production operations (`lock`/`unlock`, safer `delete`, better `publish --yes`), TS React `useProcedure`, table clearing APIs, empty-table drop automigration, primary-key migration fixes, bytes-key B-tree support, durability hardening. -- **2.3.0**: first-party Godot SDK, more WebSocket pipelining/batching, HTTP/2 backend support, Vue `useProcedure`, Unity 6 WebGL support, commitlog compression/throughput improvements, Rust `DbContext` generics, `ReducerContext::identity` deprecated in favor of `database_identity`, connection lifecycle and unsubscribe fixes. -- **2.4.0**: unstable module HTTP handlers/webhooks, faster synchronous WASM reducer runtime, commitlog resume truncation fix for silent data loss risk, better commitlog decode context, V8 heap metrics for procedure workers, JS execution-time billing regression reverted. -- **2.4.1**: Rust and TypeScript procedural views can declare primary keys, enabling `OnUpdate` events for subscribed views; fixed index schema from ST tables. -- **2.5.0**: procedures are stable, C# procedural views gain primary keys, event tables allow broader layout-altering automigrations, BTreeSet storage makes row insertion deterministic and avoids accidentally quadratic bulk insert behavior, `wasm_memory_bytes` billing metric semantics changed, template version constraints unified, `publish --delete-data` config fallback fixed, CLI `call` accepts hex Identity arguments. -- **2.6.0**: procedural-view primary keys are available across Rust, TypeScript, and C#, commitlog gains `max_segment_size` / `write_buffer_size` / `preallocate_segments`, the default write buffer increases for throughput, event-table automigrations improve, and CLI binary distribution expands. -- **2.6.1**: procedure contexts again receive the caller `Identity` and `ConnectionId`; generated TypeScript `Option` fields use optional keys; `spacetime init --template` lists available templates when no template argument is supplied. -- **2.7.0**: existing tables can add unique or primary-key constraints when current data satisfies them; standalone exposes an authenticated database MCP endpoint; Rust adds context-capability and table-accessor traits; `spacetime sql --format json` and database locking are available; view cleanup, backing-table migration, connection metrics, and memory metrics improve. Official current release assets use the `v2.7.0-hotfix3` tag while binaries report `2.7.0`. - -## Debugging Checklist - -1. Is the Genarrative SpacetimeDB server running? Use `npm run dev:spacetime` locally or host-local `systemctl`. -2. Is the module published to the same server the API uses? -3. Are generated bindings current? Use `npm run spacetime:generate`. -4. Is `api-server` using the same database and token? -5. Is the reducer/procedure actually called? -6. Did `/healthz` / `/readyz` pass while business SpacetimeDB calls still timeout? Inspect API logs and public route behavior. - -## Editing Behavior - -- Make the smallest change necessary. -- Do not invent SpacetimeDB APIs; verify against current docs, generated bindings, or source. -- For Genarrative schema edits, update migration code, table catalog/docs, generated bindings, and relevant tests. -- After schema edits, run `npm run spacetime:generate` and `npm run check:spacetime-schema`. diff --git a/.codex/skills/spacetimedb-rust/SKILL.md b/.codex/skills/spacetimedb-rust/SKILL.md deleted file mode 100644 index ef0a239d6..000000000 --- a/.codex/skills/spacetimedb-rust/SKILL.md +++ /dev/null @@ -1,280 +0,0 @@ ---- -name: spacetimedb-rust -description: Develop SpacetimeDB 2.7 server modules in Rust for Genarrative. Use when writing or reviewing tables, reducers, procedures, views, migrations, row mappers, schema changes, and module logic. ---- - -# SpacetimeDB Rust Module Development - -Use this skill for Rust code in `server-rs/crates/spacetime-module` and related Genarrative schema/migration work. - -## Genarrative Rules - -- Keep domain rules in `module-*`; keep SpacetimeDB tables, reducers, procedures, views, mappers, and transaction adapters in `spacetime-module`. -- Existing table fields must be appended at the end with explicit defaults. Do not rename, remove, reorder, or change field types without a user-confirmed migration plan. -- After schema changes, update `migration.rs`, table catalog/docs, generated bindings, and run `npm run spacetime:generate` plus `npm run check:spacetime-schema`. -- Private tables are backend facts. Expose user-visible state through BFF endpoints/read models rather than direct client SQL. - -## Hallucinated APIs: Do Not Use - -```rust -#[derive(Table)] // Tables use #[table], not derive -#[derive(Reducer)] // Reducers use #[reducer], not derive -#[derive(SpacetimeType)] // Do not derive this on #[table] structs - -pub fn reducer(ctx: &mut ReducerContext) {} // Use &ReducerContext - -ctx.db.player // Use ctx.db.player() -ctx.db.player.find(id) // Use ctx.db.player().id().find(&id) -ctx.sender // Use ctx.sender() -ctx.db.user().name().update(..) // Update by primary key only - -spacetimedb = { version = "...", features = ["unstable"] } // Not needed for procedures since 2.5 -``` - -## Required Patterns - -```rust -use spacetimedb::{reducer, table, Identity, ReducerContext, Table, Timestamp}; -use spacetimedb::SpacetimeType; // Custom types only, not tables - -#[table(accessor = player, public)] -pub struct Player { - #[primary_key] - #[auto_inc] - pub id: u64, - pub owner: Identity, - pub name: String, - pub created_at: Timestamp, -} - -#[reducer] -pub fn create_player(ctx: &ReducerContext, name: String) -> Result<(), String> { - if name.trim().is_empty() { - return Err("name required".to_string()); - } - ctx.db.player().try_insert(Player { - id: 0, - owner: ctx.sender(), - name, - created_at: ctx.timestamp, - })?; - Ok(()) -} -``` - -Hard requirements: - -- Import `Table` for table operations. -- Use `accessor = identifier`, not string literals. -- Use `ctx.sender()` for authorization. -- Use `ctx.rng()` / `ctx.random()` / `ctx.new_uuid_*()` for deterministic randomness and UUIDs. -- Use `Result<(), String>` for expected sender errors; avoid panics except impossible states. -- Use `try_insert()` in `Result` reducers when constraint violations should be reported cleanly. - -## Tables - -```rust -#[spacetimedb::table(accessor = game_tick_schedule, scheduled(game_tick))] -pub struct GameTickSchedule { - #[primary_key] - #[auto_inc] - pub scheduled_id: u64, - pub scheduled_at: ScheduleAt, -} -``` - -Table attributes: - -| Attribute | Description | -|-----------|-------------| -| `accessor = identifier` | API name used in `ctx.db.{accessor}()` | -| `public` | Visible to clients via subscriptions | -| `event` | Transient event table | -| `scheduled(function_name)` | Schedule table that triggers a reducer/procedure | -| `index(accessor = idx, btree(columns = [a, b]))` | Multi-column index | - -Column attributes: - -| Attribute | Description | -|-----------|-------------| -| `#[primary_key]` | One primary key per table | -| `#[auto_inc]` | Auto-generates integer values when inserting `0` | -| `#[unique]` | Unique constraint and `find()` accessor | -| `#[index(btree)]` | B-tree index and `filter()` accessor | -| `#[default(...)]` | Required for new fields on existing Genarrative tables | - -## Genarrative Schema Change Pattern - -```rust -#[spacetimedb::table(accessor = creation_entry_config, public)] -pub struct CreationEntryConfig { - #[primary_key] - pub id: u64, - pub existing_field: String, - - // Append new fields at the end and provide a default. - #[default(false)] - pub new_flag: bool, -} -``` - -Then update `migration.rs`, table catalog/docs, generated bindings, and run: - -```bash -npm run spacetime:generate -npm run check:spacetime-schema -``` - -## Table Operations - -```rust -let row = ctx.db.player().insert(Player { id: 0, owner, name, created_at }); -ctx.db.player().try_insert(row)?; - -let by_id = ctx.db.player().id().find(&123u64); -for player in ctx.db.player().owner().filter(&ctx.sender()) {} -for player in ctx.db.player().level().filter(&(18u32..=65u32)) {} -for player in ctx.db.player().iter() {} -let count = ctx.db.player().count(); - -if let Some(player) = ctx.db.player().id().find(&id) { - ctx.db.player().id().update(Player { name: new_name, ..player }); -} - -ctx.db.player().id().delete(&id); -``` - -For delete/update based on non-PK filters, collect keys first to avoid iterator invalidation. - -## Indexes - -```rust -#[spacetimedb::table( - accessor = score, - public, - index(accessor = by_player_level, btree(columns = [player_id, level])) -)] -pub struct Score { - pub player_id: u32, - pub level: u32, - pub points: i64, -} - -for row in ctx.db.score().by_player_level().filter(&(42,)) {} -for row in ctx.db.score().by_player_level().filter(&(42, 5)) {} -``` - -## Event Tables - -```rust -#[table(accessor = damage_event, public, event)] -pub struct DamageEvent { - pub target: Identity, - pub amount: u32, -} - -#[reducer] -fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) { - ctx.db.damage_event().insert(DamageEvent { target, amount }); -} -``` - -Event tables must be subscribed explicitly and are excluded from `subscribe_to_all_tables()`. - -Since 2.6, event tables support broader layout-altering automigrations than regular tables, including column removal, reordering, and type changes. This relaxed migration policy does not apply to persistent tables. - -Event-table primary keys and constraints are enforced only within the current transaction. They do not make event rows persistent, and client SDKs expose event tables as insert-only event streams. Do not rely on `OnUpdate` / `on_update` / `onUpdate` for event tables; use a persistent table or a primary-keyed procedural view when update callbacks are required. - -Official 2.4.1 through 2.7 release notes tie primary-key-backed update callbacks to procedural views, not event tables. - -## Views - -```rust -#[spacetimedb::view(accessor = my_players, public, primary_key = id)] -pub fn my_players(ctx: &spacetimedb::ViewContext) -> Vec { - ctx.db.player().owner().filter(&ctx.sender()).collect() -} -``` - -Rust and TypeScript gained primary key support for procedural views in 2.4.1. With primary keys, clients can receive update events when subscribed to such views. Avoid duplicate primary keys in view results. - -## Lifecycle & Scheduled Reducers - -```rust -#[spacetimedb::reducer(init)] -pub fn init(ctx: &ReducerContext) -> Result<(), String> { Ok(()) } - -#[spacetimedb::reducer(client_connected)] -pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> { Ok(()) } - -#[spacetimedb::reducer(client_disconnected)] -pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> { Ok(()) } - -use spacetimedb::{ScheduleAt, TimeDuration}; - -ctx.db.game_tick_schedule().insert(GameTickSchedule { - scheduled_id: 0, - scheduled_at: ScheduleAt::Interval(std::time::Duration::from_millis(100).into()), -}); - -let run_at = ctx.timestamp + std::time::Duration::from_secs(60); -ctx.db.game_tick_schedule().insert(GameTickSchedule { - scheduled_id: 0, - scheduled_at: ScheduleAt::Time(run_at), -}); -``` - -For scheduled reducers, check `ctx.sender_auth().is_internal()` when the reducer should only be system-triggered. - -## Procedures - -Procedures remain stable in 2.7 and no longer require the `unstable` feature. - -```rust -use spacetimedb::{procedure, ProcedureContext}; - -#[procedure] -fn save_external_data(ctx: &mut ProcedureContext, url: String) -> Result<(), String> { - let body = ctx.http.get(url).send()?.text()?; - ctx.try_with_tx(|tx| { - tx.db.external_data().insert(ExternalData { id: 0, content: body }); - Ok(()) - })?; - Ok(()) -} -``` - -| Reducers | Procedures | -|----------|------------| -| `&ReducerContext` | `&mut ProcedureContext` | -| Direct `ctx.db` access | Use `with_tx()` / `try_with_tx()` | -| No HTTP/network | Outgoing HTTP via `ctx.http` | -| Deterministic transaction path | Side-effect-capable workflow path | - -In Genarrative, keep external provider protocols in `platform-*` by default unless the architecture explicitly moves that workflow into the module. - -## Identity & Auth - -```rust -fn require_owner(ctx: &ReducerContext, owner: &Identity) -> Result<(), String> { - if ctx.sender() != *owner { - return Err("Not authorized".to_string()); - } - Ok(()) -} -``` - -`ReducerContext::identity` is deprecated since 2.3; use the current database/module identity API when needed, and use `ctx.sender()` for caller identity. - -## Commands - -```bash -spacetime build -spacetime publish my_database --server http://127.0.0.1:3101 --module-path . --yes=migrate -spacetime publish my_database --server http://127.0.0.1:3101 --delete-data=on-conflict --module-path . --yes=migrate -spacetime logs my_database --server http://127.0.0.1:3101 -spacetime call --server http://127.0.0.1:3101 my_database create_player '"Alice"' -spacetime sql my_database --server http://127.0.0.1:3101 "SELECT * FROM player" -npm run spacetime:generate -npm run check:spacetime-schema -``` diff --git a/.hermes/README.md b/.hermes/README.md deleted file mode 100644 index e8b760a11..000000000 --- a/.hermes/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Genarrative Hermes 工具目录 - -本目录只保留 Hermes 专用的仓库级工具资源,例如 Hermes skills、plugins 和启用说明。项目知识本体、长期记忆、计划和 TODO 不再放在 `.hermes/`,统一迁移到 `docs/project-memory/`。 - -## 使用原则 - -- `.hermes/` 中只保存 Hermes 工具运行或加载所需内容。 -- 项目长期知识、架构约定、排障经验、协作规则、计划和 TODO 统一放在 `docs/project-memory/`。 -- 不提交个人配置、API Key、会话转录、模型密钥、本地路径密钥等敏感内容。 -- 个人 Hermes 的 `~/.hermes/config.yaml`、`~/.hermes/.env`、`~/.hermes/sessions/` 不应复制到本仓库。 -- 后续新增的 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`,便于团队跨目录检索。 -- 若 `.hermes/` 中的工具说明与代码或 `docs/` 冲突,以当前代码和最新 `docs/` 为准。 - -## 目录结构 - -```text -.hermes/ -├─ README.md # Hermes 工具目录说明 -├─ skills/ # 仓库级 Hermes skills -└─ plugins/ # 仓库级 Hermes plugins(需显式启用项目 plugin) -``` - -## 仓库级 Plugins - -本仓库可共享的 Hermes plugin 放在 `.hermes/plugins//`。当前已包含: - -- `.hermes/plugins/game-studio/`:浏览器游戏设计、原型、2D/3D 技术栈、素材管线与 playtest 相关工作流。 - -Hermes 的项目级 plugin 默认不会自动加载。团队成员拉取仓库后,如需使用本仓库内 plugin,请在仓库根目录启动 Hermes 前设置: - -```bash -export HERMES_ENABLE_PROJECT_PLUGINS=1 -``` - -然后确认当前 Hermes 配置的 `plugins.enabled` 中包含 `game-studio`。如果成员本机尚未启用过该 plugin,当前 Hermes 的 `hermes plugins enable` 只识别用户级或内置 plugin,可能不会识别项目级 plugin;可用以下命令写入个人配置: - -```bash -python - <<'PY' -from hermes_cli.config import load_config, save_config -config = load_config() -plugins = config.setdefault('plugins', {}) -enabled = set(plugins.get('enabled') or []) -disabled = set(plugins.get('disabled') or []) -enabled.add('game-studio') -disabled.discard('game-studio') -plugins['enabled'] = sorted(enabled) -plugins['disabled'] = sorted(disabled) -save_config(config) -PY -``` - -启用后重新进入一个新 Hermes 会话。`hermes plugins list` 当前主要展示内置和用户级 plugin,未必列出项目级 plugin;如需验证项目级扫描,可在仓库根目录运行: - -```bash -HERMES_ENABLE_PROJECT_PLUGINS=1 HERMES_PLUGINS_DEBUG=1 hermes chat -q "请读取 game-studio:game-studio skill 并概括它的用途" -``` - -该 plugin 注册的是带命名空间的 plugin skills,可用类似 `game-studio:phaser-2d-game` 的名称显式加载。 - -## 推荐给 Hermes 的启动提示 - -在本仓库中开始复杂任务时,可以先对 Hermes 说: - -```text -请先读取 AGENTS.md 以及 docs/project-memory/shared-memory/ 下与本任务相关的团队共享记忆,再开始分析。若任务完成后产生稳定项目知识,请更新 docs/project-memory/shared-memory/ 对应文件。 -``` diff --git a/.hermes/skills/README.md b/.hermes/skills/README.md deleted file mode 100644 index 3464aa0e3..000000000 --- a/.hermes/skills/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# 仓库级 Hermes Skills - -本目录预留给未来可共享的仓库级 Hermes skills。 - -## 什么时候沉淀为 Skill - -当某个流程满足以下条件之一时,可以考虑从普通 Markdown 升级为 skill: - -- 需要反复执行,且步骤稳定。 -- 涉及多个目录、命令或验证步骤。 -- 曾经踩过坑,需要明确规避步骤。 -- 新成员容易做错。 -- Hermes 在执行时需要强制加载专门知识。 - -## 建议结构 - -```text -.hermes/skills/ -└─ skill-name/ - └─ SKILL.md -``` - -## 注意 - -- 不要把 API Key、Token、账号密码写入 skill。 -- 如果 skill 与 `AGENTS.md` 或 `docs/` 冲突,先更新冲突来源再使用。 -- Skill 应包含触发条件、步骤、坑点和验证方式。 diff --git a/.hermes/skills/genarrative-play-type-integration/SKILL.md b/.hermes/skills/genarrative-play-type-integration/SKILL.md deleted file mode 100644 index f34f15583..000000000 --- a/.hermes/skills/genarrative-play-type-integration/SKILL.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -name: genarrative-play-type-integration -description: 在 Genarrative 新增、开放或重构玩法创作工具时,按平台级强约束 SOP 接入入口配置、表单/图片输入创作工作台、单图资产槽位、系列素材图集生成、独立契约、后端 DDD、结果页、运行态、作品架、广场与验证;用于避免复制既有玩法、默认对话式 Agent、页面内手写图片输入或复用玩法专属素材模型。 ---- - -# Genarrative 新增玩法创作工具平台 SOP - -把新增玩法当成平台能力接入,不把任何既有玩法当作默认模板。先确定通用模式和契约,再写具体玩法代码。 - -## 硬性禁区 - -- 不恢复前端硬编码入口配置;创作入口事实源必须来自 SpacetimeDB 和 `/api/creation-entry/config`。 -- 不把聊天输入区、流式消息或轻输入 Agent 作为新增玩法默认工作台。 -- 不在新页面内手写图片上传、参考图、AI 重绘、历史图选择、预览或删除确认逻辑。 -- 不把通用系列素材建模成任一玩法专属 DTO;玩法只能追加自己的运行态字段。 -- 不让前端承接正式业务真相;发布、试玩、通关、失败、计分、资产持久化和作品状态以后端投影为准。 -- 不新建平行入口系统、平行作品架或平行公开列表;优先扩展现有平台壳、现有阶段和现有聚合。 -- 不在 UI 面板内默认写功能说明、规则说明或开发解释文案。 - -## 接入前输入 - -开始编码前,PRD 或当前玩法文档必须已经明确: - -- `playId`、对外名称、工程域名、入口 `visible/open` 状态。 -- 创作链路:入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态。 -- 表单字段:字段名、默认值、校验、后端落库位置、生成提示词来源。 -- 单图资产槽位:`slotId`、`slotType`、`slotName`、提示词来源、读取字段、写回字段、是否允许历史图和 AI 重绘。 -- 系列素材槽位:`batchId` 语义、`sheetSpec`、`slotSpecs`、切图规则、透明化规则、失败回写、局部重生成策略。 -- API 命名空间:`/api/creation//sessions`、`actions`、`works`、`runtime`。 -- 草稿恢复、生成中恢复、失败重试、登录切换、发布后回读和移动端行为。 -- 验证命令和例外声明;没有例外时写明“无创作工具模式例外”。 - -## 默认模式 - -新增玩法默认采用表单/图片输入创作工作台: - -```text -创作入口 -> 表单/图片输入工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 -``` - -工作台只提交结构化表单、图片槽位和配置 payload。确需自然语言对话时,先走“例外流程”,不能把聊天区直接加进默认工作台。 - -## SOP - -### 1. 文档和领域词先行 - -先读: - -- `AGENTS.md` -- `docs/project-memory/shared-memory/` -- `CONTEXT.md` -- `docs/README.md` -- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` -- 相关玩法 PRD 或设计文档 - -如果文档不能精确指导字段、契约、资产槽位、生成流程和恢复语义,先补文档再编码。新增长期约定时同步 `docs/project-memory/shared-memory/`。 - -### 2. 定玩法边界 - -固定 `playId`、对外名称、工程域、入口状态、是否支持结果页、试玩、发布、作品架、广场、分享和 runtime。不要先用临时 ID 接线后再批量改名。 - -### 3. 接入口配置 - -入口配置事实源是 SpacetimeDB `creation_entry_type_config`。后台通过 `/admin/api/creation-entry/config` 管理,前台通过 `/api/creation-entry/config` 读取。 - -前端只允许在展示层派生: - -- 可见入口卡片。 -- 锁定或开放状态。 -- 排序、图标、短标题等展示信息。 - -`api-server` 路由熔断必须使用同一份入口配置。禁止新增或恢复前端本地默认入口配置作为事实源。 - -### 4. 前端阶段 - -按需要扩展 `SelectionStage`: - -- `-workspace` -- `-generating` -- `-result` -- `-runtime` -- `-gallery-detail` - -阶段名可以按玩法命名,UI 形态必须仍是表单/图片创作工作台。进入工作台时只初始化结构化草稿状态,不启动默认聊天会话。 - -### 5. 工作台实现 - -工作台必须满足: - -- 使用表单控件、图片槽位、风格选项、难度选项、开关和提交按钮组织输入。 -- 单图槽位统一使用 `CreativeImageInputPanel`。 -- 组件缺少能力时先扩展 `CreativeImageInputPanel` 的受控 props,不在玩法页面复制上传、参考图、AI 重绘、历史图、预览或删除确认。 -- 主图读取、裁剪、历史素材弹层、计费确认、自动保存和后端请求由外层页面持有;通用面板只表达输入 UI 和短生命周期 UI 状态。 -- 提交 payload 必须是表单字段与图片槽位结构,不是用户消息文本。 - -### 6. 单图资产槽位 - -角色形象、UI 背景、容器、封面、分享图、图标等单张图都按单图资产槽位处理。 - -统一约定: - -- 槽位用 `slotId` 稳定标识,`slotType` 表达用途,`slotName` 用于 UI 标签。 -- 上传图、参考图、AI 重绘、历史图选择和删除确认都通过 `CreativeImageInputPanel` 入口表达。 -- 后端写回 `imageSrc`、`imageObjectKey`、`assetObjectId` 中可用字段;前端展示前通过平台资产读取能力换签。 -- 单个槽位重生成只禁用该槽位动作,不阻塞结果页其它槽位、系列素材槽位或导航。 - -### 7. 系列素材图集生成 - -地块、物品、障碍、装饰、UI 部件等一组同类素材都走通用系列素材图集生成流程: - -```text -批量规划 -> sheet 生图 -> 后端切图 -> 去背景/透明化 -> PNG 输出 -> OSS 持久化 -> 状态回写 -> 局部重生成 -``` - -玩法只提供: - -- `sheetSpec`:画布比例、行列、单格尺寸、输出格式、背景处理策略。 -- `slotSpecs`:每个素材槽位的 `slotId`、`slotType`、`slotName`、提示词、sheet 单元格映射。 -- 玩法字段映射:把通用素材结果映射回玩法自己的 draft/profile/runtime 字段。 - -通用系列素材结果建议字段: - -- `batchId` -- `slotId` -- `slotType` -- `slotName` -- `prompt` -- `imageSrc` -- `imageObjectKey` -- `assetObjectId` -- `sourceSheetCell` -- `status` -- `error` - -玩法可追加运行态字段,例如半径、宽度、视图索引或碰撞参数,但不能依赖任何玩法专属字段作为平台通用模型。新增玩法 compile action 内部调用通用系列素材服务;如果通用服务还缺能力,先补通用服务再接玩法。 - -### 8. 契约与 API - -前后端必须同步补契约: - -- `packages/shared/src/contracts/` -- `server-rs/crates/shared-contracts/src/` - -玩法 API 保留独立命名空间: - -- `POST /api/creation//sessions` -- `GET /api/creation//sessions/{sessionId}` -- `POST /api/creation//sessions/{sessionId}/actions` -- `/api/creation//works` -- `/api/creation//runtime` - -契约需要区分: - -- 工作台输入。 -- 草稿 snapshot。 -- 单图资产槽位。 -- 系列素材批次与槽位。 -- 结果页操作。 -- 发布作品摘要。 -- runtime snapshot。 - -### 9. 后端分层 - -按 DDD 边界落地: - -- `module-`:纯领域规则、状态机、draft/runtime 校验。 -- `shared-contracts`:前后端 DTO。 -- `spacetime-module`:表、reducer、procedure、事务编排、migration。 -- `spacetime-client`:typed facade 和 row mapper。 -- `api-server`:Axum 路由、鉴权、BFF、SSE、生成编排。 -- `platform-*`:LLM、图片生成、OSS、认证等外部副作用。 - -涉及 SpacetimeDB schema 时同步 `migration.rs`、表目录和绑定,并运行 `npm run check:spacetime-schema`。 - -### 10. 结果页 - -结果页至少支持: - -- 展示草稿和生成状态。 -- 返回工作台编辑。 -- 单图槽位重生成。 -- 系列素材追加、替换、局部重生成。 -- 发布。 -- 试玩。 -- 错误展示和失败重试。 - -单图槽位和系列素材槽位的生成状态互不阻塞。已有可查看结果时,局部重生成不能把作品架草稿重新变成不可打开的全局生成中。 - -### 11. 运行态、作品架和广场 - -需要试玩或发布时补齐: - -- runtime start/action/finish API。 -- 作品保存、发布、删除、回读。 -- 作品架摘要。 -- 公开列表、详情、分享码。 -- 公开列表优先消费后端投影或 BFF 缓存,不让前端直接拼源表事实。 - -运行态可以做低延迟表现,但正式胜负、分数、奖励、排行榜和发布状态以后端裁决为准。 - -### 12. 恢复与登录态 - -必须处理: - -- 刷新恢复生成中草稿。 -- 生成页计时从后端摘要时间恢复。 -- 失败后回读 session/work detail 再决定是否展示失败。 -- 退出登录清空私有玩法状态。 -- 私有生成图展示前换签。 -- result/runtime 缺必要 draft 时回到可恢复入口,不停在空白页。 - -### 13. 例外流程 - -任何非表单/图片工作台、对话式 Agent、独立创作系统或特殊资产模型都必须先更新 PRD 和平台文档。例外声明至少写清: - -- 为什么默认表单/图片工作台不能满足。 -- 例外影响哪些输入、契约、后端流程和测试。 -- 如何保留单图资产槽位和系列素材槽位的通用能力。 -- 如何回退到平台默认链路。 - -没有文档例外,不进入编码。 - -## PRD 检查块 - -在新增玩法 PRD 中保留这一段: - -```md -## 创作工具平台接入声明 - -- 工作台模式:表单/图片输入创作工作台 -- 创作链路:入口 -> 工作台 -> 生成页 -> 结果页 -> 试玩 -> 发布 -> 运行态 -- 单图资产槽位: - - slotId / slotType / slotName / 提示词来源 / 写回字段 / 是否允许历史图 / 是否允许 AI 重绘 -- 系列素材槽位: - - batchId / sheetSpec / slotSpecs / 切图规则 / 透明化规则 / 失败回写 / 局部重生成 -- API 命名空间:/api/creation//... -- 业务真相:后端裁决字段和前端表现字段边界 -- 创作工具模式例外:无;如有,先写明例外原因和回退方式 -- 验证命令: -``` - -## 验证门禁 - -按改动范围运行: - -- `npm run check:encoding` -- `npm run typecheck` -- 前端工作台测试:确认没有聊天式 Agent 输入,提交的是表单/图片 payload。 -- `CreativeImageInputPanel` 测试:覆盖多玩法标签、上传、AI 重绘、参考图上限、历史图入口和删除确认。 -- 系列素材测试:覆盖 sheet layout、切图、透明化、OSS 持久化、追加、替换、局部重生成和失败回写。 -- 结果页测试:覆盖单图槽位重生成和系列素材槽位重生成互不阻塞。 -- 后端定向测试:覆盖 compile action、资产持久化、失败回写、发布和 runtime start。 -- 涉及 SpacetimeDB schema 时运行 `npm run check:spacetime-schema`。 diff --git a/AGENTS.md b/AGENTS.md index f7969ee42..4ba512832 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,11 +4,12 @@ - 本文件只保留 Agent 进入仓库后必须立即遵守的最高优先级规则;完整执行细则见 [`docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`](docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md)。 - 团队级长期项目记忆位于 [`docs/project-memory/`](docs/project-memory/),供 3 名开发人员和各自本地 Agent 通过 Git 同步。 -- [`.hermes/`](.hermes/) 只保存 Hermes 专用仓库级工具资源,例如 skills、plugins 和启用说明;长期项目知识不要写入 `.hermes/`。 +- [`.codex/`](.codex/) 只保存仓库级 Codex 工具资源,例如 skills、plugins、hooks 和配置模板;长期项目知识不要写入 `.codex/`。 - 若 `docs/project-memory/shared-memory/` 与当前代码或最新 `docs/` 冲突,以代码和最新 `docs/` 为准,并同步修正过期共享记忆。 ## 开始任务前 +- 先写清一句话交付结果、验收判据和不做项,再按“必须项 / 风险项 / 可选项”排序;优先完成修改、定向验证和边界检查组成的最小闭环。设置时间盒和检查点,新增发现只有在影响交付判据时才扩大范围,否则记录为后续事项;工具探测、历史整理或验证便利不能自行改变任务目标。 - 简单自包含任务可以直接执行;复杂开发、跨模块修改、后端 / UI / 文档体系调整前,按顺序读取: 1. 本文件。 2. [`docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md`](docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md)。 @@ -19,7 +20,7 @@ ## 绝对约束 -- 禁止提交个人 `~/.hermes` 配置、`.env`、API Key、Token、Cookie、会话记录、认证文件、本地私密路径、构建产物、日志、缓存和数据库 dump。 +- 禁止提交个人 `~/.codex` 配置、`.env`、API Key、Token、Cookie、会话记录、认证文件、本地私密路径、构建产物、日志、缓存和数据库 dump。 - 不要在 `.gitignore` 中新增 `.env.local`。 - 不要擅自把现有中文文案、注释、剧情或文档改写成英文;看到中文乱码时先确认真实编码,不要沿用乱码或用英文替换。 - 修改包含中文的文件时优先局部补丁,避免整文件重写;修改后优先运行仓库编码检查。 @@ -38,11 +39,11 @@ ## 任务路由 - Issue 使用自托管 Gitea;优先用 Gitea UI/API 或 `tea` CLI,不使用 GitHub `gh` 或 GitLab `glab`,除非仓库已迁移。默认 triage 标签:`needs-triage`、`needs-info`、`ready-for-agent`、`ready-for-human`、`wontfix`。 -- 需要仓库级 Hermes skills/plugins 时,再读取 [`.hermes/README.md`](.hermes/README.md)。 +- 需要仓库级 Codex skills/plugins 时,再读取 [`.codex/README.md`](.codex/README.md)。 - 涉及 AI 游戏创作独立 App、多智能体 Runtime、本地项目产物或本地 HTTP 预览时,先读取 [`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`](docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)。 - 新增、补齐、迁移或重构玩法入口、玩法类型、创作工作台、生成页、结果页、发布、运行态、作品架、广场或公开 read model 前,必须读取并按 [`genarrative-play-type-integration`](.codex/skills/genarrative-play-type-integration/SKILL.md) 执行。 -- 涉及 `npm run dev` / `npm run dev:spacetime` / `npm run dev:api-server` / `npm run dev:web` / `npm run dev:admin-web` 的端口探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标或后台 dev 端口时,按 [`.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md`](.hermes/skills/genarrative-dev-stack-port-routing/SKILL.md) 执行。 -- 涉及 SpacetimeDB 的设计、实现、脚本、调试、发布、绑定生成、schema、reducer、procedure、view 或 Rust API 时,必须读取并按 [`spacetimedb-cli`](.codex/skills/spacetimedb-cli/SKILL.md)、[`spacetimedb-rust`](.codex/skills/spacetimedb-rust/SKILL.md)、[`spacetimedb-concepts`](.codex/skills/spacetimedb-concepts/SKILL.md) 中相关 skill 执行。 +- 涉及 `npm run dev` / `npm run dev:spacetime` / `npm run dev:api-server` / `npm run dev:web` / `npm run dev:admin-web` 的端口探测、端口漂移、SpacetimeDB publish server、api-server 环境变量、Vite 代理目标或后台 dev 端口时,按 [`.codex/skills/genarrative-dev-stack-port-routing/SKILL.md`](.codex/skills/genarrative-dev-stack-port-routing/SKILL.md) 执行。 +- 涉及 SpacetimeDB 的设计、实现、脚本、调试、发布、绑定生成、schema、reducer、procedure、view 或 API 时,必须先读取并按 [`genarrative-spacetimedb`](.codex/skills/genarrative-spacetimedb/SKILL.md) 执行。 ## 后端红线 diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json index 005877a1e..1f87e5b8c 100644 --- a/apps/ai-game-creator-shell/package.json +++ b/apps/ai-game-creator-shell/package.json @@ -1,13 +1,16 @@ { "name": "@genarrative/ai-game-creator-shell", "private": true, - "version": "0.1.8", + "version": "0.1.10", "type": "module", "scripts": { "dev": "node scripts/start-tauri-dev.mjs", "dev-server": "node scripts/start-dev-server.mjs", "dev-stack": "node scripts/start-dev-stack.mjs", "build": "npm --prefix ../.. exec tauri -- build", + "skill-pack:check": "node scripts/check-skill-pack.mjs", + "skill-pack:sync": "node scripts/check-skill-pack.mjs --write", + "skill-pack:test": "node --test scripts/check-skill-pack.test.mjs", "llm-status": "node scripts/run-cli-with-config.mjs --llm-status", "agent-task": "node scripts/run-cli-with-config.mjs --agent-task", "chat": "node scripts/run-cli-with-config.mjs --swarm-chat", @@ -31,7 +34,7 @@ "agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill", "agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs", "agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill", - "typecheck": "tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs" + "typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs" }, "dependencies": { "@cubone/react-file-manager": "^1.35.0", diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index e4ff8a792..4f0d64715 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -1533,11 +1533,13 @@ if ( } if ( - tauriConfig.version !== '0.1.8' || - packageConfig.version !== '0.1.8' || - cargoPackageVersion !== '0.1.8' + tauriConfig.version !== '0.1.10' || + packageConfig.version !== '0.1.10' || + cargoPackageVersion !== '0.1.10' ) { - throw new Error('AI game creator standard release must remain version 0.1.8'); + throw new Error( + 'AI game creator standard release must remain version 0.1.10', + ); } const devServerSource = fs.readFileSync( diff --git a/apps/ai-game-creator-shell/scripts/check-skill-pack.mjs b/apps/ai-game-creator-shell/scripts/check-skill-pack.mjs new file mode 100644 index 000000000..c317981f9 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/check-skill-pack.mjs @@ -0,0 +1,48 @@ +import process from 'node:process'; + +import { + inspectSkillPack, + syncSkillPackManifest, +} from './skill-pack-manifest.mjs'; + +const argumentsList = process.argv.slice(2); +const writeMode = argumentsList.length === 1 && argumentsList[0] === '--write'; +if ( + argumentsList.length > 1 || + (argumentsList.length === 1 && argumentsList[0] !== '--write') +) { + console.error('用法:node scripts/check-skill-pack.mjs [--write]'); + process.exit(1); +} + +try { + if (writeMode) { + const result = syncSkillPackManifest(); + if (!result.changed) { + console.log(`[skill-pack] 已是最新(version=${result.version})`); + } else { + console.log( + `[skill-pack] 已同步 ${result.mismatches.map((item) => item.name).join('、')}(version=${result.version})`, + ); + } + } else { + const result = inspectSkillPack(); + if (result.mismatches.length > 0) { + console.error('[skill-pack] 内容指纹与 manifest 不一致:'); + for (const mismatch of result.mismatches) { + console.error( + `- ${mismatch.name}: manifest=${mismatch.expected} actual=${mismatch.actual}`, + ); + } + console.error('[skill-pack] 内容变更后运行:npm run agc:skill-pack:sync'); + process.exitCode = 1; + } else { + console.log(`[skill-pack] OK(version=${result.manifest.version})`); + } + } +} catch (error) { + console.error( + `[skill-pack] ${error instanceof Error ? error.message : String(error)}`, + ); + process.exitCode = 1; +} diff --git a/apps/ai-game-creator-shell/scripts/check-skill-pack.test.mjs b/apps/ai-game-creator-shell/scripts/check-skill-pack.test.mjs new file mode 100644 index 000000000..b92b38e7c --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/check-skill-pack.test.mjs @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { + computeSkillContentFingerprint, + inspectSkillPack, +} from './skill-pack-manifest.mjs'; + +test('bundled skill pack manifest is synchronized', () => { + const result = inspectSkillPack(); + assert.deepEqual(result.mismatches, []); +}); + +test('skill content fingerprint canonicalizes CRLF', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-skill-pack-')); + try { + fs.mkdirSync(path.join(root, 'demo'), { recursive: true }); + const entry = { + name: 'demo', + files: ['SKILL.md'], + }; + fs.writeFileSync(path.join(root, 'demo', 'SKILL.md'), 'line 1\nline 2\n'); + const lf = computeSkillContentFingerprint(root, entry); + fs.writeFileSync( + path.join(root, 'demo', 'SKILL.md'), + 'line 1\r\nline 2\r\n', + ); + assert.equal(computeSkillContentFingerprint(root, entry), lf); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('check command without arguments remains read-only', () => { + const scriptPath = path.join( + path.dirname(fileURLToPath(import.meta.url)), + 'check-skill-pack.mjs', + ); + const result = spawnSync(process.execPath, [scriptPath], { + encoding: 'utf8', + }); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /\[skill-pack\] OK/u); +}); diff --git a/apps/ai-game-creator-shell/scripts/skill-pack-manifest.mjs b/apps/ai-game-creator-shell/scripts/skill-pack-manifest.mjs new file mode 100644 index 000000000..d660c2a70 --- /dev/null +++ b/apps/ai-game-creator-shell/scripts/skill-pack-manifest.mjs @@ -0,0 +1,213 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { TextDecoder } from 'node:util'; + +export const SKILL_PACK_SCHEMA_VERSION = 'agc-skill-pack.v1'; +export const EXPECTED_SKILL_NAMES = Object.freeze([ + 'agc-browser-playtest', + 'agc-client-projection', + 'agc-project-structure', + 'agc-web-game-development', + 'taonier-art-assets', +]); + +const utf8Decoder = new TextDecoder('utf-8', { fatal: true }); +const defaultRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../src-tauri/resources/agc-skills', +); + +function canonicalTextBytes(filePath) { + const decoded = utf8Decoder.decode(fs.readFileSync(filePath)); + return Buffer.from(decoded.replaceAll('\r\n', '\n'), 'utf8'); +} + +export function isSafeSkillRelativePath(value) { + if ( + typeof value !== 'string' || + value.length === 0 || + value.includes('\\') || + value.includes(':') || + value.startsWith('/') + ) { + return false; + } + return value + .split('/') + .every( + (segment) => segment.length > 0 && segment !== '.' && segment !== '..', + ); +} + +function skillFilePath(rootDir, skillName, relativePath) { + if (!isSafeSkillRelativePath(relativePath)) { + throw new Error(`Skill ${skillName} 包含不安全相对路径: ${relativePath}`); + } + const target = path.resolve(rootDir, skillName, ...relativePath.split('/')); + const skillRoot = path.resolve(rootDir, skillName); + const prefix = `${skillRoot}${path.sep}`; + if (!target.startsWith(prefix)) { + throw new Error(`Skill ${skillName} 路径越过审核根目录: ${relativePath}`); + } + return target; +} + +export function computeSkillContentFingerprint(rootDir, entry) { + const digest = crypto.createHash('sha256'); + for (const relativePath of [...entry.files].sort()) { + const filePath = skillFilePath(rootDir, entry.name, relativePath); + const bytes = canonicalTextBytes(filePath); + digest.update(relativePath, 'utf8'); + digest.update(Buffer.from([0])); + digest.update(bytes); + digest.update(Buffer.from([0])); + } + return digest.digest('hex'); +} + +function collectBundledFiles(rootDir) { + const files = []; + const walk = (directory, prefix) => { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; + const absolutePath = path.join(directory, entry.name); + if (entry.isSymbolicLink()) { + throw new Error(`内置 AGC Skill 不允许符号链接: ${relativePath}`); + } + if (entry.isDirectory()) { + walk(absolutePath, relativePath); + } else if (entry.isFile()) { + files.push(relativePath.replaceAll('\\', '/')); + } else { + throw new Error(`内置 AGC Skill 文件类型不受支持: ${relativePath}`); + } + } + }; + walk(rootDir, ''); + return files.filter((file) => file !== 'manifest.json').sort(); +} + +function readManifest(rootDir) { + const manifestPath = path.join(rootDir, 'manifest.json'); + return { + manifestPath, + manifest: JSON.parse(fs.readFileSync(manifestPath, 'utf8')), + }; +} + +function validateManifestShape(rootDir, manifest) { + if (manifest?.schemaVersion !== SKILL_PACK_SCHEMA_VERSION) { + throw new Error('内置 AGC Skill 清单 schemaVersion 不受支持'); + } + if (typeof manifest.version !== 'string' || manifest.version.trim() === '') { + throw new Error('内置 AGC Skill 清单缺少版本'); + } + if (!Array.isArray(manifest.skills)) { + throw new Error('内置 AGC Skill 清单缺少 skills 数组'); + } + const names = manifest.skills.map((entry) => entry?.name); + if ( + names.length !== EXPECTED_SKILL_NAMES.length || + [...names].sort().join('\n') !== [...EXPECTED_SKILL_NAMES].sort().join('\n') + ) { + throw new Error('内置 AGC Skill 清单不等于审核白名单'); + } + + const declaredFiles = new Set(); + const mismatches = []; + for (const entry of manifest.skills) { + if ( + typeof entry.name !== 'string' || + !Array.isArray(entry.files) || + entry.files.length === 0 || + !entry.files.includes('SKILL.md') || + new Set(entry.files).size !== entry.files.length + ) { + throw new Error( + `内置 AGC Skill ${entry.name ?? ''} 元数据不完整`, + ); + } + for (const relativePath of entry.files) { + if (!isSafeSkillRelativePath(relativePath)) { + throw new Error( + `内置 AGC Skill ${entry.name} 包含不安全相对路径: ${relativePath}`, + ); + } + declaredFiles.add(`${entry.name}/${relativePath}`); + } + const actual = computeSkillContentFingerprint(rootDir, entry); + if (actual !== entry.sha256) { + mismatches.push({ + name: entry.name, + expected: entry.sha256, + actual, + }); + } + } + + const bundledFiles = collectBundledFiles(rootDir); + if ( + declaredFiles.size !== bundledFiles.length || + [...declaredFiles].sort().join('\n') !== bundledFiles.join('\n') + ) { + throw new Error('内置 AGC Skill 文件集合与审核清单不一致'); + } + return { mismatches }; +} + +export function inspectSkillPack(rootDir = defaultRoot) { + const resolvedRoot = path.resolve(rootDir); + const { manifestPath, manifest } = readManifest(resolvedRoot); + const { mismatches } = validateManifestShape(resolvedRoot, manifest); + return { manifestPath, manifest, mismatches }; +} + +function incrementPackVersion(version) { + const match = /^(\d{4}-\d{2}-\d{2})\.(\d+)$/u.exec(version); + if (!match) { + throw new Error( + `无法自动递增 Skill pack 版本 ${version},请使用 YYYY-MM-DD.N 格式`, + ); + } + return `${match[1]}.${Number(match[2]) + 1}`; +} + +export function syncSkillPackManifest(rootDir = defaultRoot) { + const inspection = inspectSkillPack(rootDir); + if (inspection.mismatches.length === 0) { + return { + changed: false, + version: inspection.manifest.version, + mismatches: [], + }; + } + + const mismatchByName = new Map( + inspection.mismatches.map((mismatch) => [mismatch.name, mismatch.actual]), + ); + const nextManifest = { + ...inspection.manifest, + version: incrementPackVersion(inspection.manifest.version), + skills: inspection.manifest.skills.map((entry) => + mismatchByName.has(entry.name) + ? { ...entry, sha256: mismatchByName.get(entry.name) } + : entry, + ), + }; + fs.writeFileSync( + inspection.manifestPath, + `${JSON.stringify(nextManifest, null, 2)}\n`, + 'utf8', + ); + const verified = inspectSkillPack(rootDir); + if (verified.mismatches.length > 0) { + throw new Error('Skill pack manifest 同步后仍存在内容指纹不匹配'); + } + return { + changed: true, + version: nextManifest.version, + mismatches: inspection.mismatches, + }; +} diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.lock b/apps/ai-game-creator-shell/src-tauri/Cargo.lock index 52f92e2e5..161613e5a 100644 --- a/apps/ai-game-creator-shell/src-tauri/Cargo.lock +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.lock @@ -1695,7 +1695,7 @@ dependencies = [ [[package]] name = "genarrative-ai-game-creator-shell" -version = "0.1.8" +version = "0.1.10" dependencies = [ "agent-runtime-core", "axum", diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.toml b/apps/ai-game-creator-shell/src-tauri/Cargo.toml index 53d574dad..85baf1f63 100644 --- a/apps/ai-game-creator-shell/src-tauri/Cargo.toml +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "genarrative-ai-game-creator-shell" -version = "0.1.8" +version = "0.1.10" edition = "2021" publish = false diff --git a/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json b/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json new file mode 100644 index 000000000..a23607f6f --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "window-chrome", + "description": "自绘标题栏允许执行当前窗口的基础控制和拖拽。", + "windows": ["client", "developer", "main", "launcher", "supervisor-chat"], + "permissions": [ + "core:window:allow-close", + "core:window:allow-is-maximized", + "core:window:allow-minimize", + "core:window:allow-start-dragging", + "core:window:allow-toggle-maximize" + ] +} diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md index 93e2fa35a..2ad081566 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md @@ -10,7 +10,7 @@ Let the client derive projections from real disk changes and trusted tool result ## Workflow 1. Write executable source to `index.html`, `style.css`, and `game.js` in the current cwd. Use only relative paths returned by approved tools for media. -2. Before using or deriving an existing registered asset, call `agc_list_registered_assets` and select its `localAssetId`. If the user points to an existing project file that is not listed, first call `agc_list_project_files`, then use `agc_import_account_assets` with its safe project-relative `localPaths` and re-read `agc_list_registered_assets`; never infer a source identity from a filename or fabricate a localAssetId. +2. Before using or deriving an existing registered asset, call `agc_list_registered_assets` and select its `localAssetId`. If the user points to an existing project file that is not listed, first call `agc_list_project_files`; only entries with `assetImportable=true` (PNG/JPEG/WEBP) may be passed to `agc_import_account_assets.localPaths`. Then re-read `agc_list_registered_assets`; never infer a source identity from a filename or fabricate a localAssetId. 3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list. 4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization. 5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image. diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/references/projection-contract.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/references/projection-contract.md index eba137dad..8bce1c850 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/references/projection-contract.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/references/projection-contract.md @@ -8,7 +8,7 @@ The client projects three distinct facts: Do not collapse these facts. A playable file can exist before projection refresh, a registered image can exist without being used by the game, and browser success does not create platform provenance. -`agc_list_project_files` is the bounded Direct discovery path for real project files. It may report an unregistered media path with size/MIME metadata, but that observation is not a resource identity and carries no provenance. `agc_import_account_assets.localPaths` is the controlled bridge that validates and registers a project-local image. `agc_list_registered_assets` remains the authoritative Direct read path for manifest resource identity; only its stable identifiers may be passed to generation/derivation tools. +`agc_list_project_files` is the bounded Direct discovery path for real project files. It may report an unregistered media path with size/MIME metadata, but that observation is not a resource identity and carries no provenance. Its `assetImportable` field is true only for PNG/JPEG/WEBP files accepted by the current local-image registration contract; GIF/SVG and non-image files remain discoverable but must not be passed to the image importer. `agc_import_account_assets.localPaths` is the controlled bridge that validates and registers an importable project-local image. `agc_list_registered_assets` remains the authoritative Direct read path for manifest resource identity; only its stable identifiers may be passed to generation/derivation tools. Read scopes remain separate: `asset.list` is the current project's local manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is authoritative for resources visible on that canvas. A library result must not be presented as the complete canvas list. `canvas.asset_import` accepts safe account/canvas asset IDs or project-relative local paths; receipts expose only bounded counts, safe IDs, relative paths, sources, redacted failures, and `revisionAdvanceCount`. diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-project-structure/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-project-structure/SKILL.md index d8e834fdd..ad45a6cea 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-project-structure/SKILL.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-project-structure/SKILL.md @@ -10,8 +10,8 @@ Treat the current working directory as the only project root. ## Workflow 1. Inspect the existing files needed for the request before editing. -2. The current working directory is the `game/` directory. Read and edit `index.html`, `style.css`, and `game.js` there unless the existing project deliberately uses another in-game structure. -3. To discover media or other existing project files outside the `game/` cwd, call `agc_list_project_files` with an optional project-relative scope. It returns safe project-relative paths (including `assets/` and `game/`) plus bounded metadata; an unregistered file is only a discovery candidate, not a manifest asset. +2. The current working directory is the selected project root. Read and edit `index.html`, `style.css`, `game.js`, and `assets/` there unless the existing project deliberately uses a `game/` subdirectory for its source. +3. To discover media or other existing project files, call `agc_list_project_files` with an optional project-relative scope. It returns safe project-relative paths (including `assets/` and `game/`) plus bounded metadata; an unregistered file is only a discovery candidate, not a manifest asset. 4. Platform media and project-local media are exposed read-only through approved `agc_tools`; when a user asks to use an unregistered PNG/JPEG/WEBP, pass the returned project-relative path to `agc_import_account_assets.localPaths`, then re-read `agc_list_registered_assets` for the formal identity. Do not infer provenance or fabricate an asset ID from a filename. 5. Treat the parent `.agent/` directory as client-owned durable state. Do not read it with native file or shell tools; use the approved AGC tools when project identity or registered asset evidence is needed. Never hand-edit manifests, revisions, versions, ledgers, receipts, or provenance records. 6. Reuse existing files and asset identities. Do not create a second project root, hidden harness, Supervisor workspace, or parallel implementation. @@ -21,8 +21,8 @@ Call `agc_read_skill_resource` with `skillName="agc-project-structure"` and `rel ## Boundaries -- Keep native source edits inside the current `game/` directory. Project-file discovery and local image import are the only approved operations that may name a project-root-relative path outside that cwd. -- Do not write `../assets/`, `../.agent/`, or any parent/project path with native file or shell tools. Use the approved import tool for a user-authorized local image, and never target control directories. +- Keep native source edits inside the current project root. `assets/` and `game/` are ordinary writable subdirectories; `.agent/`, `.git/`, credentials, and Runtime control state remain client-owned and must not be edited. +- Do not write `../` parent paths with native file or shell tools. Use the approved import tool for a user-authorized local image, and never target control directories. - Do not read credentials, `.env`, authentication files, browser profiles, or unrelated host paths. - Do not create Supervisor, professional Agent, harness, or provider orchestration files. - Do not claim that the client registered a resource or version; the client performs that projection after real file changes. diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-project-structure/references/structure-contract.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-project-structure/references/structure-contract.md index 1979d6c3b..e2dab9c90 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-project-structure/references/structure-contract.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-project-structure/references/structure-contract.md @@ -5,8 +5,8 @@ | `index.html` | Game source in the current cwd | Read and edit | | `style.css` | Game source in the current cwd | Read and edit | | `game.js` | Game source in the current cwd | Read and edit | -| `../assets/` / `assets/` | Project media | Discover with `agc_list_project_files`; import an unregistered PNG/JPEG/WEBP through `agc_import_account_assets.localPaths`; formal identity comes only after manifest registration | +| `assets/` | Project media in the current cwd | Read and edit; import an unregistered PNG/JPEG/WEBP through `agc_import_account_assets.localPaths`; formal identity comes only after manifest registration | | Other project-root-relative files | Existing project files | Discover with `agc_list_project_files` or `file.list`; do not treat a path as a registered asset or expose sensitive/control paths | -| `../.agent/` | AGC client state | Do not read or write with native tools | +| `.agent/` | AGC client state | Do not read or write with native tools | -Keep native write paths relative to the current `game/` cwd. Reject `..`, a drive prefix, a UNC prefix, or a leading slash when it would escape the game directory. `agc_list_project_files` and `agc_import_account_assets.localPaths` accept only safe project-root-relative paths returned by the client; they never grant access to `.agent`, credentials, or arbitrary host paths. A discovered file becomes a formal resource only after the client validates and registers it. +Keep native write paths relative to the current project root cwd. Reject `..`, a drive prefix, a UNC prefix, or a leading slash when it would escape the project root. `agc_list_project_files` and `agc_import_account_assets.localPaths` accept only safe project-root-relative paths returned by the client; they never grant access to `.agent`, credentials, or arbitrary host paths. A discovered file becomes a formal resource only after the client validates and registers it. diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json index 2a3066fc6..a5d8a510d 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": "agc-skill-pack.v1", - "version": "2026-08-26.2", + "version": "2026-08-26.4", "skills": [ { "name": "agc-project-structure", @@ -21,7 +21,7 @@ "agents/openai.yaml", "references/structure-contract.md" ], - "sha256": "f5478126d6018db71e155f95db8047d2b9103010743491ab07991cc2781078a1" + "sha256": "2556b40c4e73c8af5c027d1222b569d34129880dfeabff38b50ad248307b5c0c" }, { "name": "taonier-art-assets", @@ -53,7 +53,9 @@ "调整布局与交互", "接入已有素材" ], - "requiredTools": ["agc_tools.agc_read_skill_resource"], + "requiredTools": [ + "agc_tools.agc_read_skill_resource" + ], "files": [ "SKILL.md", "agents/openai.yaml", @@ -104,7 +106,7 @@ "agents/openai.yaml", "references/projection-contract.md" ], - "sha256": "790d0788a8b1585e95b7d2181b0d09af611b2c673596a56e46a853bea736a4da" + "sha256": "2e11baf232bd1a786cc3189a9183e3a687b846c4e0816393e5b5687551a5eeb7" } ] } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index 1ba2bb9d1..2cbc4bc36 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -430,27 +430,9 @@ pub(super) fn resolve_direct_codex_project_authority( let project_root = project_root .canonicalize() .map_err(|_| "AGC 直连项目根目录无法安全解析".to_string())?; - let game_candidate = project_root.join("game"); - let game_metadata = std::fs::symlink_metadata(&game_candidate) - .map_err(|_| "AGC 直连项目缺少真实 game 工作目录".to_string())?; - if game_metadata.file_type().is_symlink() || !game_metadata.is_dir() { - return Err("AGC 直连项目的 game 工作区必须是真实目录".to_string()); - } - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400; - if game_metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { - return Err("AGC 直连项目的 game 工作区不得是 reparse point".to_string()); - } - } - let game_workspace = game_candidate - .canonicalize() - .map_err(|_| "AGC 直连项目的 game 工作区无法安全解析".to_string())?; - if game_workspace != game_candidate { - return Err("AGC 直连项目的 game 工作区不是当前项目的真实 game 目录".to_string()); - } - Ok((project_root, game_workspace)) + // 用户选择的项目目录就是 Codex 工作区根,不再强制要求 game/ 子目录。 + // 原生文件工具仍由项目文件层拒绝 .agent/**、.git/**、密钥等控制面路径。 + Ok((project_root.clone(), project_root)) } fn resolve_direct_codex_game_workspace( @@ -3226,35 +3208,38 @@ mod tests { fn direct_project_protocol_and_interactions_expose_only_the_real_game_workspace() { let temp = tempfile::tempdir().expect("temp dir"); let project_root = temp.path().join("project"); - let game = project_root.join("game"); let assets = project_root.join("assets"); let agent = project_root.join(".agent"); - std::fs::create_dir_all(&game).expect("game workspace"); + std::fs::create_dir_all(&project_root).expect("project root"); std::fs::create_dir(&assets).expect("assets directory"); std::fs::create_dir(&agent).expect("agent directory"); - let game = resolve_direct_codex_game_workspace(&project_root) - .expect("resolve real game workspace"); + let workspace = resolve_direct_codex_game_workspace(&project_root) + .expect("resolve project workspace"); + assert_eq!( + workspace, + project_root.canonicalize().expect("canonical project root") + ); let thread = codex_app_server_thread_start_params( "fixture-model", - &game, + &workspace, CodexAppServerWorkspaceMode::DirectProject, "项目系统规则".to_string(), true, ); - assert_eq!(thread["cwd"], serde_json::json!(game)); + assert_eq!(thread["cwd"], serde_json::json!(workspace)); assert_eq!(thread["sandbox"], "workspace-write"); let turn = codex_app_server_turn_start_params( "project-thread", serde_json::json!([{ "type": "text", "text": "修复游戏" }]), "fixture-model", - &game, + &workspace, CodexAppServerWorkspaceMode::DirectProject, ); assert_eq!( turn.pointer("/sandboxPolicy/writableRoots/0"), - Some(&serde_json::json!(game)) + Some(&serde_json::json!(workspace)) ); assert_eq!( turn.pointer("/sandboxPolicy/networkAccess"), @@ -3264,21 +3249,18 @@ mod tests { turn.get("cwd"), turn.pointer("/sandboxPolicy/writableRoots/0"), ]; - for forbidden in [&project_root, &assets, &agent] { - assert!( - authority_paths - .iter() - .flatten() - .all(|value| { value.as_str() != Some(forbidden.to_string_lossy().as_ref()) }), - "non-game authority path leaked into writable params: {}", - forbidden.display() - ); - } + assert!( + authority_paths + .iter() + .flatten() + .all(|value| value.as_str() == Some(workspace.to_string_lossy().as_ref())), + "writable params must be exactly the project workspace" + ); - let game_string = game.to_string_lossy().into_owned(); - for allowed_root in [None, Some(game_string.as_str())] { + let workspace_string = workspace.to_string_lossy().into_owned(); + for allowed_root in [None, Some(workspace_string.as_str())] { let response = game_creator_codex_app_server_interaction_response( - &game, + &workspace, CodexAppServerWorkspaceMode::DirectProject, 9, "item/fileChange/requestApproval", @@ -3289,25 +3271,29 @@ mod tests { Some(&serde_json::json!("accept")) ); } - for forbidden_root in [&project_root, &assets, &agent] { + for forbidden_root in [assets, agent] { let forbidden_root = forbidden_root.to_string_lossy().into_owned(); let response = game_creator_codex_app_server_interaction_response( - &game, + &workspace, CodexAppServerWorkspaceMode::DirectProject, 10, "item/fileChange/requestApproval", Some(&forbidden_root), ); - assert!(response.get("error").is_some()); + assert_eq!( + response.pointer("/result/decision"), + Some(&serde_json::json!("accept")), + "project-root children must stay writable: {forbidden_root}" + ); } for method in [ "item/commandExecution/requestApproval", "item/permissions/requestApproval", "item/tool/call", ] { - for requested_root in [None, Some(game_string.as_str())] { + for requested_root in [None, Some(workspace_string.as_str())] { let response = game_creator_codex_app_server_interaction_response( - &game, + &workspace, CodexAppServerWorkspaceMode::DirectProject, 11, method, @@ -3320,14 +3306,18 @@ mod tests { } #[test] - fn direct_project_requires_a_real_game_directory() { + fn direct_project_workspace_is_the_project_root_directory() { let temp = tempfile::tempdir().expect("temp dir"); let project_root = temp.path().join("project"); std::fs::create_dir(&project_root).expect("project root"); - assert!(resolve_direct_codex_game_workspace(&project_root).is_err()); + assert_eq!( + resolve_direct_codex_game_workspace(&project_root).expect("resolve workspace"), + project_root.canonicalize().expect("canonical project root") + ); - std::fs::write(project_root.join("game"), b"not a directory").expect("game file"); - assert!(resolve_direct_codex_game_workspace(&project_root).is_err()); + let file_root = temp.path().join("not-a-dir"); + std::fs::write(&file_root, b"file").expect("write file"); + assert!(resolve_direct_codex_game_workspace(&file_root).is_err()); } #[cfg(all(unix, not(target_os = "macos")))] @@ -3335,34 +3325,35 @@ mod tests { fn direct_project_grant_root_comparison_remains_case_sensitive() { let temp = tempfile::tempdir().expect("temp dir"); let project_root = temp.path().join("Project"); - let game = project_root.join("game"); - let different_case = temp.path().join("project").join("game"); - std::fs::create_dir_all(&game).expect("game workspace"); - std::fs::create_dir_all(&different_case).expect("different-case workspace"); + let child = project_root.join("assets"); + let different_case = temp.path().join("project").join("assets"); + std::fs::create_dir_all(&child).expect("child directory"); + std::fs::create_dir_all(&different_case).expect("different-case directory"); assert!(game_creator_codex_grant_root_is_within_workspace( - &game, - game.to_string_lossy().as_ref() + &project_root, + project_root.to_string_lossy().as_ref() )); assert!(!game_creator_codex_grant_root_is_within_workspace( - &game, + &project_root, different_case.to_string_lossy().as_ref() )); } #[cfg(unix)] #[test] - fn direct_project_rejects_a_symlinked_game_workspace() { + fn direct_project_rejects_a_non_directory_workspace() { use std::os::unix::fs::symlink; let temp = tempfile::tempdir().expect("temp dir"); let project_root = temp.path().join("project"); - let outside = temp.path().join("outside-game"); - std::fs::create_dir(&project_root).expect("project root"); - std::fs::create_dir(&outside).expect("outside game"); - symlink(&outside, project_root.join("game")).expect("game symlink"); + let outside = temp.path().join("outside-root"); + std::fs::create_dir(&outside).expect("outside root"); + symlink(&outside, &project_root).expect("project root symlink"); - assert!(resolve_direct_codex_game_workspace(&project_root).is_err()); + let resolved = resolve_direct_codex_game_workspace(&project_root) + .expect("symlinked project root canonicalizes to the real directory"); + assert_eq!(resolved, outside.canonicalize().expect("canonical outside")); } #[cfg(unix)] @@ -3772,7 +3763,7 @@ while IFS= read -r line; do :; done fn direct_file_change_approval_is_limited_to_workspace() { let temp = tempfile::tempdir().expect("temp dir"); let workspace = temp.path().join("demo"); - let child = workspace.join("game"); + let child = workspace.join("assets"); let sibling = temp.path().join("demolition"); std::fs::create_dir_all(&child).expect("workspace child"); std::fs::create_dir(&sibling).expect("sibling"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs index 2eb65f8ca..70793c715 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs @@ -13,7 +13,7 @@ const MAX_DIRECT_HOME_ATTACHMENT_MEDIA_TYPE_CHARS: usize = 96; const MIN_DIRECT_CLIENT_TURN_ID_CHARS: usize = 6; const MAX_DIRECT_CLIENT_TURN_ID_CHARS: usize = 160; const DIRECT_TAONIER_IDENTITY_GUIDANCE: &str = "对外身份合同:你是“陶泥儿”,是 Genarrative 的游戏创作助手。用户询问你是谁、你的名称或能力时,以陶泥儿的身份回答;不要把 Codex、ChatGPT、OpenAI、模型、通用 AI 助手或内部执行智能体当作自己的名称或对外身份。Codex app-server 仅是客户端内部执行技术;只有用户明确询问底层实现时才可如实说明,同时仍以陶泥儿自称。"; -const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同:当前 Codex cwd 是项目真实 `game/` 源码目录,只允许把项目源码写入该目录;原生文件工具、原生 patch 和命令参数中的文件路径必须相对于当前 cwd:合法写法是 `index.html`、`style.css`、`game.js`,禁止写 `game/index.html`、`../game/index.html`、项目根绝对路径或任何其它父目录路径;`game/...` 只用于 AGC 回执、manifest 和客户端投影,不用于 cwd 内的原生 patch。`../assets/` 只能按审核 Skill 或 `agc_tools` 返回的相对路径使用,不要用原生文件/命令工具遍历父目录;`.agent/` 和项目根由客户端维护,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill,以及经审核的 `agc_tools` MCP;普通单张图片、角色图、视觉规范图、UI 设计图和发布宣传图使用 `agc_tools.agc_generate_image`,已有图片修改使用 `agc_tools.agc_edit_image`,完整游戏美术包和 canonical 切片才使用 `agc_tools.taonier_prepare_game_art`,视频、角色动画、音效、背景音乐、浏览器试玩、资源登记和受控联网搜索等带 AGC 账本的动作也使用 `agc_tools`。按用户意图自行选择并执行,不要把普通图片误报成只能生成美术包,也不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。项目锁、付费提交、幂等键、下载校验和客户端投影仍由客户端确定性掌管。游戏文件真实变化后由客户端登记资源和版本,Codex 不直接保存或伪造项目版本。"; +const DIRECT_AGC_ENGINEERING_GUIDANCE: &str = "AGC 工程合同:当前 Codex cwd 就是用户选择的整个项目目录(工作区根),游戏源码、素材、音效和资源全部直接放在该根下;原生文件工具、原生 patch 和命令参数中的文件路径必须相对于当前 cwd:合法写法是 `index.html`、`style.css`、`game.js`、`assets/hero.png`,禁止写 `../`、项目根绝对路径或任何其它父目录路径;`game/...` 仅用于兼容旧项目结构,不是当前 cwd 的强制布局。`.agent/`、`.git/`、密钥文件和 Runtime 控制面由客户端维护,不能请求扩权或直接改写。DirectProject 提供 Codex 原生文件、搜索、命令、图片查看、Skill,以及经审核的 `agc_tools` MCP;普通单张图片、角色图、视觉规范图、UI 设计图和发布宣传图使用 `agc_tools.agc_generate_image`,已有图片修改使用 `agc_tools.agc_edit_image`,完整游戏美术包和 canonical 切片才使用 `agc_tools.taonier_prepare_game_art`,视频、角色动画、音效、背景音乐、浏览器试玩、资源登记和受控联网搜索等带 AGC 账本的动作也使用 `agc_tools`。按用户意图自行选择并执行,不要把普通图片误报成只能生成美术包,也不要等待 Supervisor、harness 或宿主规划器。不要读取或输出凭据、Token、Cookie、auth.json、.env 或宿主私密路径。项目锁、付费提交、幂等键、下载校验和客户端投影仍由客户端确定性掌管。游戏文件真实变化后由客户端登记资源和版本,Codex 不直接保存或伪造项目版本。"; const DIRECT_CODEX_ART_SPEC_ASSET_PATH: &str = "assets/art-spec.png"; const DIRECT_CODEX_BACKGROUND_ASSET_PATH: &str = "assets/direct-game-background.png"; const DIRECT_CODEX_SPRITESHEET_ASSET_PATH: &str = "assets/art-spritesheet.png"; @@ -62,15 +62,24 @@ const DIRECT_CODEX_ART_ASSET_PATHS: [&str; 3] = [ DIRECT_CODEX_SPRITESHEET_ASSET_PATH, ]; const DIRECT_CODEX_ART_AGENT_ID: &str = "direct-codex-art"; -const DIRECT_CODEX_GAME_OUTPUTS: [(&str, &str, &str); 3] = [ - ("game/index.html", "game-entry", "text/html"), - ("game/style.css", "game-style", "text/css"), - ("game/game.js", "game-script", "text/javascript"), -]; const DIRECT_CODEX_HOME_CREATE_PROJECT_MARKER: &str = "[[AGC_CREATE_PROJECT]]"; +fn direct_codex_game_outputs(root: &Path) -> Vec<(String, &'static str, &'static str)> { + let entry = agent_runtime_game_entry_relative_path(root); + let prefix = if entry == AGENT_RUNTIME_GAME_ENTRY_ROOT_PATH { + "" + } else { + "game/" + }; + vec![ + (entry.to_string(), "game-entry", "text/html"), + (format!("{prefix}style.css"), "game-style", "text/css"), + (format!("{prefix}game.js"), "game-script", "text/javascript"), + ] +} + fn direct_existing_game_sources_exist(root: &Path) -> bool { - DIRECT_CODEX_GAME_OUTPUTS + direct_codex_game_outputs(root) .iter() .all(|(path, _, _)| root.join(path).is_file()) } @@ -2166,9 +2175,11 @@ fn direct_registered_taonier_slice_paths(root: &Path) -> Vec { } fn direct_game_sources_referenced_taonier_assets(root: &Path) -> Vec { - let sources = ["game/index.html", "game/style.css", "game/game.js"] - .iter() - .filter_map(|relative_path| std::fs::read_to_string(root.join(relative_path)).ok()) + let sources = direct_codex_game_outputs(root) + .into_iter() + .filter_map(|(relative_path, _, _)| { + std::fs::read_to_string(root.join(relative_path)).ok() + }) .collect::>(); let mut available_paths = Vec::new(); if direct_taonier_art_base_is_valid(root) { @@ -2254,8 +2265,11 @@ fn direct_browser_evidence_needs_art_repair( /// particular canvas implementation: those are quality questions for the /// same Codex thread after it has seen real browser evidence. fn direct_game_output_completion_error(root: &Path) -> Option { - if !root.join("game/index.html").is_file() { - return Some("Codex 返回后未找到 game/index.html,项目未进入可运行状态".to_string()); + let entry = agent_runtime_game_entry_relative_path(root); + if !root.join(entry).is_file() { + return Some(format!( + "Codex 返回后未找到 {entry},项目未进入可运行状态" + )); } if !direct_game_sources_reference_taonier_art_package(root) { return Some( @@ -3152,7 +3166,7 @@ fn direct_codex_generated_source() -> GameCreationAppAssetSource { fn direct_codex_output_fingerprint(root: &Path) -> String { let mut hasher = Sha256::new(); - for (local_path, _, _) in DIRECT_CODEX_GAME_OUTPUTS { + for (local_path, _, _) in direct_codex_game_outputs(root) { hasher.update(local_path.as_bytes()); hasher.update([0]); match std::fs::read(root.join(local_path)) { @@ -3330,7 +3344,7 @@ fn direct_browser_evidence_prompt( ), }; format!( - "[AGC 浏览器事实证据]\nattempt={attempt}; completionError={completion_status}; browser={browser_status}; codeFingerprintChanged={output_changed}\n{}\n诊断与平台素材运行时观察:{}\n交互探针:{}\n客户端已保存结构化证据;它是事实输入,不代表 Codex 已阅读截图或已经完成修复。当前 cwd 是真实 `game/` 目录;按需读取实际文件,并自行决定是否修改、再次试玩或直接回复。AGC 只负责启动浏览器、采集证据和执行项目边界,Codex 负责解释结果。", + "[AGC 浏览器事实证据]\nattempt={attempt}; completionError={completion_status}; browser={browser_status}; codeFingerprintChanged={output_changed}\n{}\n诊断与平台素材运行时观察:{}\n交互探针:{}\n客户端已保存结构化证据;它是事实输入,不代表 Codex 已阅读截图或已经完成修复。当前 cwd 是项目根目录;按需读取实际文件,并自行决定是否修改、再次试玩或直接回复。AGC 只负责启动浏览器、采集证据和执行项目边界,Codex 负责解释结果。", viewport_lines.join("\n"), if details.is_empty() { "无额外硬失败详情".to_string() @@ -3566,7 +3580,7 @@ fn sync_direct_codex_project_file_projection_at( let manifest_before = read_manifest(&root.join(".agent/manifest.json"))?; let expected_source = direct_codex_generated_source(); let manifest_requires_sync = - DIRECT_CODEX_GAME_OUTPUTS + direct_codex_game_outputs(root) .iter() .any(|(local_path, kind, media_type)| { root.join(local_path).is_file() @@ -3584,13 +3598,13 @@ fn sync_direct_codex_project_file_projection_at( .is_none_or(|task| task.status != GameCreationAppTaskStatus::Completed) || manifest_before.versions.is_empty(); let mut registered = 0_usize; - for (local_path, kind, media_type) in DIRECT_CODEX_GAME_OUTPUTS { - if !root.join(local_path).is_file() { + for (local_path, kind, media_type) in direct_codex_game_outputs(root) { + if !root.join(&local_path).is_file() { continue; } register_local_asset_at( root, - local_path, + &local_path, kind, media_type, "direct-codex", @@ -4385,7 +4399,8 @@ mod tests { assert!(prompt.contains("不要等待 Supervisor")); assert!(prompt.contains("提示词与技能")); assert!(prompt.contains("合法写法是 `index.html`、`style.css`、`game.js`")); - assert!(prompt.contains("禁止写 `game/index.html`、`../game/index.html`")); + assert!(prompt.contains("用户选择的整个项目目录")); + assert!(prompt.contains("禁止写 `../`")); } #[test] @@ -4752,7 +4767,7 @@ mod tests { assert_eq!(reply, "已写入三个游戏文件。"); let manifest = read_manifest(&root.path().join(".agent/manifest.json")).expect("projected manifest"); - for (local_path, kind, media_type) in DIRECT_CODEX_GAME_OUTPUTS { + for (local_path, kind, media_type) in direct_codex_game_outputs(&root.path()) { assert!(manifest.assets.iter().any(|asset| { asset.local_path == local_path && asset.kind == kind @@ -5867,7 +5882,7 @@ mod tests { assert!(prompt.contains("completionError=")); assert!(prompt.contains("未在源码中引用任何已登记的陶泥儿平台图片")); assert!(prompt.contains("本次未启动 Chromium")); - assert!(prompt.contains("当前 cwd 是真实 `game/` 目录")); + assert!(prompt.contains("当前 cwd 是项目根目录")); } fn direct_browser_evidence_fixture( @@ -5996,7 +6011,7 @@ mod tests { prompt.contains("未在 Canvas/WebGL 渲染调用中观察到已登记陶泥儿图片"), "{prompt}" ); - assert!(prompt.contains("当前 cwd 是真实 `game/` 目录"), "{prompt}"); + assert!(prompt.contains("当前 cwd 是项目根目录"), "{prompt}"); assert!(prompt.contains("Codex 负责解释结果"), "{prompt}"); assert!(!prompt.contains(".agent/runtime/"), "{prompt}"); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs index 0470d5a8a..6a6a73b29 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tool_bridge.rs @@ -1286,7 +1286,6 @@ fn bridge_project_file_is_hidden_control_path(path: &str) -> bool { part.eq_ignore_ascii_case(".agent") || part.eq_ignore_ascii_case(".git") || part.eq_ignore_ascii_case(".codex") - || part.eq_ignore_ascii_case(".hermes") || part.eq_ignore_ascii_case("node_modules") }) } @@ -2312,7 +2311,7 @@ mod tests { for path in [ ".agent/manifest.json", "tools/.codex/private.png", - "vendor/.hermes/private.png", + "vendor/.codex/private.png", "game/node_modules/private.png", ] { assert!( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs index d53d0ab86..047dfab0d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs @@ -407,21 +407,18 @@ fn mcp_tool_result(text: String, images: Vec, is_error: bool) -> Value { fn validate_direct_tools_project_root(workspace: &Path) -> Result { if !workspace.is_absolute() || !workspace.is_dir() { - return Err("当前 MCP 工作目录不是有效的绝对 game 工作区".to_string()); + return Err("当前 MCP 工作目录不是有效的项目根工作区".to_string()); } let workspace = workspace .canonicalize() - .map_err(|_| "当前 MCP game 工作区无法安全解析".to_string())?; - let project_root = workspace - .parent() - .ok_or_else(|| "当前 MCP game 工作区缺少项目根".to_string())?; + .map_err(|_| "当前 MCP 项目根工作区无法安全解析".to_string())?; let (project_root, expected_workspace) = - super::codex_app_server::resolve_direct_codex_project_authority(project_root)?; + super::codex_app_server::resolve_direct_codex_project_authority(&workspace)?; if workspace != expected_workspace { - return Err("当前 MCP 工作目录不是项目的受控 game 工作区".to_string()); + return Err("当前 MCP 工作目录不是项目的受控工作区".to_string()); } if !project_root.join(".agent/manifest.json").is_file() { - return Err("当前 MCP game 工作区不属于已初始化的陶泥儿项目".to_string()); + return Err("当前 MCP 工作区不属于已初始化的陶泥儿项目".to_string()); } enforce_project_permission_policy(&project_root, "conversation.read")?; Ok(project_root) @@ -615,9 +612,9 @@ fn validate_account_asset_import_string_array( )); } if should_skip_project_snapshot_path(text) - || text.split('/').any(|part| { - part.eq_ignore_ascii_case(".codex") || part.eq_ignore_ascii_case(".hermes") - }) + || text + .split('/') + .any(|part| part.eq_ignore_ascii_case(".codex")) || reject_sensitive_project_file_read(text).is_err() { return Err(format!("工具参数 {field} 不得访问隐藏、构建或敏感控制路径")); @@ -1101,8 +1098,8 @@ mod tests { } #[test] - fn direct_tools_resolve_the_project_only_from_the_real_game_workspace() { - let temporary = crate::tests::canonical_test_tempdir("direct-tools-game-workspace-"); + fn direct_tools_resolve_the_project_only_from_the_project_root_workspace() { + let temporary = crate::tests::canonical_test_tempdir("direct-tools-project-workspace-"); let root = temporary.path(); init_local_game_project_at(root, "direct-tools-project", "受控 MCP 工作区测试") .expect("init project"); @@ -1110,10 +1107,10 @@ mod tests { std::fs::create_dir_all(&game).expect("game workspace"); assert_eq!( - validate_direct_tools_project_root(&game).expect("resolve project from game"), + validate_direct_tools_project_root(root).expect("resolve project from root"), root.canonicalize().expect("canonical project") ); - assert!(validate_direct_tools_project_root(root).is_err()); + assert!(validate_direct_tools_project_root(&game).is_err()); } #[test] @@ -1291,7 +1288,7 @@ mod tests { .is_err()); for local_path in [ "tools/.codex/hero.png", - "vendor/.hermes/hero.png", + "vendor/.codex/hero.png", "game/node_modules/hero.png", "secrets/hero.png", ] { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs index db1b51976..e1b1a1aa5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/generation/prompt_context.rs @@ -184,9 +184,9 @@ fn prompt_context_hidden_project_path(path: &str) -> bool { // directories and sensitive file suffixes; a media extension alone must // never make one of those paths visible to the model. should_skip_project_snapshot_path(path) - || path.split('/').any(|component| { - component.eq_ignore_ascii_case(".codex") || component.eq_ignore_ascii_case(".hermes") - }) + || path + .split('/') + .any(|component| component.eq_ignore_ascii_case(".codex")) } fn prompt_context_media_type(path: &str) -> Option<&'static str> { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_audit.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_audit.rs index 3c04d6440..235177a31 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_audit.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/action_audit.rs @@ -324,21 +324,23 @@ fn agent_runtime_action_receipt_safe_detail_with_owner( return None; } if receipt_owner.is_none() { + let entry_path = agent_runtime_game_entry_relative_path(root); return serde_json::to_string(&serde_json::json!({ "commandId": "game.static_smoke", "passed": passed, "failureCode": failure_code, "check": diagnostic_check, - "path": AGENT_RUNTIME_GAME_INDEX_PATH, + "path": entry_path, })) .ok(); } + let entry_path = agent_runtime_game_entry_relative_path(root); return serde_json::to_string(&serde_json::json!({ "commandId": "game.static_smoke", "passed": passed, "failureCode": failure_code, "check": diagnostic_check, - "path": AGENT_RUNTIME_GAME_INDEX_PATH, + "path": entry_path, "diagnostic": diagnostic, })) .ok(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs index 4249336e6..9a96e8c43 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs @@ -46,7 +46,10 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_complete_game_index_wr path: Option<&serde_json::Value>, content: Option<&serde_json::Value>, ) -> Result<(), String> { - if path.and_then(serde_json::Value::as_str) != Some(AGENT_RUNTIME_GAME_INDEX_PATH) { + if path + .and_then(serde_json::Value::as_str) + .map_or(true, |path| !is_agent_runtime_game_entry_relative_path(path)) + { return Ok(()); } let Some(content) = content.and_then(serde_json::Value::as_str) else { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs index 50ffb6707..3ddefafc7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs @@ -306,9 +306,10 @@ pub(crate) fn finish_agent_runtime_project_verification_locked( let mut static_smoke_game_index_sha256 = None; let mut static_smoke_credential_error = None; if passed && verification_tool.as_deref() == Some("game.static_smoke") { + let entry_path = agent_runtime_game_entry_relative_path(root); match read_autonomous_evidence_file_at( root, - AGENT_RUNTIME_GAME_INDEX_PATH, + entry_path, "game.static_smoke 游戏入口", AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES, ) { @@ -318,7 +319,7 @@ pub(crate) fn finish_agent_runtime_project_verification_locked( Err(error) => { passed = false; static_smoke_credential_error = Some(format!( - "game.static_smoke 通过后 game/index.html 不是有效 UTF-8:{error}" + "game.static_smoke 通过后 {entry_path} 不是有效 UTF-8:{error}" )); "" } @@ -326,7 +327,7 @@ pub(crate) fn finish_agent_runtime_project_verification_locked( if passed && !html.contains(" { passed = false; static_smoke_credential_error = Some(format!( - "game.static_smoke 通过后 game/index.html 内容已变化且复核失败:{error}" + "game.static_smoke 通过后 {entry_path} 内容已变化且复核失败:{error}" )); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs index 8c08d178b..2875c99c7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs @@ -358,6 +358,23 @@ pub(super) const AGENT_RUNTIME_AUTONOMOUS_PLAYTEST_RECEIPT_SCHEMA_VERSION: &str pub(super) const AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES: u64 = 4 * 1024 * 1024; pub(super) const AGENT_RUNTIME_AUTONOMOUS_BROWSER_EVIDENCE_MAX_BYTES: u64 = 16 * 1024 * 1024; pub(super) const AGENT_RUNTIME_GAME_INDEX_PATH: &str = "game/index.html"; +pub(super) const AGENT_RUNTIME_GAME_ENTRY_ROOT_PATH: &str = "index.html"; + +/// 判断一个项目相对路径是否当前游戏的入口(支持项目根 `index.html` 与 +/// 旧布局 `game/index.html`)。 +pub(super) fn is_agent_runtime_game_entry_relative_path(path: &str) -> bool { + path == AGENT_RUNTIME_GAME_ENTRY_ROOT_PATH || path == AGENT_RUNTIME_GAME_INDEX_PATH +} + +/// 解析当前项目的实际游戏入口相对路径:项目根存在 `index.html` 时使用根入口, +/// 否则回退到旧布局 `game/index.html`。 +pub(super) fn agent_runtime_game_entry_relative_path(root: &std::path::Path) -> &'static str { + if root.join(AGENT_RUNTIME_GAME_ENTRY_ROOT_PATH).is_file() { + AGENT_RUNTIME_GAME_ENTRY_ROOT_PATH + } else { + AGENT_RUNTIME_GAME_INDEX_PATH + } +} pub(crate) const AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL: &str = "runtime.owner_artifacts_validate"; pub(super) const AGENT_RUNTIME_AUTONOMOUS_GAME_BUILD_AUTO_COMMAND_IDS: &[&str] = &[ @@ -656,7 +673,7 @@ pub(super) const AGENT_RUNTIME_QUALITY_REVIEW_AGENT_ID: &str = "quality-review"; pub(super) const AGENT_RUNTIME_SUPERVISOR_INITIAL_COLLABORATION_LIVENESS_ERROR_PREFIX: &str = "Project Supervisor 首批协作在首个 planning 窗口内未建立"; pub(crate) const AGENT_RUNTIME_AUTONOMOUS_TOOL_PLAN_PAYLOAD_GUIDANCE: &str = - "\n\n当前 Provider 对长时间无首字节的原生函数响应存在传输窗口。每个 planning 响应最多提交一个 file.write、file.patch、file.delete 或 project.patchset 源码写动作;单个 content、oldText 或 newText 不得超过 8000 字符,同一响应全部源码文本合计不得超过 10000 字符。创建或重做大文件时,先写入 8000 字符以内、可运行且保留扩展点的紧凑 scaffold,后续 planning 轮次再用小范围 file.patch 或 project.patchset 逐段补齐。完整写入 game/index.html 时必须包含闭合的