feat(flywheel): 阶段三执行质量预测层(trailer 死链修复 + 风险画像) #2
@ -240,6 +240,11 @@ pub fn tools_list_result() -> Value {
|
|||||||
},
|
},
|
||||||
"required": ["project_id"]
|
"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)
|
crate::commands::cicd::distribute_workflow_for_project(pid, wt)
|
||||||
}
|
}
|
||||||
"list_all_projects" => list_all_projects(),
|
"list_all_projects" => list_all_projects(),
|
||||||
|
"scan_unregistered_projects" => scan_unregistered_projects(),
|
||||||
"apply_onboarding_pack" => {
|
"apply_onboarding_pack" => {
|
||||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
resolve_project_root(&gid, pid).and_then(|root| {
|
resolve_project_root(&gid, pid).and_then(|root| {
|
||||||
@ -897,6 +903,117 @@ pub fn list_all_projects() -> Result<String, String> {
|
|||||||
Ok(out)
|
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)]
|
#[cfg(test)]
|
||||||
@ -915,6 +1032,10 @@ mod tests {
|
|||||||
assert!(names.contains(&"apply_onboarding_pack"), "缺少 apply_onboarding_pack");
|
assert!(names.contains(&"apply_onboarding_pack"), "缺少 apply_onboarding_pack");
|
||||||
assert!(names.contains(&"verify_onboarding"), "缺少 verify_onboarding");
|
assert!(names.contains(&"verify_onboarding"), "缺少 verify_onboarding");
|
||||||
assert!(names.contains(&"list_all_projects"), "缺少 list_all_projects");
|
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(&"inject_mcp"), "缺少 inject_mcp");
|
||||||
assert!(names.contains(&"create_pull_request"), "缺少 create_pull_request");
|
assert!(names.contains(&"create_pull_request"), "缺少 create_pull_request");
|
||||||
assert!(names.contains(&"poll_now"), "缺少 poll_now");
|
assert!(names.contains(&"poll_now"), "缺少 poll_now");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user