This commit is contained in:
2026-04-24 22:25:13 +08:00
parent 75681751c2
commit 67062a8af3
43 changed files with 1857 additions and 268 deletions

View File

@@ -6,7 +6,10 @@
## 落地规则
- 作品卡右上角固定展示删除 icon底部主操作区只保留继续创作、查看详情、体验等正向操作
- 作品卡整体就是继续创作 / 继续完善 / 查看详情入口,不再在底部展示“继续完善”等重复主按钮
- 作品卡右上角固定展示删除 icon底部主操作区只保留体验等必须独立触发的正向操作。
- 点击作品卡任意非独立按钮区域都进入继续完善链路;点击删除或体验时不得冒泡触发作品卡打开。
- 作品卡保留键盘可访问性:焦点落在卡片时按 Enter 或空格等同点击作品,焦点落在删除 / 体验按钮时只执行对应按钮动作。
- 删除入口不按发布状态隐藏:草稿、已发布作品均可删除。
- 删除入口不按玩法类型隐藏RPG、大鱼吃小鱼、拼图作品均应在创作页可删除。
- 点击删除前保留浏览器确认弹窗,避免误触;删除中仅禁用当前作品卡的删除 icon。
@@ -18,3 +21,5 @@
- 大鱼作品按 `sourceSessionId` 删除创作 session并同步清理消息、素材槽和运行快照。
- 拼图作品按 `profileId` 删除作品 profile并同步清理来源 Agent session、消息和入口运行快照。
- RPG 已发布/持久草稿按 `profileId` 走既有自定义世界删除链路;纯 Agent session 草稿按 `sessionId` 走 owner-only session 删除过程,并清理消息、操作与草稿卡。
- 自定义世界 Agent 的异步进度写回必须通过 `upsert_custom_world_agent_operation_progress` 过程落到 SpacetimeDB`server-rs` 只做字符串入参与过程封装,不在 API 层维护额外进度状态。
- `server-rs` 的删除路由使用 Axum 标准 `Path(sessionId)` 提取参数,并在进入 SpacetimeDB 前做 owner-only 与空值校验,避免 handler 签名和过程入参漂移。

View File

@@ -71,6 +71,7 @@
弹窗内默认只保留:
- 标题:`登录账号`
- 登录方式页签:`短信登录` / `密码登录`
- 手机号输入框
- 验证码输入框
- 获取验证码按钮
@@ -88,6 +89,17 @@
- “先登录再同步进度”这类描述性文案
- 占据视觉主体的装饰信息块
## 3.2.1 登录页签落地约束
账号面板需要把短信验证码登录和密码登录拆成互斥页签,避免两个登录表单在同一个面板里上下堆叠。
- 同时开放短信与密码登录时,面板顶部展示两个居中的文字页签,当前页签使用深色字重和短下划线强调。
- 只渲染当前页签对应的输入区;切换页签不弹出新面板,不展示二维码入口。
- `短信登录` 页签包含手机号、验证码、获取验证码和主按钮。
- `密码登录` 页签包含手机号/邮箱、密码、主按钮和忘记密码入口。
- 未开放某个登录方式时不展示对应页签,避免用户进入不可用表单。
- 移动端页签保持等分点击区域,输入框与按钮宽度仍随弹窗收缩。
## 3.3 登录成功后的行为
- 手机号登录成功后,关闭弹窗

View File

@@ -24,4 +24,8 @@
- PC 端使用更明确的 `xl:grid`、固定信息侧栏和更小间距,让主内容首屏承载更多信息。
- 卡片在 PC 端降低无效高度,操作按钮与状态信息尽量同行展示。
- 作品卡片底部统计标签必须保留在卡片圆角范围内,不能为了压缩高度让标签贴边或被 `overflow-hidden` 裁掉。
- 卡片正文摘要优先缩短行数来给底部标签留空间;当标题、摘要或标签变长时,允许卡片自然增高。
- RPG 作品卡片点击行为按作品状态分流:草稿统一继续创作,已发布作品进入详情或世界;不要只依赖 `sourceType` 判断草稿可打开性。
- 整张作品卡片需要由卡片根节点承载点击与键盘打开能力,避免透明绝对定位按钮在真实浏览器中被判定不可见,导致自动化和用户点击不稳定。
- 保留现有 `platform-*` 视觉体系,避免引入新的 UI 系统。

View File

