Merge branch 'master' into feat/external-scene-generation
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m31s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m56s
Project CI / Backend tests (pull_request) Successful in 3m53s
Project CI / Frontend tests (pull_request) Successful in 2m9s
Project CI / Native shell tests (pull_request) Successful in 5m58s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m6s
Project CI / Repository checks (pull_request) Successful in 1m54s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m58s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m32s

This commit is contained in:
2026-09-24 23:47:34 +08:00
195 changed files with 14753 additions and 7962 deletions
@@ -4,6 +4,8 @@
`apps/ai-game-creator-shell` 的 UI 编辑器以 `useUiEditorSession` 作为视图与 adapter 的唯一协调边界。会话持有资源加载、revision、预览请求、选择、节点可见性、步骤 gate、异步操作和保存意图;`UiDesignStateStore` 与 Tauri 调用仍由该模块注入,不进入视图组件。
实现层的深模块 seam 固定为:`features/ui-editor/stateTransition.ts`(React-free 语义 State transition)、`stateInvariants.ts`(保存前不变量 projection)、`nodeTransformGeometry.ts`(State 级节点几何)以及 `view/ui-editor/operationLifecycle.ts`(异步操作 adapter)。`useUiEditorState` 和 `useUiEditorSession` 只负责 React/history/lock 与平台 adapter 编排,不在视图中复制树遍历或几何反演。
页面与子视图不得再传递完整 controller。它们按职责读取以下小 projection:
- `input`:资源输入、节点树及调试操作。
@@ -19,6 +21,18 @@
保存与代码生成共享同一份持久化 State/revision。会话层在保存或生成进行期间互斥拦截,且代码生成必须基于已加载的持久化 revision;视图层的保存按钮和“保存并返回”按钮同步遵守该互斥状态。
Rust UI workflow 的结构化 LLM 动作共享 `commands::utils::required_tool_arguments` seam:它只负责必需 tool-call 定位与有界 JSON 解析;prompt、schema、领域校验和 materializer 继续留在 recognition/binding/merge 各自 command。
UI 编辑器的普通“保存”和“保存并生成代码”结果使用独立结果弹窗呈现,不在编辑器内容区追加状态条。普通保存成功仅提示保存成功;保存并生成成功展示生成器返回的项目相对路径(例如 `ui/generated-xxx.js`),复制按钮通过 Tauri clipboard manager 的 `writeText` 写入剪贴板。复制失败时保留可选中文本并提示手动复制;设置用户 fallback 后仍必须重新抛出原始错误,让 error report 链路收到未知 / 未处理的异常,不能把异常静默吞掉。此异常传播原则适用于所有 JS 边界,不限于生成路径或剪贴板。两类操作失败均在弹窗中展示;组合操作若保存成功但生成失败,明确提示项目已保存,重试动作仍复用原操作。保存并返回成功后直接返回,不打开结果弹窗;结果弹窗关闭后不保留路径状态。
## 2026-09-14 多树预览与树级偏移
UI 编辑器预览同时渲染 State 中全部 `ui_trees`。每棵树的 `root.offset` 包含 `min` / `max`;当前仅读取 `min` 作为树 wrapper 的左上角,`max` 由 `min + 对应界面图 pixel_size / pixels_per_unit` 派生。所有新树必须经 `createTree` 创建:首棵树位置为 `[0, 0]`,后续树按现有树实际右边界最大值加固定 padding 横向排列,并与现有树最小 top 对齐。删除或排序不重排已有树。
预览中原图与树 root 共享同一空间,root 空白区域可拖动整树,拖动结束一次性写回 root offset 并进入撤销/重做;root 不提供 resize,子节点沿用既有手势。预览选中节点不改变左侧图片面板的 `activeImageId`,Inspector 通过节点所属树反查编辑目标。只有首次进入预览和用户手动点击“适配画布”使用全部树联合边界进行 fit。
原有 render mode 已拆为三个会话级临时开关:`showFrame=true`、`showOriginImage=true`、`showComponent=false`。普通节点框线/名称受 `showFrame` 控制,选中节点强调始终保留;原图和组件显示互不耦合。
`UiDesignStateStore` 的 `generateCode(assetId)` 是必需能力,返回成功结果时不得为 nullable;所有注入的 adapter 与测试替身都必须实现该方法。
资源切换时,会话必须清理上一资源的保存/生成错误和生成中状态;普通保存开始时也清理代码生成错误。生成请求若因加载、锁定或 revision 等前置条件被拦截,必须向视图提供可见错误,而不是静默返回。
@@ -0,0 +1,146 @@
# DirectProject 命令接单化实施计划
更新时间:`2026-09-23`
状态:**四步全部落地**。
设计口径见 [`【ADR】DirectProject命令接单化-2026-09-23`](../adr/【ADR】DirectProject命令接单化-2026-09-23.md)。
本文件只排实施顺序、不变式与验收,不重复设计理由。
## 第 0 步:文档与既有缺陷清理(已落地)
- 设计定稿:ADR、`CONTEXT.md` 术语(逻辑回合 / 接单 / 拒单 / 在途回合)、两处旧文档的取代注。
- 前端删除由 invoke 拒绝驱动的认证重试(`directCodexSessionKeepalive.ts` 只留会话保活)。
- 用户可见文案不再带 `详情:` 引用、失败进错误上报池、失败说明不再写进项目历史,
只服务详情展开的 IPC `read_agent_runtime_error_detail` 已删除。
## 第 1 步:Thread Manager 拥有逻辑回合(Rust,一个原子提交)——已落地
改动点:
- 新模块 `agent/direct_turn_accept.rs`:按 thread 维护占用登记。`accept(thread, user_item_id, client_turn_id)`
在同一个临界区里完成"拒绝并发 + 登记占用 + 追加逻辑回合开始事件";`AcceptedTurn::finish(terminal)`
幂等写出 `turn.completed` 并解除占用;`Drop` 兜底补 `host-dropped` 终态。终态写出后占用才释放。
- `direct_thread_manager.rs`:登记与事件追加共用同一把锁(没有第二张静态表)。
- 删除了 `codex_app_server/mod.rs` 里镜像 Codex 原生回合的开始事件与终态追加,以及 app-server 侧
武装的 `DirectTurnFailureGuard`;终态统一交给 `AcceptedTurn::finish`。
- `direct_thread_wire.rs`:`userItemId` 的说明由"从已落盘条目读取"改成"由 `clientTurnId` 推导"。
不变式(已验证):线上仍只有一对生命周期事件;同一 thread 任意时刻至多一个占用;`turn.completed`
必带 `userItemId`。
## 第 2 步:命令改接单 + 后台跑整轮(Rust)——已落地
- 顺序固定为:`clientTurnId` 校验 → 占用调用身份 → 工作流恢复 → 用户条目校验 → 工程准备 →
`accept` → 落盘用户条目 → spawn 整轮。
- 接单前的检查从 `run_..._and_emitter` 上移到命令;分流判据改成位置(接单后一律回合失败),
`EnvironmentNotReady` 增加 `wire_kind() = "environment-not-ready"`,"调用级拒绝直通"的分支作废。
- spawn 出的任务在正常 / 失败 / 提前收场(早退:回合内任何没走到正常终态的收口点,如 `turn/start`
被拒、注入失败、panic)三条路径上都走 `AcceptedTurn::finish`;任务 panic 或被取消时由占用对象的
`Drop` 兜底。
- 落盘即接单:接单成功后落盘用户条目,再起 codex;落盘失败仍是接单后的回合失败(有回合事件解释)。
## 第 3 步:拒单返回 typed 错误(Rust + TS)——已落地
- `DirectTurnError` 加 `Serialize + TS`(含嵌套枚举)并导出到 `chat/generated/`;命令返回
`Result<(), DirectTurnError>`,文案仍由 `Display` 生成一次随载荷带出。
- 前端 catch 按变体分流(`readDirectTurnRejection` / `directTurnRejectionNotice`):认得的
前置 / 参数类 → 与用户消息同级的提示、不走 `captureAgentRuntimeError`;认不出的 → 抛出;
状态行只显示回合状态。
- 认可名单:`clientTurnIdMissing` / `clientTurnIdMalformed` / `turnAlreadyRunning` /
`projectRootUnanchored` / `projectRootUnusable` / `permissionRejected` / `inputRejected` /
`contentEmpty`;`environmentNotReady` / `hostStateUnavailable` 返回 `null`(抛出上报)。
- 拒单**不结算埋点**(埋点句柄只清不发)。
## 第 4 步:队列、埋点、快照、reducer(TS + Rust)——已落地
- 前端队列放行改听"回合完成或拒单":reducer 新增 `completedTurnCount`,作为放行与埋点结算的唯一
判据(不能用 `turnRunning` 的下降沿,一轮可能同批开始 + 结束)。**TODO(已写在代码里)**:这条
队列整体挪到 Rust 端,放行点就是 Thread Manager 的接单动作。
- 埋点结算挂到回合终态事件:句柄活过命令返回,接单成功才在终态结算,接单被拒不结算。
- 首页"运行中的项目"改由 TM 的逻辑回合导出(`list_direct_active_turns`);`DirectActiveTurnSnapshot`
移入 `direct_thread_manager.rs`,`DirectTaonierActiveInvocation` 退回纯单飞锁,不留两处事实。
- 删除取消占位的本地收口 `markTurnStopped()` 与 `turn.started` 的"重复起点保留第一次"兼容分支。
- 本地在途标签(`awaiting-start`)活到宿主认领,认领三判据:`turnUserItemId === pendingUserItemId`
(身份认领)、`currentTurnRunning`、`completedTurnCount > pendingTurnBaselineRef.current`。
## 验收证据
- Rust 定向:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bins "agent::"`
(949 passed);TM 单测覆盖并发接单被拒 / finish 幂等 / Drop 兜底 / 收口后可再次接单。
- 前端:`NODE_OPTIONS=--localstorage-file=/tmp/ls-gen.json npm test`(4473 passed)、
`npm run ai-game-creator-shell:typecheck`。
- 仓库门禁:`cargo fmt --check`、`npm run check:encoding`、`git diff --check`。
- 手工:连发两条确认第二条不被丢;重进页面忙碌态正确;真实客户端观感未复核。
## 已知坑
- `project.jsonl` 与项目主对话共用信封类型,不要为了"可见但不喂模型"新增行结构。
- 埋点 `settle` 早于成绩入库会静默丢事件(未来"进历史但不喂模型"的条目同理要落在注入侧,不在读取侧)。
- `cargo test export_bindings` 会重写全部 `chat/generated/`(引号风格漂移),跑完要 `git checkout --`
掉不是本次新增的文件。
- 本机 rust 全量 `--bins` 测试会挂在 mock server 的 `inet_csk_accept` 上,用 `--bins "agent::"` 之类过滤跑。
## review 收口第二轮(2026-09-24)
第 3 步的拒单表与第 4 步的界面口径按 review 收口后的状态为准:
- 失败载荷的 `kind` 从裸 `string` 收成 typed `DirectTurnFailureKind`(7 个变体,含先前漏登记的
`turn-interrupted`);线上形状与取值不变,TS 侧只是变成可穷尽收窄的联合类型。
- 并发拒单(`TurnAlreadyRunning`)的两个身份改成回合身份:`existingInvocationId` 是占用对象的
`turnId`、`incomingInvocationId` 是这一轮请求的 `clientTurnId`;占用对象自己的 `token` 仍是 UUID。
- 可留痕的拒单只剩 `environmentNotReady` / `hostStateUnavailable`:`projectRootUnanchored` 归到
"用户自己就能修"那一档,不再写诊断、界面按 `Display` 显示。
- 聊天里的提示分两条通道:认得的拒单给 `Display` 原文;认不出的拒单(宿主 / 环境事实)除上报 + 横幅
外也补一条同级提示,文案取宿主收口文案里的脱敏摘要与建议(不带阶段标签)。失败说明的文案映射
口径见 `docs/project-memory/shared-memory/decision-log.md` 与 `conversation/directTurnFailure.ts`。
- 第 1 步的命令返回值只剩"接单 / 拒单"两种含义:接单成立之后的一切失败(含接单后的历史落盘失败)由
占用对象收口成 `turn.completed`,命令一律返回 `Ok(())`;落盘失败**不继续起整轮**。
- 第 2 步的"谁先到谁写"加一条前提:连接死亡的**失败事实必须先于看门狗可见**
(`CodexAppServerInner::closed` 不再兼作去重标志,去重改用私有的 `connection_end_claimed`,
`closed` 在 `record_execution_turn_failure` 之后才置位);回归用例
`connection_death_records_the_failure_fact_before_the_watchdog_seals_the_turn` 把看门狗真正跑起来钉这条。
## 回合顺序修复(2026-09-24)
现场:用户在同一个项目里连发几条消息,每条都在 `turn/start` 之前失败(执行器版本未通过验收),
界面上"错误显示在用户消息上面",上一轮还显示出本轮的耗时(15.6 秒),本轮气泡自成一轮显示 0.0 秒;
后面再发一条,失败说明落进更早的分区里,用户以为"这条没报错"。
根因是**一条顺序**:本轮的开口用户条目原来在 `turn/start` 应答之后才下发,而失败说明按"当前回合"
归位(前端按条目顺序分回合),于是接单后、`turn/start` 前的失败没有用户条目可挂。
- 宿主:用户条目改成"落盘成功、起 codex 之前"下发(`emit_direct_thread_user_item`),删掉 `turn/start`
之后那一次;不变式:`接单 → 开口用户条目 → 整轮里其余一切`。
- 前端:失败说明带 `turnUserItemId`,`buildDirectChatTurns` 按身份分组(同一身份的条目永远同一轮),
本地乐观气泡按身份挂回自己的回合而不是另开一轮;收口早退只挡重复终态,不再吞掉还没写进界面的失败说明。
- 回归用例:宿主 `the_opening_user_item_is_emitted_before_anything_that_can_fail_in_the_turn`、
`direct_project_turn_does_not_forward_codex_user_echo_as_chat_items`(补上同一发点);
前端 `本轮用户条目没到时,失败说明按身份挂回自己那一轮,本地气泡不再自成假回合`、
`失败说明带上它所属回合的身份,用户条目没到时投影层也能归位`、
`收口早退不吞掉还没写进界面的失败说明(订阅重建只回放生命周期锚点)`。
- 已知边界:`project.jsonl` 里的用户条目依旧只在首屏 / 翻页时读进前端,本次不改这条读取时机——
开口条目的运行态下发与身份归位已经让"说明挂错回合"不再成立。
## 删掉本地乐观用户气泡(2026-09-24)
上一节的"按身份归位"落地后,本地乐观气泡只剩一个作用:把"接单窗口期"变成一种展示态
(`awaiting-start`),并给投影多带一份与宿主条目同身份的本地用户消息。用户确认按"这条消息就像从来
没存在过"处理,于是整套删掉。
- controller:删 `pendingUserItemId`、`beginTurnCommand` / `endTurnCommand`;忙态保留(改叫
`beginTurnBusy` / `endTurnBusy`),宿主认领判据 = `turnRunning` 或收口计数变过(一轮在同一次
consume 里开始并结束)。同时删掉 `startTurn` 的乐观追加、权限确认重跑的 `messageAppended` 参数、
`DirectProjectTurnInput.messageText` 与首轮的 `directInitialTurnText`;controller 不再需要 `assets`。
- 投影 / 渲染:`DirectChatTurnState` 只剩 `running` / `finished`;删 `localSentTimes` /
`sameIdentitySentAt`、本地用户气泡与它开回合的那条路径。本地说明保留:带身份的拒单提示在会话末尾
自成一组(不挂上一轮,也不造耗时文案),不带头身份的壳层 `announce` 照旧挂当前回合末尾。
- 时间口径:起点只认 `turn.started.at`(运行中读实时值、收口后读盖在条目上的值),终点只认
`turn.completed.at`;用户气泡的时钟就是宿主落盘 / 观测时间。历史回合两边都是 0 → 整条
「本轮结束于 … 」隐藏,不再出现 0.0 秒。
- 回归用例:`directTurnPresentation.test.ts`(本地用户消息不进回合、带身份的本地说明自成一组、
两态判据、失败说明按身份归位)、`directProjectTurn.test.tsx`(`running` 不显示终态文案;无边界的
历史回合整条隐藏)、`directProjectTurnStatus.test.ts`、`appSurface` 的
`keeps the accept window silent in the chat and busy in the composer`。
- 已知边界:条目下发之前(接单窗口、订阅重建窗口)聊天区里没有这一轮的任何显示,只有 composer 忙态、
状态行与「陶泥儿正在处理」卡片(卡片这一段不读秒:起点要等宿主的 `turn.started.at` 到);
`project.jsonl` 里的用户条目依旧只在首屏 / 翻页时读进前端。
@@ -1,5 +1,7 @@
# AGC 后台模型别名与对话选择
更新时间:`2026-09-24`。本次只改“目录初始值从哪来”:缺配置时不再回退写死的 `高质量 → gpt-6-astra`、`快速 → gpt-5.6-luna`,改为启动期从上游同步(这两条初始目录里的模型已从上游移除)。目录结构、后台维护字段和客户端契约都保持不变。
## 本地自定义 LLM
- 本地 `game-creator.config.json` 的 `llm.customEnabled` 默认 `false`;显式设为 `true` 后,常用设置展示 API 地址、API Key、读取模型列表与勾选区域。DirectProject 沿用 OpenAI Responses 协议,地址填写 API 根地址(例如 `https://provider.example/v1`)。开关只由配置文件控制。
@@ -33,7 +35,11 @@
## 官方路由契约
- 后台 owner 在“AGC 模型”维护列表;每项包含稳定 `id`、必填 `alias`、服务端 `modelId`、`enabled`。默认项必须启用。标识唯一,别名唯一,列表最多 32 项。
- 配置保存到私有 `agc_model_catalog` 单例表,使用 revision 乐观锁,重启及多 api-server 实例共享同一事实。缺少配置时使用初始目录,高质量对应 `gpt-6-astra`,快速对应 `gpt-5.6-luna`。
- 配置保存到私有 `agc_model_catalog` 单例表,使用 revision 乐观锁,重启及多 api-server 实例共享同一事实。
- 目录初始值来自上游同步:api-server(API/All 角色)启动时检查目录,缺失、结构与当前定义不符或校验不通过都算“未初始化”;此时调用上游 Router 控制面的分组定价列表 `GET {Router 控制面}/api/pricing?group=taonier`(控制面地址由 `{LLM Router 地址}` 去掉 `/v1` 得到;公开只读接口,不带凭据),读取 `data[].model_name` 作为“该分组可见的在售模型”,按模型名排序后生成目录:每项 `modelId` 与 `alias` 都用上游原始模型名(不再填“高质量/快速”这类人工别名),`id` 是模型名的稳定 slug(小写字母、数字、`-`、`_`,同名冲突追加 `-2`),`enabled = true`,默认项取排序后第一项,并以存量 revision 写回(`revision` 自增)。不使用管理面模型注册表 `/api/models/`——它会残留已下线、没有路由绑定的条目;也不使用 `/v1/models`——它要求 Router 用户 Key,平台没有服务级 Key。并发启动的多个实例里只有一个写入成功,其余接受既有目录。
- 同步失败(网络、非 2xx、空列表、响应超过 1 MiB、缺少目录行 revision、写回失败)只记录 error 日志,不写任何替代目录、不使用任何内置模型名;本次启动保持未初始化,下一次启动继续重试,直到目录里有数据。
- 目录未初始化时 `GET /api/llm/models`、`/api/llm/responses`、`/api/llm/chat/completions` 与后台 `GET/PUT /admin/api/agc-models` 一律失败关闭(`503`),错误文案指向“模型目录未初始化”。恢复路径是修好上游可达性后重启 api-server,或由运维清空 `agc_model_catalog` 该行后再重启。
- 上游变化不自动跟随:目录只在未初始化时重建;上游新增或移除模型由 owner 在后台增删条目或调整启用、默认项。
- `GET/PUT /admin/api/agc-models` 仅 owner 可用,返回完整配置;PUT 携带上次读取的 revision,冲突拒绝覆盖。
- `GET /api/llm/models` 返回启用项的 `id/displayName`、`defaultModelId` 和目录 `revision`,不返回实际模型名、Router 目录、凭据或能力原始数据。
- 客户端缓存最近 `revision`,在项目切换 / 对话表面挂载 / 下拉展开 / 窗口聚焦时条件刷新:`revision` 未变化不更新界面,同一时刻只保留一个在途请求,刷新失败保留上一次有效目录与本地选择。发起对话前用同一份快照校验所选模型仍启用,已停用或删除则回退默认模型并提示。
@@ -48,6 +54,9 @@
## 验收
- 空目录 + 上游可达:启动后目录自动生成(`id` 为模型名 slug、`alias`/`modelId` 为上游原名、`enabled` 全为真、默认项为排序后第一项),`revision` 自增一次,`GET /api/llm/models` 的 `displayName` 就是上游原名,界面不出现任何内置模型名。
- 空目录 + 上游不可达/空列表/非 2xx:启动只记录 error,不生成替代目录;AGC 接口与后台目录接口返回 `503`“模型目录未初始化”;下游可恢复后重启即同步成功(不需要人工造目录)。
- 同一模型集合重复同步结果一致(上游返回顺序不影响目录与默认项)。
- 目录领域校验、未知/停用模型拒绝、客户端响应不包含实际模型名。
- 后台鉴权、持久化 revision 冲突处理;客户端选择保存后重新读取,设置保存不覆盖选择。
- 目录 `revision` 条件刷新与并发触发去重、发送前回退默认模型、刷新失败可恢复。
@@ -1,5 +1,19 @@
# AI 游戏创作智能体 App 实施计划
## 2026-09-23 UI 编辑器退役界面图参考语义建议
本节覆盖下文“2026-08-18 UI Editor 从属页面、手势与保存失败边界”中的 `UIDesignImage.metadata.slave_to` 口径,以及“界面语义建议”相关描述。
UI 编辑器的“分析参考图”步骤、Rust 命令 `suggest_ui_design_semantic` 与 `UIDesignImage` 的 `metadata`(`name` / `description` / `role` / `slave_to`)整体退役,不保留兼容字段、回退路径或旧文档迁移:界面图只剩 `path`、`pixel_size`、`pixels_per_unit`;界面图之间不再存在持久化归属关系,结构识别按“每张界面图各自一棵树”执行;界面图在 UI 上的显示名统一取 `path` basename(`view/project-development/resourceAssetDisplayName.ts`),没有可选主页面过滤器、角色选择器和归属选择器。
多树合并当前不产生有效优先级:`merge` 的所有输入树优先级恒为 0(代码内留 `TODO`,等待重新设计),因此合并冲突时的代表节点取 `merged_from` 首位成员。旧的 `ui_design.json` 里残留的 `metadata` 字段由 serde 默认忽略,读取后不再写回;不新增拒绝或迁移逻辑。
| 要求 | 必须成立的行为 | 完成证据 |
| --- | --- | --- |
| 权威层收敛 | `resource/ui_design_image.rs` 只保留 `path` / `pixel_size` / `pixels_per_unit`;`commands/ui_design_suggestion.rs` 与 `suggest_ui_design_semantic` 注册删除;持久化校验不再有 `slave_to` 引用与环校验 | `cargo check`、`cargo test --bin genarrative-ai-game-creator-shell ui_editor`(160 passed) |
| 前端两步工作流 | `model.ts` 只保留“识别界面结构”“自动切分素材”;`ToolNavigation` 渲染两格;suggestion 操作、结果通知分支、role/slave_to 编辑器与 `ImportOverview`(原“分析参考图”步骤概览)全部删除 | `npx vitest run uiEditorPage/uiEditorState/uiDesignStateStore/previewWorkspaceZoom/appSurface` |
| 无迁移 | 旧文档中的 `metadata` 被静默忽略并在下次保存时消失,`ui_trees` 不受影响;不做迁移脚本或写入回填 | `persistence.rs` 既有加载/保存用例 |
## 当前策划入口与退役边界
策划 V1、策划会话 Runtime V2 均已删除,当前“做方案”只使用独立 Design Agent,现行合同见[策划 Agent 生产迁移与工作区浏览](./【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。旧 V1/V2 Runtime、命令、会话、审批卡、身份白名单和专属测试不作为兼容或恢复目标;历史方案中的 lifecycle v3、planning binding 等要求不能作为孤立代码的保留依据。共享能力按现役调用判断,不因名称相似删除当前 Design Agent 或通用 Runtime。
@@ -1739,3 +1753,10 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面
`AgentRuntimeErrorEvent` 把失败投影到用户消息、运行面板和项目内 `.agent/runtime/errors/<eventId>.json` 时,同一份已脱敏诊断还要投影成 AppData `diagnostics/application.log` 的两行:`agent.runtime.error`(身份行:`eventId / source / stage / code / retryable / clientTurnId / elapsedMs / detailRef`)与 `agent.runtime.error.detail`(详情行:`hint / summary / detail / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;`summary` 按 320 字符、`detail` 与 `metadata` 按(1200 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(summary / hint / detail)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
## 2026-09-23 AGC UI 设计文档 Agent 工具化重写
- `ui.workflow.run` 单工具(`discover → prepare → recognize → merge → binding → status → finalize`)整体退役:它的参数就是工作流状态,任一步失败只能整轮重来,步骤产物又由前端会话落盘,Agent 侧没有任何恢复点。同时退役 `merge.rs`、`binding.rs`、`workflow.rs`、`ensure_ui_design_resource_for_prototype`(原型 → 文档的幂等查找)、`ui/ui-workflow-<sha256前24>.json` 命名与 `ui-workflow.*` manifest 阶段,均不保留兼容、迁移或 fallback。
- 当前只保留三个工具:`ui-design-doc.from-images`(一至四张设计图 → 新建并登记文档,返回 `assetId` 与 `relativePath`,无状态)、`ui-design-doc.run-workflow`(`recognize → separate → write-back`,唯一带崩溃恢复的工具)、`ui-design-doc.into-js`(渲染 `ui/generated-<stem>-<digest>.js`,无状态、不推进 revision)。项目根目录、项目 ID 与 provider 身份由 Runtime 注入,模型只给设计图引用或文档 `assetId`。
- `run-workflow` 的恢复判据只有一条:这一步有没有对应、且带着 State 快照的检查点行。检查点是文档旁追加式 JSONL(`ui/.<文档名>-workflow.jsonl`),行类型为 `run` / `recognize` / `separate` / `write-back` / `outdated`,`run` 行带本轮起始 State,`recognize` / `separate` 行带该步应用完之后的 State。恢复只读快照、整步跳过已完成步骤,只把新完成那一步的改动应用到文档,绝不照 DTO 重放(重放会重复登记切图、重复累加回填出错说明);只有 DTO 的旧行按未完成重跑。一轮以 `run` 开头、以首个 `write-back` 或 `outdated` 结束;只有最后一轮没有结束行时才恢复。追加前先截断崩溃留下的半行,文档中途漂移(当前 State 既不是 `run` 行快照、也不是切分后那份快照)时追加 `outdated` 并返回错误,由下一次调用显式开新一轮,不在同一次调用里自动重启。切图资源失败不回滚,靠 manifest 的 by-path 复用接上;切分 op 内部更细粒度的恢复仍由 `SeparationState` sidecar 承担,检查点日志不复制它的进度。
- 三个工具的描述与参数文案都在 `prompts/runtime/texts/ui-design-doc.json`(目录 ID `uiDesignDoc`),Rust 侧不得硬编码面向模型的长文案。策划 `design-foundation` 的自主构建白名单同步登记这三个工具,命令映射复用 `asset.register` / `file.write`。
@@ -1,6 +1,12 @@
# DirectProject Codex 原始历史与异常恢复
更新时间:`2026-09-16`
更新时间:`2026-09-23`
> 注:本文件里"事件不带回合身份"、"`turn.started` 之前的早退不产生终态事件"这两条结论已被
> [`【ADR】DirectProject命令接单化-2026-09-23`](../adr/【ADR】DirectProject命令接单化-2026-09-23.md)
> 取代并落地:生命周期事件带可选的 `userItemId`,逻辑回合在**接单**时成对发出,接单之前的失败一律
> 是拒单(不产生回合事件)。下文相关段落已按该 ADR 修订;"失败说明不写进 `project.jsonl`"仍是
> 当前口径。
## 目标
@@ -27,13 +33,13 @@ DirectProject 自己的写侧只写新格式:格式切换(#282)时仍会
## 正常回合
1. 启动 `ephemeral: true` 线程,并启用 `experimentalRawEvents: true`。
2. 新线程先把历史 item 数组逐项投影为 Codex 可接受 item 后一次注入;注入成功后执行新的 `turn/start`。本轮 canonical user item 在发送前完成同样的投影校验,再写入项目历史。
2. 命令**接单**后先写本轮 canonical user item(发送前完成同样的投影校验),再在后台起 codex;新线程把历史 item 数组逐项投影为 Codex 可接受 item 后一次注入,注入成功后执行新的 `turn/start`。这一轮的逻辑回合在接单那一刻就已开始,落盘与注入、`turn/start` 都在回合内,失败由这一轮的终态事件解释(见「异常回合收尾」)。
3. 收到 `rawResponseItem/completed` 后立即追加其 `params.item` 并 flush。
4. 正常 `turn/completed: completed` 不生成额外记录。
## 异常回合收尾
AGC 判定本轮不会再产生新事件时收尾:用户中断、turn failed、无响应/idle timeout、硬超时、transport closed、stdout EOF 或 app-server 卡死终止均属于异常终态;正常 completed 不收尾。
AGC 判定本轮不会再产生新事件时收尾:用户中断、turn failed、无响应/idle timeout、硬超时、transport closed、stdout EOF 或 app-server 卡死终止均属于异常终态;正常 completed 不收尾。终态出口只有接单时登记的占用对象一个:正常 / 失败 / 中断 / 取消谁先算出来谁写 `turn.completed`,都写不出时由它的 `Drop` 补 `host-dropped`。
`item/agentMessage/delta` 正常带有 `itemId`;若协议异常缺失,AGC 记录 warning 并按当前 turn 生成稳定回退 id。AGC 在内存中按该 id 累计 assistant 文本,不实时写 delta。异常终态时,对仍有累计文本的 item 合成普通 Responses assistant `message` item:
@@ -55,7 +61,7 @@ Codex 启动时注入的 `host_skills.instructions`、`permissions.instructions`
聊天界面只从 message item 提取 user/assistant 内容;工具 item 不再拼成 `tool: ...` 假文本。
DirectProject 的浏览器层只负责显示和乐观状态,不再调用通用对话写入器。历史读写与回合累计分别位于 `agent/direct_project_history.rs` 和 `agent/direct_project_turn_history.rs`。
DirectProject 的浏览器层只负责显示与本地忙态,不再调用通用对话写入器,也不再造用户消息(本地乐观气泡已删,见 [`【ADR】DirectProject命令接单化-2026-09-23`](../adr/【ADR】DirectProject命令接单化-2026-09-23.md) 的后续更新)。历史读写与回合累计分别位于 `agent/direct_project_history.rs` 和 `agent/direct_project_turn_history.rs`。
`project.jsonl` 的 DirectProject 现行合同只允许 `response_item` envelope。其它模式产生的旧 conversation 行不属于本合同,不得注入 DirectProject。
@@ -112,8 +118,8 @@ Thread 内所有公开事件共用一个单调递增 seq,但 **seq 只是 Thre
```ts
type DirectThreadEvent =
| { type: 'turn.started' }
| { type: 'turn.completed'; status: string }
| { type: 'turn.started'; at?: number; userItemId?: string }
| { type: 'turn.completed'; status: string; at?: number; userItemId?: string; failure?: { kind: string; message: string } }
| { type: 'item.started'; item: DirectThreadItem }
| { type: 'item.completed'; item: DirectThreadItem }
| { type: 'item.delta'; itemId: string; kind: 'message' | 'reasoning'; delta: string }
@@ -122,12 +128,42 @@ type DirectThreadEvent =
进入 Thread Manager 的是已经完成安全过滤和协议标准化的公开 raw event,不是未经审查的 app-server JSON。事件可交错包含多个并发 item:`item.started`、`item.delta`、`item.completed`、approval/request/resolved 事件,以及 `turn.started`、`turn.completed` 生命周期事件。前端按事件顺序 reduce,只用一个 reducer。
**事件不带回合身份。** DirectProject 同一时刻只有一个回合在跑,`turn.started` 无载荷、`turn.completed` 只带 `status`;条目、增量、请求与生命周期锚点都不带 turn id。前端 state 里只有一个 `turnRunning` 布尔,历史条目也不记录回合身份。
**回合身份只挂在生命周期事件上,且由 `clientTurnId` 现算。** DirectProject 同一时刻只有一个回合在跑;
`turn.started` / `turn.completed` 各带一个可选的 `userItemId`(本轮开口用户条目的 canonical id,
`direct-codex:{clientTurnId}:user`),`turn.completed` 另外带 `status` 与失败时必有的 `failure`。
条目、增量、请求与生命周期锚点仍不带 turn id:这个字段只把"这一轮的边界属于哪条用户消息"讲清楚,
不新增一套回合身份,**不读盘回填**(开始事件发生在用户条目落盘之前,落盘本身也可能失败)。前端 state
里的 `turnRunning` 仍是唯一的活动判定,历史条目不记录回合身份;身份缺失时不猜历史归属。
**终态只有 `turn.completed` 一种,失败靠 `failure` 载荷区分。** `status !== "failed"` 表示正常结束 / 中断 / 终止,事件不带 `failure`;`status === "failed"` 是失败终态,**必须**带 `failure { kind, message }`:`kind` 是稳定分类(`timeout` / `model-failed` / `transport-failed` / `request-rejected` / `environment-not-ready` / `host-dropped`,只给界面选语气,界面不拿它做流程分支),`message` 是脱敏截断后的失败原因。失败原因只走这一条通道——前端不从命令返回或另一条 IPC 里另造失败文案;`status="failed"` 却没有载荷视为协议违规。
**接单之前发生的不是回合失败,是拒单。** 判据是**发生位置**而不是错误种类:目录、权限、输入、
并发、工程准备未就绪这类"接单前就能判定"的失败由命令以结构化的 `DirectTurnError`(ts-rs 导出,
载荷 = 变体 + `Display` 生成的一句文案)返回,不产生任何回合事件、不写用户条目、不写失败诊断;
接单之后的连接、配置、历史注入、`turn/start` 被拒以及回合过程中的一切,都只走 `turn.completed`
带失败载荷这一条通道。`EnvironmentNotReady` 接单前后都可能出现,因此它有自己的失败分类
(`environment-not-ready`),不会被投影成 `model-failed`。
执行通道断开(app-server 进程退出、stdout 流断、回合事件通道关闭)也走同一条终态:`kind="transport-failed"`,`message` 是宿主当场记下的诊断(`exitStatus` + stderr 摘要,脱敏截断)。宿主在检测到连接终止时**第一时间**把这条事实记到本回合的执行适配器上,终态判定再从适配器读——执行适配器的看门狗盯着同一个 `closed` 标志,若只在调用点用局部变量记录,会与看门狗的收束竞争,输掉时就只剩 `status="interrupted"` 加一句收尾说明,界面只显示"本轮已结束"、看不到原因。判据是"适配器是否已由宿主主动关闭":宿主自己收束(正常终态 / 用户主动停止 / 预算与交付收尾)同样会发 `TransportClosed`,但那些不算失败。
宿主的异常收场同样靠这条事件:**接单**时登记占用对象并发出 `turn.started`,占用对象持有这一轮唯一的
终态出口——正常 / 失败 / 中断 / 取消谁先算出来谁写终态,都写不出时由它的 `Drop` 补一条
`status="failed"` + `failure.kind="host-dropped"`,因此"接单成功 ⇔ 事件流里有开始且有结束"是结构性
成立的,不依赖实现者记得给每条"接单后提前收场"(早退:回合内任何没走到正常终态的收口点,比如
`turn/start` 被拒、注入失败、panic)的路径补事件。唯一的已知边界是宿主进程被强杀(`kill -9`):没有任何
`Drop` 执行,队列随进程消失,新进程的订阅 bootstrap 因此不会看到"有开始没结束",界面不会卡在忙碌态。
接单**之前**的失败根本不产生回合(见上一条:那是拒单),所以不存在"没有事件可解释的回合"。
**终态由事实判定,不由收尾阶段反推。** `turn.completed.status` 不是收尾阶段的口径(`lifecycle_status` 只描述 ledger 阶段,没有终态否决权):判定按「宿主当场记下的失败(通道断开 / 等待超时 / app-server 单方面中断)→ 本回合的错误结果是 Err → 只有账本读不出来时才用交付报告」取原因,有载荷一定写 `status="failed"`。模型自报失败(原生 `turn/completed` 的 `error`,含 `codexErrorInfo`)复用同一条通道:宿主把它投影成 `LlmError` 后当作本回合的错误结果返回,原因文本里带着 `codex-app-server-error:<kind>` 前缀(前端 `projectRuntimeVisibleError` 已有对应中文映射),既不为载荷新增输入字段,也不让交付报告顶掉原因。`RepairRequired`(封口复核要求继续当前返修批次)**不是失败**:它是控制流,有独立的 typed 变体(宿主侧 `DirectTurnRunFailure::RepairRequired`,跨界后是 `DirectTurnError::RepairRequired`),不写终态、不进失败载荷、不上报,由返修循环把它写回提示词继续跑;伪装成 `LlmError` 会让"继续返修"被讲成一次用户可见的失败,还会让同一个逻辑回合写出第二条终态。
**终态的写点在整轮真正结束之后。** 执行结果收集(含执行器收尾)、历史落盘、structured output 解析都定型了才写 `turn.completed`,成功与失败共用这一个写点:解析失败也是这一轮的失败,必须落进同一份失败载荷。反过来(先写终态、再解析)会让"终态写完又失败"的回合在协议上无解——终态已经是 `completed`,占用对象的兜底变成空操作,用户看到的是"本轮结束、没有回复、没有任何解释"。
一个 thread 同时最多有一个 active turn;一个 turn 内允许多个并发 item。`turn.completed` 必须在该 turn 的完成 item 均成功持久化后进入队列,前端据此结束运行态;不能用“不存在 unfinished item”猜测 turn 是否完成。
前端 reducer 的活动回合判定只有一条:事件序列中出现 `turn.started` 且其后没有 `turn.completed` 时才是活动回合,界面才允许显示忙碌态。`subscribe` bootstrap 里没有这样的序列,就表示当前没有活动回合;Thread Manager 队列随进程消失,因此进程重启后历史里留下的半截回合一律按已结束渲染,前端不发明中断态,也不从历史条目反推忙碌态。
失败终态与正常终态同权:`turn.completed`(无论 `status`)都顶替更早的 `turn.started` 成为队列锚点,重放时新订阅既不会把已收口的回合看成"还在跑",也不会看到已经过期的失败原因。
生命周期锚点独立于 replay 队列保存:`turn.started` / `turn.completed` 事件即使已被队列前缀回收,`subscribe` 仍必须把最新的一条作为 bootstrap 事件返回。因此进程内任意时刻新建订阅,都能判定最新回合是运行中还是已结束,不依赖"未完成 item 恰好还在队列里"。
`item.started` 与 `item.completed` 必须携带与历史切片同形的**脱敏原始条目**(经同一套挑字段、脱敏、截断、路径归一),不得只给 item 类型或空 payload。前端不得依赖"按 `itemId` 单点取快照"补齐正文:Rust 不提供 `getItemSnapshot(itemId)`,未完成条目的正文随事件下发,已完成条目一律通过历史读取。
@@ -0,0 +1,111 @@
# UI 编辑器 Agent 工具化重写
更新时间:`2026-09-23`
一句话定位:把 UI 编辑器今天由前端会话编排、由单个 `ui.workflow.run` 驱动的 Agent 链路,重写成三个各自只做一件事的工具——建文档、跑工作流、出 JS——且只有工作流工具带逐步骤崩溃恢复。
## 现状与问题
- `ui.workflow.run`(已退役)把发现页面、桥接设计图、结构识别、多树合并、组件绑定、回读、finalize 全塞进一个工具,工具参数本身就是工作流状态:模型不能只做其中一步,任何一步失败都只能整轮重来。
- 步骤产物由前端 `useUiEditorPage.ts` 落 State 再保存,Agent 侧没有任何恢复点。
- `merge` / `binding` 两条实验链路无现役价值,已随工具一起退役(`ui_editor/commands/merge.rs`、`binding.rs`、`ui_editor/workflow.rs` 已删除)。
## 工具契约
| 工具 | 输入 | 输出 | 恢复 |
| --- | --- | --- | --- |
| `ui-design-doc.from-images` | 1–4 张设计图,每张给 manifest `assetId` 或项目内相对路径 | 新建文档的 `assetId` 与 `relativePath` | 无状态 |
| `ui-design-doc.run-workflow` | 文档 `assetId` | 各步骤摘要与文档新 `revision` | 逐步骤 JSONL 检查点 |
| `ui-design-doc.into-js` | 文档 `assetId` | `ui/generated-<stem>-<digest>.js` 路径与导出树 | 无状态 |
工具名用 `<域>.<动作>` 形式,域内动作保留连字符(`from-images`、`run-workflow`、`into-js`);调用这些工具时项目根目录仍由 Runtime 注入,模型不得传入宿主路径。
### 建文档:`ui-design-doc.from-images`
- 文档内设计图 id 直接采用该图在 manifest 里的 `assetId`;输入给相对路径时先登记成资源再用它的 `assetId`,不额外发明文档内身份。
- 文档文件名沿用 `ui/UI 设计 N.json` 取号,不登记半成品命名(旧 `ui/ui-workflow-<sha256前24>.json` 口径废弃)。
- 每次调用都新建一份文档并登记:不做「原型 → 已存在文档」的幂等查找,原 `ensure_ui_design_resource_for_prototype` 的复用分支随之删除。
- 建项与登记在项目写锁内完成;写盘失败要回滚已登记的 manifest 条目,不留半成品。
### 工作流:`ui-design-doc.run-workflow`
只做三步,全部在 Rust 内完成,结果不回传前端编排:
| 步骤 | 实现 | 产物 |
| --- | --- | --- |
| `recognize` | `recognize_ui_impl_with_provider` | `ui_trees`(每棵树的 `src_ui_design` 指向文档内设计图) |
| `separate` | `separate_ui_impl` | 切分图落盘 → 登记 asset → 转 `SpriteAsset` → 写入 State → 回填 `target_graphic`、清 `component_status`、写 `NeedReview` |
| `write-back` | `save_ui_design_state_at` | 文档新 `revision`;没有回填问题且没有问题节点时再 `finalize_separation` 清理 sidecar |
不再有合并、组件绑定与页面级 profile/finalize 阶段;`recognize` 之前不做任何前置发现。
`NeedReview` 与 `component_status` 的清理是 `separation` DTO 的一部分:问题节点由 DTO 的
`problematic_nodes` 给出,编排阶段统一回写,重放时不需要模型再判一次;文案与判据镜像
前端 `features/ui-editor/separationStatus.ts`。
## 崩溃恢复
检查点是文档旁一条追加式 JSONL 日志,方案与被否方案见 [ADR:UI 工作流检查点用追加式 JSONL 日志](../adr/【ADR】UI工作流检查点用追加式JSONL日志-2026-09-23.md)。
- 位置:`ui/.<文档文件名去扩展名>-workflow.jsonl`,与文档同级,不进 manifest、不推进项目 revision。
- 行格式:`run`(原始 State 快照 + 起始 revision)、`recognize`(DTO + 该步应用完的 State 快照)、`separate`(DTO + 该步应用完的 State 快照 + 回填说明)、`write-back`(新 revision)、`outdated`(本轮作废原因);行内另带 `at` 时间戳。
- 判据只有一条:「本步有没有对应的行」。每行必须一次性原子追加,崩溃留下的半行一律视为该步未完成。
- 恢复只看快照、不重放步骤:已完成的步骤在检查点里带着那一步应用完之后的 State 快照,恢复时直接读回这份 State 并整步跳过,不按记录下来的 DTO 重放该步 delta——重放会重复登记切图、重复累加回填说明,把同一份错误报两遍。只有带状态快照的行才算已完成,旧格式(只有 DTO)的行按未完成重跑。
- 轮次:一轮以 `run` 行开头,以该轮第一行 `write-back` 或 `outdated` 结束;只有最后一轮没有结束行时才需要恢复。`outdated` 只作废未完成的那一轮,不删除既有行。
- 半行:追加前先把日志截断到最后一个换行,丢掉崩溃留下的半行,避免它夹在日志中间。
- 漂移:文档在轮次中途被改动(当前 State 既不是 `run` 行快照、也不是切分后那份快照)时,追加一行 `outdated` 并**返回错误**,不在同一次调用里自动重开新一轮;下一次调用看到 `outdated` 才从头开新一轮,且以当前文档为基准。
- 写回幂等:`save` 成功但 `write-back` 行没追加时,恢复读回的切分后 State 与文档当前 State 相等,即判为已写完,直接补 `write-back` 行并返回成功。这条判据成立的前提是工作流这条路不产生随机身份:识别树的根节点 id 来自 DTO,切图资源 id 走 manifest 的 by-path 复用。
- 切图资源不回滚:切分产出的图片与已登记资源在失败后保留,重放靠 by-path 复用接上,不做回滚清理。
- 写回成功后才清理 sidecar:与前端切分链路一致,`backfill_errors` 为空且 `problematic_nodes` 为空时调用
`finalize_separation`;有回填问题或问题节点时保留 sidecar,交给编辑器显示恢复入口。
- 切分 op 内部的细粒度恢复仍由 `SeparationState` 承担(`ui-editor-separation-state.v2` sidecar),日志只记录工作流层面的步骤完成,不复制它的进度。
- 不引入跨语言 fixture 比对:镜像的四个 seam 都是简单变换,靠同语义实现与各自单测覆盖,不为它们额外维护一套 golden。
## 模块布局
### Rust
```text
src/ui_editor/agent_tools/
├─ mod.rs 模块声明与三个工具的对外导出
├─ creation.rs from-images:登记图片、建文档、manifest 注册、命名取号
├─ checkpoint.rs JSONL 追加、读取、轮次判定
├─ run_workflow.rs recognize → separate → write-back 编排与恢复
├─ steps/
│ ├─ mod.rs 步骤子模块声明
│ ├─ recognize.rs 识别 DTO 落 State
│ ├─ separate/
│ │ ├─ mod.rs 切分 DTO 落 State(回填、清状态、写 NeedReview)
│ │ └─ cut_images.rs 切图图片登记与 SpriteAsset 构造
│ └─ write_back.rs 保存 State、记 write-back / outdated 行、漂移文案
└─ test_support.rs agent_tools 单测共用夹具
src/agent/runtime_tools/ui_design_doc.rs 工具参数解析与 Runtime 侧调用
```
`into-js` 不需要独立模块:它直接复用 `persistence.rs` 的 `generate_ui_design_code_at`,
该入口本来就只渲染 `ui/generated-<stem>-<digest>.js` 且不推进项目 revision。
每个文件只承担一件事;`checkpoint.rs` 不感知切分,`creation.rs` 不感知识别。
### 提示词目录模块
工具描述与参数文案一律进 `src-tauri/prompts/runtime/`,不在 Rust 里硬编码面向模型的中文长文案:
- 新增文本目录 `texts/ui-design-doc.json`,在 `manifest.json` 的 `textCatalogs` 登记为 `uiDesignDoc`。
- 键名规则 `<工具名>.<字段>`,工具名用下划线形式:`from_images.description`、`run_workflow.parameters.designDocAssetId` 等;Rust 侧用 `prompt_text!("uiDesignDoc.from_images.description")` 引用。
- 构建期 `build_support/runtime_prompt_bundle.rs` 会校验目录已登记、key 非空、bundle 内没有未登记的 `.md`/`.json`,因此新增文件必须同步 `manifest.json`。
## 落地顺序(全部已完成)
1. 清理 legacy:`ui.workflow.run`、`merge`、`binding` 与前端合并调用。
2. 术语与 ADR:`CONTEXT.md` 四个词条、检查点 ADR。
3. 本文档。
4. 提示词目录模块 + 两个无状态工具(`from-images`、`into-js`)。
5. `run-workflow` 与 JSONL 检查点(`agent_tools/{checkpoint,run_workflow}.rs` + `steps/separate/`)。
6. 前端收口:删 `uiDesignResourceBridge` 的 `ui-workflow.*` 优先级与 `project-development` 的自动打开分支。
7. 三个工具注册进 Runtime(`agent_native_tools`、`runtime_tools/ui_design_doc.rs`、可执行工具目录、并行账本映射、design-foundation 白名单)。
## 关联文档
- [UI 编辑器代码地图与模块职责](./【技术方案】UI编辑器代码地图与模块职责-2026-09-23.md)
- [UI 编辑器自动切分素材工作流](./【技术方案】UI编辑器自动切分素材工作流-2026-09-08.md)
@@ -0,0 +1,74 @@
# UI 编辑器代码地图与模块职责
更新时间:`2026-09-23`
一句话定位:`apps/ai-game-creator-shell` 的 UI 编辑器由「Rust/Tauri 权威层 + React 会话层 + 视图层」三段组成;Rust 持有可持久化 State、校验、LLM 工具链、预览渲染与代码生成,前端只负责语义编辑、会话编排和表现。
## 分层与数据流
```text
view/ui-editor (页面/组件)
└─ useUiEditorSession ← features/ui-editor (语义 + adapter)
├─ useUiEditorState / stateTransition / nodeTransformGeometry
├─ uiDesignStateStore → Tauri command
└─ invoke: recognize_ui / separate_ui / ...
└─ src-tauri/src/ui_editor (权威 State、校验、持久化、切分)
```
- 唯一事实来源是项目内的 `ui_design` JSON 文档(含 `revision`),前端 State 只是它的编辑副本。
- 所有跨进程类型由 Rust 经 `ts_rs` 生成到 `features/ui-editor/types/`(当前 53 个文件),前端不得手改。
## Rust 侧:`src-tauri/src/ui_editor`
| 模块 | 职责 |
| --- | --- |
| `state/mod.rs` | 权威 State 根类型:`State`(`ui_trees` + 界面图/sprite/字体三张资源表)、`UITree`;ts-rs 导出源 |
| `layout/` | 节点模型:`Node`、`NodeMetadata`/`StageStatus`、`ControlLayout`、`Container`、`NodeOffset`、`transform`、`ChildrenDisplayMode`(`Stack`/`Exclusive`) |
| `component/` | 组件枚举 `Component::{Image, Text}`、`NodeComponent`(LLM 工具载荷的 `PureNode`/`WithComponent` 判别式) |
| `resource/` | 界面图(`path` / `pixel_size` / `pixels_per_unit`)、sprite(含 `SpriteBorder` 九宫格)、字体(格式/媒体类型/CSS format)资源描述 |
| `persistence.rs` | 文档读写、`revision` 乐观并发保存、领域校验(重复 ID、树/资源引用、组件状态)、代码生成写盘 |
| `agent_tools/` | Agent 工具链路:`creation.rs` 用一至四张设计图新建文档(登记未登记图片、按 `ui/UI 设计 N.json` 取号、持项目写锁装 revision 0、失败回滚)、`checkpoint.rs` JSONL 检查点日志、`run_workflow.rs` 三步编排与崩溃恢复(恢复只读检查点里的 State 快照、整步跳过已完成步骤,不按 DTO 重放)、`steps/` 逐步落 State(`mod.rs` 步骤子模块声明、`recognize.rs` 识别、`separate/` 切分:`cut_images.rs` 登记切图与 `SpriteAsset` 构造、`mod.rs` 回填与问题状态、`write_back.rs` 保存与漂移文案) |
| `html_renderer/` | 由 State 生成 HTML 片段与 JS(maud + 布局/组件 CSS 映射),供预览与 `ui/generated-*.js` |
| `commands/` | LLM 工具链:`recognition`(结构识别)、`separation/`(自动切分素材)、`utils.rs`(LLM 请求、重试、`required_tool_arguments`) |
| `commands/separation/` | 切分批处理、截图/预切、sidecar 恢复(inspect / finalize / discard)、patch 回写 |
Agent 工具(`agent_native_tools.rs` + `agent/runtime_tools/ui_design_doc.rs`):`ui-design-doc.from-images` 新建文档并登记;`ui-design-doc.run-workflow` 在 Rust 内跑 `recognize → separate → write-back`,按文档旁 JSONL 检查点恢复;`ui-design-doc.into-js` 复用 `generate_ui_design_code_at` 产出 `ui/generated-*.js`,不推进 revision。旧 `ui.workflow.run` 与 `ensure_ui_design_resource_for_prototype` 已退役。
关键命令(`main.rs` 注册):`load_ui_design_state`、`save_ui_design_state`、`generate_ui_design_code`、`create_ui_design_doc_from_images`、`recognize_ui`、`separate_ui`、`inspect_separation_recovery`、`finalize_separation`、`discard_separation_recovery`。
保存语义(`save_ui_design_state_at`):`Saved` / `Unchanged` / `Conflict`(返回当前快照)三态;先 `validate_state` 再持锁重读比对 `expected_revision`,成功后推进项目 revision。代码生成只接受已保存的 revision,产物路径为 `ui/generated-<stem>-<digest>.js`。
## 前端:`src/features/ui-editor`(无视图依赖的语义层)
- `useUiEditorState.ts`:State reducer + 撤销/重做(`undo/redo/resetHistory`)、`isLocked` 与 `runWithStateLocked`、节点/资源/树偏移的语义写操作、`createTree`(新树横向排布 + `UI_TREE_PADDING`)、删除影响 projection。
- `stateTransition.ts`:React-free 的命令 → State 语义 transition(`set-tree-offset`、`set-node-metadata`、`set-node-component`)。
- `nodeTransformGeometry.ts`:State 级节点几何(页面矩形、父矩形、resize 手柄反演),预览/Inspector 共用。
- `stateInvariants.ts`:保存前不变量 projection,给视图稳定的中文失败信息;Rust 仍是权威校验。
- `uiDesignStateStore.ts`:`IUiDesignStateStore`(`load`/`save`/`generateCode`)+ Tauri 实现 + 内存替身。
- 结果应用 seam:`recognition.ts`、`separationStatus.ts`(问题节点 → `NeedReview`)。
- 概览 projection:`stageStatusOverview.ts`、`separationOverview.ts`。
- 前置校验:`requisites.ts` 在发起 LLM 操作前检查必需资源与结果完整性。
- 适配器:`importAdapter.ts`(图片解码、批量导入、字体准备)、`uiDesignResourceBridge.ts`(调用 `create_ui_design_doc_from_images` 新建文档)、`useUiEditorFontFaces.ts`(私有字体族加载)、`spriteBorder.ts`。
- `utils/`:State → CSS 映射(`componentToCss`、`controlLayoutToCss`、`textStyleToCss`、`transform/tf2css`)、`treeUtils`。
## 前端:`src/view/ui-editor`(表现与编排)
- `useUiEditorPage.ts`:`useUiEditorSession` 是视图与 adapter 的唯一协调边界,产出 `input` / `canvas` / `inspector` / `workflow` / `dialogs` / `save` 六个小 projection;视图不接收完整 controller。
- `index.tsx`:页面骨架(输入侧栏、预览、Inspector、工具栏、保存/生成结果弹窗、键盘快捷键绑定),由 `view/project-development` 挂载。
- `model.ts`:两步工作流(识别界面结构 / 自动切分素材)、导入种类、操作失败文案。
- `operationLifecycle.ts`:recognition/separation 共用的异步操作 adapter。
- `components/`:`InputSidebar`、`UiTreePanel`、`Inspector/*`(Transform、Components Text/Image、SpriteBorder)、`preview/*`(`PreviewWorkspace`、`UiTreeRenderer`、组件视图、排他子节点 tab、缩放/平移/拖拽手势)、工作流与结果弹窗。界面图没有独立显示名字段,列表与 Inspector 的统一显示名取 `path` basename(`view/project-development/resourceAssetDisplayName.ts`)。
- 已退役的 render mode 由会话级开关 `showFrame` / `showOriginImage` / `showComponent` 取代。
## 扩展指引
- 新增节点/组件字段:先改 `layout/`(或 `component/`)并让 ts-rs 重新导出,再补 `stateTransition`、`stateInvariants`、`persistence::validate_node`、Inspector 与预览映射。
- 新增 LLM 步骤:在 `commands/` 内自成 command(prompt + schema + 领域校验 + materializer),共用 `commands::utils` 的请求、重试与 tool-call 解析 seam。
- 新增界面图字段:改 `resource/ui_design_image.rs` 后让 ts-rs 重新导出,再补 `persistence::validate_state`、导入 adapter(`features/ui-editor/importAdapter.ts`)与 Inspector / 预览展示。
## 关联文档
- [UI 编辑会话模块边界](./【前端架构】UI编辑会话模块边界-2026-08-19.md)
- [UI 编辑器 Godot 容器布局模型](./【技术方案】UI编辑器Godot容器布局模型-2026-08-18.md)
- [UI 编辑器子节点显示规则](./【技术方案】UI编辑器子节点显示规则-2026-08-18.md)
- [UI 编辑器自动切分素材工作流](./【技术方案】UI编辑器自动切分素材工作流-2026-09-08.md)
@@ -2,11 +2,11 @@
## 目标
UI 编辑器的“分析参考图”“识别界面结构”“自动切分素材”三个工作流动作在每次运行结束后,用独立的阻塞通知弹窗明确反馈结果,避免仅依赖卡片内一行状态文本而被忽略。
UI 编辑器的“识别界面结构”“自动切分素材”两个工作流动作在每次运行结束后,用独立的阻塞通知弹窗明确反馈结果,避免仅依赖卡片内一行状态文本而被忽略。
## 交互约定
- 三个动作的每次运行在终态(成功或失败)时自动弹出一次通知。
- 两个动作的每次运行在终态(成功或失败)时自动弹出一次通知。
- 弹窗打开期间遮挡并阻塞工作台底层交互;关闭后恢复当前步骤,不自动切换步骤、不自动重跑。
- 使用现有 `ThemedModal` 的普通关闭行为(遮罩、Esc 和关闭按钮均可关闭)。
- 弹窗仅承载通知,不提供“继续”“重试”或其他业务操作。
@@ -19,7 +19,6 @@ UI 编辑器的“分析参考图”“识别界面结构”“自动切分素
成功状态的基线文案:
- 分析参考图:保留已应用的语义建议数量;若现有状态可可靠取得问题/待确认数量,则一并展示。
- 识别界面结构:保留替换的界面树数量,并展示识别结果中的待检查/必须修复数量(若可取得)。
- 自动切分素材:保留现有 `B/B` 批次计数,改为用户可读的切分结果。
@@ -28,12 +27,12 @@ UI 编辑器的“分析参考图”“识别界面结构”“自动切分素
## 实现边界
- 新增独立的工作流通知弹窗组件文件,组件只负责展示和关闭,不包含工作流领域规则或后端副作用。
- 在 UI 编辑器页面/会话投影中维护临时通知状态,并在三个异步动作的成功与失败终态写入。
- 在 UI 编辑器页面/会话投影中维护临时通知状态,并在两个异步动作的成功与失败终态写入。
- 不新增后端字段或公开契约;数量只能使用当前前端已有且可靠的数据。
## 验收
1. 三个动作成功和失败终态各弹出一次通知;绑定批次只弹最终一次。
1. 两个动作成功和失败终态各弹出一次通知;绑定批次只弹最终一次。
2. 弹窗打开时底层工作台不可操作,且无继续/重试等业务按钮。
3. 弹窗可通过标准关闭方式退出;关闭后卡片状态仍可见。
4. 每条成功文案保留原有数量信息并增加可用的检查数量,所有文案包含“请检查”。