//! 网络层 AI 工具声明式注册(register_http_tools 1 个:http_request 迁入)。 //! //! 迁自 `tool_registry.rs::register_http_tools`(原 1 个 http_request),改用 `declare_tool!` 宏。 //! //! 迁移策略(handler 逻辑零变更): //! - handler body 逐字照搬原 async move 块(转调 `crate::commands::ai::http::execute_http_request`), //! 仅闭包包装改由 `declare_tool!` 宏生成。 //! - name/desc/schema/risk 与原手写定义逐字一致(headers 是对象 map,object_schema 仅支持扁平 //! 标量三元组,故保留原手工拼 serde_json::Map schema 表达式)。 //! - 无捕获:原 handler 不 clone db(handler 仅转调 http.rs),用占位 `Arc<()>` 捕获(宏要求 capture 形参)。 //! //! 风险:GET=Medium(只读但触发外发)/ 写方法=High(须人工批准),一个工具名两种 risk 不支持(register //! 单一 risk_level),故按最高风险 High 注册(写方法 High 兜底;GET 也走 High 审批更保守)。 //! //! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 + //! 工具名集合稳定(防 rename / 漏注册)。 use std::sync::Arc; use df_ai::ai_tools::{AiToolRegistry, RiskLevel}; use df_ai::declare_tool; /// 注册 http_request 工具到 `$registry`(无 db 捕获,纯 reqwest 调用)。 /// /// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1: /// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义 /// - risk=High(写方法兜底,GET 同走审批更保守) /// - handler body 与原 async move 块逐字一致(逻辑零变更) /// /// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。 pub fn register(registry: &mut AiToolRegistry) { // 无捕获:占位 Arc<()>(原 handler 不持 db,仅转调 http.rs)。 let dummy: Arc<()> = Arc::new(()); // schema:headers 是对象 map,object_schema 仅支持扁平标量三元组,故手工拼 serde_json::Map。 // 提取为 let 绑定:declare_tool! 的 schema: $schema:expr 形参对花括号块表达式解析有歧义, // 先求值到局部变量再传入,语义等价(原手写 register 亦以此 Map 作为 schema 实参)。 let schema = { let mut props = serde_json::Map::new(); props.insert("method".into(), serde_json::json!({ "type": "string", "description": "HTTP 方法:GET/POST/PUT/PATCH/DELETE(默认 GET)", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] })); props.insert("url".into(), serde_json::json!({ "type": "string", "description": "请求 URL,仅 http/https,拒私网/localhost(SSRF 防护)" })); props.insert("headers".into(), serde_json::json!({ "type": "object", "description": "请求头 map,如 {\"Authorization\":\"Bearer xxx\",\"Content-Type\":\"application/json\"}", "additionalProperties": { "type": "string" } })); props.insert("body".into(), serde_json::json!({ "type": "string", "description": "请求体(POST/PUT/PATCH 用),原样发送,Content-Type 须在 headers 显式指定" })); props.insert("timeout_secs".into(), serde_json::json!({ "type": "integer", "description": "超时秒数(默认 30,上限 60)", "minimum": 1, "maximum": 60 })); props.insert("parse".into(), serde_json::json!({ "type": "string", "description": "响应 body 解析:json(pretty 格式化)/text(原样)/auto(按 Content-Type 自动,默认)", "enum": ["json", "text", "auto"] })); serde_json::json!({ "type": "object", "properties": props, "required": ["url"], }) }; declare_tool!( registry, dummy: Arc<()>, "http_request", "发起结构化 HTTP 请求(GET/POST/PUT/PATCH/DELETE),用于查询外部 API。参数:method(默认 GET)、url(http/https)、headers(对象 map)、body(请求体字符串)、timeout_secs(默认 30,上限 60)、parse(json/text/auto,默认 auto)。安全:仅 http/https 协议,拒绝私网/保留 IP(SSRF 防护含 DNS resolve 后校验防重绑定),重定向≤3 跳且每跳重校验。响应 body 截断 50KB。GET 为只读但触发外发网络,POST/PUT/PATCH/DELETE 有副作用,统一按高风险须人工批准", RiskLevel::High, schema: schema, args => { // 转调 http.rs handler(SSRF 防护 + 重定向 + 截断全在那) crate::commands::ai::http::execute_http_request(args).await } ); }