@@ -23,3 +23,7 @@
- 只需要读一份时,优先看 `PROJECT_WORK_EXPERIENCE_PLAYBOOK`
- 做 UI 改动时,把本目录和根目录的 `UI_CODING_STANDARD.md` 对照着看。
- 做运行时流程改动时,把本目录和 `docs/audits/engineering/README.md` 一起看,能更快发现风险边界。
## 近期专项记录
- [RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md](./RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md):记录 RPG 底稿阶段角色主形象与场景背景图并行生成约束。

View File

@@ -0,0 +1,26 @@
# RPG 幕背景默认描述来源修正 2026-04-24
## 背景
草稿编辑器中“AI 生成幕背景”的“画面内容描述”曾出现类似“温馨员工宿舍第1幕背景玩家入职后的首个落脚处玩家会在温馨员工宿舍接住这一章的开场入口。”的默认文本。这类文本不是大模型直接写出的画面描述而是前端或后端在缺少 `backgroundPromptText` 时,把地点名、幕标题、摘要规则句拼接出来的兜底文案。
## 落地约束
1. 幕背景图的默认画面描述必须来自草稿生成链路里的关键场景生成步骤,字段源为 `landmarks[*].actBackgroundPromptTexts[*]`
2. `sceneChapterBlueprints[*].acts[*].backgroundPromptText` 只承接上述幕级大模型产物,不再用 `title``summary`、地点描述或规则句拼接。
3. 如果大模型漏产某一幕描述,后端规范化只保留空字符串,让后续生图前的 `backgroundPromptText` 校验暴露底稿质量问题,不能伪造可用默认文本。
4. 前端编辑器 sanitize 只展示已有 `act.backgroundPromptText`;缺失时留空,不能在 UI 层重新拼接默认描述。
5. 手动打开 AI 生成面板时,若字段为空,可由用户输入,但系统默认不替用户生成规则句。
## 当前实现
- `server-rs/crates/api-server/src/custom_world_foundation_draft.rs` 的关键场景框架 prompt 要求 LLM 为每个地点生成 3 条 `actBackgroundPromptTexts`
- 草稿合成阶段通过 `build_scene_chapter_blueprints_from_landmarks` 把这些幕级描述写入 `sceneChapterBlueprints[*].acts[*].backgroundPromptText`
- `normalize_scene_act_blueprint` 不再把缺失描述补成“标题 + 摘要 + 通用场景背景”格式。
- `src/components/rpg-creation-editor/RpgCreationEntityEditorShared.tsx` 不再用地点名、幕标题和 `actSummary` 生成 `backgroundPromptText` fallback。
## 验收要点
- 新草稿中每一幕的 `backgroundPromptText` 应该像自然的画面描述,包含主体、前中远景、站位空间、氛围识别点。
- 不应再出现“第1幕背景玩家会在……”这类明显拼接句。
- 如果 LLM 漏掉 `actBackgroundPromptTexts`,生成幕背景图阶段应失败并提示缺少 `backgroundPromptText`,而不是静默使用拼接文案。

View File

