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 Events(SSE)接口(如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 是 Python,React 是 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] = useState<AgentState>(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 默认启用 SSE(Server-Sent Events)而非 WebSocket,原因有三:第一,SSE 基于 HTTP,天然兼容所有现代浏览器,且能被 Nginx/Apache 等传统反向代理无缝转发;第二,SSE 的
EventSourceAPI 比 WebSocket 更简单,onmessage事件天然按行解析,完美匹配 OpenClaw 的逐行日志输出格式;第三,SSE 支持自动重连(retry: 3000),当 OpenClaw 重启时,前端无需任何代码即可恢复连接。你可以在paperclip.config.js中强制切换为 WebSocket:module.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 的响应是分块的(chunked),Paperclip 的 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 配置,无需手动重启任何进程。- 启动一个 Vite 开发服务器(
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 Library(React 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(): 中断当前运行的 agent,Paperclip 会向 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()方法返回的是一个Promise<string>,这个string是 Paperclip 生成的临时 URL(如http://localhost:3002/uploads/abc123.pdf),OpenClaw 的工具函数可以直接用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.9,node -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; - 更新
PATH:echo '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 vite@latest 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.ts:
import { 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.tsx:
import { useState, useRef, useCallback } from 'react'; import { usePaperclipAgent, usePaperclipTools, usePaperclipFileUpload } from '@paperclip/react'; export default function PdfSummarizer() { const [selectedFile, setSelectedFile] = useState<File | null>(null); const [summary, setSummary] = useState<string>(''); const fileInputRef = useRef<HTMLInputElement>(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.ChangeEvent<HTMLInputElement>) => { 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 className="max-w-2xl mx-auto p-6"> <h2 className="text-2xl font-bold mb-4">PDF 摘要生成器</h2> {/* 文件选择区域 */} <div className="mb-6"> <label className="block text-sm font-medium text-gray-700 mb-2"> 选择 PDF 文件 </label> <div className="flex items-center space-x-4"> <input type="file" ref={fileInputRef} onChange={handleFileChange} accept=".pdf" className="hidden" /> <button type="button" onClick={() => fileInputRef.current?.click()} className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" > {selectedFile ? selectedFile.name : '点击选择文件'} </button> {selectedFile && ( <span className="text-sm text-gray-500"> {Math.round(selectedFile.size / 1024)} KB </span> )} </div> </div> {/* 进度条区域 */} {state.isRunning && ( <div className="mb-6"> <div className="flex justify-between text-sm mb-1"> <span>正在处理...</span> <span>{state.progress}%</span> </div> <div className="w-full bg-gray-200 rounded-full h-2.5"> <div className="bg-blue-600 h-2.5 rounded-full transition-all duration-300" style={{ width: `${state.progress}%` }} ></div> </div> </div> )} {/* 操作按钮 */} <div className="flex 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} className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" > 取消 </button> )} </div> {/* 结果展示 */} {summary && ( <div className="mt-8 p-4 bg-gray-50 rounded-lg"> <h3 className="font-semibold mb-2">生成的摘要:</h3> <p className="whitespace-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-app):
upstream 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_* 设置同上 } }部署后验证步骤:
- 重启 Nginx:
sudo systemctl restart nginx; - 检查 Paperclip 进程是否在运行:
ps aux | grep paperclip; - 在浏览器打开
https://my-app.example.com,打开开发者工具 Network 标签页; - 触发一次 PDF 上传,观察
GET /api/paperclip?agent=pdf-summarizer请求的状态:- Status 应为
200 OK,且 Response Headers 中应有Content-Type: text/event-stream; - Preview 标签页应持续滚动显示
event: progress\ndata: {...}\n\n这样的事件流; - 如果看到
Status: 502 Bad Gateway,检查upstream paperclip_backend的地址是否正确,以及netstat -tuln | grep :3002是否监听成功。
- Status 应为
实操心得:我在阿里云 ECS 上部署时,发现
proxy_read_timeout 3600在某些内核版本下不起作用,最终解决方案是在location /api/paperclip块内添加proxy_ignore_client_abort off;。这个参数告诉 Nginx,即使客户端(浏览器)意外关闭连接,也不要立即终止后端 Paperclip 的请求。这对于处理长达数分钟的 PDF 分析任务至关重要,否则用户刷新页面就会中断整个流程。