feat(flywheel): 阶段三执行质量预测层(trailer 死链修复 + 风险画像) #2

Merged
lanrtop merged 14 commits from feat/flywheel-intelligence into master 2026-07-04 14:45:28 +08:00
Showing only changes of commit ccbc26acf3 - Show all commits

View File

@ -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 &registered_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");