use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use axum::extract::FromRef;
use crate::config::SharedAgentConfig;
use crate::conversation_store::SaveConversationOutcome;
use crate::health::CachedLlmModelsHealthProbe;
use crate::process_handles::ProcessHandles;
use crate::sse::SseStreamHub;
use crate::web::async_chat_job::AsyncChatJobsMap;
use crate::chat_job_queue::ChatJobQueue;
use crate::memory::long_term_memory::LongTermMemoryRuntime;
use super::app_state::{
AppState, AppStateChatRuntime, AppStateConversationRuntime, AppStateHttpCore,
ApprovalSessionSlot, ConversationBacking, ConversationTurnSeed, WebChatJobAppFacet,
effective_workspace_path_from_override, open_conversation_sqlite,
workspace_is_set_from_override,
};
#[derive(Clone)]
pub(crate) struct ConfigReloadFacet {
pub(crate) cfg: SharedAgentConfig,
pub(crate) config_path_for_reload: Option<String>,
}
#[derive(Clone)]
pub(crate) struct UploadsFacet {
pub(crate) cfg: SharedAgentConfig,
pub(crate) fallback_uploads_dir: PathBuf,
}
impl UploadsFacet {
pub(crate) async fn uploads_dir(&self) -> PathBuf {
self.cfg
.read()
.await
.chat_uploads_dir
.clone()
.unwrap_or_else(|| self.fallback_uploads_dir.clone())
}
}
#[derive(Clone)]
pub(crate) struct WebTasksAppFacet {
pub(crate) http: AppStateHttpCore,
pub(crate) process_handles: Arc<ProcessHandles>,
}
pub(crate) type WebChangelogAppFacet = WebTasksAppFacet;
#[derive(Clone)]
pub(crate) struct WebHealthAppFacet {
pub(crate) http: AppStateHttpCore,
pub(crate) llm_models_health_cache: Arc<std::sync::Mutex<Option<CachedLlmModelsHealthProbe>>>,
pub(crate) mount_web_ui: bool,
}
#[derive(Clone)]
pub(crate) struct WebStatusAppFacet {
pub(crate) http: AppStateHttpCore,
pub(crate) conversation: AppStateConversationRuntime,
pub(crate) chat_queue: ChatJobQueue,
pub(crate) long_term_memory: Option<Arc<LongTermMemoryRuntime>>,
pub(crate) process_handles: Arc<ProcessHandles>,
}
#[derive(Clone)]
pub(crate) struct WebChatAppFacet {
pub(crate) cfg: SharedAgentConfig,
pub(crate) conversation: AppStateConversationRuntime,
pub(crate) approval_sessions: Arc<tokio::sync::RwLock<HashMap<String, ApprovalSessionSlot>>>,
}
#[derive(Clone)]
pub(crate) struct WebChatTurnAppFacet {
pub(crate) cfg: SharedAgentConfig,
pub(crate) api_key: Arc<str>,
pub(crate) client: reqwest::Client,
pub(crate) workspace_override: Arc<tokio::sync::RwLock<Option<String>>>,
pub(crate) conversation: AppStateConversationRuntime,
pub(crate) chat: AppStateChatRuntime,
pub(crate) approval_sessions: Arc<tokio::sync::RwLock<HashMap<String, ApprovalSessionSlot>>>,
pub(crate) process_handles: Arc<ProcessHandles>,
pub(crate) sse_stream_hub: Arc<SseStreamHub>,
pub(crate) async_chat_jobs: AsyncChatJobsMap,
pub(crate) tool_job_registry: std::sync::Arc<crate::cm_internal::tool_jobs::ToolJobRegistry>,
}
#[derive(Clone)]
pub(crate) struct AsyncChatJobsFacet {
pub(crate) async_chat_jobs: AsyncChatJobsMap,
}
#[derive(Clone)]
pub(crate) struct E2eConversationFixtureFacet {
pub(crate) conversation: AppStateConversationRuntime,
}
impl E2eConversationFixtureFacet {
pub(crate) async fn delete_conversation_record(&self, conversation_id: &str) {
self.conversation
.delete_conversation_record(conversation_id)
.await
}
pub(crate) async fn save_conversation_messages_if_revision(
&self,
conversation_id: String,
messages: Vec<crate::types::Message>,
active_agent_role: Option<&str>,
active_session_mode: Option<&str>,
expected_revision: Option<u64>,
) -> SaveConversationOutcome {
self.conversation
.save_conversation_messages_if_revision(
conversation_id,
messages,
active_agent_role,
active_session_mode,
expected_revision,
)
.await
}
}
impl WebTasksAppFacet {
pub(crate) async fn effective_workspace_path(&self) -> String {
self.http.effective_workspace_path().await
}
}
impl WebHealthAppFacet {
pub(crate) async fn effective_workspace_path(&self) -> String {
self.http.effective_workspace_path().await
}
}
impl WebStatusAppFacet {
pub(crate) async fn effective_workspace_path(&self) -> String {
self.http.effective_workspace_path().await
}
pub(crate) async fn conversation_count(&self) -> usize {
self.conversation.conversation_count().await
}
}
impl WebChatAppFacet {
pub(crate) async fn load_conversation_seed(
&self,
conversation_id: &str,
) -> Option<ConversationTurnSeed> {
self.conversation
.load_conversation_seed(conversation_id)
.await
}
pub(crate) async fn truncate_conversation_before_user_ordinal_if_revision(
&self,
conversation_id: String,
user_ordinal: usize,
expected_revision: u64,
) -> SaveConversationOutcome {
self.conversation
.truncate_conversation_before_user_ordinal_if_revision(
conversation_id,
user_ordinal,
expected_revision,
)
.await
}
pub(crate) async fn set_web_conversation_store_sqlite(
&self,
sqlite: bool,
) -> Result<(), String> {
if sqlite {
let path = {
let g = self.cfg.read().await;
g.conversation_persistence
.conversation_store_sqlite_path
.clone()
};
if path.trim().is_empty() {
return Err(
"未配置 conversation_store_sqlite_path,无法启用 SQLite 会话存储。".into(),
);
}
let new_backing = {
let conn =
open_conversation_sqlite(Path::new(path.trim())).map_err(|e| e.to_string())?;
ConversationBacking::Sqlite(conn)
};
let mut w = self.conversation.conversation_backing.write().await;
*w = new_backing;
} else {
let mut w = self.conversation.conversation_backing.write().await;
*w = ConversationBacking::memory_default();
}
Ok(())
}
}
impl WebChatTurnAppFacet {
pub(crate) async fn effective_workspace_path(&self) -> String {
effective_workspace_path_from_override(&self.workspace_override, &self.cfg).await
}
pub(crate) async fn workspace_is_set(&self) -> bool {
workspace_is_set_from_override(&self.workspace_override).await
}
pub(crate) fn next_conversation_id(&self) -> String {
let n = self
.conversation
.conversation_id_counter
.fetch_add(1, Ordering::Relaxed);
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
format!("conv_{}_{}", ts, n)
}
pub(crate) async fn load_conversation_seed(
&self,
conversation_id: &str,
) -> Option<ConversationTurnSeed> {
self.conversation
.load_conversation_seed(conversation_id)
.await
}
pub(crate) fn chat_job_app_facet(&self) -> WebChatJobAppFacet {
WebChatJobAppFacet {
conversation: self.conversation.clone(),
process_handles: self.process_handles.turn_handles_arc(),
approval_sessions: Arc::clone(&self.approval_sessions),
tool_job_registry: Arc::clone(&self.tool_job_registry),
}
}
}
impl FromRef<Arc<AppState>> for AppStateHttpCore {
fn from_ref(state: &Arc<AppState>) -> Self {
state.http.clone()
}
}
impl FromRef<Arc<AppState>> for ConfigReloadFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
cfg: Arc::clone(&state.http.cfg),
config_path_for_reload: state.http.config_path_for_reload.clone(),
}
}
}
impl FromRef<Arc<AppState>> for UploadsFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
cfg: Arc::clone(&state.http.cfg),
fallback_uploads_dir: state.http.uploads_dir.clone(),
}
}
}
impl FromRef<Arc<AppState>> for WebTasksAppFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
http: state.http.clone(),
process_handles: Arc::clone(&state.aux.process_handles),
}
}
}
impl FromRef<Arc<AppState>> for WebHealthAppFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
http: state.http.clone(),
llm_models_health_cache: Arc::clone(&state.aux.llm_models_health_cache),
mount_web_ui: state.aux.mount_web_ui,
}
}
}
impl FromRef<Arc<AppState>> for WebStatusAppFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
http: state.http.clone(),
conversation: state.conversation.clone(),
chat_queue: state.chat.chat_queue.clone(),
long_term_memory: state.aux.long_term_memory.clone(),
process_handles: Arc::clone(&state.aux.process_handles),
}
}
}
impl FromRef<Arc<AppState>> for WebChatAppFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
cfg: Arc::clone(&state.http.cfg),
conversation: state.conversation.clone(),
approval_sessions: Arc::clone(&state.aux.approval_sessions),
}
}
}
impl FromRef<Arc<AppState>> for WebChatTurnAppFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
cfg: Arc::clone(&state.http.cfg),
api_key: Arc::clone(&state.http.api_key),
client: state.http.client.clone(),
workspace_override: Arc::clone(&state.http.workspace_override),
conversation: state.conversation.clone(),
chat: state.chat.clone(),
approval_sessions: Arc::clone(&state.aux.approval_sessions),
process_handles: Arc::clone(&state.aux.process_handles),
sse_stream_hub: Arc::clone(&state.aux.sse_stream_hub),
async_chat_jobs: Arc::clone(&state.aux.async_chat_jobs),
tool_job_registry: Arc::clone(&state.aux.tool_job_registry),
}
}
}
impl FromRef<Arc<AppState>> for AsyncChatJobsFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
async_chat_jobs: Arc::clone(&state.aux.async_chat_jobs),
}
}
}
impl FromRef<Arc<AppState>> for E2eConversationFixtureFacet {
fn from_ref(state: &Arc<AppState>) -> Self {
Self {
conversation: state.conversation.clone(),
}
}
}