use crate::{db, db::log_event}; use serde_json::{json, Value}; use std::path::PathBuf; const MCP_KEY_PREFIX: &str = "lian-jing:group-"; // ── 路径解析 ────────────────────────────────────────────────────────────────── /// 解析项目根目录路径(WSL 项目自动转为 Windows UNC 路径) fn project_root(project_id: &str) -> Result { let conn = db::pool().get().map_err(|e| e.to_string())?; let row: rusqlite::Result<(Option, Option, Option)> = conn.query_row( "SELECT win_path, wsl_path, platform FROM project_workspaces WHERE id = ?1", [project_id], |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)), ); let (win_path, wsl_path, platform) = row.map_err(|_| format!("项目 {} 不存在", project_id))?; if platform.as_deref() == Some("wsl") { let wsl = wsl_path.ok_or_else(|| "WSL 项目未配置 wsl_path".to_string())?; let distro: String = conn .query_row( "SELECT value FROM settings WHERE key = 'wsl_distro'", [], |r| r.get(0), ) .unwrap_or_else(|_| "Ubuntu".to_string()); let inner = wsl.trim_start_matches('/').replace('/', "\\"); Ok(PathBuf::from(format!( "\\\\wsl.localhost\\{}\\{}", distro, inner ))) } else { let win = win_path.ok_or_else(|| "Windows 项目未配置 win_path".to_string())?; Ok(PathBuf::from(win)) } } fn settings_path(project_id: &str) -> Result { Ok(project_root(project_id)?.join(".claude").join("settings.json")) } fn liangjing_path(project_id: &str) -> Result { Ok(project_root(project_id)?.join(".claude").join("liangjing.json")) } /// 写炼境上下文文件,供子项目 Claude 会话直接读取 project_id / group_id fn write_liangjing_context(project_id: &str, group_id: &str, port: u16) -> Result<(), String> { let path = liangjing_path(project_id)?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("创建 .claude 目录失败: {}", e))?; } let ctx = json!({ "project_id": project_id, "group_id": group_id, "mcp_port": port }); let content = serde_json::to_string_pretty(&ctx).map_err(|e| e.to_string())?; std::fs::write(&path, content) .map_err(|e| format!("写入 liangjing.json 失败: {}", e)) } /// 查找 claude CLI 路径(优先环境变量 CLAUDE_BIN,然后常用安装目录,最后 where 命令) fn find_claude_bin() -> Option { if let Ok(path) = std::env::var("CLAUDE_BIN") { if std::path::Path::new(&path).exists() { return Some(path); } } let home = std::env::var("USERPROFILE") .or_else(|_| std::env::var("HOME")) .unwrap_or_default(); let candidates = [ format!(r"{}\.local\bin\claude.exe", home), format!(r"{}\AppData\Local\AnthropicClaude\claude.exe", home), r"C:\Program Files\AnthropicClaude\claude.exe".to_string(), ]; for c in &candidates { if std::path::Path::new(c).exists() { return Some(c.clone()); } } // 最后尝试 where 命令(GUI 进程 PATH 可能不含用户 bin,but worth trying) if let Ok(out) = std::process::Command::new("where").arg("claude.exe").output() { if out.status.success() { if let Ok(stdout) = String::from_utf8(out.stdout) { if let Some(line) = stdout.lines().next() { return Some(line.trim().to_string()); } } } } None } /// 将产品组 MCP 注册到 Claude 全局配置(~/.claude.json),best-effort,失败仅记录日志 fn try_register_globally(group_id: &str, port: u16) { let server_name = format!("lian-jing-{}", group_id); let server_url = format!("http://127.0.0.1:{}/mcp/group/{}", port, group_id); let Some(claude_bin) = find_claude_bin() else { db::log_event( "mcp_inject", "warn", "claude CLI 未找到,跳过全局 Mcp 注册", None, ); return; }; let git_bash = std::env::var("CLAUDE_CODE_GIT_BASH_PATH").unwrap_or_else(|_| { r"C:\Program Files\Git\bin\bash.exe".to_string() }); match std::process::Command::new(&claude_bin) .args([ "mcp", "add", "--transport", "sse", "-s", "user", &server_name, &server_url, ]) .env("CLAUDE_CODE_GIT_BASH_PATH", &git_bash) .output() { Ok(out) if out.status.success() => { db::log_event( "mcp_inject", "info", "全局 MCP 注册成功", Some(&server_name), ); } Ok(out) => { let msg = format!( "claude mcp add 非零退出: {}", String::from_utf8_lossy(&out.stderr) ); db::log_event("mcp_inject", "warn", &msg, Some(&server_name)); } Err(e) => { db::log_event( "mcp_inject", "warn", &format!("执行 claude mcp add 失败: {}", e), Some(&server_name), ); } } } // ── JSON 读写 ───────────────────────────────────────────────────────────────── fn read_json(path: &PathBuf) -> Result { if !path.exists() { return Ok(json!({})); } let content = std::fs::read_to_string(path) .map_err(|e| format!("读取 settings.json 失败: {}", e))?; if content.trim().is_empty() { return Ok(json!({})); } serde_json::from_str(&content).map_err(|e| { format!( "settings.json 格式错误,请手动修复后重试({}): {}", path.display(), e ) }) } fn write_json(path: &PathBuf, value: &Value) -> Result<(), String> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("创建 .claude 目录失败: {}", e))?; } let content = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?; std::fs::write(path, content).map_err(|e| format!("写入 settings.json 失败: {}", e)) } // ── 公开接口 ────────────────────────────────────────────────────────────────── /// 向项目的 .claude/settings.json 注入产品组 MCP 配置 pub fn inject_project_mcp(project_id: &str, group_id: &str) -> Result<(), String> { let port = crate::mcp_server::read_port(); let path = settings_path(project_id)?; let mut settings = read_json(&path)?; let obj = settings .as_object_mut() .ok_or("settings.json 顶层必须是 JSON 对象")?; // 炼境管理的项目需要无人值守运行(/ship 等技能不应被 permission 弹窗打断) obj.entry("permissions") .or_insert(json!({"defaultMode": "bypassPermissions"})); let servers = obj.entry("mcpServers").or_insert(json!({})); let key = format!("{}{}", MCP_KEY_PREFIX, group_id); servers[&key] = json!({ "type": "sse", "url": format!("http://127.0.0.1:{}/mcp/group/{}", port, group_id) }); write_json(&path, &settings).inspect(|_| { let ctx = serde_json::json!({"project_id": project_id, "group_id": group_id}).to_string(); log_event("mcp_inject", "info", "MCP 配置注入成功", Some(&ctx)); })?; // 写炼境上下文(含 group_id),让子项目 Claude 会话无需遍历即可知道 project_id + group_id let _ = write_liangjing_context(project_id, group_id, port); // 同步注册到 ~/.claude.json,让 Claude Code 能加载 MCP 工具(best-effort) try_register_globally(group_id, port); Ok(()) } /// 从项目的 .claude/settings.json 移除产品组 MCP 配置 pub fn remove_project_mcp(project_id: &str, group_id: &str) -> Result<(), String> { let path = settings_path(project_id)?; if !path.exists() { return Ok(()); } let mut settings = read_json(&path)?; let key = format!("{}{}", MCP_KEY_PREFIX, group_id); if let Some(servers) = settings .get_mut("mcpServers") .and_then(|v| v.as_object_mut()) { servers.remove(&key); } write_json(&path, &settings) } /// 端口变更后重新注入所有产品组成员的 MCP 配置 /// 返回注入失败的错误列表(部分失败不影响其他项目) pub fn reinject_all_mcp() -> Vec { let conn = match db::pool().get() { Ok(c) => c, Err(e) => return vec![e.to_string()], }; let mappings: Vec<(String, String)> = { let mut stmt = match conn.prepare("SELECT project_id, group_id FROM project_group_map") { Ok(s) => s, Err(e) => return vec![e.to_string()], }; stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?))) .map(|iter| iter.filter_map(|r| r.ok()).collect()) .unwrap_or_default() }; let mut errors = Vec::new(); for (project_id, group_id) in mappings { if let Err(e) = inject_project_mcp(&project_id, &group_id) { errors.push(format!("项目 {}: {}", project_id, e)); } } errors }