1
This commit is contained in:
@@ -16,6 +16,22 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-09 GPT-image-2 图片生成统一迁移到 VectorEngine
|
||||
|
||||
- 背景:仓库内 RPG、拼图、方洞和本地模板脚本的 GPT-image-2 生图此前依赖 APIMart 图片网关;团队要求参考 VectorEngine Apifox `api-448710071`,后续不再使用 APIMart 执行 GPT-image-2 图片生成。
|
||||
- 决策:所有 GPT-image-2 生图请求统一走 VectorEngine `POST /v1/images/generations`,基础配置读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` / `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`,上游模型使用 `gpt-image-2-all`,请求体不再携带 `official_fallback`,参考图字段改为 `image`。APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。
|
||||
- 影响范围:`api-server` 共享图片 helper、拼图图片生成、角色主图、RPG 场景图、开局 CG 故事板、方洞视觉资产、生产环境示例、gpt-image-2 本地 skill 和相关技术文档。
|
||||
- 验证方式:执行 `npm run check:encoding`、`cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server character_visual --manifest-path server-rs/Cargo.toml`,并用 `npm run api-server` + `/healthz` 做后端 smoke。
|
||||
- 关联文档:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。
|
||||
|
||||
## 2026-05-08 Hyper3D Rodin Gen-2 只通过后端安全代理接入
|
||||
|
||||
- 背景:需要接入 Hyper3D Rodin Gen-2 的文生 3D 模型与图生 3D 模型,但供应商 API Key 不能进入前端、文档或 Git;本次只是外部副作用代理,不需要新增平台真相表。
|
||||
- 决策:Hyper3D 统一走 `api-server` 的 `/api/assets/hyper3d/*` 鉴权路由,配置只读取 `HYPER3D_BASE_URL` / `HYPER3D_API_KEY` / `HYPER3D_MODEL_REQUEST_TIMEOUT_MS` 及兼容 `RODIN_*` 变量;生成提交、状态查询和下载列表都由后端代理。首版不写 SpacetimeDB、不确认 `asset_object`,下载链接后续由调用方决定是否进入 OSS 资产链。
|
||||
- 影响范围:`api-server` 外部服务配置、Hyper3D route、`shared-contracts` / TS contract、前端 service、生产环境示例和外部服务环境变量文档。
|
||||
- 验证方式:执行 `cargo test -p api-server hyper3d --manifest-path server-rs/Cargo.toml`、`cargo test -p shared-contracts hyper3d --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run typecheck` 和编码检查;真实 API smoke 只在本地私密环境配置 key 后手动执行。
|
||||
- 关联文档:`docs/technical/HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md`、`docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md`。
|
||||
|
||||
## 2026-05-08 APIMart 接口统一携带 `official_fallback`
|
||||
|
||||
- 背景:APIMart 的图片生成和 Responses 接口在仓库内分散于 `api-server`、`platform-llm` 和本地 skill 脚本,若只修单点,容易出现不同入口的上游请求体不一致。
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
- 验证:提交前检查 `git diff -- .hermes`,确认没有密钥、会话记录或个人路径敏感信息。
|
||||
- 关联:`.hermes/README.md`。
|
||||
|
||||
## GPT-image-2 不再读 APIMart 图片配置
|
||||
|
||||
- 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`。
|
||||
- 原因:2026-05-09 后 GPT-image-2 图片生成已切到 VectorEngine `gpt-image-2-all`,APIMart 只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态链路。
|
||||
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai`、`VECTOR_ENGINE_API_KEY`、`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认路径为 `/v1/images/generations`、模型为 `gpt-image-2-all`、参考图字段为 `image`。
|
||||
- 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。
|
||||
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md`、`server-rs/crates/api-server/src/openai_image_generation.rs`。
|
||||
|
||||
## 旧后端路线文档造成判断漂移
|
||||
|
||||
- 现象:开发时参考到 Express、Node、PostgreSQL 或 Go 方向旧文档,导致接口、数据真相或部署路径与当前主线不一致。
|
||||
@@ -67,6 +75,14 @@
|
||||
- 验证:本地 SpacetimeDB 可正常启动并 publish / 访问。
|
||||
- 关联:`docs/technical/SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md`。
|
||||
|
||||
## 本地 SpacetimeDB publish 403 优先查 CLI root 身份
|
||||
|
||||
- 现象:`spacetime publish` 在 `Pre-publish check` 阶段返回 `403 Forbidden`,提示当前 identity 无权对目标 database identity 执行 `update database`。
|
||||
- 原因:裸 `spacetime` 命令使用了全局 CLI 登录态,但本地开发库权限绑定在 `server-rs/.spacetimedb/local` root-dir 的另一个身份上。
|
||||
- 处理:对比 `spacetime login show` 和 `spacetime --root-dir server-rs/.spacetimedb/local login show`;本地开发发布必须使用 `npm run dev:rust`,或显式追加 `--root-dir=server-rs/.spacetimedb/local`。
|
||||
- 验证:`spacetime --root-dir server-rs/.spacetimedb/local list --server http://127.0.0.1:3101` 能看到目标库;重新发布不再使用无权限的全局 identity。
|
||||
- 关联:`scripts/dev-rust-stack.sh`、`docs/technical/SPACETIMEDB_START_SH_PUBLISH_403_IDENTITY_FIX_2026-04-26.md`。
|
||||
|
||||
## Vite SPA fallback 吞掉 API 请求
|
||||
|
||||
- 现象:本地请求 `/api/profile/*` 等接口时返回 HTML,被前端当 JSON 解析报错。
|
||||
@@ -114,6 +130,7 @@
|
||||
- 本地启动脚本没有让 `.env.local` 覆盖 `.env`,`SMS_AUTH_ENABLED=true` 不生效,后端只返回 `["password"]`。
|
||||
- Rust API 直连已返回 `["phone","password"]`,但 Vite 代理目标指向未监听端口,导致 3000 域名下的 `login-options` 返回 `500`,`AuthGate` 降级成 `["password"]`。
|
||||
- 3000 端口被旧 `dev:web` 占用后,新的完整栈 Vite 自动漂移到 3001/3002;浏览器仍打开旧 3000 页面,旧页面继续代理到已经下线的端口。
|
||||
- 单独 `npm run dev:web` 启动瞬间另一个临时 API 端口可用,脚本若自动切过去,之后临时 API 停掉也会让 3000 继续代理到空端口。
|
||||
- 处理:优先用 `npm run api-server`、`npm run dev:rust` 或 `npm run dev` 启动,这些入口应保持 shell 环境变量最高优先级,并允许 `.env.local` 覆盖 `.env`;完整栈启动时还要确保脚本计算出的 `RUST_SERVER_TARGET` 不被 `.env.local` 里的旧值覆盖。排查时先请求 3000 域名下的 `/api/auth/login-options`,再直连 Rust API 目标,并核对 `.env.local` 的 `SMS_AUTH_ENABLED` 与代理端口;若 3001/3002 才返回正确结果,说明当前 3000 是旧前端进程,应清理旧进程后重启。
|
||||
- 验证:`http://127.0.0.1:3000/api/auth/login-options` 返回至少 `{"availableLoginMethods":["phone","password"]}` 后,登录弹窗会恢复短信登录页签和“获取验证码”按钮。
|
||||
- 关联:`scripts/api-server-dev.mjs`、`scripts/api-server-maincloud.mjs`、`scripts/dev-rust-stack.sh`、`scripts/dev-web-rust.mjs`、`docs/technical/AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md`。
|
||||
@@ -139,8 +156,10 @@
|
||||
- 现象:前端登录成功后进入推荐页,推荐页自动加载出一个作品,随后瞬间回到未登录;停留在其他页面或推荐页没加载出作品时不复现。
|
||||
- 原因:推荐页 embedded 运行态会自动发起受保护写请求。若这些卡片级后台请求遇到 `401` 或 refresh 失败,默认请求层曾清空 access token 并广播全局 auth 事件,导致 `AuthGate` 重新 hydrate 成未登录态。
|
||||
- 处理:推荐页自动运行态请求传 `skipRefresh: true`、`notifyAuthStateChange: false`、`clearAuthOnUnauthorized: false`,并等 `canReadProtectedData` 为 true 后再启动;用户主动点击的受保护动作仍保留默认鉴权失败处理。
|
||||
- 验证:`npm run test -- src/services/apiClient.test.ts` 和 `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"`。
|
||||
- 关联:`src/services/apiClient.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/technical/RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md`。
|
||||
- 追加处理:generated 私有图片换签 `/api/assets/read-url` 也属于展示层后台请求;推荐页拼图运行态挂载后会立即解析封面图,若换签 401 触发全局鉴权事件,也会表现成“进入拼图作品后瞬间未登录”。资源换签失败只应让当前图片为空,不应清 token、广播 auth 事件或主动 refresh。
|
||||
- 追加处理:从推荐页点进公开拼图作品并启动完整运行态后,`startPuzzleRun`、通关自动 `submitPuzzleLeaderboard`、下一关 `advancePuzzleNextLevel` 和重开同样属于当前玩法局部同步;这些请求失败时只应留在拼图错误态,不应清 token 或广播 auth 事件。
|
||||
- 验证:`npm run test -- src/services/apiClient.test.ts src/services/assetReadUrlService.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle runtime uses frontend move merge logic and backend leaderboard"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle similar work keeps current run level progression"`。
|
||||
- 关联:`src/services/apiClient.ts`、`src/services/assetReadUrlService.ts`、`src/services/puzzle-runtime/puzzleRuntimeClient.ts`、`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、`docs/technical/RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md`。
|
||||
|
||||
## Rust 冷编译导致 api-server 健康检查误超时
|
||||
|
||||
@@ -235,11 +254,11 @@
|
||||
|
||||
## Rust 构建不要让不可用的 sccache 阻断 rustc
|
||||
|
||||
- 现象:Cargo 报 `could not execute process sccache ... rustc.exe -vV (never executed)`,真实 `rustc -Vv` 可以执行,但构建在调用包装器时失败。
|
||||
- 原因:环境或 Jenkinsfile 设置了 `RUSTC_WRAPPER=sccache`,但当前 Windows/Linux agent 上没有可执行的 `sccache`,或 PATH 中的 `sccache` shim 损坏。
|
||||
- 处理:本地临时排障可执行 `Remove-Item Env:RUSTC_WRAPPER -ErrorAction SilentlyContinue` 后重跑 Cargo;生产流水线必须先实际执行 `sccache --version`,失败时移除 `RUSTC_WRAPPER` 并回退到直接 `rustc`。
|
||||
- 验证:`rustc -Vv` 能输出版本;`cargo` 不再尝试调用不可用的 `sccache`;Jenkins 日志出现“未找到可用 sccache,改用 rustc 直接构建”后仍继续真实构建。
|
||||
- 关联:`jenkins/Jenkinsfile.production-stdb-module-build`、`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
- 现象:Cargo 报 `could not execute process sccache ... rustc.exe -vV (never executed)`,或 `sccache: caused by: Failed to send data to or receive data from server / Failed to read response header / failed to fill whole buffer`;真实 `rustc -Vv` 可以执行,但构建在调用包装器时失败。
|
||||
- 原因:环境、Jenkinsfile 或 `server-rs/.cargo/config.toml` 启用了 `sccache` wrapper,但当前 agent 没有可执行的 `sccache`、PATH 中 shim 损坏,或本地 sccache server/client 通道状态损坏。
|
||||
- 处理:本地临时排障可在 Git Bash 中执行 `RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo build ...`;`npm run dev:rust` 的 SpacetimeDB publish 已在命中 sccache 通信失败时自动清空 wrapper 重试一次;生产流水线必须先实际执行 `sccache --version`,失败时移除 `RUSTC_WRAPPER` 并回退到直接 `rustc`。
|
||||
- 验证:`rustc -Vv` 能输出版本;清空 wrapper 后 `cargo check --target=wasm32-unknown-unknown --release` 能通过;Jenkins 日志出现“未找到可用 sccache,改用 rustc 直接构建”后仍继续真实构建。
|
||||
- 关联:`scripts/dev-rust-stack.sh`、`jenkins/Jenkinsfile.production-stdb-module-build`、`docs/technical/SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md`、`docs/technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。
|
||||
|
||||
## 生产发布入口不要沿用旧 Jenkinsfile / 一体化脚本
|
||||
|
||||
@@ -256,3 +275,11 @@
|
||||
- 处理:Admin 任务配置页不展示范围选择,保存时固定 `scopeKind: 'user'`;API 和领域构造层拒绝非 `User`。
|
||||
- 验证:非 `user` scope 返回错误;相关测试覆盖 `Site` / `Module` / `Work` 被拒绝。
|
||||
- 关联:`docs/technical/RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md`、`docs/technical/ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`。
|
||||
|
||||
## 拼图发布 409 不一定是接口故障
|
||||
|
||||
- 现象:拼图结果页点击发布后,控制台出现 `POST /api/runtime/puzzle/agent/sessions/{sessionId}/actions 409 (Conflict)`,用户只看到发布失败。
|
||||
- 原因:`publish_puzzle_work` 是资产操作发布入口,发布前会预扣 `1` 枚光点;余额不足时后端按业务冲突返回 `409 CONFLICT`,`details.message` 为 `光点余额不足`。
|
||||
- 处理:前端发布弹窗在用户点击发布后必须保留并展示后端业务错误,不能只把错误写到弹窗背后的页面 banner。
|
||||
- 验证:`PuzzleResultView` 单测覆盖发布弹窗内展示 `光点余额不足`。
|
||||
- 关联:`src/components/puzzle-result/PuzzleResultView.tsx`、`docs/technical/PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md`、`docs/technical/ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md`。
|
||||
|
||||
Vendored
+6
-2
@@ -37,12 +37,16 @@ GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=false
|
||||
|
||||
APIMART_BASE_URL=
|
||||
APIMART_API_KEY=
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
VECTOR_ENGINE_BASE_URL=
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
HYPER3D_BASE_URL=https://api.hyper3d.com/api/v2
|
||||
HYPER3D_API_KEY=
|
||||
HYPER3D_MODEL_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
VOLCENGINE_SPEECH_API_KEY=
|
||||
VOLCENGINE_SPEECH_APP_ID=
|
||||
VOLCENGINE_SPEECH_ACCESS_KEY=
|
||||
|
||||
@@ -100,3 +100,13 @@
|
||||
3. 发现
|
||||
|
||||
创作 Tab 必须位于中间,并使用原推荐 Tab 的星光图标,保持几何和视觉上的主行动入口。推荐 Tab 改用游戏手柄图标,避免与创作图标重复。
|
||||
|
||||
## 9. 2026-05-08 新用户默认发现与推荐门禁补充
|
||||
|
||||
未登录新用户首次进入平台时默认落在“发现”Tab,不再直接进入“推荐”Tab 的内嵌运行态。
|
||||
|
||||
- 未登录用户点击底部或侧边栏“推荐”Tab 时,页面可切到推荐封面预览态,同时打开登录弹窗。
|
||||
- 未登录状态下推荐页只展示当前推荐作品封面,不启动作品运行态,不展示推荐作品信息区。
|
||||
- 未登录用户点击推荐页封面时,再次打开同一个登录弹窗;登录成功后由既有受保护动作继续进入作品详情或玩法入口。
|
||||
- 未登录状态下点击“下一个”只切换下一张推荐封面,不触发登录弹窗,也不启动玩法。
|
||||
- 已登录用户继续沿用推荐页内嵌运行态、上下滑切换和底部“下一个”行为。
|
||||
|
||||
@@ -34,16 +34,21 @@ GENARRATIVE_LLM_BASE_URL=
|
||||
GENARRATIVE_LLM_API_KEY=
|
||||
GENARRATIVE_LLM_MODEL=
|
||||
|
||||
# APIMart / OpenAI 兼容图片网关
|
||||
# APIMart / OpenAI 兼容 Responses 文本网关
|
||||
APIMART_BASE_URL=
|
||||
APIMART_API_KEY=
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
# VectorEngine / Suno / Vidu 音频生成网关
|
||||
VECTOR_ENGINE_BASE_URL=
|
||||
# VectorEngine / GPT-image-2 / Suno / Vidu 生成网关
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
# Hyper3D Rodin Gen-2 3D 模型生成
|
||||
HYPER3D_BASE_URL=https://api.hyper3d.com/api/v2
|
||||
HYPER3D_API_KEY=
|
||||
HYPER3D_MODEL_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
# 火山引擎豆包语音 ASR / TTS
|
||||
VOLCENGINE_SPEECH_API_KEY=
|
||||
VOLCENGINE_SPEECH_APP_ID=
|
||||
@@ -84,6 +89,9 @@ ARK_CHARACTER_VIDEO_MODEL / DASHSCOPE_CHARACTER_VIDEO_MODEL
|
||||
VOLCENGINE_SPEECH_API_KEY / VOLCENGINE_API_KEY
|
||||
VOLCENGINE_SPEECH_APP_ID / VOLCENGINE_ACCESS_KEY_ID
|
||||
VOLCENGINE_SPEECH_ACCESS_KEY / VOLCENGINE_SECRET_ACCESS_KEY
|
||||
HYPER3D_BASE_URL / RODIN_BASE_URL
|
||||
HYPER3D_API_KEY / RODIN_API_KEY
|
||||
HYPER3D_MODEL_REQUEST_TIMEOUT_MS / RODIN_MODEL_REQUEST_TIMEOUT_MS
|
||||
```
|
||||
|
||||
## 运行时行为
|
||||
@@ -93,8 +101,10 @@ VOLCENGINE_SPEECH_ACCESS_KEY / VOLCENGINE_SECRET_ACCESS_KEY
|
||||
3. 文本 LLM provider 为 `ark` 且未配置 `GENARRATIVE_LLM_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
|
||||
4. 角色视频 provider 复用 Ark 且未配置 `ARK_CHARACTER_VIDEO_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
|
||||
5. 具体模型名缺失时不在配置层伪造默认模型,调用到对应能力时由下游配置校验返回缺配置错误。
|
||||
6. VectorEngine 音频生成只读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,不复用 `APIMART_*`、`GENARRATIVE_LLM_*` 或前端变量。
|
||||
6. VectorEngine 图片与音频生成只读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,其中 GPT-image-2 图片生成额外读取 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;不复用 `APIMART_*`、`GENARRATIVE_LLM_*` 或前端变量。
|
||||
7. 火山引擎语音能力由 `platform-speech` 收口协议帧与上游鉴权,`api-server` 只暴露平台鉴权后的代理路由,不向前端返回任何密钥字段。
|
||||
8. Hyper3D Rodin Gen-2 使用公开默认 `https://api.hyper3d.com/api/v2`,API Key 只读取 `HYPER3D_API_KEY` / `RODIN_API_KEY`,不复用文本 LLM、图片或音频网关密钥。
|
||||
9. APIMart 当前只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态理解链路;GPT-image-2 图片生成不得再读取 APIMart 配置。
|
||||
|
||||
## 示例文件
|
||||
|
||||
|
||||
@@ -135,5 +135,5 @@
|
||||
2. 再请求当前 Rust API 目标,例如 `http://127.0.0.1:3100/api/auth/login-options` 或 `http://127.0.0.1:8082/api/auth/login-options`。
|
||||
3. 若直连 API 成功而 3000 返回 `500`,检查 `RUST_SERVER_TARGET`、`GENARRATIVE_API_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 是否指向仍在监听的 API 端口。
|
||||
4. `npm run dev` / `npm run dev:rust` 完整栈默认由脚本计算 API 端口;加载 `.env.local` 给后端使用后,脚本必须重新固定 `RUST_SERVER_TARGET`,避免 `.env.local` 中的旧代理目标覆盖本次启动的实际 API 端口。
|
||||
5. `npm run dev:web` 只启动前端,不会自动拉起 Rust API;如果单独使用它,脚本会先探测 `.env.local` / 当前环境里声明的目标,再回退到本机常见端口,最终只会接入一个真实可用的 `api-server`。
|
||||
5. `npm run dev:web` 只启动前端,不会自动拉起 Rust API;如果 `.env.local` / 当前环境已经显式声明 `GENARRATIVE_RUNTIME_SERVER_TARGET`、`RUST_SERVER_TARGET`、`GENARRATIVE_API_TARGET` 或 `GENARRATIVE_API_PORT`,脚本必须固定使用该目标。目标当下不可用时只打印警告,不自动切到另一个端口,避免前端进程长时间绑定到随后会停掉的临时 API。
|
||||
6. 如果 `3000` 仍然返回 `500`,先确认浏览器是不是还开着旧的前端进程。当前脚本如果因为端口占用漂移到 `3001` / `3002`,应直接关掉旧进程后重启,而不是继续用旧的 3000 页面判断登录入口状态。
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Hyper3D Rodin Gen-2 3D 模型生成接入方案 2026-05-08
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本方案用于接入 Hyper3D Rodin Gen-2 的文生 3D 模型与图生 3D 模型能力。
|
||||
|
||||
本次只做后端安全代理与前端可复用 client,不新增 SpacetimeDB 表,不落正式资产对象,不把 Hyper3D API Key 下发到前端。生成结果仍由调用方在拿到下载链接后决定是否进入 OSS / `asset_object` 的正式资产链。
|
||||
|
||||
## 2. 参考接口
|
||||
|
||||
参考文档:
|
||||
|
||||
- `https://developer.hyper3d.ai/api-specification/rodin-generation-gen2`
|
||||
- `https://developer.hyper3d.ai/api-specification/check-status`
|
||||
- `https://developer.hyper3d.ai/api-specification/download-results`
|
||||
|
||||
上游接口:
|
||||
|
||||
```text
|
||||
POST https://api.hyper3d.com/api/v2/rodin
|
||||
POST https://api.hyper3d.com/api/v2/status
|
||||
POST https://api.hyper3d.com/api/v2/download
|
||||
```
|
||||
|
||||
Rodin Gen-2 提交接口必须使用 `multipart/form-data`。文本生成时提交 `prompt`;图片生成时提交一个或多个 `images` 文件,可选 `prompt` 作为辅助描述。两种模式均固定提交 `tier=Gen-2`。
|
||||
|
||||
## 3. 环境变量
|
||||
|
||||
```text
|
||||
HYPER3D_BASE_URL=https://api.hyper3d.com/api/v2
|
||||
HYPER3D_API_KEY=
|
||||
HYPER3D_MODEL_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
兼容变量:
|
||||
|
||||
```text
|
||||
RODIN_BASE_URL
|
||||
RODIN_API_KEY
|
||||
RODIN_MODEL_REQUEST_TIMEOUT_MS
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. `HYPER3D_API_KEY` / `RODIN_API_KEY` 只允许写入本地或生产私密环境,不提交到 Git。
|
||||
2. 缺少 API Key 时,后端返回 `503 SERVICE_UNAVAILABLE`。
|
||||
3. `HYPER3D_BASE_URL` 默认使用公开 API 基础地址;如果团队后续改用代理网关,可通过环境变量覆盖。
|
||||
|
||||
## 4. 后端路由
|
||||
|
||||
新增 4 个鉴权路由:
|
||||
|
||||
| 方法 | 路由 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/assets/hyper3d/text-to-model` | 提交 Rodin Gen-2 文生模型任务 |
|
||||
| `POST` | `/api/assets/hyper3d/image-to-model` | 提交 Rodin Gen-2 图生模型任务 |
|
||||
| `POST` | `/api/assets/hyper3d/status` | 使用 `subscriptionKey` 查询任务状态 |
|
||||
| `POST` | `/api/assets/hyper3d/download` | 使用 `taskUuid` 获取模型下载列表 |
|
||||
|
||||
文生模型请求最小体:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "一只低多边形宝箱,适合 RPG 游戏资产",
|
||||
"geometryFileFormat": "glb",
|
||||
"material": "PBR",
|
||||
"quality": "medium",
|
||||
"meshMode": "Quad",
|
||||
"previewRender": true
|
||||
}
|
||||
```
|
||||
|
||||
图生模型请求最小体:
|
||||
|
||||
```json
|
||||
{
|
||||
"imageDataUrls": ["data:image/png;base64,..."],
|
||||
"prompt": "保留主体轮廓,生成游戏可用 3D 模型",
|
||||
"conditionMode": "concat",
|
||||
"geometryFileFormat": "glb"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 约束
|
||||
|
||||
1. 图片只接受 `data:image/png|jpeg|webp;base64,...`,最多 5 张。
|
||||
2. 单张图片解码后不超过 10MB。
|
||||
3. `geometryFileFormat` 限定为 `glb/usdz/fbx/obj/stl`,默认 `glb`。
|
||||
4. `material` 限定为 `PBR/Shaded/All`,默认 `PBR`。
|
||||
5. `quality` 限定为 `high/medium/low/extra-low`,默认 `medium`。
|
||||
6. `meshMode` 限定为 `Quad/Raw`,默认 `Quad`。
|
||||
7. `addons` 首版只允许 `HighPack`。
|
||||
8. `bboxCondition` 必须为 3 个正数,按上游要求序列化为 JSON 字符串。
|
||||
|
||||
## 6. 返回语义
|
||||
|
||||
提交任务成功后返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"provider": "hyper3d-rodin",
|
||||
"mode": "text-to-model",
|
||||
"taskUuid": "task-uuid",
|
||||
"subscriptionKey": "subscription-key",
|
||||
"jobUuids": ["job-uuid"],
|
||||
"message": "Submitted.",
|
||||
"tier": "Gen-2"
|
||||
}
|
||||
```
|
||||
|
||||
状态查询会把上游 `Waiting / Generating / Done / Failed` 归一化为 `waiting / generating / done / failed / unknown`。下载接口只返回上游 `list.name` 与 `list.url`,不在后端转存文件。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
建议执行:
|
||||
|
||||
```bash
|
||||
npm run check:encoding
|
||||
npm run typecheck
|
||||
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts hyper3d
|
||||
cargo test -p api-server hyper3d
|
||||
cargo check -p api-server
|
||||
```
|
||||
|
||||
真实 API smoke 只在本地私密环境设置 `HYPER3D_API_KEY` 后执行。提交生成任务会消耗 Hyper3D Credit,默认验证不自动调用真实生成接口。
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
### 1. 图片生成
|
||||
|
||||
1. 拼图默认使用 APIMart `gpt-image-2` 生成图,外部请求尺寸固定为 `1:1`;`nanobanana2` 仍映射为 `gemini-3.1-flash-image-preview`。
|
||||
1. 拼图默认使用 VectorEngine `gpt-image-2-all` 生成图,外部请求尺寸固定为 `1024x1024`;前端历史 `nanobanana2` 选项只保留兼容展示,后端同样回落到 VectorEngine GPT-image-2-all,不再调用 APIMart 图片网关。
|
||||
2. 历史 `original` 或空模型值只做兼容输入,不再进入 DashScope 原模型链路,统一按 `gpt-image-2` 路由。
|
||||
3. 文生图和参考图生图共用同一个正方形尺寸口径,禁止一条链路仍生成竖屏或横版图。
|
||||
4. 拼图图片提示词明确写入 `1:1 正方形画布`,继续保留适配 `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 拼图切块、主体清晰、层次明确、无文字水印等约束。
|
||||
5. 文生图正向 prompt 必须由后端压缩到 `500` 字符以内,优先保留玩家画面描述开头与固定拼图约束,避免上游把超长 prompt 判为“请求参数不合法”。
|
||||
6. APIMart 上游失败时,api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。
|
||||
6. VectorEngine 上游失败时,api-server 必须在错误 details 中保留业务 message、`upstreamStatus` 和截断后的 `rawExcerpt`,日志也要记录同样的摘要,避免生成进度页只能看到通用 HTTP 文案。
|
||||
7. 图片生成仍由 `api-server` 执行。SpacetimeDB reducer 不做网络 I/O。
|
||||
8. 光点预扣失败属于钱包或 SpacetimeDB 服务链路错误,不得映射成 `400 BAD_REQUEST`。除余额不足返回 `409 CONFLICT` 外,其余预扣异常统一按上游/服务错误暴露,避免生成页误提示“请求参数不合法”。
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
|
||||
## 验收
|
||||
|
||||
1. 点击拼图草稿生成或重新生成画面时,后端请求 APIMart 的 `size` 为 `1:1`,默认模型为 `gpt-image-2`。
|
||||
1. 点击拼图草稿生成或重新生成画面时,后端请求 VectorEngine 的 `size` 为 `1024x1024`,上游模型为 `gpt-image-2-all`。
|
||||
2. 图片提示词包含 `1:1 正方形拼图关卡`。
|
||||
3. 图片提示词长度不超过 `500` 字符,超长画面描述会被截断,但适配 `3x3 / 4x4 / 5x5 / 6x6 / 7x7` 拼图切块、`避免文字、水印、边框和 UI 元素` 等玩法约束不能丢。
|
||||
4. APIMart 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message,后端日志能看到 `upstreamStatus` 和 `rawExcerpt`。
|
||||
4. VectorEngine 返回参数错误、任务失败或非 2xx 时,前端错误优先展示后端 details.message,后端日志能看到 `upstreamStatus` 和 `rawExcerpt`。
|
||||
5. 正式拼图 run 中拖动拼块后,前端立即更新棋盘、合并块和通关状态,不再等待 `/drag`。
|
||||
6. 移动端运行时棋盘为正方形,并尽量贴近屏幕两侧边缘。
|
||||
7. 基础单块和合并块都能看到圆角,合并块的外凸角与内凹角都不是直角,且图片不会溢出圆角裁剪。
|
||||
|
||||
@@ -56,3 +56,9 @@
|
||||
3. 标签少于 `3` 个时,发布弹窗明确提示“正式标签数量必须在 3 到 6 之间”。
|
||||
4. 标签补到 `3~6` 个后,无需刷新页面即可通过前端发布校验。
|
||||
5. 结果页顶部能看到轻量自动保存状态,不额外堆叠说明文案。
|
||||
|
||||
## 2026-05-09 发布失败提示补充
|
||||
|
||||
`publish_puzzle_work` 属于资产操作发布入口,按 `ASSET_GENERATION_POINTS_CONSUMPTION_2026-04-27.md` 会在发布 mutation 前预扣 `1` 枚光点。余额不足时后端返回 `409 CONFLICT`,响应 `details.message` 为 `光点余额不足`,这属于业务拒绝,不是拼图发布接口不可用。
|
||||
|
||||
结果页发布弹窗必须在用户点击发布后继续展示后端错误原因,不能只把错误写到弹窗背后的页面 banner。这样余额不足、SpacetimeDB 发布门禁或其他后端业务错误都会在当前独立发布面板中直接可见。
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
## 文档列表
|
||||
|
||||
- [RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md](./RUST_WORKSPACE_DEPENDENCY_CONSOLIDATION_2026-05-07.md):记录 `server-rs` Cargo 依赖集中配置口径,第三方版本和 workspace 内部 crate path 统一维护在根 `server-rs/Cargo.toml`,成员 crate 只保留 feature/optional 差异。
|
||||
- [RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md](./RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md):记录平台推荐页自动加载作品后局部运行态请求 `401` 不应扩散成全局登出的修复,覆盖请求层局部鉴权失败隔离、推荐页 embedded 运行态启动和回归测试。
|
||||
- [VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md](./VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md):记录 GPT-image-2 图片生成从 APIMart 迁移到 VectorEngine `gpt-image-2-all` 的接口、环境变量、尺寸映射、错误口径和验收命令。
|
||||
- [SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md](./SPACETIMEDB_PUBLISH_SCCACHE_FALLBACK_2026-05-09.md):记录本地 `spacetime publish` 被 sccache wrapper 通信异常阻断时的根因、`dev-rust-stack` 自动降级策略和手动排障命令。
|
||||
- [RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md](./RECOMMEND_RUNTIME_AUTH_FAILURE_ISOLATION_FIX_2026-05-09.md):记录平台推荐页自动加载作品、公开拼图作品完整运行态和展示层图片换签的局部请求 `401` 不应扩散成全局登出的修复,覆盖请求层局部鉴权失败隔离、推荐页 embedded 运行态启动、拼图开局/排行榜/下一关和回归测试。
|
||||
- [AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md](./AUTH_GATE_LOGIN_RACE_GUARD_FIX_2026-05-09.md):记录 `AuthGate` 登录成功后又被旧 hydrate 覆盖回未登录态的竞态根因、版本号保护修复与回归测试。
|
||||
- [HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md](./HYPER3D_RODIN_GEN2_MODEL_GENERATION_2026-05-08.md):记录 Hyper3D Rodin Gen-2 文生 3D 模型、图生 3D 模型、状态查询和下载列表的后端代理、环境变量、请求约束与验收边界。
|
||||
- [VOLCENGINE_SPEECH_STREAMING_INTEGRATION_2026-05-08.md](./VOLCENGINE_SPEECH_STREAMING_INTEGRATION_2026-05-08.md):记录火山引擎大模型 ASR 双向流式、TTS WebSocket 双向流式和 TTS HTTP SSE 单向流式的后端代理、环境变量、协议帧和验收边界。
|
||||
- [VECTOR_ENGINE_AUDIO_GENERATION_SUNO_VIDU_2026-05-08.md](./VECTOR_ENGINE_AUDIO_GENERATION_SUNO_VIDU_2026-05-08.md):记录视觉小说结果页接入 VectorEngine Suno 文生背景音乐与 Vidu 文生音效的接口、环境变量、后端路由、OSS 资产回写和前端弹层交互边界。
|
||||
- [PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md](./PROFILE_FEEDBACK_BACKEND_INTEGRATION_2026-05-08.md):冻结“我的”页签帮助与反馈入口的后端接入方案,覆盖 `POST /api/profile/feedback`、`profile_feedback_submission`、凭证图片 Data URL 校验和前端预览/提交边界。
|
||||
@@ -20,7 +23,7 @@
|
||||
- [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md):冻结单机生产部署目标,从旧一体化启动脚本切到 Nginx、systemd 托管 SpacetimeDB 与 Rust `api-server`,并记录生产 Jenkins 流水线拆分计划和首批部署骨架。
|
||||
- [PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md](./PUZZLE_RUNTIME_FRONTEND_LOGIC_REHOME_2026-05-02.md):记录拼图正式平台入口移动、交换、合并、拆分和通关裁决收回前端即时运行态,排行榜、下一关和游玩记录继续由后端持久化处理。
|
||||
- [RPG_FOUNDATION_DRAFT_ROLE_DOSSIER_TIMEOUT_FALLBACK_2026-05-02.md](./RPG_FOUNDATION_DRAFT_ROLE_DOSSIER_TIMEOUT_FALLBACK_2026-05-02.md):记录 `agent-foundation-*-dossier-batch-*` 无搜索 Responses 请求超时后的本地养成档案兜底,避免底稿主链被尾部角色润色阶段阻断。
|
||||
- [RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md](./RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md):记录 RPG 角色主图与场景幕背景图统一迁移到 APIMart OpenAI 兼容 `gpt-image-2` 生图入口的边界、配置和验收口径。
|
||||
- [RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md](./RPG_IMAGE_GENERATION_GPT_IMAGE_2_MIGRATION_2026-05-02.md):记录 RPG 角色主图与场景幕背景图统一迁移到 `gpt-image-2` 生图入口的边界、配置和验收口径;2026-05-09 起实际上游以 VectorEngine 迁移文档为准。
|
||||
- [RPG_FOUNDATION_DRAFT_LANDMARK_SEED_BATCH_TIMEOUT_FIX_2026-05-02.md](./RPG_FOUNDATION_DRAFT_LANDMARK_SEED_BATCH_TIMEOUT_FIX_2026-05-02.md):记录 `agent-foundation-landmark-seed-batch-1` 无搜索 Responses 请求超时的根因,并将场景骨架批次收敛为单场景生成。
|
||||
- [PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md](./PROFILE_MAIN_ROUTE_VITE_PROXY_FIX_2026-05-02.md):记录“我的”和“存档”页面在本地把 `/api/profile/*` 请求落到 Vite SPA fallback、导致 HTML 被当 JSON 解析的根因,以及 `/api/profile` 代理补齐与回归测试。
|
||||
- [SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md](./SERVER_RS_DDD_WP_DEL_CLEANUP_2026-05-01.md):记录 `WP-DEL 删除旧层与命名收口`,物理删除旧 runtime story HTTP DTO、前端 `Rpg*` alias、旧 `/api/custom-world/*` 非 runtime 前缀、Puzzle `local-next-level` 入口和 `/generated-*` 资产直读代理;生成资产读取统一走 OSS read-url 链路。
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
登录成功进入平台推荐页后,推荐页会自动加载一个公开作品并启动嵌入式运行态。实际联调中出现过:作品刚加载出来,前端又瞬间回到未登录状态;停留在其他页面,或推荐页没有成功加载出作品时不会复现。
|
||||
|
||||
后续复测又发现:登录成功后,从推荐页点进拼图公开作品详情并启动完整拼图运行态,也可能在开局或通关后瞬间退回未登录。两类现象的底层问题一致,都是玩法/展示层局部请求把 `401` 扩散成全局鉴权事件。
|
||||
|
||||
## 根因
|
||||
|
||||
推荐页首屏的作品运行态启动是后台自动副作用,不是用户主动点击的账号操作。它会触发多条受保护请求,例如:
|
||||
@@ -16,6 +18,10 @@
|
||||
|
||||
这些请求一旦遇到本地代理错配、后端短暂不可用或 token 刷新失败,原请求层会按普通受保护请求处理 `401`,清空 access token 并广播全局鉴权变更。`AuthGate` 收到事件后重新 hydrate,于是当前用户界面被切回未登录态。
|
||||
|
||||
推荐页里还有一类更隐蔽的触发点:`ResolvedAssetImage` / `useResolvedAssetReadUrl` 在挂载时会请求 `/api/assets/read-url` 给 generated 私有图片换签。它本质上也是展示层后台请求,若按普通受保护请求处理 `401`,同样会把一次图片换签失败放大成全局掉线。
|
||||
|
||||
公开拼图作品的完整运行态还会在用户进入作品后自动发起 `startPuzzleRun`,通关后自动 `submitPuzzleLeaderboard`,点击下一关时 `advancePuzzleNextLevel`。这些请求属于当前玩法的运行态同步,失败时应该落到当前拼图错误态;它们不能清空全局 access token,也不能触发 `AuthGate` 重新 hydrate。
|
||||
|
||||
## 修复
|
||||
|
||||
本次把推荐页自动运行态请求定义为“卡片级后台请求”:
|
||||
@@ -23,14 +29,18 @@
|
||||
1. `apiClient` 增加 `clearAuthOnUnauthorized` 选项,允许局部请求在 `401` 时不清空全局 token。
|
||||
2. 推荐页嵌入式运行态请求统一传入 `skipRefresh: true`、`notifyAuthStateChange: false`、`clearAuthOnUnauthorized: false`。
|
||||
3. 推荐页自动启动作品前必须满足 `canReadProtectedData`,避免 `AuthGate` 仍在恢复阶段就提前发起受保护写请求。
|
||||
4. 普通用户主动点击“启动”、Remix、发布、点赞等路径继续保留默认全局鉴权处理。
|
||||
4. generated 图片换签请求同样使用局部后台鉴权选项并跳过 refresh,失败只让当前图片为空,不触发全局登录态清理。
|
||||
5. 公开拼图作品进入完整运行态后,把本次 run 标记为 `isolated` 鉴权模式;开局、重开、排行榜提交和下一关推进都沿用局部鉴权选项。
|
||||
6. Remix、发布、点赞、账号设置、退出登录等真正账号动作继续保留默认全局鉴权处理。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `npm run test -- src/services/apiClient.test.ts`
|
||||
1. `npm run test -- src/services/apiClient.test.ts src/services/assetReadUrlService.test.ts`
|
||||
2. `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "home recommendation starts embedded puzzle"`
|
||||
3. `npm run typecheck`
|
||||
4. `npm run check:encoding`
|
||||
3. `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle runtime uses frontend move merge logic and backend leaderboard"`
|
||||
4. `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "formal puzzle similar work keeps current run level progression"`
|
||||
5. `npm run typecheck`
|
||||
6. `npm run check:encoding`
|
||||
|
||||
## 关联文件
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ RPG 创作链路里有两类正式图片资产需要统一模型:
|
||||
1. 角色主图候选生成。
|
||||
2. 场景幕背景图生成。
|
||||
|
||||
旧实现中角色主图默认使用 `wan2.7-image-pro`,场景图根据是否有参考图分别使用 DashScope 文生图与图生图模型。拼图链路已经接入 APIMart 的 OpenAI 兼容 `/images/generations`,并以 `gpt-image-2` 作为默认图片模型,因此本次 RPG 图片迁移复用同一类服务端配置与请求口径。
|
||||
旧实现中角色主图默认使用 `wan2.7-image-pro`,场景图根据是否有参考图分别使用 DashScope 文生图与图生图模型。拼图链路已经接入 GPT-image-2 图片生成,因此本次 RPG 图片迁移复用同一类服务端配置与请求口径。2026-05-09 起,GPT-image-2 图片生成上游统一迁移到 VectorEngine,具体接口以 `VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md` 为准。
|
||||
|
||||
## 落地范围
|
||||
|
||||
@@ -26,9 +26,9 @@ RPG 创作链路里有两类正式图片资产需要统一模型:
|
||||
服务端使用:
|
||||
|
||||
```text
|
||||
POST {APIMART_BASE_URL}/images/generations
|
||||
Authorization: Bearer {APIMART_API_KEY}
|
||||
model = gpt-image-2
|
||||
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
|
||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||
model = gpt-image-2-all
|
||||
```
|
||||
|
||||
请求体统一包含:
|
||||
@@ -36,16 +36,15 @@ model = gpt-image-2
|
||||
1. `model`
|
||||
2. `prompt`
|
||||
3. `n`
|
||||
4. `official_fallback = true`
|
||||
5. `size`
|
||||
6. 有参考图时增加 `image_urls`
|
||||
4. `size`
|
||||
5. 有参考图时增加 `image`
|
||||
|
||||
尺寸归一规则:
|
||||
|
||||
1. `1024*1024`、`1024x1024`、`1:1` -> `1:1`
|
||||
2. `1280*720`、`1600*900`、`16:9` -> `16:9`
|
||||
1. `1024*1024`、`1024x1024`、`1:1` -> `1024x1024`
|
||||
2. `1280*720`、`1600*900`、`16:9` -> `1536x1024`
|
||||
|
||||
响应解析兼容同步 `data[].url`、`data[].b64_json` 与异步 `task_id` / `GET /tasks/{task_id}` 结构。
|
||||
响应解析同步 `data[].url` 与 `data[].b64_json`;VectorEngine GPT-image-2-all 当前不再使用 APIMart 异步 `task_id` / `GET /tasks/{task_id}` 结构。
|
||||
|
||||
## 非范围
|
||||
|
||||
@@ -56,22 +55,22 @@ model = gpt-image-2
|
||||
|
||||
## 配置
|
||||
|
||||
本次复用已有 APIMart 配置:
|
||||
本次复用 VectorEngine 图片配置:
|
||||
|
||||
```text
|
||||
APIMART_BASE_URL=https://api.apimart.ai/v1
|
||||
APIMART_API_KEY=...
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=...
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
`APIMART_API_KEY` 缺失时,角色主图与场景图返回 `SERVICE_UNAVAILABLE`,`details.provider = "apimart"`。
|
||||
`VECTOR_ENGINE_API_KEY` 缺失时,角色主图与场景图返回 `SERVICE_UNAVAILABLE`,`details.provider = "vector-engine"`。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 角色主图生成请求上游 `model` 为 `gpt-image-2`,且携带 `official_fallback = true`。
|
||||
2. 场景图生成请求上游 `model` 为 `gpt-image-2`,且携带 `official_fallback = true`。
|
||||
1. 角色主图生成请求上游 `model` 为 `gpt-image-2-all`,且不携带 `official_fallback`。
|
||||
2. 场景图生成请求上游 `model` 为 `gpt-image-2-all`,且不携带 `official_fallback`。
|
||||
3. 旧前端或历史草稿传 `wan2.7-image-pro` 时不会回退旧模型。
|
||||
4. 场景参考图生成仍能把参考图 Data URL 放入 `image_urls`。
|
||||
4. 场景参考图生成仍能把参考图 Data URL 放入 `image`。
|
||||
5. 角色主图生成后仍执行原有 PNG 透明背景处理与 OSS 写入。
|
||||
6. `cargo test -p api-server character_visual --manifest-path server-rs/Cargo.toml` 通过。
|
||||
7. `cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml` 通过。
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# SpacetimeDB publish sccache 降级处理
|
||||
|
||||
## 背景
|
||||
|
||||
Windows 本地执行 `npm run dev:rust` 或 `spacetime publish` 时,`spacetime` 会在内部调用 Cargo 构建 `server-rs/crates/spacetime-module`。因为 `server-rs/.cargo/config.toml` 配置了 `rustc-wrapper = "sccache"`,即使当前 shell 没有设置 `RUSTC_WRAPPER`,Cargo 仍会先执行 `sccache rustc -vV`。
|
||||
|
||||
当本机 sccache server 状态损坏、client/server 通信异常或版本残留不一致时,可能出现:
|
||||
|
||||
```text
|
||||
sccache: error: failed to execute compile
|
||||
sccache: caused by: Failed to send data to or receive data from server
|
||||
sccache: caused by: Failed to read response header
|
||||
sccache: caused by: failed to fill whole buffer
|
||||
```
|
||||
|
||||
这类错误发生在 rustc wrapper 层,不能说明 SpacetimeDB module 代码本身编译失败。
|
||||
|
||||
## 本地开发处理
|
||||
|
||||
`scripts/dev-rust-stack.sh` 的 publish 阶段保留首次正常 `sccache` 构建;如果 stderr 命中 sccache 通信或 wrapper 失败特征,则自动在同一 `--root-dir`、同一发布参数下清空本次子进程的 `RUSTC_WRAPPER` 与 `CARGO_BUILD_RUSTC_WRAPPER` 后重试。
|
||||
|
||||
该处理只影响本次 publish 子进程,不修改 `server-rs/.cargo/config.toml`,也不删除本地 target 缓存。
|
||||
|
||||
## 手动排障命令
|
||||
|
||||
优先确认 rustc 本身可用:
|
||||
|
||||
```bash
|
||||
rustc -vV
|
||||
```
|
||||
|
||||
如果只想绕过本次 Cargo 构建的 sccache wrapper,可在 Git Bash 中执行:
|
||||
|
||||
```bash
|
||||
cd server-rs/crates/spacetime-module
|
||||
RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo build --target=wasm32-unknown-unknown --release
|
||||
```
|
||||
|
||||
如果需要排查 sccache server 状态:
|
||||
|
||||
```bash
|
||||
sccache --show-stats
|
||||
sccache --stop-server
|
||||
sccache --start-server
|
||||
```
|
||||
|
||||
`sccache --stop-server` 本身也可能因为 server 通道已损坏而失败;此时不应阻断本地开发 publish,先使用 wrapper 降级完成验证。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `bash -n scripts/dev-rust-stack.sh`
|
||||
2. `RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo check --target=wasm32-unknown-unknown --release`
|
||||
3. 重新运行 `npm run dev:rust`,看到 sccache 通信失败时脚本应打印降级提示并继续真实构建。
|
||||
@@ -39,6 +39,8 @@ spacetime --root-dir="${GENARRATIVE_SPACETIME_ROOT_DIR}" ...
|
||||
|
||||
`spacetime start` 不再额外设置 `--data-dir`,启动前会先执行 Ubuntu 专用 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin/<version>/spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin/<version>/spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`;当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`。启动参数、探活和 root-dir 占用判定都使用同一个 `.spacetimedb/`。这样可以把发布包与部署机全局 `~/.spacetime` 隔离,避免后续人工 `spacetime login` 影响本地发布包。但如果旧 `.spacetimedb/` 已经由另一个身份创建,仍需要按第 4 节处理。
|
||||
|
||||
本地 `npm run dev:rust` / `scripts/dev-rust-stack.sh` 也必须遵循同一条规则:`server ping`、`start` 和 `publish` 都显式使用 `server-rs/.spacetimedb/local` 作为 `--root-dir`。不要让发布命令回退到全局 CLI 登录态,否则会出现本地 root 已有目标库权限,但裸 `spacetime publish` 仍使用另一个身份发起预检查并返回 403。
|
||||
|
||||
## 4. 排查与处理
|
||||
|
||||
先在执行 `start.sh` 的同一台机器、同一用户下确认身份:
|
||||
@@ -48,6 +50,16 @@ spacetime --root-dir ./.spacetimedb login show
|
||||
spacetime --root-dir ./.spacetimedb list --server http://127.0.0.1:3101
|
||||
```
|
||||
|
||||
本地开发栈排查时使用仓库本地 root:
|
||||
|
||||
```bash
|
||||
spacetime login show
|
||||
spacetime --root-dir server-rs/.spacetimedb/local login show
|
||||
spacetime --root-dir server-rs/.spacetimedb/local list --server http://127.0.0.1:3101
|
||||
```
|
||||
|
||||
如果裸 `spacetime login show` 的身份与 `--root-dir server-rs/.spacetimedb/local login show` 不一致,而目标库只出现在本地 root 的 `list` 结果中,说明不能使用裸 `spacetime publish`。应通过 `npm run dev:rust` 或显式追加 `--root-dir=server-rs/.spacetimedb/local` 重新发布。
|
||||
|
||||
如果目标是本地部署库,且允许清空本地数据:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# VectorEngine GPT-image-2 图片生成迁移 2026-05-09
|
||||
|
||||
## 背景
|
||||
|
||||
GPT-image-2 图片生成此前通过 APIMart OpenAI 兼容入口执行。为统一供应商网关,本次参考 VectorEngine Apifox 文档 `https://vectorengine.apifox.cn/api-448710071`,把仓库内所有 GPT-image-2 生图调用迁移到 VectorEngine,不再使用 APIMart 图片网关。
|
||||
|
||||
APIMart 仍只保留给创意 Agent 的 `gpt-5` Responses 文本/多模态理解链路;不要把该文本链路与 GPT-image-2 图片生成配置混用。
|
||||
|
||||
## 参考接口
|
||||
|
||||
VectorEngine 正式环境基础地址来自 Apifox 项目环境:
|
||||
|
||||
```text
|
||||
https://api.vectorengine.ai
|
||||
```
|
||||
|
||||
GPT-image-2-all 生图接口:
|
||||
|
||||
```text
|
||||
POST /v1/images/generations
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-image-2-all",
|
||||
"size": "1024x1024",
|
||||
"n": 1,
|
||||
"prompt": "生成一只猫"
|
||||
}
|
||||
```
|
||||
|
||||
参考图场景可按文档字段追加:
|
||||
|
||||
```json
|
||||
{
|
||||
"image": ["data:image/png;base64,..."]
|
||||
}
|
||||
```
|
||||
|
||||
响应体按同步 OpenAI Images 结构读取:
|
||||
|
||||
```json
|
||||
{
|
||||
"created": 1776909189,
|
||||
"data": [
|
||||
{
|
||||
"revised_prompt": "",
|
||||
"url": "https://pro.filesystem.site/cdn/20260423/example.webp"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 尺寸映射
|
||||
|
||||
VectorEngine 文档要求使用像素尺寸,不再使用 APIMart 的比例写法:
|
||||
|
||||
| 旧输入 | VectorEngine 请求值 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `1:1`、`1024*1024`、`1024x1024` | `1024x1024` | 拼图、方洞局部贴图、角色主形象 |
|
||||
| `16:9`、`1280*720`、`1600*900`、`1536x1024` | `1536x1024` | 场景图、封面图、方洞横版背景 |
|
||||
| `1024x1536` | `1024x1536` | 竖版图 |
|
||||
| `2048x1152` | `1536x1024` | 开局 CG 故事板首版降为文档明确支持的横版尺寸 |
|
||||
|
||||
若调用方传入其它非空尺寸,后端先透传,方便后续跟随 VectorEngine 文档扩展;空值统一回落到 `1024x1024`。
|
||||
|
||||
## 后端落点
|
||||
|
||||
1. `server-rs/crates/api-server/src/openai_image_generation.rs`
|
||||
- 保留当前共享 helper 文件名与函数名,减少 RPG、方洞等调用方改动面。
|
||||
- 内部配置改读 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` / `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`。
|
||||
- 请求模型固定为 `gpt-image-2-all`,不再写 `official_fallback`。
|
||||
- 请求路径改为 `/v1/images/generations`,响应直接解析 `data[].url` / `data[].b64_json`,不再轮询 `/tasks/{task_id}`。
|
||||
2. `server-rs/crates/api-server/src/puzzle.rs`
|
||||
- 拼图默认 `gpt-image-2` 前端值继续兼容,但上游请求模型统一映射到 `gpt-image-2-all`。
|
||||
- `nanobanana2` / `gemini-3.1-flash-image-preview` 不再走 APIMart;当前阶段统一回落到 VectorEngine GPT-image-2-all,避免保留旧图片网关。
|
||||
- 错误 `details.provider` 改为 `vector-engine`。
|
||||
3. `.codex/skills/gpt-image-2-apimart/`
|
||||
- 目录名暂不强制迁移,避免本地插件索引漂移;Skill 文案与脚本行为改为 VectorEngine。
|
||||
|
||||
## 环境变量
|
||||
|
||||
```text
|
||||
VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai
|
||||
VECTOR_ENGINE_API_KEY=
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
1. GPT-image-2 图片生成不读取 `APIMART_BASE_URL`、`APIMART_API_KEY` 或 `APIMART_IMAGE_REQUEST_TIMEOUT_MS`。
|
||||
2. `VECTOR_ENGINE_BASE_URL` 仍允许部署环境覆盖,不在代码中绑定私有网关。
|
||||
3. `VECTOR_ENGINE_API_KEY` 只能进入本地或生产私密环境文件,不提交到 Git。
|
||||
|
||||
## 非范围
|
||||
|
||||
1. 不迁移创意 Agent 的 APIMart `gpt-5` Responses 链路。
|
||||
2. 不改变 SpacetimeDB 表结构、migration 或 bindings。
|
||||
3. 不改前端 UI 文案和模型选择控件展示。
|
||||
4. 不新增新的图片资产表或图片代理路由。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 所有 GPT-image-2 生图请求都走 `POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations`。
|
||||
2. 请求体 `model = gpt-image-2-all`,尺寸为 VectorEngine 支持的像素尺寸。
|
||||
3. 请求体不再包含 `official_fallback`。
|
||||
4. 参考图字段使用 `image`,不再使用 APIMart 的 `image_urls`。
|
||||
5. 缺少 `VECTOR_ENGINE_BASE_URL` 或 `VECTOR_ENGINE_API_KEY` 时返回 `503 SERVICE_UNAVAILABLE`,`details.provider = "vector-engine"`。
|
||||
6. 上游错误映射为 `502 UPSTREAM_ERROR`,保留 `upstreamStatus`、业务 message 和截断后的 raw excerpt。
|
||||
7. 运行 `npm run check:encoding`、`cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server puzzle --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server custom_world_ai --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server character_visual --manifest-path server-rs/Cargo.toml`。
|
||||
8. 后端改动后使用 `npm run api-server` 重启,并确认 `/healthz`。
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 86 KiB |
@@ -0,0 +1,75 @@
|
||||
export type Hyper3dGenerationMode = 'text-to-model' | 'image-to-model';
|
||||
|
||||
export type Hyper3dTextToModelRequest = {
|
||||
prompt: string;
|
||||
negativePrompt?: string | null;
|
||||
seed?: number | null;
|
||||
geometryFileFormat?: string | null;
|
||||
material?: string | null;
|
||||
quality?: string | null;
|
||||
meshMode?: string | null;
|
||||
addons?: string[];
|
||||
bboxCondition?: number[] | null;
|
||||
previewRender?: boolean | null;
|
||||
};
|
||||
|
||||
export type Hyper3dImageToModelRequest = {
|
||||
imageDataUrls?: string[];
|
||||
imageUrls?: string[];
|
||||
prompt?: string | null;
|
||||
conditionMode?: string | null;
|
||||
seed?: number | null;
|
||||
geometryFileFormat?: string | null;
|
||||
material?: string | null;
|
||||
quality?: string | null;
|
||||
meshMode?: string | null;
|
||||
addons?: string[];
|
||||
bboxCondition?: number[] | null;
|
||||
previewRender?: boolean | null;
|
||||
};
|
||||
|
||||
export type Hyper3dTaskSubmitResponse = {
|
||||
ok: boolean;
|
||||
provider: string;
|
||||
mode: Hyper3dGenerationMode;
|
||||
taskUuid: string;
|
||||
subscriptionKey: string;
|
||||
jobUuids: string[];
|
||||
message?: string | null;
|
||||
tier: string;
|
||||
};
|
||||
|
||||
export type Hyper3dTaskStatusRequest = {
|
||||
subscriptionKey: string;
|
||||
};
|
||||
|
||||
export type Hyper3dJobStatusPayload = {
|
||||
uuid?: string | null;
|
||||
status: string;
|
||||
progress?: number | null;
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
export type Hyper3dTaskStatusResponse = {
|
||||
ok: boolean;
|
||||
provider: string;
|
||||
status: string;
|
||||
jobs: Hyper3dJobStatusPayload[];
|
||||
raw: unknown;
|
||||
};
|
||||
|
||||
export type Hyper3dDownloadRequest = {
|
||||
taskUuid: string;
|
||||
};
|
||||
|
||||
export type Hyper3dDownloadFilePayload = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type Hyper3dDownloadResponse = {
|
||||
ok: boolean;
|
||||
provider: string;
|
||||
files: Hyper3dDownloadFilePayload[];
|
||||
raw: unknown;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export type * from './creativeAgent';
|
||||
export type * from './hyper3d';
|
||||
export type * from './puzzleCreativeTemplate';
|
||||
export type * from './visualNovel';
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from './contracts/common';
|
||||
export type * from './contracts/creationAgentDocumentInput';
|
||||
export type * from './contracts/creativeAgent';
|
||||
export type * from './contracts/customWorldAgent';
|
||||
export type * from './contracts/hyper3d';
|
||||
export * from './contracts/match3dAgent';
|
||||
export * from './contracts/match3dRuntime';
|
||||
export * from './contracts/match3dWorks';
|
||||
|
||||
@@ -195,7 +195,7 @@ is_spacetime_ready() {
|
||||
local server="$1"
|
||||
local output
|
||||
|
||||
if output="$(spacetime server ping "${server}" 2>&1)" &&
|
||||
if output="$(spacetime --root-dir="${SPACETIME_ROOT_DIR}" server ping "${server}" 2>&1)" &&
|
||||
[[ "${output}" == *"Server is online:"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
@@ -345,6 +345,48 @@ prepare_migration_bootstrap_secret() {
|
||||
echo "[dev:rust] 迁移引导密钥: ${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
}
|
||||
|
||||
is_sccache_wrapper_failure_log() {
|
||||
local log_file="$1"
|
||||
|
||||
grep -Eiq \
|
||||
'sccache: error|could not execute process.*sccache|Failed to send data to or receive data from server|Mismatch of client/server versions|Failed to read response header|failed to fill whole buffer' \
|
||||
"${log_file}"
|
||||
}
|
||||
|
||||
run_spacetime_publish_with_sccache_fallback() {
|
||||
local root_dir="$1"
|
||||
shift
|
||||
|
||||
local publish_log
|
||||
publish_log="$(mktemp -t genarrative-spacetime-publish.XXXXXX.log)"
|
||||
|
||||
set +e
|
||||
spacetime --root-dir="${root_dir}" "$@" 2> >(tee "${publish_log}" >&2)
|
||||
local publish_status="$?"
|
||||
set -e
|
||||
|
||||
if [[ "${publish_status}" -eq 0 ]]; then
|
||||
rm -f "${publish_log}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! is_sccache_wrapper_failure_log "${publish_log}"; then
|
||||
rm -f "${publish_log}"
|
||||
return "${publish_status}"
|
||||
fi
|
||||
|
||||
echo "[dev:rust] 检测到 sccache wrapper 通信异常,改用 rustc 直连重试 SpacetimeDB 发布。"
|
||||
echo "[dev:rust] 这只影响本次 publish;项目级 sccache 配置不会被修改。"
|
||||
|
||||
set +e
|
||||
RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= spacetime --root-dir="${root_dir}" "$@"
|
||||
publish_status="$?"
|
||||
set -e
|
||||
|
||||
rm -f "${publish_log}"
|
||||
return "${publish_status}"
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
SERVER_RS_DIR="${REPO_ROOT}/server-rs"
|
||||
@@ -564,6 +606,7 @@ if [[ "${SKIP_SPACETIME}" -ne 1 ]]; then
|
||||
# 当目标端口被占用时,SpacetimeDB 会询问是否使用最近的可用端口;
|
||||
# 这里直接发送回车接受默认建议,再从启动日志解析实际监听端口。
|
||||
printf '\n' | spacetime \
|
||||
--root-dir="${SPACETIME_ROOT_DIR}" \
|
||||
start \
|
||||
--data-dir "${SPACETIME_DATA_DIR}" \
|
||||
--listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}" \
|
||||
@@ -601,7 +644,9 @@ if [[ "${SKIP_PUBLISH}" -ne 1 ]]; then
|
||||
cd "${SERVER_RS_DIR}"
|
||||
# spacetime publish 会在内部调用 Cargo;从 server-rs 目录执行,确保读取
|
||||
# server-rs/.cargo/config.toml 中的 sccache/linker 配置,并复用同一套 target 缓存。
|
||||
spacetime "${PUBLISH_ARGS[@]}"
|
||||
# Windows 本地 sccache server 偶发通信异常时,保留 root-dir/target 缓存路径,
|
||||
# 仅对重试子进程清空 Cargo wrapper,避免可选缓存工具阻断真实构建。
|
||||
run_spacetime_publish_with_sccache_fallback "${SPACETIME_ROOT_DIR}" "${PUBLISH_ARGS[@]}"
|
||||
)
|
||||
fi
|
||||
|
||||
|
||||
+42
-29
@@ -37,14 +37,30 @@ loadEnvFile(resolve(repoRoot, '.env'), fileEnv);
|
||||
loadEnvFile(resolve(repoRoot, '.env.local'), fileEnv);
|
||||
loadEnvFile(resolve(repoRoot, '.env.secrets.local'), fileEnv);
|
||||
|
||||
function buildTargetCandidates() {
|
||||
function resolveConfiguredTarget() {
|
||||
if (fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET) {
|
||||
return fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET;
|
||||
}
|
||||
|
||||
if (fileEnv.RUST_SERVER_TARGET) {
|
||||
return fileEnv.RUST_SERVER_TARGET;
|
||||
}
|
||||
|
||||
if (fileEnv.GENARRATIVE_API_TARGET) {
|
||||
return fileEnv.GENARRATIVE_API_TARGET;
|
||||
}
|
||||
|
||||
if (fileEnv.GENARRATIVE_API_PORT) {
|
||||
return `http://127.0.0.1:${fileEnv.GENARRATIVE_API_PORT}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function buildFallbackCandidates() {
|
||||
const candidates = [
|
||||
fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET,
|
||||
fileEnv.RUST_SERVER_TARGET,
|
||||
fileEnv.GENARRATIVE_API_TARGET,
|
||||
`http://127.0.0.1:${fileEnv.GENARRATIVE_API_PORT || '3100'}`,
|
||||
'http://127.0.0.1:8082',
|
||||
'http://127.0.0.1:3100',
|
||||
'http://127.0.0.1:8082',
|
||||
].filter(Boolean);
|
||||
|
||||
return Array.from(new Set(candidates));
|
||||
@@ -70,39 +86,30 @@ async function isTargetReachable(target) {
|
||||
}
|
||||
|
||||
async function resolveRuntimeTarget() {
|
||||
const candidates = buildTargetCandidates();
|
||||
const reachableTargets = [];
|
||||
const configuredTarget = resolveConfiguredTarget();
|
||||
|
||||
for (const target of candidates) {
|
||||
if (configuredTarget) {
|
||||
return {
|
||||
target: configuredTarget,
|
||||
fallbackUsed: false,
|
||||
targetUnavailable: !(await isTargetReachable(configuredTarget)),
|
||||
};
|
||||
}
|
||||
|
||||
for (const target of buildFallbackCandidates()) {
|
||||
if (await isTargetReachable(target)) {
|
||||
reachableTargets.push(target);
|
||||
if (
|
||||
target === fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET ||
|
||||
target === fileEnv.RUST_SERVER_TARGET ||
|
||||
target === fileEnv.GENARRATIVE_API_TARGET
|
||||
) {
|
||||
return {
|
||||
target,
|
||||
fallbackUsed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reachableTargets.length > 0) {
|
||||
return {
|
||||
target: reachableTargets[0],
|
||||
fallbackUsed: true,
|
||||
targetUnavailable: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
target:
|
||||
fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET ||
|
||||
fileEnv.RUST_SERVER_TARGET ||
|
||||
fileEnv.GENARRATIVE_API_TARGET ||
|
||||
`http://127.0.0.1:${fileEnv.GENARRATIVE_API_PORT || '3100'}`,
|
||||
target: 'http://127.0.0.1:3100',
|
||||
fallbackUsed: false,
|
||||
targetUnavailable: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +120,12 @@ if (runtimeTarget.fallbackUsed) {
|
||||
);
|
||||
}
|
||||
|
||||
if (runtimeTarget.targetUnavailable) {
|
||||
console.warn(
|
||||
`[dev:web] Rust target 当前不可用: ${runtimeTarget.target},请先启动 api-server。`,
|
||||
);
|
||||
}
|
||||
|
||||
const mergedEnv = {
|
||||
...fileEnv,
|
||||
RUST_SERVER_TARGET: runtimeTarget.target,
|
||||
|
||||
@@ -10,7 +10,7 @@ axum = { workspace = true, features = ["ws"] }
|
||||
base64 = { workspace = true }
|
||||
dotenvy = { workspace = true }
|
||||
image = { workspace = true, features = ["jpeg", "png", "webp"] }
|
||||
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["json", "multipart", "rustls-tls"] }
|
||||
webp = { workspace = true }
|
||||
module-ai = { workspace = true }
|
||||
module-assets = { workspace = true, features = ["server-service"] }
|
||||
|
||||
@@ -74,6 +74,10 @@ use crate::{
|
||||
},
|
||||
error_middleware::normalize_error_response,
|
||||
health::health_check,
|
||||
hyper3d_generation::{
|
||||
get_hyper3d_downloads, get_hyper3d_task_status, submit_hyper3d_image_to_model,
|
||||
submit_hyper3d_text_to_model,
|
||||
},
|
||||
llm::proxy_llm_chat_completions,
|
||||
login_options::auth_login_options,
|
||||
logout::logout,
|
||||
@@ -166,6 +170,7 @@ use crate::{
|
||||
|
||||
const PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES: usize = 12 * 1024 * 1024;
|
||||
const PROFILE_FEEDBACK_BODY_LIMIT_BYTES: usize = 6 * 1024 * 1024;
|
||||
const HYPER3D_IMAGE_TO_MODEL_BODY_LIMIT_BYTES: usize = 56 * 1024 * 1024;
|
||||
|
||||
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
|
||||
pub fn build_router(state: AppState) -> Router {
|
||||
@@ -550,6 +555,38 @@ pub fn build_router(state: AppState) -> Router {
|
||||
post(resolve_role_asset_workflow).put(put_role_asset_workflow),
|
||||
)
|
||||
.route("/api/assets/read-url", get(get_asset_read_url))
|
||||
.route(
|
||||
"/api/assets/hyper3d/text-to-model",
|
||||
post(submit_hyper3d_text_to_model).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/assets/hyper3d/image-to-model",
|
||||
post(submit_hyper3d_image_to_model)
|
||||
.layer(DefaultBodyLimit::max(
|
||||
HYPER3D_IMAGE_TO_MODEL_BODY_LIMIT_BYTES,
|
||||
))
|
||||
.route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/assets/hyper3d/status",
|
||||
post(get_hyper3d_task_status).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/assets/hyper3d/download",
|
||||
post(get_hyper3d_downloads).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/assets/history",
|
||||
get(get_asset_history).route_layer(middleware::from_fn_with_state(
|
||||
|
||||
@@ -96,10 +96,13 @@ pub struct AppConfig {
|
||||
pub dashscope_image_request_timeout_ms: u64,
|
||||
pub apimart_base_url: String,
|
||||
pub apimart_api_key: Option<String>,
|
||||
pub apimart_image_request_timeout_ms: u64,
|
||||
pub vector_engine_base_url: String,
|
||||
pub vector_engine_api_key: Option<String>,
|
||||
pub vector_engine_image_request_timeout_ms: u64,
|
||||
pub vector_engine_audio_request_timeout_ms: u64,
|
||||
pub hyper3d_base_url: String,
|
||||
pub hyper3d_api_key: Option<String>,
|
||||
pub hyper3d_model_request_timeout_ms: u64,
|
||||
pub volcengine_speech_api_key: Option<String>,
|
||||
pub volcengine_speech_app_id: Option<String>,
|
||||
pub volcengine_speech_access_key: Option<String>,
|
||||
@@ -203,10 +206,13 @@ impl Default for AppConfig {
|
||||
dashscope_image_request_timeout_ms: 150_000,
|
||||
apimart_base_url: String::new(),
|
||||
apimart_api_key: None,
|
||||
apimart_image_request_timeout_ms: 180_000,
|
||||
vector_engine_base_url: String::new(),
|
||||
vector_engine_api_key: None,
|
||||
vector_engine_image_request_timeout_ms: 180_000,
|
||||
vector_engine_audio_request_timeout_ms: 180_000,
|
||||
hyper3d_base_url: "https://api.hyper3d.com/api/v2".to_string(),
|
||||
hyper3d_api_key: None,
|
||||
hyper3d_model_request_timeout_ms: 180_000,
|
||||
volcengine_speech_api_key: None,
|
||||
volcengine_speech_app_id: None,
|
||||
volcengine_speech_access_key: None,
|
||||
@@ -567,12 +573,6 @@ impl AppConfig {
|
||||
|
||||
config.apimart_api_key = read_first_non_empty_env(&["APIMART_API_KEY"]);
|
||||
|
||||
if let Some(apimart_image_request_timeout_ms) =
|
||||
read_first_positive_u64_env(&["APIMART_IMAGE_REQUEST_TIMEOUT_MS"])
|
||||
{
|
||||
config.apimart_image_request_timeout_ms = apimart_image_request_timeout_ms;
|
||||
}
|
||||
|
||||
if let Some(vector_engine_base_url) = read_first_non_empty_env(&["VECTOR_ENGINE_BASE_URL"])
|
||||
{
|
||||
config.vector_engine_base_url = vector_engine_base_url;
|
||||
@@ -580,12 +580,33 @@ impl AppConfig {
|
||||
|
||||
config.vector_engine_api_key = read_first_non_empty_env(&["VECTOR_ENGINE_API_KEY"]);
|
||||
|
||||
if let Some(vector_engine_image_request_timeout_ms) =
|
||||
read_first_positive_u64_env(&["VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS"])
|
||||
{
|
||||
config.vector_engine_image_request_timeout_ms = vector_engine_image_request_timeout_ms;
|
||||
}
|
||||
|
||||
if let Some(vector_engine_audio_request_timeout_ms) =
|
||||
read_first_positive_u64_env(&["VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS"])
|
||||
{
|
||||
config.vector_engine_audio_request_timeout_ms = vector_engine_audio_request_timeout_ms;
|
||||
}
|
||||
|
||||
if let Some(hyper3d_base_url) =
|
||||
read_first_non_empty_env(&["HYPER3D_BASE_URL", "RODIN_BASE_URL"])
|
||||
{
|
||||
config.hyper3d_base_url = hyper3d_base_url;
|
||||
}
|
||||
|
||||
config.hyper3d_api_key = read_first_non_empty_env(&["HYPER3D_API_KEY", "RODIN_API_KEY"]);
|
||||
|
||||
if let Some(hyper3d_model_request_timeout_ms) = read_first_positive_u64_env(&[
|
||||
"HYPER3D_MODEL_REQUEST_TIMEOUT_MS",
|
||||
"RODIN_MODEL_REQUEST_TIMEOUT_MS",
|
||||
]) {
|
||||
config.hyper3d_model_request_timeout_ms = hyper3d_model_request_timeout_ms;
|
||||
}
|
||||
|
||||
config.volcengine_speech_api_key =
|
||||
read_first_non_empty_env(&["VOLCENGINE_SPEECH_API_KEY", "VOLCENGINE_API_KEY"]);
|
||||
config.volcengine_speech_app_id =
|
||||
@@ -910,6 +931,7 @@ mod tests {
|
||||
assert!(config.apimart_base_url.is_empty());
|
||||
assert!(config.vector_engine_base_url.is_empty());
|
||||
assert!(config.ark_character_video_base_url.is_empty());
|
||||
assert_eq!(config.hyper3d_base_url, "https://api.hyper3d.com/api/v2");
|
||||
assert!(config.ark_character_video_model.is_empty());
|
||||
assert!(config.dashscope_scene_image_model.is_empty());
|
||||
assert!(config.dashscope_reference_image_model.is_empty());
|
||||
@@ -938,6 +960,8 @@ mod tests {
|
||||
std::env::remove_var("GENARRATIVE_LLM_MODEL");
|
||||
std::env::remove_var("APIMART_BASE_URL");
|
||||
std::env::remove_var("VECTOR_ENGINE_BASE_URL");
|
||||
std::env::remove_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS");
|
||||
std::env::remove_var("HYPER3D_BASE_URL");
|
||||
std::env::remove_var("DASHSCOPE_SCENE_IMAGE_MODEL");
|
||||
std::env::remove_var("DASHSCOPE_REFERENCE_IMAGE_MODEL");
|
||||
std::env::remove_var("DASHSCOPE_COVER_IMAGE_MODEL");
|
||||
@@ -949,8 +973,10 @@ mod tests {
|
||||
"https://llm.internal.example/v1",
|
||||
);
|
||||
std::env::set_var("GENARRATIVE_LLM_MODEL", "internal-text-model");
|
||||
std::env::set_var("APIMART_BASE_URL", "https://image.internal.example/v1");
|
||||
std::env::set_var("VECTOR_ENGINE_BASE_URL", "https://audio.internal.example");
|
||||
std::env::set_var("APIMART_BASE_URL", "https://responses.internal.example/v1");
|
||||
std::env::set_var("VECTOR_ENGINE_BASE_URL", "https://vector.internal.example");
|
||||
std::env::set_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS", "210000");
|
||||
std::env::set_var("HYPER3D_BASE_URL", "https://model.internal.example/api/v2");
|
||||
std::env::set_var("DASHSCOPE_SCENE_IMAGE_MODEL", "scene-model");
|
||||
std::env::set_var("DASHSCOPE_REFERENCE_IMAGE_MODEL", "reference-model");
|
||||
std::env::set_var("DASHSCOPE_COVER_IMAGE_MODEL", "cover-model");
|
||||
@@ -965,10 +991,18 @@ mod tests {
|
||||
assert_eq!(config.llm_provider, LlmProvider::OpenAiCompatible);
|
||||
assert_eq!(config.llm_base_url, "https://llm.internal.example/v1");
|
||||
assert_eq!(config.llm_model, "internal-text-model");
|
||||
assert_eq!(config.apimart_base_url, "https://image.internal.example/v1");
|
||||
assert_eq!(
|
||||
config.apimart_base_url,
|
||||
"https://responses.internal.example/v1"
|
||||
);
|
||||
assert_eq!(
|
||||
config.vector_engine_base_url,
|
||||
"https://audio.internal.example"
|
||||
"https://vector.internal.example"
|
||||
);
|
||||
assert_eq!(config.vector_engine_image_request_timeout_ms, 210_000);
|
||||
assert_eq!(
|
||||
config.hyper3d_base_url,
|
||||
"https://model.internal.example/api/v2"
|
||||
);
|
||||
assert_eq!(config.dashscope_scene_image_model, "scene-model");
|
||||
assert_eq!(config.dashscope_reference_image_model, "reference-model");
|
||||
@@ -985,6 +1019,8 @@ mod tests {
|
||||
std::env::remove_var("GENARRATIVE_LLM_MODEL");
|
||||
std::env::remove_var("APIMART_BASE_URL");
|
||||
std::env::remove_var("VECTOR_ENGINE_BASE_URL");
|
||||
std::env::remove_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS");
|
||||
std::env::remove_var("HYPER3D_BASE_URL");
|
||||
std::env::remove_var("DASHSCOPE_SCENE_IMAGE_MODEL");
|
||||
std::env::remove_var("DASHSCOPE_REFERENCE_IMAGE_MODEL");
|
||||
std::env::remove_var("DASHSCOPE_COVER_IMAGE_MODEL");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user