//! 本地 DB/SSH 工具代理 AI 工具声明式注册(11 个:mysql/ssh/mongo/redis 本地代理)。
//!
//! 本地跑 4 个 HTTP 代理服务(mysql-proxy:3307 / ssh-proxy:3308 / mongo-proxy:3309 /
//! redis-proxy:3310,默认绑 127.0.0.1 无鉴权),经 HTTP API 操作数据库/SSH。本模块把它们
//! 注册为 AI 工具,让 AI 对话中直接查库/执行 SSH 命令,根治此前「AI 绕 run_command 执行
//! PowerShell 内联脚本查 db,引号嵌套必失败」的场景。
//!
//! 访问路径:DevFlow 内嵌本地代理统一网关(`local_proxy_gateway`,监听 127.0.0.1:8899,
//! env DF_LOCAL_PROXY_PORT 可覆盖),`POST /{type}/{path}` 前缀路由转发到对应代理端口。
//! 端口收敛到网关一张映射表(mysql→3307 / ssh→3308 / mongo→3309 / redis→3310),
//! rust-work 改端口只改网关,本模块工具零改动;工具只连网关,端口常量不散落工具代码。
//!
//! 命名:模块名 `local_proxy`(本地工具代理)—— 强调固定 gateway_url(默认
//! http://127.0.0.1:8899,环境变量 DF_LOCAL_PROXY_BASE 可覆盖如远程部署)。gateway_url 每次
//! 请求前校验仅允许 loopback(localhost/127.0.0.1/::1),防环境变量被设为内网/云元数据地址致
//! SSRF。不做具体协议名(如 db_proxy 漏 ssh)也不做泛指操作名(如 ops_proxy 过载),留普适的
//! 「本地代理」语义,未来加其他本地代理(kafka/etcd-proxy 等)直接并入,工具名
//! (mysql_query/ssh_exec 等)各自描述协议域。
//!
//! 代理 API 契约(经网关 `/{type}/{path}` 路由,type=mysql/ssh/mongo/redis):
//! - mysql  POST /query {conn,sql,format} → {columns,rows,rowCount}
//!               POST /execute {conn,sql} → {affectedRows,lastInsertId}
//! - ssh    POST /exec {server,command} → {stdout,stderr,exitCode}
//! - mongo  POST /find {conn,collection,filter} → {documents,count};/insert /update /delete
//! - redis  POST /get {conn,key} → {value};/set /del /keys /run
//! - 各代理 GET /connections → {connections:[{name,...}]};错误统一 {"error":"..."} + HTTP 400
//!
//! 安全:gateway_url 默认本地 127.0.0.1:8899,但经环境变量 DF_LOCAL_PROXY_BASE 可覆盖,故每次
//! 请求前校验仅允许 loopback 地址([`validate_proxy_gateway_url`]),防环境变量被设为内网/云
//! 元数据地址致 SSRF(http.rs validate_url 拒 localhost,本工具默认连本地代理,白名单相反,
//! 不复用)。网关侧另有 path 白名单 + connections/add 等凭据注入端点拒绝,工具侧依赖
//! gateway_url loopback 校验兜底。risk 依据:只读查询(Low)/ 数据写入(High)/ 轻量写(Medium),
//! 对齐 http_request 口径。
//!
//! declare_tool! 宏单捕获限制:本模块 11 工具仅需 gateway_url,包进 [`LocalProxyCtx`] struct
//! (Clone 廉价,单 String 字段),闭包内 `&ctx` 转调 proxy_post / proxy_connections 助手。

use std::sync::OnceLock;
use std::time::Duration;

use df_ai::ai_tools::{AiToolRegistry, RiskLevel};
use df_ai::declare_tool;
use serde_json::{json, Value};

/// 本地工具代理配置(固定 gateway_url,仅允许 loopback 地址)。
/// gateway_url 默认 http://127.0.0.1:8899(DevFlow 内嵌本地代理统一网关),可用环境变量
/// DF_LOCAL_PROXY_BASE 覆盖(如远程部署)。约定含端口(网关监听端口),如 "http://127.0.0.1:8899"。
/// 端口收敛到网关映射表(mysql→3307/ssh→3308/mongo→3309/redis→3310),工具不感知代理端口。
/// 安全:每次请求前经 [`validate_proxy_gateway_url`] 校验仅 loopback,防环境变量设内网/公网致 SSRF。
#[derive(Clone)]
pub struct LocalProxyCtx {
    pub gateway_url: String,
}

