1
This commit is contained in:
@@ -23,6 +23,8 @@ mod creation_agent_anchor_templates;
|
||||
mod creation_agent_chat;
|
||||
mod creation_agent_document_input;
|
||||
mod creation_agent_llm_turn;
|
||||
mod creative_agent;
|
||||
mod creative_agent_sse;
|
||||
mod custom_world;
|
||||
mod custom_world_agent_entities;
|
||||
mod custom_world_agent_turn;
|
||||
@@ -66,11 +68,13 @@ mod square_hole_agent_turn;
|
||||
mod state;
|
||||
mod story_battles;
|
||||
mod story_sessions;
|
||||
mod visual_novel;
|
||||
mod wechat_auth;
|
||||
mod wechat_provider;
|
||||
mod work_author;
|
||||
|
||||
use shared_logging::init_tracing;
|
||||
use std::{collections::HashSet, env, fs, io, panic, thread};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::runtime::Builder as TokioRuntimeBuilder;
|
||||
use tracing::info;
|
||||
@@ -79,30 +83,30 @@ use crate::{app::build_router, config::AppConfig, state::AppState};
|
||||
|
||||
const API_SERVER_STARTUP_STACK_SIZE_BYTES: usize = 32 * 1024 * 1024;
|
||||
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
fn main() -> Result<(), io::Error> {
|
||||
// Windows 本地调试下 Axum 路由树和启动恢复链较重,显式放大启动线程栈,避免 debug 构建在进入监听前栈溢出。
|
||||
std::thread::Builder::new()
|
||||
let server_thread = thread::Builder::new()
|
||||
.name("api-server-bootstrap".to_string())
|
||||
.stack_size(API_SERVER_STARTUP_STACK_SIZE_BYTES)
|
||||
.spawn(run_api_server_with_runtime)?
|
||||
.join()
|
||||
.map_err(|_| std::io::Error::other("api-server 启动线程异常退出"))?
|
||||
.spawn(|| {
|
||||
TokioRuntimeBuilder::new_multi_thread()
|
||||
.enable_all()
|
||||
.thread_name("api-server-worker")
|
||||
.thread_stack_size(API_SERVER_STARTUP_STACK_SIZE_BYTES)
|
||||
.build()?
|
||||
.block_on(run_server())
|
||||
})?;
|
||||
|
||||
match server_thread.join() {
|
||||
Ok(result) => result,
|
||||
Err(payload) => panic::resume_unwind(payload),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_api_server_with_runtime() -> Result<(), std::io::Error> {
|
||||
TokioRuntimeBuilder::new_multi_thread()
|
||||
.enable_all()
|
||||
.thread_name("api-server-worker")
|
||||
.thread_stack_size(API_SERVER_STARTUP_STACK_SIZE_BYTES)
|
||||
.build()?
|
||||
.block_on(run_api_server())
|
||||
}
|
||||
|
||||
async fn run_api_server() -> Result<(), std::io::Error> {
|
||||
// 运行本地开发与联调时,优先从仓库根目录加载本地变量,避免手工逐项导出 OSS / APIMart 配置。
|
||||
let _ = dotenvy::from_filename(".env");
|
||||
let _ = dotenvy::from_filename(".env.local");
|
||||
let _ = dotenvy::from_filename(".env.secrets.local");
|
||||
async fn run_server() -> Result<(), io::Error> {
|
||||
// 运行本地开发与联调时,优先从仓库根目录加载本地变量。
|
||||
// 只尊重外层 shell 先注入的变量;.env.local 需要能覆盖 .env。
|
||||
load_local_env_files();
|
||||
|
||||
// 统一先从配置对象读取监听地址,避免后续把环境变量读取散落到入口和路由层。
|
||||
let config = AppConfig::from_env();
|
||||
@@ -120,3 +124,92 @@ async fn run_api_server() -> Result<(), std::io::Error> {
|
||||
|
||||
axum::serve(listener, router).await
|
||||
}
|
||||
|
||||
fn load_local_env_files() {
|
||||
let shell_env_keys = env::vars().map(|(key, _)| key).collect::<HashSet<_>>();
|
||||
|
||||
for path in [".env", ".env.local", ".env.secrets.local"] {
|
||||
load_env_file(path, &shell_env_keys);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_env_file(path: &str, shell_env_keys: &HashSet<String>) {
|
||||
let Ok(raw_text) = fs::read_to_string(path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let raw_text = raw_text.trim_start_matches('\u{feff}');
|
||||
|
||||
for raw_line in raw_text.split('\n') {
|
||||
let line = raw_line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((raw_key, raw_value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let key = raw_key.trim().trim_start_matches('\u{feff}');
|
||||
if !is_valid_env_key(key) || shell_env_keys.contains(key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 这里只在启动前、Tokio runtime 创建前写入进程环境,避免并发读写 env。
|
||||
unsafe {
|
||||
env::set_var(key, strip_env_value(raw_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_env_value(raw_value: &str) -> String {
|
||||
let value = raw_value.trim_end_matches('\r');
|
||||
if value.len() >= 2 {
|
||||
let bytes = value.as_bytes();
|
||||
let first = bytes[0];
|
||||
let last = bytes[value.len() - 1];
|
||||
if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
|
||||
return value[1..value.len() - 1].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
value.to_string()
|
||||
}
|
||||
|
||||
fn is_valid_env_key(key: &str) -> bool {
|
||||
let mut chars = key.chars();
|
||||
match chars.next() {
|
||||
Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{is_valid_env_key, strip_env_value};
|
||||
|
||||
#[test]
|
||||
fn strip_env_value_removes_wrapping_quotes() {
|
||||
assert_eq!(strip_env_value("\"true\""), "true");
|
||||
assert_eq!(strip_env_value("'aliyun'"), "aliyun");
|
||||
assert_eq!(strip_env_value("plain\r"), "plain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_env_key_can_strip_utf8_bom_prefix() {
|
||||
let key = "\u{feff}SMS_AUTH_ENABLED"
|
||||
.trim()
|
||||
.trim_start_matches('\u{feff}');
|
||||
|
||||
assert_eq!(key, "SMS_AUTH_ENABLED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_valid_env_key_accepts_dotenv_key_subset() {
|
||||
assert!(is_valid_env_key("SMS_AUTH_ENABLED"));
|
||||
assert!(is_valid_env_key("_LOCAL_KEY_1"));
|
||||
assert!(!is_valid_env_key("1_BAD"));
|
||||
assert!(!is_valid_env_key("BAD-KEY"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user