fix: tolerate null legacy custom-world profile
This commit is contained in:
@@ -398,6 +398,14 @@
|
|||||||
- 验证:`npm run test -- src/data/customWorldLibrary.test.ts src/components/CustomWorldResultView.test.tsx`,确认生成后即使父层做一次归一化回写,开局 CG 仍继续显示。
|
- 验证:`npm run test -- src/data/customWorldLibrary.test.ts src/components/CustomWorldResultView.test.tsx`,确认生成后即使父层做一次归一化回写,开局 CG 仍继续显示。
|
||||||
- 关联:`src/data/customWorldLibrary.ts`、`src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx`、`src/components/CustomWorldEntityCatalog.tsx`。
|
- 关联:`src/data/customWorldLibrary.ts`、`src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx`、`src/components/CustomWorldEntityCatalog.tsx`。
|
||||||
|
|
||||||
|
## RPG 发布报 legacy_result_profile_json 非法先查 null 兼容
|
||||||
|
|
||||||
|
- 现象:RPG 结果页发布动作返回 `UPSTREAM_ERROR`,SpacetimeDB details 里是 `custom_world.compile.legacy_result_profile_json 不是合法 JSON object`。
|
||||||
|
- 原因:`publish_world` 前端契约只要求 `{ action: 'publish_world' }`;`ExecuteCustomWorldAgentActionRequest.legacy_result_profile` 是可选字段,经 HTTP / serde / SpacetimeDB payload 传递时可能显式成为 JSON `null`。旧的编译器只接受 object 或缺省,把 `Some("null")` 当成非法 legacy JSON。
|
||||||
|
- 处理:`module-custom-world` 的 optional JSON object 解析要把 `null` 视为未提供,仍拒绝数组、字符串、数字和坏 JSON;正式发布继续以 session `draft_profile_json` 为草稿真相。
|
||||||
|
- 验证:`cargo test -p module-custom-world published_profile_compile --manifest-path server-rs/Cargo.toml`。
|
||||||
|
- 关联:`server-rs/crates/module-custom-world/src/application.rs`、`server-rs/crates/spacetime-module/src/custom_world.rs`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||||
|
|
||||||
## 本地脚本调 VectorEngine 生图卡住先区分 fetch 首部超时
|
## 本地脚本调 VectorEngine 生图卡住先区分 fetch 首部超时
|
||||||
|
|
||||||
- 现象:用 Node `fetch` 直接请求 `POST /v1/images/generations`,已经设置较长的 AbortController 超时,但仍在约 180 到 300 秒后抛 `AbortError`、`TypeError: fetch failed` 或 `UND_ERR_HEADERS_TIMEOUT`;同一 prompt 改用原生 `https.request` 可以在较短时间内成功返回图片。
|
- 现象:用 Node `fetch` 直接请求 `POST /v1/images/generations`,已经设置较长的 AbortController 超时,但仍在约 180 到 300 秒后抛 `AbortError`、`TypeError: fetch failed` 或 `UND_ERR_HEADERS_TIMEOUT`;同一 prompt 改用原生 `https.request` 可以在较短时间内成功返回图片。
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ RPG API 仍沿用历史命名空间:`/api/runtime/custom-world*`、`/api/story
|
|||||||
|
|
||||||
RPG Agent 结果页发布动作的前端契约只保证提交 `{ action: 'publish_world' }`;后端发布时以当前 `custom_world_agent_session.draft_profile_json` 为草稿真相,从 `settingText`、`creatorIntent.rawSettingText`、`creatorIntent.worldHook`、`worldHook`、`anchorContent.worldPromise(.hook)`、`summary`、`name/title` 依次派生正式 `setting_text`,最后才回退 `seed_text`。不要把 `seed_text` 当作唯一设定来源,旧会话可能为空。
|
RPG Agent 结果页发布动作的前端契约只保证提交 `{ action: 'publish_world' }`;后端发布时以当前 `custom_world_agent_session.draft_profile_json` 为草稿真相,从 `settingText`、`creatorIntent.rawSettingText`、`creatorIntent.worldHook`、`worldHook`、`anchorContent.worldPromise(.hook)`、`summary`、`name/title` 依次派生正式 `setting_text`,最后才回退 `seed_text`。不要把 `seed_text` 当作唯一设定来源,旧会话可能为空。
|
||||||
|
|
||||||
|
`legacyResultProfile` 只作为历史结果页 profile 兼容兜底;`publish_world` 请求缺省或显式为 `null` 时等价于未提供,编译正式 profile 时不得因此报 `custom_world.compile.legacy_result_profile_json 不是合法 JSON object`。真正的数组、字符串、数字等非 object legacy 载荷仍应拒绝。
|
||||||
|
|
||||||
RPG 结果页开局 CG 是 `profile.openingCg` 资产槽位:`api-server` 负责 VectorEngine / OSS 副作用并返回故事板和视频引用,前端只把结果写回当前 profile;`sync_result_profile`、作品库保存和 `normalizeCustomWorldProfileRecord` 都必须保留该槽位。若生成成功后画面短暂显示又变回空白,优先检查父层重新同步或 profile 归一化是否把 `openingCg` 丢掉,而不是先怀疑已生成资源本身失效。
|
RPG 结果页开局 CG 是 `profile.openingCg` 资产槽位:`api-server` 负责 VectorEngine / OSS 副作用并返回故事板和视频引用,前端只把结果写回当前 profile;`sync_result_profile`、作品库保存和 `normalizeCustomWorldProfileRecord` 都必须保留该槽位。若生成成功后画面短暂显示又变回空白,优先检查父层重新同步或 profile 归一化是否把 `openingCg` 丢掉,而不是先怀疑已生成资源本身失效。
|
||||||
|
|
||||||
## 拼图
|
## 拼图
|
||||||
|
|||||||
@@ -694,7 +694,13 @@ fn parse_optional_json_object(
|
|||||||
error: CustomWorldFieldError,
|
error: CustomWorldFieldError,
|
||||||
) -> Result<Map<String, Value>, CustomWorldFieldError> {
|
) -> Result<Map<String, Value>, CustomWorldFieldError> {
|
||||||
match normalize_optional_json_slice(value) {
|
match normalize_optional_json_slice(value) {
|
||||||
Some(value) => parse_required_json_object(&value, error),
|
Some(value) => match serde_json::from_str::<Value>(&value) {
|
||||||
|
Ok(Value::Object(object)) => Ok(object),
|
||||||
|
// 中文注释:跨层可选字段经 serde 结构体序列化后可能显式落成 null;
|
||||||
|
// 对 optional JSON object 而言 null 等价于未提供,不能阻断发布链路。
|
||||||
|
Ok(Value::Null) => Ok(Map::new()),
|
||||||
|
_ => Err(error),
|
||||||
|
},
|
||||||
None => Ok(Map::new()),
|
None => Ok(Map::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,6 +1024,49 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn build_test_compile_input(
|
||||||
|
legacy_result_profile_json: Option<String>,
|
||||||
|
) -> CustomWorldPublishedProfileCompileInput {
|
||||||
|
CustomWorldPublishedProfileCompileInput {
|
||||||
|
session_id: "session-1".to_string(),
|
||||||
|
profile_id: "cwprof_001".to_string(),
|
||||||
|
owner_user_id: "user-1".to_string(),
|
||||||
|
draft_profile_json: json!({
|
||||||
|
"name": "潮雾列岛",
|
||||||
|
"summary": "群岛与旧灯塔之间的沉船疑案。",
|
||||||
|
"playableNpcs": [],
|
||||||
|
"storyNpcs": [],
|
||||||
|
"landmarks": []
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
legacy_result_profile_json,
|
||||||
|
setting_text: "海图会在午夜改写群岛航路。".to_string(),
|
||||||
|
author_display_name: "创作者".to_string(),
|
||||||
|
updated_at_micros: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn published_profile_compile_treats_null_legacy_result_profile_as_absent() {
|
||||||
|
let snapshot = build_custom_world_published_profile_compile_snapshot(
|
||||||
|
build_test_compile_input(Some("null".to_string())),
|
||||||
|
)
|
||||||
|
.expect("null legacy result profile should be treated as absent");
|
||||||
|
|
||||||
|
assert_eq!(snapshot.profile_id, "cwprof_001");
|
||||||
|
assert_eq!(snapshot.world_name, "潮雾列岛");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn published_profile_compile_rejects_non_object_legacy_result_profile() {
|
||||||
|
let error = build_custom_world_published_profile_compile_snapshot(
|
||||||
|
build_test_compile_input(Some("[]".to_string())),
|
||||||
|
)
|
||||||
|
.expect_err("array legacy result profile should still be invalid");
|
||||||
|
|
||||||
|
assert_eq!(error, CustomWorldFieldError::InvalidLegacyResultProfileJson);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn publish_setting_text_falls_back_to_draft_profile_when_seed_is_empty() {
|
fn publish_setting_text_falls_back_to_draft_profile_when_seed_is_empty() {
|
||||||
let payload = Map::new();
|
let payload = Map::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user