Merge remote-tracking branch 'origin/master' into feat/agc_add_on
# Conflicts: # docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -550,7 +550,6 @@ type ProjectAgentMudPointAttribution = {
|
||||
|
||||
### 7.6 素材创作无限画布阶段一至五最终验收
|
||||
|
||||
|
||||
1. 网站与 Tauri 实际 import 同一份 `@genarrative/image-canvas-core` 和 `@genarrative/image-canvas-react`,客户端没有复制的主站画布目录;viewport、selection、变换、renderer 与 history 算法位于共享层,宿主只保留事件接线与 adapter 副作用。
|
||||
2. “新增资源”在当前产品切片中保持禁用;“编辑资源”只接受现有资源。图片 refine 为稳定 `sourceAssetId` 保存一个持续精修草稿和多个私有 `draft-media` 候选;只有用户显式“设为最终图”时才安装新的正式 PNG,并保持原 asset ID 不变、事务化更新其 manifest `localPath/mediaType`。候选图不进入 manifest 或资源总览。其他类型继续只追加派生文件/asset 或子版本。
|
||||
3. 草稿 schema、revision、容量、项目身份、OS 锁、CAS、恢复副本和媒体引用符合权威专题;损坏、未知 schema、身份错配和超限均失败关闭。应用重启后必须按 `projectId + intent + sourceAssetId + 可恢复/可继续状态` 从正式 sidecar 唯一发现原 refine 草稿并保留 `draftId`、候选图和生成占位,同时生成新 `sessionId`;零条才允许创建,多条必须进入对账,不得依赖进程内 Map 或按时间猜测。
|
||||
@@ -559,14 +558,14 @@ type ProjectAgentMudPointAttribution = {
|
||||
6. 搜索/筛选隐藏新资源时保留条件,明确提示“新资源已保存,当前筛选条件下不可见”,只通过显式动作清除条件并定位。
|
||||
7. 现有资源编辑、生成、保存、取消、失败和恢复必须覆盖权威专题 §13 中与当前非破坏性编辑切片对应的验收矩阵;只完成画布 UI 或只完成本地写文件都不能算正式闭环。
|
||||
8. 自动定位必须分别证明资源已投影、dependency/type 两份布局都 settled 且存在目标位置、目标卡 DOM 已提交;搜索隐藏走显式清除/定位,任何 commit 最多自动聚焦一次。
|
||||
10. 普通素材画布生成账本的服务身份固定为 `official-platform-v1 + 官方 origin + ownerUserId`,不绑定 Access Token;高级 External v1 账本只绑定显式服务 origin,不绑定 Developer API Key。两种模式的 `accepted/running` 都只恢复原 GET,`prepared` 只可精确重放冻结的原 POST、原正文和原幂等键。普通模式退出或换号后提升账号 generation,中止并脱离旧请求;旧账号账本在新账号下零网络、零安装,只有重新登录同一 owner 后才可恢复。不能通过更换 Token、Key、URL、请求正文或 operationId 绕过该隔离。
|
||||
11. 资源编辑恢复面板必须为独立 modal,展示后端权威队列的全部 operation。用户可继续任意可恢复项;`remote-failed` 只允许显式移出活动队列,并保留私有账本审计;`reconciliation-required` 只读展示对账。读取失败必须提供重试,不得伪装空队列;操作后必须重读后端。
|
||||
12. `remote-failed` 已是远端明确终态,重启后不再 POST、不再轮询、不再扣费;`archived` 仅表示用户已将它移出活动恢复队列,不等于 `committed`。`result-unknown`、鉴权临时失败和 `reconciliation-required` 均不允许归档或重新生成。
|
||||
13. 派生资产提交必须通过 durable asset transaction journal 串起最终文件、manifest 和 project revision。任一崩溃阶段恢复后只有一份派生文件、一条 manifest asset,revision 精确推进一次;manifest 已写而 revision 未写时只前向补 revision,无法证明的组合进入人工对账。journal 已证明目标 asset、媒体和 target revision 写入后,即使后续合法提交继续推进 manifest/revision,也应按目标 asset 精确身份与 `currentRevision >= targetRevision` 补齐 ledger/draft,不得要求整个 manifest 永远等于历史 after 快照。durable committed 后遗留的 staging 只有在 staging/正式媒体摘要一致,且 manifest 中按 asset ID 或路径唯一命中并与 journal asset 精确相等时才尽力删除;删除失败不降级已提交结果,身份或媒体漂移则保留 staging 并进入对账。
|
||||
14. generation progress、草稿保存队列、生成/提交回包与延迟 `loadDraft` 必须共用单调 revision 门禁,低 revision 不得覆盖已落地的新草稿。Shift 指针与键盘选择必须与共享 core 一致;单选自身 Shift 不能清空选择,已选多图层普通指针拖动应保持并同步移动选择集。零位移不得产生 undo、documentVersion 或草稿保存。
|
||||
15. 失败 UI 必须按 `generation / draft-save / asset-commit / recovery / cancellation` 五类 operation 显示可访问名称与安全动作。生成是任务级状态:排队、运行、失败和对账不得用全屏遮罩终结画布;失败占位保留原参数并允许重新修改。草稿、CAS、最终图提交、恢复和取消故障不得出现会发起新生成的按钮。真实写入和恢复阶段仍由 handler、revision 与锁负责门禁,视觉 disabled、旧闭包或遮罩不能充当业务门禁。
|
||||
16. 派生子版本 journal 必须冻结 project revision before/after 身份和目标 after 记录。manifest 已有目标子版本但 journal 缺失时失败关闭;旧 journal 缺少 revision 身份且当前 revision 已推进、无法证明是同一事务写入时进入人工对账,不得把现状猜测为已提交。
|
||||
17. 文本、SVG 与 Agent 回执编辑必须在调用 Provider 前持久化 request-issued;Provider 成功正文必须在解析、格式校验和 staging 之前原子写入私有 durable handoff,并绑定原 operation、请求指纹和内容摘要。issued 后缺少可信 handoff 只能对账,已有可信 handoff 则只消费原正文,两者都不得再次调用 Provider。普通模式首次 `401` 只允许当前账号 generation 的单飞 refresh,成功后以更高 generation CAS 安装 Token,并使用原正文、原幂等键重试一次;refresh 失败、换号或再次 `401` 时保留原 operation,停止网络。`403` 不触发 refresh。高级 External v1 模式修正 Developer Key 后仍只查询或恢复原 operation。
|
||||
9. 普通素材画布生成账本的服务身份固定为 `official-platform-v1 + 官方 origin + ownerUserId`,不绑定 Access Token;高级 External v1 账本只绑定显式服务 origin,不绑定 Developer API Key。两种模式的 `accepted/running` 都只恢复原 GET,`prepared` 只可精确重放冻结的原 POST、原正文和原幂等键。普通模式退出或换号后提升账号 generation,中止并脱离旧请求;旧账号账本在新账号下零网络、零安装,只有重新登录同一 owner 后才可恢复。不能通过更换 Token、Key、URL、请求正文或 operationId 绕过该隔离。
|
||||
10. 资源编辑恢复面板必须为独立 modal,展示后端权威队列的全部 operation。用户可继续任意可恢复项;`remote-failed` 只允许显式移出活动队列,并保留私有账本审计;`reconciliation-required` 只读展示对账。读取失败必须提供重试,不得伪装空队列;操作后必须重读后端。
|
||||
11. `remote-failed` 已是远端明确终态,重启后不再 POST、不再轮询、不再扣费;`archived` 仅表示用户已将它移出活动恢复队列,不等于 `committed`。`result-unknown`、鉴权临时失败和 `reconciliation-required` 均不允许归档或重新生成。
|
||||
12. 派生资产提交必须通过 durable asset transaction journal 串起最终文件、manifest 和 project revision。任一崩溃阶段恢复后只有一份派生文件、一条 manifest asset,revision 精确推进一次;manifest 已写而 revision 未写时只前向补 revision,无法证明的组合进入人工对账。journal 已证明目标 asset、媒体和 target revision 写入后,即使后续合法提交继续推进 manifest/revision,也应按目标 asset 精确身份与 `currentRevision >= targetRevision` 补齐 ledger/draft,不得要求整个 manifest 永远等于历史 after 快照。durable committed 后遗留的 staging 只有在 staging/正式媒体摘要一致,且 manifest 中按 asset ID 或路径唯一命中并与 journal asset 精确相等时才尽力删除;删除失败不降级已提交结果,身份或媒体漂移则保留 staging 并进入对账。
|
||||
13. generation progress、草稿保存队列、生成/提交回包与延迟 `loadDraft` 必须共用单调 revision 门禁,低 revision 不得覆盖已落地的新草稿。Shift 指针与键盘选择必须与共享 core 一致;单选自身 Shift 不能清空选择,已选多图层普通指针拖动应保持并同步移动选择集。零位移不得产生 undo、documentVersion 或草稿保存。
|
||||
14. 失败 UI 必须按 `generation / draft-save / asset-commit / recovery / cancellation` 五类 operation 显示可访问名称与安全动作。生成是任务级状态:排队、运行、失败和对账不得用全屏遮罩终结画布;失败占位保留原参数并允许重新修改。草稿、CAS、最终图提交、恢复和取消故障不得出现会发起新生成的按钮。真实写入和恢复阶段仍由 handler、revision 与锁负责门禁,视觉 disabled、旧闭包或遮罩不能充当业务门禁。
|
||||
15. 派生子版本 journal 必须冻结 project revision before/after 身份和目标 after 记录。manifest 已有目标子版本但 journal 缺失时失败关闭;旧 journal 缺少 revision 身份且当前 revision 已推进、无法证明是同一事务写入时进入人工对账,不得把现状猜测为已提交。
|
||||
16. 文本、SVG 与 Agent 回执编辑必须在调用 Provider 前持久化 request-issued;Provider 成功正文必须在解析、格式校验和 staging 之前原子写入私有 durable handoff,并绑定原 operation、请求指纹和内容摘要。issued 后缺少可信 handoff 只能对账,已有可信 handoff 则只消费原正文,两者都不得再次调用 Provider。普通模式首次 `401` 只允许当前账号 generation 的单飞 refresh,成功后以更高 generation CAS 安装 Token,并使用原正文、原幂等键重试一次;refresh 失败、换号或再次 `401` 时保留原 operation,停止网络。`403` 不触发 refresh。高级 External v1 模式修正 Developer Key 后仍只查询或恢复原 operation。
|
||||
|
||||
### 7.7 主站 UI 对齐验收
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_coordinator.rs`。
|
||||
|
||||
## 2026-08-27 退款 emergency spool 容量溢出保持可恢复
|
||||
|
||||
## 2026-08-27 退款 emergency spool 容量溢出保持可恢复
|
||||
|
||||
- 背景:本机 emergency spool 仅作为 SpacetimeDB 完全不可达时的最后恢复路径,原有 `MAX_BYTES` 分支会直接返回 `Dropped`,导致扣费已经完成但没有可重放记录。
|
||||
|
||||
@@ -41,15 +41,15 @@ SpacetimeDB 任务统一先读取 `.codex/skills/genarrative-spacetimedb/SKILL.m
|
||||
|
||||
按改动范围选择定向门禁,不以无关全量扫描代替契约验证:
|
||||
|
||||
| 范围 | 至少运行 |
|
||||
| --- | --- |
|
||||
| 文档 / 中文文本 | `npm run check:encoding`、`git diff --check` |
|
||||
| 前端 | 相关 Vitest、类型检查;需要时做桌面/移动视口 smoke |
|
||||
| Rust 后端 | 对应 crate 的 `cargo test` / `cargo check`、`/healthz` smoke |
|
||||
| External v1 | OpenAPI 解析、实现/DTO 契约测试和鉴权 smoke |
|
||||
| SpacetimeDB schema | `npm run spacetime:generate`、`npm run check:spacetime-schema`、运行时访问检查 |
|
||||
| 范围 | 至少运行 |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 文档 / 中文文本 | `npm run check:encoding`、`git diff --check` |
|
||||
| 前端 | 相关 Vitest、类型检查;需要时做桌面/移动视口 smoke |
|
||||
| Rust 后端 | 对应 crate 的 `cargo test` / `cargo check`、`/healthz` smoke |
|
||||
| External v1 | OpenAPI 解析、实现/DTO 契约测试和鉴权 smoke |
|
||||
| SpacetimeDB schema | `npm run spacetime:generate`、`npm run check:spacetime-schema`、运行时访问检查 |
|
||||
| AGC / DirectProject | 使用 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 中的当前定向门禁;真实 Provider/登录缺失必须记为未验证 |
|
||||
| 生产发布 | 当前开发运维文档、脚本门禁、主机进程、备份和公开端点证据 |
|
||||
| 生产发布 | 当前开发运维文档、脚本门禁、主机进程、备份和公开端点证据 |
|
||||
|
||||
只有 `cargo check/test/clippy/fmt/build`、`npm test`、规范命名的 npm 验证脚本和精确 `node --test` 形成验证凭证;`git`、`rg`、`cargo metadata` 和普通 `npm run` 只作为诊断。
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
|
||||
## 已完成
|
||||
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
|
||||
## 未完成
|
||||
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
- [ ]
|
||||
|
||||
## 关键文件
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
## 当前问题/风险
|
||||
|
||||
-
|
||||
-
|
||||
|
||||
## 已执行验证
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
|
||||
## 建议下一步
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## 是否需要更新团队记忆
|
||||
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
- 处理:本阶段不加机制,manifest 明确降级为 lineage 判定通过后的补充信号(见 decision-log 2026-08-12 条)。将来要做,必须同时覆盖两条写入路径,并统一走毫秒换算 helper。
|
||||
- 验证:`apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts` 的「keeps an unbound manifest out of the verdict until the current main is terminal」钉住了现有边界与残余风险;该用例最后一条断言即为已记录的残余风险,改动它就意味着重新裁决,必须同步更新决策记录。
|
||||
|
||||
|
||||
- 现象:前端 Runtime map 以 Agent ID 保存当前记录。新一轮仍会复用 `code-prototype`、`art-director`、`art-asset-plan` 这些 Agent ID;若旧 root 已终态但 main/美术 child 或 manifest 仍在收口,直接从 live map 归档会在新 root 接管后丢失旧后代,或把新轮证据误接到旧阶段记录。
|
||||
- 修复:根进入终态时按完整 `agent/session/run` 身份保存 root-scoped Runtime 快照,后续只合并同一稳定身份的更新;manifest 快照只在该 root 仍为当前 root 时捕获。归档前重新执行严格 lineage、全终态、单 main/单 active art 与 reconciliation 门禁,并用 root run 派生稳定 message ID。
|
||||
|
||||
@@ -109,6 +108,7 @@
|
||||
- 并发恢复补充:自主根任务 journal 写入后建立或重建 completion contract 时,初始 manifest reset 与 continuation reconciliation reset 不能重新使用 fail-fast 项目锁。异步 child finalization 可以合法插入两次取锁之间,使已入 journal 的新根被误记为 `completion-contract-failed`。这两条 reset 必须使用现有有界等待项目锁,超时仍失败关闭;只验证 scheduler 合同的测试应预占 child Runtime lane,不能真实启动后台 worker 后再手工改 manifest。确定性回归要显式持锁,分别证明初始合同与 continuation 合同等待释放后成功落盘。
|
||||
- 验证:夹具必须从 `init_local_game_project_at` 开始,先断言无 `package.json` 且占位入口 smoke 失败,再证明产物不齐阻断、齐全后内部验证通过、无 smoke trace、再次 mutation 失效;另覆盖四个 owner 路径矩阵、错误身份、恢复、跨 run 凭证、`art-director` 有/无 Key、动态美术借凭证拒绝、`code-prototype` project.verify-only 阻断和试玩 executor 身份。
|
||||
- 关联:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。
|
||||
|
||||
## UI 设计 State 的 strict JSON round-trip 不能混用两种浮点序列化表示(2026-08-19)
|
||||
|
||||
- 现象:为节点拖拽/缩放生成非整数 Transform 后,保存报“UI 设计 State 安装后回读与待写内容不一致”;由于读取主文件失败关闭后恢复 `.previous`,后续回读表现为刚导入的 spirit/sprite 资产丢失。
|
||||
@@ -4364,7 +4364,6 @@
|
||||
- 处理:历史花费只累计 `asset_operation_consume` 负向流水绝对值,退款不冲减;通过 `profile_wallet_consumption_total` 在已有投影时按主键 O(1) 累加。首次上线必须在停写维护窗口由 owner 执行全量初始化,为每个已有钱包流水的用户建立投影,不能让所有存量用户的首次正常消费各自扫描历史;维护遗漏或新用户缺行时才在首次消费或详情读取中按用户索引兜底重建一次。手动对账扫描是独立高风险操作,member 必须单独持有 `profile-wallet-consumption-reconcile`,不能因为能打开共享用户详情就自动获得。
|
||||
- 验证:构造消费、退款、充值退款追回和赠送混合流水,断言只累计消费;维护初始化后正常消费只按主键累加;重复详情读取不得重复扫描或重复累计;任意 Tab 权限不能调用手动对账,同时确认充值订单列表的通用钱包快照没有新增历史流水扫描。
|
||||
|
||||
|
||||
- 症状:`code-prototype` 首次完成后 `.agent/logs/command.log` 已出现 `permission.confirm preview.start`,但客户端没有 iframe,`.agent/logs/preview.log` 也没有新的 running 记录;后续即使父 run 完成也不再启动。
|
||||
- 根因:旧实现调用 `start_local_game_preview` 前就把“项目 + parent run”的授权加入 attempted 集合并清空;首版完成投影与后续专业任务仍在写项目时,启动恰逢项目写锁竞争,catch 只显示错误却无法重试。
|
||||
- 约束:一次性语义应按“成功或确定性终态”消费,不按“函数调用次数”消费。项目写锁竞争保留同一授权并轮询重试;成功、显式 deny 与非瞬时失败才清除。授权需持久化项目路径和 accepted runId,重启恢复时仍必须逐项匹配,切换项目不得继承。
|
||||
@@ -4460,7 +4459,6 @@
|
||||
- 处理:严格继续意图必须在同一 Supervisor Session、同一持久 source 内继承最近失败根 run 的原始目标和 baseline,但保持新的 run/Provider/sidecar 身份;纯继续词表只能有一个权威实现,中英文短语都走同一入口,真正新需求仍独立 reset。非占位入口禁止 fallback 整体覆盖,也不能反复运行只读 smoke;当前 `code-prototype` 必须先读取并实际 patch,取得本人 mutation 后才能验证和交付。占位 fallback 只支持具备真实语义的显式模板,俄罗斯方块必须实际实现棋盘、下落、旋转、锁定和消行,未知玩法失败关闭。`art-spec.png` 只作规范参考,核心运行时位图必须来自独立派生的透明 `art-spritesheet.png` 及其 `iconImageSrcs` 本地切片;切片清单绑定当前图集 resourceId,Canvas 分别使用玩家、目标、场景和反馈四类素材。不得猜测图集是 2×2 等分、把规范板塞进画面或以纯代码核心实体绕过派生素材。
|
||||
- 验证:覆盖失败根任务“水晶俄罗斯方块”后输入“继续”、连续 successor、跨 Session、跨 source、正常完成后新输入、带具体新需求、既有非占位入口先 patch 后 smoke、初始化占位的俄罗斯方块真实语义、未知玩法失败关闭、纯继续目标缺失、规范图不在运行 DOM/Canvas、真实动作前后 `sequence` 与 RAF 空转。浏览器验收必须同时比较 baseline 玩法关键文本/控件/状态和当前 revision,不能只看 Canvas 非空与三个固定按钮。
|
||||
|
||||
|
||||
- 现象:用户要求把已有美术资源接入游戏时,固定 `code-director -> art-director / art-asset-plan -> code-prototype` 图会在缺少主 Agent 审计的情况下启动美术生成,或把“整体重做”错误实现为无条件生图;美术完成后又换了 Run,代码接入、静态检查和试玩无法形成连续责任链。
|
||||
- 原因:固定节点把“是否需要美术”的语义判断编码为 Runtime 前置流程,`code-director` 成为另一个主控,而不是让真正接入游戏的 `code-prototype` 基于权威资产事实决策;如果再把固定审计策略塞进用户意图字段,Supervisor 的理解也会被 Runtime 规则覆盖。两个素材槽都缺失时若先消耗不可重试的 `art-asset-plan` 委派,其 child 又必然因缺规范图失败,整个 Run 会进入无法补救的死路。
|
||||
- 处理:Supervisor 用 `intentSummary` 持久化自己对用户意图的理解,固定 `audit-existing-first` 只作安全执行策略,随后只启动 `code-prototype`。主 Agent 先 `asset.list`,完整覆盖就直接使用;仅在事实证明缺少规范图或核心图集时,才建立一个写入范围受限为 `assets/**` 的美术 durable delivery。两槽都缺失时必须先完成并认领 `art-director` 的 `EvidenceReady` delivery,再委派 `art-asset-plan`;回执返回同一主 Run 后再接入素材、原玩法语义检查、静态检查和双视口试玩。读取旧 v1 决策时必须复核其旧 fingerprint,并从完成合同绑定的有效任务迁移 intent;旧 `code-director` coverage/route 只能触发当前主 Agent 重新审计和原位替换,不能直接成为新完成证据。整体视觉重做意图同样必须经过这次审计,不能成为绕过资产复用或强制重生成的固定规则。完整 GUI / CLI DAG 不使用该例外。
|
||||
@@ -4489,7 +4487,6 @@
|
||||
- 验证:DOM 与截图不得出现外部品牌或 unsupported 列;AppSurface 覆盖 populated / invalid / empty、搜索与菜单;Playwright 在 `1280×720` 测量无页面级溢出。视频控制在有用时长内,清楚展示搜索、清除、菜单、状态反馈和项目打开结果,每一段都有可观察变化。
|
||||
- 关联:`apps/ai-game-creator-shell/src/features/app-shell/ProjectCreation.tsx`、`apps/ai-game-creator-shell/src/features/app-shell/model.ts`、`apps/ai-game-creator-shell/src/features/app-shell/useRecentProjects.ts`、`apps/ai-game-creator-shell/tests/appSurface/home.suite.ts`。
|
||||
|
||||
|
||||
- 现象:首波从单个美术任务扩展为三个 Director 后,hydration 若仍只容忍 seed lane 的第一个任务在 manifest 短暂恢复 `Pending` 时收束,另外两个已启动 Director 会被卡住。另外默认 `llm.stream=false` 下的专业 final reply 虽已由 finalization 提交,但后续阶段推进项目 revision 后,早期回复会从 Runtime 查询中消失。
|
||||
- 原因:hydration 例外把“首波”错误收窄成了单个固定或数组第一项任务;`visible_game_creator_agent_runtime_response_stream_at` 又把未提交流的 revision 新鲜度门误用到了已终态提交的 durable final reply。
|
||||
- 处理:从当前 root source 的 seed lane 动态解析全部零依赖首波任务,只对这些 child 容忍 hydration `Pending`,后续 code prototype / preview 仍严格要求 Running/Completed。`streaming / ready` 仍要求当前 revision,`committed` 回复改为依据 finalization 的稳定身份查询,不随后续项目 revision 失效。
|
||||
@@ -4865,7 +4862,6 @@
|
||||
- 现象二:图片预览已经读取真实宽高,但大量资源触发 LRU 淘汰后,卡片又退回 `180x128`,布局和连线随之跳动。原因是布局尺寸直接从可淘汰的 Blob/Object URL 预览缓存派生。处理时预览二进制仍按预算淘汰,但已验证的轻量 `pixelWidth/pixelHeight` 必须在当前项目、模式和资源 identity 作用域内独立保留;identity 或 scope 变化时再清理。
|
||||
- 验证:分别覆盖严格 edit body、错误分类、生成占位恢复,以及超过预览缓存条目上限后首张图片仍保持真实比例、布局碰撞和依赖端点不退化。
|
||||
|
||||
|
||||
# 2026-08-21 已有资源 ID 不等于可用于快速编辑的 canonical 来源
|
||||
|
||||
- manifest 中的 `source.resourceId` 可能指向历史按 `game-background` 等 Game Agent 私有 kind 登记的远端资源。只检查 ID 前缀并直接传给 `sourceReferenceId` 会在 External v1 入队前得到 `unsupported-source-kind`;本地 kind 映射只有在真正重新登记来源时才生效。
|
||||
@@ -4873,14 +4869,12 @@
|
||||
- 批量导入不能在安装首个媒体后继续执行带 `?` 的 ID、路径、层序或 revision 计算;这些步骤必须先完成。原子草稿写入返回错误后,回读失败属于提交结果未知,必须保留媒体并报对账错误,不能把回读错误压成“未提交”后删除可能已被草稿引用的文件。所有回滚删除失败也必须显式上报。
|
||||
- 失败 generation 同时存在私有 ledger 和 draft 投影,只在 React state 中 `filter` 会在重启后复活。删除 UI 必须调用只允许明确失败任务的后端归档操作,结果未知任务不能删除。
|
||||
|
||||
|
||||
## 运行中 generation 与 refine 来源身份不能按 create 链路处理(2026-08-21)
|
||||
|
||||
- 草稿 hydrate 后同步等待远端 generation recovery,会让生命周期长期停在 recovering,连平移和选图也被 inert。关键事务恢复与任务恢复必须拆开:前者先完成,后者后台推进且失败只进入任务/notice。
|
||||
- refine 提交若无条件校验 `source.resourceId == local-asset:<assetId>`,会拒绝本来合法的 `editor-resource-*` 已登记来源,并可能把事务卡在 `revision-installed`。创建与未登记本地资产才补 local identity;已登记 refine 必须保留并按解析后的来源身份回读。
|
||||
- 画布内部 absolute overlay 只能覆盖宿主网格,嵌在左右分栏时不会遮住整个窗口。阻断性失败必须 portal 到 `document.body`,并用 fixed inset 覆盖整个 WebView;测试应验证 portal 的直接宿主和 fullscreen modifier。
|
||||
|
||||
|
||||
## 自由画板 viewport 与资源 extent 分离(2026-08-24)
|
||||
|
||||
- 非空资源栏目使用无限画布:普通平移的 `x / y` 不按资源 extent 夹取,窗口 resize、媒体测量和资源 extent 变化也不得把用户 viewport 拉回内容边界;共享 `MIN_SCALE/MAX_SCALE` 只约束缩放比例。只有首次进入组合或用户显式复位时,才用真实卡片包围盒计算 fit。
|
||||
|
||||
@@ -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 请求提早进入 fallback,complex 失败。
|
||||
|
||||
@@ -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 origin;iframe 不带主站 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 sandbox;CSP;禁用 Service Worker;不授权敏感能力 |
|
||||
| 资产 | 主要威胁 | MVP 缓解 |
|
||||
| -------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| 主站 access token / cookie | 预览代码同源读取、XSS 窃取 | 独立 preview origin;iframe 不带主站 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 sandbox;CSP;禁用 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-only,Brotli 继续由 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-only,Brotli 继续由 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 receipt;pending 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 保留,新建 asset,source.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-committed,asset/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 保留,新建 asset,source.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-committed,asset/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;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -138,37 +138,37 @@ type HostBridgeEvent = {
|
||||
|
||||
首批 method:
|
||||
|
||||
| method | 用途 | Expo 壳 | Tauri 壳 |
|
||||
| --- | --- | --- | --- |
|
||||
| `host.getRuntime` | 返回宿主、平台、版本和能力清单 | 支持 | 支持 |
|
||||
| `appearance.getColorScheme` | 读取宿主当前配色模式 | 支持系统 Appearance 读取 | 支持窗口 theme 读取 |
|
||||
| `auth.requestLogin` | 打开宿主登录或账号绑定流程 | 未声明,继续走 H5 fallback | 未声明,继续走 H5 fallback |
|
||||
| `payment.request` | 发起宿主支付 | 未声明,等待真实 IAP / Google Play Billing / 国内渠道流 | 未声明,等待真实桌面支付渠道流 |
|
||||
| `share.setTarget` | 同步当前作品分享目标 | 支持 | 支持 |
|
||||
| `share.open` | 打开分享动作 | 支持系统分享面板 | 复制分享文本到剪贴板 |
|
||||
| `navigation.openNativePage` | 打开受控宿主页 | 支持同源 H5 route | 支持同源 H5 route |
|
||||
| `app.reloadWebView` | 受控刷新当前 WebView | 支持 WebView reload | 支持主窗口 reload |
|
||||
| `app.lifecycle` | 通知 H5 宿主前后台 / 焦点状态 | 支持 AppState 事件 | 支持窗口 focus / blur 事件 |
|
||||
| `navigation.canGoBack` | 通知 H5 宿主返回栈状态 | 支持事件 | 支持 H5 当前文档路由栈事件 |
|
||||
| `app.openExternalUrl` | 用系统浏览器打开外链 | 支持白名单协议 | 支持白名单协议 |
|
||||
| `app.setTitle` | 同步宿主窗口标题 | 不声明 | 支持 |
|
||||
| `app.setBadgeCount` | 设置应用 / 任务栏角标 | 仅 iOS 支持,需 Expo Notifications badge 权限和 `setBadgeCountAsync` 成功返回 | 支持;平台底层不支持时返回错误 |
|
||||
| `network.status` | 查询宿主网络状态 | 支持 Expo Network | 支持主站可达性短超时查询 |
|
||||
| `network.statusChanged` | 通知 H5 网络状态变化 | 支持 Expo Network 事件 | 桌面暂不声明,避免把 WebView online / offline 当作 Rust 桌面网络事实 |
|
||||
| `clipboard.writeText` | 写剪贴板 | 支持 | 支持 |
|
||||
| `clipboard.readText` | 读取纯文本剪贴板 | 支持 | 支持 |
|
||||
| `file.exportText` | 导出文本到用户选择的本地文件 | 支持系统分享 / 保存面板 | 支持系统保存对话框 |
|
||||
| `file.importText` | 导入用户选择的文本文件 | 支持系统文档选择器 | 支持系统选择文本文件 |
|
||||
| `file.importDocument` | 导入用户选择的文本 / DOCX 文档副本 | 支持系统文档选择器 | 支持系统选择文档文件 |
|
||||
| `file.exportImage` | 导出当前 H5 已持有的图片文件 | 支持系统分享 / 保存面板 | 支持系统保存对话框 |
|
||||
| `file.importImage` | 导入用户选择的图片文件 | 支持系统相册选择图片 | 支持系统选择图片 |
|
||||
| `file.captureImage` | 拍摄图片并导入当前 H5 流程 | 支持系统相机拍照 | 不声明 |
|
||||
| `scanner.scanQrCode` | 扫描二维码并回传文本结果 | 支持 Expo Camera 扫码 | 不声明,返回 unsupported |
|
||||
| `file.importAudio` | 导入用户选择的音频文件 | 支持系统文档选择器 | 支持系统选择音频文件 |
|
||||
| `file.exportAudio` | 导出当前 H5 已持有的音频文件 | 支持系统分享 / 保存面板 | 支持系统保存对话框 |
|
||||
| `file.imageDropped` | 通知 H5 桌面拖入图片 | 不声明 | 支持主窗口拖拽图片事件 |
|
||||
| `haptics.impact` | 轻量触感反馈 | 支持 | 不声明 |
|
||||
| `notification.showLocal` | 发送即时本地系统通知 | 支持 Expo Notifications | 支持 Rust 侧 Tauri notification |
|
||||
| method | 用途 | Expo 壳 | Tauri 壳 |
|
||||
| --------------------------- | ---------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `host.getRuntime` | 返回宿主、平台、版本和能力清单 | 支持 | 支持 |
|
||||
| `appearance.getColorScheme` | 读取宿主当前配色模式 | 支持系统 Appearance 读取 | 支持窗口 theme 读取 |
|
||||
| `auth.requestLogin` | 打开宿主登录或账号绑定流程 | 未声明,继续走 H5 fallback | 未声明,继续走 H5 fallback |
|
||||
| `payment.request` | 发起宿主支付 | 未声明,等待真实 IAP / Google Play Billing / 国内渠道流 | 未声明,等待真实桌面支付渠道流 |
|
||||
| `share.setTarget` | 同步当前作品分享目标 | 支持 | 支持 |
|
||||
| `share.open` | 打开分享动作 | 支持系统分享面板 | 复制分享文本到剪贴板 |
|
||||
| `navigation.openNativePage` | 打开受控宿主页 | 支持同源 H5 route | 支持同源 H5 route |
|
||||
| `app.reloadWebView` | 受控刷新当前 WebView | 支持 WebView reload | 支持主窗口 reload |
|
||||
| `app.lifecycle` | 通知 H5 宿主前后台 / 焦点状态 | 支持 AppState 事件 | 支持窗口 focus / blur 事件 |
|
||||
| `navigation.canGoBack` | 通知 H5 宿主返回栈状态 | 支持事件 | 支持 H5 当前文档路由栈事件 |
|
||||
| `app.openExternalUrl` | 用系统浏览器打开外链 | 支持白名单协议 | 支持白名单协议 |
|
||||
| `app.setTitle` | 同步宿主窗口标题 | 不声明 | 支持 |
|
||||
| `app.setBadgeCount` | 设置应用 / 任务栏角标 | 仅 iOS 支持,需 Expo Notifications badge 权限和 `setBadgeCountAsync` 成功返回 | 支持;平台底层不支持时返回错误 |
|
||||
| `network.status` | 查询宿主网络状态 | 支持 Expo Network | 支持主站可达性短超时查询 |
|
||||
| `network.statusChanged` | 通知 H5 网络状态变化 | 支持 Expo Network 事件 | 桌面暂不声明,避免把 WebView online / offline 当作 Rust 桌面网络事实 |
|
||||
| `clipboard.writeText` | 写剪贴板 | 支持 | 支持 |
|
||||
| `clipboard.readText` | 读取纯文本剪贴板 | 支持 | 支持 |
|
||||
| `file.exportText` | 导出文本到用户选择的本地文件 | 支持系统分享 / 保存面板 | 支持系统保存对话框 |
|
||||
| `file.importText` | 导入用户选择的文本文件 | 支持系统文档选择器 | 支持系统选择文本文件 |
|
||||
| `file.importDocument` | 导入用户选择的文本 / DOCX 文档副本 | 支持系统文档选择器 | 支持系统选择文档文件 |
|
||||
| `file.exportImage` | 导出当前 H5 已持有的图片文件 | 支持系统分享 / 保存面板 | 支持系统保存对话框 |
|
||||
| `file.importImage` | 导入用户选择的图片文件 | 支持系统相册选择图片 | 支持系统选择图片 |
|
||||
| `file.captureImage` | 拍摄图片并导入当前 H5 流程 | 支持系统相机拍照 | 不声明 |
|
||||
| `scanner.scanQrCode` | 扫描二维码并回传文本结果 | 支持 Expo Camera 扫码 | 不声明,返回 unsupported |
|
||||
| `file.importAudio` | 导入用户选择的音频文件 | 支持系统文档选择器 | 支持系统选择音频文件 |
|
||||
| `file.exportAudio` | 导出当前 H5 已持有的音频文件 | 支持系统分享 / 保存面板 | 支持系统保存对话框 |
|
||||
| `file.imageDropped` | 通知 H5 桌面拖入图片 | 不声明 | 支持主窗口拖拽图片事件 |
|
||||
| `haptics.impact` | 轻量触感反馈 | 支持 | 不声明 |
|
||||
| `notification.showLocal` | 发送即时本地系统通知 | 支持 Expo Notifications | 支持 Rust 侧 Tauri notification |
|
||||
|
||||
每个 method 都必须有明确 payload schema、超时、错误码和能力开关;H5 看到不支持时回退到现有浏览器路径。H5 发起原生壳请求的超时边界统一从 `packages/shared/src/contracts/hostBridge.ts` 读取:普通请求使用 `HOST_BRIDGE_DEFAULT_REQUEST_TIMEOUT_MS`,传入值最多裁剪到 `HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS`;宿主 runtime 回读使用 `HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS`;文件导入 / 导出、图片选择 / 拍摄和音频导入 / 导出使用 `HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS`;二维码扫码使用 `HOST_BRIDGE_SCANNER_TIMEOUT_MS`。H5 facade 不得在调用点手写 `30000` 或 `60000` 等毫秒字面量,根级 `check:native-shells` 与桌面壳配置检查会拦截回退。
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -49,16 +49,16 @@
|
||||
|
||||
Worker 任务记录和内部观测使用以下稳定 reason code:
|
||||
|
||||
| 阶段 | reason code |
|
||||
| --- | --- |
|
||||
| 翻译候选不合格 | `translation_invalid` |
|
||||
| 翻译 transport / upstream | `translation_upstream_failed` |
|
||||
| 翻译 / provider 预算耗尽 | `translation_budget_exhausted` / `elevenlabs_http_failed` |
|
||||
| ElevenLabs HTTP / timeout / body 读取 | `elevenlabs_http_failed` |
|
||||
| MIME、空 body、大小等音频门禁 | `invalid_audio` |
|
||||
| MP3 识别、帧读取、有限正时长或 600 秒门禁 | `duration_probe_failed` |
|
||||
| OSS PUT / HEAD | `oss_failed` |
|
||||
| asset object / bind 候选、原子项目资源 / 账号素材 / 画布写回 | `writeback_failed` |
|
||||
| 阶段 | reason code |
|
||||
| ------------------------------------------------------------ | --------------------------------------------------------- |
|
||||
| 翻译候选不合格 | `translation_invalid` |
|
||||
| 翻译 transport / upstream | `translation_upstream_failed` |
|
||||
| 翻译 / provider 预算耗尽 | `translation_budget_exhausted` / `elevenlabs_http_failed` |
|
||||
| ElevenLabs HTTP / timeout / body 读取 | `elevenlabs_http_failed` |
|
||||
| MIME、空 body、大小等音频门禁 | `invalid_audio` |
|
||||
| MP3 识别、帧读取、有限正时长或 600 秒门禁 | `duration_probe_failed` |
|
||||
| OSS PUT / HEAD | `oss_failed` |
|
||||
| asset object / bind 候选、原子项目资源 / 账号素材 / 画布写回 | `writeback_failed` |
|
||||
|
||||
reason code 不包含 endpoint、provider 原始正文或凭据。普通用户读取失败任务时仍只看到“音效生成失败,请稍后重试。”;原始诊断继续留在受控 Worker / tracing / 后台边界。
|
||||
|
||||
@@ -77,31 +77,31 @@ External v1 仍要求 `Idempotency-Key`。接受的 model 形态在 enqueue 前
|
||||
|
||||
## 验证结果
|
||||
|
||||
| 门禁 | 结果 |
|
||||
| --- | --- |
|
||||
| `platform-audio` 分层与 ElevenLabs loopback | `60/60` |
|
||||
| `platform-editor-agent` | `24/24` |
|
||||
| api-server SFX Prompt / 翻译 / Worker 组合矩阵 | `32/32` |
|
||||
| External v1 / OpenAPI / 幂等 | `13/13` |
|
||||
| external-generation Worker / compact / deadline | `31/31` |
|
||||
| api-server BGM 回归 | `35/35` |
|
||||
| SFX / BGM 前端提交、刷新、重绘、metadata | `14` 个文件、`451/451` |
|
||||
| `cargo check -p api-server --all-targets` | 通过;仅既有 dead-code warning |
|
||||
| `npm run typecheck` | 通过 |
|
||||
| `npm run check:spacetime-schema` | `137` 张表通过,确认无 schema diff |
|
||||
| `npm run check:encoding` | `5248` 个文件通过 |
|
||||
| `cargo fmt --all -- --check` | 通过 |
|
||||
| `git diff --check` | 通过 |
|
||||
| 门禁 | 结果 |
|
||||
| ----------------------------------------------- | ---------------------------------- |
|
||||
| `platform-audio` 分层与 ElevenLabs loopback | `60/60` |
|
||||
| `platform-editor-agent` | `24/24` |
|
||||
| api-server SFX Prompt / 翻译 / Worker 组合矩阵 | `32/32` |
|
||||
| External v1 / OpenAPI / 幂等 | `13/13` |
|
||||
| external-generation Worker / compact / deadline | `31/31` |
|
||||
| api-server BGM 回归 | `35/35` |
|
||||
| SFX / BGM 前端提交、刷新、重绘、metadata | `14` 个文件、`451/451` |
|
||||
| `cargo check -p api-server --all-targets` | 通过;仅既有 dead-code warning |
|
||||
| `npm run typecheck` | 通过 |
|
||||
| `npm run check:spacetime-schema` | `137` 张表通过,确认无 schema diff |
|
||||
| `npm run check:encoding` | `5248` 个文件通过 |
|
||||
| `cargo fmt --all -- --check` | 通过 |
|
||||
| `git diff --check` | 通过 |
|
||||
|
||||
本地运行态 smoke 使用独立临时数据库、临时 data dir 和 `18000–18004` 端口,未复用或修改原工作树正在运行的 `3000 / 3101 / 8082 / 8083` 服务。结果如下:
|
||||
|
||||
| 服务 | 地址 | 门禁 | 结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| SpacetimeDB | `http://127.0.0.1:18002` | `GET /v1/ping` | HTTP 200 |
|
||||
| BgFilter worker | `http://127.0.0.1:18004` | `GET /readyz` | HTTP 200,`ready=true` |
|
||||
| api-server | `http://127.0.0.1:18001` | `GET /healthz` | HTTP 200,`service=genarrative-api-server` |
|
||||
| Web | `http://127.0.0.1:18000` | `GET /` | HTTP 200 |
|
||||
| Admin Web | `http://127.0.0.1:18003/admin/` | `GET /admin/` | HTTP 200 |
|
||||
| 服务 | 地址 | 门禁 | 结果 |
|
||||
| --------------- | ------------------------------- | -------------- | ------------------------------------------ |
|
||||
| SpacetimeDB | `http://127.0.0.1:18002` | `GET /v1/ping` | HTTP 200 |
|
||||
| BgFilter worker | `http://127.0.0.1:18004` | `GET /readyz` | HTTP 200,`ready=true` |
|
||||
| api-server | `http://127.0.0.1:18001` | `GET /healthz` | HTTP 200,`service=genarrative-api-server` |
|
||||
| Web | `http://127.0.0.1:18000` | `GET /` | HTTP 200 |
|
||||
| Admin Web | `http://127.0.0.1:18003/admin/` | `GET /admin/` | HTTP 200 |
|
||||
|
||||
临时进程树停止后,`18000–18004` 五个端口均已释放。该 smoke 只证明本地进程、路由和临时 SpacetimeDB 模块可以启动,不包含真实 ElevenLabs / LLM / OSS 调用,也不代表生产配置或生产队列已验收。
|
||||
|
||||
|
||||
@@ -365,10 +365,22 @@ npm run check:spacetime-schema
|
||||
```
|
||||
|
||||
仓库根目录的 `rust-toolchain.toml` 固定 Rust `1.96.0` 并要求 `rustfmt` 组件,
|
||||
`rustfmt.toml` 固定 Edition 2024 的格式化口径。Rust 源码统一使用
|
||||
`cargo fmt --all --manifest-path server-rs/Cargo.toml` 格式化,并用
|
||||
`npm run check:rustfmt` 做只读校验;Codex 提交前门禁、API 生产构建和
|
||||
SpacetimeDB module 生产构建都会执行同一检查,避免不同开发机或构建节点反复产生格式差异。Web 生产构建还会执行 production-ops、ESLint、主站与后台类型检查,以及排除已下线旧玩法后的当前 Vitest;API 生产构建追加 production-ops、DDD/schema/runtime-access 和 api-server 全 target 编译检查;SpacetimeDB module 生产构建追加 production-ops、DDD/schema/runtime-access 和管理员 procedure smoke。上述门禁由 `npm run check:production-ops` 反查,不能只保留在本地说明中。对需要跨格式保持稳定的脚本片段,门禁按去除空白后的源码片段匹配,避免仅因换行或格式化差异误报。
|
||||
`rustfmt.toml` 固定 Edition 2024 的格式化口径。Rust 源码分属两个独立 Cargo
|
||||
workspace,必须分别执行以下格式化命令:
|
||||
|
||||
- `cargo fmt --all --manifest-path server-rs/Cargo.toml`
|
||||
- `cargo fmt --all --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`
|
||||
|
||||
统一只读入口
|
||||
`npm run check:rustfmt` 会按同样顺序对两个 manifest 执行 `-- --check`。Codex
|
||||
提交前门禁、API 生产构建和 SpacetimeDB module 生产构建都会执行同一检查,避免
|
||||
不同开发机或构建节点反复产生格式差异。Web 生产构建还会执行 production-ops、
|
||||
ESLint、主站与后台类型检查,以及排除已下线旧玩法后的当前 Vitest;API 生产构建
|
||||
追加 production-ops、DDD/schema/runtime-access 和 api-server 全 target 编译检查;
|
||||
SpacetimeDB module 生产构建追加 production-ops、DDD/schema/runtime-access 和管理员
|
||||
procedure smoke。上述门禁由 `npm run check:production-ops` 反查,不能只保留在本地
|
||||
说明中。对需要跨格式保持稳定的脚本片段,门禁按去除空白后的源码片段匹配,避免仅
|
||||
因换行或格式化差异误报。
|
||||
|
||||
## 前端改动验收
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user