codex/frontend-error-dialogs #40

Merged
kdletters merged 6 commits from codex/frontend-error-dialogs into master 2026-05-26 22:28:59 +08:00
119 changed files with 8912 additions and 6588 deletions
+42 -2
View File
@@ -16,6 +16,30 @@
---
## 2026-05-26 平台跨流程错误统一用可复制来源弹窗展示
- 背景:拼图等生成链路可能同时存在多个草稿或游玩实例,页面内裸错误 banner 容易让用户误以为当前正在看的拼图失败,也不方便复制完整错误给开发排查。
- 决策:平台入口、生成页、结果页、作品详情、作品架和运行态的跨流程错误统一收口到 `PlatformErrorDialog`;弹窗必须带错误来源,例如某个草稿、生成会话、作品详情或游玩实例,并提供复制按钮复制来源与错误内容。页面内旧的裸错误 banner、创作入口 modal 错误、生成页错误徽标等不再重复展示;表单校验和发布确认弹窗里的局部业务错误仍可保留在原弹窗内。
- 影响范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``src/components/platform-entry/PlatformErrorDialog.tsx``src/components/CustomWorldGenerationView.tsx``src/components/custom-world-home/CustomWorldCreationHub.tsx``src/components/custom-world-home/CustomWorldCreationStartCard.tsx``src/components/platform-entry/PlatformWorkDetailView.tsx``src/components/platform-entry/PlatformEntryCreationTypeModal.tsx``src/components/puzzle-result/PuzzleResultView.tsx`
- 验证方式:`npm run test -- src/components/platform-entry/PlatformErrorDialog.test.tsx src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx``npm run typecheck``npm run check:encoding` 通过;手测时异步失败应弹出包含“错误来源”和“错误内容”的弹窗,复制按钮应复制完整诊断文本。
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-05-26 生成任务完成在离开生成页后弹独立完成弹窗
- 背景:抓大鹅、拼图等生成任务完成时,用户如果已经离开生成页,草稿页的未读红点不足以表达“这次生成已完成”;但如果用户仍停留在生成页,结果页或试玩页本身就是完成反馈,不需要再叠一个成功提示。
- 决策:平台壳层在 `markDraftReady(..., viewedImmediately=false)` 时额外弹出 `PlatformTaskCompletionDialog`,完成弹窗必须带来源和复制按钮;如果 `viewedImmediately=true`,只保留结果页 / 试玩页本身的完成反馈和草稿未读态,不重复弹窗。
- 影响范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``src/components/platform-entry/PlatformTaskCompletionDialog.tsx``src/components/platform-entry/PlatformErrorDialog.test.tsx``src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
- 验证方式:`npm run test -- src/components/platform-entry/PlatformErrorDialog.test.tsx``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "completed match3d draft"` 通过后,离开生成页再完成的草稿应出现“生成完成”弹窗,且复制内容包含来源与状态。
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-05-26 “我的”页任务卡读后端任务摘要并移除常驻填邀请码入口
- 背景:移动端“我的”页每日任务卡曾硬编码 `0 / 1`,任务领取完成后只刷新弹窗内任务中心,卡片本身不更新;页面底部还保留旧的“填邀请码”次级按钮,和当前五项常用功能宫格口径重复。
- 决策:`RpgEntryHomeView` 的每日任务卡以 `/api/profile/tasks` 返回的任务中心为事实源,展示当前可操作任务的奖励、进度和状态;领取成功后同步使用 claim 响应里的 `center` 刷新卡片。移动端“我的”页不再渲染常驻“填邀请码”次级入口,邀请码填写仅保留邀请链接 query 自动打开弹窗和其它明确引导。
- 影响范围:`src/components/rpg-entry/RpgEntryHomeView.tsx``src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx``docs/【项目基线】当前产品与工程约束-2026-05-15.md`
- 验证方式:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx` 应断言任务卡显示 `1 / 1`、领取后显示已完成,且新用户账号也没有 `次级入口` / `填邀请码` 常驻按钮;`npm run typecheck``npm run check:encoding` 通过。
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`
## 2026-05-25 抓大鹅发现页官方 demo 使用静态资源与本地运行态
- 背景:本轮抓大鹅资源管线已经生成完整 `level-scene`、背景、UI spritesheet、物品 spritesheet 和切片资源,需要放入发现页作为可试玩验证入口,但不应把一次性本地资源包装成后端正式作品。
@@ -144,6 +168,14 @@
- 验证方式:执行 `cargo test -p api-server wooden_fish --manifest-path server-rs/Cargo.toml``cargo test -p spacetime-client wooden_fish --manifest-path server-rs/Cargo.toml``npm run spacetime:generate``npm run check:spacetime-schema``npm run typecheck`
- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-05-25 通用系列素材图集实现下沉到 platform-image
- 背景:`generated_asset_sheets` 同时承载 sheet prompt、切图、绿幕去背、边缘 matte 清理和 OSS 持久化准备,长期放在 `api-server` 会把多个玩法的图片 seam 继续绑死在 HTTP crate 上。
- 决策:通用系列素材图集的实现真值源下沉到 `platform-image::generated_asset_sheets``api-server::generated_asset_sheets` 只保留 `AppState` / `AppError` 适配与调用方兼容导出,不再承载图像处理和 OSS 请求构造细节。
- 影响范围:`server-rs/crates/platform-image/src/generated_asset_sheets/``server-rs/crates/api-server/src/generated_asset_sheets.rs``server-rs/crates/api-server/src/match3d/item_assets.rs``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
- 验证方式:`cargo test -p platform-image --test generated_asset_sheets --manifest-path server-rs/Cargo.toml``cargo check -p api-server --manifest-path server-rs/Cargo.toml` 通过;调用方继续通过 `api-server` 的薄包装访问同一组能力。
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-05-22 敲木鱼敲击物暂不做服务端抠图后处理
- 背景:gpt-image-2 偶尔会把木鱼图直接回成带黑底或其它实底背景的 PNG,但服务端抠图后处理在玉米等主题上误伤过主体像素。
@@ -172,11 +204,19 @@
## 2026-05-25 VectorEngine 图片 provider 收到 platform-image
- 背景:`api-server` 里原本同时混着 VectorEngine 创建 / 编辑协议、响应解析、远端图片下载、失败日志和审计落库逻辑,Puzzle / Match3D 还各自藏着一份近似实现,导致“provider 协议”和“业务编排”边界不清。
- 决策:把 VectorEngine `gpt-image-2` 图片 provider 协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志统一收口到 `server-rs/crates/platform-image``api-server` 只保留配置校验、玩法 prompt 编排、OSS / asset object / binding 持久化、计费和外部 API 失败审计桥接;旧 `openai_image_generation.rs` 只作为兼容转接层,不再承担 provider 实现。
- 决策:把 VectorEngine `gpt-image-2` 图片 provider 协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志统一收口到 `server-rs/crates/platform-image/src/vector_engine/`,并按 `client.rs``transport.rs``request.rs``payload.rs``response.rs``image_source.rs` 等小模块拆分,避免把大文件从 `api-server` 平移到平台 crate`api-server` 只保留配置校验、玩法 prompt 编排、OSS / asset object / binding 持久化、计费和外部 API 失败审计桥接;旧 `openai_image_generation.rs` 只作为兼容转接层,不再承担 provider 实现。
- 影响范围:`server-rs/crates/platform-image``server-rs/crates/api-server/src/openai_image_generation.rs``server-rs/crates/api-server/src/puzzle/vector_engine.rs``server-rs/crates/api-server/src/external_api_audit.rs`、后端架构与运维文档。
- 验证方式:`cargo test -p platform-image --manifest-path server-rs/Cargo.toml``cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml -- --nocapture``cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml -- --nocapture``cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:encoding`
- 验证方式:`cargo test -p platform-image --manifest-path server-rs/Cargo.toml``cargo test -p platform-image --test vector_engine --manifest-path server-rs/Cargo.toml``cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml -- --nocapture``cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run check:encoding`
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`
## 2026-05-26 音频 provider 协议收口到 platform-audioHyper3D 继续保持薄代理
- 背景:`api-server/src/vector_engine_audio_generation.rs``api-server/src/hyper3d_generation.rs` 仍然承担太多 provider 细节,容易把外部协议、下载、解析和 BFF 编排混在一起。
- 决策:VectorEngine Suno/Vidu 音频协议、任务提交/轮询、下载和 OSS 持久化请求准备收口到 `platform-audio`,并继续按 `client.rs``request.rs``response.rs``download.rs``persist.rs``error.rs` 拆小模块;`api-server` 只保留路由、配置、计费、asset_object confirm、entity binding 和错误映射。Hyper3D 维持后端安全代理和旧数据兼容,`platform-hyper3d` 承接 Rodin 的协议与解析,`api-server` 仅做薄 wrapper。
- 影响范围:`server-rs/crates/platform-audio/``server-rs/crates/platform-hyper3d/``server-rs/crates/api-server/src/vector_engine_audio_generation.rs``server-rs/crates/api-server/src/hyper3d_generation.rs`、相关后端架构文档。
- 验证方式:`cargo test -p platform-audio --manifest-path server-rs/Cargo.toml``cargo test -p platform-hyper3d --manifest-path server-rs/Cargo.toml``cargo check -p api-server --manifest-path server-rs/Cargo.toml` 通过;`api-server` 不再包含音频 provider 协议和 Hyper3D parser 主实现。
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/technical/【后端架构】复杂媒体资产链路Adapter扩展计划-2026-05-14.md`
## 2026-05-21 拼图参考图主链改为 OSS assetObjectId 与只读签名 URL
- 背景:release 上拼图图生图生成草稿时,旧链路把上传图转成 Data URL/base64 放进创作 action JSON body,容易先触发 Nginx `413 Request Entity Too Large`,也让外部模型调用前的 HTTP body 过大。
+51 -3
View File
@@ -15,6 +15,30 @@
- 关联:相关文件、文档、提交或 Issue
```
## 平台异步错误必须带来源弹窗,不要只显示裸错误
- 现象:用户先后触发多个拼图或草稿生成时,旧请求失败后会在当前页面显示“图片生成失败”等裸错误,容易误判为当前正在看的拼图失败;错误文本也不便复制给开发排查。
- 原因:不同入口、生成页、结果页、作品详情和运行态各自渲染局部错误,没有统一携带草稿、生成会话、作品或游玩来源。
- 处理:跨流程错误统一由 `PlatformEntryFlowShellImpl` 汇总为 `PlatformErrorDialog`,来源使用玩法、草稿 / session / work / run 标识组成;弹窗提供复制按钮。关闭弹窗时只清理可安全清理的错误状态;恢复类错误用 dismiss key 防止反复弹出但不擅自改底层状态。
- 验证:触发任一平台级异步失败时,页面应出现包含“错误来源”和“错误内容”的弹窗;复制内容应包含来源和错误正文;旧页面内错误 banner 不再重复出现。
- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``src/components/platform-entry/PlatformErrorDialog.tsx``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## “我的”页每日任务卡不要硬编码进度
- 现象:用户完成或领取每日任务后,任务中心弹窗里的任务状态已经变化,但“我的”页卡片仍显示 `0 / 1` 和“去完成”。
- 原因:卡片首版只写了静态展示文案,没有读取 `/api/profile/tasks` 返回的 `ProfileTaskCenterResponse`,领取接口返回的新 `center` 也只用于弹窗。
- 处理:进入“我的”页时读取任务中心,卡片用当前可操作任务或已领取任务派生奖励、进度条和操作状态;`claimRpgProfileTaskReward(...)` 成功后用响应里的 `center` 覆盖本地任务中心。
- 验证:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx` 应覆盖卡片从后端任务摘要显示 `1 / 1`,领取后显示已完成。
- 关联:`src/components/rpg-entry/RpgEntryHomeView.tsx``src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx``docs/【项目基线】当前产品与工程约束-2026-05-15.md`
## “我的”页不要恢复旧的填邀请码次级按钮
- 现象:移动端“我的”页在五项常用功能和设置入口下方又出现一个“填邀请码”按钮,看起来像旧入口残留。
- 原因:邀请码流程迁移后仍按新用户窗口保留 `canShowReferralRedeemShortcut` 次级入口;但当前页面口径已经固定为五项常用功能宫格,邀请码填写应由邀请链接 query 或明确引导打开弹窗。
- 处理:移除常驻 `次级入口` / `填邀请码` 渲染,不删除 `ProfileReferralModal``redeem` 面板,也不破坏 `?inviteCode=` / `?invite_code=` 自动打开填写弹窗。
- 验证:新用户账号打开“我的”页时没有 `次级入口``填邀请码` 按钮;带 `?inviteCode=spring-2026` 的登录用户仍自动打开邀请码弹窗并预填 `SPRING2026`
- 关联:`src/components/rpg-entry/RpgEntryHomeView.tsx``.hermes/skills/genarrative-profile-invite-flow/SKILL.md`
## 创作卡片点击要直达已有入口表单,别再保留空白入口页
- 现象:创作 Tab 模板卡点击后如果仍然停留在创作大厅,或者先进入“X 创作入口”这种空白页,就会让用户多走一层,还可能被错误的 stage 白名单拉回平台。
@@ -126,6 +150,14 @@
- 验证:`npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx` 应覆盖“运行态不把兼容写入的UI spritesheet当中心容器图”。
- 关联:`src/components/match3d-runtime/Match3DRuntimeShell.tsx``server-rs/crates/api-server/src/match3d/mappers.rs``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 通用系列素材图集先看 platform-image,不要先翻 api-server 大文件
- 现象:排查跳一跳、抓大鹅或其它玩法的系列素材图集切片 / 去绿 / 持久化时,最容易先打开 `api-server/src/generated_asset_sheets.rs`,结果在一个 60KB+ 大文件里找实现、测试和辅助函数,定位很慢。
- 原因:这条通用图片 seam 已经下沉到 `server-rs/crates/platform-image/src/generated_asset_sheets/``api-server` 只剩薄包装和调用方兼容;继续把 `api-server` 当真值源会把理解路径拉回旧位置。
- 处理:先看 `server-rs/crates/platform-image/src/generated_asset_sheets/mod.rs``prompt.rs``sheet.rs``alpha.rs``persist.rs``error.rs`,再看 `api-server/src/generated_asset_sheets.rs` 的 AppError / AppState 适配和玩法调用点。
- 验证:`cargo test -p platform-image --test generated_asset_sheets --manifest-path server-rs/Cargo.toml` 通过,且 `cargo check -p api-server --manifest-path server-rs/Cargo.toml` 保持绿灯。
- 关联:`server-rs/crates/platform-image/src/generated_asset_sheets/``server-rs/crates/api-server/src/generated_asset_sheets.rs``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## UI spritesheet 不要依赖模型直接生成透明背景
- 现象:拼图或抓大鹅运行态解析 UI spritesheet 时,把整张背景图、棋盘格、叶子或装饰图也当作 UI 素材区域,按钮映射错乱;截图里常表现为底部按钮区只剩透明棋盘格或素材碎片。
@@ -293,9 +325,25 @@
- 现象:排查拼图或其它玩法的生图失败时,如果直接在 `api-server` 的大文件里找 `images/generations``images/edits`、base64 解码或下载逻辑,会看到很多历史 helper 和测试桥,看起来像每个玩法都自带一份 provider 实现。
- 原因:旧实现把 VectorEngine 图片 provider 协议、响应解析、下载和日志混在 `api-server` 里,后来虽然迁出到 `platform-image`,但兼容层和测试 helper 仍会让人误判真相源位置。
- 处理:先看 `server-rs/crates/platform-image/src/lib.rs` 的 provider 协议和结构化日志,再看 `server-rs/crates/api-server/src/openai_image_generation.rs` 的兼容桥和 `external_api_audit.rs` 的落库映射;`puzzle/vector_engine.rs` 只保留玩法编排,不再作为 provider 协议真相源。
- 验证:`cargo test -p platform-image --manifest-path server-rs/Cargo.toml``cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml -- --nocapture``cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml -- --nocapture` 通过时,排障先按 `platform-image` 的日志字段查 provider / endpoint / failure_stage。
- 关联:`server-rs/crates/platform-image/src/lib.rs``server-rs/crates/api-server/src/openai_image_generation.rs``server-rs/crates/api-server/src/external_api_audit.rs``server-rs/crates/api-server/src/puzzle/vector_engine.rs`
- 处理:先看 `server-rs/crates/platform-image/src/vector_engine/``request.rs` 查路径和请求体,`client.rs` 查生成 / 编辑编排,`transport.rs` 查 HTTP client 与 reqwest 错误归一,`payload.rs` 查响应字段提取,`response.rs` 查上游状态、解析、缺图和下载分流,`image_source.rs` 查参考图和远端图片下载。再看 `server-rs/crates/api-server/src/openai_image_generation.rs` 的兼容桥和 `external_api_audit.rs` 的落库映射;`puzzle/vector_engine.rs` 只保留玩法编排,不再作为 provider 协议真相源。
- 验证:`cargo test -p platform-image --manifest-path server-rs/Cargo.toml``cargo test -p platform-image --test vector_engine --manifest-path server-rs/Cargo.toml``cargo test -p api-server openai_image_generation --manifest-path server-rs/Cargo.toml -- --nocapture` 通过时,排障先按 `platform-image` 的日志字段查 provider / endpoint / failure_stage。
- 关联:`server-rs/crates/platform-image/src/vector_engine/``server-rs/crates/api-server/src/openai_image_generation.rs``server-rs/crates/api-server/src/external_api_audit.rs``server-rs/crates/api-server/src/puzzle/vector_engine.rs`
## 音频 provider 协议先看 platform-audio,不要先翻 api-server 大文件
- 现象:排查 Visual Novel 或通用创作音频生成失败时,如果直接打开 `api-server/src/vector_engine_audio_generation.rs`,会同时看到路由、计费、asset binding、下载、解析和 provider 协议,定位时很容易在同一个文件里来回跳。
- 原因:音频 provider 已经迁到 `server-rs/crates/platform-audio/`,但 `api-server` 仍保留薄 wrapper;如果把 wrapper 当真值源,就会误判边界。
- 处理:先看 `server-rs/crates/platform-audio/src/client.rs``request.rs``response.rs``download.rs``persist.rs``error.rs`,再看 `api-server/src/vector_engine_audio_generation.rs` 的路由、配置、计费、asset object confirm 和 entity binding 包裹。
- 验证:`cargo test -p platform-audio --manifest-path server-rs/Cargo.toml` 通过,且 `cargo check -p api-server --manifest-path server-rs/Cargo.toml` 保持绿灯。
- 关联:`server-rs/crates/platform-audio/``server-rs/crates/api-server/src/vector_engine_audio_generation.rs`
## Hyper3D 现在只剩后端薄代理,不要再把协议解析写回 api-server
- 现象:排查 Hyper3D/Rodin 时,如果继续在 `api-server/src/hyper3d_generation.rs` 里扩协议解析、请求体构造或下载列表处理,文件会重新变厚。
- 原因:`platform-hyper3d` 已经承接 Rodin 的提交、状态和下载协议解析;`api-server` 只是薄 wrapper 和错误 envelope 映射。
- 处理:新增或修改 Hyper3D 协议时优先放到 `server-rs/crates/platform-hyper3d/``client.rs``request.rs``response.rs``transport.rs` 和子模块,`api-server` 只保留鉴权、配置校验和错误映射。
- 验证:`cargo test -p platform-hyper3d --manifest-path server-rs/Cargo.toml` 通过后再看 `cargo check -p api-server --manifest-path server-rs/Cargo.toml`
- 关联:`server-rs/crates/platform-hyper3d/``server-rs/crates/api-server/src/hyper3d_generation.rs`
## release 创作接口 413 先查是否还在提交 Data URL
@@ -88,6 +88,10 @@ Adapter 只负责媒体持久化和资产绑定,不负责:
| Custom World opening CG video | `custom_world_ai.rs` 或后续分层文件 | video | Ark/火山视频 task 结果 URL | 视频下载、OSS、confirm、binding | storyboard->video 顺序、固定点数计费、超时错误 | storyboard 图片仍走图片 Adapter,最终视频走 media persist |
| Character visual reference/workflow | `character_visual_assets.rs` | image/cache metadata | GPT image helper、workflow cache | 可复用 media persist 的 source/OSS/confirm/binding;图片生成 provider 不迁入复杂媒体 | 角色 workflow cache 可空继续生成 | 角色视觉发布链路回包字段不变 |
| Character animation publish/import | `character_animation_assets.rs` | video / image sequence | data:video base64、remote video、阶段占位 | data URL/base64 解码、视频 OSS、confirm、binding | stage1 placeholder 语义、import-video contract | 导入视频和发布视频都不再复制 OSS/confirm 代码 |
2026-05-26 补充:图片生成 provider 不再作为复杂媒体 Adapter 的实现细节散落在 `api-server`。VectorEngine `gpt-image-2` 创建 / 编辑协议、响应解析、URL / base64 图片归一、远端下载和 provider 侧结构化失败日志已经收口到 `server-rs/crates/platform-image/src/vector_engine/``api-server/src/openai_image_generation.rs` 只保留配置、兼容调用面和外部失败审计桥接。后续扩展视频、音频或 Hyper3D 时,可以复用“platform crate 承接 provider 协议,api-server 承接 HTTP/BFF、计费、OSS 绑定和失败审计桥接”的分层方式,但不得把新的 provider 协议塞回 `api-server` 大文件。
2026-05-26 补充:音频生成 provider 协议也不应继续挤在 `api-server/src/vector_engine_audio_generation.rs`。VectorEngine Suno/Vidu 的任务提交、轮询、下载、MIME/extension 归一和 OSS 持久化请求准备已经收口到 `server-rs/crates/platform-audio/`,并继续按 `client.rs``request.rs``response.rs``download.rs``persist.rs``error.rs` 拆成小模块;`api-server` 只保留路由、配置、计费、asset object confirm、entity binding 和错误 envelope 映射,不再承担 provider 协议和下载细节。后续若再增加音频子能力,也必须优先放进平台 crate,而不是扩张 `api-server`
2026-05-26 补充:Hyper3D 只保留后端安全代理和旧数据兼容,`api-server/src/hyper3d_generation.rs` 应保持薄 wrapper`platform-hyper3d` 承接 Rodin 的提交、状态和下载协议解析。若未来要继续压缩这一条线,应优先继续下沉协议解析与 transport helper,而不是把 provider 逻辑回流到 `api-server`
| Match3D 背景音乐 | `match3d.rs` | audio | 现有生成/上传链路 | 仅复用音频持久化 | 不恢复 Rodin/GLB 新草稿回退 | 图片素材仍按图片 Adapter 计划处理 |
| Puzzle 背景音乐 | `puzzle.rs` | audio | 现有生成/上传链路 | 仅复用音频持久化 | puzzle 运行态和排行榜语义不变 | `puzzle_background_music` kind/binding 不变 |
| Hyper3D/GLB 历史代理 | `hyper3d.rs` | model/glb | Hyper3D Rodin status/download | 如存在转存需求,仅复用 media persist | Match3D 新草稿禁止回退 Rodin/GLB | 历史代理 route contract 不变 |
@@ -119,10 +119,10 @@ npm run check:server-rs-ddd
2. Adapter 输入应显式包含 provider、prompt、reference images、OSS prefix/path/file name、asset kind、entity kind/id、slot、owner/profile/source job、metadata 和可选透明背景后处理。
3. Adapter 输出应保留 legacy public path、object key、asset object id、MIME、extension、task id 和实际 prompt。
4. Adapter 不负责扣费、退款或钱包读取;计费仍由调用方显式包裹。
5. 图片 provider 协议不再放在玩法模块里实现。VectorEngine `gpt-image-2` 创建 / 编辑协议、URL / base64 图片解析、远端图片下载、请求超时 / 上游状态 / 响应解析 / 缺图 / 下载失败的结构化日志统一在 `server-rs/crates/platform-image``api-server` 只负责配置校验、玩法 prompt 编排、OSS / asset object / binding 持久化、计费和外部 API 失败审计落库。
5. 图片 provider 协议不再放在玩法模块里实现。VectorEngine `gpt-image-2` 创建 / 编辑协议、URL / base64 图片解析、远端图片下载、请求超时 / 上游状态 / 响应解析 / 缺图 / 下载失败的结构化日志统一在 `server-rs/crates/platform-image/src/vector_engine/`;其中 `client.rs` 只保留 provider 调用编排,`transport.rs` 负责 HTTP client 与 reqwest 错误归一,`request.rs` 负责请求体和路径,`payload.rs` 负责响应 JSON 字段提取,`response.rs` 负责响应状态分流和图片结果归一。`api-server` 只负责配置校验、玩法 prompt 编排、OSS / asset object / binding 持久化、计费和外部 API 失败审计落库。
6. Puzzle、Match3D、音频、GLB、视频等复杂媒体可以复用 OSS + asset object + binding 的底层持久化能力,但玩法专属处理规则留在各自编排层,不塞进公共接口。
7. 拼图入口页与结果页新增关卡的本地参考图不走浏览器直传 OSS,前端读取为 Data URL 后随创作 action 提交,并在读取前限制 6MB、显示“图片≤6MB”。`api-server` 必须对 Data URL 实际字节数再次校验;历史图片才提交 `referenceImageAssetObjectId(s)`,后端校验 `asset_object` 的 bucket、kind、图片 MIME、大小和 owner 后签发只读 URL 给 VectorEngine 读取。
8. 系列素材图集使用 `server-rs/crates/api-server/src/generated_asset_sheets.rs`:调用方必须传入 `grid_size` 作为 `n*n``n`,可选传入物品名称 prompt 模板和特殊设定 prompt;模块负责 sheet prompt 组装、按 `n*n` 切片、透明化、PNG 输出、OSS private upload 请求构造和 sheet / item / special prompt 元数据持久化。玩法只负责规划 slot、调用具体生图 provider、计费、失败回写,以及把通用切片结果映射回自己的 DTO / 草稿 / runtime 字段。
8. 系列素材图集实现真相源在 `server-rs/crates/platform-image/src/generated_asset_sheets/`:调用方必须传入 `grid_size` 作为 `n*n``n`,可选传入物品名称 prompt 模板和特殊设定 prompt;模块负责 sheet prompt 组装、按 `n*n` 切片、透明化、PNG 输出、OSS private upload 请求构造和 sheet / item / special prompt 元数据持久化。`server-rs/crates/api-server/src/generated_asset_sheets.rs` 只保留 `AppState` / `AppError` 适配和兼容导出。玩法只负责规划 slot、调用具体生图 provider、计费、失败回写,以及把通用切片结果映射回自己的 DTO / 草稿 / runtime 字段。
## SpacetimeDB schema 变更规则
@@ -164,8 +164,8 @@ npm run check:server-rs-ddd
- Match3D UI spritesheet 和背景派生图:关卡整图作为参考图并发生成 `1K 1:1` UI spritesheet 与 `1K 9:16` 背景图,模型均为 `gpt-image-2`。UI spritesheet prompt 固定要求纯绿色绿幕背景,后端上传 OSS 前必须把绿幕扣成透明 PNG;背景图必须合成为全画幅不透明 PNG。
- Match3D 1:1 容器 UIVectorEngine `/v1/images/edits` multipart 参考图。该容器参考图是后端生图协议输入,必须通过 `include_bytes!``api-server` 编译进二进制,避免 API 单独发布或运行目录缺少 `public/` 时生成失败。
- 敲木鱼敲击物和背景环境图:VectorEngine `/v1/images/edits`,模型固定 `gpt-image-2`。敲击物支持 multipart 多参考图,第一张固定为后端内嵌默认木鱼图,用户上传图只作为新主题参考;prompt 必须要求 `1:1` 真实透明 alpha PNG 并禁止黑底、白底、棋盘格和任何实底背景。当前敲击物上传 OSS 前不做服务端抠图后处理,避免误伤玉米等主体像素。背景环境图只使用第一步抠图完成后的透明敲击物图作为参考,prompt 必须要求中央主体预留区保持干净,中央 40% 区域禁止出现主题主体、主体局部特写、轮廓影子或重复元素,主题元素只能作为外围氛围,且必须显式声明不继承任何绿色底色、绿幕底色或纯绿色画布。
- Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;新 Match3D 草稿和批量新增不再生成 GLB。
- 音频:视觉小说专用音频路由保留;拼图、抓大鹅和敲木鱼提示词生成音效入口暂时关闭,通用 `/api/creation/audio/*` 对这些目标返回 `410 Gone`。敲木鱼创作只接收上传 / 录音音频资产;未提供时由 `api-server` 写回内置默认木鱼音 `/wooden-fish/default-hit-sound.mp3`
- 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`。敲木鱼创作只接收上传 / 录音音频资产;未提供时由 `api-server` 写回内置默认木鱼音 `/wooden-fish/default-hit-sound.mp3`
- OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`
- 外部 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。入库优先复用 tracking outboxoutbox 不可写或保护阈值拒绝时回退同步写 SpacetimeDB;不得新增前端兜底或在 SpacetimeDB reducer 内做外部 I/O。
@@ -12,6 +12,10 @@
创作表单提交前的泥点余额前置校验只允许用独立弹窗提示失败原因,不得把用户退回创作入口或玩法模板列表,也不得清空当前表单状态。当前适用拼图、抓大鹅和汪汪声浪等会在前端提交前校验泥点的生成入口;余额不足、余额读取失败都应停留在当前工作台,由用户关闭提示后继续编辑或自行补足泥点。
平台入口、生成页、结果页、作品详情、作品架和运行态的跨流程错误统一收口到 `PlatformErrorDialog`。弹窗必须带明确错误来源,例如某个草稿、某次生成、作品详情或某个游玩实例,并提供复制按钮复制“错误来源 + 错误内容”。页面内不再重复渲染裸错误 banner;表单校验、发布确认弹窗里的局部业务错误可以保留在原弹窗内。
生成任务在用户离开生成页后异步完成时,平台壳层必须弹出 `PlatformTaskCompletionDialog`。完成弹窗同样要带来源,例如某个草稿或生成会话,并提供复制按钮复制“来源 + 状态”;如果用户仍停留在生成页并被自动带入结果页或试玩页,生成页 / 结果页本身即为完成反馈,不再额外叠加完成弹窗。
`PlatformEntryFlowShellImpl.tsx` 仍是平台入口编排壳,后续维护时应优先把独立 UI 片段、公开作品映射、草稿生成 notice 和运行态状态 helper 拆到 `src/components/platform-entry/PlatformEntryFlowShellImpl/` 或同目录紧邻 helper 文件。拆分只允许改变文件组织,不改变入口配置事实源、默认导出、props、页面阶段、UI 文案或现有交互;其中拼图首访 onboarding 已拆为 `PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx`
`platformEntryCreationTypes.ts` 只做前端展示派生,分组时必须把后端 `creationTypes` 里的 `categoryId` / `categoryLabel` 当作可缺失字段处理,空值统一回退到 `recent` / `最近创作`,避免旧数据、局部 mock 或异常返回把创作入口初始化直接打崩。
@@ -30,7 +34,7 @@
单图资产编辑统一通过 `CreativeImageInputPanel` 承载上传、AI 重绘、参考图、历史图和删除确认;新玩法页面不得重复手写这些交互。系列素材图集生成统一走“批量规划 -> sheet 生图 -> 后端切图 -> 透明化 -> OSS 持久化 -> 状态回写 -> 局部重生成”流程,玩法只提供 `sheetSpec``slotSpecs`、提示词和字段映射,不把任一玩法专属素材 DTO 当作平台通用模型。
`api-server``generated_asset_sheets` 是当前通用系列素材图集模块`n` 是必选参数,模块负责组装 `n*n` sheet prompt、按 `n*n` 切片、绿幕 / 近白底透明化、导出 PNG 和 OSS 持久化请求。物品名称 prompt 和特殊设定 prompt 是可选输入;调用方可传入类似“每个物品生成五个不同视图”的视角约束,通用模块会把 sheet prompt、物品行 prompt、特殊设定 prompt 编码写入 OSS 元数据。玩法仍负责计费、物品规划、slot 映射、失败回写和把通用切片结果映射回自己的草稿 / profile / runtime 字段。
通用系列素材图集能力的实现真相源在 `platform-image::generated_asset_sheets``n` 是必选参数,模块负责组装 `n*n` sheet prompt、按 `n*n` 切片、绿幕 / 近白底透明化、导出 PNG 和 OSS 持久化请求。`api-server::generated_asset_sheets` 只保留 `AppError` / `AppState` 适配,不再承载图像处理和 OSS 请求构造细节。物品名称 prompt 和特殊设定 prompt 是可选输入;调用方可传入类似“每个物品生成五个不同视图”的视角约束,通用模块会把 sheet prompt、物品行 prompt、特殊设定 prompt 编码写入 OSS 元数据。玩法仍负责计费、物品规划、slot 映射、失败回写和把通用切片结果映射回自己的草稿 / profile / runtime 字段。
当前所有玩法生成页 UI 统一收敛为圆环主视觉:`media/create_bg_video.mp4` 作为生成页固定全屏背景层循环静音播放,主进度圆环居中覆盖在背景之上,围绕陶泥儿视觉展示;页面只保留当前步骤名称和当前步骤进度,不再渲染步骤列表块。视频层需要显式触发播放,不能只依赖 `autoPlay/loop/muted` 属性。共用生成页 `CustomWorldGenerationView` 和汪汪声浪生成页都必须遵循这一口径。
@@ -45,7 +49,7 @@
7. 从草稿 Tab 作品架打开草稿工作区、生成页或结果页时,返回按钮必须回到草稿 Tab 的同一作品架语境;从创作 Tab 新建或直接进入创作链路时才回到创作 Tab。平台壳层需要显式记录本次创作流的返回来源,不能让结果页返回动作固定跳到创作入口。
8. 私有 generated 图片必须通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签读取。
发现 Tab、创作 Tab 与草稿 Tab 的页面根内容区不再套 `platform-page-stage` 外层全局卡片壳,让列表、筛选和玩法卡获得更宽的横向空间;推荐页和我的页仍按各自页面设计保留原有全局卡片口径。移动端“我的”页仍按顶部头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口、次级入口带和法律信息组织,字号必须维持平台普通 UI 档位,不能因为窄屏把卡片标题、功能 label 或法律信息撑成展示级字号;最后一屏内容必须能在底部 dock 上方完整滚动露出,不得被固定底部导航遮挡。
发现 Tab、创作 Tab 与草稿 Tab 的页面根内容区不再套 `platform-page-stage` 外层全局卡片壳,让列表、筛选和玩法卡获得更宽的横向空间;推荐页和我的页仍按各自页面设计保留原有全局卡片口径。移动端“我的”页仍按顶部头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口和法律信息组织,不保留旧的底部“填邀请码”次级入口;每日任务卡必须读取 `/api/profile/tasks` 的当前任务摘要并在领取后同步刷新卡片进度。字号必须维持平台普通 UI 档位,不能因为窄屏把卡片标题、功能 label 或法律信息撑成展示级字号;最后一屏内容必须能在底部 dock 上方完整滚动露出,不得被固定底部导航遮挡。
## RPG / 自定义世界
@@ -73,7 +77,7 @@ RPG 从作品架、广场详情或作品号搜索点击“启动”前,入口
RPG 运行态的战斗终局、继续冒险、继续探索和切场景都属于服务端 runtime 快照真相:`module-runtime-story` 必须在终局战斗 action 后调用 post-battle finalization,持久写入 `story_continue_adventure``deferredOptions``deferredRuntimeState.storyEngineMemory.currentSceneActState` 和清理后的战斗状态;`idle_travel_next_scene` / `camp_travel_home_scene` 必须由后端写入新的 `currentScenePreset``currentSceneActState``currentEncounter``runtimeStats.scenesTraveled`。前端只播放退场、进场和继续按钮表现,不能用默认 `观察/试探/调息` fallback 或本地动画假装推进剧情。旧 bootstrap 快照可能只有 `connectedSceneIds` / `forwardSceneId` 而没有 `connections`,后端生成战后旅行选项时必须兼容这些字段。
RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列表为真相,恢复动作继续走对应恢复接口,但移动端“我的”页已经不再提供独立的 `次级入口 > 存档` 和设置入口存档按钮;“玩过”弹窗可以继续合并展示可继续存档,个人中心只保留设置、扫码常用功能和条件性次级入口。移动端“我的”页的五项常用功能宫格只放泥点充值、邀请好友、兑换码、玩家社区、反馈与建议,避免把存档挤入主宫格破坏参考图布局。前端只展示 `/api/profile/save-archives` 返回的列表并在用户选择后调用对应恢复接口,不能本地拼装或筛选正式存档真相。
RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列表为真相,恢复动作继续走对应恢复接口,但移动端“我的”页已经不再提供独立的 `次级入口 > 存档` 和设置入口存档按钮;“玩过”弹窗可以继续合并展示可继续存档,个人中心只保留设置、扫码和五项常用功能。移动端“我的”页的五项常用功能宫格只放泥点充值、邀请好友、兑换码、玩家社区、反馈与建议,避免把存档或填邀请码挤入主宫格破坏参考图布局。前端只展示 `/api/profile/save-archives` 返回的列表并在用户选择后调用对应恢复接口,不能本地拼装或筛选正式存档真相。
## 拼图
@@ -93,11 +93,12 @@ server-rs + Axum + SpacetimeDB
7. 主站入口已锁定移动端页面级缩放;单个游戏页面不要再重复实现整页缩放锁定。
8. 图像输入通用 UI 统一走 `src/components/common/CreativeImageInputPanel.tsx`。外层页面持有业务状态,组件只承担上传卡、预览、参考图缩略图、AI 重绘开关、错误展示和提交按钮。
9. 发现页 `分类` 子频道的筛选必须打开独立 dialog / drawer / modal,至少支持玩法类型过滤与排序切换;筛选结果为空时显示空状态,不把筛选内容展开在当前列表下方。
10. 移动端“我的”页顶部品牌行承载扫码和设置入口,正文按参考图顺序组织为头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口、可选次级入口和法律信息;`media/profile/` 中的陶泥素材作为该页图形资产。常用功能宫格固定承载泥点充值、邀请好友、兑换码、玩家社区、反馈与建议;页面不再提供独立存档按钮入口,填邀请码仅在新用户可填写窗口内展示为次级入口
11. “我的”页泥点、游戏时长、已玩游戏数量三张统计卡只展示各自标签和值,三个统计 icon 使用小尺寸普通 UI 档位,内容不换行,不在统计区底部展示“更新于”时间;移动端昵称、会员卡、每日任务、常用功能和法律信息也应保持 `10px``14px` 的普通 UI 字号区间,避免展示级字号挤压内容
12. 移动端“我的”页需要兼容窄屏:头像 / 昵称 / 陶泥号、三张统计卡、每日任务、五项常用功能、可选次级入口和法律信息都必须能在底部固定 TabBar 上方完整滚动露出,不得与底部 dock、刘海 safe-area 或相邻 UI 元素遮挡重叠
13. RPG 等运行态的战斗飘字、血量变化和即时反馈必须在暗色、噪声高的场景背景上保持可读:使用高亮文字、深色描边、强阴影或小面积半透明底,不只依赖红/绿文字本身表达伤害或治疗
14. 平台亮色 UI 配色以陶泥儿主视觉为准:暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;新增界面优先复用 `src/index.css``--platform-*` 主题变量和 `apps/admin-web/src/styles/admin.css` 的同系色值,不再引入粉红、蓝绿等独立主色方案
10. 移动端“我的”页顶部品牌行承载扫码和设置入口,正文按参考图顺序组织为头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口和法律信息;`media/profile/` 中的陶泥素材作为该页图形资产。常用功能宫格固定承载泥点充值、邀请好友、兑换码、玩家社区、反馈与建议;页面不再提供独立存档按钮入口,也不在底部保留旧的填邀请码次级入口。填邀请码只由邀请链接 query 或其它明确引导打开独立弹窗,不作为“我的”页常驻按钮
11. “我的”页每日任务卡必须展示后端 `/api/profile/tasks` 返回的当前任务摘要,包括奖励泥点数、进度和领取 / 去完成 / 已完成状态;任务领取成功后,卡片摘要必须跟随返回的任务中心数据同步刷新,不能继续硬编码 `0 / 1` 或只更新弹窗内任务列表
12. “我的”页泥点、游戏时长、已玩游戏数量三张统计卡只展示各自标签和值,三个统计 icon 使用小尺寸普通 UI 档位,内容不换行,不在统计区底部展示“更新于”时间;移动端昵称、会员卡、每日任务、常用功能和法律信息也应保持 `10px``14px` 的普通 UI 字号区间,避免展示级字号挤压内容
13. 移动端“我的”页需要兼容窄屏:头像 / 昵称 / 陶泥号、三张统计卡、每日任务、五项常用功能和法律信息都必须能在底部固定 TabBar 上方完整滚动露出,不得与底部 dock、刘海 safe-area 或相邻 UI 元素遮挡重叠
14. RPG 等运行态的战斗飘字、血量变化和即时反馈必须在暗色、噪声高的场景背景上保持可读:使用高亮文字、深色描边、强阴影或小面积半透明底,不只依赖红/绿文字本身表达伤害或治疗
15. 平台亮色 UI 配色以陶泥儿主视觉为准:暖白 / 米杏底、陶土橙主按钮、深棕正文与浅杏边框;新增界面优先复用 `src/index.css``--platform-*` 主题变量和 `apps/admin-web/src/styles/admin.css` 的同系色值,不再引入粉红、蓝绿等独立主色方案。
## 文案与编码
+27
View File
@@ -107,7 +107,9 @@ dependencies = [
"module-visual-novel",
"opentelemetry",
"platform-agent",
"platform-audio",
"platform-auth",
"platform-hyper3d",
"platform-image",
"platform-llm",
"platform-oss",
@@ -2301,6 +2303,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "platform-audio"
version = "0.1.0"
dependencies = [
"platform-oss",
"reqwest 0.12.28",
"serde_json",
"tokio",
"tracing",
"urlencoding",
]
[[package]]
name = "platform-auth"
version = "0.1.0"
@@ -2322,11 +2336,24 @@ dependencies = [
"urlencoding",
]
[[package]]
name = "platform-hyper3d"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"reqwest 0.12.28",
"serde_json",
"shared-contracts",
"tokio",
]
[[package]]
name = "platform-image"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"image",
"platform-oss",
"reqwest 0.12.28",
"serde_json",
"tokio",
+4
View File
@@ -32,6 +32,8 @@ members = [
"crates/module-visual-novel",
"crates/platform-oss",
"crates/platform-auth",
"crates/platform-audio",
"crates/platform-hyper3d",
"crates/platform-image",
"crates/platform-llm",
"crates/platform-speech",
@@ -75,6 +77,8 @@ module-story = { path = "crates/module-story", default-features = false }
module-visual-novel = { path = "crates/module-visual-novel", default-features = false }
platform-agent = { path = "crates/platform-agent", default-features = false }
platform-auth = { path = "crates/platform-auth", default-features = false }
platform-audio = { path = "crates/platform-audio", default-features = false }
platform-hyper3d = { path = "crates/platform-hyper3d", default-features = false }
platform-image = { path = "crates/platform-image", default-features = false }
platform-llm = { path = "crates/platform-llm", default-features = false }
platform-oss = { path = "crates/platform-oss", default-features = false }
+2
View File
@@ -33,7 +33,9 @@ module-square-hole = { workspace = true }
module-story = { workspace = true }
module-visual-novel = { workspace = true }
platform-agent = { workspace = true }
platform-audio = { workspace = true }
platform-auth = { workspace = true }
platform-hyper3d = { workspace = true }
platform-image = { workspace = true }
platform-llm = { workspace = true }
platform-oss = { workspace = true }
+2 -2
View File
@@ -9,8 +9,8 @@ use axum::{
response::Response,
};
use platform_auth::{
AccessTokenClaims, AuthProvider, BindingStatus, RuntimeGuestTokenClaims,
RuntimeGuestTokenClaimsInput, RUNTIME_GUEST_SCOPE_PUBLIC_PLAY, read_refresh_session_token,
AccessTokenClaims, AuthProvider, BindingStatus, RUNTIME_GUEST_SCOPE_PUBLIC_PLAY,
RuntimeGuestTokenClaims, RuntimeGuestTokenClaimsInput, read_refresh_session_token,
sign_runtime_guest_token, verify_access_token, verify_runtime_guest_token,
};
use serde_json::{Value, json};
@@ -1792,9 +1792,8 @@ mod tests {
"publishedAtMicros": 1_713_686_401_234_000i64
});
let work =
map_work_summary_record(gallery_row, &request_context, "画廊作者".to_string())
.expect("gallery summary should use provided author display name");
let work = map_work_summary_record(gallery_row, &request_context, "画廊作者".to_string())
.expect("gallery summary should use provided author display name");
assert_eq!(work.author_display_name, "画廊作者");
assert_eq!(
@@ -148,7 +148,8 @@ pub(crate) fn default_creation_entry_config_response() -> CreationEntryConfigRes
description: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_DESCRIPTION.to_string(),
cover_image_src: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_COVER_IMAGE_SRC
.to_string(),
prize_pool_mud_points: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_PRIZE_POOL_MUD_POINTS,
prize_pool_mud_points:
module_runtime::DEFAULT_CREATION_ENTRY_EVENT_PRIZE_POOL_MUD_POINTS,
starts_at_text: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_STARTS_AT_TEXT.to_string(),
ends_at_text: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT.to_string(),
},
@@ -269,7 +270,10 @@ mod tests {
assert_eq!(baby_object_match.badge, "\u{53ef}\u{521b}\u{5efa}");
assert_eq!(baby_object_match.sort_order, 90);
assert_eq!(baby_object_match.category_id, "character");
assert_eq!(baby_object_match.category_label, "\u{89d2}\u{8272}\u{521b}\u{4f5c}");
assert_eq!(
baby_object_match.category_label,
"\u{89d2}\u{8272}\u{521b}\u{4f5c}"
);
assert_eq!(baby_object_match.category_sort_order, 40);
}
}
@@ -1,6 +1,6 @@
use axum::http::StatusCode;
use platform_image::PlatformImageFailureAudit;
use module_runtime::RuntimeTrackingScopeKind;
use platform_image::PlatformImageFailureAudit;
use serde_json::{Value, json};
use time::OffsetDateTime;
use uuid::Uuid;
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,20 @@
// 中文注释:C0 先落公共骨架,真实调用方迁移到 C1 后再移除未使用豁免。
#![allow(dead_code, unused_imports)]
pub mod adapter {
pub use platform_image::generated_assets::adapter::*;
}
pub mod adapter;
pub mod helpers;
pub mod helpers {
pub use platform_image::generated_assets::helpers::*;
}
pub(crate) use adapter::{GeneratedImageAssetAdapter, GeneratedImageAssetAdapterBoundary};
pub(crate) use helpers::{
GeneratedImageAssetDataUrl, GeneratedImageAssetImageFormat, GeneratedImageAssetMetadataInput,
GeneratedImageAssetStoragePaths, build_generated_image_asset_metadata,
build_generated_image_asset_storage_paths, decode_generated_image_asset_data_url,
merge_generated_image_asset_metadata, normalize_generated_image_asset_mime,
pub(crate) use adapter::{
GeneratedImageAssetAdapter, GeneratedImageAssetAdapterBoundary,
GeneratedImageAssetAdapterMetadata, GeneratedImageAssetPersistInput,
GeneratedImageAssetPreparedPut,
};
pub(crate) use helpers::{
GeneratedImageAssetDataUrl, GeneratedImageAssetHelperError, GeneratedImageAssetImageFormat,
GeneratedImageAssetMetadataInput, GeneratedImageAssetStoragePaths,
build_generated_image_asset_metadata, build_generated_image_asset_storage_paths,
decode_generated_image_asset_data_url, merge_generated_image_asset_metadata,
normalize_generated_image_asset_mime,
};
File diff suppressed because it is too large Load Diff
+85 -60
View File
@@ -20,12 +20,14 @@ use shared_contracts::jump_hop::{
};
use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros};
use spacetime_client::SpacetimeClientError;
use std::{collections::BTreeMap, time::{SystemTime, UNIX_EPOCH}};
use std::{
collections::BTreeMap,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{
api_response::json_success_body,
auth::{AuthenticatedAccessToken, RuntimePrincipal},
http_error::AppError,
generated_asset_sheets::{
GeneratedAssetSheetPromptInput, build_generated_asset_sheet_prompt,
slice_generated_asset_sheet,
@@ -35,16 +37,18 @@ use crate::{
adapter::{GeneratedImageAssetAdapterMetadata, GeneratedImageAssetPersistInput},
normalize_generated_image_asset_mime,
},
http_error::AppError,
openai_image_generation::{
build_openai_image_http_client, create_openai_image_generation,
require_openai_image_settings,
},
request_context::RequestContext,
state::AppState,
work_play_tracking::{record_work_play_start_after_success, WorkPlayTrackingDraft},
work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success},
};
const JUMP_HOP_TILE_ITEM_NAMES: [&str; 6] = ["start", "normal", "target", "finish", "bonus", "accent"];
const JUMP_HOP_TILE_ITEM_NAMES: [&str; 6] =
["start", "normal", "target", "finish", "bonus", "accent"];
const JUMP_HOP_PROVIDER: &str = "jump-hop";
const JUMP_HOP_CREATION_PROVIDER: &str = "jump-hop-creation";
@@ -384,7 +388,10 @@ async fn maybe_generate_jump_hop_assets(
}
if payload.character_asset.is_some()
&& payload.tile_atlas_asset.is_some()
&& payload.tile_assets.as_ref().is_some_and(|assets| !assets.is_empty())
&& payload
.tile_assets
.as_ref()
.is_some_and(|assets| !assets.is_empty())
{
return Ok(());
}
@@ -397,19 +404,18 @@ async fn maybe_generate_jump_hop_assets(
.unwrap_or_else(|| build_prefixed_uuid_id("jump-hop-profile-"));
payload.profile_id = Some(profile_id.clone());
let settings = require_openai_image_settings(state)
.map_err(|error| jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error))?;
let http_client = build_openai_image_http_client(&settings)
.map_err(|error| jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error))?;
let settings = require_openai_image_settings(state).map_err(|error| {
jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error)
})?;
let http_client = build_openai_image_http_client(&settings).map_err(|error| {
jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error)
})?;
let character_prompt = payload
.character_prompt
.as_deref()
.unwrap_or("俯视角可爱主角,透明背景");
let tile_prompt = payload
.tile_prompt
.as_deref()
.unwrap_or("等距立体地块图集");
let tile_prompt = payload.tile_prompt.as_deref().unwrap_or("等距立体地块图集");
let character_generated = create_openai_image_generation(
&http_client,
@@ -423,16 +429,20 @@ async fn maybe_generate_jump_hop_assets(
)
.await
.map_err(|error| jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error))?;
let character_image = character_generated.images.into_iter().next().ok_or_else(|| {
jump_hop_error_response(
request_context,
JUMP_HOP_CREATION_PROVIDER,
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": "跳一跳角色资产生成成功但未返回图片。",
})),
)
})?;
let character_image = character_generated
.images
.into_iter()
.next()
.ok_or_else(|| {
jump_hop_error_response(
request_context,
JUMP_HOP_CREATION_PROVIDER,
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": "跳一跳角色资产生成成功但未返回图片。",
})),
)
})?;
let character_asset = persist_jump_hop_generated_image_asset(
state,
owner_user_id,
@@ -449,7 +459,14 @@ async fn maybe_generate_jump_hop_assets(
let sheet_prompt = build_generated_asset_sheet_prompt(&GeneratedAssetSheetPromptInput {
subject_text: tile_prompt,
item_names: &vec!["start".to_string(), "normal".to_string(), "target".to_string(), "finish".to_string(), "bonus".to_string(), "accent".to_string()],
item_names: &vec![
"start".to_string(),
"normal".to_string(),
"target".to_string(),
"finish".to_string(),
"bonus".to_string(),
"accent".to_string(),
],
grid_size: 3,
item_name_prompt_template: Some("第{row_index}行:{item_name} 的 {view_count} 个不同视图"),
special_prompt: Some("每个格子对应一个 tile 类型,供跳一跳地块裁切使用。"),
@@ -479,7 +496,14 @@ async fn maybe_generate_jump_hop_assets(
})?;
let tile_slices = slice_generated_asset_sheet(
&tile_image,
&vec!["start".to_string(), "normal".to_string(), "target".to_string(), "finish".to_string(), "bonus".to_string(), "accent".to_string()],
&vec![
"start".to_string(),
"normal".to_string(),
"target".to_string(),
"finish".to_string(),
"bonus".to_string(),
"accent".to_string(),
],
3,
)
.map_err(|error| jump_hop_error_response(request_context, JUMP_HOP_CREATION_PROVIDER, error))?;
@@ -521,10 +545,11 @@ async fn maybe_generate_jump_hop_assets(
payload.character_asset = Some(character_asset);
payload.tile_atlas_asset = Some(tile_atlas_asset);
payload.tile_assets = Some(tile_assets);
payload.cover_composite = payload
.cover_composite
.clone()
.or_else(|| Some(format!("/generated-jump-hop-assets/{profile_id}/cover-composite.png")));
payload.cover_composite = payload.cover_composite.clone().or_else(|| {
Some(format!(
"/generated-jump-hop-assets/{profile_id}/cover-composite.png"
))
});
Ok(())
}
@@ -541,36 +566,37 @@ async fn persist_jump_hop_generated_image_asset(
request_context: &RequestContext,
) -> Result<JumpHopCharacterAsset, Response> {
let image_format = normalize_generated_image_asset_mime(image.mime_type.as_str());
let prepared = GeneratedImageAssetAdapter::prepare_put_object(GeneratedImageAssetPersistInput {
prefix,
path_segments: vec![profile_id.to_string(), slot.to_string()],
file_stem: "image".to_string(),
image: GeneratedImageAssetDataUrl {
format: image_format,
bytes: image.bytes,
},
access: OssObjectAccess::Private,
metadata: GeneratedImageAssetAdapterMetadata {
asset_kind: Some(format!("jump-hop-{slot}")),
owner_user_id: Some(owner_user_id.to_string()),
entity_kind: Some("jump_hop_work".to_string()),
entity_id: Some(profile_id.to_string()),
slot: Some(slot.to_string()),
provider: Some("vector-engine".to_string()),
task_id: None,
},
extra_metadata: BTreeMap::new(),
})
.map_err(|error| {
jump_hop_error_response(
request_context,
JUMP_HOP_CREATION_PROVIDER,
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "generated-image-assets",
"message": format!("准备跳一跳图片资产上传请求失败:{error:?}"),
})),
)
})?;
let prepared =
GeneratedImageAssetAdapter::prepare_put_object(GeneratedImageAssetPersistInput {
prefix,
path_segments: vec![profile_id.to_string(), slot.to_string()],
file_stem: "image".to_string(),
image: GeneratedImageAssetDataUrl {
format: image_format,
bytes: image.bytes,
},
access: OssObjectAccess::Private,
metadata: GeneratedImageAssetAdapterMetadata {
asset_kind: Some(format!("jump-hop-{slot}")),
owner_user_id: Some(owner_user_id.to_string()),
entity_kind: Some("jump_hop_work".to_string()),
entity_id: Some(profile_id.to_string()),
slot: Some(slot.to_string()),
provider: Some("vector-engine".to_string()),
task_id: None,
},
extra_metadata: BTreeMap::new(),
})
.map_err(|error| {
jump_hop_error_response(
request_context,
JUMP_HOP_CREATION_PROVIDER,
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "generated-image-assets",
"message": format!("准备跳一跳图片资产上传请求失败:{error:?}"),
})),
)
})?;
let persisted_mime_type = prepared.format.mime_type.clone();
let oss_client = state.oss_client().ok_or_else(|| {
jump_hop_error_response(
@@ -709,7 +735,6 @@ fn build_jump_hop_work_play_tracking_draft(
WorkPlayTrackingDraft::runtime_principal("jump-hop", work_id, principal, source_route)
}
fn build_jump_hop_draft(payload: &JumpHopWorkspaceCreateRequest) -> JumpHopDraftResponse {
JumpHopDraftResponse {
template_id: JUMP_HOP_TEMPLATE_ID.to_string(),
@@ -65,7 +65,10 @@ pub fn router(state: AppState) -> Router<AppState> {
attach_refresh_session_token,
)),
)
.route("/api/auth/runtime-guest-token", post(issue_runtime_guest_token))
.route(
"/api/auth/runtime-guest-token",
post(issue_runtime_guest_token),
)
.route("/api/auth/phone/send-code", post(send_phone_code))
.route("/api/auth/phone/login", post(phone_login))
.route("/api/auth/wechat/start", get(start_wechat_login))
@@ -6,10 +6,10 @@ use axum::{
use crate::{
auth::require_bearer_auth,
bark_battle::{
create_bark_battle_draft, finish_bark_battle_run, get_bark_battle_run,
generate_bark_battle_image_asset, get_bark_battle_runtime_config,
list_bark_battle_gallery, list_bark_battle_works, publish_bark_battle_work,
start_bark_battle_run, update_bark_battle_draft_config,
create_bark_battle_draft, finish_bark_battle_run, generate_bark_battle_image_asset,
get_bark_battle_run, get_bark_battle_runtime_config, list_bark_battle_gallery,
list_bark_battle_works, publish_bark_battle_work, start_bark_battle_run,
update_bark_battle_draft_config,
},
state::AppState,
};
+12 -11
View File
@@ -55,17 +55,18 @@ use spacetime_client::{
PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput,
PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord,
PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord,
PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord,
PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput, PuzzleGalleryCardRecord,
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput,
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput,
PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord,
PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, PuzzleRunDragRecordInput,
PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord,
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleSelectCoverImageRecordInput,
PuzzleUiBackgroundSaveRecordInput, PuzzleWorkLikeReportRecordInput,
PuzzleWorkPointIncentiveClaimRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput,
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftCompileFailureRecordInput,
PuzzleDraftLevelRecord, PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput,
PuzzleGalleryCardRecord, PuzzleGeneratedImageCandidateRecord,
PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord,
PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord,
PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord,
PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunPauseRecordInput,
PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput,
PuzzleRunSwapRecordInput, PuzzleSelectCoverImageRecordInput, PuzzleUiBackgroundSaveRecordInput,
PuzzleWorkLikeReportRecordInput, PuzzleWorkPointIncentiveClaimRecordInput,
PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput,
SpacetimeClientError,
};
use std::convert::Infallible;
@@ -606,6 +606,36 @@ pub async fn execute_puzzle_agent_action(
),
"拼图 Agent action 开始执行"
);
let mark_puzzle_compile_failure = |error: &AppError, compile_session_id: &str| {
let state = state.clone();
let owner_user_id = owner_user_id.clone();
let error_message = error.body_text();
let session_id = compile_session_id.to_string();
let log_session_id = session_id.clone();
let log_owner_user_id = owner_user_id.clone();
async move {
let result = state
.spacetime_client()
.mark_puzzle_draft_generation_failed(PuzzleDraftCompileFailureRecordInput {
session_id,
owner_user_id,
error_message,
failed_at_micros: now,
})
.await;
if let Err(error) = result {
tracing::warn!(
provider = PUZZLE_AGENT_API_BASE_PROVIDER,
session_id = %log_session_id,
owner_user_id = %log_owner_user_id,
message = %error,
"拼图草稿失败态回写失败,继续返回原始错误"
);
}
}
};
let (operation_type, phase_label, phase_detail, session) = match action.as_str() {
"compile_puzzle_draft" => {
let ai_redraw = payload.ai_redraw.unwrap_or(true);
@@ -666,10 +696,18 @@ pub async fn execute_puzzle_agent_action(
now,
)
.await
}
.map_err(|error| {
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
});
};
let session = match session {
Ok(session) => Ok(session),
Err(error) => {
mark_puzzle_compile_failure(&error, &compile_session_id).await;
Err(puzzle_error_response(
&request_context,
PUZZLE_AGENT_API_BASE_PROVIDER,
error,
))
}
};
(
"compile_puzzle_draft",
"首关拼图草稿",
@@ -490,13 +490,15 @@ pub(crate) async fn resolve_puzzle_reference_image(
if let Some(parsed) = parse_puzzle_image_data_url(trimmed) {
let bytes_len = parsed.bytes.len();
if bytes_len > PUZZLE_REFERENCE_IMAGE_MAX_BYTES {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "puzzle",
"field": "referenceImageSrc",
"message": build_puzzle_reference_image_too_large_message(bytes_len),
"maxBytes": PUZZLE_REFERENCE_IMAGE_MAX_BYTES,
"actualBytes": bytes_len,
})));
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "puzzle",
"field": "referenceImageSrc",
"message": build_puzzle_reference_image_too_large_message(bytes_len),
"maxBytes": PUZZLE_REFERENCE_IMAGE_MAX_BYTES,
"actualBytes": bytes_len,
})),
);
}
return Ok(PuzzleResolvedReferenceImage {
mime_type: parsed.mime_type,
@@ -648,16 +650,18 @@ pub(crate) fn validate_puzzle_reference_asset_object(
if asset_object.content_length == 0
|| asset_object.content_length > PUZZLE_REFERENCE_IMAGE_MAX_BYTES as u64
{
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "asset-object",
"field": "referenceImageAssetObjectId",
"assetObjectId": asset_object.asset_object_id,
"message": build_puzzle_reference_image_too_large_message(
asset_object.content_length as usize,
),
"maxBytes": PUZZLE_REFERENCE_IMAGE_MAX_BYTES,
"actualBytes": asset_object.content_length,
})));
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "asset-object",
"field": "referenceImageAssetObjectId",
"assetObjectId": asset_object.asset_object_id,
"message": build_puzzle_reference_image_too_large_message(
asset_object.content_length as usize,
),
"maxBytes": PUZZLE_REFERENCE_IMAGE_MAX_BYTES,
"actualBytes": asset_object.content_length,
})),
);
}
if let Some(expected_owner_user_id) = owner_user_id
.map(str::trim)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
pub(super) fn current_utc_micros() -> i64 {
shared_kernel::offset_datetime_to_unix_micros(time::OffsetDateTime::now_utc())
}
pub(super) fn current_utc_iso_text() -> String {
shared_kernel::format_rfc3339(time::OffsetDateTime::now_utc())
.unwrap_or_else(|_| shared_kernel::format_timestamp_micros(current_utc_micros()))
}
@@ -0,0 +1,120 @@
use axum::{Json, extract::rejection::JsonRejection, http::StatusCode, response::Response};
use platform_audio::{AudioError, AudioStatusHint};
use serde_json::json;
use crate::{http_error::AppError, request_context::RequestContext};
use super::types::VECTOR_ENGINE_PROVIDER;
pub(super) fn normalize_limited_text(
value: &str,
field: &'static str,
max_chars: usize,
) -> Result<String, AppError> {
let normalized = value.trim().to_string();
if normalized.is_empty() {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": VECTOR_ENGINE_PROVIDER,
"field": field,
"message": format!("{field} 不能为空"),
})),
);
}
if normalized.chars().count() > max_chars {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": VECTOR_ENGINE_PROVIDER,
"field": field,
"message": format!("{field} 超过 {} 字符", max_chars),
})),
);
}
Ok(normalized)
}
pub(super) fn normalize_optional_text(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
pub(super) fn map_asset_field_error(error: module_assets::AssetObjectFieldError) -> AppError {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "asset-object",
"message": error.to_string(),
}))
}
pub(super) fn map_spacetime_error(error: spacetime_client::SpacetimeClientError) -> AppError {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "spacetimedb",
"message": error.to_string(),
}))
}
pub(super) fn map_platform_audio_error(error: AudioError) -> AppError {
let status = match error.status_hint() {
AudioStatusHint::BadRequest => StatusCode::BAD_REQUEST,
AudioStatusHint::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
AudioStatusHint::BadGateway => StatusCode::BAD_GATEWAY,
AudioStatusHint::GatewayTimeout => StatusCode::GATEWAY_TIMEOUT,
};
let mut details = json!({
"provider": error.provider(),
"message": error.message(),
});
match &error {
AudioError::InvalidConfig { .. } | AudioError::InvalidRequest { .. } => {}
AudioError::Request {
endpoint,
timeout,
connect,
request,
body,
status_code,
source,
..
} => {
details["endpoint"] = json!(endpoint);
details["timeout"] = json!(timeout);
details["connect"] = json!(connect);
details["request"] = json!(request);
details["body"] = json!(body);
details["status"] = json!(status_code);
details["source"] = json!(source);
}
AudioError::Upstream {
upstream_status,
raw_excerpt,
..
} => {
details["upstreamStatus"] = json!(upstream_status);
details["rawExcerpt"] = json!(raw_excerpt);
}
AudioError::ResponseParse { raw_excerpt, .. } => {
details["rawExcerpt"] = json!(raw_excerpt);
}
AudioError::MissingAudio { .. } => {}
}
AppError::from_status(status).with_details(details)
}
pub(super) fn vector_engine_bad_gateway(message: impl Into<String>) -> AppError {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": VECTOR_ENGINE_PROVIDER,
"message": message.into(),
}))
}
pub(super) fn parse_json_payload<T>(
request_context: &RequestContext,
payload: Result<Json<T>, JsonRejection>,
) -> Result<Json<T>, Response> {
payload.map_err(|rejection| {
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message(format!("请求体 JSON 不合法:{rejection}"))
.into_response_with_context(Some(request_context))
})
}

Some files were not shown because too many files have changed in this diff Show More