新增: 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:
lxy
2026-08-02 02:21:32 +08:00
parent e4f7b432aa
commit b999f79fdf
17 changed files with 2862 additions and 56 deletions
@@ -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
}
);
}
+29 -16
View File
@@ -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
}
);
}
+4
View File
@@ -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;