修复动作提示缓存缺失降级

角色资产工作流解析在 OSS 缓存未配置时返回默认提示词

补充无 OSS 缓存的 workflow 路由回归测试

同步后端契约文档说明
This commit is contained in:
2026-06-27 17:24:05 +08:00
parent 228d31db31
commit 8324911bbb
3 changed files with 54 additions and 3 deletions
@@ -60,7 +60,7 @@ npm run check:server-rs-ddd
- 平台基础能力:`/api/llm/*``/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。
- 资产基础能力:`/api/assets/direct-upload-tickets``/api/assets/sts-upload-credentials``/api/assets/objects/*``/api/assets/read-*`,负责直传、确认、绑定和读取。
- 外部 OpenAPI`/api/external/v1/openapi.json``/api/external/v1/assets/direct-upload-tickets``/api/external/v1/assets/objects/confirm``/api/external/v1/assets/read-url``/api/external/v1/editor/*`,使用 Bearer API Key 鉴权;API Key 管理仍在登录态 `/api/profile/api-keys`,不进入外部 OpenAPI JSON。
- 创作 / 游玩支撑能力:`/api/creation-entry/config``/api/ai/tasks*``/api/runtime/chat/*``/api/runtime/settings``/api/runtime/save/snapshot``/api/profile/browse-history``/api/profile/save-archives*``/api/profile/play-stats``/api/assets/history``/api/assets/character-visual/*``/api/assets/character-animation/*``/api/assets/character-workflow-cache*``/api/assets/hyper3d/*``/api/runtime/custom-world/asset-studio/*``/api/editor/projects*`
- 创作 / 游玩支撑能力:`/api/creation-entry/config``/api/ai/tasks*``/api/runtime/chat/*``/api/runtime/settings``/api/runtime/save/snapshot``/api/profile/browse-history``/api/profile/save-archives*``/api/profile/play-stats``/api/assets/history``/api/assets/character-visual/*``/api/assets/character-animation/*``/api/assets/character-workflow-cache*``/api/assets/hyper3d/*``/api/runtime/custom-world/asset-studio/*``/api/editor/projects*``/api/runtime/custom-world/asset-studio/*` 解析默认角色形象 / 动作提示词时可以在 OSS 缓存不可用或未配置时按无缓存返回默认提示;保存 workflow 缓存和真实素材读写仍必须要求 OSS 正常可用。
- 后台入口配置:`/admin/api/creation-entry/config``/admin/api/creation-entry/config/banners``/admin/api/creation-entry/config/interactions`
- 自定义世界 / RPG`/api/runtime/custom-world*``/api/story/*``/api/runtime/chat/*`
- 拼图:`/api/runtime/puzzle/*`
+39
View File
@@ -690,6 +690,45 @@ mod tests {
assert_eq!(body["error"]["details"]["creationTypeId"], "rpg");
}
#[tokio::test]
async fn role_asset_workflow_returns_default_prompts_without_oss_cache() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/runtime/custom-world/asset-studio/role/hero-01/workflow")
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"role": {
"id": "hero-01",
"name": "沈砺",
"title": "灰炬向导",
"role": "边路同行者",
"actionDescription": "持灯快步穿过灰烬街巷"
}
})
.to_string(),
))
.expect("workflow request should build"),
)
.await
.expect("workflow request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = read_json_response(response).await;
assert_eq!(body["ok"], Value::Bool(true));
assert_eq!(body["cache"], Value::Null);
assert!(
body["workflow"]["animationPromptText"]
.as_str()
.unwrap_or_default()
.contains("持灯快步穿过灰烬街巷")
);
}
#[tokio::test]
async fn healthz_returns_standard_envelope_when_requested() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
@@ -1124,9 +1124,13 @@ pub async fn resolve_role_asset_workflow(
})?;
let cache_scope_id = trim_optional_text(payload.cache_scope_id.as_deref());
let cache = load_workflow_cache(&state, character_id.as_str(), cache_scope_id.as_deref())
let cache = match load_workflow_cache(&state, character_id.as_str(), cache_scope_id.as_deref())
.await
.map_err(|error| character_animation_error_response(&request_context, error))?;
{
Ok(cache) => cache,
Err(error) if is_missing_oss_config_error(&error) => None,
Err(error) => return Err(character_animation_error_response(&request_context, error)),
};
let workflow = build_role_asset_workflow(payload.role, cache.as_ref());
Ok(json_success_body(
@@ -4133,6 +4137,14 @@ fn require_oss_client(state: &AppState) -> Result<&platform_oss::OssClient, AppE
})
}
fn is_missing_oss_config_error(error: &AppError) -> bool {
error.status_code() == StatusCode::SERVICE_UNAVAILABLE
&& error.details().is_some_and(|details| {
details.get("provider").and_then(Value::as_str) == Some("aliyun-oss")
&& details.get("reason").and_then(Value::as_str) == Some("OSS 未完成环境变量配置")
})
}
fn normalize_required_text(value: &str, fallback: &str) -> String {
let normalized = value
.trim()