feat(mcp): 新增 scan_unregistered_projects 工具,扫描文件系统找出未登记炼境的本地项目
This commit is contained in:
parent
95310890a3
commit
ccbc26acf3
@ -240,6 +240,11 @@ pub fn tools_list_result() -> Value {
|
||||
},
|
||||
"required": ["project_id"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "scan_unregistered_projects",
|
||||
"description": "扫描已登记项目的父目录,找出存在于文件系统但尚未在炼境注册的项目目录。返回每个未注册目录的路径、是否有 .git、技术栈特征文件(package.json / Cargo.toml / go.mod)、是否有 AGENTS.md。用于日常发现漏网项目并决定是否接入炼境。",
|
||||
"inputSchema": { "type": "object", "properties": {}, "required": [] }
|
||||
}
|
||||
]
|
||||
})
|
||||
@ -456,6 +461,7 @@ pub async fn tools_call(group_id: &str, params: Option<&Value>) -> Value {
|
||||
crate::commands::cicd::distribute_workflow_for_project(pid, wt)
|
||||
}
|
||||
"list_all_projects" => list_all_projects(),
|
||||
"scan_unregistered_projects" => scan_unregistered_projects(),
|
||||
"apply_onboarding_pack" => {
|
||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
resolve_project_root(&gid, pid).and_then(|root| {
|
||||
@ -897,6 +903,117 @@ pub fn list_all_projects() -> Result<String, String> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn scan_unregistered_projects() -> Result<String, String> {
|
||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||
|
||||
// 取所有已登记的 win_path
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT win_path FROM project_workspaces WHERE win_path IS NOT NULL")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let registered_paths: std::collections::HashSet<String> = stmt
|
||||
.query_map([], |r| r.get::<_, String>(0))
|
||||
.map_err(|e| e.to_string())?
|
||||
.filter_map(|r| r.ok())
|
||||
.map(|p| p.trim_end_matches(['/', '\\']).to_string())
|
||||
.collect();
|
||||
|
||||
// 推导父目录(去重)
|
||||
let mut parent_dirs: std::collections::HashSet<std::path::PathBuf> =
|
||||
std::collections::HashSet::new();
|
||||
for p in ®istered_paths {
|
||||
if let Some(parent) = std::path::Path::new(p).parent() {
|
||||
parent_dirs.insert(parent.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
if parent_dirs.is_empty() {
|
||||
return Ok("炼境暂无登记项目,无法推导扫描目录。".to_string());
|
||||
}
|
||||
|
||||
let mut unregistered: Vec<(String, bool, Vec<String>, bool)> = Vec::new();
|
||||
|
||||
for parent in &parent_dirs {
|
||||
let entries = std::fs::read_dir(parent).map_err(|e| e.to_string())?;
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let path_str = path.to_string_lossy().trim_end_matches(['/', '\\']).to_string();
|
||||
if registered_paths.contains(&path_str) {
|
||||
continue;
|
||||
}
|
||||
// 隐藏目录跳过
|
||||
if path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| n.starts_with('.'))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let has_git = path.join(".git").exists();
|
||||
let mut stack: Vec<String> = Vec::new();
|
||||
if path.join("package.json").exists() {
|
||||
stack.push("JS/TS".into());
|
||||
}
|
||||
if path.join("Cargo.toml").exists() {
|
||||
stack.push("Rust".into());
|
||||
}
|
||||
if path.join("go.mod").exists() {
|
||||
stack.push("Go".into());
|
||||
}
|
||||
if path.join("pom.xml").exists() {
|
||||
stack.push("Java".into());
|
||||
}
|
||||
let has_agents = path.join("AGENTS.md").exists();
|
||||
|
||||
unregistered.push((path_str, has_git, stack, has_agents));
|
||||
}
|
||||
}
|
||||
|
||||
if unregistered.is_empty() {
|
||||
return Ok(format!(
|
||||
"扫描了 {} 个父目录,所有子目录均已在炼境注册。",
|
||||
parent_dirs.len()
|
||||
));
|
||||
}
|
||||
|
||||
unregistered.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let mut out = format!(
|
||||
"## 未注册目录(共 {} 个)\n\n扫描范围:{}\n\n",
|
||||
unregistered.len(),
|
||||
parent_dirs
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("、")
|
||||
);
|
||||
|
||||
for (path, has_git, stack, has_agents) in &unregistered {
|
||||
let git_icon = if *has_git { "🗂 git" } else { "❌ 无git" };
|
||||
let agents_icon = if *has_agents { "📄 AGENTS" } else { "—" };
|
||||
let stack_str = if stack.is_empty() {
|
||||
"未知".to_string()
|
||||
} else {
|
||||
stack.join("/")
|
||||
};
|
||||
let name = std::path::Path::new(path)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.clone());
|
||||
out.push_str(&format!(
|
||||
"- **{}** | {} | {} | {}\n `{}`\n\n",
|
||||
name, git_icon, stack_str, agents_icon, path
|
||||
));
|
||||
}
|
||||
|
||||
out.push_str("---\n在炼境 UI 添加项目后,调用 `apply_onboarding_pack(project_id)` 分发接入包。");
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ── 测试 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@ -915,6 +1032,10 @@ mod tests {
|
||||
assert!(names.contains(&"apply_onboarding_pack"), "缺少 apply_onboarding_pack");
|
||||
assert!(names.contains(&"verify_onboarding"), "缺少 verify_onboarding");
|
||||
assert!(names.contains(&"list_all_projects"), "缺少 list_all_projects");
|
||||
assert!(
|
||||
names.contains(&"scan_unregistered_projects"),
|
||||
"缺少 scan_unregistered_projects"
|
||||
);
|
||||
assert!(names.contains(&"inject_mcp"), "缺少 inject_mcp");
|
||||
assert!(names.contains(&"create_pull_request"), "缺少 create_pull_request");
|
||||
assert!(names.contains(&"poll_now"), "缺少 poll_now");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user