feat(agent-infra): monorepo 扫描支持——子包依赖采集 + apps/* 路径检测

This commit is contained in:
lanrtop 2026-06-29 20:33:56 +09:00
parent a0f0cee790
commit cc5d4a219b

View File

@ -34,9 +34,9 @@ pub struct AgentInfraStack {
// ── package.json 解析 ─────────────────────────────────────────────────────────
/// 从 package.json 提取 dependencies + devDependencies 的所有 key。
fn pkg_json_dep_keys(root: &Path) -> Vec<String> {
let content = match std::fs::read_to_string(root.join("package.json")) {
/// 从指定 package.json 文件提取 dependencies + devDependencies 的所有 key。
fn pkg_json_dep_keys_from_file(path: &Path) -> Vec<String> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return vec![],
};
@ -53,6 +53,50 @@ fn pkg_json_dep_keys(root: &Path) -> Vec<String> {
keys
}
/// 收集根目录 + monorepo 子包apps/*、packages/*)的所有依赖 key。
fn collect_all_pkg_deps(root: &Path) -> Vec<String> {
let mut keys = pkg_json_dep_keys_from_file(&root.join("package.json"));
if root.join("pnpm-workspace.yaml").exists() {
for dir in &["apps", "packages"] {
if let Ok(entries) = std::fs::read_dir(root.join(dir)) {
for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let pkg = entry.path().join("package.json");
if pkg.exists() {
keys.extend(pkg_json_dep_keys_from_file(&pkg));
}
}
}
}
}
keys.sort();
keys.dedup();
}
keys
}
/// monorepo 中在 apps/* 子包里找第一个含 sub_path 的目录,返回相对根的路径。
fn find_in_apps(root: &Path, sub_path: &str) -> Option<String> {
let apps_dir = root.join("apps");
if !apps_dir.is_dir() {
return None;
}
let mut names: Vec<_> = std::fs::read_dir(&apps_dir)
.ok()?
.flatten()
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
names.sort();
for name in names {
let rel = format!("apps/{name}/{sub_path}");
if root.join(&rel).exists() {
return Some(rel);
}
}
None
}
/// 从 Cargo.toml 内容字符串中粗略判断某 crate 是否在依赖中。
fn cargo_has(content: &str, crate_name: &str) -> bool {
// 匹配 `crate_name = ...` 或 `crate_name = { ... }` 形式
@ -79,7 +123,7 @@ fn confidence(root: &Path, path: &str) -> String {
fn detect_contracts(root: &Path) -> Vec<DetectedContract> {
let mut contracts: Vec<DetectedContract> = Vec::new();
let pkg_deps = pkg_json_dep_keys(root);
let pkg_deps = collect_all_pkg_deps(root);
// 纯 Rust 项目Cargo.toml 在根Tauri 项目Cargo.toml 在 src-tauri/
let cargo_content = std::fs::read_to_string(root.join("Cargo.toml"))
.or_else(|_| std::fs::read_to_string(root.join("src-tauri/Cargo.toml")))
@ -95,18 +139,24 @@ fn detect_contracts(root: &Path) -> Vec<DetectedContract> {
// Drizzle ORM
if pkg_deps.iter().any(|k| k == "drizzle-orm") {
let p = if path_exists(root, "drizzle/schema.ts") {
"drizzle/schema.ts"
// monorepo优先在 apps/* 子包里找 drizzle 目录
let p: String = find_in_apps(root, "src/drizzle")
.map(|d| format!("{d}/**"))
.or_else(|| find_in_apps(root, "drizzle").map(|d| format!("{d}/**")))
.unwrap_or_else(|| {
if path_exists(root, "drizzle/schema.ts") {
"drizzle/schema.ts".into()
} else if path_exists(root, "src/db/schema.ts") {
"src/db/schema.ts"
"src/db/schema.ts".into()
} else {
"drizzle/schema.ts"
};
"drizzle/schema.ts".into()
}
});
contracts.push(DetectedContract {
label: "数据库 schemaDrizzle".into(),
paths: vec![p.into()],
paths: vec![p.clone()],
doc: "schema 变更需同步更新数据库文档,检查 project-context.yaml".into(),
confidence: confidence(root, p),
confidence: confidence(root, &p),
});
}
@ -123,14 +173,19 @@ fn detect_contracts(root: &Path) -> Vec<DetectedContract> {
// Hono
if pkg_deps.iter().any(|k| k == "hono") {
let p = if path_exists(root, "src/routes") {
"src/routes/**"
// monorepo优先在 apps/* 子包里找 routes 目录
let p: String = find_in_apps(root, "src/routes")
.map(|d| format!("{d}/**"))
.unwrap_or_else(|| {
if path_exists(root, "src/routes") {
"src/routes/**".into()
} else {
"src/index.ts"
};
"src/index.ts".into()
}
});
contracts.push(DetectedContract {
label: "API 接口Hono".into(),
paths: vec![p.into()],
paths: vec![p],
doc: "接口路由变更需同步更新 API 文档或 openapi.yaml".into(),
confidence: "medium".into(),
});
@ -173,17 +228,30 @@ fn detect_contracts(root: &Path) -> Vec<DetectedContract> {
// pnpm monorepo 共享类型
if path_exists(root, "pnpm-workspace.yaml") {
let p = if path_exists(root, "packages/types") {
"packages/types/**"
// 优先已知命名约定;否则列出 packages/ 下实际存在的子包目录
let paths: Vec<String> = if path_exists(root, "packages/types") {
vec!["packages/types/**".into()]
} else if path_exists(root, "packages/shared") {
"packages/shared/**"
vec!["packages/shared/**".into()]
} else if let Ok(entries) = std::fs::read_dir(root.join("packages")) {
let mut dirs: Vec<String> = entries
.flatten()
.filter(|e| {
e.file_type().map(|t| t.is_dir()).unwrap_or(false)
&& e.path().join("package.json").exists()
})
.map(|e| format!("packages/{}/**", e.file_name().to_string_lossy()))
.collect();
dirs.sort();
dirs.truncate(3);
if dirs.is_empty() { vec!["packages/**".into()] } else { dirs }
} else {
"packages/types/**"
vec!["packages/**".into()]
};
contracts.push(DetectedContract {
label: "共享类型pnpm monorepo".into(),
paths: vec![p.into()],
doc: "共享类型只在此处定义,禁止在其他包内手抄".into(),
label: "共享pnpm monorepo".into(),
paths,
doc: "共享包变更影响所有 apps禁止在各 app 内手抄类型".into(),
confidence: "medium".into(),
});
}