@@ -0,0 +1,32 @@
# RPG 底稿图片并行生成说明 2026-04-24
## 背景
RPG 草稿生成进入底稿素材阶段后,角色主形象与场景幕背景图都依赖同一份结构化底稿,但二者之间没有数据依赖。旧流程先生成所有角色主形象,再生成场景背景图,导致用户需要串行等待两类图片任务。
## 落地约束
1. 角色主形象与场景背景图必须在 API 编排层并行发起,且类内每个角色、每一幕背景也必须同时调用生图接口,不能只做到“角色大类”和“背景大类”并行。这里的场景背景图指 `sceneChapterBlueprints[*].acts[*]` 中每一幕的 `backgroundImageSrc`,不是世界封面图,也不是只按章节或地点生成一张图。
2. SpacetimeDB reducer 只负责持久化操作进度和底稿写入,不承载外部 LLM / 图片生成调用。
3. 生图前必须已经有文本设定:角色主形象使用角色对象的 `visualDescription`;幕背景图使用对应幕的 `backgroundPromptText`。缺字段时应中断并暴露底稿质量问题,不能退回 `description``summary` 或通用兜底词直接生图。
4. 并行分支各自基于同一份底稿副本写入素材字段,完成后只合并背景图生成产物字段,避免覆盖角色图片字段或其他草稿内容。
5. 单个大类失败仍按原有失败语义终止底稿写入,保留“生成角色主形象失败”和“生成幕背景图失败”的进度提示。
## 当前实现
- `server-rs/crates/api-server/src/custom_world.rs``spawn_custom_world_draft_foundation_job` 中使用 `tokio::join!` 同时执行:
- `generate_draft_foundation_role_visuals`
- `generate_draft_foundation_act_backgrounds`
- 角色分支使用 `JoinSet` 把所有角色主形象任务一次性投递,返回后再按角色位置写入 `imageSrc``generatedVisualAssetId`
- 背景分支使用 `JoinSet``sceneChapterBlueprints[*].acts[*]` 的每一幕背景任务一次性投递,返回后写入 `backgroundImageSrc``backgroundAssetId``generatedScenePrompt``generatedSceneModel`
- `merge_generated_act_backgrounds` 只把背景图字段合并回角色分支副本,再进入后续草稿卡编译和 SpacetimeDB 写入。
- 幕背景 prompt 同时兼容 `backgroundPromptText``scenePromptText``visualPromptText``promptText``imagePromptText``backgroundPrompt``visualPrompt`,避免 LLM 输出字段别名导致整批背景图被误判缺失。
- 每个角色主形象、每一幕背景图都必须独立自动重试,单项最多尝试 3 次。任一单项超过 3 次仍失败时,后台任务必须把 operation 标记为 `failed` 并停止写入草稿卡,避免生成“缺主图 / 缺背景图”的可进入世界档案。
- 中止前必须持久化已经成功生成的部分底稿到会话 `draftProfile`,不能因为某个角色或某一幕失败而丢掉其它已生成的 `imageSrc / generatedVisualAssetId / backgroundImageSrc / backgroundAssetId`
- 前端 `CharacterAnimator` 对带 `generatedVisualAssetId` 但尚无 `animationMap` 的自定义角色,所有状态优先渲染生成主图;只有真正发布了动作集后才按动作帧播放,避免运行或战斗状态回落到模板 sprite。
## 后续注意
如果后续图片供应商出现强限流,再在网关层做队列或供应商侧限流;不要在 RPG 底稿编排层恢复逐张串行,否则会重新退化成多张图片总耗时累加。

View File

@@ -0,0 +1,30 @@
# RPG 角色形象描述数据链路核查 2026-04-24
## 结论
草稿生成阶段会让大模型为每个可扮演角色和场景角色生成 `visualDescription`,该字段是角色主形象生成和资产工坊“形象描述”输入框的同一份默认文本来源。
本次核查发现前端 `normalizeCustomWorldProfileRecord` 曾在规范化 `playableNpcs` / `storyNpcs` 时丢弃 `visualDescription``actionDescription``sceneVisualDescription`。因此后端草稿 JSON 中有大模型生成的文字,但草稿进入前端编辑器后,资产工坊可能只能回退到 `description`,用户看不到真正的角色形象文字描述。
## 数据链路
1. 后端草稿生成:`server-rs/crates/api-server/src/custom_world_foundation_draft.rs`
- 角色框架名单 prompt 要求 LLM 输出 `visualDescription``actionDescription``sceneVisualDescription`
- `visualDescription` 定义为打开角色形象图像生成面板时默认填入的角色形象描述。
2. 后端角色主形象生成:`server-rs/crates/api-server/src/custom_world.rs`
- `generate_draft_foundation_role_visuals` 从角色对象读取 `visualDescription`
- 缺失时直接失败,提示不能在角色形象设定文本生成前生图。
- 生图成功只写回 `imageSrc``generatedVisualAssetId`,不会覆盖 `visualDescription`
3. 草稿持久化:草稿 profile JSON 保留角色对象字段,`visualDescription` 应与图片字段一起进入保存载荷。
4. 前端规范化:`src/data/customWorldLibrary.ts`
- `normalizePlayableNpc` / `normalizeStoryNpc` 必须保留三类资产描述字段。
5. 资产工坊展示:`src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModalImpl.tsx`
- modal 用角色对象构造 `baseRole`
- `buildDefaultRolePromptBundle(baseRole)` 优先把 `role.visualDescription` 转成 `visualPromptText`
- `RpgCreationRoleVisualSection` 的“形象描述” TextArea 展示 `visualPromptText`
## 验收要点
- 草稿生成完毕后,打开某个角色的资产工坊,应在“形象描述”框看到 LLM 生成的 `visualDescription`
- 如果角色有 `visualDescription`,缓存中的旧 `visualPromptText` 不应覆盖它。
- 如果角色缺 `visualDescription`,才允许前端回退到更弱的字段或缓存文本。

View File

