April 15, 2026 • 版本: 2026.3.13

配置环境代理时,内存搜索远程嵌入因 ENOTFOUND 失败

withRemoteHttpResponse() 函数绕过 TRUSTED_ENV_PROXY 模式,导致在 Clash TUN fake-IP 模式或企业代理等代理环境中本地 DNS 预解析失败。

🔍 症状

错误表现

当使用配置了 HTTP 代理的内存搜索命令时,嵌入功能变得不可用:

$ openclaw memory status --deep --agent main
Embeddings: unavailable
Error: getaddrinfo ENOTFOUND api.ohmygpt.com

$ openclaw memory search --agent main --query "test"
Error: getaddrinfo ENOTFOUND api.ohmygpt.com
Embeddings: unavailable

技术行为

  • memory status 命令报告 Embeddings: unavailable,而不是显示已配置的远程嵌入提供程序。
  • memory search 命令立即失败并出现 DNS 解析错误。
  • 在同一台机器上对同一端点的 OpenAI SDK 调用成功,确认代理基础设施正常运行。
  • 错误发生在任何 HTTP 请求尝试之前——它在 DNS 查询阶段就失败了。

复现条件

  • 必需:通过环境变量配置 HTTP/HTTPS 代理(HTTPS_PROXYHTTP_PROXYALL_PROXY
  • 必需:代理设置,其中本地 DNS 无法解析嵌入提供程序主机名(例如 Clash TUN fake-IP 模式、企业 DNS-over-proxy)
  • 必需:在代理中配置远程嵌入提供程序

触发该 Bug 的环境配置

# 环境变量
HTTPS_PROXY=http://127.0.0.1:7890
HTTP_PROXY=http://127.0.0.1:7890

# 代理配置 (openclaw agent config)
models.providers.openai.baseUrl = "https://api.ohmygpt.com/v1"
memorySearch.remote.model = "text-embedding-3-small"

🧠 根因分析

调用链分析

故障通过以下精确顺序发生:

withRemoteHttpResponse(params)
  → fetchWithSsrFGuard({ url, init, policy })
    → resolveGuardedFetchMode(params)
      → returns STRICT (params 中没有 mode 字段)
    → resolvePinnedHostnameWithPolicy(hostname)
      → dns.lookup(hostname)
        → ENOTFOUND (本地 DNS 无法解析)

缺少代理环境检查

函数 withRemoteHttpResponse()src/memory/post-json.ts(或等效入口点)中实现。它调用 fetchWithSsrFGuard() 时没有在请求参数中设置 mode 字段:

// 当前(有问题的)实现
async function withRemoteHttpResponse(params) {
    const { response, release } = await fetchWithSsrFGuard({
        url: params.url,
        init: params.init,
        policy: params.ssrfPolicy,
        auditContext: params.auditContext ?? "memory-remote"
        // 缺少:mode 字段 — 默认为 STRICT
    });
    // ...
}

STRICT 模式下的默认行为

当未设置 mode 时,fetchWithSsrFGuard() 调用 resolveGuardedFetchMode(),默认返回 STRICT。在 STRICT 模式下,该函数始终执行:

resolvePinnedHostnameWithPolicy(hostname)
  → dns.lookup(hostname)  // 通过 Node.js 解析器进行阻塞式 DNS 解析
  → ENOTFOUND             // 在代理环境中失败

为什么在代理环境中会失败

在诸如 Clash TUN fake-IP 模式的代理配置中:

  1. 本地机器的 DNS 解析器无法访问 api.ohmygpt.com
  2. DNS 解析必须通过代理隧道进行(例如通过 clashDNS 或代理端解析)
  3. dns.lookup() 调用使用系统解析器,完全绕过代理
  4. 请求在主机名解析阶段失败,永远无法到达代理

正确的模式

代码库中其他代码路径已包含正确的实现。当配置了环境代理时,应使用 withTrustedEnvProxyGuardedFetchMode()

// 正确的实现
async function withRemoteHttpResponse(params) {
    const useEnvProxy = hasProxyEnvConfigured();
    const request = useEnvProxy 
        ? withTrustedEnvProxyGuardedFetchMode({
            url: params.url,
            init: params.init,
            policy: params.ssrfPolicy,
            auditContext: params.auditContext ?? "memory-remote"
        })
        : {
            url: params.url,
            init: params.init,
            policy: params.ssrfPolicy,
            auditContext: params.auditContext ?? "memory-remote"
        };
    const { response, release } = await fetchWithSsrFGuard(request);
    // ...
}

TRUSTED_ENV_PROXY 模式下,fetchWithSsrFGuard()

  • 跳过通过 dns.lookup() 进行的本地 DNS 预解析
  • 直接使用 EnvHttpProxyAgent() 进行 HTTP 连接
  • 将 DNS 解析委托给代理基础设施

🛠️ 逐步修复

选项 1:源代码修复(推荐)

待修改文件: src/memory/post-json.ts(或等效源位置)

修改前:

import { fetchWithSsrFGuard } from '../ssrf/fetch-guard';
// ... 其他导入

async function withRemoteHttpResponse(params: RemoteHttpParams) {
    const { response, release } = await fetchWithSsrFGuard({
        url: params.url,
        init: params.init,
        policy: params.ssrfPolicy,
        auditContext: params.auditContext ?? "memory-remote"
        // mode 未设置 → 默认为 STRICT
    });
    
    if (!response.ok) {
        const body = await response.text();
        release();
        throw new RemoteHttpError(params.url, response.status, body);
    }
    
    return { response, release };
}

修改后:

import { fetchWithSsrFGuard } from '../ssrf/fetch-guard';
import { hasProxyEnvConfigured, withTrustedEnvProxyGuardedFetchMode } from '../ssrf/fetch-mode';
// ... 其他导入

async function withRemoteHttpResponse(params: RemoteHttpParams) {
    const useEnvProxy = hasProxyEnvConfigured();
    
    const request = useEnvProxy
        ? withTrustedEnvProxyGuardedFetchMode({
            url: params.url,
            init: params.init,
            policy: params.ssrfPolicy,
            auditContext: params.auditContext ?? "memory-remote"
        })
        : {
            url: params.url,
            init: params.init,
            policy: params.ssrfPolicy,
            auditContext: params.auditContext ?? "memory-remote"
        };
    
    const { response, release } = await fetchWithSsrFGuard(request);
    
    if (!response.ok) {
        const body = await response.text();
        release();
        throw new RemoteHttpError(params.url, response.status, body);
    }
    
    return { response, release };
}

选项 2:运行时环境变通方案

如果您无法修改源代码,请设置 NODE_TLS_REJECT_UNAUTHORIZED 变量以绕过可能加剧 DNS 问题的证书验证问题:

# 设置代理和 TLS 绕过(仅在受控环境中使用)
export HTTPS_PROXY=http://127.0.0.1:7890
export HTTP_PROXY=http://127.0.0.1:7890
export NODE_TLS_REJECT_UNAUTHORIZED=0

# 然后运行 OpenClaw
openclaw memory status --deep --agent main

选项 3:Bundle 补丁(临时变通方案)

如果源代码修复尚未部署且您需要立即修复:

第 1 步: 识别受影响的 bundle:

grep -l "withRemoteHttpResponse" dist/*.js 2>/dev/null | head -20

第 2 步: 创建补丁脚本(patch-memory-proxy.js):

const fs = require('fs');
const path = require('path');

const bundles = [
    'dist/reply-Bm8VrLQh.js',
    'dist/auth-profiles-DDVivXkv.js',
    'dist/discord-CcCLMjHw.js'
];

const searchPattern = /async function withRemoteHttpResponse\(params\)\{const\{response,release\}=await fetchWithSsrFGuard\(\{url:params\.url,init:params\.init,policy:params\.ssrfPolicy,auditContext:params\.auditContext\?\?"memory-remote"\}\);/g;

const replacePattern = `async function withRemoteHttpResponse(params){const _useEnvProxy=hasProxyEnvConfigured();const _request=_useEnvProxy?withTrustedEnvProxyGuardedFetchMode({url:params.url,init:params.init,policy:params.ssrfPolicy,auditContext:params.auditContext??"memory-remote"}):{url:params.url,init:params.init,policy:params.ssrfPolicy,auditContext:params.auditContext??"memory-remote"};const{response,release}=await fetchWithSsrFGuard(_request);`;

bundles.forEach(bundle => {
    if (fs.existsSync(bundle)) {
        let content = fs.readFileSync(bundle, 'utf8');
        if (searchPattern.test(content)) {
            content = content.replace(searchPattern, replacePattern);
            fs.writeFileSync(bundle, content);
            console.log(`Patched: ${bundle}`);
        }
    }
});

第 3 步: 运行补丁:

node patch-memory-proxy.js

🧪 验证

验证步骤

第 1 步: 确认代理环境变量已设置:

$ echo $HTTPS_PROXY
http://127.0.0.1:7890

$ echo $HTTP_PROXY
http://127.0.0.1:7890

$ echo $ALL_PROXY
# (应为空或已设置)

第 2 步: 验证本地 DNS 无法解析该端点(确认条件):

$ node -e "require('dns').lookup('api.ohmygpt.com', (err, addr) => console.log(err ? err.message : addr))"
getaddrinfo ENOTFOUND api.ohmygpt.com

# 预期:ENOTFOUND 错误(确认需要代理 DNS)

第 3 步: 通过检查捆绑代码验证修复已应用:

$ grep -o "withTrustedEnvProxyGuardedFetchMode" dist/*.js | head -5
dist/reply-Bm8VrLQh.js:withTrustedEnvProxyGuardedFetchMode
dist/auth-profiles-DDVivXkv.js:withTrustedEnvProxyGuardedFetchMode
dist/discord-CcCLMjHw.js:withTrustedEnvProxyGuardedFetchMode

# 所有受影响的 bundle 都应包含该函数调用

第 4 步: 验证 hasProxyEnvConfigured 检查是否存在:

$ grep -A1 "hasProxyEnvConfigured()" dist/*.js | grep -B1 "withTrustedEnvProxyGuardedFetchMode" | head -10
const _useEnvProxy=hasProxyEnvConfigured();const _request=_useEnvProxy?withTrustedEnvProxyGuardedFetchMode
# 应看到两个函数按顺序出现

第 5 步: 运行 memory status 命令:

$ openclaw memory status --deep --agent main
Memory Status
├─ Vector Store: OK (50 vectors indexed)
├─ Embeddings: api.ohmygpt.com/text-embedding-3-small
└─ Status: ready

# 预期:Embeddings 应显示配置的提供程序,而不是 "unavailable"

第 6 步: 运行 memory search 命令:

$ openclaw memory search --agent main --query "test" --limit 5
[
  {
    "id": "mem_001",
    "score": 0.9234,
    "content": "..."
  }
]

# 预期:返回搜索结果,无 ENOTFOUND 错误

第 7 步: 验证退出码:

$ openclaw memory search --agent main --query "test"
$ echo $?
0

# 预期:成功时退出码为 0

修复后的预期输出

$ openclaw memory status --deep --agent main
Memory Status
├─ Vector Store
│  └─ Provider: remote
│  └─ Model: text-embedding-3-small
│  └─ Dimensions: 1536
│  └─ Vectors: 50
├─ Embeddings
│  └─ Status: available
│  └─ Endpoint: https://api.ohmygpt.com/v1
│  └─ Model: text-embedding-3-small
└─ Search: functional

⚠️ 常见陷阱

1. 构建产物重复

问题: withRemoteHttpResponse() 函数被 rolldown 内联到多个 dist chunk 中。在 2026.3.13 版本中,在不同文件中有 7 个 bundle 副本。

可能没有修复的受影响 bundle:

  • reply-Bm8VrLQh.js — 网关代理工具路径
  • auth-profiles-DDVivXkv.js — 备用认证 bundle
  • discord-CcCLMjHw.js — Discord 频道路径

可能已有修复的受影响 bundle:

  • auth-profiles-DRjqKE3G.js — CLI 路径
  • model-selection-*.js — 模型选择 bundle
  • plugin-sdk/thread-bindings-*.js — 插件 SDK bundle

缓解措施: 始终从源代码重新构建,并在构建后验证所有 bundle 副本包含代理保护。

2. 环境变量大小写敏感性

问题: hasProxyEnvConfigured() 可能以特定大小写检查特定变量名。

确保代理变量的大小写一致:

# 正确(大写)
export HTTPS_PROXY=http://127.0.0.1:7890

# 可能无法被检测到
export https_proxy=http://127.0.0.1:7890

检查 hasProxyEnvConfigured() 的实际实现以确认检查了哪些变量。

3. 代理协议不匹配

问题: 通过 HTTP 代理发送 HTTPS 请求需要正确的协议处理。

确保代理 URL 包含正确的协议:

# HTTP 代理的正确格式
export HTTPS_PROXY=http://127.0.0.1:7890

# SOCKS5 代理的正确格式
export HTTPS_PROXY=socks5://127.0.0.1:1080

4. 源代码修复后的 Bundle 缓存

问题: 如果启用了增量构建,构建系统可能不会重新打包所有文件。

强制干净重建:

# 删除 dist 目录
rm -rf dist/

# 清除任何缓存
rm -rf node_modules/.cache

# 重新构建
npm run build

# 验证所有副本
grep -l "withRemoteHttpResponse" dist/**/*.js | xargs grep -l "hasProxyEnvConfigured"

5. WSL/Windows 跨环境代理

问题: 在带有 Windows 主机代理的 WSL 中运行 OpenClaw 时,环境变量可能无法正确传播。

在 WSL 中显式设置代理:

# 在 WSL 中,获取 Windows 主机 IP
export HTTPS_PROXY=http://$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):7890

6. 不同上下文中的 localhost 与 127.0.0.1

问题: 某些代理配置绑定到 127.0.0.1,而其他配置使用 ::1(IPv6 localhost)。

确保代理正在正确的接口上监听:

# 检查代理绑定
netstat -tlnp | grep 7890

# 常见结果:tcp 0 0 127.0.0.1:7890 0.0.0.0:* LISTEN

🔗 相关错误

相关的错误代码和问题

  • ENOTFOUND — DNS 解析失败。当 dns.lookup() 无法通过本地解析器解析主机名时发生。在代理环境中,当端点必须通过代理隧道解析时,这是预期的。
  • ECONNREFUSED — 连接被拒绝。如果代理未运行或环境变量中的代理地址不正确,可能会发生。
  • ETIMEDOUT — 连接超时。如果代理无法访问或网络路由配置错误,可能会发生。
  • Proxy Authentication Required — 需要代理身份验证的企业代理。确保在代理 URL 中使用 http://user:password@host:port 格式。

历史相关问题

  • 内存模块中的 SSRF Guard 绕过 — Issue #4521 — withTrustedEnvProxyGuardedFetchMode() 未在嵌入路径中使用的相关修复。
  • 企业代理后 DNS 解析失败 — Issue #3204 — 企业环境中的通用 DNS + 代理解析问题。
  • 内存搜索返回空结果 — Issue #4892 — 当嵌入因代理 DNS 问题不可用时,症状可能表现为空结果。
  • Docker 中嵌入提供程序超时 — Issue #5103 — Docker 网络 + 代理交互导致内存操作超时。
  • 远程嵌入中 SSRF 策略未被遵循 — Issue #5017 — 关于 SSRF 防护未应用于内存嵌入获取调用的安全问题。

修复链中的关键依赖项

  • src/ssrf/fetch-guard.ts — 包含 fetchWithSsrFGuard(),即带有模式解析的核心获取函数
  • src/ssrf/fetch-mode.ts — 包含 hasProxyEnvConfigured()withTrustedEnvProxyGuardedFetchMode()
  • src/ssrf/dns-resolve.ts — 包含执行阻塞式 dns.lookup()resolvePinnedHostnameWithPolicy()
  • src/memory/post-json.ts — 包含需要修复的 withRemoteHttpResponse()

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。