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
This commit is contained in:
2026-09-24 19:17:48 +08:00
30 changed files with 1745 additions and 117 deletions
@@ -97,6 +97,17 @@
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{direct_turn_error.rs,direct_turn_failure.rs,direct_delivery.rs,direct_runtime/mod.rs,direct_runtime/user_input.rs,codex_app_server/mod.rs,codex_app_server/execution.rs}` 与 `apps/ai-game-creator-shell/src-tauri/src/cli.rs`;文档 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md`。
- 验证:`cargo test --bins -- direct_`(460 passed)、`cargo test --bins -- codex_app_server`(100 passed)、`cargo fmt --check`、`npm run check:encoding`、定向 `git diff --check`。整包 `cargo test --bins` 在本机被既有 `tests::provider` / `tests::project` 重型用例挂住(并发跑测试时另有一条锁竞争用例会假失败),非本次改动引入。真实客户端观感未复核。
## 2026-09-23 ACL 提权修复按目标做 single-flight
- 背景:`windows_acl_repair_target` 对 Managed 作用域返回的是「第一个读取被拒的祖先」,同一祖先下的多个项目会解析到**同一个** repair target;而唯一的去重只是单次调用内的局部 `attempted_targets`。于是启动页一次挂载(≤8 个最近项目并发检查)会启动同样多次 `powershell -Verb RunAs`,用户看到叠在一起的 UAC 弹窗(issue #498)。
- 决策:新增进程级闸门 `acl_repair_gate`,key = `(规范化 repair target, scope)`。并发调用只允许一次真实提权,其余等待并复用**同一结果**;结果在冷却窗口内直接复用(成功 30s / 失败 15s / 用户取消 120s),等待窗口 60s 超时按失败关闭。leader 异常退出由 RAII 兜底记为失败并唤醒全部等待者,避免等待者被永久挂住。
- 决策补充(key 归一化):key 的路径半边经 `windows_acl_repair_gate_key` 归一化——去掉 `\\?\` / `\\?\UNC\` 前缀并统一小写。最近项目列表里同一项目实测同时存在 `\\?\C:\...` 与 `C:\...` 两种写法(客户端 localStorage 实测),不归一化就是两个 key,同一个目录仍会弹两次 UAC。这里刻意只做前缀与大小写归一而不 `canonicalize`:待修复目标恰恰是「读不动的目录」,解析不可靠。
- 决策补充(冷却基准):冷却从**结果落库**时刻算起,不是 leader 起跑时刻。UAC 弹窗会被挂着几十秒到两分钟,用起跑时刻会让 120s 拒绝冷却在用户应答前就过期,前端 15s/45s/120s 的整表重查紧跟着再弹一次。
- 决策补充(leader 失效接管):`leader_deadline`(默认 5 分钟)之后,新调用可以接管仍是 `running` 的 key;每个 leader 带令牌,被接管后旧 leader 迟到的结果直接丢弃,不会覆盖接管者的结果。真机上无人应答的 UAC 约 2 分钟自然超时,所以这个上限只兜「提权子进程真挂死」——否则该目标会永久按失败关闭(`clear_denials` 不清理 running,只能重启客户端)。
- 错误类型化:用户取消 UAC 的错误统一带稳定标记 `AGC_ACL_ELEVATION_DENIED`,前端据此判定「不可自动重试」,不再依赖中文文案匹配。
- 用户主动操作(打开/新建项目、文件选择器选择目录、重命名刷新)会调用 `clear_game_creator_acl_elevation_denials` 清除拒绝记忆,保证显式重试仍能再次请求提权。前端唯一入口是 `features/app-shell/aclElevation.ts` 的 `clearAclElevationDenials()`:最近项目 hook(`rememberRecentWorkspace` / `refreshRecentWorkspace`)与打开/新建链路(`useHomeProjectCreation.openProject`,覆盖行内打开与 picker)共用它;漏挂入口会让用户「点了打开立即失败、也不问授权」。
- 未做:给提权子进程加有界等待(`Start-Process -Wait` 目前无超时)。理由:中断挂起的 UAC 流程比等待更糟,single-flight 已把并发弹窗收成一个,follower 的等待由 60s 窗口兜底。
## 2026-09-24 DirectProject 状态条口径翻转、几何约束与对话 Markdown 容错
- 背景:AGC DirectProject 对话区底部的「陶泥儿正在处理 / 已耗时 12.4秒」状态条同时退化三处:① 读秒 1 秒一跳(耗时文案不足一分钟显示一位小数,小数位却一秒才动一格);② 窗口压矮时被挤扁(300px 高压到 33px、240px 时 24px,文字被 `overflow: hidden` 裁掉);③ `turn.started` 之前(模型首 token 前,实测约十秒)整条卡片不出现,界面没有任何「正在处理」的交代。同批还修了对话 Markdown 的两处代码块问题(不换行把消息拉宽、粘在正文行里的围栏导致代码块解析错位)。
@@ -9500,3 +9511,17 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 代价(已知并接受):接单窗口与订阅重建窗口里聊天区没有这一轮的显示,反馈只有 composer 忙态、状态行与「陶泥儿正在处理」卡片(卡片这一段还读不出「已耗时」——起点是宿主的 `turn.started.at`,开始事件到了才开始读秒);`project.jsonl` 用户条目依旧只在首屏 / 翻页读进前端。
- 影响面:`apps/ai-game-creator-shell/src/view/project-development/chat/conversation/{directTurnPresentation.ts,directCodexConversation.ts}`、`.../chat/controller/{useDirectProjectChatController.ts,useDirectProjectTurnStatus.ts}`、`.../chat/DirectProjectChatView.tsx`、`.../chat/components/DirectProjectConversation/DirectProjectTurn.tsx`、`src-tauri/src/agent/codex_app_server/mod.rs`(用户条目时间的注释口径)、对应 ADR 与实施计划。
- 验证:`npx vitest run tests/directTurnPresentation.test.ts tests/directProjectTurn.test.tsx tests/directProjectTurnStatus.test.ts tests/directThreadChat.test.ts tests/chatComposerAttachmentCap.test.tsx`、`tests/appSurface.test.ts`(213 passed / 9 skipped)、`npx tsc -p tsconfig.json --noEmit`、`eslint`、`prettier --check`、`npm run check:encoding`、`git diff --check` 全绿。真实客户端观感未复核。
## 2026-09-24 AGC 模型目录初始值改为上游同步:不再回退写死的 gpt-6-astra/gpt-5.6-luna
- 背景:`agc_model_catalog` 缺行时 procedure 兜底返回内置目录(`quality → gpt-6-astra`、`fast → gpt-5.6-luna`),两个模型都已从上游移除;从未配置过目录的环境(新库、清库、本地调试)会把不存在的模型下发给客户端,选中后上游 `model_not_found`。
- 决策(范围):本次只改目录初始值的来源,保持既有格式与契约不变 —— 目录字段仍是 `id`/`alias`/`modelId`/`defaultModelId`,后台页面与 admin DTO、`GET /api/llm/models` 形状、客户端 `select_game_creator_model` 的标识校验都不动,因此没有不兼容变更。
- 决策(初始化):api-server(API/All 角色)启动时目录缺失、结构与当前定义不符或校验不通过即视为未初始化;此时请求上游 Router 控制面的分组定价列表 `GET {控制面}/api/pricing?group=taonier`(公开只读、不带凭据),按 `data[].model_name` 排序生成目录:`modelId` 与 `alias` 用上游原名(不再填“高质量/快速”),`id` 用模型名 slug(小写字母/数字/`-`/`_`,同名冲突追加 `-2`,因此客户端标识校验无需放宽),全部 enabled,默认项取排序后第一项,并按存量 revision 写回自增。
- 决策(来源选择,2026-09-24 实测后确定):不用管理面模型注册表 `/api/models/`(会带出已下线、没有路由绑定的 `gpt-6-astra`/`gpt-6-luna`),也不用 `/v1/models`(要求 Router 用户 Key,用管理 token 实测 401)。当日 `group=taonier` 在售 6 个:`deepseek-flash`、`deepseek-v4-pro`、`glm-5.3`、`glm-5.3-flash`、`qwen-image-3.0`、`qwen3.8-flash`。
- 决策(失败关闭与重试):拉取失败、空列表、响应超 1 MiB、缺可解析 revision 或写回失败都只记录 error,不写替代目录;未初始化期间 `GET /api/llm/models`、`/api/llm/responses`、`/api/llm/chat/completions` 与后台 `GET/PUT /admin/api/agc-models` 返回 `503`“模型目录未初始化”;只在启动期尝试一次,下一次启动重试,直到目录里有数据。启动本身不因同步失败而失败,避免 Router 短时不可用放大成 api-server 起不来。
- 决策(幂等与并发):目录只取决于模型集合(排序后生成),重复同步结果一致;多实例并发启动只有一个写入成功,冲突方重读并校验既有目录可用性。目录只在未初始化时重建,上游变化不自动跟随。
- 决策(存量目录):结构合法的目录不会被自动重建,包括旧版写死的 `quality/fast` —— 需要 owner 在后台改掉,或清空该行后重启重新同步。
- 影响范围:`module-runtime`(`from_upstream_models` + slug 生成,删除写死的 `Default`)、`spacetime-module`(缺行返回 `AGC_MODEL_CATALOG_NOT_INITIALIZED`)、`api-server`(启动期同步、上游请求硬化、只校验地址的目标校验、后台 PUT 未初始化门禁)、AGC 客户端(默认模型占位改为 `platform-default`)、AGC 模型弹层 CSS、主规范/后端契约/运维文档。
- 验证:`cargo test -p module-runtime --lib agc_models::`(4 passed)、`cargo test -p api-server --bin api-server agc` 与 `llm::`、AGC 客户端 `configuration::`、admin-web 页面定向 Vitest 与 typecheck、两套 workspace 的 `cargo fmt -- --check`、`check:encoding`/`check:doc-index`/`check:spacetime-schema`/`git diff --check`。
- 验证(真实上游 smoke,本地 dev DB):清空 `agc_model_catalog` 后启动 api-server → 日志 `已按上游模型列表初始化 AGC 模型目录 revision=1 model_count=6`;登录后 `GET /api/llm/models` 返回同一批模型、`displayName` 即上游原名、默认项为排序后第一项;上游不可达/非 2xx 时启动只记录 error、AGC 接口 `503` 且目录保持未初始化;目录已存在时重启不重写。
- 边界(未验证/残留):上游在售模型超过 32 条时同步会失败(目录项上限未改);`qwen-image-3.0` 这类图像模型会一起进入目录,是否对 AGC 隐藏由 owner 在后台停用;混合版本期间未升级的 api-server 会把自己的 AGC 接口打到 `503`,module 与 api-server 必须同批发布/回滚。
@@ -1,5 +1,15 @@
# 踩坑与排障记录
## 同一祖先下的多个项目会各自弹一次 UAC
- **现象**:AGC 启动页一次挂载出现多个叠在一起的 UAC 提权弹窗;用户点「否」后仍会被再问一次。
- **原因**:`windows_acl_repair_target`(`src-tauri/src/config.rs`)对 Managed 作用域返回「第一个读取被拒的祖先」——同一祖先下的多个项目解析到**同一个** repair target;而唯一的去重是单次调用内的局部 `attempted_targets`,跨调用、跨线程都没有记忆。启动页一次并发检查 ≤8 个最近项目,就会并发启动同样多次 `powershell -Verb RunAs`。
- **处理**:进程级 single-flight(key = `(规范化 repair target, scope)`)+ 结果冷却(成功 30s / 失败 15s / 用户取消 120s)+ 等待窗口 60s 超时按失败关闭;leader 异常退出由 RAII 兜底唤醒等待者。用户取消带稳定标记 `AGC_ACL_ELEVATION_DENIED`,前端据此不自动重试;用户主动操作会清除拒绝记忆。
- **不要踩的坑**:① 闸门 key 必须归一化 `\\?\` / `\\?\UNC\` 前缀——最近项目列表里同一项目实测同时存在 `\\?\C:\...` 与 `C:\...` 两种写法,按原始字符串做 key 会让同一个目录弹两次 UAC(`windows_acl_repair_gate_key`);② 冷却必须从**结果落库**时刻算起,用 leader 起跑时刻会让 120s 拒绝冷却在 UAC 被挂着两分钟时提前过期,紧接着的自动重查立刻再弹一次;③ 复现「多个项目共用同一 target」时,DENY 要写在祖先的**父目录**上靠继承落入祖先——`icacls` 直接加在容器自身实测只影响子项(容器自身 `GetFileAttributes` 仍成功),target 会退化成每个项目自己,repro 不出并发弹窗;④ 夹具路径必须落在 `game_creator_private_path_allows_auto_elevation` 放行范围内(runtime config dir / `.config/genarrative` / 打包 AppData / 带 `.agent/manifest.json` 的项目根),因为提权子进程会按 **repair target** 再校验一次 `scope.allows_path`,否则失败关闭。
- **验证**:`src-tauri/src/tests/acl_repair_gate.rs`(并发只执行一次、冷却复用、拒绝冷却、清除后可重试、follower 超时、leader panic 唤醒等待者、冷却基准、路径写法归一、leader 卡死接管与迟到结果丢弃)。真机复现(无需提权交互即可计数):在 Managed 放行范围内建 8 个带 `.agent/manifest.json` 的假项目 → 对共同祖先的**父目录** `icacls <父目录> /deny *<sid>:(OI)(CI)(RX)` → 挂载启动页,同时数 `powershell.exe` 里命令行带 `RunAs` 的进程数(`Start-Process -Wait` 会让它一直存活到用户应答)与 `consent.exe` 峰值:修复前 8 个并发请求,修复后 1 个;把同一目录的 `\\?\C:\...` 与 `C:\...` 两种写法一起塞进最近项目,还能验证 key 归一化是否生效(修复前 2 个、修复后 1 个)。
- **leader 卡死的兜底**:闸门只有 follower 的有界等待(60s),若提权子进程真的挂死(`Start-Process -Wait` 无超时),`leader_deadline`(5 分钟)之前该 key 一直被占住,之后新调用会接管并按新 leader 执行;被接管后旧 leader 迟到的结果按令牌丢弃,不会覆盖接管者。`clear_game_creator_acl_elevation_denials` 只清「被拒绝」记忆,不清理 running。
- **关联**:`src-tauri/src/acl_repair_gate.rs`、`src-tauri/src/config.rs`、issue #498。
> 策划历史条目边界:旧策划 V1/V2 已全部退役,当前入口仅使用 Design Agent。下文带日期的旧 Planning V2、Fast GDD、`plan.submit_gdd`、旧 IPC/模块记录仅用于追溯,不能作为恢复旧代码、身份门禁或专属测试的依据;共享问题需在现役调用上核查。现行合同见[策划 Agent 生产迁移与工作区浏览](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md)。
## 2026-09-24 模型输出的围栏会粘在正文行里:聊天 Markdown 必须先归一化再解析