/// 本地工具代理请求超时(秒)。防代理挂起冻结 AI 会话(本地代理应毫秒级响应)。
const PROXY_TIMEOUT_SECS: u64 = 15;

/// 响应体截断上限(字节)。对齐 http.rs 默认 50KB,防大结果集(大表全量行/长 stdout)撑爆
/// LLM context(本仓库已多次实证该问题的代价,read_file 全文回灌 prompt 爆等)。
const MAX_RESPONSE_BYTES: usize = 50 * 1024;

/// 从工具 args 提取必填 string 参数,缺省报清晰错误。
fn required_str<'a>(args: &'a Value, name: &str) -> anyhow::Result<&'a str> {
    args.get(name)
        .and_then(|v| v.as_str())
        .ok_or_else(|| anyhow::anyhow!("缺少 {} 参数", name))
}

/// 从工具 args 提取必填任意值参数(对象/数组),缺省报清晰错误。
fn required_value(args: &Value, name: &str) -> anyhow::Result<Value> {
    args.get(name)
        .cloned()
        .ok_or_else(|| anyhow::anyhow!("缺少 {} 参数", name))
}

/// 校验本地工具代理 gateway_url:仅允许 http/https + loopback host(localhost/127.0.0.1/::1)。
///
/// 安全(S1):gateway_url 来自环境变量 DF_LOCAL_PROXY_BASE(运行时不可变)。若被设为内网/云元数据
/// 地址(如 http://169.254.169.254)则 AI 工具可被诱导请求该地址 → SSRF。故每次请求前校验:
/// 协议白名单(http/https)+ host 白名单(仅 loopback,本地代理固定本地端点)。
/// 不复用 http.rs validate_url —— 它拒 localhost,而本工具默认连 127.0.0.1,白名单相反。
fn validate_proxy_gateway_url(gateway_url: &str) -> anyhow::Result<()> {
    let parsed = reqwest::Url::parse(gateway_url)
        .map_err(|e| anyhow::anyhow!("gateway_url 解析失败({}): {}", gateway_url, e))?;
    let scheme = parsed.scheme().to_lowercase();
    if scheme != "http" && scheme != "https" {
        anyhow::bail!(
            "本地工具代理 gateway_url 仅允许 http/https(拒 file/ftp/gopher 等): {}",
            gateway_url
        );
    }
    let host = parsed
        .host_str()
        .ok_or_else(|| anyhow::anyhow!("本地工具代理 gateway_url 缺少 host: {}", gateway_url))?
        .to_lowercase();
    // host 白名单:仅 loopback(localhost/127.0.0.1/::1)。IPv6 字面量 Url::host_str 去方括号,
    // 此处再防御性 trim(对齐 http.rs),与三值精确比对;其他 host(内网 IP/公网域名)→ 拒。
    let bare = host.trim_start_matches('[').trim_end_matches(']');
    if !matches!(bare, "localhost" | "127.0.0.1" | "::1") {
        anyhow::bail!(
            "本地工具代理 gateway_url 仅允许 loopback 地址(localhost/127.0.0.1/::1),当前 '{}': 拒绝内网/公网 host 防 SSRF",
            bare
        );
    }
    Ok(())
}

/// 复用本地工具代理 HTTP client(连接池复用,避免每次请求新建)。仅设 timeout,reqwest 构建
/// 失败仅 TLS 后端缺失等极端情况,本地 http 代理下不会发生,故 expect(对齐 http.rs 构建条件)。
fn local_client() -> &'static reqwest::Client {
    static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
    CLIENT.get_or_init(|| {
        reqwest::Client::builder()
            .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
            .build()
            .expect("构建本地工具代理 HTTP client 失败")
    })
}

