This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -1,7 +1,9 @@
use std::{error::Error, fmt, sync::Arc};
#[cfg(test)]
use std::{collections::HashMap, sync::Mutex};
use std::{
collections::HashMap,
error::Error,
fmt,
sync::{Arc, Mutex},
};
use module_ai::{AiTaskService, InMemoryAiTaskStore};
use module_auth::{
@@ -11,14 +13,16 @@ use module_auth::{
use module_runtime::RuntimeSnapshotRecord;
#[cfg(test)]
use module_runtime::{SAVE_SNAPSHOT_VERSION, format_utc_micros};
use platform_agent::MockLangChainRustAgentExecutor;
use platform_auth::{
AccessTokenClaims, AccessTokenClaimsInput, AuthProvider, BindingStatus, JwtConfig, JwtError,
RefreshCookieConfig, RefreshCookieError, RefreshCookieSameSite, SmsAuthConfig, SmsAuthProvider,
SmsAuthProviderKind, SmsProviderError, WechatProvider, sign_access_token, verify_access_token,
};
use platform_llm::{LlmClient, LlmConfig, LlmError};
use platform_llm::{LlmClient, LlmConfig, LlmError, LlmProvider};
use platform_oss::{OssClient, OssConfig, OssError};
use serde_json::Value;
use shared_contracts::creative_agent::CreativeAgentSessionSnapshot;
use spacetime_client::{SpacetimeClient, SpacetimeClientConfig, SpacetimeClientError};
use time::OffsetDateTime;
use tracing::{info, warn};
@@ -51,11 +55,22 @@ pub struct AppState {
ai_task_service: AiTaskService,
spacetime_client: SpacetimeClient,
llm_client: Option<LlmClient>,
creative_agent_gpt5_client: Option<LlmClient>,
creative_agent_executor: Arc<MockLangChainRustAgentExecutor>,
// Phase 1 任务 E 的 creative session facade 暂存在 api-server。
// creative_agent_* 表由任务 D 收口后,这里只保留读写 facade。
creative_agent_sessions: Arc<Mutex<HashMap<String, CreativeAgentSessionRuntimeRecord>>>,
#[cfg(test)]
// 测试环境允许在未启动 SpacetimeDB 时,用内存快照兜底当前 runtime story 回归链。
test_runtime_snapshot_store: Arc<Mutex<HashMap<String, RuntimeSnapshotRecord>>>,
}
#[derive(Clone, Debug)]
struct CreativeAgentSessionRuntimeRecord {
owner_user_id: String,
snapshot: CreativeAgentSessionSnapshot,
}
// 后台管理员运行态独立于普通玩家登录体系,只从环境变量构造。
#[derive(Clone, Debug)]
pub struct AdminRuntime {
@@ -167,6 +182,7 @@ impl AppState {
procedure_timeout: config.spacetime_procedure_timeout,
});
let llm_client = build_llm_client(&config)?;
let creative_agent_gpt5_client = build_creative_agent_gpt5_client(&config)?;
Ok(Self {
config,
@@ -185,6 +201,9 @@ impl AppState {
ai_task_service,
spacetime_client,
llm_client,
creative_agent_gpt5_client,
creative_agent_executor: Arc::new(MockLangChainRustAgentExecutor),
creative_agent_sessions: Arc::new(Mutex::new(HashMap::new())),
#[cfg(test)]
test_runtime_snapshot_store: Arc::new(Mutex::new(HashMap::new())),
})
@@ -336,6 +355,44 @@ impl AppState {
self.llm_client.as_ref()
}
pub fn creative_agent_gpt5_client(&self) -> Option<&LlmClient> {
self.creative_agent_gpt5_client.as_ref()
}
pub fn creative_agent_executor(&self) -> Arc<MockLangChainRustAgentExecutor> {
self.creative_agent_executor.clone()
}
pub fn get_creative_agent_session(
&self,
session_id: &str,
owner_user_id: &str,
) -> Option<CreativeAgentSessionSnapshot> {
self.creative_agent_sessions
.lock()
.expect("creative agent session store should lock")
.get(session_id)
.filter(|record| record.owner_user_id == owner_user_id)
.map(|record| record.snapshot.clone())
}
pub fn put_creative_agent_session(
&self,
owner_user_id: String,
session: CreativeAgentSessionSnapshot,
) {
self.creative_agent_sessions
.lock()
.expect("creative agent session store should lock")
.insert(
session.session_id.clone(),
CreativeAgentSessionRuntimeRecord {
owner_user_id,
snapshot: session,
},
);
}
pub async fn get_runtime_snapshot_record(
&self,
user_id: String,
@@ -710,6 +767,31 @@ fn build_llm_client(config: &AppConfig) -> Result<Option<LlmClient>, AppStateIni
Ok(Some(LlmClient::new(llm_config)?))
}
fn build_creative_agent_gpt5_client(
config: &AppConfig,
) -> Result<Option<LlmClient>, AppStateInitError> {
let Some(api_key) = config
.apimart_api_key
.as_ref()
.map(|value| value.trim())
.filter(|value| !value.is_empty())
else {
return Ok(None);
};
let llm_config = LlmConfig::new(
LlmProvider::OpenAiCompatible,
config.apimart_base_url.clone(),
api_key.to_string(),
platform_agent::CREATIVE_AGENT_GPT5_MODEL.to_string(),
config.apimart_image_request_timeout_ms,
0,
config.llm_retry_backoff_ms,
)?;
Ok(Some(LlmClient::new(llm_config)?))
}
// 只有在用户名和密码都已配置时才启用后台,避免半配置状态暴露伪入口。
fn build_admin_runtime(
config: &AppConfig,
@@ -783,5 +865,28 @@ mod tests {
let state = AppState::new(AppConfig::default()).expect("state should build");
assert!(state.llm_client().is_none());
assert!(state.creative_agent_gpt5_client().is_none());
}
#[test]
fn app_state_builds_creative_agent_gpt5_client_from_apimart_settings() {
let mut config = AppConfig::default();
config.llm_api_key = None;
config.apimart_base_url = "https://api.apimart.test/v1".to_string();
config.apimart_api_key = Some("apimart-key".to_string());
let state = AppState::new(config).expect("state should build");
let client = state
.creative_agent_gpt5_client()
.expect("creative agent gpt5 client should exist");
assert_eq!(
client.config().model(),
platform_agent::CREATIVE_AGENT_GPT5_MODEL
);
assert_eq!(
client.config().responses_url(),
"https://api.apimart.test/v1/responses"
);
}
}