Merge remote-tracking branch 'origin/master' into feat/agc_add_on
Project CI / Repository checks (pull_request) Failing after 2m51s
Project CI / Native shell tests (pull_request) Failing after 5m0s
Project CI / Frontend tests (pull_request) Successful in 5m6s
Project CI / Backend tests (pull_request) Successful in 7m39s

# Conflicts:
#	docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md
This commit is contained in:
2026-09-01 08:52:07 +00:00
1052 changed files with 28412 additions and 19835 deletions
@@ -19,14 +19,17 @@
## 第一阶段模块
- `ImageCanvasEditorTypes.ts`
- 承载编辑器前端共享类型:素材、图层、视口、工具、生成对象、历史快照、剪贴板、右键菜单、拖拽状态等。
- 只暴露类型,不承载运行时逻辑。
- `ImageCanvasEditorModel.ts`
- 承载画布基础模型:尺寸、缩放、背景色、素材默认文件夹、快照序列化 / 水合、素材库快照映射、吸附、右键菜单定位、DataTransfer 工具和通用数值格式化。
- 保留“图片显示尺寸跟随 Resolution”“只保留一个默认素材文件夹”“右键菜单不滚动而是限制到视口内”等规则。
- `ImageCanvasGenerationModel.ts`
- 承载生成相关模型:生成占位尺寸、默认模型、规范表单默认值、角色动画选项、生成输入快照、规范 prompt 构建、生成对象识别和错误文案。
- 保留角色动画优先用 `objectKey` 的体积保护规则。
@@ -111,6 +114,7 @@
## 第十一阶段模块
- `ImageCanvasFileModel.ts`
- 承载图片文件判定和 `FileReader` Data URL 读取工具,供素材上传、生成参考图上传和后续导入能力复用。
- 该模块不依赖素材库状态,避免把通用文件读取继续挂在素材库 hook 上。
@@ -221,6 +225,7 @@
## 第二十五阶段模块
- `ImageCanvasStageControllerModel.ts`
- 承载舞台派生状态和右键菜单模型:选中图层、选中浮动工具栏位置、图片菜单图层、右键菜单目标图层,以及显示 / 解锁菜单文案判断。
- 该模型复用既有图层命令模型与浮层定位模型,不重新实现右键目标和选中工具栏坐标规则;生成 Composer 锚点不属于舞台控制器,后续由生成表面编排统一负责。
- 新增单测覆盖选中工具栏位置、右键目标集合、显示 / 解锁判断和菜单位置限制。
@@ -43,30 +43,30 @@
`ADMIN_TAB_PERMISSIONS` 必须是 shared-contracts 与 admin-web 共用的闭合集合,值与现有 `AdminRouteId` 一致。15 个可分配权限如下,顺序同时作为前端寻找“第一可访问项”的稳定顺序:
| permission id | 一级 Tab | hash |
| --- | --- | --- |
| `dashboard` | Dashboard | `#dashboard` |
| `overview` | 服务总览 | `#overview` |
| `tables` | 表查询 | `#tables` |
| `debug` | API 调试 | `#debug` |
| `tracking` | 埋点数据 | `#tracking` |
| `gray-release` | 灰度发布 | `#gray-release` |
| `redeem` | 兑换码 | `#redeem` |
| `invite` | 邀请码 | `#invite` |
| `profile-wallet` | 账号配置 | `#profile-wallet` |
| `tasks` | 任务配置 | `#tasks` |
| `recharge-products` | 充值商品 | `#recharge-products` |
| `recharge-orders` | 充值管理 | `#recharge-orders` |
| `editor-generation-pricing` | 模型定价 | `#editor-generation-pricing` |
| `editor-showcase` | 精选审核 | `#editor-showcase` |
| `editor-assets` | 素材查询 | `#editor-assets` |
| permission id | 一级 Tab | hash |
| --------------------------- | --------- | ---------------------------- |
| `dashboard` | Dashboard | `#dashboard` |
| `overview` | 服务总览 | `#overview` |
| `tables` | 表查询 | `#tables` |
| `debug` | API 调试 | `#debug` |
| `tracking` | 埋点数据 | `#tracking` |
| `gray-release` | 灰度发布 | `#gray-release` |
| `redeem` | 兑换码 | `#redeem` |
| `invite` | 邀请码 | `#invite` |
| `profile-wallet` | 账号配置 | `#profile-wallet` |
| `tasks` | 任务配置 | `#tasks` |
| `recharge-products` | 充值商品 | `#recharge-products` |
| `recharge-orders` | 充值管理 | `#recharge-orders` |
| `editor-generation-pricing` | 模型定价 | `#editor-generation-pricing` |
| `editor-showcase` | 精选审核 | `#editor-showcase` |
| `editor-assets` | 素材查询 | `#editor-assets` |
Tab 权限数组必须去重并按上表顺序规范化后保存。保存时拒绝未知值和 `accounts`;读取旧数据时遇到未知值应忽略并记录告警,绝不能将未知值解释为全权限。空数组合法,表示 member 可以登录但没有业务页面权限。
`ADMIN_ACTION_PERMISSIONS` 是独立操作权限闭合集合,当前只有:
| permission id | 操作 | 授权边界 |
| --- | --- | --- |
| permission id | 操作 | 授权边界 |
| -------------------------------------- | -------------------- | ----------------------------------------------------------------------- |
| `profile-wallet-consumption-reconcile` | 手动对账用户历史花费 | owner 默认拥有;member 必须在账号管理中单独勾选,不要求同时持有特定 Tab |
独立操作权限保存在 `action_permissions_json`,响应为 `actionPermissions`;未知值必须拒绝。后续新增一级 Tab 时,必须在同一次改动中更新:
@@ -80,20 +80,20 @@ Tab 权限数组必须去重并按上表顺序规范化后保存。保存时拒
新增 SpacetimeDB 私有表 `admin_account`。表不能标记 `public`,浏览器不能订阅或直查;所有读写都由 `api-server -> spacetime-client facade -> 受限 procedure` 完成。
| 字段 | Rust / SpacetimeDB 类型 | 约束与语义 |
| --- | --- | --- |
| `account_id` | `String` | 主键;服务端生成不可变 opaque id,建议 `admin-account-<uuid>`,请求体不得指定 |
| `username` | `String` | `unique`;登录名,创建后不可修改;按 `trim + ASCII lowercase` 规范化 |
| `display_name` | `String` | 展示名,去除首尾空白后 1 至 64 字符 |
| `password_hash` | `String` | Argon2id PHC 字符串;只在内部登录查询中返回给 api-server,永不进入 HTTP DTO、日志或前端状态 |
| `tab_permissions_json` | `String` | 规范化后的 Tab permission JSON;只允许第 4 节 15 个值,空数组为 `[]` |
| `enabled` | `bool` | 是否允许登录和继续使用现有 JWT |
| `token_version` | `u64` | 初始为 `1`;权限、密码或启停状态发生有效变化时加 `1` |
| `created_by` | `String` | 创建者后台 subject;当前只能是 owner subject |
| `updated_by` | `String` | 最近更新者后台 subject;当前只能是 owner subject |
| `created_at` | `Timestamp` | 创建时间,使用 `ctx.timestamp` |
| `updated_at` | `Timestamp` | 最近更新时间,使用 `ctx.timestamp` |
| `action_permissions_json` | `Option<String>` | 既有表末尾追加;旧行默认 `None` 并按 `[]` 读取,只允许第 4 节独立操作权限 |
| 字段 | Rust / SpacetimeDB 类型 | 约束与语义 |
| ------------------------- | ----------------------- | ------------------------------------------------------------------------------------------- |
| `account_id` | `String` | 主键;服务端生成不可变 opaque id,建议 `admin-account-<uuid>`,请求体不得指定 |
| `username` | `String` | `unique`;登录名,创建后不可修改;按 `trim + ASCII lowercase` 规范化 |
| `display_name` | `String` | 展示名,去除首尾空白后 1 至 64 字符 |
| `password_hash` | `String` | Argon2id PHC 字符串;只在内部登录查询中返回给 api-server,永不进入 HTTP DTO、日志或前端状态 |
| `tab_permissions_json` | `String` | 规范化后的 Tab permission JSON;只允许第 4 节 15 个值,空数组为 `[]` |
| `enabled` | `bool` | 是否允许登录和继续使用现有 JWT |
| `token_version` | `u64` | 初始为 `1`;权限、密码或启停状态发生有效变化时加 `1` |
| `created_by` | `String` | 创建者后台 subject;当前只能是 owner subject |
| `updated_by` | `String` | 最近更新者后台 subject;当前只能是 owner subject |
| `created_at` | `Timestamp` | 创建时间,使用 `ctx.timestamp` |
| `updated_at` | `Timestamp` | 最近更新时间,使用 `ctx.timestamp` |
| `action_permissions_json` | `Option<String>` | 既有表末尾追加;旧行默认 `None` 并按 `[]` 读取,只允许第 4 节独立操作权限 |
账号规则:
@@ -108,13 +108,13 @@ Tab 权限数组必须去重并按上表顺序规范化后保存。保存时拒
建议新增 `server-rs/crates/spacetime-module/src/admin_account.rs`,并在 `spacetime-client` 增加对应 admin facade。至少提供以下 typed procedures
| procedure | 用途 | 是否可返回 `password_hash` |
| --- | --- | --- |
| `get_admin_account_by_username_and_return` | member 登录查询 | 是,仅返回给 api-server 内部认证路径 |
| `get_admin_account_by_id_and_return` | member JWT 逐请求校验 | 否 |
| `list_admin_accounts_and_return` | owner 账号列表 | 否 |
| `create_admin_account_and_return` | owner 创建 member | 否 |
| `update_admin_account_and_return` | owner 更新展示名、密码 hash、权限、启停 | 否 |
| procedure | 用途 | 是否可返回 `password_hash` |
| ------------------------------------------ | --------------------------------------- | ------------------------------------ |
| `get_admin_account_by_username_and_return` | member 登录查询 | 是,仅返回给 api-server 内部认证路径 |
| `get_admin_account_by_id_and_return` | member JWT 逐请求校验 | 否 |
| `list_admin_accounts_and_return` | owner 账号列表 | 否 |
| `create_admin_account_and_return` | owner 创建 member | 否 |
| `update_admin_account_and_return` | owner 更新展示名、密码 hash、权限、启停 | 否 |
所有 `admin_account` procedures 都必须在事务入口调用现有 `require_editor_generation_runtime_service_identity(...)` 等价的统一 runtime service identity 守卫,只允许 api-server 当前 runtime service identity 调用。不能因为它们位于后台命名空间就接受任意 SpacetimeDB client identity,也不能新增 public table/view 暴露账号或 hash。
@@ -191,53 +191,53 @@ owner 返回全部 15 个 Tab permission id 和全部独立操作权限;member
下表覆盖 `server-rs/crates/api-server/src/modules/admin.rs` 当前全部路由,并追加账号管理 API。`A OR B` 表示 member 拥有任一权限即可;owner 对全部行自动通过。
| Method | 路径 | 权限 |
| --- | --- | --- |
| `POST` | `/admin/api/login` | 公开登录入口,不要求 JWT |
| `GET` | `/admin/api/me` | 任意有效后台会话 |
| `GET` | `/admin/api/overview` | `overview` |
| `GET` | `/admin/api/dashboard` | `dashboard` |
| `POST` | `/admin/api/debug/http` | `debug` |
| `GET` | `/admin/api/tracking/events` | `tracking` |
| `GET` | `/admin/api/tracking/event-keys` | `tracking OR tasks` |
| `GET` | `/admin/api/database/tables` | `tables` |
| `GET` | `/admin/api/database/tables/{table_name}/rows` | `tables` |
| `GET` | `/admin/api/feature-gates` | `gray-release` |
| `PUT` | `/admin/api/feature-gates` | `gray-release` |
| `GET` | `/admin/api/editor-generation-pricing` | `editor-generation-pricing` |
| `POST` | `/admin/api/editor-generation-pricing` | `editor-generation-pricing` |
| `GET` | `/admin/api/editor-assets` | `editor-assets` |
| `GET` | `/admin/api/assets/read-url` | `editor-assets OR editor-showcase` |
| `GET` | `/admin/api/editor-showcase/assets` | `editor-showcase` |
| `POST` | `/admin/api/editor-showcase/assets/review` | `editor-showcase` |
| `POST` | `/admin/api/editor-showcase/assets/display` | `editor-showcase` |
| `GET` | `/admin/api/editor-showcase/campaign` | `editor-showcase` |
| `POST` | `/admin/api/editor-showcase/campaign` | `editor-showcase` |
| `POST` | `/admin/api/editor-showcase/campaign/image-upload-ticket` | `editor-showcase` |
| `GET` | `/admin/api/profile/redeem-codes` | `redeem` |
| `POST` | `/admin/api/profile/redeem-codes` | `redeem` |
| `POST` | `/admin/api/profile/redeem-codes/disable` | `redeem` |
| `GET` | `/admin/api/profile/invite-codes` | `invite` |
| `POST` | `/admin/api/profile/invite-codes` | `invite` |
| `GET` | `/admin/api/profile/tasks` | `tasks` |
| `POST` | `/admin/api/profile/tasks` | `tasks` |
| `POST` | `/admin/api/profile/tasks/disable` | `tasks` |
| `GET` | `/admin/api/profile/wallet-config` | `profile-wallet` |
| `POST` | `/admin/api/profile/wallet-config` | `profile-wallet` |
| `GET` | `/admin/api/profile/recharge-products` | `recharge-products` |
| `POST` | `/admin/api/profile/recharge-products` | `recharge-products` |
| `GET` | `/admin/api/profile/recharge-orders` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/preview` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/execute` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/register` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/manual-review/resolve` | `recharge-orders` |
| `GET` | `/admin/api/profile/users/detail` | `tables OR tracking OR recharge-orders OR editor-showcase OR editor-assets` |
| `POST` | `/admin/api/profile/users/reconcile-consumption` | 独立操作权限 `profile-wallet-consumption-reconcile` |
| `POST` | `/admin/api/profile/users/initialize-consumption-projections` | owner-only 维护窗口操作 |
| `POST` | `/admin/api/profile/wallet-restriction` | `recharge-orders` |
| `GET` | `/admin/api/accounts` | owner-only |
| `POST` | `/admin/api/accounts` | owner-only |
| `PUT` | `/admin/api/accounts/{account_id}` | owner-only |
| Method | 路径 | 权限 |
| ------ | ------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `POST` | `/admin/api/login` | 公开登录入口,不要求 JWT |
| `GET` | `/admin/api/me` | 任意有效后台会话 |
| `GET` | `/admin/api/overview` | `overview` |
| `GET` | `/admin/api/dashboard` | `dashboard` |
| `POST` | `/admin/api/debug/http` | `debug` |
| `GET` | `/admin/api/tracking/events` | `tracking` |
| `GET` | `/admin/api/tracking/event-keys` | `tracking OR tasks` |
| `GET` | `/admin/api/database/tables` | `tables` |
| `GET` | `/admin/api/database/tables/{table_name}/rows` | `tables` |
| `GET` | `/admin/api/feature-gates` | `gray-release` |
| `PUT` | `/admin/api/feature-gates` | `gray-release` |
| `GET` | `/admin/api/editor-generation-pricing` | `editor-generation-pricing` |
| `POST` | `/admin/api/editor-generation-pricing` | `editor-generation-pricing` |
| `GET` | `/admin/api/editor-assets` | `editor-assets` |
| `GET` | `/admin/api/assets/read-url` | `editor-assets OR editor-showcase` |
| `GET` | `/admin/api/editor-showcase/assets` | `editor-showcase` |
| `POST` | `/admin/api/editor-showcase/assets/review` | `editor-showcase` |
| `POST` | `/admin/api/editor-showcase/assets/display` | `editor-showcase` |
| `GET` | `/admin/api/editor-showcase/campaign` | `editor-showcase` |
| `POST` | `/admin/api/editor-showcase/campaign` | `editor-showcase` |
| `POST` | `/admin/api/editor-showcase/campaign/image-upload-ticket` | `editor-showcase` |
| `GET` | `/admin/api/profile/redeem-codes` | `redeem` |
| `POST` | `/admin/api/profile/redeem-codes` | `redeem` |
| `POST` | `/admin/api/profile/redeem-codes/disable` | `redeem` |
| `GET` | `/admin/api/profile/invite-codes` | `invite` |
| `POST` | `/admin/api/profile/invite-codes` | `invite` |
| `GET` | `/admin/api/profile/tasks` | `tasks` |
| `POST` | `/admin/api/profile/tasks` | `tasks` |
| `POST` | `/admin/api/profile/tasks/disable` | `tasks` |
| `GET` | `/admin/api/profile/wallet-config` | `profile-wallet` |
| `POST` | `/admin/api/profile/wallet-config` | `profile-wallet` |
| `GET` | `/admin/api/profile/recharge-products` | `recharge-products` |
| `POST` | `/admin/api/profile/recharge-products` | `recharge-products` |
| `GET` | `/admin/api/profile/recharge-orders` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/preview` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/execute` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/register` | `recharge-orders` |
| `POST` | `/admin/api/profile/recharge-refunds/manual-review/resolve` | `recharge-orders` |
| `GET` | `/admin/api/profile/users/detail` | `tables OR tracking OR recharge-orders OR editor-showcase OR editor-assets` |
| `POST` | `/admin/api/profile/users/reconcile-consumption` | 独立操作权限 `profile-wallet-consumption-reconcile` |
| `POST` | `/admin/api/profile/users/initialize-consumption-projections` | owner-only 维护窗口操作 |
| `POST` | `/admin/api/profile/wallet-restriction` | `recharge-orders` |
| `GET` | `/admin/api/accounts` | owner-only |
| `POST` | `/admin/api/accounts` | owner-only |
| `PUT` | `/admin/api/accounts/{account_id}` | owner-only |
两个共享读取接口必须按 OR 规则实现,不能为了复用简单中间件扩大成“任意 member 可访问”:
@@ -8,23 +8,23 @@
## 1. 决策摘要
| 决策项 | 首版结论 |
| --- | --- |
| 调度单位 | 一次逻辑 BgFilter 调用;角色动画为单帧 |
| 父流程 | 保持原 future、调用栈、lease 和 `attempt`,同步等待内部 HTTP |
| 通用 worker 槽 | 等待期间继续占用;父 heartbeat 继续运行 |
| 请求输入(父 → 子) | 只传私有 OSS `objectKey`、BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联;不重复传源图字节,也不传签名 URL |
| 成功输出(子 → 父) | 内部 HTTP body 直接传回 BgFilter 结果图片的原始字节;不使用 Base64、不返回结果 object key、不先写 raw OSS |
| BgFilter worker | 首版只运行一个内部 HTTP worker 实例 |
| 并发 | 进程内 `Semaphore(N)`,并增加有界 admission 上限 `Q` |
| 重试 | 子 worker 对一次逻辑调用最多做两次顺序 provider attempt;父侧不重试已被 worker 接收的内部 RPC,仅连接从未建立时按预算有界重连(§5.1 / §7.1) |
| 超时 | 双预算:父侧派生排队预算 `maxQueueWaitMs` 与调用预算 `callBudgetMs`attempt 上限由 `N × est × 2` 公式运行时派生(est 默认 `5s`),排队不侵蚀调用时间 |
| flat / complex 熔断 | 迁到唯一子 worker;两种模式共享阈值和 `120s` cooldown,但分别维护独立进程内状态 |
| 业务语义 | 父流程继续负责 Alpha / 尺寸恢复、flat fallback、最终 OSS、画布写回、计费和父终态 |
| 动画失败 | 首版保持当前“所有已提交帧都等待并排空”语义,不新增跨帧取消组 |
| 崩溃恢复 | 不查询、不恢复 BgFilter 结果;父 job 沿用现有 lease、失败和退款语义 |
| 数据模型 | 不新增 SpacetimeDB 表,不修改 `external_generation_job` schema |
| 配置加载 | 子 worker 先加载 API 基础环境,再加载 worker 专属环境覆盖;共享超时保持单一来源 |
| 决策项 | 首版结论 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| 调度单位 | 一次逻辑 BgFilter 调用;角色动画为单帧 |
| 父流程 | 保持原 future、调用栈、lease 和 `attempt`,同步等待内部 HTTP |
| 通用 worker 槽 | 等待期间继续占用;父 heartbeat 继续运行 |
| 请求输入(父 → 子) | 只传私有 OSS `objectKey`、BgFilter 参数、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和有界审计关联;不重复传源图字节,也不传签名 URL |
| 成功输出(子 → 父) | 内部 HTTP body 直接传回 BgFilter 结果图片的原始字节;不使用 Base64、不返回结果 object key、不先写 raw OSS |
| BgFilter worker | 首版只运行一个内部 HTTP worker 实例 |
| 并发 | 进程内 `Semaphore(N)`,并增加有界 admission 上限 `Q` |
| 重试 | 子 worker 对一次逻辑调用最多做两次顺序 provider attempt;父侧不重试已被 worker 接收的内部 RPC,仅连接从未建立时按预算有界重连(§5.1 / §7.1) |
| 超时 | 双预算:父侧派生排队预算 `maxQueueWaitMs` 与调用预算 `callBudgetMs`attempt 上限由 `N × est × 2` 公式运行时派生(est 默认 `5s`),排队不侵蚀调用时间 |
| flat / complex 熔断 | 迁到唯一子 worker;两种模式共享阈值和 `120s` cooldown,但分别维护独立进程内状态 |
| 业务语义 | 父流程继续负责 Alpha / 尺寸恢复、flat fallback、最终 OSS、画布写回、计费和父终态 |
| 动画失败 | 首版保持当前“所有已提交帧都等待并排空”语义,不新增跨帧取消组 |
| 崩溃恢复 | 不查询、不恢复 BgFilter 结果;父 job 沿用现有 lease、失败和退款语义 |
| 数据模型 | 不新增 SpacetimeDB 表,不修改 `external_generation_job` schema |
| 配置加载 | 子 worker 先加载 API 基础环境,再加载 worker 专属环境覆盖;共享超时保持单一来源 |
首版明确不实现:
@@ -102,13 +102,13 @@ flowchart LR
请求和响应采用不同口径,不能把“请求不传源图字节”理解成“响应也不能传图片字节”:
| 阶段 | 传递内容 | 是否新增持久化 |
| --- | --- | --- |
| 父流程 → `bgfilter-worker` | JSON:源图 `objectKey`、参数和预算 | 否 |
| `bgfilter-worker` → BgFilter provider | 子 worker 现场签发的源图短期 URL | 否 |
| BgFilter provider → `bgfilter-worker` | 结果图片字节 | 否,只在子 worker 有界内存中读取和校验 |
| `bgfilter-worker` → 父流程 | `2xx` HTTP body 中的原始结果图片字节 | 否,父侧直接读入有界字节缓冲 |
| 父流程 → OSS / 业务写回 | 现有后处理后的最终图片 | 是,仍只走父流程现有最终持久化路径 |
| 阶段 | 传递内容 | 是否新增持久化 |
| ------------------------------------- | ------------------------------------ | -------------------------------------- |
| 父流程 → `bgfilter-worker` | JSON:源图 `objectKey`、参数和预算 | 否 |
| `bgfilter-worker` → BgFilter provider | 子 worker 现场签发的源图短期 URL | 否 |
| BgFilter provider → `bgfilter-worker` | 结果图片字节 | 否,只在子 worker 有界内存中读取和校验 |
| `bgfilter-worker` → 父流程 | `2xx` HTTP body 中的原始结果图片字节 | 否,父侧直接读入有界字节缓冲 |
| 父流程 → OSS / 业务写回 | 现有后处理后的最终图片 | 是,仍只走父流程现有最终持久化路径 |
因此,本方案所说的“直接返回二进制”就是直接传图片字节:父侧内部 client 的成功结果是 `Bytes` / `Vec<u8>` 一类有界内存缓冲及可信的图片类型,而不是 Base64 字符串、临时 object key 或子任务结果记录。这里不是把 provider 响应边读边透明转发;子 worker 要先完整读取并校验结果,确认本次 attempt 成功后,再把同一份图片内容作为内部 HTTP body 返回,以保留第二次顺序尝试和无效图片拦截能力。
@@ -298,11 +298,11 @@ inline / External v1 当前没有显式 `RequestContext` deadline 时,内部 R
当前父 worker 可能提交的最大帧请求数并不只有 `96`
| 场景 | 潜在同时提交的动画帧调用 |
| --- | ---: |
| 一个动画父 job | `48` |
| 一个默认 external-generation-worker,父并发 `2` | `96` |
| controller 最多 `8` 个父 worker、每个并发 `2` | `768` |
| 场景 | 潜在同时提交的动画帧调用 |
| ----------------------------------------------- | -----------------------: |
| 一个动画父 job | `48` |
| 一个默认 external-generation-worker,父并发 `2` | `96` |
| controller 最多 `8` 个父 worker、每个并发 `2` | `768` |
理论最大 `768` 远低于保险丝 `2048`,正常业务不会触发 `overloaded`;过载时的降级路径改由 §5.2 的动态排队探测承担——排队超出合理预期的 flat 请求提早进入 fallbackcomplex 失败。
@@ -364,16 +364,16 @@ flat / complex 统一使用的 `GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRE
## 8. 业务语义保持
| 场景 | 内部响应 | 父流程行为 |
| --- | --- | --- |
| flat 单图 / 动画帧 | 图片二进制 | 继续现有 Alpha / 尺寸恢复、finalizer 和最终持久化 |
| flat,父业务预算仍有效 | `provider_exhausted / circuit_open / deadline_exceeded / overloaded / invalid_result / internal_error` 或内部断连 | 记录对应故障后进入现有“阿里云通用抠图 → 本地键色”;这些内部错误本身不都计入熔断 |
| flat | `cancelled`(保留码,首版子 worker 不产生),或父 job cancellation / 绝对 deadline 已生效 | 立即向上退出,不再启动阿里云或本地 fallback |
| flat | `invalid_request / unauthorized` | 作为内部契约或部署配置错误失败,不 fallback、不计入 BgFilter 熔断 |
| complex 手动去背景 | 图片二进制 | 父流程继续最终 OSS、资源和画布写回 |
| complex 手动去背景 | 任意非成功或断连 | 父流程直接失败;provider 失败只累计 complex 熔断,不得接 flat fallback 或修改 flat 熔断 |
| 角色 / 图标 / UI 后处理最终失败 | BgFilter 与 fallback 都未得到可用结果 | 保留已持久化 provider 原图,以现有 `completed + warning` 收口 |
| 动画任一帧最终失败 | 该帧完整 fallback / finalizer / PUT 仍失败 | 排空其它已提交帧后,整项动画按现有语义失败退款 |
| 场景 | 内部响应 | 父流程行为 |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| flat 单图 / 动画帧 | 图片二进制 | 继续现有 Alpha / 尺寸恢复、finalizer 和最终持久化 |
| flat,父业务预算仍有效 | `provider_exhausted / circuit_open / deadline_exceeded / overloaded / invalid_result / internal_error` 或内部断连 | 记录对应故障后进入现有“阿里云通用抠图 → 本地键色”;这些内部错误本身不都计入熔断 |
| flat | `cancelled`(保留码,首版子 worker 不产生),或父 job cancellation / 绝对 deadline 已生效 | 立即向上退出,不再启动阿里云或本地 fallback |
| flat | `invalid_request / unauthorized` | 作为内部契约或部署配置错误失败,不 fallback、不计入 BgFilter 熔断 |
| complex 手动去背景 | 图片二进制 | 父流程继续最终 OSS、资源和画布写回 |
| complex 手动去背景 | 任意非成功或断连 | 父流程直接失败;provider 失败只累计 complex 熔断,不得接 flat fallback 或修改 flat 熔断 |
| 角色 / 图标 / UI 后处理最终失败 | BgFilter 与 fallback 都未得到可用结果 | 保留已持久化 provider 原图,以现有 `completed + warning` 收口 |
| 动画任一帧最终失败 | 该帧完整 fallback / finalizer / PUT 仍失败 | 排空其它已提交帧后,整项动画按现有语义失败退款 |
父侧移除现有 flat / complex BgFilter retry loop 和本地 flat 熔断,避免父侧两次 × 子 worker 两次变成四次。所有入口只替换共同的低层 BgFilter helper;这样 External v1 直接调用 `_for_owner` 的路径也会自然经过内部 worker。
@@ -60,29 +60,29 @@ BFF 只做鉴权、授权裁剪、字段脱敏和契约映射;worker 调度、
新增私有表 `external_generation_job`
| 字段 | 说明 |
| --- | --- |
| `job_id` | 主键,`extgen-` 前缀 UUID |
| `dedupe_key` | 唯一键,建议为 `play/action/session/scope` |
| `job_kind` | 执行类型,当前覆盖 `puzzle_compile_draft``puzzle_generate_images``puzzle_generate_ui_background`、跳一跳 / 拼消消 / 敲木鱼生成动作,以及 `editor_image_generation``editor_image_edit``editor_background_removal``editor_icon_spritesheet_generation``editor_ui_design_asset_extraction``editor_character_animation_generation``editor_video_generation``editor_sound_effect_generation``editor_background_music_generation` |
| `owner_user_id` | 触发用户 |
| `source_module` | 玩法或能力名,例如 `puzzle` |
| `source_entity_id` | session/profile/work 等作用域 |
| `request_label` | 排障标签 |
| `request_payload_json` | worker 执行入参 JSON |
| `status` | `pending/running/completed/failed/cancelled` |
| `attempt` / `max_attempts` | 当前尝试次数与最大尝试次数 |
| `last_error_message` | 最近失败原因 |
| `worker_id` | 当前 lease owner |
| `lease_expires_at` | lease 到期时间 |
| `lease_token` | 本次 claim 的 fencing token,用于阻止过期 worker 回写 |
| `available_at` | 下次可领取时间 |
| `result_payload_json` | 完成摘要 |
| `created_at/started_at/completed_at/updated_at` | 审计时间 |
| `price_mud_points` | 后端计算的本任务价格,用于任务列表展示和排障 |
| `refund_ledger_id` | 失败退款产生的钱包退款流水 ID,便于从任务追到退款记录 |
| `notification_acknowledged_at` | 用户已确认完成 / 失败提示的时间,未确认终态任务下次登录继续集中弹出 |
| `phase` | 尾部可选字段;`null / generating / processing`claim 时写 `generating`,进入正式后处理时写 `processing` |
| 字段 | 说明 |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `job_id` | 主键,`extgen-` 前缀 UUID |
| `dedupe_key` | 唯一键,建议为 `play/action/session/scope` |
| `job_kind` | 执行类型,当前覆盖 `puzzle_compile_draft``puzzle_generate_images``puzzle_generate_ui_background`、跳一跳 / 拼消消 / 敲木鱼生成动作,以及 `editor_image_generation``editor_image_edit``editor_background_removal``editor_icon_spritesheet_generation``editor_ui_design_asset_extraction``editor_character_animation_generation``editor_video_generation``editor_sound_effect_generation``editor_background_music_generation` |
| `owner_user_id` | 触发用户 |
| `source_module` | 玩法或能力名,例如 `puzzle` |
| `source_entity_id` | session/profile/work 等作用域 |
| `request_label` | 排障标签 |
| `request_payload_json` | worker 执行入参 JSON |
| `status` | `pending/running/completed/failed/cancelled` |
| `attempt` / `max_attempts` | 当前尝试次数与最大尝试次数 |
| `last_error_message` | 最近失败原因 |
| `worker_id` | 当前 lease owner |
| `lease_expires_at` | lease 到期时间 |
| `lease_token` | 本次 claim 的 fencing token,用于阻止过期 worker 回写 |
| `available_at` | 下次可领取时间 |
| `result_payload_json` | 完成摘要 |
| `created_at/started_at/completed_at/updated_at` | 审计时间 |
| `price_mud_points` | 后端计算的本任务价格,用于任务列表展示和排障 |
| `refund_ledger_id` | 失败退款产生的钱包退款流水 ID,便于从任务追到退款记录 |
| `notification_acknowledged_at` | 用户已确认完成 / 失败提示的时间,未确认终态任务下次登录继续集中弹出 |
| `phase` | 尾部可选字段;`null / generating / processing`claim 时写 `generating`,进入正式后处理时写 `processing` |
用户正式读取使用私有轻量投影 `external_generation_job_summary`。该表同步保存 owner、来源、状态、`phase`、价格、有限错误/告警摘要、通知确认和时间字段,不复制 request/result payload、worker lease 或 dedupe 内部字段;enqueue、claim、renew、phase update、complete、fail 与 acknowledge 都必须维护对应投影语义。
@@ -30,17 +30,17 @@ preview gateway / 独立 preview origin
## 资产与威胁
| 资产 | 主要威胁 | MVP 缓解 |
| --- | --- | --- |
| 主站 access token / cookie | 预览代码同源读取、XSS 窃取 | 独立 preview originiframe 不带主站 cookie;预览页不能访问主站 storage |
| 用户 Web 工程源码 | 跨租户读取、snapshot 枚举 | project / owner 校验;snapshotId 不可枚举;preview token 绑定 owner / project / snapshot |
| preview artifact | 路径穿越、MIME 错误、旧 token 访问 | preview gateway 校验 token;禁止 `..`;按白名单 MIME 服务;短期 token 可撤销 |
| runner 临时工作区 | 逃逸到宿主源码、读取密钥 | 独立临时目录或容器;非 root;无宿主源码挂载;任务结束销毁 |
| 依赖缓存 | 缓存污染、恶意 postinstall | MVP 固定依赖;禁用 scripts;缓存 key 包含模板、Node 版本和 lock digest |
| api-server / SpacetimeDB | runner 横向访问内部服务 | runner 默认无内网访问;阻断 api-server 管理端口、SpacetimeDB 和生产数据库 |
| OSS / artifact store | 越权读写、签名 URL 泄露 | runner 只拿短期只读资产签名或受控写 artifact 能力;日志脱敏 |
| 构建日志 | 泄露环境变量、宿主路径、签名 URL | 日志限长、脱敏、错误摘要化;不回显平台密钥 |
| 用户浏览器 | 弹窗逃逸、下载、剪贴板、摄像头、Service Worker 常驻 | iframe sandboxCSP;禁用 Service Worker;不授权敏感能力 |
| 资产 | 主要威胁 | MVP 缓解 |
| -------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| 主站 access token / cookie | 预览代码同源读取、XSS 窃取 | 独立 preview originiframe 不带主站 cookie;预览页不能访问主站 storage |
| 用户 Web 工程源码 | 跨租户读取、snapshot 枚举 | project / owner 校验;snapshotId 不可枚举;preview token 绑定 owner / project / snapshot |
| preview artifact | 路径穿越、MIME 错误、旧 token 访问 | preview gateway 校验 token;禁止 `..`;按白名单 MIME 服务;短期 token 可撤销 |
| runner 临时工作区 | 逃逸到宿主源码、读取密钥 | 独立临时目录或容器;非 root;无宿主源码挂载;任务结束销毁 |
| 依赖缓存 | 缓存污染、恶意 postinstall | MVP 固定依赖;禁用 scripts;缓存 key 包含模板、Node 版本和 lock digest |
| api-server / SpacetimeDB | runner 横向访问内部服务 | runner 默认无内网访问;阻断 api-server 管理端口、SpacetimeDB 和生产数据库 |
| OSS / artifact store | 越权读写、签名 URL 泄露 | runner 只拿短期只读资产签名或受控写 artifact 能力;日志脱敏 |
| 构建日志 | 泄露环境变量、宿主路径、签名 URL | 日志限长、脱敏、错误摘要化;不回显平台密钥 |
| 用户浏览器 | 弹窗逃逸、下载、剪贴板、摄像头、Service Worker 常驻 | iframe sandboxCSP;禁用 Service Worker;不授权敏感能力 |
## Runner 限制
@@ -472,70 +472,70 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清
## 环境变量
| 变量 | 默认值 | 说明 |
| ------------------------------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `GENARRATIVE_PINGORA_GATEWAY_LISTEN` | `127.0.0.1:18081` | Pingora 监听地址。 |
| `GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN` | 空 | 可选 HTTPS 监听地址;启用时必须同时设置 `TLS_CERT_FILE``TLS_KEY_FILE`。 |
| `GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE` | 空 | 可选 HTTPS 证书链文件;必须是网关运行用户可读取的文件。Certbot 证书建议先同步到 `/etc/genarrative/pingora-tls/<域名>/fullchain.pem`。 |
| `GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE` | 空 | 可选 HTTPS 私钥文件;必须是网关运行用户可读取的文件。Certbot 私钥建议先同步到 `/etc/genarrative/pingora-tls/<域名>/privkey.pem`。 |
| `GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN` | 空 | 可选 HTTP 重定向监听地址;启用时必须已配置 TLS 入口,ACME challenge 仍静态读取。 |
| `GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_TARGET_SCHEME` | `https` | HTTP 重定向目标 scheme,当前只允许 `https`。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM` | `127.0.0.1:8082` | `api-server` 上游地址。 |
| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM` | `127.0.0.1:3101` | SpacetimeDB 上游地址。 |
| `GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS` | 空 | 可选 Gitea Host 白名单,逗号分隔;匹配时整站代理到 Gitea。 |
| `GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM` | 空 | 可选 Gitea 上游;配置 Gitea Host 时必须同时设置。 |
| `GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT` | `/srv/genarrative/web` | 前端静态文件根目录。 |
| `GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT` | `/var/www/html` | ACME challenge 静态目录。 |
| `GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_FILE` | `/var/lib/genarrative/maintenance/enabled` | 存在即进入维护模式。 |
| `GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO` | `http` | 写入 `X-Forwarded-Proto` 的值。 |
| `GENARRATIVE_PINGORA_GATEWAY_MAX_API_BODY_BYTES` | `67108864` | `/api` 通用路由的 `Content-Length` 上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS` | `gzip` | 当前唯一允许的压缩算法白名单;Pingora 正式化口径固定为 gzip-onlyBrotli 继续由 Nginx / 前置代理承担。 |
| `GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED` | `true` | 是否启用 gzip 响应压缩。 |
| `GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL` | `5` | gzip 压缩等级,必须在 `0..=9`。 |
| `GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES` | `1024` | gzip 最小响应长度,默认对齐 Nginx `gzip_min_length 1024`,必须大于 `0`。 |
| `GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL` | `no-cache` | HTML、目录 index 和 SPA fallback 的缓存头,避免入口 HTML 被长期缓存。 |
| `GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL` | `public, max-age=31536000, immutable` | `/assets/*``/admin/assets/*` 中带 Vite 指纹文件名的静态资源缓存头。 |
| `GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL` | `no-cache` | 非指纹静态资源和 ACME challenge 的默认缓存头。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_CONNECT_TIMEOUT_MS` | `3000` | 连接上游的超时,必须大于 `0`。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_DEFAULT_READ_TIMEOUT_SECONDS` | `60` | 没有 Nginx 显式长超时的代理路由读取超时,必须大于 `0`。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_API_READ_TIMEOUT_SECONDS` | `3600` | 通用 `/api` 路由读取超时,对齐当前 Nginx `proxy_read_timeout 3600s`。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_LONG_READ_TIMEOUT_SECONDS` | `3600` | SpacetimeDB subscribe 长连接读取超时。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_WRITE_TIMEOUT_SECONDS` | `3600` | 写上游请求头 / 请求体超时,对齐当前 Nginx `proxy_send_timeout 3600s` 口径。 |
| 变量 | 默认值 | 说明 |
| ------------------------------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `GENARRATIVE_PINGORA_GATEWAY_LISTEN` | `127.0.0.1:18081` | Pingora 监听地址。 |
| `GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN` | 空 | 可选 HTTPS 监听地址;启用时必须同时设置 `TLS_CERT_FILE``TLS_KEY_FILE` |
| `GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE` | 空 | 可选 HTTPS 证书链文件;必须是网关运行用户可读取的文件。Certbot 证书建议先同步到 `/etc/genarrative/pingora-tls/<域名>/fullchain.pem` |
| `GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE` | 空 | 可选 HTTPS 私钥文件;必须是网关运行用户可读取的文件。Certbot 私钥建议先同步到 `/etc/genarrative/pingora-tls/<域名>/privkey.pem` |
| `GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN` | 空 | 可选 HTTP 重定向监听地址;启用时必须已配置 TLS 入口,ACME challenge 仍静态读取。 |
| `GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_TARGET_SCHEME` | `https` | HTTP 重定向目标 scheme,当前只允许 `https` |
| `GENARRATIVE_PINGORA_GATEWAY_API_UPSTREAM` | `127.0.0.1:8082` | `api-server` 上游地址。 |
| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_UPSTREAM` | `127.0.0.1:3101` | SpacetimeDB 上游地址。 |
| `GENARRATIVE_PINGORA_GATEWAY_GITEA_HOSTS` | 空 | 可选 Gitea Host 白名单,逗号分隔;匹配时整站代理到 Gitea。 |
| `GENARRATIVE_PINGORA_GATEWAY_GITEA_UPSTREAM` | 空 | 可选 Gitea 上游;配置 Gitea Host 时必须同时设置。 |
| `GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT` | `/srv/genarrative/web` | 前端静态文件根目录。 |
| `GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT` | `/var/www/html` | ACME challenge 静态目录。 |
| `GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_FILE` | `/var/lib/genarrative/maintenance/enabled` | 存在即进入维护模式。 |
| `GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO` | `http` | 写入 `X-Forwarded-Proto` 的值。 |
| `GENARRATIVE_PINGORA_GATEWAY_MAX_API_BODY_BYTES` | `67108864` | `/api` 通用路由的 `Content-Length` 上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_COMPRESSION_ALGORITHMS` | `gzip` | 当前唯一允许的压缩算法白名单;Pingora 正式化口径固定为 gzip-onlyBrotli 继续由 Nginx / 前置代理承担。 |
| `GENARRATIVE_PINGORA_GATEWAY_GZIP_ENABLED` | `true` | 是否启用 gzip 响应压缩。 |
| `GENARRATIVE_PINGORA_GATEWAY_GZIP_LEVEL` | `5` | gzip 压缩等级,必须在 `0..=9` |
| `GENARRATIVE_PINGORA_GATEWAY_GZIP_MIN_LENGTH_BYTES` | `1024` | gzip 最小响应长度,默认对齐 Nginx `gzip_min_length 1024`,必须大于 `0` |
| `GENARRATIVE_PINGORA_GATEWAY_HTML_CACHE_CONTROL` | `no-cache` | HTML、目录 index 和 SPA fallback 的缓存头,避免入口 HTML 被长期缓存。 |
| `GENARRATIVE_PINGORA_GATEWAY_ASSET_CACHE_CONTROL` | `public, max-age=31536000, immutable` | `/assets/*``/admin/assets/*` 中带 Vite 指纹文件名的静态资源缓存头。 |
| `GENARRATIVE_PINGORA_GATEWAY_STATIC_CACHE_CONTROL` | `no-cache` | 非指纹静态资源和 ACME challenge 的默认缓存头。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_CONNECT_TIMEOUT_MS` | `3000` | 连接上游的超时,必须大于 `0` |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_DEFAULT_READ_TIMEOUT_SECONDS` | `60` | 没有 Nginx 显式长超时的代理路由读取超时,必须大于 `0` |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_API_READ_TIMEOUT_SECONDS` | `3600` | 通用 `/api` 路由读取超时,对齐当前 Nginx `proxy_read_timeout 3600s` |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_LONG_READ_TIMEOUT_SECONDS` | `3600` | SpacetimeDB subscribe 长连接读取超时。 |
| `GENARRATIVE_PINGORA_GATEWAY_UPSTREAM_WRITE_TIMEOUT_SECONDS` | `3600` | 写上游请求头 / 请求体超时,对齐当前 Nginx `proxy_send_timeout 3600s` 口径。 |
| `GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR` | `false` | 是否用 `X-Forwarded-For` 首个 IP 作为接流保护 client key;公网直连 Pingora 时必须保持 `false`direct preflight 会阻断公网监听误开启。 |
| `GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED` | `false` | 开启 `TRUST_X_FORWARDED_FOR` 时必须显式设为 `true`,表示前置代理会清洗 `X-Forwarded-For`。 |
| `GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED` | `true` | 是否启用单进程接流保护。 |
| `GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT` | `1` | 当前接流保护覆盖的 Pingora 实例数;必须是正整数。开启网关保护且大于 `1` 时必须确认共享保护层。 |
| `GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED` | `false` | 多实例仍启用网关保护时必须显式设为 `true`,表示已落地共享限流 / 共享并发保护层。 |
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_MAX_CONCURRENT` | `64` | `/admin/api/*` 每 client 并发上限;`0` 表示不限制并发。 |
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_RATE_PER_SECOND` | `30` | `/admin/api/*` 每 client token bucket 回填速率;`0` 表示不限制 RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_BURST` | `16` | `/admin/api/*` 每 client 额外 burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_MAX_CONCURRENT` | `64` | 通用 `/api` 路由每 client 并发上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_RATE_PER_SECOND` | `300` | 通用 `/api` 路由每 client RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_BURST` | `64` | 通用 `/api` 路由每 client burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_MAX_CONCURRENT` | `256` | SpacetimeDB 公开最小路由每 client 并发上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_RATE_PER_SECOND` | `1000` | SpacetimeDB 公开最小路由每 client RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_BURST` | `256` | SpacetimeDB 公开最小路由每 client burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN` | 空 | 内部 shadow 探针 token;为空时探针端点关闭。 |
| `GENARRATIVE_PINGORA_GATEWAY_LOG` | `info,pingora=info,pingora_gateway=info` | tracing 过滤器。 |
| `GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE` | 空 | 可选 access log 文件路径;生产 shadow 示例使用 `/var/log/genarrative/pingora-gateway.access.log`。 |
| `GENARRATIVE_PINGORA_GATEWAY_OTEL_ENABLED` | `false` | 是否启用共享 OpenTelemetry 初始化。 |
| `GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED` | `false` | 开启 `TRUST_X_FORWARDED_FOR` 时必须显式设为 `true`,表示前置代理会清洗 `X-Forwarded-For` |
| `GENARRATIVE_PINGORA_GATEWAY_PROTECTION_ENABLED` | `true` | 是否启用单进程接流保护。 |
| `GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT` | `1` | 当前接流保护覆盖的 Pingora 实例数;必须是正整数。开启网关保护且大于 `1` 时必须确认共享保护层。 |
| `GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED` | `false` | 多实例仍启用网关保护时必须显式设为 `true`,表示已落地共享限流 / 共享并发保护层。 |
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_MAX_CONCURRENT` | `64` | `/admin/api/*` 每 client 并发上限;`0` 表示不限制并发。 |
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_RATE_PER_SECOND` | `30` | `/admin/api/*` 每 client token bucket 回填速率;`0` 表示不限制 RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_ADMIN_API_BURST` | `16` | `/admin/api/*` 每 client 额外 burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_MAX_CONCURRENT` | `64` | 通用 `/api` 路由每 client 并发上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_RATE_PER_SECOND` | `300` | 通用 `/api` 路由每 client RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_API_BURST` | `64` | 通用 `/api` 路由每 client burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_MAX_CONCURRENT` | `256` | SpacetimeDB 公开最小路由每 client 并发上限。 |
| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_RATE_PER_SECOND` | `1000` | SpacetimeDB 公开最小路由每 client RPS。 |
| `GENARRATIVE_PINGORA_GATEWAY_SPACETIME_BURST` | `256` | SpacetimeDB 公开最小路由每 client burst。 |
| `GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN` | 空 | 内部 shadow 探针 token;为空时探针端点关闭。 |
| `GENARRATIVE_PINGORA_GATEWAY_LOG` | `info,pingora=info,pingora_gateway=info` | tracing 过滤器。 |
| `GENARRATIVE_PINGORA_GATEWAY_ACCESS_LOG_FILE` | 空 | 可选 access log 文件路径;生产 shadow 示例使用 `/var/log/genarrative/pingora-gateway.access.log` |
| `GENARRATIVE_PINGORA_GATEWAY_OTEL_ENABLED` | `false` | 是否启用共享 OpenTelemetry 初始化。 |
## 当前路由口径
| 路由 | 行为 |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `/.well-known/acme-challenge/*` | 从 `GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT` 精确读取静态文件,默认 `Cache-Control: no-cache`,并带 `ETag` / `Last-Modified` / `Accept-Ranges: bytes`。 |
| `/admin` | 301 到 `/admin/` |
| `/admin/api/*` | 转发到 `api-server` |
| `/admin/assets/*` | 从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| `/admin/*` | 先读取静态文件或目录 index,失败回退 `/admin/index.html`HTML 默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| `/assets/*` | 从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| `/api``/api/*` | 转发到 `api-server`,按配置执行 `Content-Length` 与流式 body 累计上限检查。 |
| `/v1/database/{db}/subscribe``/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 |
| `/__genarrative_pingora/healthz` | 仅在携带 `X-Genarrative-Pingora-Probe` 且匹配配置 token 时返回 shadow JSON,否则 404。 |
| `/v1/*``/generated-*``/healthz*``/readyz*` | 返回 404,保持生产公网不暴露口径。 |
| 主站 SPA allowlist | 只对 `/``/creation``/project``/profile``/editor/canvas` 失败回退 `/index.html`;匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache`。 |
| 其它 Web 路径 | 只读取真实静态文件或目录 index,缺失时返回真实 404;`/creation/not-exist``/runtime/not-exist``/puzzle/not-exist` 不进入 SPA fallback。 |
| 路由 | 行为 |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/.well-known/acme-challenge/*` | 从 `GENARRATIVE_PINGORA_GATEWAY_ACME_ROOT` 精确读取静态文件,默认 `Cache-Control: no-cache`,并带 `ETag` / `Last-Modified` / `Accept-Ranges: bytes` |
| `/admin` | 301 到 `/admin/` |
| `/admin/api/*` | 转发到 `api-server` |
| `/admin/assets/*` | 从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| `/admin/*` | 先读取静态文件或目录 index,失败回退 `/admin/index.html`HTML 默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416` |
| `/assets/*` | 从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
| `/api``/api/*` | 转发到 `api-server`,按配置执行 `Content-Length` 与流式 body 累计上限检查。 |
| `/v1/database/{db}/subscribe``/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 |
| `/__genarrative_pingora/healthz` | 仅在携带 `X-Genarrative-Pingora-Probe` 且匹配配置 token 时返回 shadow JSON,否则 404。 |
| `/v1/*``/generated-*``/healthz*``/readyz*` | 返回 404,保持生产公网不暴露口径。 |
| 主站 SPA allowlist | 只对 `/``/creation``/project``/profile``/editor/canvas` 失败回退 `/index.html`;匹配大小写不敏感并允许一个尾部斜杠,HTML 默认 `no-cache` |
| 其它 Web 路径 | 只读取真实静态文件或目录 index,缺失时返回真实 404;`/creation/not-exist``/runtime/not-exist``/puzzle/not-exist` 不进入 SPA fallback。 |
维护模式下,公网 API-like 路由返回 JSON `503`;公网 Web 静态路由先读取 `GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_PAGE_FILE` 指向的 release 外运行态公告,缺失时回退 `GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT/maintenance.html`,两者都不存在时返回纯文本 `503`。版本化默认页不得包含日期或具体时段,临时公告由 `maintenance-on.sh --page-file` 安装并在 `maintenance-off.sh` 时清理。IPv4 loopback / RFC1918 / link-local 和 IPv6 loopback / ULA / link-local 来源绕过整站维护闸,主站页面与静态资源、普通 API、后台页面与后台 API、SpacetimeDB 路由均按非维护状态继续处理;应用层登录、管理员鉴权和其它业务鉴权保持不变。Pingora 直连按 TCP peer 判定来源;仅当 peer 是 loopback 的同机 Nginx 时才接受 Nginx 强制覆盖的 `X-Real-IP`,绝不使用客户端可伪造的 `X-Forwarded-For` 做维护放行。该放行只绕过网关维护响应;若 `pause-after-stdb` 已停止 api-server,内网普通 API 和后台 API 仍不可用。
代理失败时,API / SpacetimeDB 等代理路由返回统一 JSON 网关错误;本地静态路由仍保持对应 HTTP 错误状态。
@@ -1087,7 +1087,6 @@ Runtime 只在以下客观条件同时满足时写 `contractStatus=evidence-read
### 单层 repair
repair 深度固定为 `1`;同一原 delivery 同时最多存在一个非 `suppressed` repair。相同 durable action/身份重放必须幂等复用已预留或已创建的 repair;不同 action 的重复或并发竞争必须在 delivery 锁内发现既有非 suppressed repair 后拒绝,不能创建第二个活跃目标 run、第二份可认领回执或 `-dup-*` repair。repair 结果继续唤醒、认领并收束到原可信父 Session/run;它不能创建第二条面向用户的 assistant。repair 再次 `needs-repair` 时不得继续嵌套委派,当前可信父 Run 只能基于现有证据裁决或由根 Supervisor 走用户输入门禁。`suppressed` repair 不视为已完成返工,原 `repairRequired` 门禁必须继续阻断 finalization;同一 durable action 可以在无终态字段时把原 delivery 恢复为 `dispatched`,若该 action 已持久失败,新 action 也只可在既有 repair 全部 suppressed 时创建替代的基础设施投递,不能形成第二轮语义返工。已 suppressed 且未形成 child task 的旧 delivery 不再参与 capability、claim 或 completion barrier 的身份验证,避免恢复入口被失败前置记录永久堵死;所有非 suppressed delivery 仍必须逐条通过完整可信链校验。
当前可信父 Run 认领回执后必须能够再次从 durable delivery 取回权威返工合同,不能依赖首次 `agent.run_status` observation 或模型记忆。普通 `agent.run_status` 要返回有界的 `claimedDelegateContracts` 目录,至少包含 `delegationId / targetAgentId / repairOfDelegationId / contractStatus / acceptanceCriteriaCount / expectedArtifactsCount`;带可选 `delegationId` 查询时,只允许当前可信父 Run 读取属于自己且已 `claimed-by-parent` 的 delivery,并返回未截断的 `delegationId / targetAgentId / acceptanceCriteria / expectedArtifacts / repairOfDelegationId / deliveryStatus / terminalStatus / contractStatus`。该查询是只读、可幂等重放的私有 observation,不返回 task 正文、Provider payload、凭据或绝对路径;合同超过明确有界输出上限时失败关闭,不能截断后让模型猜测。返工被“合同未完整继承”拒绝时,失败 observation 必须携带同一 durable delivery 的完整 `claimedDelegateContract` 权威快照,当前可信父 Run 可直接逐字段据此修正;该字段缺失或身份不确定时才必须按原 `delegationId` 重读,不得重复无目标地轮询状态或从 action history 的摘要反推。
@@ -184,16 +184,13 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
- 增量预览刷新:Tauri 客户端记录当前 iframe 已展示的 validated revision;同一当前 run 后续成功 `preview.validate` 的 revision 严格高于已展示 revision 时,只在原 Tauri preview server 和原 loopback origin 上刷新 iframe,不得再次调用 `preview.start`、新增 server 或切换到 Runner registry。相同或更低 revision 不触发刷新。preview HTTP server 对 HTML、脚本、样式、资源和错误响应统一发送 `Cache-Control: no-store`iframe 刷新必须读取新 revision,不能继续命中 WebView 缓存中的旧版本。自动预览轮询回归的等待上限必须严格大于生产 `1000ms` 轮询间隔,不得使用同为 `1000ms` 的默认上限制造 CI 边界竞争。
- 系统边界:该页面是既有 AI 游戏创作工作台的独立构建例外,不新增平台玩法入口、后端 API、会话库、Runner 或预览服务,也不把入口并回普通正式客户端。原 `supervisor-chat` 继续固定使用 `standard` profile 并保持纯聊天行为,不继承本例外的自主构建、事件聚合或自动预览授权。
- 对话输出中的 `eventId + publicText` 只指需要独立进入聊天的进度事件;`turn.started` 和根 Run 终态失败事件由上一条 `runtime-public-status-*` 硬门覆盖,不得同时转成事件消息。专业 Agent child 的失败消息继续留在其 Agent Session,根项目聊天只接收 Supervisor 终态失败、明确公开进度和安全 final-reply,避免一项失败被 Runtime event 与 conversation 各播报一次。
- 验证:前端运行时模型定向测试、Rust completion/source/asset 合同测试、`cargo fmt --check``npm run check:encoding``git diff --check` 必须全部执行;Windows 文件锁竞态只可作为既有测试失败单独记录,不得将其改写为本次改动的通过证据。
- Provider 次数:Supervisor 先用一个 Provider turn 理解目标并持久化条件路由;随后唯一 `code-prototype` 主 Agent 先 `asset.list`,只有审计的精确缺口才建立相应的受限美术委派。完整复用不得调用图片生成接口;图集复用或主 Agent 认领生成回执后,仍由该主 Agent 接入、执行 `game.static_smoke` 和 desktop/mobile `preview.validate`。软预算耗尽时只允许使用已登记图集和当前 resourceId 切片清单的确定性本地兜底,不得由 Runtime 或关键词强制生成图片。
- 可玩兜底:软预算或首版 Provider 无法及时完成时,只能为已显式实现真实语义的玩法生成完整、自包含、无远程运行依赖的中文 HTML 模板;未知玩法失败关闭,不能只替换标题后套用固定收集游戏。俄罗斯方块模板必须包含 10×20 棋盘、下落、移动、旋转、锁定、消行和触顶失败;收集模板只匹配明确收集类目标。模板必须从 `ready` 开始,包含真实 Canvas 绘制、`requestAnimationFrame`、键盘 / 触控主要操作、唯一可见且启用的 start / primary-action / restart 控件,状态 JSON 只随真实输入、状态迁移或模拟状态变化推进,并能在 primary-action 后保持 `playing`、在 restart 后稳定回到 `ready | playing`;不得在开始前固定进入 `lost`,不得通过固定失败冒充试玩通过,也不得由纯渲染帧空转 `sequence`。兜底只允许写入缺失或精确初始化占位的 `game/index.html`;存在非占位入口时,当前 `code-prototype` 必须读取并实际 patch,取得本人 `mutationRevision` 后再静态检查和试玩,不得反复用只读 smoke 冒充续作。
- 关联验收:快车道必须分别验证 Supervisor 决策前零 child、持久路由后只启动 `code-prototype`、主 Agent 成功 `asset.list` 后才可判断缺口、完整覆盖零图片生成/零委派、精确缺口只委派对应 owner、整体重做仍先审计且不产生无关委派、美术 child 对 `game/**` 写入拒绝而 `assets/**` 允许、回执恢复同一主 Run、主 Agent 自行完成接入/静态 smoke/desktop-mobile 试玩、4200 / 4500 秒累计预算、规范图到 icon-spritesheet 的真实引用、`iconImageSrcs` 本地持久化与资源 ID 绑定、失败续跑目标继承、非占位入口禁止整文件覆盖、纯代码核心画面、猜测单个 atlas 裁切与整图展示失败、四类独立切片可见使用通过与 action-driven `sequence`。完整 GUI / CLI 的固定 16 节点 DAG 另行保持原有回归。
- 开发态启动必须在 Tauri CLI 之前解析并预检 AGC Vite 最终地址。Linux 使用系统级用户端口段的 `start + 5` 槽位并只在本段内漂移,Windows / macOS 以 `3080` 为兼容首选;启动器通过 `GENARRATIVE_AGC_VITE_PORT` 绑定 `beforeDevCommand` 和配套后端预留,通过 Tauri CLI `--config` 绑定 `build.devUrl`,并通过 Vite CLI `--port` 绑定 `strictPort` 监听。任何竞态中已存在的 AGC Vite、非 HTTP 监听器或其它服务都必须在原生窗口创建前失败关闭。启动器不擅自终止无法证明归属的旧服务,也不得把当前 Rust 壳 / Runner 与其它 worktree 的旧 Vite 前端混用。Tauri CLI 任意退出后,外层启动器必须有界收束已启动的客户端进程树,避免 `beforeDevCommand` 失败后留下假在线窗口。
- source-aware lane 的主 Run 或其经授权美术 child 可能在 UI hydration 写回时短暂恢复为 `Pending`。该例外必须从当前 root source、持久工作流决策、单主 route 与 child delegation 解析本轮已开放工作,不得从旧七节点图硬编码重启 Director、验证或试玩节点;未授权 child、第二个活跃美术 child,或缺少成功 `asset.list` 审计的美术委派仍严格失败关闭。
@@ -1087,7 +1084,6 @@ game-project/
- 浏览器未发现、临时环境不可建、启动超时或在 WebSocket URL 解析前退出统一分类为 `preview-infrastructure-unavailable`。首个持久 observation 后收束当前 action batch并失败结束 child/root run,禁止继续用 Provider 逐轮规划同一 revision 的重复启动;普通页面/玩法验收失败仍保留为业务失败,不混入基础设施分类。
- 规范 Agent 默认推理档覆盖全部 21 个角色:核心规划、生成、设计/美术/代码原型和质量角色使用 `high`,协调与结构化交付使用 `medium`,确定性预览 gate、音频总监和发布策略使用 `low`;显式 `agentLlm.<id>.reasoningEffort` 始终最高优先。规范默认由 Runtime resolver 解析,模板与 GUI 初始草稿保持 `agentLlm` 为空,避免默认值被误判成角色独立 LLM 路由;GUI 必须显示每个角色的实际默认档。全局与逐 Agent status/CLI 必须同时显示实际解析后的 reasoning、request timeout、max retries 和 retry backoff,区分运行快照与后来配置。
- code-prototype 的确定性交付需要同时满足当前 Run 存在 `status=ok` 的真实 mutation action、本人当前 mutation revision 的 `game.static_smoke=passed` 与完整 autonomous completion gate 无阻塞;失败 patch 即使因保守失效旧凭证而推进 revision,也不能取得 mutation ownership。mutation ownership 使用最后一条同工具调用,并严格匹配当前 Agent/task/session/run/actionId/actionFingerprint/tool 的 durable receiptpending action 的 `plannedSteerCursor` 必须进入 recent tool-call 与 receipt 的同一 fingerprint,不能因非零 steer cursor 把真实成功误判为外来动作。static smoke 通过但仍有素材或正式产物缺口时,Runtime 对尚未完成的计划用单一 in-progress repair 替换首个非终态步骤并保留后续 pending;只有计划全 completed 且未满 8 步时才追加 repair。下一轮交还 Provider 生成实际 `file.patch`,完整门未通过时禁止自动完成计划,也不得因 retained completed 步骤与 steer 新计划合并超限而进入空转;结构化计划只要已经包含不可改写的 failed 步骤,就在快车道入口明确失败关闭,不再依赖 mutation ownership 或完成门诊断是否仍存在。
- durable active child 的权威性高于 stale Completed manifest 快照,但 Failed manifest 仍立即失败关闭。所有 ready child 的文件、patchset、命令产物和其它项目 mutation 在写锁内再次核对当前根 Run;新根 Run 建立后旧 child 只能安全收束/审计,不能再修改项目或推进 revision。
- 大 classic 游戏脚本的函数可达性查询必须对固定 invocation graph 使用整轮 visited,每个 function node 最多访问一次;递归栈只负责去环、返回时删除节点会在 render/update 扇入图中指数回溯并阻塞 Runner 事件循环。Canvas alias 全空历史、稳定祖先初始化、整画布尺寸引用和可证明的 `COLS × ROWS × CELL` 格子目标使用有界静态路径,普通未知动态坐标继续失败关闭。
@@ -87,11 +87,11 @@ struct DirectCodexTurnAttachment {
一个函数 `render_direct_codex_user_prompt(prompt, attachments) -> Result<String, String>`
| 输入 | 输出 |
|---|---|
| 无附件 | `prompt.trim()`;若也空则 `Err("聊天内容不能为空")` |
| 附件都没有 `localPath` 且都没有 `status` | 保持现有 Home 文案与行格式,测试须逐字兼容 |
| 任一条有 `localPath``status` | Project 头 + Project 行格式 |
| 输入 | 输出 |
| ---------------------------------------- | --------------------------------------------------- |
| 无附件 | `prompt.trim()`;若也空则 `Err("聊天内容不能为空")` |
| 附件都没有 `localPath` 且都没有 `status` | 保持现有 Home 文案与行格式,测试须逐字兼容 |
| 任一条有 `localPath``status` | Project 头 + Project 行格式 |
Home 行(禁止改字):
@@ -51,13 +51,13 @@ Codex item/completed
项目里现有:
| 产物 | 记下的 | 缺的 |
|---|---|---|
| `.agent/conversations/project.jsonl` | 用户原文 + 助手终稿 | sidecar、工具调用 |
| `.agent/agent.db` | init / upload / 美术登记 / 对话指针 | native 读、MCP 调用、`agc_write_file` |
| `.agent/logs/command.log` | 权限确认 | 原生命令 |
| `asset.register` / `canvas.asset_generate` | 路径、切片、部分 `source.prompt` | 与读附件的先后 |
| 隔离 `CODEX_HOME` | Codex 自己的 session | 回合结束即删 |
| 产物 | 记下的 | 缺的 |
| ------------------------------------------ | ----------------------------------- | ------------------------------------- |
| `.agent/conversations/project.jsonl` | 用户原文 + 助手终稿 | sidecar、工具调用 |
| `.agent/agent.db` | init / upload / 美术登记 / 对话指针 | native 读、MCP 调用、`agc_write_file` |
| `.agent/logs/command.log` | 权限确认 | 原生命令 |
| `asset.register` / `canvas.asset_generate` | 路径、切片、部分 `source.prompt` | 与读附件的先后 |
| 隔离 `CODEX_HOME` | Codex 自己的 session | 回合结束即删 |
Codex app-server 协议里,`commandExecution.commandActions` 已分类为 `Read | ListFiles | Search | Unknown``Read.path` 在协议侧会拼成 cwd 绝对路径。Direct cwd 就是项目根(`resolve_direct_codex_project_authority` 不再强制 `game/` 子目录)。抽取时把绝对路径收回项目相对 POSIX,失败则丢路径,不写宿主绝对路径。
@@ -67,12 +67,12 @@ Codex app-server 协议里,`commandExecution.commandActions` 已分类为 `Rea
落地后,对类似 `gameagent-9baa5293` 的 run,应能三分:
| 时间线 | 结论 | 下一刀不该打哪 |
|---|---|---|
| `offeredRead.read=false``firstDesign` 已是 `taonier_prepare_game_art` 且 brief 是收集类 | 没打开附件就定了玩法 | 不是「GDD 解析不够」 |
| 先 `Read` 且 hash 对上,brief 仍是收集类 | 读了但没用 | sidecar 已够;看四切片 / icon-spec「收集物」/ 完成合同 |
| 只有 `ListFiles` / `Search` 命中 uploads,没有 `Read` | 发现了没读正文 | 映射可能够,缺的是读 |
| `Read` 的 path 是 `fast_gdd.md` 而不是 `assets/uploads/…` | sidecar 没被当成磁盘路径 | 还是路径合同 |
| 时间线 | 结论 | 下一刀不该打哪 |
| ----------------------------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------ |
| `offeredRead.read=false``firstDesign` 已是 `taonier_prepare_game_art` 且 brief 是收集类 | 没打开附件就定了玩法 | 不是「GDD 解析不够」 |
| 先 `Read` 且 hash 对上,brief 仍是收集类 | 读了但没用 | sidecar 已够;看四切片 / icon-spec「收集物」/ 完成合同 |
| 只有 `ListFiles` / `Search` 命中 uploads,没有 `Read` | 发现了没读正文 | 映射可能够,缺的是读 |
| `Read` 的 path 是 `fast_gdd.md` 而不是 `assets/uploads/…` | sidecar 没被当成磁盘路径 | 还是路径合同 |
不在账本里写「已遵循 GDD」或「未遵循 GDD」布尔。
@@ -148,16 +148,16 @@ camelCase JSON。禁止出现附件正文、命令 stdout、patch diff、宿主
按类型附加字段:
| `item.type` | 追加 | 禁止 |
|---|---|---|
| `commandExecution` | `command` 截断 240 字;`exitCode``durationMs``actions[]` | `aggregatedOutput` |
| `mcpToolCall` | `tool``server`(可省略默认 `agc_tools`)、`durationMs`、§5.4 参数 | `result``error` 原文(只留 `status` / `errorKind` |
| `fileChange` | `changes: [{ path, kind }]``kind``add` / `delete` / `update` | `diff``movePath` 的宿主绝对路径(相对化失败则整条 change 丢 path |
| `imageView` | `path` | 图像字节 |
| `functionCallOutput` | `name``namespace` | `output` |
| `webSearch` | `query` 截断 400 字 | 结果页正文 |
| `agentMessage` / `userMessage` / `plan` / `reasoning` / `contextCompaction` / `hookPrompt` | **整类跳过**(终稿已在 jsonl;推理正文不是本账本) | — |
| 其它未知 | 只留公共字段 | 原始 `item` 对象 |
| `item.type` | 追加 | 禁止 |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `commandExecution` | `command` 截断 240 字;`exitCode``durationMs``actions[]` | `aggregatedOutput` |
| `mcpToolCall` | `tool``server`(可省略默认 `agc_tools`)、`durationMs`、§5.4 参数 | `result``error` 原文(只留 `status` / `errorKind` |
| `fileChange` | `changes: [{ path, kind }]``kind``add` / `delete` / `update` | `diff``movePath` 的宿主绝对路径(相对化失败则整条 change 丢 path |
| `imageView` | `path` | 图像字节 |
| `functionCallOutput` | `name``namespace` | `output` |
| `webSearch` | `query` 截断 400 字 | 结果页正文 |
| `agentMessage` / `userMessage` / `plan` / `reasoning` / `contextCompaction` / `hookPrompt` | **整类跳过**(终稿已在 jsonl;推理正文不是本账本) | — |
| 其它未知 | 只留公共字段 | 原始 `item` 对象 |
`commandExecution.actions[]`
@@ -176,22 +176,22 @@ camelCase JSON。禁止出现附件正文、命令 stdout、patch diff、宿主
只抄这些键,其它键丢弃。字符串再经 path 清洗或截断。
| 工具 | 落盘参数 | 正文类字段 |
|---|---|---|
| `agc_list_project_files` | `path``query`120)、`kind``offset``limit` | 无 |
| `agc_write_file` | `path``contentChars``content` 的字符数,不是正文) | 不落 `content` |
| `taonier_prepare_game_art` | `mode``brief`(截断 4000)、`briefChars``briefSha256` | **要 brief 原文**(分析定玩法的吸烟枪;上限已是 MCP 合同) |
| `agc_generate_image` | `kind``aspectRatio``imageSize``assetName``outputPath``prompt` 截断 4000、`promptChars``promptSha256` | 不落 32k 全文 |
| `agc_edit_image` | `sourceLocalAssetId``assetName``prompt` 截断 4000、`promptChars``promptSha256` | 同上 |
| `agc_create_or_derive_resource` | `kind``mode``sourceLocalAssetId``assetName``prompt` 截断 4000、`promptChars``promptSha256` | MCP 上限已是 4000 |
| `agc_list_registered_assets` | `kind``assetId``includeSequenceFrames``offset``limit` | 无 |
| `agc_list_account_assets` | `folderId``query``offset``limit` | 无 |
| `agc_import_account_assets` | `assetIds`(最多 8 个 id,超出 `assetIdsOmitted`)、`localPaths`(清洗后相对路径,最多 8 | 无 |
| `agc_remove_background` | `sourceLocalAssetId``assetName` | 无 |
| `agc_browser_playtest` | `attempt` | 无 |
| `agc_web_search` | `query` 截断 400、`maxResults` | 无 |
| `agc_read_skill_resource` | `skillName``relativePath` | 不落 Skill 正文 |
| 未知 MCP 名 | 只留 `tool` + `status` | 不落 `arguments` |
| 工具 | 落盘参数 | 正文类字段 |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `agc_list_project_files` | `path``query`120)、`kind``offset``limit` | 无 |
| `agc_write_file` | `path``contentChars``content` 的字符数,不是正文) | 不落 `content` |
| `taonier_prepare_game_art` | `mode``brief`(截断 4000)、`briefChars``briefSha256` | **要 brief 原文**(分析定玩法的吸烟枪;上限已是 MCP 合同) |
| `agc_generate_image` | `kind``aspectRatio``imageSize``assetName``outputPath``prompt` 截断 4000、`promptChars``promptSha256` | 不落 32k 全文 |
| `agc_edit_image` | `sourceLocalAssetId``assetName``prompt` 截断 4000、`promptChars``promptSha256` | 同上 |
| `agc_create_or_derive_resource` | `kind``mode``sourceLocalAssetId``assetName``prompt` 截断 4000、`promptChars``promptSha256` | MCP 上限已是 4000 |
| `agc_list_registered_assets` | `kind``assetId``includeSequenceFrames``offset``limit` | 无 |
| `agc_list_account_assets` | `folderId``query``offset``limit` | 无 |
| `agc_import_account_assets` | `assetIds`(最多 8 个 id,超出 `assetIdsOmitted`)、`localPaths`(清洗后相对路径,最多 8 | 无 |
| `agc_remove_background` | `sourceLocalAssetId``assetName` | 无 |
| `agc_browser_playtest` | `attempt` | 无 |
| `agc_web_search` | `query` 截断 400、`maxResults` | 无 |
| `agc_read_skill_resource` | `skillName``relativePath` | 不落 Skill 正文 |
| 未知 MCP 名 | 只留 `tool` + `status` | 不落 `arguments` |
`brief` / 截断后的 `prompt`**模型自己写的设计文本**,不是用户 GDD 转储。这是分析「仍走收集类」的关键,允许进 jsonl。`agent.db` 摘要只留 `briefPreview` 240 字。
@@ -242,8 +242,14 @@ camelCase JSON。禁止出现附件正文、命令 stdout、patch diff、宿主
"turnLog": ".agent/runtime/direct-codex/turns/Abc123-def.jsonl",
"sidecarPresent": true,
"offeredCount": 1,
"offeredRead": [ { "localPath": "assets/uploads/…-fast_gdd.md", "read": false } ],
"firstDesign": { "kind": "mcp:taonier_prepare_game_art", "seq": 3, "briefPreview": "…" },
"offeredRead": [
{ "localPath": "assets/uploads/…-fast_gdd.md", "read": false }
],
"firstDesign": {
"kind": "mcp:taonier_prepare_game_art",
"seq": 3,
"briefPreview": "…"
},
"itemCount": 17,
"itemsTruncated": false,
"completed": true,
@@ -255,15 +261,15 @@ camelCase JSON。禁止出现附件正文、命令 stdout、patch diff、宿主
### 5.6 上限
| 项 | 值 |
|---|---|
| 每回合 item 条数 | 256;超出再写一条 `recordType: "direct.codex.items_truncated"`,之后 item 丢弃但仍把 `turn_end.itemsTruncated=true` |
| `command` | 240 字 |
| `brief` / 生成类 `prompt` 落盘 | 4000 字 |
| `briefPreview` | 240 字 |
| 文件 hash | 2 MiB |
| 附件条数 | 8(与 sidecar 相同) |
| jsonl 单行 | 沿用现有 jsonl 追加上限;超长截断正文类字段,不截断结构 |
| 项 | 值 |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| 每回合 item 条数 | 256;超出再写一条 `recordType: "direct.codex.items_truncated"`,之后 item 丢弃但仍把 `turn_end.itemsTruncated=true` |
| `command` | 240 字 |
| `brief` / 生成类 `prompt` 落盘 | 4000 字 |
| `briefPreview` | 240 字 |
| 文件 hash | 2 MiB |
| 附件条数 | 8(与 sidecar 相同) |
| jsonl 单行 | 沿用现有 jsonl 追加上限;超长截断正文类字段,不截断结构 |
## 6. 调用链
@@ -130,12 +130,12 @@ type ImageCanvasHostCommitResult = {
interface ImageCanvasHostPort {
readonly kind: ImageCanvasHostKind;
readonly capabilities: ImageCanvasHostCapabilities;
loadDraft(input: ImageCanvasHostScope): Promise<
ImageCanvasHostResult<AssetCanvasDraft | null>
>;
createDraft(input: ImageCanvasHostScope): Promise<
ImageCanvasHostResult<AssetCanvasDraft>
>;
loadDraft(
input: ImageCanvasHostScope,
): Promise<ImageCanvasHostResult<AssetCanvasDraft | null>>;
createDraft(
input: ImageCanvasHostScope,
): Promise<ImageCanvasHostResult<AssetCanvasDraft>>;
updateDraft(input: {
scope: ImageCanvasHostScope;
expectedDraftRevision: number;
@@ -207,10 +207,10 @@ interface ImageCanvasHostPort {
第一批通用视觉组件固定由 `@genarrative/image-canvas-react` 暴露:
```ts
CanvasChromeButton
CanvasToolbar
CanvasToolbarGroup
CanvasToolbarDivider
CanvasChromeButton;
CanvasToolbar;
CanvasToolbarGroup;
CanvasToolbarDivider;
```
- `CanvasChromeButton` 统一原生 button 的可访问名称、tooltip、`aria-pressed``aria-expanded`、禁用态和画布 action 视觉;图标、短文案、业务事件和禁用条件由宿主传入。共享组件不得 import Lucide、平台账号 store、钱包 store 或宿主请求客户端。
@@ -598,10 +598,7 @@ type CommitLocalProjectAssetSuccess = {
type CommitLocalProjectAssetConflict = {
status: 'conflict';
conflictKind:
| 'project-identity'
| 'project-revision'
| 'draft-revision';
conflictKind: 'project-identity' | 'project-revision' | 'draft-revision';
expectedProjectId: string;
projectId: string | null;
expectedRevision: number;
@@ -870,46 +867,46 @@ cancelling
## 13. 验收矩阵
| 编号 | 宿主/场景 | 前置或故障注入 | 必须结果 |
| --- | --- | --- | --- |
| A01 | Web + Tauri 共享源码 | 构建两个宿主 | 两者 import 同一 core/react;客户端无画布目录镜像 |
| A02 | 新增图片 | create,导入/编辑/保存 | 新 asset 落盘并进入 manifest、投影、依赖图和两种布局;无需刷新 |
| A03 | 精修图片 | refine 已有本地图片 | 原文件/asset 保留,新建 assetsource.resourceId 补齐且血缘包含源 |
| A04 | 基础编辑 | 平移、缩放、多选、移动/缩放、层序、显隐、锁定、翻转、分组 | 两宿主行为和序列化 fixture 一致,undo/redo 最多 60 步 |
| A05 | 生成成功 | 响应正常 | 只新增一次 generation 结果,草稿 CAS 递增且可继续编辑/保存 |
| A06 | 生成失败 | 上游确定失败 | 状态可恢复,不创建正式资产,不用新幂等键自动重试 |
| A07 | 生成响应丢失 | 上游已受理、客户端未收到结果 | 以原 operation/idempotency 对账,只产生一份结果/扣费 |
| A08 | 保存成功 | project/draft revision 匹配 | file -> manifest/revision -> 回读 -> ledger/draft -> event 顺序成立 |
| A09 | 重复保存 | 相同 commit/key/指纹 | 返回 already-committedasset/revision/eventId 均不重复 |
| A10 | 幂等冲突 | 同 key 或 commitId、不同指纹 | 失败关闭,原 ledger/文件/manifest 不变 |
| A11 | 两窗口并发 | 相同 expectedRevision 同时提交 | 最多一笔 committed,另一笔 typed conflict,不覆盖成功方 |
| A12 | draft 并发 | 相同 expectedDraftRevision 更新 | 最多一笔 updated,另一笔返回最新完整 draft |
| A13 | 崩溃:首个/全部事务快照、journal 或 prepared 后 | 尚未装图片 | 无 ledger 的未发布 transaction 只在正式文件不存在且 manifest/revision 仍为 before 时清理;prepared 安全回滚或继续,不生成幽灵 asset |
| A14 | 崩溃:图片后 | manifest 前 | 仅在摘要/before 全匹配时删除新文件,否则 reconciliation-required |
| A15 | 崩溃:manifest 后 | revision 前 | before/after 匹配时前向补 revision,否则 reconciliation-required |
| A16 | 崩溃:revision 后 | ledger/event 前 | 回读验证后补 ledger/draft,并重发相同 eventId |
| A17 | 崩溃:emit 后 | 投递标记前 | 允许重复事件,前端 eventId 去重且不重复选中/布局 |
| A18 | 切项目后的迟到保存 | 提交在途时打开其它项目 | 当前项目 UI 不变;旧项目缓存可按精确身份更新 |
| A19 | 切模式/离开流程 | 提交在途时进入 run/overview/新 session | 不切回素材画布、不抢焦点,正式结果仍可投影到对应项目 |
| A20 | 改选择后的迟到保存 | 等待时选择其它资源 | 保持用户选择,新资源只进入投影和布局 |
| A21 | 搜索隐藏新资源 | query epoch 改变且不匹配新资源 | 不清搜索、不自动选中,提示并提供显式清除/定位动作 |
| A22 | 即时投影 | 提交后不刷新/不重开 | manifest、资源卡、依赖图输入、布局和允许时的选中全部完成 |
| A23 | 草稿损坏/身份错配 | 损坏 JSON、未知 schema、项目路径被重建 | 失败关闭,不用空草稿覆盖,不创建其它项目副作用 |
| A24 | 锁与恢复 | 活锁 mtime 很旧、进程退出、Windows/Unix | 不按时间/PID删锁;句柄释放后正常取得同一锁入口 |
| A25 | 容量边界 | 2 MiB/4096 层/64 MiB/像素上限边界及超限 | 边界内成功,超限零副作用且错误不泄露绝对路径/密钥 |
| A26 | 导出 | PNG/JPEG/WebP | Web 下载/云端、Tauri 保存对话框均成功;共享 UI 不接收绝对路径 |
| A27 | 取消 | clean、dirty、generating、staging、committing | 分别符合第 12 节;committing 不伪装成可取消 |
| A28 | 恢复草稿 | 主文件损坏但恢复副本可信/不可信 | 可信副本恢复到 clean history 基线;不可信进入对账,不猜测 |
| A29 | 登录刷新重放 | context、参考图准备或首次提交返回 401,刷新后以相同 generationId 和幂等身份重放 | 401 账本保持可恢复且第二次真实访问平台;403 直接失败且不刷新;远端最多受理一次 |
| A30 | 生成卡片拖动 | 生成中拖动占位卡片 | 卡片位置按画布坐标更新并持久化到 generation record,任务状态刷新不覆盖用户位置 |
| A31 | 精修最终图唯一性 | 选择另一候选图设为最终图 | 入口原图和所有其他候选保持各自快照,只有 `lastCommit.sourceLayerId` 标识唯一正式候选,更新正式 asset 不反向改写历史图层 |
| A32 | 精修默认比例 | 打开图片的快速编辑 | 按原图宽高映射到最接近的支持比例(1:1、2:3、3:2、9:16、16:9);尺寸无效时回退 1:1 |
| A33 | 旧提交被后继提交取代 | 同一 asset 的旧事务未收尾,且后继 committed 事务链、当前 manifest/revision 与最终文件全部可证明 | 旧事务进入 `superseded`,不回滚、不覆盖当前正式图、不重放旧事件;证据不完整仍进入对账 |
| A34 | 同资源并发正式提交 | 同一 project/draft/asset 存在 prepared 或 reconciliation 事务时再次提交 | 拒绝新提交并要求先安全恢复;已 committed/rolled-back/superseded 事务不阻塞后续提交 |
| A35 | 精修文件名包含历史提交后缀 | 后续精修重新打开当前 `localPath`,或再次生成 / 设为最终图 | 统一剥离文件名末尾一个或多个 `--<uuid>` 后缀并规范化为合法 1..=80 字符显示名;生成与最终提交使用同一结果 |
| A36 | 确定性提交参数无效 | 候选提交名称或用途在校验阶段失败 | 在读取候选、staging、transaction 或 ledger 写入前零副作用失败;UI 作为输入校验错误允许继续编辑,不触发安全恢复 |
| A37 | 候选首次确认 | 生成完成后与旧 autosave 并发,或重复打开已确认候选 | 前端把候选确认排入草稿保存 FIFO,并在提交、导入、生成、归档和放弃草稿前等待确认屏障;Tauri 在草稿锁内只为当前权威草稿中仍存在且尚未确认的候选更新私有 ledger,不改写草稿或推进 revision。普通 update 在确认前继续把候选层合回旧保存,重复确认无写入,确认后的显式删除仍允许 |
| A38 | 稳定运行入口 | 精修替换已在游戏源码中引用的图片,或继续精修旧版本事务创建的资源 | manifest 指向不可变正式版本,同时原稳定入口路径不变并刷新为新版本字节;新事务可从旧事务 `manifest.before.json` 迁移稳定入口身份,幂等重放和事务恢复会修复缺失或不匹配入口,游戏源码不需要改路径 |
| 编号 | 宿主/场景 | 前置或故障注入 | 必须结果 |
| ---- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A01 | Web + Tauri 共享源码 | 构建两个宿主 | 两者 import 同一 core/react;客户端无画布目录镜像 |
| A02 | 新增图片 | create,导入/编辑/保存 | 新 asset 落盘并进入 manifest、投影、依赖图和两种布局;无需刷新 |
| A03 | 精修图片 | refine 已有本地图片 | 原文件/asset 保留,新建 assetsource.resourceId 补齐且血缘包含源 |
| A04 | 基础编辑 | 平移、缩放、多选、移动/缩放、层序、显隐、锁定、翻转、分组 | 两宿主行为和序列化 fixture 一致,undo/redo 最多 60 步 |
| A05 | 生成成功 | 响应正常 | 只新增一次 generation 结果,草稿 CAS 递增且可继续编辑/保存 |
| A06 | 生成失败 | 上游确定失败 | 状态可恢复,不创建正式资产,不用新幂等键自动重试 |
| A07 | 生成响应丢失 | 上游已受理、客户端未收到结果 | 以原 operation/idempotency 对账,只产生一份结果/扣费 |
| A08 | 保存成功 | project/draft revision 匹配 | file -> manifest/revision -> 回读 -> ledger/draft -> event 顺序成立 |
| A09 | 重复保存 | 相同 commit/key/指纹 | 返回 already-committedasset/revision/eventId 均不重复 |
| A10 | 幂等冲突 | 同 key 或 commitId、不同指纹 | 失败关闭,原 ledger/文件/manifest 不变 |
| A11 | 两窗口并发 | 相同 expectedRevision 同时提交 | 最多一笔 committed,另一笔 typed conflict,不覆盖成功方 |
| A12 | draft 并发 | 相同 expectedDraftRevision 更新 | 最多一笔 updated,另一笔返回最新完整 draft |
| A13 | 崩溃:首个/全部事务快照、journal 或 prepared 后 | 尚未装图片 | 无 ledger 的未发布 transaction 只在正式文件不存在且 manifest/revision 仍为 before 时清理;prepared 安全回滚或继续,不生成幽灵 asset |
| A14 | 崩溃:图片后 | manifest 前 | 仅在摘要/before 全匹配时删除新文件,否则 reconciliation-required |
| A15 | 崩溃:manifest 后 | revision 前 | before/after 匹配时前向补 revision,否则 reconciliation-required |
| A16 | 崩溃:revision 后 | ledger/event 前 | 回读验证后补 ledger/draft,并重发相同 eventId |
| A17 | 崩溃:emit 后 | 投递标记前 | 允许重复事件,前端 eventId 去重且不重复选中/布局 |
| A18 | 切项目后的迟到保存 | 提交在途时打开其它项目 | 当前项目 UI 不变;旧项目缓存可按精确身份更新 |
| A19 | 切模式/离开流程 | 提交在途时进入 run/overview/新 session | 不切回素材画布、不抢焦点,正式结果仍可投影到对应项目 |
| A20 | 改选择后的迟到保存 | 等待时选择其它资源 | 保持用户选择,新资源只进入投影和布局 |
| A21 | 搜索隐藏新资源 | query epoch 改变且不匹配新资源 | 不清搜索、不自动选中,提示并提供显式清除/定位动作 |
| A22 | 即时投影 | 提交后不刷新/不重开 | manifest、资源卡、依赖图输入、布局和允许时的选中全部完成 |
| A23 | 草稿损坏/身份错配 | 损坏 JSON、未知 schema、项目路径被重建 | 失败关闭,不用空草稿覆盖,不创建其它项目副作用 |
| A24 | 锁与恢复 | 活锁 mtime 很旧、进程退出、Windows/Unix | 不按时间/PID删锁;句柄释放后正常取得同一锁入口 |
| A25 | 容量边界 | 2 MiB/4096 层/64 MiB/像素上限边界及超限 | 边界内成功,超限零副作用且错误不泄露绝对路径/密钥 |
| A26 | 导出 | PNG/JPEG/WebP | Web 下载/云端、Tauri 保存对话框均成功;共享 UI 不接收绝对路径 |
| A27 | 取消 | clean、dirty、generating、staging、committing | 分别符合第 12 节;committing 不伪装成可取消 |
| A28 | 恢复草稿 | 主文件损坏但恢复副本可信/不可信 | 可信副本恢复到 clean history 基线;不可信进入对账,不猜测 |
| A29 | 登录刷新重放 | context、参考图准备或首次提交返回 401,刷新后以相同 generationId 和幂等身份重放 | 401 账本保持可恢复且第二次真实访问平台;403 直接失败且不刷新;远端最多受理一次 |
| A30 | 生成卡片拖动 | 生成中拖动占位卡片 | 卡片位置按画布坐标更新并持久化到 generation record,任务状态刷新不覆盖用户位置 |
| A31 | 精修最终图唯一性 | 选择另一候选图设为最终图 | 入口原图和所有其他候选保持各自快照,只有 `lastCommit.sourceLayerId` 标识唯一正式候选,更新正式 asset 不反向改写历史图层 |
| A32 | 精修默认比例 | 打开图片的快速编辑 | 按原图宽高映射到最接近的支持比例(1:1、2:3、3:2、9:16、16:9);尺寸无效时回退 1:1 |
| A33 | 旧提交被后继提交取代 | 同一 asset 的旧事务未收尾,且后继 committed 事务链、当前 manifest/revision 与最终文件全部可证明 | 旧事务进入 `superseded`,不回滚、不覆盖当前正式图、不重放旧事件;证据不完整仍进入对账 |
| A34 | 同资源并发正式提交 | 同一 project/draft/asset 存在 prepared 或 reconciliation 事务时再次提交 | 拒绝新提交并要求先安全恢复;已 committed/rolled-back/superseded 事务不阻塞后续提交 |
| A35 | 精修文件名包含历史提交后缀 | 后续精修重新打开当前 `localPath`,或再次生成 / 设为最终图 | 统一剥离文件名末尾一个或多个 `--<uuid>` 后缀并规范化为合法 1..=80 字符显示名;生成与最终提交使用同一结果 |
| A36 | 确定性提交参数无效 | 候选提交名称或用途在校验阶段失败 | 在读取候选、staging、transaction 或 ledger 写入前零副作用失败;UI 作为输入校验错误允许继续编辑,不触发安全恢复 |
| A37 | 候选首次确认 | 生成完成后与旧 autosave 并发,或重复打开已确认候选 | 前端把候选确认排入草稿保存 FIFO,并在提交、导入、生成、归档和放弃草稿前等待确认屏障;Tauri 在草稿锁内只为当前权威草稿中仍存在且尚未确认的候选更新私有 ledger,不改写草稿或推进 revision。普通 update 在确认前继续把候选层合回旧保存,重复确认无写入,确认后的显式删除仍允许 |
| A38 | 稳定运行入口 | 精修替换已在游戏源码中引用的图片,或继续精修旧版本事务创建的资源 | manifest 指向不可变正式版本,同时原稳定入口路径不变并刷新为新版本字节;新事务可从旧事务 `manifest.before.json` 迁移稳定入口身份,幂等重放和事务恢复会修复缺失或不匹配入口,游戏源码不需要改路径 |
阶段一至五最终审计只有在矩阵对应的纯模型、共享 React、Web adapter、Tauri adapter、Rust 持久化与 AppSurface 测试全部通过后,才可宣称图片素材创作正式闭环完成。
File diff suppressed because it is too large Load Diff
@@ -8,13 +8,13 @@
## 0. 摘要
| # | 缺陷 | 表现 | 性质 |
|---|---|---|---|
| 1 | 前端保存设置时把 `agentMode` 硬写成 `codex_app_server` | UI 里换 provider 只改了 `llm.*`,运行模式换不掉,且界面上看不到这个字段 | 产品缺陷 |
| 2 | `codex_app_server` 模式把第三方端点喂给 codex | apiKind≠openai_responses 时秒挂;否则 413 + 工具误用,180 秒超时后留下待核对的孤儿请求 | 模式前提未被约束 |
| 3 | `provider` 模式下 `tool_choice=required` 与 DeepSeek 思考模式互斥 | 首个 tool-plan 请求 400,整个 runtime 起不来 | 参数空间缺一个值 |
| 4 | 普通 action 批次带 plan update 时,两条预检规则互斥 | 「更新计划 + 委派专业 Agent」同一轮返回就报「批次成员身份或顺序不匹配」 | **本分支回归**(已修) |
| 5 | `llm.stream` 只记录配置,不驱动 Provider tool-plan 传输 | 要求 `stream=true` 的网关第一发 tool-plan 得到 HTTP 400,整轮不可用 | 传输配置失效(已修) |
| # | 缺陷 | 表现 | 性质 |
| --- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------- |
| 1 | 前端保存设置时把 `agentMode` 硬写成 `codex_app_server` | UI 里换 provider 只改了 `llm.*`,运行模式换不掉,且界面上看不到这个字段 | 产品缺陷 |
| 2 | `codex_app_server` 模式把第三方端点喂给 codex | apiKind≠openai_responses 时秒挂;否则 413 + 工具误用,180 秒超时后留下待核对的孤儿请求 | 模式前提未被约束 |
| 3 | `provider` 模式下 `tool_choice=required` 与 DeepSeek 思考模式互斥 | 首个 tool-plan 请求 400,整个 runtime 起不来 | 参数空间缺一个值 |
| 4 | 普通 action 批次带 plan update 时,两条预检规则互斥 | 「更新计划 + 委派专业 Agent」同一轮返回就报「批次成员身份或顺序不匹配」 | **本分支回归**(已修) |
| 5 | `llm.stream` 只记录配置,不驱动 Provider tool-plan 传输 | 要求 `stream=true` 的网关第一发 tool-plan 得到 HTTP 400,整轮不可用 | 传输配置失效(已修) |
缺陷 1~3 叠加的结果:**当前代码里没有任何一组配置能让 DeepSeek 跑起来**。缺陷 4 与 provider 无关,换成 `gpt-5.6-terra` 打通 LLM 链路后才暴露出来。
@@ -136,20 +136,25 @@ error=unable to locate image at `<project>/memory/README.md`: (os error 2)
`agentMode` 切换为 `provider` 后重跑仍然失败;脱敏后的上游错误为:
```json
{"error":{"message":"Thinking mode does not support this tool_choice",
"type":"invalid_request_error","code":"invalid_request_error"}}
{
"error": {
"message": "Thinking mode does not support this tool_choice",
"type": "invalid_request_error",
"code": "invalid_request_error"
}
}
```
### 复现矩阵(直接打 DeepSeek,非流式)
| 请求 | 结果 |
|---|---|
| `tool_choice: "required"` | **400** Thinking mode does not support this tool_choice |
| `tool_choice: "auto"` | 200 |
| `tool_choice: "none"` | 200 |
| `tool_choice: "required"` + `reasoning.effort: "none"` | **200**,且正常返回 `function_call` |
| `tool_choice: "required"` + `reasoning.effort: "minimal"` | 400 |
| `tool_choice: "required"` + `thinking: {type:"disabled"}` | 400 |
| 请求 | 结果 |
| --------------------------------------------------------- | ------------------------------------------------------- |
| `tool_choice: "required"` | **400** Thinking mode does not support this tool_choice |
| `tool_choice: "auto"` | 200 |
| `tool_choice: "none"` | 200 |
| `tool_choice: "required"` + `reasoning.effort: "none"` | **200**,且正常返回 `function_call` |
| `tool_choice: "required"` + `reasoning.effort: "minimal"` | 400 |
| `tool_choice: "required"` + `thinking: {type:"disabled"}` | 400 |
`/chat/completions``/responses` 两条 wire、`deepseek-v4-flash``deepseek-v4-pro` 两个模型表现完全一致。即:**DeepSeek 支持强制工具调用,但必须先关掉思考模式,而唯一能关掉它的开关是 `reasoning.effort: "none"`。**
@@ -186,10 +191,10 @@ Agent Runtime Provider action 批次成员身份或顺序不匹配:index=0
两轮的 `tool_plan.protocol` 记录:
| loop | 模型返回的 function call | 结果 |
|---|---|---|
| 1 | `runtime_tool_agent_goal_contract` | ok |
| 2 | `update_agent_plan` + `runtime_tool_agent_delegate` | 预检失败 |
| loop | 模型返回的 function call | 结果 |
| ---- | --------------------------------------------------- | -------- |
| 1 | `runtime_tool_agent_goal_contract` | ok |
| 2 | `update_agent_plan` + `runtime_tool_agent_delegate` | 预检失败 |
即「更新计划 + 委派专业 Agent」同一轮返回——总控最常规的动作。
@@ -261,6 +266,7 @@ let expected_member_plan_update = batch
### P0 —— 让第三方 provider 可用
1. **`reasoningEffort` 增加 `none`**
- [platform-llm lib.rs:195](../../server-rs/crates/platform-llm/src/lib.rs)`LlmResponseReasoningEffort``None` 变体,`as_str()` 返回 `"none"`;注意仓库内对该枚举有多处 exhaustive match`Max` 刚加时踩过),需一并补齐。
- [config.rs:145](../../apps/ai-game-creator-shell/src-tauri/src/config.rs) `parse_game_creator_llm_reasoning_effort` 接受 `none`,同步 :155 的错误文案。
- [types.ts:605](../../apps/ai-game-creator-shell/src/app/types.ts) `gameCreatorLlmReasoningEfforts``'none'`RuntimeConfigDialog 的默认表(:40~:65)与下拉项同步。
@@ -68,10 +68,7 @@ Direct Codex 调用的输入也只有:
```ts
{
projectPath,
prompt,
clientTurnId,
creationType
projectPath, prompt, clientTurnId, creationType;
}
```