678APIdocs
https://www.678api.com/v1
Hermes / OpenClaw

使用本地代理解决 Hermes 和 OpenClaw 403

部分客户端直接配置 678API 官方地址时,可能因为客户端网络环境、请求头、证书或代理链路差异出现 403。可以在本机运行一个轻量 Node.js 中转服务,让客户端只连接本地地址,由代理转发到 678API。

适用场景

如果 Cursor、Chatbox 等客户端可以正常使用 678API,但 Hermes 或 OpenClaw 直接填写 https://www.678api.com/v1 后返回 403,可以尝试本教程。该方案不会绕过账户权限、余额或模型权限校验,只是把客户端请求稳定地转发到 678API。

最终配置

Hermes / OpenClaw Base URLhttp://127.0.0.1:8787/v1
Hermes / OpenClaw API Keylocal-proxy
Windows 默认上游https://www.wintoken.dev/v1
macOS / Linux 默认上游https://www.678api.com/v1
真实 API Key保存在本机代理配置文件中

Windows 10 / 11(推荐:PowerShell 一键安装)

先安装 Node.js 18 或更高版本,安装完成后重新打开 PowerShell。普通用户权限即可,不需要以管理员身份运行。

打开 PowerShell

按 Win 键,搜索并打开“PowerShell”。不要在 CMD 中执行下面的命令。

下载并运行安装脚本

依次复制下面 3 行命令。脚本会提示输入 678API 的真实 API Key,输入时屏幕不会显示字符,这是正常的安全行为。

text
$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,也可以在安装时主动指定原地址:

text
powershell -NoProfile -ExecutionPolicy Bypass -File $installer -UpstreamBaseUrl "https://www.678api.com/v1"

Windows 安装后验证

安装成功后,在 PowerShell 中运行:

text
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

添加 OpenAI Compatible 服务商

在 Hermes 或 OpenClaw 的模型/供应商设置中新增 OpenAI Compatible / Custom OpenAI Endpoint。

填写本地 Base URL

填写 http://127.0.0.1:8787/v1,不要追加 /chat/completions。

填写本地占位 Key

填写 local-proxy。不要把真实 678API Key 再填进客户端。

保存并彻底重启客户端

Hermes 退出后重新打开;OpenClaw 保存后重启 Gateway 或重新打开客户端,避免继续使用缓存的旧地址。

Windows 日常维护

查看实时日志:

text
Get-Content "$env:LOCALAPPDATA\API678\Proxy\api678-proxy.log" -Wait

更换真实 API Key:

text
notepad "$env:LOCALAPPDATA\API678\Proxy\api678-proxy.env"

保存配置后重新运行安装脚本即可安全重启代理;脚本只会停止它自己记录的代理进程,不会结束电脑上的其他 Node.js 程序。若需检查端口占用:

text
Get-NetTCPConnection -LocalPort 8787 -ErrorAction SilentlyContinue
Windows 常见错误

出现“找不到 node”时,请安装 Node.js 18+ 并重新打开 PowerShell;出现“禁止运行脚本”时,请使用上面的 -ExecutionPolicy Bypass 命令;健康检查失败时,查看 api678-proxy.err.log,并确认 8787 端口未被占用。

macOS / Linux 手动配置

下面是原有的 macOS / Linux 教程。Windows 用户完成上面的一键安装和客户端配置后,无需继续执行这一部分。

准备目录

text
mkdir -p ~/api678-proxy
cd ~/api678-proxy

创建代理脚本

创建文件 api678-proxy.mjs:

text
#!/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:

text
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.0
安全提醒

api678-proxy.env 里包含真实 API Key,请不要上传到公开仓库、截图或发给他人。

启动和测试

text
cd ~/api678-proxy
chmod +x api678-proxy.mjs
node api678-proxy.mjs

另开一个终端测试:

text
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 配置

添加 OpenAI Compatible 服务商

在 Hermes 的模型或供应商设置中选择 OpenAI Compatible / Custom OpenAI Endpoint。

填写 Base URL

http://127.0.0.1:8787/v1

填写 API Key

local-proxy。真实 Key 已由本地代理从 api678-proxy.env 注入。

选择模型并开启流式输出

模型名使用 678API 控制台可用模型。建议开启 Streaming / Stream,提高首 token 体验。

OpenClaw 配置

添加自定义 OpenAI Compatible Provider

在 OpenClaw 的模型配置中新增自定义服务商。

填写本地地址

Base URL 填 http://127.0.0.1:8787/v1,不要再追加 /chat/completions。

填写占位密钥

API Key 填 local-proxy。

重启网关或客户端

保存后重启 OpenClaw Gateway 或重新打开客户端,让配置生效。

macOS 登录自启动

如果希望每次登录 macOS 后自动启动代理,可创建 LaunchAgent:

text
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: 5plist 文件不存在、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;先用轻量模型测试;减少上下文长度。
© 2026 678API · AI API Gateway Documentation