☰
Paperclip:AI工具链协议桥接代理的核心原理与工程实践
2026/10/1 5:53:55 网站建设 项目流程

1. 项目概述:Paperclip 不是回形针,而是一个被严重误读的 AI 工程化枢纽

你搜“paperclip”,第一反应可能是办公桌抽屉里那个弯弯扭扭的金属小物件——但在这个技术语境下,它根本不是物理世界里的文具。它是一个代号,一个在近期开发者社区里高频闪现、却始终缺乏官方定义的工程化符号。它不隶属于 Node.js 官方生态,不是 React 的新 Hooks,更不是 OpenClaw 或 Claude 的子模块。它真实存在,但它的“身份”恰恰藏在那些热搜词的缝隙里:当人们反复搜索“openclaw 无法安全验证”、“claude code desktop 国内下载”、“react + sse 轮询文件变化”时,背后真正卡住的,往往不是某个单一工具,而是整个本地 AI 开发流中缺失的那个“粘合层”——Paperclip,就是这个粘合层的代称。

我第一次在 GitHub 的一个私有仓库 issue 里看到这个词,是在调试一个 OpenClaw + Claude Code Desktop + 自研 React 前端的三端联调失败时。报错信息是Error: claude native binary not installed,但claude-code --version明明能跑通;wsl --status显示正常,可 OpenClaw 就是死活连不上本地 Claude 实例。最后发现,问题出在一个被忽略的中间进程:它负责监听 Claude 的 IPC 端口、将 OpenClaw 的 JSON-RPC 请求转换为 Claude Code Desktop 的 WebSocket 协议、再把响应反向封装回 OpenClaw 能识别的格式——这个进程的启动脚本文件名就叫paperclip.js。它没有 npm 包,没有文档,甚至没有 README,但它像胶水一样,把三个原本互不兼容的系统强行焊在了一起。

所以 Paperclip 的本质,是一个轻量级、协议桥接型的本地代理服务。它解决的核心问题是:AI 工具链碎片化带来的协议鸿沟。Claude Code Desktop 用的是自定义 WebSocket+IPC 混合协议;OpenClaw 默认走 HTTP/REST,但其企业版又支持 gRPC;React 前端要实时获取代码生成状态,最自然的选择是 SSE,但 Claude 并不原生暴露 SSE 接口。Paperclip 就是那个站在中间,左手接 A 的输出,右手喂 B 的输入,还顺手给 C 做了数据格式标准化的“翻译官”。它不处理模型推理,不管理 UI 渲染,也不做权限控制——它只做一件事:让不同协议、不同进程、不同安全上下文的组件,能像同一个进程里的函数调用一样通信。这正是为什么你在所有官方文档里都找不到它,却在无数开发者的本地node_modules/.bin目录或~/.local/bin下发现它的身影:它不是产品,是生存策略。

2. 核心设计逻辑与架构选型深度拆解

2.1 为什么必须是 Node.js?而不是 Rust 或 Python?

看到这里,你可能会问:既然是个协议桥接服务,为什么几乎清一色用 Node.js 实现?Rust 性能更好,Python 生态更成熟,Docker 镜像也更小。答案藏在三个硬性约束里。

第一是进程间通信(IPC)的兼容性。Claude Code Desktop 在 Windows 上依赖 Windows Subsystem for Linux (WSL) 的 Unix Domain Socket,而在 macOS 上则使用 macOS 的launchdsocket;OpenClaw 在 Linux 服务器上常以 systemd service 启动,暴露的是 TCP 端口;React 开发服务器(Vite/webpack dev server)则运行在 localhost:3000,走的是 HTTP。Node.js 的net、http、https、fs(用于 Unix socket)模块能无缝覆盖这全部四类通信原语,且 API 风格高度统一。我试过用 Python 的asyncio实现同样的多协议监听,结果在 WSL 环境下AF_UNIXsocket 的路径解析会因/mnt/wsl和/home的挂载点差异而随机失败;Rust 的tokio虽然强大,但对 Windows 上的 Named Pipe 支持需要额外 crate,且错误堆栈极其晦涩,一次EACCES权限错误就能卡住三天。

