Compare commits

...

223 Commits

Author SHA1 Message Date
lhk229 6ed1fd26ed 修复 Linux 沙箱命令退出回收的偶发竞态
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / Backend tests (pull_request) Successful in 3m49s
Project CI / Frontend tests (pull_request) Successful in 1m53s
Project CI / Native shell tests (pull_request) Successful in 5m51s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m57s
Project CI / Repository checks (pull_request) Successful in 1m55s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m24s
Project CI / AI game creator shell Rust crates (push) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m9s
Project CI / Backend tests (push) Successful in 4m49s
Project CI / Frontend tests (push) Successful in 2m9s
Project CI / Native shell tests (push) Successful in 6m54s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m56s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 10m14s
Project CI / Repository checks (push) Successful in 2m2s
Project CI / AI game creator shell web tests (push) Successful in 1m30s
统一正常退出、取消和超时的有界进程组退出确认,保留归属校验
提前记录启动身份,修补终态协议错误的清理及输出任务回收
补充确定性竞态回归并同步运行时规范和排障记忆
保持现有 CI 并行、分片和重试策略不变
2026-09-24 16:45:07 +00:00
lhk229 4912aa4df0 删除创作首页登录送泥点营销文案
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m34s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m14s
Project CI / Backend tests (pull_request) Successful in 3m51s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m40s
Project CI / Frontend tests (pull_request) Successful in 2m0s
Project CI / Native shell tests (pull_request) Successful in 5m52s
Project CI / Repository checks (pull_request) Successful in 2m8s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m50s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m17s
删除 CreationLandingView hero 区“登录即送 100 泥点,可以免费制作 50 个素材”文案
删除 CreationLandingView.test.tsx 中对应的文案断言
清理 index.css 中已无调用方的 creation-landing__hero-benefit 样式
更正 decision-log 中已过期的“注册赠送固定为 100 泥点”决策记录,注明金额由线上钱包配置决定
2026-09-24 16:08:42 +00:00
k88936 34eee8ec6a Merge pull request 'ui editor 页面优化' (#337) from feat/ui-editor-v3 into master
Project CI / AI game creator shell Rust crates (push) Successful in 1m13s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m52s
Project CI / Backend tests (push) Successful in 4m42s
Project CI / Native shell tests (push) Successful in 6m4s
Project CI / Frontend tests (push) Successful in 2m7s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m39s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m6s
Project CI / AI game creator shell web tests (push) Successful in 1m28s
Project CI / Repository checks (push) Successful in 1m53s
Reviewed-on: #337
2026-09-24 19:45:46 +08:00
k88936 fecb8fe76f Merge remote-tracking branch 'origin/master' into feat/ui-editor-v3
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m25s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Successful in 4m44s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m40s
Project CI / Native shell tests (pull_request) Successful in 6m11s
Project CI / Frontend tests (pull_request) Successful in 2m27s
Project CI / Repository checks (pull_request) Successful in 2m50s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m44s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m38s
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
2026-09-24 19:43:20 +08:00
k88936 bdab414d32 Merge pull request '重构/对话错误类型化, 避免string-typed' (#474) from feat/fail-as-event into master
Project CI / AI game creator shell Rust crates (push) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m57s
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Reviewed-on: #474
2026-09-24 19:42:49 +08:00
k88936 2fa89006c4 Merge remote-tracking branch 'origin/master' into feat/fail-as-event
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m39s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m53s
Project CI / Backend tests (pull_request) Successful in 5m10s
Project CI / Native shell tests (pull_request) Successful in 6m36s
Project CI / Frontend tests (pull_request) Successful in 2m6s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m8s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m2s
Project CI / Repository checks (pull_request) Successful in 1m53s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m24s
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
2026-09-24 19:17:48 +08:00
suzmii 96b0fada3a Merge pull request 'AGC ACL 提权修复按目标做 single-flight,避免并发重复弹 UAC(#498)' (#502) from fix/acl-elevation-single-flight into master
Project CI / AI game creator shell Rust crates (push) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m7s
Project CI / Backend tests (push) Successful in 5m6s
Project CI / Native shell tests (push) Successful in 6m39s
Project CI / Frontend tests (push) Successful in 2m14s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m0s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 10m12s
Project CI / AI game creator shell web tests (push) Successful in 1m36s
Project CI / Repository checks (push) Has been cancelled
Reviewed-on: #502
2026-09-24 19:15:34 +08:00
suzmii a9f57c98ce Merge pull request 'AGC 模型目录改为启动期从上游同步,退役写死的 gpt-6-astra/luna 与别名(#506)' (#507) from enhance/default-model into master
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Reviewed-on: #507
2026-09-24 19:15:22 +08:00
k88936 7cb7bd9581 合并:把 origin/master 的对外 MCP 语义工具与状态条、Markdown 修复并进接单化分支
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- 冲突(`chat-composer.suite.ts` 接单窗口用例)按双方意图合并:保留上游「窗口期就要显示处理中卡片」,同时保留本分支「卡片要等宿主 `turn.started.at` 才开始读秒」;用例改为窗口期断言卡片在、无「已耗时」,再补发 `turn.started` 与开口用户条目后才断言气泡与读秒出现。
- 补上运行中回合的起点链路(合入前只有收口条目带起点,运行中卡片读不到秒):`useDirectThreadChatSubscription` 暴露 `turnStartedAt`,控制器透传 `directTurnStartedAt`,`DirectProjectChatView` 交给 `buildDirectChatTurns`。
- 同步 ADR、实施计划、`decision-log`、`pitfalls` 的口径与注释(两态投影;接单窗口只有卡片且不读秒)。
- 其余上游变更直接并入:对外 OpenAPI / MCP 语义工具方案与实现、状态条读秒粒度与几何、对话 Markdown 容错。
2026-09-24 19:13:53 +08:00
k88936 065b4ea330 给 UI 设计文档三个工具补上可读的回执明细与入参摘要
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
- 三个工具的持久回执补白名单分支:只放身份、相对路径、计数与回填说明,不再整块变成明细不可用
- 身份字段禁控制字符并限长,相对路径必须归一化后落在 ui/ 下,含宿主路径或 file URI 即失败关闭
- 回填说明按既有口径把绝对路径脱敏成占位符,超长或带控制字符的整条明细失败关闭
- 明细总长仍守 500 字符上限:清单能放多少放多少,放不下的部分用总数表达
- 入参摘要给出设计图逐张身份或目标文档 id,并把三个工具加进可读名单,不再只报哈希
- 补四条用例覆盖可读明细、脱敏、越界路径失败关闭与入参摘要
2026-09-24 19:08:18 +08:00
k88936 74609e2a5e 把 UI 设计文档三个工具算进项目变更门禁
- 项目变更判定与项目版本号推进判定补上 UI 设计文档的三个工具:成功返回即算改过项目
- 只重写派生产物的代码生成工具不推进项目版本号,避免虚报推进量、把"项目被别处改动"的误报再造出来
- 新建文档与跑工作流这两个工具在观察明细里报出真实推进格数,失败路径也报,避免切图素材已登记却漏计
- 非法人写的读明细逻辑收敛成一个helper,三处调用共用
- 补六条门禁用例覆盖成功即算、只出代码不算推进、真实推进量优先、失败但已推进才算
- 同步共享记忆的项目概览与决策记录
2026-09-24 18:58:12 +08:00
k88936 1ff1a9965b 前端:删掉 Direct 的本地乐观用户气泡,用户气泡只来自宿主条目
- controller 删 `pendingUserItemId` / `beginTurnCommand` / `endTurnCommand`:忙态改由 `beginTurnBusy` / `endTurnBusy` 持有,宿主认领判据 = `turnRunning` 或收口计数变过(一轮在同一次 consume 里开始并结束)
- controller 删 `startTurn` 的乐观追加与 `messageAppended` 重跑参数、`DirectProjectTurnInput.messageText` 与首轮的 `directInitialTurnText`;controller 不再需要 `assets`
- 投影删 `awaiting-start` 展示态(只剩 `running` / `finished`)、`localSentTimes` / `sameIdentitySentAt`、本地用户气泡与它开回合的路径;带身份的拒单提示在会话末尾自成一组,不挂进上一轮,也不开运行态标记
- 时间口径:起点只认 `turn.started.at`、终点只认 `turn.completed.at`,用户气泡时钟取宿主落盘 / 观测时间;两边都空的回合整条「本轮结束于 … 」隐藏,不再出现 0.0 秒
- 用例:改造 `directTurnPresentation`(本地用户消息不进回合、拒单提示自成一组、两态判据、失败说明按身份归位)、`directProjectTurn` / `directProjectTurnStatus` / appSurface 窗口期用例,`directProjectTurn` 补 `afterEach(cleanup)`
- 注释与文档:ADR「命令接单化」后续更新、实施计划新增「删掉本地乐观用户气泡」、decision-log 与 pitfalls 同日条目、Codex 原始历史方案的口径句、`codex_app_server` 用户条目时间注释
2026-09-24 18:20:24 +08:00
k88936 565c2277d5 恢复工作流时只读快照,不再重放已完成步骤的改动
- 检查点每步除记录结果外,再落一份「该步应用完之后」的状态快照,切分那步另带回填出错说明,旧日志无快照仍可解析
- 主流程只应用本次新完成那一步的改动,已完成步骤整步跳过,不再重复登记切图或重复累加回填说明
- 崩溃恢复改为只读检查点里的状态快照并逐级取回,删掉按记录结果重算目标状态的旧实现
- 只有结果、没有状态快照的旧检查点行按未完成处理,由主流程重跑该步
- 漂移与写回幂等判据改为对照快照:状态既不是本轮起点、也不是切分后那份才算漂移;文档已等于切分后快照即视为写回完成
- 同步 ADR、技术方案、代码地图与共享记忆里的检查点口径
2026-09-24 18:16:34 +08:00
suzmii 29c5409ac4 Merge remote-tracking branch 'origin/master' into fix/acl-elevation-single-flight
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m4s
Project CI / Backend tests (pull_request) Successful in 3m49s
Project CI / Frontend tests (pull_request) Successful in 1m57s
Project CI / Native shell tests (pull_request) Successful in 5m48s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m59s
Project CI / Repository checks (pull_request) Successful in 1m50s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m21s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m36s
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-24 18:16:15 +08:00
k88936 d6f2ae157a 文档:回合顺序修复写进 ADR、实施计划与共享记忆
- ADR:补「开口用户条目先于整轮里的一切失败」这条顺序不变式(发点在接单 + 落盘之后、起 codex 之前),以及界面「回合归属只认身份」的口径
- 实施计划:新增「回合顺序修复(2026-09-24)」一节,写清现场、根因、两条改动与回归用例
- decision-log:新增同日决策(宿主发点提前 + 前端按身份归位、收口早退不吞说明)
- pitfalls:新增同日条目,并记下排查提示——先分清逻辑回合的 turn.started / turn.completed 与 app-server 协议的 turn/start 请求
2026-09-24 18:02:50 +08:00
k88936 4a75de1c31 前端:失败说明按回合身份归位,不再落进上一轮、气泡也不再自成假回合
- 回合归属改成按身份(开口用户条目的 canonical itemId):失败说明条目带 turnUserItemId(reducer 写,缺身份时保持原顺序语义),buildDirectChatTurns 按身份分组,同一身份的条目永远同一轮
- 本轮开口条目还没到(回合在宿主下发条目之前就失败、或历史切片还没读回)时,本地乐观气泡按身份挂回自己那一轮,不再另开一轮:界面不再出现「错误显示在用户消息上面」+「气泡底下 0.0 秒」+「上一轮借走本轮终点(15.6 秒)」这一组现象
- 收口早退不再吞掉还没写进界面的失败说明(订阅重建后的 bootstrap 只回放生命周期锚点):只补说明、终点时间与「回合完成」计数,不重开回合、不动本轮起点 / 终点 / 身份
- 用例:directTurnPresentation 复现现场(两个回合、说明与气泡同段、耗时不再借上一轮的终点);directThreadChat 补身份字段与早退不吞说明两条
2026-09-24 18:02:43 +08:00
k88936 2487a2c8e6 宿主:开口用户条目的发点提前到接单之后,失败说明才能挂回自己那一轮
- 本轮开口用户条目(item_completed,direct-codex:{clientTurnId}:user)原来在 app-server turn/start 应答之后才下发;接单到 turn/start 之间的失败(连不上 app-server、执行器未通过验收、历史注入失败)走不到那一步,事件流里只有逻辑回合的一对事件,没有开口条目
- 把那段内联下发抽成 emit_direct_thread_user_item,发点提前到「接单成立、用户条目落盘成功、起 codex 之前」(direct_runtime/user_input.rs 的命令主体),并删掉 turn/start 之后那一处:线上仍只有一处下发,不变式变成「接单 → 开口用户条目 → 整轮里其余一切」
- 新增回归用例 the_opening_user_item_is_emitted_before_anything_that_can_fail_in_the_turn:断言行首两条事件是带身份的 turn.started 与开口用户条目,终态只能在它们之后
- 回显过滤用例补上同一发点的模拟步骤(生产入口的两个动作:落盘 + 下发)
2026-09-24 18:02:26 +08:00
suzmii 997eba2b7b Merge branch 'master' into enhance/default-model
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m35s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / Backend tests (pull_request) Successful in 4m40s
Project CI / Native shell tests (pull_request) Successful in 6m13s
Project CI / Frontend tests (pull_request) Successful in 2m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m13s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m55s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m34s
Project CI / Repository checks (pull_request) Successful in 1m58s
2026-09-24 18:02:01 +08:00
k88936 62b0f103de 把 ui-design-doc 的阻塞 IO 挪出 async 执行器
src-tauri/src/agent/runtime_tools/ui_design_doc.rs 的 from-images 与 into-js 观察函数改为 async,核心逻辑用 tokio::task::spawn_blocking 执行:from-images 要逐张 image::open 解码设计图再写 manifest,into-js 要读整份文档、算 SHA-256 再落盘 JS,原先都在 async 执行器线程上同步跑,会和其它已派发的动作抢执行器

阻塞任务未完成(join 失败)时返回 status=error 的观察,沿用仓库既有的 spawn_blocking 失败兜底写法

src-tauri/src/agent/runtime_actions/action_execution.rs 两个分发分支改为 await;ui_design_doc.rs 的两个用例改成 #[tokio::test] 并 await
2026-09-24 17:47:58 +08:00
k88936 4046318118 先整批校验设计图再登记,避免留下孤儿资源
src-tauri/src/ui_editor/agent_tools/creation.rs 把 prepare_design_image 拆成只读的 resolve_design_image 与登记两步:旧实现边校验边 register_local_asset_at,批次里后面某张设计图格式/尺寸不合法或路径缺失时,前面已登记的图片会留在 manifest.json 里且没有任何文档引用,而 rollback_created_document 只覆盖更晚的初始化失败

同批次查重改为按解析出的身份(已登记图片用 assetId、待登记图片用归一化路径)在登记前判定,重复引用不再先写一次 manifest

同文件新增 ResolvedDesignImage 承载「已登记 / 本次待登记」两种解析结果,待登记项在第二阶段登记后才拿到 assetId

src-tauri/src/tests/project.rs 新增 create_ui_design_doc_from_images_leaves_no_orphan_assets_when_a_later_image_fails,覆盖「第二张缺图时第一张不得进 manifest」(已用回退实现的失败结果反证过)
2026-09-24 17:44:19 +08:00
k88936 cd196cf61c 宿主:开发构建跳过 Codex 执行器版本门禁
- codex_app_server 逐次审批门禁与 direct_execution 补丁执行器门禁改为按 profile 分流:发行构建仍要求严格等于捆绑侧车固定版本,开发构建(debug_assertions)直接通过
- 修正开发态必然被拒的问题:开发构建从宿主 PATH 解析到的 Codex(本机 codex-cli 0.156.0)与固定版本 codex-cli 0.155.1 不等,且 Linux 与未 stage 侧车时没有可选固定版本,导致 Direct 回合在建连前就被拒
- 发行构建的拒单文案补上期望版本与实际版本,便于排障
- 同步调整受影响的单测:开发构建断言跳过门禁,发行构建断言仍拒绝版本漂移
2026-09-24 17:42:25 +08:00
suzmii c07c10c0c7 Merge pull request '修复 DirectProject 对话状态条时机与被挤扁、Markdown 代码块换行与围栏解析' (#511) from fix/recover-100ms into master
Project CI / AI game creator shell Rust smoke (push) Successful in 1m20s
Project CI / AI game creator shell Rust crates (push) Successful in 1m3s
Project CI / Backend tests (push) Successful in 3m55s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m35s
Project CI / Frontend tests (push) Successful in 2m19s
Project CI / Native shell tests (push) Successful in 6m5s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m50s
Project CI / Repository checks (push) Successful in 1m54s
Project CI / AI game creator shell web tests (push) Successful in 1m21s
Reviewed-on: #511
2026-09-24 17:36:38 +08:00
suzmii 2e45608f68 闸门补 leader 失效接管,避免卡死目标永久失败关闭
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m25s
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m6s
Project CI / Frontend tests (pull_request) Successful in 2m10s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m28s
Project CI / Native shell tests (pull_request) Successful in 5m38s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m9s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m38s
- acl_repair_gate:策略新增 leader_deadline(默认 5 分钟),超过后新调用接管仍是 running 的 key;Entry 记 started_at + leader_id,被接管后旧 leader 迟到的结果按令牌丢弃,不覆盖接管者写下的结果
- acl_repair_gate:complete()/Drop 改为按令牌就地更新(不再无条件 insert),panic 兜底与接管语义保持一致
- acl_repair_gate:running 状态的 entry 不再写 recorded_at(那时还没有结果),冷却基准只在真正落库时记录
- tests/acl_repair_gate:新增 stale_leader_is_taken_over_and_its_late_result_is_discarded;临时关掉接管分支即红(逆向确认:Reused(Failed(...)) 而非 Executed(Repaired))
- docs:decision-log 记 leader 失效接管决策,pitfalls 把「已知残余边界」改成已兜底说明,验证清单补该用例
2026-09-24 17:28:38 +08:00
k88936 56760a7397 让 Node 类型绑定回到 ts-rs 生成结果
src/features/ui-editor/types/Node.ts 补回末位逗号:该目录在 .prettierignore 内,文件内容就是 ts-rs 直出,46f859237 手工补 offset 字段时漏掉这个逗号,导致每次 cargo test 跑导出后这个文件都会以「已修改」状态留在工作区
2026-09-24 17:25:28 +08:00
k88936 ec9a4fcbac 给节点 offset 补上有限数值校验
src-tauri/src/ui_editor/persistence.rs 的 validate_node 现在校验 node.offset.min/max 必须是有限数值,与相邻的 Transform/custom_minimum_size 校验一致;此前只有 Transform 的 anchor/offset 被校验,NaN 或 Infinity 的树级偏移可以存盘,预览按 offset.min 定位时整棵树的渲染会一起失效

同文件新增 rejects_non_finite_node_offset,覆盖「offset 全有限可保存、min 变成 Infinity 后校验失败」
2026-09-24 17:25:03 +08:00
lhk229 a07bff85ec 升级mcp,增加按语义分类的工具。旧工具不变 (#493)
Project CI / AI game creator shell Rust crates (push) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m0s
Project CI / Backend tests (push) Successful in 3m45s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 6m30s
Project CI / Frontend tests (push) Successful in 1m53s
Project CI / Native shell tests (push) Successful in 5m50s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m15s
Project CI / Repository checks (push) Successful in 1m58s
Project CI / AI game creator shell web tests (push) Successful in 1m27s
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/493
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-24 17:19:45 +08:00
suzmii 26eb32ea22 按 review 收口状态条与对话 Markdown 的容错
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m7s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m26s
Project CI / Backend tests (pull_request) Successful in 3m51s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m10s
Project CI / Native shell tests (pull_request) Successful in 6m3s
Project CI / Frontend tests (pull_request) Successful in 2m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m37s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m26s
Project CI / Repository checks (pull_request) Successful in 1m52s
- apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx 围栏归一化补「同一行出现第二段围栏串就跳过」判据:行内代码 `文本 ```x``` ` 的末尾那截曾被当成收场围栏拆开,凭空造出一个开场围栏、把后面的正文全变成代码
- apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx 注释补归一化顺序与行内代码判据
- apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectTurnStatus.ts 修正 displayBusy 的说明:原注释仍写着卡片只读 nativeRunning,与现状相反
- apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx 新增行内代码用例(去掉新判据即红)
- docs/project-memory/shared-memory/decision-log.md 新增 2026-09-24 条:记录卡片口径翻转(更正 2026-09-22「卡片口径取保守」那条)并处置其「running 在渲染层就没有消费者」的预言,同时记录状态条几何约束与对话 Markdown 容错口径
- docs/project-memory/shared-memory/pitfalls.md 围栏归一化条补行内代码判据
2026-09-24 16:56:49 +08:00
k88936 8ff155e14f 文档:接单化 review 收口第二轮的剩余两条写进 ADR、实施计划与共享记忆
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m4s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m25s
Project CI / Backend tests (pull_request) Successful in 4m0s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m1s
Project CI / Frontend tests (pull_request) Successful in 2m7s
Project CI / Native shell tests (pull_request) Successful in 6m3s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m47s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m12s
- ADR §1 补"接单之后的一切失败都回 `Ok(())`"、§2 补"失败事实先于看门狗可见",并说明落盘失败不继续起整轮
- 实施计划第二轮小节补命令返回值口径与连接死亡的记录顺序,点名看门狗回归用例
- 共享记忆同日条目的"两条待决策"转成决策,验证计数更新为 902 passed / 5 ignored
2026-09-24 16:53:47 +08:00
k88936 cd5feac5fe 宿主:接单之后的落盘失败不再从命令返回 Err
- `chat_with_game_creator_direct_codex_typed` 在接单后的历史追加写失败时仍写失败终态,但返回 `Ok(())`:命令的 `Err` 只表示拒单,同一个失败不该从事件与横幅两条通道下发,前端也不该把已经开始的回合读成没开始
- 不继续起整轮:`project.jsonl` 是这条对话的单一事实源,用户消息没落盘时继续跑只会得到一条没有开口用户消息的助手回复
- 补 Rust 用例 `a_history_write_failure_after_accept_closes_the_turn_instead_of_rejecting`:借历史追加写的测试注入钉住恰好一条失败终态、不带拒单收口文案、占用已释放
- 补前端用例:落盘失败的说明只来自事件且恰好一条,忙态放掉,下一条能直接发出去
2026-09-24 16:51:44 +08:00
k88936 ab970b9fdb 宿主:连接死亡的失败事实先于看门狗落地
- `CodexAppServerInner` 新增私有去重标志 `connection_end_claimed`,与 `closed` 分开:认领只保证死亡收口只跑一次,"看门狗可以开始收束"必须等失败事实写进执行适配器
- `fail_game_creator_codex_app_server_connection` 改用新标志去重,不再顺带置 `closed`;`closed` 交给 `shutdown_game_creator_codex_app_server_inner` 在 `record_execution_turn_failure` 之后置位,看门狗在事实落地前没有可观测信号
- 补一条把看门狗真正跑起来的回归用例 `connection_death_records_the_failure_fact_before_the_watchdog_seals_the_turn`:卡住 stderr 摘要锁把窗口拉成确定性,断言终态仍带 `transport-failed` 载荷(顺序反了就红)
- 失败事实是在模型终态那一刻被快照进终态上下文的,晚补记无用,所以只修"事实先于可见性"这一条落点
2026-09-24 16:51:36 +08:00
suzmii 20a0d08772 记录聊天 Markdown 围栏归一化的排障经验
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m31s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m0s
Project CI / Backend tests (pull_request) Successful in 4m2s
Project CI / Native shell tests (pull_request) Successful in 5m54s
Project CI / Frontend tests (pull_request) Successful in 2m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m36s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m5s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m49s
Project CI / Repository checks (pull_request) Successful in 2m31s
- docs/project-memory/shared-memory/pitfalls.md 新增一条:模型输出会把 ``` 粘在正文行末尾,CommonMark 只认整行围栏,故必须先归一化再解析;同时记录块级 pre 与 code 都要给换行类名
2026-09-24 16:40:22 +08:00
suzmii 0daf8cdaad 修复 DirectProject 对话状态条时机与 Markdown 代码块渲染
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx 状态条改为读 DirectProjectTurnStatus.displayBusy(本地命令在飞 ∪ 原生在跑),计时起点放宽到最新一个未结束回合:原先只认原生 turnRunning,而 turn.started 要等宿主应答返回才发出,模型首 token 之前那约十秒界面完全没有「正在处理」的交代
- apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx 入参 nativeRunning 更名 turnInFlight 并同步注释
- apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx 块级 pre 与 code 补 whitespace-pre-wrap + break-words:窄面板里长行不再把消息拉宽、不再顶出横向滚动条
- apps/ai-game-creator-shell/src/components/ChatMarkdownMessage/index.tsx 新增 normalizeMarkdownFences:把粘在正文行里的 ``` 拆到独立行(模型常写成「…实现细节(game.js):```js」「… }```」),CommonMark 只认整行围栏,粘着的围栏会让正文被当成代码、或代码块不闭合把后续内容一起吞掉;整行/缩进围栏、行内代码、代码里的 ``` 与引用块 / 列表项开头的合法围栏都不受影响
- apps/ai-game-creator-shell/tests/ChatMarkdownMessage.test.tsx 新增粘住围栏三种场景与代码块换行契约用例
- apps/ai-game-creator-shell/tests/appSurface/chat-composer.suite.ts 补「turn.started 未到时卡片已出现且已计时」断言
- apps/ai-game-creator-shell/tests/directProjectProcessStatus.test.tsx 同步入参更名
2026-09-24 16:38:44 +08:00
k88936 246059af4c 让工作流步骤标签用例跟随步骤目录
tests/workflowCompletionNotice.test.ts 改为遍历 UI_EDITOR_STEPS 逐一比对 workflowStepLabel(step.id) 与 step.label,断言里写死的 reference-analysis 步骤已在「前端移除分析参考图步骤与界面图元编辑」中退役,导致用例在 HEAD 上必然失败
2026-09-24 16:25:21 +08:00
k88936 2f5e0b0ede 文档:接单化 review 收口第二轮写进 ADR、实施计划与共享记忆
- ADR 的后续更新补第二轮:§4 的拒单载荷 `kind` 收成 typed 枚举与并发拒单身份改成回合身份、§5 的回合身份口径覆盖拒单载荷、§6 的可留痕判据收掉 `ProjectRootUnanchored`、§7 的"同级提示"补上认不出的拒单
- 实施计划加「review 收口第二轮(2026-09-24)」一节,记下拒单表与界面提示口径的现状
- 决策记录追加同日第二条:五条决策、明确不做、两条待决策(连接收束时序、接单后落盘失败的双通道)与影响范围 / 验证证据
2026-09-24 16:24:38 +08:00
k88936 85d69a29a9 前端:认不出的拒单也在聊天里补一条同级提示
- 新增 `directTurnUnrecognizedRejectionNoticeText`:宿主 / 环境事实的拒单在聊天里的文案取宿主收口文案的脱敏摘要与建议(`projectRuntimeVisibleRejectionError`),不是收口形状时只给一句通用兜底,机器字段不进聊天
- 控制器在「认不出的拒单」分支补写一条与用户消息同级的提示(沿用 `directTurnRejectionNoticeMessageId` 身份):拒单不产生 `turn.completed`,这条乐观用户气泡后面不会再有事件来解释它;上报与横幅照旧保留
- 修正该处注释「聊天里的失败说明不由这里写」——那条只对回合失败成立,拒单没有终态出口;同时把非结构化错误继续只走横幅的理由写清楚
- 用例:`chat-composer.suite.ts` 补一条结构化拒单的界面用例(同级提示可见、`direct-codex-failure` / `stage=` 不进聊天、忙碌态放掉);`project-conversation.suite.ts` 补该文案函数的单元断言
2026-09-24 16:21:35 +08:00
suzmii 7cdbc61ffb 修复窗口变矮时对话状态条被挤扁
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m37s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m9s
Project CI / Backend tests (pull_request) Successful in 3m48s
Project CI / Frontend tests (pull_request) Successful in 2m1s
Project CI / Native shell tests (pull_request) Successful in 5m48s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m34s
Project CI / Repository checks (pull_request) Successful in 2m5s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m48s
- apps/ai-game-creator-shell/src/styles.css 给 .project-chat-conversation > .project-chat-process-card 补 flex: 0 0 auto:卡片带 overflow: hidden,按 flex 规范该项自动最小尺寸归零,窗口压矮时会先被压缩(实测 300px 高挤到 33px、240px 高 24px,文字被裁),而这一列里只有消息列表该被压缩
- apps/ai-game-creator-shell/src/styles.css 把这条卡片的会话列几何规则从文件末尾移到「面板纵向布局兜底」的 flex 链旁边,内缩与不可压缩合并在同一条规则里,注释说明为什么必须是 0 0 auto
- apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts 卡片契约补 flex: 0 0 auto 断言
2026-09-24 16:21:24 +08:00
k88936 b42966eb9e 前端:Direct 失败说明补上宿主事实句的文案模式
- `projectRuntimeVisibleError` 增加宿主 `Display` 事实句模式:`执行通道已断开`(TransportClosed)、`等待模型回合结束达到硬上限`(TimedOut 的硬上限那档)、`宿主任务提前结束`(host-dropped),并给落盘那档补上 `收尾历史失败` / `未确认历史完整落盘` / `写入本项目对话历史失败`——改动前这几种都掉进「执行失败,请稍后重试」
- 只认宿主写死的短语、不回落原文:原文带 `exitStatus=` / `stderrClass=` 这类内部字段,`TransportClosed` 就是这种
- 修掉宿主收口文案的版本口径:解析只认 `v1`,而宿主发的是多一段 `code=` 的 `v2`,于是脱敏摘要永远命中不了;现在两版都认,并把解析结果拆成 parts,供拒单文案复用(`projectRuntimeVisibleRejectionError`,不带阶段标签——拒单这一轮没有开始)
- `directTurnFailureNoticeText` 的文档注释写明「不加模式就只会看到通用文案」是有意取舍,加模式时补 `agentRuntimeModel.test.ts` 用例
- 用例:`agentRuntimeModel.test.ts` 补 v2 收口文案、拒单文案与三句宿主事实句;`directThreadChat.test.ts` 的「收尾历史失败」期望改成映射后的句子
2026-09-24 16:19:37 +08:00
suzmii 8514f1094c 忽略嵌套的 server-rs/.data 运行期产物
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m33s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 53s
Project CI / Backend tests (pull_request) Successful in 4m24s
Project CI / Frontend tests (pull_request) Successful in 2m12s
Project CI / Native shell tests (pull_request) Successful in 6m2s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m52s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m1s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m55s
Project CI / Repository checks (pull_request) Successful in 2m34s
- 以 crates/api-server 为 cwd 启动时会生成 server-rs/crates/api-server/server-rs/.data,补充 **/server-rs/.data/ 忽略规则
2026-09-24 16:18:09 +08:00
suzmii f04d9ae3f5 AGC 模型弹层去掉滚动条并按最长模型名自动拓宽
- .conversation-model-menu 宽度改为 max-content(保留 min-width 150px 与视口宽度上限),不再用固定 150–190px 截断
- 去掉 max-height/overflow 滚动,菜单按目录项展开
2026-09-24 16:18:08 +08:00
suzmii 398564a9f8 AGC 客户端不再写死 gpt-6-astra 默认模型
- main.rs:DEFAULT_GAME_CREATOR_LLM_MODEL 改用官方占位标识 platform-default
- game-creator.config.json:首启模板 llm.model 同步为 platform-default
- check-config.mjs 与客户端测试同步该口径,去掉已退役模型名的断言
2026-09-24 16:18:07 +08:00
suzmii 4234a21eed 服务端:AGC 模型目录缺配置时改为启动期从上游同步,保留原目录格式
- module-runtime:删除写死的初始目录(高质量→gpt-6-astra、快速→gpt-5.6-luna),新增 from_upstream_models:id 用模型名 slug、alias/modelId 用上游原名、默认项取排序后第一项;字段与校验规则不变
- spacetime-module:read_agc_model_catalog 缺行返回 AGC_MODEL_CATALOG_NOT_INITIALIZED,不再返回内置目录
- api-server:新增启动期 ensure_agc_model_catalog_initialized,未初始化时从分组定价列表 GET /api/pricing?group=taonier 生成目录并按存量 revision 写回;拉不到只记录 error、不写替代目录,下次启动重试
- api-server:目录未初始化时 AGC 目录/对话接口与后台目录接口返回 503,不回落任何内置模型名;后台 PUT 同样要求已初始化
- api-server:上游请求 10s 超时、1 MiB 流式上限、禁止重定向、不带凭据;目标校验只校验地址,不再绑定已下线的固定模型哨兵;写回冲突后重读校验既有目录
2026-09-24 16:18:06 +08:00
k88936 5a0f3b8037 宿主:目录锚不定的拒单不再写诊断
- `ProjectRootUnanchored` 从 `is_reportable()` 拿掉,与 `ProjectRootUnusable` 同类:符号链接 / 权限 / 目录被删都是用户自己就能修的文件系统事实,留痕只会变成噪声
- 它不再被 `direct_turn_rejection` 覆写成 `direct-codex-failure:v2 …` 诊断文案,界面按 `Display` 显示「无法锚定 Direct 调用项目目录:{cause}」,两侧对同一变体的分类不再自相矛盾
- 同步 `only_host_and_environment_rejections_are_reportable` 用例与 `is_reportable` 的文档注释
2026-09-24 16:16:06 +08:00
suzmii 6c92e5d673 文档:AGC 模型目录初始值改为上游同步的规范与运维口径
- 主规范补充目录初始化来源、失败关闭与重试口径,并记录本变更不改变目录格式与客户端契约
- 后端数据契约补充缺行语义与启动期同步
- 运维文档新增「AGC 模型目录上游同步」小节(来源、恢复路径、同批发布约束)
- decision-log 记录本次决策、真实上游实测结论与残留项
- 新增里程碑《AGC 模型目录初始值改为上游同步》与对应实施计划
2026-09-24 16:14:19 +08:00
k88936 0f9e2f1737 把设计图引用建模成 assetId 与 path 二选一的联合类型
src/features/ui-editor/uiDesignResourceBridge.ts 的 UiDesignDocImageReference 改为判别联合:原先两个字段都可选,`{}` 或两个都给要等到 Rust 运行时才被判错
2026-09-24 16:10:50 +08:00
k88936 c8852ac2af 把只赋值一次的 overview 改成 const 三元
src/view/ui-editor/index.tsx 用三元表达式声明 overview,去掉 let + if/else 两处赋值,表达不可变意图
2026-09-24 16:10:27 +08:00
k88936 95fec72ef0 删掉会话投影里无人使用的 generateCode 包装
src/view/ui-editor/useUiEditorPage.ts 移除 save.generateCode 及其实现:全仓只有 saveAndGenerateCode 与 stateStore.generateCode 有消费者,这个包装失败时静默返回 null,留着容易让后续调用方拿到没有原因的 null
2026-09-24 16:09:53 +08:00
k88936 bd78a91e55 宿主:并发拒单的两个身份改成回合身份
- Thread Manager 的 `accept_turn` 在并发冲突时回**已有的 `turn_id`**(调用方的 `clientTurnId`),不再回进程内的占用 token
- `DirectTurnReservation::accept` 的拒单载荷改成 `existing`=已在跑那一轮的 `clientTurnId`、`incoming`=本次请求的 `clientTurnId`;同一轮重发时两者相等,"同一轮消息仍在处理中"那条文案才走得到
- 补 `accept_conflict_reports_client_turn_ids_not_reservation_tokens`(同一轮 / 另一轮两条分支都钉住)与 `accept_conflict_returns_the_existing_turn_id`(manager 侧只回回合身份)
- 占用对象自己的 token 保持 UUID 不变(`complete_direct_thread_turn_if_reserved` 靠它配对),改的只有错误载荷
2026-09-24 16:08:39 +08:00
k88936 d7f3790fce 让节点拖动与缩放用同一套树/尺寸判据
useNodeTransformInteraction.ts 的 onNodePointerDown 补上 !tree || !logicalSize 守卫,与 onNodeResizePointerDown 一致:命中不到树或逻辑尺寸时不再启动手势并占用指针捕获
2026-09-24 16:08:05 +08:00
k88936 76dfd74dea 窗口失焦时丢弃未提交的树拖拽
PreviewWorkspace.tsx 的 onWindowBlur 一并清掉 treeDragRef 与树拖拽预览偏移:失焦时浏览器不一定补 pointercancel,陈旧手势会被后续 pointerup 复用并把意外偏移写进文档
tests/previewRightPanDrag.test.tsx 补失焦后抬起不提交偏移的用例
2026-09-24 16:07:56 +08:00
k88936 134046f806 树根拖拽的指针取消不再提交偏移
PreviewWorkspace.tsx 拆出 handleTreePointerCancel:pointercancel 只清拖拽状态与指针捕获,不再按抬手坐标算最终偏移,避免浏览器取消手势时把意外偏移写进文档
tests/previewRightPanDrag.test.tsx 补「抬起提交、取消不提交」用例,并给画布夹具补上 setTreeOffset 桩
2026-09-24 16:06:48 +08:00
k88936 9ecb084b68 宿主:失败载荷的 kind 改成 typed 枚举
- 新增 `DirectTurnFailureKind`,成为 `DirectTurnFailure.kind` 的唯一取值表:`timeout / model-failed / transport-failed / request-rejected / environment-not-ready / turn-interrupted / host-dropped`(补齐原先两份注释都漏掉的 `turn-interrupted`)
- `DirectModelCallKind::wire_kind` 与 `DirectTurnError::wire_kind` 改成返回该枚举,`DirectTurnFailure::new` / `DirectTurnTerminal::host_dropped` 同步改签名;线上取值仍是原来的 kebab-case 字符串
- 补 `failure_kind_wire_values_are_stable` 用例:7 个变体的序列化 / 反序列化取值逐条钉住,改名即改协议会先在这里失败
- 前端生成绑定重新导出:`chat/generated/DirectTurnFailureKind.ts` 新增,`DirectTurnFailure.ts` 的 `kind` 由 `string` 收窄成 union
- 受影响断言(`direct_thread_manager` / `direct_turn_accept` / `direct_thread_wire` / `codex_app_server`)改成比较枚举变体
2026-09-24 16:05:34 +08:00
k88936 064c26d7c9 按右键按下时的命中目标决定是否弹节点菜单
PreviewWorkspace.tsx 在右键按下时记住命中元素,抬起时改成按它解析节点与树:指针被视口捕获后 pointerup 的 target 是捕获元素,原先按抬起目标找不到 [data-node-id],节点菜单永远打不开
指针取消时同步清掉记录的目标
tests/previewRightPanDrag.test.tsx 补「抬起被重定向到捕获元素仍弹菜单」用例
2026-09-24 16:05:16 +08:00
k88936 ef4601c555 让节点删除默认落到选中节点所在的树
src/view/ui-editor/useUiEditorPage.ts 的 deleteNode 默认 treeId 改为 treeForSelectedNode?.src_ui_design ?? activeImageId:选中节点可以来自非激活界面图,Inspector 删除按钮不给 treeId 时会静默删不掉
src/view/ui-editor/uiEditorKeyboardShortcuts.ts 与 view/ui-editor/index.tsx 去掉键盘删除强制传 activeImageId 的接线,treeId 改为可选,同时删掉已无消费者的 activeImageId 入参
tests/uiEditorPage.test.ts 补「非激活树里的节点默认也能删除」用例,uiEditorKeyboardShortcuts.test.ts 断言改为只传节点 id
2026-09-24 16:04:29 +08:00
k88936 12e8287f92 避免超大缩放让网格步长计算死循环
previewGrid.ts 的 resolvePreviewGridStep 在 screenSpacing 溢出成 Infinity 时直接回基础步长:原先第二个 while 里 Infinity/2 仍是 Infinity,画布会卡死
previewWorkspaceZoom.test.tsx 补 Number.MAX_VALUE 缩放的回退用例
2026-09-24 16:02:10 +08:00
k88936 9e49db419f 让状态不变量校验先判字段类型再取值
src/features/ui-editor/stateInvariants.ts 在取 path.trim 与 pixel_size.every 前先判 string / Array.isArray,残缺持久化文档此前会在 firstUiDesignInvariantMessage 里抛 TypeError,被保存流程吞成通用失败文案
tests/uiEditorState.test.ts 补三组残缺界面图都回落到 invalid-image 的用例
2026-09-24 16:01:14 +08:00
k88936 e2e9243415 拒绝不是二元组的树偏移输入
src/features/ui-editor/stateTransition.ts 的 set-tree-offset 分支增加 min.length !== 2 判据:空数组会让 every 空真通过,缺坐标时 max 会算出 NaN 污染渲染消费的偏移
tests/uiEditorState.test.ts 补「[12] 与 [] 都判 invalid」用例
2026-09-24 16:00:13 +08:00
k88936 a871684bd7 改掉检查点漂移文案把时间戳说成轮次的说法
ui_editor/agent_tools/steps/write_back.rs 的 checkpoint_drift_message 原先输出「检查点第 1759... 轮基线 revision」,round.at 其实是秒级 Unix 时间戳;改为先给基线 revision,再标注时间戳含义
2026-09-24 15:58:45 +08:00
k88936 0e15d76046 让 ui-design-doc 的参数说明真正进入工具 schema
agent_native_tools.rs 给 from-images 的 images 属性挂上 texts/ui-design-doc.json 里已写好的说明,run-workflow 与 into-js 改用带 description 的单字符串 schema 生成器 one_string_input_schema_with_description,原先这三条文案没有任何引用点,模型看不到
2026-09-24 15:58:35 +08:00
k88936 ac870fd41a 文档登记失败时回滚本次顺带登记的设计图
ui_editor/agent_tools/creation.rs 登记文档资源失败时先摘掉 prepare_design_images 登记进来的图片条目,避免 manifest 留下没有文档引用的孤儿设计图
把「按 id 摘条目」抽成 drop_manifest_assets,rollback_created_document 复用同一实现
2026-09-24 15:58:24 +08:00
k88936 588768bff0 给 ui-design-doc 三个观察的 rejected 摘要统一脱敏
agent/runtime_tools/ui_design_doc.rs 让 rejected 接收项目根并复用 redact_agent_runtime_project_paths:项目上下文缺失时的报错会带上宿主路径,原先把 rejected 当成「只含模型参数」直接透出,摘要会泄露绝对路径
2026-09-24 15:58:08 +08:00
k88936 940d7b5793 修正 run-workflow 观察里误用文档 revision 判断清单失效
agent/runtime_tools/ui_design_doc.rs 改为在工作流结束后重读项目 revision,与开始前的项目 revision 比较;原先比较的 result.revision 是随文档重置的文档 revision,项目 revision 一超过它就永远判不出清单变化,前端会一直看到旧清单
2026-09-24 15:57:52 +08:00
k88936 1c7d90a950 把 ui-design-doc.run-workflow 认成可推进 revision 的验收证据
agent/runtime_protocol/acceptance_graph.rs 在 acceptance_evidence_tool_may_advance_project_revision 里补回被退役工具腾出的位置,写回文档会让 before 与 after 不同,不再被误判为 revision 不匹配
同一文件补 ui_design_doc_workflow_evidence_may_advance_project_revision 用例,锁住「证据工具」与「可推进 revision」两处识别一致
2026-09-24 15:57:27 +08:00
k88936 c844f857ef 交接阶段补上 ui-design-doc.from-images 设计图路径校验
tool_plan_handoff/content_validation.rs 为 from-images 的 images[].path 逐项做绝对路径与逃逸判据,恢复退役工作流工具留出的早检位
2026-09-24 15:48:53 +08:00
k88936 d082c5ab15 删除无人调用的 required_tool_arguments 组合封装
ui_editor/commands/utils.rs 去掉只做「找工具调用 + 解析参数」拼接的封装,识别与切分调用点都自带日志与错误映射,没有消费方
2026-09-24 15:46:57 +08:00
k88936 9694faa620 让节点 offset 在反序列化时缺省成零偏移
ui_editor/layout/node.rs 给 Node.offset 加 #[serde(default)],历史设计文档缺该字段时按零偏移解析
ui_editor/persistence.rs 补 legacy_nodes_without_offset_field_fall_back_to_zero_offset 锁定该回退口径
2026-09-24 15:45:25 +08:00
k88936 2331f62f1a 测试:登录态用例的会话刷新 spy 补 mock 实现
- `chat-composer` 的"登录态失效不重跑整轮"用例给 `requestPlatformSessionRefresh` 的 spy 补 `mockResolvedValue`,真回归时以 mock 结果干净失败,不再在测试里发起真实刷新
- 桩值用现役的 `stale`(`PlatformSessionRefreshResult` 只有 `refreshed / stale / failed` 三种)
2026-09-24 14:48:37 +08:00
k88936 7941aaa660 前端:埋点句柄只在结构化拒单时清掉
- `runTurn` 的 catch 先读结构化拒单,只有"这一轮没接单"的拒单才清 `pendingRunAnalyticsRef`;非结构化错误(IPC 失败、命令 panic)可能发生在接单之后,句柄留着等 `turn.completed` 结算,不再让宿主侧这一轮的候选永远没人结算
- chat-composer 用例的注释同步:这一轮没接单,就没有回合终态事件来驱动结算
2026-09-24 14:48:32 +08:00
k88936 6cee61b973 前端:Direct 回合失败分支去掉嵌套三元
- `runTurn` 的 catch 先把非结构化错误折成文本,再让结构化拒单文案覆盖,替掉原先"拒单 / Error / 其它"三层嵌套的三元表达式
2026-09-24 14:48:21 +08:00
k88936 2ce96a73f0 前端:拒单提示的空文案不再返回空串
- `directTurnRejectionNotice` 认得的拒单在宿主文案为空白时返回 `null`,不再返回空串:`''` 显示不出任何提示,却会被按 `!== null` 判据的调用方当成"有提示"
- 补注释把"有提示"的判据说清:要么给一条能显示的话,要么给 `null`
2026-09-24 14:48:10 +08:00
k88936 8d635c2ff7 合并:把 master 的告警清理与首页对话退役并进接单化分支
- 冲突 codex_app_server/mod.rs:保留类型化回合失败 DirectTurnRunFailure 与 EnvironmentNotReady 分类,接受删除 audit/metrics 参数
- 冲突 direct_runtime/mod.rs:保留类型化错误反馈循环与"交付报告只读一次",保留 check_direct_turn_preconditions,接受 master 的 turn_kind 贯穿与首页无项目对话退役
- 冲突 direct_runtime/user_input.rs:保留接单后发射器与 canonical 用户条目的新调用签名
- 删除:随 master 移除 direct_codex_audit、direct_turn_metrics 两个账本模块及其全部引用
- 删除:direct_codex_error_should_feedback 字符串判据与首页对话函数及其测试
- 调整:direct_codex_error_feedback_prompt 收成单参数,与已落地的提示词模板一致
- 修复:master 新插入的用户条目冻结块改用 DirectTurnError::turn_failed
2026-09-24 14:30:02 +08:00
k88936 dc5a5a4a62 修复合并后 CI 失败:退役命令的残留清单与用例
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m21s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Successful in 4m0s
Project CI / Frontend tests (pull_request) Successful in 1m53s
Project CI / Native shell tests (pull_request) Successful in 5m51s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m13s
Project CI / Repository checks (pull_request) Successful in 2m1s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m20s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m40s
- scripts/check-config.mjs 的 native-only 白名单删除已退役的 bind_components 与 create_ui_design_resource,两端原生能力检查重新自洽
- appSurface 的 UI 原型入口用例改断言 create_ui_design_doc_from_images,并按新契约补齐 relativePath / imageIds 返回形状
- 验证:npm run typecheck(含 skill-pack 与 check-config)、npx vitest run tests/appSurface.test.ts(211 passed / 9 skipped)
2026-09-24 14:21:14 +08:00
k88936 2873bed732 合并 origin/master 到 feat/ui-editor-v3
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m27s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / Native shell tests (pull_request) Failing after 1m47s
Project CI / Backend tests (pull_request) Successful in 3m53s
Project CI / Frontend tests (pull_request) Successful in 1m38s
Project CI / AI game creator shell web tests (pull_request) Failing after 38s
Project CI / Repository checks (pull_request) Failing after 1m33s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m0s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m2s
- 合入 master 179 个提交(后台管理页、游戏发行入口、AGC 运行页签、编译 warning 清理等)
- policy.rs 采用 master 版本:删除无调用方的 autonomous_design_foundation_command_is_allowed 重复名单,design-foundation 权限以 tool_policy_snapshot.rs 为准
- native-tools.json 保留双方结果:master 的描述文本 + 分支删除已退役的 ui.workflow.run.description
- useUiEditorPage.ts 保留分支的 save / saveAndGenerateCode 状态返回契约,并接上 master 的 beginUiSaveAnalytics 与 saveSource: 'auto' 埋点
- 验证:cargo check --bin genarrative-ai-game-creator-shell、npm run check:rustfmt、tsc --noEmit、vitest tests/uiEditorPage.test.ts + tests/clientAnalytics.test.tsx(47 passed)
2026-09-24 13:53:34 +08:00
k88936 c8478f07ef 文档:接单化 review 收口的不变式写进 ADR 与共享记忆
- ADR §2 补两条不变式:终态的写点在整轮结束之后、封口返修要求不是回合失败
- 技术方案同步改写"终态由事实判定"一段,并补"终态写点在整轮结束之后"的判据
- ADR 末尾补 2026-09-24 后续更新索引,指向技术方案与决策记录
- 决策记录追加 2026-09-24 条目:终态写点、返修控制流、终止判据、登录态重试与失败载荷健壮性
2026-09-24 13:51:10 +08:00
k88936 f19bd8de8c 宿主:终态写点挪到整轮结束之后,解析失败也能落进终态
- Direct 回合的终态判定事实改成先固定上下文,写点留到解析与线程释放之后
- structured output 解析折进同一个收尾结果:解析失败不再"终态写完才失败",改走失败载荷
- 收尾结果拆成 `DirectTurnReport`(报告正文 + 解析结果),终态兜底文案仍取被解析的那份文本
- 新增 `DirectTurnTerminalContext::write` 作为唯一终态出口,占用解除与 `turn.completed` 一起走
- `direct_turn_terminal_write` 改成"报告 / 失败"两个入参,便于单测覆盖三种投影
- 补单测:解析失败投影成 `model-failed` 载荷,正常收尾不带失败载荷
2026-09-24 13:49:38 +08:00
k88936 64e3eba55a 宿主:封口返修要求改成 typed 控制流,不再写成失败
- 新增 `HostOutcomeText`:封口复核的"继续返修批次"用独立变体表达,不再伪装成 `LlmError::InvalidRequest`
- 新增 `DirectTurnRunFailure`:app-server 回合结果区分"真失败"与"返修控制流",`RepairRequired` 不写终态
- `direct_turn_terminal_write` 收口终态写出:返修要求跳过,真失败从 typed 错误投影 `kind` / `message`
- 早退取得宿主收尾事实的分支只认真失败,返修控制流不再被中断成一次收束
- `DirectTurnError` 新增 `RepairRequired` 变体并重新生成前端绑定
- 返修循环同时消费 `ReviewRequired` / `RepairRequired`,次数上限仍留在产生侧
- 补单测:返修要求不写终态、真失败投影成 transport-failed、正常收尾不带载荷
2026-09-24 13:40:36 +08:00
k88936 a39b322af6 将 UI 编辑器 Agent 工具模块由 design_doc 重命名为 agent_tools
- 重命名 src-tauri/src/ui_editor/design_doc/{checkpoint,creation,mod,run_workflow,test_support}.rs 与 steps/** 到 ui_editor/agent_tools/
- 更新 main.rs、ui_editor/mod.rs、agent/runtime_tools/ui_design_doc.rs 中的模块路径引用
- 更新 steps/recognize.rs 与 steps/separate/mod.rs 内部对 agent_tools 夹具的引用
- 更新 tests/project.rs 注释中的 creation.rs 与 next_ui_design_path 模块路径
- 更新 agent_tools/test_support.rs 模块注释为 agent_tools 单测共用夹具
- 更新技术方案《UI编辑器代码地图与模块职责》《UI编辑器Agent工具化重写》的模块树与落地顺序路径
- 更新 shared-memory decision-log 中该次重写的测试过滤器为 agent_tools
2026-09-24 13:34:14 +08:00
k88936 0da63721df 宿主:用户按下的终止不再被记成通道失败
execution.rs:`fail_turn` 的判据从"只看 is_closed"改成"`is_closed` 或 `host_stop_requested` 都不算失败"。用户点「终止」时 `cancel_from_host` 先同步置位 `host_stop_requested`、再异步中断会话,`closed` 与阶段要等那个任务跑到才变;这段窗口里到达的 `TransportClosed` / `interrupted` 都是宿主自己收尾的结果,以前会被记成 `transport-failed`。判据收在 `fail_turn` 里,调用点不必各写一遍,将来新增收口路径也不会漏。不记失败事实照旧收束,原因仍写进报告。
mod.rs:`interrupted` 分支去掉现在重复的 `!host_stop_requested()` 检查(同一个事实只留一处判据)。
execution.rs 单测:新增"用户请求过终止 + 未 closed 时 fail_turn 不写失败事实、原因仍进报告";变异验证:撤掉新判据该用例变红。codex_app_server 过滤 101 passed。
2026-09-24 13:21:26 +08:00
k88936 080ef53b68 宿主:登录态失效的两条分类路径统一成可重试
direct_turn_error.rs:DirectCodexNativeKind::is_retryable 把 Unauthorized 归进 false 组,而同一份事实走 DirectDomainFact::AuthenticationRejected 时是 true,于是 retryable 取决于哪一层先认出它;旧口径对 401 / authentication-required 一律返回 true,这里对齐成可重试,并写明与 recovery_hint 同口径的理由。
direct_runtime/mod.rs:补一条断言(原生 codex-app-server-error:unauthorized 与深层 authentication-required: HTTP 401 同为可重试、都不可反馈给模型);agent:: 过滤 950 passed。
2026-09-24 13:17:21 +08:00
k88936 0262e82572 把 write-back 步骤拆成独立模块
新增 steps/write_back.rs,承载保存 State、追加 write-back / outdated 行与漂移文案
把 rebuild_target_state 从 run_workflow.rs 移到 steps/mod.rs,作为已记录 DTO 的重放入口
run_workflow.rs 只保留输入输出类型、三步编排与检查点恢复判定
把 DRIFT_REASON 与两条漂移文案收敛到 write_back 模块,避免编排层和写回层各写一套
同步两份 UI 编辑器技术方案的模块布局与代码地图
2026-09-24 13:09:57 +08:00
k88936 d287061c61 宿主:交付报告兜底只读一次 terminal_report
direct_runtime/mod.rs:两处 `Err(_) if terminal_report(...).is_some()` 的 guard 与取值各调了一次 terminal_report,两次之间状态变化就会拿到不一致的结果——流式分支第二次拿到 None 时会把空串当回复返回(界面显示"未返回可展示的回复"),非流式分支则绕过"未返回结果"的兜底错误。改成一次读取后落变量,判据与取值同源;两个分支的优先级(有报告 > 可反馈修复 > 原样抛出)不变。direct_ 过滤 474 passed。
2026-09-24 13:08:48 +08:00
k88936 1c03736d6b 宿主:删掉恒为 false 的 invites_repair 判据
direct_turn_error.rs:DirectDomainFact::invites_repair 每个分支都返回 false,is_model_repairable 里的 is_none_or(invites_repair) 实际等价于 is_none(),第一个分支还误导性地暗示"有些事实值得反馈"。删掉该方法,调用点直接写 is_none(),并把"认出是哪一类就拦"的理由写进注释;行为逐条不变(direct_ 过滤 474 passed)。
2026-09-24 13:06:21 +08:00
k88936 6ef64c3bcf 将 design_doc 的切分模块提升为目录模块
把 steps/separate.rs 提升为 steps/separate/mod.rs,只保留纯 State 变换
把 design_doc/cut_images.rs 移到 steps/separate/cut_images.rs,与它服务的切分步骤同层
把 normalize_cut_image_path 从 steps/mod.rs 下沉到 steps/separate/mod.rs,紧邻两个使用点
run_workflow.rs 改从 steps::separate 统一导入切分入口与登记入口
同步两份 UI 编辑器技术方案的模块布局与代码地图
2026-09-24 13:05:44 +08:00
k88936 db11bae2d8 注释:说明失败说明身份在无身份无时间时会撞成一条
directTurnFailure.ts:原来那句"身份不可证明时退化成与事件时间绑定的固定形状……不会让两轮失败互相覆盖"漏了 `at` 也拿不到的那一档——常量 direct-thread-turn-failure 会让两条这样的失败按同一个 itemId 合并。补上这一档的真实行为与取舍(唯一性与重放不变不可兼得,这里选重放不变),代码不动。
2026-09-24 13:04:11 +08:00
k88936 b0e870e7e2 前端:失败终态载荷的 message 缺字段不再打断 reducer
directThreadChat.ts:`turn.completed.failure.message` 在生成类型里是必填 string,但跨 IPC 的载荷没有运行时校验,缺字段 / null 时 `.trim()` 会在 reducer 里抛错,把这条订阅之后的所有事件一起打断;改成与兄弟函数 directTurnFailureNoticeText 一致的 typeof 判据,取不到非空字符串就按"没有原因"收口。
directThreadChat.test.ts:补一条回归用例(message 为 undefined / null 时不抛错、不补空气泡、终态照样收口);变异验证:撤掉 typeof 判据后该用例变红。
2026-09-24 13:03:53 +08:00
k88936 286d919129 测试:失败终态用例断言映射后的文案不落会话列表
断言原来查的是命令 Err 的原始文本,而这条文本永远不会被渲染(前端先用 projectRuntimeVisibleError 映射成通用可见文案,再走头部状态行的横幅),因此断言空过、盖不住"命令通道又写一条聊天文案"这个回归。
改成在 `陶泥儿消息` 列表里查映射后的文案(横幅不在这个列表里),并用一次变异验证:在 catch 里补一条 appendLocalMessage 后该用例变红,撤掉即绿。
2026-09-24 13:02:41 +08:00
suzmii 1931852e9e 补齐打开/选择目录入口的提权拒绝记忆清除
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m25s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m45s
Project CI / Backend tests (pull_request) Successful in 3m54s
Project CI / Frontend tests (pull_request) Successful in 2m5s
Project CI / Native shell tests (pull_request) Successful in 6m0s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m24s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m38s
Project CI / Repository checks (pull_request) Successful in 2m10s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m1s
- aclElevation:新增唯一入口 clearAclElevationDenials()(Tauri 环境判断 + 命令失败只吞掉,旁路动作不影响本次操作)
- useRecentProjects:rememberRecentWorkspace / refreshRecentWorkspace 改用该共享入口,删掉本地同名实现
- useHomeProjectCreation:openProject 入口先清除拒绝记忆再 inspect,覆盖行内打开、运行中项目入口与文件选择器选择目录;此前只挂在「打开/新建成功之后」,用户点了打开会撞上 120s 冷却直接失败且不弹 UAC
- appSurface/home.suite:新增断言「用户动作先 clear 再 inspect」,并做逆向确认(去掉该调用即红)
- docs:decision-log 写清前端唯一入口与必须挂的四个入口,pitfalls 记录 leader 失效接管这条残余边界
2026-09-24 12:56:47 +08:00
suzmii 9034b8a2d3 合并 master 到 fix/acl-elevation-single-flight
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m8s
Project CI / Backend tests (pull_request) Successful in 4m8s
Project CI / Frontend tests (pull_request) Successful in 2m10s
Project CI / Native shell tests (pull_request) Successful in 6m13s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m48s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m39s
Project CI / Repository checks (pull_request) Successful in 2m14s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
- 合入 origin/master(dcb4f2a1c):DirectProject 过程卡读秒粒度与样式修复(#508)
- pitfalls.md 冲突按「两边都保留」解决:ACL 单飞条目置顶,其后保留上游 2026-09-24 读秒条目
- 本次合并未改动 ACL 单飞实现(含刚收口的 key 归一化与冷却基准改动)
2026-09-24 12:08:58 +08:00
suzmii 3480a2f331 评审收口:闸门 key 归一化路径写法 + 冷却改从结果落库时刻算起(#498)
- acl_repair_gate:冷却基准从 leader 起跑时刻改为结果落库时刻;UAC 被挂着几十秒到两分钟时,120s 拒绝冷却不再提前过期,避免紧跟的自动整表重查立刻再弹一次
- config:新增 windows_acl_repair_gate_key,闸门 key 的路径半边先去掉 \\?\ / \\?\UNC\ 前缀再统一小写;最近项目列表里同一项目实测同时存在 \\?\C:\... 与 C:\... 两种写法,按原始字符串做 key 会让同一个目录弹两次 UAC
- tests/acl_repair_gate:补「冷却从结果落库时刻算起」与「路径写法归一成一个 key」两条用例;两条都做过逆向确认(改回修复前语义即失败)
- docs:decision-log 与 pitfalls 补记 key 归一化、冷却基准,以及真机复现的三个坑(DENY 要加在祖先的父目录、夹具路径必须落在 Managed 放行范围内、提权子进程会按 repair target 再校验 scope)
2026-09-24 12:05:42 +08:00
suzmii 67d5ad6026 合并 master 到 fix/acl-elevation-single-flight
- 合入 origin/master(87e52860a):后台游戏审核弹窗、客户端维护态错误弹窗、发行入口同源路径等上游变更
- decision-log.md 冲突按「两边都保留」解决:单飞决策条目置顶,其后保留上游同日条目
- 本次合并未改动 ACL 单飞实现本身(acl_repair_gate.rs / config.rs / useRecentProjects.ts 无冲突)
2026-09-24 11:47:33 +08:00
k88936 cf79ac31f7 文档:给"早退"补一句定义
docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md:§2 写明"早退"= 回合内任何没走到正常终态的收口点(turn/start 被拒、注入失败、panic)。
docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md:第 2 步同一处补定义。
docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md:宿主异常收场段补同一句定义。
2026-09-23 22:09:15 +08:00
k88936 ce668bbff3 文档:接单化落地收尾,ADR 转已接受并同步下游口径
docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md:状态改成已接受并指向实施计划;"落地时要同步的文档与注释"改成已同步清单。
docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md:顶部取代注扩到"事件不带回合身份"与"宿主侧 Drop 守卫兜底"两条决策形状,影响一节的两条已知边界逐条写明新口径。
docs/technical/【实施计划】DirectProject命令接单化-2026-09-23.md:四步标记落地并补每步落地结果、验收证据(rust agent:: 949 / 前端 4473)与已知坑。
docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md:正常回合改成接单后先落盘再注入;异常回合收尾写明唯一终态出口是占用对象。
docs/project-memory/shared-memory/decision-log.md:host-dropped 两条口径加取代注(含 kind 追加 environment-not-ready),并追加 2026-09-23 接单化决策一条。
docs/README.md:索引行去掉"未实施",改成四步均已落地。
2026-09-23 22:05:11 +08:00
k88936 2772081791 注释:收尾接单化后两个入口的分工与待办
direct_runtime/user_input.rs:补 CLI 与 GUI 的分工——CLI 入口保持 await(它要回复文本,没有事件订阅),两个入口共用同一份接单前检查、同一个命令主体与同一份 Display 文案;DirectTaonierActiveInvocationGuard 的注释改成只挡并发、不再是首页快照来源。
direct_runtime/mod.rs:TODO(Direct 命令接单化) 改成 TODO(失败条目进历史),指向备选方案第 3 条;失败说明本轮不落历史的口径不变。
cli.rs:删掉"带上诊断与 详情: 引用"的过期注释,改成与 GUI 同一份 Display 文案、不另加引用,差别只在 CLI 自己 await 整轮。
2026-09-23 22:04:48 +08:00
k88936 93202f2f91 前端:发送队列放行与埋点结算改听回合终态
- reducer 新增 `completedTurnCount`(单调计数):一轮可能在同一次 consume 里开始并结束,
  下降沿不可靠,收口是**状态**不是转移
- 队列放行只在"回合终态或拒绝接单"发生;命令返回不再驱动出队
  (接单被拒仍当场出队,权限被拒等从未发出的路径保持原样)
- 埋点结算挂到回合终态:接单返回时成绩还没入账,句柄因此活过命令返回;
  拒单只丢句柄、不发一次注定被丢弃的结算
- 本地在途标签活到宿主认领这一轮(身份命中 / 出现开始事件 / 收口计数变化),
  "命令返回"不再等于"这一轮结束",命令与开始事件之间不再有可发送的空窗
- 删掉 `markTurnStopped()`:终止成功的回合边界由宿主写的兜底终态收口
- 删掉 `turn.started` 的"重复起点保留第一次"兼容分支(接单只发一次开始事件)
- 同步注释:发送时序、待认领窗口、`commandInFlight` 的真实含义
- 测试:队列用例改用终态事件驱动,认证失败用例断言拒单不结算,reducer 补收口计数用例
2026-09-23 21:54:53 +08:00
k88936 fbb161b33e 技术方案标注三个工具的落地顺序已完成 2026-09-23 21:51:52 +08:00
k88936 e8a33591c1 修正 run-workflow 的工具描述口径
- 明确本工具会把切分素材回填到 Image 组件、清组件状态、写 NeedReview 后写回并推进 revision
- 明确不做页面发现、多树合并与独立组件绑定,漂移时本轮作废需重新调用
2026-09-23 21:48:51 +08:00
k88936 7f4012697f run-workflow 的项目权限判据与工具命令映射对齐
- 内部 enforce_project_permission_policy 改用与并行账本一致的 asset.register,避免引入无法在项目策略里配置的私有命令 ID
2026-09-23 21:48:12 +08:00
k88936 784f6f0e32 收敛 design_doc 未使用接口并补齐切分 sidecar 收尾
- run-workflow 干净跑完后调用 finalize_separation 清理 sidecar,有回填问题时保留给编辑器恢复,与前端切分链路一致
- 检查点恢复校验轮次归属文档,漂移与写回冲突说明带上基线 revision
- WorkflowLog 删除仅供测试的 path 访问器,测试直接用 workflow_log_path
- mod.rs 不再导出仅内部使用的 next_ui_design_path,消除未使用告警
- 技术方案同步 sidecar 收尾口径
2026-09-23 21:44:45 +08:00
k88936 af5fdf8a0e 宿主:首页「运行中的项目」改由 Thread Manager 的逻辑回合导出
活动回合表的唯一事实源从"调用身份守卫"搬进逻辑回合占用,任务侧不再另建一张表。
- `ActiveDirectTurn` 带上快照字段(回合身份 / 项目名 / 起点 / 状态 / 活动 / 序号),
  接单时初始化,收口时随占用一起消失
- 新增 `update_direct_thread_active_turn`(进度回填,只认身份一致且序号不倒退)与
  `list_direct_active_turns`(只导出仍有未收口回合的 thread)
- `DirectActiveTurnSnapshot` 移进 `direct_thread_manager`,`projectPath` 用线程身份,
  与事件流里的项目身份是同一个字符串
- `DirectTaonierActiveInvocation` 退回纯单飞锁:只留调用身份与登记时刻
- 回合更新发射器不再按项目路径 canonicalize 找表,改为持线程身份回填
- `DirectTurnReservation::accept` 多带一个 `clientTurnId`(快照与进度匹配用),
  与占用 token 是两个身份
- 上下文身份的两个测试补上"逻辑回合也接单"这一步:身份来自接单,不是调用守卫
2026-09-23 21:39:01 +08:00
k88936 544f9ac040 重写 UI 设计文档桥接单测到当前入口
- 退役 ensureUiDesignResourceForPrototype / findLinkedUiDesignResource 的 6 条旧用例已随实现删除而无对应导出,测试文件整体失败
- 改为覆盖 createUiDesignDocFromImages:空图片列表拒绝且不调用命令,正常调用透传项目身份与设计图引用
2026-09-23 21:35:57 +08:00
k88936 e360d12ccb 同步 UI 设计文档工具化重写的文档口径
- 代码地图补 design_doc 各模块与三个 Agent 工具,并标注旧工具已退役
- project-overview 用三个新工具替换 ui.workflow.run 的旧口径
- App 实施计划追加 2026-09-23 节,记录工具拆分、检查点恢复与退役范围
- decision-log 新增本次决策、取舍与验证方式
- UI 工作流资源桥接旧方案标记 historical,从 docs/README 移入历史集合
2026-09-23 21:33:23 +08:00
k88936 b8d218f741 注册 UI 设计文档三个 Agent 工具
- 新增 agent/runtime_tools/ui_design_doc.rs:三个工具的参数解析、项目 ID 与 provider 身份注入、manifest 失效广播
- agent_native_tools 增加三个工具的描述与入参 schema,函数名归一同时处理 '.' 与 '-'
- Runtime 可执行工具目录、并行账本命令映射、design-foundation 自主白名单同步登记
- 新增工具描述可用性与入参校验单测
2026-09-23 21:30:50 +08:00
k88936 116d9d5940 新增 run-workflow 编排、切图素材登记与检查点工具函数
- 新增 design_doc/run_workflow.rs:recognize → separate → write-back 编排,按检查点行恢复,文档漂移追加 outdated 并报错
- 新增 design_doc/cut_images.rs:按切分路径登记图片并构造 SpriteAsset,按路径复用 manifest 条目、失败不回滚
- persistence 新增 load_ui_design_document_snapshot_at 并让 load_ui_design_state_at 复用它
- checkpoint 新增 checkpoint_timestamp 秒级时间戳
- 切分路径归一下沉到 steps/mod.rs,separate 步骤改用共享实现
- 技术方案同步 design_doc 模块布局与 into-js 复用 persistence 的说明
2026-09-23 21:08:52 +08:00
suzmii f056c8a325 Merge remote-tracking branch 'origin/master' into fix/acl-elevation-single-flight
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m50s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m9s
Project CI / Backend tests (pull_request) Successful in 4m43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m14s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m4s
Project CI / Frontend tests (pull_request) Successful in 2m27s
Project CI / Native shell tests (pull_request) Successful in 6m16s
Project CI / Repository checks (pull_request) Successful in 2m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
2026-09-23 20:13:56 +08:00
suzmii d94ee96836 补一条用例:Rust 侧取消 UAC 的稳定标记同样不触发重试
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
- tests/recentProjectsHook:AGC_ACL_ELEVATION_DENIED 这类失败只检查一次,钉住 Rust 错误标记与前端「不可重试」判定之间的契约
2026-09-23 20:11:34 +08:00
suzmii c079003719 Merge remote-tracking branch 'origin/master' into fix/acl-elevation-single-flight
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-23 19:59:41 +08:00
k88936 d31a758c9c 宿主与前端:接单被拒返回 typed 错误,界面按变体分流
宿主侧把拒单收成结构化载荷,界面不再解析任何文案前缀。
- `DirectTurnError` 及其嵌套枚举补 `Serialize + TS`,导出到 `chat/generated/`
- 新增 `DirectTurnRejection`(结构化变体 + `Display` 生成的唯一一份文案),命令返回类型改为它
- `EnvironmentNotReady` 补 `environment-not-ready` 失败分类,避免回合失败被写成 `model-failed`
- `TurnAlreadyRunning` 去掉机器前缀,两条文案按身份是否相同分岔
- 删掉「按文案前缀判定」的协议约定与 `is_turn_failure`,通道改由**发生位置**决定
- 兜底终止路径改走 `complete_direct_thread_turn`:写终态的同时解除占用,不再只裸追加事件

前端按 `error.type` 分流,删掉三个按文案判断的旧函数。
- 新增 `readDirectTurnRejection` / `directTurnRejectionNotice` / `directTurnRejectionNoticeMessageId`
- 认得的参数 / 前置条件类(空内容、并发、参数非法、工程根等)写成与用户消息同级的提示,
  不占状态行、不写运行错误、不上报
- 认不得的宿主 / 环境事实与其它非结构化错误原样抛出,走既有捕获链路(上报 + 横幅)
- `chat_with_game_creator_direct_codex` 的 catch 从此只剩「拒单」一种输入

测试与绑定同步更新:appSurface 两条用例按新语义重写,`userItemId` / 终态时刻的注释跟着改。
2026-09-23 19:56:26 +08:00
k88936 3c4c546ed5 新增自动切分结果落 State 的镜像步骤
- Node 新增 find_mut,深度优先可变定位节点,镜像前端 findUiNodeLocation
- 新增 design_doc/steps/separate.rs:素材合并去重与资源校验、按 bound_nodes 回填 target_graphic、统一写 problematic_nodes 的 NeedReview
- 覆盖重复素材、缺失素材、节点缺失、非 Image 组件、已绑定其他素材与问题文案回退的单测
2026-09-23 19:51:16 +08:00
suzmii af099abbfa AGC ACL 提权修复按目标做 single-flight,避免并发重复弹 UAC
- 新增 acl_repair_gate:以 (规范化 repair target, scope) 为 key 的进程级 single-flight 与结果冷却(成功 30s / 失败 15s / 用户取消 120s),等待窗口 60s 超时按失败关闭
- acl_repair_gate:leader 异常退出由 RAII 兜底记为失败并唤醒全部等待者,避免等待者被永久挂住
- config:提权修复改经闸门执行;用户取消 UAC 统一返回稳定标记 AGC_ACL_ELEVATION_DENIED,不再依赖中文文案判定
- commands/main:新增 clear_game_creator_acl_elevation_denials,供用户主动操作时解除拒绝记忆
- useRecentProjects:识别新的稳定标记;打开/新建项目与重命名刷新时清除提权拒绝记忆
- tests/acl_repair_gate:并发只执行一次、冷却复用、清除后可重试、follower 超时、leader panic 唤醒等待者
- docs:decision-log 与 pitfalls 记录 single-flight 取舍与未做项
2026-09-23 19:44:06 +08:00
k88936 b1a356aa0d 新增自动切分素材构造器与 design_doc 共用夹具
- SpriteAsset 新增 path 访问器与 from_registered_image,镜像前端 prepareSpriteAssetBatch 的空名称、无边框、像素比 1 口径
- 新增 design_doc/test_support.rs,集中 State、节点、素材的最小构造夹具
- recognize 单测改用共用夹具,删除本地重复的 image/state_with/root 构造
2026-09-23 19:41:01 +08:00
k88936 b34f1c9b3a 新增结构识别结果落 State 的镜像步骤
- design_doc/steps/recognize.rs:apply_recognition 整树替换,并按 DTO 顺序逐棵重推横向偏移
- 镜像 deriveTreeOffset / treeSize 与 UI_TREE_PADDING=48,先落位的树把后面的树推到右边
- 设计图缺失或像素比非法时按前端同口径报「界面图 X 缺少合法尺寸」
- 三个单测覆盖左右排布、整树替换与缺失设计图失败
2026-09-23 19:32:13 +08:00
k88936 f9a2cbe26e 新增工作流检查点日志模块
- design_doc/checkpoint.rs:run / recognize / separate / write-back / outdated 五种行
- 路径推导 ui/.<文档名>-workflow.jsonl,与文档同级且不进 manifest
- 追加时先截断崩溃留下的半行再一次性写入并 sync,避免半行夹在日志中间
- 扫描只保留最后一轮,写回或 outdated 视为轮次结束;坏行失败关闭
- 载荷一律按 serde_json::Value 存回,模块不认识业务 DTO 与 State 类型
- 六个单测覆盖路径推导、轮次遮蔽、两种结束行、半行丢弃与坏行失败
2026-09-23 19:24:07 +08:00
k88936 8ac8f2ff5d 补充 run-workflow 的轮次、漂移与写回幂等口径
- 检查点行补充 outdated 与起始 revision,明确 write-back / outdated 都是轮次结束行
- 追加前截断半行,避免崩溃留下的半行夹在日志中间
- 文档中途漂移时追加 outdated 并返回错误,由下一次调用显式开新一轮,不在同一次调用内自动重启
- 写回按「目标 State 与文档当前 State 相等」判幂等,并记录该判据成立的前提
- 切图资源失败不回滚,NeedReview 由 separation DTO 的 problematic_nodes 在编排阶段统一回写
2026-09-23 19:12:19 +08:00
k88936 ea4fbc66ff 宿主:DirectProject 命令只接单,整轮改由后台任务跑
- 命令顺序固定为 clientTurnId 校验 → 占用调用身份 → 工作流恢复 → 用户条目校验 → 前置条件 → 工程准备 → 接单 → 落盘用户条目 → spawn
- 命令返回值收窄成"拒单":接单成立后不再有 Err,整轮结果只由事件流回答
- 新增 check_direct_turn_preconditions,前置检查从 run_..._and_emitter 上移,GUI 与 CLI 共用
- 作废"调用级拒绝直通"分支:判据改成位置,接单后一律按回合失败处理
- 删除 DirectTurnError::is_turn_failure,EnvironmentNotReady 补 wire_kind = environment-not-ready
- run_turn 不再重复落盘用户条目,只把它的身份作为第一条运行态条目下发
- 新增用例:接单之后才发现的失败也必须补出 turn.completed
2026-09-23 18:57:27 +08:00
k88936 c650c0297c 宿主:逻辑回合的边界交给 Thread Manager,接单即成对
- direct_thread_manager 新增逻辑回合占用:接单在同一个临界区里拒并发 + 登记占用 + 追加 turn.started,返回已占用的 token
- direct_thread_manager 拆出深层终态出口与占用兜底出口,notify 从 append 里抽出来复用
- 新增 direct_turn_accept:接单对象持有这一轮的终态出口,Drop 兜底补 host-dropped
- direct_turn_failure 删除 DirectTurnFailureGuard,终态改成显式构造的 DirectTurnTerminal
- codex_app_server 不再镜像 Codex 原生回合:删掉 run_turn 内的 turn.started 与守卫武装,终态改走 complete_direct_thread_turn
- 用户条目事件仍由 run_turn 下发,顺序固定为逻辑回合开始 → 用户消息 → 起 codex
2026-09-23 18:42:27 +08:00
k88936 648ead2150 新增 UI 设计文档创建入口 create_ui_design_doc_from_images
- 新增 ui_editor/design_doc/creation.rs:由一至四张设计图新建文档,未登记路径顺带登记成 ui-design 图片资源,文档内设计图身份取图片 assetId
- persistence.rs 把单图初始化泛化为 initialize_ui_design_state_with_images_at,支持多张设计图并拒绝重复
- 删除 resource_bridge.rs 与 create_ui_design_resource、ensure_ui_design_resource_for_prototype 两个命令,取消「原型 → 已存在文档」的幂等查找
- main.rs 注册 create_ui_design_doc_from_images 并移除两个旧命令注册项
- 前端 uiDesignResourceBridge 改为调用新命令,project-development 去掉 ui-workflow.completed 的自动打开与阶段跳转分支
- 代码地图同步 design_doc 模块与关键命令列表,测试改为覆盖新入口的编号避让
2026-09-23 18:14:15 +08:00
k88936 f672a04ebb 文档:DirectProject 命令接单化实施计划
- 新增实施计划:四步落地顺序(Thread Manager 逻辑回合 → 命令接单 + 后台整轮 → typed 拒单 → 队列/埋点/快照/reducer),每步给改动点、不变式与验收
- 记录已落地三项(重试删除、详情引用删除、失败进池与不落历史)与三条已知坑
- docs/README.md 索引补上该计划
2026-09-23 17:59:34 +08:00
k88936 5398a53e6e 宿主与前端:用户可见文案不再带诊断引用,失败也进错误上报池
- `record_direct_codex_failure` 的收口文案去掉 `;详情:<path>`,诊断 sidecar 与应用日志照写
- 同一出口把最终文案送进错误上报池:命令接单化后前端 catch 只剩"接单被拒",池不能只靠前端填
- 删除 `persist_direct_codex_failure_context`:失败说明本轮不写进项目历史,留 TODO 记录以后"进历史但不喂模型"的通道
- 删除只服务详情展开的 IPC `read_agent_runtime_error_detail` 及其注册
- 前端去掉 `详情:` 正则与第二次读取,横幅只显示一句话
- 同步命令边界与相关测试注释
2026-09-23 17:57:36 +08:00
k88936 665788760e 新增 UI 设计文档工具的提示词目录模块
- 新增 prompts/runtime/texts/ui-design-doc.json,承载 from-images、run-workflow、into-js 三个工具的描述与参数文案
- manifest.json 的 textCatalogs 登记 uiDesignDoc 目录
2026-09-23 17:51:40 +08:00
k88936 e776aa1084 前端:删掉由 invoke 拒绝驱动的认证重试
- `directCodexSession.ts` 改名 `directCodexSessionKeepalive.ts`,只保留会话保活常量,删除 `withDirectCodexSessionRefresh` 的"刷新 + 重跑整轮"及其登录失效识别
- 发送回合与终止回合两处调用点直接 invoke,不再包一层重试
- controller 的 catch TODO 更新为新形状:命令只接单、拒单返回 typed 错误、整轮结果只由事件载荷回答
2026-09-23 17:50:58 +08:00
k88936 02e29ba555 新增 UI 编辑器 Agent 工具化重写技术方案
- 新增技术方案:三个工具契约、两步工作流、JSONL 检查点、Rust 与提示词目录模块布局、落地顺序
- 代码地图同步退役 merge/binding/workflow.rs、关键命令列表与类型文件数量
- docs/README.md 收录该方案
2026-09-23 17:49:27 +08:00
k88936 e0d88fe8e4 文档:DirectProject 命令接单化定稿
- 重写 ADR:逻辑回合归 Thread Manager、接单/拒单判据改成发生位置、拒单载荷复用 typed 错误、userItemId 由 clientTurnId 推导、提示与用户消息同级并删除详情、队列与埋点改挂回合完成、失败原因本轮不落历史
- CONTEXT.md 新增「逻辑回合」「接单」「拒单」「在途回合」四个术语
- 对话历史单一事实源 ADR 加注:两条已知边界已由新 ADR 重新决策
- Codex 原始历史技术方案加注:三句结论待实施时按新 ADR 修订
- docs/README.md 索引补上该 ADR
2026-09-23 17:47:59 +08:00
k88936 3a35127b3b 记录 UI 设计文档与检查点日志术语及 ADR
- CONTEXT.md 新增 UI 设计文档、设计图、UI 工作流步骤、工作流检查点日志四个词条
- 新增 ADR 说明检查点采用追加式 JSONL 日志并记录两种被否方案
- docs/README.md 收录该 ADR
2026-09-23 17:47:00 +08:00
k88936 fb49b5fc5d 前端移除 UI 编辑器合并与绑定调用
- 删除 features/ui-editor/merge.ts 与 MergeDTO、BindingDTO、BindingChange 类型文件
- useUiEditorPage 去掉 mergeUi 操作、合并运行态、合并状态与投影字段
- isAiRunning 不再把合并运行态计入编辑器忙状态
2026-09-23 17:46:06 +08:00
k88936 31a1660e41 退役 UI 编辑器多树合并与组件绑定链路
- 删除 ui_editor/commands/merge.rs 与 binding.rs 两个实现及其 MergeDTO、BindingDTO
- commands/mod.rs 去掉两个模块声明、DTO 导出与 impl 再导出
- main.rs 去掉 merge_ui、bind_components 两个 Tauri 命令及注册项
2026-09-23 17:44:48 +08:00
k88936 006d278e9f 退役 Agent 侧 ui.workflow.run 工作流工具
- 删除 ui_editor/workflow.rs 与 agent/runtime_tools/ui_workflow.rs 两个工作流实现模块
- agent_native_tools 去掉 ui.workflow.run 的工具描述与 schema 分支
- prompts/runtime/texts/native-tools.json 删除 ui.workflow.run.description 文案
- runtime_tools/policy、tool_policy_snapshot、parallel_ledger、project_gates 移除该工具的白名单与门禁引用
- action_execution 去掉 ui.workflow.run 分发分支,action_audit 删除整块工作流审计与枚举项
- runtime_protocol 的 verification 与 acceptance_graph 去掉工作流校验项
- tool_plan_handoff/content_validation 去掉 ui-design pages 校验分支
- ui_editor/mod.rs 去掉 workflow 模块声明
2026-09-23 17:43:48 +08:00
k88936 07bac0377e 修复:调用级拒绝里属于宿主与环境事实的错补回运行错误诊断
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m23s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m51s
Project CI / Frontend tests (pull_request) Successful in 2m8s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m22s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m45s
Project CI / Native shell tests (pull_request) Successful in 6m38s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m29s
- `DirectTurnError::is_reportable`:按变体判定哪几条调用级拒绝值得进 `.agent/runtime/errors` 与应用日志(环境未就绪、宿主状态取不到、项目目录锚不定),回合级失败恒 false(上游已写过诊断)
- `record_direct_codex_turn_failure` 改名 `record_direct_codex_failure` 并开放到 crate 内:它同时服务回合失败与可留痕的调用级拒绝,摘要 / 可重试 / 建议仍全部由 typed 分类判定
- 新增 `direct_turn_error_boundary_text`:命令边界唯一的文本投影——可留痕的拒绝补一份诊断并在返回串里带 `详情:` 引用,其余只输出 `Display`
- GUI 命令与 CLI 边界共用这一份投影:字符串只在边界生成一次,前端横幅的 `详情:` 展开与诊断留痕恢复分层改 typed 之前的行为
- 新增 4 项测试:可留痕拒绝写出诊断、用户侧拒绝不留痕、回合失败不在边界二次留痕、`is_reportable` 的变体集合
2026-09-23 15:59:32 +08:00
k88936 46b86527f9 注释:记录 Direct 命令接单化的 TODO 与实现要点
- `direct_runtime/user_input.rs` 命令入口加 TODO:目标形状(接单 + spawn、`turn.started` 与终态守卫下沉、接单后早退必须补终态、环境类失败升为回合级、宿主侧承担留痕与上报)与两条已决策的作废项
- `useDirectProjectChatController.ts` 的 catch 加同一份 TODO,说明它现在兼职"接单被拒"与"回合失败"、将来只剩前者
- 两处都指向草案 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`,本次不实施、不提交该草案
2026-09-23 15:36:03 +08:00
k88936 34b95b5826 文档:Direct 回合错误改 typed 的决策与影响口径
- ADR【DirectProject对话历史单一事实源】新增一条决策:回合失败在宿主内部是 typed 的、调用级拒绝与回合级失败不共用判据,线上载荷与命令边界仍由同一出口投影
- 同 ADR 修正原措辞:原生 error 现在按 `codexErrorInfo` 解析成 typed 分类,不再描述成"投影成 LlmError";影响一节补一条调用级拒绝只回命令边界、不写诊断不发失败事件
- decision-log 记本次决策、根因、明确不做项、影响范围与验证结果
2026-09-23 14:36:42 +08:00
k88936 a553967ab9 Direct 回合失败全链路改 typed 错误:不再靠字符串匹配分类
- 新增 `agent/direct_turn_error.rs`:`DirectTurnError` 每个变体自带字段(调用级拒绝与回合级失败不共用结构和判据),分流只认 `is_turn_failure()`,不再有 `kind` 字段 + 共用字段的伪结构化
- 分类判据从"对原因文本做子串匹配"改成 `match` typed 值:`DirectCodexNativeKind` 只解析 `codex-app-server-error:<kind>` 结构化前缀,原 `direct_turn_failure_kind` / 各 `contains` 词表判据删除
- `direct_runtime`:`run_direct_game_creator_turn_*` 返回 typed 错误;本地 `DirectCodexFailureStage` / `DirectCodexTurnFailure` 与并发前缀常量改由 typed 模型提供;调用级拒绝不进失败诊断、不发 `failed` 事件
- `codex_app_server`:执行适配器把宿主亲见的收场事实(通道断开 / 超时 / 中断)存成 typed 值;模型自报失败经 `DirectTurnError::from_model_call` 投影
- `direct_turn_failure`:终态判定收 typed 错误并投影出载荷 `kind` / `message`;删除 `DIRECT_TURN_FAILURE_{TRANSPORT,INTERRUPTED,TIMEOUT}_KIND` 与 `direct_turn_failure_kind`
- `direct_delivery` 返修控制流改用 `ReviewRequired`(不是失败);命令边界与 CLI 仍是 `Result<String, String>`,字符串只在 `Display` 一处生成,`wire_kind` 取值与可见文案与改造前逐一相同
2026-09-23 14:34:43 +08:00
k88936 69f6c2dc69 Direct 回合链路引入 typed 错误数据模型
- 新增 direct_turn_error 深模块:DirectTurnError 按变体各带字段,调用级拒绝与回合级失败分开
- 原生失败分类 DirectCodexNativeKind 由结构化前缀读入,事件载荷 kind 与旧口径逐条对齐
- 模型调用失败按平台 LlmError 分支投影成 DirectModelCallKind,反馈/重试/摘要/建议改由类型判定
- 跨进程边界仍由 Display 序列化成字符串,Rust 侧不再解析该字符串
2026-09-23 13:39:37 +08:00
k88936 26c6bdbbd5 同步文档:界面图参考语义建议退役
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
- UI 编辑器代码地图更新 resource/commands 职责、关键命令列表、两步工作流与类型文件数量

- UI 工作流资源桥接文档改为普通桥接从 structure-recognition 开始

- UI 编辑器工作流完成通知弹窗文档收敛为两个动作并删除分析参考图文案

- UI 编辑会话模块边界删除 suggestion seam 描述

- App 实施计划追加 2026-09-23 退役条目覆盖旧 slave_to 口径

- decision-log 记录退役决策、合并优先级恒 0 与无迁移后果及验证结果
2026-09-23 13:30:13 +08:00
k88936 81dc7b0cd6 前端移除分析参考图步骤与界面图元数据编辑
- model.ts 收敛为“识别界面结构 / 自动切分素材”两步并删除界面角色标签表

- 删除 suggestion 异步操作、结果通知分支、uiDesignSuggestions 与其 ts-rs 生成类型

- InputSidebar、InspectorSidebar、PreviewWorkspace 的界面图显示名改用 path basename,Inspector 只保留尺寸与资源 ID

- 删除因步骤退役而不可达的 ImportOverview,ToolNavigation 改为两格,删除“清空界面归属”提示

- requisites 删除角色/归属前置校验与参考图分析结果校验,importAdapter 不再写入界面图元数据

- 测试夹具去掉界面图元数据,删除界面角色与归属用例,历史用例改用 sprite 名称断言
2026-09-23 13:30:04 +08:00
k88936 e18a398cdf 退役界面图参考语义建议的 Rust 链路
- 删除 ui_editor/commands/ui_design_suggestion.rs 与 main.rs 的 suggest_ui_design_semantic 命令注册

- 收敛 UIDesignImage 为 path/pixel_size/pixels_per_unit,删除 UIDesignImageRole 与 UIDesignImageMetadata

- 结构识别改为每张界面图各自一棵树,删除 recognition_root_image_ids 与 slave_image_ids 及 ROOT/SLAVE 上下文标注

- 多树合并删除 slave_to 祖先链优先级,输入树优先级恒置 0 并留 TODO

- 持久化校验删除 slave_to 引用与环校验,html 片段注释不再携带界面图 name/description

- 页面级工作流构造页面设计图时不再写入界面图元数据
2026-09-23 13:29:39 +08:00
k88936 217f5e8d81 Merge remote-tracking branch 'origin/master' into feat/fail-as-event 2026-09-23 11:50:42 +08:00
k88936 d8c19d9187 新增 UI 编辑器代码地图与模块职责文档
- 定义 `Rust` 权威层、`React` 会话层与视图层的职责边界
- 描述数据流、关键模块设计与核心命令
- 补充扩展指引与关联文档链接
- 更新 `docs/README.md` 引用路径
2026-09-23 11:46:31 +08:00
k88936 29d4b24226 宿主终态由事实判定:模型自报失败投影进既有错误通道,失败载荷不再被收尾阶段吞掉
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m6s
Project CI / Frontend tests (pull_request) Successful in 2m2s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m43s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 8m33s
Project CI / Native shell tests (pull_request) Successful in 6m12s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m3s
- codex_app_server 的 failed 分支先投影原生 turn.error(复用 game_creator_codex_app_server_failed_turn_error),把它当作本回合的错误结果返回:载荷形状不变,RepairRequired 保持自己的原语义,交付报告不再顶掉原因
- direct_turn_terminal 去掉 model_status 入参:判定改为「宿主当场记下的失败 -> 本回合错误结果是 Err -> 只有账本读不出来时才用交付报告」,有载荷一定写 status="failed",没载荷才用收尾阶段推出来的 status
- 执行适配器把宿主观察到的失败记在适配器上(fail_turn / turn_failure / host_stop_requested):看门狗与终态判定共用同一条事实,不用调用点局部变量
- 单测:投影后的原生失败压过被收尾改写的会话状态、账本读不出来仍带载荷、宿主自己关的连接不算失败(断言改用真实原因)
2026-09-23 11:20:48 +08:00
k88936 258c2f6cae 文档:终态由事实判定,模型自报失败的原生错误投影进既有错误通道
- ADR【DirectProject对话历史单一事实源】补一条决策:终态按事实取原因、有载荷必 failed;模型自报失败的 turn.error 投影成 LlmError 走同一条错误通道,不为载荷新增字段
- ADR「影响」补一条:可见文案仍走既有映射,区别只是原因改由事件载荷给出、命令返回恢复运行错误横幅
- 技术方案【DirectProject Codex原始历史与异常恢复】写明 lifecycle_status 没有终态否决权,以及原生错误的投影口径
- decision-log 记本次决策、根因、不做项、影响范围与验证方式
2026-09-23 11:20:39 +08:00
k88936 8b8e95908c Merge remote-tracking branch 'origin/feat/fail-as-event' into feat/fail-as-event 2026-09-23 10:07:21 +08:00
k88936 3467042000 Merge branch 'master' into feat/fail-as-event
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
2026-09-22 20:19:23 +08:00
k88936 8f2e5b4381 文档:执行通道断开定为失败终态,诊断记在执行适配器上
- ADR【DirectProject对话历史单一事实源】补一条决策:连接级故障与回合事件通道关闭同样带 failure{kind:"transport-failed"},原因用宿主当场写下的诊断,判据是"适配器是否已由宿主主动关闭"
- ADR「影响」补一条:断开时用户看到的仍是既有映射结果,真实诊断在事件载荷、宿主交付报告与运行日志里,改可见文案属于映射规则变更
- 技术方案【DirectProject Codex原始历史与异常恢复】同步线上形状,并写明失败事实为什么必须记在执行适配器上(看门狗会抢时序)
- decision-log 记本次决策、判据、不做项、影响范围与验证方式
2026-09-22 18:25:48 +08:00
k88936 7dca17d517 执行通道断开也算失败终态:诊断记在执行适配器上,事件带 transport-failed 载荷
- ExecutionAdapter 新增 transport_failed / transport_failure:调用方只给宿主诊断,文案、报告与"这算不算失败"都归适配器管;先同步记事实,再把同一份原因补进宿主交付报告
- 判据收在适配器里(is_closed):宿主自己收束(正常终态 / 用户主动停止 / 预算与交付收尾)会关掉同一条连接、发同一个 TransportClosed,那些不算失败,调用点两条分支的控制流保持不变;连接自己断掉才算,且只认第一份原因(第一份最接近现场,含 exitStatus 与 stderr 摘要)
- lifecycle_status 见到这条事实一律返回 failed:连接不是被本轮主动收束,也没有"用户主动停止"这层授权,报成 interrupted 只会让界面停在"本轮已结束"却不给原因
- 连接级故障(app-server 进程退出 / 流断 / JSON 行越界 / stderr 读取失败)在收束连接之前先把事实记到本回合的执行适配器上,避免与盯着同一个 closed 标志的看门狗抢时序
- direct_turn_failure 增加第三来源且优先级最高:通道断开时原因取宿主诊断,不取只会说"收束到哪一步"的交付报告
- 单测三条:适配器把诊断记成失败终态且只认第一份原因;宿主自己关的连接不算失败;失败载荷优先取宿主诊断(含与 LlmError 并存时的优先级)
2026-09-22 18:25:37 +08:00
k88936 80b15b24ae appSurface 用例:失败只经终态事件收口,界面不再停在"还在处理"
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
- 新增「closes the turn from the host failure payload instead of leaving it running」:宿主发过 turn.started 之后以 failure 载荷收场并让命令失败,断言失败文案来自事件载荷(经同一份可见文案映射)、"陶泥儿正在处理"消失、终止钮消失、输入盒回到「发送」
- 同一条用例反向断言命令返回的错误原文不进聊天:那条通道只负责运行错误横幅
- 变异校验:让 reducer 不落失败说明条目时该用例变红(1 failed),恢复后绿
2026-09-22 17:40:17 +08:00
k88936 b432556ee3 reducer 用例:失败终态落说明条目、文案与横幅同源、重放不重复
- 新增「失败终态(turn.completed 带 failure 载荷)」六条用例:失败照样收口并冻结终点、说明条目按本轮开口身份派生、本轮开口条目拿到边界
- 可见文案与运行错误横幅共用同一份映射(点名 codex-app-server-error:context-window-exceeded)
- 重复 / 迟到的失败终态不追加第二条说明、不抬高冻结终点、不复活运行态
- 身份不匹配的失败终态不动正在跑的这一轮;空原因不落说明条目但终态照样收口
- 没有身份时用事件时间派生说明身份,两轮失败不会合并成一条
2026-09-22 17:40:17 +08:00
k88936 258645f182 前端失败说明改由事件驱动:turn.completed.failure 落成本轮说明条目,命令返回只留横幅
- 新增 conversation/directTurnFailure.ts:失败说明条目的展示身份(本轮开口身份 + :failure)与可见文案(复用 projectRuntimeVisibleError)两条口径集中一处
- directThreadChat 的 turn.completed 分支读 failure 载荷:非空原因先落成本轮最后一条说明条目,再走同一个收口函数;失败不再是第二套生命周期
- useDirectProjectChatController 的失败分支不再写聊天气泡:聊天文案唯一来源是事件,命令返回只保留运行错误横幅(含详情 long detail)与诊断留痕
- 数据流、时序与投影注释同步:标注失败说明来自事件、本地通道只剩终止说明与壳层 announce
2026-09-22 17:40:17 +08:00
k88936 5cf4a018b3 宿主终态接线:失败写进 turn.completed 的 failure 载荷,并在 turn.started 之后武装兜底守卫
- codex_app_server 的 DirectProject 终态改用 direct_turn_failure 判定:失败走 turn_completed_failed(原因脱敏 + 截断后写进同一个事件),其余仍走 turn_completed(status)
- 失败判定两个来源:collect_result 是 Err 时用错误本身当原因;collect_result 是交付报告但 status 已判成 failed 时用那份报告当原因
- turn.started 进入队列后立即武装 DirectTurnFailureGuard,写完终态 disarm:panic、回合 future 被丢弃、终态之前的早退都会补一条 host-dropped 失败终态,前端不会停在"还在跑"
- 定向 `cargo test direct_`(438 passed,含 wire / manager / 失败策略模块)
2026-09-22 17:40:17 +08:00
k88936 5d9223c32e 失败终态策略独立成模块:分类、原因脱敏与 Drop 兜底守卫
- 新增 agent/direct_turn_failure.rs:LlmError → 稳定分类(timeout / model-failed / transport-failed / request-rejected)、判定"终态是不是失败"并给出脱敏截断后的原因、DirectTurnFailureGuard(turn.started 之后武装、写完终态 disarm,Drop 时补 host-dropped 失败终态)
- 守卫兜底覆盖 panic / future 被丢弃 / 终态之前的早退;kill -9 与 turn.started 之前的早退写进模块注释,明确不为它们补路径
- agent.rs 注册模块并再导出
- 5 条用例:错误分类映射、只有 failed 终态带载荷、原因脱敏 + 按字符截断、armed 后 Drop 补终态、disarm 后不再产出事件
2026-09-22 17:40:17 +08:00
k88936 d32c99c927 事件协议:turn.completed 增加可选 failure 载荷,失败终态与正常终态同权入锚点
- direct_thread_wire 新增 DirectTurnFailure{kind,message} 类型,给 TurnCompleted 增可选 failure 字段,并补 turn_completed_failed 构造器与 failure 读取器
- with_user_item_id 显式带上 failure:原先把 TurnCompleted 写成 `..` 会静默吞掉失败载荷,身份与原因必须一起流转
- 新增 wire 用例:失败终态带载荷、正常终态不带且回写不补 null、缺载荷的 failed 事件仍可反序列化
- direct_thread_manager 增回归用例:turn.completed(status=failed) 必须顶替更早的 turn.started 成为 lifecycle_anchor,重放不会把已收口的回合看成"还在跑"
- 重新生成 ts-rs 绑定(新增 DirectTurnFailure.ts、DirectThreadEvent.ts 增 failure 字段)并按 prettier 格式化
2026-09-22 17:40:17 +08:00
k88936 d36f5842b6 文档:失败回合终态定为 turn.completed 带 failure 载荷,宿主 Drop 守卫兜底
- ADR【DirectProject对话历史单一事实源】补三条决策:终态事件只有 turn.completed,失败时 status="failed" 必须带 failure{kind,message};宿主 Drop 守卫在 turn.started 之后武装、写完终态即解除;失败原因只走事件这条通道,聊天说明的展示位保留、数据来源换成事件
- 同 ADR「影响」补两条已知边界(进程被强杀时没有 Drop、turn.started 之前的早退不产回合也不补终态)与「可见文案映射规则不变」的口径
- 技术方案【DirectProject Codex原始历史与异常恢复】同步线上形状:turn.completed 增加可选 failure,并写明失败终态与正常终态同权顶替 lifecycle_anchor
- decision-log 记本次决策、明确不做项、影响范围与验证方式
2026-09-22 17:40:17 +08:00
k88936 981a6b0021 Revert:撤掉前端「命令返回就收口」的兜底,改由宿主 turn.failed 事件收口
- 撤销 a35956f3e 的前端实现:directThreadChat 的 commandClosedTurnUserItemId / stopDirectThreadTurn、subscription 的 stopCommandTurn、controller 失败分支的调用,以及随附的两处用例
- 原因:失败语义改由宿主事件(turn.failed)表达,前端不再自造第二条「结束」判定路径,也不再在失败路径上补本地消息
2026-09-22 17:40:16 +08:00
k88936 d5e06de33e 固化键盘菜单键不被右键手势拦截的口径
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m57s
Project CI / Frontend tests (pull_request) Successful in 3m6s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m41s
Project CI / Native shell tests (pull_request) Successful in 6m13s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m32s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m50s
- 实测 Chromium 151 下键盘菜单键触发的是 button: -1,previewRightPanGesture 注释写明只认按钮 2 的原因
- previewRightPanGesture.test.ts 增加 button: -1 不拦截的判据用例
- previewRightPanDrag.test.tsx 增加键盘菜单键仍走原有节点菜单路径的集成用例
- decision-log 条目补上该实测结论与新增用例的验证口径

验证:npx vitest run previewRightPanDrag.test.tsx previewRightPanGesture.test.ts(13 passed)
2026-09-22 15:35:51 +08:00
k88936 0ab04d0f64 预览画布接入右键拖拽平移
- PreviewWorkspace 右键按下记录起手并捕获指针,越过共用阈值后按起点全量 delta 平移视口,抬起时由 resolvePreviewRightPanRelease 裁决平移还是节点菜单
- 捕获阶段拦截按钮 2 的 contextmenu,拦截窗口保留到下一次指针按下之前,兼容"按下触发"与"抬起后触发"两类平台时序
- 右键抬起用 [data-node-id] 加树容器新增的 data-tree-id 命中节点,沿用"右键即选中该节点"语义;未平移但抬起在预览外时不弹菜单,空白网格右键静默
- 视口接线共享 isPanning,让空格、中键、右键三个平移绑定共用 grabbing 光标;onPointerCancel 拆成不进菜单的独立处理
- 新增 tests/previewRightPanDrag.test.tsx:右键拖拽只平移不弹菜单、阈值内仍弹菜单、空白右键不改选择、预览外抬起不弹、非右键 contextmenu 仍即时、中键平移回归、右键平移后左键拖动仍可用

验证:npx vitest run 定向 7 个文件 59 passed、apps/ai-game-creator-shell npx tsc --noEmit、改动文件 eslint、npm run check:encoding、git diff --check
2026-09-22 15:32:19 +08:00
k88936 f525b6dc70 新增预览右键平移手势状态机
- 新增 previewRightPanGesture.ts:按下起手、按共用阈值判定是否进入平移、按起点全量 delta 复用 moveViewportFromPan 算视口
- 抬起裁决统一收口到 resolvePreviewRightPanRelease:平移过就只回视口,未平移且在预览内才按节点菜单处理
- shouldInterceptRightContextMenu 记录"按钮 2 且处于右键按下序列"的拦截窗口,兼容按下触发与抬起后触发的两类平台时序
- 新增 tests/previewRightPanGesture.test.ts 覆盖阈值边界、越阈值后不回弹、全量 delta、预览外抬起与拦截判据

验证:npx vitest run previewRightPanGesture.test.ts(5 passed)
2026-09-22 15:27:30 +08:00
k88936 96e7ba7808 预览拖动阈值抽成公共常量模块
- 新增 previewDragThreshold.ts,导出 DRAG_THRESHOLD_SCREEN_PX 与屏幕坐标阈值判定 passedDragThresholdScreen
- useNodeTransformInteraction 删除本地阈值常量,改为复用该模块,左键拖动/缩放行为不变

验证:npx vitest run useNodeTransformInteraction.test.tsx previewWorkspaceZoom.test.tsx(18 passed)
2026-09-22 15:26:11 +08:00
k88936 65e06c4808 记录预览画布右键拖拽平移的交互约定
- decision-log 新增 2026-09-22 条目:右键拖拽平移的判定链、菜单改到抬起弹出、按下即拦截 contextmenu 的实测依据与代价
- 【UI编辑器】拖动变换提交边界补「视口平移不写 State、不进历史」以及右键平移与节点菜单按同一拖动阈值互斥的约定
- 同一文档的实现边界补拖动阈值落点 previewDragThreshold.ts 与右键裁决落点 previewRightPanGesture.ts
2026-09-22 15:25:32 +08:00
k88936 4cd5fb8d72 Merge remote-tracking branch 'origin/master' into feat/ui-editor-v3
# Conflicts:
#	apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationQueue.ts
#	apps/ai-game-creator-shell/src/features/resource-canvas/useResourceCanvasGenerationPlaceholders.ts
#	apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanel.test.tsx
#	docs/project-memory/shared-memory/decision-log.md
2026-09-22 14:08:41 +08:00
k88936 47c07f9d12 迁移节点变换交互测试接口
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m8s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m25s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m24s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m13s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m34s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m39s
Project CI / Frontend tests (pull_request) Successful in 5m56s
Project CI / AI game creator shell web tests (pull_request) Successful in 8m34s
Project CI / Repository checks (pull_request) Successful in 15m18s
Project CI / Native shell tests (pull_request) Successful in 22m28s
Project CI / Backend tests (pull_request) Successful in 27m38s
将旧单树测试夹具改为 trees 与 logicalSizes 参数

为拖拽和缩放入口补充所属 treeId 并更新树移除语义
2026-09-18 22:33:21 +08:00
k88936 0904184412 校正预览上下文菜单测试
覆盖上下文菜单回调的可选树标识参数

区分编辑器树目标与最终预览的无树标识回调
2026-09-18 22:18:58 +08:00
k88936 aaa2eb482a 更新最终预览框线测试语义
无框线场景不再传入选中节点以匹配选中态高亮规则

保留显式 showFrame 时的选中框与控制点断言
2026-09-18 22:08:53 +08:00
k88936 1679e72deb 修正删除快捷键页面测试
从仍挂载的窗口目标触发全局删除快捷键

按全部同名节点消失断言删除结果
2026-09-18 22:05:16 +08:00
k88936 4844543061 补齐检查提示动画测试名称
让动画 mock 匹配仅重启提示动画的现役筛选逻辑

保留非提示动画不被重启的生产行为
2026-09-18 22:00:58 +08:00
k88936 99132c9fa7 补齐 UI 状态持久化测试偏移字段
让当前 schema 夹具继续验证原有业务约束
2026-09-18 21:41:55 +08:00
k88936 944d2aa3db 更新 UI 编辑器返回交互测试
按现役浮动关闭按钮的可访问名称查询
2026-09-18 21:14:57 +08:00
k88936 7c16a35fda 兼容缺少偏移量的预览运行数据
校验 offset 前安全处理旧状态和测试夹具
2026-09-18 20:14:24 +08:00
k88936 e4369cd82e Merge remote-tracking branch 'origin/master' into feat/ui-editor-v3
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 5m0s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m54s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 7m15s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 6m32s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 6m56s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m21s
Project CI / Frontend tests (pull_request) Failing after 4m23s
Project CI / Repository checks (pull_request) Failing after 5m22s
Project CI / Native shell tests (pull_request) Successful in 9m10s
Project CI / Backend tests (pull_request) Successful in 9m41s
Project CI / AI game creator shell web tests (pull_request) Failing after 5m32s
# Conflicts:
#	apps/ai-game-creator-shell/tests/uiEditorPage.test.ts
#	docs/project-memory/shared-memory/decision-log.md
2026-09-18 18:44:11 +08:00
k88936 54b9bd4354 同步节点偏移量 TypeScript 绑定
更新 ts-rs 生成的 NodeOffset 类型文件
2026-09-18 17:30:53 +08:00
k88936 365e530b5f 修正测试偏移类型导入
从公开 offset 模块导入 NodeOffset
2026-09-18 17:23:06 +08:00
k88936 d46cdfb282 抽取保存结果弹窗操作底栏
统一关闭与重试按钮的布局和样式
2026-09-18 17:17:45 +08:00
k88936 2142027f85 简化保存结果弹窗内容分支
用显式早返回替换嵌套三元渲染
2026-09-18 17:15:50 +08:00
k88936 8fdf39f37e 清理预览初始适配无效依赖
移除仅通过 ref 读取的画布尺寸依赖
2026-09-18 17:14:16 +08:00
k88936 70c65c37d5 保证拖拽释放提交最新偏移
从指针释放坐标直接计算树位置
2026-09-18 17:12:30 +08:00
k88936 e72637789c 校验预览树偏移坐标
仅将有限偏移量纳入画布布局
2026-09-18 17:11:23 +08:00
k88936 0a102ea1c3 合并预览节点类型导入
复用单一 Node 类型导入声明
2026-09-18 17:10:04 +08:00
k88936 1ab7fa9991 校验预览画布像素比例
避免无效 pixels_per_unit 产生无限尺寸
2026-09-18 17:09:17 +08:00
k88936 6d02a0c29f 移除无读取的代码生成错误状态
删除失效的 generateError 状态及写入路径
2026-09-18 17:04:10 +08:00
k88936 a6c3db60fb 收窄编辑器操作生命周期接口
移除未被调用的 setRunning 与 reset 暴露
2026-09-18 16:57:42 +08:00
k88936 c921ae3b16 简化编辑器概览分支渲染
用显式条件替换嵌套三元表达式
2026-09-18 16:52:37 +08:00
k88936 31c084fce6 保留保存失败的底层错误信息
将实际异常传递到保存状态和重试提示
2026-09-18 16:48:53 +08:00
k88936 1e7a3f17ff 避免返回确认与失败弹窗重叠
返回路径保存失败时沿用确认弹窗错误状态
2026-09-18 16:45:43 +08:00
k88936 3ce2977e32 固定节点偏移量 TypeScript 元组导出
为二维数组字段补充 ts-rs 元组覆盖
2026-09-18 16:43:26 +08:00
k88936 f4e9414d63 限定状态提示动画重启动范围
仅重启动 UI 编辑器关注动画而不影响过渡效果
2026-09-18 16:41:12 +08:00
k88936 2d8b51885a 优化切分概览卡片布局
将窄侧栏概览调整为两列网格
2026-09-18 16:39:39 +08:00
k88936 91f534ed66 优化识别概览卡片布局
将窄侧栏概览调整为两列网格
2026-09-18 16:38:25 +08:00
k88936 f455d3edb2 清理未使用的界面图校验码
移除不会被发出的 missing-image 类型
2026-09-18 16:37:48 +08:00
k88936 bec225436f 简化状态节点轮询索引逻辑
用显式分支替换嵌套三元表达式
2026-09-18 16:35:25 +08:00
k88936 ce309a3b28 忽略未定义的节点元数据补丁
保留部分更新对未提供字段的原有语义
2026-09-18 16:33:37 +08:00
k88936 abd95a6231 消除复制路径未处理拒绝
复制失败仅保留界面错误状态
2026-09-18 16:31:04 +08:00
k88936 08c3b89b67 修复跨树节点选择同步界面图
选择节点时同步激活所属设计图
2026-09-18 16:28:49 +08:00
k88936 3371337346 修复树节点回调未使用参数
为跨树选择回调参数添加下划线前缀
2026-09-18 16:25:54 +08:00
k88936 899a591cda 区分语义建议工具调用缺失错误
恢复缺失工具调用的独立日志阶段和提示
2026-09-18 16:23:27 +08:00
k88936 0988c4907a 区分识别工具调用缺失错误
恢复识别工具缺失的独立日志阶段和提示
2026-09-18 16:20:49 +08:00
k88936 d370648867 区分合并工具调用缺失错误
恢复缺失工具调用的独立日志阶段和提示
2026-09-18 16:13:58 +08:00
k88936 4f4fb0873c 修复切分命令测试节点构造
补齐 NodeOffset 默认值以恢复 Rust 测试构建
2026-09-18 16:09:03 +08:00
k88936 6b8a106414 修复合并命令测试节点构造
补齐 NodeOffset 默认值以恢复 Rust 测试构建
2026-09-18 16:08:53 +08:00
k88936 55a8513de4 更新UI编辑器深模块架构文档
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 4m16s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 4m23s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 4m24s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 4m26s
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m52s
Project CI / Repository checks (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m28s
Project CI / Frontend tests (pull_request) Failing after 3m30s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m33s
Project CI / Native shell tests (pull_request) Successful in 9m17s
记录状态迁移、几何、不变量与操作生命周期 seam

同步结构化 LLM 适配器边界与验证结果
2026-09-18 10:24:03 +08:00
k88936 d986dbaeb0 收敛UI编辑器结构化LLM动作适配器
统一必需 tool-call 定位与有界 JSON 参数解析

让语义建议、结构识别、组件绑定和多树合并复用稳定机械

保留各操作的 prompt、schema、领域校验与 materializer
2026-09-18 10:23:35 +08:00
k88936 16c6b9198e 深化UI编辑器前端状态与几何模块
新增 React-free 语义状态迁移 seam

收敛 State 级节点几何与保存前不变量校验

统一四类异步编辑器操作生命周期

补充状态迁移和不变量测试
2026-09-18 10:23:04 +08:00
k88936 4df4708ba1 Merge remote-tracking branch 'origin/master' into feat/ui-editor-v3
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 3m0s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 3m4s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 3m7s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 3m9s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m41s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m32s
Project CI / Frontend tests (pull_request) Failing after 4m47s
Project CI / Repository checks (pull_request) Failing after 4m39s
Project CI / Native shell tests (pull_request) Successful in 7m14s
Project CI / Backend tests (pull_request) Successful in 8m12s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m54s
# Conflicts:
#	docs/project-memory/shared-memory/pitfalls.md
2026-09-16 14:57:26 +08:00
k88936 1e992bcdf8 扩大未知 JavaScript 异常上报原则
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
明确所有未解决和非契约化 JS 异常必须继续抛出

说明用户 fallback 不得替代全局 error report 链路
2026-09-14 19:47:08 +08:00
k88936 8368aa262c 补充剪贴板异常上报约定
记录 UI fallback 后仍需抛出原始原生错误

明确未知异常必须进入 error report 链路
2026-09-14 19:43:08 +08:00
k88936 e9ddbf16da 重构预览网格步长计算
提取预览网格自适应步长到独立模块

补充缩放网格计算与背景样式测试

同步预览画布缩放交互文档
2026-09-14 19:40:13 +08:00
k88936 22ac1f4c0b 修复 UI 编辑器剪贴板写入权限
为主窗口补充 clipboard-manager 写文本权限

保留手动复制提示并继续抛出原始 Tauri 剪贴板错误
2026-09-14 19:39:34 +08:00
k88936 9910a0eec0 优化 UI 编辑器复制反馈布局
将复制失败提示与操作按钮分离排列
2026-09-14 19:06:15 +08:00
k88936 70981b9ca9 完善 UI 编辑器结果弹窗覆盖
增加普通保存成功与保存后生成失败的场景断言

验证路径弹窗和失败重试按钮的可见行为
2026-09-14 19:01:44 +08:00
k88936 8a0d5600b3 拆分 UI 编辑器路径复制按钮
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 2m27s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m23s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m5s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m8s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m57s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m15s
Project CI / Frontend tests (pull_request) Failing after 3m22s
Project CI / Repository checks (pull_request) Successful in 3m38s
Project CI / Native shell tests (pull_request) Successful in 6m42s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m12s
Project CI / Backend tests (pull_request) Successful in 8m24s
将剪贴板状态与写入逻辑提取为独立小组件

保持保存结果弹窗只负责结果布局与动作编排
2026-09-14 18:59:51 +08:00
k88936 184d88dbb8 修复多树预览受左侧选中图片限制
移除 activeImageId 对预览拖动和缩放手势的门控

按树 wrapper 提供几何上下文并允许任意树操作

保留 activeImageId 仅用于左侧图片与图片级检查
2026-09-14 18:59:51 +08:00
k88936 791794c0a6 记录 UI 编辑器保存结果弹窗约定
补充保存与代码生成结果弹窗及相对路径复制行为

记录复制失败、重试和保存并返回边界
2026-09-14 18:59:51 +08:00
k88936 9cfb47d945 补充 UI 编辑器保存结果测试
覆盖保存和生成成功弹窗与项目相对路径复制

覆盖保存失败、冲突和剪贴板失败反馈
2026-09-14 18:59:51 +08:00
k88936 27859d3e19 接入 UI 编辑器保存结果弹窗
让普通保存和保存并生成流程返回结构化结果

将成功与失败反馈接入独立弹窗并保留保存并返回直返行为
2026-09-14 18:59:51 +08:00
k88936 43f3780a38 新增 UI 编辑器保存结果弹窗
新增保存成功、代码生成成功和失败状态弹窗

提供项目相对路径复制按钮及复制失败提示
2026-09-14 18:59:51 +08:00
k88936 a1f9149c27 UI编辑器移除预览模式残留类型
完成三开关替代 render mode 的类型清理
2026-09-14 18:59:51 +08:00
k88936 f0bba18841 UI编辑器收口多树上下文菜单目标
预览菜单携带所属树 ID

避免非当前图片树的结构操作写错目标
2026-09-14 18:59:51 +08:00
k88936 f679cfa179 UI编辑器完善多树创建与文档契约
统一识别和合并结果的 createTree 流程

按选中节点所属树驱动 Inspector 编辑

补充多树预览与树级偏移权威文档
2026-09-14 18:59:50 +08:00
k88936 f68ffcfecd UI编辑器补充合并树偏移构造
为合并产出的节点补齐 NodeOffset
2026-09-14 18:59:50 +08:00
k88936 ec94a4fe00 UI编辑器重构多树预览与独立开关
预览画布同时渲染全部界面树并支持树级拖动

拆分显示框线、原图和组件开关

预览节点选择与左侧当前图片解耦
2026-09-14 18:59:50 +08:00
k88936 46f8592375 UI编辑器补充树级偏移模型
新增 Node offset min/max 数据结构

统一新树默认横向布局入口

增加树级偏移状态更新操作与实施计划
2026-09-14 18:59:50 +08:00
k88936 cdf2882302 补齐自动切分概览字体属性
向 SeparationOverview 传入 fonts 数据

修复 UI 编辑器 TS2741 类型错误
2026-09-14 18:59:50 +08:00
k88936 d156113e71 修复 UI 编辑器待办循环高亮动画
修复受控树选中回调清除 Inspector 高亮的问题

循环游标同时使用界面树与节点 ID

补充多节点循环和动画重启回归测试
2026-09-14 18:59:50 +08:00
k88936 a8f2ac17be 注释掉 InspectorSidebar 中的 LayoutEditor 组件以暂时禁用布局编辑功能 2026-09-14 18:59:50 +08:00
k88936 43ac9a5761 补充 UI 编辑器交互回归测试
从实际节点事件目标验证删除快捷键

验证 portal 右键菜单启用后会执行操作并关闭
2026-09-14 18:59:50 +08:00
k88936 e992e0b35c 压缩 UI 编辑器概览布局
将识别概览指标改为四列横向排列

将素材切分概览改为四列网格以减少纵向高度
2026-09-14 18:59:50 +08:00
k88936 e389b54a3a 修复 UI 预览画布交互
为无限视口添加跟随平移和缩放的点阵背景

恢复画布快捷缩放与初始适配比例控制

保留节点平移选择并屏蔽浏览器原生拖拽选择
2026-09-14 18:59:50 +08:00
k88936 84c780a6f8 重构 UI 编辑器全屏外壳
通过 portal 将资源编辑器挂载到 body 并覆盖标题栏以下区域

移除外层 FocusTrap,保留内部确认对话框的模态行为

统一左右浮动面板的顶部与底部留白
2026-09-14 18:59:50 +08:00
k88936 03f900fbe5 布局重构:
- 预览改为全区域背景层。
  - 左右面板、顶部流程条改为覆盖层,可遮挡预览。
  - 左右面板开合状态仅保存在当前 React 会话。
  - 增加左右面板“收起/展开”入口。
  - 编辑器关闭入口改为单独的 ×,继续通过 onBack 注入关闭逻辑。
  - 保留全局顶部栏与现有快捷键、业务状态、保存流程。
  - 底层页面仍由现有父级路由控制,不新增 URL 状态。
2026-09-14 18:59:50 +08:00
215 changed files with 17249 additions and 8266 deletions
@@ -5,131 +5,42 @@ description: Guide use of Genarrative's hosted external editor/canvas MCP or asy
# Genarrative External Editor API
Discover the live integration through `GET https://www.genarrative.world/api/external/v1/agent-integration.json`. Treat `GET https://www.genarrative.world/api/external/v1/openapi.json` as the field-level source of truth. In this repository, the same contract is `docs/openapi/genarrative-external-v1.openapi.json`.
Use Genarrative to manage canvas projects and asset-library records, generate images, character animations, videos, and audio, and edit images. Discover the hosted integration at `https://www.genarrative.world/api/external/v1/agent-integration.json`.
Prefer the hosted Streamable HTTP MCP at `https://www.genarrative.world/api/external/v1/mcp` when the Agent supports remote MCP with a custom Bearer token. It exposes the External v1 operations as tools and the Skill documentation as resources; it does not require a local MCP server. Use this complete Skill package when remote MCP is unavailable or local-file upload needs client-side orchestration.
Connect to `https://www.genarrative.world/api/external/v1/mcp` using Streamable HTTP and a Bearer API Key. Read tool descriptions and input schemas from `tools/list`; read the documents below through `resources/read` when needed. The hosted service needs no local MCP server. For direct REST, use the live `/api/external/v1/openapi.json` contract (in this repository: `docs/openapi/genarrative-external-v1.openapi.json`).
Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses only Python stdlib, reads the local private API Key file, keeps the production base URL fixed, uploads local references, and wraps asynchronous submission, polling, and result retrieval.
## Working with the Service
## Workflow
1. Discover the integration manifest. Choose hosted MCP when supported; otherwise use the helper or direct REST.
2. Before the first generation in a new conversation, obtain a canvas name unless an existing `projectId` and `assetFolderId` were supplied. Create or reuse a project and a same-name asset-library folder. Retain `canvasName`, `projectId`, `assetFolderId`, and the current art spec.
3. Normalize art requests into a reusable spec. Ask only for missing values that block the selected operation. Reuse the spec until the user changes its style, subject family, palette, format, or constraints.
4. Infer the operation from the user's intent. Do not ask the user to select an API unless two operations would produce materially different artifacts.
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to operations that accept object references; never substitute a temporary signed URL. For an icon-spritesheet primary spec, additionally create a project resource or asset record with `assetKind="icon-spec"`, then pass the returned resource or asset ID as `referenceId`.
6. For generation endpoints that support the fields, include `projectId`, `assetFolderId`, an asset label, and `canvasCompletion` so the result enters both the canvas and its same-name library folder.
7. Treat every generation POST as asynchronous. Send one stable `Idempotency-Key` per logical request, retain the returned `operationId`, and poll the returned `statusUrl` or `GET /api/external/v1/generations/{operationId}` according to `pollAfterMs`.
8. Consume `result` only after `status=completed`. On `failed`, surface the safe error. On a client timeout or lost response, retain the operation/key; do not create a replacement request.
9. Reload the normal project or asset-library read endpoint when the caller needs complete authoritative state. Generation results are intentionally compact.
10. Stay within `/api/external/v1`. Never call internal workers, queues, admin/profile APIs, or SpacetimeDB endpoints unless the user explicitly changes scope.
## Essential Invariants
- Authenticate MCP and business API calls with `Authorization: Bearer <tnr_sk_...>`. Never ask the user to paste a key into chat or place one in repository files.
- All nine generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
- Retry an uncertain submission only with the exact same body and the same idempotency key. A polling timeout is not permission to generate again.
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
- Icon spritesheet generation requires an explicit `sliceMode` and has no default. Use `sliceMode="grid"` with the `gridX` and `gridY` the requirement actually names (1-32 each) only for equal grid cells or fixed slots; use `sliceMode="connected-components"` for free-form sheets or an open number of subjects, and constrain the count with `sliceCount` instead of inventing grid dimensions. `connected-components` must not carry `gridX`/`gridY`; an omitted, contradictory, or misapplied declaration returns 400 before billing.
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
- Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record.
- Select tools by the requested outcome. Use `find_canvas_projects` and `find_assets` to locate existing context; create projects or folders only when the task needs them. A folder need not have the same name as the project.
- For generation, specify project, library, and `canvasCompletion` fields only as supported by the selected tool and needed for the requested destination. Do not duplicate records already created by generation.
- Upload local references using `prepare_asset_upload`: request a ticket, transfer the file from the client, then confirm the object. Confirmation does not create a canvas layer or a project/library record. Use the reference type accepted by the target tool; some operations require a registered resource or asset ID rather than an object key.
- Generation is paid and asynchronous. Keep one stable `idempotencyKey` per logical generation and retain the returned `operationId`. Call `check_generation` according to `pollAfterMs`; consume `result` only after `completed`, and report the safe error on `failed`. A polling timeout does not justify another generation.
- Read actual artifacts and warnings before claiming the requested deliverable is complete. Use project/library reads for complete persisted records, and `find_assets` with `action=get_download_url` for temporary media access.
- Keep API Keys and temporary upload/download credentials out of chat, repository files, and logs. Business calls operate within the API Key's owner and scopes.
## Documentation Navigation
Read only the references needed for the task, but always verify exact schemas and enums against live OpenAPI:
Read the reference relevant to the current operation; exact input fields and enums come from the tool schema or OpenAPI.
- `references/capability-routing.md`: read before selecting an MCP tool or REST operation, creating a canvas session, or working in the AI game creator visual DAG.
- `references/api-operations.md`: read when constructing project, canvas, asset-library, upload, generation, or generation-status calls.
- `references/authentication-and-safety.md`: read before handling credentials, local files, OSS form upload, retries, private media, or logs.
- `references/requests-and-outputs.md`: read before building generation payloads, polling, interpreting compact results, applying canvas completion, or handling post-processing warnings.
| Need | Reference | MCP resource URI |
| --- | --- | --- |
| Choose tools and actions by user intent | [Capability routing](references/capability-routing.md) | `genarrative://external-editor/skill/references/capability-routing.md` |
| Map tool calls to REST operations | [API operations](references/api-operations.md) | `genarrative://external-editor/skill/references/api-operations.md` |
| Configure credentials, upload files, handle retries and deletion | [Authentication and safety](references/authentication-and-safety.md) | `genarrative://external-editor/skill/references/authentication-and-safety.md` |
| Construct requests, poll results, place media, handle warnings | [Requests and outputs](references/requests-and-outputs.md) | `genarrative://external-editor/skill/references/requests-and-outputs.md` |
The hosted MCP exposes the same documents through:
`genarrative://external-editor/usage` contains the short service instructions; `genarrative://external-editor/openapi` contains the REST contract. This entry is available at `genarrative://external-editor/skill`. Reading a resource does not install the downloadable Skill or its Python helper.
- `genarrative://external-editor/skill`
- `genarrative://external-editor/skill/references/capability-routing.md`
- `genarrative://external-editor/skill/references/api-operations.md`
- `genarrative://external-editor/skill/references/authentication-and-safety.md`
- `genarrative://external-editor/skill/references/requests-and-outputs.md`
- `genarrative://external-editor/openapi`
## Direct REST and Local Helpers
## Hosted Integration Discovery
When remote MCP is unavailable or local-file orchestration needs a helper, the complete package is available at `GET /api/external/v1/skill.zip`; the raw entry is at `GET /api/external/v1/skill/SKILL.md`. Verify the archive SHA-256 against the integration manifest before installing. The archive includes this entry, four references, `scripts/genarrative_external_api.py`, and `agents/openai.yaml`. Discovery and documentation downloads are public; MCP and business calls require authentication.
- Manifest: `GET /api/external/v1/agent-integration.json`.
- Hosted MCP: `POST /api/external/v1/mcp`, Streamable HTTP, same Bearer API Key.
- OpenAPI: `GET /api/external/v1/openapi.json`.
- Raw Skill entry: `GET /api/external/v1/skill/SKILL.md`.
- Complete Skill archive: `GET /api/external/v1/skill.zip`.
The archive contains this main file, four one-level references, the Python helper, and `agents/openai.yaml`. Verify its SHA-256 against `agent-integration.json` before installing. Discovery, OpenAPI, and Skill downloads are public; MCP and business operations require authentication.
## Python Helper
Store the API Key outside the repository at `~/.config/genarrative/external-editor-api.json`:
```json
{
"apiKey": "tnr_sk_..."
}
```
Set restrictive permissions where possible, then smoke-test without printing the key:
The Python stdlib helper reads the private API Key file described in [authentication and safety](references/authentication-and-safety.md) and uses the production base URL. For a read-only smoke test:
```bash
chmod 600 ~/.config/genarrative/external-editor-api.json
python3 .codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py list-projects
```
For a canvas-backed generation:
Its `prepare_canvas_session` convenience method creates or reuses a project and a same-name folder. Use it only when that organization matches the task; it is not a prerequisite for MCP or REST calls. Convenience generation methods wait locally while the server uses short asynchronous submit/status requests. Use `submit_generation`, `get_generation`, and `wait_for_generation` for caller-controlled orchestration; see [requests and outputs](references/requests-and-outputs.md).
```python
from genarrative_external_api import GenarrativeExternalClient
client = GenarrativeExternalClient()
session = client.prepare_canvas_session("新画板")
client.generate_image(
"生成一张 16:9 幻想森林游戏背景",
canvasSession=session,
assetLabel="森林背景",
aspectRatio="16:9",
imageSize="1K",
artSpec={
"assetType": "background",
"subject": "幻想森林主视觉",
"style": "手绘游戏概念图",
"palette": "翡翠绿与金色光斑",
"composition": "横版,中心留出角色站位",
"format": "16:9, 1K",
"constraints": "无文字、无 UI 按钮",
"references": [],
},
)
```
For background removal, pass a stable owner-scoped object key, project resource ID, or asset ID; the helper keeps the same asynchronous submission and polling contract:
```python
session = client.prepare_canvas_session("去背景画布")
client.remove_background(
"editor-upload/object.png",
source_width=720,
source_height=1280,
canvasSession=session,
assetLabel="去背景结果",
)
```
Background removal preserves the source pixel size. For normal canvas placement with `canvasSession`, pass the real `source_width` and `source_height`, or provide both `canvasWidth` and `canvasHeight`; the helper rejects missing dimensions instead of guessing a square placeholder. `assetKind` may only describe a static image and must match the authoritative source record. Prefer a project resource ID or asset ID when the same object key has multiple semantic registrations; for a raw object key outside in-place replacement, pass `sourceResourceId` to disambiguate. Passing `targetLayerId` selects in-place replacement: the helper retains the session's project/library context but does not inject `canvasCompletion`, and it rejects an explicit `canvasCompletion` combined with `targetLayerId`. The target layer must point to the same authoritative object as the source, and the server durably binds a raw object key to that target resource for Worker revalidation.
Helper convenience methods wait locally, but the server still uses short asynchronous submit/status requests. For durable caller-controlled orchestration, call `submit_generation`, persist its `operationId` and idempotency key, then call `get_generation` or `wait_for_generation`.
For character animation, pass the canvas session and asset label to `animate_character`. The helper submits asynchronously and returns the completed compact result containing the authoritative formal `resource` and `asset`; do not synthesize a library asset from the first frame.
## Guardrails
- Do not change the fixed production base URL in generated examples.
- Do not move the API Key into environment variables, source files, generated projects, logs, docs, screenshots, or shell snippets containing literal secrets.
- Do not treat a Data URL, Blob URL, expiring signed URL, worker lease, or provider diagnostic as a durable result.
- Do not reconstruct authoritative canvas, resource, or library snapshots from a compact generation response.
- Do not replace icon-spritesheet generation with ordinary image generation when the deliverable requires a reusable transparent atlas.
Stay within `/api/external/v1` for this integration. Internal workers, queues, admin/profile APIs, and SpacetimeDB endpoints are outside this contract.
@@ -4,6 +4,44 @@ Use this reference after selecting a capability. Treat `GET /api/external/v1/ope
All paths below are relative to `https://www.genarrative.world`. Discovery and Skill download routes are public. Project, asset, upload, generation, and generation-query operations require the Bearer API Key.
## MCP Tool to API Map
The hosted MCP offers the following tools. Choose the task tool when its action matches the request; the operation tool calls the indicated REST operation directly. Task tools with actions take `{ "action": "...", "input": { ... } }`; tools without actions take the operation fields directly. `idempotencyKey` is top-level in task tools. Operation tools use `body`, `pathParameters`, and `queryParameters` wrappers from their live input schemas. Read the live tool schema and OpenAPI for exact required fields.
| REST operation | Task tool (action) | Operation tool |
| --- | --- | --- |
| `GET /api/external/v1/openapi.json` | — | `get_external_open_api_json` |
| `GET /api/external/v1/editor/projects` | `find_canvas_projects` (`list`) | `list_editor_projects` |
| `GET /api/external/v1/editor/projects/recent` | `find_canvas_projects` (`recent`) | `load_recent_editor_project` |
| `GET /api/external/v1/editor/projects/{projectId}` | `find_canvas_projects` (`get`), `find_assets` (`get_project_resources`), `edit_canvas` (`get`) | `get_editor_project` |
| `POST /api/external/v1/editor/projects` | `manage_canvas_projects` (`create`) | `create_editor_project` |
| `PATCH /api/external/v1/editor/projects/{projectId}/metadata` | `manage_canvas_projects` (`rename`) | `rename_editor_project` |
| `DELETE /api/external/v1/editor/projects/{projectId}` | `delete_resources` (`delete_project`) | `delete_editor_project` |
| `PATCH /api/external/v1/editor/projects/{projectId}/canvas` | `edit_canvas` (`save_layout`) | `save_editor_project_canvas` |
| `POST /api/external/v1/editor/projects/{projectId}/resources` | `edit_canvas` (`register_resource`) | `create_editor_project_resource` |
| `POST /api/external/v1/assets/direct-upload-tickets` | `prepare_asset_upload` (`create_upload_ticket`) | `create_external_direct_upload_ticket` |
| `POST /api/external/v1/assets/objects/confirm` | `prepare_asset_upload` (`confirm_upload`) | `confirm_external_asset_object` |
| `GET /api/external/v1/assets/read-url` | `find_assets` (`get_download_url`) | `get_external_asset_read_url` |
| `GET /api/external/v1/editor/assets/library` | `find_assets` (`list_library`) | `get_editor_asset_library` |
| `POST /api/external/v1/editor/assets/folders` | `organize_asset_library` (`create_folder`) | `create_editor_asset_folder` |
| `PATCH /api/external/v1/editor/assets/folders/{folderId}` | `organize_asset_library` (`update_folder`) | `update_editor_asset_folder` |
| `DELETE /api/external/v1/editor/assets/folders/{folderId}` | `delete_resources` (`delete_folder`) | `delete_editor_asset_folder` |
| `POST /api/external/v1/editor/assets` | `organize_asset_library` (`create_asset`) | `create_editor_asset` |
| `PATCH /api/external/v1/editor/assets/{assetId}` | `organize_asset_library` (`update_asset`) | `update_editor_asset` |
| `DELETE /api/external/v1/editor/assets/{assetId}` | `delete_resources` (`delete_asset`) | `delete_editor_asset` |
| `POST /api/external/v1/editor/images/generations` | `generate_image`, `modify_image` (`variation`, fixed `kind="quick-edit"`) | `generate_external_editor_image` |
| `POST /api/external/v1/editor/images/edits` | `modify_image` (`edit`) | `edit_external_editor_image` |
| `POST /api/external/v1/editor/images/background-removals` | `modify_image` (`remove_background`) | `remove_external_editor_image_background` |
| `POST /api/external/v1/editor/icon-spritesheets/generations` | `generate_icon_spritesheet` | `generate_external_editor_icon_spritesheet` |
| `POST /api/external/v1/editor/ui-designs/assets/extractions` | `extract_ui_assets` | `extract_external_editor_ui_design_assets` |
| `POST /api/external/v1/editor/character-animations/generations` | `generate_character_animation` | `generate_external_editor_character_animation` |
| `POST /api/external/v1/editor/videos/generations` | `generate_video` | `generate_external_editor_video` |
| `POST /api/external/v1/editor/audios/sound-effects/generations` | `generate_audio` (`sound_effect`) | `generate_external_editor_sound_effect` |
| `POST /api/external/v1/editor/audios/background-music/generations` | `generate_audio` (`background_music`) | `generate_external_editor_background_music` |
| `GET /api/external/v1/generations/{operationId}` | `check_generation` | `get_external_editor_generation_job` |
The public `agent-integration.json`, `skill/SKILL.md`, and `skill.zip` routes and the MCP transport route are HTTP entry points, not callable MCP tools. The hosted resource URIs remain `genarrative://external-editor/skill`, `genarrative://external-editor/skill/references/capability-routing.md`, `genarrative://external-editor/skill/references/api-operations.md`, `genarrative://external-editor/skill/references/authentication-and-safety.md`, `genarrative://external-editor/skill/references/requests-and-outputs.md`, and `genarrative://external-editor/openapi`.
## Project and Canvas Operations
| Operation | Method and path | Minimum input |
@@ -23,7 +61,7 @@ Project listing supports two views:
- `view=full` is the REST default and returns the complete project, canvas, layers, and resources.
- `view=summary` returns only `projectId`, `title`, `updatedAt`, and nullable `cover`, so callers can display, search, disambiguate same-name projects, and select a safe target without loading every canvas snapshot.
- Hosted MCP `list_editor_projects` always uses `summary`; call `get_editor_project` after selecting a `projectId` when complete authoritative state is required.
- Hosted MCP `list_editor_projects` and `find_canvas_projects` (`list`) use `summary`; call `get_editor_project` or `find_canvas_projects` (`get`) after selecting a `projectId` when complete authoritative state is required.
- `cover` contains only `resourceId`, stable `objectKey`, dimensions, and `updatedAt`. It never embeds image bytes, a Data URL, or a signed URL. To display it, pass `cover.objectKey` to `get_external_asset_read_url`; signed URLs are temporary and must not be persisted or reused as generation references.
## Asset and Upload Operations
@@ -42,6 +80,7 @@ Project listing supports two views:
| Delete asset record | `DELETE /api/external/v1/editor/assets/{assetId}` | `assetId` |
Upload is a three-step client flow: create a ticket, POST the file and returned fields directly to the OSS form endpoint, then confirm the returned `objectKey`. See `authentication-and-safety.md` before implementing this flow.
`prepare_asset_upload` handles the ticket and confirmation as separate calls; it does not send local bytes to OSS or automatically register a project resource, asset record, or canvas layer. `manage_canvas_projects` (`create`) likewise does not create a same-name asset folder. Register or organize records only when the task needs them.
## Generation Operations
@@ -52,7 +91,7 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
| Background removal | `/api/external/v1/editor/images/background-removals` | `sourceImageSrc` | `projectId`, `sourceResourceId`, `targetLayerId`, static-image `assetKind`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceMode`, `gridX`, `gridY`, `sliceCount`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions`, `sliceMode` | `gridX`, `gridY`, `sliceCount`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
@@ -69,10 +108,10 @@ Supply the `operationId` returned by submission. Poll no faster than `pollAfterM
## Canvas and Library Field Rules
- Pass `projectId` and `canvasCompletion` to write generated output into the canvas.
- Pass `assetFolderId` plus `assetLabel` for image, edit, icon spritesheet, video, sound effect, and BGM operations when supported.
- Pass `projectId` and `canvasCompletion` when the task calls for generated output in a canvas.
- Pass `assetFolderId` plus the relevant label field when the task calls for a library record. Neither destination requires the other, and their names need not match.
- UI extraction uses `assetFolderId` and `spritesheetLabel`.
- Character animation accepts `assetFolderId` and `assetLabel`. Its completed compact result directly returns the final `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`; never create a duplicate first-frame resource or asset.
- Character animation accepts `assetFolderId` and `assetLabel` and persists the generated sequence. Consume returned artifacts and persisted identities; never create a duplicate first-frame resource or asset.
- Background removal derives the final static-image `assetKind` from the authoritative source record. A conflicting request kind or any video, audio, animation, or image-sequence kind returns `400` before queueing. Without `canvasCompletion`, `targetLayerId` must point to the same authoritative object as `sourceImageSrc` (prefer `assetObjectId`, otherwise canonical bucket/object key).
- If a caller must manually create a `character-animation` resource or asset, put the authoritative frames and total sequence duration in `imageSequenceFrames` and `imageSequenceDurationMs`. Keep `generationInputs` replayable: it must not contain legacy runtime fields such as `characterAnimation`, `frames`, `previewVideoPath`, `frameCount`, `fps`, or `durationSeconds`.
- Reload project/library state after completion when full current state is required.
@@ -21,7 +21,7 @@ Authorization: Bearer <tnr_sk_...>
Guide a logged-in user to create a key in the product UI under `开发者 API Key`. The raw key is shown only once. Never ask the user to paste it into chat.
Store it outside repositories in the user's private JSON file:
For hosted MCP, configure the Bearer token in the client's private connection settings. For the bundled REST helper, store it outside repositories in the user's private JSON file:
```text
~/.config/genarrative/external-editor-api.json
@@ -61,7 +61,7 @@ The OpenAPI document, integration manifest, raw Skill entry, and Skill archive a
For each logical generation:
1. Create one printable ASCII `Idempotency-Key` of 1-128 bytes.
1. Create one printable ASCII key of 1-128 bytes. MCP takes `idempotencyKey` at the top level of the tool arguments (outside `input`); REST takes the `Idempotency-Key` header.
2. Persist the key with the exact request body and returned `operationId`.
3. If submission transport fails or the response is lost, resend only the exact same body with the same key.
4. Never allocate a new key merely because the outcome is unknown.
@@ -69,15 +69,21 @@ For each logical generation:
Treat a different body under the same key as invalid. Do not automatically replay a failed terminal generation unless the user intentionally requests a new logical generation.
Keep the API operation as well as the request and key unchanged across a submission retry. A different tool name does not create a separate idempotency namespace. When an operation ID is known, query `check_generation` directly. A rejected submission is not permission to switch keys and generate again.
`manage_canvas_projects/create`, `edit_canvas/register_resource`, and `organize_asset_library/create_folder` accept optional top-level `idempotencyKey`. Other non-generation actions do not accept it; in particular, `create_asset` is not an idempotent generation submission.
## Local Reference Upload
Do not ask the user to convert local files to base64. Upload from the Agent/client machine:
1. Detect the original filename, MIME type, byte length, and image dimensions when relevant.
2. Create a ticket with `POST /api/external/v1/assets/direct-upload-tickets`.
2. Call `prepare_asset_upload` with `action=create_upload_ticket` and the ticket body in `input` (REST: `POST /api/external/v1/assets/direct-upload-tickets`).
3. POST all returned non-null `formFields` and the file part named `file` directly to `upload.host`.
4. Confirm the object with `POST /api/external/v1/assets/objects/confirm`.
5. Pass the confirmed stable `objectKey` to the selected editor operation.
4. Call `prepare_asset_upload` with `action=confirm_upload` and the confirmation body in `input` (REST: `POST /api/external/v1/assets/objects/confirm`).
5. Pass the confirmed stable `objectKey` where the selected tool permits it. For operations requiring a registered source, register a project resource or asset first and use its ID.
The MCP tool does not transfer file bytes and does not accept a local path or base64. The client needs an HTTP/file-transfer capability for step 3. Object ownership comes from the API Key; do not supply `ownerUserId`. Confirmation alone creates neither a library record nor a canvas layer.
For a private reference image, use a ticket body shaped like:
@@ -107,7 +113,7 @@ Confirm with the actual file metadata:
`contentLength` is a JSON number in bytes, not a quoted string. Never invent `sourceWidth` or `sourceHeight`; read them from the local image or ask the user if they cannot be determined.
For character animation, reuse a real canvas layer ID when available. For a local-only source, derive a stable synthetic `sourceLayerId`, such as `external-reference-hero`, from the filename and keep it unchanged across retries.
For character animation, use source identity and dimensions from the actual selected resource; do not invent an existing canvas layer. The bundled local-file helper can maintain its own stable source label, which is not evidence of a persisted canvas layer.
The bundled helper implements ticket creation, a stdlib multipart upload, confirmation, dimension detection for common formats, and stable source-layer IDs:
@@ -126,7 +132,7 @@ Do not print the complete confirmation response if it may contain temporary acce
- Use `objectKey`, project resource ID, asset ID, or an allowed durable public URL for generation input.
- Use a Data URL only when the endpoint explicitly allows it and the caller has a deliberate reason; do not persist it as a durable output.
- Never use a Blob URL outside the browser process that created it.
- Use `GET /api/external/v1/assets/read-url` to obtain a short-lived `signedUrl` for display/download.
- Use `find_assets` with `action=get_download_url` (REST: `GET /api/external/v1/assets/read-url`) to obtain a short-lived `signedUrl` for display/download. This returns a URL; the client still performs any download.
- Never store or feed an expiring signed URL back into generation when a stable `objectKey` exists.
## Logging and Command Safety
@@ -139,6 +145,8 @@ Do not print the complete confirmation response if it may contain temporary acce
## Scope and Retry Guardrails
- Generation spends account credits. Respect the user's authorized task and scope; do not restart generation merely because a requested derivative is missing.
- For `delete_resources`, identify the precise IDs and obtain authorization for the actual deletion scope. Project deletion cascades to its default canvas and project-resource metadata. Folder deletion moves its assets to the default folder; the default folder cannot be deleted. Deleting a folder or asset record does not delete the underlying OSS file.
- Do not use account JWT/profile endpoints as the default external integration. Logged-in profile APIs may create/revoke developer keys, but they are outside this external editor contract.
- Do not call internal workers, queues, SpacetimeDB, or admin endpoints.
- Do not bypass upload confirmation or invent an object key.
@@ -10,21 +10,17 @@ Use this reference to translate user intent into a hosted MCP tool or its corres
- Public contract: `GET /api/external/v1/openapi.json`.
- Skill fallback: `GET /api/external/v1/skill/SKILL.md` or `GET /api/external/v1/skill.zip`.
Prefer MCP when the Agent supports a remote endpoint plus a custom Bearer token. Prefer the complete Skill and Python helper when MCP is unavailable or a client-side local-file upload must be orchestrated. The MCP tool names are derived from OpenAPI `operationId` values in snake case; select by capability instead of memorizing the name.
Prefer MCP when the Agent supports a remote endpoint plus a custom Bearer token. Prefer the complete Skill and Python helper when MCP is unavailable or a client-side local-file upload must be orchestrated. Choose a task-oriented tool below for ordinary requests, or the corresponding operation tool in [API Operations](api-operations.md) when the request needs direct control of one REST call. All 44 tools remain available. Discover the live tool schema before calling it; OpenAPI remains the field-level authority. The public discovery and Skill download routes are listed below, but are not MCP tools.
## Canvas Session
## Project and Asset Destination
Before the first generation in a new conversation, obtain a canvas name unless the user already supplied an existing `projectId` and `assetFolderId`.
Use `find_canvas_projects` (`action=list`, `recent`, or `get`) to locate an existing canvas when the request involves one. Use `manage_canvas_projects` (`create` or `rename`) only when the user needs a project created or renamed. A new project does not create an asset folder automatically. Use `find_assets` and `organize_asset_library` when the task involves library records or folders. A project and folder may have different names, and either may be unnecessary for a standalone generation.
1. List or create a project. When creating one, use the canvas name as `title`.
2. Read the asset library. Reuse a folder with the same label or create one with the canvas name.
3. Retain `canvasName`, `projectId`, `assetFolderId`, and the current art spec in conversation state.
Generated artifacts must enter both the current canvas and its same-name library folder whenever the endpoint supports that invariant. Pass `projectId`, `assetFolderId`, the endpoint's label field, and `canvasCompletion`. Character animation returns the final formal resource and asset directly; use those records and never create a duplicate from the first frame.
For generation, pass `projectId` with `canvasCompletion` when the result should enter a canvas, and `assetFolderId` with the endpoint's label field when it should enter the library. Use both only when the task requires both destinations. Character animation returns its final resource and asset directly when those destinations are requested; do not duplicate its first frame.
## Art Spec Routing
Before art generation, normalize the user's request into:
For a series of related art requests, an optional reusable spec can carry the shared requirements:
```json
{
@@ -43,48 +39,48 @@ Infer what is already clear and ask only for missing fields that block the selec
## Intent Map
| User intent | MCP/REST capability |
| ----------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Generate a background, character, spec, UI mockup, or publication image | Image generation |
| Redraw, retouch, or replace an existing image | Image edit |
| Remove the background from an existing image | Background removal |
| Generate from a local reference | Upload and confirm the local file, then image generation or edit |
| Build a reusable transparent icon/game atlas from a visual spec | Icon spritesheet generation |
| Extract marked assets from an existing UI design | UI design asset extraction |
| Animate a character into frames | Character animation generation |
| Generate video | Video generation |
| Generate a sound effect | Sound-effect generation |
| Generate background music/BGM | Background-music generation |
| Upload a local image/audio/video asset | Upload ticket -> OSS form upload -> object confirm |
| Save viewport/layers | Canvas save |
| Create, load, rename, or delete a canvas | Project operations |
| Organize folders and asset records | Asset-library operations |
| Obtain temporary access to private media | Signed read URL |
| Check generation progress or retrieve its result | Generation query |
| User intent | MCP tool and action |
| --- | --- |
| Find, open, create, or rename a canvas project | `find_canvas_projects` (`list`, `recent`, `get`); `manage_canvas_projects` (`create`, `rename`) |
| Read project resources or library records | `find_assets` (`get_project_resources`, `list_library`) |
| Create or change folders and asset records | `organize_asset_library` (`create_folder`, `update_folder`, `create_asset`, `update_asset`) |
| Upload a local image/audio/video asset | `prepare_asset_upload` (`create_upload_ticket`), client-side OSS form upload, then `prepare_asset_upload` (`confirm_upload`) |
| Register existing media in a project, read a canvas, or save its full layout | `edit_canvas` (`register_resource`, `get`, `save_layout`) |
| Generate a background, character, spec, UI mockup, or publication image | `generate_image` |
| Retouch an existing image, make a reference variation, or remove its background | `modify_image` (`edit`, `variation`, `remove_background`) |
| Build a transparent icon/game atlas from a registered visual spec | `generate_icon_spritesheet` |
| Generate marked assets from an existing UI design | `extract_ui_assets` |
| Animate a character into frames | `generate_character_animation` |
| Generate video | `generate_video` |
| Generate a sound effect or background music | `generate_audio` (`sound_effect`, `background_music`) |
| Check generation progress or retrieve its result | `check_generation` |
| Obtain temporary access to private media | `find_assets` (`get_download_url`) |
| Delete an exact project, folder, or asset record | `delete_resources` (`delete_project`, `delete_folder`, `delete_asset`) |
For tools with actions, send `{ "action": "...", "input": { ... } }`; place `idempotencyKey` at the top level when supported or required. Tools without actions accept operation fields directly, with `idempotencyKey` at the top level for generation. The direct operation tools use `body`, `pathParameters`, and `queryParameters` wrappers as shown by their live schemas.
Do not present an API menu unless the request is genuinely ambiguous. Ask a follow-up when two routes create different artifacts, for example “处理这张图” could mean edit, extract marked UI assets, or use it as a reference for a new generation.
## Route-Specific Decisions
- Use image edit when the requested output replaces or modifies a source image. With `projectId`, pass `targetLayerId` to replace an existing layer when no explicit `canvasCompletion` is supplied.
- Use `modify_image` `edit` when the requested output modifies a registered source image. Use `variation` when reference images should guide a new `quick-edit` image; it is image generation with fixed `kind="quick-edit"`. Use `remove_background` for a static source image. With `projectId`, `targetLayerId` may replace a matching existing layer when no explicit `canvasCompletion` is supplied.
- Use icon spritesheet generation for a transparent reusable atlas when a stable visual-spec reference and concrete `iconDescriptions` exist. Do not use ordinary image generation just because it can draw several objects.
- Use UI extraction only for an existing UI design image with red-box annotations. It is not UI generation.
- Use a project layer ID as character animation `sourceLayerId` when one exists. For a local-only source, derive a stable synthetic ID from the filename.
- Character animation requires a real `sourceLayerId`, source image, and dimensions from an existing resource. A local-only file must first be uploaded and registered where needed; do not invent a layer ID.
- For video with image/video/audio references, use a Seedance 2.0-family model; default to `seedance2.0-fast`, `mode: "std"`, and explicit `sound`.
- Use `signedUrl` only for preview/download. Feed stable `objectKey` or registered resource/asset identifiers into generation.
- `prepare_asset_upload` obtains a ticket and confirms an uploaded object; it does not transfer the file or register a project resource, asset record, or canvas layer. Use `edit_canvas` `register_resource` or `organize_asset_library` `create_asset` only when the task needs those records.
- Use temporary signed URLs only for preview/download. Feed stable `objectKey` or registered resource/asset identifiers into generation as each operation permits.
## AI Game Creator Canonical Visual DAG
## Example: Reusable Icon Assets
Keep the existing autonomous-build task graph. Do not add a parallel task system or collapse these artifacts into one ordinary generation request:
When the task needs a visual spec and a reusable icon atlas:
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. `sliceMode` is required and has no default: send `sliceMode: "grid"` with `gridX`/`gridY` only when the requirement itself fixes the slots or names the column/row count, and otherwise send `sliceMode: "connected-components"` (with `sliceCount` when a subject count must be constrained); never invent a grid to express "kinds of assets", and never send `gridX`/`gridY` with `connected-components`.
1. Reuse an existing registered `icon-spec`, or generate the requested spec using `generate_image` with `kind=spec` and register it as `assetKind=icon-spec` if necessary.
2. Call `generate_icon_spritesheet` with that registered ID as `referenceId`, concrete `iconDescriptions`, and explicit `sliceMode`. Choose `grid` only for requested equal cells or fixed slots and provide those `gridX`/`gridY` values; otherwise use `connected-components`, optionally with `sliceCount`.
3. Query `check_generation` and inspect the full sheet and actual slices. Preserve warnings; a usable full sheet does not imply that individual slices exist. Use returned slice identities and dimensions rather than guessing crop coordinates.
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. When the requirement fixes grid slots, require the response `sliceMode` to match the declared `grid` request and exactly `gridX × gridY` slices before registering the local runtime sheet; a connected-components request is instead judged by its own `sliceCount` or by the requirement, and both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
An ordinary UI mockup or uploaded image is not automatically an `icon-spec`. For extracting marked components from a UI design, use `extract_ui_assets`, which includes generation and does not promise pixel-exact cropping.
## Scope Boundary
Stay within `/api/external/v1`. Do not invent worker, queue, runtime task-list, admin, profile, or SpacetimeDB calls. The only external generation query is `GET /api/external/v1/generations/{operationId}`.
Stay within `/api/external/v1`. Do not invent worker, queue, runtime task-list, admin, profile, or SpacetimeDB calls. The only external generation query is `GET /api/external/v1/generations/{operationId}`. The hosted MCP also exposes the Skill and OpenAPI resources at `genarrative://external-editor/skill`, its four `skill/references/*.md` URIs, and `genarrative://external-editor/openapi`; keep those URI names unchanged.
@@ -4,15 +4,46 @@ Use this reference to build generation payloads, carry canvas/library context, p
## Contents
- [MCP Argument Shapes](#mcp-argument-shapes)
- [Asynchronous Submission](#asynchronous-submission)
- [Polling State Machine](#polling-state-machine)
- [Canvas and Asset-Library Completion](#canvas-and-asset-library-completion)
- [Saving Existing Canvas Layout](#saving-existing-canvas-layout)
- [Art Spec and Image Request](#art-spec-and-image-request)
- [Local Reference Requests](#local-reference-requests)
- [Compact Completed Result](#compact-completed-result)
- [Warning Semantics](#warning-semantics)
- [Output Handling Checklist](#output-handling-checklist)
## MCP Argument Shapes
Pass these objects as the `arguments` of the named tool in `tools/call`. They are not REST request envelopes.
Single-function tools take business fields directly. For example, `generate_image`:
```json
{
"prompt": "一张横版幻想森林背景,无文字",
"aspectRatio": "16:9",
"imageSize": "1K",
"idempotencyKey": "forest-image-001"
}
```
Multi-function tools take `action` and `input`. A generation key stays outside `input`. For example, `generate_audio`:
```json
{
"action": "sound_effect",
"input": {"prompt": "轻柔的游戏菜单确认音", "duration": 1},
"idempotencyKey": "menu-sound-001"
}
```
Keys above identify distinct example requests; create and persist your own key for each new logical generation. An action with no business fields still requires `input: {}`, such as `find_canvas_projects` with `action=list`. Use only fields belonging to the selected action; do not combine branches. REST examples below use the business body directly and put the key in the HTTP header instead.
MCP returns business data in `structuredContent`, without the REST `data` envelope. Check `isError` before using it; an HTTP-successful MCP exchange can still carry a tool error. Generation acceptance contains an `operationId`, not the final media.
## Asynchronous Submission
All nine generation POST routes require `Idempotency-Key` and return HTTP `202` with an `ExternalEditorGenerationSubmissionResponse` shaped like:
@@ -50,7 +81,7 @@ Persist the key, exact request body, and `operationId`. If submission outcome is
## Polling State Machine
Poll `statusUrl`, or `GET /api/external/v1/generations/{operationId}`, no faster than `pollAfterMs`:
With MCP, call `check_generation` with `{"operationId":"<returned operationId>"}`. Each call queries once and does not wait for completion. With REST, poll `statusUrl` or `GET /api/external/v1/generations/{operationId}`. Query no faster than `pollAfterMs`:
- `queued` / `running`: retain `operationId`; show `phaseLabel`, `phaseDetail`, and `progress` when present; wait before querying again.
- `completed`: consume the compact `result` and all warning fields, then stop polling.
@@ -75,10 +106,10 @@ Background removal uses the same submission and polling state machine. `sourceIm
## Canvas and Asset-Library Completion
For endpoints that support these fields, include:
Choose destinations according to the task. Locate an existing project with `find_canvas_projects` and inspect folders with `find_assets/list_library`; create missing destinations with `manage_canvas_projects/create` and `organize_asset_library/create_folder` only when needed. Project creation does not create a folder. For endpoints that support the requested destinations, include:
- `projectId`: target canvas project.
- `assetFolderId`: folder whose label matches the canvas name.
- `assetFolderId`: target asset-library folder; its name need not match the project.
- `assetLabel` or UI extraction's `spritesheetLabel`: user-visible library label.
- `canvasCompletion`: backend canvas placement instructions.
@@ -102,15 +133,24 @@ A minimal `canvasCompletion` is:
Background removal preserves the source image dimensions. For normal canvas placement, the Python helper therefore requires the real `source_width` and `source_height` whenever `canvasSession` is used without an explicit `canvasWidth` plus `canvasHeight`; it never substitutes a square default. Passing `targetLayerId` instead selects in-place replacement, so the helper keeps the session's project/library fields without injecting `canvasCompletion` and rejects callers that explicitly combine both placement modes. The request `assetKind` is optional, static-image only, and must equal the authoritative source type when one exists. An in-place target must resolve to the same authoritative source object; a raw object key is bound to that target resource instead of relying on project-list order.
Character animation accepts `assetFolderId` and `assetLabel` and persists the final transparent sequence directly. Its completed compact result includes the authoritative `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`. Use those records directly and never synthesize a duplicate asset from the first frame.
Character animation accepts `assetFolderId` and `assetLabel` and persists the generated sequence. Consume the returned animation artifacts and persisted identities; do not synthesize a duplicate animation asset from the first frame. Use complete project/library records when complete persisted state is needed.
For the lower-level asset/resource creation endpoints, `generationInputs` is replayable request context rather than a media-runtime container. When `assetKind` is `character-animation`, the server rejects legacy runtime keys including `characterAnimation`, `frames`, `previewVideoPath`, `frameCount`, `fps`, and `durationSeconds`; send the formal sequence through `imageSequenceFrames` and `imageSequenceDurationMs`. Internal processing audit keys such as `screenColorHex`, `mattingProvider`, and `mattingModel` are removed before persistence.
## Saving Existing Canvas Layout
1. Call `edit_canvas` with `action=get` and `input.projectId` to read the latest project and canvas revision.
2. Build the intended complete `viewport` and `layers`, preserving unrelated layers. `save_layout` replaces the layout; it is not a one-layer patch.
3. Call `edit_canvas` with `action=save_layout` and `input` containing `projectId`, the read `expectedRevision`, and the complete `viewport` and `layers`.
4. On a revision conflict, reread and reconcile with the current layout before retrying. Do not blindly resend stale layers with a refreshed revision.
`edit_canvas/register_resource` registers existing media but does not create a canvas layer. `organize_asset_library/create_asset` creates metadata but does not upload or generate media. For generated media placement, prefer the generation tool's supported `canvasCompletion`; inspect returned identities before registering anything again.
## Art Spec and Image Request
Generic External v1 image generation does not expose the main-site structured game-scene contract. `kind: "scene"` and `assetKind: "scene"` are both invalid and return HTTP `400` before any generation job is queued. Do not replace the structured scene fields and server-owned prompt assembly with a generic image prompt.
Carry the current art spec in `generationInputs.artSpec` and reflect important constraints in the prompt:
When maintaining a reusable art spec, carry it in `generationInputs.artSpec` and reflect important constraints in the prompt. This is an example with both canvas and library destinations, not a requirement for every generation:
```json
{
@@ -177,11 +217,11 @@ Image edit/redraw has a stricter main-source identity rule. After upload confirm
Icon spritesheet generation has a stricter primary-spec contract. After upload confirmation, create a project resource or asset record with `assetKind: "icon-spec"`, retain its returned `resourceId` or `assetId`, and pass that ID as `referenceId`. The primary spec does not accept the uploaded `objectKey` directly; only additional style references may continue to use stable object keys in `referenceImageSrcs`.
For character animation from a local-only source, use actual dimensions and a stable synthetic layer ID:
For character animation, use the selected source identity and actual dimensions. The following is a business-body example; `generate_character_animation` also requires a top-level `idempotencyKey`:
```json
{
"sourceLayerId": "external-reference-hero",
"sourceLayerId": "<selected source layer ID>",
"sourceImageSrc": "<confirmed objectKey>",
"sourceWidth": 720,
"sourceHeight": 1280,
@@ -209,7 +249,7 @@ The completed `result` may contain stable artifact fields such as:
- `spritesheetResource`, `spritesheetAsset`, and stable spritesheet metadata.
- `warning` and `sliceWarning` structures.
It deliberately excludes a complete project/canvas/library snapshot, Data URL, Blob URL, expiring signed URL, worker lease, queue state, and internal provider diagnostics. Use `/assets/read-url` for temporary access to a stable `objectKey`.
It deliberately excludes a complete project/canvas/library snapshot, Data URL, Blob URL, expiring signed URL, worker lease, queue state, and internal provider diagnostics. Use `find_assets/get_download_url` for temporary access to a stable `objectKey` (REST: `/assets/read-url`). Reading a record or obtaining a URL does not itself inspect or download the media.
## Warning Semantics
+1
View File
@@ -65,6 +65,7 @@ temp*build*/
/apps/preview-deployer-web/node_modules/
/server-rs/.spacetimedb/
/server-rs/.data/
**/server-rs/.data/
/public/generated-animations
/public/generated-character-drafts
/public/generated-characters
+33
View File
@@ -48,6 +48,23 @@ _Avoid_: 无来源的静态素材、只显示在 UI 但不落工程资源记录
一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。
_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO
**UI 设计文档**:
AGC 本地项目里 `kind=ui-design-doc`、`mediaType=application/json` 的界面编辑载体,保存设计图集合、UI 树、组件绑定和 State revision;一份文档可同时承载多张设计图与各自的结构树,不强制收敛成一棵树。
_Avoid_: 把 UI 设计图当成设计文档、给设计文档再套一层「页面」概念
**设计图**:
UI 设计文档里的一张参考界面图,以它在 manifest 中的资产 ID 作为文档内身份,附带像素尺寸与像素比;文档内的每棵树都必须引用文档里已存在的设计图。
_Avoid_: 用文件名当设计图身份、设计图与设计文档同一概念
**UI 工作流步骤**:
对一份 UI 设计文档执行的一次受控处理阶段,当前只有「结构识别」与「素材切分」两步;步骤产物只有在写入文档并保存后才算完成。
_Avoid_: 把单次工具调用当成步骤、把中间产物当成步骤完成
**工作流检查点日志**:
一份 UI 设计文档旁按行追加的恢复用日志,每完成一个工作流步骤追加一行;某步是否有对应行即代表该步是否完成,恢复时从第一个缺失的行继续。
_Avoid_: 每步一个 sidecar 状态机、把切分专用的 SeparationState 泛化成通用检查点
## Language
### Puzzle Clear
@@ -190,6 +207,22 @@ _Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
Thread Manager 向订阅者推送的当前回合原始事件流,只服务运行期间与短期断线恢复,不替代项目对话历史。
_Avoid_: 进度通知、快照轮询、第二套历史
**逻辑回合**:
Thread Manager 拥有的一对回合边界(开始与结束),由接单动作开启、由这一轮的占用对象写出,不镜像 Codex 原生回合;界面忙碌态与回合结果只认它。
_Avoid_: Codex 原生回合、原生日志、进程生命周期
**接单**:
把一条用户消息交给宿主开始执行的动作,成立即表示这一轮已经存在;此后结果只由运行态事件回答。
_Avoid_: 发送成功、命令调用、接口返回
**拒单**:
接单成立之前拒绝这次请求(并发、权限、目录、参数、工程准备未就绪),只回一条可展示原因,不产生回合事件,也不写用户条目。
_Avoid_: 回合失败、执行失败、失败事件
**在途回合**:
界面本地已经把这条用户消息发出去、宿主还没有对应回合开始事件的那一小段状态。
_Avoid_: 运行中回合、乐观锁、发送队列
**聊天投影**:
把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。
_Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer
@@ -6,7 +6,7 @@
"visibleModels": [],
"apiKey": "",
"baseUrl": "https://dev.genarrative.world/gpt/v1",
"model": "gpt-6-astra",
"model": "platform-default",
"apiKind": "openai_responses",
"reasoningEffort": "max",
"stream": true,
@@ -157,11 +157,8 @@ const allowedUncalledTauriCommands = [
'set_active_game_creator_agent_session',
'start_game_creator_agent_goal',
'start_game_creator_supervisor_runtime_task',
// TODO: Remove the retired binding command after the legacy runtime path is removed.
'bind_components',
'chat_with_game_creator_agent',
'check_ui_editor_font_glyph_coverage',
'create_ui_design_resource',
// 图片类生成的同步变体:GUI 已改为 `start_local_project_asset_generation` + 项目内任务账本
// (提交即返回、后台生成)。这条命令**没有生产调用方**,只有 Rust 集成测试
// (`src/tests/project.rs`)与 `commands.rs` 单测在调;待后续批次删除,或改为转调
@@ -1497,6 +1494,15 @@ if (defaultAppConfig.llm?.apiKey !== '') {
throw new Error('AI game creator shell default llm.apiKey must stay empty');
}
// 首次启动模板必须写入官方路由占位模型(与 config.rs 的
// OFFICIAL_LLM_ROUTER_DEFAULT_MODEL 同源):钉死具体上游模型名会随上游目录
// 变动失效,留空则首启配置不合法。
if (defaultAppConfig.llm?.model !== 'platform-default') {
throw new Error(
'AI game creator shell default llm.model must stay the official route placeholder',
);
}
if (defaultAppConfig.agentMode !== 'codex_app_server') {
throw new Error(
'AI game creator shell default agentMode must be codex_app_server',
@@ -1,11 +1,12 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main",
"description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件;允许弹出原生打开/保存对话框用于素材上传与导出。",
"description": "AI 游戏创作主窗口允许读写系统剪贴板,用于粘贴素材附件和复制生成文件路径;允许弹出原生打开/保存对话框用于素材上传与导出。",
"windows": ["client"],
"permissions": [
"clipboard-manager:allow-read-image",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"core:image:allow-rgba",
"core:image:allow-size",
"core:resources:allow-close",
@@ -7,6 +7,7 @@
"execution": "texts/execution.json",
"media": "texts/media.json",
"nativeTools": "texts/native-tools.json",
"uiDesignDoc": "texts/ui-design-doc.json",
"recovery": "texts/recovery.json",
"interaction": "texts/interaction.json",
"goalContext": "texts/goal-context.json",
@@ -37,7 +37,6 @@
"preview.validate.description": "用真实浏览器验证桌面和移动预览并保存证据。",
"image.inspect.description": "让视觉模型检查一至两张项目内图片。",
"canvas.asset_generate.description": "通过已配置的 External Editor API 按项目需求生成图片或图集并登记到画布、素材库和项目 assets;可使用已登记资源作为参考。assetKind=icon-spritesheet 的 prompt 去除首尾空白后须为 1 到 200 个 Unicode 字符,保留内部换行并作为单条 iconDescriptions 原样提交,超限拒绝,不截断、不拆条,客户端不追加生图指令。assetKind=icon-spritesheet 时 sliceMode 必填且没有默认值:需求要求等分网格、固定槽位或指定行列数时用 grid 并提供来自需求本身的 gridX/gridY;自由排布、数量不定或只要求一张图集时用 connected-components,可用 sliceCount 约束素材张数;其它 assetKind 不得携带 sliceMode/gridX/gridY。",
"ui.workflow.run.description": "先用 discover 从受控 game/ui-pages.json 或页面声明标记自动发现全部功能页面,再把已登记 ui-design 与每个页面的设计图桥接成独立 UI JSON State;可同时载入已登记图片、图标和项目字体,执行 Provider 结构识别、多树合并与分批组件绑定、回读阶段,并且只有所有页面已绑定且已应用到 game/ 后才允许 finalize。项目根目录由 Runtime 注入,模型不得传入宿主路径。",
"cocos.editor.execute.description": "在当前项目对应的已打开 Cocos Creator 编辑器中执行一段有界代码;仅提交 code,客户端负责绑定项目与编辑器进程。",
"unity.editor.execute.description": "在当前 Unity 项目已打开的编辑器中执行 C#。仅提交 code;结果待核对时禁止自动重发。",
"godot.editor.execute.description": "在当前 Godot 项目已打开的编辑器中执行支持 return/await 的 GDScript 函数体。仅提交 code;结果待核对时禁止自动重发。",
@@ -0,0 +1,8 @@
{
"from_images.description": "用一至四张设计图新建一份 UI 设计文档:输入给已登记资源的 assetId,或给项目内相对路径由本工具顺带登记;随后按 ui/UI 设计 N.json 取号创建文档、把设计图写进文档并登记为 ui-design-doc 资源,返回新文档的 assetId 与 relativePath。每次调用都新建文档,不复用既有文档。",
"from_images.parameters.images": "一至四张设计图。每张给 assets 里已登记图片资源的 assetId,或给项目内相对路径(相对路径会先登记成图片资源);文档内的设计图身份就是该图片资源的 assetId。",
"run_workflow.description": "对一份 UI 设计文档在 Rust 内依次执行结构识别与自动切分素材:把切分素材回填到 Image 组件的 target_graphic、清掉已处理节点的组件状态、给达到返工上限的问题节点写 NeedReview,最后写回文档并推进 revision。每完成一步追加一行检查点,崩溃后从第一个缺失的步骤继续;不做页面发现,也不做多树合并与独立组件绑定。文档在轮次中途被改动时本轮作废并报错,必须重新调用本工具开新一轮。",
"run_workflow.parameters.designDocAssetId": "目标 UI 设计文档在 assets 里的 assetId;文档内必须已有设计图。",
"into_js.description": "把一份 UI 设计文档的当前 revision 渲染成 ui/generated-<stem>-<digest>.js,返回相对路径与导出树。只读文档内容,不修改文档、不推进 revision。",
"into_js.parameters.designDocAssetId": "目标 UI 设计文档在 assets 里的 assetId。"
}
@@ -0,0 +1,269 @@
//! ACL 提权修复目标的并发去重与结果记忆。
//!
//! 同一目标被并发请求时只允许一次真实提权,其余调用等待并复用同一结果;
//! 结果在冷却窗口内直接复用,其中用户拒绝(UAC 取消)的窗口最长,
//! 避免自动重试把用户反复拽回安全桌面。
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Condvar, LazyLock, Mutex};
use std::time::{Duration, Instant};
/// 一次提权修复的结果。用户拒绝与修复失败必须可区分:前者不该被重试。
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum AclRepairOutcome {
Repaired,
Denied(String),
Failed(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum AclRepairGateResult {
Executed(AclRepairOutcome),
Reused(AclRepairOutcome),
/// leader 在等待窗口内仍未结束(例如 UAC 无人应答);调用方按失败关闭处理。
WaitTimedOut,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct AclRepairPolicy {
pub(crate) success_cooldown: Duration,
pub(crate) denial_cooldown: Duration,
pub(crate) failure_cooldown: Duration,
pub(crate) wait_timeout: Duration,
/// leader 超过这个时长仍未落库即视为卡死,允许新调用接管该 key。
/// UAC 弹窗最多被系统挂约两分钟,所以这个上限取得比它宽得多;没有它,
/// 一次挂死的 `Start-Process -Wait` 会让这个目标在进程重启前一直失败关闭。
pub(crate) leader_deadline: Duration,
}
impl AclRepairPolicy {
fn cooldown_for(&self, outcome: &AclRepairOutcome) -> Duration {
match outcome {
AclRepairOutcome::Repaired => self.success_cooldown,
AclRepairOutcome::Denied(_) => self.denial_cooldown,
AclRepairOutcome::Failed(_) => self.failure_cooldown,
}
}
fn retention(&self) -> Duration {
self.success_cooldown
.max(self.denial_cooldown)
.max(self.failure_cooldown)
}
}
struct Entry {
running: bool,
outcome: Option<AclRepairOutcome>,
recorded_at: Option<Instant>,
/// leader 起跑时刻,用于判定该 leader 是否已经卡死。
started_at: Instant,
/// 当前 leader 的令牌:被接管后旧 leader 迟到的结果不得覆盖新 leader 的结果。
leader_id: u64,
}
pub(crate) struct AclRepairGate<K> {
entries: Mutex<HashMap<K, Entry>>,
settled: Condvar,
next_leader_id: AtomicU64,
}
impl<K: Clone + Eq + Hash> AclRepairGate<K> {
pub(crate) fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
settled: Condvar::new(),
next_leader_id: AtomicU64::new(1),
}
}
/// 以 `key` 为粒度执行一次提权修复:并发调用只会有一次真正执行,
/// 其余调用等待并复用结果;冷却窗口内直接复用上一次结果。
pub(crate) fn run<F>(
&self,
key: K,
now: Instant,
policy: &AclRepairPolicy,
execute: F,
) -> AclRepairGateResult
where
F: FnOnce() -> AclRepairOutcome,
{
let wait_deadline = Instant::now() + policy.wait_timeout;
let mut entries = lock(&self.entries);
loop {
match entries.get(&key) {
Some(entry) if entry.running => {
// 卡死的 leader(例如 `Start-Process -Wait` 真挂住)不能永久占住这个 key:
// 超过 leader_deadline 就由新调用接管,否则该目标在进程重启前只会一直失败关闭。
if now.saturating_duration_since(entry.started_at) >= policy.leader_deadline {
break;
}
let remaining = wait_deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return AclRepairGateResult::WaitTimedOut;
}
let (guard, _) = self
.settled
.wait_timeout(entries, remaining)
.unwrap_or_else(|poisoned| poisoned.into_inner());
entries = guard;
}
Some(entry) => {
let reusable = entry.outcome.clone().zip(entry.recorded_at).filter(
|(outcome, recorded_at)| {
now.saturating_duration_since(*recorded_at)
< policy.cooldown_for(outcome)
},
);
match reusable {
Some((outcome, _)) => return AclRepairGateResult::Reused(outcome),
None => break,
}
}
None => break,
}
}
prune(&mut entries, now, policy);
let leader_id = self.next_leader_id.fetch_add(1, Ordering::Relaxed);
entries.insert(
key.clone(),
Entry {
running: true,
outcome: None,
// 结果尚未落库:冷却基准只在真正记录结果时才写。
recorded_at: None,
started_at: now,
leader_id,
},
);
drop(entries);
let guard = LeaderGuard {
gate: self,
key: key.clone(),
leader_id,
armed: true,
};
let outcome = execute();
guard.complete(outcome)
}
/// 用户主动操作后允许重新尝试提权:清掉「被拒绝」的记忆。
pub(crate) fn clear_denials(&self) {
let mut entries = lock(&self.entries);
entries.retain(|_, entry| {
entry.running || !matches!(entry.outcome, Some(AclRepairOutcome::Denied(_)))
});
drop(entries);
self.settled.notify_all();
}
#[cfg(test)]
pub(crate) fn is_running(&self, key: &K) -> bool {
lock(&self.entries)
.get(key)
.is_some_and(|entry| entry.running)
}
}
impl<K> Default for AclRepairGate<K>
where
K: Clone + Eq + Hash,
{
fn default() -> Self {
Self::new()
}
}
struct LeaderGuard<'a, K: Clone + Eq + Hash> {
gate: &'a AclRepairGate<K>,
key: K,
leader_id: u64,
armed: bool,
}
impl<K: Clone + Eq + Hash> LeaderGuard<'_, K> {
fn complete(mut self, outcome: AclRepairOutcome) -> AclRepairGateResult {
self.armed = false;
let mut entries = lock(&self.gate.entries);
// 只在仍是当前 leader 时落库:leader 卡死被接管后,迟到的结果必须丢弃,
// 否则会把接管者已经写下的结果覆盖回去。
if let Some(entry) = entries.get_mut(&self.key) {
if entry.leader_id == self.leader_id {
entry.running = false;
entry.outcome = Some(outcome.clone());
// 冷却从「结果落库」时刻算起,而不是 leader 起跑时刻:UAC 弹窗可能被挂着
// 几十秒到两分钟,用起跑时刻会让 120s 拒绝冷却在用户应答前就过期,
// 紧接着的自动重查会立刻再弹一次。
entry.recorded_at = Some(Instant::now());
}
}
drop(entries);
self.gate.settled.notify_all();
AclRepairGateResult::Executed(outcome)
}
}
impl<K: Clone + Eq + Hash> Drop for LeaderGuard<'_, K> {
/// leader 异常退出时不能让等待者永久挂住:记成失败并唤醒全部等待者。
fn drop(&mut self) {
if !self.armed {
return;
}
let mut entries = lock(&self.gate.entries);
if let Some(entry) = entries.get_mut(&self.key) {
if entry.leader_id == self.leader_id {
entry.running = false;
entry.outcome = Some(AclRepairOutcome::Failed(
"AGC ACL 提权修复执行线程异常退出".to_string(),
));
entry.recorded_at = Some(Instant::now());
}
}
drop(entries);
self.gate.settled.notify_all();
}
}
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn prune<K: Eq + Hash>(entries: &mut HashMap<K, Entry>, now: Instant, policy: &AclRepairPolicy) {
// 只是防止 map 随进程生命周期无限增长;窗口远大于冷却期即可。
let retention = policy.retention().saturating_mul(4);
entries.retain(|_, entry| {
if entry.running {
return true;
}
entry
.recorded_at
.is_none_or(|recorded_at| now.saturating_duration_since(recorded_at) < retention)
});
}
/// 提权修复的进程级闸门;key = (规范化目标路径, scope 名)。
pub(crate) type AclRepairKey = (String, &'static str);
pub(crate) static ACL_REPAIR_GATE: LazyLock<AclRepairGate<AclRepairKey>> =
LazyLock::new(AclRepairGate::new);
pub(crate) const ACL_REPAIR_POLICY: AclRepairPolicy = AclRepairPolicy {
success_cooldown: Duration::from_secs(30),
denial_cooldown: Duration::from_secs(120),
failure_cooldown: Duration::from_secs(15),
wait_timeout: Duration::from_secs(60),
// 系统对无人应答的 UAC 弹窗约 2 分钟超时,取 5 分钟只兜「真挂死」这一种情况。
leader_deadline: Duration::from_secs(300),
};
/// 用户主动操作(打开/新建项目、重命名刷新)后调用:解除「被拒绝」记忆。
pub(crate) fn clear_acl_repair_denials() {
ACL_REPAIR_GATE.clear_denials();
}
@@ -33,6 +33,9 @@ mod direct_thread_wire;
mod direct_tool_bridge;
mod direct_tool_calls;
mod direct_tools_mcp;
mod direct_turn_accept;
mod direct_turn_error;
mod direct_turn_failure;
mod direct_turn_stream;
mod direct_validation;
mod generation;
@@ -66,6 +69,9 @@ pub(crate) use direct_thread_wire::*;
pub(crate) use direct_tool_bridge::*;
pub(crate) use direct_tool_calls::*;
pub(crate) use direct_tools_mcp::*;
pub(crate) use direct_turn_accept::*;
pub(crate) use direct_turn_error::*;
pub(crate) use direct_turn_failure::*;
pub(crate) use direct_turn_stream::*;
pub(crate) use direct_validation::DirectValidationConfig;
pub(crate) use generation::*;
@@ -1,7 +1,7 @@
//! Native / third-party approval adapter. The host execution session owns policy
//! and persistence; this module only binds the app-server protocol to its leases.
use super::super::{direct_delivery, direct_execution, direct_validation};
use super::super::{direct_delivery, direct_execution, direct_validation, DirectTurnError};
use super::{shutdown_game_creator_codex_app_server_inner, CodexAppServerInner};
use direct_execution::{EffectKind, ExecutionLease, ExecutionPhase, ExecutionSession};
use serde_json::{json, Value};
@@ -16,11 +16,25 @@ use tokio::sync::{watch, Notify};
const MAX_PROTOCOL_ITEMS: usize = 2048;
const MAX_REQUEST_CACHE: usize = 512;
/// 逐次审批协议的版本门禁:发行构建只接受捆绑侧车的固定版本;开发构建用宿主自带的 Codex
/// (Linux 与未 stage 侧车时没有固定版本可用),按 profile 直接跳过该门禁。
pub(super) fn validate_approval_version(version: &str) -> Result<(), String> {
if version.trim() == super::super::codex_cli::codex_bundle::CLI_VERSION {
return Ok(());
#[cfg(not(debug_assertions))]
{
if version.trim() == super::super::codex_cli::codex_bundle::CLI_VERSION {
return Ok(());
}
return Err(format!(
"direct-execution-protocol: 当前 Codex 版本未通过逐次审批协议验收,请使用客户端配套版本(期望 {},实际 {});禁止降级为无控制执行",
super::super::codex_cli::codex_bundle::CLI_VERSION,
version.trim()
));
}
#[cfg(debug_assertions)]
{
let _ = version;
Ok(())
}
Err("direct-execution-protocol: 当前 Codex 版本未通过逐次审批协议验收,请使用客户端配套版本;禁止降级为无控制执行".into())
}
pub(super) fn denied_response(id: u64, method: &str) -> Value {
@@ -78,12 +92,41 @@ pub(super) enum HostOutcome {
RepairRequired,
}
pub(super) fn outcome_text(outcome: HostOutcome) -> Result<String, platform_llm::LlmError> {
/// [`HostOutcome`] 的文本投影。
///
/// 返修要求([`HostOutcome::RepairRequired`])用**自己的变体**表达:它是控制流("继续当前返修
/// 批次"),不是失败。以前它伪装成 `LlmError::InvalidRequest("validation-source-changed: …")`,
/// 于是和真失败走同一条投影——终态被判成 `failed`、界面收到一条用户可见的失败说明。
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum HostOutcomeText {
/// 正常收尾:可展示的回复 / 交付报告文本。
Report(String),
/// 封口复核要求继续当前返修批次(控制流,不是失败)。
RepairRequired { detail: String },
}
/// 返修要求写回提示词时用的说明。
pub(super) const HOST_OUTCOME_REPAIR_REQUIRED_DETAIL: &str =
"宿主收尾复核发现输入或证据变化,请读取交付状态后继续当前返修批次";
impl HostOutcomeText {
/// 投影成这一轮的收尾结果:正常报告是文本,返修要求是控制流(走 `Err` 侧自己的变体)。
pub(super) fn into_run_result(self) -> Result<String, super::DirectTurnRunFailure> {
match self {
Self::Report(text) => Ok(text),
Self::RepairRequired { detail } => {
Err(super::DirectTurnRunFailure::RepairRequired { detail })
}
}
}
}
pub(super) fn outcome_text(outcome: HostOutcome) -> HostOutcomeText {
match outcome {
HostOutcome::Report(report) => Ok(report),
HostOutcome::RepairRequired => Err(platform_llm::LlmError::InvalidRequest(
"validation-source-changed: 宿主收尾复核发现输入或证据变化,请读取交付状态后继续当前返修批次".into(),
)),
HostOutcome::Report(report) => HostOutcomeText::Report(report),
HostOutcome::RepairRequired => HostOutcomeText::RepairRequired {
detail: HOST_OUTCOME_REPAIR_REQUIRED_DETAIL.to_string(),
},
}
}
@@ -147,6 +190,11 @@ pub(super) struct ExecutionAdapter {
changed: Notify,
shutdown_gate: tokio::sync::Mutex<()>,
outcome: watch::Sender<Option<HostOutcome>>,
/// 宿主自己判定的"本轮以失败收口":`(分类, 原因)`。有值就代表本轮终态必须是失败,
/// 原因与交付报告同一份文本。
turn_failure: Mutex<Option<DirectTurnError>>,
/// 用户/宿主是否主动要求终止这一轮(界面的「终止」按钮)。用户主动终止不是失败。
host_stop_requested: AtomicBool,
}
fn identity(value: Option<&Value>) -> Option<&str> {
@@ -263,6 +311,8 @@ impl ExecutionAdapter {
changed: Notify::new(),
shutdown_gate: tokio::sync::Mutex::new(()),
outcome,
turn_failure: Mutex::new(None),
host_stop_requested: AtomicBool::new(false),
})
}
@@ -654,6 +704,7 @@ impl ExecutionAdapter {
}
pub(super) fn cancel_from_host(self: &Arc<Self>) {
self.request_host_stop();
if self.background_done.load(Ordering::Acquire) || self.closed.load(Ordering::Acquire) {
return;
}
@@ -672,6 +723,55 @@ impl ExecutionAdapter {
let _ = tokio::task::spawn_blocking(move || session.interrupt(message)).await;
}
/// 宿主判定"这一轮以失败收口":记下 `(分类, 原因)`,再把同一条原因写进宿主交付报告。
///
/// 谁调用:宿主亲眼看到或亲手判定的异常收场——执行通道断开(app-server 进程退出 / 流断 / 回合
/// 事件通道关闭)、等待模型回执超时、app-server 单方面把这一轮判成中断。终态判定会读这份事实,
/// 于是这些收场不会再被收尾阶段(`ExecutionPhase::Interrupted`)抹成一次没有原因的"已结束"。
///
/// **宿主自己收束的这一轮不算失败。** 正常终态、用户主动停止、预算与交付收尾都会把连接关掉,
/// 回合事件通道上看到的是同一个 `TransportClosed`;判据有两条,都收在这里,调用点不必各写一遍:
///
/// - [`Self::is_closed`]:适配器先于连接置位,说明这一轮是宿主在收束;
/// - [`Self::host_stop_requested`]:用户按过「终止」。`cancel_from_host` 先**同步**置位再异步
/// 中断会话,`closed` 与阶段都要等那个任务跑到才变,所以"标志已置、阶段未变"的窗口里到达的
/// 通道断开 / 中断都是宿主自己收尾的结果,不能记成 `transport-failed`。
///
/// 不记失败事实不等于不收束:原因照样写进报告(`interrupt` 会把它追加进去),便于核对。
///
/// **事实要落在适配器上,不能落在调用点的局部变量里。** 回合还开着的时候,看门狗会在同一个
/// `inner.closed` 标志上把本轮收束掉(见 [`Self::start_watchdog`]),谁先谁后取决于调度,而终态
/// 判定发生在收束之后;记不下原因,界面就只能看到"本轮已结束"、看不到为什么。
///
/// 只记第一份:第一份最接近现场(连接终止时带 exitStatus / stderr 摘要),后面更粗的收束理由
/// 不得覆盖它。
pub(super) async fn fail_turn(&self, failure: DirectTurnError) {
let reason = failure.to_string();
if !self.is_closed() && !self.host_stop_requested() {
if let Ok(mut slot) = self.turn_failure.lock() {
if slot.is_none() {
*slot = Some(failure);
}
}
}
self.interrupt(&reason).await;
}
/// 本轮以什么理由失败;有值就是宿主记下的 typed 事实。终态判定只读这一次。
pub(super) fn turn_failure(&self) -> Option<DirectTurnError> {
self.turn_failure.lock().ok().and_then(|slot| slot.clone())
}
/// 记下"用户主动要求终止这一轮"。用来把用户主动终止与 app-server 自己中断分开:
/// 前者不是失败,后者是(判据不能被事件到达的先后顺序左右,所以用标志而不是看阶段)。
pub(super) fn request_host_stop(&self) {
self.host_stop_requested.store(true, Ordering::Release);
}
pub(super) fn host_stop_requested(&self) -> bool {
self.host_stop_requested.load(Ordering::Acquire)
}
pub(super) fn start_watchdog(self: &Arc<Self>, inner: Weak<CodexAppServerInner>) {
let adapter = Arc::clone(self);
tokio::spawn(async move {
@@ -744,7 +844,19 @@ impl ExecutionAdapter {
.unwrap_or(true)
}
/// 本轮是不是**由宿主自己**在收束(正常终态 / 用户主动停止 / 预算收尾 / 交付封口)。
///
/// 用来把"连接被我们关掉"和"连接自己断了"分开:两种情况下回合事件通道都会收到
/// `TransportClosed`,但只有后者才算执行通道失败(见 [`Self::transport_failed`])。
/// `finish_model_attempt` 与 `shutdown_and_report` 都会在收束连接之前把它置位。
fn is_closed(&self) -> bool {
self.closed.load(Ordering::Acquire)
}
pub(super) fn lifecycle_status(&self, fallback: &str) -> String {
// 只按收尾阶段归类。失败事实(`fail_turn` 记下的)不在这里翻案:终态由
// `direct_turn_terminal` 拿事实判定——否则"模型已经判失败"的一轮会被这里的
// `Interrupted` 抹成一次没有原因的"已结束"。
match self.session.snapshot().map(|state| state.phase) {
Ok(ExecutionPhase::Completed) => "completed",
Ok(ExecutionPhase::Exhausted | ExecutionPhase::Interrupted) => "interrupted",
@@ -1037,6 +1149,8 @@ pub(super) async fn wait_outcome(
#[cfg(test)]
mod tests {
use super::super::DirectTurnDeadline;
use super::*;
fn fixture() -> (tempfile::TempDir, Arc<ExecutionAdapter>) {
@@ -1084,6 +1198,82 @@ mod tests {
.unwrap();
}
#[tokio::test]
async fn host_observed_failure_is_recorded_with_its_kind_and_reason() {
let (_temp, adapter) = fixture();
assert!(adapter.turn_failure().is_none());
assert!(!adapter.host_stop_requested());
adapter
.fail_turn(DirectTurnError::TransportClosed {
diagnostic: "Codex app-server 已退出;exitStatus=signal: 9 (SIGKILL)".into(),
})
.await;
// 终态判定读这份事实,界面才有理由把它当失败讲,而不是"本轮已结束"。
let failure = adapter.turn_failure().expect("host fact must be recorded");
assert_eq!(
failure.wire_kind(),
Some(super::super::DirectTurnFailureKind::TransportFailed)
);
assert!(failure.to_string().contains("SIGKILL"));
// 报告与事件载荷同一份原因:用户看到的现象和交付状态对得上。
assert!(adapter.report().contains("SIGKILL"));
// 只认第一份原因:后续更粗的收束理由不得覆盖真实诊断。
adapter
.fail_turn(DirectTurnError::TimedOut {
deadline: DirectTurnDeadline::ResponseIdle,
})
.await;
let failure = adapter.turn_failure().expect("first reason is kept");
assert_eq!(
failure.wire_kind(),
Some(super::super::DirectTurnFailureKind::TransportFailed)
);
assert!(failure.to_string().contains("SIGKILL"));
assert!(!failure.to_string().contains("超时"));
}
/// 宿主自己关的连接不算失败:正常终态、用户主动停止、预算与交付收尾都会关掉连接,回合事件通道
/// 上看到的是同一个 `TransportClosed`。判据是适配器先于连接置位 `closed`。
#[tokio::test]
async fn host_ended_turn_is_not_a_failure() {
let (_temp, adapter) = fixture();
adapter.request_host_stop();
adapter.closed.store(true, Ordering::Release);
adapter
.fail_turn(DirectTurnError::TransportClosed {
diagnostic: "模型本次执行结束,回收原生后台子树".into(),
})
.await;
assert!(adapter.turn_failure().is_none());
assert!(adapter.host_stop_requested());
// 原因照样进报告:不算失败不等于不用记。
assert!(adapter.report().contains("模型本次执行结束"));
}
/// 用户按下的「终止」不记失败事实:`cancel_from_host` 先同步置位 `host_stop_requested`、再异步
/// 中断会话,这中间到达的通道断开 / 中断都是宿主自己收尾的结果,不能讲成 `transport-failed`。
#[tokio::test]
async fn user_requested_stop_is_not_recorded_as_a_failure() {
let (_temp, adapter) = fixture();
adapter.request_host_stop();
adapter
.fail_turn(DirectTurnError::TransportClosed {
diagnostic: "Codex app-server 已退出;exitStatus=signal: 9 (SIGKILL)".into(),
})
.await;
assert!(adapter.turn_failure().is_none());
assert!(adapter.host_stop_requested());
// 不算失败不等于不用记:原因照样进报告,排障能看到现场。
assert!(adapter.report().contains("SIGKILL"));
}
#[tokio::test]
async fn production_snapshot_identity_uses_canonical_digest_and_preserves_manifest_authority() {
let (_temp, adapter) = fixture();
@@ -1428,9 +1618,13 @@ mod tests {
super::super::super::codex_cli::codex_bundle::CLI_VERSION
)
.is_ok());
// 开发构建(含本测试构建)跳过版本门禁,只有发行构建要求严格等于固定版本。
#[cfg(debug_assertions)]
assert!(validate_approval_version("codex-cli 0.156.0").is_ok());
#[cfg(not(debug_assertions))]
for version in [
"codex-cli 0.155.0",
"codex-cli 0.154.0",
"codex-cli 0.156.0",
"unknown",
"0.155.1",
] {
File diff suppressed because it is too large Load Diff
@@ -697,10 +697,14 @@ pub(super) async fn finish_sealing(
}).await.map_err(|_| "delivery-finalize-worker-exited")?
}
/// 回合末的宿主复核:返回要交付的答复,或者一个"还没完,按这份证据继续修"的要求。
///
/// 返修要求是**控制流**([`DirectTurnError::ReviewRequired`]),不是失败:调用方据此把要求写回
/// prompt 再跑一轮,界面不该看到失败文案。其余错误都是真的回合失败,按 typed 错误交给上层。
pub(super) async fn review_reply(
root: &Path,
session: &Arc<ExecutionSession>,
) -> Result<Option<String>, String> {
) -> Result<Option<String>, DirectTurnError> {
if let Some(report) = terminal_report(session) {
return Ok(Some(report));
}
@@ -751,7 +755,9 @@ pub(super) async fn review_reply(
.map_err(|_| "delivery-review-worker-exited")??;
return Ok(Some(report));
}
Err(format!("delivery-review-required: {detail}"))
Err(DirectTurnError::ReviewRequired {
detail: format!("delivery-review-required: {detail}"),
})
}
#[cfg(test)]
@@ -914,10 +920,10 @@ mod tests {
assert_eq!(chat.snapshot().unwrap().delivery_reviews, 0);
let (new_game, _new_host, required) = project_session(true);
for _ in 0..2 {
assert!(review_reply(new_game.path(), &required)
.await
.unwrap_err()
.starts_with("delivery-review-required:"));
assert!(matches!(
review_reply(new_game.path(), &required).await.unwrap_err(),
DirectTurnError::ReviewRequired { .. }
));
}
assert!(review_reply(new_game.path(), &required)
.await
@@ -704,9 +704,19 @@ pub(super) fn open_with_analytics_at(
impl ExecutionSession {
pub(super) fn bind_codex_executor(&self, path: &Path, version: &str) -> Result<(), String> {
if version.trim() != super::codex_cli::codex_bundle::CLI_VERSION {
return Err("direct-execution-executor: 尚未验证该执行器的补丁协议".into());
// 发行构建只接受捆绑侧车固定版本;开发构建用宿主自带的 Codex,按 profile 跳过该门禁。
#[cfg(not(debug_assertions))]
{
if version.trim() != super::codex_cli::codex_bundle::CLI_VERSION {
return Err(format!(
"direct-execution-executor: 尚未验证该执行器的补丁协议(期望 {},实际 {})",
super::codex_cli::codex_bundle::CLI_VERSION,
version.trim()
));
}
}
#[cfg(debug_assertions)]
let _ = version;
let path = path
.canonicalize()
.map_err(|_| "direct-execution-executor: 无法锚定执行器")?;
@@ -651,6 +651,8 @@ fn patch_executor_identity_is_frozen_and_content_changes_are_rejected() {
std::fs::write(&path, "trusted test bytes").unwrap();
let pinned = super::super::codex_cli::codex_bundle::CLI_VERSION;
assert!(session.codex_executor().is_err());
// 开发构建跳过执行器版本门禁;发行构建仍然拒绝版本漂移。
#[cfg(not(debug_assertions))]
assert!(session
.bind_codex_executor(&path, "codex-cli 0.155.0")
.is_err());
@@ -437,8 +437,14 @@ mod tests {
async fn a_replaced_active_turn_marks_the_batch_stale() {
let (_temp, root) = project();
std::fs::write(root.join("code.js"), "unchanged").unwrap();
// 身份来自逻辑回合(Thread Manager):接单才是"这一轮在跑"的唯一登记。
let owner = Arc::new(std::sync::Mutex::new(Some(
DirectTaonierActiveInvocationGuard::enter(&root, "turn-before").unwrap(),
DirectTurnReservation::accept(
&direct_thread_id_for_project(&root),
"turn-before",
None,
)
.unwrap(),
)));
let swap = Arc::clone(&owner);
let result = read_batch_with(
@@ -449,7 +455,14 @@ mod tests {
let result = read_file(r, f, b);
let mut guard = swap.lock().unwrap();
drop(guard.take());
*guard = Some(DirectTaonierActiveInvocationGuard::enter(r, "turn-after").unwrap());
*guard = Some(
DirectTurnReservation::accept(
&direct_thread_id_for_project(r),
"turn-after",
None,
)
.unwrap(),
);
result
},
)
@@ -583,7 +596,14 @@ mod tests {
#[tokio::test]
async fn host_prefetch_keeps_data_out_of_system_rules_and_matches_active_turn() {
let (_temp, root) = project();
// 调用身份(预取闸门)与逻辑回合(上下文身份)是两件事,生产入口两步都做。
let _guard = DirectTaonierActiveInvocationGuard::enter(&root, "prefetch-turn").unwrap();
let _turn = DirectTurnReservation::accept(
&direct_thread_id_for_project(&root),
"prefetch-turn",
None,
)
.unwrap();
let data = prefetch_turn_input(&root, "prefetch-turn")
.await
.unwrap()
File diff suppressed because it is too large Load Diff
@@ -7,9 +7,9 @@ use super::*;
pub(crate) fn normalize_direct_client_turn_id(
client_turn_id: Option<&str>,
) -> Result<String, String> {
) -> Result<String, DirectTurnError> {
let Some(client_turn_id) = client_turn_id else {
return Err("Direct 客户端回合缺少稳定 clientTurnId,已拒绝创建可计费生成身份".to_string());
return Err(DirectTurnError::ClientTurnIdMissing);
};
let client_turn_id = client_turn_id.trim();
let valid_length = (MIN_DIRECT_CLIENT_TURN_ID_CHARS..=MAX_DIRECT_CLIENT_TURN_ID_CHARS)
@@ -20,13 +20,28 @@ pub(crate) fn normalize_direct_client_turn_id(
.is_some_and(|byte| byte.is_ascii_alphanumeric());
let valid_rest = bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-');
if !valid_length || !valid_first || !valid_rest {
return Err(format!(
"clientTurnId 必须为 {MIN_DIRECT_CLIENT_TURN_ID_CHARS} 到 {MAX_DIRECT_CLIENT_TURN_ID_CHARS} 位 ASCII 字母、数字或连字符,且首位必须为字母或数字"
));
return Err(DirectTurnError::ClientTurnIdMalformed {
min_chars: MIN_DIRECT_CLIENT_TURN_ID_CHARS,
max_chars: MAX_DIRECT_CLIENT_TURN_ID_CHARS,
});
}
Ok(client_turn_id.to_string())
}
/// DirectProject 聊天命令:**只接单**,不再 await 整轮。
///
/// 边界文案仍只在这里生成一次(`Display`);但 `Err` 的含义收窄成**拒单**——接单成立之后的
/// 一切失败(连不上 app-server、配置 / 凭据未就绪、历史注入失败、`turn/start` 被拒、模型与
/// 交付失败)都由这一轮的占用对象收口成 `turn.completed` 带失败载荷,不再回到这条返回值上。
///
/// 于是"这一轮跑成什么"只有订阅事件一个来源:命令返回 `Ok` 只说明**接单成立**。可留痕的调用级
/// 拒绝(宿主 / 环境事实)仍在边界补一份运行错误诊断,返回串不带诊断引用。
///
/// 与 CLI 的分工:CLI 入口(`cli.rs` 的 `direct-codex.chat`)**保持 await**——它要把那段回复文本
/// 打到终端上,没有事件订阅可用;它复用同一份接单前检查与同一个命令主体,只是自己等整轮的返回值。
/// 两个入口共用 [`direct_turn_error_boundary_text`] / [`direct_turn_rejection`],不要再各写一套判据。
///
/// 设计见 `docs/adr/【ADR】DirectProject命令接单化-2026-09-23.md`。
#[tauri::command]
pub(crate) async fn chat_with_game_creator_direct_codex(
project_path: String,
@@ -34,42 +49,369 @@ pub(crate) async fn chat_with_game_creator_direct_codex(
creation_type: Option<String>,
client_turn_id: Option<String>,
analytics_attempt_id: Option<String>,
) -> Result<String, String> {
let capture = crate::analytics::gui::capture_writer_context();
) -> Result<(), DirectTurnRejection> {
let root = Path::new(project_path.trim());
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
let _active_invocation = DirectTaonierActiveInvocationGuard::enter(root, &turn_id)?;
recover_direct_taonier_regeneration_workflow_at(root).map_err(|error| {
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
})?;
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
validate_direct_codex_user_item(root, &user_item)?;
let user_prompt = direct_codex_user_item_to_prompt(root, &user_item)?;
if user_prompt.trim().is_empty() {
return Err("聊天内容不能为空".to_string());
}
let canonical_user_item =
// 创建类型来自结构化用户入口;实际工程和可信脚手架由宿主复核。
match crate::environment_check::prepare_new_web_project_at(root, creation_type.as_deref())
.await
{
Ok(_) => Some(serde_json::to_value(user_item).map_err(|error| error.to_string())?),
Err(error) => return Err(redact_agent_runtime_error(root, &error, 1800)),
};
let reply = match run_direct_game_creator_turn_at_with_creation_type_and_emitter(
let boundary_turn_id = client_turn_id.clone();
chat_with_game_creator_direct_codex_typed(
root,
user_item,
creation_type,
client_turn_id,
analytics_attempt_id,
)
.await
.map_err(|failure| direct_turn_rejection(root, boundary_turn_id.as_deref(), failure))
}
/// 命令主体:全程 typed。顺序固定,**每一步失败都还是拒单**:
/// `clientTurnId` 校验 → 占用调用身份 → 工作流恢复 → 用户条目校验 → 前置条件 → 工程准备
/// → 接单 → 落盘用户条目 → 后台起整轮。
///
/// 这个顺序不是风格问题:接单(`DirectTurnReservation::accept`)必须在所有"接单前就能判定"的
/// 检查之后,也必须早于用户条目落盘与 `turn/start`,否则并发拒单会晚于副作用、逻辑回合的开始
/// 事件会排在用户消息之后。
async fn chat_with_game_creator_direct_codex_typed(
root: &Path,
user_item: DirectCodexUserItem,
creation_type: Option<String>,
client_turn_id: Option<String>,
analytics_attempt_id: Option<String>,
) -> Result<(), DirectTurnError> {
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
// 占用调用身份:并发拒单要早于工程准备,避免两个请求同时改同一个项目。它只挡并发,**不是**
// 首页"运行中的项目"的来源(那张表由 Thread Manager 的逻辑回合导出),但仍必须与整轮同生
// 共死——随任务一起搬进后台。
let active_invocation = DirectTaonierActiveInvocationGuard::enter(root, &turn_id)?;
recover_direct_taonier_regeneration_workflow_at(root).map_err(|error| {
DirectTurnError::HostStateUnavailable {
detail: redact_agent_runtime_error(
root,
&format!("恢复上一轮陶泥儿整包事务失败:{error}"),
500,
),
}
})?;
validate_direct_codex_user_item(root, &user_item)
.map_err(|detail| DirectTurnError::InputRejected { detail })?;
let user_prompt = direct_codex_user_item_to_prompt(root, &user_item)
.map_err(|detail| DirectTurnError::InputRejected { detail })?;
check_direct_turn_preconditions(root, &user_prompt, creation_type.as_deref())?;
let canonical_user_item =
serde_json::to_value(&user_item).map_err(|error| DirectTurnError::InputRejected {
detail: error.to_string(),
})?;
// 创建类型来自结构化用户入口;实际工程和可信脚手架由宿主复核。
crate::environment_check::prepare_new_web_project_at(root, creation_type.as_deref())
.await
.map_err(|error| {
let detail = redact_agent_runtime_error(root, &error, 1800);
DirectTurnError::EnvironmentNotReady { detail }
})?;
// 接单:从这里开始这一轮就成立了。开始事件的身份由 `clientTurnId` 推导,**不读盘回填**
// ——开始事件发生在用户条目落盘之前,而落盘本身也可能失败。
let thread_id = direct_thread_id_for_project(root);
let user_item_id = direct_codex_user_item_id_for_client_turn_id(&turn_id);
let reservation = DirectTurnReservation::accept(&thread_id, &turn_id, user_item_id.as_deref())?;
// 落盘即接单:接单成功就必须在历史里留下这条用户消息,哪怕这一轮随后失败。
if let Err(error) = append_direct_project_user_message_at(root, &canonical_user_item) {
// 这一轮**已经接单**,所以收口只能走占用对象:写出失败终态(事件流里的那条失败说明就是
// 界面唯一一份解释),然后返回 `Ok`——命令的 `Err` 只表示**拒单**,回到那里会让同一个失败
// 同时从事件与横幅两条通道下发,也会让前端把"已经开始的回合"读成"没开始"。
// 不继续起整轮:历史是这条对话的单一事实源,用户消息没落盘时继续跑只会得到一条没有开口
// 用户消息的助手回复,而且失败会被静默掉。
let failure = DirectTurnError::EnvironmentNotReady {
detail: redact_agent_runtime_error(
root,
&format!("写入本项目对话历史失败:{error}"),
600,
),
};
reservation.finish_if_unfinished(DirectTurnTerminal::failed(root, &failure));
return Ok(());
}
// 用户条目落盘成功即下发:这一轮从"接单"到"起 codex"之间的一切失败(连不上
// app-server、执行器未通过验收、历史注入失败)都靠它把失败说明挂回自己那一轮;晚到
// `turn/start` 之后才发,这些失败就没有用户条目可挂,界面会把说明显示在用户消息上面。
crate::agent::codex_app_server::emit_direct_thread_user_item(root, &canonical_user_item);
let capture = crate::analytics::gui::capture_writer_context();
let root = root.to_path_buf();
tauri::async_runtime::spawn(async move {
run_accepted_direct_turn(
root,
turn_id,
user_prompt,
creation_type,
canonical_user_item,
capture,
analytics_attempt_id,
active_invocation,
reservation,
)
.await;
});
Ok(())
}
/// 接单之后的整轮:命令不再 await 它,它的收场只走事件流。
///
/// 三条收场路径都在这里收口:正常(深层的终态出口写 `turn.completed`)、失败(没有深层终态的
/// 早退由这里的占用对象补)、任务被丢弃 / panic(占用对象的 `Drop` 补 `host-dropped`)。
///
/// 两个守卫都**必须活到整轮结束**,所以随任务搬进来,不留在命令里:
/// `_active_invocation` 是这一轮的调用身份(并发拒单与首页在途回合都读它),`reservation`
/// 是逻辑回合的占用。
#[allow(clippy::too_many_arguments)]
async fn run_accepted_direct_turn(
root: std::path::PathBuf,
turn_id: String,
user_prompt: String,
creation_type: Option<String>,
canonical_user_item: serde_json::Value,
capture: Option<(
crate::analytics::contract::Context,
crate::analytics::store::AnalyticsWriter,
)>,
analytics_attempt_id: Option<String>,
_active_invocation: DirectTaonierActiveInvocationGuard,
reservation: DirectTurnReservation,
) {
let emitter = DirectGameCreatorTurnUpdateEmitter::new(&root, turn_id);
let outcome = run_direct_game_creator_turn_at_with_creation_type_and_emitter(
&root,
&user_prompt,
creation_type.as_deref(),
Some(&turn_emitter),
canonical_user_item,
Some(&emitter),
Some(canonical_user_item),
capture,
analytics_attempt_id.as_deref(),
)
.await
{
Ok(reply) => reply,
Err(error) => return Err(error),
};
turn_emitter.emit("completed", Some("none"), Some(reply.clone()), None);
Ok(reply)
.await;
match outcome {
Ok(reply) => {
// 深层的终态出口已经在 `run_turn` 里写出 `turn.completed`;这里只补最后一条回合更新。
emitter.emit("completed", Some("none"), Some(reply), None);
}
Err(error) => {
// 接单之后的失败一律是回合失败:失败诊断与失败说明已由上层写过,这里补终态事件。
// 深层已经写出终态时它不覆盖(同一轮只允许一条终态)。
reservation.finish_if_unfinished(DirectTurnTerminal::failed(&root, &error));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::{consume_direct_thread, subscribe_direct_thread, DirectThreadEvent};
/// 接单之后的早退也必须有终态。
///
/// 这里用一个"目录存在但不是项目"的根制造一条**接单之后**才发现的失败(连 `run_turn` 的
/// 收尾都走不到)。命令此时早已返回 `Ok`,前端唯一的收口依据就是事件流,所以占用对象必须
/// 补出 `turn.completed`——这正是接单化要买的那条不变式。
#[tokio::test]
async fn a_failure_after_accept_still_closes_the_logical_turn() {
let temp = tempfile::tempdir().expect("temp dir");
let root = temp.path().join("not-a-project");
std::fs::create_dir_all(&root).expect("create project dir");
let thread_id = direct_thread_id_for_project(&root);
let subscription = subscribe_direct_thread(&thread_id);
let _ = consume_direct_thread(&subscription.subscription_id);
let reservation =
DirectTurnReservation::accept(&thread_id, "turn-1", Some("direct-codex:turn-1:user"))
.expect("accept logical turn");
let invocation =
DirectTaonierActiveInvocationGuard::enter(&root, "turn-1").expect("enter invocation");
run_accepted_direct_turn(
root.clone(),
"turn-1".to_string(),
"你好".to_string(),
None,
serde_json::json!({
"type": "message",
"role": "user",
"id": "direct-codex:turn-1:user",
"content": [{ "type": "input_text", "text": "你好" }],
}),
None,
None,
invocation,
reservation,
)
.await;
let events = consume_direct_thread(&subscription.subscription_id)
.expect("consume logical turn")
.events;
let terminal = events
.iter()
.filter_map(|event| match event {
DirectThreadEvent::TurnCompleted {
status,
failure,
user_item_id,
..
} => Some((status, failure, user_item_id)),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(terminal.len(), 1, "一轮只许有一条终态:{events:?}");
let (status, failure, user_item_id) = terminal[0];
assert_eq!(status, "failed");
let failure = failure.as_ref().expect("失败终态必须带载荷");
assert!(
!failure.message.trim().is_empty(),
"接单之后的失败必须带上原因"
);
assert_eq!(user_item_id.as_deref(), Some("direct-codex:turn-1:user"));
}
/// 本轮的开口用户条目必须先于整轮里任何可能失败的东西下发。
///
/// 现场(用户可见的坏体验):命令接单、用户条目落盘之后,整轮在 `turn/start` 之前就失败
/// (连不上 app-server 一类)。这时如果用户条目还没下发,界面就只剩一条失败说明——它按位置
/// 落进**上一轮**的分区里,于是"错误显示在用户消息上面"、上一轮顶替本轮显示耗时,本轮的用户
/// 气泡再自成一个 0.0 秒的假回合。
///
/// 判据取事件流的前两条:命令体是顺序执行的,后台整轮是它之后才起的,所以"开始 → 用户条目"
/// 一定在最前面,之后才可能有失败终态。
#[tokio::test]
async fn the_opening_user_item_is_emitted_before_anything_that_can_fail_in_the_turn() {
let temp = tempfile::tempdir().expect("temp dir");
let root = temp.path().join("direct-user-item-first");
crate::init_local_game_project_at(&root, "direct-user-item-first", "用户条目先下发")
.expect("init project");
let thread_id = direct_thread_id_for_project(&root);
let subscription = subscribe_direct_thread(&thread_id);
let _ = consume_direct_thread(&subscription.subscription_id);
let user_item: DirectCodexUserItem = serde_json::from_value(serde_json::json!({
"type": "message",
"role": "user",
"id": "direct-codex:turn-1:user",
"content": [{ "type": "input_text", "text": "hello" }],
}))
.expect("canonical user item");
chat_with_game_creator_direct_codex_typed(
&root,
user_item,
None,
Some("turn-1".to_string()),
None,
)
.await
.expect("接单成立:命令只回报接单");
let events = consume_direct_thread(&subscription.subscription_id)
.expect("consume logical turn")
.events;
assert!(
matches!(
events.first(),
Some(DirectThreadEvent::TurnStarted { user_item_id, .. })
if user_item_id.as_deref() == Some("direct-codex:turn-1:user")
),
"第一条必须是带身份的回合开始:{events:?}"
);
assert!(
matches!(
events.get(1),
Some(DirectThreadEvent::ItemCompleted { item, .. })
if item.item_id() == "direct-codex:turn-1:user"
),
"第二条必须是本轮的开口用户条目:{events:?}"
);
let terminal = events
.iter()
.position(|event| matches!(event, DirectThreadEvent::TurnCompleted { .. }));
assert!(
terminal.is_none_or(|index| index > 1),
"终态只能在用户条目之后:{events:?}"
);
// 落盘与下发同一份身份:历史里的条目 id 就是事件里的 itemId。
let persisted = std::fs::read_to_string(root.join(".agent/conversations/project.jsonl"))
.expect("read project history");
assert!(
persisted.contains("direct-codex:turn-1:user"),
"用户条目必须已经落盘:{persisted}"
);
}
/// 接单之后的落盘失败:**只走占用对象的失败终态**,命令返回 `Ok`。
///
/// 这条路径的 `turn.started` 已经发过,命令再回一个 `Err` 就等于同一个失败下发两次(事件一条
/// 说明、横幅又一份),而且 `Err` 的含义是**拒单**——前端会把它读成"这一轮没开始"。历史追加写
/// 有一条测试注入(`.agent/runtime/test-fail-next-direct-project-history-append`),用它把这条
/// 路径钉成确定性:恰好一条失败终态、命令 `Ok`、占用释放(下一轮还能接单)。
#[tokio::test]
async fn a_history_write_failure_after_accept_closes_the_turn_instead_of_rejecting() {
let temp = tempfile::tempdir().expect("temp dir");
let root = temp.path().join("direct-history-write-failure");
crate::init_local_game_project_at(&root, "direct-history-write", "落盘失败")
.expect("init project");
let thread_id = direct_thread_id_for_project(&root);
let subscription = subscribe_direct_thread(&thread_id);
let _ = consume_direct_thread(&subscription.subscription_id);
// 接下来这次追加写的两次尝试都按"争用失败"返回:确定性地走到落盘失败分支。
std::fs::write(
root.join(".agent/runtime/test-fail-next-direct-project-history-append"),
"9",
)
.expect("write history contention injection");
let user_item: DirectCodexUserItem = serde_json::from_value(serde_json::json!({
"type": "message",
"role": "user",
"id": "direct-codex:turn-1:user",
"content": [{ "type": "input_text", "text": "生成一个游戏" }],
}))
.expect("canonical user item");
chat_with_game_creator_direct_codex_typed(
&root,
user_item,
None,
Some("turn-1".to_string()),
None,
)
.await
.expect("接单之后的失败不再回到命令返回值:命令只回报接单成立");
let events = consume_direct_thread(&subscription.subscription_id)
.expect("consume logical turn")
.events;
let terminals = events
.iter()
.filter_map(|event| match event {
DirectThreadEvent::TurnCompleted {
status, failure, ..
} => Some((status, failure)),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(terminals.len(), 1, "一轮只许有一条终态:{events:?}");
let (status, failure) = terminals[0];
assert_eq!(status, "failed");
let failure = failure.as_ref().expect("失败终态必须带载荷");
assert!(
failure.message.contains("写入本项目对话历史失败"),
"{}",
failure.message
);
// 这一轮已经接单,所以走的是**回合失败**:拒单那套 `direct-codex-failure:v2` 收口文案
// 不许出现在这里(它只属于可留痕的拒单)。
assert!(
!failure.message.contains("direct-codex-failure"),
"{}",
failure.message
);
// 占用已释放:下一轮还能接单。
assert!(!crate::agent::direct_thread_turn_is_active(&thread_id));
assert!(DirectTurnReservation::accept(
&thread_id,
"turn-2",
Some("direct-codex:turn-2:user")
)
.is_ok());
}
}
@@ -35,6 +35,46 @@ struct SubscriberState {
cursor: u64,
}
/// 一条正在跑的逻辑回合的占用:接单时登记,终态写出时解除。
///
/// 它同时是首页「运行中的项目」快照的**唯一事实源**([`list_direct_active_turns`]):这一格的
/// 生命周期就是"这一轮在不在跑",进度字段由运行时那一侧经 [`update_direct_thread_active_turn`]
/// 回填。任务侧不再另建一张活动回合表——同一件事只许有一处真相。
///
/// 两个身份别混:
/// - `token` 是这一次接单的占用身份:终态出口只有拿着同一个 token 的占用对象才能写兜底终态,
/// 避免迟到的旧占用把新回合的边界顶掉。它不对外。
/// - `turn_id` 是给界面看的回合身份(`clientTurnId` 派生),只服务快照与进度回填的匹配。
#[derive(Clone, Debug)]
struct ActiveDirectTurn {
token: String,
turn_id: String,
project_name: Option<String>,
started_at: u64,
status: String,
activity: Option<String>,
updated_at: u64,
sequence: u64,
}
/// 首页「运行中的项目」的一条快照。
///
/// `project_path` 与线上其它地方的项目身份取同一个字符串:Thread Manager 的线程身份就是项目的
/// canonical 路径(见 `direct_thread_id_for_project`),所以快照里的项目身份与事件流里的身份
/// 永远能对上,不需要调用方再做一次归一。
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DirectActiveTurnSnapshot {
pub(crate) project_path: String,
pub(crate) project_name: Option<String>,
pub(crate) turn_id: String,
pub(crate) started_at: u64,
pub(crate) status: String,
pub(crate) activity: Option<String>,
pub(crate) updated_at: u64,
pub(crate) sequence: u64,
}
#[derive(Clone, Debug)]
struct ThreadState {
next_seq: u64,
@@ -43,6 +83,8 @@ struct ThreadState {
total_bytes: usize,
active_items: HashSet<String>,
unresolved_requests: HashSet<String>,
/// 未收口的逻辑回合。`None` 表示这个 thread 没有正在跑的回合。
active_turn: Option<ActiveDirectTurn>,
/// 最近一条 `turn.started` / `turn.completed` 的独立拷贝。
///
/// TODO(thread-manager): 这里有意只保留"锚点",因为 replay 队列会回收可回收事件,
@@ -63,6 +105,7 @@ impl Default for ThreadState {
total_bytes: 0,
active_items: HashSet::new(),
unresolved_requests: HashSet::new(),
active_turn: None,
lifecycle_anchor: None,
subscribers: HashMap::new(),
}
@@ -155,6 +198,139 @@ impl DirectThreadManager {
}
}
/// 接单:同一个临界区里拒绝并发、登记占用、追加逻辑回合开始事件。
///
/// 返回 `Err(existing_turn_id)` 表示这个 thread 已经有一条没收口的回合——此时不动队列,
/// 由调用方把它投影成接单拒绝。回的是**回合身份**(调用方接单时给的 `turn_id`)而不是占用
/// `token`:占用 token 只活在这个进程里,界面拿它匹配不了自己发出的那一轮,也没法判断
/// "撞的是同一轮还是另一轮"。
fn accept_turn(
&mut self,
thread_id: &str,
token: &str,
turn_id: &str,
user_item_id: Option<&str>,
started_at_ms: u64,
) -> Result<DirectThreadEvent, String> {
{
let thread = self.threads.entry(thread_id.to_string()).or_default();
if let Some(active) = thread.active_turn.as_ref() {
return Err(active.turn_id.clone());
}
thread.active_turn = Some(ActiveDirectTurn {
token: token.to_string(),
turn_id: turn_id.to_string(),
project_name: std::path::Path::new(thread_id)
.file_name()
.and_then(|name| name.to_str())
.map(str::to_string),
started_at: started_at_ms,
// 与"还没有任何进度事件"的状态一致:运行时给出的第一条进度会覆盖它。
status: "accepted".to_string(),
activity: Some("request-accepted".to_string()),
updated_at: started_at_ms,
sequence: 0,
});
}
Ok(self.append(
thread_id,
DirectThreadEvent::turn_started(started_at_ms).with_user_item_id(user_item_id),
))
}
/// 运行时回填这一轮的进度。只认"仍在跑 + 回合身份一致 + 序号不倒退"的那一次。
///
/// 返回是否真的写进去了:没有未收口的回合、身份对不上(上一轮迟到的进度)、序号倒退
/// (乱序到达的旧进度)都必须原地丢弃,不能把快照改成过期的样子。
fn update_active_turn(
&mut self,
thread_id: &str,
turn_id: &str,
status: &str,
activity: Option<&str>,
sequence: u64,
updated_at: u64,
) -> bool {
let Some(active) = self
.threads
.get_mut(thread_id)
.and_then(|thread| thread.active_turn.as_mut())
else {
return false;
};
if active.turn_id != turn_id || sequence < active.sequence {
return false;
}
active.status = status.to_string();
active.activity = activity.map(str::to_string);
active.updated_at = updated_at;
active.sequence = sequence;
true
}
/// 首页快照:只导出仍有未收口逻辑回合的 thread。
fn active_turn_snapshots(&self) -> Vec<DirectActiveTurnSnapshot> {
self.threads
.iter()
.filter_map(|(thread_id, thread)| {
let active = thread.active_turn.as_ref()?;
Some(DirectActiveTurnSnapshot {
project_path: thread_id.clone(),
project_name: active.project_name.clone(),
turn_id: active.turn_id.clone(),
started_at: active.started_at,
status: active.status.clone(),
activity: active.activity.clone(),
updated_at: active.updated_at,
sequence: active.sequence,
})
})
.collect()
}
/// 深层的终态出口:解除占用并写下 `turn.completed`。
///
/// 不校验 token:这一条由真正跑完这一轮的代码调用,终态就是它算出来的那个(CLI 这类没有
/// 占用登记的入口也走这里,保持"终态一定下发"的既有语义)。
fn complete_turn(&mut self, thread_id: &str, event: DirectThreadEvent) -> DirectThreadEvent {
if let Some(thread) = self.threads.get_mut(thread_id) {
thread.active_turn = None;
}
self.append(thread_id, event)
}
/// 占用对象的兜底出口:只有当这个 thread 仍被同一个 token 占用时才写。
///
/// 返回是否真的写了。深层已经写出终态时返回 `false`——兜底不覆盖真实结果。
fn complete_turn_if_reserved(
&mut self,
thread_id: &str,
token: &str,
event: DirectThreadEvent,
) -> bool {
let reserved = match self.threads.get_mut(thread_id) {
Some(thread) => match thread.active_turn.as_ref() {
Some(active) if active.token == token => {
thread.active_turn = None;
true
}
_ => false,
},
None => false,
};
if !reserved {
return false;
}
self.append(thread_id, event);
true
}
fn turn_is_active(&self, thread_id: &str) -> bool {
self.threads
.get(thread_id)
.is_some_and(|thread| thread.active_turn.is_some())
}
fn subscriber_ids(&self, thread_id: &str) -> Vec<String> {
self.threads
.get(thread_id)
@@ -377,13 +553,105 @@ pub(crate) fn append_direct_thread_event(
thread_id: &str,
event: DirectThreadEvent,
) -> DirectThreadEvent {
let (event, subscriber_ids) = {
let mut manager = global_direct_thread_manager()
let event = {
global_direct_thread_manager()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.append(thread_id, event)
};
notify_direct_thread_subscribers(thread_id);
event
}
/// 接单:拒绝并发 + 登记占用 + 发逻辑回合开始事件(见 [`DirectThreadManager::accept_turn`])。
/// `Err` 是这一轮**已有的回合身份**(`turn_id`,也就是调用方的 `clientTurnId`),不是占用 token:
/// 调用方拿它投影成 `TurnAlreadyRunning` 的两个身份字段,界面按"撞的是同一轮还是另一轮"决定要
/// 不要动当前回合。
pub(crate) fn accept_direct_thread_turn(
thread_id: &str,
token: &str,
turn_id: &str,
user_item_id: Option<&str>,
started_at_ms: u64,
) -> Result<(), String> {
{
global_direct_thread_manager()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.accept_turn(thread_id, token, turn_id, user_item_id, started_at_ms)?;
}
notify_direct_thread_subscribers(thread_id);
Ok(())
}
/// 运行时回填某一轮逻辑回合的进度(状态 / 活动 / 序号)。返回是否真的写进去了。
pub(crate) fn update_direct_thread_active_turn(
thread_id: &str,
turn_id: &str,
status: &str,
activity: Option<&str>,
sequence: u64,
updated_at: u64,
) -> bool {
global_direct_thread_manager()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.update_active_turn(thread_id, turn_id, status, activity, sequence, updated_at)
}
/// 首页「运行中的项目」快照:逻辑回合的唯一导出口(见 [`DirectActiveTurnSnapshot`])。
pub(crate) fn list_direct_active_turns() -> Result<Vec<DirectActiveTurnSnapshot>, String> {
let mut turns = global_direct_thread_manager()
.lock()
.map_err(|_| "Direct 线程管理器已损坏".to_string())?
.active_turn_snapshots();
turns.sort_by(|left, right| left.project_path.cmp(&right.project_path));
Ok(turns)
}
/// 深层终态出口:解除占用并写 `turn.completed`。
pub(crate) fn complete_direct_thread_turn(thread_id: &str, event: DirectThreadEvent) {
{
global_direct_thread_manager()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.complete_turn(thread_id, event);
}
notify_direct_thread_subscribers(thread_id);
}
/// 占用对象的兜底出口:仍被同一 token 占用时才写,返回是否写了。
pub(crate) fn complete_direct_thread_turn_if_reserved(
thread_id: &str,
token: &str,
event: DirectThreadEvent,
) -> bool {
let written = {
global_direct_thread_manager()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.complete_turn_if_reserved(thread_id, token, event)
};
if written {
notify_direct_thread_subscribers(thread_id);
}
written
}
/// 这个 thread 是否还有没收口的逻辑回合。
pub(crate) fn direct_thread_turn_is_active(thread_id: &str) -> bool {
global_direct_thread_manager()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.turn_is_active(thread_id)
}
fn notify_direct_thread_subscribers(thread_id: &str) {
let subscriber_ids = {
let manager = global_direct_thread_manager()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let event = manager.append(thread_id, event);
let subscriber_ids = manager.subscriber_ids(thread_id);
(event, subscriber_ids)
manager.subscriber_ids(thread_id)
};
if let Some(app) = DIRECT_THREAD_MANAGER_APP_HANDLE.get() {
for subscription_id in subscriber_ids {
@@ -394,7 +662,6 @@ pub(crate) fn append_direct_thread_event(
);
}
}
event
}
pub(crate) fn subscribe_direct_thread(thread_id: &str) -> DirectThreadSubscriptionBootstrap {
@@ -603,6 +870,34 @@ mod tests {
));
}
/// 失败终态与正常终态同权:`turn.completed(status="failed")` 必须顶替更早的 `turn.started`
/// 成为锚点,否则队列被回收后新订阅只会看到 `turn.started`,把这轮已收口的回合重放成"还在跑"。
#[test]
fn failed_turn_completed_replaces_started_anchor() {
let mut manager = DirectThreadManager::with_limits(100, 100_000);
manager.append("thread-1", DirectThreadEvent::turn_started(1_000));
manager.append(
"thread-1",
DirectThreadEvent::turn_completed_failed(
crate::agent::DirectTurnFailure::new(
crate::agent::DirectTurnFailureKind::HostDropped,
"回合宿主任务提前结束",
),
FIXED_AT_MS,
),
);
let bootstrap = manager.subscribe("thread-1");
assert!(matches!(
bootstrap.events.as_slice(),
[DirectThreadEvent::TurnCompleted { status, failure, at, .. }]
if status == "failed"
&& failure.as_ref().is_some_and(|failure| failure.kind
== crate::agent::DirectTurnFailureKind::HostDropped)
&& *at == Some(FIXED_AT_MS)
));
}
/// 阶段时间必须随事件一起进队列:bootstrap 与重复订阅都拿到**原值**,
/// 重放不得重新取钟(否则每次重连都会把已固定的起止时间改掉)。
#[test]
@@ -723,4 +1018,79 @@ mod tests {
"unfinished item at queue head blocks middle cleanup"
);
}
/// 首页快照就是逻辑回合的导出:接单即出现、进度按序号回填、收口即消失。
fn snapshot_of(
manager: &DirectThreadManager,
thread_id: &str,
) -> Option<DirectActiveTurnSnapshot> {
manager
.active_turn_snapshots()
.into_iter()
.find(|turn| turn.project_path == thread_id)
}
#[test]
fn active_turn_snapshot_follows_the_logical_turn_lifecycle() {
let mut manager = DirectThreadManager::with_limits(100, 100_000);
let thread_id = "/tmp/快照项目";
assert!(snapshot_of(&manager, thread_id).is_none());
manager
.accept_turn(thread_id, "token-1", "turn-1", Some("u-1"), FIXED_AT_MS)
.expect("accept");
let accepted = snapshot_of(&manager, thread_id).expect("accepted turn is visible");
assert_eq!(accepted.turn_id, "turn-1");
assert_eq!(accepted.project_name.as_deref(), Some("快照项目"));
assert_eq!(accepted.started_at, FIXED_AT_MS);
assert_eq!(accepted.status, "accepted");
assert_eq!(accepted.activity.as_deref(), Some("request-accepted"));
assert_eq!(accepted.sequence, 0);
assert!(manager.update_active_turn(
thread_id,
"turn-1",
"streaming",
Some("file-write"),
3,
42,
));
let running = snapshot_of(&manager, thread_id).expect("running turn is visible");
assert_eq!(running.status, "streaming");
assert_eq!(running.activity.as_deref(), Some("file-write"));
assert_eq!(running.sequence, 3);
assert_eq!(running.updated_at, 42);
// 序号倒退与身份对不上的进度都不许改快照。
assert!(!manager.update_active_turn(thread_id, "turn-1", "failed", None, 2, 99));
assert!(!manager.update_active_turn(thread_id, "turn-2", "failed", None, 4, 99));
assert_eq!(
snapshot_of(&manager, thread_id)
.expect("snapshot unchanged")
.status,
"streaming"
);
manager.complete_turn(
thread_id,
DirectThreadEvent::turn_completed("completed".to_string(), 5_000),
);
assert!(snapshot_of(&manager, thread_id).is_none());
}
/// 并发接单回给调用方的是**回合身份**(`turn_id`),不是占用 `token`:token 只活在这个进程
/// 里,界面拿它匹配不了自己发出的那一轮。
#[test]
fn accept_conflict_returns_the_existing_turn_id() {
let mut manager = DirectThreadManager::with_limits(100, 100_000);
manager
.accept_turn("thread-1", "token-1", "turn-1", None, FIXED_AT_MS)
.expect("accept");
let conflict = manager
.accept_turn("thread-1", "token-2", "turn-2", None, FIXED_AT_MS)
.err();
assert_eq!(conflict.as_deref(), Some("turn-1"));
}
}
@@ -13,6 +13,7 @@
use crate::agent::redact_secret_tokens;
use crate::agent::sanitize_error_context;
use crate::agent::DirectTurnFailureKind;
use crate::redact_absolute_path_tokens;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -211,6 +212,30 @@ impl DirectThreadRequestKind {
}
}
/// 失败终态的可下发载荷(`turn.completed.status == "failed"` 时必有,其余终态没有)。
///
/// `kind` 是稳定分类,只给界面选语气,不参与流程分支;`message` 是**已在宿主侧脱敏并截断**的
/// 可展示原因——失败原因只走这一条通道,前端不再从命令返回或另一条 IPC 里另造文案。
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
pub(crate) struct DirectTurnFailure {
/// 稳定失败分类;取值表就是 [`DirectTurnFailureKind`],投影只走
/// [`DirectTurnError::wire_kind`]。
pub(crate) kind: DirectTurnFailureKind,
/// 脱敏 + 截断后的失败原因。
pub(crate) message: String,
}
impl DirectTurnFailure {
pub(crate) fn new(kind: DirectTurnFailureKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
}
}
}
/// Thread Manager 下发的运行态事件。
///
/// 顺序由数组顺序给出(同一个 subscriber 的 `consume` 按队列顺序返回),因此不需要 `seq`:
@@ -229,7 +254,8 @@ impl DirectThreadRequestKind {
/// 不能在前端收到或重放时重新取当前时间。
///
/// `turn.started` / `turn.completed` 额外带可选的 `userItemId`:本轮开口用户条目的 **canonical
/// itemId**(与同轮那条用户条目事件同源,由原生从已落盘条目上读取,不另造身份)。回合事件本身
/// itemId**(与同轮那条用户条目事件同源,由宿主按 `clientTurnId` 现算,`direct-codex:{clientTurnId}:user`;
/// **不读盘回填**——开始事件发生在用户条目落盘之前,落盘本身也可能失败)。回合事件本身
/// 不带回合身份,这个字段只用来把"这一轮的边界属于哪条用户消息"讲清楚:前端在只有生命周期锚点
/// + 历史切片、运行态一直为空时也能按身份认领开口条目,不必靠时间戳猜。缺失表示身份不可证明
/// (旧事件、没有开口用户条目、取消时拿不到 clientTurnId),此时前端不得补造。
@@ -239,7 +265,8 @@ impl DirectThreadRequestKind {
pub(crate) enum DirectThreadEvent {
#[serde(rename = "turn.started")]
TurnStarted {
/// 本轮开始的阶段时间(毫秒):宿主处理 `turn/start` 的毫秒钟。
/// 本轮开始的阶段时间(毫秒):**接单**那一刻的宿主毫秒钟(逻辑回合的起点,不是
/// `turn/start` 的时刻)。
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional, as = "Option<f64>")]
at: Option<u64>,
@@ -250,8 +277,14 @@ pub(crate) enum DirectThreadEvent {
},
#[serde(rename = "turn.completed")]
TurnCompleted {
/// 终态语义:`completed` / `interrupted` / `aborted` 是正常收场;`failed` 是**失败**,
/// 此时必须带 `failure` 载荷。
status: String,
/// 本轮终态的阶段时间(毫秒):宿主处理终态的毫秒钟,或 `durationMs` + 高精度起点的派生值。
/// 失败载荷:只有 `status == "failed"` 才有;失败原因只从这里下发一次。
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
failure: Option<DirectTurnFailure>,
/// 本轮终态的阶段时间(毫秒):宿主写下终态的毫秒钟,或 `durationMs` + 高精度起点的派生值。
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional, as = "Option<f64>")]
at: Option<u64>,
@@ -301,11 +334,30 @@ impl DirectThreadEvent {
pub(crate) fn turn_completed(status: String, at: u64) -> Self {
Self::TurnCompleted {
status,
failure: None,
at: Some(at),
user_item_id: None,
}
}
/// 失败终态:`status` 固定 `"failed"`,原因必须随事件一起带出去。
pub(crate) fn turn_completed_failed(failure: DirectTurnFailure, at: u64) -> Self {
Self::TurnCompleted {
status: "failed".to_string(),
failure: Some(failure),
at: Some(at),
user_item_id: None,
}
}
/// 失败载荷:只有失败终态有。
pub(crate) fn failure(&self) -> Option<&DirectTurnFailure> {
match self {
Self::TurnCompleted { failure, .. } => failure.as_ref(),
_ => None,
}
}
/// 附上本轮开口用户条目的 canonical itemId。
///
/// 只在构造之后补一次身份,避免 `turn.started` / `turn.completed` 的既有调用点(含各处兜底
@@ -317,8 +369,14 @@ impl DirectThreadEvent {
.map(str::to_string);
match self {
Self::TurnStarted { at, .. } => Self::TurnStarted { at, user_item_id },
Self::TurnCompleted { status, at, .. } => Self::TurnCompleted {
Self::TurnCompleted {
status,
failure,
at,
..
} => Self::TurnCompleted {
status,
failure,
at,
user_item_id,
},
@@ -1353,4 +1411,59 @@ mod tests {
);
assert_eq!(item_event.user_item_id(), None);
}
/// 失败终态:`status="failed"` 必须带 `failure{kind,message}`,正常终态不带;载荷跟着身份
/// 一起流转,缺载荷的 `failed` 事件仍能反序列化(前端按"没有原因"处理,不猜)。
#[test]
fn turn_completed_carries_failure_payload_only_when_failed() {
let failed = DirectThreadEvent::turn_completed_failed(
DirectTurnFailure::new(
crate::agent::DirectTurnFailureKind::ModelFailed,
"上游返回 500:模型服务暂不可用",
),
4_000,
)
.with_user_item_id(Some("direct-codex:turn-1:user"));
assert_eq!(
failed.failure(),
Some(&DirectTurnFailure::new(
crate::agent::DirectTurnFailureKind::ModelFailed,
"上游返回 500:模型服务暂不可用"
))
);
assert_eq!(failed.user_item_id(), Some("direct-codex:turn-1:user"));
assert_eq!(
serde_json::to_value(&failed).expect("serialize failed turn"),
json!({
"type": "turn.completed",
"status": "failed",
"failure": {"kind": "model-failed", "message": "上游返回 500:模型服务暂不可用"},
"at": 4_000u64,
"userItemId": "direct-codex:turn-1:user",
})
);
assert_eq!(
serde_json::from_value::<DirectThreadEvent>(
serde_json::to_value(&failed).expect("serialize")
)
.expect("round trip"),
failed
);
// 正常终态不带载荷,也不回写 `failure: null`。
let completed = DirectThreadEvent::turn_completed("completed".to_string(), 5_000);
assert_eq!(completed.failure(), None);
assert_eq!(
serde_json::to_value(&completed).expect("serialize completed turn"),
json!({"type": "turn.completed", "status": "completed", "at": 5_000u64})
);
// 精简 / 旧形状:`failed` 但没有载荷也要能反序列化。
let sparse: DirectThreadEvent = serde_json::from_value(json!({
"type": "turn.completed",
"status": "failed",
}))
.expect("failed turn without failure payload");
assert_eq!(sparse.failure(), None);
}
}

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