use std::collections::BTreeMap; use std::sync::Arc; use std::time::Instant; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; use tokio::sync::Mutex; use super::i18n::{self, Lang}; use super::session_bridge::{ActiveSession, SessionBridge}; use super::types::{MessageLevel, RichMessage}; use crate::acp::manager::ConnectionManager; use crate::acp::registry::all_acp_agents; use crate::acp::types::PromptInputBlock; use crate::db::entities::conversation; use crate::db::service::{conversation_service, folder_service, sender_context_service}; use crate::models::agent::AgentType; use crate::web::event_bridge::EventEmitter; // ── /folder ── pub async fn handle_folder( db: &DatabaseConnection, args: &str, channel_id: i32, sender_id: &str, lang: Lang, prefix: &str, ) -> RichMessage { if args.is_empty() { return list_folders(db, channel_id, sender_id, lang, prefix).await; } // Try parse as index (1-based) if let Ok(idx) = args.parse::() { return select_folder_by_index(db, idx, channel_id, sender_id, lang, prefix).await; } // Treat as path select_folder_by_path(db, args, channel_id, sender_id, lang).await } async fn list_folders( db: &DatabaseConnection, channel_id: i32, sender_id: &str, lang: Lang, prefix: &str, ) -> RichMessage { let folders = match folder_service::list_folders(db).await { Ok(f) => f, Err(e) => return RichMessage::error(format!("Failed to list folders: {e}")), }; if folders.is_empty() { return RichMessage::info(t(lang, "No folders found.", "没有找到项目目录。")) .with_title(t(lang, "Working Folder", "工作目录")); } let ctx = sender_context_service::get_or_create(db, channel_id, sender_id) .await .ok(); let mut body = String::new(); for (i, f) in folders.iter().take(10).enumerate() { let current = ctx .as_ref() .and_then(|c| c.current_folder_id) .map(|id| id == f.id) .unwrap_or(false); let marker = if current { " [*]" } else { "" }; body.push_str(&format!( "{}. {}{} ({})\n", i + 1, f.name, marker, f.path )); } body.push_str(&format!( "\n{}", tp( lang, prefix, "Reply {prefix}folder to select.", "回复 {prefix}folder <数字> 选择目录。" ) )); RichMessage::info(body.trim_end()) .with_title(t(lang, "Working Folder", "工作目录")) } async fn select_folder_by_index( db: &DatabaseConnection, idx: usize, channel_id: i32, sender_id: &str, lang: Lang, prefix: &str, ) -> RichMessage { if idx == 0 { return RichMessage::info(t(lang, "Index starts from 1.", "序号从 1 开始。")); } let folders = match folder_service::list_folders(db).await { Ok(f) => f, Err(e) => return RichMessage::error(format!("Failed to list folders: {e}")), }; let Some(folder) = folders.get(idx - 1) else { return RichMessage::info(tp( lang, prefix, "Index out of range. Use {prefix}folder to list.", "序号超出范围,请使用 {prefix}folder 查看列表。", )); }; let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(folder.id)) .await; RichMessage::info(format!("{} ({})", folder.name, folder.path)) .with_title(t(lang, "Folder Selected", "已选择目录")) } async fn select_folder_by_path( db: &DatabaseConnection, path: &str, channel_id: i32, sender_id: &str, lang: Lang, ) -> RichMessage { let entry = match folder_service::add_folder(db, path).await { Ok(e) => e, Err(e) => return RichMessage::error(format!("Failed to add folder: {e}")), }; let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(entry.id)).await; RichMessage::info(format!("{} ({})", entry.name, entry.path)) .with_title(t(lang, "Folder Selected", "已选择目录")) } // ── /agent ── pub async fn handle_agent( db: &DatabaseConnection, args: &str, channel_id: i32, sender_id: &str, lang: Lang, prefix: &str, ) -> RichMessage { if args.is_empty() { return list_agents(db, channel_id, sender_id, lang, prefix).await; } // Try parse as index if let Ok(idx) = args.parse::() { return select_agent_by_index(db, idx, channel_id, sender_id, lang, prefix).await; } // Try parse as agent type name select_agent_by_name(db, args, channel_id, sender_id, lang).await } async fn list_agents( db: &DatabaseConnection, channel_id: i32, sender_id: &str, lang: Lang, prefix: &str, ) -> RichMessage { let agents = all_acp_agents(); let ctx = sender_context_service::get_or_create(db, channel_id, sender_id) .await .ok(); let mut body = String::new(); for (i, at) in agents.iter().enumerate() { let at_str = agent_type_to_string(*at); let current = ctx .as_ref() .and_then(|c| c.current_agent_type.as_deref()) .map(|s| s == at_str) .unwrap_or(false); let marker = if current { " [*]" } else { "" }; body.push_str(&format!("{}. {}{}\n", i + 1, at, marker)); } body.push_str(&format!( "\n{}", tp( lang, prefix, "Reply {prefix}agent or {prefix}agent to select.", "回复 {prefix}agent <数字> 或 {prefix}agent <名称> 选择。" ) )); RichMessage::info(body.trim_end()) .with_title(t(lang, "Agent Selection", "选择 Agent")) } async fn select_agent_by_index( db: &DatabaseConnection, idx: usize, channel_id: i32, sender_id: &str, lang: Lang, prefix: &str, ) -> RichMessage { let agents = all_acp_agents(); if idx == 0 || idx > agents.len() { return RichMessage::info(tp( lang, prefix, "Index out of range. Use {prefix}agent to list.", "序号超出范围,请使用 {prefix}agent 查看列表。", )); } let at = agents[idx - 1]; let at_str = agent_type_to_string(at); let _ = sender_context_service::update_agent(db, channel_id, sender_id, Some(at_str)).await; RichMessage::info(at.to_string()) .with_title(t(lang, "Agent Selected", "已选择 Agent")) } async fn select_agent_by_name( db: &DatabaseConnection, name: &str, channel_id: i32, sender_id: &str, lang: Lang, ) -> RichMessage { let at = match parse_agent_type(name) { Some(a) => a, None => { return RichMessage::info(format!( "{}{}", t(lang, "Unknown agent: ", "未知 Agent: "), name )); } }; let at_str = agent_type_to_string(at); let _ = sender_context_service::update_agent(db, channel_id, sender_id, Some(at_str)).await; RichMessage::info(at.to_string()) .with_title(t(lang, "Agent Selected", "已选择 Agent")) } // ── /task ── #[allow(clippy::too_many_arguments)] pub async fn handle_task( db: &DatabaseConnection, task_description: &str, channel_id: i32, sender_id: &str, conn_mgr: &ConnectionManager, emitter: &EventEmitter, bridge: &Arc>, lang: Lang, prefix: &str, ) -> RichMessage { if task_description.is_empty() { return RichMessage::info(tp( lang, prefix, "Usage: {prefix}task ", "用法: {prefix}task <任务描述>", )); } // 1. Load sender context let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { Ok(c) => c, Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), }; let folder_id = match ctx.current_folder_id { Some(id) => id, None => { return RichMessage::info(tp( lang, prefix, "No folder selected. Use {prefix}folder first.", "未选择工作目录,请先使用 {prefix}folder 选择。", )); } }; // 2. Get folder info let folder = match folder_service::get_folder_by_id(db, folder_id).await { Ok(Some(f)) => f, _ => { return RichMessage::info(tp( lang, prefix, "Folder not found. Use {prefix}folder to select.", "目录不存在,请使用 {prefix}folder 重新选择。", )); } }; // 3. Resolve agent type let agent_type = resolve_agent_type(&ctx.current_agent_type, &folder.default_agent_type); // 4. Create conversation record let conv = match conversation_service::create( db, folder_id, agent_type, Some(truncate_title(task_description)), folder.git_branch.clone(), ) .await { Ok(c) => c, Err(e) => return RichMessage::error(format!("Failed to create conversation: {e}")), }; // 5. Spawn ACP agent let owner_label = format!("chat_channel:{}:{}", channel_id, sender_id); let connection_id = match conn_mgr .spawn_agent( agent_type, Some(folder.path.clone()), None, BTreeMap::new(), owner_label, emitter.clone(), ) .await { Ok(id) => id, Err(e) => { // Clean up the conversation record let _ = conversation_service::update_status( db, conv.id, conversation::ConversationStatus::Cancelled, ) .await; return RichMessage::error(format!( "{}{e}", t(lang, "Failed to start agent: ", "启动 Agent 失败: ") )); } }; // 6. Register in bridge (prompt will be sent after SessionStarted event) { let session = ActiveSession { channel_id, sender_id: sender_id.to_string(), conversation_id: conv.id, connection_id: connection_id.clone(), content_buffer: String::new(), tool_calls: Vec::new(), last_flushed: Instant::now(), pending_prompt: Some(task_description.to_string()), permission_pending: None, }; bridge.lock().await.register(connection_id.clone(), session); } // 7. Update sender context let _ = sender_context_service::update_session( db, channel_id, sender_id, Some(conv.id), Some(connection_id), ) .await; RichMessage::info(format!( "[{}] #{} @ {}", agent_type, conv.id, folder.name, )) .with_title(t(lang, "Task Started", "任务已启动")) } // ── /sessions ── pub async fn handle_sessions( db: &DatabaseConnection, channel_id: i32, sender_id: &str, lang: Lang, prefix: &str, ) -> RichMessage { let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { Ok(c) => c, Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), }; let folder_id = match ctx.current_folder_id { Some(id) => id, None => { return RichMessage::info(tp( lang, prefix, "No folder selected. Use {prefix}folder first.", "未选择工作目录,请先使用 {prefix}folder 选择。", )); } }; let folder = match folder_service::get_folder_by_id(db, folder_id).await { Ok(Some(f)) => f, _ => { return RichMessage::info(t( lang, "Folder not found.", "目录不存在。", )); } }; let convs = match conversation_service::list_by_folder( db, folder_id, None, None, None, Some("in_progress".to_string()), ) .await { Ok(c) => c, Err(e) => return RichMessage::error(format!("Failed to list sessions: {e}")), }; if convs.is_empty() { return RichMessage::info(t( lang, "No active sessions in this folder.", "当前目录没有进行中的会话。", )) .with_title(format!( "{} - {}", t(lang, "Sessions", "会话列表"), folder.name )); } let mut body = String::new(); for (i, c) in convs.iter().take(10).enumerate() { let title = c.title.as_deref().unwrap_or("(untitled)"); let current = ctx .current_conversation_id .map(|id| id == c.id) .unwrap_or(false); let marker = if current { " [*]" } else { "" }; body.push_str(&format!( "{}. [{}] {} (#{}){} \n", i + 1, c.agent_type, title, c.id, marker, )); } body.push_str(&format!( "\n{}", tp( lang, prefix, "Reply {prefix}resume to continue.", "回复 {prefix}resume <会话ID> 继续会话。" ) )); RichMessage::info(body.trim_end()).with_title(format!( "{} - {}", t(lang, "Sessions", "会话列表"), folder.name )) } // ── /resume ── #[allow(clippy::too_many_arguments)] pub async fn handle_resume( db: &DatabaseConnection, args: &str, channel_id: i32, sender_id: &str, conn_mgr: &ConnectionManager, emitter: &EventEmitter, bridge: &Arc>, lang: Lang, prefix: &str, ) -> RichMessage { if args.is_empty() { return list_recent_sessions(db, lang, prefix).await; } let conversation_id: i32 = match args.parse() { Ok(id) => id, Err(_) => { return list_recent_sessions(db, lang, prefix).await; } }; let conv = match conversation_service::get_by_id(db, conversation_id).await { Ok(c) => c, Err(_) => { return RichMessage::info(t( lang, "Conversation not found.", "会话不存在。", )); } }; let folder = match folder_service::get_folder_by_id(db, conv.folder_id).await { Ok(Some(f)) => f, _ => { return RichMessage::info(t(lang, "Folder not found.", "目录不存在。")); } }; // Spawn agent with session_id for resume let owner_label = format!("chat_channel:{}:{}", channel_id, sender_id); let connection_id = match conn_mgr .spawn_agent( conv.agent_type, Some(folder.path.clone()), conv.external_id.clone(), BTreeMap::new(), owner_label, emitter.clone(), ) .await { Ok(id) => id, Err(e) => { return RichMessage::error(format!( "{}{e}", t(lang, "Failed to start agent: ", "启动 Agent 失败: ") )); } }; // Register in bridge (no pending prompt for resume) { let session = ActiveSession { channel_id, sender_id: sender_id.to_string(), conversation_id: conv.id, connection_id: connection_id.clone(), content_buffer: String::new(), tool_calls: Vec::new(), last_flushed: Instant::now(), pending_prompt: None, permission_pending: None, }; bridge.lock().await.register(connection_id.clone(), session); } // Update sender context let _ = sender_context_service::update_session( db, channel_id, sender_id, Some(conv.id), Some(connection_id), ) .await; let _ = sender_context_service::update_folder(db, channel_id, sender_id, Some(conv.folder_id)) .await; let title = conv.title.as_deref().unwrap_or("(untitled)"); RichMessage::info(format!( "[{}] #{} {} @ {}", conv.agent_type, conv.id, title, folder.name, )) .with_title(t(lang, "Session Resumed", "会话已恢复")) } // ── /cancel ── pub async fn handle_cancel( db: &DatabaseConnection, channel_id: i32, sender_id: &str, conn_mgr: &ConnectionManager, bridge: &Arc>, lang: Lang, ) -> RichMessage { let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { Ok(c) => c, Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), }; let connection_id = match &ctx.current_connection_id { Some(id) => id.clone(), None => { return RichMessage::info(t( lang, "No active session to cancel.", "没有进行中的任务可取消。", )); } }; // Cancel the ACP connection let _ = conn_mgr.cancel(&connection_id).await; // Remove from bridge bridge.lock().await.remove(&connection_id); // Update conversation status if let Some(conv_id) = ctx.current_conversation_id { let _ = conversation_service::update_status( db, conv_id, conversation::ConversationStatus::Cancelled, ) .await; } // Clear session from context let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; RichMessage::info(t( lang, "Current task has been cancelled.", "当前任务已取消。", )) .with_title(t(lang, "Task Cancelled", "任务已取消")) } // ── /approve, /deny ── #[allow(clippy::too_many_arguments)] pub async fn handle_permission_response( approve: bool, always: bool, db: &DatabaseConnection, channel_id: i32, sender_id: &str, conn_mgr: &ConnectionManager, bridge: &Arc>, lang: Lang, ) -> RichMessage { let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { Ok(c) => c, Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), }; let connection_id = match &ctx.current_connection_id { Some(id) => id.clone(), None => { return RichMessage::info(t( lang, "No active session.", "没有活跃的会话。", )); } }; let pending = { let mut bridge_guard = bridge.lock().await; let session = match bridge_guard.get_mut(&connection_id) { Some(s) => s, None => { return RichMessage::info(t( lang, "No active session found.", "未找到活跃的会话。", )); } }; session.permission_pending.take() }; let pending = match pending { Some(p) => p, None => { return RichMessage::info(t( lang, "No pending permission request.", "没有待处理的权限请求。", )); } }; // Find the appropriate option_id let option_id = if approve { pending .options .iter() .find(|o| o.kind == "allow" || o.kind == "allowForSession") .or_else(|| pending.options.first()) .map(|o| o.option_id.clone()) } else { pending .options .iter() .find(|o| o.kind == "deny") .or_else(|| pending.options.last()) .map(|o| o.option_id.clone()) }; let Some(option_id) = option_id else { return RichMessage::info(t( lang, "No valid permission option found.", "未找到有效的权限选项。", )); }; if let Err(e) = conn_mgr .respond_permission(&connection_id, &pending.request_id, &option_id) .await { return RichMessage::error(format!( "{}{e}", t( lang, "Failed to respond to permission: ", "权限响应失败: " ) )); } // Update auto_approve if requested if always && approve { let _ = sender_context_service::update_auto_approve(db, channel_id, sender_id, true).await; } let action = if approve { t(lang, "Approved", "已批准") } else { t(lang, "Denied", "已拒绝") }; let mut msg = RichMessage::info(format!("{}: {}", action, pending.tool_description)); if always && approve { msg = msg.with_field( "", t( lang, "Auto-approve enabled for this session.", "已启用自动批准。", ), ); } msg.with_title(t(lang, "Permission Response", "权限响应")) } // ── follow-up (non-command text) ── pub async fn handle_followup( db: &DatabaseConnection, text: &str, channel_id: i32, sender_id: &str, conn_mgr: &ConnectionManager, bridge: &Arc>, lang: Lang, prefix: &str, ) -> RichMessage { let ctx = match sender_context_service::get_or_create(db, channel_id, sender_id).await { Ok(c) => c, Err(e) => return RichMessage::error(format!("Failed to load context: {e}")), }; let connection_id = match &ctx.current_connection_id { Some(id) => id.clone(), None => { return RichMessage::info(tp( lang, prefix, "No active session. Use {prefix}task to start one.", "没有活跃的会话,请使用 {prefix}task 开始新任务。", )); } }; // Check connection exists in bridge { let bridge_guard = bridge.lock().await; if bridge_guard.get(&connection_id).is_none() { // Connection lost, clear context drop(bridge_guard); let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; return RichMessage::info(tp( lang, prefix, "Session connection lost. Use {prefix}task to start a new one.", "会话连接已断开,请使用 {prefix}task 开始新任务。", )); } } // Send prompt to agent let blocks = vec![PromptInputBlock::Text { text: text.to_string(), }]; if let Err(e) = conn_mgr.send_prompt(&connection_id, blocks).await { // Connection may have died bridge.lock().await.remove(&connection_id); let _ = sender_context_service::clear_session(db, channel_id, sender_id).await; return RichMessage::error(format!( "{}{e}", t(lang, "Failed to send message: ", "发送消息失败: ") )); } RichMessage::info(t(lang, "Message sent.", "消息已发送。")) } // ── /resume (list recent) ── async fn list_recent_sessions( db: &DatabaseConnection, lang: Lang, prefix: &str, ) -> RichMessage { let recent = match conversation::Entity::find() .filter(conversation::Column::DeletedAt.is_null()) .order_by_desc(conversation::Column::CreatedAt) .limit(10) .all(db) .await { Ok(rows) => rows, Err(e) => { return RichMessage { title: Some(i18n::query_failed_title(lang).to_string()), body: e.to_string(), fields: Vec::new(), level: MessageLevel::Error, }; } }; if recent.is_empty() { return RichMessage::info(t( lang, "No conversations found.", "暂无会话记录。", )) .with_title(t(lang, "Recent Conversations", "最近会话")); } let mut body = String::new(); for conv in &recent { let title = conv.title.as_deref().unwrap_or(i18n::untitled(lang)); let agent = &conv.agent_type; let time = conv.created_at.format("%m-%d %H:%M"); body.push_str(&format!( "#{} [{}] {} ({})\n", conv.id, agent, title, time, )); } body.push_str(&format!( "\n{}", tp( lang, prefix, "Reply {prefix}resume to resume a session.", "回复 {prefix}resume <会话ID> 恢复会话。" ) )); RichMessage::info(body.trim_end()).with_title(t( lang, "Recent Conversations", "最近会话", )) } // ── Helpers ── fn t(lang: Lang, en: &str, zh: &str) -> String { match lang { Lang::ZhCn | Lang::ZhTw => zh.to_string(), _ => en.to_string(), } } /// Like `t()` but replaces `{prefix}` placeholders with the actual command prefix. fn tp(lang: Lang, prefix: &str, en: &str, zh: &str) -> String { t(lang, en, zh).replace("{prefix}", prefix) } fn agent_type_to_string(at: AgentType) -> String { serde_json::to_value(at) .ok() .and_then(|v| v.as_str().map(String::from)) .unwrap_or_default() } fn parse_agent_type(name: &str) -> Option { let normalized = name.to_lowercase().replace([' ', '-'], "_"); serde_json::from_value(serde_json::Value::String(normalized)).ok() } fn resolve_agent_type( sender_agent: &Option, folder_default: &Option, ) -> AgentType { if let Some(ref at_str) = sender_agent { if let Some(at) = parse_agent_type(at_str) { return at; } } if let Some(at) = folder_default { return *at; } AgentType::ClaudeCode } fn truncate_title(s: &str) -> String { if s.chars().count() <= 80 { s.to_string() } else { let truncated: String = s.chars().take(77).collect(); format!("{truncated}...") } }