运行时可观测性:
- 新增 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 补充蓝图驱动开发规范
146 lines
5.4 KiB
Rust
146 lines
5.4 KiB
Rust
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
|
||
}
|