优化: run_command 实时流式 + 审批浮窗修复 + 文档探索闭环(fetch_url+obscura引导+model_fetch兼容+prompt策略+厂商预设)

This commit is contained in:
lxy
2026-08-01 17:58:47 +08:00
parent 0e0c6862ba
commit d664bdc309
25 changed files with 1693 additions and 44 deletions
@@ -0,0 +1,51 @@
//! fetch_url AI 工具声明式注册(URL → markdown 文档嗅探,只读 GET)。
//!
//! 工具职责:GET 网页 URL → HTML 转 markdown(htmd)→ 去噪音 + 截断 → 返回 {url,title,markdown,...}。
//! 让 LLM 高效理解网页文档,替代 http_request 拿原始 HTML(噪声大、爆 token)。
//!
//! 风险:Low(只读 GET,无副作用)。SSRF 防护与 http_request 共享同一套(validate_url +
//! resolve_and_check_host + 重定向每跳校验,见 commands/ai/http.rs)。
//!
//! handler body 在 commands/ai/fetch_url.rs::execute_fetch_url,声明式注册收敛样板。
use std::sync::Arc;
use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
use df_ai::declare_tool;
/// 注册 fetch_url 工具到 `$registry`(无 db 捕获,纯网络 GET + htmd 转换)。
///
/// - name/desc/schema 面向 LLM 的工具说明
/// - risk=Low(只读 GET)
/// - handler 转调 fetch_url.rs::execute_fetch_url(SSRF 防护 + htmd + 截断全在那)
pub fn register(registry: &mut AiToolRegistry) {
// 无捕获:占位 Arc<()>(handler 不持 db,仅转调 fetch_url.rs)。
let dummy: Arc<()> = Arc::new(());
// schema:url(必填)+ max_length(可选,默认 8000)+ render(可选,默认 false)。
// headers 用手工 serde_json::Map 表达(additionalProperties 需对象 schema,object_schema 不支持)。
let schema = {
let mut props = serde_json::Map::new();
props.insert("url".into(), serde_json::json!({ "type": "string", "description": "要获取的网页 URL,仅 http/https(拒私网/localhost,SSRF 防护)" }));
props.insert("max_length".into(), serde_json::json!({ "type": "integer", "description": "返回 markdown 的截断长度(chars,默认 8000,clamp [500,50000])", "minimum": 500, "maximum": 50000, "default": 8000 }));
props.insert("render".into(), serde_json::json!({ "type": "boolean", "description": "是否用 obscura(JS 渲染)抓取,默认 false(静态 reqwest+htmd)。render=true 适合 SPA/JS 动态渲染/反爬文档:检测本地 obscura(Rust 无头浏览器,自带 V8),已装则 spawn 渲染,未装/失败/超时自动回退静态并附 hint。需装 obscura:github.com/h4ckf0r0day/obscura", "default": false }));
props.insert("headers".into(), serde_json::json!({ "type": "object", "description": "可选请求头 map<string,string>(如自定义 User-Agent)。默认带主流浏览器 UA 以拿完整渲染 HTML(仅静态模式生效,render=true 时由 obscura 自管 UA)", "additionalProperties": { "type": "string" } }));
props.insert("timeout_secs".into(), serde_json::json!({ "type": "integer", "description": "超时秒数(默认 30,上限 60,仅静态模式生效;render=true 用固定 45s obscura 超时)", "minimum": 1, "maximum": 60 }));
serde_json::json!({
"type": "object",
"properties": props,
"required": ["url"],
})
};
declare_tool!(
registry,
dummy: Arc<()>,
"fetch_url",
"获取网页 URL 内容并转为 markdown(HTML 清洗 + 截断),用于高效理解网页文档(API 文档/博客/技术资料)。只读 GET,自动剥离 script/style/nav 等噪声,提取 title,按 max_length 截断。返回 {url, title, markdown, length, truncated, render_mode, ?hint}。安全:仅 http/https,拒绝私网/保留 IP(SSRF 防护含 DNS resolve 后校验),重定向≤3 跳。render=true 用 obscura 渲染 JS(SPA/反爬,需装 obscura,未装自动回退静态)。如需 POST/鉴权/原始响应用 http_request",
RiskLevel::Low,
schema: schema,
args => {
// 转调 fetch_url.rs handler(SSRF 防护 + htmd 转换 + 去噪音 + 截断全在那)
crate::commands::ai::fetch_url::execute_fetch_url(args).await
}
);
}
+10 -4
View File
@@ -45,9 +45,10 @@ use crate::commands::ai::tool_registry::{
FILE_LOCKS, FileGrepHit,
};
// run_command 依赖 df_execute::shell::{execute, ShellRequest} + std HashMap
// new_id: delete_file 软删除备份命名(原 register_file_tools 闭包引用,经 df_types::types::new_id)
use df_execute::shell::{execute, ShellRequest};
// run_command 依赖 df_execute::shell::{execute_streaming, ShellRequest, StreamKind} + std HashMap
// execute_streaming:run_command 专用流式(spawn 逐行读 stdout/stderr,回调 emit AiCommandOutput 治执行黑盒)。
// new_id: delete_file 软删除备份命名(原 register_file_tools 闭包引用,经 df_types::types::new_id)。
use df_execute::shell::{execute_streaming, ShellRequest, StreamKind};
use df_types::types::new_id;
use std::collections::HashMap;
@@ -1032,7 +1033,12 @@ pub fn register(
} else {
""
};
let result = execute(request).await.map_err(|e| {
let result = execute_streaming(request, |kind: StreamKind, line: &str| {
// 实时流式 emit:每读一行 stdout/stderr 即 emit AiCommandOutput。
// task-local 未注入(command_stream::scope 未调用)→ emit_output 静默 noop,
// 不阻断命令(等价原 execute 一次性返回,无副作用)。
crate::commands::ai::command_stream::emit_output(kind, line);
}).await.map_err(|e| {
let msg = e.to_string();
if msg.contains("命令执行超时") {
anyhow::anyhow!(
+1
View File
@@ -14,6 +14,7 @@ pub mod task;
pub mod task_graph;
pub mod git;
pub mod http;
pub mod fetch_url;
pub mod workflow;
pub mod idea;
pub mod trash;