Compare commits

..

29 Commits

Author SHA1 Message Date
k88936 f7edc9d0ee 区分 Raw 图片 PNG 资源限制错误
Project CI / Frontend tests (pull_request) Successful in 4m43s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Native shell tests (pull_request) Failing after 5m14s
将有效但超出解码限制的 PNG 映射为明确的 400 提示

补充超大合法 PNG 的回归测试
2026-09-09 11:04:44 +08:00
k88936 f60771dd00 提前检查 Raw 图片请求预算
在构造 multipart 表单前校验请求 deadline

避免预算耗尽时复制输入图片并缩短耗时统计范围
2026-09-09 10:58:48 +08:00
k88936 f7cccd8592 补充 Raw 图片客户端错误上下文
为 HTTP 客户端构造失败统一保留操作上下文

补充配置错误文案回归测试
2026-09-09 10:57:04 +08:00
k88936 5ee2d940c1 修正 Raw 图片尺寸校验错误语义
尺寸校验失败改用请求参数无效语义

补充错误文案回归测试
2026-09-09 10:55:36 +08:00
k88936 c75a7725ce Merge remote-tracking branch 'origin/master' into feat/expose-image2edit
Project CI / Repository checks (pull_request) Successful in 2m51s
Project CI / Native shell tests (pull_request) Failing after 4m9s
Project CI / Backend tests (pull_request) Failing after 4m58s
Project CI / Frontend tests (pull_request) Successful in 3m46s
2026-09-09 10:42:22 +08:00
k88936 3a95715990 同步 Raw 图片编辑技术方案
Project CI / Native shell tests (pull_request) Failing after 6m36s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / Frontend tests (pull_request) Successful in 4m22s
记录解码资源上限和阻塞线程策略
补充输出格式回退与成功追踪约定
2026-09-08 23:41:27 +08:00
k88936 ed86963de2 修正 Raw 图片追踪结果类型
明确输出数据集合类型
保持 API 编译检查通过
2026-09-08 23:38:25 +08:00
k88936 65e73eee02 补充 Raw 图片生成成功追踪
记录成功任务与输出数量
完善上游响应和失败阶段日志
2026-09-08 23:25:47 +08:00
k88936 6068eeda83 补充 Raw 图片编辑请求日志
记录上游响应状态与耗时
记录传输失败阶段和结构化上下文
2026-09-08 23:22:56 +08:00
k88936 0e244c64ea 统一 GPT Image 2 尺寸常量
集中维护像素边界和对齐规则
供生成与编辑路径共同引用
2026-09-08 22:11:35 +08:00
k88936 ad7c059603 遵守 Raw 图片请求截止时间
发送前裁剪有效超时预算
预算耗尽时提前返回结构化错误
2026-09-08 21:54:22 +08:00
k88936 55d2a393d1 完善 Raw 图片尺寸错误类型
为公开错误实现 Error trait
支持通用错误链处理
2026-09-08 21:52:15 +08:00
k88936 e7f66ce950 修正 Raw 图片传输失败阶段标记
区分请求发送和响应读取失败
让错误详情准确反映故障阶段
2026-09-08 20:03:13 +08:00
k88936 35dcfea3ef 修正 Raw 图片输出格式回退
忽略空值后再使用载荷级格式
补充空字符串和 null 回归测试
2026-09-08 19:56:41 +08:00
k88936 cea8bf77fa 减少 Raw 图片编辑请求拷贝
移动 multipart 选项字段而非克隆
降低掩码和字符串的峰值内存
2026-09-08 19:52:13 +08:00
k88936 0136bfe564 移出 Raw 图片同步解码
将请求预校验放入阻塞线程
避免占用 Tokio 异步工作线程
2026-09-08 19:48:58 +08:00
k88936 22785b4d7b 限制 Raw 图片解码资源
为 PNG 解码设置边长和分配上限
避免压缩图片膨胀导致内存耗尽
2026-09-08 19:45:02 +08:00
k88936 a86a2ee4d2 简化 Raw 图片编辑响应类型
移除无必要的 JSON Value 转换
保持响应结构由类型系统校验
2026-09-08 19:32:55 +08:00
k88936 b22eae2726 收紧 Raw 图片编辑输入契约
Project CI / Frontend tests (pull_request) Successful in 3m51s
Project CI / Native shell tests (pull_request) Successful in 20m20s
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Backend tests (pull_request) Failing after 14s
新增 3840 边长、16 像素对齐、3:1 比例和总像素范围校验

要求 image 与 mask 为可完整解码的 PNG 并在扣费前拒绝非法输入

将 Raw 路由 JSON body limit 提升至 64 MiB

同步 Raw 图片编辑技术方案和边界测试
2026-09-08 18:18:58 +08:00
k88936 35d0db6377 修复 Raw 图片失败审计与响应格式
Project CI / Frontend tests (pull_request) Successful in 6m7s
Project CI / Native shell tests (pull_request) Successful in 19m41s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / Backend tests (pull_request) Failing after 9s
为 Raw provider 失败分支补齐结构化 failure audit 并接入 api-server 记录链

使用 VectorEngine 响应 output_format 生成返回图片 MIME 与扩展名

补充响应格式和审计字段定向测试并同步技术方案
2026-09-08 16:19:29 +08:00
k88936 f087ba3e2b 隔离 Raw 图片代理实现
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Successful in 36m40s
Project CI / Native shell tests (pull_request) Successful in 54m1s
移除对现有图片编辑 client 与 curl transport 的复用

将 Raw GPT Image 2 请求、multipart 构造和响应解析收敛到独立文件

保持现有 editor 图片链路代码不变
2026-09-08 13:54:31 +08:00
k88936 7fc1b841be 收口 Raw 图片 base64 校验错误
客户端仅返回稳定的 base64 格式错误提示

补充测试避免泄露解码器内部诊断细节
2026-09-08 12:30:55 +08:00
k88936 a576fdcc37 移除未使用的 multipart 请求包装函数
无选项测试直接调用统一传输入口

避免生产构建保留未使用函数告警
2026-09-08 12:29:59 +08:00
k88936 4dcf38715b 补充图片编辑选项 multipart 测试
覆盖质量、背景、输出格式与 mask 文件字段

断言文件名、MIME 类型和字节内容均正确发送
2026-09-08 12:21:11 +08:00
k88936 d1f9f4e1ef 减少图片编辑 multipart 字节复制
将已克隆的选项按值传入阻塞传输函数

构造 mask 与参考图表单时直接移动字节缓冲
2026-09-08 12:16:00 +08:00
k88936 5836aaec26 简化图片编辑 multipart 请求分支
统一通过带选项的 multipart 传输函数发送请求

移除冗余包装函数调用与未使用导入
2026-09-08 12:10:56 +08:00
k88936 18d60feb04 补充 Raw 图片参数预检查
在计费前校验质量、背景和输出格式

规范化可选参数并保持单图请求契约
2026-09-08 10:40:09 +08:00
k88936 4e1316e272 实现 Raw GPT Image 2 图片编辑代理
新增受 Bearer 保护的 /api/raw/v1/images/edit JSON 路由

按单图 image 与可选 mask 转发 GPT Image 2 参数

固定 model 与 n 并返回仅含 data[].b64_json 的响应

复用钱包计费与退款边界并补充单元测试
2026-09-08 10:36:48 +08:00
k88936 302b4addae 补充 Raw GPT Image 2 代理方案
Project CI / Native shell tests (pull_request) Successful in 55m40s
Project CI / Backend tests (pull_request) Failing after 14s
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / Frontend tests (pull_request) Successful in 23m43s
明确 JSON 请求与 data 数组响应

记录预检查和钱包事务边界