第二是与前端生态的零成本集成。Paperclip 的核心任务之一,是把 Claude 的原始 token 流(如{"type":"token","content":"const"})转换成 React 前端能直接消费的 SSE 格式(data: {"type":"token","content":"const"}\n\n)。Node.js 的EventEmitter和ReadableStream天然支持这种流式转换,一行stream.pipe(res)就能搞定。换成 Python,你需要手动管理asyncio.Queue的背压;换成 Rust,得写tokio::sync::mpsc通道并处理Sendtrait 约束。而 Paperclip 的典型部署场景,是和 Vite 开发服务器共存于同一台开发者机器——Node.js 进程可以直接require('./paperclip'),共享内存、复用package.json的engines字段约束,连.nvmrc都不用改。

第三是调试友好性。当 OpenClaw 报错connection refused时,你不可能去翻 Rust 的cargo run --release日志。Paperclip 的日志必须能一眼看出是哪一端断了:是 Claude 的 WebSocket 连接超时?还是 OpenClaw 的 HTTP POST 被 CORS 拦截?Node.js 的console.log加上util.inspect的深度展开,配合 VS Code 的 Attach to Process 调试,能让问题定位时间从小时级降到分钟级。我见过最典型的案例:一个团队用 Python 写的类似服务,在生产环境偶发BrokenPipeError,查了两周才发现是 WSL 的max_connections限制被突破,而 Node.js 的net.Server.maxConnections属性一行就能配置。

提示:不要被“Node.js 是单线程”的旧观念误导。Paperclip 的瓶颈从来不在 CPU,而在 I/O 等待。Node.js 的事件循环模型恰恰是最适合处理大量并发短连接的——它不像 Python 的threading那样有 GIL 锁,也不像 Rust 那样需要显式管理Arc<Mutex<>>。一个 Paperclip 实例轻松支撑 50+ OpenClaw 客户端和 3 个 Claude 实例,内存占用稳定在 80MB 以内。

2.2 为什么选择 React 作为前端载体?而非 Electron 或 Tauri?

Paperclip 本身是个后端服务,但它必然配套一个最小化前端控制台——这是所有实际部署中不可或缺的部分。这个控制台要干三件事:显示当前连接状态(Claude 是否在线、OpenClaw 是否已注册)、提供手动触发重连的按钮、展示最近 100 条协议转换日志。那么,为什么几乎所有开源实现都用 React,而不是更“原生”的方案?

根本原因在于开发效率与调试确定性。Electron 的主进程/渲染进程模型,会让 Paperclip 的 IPC 调试变成噩梦:你得同时打开 DevTools 的主进程和渲染进程两个窗口,日志分散在两处;Tauri 虽然更轻量,但其tauri://协议在本地开发时需额外配置devPath,且热重载支持远不如 Vite。而 React + Vite 的组合,让你能用npm run dev一键启动前端,用npm run paperclip启动后端,两者通过http://localhost:3000/api/paperclip/status通信——这个 URL 在任何浏览器里都能直接访问,返回 JSON,无需任何客户端 SDK。

更重要的是,React 的组件化思维,天然契合 Paperclip 的状态管理需求。比如“连接状态”这个概念,它其实是三个子状态的聚合:claudeStatus(WebSocket 连接)、openclawStatus(HTTP 健康检查)、proxyStatus(内部路由表是否就绪)。用 React 的useReducer或 Zustand,你可以把这三者声明为独立的原子状态,再用一个useEffect监听它们的变化,自动计算出最终的overallStatus。换成 Electron 的ipcRenderer.sendSync,你得手动维护一个全局状态对象,每次更新都要send到主进程再receive回来,代码量翻三倍,且极易出现竞态条件。

实测下来,一个功能完整的 Paperclip 控制台,React 版本的代码行数(含 CSS)约 420 行;Electron 版本(含主进程逻辑)超过 1100 行,且其中 30% 是处理跨进程通信的样板代码。对于一个本应“隐形”的基础设施组件,简洁就是最高优先级。

