feat(portable): 内网/局域网可用性工具包 + 启动绕代理 (v2.1.10)
便携版在内网/离线/受控浏览器环境下"配不上、用不了、查不清"的三个痛点工具化, Win + Mac 双端,纯 Node 零依赖脚本,中文提示由 node 打印(.bat 保持纯 ASCII, .command 保持 LF),三端真机验证(本机 Win / Mac mini / 客户机 pc-7512)。 新增工具: - lib/intranet-check.mjs (+ Windows-IntranetFix.bat / Mac-IntranetFix.command) 一键体检: 代理env + 直连可达 + 真发一条对话,分清"网络不通 vs 配置错"。 - lib/setup-local-model.mjs (+ Windows-LocalModel.bat / Mac-LocalModel.command) 纯命令行配 Ollama / newapi 并当场实测,绕开打不开的 Control UI;写前自动备份+merge。 - lib/resolve-no-proxy.mjs 接入 Windows-Start.bat / Mac-Start.command: 把配置里的模型 host 写进 NO_PROXY,避免系统代理劫持内网模型请求 (OpenClaw 自身只 bypass loopback CDP,不管模型 host)。 - OpenClaw-Doctor.bat / Mac-OpenClaw-Doctor.command: 官方 doctor 的隔离进阶入口 (只读、非交互;doctor 实跑慢且 TTY-only,故不进客户一键流程)。 测试: tests/windows-launchers.test.mjs 新增回归 - 客户面 .bat 必须纯 ASCII (UTF-8 中文会被 GBK cmd 读乱,报 usebackq 不是命令) - macOS .command 必须 LF-only (CRLF 触发 bad interpreter: /bin/bash^M) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
87
portable/lib/resolve-no-proxy.mjs
Normal file
87
portable/lib/resolve-no-proxy.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
// resolve-no-proxy.mjs — 让"内网/自建模型地址"绕开系统代理
|
||||
//
|
||||
// 背景(内网环境最大的坑):
|
||||
// 很多公司/机房的机器设置了 HTTP_PROXY / HTTPS_PROXY 环境变量(为了上外网)。
|
||||
// OpenClaw 启动时若检测到这两个变量,会 setGlobalDispatcher(new EnvHttpProxyAgent()),
|
||||
// 于是"所有" fetch——包括调用用户自己填的模型 baseUrl——都被塞进公司代理。
|
||||
// 当模型部署在内网(如 http://10.x / 192.168.x / 某机房 IP)时,代理够不着那台机器,
|
||||
// 请求直接失败。表现:互联网能连公网模型、reasonix/copilot 也能连内网,唯独本程序连不上。
|
||||
// 见 openclaw dist/auth-profiles-*.js 的 ensureGlobalUndiciEnvProxyDispatcher()。
|
||||
//
|
||||
// 方案:undici 的 EnvHttpProxyAgent 认 NO_PROXY。把用户配置里所有模型 baseUrl 的主机名
|
||||
// (IP 或域名)+ 本机回环地址,统一写进 NO_PROXY,让这些地址"直连不走代理"。
|
||||
// 纯增量、绝对安全:自建/内网模型本就不该走代理;没设代理时 NO_PROXY 也无副作用。
|
||||
//
|
||||
// 设计原则:静默失败。任何一步出错就不输出,启动照常(只是少了这层保护)。
|
||||
//
|
||||
// CLI 用法(供 .bat / .command source):
|
||||
// node resolve-no-proxy.mjs <CONFIG_PATH>
|
||||
// 输出(无代理需要保护时不输出任何内容):
|
||||
// UCLAW_NO_PROXY=localhost,127.0.0.1,::1,15.151.114.142,...
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
// 始终直连的本机地址。
|
||||
const ALWAYS = ['localhost', '127.0.0.1', '::1'];
|
||||
|
||||
// 从一个 baseUrl 字符串里抽出主机名(IP 或域名)。容错:解析不了就忽略。
|
||||
function hostOf(raw) {
|
||||
if (typeof raw !== 'string') return null;
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
// 补协议,URL() 才能解析 "host:port/v1" 这种缺协议的写法。
|
||||
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
||||
const host = new URL(withScheme).hostname; // 自动去掉 IPv6 的方括号
|
||||
return host || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归收集对象里所有 baseUrl 字段的主机名(providers 可能嵌套/命名各异,宽松收集最稳)。
|
||||
function collectHosts(node, out) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) collectHosts(item, out);
|
||||
return;
|
||||
}
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
if (key === 'baseUrl' || key === 'baseURL') {
|
||||
const h = hostOf(value);
|
||||
if (h) out.add(h);
|
||||
} else if (value && typeof value === 'object') {
|
||||
collectHosts(value, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const configPath = process.argv[2] || process.env.OPENCLAW_CONFIG_PATH;
|
||||
if (!configPath) return;
|
||||
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
||||
} catch {
|
||||
return; // 配置不存在/坏了:不输出,启动照常
|
||||
}
|
||||
|
||||
const hosts = new Set();
|
||||
collectHosts(config?.models, hosts);
|
||||
|
||||
// 合并已有的 NO_PROXY,避免覆盖用户/系统已有设置。
|
||||
const existing = (process.env.NO_PROXY || process.env.no_proxy || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const merged = Array.from(new Set([...existing, ...ALWAYS, ...hosts]));
|
||||
// 没有任何模型主机、又没有已有 NO_PROXY 时,只剩本机回环,输出也无害——但为简洁,
|
||||
// 仅当收集到了真实模型主机时才输出(本机回环本就不会被代理误伤到业务)。
|
||||
if (hosts.size === 0 && existing.length === 0) return;
|
||||
|
||||
process.stdout.write(`UCLAW_NO_PROXY=${merged.join(',')}\n`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user