From cc5d4a219bc371e8d3d7ca929a76397afed6ba9b Mon Sep 17 00:00:00 2001 From: lanrtop Date: Mon, 29 Jun 2026 20:33:56 +0900 Subject: [PATCH] =?UTF-8?q?feat(agent-infra):=20monorepo=20=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E6=94=AF=E6=8C=81=E2=80=94=E2=80=94=E5=AD=90=E5=8C=85?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E9=87=87=E9=9B=86=20+=20apps/*=20=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/agent_infra.rs | 120 ++++++++++++++++++++------ 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/commands/agent_infra.rs b/src-tauri/src/commands/agent_infra.rs index 73fc373..4d8f8b6 100644 --- a/src-tauri/src/commands/agent_infra.rs +++ b/src-tauri/src/commands/agent_infra.rs @@ -34,9 +34,9 @@ pub struct AgentInfraStack { // ── package.json 解析 ───────────────────────────────────────────────────────── -/// 从 package.json 提取 dependencies + devDependencies 的所有 key。 -fn pkg_json_dep_keys(root: &Path) -> Vec { - 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 { + 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 { keys } +/// 收集根目录 + monorepo 子包(apps/*、packages/*)的所有依赖 key。 +fn collect_all_pkg_deps(root: &Path) -> Vec { + 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 { + 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 { let mut contracts: Vec = 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 { // Drizzle ORM if pkg_deps.iter().any(|k| k == "drizzle-orm") { - let p = if path_exists(root, "drizzle/schema.ts") { - "drizzle/schema.ts" - } else if path_exists(root, "src/db/schema.ts") { - "src/db/schema.ts" - } else { - "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".into() + } else { + "drizzle/schema.ts".into() + } + }); contracts.push(DetectedContract { label: "数据库 schema(Drizzle)".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 { // Hono if pkg_deps.iter().any(|k| k == "hono") { - let p = if path_exists(root, "src/routes") { - "src/routes/**" - } else { - "src/index.ts" - }; + // 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".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 { // pnpm monorepo 共享类型 if path_exists(root, "pnpm-workspace.yaml") { - let p = if path_exists(root, "packages/types") { - "packages/types/**" + // 优先已知命名约定;否则列出 packages/ 下实际存在的子包目录 + let paths: Vec = 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 = 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(), }); }