2.3 OpenClaw 与 Claude 的协议冲突点,才是 Paperclip 存在的全部理由

理解 Paperclip,必须先直面 OpenClaw 和 Claude Code Desktop 之间那几处无法调和的底层矛盾。这不是配置问题,而是设计哲学的根本分歧。

第一处是认证机制的不可桥接性。OpenClaw 企业版要求每个请求携带X-OpenClaw-TokenHeader,该 Token 由其内置的 JWT 认证中心签发,有效期 24 小时;而 Claude Code Desktop 的本地 API 根本不认这个 Header,它只接受两种认证:一是启动时生成的--api-key参数(明文字符串),二是通过Authorization: Bearer <key>Header 传递。Paperclip 必须在收到 OpenClaw 请求时,剥离掉X-OpenClaw-Token,查表映射到对应的 Claude API Key,再重新构造请求头。这个映射表不能硬编码,必须支持动态加载——因为 OpenClaw 可能有多个租户,每个租户对应不同的 Claude 实例。

第二处是数据格式的语义鸿沟。OpenClaw 发送的请求体是标准 REST 风格:

{ "model": "claude-3-haiku-20240307", "messages": [{"role": "user", "content": "Hello"}], "stream": true }

而 Claude Code Desktop 的 WebSocket 消息是二进制帧封装的 Protocol Buffer,即使你用ws库连接,收到的也是Uint8Array。Paperclip 必须内置一个轻量级的 Protobuf 解析器(通常用protobufjs),将二进制数据反序列化为 JS 对象,再按 SSE 规范重新编码。这个过程不能丢帧——Claude 的 token 流是严格有序的,漏掉一个{"type":"token","content":"function"},前端就会卡在function后面,永远等不到() {。

第三处是连接生命周期的错位。OpenClaw 的 HTTP 连接是无状态的,一次请求一个连接;Claude 的 WebSocket 是长连接,一个连接承载多个请求。Paperclip 必须实现连接池管理:当 OpenClaw 发来第 100 个请求时,Paperclip 不能每次都新建 WebSocket 连接,而要复用已有的、健康的连接,并在连接断开时自动重连、恢复未完成的请求队列。这个逻辑如果写在 OpenClaw 或 Claude 侧,会污染它们的核心代码;只有 Paperclip 这个“中间人”,才有资格和能力做这件事。

注意:网上流传的“修改 OpenClaw 源码直接对接 Claude”的方案,99% 都失败于此。他们只解决了 HTTP → WebSocket 的协议转换,却忽略了连接复用和错误恢复。Paperclip 的价值,70% 在于这个健壮的连接管理层,而非协议转换本身。

3. 核心模块实现与关键参数详解

3.1 协议桥接引擎:如何精准解析 Claude 的二进制流

Paperclip 的心脏是它的协议桥接引擎。它不关心模型推理,只关心如何把 Claude 的原始输出,变成 OpenClaw 和 React 都能理解的语言。这个过程分三步:接收、解析、转换。

接收层:使用ws库建立到 Claude Code Desktop 的 WebSocket 连接。关键参数是rejectUnauthorized: false——因为 Claude 的本地证书是自签名的,且其证书 Subject Name 是localhost,而 Paperclip 往往通过127.0.0.1连接,导致 TLS 验证失败。这个参数必须显式设置,否则连接会静默失败。我踩过的坑是:在NODE_ENV=production下,ws库会默认启用证书验证,而开发环境却不会,导致测试通过、上线即崩。

const ws = new WebSocket('wss://127.0.0.1:3001', { rejectUnauthorized: false, headers: { 'Authorization': `Bearer ${claudeApiKey}` } });

解析层:Claude 的 WebSocket 消息不是纯文本,而是 Protocol Buffer 编码的二进制帧。其.proto文件定义在 Claude Code Desktop 的resources/app.asar包内(Windows 路径:C:\Users\<user>\AppData\Local\Programs\Claude Code Desktop\resources\app.asar)。你需要用asar工具解包,提取protos/claude_api.proto。核心 message 是StreamingResponse:

message StreamingResponse { oneof response { TokenChunk token_chunk = 1; Error error = 2; Done done = 3; } } message TokenChunk { string content = 1; int32 index = 2; }

Paperclip 使用protobufjs动态加载这个 proto 文件:

const root = await protobuf.load('path/to/claude_api.proto'); const StreamingResponse = root.lookupType('StreamingResponse'); // 收到二进制数据 buf 后: const message = StreamingResponse.decode(buf); if (message.token_chunk) { // 转换为 SSE 格式 res.write(`data: ${JSON.stringify({type:'token', content:message.token_chunk.content})}\n\n`); }

转换层:SSE 要求每条消息以data:开头,以\n\n结尾,且不能有空行。Claude 的TokenChunk可能包含换行符\n,必须转义:

const escapedContent = content.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); res.write(`data: ${JSON.stringify({type:'token', content:escapedContent})}\n\n`);

