合并:同步最新master
Project CI / Native shell tests (pull_request) Successful in 14m13s
Project CI / Frontend tests (pull_request) Failing after 2m22s
Project CI / Backend tests (pull_request) Successful in 4m6s
Project CI / Repository checks (pull_request) Failing after 43s

合并 origin/master 8f19964e3 到 BGM 优化分支

保留 BGM 记录、完美像素改动及双方前端测试

通过冲突相关前端定向测试(61项)
This commit is contained in:
2026-08-05 13:43:10 +00:00
141 changed files with 24075 additions and 1877 deletions
@@ -2991,7 +2991,7 @@
"none",
"pixelArt"
],
"description": "可选生成后处理风格,当前识别 none 与 pixelArt。省略、null、空字符串或 none 按无风格处理;pixelArt 仅支持普通图片(kind 省略)和 character。未知字符串或不支持该风格的 kind 按 none 继续生成并返回 unsupported-image-style 告警;非字符串值返回 400。"
"description": "可选生成风格,当前识别 none 与 pixelArt。省略、null、空字符串或 none 按无风格处理,提交给 provider 的提示词与未带该字段时逐字一致;pixelArt 仅支持普通图片(kind 省略)和 character,会在提示词末尾追加一行像素风约束(普通图片为「画面为像素风格」,character 为「角色主体为像素风格」)并在回图后执行像素规整。未知字符串或不支持该风格的 kind 按 none 继续生成并返回 unsupported-image-style 告警;非字符串值返回 400。"
},
"size": {
"type": "string",
@@ -3405,7 +3405,7 @@
"none",
"pixelArt"
],
"description": "可选生成后处理风格,当前识别 none 与 pixelArt。省略、null、空字符串或 none 按无风格处理;pixelArt 启用图标图集像素规整。未知字符串按 none 继续生成并返回 unsupported-image-style 告警;非字符串值返回 400。"
"description": "可选生成风格,当前识别 none 与 pixelArt。省略、null、空字符串或 none 按无风格处理,提交给 provider 的提示词与未带该字段时逐字一致;pixelArt 会在提示词末尾追加一行「每个图标素材均为像素风格」并启用图标图集像素规整。未知字符串按 none 继续生成并返回 unsupported-image-style 告警;非字符串值返回 400。"
},
"model": {
"type": "string",
@@ -1,6 +1,6 @@
# AI 游戏创作项目开发工作台 PRD
更新时间:`2026-07-28`
更新时间:`2026-08-05`(实时 manifest 与资源焦点状态机收口)
## 1. 产品定位
@@ -43,10 +43,12 @@
### 3.3 资源布局
- “按依赖”和“按类型”分别保存画布位置。
- 切换布局模式后恢复该模式最后一次用户手动拖动结果。
- 当前只允许 dependency / type 自动布局与资源卡点击,不提供资源卡手动拖动入口。
- 切换布局模式后恢复该模式 sidecar 中已有坐标;历史手动坐标只读恢复,不删除、不重置、不迁移。
- 新资源首次进入某个布局时才执行默认不重叠排版;已有坐标不得被自动排序覆盖。
- 依赖布局使用资源生成/引用关系;类型布局按资源大类、子类型、尺寸规格排序。
- 不同资源分区不可互相拖入。
- 资源分区由资源投影固定,前端交互不能改变分类。
- 资源卡手动拖动、拖动持久化、拖动性能与冲突后的重新拖动提示全部暂缓,不作为当前产品合同或验收条件。
### 3.4 数值微调
@@ -58,14 +60,14 @@
现有六个专业组为:
| group | 普通用户名称 | 当前职责 |
| --- | --- | --- |
| `design` | 策划组 | 玩法规格、界面原型、规则与验收口径 |
| `art` | 美术组 | 角色、场景、UI、动画和美术素材 |
| `code` | 程序组 | 可运行原型、模块实现和工程验证 |
| `balance` | 数值组 | 速度、生命、得分和难度参数 |
| `audio` | 音频组 | 背景音乐、音效和音频资源 |
| `publishing` | 发布组 | 质量评审、试玩、打包和发布准备 |
| group | 普通用户名称 | 当前职责 |
| ------------ | ------------ | ---------------------------------- |
| `design` | 策划组 | 玩法规格、界面原型、规则与验收口径 |
| `art` | 美术组 | 角色、场景、UI、动画和美术素材 |
| `code` | 程序组 | 可运行原型、模块实现和工程验证 |
| `balance` | 数值组 | 速度、生命、得分和难度参数 |
| `audio` | 音频组 | 背景音乐、音效和音频资源 |
| `publishing` | 发布组 | 质量评审、试玩、打包和发布准备 |
- 底栏默认突出策划、美术、程序三组。
- 允许在同一底栏展开数值、音频、发布组,不删除既有专业组。
@@ -121,9 +123,15 @@ completed -> starting(nextSlice)
idle -> focused(document|art|audio|version) -> idle
```
- 文档:在中央画布展开并独立滚动。
- 美术/音频:进入对应媒体聚焦状态,工具能力复用现有编辑器。
- 版本:高亮版本引用资源;替换动作只创建下一迭代版本。
- 文档:合法 Agent 文本回执直接使用对话投影内容;项目文件只允许读取当前 manifest 已登记资产或已完成任务产物中的 Markdown、文本、JSON、YAML、TOML,必须经过 `file.read` auto 权限、相对路径、项目边界、普通文件、符号链接 / 硬链接、读取漂移、2 MiB、UTF-8 与扩展名白名单校验。正文使用不执行 HTML、不加载远程图片、不产生可点击外链的安全 Markdown 渲染,并在中央画布内独立滚动;读取失败显示错误空态。
- 美术:PNG、JPEG、WEBP 继续使用图片魔数与像素边界预览;GIF、SVG、AVIF、BMP、MP4、WebM、MOV 通过新增受控媒体读取链路按文件签名校验后在中央画布放大聚焦。SVG 额外拒绝脚本、事件处理器、外部资源引用和实体声明;视频使用内置播放控件。读取失败显示错误空态。
- 音频:只读取 manifest 已登记音频或已成功导入且登记到 manifest 的附件,按文件签名接受 MP3、WAV、OGG / Opus、M4A、AAC、FLAC;聚焦态展示实际格式、浏览器解码后的时长以及带播放进度和暂停能力的内置播放器。音频任务声明中的未登记路径继续不得读取或播放。
- 版本:只展示 manifest 中正式、不可变的迭代版本记录;版本卡展示项目修订、创建原因与父版本,聚焦态同时展示直接子版本和资源绑定。点击版本卡后高亮仍存在于当前资源投影中的引用资源;缺失历史资源只保留绑定身份,不生成幽灵资源卡。资源替换仍留给后续切片。
- mentor 最新决定:资源聚焦不提供工具栏,也不提供工具侧边栏。
- 点击资源后,中央主视窗从 `resources.list` 切换为 `resources.focused.document / art / audio / version`,左侧平台导航、右侧 Supervisor 对话和底部 Agent 状态栏保持原位;聚焦容器只包含标题、资源主体、必要元数据与右上角收起按钮,不使用页面级浮层或可拖动标题栏。
- 焦点转换以稳定资源 ID 为准。只有从资源列表进入详情或从一个资源 ID 切换到另一个 ID 时聚焦详情 region;同一资源 ID 因 manifest 更新而重新投影时,不得抢走详情内音频 / 视频控件、文档链接或收起按钮的当前焦点。
- 显式收起或按 Escape 后恢复进入前的搜索条件、dependency / type 布局模式、资源画布滚动位置和选中资源,并优先把键盘焦点还给原触发资源卡;这些只属于当前前端会话,不写入布局 sidecar。若资源已经被后台删除,必须清理 stale focused / selected ID、关闭详情并把焦点落到“搜索项目资源”,不得落到 `body`。项目切换和进入运行视图必须取消旧项目的焦点恢复意图。
- 阶段四只新增上述受控读取与媒体展示;阶段六在同一聚焦容器内补齐正式版本只读展示和引用高亮,但不新增资源聚焦工具栏 / 工具侧边栏,不新增美术编辑、音频编辑 / 替换、资源重新生成、版本替换或运行模块。飞书原需求中“编辑并生成新资源”的条件项仍暂缓,不能只打开画板却缺少回写、`referenceResourceIds` 血缘登记、新资源自动选中与邻近布局的完整闭环。
### 4.4 历史成果与当前状态
@@ -153,7 +161,7 @@ P0 中 `approvalMode` 只能有效写入 `strict`;其它值只能作为不可
### 5.2 资源画布布局(P1)
实现状态(2026-07-28):本节布局合同已在独立客户端落地,dependency / type 双模式通过项目内 CAS sidecar 独立持久化;关系线、资源替换、缩放 / 平移等其余 P1 能力仍按本文非目标保持未实现。
实现状态(2026-08-03):dependency / type 双模式通过项目内 CAS sidecar 独立持久化;dependency 模式由 Tauri Rust 只读构建关系拓扑与确定性依赖深度、前端 SVG 派生几何,图结构和线段均不写入布局 sidecar。依赖图加载完成前设布局初始化屏障,避免以临时 `dependencyDepth=0` 生成并持久化错误坐标。当前用户入口只允许自动布局与资源卡点击;资源卡手动拖动已按 mentor 决定暂缓。历史 sidecar 坐标继续只读恢复,底层布局读写与 CAS 合同保留,但当前没有用户手动布局入口。资源替换、缩放 / 平移等其余 P1 能力仍按本文非目标保持未实现。
```ts
type ProjectResourceCanvasLayout = {
@@ -179,7 +187,7 @@ type ProjectResourceCanvasLayout = {
- `x / y` 是相对所属 `section` 内容原点的 CSS 像素坐标,落盘前四舍五入为非负整数;坐标不使用 viewport、页面或资源详情浮层坐标系。
- `updatedAt` 是持久层生成的 Unix 毫秒时间戳,前端不得自行覆盖。
- `revision` 从 `0` 开始;布局文件不存在时读取接口合成 `revision=0 / positions=[]`,首次成功写入返回 `revision=1`,后续每次成功 CAS 写入递增 `1`。JSON / Tauri / TypeScript 全链路合法范围固定为 `0..=9_007_199_254_740_991`(`Number.MAX_SAFE_INTEGER`),读取、返回或提交负数、小数、非有限值与超限整数都必须失败关闭。
- 新资源第一次进入某个 mode 时由默认布局写入 `manuallyPlaced=false`;用户完成一次有效拖动后写为 `true`。
- 新资源第一次进入某个 mode 时由默认布局写入 `manuallyPlaced=false`。`manuallyPlaced=true` 仅用于兼容历史 sidecar 和保留底层合同;当前界面不会因指针操作新增该值。
- 同一份布局中 `resourceId` 必须唯一。持久层允许暂时存在当前资源投影中没有的旧 ID,因为 Agent 文本成果等资源可能晚于 manifest 恢复;前端协调后必须在下一次成功写入中清除已确认失效的坐标。
- 单份布局最多保存 `4096` 个位置,序列化文件不得超过 `2 MiB`;`resourceId` 最多 `512` 个 Unicode 字符,`x / y` 取值范围固定为 `0..=1_000_000`。
@@ -234,21 +242,46 @@ type UpdateProjectResourceCanvasLayoutResult =
#### 5.2.4 前端布局与协调合同
- 资源卡改用 Pointer Events 驱动二维拖动;超过统一移动阈值后才进入拖动态,普通点击仍打开唯一资源详情浮层,`pointercancel` 恢复拖动前位置。
- 资源只能在原 `section` 内拖动。不同 section 之间既不能通过指针拖入,也不能通过持久 payload 改变当前资源的前端分类事实。
- dependency 默认布局按 `dependencyDepth` 形成横向层级,同层资源纵向寻找第一个不重叠位置;type 默认布局固定按“资源子类型 -> 媒体类型 -> 名称 -> 资源 ID”稳定排序,在分区内从左到右、从上到下寻找第一个空位。布局模型的 `subtype` 必填:manifest 资产使用 `asset.kind`,任务产物、导入附件与 Agent 文本成果分别使用稳定的 `task-artifact`、`attachment`、`agent-result`,不得以缺失值或显示文案兜底;资源协调签名必须包含 subtype。卡片尺寸、间距和拖动阈值必须由单一前端布局模型常量维护。
- 资源集合变化时保留全部仍存在的坐标,只为新 ID 计算默认位置,并删除已确认失效的旧 ID;无论 `manuallyPlaced` 为何,已经写入的现存坐标都不得因重新排序、模式切换或新增资源被自动改写。
- 资源卡是可点击按钮,只负责选择资源并让中央主视窗进入当前唯一资源聚焦状态;不得绑定卡片级 `pointerdown / pointermove / pointerup / pointercancel` 拖动处理器。
- 指针移动不得修改卡片 `x / y`、不得产生拖动预览、不得更新依赖线几何,也不得提交手动布局 CAS。卡片 title、cursor、`touch-action` 和 class 不得暗示可拖动。
- dependency 默认布局按 `dependencyDepth` 形成横向层级,同层资源纵向寻找第一个不重叠位置;type 默认布局固定按“资源子类型 -> 媒体类型 -> 名称 -> 资源 ID”稳定排序,在分区内从左到右、从上到下寻找第一个空位。布局模型的 `subtype` 必填:manifest 资产使用 `asset.kind`,任务产物、导入附件与 Agent 文本成果分别使用稳定的 `task-artifact`、`attachment`、`agent-result`,不得以缺失值或显示文案兜底;资源协调签名必须包含 subtype。卡片尺寸与间距由单一前端布局模型常量维护。
- type 模式资源集合变化时保留全部仍存在的坐标,只为新 ID 计算默认位置,并删除已确认失效的旧 ID。dependency 模式只永久保留 `manuallyPlaced=true` 的用户坐标;`manuallyPlaced=false` 属于可派生自动位置,在 Rust 关系图首次就绪或可信 producer / dependency depth 变化后按最终拓扑确定性重算。自动重算不得移动手动坐标,协调结果与持久布局逐项一致时不得产生 CAS 写入。
- 搜索或筛选只隐藏卡片,不删除、压缩或重排其坐标;清空搜索后恢复原位置。
- 窗口尺寸变化只改变可视范围和分区滚动边界,不回写、裁切或缩放持久坐标。当前客户端继续以 `1280×800` 横屏合同验收。
- 打开项目、切换 mode 或当前 mode 首次出现新资源时执行“读取 -> 协调 -> 必要时 CAS 写入”;项目或 mode 已切换后返回的旧异步结果必须丢弃。
- 同一 `projectPath + projectId + mode` 的首次读取与资源集合协调必须分开:资源集合变化不得取消已经发出的读取或保存。同一 scope 内全部手动拖动和资源自动协调写入使用同一 FIFO,任一时刻最多一个 CAS 在途,后一笔必须使用前一笔成功返回的 revision,不能用“最后请求获胜”跳过中间 CAS。切换项目或 mode 后,旧 scope 的在途请求不能阻塞新 scope 队列;前端放弃旧请求槽位并丢弃其迟到响应,后端继续依靠 `expectedProjectId + expectedRevision + 系统锁` 仲裁已发出的请求。
- 某笔 CAS 在途期间,同一 scope 内对相同 `resourceId + section` 重复产生但尚未发送的拖动意图必须折叠为最后坐标;已经在途的请求不得取消,不同资源的顺序不得跨越。队列增长必须受当前资源与分区数量约束,不能随连续 pointer 事件无界累积。
- 用户拖动结束后先乐观更新,再立即提交一次 CAS。成功后以返回布局更新 revision;普通写入失败时恢复最近可信持久布局并提示“布局保存失败,已恢复上次布局”。
- CAS 冲突时直接载入返回的最新布局并提示“布局已在其他窗口更新,请重新拖动”,丢弃所有基于冲突前快照排队的手动拖动,不得自动重放本地旧坐标或静默覆盖另一窗口结果。即使当前在途请求是允许自动重试的资源协调,只要本次冲突实际清除了任何排队手动拖动,也必须按当前 scope 保留重新拖动提示;后续资源协调成功、失败或通用提示定时器都不得静默清除,只有新的手动布局成功保存或切换 scope 才能解除。资源自动协调可以基于冲突返回的新 revision 有界重试,单次资源签名最多追加 `2` 次,持续跨窗口写入时不得无限自旋。
- 打开项目、切换 mode 或当前 mode 首次出现新资源时执行“读取 -> 协调 -> 必要时 CAS 写入”;dependency 模式必须先等待与当前 `projectPath + projectId + resource inputs` 匹配的 Rust 图进入 `ready` 或 `failed` 终态,等待期间不得创建 fallback、读取 sidecar、协调资源或入队保存。`failed` 只允许以空图降级初始化一次。项目或 mode 已切换后返回的旧异步结果必须丢弃。
- 同一 `projectPath + projectId + mode` 的首次读取与资源集合协调必须分开:资源集合变化不得取消已经发出的读取或保存。当前 scope 内资源自动协调写入使用单写者 FIFO,任一时刻最多一个 CAS 在途,后一笔必须使用前一笔成功返回的 revision。切换项目或 mode 后,旧 scope 的在途请求不能阻塞新 scope 队列;前端放弃旧请求槽位并丢弃其迟到响应,后端继续依靠 `expectedProjectId + expectedRevision + 系统锁` 仲裁已发出的请求。
- 自动协调 CAS 冲突时直接载入返回的最新布局;仍需协调时可以基于权威 revision 最多追加 `2` 次重试,持续跨窗口写入时不得无限自旋。当前提示只说明“布局已在其他窗口更新”,不得要求用户重新拖动。
- 缺少 Tauri bridge 的浏览器开发态可以保留当前会话内布局用于界面测试,但不得宣称已经持久保存。
#### 5.2.5 暂缓的手动拖动合同
- Pointer Events 二维拖动、移动阈值、拖动态 class、乐观手动坐标与结束时单次 CAS 提交暂缓。
- 手动意图折叠、冲突后丢弃旧手动坐标、重新拖动提示和 4096 卡片拖动帧预算暂缓,不进入当前验收。
- 已有 Hook 手动意图、命令式 SVG preview、sidecar 字段和 Rust CAS 基础设施可以保留为未接线技术资产;当前资源卡不得调用这些入口。
#### 5.2.6 资源依赖关系图层
- 阶段五实现状态(2026-08-03):dependency 自动排列同时消费任务 DAG 与精确资源引用。Rust 把可信 producer 的任务深度作为资源深度下限,再对 `asset-reference` 图做迭代式 SCC 压缩与确定性层级传播;被引用资源位于引用资源之前,同一引用环共享稳定深度,环后资源继续递增,没有引用关系的资源保持默认不重叠位置。布局深度通过独立 `dependencyDepths` 返回,不能把 producer assignment 冒充全部资源的布局结果。
- `producerMappingTruncated=true` 只关闭依赖有界 Agent DB 审计的 `producerAssignments`、`taskFlows` 和 `cyclicTaskIds`。Rust 返回的 `dependencyDepths` 仍是 manifest / 精确引用 read model 的权威结果,前端必须过滤未知资源、负数、非整数和非安全整数后继续消费;不得因 producer 截断清空全部深度,也不得在前端重算替代深度。`referenceEdges`、connection index 中的 reference 关系、`cyclicResourceIds` 和 unresolved reference 继续保持可信。
- 图层只在 dependency 模式挂载;type 模式不得渲染 SVG、连线或 marker。切换 mode、切换项目或卸载工作台时必须销毁旧图层,并清理尺寸观察和窗口事件监听。
- 输入固定为当前资源投影的全部卡片身份 / 坐标与 Tauri Rust 返回的 `ProjectResourceGraph` 只读 DTO;Rust 负责资源过滤、去重、迭代式环检测、SCC 压缩后的确定性依赖深度、任务流聚合和一跳连接索引,前端只负责 DTO 防御归一化、浏览器几何与原生 SVG path / marker。SVG 叠加在资源卡底层并设置 `pointer-events: none`,不得引入 D3、React Flow 等图表库,也不得阻断卡片点击。
- `asset-reference` 表示精确资源引用,使用明亮橙色实线与连续贝塞尔曲线。`GameCreationAppAssetManifestEntry.source.referenceResourceIds` 中的外部资源 ID 必须先唯一匹配另一项资产的 `source.resourceId`,再映射为当前资源卡 ID;缺失、重复或已删除的目标均不得渲染幽灵连线。
- `task-flow` 表示同一资源类型内的任务产物流转,使用灰色圆头虚线;文档、项目版本、美术、音频之间不得绘制跨分区虚线。任务依赖按 `sourceTaskId -> targetTaskId + section` 分区聚合为一条主线,两端只保留同分区资源并绘制平滑曲线分支,不得出现直角折线;禁止对上下游资源生成笛卡尔积连线。任务主线与分支可以使用不同线宽和透明度表达聚合层级,但不能改变端点或方向语义。
- 画布资产 producer 只能来自 `agent.runtime.canvas.asset_generate` 的 `assetId -> agentId` 审计且 `agentId` 必须存在于当前 manifest;External Editor `source.taskId` 属于平台生成任务命名空间,禁止当作 manifest task ID。证据缺失、冲突或有界审计读取未覆盖时不生成对应 task flow,不猜测归属。
- 图模型必须对资源引用图和完整任务依赖图做迭代式环检测,不得用无界递归遍历;参与环的可见边保留渲染并标记 cyclic,环本身不能造成重复生成或死循环。
- 资源自引用的起点与终点为同一张卡片时,必须绘制在卡片外侧的可见闭环并保留箭头,不得让路径穿过卡片后被底层 SVG 层级遮挡。
- 搜索只允许为当前可见端点生成几何;任一精确引用端点隐藏时该线隐藏,聚合任务流只保留仍可见的两端分支,任一侧没有可见资源时整条任务流隐藏。
- 资源点击只进入中央聚焦并保留当前选中卡片,不改变依赖卡片或连线的颜色、线宽与透明度;关系线始终直接展示,不提供点击后的上下游高亮或无关线弱化。
- 资源卡 Pointer Move 不改变基础 positions 或 SVG 几何。连线只随布局读取、资源自动协调、搜索、项目切换或 section origin 变化而更新。
- `ResizeObserver` 在单个图层生命周期只允许构造一次。dependency section 额外提供至少 `64px` 右侧视觉 gutter,确保最右侧自环和箭头可完整滚动显示,但不得修改卡片坐标或布局 sidecar。
- 阶段五不改变手动位置边界:已有 `manuallyPlaced=true` 坐标原样保留,资源引用新增或变化只允许重新派生 `manuallyPlaced=false` 的自动坐标;任务流继续按任务对与资源分区聚合,禁止为了计算深度或绘线生成资源笛卡尔积。
### 5.3 资源类型与替换兼容性(P1)
实现状态(2026-08-03):当前资源投影已收口到固定的“文档 -> 项目版本 -> 美术资源 -> 音乐音效资源”四区。文档只接收 Markdown / 文本 / JSON / YAML 等正式项目文档和合法 Agent 文本回执;项目版本只接收显式 `ProjectVersionResourceSummary` read model,未知任务产物不得兜底为版本;美术接收图片、SVG、动画和视频类产物;音频只接收 manifest 已登记音频资产或已成功导入并登记到 manifest 的音频附件,任务声明中的未登记音频路径不冒充正式音频资源。无法识别的二进制任务产物和附件不进入资源画布。阶段四已为本地文档、安全 SVG / 扩展图片 / 视频和音频补齐受控读取、中央聚焦、失败空态与媒体播放;这些都是只读表现层,不改变资源投影或 manifest 真相。
资源身份固定使用 manifest asset ID、正式 version ID、Agent ID + run ID 或已导入资源稳定路径;显示标题、来源文案变化不得改变 `resourceId`,从而避免布局、依赖边、选择和聚焦状态因改名失效。
```ts
type ProjectResourceDescriptor = {
resourceId: string;
@@ -276,19 +309,29 @@ type ProjectVersionResourceReplacement = {
### 5.4 游戏迭代版本(P1)
阶段六实现状态(2026-08-03):正式版本业务真相扩展在本地项目 `.agent/manifest.json` 的可选 `versions` 字段中;旧项目字段缺失时等价于空列表,不根据 checkpoint、布局 sidecar、预览记录或 `game-creator-project-revision.v1` 自动伪造版本。版本数组只允许追加,已有记录不得删除、重排或修改;首轮没有版本创建按钮,也不自动把当前编辑态登记为版本。
```ts
type GameIterationVersion = {
versionId: string;
parentVersionId: string | null;
projectRevision: number;
resourceBindings: Array<{ slotId: string; resourceId: string }>;
parameterSnapshotId: string;
createdReason: 'initial' | 'resource-replacement' | 'agent-revision';
createdAt: number;
};
type GameCreationAppManifest = {
// 既有字段省略
versions?: GameIterationVersion[];
};
```
版本写入后不可修改。
- `versions` 按追加顺序保存。第一条必须是 `initial + parentVersionId=null`;后续记录必须引用数组中更早出现的父版本,创建原因不能再是 `initial`,从而天然排除自引用、悬空父版本和父子环。
- `projectRevision` 与 `createdAt` 必须是 JavaScript 安全非负整数;子版本的修订必须严格大于父版本,创建时间不得早于父版本。
- 同一版本内 `slotId` 唯一;`resourceId` 固定保存 manifest asset ID,不保存资源卡显示名称、External Editor resource ID、路径或布局 ID。历史资源已不在当前 manifest 时仍保留原绑定,但界面不为其合成资源卡。
- Tauri manifest 存储边界在每次写入前校验完整版本图,并与磁盘中的旧 `versions` 前缀逐项比较;只允许追加新记录,已有记录被修改、删除或重排时写入失败且原文件保持不变。
- 版本卡标题由稳定追加序号生成,卡片与聚焦态展示 `versionId / projectRevision / createdReason / parentVersionId`;聚焦态额外展示直接子版本和全部 slot 绑定。点击版本卡只高亮当前投影中唯一匹配 `asset:<resourceId>` 的资源卡,不修改版本或资源。
### 5.5 测试切片与数值参数(P2)
@@ -339,7 +382,7 @@ type ProjectAgentMudPointAttribution = {
- 复用现有四区工作台壳。
- 资源/运行切换与客户端内 loopback 预览。
- 只读资源画布、文档展开、美术/音频聚焦入口。
- 固定四类资源投影、只读资源画布与中央主视窗资源聚焦;资源聚焦工具栏 / 工具侧边栏及后续媒体能力暂缓。
- Supervisor 正式会话、上传、Runtime 确认与安全错误。
- 当前 run 专业状态与项目历史成果分离。
- 默认三专业组,并可展开另外三组。
@@ -348,9 +391,9 @@ type ProjectAgentMudPointAttribution = {
### P1
- 先实施依赖/类型两套坐标持久化、首次默认不重叠布局、跨重启恢复与 CAS 冲突处理。
- 已实施依赖/类型两套坐标持久化、首次默认不重叠布局、历史坐标跨重启恢复与自动协调 CAS 冲突处理;资源卡手动拖动暂缓。
- 资源关系线在布局持久化验收通过后单独实施,不与本切片捆绑伪造完成。
- 版本资源高亮、兼容性判断和不可变下一迭代版本。
- 已实施正式版本只读模型、版本卡、父子关系与引用资源高亮;资源兼容性判断和不可变下一迭代版本创建仍待后续切片。
- 美术/音频编辑状态接线。
### P2
@@ -371,21 +414,55 @@ type ProjectAgentMudPointAttribution = {
4. 底栏默认显示策划、美术、程序,可展开数值、音频、发布;状态与任务来自真实 Runtime/manifest。
5. 风险审批和无需审批不能改变运行策略,点击后明确提示尚未开放;严格审批继续使用现有 Runtime 门禁。
6. 不显示伪造泥点、伪造资源完成度、伪造图片或外部浏览器成功提示。
7. 未识别任务产物不进入“项目版本”,只有正式版本 read model 可以生成版本卡;资源显示名称变化不改变资源身份。
8. 点击任一资源后只替换中央主视窗,右侧对话与底部 Agent 状态栏保持原位;收起或按 Escape 退出后恢复原搜索、布局模式、滚动位置、选中资源和触发资源卡键盘焦点。
9. 当前 Supervisor 运行期间 manifest 新增资产、任务状态、预览状态和正式版本后,工作台无需重开项目即可同步更新资源列表、依赖图输入、运行入口和版本卡;旧项目迟到回调不得覆盖当前项目。
10. 实时 manifest 验收必须捕获真实 App Tauri listener,并让 `get_local_game_manifest` 在非 Supervisor Agent 的 Runtime / manifest 失效事件后返回新快照;测试不得直接调用 `onManifestChange` 冒充数据源。读取合并、旧项目迟到响应和项目切换隔离必须分别有回归证据。
11. 音频或视频控件获得焦点后,同一资源 ID 的 manifest 更新不得把焦点移回详情 region;当前资源被删除后详情关闭、focused / selected ID 清理且焦点落到资源搜索框。显式收起与 Escape 的原卡片焦点和滚动恢复继续成立。
### 7.2 P1 资源画布布局持久化验收
1. 同一项目在 dependency 与 type mode 分别拖动资源后,关闭并重启客户端,两种 mode 都恢复各自最后一次成功保存的位置。
1. 同一项目在 dependency 与 type mode 分别读取并协调布局,关闭并重启客户端后恢复各自 sidecar 坐标;历史 `manuallyPlaced=true` 坐标保持不变。
2. 新资源进入任一 mode 时获得不重叠默认位置,现存资源坐标保持逐项不变;删除资源后,下一次成功写入不再包含已确认失效的 ID。
3. 资源不能跨 document、version、art、audio 分区;点击、搜索、筛选和资源详情浮层行为不因二维拖动回归。
4. 两个窗口基于同一 revision 写入时最多一个成功;失败方收到 `conflict` 与最新完整布局,界面不静默覆盖成功方结果。
3. 资源卡 title、cursor、`touch-action` 与 class 只表达可点击;Pointer Down / Move / Up / Cancel 不改变坐标、不产生拖动预览、不提交手动布局 CAS,点击仍打开当前资源详情。
4. 两个窗口基于同一 revision 执行资源自动协调写入时最多一个成功;失败方收到 `conflict` 与最新完整布局,界面不静默覆盖成功方结果。
5. 布局文件缺失的旧项目可以无迁移打开;损坏、未知 schema、身份冲突、超限和链接文件失败关闭,且原文件不被空布局覆盖。
6. 布局读写不改变 manifest、游戏项目 mutation revision、Runtime verification、Agent 权限与预览状态。
7. `1280×800` 最小横屏下全部资源可通过分区滚动访问,不出现页面级横向或纵向溢出,右侧对话和底部 Agent 状态栏保持可见。
### 7.3 P1 资源依赖关系图验收
1. dependency 模式显示对画布背景至少 `3:1` 对比度的橙色实线资源引用,并只在同一资源类型分区内显示灰色虚线任务流;跨类型不显示虚线,type 模式没有图层或连线。
2. 精确引用只接受唯一有效的外部资源 ID 映射,删除或不存在的资源不产生幽灵连线。
3. 多资源任务依赖按资源类型分区后,各分区只形成一条聚合主线与 `O(S+T)` 条端点分支,不产生 `S×T` 连线或跨分区虚线。
4. 资源引用环和无资源产物参与的任务环都可被有限遍历识别,界面不死循环。
5. 搜索触发端点过滤;资源点击不改变上下游卡片或任何连线的视觉状态,资源卡指针移动不更新线段,点击与中央聚焦行为不回归。
6. 切换布局模式或项目后旧 SVG、ResizeObserver 与窗口监听全部清理;图层从不写入 layout sidecar、manifest 或其它持久化。
7. 4096 资源链式 fixture 继续验证拓扑、聚合复杂度和自动布局性能;拖动局部更新与真实 Chromium 拖动帧预算暂缓,不作为当前验收条件。最右侧自环与箭头仍需完整显示。
8. Rust 图读取延迟时,dependency sidecar 在图进入 `ready / failed` 前没有读取或写入;首次布局直接使用 Rust 返回的最终 producer 与 dependency depth。Agent DB 有界读取截断时 producer、task flow 与 `cyclicTaskIds` 失败关闭,精确 manifest 引用及 Rust 返回的合法 `dependencyDepths` 继续到达布局层。重新打开包含深度 `0 / 1 / 2` 自动坐标的旧布局时不得降成全 `0` 或持久化扁平布局;手动位置逐项不变,自动位置按最终拓扑协调且相同结果不增加 revision。
9. 依赖 SVG 作为装饰层不可聚焦并对辅助技术隐藏;画布通过关联的视觉隐藏文本逐条说明当前可见资源引用和任务流,搜索过滤或模式切换后文本与可见关系同步变化。
### 7.4 P1 正式项目版本阶段六验收
1. manifest 缺少 `versions` 时旧项目正常打开且不显示伪造版本;存在合法记录时,固定“项目版本”分区按追加顺序显示稳定版本卡。
2. 根版本、父版本和直接子版本关系在卡片或聚焦态可见;悬空父版本、自引用、重复 ID、非递增修订、倒退时间、重复 slot 和超限数字均失败关闭。
3. 点击版本卡后,当前 manifest 中仍存在的绑定资产卡被高亮;历史已删除资产只在版本详情保留 ID,不创建幽灵卡,也不把 External Editor resource ID 猜成 manifest asset ID。
4. 版本聚焦态只读展示身份、修订、创建原因、父子关系、创建时间和 slot 绑定,不提供编辑、替换、切换、回滚或运行按钮。
5. 任意现有 manifest 写入只能保留磁盘版本前缀并追加新记录;存储边界以跨进程专用锁串行覆盖旧状态读取、前缀校验、安装和回读,修改、删除、重排或并发旧快照覆盖已有版本时写入失败。
6. 版本选择和高亮不写 manifest、布局 sidecar 或 project revision;dependency / type 两种布局都可显示绑定高亮,既有依赖关系 SVG 语义不变。
### 7.5 阶段七完整验收
1. 对照飞书需求、当前 PRD、技术方案、代码、测试与阶段提交复核阶段零至阶段六;美术编辑生成新资源继续按本 PRD 已确认的闭环条件暂缓,不作为遗漏或伪完成。
2. AppSurface 同时覆盖文档、图片、SVG、音频和视频聚焦;视频必须使用原生 `controls` 且 `preload="metadata"`,读取策略失败时中央主视窗显示安全空态,右侧对话和底部 Agent 状态栏继续存在。
3. `1280×800` 应用内浏览器实测 `window`、document 与 body 均无页面级横向或纵向溢出。浏览器开发页受真实登录门禁保护,不为验收绕过认证或伪造 Tauri;工作台内部结构由 AppSurface 集成测试与资源布局 CSS 合同测试复核。
4. 根目录全量 Vitest、前后端 typecheck / lint / build、Rust workspace test / check、SpacetimeDB schema、原生壳、内容 / 编码、生产运维与部署门禁全部通过后,阶段七才允许提交。
5. 本地 `.env`、`.env.local`、密钥、缓存、日志和构建产物不进入阶段七提交;提交前再次执行编码检查和 `git diff --check`。
## 8. 非目标
- 资源画布布局持久化切片不实现资源关系线、资源替换、不可变迭代版本、画板编辑状态、测试切片、数值参数或泥点归因。
- 本切片不保存资源详情浮层位置、画布缩放 / 平移、搜索条件、筛选条件或当前 mode;这些状态如需持久化必须另行扩展合同,不能塞入 `game-creator-resource-layout.v1`。
- 当前收口不实现资源卡手动拖动,也不实现资源聚焦工具栏、资源聚焦工具侧边栏、美术编辑、音频编辑 / 替换、资源重新生成、资源替换、下一迭代版本创建入口、运行版本切换、版本回滚、运行模块扩展、测试切片、运行态消费版本、数值参数或泥点归因。正式版本记录已经成为 manifest 业务真相,但当前只读取、校验和展示已有记录。
- 本切片不持久化资源聚焦状态、画布缩放 / 平移、搜索条件、筛选条件或当前 mode;聚焦退出时的列表上下文恢复只限当前前端会话,这些状态如需跨重启保存必须另行扩展合同,不能塞入 `game-creator-resource-layout.v1`。
- 不修改 SpacetimeDB schema。
- 不开放普通用户 Agent.md/Skill。
- 不自动确认 Agent 动作,不自动触发可能扣费的生成。
File diff suppressed because one or more lines are too long
@@ -94,6 +94,12 @@ npm run test -- apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts --run
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml autonomous_completion_contract -- --nocapture --test-threads=1
```
修改 manifest invalidation relay、GUI owner attach 或其测试夹具后,所有会读写进程全局事件 sink 的测试统一使用 `manifest_invalidation_sink_isolation_` 前缀,并至少以 2 个 test thread 重复运行该 filter。测试 fixture 的 accept 和 payload 读取都必须使用总 deadline,不能只在 accept 成功后给 `TcpStream` 设置 read timeout;全局 sink 只能在共享 test-only 串行锁内由 RAII guard 配置和清理。
```bash
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml manifest_invalidation_sink_isolation_ -- --nocapture --test-threads=2
```
source allowlist、game-chat 单轮完成门和 Canvas spritesheet 引用门禁均须命中实际测试;若完整 Rust suite 受 Windows `os error 32` 既有文件锁竞态影响,应单独复跑新增 filter 并如实记录,不能把锁竞态失败改报为本次改动通过。
Windows release 的非交互后台命令统一使用 `CREATE_NO_WINDOW`,包括 `command.exec / project.verify`、STDIO MCP、Repository Context Git、`git.inspect / project.git_commit` 和 `taskkill` 清理命令;需要进程组终止时再叠加 `CREATE_NEW_PROCESS_GROUP`,不要使用 `DETACHED_PROCESS`。smoke 时应在实际任务运行期间观察无额外控制台窗口,并在关闭客户端后核对整棵后台进程树为零,再重启确认 reconciliation 可继续。
@@ -541,7 +547,7 @@ npm run check:server-rs-ddd
- 仓库 CI 入口是 `.gitea/workflows/project-ci.yml`,向 `master`、`codex/ai-game-creator-app` 推送和所有 PR 创建、更新时必须运行,也允许手工触发。
- CI 固定拆分为 `Repository checks`、`Frontend tests`、`Backend tests`、`Native shell tests` 四个 required job;对应 PR context 完整名称是 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)`,首次运行后仍须从 Gitea 最近一周 context 表复核。测试使用独立 job,不能只藏在综合检查 step 中;原生壳验收单独运行以便定位重型构建失败。
- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。原生壳 job 还要通过 Google 官方签名 APT 源安装 `google-chrome-stable`,用于 headless preview 的真实 DOM / canvas smoke;同时安装 `ripgrep`,把 `actions/setup-node` 的完整 Node.js 22 发行目录与 root-owned rustup proxy 映射到 `/usr/local`,供只接受受信任系统命令目录的 `command.exec` 沙箱测试使用,不能放宽生产命令目录白名单。Tauri 的 1132 项级别 suite 固定 `--test-threads=1`,避免共享 Agent Runtime 后台锁与异步终态在 libtest 并行调度下互相干扰。
- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。Backend job 必须先用 `cargo fetch --locked` 的 5 次整命令级有界重试准备当前 `server-rs` 锁定依赖,再进入会触发 `cargo build` 的 DDD / module-runtime 产物边界门禁;不得把依赖准备放在该门禁之后,否则镜像缺少锁新增 crate 时会在首次 registry / TLS 抖动处提前失败。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。原生壳 job 还要通过 Google 官方签名 APT 源安装 `google-chrome-stable`,用于 headless preview 的真实 DOM / canvas smoke;同时安装 `ripgrep`,把 `actions/setup-node` 的完整 Node.js 22 发行目录与 root-owned rustup proxy 映射到 `/usr/local`,供只接受受信任系统命令目录的 `command.exec` 沙箱测试使用,不能放宽生产命令目录白名单。Tauri 的 1132 项级别 suite 固定 `--test-threads=1`,避免共享 Agent Runtime 后台锁与异步终态在 libtest 并行调度下互相干扰。
- checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。
- 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。
- Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发、crates.io 和 Google Chrome 的 `dl.google.com` 官方签名 APT 源;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。
+90 -2
View File
@@ -14,6 +14,45 @@
- 关联:相关文件、文档、提交或 Issue
```
## `timeout_at` 不能替代显式的预算耗尽预检
- 现象:给完美像素加端点级并发闸后,预算已经耗尽的请求仍然能拿到许可,白占一个名额继续去打几轮全账号 SpacetimeDB 扫描,直到下载那步才失败。
- 原因:`tokio::time::timeout_at` 会先 poll 一次内层 future 再判超时。信号量有空闲许可时 `acquire_owned()` 首次 poll 就绪,于是即使 deadline 早已过去,返回的仍是 `Ok(Ok(permit))` 而不是超时。既有 `acquire_editor_pixel_art_cpu_permit` 里那句 `if Instant::now() >= processing_deadline` 正是为此存在,新写的许可函数漏掉后被单测抓出。
- 处理:所有「先判预算、再等资源」的获取函数都必须在 `timeout_at` 之前显式判一次 `Instant::now() >= deadline` 并直接返回超时错误;这句不是冗余防御。同理,进入排队计数之前也要先做这个预检,避免为注定失败的请求占用队列名额。
- 验证:在有空闲许可时用已过期的 deadline 调用获取函数,只断言返回 `504` 而不是许可;`504` 已足以证明显式预检没有被 `timeout_at` 的首次 poll 绕过。禁止在该用例里读取进程级队列 Atomic 的 before/after;相对断言同样会被并行测试插入。仅靠「信号量占满时超时」的用例发现不了这个问题。
- 关联:`server-rs/crates/api-server/src/editor_project.rs`(`acquire_editor_pixel_art_snap_permit`、`acquire_editor_pixel_art_cpu_permit`)。
## 有界等待队列的计数递减必须写在 Drop 里
- 现象:给同步端点加「最多 N 个等待者」的保险丝时,若把计数递减写在正常返回路径上,客户端断连或超时触发会让等待中的 future 被丢弃而跳过递减;计数只增不减,最终队列永久判定为满,接口对所有人返回 `503` 且不会自愈。
- 原因:Rust 的 async future 可以在任意 await 点被取消,取消时只保证 `Drop` 会跑,不保证后续代码会执行。有界队列的入场与离场天然不对称。
- 处理:把递增封进一个 guard 结构体,递减放在它的 `Drop` 实现里;递增本身用 `fetch_update` 的 CAS,不能用「先读后加」——两个线程同时读到 `max - 1` 各自加一就会越界。拿到资源后立即 `drop(guard)` 让出队列名额,不要让它跟着许可一起活到请求结束。
- 验证:单测覆盖 CAS 边界(满了返回失败且计数不越界、上限为 0 时任何进入都失败),并由独立用例覆盖 guard 离开作用域后的计数归还。预算耗尽路径只断言 `504`,不得通过另一个测试也会修改的进程级 static before/after 来推断“未入队”,也不得用串行锁或 `--test-threads=1` 掩盖隔离问题。
- 关联:`server-rs/crates/api-server/src/editor_project.rs`(`try_enter_bounded_queue`、`EditorPixelArtSnapQueueGuard`)。
## Linux 生产脚本门禁不能假设本地也是 GNU userland
- 现象:macOS 本地运行维护页、生产 API 部署和 Rust 产物门禁时,依次出现 `mv: illegal option -- T`、`mapfile: command not found`、`/usr/bin/cp` / `/usr/bin/chmod` 不存在,以及 `.rlib` 明明含有 `.o` 却报告“没有可扫描成员”;安全修复计划还会把 `/var/folders` 到 `/private/var/folders` 的系统别名误判为用户符号链接。
- 原因:生产机是 Linux/GNU,而本地门禁运行在 BSD userland、Bash 3.2 和 BSD ar;测试桩硬编码 Linux 二进制路径与参数,归档解析器没有去掉 BSD 扩展成员名的尾随 NUL,路径校验也直接比较了未规范化字符串。
- 处理:维护 marker 使用同目录临时文件加 POSIX `mv -f`,并在替换前拒绝所有符号链接和目录目标,避免 `mv -f` 跟随目录链接把临时文件移入链接目标;生产部署测试桩在 macOS 忠实模拟 GNU `mv/ln -T` 的“目标不是目录”语义,并按平台选择系统工具;脚本收集服务使用 Bash 3.2 可用的 `while read`;rlib 解析清理 BSD 成员名 NUL;计划文件只规范化系统临时目录别名,仍拒绝其下用户创建的符号链接组件。
- 验证:运行 `npm run check:maintenance-page`、`npm run check:production-api-deploy`、`npm run check:server-rs-ddd`、`npm run test -- scripts/spacetime-repair-editor-canvas-resources.test.ts`,并在 Linux CI 保留同一生产脚本语义。
- 关联:`scripts/deploy/maintenance-on.sh`、`scripts/check-maintenance-page.mjs`、`scripts/check-production-api-deploy.mjs`、`scripts/deploy/production-api-deploy.sh`、`scripts/check-module-runtime-artifact.mjs`、`scripts/spacetime-repair-editor-canvas-resources.mjs`。
## External Editor taskId 不能当作本地 manifest taskId
- 现象:画布资产之间已有橙色精确引用线,但依赖任务之间没有灰色 task flow;测试用 `design-foundation` 之类字符串时正常,真实生成返回 `task-1` 后失败。
- 原因:`GameCreationAppAssetSource.taskId` 保存的是 External Editor 生成任务身份,命名空间与本地 `.agent/manifest.json` 的 Agent/task 身份不同;前端用 `taskById.get(source.taskId)` 会让真实画布资产全部失去 producer。
- 处理:资源依赖图的 Tauri Rust read model 从有界 `.agent/agent.db` 读取 `agent.runtime.canvas.asset_generate`,以 `assetId -> agentId` 映射 producer,并要求 `agentId` 存在于当前 manifest。记录缺失、多个不同有效 Agent 冲突或读取已截断时失败关闭 producer assignment、task flow 与对应 `cyclicTaskIds`,不回退 `source.taskId`。精确 `asset-reference` 仍只依赖 manifest 中外部 resourceId 的唯一匹配;Rust 独立返回的 `dependencyDepths` 继续作为 manifest / reference read model 权威结果,前端只过滤未知资源、负数、非整数和非安全整数,不得因 producer 截断把它整体清空。
- 验证:Rust fixture 把 `source.taskId` 固定为 `task-1 / task-2`,只有审计提供 `art-director / design-foundation` 后才生成 task flow;移除或截断审计后橙色引用保留、灰色任务流消失,合法深度仍为 `asset:spec=0 / asset:ui=1`。AppSurface 使用截断生产数据形状证明深度 `0 / 1 / 2` 真实到达卡片布局,并且不会把已有自动坐标持久化成扁平布局。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs`、`apps/ai-game-creator-shell/src/view/project-development/resourceDependencyGraphModel.ts`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
## 依赖图未就绪时不能先初始化资源布局
- 现象:首次打开 dependency 画布时所有资源短暂按深度 0 排列;Rust 图返回后连线正确,但卡片仍停留在同一列,错误自动坐标还可能已经写入 sidecar。
- 原因:资源图和布局读取独立异步启动,布局 Hook 在图未返回时使用空图资源创建 fallback;后续 reconcile 按旧合同保留全部已有坐标,真实 producer 与 dependency depth 无法纠正首次自动位置。
- 处理:dependency 模式增加按项目与资源输入隔离的图加载屏障,`ready / failed` 前不启动布局 Hook 的 fallback、读取、协调或保存。Rust read model 返回确定性依赖深度;已有布局只永久保留手动位置,自动位置按最终图重新派生。type 模式不受图加载影响。
- 验证:用 deferred graph Promise 断言终态前 Tauri layout read/update 调用均为 0;图就绪后首次坐标直接按最终深度生成,旧 scope 迟到结果无效,手动坐标不变且相同自动布局不增加 revision。
- 关联:`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/src/view/project-development/useProjectResourceCanvasLayout.ts`、`apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs`。
## Jenkins 异步备份不能用 nohup 脱离作业
- 现象:Stdb Publish 成功,上传日志只留下“已获取进程锁 / 上传已有备份 / 目标对象”,没有成功或可捕获错误;本地 tar.gz 和 `uploadStatus=deferred` manifest 每次发布后继续增长。
@@ -3212,9 +3251,9 @@
- 现象:release 上 api-server 周期性出现全量 `spacetime_stage="pool_acquire" elapsed_ms=45000` 业务超时,`/readyz` 503(`reason=spacetime_unhealthy, stage=pool_acquire`),`/healthz` 仍 200,只有重启能恢复,过若干小时复发。
- 原因:旧 `PooledConnectionLease` 只能显式 `release_connection` 归还;HTTP 请求方在等待 StDB 回包期间断开时 handler future 被取消,permit 自动归还但槽位 `in_use` 永不复位。后续 acquire 在拿到 permit 后进入无界 `loop + yield_now` 扫描空闲槽位,泄漏积累到 pool_size 后整池挂死。
- 处理:租约持有 `Arc<SpacetimeConnectionPool>` 并实现 `Drop` 统一复位槽位/归还连接;槽位改 `AtomicBool` CAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。
- 处理:租约持有 `Arc<SpacetimeConnectionPool>` 并实现 `Drop` 统一复位槽位/归还连接;槽位改 `AtomicBool` CAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。该保证只覆盖本地 lease / slot / permit 回收;RPC 已发出后,handler timeout/drop 不会取消或回滚远端 procedure,结果仍须按 unknown 读取权威事实。
- 验证:`cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`(`dropped_lease_releases_slot_and_permit`、`acquire_times_out_at_pool_acquire_when_pool_is_busy`)。
- 关联:`server-rs/crates/spacetime-client/src/lib.rs`、`docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md`。
- 关联:`server-rs/crates/spacetime-client/src/active.rs`、`docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md`。
## 后台灰度配置不能从 SpacetimeDB 本地表缓存读取
@@ -3707,6 +3746,7 @@
- 原因:`libc::stat.st_dev` 跟随平台 `dev_t`,macOS 为有符号整数,而 `std::os::unix::fs::MetadataExt::dev()` 统一返回 `u64`;直接比较会把 Linux 的类型偶合误当成 Unix 通用契约。
- 处理:与 Rust 标准库的 Unix `MetadataExt` 实现保持一致,先把 `st_dev / st_ino` 规范为 `u64`,再与 `metadata.dev() / metadata.ino()` 比较;设备号、inode 和文件类型三重检查均必须保留。
- macOS 测试夹具:`std::env::temp_dir()` 可能返回 `/var/folders/...`,而 `/var` 是系统兼容符号链接。需要真实项目根的 Runtime 测试应先 canonicalize 已存在的临时根目录,再创建唯一子目录;不得为了让夹具通过而放宽生产 Runtime 的项目根及祖先符号链接拒绝规则。
- 异步测试隔离:测试触发后台 continuation 后,必须等待对应 Agent lane 完整释放,再删除项目夹具或安装下一项全局 mock 配置;否则前一项后台任务可能抢占后一项的唯一 mock 响应,形成只在全量顺序执行时出现的跨测试污染。
- 验证:macOS 本机运行 `cargo check --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`,并复跑 Agent DB、project owner 和 tool-plan handoff 的 Unix 相对句柄替换检测;Linux CI 继续覆盖原有安全回归。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`runner.rs`、`tool_plan_handoff.rs`。
@@ -4085,6 +4125,27 @@
- 现象:可选 server 已成功连接,但返回超限 schema、重复 tool identity 或要求未支持 task-mode 时,整个 MCP catalog 和本轮 Agent planning 一起失败。
- 处理:连接、tools/list、工具归一化与聚合容量都使用同一 required / optional 边界。optional 将该 server 投影为 `connected=false + error + tool_count=0`,required 保持失败关闭;被包入 `action.input` 的 `$ref` 只重定位当前 document 根的 `#` / `#/...` JSON Pointer,命名 anchor、外部 URI 与带 `$id` 的 schema resource 内 fragment 不得改写。
## React 异步读取必须在组件卸载时中止并失效(2026-08-04)
- 现象:单个 Vitest 文件全部通过,全量 CI 却在 jsdom 环境销毁后出现 `ReferenceError: window is not defined`;栈指向请求 Promise 的 `finally` 中调用 React `setState`。
- 原因:测试触发了与断言无关的账户读取,较快环境中请求会在用例结束前失败,较慢 CI 中请求延迟到组件和 jsdom 均已销毁后才收束。仅用 revision 丢弃旧请求而不在卸载时推进 revision,最后一个在途请求仍会被误认作当前请求。
- 处理:调用方在未认证时不得启动受保护的钱包刷新;可取消的读取要为每轮分配 `AbortController`,新读取先失效并中止旧读取,组件卸载时同时推进 revision、abort 当前请求并清空句柄。所有 `then / catch / finally` 在更新状态前都要检查 signal 与 revision。
- 验证:定向测试覆盖卸载后请求 signal 已中止;同时复跑触发钱包刷新回调的画布生成集成测试和完整前端测试,不能以单文件偶然快速收束代替全量验证。
## 下游 manifest 回调测试不能冒充实时数据源(2026-08-05)
- 现象:工作台的资源、任务与版本重投影单测保持绿色,但后台 Agent 已更新 `.agent/manifest.json` 后,打开中的工作台仍长期显示旧快照,只有重开项目才更新。
- 原因:测试 Supervisor 直接调用 `onManifestChange`,只证明 `App manifest -> WorkspaceLauncher -> ProjectDevelopmentView` 的下游桥接;真实 Runtime event 没有失效字段,监听器也没有重读 manifest。External Runner 又与 GUI 分属不同进程,Runner 内无法使用 GUI `AppHandle`,只补普通 Tauri event 仍不能形成生产链路。
- 处理:后台 manifest mutation 收敛到共用 Runtime emitter;GUI 内进程用带 `manifestInvalidated` 的 Runtime update,External Runner 通过 GUI owner attach 登记的受令牌保护 loopback sink 转发专用失效事件。App 对当前项目做 single-flight manifest 重读,并以 mounted、项目路径和 scope version 丢弃迟到结果;WorkspaceLauncher 继续只消费完整 manifest 快照,不新增平行状态或轮询。
- 验证:集成测试必须渲染真实 `App + WorkspaceLauncher`、捕获真实 Tauri listener,让 `get_local_game_manifest` 从旧快照切换到新快照,并由非 Supervisor Agent 事件驱动资产、completed 任务、运行入口和版本卡出现;另测项目切换时旧请求迟到。旧的直接 `onManifestChange` 测试只能标记为下游桥接证据。
## React 资源详情焦点不能依赖重建对象身份(2026-08-05)
- 现象:音频 / 视频播放器、文档链接或收起按钮正在获得焦点时,后台 manifest 更新会把焦点突然移回详情 region;若当前资源被删除,详情虽然消失,stale focused ID 和焦点可能残留到 `body`。
- 原因:资源投影每次生成新对象,`useLayoutEffect([focusedResource])` 把同一资源的内容更新误判为重新进入详情;删除路径没有显式恢复状态和可聚焦 fallback,项目 / 运行视图切换也可能沿用旧 trigger。
- 处理:焦点状态机只比较稳定 `resourceId`:`null -> id` 与 `idA -> idB` 聚焦详情,`idA -> idA` 保持当前 active element。显式收起 / Escape 才恢复原卡片与滚动;后台删除清理 focused / matching selected ID 并聚焦搜索框;项目或运行视图切换清空 trigger / restore。媒体预览副作用依赖稳定 ID、路径和类别,不因同 ID 对象重建先卸载控件。
- 验证:媒体控件获得焦点后用同 ID 新 manifest 重渲染并断言 active element 不变;删除资源后断言详情关闭、选中清理且搜索框获得焦点;既有收起、Escape、项目切换和运行切换测试继续通过。
## 不要用自然语言精确 `.replace()` 维护 Runtime Prompt
- 现象:Prompt 文案稍作改写、增删空格或调整段落后,替换静默失效,代码中出现难以审阅的链式 `.replace()`。
@@ -4099,6 +4160,20 @@
- 处理:使用完整 JSON Schema validator 校验原始 catalog schema,不手写 required/type 子集;native parser、fingerprint enrichment 与实际 MCP 调用边界复用同一校验器。enrichment 错误必须映射回 classified `arguments-schema` repair,不能以普通字符串直接终止 run;执行点重验用于阻断升级前已经落盘的 schema 外 pending。关闭网络和文件 `$ref` 解析,schema 无法安全编译时不广告或不执行。`serde` 类型错误会包含实际字符串值,catalog miss 也会包含模型提交的 server/tool,因此这两类错误同样只能返回稳定类别,不能拼接原始错误、参数值或 schema 内容。
- 验证:覆盖 required、additionalProperties、type、enum、本地 `$defs/$ref`、HTTP/file 外部引用、无效 schema、错误脱敏,证明 legacy wrapper 在注入 fingerprint 前进入 repair,并证明带旧有效 fingerprint 的历史 pending 在实际调用前仍被 schema 拒绝。
## macOS 安全路径测试必须使用规范化临时目录(2026-08-05)
- 现象:调用仓库上下文、Runtime context bundle 或 pending recovery 的 Rust 测试在 macOS 报“Repository root and its ancestors must not be symbolic links”,Linux CI 却可能通过;本地 HTTP 恢复夹具在完整串行测试中还可能偶发 `WouldBlock`。
- 原因:`tempfile::tempdir()` 默认返回 `/var/folders/...`,而 macOS 的 `/var` 是指向 `/private/var` 的符号链接,生产安全校验会按设计拒绝该祖先;恢复测试的服务端读超时若仅为 2 秒,也会与完整测试负载下约 2 秒的首次请求形成窄竞态。
- 处理:凡测试会进入仓库可信路径校验,统一使用 `crate::tests::canonical_test_tempdir(...)`,不得削弱生产符号链接拒绝规则;loopback 夹具保留有界超时,但为完整 CI 负载留足稳定裕量。
- 验证:在 macOS 上定向运行 provider request、pending recovery、autonomous continuation 与 generation recovery 用例,再运行完整 `npm run check:native-shells`。
## Mach-O 文件头校验必须覆盖反字节序魔数(2026-08-05)
- 现象:macOS arm64 的 Tauri release 已成功构建且 `file` 明确认定为 Mach-O,产物 staging 仍报“must be an executable Mach-O file”。
- 原因:脚本用 `Buffer.readUInt32BE(0)` 读取文件头,却只比较 `0xfeedfacf` 等正序数值;arm64 常见头字节是 `cf fa ed fe`,读取结果为 `0xcffaedfe`。
- 处理:文件头白名单同时覆盖 32/64 位与 fat Mach-O 的正序和反字节序合法魔数,并由桌面配置门禁同时反查 staging 脚本和根级产物检查,不能改成只按扩展名或构建退出码判断。
- 验证:在 macOS 上构建真实 desktop-shell release,运行 `npm run desktop-shell:stage-release-binary`,再由 `npm run check:native-shells` 校验 staged 产物。
## 托管 MCP 新增公开域名时不能只更新网关路由(2026-08-05)
- 现象:`https://dev.genarrative.world/api/external/v1/mcp` 的 manifest、OpenAPI 和 Bearer 鉴权都正常,但鉴权后的 `initialize` 返回 `403 FORBIDDEN`;通过 SSH 隧道访问同一 api-server 的 loopback 地址却可以正常列出 tools/resources。
@@ -4113,3 +4188,16 @@
- 原因:把 dialog / canvas 的完整 UI 所有权同时用于账号级钱包和账号内项目级任务列表,或者任务列表只比较 project ID,没有校验账号。
- 处理:按副作用分层校验。钱包只比较账号;任务列表比较账号加项目;dialog、canvas、asset 和 layer 写回继续比较账号、项目、scope version 与原 dialog。正式请求已接受后,删除 UI 状态不等于取消后端任务。
- 验证:分别覆盖删除 dialog、同账号切项目、账号 A 切到账号 B 且 project ID 保持相同,以及原账号原项目原 dialog 仍有效的正常回写。
## GUI owner 锁不能替代逐 boot 的事件接收端登记(2026-08-05)
- 现象:GUI 首次启动后 manifest 事件转发正常,但 Runner 被替换为新 boot 后只剩 owner 锁和 endpoint 可用,后台更新不再到达 GUI;或者 attach 响应只确认 owner,客户端却误记当前 boot 已完整登记,后续 ensure 不再重试。
- 原因:把 OS owner 生命周期约束与进程内事件 sink attachment 混成同一状态,或在 `ensure_external_agent_runner` 之外执行一次性 attach;测试若用 actionId 等无关字段代替真实 sink port/token,也无法证明新 boot 重放的是可用接收端。
- 处理:GUI 按规范化 AppData 私有登记真实 sink port/token,`ensure_external_agent_runner` 的 endpoint 复用和新 Runner 就绪两条成功路径都按 `bootId` 重放。同 boot 成功后幂等,新 boot 必须重挂;RPC、`attached` 或 `eventSinkAttached` 任一失败或缺失都不得记录成功 boot,并允许同 boot 后续重试。不同 AppData 不共享登记,未登记 CLI 不触发 attach;sink token 不进入日志、错误或公共状态。
- 验证:分别覆盖真实 port/token 跨 boot 原样重放、同 boot 幂等、新 boot 重挂、普通 attach 失败、`eventSinkAttached` 缺失与 false 后同 boot 重试、AppData 隔离和未登记 CLI 零副作用。
## manifest relay 测试不能并行覆盖同一个全局 sink(2026-08-05)
- 现象:crate 根 relay 测试在配置全局 sink 后阻塞等待 `TcpListener::accept()`,同时 Runner GUI owner attach 测试通过另一条路径覆盖并清空 sink;事件可能被发往另一端口,原 listener 随后永久等待。断言或 `expect` 提前失败时,成功路径末尾的手动 clear 也不会执行。
- 原因:两个跨模块测试读写同一进程全局状态,却没有共用隔离边界;只给 accept 后取得的 stream 设置 read timeout 无法约束 accept 本身,payload 读取也缺少总 deadline。
- 处理:全部全局 sink 测试共用一把 test-only 串行锁,并由 RAII guard 在 `Drop` 中无条件清空;测试统一使用 `manifest_invalidation_sink_isolation_` 前缀。relay fixture 对 accept 和 payload 分别使用非阻塞轮询与总 deadline,不使用固定 sleep;生产 loopback、token、连接 / 写入超时和 payload 大小校验保持不变。
- 验证:用 `--test-threads=2` 重复运行统一 filter,覆盖正常 relay、无事件 accept 超时、不完整 payload 超时、panic 展开清理,以及 GUI owner attach 配置与 guard 清理。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -524,7 +524,7 @@ GameBridge 禁止:
2026-06-19 追加:桌面壳 macOS 媒体权限说明进入门禁。Tauri 桌面壳仍不新增摄像头或麦克风 HostBridge method,不把系统媒体能力暴露成桌面命令;同源 H5 页面可继续使用浏览器标准 `getUserMedia` 承接儿童动作热身 Demo 的实时摄像头输入和汪汪声浪正式 runtime 的实时麦克风输入。macOS 分发包必须通过 `bundle.macOS.infoPlist="Info.plist"` 合并受控用途说明:`NSCameraUsageDescription` 只描述同源 H5 实时动作输入,`NSMicrophoneUsageDescription` 只描述同源 H5 实时声音玩法。`apps/desktop-shell/scripts/check-config.mjs` 会校验 plist 路径和两条文案,并把 `Info.plist` 纳入生产壳替身词扫描,防止桌面包缺少系统授权说明、把媒体权限扩写成通用采集能力,或在 macOS 分发配置里留下临时替身文本。
2026-06-18 追加:桌面壳 release 构建烟测进入统一验收。`npm run check:native-shells` 会在 H5 HostBridge、Expo 壳和 Tauri 单测通过后执行 `npm run desktop-shell:build -- --no-bundle`,确认 Tauri release 入口指向共享公开主站、受控命令白名单、图标和 Rust release 编译可以共同产出桌面二进制;构建后 `desktop-shell:stage-release-binary` 会把当前平台二进制复制到根目录 `build/native/desktop/genarrative-desktop-shell` 或 `build/native/desktop/genarrative-desktop-shell.exe`,该目录沿用根 `build/` 的 gitignore,只作为本机或 CI 可收集产物目录。统一验收必须检查 staged 二进制存在、非空且符合当前平台可执行文件头。`apps/desktop-shell/scripts/check-config.mjs` 会反查根级门禁仍保留 release build smoke、staging 步骤、二进制路径、Linux ELF / macOS Mach-O / Windows PE 文件头和可执行位检查,避免桌面产物验收被改成只看命令退出码。该烟测不生成平台安装包,避免把 Linux 本机缺少的系统打包器误判为 HostBridge 回归。
2026-06-18 追加:桌面壳 release 构建烟测进入统一验收。`npm run check:native-shells` 会在 H5 HostBridge、Expo 壳和 Tauri 单测通过后执行 `npm run desktop-shell:build -- --no-bundle`,确认 Tauri release 入口指向共享公开主站、受控命令白名单、图标和 Rust release 编译可以共同产出桌面二进制;构建后 `desktop-shell:stage-release-binary` 会把当前平台二进制复制到根目录 `build/native/desktop/genarrative-desktop-shell` 或 `build/native/desktop/genarrative-desktop-shell.exe`,该目录沿用根 `build/` 的 gitignore,只作为本机或 CI 可收集产物目录。统一验收必须检查 staged 二进制存在、非空且符合当前平台可执行文件头;macOS 校验同时接受 32/64 位与 fat Mach-O 的大端、反字节序合法魔数,不得把 arm64 常见的 `cf fa ed fe` 文件头误拒绝。`apps/desktop-shell/scripts/check-config.mjs` 会反查根级门禁仍保留 release build smoke、staging 步骤、二进制路径、Linux ELF / macOS Mach-O / Windows PE 文件头和可执行位检查,避免桌面产物验收被改成只看命令退出码。该烟测不生成平台安装包,避免把 Linux 本机缺少的系统打包器误判为 HostBridge 回归。
2026-06-18 追加:移动壳 Expo managed config 烟测进入统一验收。`npm run check:native-shells` 会执行 `npm run mobile-shell:config`,在 `apps/mobile-shell` 目录内调用 `expo config --type public --json`,校验 Expo CLI 实际解析结果中的包名、scheme、深链、ATS / cleartext / backup / 相机与麦克风权限、启动页、adaptive icon、插件配置和 HostBridge 版本没有漂移。`apps/mobile-shell/scripts/check-config.mjs` 会反查根级门禁仍保留 EAS build profile、Expo config 和 Metro export 三个移动分发烟测,避免移动壳验收退回到只看源码类型检查。
@@ -2,7 +2,7 @@
- 日期:2026-06-11
- 关联故障:release 环境 api-server 周期性全量 `spacetime_stage="pool_acquire" elapsed_ms=45000` 超时,`/readyz` 503(`reason=spacetime_unhealthy, stage=pool_acquire`),重启后临时恢复。
- 涉及代码:`server-rs/crates/spacetime-client/src/lib.rs`
- 涉及代码:`server-rs/crates/spacetime-client/src/active.rs`
## 故障根因
@@ -28,6 +28,8 @@
3. acquire 改为 CAS 抢占槽位:持有 permit 即保证并发持有者不超过 `pool_size`,扫描一轮必然命中空闲槽位,彻底删除自旋循环;建连失败直接返回错误,槽位由租约 Drop 复位。
4. `release_connection` 退化为 `drop(lease)`,显式与隐式归还共用同一条兜底路径。
这里的“取消安全”只指本地连接租约、槽位与 permit 可回收。RPC 已发出后,handler timeout/drop 不会取消或回滚远端 procedure,结果必须按 unknown 读取权威事实对账;`dropped_lease_releases_slot_and_permit` 只覆盖本地 Drop,不构成远端取消测试。
## 验收
- `cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`(44 通过,含上述连接池与缓存连接测试)
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@
允许撤销的典型操作包括移动图片、移动生成结果、调整层级、组合与取消组合、删除或剪切图片、隐藏图片、锁定与解锁、翻转、修改素材类型和调整画布视图。删除、剪切和隐藏允许撤销,是因为目标只会让内容重新出现。
添加素材、上传到画布、粘贴、创建副本、生成图片、扩图新增结果、显示隐藏图片、替换图片以及其它会让当前结果消失的撤销必须被安全检查阻止。`Ctrl+C`、选择变化、滚轮或抓手视口移动、导出下载、项目重命名、素材库后端删除和生成任务副作用不进入画布历史;素材库后端删除发生后,同时剪除撤销栈和恢复栈中所有包含关联图层的目标快照,不能让更早的画布历史复活已删除素材。
添加素材、上传到画布、粘贴、创建副本、生成图片、扩图新增结果、完美像素新增结果、显示隐藏图片、替换图片以及其它会让当前结果消失的撤销必须被安全检查阻止。`Ctrl+C`、选择变化、滚轮或抓手视口移动、导出下载、项目重命名、素材库后端删除和生成任务副作用不进入画布历史;素材库后端删除发生后,同时剪除撤销栈和恢复栈中所有包含关联图层的目标快照,不能让更早的画布历史复活已删除素材。
恢复同样执行动态安全检查。移动、层级、分组、锁定、翻转和视图等不会减少内容的操作可以恢复;重新执行删除、剪切、隐藏、删除生成结果或替换当前素材时必须被阻止。
@@ -24,6 +24,7 @@
- 撤销栈和恢复栈均保存操作类型、目标 `CanvasHistorySnapshot` 和创建时间,分别最多保留 60 条。
- 新画布操作把操作前快照写入撤销栈并清空恢复栈;成功撤销把当前快照写入恢复栈,成功恢复把当前快照写回撤销栈。
- 操作类型用于生成用户提示,并对添加、上传、生成和替换等明确会移除当前结果的撤销做保护;其它操作能否应用由当前快照与目标快照的差异检查决定。
- 已有图片完美像素化成功落入画布前记录独立 `perfect-pixel` 历史类型,中文提示使用“完美像素”。该类型与 `generate-image`、`expand-image`、`remove-background` 一样属于新增结果保护操作:撤销不得删除派生 PNG,源图继续保留也不改变这一保护语义。像素处理失败、超时或不适用时不写历史。
- 安全检查以稳定的 `layer.id` 判断当前图层是否仍存在;内容身份优先比较对象存储 key、对象标识和媒体地址,序列帧结果比较完整帧列表。`resourceId`、`sourceResourceId`、`sourceAssetId` 等内部关联 ID 的延迟回填不视为图片替换。
- 恢复历史快照时,相同 ID 的图层以当前对象为权威,只从目标快照覆盖 `x`、`y`、`zIndex`、`groupId`、`assetKind`、`hidden`、`locked`、`flipX`、`flipY`。当前图层的资源关联、内容、媒体、生成元数据、`width` / `height` / `originalWidth` / `originalHeight` 和标题必须保留,不能被异步回填前的旧快照覆盖。
- 当前生成对话框和非活动生成结果按稳定 ID 纳入内容存在性检查,避免恢复操作删除当前生成结果。相同 ID 的生成对话框只从目标快照恢复占位框 `x` / `y` 以及 active / inactive 槽位对应的 `composerOpen`,当前占位框的 `width` / `height` / `originalWidth` / `originalHeight`、当前比例与清晰度等参数、`generating` / `failed` / 完成态、提示词、参考图和任务结果继续以当前状态为准,不能被旧历史快照降级。
@@ -34,6 +35,7 @@
- 修改素材类型的撤销与恢复仍以当前图层内容为权威,但每次成功恢复类型后都要创建与恢复类型一致的正式项目 resource,再回填新 `resourceId`。同一图层的 resource 创建请求使用单调版本号,迟到的旧类型响应不得覆盖更晚的 undo / redo 结果。
- 鼠标拖动在按下时暂存操作前快照;屏幕位移达到点击阈值后才开始改变画布坐标并只提交一条历史,阈值内的指针抖动和单击都不产生位移或历史记录。
- 本地即时结果与后端项目快照结果都必须在生成图层加入画布前写入一条生成历史;生成完成后的自动适合视图不再额外压入视口历史,保证用户第一次撤销就命中生成保护。
- 完美像素的关闭 composer 占位沿用 generation dialog 的内容存在性保护。占位删除已先持久化时,后端 completion 不得用旧 placeholder 复活占位或派生图层;回包时本地占位已删除则前端不应用完成快照或写 `perfect-pixel` 历史,已经持久化的 project resource / 账号素材仍可由资源或素材入口读取。现有布局 CAS 没有 deletion tombstone,completion 先提交、删除保存后冲突的极端竞态仍按权威快照收口。
- 顶部消息复用 `PlatformRuntimeStatusToast`,成功使用中性色,被阻止使用警告色;连续触发会替换消息并重新开始 3 秒计时。
## 验收重点
@@ -55,3 +57,5 @@
15. 图层移动后从素材库删除关联素材,随后撤销或恢复都不得把已删除图层重新加入画布;普通画布删除未伴随素材库删除时仍可撤销。
16. 打开“修改图片”并输入未提交提示词后,从按钮等非输入控件触发撤销不得关闭弹窗或回退当前草稿。
17. 修改素材类型后立即撤销或快速撤销再恢复,最终只允许最新类型的 resource 响应回填;刷新项目后类型与最后一次成功历史操作一致。
18. 完美像素结果加入画布后只写一条 `perfect-pixel` 历史;第一次撤销命中保护提示,源图和派生 PNG 都不消失。
19. 完美像素处理中删除占位后,本次完成回包不得在本地重新应用结果或写 `perfect-pixel` 历史;删除已先持久化时,后端不得复活占位或结果图层;已成功持久化的资源或账号素材允许保留。
@@ -1,6 +1,6 @@
# 本地开发验证与生产运维
更新时间:`2026-08-03`
更新时间:`2026-08-05`
## 标准开发流程
@@ -230,7 +230,7 @@ npm run check
- `Repository checks`:执行 `npm run lint`、主站与后台生产构建、内容数据检查和提交差异空白检查。
- `Frontend tests`:按根 lockfile 与 `apps/ai-game-creator-shell/package-lock.json` 分别执行干净的 `npm ci`,再独立执行根 `npm run test`、`npm run bgfilter-worker:smoke-test`、`npm run check:production-health-patrol`、`npm run check:production-api-release` 和 `npm run check:production-api-deploy`,让 Vitest、Node test smoke harness 及不依赖真实服务的生产巡检 / 发布 / 部署行为 fixture 在 Gitea job 中持续执行;其中 `.test.mjs` 使用 Node test runner,不依赖 Vitest 的 `scripts/**/*.test.ts` 收集规则。
- `Backend tests`:执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast`、`api-server --all-targets` 编译和 `spacetime-module` 编译;runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。
- `Backend tests`:先对 `server-rs/Cargo.lock` 执行带 5 次整命令级有界重试的 `cargo fetch --locked`,再执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast`、`api-server --all-targets` 编译和 `spacetime-module` 编译;依赖准备必须位于会触发 Cargo build 的 DDD / 产物边界门禁之前,避免锁新增依赖未命中镜像缓存时绕过既有下载重试。runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。
- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke;共享 Agent Runtime 后台锁 suite 固定 `--test-threads=1`,不能用并行偶发失败后的逐项通过替代整套稳定门禁。
四个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 BgFilter worker smoke harness、无密钥生产巡检 / 发布 / 部署行为 fixture、server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。
@@ -607,7 +607,7 @@ Nginx 与 Pingora 在维护 marker 存在时对内网来源绕过整站维护闸
该规则只绕过网关维护响应,不会自动拉起 api-server、SpacetimeDB 或其它已停止的服务。人工执行 `maintenance-on.sh` 且后端仍运行时,可以从内网继续访问整站和修改后台数据;`pause-after-stdb` 会停止旧 API/controller/worker,在 API 被停期间静态页面可能仍可加载,但普通 API 与 `/admin/api/**` 仍不可用。验证使用 `npm run check:nginx-spa-routes`、`npm run check:pingora-route-parity`、`cargo test -p pingora-gateway --manifest-path server-rs/Cargo.toml`、`npm run check:pingora-gateway-smoke` 和 `npm run check:production-ops`,不要在 live 机器上为测试临时创建维护 marker。
版本化默认维护页固定为 `public/maintenance.html`,使用 `public/branding/taonier-maintenance-page.png` 作为品牌视觉,只允许保存无日期、无具体时段的通用文案;正常 Web 构建由 Vite 复制到发布包根目录的 `web/maintenance.html`,并由 `check-maintenance-page.mjs` 在打包前拒绝“今天 / 今晚”、具体日期或 `HH:MM` 等临时公告。计划内停服的临时公告必须放在 release 外文件中,通过 `/opt/genarrative/current/scripts/deploy/maintenance-on.sh --page-file <公告HTML> <维护原因>` 原子安装到 `/var/lib/genarrative/maintenance/page.html`。Nginx 与 Pingora 在该文件存在时优先返回它,缺失时回退当前 Web 制品的默认维护页;同一维护窗口内 Stdb / API 的后续 `maintenance-on.sh` 调用保留已安装公告,`maintenance-off.sh` 同时删除 marker 和运行态公告,避免下次维护复活旧内容。公告启用后同时用 `genarrative.world` 与 `www.genarrative.world` 的真实 HTTPS 响应校验 `503` 和公告正文。
版本化默认维护页固定为 `public/maintenance.html`,使用 `public/branding/taonier-maintenance-page.png` 作为品牌视觉,只允许保存无日期、无具体时段的通用文案;正常 Web 构建由 Vite 复制到发布包根目录的 `web/maintenance.html`,并由 `check-maintenance-page.mjs` 在打包前拒绝“今天 / 今晚”、具体日期或 `HH:MM` 等临时公告。计划内停服的临时公告必须放在 release 外文件中,通过 `/opt/genarrative/current/scripts/deploy/maintenance-on.sh --page-file <公告HTML> <维护原因>` 原子安装到 `/var/lib/genarrative/maintenance/page.html`;公告页和 marker 都使用同目录临时文件加 POSIX 兼容 `mv -f` 的原子替换,不得依赖 GNU `mv -T`,确保 Linux 生产机与 macOS/BSD 本地门禁语义一致。Nginx 与 Pingora 在该文件存在时优先返回它,缺失时回退当前 Web 制品的默认维护页;同一维护窗口内 Stdb / API 的后续 `maintenance-on.sh` 调用保留已安装公告,`maintenance-off.sh` 同时删除 marker 和运行态公告,避免下次维护复活旧内容。公告启用后同时用 `genarrative.world` 与 `www.genarrative.world` 的真实 HTTPS 响应校验 `503` 和公告正文。
生产 Jenkins 的 `Pipeline script from SCM` 由 Jenkins controller 读取 Jenkinsfile。所有生产 Job 的 SCM URL,以及 Jenkinsfile 内部在 Jenkins Built-In Node 执行的源码准备,统一使用 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`,并显式传入 Jenkins SSH 凭据 `genarrative-local-gitea-ssh`;不再配置局域网 IP、`https://git.genarrative.world/...` 公网 fallback 或 `https://git.genarrative.world/git/GenarrativeAI/Genarrative.git`。所有 `GitSCM checkout` 都必须保留单分支 refspec、`shallow=true`、`depth=1`、`noTags=true` 与 `honorRefspec=true`。API / Web / Stdb 发布类流水线不在目标机器 checkout Git,统一执行上游构建归档里的部署脚本;Server-Provision 和数据库导入导出也由带 `linux && genarrative-build` 标签的 Jenkins Built-In Node 先 checkout 并 stash 所需脚本,再交给目标 dev / release agent,避免目标机把 `127.0.0.1` 误解为远端 Gitea 或让产物 commit 与执行脚本漂移。
@@ -898,3 +898,7 @@ node scripts/rebind-orphan-work-owners.mjs --in <exported-migration.json> --out
- `--out`:写回后的迁移 JSON 输出路径。
- `--dry-run`:只统计回填行数,不写文件。
- `--placeholder-user-id`:需要时可覆盖默认占位账号 ID。
## 维护页目标文件安全边界(2026-08-05)
`scripts/deploy/maintenance-on.sh` 只允许把同目录临时普通文件原子替换到普通文件或尚不存在的 `page.html` / `enabled` 目标。目标只要是符号链接(包括指向目录的链接)或目录,脚本必须在替换前失败,不能跟随链接把临时文件移入链接目标,也不能打印“已进入维护模式”。`page_temp` 与 `marker_temp` 必须在 `set -u` 下安全初始化,清理 trap 必须在首次 `mktemp` 前生效;任一失败退出都不得在目标同级遗留 `page.html.tmp.*` 或 `enabled.tmp.*`,成功替换后则清空临时路径并解除 trap,不能误删已安装目标。跨平台实现继续使用 POSIX `mv -f`,安全语义由替换函数的目标类型门禁保证;修改后运行 `bash -n scripts/deploy/maintenance-on.sh` 与 `npm run check:maintenance-page`。
@@ -69,6 +69,21 @@ worker 完成生成任务时,本次先用读取时 revision 调用 CAS 保存
重复 completion 必须返回同一资源、layer 和 dialog 终态,不得重复插入,也不能因 dialog 暂时缺失而返回 `changed=false` 后仍把任务标记完成。任一步失败时整笔业务写回回滚,任务保留可诊断的失败或可重试状态。
### 3.6 免费同步栅格派生完成
`POST /api/editor/images/pixel-art-snaps` 的完美像素化不是 external job completion:它免费、在当前 HTTP 请求内 inline 执行,不创建任务行,也没有 `job_id / worker_id / lease_token`。前端仍须先创建关闭 composer 的右侧 generation dialog,再解析或上传源图以取得稳定引用,随后 flush 包含该占位的当前布局,最后把稳定源媒体引用和带非空 `dialogId` 的 `canvasCompletion` 一次提交;结构化 / legacy canvas 的完成分流继续由后端决定,前端不能直接写表或本地补造正式 layer。
端点级并发排队、像素读取、静态 PNG / JPEG / WebP 编码门禁、解码、输入限制、legacy 网格步长估算、CPU 并发排队、规整和 PNG 编码全部发生在持久化前。两层排队的位置不同:端点级闸在首次 IO 之前,因此队列满的 `503` 早于任何 SpacetimeDB 读取和 OSS 下载返回;CPU 排队仍在下载之后、规整之前,等待超预算返回 `504`。两者都在持久化前失败,零写入结论不变。strict 与生成风格使用同一 profile、峰值估算、单轴步长补全、walker、采样和编码;仅在横纵两轴都未检测到步长、legacy 即将进入统一网格兜底时拒绝,任一轴已检测到步长时行为和输出完全一致。任一步失败、超时或不适用时不执行最终 OSS PUT,不创建 asset object、`editor_project_resource`、`editor_asset` 或结果 layer;不得保存原图副本、逻辑低分辨率图、诊断图或前后对比图冒充结果。处理成功时只 PUT 一张最终 PNG,并至多各创建一个 project resource 和一个账号素材;源图已有正式 project resource 时,结果资源的 `source_resource_id` 指向该资源。
该零写入保证只覆盖首个最终 PNG PUT 前的可预判与处理阶段。进入持久化后,PNG / asset object、project resource、账号素材与 canvas completion 仍跨 OSS 和多个 SpacetimeDB procedure,沿用既有非事务顺序;后段失败可以保留此前已经确认的对象或记录,不做自动删除补偿,也不由客户端重放请求。调用方应按 `task_id / object_key / resource_id` 重新读取权威项目和素材快照后显式收口。
同步 completion 写画布前必须读取当前 revision 和 dialog,而不能信任请求中提交时的旧 placeholder:
1. dialog 仍存在时,在同一当前布局上完成一个结果 layer,保留源图,并关联结果 resource;
2. dialog 的删除已先持久化时,跳过 layer / dialog 写回,不重建占位、不复活派生图层;已成功持久化的 project resource / 账号素材允许保留;
3. CAS 冲突时不得拿新 revision 原样重放旧整包;按当前项目保存冲突规则重新读取权威快照并显式收口;
4. 客户端回包时若本地 dialog 已删除,不应用完成快照或写历史;现有布局 CAS 没有 deletion tombstone,completion 先提交、删除保存后冲突的极端竞态仍按权威快照收口。客户端不得为该 unsafe POST 配置 `EDITOR_REQUEST_RETRY_OPTIONS`。请求字节可能已发出后的 transport 异常或 `408 / 425 / 429 / 502 / 503 / 504` 不自动重放,先通过 GET 核对项目 / 素材快照,再由用户显式决定是否再次执行;Bearer 中间件在 handler 前拒绝请求后的既有认证恢复继续保留。
## 4. 存量迁移
迁移按 canvas 执行 `backfill → hash 核对 → activate`,并保持幂等:
@@ -110,5 +125,6 @@ SpacetimeDB 必须先于依赖新 procedure / bindings 的 API 发布;前端
- structured 模式下 typed 列而非扩展 JSON 决定几何、层级、分组、显示 / 锁定、资源引用、`asset_kind_override` 和 dialog 状态;标签展示和类型能力判断统一按 `override ?? resource default`。修改当前图层标签与清除覆盖都保持 `resource_id` 和资源行数量不变;复制共享同一资源并复制 override,随后各副本可独立修改 override。两个客户端基于同一 revision 写入时只允许一个成功,冲突方重载后端最新快照,不换上新 revision 原样重放旧整包。细粒度 batch mutation 是取消 2 MiB 兼容入口的后续项,不冒充为本次已完成。
- worker completion 当前以读取时 revision 做 CAS,冲突时拒绝覆盖;V2 保存和保存后快照在同一 procedure 结果内返回,避免“已提交但后续 GET 失败”的不确定结果。lease-fenced 资源 / layer / dialog / job 单事务 completion 仍是后续收口项。
- structured 快照刷新后,上传参考图、生成结果、占位与 dialog 状态均可恢复;资源存在但布局写入失败时不会伪装为保存成功。
- 完美像素处理失败 / 超时时 OSS、resource、asset 和 layer 均无新增;成功时只有一个最终 PNG、至多一个 project resource 和一个账号素材。处理中占位删除已先持久化时,完成请求不复活 dialog 或结果 layer;回包时本地占位已删除则不应用完成快照,已成功创建的资源 / 素材仍可读取;传输结果未知时客户端不自动重放 unsafe POST。
- 回滚重组结果经 schema 校验、canonical hash / 资源引用核对且不超过 2 MiB;超限或不一致时明确拒绝且 structured 快照仍可读取。
- 完成 `npm run spacetime:generate`,确认 Rust 表字段、migration、生成 bindings、HTTP DTO 与前端 `assetKindOverride` 形状一致;再运行 `npm run check:spacetime-runtime-access`、`npm run check:spacetime-schema`、相关 Rust / API / 前端定向测试、`npm run check:encoding` 和 `git diff --check`。
@@ -46,7 +46,7 @@
- `model`:支持 `gemini-3.1-flash-image-preview`(UI 显示 `nanobanana2`)和 `gpt-image-2`,默认 `nanobanana2`。
- `aspectRatio`:按 `x:y` 展示,选项跟随模型。
- `imageSize`:按 `0.5K / 1K / 2K` 展示,选项跟随模型。
- `style`:可选生成后处理风格;未勾选像素艺术时传 `"none"`,勾选时传 `"pixelArt"`。
- `style`:可选生成风格,同时影响提交给 provider 的提示词和回图后的像素规整;未勾选像素艺术时传 `"none"`,勾选时传 `"pixelArt"`。
- `priceMudPoints`:按当前模型和尺寸从编辑器生成计费配置计算;`nanobanana2 1K` 为 `12`,`gpt-image-2 1K` 为 `3`、`gpt-image-2 2K` 为 `5`。前端只提交配置函数计算值,后端用 `editor_generation_config` 校验,不允许素材生成面板自行写死价格。
- 模型与尺寸选项:
- `nanobanana2`:比例 `1:1 / 4:3 / 3:2 / 2:3 / 9:16 / 16:9`;大小 `0.5K / 1K / 2K`。后端走 `/v1beta/models/{model}:generateContent`,把图标规范图作为 `inline_data`,并把 `aspectRatio` / `imageSize` 写入 `generationConfig.imageConfig`;`0.5K` 按 VectorEngine 文档传 `"512"`。
@@ -65,6 +65,7 @@
- 图标素材面板增加紧凑的 `像素艺术` 勾选项。选择保存于现有生成器快照,并可随现有请求和队列 payload 传递;不写入用户可见 `generationInputs`、素材元数据或新建的持久化记录。
- `style` 省略、为 `null`、空字符串或 `"none"` 时按内部 `None` 处理且不告警;`"pixelArt"` 启用像素规整。未知字符串按 `None` 继续生成,并通过既有通用 `warning` 返回 `unsupported-image-style`;非字符串 JSON 仍返回 `400`。
- 2026-08-01 修订:`"pixelArt"` 不再只是后处理,同时向提交给 provider 的提示词末尾追加独立一行约束。图标链路使用「每个图标素材均为像素风格」,**不得**使用「画面为像素风格」——图集生成后要按纯色抠像,绿幕底必须保持平整,画面级像素化要求会与同一段提示词里的「纯色背景必须平整无纹理、无渐变」互相拆台;一张图内是多个彼此分离的素材,需要逐个点名,避免模型只把其中一部分做成像素块。注入发生在 `build_editor_icon_spritesheet_prompt` 返回之后,该函数签名和输出契约不变。约束句只随工程化提示词写入**原图 spritesheet** 的 `editor_project_resource` prompt 列;透明结果的 prompt 列是 `"去除纯色背景"`,自动拆分的切片是 `"自动拆分图集"`,两者都不含约束句。与普通图片和角色形象不同,本链路**响应体**的 `prompt` 字段返回的也是含约束句的工程化提示词,而不是用户输入——图标请求本身没有 `prompt` 字段(收的是 `iconDescriptions`),因此调用方(含外部 API v1)能直接看到绿幕子句、间距要求和本次新增的像素约束。以上 prompt 列写入与响应字段规则都是既有行为,与 `web/master` 一致,本次只是让被回传的模板多了一行。不新增 OSS PUT、项目资源、图集画布项或切片画布项。尚未约束各素材共用同一像素块大小(`estimate_step_size` 取全图相邻峰间距的第 30 百分位,块大小不一时步长估计会偏),等实测。
- 图标链路以已持久化的带纯色背景 provider 原图实际尺寸为基准;BgFilter 正常成功后,把 Alpha 蒙版回贴到该同尺寸平底原图,再执行像素规整。网格分析源使用平底 provider 原图,RGBA 采样源使用 Alpha 已回贴的透明图;规整结果不再经过独立的最终尺寸处理,直接上传透明 spritesheet,成功后才进入原有连通域自动拆分。
- 首版固定参数为分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、固定色板关闭、K-means 最大采样 `262144`。单格颜色按 `Σ(A × RGB) / ΣA` 进行 Alpha 加权;覆盖率 `Σ(A / 255) / N >= 0.375` 且 `ΣA > 0` 时输出硬 Alpha `255`,否则输出严格 `[0,0,0,0]`。分析色数不限制最终输出色数。
- 像素规整 CPU 工作使用进程级最大并发 `2`;取得并发许可的排队时间与实际处理时间共享最多 `30` 秒预算,同时不得晚于当前请求 deadline,最终以两者中更早者为准。输入图片任一边不得超过 `10000` 像素,总像素不得超过 `8294400`;超限、排队超时或处理超时均保留 Alpha 已回贴的透明图并走非致命降级,随后仍可进入原有自动拆分。
@@ -93,7 +94,7 @@
- 默认提示文本会完整进入 prompt;用户输入不再被解析为素材数量。例如“各种敌人头像:骷髅 哥布林 强盗 龙 蝙蝠等”只是一段完整需求,不代表必须生成或拆出 `6` 个素材。
- 默认打开图标素材面板时选中 `nanobanana2 / 1:1 / 1K`;模型切换后,角色和图标素材面板之间沿用上次选择的模型。
- 图标素材生成请求必须带 `model`、`aspectRatio` 和 `imageSize`;`nanobanana2` 请求体必须包含 `generationConfig.imageConfig.aspectRatio/imageSize`,`gpt-image-2` 请求必须包含文档映射后的 `size`。
- 图标素材面板可选择 `style: "none" | "pixelArt"`;`none` 完整保持原处理路径,`pixelArt` 在 Alpha 回贴后、自动拆分前执行内存像素规整,最终 OSS PUT、项目资源、图集画布项和切片画布项数量不得因此增加。
- 图标素材面板可选择 `style: "none" | "pixelArt"`;`none` 完整保持原处理路径,且提交给 provider 的提示词与未带该字段时逐字一致,`pixelArt` 在提示词末尾追加「每个图标素材均为像素风格」并在 Alpha 回贴后、自动拆分前执行内存像素规整,最终 OSS PUT、项目资源、图集画布项和切片画布项数量不得因此增加。
- 图标素材生成可以上传普通参考图;提交时图标规范图仍走 `referenceImageSrc`,普通参考图走 `referenceImageSrcs`,二者都必须是稳定引用(`objectKey` / 项目资源 ID / 素材 ID),禁止 Data URL / Blob URL,并写入 `generationInputs.references`。
- 透明背景处理和自动拆分都成功后,画布同时出现透明 spritesheet 主图、其右侧的 provider 原图,以及从原图右侧铺开的全部有效连通域图标图层,图标依次命名为 `素材 N`;透明图集成功但拆分失败时仍出现透明主图与右侧原图,透明背景处理最终失败时只出现 provider 原图。
- 选中透明图集图层时显示 `拆分图集`;点击后源图集显示扫描蒙层与 `拆图中` 状态,工具栏按钮同步切换为旋转图标和 `拆图中` 并禁用重复提交。完成后恢复工具栏,不新增第二张图集,只在 provider 原图右侧追加自动识别的独立素材,并同步写入素材库。
@@ -65,6 +65,7 @@
- 角色面板增加紧凑的 `像素艺术` 勾选项,请求使用可选字符串字段 `style`:未勾选传 `"none"`,勾选传 `"pixelArt"`。该选择可以随现有生成器快照和队列 payload 保存,但不写入用户可见 `generationInputs`、素材元数据或新建的持久化记录。
- `style` 省略、为 `null`、空字符串或 `"none"` 时按内部 `None` 处理且不告警;`"pixelArt"` 在 `kind="character"` 时启用像素规整。未知字符串按 `None` 继续生成,并通过既有通用 `warning` 返回 `unsupported-image-style`;非字符串 JSON 仍返回 `400`。同一图片生成请求 DTO 被其它 `kind` 复用时,只有普通图片和 `character` 支持 `"pixelArt"`,其它 `kind` 收到该值也按不支持风格降级。
- 2026-08-01 修订:`"pixelArt"` 不再只是后处理,同时向提交给 provider 的提示词末尾追加独立一行约束。角色链路使用「角色主体为像素风格」,**不得**使用「画面为像素风格」——角色生成后要按纯色抠像,绿幕底必须保持平整,同一段提示词里已写死「纯色背景必须平整无纹理、无渐变」,画面级像素化要求会与之互相拆台;且该提示词已禁止出现角色以外的场景内容,因此只需点名角色本身。注入发生在 `build_editor_character_image_prompt` 返回之后,该函数签名和输出契约不变。约束句**不会**进入角色链路的任何 `editor_project_resource`:原图 resource 的 prompt 列存的是 `role_setting`(用户原文),透明结果的 `output_prompt` 在抠图成功后被无条件覆盖为 `"去除纯色背景"`;完整提交提示词是否留存取决于 provider:`persist_editor_provider_source_image` 写原图 asset object 元数据时用的是 `actual_prompt.unwrap_or(prompt)`,provider 未回 `actualPrompt` 时才存 `submitted_prompt`(含约束句),此时排障可按 `object_key` 查;provider 回了 `actualPrompt` 就存 provider 改写后的文本,该次生成的 `submitted_prompt` 在系统内一处都不落——外部 API 审计的 `request_payload` 只记 `promptChars` 字符数,没有提示词原文。响应体返回的是用户原文,前端显示不变。以上 prompt 列写入与 asset object 元数据规则都是既有行为,与 `web/master` 逐行一致,本次未改动。角色提示词里既有的「严格基于图1的角色美术视觉规范的美术风格」与像素约束存在潜在冲突,本次未改写,等实测。
- 角色 provider 回图先按统一业务像素矩阵执行交付尺寸归一:允许无放大恢复时使用 Lanczos 重采样并居中裁切,无法安全恢复时保留 provider 实际尺寸并返回非阻断告警。归一后的带纯色背景图先持久化并作为 BgFilter 输入;BgFilter 正常成功后,把 Alpha 蒙版回贴到这张同尺寸平底原图,再执行像素规整并上传透明主图。网格分析源使用已收口到实际交付尺寸的平底原图,RGBA 采样源使用 Alpha 已回贴的透明图;软 Alpha 只参与单格覆盖率和 Alpha 加权 RGB 计算,输出 Alpha 硬化为 `0 / 255`。
- 首版参数固定为分析色数 `16`、Alpha 覆盖阈值 `0.375`、像素格尺寸自动检测、固定色板关闭、K-means 最大采样 `262144`。单格覆盖率 `Σ(A / 255) / N >= 0.375` 且 `ΣA > 0` 时输出 `A=255`,颜色按 `Σ(A × RGB) / ΣA` 计算;否则输出 `[0,0,0,0]`。分析色数不限制最终输出色数。
- 像素规整 CPU 工作使用进程级最大并发 `2`;取得并发许可的排队时间与实际处理时间共享最多 `30` 秒预算,同时不得晚于当前请求 deadline,最终以两者中更早者为准。输入图片任一边不得超过 `10000` 像素,总像素不得超过 `8294400`;超限、排队超时或处理超时均保留 Alpha 已回贴的透明图并走非致命降级。
@@ -118,7 +119,7 @@
- `从画布中选择` 后点击已有画布图片可绑定为角色规范,`Esc` 可退出点选状态。
- 上传常规参考图后缩略图右下角显示序号。
- 输入角色设定并生成时,请求包含 `kind: "character"`、角色设定 prompt、参考图数组、`model`、`screenColor`、`aspectRatio` 和 `imageSize`。
- 角色面板可选择 `style: "none" | "pixelArt"`;`none` 的处理路径和产物保持不变,`pixelArt` 在 Alpha 回贴后执行内存像素规整,最终 OSS PUT、项目资源和画布图层数量不得增加。
- 角色面板可选择 `style: "none" | "pixelArt"`;`none` 的处理路径和产物保持不变,且提交给 provider 的提示词与未带该字段时逐字一致,`pixelArt` 在提示词末尾追加「角色主体为像素风格」并在 Alpha 回贴后执行内存像素规整,最终 OSS PUT、项目资源和画布图层数量不得增加。
- 默认打开角色生成面板时选中 `nanobanana2 / 1:1 / 1K`;切换到 `gpt-image-2` 后再次打开角色或图标素材面板应沿用该模型。
- 生成成功后在占位图位置创建 `assetKind: "character"` 图层,右上角显示 `角色` 标签,布局保存包含该字段。