合并画布 Agent 侧边栏聊天
# Conflicts: # docs/project-memory/shared-memory/decision-log.md # server-rs/crates/spacetime-client/src/module_bindings.rs
This commit is contained in:
+12
@@ -16,6 +16,18 @@ _Avoid_: 在玩法页面内手写上传、参考图、重绘、预览、删除
|
||||
独立 `/editor` 中可保存、恢复和继续编辑的图片画布工作状态,包含画布视图、图层布局和资源引用;用于多图对比、生成结果衍生和画布级编辑,不替代玩法页面内的单图资产编辑。
|
||||
_Avoid_: 玩法结果页单图槽位、发布态作品、只存在前端内存里的临时画布
|
||||
|
||||
**画布Agent对话**:
|
||||
图片画布工程右侧的对话式编辑器工具,用户通过自然语言调度画布已有的图片类生成与编辑能力(生成图片、生成角色形象、生成图标素材、生成 UI 设计图、基于附件的图片修改),并可附加画布素材或素材库图片作为参考;对话归属单个图片画布工程,可保存历史、新开会话和软删会话。属于画布域工具,不承接玩法创作、不产出玩法作品或模板,与「表单/图片输入创作工作台」的 Avoid 边界不冲突。
|
||||
_Avoid_: 对话式玩法创作工作台、绕过模型定价收口的生成入口、把对话消息当作画布布局真相、复用拼图专用 creative-agent 内存会话
|
||||
|
||||
**画布Agent会话记录**:
|
||||
画布Agent对话的持久化形态:SpacetimeDB 表只存会话元数据(会话 ID、所属工程、属主、标题、软删标记、聊天记录 OSS 对象引用、时间戳),完整消息内容以会话粒度 JSON 对象存 OSS,追加消息即整体重写对象。
|
||||
_Avoid_: api-server 内存会话、消息全文入 SpacetimeDB 表、对话混入工程布局快照、每条消息一个 OSS 对象
|
||||
|
||||
**画布Agent对话附件**:
|
||||
画布Agent对话消息携带的图片参考,统一为画布资源 / 素材库对象引用(resourceId / assetId + 可选 objectKey),单条消息上限 9 张;上传图片若从对话入口进入,必须复用素材库 / 画布资源登记链路,在上传格未落地前只从已有画布资源和账号素材库选择,不存在只属于对话的第三种图。
|
||||
_Avoid_: 对话私有图片副本、内嵌 base64 附件、音视频附件
|
||||
|
||||
**画布资源**:
|
||||
图片画布工程中可被一个或多个图层引用的图片资源记录,保存 OSS 对象引用、上传 / 生成来源、提示词、模型、任务和尺寸等资源元数据;同一资源可以在工程布局中出现多次。
|
||||
_Avoid_: 图层位置、前端 hover / selected 状态、直接内嵌图片二进制
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- [审计与复盘](./audits/README.md):工程审查、文本/乱码审计、专项落地审计。
|
||||
- [系统设计](./design/README.md):玩法、关系、物品与对话设计。
|
||||
- [技术方案](./technical/README.md):动画、服务端、外部产品形态拆解。
|
||||
- [架构决策](./adr/):记录已经接受的跨模块、长期有效架构取舍;对应长期摘要仍应同步到 `docs/project-memory/shared-memory/decision-log.md`。
|
||||
- [规划与优先级](./planning/README.md):当前阶段的迭代排序与落地优先级;创作流程统一总计划见 [【玩法创作】创作流程统一总计划-2026-05-30.md](./planning/%E3%80%90%E7%8E%A9%E6%B3%95%E5%88%9B%E4%BD%9C%E3%80%91%E5%88%9B%E4%BD%9C%E6%B5%81%E7%A8%8B%E7%BB%9F%E4%B8%80%E6%80%BB%E8%AE%A1%E5%88%92-2026-05-30.md)。
|
||||
- [参考目录](./reference/README.md):脚本/Function 速查入口。
|
||||
重点补充:RPG 创作与运行时脚本职责地图见 [RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md](./reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md)。
|
||||
@@ -32,6 +33,8 @@ Expo React Native 移动壳和 Tauri 桌面壳的工程结构、同源 WebView
|
||||
|
||||
`/editor/canvas` 图片画布编辑器的画布素材 ZIP 导出能力,入口放在右上角标题栏下载图标内,采用前端 JSZip 打包画布中有效图层引用的上传图、生成图、修改结果和角色动作序列帧;动作图层右键“导出为”提供序列帧 ZIP(含前端生成的 `preview.gif`)与 Spine JSON ZIP 两个二级选项,方案见 [【前端架构】图片画布素材导出方案-2026-06-15.md](./technical/【前端架构】图片画布素材导出方案-2026-06-15.md)。
|
||||
|
||||
`/editor/canvas` 右侧画布 Agent 对话面板、会话持久化、SSE 事件、附件与生成落画板例外见 [【编辑器】画布Agent对话面板-2026-07-03.md](./【编辑器】画布Agent对话面板-2026-07-03.md);消息正文存 OSS、元数据进 SpacetimeDB 的取舍见 [【ADR】画布Agent会话消息存OSS-2026-07-03.md](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)。
|
||||
|
||||
图片画布生成类面板的模型泥点默认 JSON、运行时 override、后台“模型定价”页面和主站动态下发口径见 [【编辑器】模型定价配置管理方案-2026-06-22.md](./%E3%80%90%E7%BC%96%E8%BE%91%E5%99%A8%E3%80%91%E6%A8%A1%E5%9E%8B%E5%AE%9A%E4%BB%B7%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88-2026-06-22.md)。
|
||||
|
||||
React 组件测试的用户行为、稳定契约、hook / model 分层断言口径,以及避免内部 DOM 探针、图标 class 和完整对象快照式断言的规则见 [【前端测试】React组件测试准则-2026-06-26.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%B5%8B%E8%AF%95%E3%80%91React%E7%BB%84%E4%BB%B6%E6%B5%8B%E8%AF%95%E5%87%86%E5%88%99-2026-06-26.md)。
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# 【ADR】画布Agent会话消息存OSS-2026-07-03
|
||||
|
||||
状态:已接受
|
||||
|
||||
## 背景
|
||||
|
||||
画布Agent对话需要保存历史记录并支持新开会话。消息正文是流式追加、体量随对话增长的 JSON 数据;SpacetimeDB 是后端唯一结构化真相存储,但表行不适合承载不断增长的长文本;画布工程快照已有独立保存链路,且对话流式更新频率远高于布局保存。
|
||||
|
||||
## 决策
|
||||
|
||||
- SpacetimeDB 表 `editor_agent_conversation` 只存会话元数据:会话 ID、projectId、ownerUserId、标题、软删标记、聊天记录 OSS 对象引用、时间戳。
|
||||
- 完整消息内容以会话粒度 JSON 对象存 OSS(`editor-agent/{conversationId}.json`),追加消息即整体重写对象。
|
||||
- `editor-agent/` 是服务端内部消息文档前缀,只保存 `editor-agent/{conversationId}.json` 形态的会话级 JSON 文档;它不是浏览器直传前缀,也不是公开 generated 资源前缀。
|
||||
- 浏览器禁止直接上传、覆盖或签名写入 `editor-agent/` 对象;前端只通过 `api-server` 的会话接口创建会话、发送消息、读取历史,OSS 读写由服务端完成。
|
||||
- 消息文档序列化后的读写上限为 2 MiB;超过上限时后端拒绝继续读写该会话消息文档,并返回 payload too large 语义错误。
|
||||
- 同一会话内的消息追加采用 `conversationId` 级串行锁,避免同一会话的“读-改-写”整对象过程互相覆盖。
|
||||
- Agent 工具调用失败时,仍保留 assistant 消息和 `failed` generation record,包括 tool call、模型、错误信息等排障字段;失败记录是会话历史的一部分,不因 SSE 已返回错误而丢弃。
|
||||
- 不把消息明细写入 SpacetimeDB 表,不把对话混入画布工程快照,不在 api-server 内存中保存会话真相。
|
||||
|
||||
## 备选方案与取舍
|
||||
|
||||
1. **独立 message 表(逐条入 SpacetimeDB)**:查询灵活,但消息文本长、附件结构嵌套,行数与行体积随聊天无界增长,挤占 SpacetimeDB 订阅与快照成本;对话消息没有跨会话结构化查询需求,放表里收益低。
|
||||
2. **消息塞进画布工程快照 payload**:省一张表,但每条消息都会触发整个工程快照保存,与画布布局保存互相竞争,流式期间冲突概率高。
|
||||
3. **每条消息一个 OSS 对象**:追加成本最低,但加载历史需要 N 次取对象或额外清单维护;对话消息量级小,整体读写实现最简单。
|
||||
|
||||
选择"表存引用 + OSS 存整段 JSON":与仓库既有「画布资源」「敲击音效」等 OSS 对象引用模式一致,加载历史一次取对象即可。
|
||||
|
||||
## 影响
|
||||
|
||||
- 消息写入是"读-改-写"整对象;当前由 `api-server` 按 `conversationId` 做会话内串行化,保证同一进程内同一会话不会并发覆写。若未来横向多实例部署,需要补充分布式锁、对象版本条件写或等价的跨实例并发控制。
|
||||
- 会话表仍只保存元数据和 `messagesObjectKey`;API 可以在读取时把 OSS 消息文档拼装为会话详情返回,但完整消息正文的持久化真相仍是 OSS JSON 文档。
|
||||
- 消息文档最大 2 MiB;该限制用于阻止单个会话无限增长。后续如果需要更长历史,应引入归档、分页对象或摘要压缩,不应把正文回填进 SpacetimeDB 表。
|
||||
- 会话软删只打表标记,OSS 对象保留,便于恢复与审计。
|
||||
- 工具生成失败也会写入消息文档:assistant 消息可标记为失败 / 错误类型,`generations` 中保留 `failed` record、模型和错误信息,便于用户回看失败原因和后续排障。
|
||||
- 若未来出现跨会话消息检索需求,需另建投影或索引,不回退为消息入表。
|
||||
@@ -48,6 +48,14 @@
|
||||
- 验证方式:运行 `cargo test -p module-ai --manifest-path server-rs/Cargo.toml`、定向 api-server editor project 测试、编码检查和 `git diff --check`。
|
||||
- 关联文档:`docs/openapi/genarrative-external-v1.openapi.json`。
|
||||
|
||||
## 2026-07-03 画布Agent会话元数据入 SpacetimeDB、消息正文存 OSS
|
||||
|
||||
- 背景:图片画布工程需要对话式编辑历史,但消息正文随流式输出增长,不适合放入表行或画布布局快照;同时画布 Agent 只属于编辑器画布域,不能复用拼图 `creative-agent` 内存会话。
|
||||
- 决策:新增 `module-editor-agent` 承载纯领域规则,`editor_agent_conversation` 只保存会话元数据,完整消息以 `editor-agent/{conversationId}.json` 会话粒度存 OSS;`api-server` 负责编排 LLM、SSE、OSS 读写和既有生成工具调用。
|
||||
- 影响范围:图片画布右侧 Agent 面板、`shared-contracts` / `packages/shared` 的 `editorAgent` 契约、`spacetime-module` / `spacetime-client`、`platform-oss` 内部读签名边界、画布生成落板规则。
|
||||
- 验证方式:`npm run spacetime:generate`、`npm run check:spacetime-schema`、`cargo test -p module-editor-agent --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server --manifest-path server-rs/Cargo.toml editor_agent`、前端 Agent 面板与 SSE client 定向测试、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/【编辑器】画布Agent对话面板-2026-07-03.md`、`docs/adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 2026-07-01 认证工作集只经 typed projection 同步正式表
|
||||
|
||||
- 背景:同手机号重复账号、兑换码白名单错配和微信资料不回写暴露出 `module-auth` 内存工作集、`auth_store_snapshot` 和正式认证表之间仍有历史互刷路径;旧 JSON 快照会把过期手机号索引或用户资料重新带回运行态。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 文档地图与阅读索引
|
||||
|
||||
更新时间:`2026-06-22`
|
||||
更新时间:`2026-07-03`
|
||||
|
||||
## 当前文档入口
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
| 微信小程序虚拟支付 | `docs/【技术方案】微信虚拟支付接入-2026-05-26.md` |
|
||||
| UI 像素资产与 9-slice 规范 | `UI_CODING_STANDARD.md` |
|
||||
| 图片画布生成面板与模型定价 | `docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md`、`docs/【编辑器】模型定价配置管理方案-2026-06-22.md` |
|
||||
| 图片画布右侧 Agent 对话、会话消息 OSS 持久化、SSE 事件契约、与任务侧栏 / 左侧栏互斥规则 | `docs/【编辑器】画布Agent对话面板-2026-07-03.md`、`docs/adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md` |
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- `readSseStream` 负责读取 `Response.body`、识别 `\n\n` 与 `\r\n\r\n` 事件边界、合并多行 `data:`、flush `TextDecoder` 尾部缓冲,并支持事件处理函数返回 `false` 后取消 reader。
|
||||
- `readSseJsonStream` 只在传输事件基础上解析 JSON object,空 data 与异常 JSON 继续按旧口径静默跳过。
|
||||
- 各业务 client 只保留领域事件归一化、最终结果聚合和中文错误文案,不再重复实现 SSE 边界扫描、reader 循环或 UTF-8 flush。
|
||||
- `src/services/image-editor/editorAgentSse.ts` 消费画布 Agent 的 JSON SSE,只校验并归一 `stage`、`message_delta`、`tool_started`、`tool_completed`、`generation_result`、`error`、`done` 事件,不持有 reader 循环或事件边界解析。
|
||||
- OpenAI 兼容流、`[DONE]` 哨兵或其它非 JSON SSE 可直接使用 `readSseStream`;业务 JSON 事件优先使用 `readSseJsonStream`。
|
||||
|
||||
## 落地范围
|
||||
@@ -25,12 +26,27 @@
|
||||
- `src/services/visual-novel-runtime/visualNovelRuntimeSse.ts`
|
||||
- `src/services/rpg-entry/rpgProfileClient.ts`
|
||||
- `src/services/llmClient.ts`
|
||||
- `src/services/image-editor/editorAgentSse.ts`
|
||||
|
||||
后续新增 SSE client 时不得复制 `findSseEventBoundary`、`parseSseEventBlock` 或手写 reader 循环;若确实需要特殊 framing,应先扩展 `sseStream.ts` 的传输能力,再在业务 client 中处理领域语义。
|
||||
|
||||
## 画布 Agent SSE client 口径
|
||||
|
||||
`src/services/image-editor/editorAgentSse.ts` 是画布 Agent 的业务事件归一层,只允许做以下事情:
|
||||
|
||||
- 调用 `readSseJsonStream(response, handler)` 读取 JSON SSE。
|
||||
- 白名单识别 `stage`、`message_delta`、`tool_started`、`tool_completed`、`generation_result`、`error`、`done`。
|
||||
- 支持两种兼容输入形态:优先使用 SSE `event:` 字段作为事件名;若 `data` 内含 `{ "event": "...", "data": {...} }`,则归一为同一 `EditorAgentSseEvent`。
|
||||
- 对未知事件名、缺失 `data` 或非对象 JSON 静默跳过,保持业务 client 的容错口径。
|
||||
|
||||
`editorAgentSse.ts` 不得实现 `Response.body.getReader()`、`TextDecoder`、SSE 边界扫描、JSON 字符串拼接、fetch、鉴权、错误文案、会话状态聚合或画布聚焦逻辑;这些职责分别留在 `sseStream.ts`、`editorAgentClient.ts`、`useEditorAgentConversation.ts` 和画布视图层。
|
||||
|
||||
## 验收
|
||||
|
||||
- `src/services/sseStream.test.ts` 覆盖 CRLF / LF 边界、UTF-8 尾部 flush、异常 JSON 跳过和提前停止取消 reader。
|
||||
- `src/services/llmClient.test.ts` 覆盖 OpenAI 兼容文本流、异常 JSON 跳过和 `[DONE]` 后提前停止。
|
||||
- `src/services/image-editor/editorAgentSse.test.ts` 覆盖画布 Agent 的 `stage`、`generation_result`、`done` 事件解析,以及 `{ event, data }` fallback 形态归一。
|
||||
- `src/services/image-editor/editorAgentClient.test.ts` 覆盖会话 CRUD 路由和 `/messages/stream` 发起后能收到类型化 SSE 事件。
|
||||
- 后续修改 `editorAgentSse.ts` 时,若出现手写 reader、`TextDecoder`、SSE 边界扫描或 fetch 逻辑,应视为违反本收口约定。
|
||||
- 已有 OpenAI 兼容文本流、NPC 聊天流、创作 Agent、创意互动 Agent、视觉小说运行态和充值订单状态测试继续通过。
|
||||
- `npm run typecheck` 不产生新的类型错误。
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
- 图片快速编辑面板只保留一个提示词输入框和模型选择,不展示额外参考图或比例 / 尺寸控件;原图 / 原素材作为 `/api/editor/images/edits` 的 `sourceImageSrc` 直接提交,不作为 `referenceImageSrcs`。打开快速编辑时画布必须自动平移缩放,让原素材完整落在可视区上半部分,底部面板固定出现在素材下方且不遮挡内容,竖屏 UI 素材也必须完整展示。快速编辑右侧显示矩形、椭圆、画笔框选工具,但进入时不默认启用;点击工具后显示选中态,再点同一工具取消启用。完成框选后,画布红色细框显示连续序号,提示词可按这些编号填写每个区域怎么改。点击 `修改` 后仍停留在当前快速编辑面板显示修改中,不创建独立 `Quick Edit Generator` 画布占位;生成成功后直接用结果覆盖原图图层,失败时保留当前面板并显示错误。
|
||||
- 底部生成类按钮每次点击都必须创建独立的画布生成对象;新建规范、角色形象或图标素材时,只切换当前编辑面板,不得销毁此前尚未生成或已生成后的其它生成对象状态。归档为非当前编辑对象的生成占位仍可拖动、删除和等待异步完成,完成 / 失败回写必须按生成对象 ID 读取最新占位状态,不能使用提交瞬间的旧快照。
|
||||
- 画布右上角提供自动隐藏任务侧栏。列表为空且侧栏关闭时只保留图标开关;生成或去背景任务进入时默认打开;用户可手动切换开关状态。
|
||||
- 画布底部工具栏 / 面板 Dock 提供“画布 Agent”入口。点击后打开右侧独立 Agent 对话面板;桌面端为右侧窄面板,移动端占满可用宽度。该面板与素材侧栏、图层侧栏、右上角任务侧栏互斥,打开 Agent 时必须收起其它侧栏,打开其它侧栏或任务侧栏时也必须收起 Agent。Agent 面板不得在当前画布内容下方追加内联内容,也不默认展示大段功能说明文案。
|
||||
- 所有会新建画布生成占位的入口必须先创建 draft,再统一经过 `ImageCanvasGenerationPlacementModel` 计算落点,禁止各入口自行使用当前视口中心裸坐标或原图右侧固定偏移。当前覆盖入口包括 `生成图片`、`生成规范`、`生成角色形象`、`生成图标素材`、`生成视频`、`生成UI设计图` 和 `生成角色动作`。placement 模型的避让对象为所有未隐藏画布图层,以及当前 active / inactive generation dialogs 中仍存在的 placeholder;每个避让矩形按 32px 画布世界坐标间距外扩。候选落点以当前视口世界中心为距离目标,优先选择离视口中心最近且不重叠的占位位置;若中心被占用,会按上下左右和环形候选继续寻找。打开生成面板时必须把避让后的 placeholder 写入 `openCanvasGenerationDialog(...)`,并立即调用 `centerViewportOnPlacement(...)` 居中到新占位中心,保持原 viewport scale 不变;图片快速编辑不属于新建占位入口,提交后覆盖源图。
|
||||
|
||||
## 交互规则
|
||||
@@ -54,6 +55,8 @@
|
||||
- 登录态上传和生成结果必须先落 OSS / asset object,再向 `editor_project_resource` / `editor_asset` 写入轻量 `imageSrc: "/<objectKey>"`、`objectKey` 和 `assetObjectId`;未登录演示态可以在内存里使用 Data URL 预览,但项目、素材库、项目资源和 `editor_canvas.layers_json` 不得写入 `data:image/*`、`data:video/*`、`data:audio/*` 或 `blob:`。旧数据读取时如果已有 `objectKey`,`imageSrc` 归一成 `/<objectKey>`;没有 `objectKey` 的旧 Data URL 需要走修复上传并回写轻量引用。上传到生成面板参考图槽位的图片必须先创建 `editor_project_resource` 行;没有当前工程 ID 时才创建账号级 `editor_asset` 行,随后把对应 `resourceId` 或 `assetId` 写入参考图临时状态,生成请求仍使用临时状态中的图片源或 `objectKey`。
|
||||
- 资源表保存资源和素材级元数据;图层位置、层级、分组选中所需 ID 和 groupId 保存在 `editor_canvas` 的布局 JSON。布局 JSON 是混合数组:普通图层按 `layerId/resourceId` 保存,生成器占位和生成器对话框按 `itemType: "generation-dialog"` 保存,不新增单独表。普通图层的新保存不再把 `assetKind/generationInputs` 写入布局 JSON;刷新时优先从 `editor_project_resource` 恢复,旧布局中的同名字段只作为兼容兜底。生成器快照必须包含生成器 ID、模式、提示词、参数、参考图、状态、占位框位置和可选 `generatedLayerId`;宣发素材生成器还必须保存并恢复 `publicationWorkflowId`、`publicationGameInfo` 和 `publicationReferences`,避免刷新后生成卡片字段或参考图丢失。生成器快照中的参考图同样只保存 `resourceId/sourceAssetId` 行引用和展示所需 label,不保存图片 Data URL、signed URL 或 `objectKey`;刷新时用 `editor_project_resource` / `editor_asset` 行恢复临时生成请求所需图片源。生成成功后仍保存该快照,只是渲染时由 `generatedLayerId` 锚定到成品图层而不重复显示灰色占位框。`generationInputs.references` 是用户可见输入快照中的行级索引,只允许保存 `{ title, label, refType, refId }`;生成接口所需的图片 Data URL、signed URL 或 `objectKey` 只存在于提交前的临时参考图状态和请求体字段,不进入资源 / 素材元数据。图层展示尺寸不再作为独立 `Size` 真相保存,刷新与新建图层均按 `Resolution`(`originalWidth/originalHeight`)原分辨率显示。图层组第一版是画布内布局语义,不单独建表。
|
||||
- 图片类、生成视频和音频结果除作为 `editor_project_resource` 和画布图层保存外,还要写入账号级 `editor_asset` 素材库;该写入由生成 BFF 在请求携带 `assetFolderId` 时完成。`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 下,画布图片、改图、图标素材、UI 素材提取、角色动作、视频、音效和背景音乐生成都先返回 `queueState`,前端轮询 `/api/runtime/external-generation/jobs/{jobId}` 到完成后重新读取项目快照;`inline` 或无项目上下文时才使用响应中的 resource / asset 快照做本地落画布兜底,不再把同一生成结果二次调用素材创建接口。生成请求失败、inline 完成或 queue 任务终态完成 / 失败后,右上角泥点 chip 必须通过 `/profile/dashboard` 回读余额,不做本地乐观扣减。生成视频会单独抽取首帧封面并写入 `thumbnailSrc`,素材栏和拖回画布时沿用该封面作为 poster。
|
||||
- 画布 Agent 会话按“SpacetimeDB 元数据 + OSS 消息正文”存储:`editor_agent_conversation` 只保存 `conversationId/projectId/ownerUserId/title/messagesObjectKey/deleted/createdAt/updatedAt` 等会话元数据;消息正文整体保存为私有 OSS JSON 文档 `editor-agent/{conversationId}.json`。消息文档单对象上限为 2 MiB,同一会话的消息追加和 SSE 最终写回由 api-server 按 `conversationId` 串行化,避免“读 OSS → 改消息 → 写 OSS”并发覆盖。前端只通过 api-server BFF 读取和发送会话,不直接读写 SpacetimeDB,也不直接读写 OSS。
|
||||
- Agent 消息附件只允许引用当前工程画布资源或账号素材库图片,来源类型为 `canvas_resource` / `library_asset`,最多 9 张。附件请求可携带展示用 `imageSrc/thumbnailSrc/objectKey/width/height/label`,但持久化真相仍以后端校验后的 resource / asset 行和 OSS 对象为准;不得把 Data URL、signed URL 或 blob URL 当作会话长期事实。
|
||||
- 前端不直接订阅 SpacetimeDB,统一通过 api-server 的 `/api/editor/projects*` BFF 读写。
|
||||
- 工程刷新恢复可先应用 session 级轻量项目快照缓存,让画布和素材 chrome 尽快显示;缓存快照必须排除 `data:*` / `blob:` 内联媒体,且在后端项目快照返回前不得触发自动保存。后端快照回来后覆盖本地缓存显示并恢复正常保存队列。
|
||||
- 未登录用户可以使用本地演示态,但不触发工程自动保存;真实图片生成 / 修改需要登录。编辑器 API 请求允许使用 refresh cookie 静默补 access token,但 401 / 403 只在编辑器局部提示登录,不清空整站登录态,也不把后端 requestId 直接作为生图弹窗主文案。
|
||||
@@ -68,6 +71,11 @@
|
||||
- `PATCH /api/editor/projects/{projectId}/metadata`:重命名指定工程。
|
||||
- `DELETE /api/editor/projects/{projectId}`:删除指定工程,并级联删除默认画布和资源元数据。
|
||||
- `POST /api/editor/projects/{projectId}/resources`:创建画布资源记录,接收上传资源或真实生成资源元数据。
|
||||
- `GET /api/editor/projects/{projectId}/agent-conversations`:读取当前工程的画布 Agent 会话列表,按更新时间倒序返回会话摘要。
|
||||
- `POST /api/editor/projects/{projectId}/agent-conversations`:在当前工程下创建画布 Agent 会话;可选传入标题,默认标题为“新对话”。
|
||||
- `GET /api/editor/agent-conversations/{conversationId}`:读取指定画布 Agent 会话详情,返回会话摘要和 OSS 消息正文中的消息列表。
|
||||
- `DELETE /api/editor/agent-conversations/{conversationId}`:软删除指定画布 Agent 会话,并返回删除后的会话摘要。
|
||||
- `POST /api/editor/agent-conversations/{conversationId}/messages/stream`:发送画布 Agent 消息并返回 JSON SSE。请求体包含 `clientMessageId`、`text` 和可选 `attachments`;文本与附件不可同时为空。SSE 事件名固定为 `stage`、`message_delta`、`tool_started`、`tool_completed`、`generation_result`、`error`、`done`。生成结果中的图片通过后端写入 project resource / 账号素材后返回轻量快照,前端只消费返回的资源引用并刷新 / 聚焦画布。工具失败时也必须保留失败 generation record,不得只返回一次性错误。
|
||||
- `GET /api/editor/assets/library`:读取当前账号的素材文件夹和素材。首次读取时自动创建“项目素材”默认文件夹。
|
||||
- `POST /api/editor/assets/folders`:新建素材文件夹。
|
||||
- `PATCH /api/editor/assets/folders/{folderId}`:重命名、折叠 / 展开素材文件夹。
|
||||
@@ -91,6 +99,8 @@
|
||||
- 示例素材可继续复用 `public/creation-type-references/` 下的站内图片;用户上传和后续生成资源必须通过资源记录表达。
|
||||
- 不把 hover、dragging、临时吸附线、Space 临时抓手等瞬时 UI 状态写入后端。
|
||||
- 不在 UI 中加入大段功能说明,编辑器界面只展示必要的工具、素材和状态信息。
|
||||
- 画布 Agent 前端分层固定为:`editorAgentClient.ts` 负责 BFF 请求、鉴权错误和 stream 发起;`editorAgentSse.ts` 只负责 SSE JSON 事件归一;`useEditorAgentConversation.ts` 负责会话列表、当前会话、消息流、阶段和停止状态;`EditorAgentConversationPanelView.tsx` 只负责右侧面板展示与交互。
|
||||
- `editorAgentSse.ts` 必须复用 `src/services/sseStream.ts` 的 `readSseJsonStream`,不得复制 SSE reader、`TextDecoder`、事件边界扫描或 UTF-8 flush 逻辑;该文件不得承接 fetch、鉴权、会话聚合、画布聚焦或 UI 错误文案。
|
||||
- 不复用或改写 `CreativeImageInputPanel` 的单图资产编辑语义;`/editor/canvas` 是独立图片画布工程的画布入口。
|
||||
|
||||
## 验收用例
|
||||
@@ -110,6 +120,12 @@
|
||||
- 快速编辑面板底部只显示模型选择和 `修改` 按钮;打开时视口聚焦必须预留底部面板空间,面板位于素材下方,不得遮挡原素材,且素材在当前屏幕内完整可见。快速编辑请求只把原图或红框序号标注图作为 `sourceImageSrc` 直接提交,信息面板输入快照只展示用户填写的快速编辑提示词。
|
||||
- 点击生成、生成规范、生成角色形象或生成图标素材后创建的占位图可继续保留;点击画布空白区域让当前图片或占位图失焦时,关闭当前生成面板并移除图片选中样式,但不删除占位图本身。
|
||||
- 生成资源显示元数据按钮,元数据窗口展示来源、生成输入快照、model、task、Resolution 和 OSS 引用;生成输入快照只包含用户面板输入和参考图行引用,不包含后端拼接 Prompt,不再展示独立 Size 字段,也不渲染参考图 Data URL 缩略图。
|
||||
- 点击底部 Dock 的“画布 Agent”后,右侧独立 Agent 面板打开;素材 / 图层侧栏和任务侧栏被收起。再次点击或点击面板关闭按钮后收起 Agent。打开素材 / 图层侧栏或任务侧栏时,Agent 面板同步关闭。
|
||||
- Agent 面板能读取当前工程会话列表;无历史会话时发送第一条消息会先创建“新对话”。支持新建会话、切换会话和删除当前会话;删除必须通过独立确认弹窗完成,不能在面板下方追加确认内容。
|
||||
- Agent 输入支持文本消息、附件消息和纯附件消息;附件选择弹窗可在“画布 / 素材库”之间切换,只展示图片类资源,最多选择 9 张。
|
||||
- 发送消息后,面板展示用户消息、Agent 阶段状态和 SSE 增量回复;`stage/message_delta/tool_started/tool_completed/generation_result/error/done` 都能被正确渲染。流式响应中点击“停止”会中断当前请求,并把仍在 streaming / generating 的消息标记为停止态。
|
||||
- Agent 返回生成结果缩略图后,点击缩略图应优先聚焦当前画布中已有 `resourceId` 对应图层;如果当前内存布局尚未包含该资源,则重新读取工程快照,应用后再聚焦新图层。对话入口触发生成时不创建“即将生成”画布占位;生成中状态只显示在消息流,生成完成后通过后端 `canvasCompletion` 落新图层。工具失败时消息内必须保留失败 generation record 和错误气泡,不能只弹一次性 toast。
|
||||
- 画布 Agent 会话刷新后能从后端恢复会话标题、消息、附件和生成记录;前端不得根据本地临时状态伪造会话持久化结果。
|
||||
- 图片选中后的浮动工具栏按钮顺序固定为:快速编辑、分割线、裁扩按钮、去除背景按钮、UI设计图专属提取素材、角色图专属生成动画、分割线、重绘、下载按钮。裁扩通过画布边界拖拉完成,不再展示四边数值输入;默认自由比例,选择固定比例后拖拉边界保持对应比例,完成后在原素材旁边新增裁扩结果图层,扩展区域透明填充。去除背景调用同源 BFF `POST /api/editor/images/background-removals`,由 api-server 代理远端 BiRefNet 服务并持久化结果;有项目上下文时先在画布创建关闭面板的去背景生成占位,完成后由后端通过 `canvasCompletion` 把新 project resource 写入该占位并返回快照,无占位上下文时才用新的 project resource 引用替换当前图层。画布任务侧栏按“排队/生成中”和“已完成”分页,生成中排在排队前,生成中耗时从任务开始时间戳实时计算,排队中不计时;进行中任务只显示阶段文本和已用时,不显示百分比;完成态生成任务副标题显示用户提示词并单行截断;点击任务只聚焦对应画布内容,不激活生成面板或改变任务顺序,聚焦时必须预留图片上方工具栏、底部工具栏和可见生成对话框空间。UI设计图的提取素材必须先进入红框素材框选状态,默认启用矩形框选,右侧框选工具与快速编辑统一且可再次点击取消启用态,当前启用工具按钮必须保持高亮。素材提取面板必须在素材下方,使用与生成新素材一致的面板宽度和底部模型 / 按钮样式,提示语显示 `使用框选工具框选你希望从画面中提取的素材`,并展示按原图坐标准确裁剪的框选区域截图预览、固定模型 `gpt-image-2`、左下角计划规格 `1:1·1K/2K` 和 `提取 · N泥点` 按钮,不显示额外取消按钮;点击素材和面板以外的画布区域即退出 UI 素材提取。至少框选一个区域后才可提交,前端把红色轮廓绘入原图后固定走 `gpt-image-2` 和提示词 `仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕。绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现绿色描边、绿色底板、绿色投影或绿色反光。`,生成的 spritesheet 原图和拆分后的独立素材都作为画布图层保留。
|
||||
- 重绘生成资源后,右侧出现新生成结果图层,并自动 fit 原图 + 新图,且重绘面板保持打开。
|
||||
- 快速编辑 / 重绘站内 public 示例图、历史 generated 图或 OSS generated 图时,前端先读取成 `data:image/*;base64,...` 再提交,后端不得再收到 `/creation-type-references/*`、`/generated-*` 或 OSS URL 作为 `referenceImageSrcs/sourceImageSrc`。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# server-rs 与 SpacetimeDB 数据契约
|
||||
|
||||
更新时间:`2026-07-01`
|
||||
更新时间:`2026-07-03`
|
||||
|
||||
## 后端主线
|
||||
|
||||
@@ -21,7 +21,7 @@ SpacetimeDB 版本口径:当前 Rust crate `spacetimedb`、`spacetimedb-sdk`
|
||||
当前主要 crate:
|
||||
|
||||
- HTTP 服务:`api-server`。
|
||||
- 领域模块:`module-ai`、`module-assets`、`module-auth`、`module-bark-battle`、`module-big-fish`、`module-combat`、`module-creative-agent`、`module-custom-world`、`module-inventory`、`module-match3d`、`module-npc`、`module-progression`、`module-puzzle`、`module-quest`、`module-runtime`、`module-runtime-item`、`module-runtime-story`、`module-square-hole`、`module-story`、`module-visual-novel`。
|
||||
- 领域模块:`module-ai`、`module-assets`、`module-auth`、`module-bark-battle`、`module-big-fish`、`module-combat`、`module-creative-agent`、`module-editor-agent`、`module-custom-world`、`module-inventory`、`module-match3d`、`module-npc`、`module-progression`、`module-puzzle`、`module-quest`、`module-runtime`、`module-runtime-item`、`module-runtime-story`、`module-square-hole`、`module-story`、`module-visual-novel`。
|
||||
- 平台副作用:`platform-agent`、`platform-auth`、`platform-image`、`platform-llm`、`platform-oss`、`platform-wechat`、`platform-speech`。
|
||||
- 共享层:`shared-contracts`、`shared-kernel`、`shared-logging`。
|
||||
- SpacetimeDB:`spacetime-client`、`spacetime-module`。
|
||||
@@ -60,7 +60,7 @@ npm run check:server-rs-ddd
|
||||
- 平台基础能力:`/api/llm/*`、`/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。
|
||||
- 资产基础能力:`/api/assets/direct-upload-tickets`、`/api/assets/sts-upload-credentials`、`/api/assets/objects/*`、`/api/assets/read-*`,负责直传、确认、绑定和读取。
|
||||
- 外部 OpenAPI:`/api/external/v1/openapi.json`、`/api/external/v1/assets/direct-upload-tickets`、`/api/external/v1/assets/objects/confirm`、`/api/external/v1/assets/read-url`、`/api/external/v1/editor/*`,使用 Bearer API Key 鉴权;API Key 管理仍在登录态 `/api/profile/api-keys`,不进入外部 OpenAPI JSON。
|
||||
- 创作 / 游玩支撑能力:`/api/creation-entry/config`、`/api/ai/tasks*`、`/api/runtime/chat/*`、`/api/runtime/settings`、`/api/runtime/save/snapshot`、`/api/profile/browse-history`、`/api/profile/save-archives*`、`/api/profile/play-stats`、`/api/assets/history`、`/api/assets/character-visual/*`、`/api/assets/character-animation/*`、`/api/assets/character-workflow-cache*`、`/api/assets/hyper3d/*`、`/api/runtime/custom-world/asset-studio/*`、`/api/editor/projects*`。`/api/runtime/custom-world/asset-studio/*` 解析默认角色形象 / 动作提示词时可以在 OSS 缓存不可用或未配置时按无缓存返回默认提示;保存 workflow 缓存和真实素材读写仍必须要求 OSS 正常可用。
|
||||
- 创作 / 游玩支撑能力:`/api/creation-entry/config`、`/api/ai/tasks*`、`/api/runtime/chat/*`、`/api/runtime/settings`、`/api/runtime/save/snapshot`、`/api/profile/browse-history`、`/api/profile/save-archives*`、`/api/profile/play-stats`、`/api/assets/history`、`/api/assets/character-visual/*`、`/api/assets/character-animation/*`、`/api/assets/character-workflow-cache*`、`/api/assets/hyper3d/*`、`/api/runtime/custom-world/asset-studio/*`、`/api/editor/projects*`、`/api/editor/projects/{projectId}/agent-conversations`、`/api/editor/agent-conversations/{conversationId}*`。`/api/runtime/custom-world/asset-studio/*` 解析默认角色形象 / 动作提示词时可以在 OSS 缓存不可用或未配置时按无缓存返回默认提示;保存 workflow 缓存和真实素材读写仍必须要求 OSS 正常可用。
|
||||
- 后台入口配置:`/admin/api/creation-entry/config`、`/admin/api/creation-entry/config/banners` 和 `/admin/api/creation-entry/config/interactions`。
|
||||
- 后台素材查询:`GET /admin/api/editor-assets` 通过 `admin_list_editor_assets_and_return` 后台只读 procedure 读取私有账号级 `editor_asset` 中 `source_type = 'generated'` 的素材,支持 `ownerUserId`、`assetKind`、`keyword`、`createdAfter`、`createdBefore`、`cursor` 和 `limit`;返回缩略图 / Object Key、作者展示名、陶泥号、提示词、生成输入和生成成本,只用于查询,不执行精选审核、返还或展示状态修改,不通过后台 SQL 直查私有表。
|
||||
- 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。
|
||||
@@ -76,6 +76,15 @@ npm run check:server-rs-ddd
|
||||
|
||||
需要新增路由时,先确认玩法入口配置和 tracking 分类,不要绕过 `app.rs` 的统一中间件、鉴权和入口开关。涉及创作、生成、作品、公开详情、试玩、正式运行态、运行态库存、运行态设置 / 存档、游玩历史、存档归档、游玩统计、AI task、角色资产工坊或玩法生成支撑资产的路由,不再直接在 `app.rs` 逐玩法 `.merge(...)`,也不挂到 `modules/platform.rs`;必须先进入 `server-rs/crates/api-server/src/modules/play_flow.rs` 的统一玩法流程主干,再由主干注册表分发到各领域 HTTP Adapter 或支撑能力 handler。
|
||||
|
||||
### 图片画布 Agent 对话
|
||||
|
||||
- `/api/editor/projects/{projectId}/agent-conversations` 负责当前工程会话列表和新建;`/api/editor/agent-conversations/{conversationId}` 负责详情读取和软删;`/api/editor/agent-conversations/{conversationId}/messages/stream` 负责发送消息并返回 SSE。
|
||||
- `module-editor-agent` 只承载纯领域校验:标题派生、附件上限、消息输入规则和会话软删访问规则;不直接依赖 Axum、SpacetimeDB、OSS、LLM 或 Tokio。
|
||||
- `spacetime-module` 的 `editor_agent_conversation` 只保存元数据;创建、列表、读取、更新时间和软删通过 `create_editor_agent_conversation_and_return`、`list_editor_agent_conversations_and_return`、`get_editor_agent_conversation_and_return`、`touch_editor_agent_conversation_and_return`、`delete_editor_agent_conversation_and_return` procedure 完成,`api-server` 只能经 `spacetime-client` facade 访问。
|
||||
- 完整消息文档存 OSS `editor-agent/{conversationId}.json`,由 `api-server` 负责 2 MiB 上限、会话内串行锁、读改写、SSE 事件持久化和 `touch` 元数据更新时间;该 JSON 不进入 `editor_canvas.layers_json`,也不作为画布布局真相。
|
||||
- 对话附件只允许引用当前工程 `editor_project_resource` 或当前账号 `editor_asset` 的图片;前端可提交展示用 `imageSrc` / `thumbnailSrc`,后端必须按 `resourceId` / `assetId` 重新归一、校验 owner / project 和 `objectKey`,再给 LLM 或生成工具使用。
|
||||
- 画布 Agent 工具复用既有编辑器图片生成 / 修改 / 图标 spritesheet BFF,并继续使用后端模型定价和 `execute_billable_asset_operation_with_cost`;前端不提交 `priceMudPoints`。
|
||||
|
||||
### 创作 / 游玩统一流程主干
|
||||
|
||||
`modules/play_flow.rs` 是后端创作与游玩流程的统一入口。现有外部 URL、DTO、错误 envelope、鉴权方式、入口开关语义和 SpacetimeDB schema 默认不变,但路由组织必须遵循:
|
||||
@@ -215,7 +224,7 @@ npm run check:server-rs-ddd
|
||||
- 敲木鱼敲击物和背景环境图:VectorEngine `/v1/images/edits`,模型固定 `gpt-image-2`。敲击物支持 multipart 多参考图,第一张固定为后端内嵌默认木鱼图,用户上传图只作为新主题参考;prompt 必须要求 `1:1` 单一纯绿色 `#00FF00 / RGB(0,255,0)` 绿幕背景主体图,并禁止黑底、白底、棋盘格和任何实底背景。当前敲击物和返回按钮上传 OSS 前只做服务端绿幕去背后处理,避免泛抠图误伤玉米等主体像素。背景环境图只使用第一步抠图完成后的透明敲击物图作为参考,prompt 必须要求中央主体预留区保持干净,中央 40% 区域禁止出现主题主体、主体局部特写、轮廓影子或重复元素,主题元素只能作为外围氛围,且必须显式声明不继承任何绿色底色、绿幕底色或纯绿色画布。
|
||||
- Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;Rodin 提交、状态、下载和响应解析归属 `platform-hyper3d`,`api-server/src/hyper3d_generation.rs` 只做路由、配置和错误 envelope 映射;新 Match3D 草稿和批量新增不再生成 GLB。
|
||||
- 音频:视觉小说专用音频路由保留;VectorEngine Suno/Vidu provider 协议、任务提交/查询、音频 URL 提取、下载、MIME/extension 归一和 OSS put 请求准备归属 `platform-audio`。`api-server/src/vector_engine_audio_generation.rs` 只做路由、配置、计费、asset object confirm、entity binding 和错误 envelope 映射;拼图、抓大鹅和敲木鱼提示词生成音效入口暂时关闭,通用 `/api/creation/audio/*` 对这些目标返回 `410 Gone`。敲木鱼创作只接收上传 / 录音音频资产;前端选择或录音阶段只在浏览器本地处理待提交音频,统一限制裁切后最长 1 秒、裁掉前后声音过小片段,并用浏览器端近似响度算法平衡到 `-15 LKFS` 后做峰值保护。点击生成时才直传 OSS 并确认 `asset_object`,创作 JSON 只提交轻量 `WoodenFishAudioAsset`,不得继续上传 Data URL 音频;未提供时由 `api-server` 写回内置默认木鱼音 `/wooden-fish/default-hit-sound.mp3`。
|
||||
- OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。前端如果收到同一 OSS bucket 的完整 `https://*.oss-*.aliyuncs.com/generated-*` 地址,也必须先归一为 legacy path 后走同一换签链路,避免裸连私有 bucket 403 或绕过签名缓存。OSS 签名、读签名、HEAD 和 PUT 的结构化日志由 `platform-oss` 输出,排查资产写入 / 确认失败时优先按 `operation`、`object_key` / `key_prefix`、`status_class`、`error_kind` 和 `elapsed_ms` 下钻。新上传 generated 私有对象默认写入 `Cache-Control: public, max-age=31536000, immutable`;旧对象若缺该头,只能依赖 `ETag` / `Last-Modified` 协商缓存,应通过 OSS 元数据刷新或 CDN 配置补齐,不要恢复 api-server 静态代理。
|
||||
- OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。前端如果收到同一 OSS bucket 的完整 `https://*.oss-*.aliyuncs.com/generated-*` 地址,也必须先归一为 legacy path 后走同一换签链路,避免裸连私有 bucket 403 或绕过签名缓存。OSS 签名、读签名、HEAD 和 PUT 的结构化日志由 `platform-oss` 输出,排查资产写入 / 确认失败时优先按 `operation`、`object_key` / `key_prefix`、`status_class`、`error_kind` 和 `elapsed_ms` 下钻。新上传 generated 私有对象默认写入 `Cache-Control: public, max-age=31536000, immutable`;旧对象若缺该头,只能依赖 `ETag` / `Last-Modified` 协商缓存,应通过 OSS 元数据刷新或 CDN 配置补齐,不要恢复 api-server 静态代理。`editor-agent/` 前缀只用于服务端内部读写画布 Agent 会话消息文档,不属于浏览器直传 legacy public prefix;`/api/assets/direct-upload-tickets` 必须拒绝 `legacyPrefix=editor-agent`,内部读取只允许 `editor-agent/{conversationId}.json` 形态。
|
||||
- 外部 API 失败审计:外部供应商调用未成功时,`api-server` 必须发送 OTLP 失败事件并写入 `tracking_event`。VectorEngine 图片 provider 在 `platform-image` 内输出结构化日志和 `PlatformImageFailureAudit`,覆盖 `request_send`、`response_body`、`upstream_status`、`response_parse`、`missing_image` 和 `image_download` 阶段;`api-server` 只把该 audit 映射成 `external_api_call_failure`,`scope_kind = module`、`scope_id = provider`、`module_key = external-api`。metadata 固定包含 provider、endpoint、operation、failureStage、statusCode、statusClass、timeout、retryable、errorMessage、latencyMs、promptChars、referenceImageCount、imageModel、rawExcerpt,以及在调用方可获得上下文时补充的 `userId`(触发者)和 `profileId`(草稿 / 作品 / 场景作用域)。图片生成入口应优先把 owner user id 和 profile id 透传到失败审计,不要只保留 provider 级聚合,否则很难按“谁触发、哪个作品触发”定位问题。入库优先复用 tracking outbox,outbox 不可写或保护阈值拒绝时回退同步写 SpacetimeDB;不得新增前端兜底或在 SpacetimeDB reducer 内做外部 I/O。
|
||||
- 外部生成运行记录:所有外部生成编排的完成态统一写入 `tracking_event`,`event_key = external_generation_run`,`scope_kind = module`,`scope_id = provider`,`module_key = external-generation`。metadata 固定包含 `runId`、`provider`、`operation`、`requestLabel`、`requestPayload`、`status`、`success`、`failureReason`、`providerRequestId`、`resultPayload`、`startedAtMicros`、`completedAtMicros` 和 `durationMs`。这类记录只用于运行审计和排障,不再走 `ai_task` 旧表。
|
||||
|
||||
@@ -446,11 +455,19 @@ npm run check:server-rs-ddd
|
||||
- 说明:外部 OpenAPI 调用使用的账号级 API Key 凭据表,只保存 key prefix、SHA-256 hash、作用域、撤销状态和使用时间;明文 Key 只在 `/api/profile/api-keys` 创建接口返回一次,不进入 SpacetimeDB,且 API Key 管理接口不写入外部 OpenAPI JSON。v1 默认作用域为 `editor:project`、`editor:canvas`、`editor:image-generate`、`editor:asset`;其中 `editor:project` 覆盖项目列表、最近项目、创建、读取、重命名和删除,`editor:canvas` 覆盖默认画布布局保存,`editor:image-generate` 覆盖编辑器现有图片生成、重绘 / 调整、规范图、宣发素材、图标 spritesheet 生成 / 拆分、UI 设计图素材拆分、角色动画、视频、音效和背景音乐生成,`editor:asset` 覆盖素材直传凭证、素材对象确认、签名读取、账号级素材库和项目画布资源记录操作。
|
||||
- 索引:`by_external_api_key_owner_user_id` 用于登录态 API Key 列表;`key_hash` 唯一索引用于外部 API 鉴权。
|
||||
|
||||
### `editor_agent_conversation`
|
||||
|
||||
- Rust 结构体:`EditorAgentConversation`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_agent_storage.rs`
|
||||
- 说明:画布Agent对话会话元数据表,归属单个 `editor_project`;只保存会话 ID、project、owner、标题、消息 OSS 对象键(`editor-agent/{conversationId}.json`)、软删标记和时间戳。消息正文整体存 OSS,按会话粒度整体读写,不进 SpacetimeDB、不进画布工程快照 payload。删除为软删(`deleted = true`,OSS 对象保留)。领域校验(标题截取、附件上限、归属 / 软删规则)沉在 `module-editor-agent`。
|
||||
- 过程:只通过 `create_editor_agent_conversation_and_return`、`list_editor_agent_conversations_and_return`、`get_editor_agent_conversation_and_return`、`touch_editor_agent_conversation_and_return`、`delete_editor_agent_conversation_and_return` procedure 操作;`api-server` 不直接绕过 `spacetime-client` facade 读写表。消息文档当前由 `api-server` 做单会话串行锁和 2 MiB 上限保护,避免同一 SSE 回合并发重写同一个 OSS JSON 文档。
|
||||
- 索引:`by_editor_agent_conversation_project_id` 用于会话列表;`by_editor_agent_conversation_owner_user_id` 用于账号级归属校验。
|
||||
|
||||
### `editor_project`
|
||||
|
||||
- Rust 结构体:`EditorProject`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
|
||||
- 说明:图片画布工程真相表,保存 owner、标题和工程时间戳;viewport 与图层布局已拆到 `editor_canvas`,旧 layout columns 暂作为兼容列保留,不再作为权威数据源。只通过 `/api/editor/projects*` BFF 和 `spacetime-client` facade 读写;项目页列表、重命名和删除也使用该能力,删除工程时级联清理默认画布和资源元数据。
|
||||
- 说明:图片画布工程真相表,保存 owner、标题和工程时间戳;viewport 与图层布局已拆到 `editor_canvas`,旧 layout columns 暂作为兼容列保留,不再作为权威数据源。只通过 `/api/editor/projects*`、`/api/editor/projects/{projectId}/agent-conversations`、`/api/editor/agent-conversations/{conversationId}*` BFF 和 `spacetime-client` facade 读写;项目页列表、重命名和删除也使用该能力,删除工程时级联清理默认画布和资源元数据。
|
||||
- 索引:`by_editor_project_owner_user_id` 用于读取当前用户最近编辑工程和项目页工程列表。
|
||||
|
||||
### `editor_canvas`
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
- 面板中用户修改比例、尺寸或清晰度后,已有空白待生成占位立即同步更新 `width / height / originalWidth / originalHeight`,且保持中心点不跳动。
|
||||
- 快速编辑点击修改后不创建独立 `Quick Edit Generator` 画布生成占位;当前快速编辑面板显示修改中,生成成功后结果直接覆盖源图,失败时保留当前面板并显示错误。需要新建占位的是生成图片、生成视频、重绘、去背景和角色动作等会产出新图层的入口。
|
||||
- 任何会打开画布内 composer / 面板的入口,必须在面板渲染后通过统一 overlay 可见性校正检查真实 DOM 矩形;如果面板超出画布视口,或底部工具栏 / 左下 dock 会遮住面板,就只平移当前 viewport 让面板完整进入安全区域。新增生成类入口不要在按钮 handler 里手写单独的避让偏移。
|
||||
- 画布Agent对话入口例外:画布Agent对话(右侧对话面板)触发的生成不创建"即将生成"画布占位,生成中状态由对话消息流内的条目承载(阶段提示、模型标注、进行中动画);生成完成后结果图才按统一 placement 避让模型落画板为新图层,并在对话消息内显示缩略图。工具失败时也必须在对话消息内保留失败 generation record,而不是只弹一次性错误气泡。该例外仅限画布Agent对话入口,其余生成入口仍必须先落占位。详见 docs/【编辑器】画布Agent对话面板-2026-07-03.md。
|
||||
|
||||
## 画布悬浮信息
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# 画布Agent对话面板
|
||||
|
||||
日期:`2026-07-03`
|
||||
|
||||
## 定位与边界
|
||||
|
||||
- **画布Agent对话** 是图片画布工程(`/editor/canvas`)右侧的对话式编辑器工具:用户通过自然语言调度画布已有的图片类生成与编辑能力,并可附加画布资源或素材库图片作为参考。
|
||||
- 它是画布域工具,**不承接玩法创作**、不产出玩法作品或模板,与 `CONTEXT.md` 中「表单/图片输入创作工作台」的 Avoid 边界不冲突。
|
||||
- 独立于拼图专用的 `/api/runtime/creative-agent/sessions`(该会话为 api-server 内存态、拼图领域专用,不复用)。
|
||||
|
||||
## 能力范围(第一期)
|
||||
|
||||
对话 Agent 可通过 function-calling 触发以下五类工具,全部复用既有计费收口接口:
|
||||
|
||||
| 工具 | 后端接口 |
|
||||
| --- | --- |
|
||||
| 生成图片 | `POST /api/editor/images/generations` |
|
||||
| 修改图片(基于附件/画布素材) | `POST /api/editor/images/edits` |
|
||||
| 生成角色形象 | 既有角色形象生成入口对应接口 |
|
||||
| 生成图标素材 | `POST /api/editor/icon-spritesheets/generations` |
|
||||
| 生成 UI 设计图 | 既有 UI 设计图生成入口对应接口 |
|
||||
|
||||
- 意图解析与工具编排在后端 api-server,前端只渲染状态,不承接业务规则。
|
||||
- 所有生成必须走 `execute_billable_asset_operation_with_cost` 与模型定价配置,禁止绕过定价收口。
|
||||
- 视频 / 音频 / 图层操作等其余画板功能第一期不进入对话工具面,仍走现有面板。
|
||||
|
||||
## 当前分支落地状态
|
||||
|
||||
- 已落地:会话元数据、OSS 消息文档、会话 CRUD、SSE 消息流、后端 LLM 工具规划、右侧对话面板、会话历史、新建 / 软删会话、停止当前 SSE 回合、附件从画布资源 / 账号素材库选择,以及五类图片工具对既有生成入口的复用。
|
||||
- 已落地:`tool_started` / `tool_completed` 事件携带 `status`;工具失败时也会写入失败 generation record,并随后发送 `stage=failed` 与 `error`,前端应保留消息内失败条目。
|
||||
- 未落地:附件弹窗末尾上传格、跨刷新异步生成恢复、external generation task 轮询回填。未落地前,对话消息状态只表示本次 SSE 回合记录,不作为后台任务队列真相。
|
||||
|
||||
## 会话与持久化
|
||||
|
||||
- 对话归属单个图片画布工程;每个工程有自己的会话列表,可新开会话。
|
||||
- SpacetimeDB 新表 `editor_agent_conversation` 只存会话元数据:会话 ID、projectId、ownerUserId、标题、软删标记、聊天记录 OSS 对象引用、创建/更新时间。
|
||||
- 完整消息内容存 OSS:`editor-agent/{conversationId}.json`,**会话粒度整体读写**(追加消息=重写对象),不按消息拆对象。
|
||||
- 不把对话塞进画布工程快照 payload,不在 api-server 内存中保存会话真相。
|
||||
- 会话标题:新会话默认「新对话」,首条用户消息发出后自动截取前 N 字作为标题;第一期不做手动重命名。
|
||||
- 会话删除:列表项 hover 出删除按钮 + 确认;软删(表打 deleted 标记,OSS 对象保留)。
|
||||
|
||||
## 生成结果落画板(对现有占位规则的例外)
|
||||
|
||||
- 对话入口触发的生成**不创建"即将生成"画布占位**(区别于其余生成面板);生成中状态由对话消息流承载。
|
||||
- 生成完成后:结果图按统一 placement 避让模型(视口中心就近、避开现有图层、32px 间距)落画板为新图层,同时登记到默认项目素材库,并在对话消息内显示纯缩略图;前端收到 `generation_result` 后立即刷新工程快照与素材库,缩略图本身不显示名称也不承担图层跳转。
|
||||
- 消息内生成结果缩略图必须携带并优先使用 `objectKey` / `assetObjectId`,前端通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签后渲染,不能把裸 `/generated-*` 私有路径直接交给 `<img>`。
|
||||
- 当前第一阶段通过既有编辑器生成 BFF 的 `canvasCompletion` 写回工程快照;刷新后异步任务恢复和轮询回填属于后续能力,不在本阶段声明为已完成。
|
||||
- 该例外已同步登记在《生成类面板Lovart统一改造方案-2026-06-17》「画布占位落点」节。
|
||||
|
||||
## 右侧布局
|
||||
|
||||
- 对话框与既有任务侧栏(`ImageCanvasTaskSidebarView`)**互斥展开**:展开一个自动收起另一个;各自收起后保留入口按钮。
|
||||
- 对话框与左侧素材 / 图层侧栏也互斥:打开画布 Agent 时收起左侧栏;再次打开素材、图层或任务侧栏时收起 Agent 面板。
|
||||
- 桌面端对话框固定宽约 360–400px;移动端抽屉式全宽覆盖;收起态为胶囊/圆形入口按钮。
|
||||
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
|
||||
- 收起对话框只是隐藏面板,不卸载当前会话 hook;流式回复、`生成中` 阶段和停止按钮状态必须在收起 / 重新打开之间保持一致。
|
||||
|
||||
## 附件
|
||||
|
||||
- 输入区 `+` 按钮打开**图片选择弹窗**(仅图片,无音视频):
|
||||
- 「画布」页签(默认):展示当前工程图层引用的图片资源;
|
||||
- 「素材库」页签:账号级素材库(复用 `ImageCanvasAssetLibrary` 数据源);
|
||||
- 多选 + 底部「取消 / 应用」。
|
||||
- 网格末尾上传格为后续补齐项;在上传格未落地前,对话附件只从已有画布资源和账号素材库选择。后续若从对话入口上传图片,必须复用素材库 / 画布资源登记链路,不新增对话私有图片类型。
|
||||
- 应用后附件以胶囊 chip 挂在输入框上方;发出的消息内附件渲染为纯文本胶囊 chip(名称 + 小图标),**默认无缩略图,鼠标悬浮才浮出缩略图预览**。
|
||||
- 附件领域形状:统一为画布资源 / 素材库对象引用(`resourceId` / `assetId` + 可选 `objectKey`),不存在只属于对话的第三种图;单条消息上限 9 张(前后端共同校验)。前端可携带展示用 `imageSrc` / `thumbnailSrc`,后端必须按当前工程和当前账号重新归一、校验归属与 `objectKey`。
|
||||
|
||||
## LLM 与计费
|
||||
|
||||
- 编排复用 `creative_agent_gpt5_client` 的 LLM 接入配置(同 provider/env,独立用途标识),但画布 Agent 的轻量规划请求显式使用 `gpt-4o`,避免把简单 JSON 意图识别交给容易长 reasoning 的 gpt-5;function-calling 注册五类工具。
|
||||
- 每个用户回合必须由 LLM 返回结构化计划;LLM 未配置、请求失败或返回格式不可解析时,后端写入明确错误消息,不使用本地关键词或“收到:...”回显兜底。
|
||||
- 画布 Agent 规划请求使用 Responses API、1024 `max_output_tokens` 和 60 秒 Agent 专用请求超时;生成图片/编辑图片仍走对应生成工具和模型计费。
|
||||
- **对话回合免费**(聊天、分析回复不扣泥点),仅 Agent 实际触发生成工具时按对应模型定价扣泥点。
|
||||
- 工具调用前后端校验泥点余额;不足时该次生成失败并在对话中以明确错误气泡告知,对话本身可继续。
|
||||
|
||||
## Lovart 参照做/不做清单(验收标准)
|
||||
|
||||
做(第一期):
|
||||
|
||||
1. 助手文本 SSE 流式输出;
|
||||
2. 阶段提示行(思考中 → 思考完成 → 生成中 → 完成/失败);
|
||||
3. 工具/模型标注行(生成时显示模型名 + 图标);
|
||||
4. 消息内生成结果缩略图(纯预览,不显示名称,不点击聚焦图层);
|
||||
5. 生成中的进行中动画;
|
||||
6. 错误气泡(失败/余额不足,带原因);
|
||||
7. 发送中断:进行中时发送按钮变「停止」,可中断当前回合(已提交的生成任务不追回,照常落画板)。
|
||||
|
||||
不做(明确排除,防止后人补齐):
|
||||
|
||||
- 点赞/点踩反馈按钮;
|
||||
- 消息复制、分享/导出对话;
|
||||
- Agent 模式切换下拉(固定单一 Agent);
|
||||
- 语音输入、@引用、多 Agent 协作;
|
||||
- Lovart 的积分/加速档位显示(泥点扣费只在生成动作上体现)。
|
||||
|
||||
## 顺手需求
|
||||
|
||||
- 左侧侧边栏默认隐藏:`useImageCanvasEditorChrome.ts` 中 `activeSidebarPanel` 初始值 `'assets'` → `null`;
|
||||
- 小地图默认隐藏:`isMinimapOpen` 初始值 `true` → `false`;
|
||||
- 纯默认值修改,不加 localStorage 偏好记忆。
|
||||
|
||||
## 后端分层落位
|
||||
|
||||
- `module-editor-agent`(新 crate):领域规则——会话/消息校验、状态机、附件上限、软删规则、工具清单领域定义;纯逻辑无 IO。
|
||||
- `spacetime-module`:新表 `editor_agent_conversation` + procedure;同步 `migration.rs`、表目录、生成绑定,运行 `npm run check:spacetime-schema`。
|
||||
- `spacetime-client`:facade 读写方法。
|
||||
- `api-server`:
|
||||
- `GET/POST /api/editor/projects/{projectId}/agent-conversations`(列表/新建);
|
||||
- `GET/DELETE /api/editor/agent-conversations/{conversationId}`(详情/软删);
|
||||
- `POST /api/editor/agent-conversations/{conversationId}/messages/stream`(SSE);
|
||||
- Agent 编排(function-calling 循环、工具内部调既有生成执行链路)放 api-server 编排层,独立文件,不复用 `creative_agent.rs` 内存会话。
|
||||
- `shared-contracts` + `packages/shared`:新 `editorAgent` DTO 与 SSE 事件契约(`stage`、`message_delta`、`tool_started`、`tool_completed`、`generation_result`、`error`、`done`)。
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 契约与领域规则(shared-contracts / packages/shared + module-editor-agent);
|
||||
2. 存储层(spacetime-module 表 + procedure + migration + spacetime-client + schema check);
|
||||
3. api-server 会话 CRUD + 消息 OSS 读写 + SSE 回显桩(不接 LLM,先保证会话链路端到端真实落库);
|
||||
4. 前端最小纵切(对话框、会话管理、消息流、附件弹窗、侧边栏/小地图默认值)——可与 3 并行:3 只碰 `server-rs/`,4 只碰 `src/` 且先以契约 mock 客户端联调,汇合点在 4 末接真实 API;
|
||||
5. LLM 编排 + 五类工具接入 + 生成落画板 + 停止/错误态(替换回显桩这一个点);
|
||||
6. 验证与文档:定向测试、类型检查、`npm run check:encoding`、`git diff --check`、`npm run check:spacetime-schema`、api-server smoke `/healthz`;同步 `CONTEXT.md` 与相关文档。
|
||||
|
||||
## 第一阶段验收补充
|
||||
|
||||
- 打开画布 Agent 后,任务侧栏和左侧素材 / 图层面板应关闭;再次打开任务侧栏或素材 / 图层面板时,Agent 面板应关闭。
|
||||
- 发送消息时先本地追加用户消息,再消费 SSE 增量;停止按钮只中断当前 SSE 回合,不追回已经提交的生成工具调用。
|
||||
- Agent 消息内生成结果缩略图只用于预览,不显示名称,也不点击跳转图层;收到 `generation_result` 时统一刷新工程快照和素材库。
|
||||
- 对话内容可被用户选中复制;用户从输入框或对话内容点击回画布图层 / 生成器时,焦点应回到画布对象,Backspace / Delete 等画布快捷键继续生效。
|
||||
@@ -0,0 +1,170 @@
|
||||
// 画布Agent对话契约:会话元数据存 SpacetimeDB,消息正文整体存 OSS(editor-agent/{conversationId}.json)。
|
||||
|
||||
export const EDITOR_AGENT_MAX_ATTACHMENTS = 9;
|
||||
export const EDITOR_AGENT_TITLE_MAX_CHARS = 20;
|
||||
export const EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE = '新对话';
|
||||
export const EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION = 1;
|
||||
|
||||
export type EditorAgentStage =
|
||||
| 'idle'
|
||||
| 'thinking'
|
||||
| 'responding'
|
||||
| 'generating'
|
||||
| 'completed'
|
||||
| 'failed';
|
||||
|
||||
export type EditorAgentMessageRole = 'user' | 'assistant';
|
||||
|
||||
export type EditorAgentMessageKind = 'chat' | 'stage' | 'error';
|
||||
|
||||
export type EditorAgentMessageStatus =
|
||||
| 'streaming'
|
||||
| 'generating'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'stopped';
|
||||
|
||||
export type EditorAgentAttachmentSource = 'canvas_resource' | 'library_asset';
|
||||
|
||||
export type EditorAgentToolName =
|
||||
| 'generate_image'
|
||||
| 'edit_image'
|
||||
| 'generate_character'
|
||||
| 'generate_icon_spritesheet'
|
||||
| 'generate_ui_design';
|
||||
|
||||
export interface EditorAgentAttachmentRef {
|
||||
source: EditorAgentAttachmentSource;
|
||||
referenceId: string;
|
||||
objectKey?: string | null;
|
||||
imageSrc: string;
|
||||
thumbnailSrc?: string | null;
|
||||
label?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
}
|
||||
|
||||
export interface EditorAgentGeneratedImage {
|
||||
resourceId: string | null;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
imageSrc: string;
|
||||
thumbnailSrc: string | null;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
}
|
||||
|
||||
export type EditorAgentGenerationStatus = 'generating' | 'completed' | 'failed';
|
||||
|
||||
export interface EditorAgentGenerationRecord {
|
||||
toolCallId: string;
|
||||
toolName: EditorAgentToolName;
|
||||
taskId: string | null;
|
||||
status: EditorAgentGenerationStatus;
|
||||
model: string | null;
|
||||
images: EditorAgentGeneratedImage[];
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface EditorAgentMessage {
|
||||
id: string;
|
||||
role: EditorAgentMessageRole;
|
||||
kind: EditorAgentMessageKind;
|
||||
text: string;
|
||||
attachments: EditorAgentAttachmentRef[];
|
||||
generations: EditorAgentGenerationRecord[];
|
||||
status: EditorAgentMessageStatus;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface EditorAgentConversationSummary {
|
||||
conversationId: string;
|
||||
projectId: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface EditorAgentConversationDetail
|
||||
extends EditorAgentConversationSummary {
|
||||
messages: EditorAgentMessage[];
|
||||
}
|
||||
|
||||
export interface EditorAgentConversationMessagesDocument {
|
||||
version: number;
|
||||
conversationId: string;
|
||||
messages: EditorAgentMessage[];
|
||||
}
|
||||
|
||||
export interface CreateEditorAgentConversationRequest {
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
export interface EditorAgentConversationListResponse {
|
||||
conversations: EditorAgentConversationSummary[];
|
||||
}
|
||||
|
||||
export interface EditorAgentConversationResponse {
|
||||
conversation: EditorAgentConversationDetail;
|
||||
}
|
||||
|
||||
export interface StreamEditorAgentMessageRequest {
|
||||
clientMessageId: string;
|
||||
text: string;
|
||||
attachments?: EditorAgentAttachmentRef[];
|
||||
}
|
||||
|
||||
export interface EditorAgentStageEvent {
|
||||
conversationId: string;
|
||||
stage: EditorAgentStage;
|
||||
}
|
||||
|
||||
export interface EditorAgentMessageDeltaEvent {
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
role: EditorAgentMessageRole;
|
||||
kind: EditorAgentMessageKind;
|
||||
textDelta: string;
|
||||
}
|
||||
|
||||
export interface EditorAgentToolEvent {
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
toolCallId: string;
|
||||
toolName: EditorAgentToolName;
|
||||
summary?: string | null;
|
||||
taskId?: string | null;
|
||||
model?: string | null;
|
||||
status?: EditorAgentGenerationStatus | null;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface EditorAgentGenerationResultEvent {
|
||||
conversationId: string;
|
||||
messageId: string;
|
||||
toolCallId: string;
|
||||
toolName: EditorAgentToolName;
|
||||
model: string | null;
|
||||
images: EditorAgentGeneratedImage[];
|
||||
}
|
||||
|
||||
export interface EditorAgentErrorEvent {
|
||||
conversationId: string | null;
|
||||
code: string;
|
||||
message: string;
|
||||
recoverable: boolean;
|
||||
}
|
||||
|
||||
export interface EditorAgentDoneEvent {
|
||||
conversationId: string;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
export type EditorAgentSseEvent =
|
||||
| { event: 'stage'; data: EditorAgentStageEvent }
|
||||
| { event: 'message_delta'; data: EditorAgentMessageDeltaEvent }
|
||||
| { event: 'tool_started'; data: EditorAgentToolEvent }
|
||||
| { event: 'tool_completed'; data: EditorAgentToolEvent }
|
||||
| { event: 'generation_result'; data: EditorAgentGenerationResultEvent }
|
||||
| { event: 'error'; data: EditorAgentErrorEvent }
|
||||
| { event: 'done'; data: EditorAgentDoneEvent };
|
||||
@@ -1,6 +1,7 @@
|
||||
export type * from './barkBattle';
|
||||
export type * from './creationAudio';
|
||||
export type * from './creativeAgent';
|
||||
export * from './editorAgent';
|
||||
export * from './hostBridge';
|
||||
export type * from './hyper3d';
|
||||
export type * from './jumpHop';
|
||||
|
||||
Generated
+12
@@ -222,6 +222,7 @@ dependencies = [
|
||||
"module-combat",
|
||||
"module-creative-agent",
|
||||
"module-custom-world",
|
||||
"module-editor-agent",
|
||||
"module-inventory",
|
||||
"module-match3d",
|
||||
"module-npc",
|
||||
@@ -3147,6 +3148,16 @@ dependencies = [
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "module-editor-agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shared-kernel",
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "module-inventory"
|
||||
version = "0.1.0"
|
||||
@@ -5809,6 +5820,7 @@ dependencies = [
|
||||
"module-big-fish",
|
||||
"module-combat",
|
||||
"module-custom-world",
|
||||
"module-editor-agent",
|
||||
"module-inventory",
|
||||
"module-jump-hop",
|
||||
"module-match3d",
|
||||
|
||||
@@ -15,6 +15,7 @@ members = [
|
||||
"crates/module-big-fish",
|
||||
"crates/module-combat",
|
||||
"crates/module-creative-agent",
|
||||
"crates/module-editor-agent",
|
||||
"crates/module-inventory",
|
||||
"crates/module-custom-world",
|
||||
"crates/module-jump-hop",
|
||||
@@ -64,6 +65,7 @@ module-bark-battle = { path = "crates/module-bark-battle", default-features = fa
|
||||
module-big-fish = { path = "crates/module-big-fish", default-features = false }
|
||||
module-combat = { path = "crates/module-combat", default-features = false }
|
||||
module-creative-agent = { path = "crates/module-creative-agent", default-features = false }
|
||||
module-editor-agent = { path = "crates/module-editor-agent", default-features = false }
|
||||
module-custom-world = { path = "crates/module-custom-world", default-features = false }
|
||||
module-inventory = { path = "crates/module-inventory", default-features = false }
|
||||
module-jump-hop = { path = "crates/module-jump-hop", default-features = false }
|
||||
|
||||
@@ -25,6 +25,7 @@ module-bark-battle = { workspace = true }
|
||||
module-combat = { workspace = true }
|
||||
module-creative-agent = { workspace = true }
|
||||
module-custom-world = { workspace = true }
|
||||
module-editor-agent = { workspace = true }
|
||||
module-inventory = { workspace = true }
|
||||
module-match3d = { workspace = true }
|
||||
module-npc = { workspace = true }
|
||||
|
||||
@@ -85,6 +85,14 @@ pub(crate) async fn create_direct_upload_ticket_for_owner(
|
||||
"supported": platform_oss::LEGACY_PUBLIC_PREFIXES,
|
||||
}))
|
||||
})?;
|
||||
if matches!(legacy_prefix, LegacyAssetPrefix::EditorAgent) {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"field": "legacyPrefix",
|
||||
"supported": platform_oss::LEGACY_PUBLIC_PREFIXES,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
let signed = oss_client
|
||||
.sign_post_object(OssPostObjectRequest {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,8 @@ pub async fn proxy_llm_chat_completions(
|
||||
max_tokens: None,
|
||||
enable_web_search: false,
|
||||
request_timeout_ms: None,
|
||||
response_reasoning_effort: None,
|
||||
response_text_verbosity: None,
|
||||
};
|
||||
|
||||
if payload.stream {
|
||||
|
||||
@@ -36,6 +36,7 @@ mod custom_world_asset_prompts;
|
||||
mod custom_world_foundation_draft;
|
||||
mod custom_world_result_prompts;
|
||||
mod custom_world_rpg_draft_prompts;
|
||||
mod editor_agent;
|
||||
mod editor_generation_config;
|
||||
mod editor_generation_queue;
|
||||
mod editor_green_screen;
|
||||
|
||||
@@ -7,6 +7,11 @@ use axum::{
|
||||
|
||||
use crate::{
|
||||
auth::require_bearer_auth,
|
||||
editor_agent::{
|
||||
create_editor_agent_conversation, delete_editor_agent_conversation,
|
||||
get_editor_agent_conversation, list_editor_agent_conversations,
|
||||
stream_editor_agent_message,
|
||||
},
|
||||
editor_project::{
|
||||
create_editor_asset, create_editor_asset_folder, create_editor_project,
|
||||
create_editor_project_resource, delete_editor_asset, delete_editor_asset_folder,
|
||||
@@ -22,6 +27,7 @@ use crate::{
|
||||
};
|
||||
|
||||
const EDITOR_IMAGE_REFERENCE_BODY_LIMIT_BYTES: usize = 12 * 1024 * 1024;
|
||||
const EDITOR_AGENT_MESSAGE_BODY_LIMIT_BYTES: usize = 2 * 1024 * 1024;
|
||||
|
||||
pub fn router(state: AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
@@ -73,6 +79,33 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/editor/projects/{project_id}/agent-conversations",
|
||||
get(list_editor_agent_conversations)
|
||||
.post(create_editor_agent_conversation)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/editor/agent-conversations/{conversation_id}",
|
||||
get(get_editor_agent_conversation)
|
||||
.delete(delete_editor_agent_conversation)
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/editor/agent-conversations/{conversation_id}/messages/stream",
|
||||
post(stream_editor_agent_message)
|
||||
.layer(DefaultBodyLimit::max(EDITOR_AGENT_MESSAGE_BODY_LIMIT_BYTES))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/editor/project-resources/{resource_id}/showcase",
|
||||
patch(update_editor_project_resource_showcase).route_layer(
|
||||
|
||||
@@ -13,19 +13,18 @@ use module_runtime::{
|
||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5,
|
||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM,
|
||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL,
|
||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE, RuntimeProfileFeedbackEvidenceRecord,
|
||||
RuntimeProfileCodeOperationRecord, RuntimeProfileFeedbackEvidenceSnapshot,
|
||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE, RuntimeProfileCodeOperationRecord,
|
||||
RuntimeProfileFeedbackEvidenceRecord, RuntimeProfileFeedbackEvidenceSnapshot,
|
||||
RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeRecord,
|
||||
RuntimeProfileMembershipBenefitRecord,
|
||||
RuntimeProfileMembershipTier, RuntimeProfileRechargeCenterRecord,
|
||||
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus,
|
||||
RuntimeProfileRechargeProductConfigRecord, RuntimeProfileRechargeProductKind,
|
||||
RuntimeProfileRechargeProductRecord, RuntimeProfileRedeemCodeMode,
|
||||
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
||||
RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord,
|
||||
RuntimeProfileTaskCycle, RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus,
|
||||
RuntimeProfileWalletLedgerSourceType, RuntimeReferralInviteCenterRecord,
|
||||
RuntimeTrackingScopeKind,
|
||||
RuntimeProfileMembershipBenefitRecord, RuntimeProfileMembershipTier,
|
||||
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
||||
RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeProductConfigRecord,
|
||||
RuntimeProfileRechargeProductKind, RuntimeProfileRechargeProductRecord,
|
||||
RuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord,
|
||||
RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileTaskCenterRecord,
|
||||
RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord, RuntimeProfileTaskCycle,
|
||||
RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus, RuntimeProfileWalletLedgerSourceType,
|
||||
RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind,
|
||||
};
|
||||
use platform_wechat::pay::WechatPayNotifyOrder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -53,18 +52,17 @@ use shared_contracts::runtime::{
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC, ProfileDashboardSummaryResponse,
|
||||
ProfileCodeOperationAdminResponse, ProfileFeedbackEvidenceItemResponse,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC, ProfileCodeOperationAdminResponse,
|
||||
ProfileDashboardSummaryResponse, ProfileFeedbackEvidenceItemResponse,
|
||||
ProfileFeedbackSubmissionResponse, ProfileInviteCodeAdminListResponse,
|
||||
ProfileInviteCodeAdminResponse,
|
||||
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
|
||||
ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse, ProfileRechargeOrderResponse,
|
||||
ProfileRechargeProductConfigAdminListResponse, ProfileRechargeProductConfigAdminResponse,
|
||||
ProfileRechargeProductResponse, ProfileRedeemCodeAdminListResponse,
|
||||
ProfileRedeemCodeAdminResponse, ProfileReferralInviteCenterResponse,
|
||||
ProfileReferralInvitedUserResponse, ProfileTaskCenterResponse,
|
||||
ProfileTaskConfigAdminListResponse, ProfileTaskConfigAdminResponse, ProfileTaskItemResponse,
|
||||
ProfileWalletConfigAdminResponse, ProfileWalletLedgerEntryResponse,
|
||||
ProfileInviteCodeAdminResponse, ProfileMembershipBenefitResponse, ProfileMembershipResponse,
|
||||
ProfilePlayStatsResponse, ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse,
|
||||
ProfileRechargeOrderResponse, ProfileRechargeProductConfigAdminListResponse,
|
||||
ProfileRechargeProductConfigAdminResponse, ProfileRechargeProductResponse,
|
||||
ProfileRedeemCodeAdminListResponse, ProfileRedeemCodeAdminResponse,
|
||||
ProfileReferralInviteCenterResponse, ProfileReferralInvitedUserResponse,
|
||||
ProfileTaskCenterResponse, ProfileTaskConfigAdminListResponse, ProfileTaskConfigAdminResponse,
|
||||
ProfileTaskItemResponse, ProfileWalletConfigAdminResponse, ProfileWalletLedgerEntryResponse,
|
||||
ProfileWalletLedgerResponse, RedeemProfileReferralInviteCodeRequest,
|
||||
RedeemProfileReferralInviteCodeResponse, RedeemProfileRewardCodeRequest,
|
||||
RedeemProfileRewardCodeResponse, SubmitProfileFeedbackRequest, SubmitProfileFeedbackResponse,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "module-editor-agent"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
spacetime-types = ["dep:spacetimedb"]
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
shared-kernel = { workspace = true }
|
||||
spacetimedb = { workspace = true, optional = true }
|
||||
@@ -0,0 +1,127 @@
|
||||
use shared_kernel::normalize_required_string;
|
||||
|
||||
use crate::{
|
||||
EDITOR_AGENT_MAX_ATTACHMENTS, EditorAgentConversationCreateInput,
|
||||
EditorAgentConversationDeleteInput, EditorAgentConversationTouchInput, EditorAgentError,
|
||||
};
|
||||
|
||||
pub fn validate_conversation_key(
|
||||
conversation_id: &str,
|
||||
owner_user_id: &str,
|
||||
) -> Result<(String, String), EditorAgentError> {
|
||||
let conversation_id = normalize_required_string(conversation_id)
|
||||
.ok_or(EditorAgentError::MissingConversationId)?;
|
||||
let owner_user_id =
|
||||
normalize_required_string(owner_user_id).ok_or(EditorAgentError::MissingOwnerUserId)?;
|
||||
Ok((conversation_id, owner_user_id))
|
||||
}
|
||||
|
||||
pub fn validate_create_conversation(
|
||||
input: &EditorAgentConversationCreateInput,
|
||||
) -> Result<(), EditorAgentError> {
|
||||
validate_conversation_key(&input.conversation_id, &input.owner_user_id)?;
|
||||
if normalize_required_string(&input.project_id).is_none() {
|
||||
return Err(EditorAgentError::MissingProjectId);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_touch_conversation(
|
||||
input: &EditorAgentConversationTouchInput,
|
||||
) -> Result<(), EditorAgentError> {
|
||||
validate_conversation_key(&input.conversation_id, &input.owner_user_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_delete_conversation(
|
||||
input: &EditorAgentConversationDeleteInput,
|
||||
) -> Result<(), EditorAgentError> {
|
||||
validate_conversation_key(&input.conversation_id, &input.owner_user_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 校验会话访问权:拥有者匹配且未软删。
|
||||
pub fn ensure_conversation_accessible(
|
||||
snapshot_owner_user_id: &str,
|
||||
snapshot_deleted: bool,
|
||||
requester_user_id: &str,
|
||||
) -> Result<(), EditorAgentError> {
|
||||
if snapshot_owner_user_id != requester_user_id {
|
||||
return Err(EditorAgentError::NotConversationOwner);
|
||||
}
|
||||
if snapshot_deleted {
|
||||
return Err(EditorAgentError::ConversationDeleted);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 校验用户消息:文本与附件不可同时为空,附件数量不超过上限,附件引用需带资源标识。
|
||||
pub fn validate_user_message(
|
||||
text: &str,
|
||||
attachment_reference_ids: &[String],
|
||||
) -> Result<(), EditorAgentError> {
|
||||
let has_text = normalize_required_string(text).is_some();
|
||||
if !has_text && attachment_reference_ids.is_empty() {
|
||||
return Err(EditorAgentError::EmptyMessage);
|
||||
}
|
||||
if attachment_reference_ids.len() > EDITOR_AGENT_MAX_ATTACHMENTS {
|
||||
return Err(EditorAgentError::TooManyAttachments);
|
||||
}
|
||||
if attachment_reference_ids
|
||||
.iter()
|
||||
.any(|reference_id| normalize_required_string(reference_id).is_none())
|
||||
{
|
||||
return Err(EditorAgentError::InvalidAttachmentReference);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::derive_conversation_title;
|
||||
|
||||
#[test]
|
||||
fn derive_title_truncates_and_normalizes_whitespace() {
|
||||
assert_eq!(
|
||||
derive_conversation_title(" 帮我生成\n一张森林背景 "),
|
||||
"帮我生成 一张森林背景"
|
||||
);
|
||||
assert_eq!(derive_conversation_title(""), "新对话");
|
||||
assert_eq!(derive_conversation_title(" \n "), "新对话");
|
||||
let long_text = "一二三四五六七八九十一二三四五六七八九十超出部分";
|
||||
assert_eq!(derive_conversation_title(long_text).chars().count(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_user_message_rules() {
|
||||
assert_eq!(
|
||||
validate_user_message("", &[]),
|
||||
Err(EditorAgentError::EmptyMessage)
|
||||
);
|
||||
assert!(validate_user_message("", &["resource-1".to_string()]).is_ok());
|
||||
assert!(validate_user_message("画一棵树", &[]).is_ok());
|
||||
let too_many: Vec<String> = (0..10).map(|i| format!("resource-{i}")).collect();
|
||||
assert_eq!(
|
||||
validate_user_message("画一棵树", &too_many),
|
||||
Err(EditorAgentError::TooManyAttachments)
|
||||
);
|
||||
assert_eq!(
|
||||
validate_user_message("画一棵树", &[" ".to_string()]),
|
||||
Err(EditorAgentError::InvalidAttachmentReference)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_conversation_accessible_rules() {
|
||||
assert!(ensure_conversation_accessible("user-1", false, "user-1").is_ok());
|
||||
assert_eq!(
|
||||
ensure_conversation_accessible("user-1", false, "user-2"),
|
||||
Err(EditorAgentError::NotConversationOwner)
|
||||
);
|
||||
assert_eq!(
|
||||
ensure_conversation_accessible("user-1", true, "user-1"),
|
||||
Err(EditorAgentError::ConversationDeleted)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EditorAgentConversationCreateInput {
|
||||
pub conversation_id: String,
|
||||
pub project_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub title: String,
|
||||
pub messages_object_key: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EditorAgentConversationTouchInput {
|
||||
pub conversation_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub title: Option<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EditorAgentConversationDeleteInput {
|
||||
pub conversation_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EditorAgentConversationListInput {
|
||||
pub project_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EditorAgentConversationGetInput {
|
||||
pub conversation_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user