@@ -361,10 +361,26 @@
1. `id`
2. `name`
3. `role`
4. `publicMask`
5. `hiddenHook`
6. `relationToPlayer`
7. `summary`
4. `description`
5. `visualDescription`
6. `actionDescription`
7. `sceneVisualDescription`
8. `publicMask`
9. `hiddenHook`
10. `relationToPlayer`
11. `summary`
### 角色资产工坊默认文本来源
`visualDescription` 是角色主形象生成入口的默认形象描述主源,必须在可扮演角色 / 场景角色的草稿生成步骤中跟随角色一并生成,不允许在资产工坊打开时再用本地规则把 `description``role``tags` 拼成默认文案。
生成要求:
1. `description` 只写角色定位,控制在 8 到 18 个汉字内,用于角色卡摘要。
2. `visualDescription` 专门写角色外观,包含轮廓、服饰 / 身体特征、携带物或材质气质,不写性格规则和玩法说明。
3. `actionDescription` 专门写动作气质,用于动作生成默认文本。
4. `sceneVisualDescription` 专门写角色常出现的场景氛围,用于场景图或角色场景联动默认文本。
5. 资产工坊默认值优先读取 `visualDescription`,只有历史草稿缺失该字段时才允许回退到 `description`
### 插入规则

View File

@@ -0,0 +1,45 @@
# AI 生成过程草稿持久化设计2026-04-24
## 1. 背景
当前创作类模板已经具备 session / message / operation 级别的最终态落库能力,但部分流式生成只把模型增量推给前端。若 HTTP/SSE 连接、浏览器页面或 LLM 请求在最终解析前中断,用户只能看到短暂流式文本,服务端缺少可恢复的生成中间态。
本设计补齐“生成过程中已经生成的内容必须持续持久化”的机制,并要求该机制对所有创作模板统一生效。
## 2. 目标
1. 每次模板生成开始前创建或绑定一个 `ai_task`
2. 模型每次产出可见文本增量时,写入 `ai_text_chunk`,并同步更新 `ai_task.latest_text_output` 与对应 stage 的 `text_output`
3. 生成失败或连接中断时,不丢弃已经落库的 chunk后续可用 `ai_task.latest_text_output` 作为续写上下文。
4. 成功解析并 finalize 后,将最终结构化结果继续写回各模板原有 session 表,保持现有业务快照不变。
## 3. 统一落库边界
### 3.1 真相表
- `ai_task`:记录一次模板生成任务的业务来源、状态、最新聚合文本、结构化结果。
- `ai_task_stage`:记录模板生成阶段状态;当前创作对话统一使用 `DraftGeneration`
- `ai_text_chunk`:按 `sequence` 追加保存模型增量文本,是断点恢复的最小粒度。
### 3.2 适用模板
- 自定义世界创作 Agent。
- 解谜游戏创作 Agent。
- 大鱼吃小鱼创作 Agent。
- 后续新增模板必须复用同一生成草稿持久化工具,不允许只在 UI 内存保存流式文本。
## 4. 续写策略
1. 发起生成时,后端根据 `template_key + session_id + operation_id` 创建稳定 `task_id`
2. LLM 流式回调收到 `replyText` 的最新可见文本后,计算相对上一次文本的增量;只有非空增量写入 `ai_text_chunk`
3. 写入失败不应阻断当前生成主流程,但必须记录 warn 日志,避免因持久化瞬时失败导致用户生成直接失败。
4. 若最终解析失败,`ai_task` 保持 `Running` 或显式 `Failed`,已写入的 `latest_text_output` 仍可作为下一轮 prompt 的“已生成草稿”。
5. 下一轮续写 prompt 应优先带上最近未完成任务的 `latest_text_output`;本次先落地服务端 chunk 持久化能力,后续模板 prompt 可逐步消费该草稿。
## 5. 编码要求
1. 持久化逻辑放在 `server-rs/crates/api-server` 的通用工具中,由各模板路由接入。
2. 不引入 `server-node` 兼容分支。
3. SpacetimeDB 写入必须通过 `spacetime-client` 已生成绑定,不在 reducer 中访问网络或文件系统。
4. 所有新增 Rust 代码保留中文注释,且只做局部修改,避免重写包含中文的大文件。

View File

