dev-manager-tauri/src-tauri/src/mcp_inject.rs
lanrtop eb346ae1fd feat: add runtime diagnostics, health center, and project mentor
运行时可观测性:
- 新增 runtime_logs SQLite 表,提供全局 log_event 写入函数(保留最近 500 条)
- mcp_server/mcp_inject/groups 关键路径接入结构化日志
- MCP 新增 get_diagnostics 工具,返回注入摘要与近期错误
- add_group_member 注入失败时前端显示黄色 toast 警告

健康中心:
- 新增 run_health_check 命令(6 项并行检查:MCP/Git 路径/项目路径/GitHub Token/编辑器/DB 延迟)
- 新增质量指标聚合(休眠项目/孤立项目/注入异常组/无路径项目)
- 侧边栏新增健康中心页面,支持手动刷新和诊断报告复制
- 设置页 MCP 状态改为真实 HTTP ping 检测

项目导师:
- 新增 get_mentor_context 后端命令,聚合健康状态/蓝图摘要/近期错误
- MCP 新增 get_project_mentor_context 工具(含产品组归属校验)
- 项目卡片底部新增 🎓 导师按钮,面板支持复制结构化 Markdown 报告
- 修复 WSL 项目路径识别:wsl_path 存 UNC 格式时自动提取 distro 和 Linux 路径,改用 wsl 命令检查路径存在性(规避 Path::exists() 对 \wsl.localhost\... 不可靠的问题)

AI 工作流:
- 新增 AI-WORKFLOW.md、.claude/skills/ 技能体系配置
- 更新 CLAUDE.md 补充蓝图驱动开发规范
2026-04-04 19:58:49 +09:00

146 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<PathBuf, String> {
let conn = db::pool().get().map_err(|e| e.to_string())?;
let row: rusqlite::Result<(Option<String>, Option<String>, Option<String>)> = 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<PathBuf, String> {
Ok(project_root(project_id)?.join(".claude").join("settings.json"))
}
// ── JSON 读写 ─────────────────────────────────────────────────────────────────
fn read_json(path: &PathBuf) -> Result<Value, String> {
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 servers = settings
.as_object_mut()
.ok_or("settings.json 顶层必须是 JSON 对象")?
.entry("mcpServers")
.or_insert(json!({}));
let key = format!("{}{}", MCP_KEY_PREFIX, group_id);
servers[&key] = json!({
"type": "http",
"url": format!("http://localhost:{}/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));
})
}
/// 从项目的 .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<String> {
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
}