新增: AI 工具(download_file + http output_file + grep 窗口 + fetch_search 搜索 + get_app_config + generate_image)
download_file 跨平台 URL 到文件流式下载;http_request output_file 落盘 + 截断可配; grep context_chars 大单行窗口;fetch_search DuckDuckGo 免 key 搜索;get_app_config 只读返当前配置(治 AI 查配置绕 PowerShell);generate_image SenseNova 图像生成。
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
//! download_file AI 工具声明式注册(跨平台 URL → 文件流式下载)。
|
||||
//!
|
||||
//! 工具职责:GET 远程 URL → 流式写本地文件(reqwest bytes_stream + tokio::fs)。
|
||||
//! 替代 run_command 的 curl/wget/Invoke-WebRequest(踩 PowerShell alias/编码/跨平台陷阱)。
|
||||
//!
|
||||
//! 风险:High(下载文件副作用:写磁盘 + 触发外发网络 + 来源不可信)。SSRF 防护复用
|
||||
//! commands/ai/http.rs(validate_url / resolve_and_check_host / build_client /
|
||||
//! execute_with_redirects,经 pub(crate) 暴露)。路径校验复用 tool_registry 的
|
||||
//! validate_path + resolve_workspace_path_with_allowed(与 write_file 同款,授权目录白名单)。
|
||||
//!
|
||||
//! handler 在 commands/ai/download_file.rs::execute_download_file,声明式注册收敛样板。
|
||||
//! 闭包捕获 allowed_dirs:Arc<RwLock<AllowedDirs>>(download_file 写文件需路径授权,与 write_file 同源),
|
||||
//! 在闭包内解析 output_path(白名单快照 + symlink 逃逸拦截),把已解析的绝对路径字符串传给 handler。
|
||||
//!
|
||||
//! ## 注册位置说明
|
||||
//!
|
||||
//! download_file 既涉网络(SSRF)又涉文件(路径校验落盘),按「副作用类型」归类:
|
||||
//! 写文件(授权目录白名单)是核心安全约束,与 write_file/patch_file 同源(均持 allowed_dirs),
|
||||
//! 故在 register_file_tools 注册(tool_registry.rs),而非 register_http_tools(后者不传 allowed_dirs)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::commands::ai::tool_registry::resolve_workspace_path_with_allowed;
|
||||
use crate::state::AllowedDirs;
|
||||
|
||||
/// 注册 download_file 工具到 `$registry`。
|
||||
///
|
||||
/// - name/desc/schema 面向 LLM 的工具说明
|
||||
/// - risk=High(写磁盘 + 外发网络 + 来源不可信)
|
||||
/// - handler 捕获 allowed_dirs,解析 output_path 后转调 download_file.rs::execute_download_file
|
||||
/// (SSRF 防护 + 流式写 + 大小上限 + 原子写全在那)
|
||||
pub fn register(
|
||||
registry: &mut AiToolRegistry,
|
||||
allowed_dirs: &Arc<RwLock<AllowedDirs>>,
|
||||
) {
|
||||
// schema:url(必填)+ output_path(必填,落盘路径)+ timeout_secs(可选)。
|
||||
let schema = {
|
||||
let mut props = serde_json::Map::new();
|
||||
props.insert("url".into(), serde_json::json!({ "type": "string", "description": "要下载的文件 URL,仅 http/https(拒私网/localhost,SSRF 防护含 DNS resolve 后校验防重绑定,重定向≤3 跳每跳重校验)" }));
|
||||
props.insert("output_path".into(), serde_json::json!({ "type": "string", "description": "落盘文件路径(相对路径锚定到授权项目根,绝对路径须在授权目录内)。父目录不存在自动创建,覆盖既有同名文件。拒路径遍历(..)与敏感系统目录" }));
|
||||
props.insert("timeout_secs".into(), serde_json::json!({ "type": "integer", "description": "超时秒数(默认 60,上限 600)", "minimum": 1, "maximum": 600 }));
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
"required": ["url", "output_path"],
|
||||
})
|
||||
};
|
||||
declare_tool!(
|
||||
registry,
|
||||
allowed_dirs: Arc<RwLock<AllowedDirs>>,
|
||||
"download_file",
|
||||
"下载远程 URL 文件到本地磁盘(跨平台流式,替代 curl/wget/Invoke-WebRequest 的 alias/编码/跨平台陷阱)。参数:url(http/https,output_path(落盘路径,须在授权目录内),timeout_secs(默认 60,上限 600)。安全:仅 http/https + 拒私网/保留 IP(SSRF 防护)+ 路径白名单(拒遍历/越界)+ 200MB 大小上限(超限中止删半成品)+ 原子写(tmp→rename)。返回 {path, bytes_written, status, url, elapsed_ms}。下载二进制/大文件优先用此工具(不进 prompt)",
|
||||
RiskLevel::High,
|
||||
schema: schema,
|
||||
args => {
|
||||
// 路径解析:取 allowed_dirs 快照,解析 output_path(白名单 + symlink 逃逸拦截)。
|
||||
// 与 write_file 同款:相对路径锚定首个持久授权目录,绝对路径走白名单校验。
|
||||
let snap = allowed_dirs.read().await.clone();
|
||||
let output_path_raw = args["output_path"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("缺少 output_path 参数"))?;
|
||||
let resolved = resolve_workspace_path_with_allowed(output_path_raw, &snap)?;
|
||||
let resolved_str = resolved.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("output_path 含非法字符"))?
|
||||
.to_string();
|
||||
// 转调 download_file.rs handler(SSRF 防护 + 流式写 + 大小上限 + 原子写全在那)
|
||||
crate::commands::ai::download_file::execute_download_file(args, resolved_str).await
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! fetch_search AI 工具声明式注册(搜索引擎查询,DuckDuckGo HTML 免 key)。
|
||||
//!
|
||||
//! 工具职责:GET DuckDuckGo HTML 端点 → 解析结果列表(标题+URL+摘要)→ 截断 max_results
|
||||
//! → 返回 {query, results, count}。补「fetch_url 只抓已知 URL,无搜索能力」缺口:
|
||||
//! 用户给"调研 X"时,LLM 需先搜索找文档入口,再 fetch_url 深读。
|
||||
//!
|
||||
//! 风险:Low(只读 GET 搜索,无副作用,与 fetch_url 同级)。SSRF 防护复用 commands/ai/http.rs
|
||||
//! (validate_url / resolve_and_check_host / build_client / execute_with_redirects)。
|
||||
//! DDG HTML 免 API key(html.duckduckgo.com/html/),返回结果列表 HTML,handler 解析提取。
|
||||
//!
|
||||
//! handler 在 commands/ai/fetch_search.rs::execute_fetch_search,声明式注册收敛样板。
|
||||
//! 无 db 捕获,纯网络 GET + HTML 字符串扫描解析(对齐 fetch_url::extract_title 风格,
|
||||
//! 不引 scraper 重依赖 — html5ever+selectors+cssparser 编译产物大,DDG 单一来源不值)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
|
||||
/// 注册 fetch_search 工具到 `$registry`(无 db 捕获,纯网络 GET + HTML 解析)。
|
||||
///
|
||||
/// - name/desc/schema 面向 LLM 的工具说明
|
||||
/// - risk=Low(只读 GET 搜索,无副作用,与 fetch_url 同级)
|
||||
/// - handler 转调 fetch_search.rs::execute_fetch_search(SSRF 防护 + DDG GET + HTML 解析全在那)
|
||||
pub fn register(registry: &mut AiToolRegistry) {
|
||||
// 无捕获:占位 Arc<()>(handler 不持 db,仅转调 fetch_search.rs)。
|
||||
let dummy: Arc<()> = Arc::new(());
|
||||
// schema:query(必填)+ max_results(可选,默认 5,clamp [1,10])。
|
||||
let schema = {
|
||||
let mut props = serde_json::Map::new();
|
||||
props.insert("query".into(), serde_json::json!({ "type": "string", "description": "搜索关键词(必填)。返回 DuckDuckGo HTML 结果列表(标题+URL+摘要),供挑选入口用 fetch_url 深读" }));
|
||||
props.insert("max_results".into(), serde_json::json!({ "type": "integer", "description": "返回结果数(默认 5,clamp [1, 10])", "minimum": 1, "maximum": 10, "default": 5 }));
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
"required": ["query"],
|
||||
})
|
||||
};
|
||||
declare_tool!(
|
||||
registry,
|
||||
dummy: Arc<()>,
|
||||
"fetch_search",
|
||||
"搜索引擎查询(免 API key),返回 DuckDuckGo 结果列表(标题+URL+摘要)。补「fetch_url 只抓已知 URL」缺口:用户给「调研 X」时,先用本工具找文档入口,再 fetch_url 深读。参数:query(必填,搜索关键词)、max_results(默认 5,clamp [1,10])。安全:只读 GET,SSRF 防护复用 fetch_url/http_request 同套(仅 http/https + 拒私网 + 重定向≤3 跳)。返回 {query, results:[{title,url,snippet}], count, status, elapsed_ms}。每条 url 已解包 DDG 重定向跳板(uddg 参数 percent-decode),可直接 fetch_url",
|
||||
RiskLevel::Low,
|
||||
schema: schema,
|
||||
args => {
|
||||
// 转调 fetch_search.rs handler(SSRF 防护 + DDG GET + HTML 解析 + 截断全在那)
|
||||
crate::commands::ai::fetch_search::execute_fetch_search(args).await
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! generate_image AI 工具声明式注册(调 provider 图像生成端点 + 下载落地)。
|
||||
//!
|
||||
//! 工具职责:调 provider 的 OpenAI 兼容 `/v1/images/generations` 端点生成图片,
|
||||
//! 自动下载到工作区 output_path。默认 model=sensenova-u1-fast(SenseNova U1 信息图生成),
|
||||
//! 也支持 DALL-E 等 OpenAI 兼容图像端点。
|
||||
//!
|
||||
//! 风险:High(付费 API 按张计费 + 写磁盘 + 外发网络)。
|
||||
//!
|
||||
//! 与 download_file 的分工:download_file 是「已知 URL → 落盘」,generate_image 是
|
||||
//! 「调 provider 生成 + 自动落盘」(封装 provider 凭证 + 端点拼接 + 响应解析 + 图片下载全链路)。
|
||||
//! 图像生成模型不是 chat 模型,走 /v1/images/generations 非 chat completions,
|
||||
//! 故不能经主对话链路调用,必须用本工具显式触发。
|
||||
//!
|
||||
//! handler 在 commands/ai/generate_image.rs::execute_generate_image,声明式注册收敛样板。
|
||||
//! 闭包捕获 db:Arc<Database>(选 provider + 解析凭证)+ allowed_dirs:Arc<RwLock<AllowedDirs>>
|
||||
//! (output_path 白名单校验 + 默认路径锚定)。
|
||||
//!
|
||||
//! ## 捕获变量说明(declare_tool! 单捕获限制)
|
||||
//!
|
||||
//! declare_tool! 宏只支持单捕获变量(见 crates/df-ai/src/ai_tools_decl.rs),本工具需 db +
|
||||
//! allowed_dirs 两个,故包进 tuple Arc `(Arc<Database>, Arc<RwLock<AllowedDirs>>)` 在闭包内解构。
|
||||
//! 这是宏单捕获限制的标准绕法(零架构改动:不改宏、不改 process_tool_calls 签名)。
|
||||
//!
|
||||
//! ## 注册位置说明
|
||||
//!
|
||||
//! generate_image 涉付费 API(网络)+ 写文件(落盘 output_path),与 download_file 同源
|
||||
//! (均持 allowed_dirs),归 register_http_tools 同层(register_generate_image_tool 子函数,
|
||||
//! 在 register_http_tools 之后调用)。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::state::AllowedDirs;
|
||||
use df_storage::db::Database;
|
||||
|
||||
/// generate_image 捕获的 (db, allowed_dirs) tuple 类型(declare_tool! 单捕获绕法)。
|
||||
type GenerateImageCtx = (Arc<Database>, Arc<RwLock<AllowedDirs>>);
|
||||
|
||||
/// 注册 generate_image 工具到 `$registry`。
|
||||
///
|
||||
/// - name/desc/schema 面向 LLM 的工具说明
|
||||
/// - risk=High(付费 API + 写磁盘 + 外发网络)
|
||||
/// - handler 捕获 (db, allowed_dirs) tuple Arc,解构后转调 generate_image.rs::execute_generate_image
|
||||
/// (provider 选择 + 凭证解析 + 端点拼接 + SSRF 防护下载 + 原子写全在那)
|
||||
pub fn register(
|
||||
registry: &mut AiToolRegistry,
|
||||
db: &Arc<Database>,
|
||||
allowed_dirs: &Arc<RwLock<AllowedDirs>>,
|
||||
) {
|
||||
// 捕获 tuple:declare_tool! 单捕获限制的绕法。
|
||||
let ctx: GenerateImageCtx = (db.clone(), allowed_dirs.clone());
|
||||
|
||||
// schema:prompt(必填)+ model(默认 sensenova-u1-fast)+ size(可选)+ n(默认 1)
|
||||
// + provider_id(可选)+ output_path(可选)。
|
||||
let schema = {
|
||||
let mut props = serde_json::Map::new();
|
||||
props.insert("prompt".into(), serde_json::json!({ "type": "string", "description": "图片描述(中英文均可),越具体生成质量越高。例:'一只戴墨镜的橘猫坐在键盘上,赛博朋克风格,霓虹光'" }));
|
||||
props.insert("model".into(), serde_json::json!({ "type": "string", "description": "图像生成模型名。默认 sensenova-u1-fast(SenseNova U1 信息图生成,走 /v1/images/generations)。也支持 dall-e-3 等 OpenAI 兼容图像模型。不传用默认", "default": "sensenova-u1-fast" }));
|
||||
props.insert("size".into(), serde_json::json!({ "type": "string", "description": "可选图片尺寸字符串,不传则让 provider 用默认。SenseNova U1 常用 2752x1536 / 1536x2752(横/竖海报),DALL-E 常用 1024x1024" }));
|
||||
props.insert("n".into(), serde_json::json!({ "type": "integer", "description": "生成数量(默认 1,clamp [1,4])。多张时取首张 url/b64 落地", "minimum": 1, "maximum": 4, "default": 1 }));
|
||||
props.insert("provider_id".into(), serde_json::json!({ "type": "string", "description": "可选,指定 provider(多 provider 时);不传则按 model 名自动匹配厂商 host(sensenova→SenseNova,dall-e→OpenAI,google/imagen→Google),匹配不到取首个 openai_compat provider" }));
|
||||
props.insert("output_path".into(), serde_json::json!({ "type": "string", "description": "可选落盘路径(相对路径锚定到授权项目根,绝对路径须在授权目录内)。不传则默认 {首个持久授权目录}/generated_images/{model}-{时间戳}.png。父目录不存在自动创建,覆盖既有同名文件。注意:这是图像生成模型(非对话模型),通过本工具调用而非主对话链路" }));
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
"required": ["prompt"],
|
||||
})
|
||||
};
|
||||
declare_tool!(
|
||||
registry,
|
||||
ctx: GenerateImageCtx,
|
||||
"generate_image",
|
||||
"调用图像生成模型(默认 SenseNova U1 Fast 信息图生成,也支持 DALL-E 等 OpenAI 兼容图像端点)生成图片并下载到工作区。参数:prompt(图片描述,必填),model(默认 sensenova-u1-fast),size(可选,SenseNova U1 常用 2752x1536/1536x2752,DALL-E 常用 1024x1024),n(默认 1,clamp [1,4]),provider_id(可选,多 provider 时指定,否则按 model 名自动匹配厂商),output_path(可选,默认 generated_images/{model}-{时间戳}.png)。注意:本工具调用图像生成模型(非对话模型),走 /v1/images/generations 端点。返回 {path, url(或 null), model, provider_id, bytes_written, elapsed_ms}。风险:付费 API 按张计费 + 写磁盘",
|
||||
RiskLevel::High,
|
||||
schema: schema,
|
||||
args => {
|
||||
// 解构 tuple 捕获:db 选 provider + 解析凭证,allowed_dirs 解析 output_path 白名单
|
||||
let (db, allowed_dirs) = ctx;
|
||||
crate::commands::ai::generate_image::execute_generate_image(args, &db, &allowed_dirs).await
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! get_app_config AI 工具声明式注册(查 DevFlow 自身当前 AI 配置,只读 + 脱敏)。
|
||||
//!
|
||||
//! 工具职责:返当前生效的 provider/default_provider/agent 配置/app_settings 概要 JSON,
|
||||
//! 让 LLM 一调即得「现在用什么 provider/model/迭代上限」,根治 run_command PowerShell
|
||||
//! 内联 node/python 脚本查 db(引号三重嵌套必失败)的绕行。
|
||||
//!
|
||||
//! 风险:Low(纯只读 list/load,无写副作用)。安全:api_key 经 mask_api_key 脱敏
|
||||
//! (前 4 + `••••` + 后 4),绝不返明文;app_settings 走白名单不返敏感 KV。
|
||||
//!
|
||||
//! handler body 在 commands/ai/get_app_config.rs::execute_get_app_config,
|
||||
//! 声明式注册收敛样板(对齐 fetch_url/generate_image 模式)。
|
||||
//!
|
||||
//! ## 捕获变量说明
|
||||
//!
|
||||
//! declare_tool! 宏单捕获限制:本工具需 db + 3 个 agent 配置 Arc + LlmConcurrency 共 5 项,
|
||||
//! 包进 [`GetAppConfigCtx`] struct(各字段 Arc/Clone-廉价),闭包内不解构直接
|
||||
//! `&ctx` 转调 handler。ctx 字段与 AppState 同名字段共享同一底层原子量(热改即反映)。
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
|
||||
use crate::commands::ai::get_app_config::GetAppConfigCtx;
|
||||
|
||||
/// 注册 get_app_config 工具到 `$registry`。
|
||||
///
|
||||
/// - name/desc/schema 面向 LLM 的工具说明(无参数,返全部当前配置)
|
||||
/// - risk=Low(纯只读)
|
||||
/// - handler 捕获 GetAppConfigCtx(共享 AppState 同名 Arc 字段),转调 get_app_config.rs
|
||||
pub fn register(registry: &mut AiToolRegistry, ctx: GetAppConfigCtx) {
|
||||
// schema:无参数(object_schema 空参返仅 type:object 无 required)。
|
||||
// 不加可选 section 参数(provider/agent/settings 选返):返全部配置最简,LLM 自行读所需字段;
|
||||
// 加 section 反增参数解析分支 + schema 复杂度,违背「简洁」约束。
|
||||
let schema = df_ai::ai_tools::object_schema(vec![]);
|
||||
declare_tool!(
|
||||
registry,
|
||||
ctx: GetAppConfigCtx,
|
||||
"get_app_config",
|
||||
"获取 DevFlow 自身当前生效的 AI 配置(只读,无参数)。返回 {default_provider, providers, agent, app_settings}:default_provider 含 name/provider_type/base_url/default_model/is_default/enabled/api_key_masked(api_key 脱敏,空串=未配置 key);providers 是全部 provider 概要列表(无 api_key);agent 含 max_iterations/max_retries/approval_timeout_minutes/concurrency.per_conv(当前热改生效值);app_settings 含知识库/审批超时等 AI 相关 KV(custom_prompt 只返长度不返原文)。查「现在用什么 provider/model」「agent 迭代上限多少」「key 是否配置」用本工具,不要 run_command 执行 PowerShell 内联脚本查 db(引号嵌套易失败)。",
|
||||
RiskLevel::Low,
|
||||
schema: schema,
|
||||
_args => {
|
||||
// 转调 get_app_config.rs handler(providers/settings/agent 配置读取 + 脱敏全在那)
|
||||
crate::commands::ai::get_app_config::execute_get_app_config(&ctx).await
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2,16 +2,22 @@
|
||||
//!
|
||||
//! 迁自 `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!` 宏生成。
|
||||
//! 迁移策略(原 handler 逻辑零变更,2026-08-01 增 output_file/max_response_chars + allowed_dirs 捕获):
|
||||
//! - handler body 转调 `crate::commands::ai::http::execute_http_request`,闭包包装由 `declare_tool!` 宏生成。
|
||||
//! output_file 参数引入后,handler 内增 `allowed_dirs.read().await.clone()` 快照传入(路径校验用,
|
||||
//! 无 output_file 时校验不触发,行为零变更)。
|
||||
//! - name/desc/schema/risk 与原手写定义逐字一致(headers 是对象 map,object_schema 仅支持扁平
|
||||
//! 标量三元组,故保留原手工拼 serde_json::Map schema 表达式)。
|
||||
//! - 无捕获:原 handler 不 clone db(handler 仅转调 http.rs),用占位 `Arc<()>` 捕获(宏要求 capture 形参)。
|
||||
//! 标量三元组,故保留原手工拼 serde_json::Map schema 表达式);新增 output_file/max_response_chars 两 prop。
|
||||
//! - 捕获:handler 不持 db(纯转调 http.rs),但需 allowed_dirs 做 output_file 路径校验,
|
||||
//! 故捕获 `Arc<RwLock<AllowedDirs>>`(宏要求 capture 形参)。
|
||||
//!
|
||||
//! 风险:GET=Medium(只读但触发外发)/ 写方法=High(须人工批准),一个工具名两种 risk 不支持(register
|
||||
//! 单一 risk_level),故按最高风险 High 注册(写方法 High 兜底;GET 也走 High 审批更保守)。
|
||||
//!
|
||||
//! 2026-08-01 `output_file` 参数:响应可落盘(全量写不截断),路径走文件路径校验
|
||||
//! (validate_path + resolve_workspace_path_with_allowed + parent 授权,与 write_file 同源),
|
||||
//! 故 register 增 `allowed_dirs` 捕获,转调 execute_http_request 时读快照传入。
|
||||
//!
|
||||
//! 等价性验证:基线测试 `test_build_ai_tool_registry_baseline_tool_count` 仍断言 48 总量 +
|
||||
//! 工具名集合稳定(防 rename / 漏注册)。
|
||||
|
||||
@@ -19,18 +25,21 @@ use std::sync::Arc;
|
||||
|
||||
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
|
||||
use df_ai::declare_tool;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// 注册 http_request 工具到 `$registry`(无 db 捕获,纯 reqwest 调用)。
|
||||
use crate::state::AllowedDirs;
|
||||
|
||||
/// 注册 http_request 工具到 `$registry`(无 db 捕获,但捕获 allowed_dirs 供 output_file 路径校验)。
|
||||
///
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1:
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义
|
||||
/// 与原手写 register(name, desc, schema, risk, handler) 语义 1:1(2026-08-01 增 output_file /
|
||||
/// max_response_chars 两参数 + allowed_dirs 捕获):
|
||||
/// - name/desc/schema 字符串与 JSON Schema 逐字照搬原定义(新增两 prop)
|
||||
/// - risk=High(写方法兜底,GET 同走审批更保守)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更)
|
||||
/// - handler body 与原 async move 块逐字一致(逻辑零变更),新增 allowed_dirs read lock clone
|
||||
/// 传入 execute_http_request(output_file 路径校验用,无 output_file 时不触发)
|
||||
///
|
||||
/// 唯一差异:闭包包装改由 `declare_tool!` 宏生成,handler body 直接写业务逻辑。
|
||||
pub fn register(registry: &mut AiToolRegistry) {
|
||||
// 无捕获:占位 Arc<()>(原 handler 不持 db,仅转调 http.rs)。
|
||||
let dummy: Arc<()> = Arc::new(());
|
||||
pub fn register(registry: &mut AiToolRegistry, allowed_dirs: &Arc<RwLock<AllowedDirs>>) {
|
||||
// schema:headers 是对象 map,object_schema 仅支持扁平标量三元组,故手工拼 serde_json::Map。
|
||||
// 提取为 let 绑定:declare_tool! 的 schema: $schema:expr 形参对花括号块表达式解析有歧义,
|
||||
// 先求值到局部变量再传入,语义等价(原手写 register 亦以此 Map 作为 schema 实参)。
|
||||
@@ -42,6 +51,8 @@ pub fn register(registry: &mut AiToolRegistry) {
|
||||
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"] }));
|
||||
props.insert("output_file".into(), serde_json::json!({ "type": "string", "description": "响应 body 写入指定文件路径(而非返回 JSON body),适合大响应。路径走文件路径校验 + 授权目录,全量写不截断。提供时返回 body=null + path/bytes_written/truncated=false" }));
|
||||
props.insert("max_response_chars".into(), serde_json::json!({ "type": "integer", "description": "body 截断阈值(字节,默认 51200/50KB,clamp [1024, 10485760])。替代默认截断上限;有 output_file 时不截断", "minimum": 1024, "maximum": 10485760 }));
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
@@ -50,14 +61,16 @@ pub fn register(registry: &mut AiToolRegistry) {
|
||||
};
|
||||
declare_tool!(
|
||||
registry,
|
||||
dummy: Arc<()>,
|
||||
allowed_dirs: Arc<RwLock<AllowedDirs>>,
|
||||
"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 有副作用,统一按高风险须人工批准",
|
||||
"发起结构化 HTTP 请求(GET/POST/PUT/PATCH/DELETE),用于查询外部 API。参数:method(默认 GET)、url(http/https)、headers(对象 map)、body(请求体字符串)、timeout_secs(默认 30,上限 60)、parse(json/text/auto,默认 auto)、output_file(响应落盘路径,适合大响应,全量写不截断)、max_response_chars(body 截断阈值字节,默认 50KB,clamp [1KB,10MB])。安全:仅 http/https 协议,拒绝私网/保留 IP(SSRF 防护含 DNS resolve 后校验防重绑定),重定向≤3 跳且每跳重校验。响应 body 默认截断 50KB(可 max_response_chars 覆盖或 output_file 落盘)。GET 为只读但触发外发网络,POST/PUT/PATCH/DELETE 有副作用,统一按高风险须人工批准",
|
||||
RiskLevel::High,
|
||||
schema: schema,
|
||||
args => {
|
||||
// 转调 http.rs handler(SSRF 防护 + 重定向 + 截断全在那)
|
||||
crate::commands::ai::http::execute_http_request(args).await
|
||||
// allowed_dirs read lock clone(output_file 路径校验用;无 output_file 时校验不触发)
|
||||
let snap = allowed_dirs.read().await.clone();
|
||||
// 转调 http.rs handler(SSRF 防护 + 重定向 + 截断 + output_file 落盘全在那)
|
||||
crate::commands::ai::http::execute_http_request(args, &snap).await
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ pub mod task_graph;
|
||||
pub mod git;
|
||||
pub mod http;
|
||||
pub mod fetch_url;
|
||||
pub mod fetch_search;
|
||||
pub mod generate_image;
|
||||
pub mod get_app_config;
|
||||
pub mod workflow;
|
||||
pub mod idea;
|
||||
pub mod trash;
|
||||
pub mod file;
|
||||
pub mod download_file;
|
||||
|
||||
Reference in New Issue
Block a user