合并远端master更新
融合移动端提示恢复与远端钱包刷新改动 # Conflicts: # src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx # src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
This commit is contained in:
@@ -19,6 +19,10 @@
|
||||
|
||||
- [图片画布编辑器 MVP 接入方案](./technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md)
|
||||
- [图片画布编辑器前端拆分计划](./technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md)
|
||||
- [图片画布游戏场景生成链路](./technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md)
|
||||
- [画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md)
|
||||
- [音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md)
|
||||
- [BGM 提示词优化 T6 测试与发布门禁](./【实施记录】BGM生成提示词优化T6测试与发布门禁-2026-08-05.md)
|
||||
- [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md)
|
||||
- [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)
|
||||
- [图片画布撤销范围与操作提示方案](./【图片画布】撤销范围与操作提示方案-2026-07-17.md)
|
||||
|
||||
@@ -365,13 +365,36 @@
|
||||
"ExternalApiKey": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "view",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"description": "返回视图。full 返回完整项目、画布、图层与资源;summary 只返回项目选择所需元数据和封面稳定引用。MCP 的 list_editor_projects 工具固定使用 summary。",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"full",
|
||||
"summary"
|
||||
],
|
||||
"default": "full"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "项目列表",
|
||||
"description": "项目列表。view=full 返回完整项目列表;view=summary 返回紧凑项目摘要列表。",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ExternalEditorProjectListResponse"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ExternalEditorProjectListResponse"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/ExternalEditorProjectSummaryListResponse"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2051,6 +2074,47 @@
|
||||
},
|
||||
"ExternalEditorAssetCreateRequest": {
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"assetKind": {
|
||||
"const": "character-animation"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetKind"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"imageSequenceFrames",
|
||||
"imageSequenceDurationMs"
|
||||
],
|
||||
"properties": {
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/ExternalCharacterAnimationGenerationInputs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"imageSequenceFrames"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"imageSequenceDurationMs"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"folderId",
|
||||
"label",
|
||||
@@ -2132,16 +2196,71 @@
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。"
|
||||
},
|
||||
"imageSequenceFrames": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EditorImageSequenceFrame"
|
||||
}
|
||||
},
|
||||
"imageSequenceDurationMs": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。"
|
||||
},
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
"$ref": "#/components/schemas/JsonValue",
|
||||
"description": "可重放的生成输入。assetKind=character-animation 时不得包含 characterAnimation、frames、previewVideoPath、frameCount、fps 或 durationSeconds 等旧运行字段;正式媒体数据必须写入 imageSequenceFrames 和 imageSequenceDurationMs。服务端会移除 screenColorHex、mattingProvider、mattingModel 等内部处理审计字段。"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ExternalEditorProjectResourceCreateRequest": {
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"assetKind": {
|
||||
"const": "character-animation"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetKind"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"imageSequenceFrames",
|
||||
"imageSequenceDurationMs"
|
||||
],
|
||||
"properties": {
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/ExternalCharacterAnimationGenerationInputs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"imageSequenceFrames"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"imageSequenceDurationMs"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"imageSrc",
|
||||
"width",
|
||||
@@ -2215,14 +2334,71 @@
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。"
|
||||
},
|
||||
"imageSequenceFrames": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EditorImageSequenceFrame"
|
||||
}
|
||||
},
|
||||
"imageSequenceDurationMs": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。"
|
||||
},
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
"$ref": "#/components/schemas/JsonValue",
|
||||
"description": "可重放的生成输入。assetKind=character-animation 时不得包含 characterAnimation、frames、previewVideoPath、frameCount、fps 或 durationSeconds 等旧运行字段;正式媒体数据必须写入 imageSequenceFrames 和 imageSequenceDurationMs。服务端会移除 screenColorHex、mattingProvider、mattingModel 等内部处理审计字段。"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ExternalCharacterAnimationGenerationInputs": {
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"characterAnimation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"frames"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"previewVideoPath"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"frameCount"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"fps"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"durationSeconds"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "assetKind=character-animation 时 generationInputs 不得包含旧运行字段;正式媒体数据使用 imageSequenceFrames 和 imageSequenceDurationMs。"
|
||||
},
|
||||
"ExternalEditorAssetUpdateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2278,6 +2454,87 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExternalEditorProjectSummaryListResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"projects"
|
||||
],
|
||||
"properties": {
|
||||
"projects": {
|
||||
"type": "array",
|
||||
"description": "用于展示、查找、同名确认和安全选择目标的紧凑项目摘要;不包含 canvas、viewport、layers 或 resources。",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EditorProjectSummary"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EditorProjectSummary": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"projectId",
|
||||
"title",
|
||||
"updatedAt",
|
||||
"cover"
|
||||
],
|
||||
"properties": {
|
||||
"projectId": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"cover": {
|
||||
"description": "项目最新封面快照的稳定引用;项目没有封面时为 null。需要展示时使用 objectKey 调用 /assets/read-url 获取临时签名 URL。",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EditorProjectSummaryCover"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EditorProjectSummaryCover": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"resourceId",
|
||||
"objectKey",
|
||||
"width",
|
||||
"height",
|
||||
"updatedAt"
|
||||
],
|
||||
"properties": {
|
||||
"resourceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"objectKey": {
|
||||
"type": "string",
|
||||
"description": "封面对象的稳定引用,不是图片正文、Data URL 或临时签名 URL。"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ExternalEditorProjectDeleteResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -2545,7 +2802,19 @@
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。"
|
||||
},
|
||||
"imageSequenceFrames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EditorImageSequenceFrame"
|
||||
}
|
||||
},
|
||||
"imageSequenceDurationMs": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。"
|
||||
},
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
@@ -2688,6 +2957,18 @@
|
||||
"sourceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageSequenceFrames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EditorImageSequenceFrame"
|
||||
},
|
||||
"description": "assetKind=character-animation 时的完整可播放帧集合。"
|
||||
},
|
||||
"imageSequenceDurationMs": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。"
|
||||
},
|
||||
"prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -2716,7 +2997,8 @@
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。"
|
||||
},
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
@@ -3017,7 +3299,7 @@
|
||||
"ui-design",
|
||||
"publication-material"
|
||||
],
|
||||
"description": "省略时生成普通图片;其它值选择对应的专用生成流程。"
|
||||
"description": "省略时生成普通图片;其它值选择对应的专用生成流程。External v1 当前不开放结构化游戏场景生成,scene 不能通过该通用图片接口提交。"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
@@ -3058,10 +3340,18 @@
|
||||
]
|
||||
},
|
||||
"assetKind": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"not": {
|
||||
"pattern": "^\\s*scene\\s*$"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "生成产物分类。External v1 通用图片接口禁止使用 scene;结构化游戏场景必须使用主站场景专用契约。"
|
||||
},
|
||||
"generationInputs": {
|
||||
"$ref": "#/components/schemas/JsonValue"
|
||||
@@ -3852,7 +4142,7 @@
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "角色动作绿幕预览视频写入账号素材库的文件夹;省略时写入默认项目素材文件夹。"
|
||||
"description": "角色动作预览视频和最终透明序列帧素材写入账号素材库的文件夹;省略时写入默认项目素材文件夹。"
|
||||
},
|
||||
"assetLabel": {
|
||||
"type": [
|
||||
@@ -3865,22 +4155,29 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EditorCharacterAnimationFrame": {
|
||||
"EditorImageSequenceFrame": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"frameIndex",
|
||||
"imageSrc",
|
||||
"objectKey",
|
||||
"assetObjectId",
|
||||
"width",
|
||||
"height"
|
||||
],
|
||||
"properties": {
|
||||
"frameIndex": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"imageSrc": {
|
||||
"type": "string"
|
||||
},
|
||||
"objectKey": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "帧对应的稳定 OSS Object Key;服务端据此重建持久 imageSrc,不能只提交临时签名 URL。"
|
||||
},
|
||||
"assetObjectId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "帧对应的稳定资产对象 ID。"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
@@ -3889,7 +4186,9 @@
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"description": "图片序列帧。数组位置是唯一播放顺序,不携带额外序号字段;每帧必须同时携带 objectKey 与 assetObjectId,imageSrc 按 objectKey 规范化为持久站内路径。"
|
||||
},
|
||||
"EditorCharacterAnimationGenerationResponse": {
|
||||
"type": "object",
|
||||
@@ -3926,7 +4225,7 @@
|
||||
"frames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/EditorCharacterAnimationFrame"
|
||||
"$ref": "#/components/schemas/EditorImageSequenceFrame"
|
||||
}
|
||||
},
|
||||
"frameCount": {
|
||||
@@ -3964,6 +4263,28 @@
|
||||
],
|
||||
"description": "当请求携带 canvasCompletion 且服务端成功写入画布布局时返回最新项目快照。"
|
||||
},
|
||||
"resource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EditorProjectResource"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "最终透明角色动作对应的项目资源。携带 projectId 时,画布图层必须直接引用其 resourceId,不得再次创建重复资源。"
|
||||
},
|
||||
"asset": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EditorAsset"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "最终透明角色动作素材。assetKind 为 character-animation,并直接包含序列帧字段。"
|
||||
},
|
||||
"queueState": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# 音频生成 Composer 恢复共享分流方案
|
||||
|
||||
日期:`2026-08-06`
|
||||
|
||||
状态:`已实施`
|
||||
|
||||
## 一、目标
|
||||
|
||||
图片画布的音效与背景音乐恢复使用同一个音频 composer。`ImageCanvasGenerationComposerView.tsx` 内只保留一个 `ImageCanvasAudioGenerationComposerView`,并在组件内定义:
|
||||
|
||||
```ts
|
||||
const isSoundEffect = dialog.mode === 'audio-sound-effect';
|
||||
```
|
||||
|
||||
`isSoundEffect === true` 渲染现有 SFX 分支,`isSoundEffect === false` 渲染现有 BGM V1 分支。删除完整的独立 BGM composer 文件,但保留确有独立职责的 BGM 纯模型、助手 controller 和预设跑马灯组件。
|
||||
|
||||
这次只调整前端视图组织方式,不改变任何生成契约、业务规则、请求时序、计费或持久化语义。
|
||||
|
||||
## 二、范围与非目标
|
||||
|
||||
### 2.1 必须保留的 BGM 能力
|
||||
|
||||
- `gpt_description_prompt` 可见原文、Unicode `White_Space` canonicalization 和 200 字生成限制。
|
||||
- 30 个预设、字符计数、AI 补全、一键简化和单层交换式撤销。
|
||||
- AI 处理锁、同步正式提交锁、`generating` 锁和迟到响应隔离。
|
||||
- 稳定 dialog ID、账号 / 项目 scope 校验和按 dialog ID 写回。
|
||||
- 固定 `Suno`、当前动态泥点价格和隐藏 `make_instrumental` 的现状。
|
||||
|
||||
### 2.2 必须保持不变的现有 SFX 能力
|
||||
|
||||
- 固定 Vidu `audio1.0`,Prompt 同值映射到现有 `prompt + sound` 请求字段。
|
||||
- `2–10` 秒、步长 `1` 秒、默认 `5` 秒。
|
||||
- 当前 Prompt 规范化、全空白时回退“游戏音效”、1500 字限制、价格和失败态。
|
||||
- 现有提交 callback、生成占位和完成链路。
|
||||
- SFX 中不出现 Suno、BGM 预设、AI 补全、一键简化、BGM 字符计数或撤销。
|
||||
|
||||
### 2.3 本次明确不做
|
||||
|
||||
- 不实现 SFX V2 独有的一键优化、自动中译英、ElevenLabs、自动时长、30 秒、Loop 或 SFX 预设。
|
||||
- 不修改 BGM 或 SFX 需求原文。
|
||||
- 不修改 BFF、队列、`platform-audio`、External v1、OpenAPI、SpacetimeDB schema、计费或重试。
|
||||
- 不把 BGM Prompt controller 泛化为音频通用 controller。
|
||||
- 不新建配置驱动的 composer 框架、第二套音频组件或其它顺带重构。
|
||||
|
||||
## 三、当前问题
|
||||
|
||||
当前 `ImageCanvasGenerationComposerView.tsx` 分别渲染 SFX 内部组件和 `ImageCanvasBackgroundMusicGenerationComposerView.tsx`。这层完整视图拆分让同一个音频入口形成两套 composer 边界,而 SFX V2 需求已经表明预设、AI 写回、撤销和锁定等交互并非 BGM 永久独占,继续用“BGM 专属交互较多”作为完整组件分叉理由不再成立。
|
||||
|
||||
同时,最近一次合并把共享架构中的 `isSoundEffect` 条件表达式带回了当前 SFX-only 组件,却没有带回变量定义,形成 `isSoundEffect is not defined`。该问题不是增加一个局部常量后就可以收口的长期架构问题;本次重构应恢复单一音频组件,使变量与它控制的两个分支重新处于同一组件边界。
|
||||
|
||||
## 四、目标结构
|
||||
|
||||
```text
|
||||
ImageCanvasEditorView
|
||||
└─ useImageCanvasGenerationSurface
|
||||
└─ ImageCanvasGenerationComposerView
|
||||
└─ ImageCanvasAudioGenerationComposerView
|
||||
├─ isSoundEffect === true → 现有 SFX UI 与行为
|
||||
└─ isSoundEffect === false → 现有 BGM V1 UI 与行为
|
||||
└─ ImageCanvasBackgroundMusicPresetMarquee
|
||||
```
|
||||
|
||||
继续保留的 BGM 专项模块:
|
||||
|
||||
- `ImageCanvasBackgroundMusicPromptModel.ts`:canonicalization、计数、动作资格和 dialog 类型收窄。
|
||||
- `useImageCanvasBackgroundMusicPromptAssist.ts`:按 dialog ID 隔离的异步助手状态与提交锁。
|
||||
- `ImageCanvasBackgroundMusicPresetModel.ts`:30 个预设与追加规则。
|
||||
- `ImageCanvasBackgroundMusicPresetMarquee.tsx`:展开、滚动、hover、触摸和 reduced-motion。
|
||||
|
||||
删除的完整视图模块:
|
||||
|
||||
- `ImageCanvasBackgroundMusicGenerationComposerView.tsx`
|
||||
- `ImageCanvasBackgroundMusicGenerationComposerView.test.tsx`
|
||||
|
||||
删除测试文件不等于删除覆盖;其中全部用例必须迁入总 composer 测试。
|
||||
|
||||
## 五、实现边界
|
||||
|
||||
### 5.1 单一渲染入口
|
||||
|
||||
`ImageCanvasGenerationComposerView` 对 `audio-sound-effect` 与 `audio-background-music` 只保留一个渲染条件和一个 `ImageCanvasAudioGenerationComposerView` 调用。组件内部以 `isSoundEffect` 选择两棵现有表单子树,不为本次重构新造配置层。
|
||||
|
||||
共享组件使用基于 dialog ID 与 mode 的稳定 `key`。连续切换 BGM dialog 时,预设展开、滚动、hover 和触摸状态不得继承;从 SFX 切到 BGM 时,音效时长菜单状态也不得泄漏。
|
||||
|
||||
### 5.2 Hook 与类型安全
|
||||
|
||||
- React Hook 不得放进 `isSoundEffect` 条件分支。`useState`、`useRef`、`useId` 和 `useImageCanvasFloatingOptionDismiss` 保持固定调用顺序。
|
||||
- `GenerateDialogState` 不是可判别联合。BGM 分支继续通过 `toBackgroundMusicGenerationDialog` 取得带稳定 ID 的 `BackgroundMusicGenerationDialogState`。
|
||||
- BGM 缺少稳定 ID 或 `backgroundMusicPromptAssist` 时失败关闭,不渲染不完整的 BGM 表单;SFX 不依赖这两个条件。
|
||||
|
||||
### 5.3 两套状态写回不得合并
|
||||
|
||||
- SFX 继续走现有 `setGenerateDialog` 路径,保持按 mode 更新、失败态复位和时长写回语义。
|
||||
- BGM 继续走 `updateCanvasGenerationDialogById(dialog.id, updater)`,不能降级成只比较 mode。助手响应、预设、撤销和提交锁仍按稳定 dialog ID 隔离。
|
||||
- `backgroundMusicPromptAssist` 可以继续由 surface 传给共享 composer,但只允许 BGM 分支读取或调用。
|
||||
|
||||
### 5.4 锁定与提交资格不得串用
|
||||
|
||||
|分支|锁定条件|生成资格|
|
||||
|---|---|---|
|
||||
|SFX|沿用现有 `dialog.status === 'generating'`|沿用现有 SFX 提交入口和默认 Prompt 规则|
|
||||
|BGM|AI processing、`submitting` 或 `generating` 任一成立|沿用 canonical Prompt 的有效字符与 `1–200` code point 规则|
|
||||
|
||||
BGM 使用 `PlatformTextField + readOnly + aria-invalid`;SFX 继续使用 `AutoGrowTextArea + disabled`。本次不统一这两个输入控件,也不改错误、可访问名称或按钮文案。
|
||||
|
||||
## 六、预计代码改动
|
||||
|
||||
|文件|最小改动|
|
||||
|---|---|
|
||||
|`ImageCanvasGenerationComposerView.tsx`|恢复共享 `ImageCanvasAudioGenerationComposerView` 与 `isSoundEffect`;移入现有 BGM JSX、常量和依赖;删除独立 BGM import 与双渲染入口|
|
||||
|`ImageCanvasGenerationComposerView.test.tsx`|迁入独立 BGM composer 的全部测试,并增加双 mode 隔离与切换回归|
|
||||
|`ImageCanvasBackgroundMusicGenerationComposerView.tsx`|删除|
|
||||
|`ImageCanvasBackgroundMusicGenerationComposerView.test.tsx`|覆盖迁完后删除|
|
||||
|`useImageCanvasBackgroundMusicPromptAssist.ts`|只修正指向独立 composer 的历史注释;不改 controller 行为|
|
||||
|
||||
以下生产文件预计不改:`useImageCanvasGenerationSurface.tsx` 的现有 props 接线、BGM Prompt / 预设模型、预设跑马灯、submission workflow、submission model、dialog model、`src/index.css` 的现有 BGM 样式,以及全部后端代码。
|
||||
|
||||
如实施时发现必须超出该清单才能保持现有行为,应先停下并重新确认边界,不能借本次组件归并顺带重构。
|
||||
|
||||
## 七、测试迁移与回归矩阵
|
||||
|
||||
### 7.1 BGM 原覆盖完整迁移
|
||||
|
||||
独立组件测试中的下列覆盖必须逐项迁入 `ImageCanvasGenerationComposerView.test.tsx`:
|
||||
|
||||
- canonical preview 计数、超限展示和不改写输入框。
|
||||
- 0 / 1 / 2 个有效字符及 200 / 201 / 2000 / 2001 边界。
|
||||
- 补全、简化、撤销和预设按正确 dialog ID 路由。
|
||||
- `preparePreset` 拒绝时不写回,预设成功时沿用追加和清快照语义。
|
||||
- 助手错误与生成错误的展示顺序。
|
||||
- Suno、泥点价格、生成允许 / 拒绝。
|
||||
- `completing`、`simplifying`、`submitting`、`generating` 锁定。
|
||||
- 完整单层撤销按钮矩阵和现有可访问属性。
|
||||
|
||||
### 7.2 mode 隔离与切换
|
||||
|
||||
- SFX 渲染时不存在 BGM 控件,也不存在本次明确排除的 SFX V2 控件。
|
||||
- BGM 渲染时不存在 Vidu 和音效时长控件。
|
||||
- SFX 仍保持 `audio1.0`、2–10 秒、默认 5 秒、当前价格和既有提交参数。
|
||||
- BGM dialog A 展开预设后切到 dialog B,局部展开与滚动状态不继承。
|
||||
- SFX 与 BGM 相互切换时,菜单、锁和 Prompt 助手状态不跨 mode 泄漏。
|
||||
- 缺少稳定 ID 或 controller 的 BGM 失败关闭;同样条件不影响 SFX 正常渲染。
|
||||
|
||||
### 7.3 最小验证命令
|
||||
|
||||
```powershell
|
||||
npm run test -- src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasBackgroundMusicPromptModel.test.ts src/components/image-editor/ImageCanvasBackgroundMusicPresetModel.test.ts src/components/image-editor/ImageCanvasBackgroundMusicPresetMarquee.test.tsx src/components/image-editor/useImageCanvasBackgroundMusicPromptAssist.test.tsx
|
||||
npm run test -- src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx
|
||||
npm run typecheck
|
||||
npm run check:encoding
|
||||
git diff --check
|
||||
```
|
||||
|
||||
删除旧测试文件后,还要确认测试收集清单中不再引用它。若本次改动触发其它现有图片画布测试失败,只修复由共享 composer 归并直接造成的回归,不扩大到无关模块。
|
||||
|
||||
## 八、实施顺序
|
||||
|
||||
1. 在总 composer 内恢复共享音频组件、`isSoundEffect` 和单一音频渲染入口。
|
||||
2. 原样迁入 BGM 分支,保留按 ID 写回、锁定、错误顺序、Suno 与预设行为。
|
||||
3. 迁移独立 BGM 组件测试,并补齐 SFX/BGM 隔离与切换用例。
|
||||
4. 删除独立 BGM composer 及其测试文件,修正相关注释与文档引用。
|
||||
5. 运行定向测试、typecheck、编码检查和差异检查;实现完成后再把实际结果补入 T6 后续记录。
|
||||
|
||||
## 九、完成定义
|
||||
|
||||
- 两个音频 mode 均从同一个 `ImageCanvasAudioGenerationComposerView` 渲染,且组件内存在唯一的 `isSoundEffect` 分流。
|
||||
- 独立完整 BGM composer 文件和独立测试文件已删除,原测试覆盖无遗漏地迁入总 composer。
|
||||
- BGM V1 所有已交付能力与正式提交语义不变。
|
||||
- 现有 SFX UI、Prompt、Vidu、时长、价格、校验和提交链路无回归。
|
||||
- 没有实现任何 SFX V2 独有功能,没有修改需求原文或后端契约。
|
||||
- 规定的定向测试、typecheck、编码检查和 `git diff --check` 全部通过。
|
||||
|
||||
## 十、实施结果
|
||||
|
||||
2026-08-06 已按本文边界完成:
|
||||
|
||||
- `ImageCanvasGenerationComposerView.tsx` 恢复唯一 `ImageCanvasAudioGenerationComposerView`,由组件内 `isSoundEffect` 分流;BGM 继续按稳定 dialog ID 写回,SFX 继续走原 `setGenerateDialog` 路径。
|
||||
- 删除 `ImageCanvasBackgroundMusicGenerationComposerView.tsx`,保留 BGM Prompt / 预设纯模型、助手 controller 和预设跑马灯。
|
||||
- 把独立组件全部用例迁入 `ImageCanvasGenerationComposerView.test.tsx` 后删除旧测试文件;总 composer 现有 50 项测试同时覆盖 BGM 完整动作矩阵和 SFX/BGM 控件、状态、dialog 切换隔离。
|
||||
- 只修正 `useImageCanvasBackgroundMusicPromptAssist.ts` 的组件归属注释;没有修改 controller、surface、submission workflow、样式、后端、契约或需求原文,也没有实现 SFX V2 独有功能。
|
||||
|
||||
实际验证:
|
||||
|
||||
- Prompt / 预设 / controller / 总 composer:`121/121`。
|
||||
- surface 与 submission workflow:`72/72`。
|
||||
- `npm run typecheck`、变更文件 ESLint、Prettier、`npm run check:encoding` 和 `git diff --check`:全部通过。
|
||||
|
||||
2026-08-06 已执行共享 composer 归并后的浏览器视觉 smoke,未发现阻断性问题;本轮未创建真实音频生成任务或产生扣费。
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -14,6 +14,22 @@
|
||||
- 关联:相关文件、文档、提交或 Issue
|
||||
```
|
||||
|
||||
## 派生 Debug 会让完整配置经应用状态递归进入日志
|
||||
|
||||
- 现象:配置和状态当前没有直接日志调用,但新增一行 `debug!(?state, ...)` 或 `format!("{config:?}")` 就能把 JWT、后台口令、支付私钥、OSS / provider key 与 SpacetimeDB token 一次性写入日志及 OTel 留存面。
|
||||
- 原因:`AppConfig`、`AppState` 与 `AppStateInner` 曾使用派生 `Debug`;状态继续递归格式化多个含配置的 client。即使顶层状态停止下钻,`SpacetimeClientConfig` 及 `SpacetimeClient` 的独立手写路径仍会绕过顶层防线。
|
||||
- 处理:配置和聚合状态只实现封闭的手写安全摘要,不格式化任一自由字符串或含凭据的嵌套 client;`SpacetimeClientConfig` 独立脱敏,`SpacetimeClient` 只复用该安全摘要。不要以默认 `info` 级别或当前零调用点代替代码约束。
|
||||
- 验证:同一唯一哨兵同时填入全部凭据字段、可能带凭据的 SpacetimeDB URL 和数据库名,逐一格式化 `AppConfig`、`AppStateInner`、`AppState`、`SpacetimeClientConfig`、`SpacetimeClient`,断言哨兵零出现且安全运行摘要仍存在。
|
||||
- 关联:`server-rs/crates/api-server/src/config.rs`、`server-rs/crates/api-server/src/state.rs`、`server-rs/crates/spacetime-client/src/active.rs`、Issue #148。
|
||||
|
||||
## Chat 生成预算字段不能按模型名猜测或失败后自动重放
|
||||
|
||||
- 现象:同一个 OpenAI-compatible Chat endpoint 调用 reasoning 模型时返回 `Unsupported parameter: max_tokens`;直接把全局请求字段改成 `max_completion_tokens` 后,旧兼容网关又可能拒绝新字段。
|
||||
- 原因:内部生成预算语义与上游 wire dialect 被混在一起。Chat 当前字段是 `max_completion_tokens`,旧兼容层仍只接受 `max_tokens`;Responses 和 Anthropic 又分别使用自己的字段。模型名、base URL 和 `OpenAiCompatible` 标签都不能证明 endpoint 能力,收到 `400` 后重发还可能重复计费。
|
||||
- 处理:在 `LlmConfig` 上显式声明 Chat token budget field capability;通用兼容配置默认 legacy,已验证的 VectorEngine 专用 client opt-in `max_completion_tokens`,每次只发送一个字段。内部 `max_output_tokens` 与 AGC 持久指纹键 `maxOutputTokens` 保持不变。
|
||||
- 验证:序列化测试分别断言 modern / legacy Chat 只出现选定字段,请求级 model override 不改变字段;Responses 继续只发 `max_output_tokens`,Anthropic 继续只发 `max_tokens`;AppState 测试断言 VectorEngine client 已显式启用 modern capability。
|
||||
- 关联:`server-rs/crates/platform-llm/src/lib.rs`、`server-rs/crates/api-server/src/state.rs`、`scripts/test-ve-llm.mjs`、Issue #143。
|
||||
|
||||
## Runtime 状态写失败不能发生在公开失败消息之前
|
||||
|
||||
- 现象:用户提交长任务后只看到运行失败或任务直接消失,聊天里一条有用消息都没有;另一些失败又同时出现 Runtime event 和 conversation 两条近似提示。
|
||||
@@ -35,18 +51,26 @@
|
||||
|
||||
- 现象:用户只说“现在没有用到任何美术资源”,Graph 就在 Supervisor 输出任何计划前自动打开美术节点;或者用户想复用现有素材,Runtime 直接按关键词预完成节点。Supervisor 无固定计划时随即 `fixed-task-graph-stalled`,看起来像模型不理解意图,实际上模型根本没有获得决策机会。
|
||||
- 原因:同一套关键词函数同时承担 prompt hint、Graph reset、baseline 豁免和历史试玩类型继承,启发式信号越过 Supervisor 成为了控制面真相;main loop 又在 Provider 请求前优先调度 ready task。
|
||||
- 处理:启发式结果只序列化成 `advisoryOnly=true` 的 Supervisor context。Scheduler 以持久 `GameChatWorkflowDecision` 为首轮前置门;Supervisor 先选审计或整体重做,code-director 再用成功 `asset.list` 和 Runtime 复核的覆盖合同选择复用或精确补缺。Runtime 可以拒绝过期、伪造、遗漏或重复 route,但不得替 Supervisor 补写决定。
|
||||
- 验证:直接使用用户原句,断言 hint 命中但 manifest 全部保持 pending、决策前零 child、Provider request 包含路由工具;决策后首波只有 design/code,code-director 未完成 asset.list/route 时不能完成;再分别覆盖完整复用、真实缺口和显式重做。
|
||||
- 处理:启发式结果只序列化成 `advisoryOnly=true` 的 Supervisor context。Scheduler 以持久 `GameChatWorkflowDecision` 为首轮前置门;Supervisor 只持久化用户 intent,随后由唯一 `code-prototype` 用成功 `asset.list` 和 Runtime 复核的覆盖合同选择复用或精确补缺。Runtime 可以拒绝过期、伪造、遗漏或重复 route/delivery,但不得替 Supervisor 补写决定或固定生成美术。
|
||||
- 验证:直接使用用户原句,断言 hint 命中但 manifest 全部保持 pending、决策前零 child、Provider request 包含路由工具;决策后只启动 code-prototype,它未完成 asset.list 时不得委派美术;再分别覆盖完整复用、真实缺口和显式重做。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs`、`runtime_driver/main_loop.rs`、`runtime_driver/task_start.rs`、`runtime_protocol/autonomous_completion.rs`。
|
||||
|
||||
## 既有正式产物不能同时被快车道视为已完成、被本轮 baseline 门视为未变化
|
||||
|
||||
- 现象:增量任务已有完整美术图集,`art-asset-plan` 每轮都返回零 action 和“已验证交付”,但 completion gate 每轮都报告 `assets/manifest.art.json(unchanged-from-run-baseline)`;最终 child `loop-budget-exhausted`,随后 Graph 和父 Run 失败。日志中没有本轮 Provider request、tool plan 或 action receipt。
|
||||
- 原因:Graph reset 无差别重新打开稳定的美术 owner 节点;快车道按“当前产物有效”判断完成,owner 完成合同则按“本轮必须修改 baseline 产物”判断完成,两套语义互相冲突。增加 loop 预算、伪造版本号或机械改写 manifest 都不能消除冲突,还会引入 verification loop、字段丢失或错误复用旧主题。
|
||||
- 处理:关键词和资产探测只作为 Supervisor 的 advisory context,不能直接修改 Graph。根 Run 先持久化 `audit-existing-first / regenerate-art` 决策;前者只启动 `design-director + code-director`,由 code-director 在成功 `asset.list` 后提交权威覆盖合同和精确缺口。Runtime 验证合同后才把已有 owner 投影为 completed,或只打开缺口 owner;根完成门只对持久路由明确复用的 art manifest 忽略 baseline 相同,所有结构、Canvas、私有回执、切片和可见使用验收继续失败关闭。
|
||||
- 验证:先断言 Supervisor 决策前零 child、固定关键词不会预完成节点,再覆盖完整复用、仅缺图集和明确重做。还要直接经过父完成门,证明合法持久路由不再出现 art baseline gap,并证明删除切片后覆盖合同拒绝复用;旧 root、错误 fingerprint、虚构或遗漏缺口和重复改写路由都应失败关闭。
|
||||
- 处理:关键词和资产探测只作为 Supervisor 的 advisory context,不能直接修改 Graph。根 Run 先以 `audit-existing-first` 持久化用户 intent;即使用户提出整体视觉重做,这也不授权强制重生成。决策后只启动 `code-prototype`,由它在成功 `asset.list` 后提交或建立权威覆盖/缺口 delivery。Runtime 验证合同后才允许已有资产复用,或只打开精确缺口 owner;根完成门继续要求主 Agent 认领回执并完成接入、Canvas、私有回执、切片、可见使用和试玩验收。
|
||||
- 验证:先断言 Supervisor 决策前零 child、固定关键词不会预完成节点,再覆盖完整复用、仅缺图集和明确重做。还要直接经过父完成门,证明合法持久 route/delivery 不再出现 art baseline gap,并证明删除切片后覆盖合同拒绝复用;旧 root、错误 fingerprint、虚构或遗漏缺口、重复委派以及 child 写入 `game/**` 都应失败关闭。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`。
|
||||
|
||||
## 单主 Graph 升级不能只迁移 sidecar,必须同时处理活跃旧 Run
|
||||
|
||||
- 现象:v1 decision/route 能迁移,但升级前正在运行的 `code-prototype` 因 prompt 文本变化被 scheduler 判为身份冲突;同时旧 fixed-graph 的 `art-director / art-asset-plan` 仍持有 scheduler binding,可以脱离新主 Agent 继续生图。
|
||||
- 原因:迁移测试只手工构造了非确定性主 Run,未经过真实 ready scheduler;资源 route 的迁移也没有自动让已启动的旧责任链失效。硬截止处理若仍只接受根的直接 child,还会把新的嵌套美术 child 留在 running,而丢失 reconciliation 投影。
|
||||
- 处理:确定性主 Run 只白名单兼容已知 canonical task 文本版本,所有其它身份字段继续精确校验;旧 fixed-graph 美术 child 及沿 isolated instance 父链可证的历史后代在计划和所有非只读工具入口失败关闭,只允许当前 `code-prototype` 经 `asset.list` 后重新委派。新美术 child 同样使用显式只读白名单,写工具只允许可证明落在 `assets/**` 的文件/patchset 与 `canvas.asset_generate`,不能借 `memory.write` 或 `task.create/update` 修改 memory 和 manifest;会认领 delivery 并写 observed 状态的 `agent.run_status` 也不是只读。绝对硬截止显式验证根、主 Agent、delegated art child 的完整 task/binding/delegation 链。
|
||||
- 验证:用真实 scheduler 恢复确定性 v1 主 Run;把旧 scheduler 美术 child 置为 running,断言 `canvas.asset_generate`、`memory.write`、`task.create`、`task.update` 和 `agent.run_status` 均被拒绝;再持久化其历史 `game/**` writeScope isolated 后代,断言恢复执行写操作仍失败且项目未变。对当前合法美术 child 同样验证 memory/manifest 零写入,再让它带在途外部生成命中硬截止,断言状态进入 `needs-reconciliation` 且 pending/batch/外部生成账本原样保留。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs`、`runtime_driver/game_chat_fast_path.rs`、`runtime_driver/main_loop.rs`、`runtime_tools/file_ops.rs`。
|
||||
|
||||
## 执行锁移交给未确认启动的异步 future 会制造永久 queued
|
||||
|
||||
- 现象:父 Supervisor 与 Runner 一直显示运行中、heartbeat 正常,专业 Agent 已有 `background_task.queued` 和 `autonomous_ready_task.scheduled`,对应执行锁也被 Runner 持有,但该 child 永远没有 running journal、`turn.started` 或后续 Runtime event;其它同批 Agent 可能已经完成。
|
||||
@@ -70,6 +94,7 @@
|
||||
- 处理:旧 action 继续禁止重放或伪造 observation;旧 child 与父 Run 先真实终态。新 Supervisor continuation 仅扫描同 Session、同 source、同有效任务合同的历史根 Run,并要求对应 ready-task 同时存在 `failed / needs-reconciliation` 记录、最终 `cancelled` 记录和 durable cancel tombstone,才把当前 manifest 的同一 failed 节点恢复为 pending,让 scheduler 创建新 child Run。manifest 的读取、筛选、child 证据重验和写回放在同一项目写锁内;每个 task journal 只读取一次并按 parent Run 建索引。较新的无 child Run 默认阻断旧凭证,只有其 root journal 精确证明为旧 failed Graph 在进入 scheduler 前即失败时才允许向前查找;scheduler 自身失败不得被当成该兼容场景。
|
||||
- 验证:构造 reconciliation child、人工 cancel tombstone、failed manifest 和终态父 Run,证明同源 continuation 只重排该节点;并列普通 failed 节点保持 failed,完成合同继续继承原任务 SHA 与项目 baseline,旧 pending action 不恢复。追加覆盖“旧 failed Graph 未调度”的中间 Run 可以跨过,而较新的 scheduler failure 即使没有 child journal 也会阻断更老 tombstone。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs`。
|
||||
|
||||
## `timeout_at` 不能替代显式的预算耗尽预检
|
||||
|
||||
- 现象:给完美像素加端点级并发闸后,预算已经耗尽的请求仍然能拿到许可,白占一个名额继续去打几轮全账号 SpacetimeDB 扫描,直到下载那步才失败。
|
||||
@@ -85,6 +110,7 @@
|
||||
- 处理:把递增封进一个 guard 结构体,递减放在它的 `Drop` 实现里;递增本身用 `fetch_update` 的 CAS,不能用「先读后加」——两个线程同时读到 `max - 1` 各自加一就会越界。拿到资源后立即 `drop(guard)` 让出队列名额,不要让它跟着许可一起活到请求结束。
|
||||
- 验证:单测覆盖 CAS 边界(满了返回失败且计数不越界、上限为 0 时任何进入都失败),并由独立用例覆盖 guard 离开作用域后的计数归还。预算耗尽路径只断言 `504`,不得通过另一个测试也会修改的进程级 static before/after 来推断“未入队”,也不得用串行锁或 `--test-threads=1` 掩盖隔离问题。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_project.rs`(`try_enter_bounded_queue`、`EditorPixelArtSnapQueueGuard`)。
|
||||
|
||||
## Linux 生产脚本门禁不能假设本地也是 GNU userland
|
||||
|
||||
- 现象:macOS 本地运行维护页、生产 API 部署和 Rust 产物门禁时,依次出现 `mv: illegal option -- T`、`mapfile: command not found`、`/usr/bin/cp` / `/usr/bin/chmod` 不存在,以及 `.rlib` 明明含有 `.o` 却报告“没有可扫描成员”;安全修复计划还会把 `/var/folders` 到 `/private/var/folders` 的系统别名误判为用户符号链接。
|
||||
@@ -274,7 +300,7 @@
|
||||
|
||||
- 现象:测试点击“添加素材”后,图层状态已经写入,但立即用 `getByAltText('画布图片:...')` 偶发或稳定找不到图片;前一张图可能通过,紧接着添加的第二张失败。
|
||||
- 原因:带 `objectKey` 的画布图片通过 `useResolvedAssetReadUrl` 异步获取签名 URL,`resolvedUrl` 就绪前不会渲染带 `alt` 的 `<img>`。`user.click` 只等待点击交互完成,不等待 effect 内的换签 Promise;前一张图在后续操作期间出现只是调度时机,不是同步契约。
|
||||
- 处理:每次点击添加后分别用 `await screen.findByAltText(...)` 等待对应图片可见,再执行依赖该图层的下一步操作;不要用固定 sleep,也不要只等待最后一张图而让前面的断言依赖偶然调度。
|
||||
- 处理:每次点击添加后分别用 `await screen.findByAltText(...)` 等待对应图片可见,再执行依赖该图层的下一步操作;不要用固定 sleep,也不要只等待最后一张图而让前面的断言依赖偶然调度。完整前端回归并行负载较高时,可只对明确跨越换签 Promise 的目标查询设置局部、有界的 `5_000ms` 超时,不要放宽 Testing Library 全局超时。
|
||||
- 验证:先精确运行目标用例并连续重复,再运行所在测试文件和完整前端测试;删除场景仍要保留 A/B 都消失、两个删除调用和撤销不恢复已删除素材的断言。
|
||||
- 关联:`src/hooks/useResolvedAssetReadUrl.ts`、`src/components/image-editor/ImageCanvasWorldView.tsx`、`src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`。
|
||||
|
||||
@@ -286,6 +312,14 @@
|
||||
- 验证:`ImageCanvasEditorModel.test.ts` 覆盖素材库 source resource 保留,`useImageCanvasAssetCanvasBridge.test.tsx` 覆盖资源 ID 级联清理,`ImageCanvasEditorAssetsIntegration.test.tsx` 覆盖删除后保存的新 layout 不再包含被删图层。
|
||||
- 关联:`src/components/image-editor/ImageCanvasEditorModel.ts`、`src/components/image-editor/useImageCanvasAssetCanvasBridge.ts`、`src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`。
|
||||
|
||||
## 图片画布素材选择有效性不要绑定搜索与折叠可见性
|
||||
|
||||
- 现象:批量选择多个素材后,搜索、折叠文件夹或展开文件夹会让已选数量下降、Shift 范围锚点丢失,后续批量下载或删除遗漏此前已选素材。
|
||||
- 原因:搜索结果和文件夹展开状态只描述当前 UI 可见范围,不描述素材是否仍然有效;用 `visibleAssetIds` reconcile 全局选择会把暂时隐藏误判为素材失效。
|
||||
- 处理:由唯一 `useImageCanvasAssetSelection` 持有选择集合、范围锚点、框选和全部选择 mutation;全局选择只按全部 `selectableAssetIds` 清理真正删除、上传未完成、上传失败或媒体地址无效的 ID。`visibleAssetIds` 只作为单项切换、Shift 可见区间和当前结果全选 / 取消全选的动作入参,批量下载与删除消费 hook 输出的完整 `selectedAssets`;删除中包含当前未显示选择时,必须明确展示全部数量和未显示数量并二次确认。
|
||||
- 验证:模型测试覆盖隐藏选择保留、可见范围增量和真正失效 ID 清理;图片画布素材集成测试覆盖搜索、折叠 / 展开后选中数量稳定及当前可见全选不影响隐藏选择。
|
||||
- 关联:`src/components/image-editor/useImageCanvasAssetSelection.ts`、`src/components/image-editor/useImageCanvasAssetLibrary.ts`、`src/components/image-editor/ImageCanvasSidebarView.tsx`、`src/components/image-editor/ImageCanvasEditorView.tsx`。
|
||||
|
||||
## 后台素材查询不要用 SQL 直查 editor_asset
|
||||
|
||||
- 现象:后台“素材查询”报 `HTTP 400:no such table: editor_asset. If the table exists, it may be marked private.`。
|
||||
@@ -343,6 +377,14 @@
|
||||
- 验证:画板生成 workflow 测试覆盖 queueState 持续 `running` 到前端等待窗口结束时,不进入 failed、不显示该排队文案、不添加本地临时结果层。
|
||||
- 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`。
|
||||
|
||||
## 场景队列终态不保证首次项目快照已经收口生成占位
|
||||
|
||||
- 现象:游戏场景任务已经显示完成,但画布仍保留 `generating` 占位;场景链路又禁止用本地结果补层,因此当前会话可能一直停在生成中。
|
||||
- 原因:外部生成任务终态与项目画布投影不是同一个原子观测点。队列轮询先看到 `completed` 后,紧接着的首次项目 GET 仍可能读到同一 `dialogId` 的未收口占位;若调用统一回读函数时没有传 completion dialog ID,函数无法识别该快照仍未完成,也不会执行已有的有界延迟重读。
|
||||
- 处理:游戏场景队列调用要把本次占位 `dialogId` 传给 `applyQueuedEditorGenerationProject`。首个快照中该 ID 仍为 unresolved 时,只按既有间隔补读一次项目;不追加本地图层,也不把任务终态直接等同于画布投影终态。其他生成类型若要补同类保护,必须分别复现其权威回填时序后再改,不能用本条场景结论替代验证。
|
||||
- 验证:场景 workflow 用两个连续快照复现时序:第一个保留 `scene / generating`,第二个包含场景结果并把同一占位置为 `idle`。修复前只读一次并超时,修复后依次应用两个权威快照。
|
||||
- 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`、`docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md`。
|
||||
|
||||
## 图片画布历史不能回退当前权威状态或复活后端已删素材
|
||||
|
||||
- 现象:生成占位框移动后开始生成,撤销移动会把仍在运行的生成对象恢复成待生成状态;切换到 2K 或改变比例后撤销位置,旧占位框还可能把当前尺寸回退。上传图层落库后,普通移动撤销可能被提示“可能会使图片消失”并永久卡在栈顶;即使安全检查已放行,直接恢复旧图层快照也会丢失刚回填的资源关联。素材库后端删除关联素材后,更早的移动快照还可能把已删图层重新加入并自动保存;修改素材类型虽然界面提示撤销成功,刷新后却可能从仍指向新类型的 resource 回弹。无稳定 ID 的“修改图片”草稿也可能被 target-null 快照直接关闭。
|
||||
@@ -512,6 +554,14 @@
|
||||
- 验证:`npm run test -- src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx`;`cargo test -p api-server inline_data_url --manifest-path server-rs/Cargo.toml`。
|
||||
- 关联:`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/services/image-editor/editorProjectClient.ts`、`server-rs/crates/api-server/src/editor_generation_queue.rs`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。
|
||||
|
||||
## 专用生成契约不能被通用生成接口和任务摘要绕过
|
||||
|
||||
- 现象:专用场景接口要求结构化 `sceneContent + stylePreset`,但调用方仍可向通用图片接口传 `kind = scene` 或 `assetKind = scene`,用任意完整 Prompt 生成并持久化正式场景;合法场景入队后,任务侧栏还可能显示后端完整规则文本和通用“生成图片”标题,空白素材名则可能回退成完整 Prompt。
|
||||
- 原因:专用 handler 内部复用了通用图片 payload、队列和 Worker,但公开通用 HTTP handler 没有限制专用身份;任务摘要又无条件优先提取 payload 顶层 `prompt`,素材名默认值只处理了字段省略,没有处理空白字符串。
|
||||
- 处理:公开通用 handler 拒绝专用 `kind / assetKind`,专用 handler 仍可直接调用内部共享执行函数;队列投影按 `kind = scene` 从权威 `generationInputs.fields[画面内容]` 派生标题和摘要,缺字段时失败关闭而不是回退内部 Prompt,并重新计算历史缓存;专用素材名统一把省略和空白收口为产品默认值。
|
||||
- 验证:路由测试先证明旁路会越过 HTTP 边界,再断言两种旁路均返回 `400` 且指向专用端点;摘要测试覆盖新任务、历史错误缓存和缺少画面内容三种情况;标签测试覆盖省略、空白、自定义和 80 字上限。
|
||||
- 关联:`server-rs/crates/api-server/src/editor_project.rs`、`server-rs/crates/spacetime-module/src/external_generation.rs`、`docs/technical/【技术方案】图片画布游戏场景生成链路-2026-08-04.md`。
|
||||
|
||||
## 图片编辑器角色动画必须提交稳定图片引用
|
||||
|
||||
- 现象:图片编辑器里对尚未上传的角色图点击 `生成动画` 后,前端或后端返回 `sourceImageSrc 必须先上传 OSS`。
|
||||
@@ -4000,11 +4050,18 @@
|
||||
|
||||
- 现象:`Repository checks`、`Frontend tests`、`Backend tests` 和 `Native shell tests` 都从全新 job 容器开始,apt、setup-node、rustup 和原生系统库在不同 job 里重复安装;后端与原生壳的安装时间可达数分钟,并把软件源和代理瞬时失败放大为四份。
|
||||
- 原因:Gitea Actions job 彼此隔离,上一个 job 在容器内安装的包不会自动进入下一个 job;把同一套不随 PR 变化的工具链写在 workflow step 中,必然每次重做。
|
||||
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、Rust 1.96、`rustfmt`、Chrome、`bwrap`、`rg`、`ffmpeg`、`clang/lld` 和 Tauri / 后端系统依赖,并按锁预热根 npm、server-rs 与桌面壳 Cargo 下载缓存。四个 job 统一 `runs-on: genarrative-ci`,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh`,同时检查缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0`;`rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
|
||||
- 处理:用 `deploy/container/gitea-ci-job.Dockerfile` 预装 Node 22、Rust 1.96、`rustfmt`、Chrome、`bwrap`、`rg`、`ffmpeg`、`clang/lld` 和 Tauri / 后端系统依赖,并按锁预热根与 AI 游戏创作壳 npm、server-rs、桌面壳与 AI 游戏创作壳 Cargo 下载缓存。四个 job 统一 `runs-on: genarrative-ci`,先用镜像内脚本直接从 Gitea checkout,再以 runtime 模式运行 `scripts/check-gitea-ci-job-image.sh`,同时检查五份缓存锁、工具链、完整 bwrap 与 Chrome headless。`RUSTUP_AUTO_INSTALL=0`;`rust-toolchain.toml` 变更时先重建镜像,不把下载 fallback 放回 job。
|
||||
- 依赖边界:每个 job 仍必须各自执行 `npm ci`,让当前 lockfile 和 PR 依赖在干净环境中验证;区别是命中镜像 cache 时只做本地解包,锁新增依赖时才走受控网络。不要把 `node_modules` 或 Cargo `target` 烘进镜像,也不要向不受信任 PR 挂载跨 job 可写 cache。
|
||||
- 锁漂移边界:runtime 校验输出 `server_rust_cache_lock=partial` 说明镜像内 Cargo lock 与当前 checkout 不同,不代表新增 crate 已经缓存。必须在新镜像中以 `--network none` 对当前 lock 执行真实 `cargo fetch/build --offline`;`cargo metadata --no-deps` 不会证明依赖 archive 可用,不能作为替代。
|
||||
- 锁漂移边界:runtime 校验输出任一 `*_cache_lock=partial` 说明镜像内 lock 与当前 checkout 不同,不代表新增依赖已经缓存;必须同时输出 Actions warning,提示可信分支落地后刷新镜像。必须在新镜像中对 server-rs、桌面壳和 AI 游戏创作壳当前 lock 执行真实 `cargo fetch --locked --offline`;`cargo metadata --no-deps` 不会证明依赖 archive 可用,不能作为替代。
|
||||
- 构建网络边界:`CARGO_NET_RETRY` 只覆盖部分 crate 下载,registry `config.json` / index TLS 握手仍可能直接终止整次 fetch。Dockerfile 对每个 `cargo fetch --locked` 再做最多 5 次整命令级有界重试,最终仍执行断网 fetch,不能降低为无锁重试或省略离线闭合验证。
|
||||
- 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像在 `--network none` 下能按当前 npm / Cargo lock 完成依赖准备,四个 job 的环境校验、干净 `npm ci` 和原有测试门禁仍全部执行。
|
||||
- 验证:workflow 不再出现 GitHub checkout action、apt、setup-node 或 rustup 安装 step;镜像能按五份当前 lock 完成缓存闭合,四个 job 的环境校验、经 3 次整命令级有界重试保护的干净 `npm ci` 和原有测试门禁仍全部执行。
|
||||
|
||||
## Gitea Actions HTTPS CONNECT 隧道必须双向收束 socket(2026-08-07)
|
||||
|
||||
- 现象:CI 的 `npm ci` 高频出现 `ECONNRESET / network aborted`,Cargo 则出现 crates.io TLS EOF、连接超时或下载失败;同一出口 gateway 容器看似健康,却累计自动重启数百次,日志反复出现 `Socket.ondata -> Writable.write -> write EPIPE -> Unhandled 'error' event`。
|
||||
- 原因:HTTPS CONNECT 建立后使用 `upstreamSocket.pipe(clientSocket)` 与反向 pipe,但只监听 upstream `error`;客户端在 DNS 等待、下载或 job 清理期间关闭连接时,pipe 继续向已断开的 client socket 写入,未处理的 EPIPE 会让 Node 进程退出。`unless-stopped` 自动拉起和浅层 healthcheck 会掩盖崩溃,所有并发 npm / Cargo 隧道同时被 reset。
|
||||
- 处理:CONNECT 一开始就为 client socket 注册 `error / close`,解析完成后为 upstream socket注册同样的双向销毁处理;DNS 返回、写 200 和开始 pipe 前都检查 client 是否已销毁。任一端 error、close 或 timeout 都幂等 destroy 两端,不把普通客户端 reset 写成错误日志。不要用进程级 `uncaughtException` 吞掉问题,也不要只增加 npm/Cargo 重试掩盖 gateway 崩溃。
|
||||
- 验证:在独立 canary 和正式 gateway 上分别并发制造至少 500 次“CONNECT 后立即断开”,随后确认容器仍运行、restart count 不增加、日志无 EPIPE;再通过同一 proxy 对 npm registry 与 crates index 建立完整 TLS 隧道。切换前仍须确认 Gitea 无活跃 run 且 Runner 内层无 job 容器。
|
||||
|
||||
## Gitea CI 预构建镜像不能只靠 tag 判断内容
|
||||
|
||||
@@ -4030,6 +4087,34 @@
|
||||
- 处理:灰度页只能以 `/admin/api/feature-gates` 为数据源,固定目标列表只登记现役功能;新增或退役业务 target 只修改固定目标注册,不得让通用页面依赖业务列表接口。旧 `creation-entry:*` 目标、接口和页面保持退役。
|
||||
- 验证:`adminRoutes` 必须包含 `gray-release`,admin-web TypeScript/ESLint/Vitest 不得排除灰度页;页面测试必须断言只请求 feature-gates,并继续覆盖现役固定 target、直接 Gate Key 保存与新 target 状态重置。
|
||||
- 关联:`apps/admin-web/src/pages/AdminGrayReleaseConfigPage.tsx`、`apps/admin-web/src/app/adminRoutes.ts`、`server-rs/crates/api-server/src/modules/admin.rs`、`docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md`。
|
||||
|
||||
## 角色动作不能靠素材主图或通用生成输入恢复
|
||||
|
||||
- 现象:角色动作在整画布导出时正常,但从素材库单项下载只得到第一帧 PNG,拖回画布也成为普通静态图片。
|
||||
- 原因:`editor_asset.image_src` 只指向首帧;若 worker 把完整帧集塞进 `generation_inputs_json`,素材 DTO、用户输入清洗或画布布局任一层丢字段,就会退化成 PNG。再增加一个 `mediaType` 只能掩盖结果字段没有落到正式资源的问题。
|
||||
- 处理:worker 只把完整帧集与图片序列毫秒时长写入 `editor_project_resource` / `editor_asset` 的 `image_sequence_frames_json`、`image_sequence_duration_ms`;数组位置是唯一帧序,不保存 `frameIndex`,帧数和 FPS 均按需派生。`assetKind=character-animation` 决定序列渲染。素材映射、单项下载和拖回画布只读取这两个正式字段,项目 resource 在保存 / 刷新后继续作为主真相;动作 layout 只保留资源引用和 placement,不再复制正式媒体结果。账号素材提交精选审核时,`editor_showcase_asset` 必须冻结复制相同字段,公开 read model 只返回正式字段。外部 helper 只调用一次动作生成接口并直接使用响应 `resource` / `asset`。
|
||||
- 画布回填:角色动作会形成“原角色资源 → 预览视频资源 → 最终序列资源”的血缘链。生成响应必须返回已经持久化的最终 resource,前端图层直接使用其 `resourceId`;不能继续构造 `local-resource-character-animation-*`,否则 `appendCanvasLayersWithResources` 会再次创建重复资源。新图层的 `sourceResourceId` 同时使用最终 resource 的直接来源(预览视频 resource),不能继续沿用请求中的原角色 resource;否则结构化保存会在已生成并计费后因血缘不一致而拒绝。修复时只替换资源关联与血缘字段,不要顺带把动作图层显示尺寸从生成占位尺寸改成原始帧分辨率。
|
||||
- 历史处理:不要再在 read mapper 增加 `generationInputs` / layout fallback。使用 migration operator procedure 按 `asset → project-resource → showcase → canvas` 迁移;只有动作身份已由 `assetKind`、正式字段、嵌套 `characterAnimation` 或权威对象证明后,才解释顶层 `frames/durationSeconds`,否则会把无关任意 JSON 误分类。同一 task 可能同时存在误标为动作的预览 MP4 和最终首帧 PNG,候选查找必须先按权威对象类型做计划态分类,排除视频并要求唯一正式图片序列,不能按原始 `assetKind` 计数。账号素材仍有旧帧、但后来拖入画布的 project-resource 只剩清洗后 `fields/references` 时,project-resource dry-run 必须按同 owner / task / 首帧对象精确消费 asset 计划态结果;apply 仍要求前置 asset scope 已物理完成。canvas 判断已有 resource 是否为动作时也必须消费 project-resource 的计划态类型:旧库误标为动作、但权威对象证明为 preview MP4 且 layout 本身是 video 的图层直接跳过动作清理;layout 明确为 `image-sequence` 却指向该视频时继续形成 blocker,资源规划本身有 blocker 时也不得静默跳过。正式序列还要逐帧用稳定对象路径匹配同 owner / task 的已登记图片对象并补齐 `objectKey/assetObjectId`。迁移不得验证 layout 复制的 `sourceResourceId`:历史 layer 可能仍指向原角色,而最终素材已指向预览资源;清理副本后采用最终素材的 DB 血缘即可,新生成链路仍保持严格校验。正式与旧版结果冲突、候选为零或多个均形成 blocker;脚本诊断应直接打印 scope、ID、原因、owner/project/task、对象身份和来源资源,不能只报 blocker ID。普通 layer 顶层 `mediaType` 在迁移和响应清洗时删除,但嵌套生成参考的 `mediaType` 保留。
|
||||
- 新写入与验证:动作 `generationInputs` 出现 `characterAnimation/frames/previewVideoPath/frameCount/fps/durationSeconds/screenColorHex`,或正式帧出现 `frameIndex`,HTTP 与 storage 双层拒绝;其它 asset kind 的任意 JSON 不受该动作门禁影响。测试覆盖两种历史 JSON、无关顶层同名字段、正式/旧版相等与冲突、可选帧引用合并、screen color 和 frameIndex 清理、预览 MP4 重分类、幂等、blocker/hash apply、画布 placement 清理/资源补建、正式字段缺失失败关闭和 helper 单请求。
|
||||
|
||||
## 图片序列时长不要复用通用媒体秒数
|
||||
|
||||
- 现象:把角色动作、视频、音频和上传媒体都写进通用 `duration_seconds`,随后又尝试用持久化 `frame_count/fps/duration_seconds` 互相校验,造成取整口径、生成参数和实际播放时长彼此污染。
|
||||
- 原因:角色动作需要的是一组图片完整播放一次的精确时长;视频 / 音频的 `durationSeconds` 是生成请求或临时运行态参数。帧数已经由数组长度唯一确定,FPS 也可按需要推导,无需维护三份可冲突真相。
|
||||
- 处理:资源 / 素材只保存 `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`。
|
||||
|
||||
## 精选角色动作显示首帧还要检查前端 renderer 与逐帧授权
|
||||
|
||||
- 现象:精选接口已经返回 `imageSequenceFrames` 和正确的 5 / 6 秒成本,但创作主页或后台审核仍只显示首帧;接入播放器后又可能只有第一帧成功、后续帧换签返回 404。
|
||||
- 原因:快照字段、展示 renderer 和私有对象授权是三道独立边界。公开 `imageSrc/objectKey` 只代表首帧,不能让前端自动获得完整帧集;顶层精选 exact grant 也不会自动覆盖其它帧对象。
|
||||
- 处理:公开精选模型必须把 `assetKind=character-animation` 映射到序列 renderer,并携带完整帧与毫秒时长;后台素材查询和精选审核共同透传同一字段并复用 `AdminEditorAssetMedia`。生成端不能在 `ProcessedEditorCharacterAnimationFrame → EditorCharacterAnimationFramePayload` 收口时丢弃逐帧 `assetObjectId/objectKey`,正式序列 JSON 必须保留已确认对象的稳定引用。公开授权在 SpacetimeDB 同一事务快照中只按有效精选动作的同 owner 逐帧 `assetObjectId/objectKey` 匹配,不能放宽 generated 前缀。列表未交互时只读首帧,打开或激活动作预览后也只挂载当前帧和有界预读窗口,避免再次制造换签突发;单帧换签或解码失败时跳过该帧、暂停全帧失败的序列并提供显式重试,不能长期显示空白或旧帧。卡片 hover 与 focus 分别跟踪,只要任一状态仍成立就继续播放,系统请求 `prefers-reduced-motion` 时卡片和弹窗默认暂停,用户仍可在弹窗中手动播放。
|
||||
- 验证:模型 / 组件测试覆盖 4 / 5 / 6 秒动作、损坏序列不回退 PNG、后台两页共用播放器和未激活列表不逐帧请求;SpacetimeDB 测试覆盖主对象、每帧对象、无关对象、跨 owner 与取消展示后的授权撤销。真实浏览器和端到端验收由人工单独执行,不把 unit / component 结果写成 E2E PASS。
|
||||
|
||||
## 可复用资源回填必须保持时间戳单调
|
||||
|
||||
- 现象:延迟重试携带比既有行更旧的调用方时间,回填图片序列字段时若无条件写入,会使 `updated_at` 倒退,导致基于时间戳的同步看不到更新或排序错误。
|
||||
- 处理:同源图片序列字段只允许 `None → Some`,非空冲突失败关闭;发生回填时 `updated_at = max(existing.updated_at, request_timestamp)`。legacy 音频 repair 不派生资源级图片序列或通用时长,重放继续精确匹配。
|
||||
## 历史钱包消费不能从最近流水或通用订单快照推算
|
||||
|
||||
- 现象:后台用户详情要展示累计花费时,直接复用只返回最近 50 条的 `list_profile_wallet_ledger`,或在充值订单每行使用的通用钱包快照里扫描该用户全部流水。
|
||||
@@ -4136,6 +4221,13 @@
|
||||
- 验证:覆盖失败根任务“水晶俄罗斯方块”后输入“继续”、连续 successor、跨 Session、跨 source、正常完成后新输入、带具体新需求、既有非占位入口先 patch 后 smoke、初始化占位的俄罗斯方块真实语义、未知玩法失败关闭、纯继续目标缺失、规范图不在运行 DOM/Canvas、真实动作前后 `sequence` 与 RAF 空转。浏览器验收必须同时比较 baseline 玩法关键文本/控件/状态和当前 revision,不能只看 Canvas 非空与三个固定按钮。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs`。
|
||||
|
||||
## game-chat 的固定图不能替代主 Agent 对已有美术的理解(2026-08-07)
|
||||
|
||||
- 现象:用户要求把已有美术资源接入游戏时,固定 `code-director -> art-director / art-asset-plan -> code-prototype` 图会在缺少主 Agent 审计的情况下启动美术生成,或把“整体重做”错误实现为无条件生图;美术完成后又换了 Run,代码接入、静态检查和试玩无法形成连续责任链。
|
||||
- 原因:固定节点把“是否需要美术”的语义判断编码为 Runtime 前置流程,`code-director` 成为另一个主控,而不是让真正接入游戏的 `code-prototype` 基于权威资产事实决策;如果再把固定审计策略塞进用户意图字段,Supervisor 的理解也会被 Runtime 规则覆盖。两个素材槽都缺失时若先消耗不可重试的 `art-asset-plan` 委派,其 child 又必然因缺规范图失败,整个 Run 会进入无法补救的死路。
|
||||
- 处理:Supervisor 用 `intentSummary` 持久化自己对用户意图的理解,固定 `audit-existing-first` 只作安全执行策略,随后只启动 `code-prototype`。主 Agent 先 `asset.list`,完整覆盖就直接使用;仅在事实证明缺少规范图或核心图集时,才建立一个写入范围受限为 `assets/**` 的美术 durable delivery。两槽都缺失时必须先完成并认领 `art-director` 的 `EvidenceReady` delivery,再委派 `art-asset-plan`;回执返回同一主 Run 后再接入素材、原玩法语义检查、静态检查和双视口试玩。读取旧 v1 决策时必须复核其旧 fingerprint,并从完成合同绑定的有效任务迁移 intent;旧 `code-director` coverage/route 只能触发当前主 Agent 重新审计和原位替换,不能直接成为新完成证据。整体视觉重做意图同样必须经过这次审计,不能成为绕过资产复用或强制重生成的固定规则。完整 GUI / CLI DAG 不使用该例外。
|
||||
- 验证:正反向测试同时证明 `intentSummary` 非空且不被固定策略代替、v1 决策与旧 route 同根恢复、完整资产零委派、真实缺口精确委派、两槽缺失时规范图优先、单个活跃 child、`game/**` 写拒绝、`assets/**` 写允许、回执恢复同一主 Run 和最终主 Agent 自验收;不能只凭 Prompt 出现关键词或 manifest 状态投影判通过。
|
||||
|
||||
## Tauri beforeDevCommand 失败不等于已启动客户端会自动退出(2026-08-03)
|
||||
|
||||
- 现象:旧 worktree 的 AGC Vite 长期占用 `127.0.0.1:3080`,marker 仍指向旧 API;新 worktree 启动 game-chat 后,配套后端在新端口 ready,随后 `beforeDevCommand` 因代理 target 不匹配返回非零,终端已经回到提示符,但原生客户端和它启动的 Runner 仍存活。客户端 WebView 实际加载旧 Vite,因此当前 master 的界面优化看起来全部缺失。
|
||||
@@ -4152,6 +4244,7 @@
|
||||
- 处理:从当前 root source 的 seed lane 动态解析全部零依赖首波任务,只对这些 child 容忍 hydration `Pending`,后续 code prototype / preview 仍严格要求 Running/Completed。`streaming / ready` 仍要求当前 revision,`committed` 回复改为依据 finalization 的稳定身份查询,不随后续项目 revision 失效。
|
||||
- 验证:覆盖 `design-director / art-director / code-director` 三个 Pending 首波 child 均可投影 Completed、`code-prototype` Pending 仍被拒绝;非流式专业 Agent 在 finalization 前无 stream,提交后形成 committed stream,再推进项目 revision 后仍可查询且正文不变。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/response_stream.rs`。
|
||||
|
||||
## 异步生成结果未知时不能换幂等键重提(2026-07-31)
|
||||
|
||||
- 现象:生成提交发生客户端超时、连接中断或响应丢失后,调用方创建新的 `Idempotency-Key` 再提交一次;原任务其实已经入队,最终造成重复生成、重复扣费和重复画布 / 素材库写入。
|
||||
@@ -4163,6 +4256,13 @@
|
||||
- 验证:覆盖“服务端已入队但提交响应丢失”后两次 POST 的 endpoint、正文 bytes 与 `Idempotency-Key` 完全相同,原键重试仍返回同一 operation,最终只出现一份 completed result 和一次计费 / 写回;恢复再次 transport 失败或临时鉴权失败仍保留同一账本;换 owner 不可见;MCP 与 REST 对同一 owner、同一请求和同一键必须命中同一 operation。
|
||||
- 关联:`server-rs/crates/api-server/src/external_generation.rs`、`server-rs/crates/api-server/src/external_mcp.rs`、`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`。
|
||||
|
||||
## MCP 列表不能透传完整项目快照(2026-08-07)
|
||||
|
||||
- 现象:账号项目数量增长后,`list_editor_projects` 把每个项目的 `canvas / layers / resources` 全量透传,REST 响应超过 MCP 4 MiB 上限,Agent 因整批失败而无法展示、查重或安全选择项目;缺少必填请求体时,内部 Axum JSON extractor 的文本 `415` 又会被泛化成“非 JSON 响应”。
|
||||
- 处理:项目列表 REST 保持默认 `view=full` 兼容,并提供 `view=summary`;MCP 固定使用 summary 且不向 Agent 暴露或接受 `view=full`。摘要只返回 `projectId / title / updatedAt / cover`,封面取最新且存在稳定 `objectKey` 的 `project-cover-snapshot`,展示时再调用 `/assets/read-url`,不在列表内嵌图片或签名 URL。MCP 在构造内部 REST 请求前按 OpenAPI schema 校验 required body;缺正文和缺字段分别返回结构化错误,不进入写入、上传票据或计费路径。
|
||||
- 验证:用 19 个完整序列化后超过 4 MiB 的项目 fixture 证明摘要仍低于上限且不含大型布局;覆盖四个历史 `415` 工具的缺正文、空对象和非对象输入,并断言项目列表工具固定 summary、调用方不能通过 query 覆盖。
|
||||
- 关联:`server-rs/crates/api-server/src/external_mcp.rs`、`server-rs/crates/api-server/src/external_editor_api.rs`、`docs/openapi/genarrative-external-v1.openapi.json`、`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`。
|
||||
|
||||
## api-server 嵌入仓库外资源时必须同步容器构建上下文(2026-07-31)
|
||||
|
||||
- 现象:本地 `cargo test` 可以编译 MCP 与 Skill 下载模块,但 api-server 镜像在 Rust 编译阶段报 `include_str!` 找不到 OpenAPI 或 Skill 文件。
|
||||
@@ -4196,12 +4296,26 @@
|
||||
- 处理:调用方在未认证时不得启动受保护的钱包刷新;可取消的读取要为每轮分配 `AbortController`,新读取先失效并中止旧读取,组件卸载时同时推进 revision、abort 当前请求并清空句柄。所有 `then / catch / finally` 在更新状态前都要检查 signal 与 revision。
|
||||
- 验证:定向测试覆盖卸载后请求 signal 已中止;同时复跑触发钱包刷新回调的画布生成集成测试和完整前端测试,不能以单文件偶然快速收束代替全量验证。
|
||||
|
||||
## 账号级轮询和并发 bootstrap 必须中止整条旧生命周期(2026-08-06)
|
||||
|
||||
- 现象:任务列表 `Promise.all` 一侧失败后,另一侧请求可能跨过重试和卸载继续悬挂;微信充值第一次确认返回 pending 后切换账号,旧订单的延迟重试可能使用新账号 Token 再次请求,401 路径还会影响新账号登录态。
|
||||
- 原因:只用 React state 或最终回调里的 owner 判断,无法阻止已安排的 timer、下一次 HTTP 请求和同轮未完成分支继续执行;每轮重试覆盖单个 controller ref,也会遗失更早的悬挂请求。
|
||||
- 处理:并发 bootstrap 每次 attempt 使用独立 `AbortController`,任一分支失败时先中止同轮 controller 再安排有界重试,卸载时中止当前 attempt。充值订单从创建成功起持有同一个 owner、账号 revision 和 `AbortController`;每次 delay、confirm 和 SSE watch 前后都校验生命周期,并把同一 signal 传到请求层;账号切换和卸载先 abort,再清理 ref、state 与旧支付回调 hash。
|
||||
- 验证:bootstrap 用例覆盖“一侧 reject、另一侧 pending、重试后卸载”,并断言每轮 signal 都已中止;充值用 fake timer 证明首次确认 pending 后切换账号会中止 signal,推进全部退避时间也不会产生第二个确认请求或清理新账号 Token。
|
||||
|
||||
## 中止 refresh 等待不等于隔离 token 发布(2026-08-07)
|
||||
|
||||
- 现象:A 账号的写请求 401 后开始共享 refresh,随后切换到 B。A 的 `AbortSignal` 虽然让业务请求立即结束且不再重放 POST,但底层 refresh 为了其它共享等待者不会被中止;A 的成功回包晚到时仍可能覆盖 B 的 token。
|
||||
- 原因:只对 `await` 叠加 abort 保护了调用链,没有给共享 Promise 的归属和最终 token 写入加账号栅栏;单一全局 Promise 还会让 B 加入 A 已在途的 refresh。
|
||||
- 处理:公开 token setter / clearer 每次都推进 auth generation;refresh 按 `generation + 发起时 access token` 共享、并以该快照 CAS 发布成功 token。快照已过期时成功回包转为失效结果,401/403 也不得清理新代际 token;新代际建立自己的 refresh Promise,旧 Promise 收尾时不得清掉新尝试。
|
||||
- 验证:`src/services/apiClient.test.ts` 要等旧 refresh 完整收束后断言 B token 不变,并用两个独立 deferred response 证明 B 会发起第二个 `/api/auth/refresh`;另覆盖旧 refresh 401 晚到不清 B token。
|
||||
|
||||
## 下游 manifest 回调测试不能冒充实时数据源(2026-08-05)
|
||||
|
||||
- 现象:工作台的资源、任务与版本重投影单测保持绿色,但后台 Agent 已更新 `.agent/manifest.json` 后,打开中的工作台仍长期显示旧快照,只有重开项目才更新。
|
||||
- 原因:测试 Supervisor 直接调用 `onManifestChange`,只证明 `App manifest -> WorkspaceLauncher -> ProjectDevelopmentView` 的下游桥接;真实 Runtime event 没有失效字段,监听器也没有重读 manifest。External Runner 又与 GUI 分属不同进程,Runner 内无法使用 GUI `AppHandle`,只补普通 Tauri event 仍不能形成生产链路。
|
||||
- 处理:后台 manifest mutation 收敛到共用 Runtime emitter;GUI 内进程用带 `manifestInvalidated` 的 Runtime update,External Runner 通过 GUI owner attach 登记的受令牌保护 loopback sink 转发专用失效事件。App 对当前项目做 single-flight manifest 重读,并以 mounted、项目路径和 scope version 丢弃迟到结果;WorkspaceLauncher 继续只消费完整 manifest 快照,不新增平行状态或轮询。
|
||||
- 验证:集成测试必须渲染真实 `App + WorkspaceLauncher`、捕获真实 Tauri listener,让 `get_local_game_manifest` 从旧快照切换到新快照,并由非 Supervisor Agent 事件驱动资产、completed 任务、运行入口和版本卡出现;另测项目切换时旧请求迟到。旧的直接 `onManifestChange` 测试只能标记为下游桥接证据。
|
||||
- 验证:集成测试必须渲染真实 `App + WorkspaceLauncher`、捕获真实 Tauri listener,让 `get_local_game_manifest` 从旧快照切换到新快照,并由非 Supervisor Agent 事件驱动资产、completed 任务、运行入口和版本卡出现;另测项目切换时旧请求迟到。测试夹具必须先等待目标 Tauri listener 注册完成再发失效事件,并对“事件 -> manifest 重读 -> 工作台重投影”使用局部、有界的 `5_000ms` 等待,避免并行全量回归把监听注册或异步投影调度误判为功能失败。旧的直接 `onManifestChange` 测试只能标记为下游桥接证据。
|
||||
|
||||
## React 资源详情焦点不能依赖重建对象身份(2026-08-05)
|
||||
|
||||
@@ -4256,6 +4370,7 @@
|
||||
- 现象:parent wake 的 200 次瞬态预算耗尽后 Runtime 仍长期显示 running,或 lane 忙、取消、child 前进、manifest 损坏时 reconciliation 被静默丢弃或覆盖新状态。
|
||||
- 处理:预算耗尽错误必须向上传递;lane 忙先持久化 deferred signal,再在 lane + 项目锁内重检最新事实。结构损坏路径使用不依赖 manifest hydration 的专用 journal/state 写入,CAS 失败转为继续对账,绝不覆写并发取消或 DAG 进展。可解析的空对象/空 runId 仍是损坏身份,只有完整有效的新 Run 才能阻止旧 marker;event/audit 的同键记录必须完整比对并拒绝冲突或重复。旧 task 已终态、Runtime 非 waiting 或新 Run 接管时,deferred signal 必须写 resolved/superseded,不能留给后续 wake 永久重复 settle。
|
||||
- 测试注意:autonomous child fixture 先 linked Pending、后正式 Running;终态 runId 必须拒绝复用。判断 Completed-only 诊断时按每个 seed task 的实际状态分析,不能因为 `code-prototype` Pending 就忽略已经 Completed 的 `art-asset-plan` 深验。
|
||||
|
||||
## macOS 安全路径测试必须使用规范化临时目录(2026-08-05)
|
||||
|
||||
- 现象:调用仓库上下文、Runtime context bundle 或 pending recovery 的 Rust 测试在 macOS 报“Repository root and its ancestors must not be symbolic links”,Linux CI 却可能通过;本地 HTTP 恢复夹具在完整串行测试中还可能偶发 `WouldBlock`。
|
||||
@@ -4278,6 +4393,13 @@
|
||||
- 验证:自动测试使用真实公开 Host/Origin 执行 `initialize`;部署后再从公网域名完成带 Key 的 `initialize`、`tools/list`、`resources/list`、Skill resource 读取和至少一个只读业务 tool 调用。loopback 成功只能证明 MCP 实现和 Key 可用,不能替代公网 Host 验收。
|
||||
- 关联:`server-rs/crates/api-server/src/external_mcp.rs`、`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`。
|
||||
|
||||
## 异步任务接受后的刷新回调不能统一套用 dialog 所有权(2026-08-05)
|
||||
|
||||
- 现象:正式生成任务已被后端接受,用户随后删除 dialog 或切换项目,任务仍继续并可能扣费,但钱包和任务列表没有刷新;反向问题是账号切换时若 project ID 暂时相同,旧任务可能刷新新账号的任务列表。
|
||||
- 原因:把 dialog / canvas 的完整 UI 所有权同时用于账号级钱包和账号内项目级任务列表,或者任务列表只比较 project ID,没有校验账号。
|
||||
- 处理:按副作用分层校验。钱包只比较账号;任务列表比较账号加项目;dialog、canvas、asset 和 layer 写回继续比较账号、项目、scope version 与原 dialog。正式请求已接受后,删除 UI 状态不等于取消后端任务。
|
||||
- 验证:分别覆盖删除 dialog、同账号切项目、账号 A 切到账号 B 且 project ID 保持相同,以及原账号原项目原 dialog 仍有效的正常回写。
|
||||
|
||||
## GUI owner 锁不能替代逐 boot 的事件接收端登记(2026-08-05)
|
||||
|
||||
- 现象:GUI 首次启动后 manifest 事件转发正常,但 Runner 被替换为新 boot 后只剩 owner 锁和 endpoint 可用,后台更新不再到达 GUI;或者 attach 响应只确认 owner,客户端却误记当前 boot 已完整登记,后续 ensure 不再重试。
|
||||
@@ -4298,3 +4420,38 @@
|
||||
- 原因:客户端虽在重试中复用 `x-request-id`,队列入口却用随机 job id 生成 dedupe key;前端允许无限追加,api-server 和 provider 用 `.take(...)` 静默截断;`generationInputs.references` 被当成可信持久 provenance。
|
||||
- 处理:主站生成 POST 禁止自动重试,把显式复用的稳定 request id 接到队列唯一键并校验 replay payload;所有边界显式拒绝超限,前端还要预留主图槽位、统计在途上传,并在上传完成前拒绝模型切换、画布选图、提交生成、关联源图删除 / 剪切 / 素材删除和面板切换 / 关闭;reservation 必须绑定原面板上下文,批量部分失败时不能丢弃已经持久化的成功项。入队、完美像素及直接创建资源 / 素材时删除客户端 references,执行时按真实参考源和 owner 资源记录重建权威引用。历史任务比较必须兼容仅差已删除 references 的旧 payload,不能只保留旧 hash 却让 payload 比较误报冲突。
|
||||
- 验证:覆盖同键同 payload / 不同 payload、普通图片第 6 张、带主图的 GPT-image-2 第 5 张额外引用、provider 6 / 15 张边界、伪造引用删除和 owned 资源 / 素材重建。
|
||||
|
||||
## 共享音频 Composer 架构冲突不能按单行选边(2026-08-06)
|
||||
|
||||
- 现象:master 的音频 composer 同时承载 SFX 与 BGM,并在组件内定义 `isSoundEffect`;功能分支把 BGM 拆成独立组件后,原组件变成 SFX-only。合并时只把 master 的条件占位表达式带回 SFX-only 组件,没有带回变量定义,最终在测试渲染阶段报 `isSoundEffect is not defined`。
|
||||
- 原因:冲突两侧代表不同组件架构,逐行保留看似有用的 JSX 会把一个架构中的局部条件拼进另一个架构。import 排序、格式检查和只覆盖单一 mode 的测试都不能证明这种组合成立。
|
||||
- 处理:先确定权威组件边界,再按完整调用链解决冲突。图片画布音频入口当前决策是恢复一个共享 `ImageCanvasAudioGenerationComposerView`,由组件内 `isSoundEffect` 分流;BGM/SFX 的 validator、写回、锁和提交契约仍分别保持。不要只补一个常量后继续维持已经废弃的双 composer 边界。
|
||||
- 验证:同时渲染 `audio-sound-effect` 与 `audio-background-music`,覆盖两个 mode 的正向控件和互斥负向断言、dialog / mode 切换、BGM 稳定 ID 与 controller 缺失的失败关闭,并运行 `ImageCanvasGenerationComposerView.test.tsx` 与 typecheck。
|
||||
|
||||
## 生成结果的稳定 ID 和 job 终态都不能代替 durable receipt(2026-08-06)
|
||||
|
||||
- 现象:Provider / OSS 已成功,但项目资源、账号素材、binding、画布和 job 只完成一部分;不确定结果重放时,有时又复制一批素材或重复推进 canvas revision。inline 路径在进程重启后尤其无法判断前一次提交是否整笔完成。
|
||||
- 原因:把“请求已入队”、“某个稳定 ID 已存在”或“job 已 completed”误当成整批业务记录已原子提交的证据。request fingerprint 只证明用户请求,不绑定最终 slot、派生记录、画布候选和 compact result;仅比较资源 ID 也无法发现内容漂移。
|
||||
- 处理:用 `editor_generation_operation` 记录 durable receipt,分开 request fingerprint 与整笔 commit SHA-256。首次调用在同一 SpacetimeDB 事务中校验 lease 并写 object/resource/asset/binding/canvas/job/receipt;重放先查 receipt,再读回逐 slot 权威事实精确比较。receipt 缺失但 resource/asset/binding 已存在时失败关闭,不得补写 receipt;事务前已确认的 asset object 只能在 ID、bucket/key、owner、策略、媒体、来源和实体字段全部相等时复用。
|
||||
- 时间与并发:`completed_at_micros` 必须为正数,object/resource/asset/binding/canvas 候选原时间字段与它一起纳入 commit SHA-256,不能在每次重放时重新取时;job 终态和完成事件只用 SpacetimeDB `ctx.timestamp`。canvas CAS 冲突后只刷新 project 并重算布局,不重跑 Provider / OSS。OSS 尚未进入该事务,无引用 object 仍是需另行清理的边界,不要宣称跨 OSS exactly-once。
|
||||
- queue completion 不能把 inline 完整响应无条件同时复制到 `result` 和 `editor-agent-tool-call-result`。图集/UI 最多 64 个切片会重复携带 resource/asset/prompt/generationInputs,容易超过 job payload 512 KiB 上限并让整个原子提交回滚。必须先按普通 UI、Editor Agent、External API 的消费方契约裁剪,再把最终 JSON 交给统一 procedure。
|
||||
- 消费方身份不能在提交前重新读取 summary 兼容快照来判断:该快照按设计清空 dedupe key 并删除 generationInputs,Editor Agent / External API 会因此被误判成普通 UI。应在 worker 持有完整 claimed job 时把安全的 consumer kind 与 source identity 固化到调用上下文。
|
||||
- procedure future 超时或连接断开不能直接映射为业务失败,远端事务可能已经提交。必须有界重放同一 prepared commit;明确 CAS 后才刷新 layout,且刷新 layout 应使用新时间,不能把项目 `updated_at` 回拨。receipt 不复制 queue payload,只存摘要并从 job 权威行回读;跨记录 object/project 一致性必须在事务内验证,不能依赖当前 builder 通常会携带完整 candidate。
|
||||
- job 的 owner/kind/fingerprint/lease 都正确仍不够:`source_entity_id` 还必须绑定结果项目,来源资源必须另查存在性与 owner/project 归属;否则同 owner 的 job 可以误写别的项目,或伪造跨用户/跨项目血缘。
|
||||
- Provider 成功时计费 guard 已解除,后续原子持久化失败不会自动退款。但也不能在 api-server 先独立退款再尝试 fail job:过期 worker、fail 断线或原子提交已成功但回包丢失时,会变成「结果成功且已退款」。正确边界是在同一 SpacetimeDB 事务内先 fencing 当前 lease,再同步写退款账本和失败终态;不得期待 `max_attempts = 1` 的编辑器任务再走租约耗尽路径补退。
|
||||
- compact result 只能删除大 payload,不能删除消费方 DTO 必填字段或定位正式结果的稳定引用。角色动作/视频缺 `ok`、音效/BGM 缺 `prompt` 都会让 Editor Agent 把已完成 job 判成不可重试的回填失败;External 角色动作/视频如果创建了账号素材,completed 结果还必须保留 `assetId`。
|
||||
- `project_resource.source_resource_id` 校验不会自动覆盖 `editor_asset.source_resource_id`;asset-only 结果可以没有项目资源候选,必须另查来源是本事务候选或已登记资源且属于同 owner;若本次结果有 project,还必须同 project。
|
||||
- inline 模式不会走 queue `fail_job`,若计费 wrapper 在 Provider 成功时立即 disarm,后续的上传/原子持久化明确失败会扣费无结果。应在全部 inline owner handler 外统一延迟已成功 billing guard 到 durable commit;明确失败退款,但传输未知结果不退,否则远端已成功时又会变成「结果 + 退款」。
|
||||
- 消费契约不能只测上游 builder:External v1 在 durable job 入库前还有一层 allowlist compactor,必须对最终 JSON 断言 `ok / prompt / actualPrompt` 及稳定 resource/asset 引用。
|
||||
- 计费 guard 的取消补偿必须区分 procedure dispatch 边界:`Build / PoolAcquire / ConnectBuild / ConnectHandshake` 等未发出阶段可确定退款;dispatch 后回包前的 future 取消与断连必须视为结果未知并保留扣款,等 durable receipt 对账。只在 error 返回后再标记 unknown 会留下取消窗口;必须在真正调用 procedure 前同步设置 task-local 标记,并在 `Procedure` 结果或确定未发出的失败后清除。
|
||||
- compact DTO 的可选字段必须用最终 consumer payload 回归:Editor Agent 图片生成/修改的 `provider` 会被脱敏删除,必须是可选字段;图标/UI 正常与 source-only fallback 则必须保留 `ok / prompt / actualPrompt`。fallback 不得从可选 project resource 反推必填字段,否则无 `projectId` 任务会持久 `prompt/model=null`、尺寸为零且图标/UI 丢失 `priceMudPoints`。
|
||||
- receipt 存在不等于引用 object 仍然可信:省略 candidate 的已登记 object 在重放时也要回读 owner/key/task/kind/媒体身份。同时先查同 operation ID job,存在 job 却漏传 completion 必须整笔回滚,否则会得到 receipt 成功而 job 仍 running 的永久分裂。resource/asset/binding 也不得仅核对 object ID/key,必须按 operation 合法 tuple 交叉验证业务元数据。
|
||||
- 验证:故障注入覆盖 resource 后 asset/binding 失败、canvas CAS 冲突、过期 lease、同 operation 异 fingerprint / 异 commit、receipt 缺失的部分既有记录、精确既有 object 复用与 object 内容漂移;成功重放必须证明记录数、时间、binding/job 事件数和 canvas revision 全部不变。
|
||||
- 关联:`docs/technical/【后端架构】编辑器生成结果原子提交与幂等重放方案-2026-08-06.md`、Issue #134。
|
||||
|
||||
## 付费生成不能把素材目录归属校验留到 provider 之后(2026-08-07)
|
||||
|
||||
- 现象:登录用户给自己的合法 `projectId` 搭配不存在或属于其他账号的 `assetFolderId`,图片、改图、图集、UI 提取、视频、角色动作或音频生成会先扣泥点并调用付费 provider / OSS,直到创建 `editor_asset` 才拒绝目录;失败退款让用户成本归零,平台侧 provider 和存储成本不可逆。
|
||||
- 原因:`normalize_generated_asset_folder_id` 只处理 `project`、旧 `folder-*` 和默认目录 ID 的兼容映射,不读取 SpacetimeDB;真正的 `require_owned_asset_folder` 位于生成结果持久化末端。把“失败会退款”误当成副作用补偿,漏掉退款不能撤销 provider 请求与 OSS PUT。
|
||||
- 处理:所有付费编辑器生成在队列 enqueue 前和 worker / inline 执行前复用只读 `preflight_editor_generation_target_and_return`,按认证 owner 校验可选项目及归一化目录;读取失败和归属不匹配一律失败关闭。helper 返回 canonical 项目与目录并覆写后续入队 / worker / 原子准备使用的 payload,不能校验 trim 后的项目却持久化原始空白值。角色图片、角色动作、图标 spritesheet 与 UI 提取省略目录时按实际默认目录预检;默认目录允许尚未创建,自定义目录必须存在且 owned。预检不替代最终 procedure 复验,也不保证跨外部调用的目录锁定。
|
||||
- 验证:源码顺序回归必须覆盖图片生成、图片修改、图标 spritesheet、UI 设计图提取、视频、角色动作、SFX 与 BGM 的 enqueue / direct 两层,证明纯本地格式和 `data:` / `blob:` 稳定引用门禁先执行,canonical target 在预检后写回 payload,远端引用解析、generation input rebuild、扣费、入队、provider 与 OSS 均留在预检之后;模块侧扫描证明预检只调用 runtime identity、项目、目录只读校验且不含 insert / update / delete,并覆盖带空白项目、`project`、旧 `folder-*`、默认目录 ID、自定义目录与 `None` 归一化。
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
- 新增 Markdown 文档时,文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;只在任务需要时重命名历史文档,避免无关大 diff。
|
||||
- 涉及中文文本时注意 UTF-8 编码和乱码排查。
|
||||
- 涉及后端时遵循 DDD 分层,不把业务真相下沉到前端或临时兼容层。
|
||||
- 运行时日志禁止对完整配置、应用状态或 provider client 做递归 `Debug` 输出;当前 `AppConfig`、`AppState`、`AppStateInner`、`SpacetimeClientConfig` 与 `SpacetimeClient` 必须保持封闭的手写安全摘要,并用唯一哨兵测试锁定顶层与可独立格式化路径。其它仍使用派生 `Debug` 的历史 provider 类型不得新增整对象日志调用,后续按类型独立脱敏。新增字段默认不进入摘要,确需排障时只增加枚举、数值、布尔值或是否配置等非敏感字段。
|
||||
- `packages/shared` 用于前后端 DTO、公开契约及跨页面复用的无业务真相 UI 组件和纯工具;不得把领域规则、后端副作用或正式状态放入其中。
|
||||
- 修改 `/api/external/v1` 的路由、HTTP 方法、请求 / 响应 DTO、请求头、状态码、鉴权或异步语义时,必须同批更新 `docs/openapi/genarrative-external-v1.openapi.json` 和对应契约测试;Rust 实现与 OpenAPI 未对齐时不得完成、提交或发布。
|
||||
- `maincloud` / `Maincloud` / `MAINCLOUD` 相关代码、脚本、测试、环境变量、命令和文档要求均视为历史残留,禁止新增、运行或引用;API smoke 统一使用 `npm run dev:api-server` 与 `/healthz`。
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
- 锁定、分组、翻转等状态不影响图片文件导出,但写入元数据。
|
||||
- 空画布时按钮置灰,或点击后显示轻提示。
|
||||
|
||||
左侧素材库选择模式同时支持导出当前可见范围内的选中素材:只选中一个普通素材时直接下载原文件;选中多个素材时沿用画布素材 ZIP 的读取、去重、媒体分目录、序列帧、元数据、部分失败和浏览器下载能力,生成 `项目名-选中素材-YYYYMMDD-HHmmss.zip`,包内根目录为 `项目名-选中素材/`。单素材、单序列帧、选中素材 ZIP 和画布素材 ZIP 的下载文件名都必须包含到秒的本地时间戳,避免同一天重复导出时重名。序列帧层只要至少一帧具有可读 `imageSrc / objectKey` 即可进入导出计划,不要求层级 `src / objectKey`;单层、选中素材和画布集合导出必须共用同一互斥状态,避免并发下载覆盖进度与结果提示。该入口不改变中央画布和图层列表的选择 / 下载语义。
|
||||
|
||||
持久素材只要 `src` 或 `objectKey` 任一有效即可进入点击、Shift、框选和全选范围。`src` 为空但保留私有 `objectKey` 的素材,导出时复用统一资源读取链路换签;浏览器无法直读签名 URL 时继续回退同源 `/api/assets/read-bytes` 字节代理,不得因缺少临时展示地址而过滤。
|
||||
|
||||
暂不实现:
|
||||
|
||||
- 画布整体截图 PNG。
|
||||
@@ -40,7 +44,7 @@
|
||||
导出文件名:
|
||||
|
||||
```text
|
||||
项目名-画布素材-YYYYMMDD.zip
|
||||
项目名-画布素材-YYYYMMDD-HHmmss.zip
|
||||
```
|
||||
|
||||
包内结构:
|
||||
@@ -72,7 +76,7 @@
|
||||
普通序列帧 ZIP 结构:
|
||||
|
||||
```text
|
||||
角色动作-Sequence.zip
|
||||
角色动作-Sequence-YYYYMMDD-HHmmss.zip
|
||||
├─ frames/
|
||||
│ ├─ frame-01.png
|
||||
│ └─ frame-02.png
|
||||
@@ -84,7 +88,7 @@
|
||||
Spine JSON ZIP 结构:
|
||||
|
||||
```text
|
||||
角色动作-SpineJSON.zip
|
||||
角色动作-SpineJSON-YYYYMMDD-HHmmss.zip
|
||||
├─ frames/
|
||||
│ ├─ frame-01.png
|
||||
│ └─ frame-02.png
|
||||
@@ -164,6 +168,7 @@ assetObjectId > objectKey > sourceAssetId > src
|
||||
2. 过滤无效图层,保留隐藏图层。
|
||||
3. 按去重 key 合并图片源或序列帧源。
|
||||
4. 对每个素材源读取 Blob:
|
||||
- 集合导出先按图层顺序完成去重和文件名规划,再用同一个四路并发读取器读取 / 转换普通素材与序列帧,最后按规划和原帧顺序写入 ZIP;不得让响应完成顺序改变导出目录或帧顺序。
|
||||
- `data:image/...` 直接转换为 Blob。
|
||||
- 同源或可访问 URL 使用 `fetch` 拉取 Blob。
|
||||
- 私有 generated / OSS 素材先走 `/api/assets/read-url` 换签并由浏览器直接 `fetch` OSS 签名 URL;必须在同一保护边界内完整消费响应体,换签、请求、状态码或响应体读取任一阶段失败时,才 fallback 到同源 `/api/assets/read-bytes`,不得在只拿到 `2xx/206` 响应头后提前视为读取成功。
|
||||
@@ -198,6 +203,10 @@ assetObjectId > objectKey > sourceAssetId > src
|
||||
- 动作图层右键菜单把 `导出为` 作为一级入口,二级菜单提供 `序列帧导出(zip)` 和 `Spine 导出(zip)`。
|
||||
- 单图层普通序列帧 ZIP 包含 `frames/`、`preview.gif`、`metadata.json` 和 `manifest.txt`,且不包含 `skeleton.json`。
|
||||
- 序列帧导出的 `skeleton.json` 可被独立验证器解析并预览。
|
||||
- 左侧素材库只选一个素材时直接下载该素材,不创建集合 ZIP。
|
||||
- 左侧素材库选择多个素材时只把选中素材写入 `项目名-选中素材-YYYYMMDD-HHmmss.zip`,不混入未选素材,并继续覆盖部分失败和全部失败边界。
|
||||
- 同一事件循环内重复触发集合导出时只启动一次读取和下载;单个选中素材下载成功后显示 `选中素材已导出`。
|
||||
- 多素材与长序列帧集合导出同时读取不超过四项,外层素材无需等待前一个序列全部完成,异步读取逆序完成时 ZIP 文件与帧顺序仍保持稳定。
|
||||
|
||||
## Spine JSON 验证器
|
||||
|
||||
@@ -226,7 +235,6 @@ npm run spine-export-validator:build
|
||||
|
||||
## 后续扩展
|
||||
|
||||
- 导出当前选中素材。
|
||||
- 导出画布快照 PNG。
|
||||
- 导出可恢复工程包。
|
||||
- 导入工程包恢复画布。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -101,9 +101,12 @@
|
||||
## 第十阶段模块
|
||||
|
||||
- `useImageCanvasAssetLibrary.ts`
|
||||
- 承载账号级素材库状态模型:素材文件夹、素材列表、文件夹折叠 / 新建 / 重命名 / 删除、素材重命名 / 删除、素材选择模式、框选、多选删除、素材拖到文件夹和鉴权失败登录弹窗。
|
||||
- 承载账号级素材库状态模型:素材文件夹、素材列表、文件夹折叠 / 新建 / 重命名 / 删除、素材重命名 / 删除、批量删除资源副作用、素材拖到文件夹和鉴权失败登录弹窗;不再持有选择集合、范围锚点或框选交互状态。
|
||||
- 主视图继续保留上传文件读取、上传占位卡片进度、拖到画布坐标、创建画布图层、工程资源持久化和画布图层清理;素材删除通过 `onDeleteAssets` 回调通知主视图清理关联图层。
|
||||
- 该 hook 有独立单测覆盖素材库加载归一化、401 登录、新建文件夹临时 id 替换、素材移动、删除回调和多选删除,避免后续整理侧栏 JSX 时丢失素材库能力。
|
||||
- `useImageCanvasAssetSelection.ts`
|
||||
- 作为素材选择的唯一状态边界,统一持有选择模式、完整选中集合、范围锚点、可选素材有效性 reconcile、单项 / Shift / 当前可见全选增量、框选几何与框选生命周期;框选除 `pointerup / pointercancel` 外必须在匹配的 `lostpointercapture` 到达时只清理框选状态,不得再次释放已经丢失的 capture。该 hook 向批量下载 / 删除只暴露已经按素材顺序解析的 `selectedAssets`;删除入口根据当前 `visibleAssetIds` 识别未显示选择并在执行完整集合删除前弹出危险确认。
|
||||
- 搜索和文件夹折叠只在侧栏产生 `visibleAssetIds` 并传入增量动作,不得直接修改或 reconcile 选择集合;选择行为测试集中在该 hook,素材库 model 不再维护第二套选择状态机。
|
||||
|
||||
## 第十一阶段模块
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# React 组件测试准则
|
||||
|
||||
更新时间:`2026-06-26`
|
||||
更新时间:`2026-08-07`
|
||||
|
||||
## 背景
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
- `data-testid` 名称必须描述用户或稳定渲染边界,例如 `image-canvas-editor-snap-guide-vertical`;不要描述 React 私有 state 名称。
|
||||
- 测试用 fixture 只包含本行为需要的字段。演化中的 payload 使用 `expect.objectContaining(...)` 或 helper 生成默认对象,避免一处契约加字段导致大量无关用例碎裂。
|
||||
- 当测试是为防止历史回归,应在测试名或邻近注释中说明防的是什么行为,而不是记录实现步骤。
|
||||
- 同一用例既要验证定时器调度参数,又要断言确定的中间帧或中间状态时,必须 mock 定时器回调或使用可控假时钟;不得让真实墙上时间在异步交互期间推进被断言的状态,否则本地通过的用例会在较慢 CI 中偶发失败。
|
||||
- 测试 React effect 中注册的事件监听时,触发事件前先用可观测的 listener 调用确认注册已完成;异步请求已开始应用有界 `waitFor` 断言确认,不要用无界手工 Promise 等待一次性信号。解除挂起请求时将 Promise 收尾纳入异步 `act`,确保后续 React 更新在断言前已冲刷。
|
||||
- 公共 `AutoGrowTextArea` 使用 Lexical `contenteditable`,业务测试通过 `setPlainTextEditorValue` 写入文本、通过 `getPlainTextEditorHost` 断言外层样式;占位文案是独立渲染节点,不读取原生 `placeholder` 属性,也不对编辑根触发 textarea 专属的 `change` 事件。
|
||||
- 异步请求失败时,错误状态与调用方的草稿 / 附件恢复可能分属连续两次 React 更新;用例必须对最终恢复结果使用有界 `waitFor`,不能把错误文案刚出现的中间帧当成恢复已经完成。
|
||||
|
||||
## 试点调整
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ VectorEngine `gpt-image-2`、音频、LLM 等外部生成不能由面向外部
|
||||
- `claim_external_generation_jobs_and_return`:worker 按 `worker_id`、`limit` 和 lease 时长抢占 `pending` 或 lease 过期的 `running` 任务,返回本次 claim 的 `lease_token`。
|
||||
- `renew_external_generation_job_lease_and_return`:worker 长任务执行期间按 `worker_id + lease_token` 续租,防止外部生成超过单次 lease 后被重复领取。
|
||||
- `update_external_generation_job_phase_and_return`:worker 按 `job_id + worker_id + lease_token` 把当前执行阶段更新为 `generating` 或 `processing`,并同步现有摘要投影;不新增阶段任务或阶段表。procedure 用结构化结果区分 `LeaseFencingRejected` 与 `OtherRejected`,调用方不解析错误文案;`LeaseFencingRejected` 立即终止,`OtherRejected` 以及 SDK 的 `Procedure` / `Runtime` 错误不重试,只有 `Build` / `ConnectDropped` / `Timeout` 在同一个 job attempt 内重试 `1` 次。该重试只重新上报 phase,不把任务写回 `pending`,也不重新调用 provider;编辑器 job 入队固定 `max_attempts=1`,第二次传输失败后任务进入 `failed`,不会回到 `pending` 或从 provider 生成起点重跑。
|
||||
- `complete_external_generation_job_and_return`:worker 成功后按 `worker_id + lease_token` 写入 `result_payload_json`,任务进入 `completed`。
|
||||
- `complete_external_generation_job_and_return`:只保留给不携带编辑器正式 object/resource/asset/canvas 业务写回的兼容路径。现役编辑器生成成功时不得单独调用它。
|
||||
- `persist_editor_generation_result_and_return`:编辑器 queue / inline 共用的结果提交口。单一事务写入可选 asset object、全部 project resource / account asset / asset binding、可选 canvas V2 CAS、queue job 完成与 durable receipt;返回 `Applied / AlreadyApplied` 和权威快照。
|
||||
- `fail_external_generation_job_and_return`:worker 失败后按 `worker_id + lease_token` 回写错误,并按 `max_attempts` 决定回到 `pending` 重试或进入 `failed`。
|
||||
- `list_external_generation_job_summaries_and_return`:按当前账号从轻量摘要投影读取正式生成任务列表,返回 pending / running / 未确认终态数量、任务价格、执行阶段和完成提示确认状态。
|
||||
- `acknowledge_external_generation_job_summaries_and_return`:按当前账号确认已终态任务的完成 / 失败提示,写入摘要投影的 `notification_acknowledged_at` 并追加审计事件。
|
||||
@@ -87,6 +88,8 @@ BFF 只做鉴权、授权裁剪、字段脱敏和契约映射;worker 调度、
|
||||
|
||||
新增私有审计表 `external_generation_job_event`,记录 `enqueued/claimed/lease_renewed/completed/failed/acknowledged` 等事件。事件表只追加状态转换事实,不作为当前状态源;排障时先看 `external_generation_job` 当前状态,再按 `job_id` 追 `external_generation_job_event` 时间线。
|
||||
|
||||
另新增私有 `editor_generation_operation` durable commit receipt。它不与 `external_generation_job` 争抢任务状态:job 仍负责队列、lease、计费和通知,receipt 只固化某个 owner/kind/operation 的 request fingerprint、整笔 commit SHA-256、可选 project 以及 queue 的 job/worker/lease/result 绑定。inline 虽没有 job,也必须写 receipt;否则 API 进程重启后无法安全区分“完整提交”与“稳定 ID 巧合/历史部分记录”。
|
||||
|
||||
索引:
|
||||
|
||||
- `by_external_generation_job_status_available(status, available_at)`
|
||||
@@ -205,9 +208,11 @@ controller 配置:
|
||||
- `editor_video_generation`:画布视频生成和视频素材快速编辑。
|
||||
- `editor_sound_effect_generation` / `editor_background_music_generation`:画布音效与背景音乐。
|
||||
|
||||
画板结果的业务真相仍是 `editor_project_resource`、账号级 `editor_asset` 和 `editor_canvas.layers_json`。请求携带 `projectId + canvasCompletion` 时,worker 成功后读取当前项目 layout,用最新 generation dialog placeholder 或无 dialog 完成占位写入结果图层,并保存项目快照;前端轮询单 job 到 completed 后重新读取项目快照,不从队列 payload 或本地临时响应重建正式图层。生成器已被删除时,worker 只保留生成出的资源 / 素材记录,不把结果重新塞回画布。
|
||||
画板结果的业务真相仍是 `asset_object`、`editor_project_resource`、账号级 `editor_asset`、可选 `asset_entity_binding` 和对应的 legacy / structured canvas 表;`editor_generation_operation` 只是提交回执,不替代这些 read model。worker 在 Provider 与 OSS 完成后只做 prepare:使用 owner + operation kind + job ID + stable slot 派生 resource/asset ID,构造可选 object/binding、候选 layout 和 compact job result,然后一次调用 `persist_editor_generation_result_and_return`。该 procedure 必须在当前事务快照校验 owner、job kind、由 `request_payload_json` 重算的 fingerprint 与未过期 lease,最后与业务记录一起完成 job 和 receipt。任一验证、binding 或 canvas CAS 失败都回滚全部数据库事实;worker 不得随后再调用 `complete_external_generation_job_and_return`。前端轮询单 job 到 completed 后重新读取项目快照,不从队列 payload、receipt 或本地临时响应重建正式图层。
|
||||
|
||||
角色形象、图标 spritesheet 和 UI 素材提取在 provider 原图已经持久化后,如果透明背景处理最终失败,只用原图完成 `canvasCompletion`,不创建或回填透明处理图,图标和 UI 也不继续拆分,任务保持 `completed`。这个 source-only 降级只包住透明背景处理的最终失败;phase 上报、provider 原图持久化、透明处理图持久化或画布写回失败仍按任务错误传播。
|
||||
结果重放必须保持同一 operation fingerprint 和同一 prepared commit:receipt 存在时核对 commit SHA-256、project/job/worker/lease/result 绑定与逐 slot 权威记录,完全一致才返回 `AlreadyApplied`,不重复 job/binding 事件或 canvas revision。receipt 缺失但任一稳定 resource/asset/binding 已存在、同 operation 异指纹/异内容、已过期 lease 都失败关闭;事务前已确认 object 只在候选全字段精确一致时复用。canvas CAS 冲突时只刷新 project 重算 layout,不再次调用 Provider 或上传 OSS。`completed_at_micros` 必须为正数,候选原时间字段与它一起绑定到 commit SHA-256,重放不得重新取时;job 终态与事件使用 SpacetimeDB `ctx.timestamp`。OSS `PUT / HEAD` 仍位于事务外,可留下无引用 object,不声称跨 OSS exactly-once。
|
||||
|
||||
角色形象、图标 spritesheet 和 UI 素材提取在 provider 原图已可用且 OSS 上传已验证后,如果透明背景处理最终失败,最终 prepared commit 只保留原图并用它完成 `canvasCompletion`,不创建或回填透明处理图,图标和 UI 也不继续拆分,任务保持 `completed`。这个 source-only 降级只包住透明背景处理的最终失败;phase 上报、原图候选构造、透明处理图候选构造或统一原子提交失败仍按任务错误传播。
|
||||
|
||||
透明背景处理正常成功时,角色形象、图标 spritesheet 和 UI 素材提取的画布都同时放透明主结果与 provider 原图:透明主结果保持生成器 `generatedLayerId` 主锚点,provider 原图作为第二个图层放在其右侧;图标和 UI 实际拆分出的业务素材从 provider 原图右侧继续排列。
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# 编辑器生成结果原子提交与幂等重放方案
|
||||
|
||||
日期:`2026-08-06`
|
||||
|
||||
## 目标
|
||||
|
||||
修复 Issue #134:现役图片、图片修改、背景移除、图标图集、UI 素材提取、角色动作、视频、音效和背景音乐生成,在 OSS 结果已经可用后,必须把正式 `asset_object`、`editor_project_resource`、`editor_asset`、可选画布完成和队列终态作为同一个可重放提交处理,禁止继续按多个独立 SpacetimeDB procedure 分段写入。
|
||||
|
||||
本方案只承诺数据库内原子性。OSS `PUT / HEAD` 仍位于 SpacetimeDB 事务外;事务失败可能留下尚未登记或尚未引用的对象,后续按 operation 前缀做异步清理,不把它描述成跨 OSS 的 exactly-once。
|
||||
|
||||
## 权威操作身份
|
||||
|
||||
- 默认 queue 模式:`external_generation_job.job_id` 是唯一 operation ID。External v1 的 `Idempotency-Key`、主站稳定 `x-request-id` 和 Editor Agent 确定性任务 ID 都先收敛为该 job ID。
|
||||
- inline 兼容模式:使用 `RequestContext.request_id` 作为 operation ID,并对规范请求计算 SHA-256 fingerprint;同 ID 异 fingerprint 必须返回幂等冲突。inline 也必须写 durable receipt,不能只靠进程内 prepared result 或稳定记录 ID 猜测是否已提交。
|
||||
- Provider `taskId` 只保留为生成审计字段,不参与正式记录唯一性。
|
||||
- 每个 operation 的产物以稳定 `slot` 区分,例如 `provider-source`、`primary`、`processed`、`slice-0000`、`animation-preview`、`animation-final`。记录 ID 按 `owner + operation kind + operation ID + slot + record kind` 做 domain-separated SHA-256 派生,产物顺序变化不能改变既有 slot 的 ID。
|
||||
|
||||
## 统一 procedure
|
||||
|
||||
在 `spacetime-module` 增加 `persist_editor_generation_result_and_return`。procedure 只允许 editor generation runtime service identity 调用,并在一个 `try_with_tx` 内完成全部动作。
|
||||
|
||||
输入的编码级形状:
|
||||
|
||||
```rust
|
||||
EditorGenerationResultPersistItemInput {
|
||||
slot: String,
|
||||
asset_object: Option<AssetObjectUpsertInput>,
|
||||
project_resource: Option<EditorProjectResourceCreateInput>,
|
||||
asset: Option<EditorAssetCreateInput>,
|
||||
binding: Option<AssetEntityBindingInput>,
|
||||
}
|
||||
|
||||
EditorGenerationResultPersistInput {
|
||||
owner_user_id: String,
|
||||
operation_kind: String,
|
||||
operation_id: String,
|
||||
operation_fingerprint: String,
|
||||
items: Vec<EditorGenerationResultPersistItemInput>,
|
||||
canvas_layout: Option<EditorProjectLayoutSaveV2Input>,
|
||||
job_completion: Option<ExternalGenerationJobCompleteInput>,
|
||||
completed_at_micros: i64,
|
||||
}
|
||||
```
|
||||
|
||||
输出返回 `Applied / AlreadyApplied`、逐 slot 的 object/resource/asset/binding 快照、可选 project 快照和可选 job 快照。
|
||||
|
||||
### Durable receipt
|
||||
|
||||
新增私有表 `editor_generation_operation`,它是 queue 和 inline 共用的 durable commit receipt,不是第二套业务状态或任务队列。主键 `operation_key` 由 owner 和 operation ID 做 domain-separated SHA-256 派生,因此同 owner 不得跨 operation kind 复用同一 operation ID;表内固化 `owner_user_id / operation_kind / operation_id / operation_fingerprint / commit_sha256 / project_id / job_id / job_worker_id / job_lease_token / job_result_payload_sha256 / completed_at`。
|
||||
|
||||
- `operation_fingerprint` 绑定用户请求;`commit_sha256` 对完整 `EditorGenerationResultPersistInput` 的稳定 BSATN 编码做 domain-separated SHA-256,另外绑定本次准备提交的 slot、object/resource/asset/binding、画布候选与 job completion。不得使用 Rust `Debug` 文本充当持久协议,两类指纹也不得混为一个。
|
||||
- queue 路径必须把 receipt 与原 `job_id + worker_id + lease_token + result_payload_json` 全量绑定;receipt 只保存 payload SHA-256,不复制正文。首次提交仍必须验证当前有效 lease,完成后重放以 receipt 为提交凭证,并回读已完成 job 核对业务身份、权威 compact result 及其 SHA-256。
|
||||
- receipt 只保存幂等校验所需的有界元数据与摘要,不复制 project/canvas 大快照,不代替 resource、asset、binding 和 job 的权威表。
|
||||
|
||||
### 首次提交顺序
|
||||
|
||||
1. 校验调用身份、operation 字段、fingerprint、item 数量上限和 slot 唯一性。统一提交最多接受 66 个 item,用于容纳最多 64 个图集切片以及 provider 原图和透明整图。
|
||||
2. queue 输入必须完整携带 `job_id + worker_id + lease_token + result_payload_json`;inline 输入必须全部省略,禁止半套 guard。
|
||||
3. queue 路径在同一事务快照内校验 job owner、kind、request fingerprint、running 状态和有效 lease;过期 worker 不得写业务结果。`source_entity_id` 必须精确等于本次唯一结果 `project_id`,不得用同 owner 的 job 向其他项目提交。
|
||||
4. 对每个 item 校验稳定 object/resource/asset ID、owner、project、folder、object key、source resource、task 审计字段和媒体字段的交叉一致性。project resource 和 account asset 的 `source_resource_id` 均必须单独验证:来源资源必须是本次同事务候选或已登记资源,属于同 owner,且在结果具有项目上下文时属于同 project;不接受 asset-only 分支绕过血缘校验。
|
||||
5. `asset_object` 存在于输入时在同一事务内做精确 upsert;省略时,resource/asset/binding 引用的 object 必须已登记且属于同 owner。事务前 OSS `HEAD` 成功不等于 object 已正式登记。
|
||||
6. 创建全部 project resource、account asset 和可选 `asset_entity_binding`。binding 必须指向同 slot 的 object 与对应 resource/asset 实体,且 owner、asset kind、entity kind/id 和稳定 binding ID 完全一致。不得接受普通 media reuse 返回另一个随机 resource ID;稳定 ID 已被占用且内容不一致时失败关闭。
|
||||
7. 有 `canvas_layout` 时调用既有 V2 layout 持久化函数,以 `expected_revision` 做 CAS,并继续执行 legacy / structured 大小、资源引用和媒体族门禁。
|
||||
8. queue 路径最后调用事务内 job complete,写入调用方预先按现有规则构造的 compact result payload;再写入 durable receipt。任一步失败时 object/resource/asset/binding/canvas/job/receipt 全部回滚。
|
||||
|
||||
`completed_at_micros` 必须为正数,首次提交把它固化为 receipt `completed_at`。object/resource/asset/binding/canvas 候选各自现有的时间字段连同 `completed_at_micros` 一起进入 commit SHA-256;同一 prepared commit 的未知结果重放必须复用原时间,不得重新取时。明确的 canvas CAS 表示该事务已回滚,刷新 project 后形成新的 layout candidate,使用刷新时的 `updated_at_micros`,避免把并发用户刚写入的项目时间回拨。job `completed_at/updated_at` 与 job event 时间仍由 SpacetimeDB 事务时间 `ctx.timestamp` 产生,不信任调用方时钟。成功重放返回原快照,不刷新 receipt、业务记录、事件或 canvas revision。
|
||||
|
||||
### 重放
|
||||
|
||||
- receipt 存在时,只允许相同 owner/kind/ID、operation fingerprint、commit SHA-256 与原 project/job 绑定的完整重放;queue 额外核对原 worker/lease/result payload。逐 slot 权威 object/resource/asset/binding 和可选 project/job 仍必须可读且与候选一致,不得只看 receipt 就伪造快照。
|
||||
- receipt 存在且全部事实一致时返回 `AlreadyApplied`,不得新增记录、重复 binding changed / job completed 事件、刷新时间或推进 canvas revision。
|
||||
- receipt 缺失但任一稳定 asset object/resource/asset/binding、画布结果或已完成 job 已存在,属于可疑的部分写入,必须失败关闭;不得临时补 receipt 后声称幂等。现役生成 prepare 阶段只做 OSS PUT/HEAD,不得在统一 procedure 前单独登记稳定 asset object。
|
||||
- procedure 调用结果未知时,调用方最多自动重放同一 prepared commit 两次,不重新调用 Provider 或重新上传 OSS;明确的业务错误和 CAS 冲突不进入传输重放。
|
||||
- `operation_id` 在 `external_generation_job` 中已存在时,首次提交和 receipt 重放都必须携带与它一致的完整 job completion guard;只有事务内确认不存在同 ID job 时才允许 inline。
|
||||
- 同一 item 的 resource/asset 尺寸、媒体引用、task、asset kind 与生成元数据必须一致;binding 必须匹配 operation 明确允许的 entity/slot/kind/profile tuple。音频使用 `sound-effect -> editor_sound_effect`、`background-music -> editor_background_music` 显式映射,不使用粗暴的全字段硬等。
|
||||
- item 省略 `asset_object` candidate 而复用已登记对象时,首次提交与 `AlreadyApplied` 重放都要回读 canonical object,重新验证存在性、owner、object key、task、kind 和音频媒体类型。
|
||||
|
||||
## api-server 接入
|
||||
|
||||
- 通用持久化改为 `prepare -> build canvas candidate -> atomic commit`。prepare 阶段只生成稳定 ID、上传/验证对象和构造候选 DTO,不创建 resource/asset。
|
||||
- api-server 继续复用现有画布 completion / replacement 逻辑计算候选 `layers_json` 和 `expected_revision`;统一 procedure 在最终事务内重新执行既有 layout 校验和 CAS。
|
||||
- CAS 冲突只刷新当前 project、重新计算 layout 并重试 prepared commit;相同 operation、slot、对象和记录候选保持不变,禁止重跑 Provider。
|
||||
- 重新计算 layout 时只允许 revision、layers 与 layout `updated_at_micros` 随最新 project 变化;业务 items、job result payload 和 `completed_at_micros` 保持不变。首次 CAS 事务已明确回滚,因此刷新后的 layout 是新的 prepared commit;该 commit 若结果未知,只能原样重放自身。调用方最多自动刷新一次,第二次冲突直接返回。
|
||||
- queue completion 不持久化 inline handler 的完整响应:普通画布任务只保留 source/warning 元数据,Editor Agent 只写入裁剪后的 `editor-agent-tool-call-result`,External API 只写入裁剪后的 `result`。图集/UI 切片不得在 queue payload 中重复携带完整 resource/asset/prompt/generationInputs,最终 JSON 必须在 512 KiB 持久化上限内。
|
||||
- compact result 必须先满足原消费 DTO 的必填字段:角色动作/视频保留 `ok`,图标/UI 正常与 source-only fallback 保留 `ok / prompt / actualPrompt`,音效/BGM 保留 `prompt`;Editor Agent 与 External v1 的二次 allowlist 裁剪都不得再删除 `prompt / actualPrompt`,最终持久 payload 必须能反序列化为对应 response contract。Editor Agent 图片生成/修改 DTO 的 `provider` 为可选审计字段,compact payload 可删除它而不影响终态回填。External API 的角色动作与视频结果还必须保留本次已创建账号素材的稳定 `assetId`;裁剪可移除大 payload,但不得让 completed 结果无法定位正式素材。
|
||||
- queue 消费者身份在 worker 从完整 claimed job 构造调用上下文时固化;原子提交不得再从 summary 兼容快照反推,因为该快照会清空 dedupe key 并裁剪 request payload。
|
||||
- queue 的 compact result 当前不保存 `project`,因此可在事务前由稳定候选 resource/asset 和生成响应元数据构造;HTTP 成功响应中的 project 使用 procedure 返回的权威快照。
|
||||
- worker 在统一 procedure 已完成 job 后不得再次调用 `complete_external_generation_job`。只有 `Applied / AlreadyApplied` 才能作为成功终态。
|
||||
- Provider 已成功且计费 attempt 已扣款后,若原子提交确定失败并要把 job 置为终态 `failed`,必须由同一 SpacetimeDB 事务先验证当前 worker/lease,再结算当前 attempt 退款并写失败终态。不得在 api-server 先独立退款,否则过期 worker 或已成功但回包丢失的提交可能同时得到正式结果与退款。
|
||||
- inline 模式没有 job 失败事务补退,计费成功边界必须延迟到 durable result commit 完成。Provider/上传成功后的明确持久化失败退还已扣泥点;`Build / PoolAcquire / ConnectBuild / ConnectHandshake` 等 procedure 未发出阶段的失败或取消仍通过 deferred guard 退款。procedure dispatch 后到明确回包前必须标记结果未知;连续传输不确定或此窗口内 HTTP future 被取消时保留扣款,避免远端已成功时变成「正式结果 + 退款」。`Procedure` 回包是确定结果,成功或明确失败后必须清除未知标记。
|
||||
|
||||
## 多产物与现有特例
|
||||
|
||||
- 图片的 provider source、透明/规整结果和 source-only fallback 必须在最终选择明确后一次提交;fallback 只提交实际保留的结果集合。fallback compact result 的尺寸、`prompt / actualPrompt`、model 和图标/UI `priceMudPoints` 必须来自本次已冻结生成上下文,不得从可选 project resource 反推;不带 `projectId` 时仍必须产生完整消费契约。
|
||||
- 图标图集和 UI 提取使用稳定 slot 提交 provider source、透明整图和成功切片;切片失败时按既有 warning 语义只提交可信整图集合。
|
||||
- 角色动作一次提交预览视频与最终序列素材;逐帧 `asset_object` 可作为 item upsert 或已登记对象被最终序列引用,正式 project resource / account asset 与 canvas 不得分段提交。
|
||||
- 视频、音效和背景音乐使用单个 primary item。
|
||||
- 完美像素保留现有专用 operation/fingerprint/procedure;手动图集拆分不调用 Provider,不属于本次九类生成 job 的原子提交范围,继续使用现有批量 procedure 与画布完成链路。
|
||||
|
||||
## Schema 与兼容性
|
||||
|
||||
- 新增私有 `editor_generation_operation` durable receipt 表;它与 `external_generation_job` 分工,前者证明一笔业务结果原子提交,后者仍是 queue 执行、lease、计费和通知真相。新表必须纳入 `migration.rs` 导入/导出、schema 检查和本文档表目录。
|
||||
- 新增 Spacetime procedure/type ABI 后必须重新生成 `spacetime-client` bindings,并同步 facade mapper。
|
||||
- HTTP 路由、请求/响应 DTO、header、状态码和 External v1 异步语义保持不变,因此不修改 OpenAPI;必须复跑 External v1 契约测试证明没有漂移。
|
||||
- inline 兼容模式没有 durable job,但必须具有同样的 durable receipt、稳定 ID、fingerprint 和单事务重放;这仍不授权浏览器自动重试已可能发出的生成 POST,调用方应先走结果对账。
|
||||
|
||||
## 验收
|
||||
|
||||
- 资源创建后资产或 binding 校验失败:事务结束后 object/resource/asset/binding/canvas/job/receipt 均无部分写入。
|
||||
- 资源/资产创建后 canvas revision 冲突:全部业务记录回滚;使用同 operation 和 prepared result 刷新布局后可成功。
|
||||
- 成功后相同 operation 重放:返回原 object/resource/asset/binding/project/job,receipt 只有一行,记录数、时间、完成事件数和 canvas revision 不变。
|
||||
- 同 operation 异 request fingerprint、异 commit SHA-256、异 project/job 绑定、除精确可复用 object 外的部分既有记录、缺失 receipt 和过期 lease:失败关闭且零新增写入。
|
||||
- queue job 的 `source_entity_id` 与结果项目不同、或 `source_resource_id` 不属于同 owner / project:失败关闭且零新增写入。
|
||||
- Provider 成功后原子持久化确定失败:有效 lease、当前计费 attempt 退款与 job `failed` 在同一事务内成功或回滚;已 completed 或过期 lease 失败关闭且不退款。External 角色动作/视频成功结果保留稳定素材引用。
|
||||
- legacy 与 structured canvas、dialog 已删除、无 project/asset folder、单产物、多产物、64 切片和角色动作序列均覆盖。
|
||||
- 图片、修改、背景移除、图集、UI 提取、角色动作、视频、音效、背景音乐的生产路径不得再出现 `create resource -> create asset -> save canvas -> complete job` 分段组合。
|
||||
@@ -872,7 +872,7 @@ V1.21 对标 Codex CLI 的 `model_context_window`、`model_auto_compact_token_li
|
||||
|
||||
### 配置与预算
|
||||
|
||||
- `llm` 新增 `contextWindowTokens / autoCompactTokenLimit / toolOutputTokenLimit`,发布默认分别为 `128000 / 64000 / 12000`;`agentLlm.<agentId>` 复用现有 patch 继承,显式 Agent 值覆盖全局。三项都必须大于 0,自动阈值必须小于 context window,并为当前请求的 `maxOutputTokens` 与固定安全余量留下空间。
|
||||
- `llm` 新增 `contextWindowTokens / autoCompactTokenLimit / toolOutputTokenLimit`,发布默认分别为 `128000 / 64000 / 12000`;`agentLlm.<agentId>` 复用现有 patch 继承,显式 Agent 值覆盖全局。三项都必须大于 0,自动阈值必须小于 context window,并为当前请求的生成 token 预算与固定安全余量留下空间。既有配置和持久协议键 `maxOutputTokens` 保持冻结以兼容恢复;它表示包含可见输出与隐藏 reasoning token 的生成侧预算,不表示输入加输出总量,也不保证可见正文长度。
|
||||
- Runtime 在发送 tool-plan、context-compaction 或 final-reply 前,按消息、multimodal 文本和 function schema 的规范序列化字符数做保守 token 估算;Provider 返回 usage 时再记录真实 `prompt/completion/total`。估算只用于提前门禁,不能伪装成 Provider 计费事实。
|
||||
- 单条 observation 进入模型上下文前按 `toolOutputTokenLimit` 收紧;完整命令输出仍留在 owning Agent 的私有 sidecar,通过既有分页工具读取。公共状态只显示估算 token、最近真实 usage、阈值、压缩次数和时间,不显示被压缩正文。
|
||||
|
||||
|
||||
@@ -31,17 +31,17 @@
|
||||
- Windows AppData 安全迁移:首次创建客户端 AppData 时必须以进程 `TokenUser` SID 显式设置 owner,并写入当前用户私有 DACL,不能把可能为 Administrators 的 `TokenOwner` 当作用户身份。发现历史目录 owner 不属于当前 `TokenUser` 时,不在原目录上放宽权限,而是拒绝 reparse point / junction / symlink 后,将旧目录原子重命名到同级唯一 `.owner-mismatch-backup-*` 备份,再新建并验证当前用户 owner 与私有 DACL;迁移或备份失败必须失败关闭,不覆盖旧配置。
|
||||
- Windows Runner 私有文件初始化:父 AppData 已归当前 `TokenUser` 后,新建 `agent-runner.lock`、endpoint 临时文件、project-owner 诊断临时文件与 real-E2E 私有文件的 owner 仍可能采用 token 默认 owner `Administrators`。固定 stale lock 只有在父目录已验证为当前用户 protected 私有 DACL、Windows 不共享独占句柄已取得、且句柄确认普通文件、非 reparse point、链接数为一时才允许修复;其它三类文件只允许在本进程 `create_new` 成功且仍持有同一独占句柄时初始化 `TokenUser` owner / DACL,再写入、原子安装并严格复核,初始化失败必须清理刚创建的文件。既有 durable endpoint / diagnostic 读取不得自动接管;活锁不得截断,只有 sharing / lock violation `32/33` 表示占用,access denied 等其它错误立即返回。父进程观察到 Runner 子进程退出后立即返回错误,不等待完整 30 秒 deadline。
|
||||
- 启动诊断:独立 release 的 `startup.log` 和 `agent-runner.log` 只记录有界、脱敏的阶段与 stdout / stderr 摘要,凭据、AppData 路径和其它绝对路径不得原样落盘;单文件达到 256 KiB 后只轮转保留一份 `.previous.log`。`startup.log` 优先写独立 AppData,目录不可写时回退到系统 TEMP 下的 `Genarrative-Game-Chat-Diagnostics`;Tauri context、窗口 URL、AppData、Runner 或 `.setup()` / `.build()` 初始化失败时,Windows 必须显示可见错误对话框并给出诊断日志位置,不能只在无控制台 release 中静默退出。
|
||||
- 对话与事件:窗口固定使用 `project-supervisor + autonomous-game-build`,继续复用 active Session、External Runner、持久 conversation、流式回复、same-run steer、工具确认与用户追问。以 `/` 开头的输入必须继续走现有内置命令解析,例如 `/preview` 只能生成 `preview.start` 确认卡,不得作为自主构建任务投递给 Supervisor。界面聚合当前 Supervisor 父 run 及其直接委派专业 Agent 的最新原始事件,按时间倒序稳定去重并标注 Agent;默认显示 4 条,可展开至最新 20 条。原始 `summary / detail` 仍只作 Runtime 状态投影,不直接写入 conversation。需要进入聊天的事件必须由 Rust 同步生成唯一 `eventId` 与安全 `publicText`;前端只按这两个字段形成独立 assistant 消息,无 `eventId`、空 `publicText`、legacy 事件和内部 tool / Provider / Runner 协议一律忽略。
|
||||
- 对话与事件:窗口固定使用 `project-supervisor + autonomous-game-build`,继续复用 active Session、External Runner、持久 conversation、流式回复、same-run steer、工具确认与用户追问。以 `/` 开头的输入必须继续走现有内置命令解析,例如 `/preview` 只能生成 `preview.start` 确认卡,不得作为自主构建任务投递给 Supervisor。game-chat 的自主链路中,Supervisor 持久化意图后只有 `code-prototype` 是主 Agent;它可能临时委派一个受限美术 child,后者只写 `assets/**`,回执返回同一主 Run 后由主 Agent 接入与验收。界面聚合当前 Supervisor 父 run、单主 Agent 及其直接美术 child 的最新原始事件,按时间倒序稳定去重并标注 Agent;默认显示 4 条,可展开至最新 20 条。原始 `summary / detail` 仍只作 Runtime 状态投影,不直接写入 conversation。需要进入聊天的事件必须由 Rust 同步生成唯一 `eventId` 与安全 `publicText`;前端只按这两个字段形成独立 assistant 消息,无 `eventId`、空 `publicText`、legacy 事件和内部 tool / Provider / Runner 协议一律忽略。
|
||||
- 公开消息硬门:模型仍负责 Supervisor / 专业 Agent 回复的业务语义,Runtime 不根据 tool 或 Provider 事件自行补写业务结论;但用户直接投递的 Project Supervisor 根后台任务必须先落为不可执行的 `preparing / public-status-pending`,再以 `runtime-public-status-*` 稳定 message ID 把“任务已接收,正在启动处理”写入项目 conversation,成功后才转为 `pending / queued`;恢复预检只读,只能在验证到同 run accepted 消息后把该任务临时分类为可恢复,真实 resume 持有 Agent 锁后才可持久提升为 `pending / queued`;写入失败则落为 `failed / public-status-write-failed`,不得继续执行。这些 Runtime 公开状态只供 UI 展示,prompt 构建器必须按稳定前缀排除。根 Supervisor 通过正式失败 / 预算耗尽收束或 game-chat 绝对硬期限进入 reconciliation 时,必须在 task、event、state 等其它终态投影之前先幂等写入一条脱敏、用户可理解的失败消息;专业 Agent 命中该全局硬期限时,也必须通过权威 Run Profile 和根 task 将同一根终态写入项目 conversation,同时保留 child 私有 Session 状态;状态文件本身写坏也不能导致零公开结果。当前 Runtime 自称根 agent/run 时,其 session 和两个 parent 字段必须与权威根 task 一致;任一身份冲突必须失败关闭,不得以另一 session 派生第二条项目终态。前端把该前缀识别为 Runtime-owned,同秒时排在触发它的 Supervisor 用户消息之后,不二次持久化;仅根 Supervisor 的 `turn.started / turn.failed / turn.budget_exhausted` 只保留在 Runtime 详情和进度投影中,不能再生成第二条聊天消息,专业 Agent 的公开启动事件仍可见。该硬门不改变 final-reply 的唯一性;非 Supervisor 专业 Agent 的失败消息继续留在对应 Agent Session,不把私有诊断写进项目 conversation。
|
||||
- 启动恢复和续跑边界:本条取代上一条中“只有 accepted 才可恢复”的窄口径。若进程在 Supervisor 用户消息已持久、accepted 未持久之间崩溃,只读 preflight 可以把该 `preparing` 识别为可恢复,但不改写 task/conversation;真实 resume 持有 Agent 锁后必须先幂等补写 accepted,再提升为 `pending / queued`。用户消息或 accepted conversation 已落盘而辅助审计失败时,以 conversation 为公开真相继续入队,不留下“已接收但永不执行”的任务;根终态首次公开写入的瞬时失败必须在终态投影后用相同 message ID 重试。receipt / isolated-join 等带 parent 的 Supervisor continuation 不再另写 Session 终态,只保留单一后端公开事件;`runtime-task-*` 与 `runtime-public-status-*` 共享同 run 的不透明关联摘要,秒级时间戳下多个连续任务必须按实际 run 对应的 `user -> accepted -> terminal` 顺序交错展示。
|
||||
- Supervisor 进度播报:聊天消息流内保留且只保留一条当前 run 的 Runtime-owned 播报卡,由客户端从 manifest 任务图、Supervisor 结构化计划、`loopIteration`、当前动作、直接委派专业 Agent 及其持久事件确定性整理;显示当前轮次、任务 / 计划进度、活跃 Agent、最近试玩与静态检查、返工决定、代码修改和截图检查证据。同一 run 原位更新,切换 run 时替换,不调用额外模型、不追加持久 conversation,也不改变最终 assistant 回复的唯一性;任意详情必须有界且不展示绝对路径、Provider 元数据或内部指纹。
|
||||
- ready-task 启动活性:`background_task.queued`、`autonomous_ready_task.scheduled`、Runner heartbeat 或执行锁已移交都不等于 child 已启动。实际持有执行权的 Runner 必须在释放项目写锁后同步写入 child 的 running task、`turn.started` 与 started journal,再把已启动 state 和 per-Agent 执行锁交给已确认开始轮询的独立 execution worker;同步启动或 worker 接管失败时,要在仍持有执行锁期间依次把 child 和 manifest Graph 节点明确落为 failed,再释放锁并让 parent 收到调度错误。`autonomous_ready_task.scheduled` 只作诊断审计,其写入失败不能阻断 durable child 启动;external client 只 wake Runner,不在客户端抢占执行。Supervisor 进度卡通过 durable `startedAt`(旧 Run 从完整 task journal 恢复,最新 task-record fallback 保持 0)显示真实持续时间,并以父 Run 与当前关联专业 Agent 的最大事件时间计算运行态活跃度:运行超过 5 分钟无新事件时显示“运行中 · 疑似停滞”和静默时长;等待用户、等待确认、Provider retry、视觉资产、进程会话、pausing 与 paused 不误报。父 Run terminal 后,持续时间冻结在父 Run 自身最后活动,不随 child 晚到收口事件增长。消息时间统一校验为 JavaScript 可表示的 Date;越界值显示“时间未知”且不写无效 `datetime`。实时回复只显示 response stream 自己的 `updatedAt`,缺失时同样显示“时间未知”,不能借用其它 Runtime 活动时间或随前端时钟漂移。该提示只提供可观测性,不改变 Runtime/manifest 正式状态。
|
||||
- ready-task manifest 漂移:父 Supervisor 必须分别判断“能否调度新节点”和“是否存在必须等待的工作”。派生视觉需要父规划修复时不再调度新 child,但当前最新且活跃的根 Run 下,只要存在确定性 runId、scheduler source、正确父绑定且 durable journal 为 queued/running 的 ready child,父 Run 就保持 `waiting-for-manifest-tasks`,不能因旧 hydration 快照把 manifest running 覆盖成 pending 而提前 fixed-graph-stalled。game-chat child 可在相同严格身份下容忍 pending 漂移;正式产物、Canvas、revision、`game.static_smoke` 与 `preview.validate` 门禁不放宽。GUI/CLI、旧父 Run、终态、确认/用户输入/reconciliation、伪造绑定或非确定性 runId 全部失败关闭;更新根 Run 后旧 child 不得继续维持新 DAG 或投影完成。
|
||||
- Supervisor 主导的条件美术路由:game-chat 的关键词、用户是否报告“美术未接入”、占位状态和当前资产探测只形成 `advisoryOnly=true` 的补充上下文,不得直接重置 Graph、预完成美术节点、选择复用/生成分支或继承历史试玩类型。当前根 Run 没有持久化 Supervisor 决策时,scheduler 不启动任何 manifest child,Supervisor Provider 必须先通过 auto-safe 的 `agent.route_manifest` 选择 `audit-existing-first` 或用户明确要求整体重做时的 `regenerate-art`。决策后首波只开放 `design-director + code-director`;`code-director` 必须先成功调用 `asset.list`,再以同一工具提交 `use-existing-art / generate-missing-art / regenerate-art` 与精确 `missingAssetSlots`。Runtime 只负责校验 root/child 身份、当前 revision、Canvas 登记、私有图集合同、四张语义切片、art manifest、覆盖 fingerprint 和路由 fingerprint,并据此把 `art-director / art-asset-plan` 投影为 completed 或 pending;缺少 art spec 若同时使图集引用合同失效,两个槽位都属于真实缺口,不能为了少调一个 Agent 伪称图集可复用。只有持久路由明确复用 `art-asset-plan` 时,根完成门才忽略 `assets/manifest.art.json(unchanged-from-run-baseline)`;其余产物、可见代码使用与试玩门禁不放宽。明确 `regenerate-art` 时两个正式图片 owner 使用严格绑定当前 root Run 的原位替换授权。纯“继续”仍走既有正式 continuation 合同;普通美术措辞不得借用更老项目的具体试玩场景。不得以增加 loop 预算、伪造 revision、机械改写 manifest 或重放历史图片 action 代替 Supervisor 决策和程序侧审计。
|
||||
- Supervisor 持久决策与单主条件美术:game-chat 的关键词、用户是否报告“美术未接入”、占位状态和当前资产探测只形成 `advisoryOnly=true` 的补充上下文,不得直接重置 Graph、预完成美术节点、选择复用/生成分支或继承历史试玩类型。当前根 Run 没有持久化 Supervisor 决策时,scheduler 不启动任何 child;Supervisor Provider 只通过 auto-safe 的 `agent.route_manifest` 提交 `game-chat-workflow-decision.v2`:`intentSummary` 是 Supervisor 自行理解并持久化的用户意图,`strategy=audit-existing-first` 只是固定安全执行策略,两者不得混用。此动作不能审计、生成、委派或替代后续判断,也不能把整体视觉重做解释成整套美术的强制重生成;成功后 Runtime 只启动唯一 `code-prototype` 主 Agent。升级恢复时严格校验 v1 sidecar 的旧 fingerprint,并从完成合同绑定的有效任务恢复 `intentSummary`;旧 `code-director` coverage/route 只作为迁移输入,不作为当前完成证据,必须由同一根 Run 的 `code-prototype` 重新 `asset.list` 后原位替换为单主合同。确定性 `code-prototype` Run 仅兼容已知 canonical task 文本版本,其余 task/binding/root 身份继续失败关闭;升级前已运行的 fixed-graph 美术 child 不再具备任何 mutation 或生图权限。主 Agent 必须以当前正式资产、Canvas 登记、私有图集合同、四张语义切片和 art manifest 判断真实缺口;完整覆盖时直接接入,不得生成或扣费。只有可证实缺失 `art-spec` 或核心 spritesheet 时,主 Agent 才可对相应 `art-director` 或 `art-asset-plan` 建立一条 durable 委派;每次最多一个活跃美术 child,child 仅可写 `assets/**`,不得修改 `game/**` 或接入/验收游戏。若两个槽位都缺失,必须先完成 `art-director`,由同一主 Run 认领其 `EvidenceReady` delivery 后,才能委派依赖规范图的 `art-asset-plan`;失败或未就绪 delivery 不得消耗不可重试的图集委派槽位。主 Agent 认领必要回执后继续同一 Run 完成素材接入、原玩法语义校验、`game.static_smoke` 与桌面/移动 `preview.validate`。绝对硬截止对嵌套美术 child 继续核验 `root -> code-prototype -> agent-delegate` 完整身份并保留未知外部生成的 reconciliation 证据。Runtime 只负责校验根/父子身份、当前 revision、路径、Canvas 登记、缺口/路由 fingerprint、写入范围及完成证据;纯“继续”仍走既有正式 continuation 合同,普通美术措辞不得借用更老项目的具体试玩场景。不得以增加 loop 预算、伪造 revision、机械改写 manifest 或重放历史图片 action 代替 Supervisor 决策和程序侧审计。
|
||||
- ready-task 对账取消续跑:未知工具结果仍停在 `needs-reconciliation` 且禁止自动重放;人工核对后显式取消原 child,保留 cancel tombstone,旧 child 和旧父 Run 按真实终态收口。若随后创建同 Session、同 Supervisor source、同有效任务语义的 continuation,新完成合同只对同时具有历史 `failed / needs-reconciliation`、最终 `cancelled` 和 durable tombstone 的 ready-task,把当前 manifest 对应 failed 节点恢复为 pending,并由 scheduler 创建全新 child Run。manifest 的读取、failed 筛选、每任务一次的 child journal 索引、证据重验和写回必须位于同一项目写锁域;较新的无 child 根 Run 只有在 durable journal 精确表明为旧 failed Graph 在进入调度前即失败时才能跨过,scheduler 自身失败必须阻断借用更老 tombstone。普通失败、无 tombstone、不同 source/Session/任务语义或证据冲突均保持失败关闭;不得复活旧 pending action、补造 observation 或把取消任务标成 completed。
|
||||
- 完成门静态分析预算:Canvas 视觉门必须先做只会提前拒绝的词法预检。经典或模块脚本同时不含大小写精确的 `import` 与 `export` 字节序列时,不运行模块依赖语义分析;纯 `export ... from` / `export * from` 仍须进入正式模块图分析。当前脚本不含目标文件名或任一已绑定 DOM 图片元素 ID 时,先低成本解码 `\\xNN`、`\\uNNNN`、`\\u{...}`、简单转义和续行;解码后仍无候选才不运行完整 Canvas alias / 函数可达性分析,解码不确定则保守进入 Oxc。存在任一候选时仍执行原 parser、semantic binding、解码后的 computed 属性/StringLiteral 路径、可达 `drawImage`、可见 Canvas、路径大小写和动态 namespace 写入门禁;HTML 中存在某个绑定元素不得使所有无关 JavaScript 单元进入重分析,禁止把词法命中当作通过条件。
|
||||
- Provider 故障展示:Provider retry 的“是否可重试”继续使用 `upstream-5xx` 等稳定类别判断,但 durable retry record 保留安全的精确 `upstream-<HTTP status>` 身份。等待态必须从真实 record 显示 HTTP 状态、`nextAttempt/maxRetries` 与当前持久退避剩余秒数,例如“Provider 上游返回 HTTP 503,准备自动重试 1/3;预计 8 秒后重试”;不得以动画或前端自增计时伪造 attempt。重试耗尽的 Runtime 私有错误只保存 `kind/httpStatus/fingerprint/chars/retryAttempt/maxRetries/retryState`,前端和持久 conversation 仅在字段顺序、范围、状态一致且无尾随正文时派生“上游服务返回 HTTP 503;自动重试已耗尽(3/3)”;其它错误使用固定安全摘要。Provider 响应正文、URL/query、凭据、本地绝对路径、fingerprint、字符数和 `[redacted ...]` 占位符均不得进入用户可见消息。
|
||||
- 跨轮阶段记录:game-chat 父 run 进入真实 completed / failed / cancelled 终态后,客户端等待 `design-director / art-director / art-asset-plan / code-director / code-prototype / preview-readiness / preview-playtest` 七项首版任务也全部投影到 completed / failed 终态,再把本轮、任务 / 计划完成度、最新试玩 / 静态检查、最近返工决定和已登记成果图片路径整理成一条 `【Supervisor 阶段记录】` 项目 assistant 消息。父 run 先终态而 manifest 仍在 hydration 时不得用陈旧 `0/7` 提前归档,要暂存终态 Runtime 并在 manifest 刷新后重试。页面初始 hydration 若直接读到缺少阶段记录的真实终态 run,也必须补写,但 `idle` 不是可归档终态。每个“项目 + 父 run”最多追加一次,进入现有 `conversation.write` 权限与项目 conversation 持久化链路,下一轮及重载后继续保留。阶段记录不是 Supervisor Runtime 正式回复,不写入 Agent Session、不增加 final assistant 数量,也不逐条复制原始事件或内部正文。
|
||||
- 跨轮阶段记录:game-chat 父 run 进入真实 completed / failed / cancelled 终态后,客户端等待唯一 `code-prototype` 主 Run 及其所有必要美术委派都已形成真实终态,再把本轮、主 Agent 进度、是否复用/补齐素材、最新试玩 / 静态检查、最近返工决定和已登记成果图片路径整理成一条 `【Supervisor 阶段记录】` 项目 assistant 消息。父 run 先终态而 child 或 manifest 仍在 hydration 时不得以陈旧快照提前归档,要暂存终态 Runtime 并在状态刷新后重试。页面初始 hydration 若直接读到缺少阶段记录的真实终态 run,也必须补写,但 `idle` 不是可归档终态。每个“项目 + 父 run”最多追加一次,进入现有 `conversation.write` 权限与项目 conversation 持久化链路,下一轮及重载后继续保留。阶段记录不是 Supervisor Runtime 正式回复,不写入 Agent Session、不增加 final assistant 数量,也不逐条复制原始事件或内部正文。
|
||||
- 图片成果:当前 manifest 新增或恢复已登记的 PNG / JPEG / WebP 资源时,聊天消息流同步显示 Runtime-owned “Supervisor 成果图片”卡,最多展示最新 4 张并随 manifest 原位更新。图片必须通过现有 `read_local_project_image_preview` 读取,只允许当前授权项目中 `assets/` 下的已登记资源,继续执行 `file.read` auto 权限、真实格式、大小、尺寸、普通文件、祖先目录和项目根边界校验;前端只接受返回路径、媒体类型和 `data:` 前缀与请求完全一致的结果。缩略图点击后使用独立模态查看器,支持按钮与滚轮缩放、指针拖拽、双击 / 按钮复位、Esc / 按钮 / 遮罩关闭,移动端占满视口;不得在聊天卡下方追加展开区。图片卡不写入 conversation,不解析 assistant 文本中的任意 Markdown / 绝对路径,也不开放 `.agent` 验收截图读取。
|
||||
- Run 接管:External Runner 模式下首次提交可能返回“旧 canonical state + 新 `acceptedRunId`”;页面必须以 `acceptedRunId` 作为本轮权威身份,在 state 尚未切换时显示“已投递,正在同步 Agent Runner”,并允许该 run 的 Tauri event 或轮询结果接管。不得把旧 idle state 当作本轮结果、过滤新 run 事件,自动预览授权也必须绑定 `acceptedRunId`。
|
||||
- 运行容器:当前项目没有由 Tauri 客户端 `PreviewRegistry` 返回的有效 `running` 预览时,页面只渲染聊天,不显示游戏区域或占位文案,顶部运行状态必须明确显示“预览未启动”,不得再使用含义不明的“未启动”;预览运行后自动显示 iframe,桌面端按“游戏 2 / 聊天 1”分栏,移动端改为上下布局。预览停止、失败或切换项目后立即移除 iframe。运行容器继续只接受当前授权项目的 `http://127.0.0.1:*`,复用现有 CSP、iframe sandbox、autoplay、fullscreen 和 gamepad 约束;远程 URL、`file://`、手填地址或陈旧 manifest 状态均不得显示。
|
||||
@@ -59,27 +59,27 @@
|
||||
|
||||
## 2026-07-31 game-chat 输出、单轮预览与平台美术资源
|
||||
|
||||
- 对话输出:game-chat 的 Supervisor `ready` response stream 继续以稳定身份显示;七任务目录中本轮实际启动的专业 Agent,其 `requestKind=final-reply` 且 `status=ready|committed` 的非空安全回复分别以 Agent、Session、run、request slot 和 response revision 形成 durable message ID,并带 Agent 标签逐条追加到项目聊天。条件路由跳过的美术节点只要求 manifest 正确投影为 completed,不伪造 Agent 回复。每条 Rust `eventId + publicText` 公开输出同样形成独立 durable 消息。所有这些消息通过 `append_local_conversation_message` 的顶层 `messageId` 幂等写入,事件、轮询、React StrictMode 和 hydration 重放不重复;tool-plan、半成品 stream、原始事件 detail、命令正文、绝对路径、Provider / Runner 元数据、哈希和凭据不得进入聊天。普通 `supervisor-chat` 保持原有 transient response 行为。
|
||||
- 对话输出:game-chat 的 Supervisor `ready` response stream 继续以稳定身份显示;本轮唯一主 `code-prototype` 与实际按缺口启动的美术 child,其 `requestKind=final-reply` 且 `status=ready|committed` 的非空安全回复分别以 Agent、Session、run、request slot 和 response revision 形成 durable message ID,并带 Agent 标签逐条追加到项目聊天。现有素材完整时不伪造美术 Agent 回复。每条 Rust `eventId + publicText` 公开输出同样形成独立 durable 消息。所有这些消息通过 `append_local_conversation_message` 的顶层 `messageId` 幂等写入,事件、轮询、React StrictMode 和 hydration 重放不重复;tool-plan、半成品 stream、原始事件 detail、命令正文、绝对路径、Provider / Runner 元数据、哈希和凭据不得进入聊天。普通 `supervisor-chat` 保持原有 transient response 行为。
|
||||
- 对话输出中的 `eventId + publicText` 只指需要独立进入聊天的进度事件;`turn.started` 和根 Run 终态失败事件由上一条 `runtime-public-status-*` 硬门覆盖,不得同时转成事件消息。专业 Agent child 的失败消息继续留在其 Agent Session,根项目聊天只接收 Supervisor 终态失败、明确公开进度和安全 final-reply,避免一项失败被 Runtime event 与 conversation 各播报一次。
|
||||
- 单轮收束:game-chat source 只生成至 `preview-playtest` 的 manifest seed task,试玩完成后父 Run 直接进入完成门,不再调度 `publish-strategy` / `publish-package`;`agent.schedule_ready` 必须按当前 Supervisor Run 的持久 source/profile 选择同一 source-aware scheduler,不能绕过该边界。`task.list` 对同一 root source 必须从任务行、readyTaskIds 和统计中排除两个发布节点,`agent.delegate` 也必须按 root binding 拒绝直接委派这两个节点,不能让 Provider 用“读取完整 DAG 后手工委派”恢复已裁掉的发布阶段。完成门满足且 collaboration、Provider batch、进程会话、视觉资源等非验证屏障全部清零后,Runtime 必须用确定性回复直接收束结构化计划并结束父 Run,不再请求下一次 Provider 工具计划。普通 GUI / CLI 仍执行完整发布 DAG。
|
||||
- 轮次展示:`loopIteration` 只是同一父 Run 内的 Provider / 工具规划循环,用于委派、回执、返工和验收,不是用户发起的游戏生成轮次。game-chat 的进度卡、当前工作和“最新状态”事件统一显示“本轮”,整个页面不向用户显示“第 N 轮”;完整 GUI / CLI pre-publish 任务图仍可显示 `x/14`,但首版只按七项任务显示 `x/7`(详见 2026-08-03 小节),不得把两个发布节点计入任一分母。父 Run 终态后移除运行中进度卡,只保留终态阶段记录与预览。
|
||||
- 平台美术资源:game-chat 保留正式视觉 DAG,但是否进入生成节点由 2026-08-06 的持久条件路由决定。现有正式资源覆盖完整时直接复用;存在真实缺口或 Supervisor 明确选择整体重做时,`art-director` 才通过平台 `images/generations(kind=spec)` 生成并登记 `assets/art-spec.png`,`art-asset-plan` 再以该规范图的稳定 resourceId 调用 `icon-spritesheets/generations` 生成真实透明 `assets/art-spritesheet.png`,并把响应中的 `iconImageSrcs` 下载为本地独立切片,写入 `assets/art-spritesheet-slices/manifest.json`。`code-prototype` 必须等待复用或补齐后的图集与切片清单,并在活动 Canvas 中分别绘制玩家、方块/目标、障碍/场景与反馈四类切片;规范图只作 reference,纯代码核心画面、猜测图集等分网格、整图 `<img>` / CSS 背景、完整图集直绘或只出现路径均不得完成。
|
||||
- 单轮收束:game-chat 只在 `code-prototype` 完成当前 revision 的接入、静态 smoke 与 desktop/mobile 试玩后进入完成门,不调度 `publish-strategy` / `publish-package` 或旧固定验证节点;`agent.schedule_ready` 必须按当前 Supervisor Run 的持久 source/profile 选择同一 source-aware scheduler,不能绕过该边界。`task.list` 对同一 root source 必须从任务行、readyTaskIds 和统计中排除发布节点以及不属于单主 route 的固定 DAG 节点,`agent.delegate` 也必须按 root binding 拒绝直接恢复这些节点。完成门满足且美术 delivery、Provider batch、进程会话等非验证屏障全部清零后,Runtime 必须用确定性回复直接收束结构化计划并结束父 Run,不再请求下一次 Provider 工具计划。普通 GUI / CLI 仍执行完整发布 DAG。
|
||||
- 轮次展示:`loopIteration` 只是同一父 Run 内的 Provider / 工具规划循环,用于委派、回执、返工和验收,不是用户发起的游戏生成轮次。game-chat 的进度卡、当前工作和“最新状态”事件统一显示“本轮”,整个页面不向用户显示“第 N 轮”;完整 GUI / CLI pre-publish 任务图仍可显示 `x/14`,game-chat 只显示单主及必要美术 child,不显示固定七项分母。父 Run 终态后移除运行中进度卡,只保留终态阶段记录与预览。
|
||||
- 平台美术资源:game-chat 不再执行固定视觉 DAG。现有正式资源覆盖完整时,单主 `code-prototype` 直接复用;只有它的 `asset.list` 审计证实真实缺口时,才临时委派相应美术 owner。`art-director` 通过平台 `images/generations(kind=spec)` 生成并登记 `assets/art-spec.png`,`art-asset-plan` 再以该规范图的稳定 resourceId 调用 `icon-spritesheets/generations` 生成真实透明 `assets/art-spritesheet.png`,并把响应中的 `iconImageSrcs` 下载为本地独立切片,写入 `assets/art-spritesheet-slices/manifest.json`;两者只可写 `assets/**`。`code-prototype` 必须认领复用或补齐后的结果,并在活动 Canvas 中分别绘制玩家、方块/目标、障碍/场景与反馈四类切片;规范图只作 reference,纯代码核心画面、猜测图集等分网格、整图 `<img>` / CSS 背景、完整图集直绘或只出现路径均不得完成。
|
||||
- 验证:前端运行时模型定向测试、Rust completion/source/asset 合同测试、`cargo fmt --check`、`npm run check:encoding` 与 `git diff --check` 必须全部执行;Windows 文件锁竞态只可作为既有测试失败单独记录,不得将其改写为本次改动的通过证据。
|
||||
|
||||
## 2026-08-01 game-chat 首版七任务素材完整快车道与美术硬门
|
||||
## 2026-08-07 game-chat 单主素材审计、按需美术委派与自验收
|
||||
|
||||
- 首版任务边界:game-chat 首版只展示 `design-director`、`art-director`、`art-asset-plan`、`code-director`、`code-prototype`、`preview-readiness`、`preview-playtest` 七项任务,进度统一显示为 `x/7`;当前根 Run 在 Supervisor 持久路由前零 child,`audit-existing-first` 决策后的首波只激活 `design-director + code-director`。code-director 审计后,完整资源把两个美术节点投影为 completed;真实缺口或整体重做才按依赖开放对应美术 owner,随后 `code-prototype` 与验证节点串行推进。不把完整 GUI / CLI 任务图的其它节点投影到该页面,也不显示内部 Provider / child loop 轮次。
|
||||
- 单主任务边界:game-chat 的根 Run 在 Supervisor 持久路由前零 child;决策后只启动 `code-prototype`。它先 `asset.list` 审计,再视真实缺口临时委派一个受限美术 child;现有资源完整时零美术委派,用户明确整体重做也不能绕过审计或把生成变成固定规则。美术 child 只写 `assets/**`,回执由主 Agent 认领并恢复同一 Run;主 Agent 随后接入素材、执行静态检查和 desktop/mobile 试玩。game-chat 不投影或调度 `design-director`、`code-director`、`preview-readiness`、`preview-playtest` 等固定节点,也不显示内部 Provider / child loop 轮次。完整 GUI / CLI 的 16 节点 DAG 不受影响。
|
||||
- 时间预算:从 game-chat 父 Run 接受用户请求开始,素材完整首版使用 `4200` 秒软预算;父 Run 与其全部 child Run、等待和回收阶段共享从 root `bound_at` 计算的 `4500` 秒绝对硬上限。该值来自现有两次串行生成各自最长 35 分钟的客户端等待合同,并为代码兜底、静态检查和双视口试玩保留 5 分钟。整个 Runtime pass 受同一 `timeout_at` 约束;硬上限内未通过完成门必须失败关闭,不得为了守时跳过图集、换普通生图或回退纯代码核心画面。即使完成证据恰好在上限后到齐,单轮确定性收束也必须再次检查累计预算并拒绝写入 `single_round_converged`。
|
||||
- Provider 次数:Supervisor 先用一个 Provider turn 理解目标并持久化条件路由;`audit-existing-first` 的首波仅请求 `design-director / code-director`,code-director 成功 `asset.list` 并提交覆盖合同后,Runtime 才确定性派发真实缺口对应的 `art-director / art-asset-plan`。完整复用不得调用图片生成接口;图集复用或登记完成后 `code-prototype` 最多执行一次 Provider 首版写入请求。软预算耗尽时只允许使用已登记图集和当前 resourceId 切片清单的确定性本地兜底,Provider 成功返回后由 Runtime 依次执行确定性的 `game.static_smoke` 与 `preview.validate`。
|
||||
- Provider 次数:Supervisor 先用一个 Provider turn 理解目标并持久化条件路由;随后唯一 `code-prototype` 主 Agent 先 `asset.list`,只有审计的精确缺口才建立相应的受限美术委派。完整复用不得调用图片生成接口;图集复用或主 Agent 认领生成回执后,仍由该主 Agent 接入、执行 `game.static_smoke` 和 desktop/mobile `preview.validate`。软预算耗尽时只允许使用已登记图集和当前 resourceId 切片清单的确定性本地兜底,不得由 Runtime 或关键词强制生成图片。
|
||||
- 可玩兜底:软预算或首版 Provider 无法及时完成时,只能为已显式实现真实语义的玩法生成完整、自包含、无远程运行依赖的中文 HTML 模板;未知玩法失败关闭,不能只替换标题后套用固定收集游戏。俄罗斯方块模板必须包含 10×20 棋盘、下落、移动、旋转、锁定、消行和触顶失败;收集模板只匹配明确收集类目标。模板必须从 `ready` 开始,包含真实 Canvas 绘制、`requestAnimationFrame`、键盘 / 触控主要操作、唯一可见且启用的 start / primary-action / restart 控件,状态 JSON 只随真实输入、状态迁移或模拟状态变化推进,并能在 primary-action 后保持 `playing`、在 restart 后稳定回到 `ready | playing`;不得在开始前固定进入 `lost`,不得通过固定失败冒充试玩通过,也不得由纯渲染帧空转 `sequence`。兜底只允许写入缺失或精确初始化占位的 `game/index.html`;存在非占位入口时,当前 `code-prototype` 必须读取并实际 patch,取得本人 `mutationRevision` 后再静态检查和试玩,不得反复用只读 smoke 冒充续作。
|
||||
- 平台图片:`art-spec.png` 只用于约束色板、材质、形状与后续派生,不是运行时背景、角色、目标或图集。`art-asset-plan` 必须以其稳定资源 ID 走专用 icon-spritesheet 路由,透明像素、generation route/kind、同画布归属和 reference resource ID 继续由 Runtime 验证。完整透明图集在编辑器合同中即使产生 `sliceWarning` 仍可登记,但 game-chat 不能据此猜测 atlas 坐标;图集 resourceId 缺失或空白时必须在任何本地写入前失败,四个切片均须保留与整图完全一致的 `sourceResourceId`,切片数量也必须在正式图集登记前严格等于四,多或少都失败关闭。四类唯一性按尺寸与解码后的规范 RGBA 像素摘要判断,不能用不同 PNG 压缩或 ancillary chunk 冒充不同素材。主图、四张 canonical 切片和切片清单必须共同提交,并把主图/切片摘要及 Canvas 身份冻结到 `.agent/runtime/art-spritesheet-contract.json` 私有回执;后续主图安装、回执或登记失败时恢复旧合同,避免出现旧图集绑定新切片。进程在固定主图落盘后崩溃时,保留的 generation 账本只允许按远端预期摘要幂等补齐同一组合同,路径内容冲突继续失败关闭。全部切片下载累计最多 `32 MiB`。完成门重新读取本地素材时仍执行相同文件大小、解码内存、内容摘要、规范像素摘要、可见 alpha 与四类唯一性校验,并要求公开切片清单、当前 Canvas 登记与私有回执完全一致,后续代码 Agent 不能通过同时改写素材和公开清单重定义合同。`postprocess-failed-source-preserved`、无真实 alpha、缺文件或缺登记同样失败关闭。首版只在 `preview-playtest` 后单轮收束,不进入发布节点。
|
||||
- 关联验收:快车道必须分别验证 Supervisor 决策前零 child、审计首波只有 design/code、完整覆盖零图片生成、精确缺口只启动对应 owner、显式重做两个正式 owner、`art-asset-plan` 在代码前完成、`x/7` 投影、实际启动的专业 Agent 安全 final-reply、跳过节点正确终态投影、4200 / 4500 秒累计预算、规范图到 icon-spritesheet 的真实引用、`iconImageSrcs` 本地持久化与资源 ID 绑定、失败续跑目标继承、非占位入口禁止整文件覆盖、纯代码核心画面、猜测单个 atlas 裁切与整图展示失败、四类独立切片可见使用通过、action-driven `sequence`,以及当前 revision 的静态 smoke 与浏览器试玩。
|
||||
- 平台图片:`art-spec.png` 只用于约束色板、材质、形状与后续派生,不是运行时背景、角色、目标或图集。`art-asset-plan` 必须以其稳定资源 ID 走专用 icon-spritesheet 路由,透明像素、generation route/kind、同画布归属和 reference resource ID 继续由 Runtime 验证。完整透明图集在编辑器合同中即使产生 `sliceWarning` 仍可登记,但 game-chat 不能据此猜测 atlas 坐标;图集 resourceId 缺失或空白时必须在任何本地写入前失败,四个切片均须保留与整图完全一致的 `sourceResourceId`,切片数量也必须在正式图集登记前严格等于四,多或少都失败关闭。四类唯一性按尺寸与解码后的规范 RGBA 像素摘要判断,不能用不同 PNG 压缩或 ancillary chunk 冒充不同素材。主图、四张 canonical 切片和切片清单必须共同提交,并把主图/切片摘要及 Canvas 身份冻结到 `.agent/runtime/art-spritesheet-contract.json` 私有回执;后续主图安装、回执或登记失败时恢复旧合同,避免出现旧图集绑定新切片。进程在固定主图落盘后崩溃时,保留的 generation 账本只允许按远端预期摘要幂等补齐同一组合同,路径内容冲突继续失败关闭。全部切片下载累计最多 `32 MiB`。完成门重新读取本地素材时仍执行相同文件大小、解码内存、内容摘要、规范像素摘要、可见 alpha 与四类唯一性校验,并要求公开切片清单、当前 Canvas 登记与私有回执完全一致,后续代码 Agent 不能通过同时改写素材和公开清单重定义合同。`postprocess-failed-source-preserved`、无真实 alpha、缺文件或缺登记同样失败关闭。game-chat 首版由 `code-prototype` 在同一主 Run 完成静态 smoke 和试玩后单轮收束,不进入发布节点。
|
||||
- 关联验收:快车道必须分别验证 Supervisor 决策前零 child、持久路由后只启动 `code-prototype`、主 Agent 成功 `asset.list` 后才可判断缺口、完整覆盖零图片生成/零委派、精确缺口只委派对应 owner、整体重做仍先审计且不产生无关委派、美术 child 对 `game/**` 写入拒绝而 `assets/**` 允许、回执恢复同一主 Run、主 Agent 自行完成接入/静态 smoke/desktop-mobile 试玩、4200 / 4500 秒累计预算、规范图到 icon-spritesheet 的真实引用、`iconImageSrcs` 本地持久化与资源 ID 绑定、失败续跑目标继承、非占位入口禁止整文件覆盖、纯代码核心画面、猜测单个 atlas 裁切与整图展示失败、四类独立切片可见使用通过与 action-driven `sequence`。完整 GUI / CLI 的固定 16 节点 DAG 另行保持原有回归。
|
||||
|
||||
## 2026-08-03 game-chat 开发态同源与持久输出修复
|
||||
|
||||
- 开发态启动必须在 Tauri CLI 之前预检固定 `3080`。现有 marker 只包含 API target,不能证明监听器属于当前 worktree;因此只有端口空闲时才允许继续,任何已存在的 AGC Vite、非 HTTP 监听器或其它服务都必须在原生窗口创建前失败关闭。启动器不擅自终止无法证明归属的旧服务,也不得把当前 Rust 壳 / Runner 与其它 worktree 的旧 Vite 前端混用。Tauri CLI 任意退出后,外层启动器必须有界收束已启动的客户端进程树,避免 `beforeDevCommand` 失败后留下假在线窗口。
|
||||
- game-chat root binding 的 `source` 必须精确为 `project-supervisor-game-chat`。只有该持久 source 才能进入 Supervisor 决策前零 child 的条件 lane;`audit-existing-first` 路由后先运行 `design-director + code-director`,再按覆盖合同选择复用或补齐美术,最后推进 `code-prototype → preview-readiness → preview-playtest`、单轮确定性收束和自动预览。若绑定为 `project-supervisor-gui`,必须视为启动链路错误,不能用完整 16 节点 DAG 的运行状态伪装 game-chat 进度。
|
||||
- source-aware lane 的 ready child 可能在 UI hydration 写回时短暂恢复为 `Pending`。该例外必须从当前 root source、持久工作流决策和资产路由解析本轮已开放节点,不得硬编码旧三 Director 首波;当前审计首波是 `design-director / code-director`,后续美术、code prototype 与 preview child 仍严格拒绝未授权或不满足依赖的 `Pending` 收束。
|
||||
- game-chat root binding 的 `source` 必须精确为 `project-supervisor-game-chat`。只有该持久 source 才能进入 Supervisor 决策前零 child 的单主 lane;Supervisor 持久 intent 后先运行 `code-prototype`,由它按权威审计结果选择复用或暂时委派受限美术,再在同一主 Run 完成接入、静态 smoke、desktop/mobile 试玩、单轮确定性收束和自动预览。若绑定为 `project-supervisor-gui`,必须视为启动链路错误,不能用完整 16 节点 DAG 的运行状态伪装 game-chat 进度。
|
||||
- source-aware lane 的主 Run 或其经授权美术 child 可能在 UI hydration 写回时短暂恢复为 `Pending`。该例外必须从当前 root source、持久工作流决策、单主 route 与 child delegation 解析本轮已开放工作,不得从旧七节点图硬编码重启 Director、验证或试玩节点;未授权 child、第二个活跃美术 child,或缺少成功 `asset.list` 审计的美术委派仍严格失败关闭。
|
||||
- 专业 Agent 的非流式 final reply 继续由既有 finalization journal 重建并提交 `responseStream`。`streaming / ready` 投影仍必须匹配当前项目 revision;已经 finalization 提交的 `committed` 回复以 Agent / Session / run / request slot / response revision 稳定身份为准,不得因后续阶段推进项目 revision 而从 game-chat 查询中消失。
|
||||
|
||||
## Runtime 边界
|
||||
@@ -258,7 +258,7 @@ Agent Runtime 负责:
|
||||
- v4 在压缩状态校验通过后迁移为当前格式;v3 在原身份、任务、project revision、verification gate 和结构化计划校验通过后,从当前 Runtime/Goal sidecar 补齐 Goal 快照并继续;v2 先按 V1.17 规则补齐结构化计划,再补 Goal;后续 checkpoint 统一写 v5。v1、缺失既有 gate 关联或无法证明 Goal 快照的记录不自动迁移。Provider pause 中断/返回边界先持久化 continuation;该恢复快照把 `goalStatus` 设为恢复后的 `active`,避免 resume 后用 paused 上下文自相矛盾。
|
||||
- stale continuation 必须清空旧 actions 与 fallback response,保留 blocker、loop 位置、窗口进度和结构化计划;`contextStalled` 一旦成立,同 run 重规划和重启不得清除。动态 revision 数字、时间戳与验证命令输出不构成独立进展;重复 stale 最迟在相邻窗口指纹重复时以 `loop-budget-exhausted` 终止。单文件最多 128 KiB、最多 12 条 observation;写入前统一限长并过滤敏感内容和项目绝对路径。revision 与验证资格仍以锁内独立文件为准,bundle 只是 Runtime 私有恢复上下文,不等同于根级 `.agent/context.bundle.json`,不得由通用文件工具暴露。
|
||||
- 2026-07-11 调整:后台任务的可执行正文上限统一为 4,000 字符。入队 JSONL、启动后的 `currentTask/currentGoal`、planning prompt、待确认动作 task context、确认续跑和重启恢复都保留同一份正文;对话仍保存用户原始消息。状态事件、列表卡片和 `agent.db` 摘要可继续使用较短安全预览,但不能再反向作为后续 LLM 执行输入。这样长任务末尾的验收标记和输出格式要求不会在队列边界被 180 字符截断。
|
||||
- 2026-07-11 调整,2026-07-12 由 Runtime V1.2 更新:后台 planning 使用 4,000 输出 token,最终回复使用 2,400,并继续叠加最多 3 次 EmptyResponse 重试。推理档位不再硬编码为 `low`:planning、普通单 Agent 聊天和最终回复统一使用解析后的 `llm.reasoningEffort`,`agentLlm.<agentId>.reasoningEffort` 有值时覆盖全局、缺省时继承全局;取值只允许 `default / low / medium / high`,发布默认 `high`,`default` 表示不向 Provider 发送推理档位。
|
||||
- 2026-07-11 调整,2026-07-12 由 Runtime V1.2 更新,2026-08-06 仅澄清 token 口径:后台 planning 使用 4,000 生成 token 预算,最终回复使用 2,400;预算包含可见输出与 Provider 可能使用的隐藏 reasoning token,不等于可见正文长度。既有配置与持久协议键 `maxOutputTokens` 保持冻结,Provider adapter 再按协议映射为 Chat endpoint capability 选定的 `max_completion_tokens` 或 legacy `max_tokens`、Responses `max_output_tokens`、Anthropic `max_tokens`;AGC 通用自定义网关当前保持 legacy 默认。本条原有“最多 3 次 EmptyResponse 重试”已由本文后续 2026-07-15 的 V1.18 收口条目取代。推理档位不再硬编码为 `low`:planning、普通单 Agent 聊天和最终回复统一使用解析后的 `llm.reasoningEffort`,`agentLlm.<agentId>.reasoningEffort` 有值时覆盖全局、缺省时继承全局;取值只允许 `default / low / medium / high`,发布默认 `high`,`default` 表示不向 Provider 发送推理档位。
|
||||
- 2026-07-11 补充,2026-07-15 由 V1.17 更新:后台单 Agent 的工具 planning 响应必须提供可反序列化为 `thinkingSummary / planUpdate / plan / actions / response` schema 的 JSON object。Runtime 从模型输出中解析首个完整对象,因此对象后的尾随说明可以忽略;只有普通文本、没有完整对象,或对象无法反序列化时都不构成有效工具计划。对于这两类无效输出,Runtime 最多追加 2 次自动格式修复请求;同一次 planning 的私有 repair 请求可携带限长且经过统一敏感信息过滤的上一条模型输出或 function call 预览与协议错误,以便 Provider 真正修正格式。`.agent/agent.db` 的 `agent.runtime.tool_plan.repair` 公共审计只写 attempt/maxAttempts、protocol,以及错误、输出/调用体预览、callId 和 functionName 的 SHA-256、字符数或计数,不保存原始模型正文、错误或 function arguments。修复预算耗尽后进入既有工具规划失败路径,不得把普通文本折算为空 actions + response,也不得因此进入 completed;最终回复阶段仍按其独立的普通文本契约处理。旧文本协议可省略 `planUpdate`,但只能继续走 legacy `plan` fallback。
|
||||
- 2026-07-12 补充,2026-07-15 由 V1.17 更新,2026-07-27 由「Anthropic 与流式统一使用 Provider 原生工具」更新,2026-08-03 收紧 strict 边界:OpenAI Chat / Responses 的后台工具 planning 优先注册唯一的 `submit_agent_tool_plan` function tool,并使用字符串形式 `tool_choice=required` 和 strict schema;Runtime 只接受恰好一次同名 function call,并把 arguments 复用现有 `AgentRuntimeToolPlan` 校验与两次格式修复循环。strict arguments 中 `planUpdate` 必须出现但可为 `null`,使用结构化更新时 legacy `plan` 必须为空。错误函数名、多次调用和非法 arguments 都不得执行工具。Anthropic 自 2026-07-27 起与另外两种协议一致发送原生工具目录:请求体顶层携带 `tools`(schema 字段名为 `input_schema`)。`strict` 能力不从 `apiKind` 推断:AGC 只对无凭据 / 自定义端口 / 路径的官方 HTTPS endpoint 和 Claude 4.5+ 版本化 model id 显式开启,旧模型、未知别名和兼容网关默认关闭。开启后使用官方支持关键词白名单生成 Anthropic 专用传输 schema,已知不支持约束只从传输副本剔除,调用方原 schema 保持不变;未知关键词、不可解析 / 递归 `$ref` 和 strict 工具 / optional / union 请求级复杂度超限时该工具保持 non-strict,不能因完整 AGC 工具集超限让整次请求被上游拒绝。工具数组最后一项携带 `cache_control: {"type":"ephemeral"}` 作为 prompt cache breakpoint;非流式和流式 usage 都将 `input_tokens + cache_creation_input_tokens + cache_read_input_tokens` 合并为 prompt tokens。`tool_choice` 使用对象形态(`Auto → {"type":"auto"}`、`Required → {"type":"any"}`,裸字符串会被上游拒绝),响应解析 `tool_use` block 并把 `input` 序列化为 `arguments`。planning 不再因协议强制非流式,最终普通回复继续按 Agent 配置决定是否流式。`platform-llm` 仍在本地拒绝无 function tools 的 tool choice,但不再拒绝 Anthropic function tools;协议类型继续写入 `agent.runtime.tool_plan.protocol` 审计,Anthropic 正常路径的取值为 `native_runtime_tools` 而不是 `text_json`。
|
||||
- 2026-07-11 调整,2026-07-15 由 V1.17 更新:工具计划五个顶层字段均为必填并拒绝未知顶层字段;`thinkingSummary`、结构化计划的 `explanation / step` 与 `action.tool` 必须非空。`planUpdate` 只接受 `null` 或最多 8 个唯一步骤,状态限于 `pending / in_progress / completed` 且至多一个 `in_progress`。这样 `{}`、前置无关 JSON 或结构不完整对象会触发格式修复,不会成为假完成信号。空 actions 只有在 verification、process/join/delivery 和结构化计划完成门禁都通过后才表示 planning 收束;response 非空时直接采用,response 为空时进入独立最终回复生成。`agent.runtime.project.verify` 记录补充 `runId / actionId / actionFingerprint`,用于在多 Agent 并行验证时把命令终态与具体 Runtime 动作关联。
|
||||
@@ -895,7 +895,7 @@ game-project/
|
||||
|
||||
## 2026-07-31 长耗时与恢复收口
|
||||
|
||||
- `autonomous-game-build` 的固定 manifest DAG 是唯一缺省首轮专业执行链。缺省 collaboration policy 不再额外强制 `code-prototype / quality-review / art-*` 静态首波,Runtime 也不再按 Editor Key 或已有图片偷偷追加 Agent。2026-08-03 起,显式项目 policy 或旧 batch 恢复若进入首批 `agent.delegate` 兜底,同样只能激活 `design-director / art-director / code-director`,不得提前激活底层 Agent;这条兜底不能在默认 manifest 前复制同职责委派。
|
||||
- 除持久 `project-supervisor-game-chat` 单主 route 外,`autonomous-game-build` 的固定 manifest DAG 是唯一缺省首轮专业执行链。缺省 collaboration policy 不再额外强制 `code-prototype / quality-review / art-*` 静态首波,Runtime 也不再按 Editor Key 或已有图片偷偷追加 Agent。2026-08-03 起,显式项目 policy 或旧 batch 恢复若进入首批 `agent.delegate` 兜底,同样只能激活 `design-director / art-director / code-director`,不得提前激活底层 Agent;这条兜底不能在默认 manifest 前复制同职责委派。game-chat 例外只允许持久路由后的 `code-prototype`,以及该主 Run 经审计后临时建立的受限美术 delivery。
|
||||
- `preview-readiness` 只有在自己的 child run 持有当前 project revision 的 `game.static_smoke=passed` 凭证后才能完成;`preview-playtest` 作为根 Supervisor 的直接 manifest child,必须解析并写入根完成合同的当前 revision browser receipt,报告、desktop/mobile 截图及摘要复核通过后才能投影 manifest completed。
|
||||
- 浏览器未发现、临时环境不可建、启动超时或在 WebSocket URL 解析前退出统一分类为 `preview-infrastructure-unavailable`。首个持久 observation 后收束当前 action batch并失败结束 child/root run,禁止继续用 Provider 逐轮规划同一 revision 的重复启动;普通页面/玩法验收失败仍保留为业务失败,不混入基础设施分类。
|
||||
- game-chat release 在 `CloseRequested / ExitRequested` 前复用 Runner durable idle probe;只要存在 process session、pending/finalization/provider/tool-plan handoff 或非终态 Agent queue/phase,就阻止关闭并提示先完成、暂停或取消。不可撤销的最终 `Exit` 不再作为唯一保护点,Windows Job Object 的 child-owned 安全边界保持不变。
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# 图片画布游戏场景生成链路
|
||||
|
||||
更新时间:`2026-08-04`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在现役图片画布中补齐单张、静态、非分层游戏环境背景的专用生成链路:
|
||||
|
||||
```text
|
||||
首页游戏场景 / 画布底部游戏场景
|
||||
-> 场景专用生成表单
|
||||
-> 后端确定性组装场景 Prompt
|
||||
-> 现有 editor_image_generation 队列与 Worker
|
||||
-> 现有计费、失败、资源持久化和 canvasCompletion
|
||||
```
|
||||
|
||||
本期不是 Agent 工作流,不新增场景 Agent、场景 Worker、场景任务表或场景专属计费系统。
|
||||
|
||||
## 2. 输入和默认值
|
||||
|
||||
用户输入:
|
||||
|
||||
- 必填画面内容。
|
||||
- 视觉风格:日系动画、清透水彩、平面几何、定格模型、自定义。
|
||||
- 自定义风格下必填自定义画风。
|
||||
- 可选用户参考图,沿用普通图片生成的参考图处理能力。
|
||||
- 图片比例、清晰度和图片模型。
|
||||
|
||||
默认值:
|
||||
|
||||
- 图片比例:`16:9`。
|
||||
- 清晰度:`1K`。
|
||||
- 模型:`gemini-3.1-flash-image-preview`,产品展示名沿用现有画布。
|
||||
- 视觉风格:日系动画。
|
||||
- 参考图:无。
|
||||
|
||||
`model`、`aspectRatio`、`imageSize` 继续使用现有编辑器的字符串契约、模型选项、尺寸选项和后端标准化函数,不维护场景专属枚举列表。
|
||||
|
||||
## 3. 前端边界
|
||||
|
||||
前端只保存和提交结构化场景意图,不保存或拼接完整场景 Prompt。
|
||||
|
||||
正式场景状态使用 `GenerateDialogState.mode = "scene"`,不沿用前端壳中的 `generatorVariant = "game-scene"` Demo 标识。
|
||||
|
||||
交接壳复用范围:
|
||||
|
||||
- 画风选择控件、交互和局部样式。
|
||||
- 自定义画风条件输入框。
|
||||
- 底部游戏场景按钮。
|
||||
- 四张固定画风预览图。
|
||||
|
||||
不复用:
|
||||
|
||||
- Demo `App.tsx`。
|
||||
- `setTimeout` 模拟生成。
|
||||
- FileReader Data URL 正式请求。
|
||||
- 复制出的整套画布和缩减版类型。
|
||||
|
||||
四张固定图片只用于前端预览,不进入 `generationReferences`,不随生成请求提交。保留原分辨率,只做轻量无损压缩;画风菜单打开时不同时挂载全部预览图,只在用户悬停、键盘聚焦或点击对应预览入口时按需挂载当前图片。点击同一入口可关闭预览,保证无 hover 的触屏设备可用。
|
||||
|
||||
## 4. API 契约
|
||||
|
||||
主站新增:
|
||||
|
||||
```text
|
||||
POST /api/editor/scenes/generations
|
||||
```
|
||||
|
||||
请求字段:
|
||||
|
||||
```text
|
||||
sceneContent
|
||||
stylePreset
|
||||
customStyle?
|
||||
model?
|
||||
aspectRatio?
|
||||
imageSize?
|
||||
referenceImageSrcs?
|
||||
projectId?
|
||||
generationInputs?
|
||||
assetFolderId?
|
||||
assetLabel?
|
||||
canvasCompletion?
|
||||
```
|
||||
|
||||
请求不接受前端组装后的完整 `prompt`。通用 `/api/editor/images/generations` 与 `/api/external/v1/editor/images/generations` 共用同一边界校验,均拒绝 `kind = scene` 或 `assetKind = scene`,防止调用方绕过结构化字段校验和后端 Prompt 组装。External v1 当前没有场景专用路由,因此不能通过通用图片接口提交结构化场景;主站 `/api/editor/scenes/generations` 构造规范请求后直接复用队列,不经过通用入口校验。
|
||||
|
||||
完整 Provider Prompt 仍只能由后端生成。
|
||||
|
||||
场景参考图沿用普通图片生成的客户端前置门禁,最多 5 张;超限时不得发送 HTTP 请求。场景生成 POST 使用生成专用零重试策略,避免 inline 响应丢失后重复调用 Provider。
|
||||
|
||||
后端对模型、比例和清晰度先沿用 `normalize_editor_generation_options` 标准化,再使用标准化比例决定画幅描述和入队价格。
|
||||
|
||||
## 5. Prompt
|
||||
|
||||
场景 Prompt 在 `server-rs/crates/api-server/src/prompt/editor_scene.rs` 中维护,经 `api-server::prompt` 现役模块出口编译,沿用现有后端 Prompt 常量和 builder 模式,不拆分为运行时文本模板文件,不调用 LLM 润色、扩写或改写。旧 `prompt/scene_background.rs` 属于已退役的自定义世界 / RPG 场景链路,不作为本功能复用入口。
|
||||
|
||||
提示词来源为产品提供的 `scene_prompt.md`:
|
||||
|
||||
- 删除文档章节编号和 `[图片]` 占位。
|
||||
- 保留场景结构约束和四套完整预设风格正文。
|
||||
- 自定义模式只使用用户自定义画风,不附加预设风格正文。
|
||||
- `视觉风格:` 标题只由场景结构模板输出,预设正文不重复携带标题。
|
||||
- 先替换受控的比例和画幅方向,再切分静态用户占位符并一次性拼入画面内容与画风正文;用户输入中的模板占位符按原文保留,不参与后续替换。
|
||||
|
||||
画幅方向按标准化比例映射:
|
||||
|
||||
- `16:9`、`4:3`、`3:2`:横幅。
|
||||
- `1:1`:方形。
|
||||
- `9:16`、`2:3`:竖幅。
|
||||
|
||||
## 6. 队列和计费
|
||||
|
||||
场景请求在后端组装完整 Prompt 后,转换成现有 `EditorImageGenerationRequest`:
|
||||
|
||||
```text
|
||||
kind = scene
|
||||
assetKind = scene
|
||||
prompt = 后端完整 Prompt
|
||||
```
|
||||
|
||||
随后复用 `enqueue_editor_image_generation_for_owner`,队列类型继续是 `editor_image_generation`,Worker 继续执行 `generate_editor_image_for_owner`。
|
||||
|
||||
队列标题固定为“图片画布生成游戏场景”。任务摘要只展示 `generationInputs.fields` 中的“画面内容”,不得把队列 payload 里的后端完整 Prompt 暴露到任务侧栏;历史错误摘要在投影刷新时按同一规则重新派生。`assetLabel` 省略、空字符串或纯空白时统一使用“游戏场景”,不能退回完整 Prompt 作为素材名称。
|
||||
|
||||
计费规则:
|
||||
|
||||
- 前端展示价继续读取 `/api/editor/generation-pricing`。
|
||||
- 入队时按标准化后的模型和清晰度由后端计算并固化 `external_generation_job.price_mud_points`。
|
||||
- Worker 按入队价格扣费,不能在场景接口增加第二层扣费。
|
||||
- `scene` 明确按普通图片模型档位计价。
|
||||
- Provider 失败、执行取消和 lease 耗尽沿用现有扣退费语义。
|
||||
- Provider 成功后的持久化或画布写回失败语义不在本期调整。
|
||||
- inline 响应中的通用生成告警必须在队列终态处理前转发;有项目画布占位但响应缺少权威 `project` 快照时,不得追加本地图层或把占位标记为已完成。
|
||||
- 队列任务进入 `completed` 后,首次项目快照仍可能短暂保留同一 `dialogId` 的 `generating` 占位。场景提交必须把 completion 的 `dialogId` 传给统一项目回读逻辑;首个权威快照仍未收口时做一次有界延迟重读,不能把该快照当作最终状态永久套用。
|
||||
|
||||
## 7. 数据落点
|
||||
|
||||
不修改 SpacetimeDB schema,继续复用:
|
||||
|
||||
- `external_generation_job`。
|
||||
- `editor_canvas_generation_dialog`。
|
||||
- `editor_project_resource`。
|
||||
- `editor_asset`。
|
||||
- `editor_canvas_layer`。
|
||||
- `asset_object` / `asset_entity_binding`。
|
||||
- 现有钱包流水和 `asset_operation_wallet_settlement`。
|
||||
|
||||
字段语义:
|
||||
|
||||
- `prompt`:后端最终提交给图片 Provider 的完整场景 Prompt。
|
||||
- `actual_prompt`:Provider 返回的 actual/revised Prompt。
|
||||
- `asset_kind`:`scene`。
|
||||
- `generation_inputs_json`:画面内容、视觉风格、自定义画风和用户参考图引用。
|
||||
|
||||
## 8. 验收
|
||||
|
||||
- 首页和底部按钮进入同一个场景生成器。
|
||||
- 默认值为 `16:9 / 1K / Banana / 日系动画 / 无参考图`。
|
||||
- 预设顺序和自定义条件输入正确。
|
||||
- 预览图不进入请求参考图。
|
||||
- 前端请求中不存在完整场景 Prompt。
|
||||
- 后端正确组装四种预设、一个自定义风格和三类画幅。
|
||||
- 展示价、入队价、实际扣费和资源成本一致。
|
||||
- 任务复用现有等待、失败、轮询、资源入库和画布添加逻辑。
|
||||
- 场景产物以 `assetKind = scene` 持久化,画布图层右上角显示“场景”标签,不降级为“未知”。
|
||||
File diff suppressed because one or more lines are too long
@@ -17,7 +17,7 @@ v1 只开放以下能力:
|
||||
- `POST /api/external/v1/assets/direct-upload-tickets`:创建素材直传 OSS 凭证。
|
||||
- `POST /api/external/v1/assets/objects/confirm`:确认已上传素材对象,`ownerUserId` 固定为 API Key 所属账号。
|
||||
- `GET /api/external/v1/assets/read-url`:获取私有素材读取签名 URL。
|
||||
- `GET /api/external/v1/editor/projects`:列出当前 API Key 所属账号的图片画布项目。
|
||||
- `GET /api/external/v1/editor/projects`:列出当前 API Key 所属账号的图片画布项目;`view=full|summary`,REST 默认 `full`,MCP 固定使用 `summary`。
|
||||
- `POST /api/external/v1/editor/projects`:创建图片画布项目。
|
||||
- `GET /api/external/v1/editor/projects/recent`:读取当前账号最近图片画布项目。
|
||||
- `GET /api/external/v1/editor/projects/{projectId}`:读取项目与默认画布。
|
||||
@@ -32,7 +32,7 @@ v1 只开放以下能力:
|
||||
- `POST /api/external/v1/editor/assets`:创建素材记录。
|
||||
- `PATCH /api/external/v1/editor/assets/{assetId}`:更新素材名称或所在文件夹。
|
||||
- `DELETE /api/external/v1/editor/assets/{assetId}`:删除素材记录。
|
||||
- `POST /api/external/v1/editor/images/generations`:异步提交编辑器图片素材生成;通过 `kind` 支持普通图、规范图 `spec`、角色图 `character`、快速编辑参考图 `quick-edit`、UI 设计图 `ui-design` 和宣发素材 `publication-material`。
|
||||
- `POST /api/external/v1/editor/images/generations`:异步提交编辑器图片素材生成;通过 `kind` 支持普通图、规范图 `spec`、角色图 `character`、快速编辑参考图 `quick-edit`、UI 设计图 `ui-design` 和宣发素材 `publication-material`。External v1 当前不开放结构化游戏场景生成,`kind = scene` 与 `assetKind = scene` 均在入队前返回 `400`。
|
||||
- `POST /api/external/v1/editor/images/edits`:异步提交已有图片重绘 / 调整。
|
||||
- `POST /api/external/v1/editor/icon-spritesheets/generations`:异步提交规范图驱动的图标 spritesheet 生成和拆分。
|
||||
- `POST /api/external/v1/editor/ui-designs/assets/extractions`:异步提交 UI 设计图素材提取 / 拆分。
|
||||
@@ -56,6 +56,8 @@ v1 只开放以下能力:
|
||||
- `unsupported-image-style`:请求的图片后处理风格未知或不适用于当前生成类型,接口按无风格继续生成。
|
||||
- `multiple-generation-warnings`:同一成功响应合并了不同 `code` 的多条非阻断告警,具体原因按顺序拼接在 `reason`。
|
||||
|
||||
手工调用 `POST /api/external/v1/editor/assets` 或 `POST /api/external/v1/editor/projects/{projectId}/resources` 创建 `assetKind=character-animation` 记录时,`generationInputs` 只保存可重放的生成输入,不接受 `characterAnimation`、`frames`、`previewVideoPath`、`frameCount`、`fps`、`durationSeconds` 等旧运行字段;完整正式帧序列和总时长必须分别写入 `imageSequenceFrames` 与 `imageSequenceDurationMs`。`screenColorHex`、`mattingProvider`、`mattingModel` 属于内部处理审计字段,External v1 会在持久化前移除。角色动作生成接口已经直接返回正式 resource / asset,正常调用方不应再手工复制第一帧创建重复记录。
|
||||
|
||||
provider 原图已保存但透明背景处理最终失败时,worker 保留原图稳定引用,不返回不存在的透明处理图,图标和 UI 也不继续拆分;有 `projectId + canvasCompletion` 时由原图完成画布写回。调用方应展示告警,但不得把 completed 任务改判为失败。该降级只覆盖透明背景处理的最终失败,phase 上报、原图或透明处理图持久化、画布写回失败仍使任务失败。图标 / UI 已成功生成透明图、只有自动拆分失败时继续使用既有 `sliceWarning`。通用 `warning` 与 `sliceWarning` 只在「透明背景最终失败」这一条上互斥;风格归一化或像素规整产生的通用 `warning` 可以与 `sliceWarning` 并存,compact result 不得丢弃任一条。
|
||||
|
||||
## 异步提交、查询与幂等
|
||||
@@ -81,6 +83,8 @@ MCP transport 的 DNS rebinding 防护必须同时允许正式入口 `www.genarr
|
||||
|
||||
MCP tools 从同一份 OpenAPI operation 自动形成 snake_case 名称,并在进程内复用 External REST router,因此鉴权、scope、owner、入参、幂等、计费和结果查询契约只有一份。生成 tools 把 `idempotencyKey` 显式放进参数,因为 MCP transport 的 Authorization 头不能代替逐次业务幂等键。工具结果使用 `structuredContent`;业务失败使用 `isError=true` 的结构化安全错误,协议不可路由时才返回 JSON-RPC error。
|
||||
|
||||
`list_editor_projects` 是项目选择工具,服务端固定以 `view=summary` 调用项目列表,不允许因 OpenAPI 的 REST 默认值退回完整视图。摘要逐项目只返回 `projectId`、`title`、`updatedAt` 和可空 `cover`,不携带 `canvas`、`viewport`、`layers`、`resources` 或图片正文;选定目标后再用 `get_editor_project` 读取完整权威状态。`cover` 只包含最新项目封面快照的 `resourceId`、稳定 `objectKey`、尺寸与 `updatedAt`,没有封面时为 `null`。需要展示封面时,以 `objectKey` 调用 `/api/external/v1/assets/read-url` 获取短期签名 URL;列表不得内嵌 Data URL、图片二进制或临时签名 URL,也不得把签名 URL 当作持久引用。
|
||||
|
||||
MCP 暴露下列稳定文本资源:
|
||||
|
||||
- `genarrative://external-editor/usage`:关键工作流和异步轮询规则。
|
||||
@@ -210,7 +214,7 @@ SpacetimeDB procedure:
|
||||
|
||||
外部生成接口复用站内编辑器已有 DTO、入队器和 worker executor,不维护第二套生成语义:
|
||||
|
||||
- 图片生成 / 重绘 / 规范图 / 宣发图 / UI 设计图复用 `/api/editor/images/generations` 与 `/api/editor/images/edits` 的校验、模型归一、计费和持久化规则,但 External handler 固定只入队。
|
||||
- 图片生成 / 重绘 / 规范图 / 宣发图 / UI 设计图复用 `/api/editor/images/generations` 与 `/api/editor/images/edits` 的校验、模型归一、计费和持久化规则,但 External handler 固定只入队。主站和 External 的通用图片入口共用场景专用合同边界校验,禁止用 `kind = scene` 或 `assetKind = scene` 绕过后端场景 Prompt 组装;场景专用 handler 自己构造规范请求,不受该通用入口校验影响。
|
||||
- 图标 spritesheet 和 UI 设计图素材提取复用站内拆分逻辑,生成图集后按连通域切片,并把图集与切片都按请求写入项目资源和素材库。
|
||||
- 角色动画、视频、音效和背景音乐复用站内编辑器生成链路;请求携带 `assetFolderId` 时按站内规则写入素材库,音频类外部调用使用 API Key 所属账号作为 asset owner。
|
||||
- API Key 管理接口仍只属于登录态个人中心,不进入外部 OpenAPI JSON。
|
||||
@@ -250,11 +254,14 @@ docs/openapi/genarrative-external-v1.openapi.json
|
||||
- API Key 创建只返回一次明文,列表不返回明文。
|
||||
- 撤销后的 API Key 调用外部接口返回 `401`。
|
||||
- 八类外部生成 POST 缺少或携带非法 `Idempotency-Key` 时返回 `400`;同一 owner、请求和 key 重试只得到同一 operation。
|
||||
- External 通用图片生成携带 `kind = scene` 或 `assetKind = scene` 时均返回 `400`,且不得产生入队尝试。
|
||||
- 八类外部生成 POST 固定返回 `202`,查询能从 `queued/running` 收敛到 `completed/failed`;调用方超时后使用原 operationId 继续查询。
|
||||
- 外部图片生成、重绘、图标拆分、UI 素材拆分、角色动画、视频、音效和音乐 completed 后,生成结果按请求同时出现在画布资源和账号级素材库。
|
||||
- 角色图、图标 spritesheet 和 UI 素材提取的 completed result 允许携带 `EditorGenerationWarning`;provider 原图保留降级与自动拆分降级必须保持成功状态,并分别使用通用 `warning` 与兼容 `sliceWarning` 表达。
|
||||
- 外部视频、角色动画、音效和音乐接口使用站内编辑器相同的请求校验、模型限制和价格校验。
|
||||
- OpenAPI JSON 能被 `serde_json` 解析,且 security scheme 为 Bearer API Key。
|
||||
- 项目列表 REST 默认 `view=full` 并保持完整响应兼容;`view=summary` 只返回项目选择元数据和可空封面稳定引用,MCP `list_editor_projects` 固定使用该摘要视图,不因完整项目数据量增长触发返回体上限。
|
||||
- 摘要封面不内嵌图片或签名 URL;使用 `cover.objectKey` 调 `/assets/read-url` 后才能临时展示。
|
||||
- OpenAPI JSON 不包含 `/api/profile/api-keys`、`UserAccessToken` 或 API Key 管理 schema。
|
||||
- `agent-integration.json` 能发现 MCP、OpenAPI、Skill entry/archive;下载 archive 的 SHA-256 与 manifest 一致,ZIP 包含 `SKILL.md`、四篇 references、Python helper 和 `agents/openai.yaml` 七个声明文件且不含凭据。
|
||||
- MCP 在无 Bearer、Bearer 格式错误或 Key 无效时返回相同的 `401 + WWW-Authenticate + details.guide` 鉴权引导,且不暴露 tools/resources/owner;合法 Key 可完成 initialize、tools/list、resources/list/read 和生成提交/查询;resource catalog 必须包含 usage、OpenAPI、`skill` 主入口和当前全部 Skill references,当前精确为 `skill/references/capability-routing.md`、`skill/references/api-operations.md`、`skill/references/authentication-and-safety.md` 与 `skill/references/requests-and-outputs.md`,且不包含 CLI 脚本、测试或 workflow;多实例不依赖 sticky session,不暴露内部 SpacetimeDB MCP 或 worker 控制面。
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# BGM 生成提示词优化 T6 测试与发布门禁实施记录
|
||||
|
||||
日期:`2026-08-05`
|
||||
|
||||
状态:`实施完成并已提交、推送;本文保留实施时门禁记录`
|
||||
|
||||
## 文档定位
|
||||
|
||||
本文承接 BGM 生成提示词优化 V1.0 的 T6 实施边界、实际测试覆盖和发布门禁记录。产品与技术规则以[画板音乐生成入口设计](./【编辑器】画板音乐生成入口设计-2026-06-18.md)为准;长期稳定边界同步记录在 `docs/project-memory/shared-memory/decision-log.md` 和 `pitfalls.md`。
|
||||
|
||||
本文不重新解释原始需求,不把测试便利转化为产品规则,也不作为生产部署授权。
|
||||
|
||||
## 实施基线与边界
|
||||
|
||||
- T0–T5 已完成并提交;T5 提交为 `3b69b4886`。
|
||||
- 用户已于 2026-08-05 完成人工全链路浏览器测试,并确认未发现阻断性问题。
|
||||
- 当前分支通过 merge commit `69426ee61` 包含 `origin/master@0cc257ce6`;实施结束时相对该 ref 为 `0 behind / 12 ahead`。
|
||||
- T6 只补测试、共用 fixture、文档和发布门禁。唯一生产代码调整是把既有 inline BGM 响应字段原样抽为同文件私有构造函数,供生产路径与测试共同调用。
|
||||
|
||||
明确未做:
|
||||
|
||||
- 不修改 BGM / SFX UI、Prompt 业务规则、助手模板、字符或 body 上限、限流、埋点、价格、计费、队列、重试或持久化语义。
|
||||
- 不新增 Idempotency-Key、服务端 dedupe、持久提交锁或跨页面恢复机制。
|
||||
- 不修改 SpacetimeDB schema、migration、bindings 或表目录。
|
||||
- 不修改 External v1 路由、DTO、状态码、异步语义、OpenAPI 或 worker。
|
||||
- 不修改 `platform-llm` 原文日志策略。
|
||||
- 不挂载或清理历史 `vector_engine_audio_generation/tests.rs`。
|
||||
- 不执行生产部署,也不重复创建可能扣费的真实 BGM 任务。
|
||||
|
||||
## 实际实施
|
||||
|
||||
### 共用 canonicalization 向量
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
packages/shared/test-fixtures/background-music-prompt-canonicalization.json
|
||||
```
|
||||
|
||||
共 10 个 case,由 TypeScript 与 Rust 共同消费,覆盖:
|
||||
|
||||
- 完整 Unicode `White_Space` 边界和 U+0085;
|
||||
- 内部 LF、CRLF 与空白保持;
|
||||
- U+200B、U+FEFF 保持;
|
||||
- 组合字符不做 NFC;
|
||||
- ZWJ emoji 按 code point 计数;
|
||||
- 标点、数字、ASCII 与 canonicalization 幂等性。
|
||||
|
||||
代表性 case `representative-complex` 的 canonical `charCount=16`、`effectiveCharCount=13`。同一 case 用于前端正式请求、成功结果 layer、client JSON body、站内 queue serializer、inline 响应和 Suno body 等值断言。
|
||||
|
||||
### 前端与 SFX 回归
|
||||
|
||||
只修改既有测试文件:
|
||||
|
||||
- `ImageCanvasBackgroundMusicPromptModel.test.ts`
|
||||
- `useImageCanvasGenerationSubmissionWorkflow.test.tsx`
|
||||
- `ImageCanvasGenerationSubmissionModel.test.ts`
|
||||
- `editorProjectClient.test.ts`
|
||||
|
||||
新增或收紧的证据:
|
||||
|
||||
- Prompt model 对全部共用 fixture 逐项断言 canonical Prompt、总字符数、有效字符数和幂等性。
|
||||
- BGM 正式请求与成功 layer 的 `prompt`、`actualPrompt`、唯一 `gpt_description_prompt` 均等于代表性 canonical Prompt。
|
||||
- 正式 BGM client JSON body 保持 canonical Prompt 原值。
|
||||
- SFX 全空白 Prompt 继续回退“游戏音效”,并保持 `audio1.0` 与默认 5 秒。
|
||||
|
||||
未修改前端生产实现。
|
||||
|
||||
### 后端与平台回归
|
||||
|
||||
修改:
|
||||
|
||||
- `server-rs/crates/platform-audio/tests/vector_engine_audio.rs`
|
||||
- `server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs`
|
||||
|
||||
覆盖:
|
||||
|
||||
- `platform-audio` 消费全部共用 fixture,并保留 BGM 长度、资格和 SFX 回归。
|
||||
- Suno body 继续精确包含 `mv`、`gpt_description_prompt`、`make_instrumental` 三个字段。
|
||||
- 已挂载 generation 模块覆盖 BGM 空字符串、全 Unicode 空白、1 / 200 / 201 code point。
|
||||
- 登录态 queue serializer 与 inline 完成响应保持 canonical Prompt 等值。
|
||||
- 已挂载 SFX 测试覆盖唯一 `audio1.0`、Prompt 规范化、1500 / 1501、2 / 10 / 1 / 11 秒和动态价格。
|
||||
|
||||
生产路径只新增同文件私有 `build_editor_background_music_generate_response` 抽取;字段、DTO、分支和业务语义不变。
|
||||
|
||||
## 验证结果
|
||||
|
||||
|门禁|结果|
|
||||
|---|---|
|
||||
|前端 11 个定向测试文件|`259/259`|
|
||||
|shared-contracts BGM DTO|`1/1`|
|
||||
|`platform-audio`|`29/29`|
|
||||
|`platform-llm` 未完成原因与普通文本降级|两个定向测试均通过|
|
||||
|`api-server background_music`|`36/36`|
|
||||
|已挂载 generation 模块|`6/6`|
|
||||
|`editor_sound_effect`|`2/2`,不再是空过滤器|
|
||||
|External v1|`7/7`|
|
||||
|External BGM|`2/2`|
|
||||
|`cargo check -p api-server --all-targets`|通过|
|
||||
|TypeScript typecheck|通过|
|
||||
|变更文件 ESLint|通过|
|
||||
|Rust 格式|通过|
|
||||
|编码检查|5190 个文件通过|
|
||||
|`git diff --check`|通过|
|
||||
|
||||
Rust 输出只有既有 dead-code warning,没有新增失败。
|
||||
|
||||
## 运行态与人工门禁
|
||||
|
||||
本轮实际验证:
|
||||
|
||||
|服务|地址|门禁|结果|
|
||||
|---|---|---|---|
|
||||
|SpacetimeDB|`http://127.0.0.1:3101`|`GET /v1/ping`|HTTP 200|
|
||||
|BgFilter worker|`http://127.0.0.1:8083`|`GET /readyz`|HTTP 200|
|
||||
|api-server|`http://127.0.0.1:8082`|`GET /healthz`|HTTP 200|
|
||||
|
||||
API 安全响应摘要:
|
||||
|
||||
```json
|
||||
{"ok":true,"service":"genarrative-api-server"}
|
||||
```
|
||||
|
||||
本轮启动的进程树已按归属清理,三个端口均已关闭。
|
||||
|
||||
人工门禁如实记录为:`2026-08-05,用户人工全链路浏览器测试完成,未发现阻断性问题`。T6 没有把该结论扩写为未提供的逐项观察数据,也没有重复创建可能扣费的正式任务。
|
||||
|
||||
## 发布判定与交接
|
||||
|
||||
当前工作树相对已核对的 `origin/master@0cc257ce6` 满足 T6 本地发布门禁:
|
||||
|
||||
- TypeScript 与 Rust 共同消费同一 canonicalization fixture。
|
||||
- 前端、shared-contracts、Prompt 助手、generation、`platform-llm`、`platform-audio`、SFX 和 External v1 定向门禁通过。
|
||||
- 跨层等值证据使用同一代表性 Prompt,没有隐藏字段或二次改写。
|
||||
- API 健康检查与用户人工浏览器门禁通过。
|
||||
- diff 不包含原始需求文件、SpacetimeDB schema、External OpenAPI 或 `platform-llm` 日志策略修改。
|
||||
|
||||
本节记录门禁时,相关改动尚未提交;之后已以 `aeb5894b9` 提交并推送。后续 PR、四个 required jobs 和生产部署是独立动作,不由 T6 自动执行。
|
||||
|
||||
## BGM 助手模型试验
|
||||
|
||||
2026-08-05 起,补全和简化的请求级模型改为 `gpt-5.6-luna`;画布 Agent 其它调用继续使用 `gpt-5.4-mini`。该调整只作用于两个 BGM Prompt 助手路由,不改变共享编辑器 Agent 的默认模型。
|
||||
|
||||
- 本地 API mock 链路 `27/27` 通过,并确认请求 body 的 `model` 为 `gpt-5.6-luna`。
|
||||
- 真实 VectorEngine smoke 已尝试:`GET /v1/models` 与 `POST /v1/chat/completions` 均在建立 HTTP 连接前以 `fetch failed` 结束,没有返回状态码或模型响应;该结果只能说明当前环境网络不可达,不能判定 `gpt-5.6-luna` 被上游拒绝或支持。
|
||||
- 网络恢复后重试成功:补全和简化各发送一条最小 Chat Completions 请求,均返回 HTTP 200、`model=gpt-5.6-luna`、`finish_reason=stop`,并解析出完整四字段 JSON object;补全候选 `126` code points,简化候选 `79` code points。由此确认当前模型和两条助手请求形态可以跑通。
|
||||
|
||||
## 2026-08-06 组件架构后续修订
|
||||
|
||||
本文前述 T6 数字与文件范围是当时实施完成后的历史记录,不因后续组件归并而改写。新的权威方向是让 SFX 与 BGM 恢复使用 `ImageCanvasGenerationComposerView.tsx` 内同一个音频 composer,并由 `isSoundEffect` 分流;详见[音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md)。
|
||||
|
||||
该修订已于 2026-08-06 实施:`ImageCanvasGenerationComposerView.tsx` 恢复唯一共享音频 composer 和 `isSoundEffect` 分流,独立完整 BGM composer 及其测试文件已删除,原用例全部迁入总 composer。Prompt / 预设 / controller / 总 composer `121/121`、surface 与 submission workflow `72/72` 通过,typecheck、变更文件 ESLint、Prettier、编码检查和差异检查通过。现有 SFX 的 Vidu、2–10 秒、默认 5 秒、Prompt 回退、1500 字、价格与提交路径未改;本轮未实现 SFX V2、未修改后端或需求原文,也未执行浏览器视觉测试或创建真实付费任务。
|
||||
@@ -92,6 +92,8 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去
|
||||
|
||||
外部生成任务摘要投影与历史 payload 维护使用 `npm run spacetime:external-generation:maintain -- ...`,且只能由已授权 migration operator 的 SpacetimeDB CLI 登录态执行。脚本默认 dry-run、每次只处理一批,绝不自动循环全表;`--apply` 才写入。先发布包含 `external_generation_job_summary` 与 cursor 索引的 SpacetimeDB 模块,在维护模式内对事故时间以前的编辑器终态任务执行小批 dry-run,例如 `npm run spacetime:external-generation:maintain -- --database <database> --server-url <url> --limit 5 --completed-before-micros <micros>`;核对 `matched_count`、`before_bytes`、`after_bytes` 和 `inline_media_count` 后,保持本批输入 cursor 不变并追加 `--apply` 重跑同一批,即使最后一批 `has_more = false`,只要 dry-run 仍有 `matched_count` / `selected_count` 也必须 apply;只有 apply 成功后才使用它返回的 `next_cursor_job_id` 继续。B-tree cursor 的选择阶段最多反序列化 `limit + 1` 行,apply 会再按主键逐条读取选中行但不会同时保留整批 payload;如怀疑存在单行异常巨型历史 JSON,先用 `--limit 1`。payload 压缩硬限制 `source_module = editor-canvas`;终态压缩完成后,用 `--backfill-summaries` 先 dry-run、再 `--apply` 分批补齐仍缺失的活动任务或无内联媒体历史任务摘要,直到 `has_more = false`,最后再切换使用 summary procedure 的 api-server。Stdb 构建 artifact 和完整 release 包都必须包含 `scripts/spacetime-maintain-external-generation-jobs.mjs` 与 `scripts/spacetime-migration-common.mjs`。首次上线不得让 Full Build 从 Stdb 自动直落 API:`STDB_API_ROLLOUT_MODE` 默认 fail-closed 为 `pause-after-stdb`,必须填写受限的 `STDB_API_ROLLOUT_APPROVERS`;Stdb Publish 通过 `KEEP_MAINTENANCE_MODE` 保持维护文件并停止旧 API/controller/worker,暂停点最多等待 4 小时,完成上述维护并确认无后续批次后才由指定审批人放行 API。定时构建缺少审批人时必须在发布前失败,不能静默退回 `normal`;也可分开运行 Stdb publish、维护、API deploy 三个受控 Job。任一批次都不得处理 pending / running payload;不要用 runtime writer、bootstrap secret 或匿名 identity 代替 migration operator,也不要在未核对 dry-run 时直接 apply。
|
||||
|
||||
角色动作正式字段收口使用 `node scripts/spacetime-normalize-editor-character-actions.mjs --database <database> --server-url <url>`,且同样只能由已授权 migration operator 执行。必须先发布包含 normalization cursor 索引和 `normalize_editor_character_animation_metadata_and_return` 的 SpacetimeDB 模块,在 API / worker 仍处于维护模式时先运行默认全量 dry-run;脚本固定按 `asset → project-resource → showcase → canvas` 扫描,普通 scope 每批最多 25 行,canvas 每批最多 5 行。全量 dry-run 会在不写库的情况下把 asset 计划结果投影给同 owner / task / 首帧对象精确匹配的 project-resource,再把前置 scope 的计划结果投影给 canvas 检查;因此同 task 的误标预览 MP4 会先按权威视频对象排除,最终图片序列会逐帧核对并补齐精确 `asset_object` 身份。canvas 中仍引用误标 preview resource 的普通 video layer 会按 project-resource 计划态 `video` 跳过,只有 layout 明确声明动作却指向视频,或资源规划本身失败时才形成 blocker。apply 时仍要求前置 scope 已按顺序物理完成,不能跳过 asset 直接让 project-resource 借未落库结果。历史 canvas 复制的 `sourceResourceId` 不是迁移证据,不要因它仍指向原角色而手工改库,补建资源会采用最终账号素材的 DB 血缘。出现 blocker 时脚本会打印 ID、原因、owner、project、task、对象身份和来源资源;先据此区分最终候选为零 / 多个、正式与旧版冲突、帧对象不匹配或缺失资源,不得跳过 scope。确认 dry-run 后追加 `--apply`,脚本会对每批重新 dry-run、携带该批 SHA-256 apply,并在最后从头要求四个 scope 均为零匹配、零 blocker。只有该复核通过后才发布移除 action fallback 的 API / Web。Stdb build artifact 和完整 release 包必须同时包含 `scripts/spacetime-normalize-editor-character-actions.mjs` 与 `scripts/spacetime-migration-common.mjs`。本地切换分支时若要避免 dev publish 因 schema 冲突使用 `-c=on-conflict` 清库,启动命令必须追加 `--preserve-database`,让冲突直接失败。
|
||||
|
||||
自 2026-07-11 起,`Genarrative-Full-Build-And-Deploy` 的每日 04:00 timer 默认以 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate。三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,不得依赖下游 Job 默认值或提前各自发布;统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。上文“定时构建缺少审批人时失败”的旧口径不再作为当前 dev 定时发布行为。
|
||||
|
||||
Full Job 通过 `EXIT_MAINTENANCE_MODE_AFTER_COMPLETION` 明确选择完整发布成功后是否退出维护,默认勾选以保持历史行为。Full 对 Stdb Publish 和 API Deploy 两个下游阶段都固定传 `KEEP_MAINTENANCE_MODE=true`,让 maintenance marker 持续覆盖 Stdb → API → Web 整段发布;Web Deploy 成功后才进入独立 `Exit Maintenance` 阶段。该阶段只能通过 `agent none` 和显式 `node(...)` 分配目标机,直接执行 `/opt/genarrative/current/scripts/deploy/maintenance-off.sh`;目标机不得 checkout Git、挂载 Git SSH 凭据或依赖 Jenkins workspace 源码。取消勾选时跳过最终退出阶段,便于内网验收完成后人工恢复公网。`Genarrative-Api-Deploy` 也单独暴露 `KEEP_MAINTENANCE_MODE` 参数,并转换为随发布包脚本的 `--keep-maintenance-mode`;失败路径仍按既有 current 切换边界保留或退出维护,不受成功态选项覆盖。外部生成 queue 的 `warning` 由 API/worker 固化为可直接展示的完整文案,Web 不再补前缀,因此 API/worker 与 Web 必须在同一维护窗口按同一版本协调发布;分开运行 Job 时先保持维护态完成 API/worker,再发布 Web,二者完成后才能恢复公网,不得在公网可用期间只滚动其中一侧。
|
||||
@@ -241,20 +243,20 @@ PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME
|
||||
|
||||
当前 `genarrative-station` 使用 Gitea `1.26.4` 和基于 Gitea Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像。Runner 2.0.0 会先把 `systempaths=unconfined` 解析为空 `MaskedPaths` / `ReadonlyPaths`,再被 `mergo.WithOverride` 当成 empty value 丢失;站点修补只在 merge 后保留这两个显式空 slice,不改其它 runner 行为。真实 job inspect 必须看到 `MaskedPaths=[]`、`ReadonlyPaths=[]`、`SecurityOpt=[seccomp=unconfined]`、`Privileged=false`、无 CapAdd 且 `Binds=[]`。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置保持 `docker_host: "-"`、`valid_volumes: []`、`bind_workdir: false` 和 `force_pull: false`,防止内部 Docker socket 或宿主 bind mount 进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway 解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket 的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。
|
||||
|
||||
CI job 镜像由 `deploy/container/gitea-ci-job.Dockerfile` 定义:Ubuntu job base 固定为 `sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614`,Rust stage 固定为 `sha256:19817ead3289c8c631c73df281e18b59b172f6a31f4f563290f69cddd06c30e9`,Node `22.23.1` 发行包执行 SHA-256 校验,Google Linux 主签名指纹固定,Chrome 固定为 `150.0.7871.181-1`。构建脚本以 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本和 npm / Cargo manifests/lock;当前 context 约 `1.638 MB`。镜像按根 npm 锁、server-rs 锁和桌面壳锁预热下载缓存,不包含 `node_modules` 或 Cargo `target`;两个 `cargo fetch --locked` 在 Cargo 自身重试之外再执行最多 5 次整命令级有界重试,处理 registry index 握手失败,最终仍以断网 `cargo fetch --locked` 关闭验证。当前验证镜像约 `1.788 GB`,默认 tag 为 `genarrative/gitea-project-ci:20260723.1`,完整 Image ID 为 `sha256:c04b114b1f145072c9df7842c4c974e1bb2eaaf391d95d84c9212a460546b7d5`;runner 标签保留 `ubuntu-latest`,并将 `genarrative-ci` 映射到 `docker://sha256:c04b114b1f145072c9df7842c4c974e1bb2eaaf391d95d84c9212a460546b7d5`。内层 Docker 数据必须持久化;`force_pull: false` 表示只使用这个已装载的精确内容,Image ID 缺失时 job 必须失败关闭,不得回退浮动 tag 或临时连 registry。
|
||||
CI job 镜像由 `deploy/container/gitea-ci-job.Dockerfile` 定义:Ubuntu job base 固定为 `sha256:58ea92624c7c09582e05594d95488331045053d3a3f34cf09649f2a32313a614`,Rust stage 固定为 `sha256:19817ead3289c8c631c73df281e18b59b172f6a31f4f563290f69cddd06c30e9`,Node `22.23.1` 发行包执行 SHA-256 校验,Google Linux 主签名指纹固定,Chrome 固定为 `150.0.7871.181-1`。构建脚本以 NUL 分隔白名单 tar 流只发送 Dockerfile、checkout 脚本,以及根、AI 游戏创作壳、server-rs 与桌面壳所需的 npm / Cargo manifests/lock;当前 context 约 `2.13 MB`。镜像按根 npm 锁、AI 游戏创作壳 npm 锁、server-rs 锁、桌面壳锁和 AI 游戏创作壳 Cargo 锁预热下载缓存,不包含 `node_modules` 或 Cargo `target`;三个 `cargo fetch --locked` 在 Cargo 自身重试之外再执行最多 5 次整命令级有界重试,处理 registry index 握手失败,最终仍分别以断网 `cargo fetch --locked` 关闭验证。当前验证镜像约 `1.85 GB`,默认 tag 为 `genarrative/gitea-project-ci:20260807.1`,完整 Image ID 为 `sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf`;runner 标签保留 `ubuntu-latest`,并将 `genarrative-ci` 映射到 `docker://sha256:8b4b30f5a096522942947927b06cf47bdb1a1016dde9a3573f9780e79d8e40cf`。内层 Docker 数据必须持久化;`force_pull: false` 表示只使用这个已装载的精确内容,Image ID 缺失时 job 必须失败关闭,不得回退浮动 tag 或临时连 registry。
|
||||
|
||||
镜像更新命令:
|
||||
|
||||
```bash
|
||||
bash scripts/gitea-ci-job-image.sh build
|
||||
bash scripts/gitea-ci-job-image.sh verify
|
||||
bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-gitea-project-ci-20260723.1.tar.zst
|
||||
bash scripts/gitea-ci-job-image.sh export /仓库外受控路径/genarrative-gitea-project-ci-20260807.1.tar.zst
|
||||
bash scripts/gitea-ci-job-image.sh load-runner
|
||||
```
|
||||
|
||||
执行账号只要有权访问宿主 Docker API 并管理 runner 容器即可,不强制使用 root;无该权限时由 runner 运维人员执行。更新顺序必须是 `build/verify -> export 仓库外镜像归档与 SHA-256 sidecar -> load-runner -> 确认无活跃 job -> 备份当前 config -> 增加或替换 label -> docker restart --timeout 660 gitea-runner`。`--timeout 660` 只是停止宽限,不是 drain API;rootless DinD supervisor 可能同时停止内层 dockerd,因此重启前必须确认 Gitea 没有 `in_progress` run 且内层 `docker ps` 为空。config 和镜像归档只保存到仓库外受控位置,不在文档、仓库或日志中记录注册信息。重启后先重跑真实 PR 的四个 job,复核隔离边界并确认全部通过,再清理旧镜像。回滚时先把 workflow 的 `runs-on` 改回 `ubuntu-latest`,再恢复 config 备份并重启 runner。
|
||||
|
||||
四个 job 先运行镜像内 `genarrative-gitea-checkout`,再以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,校验 Node 主版本、仓库 Rust toolchain、受信任 PATH、缓存锁命中状态、原生命令、pkg-config 依赖、完整 bwrap sandbox 和 Chrome headless。`RUSTUP_AUTO_INSTALL=0`,因此仓库 `rust-toolchain.toml` 变更必须先更新镜像,不能让 job 现场下载。每个 job 仍独立运行 `npm ci`,以当前 lockfile 为准验证 PR 依赖;`NPM_CONFIG_PREFER_OFFLINE=true` 且网络重试为 10 次,命中镜像 cache 时只做干净解包,lock 变化时允许补齐差量。不在镜像内烘入 `node_modules`,也不挂载跨 PR 可写缓存。任何 job 的 sandbox canary 失败都必须停止,不允许跳过。Cargo 通过受控 proxy 下载 lock 差量时继续关闭 HTTP multiplexing 并设置 `CARGO_NET_RETRY=10`。
|
||||
四个 job 先运行镜像内 `genarrative-gitea-checkout`,再以 `GENARRATIVE_GITEA_CI_CHECK_RUNTIME=1` 执行 `scripts/check-gitea-ci-job-image.sh`,校验 Node 主版本、仓库 Rust toolchain、受信任 PATH、五份缓存锁命中状态、原生命令、pkg-config 依赖、完整 bwrap sandbox 和 Chrome headless。运行时发现锁不匹配时必须输出对应 `*_cache_lock=partial` 和 Actions warning,提示可信分支落地后刷新镜像,不能把陈旧缓存误报为闭合。`RUSTUP_AUTO_INSTALL=0`,因此仓库 `rust-toolchain.toml` 变更必须先更新镜像,不能让 job 现场下载。每个 job 仍独立运行 `npm ci`,以当前 lockfile 为准验证 PR 依赖;统一通过 `scripts/ci-npm-ci-with-retry.sh` 做最多 3 次整命令级有界重试,同时保留 `NPM_CONFIG_PREFER_OFFLINE=true` 和 npm 自身 10 次 fetch retry。命中镜像 cache 时只做干净解包,lock 变化时允许补齐差量。不在镜像内烘入 `node_modules`,也不挂载跨 PR 可写缓存。任何 job 的 sandbox canary 失败都必须停止,不允许跳过。Cargo 通过受控 proxy 下载 lock 差量时继续关闭 HTTP multiplexing,并设置 `CARGO_NET_RETRY=10`。
|
||||
|
||||
站点 stack 仍由宿主受控目录管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到与宿主相同的绝对路径再执行;挂载到不同路径会让相对 bind source 被 Docker daemon 解析到错误的宿主目录并启动空数据。升级或 runner 迁移前先停止 Gitea 写入,并把 Gitea 冷快照、数据库导出、compose/env 与 runner config/.runner 保存到仓库外受控备份位置。备份文件、绝对宿主配置和注册 token 不得提交 Git,也不在共享文档中记录具体路径或注册内容。
|
||||
|
||||
@@ -589,7 +591,7 @@ Pingora current release 自审脚本 `scripts/ops/pingora-current-release-audit.
|
||||
|
||||
同一 API release 随包依赖还必须包含 `scripts/check-pingora-release-readiness.mjs` 与 `scripts/check-pingora-canary-live.mjs`。前者在 current release 上以 `--release-runtime-only` 汇总运行时复核,后者支撑目标 Nginx canary live smoke;缺少任一脚本时不能进入直连切换窗口。
|
||||
|
||||
`Genarrative-Stdb-Module-Build` 的 Jenkins 归档产物必须包含 `build/<version>/spacetime_module.wasm`、`spacetime_module.wasm.sha256`、`release-manifest.json`、`scripts/deploy/production-stdb-publish.sh`、`scripts/deploy/production-runtime-writer-identity-rotate.mjs`、`scripts/deploy/maintenance-on.sh`、`scripts/deploy/maintenance-off.sh`、`scripts/spacetime-migration-common.mjs`、`scripts/spacetime-maintain-external-generation-jobs.mjs`、`scripts/spacetime-migrate-editor-canvas-layout.mjs` 和 `scripts/database-backup-to-oss.mjs`,不得包含 `migration-bootstrap-secret.txt` 或任何原始 bootstrap secret。`Genarrative-Stdb-Module-Build` 只接受 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256` 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb `release-manifest.json` 以 `migration_bootstrap_secret_sha256` 记录该非敏感摘要。`Genarrative-Stdb-Module-Publish` 只通过 `copyArtifacts` 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret File;publish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 `migration_bootstrap_secret_sha256` 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secret;ID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
|
||||
`Genarrative-Stdb-Module-Build` 的 Jenkins 归档产物必须包含 `build/<version>/spacetime_module.wasm`、`spacetime_module.wasm.sha256`、`release-manifest.json`、`scripts/deploy/production-stdb-publish.sh`、`scripts/deploy/production-runtime-writer-identity-rotate.mjs`、`scripts/deploy/maintenance-on.sh`、`scripts/deploy/maintenance-off.sh`、`scripts/spacetime-migration-common.mjs`、`scripts/spacetime-maintain-external-generation-jobs.mjs`、`scripts/spacetime-normalize-editor-character-actions.mjs`、`scripts/spacetime-migrate-editor-canvas-layout.mjs` 和 `scripts/database-backup-to-oss.mjs`,不得包含 `migration-bootstrap-secret.txt` 或任何原始 bootstrap secret。`Genarrative-Stdb-Module-Build` 只接受 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 指向的受保护 Jenkins Secret File:构建 shell 从临时文件读取原始值,强制校验为 64 位十六进制,计算 SHA-256,随后只通过 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET_SHA256` 注入 Rust 编译;WASM 因而只包含摘要,不包含可下载的原文,Stdb `release-manifest.json` 以 `migration_bootstrap_secret_sha256` 记录该非敏感摘要。`Genarrative-Stdb-Module-Publish` 只通过 `copyArtifacts` 复制上述非敏感产物,不在目标机器 checkout Git,并在发布阶段用同一个凭据 ID 再次挂载 Secret File;publish 必须再次校验 64 位十六进制、重算 SHA-256,并与 manifest 的 `migration_bootstrap_secret_sha256` 强制匹配后才可发布。Full Build 必须保证 Stdb Build / Publish 的 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 完全相同并把同一个 ID 同时透传,不能从构建 artifact 传 secret;ID 不同、manifest 缺摘要或摘要不匹配都必须在发布前失败。
|
||||
|
||||
三个 SCM Jenkinsfile 将 `MIGRATION_BOOTSTRAP_SECRET_CREDENTIAL_ID` 默认固定为 `genarrative-spacetime-bootstrap-secret-dev-file`。Secret File 的原文只存在于 Jenkins Credentials;credential ID、参数默认值和定时 / 发布行为以仓库 Jenkinsfile 为事实源,不能只改 Job UI,因为 Declarative Pipeline 下一次载入会重写参数定义。旧 Secret Text `genarrative-spacetime-bootstrap-secret-dev` 继续保留给 Database Import / Export,不得原地改类型或删除。
|
||||
|
||||
|
||||
@@ -59,15 +59,18 @@ layer 只表达“某个资源怎样放在画布上”。`src / prompt / actualP
|
||||
|
||||
### 3.5 worker 原子完成
|
||||
|
||||
worker 完成生成任务时,本次先用读取时 revision 调用 CAS 保存;发生并发变更时拒绝覆盖并让任务保留可诊断失败,不再静默覆盖用户布局。最终收口仍是受 `job_id + worker_id + lease_token` 栅栏保护的后端 procedure 在同一事务内:
|
||||
worker 完成生成任务时,`api-server` 先把 Provider / OSS 结果准备为稳定 operation/slot 候选,再调用 `persist_editor_generation_result_and_return`。procedure 受 editor generation runtime service identity 保护,queue 路径还必须在同一快照校验 `job_id + worker_id + lease_token`、owner、job kind 和由 job 规范请求重算的 SHA-256 fingerprint;inline 三个 job guard 全空,不接受半套栅栏。同一 `try_with_tx` 内:
|
||||
|
||||
1. 校验 job、owner、project、canvas、dialog 和租约;
|
||||
2. 幂等创建或确认 `editor_project_resource`;
|
||||
3. 创建 / 替换结果 layer,并删除或更新占位 layer;
|
||||
4. 把 dialog 更新为终态并关联 `generated_layer_id`;
|
||||
5. 递增 canvas revision,最后才允许完成 external job。
|
||||
1. 校验 operation 身份、request fingerprint、slot 唯一性、稳定 ID 和全部 owner/project/folder/source/task/媒体交叉关系;
|
||||
2. 精确 upsert 可选 `asset_object`,或验证省略的 object 已登记且归属同 owner;
|
||||
3. 创建全部 `editor_project_resource`、`editor_asset` 和可选 `asset_entity_binding`;
|
||||
4. 对候选布局重新执行 legacy / structured 验证,以 `expected_revision` CAS 写入 layer / dialog 完成态并且只递增一次 canvas revision;
|
||||
5. queue 路径写入 compact result 并完成 external job;
|
||||
6. 写入 `editor_generation_operation` durable receipt,固化 operation fingerprint、整笔 commit SHA-256、project/job/worker/lease/result 绑定和首次完成时间。
|
||||
|
||||
重复 completion 必须返回同一资源、layer 和 dialog 终态,不得重复插入,也不能因 dialog 暂时缺失而返回 `changed=false` 后仍把任务标记完成。任一步失败时整笔业务写回回滚,任务保留可诊断的失败或可重试状态。
|
||||
任一步失败时 object/resource/asset/binding/canvas/job/receipt 全部回滚。CAS 冲突时调用方只刷新当前 project 并重算 layout 候选,原 operation、slot、对象和业务记录候选不变,不重跑 Provider 或 OSS。完整重放只在 receipt 存在,且 request fingerprint、commit SHA-256、project/job 绑定与全部权威记录一致时返回 `AlreadyApplied`;不重复事件、不刷新时间、不推进 revision。receipt 缺失但稳定业务记录已存在、同 operation 内容漂移或不完整重放都必须失败关闭。
|
||||
|
||||
`completed_at_micros` 必须为正数并固化到 receipt;object/resource/asset/binding/canvas 候选的原时间字段也纳入 commit SHA-256,重放复用原 prepared commit,不重新取时。job 完成时间和完成事件使用事务 `ctx.timestamp`,不信任 worker 时钟。OSS `PUT / HEAD` 仍在 SpacetimeDB 事务外,因此事务失败可以留下未登记或未引用 object,不将本契约表述为跨 OSS exactly-once。
|
||||
|
||||
### 3.6 免费同步栅格派生完成
|
||||
|
||||
@@ -123,7 +126,7 @@ SpacetimeDB 必须先于依赖新 procedure / bindings 的 API 发布;前端
|
||||
- release 存量抽样中的缺资源 `local-*` 角色动作序列可无损 round-trip,并被识别为已持久化终态而非资源登记 pending;同形状但空帧、相对路径、HTTP / 签名 URL、`data:` / `blob:` 引用必须拒绝。已有资源的 `sourceResourceId == resourceId` 历史自引用应按资源表真相安全剥离,其他来源 ID 或资源字段冲突仍必须拒绝。
|
||||
- release 全量审计暴露的普通缺资源行必须先通过定向 repair dry-run;图片只能复用同工程唯一资源,音频只能从已登记 private asset_object 恢复。修复后同一 plan 全部命中 `already_repaired`,再重跑全量 backfill dry-run,要求所有 canvas 均通过。
|
||||
- structured 模式下 typed 列而非扩展 JSON 决定几何、层级、分组、显示 / 锁定、资源引用、`asset_kind_override` 和 dialog 状态;标签展示和类型能力判断统一按 `override ?? resource default`。修改当前图层标签与清除覆盖都保持 `resource_id` 和资源行数量不变;复制共享同一资源并复制 override,随后各副本可独立修改 override。两个客户端基于同一 revision 写入时只允许一个成功,冲突方重载后端最新快照,不换上新 revision 原样重放旧整包。细粒度 batch mutation 是取消 2 MiB 兼容入口的后续项,不冒充为本次已完成。
|
||||
- worker completion 当前以读取时 revision 做 CAS,冲突时拒绝覆盖;V2 保存和保存后快照在同一 procedure 结果内返回,避免“已提交但后续 GET 失败”的不确定结果。lease-fenced 资源 / layer / dialog / job 单事务 completion 仍是后续收口项。
|
||||
- worker completion 已使用 durable receipt 与统一原子提交;V2 布局 CAS、object/resource/asset/binding、job 终态和 receipt 在同一 procedure 结果内返回。故障注入必须证明资产校验失败与 canvas revision 冲突均为零部分写入,成功后重放不新增记录、事件或 revision。
|
||||
- structured 快照刷新后,上传参考图、生成结果、占位与 dialog 状态均可恢复;资源存在但布局写入失败时不会伪装为保存成功。
|
||||
- 完美像素处理失败 / 超时时 OSS、resource、asset 和 layer 均无新增;成功时只有一个最终 PNG、至多一个 project resource 和一个账号素材。处理中占位删除已先持久化时,完成请求不复活 dialog 或结果 layer;回包时本地占位已删除则不应用完成快照,已成功创建的资源 / 素材仍可读取;传输结果未知时客户端不自动重放 unsafe POST。
|
||||
- 回滚重组结果经 schema 校验、canonical hash / 资源引用核对且不超过 2 MiB;超限或不一致时明确拒绝且 structured 快照仍可读取。
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
- 工具参数中的图片 ID 是由真实 object key 或图片地址计算的稳定 SHA-256 标识;真实 data key 仅存于 api-server 的工具上下文映射,所有图片工具在执行时查表恢复,不能把 object key 或图片地址作为 LLM 可见的工具 ID。
|
||||
- 规划 prompt 必须显式区分“规范展板”和“实际素材产出”:规范图、视觉规范图、风格规范图、素材规范展板、角色规范图等规范展板请求走 `generate-image`,并补齐统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等要求;实际角色立绘才走 `generate-character`,多个图标素材 / 图集才走 `generate-icon-spritesheet`。
|
||||
- 用户的当前消息确实在确认或取消一条已存在且仍为 pending 的工具调用时,画布 Agent 只引导使用该卡片的确认 / 取消按钮,本条确认 / 取消意图不产生新 tool call。这条边界必须使用“匹配 pending 调用时如何处理”的正向、条件化描述,不得改写成“不得重新发起相同工具调用”一类全局否定话术:实测中模型会把这类否定句过度泛化为拒绝后续新请求。已 cancelled 的卡片不再处理;用户明确要求修改、重做或发起新任务时必须允许新 tool call,pending 卡片也不阻塞无关的新请求。
|
||||
- 画布 Agent 规划请求使用 Chat Completions 和 1024 `max_tokens`。发送后 120 秒是前端软提示阈值,不是 provider 失败 deadline:若普通 JSON POST 仍 pending,消息流临时显示“仍在处理中,请耐心等待”并继续等待,提示不写入 OSS 消息历史;连接或请求明确失败则立即按正式错误收口。provider 单 attempt 保留 8 分钟 hard timeout;请求发起阶段的 timeout、连接失败、`408`、`429` 与 `5xx` 读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次,专用重试退避最多 60 秒。消息规划生命周期从 handler 入口开始计入 18 分钟总 deadline,进入 `agent.prompt(...)` 时使用扣除会话锁和上下文准备后的剩余预算;该 deadline 必须作为 runner 内部 deadline future 参与 completion await,并在每个 tool 开始前、返回后检查,不能用外层 `tokio::timeout` 丢弃整个 prompt future,也不能中途 drop 已开始的工具。工具一旦开始就等待其返回,再按 deadline 携带结果收口;当前八类画布工具只做同步参数校验并返回待确认,因此不会延长正式生成链。deadline 命中时仍按 `PromptRunError` 返回已经完成的工具结果、提交对应 staged memory 并追加终态错误。该 deadline 覆盖非法 JSON/工具校验失败触发的后续规划轮,并为错误持久化和 HTTP 返回保留约 2 分钟,不再让前端 20 分钟 transport timeout 先触发。已收到成功响应头后的响应体读取或解析失败直接按明确失败收口,错误计数/日志使用该响应所属的真实 attempt。规划重试发生在任何生成工具执行之前,不会重复提交生成任务或扣费;生成图片/编辑图片仍走对应生成工具和模型计费。
|
||||
- 画布 Agent 规划请求使用 Chat Completions 和 1024 生成 token 预算;VectorEngine 专用 client 发送 `max_completion_tokens`,预算包含可见输出与隐藏 reasoning token,不等于可见正文长度。发送后 120 秒是前端软提示阈值,不是 provider 失败 deadline:若普通 JSON POST 仍 pending,消息流临时显示“仍在处理中,请耐心等待”并继续等待,提示不写入 OSS 消息历史;连接或请求明确失败则立即按正式错误收口。provider 单 attempt 保留 8 分钟 hard timeout;请求发起阶段的 timeout、连接失败、`408`、`429` 与 `5xx` 读取 `GENARRATIVE_LLM_MAX_RETRIES`,但画布 Agent 最多重试 1 次,专用重试退避最多 60 秒。消息规划生命周期从 handler 入口开始计入 18 分钟总 deadline,进入 `agent.prompt(...)` 时使用扣除会话锁和上下文准备后的剩余预算;该 deadline 必须作为 runner 内部 deadline future 参与 completion await,并在每个 tool 开始前、返回后检查,不能用外层 `tokio::timeout` 丢弃整个 prompt future,也不能中途 drop 已开始的工具。工具一旦开始就等待其返回,再按 deadline 携带结果收口;当前八类画布工具只做同步参数校验并返回待确认,因此不会延长正式生成链。deadline 命中时仍按 `PromptRunError` 返回已经完成的工具结果、提交对应 staged memory 并追加终态错误。该 deadline 覆盖非法 JSON/工具校验失败触发的后续规划轮,并为错误持久化和 HTTP 返回保留约 2 分钟,不再让前端 20 分钟 transport timeout 先触发。已收到成功响应头后的响应体读取或解析失败直接按明确失败收口,错误计数/日志使用该响应所属的真实 attempt。规划重试发生在任何生成工具执行之前,不会重复提交生成任务或扣费;生成图片/编辑图片仍走对应生成工具和模型计费。
|
||||
- function-calling runner 必须把“等待用户确认”作为显式工具语义:当本批所有工具都校验成功并进入待确认状态时,立即以成功结果结束当前规划回合并持久化助手文本与待确认卡,不得继续依赖 LLM 自行停止;未知工具、参数错误、普通连续工具和不可解析响应仍受 `max_turns` 保护。
|
||||
- runner 失败必须返回显式的 `PromptRunError { error, partial_outputs }`,不得只返回终态错误而丢弃本轮已产生的文本或工具事实。prompt 执行使用 `AgentMemory::begin_staged` 创建行为等价且写入隔离的 `StagedAgentMemory` 事务,限长、摘要、脱敏等 append 规则必须在本轮 completion 前生效;成功或已发生工具活动时必须显式调用 `commit()`,直接 drop staged transaction 表示回滚,不得统一复制成 `VecMemory` 或仅替换 box 冒充持久化提交。本轮无工具活动失败时回滚 staged 用户消息、助手文本和不可解析响应;已有工具活动时在末尾追加 terminal error closure 后提交。外部 drop / abort 若尚无工具活动则回滚并保持原 committed memory;若工具已完成则提交结果与取消闭环,若工具仍在执行则提交“已启动、结果未知”事实与取消闭环,后续必须先 reconcile 再决定是否重试。
|
||||
- `ToolFailure` 必须以结构化工具失败输出暴露给 harness 调用方:调用方能读取 `kind`、`retryable`、`fatal` 和工具返回的原始 `output`;不得把它们压成单一错误字符串。这些字段只提供流程决策与诊断事实,是否重试、如何展示或持久化仍由业务调用方决定。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
日期:`2026-06-15`
|
||||
|
||||
更新时间:`2026-07-28`
|
||||
更新时间:`2026-08-07`
|
||||
|
||||
## 背景
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
### Prompt 与生成契约
|
||||
|
||||
- 前端提交到 `POST /api/editor/character-animations/generations`。
|
||||
- 请求必须带上角色图片来源、原始尺寸、动画描述、分辨率、画面比例、帧数和时长,并可通过 `assetFolderId` 指定中间视频素材落点。`sourceImageSrc` 只允许传当前账号的 `objectKey`、项目资源 ID 或素材 ID;未持久化的本地临时图片必须先上传 OSS,不能提交 Data URL 或 Blob URL。后端解析稳定引用后必须校验对象属于当前 owner,再读取 OSS 内容。
|
||||
- 请求必须带上角色图片来源、原始尺寸、动画描述、分辨率、画面比例、帧数和时长,并可通过 `assetFolderId` 指定中间视频素材落点。携带 `canvasCompletion` 时必须同时提供非空 `projectId`,该校验必须在解析来源、调用 provider、持久化和预扣泥点之前完成,禁止无项目画布完成请求在产生费用或产物后才失败。`sourceImageSrc` 只允许传当前账号的 `objectKey`、项目资源 ID 或素材 ID;未持久化的本地临时图片必须先上传 OSS,不能提交 Data URL 或 Blob URL。后端解析稳定引用后必须校验对象属于当前 owner,再读取 OSS 内容。
|
||||
- 后端使用角色图片作为首帧和尾帧参考,模型固定映射到 `doubao-seedance-2-0-fast-260128`。
|
||||
- 后端在内联执行和队列入队前都会拒绝 Data URL / Blob URL;路由使用默认 JSON body limit,不再为内联图片请求单独放宽。
|
||||
- 后端 prompt 使用以下固定骨架,并把面板输入追加到 `动作描述:` 后:
|
||||
@@ -182,9 +182,12 @@
|
||||
- 后端先计算整批精确采样时刻,再用单个 FFmpeg filter graph 统一解码预览视频并输出 `32 / 40 / 48` 张源帧;不得为每帧重新启动 FFmpeg、重复解码同一视频,也不得用会改变现有尾帧安全时刻的粗粒度 `fps` 抽帧替代。批量命令成功后必须逐一确认全部目标帧文件存在,缺少任一帧都按整批失败处理并保留缺帧编号、目标时刻和输出路径诊断。
|
||||
- 每帧绿幕源图字节由上传 owned 消费(`frame.bytes` 移入 put,上传完成后释放原帧缓冲,不克隆保留);后续父流程只持 object key,并为每帧向唯一 loopback `bgfilter-worker` 发起一次内部 HTTP RPC,不直接签发 BgFilter URL、不直连 provider,也不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。每帧请求分别携带按父剩余绝对预算派生的 `maxQueueWaitMs` 和公式化 `callBudgetMs`;子 worker 在默认 `Q=2048` admission 保险丝和 `Semaphore(N=16)` 约束下排队,取得 provider permit 后才启动调用预算,每次 provider attempt 前重新签发 600 秒 GET URL,multipart 仅传 `image_url`(加 `background_mode=flat`、`seg_model=birefnet`、`cross_check=on` 与同一次生成已选定的 `screenColor`),不传 `file`。真实 provider attempt 按 `N × est × 2` 派生,调用预算按 `2 × attempt + 1s` 派生;冻结 `est=5000ms` 时分别为 `160s / 321s`,排队不侵蚀最多两次顺序 attempt 的完整窗口,`32 / 40 / 48` 帧也不再增加单帧 attempt。成功图片由子 worker 以内部 HTTP 二进制 body 返回父流程,不在子侧落 OSS;BgFilter 最终失败且父业务预算仍有效时,由父流程进入 `阿里云通用抠图(按签名 URL 单独下载)→ 本地 editor_green_screen(再按 object key 独立下载一次并在产出后释放)`。
|
||||
- BgFilter、阿里云或本地键色返回透明结果后,父流程继续通过现有最终帧 finalizer 转为 RGBA8,按宽高比居中放入最终目标尺寸,并使用 `RGBA(0,0,0,0)` 补边;最终帧 OSS 与业务写回仍由父流程完成。上述样例最终输出必须为 `323×480 RGBA8 PNG`,顶部和底部各 `23px` 透明 padding,内容区域完整保留抠图结果。
|
||||
- 全部 `32 / 40 / 48` 帧以覆盖本次所有帧的无序在途集合向内部 worker 提交,允许乱序完成并最终按 `frameIndex` 排序;BgFilter provider 的实际在途请求受唯一 worker 的 `N / Q` 限制。任一帧最终失败时先排空全部已启动 Future,再让整项任务失败退款,不发布缺帧动画。
|
||||
- 全部 `32 / 40 / 48` 帧以覆盖本次所有帧的无序在途集合向内部 worker 提交,允许乱序完成并最终按后端内部 `frame_index` 排序;该索引只服务并发收口、OSS 命名、日志和错误定位,不写入正式 asset / resource 帧对象。BgFilter provider 的实际在途请求受唯一 worker 的 `N / Q` 限制。任一帧最终失败时先排空全部已启动 Future,再让整项任务失败退款,不发布缺帧动画。
|
||||
- 抽帧结果写入 OSS,并返回帧路径、帧尺寸、帧数、fps、预览视频路径、模型、价格和实际 prompt。
|
||||
- 画板前端回填角色动作结果时,必须以 `frames[0].imageSrc` 创建 `mediaType: "image-sequence"`、`assetKind: "character-animation"` 图层,并把完整 `frames` 保存为图层 `imageSequenceFrames`;`previewVideoPath` 只保留为上游预览视频来源,不作为画布主媒体。
|
||||
- 角色动作图层在画布中使用序列帧播放器循环展示透明 PNG 帧;刷新恢复时必须继续读取 `imageSequenceFrames`,不能回退到 `<video>` 预览。
|
||||
- 画板前端回填角色动作结果时,以正式 resource / asset 的 `assetKind: "character-animation"` 选择序列帧播放器并以首帧作为图层主图;带项目上下文的生成必须返回正式 project resource,图层直接使用其 `resourceId`。无项目放置只允许绑定响应中的正式账号素材,不从生成结果 `frames` 构造自包含 `local-resource-character-animation-*`;此时图层的 `sourceResourceId` 继续使用正式账号素材记录的直接来源,不回退成请求中的原角色资源。预览视频作为独立 `assetKind: "video"` 资源保存,但不复制进最终动作 layout。
|
||||
- 角色动作图层在画布中循环展示透明 PNG 帧;刷新恢复只从 project resource 的 `imageSequenceFrames/imageSequenceDurationMs` 读取正式结果。动作 layer layout 只保存 `layerId/resourceId` 和几何、层级、可见性等放置信息,不保存 `mediaType`、动作帧、时长、预览、生成输入或资源元数据副本;资源缺字段时按损坏数据失败关闭。
|
||||
- 点击角色动作图层下载时,必须打包下载序列帧 ZIP;画布素材 ZIP 导出时,角色动作写入 `sequences/<编号-标题>/frames/`,而不是导出预览视频。
|
||||
- 集合 ZIP 对角色动作去重时,稳定身份依次取层级 `taskId`、`sourceResourceId`,再按帧顺序取每帧 `assetObjectId -> objectKey -> imageSrc`;帧数组必须使用有边界编码,不得只拼接可为空的 `imageSrc`。两个仅含不同私有 `objectKey` 的序列必须同时进入 ZIP,根 `metadata.json` 分别指向各自的序列目录。
|
||||
- 角色动作生成 BFF 在同一请求内保存两类账号素材:上游预览 MP4 使用 `assetKind=video` 并承载生成成本,最终透明帧集使用 `assetKind=character-animation` 且派生成本为 0;响应中的 `resource` / `asset` 固定指向最终透明帧集对应的项目资源与账号素材。调用方必须直接使用它们,不得再以第一帧调用资源或素材创建接口。
|
||||
- 最终项目资源和账号素材只把 `imageSequenceFrames/imageSequenceDurationMs` 写入正式媒体字段,并以 `sourceResourceId` 指向独立预览视频资源;正式帧对象和角色动作生成响应都不含 `frameIndex`,数组位置是唯一播放顺序,帧数和 FPS 均按需派生。`generationInputs` 只保存 `fields/references` 等用户可重放输入,`screenColorHex` 和动作运行结果均不落入其中。存量 `characterAnimation`、已确认动作行的 helper 顶层字段、`frameIndex`、误标预览 MP4 及动作 layout 副本由受 migration operator 限制的四阶段数据库 procedure 一次性规范化;迁移按权威对象类型排除同 task 的预览视频、只接受唯一最终图片序列并逐帧补齐登记对象身份,且不把 layout 复制的 `sourceResourceId` 当作历史血缘证据。CLI tuple 与 HTTP object 两种 procedure 返回都必须先归一为脚本内部统一的 `batch_sha256`,分页还必须拒绝未推进或重复出现的 cursor,避免安全校验误拒绝或生产迁移无限循环。该宽容只属于迁移;新生成仍严格使用“原角色资源 → 预览视频资源 → 最终序列资源”直接来源链。上线完成后 api-server、后台、Web 和外部 helper 不再读取 legacy fallback。新动作写入若提交旧运行字段或 `frameIndex`,HTTP 与 SpacetimeDB storage 均明确拒绝。
|
||||
- `frames[0].imageSrc` 仍作为后续动作素材快速编辑或再次生成动作时的透明帧来源。该规则不得跳过后端原有的视频生成、抽帧、透明化处理和帧素材落盘流程。
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
|
||||
日期:`2026-06-18`
|
||||
|
||||
更新时间:`2026-08-06`
|
||||
|
||||
## 范围
|
||||
|
||||
本次只在 `/editor/canvas` 图片画布编辑器内新增底部 `生成音乐` 入口,用于生成完整游戏音效或游戏背景音乐。该入口属于画板生成类工具,不新增平台玩法入口、不进入作品发布链路,也不修改现有视觉小说音频生成开关。
|
||||
|
||||
2026-08-04 起,本文增加 BGM Prompt 优化 V1.0 口径。该切片只修改 `audio-background-music`;`audio-sound-effect` 的 UI、Prompt 回退、Vidu `audio1.0` 请求、`2-10` 秒时长和 1500 字限制全部保持不变。音效与背景音乐使用同一个音频 composer,并由组件内的 `isSoundEffect = dialog.mode === 'audio-sound-effect'` 隔离行为;不得把 BGM 规则扩散到 SFX。
|
||||
|
||||
2026-08-06 的组件架构修订只撤销完整 BGM composer 的独立视图边界。共享音频 composer 的 SFX 分支保留当前 Vidu、时长、默认 Prompt、1500 字和价格行为,BGM 分支保留本文规定的预设、计数、AI 补全 / 简化、撤销、锁定、canonical Prompt 和 Suno 行为。BGM 纯状态模型、助手 controller 与预设跑马灯仍可保持独立职责;本轮不实现 SFX V2 的 ElevenLabs、中译英、自动时长、30 秒、Loop、一键优化或预设等独有能力。具体实施边界见[音频生成 Composer 恢复共享分流方案](./project-memory/plans/【前端重构】音频生成面板恢复共享分流方案-2026-08-06.md)。
|
||||
|
||||
## 入口与交互
|
||||
|
||||
1. 底部 AI 画布工具栏新增 `生成音乐`。
|
||||
@@ -13,7 +19,7 @@
|
||||
- `生成游戏音效`
|
||||
- `生成游戏背景音乐`
|
||||
3. 选择某一项后创建独立 `generation-dialog` 画布生成对象,并通过现有 placement 模型避让已有图层和占位。
|
||||
4. 面板 UI 复用 `生成角色形象` 的紧凑结构:上方为字段区,底部为参数 / 模型 / 生成按钮区,不写规则说明类文案。音效参数按钮靠左下角,固定模型胶囊紧贴生成按钮;背景音乐同样在右下角显示固定模型胶囊并紧贴生成按钮。
|
||||
4. 面板 UI 复用 `生成角色形象` 的紧凑结构:上方为字段区,底部为参数 / 模型 / 生成按钮区,不写规则说明类文案。音效参数按钮靠左下角,固定模型胶囊紧贴生成按钮;背景音乐字段区增加字符计数、可展开预设词条、AI 补全、一键简化和单层撤销,底部仍保留现有动态泥点价格、固定 `Suno` 模型胶囊和生成按钮。
|
||||
5. 生成中隐藏设置面板,只保留画布中的音频生成占位;失败后恢复面板并展示短错误。
|
||||
|
||||
## 面板字段
|
||||
@@ -27,10 +33,176 @@
|
||||
|
||||
### 生成游戏背景音乐
|
||||
|
||||
- `gpt_description_prompt`:用户输入的背景音乐提示词。
|
||||
- `gpt_description_prompt`:用户输入框当前文本按本文规则清理首尾 Unicode 空白后形成的背景音乐提示词,是正式 BGM 生成的唯一最终 Prompt。
|
||||
- `make_instrumental`:固定传 `true`,不在 UI 中展示为可改字段。
|
||||
- 提交到 VectorEngine 时映射为 Suno 纯音乐模式字段:`mv`、`gpt_description_prompt`、`make_instrumental: true`。`mv` 后端固定使用默认 Suno 模型,UI 以禁用态模型胶囊显示 `Suno`。
|
||||
- `gpt_description_prompt` 按 Apifox 契约限制 200 字,超出时由 BFF 返回参数错误。
|
||||
- 前端请求继续使用 `gptDescriptionPrompt`,Rust DTO 继续使用 `gpt_description_prompt`;不得因“唯一最终 Prompt”语义把请求字段改名为 `actualPrompt`。响应中的 `actualPrompt` / `actual_prompt` 继续保留既有生成审计语义。
|
||||
- `gpt_description_prompt` 按 Apifox 契约限制 200 个 Unicode code point。前端、BFF 和 `platform-audio` 允许且必须执行同一套首尾 Unicode 空白清理;除此之外不得执行 Unicode 规范化、内部空白折叠、内部换行转换、标点替换或静默截断。
|
||||
|
||||
## BGM Prompt 优化 V1.0
|
||||
|
||||
### 唯一可见最终 Prompt、首尾空白与字符口径
|
||||
|
||||
- 预设和 AI 助手都只修改同一个 BGM 输入框。“唯一最终 Prompt”约束的是语义来源和用户可见性:不得在正式请求中额外拼入用户看不到的前缀、后缀、预设元数据、分组信息、内部模板或系统提示词;它不要求保留编辑缓冲区中的首尾空白。这里的“用户不可见内容”指应用额外注入的语义文本,不指用户输入或粘贴的零宽字符等不可见 Unicode code point。
|
||||
- 从当前字符串两端删除属于 Unicode `White_Space` 属性的 code point 后得到的文本,以下称 canonical Prompt(规范化 Prompt);内部空格、内部换行和其它 code point 保持不变。
|
||||
- 用户编辑期间输入框可以暂时保留首尾空格、Tab 和换行。字符计数与动作可用状态基于 canonical Prompt 的非写入式预览计算,不得在每次键入时改写输入框;点击预设、AI 补全、一键简化、撤销或正式生成时,前端才在同步操作阶段计算 canonical Prompt 并写回输入框。
|
||||
- 正式生成时,写回后的输入框、前端 BFF 请求、队列请求载荷、Suno body、生成记录 `prompt`、生成记录 `actual_prompt` 和结果响应必须逐 code point 完全一致。
|
||||
- 首尾空白清理必须跨语言一致,不能直接混用语义不同的原生函数。TypeScript 按 `\p{White_Space}` 删除两端 code point;Rust 使用 `char::is_whitespace` 删除两端 code point。`U+200B` 和 `U+FEFF` 不属于 Unicode `White_Space`,不得被这一步顺带删除。
|
||||
- 除首尾 Unicode 空白清理外,不删除零宽字符,不执行 NFC 或其它 Unicode 规范化,不折叠内部空格,不转换或合并内部换行,不替换标点,不静默截断。
|
||||
- 总字符数按规范化后最终 Prompt 的 Unicode code point 数计算。TypeScript 使用 `Array.from(finalPrompt).length`,Rust 使用 `final_prompt.chars().count()`;被删除的首尾 Unicode 空白不计入 200 字。
|
||||
- “有效字符”只用于动作可用性和参数校验,定义为最终 Prompt 中的非 Unicode 空白 code point。标点、数字、字母和不可见但不属于 Unicode `White_Space` 的 code point 均计为有效字符;内部空格、内部换行和全部不可见 code point 仍计入 200 字总数。
|
||||
|
||||
| 规范化后的最终 Prompt | AI 补全 | 一键简化 | 正式生成 |
|
||||
| --- | --- | --- | --- |
|
||||
| 空字符串(总数 0、有效字符数 0) | 禁止 | 禁止 | 禁止 |
|
||||
| 1 个有效字符且总数不超过 200 | 禁止 | 禁止 | 允许 |
|
||||
| 至少 2 个有效字符且总数不超过 200 | 允许 | 禁止 | 允许 |
|
||||
| 至少 1 个有效字符且总数为 201–2000 | 禁止 | 允许 | 禁止 |
|
||||
| 总数超过 2000 | 禁止 | 禁止 | 禁止 |
|
||||
|
||||
- canonical Prompt 满足不变量:`有效字符数 = 0` 当且仅当 `总字符数 = 0`。原始输入即使完全由 201 个或更多 Unicode `White_Space` 组成,canonicalization 后仍为空字符串,AI 补全、一键简化和正式生成全部禁止;不得按规范化前的长度把全空白输入归入“可简化”状态。
|
||||
- 一键简化最大输入固定为正式生成合法上限的 10 倍,即 `200 × 10 = 2000` 个 canonical Unicode code point。超过 2000 时必须完整保留当前文本供用户手动编辑,不得截断或丢失,但 AI 补全、一键简化和正式生成都不可用。
|
||||
|
||||
### 预设库与追加规则
|
||||
|
||||
预设分为三组,仅用于颜色和滚动组织;分组、ID、颜色等隐藏元数据不进入 Prompt、Suno 请求或生成记录。写入输入框的是下表右列完整可见文本。
|
||||
|
||||
| 分组 | 预设 | 写入输入框的文本 |
|
||||
| --- | --- | --- |
|
||||
| 用途 | 菜单待机 | 低干扰、适合菜单待机的背景音乐 |
|
||||
| 用途 | 休闲消除 | 轻快可爱的休闲消除背景音乐 |
|
||||
| 用途 | 解谜思考 | 安静专注的解谜思考背景音乐 |
|
||||
| 用途 | 探索冒险 | 温和推进的探索冒险背景音乐 |
|
||||
| 用途 | 对白场景 | 克制柔和、留出对白空间的背景音乐 |
|
||||
| 用途 | 战斗前 | 蓄势待发的战斗前背景音乐 |
|
||||
| 用途 | 胜利结算 | 明亮满足的胜利结算背景音乐 |
|
||||
| 用途 | 失败结算 | 克制低落的失败结算背景音乐 |
|
||||
| 用途 | 日常经营 | 轻松有序的日常经营背景音乐 |
|
||||
| 用途 | 农场经营 | 自然温暖的农场经营背景音乐 |
|
||||
| 用途 | 校园日常 | 青春轻松的校园日常背景音乐 |
|
||||
| 用途 | 美食厨房 | 温暖活泼的美食厨房背景音乐 |
|
||||
| 氛围 | 温暖治愈 | 柔和明亮的治愈背景音乐 |
|
||||
| 氛围 | 神秘悬疑 | 克制神秘的悬疑背景音乐 |
|
||||
| 氛围 | 紧张推进 | 稳定推进、逐渐紧张的背景音乐 |
|
||||
| 场景 | 森林自然 | 清新自然的森林背景音乐 |
|
||||
| 场景 | 雨夜静谧 | 雨夜静谧、略带神秘感的背景音乐 |
|
||||
| 场景 | 海洋漂流 | 开阔舒缓的海洋漂流背景音乐 |
|
||||
| 场景 | 山野远行 | 自由舒展的山野远行背景音乐 |
|
||||
| 场景 | 糖果乐园 | 甜美活泼的糖果乐园背景音乐 |
|
||||
| 场景 | 温馨小屋 | 温暖安静的温馨小屋背景音乐 |
|
||||
| 场景 | 城市夜晚 | 克制迷人的城市夜晚背景音乐 |
|
||||
| 场景 | 太空科幻 | 空灵未来感的太空科幻背景音乐 |
|
||||
| 场景 | 赛博街区 | 冷静律动的赛博街区背景音乐 |
|
||||
| 场景 | 古风幻想 | 空灵雅致的古风幻想背景音乐 |
|
||||
| 场景 | 童话花园 | 梦幻轻盈的童话花园背景音乐 |
|
||||
| 场景 | 海底遗迹 | 深邃神秘的海底遗迹背景音乐 |
|
||||
| 场景 | 沙漠遗迹 | 苍茫神秘的沙漠遗迹背景音乐 |
|
||||
| 场景 | 熔岩洞穴 | 炽热压迫的熔岩洞穴背景音乐 |
|
||||
| 场景 | 奇妙博物馆 | 好奇灵动的奇妙博物馆背景音乐 |
|
||||
|
||||
点击预设时:
|
||||
|
||||
1. 先按统一规则删除输入框文本两端的 Unicode `White_Space` code point,并把结果写回输入框。
|
||||
2. 规范化后的字符串为空,直接写入该预设的完整可见文本。
|
||||
3. 规范化后的字符串非空,检查其最后一个 Unicode code point。
|
||||
4. 最后一个字符属于 Unicode 标点类别时,直接追加预设文本;否则先追加中文句号 `。`,再追加预设文本。
|
||||
5. 首尾空白在追加前已经删除;内部空格和内部换行保持原位,不得继续清理。写入输入框的规范化文本、句号和预设可见文本共同构成新的唯一最终 Prompt。
|
||||
6. 允许重复点击同一个预设,每次都按同一规则追加。
|
||||
7. 点击任意预设后清除旧撤销快照并隐藏撤销按钮。
|
||||
8. 追加后允许超过 200 字,完整文本必须保留;超限只复用现有通用 Prompt 过长状态,不增加预设专用警告。
|
||||
|
||||
预设滚动交互:
|
||||
|
||||
- 展开后全部词条横向、连续、缓慢循环;收起后隐藏词条并停止可见滚动。
|
||||
- 桌面端左侧 15% 悬停区快速向左滚动,中间 70% 立即暂停并稳定展示至少 5 个词条,右侧 15% 快速向右滚动;鼠标移出后恢复默认慢速循环。
|
||||
- 左右箭头所在区域也必须触发对应方向的加速滚动,避免箭头可见但悬停无反馈。
|
||||
- 循环使用多份词条队列无缝衔接,末端不跳回、不留白,也不提供“换一批”。
|
||||
- 三组词条使用协调但可区分的颜色;底部细线标记当前悬停控制区。
|
||||
- 组件在触摸环境被渲染时支持横向滚动和箭头操作,不依赖 hover 才能选择预设;本需求不恢复移动端图片画布入口。
|
||||
- AI 处理或正式提交期间暂停滚动并禁用展开、收起、箭头和词条点击。
|
||||
|
||||
### AI 补全
|
||||
|
||||
- 点击 AI 补全时先按统一规则规范化输入框首尾空白并同步写回;至少 2 个有效字符且总字符数不超过 200 时,前端才把这份可见最终 Prompt 传给登录态内部 BFF。
|
||||
- Prompt 助手当前使用专用请求模型 `gpt-5.6-luna`,请求级 `reasoning_effort` 固定为 `medium`;画布 Agent 本身仍使用现有编辑器 Agent LLM 配置中的 `gpt-5.4-mini`。两者复用现有 `LlmClient`,不建立新的平台 LLM 能力。
|
||||
- 服务端模板必须要求:保留用户明确的主题、场景、风格、情绪、乐器、能量、韵律、时长、循环和避免项;按场景选择性补足场景、氛围、能量、韵律、乐器、旋律、声音设计、循环和避免项,不为凑全方向堆砌形容词。
|
||||
- 用户描述已足够完整时,只补充一至两个与主题匹配的具体声音细节。发现冲突时,优先级为“明确避免项和限制 > 明确玩法用途与场景 > 风格、情绪、能量与韵律 > AI 补充细节”。
|
||||
- 内部 envelope 的 `prompt` 字段只允许包含一条可直接写回输入框的中文 BGM Prompt;候选文本本身不得包含解释、标题、Markdown、JSON、代码块、具体艺人或歌曲模仿要求。
|
||||
- 补全与简化共用同一份内部结构化 envelope:`prompt: string`、`isDirectWritebackFormat: boolean`、`isContentComplete: boolean`、`hasObviousFragment: boolean`。助手显式使用现有 OpenAI Chat 协议,envelope 由完整 `response.text` 中唯一一个 JSON object 承载;服务端只允许 `serde_json` 对完整文本做全量解析,允许 JSON object 外围存在 JSON whitespace,但不接受代码块、前后解释、多个 JSON 值或从字符串中截取 object,也不执行自动修复。助手请求不发送 function tools,不做运行时双协议 fallback;响应出现 tool call 时同样视为结构非法。envelope 只用于服务端解析和判断,不属于候选文本,不写回输入框,也不通过 BFF 暴露给客户端。
|
||||
- 服务端必须在解析 envelope、canonicalize 候选或提取任何 `prompt` 前检查 OpenAI Chat `finish_reason`。去除外围空白并忽略 ASCII 大小写后,`length` 和 `content_filter` 都属于未完成响应;即使正文恰好是合法四字段 JSON,也不得接受、解析或提取候选。`finish_reason` 缺失、为空或为未知自定义值时,不因该字段单独拒绝,继续执行其余结构与候选门禁;不得改为只允许 `stop`。
|
||||
- 补全遇到 `finish_reason = length` 或 `content_filter` 时均直接失败,不写回、不泄漏响应正文或候选,且仍只执行一个业务语义轮。
|
||||
- 补全候选先执行同一套首尾 Unicode 空白规范化,再由程序计算字符数。候选只有在四字段结构有效、canonical `prompt` 包含有效字符且不超过 200 个 Unicode code point,并且三个布尔字段依次为 `true / true / false` 时才通过;程序不使用关键词或未定义正则猜测候选格式、完整性或残句。通过后整段写回输入框并成为唯一最终 Prompt。
|
||||
- 补全只执行一个业务语义轮。`LlmClient` 在该轮内部按现有配置执行的 transport retry 不计为新增业务语义轮;该轮最终发生 transport、超时或上游失败时直接返回失败,不再发起内容修复轮。
|
||||
- 调用失败、结果为空、结构或判断不合格、格式非法或超限时保留请求前已经写回的规范化 Prompt,不写回部分结果;错误响应不得包含未通过候选或内部 envelope。
|
||||
- 补全过程不调用 Suno、不创建正式生成任务、不触发正式音乐生成扣费。
|
||||
|
||||
### 一键简化
|
||||
|
||||
- 点击一键简化时先按统一规则规范化输入框首尾空白并同步写回;只在规范化后的当前 Prompt 至少含 1 个有效字符且总字符数为 201–2000 时允许调用。超过 2000 时完整保留文本,但不得调用简化 BFF。点击前保存这份规范化后的完整 Prompt,AI 处理中保持输入框不变。
|
||||
- 服务端在本次简化中冻结 `originalPrompt` 为入站 canonical Prompt,最多两个业务语义轮期间始终不变,作为内容保真参照。第一次业务语义轮使用 `currentPrompt = originalPrompt`,目标为 180 字。这里的 `originalPrompt` / `currentPrompt` 是服务端组装 LLM 简化模板时的内部变量;客户端简化 BFF DTO 仍只提交一个 `currentPrompt`,该入站值经 canonicalization 后同时成为内部 `originalPrompt` 和第一次内部 `currentPrompt`。每个业务语义轮内部由 `LlmClient` 按现有配置执行的 transport retry 不增加业务语义轮数。
|
||||
- 180 不是硬门槛。简化使用与补全相同的四字段内部结构化 envelope;envelope 不属于候选文本,也不得写回输入框或通过 BFF 暴露。响应不能解析为包含上述正确字段类型的对象时,本次候选不通过。
|
||||
- 候选“格式合法”专指 `isDirectWritebackFormat = true`:模型确认 canonical 候选只包含一条可直接写回输入框的中文 BGM Prompt,不包含解释、标题、Markdown、JSON、代码块、字数报告、处理过程或删改说明。它与“内部结构可解析”是两个独立校验项;程序不得另用关键词或未定义的正则推断标题、解释等语义格式。
|
||||
- 第一次业务语义轮成功取得上游响应、但候选未通过任一通过条件时,自动进行唯一一个第二业务语义轮,目标为 170 字。只有当完整 `response.text` 已按上述规则全量解析为单个 JSON object 时,才允许从该 object 读取字符串 `prompt`;禁止从未完整解析的文本、代码块、解释或畸形 JSON 中做子串提取。若可提取的字符串 `prompt` canonicalize 后包含有效字符,第二次使用 `currentPrompt = canonicalize(第一次候选)`,即使第一次因其它字段缺失、超限、格式、完整性或残句判断而不通过;若完整响应无法解析为单个 object、object 中没有字符串 `prompt`,或候选 canonicalize 后不含有效字符,第二次回退使用 `currentPrompt = originalPrompt`。第二次业务语义轮中的 `originalPrompt` 始终仍是最初入站 canonical Prompt。
|
||||
- 第一轮 `finish_reason = length` 时整份响应不可信,不得接受或提取其中的 `prompt`;它只按“成功取得响应但候选不合格”进入唯一的 170 字轮,第二轮必须使用 `currentPrompt = originalPrompt`。第一轮 `finish_reason = content_filter` 时直接失败,不进入第二轮。第二轮出现 `length` 或 `content_filter` 时最终失败,不得发起第三轮。该规则优先于上一条的一般候选提取与回退规则。
|
||||
- 第一次业务语义轮最终发生 transport、超时或上游失败时直接返回失败,不进入 170 字内容修复轮;这类失败只应用该轮内部既有的 `LlmClient` transport retry。
|
||||
- 每次候选都先执行同一套首尾 Unicode 空白规范化,再计算实际字符数。第二次仍不合格时返回失败并保留请求前已经写回的规范化 Prompt,提示用户手动精简;错误响应不得包含第一次或第二次未通过候选、可提取的 `prompt` 或内部 envelope。最多两个业务语义轮,任何情况下都不得由程序截断到 200 字。
|
||||
- 简化模板必须优先保留明确限制、场景与用途、情绪与风格、核心乐器与速度、能量/韵律/旋律、循环结构、区分度较高的声音设计,再删除次要修饰细节;不得改变专有名词、BPM、调性、时长、数值、乐器和明确限制。
|
||||
- 程序不抽取关键词,不对规范化后的输入与候选执行内容硬编码逐项比对。是否为直接写回格式、是否保留重要信息且内容完整、是否形成明显残句由 LLM 在同一次调用的三个布尔字段中分别判断。
|
||||
- BFF 成功时只返回已通过校验的 Prompt 和程序计算的字符数,不暴露三个内部判断字段;失败时使用现有 API 错误 envelope,且不返回任何候选或内部判断字段。
|
||||
- 候选只有同时满足以下条件才通过:内部结构可解析且四个字段类型正确;canonical 候选包含有效字符;canonical 候选实际字符数不超过 200;`isDirectWritebackFormat = true`;`isContentComplete = true`;`hasObviousFragment = false`。程序负责解析和检查字段类型、canonicalization、有效字符与实际字符数,并执行三个布尔判断结果;字符数由程序计算且不采信模型报告,程序不自行推断三个语义判断。
|
||||
- 一键简化不维护或恢复“已选预设”元数据;预设已写入输入框的文本只是当前最终 Prompt 的一部分。
|
||||
|
||||
### 单层撤销
|
||||
|
||||
- AI 补全和一键简化成功都必须产生一层 Prompt 快照。发起 AI 操作前,先把当时输入规范化并写回,再以该 canonical Prompt 建立本次临时快照。
|
||||
- AI 成功写回后,该快照成为可撤销版本;AI 失败时输入框保留请求前已经写回的 canonical Prompt,清除本次临时快照,不生成新的可撤销版本,也不恢复发起本次操作时已经被替换的旧快照。
|
||||
- 用户手动编辑 AI 结果后,撤销仍可用。再次发起 AI 操作时,先规范化并写回当时的当前 Prompt,再以该 canonical Prompt 替换旧快照。
|
||||
- 点击预设时先规范化并写回,再清除旧快照;正式提交被后端拒绝时,保留已经写回的 canonical Prompt 和提交前已有快照。
|
||||
- 点击撤销时,先把当前输入规范化并写回,再让该 canonical Prompt 与 canonical 快照互换;按钮继续可用,允许用户在两个规范化版本间来回切换。
|
||||
- 只保存一层,快照只包含 canonical Prompt,不包含预设滚动位置、展开状态、模型、时长或其它参数。
|
||||
|
||||
撤销按钮的可见性与可用性:
|
||||
|
||||
- 可见条件为存在可撤销快照或本次 AI 操作的临时快照;启用条件为可见且当前 BGM 面板未处于 `completing`、`simplifying`、`submitting` 或现有 `generating` 锁定状态。发起 AI 操作时可撤销快照被本次临时快照取代,因此“处理中没有可撤销快照”不等于“没有快照”,不得据此隐藏按钮。
|
||||
- AI 处理期间按钮显示并禁用,不得隐藏。禁用必须使用真实禁用态并保留按钮在 DOM 与可访问树中的位置,不得用隐藏、透明度或其它视觉伪装代替,也不得在处理前后改变按钮占位。
|
||||
- 完全没有快照时隐藏,不显示灰色占位按钮。
|
||||
|
||||
| 状态 | 可撤销快照 | 本次临时快照 | 撤销按钮 |
|
||||
| --- | --- | --- | --- |
|
||||
| 初始进入面板、没有历史快照 | 无 | 无 | 隐藏 |
|
||||
| AI 开始处理,首次或再次均相同 | 无 | 有 | 显示并禁用 |
|
||||
| AI 处理成功并写回输入框 | 有 | 无 | 显示并启用 |
|
||||
| AI 处理失败、原文未改变 | 无 | 无 | 隐藏 |
|
||||
| 用户手动编辑 AI 结果 | 有 | 无 | 显示并启用 |
|
||||
| 点击预设词条 | 无 | 无 | 隐藏 |
|
||||
| 点击生成提交且已有快照 | 有 | 无 | 显示并禁用 |
|
||||
| 点击生成提交且没有快照 | 无 | 无 | 隐藏 |
|
||||
| 提交成功或失败后解除锁定 | 有或无 | 无 | 有快照时显示并启用,否则隐藏 |
|
||||
| 点击撤销 | 有,与当前文本互换 | 无 | 保持显示并启用 |
|
||||
|
||||
- 再次发起 AI 操作时仍按上面的规则用当时的 canonical Prompt 替换旧快照;不得为了让处理期间按钮可见而保留旧快照,那会与“AI 失败不恢复已被替换的旧快照”冲突,并让失败后的撤销指向不相关内容。
|
||||
- 视图只按上表决定显示与启用;是否真正执行撤销仍由状态层在非 `idle` 或无快照时拒绝,两处不得各写一套判定。
|
||||
|
||||
### AI 操作与方案 A 提交锁
|
||||
|
||||
当前 BGM generation dialog 的 Prompt 助手状态为:
|
||||
|
||||
```text
|
||||
idle
|
||||
├─ completing
|
||||
├─ simplifying
|
||||
└─ submitting
|
||||
```
|
||||
|
||||
- `completing` / `simplifying` 开始时以同步 operation ID 取得当前 dialog 操作权;同一操作的后续双击无效。新操作使旧 operation ID 失效,关闭 dialog 时中止请求,dialog ID 或 operation ID 不匹配的迟到响应必须丢弃。
|
||||
- AI 处理中输入框只读,禁用预设展开/收起、预设词条、滚动箭头、AI 补全、一键简化、撤销和生成。这里的“禁用撤销”指显示并禁用,可见性按“单层撤销”一节的矩阵判定,不得隐藏。
|
||||
- 点击生成的同步事件内必须在任何 `await` 前依次完成:立即把当前 BGM dialog 切换到 `submitting`、计算 canonical Prompt 并写回输入框、按 canonical Prompt 校验 0 个有效字符和 200 字上限、冻结本次请求值。前端校验失败时立即解除锁并保留写回后的 canonical Prompt,不发送请求。提交中锁定该 dialog 的输入框、预设、展开/收起、滚动箭头、AI 操作、撤销、参数控件和生成按钮,忽略后续重复点击。
|
||||
- 锁只作用于当前 BGM dialog,不锁整个画布、其它 generation dialog、画布拖动、缩放、图层操作或其它编辑能力。
|
||||
- 后端拒绝正式提交时,解除锁并保留已经写回的 canonical Prompt 与提交前已有撤销快照,继续使用现有正式生成错误展示。原 dialog 仍是当前面板时恢复面板;用户已归档或切走时只记录失败并保持关闭,不自动激活旧面板。
|
||||
- 后端接受请求并创建正式生成任务后,`submitting` 结束;原账号、项目和 dialog 仍匹配时进入现有 `queued/generating` 占位并隐藏输入 composer。若 dialog 已删除或账号 / 项目已切换,正式任务继续,但旧回调不得按同名 dialog ID 写入新 scope。
|
||||
- 正式任务接受后的异步回调按副作用作用域分别校验:钱包刷新只校验原账号仍是当前账号;任务列表通知同时校验原账号和原项目仍是当前账号与项目;dialog、canvas、asset 和 layer 写回继续校验账号、项目、scope version 与原 BGM dialog。删除 dialog 不得阻止同账号钱包刷新或同账号同项目任务列表通知,切换账号不得让旧任务触发新账号页面回调。
|
||||
- “提交成功”仅表示后端已接受请求并创建正式任务,不表示 Suno 已完成音乐生成。
|
||||
- 上述“接受后切占位”以现役默认 queue 模式的任务创建响应为观察点;兼容 inline 模式没有中间接受响应,沿用现有 HTTP 终态响应作为客户端可观察结算点,不为此新增协议。
|
||||
|
||||
## 画布数据
|
||||
|
||||
@@ -51,10 +223,10 @@
|
||||
- generated 私有音频资源播放前必须通过 `/api/assets/read-url` 换签;画布卡片不得直接把 `/generated-*` 或 generated OSS 私有地址交给 `<audio>` 裸请求。
|
||||
- 音频元数据弹窗使用 `时长`,不使用图片 / 视频的分辨率语义;音频生成占位不显示分辨率或时长角标。
|
||||
- 音频图层右上角标签显示在信息按钮左侧,和其他素材卡右上角信息区保持一致。
|
||||
- 元数据弹窗按音频显示 `音频信息` / `音频类型` / `时长`,生成输入快照只展示用户面板字段。
|
||||
- 元数据弹窗按音频显示 `音频信息` / `音频类型` / `时长`,生成输入快照只展示用户面板字段;BGM 快照保存输入框已经写回的 canonical Prompt,不保存助手系统模板、内部引导或预设元数据。
|
||||
- 音频图层上方浮动工具栏只保留 `改造` 和 `下载按钮`。点击 `改造` 后打开对应的音效或背景音乐生成面板,不展示参考图组件;面板底部模型与参数位置和原生成入口一致,并允许继续修改后再次生成,新结果落在原音频旁边。
|
||||
|
||||
## 前端提交契约
|
||||
## 前端与 BFF 契约
|
||||
|
||||
前端新增两个 BFF client:
|
||||
|
||||
@@ -63,8 +235,7 @@ POST /api/editor/audios/sound-effects/generations
|
||||
{
|
||||
prompt: string,
|
||||
model: "audio1.0",
|
||||
duration: number,
|
||||
priceMudPoints: 10
|
||||
duration: number
|
||||
}
|
||||
```
|
||||
|
||||
@@ -72,11 +243,46 @@ POST /api/editor/audios/sound-effects/generations
|
||||
POST /api/editor/audios/background-music/generations
|
||||
{
|
||||
gptDescriptionPrompt: string,
|
||||
makeInstrumental: true,
|
||||
priceMudPoints: 5
|
||||
makeInstrumental: true
|
||||
}
|
||||
```
|
||||
|
||||
BGM Prompt 助手新增两个登录态内部 BFF:
|
||||
|
||||
```ts
|
||||
POST /api/editor/audios/background-music/prompts/completions
|
||||
{
|
||||
currentPrompt: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
POST /api/editor/audios/background-music/prompts/simplifications
|
||||
{
|
||||
currentPrompt: string
|
||||
}
|
||||
```
|
||||
|
||||
统一 Prompt 助手响应:
|
||||
|
||||
```ts
|
||||
{
|
||||
prompt: string,
|
||||
charCount: number
|
||||
}
|
||||
```
|
||||
|
||||
- 客户端不得提交 `maxChars`、`targetChars`、模型名、预设 ID、分组、颜色、内部模板或格式 / 完整性 / 残句判断;这些由服务端固定或由内部 LLM 结果产生。
|
||||
- 两个助手 BFF 不调用 Suno、钱包扣费、正式 generation queue、OSS、素材库,也不在 handler 中同步执行 SpacetimeDB 业务写入;成功路由的通用 tracking 仍由现有本机 outbox 异步承接。
|
||||
- 两个助手请求中的 `currentPrompt` 必须是前端已经写回输入框的 canonical Prompt;响应 `prompt` 也必须先执行同一 canonicalization,`charCount` 是响应 canonical Prompt 的 Unicode code point 数。
|
||||
- 补全与简化成功响应都只包含上面的 `prompt` / `charCount` 业务字段;内部四字段 envelope、`originalPrompt`、目标字数、业务语义轮信息和未通过候选均不得出现在成功或失败响应中。失败继续使用现有 API 错误 envelope。
|
||||
- 简化 BFF 只接受总字符数为 201–2000 且至少含 1 个有效字符的 canonical `currentPrompt`;超过 2000 返回现有 `400 BAD_REQUEST` 错误 envelope,并标记字段 `currentPrompt`。
|
||||
- 两个助手路由都设置 `32 KiB` HTTP 请求体上限。请求体超过该上限时保留 Axum `413 PAYLOAD_TOO_LARGE`,不得被通用 JSON rejection 降为 `400`;字符上限与原始 body 上限分别校验,不能互相替代。
|
||||
- 不增加 Prompt 助手专属的用户级、IP 级、时间窗口或令牌桶限流,也不新增本功能主动产生的 `429` / `Retry-After`。现有 api-server 全局并发背压、前端防重复操作、Nginx 保护和上游真实 `429` 的安全映射保持不变。
|
||||
- 两个成功路由必须进入 `tracking.rs` 显式静态映射:补全使用 `event_key = editor_background_music_prompt_completion`,简化使用 `event_key = editor_background_music_prompt_simplification`;两者都使用 `module_key = editor`、User scope。普通 route tracking 继续只记录成功响应,不在助手 handler 中新增同步埋点副作用。
|
||||
- BGM 正式提交必须使用输入框已经写回的 canonical Prompt,不得额外拼接用户不可见内容,也不得把空 Prompt 回退为“游戏背景音乐”。
|
||||
- BGM 正式 POST 不使用现有允许 unsafe method 的自动重试,保证一次点击不会由 client 内部重发。本文不新增 Idempotency-Key、持久提交账本或服务端 dedupe;其它生成 mode 的 retry 行为保持不变。
|
||||
|
||||
统一响应:
|
||||
|
||||
```ts
|
||||
@@ -99,11 +305,17 @@ POST /api/editor/audios/background-music/generations
|
||||
}
|
||||
```
|
||||
|
||||
- BGM 成功响应必须同时填充 `prompt` 和 `actualPrompt`,两者都与本次 canonical Prompt 逐 code point 等值,不得携带助手模板、内部引导或隐藏前后缀;共享响应类型为兼容 SFX 与历史数据仍可保留 `actualPrompt` 可选。
|
||||
- 默认 queue 模式的首次生成响应继续只携带现有 `queueState`;上面的完整音频响应是 inline 或 worker 内部完成边界,不要求给 queue 首次响应或普通 metadata-only job result 新增 Prompt 字段。
|
||||
|
||||
## 后端实现
|
||||
|
||||
- 在 `shared-contracts/src/assets.rs` 增加编辑器音频请求 / 响应 DTO。
|
||||
- 在 `shared-contracts` 增加最小 BGM Prompt 助手请求 / 响应 DTO;前后端共享 `currentPrompt`、`prompt` 和 `charCount` 字段,不把内部 LLM 判断暴露给客户端。
|
||||
- 前端、`api-server` 与 `platform-audio` 必须实现同一 BGM canonicalization 语义并复用同一组跨语言测试向量:只删除首尾 Unicode `White_Space`,保留内部空白和全部其它 code point。该操作必须幂等,不得直接混用语义不同的 TypeScript / Rust 原生 `trim`。
|
||||
- 在 `platform-audio` 增加编辑器专用 body builder 和 submit 函数:
|
||||
- 背景音乐 body 使用 `mv`、`gpt_description_prompt`、`make_instrumental`。
|
||||
- 背景音乐在 body builder 边界防御性执行幂等 canonicalization,再按 canonical Prompt 检查至少一个有效字符和最多 200 个 Unicode code point;校验通过后用 canonical Prompt 构造 Suno body。不得复用语义不同的 `normalize_limited_text`,不得提供默认 Prompt。
|
||||
- Suno 音乐接口路径固定为 `/suno/submit/music`;`VECTOR_ENGINE_BASE_URL` 即使配置为带 `/v1` 的图片接口根,也要在 `platform-audio` 中归一为根路径后再拼接,避免误请求 `/v1/suno/submit/music`。
|
||||
- 音效 body 使用 Vidu 文生音频契约:提交 `/ent/v2/text2audio`,请求体包含 `model: "audio1.0"`、`prompt`、`sound: prompt`、`duration` 和可选 `seed`;`model` 和 `prompt` 为文档必填,`sound` 用于兼容线上网关实际校验,`prompt` 最长 1500 字符,`duration` 按 Vidu 文档限制在 `2-10` 秒。
|
||||
- 编辑器音效轮询使用 Vidu 路径 `/ent/v2/tasks/{taskId}/creations`,不再使用 Suno `/suno/fetch/{taskId}`;Suno 文生音效 `task: "sound"` 暂不从编辑器入口暴露。
|
||||
@@ -112,8 +324,17 @@ POST /api/editor/audios/background-music/generations
|
||||
- 在 `api-server` 增加编辑器音频 BFF:
|
||||
- `/api/editor/audios/sound-effects/generations`
|
||||
- `/api/editor/audios/background-music/generations`
|
||||
- BFF 复用现有 `vector_engine_audio_generation` 的任务轮询、下载、OSS 持久化和计费包装;音效 10 泥点,背景音乐 5 泥点。
|
||||
- 在 `api-server` 增加登录态内部 BGM Prompt 助手 BFF:
|
||||
- `POST /api/editor/audios/background-music/prompts/completions`
|
||||
- `POST /api/editor/audios/background-music/prompts/simplifications`
|
||||
- Prompt 助手 BFF 在入站和 LLM 候选出站边界执行 BGM canonicalization;服务端字符数、0 / 1 / 2 个有效字符规则、正式生成 200 字限制和简化 201–2000 字资格都基于 canonical Prompt。助手使用现有编辑器专用 LLM client、`gpt-5.6-luna` 请求模型、固定 `reasoning_effort=medium` 和显式 OpenAI Chat 协议;画布 Agent 其它调用仍使用 `gpt-5.4-mini`。补全固定执行一个业务语义轮,简化按 `180 -> 170` 最多两个业务语义轮,并按“一键简化”章节冻结 `originalPrompt`、派生每轮 `currentPrompt`。`LlmClient` 在单轮内部执行的 transport retry 不计入业务语义轮数,简化第一轮 transport、超时或上游失败不进入 170 字轮。服务端负责模板组装、在正文解析或候选提取前检查 `finish_reason`、canonical 字符校验、对完整 `response.text` 中单个 JSON object 的 `serde_json` 全量解析、补全与简化共用的内部 envelope 校验、执行格式 / 完整性 / 残句三个布尔判断和现有 API 错误 envelope;不自行猜测三个语义判断,也不向客户端返回未通过候选。助手不发送 function tools,不接受 tool call,不从代码块或解释中截取 JSON,不自动修复,也不做运行时双协议 fallback。
|
||||
- `finish_reason` 检查复用并公开 `platform-llm` 现有 API-kind-aware 未完成原因 predicate;不得在 `LlmClient` 全局拒绝普通纯文本响应,也不得改变其它调用方既有的长文本降级行为。
|
||||
- 两个助手路由使用各自的 `32 KiB` body limit,并在 `tracking.rs` 中注册上述 User-scope 成功事件;不增加助手专属限流器、本地额度计数或功能级 `429`。
|
||||
- Prompt 助手继续复用 `LlmClient` 现有失败原文日志行为。本需求不增加请求级日志开关、脱敏、metadata-only 模式或相关上线门禁。
|
||||
- BGM generation BFF 在入站时防御性执行同一幂等 canonicalization,规范化后校验有效字符和 200 字限制。登录态站内 handler 在 queue / inline 分流前把 canonical Prompt 和固定 `make_instrumental:true` 写入本次请求值,确保正式 generation queue 请求载荷、持久化记录和内部完成响应等值;删除空 Prompt 默认回退。该站内收口不改变 External v1 的路由、OpenAPI、Idempotency-Key 或 payload 等值语义。助手模板只用于生成输入框可见候选,不得进入正式队列或 Suno 请求。SFX 继续使用现有规范化、回退、Vidu body 和 1500 字限制。
|
||||
- BFF 复用现有 `vector_engine_audio_generation` 的任务轮询、下载、OSS 持久化和计费包装。客户端不提交 BGM `priceMudPoints`;服务端按现役动态定价配置解析并在入队时冻结本次价格,后续计费、资产成本和内部完成响应复用该冻结值。T5 不修改价格或计费规则。
|
||||
- 生成音频持久化后返回 OSS `objectKey` 与 `assetObjectId`;前端保存素材库时继续使用 `audioSrc` 作为兼容路径,并把 OSS 身份写入素材记录。
|
||||
- 本切片不修改 SpacetimeDB schema,不新增 Prompt 助手持久化表,不向 `/api/external/v1` 暴露助手,也不修改 External v1 OpenAPI 或 `platform-llm` 日志策略。
|
||||
|
||||
## 验收
|
||||
|
||||
@@ -134,3 +355,23 @@ POST /api/editor/audios/background-music/generations
|
||||
- 音频素材浮动工具栏只显示 `改造` 与 `下载按钮`,`改造` 复用对应生成面板且没有参考图组件。
|
||||
- 私有 generated 音频能先换签再预览播放,不出现播放条一直为 `0:00` 的裸路径失败状态。
|
||||
- 刷新后 layout 能恢复音频生成器和音频图层。
|
||||
- BGM 输入框按 canonical Prompt 的 Unicode code point 显示 `0 / 200` 计数和动作状态,但编辑期间不因计数而改写输入框;canonical Prompt 为空时三个动作全部禁止,1 个有效字符且不超限时可以生成但不能 AI 补全,至少 2 个有效字符且不超限时可以 AI 补全,至少含 1 个有效字符且为 201–2000 个 code point 时只能一键简化,超过 2000 时三个动作全部禁止且完整保留文本。200 / 201 个纯 Unicode `White_Space` 原始输入均先归一为空,不能简化。
|
||||
- 三组 30 个 BGM 预设按本文固定文案写入或追加;点击前先删除首尾 Unicode `White_Space` 并写回,再基于规范化结果判断空值和末尾标点。重复点击和追加后超限不得丢失 canonical Prompt,内部空格和内部换行保持原位。
|
||||
- 预设展开后默认无缝慢速循环,桌面端左 / 中 / 右区域分别加速向左、暂停、加速向右,左右箭头同步加速;组件在触摸环境被渲染时可横向滚动并选择词条,本需求不恢复移动端图片画布入口。
|
||||
- AI 补全的输入和候选都先 canonicalize;补全只执行一个业务语义轮,候选必须通过共用四字段 envelope、有效字符、200 字和 `true / true / false` 三个判断后,才写回一条中文 canonical Prompt。失败、空值、结构或判断错误、格式错误或超限结果保留请求前已经写回的 canonical Prompt,错误响应不暴露候选,也不调用 Suno 或扣除正式音乐生成泥点。
|
||||
- 一键简化资格、180 / 170 目标后的实际字符数和候选校验都基于 canonical Prompt;第一次使用冻结的 `originalPrompt`,第二次优先处理第一次成功响应中可提取的非空 canonical 候选,否则回退处理 `originalPrompt`,且两个业务语义轮都以同一 `originalPrompt` 作保真参照。第一轮 transport、超时或上游失败直接失败,不进入第二轮;每轮内部的 `LlmClient` transport retry 不增加业务语义轮数。候选必须同时通过结构、有效字符、200 字、格式、完整性和残句校验;第二次仍失败时保留请求前已经写回的 canonical Prompt,不暴露任一未通过候选,程序不得截断。
|
||||
- OpenAI Chat `finish_reason = length / content_filter` 的正文即使形成合法四字段 JSON 也不能通过或被提取;补全遇到两者均直接失败,简化第一轮 `length` 只以冻结的 `originalPrompt` 进入 170 字轮、第一轮 `content_filter` 直接失败,第二轮遇到任一未完成 reason 都最终失败。缺失、空值和未知自定义 reason 继续执行其余门禁。
|
||||
- Prompt 助手协议测试必须证明请求显式使用 OpenAI Chat 且不发送 function tools;只接受完整 `response.text` 全量解析所得的单个 JSON object。外围 JSON whitespace 可以通过,代码块、前后解释、多个 JSON 值、畸形 JSON、仅能子串提取的 object 和任意 tool call 均失败,不触发自动修复或运行时协议 fallback。
|
||||
- Prompt 助手轮次测试必须区分业务语义轮和单轮内部 transport retry:补全始终只有一个业务语义轮;简化只有第一轮成功返回但候选不合格时才进入 170 字轮,第一轮 transport、超时或上游失败不进入第二轮。
|
||||
- AI 补全和简化成功均产生一层 canonical Prompt 交换式撤销快照;手动编辑后仍可撤销,点击预设清除快照,点击撤销前先规范化当前输入并可在两个 canonical 版本间反复互换。
|
||||
- 撤销按钮按“单层撤销”一节的矩阵逐行验收:初始与无快照时隐藏;AI 处理期间显示并禁用,且仍在可访问树中,不得用隐藏或视觉伪装代替禁用;成功后启用,失败后隐藏,手动编辑后仍启用,点击预设后隐藏;`submitting` 期间有快照显示并禁用、无快照隐藏,解除锁定后按快照恢复启用或隐藏;连续点击撤销在两个版本间互换且保持启用。
|
||||
- AI 操作的旧响应、关闭 dialog 后的响应或其它 dialog 的响应不得覆盖当前 Prompt;同一按钮双击只产生一个有效助手请求。
|
||||
- BGM 点击生成后在首个 `await` 前同步锁定当前 dialog;同一 dialog 快速重复点击只产生一次正式请求、一个生成任务和一次扣费,retryable HTTP 状态或 transport error 也不由 client 自动重发,不锁整个画布或其它 dialog。
|
||||
- BGM 提交期间归档或切走面板不会取消已发出的正式请求;失败时旧面板不抢回焦点。删除 dialog 或切换账号 / 项目后,后端已经创建的正式任务继续处理;钱包回调只允许作用于原账号仍为当前账号的页面,任务列表回调只允许作用于原账号与原项目仍为当前账号与项目的页面,dialog / canvas / asset / layer 写回还必须匹配原 scope version 与原 BGM dialog。本需求不新增跨 scope 的恢复或刷新机制。
|
||||
- BGM 正式提交会删除首尾 Unicode `White_Space` 并同步写回输入框,不回退默认 Prompt;首尾 U+0085 等 `White_Space` 被删除,内部空格和 LF / CRLF 原样保留,U+200B、U+FEFF、组合字符和 ZWJ emoji 不被误删。canonical Prompt 在输入框、BFF、队列载荷、Suno body、生成记录和结果响应中完全一致,且没有用户不可见的前缀、后缀或模板。
|
||||
- BGM 边界测试覆盖 200 / 201 个纯 Unicode `White_Space` 均归一为空并禁止三动作、大量边界空白包围 `A` 后只允许生成、`A` 加 199 个内部空格再加 `B` 后只允许简化、201 个 U+200B 或 U+FEFF 只允许简化、边界空白包围 200 个 `A` 后允许补全和生成,以及 TypeScript 与 Rust 对 U+0085、U+200B 和 U+FEFF 的一致行为。
|
||||
- BGM 助手入口测试覆盖 canonical 2000 字允许简化、2001 字返回 `400` 且不调用 LLM;两个助手路由 body 超过 `32 KiB` 时返回 `413`;连续合法请求不因本功能新增限流器返回 `429`。
|
||||
- 两个助手成功路由分别产生 `editor_background_music_prompt_completion` / `editor_background_music_prompt_simplification` tracking event,均为 `module_key = editor`、User scope;失败响应沿用普通 route tracking 只记录成功的现状。
|
||||
- BGM Suno body 仍只包含 `mv`、`gpt_description_prompt`、`make_instrumental`,固定 `Suno` 胶囊和动态泥点价格不变;SFX 的 Vidu body、默认 Prompt、时长与 1500 字限制无回归。
|
||||
- `audio-sound-effect` 与 `audio-background-music` 必须由同一个音频 composer 渲染,并在组件内通过 `isSoundEffect` 分支。SFX 不得渲染 BGM 控件或尚未实施的 SFX V2 控件,BGM 不得渲染 Vidu 与音效时长控件;两个 mode 相互切换时,菜单、预设滚动、锁和助手状态不得跨分支泄漏。
|
||||
- 本切片的定向前端、shared-contracts、`api-server`、`platform-audio` 和端到端 Prompt 等值测试通过,并执行 `npm run typecheck`、对应 Rust 定向测试、`npm run check:encoding` 与 `git diff --check`。
|
||||
|
||||
@@ -52,21 +52,21 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当
|
||||
6. 小程序外壳注入到 H5 URL 的 `clientType`、`clientRuntime`、`miniProgramEnv` 是宿主上下文,H5 内部 `pushState` / 阶段导航必须跨页面保留,避免登录和充值误判为普通浏览器;首点时微信 JS bridge 可能尚未就绪,前端还需用 `MicroMessenger + miniProgram` User-Agent 作为小程序识别兜底。
|
||||
7. 小程序 `web-view` 页必须启用好友分享与朋友圈分享,分享目标固定回到 `pages/web-view/index`,不把 H5 当前 URL 作为不受控启动参数传回小程序页。
|
||||
8. 小程序 `web-view` 外壳运行时通过 `wx.getAccountInfoSync().miniProgram.envVersion` 自动识别版本:线上版 `release` 使用 `www.genarrative.world`,体验版 `trial` 与开发版 `develop` 使用 `dev.genarrative.world`;传给后端的 `x-mini-program-env` 分别为 `release`、`trial`、`dev`。
|
||||
9. 账号信息面板只展示 `账号信息` 标题;绑定手机号和绑定微信以紧凑模块展示当前绑定状态,已绑定手机号展示完整手机号,已绑定微信优先展示微信平台实际返回并由后端保存的 `wechatDisplayName`。小程序 `jscode2session` 不能直接返回微信昵称或个人微信号,只能稳定拿到当前小程序维度的 `openid`,并在满足微信开放平台条件时拿到 `unionid`;小程序昵称来自快捷登录后按需展示的原生 `input type="nickname"` 提交的 `displayName`。后端下发 `wechatAccount` 作为绑定账号标识,前端在没有真实昵称时展示微信账号尾号,不展示裸“已绑定”。换绑入口放在对应模块右上角,退出登录和退出全部设备固定放在面板内容最底部。
|
||||
9. 账号信息面板只展示 `账号信息` 标题;绑定手机号和绑定微信以紧凑模块展示当前绑定状态,已绑定手机号展示完整手机号,已绑定微信优先展示微信平台实际返回并由后端保存的 `wechatDisplayName`。小程序 `jscode2session` 不能直接返回微信昵称或个人微信号,只能稳定拿到当前小程序维度的 `openid`,并在满足微信开放平台条件时拿到 `unionid`;小程序昵称来自快捷登录后按需展示的原生 `input type="nickname"` 提交的 `displayName`。后端下发 `wechatAccount` 作为绑定账号标识,前端在没有真实昵称时展示微信账号尾号,不展示裸“已绑定”。换绑入口放在对应模块右上角,退出登录和退出全部设备固定放在面板内容最底部。直接打开账号信息时,外层弹窗只负责定位与可访问语义,保持无背景、无边框并允许内层阴影自然溢出;背景、边框、圆角、内容裁切和阴影统一由内层账号卡片承载,避免直角外壳在四角露出不透明底色或把圆角阴影裁成矩形。
|
||||
10. H5 登录态从未登录变为已登录,或从已登录变为未登录后,必须刷新当前页面一次,确保推荐运行态、作品架、个人缓存和私有 query 都按新身份重新初始化;普通 access token 续期、账号资料更新和同一登录态内的设置变化不得触发整页刷新。
|
||||
11. 同一账号允许多端同时在线。新增登录和单设备退出只影响对应 refresh session,不得提升账号级 `tokenVersion` 让其它设备的 access token 失效;只有“退出全部设备”、修改密码、重置密码等明确安全动作才吊销全端 refresh session 并提升 `tokenVersion`。
|
||||
12. 手机号认证只支持中国大陆号码:验证码、密码登录、绑定、换绑和重置密码请求统一提交 `purePhoneNumber` 与可选 `countryCode`,省略国家码时默认 `86`,旧 `phone` 字段不再接受;显式国家码必须为无加号的 `86`,其他值返回“仅支持中国大陆手机号(+86)”。主站输入框保留 `autocomplete="tel"` 与浏览器默认电话号码回填能力,认证 service 把浏览器可能回填的 `+86 1xxxxxxxxxx` 或 `86 1xxxxxxxxxx` 拆成 `{ countryCode: "86", purePhoneNumber: "1xxxxxxxxxx" }` 后提交。微信小程序手机号授权必须使用微信真实返回的 `countryCode + purePhoneNumber`,不得套用普通请求的缺省国家码。后端分别验证国家码和纯号码后再生成 E.164 存储;前端校验只提供即时反馈,`inputMode="numeric"` 也只提示软键盘布局。
|
||||
|
||||
## 账户与充值
|
||||
|
||||
1. 主站和图片画板统一使用公共泥点资产入口。收起态展示“泥点图标 + 泥点总额 | 充值”;桌面端通过 hover / focus 展开,移动端通过点击展开。展开态只展示不限时泥点、每日免费泥点及“每天重置为 20 泥点”,并提供“使用详情”入口;余额都以后端充值中心 read model 为准,前端不得自行相减推算。
|
||||
1. 主站、图片画板和 AI Game Creator 统一使用 `packages/shared` 的依赖注入式钱包 Zustand Store;主站与 Tauri 客户端只保留各自的 URL、认证和重试 transport adapter。主站顶部、图片画板顶部和“我的”统计必须消费同一份 `ProfileMudPointBalance` 快照,泥点总额固定取 `totalPoints`,不得混用 dashboard 的 `walletBalance`;只有充值中心响应缺少 `mudPointBalance` 时,Store 才可按 owner 保存同一响应的 legacy `walletBalance` 作为纯总额兜底。较新的 legacy-only 响应必须原子清除旧 `mudPointBalance`,充值弹窗、空账单、个人中心统计卡与图片画板顶部可读取该总额,但不得据此伪造分桶明细。切换或退出账号必须立即清空快照并拒绝旧账号在途响应;消费端在 owner 绑定 effect 生效前也必须按当前用户 ID 同步屏蔽 owner 不匹配的快照,旧账号请求不得阻塞新账号首次读取。普通充值中心 GET 与会应用余额的异步操作必须在发起时捕获钱包 owner 生命周期、invalidation 版本和 operation sequence;回包只能结算不晚于该版本的刷新,过期快照不得覆盖余额或中止更新的终态刷新。生成、退款、充值、兑换码等余额可能变化事件只通知 Store 合并刷新。external generation 在 worker 领取后才预扣泥点,因此主站必须在任一项目的 active task 轮询期间持续推动钱包合并刷新,并在 `completed / failed` 任一终态再刷新以覆盖成功结算或失败退款;任务列表首次 bootstrap 的 active / terminal 任一读取瞬时失败时必须有界退避重试,任一分支成功结果都应立即保留,成功取得全局 active ID 后再交给常规轮询,不能因 terminal 分支失败而清空 active ID、停止钱包通知。公共泥点资产入口收起态展示“泥点图标 + 泥点总额 | 充值”;桌面端通过 hover / focus 展开,移动端通过点击展开。展开态只展示不限时泥点、每日免费泥点及后端返回的每日重置额度,并提供“使用详情”入口;余额都以后端充值中心 read model 为准,前端不得自行相减推算。
|
||||
2. 账户充值弹窗标题统一为“购买更多泥点”,当前版本只展示泥点商品,不展示会员页签、会员商品、购买会员或升级会员入口。底层会员数据与周期刷新能力继续保留用于存量兼容和结算,会员周期限时泥点不在当前版本前台展示。
|
||||
3. 泥点默认商品固定为四档:`60 泥点 / ¥6`、`180 + 90 泥点 / ¥18`、`300 + 150 泥点 / ¥30`、`680 + 340 泥点 / ¥68`。`60` 档不加赠,后三档首次购买各加赠基础泥点的 `50%`;实际展示、下单校验和支付确认仍以后端返回的充值商品配置为准。
|
||||
4. 首充加赠资格按泥点商品档位独立计算。用户买过 `points_180` 后,只影响 `points_180` 的首充展示和结算,其它未购买档位仍保留各自首充加赠资格。
|
||||
5. 前端不得用 `hasPointsRecharged` 统一隐藏所有泥点档位首充权益;该字段只表示账号是否发生过任一泥点充值。
|
||||
6. 充值支付渠道只允许由设备平台隔离层解析为 `wechat_mp`、`wechat_mp_virtual`、`wechat_jsapi`、`wechat_h5` 或 `wechat_native`;生产真实支付不得默认落到 `mock`,缺失或未知 `paymentChannel` 必须拒绝。
|
||||
7. 小程序 WebView 充值使用 `wechat_mp_virtual` 调起小程序虚拟支付;微信内浏览器使用 `wechat_jsapi` 调起微信支付 JSAPI;普通 Web 使用 `wechat_native` 二维码支付,避免因移动 UA、触控能力或窄屏误入 `wechat_h5`。只有微信通知或查单确认 `SUCCESS` 后才刷新余额或会员状态。
|
||||
8. 后端必须按 access JWT 中的最小设备快照拦截真实微信充值路径,不能只依赖前端隐藏入口或请求体传入的 `paymentChannel`。
|
||||
7. 小程序 WebView 充值使用 `wechat_mp_virtual` 调起小程序虚拟支付;微信内浏览器使用 `wechat_jsapi` 调起微信支付 JSAPI;普通 Web 使用 `wechat_native` 二维码支付,避免因移动 UA、触控能力或窄屏误入 `wechat_h5`。只有微信通知或查单确认 `SUCCESS` 后才刷新余额或会员状态。一次充值从下单、宿主 / JSAPI / H5 / Native 调起到查单确认与 watch 必须始终携带同一个 `ownerUserId + account lifecycle revision`;pending / confirming order、二维码、提交状态、错误结果、成功回调以及清 token、重新登录等认证副作用在每次写入前都必须校验该令牌,账号切换或卸载后旧链路不得再影响新账号。
|
||||
8. 后端必须按 access JWT 中的最小设备快照拦截真实微信充值路径,不能只依赖前端隐藏入口或请求体传入的 `paymentChannel`。前端下单入口还必须使用同步 operation token 防止同一 React 提交周期内重复创建订单;充值下单、邀请码兑换和奖励码兑换必须绑定同一个账号生命周期 `AbortSignal`,切号或卸载时先中止旧 signal,禁止 POST 的 401、503 或网络重试重新读取新账号 Token。共享 access token refresh 还必须按账号认证代际与发起时 token 快照隔离:旧代际成功回包不得发布 token,旧代际失败不得清理新账号 token,新账号不得复用旧账号的在途 refresh Promise。账号切换时,充值、账单、邀请码中心、邀请码输入、弹窗和在途读取结果都必须按账号生命周期整体失效。
|
||||
9. 后台“充值商品”页继续维护泥点和会员商品配置,保存后影响新的充值中心快照、下单和支付确认;历史订单保留下单时快照。会员商品配置保留不表示当前版本开放公开购买或升级入口。
|
||||
|
||||
## 唯一后端路线
|
||||
|
||||
Reference in New Issue
Block a user