//! 声明式工具注册基础设施(tool_registry 拆分第一步) //! //! 背景:`tool_registry.rs` 4224 行,每加一个工具手写一段 //! `registry.register(name, desc, object_schema(...), RiskLevel::X, { ... Box::new ... })`, //! 5 段重复样板 + 闭包捕获 + 缩进极易出错。本模块提供声明式宏 `declare_tool!`, //! 把工具定义收敛为「名字 / 描述 / schema / 风险 / handler 块」五要素一行式声明。 //! //! 设计取舍(三选一权衡): //! - A. proc-macro `#[ai_tool]` 属性宏:需新建 proc-macro crate + syn/quote 重依赖,违反 //! 「不引新依赖」。否决。 //! - B. `inventory` crate 自动收集:新外部依赖 + 拉入 linkme/ctor 运行期注册语义, //! 与现有 `register_*(&mut registry)` 命令式收集并存需双重 source-of-truth。否决。 //! - C. **`macro_rules!` 声明式宏(本方案)**:零新依赖(纯 std `macro_rules!`), //! 展开为等价的 `registry.register(...)` 调用——与现有 48 个 `register` 调用**完全并存**, //! 旧工具零改动,新工具可选声明式。handler 块就地书写,`$db`/`$registry` 等捕获变量 //! 原样透传,语义 1:1 等价(schema/risk/handler 同源不变)。 //! //! 试点:`list_projects` 已迁至声明式(见 commands/ai/tools/list_projects.rs),验证编译过 + //! 行为等价 + 48 工具基线不破。其余 47 个工具后续批次渐进迁移,不在此步。 //! //! 宏展开示例(输入): //! ```ignore //! declare_tool!(registry, db: Arc, "list_projects", //! "列出所有项目...", RiskLevel::Low, //! schema: object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), //! args => { //! let repo = ProjectRepo::new(&db); //! let items = repo.list_active().await?; //! // ... //! Ok(json!({ "items": items, ... })) //! }); //! ``` //! 展开后等价于现有手写的: //! ```ignore //! registry.register( //! "list_projects", "...", //! object_schema(vec![...]), RiskLevel::Low, //! { let db = db.clone(); Box::new(move |args: serde_json::Value| { //! let db = db.clone(); //! Box::pin(async move { /* handler body */ }) //! })}, //! ); //! ``` /// 声明式注册一个 AI 工具到 `$registry`。 /// /// 与手写 `registry.register(name, desc, schema, risk, handler)` 语义 1:1 等价, /// 仅消除闭包包装样板(`{ let db = db.clone(); Box::new(move |args| { let db = db.clone(); Box::pin(async move { ... }) }) }`)。 /// /// 形参: /// - `$registry`: `&mut AiToolRegistry` 注册表句柄。 /// - `$capture: $cap_ty`: handler 需捕获的外部变量(如 `db: Arc`)。宏自动 clone 进闭包, /// handler body 内以 `$capture` 名访问。无捕获工具用 `_` 占位(并保证 body 不引用它)。 /// - `$name`: 工具名 `&str`。 /// - `$desc`: 工具描述 `&str`(发给 LLM)。 /// - `$risk`: `RiskLevel`(如 `RiskLevel::Low`)。 /// - `$schema`: 参数 JSON Schema(`serde_json::Value`),常用 `object_schema(...)`。 /// - `$handler`: 一个 **block 表达式**(花括号体),返回 `anyhow::Result`。 /// 体内在 `$args`(serde_json::Value)与 `$capture`(克隆后的捕获变量)上工作。 /// 宏负责把它包进 `async move { ... }` 并 `Box::pin`——故调用方只写「同步语义的体」, /// 不写 `async move`/`Box::pin`/`Box::new` 三层样板。 /// /// 注意:此宏不替代 `register`,而是包装它——`register` 仍是 `AiToolRegistry` 的唯一注册入口, /// 宏仅是语法糖。现有 48 个手写 `register` 调用不动,新工具改用 `declare_tool!`。 #[macro_export] macro_rules! declare_tool { ( $registry:expr, $capture:ident : $cap_ty:ty, $name:expr, $desc:expr, $risk:expr, schema: $schema:expr, $args:ident => $handler:block ) => {{ let __cap: $cap_ty = $capture.clone(); $registry.register( $name, $desc, $schema, $risk, { let $capture = __cap.clone(); ::std::boxed::Box::new(move |$args: ::serde_json::Value| { let $capture = $capture.clone(); ::std::boxed::Box::pin(async move { let $capture: $cap_ty = $capture; $handler }) as ::std::pin::Pin<::std::boxed::Box< dyn ::std::future::Future> + ::std::marker::Send, >> }) }, ); }}; } #[cfg(test)] mod tests { use crate::ai_tools::{object_schema, AiToolRegistry, RiskLevel}; /// 声明式宏注册的工具与手写 register 行为等价(name/schema/risk/执行结果一致)。 /// 这是「现有工具零回归 + 语义等价」的最小验证(试点 list_projects 迁移的微观镜像)。 #[tokio::test] async fn declare_tool_equivalent_to_register() { let mut reg = AiToolRegistry::new(); // 声明式注册(新基础设施) let counter: std::sync::Arc = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); declare_tool!( reg, counter: std::sync::Arc, "echo_decl", "声明式 echo 工具", RiskLevel::Low, schema: object_schema(vec![("msg", "string", true)]), args => { counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst); let msg = args["msg"].as_str().unwrap_or(""); Ok(serde_json::json!({ "echo": msg })) } ); // 等价断言:定义层(name 存在 / risk 正确 / schema 透传) assert_eq!(reg.len(), 1, "声明式宏应注册 1 个工具"); let tool = reg.get("echo_decl").expect("echo_decl 应已注册"); assert_eq!(tool.risk_level, RiskLevel::Low); assert_eq!(tool.definition.function.name, "echo_decl"); assert_eq!(tool.definition.function.parameters["properties"]["msg"]["type"], "string"); // 执行等价:handler 收到 args、捕获变量可访问、返回 handler body 的 Value let out = reg.execute("echo_decl", serde_json::json!({ "msg": "hi" })).await.unwrap(); assert_eq!(out["echo"], "hi"); assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1, "捕获变量应在执行时递增"); } /// 无捕获工具(纯计算/常量返回)用占位 capture,handler 不引用它。 #[tokio::test] async fn declare_tool_no_capture() { let mut reg = AiToolRegistry::new(); let dummy: std::sync::Arc<()> = std::sync::Arc::new(()); // 无真实捕获,占位 declare_tool!( reg, dummy: std::sync::Arc<()>, "const_tool", "无捕获常量工具", RiskLevel::Medium, schema: object_schema(vec![]), _args => { Ok(serde_json::json!({ "ok": true })) } ); let out = reg.execute("const_tool", serde_json::json!({})).await.unwrap(); assert_eq!(out["ok"], true); } }