/// 统一本地工具代理请求:校验 gateway_url(SSRF)→ 复用 client 发送 → 读取/截断/解析响应。
///
/// 返回 (response, truncated):response 优先 JSON.parse(代理返 {columns,rows}/{stdout,stderr,
/// exitCode} 等结构化结果;错误 {"error":"..."} 亦按 JSON 返回),解析失败回退 raw text;
/// 响应体 >50KB 时按 char 边界截断 + truncated=true(截断后不再尝试 JSON 解析,半截 JSON 无意义)。
/// `body` 为 None 时发 GET(POST 用 Some)。错误返回 Err 含 URL 上下文。
async fn proxy_request(
    gateway_url: &str,
    proxy_type: &str,
    method: reqwest::Method,
    path: &str,
    body: Option<Value>,
) -> anyhow::Result<(Value, bool)> {
    // SSRF 防护:每次请求前校验 gateway_url(环境变量运行时不可变,但防御性每次校验)。
    validate_proxy_gateway_url(gateway_url)?;
    let url = format!("{}/{}/{}", gateway_url.trim_end_matches('/'), proxy_type, path);
    let mut req = local_client().request(method, &url);
    if let Some(b) = body {
        req = req.json(&b);
    }
    let resp = req
        .send()
        .await
        .map_err(|e| anyhow::anyhow!("本地工具代理请求失败({}): {}", url, e))?;
    let bytes = resp
        .bytes()
        .await
        .map_err(|e| anyhow::anyhow!("读取代理响应失败({}): {}", url, e))?;
    let total = bytes.len();
    // 截断:超上限按 char 边界切(UTF-8 容错,切到多字节中间时 lossy 插 U+FFFD,可接受)+ 尾注。
    let (text, truncated) = if total > MAX_RESPONSE_BYTES {
        let head = String::from_utf8_lossy(&bytes[..MAX_RESPONSE_BYTES]).into_owned();
        (
            format!("{}... [响应已截断,原始 {} 字节,仅保留前 {} 字节]", head, total, MAX_RESPONSE_BYTES),
            true,
        )
    } else {
        (String::from_utf8_lossy(&bytes).into_owned(), false)
    };
    let response = if truncated {
        Value::String(text)
    } else {
        serde_json::from_str::<Value>(&text).unwrap_or_else(|_| Value::String(text))
    };
    Ok((response, truncated))
}

/// POST 到本地工具代理网关端点。返回 `{proxy_url, response, truncated}`(解析/截断逻辑见
/// [`proxy_request`])。写工具(执行 SQL/SSH/mongo 写)与只读查询共用此入口。
async fn proxy_post(
    gateway_url: &str,
    proxy_type: &str,
    path: &str,
    body: &Value,
) -> anyhow::Result<Value> {
    let (response, truncated) =
        proxy_request(gateway_url, proxy_type, reqwest::Method::POST, path, Some(body.clone()))
            .await?;
    let url = format!("{}/{}/{}", gateway_url.trim_end_matches('/'), proxy_type, path);
    Ok(json!({ "proxy_url": url, "response": response, "truncated": truncated }))
}

/// GET /connections 拉单个代理连接列表。失败返 `{"error":"..."}`(不 panic,供汇总容错)。
async fn proxy_connections(gateway_url: &str, proxy_type: &str) -> Value {
    match proxy_request(gateway_url, proxy_type, reqwest::Method::GET, "connections", None).await {
        Ok((response, _truncated)) => response,
        Err(e) => json!({ "error": format!("{}", e) }),
    }
}

