新增: 本地工具代理网关(AI 工具统一走 127.0.0.1:8899 转发 mysql/ssh/mongo/redis)

This commit is contained in:
lxy
2026-08-08 14:19:03 +08:00
parent 75d5a7091a
commit c1f2fe16c8
8 changed files with 823 additions and 10 deletions
+50 -9
View File
@@ -500,11 +500,17 @@ pub(crate) async fn bind_dir_to_project(
/// (agent_max_iterations 等)+ db。因本函数在 AppState::init 内、AppState 构建中被调,
/// 无法传 &AppState(自引用),故把所需 Arc 句柄打包成 GetAppConfigCtx 传入(state.rs
/// init 先建这些 Arc 再 build registry 再组装 state,顺序调整)。
///
/// local_proxy(2026-08-06):新增 `local_proxy_ctx: super::tools::local_proxy::LocalProxyCtx` 形参,
/// 该工具集操作本地 4 个工具代理(mysql/ssh/mongo/redis),经 DevFlow 内嵌统一网关
/// (gateway_url 默认 http://127.0.0.1:8899,DF_LOCAL_PROXY_BASE 可覆盖),无 SSRF 面故直接
/// reqwest,不持 db/路径。声明式注册在 tools/local_proxy.rs(11 个工具)。
pub fn build_ai_tool_registry(
db: &Arc<Database>,
allowed_dirs: &Arc<RwLock<AllowedDirs>>,
data_dir: PathBuf,
get_app_config_ctx: super::get_app_config::GetAppConfigCtx,
local_proxy_ctx: super::tools::local_proxy::LocalProxyCtx,
) -> AiToolRegistry {
let mut registry = AiToolRegistry::new();
register_data_tools(&mut registry, db);
@@ -514,6 +520,7 @@ pub fn build_ai_tool_registry(
register_fetch_search_tool(&mut registry);
register_generate_image_tool(&mut registry, db, allowed_dirs);
register_get_app_config_tool(&mut registry, get_app_config_ctx);
register_local_proxy_tools(&mut registry, local_proxy_ctx);
registry
}
@@ -606,6 +613,21 @@ fn register_get_app_config_tool(
super::tools::get_app_config::register(registry, ctx);
}
/// local_proxy 工具代理注册(11 个:mysql/ssh/mongo/redis 本地代理)— thin 委托。
///
/// 经 DevFlow 内嵌统一网关(local_proxy_gateway:127.0.0.1:8899)路由到本地 4 个 HTTP 代理
/// (mysql→3307 / ssh→3308 / mongo→3309 / redis→3310),固定 gateway_url 无 SSRF 面,
/// 声明式注册在 tools/local_proxy.rs。
///
/// risk 口径对齐 http_request:只读查询(Low)/ 数据写入(High)/ 轻量写(Medium)。
/// declare_tool! 单捕获:仅需 gateway_url,包进 LocalProxyCtx struct(Clone 廉价)。
fn register_local_proxy_tools(
registry: &mut AiToolRegistry,
ctx: super::tools::local_proxy::LocalProxyCtx,
) {
super::tools::local_proxy::register(registry, ctx);
}
/// 数据层 AI 工具注册(25 个持 db 的 CRUD/状态机/工作流/知识图谱工具)——从 build_ai_tool_registry 抽出。
///
/// 工具闭包捕获 `db: &Arc<Database>` Arc 重建 Repo(列表/创建/更新/删除/状态推进/工作流/任务关联)。
@@ -1265,6 +1287,14 @@ mod tests {
}
}
/// build_ai_tool_registry 第 5 参(local_proxy 工具代理 ctx),测试调用共用。
/// gateway_url 用默认本地网关即可,handler 不实际执行无需真实代理。
fn make_test_local_proxy_ctx() -> crate::commands::ai::tools::local_proxy::LocalProxyCtx {
crate::commands::ai::tools::local_proxy::LocalProxyCtx {
gateway_url: "http://127.0.0.1:8899".to_string(),
}
}
// ============================================================
// 工具注册基线测试(SMELL-P0-2 拆分防护)
//
@@ -1273,7 +1303,7 @@ mod tests {
// 任一层漏移 register 调用,此测试立即红。工具名集合也断言,防 rename 致 LLM tool 突变。
// ============================================================
/// build_ai_tool_registry 应注册恰好 55 个工具(36 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config),且工具名集合稳定。
/// build_ai_tool_registry 应注册恰好 66 个工具(36 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config + 11 local_proxy),且工具名集合稳定。
///
/// 用 in-memory SQLite(Database::open_in_memory 自跑迁移),构造零外部依赖的 db,
// 不实际执行任何 handler——仅断言注册阶段的定义完整性,故无需真实数据。
@@ -1284,7 +1314,7 @@ mod tests {
// F-260619-03 Phase A: build_ai_tool_registry 新增 allowed_dirs 形参,
// 测试用 default_with_root(仅 workspace_root),零回归(白名单含 workspace_root)。
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs::default_with_root()));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db), make_test_local_proxy_ctx());
// 总量基线:41(27 data + 13 file + 1 http)。拆分前后必须一致。
// F-260621: file 层 10→11(新增 grep 跨文件内容搜索工具)。
@@ -1326,10 +1356,15 @@ mod tests {
// build_ai_tool_registry 加第 4 参 GetAppConfigCtx 绕 AppState 自引用,declare_tool!
// 单捕获用 struct ctx 包 db + 3 agent Arc + LlmConcurrency,共享 AppState 同名句柄)。
// 55 = 36 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config。
// local_proxy(2026-08-06): local_proxy 层 0→11(新增 mysql/ssh/mongo/redis 本地工具代理
// 集成:list_proxy_connections/mysql_query/mysql_execute/ssh_exec/mongo_find/mongo_insert/
// mongo_update/mongo_delete/redis_get/redis_set/redis_del,声明式注册在 tools/local_proxy.rs,
// build_ai_tool_registry 加第 5 参 LocalProxyCtx)。
// 66 = 36 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config + 11 local_proxy。
assert_eq!(
registry.len(),
55,
"工具总数应为 55(36 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config),实际 {}", registry.len()
66,
"工具总数应为 66(36 data + 14 file + 1 http + 1 fetch_url + 1 fetch_search + 1 generate_image + 1 get_app_config + 11 local_proxy),实际 {}", registry.len()
);
// 工具名集合基线:防 rename / 漏注册 / 误删除。
@@ -1379,6 +1414,12 @@ mod tests {
// ── get_app_config 层 (1) ──(查 DevFlow 自身当前 AI 配置,只读 + api_key 脱敏,
// 治 AI 查配置绕 run_command PowerShell 内联脚本引号嵌套失败)
"get_app_config",
// ── local_proxy 层 (11) ──(本地 4 个工具代理:mysql/ssh/mongo/redis,
// 只读查询 Low / 数据写 High / 轻量写 Medium,声明式注册 tools/local_proxy.rs)
"list_proxy_connections", "mysql_query", "mysql_execute",
"ssh_exec", "mongo_find", "mongo_insert",
"mongo_update", "mongo_delete", "redis_get",
"redis_set", "redis_del",
];
expected.sort_unstable();
@@ -1580,7 +1621,7 @@ mod tests {
let db = Database::open_in_memory().await.expect("in-memory db 初始化失败");
let db = Arc::new(db);
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db), make_test_local_proxy_ctx());
let canon_file = file.canonicalize().unwrap().to_string_lossy().to_string();
let args = serde_json::json!({ "path": canon_file, "limit": 15 });
let res = registry.execute("read_file", args).await.expect("read_file 执行失败");
@@ -1607,7 +1648,7 @@ mod tests {
let db = Database::open_in_memory().await.expect("in-memory db 初始化失败");
let db = Arc::new(db);
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db), make_test_local_proxy_ctx());
let canon_file = file.canonicalize().unwrap().to_string_lossy().to_string();
let args = serde_json::json!({ "path": canon_file });
let res = registry.execute("read_file", args).await.expect("read_file 执行失败");
@@ -1686,7 +1727,7 @@ mod tests {
persistent.insert(tmp.clone());
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs { persistent, session: Default::default(), once: Default::default() }));
let db = Arc::new(Database::open_in_memory().await.expect("in-memory db 初始化失败"));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db), make_test_local_proxy_ctx());
let canon_file = file.canonicalize().unwrap().to_string_lossy().to_string();
let args = serde_json::json!({ "path": canon_file });
@@ -1715,7 +1756,7 @@ mod tests {
persistent.insert(tmp.clone());
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs { persistent, session: Default::default(), once: Default::default() }));
let db = Arc::new(Database::open_in_memory().await.expect("in-memory db 初始化失败"));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db), make_test_local_proxy_ctx());
let canon_file = file.canonicalize().unwrap().to_string_lossy().to_string();
let args = serde_json::json!({ "path": canon_file });
@@ -2197,7 +2238,7 @@ mod tests {
.await
.unwrap();
let allowed_dirs = Arc::new(RwLock::new(AllowedDirs::default_with_root()));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db));
let registry = build_ai_tool_registry(&db, &allowed_dirs, PathBuf::from(""), make_test_get_app_config_ctx(&db), make_test_local_proxy_ctx());
(db, registry)
}