Compare commits

..

No commits in common. "1309fa28dac4d2ec1dc23a2ef89a269a8bb6b843" and "2a02adc7c3016fe4e649bcc3ec872585d2a437cf" have entirely different histories.

12 changed files with 61 additions and 216 deletions

1
.gitignore vendored
View File

@ -35,4 +35,3 @@ uv.lock
node_modules node_modules
logs logs
dist dist
.trae

View File

@ -13,7 +13,6 @@ serde_yaml = "0.9"
async-trait = "0.1" async-trait = "0.1"
thiserror = "2.0.18" thiserror = "2.0.18"
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }
uuid = { version = "1.0", features = ["v4"] } uuid = { version = "1.0", features = ["v4"] }
axum = { version = "0.8", features = ["ws"] } axum = { version = "0.8", features = ["ws"] }
tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-webpki-roots", "rustls"] } tokio-tungstenite = { version = "0.29.0", features = ["rustls-tls-webpki-roots", "rustls"] }

View File

@ -2,7 +2,6 @@ use async_trait::async_trait;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{RwLock, mpsc}; use tokio::sync::{RwLock, mpsc};
use tokio_util::sync::CancellationToken;
use crate::bus::{MessageBus, OutboundMessage}; use crate::bus::{MessageBus, OutboundMessage};
use crate::protocol::WsOutbound; use crate::protocol::WsOutbound;
@ -19,36 +18,34 @@ struct CliConnection {
#[derive(Clone)] #[derive(Clone)]
pub struct CliChannel { pub struct CliChannel {
connections: Arc<RwLock<HashMap<String, CliConnection>>>, connections: Arc<RwLock<HashMap<String, CliConnection>>>,
/// 全局关闭信号stop() 时 cancel所有 handle_socket 同时收到信号
shutdown_token: CancellationToken,
} }
impl CliChannel { impl CliChannel {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
connections: Arc::new(RwLock::new(HashMap::new())), connections: Arc::new(RwLock::new(HashMap::new())),
shutdown_token: CancellationToken::new(),
} }
} }
/// 注册连接,返回 CancellationToken 供 handle_socket 监听
///
/// 注意:此方法不会向被替换的旧连接发送关闭信号。
/// 关闭信号仅由 stop() 统一触发,避免正常消息处理时误关连接。
pub async fn register_connection( pub async fn register_connection(
&self, &self,
session_id: impl Into<String>, session_id: impl Into<String>,
connection_id: impl Into<String>, connection_id: impl Into<String>,
sender: mpsc::Sender<WsOutbound>, sender: mpsc::Sender<WsOutbound>,
) -> CancellationToken { ) {
self.connections.write().await.insert( let session_id = session_id.into();
session_id.into(), let connection_id = connection_id.into();
let previous = self.connections.write().await.insert(
session_id.clone(),
CliConnection { CliConnection {
connection_id: connection_id.into(), connection_id: connection_id.clone(),
sender, sender,
}, },
); );
self.shutdown_token.clone()
if previous.is_some() {
tracing::info!(session_id = %session_id, connection_id = %connection_id, "CLI session sender replaced");
}
} }
pub async fn unregister_connection(&self, connection_id: &str) { pub async fn unregister_connection(&self, connection_id: &str) {
@ -80,11 +77,7 @@ impl Channel for CliChannel {
} }
async fn stop(&self) -> Result<(), ChannelError> { async fn stop(&self) -> Result<(), ChannelError> {
// 先 cancel 所有 handle_socket再清空 connections
self.shutdown_token.cancel();
let count = self.connections.read().await.len();
self.connections.write().await.clear(); self.connections.write().await.clear();
tracing::info!(connection_count = count, "CliChannel stopped, all connections signaled to close");
Ok(()) Ok(())
} }
@ -118,7 +111,7 @@ mod tests {
async fn test_cli_channel_sends_to_registered_session() { async fn test_cli_channel_sends_to_registered_session() {
let channel = CliChannel::new(); let channel = CliChannel::new();
let (sender, mut receiver) = mpsc::channel(4); let (sender, mut receiver) = mpsc::channel(4);
let _token = channel channel
.register_connection("session-1", "conn-1", sender) .register_connection("session-1", "conn-1", sender)
.await; .await;
@ -142,7 +135,7 @@ mod tests {
async fn test_cli_channel_unregisters_connection_sessions() { async fn test_cli_channel_unregisters_connection_sessions() {
let channel = CliChannel::new(); let channel = CliChannel::new();
let (sender, _receiver) = mpsc::channel(4); let (sender, _receiver) = mpsc::channel(4);
let _token = channel channel
.register_connection("session-1", "conn-1", sender) .register_connection("session-1", "conn-1", sender)
.await; .await;
channel.unregister_connection("conn-1").await; channel.unregister_connection("conn-1").await;
@ -161,36 +154,4 @@ mod tests {
assert!(error.to_string().contains("No active CLI connection")); assert!(error.to_string().contains("No active CLI connection"));
} }
#[tokio::test]
async fn test_stop_cancels_token() {
let channel = CliChannel::new();
let (sender, _receiver) = mpsc::channel(4);
let token = channel
.register_connection("session-1", "conn-1", sender)
.await;
assert!(!token.is_cancelled());
channel.stop().await.unwrap();
assert!(token.is_cancelled());
}
#[tokio::test]
async fn test_register_connection_does_not_cancel_old_token() {
// 验证register_connection 替换旧连接时不会 cancel token
// 只有 stop() 才会 cancel
let channel = CliChannel::new();
let (sender1, _receiver1) = mpsc::channel(4);
let token1 = channel
.register_connection("session-1", "conn-1", sender1)
.await;
let (sender2, _receiver2) = mpsc::channel(4);
let _token2 = channel
.register_connection("session-1", "conn-2", sender2)
.await;
// 旧 token 不应被 cancel只有 stop() 才 cancel
assert!(!token1.is_cancelled());
}
} }

View File

@ -15,8 +15,6 @@ pub(crate) struct AgentFactory {
skills: Arc<SkillRuntime>, skills: Arc<SkillRuntime>,
reinject_every: usize, reinject_every: usize,
prompt_repository: Arc<dyn PromptInjectionRepository>, prompt_repository: Arc<dyn PromptInjectionRepository>,
/// 实例创建时间戳(用于区分新旧 AgentFactory 实例)
instance_id: u64,
} }
pub(crate) struct AgentBuildRequest<'a> { pub(crate) struct AgentBuildRequest<'a> {
@ -39,36 +37,17 @@ impl AgentFactory {
reinject_every: usize, reinject_every: usize,
prompt_repository: Arc<dyn PromptInjectionRepository>, prompt_repository: Arc<dyn PromptInjectionRepository>,
) -> Self { ) -> Self {
// 使用 Arc 指针地址作为实例标识符,用于区分新旧 AgentFactory 实例
let instance_id = Arc::as_ptr(&tools) as u64;
tracing::info!(
instance_id = instance_id,
tool_count = tools.tool_names().len(),
"AgentFactory::new created"
);
Self { Self {
tools, tools,
skills, skills,
reinject_every, reinject_every,
prompt_repository, prompt_repository,
instance_id,
} }
} }
pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result<AgentLoop, AgentError> { pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result<AgentLoop, AgentError> {
let session_id = persistent_session_id(request.channel_name, request.session_chat_id); let session_id = persistent_session_id(request.channel_name, request.session_chat_id);
// 诊断日志:记录 agent 实际使用的配置和实例 ID
tracing::info!(
instance_id = self.instance_id,
channel = %request.channel_name,
session_id = %session_id,
provider = %request.provider_config.name,
model_id = %request.provider_config.model_id,
tool_count = self.tools.tool_names().len(),
"AgentFactory: creating agent with config"
);
// 创建组合的系统提示词提供者 // 创建组合的系统提示词提供者
let system_prompt_provider = Arc::new(CompositeSystemPromptProvider::new(vec![ let system_prompt_provider = Arc::new(CompositeSystemPromptProvider::new(vec![
Box::new(AgentPromptProvider::new( Box::new(AgentPromptProvider::new(

View File

@ -55,15 +55,6 @@ impl SystemPromptProvider for AgentPromptProvider {
return None; return None;
} }
// 诊断日志:记录系统提示词实际使用的模型配置(用于排查"配置不生效"问题)
tracing::info!(
session_id = ?context.session_id,
chat_id = %context.chat_id,
provider = %self.provider_config.name,
model_id = %self.provider_config.model_id,
"AgentPromptProvider: building system prompt with model config"
);
// 加载 Agent 提示词AGENT.md + builtin + MEMORY_SUMMARY.md // 加载 Agent 提示词AGENT.md + builtin + MEMORY_SUMMARY.md
let agent_prompt = load_agent_prompt().ok().flatten()?; let agent_prompt = load_agent_prompt().ok().flatten()?;

View File

@ -6,7 +6,7 @@ use crate::agent::{AgentError, AgentProcessResult, EmittedMessageHandler, Persis
use crate::bus::message::ToolMessageState; use crate::bus::message::ToolMessageState;
use crate::bus::{ChatMessage, MediaItem, OutboundMessage, SYSTEM_CONTEXT_SCHEDULED_PROMPT}; use crate::bus::{ChatMessage, MediaItem, OutboundMessage, SYSTEM_CONTEXT_SCHEDULED_PROMPT};
use crate::config::LLMProviderConfig; use crate::config::LLMProviderConfig;
use crate::storage::{persistent_session_id, ConversationRepository}; use crate::storage::ConversationRepository;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::compaction::schedule_background_history_compaction; use super::compaction::schedule_background_history_compaction;
@ -252,7 +252,7 @@ impl AgentExecutionService {
// 构建系统提示词上下文 // 构建系统提示词上下文
let system_prompt_context = SystemPromptContext { let system_prompt_context = SystemPromptContext {
session_id: Some(persistent_session_id(request.channel_name, request.chat_id)), session_id: Some(format!("{}:{}", request.channel_name, request.chat_id)),
chat_id: request.chat_id.to_string(), chat_id: request.chat_id.to_string(),
user_message_count, user_message_count,
}; };
@ -356,7 +356,7 @@ impl AgentExecutionService {
// 构建系统提示词上下文 // 构建系统提示词上下文
let system_prompt_context = SystemPromptContext { let system_prompt_context = SystemPromptContext {
session_id: Some(persistent_session_id(request.channel_name, request.chat_id)), session_id: Some(format!("{}:{}", request.channel_name, request.chat_id)),
chat_id: request.chat_id.to_string(), chat_id: request.chat_id.to_string(),
user_message_count, user_message_count,
}; };

View File

@ -104,12 +104,6 @@ impl GatewayState {
Some(bus.clone()), Some(bus.clone()),
)?; )?;
// 诊断日志:记录新 GatewayState 的创建(用于排查重启后是否使用了新状态)
tracing::info!(
mcp_manager_present = mcp_manager.is_some(),
"GatewayState::from_config: new GatewayState created"
);
let cancel_manager = CancelManager::new(); let cancel_manager = CancelManager::new();
Ok(Self { Ok(Self {

View File

@ -224,15 +224,6 @@ pub(crate) fn build_session_manager_with_sender(
// Build base tools // Build base tools
let tools = factory.build(); let tools = factory.build();
// 诊断日志:记录 MCP 初始化状态和基础工具数量
tracing::info!(
mcp_enabled = mcp_initializer.is_enabled(),
mcp_tools_collected = mcp_tools_for_subagents.len(),
base_tool_count = tools.tool_names().len(),
base_tools = ?tools.tool_names(),
"build_session_manager: base tools built (before MCP registration)"
);
// Register MCP tools to main agent (async) // Register MCP tools to main agent (async)
// Note: MCP tools for subagents are already collected above // Note: MCP tools for subagents are already collected above
if mcp_initializer.is_enabled() { if mcp_initializer.is_enabled() {
@ -249,13 +240,6 @@ pub(crate) fn build_session_manager_with_sender(
let tools = Arc::new(tools); let tools = Arc::new(tools);
// 诊断日志:记录最终工具数量(含 MCP 工具,如果启用)
tracing::info!(
final_tool_count = tools.tool_names().len(),
final_tools = ?tools.tool_names(),
"build_session_manager: final tools registered to main agent"
);
let prompt_repository: Arc<dyn PromptInjectionRepository> = store.clone(); let prompt_repository: Arc<dyn PromptInjectionRepository> = store.clone();
let agent_factory = AgentFactory::new( let agent_factory = AgentFactory::new(
tools.clone(), tools.clone(),

View File

@ -28,7 +28,6 @@ use crate::command::handlers::switch_topic::SwitchTopicCommandHandler;
use crate::gateway::agent_prompt_provider::AgentPromptProvider; use crate::gateway::agent_prompt_provider::AgentPromptProvider;
use crate::protocol::{WsInbound, WsOutbound, MediaSummary, parse_inbound, serialize_outbound}; use crate::protocol::{WsInbound, WsOutbound, MediaSummary, parse_inbound, serialize_outbound};
use crate::skills::SkillPromptProvider; use crate::skills::SkillPromptProvider;
use crate::storage::persistent_session_id;
use crate::tools::task::repository::TaskRepository; use crate::tools::task::repository::TaskRepository;
use crate::tools::task::types::TaskSessionState; use crate::tools::task::types::TaskSessionState;
use axum::extract::State; use axum::extract::State;
@ -158,10 +157,9 @@ async fn handle_socket(ws: WebSocket, state: Arc<GatewayState>) {
}; };
let runtime_session_id = uuid::Uuid::new_v4().to_string(); let runtime_session_id = uuid::Uuid::new_v4().to_string();
// 清理数据库中可能已被污染的多重前缀 session_id幂等处理 let mut current_session_id = initial_record.id.clone();
let mut current_session_id = persistent_session_id(WS_CHANNEL_NAME, &initial_record.id);
let mut current_topic_id: Option<String> = None; let mut current_topic_id: Option<String> = None;
let shutdown_token = state state
.channel_manager .channel_manager
.websocket_channel() .websocket_channel()
.register_connection( .register_connection(
@ -234,18 +232,7 @@ async fn handle_socket(ws: WebSocket, state: Arc<GatewayState>) {
} }
}); });
loop { while let Some(msg) = ws_receiver.next().await {
tokio::select! {
// 监听 shutdown 信号(来自 CliChannel::stop()
_ = shutdown_token.cancelled() => {
tracing::info!(session_id = %current_session_id, "WebSocket shutdown signal received, closing connection");
break;
}
// 监听 WebSocket 消息
msg = ws_receiver.next() => {
let Some(msg) = msg else {
break;
};
match msg { match msg {
Ok(WsMessage::Text(text)) => { Ok(WsMessage::Text(text)) => {
let text = text.to_string(); let text = text.to_string();
@ -291,8 +278,6 @@ async fn handle_socket(ws: WebSocket, state: Arc<GatewayState>) {
_ => {} _ => {}
} }
} }
}
}
state state
.channel_manager .channel_manager
@ -322,7 +307,7 @@ async fn handle_inbound(
let chat_id = chat_id.unwrap_or_else(|| current_session_id.clone()); let chat_id = chat_id.unwrap_or_else(|| current_session_id.clone());
let sender_id = resolve_ws_sender_id(sender_id.as_deref(), runtime_session_id); let sender_id = resolve_ws_sender_id(sender_id.as_deref(), runtime_session_id);
let _ = state state
.channel_manager .channel_manager
.websocket_channel() .websocket_channel()
.register_connection( .register_connection(
@ -494,7 +479,7 @@ async fn handle_inbound(
"Updating current_session_id" "Updating current_session_id"
); );
*current_session_id = session_id.clone(); *current_session_id = session_id.clone();
let _ = state state
.channel_manager .channel_manager
.websocket_channel() .websocket_channel()
.register_connection( .register_connection(

View File

@ -182,8 +182,8 @@ impl McpClientManager {
let transport = config.transport().map_err(|e| anyhow::anyhow!("{}", e))?; let transport = config.transport().map_err(|e| anyhow::anyhow!("{}", e))?;
let client = match transport { let client = match transport {
McpTransportConfig::Stdio { command, args, env, cwd } => { McpTransportConfig::Stdio { command, args, env } => {
self.connect_stdio(&command, &args, &env, &cwd).await? self.connect_stdio(&command, &args, &env).await?
} }
McpTransportConfig::Http { url, headers } => { McpTransportConfig::Http { url, headers } => {
self.connect_http(&url, &headers).await? self.connect_http(&url, &headers).await?
@ -222,7 +222,6 @@ impl McpClientManager {
command: &str, command: &str,
args: &[String], args: &[String],
env: &HashMap<String, String>, env: &HashMap<String, String>,
_cwd: &Option<std::path::PathBuf>,
) -> anyhow::Result<McpClient> { ) -> anyhow::Result<McpClient> {
let mut cmd = Command::new(command); let mut cmd = Command::new(command);
cmd.args(args); cmd.args(args);

View File

@ -63,9 +63,6 @@ pub struct McpServerConfig {
/// Environment variables for stdio transport /// Environment variables for stdio transport
#[serde(default)] #[serde(default)]
pub env: Option<HashMap<String, String>>, pub env: Option<HashMap<String, String>>,
/// Working directory for stdio transport (optional). If set, child process runs in this dir.
#[serde(default)]
pub cwd: Option<String>,
// HTTP transport fields // HTTP transport fields
/// Base URL for HTTP transport (Claude Desktop compatible naming) /// Base URL for HTTP transport (Claude Desktop compatible naming)
@ -102,7 +99,6 @@ impl McpServerConfig {
command, command,
args: self.args.clone().unwrap_or_default(), args: self.args.clone().unwrap_or_default(),
env: self.env.clone().unwrap_or_default(), env: self.env.clone().unwrap_or_default(),
cwd: self.cwd.as_ref().map(|s| std::path::PathBuf::from(s)),
}) })
} }
"http" | "streamableHttp" => { "http" | "streamableHttp" => {
@ -128,7 +124,6 @@ impl McpServerConfig {
command: Some(command.into()), command: Some(command.into()),
args: Some(args), args: Some(args),
env: Some(HashMap::new()), env: Some(HashMap::new()),
cwd: None,
base_url: None, base_url: None,
headers: None, headers: None,
description: None, description: None,
@ -144,7 +139,6 @@ impl McpServerConfig {
command: None, command: None,
args: None, args: None,
env: None, env: None,
cwd: None,
base_url: Some(url.into()), base_url: Some(url.into()),
headers: Some(HashMap::new()), headers: Some(HashMap::new()),
description: None, description: None,
@ -160,7 +154,6 @@ pub enum McpTransportConfig {
command: String, command: String,
args: Vec<String>, args: Vec<String>,
env: HashMap<String, String>, env: HashMap<String, String>,
cwd: Option<std::path::PathBuf>,
}, },
/// HTTP transport: connect to a remote server (Streamable HTTP) /// HTTP transport: connect to a remote server (Streamable HTTP)
Http { Http {
@ -307,7 +300,6 @@ mod tests {
command: Some("npx".to_string()), command: Some("npx".to_string()),
args: Some(vec!["-y".to_string(), "server".to_string()]), args: Some(vec!["-y".to_string(), "server".to_string()]),
env: None, env: None,
cwd: None,
base_url: None, base_url: None,
headers: None, headers: None,
description: None, description: None,
@ -325,7 +317,6 @@ mod tests {
command: Some("npx".to_string()), command: Some("npx".to_string()),
args: Some(vec!["-y".to_string(), "server".to_string()]), args: Some(vec!["-y".to_string(), "server".to_string()]),
env: None, env: None,
cwd: None,
base_url: None, base_url: None,
headers: None, headers: None,
description: None, description: None,
@ -344,7 +335,6 @@ mod tests {
command: None, command: None,
args: None, args: None,
env: None, env: None,
cwd: None,
base_url: None, base_url: None,
headers: None, headers: None,
description: None, description: None,
@ -359,7 +349,6 @@ mod tests {
command: None, command: None,
args: None, args: None,
env: None, env: None,
cwd: None,
base_url: None, base_url: None,
headers: None, headers: None,
description: None, description: None,
@ -374,7 +363,6 @@ mod tests {
command: Some("cmd".to_string()), command: Some("cmd".to_string()),
args: None, args: None,
env: None, env: None,
cwd: None,
base_url: None, base_url: None,
headers: None, headers: None,
description: None, description: None,
@ -397,27 +385,4 @@ mod tests {
assert!(matches!(transport_http, McpTransportConfig::Http { .. })); assert!(matches!(transport_http, McpTransportConfig::Http { .. }));
assert!(matches!(transport_streamable, McpTransportConfig::Http { .. })); assert!(matches!(transport_streamable, McpTransportConfig::Http { .. }));
} }
#[test]
fn test_stdio_cwd_field() {
let json = r#"{"mcpServers": {"test": {"type": "stdio", "command": "uv", "args": ["run", "server.py"], "cwd": "/some/path", "isActive": true}}}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
let server = config.mcp_servers.get("test").unwrap();
assert_eq!(server.cwd.as_deref(), Some("/some/path"));
let transport = server.transport().unwrap();
match transport {
McpTransportConfig::Stdio { cwd, .. } => {
assert_eq!(cwd, Some(std::path::PathBuf::from("/some/path")));
}
_ => panic!("Expected stdio transport"),
}
}
#[test]
fn test_stdio_cwd_defaults_to_none() {
let json = r#"{"mcpServers": {"test": {"type": "stdio", "command": "uv", "args": ["run", "server.py"]}}}"#;
let config: McpConfig = serde_json::from_str(json).unwrap();
let server = config.mcp_servers.get("test").unwrap();
assert!(server.cwd.is_none());
}
} }

View File

@ -1601,13 +1601,6 @@ impl SessionStore {
} }
pub fn persistent_session_id(channel_name: &str, chat_id: &str) -> String { pub fn persistent_session_id(channel_name: &str, chat_id: &str) -> String {
// 幂等:循环去除已存在的 "{channel_name}:" 前缀,防止前缀累积
let prefix = format!("{}:", channel_name);
let mut chat_id = chat_id;
while chat_id.starts_with(&prefix) {
chat_id = &chat_id[prefix.len()..];
}
if channel_name == "cli" || channel_name == "websocket" { if channel_name == "cli" || channel_name == "websocket" {
chat_id.to_string() chat_id.to_string()
} else { } else {
@ -2295,12 +2288,8 @@ mod tests {
#[test] #[test]
fn test_persistent_session_id_for_cli_and_channel() { fn test_persistent_session_id_for_cli_and_channel() {
assert_eq!(persistent_session_id("cli", "abc"), "abc"); assert_eq!(persistent_session_id("cli", "abc"), "abc");
// 幂等:已带前缀的 chat_id 会被清理,不会累积前缀 assert_eq!(persistent_session_id("websocket", "websocket:abc"), "websocket:abc");
assert_eq!(persistent_session_id("websocket", "websocket:abc"), "abc");
assert_eq!(persistent_session_id("websocket", "websocket:websocket:abc"), "abc");
assert_eq!(persistent_session_id(TEST_CHANNEL, "abc"), "test-channel:abc"); assert_eq!(persistent_session_id(TEST_CHANNEL, "abc"), "test-channel:abc");
// 其他通道也幂等
assert_eq!(persistent_session_id(TEST_CHANNEL, "test-channel:abc"), "test-channel:abc");
} }
#[test] #[test]