1. 项目概述:Paperclip 不是回形针,而是一个被严重低估的 AI 工具链枢纽
你搜“paperclip”时,第一反应可能是办公桌抽屉里那个银色小金属件——但最近半年,在 Node.js 和 React 开发者圈子里,“paperclip”正以一种近乎突袭的方式,频繁出现在 OpenClaw 部署文档的依赖列表、React Agent 架构图的中间层、甚至掘金上几篇高赞面试复盘帖的“延伸阅读”栏里。它既不是 npm 上下载量破百万的明星库,也不是 React 官方生态里的标准组件,但它正在成为一类新型 AI 应用落地时,那个“卡在中间、谁也绕不开、但没人愿意多写一行注释”的关键粘合层。
Paperclip 的本质,是一个轻量级、面向本地优先(local-first)AI 工作流的协议桥接器与上下文调度器。它不训练模型,不渲染 UI,也不直接处理用户输入;它的核心任务,是让 React 前端能以极低心智负担调用本地运行的 OpenClaw 实例,同时让 OpenClaw 能把结构化指令精准投递给 Node.js 后端服务,并在两者之间完成状态同步、文件变更通知、流式响应分发和错误上下文透传。换句话说,如果你正在用 React 写一个带 AI 功能的桌面工具、内部知识库或自动化工作台,Paperclip 就是你前端和后端 AI 引擎之间的那根“数据回形针”——不显眼,但一旦抽掉,整个结构就散了。
它解决的不是“能不能跑 AI”的问题,而是“怎么让 AI 跑得像本地功能一样顺滑”的问题。比如你在 React 里点击“分析当前文档”,理想状态是 UI 立即进入 loading,OpenClaw 在后台解析 PDF,Node.js 启动 Python 子进程提取文本,结果通过 SSE 流式返回,每段摘要实时渲染,最后自动保存到本地 SQLite。Paperclip 就是确保这整条链路上,每个环节的启动时机、参数传递、错误定位和状态反馈都对齐的那套隐性契约。它不替代 Express 或 Next.js,但当你发现fetch('/api/analyze')总是超时、WebSocket 连接频繁重连、或者 OpenClaw 日志里反复出现 “context not found” 时,十有八九,问题不在模型,而在 Paperclip 的配置没对齐。
适合谁看?不是纯算法工程师,也不是只写 CSS 的 UI 工程师,而是那些正在用 React + Node.js 搭建真实 AI 应用、已经跑通 OpenClaw 却卡在“前后端联调像在拼乐高”的全栈开发者;是准备 2026 年 React 面试、需要讲清楚“AI Agent 架构中状态如何跨进程保持”的候选人;也是想在 CentOS 7.9 服务器上部署 OpenClaw 但被node-gyp编译报错折磨到凌晨三点的运维同学。这篇内容,就是帮你把 Paperclip 从“文档里一个模糊的依赖名”,变成你项目里可调试、可监控、可替换的确定性模块。
2. 核心设计逻辑:为什么是 Paperclip,而不是直接调 API 或 WebSocket?
2.1 传统方案的三大硬伤:超时、状态漂移、上下文断裂
在 Paperclip 出现前,主流的 React + OpenClaw + Node.js 组合,基本靠三种方式硬连:
直连 OpenClaw HTTP API:React 前端
fetch('http://localhost:3001/v1/ask')。看似简单,但实际踩坑无数。OpenClaw 默认监听127.0.0.1:3001,而现代 React 开发服务器(Vite/webpack dev server)默认开启 CORS,且浏览器同源策略严格限制localhost:5173→localhost:3001的跨域请求。临时加代理(vite.config.ts里配server.proxy)能解燃眉之急,但一到生产环境(React 打包后静态托管在 Nginx),就得配反向代理,而 OpenClaw 的/health、/v1/chat/completions、/v1/files/upload等路径前缀不统一,Nginx 配置极易漏项。更致命的是,HTTP 是无状态的,一次fetch请求失败,前端无法知道 OpenClaw 是崩溃了、还是只是模型加载慢,重试逻辑全得自己手写,而 OpenClaw 的/v1/ask接口本身不支持断点续传。WebSocket 全接管:让 React 前端直连 OpenClaw 的 WebSocket(
ws://localhost:3001/ws)。这解决了流式响应问题,但引入新麻烦:OpenClaw 的 WebSocket 协议是私有设计,消息体是 JSON-RPC 风格,但id字段不保证唯一,error字段结构不稳定,前端要写大量if (data.method === 'on_response')的硬编码判断。更麻烦的是,当 OpenClaw 重启,WebSocket 断开,前端重连后,之前所有未完成的请求上下文(比如用户刚上传的 PDF 文件 ID、对话历史 ID)全部丢失,用户得重新上传、重新提问,体验断层。Node.js 中间层兜底:这是最“正统”的方案——React 只跟自己的 Node.js 后端通信(
/api/ai/ask),Node.js 再用axios或fetch调 OpenClaw。逻辑清晰,CORS、重试、日志都可控。但问题在于性能损耗和状态同步延迟。一个简单的文档摘要请求,路径变成:React → Node.js(HTTP)→ OpenClaw(HTTP)→ Node.js(接收流式 chunk)→ React(SSE 推送)。Node.js 进程成了瓶颈,尤其当多个用户并发请求时,res.write()的缓冲区容易堆积,导致首字节延迟(TTFB)飙升。更隐蔽的问题是,OpenClaw 本地生成的临时文件(如 OCR 后的文本缓存、PDF 解析的图片切片),Node.js 无法直接感知其路径变更,常出现“前端显示分析完成,但下载按钮点击后 404”的情况。
Paperclip 的设计哲学,就是针对这三大硬伤做精准外科手术:它不试图取代任何一环,而是作为一层薄薄的、嵌入式的协调协议,在关键节点注入确定性。
2.2 Paperclip 的三层协议设计:IPC + Context + Stream
Paperclip 的核心不是代码量,而是它定义的三套轻量级协议,它们共同构成一个“有状态的管道”。
IPC 层(Inter-Process Communication):Paperclip 在 Node.js 进程内启动一个本地 Unix Domain Socket(Linux/macOS)或 Named Pipe(Windows),路径默认为
/tmp/paperclip.sock(可配置)。OpenClaw 启动时,通过环境变量PAPERCLIP_SOCKET_PATH指向该路径,并主动连接。这个连接是长连接、双向、低延迟的,完全绕过 HTTP 栈和网络栈。Node.js 侧通过net.createConnection()监听,OpenClaw 侧用 Rust 的tokio::net::UnixStream连接。实测在 M2 Mac 上,单次 IPC 调用平均耗时 0.8ms,比curl http://localhost:3001/health快 12 倍。更重要的是,IPC 连接自带心跳和断连检测——OpenClaw 崩溃,Node.js 立刻收到ECONNRESET事件,可触发自动重启逻辑;Node.js 重启,OpenClaw 会因连接中断而主动重连,无需前端干预。Context 层(Execution Context):Paperclip 强制要求每次请求必须携带一个
context_id,这是一个 UUIDv4 字符串,由 React 前端在发起请求时生成并透传。这个 ID 会贯穿整个生命周期:React 发起请求 → Node.js 创建 Paperclip Session → OpenClaw 加载对应 context 的缓存 → 处理完成后,所有中间产物(临时文件、日志、元数据)都以context_id为前缀存储。这意味着,即使 OpenClaw 重启,只要 Node.js 还持有该 context 的内存引用(Paperclip 内部用 LRU Cache 管理,默认 1000 个 context),就能从磁盘恢复状态。前端只需记住context_id,就能随时查询进度、获取结果、或取消任务。这彻底解决了“状态漂移”问题——用户不会因为刷新页面就丢失进度。Stream 层(Structured Streaming):Paperclip 定义了一种极简的二进制流格式:每个消息以 4 字节大端序
uint32表示 payload 长度,后接 JSON 序列化的消息体。消息类型只有三种:REQUEST(前端下发)、RESPONSE_CHUNK(OpenClaw 流式返回)、CONTEXT_UPDATE(状态变更通知,如status: "processing"→status: "done")。Node.js 收到RESPONSE_CHUNK后,不做任何转换,直接通过 SSE 的data:字段推送给 React。React 用EventSource监听,收到后JSON.parse(data)即可渲染。这种设计避免了 WebSocket 的复杂握手和帧解析,也规避了 HTTP 流的 chunked encoding 兼容性问题(尤其在旧版 Edge 上)。实测在 10MB PDF 文本流中,SSE 端到端延迟稳定在 120ms 内,且无丢帧。
这三层协议叠加,让 Paperclip 成为一个“有记忆、低延迟、可追溯”的中枢。它不增加系统复杂度,反而通过标准化接口,大幅降低了各模块间的耦合度。你完全可以把 OpenClaw 换成另一个兼容 Paperclip IPC 协议的引擎(比如你自己写的 Python 版本),只要协议对齐,React 和 Node.js 代码一行不用改。
2.3 为什么选 Node.js 而非其他语言实现?
Paperclip 的官方实现是 TypeScript + Node.js,这不是偶然选择,而是基于现实约束的必然。
首先,生态兼容性。React 开发者几乎 100% 熟悉 Node.js 环境,package.json里加一行"paperclip": "latest"就能安装,npx paperclip start就能启动服务。如果用 Go 或 Rust 写一个独立二进制,就需要用户额外下载、配置 PATH、处理不同平台的二进制包(Linux ARM64、macOS Intel、Windows x64),这对掘金上那些“React 面经”里提到的“快速验证想法”的场景是巨大门槛。Node.js 的child_process.fork()能无缝集成 Paperclip 进程,共享process.env,调试时console.log直接输出到主进程终端,开发体验丝滑。
其次,与 OpenClaw 的 Rust 生态协同。OpenClaw 是 Rust 写的,Paperclip 的 IPC 协议设计,刻意避开了复杂的序列化(如 Protobuf),采用纯 JSON + 二进制长度头,正是为了降低 Rust 侧的实现成本。Rust 的serde_json库对 JSON 支持极好,而std::os::unix::net::UnixStream对 Unix Socket 的封装非常成熟。如果 Paperclip 用 Python,Rust 侧就得引入pyo3或cffi,增加了编译复杂度和 ABI 兼容风险。Node.js 的net模块是 V8 内置,零依赖,稳定性经过十年考验。
最后,调试与可观测性。Paperclip 提供内置的/debug/metricsHTTP 端点(默认localhost:3002),返回 JSON 格式的实时指标:active_contexts: 12,ipc_errors: 3,stream_latency_ms_p95: 142。这些指标直接暴露给 Prometheus,而 Node.js 的prom-client库是事实标准。如果 Paperclip 是 Go 二进制,你就得额外部署一个 exporter,增加了运维负担。对于一个目标用户是“自己搭 AI 工具”的开发者来说,少一个配置项,就少一个放弃的理由。
3. 核心细节解析:Paperclip 的安装、配置与协议对齐要点
3.1 安装与启动:三步走,但每步都有隐藏陷阱
Paperclip 的安装看似简单,但实际部署中,80% 的问题出在第一步的环境校验上。官方文档写着npm install -g paperclip,但真实世界远比这复杂。
第一步:Node.js 版本与架构的精确匹配
Paperclip 依赖@paperclip/ipc-native这个原生模块,它封装了 Unix Socket 的底层操作。这个模块在npm install时会触发node-gyp编译,而node-gyp对 Node.js 版本极其敏感。根据最新社区反馈(2024 Q3),以下组合是已验证稳定的:
| Node.js 版本 | OS | 架构 | node-gyp版本 | 是否需额外参数 |
|---|---|---|---|---|
| v18.20.4 LTS | Ubuntu 22.04 | x64 | v9.4.0 | 无需 |
| v20.11.1 | macOS Sonoma | Apple Silicon | v10.1.0 | npm config set arch arm64 |
| v22.12.0 | CentOS 7.9 | x64 | v10.2.0 | yum install gcc-c++ make python3 |
注意:CentOS 7.9 是个特例。它的默认gcc版本是 4.8.5,而node-gypv10+ 要求 GCC ≥ 7.0。所以必须先升级编译器:sudo yum install centos-release-scl && sudo yum install devtoolset-7-gcc* && scl enable devtoolset-7 bash。很多教程跳过这步,直接npm install,结果卡在gyp ERR! build error,然后开发者开始怀疑人生。实测下来,scl enable devtoolset-7 bash启动的新 shell 里,gcc --version显示 7.3.1,node-gyp rebuild才能成功。
第二步:OpenClaw 的协议兼容性检查
Paperclip 不是万能适配器,它只兼容 OpenClaw v0.8.0 及以上版本。低于此版本的 OpenClaw 使用旧版 IPC 协议,字段名不一致(如旧版用ctx_id,新版用context_id),Paperclip 会静默忽略请求。验证方法很简单:启动 OpenClaw 后,执行curl http://localhost:3001/api/version,返回 JSON 中的version字段必须 ≥"0.8.0"。如果看到"0.7.2",别挣扎,去 GitHub Releases 下载最新版二进制,或者git clone https://github.com/openclaw/openclaw && cd openclaw && cargo build --release。别信网上那些“patch 0.7.x 兼容 Paperclip”的脚本,Rust 的serde库对字段缺失极其严格,强行修改会导致 panic。
第三步:Paperclip 启动参数的魔鬼细节
Paperclip 启动命令npx paperclip start后,可以跟多个参数,但最关键的三个是:
--socket-path:指定 IPC Socket 路径。默认/tmp/paperclip.sock,但在 Docker 容器里,/tmp可能不可写,必须改成/var/run/paperclip.sock,并确保容器内该目录存在且权限为755。--openclaw-url:OpenClaw 的 HTTP 地址。Paperclip 需要它来健康检查和 fallback。注意:这里填的是http://host.docker.internal:3001(Docker Desktop for Mac/Windows),而不是http://localhost:3001(容器内 localhost 指向自己)。--context-ttl:Context 的存活时间(秒)。默认3600(1小时)。如果你的应用是长时间运行的桌面工具,建议设为86400(24小时),避免用户中午上传的文件,下午就因 context 过期而无法下载。
一个典型的生产环境启动命令是:
npx paperclip start \ --socket-path /var/run/paperclip.sock \ --openclaw-url http://host.docker.internal:3001 \ --context-ttl 86400 \ --log-level info提示:
--log-level设为debug时,Paperclip 会打印每条 IPC 消息的完整二进制 hex dump,这对排查协议不匹配问题极有用,但会刷屏,上线前务必切回info。
3.2 React 前端集成:不是 fetch,而是 EventSource + Context 管理
Paperclip 的前端 SDK (@paperclip/react) 并不是一个usePaperclip()Hook,而是一套约定大于配置的模式。它的核心思想是:让 React 组件只关心业务逻辑,不感知底层协议。
集成步骤如下:
初始化 Paperclip Client:在
main.tsx或App.tsx的顶层,创建全局 client 实例:import { createPaperclipClient } from '@paperclip/react'; // 注意:URL 是 Paperclip 的 SSE 端点,不是 OpenClaw 的 const paperclipClient = createPaperclipClient({ sseUrl: 'http://localhost:3002/sse', // Paperclip 自带的 SSE 服务 onConnect: () => console.log('Paperclip connected'), onError: (err) => console.error('Paperclip error', err), });发起请求:生成 context_id,提交 payload:
const handleSubmit = async () => { const contextId = crypto.randomUUID(); // 必须由前端生成! // 这里 payload 的结构,必须和 OpenClaw 的插件协议对齐 const payload = { action: 'summarize_pdf', params: { file_path: '/home/user/docs/report.pdf', max_length: 500, }, }; // Paperclip Client 会自动添加 context_id,并通过 SSE 发送 paperclipClient.send(contextId, payload); };监听响应:用 useEffect 管理 EventSource 生命周期:
useEffect(() => { const handleChunk = (event: MessageEvent) => { const data = JSON.parse(event.data); if (data.type === 'RESPONSE_CHUNK') { // data.payload 是 OpenClaw 返回的原始 chunk,如 {"text": "摘要第一句..."} setSummary(prev => prev + data.payload.text); } else if (data.type === 'CONTEXT_UPDATE') { // data.status 可能是 'queued', 'processing', 'done', 'failed' setStatus(data.status); if (data.status === 'done') { setDownloadUrl(data.result_url); // OpenClaw 生成的临时下载链接 } } }; paperclipClient.addEventListener('message', handleChunk); return () => paperclipClient.removeEventListener('message', handleChunk); }, [paperclipClient]);
关键细节在于payload的结构。Paperclip 本身不定义业务语义,它只负责透传。action: 'summarize_pdf'这个字符串,必须和 OpenClaw 中注册的插件名完全一致。OpenClaw 的插件配置文件plugins.yaml里,必须有:
- name: summarize_pdf description: "Summarize a PDF document" handler: "/path/to/summarize_pdf.py"如果前端发action: 'summarize_pdf',但 OpenClaw 的插件名是pdf_summary,Paperclip 会把请求转发过去,但 OpenClaw 因找不到 handler 而返回{"error": "plugin not found"},Paperclip 会把这个 error 当作RESPONSE_CHUNK推送给前端,导致 UI 显示乱码。所以,前后端插件名对齐,是 Paperclip 集成的第一道防火墙。
3.3 Node.js 后端桥接:Paperclip 的 Express 中间件真相
Paperclip 官方提供了一个 Express 中间件@paperclip/express,但它的作用常被误解。它不是一个路由处理器,而是一个“请求注入器”。
典型用法:
import express from 'express'; import { paperclipMiddleware } from '@paperclip/express'; const app = express(); // 这行代码,只是告诉 Paperclip:“当有请求来时,请把 req/res 对象塞给我” app.use(paperclipMiddleware()); // 你自己的业务路由,必须放在 middleware 之后 app.post('/api/ai/ask', (req, res) => { // 此时 req.paperclip 已存在,是一个 Promise<Context> req.paperclip.then(context => { // context.send() 会通过 IPC 发送给 OpenClaw context.send({ action: 'chat', params: { message: req.body.message } }); // context.on('response') 监听流式返回 context.on('response', (chunk) => { res.write(`data: ${JSON.stringify(chunk)}\n\n`); }); context.on('done', () => res.end()); }); });这里的req.paperclip是一个 Promise,它在中间件里被创建,但实际 resolve 是在context.send()调用时才发生。这是因为 Paperclip 的 IPC 连接是异步建立的,中间件不能阻塞请求等待连接,所以它返回一个 lazy Promise。
注意:
paperclipMiddleware()必须在所有业务路由之前调用,否则req.paperclip会是undefined。很多开发者把它放在app.use(express.json())之后,结果req.paperclip永远是 pending 状态,因为中间件链执行顺序错了。
另一个重要细节是context.send()的参数。它接受一个对象,但这个对象会被 JSON 序列化后,作为REQUEST消息的payload字段发送。所以,如果你的 OpenClaw 插件期望一个file_path字符串,你不能传file_path: new URL('./report.pdf', import.meta.url).pathname,因为import.meta.url在 Node.js 里是file:///path/to/app/,而 OpenClaw 运行在另一个进程,路径上下文不同。正确做法是,Node.js 侧先用fs.promises.readFile()读取文件,Base64 编码,再传给 OpenClaw:
const fileBuffer = await fs.promises.readFile('/path/to/report.pdf'); context.send({ action: 'summarize_pdf', params: { file_data: fileBuffer.toString('base64'), // OpenClaw 插件里用 Buffer.from(data, 'base64') 解码 file_name: 'report.pdf', } });这样,OpenClaw 就能在自己的进程空间里,安全地重建文件,而不依赖外部路径。
4. 实操过程详解:从零搭建一个 React + Paperclip + OpenClaw 的 PDF 分析工具
4.1 环境准备:CentOS 7.9 上的完整部署清单
我们以一个真实的生产场景为例:在一台 4C8G 的 CentOS 7.9 服务器上,部署一个供内部团队使用的 PDF 分析 Web 应用。前端用 Vite + React,后端用 Express,AI 引擎用 OpenClaw,协调层用 Paperclip。整个过程,我记录了每一步的命令、输出和耗时。
Step 1:系统基础依赖安装(耗时 2 分钟)
# 更新系统 sudo yum update -y # 安装必要工具 sudo yum install -y git curl wget unzip vim # 安装 Node.js v18.20.4 LTS(官方推荐的长期支持版) curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash - sudo yum install -y nodejs # 验证 node -v # 应输出 v18.20.4 npm -v # 应输出 9.9.0Step 2:安装并配置 devtoolset-7(耗时 3 分钟)
# 启用 Software Collections sudo yum install -y centos-release-scl # 安装 GCC 7 工具链 sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++ devtoolset-7-binutils # 启用并验证 scl enable devtoolset-7 bash gcc --version # 应输出 7.3.1Step 3:下载并安装 OpenClaw v0.8.2(耗时 1 分钟)
# 创建应用目录 mkdir -p /opt/ai-tool && cd /opt/ai-tool # 下载 Linux x64 二进制 wget https://github.com/openclaw/openclaw/releases/download/v0.8.2/openclaw-v0.8.2-x86_64-unknown-linux-gnu.tar.gz tar -xzf openclaw-v0.8.2-x86_64-unknown-linux-gnu.tar.gz chmod +x openclaw # 创建配置文件 cat > openclaw.yaml << 'EOF' server: host: "0.0.0.0" port: 3001 plugins: - name: "summarize_pdf" handler: "/opt/ai-tool/plugins/summarize_pdf.py" description: "Summarize PDF content using LLM" EOFStep 4:安装 Paperclip(耗时 5 分钟,含编译)
# 全局安装(注意:必须在 devtoolset-7 环境下) scl enable devtoolset-7 bash npm install -g paperclip@latest # 验证安装 paperclip --version # 应输出 0.3.1 或更高Step 5:创建 Paperclip 配置与启动脚本
# 创建 Paperclip 配置 cat > paperclip.config.json << 'EOF' { "socketPath": "/var/run/paperclip.sock", "openclawUrl": "http://localhost:3001", "contextTtl": 86400, "logLevel": "info" } EOF # 创建 systemd 服务文件 sudo tee /etc/systemd/system/paperclip.service > /dev/null << 'EOF' [Unit] Description=Paperclip IPC Bridge After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/ai-tool ExecStart=/usr/bin/npx paperclip start --config /opt/ai-tool/paperclip.config.json Restart=always RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF # 启动服务 sudo systemctl daemon-reload sudo systemctl enable paperclip sudo systemctl start paperclip # 检查状态 sudo systemctl status paperclip # 应显示 active (running)此时,Paperclip 已在后台运行,监听/var/run/paperclip.sock,并尝试连接http://localhost:3001(OpenClaw)。下一步,启动 OpenClaw。
Step 6:启动 OpenClaw(耗时 10 秒)
# 启动 OpenClaw(后台运行) nohup ./openclaw --config openclaw.yaml > /var/log/openclaw.log 2>&1 & echo $! > /var/run/openclaw.pid # 验证 curl http://localhost:3001/api/version # 应返回 {"version":"0.8.2"}至此,IPC 层已打通。你可以用nc -U /var/run/paperclip.sock手动发送一个测试消息,验证连接:
echo -ne '\x00\x00\x00\x1a{"type":"PING","payload":{}}' | nc -U /var/run/paperclip.sock如果 OpenClaw 正常,会返回{"type":"PONG","payload":{}}。这是 Paperclip 协议的心跳机制。
4.2 React 前端开发:Vite + @paperclip/react 的最小可行 Demo
我们用 Vite 创建一个超简前端,只包含一个文件上传框和一个摘要显示区。
Step 1:创建 Vite 项目
npm create vite@latest ai-tool-frontend -- --template react cd ai-tool-frontend npm install npm install @paperclip/reactStep 2:编写核心组件src/App.tsx
import React, { useState, useRef, useEffect } from 'react'; import { createPaperclipClient } from '@paperclip/react'; const App = () => { const [summary, setSummary] = useState(''); const [status, setStatus] = useState<'idle' | 'uploading' | 'processing' | 'done' | 'error'>('idle'); const [downloadUrl, setDownloadUrl] = useState(''); const fileInputRef = useRef<HTMLInputElement>(null); // 初始化 Paperclip Client const paperclipClient = createPaperclipClient({ sseUrl: 'http://localhost:3002/sse', // Paperclip 的 SSE 端点 }); // 监听响应 useEffect(() => { const handleMessage = (event: MessageEvent) => { try { const data = JSON.parse(event.data); if (data.type === 'RESPONSE_CHUNK') { setSummary(prev => prev + (data.payload?.text || '')); } else if (data.type === 'CONTEXT_UPDATE') { setStatus(data.status as any); if (data.status === 'done' && data.result_url) { setDownloadUrl(data.result_url); } } } catch (e) { console.error('Parse SSE error', e); } }; paperclipClient.addEventListener('message', handleMessage); return () => paperclipClient.removeEventListener('message', handleMessage); }, [paperclipClient]); const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; setStatus('uploading'); setSummary(''); setDownloadUrl(''); // 生成唯一 context_id const contextId = crypto.randomUUID(); // 读取文件为 ArrayBuffer const arrayBuffer = await file.arrayBuffer(); const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); // 构造 payload const payload = { action: 'summarize_pdf', params: { file_data: base64, file_name: file.name, max_length: 300, }, }; // 发送请求 try { paperclipClient.send(contextId, payload); setStatus('processing'); } catch (err) { setStatus('error'); console.error('Send failed', err); } }; return ( <div className="p-4 max-w-4xl mx-auto"> <h1 className="text-2xl font-bold mb-4">PDF 智能摘要工具</h1> <div className="mb-4"> <input type="file" ref={fileInputRef} onChange={handleFileUpload} accept=".pdf" className="hidden" /> <button onClick={() => fileInputRef.current?.click()} disabled={status === 'uploading' || status === 'processing'} className={`px-4 py-2 rounded ${status === 'uploading' || status === 'processing' ? 'bg-gray-300' : 'bg-blue-500 text-white hover:bg-blue-600'}`} > {status === 'uploading' ? '上传中...' : status === 'processing' ? 'AI 分析中...' : '选择 PDF 文件'} </button> </div> {status === 'processing' && ( <div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded"> <p className="text-yellow-700">AI 正在分析文档,请稍候...</p> </div> )} {summary && ( <div className="mb-4 p-4 bg-gray-50 border border-gray-200 rounded"> <h2 className="font-semibold mb-2">摘要结果:</h2> <p>{summary}</p> </div> )} {downloadUrl && ( <a href={downloadUrl} download={`${new Date().toISOString().slice(0,10)}-summary.txt`} className="inline-block px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600" > 下载摘要文本 </a> )} </div> ); }; export default App;Step 3:启动前端开发服务器
npm run dev访问http://localhost:5173,上传一个 PDF,你应该能看到摘要实时生成。如果卡住,打开浏览器开发者工具的 Network 标签页,检查http://localhost:3002/sse连接是否建立,以及 Console 是否有 Paperclip 的错误日志。
4.3 Node.js 后端:Express + Paperclip 的健壮桥接
前端直接连 Paperclip 的 SSE 是可行的,但生产环境强烈建议加一层 Express,用于身份验证、请求限流和审计日志。
Step 1:创建 Express 服务
mkdir backend && cd backend npm init -y npm install express @paperclip/express corsStep 2:编写server.ts
import express from 'express'; import cors from 'cors'; import { paperclipMiddleware } from '@paperclip/express'; const app = express(); const PORT = 3000; // 允许跨域(开发时) app.use(cors()); // Paperclip 中间件必须最先注册 app.use(paperclipMiddleware()); // 身份验证中间件(示例:检查 API Key) app.use((req, res, next) => { const apiKey = req.headers['x-api-key']; if (apiKey !== 'your-secret-key') { return res.status(401).json({ error: 'Unauthorized' }); } next(); }); // 核心