/// 注册本地工具代理(11 个)到 `$registry`。
///
/// risk 口径对齐 http_request:只读查询(Low)/ 数据写入(High)/ 轻量写(Medium)。
/// handler 统一转调 proxy_post,返回 `{proxy_url, response, truncated}`(response 优先
/// JSON,>50KB 截断 + truncated=true)。
pub fn register(registry: &mut AiToolRegistry, ctx: LocalProxyCtx) {
    // ── list_proxy_connections:汇总 4 代理连接列表(只读,无参) ──
    {
        let schema = df_ai::ai_tools::object_schema(vec![]);
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "list_proxy_connections",
            "列出本地 4 个工具代理(mysql/ssh/mongo/redis)当前已配置的连接名列表,返回 {mysql:[...], ssh:[...], mongo:[...], redis:[...]}(每项为对应代理 GET /connections 的原始响应,如 {connections:[{name,...}]};某代理未启动/失败时该项为 {\"error\":\"...\"} 不阻断整体)。用于 AI 先探查有哪些数据库连接/SSH 服务器可用,再选 conn/server 调 mysql_query/ssh_exec 等。",
            RiskLevel::Low,
            schema: schema,
            _args => {
                let gw = &ctx.gateway_url;
                Ok(json!({
                    "mysql": proxy_connections(gw, "mysql").await,
                    "ssh": proxy_connections(gw, "ssh").await,
                    "mongo": proxy_connections(gw, "mongo").await,
                    "redis": proxy_connections(gw, "redis").await,
                }))
            }
        );
    }

    // ── mysql_query:只读 SELECT(Low) ──
    {
        let schema = df_ai::ai_tools::object_schema(vec![
            ("conn", "string", true),
            ("sql", "string", true),
            ("format", "string", false),
        ]);
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "mysql_query",
            "对 mysql-proxy 连接执行只读 SELECT 查询。参数:conn(连接名,先 list_proxy_connections 查)、sql(完整 SELECT 语句)、format(可选,默认 json)。返回 {columns, rows, rowCount}。仅用于 SELECT 等只读语句;写操作(INSERT/UPDATE/DELETE/DDL)用 mysql_execute(需人工批准)。",
            RiskLevel::Low,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let sql = required_str(&args, "sql")?;
                let mut body = json!({ "conn": conn, "sql": sql });
                if let Some(f) = args.get("format") {
                    body["format"] = f.clone();
                }
                proxy_post(&ctx.gateway_url, "mysql", "query", &body).await
            }
        );
    }

    // ── mysql_execute:数据写/DDL(High) ──
    {
        let schema = df_ai::ai_tools::object_schema(vec![
            ("conn", "string", true),
            ("sql", "string", true),
        ]);
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "mysql_execute",
            "对 mysql-proxy 连接执行非只读语句(INSERT/UPDATE/DELETE/DDL 等)。参数:conn(连接名)、sql(完整 SQL 语句)。返回 {affectedRows, lastInsertId}。有数据写副作用,须人工批准;只读查询用 mysql_query。",
            RiskLevel::High,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let sql = required_str(&args, "sql")?;
                let body = json!({ "conn": conn, "sql": sql });
                proxy_post(&ctx.gateway_url, "mysql", "execute", &body).await
            }
        );
    }

    // ── ssh_exec:远程命令执行(High) ──
    {
        let schema = df_ai::ai_tools::object_schema(vec![
            ("server", "string", true),
            ("command", "string", true),
        ]);
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "ssh_exec",
            "经 ssh-proxy 连接执行远程 shell 命令。参数:server(服务器名,先 list_proxy_connections 查)、command(要执行的命令,如 \"docker ps\")。返回 {stdout, stderr, exitCode}(exitCode=0 表示成功)。远程命令有副作用 + 执行环境敏感,须人工批准。",
            RiskLevel::High,
            schema: schema,
            args => {
                let server = required_str(&args, "server")?;
                let command = required_str(&args, "command")?;
                let body = json!({ "server": server, "command": command });
                proxy_post(&ctx.gateway_url, "ssh", "exec", &body).await
            }
        );
    }

    // ── mongo_find:只读查询(Low,filter 为对象) ──
    {
        let schema = {
            let mut props = serde_json::Map::new();
            props.insert("conn".into(), json!({ "type": "string", "description": "mongo 连接名" }));
            props.insert("collection".into(), json!({ "type": "string", "description": "集合名" }));
            props.insert("filter".into(), json!({ "type": "object", "description": "查询条件(JSON 对象),如 {\"status\":\"active\"};缺省查全部" }));
            json!({ "type": "object", "properties": props, "required": ["conn", "collection"] })
        };
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "mongo_find",
            "对 mongo-proxy 连接执行只读查询。参数:conn(连接名)、collection(集合名)、filter(可选 JSON 对象查询条件)。返回 {documents, count}。写操作(insert/update/delete)用对应工具(需人工批准)。",
            RiskLevel::Low,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let collection = required_str(&args, "collection")?;
                let mut body = json!({ "conn": conn, "collection": collection });
                if let Some(f) = args.get("filter") {
                    body["filter"] = f.clone();
                }
                proxy_post(&ctx.gateway_url, "mongo", "find", &body).await
            }
        );
    }

    // ── mongo_insert:插入文档(High,documents 为数组) ──
    {
        let schema = {
            let mut props = serde_json::Map::new();
            props.insert("conn".into(), json!({ "type": "string", "description": "mongo 连接名" }));
            props.insert("collection".into(), json!({ "type": "string", "description": "集合名" }));
            props.insert("documents".into(), json!({ "type": "array", "description": "待插入文档数组(每项为 JSON 对象),如 [{\"name\":\"x\",\"age\":1}]" }));
            json!({ "type": "object", "properties": props, "required": ["conn", "collection", "documents"] })
        };
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "mongo_insert",
            "向 mongo-proxy 连接集合插入文档。参数:conn(连接名)、collection(集合名)、documents(待插入文档数组,每项 JSON 对象)。数据写副作用,须人工批准。",
            RiskLevel::High,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let collection = required_str(&args, "collection")?;
                let documents = required_value(&args, "documents")?;
                let body = json!({ "conn": conn, "collection": collection, "documents": documents });
                proxy_post(&ctx.gateway_url, "mongo", "insert", &body).await
            }
        );
    }

    // ── mongo_update:更新文档(High,filter/update 为对象) ──
    {
        let schema = {
            let mut props = serde_json::Map::new();
            props.insert("conn".into(), json!({ "type": "string", "description": "mongo 连接名" }));
            props.insert("collection".into(), json!({ "type": "string", "description": "集合名" }));
            props.insert("filter".into(), json!({ "type": "object", "description": "匹配条件(JSON 对象)" }));
            props.insert("update".into(), json!({ "type": "object", "description": "更新操作(JSON 对象),如 {\"$set\":{\"name\":\"y\"}}" }));
            json!({ "type": "object", "properties": props, "required": ["conn", "collection", "filter", "update"] })
        };
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "mongo_update",
            "更新 mongo-proxy 连接集合中匹配 filter 的文档。参数:conn、collection、filter(匹配条件对象)、update(更新操作对象,如 {\"$set\":{\"name\":\"y\"}})。数据写副作用,须人工批准。",
            RiskLevel::High,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let collection = required_str(&args, "collection")?;
                let filter = required_value(&args, "filter")?;
                let update = required_value(&args, "update")?;
                let body = json!({ "conn": conn, "collection": collection, "filter": filter, "update": update });
                proxy_post(&ctx.gateway_url, "mongo", "update", &body).await
            }
        );
    }

    // ── mongo_delete:删除文档(High,filter 为对象,multi 可选) ──
    {
        let schema = {
            let mut props = serde_json::Map::new();
            props.insert("conn".into(), json!({ "type": "string", "description": "mongo 连接名" }));
            props.insert("collection".into(), json!({ "type": "string", "description": "集合名" }));
            props.insert("filter".into(), json!({ "type": "object", "description": "匹配条件(JSON 对象)" }));
            props.insert("multi".into(), json!({ "type": "boolean", "description": "是否删除全部匹配(默认 false 仅删首条)" }));
            json!({ "type": "object", "properties": props, "required": ["conn", "collection", "filter"] })
        };
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "mongo_delete",
            "删除 mongo-proxy 连接集合中匹配 filter 的文档。参数:conn、collection、filter(匹配条件对象)、multi(可选布尔,true 删除全部匹配,默认仅删首条)。数据写副作用,须人工批准。",
            RiskLevel::High,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let collection = required_str(&args, "collection")?;
                let filter = required_value(&args, "filter")?;
                let mut body = json!({ "conn": conn, "collection": collection, "filter": filter });
                if let Some(m) = args.get("multi") {
                    body["multi"] = m.clone();
                }
                proxy_post(&ctx.gateway_url, "mongo", "delete", &body).await
            }
        );
    }

    // ── redis_get:只读取值(Low) ──
    {
        let schema = df_ai::ai_tools::object_schema(vec![
            ("conn", "string", true),
            ("key", "string", true),
        ]);
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "redis_get",
            "经 redis-proxy 读取指定 key 的值。参数:conn(连接名)、key。返回 {value}。只读;写操作(redis_set/redis_del)为 Medium 轻量写。",
            RiskLevel::Low,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let key = required_str(&args, "key")?;
                let body = json!({ "conn": conn, "key": key });
                proxy_post(&ctx.gateway_url, "redis", "get", &body).await
            }
        );
    }

    // ── redis_set:写入 key(Medium,ttl 可选) ──
    {
        let schema = df_ai::ai_tools::object_schema(vec![
            ("conn", "string", true),
            ("key", "string", true),
            ("value", "string", true),
            ("ttl", "integer", false),
        ]);
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "redis_set",
            "经 redis-proxy 写入 key 的值。参数:conn(连接名)、key、value、ttl(可选整数秒,过期时间)。轻量写副作用(Medium 审批档)。",
            RiskLevel::Medium,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let key = required_str(&args, "key")?;
                let value = required_str(&args, "value")?;
                let mut body = json!({ "conn": conn, "key": key, "value": value });
                if let Some(t) = args.get("ttl") {
                    body["ttl"] = t.clone();
                }
                proxy_post(&ctx.gateway_url, "redis", "set", &body).await
            }
        );
    }

    // ── redis_del:删除多个 key(Medium,keys 为数组) ──
    {
        let schema = {
            let mut props = serde_json::Map::new();
            props.insert("conn".into(), json!({ "type": "string", "description": "redis 连接名" }));
            props.insert("keys".into(), json!({ "type": "array", "items": { "type": "string" }, "description": "待删除的 key 列表(数组)" }));
            json!({ "type": "object", "properties": props, "required": ["conn", "keys"] })
        };
        declare_tool!(
            registry,
            ctx: LocalProxyCtx,
            "redis_del",
            "经 redis-proxy 删除一个或多个 key。参数:conn(连接名)、keys(key 数组)。轻量写副作用(Medium 审批档)。",
            RiskLevel::Medium,
            schema: schema,
            args => {
                let conn = required_str(&args, "conn")?;
                let keys = required_value(&args, "keys")?;
                let body = json!({ "conn": conn, "keys": keys });
                proxy_post(&ctx.gateway_url, "redis", "del", &body).await
            }
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // ── validate_proxy_gateway_url:loopback 白名单 + 协议白名单 ──

    #[test]
    fn test_validate_proxy_gateway_url_loopback_ok() {
        // 默认网关地址(http/https、host 形态)应通过;URL 可带端口,host 校验与端口无关
        assert!(validate_proxy_gateway_url("http://127.0.0.1").is_ok());
        assert!(validate_proxy_gateway_url("http://localhost").is_ok());
        assert!(validate_proxy_gateway_url("http://[::1]").is_ok());
        assert!(validate_proxy_gateway_url("https://127.0.0.1").is_ok());
        assert!(validate_proxy_gateway_url("http://127.0.0.1:8899").is_ok());
        assert!(validate_proxy_gateway_url("HTTP://LOCALHOST").is_ok()); // 大小写归一
    }

    #[test]
    fn test_validate_proxy_gateway_url_rejects_non_loopback_host() {
        // 内网/云元数据/公网域名一律拒绝(SSRF 核心:防环境变量指向非本地代理)
        assert!(validate_proxy_gateway_url("http://169.254.169.254").is_err()); // 云元数据服务
        assert!(validate_proxy_gateway_url("http://10.0.0.1").is_err());        // RFC1918 A 类
        assert!(validate_proxy_gateway_url("http://192.168.1.1").is_err());     // RFC1918 C 类
        assert!(validate_proxy_gateway_url("http://172.16.0.1").is_err());      // RFC1918 B 类
        assert!(validate_proxy_gateway_url("http://example.com").is_err());     // 公网域名
    }

    #[test]
    fn test_validate_proxy_gateway_url_rejects_bad_scheme_or_missing_host() {
        assert!(validate_proxy_gateway_url("file:///etc/passwd").is_err());
        assert!(validate_proxy_gateway_url("ftp://127.0.0.1").is_err());
        assert!(validate_proxy_gateway_url("").is_err());
    }
}
