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:
hfshfg
2026-06-22 18:51:16 +08:00
parent 7805ee01f7
commit 3439db32ad
13 changed files with 815 additions and 2 deletions

View 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();