mod channel;
mod commands;
mod orchestrator;
mod qq;
mod qq_client;
mod shared;
pub mod telegram;
use std::env;
use std::sync::Arc;
use tokio::runtime::Runtime;
use anyhow::{Context, Result, bail};
use crate::{
config::{AppConfig, AuthStore, ConfigPaths},
llm::LlmClient,
mcp::McpManager,
storage::SessionStore,
tooling::{FileReadTracker, ToolRegistry},
};
use orchestrator::ChannelOrchestrator;
use shared::compose_instruction_prompt;
pub fn run() -> Result<()> {
let runtime = Runtime::new().context("failed to create runtime")?;
let local = tokio::task::LocalSet::new();
local.block_on(&runtime, run_async())
}
async fn run_async() -> Result<()> {
let workspace_root = env::current_dir().context("failed to determine workspace root")?;
let paths = ConfigPaths::discover()?;
let config = AppConfig::load_or_create(&paths)?;
crate::log_info!("Gateway starting, config loaded");
let mut logging_config = config.logging.clone();
logging_config.console = true;
crate::logging::init(&paths.data_dir, logging_config);
crate::log_info!("Logging initialized (console: true)");
let auth = AuthStore::load_or_create(&paths)?;
crate::log_info!("Auth store loaded");
let default_model = config.resolve_active_model_for_gateway(&auth)?;
let instruction_prompt = compose_instruction_prompt(&workspace_root, &paths, &config);
let db_path = paths.default_database_path();
let mut orchestrator = ChannelOrchestrator::new();
if config.gateway.telegram.enabled {
let allowlist = config
.gateway
.telegram
.allowlist
.iter()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.collect::<std::collections::HashSet<_>>();
if allowlist.is_empty() {
bail!(
"gateway.telegram.allowlist is empty; configure at least one Telegram user/chat id"
);
}
let bot_token = auth
.telegram_bot_token()
.context("missing Telegram bot token in auth.json for channel 'telegram'")?
.to_string();
crate::log_info!(
"Telegram channel enabled, allowlist: {} entries",
allowlist.len()
);
let store = SessionStore::open(db_path)?;
let llm = LlmClient::new()?;
let mcp = McpManager::new(workspace_root.clone(), config.mcp.servers.clone());
let file_read_tracker = Arc::new(FileReadTracker::new());
let mut tools = ToolRegistry::new(
workspace_root.clone(),
paths.config_dir.clone(),
config.skills.clone(),
mcp,
config.permissions.clone(),
file_read_tracker,
config.rtk.enabled,
);
tools.set_active_model(default_model.clone());
let channel = telegram::TelegramChannel::new(
workspace_root.clone(),
config.clone(),
auth.clone(),
store,
llm,
tools,
instruction_prompt.clone(),
allowlist,
config.gateway.telegram.poll_timeout_secs.max(1),
bot_token,
);
orchestrator.add(Box::new(channel));
}
if config.gateway.qq.enabled {
let allowlist = config
.gateway
.qq
.allowlist
.iter()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.collect::<std::collections::HashSet<_>>();
if allowlist.is_empty() {
bail!("gateway.qq.allowlist is empty; configure at least one QQ user/channel id");
}
let app_id = auth
.qq_app_id()
.context("missing QQ AppID in auth.json")?
.to_string();
let app_secret = auth
.qq_app_secret()
.context("missing QQ AppSecret in auth.json")?
.to_string();
crate::log_info!("QQ channel enabled, allowlist: {} entries", allowlist.len());
let store = SessionStore::open(db_path)?;
let llm = LlmClient::new()?;
let mcp = McpManager::new(workspace_root.clone(), config.mcp.servers.clone());
let file_read_tracker = Arc::new(FileReadTracker::new());
let mut tools = ToolRegistry::new(
workspace_root.clone(),
paths.config_dir.clone(),
config.skills.clone(),
mcp,
config.permissions.clone(),
file_read_tracker,
config.rtk.enabled,
);
tools.set_active_model(default_model.clone());
let channel = qq::QQChannel::new(
workspace_root.clone(),
config.clone(),
auth.clone(),
store,
llm,
tools,
instruction_prompt.clone(),
allowlist,
app_id,
app_secret,
config.gateway.qq.sandbox,
);
orchestrator.add(Box::new(channel));
}
if orchestrator.is_empty() {
bail!(
"No gateway enabled; set either gateway.telegram.enabled or gateway.qq.enabled to true in config.toml"
);
}
crate::log_info!(
"Gateway ready, starting {} channel(s): {}",
orchestrator.channel_names().len(),
orchestrator.channel_names().join(", ")
);
orchestrator.run().await
}