a3s 0.10.1

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::path::{Path, PathBuf};
use std::sync::Arc;

use a3s_boot::ilink::{IlinkClient, IlinkLoginTransport, IlinkMessagingTransport, IlinkModule};
use a3s_boot::{ControllerDefinition, Module, ModuleRef, ProviderDefinition, Result as BootResult};

use super::account_controller::WeixinAccountController;
use super::capability_controller::WeixinCapabilityController;
use super::channel_config::{WeixinChannelConfig, WeixinChannelLoad};
use super::credential_store::PrivateFileCredentialStore;
use super::dto::SafeBlocker;
use super::login_controller::WeixinLoginController;
use super::remote_controller::WeixinRemoteController;
use super::service::WeixinService;
use crate::api::code_web::remote::{RemoteAgentReadService, RemoteModule};
use crate::api::code_web::state::CodeWebState;

pub(in crate::api::code_web) struct WeixinModule {
    provider: WeixinProvider,
}

enum WeixinProvider {
    ConfiguredWithRemote,
    #[cfg(test)]
    Fixed(Arc<WeixinService>),
}

impl WeixinModule {
    pub(in crate::api::code_web) fn configured() -> Self {
        Self {
            provider: WeixinProvider::ConfiguredWithRemote,
        }
    }

    #[cfg(test)]
    pub(super) fn disabled_isolated() -> Self {
        Self {
            provider: WeixinProvider::Fixed(Arc::new(WeixinService::disabled(None))),
        }
    }

    #[cfg(test)]
    pub(super) fn mock(
        transport: Arc<dyn a3s_boot::ilink::IlinkLoginTransport>,
        credential_store: Arc<dyn super::credential_store::WeixinCredentialStore>,
    ) -> Self {
        let login = Arc::new(super::login_coordinator::WeixinLoginCoordinator::new(
            transport,
            Arc::clone(&credential_store),
        ));
        Self {
            provider: WeixinProvider::Fixed(Arc::new(WeixinService::mock(login, credential_store))),
        }
    }

    #[cfg(test)]
    pub(super) fn mock_with_remote(
        transport: Arc<dyn a3s_boot::ilink::IlinkLoginTransport>,
        credential_store: Arc<dyn super::credential_store::WeixinCredentialStore>,
        remote_read: Arc<RemoteAgentReadService>,
    ) -> Self {
        let login = Arc::new(super::login_coordinator::WeixinLoginCoordinator::new(
            transport,
            Arc::clone(&credential_store),
        ));
        Self {
            provider: WeixinProvider::Fixed(Arc::new(WeixinService::mock_with_remote(
                login,
                credential_store,
                remote_read,
            ))),
        }
    }

    #[cfg(test)]
    pub(super) fn mock_with_monitor(
        login_transport: Arc<dyn a3s_boot::ilink::IlinkLoginTransport>,
        credential_store: Arc<dyn super::credential_store::WeixinCredentialStore>,
        monitor: Arc<super::monitor::WeixinMonitorSupervisor>,
        runtime_store: super::runtime_store::WeixinRuntimeStore,
    ) -> Self {
        let login = Arc::new(super::login_coordinator::WeixinLoginCoordinator::new(
            login_transport,
            Arc::clone(&credential_store),
        ));
        Self {
            provider: WeixinProvider::Fixed(Arc::new(WeixinService::mock_with_monitor(
                login,
                credential_store,
                monitor,
                runtime_store,
            ))),
        }
    }

    #[cfg(test)]
    pub(super) fn production_for_test(
        login_transport: Arc<dyn IlinkLoginTransport>,
        messaging_transport: Arc<dyn IlinkMessagingTransport>,
        credential_store: Arc<dyn super::credential_store::WeixinCredentialStore>,
        remote_read: Arc<RemoteAgentReadService>,
        runtime_directory: PathBuf,
    ) -> Self {
        Self {
            provider: WeixinProvider::Fixed(Arc::new(WeixinService::production(
                login_transport,
                messaging_transport,
                credential_store,
                runtime_directory,
                remote_read,
            ))),
        }
    }
}

