From 13547091cabdb740dd5a7c9f489fb070abe24bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=86=E5=86=B0=E9=83=81-hermes=E7=89=88?= Date: Thu, 7 May 2026 15:59:00 +0800 Subject: [PATCH] =?UTF-8?q?chore(api-server):=20=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E4=B8=8E=E7=BD=91=E5=85=B3=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/env/api-server.env.example | 21 +++- ..._EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md | 86 ++++++++++++++ docs/technical/README.md | 1 + server-rs/crates/api-server/src/config.rs | 107 ++++++++++++++++-- .../crates/api-server/src/custom_world_ai.rs | 77 +++++++------ .../crates/api-server/src/square_hole.rs | 4 +- 6 files changed, 243 insertions(+), 53 deletions(-) create mode 100644 docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md diff --git a/deploy/env/api-server.env.example b/deploy/env/api-server.env.example index bd69bd54..bd5af9d9 100644 --- a/deploy/env/api-server.env.example +++ b/deploy/env/api-server.env.example @@ -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 diff --git a/docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md b/docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md new file mode 100644 index 00000000..a89274c3 --- /dev/null +++ b/docs/technical/API_SERVER_EXTERNAL_SERVICE_ENV_CONFIG_2026-05-07.md @@ -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`,不得提交到仓库。 diff --git a/docs/technical/README.md b/docs/technical/README.md index bfc5118f..ad0f19f2 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -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_*` 和相关测试/文档口径。 diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index 2ca5ffea..12bc685e 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -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 { #[cfg(test)] mod tests { - use super::AppConfig; + use super::{AppConfig, LlmProvider}; use std::sync::{Mutex, OnceLock}; static ENV_LOCK: OnceLock> = 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 diff --git a/server-rs/crates/api-server/src/custom_world_ai.rs b/server-rs/crates/api-server/src/custom_world_ai.rs index d3ef6ccd..9f4f14bf 100644 --- a/server-rs/crates/api-server/src/custom_world_ai.rs +++ b/server-rs/crates/api-server/src/custom_world_ai.rs @@ -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 Result Result 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(), } } diff --git a/server-rs/crates/api-server/src/square_hole.rs b/server-rs/crates/api-server/src/square_hole.rs index ce6a9512..3a6e1b1e 100644 --- a/server-rs/crates/api-server/src/square_hole.rs +++ b/server-rs/crates/api-server/src/square_hole.rs @@ -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;