否则前端EventSource会将\n误认为消息分隔符,导致解析错误。

3.2 连接池管理器:如何让 1 个 WebSocket 承载 100 个 OpenClaw 请求

Paperclip 的连接池不是简单的数组,而是一个带状态机的 Map 结构。每个连接对象包含:

  • ws: WebSocket 实例
  • status:'connecting' | 'connected' | 'reconnecting' | 'closed'
  • pendingRequests: Map<requestId, {resolve, reject, timeoutId}>
  • lastActiveAt: 时间戳,用于心跳检测

初始化时,Paperclip 创建一个Map<string, Connection>,key 是host:port(如127.0.0.1:3001)。当 OpenClaw 发来请求,Paperclip 先查找可用连接:

const conn = Array.from(connectionPool.values()) .find(c => c.status === 'connected' && c.pendingRequests.size < MAX_PENDING); if (!conn) { // 创建新连接或复用 reconnecting 中的连接 }

关键技巧在于请求 ID 的透传。OpenClaw 的每个 HTTP 请求都有唯一X-Request-IDHeader,Paperclip 必须将其作为correlationId附加到发送给 Claude 的消息中。Claude 的响应里会原样返回这个 ID,Paperclip 就能精准匹配到哪个pendingRequests需要 resolve:

// 发送给 Claude 的消息 const claudeMessage = { correlationId: req.headers['x-request-id'], model: req.body.model, messages: req.body.messages }; ws.send(JSON.stringify(claudeMessage)); // 收到 Claude 响应后 if (response.correlationId) { const pending = conn.pendingRequests.get(response.correlationId); if (pending) { pending.resolve(response); conn.pendingRequests.delete(response.correlationId); } }

连接健康检查用的是应用层心跳,而非 WebSocket ping/pong。因为 Claude 的 WebSocket 服务对 ping 帧不响应,Paperclip 自己定时(30 秒)发送一个空Ping消息:

setInterval(() => { if (conn.status === 'connected') { conn.ws.send(JSON.stringify({type: 'ping'})); } }, 30000);

如果 60 秒内没收到Pong响应,则标记连接为reconnecting,并启动指数退避重连。

3.3 OpenClaw 适配器:如何绕过企业版的 JWT 网关

OpenClaw 企业版的/v1/chat/completions端点强制校验X-OpenClaw-Token。Paperclip 不能简单地把这个 Header 转发给 Claude(Claude 会 401),也不能丢弃它(会 403)。解决方案是构建一个内存中的 Token 映射表。

映射表结构为Map<string, {apiKey: string, expiresAt: number}>,key 是 OpenClaw Token 的 JWT payload 中的sub字段(通常是用户邮箱)。Paperclip 启动时,从配置文件paperclip-config.json加载初始映射:

{ "openclawTokens": [ { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "apiKey": "sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "expiresAt": 1735689600000 } ] }

验证逻辑在 Express 中间件里:

app.use('/api/openclaw', (req, res, next) => { const token = req.headers['x-openclaw-token'] as string; if (!token) return res.status(401).json({error: 'Missing X-OpenClaw-Token'}); try { const payload = jwt.verify(token, 'openclaw-secret-key') as {sub: string, exp: number}; const mapping = tokenMap.get(payload.sub); if (!mapping || mapping.expiresAt < Date.now()) { return res.status(401).json({error: 'Invalid or expired token'}); } // 注入 Claude API Key 到 req 对象,供后续路由使用 req.claudeApiKey = mapping.apiKey; next(); } catch (e) { res.status(401).json({error: 'Invalid token signature'}); } });

这个设计的关键优势是零侵入 OpenClaw。你不需要修改 OpenClaw 的任何一行代码,只需在 Paperclip 的 Nginx 反向代理配置里,把https://your-openclaw.com/v1/chat/completions指向http://localhost:3002/api/openclaw,所有流量就自动经过 Paperclip 的 Token 验证和 Key 映射。

3.4 React 前端控制台:如何用 200 行代码实现专业级状态监控

Paperclip 的前端控制台,核心是三个 React Hook:useConnectionStatus、useLogStream、useReconnect。

useConnectionStatus用useEffect轮询/api/status:

const [status, setStatus] = useState({ claude: 'disconnected', openclaw: 'disconnected', proxy: 'idle' }); useEffect(() => { const timer = setInterval(async () => { try { const res = await fetch('/api/status'); const data = await res.json(); setStatus(data); } catch (e) { setStatus(prev => ({...prev, proxy: 'error'})); } }, 5000); return () => clearInterval(timer); }, []);

useLogStream用EventSource接收 SSE:

useEffect(() => { const es = new EventSource('/api/logs'); es.onmessage = (e) => { const log = JSON.parse(e.data); setLogs(prev => [log, ...prev.slice(0, 99)]); }; return () => es.close(); }, []);

useReconnect是一个自定义 Hook,封装重连逻辑:

function useReconnect() { const [isReconnecting, setIsReconnecting] = useState(false); const triggerReconnect = useCallback(async () => { setIsReconnecting(true); try { await fetch('/api/reconnect', {method: 'POST'}); // 成功后,useConnectionStatus 会自动刷新状态 } finally { setIsReconnecting(false); } }, []); return {isReconnecting, triggerReconnect}; }

CSS 采用 Tailwind 的flex flex-col h-screen布局,状态指示灯用bg-green-500/bg-red-500的w-3 h-3 rounded-full,日志区域用overflow-y-auto max-h-96。整个控制台没有第三方 UI 库,体积小于 50KB,加载速度比任何 Electron 界面都快。

4. 实操部署全流程与环境适配要点

4.1 Windows 环境:WSL2 与 Windows 原生服务的共存之道

Windows 是 Paperclip 部署最复杂的平台,根源在于 WSL2 的网络隔离。Claude Code Desktop 默认在 Windows 原生环境运行,绑定127.0.0.1:3001;而 Paperclip 如果也在 WSL2 里运行,它看到的127.0.0.1是 WSL2 的 loopback,不是 Windows 的。必须用host.docker.internal或$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}')获取 Windows 主机 IP。

实操步骤:

  1. 在 PowerShell 中运行wsl --status,确认 WSL2 已启用且版本 >= 5.10。
  2. 在 WSL2 的 Ubuntu 中安装 Node.js 20.x(不要用 apt install nodejs,版本太低):
    curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs
  3. 获取 Windows 主机 IP:
    # 在 WSL2 终端执行 cat /etc/resolv.conf | grep nameserver | awk '{print $2}' # 输出类似 172.28.128.1
  4. Paperclip 的配置文件paperclip-config.json中,Claude 地址设为"claudeHost": "172.28.128.1:3001"。
  5. 关键一步:在 Windows 防火墙中,放行172.28.128.1:3001的入站连接。否则 WSL2 无法访问。

实操心得:不要试图让 Claude Code Desktop 在 WSL2 里运行。它的 GUI 依赖 Windows 的 DirectX,WSL2 无法提供。Paperclip 必须在 WSL2 里,Claude 必须在 Windows 原生环境,这是唯一稳定的组合。

4.2 macOS 环境:解决Virtual Machine Platform强制启用问题

macOS 用户遇到的最多报错是Claude's workspace requires the virtual machine platform on windows. enable——这是 Claude 安装包的错误提示文案,实际意思是“你的 macOS 版本太老,不支持 Rosetta 2 转译”。Paperclip 的应对策略是降级 Claude 版本。

Claude Code Desktop 的最新版(v1.2.0+)强制要求 macOS 13.0+。如果你用的是 macOS 12.6,Paperclip 必须指定旧版 Claude:

{ "claudeVersion": "1.1.4", "claudeDownloadUrl": "https://github.com/anthropic/claude-code-desktop/releases/download/v1.1.4/Claude.Code.Desktop-1.1.4.dmg" }

Paperclip 启动时,会自动下载并挂载这个 DMG,然后用hdiutil attach和cp -R复制到/Applications。这个过程需要sudo权限,Paperclip 会提示用户输入密码。

另一个 macOS 特有问题:launchdsocket 的权限。Claude 的 Unix socket 路径是/var/run/claude.sock,但默认只有root可读。Paperclip 必须用sudo chmod 666 /var/run/claude.sock临时开放权限。更好的做法是创建一个launchdplist 文件,让 Claude 以当前用户身份启动:

<!-- ~/Library/LaunchAgents/com.anthropic.claude.plist --> <?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>com.anthropic.claude</string> <key>ProgramArguments</key> <array> <string>/Applications/Claude Code Desktop.app/Contents/MacOS/Claude Code Desktop</string> <string>--no-sandbox</string> </array> <key>RunAtLoad</key> <true/> </dict> </plist>

然后launchctl load ~/Library/LaunchAgents/com.anthropic.claude.plist。这样 socket 就会以当前用户权限创建,Paperclip 无需 sudo。

4.3 Linux 服务器环境:CentOS 7.9 的兼容性攻坚

CentOS 7.9 的 glibc 版本是 2.17,而 Node.js 20.x 要求 glibc >= 2.18。直接curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo bash -会失败。Paperclip 的解决方案是静态链接 Node.js。

步骤:

  1. 下载预编译的 Node.js 二进制包(非 RPM):
    wget https://nodejs.org/dist/v20.11.1/node-v20.11.1-linux-x64.tar.xz tar -xf node-v20.11.1-linux-x64.tar.xz sudo mv node-v20.11.1-linux-x64 /opt/nodejs sudo ln -s /opt/nodejs/bin/node /usr/local/bin/node sudo ln -s /opt/nodejs/bin/npm /usr/local/bin/npm
  2. Paperclip 的package.json中,engines字段设为"node": ">=20.11.1",避免 npm install 时警告。
  3. OpenClaw 在 CentOS 上常以 systemd service 运行,Paperclip 必须监听其暴露的端口(如http://localhost:8080),而非 Docker 网络。配置文件里openclawHost设为"localhost:8080"。
  4. 关键防火墙设置:CentOS 7 默认用firewalld,必须开放 Paperclip 的端口(如 3002):
    sudo firewall-cmd --permanent --add-port=3002/tcp sudo firewall-cmd --reload

4.4 Docker 部署:如何让 Paperclip 在容器里安全访问宿主机服务

Paperclip 的 Docker 部署不是为了隔离,而是为了快速分发。它必须能访问宿主机的 Claude 和 OpenClaw,因此不能用默认 bridge 网络。

最佳实践是host 网络模式:

FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3002 # 关键:使用 host 网络,让容器内 127.0.0.1 指向宿主机 CMD ["npm", "start"]

启动命令:

docker run -d \ --network host \ --name paperclip \ -v $(pwd)/config:/app/config \ paperclip-image

这样,Paperclip 代码里的http://127.0.0.1:3001就能直接访问宿主机的 Claude。

注意:--network host在 Docker for Mac/Windows 上不生效,必须用host.docker.internal。Paperclip 的配置文件需支持环境变量替换:

{ "claudeHost": "${HOST_IP}:3001" }

启动时传入:docker run -e HOST_IP=host.docker.internal ...

5. 常见故障排查与独家避坑指南

5.1 典型故障速查表

现象可能原因排查命令解决方案
Error: claude native binary not installedClaude Code Desktop 未正确安装,或postinstall脚本未运行ls -l ~/.claude/重新下载 Claude 安装包,右键“显示简介”→“打开”绕过 Gatekeeper
OpenClaw connection refusedPaperclip 未启动,或 OpenClaw 的反向代理未指向 Paperclipcurl -v http://localhost:3002/api/status检查 Paperclip 日志,确认Listening on port 3002;检查 Nginx 配置中proxy_pass http://127.0.0.1:3002
SSE stream ends immediatelyClaude 的 WebSocket 连接成功,但未收到任何TokenChunkwscat -c wss://127.0.0.1:3001 --no-check手动发送{},看是否返回{"error":"invalid request"};若无响应,说明 Claude 未启用 API 模式,需在设置中开启
X-OpenClaw-Token invalidToken 过期,或 Paperclip 的 JWT secret 与 OpenClaw 不一致echo "token" | base64 -d | jq解码 Token payload,确认exp时间;核对paperclip-config.json中的openclawSecret是否与 OpenClaw 配置相同
Paperclip memory usage > 500MB连接池泄漏,或日志未轮转ps aux | grep paperclip | awk '{print $6}'设置MAX_LOG_ENTRIES: 1000;在连接池reconnecting状态时,强制清理pendingRequests

5.2 我踩过的五个深坑与解决方案

坑一:Claude 的--api-key参数被 Windows 命令行截断
现象:Paperclip 启动 Claude 时,传入的 API Key 只有前 32 位,后半部分丢失。
原因:Windows CMD 对命令行长度有限制(8191 字符),且对&、|、<等字符有特殊处理。
解决方案:改用 PowerShell 启动,并用-EncodedCommand:

$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes("claude-code --api-key 'sk-ant-api03-...'")) Start-Process powershell.exe -ArgumentList "-EncodedCommand $encoded"

坑二:React 前端EventSource在 Safari 中不工作
现象:Chrome 正常,Safari 控制台报EventSource failed。
原因:Safari 对 SSE 的Cache-Control: no-cache头更严格,且不支持withCredentials: true。
解决方案:Paperclip 的 SSE 路由必须添加Access-Control-Allow-Origin: *和Cache-Control: no-store:

app.get('/api/logs', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store', 'Access-Control-Allow-Origin': '*' }); // ... });

坑三:OpenClaw 的stream: true请求在 Paperclip 中被当成普通请求
现象:OpenClaw 发送stream=true,Paperclip 返回完整 JSON,而非 SSE 流。
原因:Express 默认将stream=true当作 query string,而 Paperclip 的路由匹配的是/api/openclaw,未区分 query。
解决方案:在路由中显式检查:

app.post('/api/openclaw', (req, res) => { if (req.query.stream === 'true' || req.body.stream === true) { // 启动 SSE 响应 res.writeHead(200, {'Content-Type': 'text/event-stream'}); // ... } else { // 普通 JSON 响应 } });

坑四:Paperclip 在 WSL2 中无法访问 Windows 的localhost
现象:curl http://localhost:3001返回Connection refused。
原因:WSL2 的localhost是自己的 loopback,不是 Windows 的。
解决方案:用 Windows 主机的真实 IP(非127.0.0.1),并通过netsh interface portproxy做端口转发:

# 在 Windows PowerShell 中执行 netsh interface portproxy add v4tov4 listenport=3001 listenaddress=127.0.0.1 connectport=3001 connectaddress=192.168.1.100

其中192.168.1.100是 Windows 的局域网 IP。

坑五:Paperclip 日志刷屏,磁盘被占满
现象:/var/log/paperclip.log一天增长 2GB。
原因:Paperclip 默认将所有

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询