fix(mcp-inject): git-bash 路径多位置探测——修复 Scoop 安装 Git 时 claude CLI 调用全挂
All checks were successful
Push & PR Check / check (push) Successful in 1m32s

迁移实测发现:CLAUDE_CODE_GIT_BASH_PATH 回退路径硬编码
C:\Program Files\Git\bin\bash.exe,Scoop 安装的 Git 不在该位置,
导致 claude mcp add/remove 全部非零退出(存量 bug,组级时代同样存在)。

- find_git_bash():env(校验存在)→ Program Files / Scoop → where git.exe 推导
- try_register_globally 返回 bool;同名 already exists(exit 1)视为成功,
  不再每次 inject 刷 warn 日志
- migrate_to_single_server 标记位改以注册成功为条件——CLI 链路不通时
  不落标记,下次启动重试(幂等)
This commit is contained in:
lanrtop 2026-07-06 20:10:53 +09:00
parent 01eb941ab9
commit 0d1427f4e9

View File

@ -112,13 +112,47 @@ fn find_claude_bin() -> Option<String> {
None None
} }
fn claude_git_bash() -> String { /// 查找 git-bash 路径claude CLI 在 Windows 上必需)。
std::env::var("CLAUDE_CODE_GIT_BASH_PATH") /// 顺序:环境变量(校验存在)→ 常见安装位置Program Files / Scoop→ 从 git.exe 位置推导
.unwrap_or_else(|_| r"C:\Program Files\Git\bin\bash.exe".to_string()) fn find_git_bash() -> Option<String> {
if let Ok(p) = std::env::var("CLAUDE_CODE_GIT_BASH_PATH") {
if std::path::Path::new(&p).exists() {
return Some(p);
}
}
let home = std::env::var("USERPROFILE").unwrap_or_default();
let scoop = std::env::var("SCOOP").unwrap_or_else(|_| format!(r"{}\scoop", home));
let candidates = [
r"C:\Program Files\Git\bin\bash.exe".to_string(),
format!(r"{}\apps\git\current\bin\bash.exe", scoop),
];
for c in &candidates {
if std::path::Path::new(c).exists() {
return Some(c.clone());
}
}
// 从 git.exe 推导:<root>\cmd\git.exe / <root>\mingw64\bin\git.exe → <root>\bin\bash.exe
if let Ok(out) = std::process::Command::new("where").arg("git.exe").output() {
if out.status.success() {
for line in String::from_utf8_lossy(&out.stdout).lines() {
let git = std::path::Path::new(line.trim());
if let Some(root) = git.parent().and_then(|d| d.parent()) {
let bash = root.join("bin").join("bash.exe");
if bash.exists() {
return Some(bash.to_string_lossy().to_string());
}
}
}
}
}
None
} }
/// 将单一全局 lian-jing 服务器注册到 Claude 全局配置(~/.claude.jsonbest-effort失败仅记录日志 /// 将单一全局 lian-jing 服务器注册到 Claude 全局配置(~/.claude.json
fn try_register_globally(port: u16) { /// best-effort失败仅记录日志返回是否成功同名已存在视为成功作为 CLI 链路健康信号)
fn try_register_globally(port: u16) -> bool {
let server_url = format!("http://127.0.0.1:{}/mcp", port); let server_url = format!("http://127.0.0.1:{}/mcp", port);
let Some(claude_bin) = find_claude_bin() else { let Some(claude_bin) = find_claude_bin() else {
@ -128,10 +162,17 @@ fn try_register_globally(port: u16) {
"claude CLI 未找到,跳过全局 MCP 注册", "claude CLI 未找到,跳过全局 MCP 注册",
None, None,
); );
return; return false;
};
let Some(git_bash) = find_git_bash() else {
db::log_event(
"mcp_inject",
"warn",
"git-bash 未找到claude CLI 依赖),跳过全局 MCP 注册",
None,
);
return false;
}; };
let git_bash = claude_git_bash();
match std::process::Command::new(&claude_bin) match std::process::Command::new(&claude_bin)
.args([ .args([
@ -149,13 +190,25 @@ fn try_register_globally(port: u16) {
{ {
Ok(out) if out.status.success() => { Ok(out) if out.status.success() => {
db::log_event("mcp_inject", "info", "全局 MCP 注册成功", Some(MCP_KEY)); db::log_event("mcp_inject", "info", "全局 MCP 注册成功", Some(MCP_KEY));
true
} }
Ok(out) => { Ok(out) => {
let msg = format!( let combined = format!(
"claude mcp add 非零退出: {}", "{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr) String::from_utf8_lossy(&out.stderr)
); );
db::log_event("mcp_inject", "warn", &msg, Some(MCP_KEY)); // 同名已存在exit 1不是故障——CLI 链路健康,注册目标已达成
if combined.contains("already exists") {
return true;
}
db::log_event(
"mcp_inject",
"warn",
&format!("claude mcp add 非零退出: {}", combined),
Some(MCP_KEY),
);
false
} }
Err(e) => { Err(e) => {
db::log_event( db::log_event(
@ -164,6 +217,7 @@ fn try_register_globally(port: u16) {
&format!("执行 claude mcp add 失败: {}", e), &format!("执行 claude mcp add 失败: {}", e),
Some(MCP_KEY), Some(MCP_KEY),
); );
false
} }
} }
} }
@ -175,6 +229,10 @@ pub fn cleanup_legacy_global_registrations() -> Vec<String> {
db::log_event("mcp_inject", "warn", "claude CLI 未找到,跳过旧注册清理", None); db::log_event("mcp_inject", "warn", "claude CLI 未找到,跳过旧注册清理", None);
return Vec::new(); return Vec::new();
}; };
let Some(git_bash) = find_git_bash() else {
db::log_event("mcp_inject", "warn", "git-bash 未找到,跳过旧注册清理", None);
return Vec::new();
};
let Ok(conn) = db::pool().get() else { let Ok(conn) = db::pool().get() else {
return Vec::new(); return Vec::new();
}; };
@ -186,7 +244,6 @@ pub fn cleanup_legacy_global_registrations() -> Vec<String> {
}) })
.unwrap_or_default(); .unwrap_or_default();
let git_bash = claude_git_bash();
let mut removed = Vec::new(); let mut removed = Vec::new();
for gid in &group_ids { for gid in &group_ids {
let mut names = vec![format!("lian-jing-{}", gid)]; let mut names = vec![format!("lian-jing-{}", gid)];
@ -316,22 +373,25 @@ pub fn migrate_to_single_server() {
let errors = reinject_all_mcp(); let errors = reinject_all_mcp();
let removed = cleanup_legacy_global_registrations(); let removed = cleanup_legacy_global_registrations();
let register_ok = try_register_globally(crate::mcp_server::read_port());
let summary = format!( let summary = format!(
"单一服务器迁移完成:重注入失败 {} 个,移除旧全局注册 {} 条{}", "单一服务器迁移完成:重注入失败 {} 个,移除旧全局注册 {} 条,全局注册 {}{}",
errors.len(), errors.len(),
removed.len(), removed.len(),
if register_ok { "成功" } else { "失败" },
if errors.is_empty() { if errors.is_empty() {
String::new() String::new()
} else { } else {
format!(";失败详情: {}", errors.join("; ")) format!(";失败详情: {}", errors.join("; "))
} }
); );
let level = if errors.is_empty() { "info" } else { "warn" }; let level = if errors.is_empty() && register_ok { "info" } else { "warn" };
db::log_event("mcp_inject", level, &summary, None); db::log_event("mcp_inject", level, &summary, None);
// claude CLI 不在时旧全局注册无法清理——不落标记位,等下次启动重试(整个流程幂等) // 全局注册失败说明 claude CLI 链路不通CLI 缺失/git-bash 缺失/命令报错),
if find_claude_bin().is_none() { // 此时旧注册也清不掉——不落标记位,等下次启动重试(整个流程幂等)
if !register_ok {
return; return;
} }
if let Ok(conn) = db::pool().get() { if let Ok(conn) = db::pool().get() {