master #14
21
deploy/env/api-server.env.example
vendored
21
deploy/env/api-server.env.example
vendored
@@ -35,14 +35,25 @@ GENARRATIVE_LLM_MODEL=
|
||||
GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=false
|
||||
GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=false
|
||||
|
||||
APIMART_BASE_URL=
|
||||
APIMART_API_KEY=
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
ARK_CHARACTER_VIDEO_BASE_URL=
|
||||
ARK_CHARACTER_VIDEO_API_KEY=
|
||||
ARK_CHARACTER_VIDEO_MODEL=
|
||||
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
|
||||
|
||||
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||
DASHSCOPE_API_KEY=
|
||||
DASHSCOPE_IMAGE_MODEL=wan2.7-image
|
||||
DASHSCOPE_SCENE_IMAGE_MODEL=
|
||||
DASHSCOPE_REFERENCE_IMAGE_MODEL=
|
||||
DASHSCOPE_COVER_IMAGE_MODEL=
|
||||
DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS=150000
|
||||
DASHSCOPE_CHARACTER_VISUAL_MODEL=wan2.7-image-pro
|
||||
DASHSCOPE_CHARACTER_IMAGE_SEQUENCE_MODEL=wan2.7-image-pro
|
||||
DASHSCOPE_CHARACTER_REFERENCE_VIDEO_MODEL=wan2.7-r2v
|
||||
DASHSCOPE_CHARACTER_MOTION_TRANSFER_MODEL=wan2.2-animate-move
|
||||
DASHSCOPE_CHARACTER_VISUAL_MODEL=
|
||||
DASHSCOPE_CHARACTER_IMAGE_SEQUENCE_MODEL=
|
||||
DASHSCOPE_CHARACTER_REFERENCE_VIDEO_MODEL=
|
||||
DASHSCOPE_CHARACTER_MOTION_TRANSFER_MODEL=
|
||||
DASHSCOPE_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
|
||||
|
||||
SMS_AUTH_ENABLED=false
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# api-server 外部服务环境变量配置 2026-05-07
|
||||
|
||||
## 背景
|
||||
|
||||
`server-rs/crates/api-server/src/config.rs` 统一收口 api-server 启动配置。外部服务分为两类:
|
||||
|
||||
1. 公共服务:阿里云、腾讯云、微信等对外公开且接口域名稳定的服务。
|
||||
2. 非公共服务:团队自选模型网关、图片网关、视频模型、内部兼容服务等,URL 与模型名可能随部署、供应商或账号策略变化。
|
||||
|
||||
本次约定:公共服务 URL 可以保留代码默认值;非公共服务的 URL 与模型名必须通过环境变量提供,不再在 `config.rs` 写死具体模型名称或私有网关地址。
|
||||
|
||||
## 公共服务默认值
|
||||
|
||||
以下默认值属于公共服务稳定接口,可继续保留在代码或示例环境中:
|
||||
|
||||
```text
|
||||
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||
ALIYUN_SMS_ENDPOINT=dypnsapi.aliyuncs.com
|
||||
WECHAT_AUTHORIZE_ENDPOINT=https://open.weixin.qq.com/connect/qrconnect
|
||||
WECHAT_ACCESS_TOKEN_ENDPOINT=https://api.weixin.qq.com/sns/oauth2/access_token
|
||||
WECHAT_USER_INFO_ENDPOINT=https://api.weixin.qq.com/sns/userinfo
|
||||
```
|
||||
|
||||
说明:DashScope 属于阿里云公开服务,基础 URL 可保留;具体图片模型名不属于稳定公共接口,必须由环境变量配置。
|
||||
|
||||
## 非公共服务必配项
|
||||
|
||||
生产环境或真实联调使用到对应能力时,应显式配置以下变量:
|
||||
|
||||
```text
|
||||
# 文本 LLM 网关
|
||||
GENARRATIVE_LLM_PROVIDER=openai-compatible
|
||||
GENARRATIVE_LLM_BASE_URL=
|
||||
GENARRATIVE_LLM_API_KEY=
|
||||
GENARRATIVE_LLM_MODEL=
|
||||
|
||||
# APIMart / OpenAI 兼容图片网关
|
||||
APIMART_BASE_URL=
|
||||
APIMART_API_KEY=
|
||||
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
|
||||
|
||||
# DashScope 图片模型名
|
||||
DASHSCOPE_SCENE_IMAGE_MODEL=
|
||||
DASHSCOPE_REFERENCE_IMAGE_MODEL=
|
||||
DASHSCOPE_COVER_IMAGE_MODEL=
|
||||
|
||||
# Ark / 角色视频模型网关
|
||||
ARK_CHARACTER_VIDEO_BASE_URL=
|
||||
ARK_CHARACTER_VIDEO_API_KEY=
|
||||
ARK_CHARACTER_VIDEO_MODEL=
|
||||
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
|
||||
```
|
||||
|
||||
## 兼容变量
|
||||
|
||||
为降低部署切换成本,当前代码仍兼容部分历史变量:
|
||||
|
||||
```text
|
||||
GENARRATIVE_LLM_BASE_URL / LLM_BASE_URL
|
||||
GENARRATIVE_LLM_MODEL / LLM_MODEL / VITE_LLM_MODEL
|
||||
GENARRATIVE_LLM_API_KEY / LLM_API_KEY / ARK_API_KEY
|
||||
DASHSCOPE_SCENE_IMAGE_MODEL / DASHSCOPE_IMAGE_MODEL
|
||||
DASHSCOPE_REFERENCE_IMAGE_MODEL / DASHSCOPE_IMAGE_EDIT_MODEL
|
||||
DASHSCOPE_COVER_IMAGE_MODEL / DASHSCOPE_IMAGE_MODEL
|
||||
ARK_CHARACTER_VIDEO_BASE_URL / ARK_BASE_URL / GENARRATIVE_LLM_BASE_URL / LLM_BASE_URL
|
||||
ARK_CHARACTER_VIDEO_API_KEY / ARK_API_KEY / GENARRATIVE_LLM_API_KEY / LLM_API_KEY
|
||||
ARK_CHARACTER_VIDEO_MODEL / DASHSCOPE_CHARACTER_VIDEO_MODEL
|
||||
```
|
||||
|
||||
## 运行时行为
|
||||
|
||||
1. `AppConfig::default()` 不再包含具体非公共模型名或私有网关 URL。
|
||||
2. `AppConfig::from_env()` 会从环境变量读取非公共模型名和 URL。
|
||||
3. 文本 LLM provider 为 `ark` 且未配置 `GENARRATIVE_LLM_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
|
||||
4. 角色视频 provider 复用 Ark 且未配置 `ARK_CHARACTER_VIDEO_BASE_URL` 时,仍回退到 Ark 公开基础 URL。
|
||||
5. 具体模型名缺失时不在配置层伪造默认模型,调用到对应能力时由下游配置校验返回缺配置错误。
|
||||
|
||||
## 示例文件
|
||||
|
||||
生产示例环境变量维护在:
|
||||
|
||||
```text
|
||||
deploy/env/api-server.env.example
|
||||
```
|
||||
|
||||
真实密钥、内部网关 URL 和具体模型名只应写入服务器 `/etc/genarrative/api-server.env` 或本地未提交的 `.env.local` / `.env.secrets.local`,不得提交到仓库。
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
- [API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md](./API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md):冻结 api-server 外部服务配置边界,公共服务 URL 可保留代码默认值,非公共模型名和私有网关 URL 统一通过环境变量注入。
|
||||
- [PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md](./PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md):冻结个人任务与埋点系统首版方案,明确 `tracking_event`、`tracking_daily_stat`、`profile_task_config`、任务进度、领奖记录和光点钱包流水的边界。
|
||||
- [SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md](./SQUARE_HOLE_IMAGE_SLOT_AND_RUNTIME_INTERACTION_FIX_2026-05-06.md):记录方洞挑战结果页图片槽位局部生成、洞口图历史素材、运行态拖拽与点击投放交互的修正口径。
|
||||
- [MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md](./MAINCLOUD_REFERENCE_REMOVAL_POLICY_2026-05-06.md):冻结 Maincloud 历史残留引用禁用策略,明确后续不得新增、运行或引用 `api-server:maincloud`、`GENARRATIVE_SPACETIME_MAINCLOUD_*` 和相关测试/文档口径。
|
||||
|
||||
@@ -5,7 +5,6 @@ use platform_llm::{
|
||||
DEFAULT_RETRY_BACKOFF_MS, LlmProvider,
|
||||
};
|
||||
|
||||
const DEFAULT_LLM_MODEL: &str = "doubao-1-5-pro-32k-character-250715";
|
||||
const DEFAULT_INTERNAL_API_SECRET: &str = "genarrative-dev-internal-bridge";
|
||||
const DEFAULT_AUTH_STORE_PATH: &str = "server-rs/.data/auth-store.json";
|
||||
const SPACETIME_LOCAL_CONFIG_FILE: &str = "spacetime.local.json";
|
||||
@@ -171,9 +170,9 @@ impl Default for AppConfig {
|
||||
spacetime_pool_size: 4,
|
||||
spacetime_procedure_timeout: Duration::from_secs(30),
|
||||
llm_provider: LlmProvider::Ark,
|
||||
llm_base_url: DEFAULT_ARK_BASE_URL.to_string(),
|
||||
llm_base_url: String::new(),
|
||||
llm_api_key: None,
|
||||
llm_model: DEFAULT_LLM_MODEL.to_string(),
|
||||
llm_model: String::new(),
|
||||
llm_request_timeout_ms: DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
llm_max_retries: DEFAULT_MAX_RETRIES,
|
||||
llm_retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS,
|
||||
@@ -181,18 +180,18 @@ impl Default for AppConfig {
|
||||
creation_agent_llm_web_search_enabled: true,
|
||||
dashscope_base_url: "https://dashscope.aliyuncs.com/api/v1".to_string(),
|
||||
dashscope_api_key: None,
|
||||
dashscope_scene_image_model: "wan2.2-t2i-flash".to_string(),
|
||||
dashscope_reference_image_model: "qwen-image-2.0".to_string(),
|
||||
dashscope_cover_image_model: "wan2.2-t2i-flash".to_string(),
|
||||
dashscope_scene_image_model: String::new(),
|
||||
dashscope_reference_image_model: String::new(),
|
||||
dashscope_cover_image_model: String::new(),
|
||||
dashscope_image_request_timeout_ms: 150_000,
|
||||
apimart_base_url: "https://api.apimart.ai/v1".to_string(),
|
||||
apimart_base_url: String::new(),
|
||||
apimart_api_key: None,
|
||||
apimart_image_request_timeout_ms: 180_000,
|
||||
draft_asset_generation_max_concurrent_requests: 4,
|
||||
ark_character_video_base_url: DEFAULT_ARK_BASE_URL.to_string(),
|
||||
ark_character_video_base_url: String::new(),
|
||||
ark_character_video_api_key: None,
|
||||
ark_character_video_request_timeout_ms: 420_000,
|
||||
ark_character_video_model: "doubao-seedance-2-0-fast-260128".to_string(),
|
||||
ark_character_video_model: String::new(),
|
||||
character_animation_ffmpeg_path: "ffmpeg".to_string(),
|
||||
character_animation_ffprobe_path: "ffprobe".to_string(),
|
||||
character_animation_frame_extract_timeout_ms: 120_000,
|
||||
@@ -456,6 +455,8 @@ impl AppConfig {
|
||||
read_first_non_empty_env(&["GENARRATIVE_LLM_BASE_URL", "LLM_BASE_URL"])
|
||||
{
|
||||
config.llm_base_url = llm_base_url;
|
||||
} else if config.llm_provider == LlmProvider::Ark {
|
||||
config.llm_base_url = DEFAULT_ARK_BASE_URL.to_string();
|
||||
}
|
||||
|
||||
config.llm_api_key =
|
||||
@@ -557,6 +558,8 @@ impl AppConfig {
|
||||
"LLM_BASE_URL",
|
||||
]) {
|
||||
config.ark_character_video_base_url = ark_character_video_base_url;
|
||||
} else if config.llm_provider == LlmProvider::Ark {
|
||||
config.ark_character_video_base_url = DEFAULT_ARK_BASE_URL.to_string();
|
||||
}
|
||||
|
||||
config.ark_character_video_api_key = read_first_non_empty_env(&[
|
||||
@@ -816,11 +819,95 @@ fn parse_positive_u16(raw: &str) -> Option<u16> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AppConfig;
|
||||
use super::{AppConfig, LlmProvider};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
|
||||
#[test]
|
||||
fn default_keeps_non_public_model_and_base_url_empty() {
|
||||
let config = AppConfig::default();
|
||||
|
||||
assert!(config.llm_model.is_empty());
|
||||
assert!(config.llm_base_url.is_empty());
|
||||
assert!(config.apimart_base_url.is_empty());
|
||||
assert!(config.ark_character_video_base_url.is_empty());
|
||||
assert!(config.ark_character_video_model.is_empty());
|
||||
assert!(config.dashscope_scene_image_model.is_empty());
|
||||
assert!(config.dashscope_reference_image_model.is_empty());
|
||||
assert!(config.dashscope_cover_image_model.is_empty());
|
||||
assert_eq!(
|
||||
config.dashscope_base_url,
|
||||
"https://dashscope.aliyuncs.com/api/v1"
|
||||
);
|
||||
assert_eq!(config.sms_endpoint, "dypnsapi.aliyuncs.com");
|
||||
assert_eq!(
|
||||
config.wechat_authorize_endpoint,
|
||||
"https://open.weixin.qq.com/connect/qrconnect"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_non_public_models_and_urls() {
|
||||
let _guard = ENV_LOCK
|
||||
.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.expect("env lock should not poison");
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("GENARRATIVE_LLM_PROVIDER");
|
||||
std::env::remove_var("GENARRATIVE_LLM_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_LLM_MODEL");
|
||||
std::env::remove_var("APIMART_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");
|
||||
std::env::remove_var("ARK_CHARACTER_VIDEO_BASE_URL");
|
||||
std::env::remove_var("ARK_CHARACTER_VIDEO_MODEL");
|
||||
std::env::set_var("GENARRATIVE_LLM_PROVIDER", "openai-compatible");
|
||||
std::env::set_var(
|
||||
"GENARRATIVE_LLM_BASE_URL",
|
||||
"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("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");
|
||||
std::env::set_var(
|
||||
"ARK_CHARACTER_VIDEO_BASE_URL",
|
||||
"https://video.internal.example/v1",
|
||||
);
|
||||
std::env::set_var("ARK_CHARACTER_VIDEO_MODEL", "video-model");
|
||||
}
|
||||
|
||||
let config = AppConfig::from_env();
|
||||
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.dashscope_scene_image_model, "scene-model");
|
||||
assert_eq!(config.dashscope_reference_image_model, "reference-model");
|
||||
assert_eq!(config.dashscope_cover_image_model, "cover-model");
|
||||
assert_eq!(
|
||||
config.ark_character_video_base_url,
|
||||
"https://video.internal.example/v1"
|
||||
);
|
||||
assert_eq!(config.ark_character_video_model, "video-model");
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("GENARRATIVE_LLM_PROVIDER");
|
||||
std::env::remove_var("GENARRATIVE_LLM_BASE_URL");
|
||||
std::env::remove_var("GENARRATIVE_LLM_MODEL");
|
||||
std::env::remove_var("APIMART_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");
|
||||
std::env::remove_var("ARK_CHARACTER_VIDEO_BASE_URL");
|
||||
std::env::remove_var("ARK_CHARACTER_VIDEO_MODEL");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_spacetime_pool_size() {
|
||||
let _guard = ENV_LOCK
|
||||
|
||||
@@ -28,9 +28,7 @@ use webp::Encoder as WebpEncoder;
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body,
|
||||
asset_billing::{
|
||||
execute_billable_asset_operation, execute_billable_asset_operation_with_cost,
|
||||
},
|
||||
asset_billing::{execute_billable_asset_operation, execute_billable_asset_operation_with_cost},
|
||||
auth::AuthenticatedAccessToken,
|
||||
custom_world_result_prompts::{
|
||||
build_result_entity_system_prompt, build_result_entity_user_prompt,
|
||||
@@ -1390,7 +1388,9 @@ async fn create_ark_storyboard_to_video_task(
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| map_ark_video_request_error(format!("请求 Seedance 视频服务失败:{error}")))?;
|
||||
.map_err(|error| {
|
||||
map_ark_video_request_error(format!("请求 Seedance 视频服务失败:{error}"))
|
||||
})?;
|
||||
let status = response.status();
|
||||
let text = response.text().await.map_err(|error| {
|
||||
map_ark_video_request_error(format!("读取 Seedance 视频任务响应失败:{error}"))
|
||||
@@ -1428,7 +1428,9 @@ async fn wait_for_ark_content_generation_task(
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| map_ark_video_request_error(format!("查询 Seedance 视频任务失败:{error}")))?;
|
||||
.map_err(|error| {
|
||||
map_ark_video_request_error(format!("查询 Seedance 视频任务失败:{error}"))
|
||||
})?;
|
||||
let status = response.status();
|
||||
let text = response.text().await.map_err(|error| {
|
||||
map_ark_video_request_error(format!("读取 Seedance 视频任务响应失败:{error}"))
|
||||
@@ -1447,11 +1449,13 @@ async fn wait_for_ark_content_generation_task(
|
||||
extract_generation_task_status(&payload.payload).as_str(),
|
||||
);
|
||||
if is_completed_generation_task_status(normalized_status.as_str()) {
|
||||
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "ark",
|
||||
"message": "开局 CG 视频任务完成但没有返回 video_url。",
|
||||
"taskId": task_id,
|
||||
})));
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "ark",
|
||||
"message": "开局 CG 视频任务完成但没有返回 video_url。",
|
||||
"taskId": task_id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
if is_failed_generation_task_status(normalized_status.as_str()) {
|
||||
return Err(parse_ark_video_upstream_error(
|
||||
@@ -1463,11 +1467,13 @@ async fn wait_for_ark_content_generation_task(
|
||||
sleep(Duration::from_millis(ARK_VIDEO_TASK_POLL_INTERVAL_MS)).await;
|
||||
}
|
||||
|
||||
Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "ark",
|
||||
"message": "开局 CG 视频生成超时,请稍后重试。",
|
||||
"taskId": task_id,
|
||||
})))
|
||||
Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "ark",
|
||||
"message": "开局 CG 视频生成超时,请稍后重试。",
|
||||
"taskId": task_id,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
async fn download_generated_video(
|
||||
@@ -1492,11 +1498,13 @@ async fn download_generated_video(
|
||||
.await
|
||||
.map_err(|error| map_ark_video_request_error(format!("{fallback_message}:{error}")))?;
|
||||
if !status.is_success() {
|
||||
return Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "ark",
|
||||
"message": fallback_message,
|
||||
"status": status.as_u16(),
|
||||
})));
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "ark",
|
||||
"message": fallback_message,
|
||||
"status": status.as_u16(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
let normalized_mime_type = normalize_downloaded_video_mime_type(content_type.as_str());
|
||||
|
||||
@@ -1767,12 +1775,10 @@ fn normalize_opening_cg_request(profile: &Value) -> Result<NormalizedOpeningCgRe
|
||||
})?;
|
||||
let world_name = read_string_field(object, "name").unwrap_or_else(|| "未命名世界".to_string());
|
||||
let profile_id = read_string_field(object, "id");
|
||||
let world_tone = read_string_field(object, "tone").ok_or_else(|| {
|
||||
missing_opening_cg_field_error("世界基调缺失,无法生成开局 CG。")
|
||||
})?;
|
||||
let world_summary = read_string_field(object, "summary").ok_or_else(|| {
|
||||
missing_opening_cg_field_error("世界概述缺失,无法生成开局 CG。")
|
||||
})?;
|
||||
let world_tone = read_string_field(object, "tone")
|
||||
.ok_or_else(|| missing_opening_cg_field_error("世界基调缺失,无法生成开局 CG。"))?;
|
||||
let world_summary = read_string_field(object, "summary")
|
||||
.ok_or_else(|| missing_opening_cg_field_error("世界概述缺失,无法生成开局 CG。"))?;
|
||||
let core_conflicts = read_string_array_field(object, "coreConflicts");
|
||||
if core_conflicts.is_empty() {
|
||||
return Err(missing_opening_cg_field_error(
|
||||
@@ -1785,9 +1791,8 @@ fn normalize_opening_cg_request(profile: &Value) -> Result<NormalizedOpeningCgRe
|
||||
.and_then(|roles| roles.first())
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| missing_opening_cg_field_error("缺少玩家扮演角色。"))?;
|
||||
let player_role_image_src = read_string_field(player_role, "imageSrc").ok_or_else(|| {
|
||||
missing_opening_cg_field_error("玩家扮演角色缺少角色参考图。")
|
||||
})?;
|
||||
let player_role_image_src = read_string_field(player_role, "imageSrc")
|
||||
.ok_or_else(|| missing_opening_cg_field_error("玩家扮演角色缺少角色参考图。"))?;
|
||||
let player_role_brief = build_opening_cg_player_role_brief(player_role);
|
||||
let opening_scene_image_src = profile
|
||||
.pointer("/sceneChapterBlueprints/0/acts/0/backgroundImageSrc")
|
||||
@@ -1887,10 +1892,12 @@ fn require_ark_video_settings(state: &AppState) -> Result<ArkVideoSettings, AppE
|
||||
.trim()
|
||||
.trim_end_matches('/');
|
||||
if base_url.is_empty() {
|
||||
return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||||
"provider": "ark",
|
||||
"reason": "ARK_CHARACTER_VIDEO_BASE_URL 未配置",
|
||||
})));
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||||
"provider": "ark",
|
||||
"reason": "ARK_CHARACTER_VIDEO_BASE_URL 未配置",
|
||||
})),
|
||||
);
|
||||
}
|
||||
let api_key = state
|
||||
.config
|
||||
@@ -3056,9 +3063,7 @@ fn normalize_downloaded_video_mime_type(content_type: &str) -> String {
|
||||
.map(str::trim)
|
||||
.unwrap_or("video/mp4");
|
||||
match mime_type {
|
||||
"video/mp4" | "video/quicktime" | "video/webm" | "video/x-msvideo" => {
|
||||
mime_type.to_string()
|
||||
}
|
||||
"video/mp4" | "video/quicktime" | "video/webm" | "video/x-msvideo" => mime_type.to_string(),
|
||||
_ => "video/mp4".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ use shared_contracts::{
|
||||
PutSquareHoleWorkRequest, RegenerateSquareHoleWorkImageRequest,
|
||||
SquareHoleHoleOptionResponse as SquareHoleWorkHoleOptionResponse,
|
||||
SquareHoleShapeOptionResponse as SquareHoleWorkShapeOptionResponse,
|
||||
SquareHoleWorkDetailResponse, SquareHoleWorkMutationResponse, SquareHoleWorkProfileResponse,
|
||||
SquareHoleWorkSummaryResponse, SquareHoleWorksResponse,
|
||||
SquareHoleWorkDetailResponse, SquareHoleWorkMutationResponse,
|
||||
SquareHoleWorkProfileResponse, SquareHoleWorkSummaryResponse, SquareHoleWorksResponse,
|
||||
},
|
||||
};
|
||||
use shared_kernel::build_prefixed_uuid_id;
|
||||
|
||||
Reference in New Issue
Block a user