Merge branch 'master' into feat/agc-ui-motion-ux
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m59s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m44s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m53s
Project CI / Frontend tests (pull_request) Successful in 3m39s
Project CI / Backend tests (pull_request) Successful in 8m27s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m37s
Project CI / Native shell tests (pull_request) Successful in 8m56s
Project CI / Repository checks (pull_request) Successful in 7m21s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m59s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m44s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m53s
Project CI / Frontend tests (pull_request) Successful in 3m39s
Project CI / Backend tests (pull_request) Successful in 8m27s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m37s
Project CI / Native shell tests (pull_request) Successful in 8m56s
Project CI / Repository checks (pull_request) Successful in 7m21s
This commit is contained in:
+14
-2
@@ -19,7 +19,12 @@
|
||||
## 当前产品与平台
|
||||
|
||||
- [当前产品与工程约束](./【项目基线】当前产品与工程约束-2026-05-15.md):现役入口、账号钱包、UI 和后端分层。
|
||||
- [平台入口与玩法链路](./【玩法创作】平台入口与玩法链路-2026-05-15.md):只描述现役平台壳与图片画布编辑器链路。
|
||||
- [平台入口与玩法链路](./【玩法创作】平台入口与玩法链路-2026-05-15.md):平台壳、图片画布、游戏分发与在线游玩合同;网站游戏评分与评价已实现并通过本地验证,待用户验收,未部署。
|
||||
- [网站游戏评分与评价里程碑](./project-memory/plans/【里程碑】网站游戏评分与评价-2026-09-30.md):唯一评价、编辑预填、4000 字符、公共分页与平均分/人数的验收边界与本地证据。
|
||||
- [后台游戏评价管理合同](./【玩法创作】平台入口与玩法链路-2026-05-15.md#后台游戏评价管理合同):查找、分页、隐藏/恢复/删除、必填原因、统计与个人状态联动;已实现并通过本地验证,待用户验收,未部署。
|
||||
- [后台游戏评价管理里程碑](./project-memory/plans/【里程碑】后台游戏评价管理-2026-10-01.md)与[实施计划](./project-memory/plans/【实施计划】后台游戏评价管理-2026-10-01.md):单里程碑范围、接口/schema 边界及验收要求;本地证据已回写主规范。
|
||||
- [游戏广场评分展示合同](./【玩法创作】平台入口与玩法链路-2026-05-15.md#游戏广场评分展示合同)、[里程碑](./project-memory/plans/【里程碑】游戏广场评分展示-2026-10-01.md)与[实施计划](./project-memory/plans/【实施计划】游戏广场评分展示-2026-10-01.md):已实现并通过本地定向验证,待用户验收,未部署;公开列表/详情携带真实摘要,卡片显示一位小数均分与人数,复用有效评价统计。
|
||||
- [游戏游玩次数计数](./adr/【ADR】游戏游玩次数计数-2026-10-03.md):点「开始游戏」前端上报一次游玩,api-server 纯内存聚合(5s flush、30min 去重、`IP+game` 限流、关停不强制 flush),批量 procedure 自增现有 `game_distribution_game.play_count`,不 bump `updated_at`。
|
||||
- [外部 OpenAPI 与 API Key 接入方案](./【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md)
|
||||
- [外部 MCP 语义工具说明与参数设计](./technical/【技术方案】外部MCP语义工具说明与参数设计-2026-09-23.md):15 个新增语义工具与全部原工具并存,复用现有 External API;包含工具说明、action、参数、幂等和兼容合同。
|
||||
- [External v1 OpenAPI](./openapi/genarrative-external-v1.openapi.json):公开 HTTP 契约唯一机器可读来源。
|
||||
@@ -47,10 +52,14 @@
|
||||
- [DirectProject 独立聊天容器与工作台钱包布局](./adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md):DirectProject 与 Supervisor 等路径分容器,钱包入口由项目工作台布局独立承载。
|
||||
- [UI 工作流检查点用追加式 JSONL 日志](./adr/【ADR】UI工作流检查点用追加式JSONL日志-2026-09-23.md):UI 设计文档的 Agent 工作流用文档旁追加式 JSONL 记录步骤完成,替代每步一个 sidecar 状态机。
|
||||
- [退役 AGC 项目对话斜杠命令](./adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md):AGC 项目对话与终端 swarm chat 均不再解析斜杠命令,终端聊天入口一并退役;实现、测试、门禁与文档承诺全部删除,命令 id 与权限位作为项目策略词汇表保留。
|
||||
- [退役 AGC 独立 Agent Runtime 与 CLI 执行面](./adr/【ADR】退役AGC独立Agent%20Runtime与CLI执行面-2026-10-02.md):自建 Runtime 内核、Tauri 命令、`--agent-*` CLI、真实 e2e harness 与 CI job 一并退役,Runner 只保留编辑器桥与 GUI owner;历史由 Git 保存。
|
||||
- [引用候选由宿主注入](./adr/【ADR】引用候选由宿主注入-2026-09-22.md):引用输入区只接受宿主注入的引用 provider,素材选择面板独立成组件,附件芯片成为本轮附件唯一事实源。
|
||||
- [DirectProject 命令接单化](./adr/【ADR】DirectProject命令接单化-2026-09-23.md):命令只负责接单、事件流回答整轮结果;拒单前置、失败后置。
|
||||
- [DirectProject 命令接单化实施计划](./technical/【实施计划】DirectProject命令接单化-2026-09-23.md):四步落地顺序、每步不变式与验收;四步均已落地。
|
||||
- [DirectProject 命令入队化与待发消息队列归宿主](./adr/【ADR】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md):命令只负责入队,放行归 Thread Manager;待发消息队列作为运行态事件归宿主、前端只投影;CLI 直连入口与调用身份守卫一并退役。
|
||||
- [AGC 命令错误结构化与错误报告口径](./adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md):AGC 命令失败按具体变体建模并用 ts-rs 导出,前端按变体分流、不匹配文案;报告池只收没人处理的错误。
|
||||
- [AGC 认证失败的 JS 侧载体与抛出时机](./adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md):认证命令统一经 `invokeClientAuth` 把拒绝装进 `ClientAuthErrorWrapper`(`error` 字段就是 ts-rs 生成的 `ClientAuthError` 判别联合),不新增手写错误类;判定只写在 catch 子句里,无字段变体用固定文案、带载荷分支先 `as` 取自己的具名载荷类型(可枚举细分再 `switch (payload.reason)` 在类型化枚举上分流),系统变体原样抛出经 `unhandledrejection` 入池,`default: expectNever` 编译期挡住漏接变体。
|
||||
- [DirectProject 对话滚动与历史自动加载](./adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md):删掉「显示更早的对话」按钮改为自动加载 + 内联加载/错误行,回合 key 冻结锚点前插不跳,底部居中「回到底部 / 有新回复」胶囊,折叠展开按跟随状态贴底或保锚点。
|
||||
- [DirectProject 命令入队化与待发消息队列归宿主实施计划](./technical/【实施计划】DirectProject命令入队化与待发消息队列归宿主-2026-09-24.md):五步落地顺序、每步不变式与验收;待实施。
|
||||
- [GameAgent 对话工具调用卡片](./technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md):把右侧对话里的执行命令 / 写文件投影成 Codex 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。
|
||||
- [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。
|
||||
@@ -69,6 +78,7 @@
|
||||
- [GameAgent 资源自由画板与快速编辑](./technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md)
|
||||
- [AGC 栏目画布底部工具栏入口矩阵](./technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md):栏目画布底部工具栏的入口矩阵、可用条件与验收口径。
|
||||
- [AGC 资源工作台三处交互收口改动前对照图](./technical/assets/agc-resource-workbench-ui-before-20260914/README.md):任务侧栏两个关闭入口、左侧贴边折叠把手、顶部播放按钮居中悬浮三张改动前截图与问题说明。
|
||||
- [AGC 顶栏布局收口改动后对照图](./technical/assets/agc-toolbar-layout-after-20261004/README.md):顶栏一行到底、版本入口钉右并三档降级、「播放」并入「运行」页签、运行画面刷新入口四张改动后截图与口径。
|
||||
- [AGC 资源派生与非破坏性编辑合同](./technical/【技术方案】AGC资源派生与非破坏性编辑合同-2026-09-09.md):AGC 全类型现有资源非破坏性编辑的权威合同,约束资源派生、替换与写回边界。
|
||||
- [AGC 聊天素材引用](./【功能说明】AGC聊天素材引用-2026-09-08.md):聊天输入框 @ 引用项目素材的入口、引用模型与「当前版本素材」口径。
|
||||
- [AGC 聊天 AI 润色与发送前提醒](./【功能说明】AGC聊天AI润色与发送前提醒-2026-09-10.md):提示词润色与发送前提醒的交互、失败与取消口径。
|
||||
@@ -121,13 +131,15 @@
|
||||
- [预览画布缩放滑杆](./【交互设计】预览画布缩放滑杆-2026-09-05.md)
|
||||
- [图片画布撤销、恢复范围与操作提示方案](./【图片画布】撤销范围与操作提示方案-2026-07-17.md)
|
||||
- [微信虚拟支付接入](./【技术方案】微信虚拟支付接入-2026-05-26.md)
|
||||
- [外部产品支付服务接入](./【技术方案】外部产品支付服务接入-2026-10-03.md)
|
||||
- [支付服务接入与收银台使用说明](./【使用说明】支付服务接入与收银台使用说明-2026-10-03.md)
|
||||
- [SSE 客户端传输层收口约定](./technical/【前端架构】SSE客户端传输层收口约定-2026-06-03.md)
|
||||
- [全站客服悬浮入口接入约定](./technical/【前端架构】全站客服悬浮入口接入约定-2026-06-23.md)
|
||||
- [图片画布素材导出方案](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)
|
||||
- [UI 编辑器图片素材选择器](./technical/【前端设计】UI编辑器图片素材选择器-2026-09-03.md)
|
||||
- [后台 Dashboard 运营看板方案](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md)
|
||||
- [后台多账号与 Tab 访问权限方案](./technical/【后台管理】多账号与Tab访问权限方案-2026-07-14.md)
|
||||
- [Pingora 独立网关试点](<./technical/【开发运维】Pingora独立网关试点-2026-06-11.md>)
|
||||
- [Pingora 独立网关试点](./technical/【开发运维】Pingora独立网关试点-2026-06-11.md)
|
||||
- [AGC 后台模型别名与对话选择](./technical/【技术方案】AGC后台模型别名与对话选择-2026-09-05.md):官方目录、本地自定义 LLM 开关、端点模型勾选与预览。
|
||||
- [UI 编辑器工作流完成通知弹窗](./technical/【设计】UI编辑器工作流完成通知弹窗-2026-09-04.md)
|
||||
- [官网 SEO 地基实施约定](./technical/【SEO】官网SEO地基实施约定-2026-07-10.md)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# 【ADR】AGC 命令错误结构化与错误报告口径
|
||||
|
||||
状态:已接受(2026-10-01 落地,实施顺序见同日的决策记录与
|
||||
[`【技术方案】AGC错误报告与诊断上传-2026-08-31`](../technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md))
|
||||
|
||||
## 背景
|
||||
|
||||
用户在登录页把密码输错一次,报告面板就出现两条"错误事件"并弹出「发现问题」:
|
||||
|
||||
```text
|
||||
登录失败:密码长度需要在 6 到 128 位之间 — auth · 1 次
|
||||
手机号或密码错误 — auth · 1 次
|
||||
```
|
||||
|
||||
根因不是文案,而是两件事叠加:
|
||||
|
||||
1. 登录已经下沉到 Rust(`login_client_with_password` 等命令),命令失败以 `Err(String)` 返回;
|
||||
Tauri 把 `String` 原样交给 JS,`invoke` 以**裸字符串**拒绝,前端拿到的东西没有任何类型信息。
|
||||
2. WebView 侧的 `shouldCaptureClientError` 对"非 object"值走默认 `return true`,于是
|
||||
`handleLoginSubmit` 的 catch 把"用户输错密码"当成缺陷事件报进了错误池。
|
||||
|
||||
这与 [`【技术方案】AGC错误报告与诊断上传-2026-08-31`](../technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md)
|
||||
已写明的"预期的 4xx 登录/鉴权失败不进入错误报告池"直接冲突——口径早就定了,是错误通道的实现方式违背了它。
|
||||
|
||||
DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接单化-2026-09-23`](./【ADR】DirectProject命令接单化-2026-09-23.md) §4):
|
||||
命令返回结构化 typed error,前端按变体分流,"认不得的变体或非结构化错误"原样抛出走上报链路。
|
||||
本 ADR 把这套口径推广成 AGC 命令错误的通用约定,并同时收窄错误报告池的入口。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 命令错误按具体变体建模,不按文案匹配
|
||||
|
||||
- Rust 侧定义具体变体枚举(auth 首个落地:`ClientAuthError`),`#[derive(Serialize, TS)]` +
|
||||
`#[serde(tag = "type", rename_all = "camelCase")]`,用 ts-rs 导出到
|
||||
`apps/ai-game-creator-shell/src/services/generated/`;生成物不手改。
|
||||
- **顶层只放调用方要分流的类别,可枚举的细分原因收进类型化 `reason` 字段**——既不拆成几十个顶层变体,
|
||||
也不用字符串。`reason` 自己也是 `#[derive(Serialize, TS)]` 的枚举。internally tagged 下无字段变体是
|
||||
`{ type: 'x' }`;newtype 变体是 `{ type: 'x' } & X`,ts-rs 为它生成 `generated/X.ts`。例如服务地址
|
||||
校验是 `serverAddressRejected` + `ServerAddressReason`(`emptyOrTooLong` / `notAUrl` / `hasCredentials` /
|
||||
`hasPathOrQueryOrFragment` / `notHttps` / `unsupportedScheme` / `outsideChannel`),网络失败是
|
||||
`authNetworkFailure` + `AuthNetworkReason`(`timeout` / `unreachable`),响应契约破损是
|
||||
`authResponseInvalid` + `AuthResponseInvalidReason`(`notJson` / `invalidBody` / `missingRefreshCookie` /
|
||||
`missingUserIdentity` / `serverRejected`),而不是拆成 7 + 2 + 5 个顶层变体。
|
||||
- 前端 `switch (error.type)` 必须列全顶层变体:无字段变体直接取本 catch 的固定文案;带载荷变体先 `as`
|
||||
取自己的具名载荷类型,可枚举的细分再 `switch (payload.reason)` 在**类型化**的 `reason` 上分流(等价于
|
||||
Java 的嵌套 `switch`,仍不碰文案)。顶层与 `reason` 的 `default` 都用 `expectNever`,漏接变体或漏接
|
||||
`reason` 都是**编译错误**。**不假设所有变体都有同一个字段**,也不做任何文案匹配。
|
||||
- Rust **不预拼用户可见文案**:载荷只装原始事实(类型化的 `reason`、服务端 400 的原文 `serverMessage`、
|
||||
HTTP 状态码、本机 IO / 网络客户端构建失败的原始 `detail`),服务端没给原文就是 `None`;前缀与句式由
|
||||
前端调用方在自己的 catch 分支按当前操作拼接。**原始错误必须留在载荷里**(调用方据此分流,报告包据此
|
||||
诊断),同时另记一行本地日志;但 `detail` **不直接贴在界面上**:`clientSessionPersistFailed` /
|
||||
`runtimeSessionInstallFailed` / `authClientInitFailed` 三个 catch 用本操作的固定文案,路径等敏感片段
|
||||
由报告侧的 sanitize 换成占位符。
|
||||
- `#[tauri::command]` 的 `Err` 直接携带该枚举(Tauri 2 的 `InvokeError(pub serde_json::Value)` 支持结构化错误)。
|
||||
这是 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`),
|
||||
不假装能区分密码长度/手机号格式;**任何地方都不允许对错误文案做判断**。
|
||||
- `429` 同样按路由判定:发码路由是频控(`smsCodeThrottled`),登录路由是「验证码错误次数过多」
|
||||
(复用 `phoneCodeLoginRejected`,仍是用户可修正的输入问题、不进池),其余路由的 `429` 才落到
|
||||
`unexpectedRejection`。
|
||||
- `/api/auth/phone/login` 的 `401` 只来自「用户不存在」(验证码错误/失效/过期都是 `400`),同样归
|
||||
`phoneCodeLoginRejected`;为此退役的 `smsCodeRejected` 曾把 401 冒充成「验证码错误或过期」,属于错配。
|
||||
- 本地前置校验只做布尔判定、拿不到更细事实时不编字段:手机号校验 `phone_is_valid` 只回 true/false,
|
||||
所以 `phoneNumberInvalid` 保持无字段,提示由调用方给固定文案;编不出事实的"细分原因"不硬造。
|
||||
|
||||
### 2. 报告池只收"没有任何调用方处理"的错误
|
||||
|
||||
谁抛出、谁判定。分层规则:
|
||||
|
||||
- **预期业务拒绝**(用户输入、前置条件、预期 4xx):由调用方消化并给用户反馈,**永不进池**。
|
||||
- **真故障**(网络不可达、5xx、写盘/运行时安装失败、agent 终态失败):由调用方带上文交给错误池
|
||||
(`ClientAuthErrorWrapper` + `captureClientError`);`window.onerror` / `unhandledrejection` 只兜底
|
||||
没人接手的错误。Rust 侧 agent 终态失败仍由失败投影入池。
|
||||
- **WebView 全局 handler 是兜底**:任何没人 catch 的错误都进池。
|
||||
- **408/5xx/网络的判定由调用方在 catch 里做**:AGC shell 的 WebView 侧没有 fetch 边界的自动判定
|
||||
(`shouldCaptureClientError` 只认测试构造过、生产代码从不产生的 `{status}` / `{networkError}`
|
||||
形状,随本 ADR 删除);4xx 一律不报,交给上层调用方。
|
||||
|
||||
### 3. 前端按变体分流(2026-10-01 修订)
|
||||
|
||||
- 本节原先的"调用方用 `isClientAuthError` 读形状、`switch (error.type)` 分流、`default` 交池"已被
|
||||
[`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](./【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md)
|
||||
取代:`invokeClientAuth` 把结构化拒绝装进 `ClientAuthErrorWrapper`(`error` 字段是判别联合),判定只写在
|
||||
catch 子句里,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as` 取具名载荷类型。
|
||||
- "未识别变体上调是**故意**的"不再成立:`default` 改为 `expectNever(failure)`,漏接变体是
|
||||
**编译错误**,不再是运行时报进池。
|
||||
- 系统变体与非结构化拒绝仍由调用方原样 `throw`,经全局 `unhandledrejection` 入池;
|
||||
`captureClientError` 的 `instanceof ClientAuthErrorWrapper` 解包保持不变。
|
||||
- 删除 `shouldCaptureClientError`:不再存在"叶子自己判定要不要报"的口径。
|
||||
|
||||
### 4. 报告面板与通知行为不变
|
||||
|
||||
默认选中快照中的全部事件、只由通知中的「查看并报告」打开、poisoned 快照用 fallback 等承诺保持不变;
|
||||
本次只保证"不该进池的东西不再进池"。
|
||||
|
||||
## 后果与边界
|
||||
|
||||
- auth 三命令(`login_client_with_password`、`login_client_with_phone_code`、`send_client_phone_login_code`)
|
||||
及其共用链路(`request_auth` / `map_auth_failure` / `response_data` / `network_error_message`)全量改为
|
||||
`Result<_, ClientAuthError>`;`read_client_auth_state`、`refresh_client_auth_session`、
|
||||
`logout_client_session` 的失败面同步结构化(会话 401/403 仍是"未登录"路径,不是错误)。
|
||||
- 未识别变体不再靠运行时"上调"兜底:前端 switch 必须列全变体,靠 `expectNever` 在编译期挡住漏接。
|
||||
- 仍保留的显式采集点(`captureAgentRuntimeError`、`ResourceReferenceInput` 偏好写盘、`invokeDiagnostic`)
|
||||
在后续变更里按同一口径重抛/删除,本 ADR 不改它们的行为。
|
||||
|
||||
## 验收
|
||||
|
||||
```text
|
||||
npm run ai-game-creator-shell:typecheck
|
||||
npx vitest run apps/ai-game-creator-shell/tests/errorReporting.test.ts apps/ai-game-creator-shell/tests/ErrorReportDialog.test.tsx apps/ai-game-creator-shell/tests/ErrorReportNotice.test.tsx
|
||||
cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml export_bindings
|
||||
npm run ai-game-creator-shell:check:rust:shell
|
||||
npm run check:generated-bindings
|
||||
npm run check:doc-index
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
@@ -0,0 +1,158 @@
|
||||
# 【ADR】AGC 认证失败的 JS 侧载体与抛出时机
|
||||
|
||||
状态:已接受(2026-10-01 落地,实施顺序见同日的决策记录与
|
||||
[`【技术方案】AGC错误报告与诊断上传-2026-08-31`](../technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md))
|
||||
|
||||
前置:[`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](./【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md)
|
||||
已经把 Rust 侧的认证命令失败建模成 `ClientAuthError`,ts-rs 生成
|
||||
`{ type: 'x' } & X` 的判别联合。本 ADR 只解决这份结构化拒绝到了 WebView 之后
|
||||
"怎么传、谁来判、什么时候抛"。
|
||||
|
||||
## 背景
|
||||
|
||||
Rust 已经返回结构化错误,但 Tauri 的 `invoke` 拒绝值是**普通对象**,不是 `Error`:
|
||||
|
||||
- 调用方 `catch (error)` 拿到的是 `{ type, reason, ... }`,没有栈。原样 `throw` 它,
|
||||
上报链路的 `error instanceof Error` 判断会把它降级成 `new Error(String(error))`
|
||||
(`[object Object]`),文案与类型一起丢掉。
|
||||
- 前一版在渲染层加了 `isClientAuthError` / `getClientAuthErrorMessage` /
|
||||
`presentAuthFailure` 三层:形状读取、文案回落、分类提示(均已删除)。它们既不是类型事实源,又在
|
||||
"取文案"里悄悄承担了"要不要上报"的判断,与"由调用方判定"的口径冲突。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 不新增 JS 错误类型:直接用生成的 `ClientAuthError`
|
||||
|
||||
ts-rs 已经把 `ClientAuthError` 生成成判别联合(`src/services/generated/ClientAuthError.ts`),
|
||||
前端只有这一个错误事实源,**不再另建 `ClientAuthFailure` 之类的手写类**——那只会退化成
|
||||
`payload` / `cause` 的一层别名,给同一个事实源挂第二个名字。
|
||||
|
||||
原始拒绝值是普通对象,直接 `throw` 会被上报链路降级成 `String(obj)`;所以包装层把它装进
|
||||
**已有**的 `ClientAuthErrorWrapper`,载体只持有一个 `ClientAuthError` 类型的 `error` 字段,值就是原始拒绝值(也就是那个
|
||||
判别联合),并在构造时把整份载荷 `JSON.stringify` 写进 `Error.message`——上报事件因此拿到的是
|
||||
机器事实(变体名与载荷),而不是 `[object Object]`。它**不读任何变体字段、不拼用户文案**:
|
||||
不塞 `context`,展示文案与上报的 `source` / `action` 都由调用 `captureClientError` 时的显式
|
||||
入参决定;catch 里 `error.error as ClientAuthError` 直接分流。
|
||||
|
||||
- 只有带载荷的变体才有具名载荷类型:无字段变体在 ts-rs 里就是 `{ type: 'x' }`,不生成文件;
|
||||
有字段的变体才生成 `X.ts`(可枚举的细分 `reason` 字段自己也是生成的枚举,如
|
||||
`ServerAddressReason.ts` / `AuthNetworkReason.ts` / `AuthResponseInvalidReason.ts`)。这些具名类型是 §3
|
||||
每个带载荷 `case` 里 `as X` 的目标,也正是"不要假设所有变体字段相同"的落点——没有字段可读的变体
|
||||
不需要、也不允许硬造一个空载荷类型。
|
||||
|
||||
### 2. 一个包装函数:`invokeClientAuth`
|
||||
|
||||
`clientAuth.ts` 里所有认证命令都经它调用:
|
||||
|
||||
```ts
|
||||
async function invokeClientAuth<T>(command, args): Promise<T> {
|
||||
const invoke = requireInvoke(); // 认证桥未装:我们自己的失败关闭错误,原样抛出
|
||||
try {
|
||||
return await invoke(command, args);
|
||||
} catch (error) {
|
||||
// 原样把 Rust 的拒绝装成 JS Error;不读字段、不加字段。
|
||||
throw new ClientAuthErrorWrapper(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **不做运行时形状嗅探**:不再检查 `type` 存不存在。Rust 与 TS 同包发布,形状由 ts-rs 保证;
|
||||
出现别的形状属于 Tauri / Rust 侧的缺陷,`switch` 的 `default` 分支仍会把它抛出去上报,不会
|
||||
静默吞掉——只是不再在包装层替 Tauri 兜底。
|
||||
- `requireInvoke()` 放在 `try` 之外:认证桥未安装是我们自己的失败关闭错误,不是命令拒绝,保持
|
||||
原样抛出(`需要在 Tauri App 内登录`)。
|
||||
- **不读任何变体字段、不拼用户文案**:`Error.message` 是构造时对整份载荷的序列化,不注入
|
||||
`source` / `action`;载体把原始拒绝值原样放进 `error`。展示文案与上报上下文都由 catch
|
||||
子句里拿到具名载荷的调用方决定。Tauri 缺陷抛出的真 `Error` 序列化后只有 `{}`,但上报链路
|
||||
对真 `Error` 优先用其自身 message/stack。
|
||||
- 该包装是"Rust 结构化错误 → JS 错误对象"的唯一转换点:不做分类、不读文案判断、不兜底文案。
|
||||
|
||||
### 3. 判定只写在 catch 子句里,用具体变体
|
||||
|
||||
```ts
|
||||
catch (error) {
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) throw error; // 超时 / 桥未装等我们自己的错误
|
||||
const failure = error.error as ClientAuthError;
|
||||
switch (failure.type) {
|
||||
case 'phoneNumberInvalid': {
|
||||
// 无字段变体:文案由本 catch 给,不读任何字段。
|
||||
setLoginStatus('手机号无效: 需为纯数字且不超过 32 位');
|
||||
break;
|
||||
}
|
||||
case 'passwordLoginRejected': {
|
||||
const payload = failure as PasswordLoginRejected;
|
||||
// 前缀由本 catch 按当前操作提供;Rust 只给服务端原文(可能为 null)。
|
||||
setLoginStatus(`登录失败: ${payload.serverMessage ?? '服务端拒绝了本次登录'}`);
|
||||
break;
|
||||
}
|
||||
// ... 每个业务 / 会话变体一个分支
|
||||
case 'authNetworkFailure': {
|
||||
const payload = failure as AuthNetworkFailure;
|
||||
// 可枚举的细分在类型化 reason 上再分流,仍然不碰文案。
|
||||
switch (payload.reason) {
|
||||
case 'timeout':
|
||||
case 'unreachable':
|
||||
break;
|
||||
default:
|
||||
expectNever(payload.reason);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
// ... 其余系统变体逐个列出后原样抛出
|
||||
default:
|
||||
expectNever(failure);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **每个带载荷的业务 / 会话 `case` 用 `as` 取自己的具名载荷类型**,再读它自己的字段;字段是可枚举的
|
||||
细分 `reason` 时,再 `switch (payload.reason)` 在类型化枚举上分流。无字段的 `case` 直接用本 catch 的
|
||||
固定文案。不写跨变体的通用读取,也不让 Rust 预拼上下文。前缀取自当前 catch 的操作语义(登录、发码、
|
||||
启动检查各自可以不同),等价于 Java 的 `catch (PasswordLoginRejected e)`。系统变体先按载荷里的原始
|
||||
事实(`reason` / `status` / `serverMessage`)给一行可见反馈,再原样抛出;带 `detail` 的本机失败是例外,
|
||||
只用固定文案,`detail` 只用于分流与诊断、不贴到界面上——变体名必须逐个列出,`default` 的
|
||||
`expectNever` 才成立。
|
||||
- 业务 / 会话变体:Rust 只给可判定事实(无字段变体连字段都没有;带载荷变体给类型化 `reason` /
|
||||
`serverMessage` / `status`),调用方在自己的 catch 里补上本次操作的上下文前缀(例如
|
||||
「服务器地址非法: 远程地址必须使用 https」、「登录失败: 密码长度需要在 6 到 128 位之间」)。
|
||||
- 系统变体:调用方处理不了,先给一行可见反馈(载荷原始事实,不建兜底文案层),再**原样 `throw`**。
|
||||
`onSubmit` / `onClick` 这类 `void` 掉的 handler 抛出的拒绝最终以 `unhandledrejection` 结算,由全局
|
||||
handler 交给错误池。
|
||||
- `default: expectNever(failure)`(`expectNever(value: never)`)让"Rust 加了变体而这里
|
||||
没接"变成**编译错误**。这是上一版"未识别变体上调是故意的"的替代方案:判据从运行时前移到
|
||||
编译期。
|
||||
- 不把这段 switch 抽成 presenter / helper 函数:判定必须发生在 catch 里,包装函数只负责
|
||||
"把结构化拒绝转成 JS 错误"。
|
||||
|
||||
### 4. Rust 侧失败不再降级成字符串
|
||||
|
||||
结构化必须一路到底:`refresh_session_inner` 的非权威失败直接返回 `Err(ClientAuthError)`,
|
||||
`ClientAuthStateView` / `ClientAuthRefreshView` 不再有 `errorMessage` 字段,续期结果不再有
|
||||
`failed` 状态(`authoritative` 只在"未登录"上为 true,`failed` 恒为 false,删除它不丢信息)。
|
||||
|
||||
- `ClientAuthState` 收敛为 `authenticated | unauthenticated`:读状态失败就是命令失败,由
|
||||
`invokeClientAuth` 装进 `ClientAuthErrorWrapper`(`error` 是判别联合),不再有第三种
|
||||
"unavailable 投影"。
|
||||
- `ClientAuthRefreshResult` 收敛为 `refreshed | unauthenticated | stale`。
|
||||
|
||||
## 后果与边界
|
||||
|
||||
- 新增认证命令或新增 `ClientAuthError` 变体,必须同时改所有 catch 的 switch,否则 `tsc` 失败。
|
||||
- `platformSession` 续期失败继续按"网络类失败不降级身份、不标权威失败"处理
|
||||
(`authoritative: false`),与旧 `failed` 分支语义一致。
|
||||
- 全局 `unhandledrejection` 是系统变体的唯一出口,调用方不再直接调 `captureClientError`;
|
||||
系统变体上报的 `source` 就是该 handler 的显式入参(`unhandledrejection`),载体不再携带
|
||||
`action`;结构化拒绝的 `Error.message` 是构造载体时生成的载荷序列化,`captureClientError`
|
||||
直接把它当作事件 `message`,事件指纹因此按变体区分,报告面板呈现的是机器事实。
|
||||
|
||||
## 验收
|
||||
|
||||
```text
|
||||
npm run ai-game-creator-shell:typecheck
|
||||
npx vitest run apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx apps/ai-game-creator-shell/tests/clientAuthHost.test.ts apps/ai-game-creator-shell/tests/platformSession.test.ts
|
||||
cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml auth_error
|
||||
npm run check:generated-bindings
|
||||
npm run check:doc-index
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## 背景
|
||||
|
||||
AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(实时)、`turn-stream.jsonl`(文本段与工具交替顺序)、`tool-calls.jsonl`(已脱敏工具卡片),重进页面时还要额外接管活动回合快照。同一段文本和同一张工具卡片因此存在多个来源,实时与回读会互相覆盖,恢复路径也只能靠"哪个源先到"决定。
|
||||
AGC 项目开发聊天框当前同时从多处取数据:Direct 回合事件(实时)、另一份按顺序重建的回合投影、以及按读取期另算的工具卡片,重进页面时还要额外接管活动回合快照。同一段文本和同一张工具卡片因此存在多个来源,实时与回读会互相覆盖,恢复路径也只能靠"哪个源先到"决定。
|
||||
|
||||
`.agent/conversations/project.jsonl` 里的 Codex 原始条目本身已经带着顺序(`function_call` 与 `function_call_output` 按写入顺序落行),顺序信息并不是协议缺陷,而是在投影层被丢弃。
|
||||
|
||||
@@ -18,10 +18,10 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
|
||||
|
||||
- AGC 项目开发对话的持久事实源只有 **项目对话历史**(`.agent/conversations/project.jsonl` 的原始条目);消息文本、工具卡片和它们的先后顺序都从它派生。
|
||||
- 运行期间的回合状态只来自 **运行态事件**(Thread Manager 的 subscribe / consume / notify);`notify` 只做唤醒,不携带状态。
|
||||
- **聊天投影** 在读取与渲染时生成,不落盘、不成为第二事实源;DirectProject 聊天框停止读取 `turn-stream.jsonl` 与 `tool-calls.jsonl`,也不再提供供前端读取的命令。DirectRuntime 自己那套进度事件与文件写入属于运行时账本,本轮保留不动。
|
||||
- **聊天投影** 在读取与渲染时生成,不落盘、不成为第二事实源;聊天视图只由项目对话历史与运行态事件驱动,不再有供前端读取的旁路命令。
|
||||
- 页面重进的运行态只由 `subscribe` 的 bootstrap 事件重建,删除活动回合快照接管路径。
|
||||
- 可见性判断留在前端聊天投影:后端历史分页只按原始条目切片,前端自己跳过不可显示条目并推进锚点。
|
||||
- 线上模型是 **ts-rs 导出的 tagged enum**(`agent/direct_thread_wire.rs`),不是"一个大结构体加一堆可空字段":`DirectThreadItem` 用 `itemType` 区分条目,`DirectThreadEvent` 用 `type` 区分事件,前端直接消费生成的 TS 类型(改完 Rust 模型跑 `cargo test export_bindings`)。条目上的毫秒时间戳标 `#[ts(as = "f64")]`,因为 ts-rs 默认把 `u64` 映射成 `bigint`,而 Tauri 的 JSON 通道传的是 `number`。
|
||||
- 线上模型是 **ts-rs 导出的 tagged enum**(`agent/thread_manager/wire.rs`),不是"一个大结构体加一堆可空字段":`ThreadItem` 用 `itemType` 区分条目,`ThreadEvent` 用 `type` 区分事件,前端直接消费生成的 TS 类型(改完 Rust 模型跑 `cargo test export_bindings`)。条目上的毫秒时间戳标 `#[ts(as = "f64")]`,因为 ts-rs 默认把 `u64` 映射成 `bigint`,而 Tauri 的 JSON 通道传的是 `number`。
|
||||
- 运行态事件与历史切片使用同形条目,Rust 在两侧套同一套安全过滤(脱敏、截断、路径归一),前端只有一个「原始条目 → 视图」投影函数。
|
||||
- 两侧的过滤口径必须完全一致,包含「哪些条目根本不是本项目的聊天条目」:Codex app-server 回显的用户消息(`userMessage` / 非 AGC 的 `role=user`)在落盘侧被过滤,在运行态事件侧也必须被过滤(`direct_thread_visible_item`)。少一侧就会出现「实时比历史多出两条同文本用户条目、各自开出一个耗时 0 秒的假回合,重进页面又正常」这类只有其中一侧的事实源缺陷。
|
||||
- 搬运层不生成展示形状:Thread Manager 只下发脱敏原始条目(`itemType` 原样透传),工具卡片的 `kind`、标题、折叠摘要都由前端生成。
|
||||
@@ -35,7 +35,7 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
|
||||
- 执行通道断开同样是失败终态,也必须带 `failure`:连接级故障(app-server 进程退出 / stdout 流断 / JSON 行越界)与回合事件通道关闭都算,`kind="transport-failed"`、`message` 用宿主当场写下的那份诊断(含 `exitStatus` 与 stderr 摘要,已脱敏截断)。宿主在检测到连接终止的第一时间把这条事实记到本回合的执行适配器上,终态判定再从适配器读:执行适配器的看门狗盯着同一个 `closed` 标志,用调用点局部变量会输给这场调度竞争,失败原因就只剩日志、界面只会看到"本轮已结束"。判据是"适配器是否已由宿主主动关闭"——宿主自己收束(正常终态 / 用户主动停止 / 预算与交付收尾)走的是同一个 `TransportClosed` 事件,但这些不算失败。
|
||||
- 终态由**事实**判定,不由收尾阶段反推:判定按优先级取「宿主当场记下的失败(通道断开 / 等待超时 / app-server 单方面中断)→ 本回合的错误结果是 Err → 只有收尾阶段的账本读不出来时才用交付报告」,**有载荷一定写 `status="failed"`**,没载荷才用收尾阶段推出来的 `status`。收尾会把 ledger 阶段推成 `Interrupted`,让阶段决定终态就会把已经失败的一轮讲成"已结束"。模型自报失败(原生 `turn/completed.status="failed"` 的 `error`,带 `codexErrorInfo` 分类)不为载荷新增输入字段:宿主把原生 `error` 的 `codexErrorInfo` 解析成 typed 分类后当作本回合的错误结果,走同一条通道进载荷;交付报告只说明"收束到哪一步",不得顶掉原因。
|
||||
- 回合失败在宿主内部是 **typed** 的:`agent/direct_turn_error.rs` 的 `DirectTurnError` 每个变体自带字段(并发拒绝带两个 invocation id、模型失败带分类、超时带撞的是哪条上限、通道断开带宿主诊断),**调用级拒绝**(这一轮没有开始)与**回合级失败**(这一轮已开始并被判失败)不共用判据,分流只认 `is_turn_failure()`。判据不再对原因文本做子串匹配,`LlmError` 只在平台层入口出现一次(`DirectTurnError::from_model_call`)。线上载荷 `{kind, message}`、命令边界字符串与 CLI 返回值都由这一个出口投影出来,Rust 侧任何地方都不再解析它们。
|
||||
- 分页锚点取原始条目 id;一次翻页操作在前端自动连拉,直到出现可显示条目或 `hasMore=false`,上限 5 页。
|
||||
- 分页锚点取原始条目 id;一次翻页操作在前端自动连拉,直到出现可显示条目或 `hasMore=false`,上限 5 页。(**按 2026-10-02 ADR 补充**:更早历史的触发时机、加载指示、失败重试与视口锚定改由 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](./【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md) 规定;本条的分页锚点口径与连拉上限仍然有效。)
|
||||
- `notify` 是唯一唤醒来源:`subscribe` 的 bootstrap 事件本身就是该 subscriber 此刻要处理的事件(游标已在队尾),前端直接 reduce 它们,不需要为了取这批事件再补一次 `consume`,之后完全由 `notify` 驱动,不设低频 tick 或任何轮询兜底。唯一例外是回执竞态:Rust 侧一注册完 subscriber 就开始 `notify`,前端却要等回执才知道自己的 `subscriptionId`,这段窗口内的通知只能记成欠账,回执到达后立刻补一次 `consume` 取回,否则该回合的尾部事件会卡在队列里等一个可能永不出现的下一次通知。
|
||||
- 迁移按一次干净切换落地:不做灰度、不做运行时开关、不双跑;允许提交序列里存在「新源已启用、旧代码尚未删除」的中间窗口,禁止反向的「新源未启用、旧源已删」。
|
||||
- 思考过程与工具活动同样从运行态事件与历史条目推断,界面展示保持不变。
|
||||
@@ -43,7 +43,7 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
|
||||
- 思考正文以 `item.delta{kind:"reasoning"}` 流式下发(`item/reasoning/summaryTextDelta` 与 `item/reasoning/textDelta`)。这不放宽可见范围:同一段文本本来就已落进 `project.jsonl` 并在 `item.completed` 展示;plan 文本与命令输出仍只降级为活动状态。
|
||||
- 首屏历史由 `subscribe` 返回的 `lastCompletedItemId` 锚定,再取最近切片;删除返回整份对话的历史命令。锚点是切片**新端(较新一侧)的边界且含该条**(命令参数 `throughItemId`):比锚点更新的条目只从运行态事件来,历史切片与实时流因此不重叠;向后翻页仍用切片返回的 `firstItemId` 作为 `beforeItemId`(不含锚点)。订阅回执到达之前不读首屏,也不退化成"取文件尾"(那会把回执之后才完成的条目也拉进历史)。
|
||||
- 生命周期锚点独立于 replay 队列保存(队列会回收 `cleanable` 事件,新订阅的游标又在队尾,回收后无法反推"最新回合是 started 还是 completed"),`subscribe` 必须返回最新的一条 `turn.started` / `turn.completed`,否则新订阅无法判定回合是否仍在运行。
|
||||
- 前端工具卡片形状是 `Omit<GameCreatorDirectToolCall, 'turnId'>`:聊天卡片不再有回合身份,`tool-calls.jsonl` 的持久化形状仍保留 `turnId`(DirectRuntime 的账本没动)。
|
||||
- 前端工具卡片形状是 `Omit<GameCreatorDirectToolCall, 'turnId'>`:聊天卡片不再有回合身份。
|
||||
- 未识别 item 类型由 Rust 原样透传(只带类型与身份,Rust 侧留 TODO),当前由前端投影丢弃:哪些类型可见属于前端决策,不回 Rust 加白名单。
|
||||
- 删除范围包含前端对 `read_direct_turn_stream`、`read_direct_tool_calls`、`read_direct_project_history`(整份历史)与 `game-creator-direct-turn-update` 事件的调用;保留分页用的历史切片读取(`read_direct_project_history_slice`),且该切片从文件尾反向扫描。`list_game_creator_direct_active_turns` 有意保留:它服务首页跨页面的「运行中的项目」列表,不是聊天框读路径。
|
||||
- 前端删掉 `directTurnStream` / `directToolCalls` / 活动回合快照接管 / 瞬时应答文本这些并行状态,聊天视图只由 reducer 状态投影(含工具卡片)。
|
||||
@@ -53,13 +53,12 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
|
||||
|
||||
## 备选方案与取舍
|
||||
|
||||
1. **保留 `tool-calls.jsonl` 作为"读侧已脱敏"缓存**:省一次脱敏与截断,但它成为与项目对话历史并行的第二事实源,卡片状态与顺序会和实时事件分叉。选择按读取期投影,必要时在进程内缓存。
|
||||
1. **保留一份"读侧已脱敏"的持久缓存**:省一次脱敏与截断,但它会成为与项目对话历史并行的第二事实源,卡片状态与顺序会和实时事件分叉。选择按读取期投影,必要时在进程内缓存。
|
||||
2. **保留 Direct 回合事件作为实时传输**:迁移量小,但同一段文本仍有两条实时链路,reducer 必须处理互相覆盖,正是本次要消除的问题。
|
||||
3. **让后端分页按"可显示消息数"切片**:界面能少写循环,代价是 Rust 需要理解 UI 可见性,界面规则一变就要同步改后端。
|
||||
|
||||
## 影响
|
||||
|
||||
- 旧项目磁盘上遗留的 `turn-stream.jsonl` / `tool-calls.jsonl` 保留不动,不迁移、不清理、不再由 DirectProject 聊天框读取。
|
||||
- 工具卡片的脱敏与截断必须在读取期执行一次,不能因为"原始条目已在磁盘"就把未脱敏内容直接渲染到界面。
|
||||
- 回合结束语义务必由 `turn.completed` 判定(失败时同一事件带 `failure` 载荷,不新增事件类型);缺少该事件的残留回合不得被渲染成运行中。
|
||||
- 两条已知边界,都**不**在本次补路径,且已被 [`【ADR】DirectProject命令接单化-2026-09-23`](./【ADR】DirectProject命令接单化-2026-09-23.md) 取代(§3、§2):① 宿主进程被强杀(`kill -9`)时没有任何 `Drop` 会执行,但队列随进程消失,新进程的订阅 bootstrap 因此不会看到"有开始没结束",界面不会卡在忙碌态;② `turn.started` 之前的失败按发生位置分流——接单**之前**的是拒单,根本不产生回合(不写用户条目、不写失败诊断),接单**之后**的由这一轮的占用对象统一收口成 `turn.completed`,不存在"有回合却没有事件解释"的路径。
|
||||
@@ -69,5 +68,5 @@ AGC 项目开发聊天框当前同时从三处取数据:Direct 回合事件(
|
||||
- **调用级拒绝**(同一 `clientTurnId` 并发复用 / 项目已有另一条回合在跑 / 权限策略拒绝 / 目录锚不定 / 输入校验 / 环境与凭据未就绪)不属于回合失败:这一轮没有开始,只把原因回给命令边界(界面出运行错误横幅),不写失败诊断、不发 `failed` 事件、不进交付报告。此前它们与回合失败混在同一层、共用同一份错误文本,现在分流只认 typed 判据。
|
||||
- 「活动回合的唯一判据」约束的是**原生回合**:界面上的「本地已发出、原生还没认领」是投影的展示态(`DirectChatTurn.state = 'awaiting-start'`),由本地在途用户条目身份派生,不构成第二套原生生命周期,也不参与 `turnRunning` 的判定。
|
||||
- 三层数据流、变量归属与一次发送的时序写在代码里:`apps/ai-game-creator-shell/src/view/project-development/chat/controller/useDirectProjectChatController.ts` 的模块注释;回合三态的定义与判据真值表在 `apps/ai-game-creator-shell/src/view/project-development/chat/conversation/directTurnPresentation.ts` 的 `DirectChatTurnState`。改判据时同步这两处与对应测试。
|
||||
- 验收证据是端到端行为,不是单元测试:回合进行中杀掉应用进程后重开项目,应看到部分文本与工具卡片按原顺序出现且不显示忙碌;正常结束后重进应与实时渲染一致;文件系统不得再新增 `turn-stream.jsonl` / `tool-calls.jsonl`。
|
||||
- 验收证据是端到端行为,不是单元测试:回合进行中杀掉应用进程后重开项目,应看到部分文本与工具卡片按原顺序出现且不显示忙碌;正常结束后重进应与实时渲染一致。
|
||||
- id 空间已用源码核对:codex-rs `app-server-protocol/src/protocol/thread_history.rs` 中所有工具 item 都是 `id: payload.call_id.clone()`,而 `project.jsonl` 落盘的是原始 response item。真实 app-server 会话核对仍列为运行时验收项。
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 【ADR】DirectProject对话滚动与历史自动加载-2026-10-02
|
||||
|
||||
状态:已接受
|
||||
|
||||
## 背景
|
||||
|
||||
DirectProject 聊天区(`apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/`)当前的滚动与历史体验有三个问题:
|
||||
|
||||
1. 更早历史要靠常驻按钮「显示更早的对话」(`.message-history-more`)手动拉,但 `handleScroll` 里已经有一条「滚到顶部 24px 以内就自动加载」的路径——按钮是半冗余的第二入口,还没有任何加载中的视觉反馈(`historyLoadingRef` 是 ref,渲染不出来)。
|
||||
2. 用户滚上去之后没有任何「回到最新」的入口,只能自己一直滚到底。
|
||||
3. 展开一折叠块(`<details>` 或 `[aria-expanded]`)时浏览器保持 `scrollTop` 不变,新展开的正文长在视口下方,用户必须再滚一次才能看到;反过来在底部展开时内容直接顶出可视区。
|
||||
|
||||
同一目录下的 `PlanningChatView` 复用 `.project-chat-message-list` / `.project-chat-conversation` 两个类名,所以任何滚动容器结构改动都必须留在 DirectProject 本地,不能改公共类。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 更早历史:删掉按钮,自动加载 + 内联加载行
|
||||
|
||||
- 删除「显示更早的对话」按钮与 `DirectProjectConversation` 里的条件渲染;`.message-history-more` 样式**保留**——策划对话(`PlanningChatView`)仍在用同一套类名与按钮,等它一起改造时再删(见「影响」的后续项)。
|
||||
- 触发一(滚动):`scrollTop <= 24` 且 `historyHasMore` 且不在加载中且没有失败记录时自动加载。
|
||||
- 触发二(填充视口):首帧之后内容填不满视口(`scrollHeight <= clientHeight`)时继续加载,直到填满或 `hasMore=false`;不允许出现「历史比视口短、又没有按钮」的死局。
|
||||
- 两个触发都不越过既有的首屏订阅锚点 `lastCompletedItemId`;一次加载仍最多连拉 5 页(口径见 [`【ADR】DirectProject对话历史单一事实源-2026-09-16`](./【ADR】DirectProject对话历史单一事实源-2026-09-16.md))。
|
||||
- 加载中在列表最上方(比最旧一条回合更靠上)挂载一行 `role="status"`、`aria-live="polite"` 的「正在加载更早的对话」,带旋转圈;延迟 150ms 才显示,加载结束即卸载。它按需挂载,靠位置补偿(见第 2 条)保证下面的消息不跳。
|
||||
- 失败:挂起自动加载,列表顶部保留一行内联错误行——`role="alert"` 只包住「加载更早对话失败」文案本身,重试是可聚焦按钮、留在 live region 之外(assertive + atomic 的 live region 里不放交互控件);**不自动重试**,只有点重试(或切换项目)才重新开始;重试成功后错误行消失。
|
||||
- 一次加载与它所属的**世代**绑定:切换项目或新起一次读取都推进世代号(`historyLoadTokenRef`),旧世代落地时整段失效——不合并条目、不写游标、不关加载态。只比项目路径不够:A→B→A 之后在飞的旧读取又落回同一个路径,原守卫放行,会把新一代的加载行与 `historyLoadingRef` 这道并发闸门一起改掉。换项目的推进放在**渲染期**(与 `projectPathRef` 同一处),不放在复位 effect 里:passive effect 走宏任务、promise 续体走微任务,旧读取可能在「切换提交完成、复位 effect 还没跑」的窗口里落地,那时世代号还是旧的,守卫会放行。
|
||||
|
||||
### 2. 前插锚定:回合 key + 块身份 + 偏移,关掉浏览器原生锚定
|
||||
|
||||
- 锚点 = 顶部可见块的 `data-turn-key` + `data-block-key` + 相对列表顶边的像素偏移。
|
||||
- 合并更早历史前记录锚点,合并后按同一 key/块身份把偏移还原,较早内容出现在上方而用户正在读的位置不动。
|
||||
- 块身份来自 `DirectChatBlock.key`(条目块是 `${回合 key}:${条目 itemId}`,本地说明块是 `messageId`),只要求**在同一回合内唯一**。
|
||||
- 不按块序号定位:`DirectProjectTurn` 的 `renderTurnProcess` 对运行中的回合把过程块**平铺**、对已结束的回合把它们折进一个 `<details>`,收口时整个回合的块序号都会后移(`[用户, 工具组1, 工具组2]` → `[用户, 执行过程, 工具组1, 工具组2, 终态正文, 终态文案]`),序号锚点会解析到隔壁块并按错误基准写 `scrollTop`,表现为一帧跳动。
|
||||
- 锚点块没有布局盒时(被折进**收起**的 `<details>`、或已从列表移除)`restoreTurnAnchor` 返回 false:调用方据此放弃这次补偿,拿当前可见位置重新起锚,不写 `scrollTop`(不回跳,也不假装对齐)。同理,读锚点时跳过没有布局盒的块——它既不是「用户正在读的位置」,也还原不出来。
|
||||
- 列表上加 `overflow-anchor: none`,关掉浏览器原生 scroll anchoring,保证只有一套补偿在跑(不做「两套补偿交替生效」)。
|
||||
- 一次加载期间(`historyLoading` 为真)所有补偿——ResizeObserver 回调、加载行挂载、历史合并——都还原同一个冻结锚点;加载结束再刷新锚点。
|
||||
- 块集合(列表里所有同时带 `data-turn-key` 与 `data-block-key` 的节点,即所有可锚定的块)在列表子元素没变时复用同一份缓存,缓存由子元素增删(与 `ResizeObserver` 重订阅共用同一个 `MutationObserver`)失效:否则一次滚动帧里读锚点、还原锚点会各查一遍整份列表,长会话下就是 O(块数) 的滚动抖动。
|
||||
|
||||
### 3. 回到底部胶囊
|
||||
|
||||
- 列表底部居中的悬浮胶囊(`sticky`),跟着滚动容器走、不随内容滚走。
|
||||
- 距底部超过 48px 时出现,文案「回到底部」;用户不跟随时来了新的终态内容就改成「有新回复 · 回到底部」。
|
||||
- 点击:平滑滚到底部 + 恢复跟随最新 + 清除「有新回复」,随后按钮自行消失。
|
||||
- 平滑滚动期间滚动位置归这次程序化滚动所有:滚动事件不再翻转「跟随最新」,布局补偿也不写 `scrollTop`(写一次就会取消动画并把画面拉回原处,表现为「点了只下去一屏、到不了底」)。滚到贴底阈值即交还控制权;用户中途用滚轮 / 触摸 / 键盘打断则立刻交还,不会卡住后续跟随。
|
||||
- 动画期间内容变高(流式正文、图片撑开)时,点击瞬间记下的 `scrollHeight` 已经不是底部:补偿不写 `scrollTop`,而是把动画目标重新对准新的底部。否则动画停在旧目标上、等不到「贴底」那次滚动事件,`programmaticScrollRef` 不会交还——跟随与布局补偿整段挂起,而 `scrollToBottom` 已把胶囊按「已贴底」隐掉,用户停在底部之上却没有任何指示与自动跟随。
|
||||
|
||||
### 4. 跟随最新与折叠展开
|
||||
|
||||
- 距底部 ≤ 48px 视为「在底部」,即跟随最新;用户手动滚回去(进入阈值)就自动恢复跟随,不需要额外开关。
|
||||
- 用 `ResizeObserver` 观察列表的直接子元素(`MutationObserver` 负责在子元素变化时重订阅)——任何高度变化:
|
||||
- 跟随时 → 贴底;
|
||||
- 不跟随时 → 还原顶部可见回合锚点(用户正在读的那条折叠头留在原位);若刚刚展开的那个块头部仍然可见,则按展开前记录的屏幕位置把它冻结,并把新展开的正文露出来:正文底边超出视口就只滚到刚好露出底边,正文比视口还高则对齐正文顶部;
|
||||
- 收起(正文高度归零)不额外滚动,只冻结头部。
|
||||
- 新一回合开始(`turnInFlight` 由假转真)时强制恢复跟随并贴底,替代原先由视图直接写 `shouldFollowLatestRef` 的做法。
|
||||
|
||||
### 5. 结构与样式
|
||||
|
||||
- 滚动所有权(列表 ref、跟随最新、ResizeObserver、锚点还原、胶囊可见性)搬进 `DirectProjectConversation` 这一层;`DirectProjectChatView` 只传加载状态与回调,不再持有 `messagesRef` / `shouldFollowLatestRef` / `handleScroll`。
|
||||
- 控制器对渲染层暴露可渲染的 `historyLoading` 与 `historyError`(外加 `retryEarlierHistory`),取代只存在于 ref 里的加载标志。
|
||||
- 新增元素全部用内联 Tailwind 工具类,不改 `styles.css`;列表本身仍保持 `message-list project-chat-message-list` 类名不变,只追加 `overflow-anchor` 工具类。
|
||||
|
||||
### 6. 换会话时复位滚动所有权
|
||||
|
||||
- 列表容器在切项目时**不会重挂载**:`DirectProjectChatView` 在 `App.tsx` 只有一处渲染、没有 `key`,内部的 `DirectProjectConversation` 也没有 `key`,而 `useConversationScroll` 的跟随最新 / `atBottom` / `hasNewReply` / 前插锚点 / 折叠头 / 程序化滚动标记只在挂载时初始化一次。控制器自己按项目重置了历史状态,滚动所有权却一直漏着。
|
||||
- 症状:在项目 A 往上滚过再切到 B——① B 的首屏不贴底,用户得自己往下滚;② B 的第一批回合会在「不跟随」分支被算成新内容,胶囊在新项目上直接显示「有新回复 · 回到底部」,可用户根本没在 B 里离开过底部。
|
||||
- 决策:把会话身份(`conversationKey`,DirectProject 传项目路径)作为**显式信号**传给 `useConversationScroll`;身份变化时复位上述滚动所有权状态、把终态指纹同步成新会话内容、再贴底。不改变组件生命周期,同一个项目重开(身份不变)也不会被当成新会话;将来策划对话复用同一个 hook 时用的是同一套信号。
|
||||
|
||||
## 备选方案与取舍
|
||||
|
||||
1. **保留按钮 + 只加自动加载**:加载中仍靠按钮做唯一反馈,且删掉按钮后失败路径没有补救入口;按钮本身与滚动自动加载重复。
|
||||
2. **在列表外面套一层 viewport 做浮层定位**:`PlanningChatView` 共用同一套容器规则,且工作台里 `.project-chat-conversation` 是 `display: block` + `height: 100%` 几何,套一层就会让 `height: 100%` 的列表塌成内容高度;改公共类会连带策划对话。改用列表内的 `sticky` 胶囊,零结构改动。
|
||||
3. **只依赖原生 CSS scroll anchoring**:前插能免费对齐,但做不到「跟随时展开要贴底」,也无法在加载期间冻结同一套锚点;因此显式补偿 + 关闭原生锚定。
|
||||
4. **展开后总是把正文滚进视口**:对正文比视口矮的折叠块会把画面大幅上移,打断正在读历史的用户;采用「跟随时贴底 / 否则冻结折叠头 + 只滚到刚好露出新展开正文的最小位移」。
|
||||
5. **平滑滚动期间照常处理滚动事件与布局补偿**:程序化滚动会被应用自己的补偿打断(第一次写 `scrollTop` 即取消动画),用户点了「回到底部」也停在半路;因此改为滚动期间冻结这两条路径。
|
||||
6. **自动重试失败的历史加载**:弱网下会反复打接口,且用户看不出到底在重试还是在挂起;改为挂起 + 内联重试行。
|
||||
7. **用 `key={projectPath}` 让 React 重建会话列表**:改动最小,但整份消息列表连同加载行的 150ms 延迟计时一起重建,而且「同一个项目重开」也会被当成新会话;改用显式身份信号复位,重建与否与「换没换会话」解耦。
|
||||
8. **锚点存块序号(`index`)而不是块身份**:序号在「回合 running→finished」时会因过程被折进 `<details>` 而整体后移,解析到隔壁块;改成稳定块身份只多一层 `data-block-key`,且块不可见仍要单独判失败,所以「失效锚点」省下的那点改动不值得留下错位。
|
||||
9. **锚点块被折进收起的 `<details>` 时把还原改成找同回合内其它可见块**:会按另一条消息的基准写 `scrollTop`,比放弃这次补偿更糟;放弃只表现为不自动对齐,不回跳。
|
||||
|
||||
## 影响
|
||||
|
||||
- 历史加载失败不再只写顶部状态行,而是落到列表里的内联错误行;顶部状态行仍保留首屏读取失败等其它用途。
|
||||
- 滚动是表现层行为,正式状态仍在后端投影与运行态事件;本 ADR 不新增领域概念。
|
||||
- 阈值(触顶 24px、贴底 48px、spinner 150ms)是可按手感调整的常量,集中放在 `components/DirectProjectConversation/conversationScrollPolicy.ts`。
|
||||
- 验收:纯函数与 jsdom 组件测试覆盖阈值、锚点还原(含收口后按块身份仍指向同一块、锚点块被折叠隐藏时放弃还原)、加载/错误行、胶囊文案与显隐;滚动观感(顶部加载圈、胶囊出现与消失、底部展开回贴、历史前插不跳、长回合收口时视口不跳、切项目后首屏贴底且不误报「有新回复」)必须真机手动验收——jsdom 没有布局。
|
||||
- 明确的后续项(不在本次范围):`PlanningChatView` 与 `App.tsx` 遗留 `message-history-more` 路径的同款改造、未读条数徽标、Playwright 端到端。
|
||||
@@ -0,0 +1,143 @@
|
||||
# 【ADR】游戏游玩次数计数
|
||||
|
||||
状态:已接受(2026-10-03)
|
||||
|
||||
## 背景
|
||||
|
||||
网站游戏分发已经展示「X 次游玩」:目录卡片、详情、作者「我的游戏」与后台「游戏管理」都读
|
||||
`game_distribution_game.play_count`,契约里也有 `playCount`。但这条链路只读不写——游戏创建时置 `0`,
|
||||
之后没有任何自增、reducer 或 procedure;因此所有游戏恒显示 `0` 次游玩。
|
||||
|
||||
主规范要求「不虚构评分、玩家数、排名和收藏状态」,里程碑与证据矩阵以「新游戏显示 0、目录数据来自真实
|
||||
接口」作为无假统计的证据,却从未定义「一次游玩如何累加」。相关现状:
|
||||
|
||||
- **触发信号不存在**:游玩页 `/games/play` 先给封面与「开始游戏」,点击后才挂
|
||||
`sandbox="allow-scripts"` 的 iframe;`startGame()` 纯本地状态,不发任何请求。AGC 客户端 12 类埋点白名单
|
||||
不含「开始游玩」,主站路由埋点只有查看统计。
|
||||
- **发行网关不能承担计数**:`serve_release_entry` / `serve_release_asset` 只按 `gameId` 服务当前公开版本
|
||||
文件;一次加载会打多个资源请求,且按设计禁带平台 Cookie、拿不到会话身份。
|
||||
- **平台没有匿名身份**:没有匿名访客 cookie,web 端没有 device/client id;`x-client-instance-id` 只在登录
|
||||
链路采集。
|
||||
- **没有现成的服务端缓冲管线可复用**:AGC 埋点管线是客户端本地 15 分钟一批、服务端同步原子入库且明确
|
||||
不做聚合;真正的「内存缓冲 + 周期 flush + 关停 flush」范式是主站路由埋点 `tracking_outbox`。
|
||||
- **部署与限流现状**:api-server 单实例;无 CSRF/Origin 中间件;应用层只有并发背压,按 IP 令牌桶在独立
|
||||
的 `pingora-gateway`;现有缓存惯例是 std `OnceLock`/`LazyLock` + `Mutex`(无 `dashmap`/`moka`)。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 触发点 = 游玩页点「开始游戏」
|
||||
|
||||
以用户点击「开始游戏」后的前端上报作为一次游玩。理由:这是产品定义的启动动作,游客可用;`iframe load`
|
||||
只代表文档加载(规范已明确不能当业务状态),发行网关无法区分会话且会因资源请求重复。点击后即使 iframe
|
||||
超时/未真正载入也计一次(用户意图)。
|
||||
|
||||
### 2. 落点 = 复用现有 `play_count`
|
||||
|
||||
`game_distribution_game.play_count` 已经是 `u64`、已进公开/后台 DTO、已进四处 UI。只新增写入路径,不改
|
||||
字段语义、不新建计数表,避免双源。计数跟随游戏身份,不随发行版本。
|
||||
|
||||
### 3. 范围 = 只做累计总次数
|
||||
|
||||
不做日粒度 / 近 7 天热度、独立玩家数、榜单、推荐。`public_work_play_daily_stat` 属已退役自定义世界口径,
|
||||
不复活。
|
||||
|
||||
### 4. api-server 纯内存缓冲 + 周期 flush
|
||||
|
||||
- **纯内存,不落盘**:崩溃、被杀和正常 SIGTERM/滚动重启都允许丢最后一个 flush 窗口;进程退出不做
|
||||
force flush,关停路径不为计数等待网络(2026-10-04 修订,见「修订记录」)。
|
||||
- **两张表**:增量表 `pending: HashMap<gameId, u64>`(5 秒级、flush 即清)与 30 分钟去重窗口表 `seen:
|
||||
HashMap<identity + gameId, timestamp>`(30 分钟级、按 TTL 清理)。两者键不同、生命周期差 360 倍,不能合并:
|
||||
合并会把 30 分钟窗口状态塞进 5 秒清空的表,或让 flush 需要按 gameId 重新聚合。
|
||||
- **限流表**:`rate: HashMap<ip + gameId, (windowStart, count)>` 固定窗口。
|
||||
- **并发**:请求路径只在短锁内做 HashMap 命中 + 自增;**flush 的网络调用移出锁外**。
|
||||
|
||||
### 5. 写入形状 = 批量 procedure
|
||||
|
||||
一次 flush 发一个 procedure,入参 `Vec<{gameId, delta}>`(按 500 分块),事务内逐条
|
||||
`play_count = play_count.saturating_add(delta)`;procedure 内**原子校验**游戏当前为 `published` 且
|
||||
`active_version_id` 存在,非公开跳过。**不 bump `updated_at`**:它只表示公开资料变更,且作者自有列表按它
|
||||
排序,bump 会让每次游玩重排作者列表。
|
||||
|
||||
### 6. 失败语义 = 少计优于双计
|
||||
|
||||
只重试确定未发出的 `Build`;`Timeout` / `ConnectDropped` 无法判断是否已提交,直接丢弃该批并 `warn!` 记录
|
||||
丢量。理由是:超时后重试会在"其实已提交"时造成系统性双计,而丢弃只是偶发少计——对一个非交易展示指标,
|
||||
后者更可接受(perf 优先于 correctness)。
|
||||
|
||||
一次 flush 按 500 分块;任一分片失败即终止本次 flush 的后续分片,剩余增量直接丢弃(`Build` 只把当前分片
|
||||
放回)。连接不通时剩余分片只会重复同样的失败,逐个重试会把 worker 卡在多次连接超时上(2026-10-04 补充)。
|
||||
|
||||
### 7. 接口
|
||||
|
||||
`POST /api/game-distribution/games/{game_id}/plays`,公开端点(可选 bearer):
|
||||
|
||||
- 不挂 `require_bearer_auth`;用 `optional_access_token_from_headers` 拿可选 `userId`。
|
||||
- **不加 `Idempotency-Key`**(与其它游戏分发写路由惯例不同):高频计数用不上幂等收据,30 分钟去重窗口就是
|
||||
护栏。
|
||||
- 非公开 / 下架 / 封禁返回 `404` 且不计数;被限流返回 `429`;成功统一 `200 {recorded: bool}`。
|
||||
- 前端 fire-and-forget,**任何失败静默、绝不阻断游玩**;不做发行网关兜底计数。
|
||||
|
||||
### 8. 身份与去重键
|
||||
|
||||
登录用 `userId`;匿名用前端 `localStorage` 持久随机 `clientId`(随请求体带上);两者都缺失时回退
|
||||
`IP + UA`。30 分钟窗口按 `identity + gameId`。不新造匿名 cookie、不依赖登录后才有的设备指纹。`IP`
|
||||
取 nginx 覆盖写入的 `X-Real-IP`(无 CDN 时即真实 TCP 对端),不取可伪造的 `X-Forwarded-For` 首段;
|
||||
限流键与微信支付下单的 `payer_client_ip` 同源(2026-10-04 补充)。
|
||||
|
||||
### 9. 落位 = 纯持久化 / 读模型
|
||||
|
||||
procedure 在 `spacetime-module`,facade + mapper 在 `spacetime-client`,缓冲 / worker / 端点在
|
||||
`api-server`;`module-game-distribution` 不动(自增不是领域规则,与既有「点赞计数」直接落持久化流程同构)。
|
||||
新增 procedure 不改表,schema guard 不触发,但必须 `npm run spacetime:generate` 重生成绑定并同步文档。
|
||||
|
||||
### 10. 展示一致性 = 接受滞后
|
||||
|
||||
读路径(目录 / 详情)继续只读 DB,不叠加内存 pending;接受「flush 间隔 + 写库」的 ≤10 秒滞后。
|
||||
|
||||
## 影响与代价
|
||||
|
||||
- 计数非实时,最多一个 flush 窗口的滞后;崩溃 / kill 丢最后一个窗口;模糊传输错误少计。
|
||||
- 匿名 `clientId` 可被清除 / 伪造,指标定位为展示用次数,仅靠限流兜底。
|
||||
- 单实例前提:去重 / 限流窗口不跨实例;将来多实例时各实例自行 flush(加法幂等),窗口不共享。
|
||||
- 不 bump `updated_at`,作者自有列表排序、公开修订 CAS 均不受影响。
|
||||
- 新增 procedure 只改 ABI(绑定),不改表 schema / `migration.rs`。
|
||||
|
||||
## 备选方案与取舍
|
||||
|
||||
1. **发行网关服务端计数**:一次加载多资源请求会重复、禁 Cookie 拿不到会话、无法去重。已否决。
|
||||
2. **复用 AGC 客户端埋点管线**:服务端同步入库且明确不做聚合,客户端 15 分钟批次,白名单不含游玩。已否决。
|
||||
3. **落盘 / outbox 保可靠**:无持久性需求(崩溃丢窗口已接受),引入磁盘与独立目录要求。已否决。
|
||||
4. **单张 map / 不做去重**:会重复计数;去重状态与增量生命周期不同。已否决。
|
||||
5. **每个游戏一次 procedure**:N 次 WebSocket 往返。已否决。
|
||||
6. **模糊失败重试**:超时已提交时系统性双计。已否决。
|
||||
7. **匿名 HttpOnly cookie / 纯 IP 去重**:前者要新造 cookie 与跨端 / 沙箱处理,后者在 NAT 下把多人并成一人。
|
||||
已否决。
|
||||
|
||||
## 明确不做
|
||||
|
||||
- 日粒度 / 近 7 天 / 独立玩家数 / 榜单 / 推荐。
|
||||
- AGC 客户端界面埋点、外部 API / External OpenAPI 扩展。
|
||||
- 服务端网关兜底计数、CSRF token、应用层全局按 IP 限流(沿用既有限流与网关能力)。
|
||||
- 幂等收据表、双计补偿、跨实例窗口共享。
|
||||
|
||||
## 落地与验收
|
||||
|
||||
- 实施边界:`spacetime-module` 新增批量自增 procedure 与 `SpacetimeType`;`spacetime-client` facade +
|
||||
mapper;`api-server` 新增计数模块(增量 / 去重 / 限流 / flush worker)、AppState 接线与公开
|
||||
端点;前端 `gameDistributionClient` 增 `recordGamePlay` 并在 `startGame()` 触发。
|
||||
- 权威文档同步:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`(路由表与游戏分发合同节)、
|
||||
`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`(`game_distribution_game` 的 procedure 与
|
||||
写入口径)。
|
||||
- 验收判据:新游戏 `0` → 游客点一次 ≤10s 内显示 `1`;同身份 30 分钟内重复点击不增、不同身份各 `+1`;
|
||||
超限流 `429` 且不写;下架 `404` 不计数但历史值保留;SIGTERM 关停允许丢最后一个未落库窗口;计数接口任何失败都
|
||||
不影响游玩页;相关 Rust / 前端定向测试与 schema / DDD / 绑定 / DTO parity / 编码 / doc-index 门禁全绿。
|
||||
|
||||
## 修订记录
|
||||
|
||||
- 2026-10-03:初版。
|
||||
- 2026-10-04:关停不再强制 flush(原「正常 SIGTERM/滚动重启必须在 `finalize_shutdown` 内 force flush」
|
||||
作废)。理由:关停时最后一个窗口丢失概率极低,而强制 flush 需要把 worker 生命周期接进关停顺序并为在途
|
||||
网络写入等待;按"perf 与简单优先"取舍,直接放弃该窗口。同日明确 flush 任一分片失败即丢弃剩余分片。
|
||||
- 2026-10-04:客户端 IP 解析改为优先 nginx 覆盖写入的 `X-Real-IP`,`X-Forwarded-For` 只作回退且取最后
|
||||
一段(nginx 用 `$proxy_add_x_forwarded_for` 追加的真实对端),不再信任可伪造的首段——公开上报端点原来
|
||||
用它做匿名身份与限流键,可被伪造 IP 绕过并灌水。无 CDN 前置时 `X-Real-IP` 即真实客户端。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02
|
||||
|
||||
状态:已接受
|
||||
|
||||
## 背景
|
||||
|
||||
AGC(`apps/ai-game-creator-shell`)长期同时存在两套「Agent 执行」:正式对话面已经收敛到 DirectProject 单容器(Rust `agent/direct_*` + codex app-server),而 `src-tauri/src/agent/runtime_*`、`agent/prompt.rs`、`agent_native_tools.rs`、`collaboration.rs`、`delegation.rs`、`goal.rs`、`context_compaction.rs`、`isolated_agent.rs`、`tool_plan_handoff/` 这一整套自建 Runtime(runtime driver / protocol / tools / actions / state)仍在编译、测试、注册 Tauri 命令、并以 `project-supervisor` 身份承担自主构建与专业 Agent 编排。
|
||||
|
||||
这套 Runtime 的入口只剩历史沉淀:前端没有任何渲染或调用入口(`ProjectSupervisorView`、Supervisor 运行态面板与专业 Agent 对话已在 2026-09-22 前后退役),`resume/confirm/read_game_creator_agent_runtime*`、`chat_with_game_creator_agent`、`*_agent_goal`、`schedule_game_creator_agent_ready_tasks`、`start_game_creator_supervisor_runtime_task` 等命令没有现役调用方;真实链路只由 `scripts/agent-runtime-real-e2e.mjs`、`agent-runtime-steer-real-e2e.mjs`、`smoke-agent-run-local-provider.mjs`、`llm-transient-fault-proxy.mjs` 等专用 harness 与它自己的 Rust 测试覆盖。外部 Runner(`--agent-runner`)里也跟着背了一份项目 execution-owner、known-roots 和 `runner.status` / `runner.shutdown_if_idle` 的 CLI 面,但其中真正仍在服役的只有编辑器桥与 manifest relay。
|
||||
|
||||
保留它的代价持续存在:一个没有用户入口的执行栈要求每次改动都同步维护四类东西——Rust Runtime 内核与专属测试、Tauri 命令注册与 `scripts/check-config.mjs` 白名单、真实 e2e harness 与 CI 预热 job、以及多份权威文档里按「现役」描述的协议与预算。
|
||||
|
||||
## 决策
|
||||
|
||||
- 自建 Agent Runtime 执行面整体退役,按「从未存在」处理:实现、专属测试、Tauri 命令注册、构建期门禁条目、真实 e2e harness、CI job 与文档承诺一并删除,历史由 Git 保存,不保留 feature flag、双跑路径或墓碑注释。
|
||||
- Rust 删除范围:`agent/runtime_driver/`、`agent/runtime_protocol/`、`agent/runtime_tools/`、`agent/runtime_actions/`、`agent/runtime_state.rs`、`agent/runtime_adapter.rs`、`agent/prompt.rs`、`agent_native_tools.rs`、`collaboration.rs`、`delegation.rs`、`goal.rs`、`context_compaction.rs`、`isolated_agent.rs`、`provider_handoff.rs`、`provider_retry.rs`、`tool_plan_handoff/`、`user_input.rs` 及其 `src/tests/` 下的专属用例与 fixture;`agent/generation/` 只保留现役生成路径(画布 / 资源生成、prompt 上下文装载、pass artifact 落盘与 trace),`generation/run_lifecycle.rs`、`generation/role_briefs.rs`、`generation/tests.rs` 一并删除。
|
||||
- Tauri 命令退役:`control_agent_run`、`generate_local_game_draft`、`chat_with_game_creator_agent`、`chat_with_game_creator_role_agent(_stream)`、`start/read/edit/pause/resume/clear_game_creator_agent_goal`、`start_game_creator_supervisor_runtime_task`、`compact_game_creator_agent_runtime_context`、`cancel/retry/confirm_retry/confirm/reject_game_creator_agent_runtime_task`、`answer_game_creator_agent_runtime_user_input`、`read_game_creator_agent_runtime(s)`、`resume/confirm_resume_game_creator_agent_runtime_tasks`、`schedule_game_creator_agent_ready_tasks` 全部移出 `desktop.rs` 的 `generate_handler!` 与 `check-config.mjs` 白名单;`agent` 会话命令(`list/create/fork/set_active/archive_game_creator_agent_session`)继续保留。
|
||||
- CLI 退役:`--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` / `--agent-goal-*` / `--agent-task` / `--agent-runner-status` / `--runner-shutdown-if-idle` 与其 CLI 解析、配置目录要求、测试一并删除;`CliCommand` 收敛为 `LlmStatus | EnvironmentCheck | PreviewServe`。`--agent-runner` 模式与 `--gui-owner-required` 保留。
|
||||
- 外部 Runner 收缩:`runner/project_owner.rs`、项目 execution-owner / known-roots 注册表、`runner.status`、read-only configure、跨启动 owner 认领与 `shutdown_if_idle` 的项目级语义全部删除;Runner 现在只剩编辑器桥 RPC(`*.editor.rpc` / `*.editor.ack` / `*.editor.mark_uncertain`)与 `runner.attach_gui_owner` + GUI owner 参与锁 / watchdog。`runner.rs` 顶部保留 `TODO(retire-runner)` 记录「等编辑器执行收进 GUI 进程后可整体退役」的规划。
|
||||
- 前端退役:删除 `agentRuntimeById` 状态与 `read_game_creator_agent_runtimes` / `resume_game_creator_agent_runtime_tasks` / `confirm_resume_game_creator_agent_runtime_tasks` 调用链、`features/agent-runtime/model.ts` 中只服务 Runtime 投影的归一化/合并/格式化函数、`app/types.ts` 的 `AgentRuntime*` / `AgentGoal*` / `GameCreatorAgentRuntimeUpdateEvent` 类型、`AgentStatusCard` 的 `runtime*` 字段与 `features/project-summary/agentPresentation.ts` 的 `projectAgentRuntimeSummaries` / `formatAgentCardRuntimeStatus`,以及 `onAgentRuntimeSummariesChange` / `activeProjectAgentRuntimeSummaries` 在 app-shell、WorkspaceLauncher、`view/project-development` 与 `App.tsx` 的透传。
|
||||
- harness 与 CI 退役:删除 `apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/`、`agent-runtime-real-e2e.mjs`、`agent-runtime-steer-real-e2e.mjs`、`smoke-agent-run-local-provider.mjs`、`llm-transient-fault-proxy.mjs` 与 `tests/llmTransientFaultProxy.test.ts`;`scripts/check-native-shells.mjs` 去掉 `agc-rust-smoke` 分组,`.gitea/workflows/project-ci.yml` 去掉 `ai-game-creator-shell-rust-smoke` job,root / App `package.json` 去掉全部 `agent-*` / `agent-runtime:*` / `agent-run:smoke` 脚本,缓存维护脚本同步去掉对应 job 名。
|
||||
|
||||
## 备选方案与取舍
|
||||
|
||||
1. **只删前端入口,保留 Rust Runtime 与命令**:命令与 Runtime 内核继续编译、测试、进白名单,等于把「没有用户入口的执行栈」永久固化,正是本次要消除的成本。
|
||||
2. **保留 Runtime 作为「本地 CLI 能力」**:`--agent-*` 与 `runner.status` 只服务真实 e2e harness,没有产品路径;保留它就要继续维护 harness、CI job 和缓存预热,收益为零。
|
||||
3. **保留兼容别名或 feature flag**:没有现役调用方、公开契约或持久化数据需要兼容,兼容层只会把死词汇表留在解析与注册层。
|
||||
4. **连带退役编辑器桥与 `--agent-runner`**:编辑器 RPC、回执确认、不确定执行 fence 与 Windows 作业对象隔离目前仍依赖独立进程托管,一次性搬进 GUI 是独立的较大重构(见 `runner.rs` 的 `TODO(retire-runner)`),不在本次范围。
|
||||
|
||||
## 影响
|
||||
|
||||
- 产品可见行为不变:正式对话、资源工作台、生成与编辑器链路本来就不经过自建 Runtime;删除后项目运行态由 DirectProject 自己的订阅与缓存(`projectResourceLiveUpdateModel` 等)持有,专业 Agent 状态卡片只从 manifest + run trace 推导。
|
||||
- AGC Rust crate 的编译面与测试面显著缩小(删除 140 个源文件),`cargo check` / 分片测试不再需要 Runtime fixture、Goal sidecar、Supervisor 协作与 tool-plan handoff 用例。
|
||||
- 术语收敛:「Runtime」在 AGC 里此后指 DirectProject / codex app-server 执行面,「Supervisor」不再是正式运行身份;后续文档与注释不得再按现役描述自建 Runtime。
|
||||
- 新增门禁边界:`scripts/check-config.mjs` 的 native-only 白名单不得再收留已删命令;退役概念不新增守卫测试或字符串钉桩,防止回归依靠「没有解析层 / 没有注册」的架构边界。
|
||||
- 保留项:AGC 会话命令、项目权限策略词汇表、DirectProject 的 `enqueue_direct_codex_turn` / `cancel_direct_codex_turn` 链路、编辑器桥(`runner.attach_gui_owner`、participant lock、watchdog)与 `--agent-runner` 模式均不受影响。
|
||||
|
||||
## 验证
|
||||
|
||||
- `cd apps/ai-game-creator-shell/src-tauri && cargo check --tests --bin genarrative-ai-game-creator-shell`、`cargo test --bin genarrative-ai-game-creator-shell runner::`。
|
||||
- `cd apps/ai-game-creator-shell && node scripts/check-config.mjs`、`npx tsc -p tsconfig.json --noEmit`。
|
||||
- `npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts` 与 `npx vitest run scripts/project-ci-workflow.test.ts`。
|
||||
- `git diff --check`、`npm run check:encoding`。
|
||||
@@ -43,6 +43,10 @@
|
||||
{
|
||||
"name": "Agent Integration",
|
||||
"description": "远程 MCP、OpenAPI 和完整 Skill 包发现"
|
||||
},
|
||||
{
|
||||
"name": "Payment",
|
||||
"description": "平台统一收款与 Web 收银台"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
@@ -786,6 +790,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",
|
||||
@@ -1574,6 +1618,52 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/external/v1/payment/orders": {
|
||||
"post": {
|
||||
"tags": ["Payment"],
|
||||
"operationId": "createExternalPaymentOrder",
|
||||
"summary": "创建支付订单并返回收银台地址",
|
||||
"description": "使用支付应用 API Key 创建平台统一收款订单。支付金额按整数分传入;支付成功必须以平台通知或服务端查单为准。",
|
||||
"security": [{"PaymentApiKey": []}],
|
||||
"parameters": [{"$ref": "#/components/parameters/IdempotencyKey"}],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/PaymentOrderCreateRequest"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "支付订单与收银台信息",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentOrderCreateResponse"}}}
|
||||
},
|
||||
"400": {"$ref": "#/components/responses/BadRequest"},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"502": {"$ref": "#/components/responses/UpstreamError"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/external/v1/payment/orders/{orderId}": {
|
||||
"get": {
|
||||
"tags": ["Payment"],
|
||||
"operationId": "getExternalPaymentOrder",
|
||||
"summary": "查询支付订单",
|
||||
"security": [{"PaymentApiKey": []}],
|
||||
"parameters": [{"name": "orderId", "in": "path", "required": true, "schema": {"type": "string"}}],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "支付订单",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentOrderCreateResponse"}}}
|
||||
},
|
||||
"401": {"$ref": "#/components/responses/Unauthorized"},
|
||||
"403": {"$ref": "#/components/responses/Forbidden"},
|
||||
"404": {"$ref": "#/components/responses/NotFound"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -1582,6 +1672,11 @@
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "tnr_sk"
|
||||
},
|
||||
"PaymentApiKey": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "tnr_pay"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
@@ -1707,6 +1802,47 @@
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"PaymentOrderCreateRequest": {
|
||||
"type": "object",
|
||||
"required": ["merchantOrderId", "title", "amountCents"],
|
||||
"properties": {
|
||||
"merchantOrderId": {"type": "string", "minLength": 1, "maxLength": 96},
|
||||
"title": {"type": "string", "minLength": 1, "maxLength": 128},
|
||||
"items": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
|
||||
"amountCents": {"type": "integer", "minimum": 1},
|
||||
"currency": {"type": "string", "default": "CNY"},
|
||||
"provider": {"type": "string", "enum": ["wechat_native"], "default": "wechat_native"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PaymentOrder": {
|
||||
"type": "object",
|
||||
"required": ["orderId", "appId", "merchantOrderId", "title", "items", "amountCents", "currency", "provider", "checkoutToken", "status", "checkoutUrl", "createdAt", "expiresAt", "updatedAt"],
|
||||
"properties": {
|
||||
"orderId": {"type": "string"},
|
||||
"appId": {"type": "string"},
|
||||
"merchantOrderId": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"items": {"type": "array", "items": {"type": "object", "additionalProperties": true}},
|
||||
"amountCents": {"type": "integer", "minimum": 1},
|
||||
"currency": {"type": "string"},
|
||||
"provider": {"type": "string", "enum": ["wechat_native"]},
|
||||
"providerTradeNo": {"type": ["string", "null"]},
|
||||
"checkoutToken": {"type": "string"},
|
||||
"status": {"type": "string", "enum": ["pending", "paying", "paid", "expired", "closed", "refunded"]},
|
||||
"checkoutUrl": {"type": "string"},
|
||||
"providerQrCode": {"type": ["string", "null"]},
|
||||
"createdAt": {"type": "string", "format": "date-time"},
|
||||
"expiresAt": {"type": "string", "format": "date-time"},
|
||||
"paidAt": {"type": ["string", "null"], "format": "date-time"},
|
||||
"updatedAt": {"type": "string", "format": "date-time"}
|
||||
}
|
||||
},
|
||||
"PaymentOrderCreateResponse": {
|
||||
"type": "object",
|
||||
"required": ["order"],
|
||||
"properties": {"order": {"$ref": "#/components/schemas/PaymentOrder"}}
|
||||
},
|
||||
"ExternalEditorProjectCreateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
- 资源总览的“资源依赖 / 资源类型”视图切换使用连通的分段按钮组,相邻选项共享边界并保持唯一选中语义。每个分段都必须有清晰的键盘焦点指示,焦点环不得被分段容器的圆角或 `overflow` 裁切。
|
||||
- 右侧 Supervisor 对话中,用户消息使用右对齐、最大宽度受限的主题暖色气泡,assistant 消息保持左对齐;消息换行不得产生水平溢出,执行过程卡继续占满消息区可用宽度。消息列表必须约束在右侧对话列内并独立滚动,不得覆盖中央资源或运行视图;提交按钮必须保留随状态变化的可访问名称。气泡正文在 light / dark 平台主题下均须满足 WCAG AA 普通文本 `4.5:1` 对比度。
|
||||
- 客户端正式产品仍只按最小 `1280×720` 横屏合同交付,并保留 `1280×800` 默认窗口与既有基线验收;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。
|
||||
- 工作台顶部播放按钮紧贴「资源管理 / 运行」模式切换之后、整组左对齐;不再在桌面视窗中水平居中悬浮;两个页签下都常驻(运行视图空态与预览失败态要靠它重跑)。资源管理视窗触发播放时直接切换到运行视窗并启动本地预览,不再弹出 `game.run_local` 二次确认。
|
||||
- 工作台顶部工具条固定**一行到底**,**两枚体量最大的分组控件分居这一行两端**:左端「资源管理 / 运行」模式分段,右端「依赖 / 类型」排序分段;中间依次是动作按钮与版本入口,**版本入口紧邻排序分段左侧**(不放在最右)、固定显示 `版本 N`,空间够时补上括号里的「创建原因 + 时间」。「运行」页签本身就是播放入口(页签前带 ▶ 图标;切过去即启动本地预览,不再单开一枚「播放」按钮,也不再弹出 `game.run_local` 二次确认;运行视图空态与预览失败态靠再点一次该页签重跑,不可运行时页签不置灰、点了不切视图并出提示)。**站在运行视图上却没有画面时自动补发一次载入**(典型场景:从别的项目切过来,工作台不重挂,运行视图原样留下而画面已经没了),同一个项目只自动补一次,画面已在就不重复启动。空间不足按固定顺序降级:先只留 `版本 N`,再把「打开项目目录 / 资源面板 / 整理画布」收进「更多」下拉——任何宽度都不把控件甩到第二行,右端始终由「依赖 / 类型」收住。运行画面右下角提供「刷新运行画面」:游戏预览不是实时刷新,改完代码需要重载页面。
|
||||
- 创建模式素材画布的“素材名称”是用户可编辑的正式输出名称;“资源用途”是 manifest subtype,不向普通用户开放自由文本。新增资源默认“普通游戏美术”,可从普通游戏美术、统一视觉规范、游戏界面原型、核心美术图集四项中选择。图片精修继承源名称和用途,不显示创建模式保存设置;候选图片只从选中图片的“设为最终图”提交。精修顶栏只保留返回、导入、定位当前最终图、撤销和重做,删除进入图片上下文工具栏,通用 AI 生成只保留给创建模式。图片输出统一使用 PNG;创建模式工具动作与保存设置分层展示,“保存到项目”在 `1280×800` 和窄容器中都必须完整可见。
|
||||
- 首页创作输入区与“最近项目”之间不展示共享项目状态文本,“最近项目”标题下也不追加解释性副标题;默认、成功、进行中或失败状态均不得在该位置形成文字行,项目管理页继续保留自己的状态反馈。
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
|
||||
- 位置与层级:画布左下角(`left: 14px; bottom: 14px; z-index: 40`)。右下角是既有的缩放 / 撤销 Dock(`right: 14px; bottom: 14px`),左下角是画布上唯一两者都不占的稳定空位。工具栏是管理区 `.game-resource-book-manager` 的**直接子节点**、与画本场景并列,不进带 `scale()` 的场景层;二级菜单与「入口不可用原因」都贴着工具栏上沿弹出,不做内嵌内容。
|
||||
- 外壳用共享 chrome(`packages/image-canvas-react` 的 `CanvasToolbar / CanvasToolbarGroup / CanvasChromeButton`),样式落在 AGC 的 `resourceCanvasChrome.css`;共享包只承接通用表现,不含业务规则。
|
||||
- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
|
||||
- 接线:图片类入口走本地 IPC `start_local_project_asset_generation`(`kind` ∈ `image / character / spec / icon-spec / ui-prototype / art-spritesheet`;**提交即返回任务记录**,生成由 Rust 后台任务跑完写回项目,进度用 `list_local_project_asset_generations` 读回项目内账本 `.agent/runtime/asset-generation-tasks/tasks.json`);音频入口也走 `start_local_project_asset_generation`(同一份项目内任务账本;`kind` = `sound-effect` / `background-music`,并额外携带该次生成的请求身份 `idempotencyKey`,任务 id 即该次生成的 operation id;生成仍复用既有音频无源生成链路,`generationMode: 'create'`、`editKind` = `sound-effect` / `background-music`,不新增平台路由与请求体口径);「上传」复用 `upload_local_asset`,并带上当前栏目 `targetCategory`(上传的 manifest `kind` 只由内容证据推导,图片 / 视频 / 代码都派生成 `unclassified`;不带入口栏目素材就落进「待归类」、在上传它的那一栏里看不见,Issue 359)。生成 / 上传成功后一律用「配对读 `(revision, manifest)`」交给 `onManifestChange`,走既有 manifest 刷新与资源投影链路,不重算依赖图、不另写布局。
|
||||
- 本地排队与进度可见:AGC 本地 durable 输出槽已按**精确动作指纹**分槽(不同 prompt / 素材名各自独立成槽,具备并行能力),但本批前端仍按「同一时刻只派发一条」排队——真并行派发需要并发收口设计(配对读 + manifest CAS + 聚焦意图互不覆盖),留待下一批;所以第一条未终态时第二条提交停在**前端本地队列**里(不调用提交 IPC,显示本地排队的「排队中。」),前一条终态后自动补发;任务状态与阶段文案(后端 `phaseDetail`)由任务账本提供,前端不拼阶段、不做百分比。进度面是**画布上常驻的可折叠任务侧栏**(位置与开合形态照抄网页端美术画布的任务侧栏的右上角锚点,颜色与外形仍走 AGC 平台 token;2026-09-21 由「左侧贴边 + 工具条入口」改为「右上角锚点 + 常驻开关」):展开是两个分栏「排队/生成中」与「已完成」(各带条数,「已完成」封顶 20 条 + 列表滚动 + 高度有界),关闭入口只保留头部那一枚 ×(底部重复的关闭按钮与其分割线已删除)、折叠即整块让出画布、只留那一枚右上角开关(工具条上不再有重复入口),开合只走画布右上角那一枚「生成任务 · N」开关(两个页签下都在;**展开后开关让位、只留面板**,收起走面板头部 × 或点画布外部);锚点是画布那一格网格里的条目(不是写死 `top` 的绝对定位),工具条换行变高也不会压上去;每项显示状态徽标 / 阶段文案 / 已耗时 / 素材名,可「定位到素材」。侧栏非模态(不铺全屏遮罩、不做焦点陷阱、不参与模态遮挡判据),位置在画布右上角锚点里(照抄美术画布那一处)、**覆盖式**(不 reflow 挤窄画布视口),提交受理后自动展开。锚点按工作面分档:资源栏目画布与 UI 编辑器用画布顶边那一档,运行表现层下移让开右上角的版本入口;锚点是**画布那一格网格里的条目**(不是写死 `top` 的绝对定位),所以工具条换行变高也不会压上去。定位动作**每次点击都终局化**:能定位就定位并选中;素材在别的栏目先切栏目;不在投影里给「素材已不在项目里 / 已登记但尚未同步」的结论;3 秒内有界兜底,不允许提示条永久停在「正在定位生成的素材…」。
|
||||
- 面板形态:独立浮层(`ThemedModal`),**不在当前面板下面追加内容**;面板内不写功能说明或规则解释文案。**点「生成」即同步关闭面板**(不等 IPC、不等排队、不等生成),面板里**不出现**「排队中。」「正在生成。」「提交中…」这类阶段文案——阶段文案的唯一去处是任务侧栏与工具栏提示条。**只有「点击瞬间就失败」**(校验不过、权限拒绝、start IPC 立即报错)才自动重开面板并带回草稿与原因;**受理之后才失败**只在侧栏把该任务收口为失败 + 原因,不重开面板。关闭 ≠ 取消请求(请求挂在任务与账本上,不挂在面板生命周期上)。
|
||||
- 参数口径:比例 / 尺寸选项来自网页端美术画布的纯模型(`src/components/image-editor/ImageCanvasGenerationModel.ts`),并按本地 IPC 白名单收窄(本地通道明确拒绝 `4:3`);默认档 `1:1 · 1K`,生成 UI 设计图沿用网页端 UI 设计面板的默认 `16:9 · 1K`。本地 IPC 没有 `model` 入参,因此面板**不渲染模型选择器**(渲染一个改不了请求的控件就是假控件)。
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# 【实施计划】AGC 发布版本以工程内部版本为准
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】AGC发布版本以工程内部版本为准-2026-10-03.md` |
|
||||
| Status | proposed |
|
||||
| Owner | Genarrative Agent |
|
||||
| 关联 | PR #565(`feat/game-works-management`);取代 `【里程碑/实施计划】AGC已发布游戏版本更新-2026-10-02` 中「用户可编辑发行版本标签」的口径 |
|
||||
|
||||
## 修改边界(不动的东西)
|
||||
|
||||
- 不改 `server-rs/crates/{spacetime-module,module-game-distribution,api-server,spacetime-client}`:服务端 `versionNumber` 语义与 `publicationRevision` CAS 保持不变。
|
||||
- 不改 `apps/admin-web/**`、不改 `src-tauri/src/project/export.rs`(两者正被其它 worker 修改)。
|
||||
- 不删除本地字段 `projectVersion`(Rust DTO 带 `deny_unknown_fields`:`server-rs/crates/shared-contracts/src/game_creation_app.rs:1121`)。
|
||||
|
||||
## 修改顺序
|
||||
|
||||
1. 文档:主规范改写 → 本里程碑 → 本实现计划 → `decision-log` 条目 → 两份 2026-10-02 计划文档的取代标注。
|
||||
2. 契约:TS 解析器改为内部版本派生。
|
||||
3. Rust:拆掉两条 `project_version` 写链路 + 删除手改命令。
|
||||
4. 渲染层:面板改只读派生、删失焦写回与 `onManifestUpdated`。
|
||||
5. service:删保存函数、改默认值与注释。
|
||||
6. 测试改写 + 门禁全跑。
|
||||
|
||||
## 逐条改动(文件 → 位置 → 改什么 → 验收)
|
||||
|
||||
| # | 文件 | 位置 | 改什么 | 验收 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | `packages/shared/src/contracts/gameCreationApp.ts` | `:1001-1013` | `resolveGameCreationAppProjectVersion` 去掉 `binding` 形参,返回 `versions.length > 0 ? versions.length : GAME_CREATION_APP_FIRST_PROJECT_VERSION`;注释改为「唯一来源 = 工程内部版本」 | `npm run agc:typecheck`;面板/服务用例 |
|
||||
| 2 | 同上 | `:1034-1037` | `projectVersion?: number \| null` 保留,注释改「遗留位:旧清单兼容读,不再读写」 | 静态检查 |
|
||||
| 3 | `server-rs/crates/shared-contracts/src/game_creation_app.rs` | `:1107-1118`、`:1090-1096` | 删 `resolve_game_creation_app_project_version` 与 `GAME_CREATION_APP_FIRST_PROJECT_VERSION` / `is_game_creation_app_project_version`;对应单测改为「字段被忽略」 | `cargo test -p shared-contracts` |
|
||||
| 4 | 同上 | `:1146-1150` | `project_version` 字段保留,注释改「遗留:仅为旧清单可解析,不再读写」 | 同上 |
|
||||
| 5 | `apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs` | `:24-27` | 去掉解析器 import | `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml game_distribution_publish` |
|
||||
| 6 | 同上 | `:536-541`(`write_publication_binding`) | 删除 `manifest.project_version = Some(resolve(...))`,改为置 `None`;保留 `manifest.publication = Some(binding.clone())` | 同上 + `jq` 自检 |
|
||||
| 7 | 同上 | `:1531-1535`(发布成功回写) | 删除 `manifest.project_version = Some(version.version_number)`,改为置 `None`;保留 publication 回写 | 同上 |
|
||||
| 8 | 同上 | `:944-965` | 删除 `update_game_distribution_project_version` 命令 | 编译 + `rg` 零命中 |
|
||||
| 9 | `apps/ai-game-creator-shell/src-tauri/src/desktop.rs` | `:614` | 从 invoke handler 清单删除该命令 | 编译 |
|
||||
| 10 | `apps/ai-game-creator-shell/scripts/check-config.mjs` | `:138` | 从命令注册表删除 `'update_game_distribution_project_version'` | `npm run agc:typecheck` |
|
||||
| 11 | `apps/ai-game-creator-shell/src/services/gameDistributionPublish.ts` | `:148-167` | 删除 `saveGameDistributionProjectVersion` | typecheck + 用例 |
|
||||
| 12 | 同上 | `:329-337`、`:362-363` | 注释改为「唯一来源 = 工程内部版本」;默认值 `args.versionNumber ?? resolveGameCreationAppProjectVersion(args.manifest)` | 同上 |
|
||||
| 13 | `apps/ai-game-creator-shell/src/components/game-distribution/GameDistributionPublishPanel.tsx` | `:129-136`、`:283-288`、`:331-332`、`:397-403`、`:683-708`、`:731-734`、`:804` | 删 `parseProjectVersion`、`projectVersion`/`savedProjectVersion` state、`projectVersionSaveRequestRef`、`handleProjectVersionCommit` 与三处 setState;新增派生值 `useMemo(() => resolveGameCreationAppProjectVersion(manifest), [manifest.versions])`;提交改为 `versionNumber: projectVersion` | 面板用例 |
|
||||
| 14 | 同上 | `:947-1000` | 「项目版本」输入框改 `readOnly`(保留 `aria-label="项目版本"`),删 `onChange`/`onBlur`/`disabled`;提示语改为「来自 AGC 内部版本(版本 N)…」 | 面板用例 |
|
||||
| 15 | 同上 | `:242-243`、`:700` | 删除 `onManifestUpdated` prop 与唯一调用点 | typecheck |
|
||||
| 16 | `apps/ai-game-creator-shell/src/App.tsx` | `:1816`、`:1951` | 删除两处 `onManifestUpdated={setManifest}` | typecheck |
|
||||
| 17 | `apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx` | `:735-761`、`:778-823`、`:825-868` | 前两条改为「标签 = 内部版本数」;后两条(失焦写回 / 非法回退)整块删除;fixture `MANIFEST` 补非空 `versions` | vitest |
|
||||
| 18 | `apps/ai-game-creator-shell/tests/gameDistributionPublish.test.ts` | `:319-420` | 删 `saveGameDistributionProjectVersion` 用例块;默认值断言改为「无 `versions` → 1;N 条 → N」 | vitest |
|
||||
| 19 | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` | `:97`、`:99`、`:106` + 新增两条 | 见主规范新条款 | `npm run check:doc-index` |
|
||||
| 20 | `docs/project-memory/plans/【实施计划】AGC已发布游戏版本更新-2026-10-02.md` | `:11`、`:23` | 行内追加「被 2026-10-03 里程碑取代」标注 | 阅读 |
|
||||
| 21 | `docs/project-memory/plans/【里程碑】AGC已发布游戏版本更新-2026-10-02.md` | `:27-30` | 同上,逐条标注被取代/收窄 | 阅读 |
|
||||
| 22 | `docs/project-memory/shared-memory/decision-log.md` | 最新条目区 | 追加本决策条目 | `npm run check:doc-index` |
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
npx vitest run apps/ai-game-creator-shell/tests/gameDistributionPublish.test.ts \
|
||||
apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx \
|
||||
apps/ai-game-creator-shell/tests/gameDistributionPublishLive.test.ts
|
||||
npm run agc:typecheck
|
||||
cd server-rs && cargo test -p shared-contracts
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml game_distribution_publish
|
||||
npm run check:doc-index
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
# 静态断言:命令与字段来源都已拆干净
|
||||
rg -n "update_game_distribution_project_version" . # 期望 0 命中
|
||||
rg -n "projectVersion" apps/ai-game-creator-shell/src packages/shared/src # 期望仅剩遗留声明/注释
|
||||
```
|
||||
|
||||
## 数据迁移
|
||||
|
||||
- 无迁移脚本。读路径立即忽略 `projectVersion`;改动点 6/7 会在下一次「平台回读绑定 / 发布成功」时把它置空,`skip_serializing_if` 使该键随下次落盘消失。
|
||||
- 平台侧历史版本行不做任何改写(不可变留痕)。
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- 回滚点:只动 AGC 客户端 + 本地契约 + 文档;回滚 = 还原提交,服务端数据无副作用。
|
||||
- 旧客户端降级:清单里 `projectVersion` 被置空后,旧版 AGC 会回落到绑定 `latestVersionNumber`(旧行为重现),仅影响本机,需写 release note。
|
||||
- 派生标签会因「截尾删除版本」而回退(删素材连带删版本),平台接受该回退,但会产生「同一标签先后指向不同包」——这属平台现行契约允许的形态(用户手动重复提交同版本同样会产生)。
|
||||
|
||||
## 不做(本里程碑之外)
|
||||
|
||||
1. 「线上最近提交 vN」加「(平台)」字样(纯文案)。
|
||||
2. 平台历史版本号对齐(**明确不做**:不可变历史行无改写入口,会破坏审核留痕)。
|
||||
3. 后台/作者侧的「同标签多实例」可辨识列(落在 `apps/admin-web/**`,正被其它 worker 修改)。
|
||||
4. 「截尾删除致标签回退」的产品兜底(引入单调计数器 = 新字段 = SpacetimeDB schema 变更,须单独评审)。
|
||||
5. `edit-<operationId>` 形状的资源编辑版本是否计入内部版本序数(当前计入,与资源卡口径一致)。
|
||||
6. 旧客户端降级提示写 release note。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 【实施计划】AGC 已发布游戏版本更新
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】AGC已发布游戏版本更新-2026-10-02.md` |
|
||||
| Status | proposed |
|
||||
| Owner | Genarrative Agent |
|
||||
|
||||
## 修改边界
|
||||
|
||||
- `packages/shared/src/contracts/gameCreationApp.ts`:本地唯一 `projectVersion` 和 `publication` 绑定,不混入内部编辑迭代 `versions[]`。**(2026-10-03 取代:发布标签改为由工程内部 `versions[]` 派生、只读;`projectVersion` 降级为不再读写的遗留兼容位,见 `【里程碑】AGC发布版本以工程内部版本为准-2026-10-03.md`。)**
|
||||
- `packages/shared/src/contracts/gameDistribution.ts` 与 Rust shared-contracts:创建版本请求支持正整数 `versionNumber`,允许重复标签提交。
|
||||
- `apps/ai-game-creator-shell`:绑定回读、首次发布/更新 UI、项目版本输入、原 gameId 新提交、结果回写。
|
||||
- `api-server`、`spacetime-client`、`spacetime-module`:创建版本事务只校验正整数;同 gameId/同 versionNumber 生成新 versionId,publicationRevision 继续 CAS。
|
||||
- 现有创建版本 procedure 输入/生成绑定需要同步,但不修改持久表字段。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 契约先补唯一 `projectVersion` 和本地 publication,明确 versionId/版本标签/内部 projectRevision 的边界。
|
||||
2. 创建版本事务不再要求大于 max;未传时保留旧客户端自动递增,传入时校验正整数,并按同版本最新有效提交处理 pending 替代。
|
||||
3. AGC 发布绑定保存 gameId、最新 versionId、状态和 revision;按账号和 API origin 隔离绑定。
|
||||
4. AGC 打开项目回读已有作品;旧项目通过 localProjectId 恢复关联。
|
||||
5. 发布面板编辑 projectVersion,区分首次发布/更新,允许重复提交和回退,不再存在独立 targetVersion。**(2026-10-03 取代:面板不再编辑版本标签,标签由工程内部 `versions[]` 派生且只读;仍区分首次发布/更新。)**
|
||||
6. Rust 发布 facade 在更新模式跳过 create game,使用原 gameId 创建新 version;幂等账本把目标 projectVersion、资料和提交意图纳入发布意图。
|
||||
7. 验证重复提交同版本、回退版本、响应丢失、切换账号、publicationRevision 冲突和旧公开版本继续可玩。
|
||||
|
||||
## 数据库影响
|
||||
|
||||
- 不新增表。
|
||||
- 不修改 `version_number` 字段类型或重排字段。
|
||||
- 修改创建版本 procedure 输入 DTO 与生成绑定;按当前 SpacetimeDB 门禁验证。
|
||||
- 老客户端不传版本号仍自动递增,已有版本和历史数据不需要回填。
|
||||
|
||||
## 验证
|
||||
|
||||
- 领域/数据库创建版本测试:默认递增、指定更高版本、重复/较小/零/负数/非整数/越界输入、并发相同目标号。
|
||||
- AGC 发布面板测试:首发、更新、默认目标版本、用户改号和恢复失败。
|
||||
- AGC Rust 发布测试:跳过 create game、原 gameId、新 version、结果持久化、幂等意图区分。
|
||||
- `npm run typecheck`、定向 Rust/Vitest、schema/生成绑定检查、编码与 diff。
|
||||
- 真机发布:首次发布 → 重开项目 → 修改游戏 → 指定更高版本 → 更新送审 → 审核后原公开链接运行新包。
|
||||
@@ -8,26 +8,26 @@
|
||||
|
||||
## 修改边界
|
||||
|
||||
- 允许修改:`agent/direct_thread_wire.rs`、`agent/direct_thread_manager.rs`、`agent/codex_app_server/`、`agent/direct_project_history.rs`、`main.rs` 命令注册、`src/features/project-workspace/generated/`(ts-rs 生成目录)、AGC 前端订阅与聊天投影、对应测试与 `docs/`。
|
||||
- 明确不修改:SpacetimeDB schema 与绑定、HTTP/OpenAPI、DirectRuntime 自己的进度事件与 `turn-stream.jsonl` / `tool-calls.jsonl` 写入、Codex durable thread 行为、审批弹层现有状态来源。
|
||||
- 允许修改:`agent/thread_manager/wire.rs`、`agent/thread_manager/mod.rs`、`agent/codex_app_server/`、`agent/direct_project_history.rs`、`main.rs` 命令注册、ts-rs 生成目录、AGC 前端订阅与聊天投影、对应测试与 `docs/`。
|
||||
- 明确不修改:SpacetimeDB schema 与绑定、HTTP/OpenAPI、Codex durable thread 行为、审批弹层现有状态来源;DirectRuntime 的进度事件与账本本轮不动(该账本随后已随 issue #553 整体删除)。
|
||||
- 保持 `.env` 未提交修改,不触碰个人配置。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. Rust 只搬运:`agent/direct_thread_wire.rs` 把 Codex 原始条目挑字段、脱敏、截断后下发,事件载荷与历史切片同形,不生成卡片形状;线上模型是 ts-rs 导出的 tagged enum(`DirectThreadItem` / `DirectThreadEvent` / bootstrap / consume / history slice),`at` 标 `#[ts(as = "f64")]`,改完模型跑 `cargo test export_bindings` 生成前端绑定。
|
||||
1. Rust 只搬运:`agent/thread_manager/wire.rs` 把 Codex 原始条目挑字段、脱敏、截断后下发,事件载荷与历史切片同形,不生成卡片形状;线上模型是 ts-rs 导出的 tagged enum(`ThreadItem` / `ThreadEvent` / bootstrap / consume / history slice),`at` 标 `#[ts(as = "f64")]`,改完模型跑 `cargo test export_bindings` 生成前端绑定。
|
||||
2. 条目身份归一:进队列前收敛成一个 `itemId`,事件 envelope 与前端形状里都不出现第二个 id 概念;历史切片的 `firstItemId` 继续取文件里的原始 item id。
|
||||
3. 删掉回合身份:`turn.started` 无载荷、`turn.completed{status}`,条目 / 增量 / 请求 / 生命周期锚点都不带 turn id;队列 `append` 直接收 `DirectThreadEvent`,`seq` 内部自算。
|
||||
4. 思考正文流式:`item/reasoning/summaryTextDelta` 与 `item/reasoning/textDelta` 产出 `item.delta{kind:"reasoning"}`;plan 文本与命令输出保持活动状态。
|
||||
5. 前端收敛为单一 reducer:`subscribe` 返回的 bootstrap 事件就是已暂存的运行态,游标已经在队尾,前端直接 reduce 这批事件即可(不需要为了拿这批事件再补一次 `consume`);此后只由 notify 唤醒 `consume`。唯一例外是回执竞态:Rust 注册完 subscriber 就开始通知,而前端要等回执才知道 `subscriptionId`,这段时间到达的通知只能记欠账,回执到达后立刻补一次 `consume`(否则整轮最后一个事件之后可能再无通知,事件会卡死在队列里)。合并规则只保留"先到定形、后到补空白"(正文只增不减、工具状态允许从 running 升级到终态),`item.delta` 直接追加到运行态条目正文,删掉 `deltaText` 缓冲,`turn.completed` 把运行态条目并入历史再清空。
|
||||
6. 首屏与分页:以 `lastCompletedItemId` 为锚点取最近切片,历史读取改为从文件尾反向扫描;切片的新端边界由这个锚点给出(含该条,命令参数 `throughItemId`),首屏读取等订阅回执里的锚点,回执到达前不发请求、也不退化成「取文件尾」;之后锚点按原始 item id 推进(`beforeItemId`,不含锚点)。一次翻页操作在前端连拉,直到合并后聊天投影出现新回合(新的用户气泡)或 `hasMore=false`,每个操作上限 5 页;锚点未推进(`items` 为空 / `firstItemId` 为 null / 与请求锚点相同)时立即停止。可见性口径只在 `features/project-workspace/directHistoryPaging.ts` 实现一份,首屏与「显示更早」共用。
|
||||
7. App.tsx 接线:订阅 + 立即 reduce bootstrap + notify 唤醒 consume,聊天视图改由 reducer 状态投影(含工具卡片),删除 Direct 回合事件订阅与 `directTurnStream` / `directToolCalls` 状态。
|
||||
8. 删除只服务旧读路径的命令与前端调用(`read_direct_project_history`、`read_direct_turn_stream`、`read_direct_tool_calls`),DirectRuntime 自己的写入保留。`list_game_creator_direct_active_turns` 是唯一的例外并有意保留:它服务首页跨页面的「运行中的项目」列表(`WorkspaceLauncher` / `directActiveTurns.ts`),不是聊天框读路径。
|
||||
8. 删除只服务旧读路径的命令与前端调用(`read_direct_project_history`、`read_direct_turn_stream`、`read_direct_tool_calls`)。`list_game_creator_direct_active_turns` 是唯一的例外并有意保留:它服务首页跨页面的「运行中的项目」列表(`WorkspaceLauncher` / `directActiveTurns.ts`),不是聊天框读路径。DirectRuntime 自己的账本写入本轮保留,随后已随 issue #553 整体删除。
|
||||
9. 测试与文档收口:补 reducer 单测、解锁跳过的工具卡片用例、更新主规范并把冲突的实施计划与工具卡片文档改写为当前状态。
|
||||
|
||||
## 验证命令
|
||||
|
||||
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 迁移)。
|
||||
|
||||
@@ -29,7 +29,7 @@ Milestone: `【里程碑】Direct回合跨页面生命周期与运行中项目
|
||||
|
||||
1. Rust:扩展活动回合注册表并暴露只读快照命令,配定向用例(进入 / 进度 / 终态移除 / 多项目并存)。
|
||||
2. 前端:接入快照读取,实现“重新进入项目 → 恢复忙碌态与进度 → 以快照 sequence 续接 → 阻止并发提交”。
|
||||
3. 前端:在窗口标题栏挂载“正在运行的项目”下拉入口;标题栏只显示最后开始的项目,展开后按开始时间列出全部项目,复用既有组件与设计 token。
|
||||
3. 前端:在窗口标题栏挂载项目入口;2026-10-04 修订为**工作台内主文案恒为当前项目名**(只在工作台视图发布这个身份,不在项目内时回落到最近启动的在跑项目),运行中项目只占圆点、数量徽标与展开菜单(展开后按开始时间列出全部在跑项目,当前项目带「当前」标记),复用既有组件与设计 token。原口径「标题栏只显示最后开始的项目」见里程碑文末修订记录。
|
||||
4. 报错归类:按审计结论修正会误导的映射,逐条加回归用例;真实权限拒绝保持原提示。
|
||||
5. 文档:主规范与共享记忆同步;里程碑验收后删除临时计划文件。
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Web 预检浏览器失败恢复实现计划
|
||||
|
||||
- Version: `1`
|
||||
- Status: `active`
|
||||
- Date: `2026-10-02`
|
||||
- Milestone: [`Web 预检浏览器失败恢复`](./【里程碑】Web预检浏览器失败恢复-2026-10-02.md)
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 将浏览器启动失败建模为带阶段、原因、子进程退出确认、清理确认的内部结果;Windows root spawn 后立即绑定 Job,并把绑定瞬间已经出现的子树逐 PID 纳入同一 Job,失败收束必须复用本轮 `BrowserProcessGuard`,并在成功收束时确认 Windows Job 为空。
|
||||
2. 在 Web 预检使用的一次浏览器启动入口加入一次有界恢复:仅 WS/CDP 瞬态失败且首次清理确认后创建新的临时目录/Profile 重试;其余失败直接失败关闭。
|
||||
3. 保留/加强 owner 与 sweep 的归属门禁,使用 Windows argv 解析核对完整 Profile 参数;无 owner、身份未知、PID 退出或复用均不得按猜测杀进程。
|
||||
4. 将启动失败和清理失败的安全诊断保留到预检 blocked 报告和 Tauri 错误中;稳定错误码独立于诊断文本,前端继续区分宿主阻塞与 IPC 故障。
|
||||
5. 补充 Rust 纯策略测试、清理边界测试和现有真实 Edge/Chromium ignored smoke;补充首页错误展示的定向测试。
|
||||
|
||||
## 验证命令
|
||||
|
||||
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell browser::`
|
||||
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell environment_check::web_creation::tests`
|
||||
- `npx vitest run apps/ai-game-creator-shell/tests/homeWebPreflight.test.tsx`
|
||||
- `npm run typecheck --workspace apps/ai-game-creator-shell`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
|
||||
可选真实环境证据:安装 Windows Edge 时运行现有 `real_browser_health_checks_can_run_concurrently` 及新增恢复 smoke;无浏览器时保持 ignored,不把缺失环境写成通过。
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
- Windows 进程命令行读取失败按不匹配处理,不执行杀进程;这可能留下临时目录,但保证不误杀。
|
||||
- 首次失败进程树收束未确认时不自动重试,避免第二个 Edge 与残留树并存;错误返回安全诊断。
|
||||
- 回滚点为浏览器启动恢复入口和 owner/sweep 归属校验,不触及 Web 预检的 Node/npm 或项目写入流程。
|
||||
@@ -0,0 +1,52 @@
|
||||
# 【实施计划】后台游戏审核详情与待审版本试玩
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】后台游戏审核详情与待审版本试玩-2026-10-02.md` |
|
||||
| Status | in-progress |
|
||||
| Owner | Genarrative Agent |
|
||||
|
||||
## 修改边界
|
||||
|
||||
### 允许修改
|
||||
|
||||
- `server-rs/crates/api-server`:管理员版本详情/预览会话/待审版本资源读取,以及现有发行资源公共逻辑的安全复用。
|
||||
- `server-rs/crates/shared-contracts`、`apps/admin-web/src/api`:审核详情、预览会话和资料字段 DTO。
|
||||
- `apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx` 及其测试:详情工作台、试玩入口、审核动作刷新。
|
||||
- 现有 admin 权限映射、发行 sandbox/CSP/storage 兼容逻辑的必要扩展。
|
||||
- 当前玩法主规范、里程碑证据和实施计划。
|
||||
|
||||
### 明确不修改
|
||||
|
||||
- `spacetime-module` 持久表结构、`migration.rs`、SpacetimeDB schema 和现有生成绑定,除非调研发现当前字段不足并重新获得迁移确认。
|
||||
- `/api/external/v1`、作者发布页面、公开游戏详情和用户评价数据模型。
|
||||
- 审核员分派、批量审核、精选推荐和后台游戏资料编辑。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 固化现有详情响应的字段映射与权限边界,先补 admin DTO 和契约测试。
|
||||
2. 定义版本绑定的短期预览会话与错误语义,复用发行 ZIP 路径白名单、大小限制、HTML 注入、CSP 和响应头。
|
||||
3. 实现管理员待审版本预览资源读取,确认公开发行路径不能读取未公开版本。
|
||||
4. 将审核列表升级为详情工作台:资料分组、发布者、版本摘要、封面/截图、试玩状态和审核动作。
|
||||
5. 补齐正常、拒绝、过期、越权、Cookie、缺文件、资源 404、CAS 冲突和幂等重放测试。
|
||||
6. 运行真实本地审核 smoke,确认打开的是待审 `versionId`,不是当前公开版本;完成后回写主规范并删除临时计划。
|
||||
|
||||
## 验证命令
|
||||
|
||||
1. `npm run typecheck`
|
||||
2. `npm exec -- vitest run apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx apps/admin-web/src/api/adminApiClient.test.ts --root .`
|
||||
3. `cargo test -p api-server game_distribution --manifest-path server-rs/Cargo.toml`
|
||||
4. `cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check`
|
||||
5. `npm run check:encoding`
|
||||
6. `npm run check:doc-index`
|
||||
7. `git diff --check`
|
||||
8. 本地 `npm run dev` + 管理员登录 + 待审版本详情/试玩 smoke。
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- **预览越权**:Token 必须绑定 `versionId` 和过期时间;错误实现会把私有待审包变成可枚举资源。回滚点是关闭预览会话路由,不影响公开发行网关。
|
||||
- **版本串播**:预览 URL 不能复用 `game.currentVersion.entryUrl`;详情和资源读取都必须携带并校验待审版本。回滚点是隐藏试玩按钮,保留资料审核。
|
||||
- **sandbox 放宽**:不得添加 `allow-same-origin`;如 storage 兼容层回归,回滚新增注入逻辑而不放宽 sandbox。
|
||||
- **资料漂移**:详情必须展示版本冻结快照;若快照缺失,只显示明确缺失状态,不静默用作者当前资料替代。
|
||||
- **审核并发**:继续使用 `publicationRevision` 和幂等键;CAS 冲突只刷新,不自动重放决定。
|
||||
- **API 兼容**:新字段可选,旧后台响应不能因缺少冻结资料而崩溃;预览接口失败不能影响公开游戏游玩。
|
||||
@@ -0,0 +1,48 @@
|
||||
# 【实施计划】后台游戏评价管理
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | [后台游戏评价管理](./【里程碑】后台游戏评价管理-2026-10-01.md) |
|
||||
| Status | implemented(本地验证通过,待用户验收;未部署) |
|
||||
| Owner | Codex |
|
||||
|
||||
## 修改边界
|
||||
|
||||
- 主规范:[后台游戏评价管理合同](../../【玩法创作】平台入口与玩法链路-2026-05-15.md#后台游戏评价管理合同);不扩大其四类筛选与单条管理范围。
|
||||
- 领域:`server-rs/crates/module-game-distribution/src/reviews.rs` 的管理动作/原因校验与有效评价统计规则。
|
||||
- 数据:`spacetime-module/src/game_distribution.rs` 追加隐藏字段、用户索引、新私有管理记录及读写事务;同步 migration、表目录与生成绑定。
|
||||
- 后端:`spacetime-client` facade/mapper、`shared-contracts`、`api-server/src/modules/game_distribution.rs` 路由与 handler,`admin.rs` 页面权限映射;不得使用新直接数据库访问路径。
|
||||
- 前端:admin-web 的路由/导航/账号权限/DTO/client/评价管理页及游戏管理跳转;网站评价 DTO、个人提示、摘要/列表读取。复用共享列表、表单、弹窗和后台写确认。
|
||||
- 文档:主规范、后端数据契约、决策记录和索引;不改 AGC、游戏发行审核或外部 OpenAPI。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 技术评审后将方案转为 ready;冻结原因校验、四类筛选、DTO及错误码。
|
||||
2. 追加默认 false 字段与索引、新管理记录表;事务实现隐藏/恢复/删除、创建时间目标核对及操作 key 重放,公共所有统计统一过滤,个人写保留隐藏状态。
|
||||
3. 同步迁移/绑定和 facade;新增后台列表、游戏选择、详情与管理 POST,接入独立 game-reviews 权限;普通用户投影不包含原因。
|
||||
4. 接入后台页、游戏入口、分页筛选、原因确认和详情内记录;网站个人状态加精确提示。请求重试复用同 key,不用旧响应覆盖新目标。
|
||||
5. 定向测试及真实运行时验证,包括旧 schema 带评价升级、删除后主键复用和后台/网站联动;回写证据和限制,交付验收。
|
||||
|
||||
## 验证命令与操作
|
||||
|
||||
- `cargo test --manifest-path server-rs/Cargo.toml --locked -p module-game-distribution`
|
||||
- `cargo test --manifest-path server-rs/Cargo.toml --locked -p api-server game_distribution`,另运行 admin 权限映射相关定向测试。
|
||||
- `cargo check --manifest-path server-rs/Cargo.toml --locked -p spacetime-module`
|
||||
- `npm run spacetime:generate`、`npm run check:spacetime-schema`、`npm run check:game-distribution-dto-parity`
|
||||
- Vitest 定向运行后台新评价页、路由/权限/client与网站 `GameReviews` 测试;以仓库 include 配置核对实际执行名单。
|
||||
- `npm run admin-web:typecheck`、`npm run typecheck`,受影响文件 ESLint/Prettier。
|
||||
- `npm run check:doc-index`、`npm run check:encoding`、`git diff --check`
|
||||
- 通过项目 dev 脚本启动隔离数据库及 `npm run dev:api-server` 并检查 `/healthz`;端口按运行状态核实,不使用人工 `spacetime --root-dir`。
|
||||
- 在隔离数据库先发布旧 schema 并写入评价,再兼容升级;核对旧评价全部公开且内容/时间不变。真实 HTTP 验证跨页过滤、隐藏/统计/恢复/删除、重新评价、旧 key 重试和权限。
|
||||
- 桌面与移动视口真实浏览器验证游戏管理跳转、长评论弹窗、原因必填/取消/失败,以及网站回读。缺失证据如实标记未验证。
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- 删除是不可恢复操作;复用确认交互,事务校验原创建时间和幂等操作 key,禁止旧请求作用于新评价。
|
||||
- schema 只兼容追加和新增私有表,不删字段/表或清空数据。发布冲突时停止,不通过 delete-data 绕过迁移。
|
||||
- 隐藏后的旧后端会重新暴露评价;产生隐藏记录后只能回滚到仍过滤隐藏状态的兼容实现,保留全部数据和操作记录。
|
||||
- 不引入前端内容版本锁、搜索服务或统计缓存;当前规模采用完整筛选后分页,不在有限首批数据上筛选造成漏查。
|
||||
|
||||
## 当前状态
|
||||
|
||||
已按上述边界完成实现;定向自动化、旧 schema 带 21 条评价无损升级、真实 HTTP 管理链路和桌面/375×812 移动视口验证通过。完整证据及未验证项见主规范“迁移与验收”。保留计划供用户验收;未部署。
|
||||
@@ -0,0 +1,52 @@
|
||||
# 【实施计划】支付应用与订单收银台
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】支付应用与订单收银台-2026-10-03.md` |
|
||||
| Status | completed |
|
||||
| Owner | Codex |
|
||||
|
||||
## 修改边界
|
||||
|
||||
允许修改:
|
||||
|
||||
- 主规范、shared-contracts、OpenAPI、支付领域 module、spacetime-module / migration / bindings、spacetime-client facade、api-server payment module、platform-wechat provider。
|
||||
- `src` 的 profile 支付入口、收银台页面、支付客户端和页面样式。
|
||||
- `apps/admin-web` 的支付 API 类型、路由、页面和后台样式。
|
||||
- 与本里程碑直接相关的测试和文档索引。
|
||||
|
||||
明确不修改:
|
||||
|
||||
- 现有个人钱包充值、微信虚拟支付和退款状态机的行为。
|
||||
- 生产商户密钥、环境文件、个人配置和构建产物。
|
||||
- 支付宝 provider、分账、提现、手续费和自动结算。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 从现有充值订单、微信 Native provider、外部 API Key、profile 路由和后台列表组件提取可复用边界,确认没有重复的公开订单模型。
|
||||
2. 冻结支付 DTO、状态枚举、错误码、幂等语义和 OpenAPI,然后补契约测试。
|
||||
3. 增加支付领域表、migration、procedure / facade 和 schema 绑定,先完成订单创建、查询、关闭与幂等。
|
||||
4. 在 `platform-wechat` 增加支付服务需要的 Native 请求 / 响应和通知确认复用,保持商户配置只在服务端。
|
||||
5. 在 `api-server` 接入外部 API、收银台 read model、微信通知、查单和外部回调 outbox;所有到账入口复用统一确认事务。
|
||||
6. 增加个人中心支付应用 / Key / 订单页面和公共收银台,按截图参考实现桌面双栏与移动端纵向布局。
|
||||
7. 增加后台支付概览、应用、订单、通知和回调投递页面,复用 `packages/shared` 后台公共组件。
|
||||
8. 运行定向测试、schema / OpenAPI / 编码检查,启动 api-server 做 healthz 和 mock provider smoke;条件具备时执行真实沙箱二维码 smoke。
|
||||
|
||||
## 验证命令
|
||||
|
||||
1. `npm run check:encoding`
|
||||
2. `git diff --check`
|
||||
3. `npm run check:spacetime-schema`
|
||||
4. `npm run typecheck`
|
||||
5. `npm run admin-web:typecheck`
|
||||
6. 支付领域和 api-server 定向 `cargo test`
|
||||
7. 外部 API、profile、收银台和 admin 页面定向 Vitest
|
||||
8. `npm run dev:api-server` 后检查 `/healthz`
|
||||
9. mock provider + Playwright 收银台 smoke;真实商户沙箱可用时再补动态二维码验证
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- 商户 API 证书或通知地址不可用时,保留 provider mock 证据,不把 mock 结果标成真实支付通过。
|
||||
- schema 变更失败时停止在绑定生成前,保留主规范和计划,不能通过删除数据绕过迁移。
|
||||
- 订单确认事务或回调 outbox 未完成时,关闭新支付渠道开关,不影响现有充值渠道。
|
||||
- 发现需要余额、提现、分账或支付宝真实接入时,先更新主规范和后续里程碑,再扩展实现。
|
||||
@@ -0,0 +1,81 @@
|
||||
# 【实施计划】游戏作品管理与客户端发布收口-2026-09-30
|
||||
|
||||
关联 Issue:`#470`「做一下创作者作品管理,从工具到平台的一键导出和发布」。
|
||||
分支:`feat/game-works-management`。
|
||||
|
||||
## 目标
|
||||
|
||||
在 10.7「陶泥儿游戏平台」上线前,把「创作者作品管理(用户侧 / 后台)+ 客户端打包上传(只保证 Phaser 4)」这条链路补齐到可用:作品能进来、能看得见状态、能管住。
|
||||
|
||||
- 发布管道(AGC 构建打包 → 8 MiB 分片上传 → 送审 → 后台审核 → 公开可玩 → 发行网关)已实现,并有真实栈证据(见[实施计划【游戏分发阶段A领域合同】](【实施计划】游戏分发阶段A领域合同-2026-09-19.md))。
|
||||
- 本轮已补齐:客户端 Phaser 4 + Vite 工程栈门禁、发布阶段/上传百分比、发布根幂等账本;Web 作者私有详情、状态/版本筛选、版本历史、资料编辑、软删除和作者操作;后台作品搜索、作者/状态筛选、游标分页与删除状态展示。
|
||||
- 生产发布开关仍默认关闭;真实客户端 GUI 发布、生产首个作品和发布包清理策略仍需运行时取证。
|
||||
|
||||
## 已落地(本分支)
|
||||
|
||||
| 项 | 落点 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| 作者读自己名下单个游戏详情 `GET /api/game-distribution/my-games/{gameId}` | `api-server/src/modules/game_distribution.rs`(`get_owner_game`、`load_owner_game_versions`、`owner_game_entry_payload`) | `cargo test -p api-server game_distribution::tests` → 26 passed;`owner_game_entry_payload_carries_private_versions_that_public_payload_omits` 断言作者条目带版本私有状态、公开投影不带 |
|
||||
| 版本私有状态补 `entryUrl` / `packageFileCount` | 同上 `private_version_payload` | 同上测试断言 |
|
||||
| 主规范路由表登记新路由并修正 `/my/games` 错名 | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` 路由表 | — |
|
||||
|
||||
设计要点:不新增 SpacetimeDB 表与 procedure。详情路由的 game 走 `get_game_distribution_game`(owner 作用域,精确命中),版本沿用作者自有列表 procedure 的聚合结果。
|
||||
|
||||
## 计划与落地状态
|
||||
|
||||
### 波 1:闭环可用
|
||||
|
||||
- [ ] AGC 真机把「构建 → 打包 → 分片上传 → 送审」跑通一次。
|
||||
- [x] 发布进度接线:前端监听既有 `game-package-upload-progress`,渲染真实阶段与百分比。
|
||||
- [x] 幂等键落盘:按账号、origin、本地项目和包摘要持久化根键,重试复用同一 key。
|
||||
- [ ] 运营开闸 + 生产发布第一个作品,走完审核 → 公开 → 游客可玩。
|
||||
|
||||
### 波 2:管理完整
|
||||
|
||||
- [x] 作品资料编辑:展示资料在 `game` 级可编辑、立即生效并留审计;包仍随版本冻结。
|
||||
- [x] 作品软删除:`game_distribution_game` 末尾追加 `deleted_at`,公开投影与列表过滤软删行。
|
||||
- [x] 网页侧对齐:`/games/mine` 补删除、资料编辑、版本历史、作者查看未公开作品。
|
||||
- [x] 后台作品列表:游标分页 + 搜索 + 按作者/状态筛选。
|
||||
|
||||
### 波 3:运营治理
|
||||
|
||||
11. 审核历史展示(复用版本已有的 `reviewed_by_user_id` / `submitted_at` / `reviewed_at` / `published_at`,不新增表)。
|
||||
12. 批量审核操作。
|
||||
13. 精选 / 推荐位(与游玩线的推荐排序定边界)。
|
||||
14. 举报处理(先确认能否复用现役反馈管道)。
|
||||
|
||||
## 尚未完成
|
||||
|
||||
- AGC 真机 GUI 端到端发布取证。
|
||||
- 生产环境发布第一个作品并开闸。
|
||||
- 非 Phaser(Godot / Cocos / Unity)发布的主动拦截:这些工程没有 npm 构建工作区,仍只给泛化错误文案(单 HTML 项目同理)。
|
||||
- 发布包清理策略的上线验收。
|
||||
|
||||
## 不变式
|
||||
|
||||
- 新增字段一律追加到 Rust 表结构体末尾并带明确默认值;不改名、不重排、不改类型。改 schema 后同步 `migration.rs`、表目录与生成绑定,并跑 `npm run check:spacetime-schema`。
|
||||
- 公开投影不得携带作者私有字段(包摘要、驳回理由、文档本地路径)。作者视角走 owner 作用域路由。
|
||||
- 复用现役管道与组件,不新造上传、审核、发行通道。
|
||||
|
||||
## 波 1 · AGC 客户端发布切片(2026-10-01 落地;自动门禁已通过,真实 GUI/生产待验收)
|
||||
|
||||
| 项 | 落点 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| 发布进度接线 | `src-tauri/src/game_package_upload.rs`、`game_distribution_publish.rs`、`commands.rs`、`src/services/gamePackageUploadProgress.ts`、`src/components/game-distribution/GamePublishProgressDialog.tsx`、`GamePublishPhaseSteps.tsx`、`GameDistributionPublishPanel.tsx`、`App.tsx` | 既有 `game-package-upload-progress` 事件扩展出 `phase`(`prepare` / `upload` / `verify` / `submit`)与 `message`,上传阶段仍带 `receivedBytes` / `totalBytes`;`/complete`(重算摘要 + 展开清单)之前由上传器回调切到「校验」。渲染层在全屏进度弹窗显示「构建」步骤,在发布面板显示「构建 → 准备 → 上传 → 校验 → 送审」步骤条与上传百分比。 |
|
||||
| 根幂等键落盘 | 新增 `src-tauri/src/game_publish_attempt.rs`(应用数据目录 `game-publish-attempts.json`,原子写 + 进程内锁) | 键按「账号 + origin + 本地项目 + 归一化包摘要」解析:同一份包重发、响应丢失、进程重启与分片续传复用同一根键(服务端因此只认一次尝试);送审成功后清除记录,下一次发布会新增版本。渲染层不再铸键,`publishLocalProjectGame` 只在显式传入时才带 `idempotencyKey`。账本只存账号标识、origin、本地项目标识、包摘要与根键,不含 token 与本地绝对路径。 |
|
||||
| 发布前工程栈校验 | `src-tauri/src/project/export.rs`(`ensure_publish_project_stack`,导出与发布两条入口都调用) | 存在 npm 构建工作区时要求 Phaser 4 + Vite:缺 `phaser` 依赖、声明的主版本不是 4(含 `package-lock.json` 回退判定)、以及既没有 `vite` 依赖也没有 `vite.config.*` 都给出可操作中文错误(指明缺什么、当前声明、怎么修)。没有 npm 工作区的老单文件项目保持原有发布能力。 |
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 作者自有游戏列表与新增的作者详情共享同一个单次条数上限(48):单作者作品数超过上限时,详情页拿不到版本记录。放量前需要补一条按 `gameId` 精确列版本的 procedure。
|
||||
- 客户端不暴露 `localProjectId`:本地项目与线上作品的对应关系由客户端本地账本维护,服务端只在发布时用它复用 `gameId`。
|
||||
- 发布尝试账本按「本地项目 + 包摘要」判同一尝试:改了工程或重新导出了不同内容的包就是新尝试(新建版本),旧版本的半成品包仍需走网页作者侧的恢复/重置入口。
|
||||
- 发布阶段条按「本次订阅窗口」收敛(事件的 `versionId` 在命令返回前对渲染层不可知);同一窗口同一时刻只有一次发布在跑。
|
||||
- 工程栈校验按首版「只支持 Phaser 4」执行:npm 工程(含 Three.js 等三维工程)未声明 `phaser` 会被拦下并提示安装 Phaser 4。若产品决定放行其它 npm 技术栈,需要在这里改成「phaser 声明存在时必须是 4,其余技术栈按白名单放行」。
|
||||
|
||||
## 尚未完成
|
||||
|
||||
- AGC 真机 GUI 端到端发布取证。
|
||||
- 生产环境发布第一个作品并开闸。
|
||||
- 非 Phaser(Godot / Cocos / Unity)发布的主动拦截:这些工程没有 npm 构建工作区,仍只给泛化错误文案(单 HTML 项目同理)。
|
||||
- 发布包清理策略的上线验收。
|
||||
@@ -49,6 +49,8 @@ AGC 发布面板完成三项收敛:
|
||||
## 6. 本次缺陷修复记录
|
||||
|
||||
- 2026-09-23:AGC 生成游戏封面请求补充 `generationInputs.source = "ai-game-creator-client"`。队列 worker 依据该来源选择 `GameCreatorResourceEditor` 结果契约;未标记来源时会按 `Standard` 紧凑化并省略 `result`,导致生成完成后无法回传 `assetObjectId`。对应前端定向测试已锁定请求字段与平台素材 ID 回填链路。
|
||||
- 2026-10-02:生成结果中的 `imageSrc/objectKey` 是私有素材引用,不再直接交给 WebView `<img>`;AGC Rust 发布 facade 经鉴权 `read-url` 有界下载为 `data:` 图片预览,预览失败不阻断已登记素材发布。
|
||||
|
||||
## 7. 本轮核对(2026-09-28)
|
||||
|
||||
六项交付结果都在合并后的工作树里,并有本轮实测:
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# 【实施计划】游戏广场评分展示
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | [游戏广场评分展示](./【里程碑】游戏广场评分展示-2026-10-01.md) |
|
||||
| Status | implemented(本地验证通过,待用户验收;未部署) |
|
||||
| Date | 2026-10-01 |
|
||||
| Owner | Codex |
|
||||
|
||||
## 修改边界
|
||||
|
||||
- 主规范:[游戏广场评分展示合同](../../【玩法创作】平台入口与玩法链路-2026-05-15.md#游戏广场评分展示合同);仅实施本里程碑。
|
||||
- 数据读取:`server-rs/crates/spacetime-module/src/game_distribution.rs` 的公开快照、列表/详情读取及 procedure 返回类型;复用已有评分 helper。现有持久化表、索引、评价写入与 migration 登记不变。
|
||||
- 后端:`spacetime-client/src/active/mapper/game_distribution.rs` 公开 record 和 mapper、`shared-contracts/src/game_distribution.rs`、`api-server/src/modules/game_distribution.rs` 公开响应与 no-store;通过既有 facade,不新增数据库访问通道。
|
||||
- 前端:`packages/shared/src/contracts/gameDistribution.ts`、共享评分文本组件及导出,网站 `GameGalleryPage.tsx`、`GameReviews.tsx` 和对应 CSS/定向测试;API client 只按必要调整类型,不新增逐卡读取。
|
||||
- 契约检查:`scripts/check-game-distribution-dto-parity.mjs` 与生成绑定;运行时复用现有隔离数据,不新增 E2E 脚本或扩大评分/后台管理回归矩阵。
|
||||
- 文档:主规范、本文、里程碑、文档索引及稳定决策。后台 UI、AGC、外部 OpenAPI、发行包链路不在修改范围。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 扩展 `GameDistributionPublicGameSnapshot` 返回类型,追加 `average_score:Option<f64>` 与 `rating_count:u64`;复用 `game_distribution_user_rating_summary` 及领域 `visible_review_summary` 的隐藏过滤、一位小数和空态。列表先核对公开可见性与有效版本、按原规则排序并限量,再只为返回项构建含摘要的快照;不能让无效版本占用限量名额。详情共用该公开快照。
|
||||
2. 同步生成绑定与公开 record/mapper,将摘要映射为既有 `GameDistributionRatingSummaryRecord`。浮点数进入公开快照/record 后,移除其及引用结果类型中不适用的 `Eq`,保留 `PartialEq`;record 嵌入摘要时按现有序列化用途补齐必要 derives。
|
||||
3. Rust/TS 共用游戏 DTO 追加可选 `ratingSummary`,Rust 允许旧字段缺失;公开 `public_game_payload` 必须输出摘要对象,复用既有摘要转换。同步列表/详情 no-store、DTO parity 的公开构建器 `mustEmit` 和必要嵌套约束;作者 `game_payload` 可省略摘要,不造零值。
|
||||
4. 将纯评分文本展示抽到 `packages/shared`,只接收摘要数据,复用一位小数及人数表现。现有 `GameRatingSummary` 保留详情评价的加载/错误/重试职责,已加载文本使用共享组件;广场卡片使用列表摘要,无额外评价请求,缺字段显示“评分暂不可用”。卡片简介下增摘要行,采用现有视觉变量并允许移动换行。
|
||||
5. 在现有广场页面测试中补有评分、零评价、缺摘要三种展示断言;复用现有公开响应/DTO 测试核对新增字段。有效评价统计、管理动作、筛选、滚动恢复和迟到响应沿用已有测试,不另加重复用例;纯文本共享组件由页面测试覆盖即可。
|
||||
6. 执行定向工程检查,以现有隔离数据做一次公开 API smoke 和桌面/移动页面检查,按主规范回写证据和限制,交付用户验收;验收通过后融合持久结论并清理临时计划。
|
||||
|
||||
## 必要测试范围
|
||||
|
||||
- 前端:在已有页面测试中覆盖正常评分、一位小数/人数、无人评分与缺字段提示;运行受影响的广场及详情评价测试。
|
||||
- 后端:复用已有响应/契约测试验证摘要字段,沿用有效评价统计测试确认隐藏过滤。相同映射不在 mapper、handler、DTO 每层重复添加一套测试。
|
||||
- 联调:复用现有两款游戏及少量评价,一次核对列表/详情/评价接口摘要一致、隐藏记录不计入且游戏间统计不串联;浏览器检查布局与详情返回后的更新。
|
||||
- 不新增测试框架、专用 E2E 脚本、大批量账号/分页 fixture、故障注入或完整管理动作矩阵。仅在实现确实改动已有行为且现有测试未覆盖时补相应回归。
|
||||
|
||||
## 验证命令与操作
|
||||
|
||||
- 运行 `module-game-distribution` 已有有效评价统计定向测试;领域逻辑未改时不新增同类测试。
|
||||
- `cargo test --manifest-path server-rs/Cargo.toml --locked -p shared-contracts game_distribution`
|
||||
- `cargo test --manifest-path server-rs/Cargo.toml --locked -p api-server game_distribution`
|
||||
- `cargo check --manifest-path server-rs/Cargo.toml --locked -p spacetime-module -p spacetime-client`
|
||||
- `npm run spacetime:generate`、`npm run check:generated-bindings`、`npm run check:spacetime-schema`、`npm run check:game-distribution-dto-parity`
|
||||
- `npm run test -- src/components/game-distribution/GameDistributionPages.test.tsx src/components/game-distribution/GameReviews.test.tsx`;只有 API client 实现发生变化时追加其已有测试,不单独新增纯文本共享组件测试。
|
||||
- `npm run typecheck`,对受影响代码运行仓库现有格式/静态检查。
|
||||
- `npm run check:doc-index`、`npm run check:encoding`、`git diff --check`。
|
||||
- 按现有 dev 脚本启动显式隔离数据库和 `npm run dev:api-server`,读取 `.app/dev-stack.json` 核对目标/端口,检查 `/healthz`;复用现有隔离数据做一次列表/详情/评价摘要对照,不为本增量重建整套 ratings/moderation E2E 流程。
|
||||
- 真实浏览器桌面与 375px 移动视口检查评分行、换行及详情改分后返回;确认摘要随列表返回。筛选/滚动恢复、错误/重试及迟到响应沿用已有定向测试,不重复建立浏览器矩阵。
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- 公开 procedure 返回类型属于配套运行时合同;module、生成绑定和后端先在隔离环境一起验证,再按现有运维流程发布,网站随后更新。无需持久化迁移或数据回填,不能用删除数据处理类型不匹配。
|
||||
- 评分聚合仅作用于返回项,并使用既有按游戏索引;保留有效版本过滤与排序,避免额外扫描全部匹配游戏的评价。当前规模不增加缓存、统计表或预计算任务。
|
||||
- 缺字段只表示不可用,不能默认成 `null/0`;详情评价状态继续由现有读取/保存驱动,避免公开投影摘要覆盖提交后的最新值。
|
||||
- 回滚网站展示可保留新增公开字段;回滚 module/后端必须恢复互相匹配的绑定,并继续过滤隐藏评价。不得退回不识别管理隐藏状态的基础评价旧实现。
|
||||
|
||||
## 执行结果
|
||||
|
||||
- 已完成上述代码与绑定接入;公开列表先过滤有效版本、排序并限量,只为返回游戏聚合评分。持久化表与评价写入规则未变。
|
||||
- 前端两文件 36 项、API game_distribution 34 项、shared-contracts 4 项、领域 summary 2 项通过;module/client 编译、typecheck、受影响代码静态/格式检查通过。
|
||||
- `spacetime:generate` 使用固定 2.8.3 完成;保留新增快照字段,清除无关生成格式 diff。生成绑定、schema(87 表)、DTO parity(31 类型/4 构建器)、编码、文档索引和 diff 检查通过。
|
||||
- `dev:api-server` 启动后 `/healthz` 通过;本机显式隔离库没有既有评分 fixture,复用现有 helper 创建两个账号、两款游戏、两条评价。游客/登录公开列表摘要一致,列表/详情/评价摘要 `8/2`,隐藏一条后 `7/1`,另一游戏始终 `null/0`;列表/详情 no-store 通过。临时联调代码与数据仅留在 ignored 本地产物,未新增专用仓库 E2E 脚本。
|
||||
- 1280px/375px 浏览器评分行无横向溢出,卡片可进入详情;详情期间通过 API 将可见评分从 7 改为 6,返回广场显示 `6.0/10 · 1 人评分`。
|
||||
- 前端实现与后端只读审查由两个 `gpt-6.1-sol` subagent 协助,后端审查未发现可执行问题。
|
||||
|
||||
## 未验证与待验收
|
||||
|
||||
用户最终验收与生产部署待完成,保留本计划。未跑完整仓库测试、完整评价/后台管理 E2E、真实手机、长标题/极大人数浏览器矩阵、浏览器登录改分和实际发行包游玩。返回刷新通过 API 改分验证,登录公开读取通过 HTTP smoke 验证;未增加复杂测试矩阵。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 【实施计划】网站游戏评分与评价
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | [网站游戏评分与评价](./【里程碑】网站游戏评分与评价-2026-09-30.md) |
|
||||
| Status | implemented(本地验证通过,待用户验收) |
|
||||
| Owner | Codex |
|
||||
|
||||
## 修改边界
|
||||
|
||||
- 按用户确认的最新主规范实施网站详情评价、评分统计、公共分页与个人编辑。
|
||||
- 后端限定现有 game-distribution 领域、私有评价表、spacetime-client、共享 DTO 与 API 路由;新增表不破坏现有表。
|
||||
- 前端限定网站详情、评价组件与 API client,复用现有登录、按钮和状态组件。
|
||||
- 不扩展 AGC、广场、外部 API,不新增删除、审核、回复、点赞、排行榜或统计缓存。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 补齐评分、码点长度、归一化、排序统计领域规则与 Rust/TS DTO。
|
||||
2. 新增 game_distribution_review 私有表,以游戏/用户组合为唯一主键,按游戏建立索引;同步 migration、表目录、生成绑定。在事务内检查公开可玩状态并保存,读取分页与统计使用一致快照。
|
||||
3. facade 和 HTTP 接入公共分页、个人读取、唯一 PUT,身份由认证主体取得,响应 no-store。
|
||||
4. 网站实现个人表单/记录/编辑、公共分页与平均分人数,输入冻结及读取请求序号隔离;补定向交互测试。
|
||||
5. 定向 Rust/Vitest、typecheck、DTO/schema/编码/索引检查及真实数据库/API/桌面移动浏览器 smoke;记录逐条证据和限制。
|
||||
|
||||
## 验证命令
|
||||
|
||||
- `cargo test --manifest-path server-rs/Cargo.toml -p module-game-distribution`
|
||||
- `cargo test --manifest-path server-rs/Cargo.toml -p api-server game_distribution`
|
||||
- `cargo test --manifest-path server-rs/Cargo.toml -p spacetime-module game_distribution`
|
||||
- `npm run spacetime:generate`、`npm run check:spacetime-schema`、`npm run check:game-distribution-dto-parity`
|
||||
- `npx vitest run src/components/game-distribution src/services/gameDistributionClient.test.ts`
|
||||
- `npm run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`
|
||||
- `npm run dev:api-server` 检查 `/healthz`,真实本地 SpacetimeDB/API 验证两账号、唯一并发与至少 21 条评价;浏览器验证桌面、移动、编辑取消与键盘评分。
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- 当前是新增私有表,无旧数据回填。回滚页面/API 时保留评价数据,不删除数据库记录。
|
||||
- 中文/emoji 统一按换行归一化后的 Unicode 码点计数,不依赖 textarea 原生 UTF-16 maxLength。
|
||||
- 保存成功与账号/游戏切换使旧读取失效,旧成功、失败、finally 均不得覆盖当前状态。
|
||||
- 若环境不支持真实 smoke,明确记录已通过的自动化证据和未验证项,不标记已上线或已验收。
|
||||
|
||||
## 验证结果
|
||||
|
||||
本计划步骤已实施;各条行为和验证证据已回写[主规范](../../【玩法创作】平台入口与玩法链路-2026-05-15.md#网站游戏评分与评价合同)。真实数据库/API 49 项及桌面/移动视口浏览器 smoke 通过,未部署。用户验收后与里程碑一起清理。
|
||||
@@ -5,6 +5,8 @@ Status: implemented-awaiting-runtime-acceptance
|
||||
Date: 2026-09-14
|
||||
Milestone: `【里程碑】项目客户端占用锁收敛-2026-09-14.md`
|
||||
|
||||
> 2026-10-02 更新:本计划里 Runner 侧的 `runner/project_owner.rs`、`.agent/runtime/execution-owner.lock` 与项目 execution-owner / known-roots 语义已随自建 Agent Runtime 退役删除(见 `docs/adr/【ADR】退役AGC独立Agent Runtime与CLI执行面-2026-10-02.md`);统一客户端占用锁(`project/write_lock.rs`)保留,下述 Runner owner 部分仅作历史追溯。
|
||||
|
||||
## 代码边界
|
||||
|
||||
- `apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs`
|
||||
@@ -67,4 +69,4 @@ Milestone: `【里程碑】项目客户端占用锁收敛-2026-09-14.md`
|
||||
|
||||
- 统一锁与 Runner owner 两条路径都在工作树里:`apps/ai-game-creator-shell/src-tauri/src/project/write_lock.rs`、`runner/project_owner.rs`。
|
||||
- 定向用例:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline -- write_lock --test-threads=1` → **21 passed**;`-- gui_ --test-threads=1` → **31 passed**(覆盖 `gui_owner_*`、`gui_owned_runner_*`、`gui_participant_lock_*` 等同进程重入、跨线程争用、owner 交接与崩溃恢复分支)。
|
||||
- 仍待补:真机多窗口/跨进程占用的运行时表现(本机只到定向用例级)。
|
||||
- 仍待补:真机多窗口/跨进程占用的运行时表现(本机只到定向用例级)。
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# 【里程碑】AGC 发布版本以工程内部版本为准
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | proposed |
|
||||
| Date | 2026-10-03 |
|
||||
| Parent Spec | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`(「身份、状态、审核与更新」章) |
|
||||
|
||||
## 目标
|
||||
|
||||
让 AGC 发布面板的「项目版本」只有一个权威来源:AGC 工程内部版本记录 `manifest.versions[]`(即资源总览「项目版本」栏目里的版本卡)。标签等于当前存活的内部版本条数(最新卡的「版本 N」),只读、不可手填、不被平台 `version_number` 回填或覆盖,与 `publicationRevision` 无关。
|
||||
|
||||
## 边界
|
||||
|
||||
- 不改服务端能力:`publicationRevision` 继续严格 CAS;平台 `versionNumber` 继续接受正整数、重复与回退(网页端与历史客户端依赖该兼容口径)。
|
||||
- 不新增或修改 SpacetimeDB 表、字段与 procedure;`game_distribution_version.version_number` 不动。
|
||||
- 不重写平台历史版本行;不改公开地址、审核状态机与发行网关。
|
||||
- 不做版本历史页、版本对比、一键回滚 UI。
|
||||
- 不删除本地清单字段 `projectVersion`(Rust DTO 带 `deny_unknown_fields`,删除会让存量清单解析失败),它降级为不再读写的兼容位。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 发布面板显示的「项目版本」等于当前 `manifest.versions` 的条数(`versions` 为空时为 1),与平台 `version_number`、`publicationRevision` 均无关。
|
||||
- [ ] 面板不提供任何修改该值的入口;提交时 `versionNumber` 逐字等于该派生值。
|
||||
- [ ] 打开已有线上作品(线上最近提交 v6、内部版本 4 条)时,面板显示 v4,且仍并列显示「线上最近提交 v6 · 审核中」。
|
||||
- [ ] AGC 任何路径都不再写入 `projectVersion`(含发布成功回写、绑定回读回填、面板失焦写回)。
|
||||
- [ ] 存量清单里已有的 `projectVersion` 值不影响展示与提交。
|
||||
- [ ] 内部版本被截尾删除(长度 4 → 3)后,面板显示 v3 且能正常提交,不需要任何本地冲突处理。
|
||||
|
||||
## 依赖
|
||||
|
||||
- 无外部依赖;与「AGC 已发布游戏版本更新(2026-10-02)」是**取代**关系(见该文档内的取代标注)。
|
||||
@@ -0,0 +1,33 @@
|
||||
# 【里程碑】AGC 已发布游戏版本更新
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | proposed |
|
||||
| Date | 2026-10-02 |
|
||||
| Parent Spec | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md#AGC 游戏分发与在线游玩合同` |
|
||||
|
||||
## 目标
|
||||
|
||||
让 AGC 区分首次发布与已有作品更新:已有作品继续使用原 `gameId`;用户维护唯一项目发行版本标签,同一标签可以重复提交或回退,具体提交由不可变 `versionId` 标识。
|
||||
- 同一 `gameId` 的新版本上传、校验、送审和结果回写。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- AGC 完整版本历史页、版本对比和一键回滚 UI;回退版本标签的发布路径属于本里程碑。
|
||||
- 原地替换已发布 `versionId`;已发布提交保持不可变。
|
||||
- 新增 SpacetimeDB 持久表或修改 `game_distribution_version.version_number` 类型。
|
||||
- 改变公开 URL、审核状态机、发行网关或后台审核权限。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 首次发布完成后项目清单保存 `gameId`、`versionId`、版本号、状态和 `publicationRevision`。
|
||||
- [ ] 重新打开同一项目显示“更新游戏”,不再显示“创建平台游戏”。
|
||||
- [ ] 项目清单只有一个用户发行版本字段,发布面板编辑它,不存在独立 targetVersion。**(2026-10-03 取代:发布标签改由 AGC 工程内部 `versions[]` 派生且只读,面板不再编辑;见 `【里程碑】AGC发布版本以工程内部版本为准-2026-10-03.md`。)**
|
||||
- [ ] 同一 `gameId`、同一 `versionNumber` 可以重复提交,生成新的 `versionId`,不覆盖旧公开实例。
|
||||
- [ ] 项目版本可以低于线上最新版本,服务端允许回退标签,但仍校验为正整数。**(2026-10-03 收窄:平台侧继续成立;AGC 侧不再由用户手填回退,派生标签只在内部版本被截尾删除时变小。)**
|
||||
- [ ] 同版本旧 pending 提交被取消或被最新提交替代,审核队列不产生重复有效任务。
|
||||
- [ ] `publicationRevision` 仍用于并发 CAS,与用户版本号规则独立。
|
||||
- [ ] 更新使用同一 `gameId`,只增加一个新 `versionId`,公开地址保持不变。
|
||||
- [ ] 旧项目没有发布绑定时能按作者和 `localProjectId` 恢复;恢复失败时明确回到首次发布。
|
||||
- [ ] 上传响应丢失、重复提交、版本号冲突和切换账号不会创建重复游戏或错误接管作品。
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话历史**(`.agent/conversations/project.jsonl`)与 **运行态事件**(Thread Manager `subscribe` / `consume` / `notify`)。Direct 回合事件、`turn-stream.jsonl`、`tool-calls.jsonl` 与活动回合快照都不再是聊天视图的输入。
|
||||
|
||||
边界固定为:Thread Manager 只是**搬运层**——把 Codex 原始条目挑字段、脱敏、截断后下发;工具卡片的形状、可见性与合并全部由前端投影完成。线上模型是 ts-rs 导出的 tagged enum(`agent/direct_thread_wire.rs`),条目身份只有一个:Rust 在进队列前归一成一个 `itemId`,不再暴露第二个 id 概念,也不带任何回合身份。
|
||||
边界固定为:Thread Manager 只是**搬运层**——把 Codex 原始条目挑字段、脱敏、截断后下发;工具卡片的形状、可见性与合并全部由前端投影完成。线上模型是 ts-rs 导出的 tagged enum(`agent/thread_manager/wire.rs`),条目身份只有一个:Rust 在进队列前归一成一个 `itemId`,不再暴露第二个 id 概念,也不带任何回合身份。
|
||||
|
||||
## 范围
|
||||
|
||||
@@ -27,7 +27,7 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话
|
||||
|
||||
- 审批、提问与用户输入请求的状态机迁移;本次事件只作同一条流的 pass-through。
|
||||
- 跨进程回合账本、按回合统计与持久 turn ledger。
|
||||
- DirectRuntime 自己的进度事件与该运行时仍在使用的 `turn-stream.jsonl` / `tool-calls.jsonl` 写入:它们属于运行时的账本,本轮只切 DirectProject 聊天框的读路径。
|
||||
- DirectRuntime 自己的进度事件与该运行时的账本:本轮只切 DirectProject 聊天框的读路径;该账本(`turn-stream.jsonl` / `tool-calls.jsonl`)随后已随 issue #553 整体删除。
|
||||
- 旧项目磁盘上既有投影文件的清理、迁移或回填。
|
||||
- 非 DirectProject 运行时、Codex durable thread 语义、SpacetimeDB 与 HTTP 契约。
|
||||
|
||||
@@ -40,7 +40,7 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话
|
||||
- 思考正文流式下发不放宽可见范围:被下发的就是此前已在 `item.completed` 展示、并已落进 `project.jsonl` 的同一段文本。
|
||||
- 失败与中止说明只在运行期显示(不写 `project.jsonl`),页面重进后不再出现。
|
||||
- 未知 item 类型由 Rust 原样透传(只有类型与身份,Rust 侧留 TODO),当前由前端投影丢弃。
|
||||
- 前端聊天卡片的工具形状是 `Omit<GameCreatorDirectToolCall, 'turnId'>`;`tool-calls.jsonl` 的持久化形状与 DirectRuntime 的写入保持不变。
|
||||
- 前端聊天卡片的工具形状是 `Omit<GameCreatorDirectToolCall, 'turnId'>`。
|
||||
- 「可显示」的判据取**前端回合反馈**:一次翻页操作连拉到「合并后聊天投影的回合数增加」为止。工具卡片与思考文本虽然能通过 `projectDirectThreadItem`,但可能整页落进已渲染回合的折叠「执行过程」,不构成用户可见反馈;口径只在 `directHistoryPaging.ts` 里实现一份,首屏与「显示更早」共用。
|
||||
- 首屏切片的**新端边界**只认 `subscribe` 回执里的 `lastCompletedItemId`(含该条):回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏,手动重读保持按当前文件尾取尾屏的恢复语义。
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user