@@ -0,0 +1,41 @@
# 世界草稿图片生成与预览补齐说明
更新时间:`2026-04-24`
## 1. 检查结论
当前 server-rs 的世界底稿生成链路已经在 `draft_foundation` 后台任务中补齐两类图片:
1. `playableNpcs``storyNpcs` 中的每个角色都会调用角色主形象生成链路,并把 `imageSrc``generatedVisualAssetId` 写回底稿。
2. `sceneChapterBlueprints[].acts[]` 中的每一幕都会调用场景图生成链路,并把 `backgroundImageSrc``backgroundAssetId`、生成提示词与模型信息写回底稿。
图片生成后不落本地真值,而是通过 OSS `put_object -> head_object -> confirm_asset_object -> bind_asset_object_to_entity` 确认对象,并用兼容的 `/generated-*` 路径供前端读取。
## 2. 前端缺口
结果页的场景列表此前只把每个场景的第一张幕背景图作为场景卡封面。这样虽然后端已经生成了每一幕图片,但用户只能看到第一幕,无法在结果页确认同一场景下其他幕的图片是否存在。
## 3. 本次落地
1.`CustomWorldEntityCatalog` 中增加每幕图片缩略条,来源为当前场景匹配到的 `sceneChapterBlueprints[].acts[].backgroundImageSrc`
2. 保留原来的场景卡封面策略:第一幕背景图仍作为主封面,旧的场景图字段继续作为兜底。
3. 缩略条只展示已生成图片的幕,不额外暴露章节结构文本,避免结果页变成规则说明面板。
4. 增加结果页测试,覆盖同一场景下两幕背景图都能在前端以图片形式预览。
## 4. 验收点
1. 生成世界草稿完成后,角色页签中所有可扮演角色和场景角色能展示 `imageSrc`
2. 场景页签中,每个场景卡片仍展示主封面。
3. 场景卡片下方能横向预览该场景所有已生成幕背景图。
4. OSS 未配置或上传失败时,后端任务应失败并把错误写入 operation而不是生成伪本地路径。
## 5. 上游图片服务失败降级
`draft_foundation` 的底稿文本结构是进入结果页和继续编辑的主产物,角色主图、幕背景图属于可后补资产。若 DashScope 或 OSS 上游临时不可用,后台任务不应把整份底稿标记为失败。
本次补充后:
1. 角色主图分支失败时operation 记录错误信息并继续使用未带角色图的底稿。
2. 幕背景图分支失败时operation 记录错误信息并继续使用未带幕图的底稿。
3. 已成功的并行资产分支仍会合并回底稿,不会被失败分支覆盖。
4. 后续可通过资产工坊或单项生成动作补齐缺失图片。

View File

@@ -37,8 +37,9 @@ npm run dev:rust
4. 等待 `spacetime --root-dir=server-rs/.spacetimedb/local server ping http://127.0.0.1:3101` 可用。
5. 执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`,确保 publish 的签名身份与 standalone 的本地控制库一致,并在当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
6. 注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
7. 注入 `GENARRATIVE_BACKEND_STACK=rust``RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite
8. 任一子进程退出时,脚本回收其余子进程
7. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`
8. 注入 `GENARRATIVE_BACKEND_STACK=rust``RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite
9. 任一子进程退出时,脚本回收其余子进程。
Vite 代理覆盖范围:
@@ -84,6 +85,13 @@ npm run dev:rust:logs -- --follow
1. 如果首页公开广场出现 `上游服务请求失败`,优先检查 `api-server` 错误详情里的 `ws://.../v1/database/<database>/subscribe` 是否指向了未发布的库。
2. `spacetime --root-dir=server-rs/.spacetimedb/local list --server http://127.0.0.1:3101` 应能看到 `spacetime.local.json` 中的库名;若没有,执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`
3. 发布库名与 `GENARRATIVE_SPACETIME_DATABASE` 不一致时,`/api/runtime/custom-world-gallery` 会从 Rust `api-server` 返回 `502`,前端首页只能展示空态或错误提示,无法自行修复。
4. 如果 Vite 输出 `/api/auth/refresh``/api/auth/login-options``/api/runtime/custom-world-gallery``ECONNREFUSED`,先确认当前脚本是否已经打印 `等待 api-server 就绪` 并通过;正常情况下 Vite 只会在 `/healthz` 可访问后启动,不应再因为 Rust 监听未完成而代理失败。
编译警告治理:
1. Rust 本地栈启动日志应保持可行动,运行态未使用函数不应长期保留为普通编译警告。
2. 仅供测试断言使用的辅助函数使用 `#[cfg(test)]` 限定,避免进入 `cargo run -p api-server` 的普通二进制编译。
3. 已无调用入口且无迁移价值的映射函数直接删除;如果后续新增同类 SpacetimeDB 记录映射,再按实际调用路径补回,避免提前保留死代码。
## 3. Ubuntu 发布包脚本