拆分 api-server 与 platform-image 文件职责
2026-09-07 16:33:34 +08:00
591 changed files with 71808 additions and 99872 deletions
-1
View File
@@ -5,7 +5,6 @@
## 目录约定
- `.codex/skills/` 是项目专属 skill 根目录。每个 skill 以目录中的 `SKILL.md` 为入口,配套的参考资料和脚本放在同一目录下。
- `spec-driven-development` 负责跨模块、公开契约、SpacetimeDB、AGC/Runtime 和复杂 UI 任务的规范先行与逐里程碑验收;具体规则以 `docs/` 下的 SDD 工作流和模板为准。
- `.codex/plugins/` 保存随仓库分发的项目插件资源及其参考资料。当前的 `game-studio` 插件提供浏览器游戏设计、原型、2D/3D 技术栈、素材管线和 playtest 工作流;是否启用遵循当前 Codex 的插件加载机制,不依赖旧工具的环境变量或个人配置脚本。
- `.codex/hooks/``.codex/environments/` 等目录保存项目工具链所需的 hooks 和环境模板;它们不替代项目代码中的运行时配置。
- 长期有效的产品、架构、接口、排障和协作知识统一放在 `docs/``docs/project-memory/`,不复制到本目录。
@@ -1,43 +0,0 @@
---
name: spec-driven-development
description: 在 Genarrative 中处理跨模块功能、公开 API/DTO、SpacetimeDB schema、AGC/Runtime 或复杂 UI 状态链路时,按主规范、里程碑规范、单里程碑实现计划和验收证据推进;小型局部修复不触发。
license: MIT
metadata:
codex:
tags: [SDD, 规范驱动开发, 主规范, 里程碑, 验收]
---
# SDD 规范驱动开发
本 skill 把 Genarrative 的复杂任务路由到规范先行、单里程碑交付的工作流。权威规则和模板分别见:
- `docs/【协作规范】规范驱动开发工作流-2026-09-12.md`
- `docs/project-memory/shared-memory/【模板】规范驱动开发主规范与里程碑模板-2026-09-12.md`
## 触发条件
使用本 skill 的任务包括:
- 跨前端、`api-server``platform-*``spacetime-client``spacetime-module``module-*` 的功能。
- `/api/external/v1`、共享 DTO、OpenAPI、SpacetimeDB schema、迁移或跨版本重放合同变化。
- AGC / DirectProject、Agent Runtime、工具白名单、审批、持久化恢复或资源工作流变化。
- 复杂 UI 状态链路、入口/页面生命周期变化,或需要多个独立验收面的功能。
文案、单文件局部修复、无行为变化重构和一次性诊断继续使用轻量流程;执行中一旦触及公开行为或跨模块合同,切换到本 skill。
## 执行要求
1. 检查工作树,读取当前专题、源码、测试、契约和历史决策;写清交付结果、验收判据和不做项。
2. 找到或更新唯一主规范。主规范描述行为合同、边界、非目标、兼容/迁移和证据,不绑定实现细节。
3.`docs/project-memory/plans/` 创建一个或多个 `【里程碑】中文标题-YYYY-MM-DD.md`,填写 `Version``Status``Date``Parent Spec`、范围、依赖和验收标准;评审通过前不写业务代码。
4. 每次只选择一个已评审里程碑,创建对应的 `【实施计划】中文标题-YYYY-MM-DD.md`,明确修改边界、顺序、验证命令、风险和回滚点。
5. 发现行为需要变化时,严格按 `主规范 → 尚未实现的里程碑规范 → 当前实现计划 → 代码与测试` 更新。
6. 完成一个里程碑后,提供主规范/里程碑逐条对照、自动化验证、必要运行时 smoke、边界验证和未验证项;未验收不得推进下一个里程碑。
7. 全部验收通过后,将持久结论合并回主规范,删除已完成的临时计划,并检查文档、契约、测试和提交边界一致。
## 约束
- 里程碑和实现计划是开发期协调文件,完成或取消后删除;阶段编号和临时名称不得进入产品代码、用户文档、测试名称或提交标题。
- 活动计划可以提交以便团队同步,但不得把一次性实现步骤写进长期规范。
- 继续遵守现有 API、SpacetimeDB、AGC、编码和中文文档约束;本 skill 不替代专题门禁。
- 文档任务至少运行 `npm run check:doc-index``npm run check:encoding``git diff --check`;代码任务再运行范围匹配的测试、类型检查、schema/OpenAPI 或运行时 smoke。
-31
View File
@@ -232,37 +232,6 @@ jobs:
done
done
- name: Prepare standalone Rust crate dependencies
shell: bash
run: |
set -euo pipefail
# agent-runtime-core / agent-runtime-orchestration 被 server-rs/Cargo.toml 的
# exclude 排除,不参与上面的 workspace 锁文件,因此上面那次锁定 fetch 覆盖不到它们;
# 而 check:native-shells 会经 agent-runtime-*:check 用 `cargo test --manifest-path`
# 单独跑这两个 crate。不在这里预热的话,这两条测试会在测试阶段自己
# `Updating crates.io index`crates.io 一抖动整条 native shell 作业就红
# (见 #327 / PR #316 run 1950)。
# 两个 crate 都没有提交 Cargo.lock,所以这里只能做不带锁标志的 fetch:
# 加锁标志会因为缺少锁文件直接失败。生成的 Cargo.lock 落在两个 crate 目录内,
# 已被各自的 .gitignore 忽略,只留在容器里;随后的测试阶段因此能用锁定版本
# 解析,不再触碰 registry index。
for manifest_path in \
server-rs/crates/agent-runtime-core/Cargo.toml \
server-rs/crates/agent-runtime-orchestration/Cargo.toml; do
for attempt in $(seq 1 5); do
if cargo fetch \
--target x86_64-unknown-linux-gnu \
--manifest-path "${manifest_path}"; then
break
fi
if [[ "${attempt}" -eq 5 ]]; then
echo "standalone crate dependency fetch failed after 5 attempts: ${manifest_path}" >&2
exit 1
fi
sleep $((attempt * 2))
done
done
- name: Run native shell gates
run: npm run check:native-shells
-2
View File
@@ -40,8 +40,6 @@ temp*build*/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-path/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-resources/
/apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/codex-package.json
/apps/ai-game-creator-shell/src-tauri/resources/plugins/
/plugins/agc-cocos-editor/native/payload/
/apps/ai-game-creator-shell/logs/
/apps/ai-game-creator-shell/.llm-drafts/
/apps/ai-game-creator-shell/game-creator.config.local.json
-1
View File
@@ -16,7 +16,6 @@
3. [`docs/project-memory/README.md`](docs/project-memory/README.md)、[`project-overview.md`](docs/project-memory/shared-memory/project-overview.md)、[`team-conventions.md`](docs/project-memory/shared-memory/team-conventions.md)、[`development-workflow.md`](docs/project-memory/shared-memory/development-workflow.md)。
4. 与任务相关的 [`decision-log.md`](docs/project-memory/shared-memory/decision-log.md)、[`pitfalls.md`](docs/project-memory/shared-memory/pitfalls.md)、[`docs/README.md`](docs/README.md) 和当前专题文档。
- 落地工程修改前,先确认是否已有足够具体的 PRD、技术方案或当前融合文档;文档仍存在编码级歧义时,先补文档再编码。
- 跨模块功能、公开 API/DTO、SpacetimeDB schema、AGC/Runtime 或复杂 UI 状态链路必须按 [`docs/【协作规范】规范驱动开发工作流-2026-09-12.md`](docs/【协作规范】规范驱动开发工作流-2026-09-12.md) 先完成主规范、里程碑规范和单里程碑实现计划;评审与验收门禁未通过前不得进入下一里程碑。局部修复、文案和无行为变化重构继续走轻量流程。
- 本仓库的本地 RAG 位于 [`scripts/rag/`](scripts/rag/);RAG 只作为候选上下文,不替代打开源文件核对。默认不安装 RAG 运行时依赖,需要启用时必须先询问用户,并只安装到 gitignored 的 `.rag/runtime/`
## 绝对约束
-4
View File
@@ -1,4 +0,0 @@
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
# “构建 -> 监听 -> 再构建”的自触发循环。
resources/plugins/
@@ -16,5 +16,8 @@
"maxRetries": 2,
"retryBackoffMs": 500
},
"agentLlm": {}
"agentLlm": {},
"planning": {
"capabilityEnabled": true
}
}
+3 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.27",
"version": "0.1.12",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -19,6 +19,8 @@
"config": "node scripts/game-creator-config-wizard.mjs",
"test:chat": "node scripts/agent-swarm-test-chat.mjs --task \"制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。\" --no-open",
"test:chat:manual": "node scripts/agent-swarm-test-chat.mjs",
"test:plan": "node scripts/agent-swarm-test-chat.mjs --plan --task \"我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。\"",
"test:plan:manual": "node scripts/agent-swarm-test-chat.mjs --plan",
"agent-run": "node scripts/run-cli-with-config.mjs --agent-run",
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
"agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs",
@@ -36,6 +36,8 @@ export const ungeneratedGameEntryMarker =
'还没有生成游戏。回到聊天输入创意并确认生成后';
export const defaultRealSwarmTestTask =
'制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。';
export const defaultRealSwarmPlanTask =
'我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。';
export const swarmTurnReportPrefix = '[turn.report] ';
export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1';
@@ -138,10 +140,15 @@ export const usage = `用法:
--keep-project 保留自动创建的一次性项目
--no-open 手工模式启动预览但不自动打开浏览器
--task <需求> 通过 manual 入口非交互提交自定义需求
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,手工模式默认不限时
--plan 走「做方案」立项策划入口,不做游戏,不做产物验收和试玩
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,--plan 默认 6 分钟,手工模式默认不限时
--dry-run 只检查目录发现和项目准备,不启动 LLM
-h, --help 显示帮助
`;
环境变量:
AGC_PLAN_GDD_DECISION 审批卡自动应答动作,默认 approverevise/reject 必须
同时用 AGC_PLAN_GDD_COMMENT 给出真实修改意见
AGC_PLAN_GDD_COMMENT revise/reject 的意见原文`;
function readOptionValue(args, index, option) {
const value = args[index + 1]?.trim();
@@ -170,6 +177,7 @@ export function parseSwarmTestArguments(args) {
keepProject: false,
openBrowser: true,
task: null,
plan: false,
timeoutMinutes: null,
dryRun: false,
help: false,
@@ -194,6 +202,8 @@ export function parseSwarmTestArguments(args) {
if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符');
options.task = task;
index += 1;
} else if (argument === '--plan') {
options.plan = true;
} else if (argument === '--timeout-minutes') {
if (options.timeoutMinutes !== null) {
throw new Error('--timeout-minutes 只能指定一次');
@@ -212,11 +222,16 @@ export function parseSwarmTestArguments(args) {
}
export function shouldStartPersistentPreview(options) {
return !options.task;
// 立项策划链路只出 GDD,没有可试玩产物,任何模式都不该起预览。
return !options.task && !options.plan;
}
export function resolveSwarmTestTimeoutMs(options) {
const minutes = options.timeoutMinutes ?? (options.task ? 50 : null);
// 立项策划的设计目标是五分钟出方案,给一分钟余量;再久就是卡住了,早失败
// 比让 harness 空等更有用。做游戏那条链路的 50 分钟不变。
const planMinutes = options.plan ? 6 : null;
const minutes =
options.timeoutMinutes ?? planMinutes ?? (options.task ? 50 : null);
return minutes === null ? null : minutes * 60_000;
}
@@ -371,15 +386,7 @@ async function runMissingConfigWizard(setActiveChild, explicitConfigDir) {
}
}
async function secureWindowsPrivateRuntimePath(
targetPath,
options,
secureWindowsPath = null,
) {
if (secureWindowsPath) {
await secureWindowsPath(targetPath, options);
return;
}
async function secureWindowsPrivateRuntimePath(targetPath, options) {
const { secureWindowsGameCreatorPathForCurrentUser } = await import(
'./game-creator-config-wizard.mjs'
);
@@ -391,7 +398,6 @@ async function copyPrivateRuntimeConfigEntry(
runtimeConfigDir,
fileName,
required,
secureWindowsPath = null,
) {
const sourcePath = path.join(sourceConfigDir, fileName);
const sourceMetadata = await lstat(sourcePath).catch((error) => {
@@ -411,11 +417,9 @@ async function copyPrivateRuntimeConfigEntry(
const sourceBytes = await readFile(sourcePath);
const destinationFile = await open(destinationPath, 'wx', 0o600);
try {
await secureWindowsPrivateRuntimePath(
destinationPath,
{ isDirectory: false },
secureWindowsPath,
);
await secureWindowsPrivateRuntimePath(destinationPath, {
isDirectory: false,
});
await destinationFile.writeFile(sourceBytes);
await destinationFile.sync();
} finally {
@@ -448,11 +452,7 @@ async function copyPrivateRuntimeConfigEntry(
return true;
}
export async function prepareSwarmTestRuntimeConfig(
sourceConfigDir,
tempRoot,
{ secureWindowsPath = null } = {},
) {
export async function prepareSwarmTestRuntimeConfig(sourceConfigDir, tempRoot) {
if (!path.isAbsolute(sourceConfigDir)) {
throw new Error('配置来源目录必须是绝对路径');
}
@@ -472,11 +472,9 @@ export async function prepareSwarmTestRuntimeConfig(
);
try {
if (process.platform === 'win32') {
await secureWindowsPrivateRuntimePath(
runtimeConfigDir,
{ isDirectory: true },
secureWindowsPath,
);
await secureWindowsPrivateRuntimePath(runtimeConfigDir, {
isDirectory: true,
});
} else {
await chmod(runtimeConfigDir, 0o700);
}
@@ -494,14 +492,12 @@ export async function prepareSwarmTestRuntimeConfig(
runtimeConfigDir,
configFileName,
true,
secureWindowsPath,
);
await copyPrivateRuntimeConfigEntry(
canonicalSourceConfigDir,
runtimeConfigDir,
localConfigFileName,
false,
secureWindowsPath,
);
return {
path: await realpath(runtimeConfigDir),
@@ -983,12 +979,25 @@ export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) {
return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1;
}
// GDD 审批位不能等 CLI 退出之后再处理:Run 停在这里时状态是 waiting-for-user-input
// 而 swarm CLI 恰好把这个状态算作「本轮还在跑」,turn 永远不 settleCLI 也就永远
// 不退出。所以审批必须在 CLI 还活着的时候并发做完,让 Run 自己继续跑到收束。
// 这一句是 PlanGddCompletionBlockerKind::AwaitingApprovalDecision 专有的投影文案,
// 另外三个 blocked 子状态都不会打出它;即便认错了,真正的判据也是随后那次
// --plan-gdd-status,没有待决定审批时不会有任何写入。
const planGddApprovalWaitPattern = /等待 Fast GDD 审批决定/u;
export function swarmOutputAwaitsPlanGddApproval(line) {
return planGddApprovalWaitPattern.test(line);
}
async function runTaskCargo(
cliArguments,
task,
setActiveChild,
timeoutMs,
autoPilot = false,
onPlanGddApprovalWait = null,
) {
const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), {
stdio: ['pipe', 'pipe', 'inherit'],
@@ -1000,6 +1009,25 @@ async function runTaskCargo(
let taskSubmitted = false;
let promptsSeen = 0;
let sittingAtPrompt = false;
let planGddApproval = null;
let planGddApprovalError = null;
let planGddApprovalStarted = false;
let planGddApprovalPromise = null;
const startPlanGddApproval = () => {
planGddApprovalStarted = true;
console.log(
`[自动审批] 检测到 Fast GDD 审批位,正在提交 ${resolvePlanGddAutoDecision().action}`,
);
planGddApprovalPromise = onPlanGddApprovalWait()
.then((value) => {
planGddApproval = value;
})
.catch((error) => {
planGddApprovalError = error;
// 审批没成的话 Run 会一直停在等待位,干等到超时只会把真正的原因埋掉。
void terminateChildTree(child).catch(() => {});
});
};
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
@@ -1012,6 +1040,13 @@ async function runTaskCargo(
reportLines.push(normalizedLine);
settled = true;
}
if (
onPlanGddApprovalWait &&
!planGddApprovalStarted &&
swarmOutputAwaitsPlanGddApproval(normalizedLine)
) {
startPlanGddApproval();
}
}
if (!autoPilot || child.stdin.writableEnded) return;
const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine);
@@ -1050,9 +1085,12 @@ async function runTaskCargo(
if (normalizedPendingLine.startsWith(swarmTurnReportPrefix)) {
reportLines.push(normalizedPendingLine);
}
await planGddApprovalPromise;
if (planGddApprovalError) throw planGddApprovalError;
return {
...result,
turnReportOutput: reportLines.join('\n'),
planGddApproval,
};
} finally {
setActiveChild(null);
@@ -1742,6 +1780,196 @@ export async function validateSwarmProjectArtifacts(projectPath, options) {
return inspection;
}
// 这四条路径的权威定义都在 Rust 侧 `planning_storage.rs``PLAN_SESSION_PATH`、
// `PLAN_GDD_INDEX_PATH`、`PLAN_STORAGE_ROOT`、`PLAN_FAST_GDD_PATH`)。跨语言没有共享
// 常量的通道,改路径时要连同 `GddApprovalCard.tsx` 一起动。
export const planningOutputPaths = [
'.agent/planning/session.json',
'.agent/planning/index.json',
'.agent/planning/pending.json',
'game/fast_gdd.md',
];
export async function inspectPlanningOutputs(projectPath) {
const outputs = [];
for (const relativePath of planningOutputPaths) {
const absolutePath = path.join(projectPath, ...relativePath.split('/'));
const metadata = await lstat(absolutePath).catch((error) => {
if (error?.code === 'ENOENT') return null;
throw error;
});
outputs.push({
path: relativePath,
exists: Boolean(metadata?.isFile()),
bytes: metadata?.isFile() ? metadata.size : 0,
});
}
return outputs;
}
async function reportPlanningOutputs(projectPath) {
const outputs = await inspectPlanningOutputs(projectPath);
console.log('\n立项策划产物:');
for (const output of outputs) {
console.log(
output.exists
? ` [有] ${output.path}${output.bytes} 字节)`
: ` [无] ${output.path}`,
);
}
}
const planGddApprovalTimeoutMs = 60_000;
export const planGddStatusOutputPrefix = 'planGddStateJson=';
export const planGddDecisionOutputPrefix = 'planGddDecisionJson=';
function parsePrefixedJsonLine(output, prefix, label) {
const line = output
.split('\n')
.map((value) => (value.endsWith('\r') ? value.slice(0, -1) : value))
.find((value) => value.startsWith(prefix));
if (!line) throw new Error(`${label}缺少 ${prefix} 输出`);
try {
return JSON.parse(line.slice(prefix.length));
} catch (error) {
throw new Error(`解析${label}失败:${error.message}`);
}
}
export function parsePlanGddStatusOutput(output) {
return parsePrefixedJsonLine(
output,
planGddStatusOutputPrefix,
'Fast GDD 审批状态',
);
}
export function parsePlanGddDecisionOutput(output) {
return parsePrefixedJsonLine(
output,
planGddDecisionOutputPrefix,
'Fast GDD 审批回执',
);
}
// 审批卡是这条链路唯一的人类判据,所以自动应答默认只投 approve,且只在投影确实有
// 一张待决定审批时出手。revise/reject 需要一段真实的修改意见,让机器编一段等于把
// 判据换成噪声——所以那两条分支只在跑的人自己用 AGC_PLAN_GDD_COMMENT 给出意见时
// 才走。手工调 --plan-gdd-decide 也能达到同样效果,但那要求 plan 根 run 仍然活着,
// 而它恰好是本进程持有的 CLI 子进程。
export function planGddAutoApprovalIsPending(state) {
return Boolean(state?.pendingApproval);
}
export function resolvePlanGddAutoDecision(env = process.env) {
const action = (env.AGC_PLAN_GDD_DECISION ?? 'approve').trim();
if (!['approve', 'revise', 'reject'].includes(action)) {
throw new Error('AGC_PLAN_GDD_DECISION 只能是 approve / revise / reject');
}
const comment = (env.AGC_PLAN_GDD_COMMENT ?? '').trim();
if (action === 'approve') return { action, comment: null };
if (!comment) {
throw new Error(
`${action} 必须同时设 AGC_PLAN_GDD_COMMENT 提供真实修改意见`,
);
}
return { action, comment };
}
async function settlePlanGddApproval(
projectPath,
runtimeConfigPath,
setActiveChild,
) {
const readStatus = async () => {
const result = await runCapturedCargo(
['--config-dir', runtimeConfigPath, '--plan-gdd-status', projectPath],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批状态查询',
},
);
if (result.code !== 0 || result.signal) {
throw new Error(
`读取 Fast GDD 审批状态失败:${result.stderr.trim() || result.stdout.trim()}`,
);
}
return parsePlanGddStatusOutput(result.stdout);
};
const before = await readStatus();
if (!planGddAutoApprovalIsPending(before)) {
return { decided: false, state: before };
}
const { action, comment } = resolvePlanGddAutoDecision();
const decision = await runCapturedCargo(
[
'--config-dir',
runtimeConfigPath,
'--plan-gdd-decide',
projectPath,
action,
...(comment === null ? [] : ['--stdin']),
],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批决定',
stdin: comment,
},
);
if (decision.code !== 0 || decision.signal) {
throw new Error(
`提交 Fast GDD 审批决定失败:${decision.stderr.trim() || decision.stdout.trim()}`,
);
}
const receipt = parsePlanGddDecisionOutput(decision.stdout);
// 回执落盘和唤醒后台任务是两件事:decide 命令把唤醒失败降级成 recoveryPending
// 于是审批已经生效、Run 却仍停在 waiting-for-user-input。实测就是这样——只有
// 补一次 --agent-resume 才会重新起 turn。这是仓库自己给这个状态定义的恢复动作。
let recovered = false;
if (receipt.recoveryPending) {
const resume = await runCapturedCargo(
['--config-dir', runtimeConfigPath, '--agent-resume', projectPath],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批后恢复后台任务',
},
);
if (resume.code !== 0 || resume.signal) {
throw new Error(
`审批已提交但恢复后台任务失败:${resume.stderr.trim() || resume.stdout.trim()}`,
);
}
recovered = true;
}
return { decided: true, receipt, recovered, state: await readStatus() };
}
async function reportPlanGddApproval(approval) {
const { state } = approval;
console.log('\nFast GDD 审批:');
if (!approval.decided) {
console.log(` [无待决定审批] 当前投影状态=${state.state}`);
return;
}
console.log(
` [已决定 ${approval.receipt.decisionRef.action}] outcome=${approval.receipt.outcome} v${approval.receipt.decisionRef.version} 投影状态=${state.state}`,
);
if (approval.recovered) {
console.log(
' [已恢复] 审批回执的 recoveryPending 由一次 --agent-resume 收口',
);
}
if (state.session) {
console.log(
` 澄清轮次=${state.session.clarificationRound} 返工深度=${state.session.repairDepth} phase=${state.session.phase}`,
);
}
}
export async function hasConfiguredEditorApiKey(configDir) {
let configured = false;
for (const fileName of [configFileName, localConfigFileName]) {
@@ -1894,7 +2122,13 @@ export async function runSwarmTestChat(options) {
const setActiveChild = (child) => {
activeChild = child;
};
// GDD 审批要和 swarm CLI 并发跑,两者不能共用 activeChild 这一个槽位:审批子进程
// 结束时的 setActiveChild(null) 会把 CLI 从槽里抹掉,Ctrl-C 就杀不到它了。
const concurrentChildren = new Set();
const setConcurrentChild = (child) => {
if (child) concurrentChildren.add(child);
else concurrentChildren.clear();
};
const stopRequested = () => receivedSignal !== null;
const handleSignal = (signal) => {
const repeatedSignal = receivedSignal !== null;
@@ -1972,10 +2206,11 @@ export async function runSwarmTestChat(options) {
);
}
console.log('LLM 配置已就绪。');
const requirementNoun = options.plan ? '立项策划需求' : '游戏需求';
console.log(
options.task
? '已提交一条非交互游戏需求,正在等待 Swarm 自主完成。\n'
: '输入一条游戏需求并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n',
? `已提交一条非交互${requirementNoun},正在等待 Swarm 自主完成。\n`
: `输入一条${requirementNoun}并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n`,
);
phase = 'chat';
@@ -1985,7 +2220,8 @@ export async function runSwarmTestChat(options) {
runtimeConfig.path,
'--swarm-chat',
'--init',
'--autonomous-game-build',
// 做方案链路只能跑 standard 档,后端对 plan + autonomous 是硬否决。
options.plan ? '--plan' : '--autonomous-game-build',
project.path,
];
let chat;
@@ -1998,6 +2234,15 @@ export async function runSwarmTestChat(options) {
timeoutDeadline === null
? null
: Math.max(1, timeoutDeadline - Date.now()),
options.plan,
options.plan
? () =>
settlePlanGddApproval(
project.path,
runtimeConfig.path,
setConcurrentChild,
)
: null,
)
: await runInteractiveCargo(chatArguments, setActiveChild);
} catch (error) {
@@ -2013,6 +2258,32 @@ export async function runSwarmTestChat(options) {
if (options.task) {
turnReport = parseSettledSwarmTurnReport(chat.turnReportOutput);
}
if (options.plan) {
// 立项策划不出游戏产物,正式验收在 GDD 审批卡上;这里只报告落盘情况,
// 是否收束已经由 CLI 的退出码判过了。
// 自动任务档的审批已经在 CLI 运行期间并发做完了;手工档(人自己敲 Ctrl+D
// 退出)没有那次触发,退出后补一次,没有待决定审批时它是只读的。
phase = 'plan-approval';
const approval =
chat.planGddApproval ??
(await settlePlanGddApproval(
project.path,
runtimeConfig.path,
setConcurrentChild,
));
if (receivedSignal) break session;
phase = 'plan-report';
await reportPlanGddApproval(approval);
await reportPlanningOutputs(project.path);
phase = 'complete';
console.log(
approval.decided
? '\n立项策划链路已收束:Fast GDD 已批准,策划产物见上方清单。'
: '\n立项策划链路已收束:Run 正常结束但没有待决定审批,策划产物见上方清单。',
);
break session;
}
phase = 'artifact-validation';
const requireEditorImages = await hasConfiguredEditorApiKey(
runtimeConfig.path,
);
@@ -4,11 +4,6 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
defaultEditorFeatures,
withDefaultCargoFeatures,
} from './cargo-features.mjs';
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const defaultReleaseTarget = 'x86_64-pc-windows-msvc';
const releaseTarget =
@@ -164,30 +159,10 @@ export async function prepareReleaseVersion() {
return nextVersion;
}
export function buildTauriBuildArguments(
args = [],
target = releaseTarget,
platform = process.platform,
) {
const noBundle = args.includes('--no-bundle');
const targetIndex = args.indexOf('--target');
const explicitTarget =
targetIndex >= 0
? args[targetIndex + 1]
: args
.find((value) => value.startsWith('--target='))
?.slice('--target='.length);
const targetArgs = noBundle || explicitTarget ? [] : ['--target', target];
const features = defaultEditorFeatures(
explicitTarget || (noBundle ? platform : target),
);
return [
'build',
...withDefaultCargoFeatures([...targetArgs, ...args], features),
];
}
export function runTauriBuild(args = []) {
const noBundle = args.includes('--no-bundle');
const hasTarget = args.includes('--target');
const targetArgs = noBundle || hasTarget ? [] : ['--target', releaseTarget];
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const result = spawnSync(
npmCommand,
@@ -197,7 +172,9 @@ export function runTauriBuild(args = []) {
'exec',
'tauri',
'--',
...buildTauriBuildArguments(args),
'build',
...targetArgs,
...args,
],
{ cwd: appRoot, stdio: 'inherit', shell: process.platform === 'win32' },
);
@@ -1,24 +0,0 @@
/** 默认桌面能力;显式 feature 参数优先,不把应用参数当 Cargo 参数。 */
export function withDefaultCargoFeatures(argv, features) {
const separator = argv.indexOf('--');
const cargoArgs = separator < 0 ? argv : argv.slice(0, separator);
if (
!features.length ||
cargoArgs.some(
(value) =>
value === '--features' ||
value === '-f' ||
value.startsWith('--features=') ||
/^-f.+/u.test(value),
)
) {
return argv;
}
return [`--features=${features.join(',')}`, ...argv];
}
export function defaultEditorFeatures(target) {
return target === 'win32' || target.includes('windows')
? ['cocos-editor-execute']
: [];
}
@@ -1,49 +0,0 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { buildTauriBuildArguments } from './build-release.mjs';
import { withDefaultCargoFeatures } from './cargo-features.mjs';
test('Windows release includes the same editor feature as development', () => {
assert.deepEqual(
buildTauriBuildArguments([], 'x86_64-pc-windows-msvc', 'win32'),
[
'build',
'--features=cocos-editor-execute',
'--target',
'x86_64-pc-windows-msvc',
],
);
assert.deepEqual(
buildTauriBuildArguments(
['--no-bundle'],
'x86_64-pc-windows-msvc',
'linux',
),
['build', '--no-bundle'],
);
assert.deepEqual(
buildTauriBuildArguments(['--target=aarch64-apple-darwin']),
['build', '--target=aarch64-apple-darwin'],
);
});
test('explicit Cargo features override defaults in every supported spelling', () => {
for (const args of [
['--features', 'custom'],
['--features=custom'],
['-f', 'custom'],
['-fcustom'],
]) {
assert.deepEqual(
withDefaultCargoFeatures(args, ['cocos-editor-execute']),
args,
);
}
assert.deepEqual(
withDefaultCargoFeatures(
['--', '--features=app'],
['cocos-editor-execute'],
),
['--features=cocos-editor-execute', '--', '--features=app'],
);
});
@@ -114,20 +114,9 @@ const allowedUncalledTauriCommands = [
'open_game_creator_launcher_window',
'open_game_creator_workspace_window',
'read_direct_project_conversation',
'reset_design_agent_session',
'stop_local_game_preview_if_matches',
'start_game_creator_external_mcp',
'stop_game_creator_external_mcp',
'list_agc_plugins',
'list_agc_extensions',
'refresh_agc_plugins',
'start_agc_plugin',
'stop_agc_plugin',
'reload_agc_plugin',
'call_agc_plugin',
'read_agc_plugin_panel',
'set_agc_plugin_project_path',
'set_agc_plugin_enabled',
];
const sourceExtensions = new Set([
'.json',
@@ -1296,10 +1285,7 @@ if (tauriConfig.identifier !== 'world.genarrative.ai-game-creator') {
throw new Error('AI game creator shell identifier drifted');
}
const expectedBundledDesignAgentResources = {
'design-agent': 'design-agent',
};
const expectedBundledWindowsResources = {
const expectedBundledCodexResources = {
'resources/codex/win-x64/bin/codex.exe': 'codex/win-x64/bin/codex.exe',
'resources/codex/win-x64/bin/codex-code-mode-host.exe':
'codex/win-x64/bin/codex-code-mode-host.exe',
@@ -1313,35 +1299,17 @@ const expectedBundledWindowsResources = {
'codex/win-x64/codex-package.json',
'resources/codex/win-x64/NOTICE.md': 'codex/win-x64/NOTICE.md',
'resources/codex/win-x64/manifest.json': 'codex/win-x64/manifest.json',
'resources/plugins': 'plugins',
};
assert.deepEqual(
tauriConfig.bundle?.resources,
expectedBundledDesignAgentResources,
'AI game creator shell base Tauri config must bundle the design-agent resource pack',
);
for (const key of Object.keys(tauriConfig.bundle?.resources ?? {})) {
if (String(key).includes('codex')) {
throw new Error(
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
);
}
if (tauriConfig.bundle?.resources !== undefined) {
throw new Error(
'AI game creator shell base Tauri config must not require Windows-only Codex resources',
);
}
assert.deepEqual(
windowsTauriConfig.bundle?.resources,
expectedBundledWindowsResources,
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set and Cocos bridge payload directory',
expectedBundledCodexResources,
'AI game creator shell Windows Tauri config must bundle the complete pinned Codex resource set',
);
if (
Object.prototype.hasOwnProperty.call(
windowsTauriConfig.bundle?.resources ?? {},
'design-agent',
)
) {
throw new Error(
'design-agent resource pack must not be mixed into the Windows Codex sidecar bundle',
);
}
if (windowsTauriConfig.bundle?.useLocalToolsDir !== true) {
throw new Error(
'AI game creator shell Windows Tauri config must cache bundling tools in the project target directory',
@@ -1737,16 +1705,10 @@ if (
runtimeConfigSetupStart === -1 ||
runtimeConfigSetupEnd === -1 ||
!runtimeConfigSetupSource.includes('sanitize_diagnostic_message(') ||
!runtimeConfigSetupSource.includes('setup_log.fail(') ||
!runtimeConfigSetupSource.includes('append_bounded_diagnostic_line(') ||
!runtimeConfigSetupSource.includes(
'startup.appdata.configure.failed details={details}',
) ||
!tauriHandlerSource.includes('impl StartupLogSlot {') ||
!tauriHandlerSource.includes('append_bounded_diagnostic_line(&path, line)') ||
!tauriHandlerSource.includes(
'self.append(line);\n show_startup_error_dialog(self.path().as_deref());',
) ||
!tauriHandlerSource.includes('early_startup_log_path(')
)
) {
throw new Error(
'AI game creator setup must configure the runtime AppData directory and log sanitized setup failures',
@@ -722,7 +722,7 @@ function runAgent() {
stderr += chunk.toString();
});
child.on('error', reject);
child.on('close', (code, signal) => {
child.on('close', (code) => {
const output = `${stdout}${stderr}`;
if (previewReadError) {
reject(previewReadError);
@@ -740,24 +740,13 @@ function runAgent() {
previewDom,
});
} else {
reject(
new Error(
`agent run failed: exitCode=${code}, signal=${signal ?? 'none'}\n` +
`stderr tail (last 8000 characters):\n${stderr.slice(-8000)}\n` +
`stdout tail (last 4000 characters):\n${stdout.slice(-4000)}`,
),
);
reject(new Error(output || `agent run exited with ${code}`));
}
});
});
}
async function seedLocalAsset() {
await fs.mkdir(path.join(projectRoot, 'game'), { recursive: true });
await fs.writeFile(
path.join(projectRoot, 'game/index.html'),
'<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><body>还没有生成游戏</body></html>',
);
await fs.mkdir(path.join(projectRoot, 'assets/uploads'), { recursive: true });
await fs.mkdir(path.join(projectRoot, '.agent'), { recursive: true });
await seedConversationContext();
@@ -1,16 +1,10 @@
import { spawn, spawnSync } from 'node:child_process';
import { spawn } from 'node:child_process';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import http from 'node:http';
import net from 'node:net';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
normalizeWindowsPath,
parseWindowsProcessSnapshot,
stopWindowsProcessTree,
stopWindowsWorktreeProcesses,
} from '../../../scripts/dev-windows-process.mjs';
import {
agcVitePortEnvKey,
readAgcDevEndpoint,
@@ -21,10 +15,6 @@ import {
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..');
const devStackStatePath = resolve(repoRoot, '.app/dev-stack.json');
const apiServerExePath = resolve(
repoRoot,
'server-rs/target/debug/api-server.exe',
);
const defaultApiTarget =
process.env.RUST_SERVER_TARGET || 'http://127.0.0.1:8082';
const backendDatabase = 'genarrative-game-creator-dev';
@@ -170,184 +160,19 @@ function readBackendServiceFailure(
return null;
}
function urlPort(url) {
try {
const port = Number(new URL(url).port);
return Number.isInteger(port) && port > 0 ? port : 0;
} catch {
return 0;
}
}
// 读取端口当前真正的监听进程身份。返回 null 表示探测本身不可用(例如缺少
// Get-NetTCPConnection),此时调用方必须退化为旧行为,不能让本地启动直接失败。
function readWindowsPortOwnerIdentities(
ports,
{ spawnImpl = spawnSync, env = process.env } = {},
) {
const uniquePorts = [...new Set(ports.filter((port) => port > 0))];
if (uniquePorts.length === 0) {
return null;
}
const command = [
'$ErrorActionPreference = "SilentlyContinue"',
'$ports = ($env:GENARRATIVE_QUERY_PORTS -split ",") | Where-Object { $_ }',
'$result = @()',
'foreach ($port in $ports) {',
' $connection = Get-NetTCPConnection -State Listen -LocalPort ([int]$port) -ErrorAction SilentlyContinue | Select-Object -First 1',
' if (-not $connection) { continue }',
' $owner = Get-CimInstance Win32_Process -Filter ("ProcessId=" + $connection.OwningProcess) -ErrorAction SilentlyContinue',
' $result += [pscustomobject]@{ port = [int]$port; processId = [int]$connection.OwningProcess; name = $owner.Name; executablePath = $owner.ExecutablePath; commandLine = $owner.CommandLine }',
'}',
'ConvertTo-Json -InputObject @($result) -Compress',
].join('\n');
const result = spawnImpl(
'powershell.exe',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
{
encoding: 'utf8',
env: { ...env, GENARRATIVE_QUERY_PORTS: uniquePorts.join(',') },
maxBuffer: 8 * 1024 * 1024,
},
);
if (result?.error || result?.status !== 0) {
return null;
}
const owners = new Map();
for (const entry of parseWindowsProcessSnapshot(result.stdout)) {
const port = Number(entry?.port);
if (Number.isInteger(port) && port > 0) {
owners.set(port, entry);
}
}
return owners;
}
function isWorktreeApiServerOwner(
owner,
{ expectedExePath = apiServerExePath } = {},
) {
if (!owner) {
return false;
}
const expected = normalizeWindowsPath(expectedExePath);
const actual = normalizeWindowsPath(owner.executablePath);
return Boolean(expected) && actual === expected;
}
function isWorktreeSpacetimeOwner(
owner,
{ expectedDataDir = backendSpacetimeDataDir } = {},
) {
if (!owner) {
return false;
}
const expected = normalizeWindowsPath(expectedDataDir);
if (!expected) {
return false;
}
const name = String(owner.name ?? '').toLowerCase();
if (!name.startsWith('spacetime')) {
return false;
}
return normalizeWindowsPath(owner.commandLine).includes(expected);
}
// 端口健康不代表后端属于当前工作树:上个工作树 Ctrl+C 残留的 api-server 仍会
// 应答 /healthz。复用前必须证明端口上的进程就是本工作树的可执行文件与数据目录。
function verifyAgcBackendOwnership({
apiUrl,
spacetimeUrl,
bgfilterWorkerUrl,
platform = process.platform,
expectedExePath = apiServerExePath,
expectedDataDir = backendSpacetimeDataDir,
readPortOwners = readWindowsPortOwnerIdentities,
} = {}) {
if (platform !== 'win32') {
return { ok: true, reason: 'platform-unsupported', owners: new Map() };
}
const ports = [
urlPort(apiUrl),
urlPort(bgfilterWorkerUrl),
urlPort(spacetimeUrl),
];
const owners = readPortOwners(ports);
if (!owners) {
return { ok: true, reason: 'owner-probe-unavailable', owners: new Map() };
}
const apiOwner = owners.get(urlPort(apiUrl));
if (!isWorktreeApiServerOwner(apiOwner, { expectedExePath })) {
return { ok: false, reason: 'api-server-owner-mismatch', owners, apiOwner };
}
const workerOwner = owners.get(urlPort(bgfilterWorkerUrl));
if (!isWorktreeApiServerOwner(workerOwner, { expectedExePath })) {
return {
ok: false,
reason: 'bgfilter-worker-owner-mismatch',
owners,
workerOwner,
};
}
const spacetimeOwner = owners.get(urlPort(spacetimeUrl));
if (!isWorktreeSpacetimeOwner(spacetimeOwner, { expectedDataDir })) {
return {
ok: false,
reason: 'spacetime-owner-mismatch',
owners,
spacetimeOwner,
};
}
return { ok: true, reason: 'owned', owners };
}
function formatOwnerLabel(owner) {
if (!owner) {
return '未知进程';
}
const pid = Number(owner.processId);
const label = owner.executablePath || owner.commandLine || owner.name || '';
return `${Number.isInteger(pid) ? `pid=${pid} ` : ''}${String(label).trim()}`.trim();
}
async function isBackendReady({
state = readJson(devStackStatePath),
isReady = isHttpReady,
verifyOwnership = verifyAgcBackendOwnership,
onOwnershipRejected = null,
} = {}) {
const { apiUrl, spacetimeUrl, bgfilterWorkerUrl, hasMatchingBackend } =
resolveBackendTargetsFromState(state, {
requireAgcBackend: true,
});
if (!hasMatchingBackend || !apiUrl || !spacetimeUrl || !bgfilterWorkerUrl) {
return false;
}
const ownership = await verifyOwnership({
apiUrl,
spacetimeUrl,
bgfilterWorkerUrl,
});
if (!ownership?.ok) {
onOwnershipRejected?.(ownership);
return false;
}
if (ownership.reason === 'owner-probe-unavailable') {
console.warn(
'[ai-game-creator-shell] 无法读取端口监听进程归属,本次按旧行为复用配套后端。',
);
}
return (
hasMatchingBackend &&
Boolean(apiUrl) &&
Boolean(spacetimeUrl) &&
Boolean(bgfilterWorkerUrl) &&
(await isReady(`${apiUrl}/healthz`)) &&
(await isReady(`${spacetimeUrl}/v1/ping`)) &&
(await isReady(`${bgfilterWorkerUrl}/readyz`))
@@ -436,10 +261,6 @@ function spawnChild(command, args, options, spawnImpl = spawn) {
const child = spawnImpl(command, args, {
...options,
shell: useShell,
// npm.cmd and the Windows shell otherwise create a visible console for
// every service in the dev stack. Their stdout/stderr is already inherited
// by the launcher, so no separate terminal window is useful.
windowsHide: process.platform === 'win32' ? true : options.windowsHide,
// POSIX 下让每个长驻服务拥有独立进程组,退出时可以连同 npm、
// Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。
detached: isPosix,
@@ -664,18 +485,14 @@ async function terminateChildTree(
return { stopped: true, forced: false };
}
const result = await taskkillImpl(child.pid);
const taskkillStopped =
!result?.timedOut &&
!result?.error &&
[0, 128].includes(result?.code ?? 0);
if (taskkillStopped) {
return { stopped: true, forced: true, result };
}
// 包装层(cmd.exe / npm.cmd)先被 Ctrl+C 杀掉时 taskkill 拿不到活着的 PID
// 这里继续按记录下来的根 PID 遍历,尽量收掉更深的后端进程。
const treeStopped = stopWindowsProcessTree(child.pid);
return { stopped: treeStopped.length > 0, forced: true, result };
return {
stopped:
!result?.timedOut &&
!result?.error &&
[0, 128].includes(result?.code ?? 0),
forced: true,
result,
};
}
const processGroupId = childLifecycles.get(child)?.processGroupId;
@@ -725,29 +542,15 @@ async function waitForBackendReady(
backendChild,
timeoutMs = 600_000,
{
checkBackendReady = (onOwnershipRejected) =>
isBackendReady({ onOwnershipRejected }),
checkBackendReady = isBackendReady,
readState = () => readJson(devStackStatePath),
resolveTargets = readBackendTargets,
} = {},
) {
const initialStateUpdatedAt = readState()?.updatedAt ?? '';
const startedAt = Date.now();
let lastOwnershipReason = '';
while (Date.now() - startedAt < timeoutMs) {
if (
await checkBackendReady((ownership) => {
if (ownership.reason === lastOwnershipReason) {
return;
}
lastOwnershipReason = ownership.reason;
// 本次自己拉起的后端如果归属校验一直不通过,必须把原因打出来,
// 否则只会表现为等待 600 秒后超时。
console.warn(
`[ai-game-creator-shell] 等待配套后端就绪时归属校验未通过(${ownership.reason}: ${formatOwnerLabel(ownership.apiOwner ?? ownership.spacetimeOwner ?? ownership.workerOwner)})。`,
);
})
) {
if (await checkBackendReady()) {
return resolveTargets();
}
const state = readState();
@@ -773,14 +576,7 @@ async function waitForBackendReady(
async function ensureBackend({
onBackendChild = () => {},
checkBackendReady = () =>
isBackendReady({
onOwnershipRejected(ownership) {
console.warn(
`[ai-game-creator-shell] 端口上的配套后端不属于当前工作树(${ownership.reason}: ${formatOwnerLabel(ownership.apiOwner ?? ownership.spacetimeOwner ?? ownership.workerOwner)}),改为启动本工作树自己的后端。`,
);
},
}),
checkBackendReady = isBackendReady,
resolveTargets = readBackendTargets,
spawnBackend = () =>
spawnChild(
@@ -860,33 +656,15 @@ async function startVite(apiTarget, endpoint = readAgcDevEndpoint()) {
async function main() {
let backendChild = null;
let startedBackend = false;
let viteChild = null;
let shutdownSignal = '';
const signalHandlers = new Map();
// 只有本次会话真正拉起过配套后端时才做兜底清扫:复用别人后端时不能连带
// 杀掉对方的进程。dev.mjs 的清理依赖它的 shell 包装层仍然活着,而 Ctrl+C
// 往往先杀掉包装层,所以这里必须按本工作树 api-server.exe 的身份再收一次。
const sweepStartedBackend = () => {
if (!startedBackend || process.platform !== 'win32') {
return;
}
const stopped = stopWindowsWorktreeProcesses({ apiServerExePath });
if (stopped.length > 0) {
console.log(
`[ai-game-creator-shell] 已清理残留后端进程: ${stopped.join(', ')}`,
);
}
};
for (const signal of ['SIGINT', 'SIGTERM']) {
const handler = () => {
shutdownSignal = signal;
stopChild(viteChild, signal);
stopChild(backendChild, signal);
// 立刻清扫,避免外层 taskkill /F 抢在 finally 之前把本进程杀掉。
sweepStartedBackend();
};
signalHandlers.set(signal, handler);
process.on(signal, handler);
@@ -905,7 +683,6 @@ async function main() {
},
});
backendChild = backend.backendChild;
startedBackend = Boolean(backendChild);
if (shutdownSignal) {
throw new Error(`启动期收到 ${shutdownSignal},已停止配套后端`);
}
@@ -939,7 +716,6 @@ async function main() {
terminateChildTree(viteChild),
terminateChildTree(backendChild),
]);
sweepStartedBackend();
for (const [signal, handler] of signalHandlers) {
process.off(signal, handler);
}
@@ -956,25 +732,20 @@ function isDirectModuleExecution() {
export {
ensureBackend,
formatChildFailure,
formatOwnerLabel,
isAiGameCreatorServer,
isBackendReady,
isDirectModuleExecution,
isProcessGroupAlive,
isWorktreeApiServerOwner,
isWorktreeSpacetimeOwner,
preflightExistingVite,
readBackendServiceFailure,
readChildFailure,
readExistingViteServer,
readLinuxProcessGroupAlive,
readWindowsPortOwnerIdentities,
resolveBackendTargetsFromState,
runWindowsTaskkill,
spawnChild,
stopChild,
terminateChildTree,
verifyAgcBackendOwnership,
waitForBackendReady,
waitForChildTermination,
};
@@ -1,7 +1,6 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { withDefaultCargoFeatures } from './cargo-features.mjs';
import {
readAgcDevEndpoint,
resolveAgcDevEndpoint,
@@ -21,10 +20,6 @@ import {
const appRoot = fileURLToPath(new URL('..', import.meta.url));
const repoRoot = resolve(appRoot, '../..');
const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG';
const AGC_DESIGN_DEBUG_VITE_ENV = 'VITE_GENARRATIVE_AGC_DESIGN_DEBUG';
const designDebugEnabled =
process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1';
function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
const args = [...argv];
@@ -48,24 +43,6 @@ function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
];
}
// `agc_cocos_execute` 与 Cocos 编辑器适配器只在 `cocos-editor-execute` feature 下
// 注册。开发构建默认在 Windows 打开它,否则 Agent 的工具清单里根本没有该工具,
// 只能退化成改写脚本。可用 AGC_DEV_CARGO_FEATURES(逗号分隔)覆盖,传空串即关闭。
function readDevCargoFeatures(env = process.env) {
const override = env.AGC_DEV_CARGO_FEATURES;
if (override !== undefined) {
return override
.split(',')
.map((value) => value.trim())
.filter(Boolean);
}
return process.platform === 'win32' ? ['cocos-editor-execute'] : [];
}
function withDevCargoFeatures(argv, features = readDevCargoFeatures()) {
return withDefaultCargoFeatures(argv, features);
}
function spawnTauriCli(argv, { env = process.env } = {}) {
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
cwd: appRoot,
@@ -129,16 +106,9 @@ async function runTauriDev(
shutdownRequested.then(() => false),
]);
if (!prepared || shutdownSignal) return 1;
const tauriArguments = buildTauriArguments(
withDevCargoFeatures(argv),
endpoint.url,
);
const tauriArguments = buildTauriArguments(argv, endpoint.url);
child = spawnCli(tauriArguments, {
env: {
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_ENV]: designDebugEnabled,
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
},
env: withAgcDevEndpointEnv(endpoint),
});
const childResult = waitForCli(child);
const outcome = await Promise.race([
@@ -187,13 +157,7 @@ async function prepareFrontendDev(endpoint, { onChild, signal }) {
const frontend = spawnChild(
process.platform === 'win32' ? 'npm.cmd' : 'npm',
['run', 'agc:serve'],
{
cwd: repoRoot,
env: {
...withAgcDevEndpointEnv(endpoint),
[AGC_DESIGN_DEBUG_VITE_ENV]: designDebugEnabled,
},
},
{ cwd: repoRoot, env: withAgcDevEndpointEnv(endpoint) },
);
onChild(frontend);
console.log(
@@ -229,7 +193,6 @@ export {
isDirectModuleExecution,
runTauriDev,
spawnTauriCli,
withDevCargoFeatures,
};
if (isDirectModuleExecution()) {
@@ -1,4 +0,0 @@
# resources/plugins 由 build.rs 从 plugins/ 复制生成,属于构建产物。
# 它在 dev 监听范围内,重新生成会让 Tauri dev 误判为源码改动而触发
# “构建 -> 监听 -> 再构建”的自触发循环。
resources/plugins/
+1 -26
View File
@@ -733,20 +733,6 @@ dependencies = [
"error-code",
]
[[package]]
name = "cocos-editor-bridge"
version = "0.1.0"
dependencies = [
"cc",
"editor-adapter-api",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2",
"tungstenite",
"windows-sys 0.61.2",
]
[[package]]
name = "combine"
version = "4.6.7"
@@ -1219,14 +1205,6 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "editor-adapter-api"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "either"
version = "1.16.0"
@@ -1725,14 +1703,12 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.27"
version = "0.1.12"
dependencies = [
"agent-runtime-core",
"axum",
"base64 0.22.1",
"chromiumoxide",
"cocos-editor-bridge",
"editor-adapter-api",
"futures",
"getrandom 0.3.4",
"http",
@@ -4305,7 +4281,6 @@ dependencies = [
"cookie",
"cookie_store",
"encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
"h2",
@@ -1,14 +1,11 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.27"
version = "0.1.12"
edition = "2021"
publish = false
[features]
default = []
cocos-editor = ["cocos-editor-bridge/process-discovery"]
cocos-editor-execute = ["cocos-editor", "cocos-editor-bridge/windows-bootstrap"]
cocos-editor-injection = ["cocos-editor-execute", "cocos-editor-bridge/windows-injection"]
[build-dependencies]
serde = { version = "1", features = ["derive"] }
@@ -22,8 +19,6 @@ ts-rs = "12.0.1"
typed_floats = { version = "1.0.7", features = ["serde"] }
nalgebra = { version = "0.35.0", features = ["serde-serialize"] }
agent-runtime-core = { path = "../../../server-rs/crates/agent-runtime-core" }
cocos-editor-bridge = { path = "../../../plugins/agc-cocos-editor/native/cocos-editor-bridge", default-features = false }
editor-adapter-api = { path = "../../../server-rs/crates/editor-adapter-api" }
base64 = "0.22"
axum = "0.8"
chromiumoxide = "0.9.1"
@@ -79,14 +74,6 @@ codegen-units = 256
lto = "off"
incremental = true
# Runner 启动阶段会对当前 Debug 可执行文件计算 SHA-256。仅优化密码学依赖,
# 保持业务代码的 Debug 编译速度,同时避免整份 Debug 构建因未优化 hash 热点而阻塞启动。
[profile.dev.package.sha2]
opt-level = 3
[profile.dev.package.digest]
opt-level = 3
[profile.test]
opt-level = 0
debug = 1
@@ -182,8 +182,6 @@ fn main() {
);
let manifest_path = manifest_dir.join("prompts/runtime/manifest.json");
stage_bundled_codex_cli(&manifest_dir);
stage_plugin_workspace(&manifest_dir);
stage_cocos_editor_payload(&manifest_dir);
let compiled = runtime_prompt_bundle::compile_manifest(&manifest_path)
.unwrap_or_else(|error| panic!("Prompt Bundle 编译失败:{error}"));
validate_seed_task_catalog(&compiled);
@@ -205,146 +203,3 @@ fn main() {
}
tauri_build::build()
}
#[cfg(windows)]
fn stage_cocos_editor_payload(manifest_dir: &std::path::Path) {
if std::env::var_os("CARGO_FEATURE_COCOS_EDITOR_INJECTION").is_none() {
return;
}
let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"));
let profile_dir = out_dir
.ancestors()
.find(|path| path.file_name().is_some_and(|name| name == "build"))
.and_then(|build_dir| build_dir.parent())
.expect("AGC Cargo profile directory not found");
let candidates = [
profile_dir.join("deps/cocos_editor_bridge.dll"),
profile_dir.join("cocos_editor_bridge.dll"),
];
let source = candidates
.iter()
.find(|path| path.is_file())
.unwrap_or_else(|| {
panic!(
"Cocos bridge native payload 未构建:{}",
candidates
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join("")
)
});
for destination in [
// 插件工作区里的 payload 是开发态与打包态的唯一真源。
manifest_dir
.join("../../../plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
// 随包资源目录与 tauri.windows.conf.json 的 `resources/plugins` 映射保持一致。
manifest_dir
.join("resources/plugins/agc-cocos-editor/native/payload/cocos-editor-bridge.dll"),
] {
std::fs::create_dir_all(destination.parent().expect("payload resource parent"))
.expect("创建 Cocos bridge payload 目录失败");
std::fs::copy(source, &destination).expect("复制 Cocos bridge native payload 失败");
}
println!("cargo:rerun-if-changed={}", source.display());
}
#[cfg(not(windows))]
fn stage_cocos_editor_payload(_manifest_dir: &std::path::Path) {}
/// 把 `plugins/` 工作区里的插件包随包映射到应用资源目录。
///
/// 只复制插件运行需要的清单、入口、面板和 native payload,不复制 native 源码、
/// Cargo target 目录或 node_modules。
#[cfg(windows)]
fn stage_plugin_workspace(manifest_dir: &std::path::Path) {
let repo_root = manifest_dir
.parent()
.and_then(|app_root| app_root.parent())
.and_then(|apps_dir| apps_dir.parent())
.expect("AGC 应用必须位于仓库 apps 目录下")
.to_path_buf();
let workspace = repo_root.join("plugins");
let destination_root = manifest_dir.join("resources/plugins");
std::fs::create_dir_all(&destination_root).expect("创建插件资源目录失败");
let entries = match std::fs::read_dir(&workspace) {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.flatten() {
let plugin_root = entry.path();
if !plugin_root.is_dir() || !plugin_root.join("plugin.json").is_file() {
continue;
}
let name = entry.file_name();
let destination = destination_root.join(&name);
copy_plugin_file(
&plugin_root.join("plugin.json"),
&destination.join("plugin.json"),
);
for relative in [
std::path::PathBuf::from("src"),
std::path::PathBuf::from("panels"),
std::path::PathBuf::from("native/payload"),
] {
copy_plugin_tree(&plugin_root.join(&relative), &destination.join(&relative));
}
println!("cargo:rerun-if-changed={}", plugin_root.display());
}
}
#[cfg(windows)]
fn stage_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
let Ok(bytes) = std::fs::read(source) else {
return;
};
if std::fs::read(destination).is_ok_and(|existing| existing == bytes) {
return;
}
if let Some(parent) = destination.parent() {
std::fs::create_dir_all(parent).expect("创建插件资源目录失败");
}
std::fs::write(destination, bytes).expect("复制插件资源失败");
}
#[cfg(windows)]
fn copy_plugin_tree(source: &std::path::Path, destination: &std::path::Path) {
let entries = match std::fs::read_dir(source) {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.flatten() {
let target = destination.join(entry.file_name());
let path = entry.path();
if path.is_dir() {
let name = entry.file_name();
let name = name.to_string_lossy();
if matches!(name.as_ref(), "target" | "node_modules" | ".git") {
continue;
}
std::fs::create_dir_all(&target).expect("创建插件资源目录失败");
copy_plugin_tree(&path, &target);
} else {
// 测试文件不随包分发。
let name = entry.file_name();
let name = name.to_string_lossy();
if name.contains(".test.") {
continue;
}
stage_plugin_file(&path, &target);
}
}
}
#[cfg(windows)]
fn copy_plugin_file(source: &std::path::Path, destination: &std::path::Path) {
if !source.is_file() {
return;
}
std::fs::create_dir_all(destination.parent().expect("插件资源父目录"))
.expect("创建插件资源目录失败");
std::fs::copy(source, destination).expect("复制插件资源失败");
}
#[cfg(not(windows))]
fn stage_plugin_workspace(_manifest_dir: &std::path::Path) {}
@@ -43,6 +43,7 @@ struct PromptCompositions {
/// 而是一份独立的完整清单:plan 根的工具面只有 7 个原生工具,专业组、
/// isolated child、任务图与视觉产物合同在这条链路上全部不可执行,逐段
/// 减法会把「plan 根到底看到什么」摊在两个函数的四个否定分支里。
supervisor_plan: Vec<String>,
supervisor_chat: SupervisorChatComposition,
}
@@ -98,6 +99,10 @@ struct ProviderFragments {
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct AgentCatalog {
supervisor: AgentGroup,
/// 立项策划子 Agent。与 `supervisor` 平级、**不进 `groups`**`specialist_nodes`
/// 只从 `groups[].roles[]` 派生,因此它不参与 `build.rs` 与种子 DAG 的一致性
/// 校验,「做游戏」的 16 任务 DAG 一行不动。详见技术方案第 3.1 节。
planning: AgentGroup,
groups: Vec<AgentGroup>,
}
@@ -226,6 +231,12 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
&sections,
&["$base", "$visualContract"],
)?;
validate_composition(
"supervisorPlan",
&manifest.compositions.supervisor_plan,
&sections,
&["$header"],
)?;
validate_section_reference(
&manifest.compositions.supervisor_chat.identity,
&sections,
@@ -288,6 +299,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
.supervisor
.roles
.iter()
.chain(manifest.agent_catalog.planning.roles.iter())
.chain(
manifest
.agent_catalog
@@ -336,6 +348,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
.runtime
.iter()
.chain(manifest.compositions.supervisor.iter())
.chain(manifest.compositions.supervisor_plan.iter())
.filter(|item| !item.starts_with('$'))
.cloned()
.collect::<BTreeSet<_>>();
@@ -408,6 +421,14 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
{
register("composition supervisor", section);
}
for section in manifest
.compositions
.supervisor_plan
.iter()
.filter(|section| !section.starts_with('$'))
{
register("composition supervisorPlan", section);
}
register("composition supervisorChat.identity", identity);
register(
"composition supervisorChat.finalReply",
@@ -436,9 +457,19 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
"composition supervisor",
"composition supervisorChat.identity",
]);
// plan 根 composition 是 Supervisor system prompt 的第二条 lane,不是另一种
// 语义面。它按设计复用 runtime lane 的 `isolatedAgentContract``agent.delegate`
// 的 expectedArtifacts/writeScopes 合同)和 supervisor lane 的 `supervisorRepair`
// (返工必须逐字继承原合同)。除这两个方向外,跨所有者复用仍然是错误。
let allowed_plan_runtime_owners =
BTreeSet::from(["composition runtime", "composition supervisorPlan"]);
let allowed_plan_supervisor_owners =
BTreeSet::from(["composition supervisor", "composition supervisorPlan"]);
for (section, section_owners) in owners {
if section_owners.len() > 1
&& !(section == identity && section_owners == allowed_identity_owners)
&& section_owners != allowed_plan_runtime_owners
&& section_owners != allowed_plan_supervisor_owners
{
return Err(format!(
"Prompt section 跨语义所有者复用:{section} -> {section_owners:?}"
@@ -675,11 +706,17 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
if catalog.supervisor.roles.len() != 1 {
return Err("agentCatalog.supervisor 必须且只能包含一个 role".to_string());
}
if catalog.planning.roles.len() != 1 {
return Err("agentCatalog.planning 必须且只能包含一个 role".to_string());
}
if catalog.groups.is_empty() {
return Err("agentCatalog.groups 不能为空".to_string());
}
let mut group_brief_names = BTreeSet::new();
for group in std::iter::once(&catalog.supervisor).chain(catalog.groups.iter()) {
for group in std::iter::once(&catalog.supervisor)
.chain(std::iter::once(&catalog.planning))
.chain(catalog.groups.iter())
{
if !group_brief_names.insert(group.brief_path_name.as_str()) {
return Err(format!(
"agent group briefPathName 重复:{}",
@@ -687,7 +724,10 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
));
}
}
let mut generated_names = BTreeSet::from(["PROJECT_SUPERVISOR".to_string()]);
let mut generated_names = BTreeSet::from([
"PROJECT_SUPERVISOR".to_string(),
"PROJECT_PLANNING".to_string(),
]);
for group in &catalog.groups {
let generated = rust_identifier(&group.id);
if !generated
@@ -713,6 +753,12 @@ fn validate_agent_catalog(catalog: &AgentCatalog) -> Result<(), String> {
&mut task_ids,
&mut tool_ids,
)?;
validate_agent_group(
&catalog.planning,
&mut group_ids,
&mut task_ids,
&mut tool_ids,
)?;
for group in &catalog.groups {
validate_agent_group(group, &mut group_ids, &mut task_ids, &mut tool_ids)?;
}
@@ -874,6 +920,10 @@ fn render_rust(manifest: &PromptBundleManifest, sections: &BTreeMap<String, Stri
"RUNTIME_PROMPT_SUPERVISOR_COMPOSITION",
&manifest.compositions.supervisor,
));
output.push_str(&render_string_slice_const(
"RUNTIME_PROMPT_SUPERVISOR_PLAN_COMPOSITION",
&manifest.compositions.supervisor_plan,
));
output.push_str(&format!(
"pub(crate) const RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION: &[&str] = &[{}, {}];\n",
rust_literal(&manifest.compositions.supervisor_chat.identity),
@@ -962,6 +1012,26 @@ fn render_agent_catalog(catalog: &AgentCatalog) -> String {
"static PROJECT_SUPERVISOR_AGENT_DEFINITION: AgentGroupDefinition = {};\n",
render_group_value(&catalog.supervisor, "&PROJECT_SUPERVISOR_AGENT_ROLES")
));
let planning_role = &catalog.planning.roles[0];
output.push_str(&format!(
"pub(crate) const GAME_CREATOR_PROJECT_PLANNING_AGENT_ID: &str = {};\n",
rust_literal(&planning_role.task_id)
));
output.push_str(&format!(
"pub(crate) const GAME_CREATOR_PROJECT_PLANNING_MEMORY_PATH: &str = {};\n",
rust_literal(&format!(
"memory/agents/{}",
catalog.planning.brief_path_name
))
));
output.push_str(&render_role_array(
"PROJECT_PLANNING_AGENT_ROLES",
&catalog.planning.roles,
));
output.push_str(&format!(
"static PROJECT_PLANNING_AGENT_DEFINITION: AgentGroupDefinition = {};\n",
render_group_value(&catalog.planning, "&PROJECT_PLANNING_AGENT_ROLES")
));
for group in &catalog.groups {
let roles_name = format!("{}_AGENT_ROLES", rust_identifier(&group.id));
output.push_str(&render_role_array(&roles_name, &group.roles));
@@ -1,7 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main",
"description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件;允许弹出原生打开/保存对话框用于素材上传与导出。",
"description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件。",
"windows": ["client"],
"permissions": [
"clipboard-manager:allow-read-image",
@@ -21,7 +21,6 @@
]
},
"opener:default",
"dialog:allow-open",
"dialog:allow-save"
"dialog:allow-open"
]
}
@@ -1 +0,0 @@
当前阶段:系统架构。明确系统清单、职责边界、依赖和数据归属。
@@ -1,6 +0,0 @@
共享过程文件(如需维护,请使用这些相对路径):
- project/analysis.md
- project/决策台账.md
- project/dialog.md
不要把正式产物写在工作区根目录,也不要等审批失败后再迁移。
阶段审批工具:当你判断本阶段必需产物已完成时,必须提交阶段审批。用户批准后 Runtime 自动进入下一阶段;你不能自行切换阶段。
@@ -1 +0,0 @@
当前阶段:概念设计。明确游戏是什么、不是什么,并形成概念设计产物。

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