impl Module for WeixinModule {
    fn name(&self) -> &'static str {
        "a3s-code-web-weixin"
    }

    fn imports(&self) -> Vec<Arc<dyn Module>> {
        match &self.provider {
            WeixinProvider::ConfiguredWithRemote => vec![
                Arc::new(RemoteModule),
                Arc::new(IlinkModule::weixin(format!(
                    "A3S/{}",
                    env!("CARGO_PKG_VERSION")
                ))),
            ],
            #[cfg(test)]
            WeixinProvider::Fixed(_) => Vec::new(),
        }
    }

    fn providers(&self) -> BootResult<Vec<ProviderDefinition>> {
        match &self.provider {
            WeixinProvider::ConfiguredWithRemote => Ok(vec![ProviderDefinition::factory_arc::<
                WeixinService,
                _,
            >(configured_service)]),
            #[cfg(test)]
            WeixinProvider::Fixed(service) => {
                Ok(vec![ProviderDefinition::from_arc(Arc::clone(service))])
            }
        }
    }

    fn controllers(&self, module_ref: &ModuleRef) -> BootResult<Vec<ControllerDefinition>> {
        let service = module_ref.get::<WeixinService>()?;
        Ok(vec![
            Arc::new(WeixinCapabilityController::new(Arc::clone(&service))).controller()?,
            Arc::new(WeixinAccountController::new(Arc::clone(&service))).controller()?,
            Arc::new(WeixinLoginController::new(Arc::clone(&service))).controller()?,
            Arc::new(WeixinRemoteController::new(service)).controller()?,
        ])
    }

    fn on_application_bootstrap(
        &self,
        module_ref: ModuleRef,
    ) -> a3s_boot::BoxFuture<'static, BootResult<()>> {
        Box::pin(async move {
            let service = module_ref.get::<WeixinService>()?;
            service.bootstrap().await
        })
    }

    fn on_application_shutdown(
        &self,
        module_ref: ModuleRef,
    ) -> a3s_boot::BoxFuture<'static, BootResult<()>> {
        Box::pin(async move {
            let service = module_ref.get::<WeixinService>()?;
            service.shutdown().await
        })
    }
}

fn configured_service(module_ref: &ModuleRef) -> BootResult<Arc<WeixinService>> {
    let remote_read = module_ref.get::<RemoteAgentReadService>()?;
    let state = module_ref.get::<CodeWebState>()?;
    match WeixinChannelConfig::load(&state.config_path) {
        WeixinChannelLoad::Enabled => {}
        WeixinChannelLoad::Unavailable(blocker) => {
            return Ok(Arc::new(WeixinService::disabled_with(
                Some(remote_read),
                blocker,
            )))
        }
    }
    let client = module_ref.get::<IlinkClient>()?;
    let login_transport: Arc<dyn IlinkLoginTransport> = client.clone();
    let messaging_transport: Arc<dyn IlinkMessagingTransport> = client;
    let state_root = match weixin_state_root(&state.config_path) {
        Ok(state_root) => state_root,
        Err(blocker) => {
            return Ok(Arc::new(WeixinService::disabled_with(
                Some(remote_read),
                blocker,
            )))
        }
    };
    let credential_store = Arc::new(PrivateFileCredentialStore::new(
        state_root.join("credentials"),
    ));
    Ok(Arc::new(WeixinService::production(
        login_transport,
        messaging_transport,
        credential_store,
        state_root.join("runtime"),
        remote_read,
    )))
}

fn weixin_state_root(config_path: &Path) -> Result<PathBuf, SafeBlocker> {
    let parent = config_path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
        .unwrap_or_else(|| Path::new("."));
    std::fs::canonicalize(parent)
        .map(|parent| parent.join("channels").join("weixin"))
        .map_err(|_| SafeBlocker {
            code: "ilink_state_path_unavailable".to_string(),
            message: "The local Weixin state directory could not be resolved safely.".to_string(),
        })
}