下面是一份从零开始的完整教程,基于 Ubuntu 环境,手把手带你用 Node 写一个本地代理,把 Claude Code 发出的请求体清洗成 DeepSeek 兼容端点能识别的格式,解决 400 报错。核心清洗逻辑来自 的"白名单 + block 级规范化 + system 归位 + 去 beta 头"四板斧。
1. 核心思路
Claude Code 发出的请求是 Anthropic 风格的,而 DeepSeek 兼容端点只认经典 Messages API,多一个字段就 400 。做法是在本地 127.0.0.1 起一个 Node 代理,做中间人:
text
Claude Code ──→ http://127.0.0.1:<随机端口> ──→ DeepSeek 兼容上游
│
└─ proxy.js:清洗请求体、请求头
2. 从0开始:环境准备
2.1 安装 Node.js 18+
Ubuntu 下推荐用 nvm 安装:
bash
# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# 重新加载 shell
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# 安装 Node 18 或更高版本
nvm install 18
nvm use 18
# 验证
node -v
代理脚本只用 Node 内置模块,不需要
npm install任何依赖 。
2.2 确认 Claude Code 已安装
bash
claude --version
如果没有安装,先安装 Claude Code。
3. 创建代理脚本
创建一个工作目录,比如 ~/claude-deepseek-proxy,然后把下面的代码保存为 proxy.js。
这段代码实现了 中描述的清洗逻辑:
- 顶层字段只保留白名单;
- 消息里的
thinking、redacted_thinking、空text全部丢弃; tool_use只保留id/name/input;- 递归删除所有
cache_control; - 把
messages[]里的role: "system"提取出来合并到顶层system; - 删除
anthropic-beta等 400 元凶头 。
javascript
// proxy.js
const http = require("http");
const https = require("https");
const { URL } = require("url");
// 上游 DeepSeek 兼容端点地址,按需修改
const UPSTREAM_URL = process.env.UPSTREAM_URL || "https://tokenrhythm.studio";
const UPSTREAM_PATH = process.env.UPSTREAM_PATH || "/v1/messages";
// 顶层字段白名单,不在名单里的直接删除
const allowedTopLevel = new Set([
"model",
"max_tokens",
"messages",
"system",
"stream",
"stop_sequences",
"temperature",
"top_p",
"top_k",
"tools",
"tool_choice",
"metadata",
]);
// 清洗 messages 里的 block:
// 丢 thinking / redacted_thinking / 空 text,剥掉 cache_control
function cleanBlocks(blocks) {
const result = [];
for (const block of blocks) {
if (!block || typeof block !== "object") continue;
// 丢弃思考块
if (block.type === "thinking" || block.type === "redacted_thinking") continue;
// 丢弃空 text
if (block.type === "text" && !block.text) continue;
const cleanBlock = { type: block.type };
// tool_use 只留四件套
if (block.type === "tool_use") {
cleanBlock.id = block.id;
cleanBlock.name = block.name;
cleanBlock.input = block.input || {};
result.push(cleanBlock);
continue;
}
// tool_result 保留必要字段
if (block.type === "tool_result") {
cleanBlock.tool_use_id = block.tool_use_id;
cleanBlock.content = block.content;
if (block.is_error) cleanBlock.is_error = block.is_error;
result.push(cleanBlock);
continue;
}
// 普通 text 只保留 text
if (block.text !== undefined) cleanBlock.text = block.text;
// cache_control 等其他扩展字段一律不复制
result.push(cleanBlock);
}
return result;
}
// 整体清洗请求体
function sanitizeBody(rawBody) {
const parsed = JSON.parse(rawBody);
// 1. 顶层白名单
const clean = {};
for (const key of Object.keys(parsed)) {
if (allowedTopLevel.has(key)) clean[key] = parsed[key];
}
// 2. 清洗 messages
if (Array.isArray(clean.messages)) {
const systemParts = [];
const messages = [];
for (const msg of clean.messages) {
// system 不能混在 messages 里,提取出来
if (msg.role === "system") {
if (typeof msg.content === "string") {
systemParts.push(msg.content);
} else if (Array.isArray(msg.content)) {
for (const b of msg.content) {
if (b.type === "text" && b.text) systemParts.push(b.text);
}
}
continue;
}
// 清洗 content 里的 block
if (Array.isArray(msg.content)) {
msg.content = cleanBlocks(msg.content);
}
messages.push(msg);
}
clean.messages = messages;
// 3. 提取出的 system 合并到顶层 system 字段
if (systemParts.length > 0) {
const existing = Array.isArray(clean.system)
? clean.system.map((b) => b.text).join("
")
: clean.system || "";
clean.system = existing ? existing + "
" + systemParts.join("
") : systemParts.join("
");
}
}
return clean;
}
const server = http.createServer((req, res) => {
// 健康检查:启动后可用 curl 确认
if (req.url === "/__health") {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok");
return;
}
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
try {
const rawBody = Buffer.concat(chunks).toString("utf-8");
// 清洗请求体
let payload = "";
let cleanBody = null;
if (rawBody) {
cleanBody = sanitizeBody(rawBody);
payload = JSON.stringify(cleanBody);
}
// 清洗请求头:删 anthropic-beta、重写 host
const headers = { ...req.headers };
delete headers["anthropic-beta"]; // 最大的 400 来源
delete headers["host"];
headers["host"] = new URL(UPSTREAM_URL).host;
headers["content-length"] = Buffer.byteLength(payload);
const upstreamReq = https.request(
{
method: "POST",
hostname: new URL(UPSTREAM_URL).hostname,
port: new URL(UPSTREAM_URL).port || 443,
path: UPSTREAM_PATH,
headers,
},
(upstreamRes) => {
const respChunks = [];
upstreamRes.on("data", (c) => respChunks.push(c));
upstreamRes.on("end", () => {
const responseBody = Buffer.concat(respChunks);
if (upstreamRes.statusCode >= 400) {
// 可观测性:至少把上游错误打到控制台
console.error("上游返回错误:", responseBody.toString("utf-8"));
}
res.writeHead(upstreamRes.statusCode, upstreamRes.headers);
res.end(responseBody);
});
}
);
upstreamReq.on("error", (err) => {
console.error("代理转发失败:", err.message);
res.writeHead(502, { "content-type": "application/json" });
res.end(JSON.stringify({ error: { message: err.message } }));
});
upstreamReq.end(payload);
} catch (err) {
console.error("清洗请求体失败:", err.message);
res.writeHead(400, { "content-type": "application/json" });
res.end(JSON.stringify({ error: { message: err.message } }));
}
});
});
// 监听随机空闲端口,避免冲突
server.listen(0, "127.0.0.1", () => {
const port = server.address().port;
console.log(`代理已启动:http://127.0.0.1:${port}`);
// 把端口写到临时文件,方便其他脚本读取
const fs = require("fs");
const os = require("os");
const path = require("path");
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawgod-tokenrhythm-"));
fs.writeFileSync(path.join(tmpDir, "port"), String(port));
});
4. 启动代理
bash
cd ~/claude-deepseek-proxy
node proxy.js
终端会输出:
text
代理已启动:http://127.0.0.1:PORT
先用健康检查确认就绪:
bash
curl http://127.0.0.1:PORT/__health
# 返回 ok
5. 配置 Claude Code 指向本地代理
打开一个新终端,设置环境变量后启动 Claude Code:
bash
export ANTHROPIC_BASE_URL="http://127.0.0.1:PORT" # 替换成实际端口
export ANTHROPIC_AUTH_TOKEN="你的DeepSeek兼容端点API Key"
claude
此时 Claude Code 的所有请求都会先经过本地代理,清洗后再转发给 DeepSeek 兼容端点 。
6. 验证效果
你可以正常向 Claude Code 提问,例如:
text
请写一个 Python 快速排序
如果之前报 400,现在应该能正常返回。也可以在代理控制台看到请求流。
7. 常见问题排查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 代理启动失败 | Node 版本过低 | node -v 确认 18+ |
| 启动 Claude Code 后仍 400 | 端口配对错 / 没设环境变量 | 核对 ANTHROPIC_BASE_URL 是否指向实际端口 |
| 上游返回 400,代理控制台打印错误体 | 清洗逻辑未覆盖某些扩展字段 | 按 扩展白名单或 block 处理逻辑 |
| 上游返回 401/403 | API Key 错误 | 检查 ANTHROPIC_AUTH_TOKEN |
| 代理进程退出后 Claude Code 无法连接 | 没有后台运行 | 用 nohup node proxy.js & 或 systemd 守护 |
8. 进阶:后台运行与开机自启
8.1 nohup 后台运行
bash
nohup node proxy.js > proxy.log 2>&1 &
echo $! # 记录 PID
8.2 systemd 守护(可选,长期使用)
创建 /etc/systemd/system/claude-proxy.service:
ini
[Unit]
Description=Claude Code → DeepSeek Proxy
After=network.target
[Service]
ExecStart=/usr/bin/node /home/你的用户名/claude-deepseek-proxy/proxy.js
Restart=always
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
然后:
bash
sudo systemctl daemon-reload
sudo systemctl enable --now claude-proxy
journalctl -u claude-proxy -f # 查看日志
9. 总结
这套方案的核心不是"转发",而是 sanitize :顶层白名单、block 级规范化、system 归位、去 beta 头。只要你的 DeepSeek 兼容端点还认 Messages API,这套代理就可以稳定解决 400。后续如果上游协议变化,也只要调整 cleanBlocks 和 allowedTopLevel 即可。