合并远端 master 到美术包合同修复分支
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m20s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m48s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m16s
Project CI / Backend tests (pull_request) Successful in 7m25s
Project CI / Frontend tests (pull_request) Successful in 3m10s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m28s
Project CI / Repository checks (pull_request) Successful in 6m39s
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m20s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m48s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m16s
Project CI / Backend tests (pull_request) Successful in 7m25s
Project CI / Frontend tests (pull_request) Successful in 3m10s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m28s
Project CI / Repository checks (pull_request) Successful in 6m39s
Project CI / Native shell tests (pull_request) Has been cancelled
同步 origin/master 的最新代码、测试与文档变更 保留图集按实际产物返回的提示词并移除已退役发布说明 合并技能包清单并递增版本,保留双方项目经验记录
This commit is contained in:
+2
-2
@@ -35,6 +35,8 @@
|
||||
|
||||
## AI 游戏创作与 Agent Runtime
|
||||
|
||||
- [导出产物面板与小红书小工具导出](./technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#2026-10-05-导出产物面板与小红书小工具导出):宿主只校验并运行项目内 `build:xhs-minitool`,适配由 code agent 首次实验固化;`.export/` flat 工作目录、内容 hash 冲突逐字段选择。已实现并通过本地定向验证,真实 vite 项目上的首轮适配与二次导出待运行时验收。
|
||||
|
||||
- [客户端本地埋点与主站入库契约](./technical/【技术方案】客户端本地埋点与主站入库契约-2026-09-21.md):本地 12 类事件采集、每 15 分钟上传、私有事件表、确认后清理与后台明细查询已完成隔离环境验收;不扩充采集范围、不做加密,未部署生产。配置要求及验证边界见第 13 节。
|
||||
|
||||
- [AGC 资源 kind 枚举化契约](./technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#2026-09-15-gamecreationapp-资源-kind-枚举化当前权威口径):GameCreationApp 资源 kind 的 Rust enum、ts-rs 绑定、Unknown 可观测性和 shell 内重构边界。
|
||||
@@ -60,8 +62,6 @@
|
||||
- [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 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# 【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
|
||||
```
|
||||
@@ -1,158 +0,0 @@
|
||||
# 【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
|
||||
```
|
||||
@@ -20,7 +20,8 @@ DirectProject 聊天区(`apps/ai-game-creator-shell/src/view/project-developme
|
||||
- 触发一(滚动):`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="status"`、`aria-live="polite"` 的「正在加载更早的对话」,带旋转圈;延迟 150ms 才显示,加载态转假后再留 300ms 才卸载(隐藏滞回)。它按需挂载,靠位置补偿(见第 2 条)保证下面的消息不跳。
|
||||
- 隐藏滞回是必须的:填充视口的自动加载是连续翻页的(一次加载落地后下一帧又起一次),两次之间只有一个 effect 回流;立即卸载会把加载行一帧内拆了又挂。全是工具调用时更明显——工具组折在收起的 `<details>` 里,每页几乎不增加可见高度,视口一直填不满,加载行就在「页与页之间」忽隐忽现。300ms 的隐藏延迟让连续加载之间的小空隙不卸载,真正结束(超时后仍为假)照常卸载。
|
||||
- 失败:挂起自动加载,列表顶部保留一行内联错误行——`role="alert"` 只包住「加载更早对话失败」文案本身,重试是可聚焦按钮、留在 live region 之外(assertive + atomic 的 live region 里不放交互控件);**不自动重试**,只有点重试(或切换项目)才重新开始;重试成功后错误行消失。
|
||||
- 一次加载与它所属的**世代**绑定:切换项目或新起一次读取都推进世代号(`historyLoadTokenRef`),旧世代落地时整段失效——不合并条目、不写游标、不关加载态。只比项目路径不够:A→B→A 之后在飞的旧读取又落回同一个路径,原守卫放行,会把新一代的加载行与 `historyLoadingRef` 这道并发闸门一起改掉。换项目的推进放在**渲染期**(与 `projectPathRef` 同一处),不放在复位 effect 里:passive effect 走宏任务、promise 续体走微任务,旧读取可能在「切换提交完成、复位 effect 还没跑」的窗口里落地,那时世代号还是旧的,守卫会放行。
|
||||
|
||||
@@ -37,8 +38,9 @@ DirectProject 聊天区(`apps/ai-game-creator-shell/src/view/project-developme
|
||||
|
||||
### 3. 回到底部胶囊
|
||||
|
||||
- 列表底部居中的悬浮胶囊(`sticky`),跟着滚动容器走、不随内容滚走。
|
||||
- 列表底部居中的悬浮胶囊,**位置基于列表可见底边**:列表是 flex 列容器,胶囊是最后一个 flex 项,`margin-top: auto` 在内容不足一屏时把它顶到可见底边,内容溢出一屏时 auto 归零、再由 `sticky bottom-3` 上拉贴底。只用 `sticky` 不够——它只能把元素**上拉**、不能下推,内容不足一屏时胶囊会停在文档流里(悬空),所以必须由 flex 的 auto margin 兜住「不足一屏」这一半。
|
||||
- 距底部超过 48px 时出现,文案「回到底部」;用户不跟随时来了新的终态内容就改成「有新回复 · 回到底部」。
|
||||
- 贴底判定不能只认 `onScroll`:内容**变短**到一屏以内时不会再有任何滚动事件(回合收口把过程折进收起的 `<details>`、历史加载行卸载),`atBottom` 会停在离开底部时的假值,胶囊就永远挂在一个滚不动的列表上(短历史一屏显示完却还悬着「回到底部」)。因此每次布局变化(`ResizeObserver`)、子元素增删(`MutationObserver`)与内容变化后都按真实几何复核一次 `isNearBottom`,复核为真即收起胶囊并清掉「有新回复」。
|
||||
- 点击:平滑滚到底部 + 恢复跟随最新 + 清除「有新回复」,随后按钮自行消失。
|
||||
- 平滑滚动期间滚动位置归这次程序化滚动所有:滚动事件不再翻转「跟随最新」,布局补偿也不写 `scrollTop`(写一次就会取消动画并把画面拉回原处,表现为「点了只下去一屏、到不了底」)。滚到贴底阈值即交还控制权;用户中途用滚轮 / 触摸 / 键盘打断则立刻交还,不会卡住后续跟随。
|
||||
- 动画期间内容变高(流式正文、图片撑开)时,点击瞬间记下的 `scrollHeight` 已经不是底部:补偿不写 `scrollTop`,而是把动画目标重新对准新的底部。否则动画停在旧目标上、等不到「贴底」那次滚动事件,`programmaticScrollRef` 不会交还——跟随与布局补偿整段挂起,而 `scrollToBottom` 已把胶囊按「已贴底」隐掉,用户停在底部之上却没有任何指示与自动跟随。
|
||||
@@ -67,7 +69,7 @@ DirectProject 聊天区(`apps/ai-game-creator-shell/src/view/project-developme
|
||||
## 备选方案与取舍
|
||||
|
||||
1. **保留按钮 + 只加自动加载**:加载中仍靠按钮做唯一反馈,且删掉按钮后失败路径没有补救入口;按钮本身与滚动自动加载重复。
|
||||
2. **在列表外面套一层 viewport 做浮层定位**:`PlanningChatView` 共用同一套容器规则,且工作台里 `.project-chat-conversation` 是 `display: block` + `height: 100%` 几何,套一层就会让 `height: 100%` 的列表塌成内容高度;改公共类会连带策划对话。改用列表内的 `sticky` 胶囊,零结构改动。
|
||||
2. **在列表外面套一层 viewport 做浮层定位**:`PlanningChatView` 共用同一套容器规则,且工作台里 `.project-chat-surface.is-direct-codex > .project-chat-conversation` 是纵向 flex、`.project-chat-message-list` 是其中 `flex: 1 1 auto` 的唯一滚动项,套一层定位容器会让列表塌成内容高度;改公共类会连带策划对话。改用列表内的 flex 项(`margin-top: auto` + `sticky`),零结构改动。
|
||||
3. **只依赖原生 CSS scroll anchoring**:前插能免费对齐,但做不到「跟随时展开要贴底」,也无法在加载期间冻结同一套锚点;因此显式补偿 + 关闭原生锚定。
|
||||
4. **展开后总是把正文滚进视口**:对正文比视口矮的折叠块会把画面大幅上移,打断正在读历史的用户;采用「跟随时贴底 / 否则冻结折叠头 + 只滚到刚好露出新展开正文的最小位移」。
|
||||
5. **平滑滚动期间照常处理滚动事件与布局补偿**:程序化滚动会被应用自己的补偿打断(第一次写 `scrollTop` 即取消动画),用户点了「回到底部」也停在半路;因此改为滚动期间冻结这两条路径。
|
||||
@@ -80,6 +82,6 @@ DirectProject 聊天区(`apps/ai-game-creator-shell/src/view/project-developme
|
||||
|
||||
- 历史加载失败不再只写顶部状态行,而是落到列表里的内联错误行;顶部状态行仍保留首屏读取失败等其它用途。
|
||||
- 滚动是表现层行为,正式状态仍在后端投影与运行态事件;本 ADR 不新增领域概念。
|
||||
- 阈值(触顶 24px、贴底 48px、spinner 150ms)是可按手感调整的常量,集中放在 `components/DirectProjectConversation/conversationScrollPolicy.ts`。
|
||||
- 阈值(触顶 24px、贴底 48px、spinner 显示 150ms / 隐藏滞回 300ms)是可按手感调整的常量,集中放在 `components/DirectProjectConversation/conversationScrollPolicy.ts`。
|
||||
- 验收:纯函数与 jsdom 组件测试覆盖阈值、锚点还原(含收口后按块身份仍指向同一块、锚点块被折叠隐藏时放弃还原)、加载/错误行、胶囊文案与显隐;滚动观感(顶部加载圈、胶囊出现与消失、底部展开回贴、历史前插不跳、长回合收口时视口不跳、切项目后首屏贴底且不误报「有新回复」)必须真机手动验收——jsdom 没有布局。
|
||||
- 明确的后续项(不在本次范围):`PlanningChatView` 与 `App.tsx` 遗留 `message-history-more` 路径的同款改造、未读条数徽标、Playwright 端到端。
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# AGC 错误具体文本展示实施计划
|
||||
|
||||
Version: 1.1
|
||||
Status: implemented-awaiting-runtime-acceptance
|
||||
Date: 2026-10-04
|
||||
Parent Milestone: `【里程碑】AGC错误具体文本展示-2026-10-04.md`
|
||||
|
||||
## 修改边界
|
||||
|
||||
1. 在前端回合失败映射中新增统一的安全 detail 投影,按 typed variant 保留 HTTP 状态、错误分类和脱敏文本。
|
||||
2. 对 `ModelCallKind`、`TransportClosed`、`TurnInterrupted`、阶段失败和 `Unclassified` 使用各自载荷,不再无条件返回通用文案。
|
||||
3. 检查并补强 Rust 错误脱敏测试,确保敏感值替换而不是整行删除。
|
||||
4. 补充 upstream、transport、stream、IPC、内存不足与 HostDropped 边界测试。
|
||||
5. 更新 ADR 的当前行为口径。
|
||||
6. HostDropped 的可选 detail 由 Rust 单点脱敏;panic hook 把可读负载和位置关联到原回合占用,Drop 与旧无字段事件分别验证,不更改回合排队、重试或计费行为。
|
||||
7. DirectProject 控制器的取消、上传、历史读取和前置异常统一复用前端精确脱敏出口,保留 IPC/OS 正文并替换凭据值、URL 与路径。
|
||||
8. 项目资源编辑/上传/预览/恢复队列/生成结果回读的 UI 错误统一使用 `normalizeDiagnosticText`,不再直出 `Error.message`。
|
||||
9. 应用壳、运行配置、认证状态、插件启动、运行预览和策划会话的非结构化错误统一使用 `visibleClientErrorMessage`。
|
||||
10. 资产导入、邀请码、策划工作区、发布封面/截图和素材命令的 UI 错误沿用同一共享出口。
|
||||
11. Codex CLI / Claude sidecar 的解析失败、失败终态、缺失终态、超时和空回执保留有界脱敏正文;即使平台 `LlmError` 变体没有 detail 字段,也由统一 Direct `ModelCallFailed.detail` 继续携带原始原因。
|
||||
12. 扩展统一正文口径到账户、模型目录、External Editor/资源编辑、发布、素材上传、错误报告与客户端受控工具桥,保留 HTTP 状态、响应安全片段、JSON 解析原因、网络因链和 IPC/桥接原因。
|
||||
13. 浏览器启动/DevTools 握手与 Codex model-catalog 子进程错误保留 stderr、退出状态、解析原因和阶段信息,机器码只作为分类字段。
|
||||
14. Node/npm 探测与 Web scaffold 构建失败保留有界脱敏 stdout/stderr、退出状态和超时原因;`preflight_web_game_creation` 不再只返回机器码。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
先锁定脱敏输入输出用例,再实现前端可见文案;随后补 Rust 映射/诊断测试,最后运行类型、编码和差异检查。
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
- 风险:直接展示未经精确脱敏的 provider detail 会泄露凭据或项目路径;前端和 Rust 双重检查,发现敏感 assignment 时替换值,不删除整条错误。
|
||||
- 风险:错误文本过长遮住分类;保留分类前缀并将 detail 限制在有界字符数。
|
||||
- 回滚:恢复 `directTurnFailureNoticeText` 的分类文案,保留 typed detail 和诊断落盘,不改协议。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 前端 DirectProject 错误展示:18 个定向测试通过;含 HostDropped detail / 旧载荷兼容。
|
||||
- AGC shell TypeScript 类型检查:通过。
|
||||
- Rust app-server 上游 detail 映射:6 passed。
|
||||
- CC sidecar 状态码/超时映射与非支付 409 回归通过;侧车非零退出、RPC error、解析失败统一补充有界 stderr detail。
|
||||
- 真实 AGC dev smoke 复现并定位 `Reached maximum number of turns (8)` → `transport-closed`;修复后客户端已热重编译重启,未再次触发付费 Provider 请求。
|
||||
- Rust 应用日志/启动诊断精确脱敏回归:通过。
|
||||
- Rust HostDropped panic detail + 旧无字段载荷兼容回归:定向通过。
|
||||
- DirectProject 控制器错误正文:新增脱敏与空错误专用提示回归,前端定向测试通过。
|
||||
- 资源工作台错误正文:项目开发页所有用户可见资源错误路径完成统一脱敏收口,TypeScript 与编码检查覆盖。
|
||||
- 真实 Provider、IPC 断链和内存压力尚未执行。
|
||||
- 2026-10-05 当前代码真实 `npm run agc` 构建启动通过;客户端与 Runner 已启动,前端/后台/API/worker/数据库健康检查通过,停止本次自有客户端后保留原有后端。
|
||||
@@ -0,0 +1,44 @@
|
||||
# 【实施计划】CC复用Codex运行态事件
|
||||
|
||||
| 字段 | 值 |
|
||||
| --------- | --- |
|
||||
| Milestone | `docs/project-memory/plans/【里程碑】CC复用Codex运行态事件-2026-10-04.md` |
|
||||
| Status | implemented-pending-acceptance |
|
||||
| Owner | Codex |
|
||||
|
||||
## 修改边界
|
||||
|
||||
- 允许修改:CC sidecar stdout 事件读取、`claude_code_cli.rs` 事件投影与历史收尾、Rust/前端定向测试、AGC 主技术方案和本里程碑文件。
|
||||
- 明确不修改:Codex app-server 协议、外部 API、SpacetimeDB schema、MCP 工具实现、现有前端通用展示组件。
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. 给 sidecar 回合读取增加逐事件回调,同时保留终态和错误收口。
|
||||
2. 在 Rust CC adapter 中建立有界、可归属的事件投影:文本/过程增量、工具开始、工具结果、进度与未知帧忽略。
|
||||
3. 复用 ThreadEvent/ThreadItem 和现有历史追加逻辑,处理重复、缺失 id、失败结果和最终回复兜底。
|
||||
4. 补 Rust parser/adapter、Thread wire 与前端 reducer/工具卡片定向测试。
|
||||
5. 运行编码、diff、Rust/TS 定向门禁;若环境允许再做真实 CC Direct smoke。
|
||||
|
||||
## 验证命令
|
||||
|
||||
1. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline --bin genarrative-ai-game-creator-shell agent::claude_code_cli --test-threads=1`
|
||||
2. `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --offline --bin genarrative-ai-game-creator-shell agent::thread_manager::wire --test-threads=1`
|
||||
3. `npx vitest run tests/directThreadChat.test.tsx tests/appSurface/tool-call-group.suite.ts apps/ai-game-creator-shell/tests/AgentMessageContent.test.tsx --root apps/ai-game-creator-shell`
|
||||
4. `npm run check:encoding`
|
||||
5. `git diff --check`
|
||||
|
||||
## 风险与回滚点
|
||||
|
||||
- SDK 事件类型会随版本增加:只读取白名单字段,未知事件忽略,不能因未知帧打断整轮。
|
||||
- 文本/工具事件可能在同一 SDK 消息中并行出现:身份必须由 message id、content block index 或 tool_use id 组成,不能按到达顺序猜测。
|
||||
- 真实 Provider/SDK 不可用时不宣称实时 smoke 通过;保留 fixture 测试作为替代证据。
|
||||
- 若 Rust 事件投影破坏 Codex 回归,回滚只限 CC adapter 与其测试,不回退 ThreadEvent 合同。
|
||||
|
||||
## 当前验收证据
|
||||
|
||||
- Rust adapter `cargo check` 通过;本地缺失随包资源校验通过临时环境开关跳过,未改变最终代码。
|
||||
- CC stream fixture 定向测试通过:增量正文、MCP started/completed、观察器状态均已验证。
|
||||
- `cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。
|
||||
- 前端 Vitest 未执行:当前 worktree 缺少 `@tailwindcss/vite`、`vite` 等 npm 依赖。
|
||||
- 真实 CC Provider smoke 未执行:当前 worktree 的随包资源校验素材不完整,且未启动真实项目回合。
|
||||
- `npm run check:doc-index` 被既有未分类文件 `docs/technical/【技术方案】Ubuntu离线AI游戏创作部署与传统工作流对比-2026-10-03.md` 阻断。
|
||||
@@ -0,0 +1,227 @@
|
||||
# 【实施计划】陶泥儿导出文件草稿化与发布媒体直传
|
||||
|
||||
- Version: `v1`
|
||||
- Status: 已实施(M1–M4 已落地;行为已回写主规范与共享记忆)
|
||||
- Date: `2026-10-06`
|
||||
- Retention: 本计划与配套里程碑规范长期保留在 `docs/project-memory/plans/`;原 §5.4「收尾删除计划与里程碑规范」不再执行。
|
||||
- Parent Spec(已回写):
|
||||
- `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`(AGC 导出面板章节)
|
||||
- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`(游戏分发媒体合同)
|
||||
- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`(游戏分发领域合同)
|
||||
- `docs/project-memory/shared-memory/decision-log.md`、`pitfalls.md`
|
||||
|
||||
> 本文档是 grilling 后的完整迁移计划(临时产物)。按《规范驱动开发工作流》,先回写主规范并通过评审,再按里程碑逐个实现;实现发现行为变化时按「主规范 → 未完成里程碑规范 → 当前实现计划 → 代码」回改。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话交付与验收判据
|
||||
|
||||
把陶泥儿(Taonier)导出迁移到小红书(xhs)重构后的文件草稿模式:**草稿 = `.export/taonier.json`(唯一真相)**、**打包 = `vite-export-taonier` skill + `build:taonier` 脚本产出 `.export/taonier.zip` 并由 code agent 适配任意工程布局**、**发布 = 宿主把草稿声明图片以 `multipart/form-data` 原始二进制直传游戏分发 API(未变图片带 OSS objectKey 沿用)**;同时**整体退役 `exports/`** 与**发布链路中的 assetId**。
|
||||
|
||||
验收判据(逐条可复核):
|
||||
|
||||
1. 未适配工程进入陶泥儿页先弹「让陶泥儿帮我调通」;适配后 `build:taonier` 成功产出 `.export/taonier.zip`,包内根目录存在 `index.html`。
|
||||
2. 表单校验只在前端;宿主 save 不做业务校验;校验不过时「打包」停在表单、不触宿主、不跑构建(对齐 xhs `157b4eb86` 断言)。
|
||||
3. 构建/草稿失败按 xhs 分类:可修复的转发给 code agent(「让陶泥儿来修」),不可修复的只提示人。
|
||||
4. 发布请求为 `multipart/form-data`:元数据 + 新/换图片原始二进制 + 沿用图片的 objectKey;`exports/playtest-package-*.zip` 不再存在,`.export/taonier.zip` 走现有整包/分片上传。
|
||||
5. 发布后线上读取显示「线上值」;草稿不自动回填、不被覆盖;每字段/图片可「沿用线上值」。
|
||||
6. 仓库中不存在 `exports/` 相关创建、读取、包路径、prompts、run-trace 白名单与命令;`assetId` 不再出现在游戏分发媒体请求/冻结资料/游戏行/公开读授权判定中。
|
||||
7. 全部定向测试、`check:spacetime-schema`、`check:generated-bindings`、`check:encoding`、`git diff --check`、`check:doc-index` 通过(`agc:skill-pack:check` 的 taptap 未登记红为已知接受项,见 §7)。
|
||||
|
||||
## 1. 背景与现状
|
||||
|
||||
- xhs 重构**已落盘**:`a6974f3ca`(表单校验只留前端,删 `validation.rs` 与 `FormInvalid`/`XHSMiniToolExportField`)、`157b4eb86`(多字段报错 + 交给陶泥儿)。重构后 xhs 形状 = `read_*`(内容)/`read_*_hash`(独立基线)/`save_*_form`(零校验、只做 baseHash 冲突)/`run_*_build`;前端 `xhsMinitoolFields.ts` 是唯一表单门;错误按 `code agent 可修 / 仅人可修` 分类;`.export/` flat 目录。
|
||||
- 陶泥儿现状:`useTaonierTab.ts` 调宿主 `export_local_project_package` 产出 `exports/playtest-package-*.zip`;`useGameDistributionPublishForm.ts`(968 行)做静默预填 + AI 资料建议 + 封面生成 + 素材登记上传;发布走 `publish_local_project_game`,staging 只读 `exports/playtest-package-*.zip`。
|
||||
- `exports/` 绑定面:项目初始化、包生成/读取/列举、4 个 Tauri 命令、生成任务产物声明(shared-contracts / platform-agent / 前端镜像)、prompts、run-trace 白名单、5 处安全/排除列表、`README` 交付物。
|
||||
- 发布媒体耦合面:`resolve_owned_game_media` → `resolve_owned_image_object_key`(客户端先登记素材、再带 assetId 发布);公开读授权 `game_distribution_asset_has_public_read_grant` 按 assetId 匹配;`axum` multipart 已启用且有两处先例(`raw_image.rs`、`admin_templates.rs`)。
|
||||
- 游戏分发模块不在线、无历史数据:可硬切、可删/改名 SpacetimeDB 字段,但必须同步 `migration.rs` 与生成绑定,并运行 `check:spacetime-schema`。
|
||||
|
||||
## 2. 已定决策(grilling 结论)
|
||||
|
||||
| # | 决策 |
|
||||
| --- | --- |
|
||||
| D1 | 对齐 xhs 重构后模式:文件草稿、typed error 转发 code agent、删除宿主业务校验。 |
|
||||
| D2 | 草稿 `.export/taonier.json` 形状 `{ form: { title, summary, category, coverPath, screenshotPaths[] } }`;不含 description/tags/deviceSupport/inputModes/orientation。 |
|
||||
| D3 | 每个目标独立、语义清晰的命令接口;不做带 target 枚举/flag 的 all-in-one 命令;内部复用共享 util。 |
|
||||
| D4 | 保留现有平台发布链(AGC native → `publish_local_project_game` 语义);打包改为 skill + `build:taonier` + code agent 首次适配,走与 xhs 相同的 adapt/fill/repair(未适配弹「让陶泥儿帮我调通」)。平台本身无额外适配概念,适配只针对打包。 |
|
||||
| D5 | 彻底退役 AI 资料建议与封面生成(前端 service、Rust 命令、api-server 路由、测试、专属 DTO);图片生成计费链路本身不动。 |
|
||||
| D6 | 删除 `useGameDistributionPublishForm`、`GameDistributionPublishFormView`、`GamePublishBlockedDialog`(及其测试);保留 `GamePublishProgressDialog` / `GamePublishPhaseSteps`。 |
|
||||
| D7 | 草稿为唯一真相;线上值只读展示;新增「沿用线上值」操作;不自动 seed、不回填覆盖。 |
|
||||
| D8 | 移除 `hasRunnablePrototype` 与 `ensure_publish_project_stack_at` 的 phaser/vite 门(含 `stage_publish_package` 的重复检查);新门 = `build:taonier` 存在、退出 0、产物 `.export/taonier.zip` 根有 `index.html`;保留符号链接/越界拒绝与 `checking`/`unavailable` 按钮禁用。 |
|
||||
| D9 | 整体退役 `exports/`(E1,含 `exports/README.md` 交付物):目录创建、README 生成、playtest 包链、4 命令、任务产物声明、prompts、run-trace 白名单、5 处排除列表、前端类型、测试、文档。 |
|
||||
| D10 | 新增 `vite-export-taonier` skill + `build:taonier` 脚本 + **`dist-taonier`** + `.export/taonier.zip`(与 xhs/taptap 同构,不做 native 特例);标准 `build`/`dist` 不被占用。 |
|
||||
| D11 | 发布 API 改为 `multipart/form-data` + 原始图片二进制(`4b`:不做单独 upload 命令,formdata 直发);整包 zip 保持现有 raw `Bytes`/分片上传,不并入 multipart。 |
|
||||
| D12 | 硬切(hard cut):不保留旧 JSON / assetId 分支与兼容 DTO。 |
|
||||
| D13 | 服务端解耦深度取 (b):发布图片落**项目快照桶** `agc/project-snapshots/v1/game-distribution/media/...`,**不建 `asset_object`**;公开读走新路由 `GET /api/game-distribution/media/read-url`(先抽公共签名/中转逻辑复用),与素材库 ACL 分离。删除请求/游戏行/冻结资料/授权判定中的 `*_asset_id`。 |
|
||||
| D14 | web 平台端一起迁到 formdata + objectKey(`gameDistributionClient.ts`、`GamePublishPage.tsx`、`GameWorkMetadataEditor.tsx`、`gamePublishAssets.ts`、`gamePublishMediaDefaults.ts`)。 |
|
||||
| D15 | skill pack 只登记 `vite-export-taonier`;未跟踪的 `vite-export-taptaph5` WIP 不登记,接受 `agc:skill-pack:check` 红(Q23①,风险见 §7)。 |
|
||||
|
||||
## 3. 主规范(行为合同)
|
||||
|
||||
### 3.1 草稿文件
|
||||
|
||||
- 路径:`<projectRoot>/.export/taonier.json`(flat `.export/`,与 xhs 同层级)。
|
||||
- 形状:`{ "form": { "title", "summary", "category", "coverPath", "screenshotPaths": [] } }`;`coverPath`/`screenshotPaths` 是**项目相对路径**;
|
||||
- `deny_unknown_fields`,camelCase;宿主只做 IO 事实守护(内容 hash 冲突、目录不可用),不做业务校验。
|
||||
- 写:前端 600ms 防抖自动保存 + 冲突逐字段选择;`save` 带 `baseHash`,格式差异且内容相同则吸收,否则返回 `SaveConflict`,绝不覆盖。
|
||||
- 读:`read`(内容)+ `read_hash`(独立基线,避免刷新移动写基线);前端 2s 轮询,输入中不覆盖草稿,只更新 `hasScript` 等外部事实。
|
||||
|
||||
### 3.2 打包
|
||||
|
||||
- 约定(对齐 xhs/taptap,不做特例):项目根或 `game/` 的 `package.json` 提供 `build:taonier`;宿主 `run_taonier_export_build` 以 `command.exec`(子命令 `build:taonier`,300s)运行;退出 0 且产出非空、非符号链接、≤200 MiB 的 `.export/taonier.zip` 才算成功。
|
||||
- 独立产物目录:`build:taonier` 由 skill 的 `vite.config.taonier.mjs` 构建到 **`dist-taonier`**,再由 `pack.mjs` 打成 `.export/taonier.zip`;**标准 `build` 脚本与标准 `dist` 不被导出占用**(标准 build 不负责打包,预览/开发仍用项目自己的 `dist`)。
|
||||
- 宿主不传任何构建 flag;skill 的 `pack.mjs` 沿用 xhs 约定自带默认值(`--vite-built-dir dist-taonier`、`--zip-out ../.export/taonier.zip`(cwd 为 `game/` 时)/ `.export/taonier.zip`(根工程时))。非标准工具链/输出目录由 agent 在项目自己的 `build:taonier` 包装脚本里适配。
|
||||
- skill `vite-export-taonier` 指导 code agent 为任意布局补齐 `build:taonier` 与必要配置。
|
||||
- 失败语义(typed error,forward 给 code agent):脚本缺失、命令被拒、命令失败(含输出尾)、产物缺失、导出目录不可用。
|
||||
- 预览不受影响:预览直接从 `project_game_root` 起服务,不读该 zip;`dist` 与 `dist-taonier` 互不干扰。
|
||||
|
||||
### 3.3 发布媒体直传
|
||||
|
||||
- 端点:`POST /api/game-distribution/games`、`POST /api/game-distribution/games/{game_id}/versions`、`PATCH /api/game-distribution/my-games/{game_id}` 改为 `multipart/form-data`。
|
||||
- 字段:`metadata` 文本 part(JSON,即原请求体的元数据部分,去掉媒体字段);`cover` 二进制 part(可选,出现时覆盖 `metadata.coverObjectKey`);`screenshot` 二进制 parts(可重复,按出现顺序消费)。`metadata.coverObjectKey` 为沿用线上封面时的 objectKey;`metadata.screenshots` 为 `(string|null)[]`,`string` 沿用该 objectKey、`null` 取下一个 `screenshot` part,数组顺序即最终截图顺序。
|
||||
- 存储:新图直接落**项目快照桶** `agc/project-snapshots/v1/game-distribution/media/<gameId>/{cover|screenshot}-<uuid>.<ext>`(与发行包同桶、与素材库解耦,不建 `asset_object`);沿用图校验 objectKey 必须属于该游戏当前媒体;构建冻结资料(只含 objectKey)。
|
||||
- 读:新增 `GET /api/game-distribution/media/read-url`,按 objectKey 返回签名读地址;判定 = 已发布且 active 的 game 且 objectKey 命中 `cover_object_key` / `screenshots_json`,**不经素材库 ACL**;需要同源字节时再补 `.../media/read-bytes`。签名与字节中转先抽公共函数,供 `/api/assets/read-url` 与新路由共用。
|
||||
- 整包:`PUT .../versions/{version_id}/package` 及分片端点保持现状(raw zip / octet-stream)。
|
||||
- 幂等:`request_digest` 重定义为「规范化元数据 + 图片字节 hash + 沿用 objectKey」的稳定摘要,覆盖 create / version / update 三条写路径。
|
||||
- 所有权/权限:仅 owner 可发布/编辑;沿用图校验 objectKey 必须属于该游戏当前媒体;附图校验 `image/*` 与张数/体积上限;公开读由新路由的域内判定负责。
|
||||
- 硬切:旧 JSON / assetId 请求直接不支持,不保留兼容分支。
|
||||
|
||||
### 3.4 线上值与沿用
|
||||
|
||||
- 读命令除草稿外返回只读 `online`:live `GameDistributionGame` 行的 `title/summary/category` + `coverObjectKey` / 截图 objectKey 列表 + 预览地址。
|
||||
- UI 逐字段内联显示「草稿值 vs 线上值」(线上值一行更小,跟在该字段输入框下,首次发布线上为空时不铺空对照);读取自动进行、失败静默自动重试,不提供手动「重新读取」入口;不自动 seed、不覆盖草稿。
|
||||
- 「沿用线上值」操作:文字字段一键把线上值写入草稿(标 dirty → 自动落盘);图片字段「替换为本地图」/「沿用线上」(清空本地路径,表示沿用);`screenshotPaths` 为空表示整组沿用线上。
|
||||
- 发布:路径非空的图发原始二进制;路径为空的图带线上 objectKey;文案沿用现有状态语义,不混淆在审/驳回状态。
|
||||
|
||||
### 3.5 错误与修复
|
||||
|
||||
- 前端错误分类(对齐 xhs):code agent 可修(草稿格式错、脚本缺失、命令失败、产物缺失)→「让陶泥儿来修」/「让陶泥儿帮我调通」;仅人可修(命令被拒、导出目录不可用)→ 只提示。
|
||||
- 失败对话框把**原始结构化错误**经项目对话转发给 code agent;适配指令引用 `vite-export-taonier` skill 与草稿路径/形状;「帮我填」只写元数据,不改打包适配。
|
||||
- 表单字段级红字 + 「交给陶泥儿」批量转发当前值,转发后清除红字并允许自动重读采纳 agent 修复。
|
||||
|
||||
### 3.6 退役(E1)
|
||||
|
||||
- `exports/`:项目初始化目录、`exports/README.md` 生成与渲染、`playtest-package-*.zip` 生成/读取/列举、`prepare/upload/list` 命令、staging 依赖。
|
||||
- 生成契约:publish-package 任务对 `exports/README.md` 的 artifact 声明(3 处)+ prompts(3 个 JSON)+ run-trace 白名单 slot。
|
||||
- 5 处安全/排除列表中的 `exports` 条目。
|
||||
- 发布媒体中的 assetId 与素材库耦合:请求 DTO、游戏行字段、冻结资料字段、公开读授权判定、`GameDistributionFrozenScreenshot.asset_id`;发布图片不再建 `asset_object`。
|
||||
- AI 资料建议/封面生成的前端调用、Rust 命令、api-server 路由与测试。
|
||||
|
||||
### 3.7 非目标
|
||||
|
||||
- 不改图片生成计费/额度链路本身;不改素材工作台对 `platform_asset_upload` 的既有使用(仅移除发布表单里的调用)。
|
||||
- 不新增 `/api/external/v1` 路由/契约(游戏分发不在 external v1,OpenAPI 不动)。
|
||||
- 不做历史数据迁移(模块不在线)。
|
||||
- 不登记 `vite-export-taptaph5`(另一条 WIP)。
|
||||
|
||||
## 4. 里程碑
|
||||
|
||||
| 里程碑 | 目标 | 依赖 | 验收证据 |
|
||||
| --- | --- | --- | --- |
|
||||
| M1 发布媒体合同解耦 | 游戏分发 API/契约/schema 改为 objectKey + multipart 直传;媒体落项目快照桶并新增公开读路由;web 平台端同迁 | 无 | 后端定向测试、`check:spacetime-schema`、`check:generated-bindings`、api-server smoke、web 端发布用例 |
|
||||
| M2 AGC 打包链路与 `exports/` 全退役 | 新 skill + `build:taonier`/`dist-taonier` + `run_taonier_export_build`;删 `exports/` 全链与旧打包门 | 无(可与 M1 并行) | Rust 定向测试、skill pack 同步(taonier 部分)、AGC typecheck、契约测试 |
|
||||
| M3 AGC 陶泥儿面板与发布媒体直传 | 草稿 hook/面板/错误转发/线上值与沿用;删 AI 建议/封面与旧表单组件;发布改 multipart | M1、M2 | vitest(字段校验、hook、面板、失败转发)、Rust 命令测试、真实栈发布 smoke |
|
||||
| M4 文档与共享记忆回写、临时计划清理 | 主规范/decision-log/pitfalls 回写;删临时计划 | M1–M3 | `check:doc-index`、`check:encoding`、`git diff --check` |
|
||||
|
||||
## 5. 实现计划
|
||||
|
||||
### 5.1 M1 发布媒体合同解耦
|
||||
|
||||
**Step 0(先做,重构优先)**:抽出可复用的「按 objectKey 签名读 + 同源字节中转」公共函数。
|
||||
- 现状:`assets.rs` 的 `get_asset_read_url_with_query` / `get_asset_read_bytes` 把「解析 target → 授权 → `sign_get_object_url` → 响应/中转」写在一起并绑定 `state.oss_client()`。
|
||||
- 目标:把「签名 + 响应形状 + 同源字节中转」拆成公共 helper(输入 OSS client、objectKey、expire、已授权标志),`/api/assets/read-url`/`read-bytes` 与游戏分发媒体路由共用,避免复制。
|
||||
- 授权保持域内分离:素材域继续走素材授权;游戏分发域走「已发布且 active 的 game + objectKey 命中 cover/screenshots」。
|
||||
- 必须用素材域现有测试锁定 `/api/assets/read-url`/`read-bytes` 的授权与过期语义不变。
|
||||
|
||||
**Step 1 shared-contracts**
|
||||
- `server-rs/crates/shared-contracts/src/game_distribution.rs`:`GameDistributionCreateGameRequest` / `GameDistributionUpdateGameMetadataRequest` 去掉 `cover_asset_id`,`screenshots` 语义改 objectKey;新增 multipart 表单 DTO(元数据字段 + `coverObjectKey` + 沿用 `screenshots`)。
|
||||
- `packages/shared/src/contracts/gameDistribution.ts`:镜像同步;`GameDistributionGameSummary.coverObjectKey` / `screenshots` 维持 objectKey。
|
||||
|
||||
**Step 2 spacetime-module**
|
||||
- `GameDistributionGame`:**保留 `cover_asset_id` 并标记退役**(保持在原字段位置、宿主不再写入,仅占位,读逻辑不得依赖),避免 SpacetimeDB 破坏性 schema 变更;`cover_object_key` 保留;`screenshots_json` 改为 objectKey 数组。
|
||||
- `GameDistributionFrozenMetadata`:删 `cover_asset_id`;`screenshots: Vec<String>`(objectKey);删 `GameDistributionFrozenScreenshot.asset_id`。
|
||||
- `apply_game_distribution_frozen_metadata`、`GameDistributionCreateGameInput` / `GameDistributionUpdateMetadataInput` / `GameDistributionGameSnapshot` 同步。
|
||||
- 公开读授权:删除 `game_distribution_asset_has_public_read_grant(ctx, asset_object_id)` 及其在 `editor_project_storage::asset_location_has_public_showcase_read_grant` 的调用(发布图不再经素材库读);公开判定迁到新路由的域内判定(已发布且 active + objectKey 命中)。
|
||||
- `migration.rs` + 表目录 + 生成绑定;运行 `npm run check:spacetime-schema`。
|
||||
|
||||
**Step 3 api-server**
|
||||
- `modules/game_distribution.rs`:`create_game` / `create_version` / `update_owner_game_metadata` 换 `Multipart` 解析(参照 `raw_image.rs` / `admin_templates.rs`);媒体写入用 `project_snapshot_oss_client` 的 `put_internal_object_with_retry`(专用前缀 + 内容类型/大小校验,不建 `asset_object`);沿用图校验改为「objectKey 必须属于该游戏当前媒体」;`resolve_version_metadata_json` 输出 objectKey;`request_digest` 重定义。
|
||||
- 新增 `GET /api/game-distribution/media/read-url`(+ 按需 `read-bytes`):匿名可读,用公共签名 helper + `project_snapshot_oss_client` + 游戏分发公开判定;更新平台 web 与 AGC 的封面/截图读地址来源。
|
||||
- 删除 `POST /api/game-distribution/publish-metadata/suggestions` 与封面生成相关路由;更新模块测试(冻结 JSON 形状、上传校验)。
|
||||
- `DefaultBodyLimit` 按图片大小设限(参照 `raw_image` 64 MB 级别,按 6 张截图 + 封面估算)。
|
||||
|
||||
**Step 4 web 平台端**
|
||||
- `src/services/gameDistributionClient.ts` / `GamePublishPage.tsx` / `GameWorkMetadataEditor.tsx` / `gamePublishAssets.ts` / `gamePublishMediaDefaults.ts`:发布/编辑改 `FormData`;未变图带 objectKey;删除发布前的素材登记步骤;封面/截图展示改走 `/api/game-distribution/media/read-url`。
|
||||
- 平台端测试与类型检查。
|
||||
|
||||
**Step 5 文档**:回写后端架构与玩法链路主规范 + OpenAPI 无需改动说明。
|
||||
|
||||
### 5.2 M2 AGC 打包链路与 `exports/` 全退役
|
||||
|
||||
**Step 1 skill**
|
||||
- 新增 `apps/ai-game-creator-shell/src-tauri/resources/agc-skills/vite-export-taonier/`,镜像 xhs:`SKILL.md`、`scripts/vite.config.taonier.mjs`(`outDir: 'dist-taonier'` + 产物收尾插件)、`scripts/pack.mjs`(默认 `--vite-built-dir dist-taonier`、`--zip-out ../.export/taonier.zip`,纯 Node zip,根目录须有 `index.html`)、以及隐藏测试 `scripts/.pack.test.mjs`、`scripts/.vite.config.taonier.test.mjs`。
|
||||
- 项目侧 `build:taonier` = 用该 config 构建到 `dist-taonier` 后跑 `pack.mjs` 产出 `.export/taonier.zip`(具体命令由 agent 写进项目 `package.json`)。
|
||||
- 登记:`scripts/skill-pack-manifest.mjs` 的 `EXPECTED_SKILL_NAMES`、`skill_pack.rs` 的 `AGC_SKILL_PACK_EXPECTED_NAMES` / `AGC_SKILL_PACK_FILES`、`manifest.json`(`agc:skill-pack:sync` 生成 sha256 并 bump version)、`codex_app_server/mod.rs` 三处测试 mock 名单。
|
||||
|
||||
**Step 2 宿主打包命令**
|
||||
- 新增 `src-tauri/src/export/draft/taonier/`:`layout.rs`(`SCRIPT_NAME="build:taonier"`、`REGISTRY_FILE_NAME="taonier.json"`、`ARTIFACT_FILE_NAME="taonier.zip"`)、`build.rs`(跑脚本 + 产物校验)、`script.rs`(npm 工作目录发现,复用 xhs 逻辑)、`registry.rs`、`dto.rs`、`error.rs`、`commands.rs`(M3 接前端;本里程碑先落 build 侧与注册)。
|
||||
- `src/desktop.rs` 注册新命令。
|
||||
|
||||
**Step 3 `exports/` 全退役**
|
||||
- `project/manifest.rs:614`:从初始化目录移除 `exports`。
|
||||
- `project/export.rs`:删 `export_local_project_package_at`、`export_local_project_package_for_publish_at`、`ensure_project_export_readme`、`render_project_export_readme`、`next_project_export_package_relative_path`、`collect_project_export_package_files`、`read_local_project_export_package_at`、`list_local_project_export_packages_at` 及专属测试。
|
||||
- `commands/desktop.rs`:删 `export_local_project_package`、`prepare_local_project_game_package`、`upload_local_project_game_package`、`list_local_project_export_packages`;同步 `src/desktop.rs` 与 `scripts/check-config.mjs` 白名单。
|
||||
- `game_distribution_publish.rs`:`stage_publish_package` 改从 `.export/taonier.zip` 读取;删 L1194 的 phaser 重复检查。
|
||||
- 新门:删 `hasRunnablePrototype` 调用与 `ensure_publish_project_stack_at` 的 phaser/vite 门;保留符号链接/越界拒绝。
|
||||
- 生成契约:`shared-contracts/game_creation_app.rs:~427`、`platform-agent/game_creation.rs:~987`、`packages/shared/src/contracts/gameCreationApp.ts:~438` 移除 `exports/README.md` artifact;`prompts/runtime/texts/{execution,generation,media}.json` 同步;`main.rs:1346` 白名单删 slot。
|
||||
- 排除列表:`preview.rs:1112/1156`、`agent/direct_runtime/mod.rs:3606`、`agent/direct_validation.rs:261`、`repository_context.rs:720` 移除 `exports`。
|
||||
- `src/app/types.ts` 的 `LocalProjectExportPackageResult` 等类型与前端调用清理。
|
||||
|
||||
**Step 4 验证**:Rust 定向测试;`agc:skill-pack:check`(taonier 部分绿,taptap 红为已知接受项);AGC typecheck;契约测试。
|
||||
|
||||
### 5.3 M3 AGC 陶泥儿面板与发布媒体直传
|
||||
|
||||
**Step 1 草稿模块补齐**:`export/draft/taonier/{dto,error,registry,commands}.rs` 落地 4 命令(`read_taonier_export` / `read_taonier_export_hash` / `save_taonier_export_form` / `run_taonier_export_build`,前端另需线上值读取)。
|
||||
|
||||
**Step 2 前端状态与错误**
|
||||
- `export/state/taonierFields.ts`:`TaonierField = 'title'|'summary'|'category'`;`TAONIER_FORM_FIELDS`;`validateTaonierForm()` 返回**全部**错误(必填 + 长度,码点计数)。
|
||||
- `export/state/taonierApi.ts` / `taonierFailure.ts` / `taonierInstruction.ts`:三命令 Error 载体 + guard + 包装;可修复分类;adapt/fill/repair 指令(引用 `vite-export-taonier` 与 `.export/taonier.json` 形状)。
|
||||
- `export/state/useTaonierExport.ts`:自动保存(baseHash)、2s 轮询、冲突、失败、`packageAll`、`adapt`、`repair`、`fillForm`、`handFieldErrorsToAgent`、`online` 与「沿用线上值」动作;返回面与 xhs hook 同构。
|
||||
|
||||
**Step 3 UI**
|
||||
- `tabs/taonier/`:`Page.tsx`(2 列)、`ArtifactsPane.tsx`(表单槽/读取中/重试/冲突/唯一的「打包并发布」入口/两个 dialog)、`FormCard.tsx`(逐字段红字 + 帮我填 + 交给陶泥儿 + 线上值逐字段内联沿用)、`ConflictCard.tsx`、`FailureDialog.tsx`、`AdaptPromptDialog.tsx`。
|
||||
- 复用 `common/PaneButton`、`ThemedModal`,去掉旧 `PublishPane` 对 `GameDistributionPublishFormView` 的依赖。
|
||||
|
||||
**Step 4 发布媒体**
|
||||
- `src-tauri/src/game_distribution_publish.rs`:读 `.export/taonier.json` 与图片路径,构建 multipart 请求(新图读文件字节;空路径发线上 objectKey),沿用 `publish_local_project_game` 语义与异步任务状态;幂等与错误语义对齐 M1。
|
||||
- 删前端 `useGameDistributionPublishForm`、`GameDistributionPublishFormView`、`GamePublishBlockedDialog` 与 `services/gameDistributionPublish.ts` 中 `suggestGameDistributionPublishMetadata` / `generateGameDistributionCover` / `readGameCoverGenerationPrice` 调用;删对应 Rust 命令与注册。
|
||||
- 保留 `GamePublishProgressDialog` / `GamePublishPhaseSteps` / 发布状态机。
|
||||
|
||||
**Step 5 测试**:`tests/taonierFields.test.ts`、`tests/taonierExport.test.tsx`(对齐 xhs 用例面:指令、左半 UI、逐命令失败表、hook 状态与「校验不过不触宿主」)、`tests/taonierContract.test.ts`(前端常量 vs Rust `layout.rs`);Rust 命令与发布单测。
|
||||
|
||||
### 5.4 M4 文档与共享记忆
|
||||
|
||||
- 回写主规范章节;更新 `decision-log.md`(命名约定、硬切、objectKey 授权、E1、skill-pack 例外)、`pitfalls.md`(multipart 幂等摘要、幂等键、taptap 未登记红)。
|
||||
- 删除本临时计划与里程碑规范;运行 `npm run check:doc-index`、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 6. 验证与证据矩阵
|
||||
|
||||
| 证据 | 内容 |
|
||||
| --- | --- |
|
||||
| 规范对照 | §3 各条 vs §4 里程碑验收逐项结果 |
|
||||
| 自动化验证 | 后端 `cargo test`、`check:spacetime-schema`、`check:generated-bindings`;前端 `typecheck` + 定向 vitest;`check:encoding`、`git diff --check`、`check:doc-index` |
|
||||
| 运行时验证 | `npm run dev:api-server` + `/healthz`;multipart 创建/编辑/发布 smoke;AGC 真实栈一次适配→打包→发布 |
|
||||
| 边界验证 | owner 归属、沿用 objectKey 校验、图片大小/张数上限、幂等重放、路径越界/符号链接拒绝、公开读授权只对已发布游戏生效 |
|
||||
| 未验证项 | 真实 OSS 与对象生命周期、web 平台端真实发布、`agc:skill-pack:check` 的 taptap 红 |
|
||||
|
||||
## 7. 风险与回滚
|
||||
|
||||
- **读签名重构回归**:抽公共 helper 会同时动 `/api/assets/read-url`/`read-bytes`;必须用素材域现有测试锁定授权与过期语义不变。
|
||||
- **SpacetimeDB 删字段**:无历史数据但仍需 `migration.rs` 与绑定;回滚 = 撤销 M1 提交,schema 随代码回滚。
|
||||
- **multipart 幂等**:digest 输入必须覆盖图片字节,否则重放会重复建图;以测试锁定。
|
||||
- **`agc:skill-pack:check` 红**:由未跟踪 taptap 造成,接受为已知项;若 CI 必须绿,需先收掉或登记 taptap(D15 之外)。
|
||||
- **发布范围大**:M1 与 M3 都改发布;小程序化落地顺序 = M1 → M2 → M3,M1 未验收前 M3 不依赖其接口。
|
||||
|
||||
## 8. 未决项
|
||||
|
||||
无。§2 决策与 §3 主规范已覆盖全部边界;taonier 与 xhs/taptap 同构(`build:taonier` + `dist-taonier`,标准 `build`/`dist` 不被占用),构建 flag 由 skill 的 `pack.mjs` 自带默认值、宿主不传;web 平台端不设灰度开关(硬切,D12)。实现中若发现行为需变化,按《规范驱动开发工作流》顺序回改:主规范 → 未完成里程碑规范 → 当前实现计划 → 代码与测试。
|
||||
@@ -0,0 +1,54 @@
|
||||
# 【里程碑】AGC命令沙箱Node版本管理器支持-2026-10-07
|
||||
|
||||
| 字段 | 值 |
|
||||
| ----------- | ------------------------------------------------------------------- |
|
||||
| Version | 1.0 |
|
||||
| Status | implemented-awaiting-runtime-acceptance |
|
||||
| Date | 2026-10-07 |
|
||||
| Parent Spec | `docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` |
|
||||
|
||||
## 目标
|
||||
|
||||
开发构建的 Linux 命令沙箱能原生解析并复用宿主 fnm / nvm 托管的 Node 安装:宿主发现与沙箱只读挂载使用同一套窄叶校验;`.nvmrc` / `.node-version` pin 权威、`engines.node` 仅为偏好;npm 以受信任 `node <npm-cli.js>` 形态启动且不丢失 `npm install` 的联网判定。不再要求用户改系统 Node、把宿主 shim 指向 `/usr/bin`,或把托管目录软链进系统路径。
|
||||
|
||||
## 范围
|
||||
|
||||
- 移除把 `node` / `npm` / `npx` 指向 `/usr/bin/*` 的宿主 shim 依赖,开发构建默认解析托管安装。
|
||||
- 枚举 fnm `node-versions/<version>/installation`(含 `aliases/default`)与 nvm `versions/node/<version>`,按窄叶规则校验完整安装前缀。
|
||||
- 版本选择:`.nvmrc` / `.node-version` pin > PATH 可解析的可用 Node > 版本管理器回退链(`engines.node` 最高匹配 > 活动版本 > 默认别名 > 已安装最高版本)。
|
||||
- Linux 命令沙箱把通过校验的完整安装前缀只读挂载,并以 `node <npm-cli.js>` 启动 npm;`npm install` 保持联网判定。
|
||||
- 沙箱内联环境不继承 fnm multishell 等临时版本管理器变量。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 不把 `fnm` / `nvm` CLI 本身做成沙箱内可用工具。
|
||||
- 不改变发布构建的 bundle / 系统 Node 解析策略。
|
||||
- 不改变 Windows 的 npm.cmd 处理与 V1.10 固定 program 口径。
|
||||
- 不引入 fnm / nvm 之外的版本管理器。
|
||||
- 不为了兼容挂载整个用户 HOME、`FNM_DIR` / `NVM_DIR` 根或 `aliases` 目录。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
- 主机已安装 fnm 或 nvm,且至少有一个 Node 22 安装;项目按 `@types/node ^22.14` 面向 Node 22。
|
||||
- Linux bubblewrap 可用;测试主机需能创建 user / mount / pid namespace。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [x] 开发构建在没有 `/usr/bin` shim 的情况下从 fnm / nvm 找到 Node,并在沙箱内运行出真实 `node --version` 与 `npm --version`。
|
||||
- [x] `.nvmrc` / `.node-version` 指定的版本未安装时命令失败关闭,且不回退到其它已安装版本;`engines.node` 不匹配时不阻塞。
|
||||
- [x] 不支持的 pin 写法(如 `iojs`、`>20`、`<=20`)按未 pin 处理并回退,而不是误判为「已理解但未命中」。
|
||||
- [x] 只有通过窄叶校验的完整安装前缀会被只读挂载;HOME、管理器根、`aliases`、宽泛目录、不完整前缀和逃逸 symlink 全部失败关闭。
|
||||
- [x] npm 在 Linux 上以受信任 `node <npm-cli.js> ...` 启动,`npm install` 仍被判定为联网命令,普通 `npm run` 仍离线。
|
||||
- [x] 宿主发现与沙箱挂载共用同一套窄叶校验,不存在第二份信任口径。
|
||||
- [x] 真实 nvm(v0.40.8 + Node v22.23.3)安装前缀的端到端沙箱运行,以及在仅 nvm 环境(`NVM_DIR` + 空 PATH + 临时 HOME)下的托管解析;同时修正 nvm `alias/default` 只写主版本号(如 `22`)时被当作 `(22,0,0)` 而匹配不到已安装补丁版本的问题。
|
||||
|
||||
## 证据要求
|
||||
|
||||
- 自动化:
|
||||
- `cargo test --locked -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell -- environment_check:: --test-threads=1`
|
||||
- `cargo test --locked -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell -- command_sandbox:: command_exec:: process_session:: --test-threads=1`
|
||||
- 可选真机:`GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1 cargo test … -- command_sandbox_real_linux_opt_in_runs_host_node_and_npm_cli`
|
||||
- 可选真机 nvm:`NVM_DIR=<nvm 目录> GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1 cargo test … -- command_sandbox_real_linux_opt_in_runs_nvm_installation_prefix`;仅 nvm 解析:`env -i HOME=<临时家目录> NVM_DIR=<nvm 目录> PATH=/nonexistent GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1 <测试二进制> --exact environment_check::tests::real_node_npm_environment_versions --ignored`
|
||||
- `cargo fmt --check`、`npm run check:encoding`、`git diff --check`。
|
||||
- 运行时:真实 fnm v22 前缀下,bwrap 内 `node -e 'process.stdout.write(process.version)'` 与 `node <npm-cli.js> --version` 均成功且版本为 v22。
|
||||
- 边界:`.nvmrc` pin 未安装、`engines.node` 不匹配、不支持 pin、宽泛 / 不完整前缀、逃逸 symlink、`npm run` 离线与 `npm install` 联网。
|
||||
@@ -0,0 +1,108 @@
|
||||
# AGC 错误具体文本展示
|
||||
|
||||
Version: 1.1
|
||||
Status: implemented-awaiting-runtime-acceptance
|
||||
Date: 2026-10-04
|
||||
Parent Spec: `docs/adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md`
|
||||
|
||||
## 目标
|
||||
|
||||
DirectProject 回合失败在确认不是客户端内部不可归类故障时,向用户显示可行动的脱敏事实:上游 HTTP 状态、上游返回文本、网络/IPC/进程传输原因、流协议错误、内存不足等操作系统错误。保留机器字段用于诊断,但不再用通用“执行通道中断”覆盖可识别原因。
|
||||
|
||||
## 范围
|
||||
|
||||
- `turn.completed.failure` 的前端展示:按 typed 变体显示分类、状态码和脱敏后的 `detail` / `diagnostic`。
|
||||
- Rust 侧错误文本脱敏:只替换敏感值、URL、绝对路径和私钥内容,保留 HTTP 状态、错误码、字段名和可行动描述。
|
||||
- app-server `codexErrorInfo` 只有嵌套机器字段时,保留经有界脱敏的结构化值;`error` 是字符串或未知 JSON 形状时也保留正文;`fields=codexErrorInfo` 不能成为唯一正文。
|
||||
- 素材/参考图/资源编辑上传、资源编辑轮询、模型目录初始化和 Direct MCP stdio 的网络、JSON、写入、读写失败保留底层错误正文;不能由 `map_err(|_| 固定句)` 把它们压成无因的连接或失败提示。
|
||||
- 插件 stdout RPC 的 `fill_buf`、UTF-8 和响应大小错误通过挂起 RPC 传播,不能在读取线程里吞掉后统一说“插件进程已退出”。
|
||||
- Codex CLI/Agent Runner 及本地素材/字体读取保留 stderr、join、序列化/解析、UTF-8 和 OS 错误正文;路径边界拒绝仍保留稳定安全码。
|
||||
- Codex Provider proxy 的请求体改写、上游发送、响应构造和 SSE 错误保留底层正文,避免代理层再次把具体网络/解析故障压成单一 unavailable。
|
||||
- 浏览器健康检查不再把发现、配置、DevTools 版本和清理失败只返回机器码;阶段码保留,同时带底层错误或明确的超时预算。
|
||||
- WebView 结构化对象错误保留 `message/detail/reason/code` 或有界 JSON 正文,避免 `[object Object]` 触发普通错误兜底。
|
||||
- Direct 工具桥图片与真实试玩证据读取/哈希错误保留 OS 正文,安全边界拒绝继续使用稳定码。
|
||||
- `agc_read_project_context` 保留 `file-read-failed` 等稳定码,同时在能取得时附带文件打开/读取/复核的 OS 正文。
|
||||
- Direct 交付复核和执行账本的 worker、产物读取、执行器摘要及宿主目录/锁失败保留底层原因,分类码不再单独承载正文。
|
||||
- Transport / Stream / IPC / host process / memory exhaustion 的回归测试与错误事件证据。
|
||||
|
||||
## 不做
|
||||
|
||||
- 不把 access token、Cookie、API Key、私钥、完整 URL 查询参数、绝对路径原文显示给用户。
|
||||
- 不改变上游协议、重试预算、计费和项目写入安全边界。
|
||||
- 无法获得更深原因时仅展示宿主真实观测事实,不把 Drop 伪造成网络中断;不回填旧版本缺失的原因。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. HTTP 401/403/408/429/5xx 显示状态码与脱敏后的上游错误文本。
|
||||
2. `connectionFailed`、`transportBroken`、`streamUnavailable`、`transportClosed` 显示对应分类及安全 detail;detail 不为空时不得落到通用错误句。
|
||||
3. 包含 `authorization=...`、`token=...`、Cookie、URL query、Windows/Unix 路径、私钥块的错误,只替换敏感值并保留同一行中的状态码和其它诊断字段。
|
||||
4. 包含 `out of memory`、`ENOMEM`、Windows 内存不足文本或 IPC/stdio 失败文本时,用户能看到对应安全文本。
|
||||
5. HostDropped 保留 panic hook 可取得的负载和位置,普通 Drop 明确仅观测到任务未写终态退出;显式捕获的 panic/transport/IPC 错误继续走 typed failure。旧无 detail 载荷可重放,新 detail 脱敏后进入同一条失败事件。
|
||||
6. 运行前端 DirectProject 错误映射测试、Rust redaction/runtime_error/turn_error 定向测试、TypeScript 类型检查、编码检查和 `git diff --check`。
|
||||
|
||||
## 当前证据
|
||||
|
||||
- `npx vitest run tests/directTurnFailure.test.ts`:15 passed。
|
||||
- `npx tsc --noEmit -p apps/ai-game-creator-shell/tsconfig.json`:通过。
|
||||
- Rust app-server 上游映射定向测试:9 passed;覆盖嵌套 `codexErrorInfo`、只有机器字段和非对象错误正文;应用日志脱敏测试与启动诊断脱敏测试均通过。
|
||||
- CC 错误分类回归:Claude sidecar 状态码/超时映射与非支付 409 保留上游状态的定向测试通过;侧车失败统一补充有界 stderr detail。
|
||||
- 真实 AGC dev smoke:客户端使用 `3080`、后端 `8084`、数据库 `3101`、后台 `3103` 启动;真实 CC 回合复现 `Reached maximum number of turns (8)` 被旧代码错误记为 `transport-closed`,修复后 Tauri 已热重编译重启。修复后的真实 Provider 回放未再次发送,避免无必要的付费请求。
|
||||
- 真实 AGC 项目诊断发现真实请求 HTTP 400 曾被摘要为 `codex-app-server-error:other detail=fields=codexErrorInfo`,另有 401 仅显示 `codex-app-server-error:unauthorized`;该漏损已在 app-server 投影边界修复。真实 Provider、真实 IPC 断链和真实内存压力仍未在本轮主动制造。
|
||||
- 继续扫描发现素材/参考图/资源编辑上传、模型目录初始化和 Direct MCP stdio 仍有固定 `map_err(|_| ...)` 丢正文,已改为保留底层网络、serde、写入和 OS 错误;平台素材上传定向 3 项、模型目录定向 4 项、Direct MCP 断输出定向 1 项通过。资源编辑全套并行测试本轮 54 项通过、11 项因共享 fixture/本地端口或账号环境超时失败,不能把该套结果记为全绿。
|
||||
- 本轮继续扫出三类未收敛口:外部 MCP HTTP 的 401/429/桥上下文缺失原先只有空状态码响应,现改为保留 HTTP 状态、稳定错误码和中文正文;认证 5xx 结构化载荷、账户/素材上传/发行发布的 401/403 现保留服务端 message/code(仍精确脱敏)。
|
||||
- 前端历史错误文本此前会把含有敏感字段的整条 Direct 诊断直接丢弃,现改为只替换凭据、链接和路径;旧版已经落盘且无法恢复正文的“执行通道中断”条目会明确追加“宿主未提供具体错误正文”,不再伪装成当前仍在吞错。
|
||||
- 新增/通过:外部 MCP HTTP 401/429 定向 2 项、前端 Direct/Runtime 错误正文 25 项、旧历史通用中断脱敏回归 1 项;认证 Rust 定向 19 项、素材上传响应错误定向 1 项通过。`appSurface` 全套仍有环境/fixture 相关失败,不能据此宣称全绿。
|
||||
- 继续扫描又发现画板项目/资产下载、模板库、图片分离、参考图上传、External Editor 生成恢复、资源编辑上传/轮询、项目快照和错误报告提交仍有“HTTP 只有编号”或未脱敏正文路径;已统一补上状态码 + 有界正文 + 精确脱敏。资源编辑拒绝原因继续复用既有 `details.message` 解析,避免丢掉平台具体原因。
|
||||
- 前端继续发现两个静默出口:模板库权限检查失败会直接清空入口,Direct 线程订阅/consume/bootstrap 的 IPC 失败会只 settle anchor 而不显示正文;现分别保留权限检查错误,并把订阅链错误正文送入项目状态栏/运行错误出口。认证失败回归已验证显示 `authentication-required`,不再变成泛化“执行失败,请稍后重试”。
|
||||
- UI 编辑器复制路径的原生剪贴板 IPC 失败也曾只显示“复制失败,请手动复制路径”,现保留脱敏后的剪贴板错误正文并同时给出手动复制指引。
|
||||
- 受控联网搜索的上游非 2xx 响应原来只返回 HTTP 状态;现保留有界、脱敏的服务端正文,错误码/限流原因不再丢失。
|
||||
- 窗口标题栏的 Direct 活动回合快照读取失败原来只给“读取失败”标志并保留旧快照;现额外保留脱敏后的 IPC/读取正文,面板不会把真实原因吞掉。
|
||||
- 项目打开时读取 `conversation.read` 权限策略的 IPC 失败原来静默 return;现把脱敏错误正文写入项目状态和聊天错误提示。
|
||||
- 错误报告通知读取本地待报告快照失败原来只重试后静默消失;现显示“项目诊断读取失败”及安全正文,并提供重试入口。
|
||||
- 共享 Prompt Polish hook 的注入请求/规范化/回填异常原来只显示固定失败提示;现保留精确脱敏后的异常正文,聊天输入和资源润色共用同一口径。
|
||||
- 资源画布历史生成任务账本读取失败原来统一显示“暂时无法恢复历史任务”;现在保留 Tauri/文件读取正文,非数组响应仍单独保留契约错误提示。
|
||||
- 发现默认 `requestChatPromptPolish` 还会把 Tauri 润色命令异常吞成 `null`,导致共享 hook 无法看到平台正文;现仅对空回包/桥不可用保留 `null`,原生异常继续上抛并统一脱敏。
|
||||
- 润色提醒偏好读取失败原来被当成“未关闭”默认值,写入失败只进错误上报池;现把读取/保存的 IPC 正文显示在输入区状态提示中,同时仍维持安全默认和回退行为。
|
||||
- Direct 项目清单首次读取和清单事件订阅失败原来只让 `@` 候选退化为空;现把脱敏后的读取/订阅正文送入项目聊天状态条。
|
||||
- Direct 活动回合事件桥注册失败原来只退回启动快照,面板没有说明后续更新已失联;现保留事件桥 IPC 正文并在标题栏/活动项目面板显示。
|
||||
- 首页富文本输入的原生剪贴板图片/文本读取失败原来直接按空剪贴板处理;现通过输入区错误提示保留脱敏后的剪贴板 IPC 正文。
|
||||
- 账号邀请码复制和微信充值状态确认仍有固定“复制失败/暂时没能确认到账”路径;现保留剪贴板/支付查询错误正文并继续给操作指引。
|
||||
- 游戏发布面板的截图上传、发布进度订阅和最终发布异常也有固定文案/裸 `Error.message` 路径;现统一通过可见错误脱敏出口展示正文。
|
||||
- 进入项目时预览活体核验的 IPC 失败原来只按“没有可复用预览”处理;现在保留安全正文并写入工作区状态,同时不误停未知活体。
|
||||
- 继续真实客户端启动和旁路扫描又发现三处仍会损失事实:旧历史通用中断文案遇到换行/多空格时未命中“正文缺失”标记,非 2xx 响应正文读取失败被 `unwrap_or_default()` 抹掉,CC sidecar 的 stderr 排空超时/读取任务失败也被当成“没有 stderr”。现分别改为折叠空白识别、保留“响应正文读取失败:底层原因”和保留 stderr 排空阶段/底层原因;不改变状态码、重试和脱敏边界。
|
||||
- 第二轮 Agent/CC 旁路扫描继续发现执行许可/执行会话/MCP 本地读取/编辑器 worker/Cocos worker 的 JoinError,以及 MCP future panic,会被固定成“任务未返回/异常/需要核对”;现把 join/panic 正文并入既有 ToolFailure / MCP 错误载荷,保留 `needs-reconciliation` 的不可自动重放语义。Native 定向测试:Direct MCP 33 项、Direct Tool Bridge 41 项、CC 14 项均通过。
|
||||
- 最新漏扫又补出 Direct Tools MCP 启动/运行退出码、Skill `resources/read` 索引与资源读取、交付 `finish_sealing` 错误三处固定句;现在分别写入 stderr、MCP 错误正文和交付复核底层原因,避免只剩退出码或“暂不可用/复核失败”。
|
||||
- 前端继续实测发现预览活体复用的 IPC / 权限拒绝会被当成“没有活体”并静默重启,资源生成事件订阅失败也只回退轮询;现在两条回退仍保留,但分别把复用失败正文附到重启结果、把订阅失败正文附到任务阶段。预览与生成队列定向测试 25 项通过。
|
||||
- 继续扫前端外围又发现 Direct 回合后的清单刷新、资源卡片预览未知错误、UI 编辑器单图预览读取失败仍会静默或使用无因固定句;现分别进入运行错误出口、保留脱敏后的预览错误正文并在编辑器状态提示中显示。
|
||||
- 资源依赖图读取失败原先只切到 fallback 布局状态,用户看不到 IPC/权限正文;现把安全错误保存到状态并在依赖画布内联显示。
|
||||
- 资源画布布局读写失败原先只把原因写日志、界面仍显示固定“布局失败”;现把脱敏后的 revision/IPC/权限/磁盘正文附到读取与保存提示,原有回退布局行为不变。
|
||||
- 又扫出发布状态、扩展列表、应用更新和生成任务的“无正文时固定句”分支;这些分支现在明确显示“未提供具体错误正文”,不再让空原因伪装成完整错误。
|
||||
- 项目权限策略读取还有两条调用方会在 IPC 异常时直接返回 `false`;现把脱敏后的读取错误同时写入工作台状态和项目聊天错误,不再把“未获权限”与“策略读取失败”混为一谈。
|
||||
- 策划 Agent 的 Tauri 事件订阅失败原先只放行回合等待、没有任何用户提示;现把订阅 IPC 正文写入项目聊天错误出口,避免只收到不完整的最终态。
|
||||
- 专业 Agent 历史回读与项目清单同步的后台 Promise 失败原先只返回 `failed` 或静默结束;现把具体读取/同步正文写入项目聊天错误出口。
|
||||
- Direct 交付计划/合同 JSON 解析、策划工作区目录枚举与文本搜索还有 `map_err(|_|)` / `Err(_)` 丢细节;现保留 serde、目录和文件读取正文,安全路径边界仍单独拒绝。
|
||||
- 直连 Codex 输出指纹此前把除 `NotFound` 之外的所有读取错误都当成“文件不存在”,可能误判产物未变化;现只对确实不存在的文件保留空位,其余磁盘/权限错误沿回合失败正文上送。
|
||||
- DirectProject 上下文的外部路径元数据读取失败原先统一成 `file-not-found`;现区分确实不存在与权限/磁盘读取错误,后者保留底层正文。
|
||||
- 错误报告对话框读取最新错误事件或诊断日志失败时原先只显示“暂不可用/使用快照”;现把读取 IPC/文件正文附在状态提示中,快照回退行为保持不变。
|
||||
- WorkspaceLauncher 的清单版本读取、失效事件重读和事件订阅失败原先只写日志或静默回退;现把安全正文送入启动器状态提示,仍保留已有回退刷新路径。
|
||||
- Direct 补丁事务的目标指纹读取原先把元数据、打开、正文读取和文件超限都折叠为 `None`,可能误判文件缺失或继续比较;现只对确实 `NotFound` 保留 `missing`,其余失败返回目标相对路径、阶段和底层正文,避免补丁结果被错误归因。
|
||||
- CC/Direct 工具桥的执行前后成果指纹、回执结算 JoinError/落盘失败,以及外部 MCP journal 的读取/JSON 行解析原先仍会被 `.ok()`、固定回执句或“无记录”吞掉;现分别保留指纹阶段、回执任务/落盘原因、journal 行号和底层正文,只有真实 `NotFound` 才视为空记录。
|
||||
- 外部 MCP `resources/read` 仍把已能返回具体原因的 Codex journal 读取错误 `unwrap_or_default()` 成空资源;现把读取失败按 MCP 错误响应返回,Direct 执行会话当前项目 canonicalize 也保留底层 I/O 正文。
|
||||
- Direct 交付终态读取原先把 `session.snapshot()` 锁/状态错误折叠成 `None`,上层继续显示“尚未确认交付完成”;现沿 `code-generation` 失败出口保留终态读取正文,并在已有回合失败时同时保留原始失败与终态读取失败。
|
||||
- 对话模型选择器读取 native 配置原先把 IPC/文件异常 `.catch(() => null)` 后只显示“读取客户端配置失败”;现通过统一可见错误脱敏出口保留具体正文,并保留配置不可读时不猜测模型路由的安全行为。
|
||||
- 策划工作区事件订阅失败仍有一处直接静默结束,导致文件刷新失联却没有提示;现把订阅 IPC 正文写入工作区错误状态,继续保留手动刷新与已有清单读取错误出口。
|
||||
- 新一轮原生扫出 app-server 进程与上下文漏损:Codex 执行器核验/OAuth 保存/回合身份的 JoinError、图片 base64 解码、Direct 上下文 canonicalize,以及进程树归属、Job、等待、终止和 Drop 清理错误原先只剩固定码;现保留 JoinError、解码器、OS/Job、等待和终止阶段正文,并为进程树 Drop 清理失败写入诊断日志。
|
||||
- 继续扫出浏览器收割的 Job/root kill/wait 错误原先压成 `bool`、External Editor 本机凭据 JSON/目录创建/保存错误丢底层正文,以及生成任务中断收口把项目清单读取失败当成“状态未知”;现分别保留浏览器清理阶段正文、凭据解析/存储正文,并让非“项目尚未初始化”的清单读取失败直接返回具体错误。
|
||||
- 受控 command 执行路径仍会把 leader 启动身份读取、进程组终止、root kill、Job/wait 失败折叠为“身份未确认/退出未确认”;现拒绝无身份接管并保留身份读取、终止兜底、等待和 OS 错误正文。
|
||||
- 继续扫到本地资源导入、平台素材 data URL、External Editor binding origin、项目模型记录读取/JSON 解析等错误仍丢底层正文;现区分真实 NotFound 与权限/元数据失败,并保留 base64、URL、文件读取和 JSON 解析细节。
|
||||
- 资源编辑上游 4xx 正文的流读取失败、正文超限、非 UTF-8 和未知 JSON 形状原先都会回落到笼统失败;现保留读取阶段、大小上限、编码错误和有界脱敏原文。
|
||||
- 认证状态订阅底层原先先把 IPC 失败转成空注销函数,导致上层新增的错误提示也收不到;现让订阅拒绝向登录页传播,后台身份投影失败时清除身份并留痕。
|
||||
- 发布构建预检原先把 `package.json` 元数据/读取/解析错误当成“没有 build”,把 `package-lock.json` 读取/解析错误当成“没有锁定版本”;现分别区分 NotFound 与真实读盘/JSON 错误,保留相对工作区路径和底层正文。
|
||||
- 发布尝试账本和工作区偏好文件原先把损坏、权限、类型和 JSON 解析失败当成空账本/默认偏好;现只有文件不存在才使用默认,其他读取错误沿发布幂等和建项目录命令返回具体正文。
|
||||
- 插件主机扫描工作区、读取进程状态、停止插件和禁用插件时原先吞掉目录项/`try_wait`/kill/wait 错误;现保留插件状态错误并阻止“停止失败却显示已禁用”,正常主动 kill 的非零退出码不再误报。
|
||||
- 插件进程 stderr 原先使用 `Stdio::null()`,即使进程退出也只能显示退出码;现以有界脱敏 sidecar 保留插件 stderr,并在退出状态中附带正文。
|
||||
- 新增发布尝试账本损坏回归:损坏记录现在阻止生成新的根幂等键,并返回账本 JSON 解析正文,避免静默重复发布。
|
||||
- 首页建项/打开项目读取本地 revision 失败原先吞成 `null` 并继续进入项目;现保留降级进入行为,同时把 IPC/文件读取正文写入首页状态。
|
||||
- 发布面板读取封面生成价格失败原先只清空价格并继续显示泛化消耗提示;现保留价格查询的脱敏正文并在确认弹窗中展示。
|
||||
- 资源模型卡、素材引用选择器和本地图片导入预览原先失败只降级成图标/文件名;现保留脱敏后的 WebGL、IPC、远程读取原因,并通过卡面 title/aria 或提示文本展示,降级预览行为不变。
|
||||
- 项目快照工作区登记失败原先只写固定诊断键,实际 IPC/权限/路径原因丢失;现把统一脱敏正文写入应用诊断日志,并保留重试登记行为。
|
||||
@@ -0,0 +1,45 @@
|
||||
# 【里程碑】CC复用Codex运行态事件
|
||||
|
||||
| 字段 | 值 |
|
||||
| ----------- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | implemented-pending-acceptance |
|
||||
| Date | 2026-10-04 |
|
||||
| Parent Spec | `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` |
|
||||
|
||||
## 目标
|
||||
|
||||
让 CC DirectProject 的流式文本、过程、MCP 工具调用和工具结果实时进入现有 Thread Manager 事件合同,并由现有聊天 reducer、工具卡片和历史投影统一展示。
|
||||
|
||||
## 范围
|
||||
|
||||
- sidecar 事件逐条回调到 Rust。
|
||||
- Claude stream event、assistant tool_use、user tool_result、工具进度和终态的安全投影。
|
||||
- CC 事件对应的 `ThreadItem` 历史追加与重复事件幂等。
|
||||
- Rust adapter、ThreadEvent/ThreadItem 现有合同的定向测试和前端 reducer/工具卡片回归。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- Claude Code 原生权限、MCP server、模型路由、会话恢复和取消语义重构。
|
||||
- 新增前端 CC 专用组件、公开 API、OpenAPI 或 SpacetimeDB 迁移。
|
||||
- 将未经安全投影的内部控制帧或完整思维链直接展示。
|
||||
|
||||
## 依赖与前置条件
|
||||
|
||||
- 现有 `ThreadEvent` / `ThreadItem` / `directThreadChat` 合同保持兼容。
|
||||
- Claude Agent SDK 当前 stream-json 消息包含 assistant content blocks 和 user tool_result。
|
||||
- 工作树已有的无关未跟踪文档保持不动。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] CC `stream_event` 文本在 `result` 前生成 `item.delta`。
|
||||
- [ ] CC 工具调用先生成运行中 `mcpToolCall`,结果生成同身份完成/失败条目。
|
||||
- [ ] CC 过程文本使用现有 reasoning 条目,不增加前端 agentMode 分支。
|
||||
- [ ] 成功回合历史可重读,重复追加幂等;失败回合不伪造成功终态。
|
||||
- [ ] Codex 现有定向测试保持通过。
|
||||
|
||||
## 证据要求
|
||||
|
||||
- 自动化:Rust `claude_code_cli` / thread wire / Direct runtime 定向测试,前端 `directThreadChat` 与工具卡片测试。
|
||||
- 运行时:若本地 Claude SDK/Provider 可用,执行一次真实 CC Direct 回合;否则用 SDK stream-json fixture 明确记录未完成项。
|
||||
- 边界:tool_result 错误、缺失 tool id、未知控制帧、重复结果、取消/失败终态。
|
||||
@@ -0,0 +1,33 @@
|
||||
# 【里程碑】陶泥儿导出文件草稿化与发布媒体直传
|
||||
|
||||
- Version: `v1`
|
||||
- Status: 已实施(M1–M4 已落地)
|
||||
- Date: `2026-10-06`
|
||||
- Parent Spec:
|
||||
- `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`(导出产物面板与陶泥儿导出)
|
||||
- `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`(游戏分发发布媒体直传合同)
|
||||
- `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`(游戏分发媒体与 schema)
|
||||
- 实现计划:`docs/project-memory/plans/【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md`
|
||||
|
||||
## 目标
|
||||
|
||||
把陶泥儿导出迁移到文件草稿 + agent 适配打包,并把游戏分发发布媒体改为 formdata 原始二进制/objectKey 直传,整体退役 `exports/` 与发布链路里的 assetId。
|
||||
|
||||
## 边界
|
||||
|
||||
- 含:`.export/taonier.json` 草稿、`build:taonier`/`dist-taonier`/`.export/taonier.zip`、adapt/fill/repair、线上值与「沿用线上值」、发布媒体 formdata 直传与新媒体读路由、`exports/` 全退役、AI 建议/封面与旧发布表单退役、web 平台端同步迁移。
|
||||
- 不含:图片生成计费链路、`/api/external/v1`、历史数据迁移、`vite-export-taptaph5` 登记。
|
||||
|
||||
## 里程碑与验收标准
|
||||
|
||||
| 里程碑 | 目标 | 验收标准 | 依赖 |
|
||||
| --- | --- | --- | --- |
|
||||
| M1 发布媒体合同解耦 | 公共读签名重构;shared-contracts/spacetime-module/api-server/web 改 objectKey + multipart;新媒体读路由 | 后端定向测试、`check:spacetime-schema`、`check:generated-bindings`、api-server smoke、web 发布用例 | 无 |
|
||||
| M2 AGC 打包链路与 exports 全退役 | skill + `build:taonier`/`dist-taonier` + `run_taonier_export_build`;删 `exports/` 全链与旧打包门 | Rust 定向测试、skill pack(taonier 部分)、AGC typecheck、契约测试 | 无 |
|
||||
| M3 AGC 陶泥儿面板与发布媒体直传 | 草稿模块与前端状态/UI;线上值与沿用;发布直传;退役 AI 建议/封面与旧表单 | vitest、Rust 命令测试、真实栈发布 smoke | M1、M2 |
|
||||
| M4 文档与共享记忆 | 主规范回写、decision-log/pitfalls、清理临时计划 | `check:doc-index`、`check:encoding`、`git diff --check` | M1–M3 |
|
||||
|
||||
## 依赖与门禁
|
||||
|
||||
- M1、M2 可并行;M3 依赖两者。
|
||||
- 每个里程碑验收通过前不得进入下一个;实现中行为变化按《规范驱动开发工作流》顺序回改主规范。
|
||||
@@ -1,5 +1,81 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-10-07 AGC 命令沙箱原生支持 fnm/nvm:只读挂载窄叶安装前缀,npm 改走 node + npm-cli.js
|
||||
|
||||
- 背景:开发构建里 AGC 让命令沙箱执行 `npm run build` / `npm install` 时,宿主 Node 由 fnm 托管,`node` / `npm` 实际是随 shell 会话变化的 fnm multishell 目录里的 shim;bwrap `--tmpfs /run` 会抹掉该路径,而只按单文件挂载 `<前缀>/bin/npm`(它软链到 `lib/node_modules/npm/bin/npm-cli.js`)会因 `Cannot find module '../lib/cli.js'` 失败。此前把宿主 `node` / `npm` / `npx` shim 指到 `/usr/bin/*` 是错误取舍:系统 Node 26 默认启用实验性 Web Storage,会顶掉 vitest 0.34 jsdom 的 localStorage,使 AGC 测试套件在 HEAD 即失败(见 `pitfalls.md` 2026-10-03 条)。
|
||||
- 决策(宿主发现与沙箱挂载共用窄叶校验):新增 `validate_node_installation_prefix`,canonicalize 后拒绝 `/home`、`/root`、`/tmp`、`/var`、`/etc`、`/proc`、`/dev`、`/run`、`/sys`、`/boot`、`/srv` 根、HOME 及其祖先和相对路径,并要求前缀同时含 `bin/node` 与 `lib/node_modules/npm/bin/npm-cli.js`(bundle 形态为 `<前缀>/node` + `node_modules/npm/bin/npm-cli.js`)。宿主版本枚举与 Linux 沙箱只读挂载都调用它,避免两处信任口径漂移。
|
||||
- 决策(版本选择):`.nvmrc` / `.node-version` 是权威 pin,能理解但未安装时返回 `node-version-pinned-not-installed` 失败关闭;`package.json` `engines.node` 只是偏好,永不阻塞;不支持的写法(`iojs`、`||`、部分 `>` / `<=`、hyphen range、prerelease)按未 pin 回退。整体顺序为 pin 命中 > PATH 可解析的可用 Node > 版本管理器回退链(`engines` 最高匹配 > 活动版本 > 默认别名 > 已安装最高版本),只实现文档化比较子集(精确三元组、major、`>=` / `>` / `<=` / `<`、`^`、`~`、`x` / `*`、`lts/*`);`v22.23.3` 这类带 `v` 的 `.nvmrc` 必须先剥前缀。nvm 的 `alias/default` 常只写主版本号(如 `22`),必须按同一 pin 子集在已安装版本里选最高匹配,不能当成完整三元组 `(22,0,0)`。
|
||||
- 决策(沙箱内启动形态):Linux npm 改为 `node <npm-cli.js>`,因为单文件挂载 npm 软链必然丢 `../lib/cli.js`;只读挂载整棵已验证的安装前缀(不是整个 HOME、`FNM_DIR` 或 `NVM_DIR`)。`npm install` 的联网判定跟随真实启动形态(`node` + `npm-cli.js` + `install`),不因包装变化丢 `--share-net`。
|
||||
- 决策(范围与非目标):托管版本管理器发现只在 `debug_assertions` / development 生效,发布构建继续只认随包 bundle;不把 fnm / nvm CLI 做成沙箱内工具;Windows 不变;不新增 fnm / nvm 之外的版本管理器。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/environment_check.rs`(窄叶校验、托管安装枚举、pin / engines 解析、选择与回退)、`command_sandbox.rs`(Node 工具链挂载收集与合并、`command_sandbox_requests_npm_install`、`FNM_MULTISHELL_PATH` 清理)、`command_exec.rs`(Linux npm `node_launcher`、`project_command_actual_target`、非 Node 程序 PATH 前置工具链 bin)、`process_session_bridge.rs`(`ProcessSessionLaunchPlan::from_launch` 改用实际目标)。
|
||||
- 验证方式:`environment_check` 24 passed、`command_sandbox` 14 passed、`command_exec` 19 passed、`process_session` 27 passed(均 `--test-threads=1`);`GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1` 真机 bwrap 内 fnm v22.23.3 的 `node --version` 与 npm 10.9.9 通过;另装 nvm v0.40.8 + Node v22.23.3,`command_sandbox_real_linux_opt_in_runs_nvm_installation_prefix` 证明真实 nvm 前缀可在 bwrap 内跑 node / npm,`real_node_npm_environment_versions`(仅 `NVM_DIR` + 空 PATH + 临时 HOME)证明托管解析确实选中 nvm;`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。验证后 fnm 仍是宿主默认,nvm 未写入任何 shell profile。
|
||||
- 边界:nvm 已在验证主机安装(v0.40.8 + Node v22.23.3)并跑通真实前缀与仅 nvm 解析;CI 仍由临时目录夹具覆盖 fnm / nvm 布局,真实安装路径测试保持 opt-in。真实验收前不宣称发布构建也支持 fnm / nvm。
|
||||
|
||||
## 2026-10-06 小红书导出 validate/pack:Chrome 61 能力按硬性 ERROR 拦下,pack 自带白名单不再共享
|
||||
|
||||
- 背景:真实项目适配反馈。① `validate.mjs` 的 `/#[A-Za-z_$][\w$]*/g`(class 私有字段)直接在原始文本上匹配,把 `"#e8f4ff"`、`document.querySelector('#hit')` 误判为 ES2018 语法并报 ERROR,把排查引向「构建链没转译」。② `CSS_MODERN_PATTERNS` / `MODERN_RUNTIME_API` 告警被移除后,`flex gap`(Chrome 84+)、`min()/max()/clamp()`(Chrome 79+)等晚于 Chrome 61 的写法被静默忽略却无人拦截,`references/manual-checks.md` 只有人读清单没有工具兜底。③ 宿主在**项目根** `.export/xhs-minitool.zip` 找产物,而 `--zip-out` 相对 cwd 解析;npm 脚本挂在 `game/` 子工程时 `.export/...` 会落到 `game/.export/`,没有任何提示。④ `pack.mjs` 直接 `import './validate.mjs'`,只复制 pack.mjs 会 `ERR_MODULE_NOT_FOUND`。⑤ `validate.mjs` 的 `[project]` 默认值只在源码 USAGE 里,SKILL 参数表没写默认值。
|
||||
- 决策(扫描先剥噪音):`scanJsSyntax` 先过 `stripJsNoise`——把注释与字符串字面量替换成等长空白(保留换行),只对真代码跑 ESM 与 ES2018 检测;模板 `${...}` 仍按代码处理。`"#e8f4ff"`、`'#hit'`、注释里的 `obj?.c` 不再命中,真 `#count` 私有字段仍报错。
|
||||
- 决策(Chrome 61 是硬性要求,不是「增强层」):恢复 `CSS_UNSUPPORTED_PATTERNS` / `MODERN_RUNTIME_API` 并判 **ERROR**(`CSS_UNSUPPORTED_FEATURE`),覆盖 `gap`/`row-gap`/`column-gap`(仅 `grid-gap` 与 `-webkit-` 前缀写法豁免)、`aspect-ratio`、`min()/max()/clamp()`、逻辑属性、`overflow: clip`、`:focus-visible`、`:has()`、`@container`、`subgrid`、`@layer`/`@property`、`dvh/svh/lvh`、现代颜色、`backdrop-filter`、`text-wrap: balance`,以及 `Object.hasOwn`/`structuredClone`/`replaceAll`/`.at`。`--strict` 保持移除:退出码只由 ERROR 决定。references 同步删除「基线层 + 增强层」与能力检测启用现代 CSS / 新 API 的写法;`flex gap` 一律用子项 `margin`。`:hover` 关键操作与安全区仍需人读清单。
|
||||
- 决策(产物落点有确定写法与预警):`SKILL.md` 写明产物是项目根的 `.export/xhs-minitool.zip`;`--zip-out` 默认 `../.export/xhs-minitool.zip`(假定 cwd 是 `game/`),相对路径落点位于 `<cwd>/.export/` 时打 `[warn]`,打印 cwd 与绝对落点。
|
||||
- 决策(pack 自带实现 + 参数表补默认值):`pack.mjs` 不再 `import './validate.mjs'`,把 `ALLOWED_EXTENSIONS` / `SKIP_DIRS` / 遍历与 `findUnsupportedFiles` 复制进 pack(改动时两处需同步);`SKILL.md` 参数表补 `validate.mjs [project]` 默认 `dist-xhs-minitool`、`pack.mjs --vite-built-dir` 默认 `dist-xhs-minitool`、`--zip-out` 默认 `../.export/xhs-minitool.zip`、`vite.config` `build.outDir` 默认 `dist-xhs-minitool`。
|
||||
- 影响范围:`resources/agc-skills/vite-export-xhs-minitool/{SKILL.md,references/{css-compatibility.md,js-compatibility.md,manual-checks.md,zip-artifact-spec.md},scripts/{validate.mjs,pack.mjs,vite.config.xhs-minitool.mjs}}`、`resources/agc-skills/manifest.json` 指纹(version=2026-08-26.62)、隐藏测试 `scripts/.validate.test.mjs`、`scripts/.pack.test.mjs`。
|
||||
- 验证方式:`node --test scripts/.validate.test.mjs scripts/.pack.test.mjs scripts/.vite.config.xhs-minitool.test.mjs`(36 passed)、`npm run agc:skill-pack:check`、`git diff --check`、`npm run check:encoding`。
|
||||
- 边界:「web 与小工具共用同一 `game/index.html`」是默认姿势,不需要另起 root;只有入口 HTML 确实不同才需要并存 recipe,本次未写,留待需要时补。
|
||||
|
||||
## 2026-10-06 新增 agc_install_skill_resource:Skill 附件由宿主直接落盘,不走模型正文
|
||||
|
||||
- 背景:`vite-export-xhs-minitool` 等审核 Skill 要把自带脚本(`scripts/validate.mjs` 约 40 KB、`pack.mjs`、`vite.config.*.mjs`)原样复制进用户项目。`agc_read_skill_resource` 的大文件正文会被模型上下文截断,重抄必然失真;原生 `cp` 在 DirectProject 只读沙箱下被审批闸门拒绝。两条路都走不通。
|
||||
- 决策:新增内置 MCP 工具 `agc_install_skill_resource`,入参 `skillName` / `relativePath` / `destinationPath`。宿主在 MCP 进程内直接读 `AGC_SKILL_PACK_FILES` 里已审核的内置字节(复用 `read_agc_skill_resource` 的清单校验与路径拒绝口径),组装成 `agc_write_file` 的 `{path, content}` 转发给客户端受控工具桥。写入因此走与 `agc_write_file` 相同的合同、lease、项目写锁与保护面校验;正文不经过模型上下文,没有截断,也不需要原生 `cp` 及其审批。
|
||||
- 边界:工具只暴露 `agc_write_file` 的两个入参,模型无法自带正文旁路审核;一次调用落一个文件,不打包、不改写、不建目录以外的副作用。DirectProject 的只读原生沙箱与 #439 审批口径不变,本工具是替代 `cp` 的受控通道,不是放开沙箱。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_tools_mcp.rs`(工具目录、分发、`install_skill_resource_write_arguments`)、`prompts/runtime/texts/direct-tools.json`、`prompts/runtime/texts/direct.json`,权威说明同步到技术方案。
|
||||
- 验证:`cargo test --offline -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell agent::`(736 passed,含新增 `install_skill_resource_*` 两条与 `tool_catalog_preserves_reviewed_resource_contracts`)、`npm run check:encoding`、`git diff --check`。
|
||||
|
||||
## 2026-10-06 小红书导出 skill:dist 收尾归 Vite 插件,pack 只读打 zip
|
||||
|
||||
- 背景:`vite-export-xhs-minitool` 的 `pack.mjs` 之前会就地改写 dist(相对路径 / 去 `type="module"` / `crossorigin` / 脚本移到 body 末尾 / 清空目录 / 兜底复制 icon)再打 zip;dist 只有在跑完 pack 后才合规,`validate.mjs` 看到的是中间态,打包脚本也因此同时承担「整理产物」与「打 zip」两件事。
|
||||
- 决策(职责边界):dist 收尾移进 `scripts/vite.config.xhs-minitool.mjs` 的 `xhsMinitoolArtifactPlugin`(`apply: 'build'` + `enforce: 'post'` 的 `closeBundle`);`minitoolBaseConfig` 只保留编译约束。构建结束 dist 即合规,`pack.mjs` 只做白名单预检与打 zip,不再写盘;`--vite-built-dir` / `--zip-out` 契约与产物落点不变,已适配项目无需改脚本。
|
||||
- 决策(失败语义):rollup 在构建失败时也会调 `closeBundle`,插件用 `buildEnd(error)` 记下失败并直接返回,避免留下只复制了 icon 的半成品 dist,也避免用插件的「index.html 不存在」盖掉真正的语法报错。
|
||||
- 注:本条同时取代 2026-10-05 条目里 `--out-dir` 与「就地删掉非白名单扩展名文件」的旧描述——现行参数是 `--vite-built-dir`,且只预检、不删除、不改写。
|
||||
- 影响范围:`resources/agc-skills/vite-export-xhs-minitool/{SKILL.md,.selective_rule.txt,scripts/{pack.mjs,vite.config.xhs-minitool.mjs}}` 与其 `resources/agc-skills/manifest.json` 指纹、`scripts/.pack.test.mjs`(补「pack 不改写 index.html」)、新增隐藏开发辅助 `scripts/.vite.config.xhs-minitool.test.mjs`(真跑 `vite build` 断言收尾、`copyPublicDir: false` 的 icon 兜底、失败不收尾、`pruneEmptyDirs` 只删空目录)。隐藏测试不进清单、不改 `skill_pack.rs` 的 `include_bytes!` 条目数(仍 36),manifest version=2026-08-26.51。
|
||||
- 验证方式:`node --test scripts/.vite.config.xhs-minitool.test.mjs scripts/.pack.test.mjs scripts/.validate.test.mjs`(30 passed)、`npm run agc:skill-pack:check`、`git diff --check`、`npm run check:encoding`。
|
||||
- 边界:仍是小红书小工具一个目标的适配脚本;宿主 `export/draft/xhs_minitool` 链路不变,第二目标出现时再抽 target 描述符。
|
||||
|
||||
## 2026-10-05 导出产物面板:适配归 code agent,宿主只校验并运行项目脚本
|
||||
|
||||
- 背景:AGC 需要把项目导出成第三方平台制品,首个目标是小红书小工具。平台不给上传 API,用户只能人工上传;而制品规范(zip 根目录、扩展名白名单、尺寸上限、`index.html` 要求)会随平台变化。把规范复刻进 Rust 等于在客户端养一份会过期的第二真源。
|
||||
- 决策(边界):宿主只做四件确定性的事——读写项目内 `.export/` 注册表、校验表单与脚本是否存在、以既有 `command.exec` 边界运行项目自己的 npm 脚本、把产物交给用户;适配与打包由 code agent 首次实验出来并固化成脚本落在用户项目里。宿主不解析 zip、不代跑 `pack.mjs`、每次导出都重新构建。
|
||||
- 决策(注册表):`.export/xhs-minitool.json` 首次打开时自动创建且字段全空;不存 status、不存「是否已适配」、不存经验文本,agent 的经验写进它自己拷进项目的脚本注释。「有没有脚本」永远现场从 npm 包 `package.json` 的 `scripts["build:xhs-minitool"]` 现算(`hasScript`)。状态一律现算,不落盘。
|
||||
- 决策(冲突):注册表有两个写者(agent 改文件、用户在面板编辑)。不引入 `revision` / `updatedAt` 这类 token 字段,改由宿主返回 `contentHash`、保存时带 `baseHash`;hash 不同再逐字段 diff,有字段真的不同才判冲突,返回两侧值让前端逐字段选择,纯格式化改动静默吸收,任何情况下不静默覆盖用户输入。
|
||||
- 决策(工作目录):`EXPORT_WORK_RELATIVE_DIR = ".export"` 锚在项目根并保持 flat;共享 icon 放 `.export/` 根,注册表存项目内相对路径,多目标复用同一文件;agent 拷贝的脚本按 `<target>` 前缀命名避免第二目标撞名。`.export/` **只**排除在项目快照同步之外(`PROJECT_SNAPSHOT_SYNC_ONLY_EXCLUDED_COMPONENTS`),恢复或换机后需重新适配(已知代价);**不得**并进通用排除口径——`agent/direct_patch.rs` 直接拿它拒绝路径,`agc_apply_patch` 会以「不得修改受保护或排除路径」拒绝 `.export/`,首次适配就落不了地;同机 checkpoint 仍含 `.export/`,可作恢复适配脚本的兜底。
|
||||
- 决策(skill 落点):`pack.mjs` 新增 `--zip-out <path>`(相对 cwd 解析、父目录自动创建),让「产物必须落在项目根 `.export/xhs-minitool.zip`」有确定写法;`--out-dir` 只允许指向 vite 构建输出目录(它会就地删掉该目录内非白名单扩展名的文件)。宿主只认结果:脚本名 + 非空产物路径,产物结构与平台规范仍归 skill 与 agent。
|
||||
- 决策(权限与副作用):跑脚本复用 `command.exec` 的权限口径(只查 deny,UI 按钮即用户确认),不新增 `GAME_CREATION_APP_COMMANDS` 条目;导出链路不拿项目写锁、不推进全局 revision,`.export/` 不进 manifest、素材、UI State 或客户端投影;发布包白名单收集,`.export/` 不会进入。
|
||||
- 决策(prompt 归属):首次适配与失败修复的指令正文、契约常量与组装都放前端(`view/project-development/export/state/xhsMinitoolInstruction.ts` 纯函数;失败说法在 `xhsMinitoolFailure.ts`),宿主不提供 `enqueue_*` 命令;前端经 `useDirectProjectChatController` 的 `chat.submit` 入队,复用它既有的会话写权限门与 clientTurnId。Rust 侧因此只有 per-target 的四个命令(读内容 / 读指纹 / 存表单 / 跑构建);内容与指纹为什么分成两条见下面 2026-10-05 的同步细节决策。
|
||||
- 影响范围:新增 `apps/ai-game-creator-shell/src-tauri/src/export/{mod,draft/mod,draft/xhs_minitool/{mod,commands}}`(`main.rs` 仅加 `pub mod export;`)、前端 `view/project-development/export/{tabs/xiaohongshu,state,generated}`、`resources/agc-skills/vite-export-xhs-minitool/scripts/*` 与其 `manifest.json` 指纹、`src-tauri/src/agent/skill_pack.rs` 的 `include_bytes!` 内容(文件数不变,仍 36 条)。不触碰 `GAME_CREATION_APP_COMMANDS`、`shared-contracts`、`server-rs`、SpacetimeDB、OpenAPI、`api-server` 与现有 `project/export.rs` 发布链路。
|
||||
- 验证方式:`cargo test ... export::` 与 `export_bindings` + `npm run check:generated-bindings`、前端 vitest(复制 / 下载 / 冲突逐字段选择 / 适配指令组装与入队 mock)、`npm run ai-game-creator-shell:typecheck`、`npm run agc:skill-pack:sync` + `agc:skill-pack:check`、`npm run check:doc-index`、`npm run check:encoding`、`git diff --check`;真实 vite 项目上验证首轮适配产出可上传 zip 与二次导出零 agent 调用。
|
||||
- 边界:本决策只覆盖小红书小工具这一个目标;第二个目标出现时再抽 target 描述符,不预留动态注册或通用表单引擎。
|
||||
|
||||
## 2026-10-05 导出面板的同步细节与文案归属:指纹不进对外状态
|
||||
|
||||
- 背景:导出面板(同上一条)落地后回头核 [`【ADR】DirectProject对话滚动与历史自动加载-2026-10-02`](../../adr/【ADR】DirectProject对话滚动与历史自动加载-2026-10-02.md),发现两件事与既有口径不一致:读状态把内容与指纹混在一条命令里、宿主自己拼了「省略前 N 个字符」这句用户可见文案。两条都在这里收口。
|
||||
- 决策(同步细节不进对外状态):命令拆成 `read_xhs_minitool_export`(`form` + `hasScript`,自动刷新反复调的高频路径)与 `read_xhs_minitool_export_hash`(写回基线),后者只在进编辑会话、或新内容真的被采纳时取一次。`contentHash` 不进前端对外状态(hook 里只在 `baselineRef`),界面不显示任何同步状态。理由:刷新这条高频路径在**类型上**就动不了写回基线,否则「顺手刷新」会把两次刷新之间的外部改动认成自己的基线、把冲突吞掉。
|
||||
- 决策(自动重读,取代「刷新按钮」,作废提交 `90f651ddf` 的「刷新取消未保存输入」):取消刷新按钮,按 2 秒固定间隔自动重读(窗口在后台时跳过)。`hasScript` 是宿主现算事实,任何时刻都采纳;表单只在用户手上没有未保存输入(也不在冲突/字段错误里)时才覆盖,否则绝不覆盖正在打的字。代价是「刷新会取消未保存输入」这条旧行为被有意作废,对应的回归用例同步替换。
|
||||
- 决策(文案归属的延伸):宿主不预拼用户可见文案这条也管载荷里的说明——构建输出尾部只回 `outputTail` + `omittedCharacters` 两个事实,「已省略前 N 个字符」由前端拼(`export/state/xhsMinitoolOutputTail.ts`,失败卡片与成功提示共用)。与上面那条 prompt 归属同源:机器事实在 Rust,句子在前端。
|
||||
- 契约常量的单一来源:Rust `export/mod.rs` 与 `draft/xhs_minitool/layout.rs` 是契约值(`.export`、脚本名、注册表名、产物名)的唯一权威;前端在 `export/state/xhsMinitoolInstruction.ts` 保留一份用于拼指令,由 `tests/xhsMinitoolContract.test.ts` 直接读 Rust 源码把两份钉住(ts-rs 不能导出 `const`,所以钉法是测试而不是生成物)。改 Rust 常量而前端没跟,测试红。
|
||||
- 影响范围:`export/draft/xhs_minitool/{commands,error,build,dto}.rs`、`export/generated/*.ts`、`export/state/{useXhsMinitoolExport,xhsMinitoolFailure,xhsMinitoolOutputTail,xhsMinitoolApi}.ts`、`export/tabs/xiaohongshu/*`。不新增命令、不碰权限位、不动发布链路。
|
||||
- 验证方式:`cargo test ... export::`(含 `export_bindings`)、`npm run check:generated-bindings`、`npx vitest run apps/ai-game-creator-shell/tests/xhsMinitoolExport.test.tsx apps/ai-game-creator-shell/tests/xhsMinitoolContract.test.ts`、`npm run ai-game-creator-shell:typecheck`、`npm run ai-game-creator-shell:check:rust:shell`、`npm run check:doc-index`、`npm run check:encoding`、`git diff --check`;真实 vite 项目上的首轮适配与二次导出仍是未验证项。
|
||||
- 边界:只覆盖小红书小工具这一个目标;第二个目标出现时再抽 target 描述符,不预留动态注册或通用表单引擎。
|
||||
|
||||
## 2026-10-04 AGC 审核 Skill Pack 新增平台抽象与小红书小工具两项,隐藏文件永不进包
|
||||
|
||||
- 背景:`resources/agc-skills` 新增两个审核 Skill `platform-abstract` 与 `vite-export-xhs-minitool`。后者自带本地开发辅助文件(`.selective_rule.txt`、`scripts/.pack.test.mjs`、`scripts/.validate.test.mjs`),旧清单工具会走目录把它们当成必须声明的审核文件,指纹、安装与只读读取也无法区分「审核正文」和「本地辅助」。
|
||||
- 决策(新规则):`agc-skill-pack.v1` 引入「`.` 前缀即隐藏、永不进包」的全局约定。JS `isHiddenSkillEntryName` / `isSafeSkillRelativePath` / `collectBundledFiles` 与 Rust `is_hidden_skill_entry_name` / `is_safe_skill_relative_path` 共用同一口径:隐藏文件与目录不写清单、不参与 SHA-256、不安装到隔离 `.agents/skills`,也不能经 `agc_read_skill_resource` / `agc://skills/*` 读取。新增 Skill 只需声明非隐藏文件,无需为 `.selective_rule.txt` 之类补 `include_bytes!`;声明了隐藏路径会直接被拒绝。
|
||||
- 决策(注册):白名单两处同步扩到 10 项——`scripts/skill-pack-manifest.mjs` 的 `EXPECTED_SKILL_NAMES` 与 `src-tauri/src/agent/skill_pack.rs` 的 `AGC_SKILL_PACK_EXPECTED_NAMES`;`AGC_SKILL_PACK_FILES` 增加 13 条 `include_bytes!`(`platform-abstract` 1 + `vite-export-xhs-minitool` 12),数组长度 23 → 36。`agc-skill-pack.v1` manifest 版本 `2026-08-26.37` → `2026-08-26.40`,两个新条目的 `sha256` 由 `npm run agc:skill-pack:sync` 对当前工作树现场重算,不从旧构建复制。`vite-export-xhs-minitool` 的 `pack.mjs` / `validate.mjs` 顺带修掉阻止 pre-commit eslint 的问题(`simple-import-sort` 导入排序、未用参数改名 `_line`、NUL 正则加 `no-control-regex` 行内说明),并去掉 `platform-abstract/SKILL.md` 的行尾空格与两个 references 的多余 EOF 空行以通过 `git diff --check`,指纹随之同步到 .40;隐藏的 `.pack.test.mjs` / `.validate.test.mjs` 被 eslint 按点文件默认忽略,pre-commit 会经 `isPathIgnored` 过滤掉。
|
||||
- 决策(lint 边界):`.eslintrc.cjs` 的 `ignorePatterns` 增加 `apps/ai-game-creator-shell/src-tauri/resources/agc-skills/**`。审核 Skill 脚本按 SHA-256 定址并用 `include_bytes!` 编进客户端,`eslint --fix` 改写会让 manifest 指纹漂移,而 pre-commit 不跑 `skill-pack:check`,只能等 CI 暴露;`.prettierignore` 本已排除该目录,eslint 补齐同一口径。仓库自身的 `scripts/skill-pack-manifest.mjs` / `check-skill-pack.test.mjs` 仍正常 lint。
|
||||
- 决策(测试夹具):`codex_app_server` 里三条 fake app-server 的 `skills/list` 响应补上两个新名字;否则 `validate_direct_project_skill_catalog` 会判定审核 Skill Pack 不完整,让 DirectProject 启动失败关闭。
|
||||
- 影响范围:`apps/ai-game-creator-shell/scripts/{skill-pack-manifest.mjs,check-skill-pack.test.mjs}`、`apps/ai-game-creator-shell/src-tauri/src/agent/skill_pack.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server/mod.rs`(仅测试夹具)、`apps/ai-game-creator-shell/src-tauri/resources/agc-skills/manifest.json`、`.eslintrc.cjs`(审核 Skill 目录加入 `ignorePatterns`);`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的 2026-08-20 审核 Skill Pack 条目同步。`docs/project-memory/plans/【里程碑】AGC编辑器分支产物归位与症状层补丁清理-2026-09-26.md` 里「`list_agc_skill_catalog` 返回 8 条」是当时实测记录,保留不改。
|
||||
- 验证方式:`npm run agc:skill-pack:check`(version=2026-08-26.40)、`node --test scripts/check-skill-pack.test.mjs`(5 passed,含隐藏路径拒绝与收集器忽略隐藏文件)、`cargo test --bin genarrative-ai-game-creator-shell -- --test-threads=1 agent::skill_pack`(7 passed)、`agent::codex_app_server::tests`(72 passed)、`agent::direct_runtime::tests`(92 passed)、`cargo fmt --all --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` 通过;`npx eslint`(变更的仓库脚本)通过,且 `resources/agc-skills/**` 经 `isPathIgnored` 全部返回 true。
|
||||
- 边界:隐藏文件规则只约束随包审核与只读读取,不改变这些文件在仓库中的存在、权限或本地用途。
|
||||
|
||||
## 2026-10-05 播放会话前缀在三处入口清空 Cookie,网关 403 纵深防御不变
|
||||
|
||||
- 背景:付费游戏的可玩入口是创建播放会话后拿到的 `/api/game-distribution/play-sessions/<token>/`(sandbox iframe 的 `src`,包内相对资源沿同一前缀解析)。该前缀落在 `/api/*` 上,边缘通用 `/api` location 必须转发 Cookie(`/api/auth/*` 需要 refresh cookie),而 `api-server` 播放网关对带可解析平台 refresh Cookie 的请求返回 403,导致真实浏览器里 iframe 与每个包内资源都 403、付费游戏实际不可玩。
|
||||
@@ -189,23 +265,6 @@
|
||||
- 影响范围:根开发脚本、AGC 开发启动编排、本地开发运维文档;不改变 API、schema、生产部署和独立 `npm run agc` 行为。
|
||||
- 验证方式:参数/状态单测、开发栈健康端点 smoke、`.app/dev-stack.json` 身份复用检查、进程树收束检查。
|
||||
|
||||
## 2026-10-01 AGC 命令错误结构化与错误报告口径
|
||||
|
||||
- 决策:AGC 命令失败按**具体变体**建模(Rust `#[derive(Serialize, TS)]` 枚举 + `#[serde(tag = "type", rename_all = "camelCase")]` + ts-rs 导出,生成物不手改),`#[tauri::command]` 的 `Err` 直接携带结构化枚举;前端先按 `type` 选类别、可枚举细分再按类型化 `reason` 分流,**任何地方都不对错误文案做判断**。做法沿用 DirectProject 既有约定(`enqueue_direct_codex_turn -> Result<(), DirectTurnEnqueueFailure>`),不是新机制。
|
||||
- 决策:错误报告池只收**没有任何调用方处理**的错误。预期业务拒绝(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给反馈,永不进池;真故障由调用方带上下文交给错误池(`ClientAuthErrorWrapper` 承载 `source/action/page`,`captureClientError` 用 `instanceof` 解包),`window.onerror` / `unhandledrejection` 只兜底没人接手的错误;408/5xx/网络的判定由调用方在 catch 里做(4xx 一律不报);Rust agent 终态失败仍由失败投影入池。删除 WebView 侧 `shouldCaptureClientError`。
|
||||
- 边界:变体按**可判定的事实**命名——服务端 400 只给 `status + message`(`AppError.code` 仍是通用 `BAD_REQUEST`),所以 400 变体按"哪条请求的输入被拒"命名(如 `passwordLoginRejected`),不假装能区分密码长度/手机号格式。报告面板默认全选、只由通知打开的既有承诺不变。`captureAgentRuntimeError`、`ResourceReferenceInput` 偏好写盘、`invokeDiagnostic` 三处显式采集点保持原行为,按同一口径改造或删除留在后续变更。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/{auth_error.rs,auth_session.rs}`、`apps/ai-game-creator-shell/src/services/{clientAuthErrorWrapper.ts,errorReporting.ts,clientAuth.ts}`、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src/services/generated/`(ts-rs 生成:`ClientAuthError.ts` + 有字段变体的载荷文件)。
|
||||
- 决策(补充):TS 形状**只有带载荷的变体才有具名载荷类型**——无字段变体在 ts-rs 里就是 `{ type: 'x' }`,有字段的变体是 newtype 变体持有同名 `#[ts(export)]` 结构体,生成 `{ type: 'x' } & X` 与 `src/services/generated/X.ts`;可枚举的细分原因是类型化枚举字段(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`),不是字符串、也不各拆一个顶层变体。前端 `switch (error.type)` 的无字段分支用固定文案,带载荷分支先 `as X` 再读它自己的字段,`reason` 是枚举时再 `switch (payload.reason)`(`default` 同样用 `expectNever`)。不允许在前端手写这层类型,也不再包派生分类 / 提示文案函数(`clientAuthErrorKind`、`clientAuthErrorNotice`、`resolveClientAuthFailure` 已删除)。
|
||||
- 验证:见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 的验收清单;关键判据是"登录 400/401 业务变体不产生 `report_client_error`、不弹「发现问题」"。
|
||||
- 决策(2026-10-01,JS 侧载体与抛出时机):认证命令统一经 `invokeClientAuth(command, args)` 调用;拒绝值原样装进已有的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体字段、不塞 `context`,构造时把整份载荷 `JSON.stringify` 进 `Error.message`,上报事件因此拿到机器事实),**不新增手写错误类**(`ClientAuthFailure` 已删除),形状完全信任 tauri + ts-rs 映射、不做运行时嗅探。形状读取 / 文案回落 / 提示分类三层(`isClientAuthError`、`getClientAuthErrorMessage`、`presentAuthFailure`)全部删除。
|
||||
- 决策(2026-10-01,判定位置与出口):要不要上报只由 catch 子句里的 `switch (failure.type)` 判,`failure = error.error`;无字段业务 / 会话变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名类型、再用它自己的 `reason` / `serverMessage` / `status` 拼上本次操作的上下文前缀(`reason` 是枚举时再 `switch (payload.reason)`);Rust 不预拼用户可见文案、服务端原文缺失就是 `null`(无兜底文案)。系统变体原样 `throw` 经全局 `unhandledrejection` 入池(`captureClientError` 用 `instanceof` 解包 `error` 字段取原始错误),`default: expectNever(failure)` 让漏接变体编译失败。取代"未识别变体上调是故意的"。
|
||||
- 决策(2026-10-01,Rust 侧不再降级):`refresh_session_inner` 的非权威失败直接 `Err(ClientAuthError)`,`ClientAuthStateView` / `ClientAuthRefreshView` 删除 `errorMessage`,续期结果删除 `failed`;`ClientAuthState` 收敛为 `authenticated | unauthenticated`,`ClientAuthRefreshResult` 收敛为 `refreshed | unauthenticated | stale`。
|
||||
- 追加(2026-10-02,429 按路由判定):`/api/auth/phone/login` 验证码错误次数耗尽返回的 429 是用户可修正的输入问题,映射为 `phoneCodeLoginRejected`(复用现有业务变体、不进错误池);发码路由仍是 `smsCodeThrottled`,其余路由的 429 仍是 `unexpectedRejection`。
|
||||
- 追加(2026-10-02,401 归 phoneCodeLoginRejected):`/api/auth/phone/login` 的 401 只来自「用户不存在」(验证码错误/失效/过期在服务端都是 400,已由 `phoneCodeLoginRejected { serverMessage }` 带原文);顶层变体 `smsCodeRejected` 退役删除,前端三个 catch 去掉了它那个「验证码错误或已过期」的固定分支,`phoneCodeLoginRejected` 的文案统一为「验证码登录失败:<服务端原文>」。
|
||||
- 追加(2026-10-02,读 body 失败按已确认状态码归类):AGC 认证请求拿到 `status` 后 `response.text()` 失败,不再一律压成 `authNetworkFailure { unreachable }`;非 2xx 走既有分类(`serverMessage` 为 `None`,如 503 → `authServiceUnavailable { 503 }`),只有 2xx 响应没收完才算传输层故障。分类收敛在 `classify_unreadable_body`。
|
||||
- 追加(2026-10-02,系统类失败保留原始错误载荷):`clientSessionPersistFailed` / `runtimeSessionInstallFailed` / `authClientInitFailed` 都带 `detail: string`(原始 error),既让调用方有机会分流处理,也让报告包带够诊断信息;原始 error 同时经 `app_log!`(落盘前过 `sanitize_diagnostic_message`)记一行本地日志。`detail` 不贴到界面上:三个 catch 用本操作的固定文案(登录检查 / 发码 / 登录各自不同)。取代上一版"本机 IO 失败不进载荷、原始 error 只进日志"。
|
||||
- 影响范围(2026-10-01 第二轮):`apps/ai-game-creator-shell/src/services/{clientAuth.ts,platformSession.ts}`(`clientAuthError.ts` 删除)、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、对应 vitest 用例。
|
||||
|
||||
## 2026-10-02 launcher 页面高度契约:外壳分高度,页面不再自己算窗口高度
|
||||
|
||||
- 背景:PR #228(`6d2c275d3`)只给项目页补了「外壳纵向 flex + 页面 `flex: 1 1 auto`」的高度修复;其余页面仍各自算高度——帮助页没写高度也没有内层滚动容器,内容一长就被外壳 `overflow: hidden` 裁掉且无法滚动;首页用 `h-screen` / `h-[calc(100vh-32px)]`,模板库用 JS 量父级高度写内联 `height`。
|
||||
@@ -9849,3 +9908,16 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- Codex 15/110 分钟事件静默与 120 分钟硬上限、Claude 独立超时保持不变;取消、回合归属、并发、交付复核次数与进程退出证明继续有效。
|
||||
- 正式执行账本为 v4,旧上限只在已发布数据的加载迁移边界识别;不保留旧运行时路径。App 管理配置安全写回,共享只读配置不改源文件;旧终态与报告不重开。
|
||||
- 权威行为与验证边界见 [AGC 主规范](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md#direct-宿主运行时间上限移除2026-10-06)。
|
||||
|
||||
## 2026-10-06:陶泥儿导出文件草稿化与游戏分发发布媒体直传
|
||||
|
||||
- 导出目标命名约定与落点:草稿 `.export/taonier.json`(唯一真相、flat `.export/` 目录)、npm 脚本 `build:taonier`、独立构建目录 `dist-taonier`、交付/发布产物 `.export/taonier.zip`(存档根必须有 `index.html`);标准 `build`/`dist` 不被占用。新增 `vite-export-taonier` skill(`vite.config.taonier.mjs` + `pack.mjs`),宿主 `export/draft/taonier` 提供读内容 / 读指纹 / 存表单 / 跑构建四个命令。
|
||||
- 草稿模型对齐小红书重构:宿主零业务校验,校验只在前端 `taonierFields`;600ms 自动保存带 `baseHash`、2s 轮询只更新外部事实、冲突逐字段选择;失败现场原样转发 code agent,adapt/fill/repair 三类指令与小红书同构;线上值只读并排,可逐字段/逐图「沿用线上值」。
|
||||
- `exports/` 整体退役(E1):目录创建、README 生成/渲染、试玩包打包/读取/列举、`prepare/upload/list` 命令、staging 依赖、生成任务产物声明、prompts、run-trace 白名单与 5 处排除列表全部移除;发布暂存固定读取 `.export/taonier.zip`,phaser/vite 发布门删除。
|
||||
- 发布媒体硬切:`POST /api/game-distribution/games`、`POST /api/game-distribution/games/{gameId}/versions`、`PATCH /api/game-distribution/my-games/{gameId}` 改收 `multipart/form-data`(`metadata` 文本 JSON 含 `coverObjectKey` 与 `screenshots: (string|null)[]`,可选 `cover` part,可重复 `screenshot` part);`string` 槽位沿用线上 objectKey,`null` 槽位按序消费二进制 part。不保留旧 JSON / assetId 兼容分支;AGC 发布先读草稿本地路径发字节、空路径发线上 objectKey,幂等摘要纳入图片字节。
|
||||
- 媒体公开读按域内判定:新图直落项目快照桶 `agc/project-snapshots/v1/game-distribution/media/<gameId>/{cover|screenshot}-<uuid>.<ext>`,不建 `asset_object`;公开读走 `GET /api/game-distribution/media/read-url`(及 `read-bytes`),判定 = 已发布且 active 的 game 且 objectKey 命中 `cover_object_key` / `screenshots_json`,不经素材库 ACL。删除 `game_distribution_asset_has_public_read_grant` 与发布链路中的 assetId;游戏行与冻结资料只存 objectKey,删除 `GameDistributionFrozenScreenshot`。
|
||||
- schema 删字段属 breaking:按既有先例用 `SPACETIME_SCHEMA_GUARD_ALLOW_BREAKING=1 npm run check:spacetime-schema` 验证,并同步 `migration.rs`、表目录与生成绑定。
|
||||
- AI 资料建议 / 封面生成退役:删前端调用、Rust 命令与注册、api-server 路由与专属 DTO;保留发布进度对话框。
|
||||
- skill pack 例外:`agc-skill-pack.v1` 只登记 `vite-export-taonier`;未跟踪的 `vite-export-taptaph5` 是另一条 WIP,不登记,`npm run agc:skill-pack:check` 对其未声明文件报红为已知接受项。
|
||||
- 权威行为已回写 AGC 实施计划「2026-10-06」章节、玩法链路「游戏分发发布媒体直传合同」与后端数据契约;实施计划《【实施计划】陶泥儿导出文件草稿化与发布媒体直传-2026-10-06.md》与对应里程碑规范**保留在 `docs/project-memory/plans/`**,不再按该计划 §5.4 在收尾时删除。
|
||||
- owner 作用域媒体读(同日补充):公开读只覆盖已发布作品,作者预览未发布 / 待审 / 被驳回作品的封面与截图会退化成占位图。新增 `GET /api/game-distribution/my-games/{gameId}/media/read-url`(及 `.../media/read-bytes`,需 bearer),判定 = `gameId` 属于当前登录主体且 objectKey 命中该作品当前行的 `cover_object_key` / `screenshots_json`;复用同一签名器与 `get_game_distribution_game(owner_user_id)`,不新增 procedure、不给素材库 ACL 开特例,公开读判定不变。AGC `resolve_preview_url`、平台 web `useGameDistributionMediaReadUrl`(带 `gameId`)、`MyGamesPage` 封面与发布/编辑回填全部切到 owner 路由。
|
||||
|
||||
@@ -10,6 +10,31 @@
|
||||
- **AGC 美术包**:核心图集新请求将原 brief 与共用的风格、内容和间距要求分别写入 `iconDescriptions`;普通图标入口仍原文单项透传。新增模板只影响新请求,旧请求继续按冻结正文和操作身份恢复,不改原 brief 的意图判据。真实 HTTP 载荷与旧请求恢复必须同时验证,不能只测试 `artSpec` 包含文案。
|
||||
- **权威说明**:[External v1 OpenAPI](../../openapi/genarrative-external-v1.openapi.json) 与 [API 指南的 Generation Inputs Metadata](../../../.codex/skills/genarrative-external-editor-api/references/requests-and-outputs.md#generation-inputs-metadata)。本条记录现有行为,不引入 API、存储或 UI 行为变更。
|
||||
|
||||
## 2026-10-06 陶泥儿导出与发布媒体直传:幂等摘要、幂等账本、skill-pack 红与 zip 根入口
|
||||
|
||||
- **multipart 发布的幂等摘要必须纳入图片字节**:写路径改 `multipart/form-data` 后,`metadata` 文本 part 只描述槽位;若摘要只序列化元数据,同一 `Idempotency-Key` 换一张新图或换一个沿用 objectKey 会被判成同请求重放,服务端静默复用旧结果、新图丢失。现行口径:服务端 `publish_request_digest` = 规范化元数据 JSON + 每个 `cover`/`screenshot` part 的 SHA-256 + 槽位里的 objectKey;AGC 侧 `metadata_digest` 同样把图片原始字节喂进哈希。create / create version / update metadata 三条写路径共用;回归时「只换图不改文案」必须得到不同摘要。
|
||||
- **幂等键与发布账本口径**:AGC 根幂等键由账本按「账号 + origin + 本地项目 + 包摘要 + 目标游戏/版本 + 资料摘要」解析,同一次用户发布的重试、响应丢失后的重发与进程重启后的分片续传都复用同一 root key,包内容 / 目标版本 / 资料任一变化才换键;分包上传再用 `<root>:upload`。服务端 `game_distribution_idempotency_receipt` 以 `owner_user_id + action + idempotency_key` 唯一,同 key 同摘要回 `replayed=true` 并复用结果 ID,同 key 不同摘要返回 409。不要把重试实现成新 key,也不要让摘要漏掉图片字节或沿用 objectKey,否则媒体直传下的重放语义失效。
|
||||
- **`npm run agc:skill-pack:check` 因未登记 taptap 报红是已知接受项**:`vite-export-taptaph5` 是另一条 WIP,未进 `AGC_SKILL_PACK_EXPECTED_NAMES` / `EXPECTED_SKILL_NAMES`;check 会列出它的未声明文件并退出非零。当前接受该红,不为它补登记或改清单;要恢复全绿必须先收口或正式登记 taptap skill。taonier 自身的清单与指纹已锁步。
|
||||
- **taonier zip 必须存档根 `index.html`**:`.export/taonier.zip` 的存档根必须直接有 `index.html`,不能套外层文件夹(`taonier/index.html`、`game/index.html` 都算失败)。`vite-export-taonier/scripts/pack.mjs` 在落盘前按根入口校验并拒绝;即使绕过,服务端发行合同只认根 `index.html`,会以 `ReleasePackageError::MissingEntry` 映射 422 `PACKAGE_VALIDATION_FAILED` 拒收。适配非标准工程布局时,让 `build:taonier` 把 `--vite-built-dir` 指到真正含 `index.html` 的目录,或让 `pack.mjs` 不套外层;客户端 `run_taonier_export_build` 也在产物收尾复核根入口。
|
||||
- **作者预览未发布作品必须走 owner 媒体读,不能再回退素材库 ACL**:发布媒体是项目快照桶对象、不建 `asset_object`,`/api/assets/read-url` 不会给它授权;公开读路由 `GET /api/game-distribution/media/read-url` 只认「已发布且 active」,两条都不覆盖"作者看自己未发布/被驳回作品"。症状是作者中心与 AGC 线上值封面/截图静默退化成占位图(换签失败被吞成空地址),不报错、不阻断。现行口径:作者侧一律走 `GET /api/game-distribution/my-games/{gameId}/media/read-url`(`read-bytes` 同理,需 bearer),判定 = 作品归属 + objectKey 命中该作品当前行的 `cover_object_key` / `screenshots_json`;AGC `resolve_preview_url` 与平台 web 的 `useGameDistributionMediaReadUrl({ gameId })`、`resolveGamePublishImagePreview(objectKey, gameId)` 都已切换。新增读路径时先确认它属于公开面还是 owner 面,别再让作者预览落到公开判定上。
|
||||
|
||||
## 2026-10-07 fnm/nvm 托管的 Node 进 bwrap:单文件挂载 npm 必失败,`--tmpfs /run` 会抹掉 multishell PATH
|
||||
|
||||
- **现象**:开发构建在 Linux 命令沙箱里执行 `npm run build` / `npm install` 时,宿主 shell 里明明能跑通的 fnm Node,进沙箱后报 `Node.js v26.10.0` 与 `Cannot find module '../lib/cli.js'`,或 npm 命令在路径解析阶段就失败。
|
||||
- **根因 1(单文件挂载软链前缀)**:fnm / nvm 的 `<前缀>/bin/npm` 是指向 `<前缀>/lib/node_modules/npm/bin/npm-cli.js` 的软链。bwrap `--ro-bind <前缀>/bin/npm <前缀>/bin/npm` 只挂载这一个文件,`npm-cli.js` 里的 `require('../lib/cli.js')` 找不到同安装内的相对目标,于是报错;必须整棵只读挂载通过窄叶校验的完整安装前缀(含 `bin/node`、`lib/node_modules/npm`),不能只挂 shim 或 `bin/`。
|
||||
- **根因 2(`--tmpfs /run` 抹掉活动版本)**:fnm 的活动 `PATH` 项是 `/run/user/<uid>/fnm_multishells/<pid>/bin`;sandbox 的 `--tmpfs /run` 会清空该目录,sandbox 内解析到的 `node` 随之失效或退回系统版本。不要依赖宿主 `PATH` 原样进入沙箱:canonicalize 路径,并把活动版本管理器变量(如 `FNM_MULTISHELL_PATH`)从 sandbox 环境里剔除。
|
||||
- **根因 3(错误取舍会打穿测试)**:把宿主 `node` / `npm` / `npx` shim 指到 `/usr/bin/*` 能让沙箱借用系统 Node,但在本机系统 Node 26 上会默认启用实验性 Web Storage,顶掉 vitest 0.34 jsdom 的 localStorage,AGC 测试在 HEAD 即红(见下方 2026-10-03「AGC 测试不在任何 tsconfig 里」条的环境提示)。正确方向是原生支持托管安装,而不是改宿主 shim。
|
||||
- **补充(nvm default 别名是主版本号)**:`nvm alias default 22` 写进 `$NVM_DIR/alias/default` 的内容是 `22`,不是完整三元组。若按精确 `(22,0,0)` 去匹配 `versions/node/v22.23.3` 会永远落空,默认别名形同不存在;必须用与 `.nvmrc` 相同的比较子集在已安装版本里选最高匹配。
|
||||
- **现行口径**:宿主发现与沙箱挂载共用 `validate_node_installation_prefix`;`.nvmrc` / `.node-version` 权威、`engines.node` 偏好;Linux npm 以 `node <npm-cli.js>` 启动并保留 `npm install` 联网判定。契约见技术方案 V1.11.2。
|
||||
- **验证**:`GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1` 跑 `command_sandbox_real_linux_opt_in_runs_host_node_and_npm_cli`,在真实 fnm v22 前缀下 bwrap 内 `node --version` 与 `node <npm-cli.js> --version` 均通过;单元用例覆盖 pin / engines / 不支持写法 / 宽叶与逃逸前缀 / 联网判定。
|
||||
|
||||
## 2026-10-07 cargo 目标目录里被"刷新 mtime"的陈旧 shared-contracts 会让编译报源文件里明明存在的字段缺失
|
||||
|
||||
- **现象**:`cargo check` 在 AGC shell 上报 `unresolved import shared_contracts::runtime::ProfileMembershipUpgradeQuoteResponse`、`no field project_version / publication`、`LlmModelsResponse: Deserialize` 等一整组「契约落后」错误;但 `server-rs/crates/shared-contracts/src` 里这些符号确实存在,`git status` 干净,刚重建的 rlib 也含符号。
|
||||
- **根因**:`target/debug/deps` 里留着早先构建的 `libshared_contracts-<hash>.rmeta`,其 `.d` 依赖文件停留在旧时间戳,而 `.rmeta` 的 mtime 在快照 / 拷贝过程中被刷新成新时间;cargo 按 mtime 判定该 crate 仍然新鲜,于是把 `--extern shared_contracts=` 指到旧 rmeta,下游就看到旧 API。多份不同 feature 组合的 `shared-contracts-<hash>` 并存时更容易踩中。
|
||||
- **处理**:删除该 crate 的全部指纹与产物后重编即可,不必清整棵 target:`rm -rf target/debug/.fingerprint/shared-contracts-* target/debug/deps/*shared_contracts*`,再跑 `cargo check`。判断依据是错误集中在某个 `shared-contracts` API,而源文件与 `git status` 都正常;先用 `cargo check -v 2>&1 | grep -m1 -- '--extern shared_contracts='` 找到实际使用的 rmeta,再核对它同名 `.d` 里的源文件路径与时间戳。
|
||||
- **边界**:这是构建缓存 / 快照产物问题,不是契约真源问题;不要因为这类报错去改 `shared-contracts` 或回退下游代码。
|
||||
|
||||
## 2026-10-05 PR #607 复核修复:档位点对齐/对比度、状态文案也走浮层、键盘去重、卸载 flush
|
||||
|
||||
- **档位圆点已删除(D1 的收口)**:这一轮把档位圆点**整体删除**(半透明备选方案未采用)。现在滑块只剩轨道 + 圆钮:轨道 6px 圆头、已选段 `--platform-accent` 由 `--strength-ratio` 驱动、**终点落在圆钮中心**(`calc(10px + ratio * (100% - 20px))`)、圆钮 20px 实心暖白(`--platform-panel-fill` + `--platform-subpanel-border` 1px 描边 + `color-mix` 柔影);强度区横向内边距 `4px 6px 2px` → `4px 0 2px`(滑块铺满卡片内容宽度,填充段与圆钮两端与轨道两端贴齐);相关 CSS(`space-between` 排布 / `z-index: 2` 抬层 / `.is-active{opacity:0}` / 点的 `color-mix` 底色 / 只为点对齐的 `padding: 0 7px`)与渲染标记一并删除。**判据**:`chatDialogFrameLayout.test.ts` 反向守卫(样式表里不再有 `.project-chat-composer-strength-stops` 规则、组件源码不再渲染该类名;滑块契约仍在:宽 100% / 高 26 / 圆钮 20×20、强度区左右内边距 0)+ `home.suite.ts` 首页菜单里查不到那组点。实测(447 视口,像素扫描):轨道 90..324(宽 234 = 卡片内容宽),档位 0 时圆钮左缘 90.5(距轨道左端 0.5px)、档位 4 时圆钮右缘 322.3(距右端 1.8px,扫描行不在圆钮正中所以略窄),填充段终点落在**圆钮中心**(`calc(10px + ratio * (100% - 20px))`,被圆钮盖住),因此圆钮右侧不会露出橙色(终点曾写成 `20px + …` = 圆钮右缘,4× 设备像素下能看到一小截溢出)。**历史成因(只留一句,细节由 Git 追溯)**:圆点此前被 6px 轨道盖住、且与圆钮两端错位 ±12.2px,曾用「抬到轨道之上 + space-between 对齐 + 浅暖色」修过一轮,最终整体删除。
|
||||
@@ -220,7 +245,7 @@
|
||||
|
||||
- **现象**:登录页密码输错(或密码长度不合规)后弹出「发现问题」,报告面板「错误事件(2)」列出 `密码长度需要在 6 到 128 位之间 — auth · 1 次` 与 `手机号或密码错误 — auth · 1 次`,默认全选,与 react-render / 5xx / agent-runtime 终态失败视觉等价。
|
||||
- **原因**:① 登录已下沉 Rust,`login_client_with_password` 等命令失败返回 `Err(String)`,Tauri 以**裸字符串**拒绝 `invoke`,前端拿不到任何类型信息;② `shouldCaptureClientError` 对非 object 值走默认 `return true`,`handleLoginSubmit` 的 catch 把预期业务拒绝报进了错误池。技术方案里"预期 4xx 登录/鉴权失败不进池"的口径早就成立,是错误通道的实现方式违背了它。
|
||||
- **处理(现行口径)**:命令错误一律按具体变体结构化(`Result<_, ClientAuthError>` + ts-rs 导出),认证命令统一经 `invokeClientAuth` 调用:结构化拒绝原样装进已有的 `ClientAuthErrorWrapper`(只有一个 `error` 字段,值是判别联合),UI 在 catch 里按具体变体分流——认得的业务 / 会话变体只给用户反馈,系统变体原样 `throw` 经 `unhandledrejection` 入池,非结构化拒绝原样抛出;删除 `shouldCaptureClientError`。详见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 与 [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](../../adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md)。
|
||||
- **处理(现行口径)**:命令错误一律按具体变体结构化(`Result<_, ClientAuthError>` + ts-rs 导出),认证命令统一经 `invokeClientAuth` 调用:结构化拒绝原样装进已有的 `ClientAuthErrorWrapper`(只有一个 `error` 字段,值是判别联合),UI 在 catch 里按具体变体分流——认得的业务 / 会话变体只给用户反馈,系统变体原样 `throw` 经 `unhandledrejection` 入池,非结构化拒绝原样抛出;删除 `shouldCaptureClientError`。
|
||||
- **判据/取证**:`npx vitest run apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx`——登录返回结构化业务变体时 `report_client_error` 不被调用;系统变体只上报一次(`source` 取全局 `unhandledrejection` handler 的显式入参;载体不再携带 `action`)。Rust 侧 `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml auth_error` 钉住变体 `type` 与 400/401/429/5xx/网络映射。
|
||||
- **形状约定**:`ClientAuthError` 顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段——无字段变体在 TS 里就是 `{ type: 'x' }`;带载荷变体是 `{ type: 'x' } & X`,`X` 由 ts-rs 导出到 `src/services/generated/X.ts`(Rust 侧是 newtype 变体持有同名结构体),细分原因枚举(`ServerAddressReason` / `AuthNetworkReason` / `AuthResponseInvalidReason`)同样由 ts-rs 生成。**不要手写这些类型**,也不要在前端再加一层分类 / 提示文案派生函数——判别一律写在 catch 子句里:`const failure = error.error; switch (failure.type)`,无字段 `case` 用本 catch 的固定文案,带载荷 `case` 先 `as X` 再读自己的字段,`reason` 是枚举时再 `switch (payload.reason)`,两处 `default` 都用 `expectNever` 保证漏接编译失败。改形状只能改 Rust 再跑 `cargo test` 重新导出,生成物保持 ts-rs 原始输出(不再经 prettier / eslint 二次改写,见本节「生成绑定」口径);ts-rs 只写文件、不删文件,变体从有载荷改成无字段时要手动清掉孤立的 `X.ts`(本次 `AuthResponseServerRejected.ts` 就是这样删的)。
|
||||
- **Rust 侧不得把结构化错误降级成字符串**:`refresh_session_inner` 的非权威失败直接返回 `Err(ClientAuthError)`,视图不带 `errorMessage`;一旦折成 `String`,前端就只能拿文案判断,变体信息永久丢失。
|
||||
@@ -6560,3 +6585,20 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
|
||||
- **CPU 边界**:发行网关是公开无鉴权端点,压缩按请求实时算。release 构建实测 level 6 为 1.3 MiB→10 ms、8 MiB→59 ms、64 MiB(单文件上限)→522 ms 纯 CPU,因此在 `RELEASE_COMPRESSION_FAST_ABOVE_BYTES`(2 MiB)以上改用 level 1(zlib 端实测 level 1 约为 level 6 的 1/3 耗时、压缩比只差约 3%)。若后续要再做减法,优先把压缩结果按 `(对象键, 资源路径)` 缓存,而不是放宽级别。
|
||||
- **验证**:`cargo test -p api-server --bin api-server -- game_distribution`(新增 ETag 作用域、`If-None-Match` 列表/弱校验命中、`gzip;q=0` 拒绝、文本压缩与二进制/小文件不压、304 无正文、`*` 对包内缺失路径仍 404 等用例);`npx vitest run packages/shared/src/components/PlatformGameLoadingSurface.test.tsx`、`src/components/game-distribution/GameDistributionPages.test.tsx`、`apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx`;`npm run check:game-distribution-ops-rollback-e2e` 47 项通过(含压缩/Vary/ETag/304/不接受 gzip 四条新断言)。真实栈同链路复跑:`phaser.min.js` 1,375,976 B → 353,336 B(gzip,4 Mbps 下 2724 ms → 774 ms),用户端游戏画面 3298 ms → 1543 ms、后台试玩 4587 ms → 2694 ms。
|
||||
- **关联**:`packages/shared/src/components/PlatformGameLoadingSurface.tsx`、`src/components/game-distribution/GamePlayPage.tsx`、`apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx`、`server-rs/crates/api-server/src/modules/game_distribution.rs`、`deploy/nginx/README.md`。
|
||||
|
||||
## 2026-10-07 cc 会话恢复只活在进程内存里,「离开项目再进入」就看不到历史对话
|
||||
|
||||
- **现象**:用户反馈「离开后再进入项目,cc 那边看不到历史对话」——聊天面板里的历史还在(`.agent/conversations/project.jsonl` 是 UI 的事实源),但模型完全不知道之前说过什么。
|
||||
- **根因**:`CLAUDE_DIRECT_SESSIONS` 是进程级 `Mutex<HashMap<PathBuf, String>>`,只负责把上一个回合的 `session_id` 交给 SDK 的 `resume`。用户离开项目再进入、重启客户端,或上一回合失败(失败路径根本不写这张表)之后,表里没有 id,cc 就以全新会话开工;而 SDK 自己的轨迹其实一直落在隔离 home 里:`<项目>/.agent/runtime/claude-code/home/claude/projects/<sanitized-cwd>/<sessionId>.jsonl`。现场:项目 `gameagent-0514673b` 的隔离 home 里留下两份独立轨迹(14:52 与 15:05),第二份的首次用户输入只有一句「现在呢」;`gameagent-754f4779` 更累积到 8 份。
|
||||
- **现行口径**:恢复顺序固定为「进程内会话表 → 本项目目录下 mtime 最新的会话轨迹」。目录名按 Claude Code 自己的派生规则算(cwd 里非字母数字一律换成 `-`,实例 `C:\Users\...\projects\gameagent-0514673b` → `C--Users-...-projects-gameagent-0514673b`),并且只认本项目目录——SDK 的 `resume` 也只在那里查这份轨迹,从别的目录捞来的 id 会被判成「No conversation found」,比不恢复更糟。日志 `agent.direct_codex.claude_resume source=memory|disk|none` 记录本回合的恢复来源,用户再报「看不到历史对话」时可以一眼分辨是内存命中、磁盘回放,还是本项目确实没有历史会话。
|
||||
- **验证**:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell claude_` 34 passed(新增 `claude_code_session_lookup_uses_the_project_directory_and_newest_trace`、`claude_direct_resume_falls_back_to_disk_and_prefers_the_process_map`)。真实环境用随包 `claude.exe`(2.1.285)复验:把项目隔离 home 的 `projects/` 拷进临时 `CLAUDE_CONFIG_DIR`、cwd 设为项目根,`--resume 00000000-…` 立刻回 `No conversation found with session ID`;`--resume f79dae28-…`(磁盘上最新那份)不报找不到,直接进入模型请求,落在本地抓包桩上的请求体里带着完整历史(`messages` 13 条,首条就是 14:23 的「做个废土风的扫雷…」原文)。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`。
|
||||
|
||||
## 2026-10-07 cc 回合超时后宿主不重试,只能用户手动点「重试」
|
||||
|
||||
- **现象**:用户截图 `等待模型回执超时(已尝试 1 次),本轮未完成;请稍后重试:Claude Agent SDK sidecar 回合超时:连续 180000 ms 没有任何事件(已收到 3 个事件)`——一轮卡满 3 分钟就判失败,用户只能自己点「重试」,而那次手动重试 58 秒就成功了;同一项目当天 15:07 又原样复现一次(15:10 失败)。
|
||||
- **根因**:cc 执行器一轮只跑一次 sidecar,超时就直接判回合失败;只有 Codex 那条路径带重试意识(`ModelCallKind::ResponseTimedOut { attempts }` 的 attempts 在 cc 侧被硬编码成 `1`)。挂住的是平台网关(一个字节都不回),不是模型拒绝——重放本来是有意义的。
|
||||
- **边界证据(真实二进制)**:把随包 `claude.exe`(2.1.285)指向一个"收下请求就不回任何字节"的本地桩,默认配置和显式 `CLAUDE_ENABLE_BYTE_WATCHDOG=1` + `CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS=60000` + `CLAUDE_STREAM_IDLE_TIMEOUT_MS=60000` + `CLAUDE_CODE_MAX_RETRIES=2` 两种配置下,300 秒(第二组 180 秒)内都只有 `system/init` 一条事件、桩上也没有第二次请求,**没有任何重试或报错**。所以"超时后重试"这件事只能由宿主做,不能指望 CLI 或那些 watchdog 环境变量自己兜。
|
||||
- **现行口径**:`direct_game_creator_claude_code_chat_at` 改成一次用户回合最多 `CLAUDE_DIRECT_TURN_MAX_ATTEMPTS = 3` 次尝试(第一版:2 次自动重试),**只有两个条件同时成立才自动重放**:①失败是"静默超时"(`连续 N ms 没有任何事件`;45 分钟硬上限不重放,否则一次回合能被拖到两小时以上);②这一轮从头到尾**没有请求过任何工具**——模型一旦发过 `tool_use`,重跑就可能把付费生成 / 构建 / 试玩再执行一遍(哪怕结果还没回来,操作也可能已经在途),这时改为落一条"本轮已经请求过工具,不会自动重放;请手动重试"的过程行。每次重试前先落一条 `role:"system"` 过程行(`等待模型回执超时,正在自动重试(第 2/3 次)`),重试同样走 `resume`,历史与上下文不丢。最终失败时的 `已尝试 N 次` 由宿主回填:详情尾部的 `;本轮已自动重试 N 次` 只是结构化传递标记,分类器会把它摘掉,不落到用户可见详情里。
|
||||
- **验证**:`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --features=cocos-editor-execute,unity-editor-execute,godot-editor-execute --bin genarrative-ai-game-creator-shell claude_` 37 passed。新增三条:`claude_direct_retry_policy_retries_only_safe_timeouts`(逐格钉住重试/停下的四种决定)、`claude_direct_timeout_is_only_auto_replayed_without_tool_requests`(硬上限不重放、请求过工具不重放)、`claude_direct_retry_attempts_reach_the_structured_error_without_leaking`(attempts 结构化,标记不落到用户可见详情)。
|
||||
- **关联**:`apps/ai-game-creator-shell/src-tauri/src/agent/claude_code_cli.rs`。
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
- AGC 模板库灰度复用 `agc:template-library`:未配置关闭,已配置时遵循现有灰度启停、用户 ID/标签和比例规则;服务端返回权威结论,客户端入口和原生清单/下载/建项均执行门禁,主体切换丢弃旧异步结果。公开 OSS 不是保密边界,已创建项目不受影响。
|
||||
|
||||
- 画布卡片类型与信息角标共用 `CanvasCardCornerActions`;菜单收纳共用 `OverflowActions`,宿主决定展示数量和资源命令。AGC 选中菜单前 5 项直显,Web 默认不折叠;浮层 portal 继续接入现有画布关闭与滚轮归属判据。
|
||||
- 断言只写在能失败的地方:`getBy*` / `findBy*` 找不到就抛,不要再包一层 `expect(...).not.toBeNull()` / `toBeDefined()`(那种写法恒真,还让每条查询多一层噪声);等元素出现用 `findBy*` 或 `await waitFor(() => screen.getBy*())`,负向用 `queryBy*` + `toBeNull()`。同理不写复述实现的断言(例如把被测函数的表达式在测试里再抄一遍),改为钉关系、边界或行为,并能逐条反证会红。
|
||||
- 修改范围保持聚焦;优先扩展现有系统、页面、组件、DTO 和脚本。
|
||||
- Agent 可见内容直接描述当前任务、输入和成功条件,细节按调用需要提供。
|
||||
- AGC 外置智能体提示词只写模型必须遵守的指令与契约(工具名与参数、调用顺序、禁止项、失败处理、产物要求),不写客户端/宿主怎么实现:内部预算字段与限额、内部状态机与枚举、执行器锁与沙箱技术栈、进程/插件部署、注入与投影管线都属于宿主实现,一律改写成对应行为要求或删除。
|
||||
@@ -52,7 +53,7 @@
|
||||
- 后端遵循 `module-*`、`spacetime-module`、`spacetime-client`、`api-server`、`platform-*`、`shared-contracts` 的现役边界。
|
||||
- 前端只负责表现、交互和临时 UI 状态;正式状态来自后端投影、API 或持久化契约。
|
||||
- AGC 渲染层是离线前端:平台接口、OSS 素材直传、Provider、错误上报与账户/认证的网络 IO 全部在 Rust 的 reqwest facade,渲染层不得再出现 `fetch(` / `XMLHttpRequest` / `EventSource` / `sendBeacon(` / `new WebSocket(` 或 `@tauri-apps/plugin-http`;客户端窗口 capability 也不得授予 `http:default`。`check:native-shells --groups=contract` 的 `ai-game-creator-shell-user-dev-boundary` 用负向门禁钉住这条,改动渲染层网络边界必须同批改该门禁与 `build-release.test.mjs`。
|
||||
- AGC 工作区的正式状态变化走 Rust 事件 + 一次受控快照读取,不建立固定频率轮询(纯 UI 计时器、拖拽重复器与动画 tick 除外):渲染层先订阅再读快照,事件重复或内容未变时保持数组身份,卸载后迟到事件不写回。Direct 活动回合的唯一事实源是 Direct 线程管理器的活动回合快照(`list_direct_active_turns` 只读它;登记、进度内容变化与收口各广播一次 `game-creator-direct-active-turns-changed`),渲染层与 Rust 都不得再引第二份注册表。
|
||||
- AGC 工作区的正式状态变化走 Rust 事件 + 一次受控快照读取,不建立固定频率轮询(纯 UI 计时器、拖拽重复器与动画 tick 除外;导出面板对 `.export/` 注册表与 npm 脚本的 2 秒重读是显式例外——那两件事实由用户项目和 agent 直接改文件,没有事件源,只能现读):渲染层先订阅再读快照,事件重复或内容未变时保持数组身份,卸载后迟到事件不写回。Direct 活动回合的唯一事实源是 Direct 线程管理器的活动回合快照(`list_direct_active_turns` 只读它;登记、进度内容变化与收口各广播一次 `game-creator-direct-active-turns-changed`),渲染层与 Rust 都不得再引第二份注册表。
|
||||
- AGC 维护态(平台 `503` 且命中 `MAINTENANCE` 或「维护」)由 Rust 在平台请求的错误分支经 `platform_maintenance::watch_platform_response` 分类并广播 `genarrative-client-maintenance-detected`;渲染层只订阅并打开唯一的「系统维护中」弹窗,不自行解析 HTTP,业务面板各自的错误文案保持不变。
|
||||
- 对已明确退役且无现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,直接清理实现、专属测试和说明,将权威文档更新为当前状态;历史由 Git 保存。公开契约、持久化数据和正式迁移按实际需求保留最小兼容及对应测试。
|
||||
- 修改 `/api/external/v1` 时,同批更新 `docs/openapi/genarrative-external-v1.openapi.json` 与契约测试。
|
||||
|
||||
@@ -8,7 +8,7 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事
|
||||
|
||||
- 诊断 URL 保留可定位的 API 路由路径,隐藏 origin、URL 账号密码、查询参数、fragment 和路径中的敏感标识;普通资源 URL 与本地文件路径继续隐藏。网络错误、HTTP 错误与响应体超时均应带安全路由,不能只剩 `<url>`。历史已经脱敏的归档不推测或补造原路由。
|
||||
|
||||
- 报告池只收**没有任何调用方处理**的错误:React render error、`window.onerror`、`unhandledrejection`,以及调用方判定为真故障后带上下文交给错误池(`ClientAuthErrorWrapper`)的错误。Agent Runtime 的终态失败、预算耗尽和启动确认失败由 Rust 失败投影统一入池;Direct Codex 与专业 Agent 的前台裸 Tauri invoke catch 作为补充入口,重复事件由同一 fingerprint 合并,主动取消和“同一 turn 已在运行”不作为错误采集。分层口径见 [`【ADR】AGC命令错误结构化与错误报告口径-2026-10-01`](../adr/【ADR】AGC命令错误结构化与错误报告口径-2026-10-01.md) 与 [`【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01`](../adr/【ADR】AGC认证失败的JS侧载体与抛出时机-2026-10-01.md)。
|
||||
- 报告池只收**没有任何调用方处理**的错误:React render error、`window.onerror`、`unhandledrejection`,以及调用方判定为真故障后带上下文交给错误池(`ClientAuthErrorWrapper`)的错误。Agent Runtime 的终态失败、预算耗尽和启动确认失败由 Rust 失败投影统一入池;Direct Codex 与专业 Agent 的前台裸 Tauri invoke catch 作为补充入口,重复事件由同一 fingerprint 合并,主动取消和“同一 turn 已在运行”不作为错误采集。
|
||||
- 事件字段包括 eventId、fingerprint、source、message、stack、时间和次数;重复事件合并。不再携带 severity、errorCode、page、action、requestId 等无法稳定关联的字段。
|
||||
- 指纹计算可使用调用方的 page/action 及脱敏后的首个调用点作为进程内区分输入,但这些上下文不会作为事件字段上传;消息与 stack 在入池前统一脱敏,WebCrypto 失败时降级为稳定可读指纹,采集本身不得产生新的未处理拒绝。
|
||||
- 命令失败按具体变体建模(Rust 枚举 + ts-rs 导出的 `type` 判别联合;顶层只放调用方要分流的类别,可枚举细分收进类型化枚举 `reason` 字段,无字段变体生成 `{ type }`,带载荷变体生成 `{ type } & 载荷类型`),前端按变体分流,**任何地方都不对错误文案做判断**:认得的业务变体(用户输入 / 前置条件 / 预期 4xx)由调用方消化并给用户反馈,永不进池;系统变体、未识别变体和结构化之外的拒绝原样抛出,走上面的兜底入口。认证命令的封装形态:`invokeClientAuth` 只是薄包装,把结构化拒绝原样装进**已有**的 `ClientAuthErrorWrapper`(载体只有一个 `ClientAuthError` 类型的 `error` 字段,值就是 ts-rs 生成的判别联合;不读变体字段、不塞 `context`,构造时把整份载荷 `JSON.stringify` 进 `Error.message`,上报事件因此拿到机器事实),不新增手写错误类;判定只写在 catch 子句里,无字段变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名载荷类型(`reason` 是枚举时再 `switch (payload.reason)`)、再用它自己的字段拼上本次操作的上下文前缀,`default` 用 `expectNever` 在编译期挡住漏接变体。
|
||||
|
||||
@@ -616,6 +616,16 @@ Runner-kill E2E 不再以 latest task 或单个 process record 推断整体恢
|
||||
|
||||
`command.poll` 私有正文虽然必须进入 owning Agent context 供后续交互,但模型 prompt 不是持久化隔离边界。后台 finalization 在创建 assistant journal 前检查当前 run 的成功 poll observation;只要存在非空输出,就把模型最终回复整体收束为固定安全完成摘要,再计算 response fingerprint 并写 conversation/event/Agent DB。该边界不按长度猜测 token,因此 challenge、ready/echo/stopped 行和短 PIN 的局部回显都不能扩大到公共持久面;没有私有 poll 正文的普通回复保持原样。
|
||||
|
||||
### V1.11.2 命令沙箱 Node 版本管理器支持
|
||||
|
||||
开发构建(`debug_assertions`)下,AGC 生成和验证 Web 项目所用的 Node 往往由 fnm / nvm 等版本管理器托管:安装前缀位于用户目录下,活动 `PATH` 指向随 shell 会话变化的临时目录(如 fnm 的 multishell)。V1.11.2 让开发构建的命令沙箱原生解析并只读复用这些托管安装,不再要求用户改系统 Node、把宿主 shim 指到 `/usr/bin`,或把托管目录软链进系统路径。发布构建继续只认随包 bundle,不新增托管版本管理器探测,也不改变 bundle 缺失时的失败口径;Windows 行为不变。
|
||||
|
||||
- 版本来源是机器上可枚举的托管安装:fnm 的 `node-versions/<version>/installation`(含 `aliases/default` 指向的默认别名)与 nvm 的 `versions/node/<version>`。宿主发现和沙箱只读挂载共用同一套窄叶校验,不允许两处信任口径漂移。
|
||||
- 解析优先级为:`.nvmrc` / `.node-version` 的权威 pin 命中 > 宿主 `PATH` 能解析出的可用 Node > 版本管理器回退链(`package.json` `engines.node` 偏好中的最高匹配 > 当前活动版本 > 默认别名 > 已安装最高版本)。`.nvmrc` / `.node-version` 能理解但未安装时必须失败关闭(`node-version-pinned-not-installed`),不得静默回退;`engines.node` 只是偏好,任何情况下都不阻塞。只实现文档化的比较子集(精确三元组、major、`>=` / `>` / `<=` / `<`、`^`、`~`、`x` / `*` 通配、`lts/*`);不支持或无法解析的写法按「未 pin」处理并回退。
|
||||
- 只读挂载只允许通过窄叶校验的完整安装前缀(同时含 `bin/node` 与 npm 的 `npm-cli.js`)。HOME、`FNM_DIR` / `NVM_DIR` 根、`aliases` 目录、宽泛用户目录、不完整前缀,以及 canonicalize 后逃逸出受控前缀的 symlink 全部拒绝并失败关闭;不得为了兼容而挂载整个用户 HOME 或版本管理器数据目录。
|
||||
- Linux 上 npm 不再直接执行 npm shim,而是以受信任的 `node <npm-cli.js> ...` 启动;`npm install` 的联网判定必须跟随这条真实启动形态,不能因为包装方式变化而丢失联网或反向放开。
|
||||
- 沙箱内联环境不继承宿主活动版本管理器的临时变量(如 fnm multishell 路径);版本管理器 CLI(`fnm` / `nvm`)本身不需要在沙箱内可用。
|
||||
|
||||
## V1.12 受控本地 Git 提交
|
||||
|
||||
V1.12 首个切片补齐“修改、验证、审阅、提交”的单 Agent 本地闭环,只新增 `project.git_commit`。它不是通用 Git 写权限:不开放 `push / fetch / pull`、分支创建或切换、merge / rebase、reset、stash、tag、submodule、worktree,也不能通过 `command.exec` 绕过 `.git` 只读沙箱。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -62,7 +62,7 @@
|
||||
### 前端合并与渲染(回合唯一归属,连续工具成块)
|
||||
|
||||
- 加载对话时按历史条目进入 `project.jsonl` 的原始顺序投影:用户消息、文本与工具块保持原序,连续工具合为一块、遇到文本另起一块,不按 `turnId` 重新归并、也不再有 `seq` 交替。
|
||||
- 回合完成后,中间文本及所有工具块统一收进默认关闭的“执行过程”;最终回复及失败提示留在外面。展开后仍按原顺序查看中间输出和工具详情;运行中不使用外层折叠区。用户消息的发送时间从消息自身的历史时间读取,不能拿工具起点补造。
|
||||
- 回合完成后,中间文本及所有工具块统一收进默认展开的“执行过程”;最终回复及失败提示留在外面。用户仍可收起过程块,重开项目后过程默认再次可见;运行中不使用外层折叠区。用户消息的发送时间从消息自身的历史时间读取,不能拿工具起点补造。
|
||||
- 实时与回读共用同一投影,正文、工具和耗时不另建实时/未归属渲染出口。先在完整历史按消息身份关联,再分页;禁止按第 N 个工具回合匹配第 N 条用户消息。详情通过当前回合 `callId` 关联;同项目回读与实时增量幂等合并,切项目清空旧状态。完整合同见 [AGC 实施计划](./【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) 的“DirectProject 回合展示唯一归属”。
|
||||
- 块 DOM 与交互(对齐 Codex):
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
- 当前 Agent 与策划 Agent 共用 `packages/shared` 的 `AgentMessageContent` 表现组件:正文为 14px / `--platform-text-strong`,思考、中间输出和工具调用为 12px / `--platform-text-soft`。实时与历史思考共用同一个折叠入口;工具输入输出继承过程色,失败状态保留错误色。Markdown 标题、表格及代码高亮在过程区同步弱化,最终回复和文档预览仍保留正常排版,不按 Agent 类型复制样式。输入提示与禁用状态保持原有反馈。
|
||||
- Windows 命令展示:仅 `command` 卡片识别 `pwsh` / `powershell`(含完整路径、`.exe`、常见启动选项)的 `-Command` / `-c` 外层包装,摘要和展开输入只展示脚本正文,并解开单个 shell 参数的引用拼接。摘要优先读取已脱敏的 `detail.command`,再按首行 120 字符截断,避免历史摘要被可执行文件路径占满。无法识别的启动方式、`-File`、`-EncodedCommand`、普通命令和 MCP 输入原样展示;执行参数、持久化原文、脱敏和输出均不改变。
|
||||
- 调试属性:块与行都带 `data-duration-ms`(原始毫秒,无法计算时为空串)与稳定 `data-testid`(块 `agent-tool-call-group`、行 `agent-tool-call-row`)。
|
||||
- 必须用 `<button aria-expanded>` + `hidden` 控制展开(键盘可达、可读屏),块头与行都是按钮:`aria-label` = 汇总 / 行文案 + 耗时;默认折叠。
|
||||
- 必须用 `<button aria-expanded>` + `hidden` 控制行级展开(键盘可达、可读屏),块头与行都是按钮:`aria-label` = 汇总 / 行文案 + 耗时;回合完成后的外层过程块默认展开,用户可手动收起。
|
||||
- 输入框、消息气泡、消息列表滚动模型**不变**;块只是消息流里的一个块。
|
||||
|
||||
## 验收判据(每条都要有可复现证据)
|
||||
|
||||
@@ -497,15 +497,17 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- Rust 结构体:`GameDistributionGame`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`
|
||||
- 用途:游戏分发稳定身份与公开版本指针。保存 owner、标题/简介/分类资料、设备与输入声明、`publication_revision`、当前 `active_version_id`、可见性和游玩计数;标签与输入模式按版本化 JSON 保存,展示资料由 `api-server` 通过 `spacetime-client` 归一后返回。
|
||||
- 公开素材:游戏行末尾追加可空 `cover_object_key` 与 `screenshots_json`(截图 `{assetId, objectKey}` 数组);创建游戏时 `api-server` 就复核封面/截图素材存在且属于当前作者(不存在 400、他人素材 403),创建版本时按同一口径再次复核并派生对象键。 发布写入受灰度配置键 `game-distribution:publish` 约束:**灰度默认关闭**,未配置或 `enabled=false` 时写入口(创建游戏/版本、确认包、送审、审核通过激活)返回 503 `GAME_DISTRIBUTION_PUBLISH_DISABLED`,`enabled=true` 且白名单/比例/标签命中才放行,读取与安全下架保持可用;同一判据在 `GET /api/runtime/frontend-config` 以 `gameDistributionPublishEnabled` 下发给前端入口,匿名恒为 `false`。只有可见性为 `published` 且存在有效 `active_version_id` 的游戏,其封面/截图素材才在 `/api/assets/read-url` 上获得匿名读授权。
|
||||
- 公开素材:游戏行末尾追加可空 `cover_object_key` 与 `screenshots_json`(objectKey 字符串数组)。发布写路径(创建游戏 / 创建版本 / 编辑资料)收 `multipart/form-data`:新图由服务端直写项目快照桶 `agc/project-snapshots/v1/game-distribution/media/<gameId>/{cover|screenshot}-<uuid>.<ext>` 并只回 objectKey,不建 `asset_object`;沿用槽位带该作品当前媒体的 objectKey,不命中即 400,附图校验 `image/*`、张数与体积上限。 发布写入受灰度配置键 `game-distribution:publish` 约束:**灰度默认关闭**,未配置或 `enabled=false` 时写入口(创建游戏/版本、确认包、送审、审核通过激活)返回 503 `GAME_DISTRIBUTION_PUBLISH_DISABLED`,`enabled=true` 且白名单/比例/标签命中才放行,读取与安全下架保持可用;同一判据在 `GET /api/runtime/frontend-config` 以 `gameDistributionPublishEnabled` 下发给前端入口,匿名恒为 `false`。只有可见性为 `published`、存在有效 `active_version_id` 且未软删除的游戏,其 `cover_object_key` / `screenshots_json` 中的 objectKey 才在 `GET /api/game-distribution/media/read-url`(需要同源字节时用 `.../media/read-bytes`)上按游戏分发域内判定获得匿名读授权,不经素材库 ACL;作者预览自己作品(未发布 / 待审 / 被驳回)走 owner 作用域 `GET /api/game-distribution/my-games/{gameId}/media/read-url`(及 `.../media/read-bytes`,需 bearer),判定 = 该 `gameId` 属于当前登录主体且 objectKey 命中其 `cover_object_key` / `screenshots_json`,与公开读共用同一把签名器;软删作品在该读路径上一律 404。
|
||||
- 复用规则:末尾可空列 `local_project_id` 保存发布方本地项目标识(AGC 的 `manifest.projectId`)。同一 `owner_user_id` 再次以相同 `local_project_id` 创建游戏时复用既有 `game_id` 并只新增版本,避免“更新”被实现成新建游戏;该字段只是复用提示,不构成所有权或路径凭证,也不能用于跨账号匹配。已软删除的游戏不参与复用:删除后重新发布同一本地项目应得到新的游戏身份。
|
||||
- 软删除:游戏行末尾追加可空 `deleted_at`(2026-10-01)。非空表示作者已删除该作品:`delete_game_distribution_game_and_return` 只写该时间戳并把公开投影下线(可见性回到 `unpublished`、撤销当前公开版本、递增 `publication_revision`),版本行、发行包与其冻结资料一律不改写。软删行不进入作者列表(`list_owner_game_distribution_games_and_return`)、公开目录(`list_public_game_distribution_games_and_return`)、公开详情(`get_public_game_distribution_game_and_return`)、发行网关素材授权(`game_distribution_asset_has_public_read_grant`)与审核队列;后台默认视图同样排除,只有显式 `status=deleted` 才会读到。作者侧版本回读对软删作品返回空(404),因此上传、确认与送审入口一并关闭。
|
||||
- 软删除:游戏行末尾追加可空 `deleted_at`(2026-10-01)。非空表示作者已删除该作品:`delete_game_distribution_game_and_return` 只写该时间戳并把公开投影下线(可见性回到 `unpublished`、撤销当前公开版本、递增 `publication_revision`),版本行、发行包与其冻结资料一律不改写。软删行不进入作者列表(`list_owner_game_distribution_games_and_return`)、公开目录(`list_public_game_distribution_games_and_return`)、公开详情(`get_public_game_distribution_game_and_return`)、公开媒体读取判定(`get_game_distribution_media_read_access_and_return`)与审核队列;后台默认视图同样排除,只有显式 `status=deleted` 才会读到。作者侧版本回读对软删作品返回空(404),因此上传、确认与送审入口一并关闭。
|
||||
- 资料编辑:`update_game_distribution_game_metadata_and_return` 覆盖游戏行上的展示字段(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向)并立即生效,要求 `expected_publication_revision` CAS;版本行与冻结资料不变,下一次审核通过仍会用新版本的冻结资料覆盖游戏行。**资料编辑不得直接改公开价格**:调价必须走新版本审核。
|
||||
- 买断制定价(2026-10-05):游戏行末尾追加 `price_mud_points: u64` 并设置 `#[default(0u64)]`;`0` 表示免费,上限 `1_000_000`(复用 `module-game-distribution::normalize_game_price_mud_points` 校验)。价格是版本冻结资料的一部分:作者在 `GameDistributionCreateVersionRequest.priceMudPoints` 提交,写入版本冻结 `metadata_json.priceMudPoints`,只有 `approve_game_distribution_version_and_return` 通过审核时才随资料整体生效到本行;未通过审核或资料编辑都不会改变当前公开价格。公开投影(`get_public_game_distribution_game_and_return` 等)在游戏快照上带出 `priceMudPoints`。
|
||||
- 索引:`by_game_distribution_game_owner_user_id` 用于作者私有游戏列表;`game_id` 为主键。公开目录只返回 `visibility = published`、`deleted_at` 为空且活动版本存在、状态为 `published`(有效 `active_version_id`)的投影。
|
||||
- 购买与播放鉴权 HTTP(2026-10-05):`POST /api/game-distribution/games/{gameId}/purchase`(`require_bearer_auth` + 必填 `Idempotency-Key`,请求体 `{ expectedPriceMudPoints }`)经 facade 调 `purchase_game_distribution_game_and_return`,返回 `{ purchase, walletBalance, replayed }`;余额不足 400 `INSUFFICIENT_MUD_POINTS`、价格已变化 409、免费游戏 400、作者本人自购 400 `GAME_PURCHASE_OWNER_EXEMPT`(作者免购买,绝不扣费)、管理员令牌 403 `GAME_PURCHASE_ADMIN_NOT_ALLOWED`(购买只接受普通用户 bearer)、游戏不可见 404、缺幂等键 400、未登录 401。`POST /api/game-distribution/games/{gameId}/play-session` 对免费作品直接回既有公开入口 `/games/{gameId}/`;付费作品同时接受管理员令牌(按现有 admin 鉴权)与用户令牌,已购买 / 作者本人 / 管理员才签发绑定 `gameId + userId`、2 小时有效期的进程内会话,令牌为内存态,进程重启即失效。网关 `GET /api/game-distribution/play-sessions/{token}[/{assetPath}]` 不挂登录中间件、凭令牌读取当前公开版本包,能解析出平台刷新会话 Cookie 时 403,令牌过期 / 不存在、游戏下架 / 封禁或没有有效公开版本一律 404,全部 `no-store`。公开详情 `GET /api/game-distribution/games/{gameId}` 可选鉴权读取查看者:`purchased` 只反映真实购买记录,付费作品对未购买且非作者 / 非管理员把 `currentVersion.entryUrl` 置 `null`(资料与价格仍可见);`GET /api/game-distribution/releases/{gameId}[/{assetPath}]` 在当前公开版本 `price_mud_points > 0` 时同样 404,付费作品只能经播放会话路径播放。
|
||||
- 游玩计数写入:`play_count` 只由批量 procedure `increment_game_distribution_game_play_counts_and_return`(输入 `GameDistributionPlayCountIncrementInput { increments: Vec<{ gameId, delta }> }`)累加。`api-server` 在内存里按 `identity + gameId` 做 30 分钟去重、按 `IP + gameId` 做固定窗口限流后,按 `GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS`(默认 5 秒)批量落库;事务内只对 `published` 且存在有效 `active_version_id` 的游戏 `saturating_add`,非公开静默跳过,且**不更新** `updated_at`。公开 HTTP 入口为 `POST /api/game-distribution/games/{gameId}/plays`,完整行为见玩法链路的「游玩计数(已实现)」。
|
||||
|
||||
- 发布媒体直传(2026-10-06):游戏行只存 `cover_object_key` 与 objectKey 数组 `screenshots_json`;版本冻结资料 `GameDistributionFrozenMetadata` 用 `coverObjectKey`(`String`)与 `screenshots`(`Vec<String>`,objectKey),`GameDistributionFrozenScreenshot` 不存在。请求 DTO(create game / create version / update metadata)用 `coverObjectKey` 与 `screenshots: (string|null)[]`:`string` 槽位沿用该作品当前媒体的 objectKey,`null` 槽位按序消费可重复的 `screenshot` 二进制 part,可选 `cover` part 出现时覆盖 `metadata.coverObjectKey`。公开读走 `GET /api/game-distribution/media/read-url`(及 `read-bytes`),判定为「已发布且 active 的 game + objectKey 命中 `cover_object_key` / `screenshots_json`」,不经素材库 ACL;owner 作用域读路由 `GET /api/game-distribution/my-games/{gameId}/media/read-url`(`read-bytes` 同理,需 bearer)按「作品归属 + objectKey 命中该作品当前媒体」放行,供作者预览未发布 / 被驳回作品的封面与截图(软删作品该路径 404);公开读授权 procedure 只按 objectKey 查询(`get_game_distribution_media_read_access_and_return`),owner 读直接复用 `get_game_distribution_game`(带 `owner_user_id`),不新增 procedure。`request_digest` 覆盖规范化元数据 + 图片字节 hash + 沿用 objectKey,覆盖 create / version / update 三条写路径。硬切、无历史数据;schema 变更仍须同步 `migration.rs`、表目录与生成绑定并运行 `npm run check:spacetime-schema`。
|
||||
|
||||
### `game_distribution_review`
|
||||
|
||||
- Rust 结构体:`GameDistributionReview`,源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`;私有表,仅通过受信 API 服务身份的 procedure 与 BFF 提供评价投影。
|
||||
@@ -538,8 +540,8 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- 源码:`server-rs/crates/spacetime-module/src/game_distribution.rs`
|
||||
- 用途:不可变发行版本与真实包确认事实。创建后冻结 `package_sha256`、字节数、文件数、根入口和版本号;后续只推进上传、校验、审核、公开、撤回状态,并记录私有对象键、文件清单、入口 URL、审核者和阶段时间。
|
||||
- 索引:`by_game_distribution_version_game_id`、`by_game_distribution_version_owner_user_id`。真实 ZIP 由 `api-server` 校验并写入私有 OSS 后,才通过 facade 确认 `uploaded`;表不保存 ZIP 正文。
|
||||
- 冻结资料:版本表末尾追加可空 `metadata_json`,保存创建版本时由 api-server 校验(标题/简介/分类/标签/设备/方向/必需封面/≤6 张截图/买断制价格 `priceMudPoints`)并从素材记录派生对象键后的资料快照;`approve_game_distribution_version_and_return` 通过审核时把该快照整体生效到游戏行,因此公开投影展示的始终是“已随版本审核通过”的资料与价格,旧版本(无快照)保持原值。**价格口径统一为「冻结资料缺 `priceMudPoints` 即免费(0)」**:待审列表、审核详情的版本价与审核通过后生效的游戏行价格都按 `0` 处理,不会回退到游戏行旧价。<code>parse_game_distribution_frozen_metadata</code> 会用 <code>normalize_game_price_mud_points</code> 校验价格上限,越界快照在审核时失败关闭。
|
||||
- 作者回读投影:版本回读(作者本人)与审核回读(管理员)在版本 payload 上追加 `frozenMetadata`(冻结快照原样 JSON,历史版本为 `null`)。只有公开投影会剥掉素材 ID,作者与管理员拿到 `coverAssetId` / `screenshots[].assetId`,因此作者续发时可以直接复用同一批封面与截图素材,不需要为了沿用封面重新上传一次;素材 ID 缺失(旧版本)时前端必须要求作者重新选择封面,不能用对象键反推素材身份。
|
||||
- 冻结资料:版本表末尾追加可空 `metadata_json`,保存创建版本时由 api-server 校验(标题/简介/分类/标签/设备/方向/必需封面/≤6 张截图/买断制价格 `priceMudPoints`)并从创建请求确定 `coverObjectKey` / 截图 objectKey 后的资料快照;`approve_game_distribution_version_and_return` 通过审核时把该快照整体生效到游戏行,因此公开投影展示的始终是“已随版本审核通过”的资料与价格,旧版本(无快照)保持原值。**价格口径统一为「冻结资料缺 `priceMudPoints` 即免费(0)」**:待审列表、审核详情的版本价与审核通过后生效的游戏行价格都按 `0` 处理,不会回退到游戏行旧价。<code>parse_game_distribution_frozen_metadata</code> 会用 <code>normalize_game_price_mud_points</code> 校验价格上限,越界快照在审核时失败关闭。
|
||||
- 作者回读投影:版本回读(作者本人)与审核回读(管理员)在版本 payload 上追加 `frozenMetadata`(冻结快照原样 JSON,历史版本为 `null`)。冻结资料只含 `coverObjectKey` 与截图 objectKey 数组,作者续发时直接把这些 objectKey 放进 `metadata` 的沿用槽位,不需要为了沿用封面或截图重新上传;公开投影仍只暴露 objectKey,不含任何素材 ID。
|
||||
- 撤回与回读:`cancel_game_distribution_version_and_return` 只允许把未参与当前公开投影的版本推进到 `cancelled`,并要求 `expected_publication_revision` 与游戏公开修订号一致;`get_game_distribution_version_and_return` 供管理员按版本 ID 直读。客户端看到的 `recoveryAction` 由 `api-server` 按 `status` 派生,不落表。
|
||||
|
||||
### 后台游戏管理读模型与恢复动作(2026-09-23)
|
||||
|
||||
@@ -51,6 +51,22 @@
|
||||
- 供应商密钥、Token、Cookie 和本地私密路径不得进入前端或文档示例。
|
||||
- 生成或导入资产必须经过后端鉴权、对象确认和换签读取;上传失败或换签失败时显示可操作错误,不回退为公开裸路径。
|
||||
|
||||
## 游戏分发发布媒体直传合同(2026-10-06)
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 0.1 |
|
||||
| Status | current(实现已落地;真实 OSS 与真实发布 smoke 待验收) |
|
||||
| Date | 2026-10-06 |
|
||||
| 适用边界 | game-distribution 发布资料媒体(封面/截图)的上传与公开读取;发行包上传链路不变 |
|
||||
|
||||
- 写:`POST /api/game-distribution/games`、`POST /api/game-distribution/games/{gameId}/versions`、`PATCH /api/game-distribution/my-games/{gameId}` 接受 `multipart/form-data`:`metadata` 文本 part(JSON,含 `coverObjectKey` 与 `screenshots: (string|null)[]`);`cover` 二进制 part(可选,出现时覆盖 `coverObjectKey`);`screenshot` 二进制 parts(≤6 张,按 `null` 槽位顺序消费)。`string` 槽位表示沿用线上 objectKey。硬切,不接受素材 ID 形式的媒体引用。
|
||||
- 存储:新图由服务端写入项目快照桶 `agc/project-snapshots/v1/game-distribution/media/<gameId>/{cover|screenshot}-<uuid>.<ext>`,**不建 `asset_object`**;冻结资料与游戏行只存 `coverObjectKey` / 截图 objectKey。
|
||||
- 读:`GET /api/game-distribution/media/read-url`(匿名)按 objectKey 返回签名读地址;判定 = 已发布且 active 的 game 且 objectKey 命中其冻结媒体;需要同源字节时用 `.../media/read-bytes`。作者预览自己作品走 owner 作用域 `GET /api/game-distribution/my-games/{gameId}/media/read-url`(需 bearer,`read-bytes` 同理):判定 = 该 `gameId` 属于当前登录主体且 objectKey 命中该作品当前媒体,未发布 / 待审 / 被驳回的作品也能换签(软删作品返回 404),与公开读共用签名器。
|
||||
- 幂等:`request_digest` 覆盖规范化元数据 + 新图字节 hash + 沿用 objectKey,保持重放语义。
|
||||
- 归属:仅 owner 可写;沿用 objectKey 必须属于该游戏当前媒体;附图校验 `image/*`、张数与体积上限。
|
||||
- 与素材库解耦:公开读判定只按游戏分发域内规则(已发布且 active 的 game + objectKey 命中冻结媒体),不经素材库 ACL;素材桶 `/api/assets/read-url` 的通用签名与字节中转抽成公共 helper 供新路由复用,授权仍域内分离。
|
||||
|
||||
## AGC 游戏分发与在线游玩合同
|
||||
|
||||
| 字段 | 值 |
|
||||
@@ -82,11 +98,11 @@
|
||||
|
||||
### 真实发行包与资料合同
|
||||
|
||||
1. AGC 发布取当前 npm 工程已成功构建的 `dist/` 内容,重新检查入口和实际字节;ZIP 内部必须把 `dist/index.html` 归一化为根 `index.html`,其余路径相对发行根保持不变。不得上传整个项目、源码快照或仅发送本地路径。网页 ZIP 同样要求根 `index.html`,不猜测并自动剥离多层目录。
|
||||
2. 所有运行依赖都必须在发行包内。资源 URL 使用与发行版本目录兼容的相对地址;前导 `/assets`、本地文件 URL、外部脚本/样式/媒体/字体地址均不属于可接受发行合同。AGC 发布归一化阶段只把包内已知根路径 `/assets/`、`/game/`、`/ui/` 转成相对引用,不修改项目源码;其它外部绝对地址仍由客户端/服务器拒绝,静态校验不能代替运行时 CSP 阻断。
|
||||
1. AGC 发布读取项目 `build:taonier` 产出的 `.export/taonier.zip`(`vite-export-taonier` skill 把 `dist-taonier` 打成存档根直接含 `index.html` 的 zip),服务端重新检查入口和实际字节。不得上传整个项目、源码快照或仅发送本地路径。网页上传 ZIP 同样要求根 `index.html`,不猜测并自动剥离多层目录。
|
||||
2. 所有运行依赖都必须在发行包内。资源 URL 使用与发行版本目录兼容的相对地址;前导 `/assets`、本地文件 URL、外部脚本/样式/媒体/字体地址均不属于可接受发行合同。`vite-export-taonier` 用 `base: './'` 生成相对路径,并在构建收尾(`assertH5Root`)拒绝仍引用根路径资源的 `index.html`,不重写项目源码;外部绝对地址由运行期最小权限 CSP 阻断,静态校验不能代替运行时 CSP 阻断。
|
||||
3. 建议首版限额:压缩包 100 MiB、展开总量 250 MiB、单文件 64 MiB、最多 10,000 个文件、展开/压缩比不超过 100。服务端拒绝加密 ZIP、重复或大小写冲突路径、绝对路径、`..`、符号链接/重解析点、设备文件和嵌套压缩包;拒绝 `.agent`、版本控制目录、`node_modules`、凭据文件与源码映射文件。超限返回明确错误,不截断后继续发布。
|
||||
4. 提交声明 ZIP 的 SHA-256 与字节数,服务端对收到的真实 ZIP 重新计算,再对展开文件建立相对路径、字节数和 SHA-256 清单。摘要不一致、缺文件或入口损坏时停止;只有 metadata 而没有已确认完整对象的提交必须失败。
|
||||
5. 游戏资料随发行版本冻结:标题 2–40 字、短简介不超过 120 字、详细介绍不超过 2,000 字、一个分类、最多 5 个标签(每个不超过 20 字)、必需封面、最多 6 张截图、操作方式不超过 240 字。分类首版为休闲、益智、动作、冒险、模拟、策略、其他;封面/截图复用平台图片上传与归属校验,不接受任意外链作为审核图片。作者不需要自己构建或打 ZIP:AGC 发布时对 `game/` 子工程按需执行 `npm install` 和 `npm run build`,将 `dist` 归一化为根 `index.html` ZIP;为兼容 AGC 上传素材的运行 URL,会补入项目根 `assets/**` 中 dist 未包含的文件,同路径以 dist 构建产物为准,不修改项目源码。
|
||||
5. 游戏资料随发行版本冻结:标题 2–40 字、短简介不超过 120 字、详细介绍不超过 2,000 字、一个分类、最多 5 个标签(每个不超过 20 字)、必需封面、最多 6 张截图、操作方式不超过 240 字。分类首版为休闲、益智、动作、冒险、模拟、策略、其他;封面/截图由发布写路径以 `multipart/form-data` 直传项目快照桶,沿用槽位必须命中该作品当前媒体的 objectKey,不接受任意外链作为审核图片。作者不需要自己构建或打 ZIP:导出产物面板运行项目声明的 `build:taonier`(`vite-export-taonier` skill 的 `vite.config.taonier.mjs` 构建到 `dist-taonier`、`pack.mjs` 打成存档根含 `index.html` 的 `.export/taonier.zip`),发布暂存固定读取该 zip。
|
||||
6. `supportedDevices` 至少包含 `desktop` 或 `mobile`;`inputModes` 来自 `keyboard`、`mouse`、`touch`;声明移动端必须包含 `touch`。`orientation` 为 `landscape`、`portrait` 或 `responsive`。这些是待人工复核的作者声明,目录只显示已经随版本审核通过的值。
|
||||
7. 原始 ZIP、未审核展开目录、审核资料均为私有对象;公开版本不暴露源码镜像键、本地路径、访问凭据或私有账号元数据。运行文件只能由发行网关按游戏、版本和文件白名单读取,不能绕过网关访问公开 OSS bucket。
|
||||
8. 现役发行网关由 `api-server` 提供:`GET /api/game-distribution/releases/{gameId}`(含尾斜杠)等价于该游戏的 `index.html`,`GET /api/game-distribution/releases/{gameId}/{assetPath}` 只服务当前已公开版本包内的文件,私有 ZIP 与未公开版本不因知道 ID 而可读。响应按扩展名白名单设定内容类型,未知扩展名返回 404;全部响应带 `X-Content-Type-Options: nosniff`、`Cross-Origin-Resource-Policy: cross-origin` 与不带 credentials 的 `Access-Control-Allow-Origin: *`(发行文档运行在 `allow-scripts` 的 opaque origin 沙箱里,`same-origin` 会让游戏自己的脚本被浏览器拦下),HTML 追加最小权限 CSP,并在游戏脚本前注入隔离的运行期 `localStorage` / `sessionStorage` 兼容层,避免游戏直接读取 opaque origin 原生 storage 时抛 `SecurityError`。兼容层只在当前运行实例内存中有效,不读取平台 Cookie、主站 DOM 或账号数据。公开发行与审核预览只拒绝真实平台 refresh Cookie;审核预览 Token 绑定单个版本且短期有效。发行包按对象键在进程内做有界缓存,单个超预算包不进入缓存。
|
||||
@@ -130,7 +146,7 @@
|
||||
| `GET /game-distribution/play-sessions/{token}[/{assetPath}]` | 凭播放令牌 | **已实现**:凭令牌读取当前公开版本包内文件,不读 Cookie(带可解析平台 refresh Cookie 的请求一律 403,边缘/dev 按该前缀清 Cookie),`no-store`;令牌不存在或过期、游戏下架/封禁、无有效公开版本一律 404 |
|
||||
| `GET /my-games` | 登录作者 | **已实现**:当前账号游戏、最近版本状态与驳回理由;owner 只从认证主体派生,单次最多 48 项 |
|
||||
| `GET /my-games/{gameId}` | 登录作者 | **已实现**:作者读自己名下单个游戏的详情,条目与 `GET /my-games` 同形(含全部版本私有状态、驳回理由与已公开版本的 `entryUrl`)。作者要能打开「审核中 / 被驳回 / 已下架 / 已撤回」的作品,公开详情只服务已公开投影,所以作者视角必须走这条 owner 作用域路由;游戏不存在或不属于当前主体都返回 404 |
|
||||
| `PATCH /my-games/{gameId}` | 登录作者 | **已实现**:作者编辑自己名下游戏的展示资料(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向),立即生效并落 `tracking_event` 审计;要求 `Idempotency-Key` 与 `expectedPublicationRevision` CAS,随版本冻结的包摘要与资料快照不受影响,缺封面/截图归属不符仍按创建口径拒绝 |
|
||||
| `PATCH /my-games/{gameId}` | 登录作者 | **已实现**:作者编辑自己名下游戏的展示资料(标题/简介/详介/分类/标签/封面/截图/设备/输入模式/方向),立即生效并落 `tracking_event` 审计;要求 `Idempotency-Key` 与 `expectedPublicationRevision` CAS,随版本冻结的包摘要与资料快照不受影响,缺封面或沿用 objectKey 不命中该作品当前媒体仍按创建口径拒绝 |
|
||||
| `DELETE /my-games/{gameId}?expectedPublicationRevision=` | 登录作者 | **已实现**:作者软删除自己的作品。只写 `deleted_at` 并把公开投影下线(可见性回到 `unpublished`、撤销当前公开版本、递增 `publication_revision`),版本行、发行包与其冻结资料保留;作者列表/公开目录/公开详情/发行网关/审核队列与后台默认视图都不再返回,后台可用 `status=deleted` 查看。要求 `Idempotency-Key`(同 key 同请求返回原结果),不受发布灰度开关约束 |
|
||||
| `POST /games` | 登录作者 | **已实现**:幂等创建游戏身份,尚不公开;带 `localProjectId` 时同一作者复用既有 `gameId` |
|
||||
| `POST /games/{gameId}/versions` | owner | **已实现**:创建不可变待上传版本,冻结包摘要/字节数/文件数与资料 |
|
||||
@@ -513,7 +529,7 @@
|
||||
- 详情必须展示:发布者 ID/名称/头像、游戏标题、简介、详细介绍、分类、标签、操作方式、支持设备、输入方式、方向、封面、截图、版本号、包大小、文件数、SHA-256、提交时间、审核时间、审核理由和 `publicationRevision`。
|
||||
- 展示的游戏资料优先使用该版本冻结的 `metadata_json`;不能用审核期间作者后来修改的 game 行资料替代待审快照。
|
||||
- 发布者和游戏资料仅通过管理员受保护接口读取;公开目录不得因此增加作者私有字段或待审版本字段。
|
||||
- 封面与截图通过现有后台素材换签接口读取 Object Key,不把私有 Object Key 当作浏览器直链。
|
||||
- 封面与截图通过 `GET /api/game-distribution/media/read-url` 按 objectKey 换取签名读地址,不把私有 Object Key 当作浏览器直链;作者中心预览自己未发布 / 被驳回的作品时改走 `GET /api/game-distribution/my-games/{gameId}/media/read-url`(带 bearer),避免公开读判定把作者自己的封面挡成占位图。
|
||||
|
||||
### 待审版本试玩
|
||||
|
||||
|
||||
Reference in New Issue
Block a user