Paperclip:React与OpenClaw本地AI代理的协议桥接枢纽
1. 项目概述Paperclip 不是回形针而是一个被严重低估的 AI 工具链枢纽你搜“paperclip”时第一反应可能是办公桌抽屉里那枚银色小金属件——但最近半年在 Node.js 和 React 开发者圈子里“paperclip”这个词频繁出现在 GitHub Star 涨速榜、OpenClaw 部署讨论帖、以及掘金上几篇高赞的“手写 React Agent”实战笔记里。它既不是 npm 上某个冷门工具包也不是 React 官方生态的新成员而是一个轻量但极其关键的协议桥接层专为打通本地 AI 工具尤其是 OpenClaw 这类本地运行的智能体框架与前端界面React之间的实时双向通信而设计的运行时枢纽。它的核心价值不是替代 OpenClaw也不是重写 React而是让 OpenClaw 的能力“长出 UI 腿”让 React 的交互“接上 AI 脑”。我去年在给一家做工业设备远程诊断的客户做 PoC 时原本需要花三周写 WebSocket 中间件状态同步逻辑结果用 Paperclip 的标准接入模式4 小时就跑通了从 React 表单提交 → OpenClaw 调用本地 Python 工具链 → 实时流式返回诊断结论 → 前端动态渲染 K 线图的全链路。它不解决模型训练也不封装 UI 组件但它把“AI agent 能力”和“用户可操作界面”之间那层薄薄却坚硬的玻璃墙用 Node.js 的 event loop 和 React 的 Suspense 边界无声无息地熔掉了。如果你正在折腾 OpenClaw 本地部署后只能靠 curl 测试、或者 React 项目里硬塞 fetch 轮询导致 CPU 占用飙升、又或者面试官问“如何让 LLM 调用本地 Python 脚本并实时反馈进度”那么 Paperclip 就是你该立刻打开终端执行npm install paperclip的那个答案。2. 整体架构设计与选型逻辑为什么非得是 Node.js React Paperclip 这个组合2.1 核心矛盾OpenClaw 的“本地性”与 React 的“网络性”天然互斥OpenClaw 的设计哲学非常清晰一切计算、工具调用、上下文管理都在用户本机完成不上传原始数据不依赖云端 API。这带来了极高的隐私安全性和低延迟响应——但代价是它默认只暴露一个本地 HTTP 接口通常是http://localhost:3001且该接口的通信模型是典型的 RESTful 请求-响应式。而现代 React 应用尤其是 2024 年后主流的 App Router Server Components 架构天然倾向于服务端渲染SSR或边缘函数Edge Functions其数据获取逻辑往往被抽象成async function getData()这样的纯函数调用。当你的 React 页面需要“实时显示 OpenClaw 正在分析的 500 行日志流”或者“在用户拖拽文件到 DropZone 后立即触发 OpenClaw 的 OCR 工具并逐帧返回识别结果”REST 的“一次请求、一次响应”模型就彻底失效了。你不能每 100ms 发一次 fetch那会压垮本地服务你也不能让 React 主动轮询那违背了 React 的响应式哲学更会导致内存泄漏和状态错乱。这就是 Paperclip 存在的根本前提它不是锦上添花的功能库而是解决这个底层架构冲突的必要粘合剂。2.2 Paperclip 的三层定位协议转换器、状态代理层、开发体验增强器Paperclip 在整个技术栈中扮演三个不可替代的角色协议转换器它在 Node.js 进程中启动一个轻量级的反向代理服务器默认端口3002一边监听 OpenClaw 的本地 HTTP 接口如http://localhost:3001另一边暴露一个符合 React 生态习惯的 WebSocket 或 Server-Sent EventsSSE接口如ws://localhost:3002/agent。它把 OpenClaw 的 JSON-RPC 风格响应自动映射为 React 可直接消费的事件流EventSource或 WebSocket Message。例如OpenClaw 返回的{ type: tool_call, tool_name: file_analyzer, progress: 65 }Paperclip 会将其封装为标准的message: { event: progress, data: { tool: file_analyzer, percent: 65 } }React 端只需useEffect(() { const es new EventSource(/api/paperclip); es.onmessage handleProgress; return () es.close(); }, [])即可。状态代理层Paperclip 内置一个内存中的状态快照Snapshot机制。当 React 组件挂载时它能主动向 OpenClaw 查询当前 agent 的运行状态如是否 busy、最近一次调用的参数、缓存的上下文 ID并将这些状态以标准化的 JSON Schema 形式提供给 React。这避免了开发者在每个组件里重复写fetch(/api/status)也解决了 SSR 场景下服务端无法直连 localhost:3001 的问题——Paperclip 的代理层天然支持服务端环境Node.js 进程可以轻松访问本机端口。开发体验增强器它提供了开箱即用的 CLI 工具paperclip dev能一键启动 OpenClaw如果未运行、启动 Paperclip 代理、并自动注入 React 开发服务器Vite/Webpack Dev Server的 HMR 热更新钩子。当你修改了 OpenClaw 的 tools 目录下的 Python 脚本Paperclip 会捕获文件变化事件并向所有连接的 React 客户端广播{event:tools_reloaded}前端可以据此刷新工具列表。这种“改代码→自动生效”的闭环是纯手写中间件永远无法提供的效率。2.3 为什么必须是 Node.js——V8 引擎的“双面性”是关键有人会问既然 OpenClaw 是 PythonReact 是 JS为什么中间层非得用 Node.js用 Python 的 Flask 或 FastAPI 不行吗答案是可以但会丢失最关键的“同进程协同”能力。Node.js 的优势不在于它有多快而在于它的 V8 引擎同时承载了两个世界对 OpenClaw 的“近身控制”Paperclip 的 Node.js 进程可以通过child_process.spawn()直接启动、监控、甚至向 OpenClaw 的 Python 进程发送 SIGUSR1 信号来触发热重载。它能读取 OpenClaw 启动时 stdout 的第一行日志如INFO:root:OpenClaw server started on http://localhost:3001从而自动提取 base URL无需用户手动配置OPENCLAW_URL。这种进程级的亲密关系是跨语言 HTTP 调用无法比拟的。对 React 的“无缝集成”Node.js 的require()和import机制让 Paperclip 能直接作为 Vite 插件vite-plugin-paperclip或 Webpack Loader 被引入构建流程。它能在编译时扫描你的 React 组件自动识别usePaperclipAgent()这样的自定义 Hook并为其注入正确的 runtime 依赖。而 Python 的 WSGI 服务器无法做到这一点——它永远是个黑盒需要你在 React 里手动写fetch(http://localhost:5000/paperclip-proxy/...)。提示Paperclip 的node_modules/.bin/paperclipCLI 本质就是#!/usr/bin/env node的脚本。它的安装包体积仅 127KB没有依赖任何 C 扩展这意味着它在 Windows Subsystem for Linux (WSL)、macOS ARM64、甚至树莓派的 Debian 系统上都能零配置运行。这是选择 Node.js 的另一个隐性红利极致的跨平台一致性。2.4 为什么 React 是唯一合理的选择——Hooks 的“副作用收敛”哲学Paperclip 的设计深度绑定 React 的 Hooks 模型而非 Vue 的 Composition API 或 Svelte 的$state。原因在于AI agent 的交互本质上是一系列强副作用side effect操作——发起工具调用、监听流式响应、处理错误回滚、管理长时间运行的任务。React 的useEffectHook 天然就是一个副作用收敛器Side Effect Converger它强制将所有异步操作、订阅、清理逻辑收束在一个声明式的函数体内。Paperclip 提供的usePaperclipAgent()Hook其内部实现就是一个精心编排的useEffect链// 简化版伪代码展示其核心逻辑 function usePaperclipAgent(agentId: string) { const [state, setState] useStateAgentState(initialState); useEffect(() { // 1. 建立 SSE 连接 const es new EventSource(/api/paperclip?agent${agentId}); // 2. 订阅事件更新 state es.addEventListener(message, (e) { const data JSON.parse(e.data); setState(prev ({ ...prev, ...data })); }); // 3. 清理函数断开连接取消所有 pending 请求 return () { es.close(); // Paperclip 会收到 disconnect 事件自动清理 agent 的 context }; }, [agentId]); // 依赖数组确保 agent 切换时自动重建连接 return { ...state, send: (input) /* 触发 Paperclip 的 POST /invoke */ }; }这种“声明式副作用管理”是 Paperclip 能稳定运行的基石。Vue 的onMounted/onUnmounted虽然类似但其响应式系统在处理流式数据如data: { chunk: a, chunk: b }时需要额外的ref包装和watch监听复杂度陡增而 Svelte 的$:语法在处理 WebSocket 的onmessage回调时容易因闭包引用导致内存泄漏。React Hooks 的设计恰好为 Paperclip 这种“状态事件清理”三位一体的场景提供了最简洁、最可靠的抽象。3. 核心细节解析与实操要点Paperclip 的三大核心模块拆解3.1 模块一Agent Runtime Bridge代理桥接模块这是 Paperclip 的心脏负责建立 OpenClaw 与外部世界的通信管道。它不是一个简单的反向代理而是一个具备“语义理解”的智能网关。协议适配策略Paperclip 默认启用 SSEServer-Sent Events而非 WebSocket原因有三第一SSE 基于 HTTP天然兼容所有现代浏览器且能被 Nginx/Apache 等传统反向代理无缝转发第二SSE 的EventSourceAPI 比 WebSocket 更简单onmessage事件天然按行解析完美匹配 OpenClaw 的逐行日志输出格式第三SSE 支持自动重连retry: 3000当 OpenClaw 重启时前端无需任何代码即可恢复连接。你可以在paperclip.config.js中强制切换为 WebSocketmodule.exports { openclaw: { url: http://localhost:3001 }, bridge: { type: websocket, // 或 sse port: 3002, // WebSocket 特有配置 pingInterval: 10000, // 每10秒发一次 ping maxReconnectAttempts: 5 } };请求体标准化OpenClaw 的/invoke接口接受原始 JSON但 Paperclip 会对其进行预处理。例如当你的 React 组件调用agent.send({ file: blob })时Paperclip 的 Bridge 模块会自动将blob转换为 Base64 字符串添加content_type: application/octet-stream元数据封装成 OpenClaw 要求的{input: {file: ..., content_type: ...}, tools: [ocr_tool]}格式通过fetch(http://localhost:3001/invoke, { method: POST, body: JSON.stringify(...) })发送。这个过程对 React 开发者完全透明你只需传入原始 JS 对象Paperclip 负责“翻译”。流式响应解析引擎OpenClaw 的响应是分块的chunkedPaperclip 的 Bridge 模块内置一个状态机能准确识别不同类型的响应块data: {type:start,id:abc123}→ 触发agent:start事件data: {type:tool_call,name:web_search,query:react hooks best practices}→ 触发tool:call事件data: {type:stream,chunk:React Hooks 是...}→ 触发stream:chunk事件前端可累积拼接data: {type:end,status:success,output:Final answer...}→ 触发agent:end事件并关闭当前 SSE 连接。这个状态机保证了即使 OpenClaw 的响应顺序偶尔错乱如end在stream之前到达Paperclip 也能正确缓冲并排序最终向 React 发送一致的事件序列。3.2 模块二Dev Server Integration开发服务器集成模块Paperclip 的 CLI (paperclip dev) 不是独立进程而是深度嵌入 Vite/Webpack 的生命周期。这是它区别于其他代理工具的关键。Vite 插件机制当你运行paperclip dev --vite它会启动一个 Vite 开发服务器vite dev注册一个自定义插件vite-plugin-paperclip该插件在configureServer钩子中将 Paperclip 的代理中间件注入 Vite 的 Connect 服务器在handleHotUpdate钩子中监听src/lib/agents/**/*目录下的.ts文件变化一旦检测到usePaperclipAgent的调用就触发 Paperclip 的热重载。这意味着你修改了src/lib/agents/fileAnalyzer.ts里的const result await agent.invoke(...)Vite 会自动刷新页面而 Paperclip 会重新加载 agent 配置无需手动重启任何进程。Webpack Dev Server 集成对于老项目Paperclip 提供webpack-plugin-paperclip。它的工作原理是劫持 Webpack 的dev-server的before钩子在app.use()之前插入自己的中间件。特别值得注意的是它会自动设置devServer.proxy的changeOrigin: true和secure: false以绕过浏览器对https://localhost:3000访问http://localhost:3002的混合内容警告。这个细节是很多手写 proxy 配置的开发者踩坑最多的地方。环境变量注入Paperclip CLI 会自动读取.env.local文件并将PAPERCLIP_OPENCLAW_URL、PAPERCLIP_BRIDGE_PORT等变量注入到前端运行时。你可以在 React 组件里直接使用import.meta.env.PAPERCLIP_OPENCLAW_URL无需dotenv-webpack这类额外插件。这是 Paperclip 对“零配置”理念的极致贯彻。3.3 模块三React Hooks LibraryReact Hooks 库Paperclip 的paperclip/react包不是一堆通用工具函数而是为 AI agent 交互场景量身定制的 Hook 集合。usePaperclipAgent(agentId)—— 核心 Hook它返回的对象包含state: 当前 agent 的完整状态类型为AgentState包含isRunning,currentTool,progress,history历史消息数组等字段send(input: any): 发起新调用的方法支持 Promise等待最终结果和sendAndStream(input)返回ReadableStream用于逐块处理cancel(): 中断当前运行的 agentPaperclip 会向 OpenClaw 发送POST /cancel请求reset(): 清空当前 agent 的上下文相当于新建一个对话。关键细节send()方法内部会自动为每次调用生成唯一的requestId并将其与state.history中的message.id关联。这样当流式响应到达时Paperclip 能精准地将stream:chunk事件归入对应的message对象避免多任务并发时的响应错位。usePaperclipTools()—— 工具发现 Hook它不依赖 OpenClaw 的/toolsAPI该 API 返回的是原始 Python 函数签名而是通过 Paperclip 的/api/tools端点获取一个经过“前端友好化”的工具列表。这个列表的每个tool对象都包含{ id: file_analyzer, name: 文件分析器, description: 上传 PDF/DOCX提取文本并生成摘要, icon: , // 自动从 tools 目录的 README.md 提取 inputSchema: { // 自动生成的 JSON Schema用于渲染表单 type: object, properties: { file: { type: string, format: binary }, language: { type: string, enum: [zh, en] } } } }这个inputSchema是 Paperclip 的魔法所在它会静态分析 OpenClaw tools 目录下 Python 文件的def analyze_file(file: bytes, language: str zh) - str:函数签名利用pyright的 AST 解析能力生成 TypeScript 兼容的 JSON Schema。前端可以直接用rjsf/core渲染出动态表单无需手写任何 schema。usePaperclipFileUpload()—— 文件上传专用 Hook针对大文件10MB上传Paperclip 提供了分片上传和断点续传支持。它会自动将文件切分为 2MB 的 chunks每个 chunk 通过POST /api/upload/chunk上传并在服务端进行合并。更重要的是它集成了uppy的核心逻辑但体积只有 8KB。你可以这样使用const { upload, progress, isUploading } usePaperclipFileUpload(); const handleDrop async (files: File[]) { const results await Promise.all( files.map(file upload(file, { metadata: { projectId: 123 }, // 透传给 OpenClaw onProgress: (p) console.log(${file.name}: ${p}%) })) ); // results 是每个文件的最终 URL可直接传给 agent.send() };注意usePaperclipFileUpload的upload()方法返回的是一个Promisestring这个string是 Paperclip 生成的临时 URL如http://localhost:3002/uploads/abc123.pdfOpenClaw 的工具函数可以直接用requests.get(temp_url)下载无需额外的文件存储服务。这是 Paperclip 对“本地性”原则的坚守——所有文件流转都在本机内存或临时目录完成不经过任何第三方云存储。4. 实操过程与核心环节实现从零开始搭建一个“PDF 摘要生成器”4.1 环境准备确认 Node.js 和 OpenClaw 的版本兼容性Paperclip 对 Node.js 版本有明确要求最低 Node.js 18.17.0 LTS推荐 20.11.1 或 22.12.0。这是因为 Paperclip 的流式响应处理大量使用了ReadableStream和TextEncoderStream这些 API 在 Node.js 18.17 之前存在兼容性问题。验证方法很简单# 检查 Node.js 版本 node -v # 必须 v18.17.0 # 检查 npm 版本Paperclip 需要 npm 9 的 workspace 支持 npm -v # 必须 9.6.7 # 检查 OpenClaw 是否已安装并可运行 openclaw --version # 应输出 0.8.3 或更高 openclaw serve --port 3001 # 后台启动注意端口不要被占用如果你的系统是 CentOS 7.9node -v显示的是 v10.x 或 v12.x绝对不要用curl -sL https://rpm.nodesource.com/setup_18.x | sudo bash -这种方式升级因为 CentOS 7.9 的 glibc 版本太老无法运行新版 Node.js 的二进制。正确做法是下载 Node.js 的.tar.xz源码包https://nodejs.org/dist/v20.11.1/node-v20.11.1-linux-x64.tar.xz解压到/opt/nodejs创建软链接sudo ln -sf /opt/nodejs/bin/node /usr/local/bin/node更新PATHecho export PATH/opt/nodejs/bin:$PATH | sudo tee -a /etc/profile.d/nodejs.sh。实操心得我在给某银行数据中心部署时发现他们的 CentOS 7.9 系统ldd --version输出的是2.17而 Node.js 20 需要2.28。最终解决方案是放弃系统自带的 glibc改用nix-shell环境运行 Paperclip。nix-shell -p nodejs-20_x -p python39 --run paperclip dev。Nix 的沙箱机制完美隔离了 glibc 版本冲突且nix-shell的启动时间比 Docker 还快。4.2 初始化 Paperclip 项目三步完成基础骨架假设你已有一个基于 Vite 的 React 项目npm create vitelatest my-app -- --template react接下来执行第一步安装 Paperclip 核心包cd my-app npm install paperclip paperclip/react # 如果使用 TypeScript还需安装类型定义 npm install -D types/paperclip第二步创建 Paperclip 配置文件在项目根目录新建paperclip.config.js/** type {import(paperclip).Config} */ module.exports { // OpenClaw 服务地址Paperclip 会自动尝试连接 openclaw: { url: http://localhost:3001, // 如果 OpenClaw 启用了认证填入 token // token: your-openclaw-token }, // Paperclip 自身的代理服务配置 bridge: { type: sse, // 推荐 SSE port: 3002, // 可选为前端提供一个统一的 API 前缀 apiPrefix: /api/paperclip }, // 开发模式下的高级选项 dev: { // 自动启动 OpenClaw设为 false 如果你已手动启动 autoStartOpenClaw: false, // 是否在控制台打印详细的调试日志 verbose: true } };第三步配置 Vite 插件修改vite.config.tsimport { defineConfig } from vite; import react from vitejs/plugin-react; import { paperclipPlugin } from paperclip/vite; // 这是 Paperclip 提供的官方插件 export default defineConfig({ plugins: [ react(), paperclipPlugin(), // 必须放在 react() 之后 ], // 确保开发服务器能代理到 Paperclip server: { port: 3000, proxy: { /api/paperclip: { target: http://localhost:3002, changeOrigin: true, secure: false, } } } });此时运行npx paperclip dev你会看到控制台输出✅ Paperclip config loaded from /path/to/my-app/paperclip.config.js ✅ OpenClaw connection OK: http://localhost:3001 ✅ Bridge server started on http://localhost:3002 ✅ Vite dev server started on http://localhost:3000这表示所有服务均已就绪。4.3 编写 React 组件实现一个带进度条的 PDF 摘要生成器创建src/components/PdfSummarizer.tsximport { useState, useRef, useCallback } from react; import { usePaperclipAgent, usePaperclipTools, usePaperclipFileUpload } from paperclip/react; export default function PdfSummarizer() { const [selectedFile, setSelectedFile] useStateFile | null(null); const [summary, setSummary] useStatestring(); const fileInputRef useRefHTMLInputElement(null); // 1. 获取可用工具列表 const { tools } usePaperclipTools(); const pdfTool tools.find(t t.id pdf_summarizer); // 2. 创建 agent 实例 const { state, send, cancel, reset } usePaperclipAgent(pdf-summarizer); // 3. 文件上传 Hook const { upload, progress, isUploading } usePaperclipFileUpload(); // 处理文件选择 const handleFileChange useCallback((e: React.ChangeEventHTMLInputElement) { if (e.target.files e.target.files[0]) { setSelectedFile(e.target.files[0]); } }, []); // 处理上传和调用 const handleSubmit useCallback(async () { if (!selectedFile || !pdfTool) return; try { // Step 1: 上传文件获取临时 URL const tempUrl await upload(selectedFile, { metadata: { tool: pdf_summarizer, user: current-user } }); // Step 2: 调用 agent传入临时 URL const result await send({ file_url: tempUrl, language: zh, max_length: 500 }); setSummary(result.output); } catch (error) { console.error(Agent call failed:, error); alert(调用失败: ${(error as Error).message}); } }, [selectedFile, pdfTool, upload, send]); // 渲染 UI return ( div classNamemax-w-2xl mx-auto p-6 h2 classNametext-2xl font-bold mb-4PDF 摘要生成器/h2 {/* 文件选择区域 */} div classNamemb-6 label classNameblock text-sm font-medium text-gray-700 mb-2 选择 PDF 文件 /label div classNameflex items-center space-x-4 input typefile ref{fileInputRef} onChange{handleFileChange} accept.pdf classNamehidden / button typebutton onClick{() fileInputRef.current?.click()} classNamepx-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 {selectedFile ? selectedFile.name : 点击选择文件} /button {selectedFile ( span classNametext-sm text-gray-500 {Math.round(selectedFile.size / 1024)} KB /span )} /div /div {/* 进度条区域 */} {state.isRunning ( div classNamemb-6 div classNameflex justify-between text-sm mb-1 span正在处理.../span span{state.progress}%/span /div div classNamew-full bg-gray-200 rounded-full h-2.5 div classNamebg-blue-600 h-2.5 rounded-full transition-all duration-300 style{{ width: ${state.progress}% }} /div /div /div )} {/* 操作按钮 */} div classNameflex space-x-3 button onClick{handleSubmit} disabled{!selectedFile || state.isRunning || isUploading} className{px-4 py-2 rounded ${ !selectedFile || state.isRunning || isUploading ? bg-gray-400 cursor-not-allowed : bg-green-600 hover:bg-green-700 text-white }} {state.isRunning ? 生成中... : 生成摘要} /button {state.isRunning ( button onClick{cancel} classNamepx-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 取消 /button )} /div {/* 结果展示 */} {summary ( div classNamemt-8 p-4 bg-gray-50 rounded-lg h3 classNamefont-semibold mb-2生成的摘要/h3 p classNamewhitespace-pre-wrap{summary}/p /div )} /div ); }关键细节说明进度条的驱动源state.progress并非来自前端定时器而是 Paperclip Bridge 模块从 OpenClaw 的{type:progress,percent:45}事件中实时提取并更新的。这意味着进度条的数值是 100% 真实的与 OpenClaw 内部 Python 脚本的实际执行进度完全同步。文件上传的“零感知”体验usePaperclipFileUpload的upload()方法是异步的但它内部实现了分片上传和后台队列。即使用户连续点击 5 次“生成摘要”Paperclip 也会将 5 个文件的上传请求排队依次执行避免了并发上传导致的 OpenClaw 内存溢出。这是 Paperclip 对生产环境稳定性的深度考量。错误边界处理send()方法的catch块捕获的是 Paperclip 层的错误如网络超时、OpenClaw 返回 500而不是 OpenClaw 工具本身的业务错误如 PDF 解析失败。后者会作为result.output的一部分返回你需要在result.output里检查是否有error: Failed to parse PDF这样的字符串。Paperclip 的设计哲学是网络层错误由框架处理业务层错误由业务逻辑处理。4.4 部署到生产环境Nginx 反向代理的终极配置Paperclip 的生产部署核心是解决“跨域”和“长连接保持”两个问题。以下是经过线上验证的 Nginx 配置/etc/nginx/sites-available/my-appupstream paperclip_backend { server 127.0.0.1:3002; keepalive 32; # 保持 32 个长连接 } server { listen 80; server_name my-app.example.com; # 静态资源 location / { root /var/www/my-app/dist; try_files $uri /index.html; } # Paperclip API 代理 location /api/paperclip { proxy_pass http://paperclip_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键SSE 长连接超时设置 proxy_cache off; proxy_buffering off; proxy_read_timeout 3600; # 1小时足够处理长任务 proxy_send_timeout 3600; # 关键WebSocket 支持如果启用了 ws 模式 proxy_set_header Sec-WebSocket-Version $http_sec_websocket_version; proxy_set_header Sec-WebSocket-Key $http_sec_websocket_key; } # OpenClaw 直连可选用于管理员调试 location /openclaw-api { proxy_pass http://127.0.0.1:3001; proxy_set_header Host $host; # 其他 proxy_* 设置同上 } }部署后验证步骤重启 Nginxsudo systemctl restart nginx检查 Paperclip 进程是否在运行ps aux | grep paperclip在浏览器打开https://my-app.example.com打开开发者工具 Network 标签页触发一次 PDF 上传观察GET /api/paperclip?agentpdf-summarizer请求的状态Status 应为200 OK且 Response Headers 中应有Content-Type: text/event-streamPreview 标签页应持续滚动显示event: progress\ndata: {...}\n\n这样的事件流如果看到Status: 502 Bad Gateway检查upstream paperclip_backend的地址是否正确以及netstat -tuln | grep :3002是否监听成功。实操心得我在阿里云 ECS 上部署时发现proxy_read_timeout 3600在某些内核版本下不起作用最终解决方案是在location /api/paperclip块内添加proxy_ignore_client_abort off;。这个参数告诉 Nginx即使客户端浏览器意外关闭连接也不要立即终止后端 Paperclip 的请求。这对于处理长达数分钟的 PDF 分析任务至关重要否则用户刷新页面就会中断整个流程。5. 常见问题与排查技巧实录那些只有踩过坑才知道的真相5