Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e695cdf499 | |||
| 4ca93a6f16 | |||
| 2f1da79f9c | |||
| c59a912eef | |||
| 3d145379f2 | |||
| ae5c2d8821 | |||
| 8b4ec2c4d4 | |||
| 95e34b8760 | |||
| c51d88fd19 | |||
| 4d1c9d9b1b | |||
| 90cfdfdf97 | |||
| f65a813768 | |||
| b5dc99e067 | |||
| c94bb6d3ed | |||
| 101fd952d5 | |||
| 2c85ea0b83 | |||
| 13afdd7a3b | |||
| 05f0e2db06 | |||
| e368145f9d | |||
| 0123f3db76 | |||
| d6cb68cf13 | |||
| 132bf7db35 | |||
| 1575afc92c |
+1
-10
@@ -23,6 +23,7 @@
|
||||
- [图片画布编辑器 MVP 接入方案](./technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md)
|
||||
- [图片画布编辑器前端拆分计划](./technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md)
|
||||
- [图片画布游戏场景生成链路](./technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md)
|
||||
- [Art Agent 多模态 Spine 序列帧动画生成](./technical/【技术方案】ArtAgent多模态Spine序列帧动画生成-2026-08-10.md)
|
||||
- [画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md)
|
||||
- [SFX 生成优化 V2.0 任务拆解](./project-memory/plans/【实施计划】SFX生成优化V2.0任务拆解-2026-08-06.md)
|
||||
- [SFX 生成优化 V2.0 T6 测试与发布门禁](./【实施记录】SFX生成优化V2.0T6测试与发布门禁-2026-08-07.md)
|
||||
@@ -34,16 +35,6 @@
|
||||
- [浏览器内 AI Web 工程沙箱预览](./technical/【技术方案】浏览器内AIWeb工程沙箱预览方案-2026-06-13.md)
|
||||
- [AI Web 工程 Runner 安全模型](./technical/【安全模型】AIWeb工程Runner与预览隔离威胁模型-2026-06-13.md)
|
||||
|
||||
### AI 游戏创作 Runtime
|
||||
|
||||
1. [AI 游戏创作 Agent Runtime 交互边界重构总览与实施计划](./technical/【技术方案】AI游戏创作Agent%20Runtime交互边界重构实施计划-2026-08-12.md)
|
||||
2. [AI 游戏创作 Agent Runtime 交互合同 V1(唯一规范性协议)](./technical/【技术协议】AI游戏创作Agent%20Runtime交互合同V1-2026-08-17.md)
|
||||
3. [AI 游戏创作 Agent Runtime 交互边界迁移矩阵](./technical/【迁移方案】AI游戏创作Agent%20Runtime交互边界迁移矩阵-2026-08-17.md)
|
||||
4. [AI 游戏创作 Agent Runtime 交互边界证据与决策附录](./technical/【设计依据】AI游戏创作Agent%20Runtime交互边界证据与决策附录-2026-08-17.md)
|
||||
5. [AI 游戏创作 Agent Runtime V1.1](./technical/【技术方案】AI游戏创作Agent%20Runtime%20V1.1-2026-07-12.md)
|
||||
|
||||
上述四份交互边界文档按“总览 → Contract → 迁移矩阵 → 证据附录”阅读;字段、状态机和错误语义只以 Contract 中的 `IC-*` 为准。
|
||||
|
||||
### 后端与公开数据
|
||||
|
||||
- [外部生成 Worker 化方案](./technical/【后端架构】外部生成Worker化方案-2026-06-03.md)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-08-15 Spine 序列帧去背景复用父队列并行处理
|
||||
|
||||
- 背景:Spine 序列帧去背景已经是 `editor_character_animation_background_removal` durable job,但父 job 内的 33 帧仍逐帧串行下载、绿幕处理、上传和帧对象准备,用户只能看到长时间的二态 loading。
|
||||
- 决策:继续只使用一个现有 `external_generation_job` 父任务,不新增逐帧子任务、表、计费或公开 DTO;worker 在父 job 内用 `buffer_unordered(frame_count.max(1))` 并行处理所有帧,必须 drain 全部已发出的 future,再按 `frame_index` 排序并一次性发布。任一帧失败时清理已成功但尚未正式提交的对象;当前帧上传后帧对象准备失败时在该 future 内立即清理,避免并行化引入孤儿对象。
|
||||
- 幂等边界:帧对象路径绑定本次 job 的 `operation_id`,同一 job 重放复用路径,不同 job 即使输入 fingerprint 相同也必须生成新的对象路径和新的 asset_object;结果作为新的 `character-animation` / `image-sequence` resource、素材和画布图层发布,原始序列保留。
|
||||
- 去背景算法边界不变:只使用本地 `screen-color-keying` 绿幕扣除,不调用 LLM、BgFilter 或阿里云;普通图片去背景链路不受影响。
|
||||
- 关联文档:`docs/technical/【技术方案】ArtAgent多模态Spine序列帧动画生成-2026-08-10.md`。
|
||||
|
||||
## 2026-08-12 Repository checks 采用 CI 与本地共用的单一门禁入口
|
||||
|
||||
- 背景:master run 1037 的 Backend/Frontend 已通过,但 `Repository checks` 因 3 个 `simple-import-sort/imports` 错误失败。原 pre-commit 只运行 Prettier,Prettier 不处理 ESLint import 排序;推送前又未运行完整仓库 lint,因此本地与 CI 的覆盖范围长期存在漂移。
|
||||
@@ -16,7 +24,6 @@
|
||||
- 验证方式:覆盖 Codex 分类与敏感诱饵、失败事件公共摘要、最近任务与各正式卡片、game-chat 阶段记录、待核对状态、final-reply fallback 白名单及 malformed 响应完整重试;运行 Rust 定向测试、前端模型/AppSurface 定向测试、Shell typecheck、编码和 diff 门禁。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
|
||||
## 2026-08-10 资源管理评审阻塞项按第二轮正式合同修复
|
||||
|
||||
- 背景:资源管理第一轮实现后,人工验证继续暴露 WebView 默认缩放、预览队列饥饿、过滤后媒体残留播放、外层滚动串 scope、超深依赖坐标越过 Rust 上限和暂时错误无法重试等问题。部分 PRD / 技术方案仍描述第一轮的中央媒体预览、单全局 Overlay 和统一 section scope,已经与第二轮代码及验收结论冲突。
|
||||
@@ -60,6 +67,7 @@
|
||||
- 替代关系:本条替代下方 2026-08-07 阶段二中“缩放只等于可视高度”和“只为完整可见卡片建立端点”的显示口径;其中曾采用的全局 SVG、四 viewport 联合 clip 和单全局 Observer 又由上方“依赖 SVG 改为分区 plane 所有”决定替代。高度模型、四分区、会话隔离、内外滚动和无布局 CAS 等其它决定继续有效。阶段一媒体卡、阶段三确定性聚类、Rust `dependencyDepths`、producer 截断降级、历史手动坐标与 type sidecar 均不变。
|
||||
- 验证方式:纯倍率模型覆盖按钮 / wheel 边界;AppSurface 覆盖项目、mode、section 隔离、普通 wheel、Ctrl wheel、WebKit gesture 与零布局写入;SVG 覆盖同 plane 倍率、橙色 marker、直线 / 小圆角横纵路由、自环间隙、双向边界继续线、分区原生裁剪、每区单 Observer、task-flow 零渲染和 4096 精确关系有界输出。
|
||||
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-08-11 Tauri 无限画布以可证明事务和分类恢复收口
|
||||
|
||||
- 终态与队列:资源编辑账本正式区分可继续阶段、`reconciliation-required`、`remote-failed` 和 `archived`。远端明确失败只保存稳定分类与终态时间,不得再 POST、轮询或重新扣费;只有该终态能由用户显式归档并移出活动恢复队列,归档保留账本且不伪装 `committed`。结果未知和需对账项继续失败关闭。
|
||||
@@ -7184,7 +7192,6 @@
|
||||
- 决策:只有同一可信根 Supervisor 能调用 `agent.acceptance_update`,且该控制面动作必须独占一轮,不能夹带 `plan_update`、legacy plan 或回复。requiredEvidence 采用 `tool:<Runtime 工具名>`,合同冻结前必须命中 Runtime 允许的持久证据工具集合并拒绝拼错、控制面和纯协调工具;动态 MCP catalog 不得冻结为不可变 requiredEvidence。passed 节点必须引用当前根任务树中对应工具的真实成功动作回执;回执同时记录动作执行边界的 `projectRevisionBefore / projectRevisionAfter`,非 mutation evidence 必须满足 before、after 与验收时 current revision 完全相同,mutation evidence 的 after 必须等于 current revision,旧 revision 或执行后延迟落账的回执不可重放。failed、not-observed、缺失节点以及落后当前 project revision 的整图确认状态均阻断普通完成、finalization 与恢复。项目 revision 变化后由 Supervisor 只更新受影响节点并确认当前图,未提交的 passed 节点保持不变;Runtime 不替 Supervisor 推断影响范围、选择具体 Agent 或实现方式。
|
||||
- 决策:不可变 Goal Contract 的根 Run 收到 steer 时,必须按旧 rootRunId 串行化整个转换,并在持锁后重新确认该旧根仍是 Session 当前权威 Run,避免不同 steerId 并发创建多个 replacement。随后为绑定旧 rootRunId 的全部非终态静态、ready 和 isolated 后代写入取消栅栏、打断 Provider 并逐个收束,再终止旧根;所有项目修改入口在看到取消栅栏后立即失败关闭。Runtime 必须确认旧树所有成员都已进入终态或 `needs-reconciliation`,超时则保持等待并拒绝启动 replacement;只有旧树停稳后,才在同一 Session、source 和 Run Profile 启动新根 Run 重新理解完整目标。
|
||||
|
||||
|
||||
## 2026-08-08 External v1 图片编辑来源字段允许原地收紧
|
||||
|
||||
- 背景:图片编辑主来源已经从可由客户端提交 objectKey 和类型提示的 `sourceImageSrc / sourceResourceId / assetKind`,收紧为服务端按项目资源 ID 或素材 ID 解析权威对象与类型的必填 `sourceReferenceId`。这会让严格 External v1 客户端立即失败,属于现役版本策略明确列出的 breaking change;2026-07-31 的历史豁免不能自动覆盖本次变更。
|
||||
@@ -8653,6 +8660,7 @@
|
||||
- 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5,WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。
|
||||
|
||||
> 2026-07-18 覆盖说明:本段后续关于微信 `navigation.openNativePage`、生成结果订阅页、`[subscribe-message]` 日志和订阅页路由门禁的 2026-06 决策均已由旧创作模板退役决策废止,只作为历史记录。Expo / Tauri 的同源 H5 受控导航及微信登录、支付、分享能力继续有效。
|
||||
|
||||
- 2026-06-20 H5 原生导航预校验:`navigateHostNativePage()` 在 `native_app` 下发送 `navigation.openNativePage` 前必须先拒绝空值、控制字符、协议相对 URL、外域绝对 URL 和非 `http:` / `https:` 协议目标;同源绝对 URL、`/path` 和保留给桌面壳兼容的相对 route 继续交给 Expo / Tauri 壳二次归一并补写宿主上下文。微信小程序分支仍按小程序页面 URL 语义走 `wx.miniProgram.navigateTo`,不套原生 App 同源 H5 预校验。根级 `npm run check:native-shells` 会反查 H5 facade 仍使用 `normalizeNativeAppPageUrl(...)` 且发送归一后的 URL,避免明显不安全目标触达原生壳。
|
||||
- 2026-06-20 微信受控原生页能力声明:微信小程序壳真实 capability profile 声明 `navigation.openNativePage`,用于承接已经登记并测试的小程序原生页 flow;当前订阅生成结果通知页通过 H5 `requestGenerationResultSubscribePermission()` 调用 `navigateHostNativePage()` 打开 `/pages/subscribe-message/index`,小程序页再调用真实 `wx.requestSubscribeMessage` 并按既有结果协议回灌。根级 `npm run check:native-shells` 必须把该能力反查到共享 profile、微信 `WECHAT_HOST_CAPABILITIES` 镜像、订阅页协议常量、H5 入口、小程序 host-bridge / shell / page 文件和相关测试;该能力不代表开放任意小程序页面跳转。
|
||||
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示受控分享动作,并按 `hostShell` 区分 Expo 系统分享面板和 Tauri 剪贴板复制表达,避免旧壳或裁剪壳露出不可用入口。
|
||||
@@ -14067,6 +14075,7 @@
|
||||
- 分类:内部稳定 reason code 固定为 `translation_invalid / translation_upstream_failed / translation_budget_exhausted / elevenlabs_http_failed / invalid_audio / duration_probe_failed / oss_failed / writeback_failed`。MIME、空 body 和大小归 `invalid_audio`;MP3 识别、帧读取和时长门禁归 `duration_probe_failed`。普通用户继续只读稳定短文案,不暴露 endpoint、上游正文或凭据。
|
||||
- 跨入口:画布 Agent `generate-sound-effect` 与站内 / External v1 共用 canonical Prompt、固定模型、nullable `0.5-30` 小数时长和 Loop;省略 duration 为手动 `5s`,显式 null 为自动。SFX 参数解析必须保留该 null,不能被通用 null-default 兼容层改写。最终仍进入相同 `editor_sound_effect_generation` queue payload,不新增 Agent 专属链路。
|
||||
- 发布边界:T6 工程实施和 mock / loopback 门禁不等于真实 provider 或生产验收。发布前关闭 SFX 入队,使用显式 `--server` / `--server-url` 只读查询 `external_generation_job` 中 pending / running 的 `editor_sound_effect_generation`,清零后按 api-server / Worker → Web 顺序部署并灰度;禁止 `--root-dir`、删除任务伪造 drain 或自动回退 Vidu。本次没有 SpacetimeDB schema、migration 或 bindings 变更。
|
||||
|
||||
## 2026-08-06 编辑器生成结果使用 durable receipt 与统一原子提交
|
||||
|
||||
- 背景:图片、改图、去背景、图集 / UI 多产物、角色动作、视频、音效和背景音乐在 OSS 结果可用后,仍分段 confirm object、创建 project resource / account asset、保存 canvas 和 complete job。任一中间失败都会留下部分业务事实;只把 `external_generation_job` 当 operation journal 又无法覆盖无 job 的 inline,也无法独立证明某批 resource/asset/canvas 已作为一笔提交完成。
|
||||
@@ -14153,6 +14162,16 @@
|
||||
- Windows 锁文件决策:提升权限进程新建 `.agent/.manifest.json.lock` 时,Windows 可能把 owner 设为 `Administrators`。仅在固定锁路径已取得不共享独占句柄并确认是普通、非 reparse、单链接文件后,才初始化为当前 `TokenUser`;随后再次复核句柄并执行原有 owner/DACL 校验,不放宽既有异常对象的安全规则。
|
||||
- Provider Schema 决策:`agent.route_manifest.missingAssetSlots` 不再广告 OpenAI-compatible 代理拒绝的 `uniqueItems`;Runtime 继续排序去重,Schema 子集门禁新增该关键字,真实 Provider smoke 必须在发布前证明工具目录可被接受。
|
||||
- 运行决策:Godot 项目提交给 Project Supervisor 时使用 `standard` Run Profile,避免触发 Web 专用 `game/index.html`、HTTP preview 与自主 Web 完成门。Godot 编辑器启动和内嵌运行预览不在本切片范围。
|
||||
|
||||
## 2026-08-13 Spine 序列帧多模态与视频直接转换
|
||||
|
||||
- 同一 Spine 序列帧面板固定分为“AI生成/改造”和“视频直接转换”两个标签,旧项目默认恢复 AI。AI 视频以 `motion` 表示动作视频、`appearance` 表示可选角色外观图;上传序列帧 PNG 只作为一张完整参考图,不做网格识别。
|
||||
- 视频直接转换走内部 `editor_character_animation_video_conversion` durable job,FFprobe + FFmpeg 覆盖完整源时长,采样率最高 8 FPS、最低 1 FPS,产出 2–66 帧;480p/720p 是只缩不放的最大长边。该路径固定免费、保留背景,不调用 Seedance 或 BgFilter,也不复制保存源视频。
|
||||
- 直接转换仍落 `character-animation` / `image-sequence`,配方 action 为 `character-animation.convert`,来源槽位为 `source`;最多 66 帧时首帧 item 同时承载最终 resource/asset。原子生成上限和角色动画拆帧上限沿用 master 的 66,但图标图集公开切片继续保持 64,既有 JSON 大小门禁不放宽。
|
||||
- merge master 后保留角色图层浮动工具栏和右键菜单中的 `生成动画` 快捷入口,统一进入同一个角色动作 dialog;`character-animation` 结果不支持快速编辑,仍保留 `改造 / 去背景 / 拆帧 / 下载`。快速编辑入口与提交门禁使用统一正向白名单,未知媒体或素材类型默认拒绝。
|
||||
- Spine 序列帧工具栏的“去背景”暂定为纯算法绿幕扣除:只读取正式序列帧 objectKey,使用本地 `screen-color-keying` 处理约定的 `#00FF00` 绿幕,不调用 BgFilter、阿里云或其它模型;普通图片 `/api/editor/images/background-removals` 的通用去背景链路不受影响。
|
||||
- `/api/external/v1` 不开放上述内部字段或直接转换路由,不修改 OpenAPI;本次不改 SpacetimeDB 表结构、迁移或生成 bindings。
|
||||
|
||||
## 2026-08-15 Jenkins 容器预览部署使用独立控制面
|
||||
|
||||
- 决策:多人内网容器预览不把操作表单塞进 Jenkins 页面,也不让 SPA 直接操作 Docker。独立 `preview-deployer` SPA 通过同源 Axum 代理触发固定 `shared/Genarrative-Preview-Deployer` Job;浏览器只持有控制面 HttpOnly 会话,Jenkins service account 和 API Token 只存在服务端环境。
|
||||
@@ -14162,8 +14181,21 @@
|
||||
- 来源与卸载:部署只接受 `SOURCE_BRANCH` 和可选 `COMMIT_HASH`,Jenkins 必须证明 commit 属于目标分支。卸载只接受受控状态中存在的 `deploymentId`,客户端不能传 Jenkins URL、Job、Compose project、容器名或端口。状态通过固定 `preview-result.json` artifact 返回,不解析或向浏览器暴露完整 console。
|
||||
- 关联文档:`docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
|
||||
## 2026-08-17 Spine 视频模式外观参考图容量门禁
|
||||
|
||||
- 背景:视频模式的 `appearanceReferenceSrcs` 原先会在 Provider 调用前逐项下载并完整解码,但该实际执行路径没有复用通用视频参考的 9 张数量上限,也没有单图、累计字节、解码边长和像素预算,可能让第 10 张或压缩炸弹图片持续占用 durable worker 资源。
|
||||
- 决策:角色外观图最多 9 张,单图最大 30MB、合计最大 64MB,编码边长最大 8192、单图总像素最大 33554432。前端上传追加与草稿恢复收口到 9 张;HTTP 入队和 worker 共用请求归一化数量门禁;worker 在 Provider 调用前按 OSS HEAD、流式下载字节上限、解码器资源限制及显式像素计算失败关闭。既有 owner、稳定 objectKey、MIME 和来源资源校验继续保留。
|
||||
- 验证:覆盖 9/10 张边界、30MB 单图字节边界、8192 边长及总像素边界,并运行角色动作 Rust 定向测试、相关前端 Vitest、类型检查、编码与 diff 门禁。
|
||||
|
||||
## 2026-08-17 Spine 视频转换去背景改为显式双模式
|
||||
|
||||
- 背景:仅凭 `generationInputs.action = character-animation.convert` 无法证明源视频是绿幕;把任意转换结果固定按 `#00FF00` 键控会误处理普通背景或绿色主体。仓库已有 BgFilter `complex + birefnet` 通用分割服务,也已有可传 `screen_color` 的 flat 协议,但纯色场景不需要承担外部服务成本。
|
||||
- 决策:只有视频直接转换序列继续显示“去背景”,点击后必须在当前选中序列帧旁的非模态悬浮窗中显式选择 `solid-color` 或 `general`;悬浮窗随画布平移和缩放更新位置,优先置于右侧并在空间不足时翻转,不增加画布遮罩、不覆盖源画面,纯色模式不使用系统吸管;内置逐帧 viewer 从正式 objectKey 读取原始帧字节,按原始比例实际绘制,且仅在可见图像区域内采样。用户悬停时实时显示原始像素色样与 11×11 放大网格,点击才回填键色,以消除容器留白和 CSS 缩放导致的取色位移。纯色模式默认 `#00FF00`,允许颜色输入及逐帧 viewer 原始像素取样;后端使用参数化本地键色算法,对轻微压缩噪声和接近键色的平缓色差有容忍度,但不承诺处理明显渐变、阴影、纹理或与主体相近的背景,后者应选择 `general`。通用模式逐帧调用 BgFilter complex,不携带键色。悬浮窗的“提交中”只表示 HTTP 入队请求,服务端接受任务后立即关闭悬浮窗,由画布生成占位和任务列表承接 durable worker 的后续状态;HTTP 提交失败则保留悬浮窗并恢复可重试状态。HTTP 入队和 durable worker 共用白名单校验,后端在结果 `generationInputs` 中权威记录模式、颜色或 Provider/model,不能再把“视频转换”本身当作绿幕证明。两种模式仍生成新序列资源、整批失败关闭并保留源序列。
|
||||
- 费用边界:本次沿用现有手动 BgFilter 的 0 泥点内部契约;供应商成本不等于已确定的用户泥点售价,在没有正式定价决策时不自行新增价格。若后续收费,必须接入统一后端定价、入队冻结价格和 worker 扣费退款链路。
|
||||
- 验证:覆盖缺失/非法纯色、通用模式禁止颜色、任意键色透传、绿色主体在非绿色键色下保留、双模式 worker 路由、前端模式选择及吸管回填,并运行角色动作 Rust 定向测试、前端 Vitest、类型检查、编码和 diff 门禁。
|
||||
|
||||
## 2026-08-17 预览发布记录使用 Jenkins 构建编号并有限保留
|
||||
|
||||
- 决策:内部稳定 `deploymentId` 继续绑定分支、Compose project 和端口租约;页面/API 记录 ID 在 Jenkins 分配执行器后改为构建编号,排队阶段为“待分配”。卸载通过构建编号找到内部实例,再向固定 Job 传内部 ID。
|
||||
- 清理:失败或取消且不存在可卸载实例的记录保留 7 天;成功卸载的内部审计记录保留 30 天;仍可卸载的失败记录永久保留到人工卸载。服务启动、读取列表和创建部署时执行清理并原子落盘。
|
||||
- 链接:服务内部仍通过 Jenkins loopback 轮询;只向浏览器返回由 `GENARRATIVE_PREVIEW_DEPLOYER_JENKINS_PUBLIC_BASE_URL` 构造的局域网构建详情地址,禁止回传 loopback URL。
|
||||
- 链接:服务内部仍通过 Jenkins loopback 轮询;只向浏览器返回由 `GENARRATIVE_PREVIEW_DEPLOYER_JENKINS_PUBLIC_BASE_URL` 构造的局域网构建详情地址,禁止回传 loopback URL。
|
||||
@@ -583,6 +583,8 @@
|
||||
- 现象:产品要求画板 `生成角色动作` 返回后按透明序列帧播放和下载,但旧实现或旧测试可能继续把结果当作预览视频处理。
|
||||
- 原因:后端仍需要先生成 `previewVideoPath` 再抽帧、绿幕去背和落 OSS;如果前端把预览视频当主媒体,就会绕过已经扣绿幕的 PNG 帧,也无法按序列帧打包下载。
|
||||
- 处理:角色动作结果图层主 `src` 使用 `frames[0].imageSrc`,`mediaType` 固定为 `image-sequence`,`assetKind` 固定为 `character-animation`,完整帧列表写入 `imageSequenceFrames`,`previewVideoPath` 只作为来源信息保留。生成端确认每帧对象后必须把该帧 `objectKey` 与 `assetObjectId` 一起写入正式 payload 和 `generation_inputs_json.characterAnimation.frames`。单图层下载必须生成序列帧 ZIP;画布素材 ZIP 中角色动作写入 `sequences/<编号-标题>/frames/`。不得移除后端原有视频生成、抽帧、绿幕去背和帧落盘流程。
|
||||
- 拆帧:拆帧只把正式帧对象登记成独立项目资源和账号素材,必须复用每帧已有的 `assetObjectId/objectKey`,禁止重新下载再上传 PNG。批次继续复用现有 spritesheet slice 批量事务,原子写入全部帧并完成 cohort;拆帧 task/cohort 必须按 owner + `sourceResourceId` + 规范化目标目录与标签稳定隔离,不能直接复用原生成 task,否则同一素材进入多个项目后会把多套帧归入一个错误 cohort。原帧 `asset_object.source_job_id` 继续保留原生成来源,不能为了匹配拆帧 task 伪造血缘。结果只进入素材库,不自动创建几十个画布图层。
|
||||
- 多模态参考:`inputMode/referenceMediaType` 只是客户端意图,不是媒体事实。图片或视频参考必须先按 owner 解析到权威 objectKey,再在扣费和 provider 调用前使用 OSS HEAD、权威 `asset_object.content_type` 或实际媒体探测验证 `image/*` / `video/*`;不能把上游报错和退款当作后端媒体校验。
|
||||
- 验证:`ImageCanvasGenerationLayerModel` 应断言动作结果 `src` 为首帧且 `mediaType="image-sequence"`;画布集成测试应出现 `画布序列帧:角色动作` 图片播放器,不应出现角色动作 `<video>`;导出测试应断言角色动作下载和画布素材导出都包含序列帧 ZIP / frames 目录;生成测试还应断言首帧与非首帧的稳定引用都被保留。
|
||||
- 关联:`src/components/image-editor/ImageCanvasGenerationLayerModel.ts`、`src/components/image-editor/ImageCanvasWorldView.tsx`、`src/components/image-editor/ImageCanvasExportModel.ts`、`server-rs/crates/api-server/src/character_animation_assets.rs`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`。
|
||||
|
||||
@@ -4248,6 +4250,13 @@
|
||||
- 处理:资源 / 素材只保存 `image_sequence_frames_json` 与 `image_sequence_duration_ms`,精选审核快照只冻结复制这两个正式字段。角色动作要求至少两帧且毫秒时长大于 0;播放器按 `时长毫秒 / 数组长度` 计算间隔,Spine 导出时再换算秒数并推导 FPS。音频 / 视频 `durationSeconds` 不映射到这两个字段。
|
||||
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`src/components/image-editor/ImageCanvasWorldView.tsx`、`src/components/image-editor/ImageCanvasExportModel.ts`。
|
||||
|
||||
## 多模态角色动作参考必须绑定上传上下文并由后端读取权威规格
|
||||
|
||||
- 现象:普通视频与 Spine 序列帧面板共用参考上传控件时,用户在异步探测或上传期间切换面板,完成结果可能串入新面板;`same` 比例若直接采用客户端宽高,还会把省略或伪造尺寸的视频错误归一化为 `1:1`。
|
||||
- 处理:图片、视频、音频参考上传必须以 generation dialog 身份和分媒体容量建立 reservation,在探测、OSS 上传、项目资源 / 素材持久化和回填各边界复验;序列帧单参考限制按 dialog 语义判断,不依赖 `activeTool`。后端在扣费和 Provider 调用前按 owner 解析稳定对象,视频用带总超时的同一 HTTP client 做 HEAD 与 50MB 有界流式 GET,再由 FFprobe 读取 2–15 秒时长、显示方向宽高;图片按 EXIF 方向后的实际解码尺寸处理。`same` 只使用这些权威尺寸重新归一化。
|
||||
- 拆帧:省略素材目录时先转换为 owner 的正式默认文件夹 ID 并做 project/folder preflight;决定 resource、asset 与 cohort 的稳定 operation ID 属于资产领域规则,放在 `module-assets`,API 只编排调用。
|
||||
- 关联:`src/components/image-editor/useImageCanvasUploadWorkflow.ts`、`server-rs/crates/api-server/src/character_animation_assets.rs`、`server-rs/crates/module-assets/src/character_animation.rs`。
|
||||
|
||||
## 精选角色动作显示首帧还要检查前端 renderer 与逐帧授权
|
||||
|
||||
- 现象:精选接口已经返回 `imageSequenceFrames` 和正确的 5 / 6 秒成本,但创作主页或后台审核仍只显示首帧;接入播放器后又可能只有第一帧成功、后续帧换签返回 404。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,345 +0,0 @@
|
||||
# AI 游戏创作 Agent Runtime 交互边界重构
|
||||
|
||||
> 文档角色:总览与分阶段实施计划
|
||||
> 状态:评审中;尚未允许进入 P1–P6 生产实现
|
||||
> 更新日期:`2026-08-17`
|
||||
|
||||
## 0. 阅读入口与权威顺序
|
||||
|
||||
本文只解释重构目的、系统边界和 P0–P6 实施顺序,不定义协议字段与状态机。四份配套文档的职责和权威顺序如下:
|
||||
|
||||
1. 本文:第一次理解方案和实施阶段的入口。
|
||||
2. [`【技术协议】AI游戏创作Agent Runtime交互合同V1-2026-08-17.md`](./【技术协议】AI游戏创作Agent%20Runtime交互合同V1-2026-08-17.md):唯一规范性协议;所有 `IC-*` 要求以它为准。
|
||||
3. [`【迁移方案】AI游戏创作Agent Runtime交互边界迁移矩阵-2026-08-17.md`](./【迁移方案】AI游戏创作Agent%20Runtime交互边界迁移矩阵-2026-08-17.md):把 `IC-*` 映射到当前代码、入口、阶段和验收证据。
|
||||
4. [`【设计依据】AI游戏创作Agent Runtime交互边界证据与决策附录-2026-08-17.md`](./【设计依据】AI游戏创作Agent%20Runtime交互边界证据与决策附录-2026-08-17.md):保存 `EV-*` 代码事实、`DR-*` 设计决策和 `EG-*` 证据门禁。
|
||||
|
||||
如四份文档发生冲突:
|
||||
|
||||
- 当前代码和仓库最新架构文档决定“系统现在是什么”;
|
||||
- Interaction Contract 决定“本次重构必须实现什么”;
|
||||
- 迁移矩阵和证据附录不得改变 Contract,只能暴露当前差距;
|
||||
- 实现发现 Contract 不可行时,先修改 Contract 并重新评审,不得在 Consumer 或 Adapter 中自行发明兼容语义。
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么要重构
|
||||
|
||||
当前 GUI、CLI、`swarm_cli`、Tauri wrapper 和测试路径分别承担了一部分 Runtime 生命周期判断:
|
||||
|
||||
- 是否启动新 Run;
|
||||
- 是否 steer 当前 Run;
|
||||
- 是否直接回复;
|
||||
- 如何处理用户输入、批准、拒绝、重试、恢复和取消;
|
||||
- 如何合并 Runtime state、event、response stream 和 conversation;
|
||||
- 如何把 Runtime 输出再次保存为聊天消息。
|
||||
|
||||
这导致同一个用户操作可能因 Consumer 不同而走不同控制流,也使前端刷新、CLI 无头运行、Runner 恢复和测试夹具难以共享同一行为边界。
|
||||
|
||||
本重构解决的不是“代码散落”本身,而是控制权归属不清:
|
||||
|
||||
> Consumer 现在既展示状态,又在部分路径中决定下一步并写入 Runtime;重构后 Consumer 只展示后端状态并表达用户意图,Supervisor Shell 统一作出交互决策。
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标边界
|
||||
|
||||
### 2.1 核心目标
|
||||
|
||||
1. GUI、CLI 和公开协议测试成为同一协议的平等 Consumer。
|
||||
2. Consumer 只执行 `render(snapshot)` 与 `dispatch(command)`,不推进 Runtime 状态机。
|
||||
3. Supervisor Shell 统一处理正式用户交互决策、命令校验、幂等受理和结果读回。
|
||||
4. 现有 Runtime task、state、pending、provider、steer、finalization、conversation 和 event records 继续作为执行事实源。
|
||||
5. External Runner 存在时,由持有现有 OS project execution-owner lock 的 Runner 执行正式写入。
|
||||
6. Public 与 Developer read model 分离,正式用户协议不泄漏路径、Provider、工具参数、内部 action 或 recovery 细节。
|
||||
7. 通过 Adapter 和明确 writer cutover 分阶段迁移,不制造第二套 Runtime authority 或第二份 conversation 正文。
|
||||
|
||||
### 2.2 不在本轮
|
||||
|
||||
- Runtime `main_loop`、task queue、Provider retry、delegation/all-join 或 Agent 执行状态机内部重构;
|
||||
- LLM、Provider、提示词或工具体系调整;
|
||||
- Runner 开机自启或无人值守常驻;
|
||||
- 新增 live Session rotation、Session handoff、第二 active-session index 或 session control lease;
|
||||
- 新增全局 owner generation/lease CAS;
|
||||
- 重新设计资源上传、项目资源 lineage、Preview Registry 或 Session 管理面;
|
||||
- 把管理命令、路径操作或 Developer 调试能力伪装成五个 Public Runtime 命令。
|
||||
|
||||
---
|
||||
|
||||
## 3. 新系统的一句话结构
|
||||
|
||||
```text
|
||||
GUI / CLI / Tests
|
||||
读取 Public Snapshot 与 Public Conversation
|
||||
提交五个公开写命令
|
||||
↓
|
||||
Supervisor Shell
|
||||
校验身份、权限、版本、目标和幂等性
|
||||
决定 direct reply / start / steer / reject / interaction required
|
||||
通过 Adapter 调用现有 Runtime 能力
|
||||
↓
|
||||
Existing Runtime
|
||||
继续维护 task / state / action / provider / finalization / conversation / event 事实
|
||||
```
|
||||
|
||||
四个角色的责任如下:
|
||||
|
||||
| 角色 | 负责 | 禁止 |
|
||||
|---|---|---|
|
||||
| Consumer | 显示 Snapshot、提交 command、读取 Conversation | 根据 phase/文案自行选择 start、steer、retry、resume;直接解释私有 Runtime records |
|
||||
| Supervisor Shell | 统一交互判断、命令受理、幂等、能力投影和错误映射 | 复制 Runtime 生命周期真相;把投影完成当成执行成功 |
|
||||
| Runtime | 实际执行、恢复、finalization 和事实持久化 | 依赖 GUI 轮询推进状态 |
|
||||
| Runner | 持 owner lock 时执行正式 Shell 写入、wake 和恢复 | 失去 owner 后继续写;依赖诊断 JSON 或本地时间接管 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 一次用户输入如何流动
|
||||
|
||||
1. Consumer 读取 `PublicSnapshot`。
|
||||
2. Snapshot 返回当前状态以及服务端生成的 command capability。
|
||||
3. 用户提交消息时,Consumer 调用 `submit_intent`,不调用 start/steer primitive。
|
||||
4. Shell 校验 schema、项目、Session、权限、requestId 和 capability target。
|
||||
5. Shell 持久化可读回的 request acceptance record。
|
||||
6. Shell 根据冻结 policy matrix 决定:
|
||||
- DirectReply;
|
||||
- Start;
|
||||
- Steer;
|
||||
- Reject;
|
||||
- InteractionRequired。
|
||||
7. 需要 Runtime 执行时,Shell 通过 Adapter 绑定现有 run/steer/action/finalization identity;不复制这些对象的生命周期。
|
||||
8. Runtime 更新 durable facts。
|
||||
9. Public projector 从 facts 重建 Snapshot;`SnapshotChanged` 只提示 Consumer 重新读取 Snapshot。
|
||||
10. 文本交付通过 Public Conversation Adapter 按 source 回读;Snapshot 和 conversation 都不能互相推导对方的权威结论。
|
||||
|
||||
### 4.1 五个 Public 写命令
|
||||
|
||||
| 命令 | 用户含义 | Shell 负责决定的内部动作 |
|
||||
|---|---|---|
|
||||
| `submit_intent` | 提交消息或受支持的内置命令 | direct reply / start / steer / reject / interaction required |
|
||||
| `answer` | 回答 UserInput | 校验 interaction revision、答案约束和 durable target |
|
||||
| `approve` | 批准、拒绝或带意见返工 | 校验 audience、policy、artifact binding 和 rework identity |
|
||||
| `cancel` | 取消精确 Supervisor Run | 校验 Session、Run 和取消矩阵;不伪造终态 |
|
||||
| `resume` | 继续、重试或受信任 reconcile | 明确区分 ContinueRun、RetryTerminalRun、ReconcileRun |
|
||||
|
||||
### 4.2 两个 read model
|
||||
|
||||
- Public Snapshot:普通 GUI、普通 CLI 和公开测试的唯一完整 Runtime 视图。
|
||||
- Developer Snapshot:受信任开发入口的独立 DTO;扩大 read,不扩大正式 Supervisor 写权限。
|
||||
|
||||
### 4.3 Conversation 是独立展示通道
|
||||
|
||||
Public Conversation 不保存第二份正文,只建立 source-to-Public 索引并从原 source 回读。V1 区分:
|
||||
|
||||
- User;
|
||||
- DirectReply;
|
||||
- RuntimeFinalReply;
|
||||
- 满足准入条件的 RuntimeStatus;
|
||||
- 满足准入条件的 PublicEvent。
|
||||
|
||||
不能稳定定位、校验或归属的 source 默认隔离,不因 GUI 当前能展示就进入永久 Public history。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键不变量
|
||||
|
||||
1. **单一执行事实源**:Runtime durable records 决定执行事实;Shell ledger 只记录 request 协调和 source binding。
|
||||
2. **单一正式 writer**:同一真实副作用和同一 conversation source identity 在任一时刻只有一个正式 writer。
|
||||
3. **服务端能力驱动**:没有 capability 就不能构造命令;有 capability 仍需 Shell 在锁内重读事实并复核。
|
||||
4. **结果未知不重放**:无法证明真实副作用是否发生时进入 outcome-unknown/reconciliation,不换 requestId 重做。
|
||||
5. **Session 不重归属**:历史 command、interaction 和 delivery 依赖已落盘 `agentId + sessionId + runId`,不依赖当前 active Session 猜测。
|
||||
6. **事件不是状态**:事件只提示重新读取 Snapshot;事件丢失、重复或乱序不能改变最终状态。
|
||||
7. **Conversation 不复制正文**:Public index 不成为正文 authority;source 不可回读时失败关闭。
|
||||
8. **Public 与 Developer 隔离**:Developer read capability 不能成为绕过同一 Shell write ingress 的通道。
|
||||
|
||||
完整规范见 Contract 中的 `IC-*` 要求。
|
||||
|
||||
---
|
||||
|
||||
## 6. 分阶段实施计划
|
||||
|
||||
### P0:行为基线与协议验证框架
|
||||
|
||||
**人话目标**:在改变系统前,建立可以重复观察当前行为、发现重构破坏的测试入口;不是决定协议,也不实现生产协议。
|
||||
|
||||
允许:
|
||||
|
||||
- 只读 fixture、golden trace、negative fixture;
|
||||
- crash-point harness;
|
||||
- 调用图和 writer inventory;
|
||||
- 对当前 identity、Session、owner、conversation 和 event 行为的代码/运行证据记录。
|
||||
|
||||
禁止:
|
||||
|
||||
- 新生产 handler;
|
||||
- Consumer fallback;
|
||||
- 改变 Runtime 生产行为;
|
||||
- 用空 DTO、stub 或 ignored test 假装协议已实现。
|
||||
|
||||
完成条件:
|
||||
|
||||
- 迁移矩阵中的 P0 inventory 均有证据;
|
||||
- 每条 `EG-*` 能区分“现状满足”“现状必须隔离”“待后续阶段实现”;
|
||||
- 当前 master 行为基线可重复通过。
|
||||
|
||||
### P1:最小持久协议底座
|
||||
|
||||
**人话目标**:实现 Shell 以后需要的请求受理、结果读回和 source binding 基础,但不开放五个 Public 写命令。
|
||||
|
||||
实现:
|
||||
|
||||
- 统一 Contract/schema 单一来源;
|
||||
- 以专用 append-only Shell ledger 为 authority 的 command acceptance/read-back、interaction/rework mapping 与 source-binding record;
|
||||
- 可从 ledger 重建的 projection journal/index;
|
||||
- RFC 8785 canonical fingerprint/checksum、连续 ledgerVersion、内容安全过滤和损坏隔离;
|
||||
- 专用 `.agent/runtime/supervisor-shell/` record namespace(不混入 `agent.db` 或 Runtime journal);
|
||||
- `TrustedProjectContext` resolver、execution-owner guard 内的 Shell lock 与调用来源基础设施。
|
||||
|
||||
P1 的实现边界:Public DTO 始终无路径,但 Shell 只能接收宿主已解析、已核验 manifest 的 trusted project context;现有 `.agent/project.lock` 具有 PID/时间回收语义,不能作为 Shell protocol lock。`.agent/runtime/supervisor-shell/ledger.jsonl` 是唯一追加顺序 authority,sidecar/index 只是可重建缓存。P1 交付的是持久协调底座和 read-back,不把当前 Runner 内存 request cache 当作幂等证据,也不开放 Public 写入口。
|
||||
|
||||
不实现:
|
||||
|
||||
- 新 Public 写入口;
|
||||
- Consumer 迁移;
|
||||
- 第二份 task/finalization/provider/steer 生命周期。
|
||||
|
||||
### P2:Public / Developer Snapshot 与事件流
|
||||
|
||||
**人话目标**:先让 Consumer 能通过一个稳定接口看懂系统,而不改变旧写行为。
|
||||
|
||||
实现:
|
||||
|
||||
- Public Snapshot;
|
||||
- Developer Snapshot;
|
||||
- Snapshot revision/hash;
|
||||
- Public/Developer 隔离的 `(projectId, view)` Snapshot 订阅、`SnapshotChanged` 有界事件和重连;
|
||||
- read-only/shadow projection;
|
||||
- User/Developer interaction view 的只读物化;
|
||||
- 带 source identity/revision/digest witness 与正常缺失/损坏矩阵的有界 projection observation:能确定 project/view scope 而观察无法闭合时,发布无 capability、无未证实 Runtime 事实的 fail-closed invalid/reconciliation Snapshot;完全不能确定安全 outcome 时返回 read error,而不挑一份跨文件旧读结果继续。
|
||||
- 为每项协作执行持久化 opaque `collaborationId` binding;同组多 child、retry successor 与 manifest fallback replacement 不依赖动态 child identity;无 parent run 的静态 fallback 绑定 project/session/manifest digest/group,不能承载可操作 interaction。
|
||||
|
||||
旧 GUI/CLI 仍保留写路径;shadow 只能比较投影,不能执行真实副作用。P2 不能把当前跨 `runtime.json`、JSONL、stream 和 sidecar 的聚合读取结果直接序列化为 Public Snapshot。
|
||||
|
||||
### P3:五命令与统一 Interaction Loop
|
||||
|
||||
**人话目标**:让后端具备完整、真实可用的统一写协议,并将新旧正式入口收进同一 Shell ingress。
|
||||
|
||||
实现:
|
||||
|
||||
- 五个 Public 命令及 strict schema;
|
||||
- requestId 幂等、业务拒绝读回、unknown outcome;
|
||||
- submit intent policy matrix;
|
||||
- answer/approve/cancel/resume 状态机;
|
||||
- Public Conversation read adapter;
|
||||
- legacy ingress 的单 writer 收口。
|
||||
|
||||
P3 不提前迁移 GUI/CLI 的读模型和界面体验,但必须先收口真实 writer:任何仍能操作同一 Supervisor Run 的旧 Tauri/CLI/`swarm_cli` wrapper 都要转发同一 Shell handler(或在新协议启用时明确禁用),不得先在 Consumer 进程写 Runtime 再通知 Runner。P5 只迁移 Consumer 的读与交互体验。
|
||||
|
||||
### P4:Runner 自驱与安全恢复
|
||||
|
||||
**人话目标**:已受理操作不依赖 GUI 轮询推进;Runner 在现有 owner/lifecycle 门禁内完成 wake、恢复和 reconciliation。
|
||||
|
||||
实现:
|
||||
|
||||
- 已接受 operation 的 durable wake/discovery;
|
||||
- dirty projection 修复;
|
||||
- Runner 重启后的安全恢复与跨重启 project discovery registry;
|
||||
- drain、owner 冲突和 GUI-owner/CLI 启动路径的区分;
|
||||
- outcome-unknown 零自动真实副作用重放;
|
||||
- watchdog 强退视为 crash 边界,而非已完成的 drain。
|
||||
|
||||
本阶段不新增 headless lease,也不承诺无人值守常驻。
|
||||
|
||||
### P5:迁移 CLI、Tests、GUI
|
||||
|
||||
**人话目标**:只切换 Consumer,不新增协议语义。
|
||||
|
||||
顺序:
|
||||
|
||||
1. 普通 Supervisor CLI;
|
||||
2. 面向 Public Contract 的测试;
|
||||
3. GUI;
|
||||
4. Developer UI/CLI 的独立 read 边界。
|
||||
|
||||
迁移后:
|
||||
|
||||
- Consumer 只读 Snapshot/Conversation,只提交五命令;
|
||||
- GUI 不再解释 Runtime phase、合成启动决策或 autosave Runtime output;
|
||||
- CLI 不再直接调用 start/steer/resume primitive;
|
||||
- 内部 Runtime 单测和恢复测试仍可直接测试内部能力。
|
||||
|
||||
P5 前产品决策 `FD-001`:`--swarm-chat` 必须明确选择为只读 Public view 的普通 Supervisor CLI,或显式受信任的 Developer CLI;无论选择哪种,其正式写操作均不得绕过 Shell。
|
||||
|
||||
### P6:删除旧公开面并最终收口
|
||||
|
||||
**人话目标**:删除已经没有正式 Consumer 的旧公开控制协议,同时保留 Runtime 内部能力和必要回归测试。
|
||||
|
||||
删除:
|
||||
|
||||
- 正式 transport 的旧 start/steer/confirm/reject/answer/cancel/retry/resume/schedule/read 注册;
|
||||
- Consumer 旧调用点和生命周期分支;
|
||||
- GUI Runtime output 派生 autosave;
|
||||
- migration fallback;
|
||||
- Public scope 内缺少稳定 messageId 的 conversation append。
|
||||
|
||||
保留:
|
||||
|
||||
- Runtime 内部 start/steer/resume/recovery primitive;
|
||||
- 验证内部能力的单元和恢复测试;
|
||||
- 独立管理面 goal/compact/session/config;
|
||||
- 明确隔离的 Developer/local history。
|
||||
|
||||
---
|
||||
|
||||
## 7. 里程碑
|
||||
|
||||
| 里程碑 | 对应阶段 | 产出 | 允许进入下一阶段的条件 |
|
||||
|---|---|---|---|
|
||||
| M0 | P0 | 基线、调用图、证据门禁 | 现状与隔离边界可证明 |
|
||||
| M1 | P1 | 最小持久底座 | 原子性、损坏、幂等基础测试通过 |
|
||||
| M2 | P2 | 双 Snapshot 与事件 | source 缺失/损坏、fail-closed 发布、协作 lineage、订阅重连/缺口、权限和字段隔离通过 |
|
||||
| M3 | P3 | 五命令、Interaction、Conversation read | crash/read-back、单 writer、跨 transport fixture 通过 |
|
||||
| M4 | P4 | Runner wake/recovery | owner、drain、重启、unknown outcome 通过 |
|
||||
| M5 | P5 | CLI/Tests/GUI 迁移 | 三类 Consumer 行为等价且无私有字段依赖 |
|
||||
| M6 | P6 | 旧公开面删除 | 静态调用图和最终协议验收通过 |
|
||||
|
||||
阶段完成条件必须引用 Contract `IC-*`、迁移矩阵 `MX-*` 和证据门禁 `EG-*`;本文不重复字段级验收。
|
||||
|
||||
---
|
||||
|
||||
## 8. 冻结与开发准入
|
||||
|
||||
当前允许:
|
||||
|
||||
- 继续评审和收束四份文档;
|
||||
- 编写不改变生产行为的 P0 基线与 fixture 骨架;
|
||||
- 解决 `FD-001` 产品决策。
|
||||
|
||||
当前不允许:
|
||||
|
||||
- 开始 P1–P6 生产实现;
|
||||
- 因实现方便而修改 Contract 语义;
|
||||
- 根据旧评论恢复 owner generation、Session rotation、handoff 或 Consumer fallback;
|
||||
- 把 P0 证据任务解释为“以后再决定协议规则”。
|
||||
|
||||
允许进入 P1 的前提:
|
||||
|
||||
1. P0/M0 已完成:当前 master 行为基线可重复通过,正式 ingress/read/writer inventory 已形成,且每条后续 `EG-*` 已标记现状、隔离边界和责任阶段;
|
||||
2. Contract 中没有未标注的候选字段、重复定义或互相冲突的 `IC-*`;
|
||||
3. 迁移矩阵覆盖所有正式 ingress、read、conversation writer 和删除面;
|
||||
4. 附录中的冻结前 `EG-*` 有明确预期结果;
|
||||
5. P1 的 trusted project resolver、owner-guard Shell lock、RFC 8785 canonicalization、专用 Shell record namespace 已明确为单一实现边界;
|
||||
6. `FD-001` 可在 P5 开始前决定,不阻塞 Contract 核心冻结或 P0–P4;
|
||||
7. PR #168 完成针对四份文档职责和 Contract 可施工性的重新评审。
|
||||
|
||||
---
|
||||
|
||||
## 9. 最小心智模型
|
||||
|
||||
```text
|
||||
Consumer 只表达意图、读取状态;
|
||||
Shell 统一交互决策和正式写入口;
|
||||
Runtime 继续保存和推进执行事实;
|
||||
Runner 只在持有现有 owner 时写;
|
||||
Snapshot 是状态视图,事件是刷新提示;
|
||||
Conversation 从原 source 回读,不复制正文;
|
||||
不知道副作用结果时停止并 reconciliation,不重复执行。
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
# 【技术方案】角色设定 → 多方向角色图 → 拆分 → 动画生成
|
||||
|
||||
- 日期:2026-08-12
|
||||
- 状态:设计稿 v3(简化:唯一新增能力是"从角色设定生成多向角色图",拆分与动画全走现有能力)
|
||||
- 实现状态:**核心已实现**(多向图生成:后端 `character-sheet` kind + 前端角色面板开关,2026-08-12)
|
||||
- 方向固定为**八方向**(上、下、左、右 + 四角,围绕一圈排布,3×3 九宫格中心留空);提示词按八向围绕一圈约束排布,降低拆分粘连概率。
|
||||
- 拆分:复用既有「拆分图集」的「连通块定位 + 外接矩形截图」算法(`slice_generated_icon_spritesheet_all_by_connected_components`),不新增像素级连通块掩码;切出的单视角切片按 `icon` 落库(`split_editor_icon_spritesheet`),不改 `character`。
|
||||
- 关联:`【技术方案】ArtAgent多模态Spine序列帧动画生成-2026-08-10.md`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
- Spine 序列帧动画(`character-animation`)是"图生视频 → 抽帧",图生视频以首帧为准;首帧里角色越多,输出多角色概率越大。
|
||||
- 现状角色形象以多视角规范图为参考,可能产出"一个角色多个视角"的多角色图,直接当动画首帧会多角色。
|
||||
- 结论:需要从角色设定**能生成一张"多方向角色图"**(同一角色、多方向、彼此分离),拆成单张后分别做动画。
|
||||
|
||||
## 2. 范围
|
||||
|
||||
**唯一新增能力**:从角色设定(角色规范图 / 已有单视角角色图)生成一张**多方向角色图**(八方向:上、下、左、右 + 四角,围绕一圈排布)。
|
||||
|
||||
**其余全部复用现有能力,不做额外保障:**
|
||||
- 图集类型:复用现有 `icon-spritesheet`(「图集」),不新增资产类型。
|
||||
- 拆分:复用现有「拆分图集」(`split_editor_icon_spritesheet`,连通块定位 + 外接矩形截图),**手动触发**,不自动拆分。
|
||||
- 方向/数量:**不保证**。生成错了用户重新抽卡;方向标签用户自己看。
|
||||
- 动画:对拆分出的单视角图,按现有动画面板逐个生成。
|
||||
|
||||
## 3. 流程
|
||||
|
||||
```
|
||||
角色设定(规范图 / 已有角色图)
|
||||
│ 参考
|
||||
▼
|
||||
【新增】生成多方向角色图 → 产出 icon-spritesheet 图集
|
||||
│
|
||||
▼
|
||||
【现有】用户手动「拆分图集」→ N 张单视角图(按 icon 落库)
|
||||
│
|
||||
▼
|
||||
【现有】选单视角图 → 图生视频 → 抽帧 → Spine序列帧
|
||||
```
|
||||
|
||||
## 4. 新增能力:生成多方向角色图
|
||||
|
||||
- 入口:在「生成角色形象」面板底部,作为「多方向图」可勾选项,与「像素艺术」并列;勾选后本次生成走 `character-sheet` 图集模式。
|
||||
- 链路:复用图集生成链路(`generate_editor_icon_spritesheet_for_owner`),模型 `nanobanana2`。
|
||||
- 输出:一张 `icon-spritesheet` 图集,八个方向(正面、右前、右侧、右后、背面、左后、左侧、左前),按 3×3 九宫格围绕一圈排布、中心留空,同一角色、方向之间纯色底充分间隔、彼此分离。
|
||||
- 提示词是质量关键(草案):
|
||||
|
||||
```
|
||||
根据角色设定生成游戏角色的八方向视图图集。
|
||||
画面中只有同一个角色,共八个方向:正面、右前、右侧、右后、背面、左后、左侧、左前,每个方向一个完整角色。
|
||||
八个方向必须是同一个角色,脸型、发型、服装、配色、武器、头身比、体型全程一致,禁止漂移、换装、变形。
|
||||
八个方向按三乘三九宫格围绕一圈排布:上、下、左、右以及四个角落各放一个方向,九宫格中心留空,不放置角色。
|
||||
每个方向角色完整居中,不裁切头顶、手脚或武器;各方向底部基线对齐,画面占比一致。
|
||||
背景为单一纯色(绿幕 #00FF00),平整无纹理、无渐变、无阴影、无地面、无道具;角色之间由纯色背景充分间隔,互不接触、互不重叠,便于后续拆分。
|
||||
禁止额外角色、分身、宠物、文字、水印、边框、编号、网格线。
|
||||
角色设定:{}
|
||||
```
|
||||
|
||||
- 参考归属校验:复用 `resolve_editor_reference_object_key_for_owner` / `ensure_editor_reference_object_key_owned`,禁止跨账号 objectKey。
|
||||
|
||||
## 5. 使用流程(全部现有)
|
||||
|
||||
1. 生成多方向角色图 → 得到 `icon-spritesheet` 图集图层。
|
||||
2. 选中图集 → 点「拆分图集」(现有按钮)→ 得到若干单视角图。
|
||||
3. 对每个想用的方向,选对应单视角图 → 动画面板生成(默认右视图起步)。
|
||||
|
||||
## 6. 边界
|
||||
|
||||
- 生成质量靠提示词;图不对 → 用户重新抽卡,**不做数量/方向校验**。
|
||||
- 外接矩形若粘连(方向贴太近)→ 拆分数量不对 → 重新抽卡或人工处理,不自动兜底;提示词以「围绕一圈 + 纯色背景充分间隔」降低粘连概率。
|
||||
- 成本:多向图一次图像生成;拆分 0 provider 成本;每个方向动画各一次(现有计价)。
|
||||
- 旧链路(直接拿单视角角色图做动画)不变。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
1. 从角色设定能生成一张多方向角色图,各方向为同一角色。
|
||||
2. 该图可作为图集手动拆分,拆出的单视角图能进动画面板生成单角色序列帧。
|
||||
3. 旧链路回归不破。
|
||||
|
||||
## 8. 决策点(留给产品)
|
||||
|
||||
- 方向数已定**八方向**(上、下、左、右 + 四角,围绕一圈;不再提供四视角选项)。
|
||||
- 入口已定:角色面板底部「多方向图」可勾选项,与「像素艺术」并列。
|
||||
- 多向图背景色(当前固定绿幕 #00FF00)。
|
||||
|
||||
## 9. 改动文件
|
||||
|
||||
- 前端:`ImageCanvasGenerationModel.ts`(多向图提示词/常量)、`ImageCanvasGenerationDialogModel.ts`、`ImageCanvasGenerationSubmissionModel.ts`、`ImageCanvasCharacterGenerationComposerView.tsx`(底部「多方向图」勾选项)、`ImageCanvasGenerationImageOptionsView.tsx`(`extraToggles` 插槽)。
|
||||
- 后端:`editor_project.rs`(`build_editor_character_multi_view_sheet_prompt` 八向围绕一圈提示词)。
|
||||
- 无需动拆分逻辑、动画逻辑、存储层;切片按 `icon` 落库,不加 `character` 分支。
|
||||
@@ -1,432 +0,0 @@
|
||||
# AI 游戏创作 Agent Runtime 交互边界证据与决策附录
|
||||
|
||||
> 文档角色:代码事实、设计决策、反例与证据门禁
|
||||
> 状态:持续维护;不能覆盖 Interaction Contract
|
||||
> 规范来源:[`【技术协议】AI游戏创作Agent Runtime交互合同V1-2026-08-17.md`](./【技术协议】AI游戏创作Agent%20Runtime交互合同V1-2026-08-17.md)
|
||||
> 迁移入口:[`【迁移方案】AI游戏创作Agent Runtime交互边界迁移矩阵-2026-08-17.md`](./【迁移方案】AI游戏创作Agent%20Runtime交互边界迁移矩阵-2026-08-17.md)
|
||||
|
||||
## 0. 使用边界
|
||||
|
||||
本附录只保存三类内容:
|
||||
|
||||
- `EV-*`:当前代码已经能直接证明的事实;
|
||||
- `DR-*`:明确采用或拒绝的设计决策及其理由;
|
||||
- `EG-*`:证明实现满足 `IC-*` 的测试/调用图门禁。
|
||||
|
||||
代码变化可能使 `EV-*` 过期;此时必须更新 evidence 和迁移矩阵。不得为了适应过期代码事实而静默放宽 Contract。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前代码事实
|
||||
|
||||
### 1.1 Session 与 conversation
|
||||
|
||||
#### EV-SESSION-001:Session catalog 按 Agent 持久化
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/project/conversation.rs`
|
||||
- 关键对象/函数:Agent Session catalog、`read_game_creator_agent_session_catalog_at` 一类 catalog 读写函数。
|
||||
- 事实:catalog 保存 `schemaVersion + agentId + activeSessionId + sessions`;不是项目级全 Agent catalog。
|
||||
- 事实:当前没有独立 `sessionRevision`;不能把候选 digest 写回或解释为第二 revision。
|
||||
- 约束:支持 `IC-ID-002`、`IC-ID-003`;对应 `MX-ID-002`、`MX-ID-003`。
|
||||
|
||||
#### EV-SESSION-002:Live task 阻止 Session mutation
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/project/conversation.rs`
|
||||
- 关键函数:`ensure_agent_session_has_no_live_tasks`、create/fork/set-active/archive Session 路径。
|
||||
- 事实:Agent 有未终结 Runtime task 时,create/fork/archive/set-active 会被拒绝。
|
||||
- 约束:V1 不能用新协议绕过该语义,也不能默认具备 live handoff。
|
||||
|
||||
#### EV-CONV-001:Conversation 支持有 identity 和无 identity append
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/commands.rs`
|
||||
- 关键函数:Tauri command `append_local_conversation_message`。
|
||||
- 事实:`messageId: Option<String>`;有值走 idempotent append,无值走普通 append。
|
||||
- 风险:Public cutover 后无 identity append 会破坏去重、source mapping 和完整性证明。
|
||||
- 约束:支持 `IC-CONV-010`;对应 `MX-CONV-012`~`MX-CONV-014`。
|
||||
|
||||
#### EV-CONV-002:Conversation 正文已有持久 source
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/project/conversation.rs`
|
||||
- 关键函数:`append_local_conversation_message_for_session_at`、`append_local_conversation_message_for_session_idempotent_at`、带 finalization 的幂等 append。
|
||||
- 事实:已有 project conversation 与 Agent Session conversation;Public adapter 无需复制正文。
|
||||
- 约束:支持 `IC-CONV-002`。
|
||||
|
||||
### 1.2 Project owner、Runner 与 CLI
|
||||
|
||||
#### EV-OWNER-001:OS lock 是当前 project execution owner
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/runner/project_owner.rs`
|
||||
- 事实:`.agent/runtime/execution-owner.lock` 通过平台 OS 文件锁实现排他;Windows/Unix 分别有安全打开与文件类型校验。
|
||||
- 约束:支持 `IC-OWNER-001`;拒绝新增平行 owner authority。
|
||||
|
||||
#### EV-OWNER-002:Owner JSON 和 bootId 是诊断信息
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/runner/project_owner.rs`、`runner/state.rs`、`runner/protocol.rs`。
|
||||
- 事实:诊断 record 描述 owner/boot,但真正写入排他来自 lock handle。
|
||||
- 风险:按 JSON、mtime、本地时钟或 bootId generation 接管会形成第二 authority。
|
||||
|
||||
#### EV-OWNER-003:现有 project 写锁不是 execution owner
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/project/filesystem.rs`。
|
||||
- 事实:`.agent/project.lock` 用 create-new 文件和 PID/时间/mtime stale reclaim;它服务现役项目写操作,不由 Runner execution owner guard 定义。
|
||||
- 约束:不能把它直接解释为 `IC-OWNER-002` 的 supervisor project lock;Shell 必须在真实 owner 下另行串行。
|
||||
|
||||
#### EV-OWNER-004:进程内 Runtime 当前只持局部锁
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs`、`runner/state.rs`。
|
||||
- 事实:production execution owner 的获取在 Runner state;未启用 Runner 的恢复主要依赖 Agent task/run lock。
|
||||
- 约束:P1/P3 进程内 Shell 必须在任何 record/projection/Runtime 写之前补同一 OS owner-lock 获取,不能把局部锁当等价 owner。
|
||||
|
||||
#### EV-RUNNER-001:Runner 已有内部 Runtime RPC
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs`
|
||||
- 当前方法包括:`runtime.resume`、`runtime.continue_action`、`runtime.steer`、`runtime.interrupt_for_steer_decision`、`runtime.pause`、`runtime.cancel`、`runtime.compact`、`runtime.wake_pending`。
|
||||
- 事实:这些是现有 Runner 内部控制能力,不能因新 Shell 再作为平行 Public 协议保留。
|
||||
- 约束:支持 `IC-CMD-001`、`IC-CMD-010`;对应 `MX-ING-006`。
|
||||
|
||||
#### EV-CLI-001:CLI 可无 GUI 启动/连接受限 Runner
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/cli.rs` 与 Runner client/server 路径。
|
||||
- 事实:Runtime 写入要求显式项目外 `--config-dir` 并可启动 External Runner;普通 CLI Runner 路径不要求 GUI-owner。
|
||||
- 约束:V1 保留该终端会话能力,不新增 headless lease,也不承诺无人值守常驻。
|
||||
|
||||
#### EV-CLI-002:CLI/`swarm_cli` 当前仍直接调用内部能力
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/cli.rs`、`src-tauri/src/swarm_cli/turn_dispatch.rs`。
|
||||
- 事实:`AgentSteer` 路径调用 `steer_game_creator_agent_runtime_task_at`;`swarm_cli` 可 dispatch Runtime turn 并直接 append user/assistant conversation。
|
||||
- 约束:支持 `MX-ING-004`、`MX-ING-005`、`MX-CONV-002`。
|
||||
|
||||
#### EV-RUNNER-002:request 去重与已知项目均是进程内状态
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/runner/state.rs`、`runner/protocol.rs`、`runner/dispatch.rs`。
|
||||
- 事实:`write_request_cache` 和 `known_roots` 都在 Runner 内存;重启后 cache 无法提供 request result read-back,Runner 也不能仅凭自身发现此前项目。
|
||||
- 约束:P1 durable command record 不能复用该 cache;P4 的自驱恢复必须增加受信任的跨重启候选项目发现,且每个候选仍重取 owner、重读 durable evidence。
|
||||
|
||||
### 1.3 Runtime final reply 与 response stream
|
||||
|
||||
#### EV-FINAL-001:messageId 与 finalizationId 不是同一 identity
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/finalization.rs`
|
||||
- 事实:final reply `messageId` 由 Agent/Session/Run 派生;`finalizationId` 还绑定 response fingerprint、revision、request slot、steer cursor、plan 和 Goal fingerprint。
|
||||
- 约束:支持 `IC-CONV-005`;Public 去重 key 不能反推 finalization。
|
||||
|
||||
#### EV-FINAL-002:Response stream 有独立 tuple
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/response_stream.rs`
|
||||
- 事实:stream 使用 taskId/sessionId/runId/requestSlot/responseRevision/appliedSteerCursor;`streaming → ready → committed` 会合法改变 status/sequence。
|
||||
- 约束:status/sequence 不能被放入“不可变 source identity digest”。
|
||||
|
||||
#### EV-FINAL-003:Response stream 不能独自证明 committed
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream.rs`
|
||||
- 事实:部分 streaming sidecar 写错误被忽略;publisher 可在 char 上限处截断;dirty 状态不能替代写后回读。
|
||||
- 约束:支持 `IC-IDEMP-004`、`IC-CONV-005`;必须交叉验证 finalization 和 conversation lifecycle。
|
||||
|
||||
#### EV-FINAL-004:成功后 recovery sidecar 会删除
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs` 与 `runtime_protocol/finalization.rs`。
|
||||
- 事实:成功路径完成 conversation assistant、Runtime completed、response committed 后会清理 finalization/provider/tool-plan handoff recovery sidecar。
|
||||
- 约束:历史正文必须从 conversation messageId 回读,不能假定 sidecar 永久存在。
|
||||
|
||||
#### EV-PROJECTION-001:现有 Runtime 聚合读取不是原子观察点
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/entrypoints.rs`、`agent/runtime_state.rs`。
|
||||
- 事实:读取会依次组合 runtime state、task/event JSONL、response stream 和 sidecar;写入跨文件,state rename 与 journal append 各有独立锁/时刻。
|
||||
- 约束:P2 不能直接把该聚合结果包装为 Public Snapshot;必须建立带 identity/revision/digest witness 的 observation,无法闭合即 fail closed。
|
||||
|
||||
### 1.4 RuntimeStatus
|
||||
|
||||
#### EV-STATUS-001:根 Supervisor status 落在 project conversation
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs`
|
||||
- 关键函数:`append_game_creator_agent_runtime_public_status_message_at`。
|
||||
- 事实:messageId 由 agentId/sessionId/runId/status correlation 派生,但 append 时 `agent_id=None`、`session_id=None`。
|
||||
- 约束:进入 `(projectId, sessionId)` Public history 前必须显式保存 correlation mapping,不能从文件 scope 猜 Session。
|
||||
|
||||
#### EV-STATUS-002:专业 Agent terminal status 落在其 Session conversation
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs`
|
||||
- 关键函数:`append_game_creator_agent_runtime_terminal_public_message_at`。
|
||||
- 事实:非根 Supervisor 的 terminal status 使用其 `agentId/sessionId` 幂等 append。
|
||||
- 约束:历史消息保持原 agent/session/run,不重归属到 Supervisor 当前 Session。
|
||||
|
||||
#### EV-STATUS-003:部分 Supervisor continuation 不写 Session status
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs`。
|
||||
- 事实:有 parent agent/run 的 Supervisor receipt 或 isolated-join continuation 为避免重复 formal project chat,terminal path直接返回,不写第二 Session message。
|
||||
- 约束:这类 status 默认不进入 Public Conversation;不能假设每个 Run 有同构 status source。
|
||||
|
||||
#### EV-STATUS-004:Accepted start status 只覆盖特定根 Supervisor
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs`。
|
||||
- 事实:`requires_public_start_status` 只对无 parent 的根 Project Supervisor 且非 receipt/join source 生效。
|
||||
- 约束:P0 必须按类型而非泛化 “RuntimeStatus” 建 fixture。
|
||||
|
||||
### 1.5 Runtime event
|
||||
|
||||
#### EV-EVENT-001:普通 eventId 不可恢复
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs`
|
||||
- 关键函数:`new_game_creator_agent_runtime_event_id`。
|
||||
- 事实:无 actionId 时使用 `pid + unixMillis + process-local sequence + eventType`;重启/重试没有稳定规范 key。
|
||||
- 约束:支持 `IC-CONV-007` 默认拒绝。
|
||||
|
||||
#### EV-EVENT-002:带 actionId 的 event 只覆盖部分路径
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs`
|
||||
- 关键函数:`append_game_creator_agent_runtime_event_with_action`。
|
||||
- 事实:有 actionId 时可按 run/eventType/phase/actionId 形成较稳定 identity 和重复检查;普通 append 仍生成新 eventId。
|
||||
- 约束:即使 action event 较稳定,也必须同时满足 reader、digest、scope 和重放条件才能显式登记 Public。
|
||||
|
||||
#### EV-EVENT-003:现有 reader 吞坏行并截断最近 20 条
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs`、`src-tauri/src/main.rs`。
|
||||
- 关键函数/常量:`read_recent_game_creator_agent_runtime_events_for_session`、`AGENT_RUNTIME_RECENT_EVENT_LIMIT = 20`。
|
||||
- 事实:JSON 解析失败直接跳过;成功记录只返回最后 20 条。
|
||||
- 约束:该 reader 只能支持当前 GUI recent display,不能作为 `IC-CONV-007` 的 Public source reader。
|
||||
|
||||
#### EV-EVENT-004:Event record 自带公开正文
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs`。
|
||||
- 事实:allowlist event 写入 `publicText`;正文不一定存在于 conversation message。
|
||||
- 约束:若未来接入,PublicEvent 是 source-record projection 例外,不能复制成普通 assistant conversation。
|
||||
|
||||
### 1.6 GUI writer 与跨 scope 落盘
|
||||
|
||||
#### EV-GUI-001:GUI 为 Runtime event 生成第二 identity
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx`。
|
||||
- 事实:GUI 以 `game-chat-runtime-event:${eventId}` 构造聊天 message,并同时聚合 Supervisor 和直接 child events。
|
||||
- 约束:该 identity 不能进入规范 Public history。
|
||||
|
||||
#### EV-GUI-002:GUI 为 final reply 生成派生 identity
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src/features/project-workspace/SupervisorChatOnlyView.tsx`。
|
||||
- 事实:GUI 使用 `game-chat-final-reply:*` 构造 final 聊天 message。
|
||||
- 约束:应归一到现有 finalization conversation `messageId`。
|
||||
|
||||
#### EV-GUI-003:派生消息会 autosave 到 project conversation
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src/App.tsx`。
|
||||
- 事实:全局 messages autosave 可用 `agentId=null` 调用 `append_local_conversation_message`;child event 的 source Session 与实际 project transcript scope 不同。
|
||||
- 约束:支持 `MX-CONV-010`、`MX-CONV-011`;历史跨 scope 项默认隔离。
|
||||
|
||||
#### EV-GUI-004:普通 Agent chat 直接 append
|
||||
|
||||
- 代码:`apps/ai-game-creator-shell/src/App.tsx`、`features/app-shell/useDeveloperAgentPanel.ts`。
|
||||
- 事实:普通 Agent user/assistant、错误回复和 Developer panel user message 存在直接 append 调用。
|
||||
- 约束:writer cutover 必须覆盖全部调用方,而不是只覆盖 Runtime output 双写。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计决策
|
||||
|
||||
### DR-001:采用统一 Supervisor Shell
|
||||
|
||||
- 决定:正式 Consumer 只读状态、表达意图;Shell 统一交互决策。
|
||||
- 原因:GUI/CLI/测试当前存在重复且不一致的生命周期判断。
|
||||
- Contract:`IC-ARC-001`~`IC-ARC-004`。
|
||||
|
||||
### DR-002:拒绝平行 Runtime authority
|
||||
|
||||
- 拒绝:让 request ledger、projection ledger 或 Public Snapshot 自己决定 task/finalization/provider 成功。
|
||||
- 原因:现有 Runtime facts 跨多个 record,当前没有可复用的全局事务;平行状态会漂移。
|
||||
- Contract:`IC-ARC-004`、`IC-IDEMP-003`、`IC-READ-004`。
|
||||
|
||||
### DR-003:拒绝 Session rotation 与 handoff
|
||||
|
||||
- 拒绝:ActiveSessionIndex、live Session rotation、handoff manifest、continuation set、rotation fence、session control lease。
|
||||
- 原因:现有 catalog 明确禁止 live task 时切换;新增能力需要跨 task/conversation/finalization/interaction 的迁移和 rollback authority,超出本轮交互边界重构。
|
||||
- Contract:`IC-ID-002`、`IC-ID-003`。
|
||||
|
||||
### DR-004:拒绝全局 owner generation/lease
|
||||
|
||||
- 拒绝:用 boot generation、诊断 JSON、lease expiry 或本地时间替代 OS lock。
|
||||
- 原因:会建立第二 owner authority,并在 pause/时钟漂移/文件残留时产生双 writer。
|
||||
- Contract:`IC-OWNER-001`。
|
||||
|
||||
### DR-005:采用 Snapshot + 有范围的事件提示
|
||||
|
||||
- 决定:Snapshot 是完整 Public read;event 只提示重新读取。Public 与 Developer 事件流按 `(projectId, view)` 隔离,订阅原子取得完整初始 Snapshot。
|
||||
- 原因:Consumer 本地合并不能可靠处理缺口、重连和跨 source 更新;全局或按路径过滤的 event 会泄露/混淆多项目状态。
|
||||
- Contract:`IC-READ-001`、`IC-READ-004`、`IC-EVT-001`~`IC-EVT-003`。
|
||||
|
||||
### DR-005A:fail-closed 也是可发布状态
|
||||
|
||||
- 决定:source observation 无法闭合但仍能确定 project/view scope 时,发布无 capability、无未证实 Runtime 事实的 `failClosed` Snapshot;完全不能确定安全 outcome 时返回 read error。
|
||||
- 原因:若 invalid 状态不推进 revision/hash/event,Consumer 会永久保留一份已失效的 valid Snapshot;保留旧 capability 会绕过失败关闭。
|
||||
- Contract:`IC-READ-001`、`IC-READ-004`、`IC-CAP-002`。
|
||||
|
||||
### DR-005B:协作实体有独立持久 identity
|
||||
|
||||
- 决定:Public collaborator 使用 durable `collaborationId` binding,不从 group 或动态 child Agent identity 临时拼接;retry/successor 延续该 ID,manifest fallback 被 Runtime binding 替换。
|
||||
- 原因:同组多 child、重试与 isolated 执行都不能由单一 group/agentId 稳定代表,且 Public 不得泄露真实 child identity。
|
||||
- Contract:`IC-ID-003`、`IC-READ-001`、`IC-INT-002`。
|
||||
|
||||
### DR-006:采用五命令,不公开内部 primitive
|
||||
|
||||
- 决定:`submit_intent/answer/approve/cancel/resume` 是唯一 Public 写集合。
|
||||
- 原因:Consumer 表达用户意图,不选择 Runtime primitive。
|
||||
- Contract:`IC-CMD-001`~`IC-CMD-010`。
|
||||
|
||||
### DR-007:Conversation 不复制正文
|
||||
|
||||
- 决定:Public 只建无正文 source index,从既有 conversation/event source 回读。
|
||||
- 原因:复制会建立第二正文 authority,并放大 GUI/CLI 双写。
|
||||
- Contract:`IC-CONV-002`。
|
||||
|
||||
### DR-008:PublicEvent 默认拒绝
|
||||
|
||||
- 决定:普通现有 event 不进入永久 Public history;只有显式登记且满足全部 identity/reader/digest/scope 条件的类型才可接入。
|
||||
- 原因:现有普通 eventId 不可恢复,reader 截断且吞坏行,GUI 还会再造 identity。
|
||||
- Contract:`IC-CONV-007`。
|
||||
|
||||
### DR-009:RuntimeStatus 按具体 source 准入
|
||||
|
||||
- 决定:不把 RuntimeStatus 泛化为所有 Run 的同构 conversation source。
|
||||
- 原因:根 Supervisor、专业 Agent 和 receipt/join 的实际落盘行为不同。
|
||||
- Contract:`IC-CONV-006`。
|
||||
|
||||
### DR-010:结果未知时停止而非重放
|
||||
|
||||
- 决定:Provider/工具/Runtime 副作用可能发生但不可证明时进入 outcome-unknown/reconciliation。
|
||||
- 原因:换 requestId 或 fallback 会产生重复真实副作用。
|
||||
- Contract:`IC-IDEMP-004`、`IC-MIG-002`。
|
||||
|
||||
### DR-011:保留 CLI 无 GUI 的受限 Runner 能力
|
||||
|
||||
- 决定:不以 GUI-owner 门禁删除现有 CLI 会话期间启动/连接 Runner 的能力。
|
||||
- 原因:普通 CLI 是协议平等 Consumer,headless 能力是前端逻辑是否泄漏的重要验收。
|
||||
- Contract:`IC-OWNER-001`。
|
||||
|
||||
### DR-012:`--swarm-chat` 产品定位仍需显式决定
|
||||
|
||||
- 决策 ID:`FD-001`。
|
||||
- 可选:普通 Public Supervisor CLI;或显式受信任 Developer CLI。
|
||||
- 不可选:无 capability 时静默读取私有字段;Developer write 绕过 Shell。
|
||||
- Contract 不变量:`IC-MIG-004`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 冻结前证据门禁
|
||||
|
||||
同一个 `EG-*` 跨多个阶段时,状态按 `EG-ID@P阶段` 独立记录:某阶段 PR 只需关闭属于该阶段的子门禁,后续阶段的未实现证据不阻塞前一阶段完成;最终门禁只有在全部子门禁关闭后才整体完成。后续实现若推翻已关闭证据,必须重新打开对应子门禁。下文“阶段”按顺序对应各阶段必须提供的证据,不得以一个阶段的局部通过冒充整项关闭。
|
||||
|
||||
### EG-BASE-001:当前行为基线
|
||||
|
||||
- 对应:全部 P0。
|
||||
- 要求:确定性 Provider/进程内 Runtime 记录 submit、等待、批准、取消、恢复、终态与副作用计数;归一化随机 ID/时间。
|
||||
- 失败处理:阻塞迁移比较,不改变 Contract。
|
||||
|
||||
### EG-SCHEMA-001:Strict wire fixture
|
||||
|
||||
- 对应:`IC-WIRE-001`、`IC-WIRE-002`。
|
||||
- 要求:`@P0` 定义全部 DTO 的 schema/golden/negative 向量和预期结果;`@P1` 实现 Rust→TypeScript 生成/校验,覆盖未知/重复/错误字段、tagged union、大小、Unicode scalar/UTF-8 byte、Public 零路径/私有字段。
|
||||
- 阶段:P0(规范向量)/P1(实现与通过);`@P1` 阻塞 P2/P3。
|
||||
|
||||
### EG-ID-001:身份与 Session
|
||||
|
||||
- 对应:`IC-ID-001~005`。
|
||||
- 要求:projectId/path mismatch;opaque catalog digest;live-task create/fork/archive/set-active 拒绝;Supervisor 与 collaborator 交叉 Session mutation;历史 delivery 不重归属。
|
||||
- 阶段:P0/P2/P3。
|
||||
|
||||
### EG-OWNER-001:Owner 与执行位置
|
||||
|
||||
- 对应:`IC-OWNER-001~002`、`IC-CMD-010`。
|
||||
- 要求:External Runner 下所有正式 writer 实际在 owner Runner;进程内测试持等价 owner;双 Runner、drain、失锁、GUI-owner 丢失、CLI 启动回归。
|
||||
- 阶段:P0/P3/P4/P5。
|
||||
|
||||
### EG-STORE-001:Shell durable 底座
|
||||
|
||||
- 对应:`IC-ID-001`、`IC-OWNER-001~002`、`IC-DUR-001~003`、`IC-IDEMP-001~005`。
|
||||
- 要求:trusted project resolver 的缺失/manifest mismatch;进程内与 Runner 对同一 project owner 互斥;`.agent/project.lock` stale reclaim 不参与 Shell 互斥;RFC 8785 向量;专用 Shell ledger 的连续 ledgerVersion、tail repair/中间损坏隔离、atomic write、回读、checksum、派生 index 重建与 Runner crash 后 read-back。
|
||||
- 阶段:P1;阻塞 P2/P3。
|
||||
|
||||
### EG-PROJECTION-001:Projection observation
|
||||
|
||||
- 对应:`IC-READ-001~004`、`IC-EVT-001~003`、`IC-CAP-001~003`。
|
||||
- 要求:读 manifest/catalog/state/task/event/stream/Shell binding/projection journal 期间并发变化;source witness 变化的有界 retry;验证 normal/required absence、损坏和 conflict;验证 failClosed 完整固定向量及 manifest identity 不可证明时 read error;逐类 capability issuance/撤销;首次 revision1/sequence0。
|
||||
- 阶段:P2;阻塞 P2 完成与 P3 capability 依赖。
|
||||
|
||||
### EG-CMD-001:Request 幂等与崩溃读回
|
||||
|
||||
- 对应:`IC-IDEMP-001~005`、`IC-CMD-001~010`。
|
||||
- 要求:同 request 同/异 fingerprint、并发重复、业务拒绝重放、prepared/executing/succeeded 各 crash point、Runner 强杀、unknown outcome 零重复副作用。
|
||||
- 阶段:P1/P3/P4。
|
||||
|
||||
### EG-INT-001:Interaction 状态机
|
||||
|
||||
- 对应:`IC-INT-001~007`。
|
||||
- 要求:identity/revision/response replay;Public interaction capability 与 allowedActions 精确一致;User/Developer audience;question/option/freeform;target set;artifact digest;requestChanges 唯一 rework;Resolving crash recovery;collaboration binding 的 restart 重建、retry successor/parent lineage/source/group 漂移旧 interaction stale、manifestFallback 不可操作。
|
||||
- 阶段:P2/P3。
|
||||
|
||||
### EG-READ-001:Snapshot 与事件
|
||||
|
||||
- 对应:`IC-READ-001~004`、`IC-EVT-001~003`、`IC-CAP-001~003`。
|
||||
- 要求:Public/Developer `(projectId, view)` 路由隔离、原子 initial Snapshot、snapshot-first/no-backlog、duplicate/out-of-order/gap/reconnect;Public failClosed 固定向量、Developer source failure 统一 read error;revision/hash/排序/size canonical vectors,Public/Developer 超限均返回固定 read error 且无 partial DTO;全部 capability 正反签发与 cancel run revision/builtin Session/retry policy guards;bootstrap collaborators 为空、parentRun multi-child、retry lineage、fallback replacement、stale cancel 复核。
|
||||
- 阶段:P2。
|
||||
|
||||
### EG-CONV-001:Final reply source
|
||||
|
||||
- 对应:`IC-CONV-005`。
|
||||
- 要求:messageId/finalizationId/stream tuple 唯一性;streaming→ready→committed;publisher 写失败/截断;sidecar 清理后 conversation 回读;Provider 调用计数。
|
||||
- 阶段:P0/P3/P4。
|
||||
|
||||
### EG-CONV-002:Status source
|
||||
|
||||
- 对应:`IC-CONV-006`。
|
||||
- 要求:根 Supervisor start/terminal、专业 Agent terminal、receipt/isolated join 无 source 三类分别验证;project status correlation 缺失/冲突失败关闭。
|
||||
- 阶段:P0/P3。
|
||||
|
||||
### EG-CONV-003:Event 默认隔离与准入
|
||||
|
||||
- 对应:`IC-CONV-007`。
|
||||
- 要求:枚举规范 action identity 与普通 pid/time identity call site;普通 event 未进入 Public chain;如接入某类型,必须通过 eventId 定位、坏行/截断报告、digest、scope 和重放 fixture。
|
||||
- 阶段:P0/P1/P3;不通过只阻塞该 event type 接入,不阻塞默认隔离方案。
|
||||
|
||||
### EG-CONV-004:Writer cutover
|
||||
|
||||
- 对应:`IC-CONV-010`。
|
||||
- 要求:全部 `append_local_conversation_message` 调用方三选一;GUI final/event autosave 停止;同 source 单 writer;Public scope 无缺少 messageId append。
|
||||
- 阶段:P0/P3/P5/P6。
|
||||
|
||||
### EG-CONV-005:Cursor 与历史完整性
|
||||
|
||||
- 对应:`IC-CONV-008`、`IC-CONV-009`。
|
||||
- 要求:origin/tail、分页、永久 sequence 空洞、duplicate、非法 cursor、index/source/digest/correlation 损坏、无 partial page、session-lifetime cursor。
|
||||
- 阶段:P3/P5/P6。
|
||||
|
||||
### EG-MIG-001:跨 Consumer golden replay
|
||||
|
||||
- 对应:`IC-ARC-001`、`IC-MIG-001~004`。
|
||||
- 要求:GUI、普通 CLI、进程内测试和 Runner transport 对同一输入产生等价 request/result/Snapshot/conversation 语义和副作用计数。
|
||||
- 阶段:P3/P5/P6。
|
||||
|
||||
|
||||
### EG-INGRESS-001:P3 正式写入口与 writer cutover
|
||||
|
||||
- 对应:`IC-CMD-010`、`IC-CONV-010`、`IC-MIG-005`。
|
||||
- 要求:legacy Tauri/CLI/`swarm_cli`/helper 的正式写入口全部转发同一 Shell handler 或禁用;Public Conversation adapter 启用前所有 writer 已接管、隔离或停止;cutover watermark 后零旧正式 writer、零派生双写。
|
||||
- 阶段:P3;阻塞 P3 完成与 P5 Consumer 迁移。
|
||||
|
||||
### EG-RUNNER-001:P4 跨重启发现与安全恢复
|
||||
|
||||
- 对应:`IC-OWNER-001~002`、`IC-IDEMP-004`、`IC-MIG-006`。
|
||||
- 要求:trusted discovery registry 的注册/删除/损坏/权限 fixture;Runner 重启后重新解析 manifest、重取 owner、按 durable evidence wake/reconcile/no-op;未知真实副作用零自动重放。
|
||||
- 阶段:P4;阻塞 P4 完成。
|
||||
|
||||
### EG-DEL-001:旧公开面删除
|
||||
|
||||
- 对应:`IC-MIG-003`。
|
||||
- 要求:正式 invoke handler、transport、Consumer、Public DTO 不再引用旧协议;内部 primitive 与回归测试仍存在;Preview/resource/session 管理面未误删。
|
||||
- 阶段:P6。
|
||||
|
||||
---
|
||||
|
||||
## 4. 证据更新规则
|
||||
|
||||
1. `EV-*` 只能由代码读取、定向测试或运行证据支持;README/旧设计声明不能单独成为事实。
|
||||
2. 代码与 `EV-*` 冲突时先更新 evidence 和迁移矩阵;若冲突使 `IC-*` 不可实现,再提交 Contract 变更评审。
|
||||
3. `EG-*` 失败的默认处理是隔离、阻塞阶段或进入 reconciliation,不是增加 Consumer fallback。
|
||||
4. 每个 P0–P6 PR 必须列出所实现的 `IC-*`、受影响 `MX-*` 和关闭的 `EG-ID@P阶段`;不得把跨阶段门禁标记为提前整体完成。
|
||||
5. 本附录不保存密钥、Token、绝对本地私密路径、Provider 原文、会话记录或构建产物。
|
||||
@@ -1,161 +0,0 @@
|
||||
# AI 游戏创作 Agent Runtime 交互边界迁移矩阵
|
||||
|
||||
> 文档角色:把 Interaction Contract 映射到当前代码、阶段和验收证据
|
||||
> 状态:P0 inventory;矩阵不得修改 `IC-*` 语义
|
||||
> 总览入口:[`【技术方案】AI游戏创作Agent Runtime交互边界重构实施计划-2026-08-12.md`](./【技术方案】AI游戏创作Agent%20Runtime交互边界重构实施计划-2026-08-12.md)
|
||||
> 规范来源:[`【技术协议】AI游戏创作Agent Runtime交互合同V1-2026-08-17.md`](./【技术协议】AI游戏创作Agent%20Runtime交互合同V1-2026-08-17.md)
|
||||
> 证据来源:[`【设计依据】AI游戏创作Agent Runtime交互边界证据与决策附录-2026-08-17.md`](./【设计依据】AI游戏创作Agent%20Runtime交互边界证据与决策附录-2026-08-17.md)
|
||||
|
||||
## 0. 使用规则
|
||||
|
||||
每一行包含:
|
||||
|
||||
```text
|
||||
当前 source/入口
|
||||
→ 适用 IC 规则
|
||||
→ 当前差距
|
||||
→ 唯一迁移动作
|
||||
→ 阶段
|
||||
→ 完成证据
|
||||
```
|
||||
|
||||
状态值:
|
||||
|
||||
- `baseline`:现状能力,尚未迁移;
|
||||
- `isolate`:不满足 Public Contract,默认隔离;
|
||||
- `adapt`:复用现有事实并通过 Shell Adapter 接入;
|
||||
- `replace-consumer`:后端能力就绪后替换 Consumer;
|
||||
- `remove-public`:P6 删除公开注册/调用;
|
||||
- `decision`:需要显式产品决定,但不得改变 Contract。
|
||||
|
||||
---
|
||||
|
||||
## 1. 正式 ingress 与执行位置
|
||||
|
||||
| MX ID | 当前入口/source | 当前事实 | Contract | 目标动作 | 阶段 | 状态/证据 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| MX-ING-001 | GUI Supervisor chat,`apps/ai-game-creator-shell/src/App.tsx` 与 `SupervisorChatOnlyView.tsx` | GUI 仍参与 start/steer、状态合并和输出同步 | `IC-ARC-002`、`IC-CMD-003`、`IC-CMD-004` | GUI 只提交 capability 中的 `submit_intent`,不选择 disposition | P5 | `replace-consumer`;GUI 调用图无 Runtime primitive |
|
||||
| MX-ING-002 | 普通 Agent chat,`apps/ai-game-creator-shell/src/App.tsx` | user/assistant 直接 append,并调用内部 Agent 能力 | `IC-CMD-010`、`IC-CONV-010`、`IC-MIG-005` | P0 归类;正式路径在 P3 接 Shell、开发路径在 P3 隔离;P5 只清理旧 Consumer 分支 | P0/P3/P5 | 全调用图、ingress cutover 与 UI 清理 |
|
||||
| MX-ING-003 | Tauri commands,`apps/ai-game-creator-shell/src-tauri/src/commands.rs` | 暴露旧 Runtime 和 conversation write wrapper | `IC-OWNER-001`、`IC-CMD-010`、`IC-MIG-005` | P3 先转发同一 Shell endpoint;旧注册 P6 删除 | P3/P6 | transport fixture + invoke handler 静态检查 |
|
||||
| MX-ING-004 | CLI commands,`apps/ai-game-creator-shell/src-tauri/src/cli.rs` | `AgentSteer` 等路径直接调用 Runtime primitive;CLI 可启动受限 Runner | `IC-CMD-001`、`IC-CMD-010`、`IC-MIG-005` | P3 先收口为 Shell transport;P5 再迁 Public CLI read/UX;保留无 GUI 启动 Runner 能力 | P3/P5 | CLI golden replay;无直接 start/steer/resume |
|
||||
| MX-ING-005 | `--swarm-chat`,`cli.rs` 与 `swarm_cli/turn_dispatch.rs` | 读取专业 Agent 状态并直接 dispatch/append | `IC-ARC-005`、`IC-CMD-010`、`IC-MIG-004`、`IC-MIG-005` | P3 先让正式写走 Shell 或禁用;P5 按 `FD-001` 选择 Public/Developer read 呈现 | P3/P5 | Shell writer fixture;`decision`;Public/Developer DTO 零交叉 |
|
||||
| MX-ING-006 | Runner `runtime.*` RPC,`src-tauri/src/runner/dispatch.rs` | 已有 resume/steer/cancel/pause/compact 等内部 RPC,request cache 仅内存 | `IC-CMD-001`、`IC-CMD-010`、`IC-IDEMP-001~005` | 仅作为 Shell 内部实现/委托 Shell;不得把 cache 当 durable read-back | P1/P3/P6 | crash 后同 requestId read-back + dispatch 调用图 |
|
||||
| MX-ING-007 | 进程内测试 transport | 可绕过 External Runner 直接调用实现 | `IC-OWNER-001`、`IC-CMD-010` | 复用同一 Shell handler,并在任何 Shell/Runtime 写前取得同一 OS owner lock;per-Agent lock 不等价 | P1/P3 | 同 fixture 跨 Runner/进程内 replay |
|
||||
| MX-ING-008 | Runtime 内部 wake/recovery | timer/lane/schedule/owner recovery 不属于用户意图;Runner known roots 当前仅在内存 | `IC-CMD-009`、`IC-ARC-004`、`IC-MIG-006` | 保持内部 recovery intent,不导出为 Public resume;P4 建跨重启候选项目发现 | P4/P6 | 静态 Public DTO 检查、重启 discovery 与恢复测试 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Read model 与 Consumer 决策
|
||||
|
||||
| MX ID | 当前 read/source | 当前事实 | Contract | 目标动作 | 阶段 | 状态/证据 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| MX-READ-001 | manifest/Session catalog/Runtime state/task/Shell binding 读取 | GUI/CLI 分别解释 status/phase;缺少完整 project/session/binding witness | `IC-READ-001`、`IC-READ-004`、`IC-ARC-002` | Shell 按完整 source dependency matrix 形成 valid/failClosed/read-error,稳定投影 status/stage/waitingOn/nextStep | P1/P2/P5 | manifest/catalog/binding/journal、bootstrap、active-task 缺失、损坏、跨 Consumer Snapshot fixture |
|
||||
| MX-READ-002 | pending action、user-input、tool confirmation sidecar | 当前由不同 UI/CLI 分流;无 open interaction 可缺失 | `IC-READ-004`、`IC-CAP-003`、`IC-INT-001`~`IC-INT-007` | P2 只读物化稳定 Interaction,并生成与 interactionId/revision 一致的 response capability;required sidecar 缺失/损坏则 fail-closed,P3 接管 answer/approve | P2/P3 | identity/revision/audience/capability/required-source fixture |
|
||||
| MX-READ-003 | response stream | 是 optional Runtime final-reply 实时/恢复辅助,写错误可能被忽略 | `IC-READ-004`、`IC-CONV-005`、`IC-IDEMP-004` | 只作为短期 source evidence;不能单独证明 committed;完成证明依赖它时损坏/缺失 fail-closed | P0/P2/P3 | optional、写失败、截断、ready→committed fixture |
|
||||
| MX-READ-004 | GUI Runtime state/event merge | Consumer 自行拼接多个 source | `IC-READ-001`、`IC-EVT-001` | P5 删除 normalize/merge 决策,只渲染 Snapshot | P5 | 前端类型/调用图检查 |
|
||||
| MX-READ-005 | Tauri best-effort update event | 不提供按 project/view scope 可靠补读历史 | `IC-EVT-001`~`IC-EVT-003` | 替换为按 `(projectId, view)` 路由的 snapshot-first subscription;V1 不补历史 event,缺口/重连均重读完整 Snapshot | P2 | 首次 revision1/sequence0、Public/Developer 隔离、重复/乱序/缺口/重连 fixture |
|
||||
| MX-READ-006 | Developer Agent panel | 可读私有 Agent 状态并直接操作 | `IC-ARC-005`、`IC-READ-003`、`IC-MIG-005` | P2 建独立 Developer DTO;正式 Supervisor 写在 P3 走五命令,Developer-local 写在 P3 隔离;P5 只清理旧 UI 分支 | P2/P3/P5 | 未授权拒绝、ingress cutover、Public 字段零泄漏 |
|
||||
| MX-READ-007 | Preview/resource/session 管理面 | 独立现役合同 | `IC-ARC-002`、`IC-MIG-003` | 保持 sibling contract,不从 Snapshot nextStep 重造 | P5/P6 | 调用图证明未误删 |
|
||||
| MX-READ-008 | command capability 投影 | 当前 Consumer 由 status/phase 自行判断按钮 | `IC-CAP-001~003`、`IC-READ-004` | 按 Contract issuance matrix 从完整 witness 必签/撤销 submit、interaction、cancel、resume 与 Developer reconcile capability | P2/P3 | 每类 capability 正反状态、witness 漂移、Public/Developer audience fixture |
|
||||
|
||||
---
|
||||
|
||||
## 3. 身份、Session 与 owner
|
||||
|
||||
| MX ID | 当前 source | 当前事实 | Contract | 目标动作 | 阶段 | 状态/证据 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| MX-ID-001 | 项目 manifest/路径 | manifest 有 project identity;transport 大量使用绝对路径定位,尚无 projectId 反向 root registry | `IC-ID-001` | Public 只传 projectId;宿主 resolver 产出并复核 `TrustedProjectContext`,locator 留在受信任边界 | P1/P2/P3 | path-free schema + resolver/subscription mismatch fixture |
|
||||
| MX-ID-002 | Session catalog,`project/conversation.rs` | 每 Agent 一份 catalog;无独立 revision;live task 禁止变更 | `IC-ID-002`、`IC-ID-003` | 对 Project Supervisor catalog 计算 opaque digest;不写回、不扩权 | P0/P2/P3 | live-task 与跨 Agent Session fixture |
|
||||
| MX-ID-003 | collaborator/child history | 各自具有 agentId/sessionId/runId,但无稳定公开协作实体 | `IC-ID-003` | 首次 binding durable 分配 collaborationId;retry/successor 保持 ID,fallback 被 Runtime binding 替换;无 parent run 的 fallback 绑定 project/session/manifest digest/group;不使用 Supervisor 当前 Session 重新归属 | P2/P3 | parentRun multi-child、retry lineage、fallback replacement、交叉 mutation fixture |
|
||||
| MX-OWNER-001 | `.agent/runtime/execution-owner.lock` | OS 排他锁是真正 owner;现仅 Runner production path 获取 | `IC-OWNER-001`、`IC-OWNER-002` | 直接复用;进程内 Shell 也必须在写前取得同一实现,不新增 generation/lease | P0/P1/P4 | 双 Runner、进程内冲突、drain、失锁测试 |
|
||||
| MX-OWNER-002 | `.agent/project.lock` | create-new 文件锁按 PID/时间/mtime reclaim,不是 owner | `IC-OWNER-002` | 不可作为 Shell protocol lock;保留其现役业务用途 | P1 | stale reclaim 与 Shell lock 分离 fixture |
|
||||
| MX-OWNER-003 | `execution-owner.json` 与 bootId | 仅诊断/实例关联 | `IC-OWNER-001` | 保持私有诊断,不用于接管/CAS | P0/P4 | 时间/mtime/诊断冲突 negative fixture |
|
||||
| MX-OWNER-004 | GUI-owner watchdog | GUI 启动 Runner 时的生命周期门禁 | `IC-OWNER-001` | 保留 GUI-owner 路径;不扩张为 CLI control lease | P4 | GUI-owner 丢失与 drain fixture |
|
||||
| MX-OWNER-005 | CLI `--config-dir` Runner | 当前可无 GUI 启动/连接受限 Runner | `IC-OWNER-001` | 保留现有终端会话能力;不承诺常驻 | P4/P5 | CLI Runner 回归 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Command 与现有 Runtime identity
|
||||
|
||||
| MX ID | Public command | 现有内部能力/source | Contract | Adapter 要求 | 阶段 | 证据 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| MX-CMD-001 | `submit_intent` DirectReply | CLI Reply/Execute kernel、conversation append | `IC-CMD-003`~`IC-CMD-005`、`IC-CONV-004` | 预分配 user/assistant messageId,先 user commit 再 reply | P3 | crash-point + same request replay |
|
||||
| MX-CMD-002 | `submit_intent` Start | Runtime start/pending/task/status | `IC-CMD-004`、`IC-CMD-005` | 绑定 input envelope、现有 task/run/status identity | P3 | user→status→queued crash fixture |
|
||||
| MX-CMD-003 | `submit_intent` Steer | 现有 V1.13 steer ledger | `IC-CMD-004`、`IC-CMD-005` | prepared 时绑定 steerId/cursor;不复制 steer 生命周期 | P3 | same-run、重复和 deferred fixture |
|
||||
| MX-CMD-004 | `answer` | user-input sidecar/answer primitive | `IC-CMD-006`、`IC-INT-001~007` | 物化稳定 interaction,按 response/revision 解决 | P2/P3 | option/freeform/stale/replay fixture |
|
||||
| MX-CMD-005 | `approve` | tool/policy confirm 与 reject primitive | `IC-CMD-007`、`IC-INT-001~007` | audience/policy/target set/artifact binding 锁内复核 | P2/P3 | approve/reject/requestChanges matrix |
|
||||
| MX-CMD-006 | `cancel` | Runtime cancel primitive | `IC-CMD-008` | 精确 Session/Run/revision;唯一 cancel operation;不伪造终态 | P3 | cancel revision/state matrix |
|
||||
| MX-CMD-007 | `resume` ContinueRun | paused Run resume | `IC-CMD-009` | 同 Run + expected revision | P3 | paused/running/waiting/finalizing negative fixture |
|
||||
| MX-CMD-008 | `resume` RetryTerminalRun | terminal retry/successor lineage | `IC-CMD-009` | 唯一 successor runId;保存 predecessor/source identity;绑定 terminal revision 与 retry policy digest | P3 | policy drift、concurrent retry + crash fixture |
|
||||
| MX-CMD-009 | `resume` ReconcileRun | 受信任 reconciliation | `IC-CMD-009`、`IC-IDEMP-004` | 只读/修复已知事实,不重放未知副作用 | P3/P4 | Developer capability + provider/tool count |
|
||||
| MX-CMD-010 | Goal replacement | 现有 replacement primitive | `IC-CMD-004` | 仅显式 Goal management operation;不由普通 execute intent 触发 | P3 | frozen Goal Contract negative fixture |
|
||||
|
||||
---
|
||||
|
||||
## 5. Conversation source 与 writer cutover
|
||||
|
||||
| MX ID | Source/writer | 当前事实 | Contract | 目标动作 | 阶段 | 状态/证据 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| MX-CONV-001 | Session user message | 现有 conversation 正文 source | `IC-CONV-002`、`IC-CONV-004` | 复用正文;Public index 只保存 source metadata | P3 | source digest/read-back fixture |
|
||||
| MX-CONV-002 | DirectReply | `swarm_cli`/GUI 可直接 append user+assistant | `IC-CONV-004`、`IC-CONV-010` | Shell 接管稳定 identity 和写入顺序 | P3/P5 | 零重复 user/assistant |
|
||||
| MX-CONV-003 | RuntimeFinalReply | finalization + response stream + conversation | `IC-CONV-005` | 保存三层 identity binding;长期正文从 conversation 回读 | P3 | sidecar 清理后历史回读 |
|
||||
| MX-CONV-004 | 根 Supervisor start/terminal status | 稳定 messageId 但正文写 project conversation | `IC-CONV-006` | 以 task/run correlation 显式绑定 Supervisor Session | P0/P3 | correlation 缺失/冲突 fixture |
|
||||
| MX-CONV-005 | 专业 Agent terminal status | 写其 Agent Session conversation | `IC-CONV-006` | 按该 agent/session/run 回读,不重归属 | P0/P3 | session scope fixture |
|
||||
| MX-CONV-006 | receipt/isolated join status | 当前不写 Session status message | `IC-CONV-006` | 默认不进入 Public Conversation | P0 | `isolate` negative fixture |
|
||||
| MX-CONV-007 | 普通 Runtime event | eventId 依赖 pid/时间/进程计数 | `IC-CONV-007` | V1 默认隔离 | P0 | `isolate`;call site inventory |
|
||||
| MX-CONV-008 | action-identity event | 部分 event 可按 action identity 幂等 | `IC-CONV-007` | 仅在规范 reader/digest/scope 全闭合后显式登记 | P0/P1/P3 | 默认 `isolate`;event replay fixture |
|
||||
| MX-CONV-009 | recent-events reader | 静默跳过坏行,只返回最近 20 条 | `IC-CONV-007`、`IC-CONV-009` | 不作为 Public source reader;若接 event 必须补新 reader | P0/P1 | 损坏/截断/定位 fixture |
|
||||
| MX-CONV-010 | GUI final autosave | `response-stream → game-chat-final-reply:* → autosave` | `IC-CONV-005`、`IC-CONV-010` | P3 adapter 启用前停止正式写入;P5 只删除旧消费/展示分支 | P3/P5 | cutover watermark 后零派生 writer;GUI 调用图清理 |
|
||||
| MX-CONV-011 | GUI event autosave | `Runtime event → game-chat-runtime-event:* → project conversation` | `IC-CONV-007`、`IC-CONV-010` | P3 adapter 启用前停止正式写入并隔离历史跨 scope 项;P5 清理旧 UI 分支 | P0/P3/P5 | source scope inventory + cutover 后零派生 writer |
|
||||
| MX-CONV-012 | 普通 Agent chat append | `App.tsx` user/assistant 可无 messageId append | `IC-CONV-010` | P3 前将正式 Supervisor 接 Shell、Developer/local 显式隔离;P5 只清理旧 Consumer 分支 | P0/P3/P5 | writer 三选一清单 + cutover fixture |
|
||||
| MX-CONV-013 | Developer panel append | Developer user history 直接写 | `IC-ARC-005`、`IC-CONV-010` | P3 前标记 Developer-local 且永不进入 Public,或接正式 Shell;P5 清理旧调用面 | P0/P3/P5 | DTO/调用面隔离 + cutover fixture |
|
||||
| MX-CONV-014 | project pending-message autosave | 项目级 conversation writer | `IC-CONV-010` | P3 adapter 启用前接 stable source binding 或停止;P5 只删除旧 Consumer 分支 | P0/P3/P5 | writer cutover fixture |
|
||||
| MX-CONV-015 | Public Conversation cursor | 当前无统一永久 source index | `IC-CONV-002`、`IC-CONV-008`、`IC-CONV-009` | P3 建无正文 index、origin/tail/cursor chain | P3 | 分页、空洞、损坏、全量补读 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Public / Developer 字段边界
|
||||
|
||||
| MX ID | 数据 | 当前风险 | Contract | 动作 | 阶段 | 证据 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| MX-DATA-001 | project path / LocalConversationResult.path | GUI/CLI 可读本地路径 | `IC-ID-001`、`IC-READ-002` | Public DTO 零 path;local transport 单独返回 | P2/P5/P6 | schema/static check |
|
||||
| MX-DATA-002 | Provider、tool、observation | Developer/runtime records 含私有原文 | `IC-READ-002`、`IC-ERR-003` | Public 严格白名单;Developer 仍脱敏有界 | P2 | sensitive fixture |
|
||||
| MX-DATA-003 | dynamic child identity | GUI 可聚合专业/child Runtime | `IC-ID-003`、`IC-READ-002` | Public Snapshot/event/error/capability 只显示 durable collaborationId/组摘要;真实 child agent/session/parentRun/run/delegation identity 只留 private binding | P2/P5 | parentRun multi-child、retry、fallback replacement、Public zero-leak、权限 fixture |
|
||||
| MX-DATA-004 | interaction private prompt/policy | sidecar 可能含原始模型内容 | `IC-INT-005`、`IC-ERR-003` | 生成独立 Public presentation;不安全则 Developer/reconciliation | P2 | redaction fixture |
|
||||
| MX-DATA-005 | finalization/provider identity | 恢复和调试需要,正式 UI 不需要 | `IC-CONV-005`、`IC-READ-002` | 保留 private binding,Public message 仅 provenance allowlist | P3 | Public schema zero-leak |
|
||||
|
||||
---
|
||||
|
||||
## 7. P6 删除清单
|
||||
|
||||
| MX ID | 删除范围 | 保留范围 | Contract | 完成证据 |
|
||||
|---|---|---|---|---|
|
||||
| MX-DEL-001 | 正式 transport 旧 start/steer/confirm/reject/answer/cancel/retry/resume/schedule/read 注册 | Runtime 内部 primitive | `IC-MIG-003` | handler/route 静态检查 |
|
||||
| MX-DEL-002 | GUI/CLI 旧生命周期判断和 fallback | Public Consumer + Developer read | `IC-ARC-002`、`IC-MIG-002` | Consumer 调用图 |
|
||||
| MX-DEL-003 | GUI Runtime output 派生 autosave | 原 conversation/finalization/event source | `IC-CONV-010` | writer cutover + 零 duplicate |
|
||||
| MX-DEL-004 | Public DTO 的 path/finalization/provider/private fields | 受信任本地/Developer DTO | `IC-READ-002`、`IC-READ-003` | schema diff |
|
||||
| MX-DEL-005 | Public scope 无稳定 messageId append | 明确 Developer/local history | `IC-CONV-010` | 所有 append caller 已分类 |
|
||||
| MX-DEL-006 | migration unknown-command fallback | 内部回归测试 | `IC-MIG-002`、`IC-MIG-003` | transport fixture |
|
||||
|
||||
---
|
||||
|
||||
## 8. 当前冻结前缺口
|
||||
|
||||
| ID | 缺口 | 性质 | 阻塞阶段 |
|
||||
|---|---|---|---|
|
||||
| FD-001 | `--swarm-chat` 是普通 Public Supervisor CLI 还是显式 Developer CLI | 产品兼容决策 | P5 产品绑定/呈现;不阻塞 Contract 核心冻结 |
|
||||
| GAP-001 | 全部 legacy Runtime ingress 的实际写入进程调用图尚未形成正式 artifact | P0 evidence | P3 |
|
||||
| GAP-002 | 全部 conversation append writer 的接管/隔离/禁用归类尚未闭合 | P0 evidence | P3/P5 |
|
||||
| GAP-003 | event type/call site identity inventory 尚未形成正式 artifact | P0 evidence | PublicEvent 接入;默认隔离不受阻 |
|
||||
| GAP-004 | 按 eventId 定位、报告坏行/截断、校验 digest 的 reader 尚不存在 | implementation gap | PublicEvent 接入;默认隔离不受阻 |
|
||||
| GAP-005 | Rust→TypeScript strict schema/golden fixture 尚未实现;冻结前只定义规范与向量 | P1 implementation | P2/P3 |
|
||||
| GAP-006 | `projectId → TrustedProjectContext` 的受信任宿主 resolver 尚未实现 | P1 implementation | P2/P3;Public DTO 始终保持无路径 |
|
||||
| GAP-007 | 现有 `.agent/project.lock` 具有 stale reclaim,不能当 Shell protocol lock | P1 implementation boundary | P1;须与 execution owner 下串行分离 |
|
||||
| GAP-008 | RFC 8785 canonicalization 尚无单一复用实现 | P1 implementation | P1;checksum/fingerprint/hash 不可各自序列化 |
|
||||
| GAP-009 | Shell record 尚无唯一 append order authority;不能由多份 sidecar 自行分配 ledgerVersion | P1 implementation | P1;建立专用 ledger,sidecar/index 只能派生 |
|
||||
| GAP-010 | Projection reader 尚无 witness、一致 observation、source absence/corruption matrix、fail-closed publication | P2 implementation | P2;不能直接公开现有聚合 read |
|
||||
| GAP-011 | Runner 的 known roots 与 request dedupe 都是内存态 | P3/P4 implementation | P3 durable read-back;P4 restart discovery |
|
||||
| GAP-012 | Snapshot subscription 尚无按 `(projectId, view)` 路由、durable sequence 与原子 initial Snapshot | P2 implementation | P2;V1 使用 snapshot-first/no-backlog,不能复用全局 best-effort event |
|
||||
| GAP-013 | collaborator/child 到 durable collaborationId 的 binding/lineage 尚不存在 | P2 implementation | P2;Public 不得临时以 agentId/组名拼接 identity |
|
||||
|
||||
这些缺口不得被解释为 Contract 规则未决定:除 `FD-001` 外,现状不满足即按 Contract 默认隔离或失败关闭。
|
||||
@@ -345,7 +345,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
|
||||
- Rust 结构体:`ExternalGenerationJob`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/external_generation.rs`
|
||||
- 现役覆盖:worker claim 只允许 `source_module = editor-canvas`;下述逐玩法生成和写回描述均为退役前历史。历史 pending / running 行继续保留原状态,不得领取、失败收口或改写 payload。
|
||||
- 用途:外部生成 worker 的内部持久任务队列;`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 时,`api-server` HTTP 角色只入队,`external-generation-worker` 角色通过 claim lease 领取、续租、执行,并用 `lease_token` 栅栏回写阶段、完成 / 失败。队列行继续保存 worker 执行、计费与滚动发布兼容所需字段,末尾可选 `phase` 只取 `generating / processing`;claim 写 `generating`,真实进入抠图处理时由受 `job_id + worker_id + lease_token` 保护的 procedure 写 `processing`。phase procedure 以结构化结果区分 `LeaseFencingRejected` 与 `OtherRejected`;`LeaseFencingRejected` 立即终止,`OtherRejected` 以及 SDK 的 `Procedure` / `Runtime` 错误不重试,只有 `Build` / `ConnectDropped` / `Timeout` 在同一个 job attempt 内重试一次。该重试只重新上报 phase,不把任务写回 `pending`,也不重新调用 provider;编辑器 job 入队固定 `max_attempts=1`,第二次传输失败后任务进入 `failed`,不会回到 `pending` 或从 provider 生成起点重跑。用户可见任务列表、价格、状态、阶段、未确认终态数量和通知确认时间的正式读取事实源已经迁到 `external_generation_job_summary`;BFF 不得再为列表 / 详情 / acknowledge 读取该大表。拼图 `compile_puzzle_draft` 的前置 `compile_puzzle_agent_draft`、`generate_puzzle_images` 与 `generate_puzzle_ui_background` 的业务写回也在对应 SpacetimeDB transaction 内校验 `job_id + worker_id + lease_token`、job kind、owner 和 source entity,避免过期 worker 写 session / work profile;图片画布编辑器的 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_video_generation`、`editor_sound_effect_generation` 和 `editor_background_music_generation` 复用同一队列表。结构化 canvas 激活后,当前 worker completion 先以读取时 canvas revision 执行 CAS,并发冲突时拒绝覆盖并保留可诊断失败;目标是进一步收口为受 lease 栅栏保护的单事务幂等写入 `editor_project_resource`、结果 `editor_canvas_layer`、`editor_canvas_generation_dialog` 终态和 canvas revision。未激活 canvas 在 2 MiB 上限内继续走 legacy `editor_canvas.layers_json` 兼容写回。前端只通过 BFF job 状态轮询和项目快照读取恢复完成态。`GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 时不创建该队列行,三个 external generation guard 字段必须同时为空才允许 api-server 受控同步写回,半空 guard 仍会拒绝。worker 成功写回业务事实后才能 complete job;业务失败态写回成功后才能 fail job,失败态未写回时保留租约等待后续重领。
|
||||
- 用途:外部生成 worker 的内部持久任务队列;`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 时,`api-server` HTTP 角色只入队,`external-generation-worker` 角色通过 claim lease 领取、续租、执行,并用 `lease_token` 栅栏回写阶段、完成 / 失败。队列行继续保存 worker 执行、计费与滚动发布兼容所需字段,末尾可选 `phase` 只取 `generating / processing`;claim 写 `generating`,真实进入抠图处理时由受 `job_id + worker_id + lease_token` 保护的 procedure 写 `processing`。phase procedure 以结构化结果区分 `LeaseFencingRejected` 与 `OtherRejected`;`LeaseFencingRejected` 立即终止,`OtherRejected` 以及 SDK 的 `Procedure` / `Runtime` 错误不重试,只有 `Build` / `ConnectDropped` / `Timeout` 在同一个 job attempt 内重试一次。该重试只重新上报 phase,不把任务写回 `pending`,也不重新调用 provider;编辑器 job 入队固定 `max_attempts=1`,第二次传输失败后任务进入 `failed`,不会回到 `pending` 或从 provider 生成起点重跑。用户可见任务列表、价格、状态、阶段、未确认终态数量和通知确认时间的正式读取事实源已经迁到 `external_generation_job_summary`;BFF 不得再为列表 / 详情 / acknowledge 读取该大表。拼图 `compile_puzzle_draft` 的前置 `compile_puzzle_agent_draft`、`generate_puzzle_images` 与 `generate_puzzle_ui_background` 的业务写回也在对应 SpacetimeDB transaction 内校验 `job_id + worker_id + lease_token`、job kind、owner 和 source entity,避免过期 worker 写 session / work profile;图片画布编辑器的 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_character_animation_video_conversion`、`editor_character_animation_background_removal`、`editor_video_generation`、`editor_sound_effect_generation` 和 `editor_background_music_generation` 复用同一队列表。结构化 canvas 激活后,当前 worker completion 先以读取时 canvas revision 执行 CAS,并发冲突时拒绝覆盖并保留可诊断失败;目标是进一步收口为受 lease 栅栏保护的单事务幂等写入 `editor_project_resource`、结果 `editor_canvas_layer`、`editor_canvas_generation_dialog` 终态和 canvas revision。未激活 canvas 在 2 MiB 上限内继续走 legacy `editor_canvas.layers_json` 兼容写回。前端只通过 BFF job 状态轮询和项目快照读取恢复完成态。`GENARRATIVE_EXTERNAL_GENERATION_MODE=inline` 时不创建该队列行,三个 external generation guard 字段必须同时为空才允许 api-server 受控同步写回,半空 guard 仍会拒绝。worker 成功写回业务事实后才能 complete job;业务失败态写回成功后才能 fail job,失败态未写回时保留租约等待后续重领。
|
||||
- 2026-08-06 收口覆盖:上一条用途描述中“当前先 CAS、单事务仍是目标”的旧句已作废。现役编辑器生成不再组合调用 object confirm、resource create、asset create、canvas save 和 job complete。`api-server` 只准备稳定候选,再经 `spacetime-client` 调用 `persist_editor_generation_result_and_return`;procedure 在同一 `try_with_tx` 内写入可选 `asset_object`、全部 `editor_project_resource`、`editor_asset`、可选 `asset_entity_binding`、可选 canvas V2 CAS、queue job 终态和 `editor_generation_operation` receipt。结构化 canvas 的 layer / dialog / revision 与未激活 canvas 的 legacy `layers_json` 仍经既有 V2 布局验证分流,前端不直接发明正式完成态。queue 首次提交在同一快照验证 owner、job kind、request fingerprint 和有效 `job_id + worker_id + lease_token`;统一 procedure 已完成 job 后 worker 不得再单独 complete。
|
||||
- 载荷约束:本次先对 `source_module = editor-canvas` 的 `request_payload_json` / `result_payload_json` 实施有限大小合法 JSON、任意层级禁止 `data:` / `blob:` 的双层门禁,只保存 worker 执行必需的普通参数和已登记媒体引用。画布 Agent 来源的任务可在 `result_payload_json.editor-agent-tool-call-result` 中保存有界的轻量结果和已登记媒体引用,供后端按已有 `externalJobId + owner_user_id` 定向懒回填;其它编辑器任务保持元数据结果,并可保存有界的 `warning.code/reason`。其它玩法在完成各自参考图资源化之前不由本次门禁静默改变既有请求契约。该主表只供 worker claim / 执行、受控维护以及画布 Agent 的定向结果回填读取;正式用户任务列表、单任务状态、队列概览与 acknowledge 不得返回或解析这两个 payload。画布 Agent 懒回填必须经对应工具 formatter 归一为有界轻量媒体引用后写入 OSS 会话,不能把原始 payload 直接透传前端。
|
||||
- 非阻断告警:角色形象、图标图集和 UI 素材提取已保存 provider 原图、但透明背景处理最终失败时,以原图唯一主图完成任务;透明图和切片不写入画布。这个 source-only 降级只包住透明背景处理的最终失败,phase 上报、provider 原图持久化、透明处理图持久化或画布写回失败仍按任务错误传播。图标 / UI 透明图集成功但自动拆分降级时仍保留透明图集;通用 `warning` 与 `sliceWarning` 只在「透明背景最终失败」这一条上互斥,风格归一化或像素规整产生的通用 `warning` 可与 `sliceWarning` 并存。两类成功降级都以既有 `completed` 状态收口,不新增状态值:source-only 的 inline / external v1 响应使用结构化 `warning.code/reason`,仅拆分失败的 inline / external v1 响应继续使用既有 `sliceWarning.code/reason`,其 `reason` 保留原始诊断;queue worker 才把两者归一为有界的 `result_payload_json.warning`:只有一条时原样保留完整 `reason`,两条并存时按“通用在前、拆分在后”拼接且 `code` 收敛为 `multiple-generation-warnings`(两条 `code` 相同则沿用原 `code`),不允许任何一条被丢弃;`sliceWarning.reason` 无论是否并存都由 worker 添加“图集已生成,但自动拆分未完成:”前缀,拼接结果最后统一做长度上界收敛。除上述画布 Agent 定向回填的轻量结果外,队列结果不保存图片、切片列表或媒体 URL。
|
||||
|
||||
@@ -68,7 +68,7 @@ Windows 本地如果已在 `%LOCALAPPDATA%\Genarrative\ffmpeg\bin` 安装 FFmpeg
|
||||
|
||||
开发态 `npm run dev` 与 `npm run dev:api-server` 都会注入 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true`,因此密码登录在本地开发环境可直接注册未知手机号账号。完整 `npm run dev` 会强制父 API 使用 `GENARRATIVE_PROCESS_ROLE=all`,忽略外层显式角色,确保本地 `api-server` 同时监听 HTTP 并消费外部生成队列;只有单模块 `npm run dev:api-server` 会保留显式 `GENARRATIVE_PROCESS_ROLE`,未设置时默认为 `all`。`all` 不内嵌 BgFilter worker;启动器总是先启动并验活独立 `GENARRATIVE_PROCESS_ROLE=bgfilter-worker` 进程,再启动父 API,并向两者注入同一个内部 base URL / Token。Linux 本地默认 `all` 角色启动前,dev 脚本会停止当前仓库、同一个 SpacetimeDB server / database 下遗留的 `GENARRATIVE_PROCESS_ROLE=external-generation-worker` 进程,避免旧 worker 二进制继续抢同一条队列并在业务写回时制造 procedure 超时;显式拆分 `api` / `external-generation-worker` 做生产式验证时不会触发这项清理。生产环境仍按 `api-server` 配置默认关闭密码自动注册,并由独立 worker 进程消费队列。
|
||||
|
||||
本地排查外部内容生成 worker 队列时,默认同一 Rust 进程同时监听 HTTP 并消费 `external_generation_job` 队列;更接近生产的验证应分别启动 `api`、`external-generation-worker` 和 `external-generation-controller`。生产默认 `GENARRATIVE_PROCESS_ROLE=api`,外部生成任务由独立 `GENARRATIVE_PROCESS_ROLE=external-generation-worker` 进程消费;生产与容器扩缩容验证保持 `queue`。当前 worker 只领取 `source_module = editor-canvas` 的图片画布任务,包括 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_video_generation`、`editor_sound_effect_generation` 和 `editor_background_music_generation`。旧玩法历史任务即使仍为 pending / running 也不领取、不改状态;显式把本地进程角色设为 `api` 且没有 worker 时,现役编辑器生成请求只返回 queued/running,不会兜底执行外部 provider。
|
||||
本地排查外部内容生成 worker 队列时,默认同一 Rust 进程同时监听 HTTP 并消费 `external_generation_job` 队列;更接近生产的验证应分别启动 `api`、`external-generation-worker` 和 `external-generation-controller`。生产默认 `GENARRATIVE_PROCESS_ROLE=api`,外部生成任务由独立 `GENARRATIVE_PROCESS_ROLE=external-generation-worker` 进程消费;生产与容器扩缩容验证保持 `queue`。当前 worker 只领取 `source_module = editor-canvas` 的图片画布任务,包括 `editor_image_generation`、`editor_image_edit`、`editor_background_removal`、`editor_icon_spritesheet_generation`、`editor_ui_design_asset_extraction`、`editor_character_animation_generation`、`editor_character_animation_video_conversion`、`editor_character_animation_background_removal`、`editor_video_generation`、`editor_sound_effect_generation` 和 `editor_background_music_generation`。旧玩法历史任务即使仍为 pending / running 也不领取、不改状态;显式把本地进程角色设为 `api` 且没有 worker 时,现役编辑器生成请求只返回 queued/running,不会兜底执行外部 provider。
|
||||
|
||||
HTTP 角色的 `GENARRATIVE_SPACETIME_POOL_SIZE` 只表示 procedure / reducer 调用池大小;池连接不订阅 read model。HTTP 角色会额外创建 1 条共享缓存读连接,当前只保留可选的 `user_account` 读取;配置为 `8` 时基础连接拓扑是 8 条调用连接加 1 条缓存读连接。`/readyz` 同时检查调用池与缓存读连接,缓存连接未准备好时不能放量。
|
||||
|
||||
|
||||
@@ -100,6 +100,82 @@ export function resolveSelectedToolbarStyle({
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCharacterAnimationBackgroundRemovalPanelStyle({
|
||||
sourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
}: {
|
||||
sourceLayer: CanvasLayer | null;
|
||||
viewport: CanvasViewport;
|
||||
canvasSize: CanvasSize;
|
||||
}): CanvasOverlayStyle | null {
|
||||
if (!sourceLayer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const panelWidth = 288;
|
||||
const panelHeight = 440;
|
||||
const gap = 12;
|
||||
const edge = 12;
|
||||
const sourceLeft = viewport.x + sourceLayer.x * viewport.scale;
|
||||
const sourceTop = viewport.y + sourceLayer.y * viewport.scale;
|
||||
const sourceWidth = sourceLayer.width * viewport.scale;
|
||||
const sourceHeight = sourceLayer.height * viewport.scale;
|
||||
const sourceRight = sourceLeft + sourceWidth;
|
||||
const sourceBottom = sourceTop + sourceHeight;
|
||||
const clampPanelLeft = (left: number) =>
|
||||
Math.min(Math.max(edge, left), Math.max(edge, canvasSize.width - panelWidth - edge));
|
||||
const clampPanelTop = (top: number) =>
|
||||
Math.min(Math.max(edge, top), Math.max(edge, canvasSize.height - panelHeight - edge));
|
||||
|
||||
const rightLeft = sourceRight + gap;
|
||||
if (rightLeft + panelWidth <= canvasSize.width - edge) {
|
||||
return { left: rightLeft, top: clampPanelTop(sourceTop) };
|
||||
}
|
||||
|
||||
const leftLeft = sourceLeft - gap - panelWidth;
|
||||
if (leftLeft >= edge) {
|
||||
return { left: leftLeft, top: clampPanelTop(sourceTop) };
|
||||
}
|
||||
|
||||
const bottomTop = sourceBottom + gap;
|
||||
if (bottomTop + panelHeight <= canvasSize.height - edge) {
|
||||
return {
|
||||
left: clampPanelLeft(sourceLeft + sourceWidth / 2 - panelWidth / 2),
|
||||
top: bottomTop,
|
||||
};
|
||||
}
|
||||
|
||||
const topTop = sourceTop - gap - panelHeight;
|
||||
if (topTop >= edge) {
|
||||
return {
|
||||
left: clampPanelLeft(sourceLeft + sourceWidth / 2 - panelWidth / 2),
|
||||
top: topTop,
|
||||
};
|
||||
}
|
||||
|
||||
// 极端缩放下四侧都放不下完整面板时,仍把面板放在可用空间最大的一侧,
|
||||
// 保证它不覆盖源画面;viewport 会裁掉越界部分,用户可先缩小画布继续操作。
|
||||
const availableSides = [
|
||||
{ space: canvasSize.width - sourceRight, left: rightLeft, top: clampPanelTop(sourceTop) },
|
||||
{ space: sourceLeft, left: leftLeft, top: clampPanelTop(sourceTop) },
|
||||
{
|
||||
space: canvasSize.height - sourceBottom,
|
||||
left: clampPanelLeft(sourceLeft + sourceWidth / 2 - panelWidth / 2),
|
||||
top: bottomTop,
|
||||
},
|
||||
{
|
||||
space: sourceTop,
|
||||
left: clampPanelLeft(sourceLeft + sourceWidth / 2 - panelWidth / 2),
|
||||
top: topTop,
|
||||
},
|
||||
];
|
||||
const bestSide = availableSides.reduce((best, candidate) =>
|
||||
candidate.space > best.space ? candidate : best,
|
||||
);
|
||||
return { left: bestSide.left, top: bestSide.top };
|
||||
}
|
||||
|
||||
export function resolveQuickEditFocusViewport({
|
||||
sourceLayer,
|
||||
canvasSize,
|
||||
|
||||
@@ -24,6 +24,7 @@ export type CanvasGenerationAction =
|
||||
| 'scene.generate'
|
||||
| 'spec.generate'
|
||||
| 'character.generate'
|
||||
| 'character-sheet.generate'
|
||||
| 'icon.generate'
|
||||
| 'ui-design.generate'
|
||||
| 'publication.generate'
|
||||
@@ -31,6 +32,8 @@ export type CanvasGenerationAction =
|
||||
| 'audio.sound-effect.generate'
|
||||
| 'audio.background-music.generate'
|
||||
| 'character-animation.generate'
|
||||
| 'character-animation.convert'
|
||||
| 'character-animation.remove-background'
|
||||
| 'image.edit'
|
||||
| 'ui-design.extract-assets'
|
||||
| 'image.perfect-pixel'
|
||||
@@ -137,6 +140,7 @@ export type CanvasTool =
|
||||
| 'upload'
|
||||
| 'generate'
|
||||
| 'video'
|
||||
| 'character-animation'
|
||||
| 'music'
|
||||
| 'spec'
|
||||
| 'character'
|
||||
@@ -203,7 +207,7 @@ export type CropExpandPanelState = {
|
||||
};
|
||||
|
||||
export type CharacterAnimationPanelState = {
|
||||
sourceLayerId: string;
|
||||
sourceLayerId?: string;
|
||||
promptText: string;
|
||||
status: 'idle' | 'generating' | 'completed' | 'failed';
|
||||
errorMessage?: string;
|
||||
|
||||
@@ -326,7 +326,8 @@ export function useImageCanvasStageInteractions({
|
||||
(generateDialog?.mode === 'generate' ||
|
||||
generateDialog?.mode === 'scene' ||
|
||||
generateDialog?.mode === 'video' ||
|
||||
generateDialog?.mode === 'spec')
|
||||
generateDialog?.mode === 'spec' ||
|
||||
generateDialog?.mode === 'character-animation')
|
||||
) {
|
||||
focusCanvasInteractionTarget(event.currentTarget);
|
||||
event.preventDefault();
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export type EditorCharacterAnimationFrameSplitInput = {
|
||||
projectId: string;
|
||||
sourceLayerId: string;
|
||||
sourceResourceId: string;
|
||||
assetFolderId?: string | null;
|
||||
assetLabel?: string | null;
|
||||
};
|
||||
|
||||
export type EditorCharacterAnimationFrameSplitResult<TAsset> = {
|
||||
frameCount: number;
|
||||
assets: TAsset[];
|
||||
};
|
||||
@@ -6,6 +6,7 @@ export type * from './contracts/creationAudio';
|
||||
export type * from './contracts/creativeAgent';
|
||||
export type * from './contracts/customWorldAgent';
|
||||
export type * from './contracts/editorAudio';
|
||||
export type * from './contracts/editorCharacterAnimation';
|
||||
export * from './contracts/editorScene';
|
||||
export * from './contracts/edutainmentBabyDrawing';
|
||||
export * from './contracts/edutainmentBabyObject';
|
||||
|
||||
Generated
+1
@@ -2940,6 +2940,7 @@ dependencies = [
|
||||
"platform-oss",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"sha2",
|
||||
"shared-kernel",
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
@@ -1855,6 +1855,54 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn editor_character_animation_video_conversion_requires_bearer_auth() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/editor/character-animations/video-conversions")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"sourceVideoSrc": "users/owner/source.mp4",
|
||||
"resolution": "480p"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn editor_character_animation_frame_split_requires_bearer_auth() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let request_body = serde_json::json!({
|
||||
"projectId": "project-1",
|
||||
"sourceLayerId": "layer-character-animation",
|
||||
"sourceResourceId": "resource-character-animation"
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/editor/character-animations/frame-splits")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(request_body))
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn editor_ui_design_asset_extraction_rejects_inline_data_url_before_queueing() {
|
||||
let state = AppState::new(AppConfig {
|
||||
|
||||
@@ -930,7 +930,7 @@ mod tests {
|
||||
for (source, expected_calls) in [
|
||||
(include_str!("editor_project.rs"), 4),
|
||||
(include_str!("editor_project_icon.rs"), 2),
|
||||
(include_str!("character_animation_assets.rs"), 2),
|
||||
(include_str!("character_animation_assets.rs"), 4),
|
||||
(
|
||||
include_str!("vector_engine_audio_generation/generation.rs"),
|
||||
2,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,10 @@ pub(crate) const EDITOR_UI_DESIGN_ASSET_EXTRACTION_JOB_KIND: &str =
|
||||
"editor_ui_design_asset_extraction";
|
||||
pub(crate) const EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND: &str =
|
||||
"editor_character_animation_generation";
|
||||
pub(crate) const EDITOR_CHARACTER_ANIMATION_VIDEO_CONVERSION_JOB_KIND: &str =
|
||||
"editor_character_animation_video_conversion";
|
||||
pub(crate) const EDITOR_CHARACTER_ANIMATION_BACKGROUND_REMOVAL_JOB_KIND: &str =
|
||||
"editor_character_animation_background_removal";
|
||||
pub(crate) const EDITOR_VIDEO_GENERATION_JOB_KIND: &str = "editor_video_generation";
|
||||
pub(crate) const EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND: &str = "editor_sound_effect_generation";
|
||||
pub(crate) const EDITOR_BACKGROUND_MUSIC_GENERATION_JOB_KIND: &str =
|
||||
|
||||
@@ -208,6 +208,37 @@ pub(crate) fn remove_editor_generated_green_screen_background(
|
||||
})
|
||||
}
|
||||
|
||||
/// 角色动作纯色背景模式使用用户显式选择的键色执行本地算法,不调用 BgFilter、
|
||||
/// 阿里云或其它模型。不额外清理近白背景;只有精确绿幕键色启用低饱和绿边缘修复。
|
||||
pub(crate) fn remove_editor_character_animation_solid_color_background(
|
||||
image: &DownloadedOpenAiImage,
|
||||
key_rgb: [u8; 3],
|
||||
) -> Result<DownloadedOpenAiImage, AppError> {
|
||||
let key_color = GeneratedAssetSheetKeyColor {
|
||||
red: key_rgb[0],
|
||||
green: key_rgb[1],
|
||||
blue: key_rgb[2],
|
||||
};
|
||||
let bytes = remove_generated_asset_sheet_green_screen_background_bytes(
|
||||
image.bytes.as_slice(),
|
||||
GeneratedAssetSheetAlphaOptions {
|
||||
key_color,
|
||||
remove_near_white_background: false,
|
||||
remove_disconnected_hard_key_background: true,
|
||||
remove_muted_green_screen_background: key_color.is_green_screen(),
|
||||
detect_internal_holes: true,
|
||||
internal_hole_min_pixels: 16,
|
||||
},
|
||||
)
|
||||
.map_err(map_editor_green_screen_error)?;
|
||||
|
||||
Ok(DownloadedOpenAiImage {
|
||||
bytes,
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn map_editor_green_screen_error(error: GeneratedAssetSheetError) -> AppError {
|
||||
let status = match error {
|
||||
GeneratedAssetSheetError::DecodeImage { .. } => StatusCode::BAD_GATEWAY,
|
||||
@@ -259,6 +290,87 @@ mod tests {
|
||||
assert_eq!(image.get_pixel(1, 1).0[3], 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_green_screen_removal_keeps_white_foreground() {
|
||||
let mut source = RgbaImage::from_pixel(3, 3, Rgba([0, 255, 0, 255]));
|
||||
source.put_pixel(1, 1, Rgba([255, 255, 255, 255]));
|
||||
let mut bytes = Vec::new();
|
||||
DynamicImage::ImageRgba8(source)
|
||||
.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)
|
||||
.expect("test PNG should encode");
|
||||
|
||||
let output = remove_editor_character_animation_solid_color_background(
|
||||
&DownloadedOpenAiImage {
|
||||
bytes,
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
},
|
||||
[0, 255, 0],
|
||||
)
|
||||
.expect("green screen should be removed");
|
||||
let image = image::load_from_memory(output.bytes.as_slice())
|
||||
.expect("output PNG should decode")
|
||||
.to_rgba8();
|
||||
|
||||
assert_eq!(image.get_pixel(0, 0).0[3], 0);
|
||||
assert_eq!(image.get_pixel(1, 1).0, [255, 255, 255, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_solid_color_removal_tolerates_small_background_color_variation() {
|
||||
let key = [72, 161, 224];
|
||||
let mut source = RgbaImage::from_pixel(9, 9, Rgba([82, 151, 229, 255]));
|
||||
source.put_pixel(0, 0, Rgba([65, 168, 217, 255]));
|
||||
source.put_pixel(4, 4, Rgba([232, 52, 48, 255]));
|
||||
let mut bytes = Vec::new();
|
||||
DynamicImage::ImageRgba8(source)
|
||||
.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)
|
||||
.expect("test PNG should encode");
|
||||
|
||||
let output = remove_editor_character_animation_solid_color_background(
|
||||
&DownloadedOpenAiImage {
|
||||
bytes,
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
},
|
||||
key,
|
||||
)
|
||||
.expect("near-key solid background should be removed");
|
||||
let image = image::load_from_memory(output.bytes.as_slice())
|
||||
.expect("output PNG should decode")
|
||||
.to_rgba8();
|
||||
|
||||
assert_eq!(image.get_pixel(0, 0).0[3], 0);
|
||||
assert_eq!(image.get_pixel(8, 8).0[3], 0);
|
||||
assert_eq!(image.get_pixel(4, 4).0, [232, 52, 48, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_animation_custom_key_color_preserves_green_foreground() {
|
||||
let mut source = RgbaImage::from_pixel(3, 3, Rgba([0, 0, 255, 255]));
|
||||
source.put_pixel(1, 1, Rgba([0, 255, 0, 255]));
|
||||
let mut bytes = Vec::new();
|
||||
DynamicImage::ImageRgba8(source)
|
||||
.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)
|
||||
.expect("test PNG should encode");
|
||||
|
||||
let output = remove_editor_character_animation_solid_color_background(
|
||||
&DownloadedOpenAiImage {
|
||||
bytes,
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
},
|
||||
[0, 0, 255],
|
||||
)
|
||||
.expect("blue screen should be removed");
|
||||
let image = image::load_from_memory(output.bytes.as_slice())
|
||||
.expect("output PNG should decode")
|
||||
.to_rgba8();
|
||||
|
||||
assert_eq!(image.get_pixel(0, 0).0[3], 0);
|
||||
assert_eq!(image.get_pixel(1, 1).0, [0, 255, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_screen_background_color_parser_uses_default_and_rejects_unknown() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -26,6 +26,7 @@ use platform_oss::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
pub use shared_contracts::assets::EditorAssetPayload;
|
||||
#[cfg(test)]
|
||||
use shared_contracts::assets::EditorAudioGenerateResponse;
|
||||
use shared_contracts::assets::{
|
||||
@@ -227,10 +228,10 @@ const EDITOR_LEGACY_GREEN_SCREEN_SOURCE_ASSET_KIND: &str = "editor_green_screen_
|
||||
pub(crate) const EDITOR_PROVIDER_SOURCE_SLOT: &str = "provider_source";
|
||||
pub(crate) const EDITOR_BGFILTER_DEFAULT_SEG_MODEL: &str = "birefnet";
|
||||
const EDITOR_BGFILTER_SEG_MODEL_ANIME_SEG: &str = "anime-seg";
|
||||
const EDITOR_BGFILTER_MATTING_PROVIDER: &str = "BgFilter";
|
||||
pub(crate) const EDITOR_BGFILTER_MATTING_PROVIDER: &str = "BgFilter";
|
||||
const EDITOR_ALIYUN_MATTING_PROVIDER: &str = "Aliyun Matting";
|
||||
const EDITOR_ALIYUN_MATTING_MODEL: &str = "segment-common-image";
|
||||
const EDITOR_LOCAL_MATTING_PROVIDER: &str = "Genarrative Local";
|
||||
pub(crate) const EDITOR_LOCAL_MATTING_PROVIDER: &str = "Genarrative Local";
|
||||
const EDITOR_LOCAL_MATTING_MODEL: &str = "screen-color-keying";
|
||||
const EDITOR_MATTING_SOURCE_URL_EXPIRE_SECONDS: u64 = 600;
|
||||
pub(crate) const EDITOR_BGFILTER_CROSS_CHECK_ENABLED: bool = true;
|
||||
@@ -488,6 +489,46 @@ pub struct EditorUiDesignAssetExtractionRequest {
|
||||
pub(crate) canvas_completion: Option<EditorCanvasGenerationCompletionRequest>,
|
||||
}
|
||||
|
||||
const EDITOR_GENERATION_OPERATION_ID_MAX_BYTES: usize = 128;
|
||||
|
||||
/// 将请求级 request id 收敛为可持久化的编辑器 operation id。
|
||||
///
|
||||
/// request id 会进入 SpacetimeDB 幂等键、资产对象标识和画布层标识,不能把任意
|
||||
/// header 内容直接带入这些边界。这里只允许客户端和队列实际使用的稳定 ID 字符集,
|
||||
/// 超限或包含路径/控制字符时拒绝请求,而不是静默改写导致幂等键碰撞。
|
||||
pub(crate) fn validate_editor_generation_operation_id(
|
||||
operation_id: &str,
|
||||
) -> Result<String, AppError> {
|
||||
let operation_id = operation_id.trim();
|
||||
if operation_id.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-generation-operation",
|
||||
"message": "编辑器生成 request id 不能为空。",
|
||||
})),
|
||||
);
|
||||
}
|
||||
if operation_id.len() > EDITOR_GENERATION_OPERATION_ID_MAX_BYTES {
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-generation-operation",
|
||||
"message": format!(
|
||||
"编辑器生成 request id 不能超过 {EDITOR_GENERATION_OPERATION_ID_MAX_BYTES} 个字节。"
|
||||
),
|
||||
})));
|
||||
}
|
||||
if !operation_id.chars().all(|character| {
|
||||
character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.' | ':')
|
||||
}) {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "editor-generation-operation",
|
||||
"message": "编辑器生成 request id 含有不支持的字符。",
|
||||
})),
|
||||
);
|
||||
}
|
||||
Ok(operation_id.to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct EditorGenerationOperationContext {
|
||||
pub(crate) operation_kind: String,
|
||||
@@ -591,9 +632,10 @@ impl EditorGenerationCaller {
|
||||
"message": format!("序列化编辑器生成幂等请求失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
let operation_id = validate_editor_generation_operation_id(request_context.request_id())?;
|
||||
self.operation = Some(EditorGenerationOperationContext {
|
||||
operation_kind: operation_kind.to_string(),
|
||||
operation_id: request_context.request_id().trim().to_string(),
|
||||
operation_id,
|
||||
operation_fingerprint: editor_generation_request_fingerprint(
|
||||
operation_kind,
|
||||
request_payload_json.as_str(),
|
||||
@@ -1842,43 +1884,6 @@ pub struct EditorAssetFolderPayload {
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorAssetPayload {
|
||||
asset_id: String,
|
||||
folder_id: String,
|
||||
label: String,
|
||||
image_src: String,
|
||||
object_key: Option<String>,
|
||||
asset_object_id: Option<String>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
source_type: String,
|
||||
prompt: Option<String>,
|
||||
actual_prompt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
provider: Option<String>,
|
||||
task_id: Option<String>,
|
||||
asset_kind: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
image_sequence_frames: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
image_sequence_duration_ms: Option<u64>,
|
||||
generation_inputs: Option<Value>,
|
||||
source_resource_id: Option<String>,
|
||||
public_showcase_enabled: Option<bool>,
|
||||
thumbnail_src: Option<String>,
|
||||
generation_cost_mud_points: u64,
|
||||
showcase_id: Option<String>,
|
||||
showcase_review_status: Option<String>,
|
||||
showcase_display_enabled: Option<bool>,
|
||||
showcase_like_count: Option<u64>,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
pub async fn load_recent_editor_project(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -2913,6 +2918,7 @@ where
|
||||
)?;
|
||||
let normalized_kind = payload.kind.as_deref().map(str::trim);
|
||||
let is_character_generation = matches!(normalized_kind, Some("character"));
|
||||
let is_character_sheet_generation = matches!(normalized_kind, Some("character-sheet"));
|
||||
let pixel_art_supported = matches!(normalized_kind, None | Some("") | Some("character"));
|
||||
let (image_style, mut generation_warning) =
|
||||
normalize_editor_image_generation_style(payload.style.as_deref(), pixel_art_supported);
|
||||
@@ -3120,6 +3126,18 @@ where
|
||||
role_setting.as_str(),
|
||||
screen_color.expect("character generation should have screen color"),
|
||||
)
|
||||
} else if is_character_sheet_generation {
|
||||
// 多方向图集以纯绿底间隔各方向,便于后续拆分;不参与编辑器的背景自动选色。
|
||||
build_editor_character_multi_view_sheet_prompt(
|
||||
role_setting.as_str(),
|
||||
crate::editor_green_screen::EditorScreenBackgroundColor {
|
||||
label: "纯绿色",
|
||||
hex: "#00FF00",
|
||||
red: 0,
|
||||
green: 255,
|
||||
blue: 0,
|
||||
},
|
||||
)
|
||||
} else if is_ui_design_generation {
|
||||
build_editor_ui_design_prompt(role_setting.as_str(), ui_has_references)
|
||||
} else {
|
||||
@@ -3640,6 +3658,7 @@ fn editor_image_generation_billing_asset_kind(normalized_kind: Option<&str>) ->
|
||||
match normalized_kind {
|
||||
Some("scene") => EDITOR_SCENE_IMAGE_ASSET_KIND,
|
||||
Some("character") => EDITOR_CHARACTER_IMAGE_ASSET_KIND,
|
||||
Some("character-sheet") => EDITOR_CHARACTER_IMAGE_ASSET_KIND,
|
||||
Some("spec") => EDITOR_SPEC_IMAGE_ASSET_KIND,
|
||||
Some("quick-edit") => EDITOR_QUICK_EDIT_IMAGE_ASSET_KIND,
|
||||
Some("ui-design") => EDITOR_UI_DESIGN_IMAGE_ASSET_KIND,
|
||||
@@ -3734,6 +3753,11 @@ fn editor_generated_image_storage_profile(
|
||||
path_kind: "character-images",
|
||||
slot: EDITOR_CHARACTER_IMAGE_SLOT,
|
||||
},
|
||||
Some("character-sheet") => EditorGeneratedImageStorageProfile {
|
||||
asset_kind: EDITOR_CHARACTER_IMAGE_ASSET_KIND,
|
||||
path_kind: "character-sheet-images",
|
||||
slot: "character-sheet",
|
||||
},
|
||||
Some("spec") => EditorGeneratedImageStorageProfile {
|
||||
asset_kind: EDITOR_SPEC_IMAGE_ASSET_KIND,
|
||||
path_kind: "spec-images",
|
||||
@@ -6410,10 +6434,10 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
||||
))
|
||||
}
|
||||
|
||||
struct EditorBackgroundRemovalImage {
|
||||
image: DownloadedOpenAiImage,
|
||||
width: u32,
|
||||
height: u32,
|
||||
pub(crate) struct EditorBackgroundRemovalImage {
|
||||
pub(crate) image: DownloadedOpenAiImage,
|
||||
pub(crate) width: u32,
|
||||
pub(crate) height: u32,
|
||||
}
|
||||
|
||||
fn validate_editor_pixel_art_snap_canvas_completion(
|
||||
@@ -7480,7 +7504,7 @@ fn editor_bgfilter_flat_deadline_reserve(aliyun_timeout_ms: u64) -> Duration {
|
||||
.saturating_add(EDITOR_BGFILTER_PARENT_TRANSPORT_WINDOW)
|
||||
}
|
||||
|
||||
async fn request_editor_background_removal_image_with_bgfilter_worker(
|
||||
pub(crate) async fn request_editor_background_removal_image_with_bgfilter_worker(
|
||||
state: &AppState,
|
||||
source_object_key: &str,
|
||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||
@@ -11402,6 +11426,29 @@ fn build_editor_character_image_prompt(
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// 多方向角色图集提示词:一张图里排布同一角色的多个方向,各方向独立成块、纯色底间隔,
|
||||
/// 供用户手动「拆分图集」后逐视角生成动画。方向/数量不校验,图不对由用户重新生成。
|
||||
fn build_editor_character_multi_view_sheet_prompt(
|
||||
role_setting: &str,
|
||||
screen_color: EditorScreenBackgroundColor,
|
||||
) -> String {
|
||||
// 八方向围绕一圈:正面、右前、右侧、右后、背面、左后、左侧、左前,按 3×3 九宫格外圈
|
||||
// 排布、中心留空,方向之间由纯色背景充分间隔,使后续外接矩形拆分不易粘连。
|
||||
const VIEW_LAYOUT: &str = "正面、右前、右侧、右后、背面、左后、左侧、左前";
|
||||
vec![
|
||||
format!(
|
||||
"根据角色设定生成游戏角色的八方向视图图集。画面中只有同一个角色,共八个方向:{VIEW_LAYOUT},每个方向一个完整角色。\
|
||||
八个方向必须是同一个角色,脸型、发型、服装、配色、武器、头身比、体型全程一致,禁止漂移、换装、变形。\
|
||||
八个方向按三乘三九宫格围绕一圈排布:上、下、左、右以及四个角落各放一个方向,九宫格中心留空,不放置角色。\
|
||||
每个方向角色完整居中,不裁切头顶、手脚或武器;各方向底部基线对齐,画面占比一致。{};角色之间由纯色背景充分间隔,互不接触、互不重叠,便于后续拆分。\
|
||||
禁止额外角色、分身、宠物、文字、水印、边框、编号、网格线。",
|
||||
editor_green_screen_character_prompt_clause(screen_color)
|
||||
),
|
||||
format!("角色设定:{}", role_setting.trim()),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// 中文注释:`style="pixelArt"` 此前只驱动 provider 返回后的确定性像素规整,完全不参与提示词。
|
||||
/// snapper 是几何对齐器——检测网格步长后按格重采样;provider 交一张柔和渐变图时两轴都测不到
|
||||
/// 步长,生成路径用的 legacy profile 会退到 `min(w,h)/64` 统一网格,产出的是马赛克而不是像素
|
||||
@@ -12674,7 +12721,7 @@ pub(crate) async fn read_editor_reference_image_object_with_client(
|
||||
})
|
||||
}
|
||||
|
||||
async fn download_editor_persisted_image_object(
|
||||
pub(crate) async fn download_editor_persisted_image_object(
|
||||
state: &AppState,
|
||||
object_key: &str,
|
||||
) -> Result<DownloadedOpenAiImage, AppError> {
|
||||
@@ -12808,6 +12855,25 @@ mod tests {
|
||||
thread,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn editor_generation_operation_id_accepts_stable_request_id_characters() {
|
||||
assert_eq!(
|
||||
validate_editor_generation_operation_id(" web-abc_123.v1:retry ").unwrap(),
|
||||
"web-abc_123.v1:retry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generation_operation_id_rejects_unsafe_or_oversized_request_ids() {
|
||||
for request_id in ["", " ", "request/id", "request id", "request\n id"] {
|
||||
assert!(
|
||||
validate_editor_generation_operation_id(request_id).is_err(),
|
||||
"request id should be rejected: {request_id:?}"
|
||||
);
|
||||
}
|
||||
assert!(validate_editor_generation_operation_id(&"x".repeat(129)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_image_prompt_validation_stays_outside_prompt_builder_execution() {
|
||||
assert!(validate_editor_image_generation_prompt("生成森林场景").is_ok());
|
||||
@@ -17072,6 +17138,42 @@ mod tests {
|
||||
assert!(prompt.contains("角色设定:菜市场卖菜大妈"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_character_multi_view_sheet_prompt_enforces_directions_and_separation() {
|
||||
let prompt = build_editor_character_multi_view_sheet_prompt(
|
||||
"日系魔法少女",
|
||||
crate::editor_green_screen::EditorScreenBackgroundColor {
|
||||
label: "纯绿色",
|
||||
hex: "#00FF00",
|
||||
red: 0,
|
||||
green: 255,
|
||||
blue: 0,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(prompt.contains("八方向视图图集"));
|
||||
assert!(prompt.contains("共八个方向:正面、右前、右侧、右后、背面、左后、左侧、左前"));
|
||||
assert!(prompt.contains("八个方向必须是同一个角色"));
|
||||
assert!(prompt.contains("按三乘三九宫格围绕一圈排布"));
|
||||
assert!(prompt.contains("九宫格中心留空"));
|
||||
assert!(prompt.contains("脸型、发型、服装、配色、武器、头身比、体型全程一致"));
|
||||
assert!(prompt.contains("背景固定为单一纯色背景 纯绿色 #00FF00"));
|
||||
assert!(prompt.contains("角色之间由纯色背景充分间隔,互不接触、互不重叠"));
|
||||
assert!(prompt.contains("禁止额外角色、分身、宠物、文字、水印、边框、编号、网格线"));
|
||||
assert!(prompt.contains("角色设定:日系魔法少女"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_character_sheet_kind_routes_to_sheet_prompt_and_icon_spritesheet_asset_kind() {
|
||||
assert_eq!(
|
||||
editor_image_generation_billing_asset_kind(Some("character-sheet")),
|
||||
EDITOR_CHARACTER_IMAGE_ASSET_KIND
|
||||
);
|
||||
let profile = editor_generated_image_storage_profile(Some("character-sheet"));
|
||||
assert_eq!(profile.path_kind, "character-sheet-images");
|
||||
assert_eq!(profile.slot, "character-sheet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generated_asset_persistence_keeps_user_prompt_separate_from_system_prompt() {
|
||||
let source = concat!(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -118,6 +118,60 @@ pub struct ExternalEditorAssetFolderUpdateRequest {
|
||||
collapsed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ExternalEditorCharacterAnimationGenerateRequest {
|
||||
source_layer_id: String,
|
||||
source_image_src: String,
|
||||
source_width: u32,
|
||||
source_height: u32,
|
||||
prompt_text: String,
|
||||
screen_color: Option<String>,
|
||||
resolution: String,
|
||||
ratio: String,
|
||||
frame_count: u32,
|
||||
duration_seconds: u32,
|
||||
model: String,
|
||||
project_id: Option<String>,
|
||||
canvas_completion: Option<shared_contracts::assets::EditorCanvasGenerationCompletionPayload>,
|
||||
generation_inputs: Option<Value>,
|
||||
source_resource_id: Option<String>,
|
||||
asset_folder_id: Option<String>,
|
||||
asset_label: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ExternalEditorCharacterAnimationGenerateRequest>
|
||||
for shared_contracts::assets::EditorCharacterAnimationGenerateRequest
|
||||
{
|
||||
fn from(payload: ExternalEditorCharacterAnimationGenerateRequest) -> Self {
|
||||
Self {
|
||||
source_layer_id: payload.source_layer_id,
|
||||
source_image_src: payload.source_image_src,
|
||||
source_width: payload.source_width,
|
||||
source_height: payload.source_height,
|
||||
input_mode: None,
|
||||
reference_media_type: None,
|
||||
reference_src: None,
|
||||
appearance_reference_srcs: Vec::new(),
|
||||
prompt_text: payload.prompt_text,
|
||||
background_color: None,
|
||||
screen_color: payload.screen_color,
|
||||
resolution: payload.resolution,
|
||||
ratio: payload.ratio,
|
||||
frame_count: payload.frame_count,
|
||||
duration_seconds: payload.duration_seconds,
|
||||
model: payload.model,
|
||||
project_id: payload.project_id,
|
||||
canvas_completion: payload.canvas_completion,
|
||||
generation_inputs: payload.generation_inputs,
|
||||
source_resource_id: payload.source_resource_id,
|
||||
appearance_source_resource_ids: Vec::new(),
|
||||
asset_folder_id: payload.asset_folder_id,
|
||||
asset_label: payload.asset_label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExternalEditorAssetCreateRequest {
|
||||
@@ -822,10 +876,7 @@ pub async fn generate_external_editor_character_animation(
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(principal): Extension<ExternalApiPrincipal>,
|
||||
headers: HeaderMap,
|
||||
payload: Result<
|
||||
Json<shared_contracts::assets::EditorCharacterAnimationGenerateRequest>,
|
||||
JsonRejection,
|
||||
>,
|
||||
payload: Result<Json<ExternalEditorCharacterAnimationGenerateRequest>, JsonRejection>,
|
||||
) -> Result<Response, Response> {
|
||||
require_scope_response(&request_context, &principal, SCOPE_EDITOR_IMAGE_GENERATE)?;
|
||||
let idempotency_key = require_idempotency_key(&headers)
|
||||
@@ -835,7 +886,7 @@ pub async fn generate_external_editor_character_animation(
|
||||
&state,
|
||||
&request_context,
|
||||
principal.owner_user_id(),
|
||||
payload,
|
||||
payload.into(),
|
||||
Some(idempotency_key),
|
||||
)
|
||||
.await?;
|
||||
@@ -1459,6 +1510,91 @@ mod tests {
|
||||
assert_eq!(request.expected_revision, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_editor_character_animation_request_keeps_legacy_image_contract() {
|
||||
let request =
|
||||
serde_json::from_value::<ExternalEditorCharacterAnimationGenerateRequest>(json!({
|
||||
"sourceLayerId": "layer-character",
|
||||
"sourceImageSrc": "editor/uploads/character.png",
|
||||
"sourceWidth": 512,
|
||||
"sourceHeight": 512,
|
||||
"promptText": "原地待机",
|
||||
"resolution": "480p",
|
||||
"ratio": "same",
|
||||
"frameCount": 32,
|
||||
"durationSeconds": 4,
|
||||
"model": "seedance2.0-fast"
|
||||
}))
|
||||
.expect("External v1 旧图片模式请求应继续通过反序列化");
|
||||
|
||||
let internal: shared_contracts::assets::EditorCharacterAnimationGenerateRequest =
|
||||
request.into();
|
||||
assert_eq!(internal.source_layer_id, "layer-character");
|
||||
assert_eq!(internal.source_image_src, "editor/uploads/character.png");
|
||||
assert_eq!(internal.input_mode, None);
|
||||
assert_eq!(internal.reference_media_type, None);
|
||||
assert_eq!(internal.reference_src, None);
|
||||
assert_eq!(internal.background_color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_editor_character_animation_request_rejects_internal_multimodal_fields() {
|
||||
let error =
|
||||
serde_json::from_value::<ExternalEditorCharacterAnimationGenerateRequest>(json!({
|
||||
"sourceLayerId": "layer-character",
|
||||
"sourceImageSrc": "editor/uploads/character.png",
|
||||
"sourceWidth": 512,
|
||||
"sourceHeight": 512,
|
||||
"inputMode": "video",
|
||||
"promptText": "原地待机",
|
||||
"resolution": "480p",
|
||||
"ratio": "same",
|
||||
"frameCount": 32,
|
||||
"durationSeconds": 4,
|
||||
"model": "seedance2.0-fast"
|
||||
}))
|
||||
.expect_err("External v1 不应接收内部三模态扩展字段");
|
||||
|
||||
assert!(error.to_string().contains("inputMode"));
|
||||
|
||||
let appearance_error =
|
||||
serde_json::from_value::<ExternalEditorCharacterAnimationGenerateRequest>(json!({
|
||||
"sourceLayerId": "layer-character",
|
||||
"sourceImageSrc": "editor/uploads/character.png",
|
||||
"sourceWidth": 512,
|
||||
"sourceHeight": 512,
|
||||
"promptText": "原地待机",
|
||||
"resolution": "480p",
|
||||
"ratio": "same",
|
||||
"frameCount": 32,
|
||||
"durationSeconds": 4,
|
||||
"model": "seedance2.0-fast",
|
||||
"appearanceReferenceSrc": "editor/uploads/appearance.png"
|
||||
}))
|
||||
.expect_err("External v1 不应接收内部角色外观字段");
|
||||
assert!(
|
||||
appearance_error
|
||||
.to_string()
|
||||
.contains("appearanceReferenceSrc")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_editor_character_animation_request_requires_legacy_source_fields() {
|
||||
let error =
|
||||
serde_json::from_value::<ExternalEditorCharacterAnimationGenerateRequest>(json!({
|
||||
"promptText": "原地待机",
|
||||
"resolution": "480p",
|
||||
"ratio": "1:1",
|
||||
"frameCount": 32,
|
||||
"durationSeconds": 4,
|
||||
"model": "seedance2.0-fast"
|
||||
}))
|
||||
.expect_err("External v1 文本模式不得绕过旧图片模式必填字段");
|
||||
|
||||
assert!(error.to_string().contains("sourceLayerId"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_editor_generation_inputs_sanitize_audit_fields_for_regular_assets() {
|
||||
let serialized = serialize_external_editor_generation_inputs(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user