Merge remote-tracking branch 'origin/master' into fix/web-preflight-browser-recovery
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m3s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 4m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m4s
Project CI / Backend tests (pull_request) Successful in 5m59s
Project CI / Frontend tests (pull_request) Successful in 2m52s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m5s
Project CI / Native shell tests (pull_request) Successful in 6m37s
Project CI / Repository checks (pull_request) Successful in 6m24s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m3s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 4m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m4s
Project CI / Backend tests (pull_request) Successful in 5m59s
Project CI / Frontend tests (pull_request) Successful in 2m52s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m5s
Project CI / Native shell tests (pull_request) Successful in 6m37s
Project CI / Repository checks (pull_request) Successful in 6m24s
This commit is contained in:
@@ -52,7 +52,14 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
`runtimeSessionInstallFailed` / `authClientInitFailed` 三个 catch 用本操作的固定文案,路径等敏感片段
|
||||
由报告侧的 sanitize 换成占位符。
|
||||
- `#[tauri::command]` 的 `Err` 直接携带该枚举(Tauri 2 的 `InvokeError(pub serde_json::Value)` 支持结构化错误)。
|
||||
这是 DirectProject 已有的做法(`enqueue_direct_codex_turn -> Result<(), DirectTurnEnqueueFailure>`),不是新约定。
|
||||
这是 DirectProject 已有的做法(`enqueue_direct_codex_turn -> Result<(), EnqueueError>`),不是新约定。
|
||||
DirectProject 的三条通道固定为三张类型表:入队拒绝 `EnqueueError`、宿主内部回合错误 `TurnError`
|
||||
(不导出、不跨进程)、`turn.completed.failure` 载荷 `TurnFailure`;类型随所属深模块命名
|
||||
(`agent/codex_app_server/turn_error.rs`、`agent/thread_manager/wire/failure.rs`),不再带 `Direct` 前缀;
|
||||
回合终态判定 `TurnCompletion` 是宿主内部判别联合(`agent/thread_manager/turn_completion.rs`),不导出、不进 `wire/`。
|
||||
`TurnFailure` 里仍带字符串的两臂专门标成"待清的债":`SuperErrorFromStringPlusStage`(`stage` 是 typed 枚举、
|
||||
`detail` 仍是产生层字符串)与 `Unclassified`(连阶段都没有),名字故意起丑,`detail` typed 化后即改名;
|
||||
`TurnError::classify`(返回 `TurnErrorClassified{ShouldStop, ShouldContinue}`)是**唯一**投影点,任何地方都不许再把 typed 失败重包成"阶段失败 + 预拼文案"。
|
||||
- 变体按**可判定的事实**命名。服务端 400 只提供 `status + message`(`AppError.code` 仍是通用
|
||||
`BAD_REQUEST`),所以 400 变体按"哪条请求的输入被拒"命名(如 `passwordLoginRejected`),
|
||||
不假装能区分密码长度/手机号格式;**任何地方都不允许对错误文案做判断**。
|
||||
|
||||
@@ -786,6 +786,46 @@
|
||||
}
|
||||
},
|
||||
"/api/external/v1/editor/assets/folders/{folderId}": {
|
||||
"get": {
|
||||
"tags": ["Editor Assets"],
|
||||
"operationId": "getEditorAssetFolder",
|
||||
"summary": "按 ID 读取当前账号素材文件夹",
|
||||
"description": "要求 editor:asset scope。仅按 folderId 读取目录并校验当前账号归属,不读取素材列表或创建默认目录;空目录也可读取。目录不存在返回 404,无权访问返回 403。",
|
||||
"security": [
|
||||
{
|
||||
"ExternalApiKey": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/FolderId"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "素材文件夹",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ExternalEditorAssetFolderResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/BadRequest"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/components/responses/Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": ["Editor Assets"],
|
||||
"operationId": "updateEditorAssetFolder",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
## 验证命令
|
||||
|
||||
1. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml direct_thread -- --nocapture`
|
||||
2. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`(生成 `src/features/project-workspace/generated/`),随后用 `prettier --write` 格式化生成目录,避免未格式化的 ts-rs 输出混进提交
|
||||
2. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`(生成 ts-rs 绑定);生成物保持原始输出直接提交,不要再跑 prettier(见 `decision-log.md` 2026-10-03「生成绑定不再经 prettier」)
|
||||
3. `npm run test -- apps/ai-game-creator-shell/tests/directThreadChat.test.ts apps/ai-game-creator-shell/tests/directTurnPresentation.test.ts apps/ai-game-creator-shell/tests/directHistoryPaging.test.ts apps/ai-game-creator-shell/tests/directHistoryAnchorGate.test.ts`
|
||||
4. `npm run test -- apps/ai-game-creator-shell/tests/appSurface.test.ts`(`project-development.suite.ts` 由该入口注册,不能作为独立测试入口;同时验证现役 Design Agent 的会话恢复与审批界面。)
|
||||
5. TypeScript 类型检查与 ESLint(范围同前次 DirectProject 迁移)。
|
||||
|
||||
@@ -1,4 +1,41 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-10-03 生成绑定不再经 prettier:ts-rs 原始输出即提交形态
|
||||
|
||||
- 背景:`scripts/check-generated-bindings.mjs` 对 AGC 的 `chat/generated` / `services/generated` 在重生成后会就地跑一遍 `npx prettier --write` 再比较。这会直接改写生成文件,还把「Rust 声明真的变了」与「prettier 版本 / 配置造成的格式漂移」混在同一条告警里——本次报出的 `ThreadRequestKind.ts` / `TurnCompletedStatus.ts`「内容变化」无法复现为语义变化(已提交内容与当前 Rust 枚举一致),prettier 归一化把格式差异也报成了「与 Rust 声明不一致」;生成物被仓库格式化工具二次改写后,重跑 `cargo test export_bindings` 也不再幂等。
|
||||
- 决策:生成绑定一律以 ts-rs 原始输出提交,不接受 prettier / eslint 等工具二次改写。`check-generated-bindings.mjs` 删掉 prettier 步骤,直接逐字节比较原始输出;`packages/shared/src/contracts/generated`、`chat/generated`、`services/generated`、`features/ui-editor/types` 四个目录统一登记进 `.prettierignore` 与 `.eslintrc.cjs` 的 `ignorePatterns`;`.prettierrc.json` 里为 `contracts/generated` 设的 `printWidth: 1000` / `singleQuote: false` 覆盖随之删除。
|
||||
- 边界:ts-rs 原始输出在多行对象 / 枚举变体行尾带空格,`.gitattributes` 对四个生成目录设 `whitespace=-trailing-space`,CI 的 `git diff --check` 不再误报;手写文件的行尾空白检查不变。生成 `.ts` 仍提交进仓库供前端消费,并标 `linguist-generated=true`。
|
||||
- 影响范围:`scripts/check-generated-bindings.mjs`、`.prettierignore`、`.prettierrc.json`、`.eslintrc.cjs`、`.gitattributes`、`apps/ai-game-creator-shell/src/{view/project-development/chat,services}/generated/**`。
|
||||
- 验证:`cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings`、`npm run check:generated-bindings`(shared-contracts 1 + AGC 104 = 105 个文件)、`npx prettier --check` 四个生成目录(全部跳过)、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-10-03 回合错误的处置分类层与线程条目脱敏边界
|
||||
|
||||
- 背景:`TurnError::terminal_failure -> Option<TurnFailure>` 用一个 `None` 同时表达"没有失败"和"不是失败、要继续跑(返修 / 复核控制流)",调用方看到 `None` 只会理解成前者,`dispatch.rs` 两处只能 `.expect("回合失败必可投影成失败载荷")`;同时 ThreadManager 在搬运线程条目时做字段级脱敏与限长,前端要被截断,而且与失败载荷的脱敏是两套实现。
|
||||
- 决策(分类层):`TurnError::classify(&self, root) -> TurnErrorClassified{ShouldStop(TurnFailure), ShouldContinue { detail: String }}` 取代 `terminal_failure`——真失败继续投影成 `TurnFailure`(脱敏 + 截断仍在这一处),控制流带自己的说明走 `ShouldContinue`;`dispatch.rs` 两处改成按分类 match,`ShouldContinue` 不写终态、不伪造失败。"这一轮怎么收场"仍只由 `TurnCompletion` 定义,private `SessionOutcome` 并入 `session_completion`。
|
||||
- 决策(脱敏边界):ThreadManager **不做任何字段级脱敏与限长**——`wire/items.rs` 的 `bounded` / `detail_text` / `sanitize_detail_text` / `relativize_project_root_paths` / `thread_delta_text` 与三个字符上限常量删除,条目与流式增量原样透传;脱敏移到前端 `chat/conversation/directThreadSanitize`,在 bootstrap / consume / 历史切片进入聊天状态之前统一做(前端手里有 `projectPath`,能把项目内绝对路径归一成相对路径)。失败载荷(错误文案)的脱敏不属于 ThreadManager,保留在 `TurnError::classify` 投影时。订阅侧的 8 MiB / 8192 事件缓冲上限不动:那是背压,不是字段限长。
|
||||
- 边界:错误尚未持久化,`TurnErrorClassified` 是纯宿主内部类型、不加 `Serialize` / `TS`;线上 `TurnFailure` 与 `ThreadEvent` 形状、占位符词表(`<absolute-path>` / `[redacted-secret]` / `[redacted-sensitive-field]` / `[redacted-config]` / `[redacted sensitive context]`)都不变。`TurnFailure` 名字加了 `// TODO badnaming` 待后续改。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/{turn_error.rs,mod.rs},thread_manager/{dispatch.rs,turn_completion.rs,wire/{items.rs,failure.rs,tests.rs}},generation/prompt_context.rs}`、`apps/ai-game-creator-shell/src-tauri/src/commands/desktop.rs`、前端 `chat/{controller/useDirectThreadChatSubscription.ts,conversation/directThreadSanitize.ts}`、`chat/generated/{ThreadItem,TurnFailure}.ts`、`tests/directThreadSanitize.test.ts`。
|
||||
- 验证:`cargo test -- agent:: --skip export_bindings`(949 passed;删掉一条已无对象的流式脱敏用例)、`npm run check:generated-bindings`、`npm run ai-game-creator-shell:typecheck`、`npx vitest run apps/ai-game-creator-shell/tests/directThreadSanitize.test.ts`(12 passed)、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`;全量 vitest 的 `gameDistributionPublish*` / `recentProjectsHook` localStorage 失败为既有问题。
|
||||
|
||||
## 2026-10-02 DirectProject 失败载荷两臂改名与 typed 失败原样下发
|
||||
|
||||
- 背景:`turn.completed.failure` 的两个载荷臂把字符串债藏在像正常类型的名字后面(`TurnFailed { stage, detail: String }`、`TurnFailedUnclassified { detail: String }`);同时 `direct_runtime` 与 `thread_manager::dispatch` 各有一处收口把**任意** `TurnError` 重包成 `TurnFailed { stage: turn_failure_stage(), detail: 预拼收口文案 }`,typed 变体(模型调用 / 超时 / 通道断开)在下发前就被吃掉,前端只能看到 catch-all 与一个伪造的 `code-generation` 阶段。
|
||||
- 决策(命名):两个载荷臂按"这是待清债"的既有约定起丑名字——`TurnFailed → SuperErrorFromStringPlusStage`(只有 `stage` 是 typed、`detail` 仍是产生层字符串)、`TurnFailedUnclassified → Unclassified`(连阶段都没有);线上 `type` 同步为 `superErrorFromStringPlusStage` / `unclassified`,生成文件 `TurnFailed.ts` / `TurnFailedUnclassified.ts` 删除。`stage` 留在变体内部,**不做成 wrapper 层的正交字段**:其余 8 个变体根本没有阶段,包到外层只会给它们编假值。
|
||||
- 决策(透传):删掉上述两处收口重包,`Err(failure)` 原样透传;`.agent/runtime/errors`、应用日志与错误上报池的审计写盘保留,只是返回值不再当失败载荷的 `detail`。前端因此第一次能按真实变体(`modelCallFailed` / `timedOut` / `transportClosed`…)选文案。
|
||||
- 边界:不改 `SuperErrorEnumFromStringTyped`(仍在 `is_retryable` / `is_model_repairable` 上做内部分类,detail 的 typed 化留给上游改造);不迁就存量字符串分流、不加别名;错误尚未持久化,线上 `type` 值直接改、不做迁移。
|
||||
- 影响范围:`agent/codex_app_server/turn_error.rs`、`agent/direct_runtime/{mod.rs,user_input.rs}`、`agent/thread_manager/{dispatch.rs,turn_completion.rs,wire/failure.rs}`、前端 `chat/{conversation/directTurnFailure.ts,generated/**}`、`src/features/agent-runtime/model.ts`(注释)、`tests/directThreadChat.test.ts`。
|
||||
- 验证:`cargo check`(bin)、`cargo test -- agent:: --skip export_bindings`(948 passed)、`cargo test export_bindings` 后 `npx prettier --write chat/generated/*.ts`、`npm run ai-game-creator-shell:typecheck`、`npx vitest run apps/ai-game-creator-shell/tests`(`NODE_OPTIONS=--localstorage-file=…`,196 files / 1930 passed)、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-10-02 DirectProject 回合错误命名化与 wire 模块拆分
|
||||
|
||||
- 背景:DirectProject 三条错误通道的类型还带着 `Direct` / `DirectCodex` 前缀(`DirectTurnError` / `DirectTurnEnqueueError` / `DirectTurnFailure`),且 `thread_manager/wire.rs`(1600+ 行)把条目投影、事件、失败载荷与终态判定混在一个文件里;`direct_turn_error.rs` 也留在 `agent/` 顶层而不是它服务的 `codex_app_server` 深模块旁边。
|
||||
- 决策(命名):既然类型已经归到具名深模块,去掉冗余前缀——`DirectTurnError → TurnError`、`DirectTurnEnqueueError → EnqueueError`、`DirectTurnFailure → TurnFailure`、`DirectTurnDeadline → Deadline`、`DirectCodexFailureStage → FailureStage`、`DirectCodexNativeKind → NativeKind`、`DirectModelCallKind → ModelCallKind`;`direct_codex_user_item` 里的 `DirectCodexUserItem/UserMessageItem/UserRole/UserContentPart/UserAttachmentReferencePart/UserRuntimeRegionPart → UserItem/UserMessageItem/UserRole/UserContentPart/UserAttachmentReferencePart/UserRuntimeRegionPart`;`codex_app_server` 内部的 `DirectCodexTurnKind/DirectCodexTurnObservation/DirectTurnRunFailure/DirectTurnReport/DirectTurnTerminalContext/DirectTurnCancelView → TurnKind/TurnObservation/RunFailure/TurnReport/TerminalContext/TurnCancelView`;`direct_now_ms → now_ms`。载荷 struct 名仍是"变体裸名",`TurnFailure` 的投影点仍是 `TurnError::classify` 一处。
|
||||
- 决策(模块归位):`agent/thread_manager/wire.rs` 提升为目录并按职责拆分——`wire/{mod,clock,items,turn,failure,tests}.rs`,`wire/` 只留**线上形状**(`TurnFailure` 载荷与 `ThreadEvent`);`agent/direct_turn_failure.rs` 的终态判定并入 `agent/thread_manager/turn_completion.rs`;`agent/direct_turn_error.rs`(三条错误表与分类)移到 `agent/codex_app_server/turn_error.rs`。`thread_manager::wire::*` 仍从 `mod.rs` 平铺 re-export,外部路径不变。
|
||||
- 决策(终态类型):宿主侧的回合终态从 `struct TurnTerminal { status, failure }` 改为判别联合 `enum TurnCompletion { Completed, Interrupted, Aborted, Failed(TurnFailure) }`——`status` 由变体反推(投影回 `turn.completed` 时才变回字符串),失败必须带载荷、正常收场不许带;「有载荷就一定是失败」这条反推关系不变。`TurnCompletion` **不是线上形状**:不加 `Serialize` / `TS`、不导出、不下发,放在 `thread_manager/turn_completion.rs` 而不是 `wire/`;收尾阶段推出来的 `status` 字符串在这里一次性收进类型,认不出的值 fail closed(按未分类失败),不冒充正常收场。`thread_manager/mod.rs` 的过期回合释放兜底也改走 `TurnCompletion::Aborted.event(...)`。
|
||||
- 边界:只改类型 / 函数 / 生成文件命名与模块位置,不改线上 JSON 形状(`TurnFailure` 的 `type` 判别值与字段名不变,`TurnCompletion` 只影响宿主内部);不迁就存量字符串分流,不保留别名或 `Display` 回落。`DirectTurnFailureKind`、`wire_kind()`、`Display for TurnError` 与 `From<TurnError> for String` 已在上一轮删除。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/{codex_app_server/{mod.rs,execution.rs,turn_error.rs},thread_manager/{mod.rs,dispatch.rs,wire/},direct_runtime/{mod.rs,user_input.rs},direct_codex_user_item/*,runtime_driver/entrypoints.rs,...}`、前端 `chat/{controller,conversation}/**` 与 `chat/generated/**`(旧绑定文件删除、新绑定文件随 `cargo test export_bindings` 生成)、`scripts/check-generated-bindings.mjs`、`tests/**`。
|
||||
- 验证:`cargo check`(bin)、`cargo test -- agent::thread_manager`(69 passed)与 `cargo test -- terminal`(73 passed)、`npm run ai-game-creator-shell:typecheck`、`npx vitest run apps/ai-game-creator-shell/tests`(DirectProject 相关用例全绿;`gameDistributionPublish*` / `recentProjectsHook` 的 localStorage 失败为既有问题,与本次无关)、`npm run check:generated-bindings`(104 个文件)、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-10-02 资源编辑远端失败的原始原因穿出到工具错误,资源编辑错误通道补一层 typed
|
||||
|
||||
- 背景:轮询到 `status=failed` 时客户端只读 `status`,丢掉平台在同一个响应里给的 `error`(契约 `ExternalEditorGenerationJobResponse.error`),统一写 `terminal_failure_code = remote-generation-failed` 并返回「remote-terminal-failed: 资源编辑生成失败」。平台的可行动原因就此消失:模型与用户卡片只看到一句「失败了」,重试路径(`ensure_resource_edit_phase_resumable`)也只有分类码。这违反 `pitfalls.md`「远端资源编辑终态必须指出唯一出口」里已写下的口径——「首次失败的原始拒绝说明继续由当次错误文案承担」;提交期 HTTP 400 分支(`editor_api_rejection_reason`)兑现了,轮询分支没有。另外 `remote-terminal-failed:` 只是文案前缀(全仓没有 `starts_with` 解析它),在第一句失败文案里与「失败」重复。
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
|
||||
## 验证路由
|
||||
|
||||
AGC 生成准备只通过单目录 GET 确认当前账号绑定,不能重新依赖完整素材库,也不能把目录查询失败当成首次无绑定而创建目录。目录删除时保留证据并停止;`prepared / accepted / legacy-completed` 生成账本继续用冻结快照恢复。该查询同时覆盖站内会话与 External v1,后端新增接口先发布,旧 API 保持兼容;详见 AGC 实施计划和后端数据契约。
|
||||
|
||||
Windows 下的移动壳 smoke 通过 Node 启动从当前 workspace 包解析出的 Expo/EAS CLI,不直接 `spawnSync('npm.cmd')`;保留原配置与导出断言。具体入口和警告清理边界见本地开发运维文档。
|
||||
|
||||
AGC 的纯桌面/生产接入按模块排除测试编译,共享实现与受测入口保持可测试;拆分后同时检查普通目标和测试目标,并保留原有行为断言。命令注册与退出接线的源码检查跟随真实模块位置更新,不能因移动文件而漏检;不通过全局允许死代码或虚假调用消除告警。具体边界见开发运维文档“编译告警的保留边界与待优化项”。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- **原因**:① 登录已下沉 Rust,`login_client_with_password` 等命令失败返回 `Err(String)`,Tauri 以**裸字符串**拒绝 `invoke`,前端拿不到任何类型信息;② `shouldCaptureClientError` 对非 object 值走默认 `return true`,`handleLoginSubmit` 的 catch 把预期业务拒绝报进了错误池。技术方案里"预期 4xx 登录/鉴权失败不进池"的口径早就成立,是错误通道的实现方式违背了它。
|
||||
- **处理(现行口径)**:命令错误一律按具体变体结构化(`Result<_, ClientAuthError>` + ts-rs 导出),认证命令统一经 `invokeClientAuth` 调用:结构化拒绝原样装进已有的 `ClientAuthErrorWrapper`(只有一个 `error` 字段,值是判别联合),UI 在 catch 里按具体变体分流——认得的业务 / 会话变体只给用户反馈,系统变体原样 `throw` 经 `unhandledrejection` 入池,非结构化拒绝原样抛出;删除 `shouldCaptureClientError`。详见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 与 [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](../../adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md)。
|
||||
- **判据/取证**:`npx vitest run apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx`——登录返回结构化业务变体时 `report_client_error` 不被调用;系统变体只上报一次(`source` 取全局 `unhandledrejection` handler 的显式入参;载体不再携带 `action`)。Rust 侧 `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml auth_error` 钉住变体 `type` 与 400/401/429/5xx/网络映射。
|
||||
- **形状约定**:`ClientAuthError` 顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段——无字段变体在 TS 里就是 `{ type: 'x' }`;带载荷变体是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体),细分原因枚举(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`)同样由 ts-rs 生成。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律写在 catch 子句里:`const failure = error.error; switch (failure.type)`,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as X` 再读自己的字段,`reason` 是枚举时再 `switch (payload.reason)`,两处 `default` 都用 `expectNever` 保证漏接编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物随后交给 prettier;ts-rs 只写文件、不删文件,变体从有载荷改成无字段时要手动清掉孤立的 `X.ts`(本次 `AuthResponseServerRejected.ts` 就是这样删的)。
|
||||
- **形状约定**:`ClientAuthError` 顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段——无字段变体在 TS 里就是 `{ type: 'x' }`;带载荷变体是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体),细分原因枚举(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`)同样由 ts-rs 生成。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律写在 catch 子句里:`const failure = error.error; switch (failure.type)`,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as X` 再读自己的字段,`reason` 是枚举时再 `switch (payload.reason)`,两处 `default` 都用 `expectNever` 保证漏接编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物保持 ts-rs 原始输出(不再经 prettier / eslint 二次改写,见本节「生成绑定」口径);ts-rs 只写文件、不删文件,变体从有载荷改成无字段时要手动清掉孤立的 `X.ts`(本次 `AuthResponseServerRejected.ts` 就是这样删的)。
|
||||
- **Rust 侧不得把结构化错误降级成字符串**:`refresh_session_inner` 的非权威失败直接返回 `Err(ClientAuthError)`,视图不带 `errorMessage`;一旦折成 `String`,前端就只能拿文案判断,变体信息永久丢失。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、`apps/ai-game-creator-shell/src/services/{clientAuth.ts,errorReporting.ts,platformSession.ts}`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`。
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
- **仍未修**:`/` 开头的 delta 片段被判成绝对路径(上面 ②)。要修必须把「上一个字符」带进边界判定(正文增量可从 `direct_project_history` 的累计文本取,思考增量目前没有累计器),属于跨 delta 状态的取舍,本次未一并改。
|
||||
- **判据/取证**:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell direct_thread_delta`——新增 `direct_thread_delta_sanitization_preserves_line_breaks` 钉住「逐段脱敏 == 整段脱敏」,去掉 `split_inclusive` 即红;真实文本的回归用渲染侧夹具复核(修复前 table/li/h2 全 0,修复后与整段脱敏一致:1 个 table / 3 个 th / 4 个 h2)。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs`、`.../agent/generation/prompt_context.rs`、`.../agent/thread_manager/wire.rs`、`apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directThreadChat.ts`、`#384`。
|
||||
- **现状(2026-10-03 更新)**:逐 delta 的 Rust 脱敏已整体退役——ThreadManager 改为原样透传,流式正文与历史条目的脱敏统一移到前端 `directThreadSanitize`,在事件进聊天状态前逐段处理并保留换行。上面两条根因与"仍未修"针对的实现已不存在,保留作历史。
|
||||
|
||||
## 2026-10-01 文件列举被无关临时锁删除打断
|
||||
|
||||
@@ -5794,7 +5795,8 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **处理(现行口径)**:不要把重写结果当改动提交。跑过 `cargo test` 或构建后只恢复 `apps/ai-game-creator-shell/src/view/project-development/chat/generated/DirectCodexUser*.ts` 的仓库版本,再删掉多出来的 `DirectCodexUserMessageEnvelope.ts`;不要恢复整个 `generated/` 目录,以免误删 DirectThread 的现役绑定。绑定与前端形状冲突时以**已提交的 DirectCodexUser 绑定 + 前端**为基准排查。
|
||||
- **验证**:恢复仓库版本后 `npm run ai-game-creator-shell:typecheck` exit 0(`[skill-pack] OK`);保留重写结果时同一条命令 exit 2。release 构建本身还会在 `src/features/ui-editor/types/` 落下 `BindingChange.ts` / `BindingDTO.ts` 两个无人引用的生成产物;它们不属于前端契约,发现后直接删除,不提交。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_codex_user_item/`(ts-rs 导出源)、`apps/ai-game-creator-shell/src/features/project-workspace/resourceReferences.ts`、`apps/ai-game-creator-shell/scripts/build-release.mjs`(`beforeBuildCommand`)。
|
||||
- **2026-09-24 补充(纯格式 churn 的一档)**:直接运行 AGC 的测试二进制(含全量套件,里面带 `export_bindings`)同样会把 `src/view/project-development/chat/generated/DirectThread*.ts` 重写成 raw ts-rs 形态——双引号、行尾空格、`};` 变 `, };`——于是 `git diff --check` 报一片 trailing whitespace,而 `npm run check:generated-bindings`(只覆盖 `packages/shared/src/contracts/generated`)不会报警。处理:对这组文件跑一次 `npx prettier --write "apps/ai-game-creator-shell/src/view/project-development/chat/generated/*.ts"` 做归一化,再确认 `git diff --stat` 为空——为空就说明这一档只是格式 churn,没有语义变化;不要为它盲目 `git checkout` 整个目录。该目录目前没有自动漂移门禁,靠「跑完测试后归一化 + diff 检查」把关。
|
||||
- **2026-09-24 补充(纯格式 churn 的一档)**:直接运行 AGC 的测试二进制(含全量套件,里面带 `export_bindings`)同样会把 `src/view/project-development/chat/generated/DirectThread*.ts` 重写成 raw ts-rs 形态——双引号、行尾空格、`};` 变 `, };`——于是 `git diff --check` 报一片 trailing whitespace,而 `npm run check:generated-bindings`(只覆盖 `packages/shared/src/contracts/generated`)不会报警。处理(2026-10-03 更新,口径已变):不再跑 prettier 归一化——`chat/generated` 与 `services/generated` 的提交形态就是 ts-rs 原始输出;两个目录已进 `.prettierignore` + eslint `ignorePatterns`,`git diff --check` 由 `.gitattributes` 的 `whitespace=-trailing-space` 豁免,`npm run check:generated-bindings` 会逐字节比较原始输出。跑完 `cargo test` 后 `git diff` 为空即为幂等。
|
||||
- **2026-10-02 补充(过滤跑也躲不开)**:`cargo test` 的位置参数是**子串匹配**,`-- agent::thread_manager::` 这类"只跑几个模块"的过滤会连带命中 `agent::thread_manager::wire::failure::export_bindings_turnfailure`,于是照样重写 `chat/generated/` 与 `src/services/generated/`(后者是 auth 绑定)。要按模块跑又不碰生成目录,用 `cargo test --bin genarrative-ai-game-creator-shell -- agent:: --skip export_bindings`;已经被重写时不要再做 prettier 归一化:两个目录的提交形态就是原始输出,按需重新生成或 `git checkout --`,再用 `npm run check:generated-bindings` 确认无漂移。
|
||||
|
||||
## 2026-09-16 Node 26 下 vitest 的 jsdom 用例拿不到 window.localStorage
|
||||
|
||||
@@ -6277,7 +6279,8 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **根因 2(缺落盘)**:assistant 回复只存在于 SDK 事件流里。codex 路径由 `finish_direct_project_collect_history` 写进 `.agent/conversations/project.jsonl`,cc 没有对应步骤,所以即使回合收口成功,UI 也读不到回复。
|
||||
- **根因 3(工具被拒)**:sidecar 用 `permissionMode: 'dontAsk'` 且没有 `allowedTools`,宿主 MCP 工具(`mcp__agc__*`)一律被直接拒绝,模型只能回"没有权限"。
|
||||
- **根因 4(界面看不到回复)**:聊天区是按 `item.completed` 事件流投影的(codex 路径在 `rawResponseItem/completed` 时下发 `ThreadItem::Message`),只把回复落进 `project.jsonl` 不会让本轮出现在界面上——用户看到"用户气泡 + 本轮结束于 … · 耗时",回复只在重进项目时从历史读出来。
|
||||
- **现行口径**:cc 成功出口由放行侧补写 `DirectTurnTerminal::completed()`(`finish_if_unfinished` 幂等,codex 已写过终态时是空操作);cc 解析成功后必须①把回复按 `{"type":"message","role":"assistant","id":"direct-codex:<clientTurnId>:assistant","content":[{"type":"output_text","text":…}]}` 落进项目历史,②用**同一个 id** 下发 `ThreadEvent::item_completed(ThreadItem::Message{role:"assistant"})`(落盘失败按回合失败收口);sidecar 按 `mcp__<server>` 前缀整体放行请求里声明的 MCP 服务器(权限策略在宿主侧执行)。
|
||||
- **根因 5(验收反馈复用 assistant ID)**:同一 client turn 进入 `ReviewRequired` 后会再次调用 cc。首次回复已经占用 `direct-codex:<clientTurnId>:assistant`,第二次不同正文沿用该 ID 会被历史层正确拒绝为冲突,随后却被错误投影成 `runtime-unclassified`。真实诊断中可见「写入本项目对话历史失败:…assistant」且历史已经有该条回复。
|
||||
- **现行口径**:cc 成功出口由放行侧补写 `DirectTurnTerminal::completed()`(`finish_if_unfinished` 幂等,codex 已写过终态时是空操作);cc 每次解析成功后都把实际落盘的回复 item id 同步下发 `ThreadEvent::item_completed(ThreadItem::Message{role:"assistant"})`。首个回复沿用 `direct-codex:<clientTurnId>:assistant`,同一回合的反馈回复遇到内容冲突时追加 `:assistant:<uuid>`,相同内容仍按原 ID 幂等;落盘失败按回合失败收口。sidecar 按 `mcp__<server>` 前缀整体放行请求里声明的 MCP 服务器(权限策略在宿主侧执行)。
|
||||
- **诊断口径**:`agent.direct_turn.host_dropped` / `agent.direct_turn.panic` 里的令牌字段必须写 `tt=`,写 `turnToken=` 会命中脱敏标记,整行变成 `<sensitive diagnostic details redacted>`,离线只剩"说不出原因"的 HostDropped。
|
||||
- **验证**:dev 栈里用 CDP 注入真实回合(`node %TEMP%\agc-cdp.mjs <expr>`):①读文件轮 `claude-parse-done chars=108`,`.agent/conversations/project.jsonl` 出现 `direct-codex:cdp-…:assistant` 条目,回复内容与 `game/index.html` 前两行(`<!doctype html>` / `<html lang="zh-CN">`)逐字一致(证明宿主工具真的执行了);②聊天视图打开时注入 `只回三个字:收到了`,DOM 断言(`document.body.innerText`)同时出现用户气泡 `11:40:05`、助手回复 `收到了` 与 `本轮结束于 11:40:16 · 耗时 10.7秒`(证明 `item.completed` 实时投影生效,不必重进项目);同一日志不再出现新的 `host_dropped`。`cargo test … -- claude_code_cli::tests direct_turn_failure::tests` 19 passed。
|
||||
- **验证**:dev 栈里用 CDP 注入真实回合(`node %TEMP%\agc-cdp.mjs <expr>`):①读文件轮 `claude-parse-done chars=108`,`.agent/conversations/project.jsonl` 出现 `direct-codex:cdp-…:assistant` 条目,回复内容与 `game/index.html` 前两行(`<!doctype html>` / `<html lang="zh-CN">`)逐字一致(证明宿主工具真的执行了);②聊天视图打开时注入 `只回三个字:收到了`,DOM 断言(`document.body.innerText`)同时出现用户气泡 `11:40:05`、助手回复 `收到了` 与 `本轮结束于 11:40:16 · 耗时 10.7秒`(证明 `item.completed` 实时投影生效,不必重进项目);同一日志不再出现新的 `host_dropped`。另有 `agent::claude_code_cli::tests::direct_claude_feedback_reply_does_not_fail_on_a_reused_client_turn_id` 回归覆盖同一回合两次不同回复。`cargo test … -- claude_code_cli::tests direct_turn_failure::tests` 19 passed。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/thread_manager/dispatch.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/direct_turn_failure.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`、`apps/ai-game-creator-shell/agent-sidecar/src/index.mjs`。
|
||||
|
||||
@@ -80,8 +80,8 @@
|
||||
|
||||
- `project.jsonl` 与项目主对话共用信封类型,不要为了"可见但不喂模型"新增行结构。
|
||||
- 埋点 `settle` 早于成绩入库会静默丢事件(未来"进历史但不喂模型"的条目同理要落在注入侧,不在读取侧)。
|
||||
- `cargo test export_bindings` 会重写全部 `chat/generated/`(引号风格漂移),跑完要 `git checkout --`
|
||||
掉不是本次新增的文件。
|
||||
- `cargo test export_bindings` 会重写全部 `chat/generated/`;生成目录的提交形态就是 ts-rs 原始输出
|
||||
(2026-10-03 起已从 prettier / eslint 排除),跑完用 `git diff` 为空确认幂等,不要做 prettier 归一化。
|
||||
- 本机 rust 全量 `--bins` 测试会挂在 mock server 的 `inet_csk_accept` 上,用 `--bins "agent::"` 之类过滤跑。
|
||||
|
||||
## review 收口第二轮(2026-09-24)
|
||||
|
||||
@@ -946,6 +946,10 @@ Agent Runtime 负责:
|
||||
- 2026-07-10 补充:后台任务工具箱已加入 `preview.start`。Agent 可在 loop 中自行请求启动当前项目的本地 HTTP 预览;Runtime 会复用 `preview.start` 策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑,并把 `agent.runtime.preview.start` 写入 `.agent/agent.db`。该 observation 只向 LLM 返回 localhost URL 与端口,不返回用户项目绝对路径。
|
||||
- External Editor 的远端项目和资源属于服务 principal,不属于本地项目的跨账号身份。`.agent/manifest.json` 中 `source.canvasProjectId / resourceId / assetObjectId` 只记录生成来源,不得作为当前登录账号的 active editable binding。客户端在 `.agent/runtime/external-editor-bindings/` 私有保存账号级投影:项目键至少包含服务 origin、principal 和本地 `projectId`,资源键再包含远端项目、本地 asset ID、源文件 SHA-256、媒体类型与 canonical asset kind;值保存该 principal 下的远端 project/folder/resource/object 身份。Token 轮换继续复用同一 principal binding,账号、服务、源字节或 canonical kind 变化必须重新建立;同名本地项目和项目改名不得参与权威匹配。
|
||||
- binding 的 lookup key 与稳定 payload 内容指纹分开校验;远端 project/folder/resource/object ID、object key 或尺寸被单独改写时必须失败关闭。同一本地项目和 principal 的首次项目 binding 建立必须串行化覆盖“复读 binding → 必要的远端创建 → 原子安装 binding”,不得因并发任务创建孤儿画布。External v1 项目、素材目录和项目资源创建请求携带由 binding key 派生的稳定 `Idempotency-Key`;服务端在 owner 与接口命名空间内生成稳定 ID,并允许同键同正文安全重试,使远端响应后、本地 sidecar 落盘前崩溃也不会重复创建。
|
||||
- 素材生成准备的目录确认采用单目录读取:站内 `GET /api/editor/assets/folders/{folderId}` 与开发者 `GET /api/external/v1/editor/assets/folders/{folderId}` 返回现有 `{folder: ...}` 数据结构,后端按目录主键读取并校验 owner,不读取账户素材、不创建默认目录、不执行媒体修复。External 入口要求 `editor:asset` scope;缺失目录返回 404,越权返回 403,未认证返回 401。原素材库快照及目录创建、修改、删除接口的合同不变;不修改持久化表结构。先部署新增后端接口,再发布依赖该接口的客户端;新客户端不回退完整素材库。
|
||||
- 已有当前账号项目 binding 或恢复 partial 的目录身份时,在新的生成准备中按该 ID 确认目录并核对响应 ID;合法空目录也可继续。首次没有可复用目录身份时直接沿用稳定幂等创建和冻结正文,不先列举素材库,不按同名认领目录。partial 继续只承载中间恢复进度,成功写入完整 binding 后删除;既有生成账本的 `prepared / accepted / legacy-completed` 恢复仍使用冻结快照,不重新查询或创建目录。
|
||||
- 单目录查询超时、断网、服务异常、鉴权失效、越权或会话切换时停止准备并保留现有 binding / 恢复证据,不解释为目录缺失、不自动 POST 新目录。明确 404 时提示绑定目录不存在并停止,不用原键复活目录、不换键重建或重发生成;自动重新绑定与素材分页、关键词筛选、按素材 ID 导入不属于该行为。验证覆盖全库不可用而单目录正常、空目录、首次建绑、缺失与越权、查询失败及切号,并保留并发单次创建、创建响应丢失、冻结正文及生成账本恢复回归。
|
||||
- 单目录合同的定向验证使用 AGC Rust `external_canvas_context`、`canvas_generation_tests`、`project::resource_editor::tests`、`background_removal_tests` 与 `direct_tools_mcp::tests`,工具链 HTTP 夹具按单目录 GET 返回 `data.folder`;后端使用 `external_editor`、`external_mcp` 和 `protected_route_matrix_keeps_auth_methods_and_head_behavior`,覆盖 OpenAPI 自动增加单目录 MCP 工具后的目录数量及名称唯一性。`npm run check:editor-idempotency-procedures` 在独立 SpacetimeDB 中验证真实目录读取、归属、删除与只读行为,host 单测不能代替该运行时证据。
|
||||
- 当前 binding wire schema 为 v2,payload fingerprint 必填。已落盘的 v1 project/resource JSON 只能经独立严格 wire 结构读取,在 lookup key、principal、本地 source、远端字段、对象键、时间和尺寸等不变量全部通过后重算指纹并原子回写 v2。不得用 `serde(default)` 把未知或篡改文档混入迁移。
|
||||
- 已完成的本地正式资源以本地文件和 manifest asset 身份为权威。切换账号后,当前账号没有资源 binding 时,必须从本地字节上传、confirm 并登记到当前账号的远端项目后再编辑或派生;任何请求都不得把旧账号的 `canvasProjectId / resourceId / objectKey` 与新账号 Token 组合。新绑定不得覆盖历史来源,也不得迁移或删除旧账号远端对象。已经 `prepared / accepted / running` 的生成或编辑 operation 仍冻结原 principal、原正文和原 operation;切换账号只能停止恢复并保留对账证据,不能改绑、重放或重复扣费。
|
||||
- 远端提交返回 `operationId` 后先持久化 accepted 身份,再在每次 poll、download 和本地 commit 前复验冻结会话;同步本地 commit 持有冻结会话租约,使切号只能发生在完整安装前或安装后。Tauri 手工生成同样使用稳定的 durable operation slot,不得使用进程内临时幂等键。历史资源编辑账本从 manifest 远端 resource ID 迁移到本地 asset ID 时,只允许在本地 asset、路径和内容摘要仍一致时兼容旧指纹,新请求不得恢复信任 manifest 远端 ID。
|
||||
|
||||
@@ -617,6 +617,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- Rust 结构体:`EditorAssetFolder`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布账号级素材文件夹表,归属于用户账号而不是 project;首次读取素材库时自动创建系统默认“项目素材”文件夹。文件夹支持重命名、折叠和删除,系统默认文件夹不能删除。
|
||||
- 单目录读取:`GET /api/editor/assets/folders/{folderId}` 与 `GET /api/external/v1/editor/assets/folders/{folderId}` 经同一 `spacetime-client` facade / 只读 procedure 按目录主键和 owner 校验,返回现有目录 DTO;不读取素材、不创建默认目录或修复媒体。External 要求 `editor:asset` scope;未认证 401、越权 403、缺失 404。既有全库、目录创建/修改/删除 API 合同和持久表结构保持不变;先部署后端再发布依赖新接口的 AGC 客户端,不提供全库回退。
|
||||
- 索引:`by_editor_asset_folder_owner_user_id`。
|
||||
|
||||
### `editor_asset`
|
||||
|
||||
Reference in New Issue
Block a user