使用本地代理解决 Hermes 和 OpenClaw 403
部分客户端直接配置 678API 官方地址时,可能因为客户端网络环境、请求头、证书或代理链路差异出现 403。可以在本机运行一个轻量 Node.js 中转服务,让客户端只连接本地地址,由代理转发到 678API。
如果 Cursor、Chatbox 等客户端可以正常使用 678API,但 Hermes 或 OpenClaw 直接填写 https://www.678api.com/v1 后返回 403,可以尝试本教程。该方案不会绕过账户权限、余额或模型权限校验,只是把客户端请求稳定地转发到 678API。
最终配置
http://127.0.0.1:8787/v1local-proxyhttps://www.wintoken.dev/v1https://www.678api.com/v1保存在本机代理配置文件中Windows 10 / 11(推荐:PowerShell 一键安装)
先安装 Node.js 18 或更高版本,安装完成后重新打开 PowerShell。普通用户权限即可,不需要以管理员身份运行。
按 Win 键,搜索并打开“PowerShell”。不要在 CMD 中执行下面的命令。
依次复制下面 3 行命令。脚本会提示输入 678API 的真实 API Key,输入时屏幕不会显示字符,这是正常的安全行为。
$installer = "$env:TEMP\api678-proxy-windows.ps1"
Invoke-WebRequest "https://doc.wintoken.dev/assets/api678-proxy-windows.ps1" -OutFile $installer
powershell -NoProfile -ExecutionPolicy Bypass -File $installer从 wintoken.dev 下载 Windows 安装脚本
安装器和代理核心从 doc.wintoken.dev 下载,代理默认连接 https://www.wintoken.dev/v1,不依赖被阻断的 678api.com 域名。代理会安装到 %LOCALAPPDATA%\API678\Proxy,仅监听本机 127.0.0.1:8787,并添加当前 Windows 用户登录后的自动启动项。
如果当前网络可以正常访问 678api.com,也可以在安装时主动指定原地址:
powershell -NoProfile -ExecutionPolicy Bypass -File $installer -UpstreamBaseUrl "https://www.678api.com/v1"Windows 安装后验证
安装成功后,在 PowerShell 中运行:
Invoke-RestMethod "http://127.0.0.1:8787/health"
Invoke-RestMethod "http://127.0.0.1:8787/v1/models" -Headers @{ Authorization = "Bearer local-proxy" }第一条命令应返回 ok: true,第二条命令应返回模型数据。若 Windows 防火墙询问是否允许 Node.js 通信,可以取消公网访问;本教程只需要本机回环地址。
Windows 上配置 Hermes / OpenClaw
在 Hermes 或 OpenClaw 的模型/供应商设置中新增 OpenAI Compatible / Custom OpenAI Endpoint。
填写 http://127.0.0.1:8787/v1,不要追加 /chat/completions。
填写 local-proxy。不要把真实 678API Key 再填进客户端。
Hermes 退出后重新打开;OpenClaw 保存后重启 Gateway 或重新打开客户端,避免继续使用缓存的旧地址。
Windows 日常维护
查看实时日志:
Get-Content "$env:LOCALAPPDATA\API678\Proxy\api678-proxy.log" -Wait更换真实 API Key:
notepad "$env:LOCALAPPDATA\API678\Proxy\api678-proxy.env"保存配置后重新运行安装脚本即可安全重启代理;脚本只会停止它自己记录的代理进程,不会结束电脑上的其他 Node.js 程序。若需检查端口占用:
Get-NetTCPConnection -LocalPort 8787 -ErrorAction SilentlyContinue出现“找不到 node”时,请安装 Node.js 18+ 并重新打开 PowerShell;出现“禁止运行脚本”时,请使用上面的 -ExecutionPolicy Bypass 命令;健康检查失败时,查看 api678-proxy.err.log,并确认 8787 端口未被占用。
macOS / Linux 手动配置
下面是原有的 macOS / Linux 教程。Windows 用户完成上面的一键安装和客户端配置后,无需继续执行这一部分。
准备目录
mkdir -p ~/api678-proxy
cd ~/api678-proxy创建代理脚本
创建文件 api678-proxy.mjs:
#!/usr/bin/env node
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { URL, fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
loadEnvFile(path.join(scriptDir, "api678-proxy.env"));
const listenHost = process.env.LISTEN_HOST || "127.0.0.1";
const listenPort = Number(process.env.PORT || "8787");
const upstreamBase = process.env.UPSTREAM_BASE_URL || "https://www.678api.com/v1";
const upstreamApiKey = process.env.UPSTREAM_API_KEY || "";
const localPrefix = normalizePrefix(process.env.LOCAL_PREFIX || "/v1");
const upstreamTimeoutMs = Number(process.env.UPSTREAM_TIMEOUT_MS || "300000");
if (!upstreamBase) {
console.error("Missing UPSTREAM_BASE_URL");
process.exit(1);
}
const baseUrl = new URL(upstreamBase);
baseUrl.pathname = stripTrailingSlash(baseUrl.pathname);
const server = http.createServer(async (req, res) => {
req.socket?.setNoDelay(true);
res.socket?.setNoDelay(true);
if (req.method === "OPTIONS") {
writeCors(res);
res.writeHead(204);
res.end();
return;
}
if (req.url === "/health" || req.url === "/") {
writeCors(res);
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ ok: true, upstream: redactUrl(baseUrl) }));
return;
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs);
try {
const target = buildTargetUrl(req.url);
const headers = buildHeaders(req.headers);
const body = ["GET", "HEAD"].includes((req.method || "GET").toUpperCase()) ? undefined : req;
const upstreamResp = await fetch(target, {
method: req.method,
headers,
body,
duplex: "half",
redirect: "manual",
signal: controller.signal,
});
writeCors(res);
for (const [key, value] of upstreamResp.headers.entries()) {
if (!isHopByHopHeader(key) && !["content-encoding", "content-length"].includes(key.toLowerCase())) {
res.setHeader(key, value);
}
}
res.writeHead(upstreamResp.status, upstreamResp.statusText);
res.flushHeaders();
if (upstreamResp.body) {
for await (const chunk of upstreamResp.body) res.write(Buffer.from(chunk));
}
res.end();
} catch (error) {
const status = error?.name === "AbortError" ? 504 : 502;
writeCors(res);
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ error: { message: String(error?.message || error), type: "api678_proxy_error" } }));
} finally {
clearTimeout(timeout);
}
});
server.listen(listenPort, listenHost, () => {
console.log("678API proxy listening on http://" + listenHost + ":" + listenPort);
console.log("Local Base URL: http://" + listenHost + ":" + listenPort + localPrefix);
console.log("Forwarding to: " + redactUrl(baseUrl));
});
function buildTargetUrl(requestUrl) {
const incoming = new URL(requestUrl || "/", "http://" + listenHost + ":" + listenPort);
let pathname = incoming.pathname;
if (localPrefix && (pathname === localPrefix || pathname.startsWith(localPrefix + "/"))) {
pathname = pathname.slice(localPrefix.length) || "/";
}
const target = new URL(baseUrl);
target.pathname = joinPath(baseUrl.pathname, pathname);
target.search = incoming.search;
return target;
}
function buildHeaders(incomingHeaders) {
const headers = new Headers();
for (const [key, value] of Object.entries(incomingHeaders)) {
if (!value || isHopByHopHeader(key)) continue;
const lower = key.toLowerCase();
if (["host", "origin", "referer", "content-length"].includes(lower)) continue;
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
}
headers.set("accept", incomingHeaders.accept || "application/json");
headers.set("user-agent", process.env.UPSTREAM_USER_AGENT || "api678-local-proxy/1.0");
if (upstreamApiKey) headers.set("authorization", "Bearer " + upstreamApiKey);
return headers;
}
function writeCors(res) {
res.setHeader("access-control-allow-origin", "*");
res.setHeader("access-control-allow-methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
res.setHeader("access-control-allow-headers", "authorization,content-type,x-requested-with");
}
function isHopByHopHeader(name) {
return ["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"].includes(name.toLowerCase());
}
function normalizePrefix(prefix) {
if (!prefix || prefix === "/") return "";
return "/" + prefix.replace(/^\/+|\/+$/g, "");
}
function joinPath(basePath, requestPath) {
return stripTrailingSlash(basePath) + (requestPath.startsWith("/") ? requestPath : "/" + requestPath);
}
function stripTrailingSlash(pathname) {
if (!pathname || pathname === "/") return "";
return pathname.replace(/\/+$/g, "");
}
function redactUrl(url) {
const safe = new URL(url);
safe.username = "";
safe.password = "";
return safe.toString();
}
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const rawLine of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) continue;
const normalizedLine = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
const separatorIndex = normalizedLine.indexOf("=");
if (separatorIndex === -1) continue;
const key = normalizedLine.slice(0, separatorIndex).trim();
let value = normalizedLine.slice(separatorIndex + 1).trim();
if (!key || process.env[key] !== undefined) continue;
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
process.env[key] = value;
}
}创建配置文件
创建文件 api678-proxy.env,把 YOUR_API_KEY 换成 678API 控制台创建的真实 API Key:
LISTEN_HOST=127.0.0.1
PORT=8787
UPSTREAM_BASE_URL=https://www.678api.com/v1
UPSTREAM_API_KEY=YOUR_API_KEY
LOCAL_PREFIX=/v1
UPSTREAM_TIMEOUT_MS=300000
UPSTREAM_USER_AGENT=api678-local-proxy/1.0api678-proxy.env 里包含真实 API Key,请不要上传到公开仓库、截图或发给他人。
启动和测试
cd ~/api678-proxy
chmod +x api678-proxy.mjs
node api678-proxy.mjs另开一个终端测试:
curl http://127.0.0.1:8787/health
curl http://127.0.0.1:8787/v1/models -H "Authorization: Bearer local-proxy"如果 /v1/models 返回模型列表,说明本地代理已经可用。
Hermes 配置
在 Hermes 的模型或供应商设置中选择 OpenAI Compatible / Custom OpenAI Endpoint。
http://127.0.0.1:8787/v1
local-proxy。真实 Key 已由本地代理从 api678-proxy.env 注入。
模型名使用 678API 控制台可用模型。建议开启 Streaming / Stream,提高首 token 体验。
OpenClaw 配置
在 OpenClaw 的模型配置中新增自定义服务商。
Base URL 填 http://127.0.0.1:8787/v1,不要再追加 /chat/completions。
API Key 填 local-proxy。
保存后重启 OpenClaw Gateway 或重新打开客户端,让配置生效。
macOS 登录自启动
如果希望每次登录 macOS 后自动启动代理,可创建 LaunchAgent:
NODE_BIN="$(command -v node)"
mkdir -p "$HOME/Library/LaunchAgents" "$HOME/Library/Logs"
cat > "$HOME/Library/LaunchAgents/dev.api678.proxy.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>dev.api678.proxy</string>
<key>ProgramArguments</key>
<array>
<string>$NODE_BIN</string>
<string>$HOME/api678-proxy/api678-proxy.mjs</string>
</array>
<key>WorkingDirectory</key>
<string>$HOME/api678-proxy</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$HOME/Library/Logs/api678-proxy.log</string>
<key>StandardErrorPath</key>
<string>$HOME/Library/Logs/api678-proxy.err.log</string>
</dict>
</plist>
EOF
plutil -lint "$HOME/Library/LaunchAgents/dev.api678.proxy.plist"
launchctl bootout gui/$(id -u) "$HOME/Library/LaunchAgents/dev.api678.proxy.plist" 2>/dev/null || true
launchctl bootstrap gui/$(id -u) "$HOME/Library/LaunchAgents/dev.api678.proxy.plist"
launchctl kickstart -k gui/$(id -u)/dev.api678.proxy常见问题
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
本地 /health 访问失败 | 代理未启动、Node.js 未安装或端口被占用 | 检查终端日志;确认 Node.js 版本为 18 或更高;换一个端口后同步修改客户端 Base URL。 |
/v1/models 返回 401 | 真实 API Key 错误或被禁用 | 重新复制 678API 控制台的 API Key,更新 api678-proxy.env 后重启代理。 |
/v1/models 返回 403 | 账户、分组或模型权限不满足 | 确认账户有可用额度和模型权限;换一个控制台可用模型测试。 |
| 客户端仍然请求官方地址 | Hermes / OpenClaw 配置没有保存或缓存旧 provider | 删除旧 provider 后重新添加;保存后重启客户端或 Gateway。 |
Bootstrap failed: 5 | plist 文件不存在、XML 格式错误、路径没有替换成真实路径,或旧服务状态冲突 | 先确认 ~/Library/LaunchAgents/dev.api678.proxy.plist 存在;执行 plutil -lint;再运行 launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/dev.api678.proxy.plist 2>/dev/null || true 后重新 bootstrap。 |
| 响应速度慢 | 上游模型首 token 慢、模型较重或未开启流式输出 | 开启 Streaming;先用轻量模型测试;减少上下文长度。 |