首页 / 资讯中心 / 文章详情

Paperclip架构:轻量级AI前端代理的工程实践

Paperclip架构:轻量级AI前端代理的工程实践 ★ FEATURED ARTICLE
1. 项目概述Paperclip 不是回形针而是一个被严重误读的 AI 工程实践入口“Paperclip”这个词在中文技术社区里最近半年正经历一场奇特的语义漂移。它不再指代办公桌上那个弯折金属丝制成的物理小物件而是悄然演变成一个高频出现、却极少被真正说清的技术代号——尤其当它和 Node.js、React、OpenClaw、Claude 这些词并列出现在搜索热榜时背后指向的是一类正在快速落地的新型 AI 应用架构模式轻量级、本地优先、可嵌入、强交互的 AI 前端代理系统。我从去年底开始在多个客户现场部署类似方案从金融合规文档初筛到制造业设备日志摘要生成再到内部知识库的实时问答增强所有项目底层都共享一个高度相似的技术骨架而团队内部就管它叫 “Paperclip 架构”——取其“小而关键、连接两端、不喧宾夺主”的隐喻。这个命名不是炫技而是精准描述了它的工程定位它不替代后端大模型服务如 Claude API 或本地部署的 Llama3也不试图重构前端框架React 仍是主力而是像一枚回形针那样把已有的、成熟的、各自运转良好的模块——Node.js 后端服务、React 前端界面、OpenClaw 这类本地化 AI 工具链、甚至 Windows/macOS 的系统能力——稳稳夹在一起形成一个低延迟、高可控、可审计、易调试的端到端 AI 工作流。它解决的不是“能不能用 AI”的问题而是“怎么让 AI 在真实业务场景里不掉链子、不卡顿、不泄露、不翻车”的问题。适合谁不是纯算法研究员也不是只写 CSS 的 UI 工程师而是那些每天要和产品经理对齐需求、和运维确认部署环境、和法务讨论数据边界的全栈型前端/应用工程师是那些手头有现成 React 管理后台、但苦于无法安全接入 Claude 实时分析 PDF 报告的中台开发也是那些想在 Ubuntu 服务器上跑起 OpenClaw又不想把整个团队拖进 Python 环境泥潭的 DevOps 实践者。它不教你怎么调参但会告诉你为什么node --max-old-space-size8192是启动 OpenClaw 服务前必须加的参数以及为什么 React 的useEffect里直接fetch(/api/claude/stream)会导致 SSE 连接在 Chrome 下莫名中断三次才重连成功。2. Paperclip 架构的整体设计与思路拆解为什么放弃“大一统”选择“小而韧”2.1 核心设计哲学拒绝“AI 中台幻觉”拥抱“边缘智能缝合”过去两年我参与过三个号称“企业级 AI 中台”的项目最终两个在上线前三个月被砍掉原因惊人一致架构太重、响应太慢、调试太难、权限太乱。它们都试图用一个统一的后端服务把模型路由、提示词管理、缓存、审计、限流全部包圆。结果呢前端工程师改个按钮文案要等后端发版测试同学想复现一个“Claude 返回空数组”的 bug得先申请 K8s 日志权限再翻三小时 Pod 日志法务要求所有 PDF 解析必须在内网完成运维发现中台服务依赖的 OCR 模块居然调用了公网 Google Vision API。Paperclip 架构正是从这些坑里爬出来后重新设计的——它彻底放弃“统一调度中心”的幻想转而信奉“能力下沉、职责清晰、边界明确”。具体来说Paperclip 将整个 AI 工作流拆解为四个物理隔离、逻辑耦合的层用户层React只负责渲染、事件捕获、状态同步。它不持有任何模型能力不处理原始文本不解析 JSON Schema。它唯一知道的“AI 能力”就是一个标准的 RESTful 接口/api/paperclip/summarize和一个 SSE 端点/api/paperclip/stream。所有 UI 交互比如点击“生成摘要”按钮、拖拽上传 PDF、实时查看流式输出都通过这两个接口完成。胶水层Node.js这是 Paperclip 的心脏但绝不是大脑。它不做模型推理不写提示词模板不管理向量数据库。它的核心职责只有三件① 接收 React 请求做最轻量的校验文件类型、大小、用户权限 token② 将请求“翻译”成下游工具能理解的格式比如把 multipart/form-data 转成 OpenClaw CLI 可读的临时文件路径③ 启动子进程调用 OpenClaw并将 stdout/stderr 流式转发给前端。它像一个严谨的海关关员只检查护照token、登记行李文件元数据、放行车辆spawn child process绝不干涉车内乘客模型要去哪里、做什么。执行层OpenClaw Claude这才是真正的“AI 执行引擎”。OpenClaw 作为本地化 AI 工具链负责 PDF 解析、代码解释、多模态输入预处理Claude无论是通过 API 还是本地 Ollama 部署则专注语言理解与生成。它们完全独立于 Node.js 进程运行有自己的内存、自己的日志、自己的配置文件。Paperclip 胶水层只通过标准输入/输出与之通信这意味着你可以今天用openclaw pdf --extract-text input.pdf | claude --modelhaiku明天换成openclaw obsidian --vault/path/to/vault | claude --modelsonnet只要输入输出格式不变React 层和 Node.js 层完全无感。系统层OS Runtime这是最容易被忽视、却最致命的一环。Paperclip 架构强制要求明确声明运行时约束Ubuntu 22.04因 OpenClaw 依赖较新 glibc、Node.js 18.20.4 LTS经实测18.x 对 child_process.spawn 的流式处理最稳定20.x 在高并发下偶发 fd 泄漏22.x 的 V8 内存模型与 OpenClaw 的 Rust runtime 存在微妙冲突、Windows 用户必须启用 WSL2因原生 Windows 版 OpenClaw 功能阉割严重。这不是矫情而是血泪教训——去年有个客户坚持用 CentOS 7.9 部署结果 OpenClaw 解析 PDF 时触发 glibc 2.17 的一个已知 segfault错误日志里只有一行Segmentation fault (core dumped)我们花了三天才定位到是系统库版本问题。这种分层设计带来的直接好处是故障域隔离。当用户反馈“点击摘要按钮没反应”你立刻能判断是 React 的 fetch 被 CORS 拦截查浏览器控制台、Node.js 服务未启动查ps aux | grep node、OpenClaw 进程崩溃查/var/log/paperclip/openclaw.log还是 Claude API 限流查curl -v https://api.anthropic.com/v1/messages。每个环节都有明确的排查路径没有“黑盒”。2.2 为什么选 Node.js 而非 Python/Go 作为胶水层这个问题我被问过至少二十次。答案很实在不是 Node.js 最好而是它在 Paperclip 场景下“刚刚好”。与 React 的天然亲和性同属 JavaScript 生态共享 npm 包管理、ESM 模块语法、甚至部分工具链如 Vite 插件可同时作用于前端和 Node.js 后端。当你需要在 Node.js 层做简单的前端资源代理比如把http://localhost:3000/api/paperclip/映射到http://localhost:3001/用 Express 的app.use(/api/paperclip, createProxyMiddleware(...))一行代码搞定而用 Python Flask你得额外装flask-cors、requests、还要处理 asyncio loop 兼容性。流式处理的成熟度Paperclip 的核心体验是“流式响应”——用户上传一个 50 页 PDF希望看到文字一行行实时浮现而不是等两分钟弹出一个完整摘要。Node.js 的ReadableStream/WritableStreamAPI 经过十年迭代稳定性和文档完备性远超其他语言。我对比过 Go 的io.Pipe和 Python 的asyncio.StreamReader前者在高并发下 buffer 管理复杂后者在 Windows 上存在已知的ProactorEventLoop兼容问题。而 Node.js 的child_process.spawn()返回的stdout本身就是ReadableStreamres.write()直接写入 HTTP 响应流中间零拷贝延迟最低。运维友好性客户 IT 部门普遍熟悉 Node.js 部署PM2、Nginx 反向代理配置、内存监控脚本但对 Python 的venv环境隔离、Go 的静态编译产物管理、Rust 的cargo install权限问题往往一头雾水。Paperclip 的交付物是一个package.jsonserver.jsDockerfile运维同学照着公司现有 Node.js 部署 SOP 执行即可极大降低落地阻力。当然Node.js 也有短板CPU 密集型任务如大文件哈希计算会阻塞事件循环。Paperclip 的应对策略是——坚决不做。所有 CPU 密集操作PDF 解析、OCR、向量编码全部交给 OpenClaw 这类专用工具Node.js 只做 I/O 编排。这恰恰印证了它的设计初衷胶水不是肌肉。2.3 为什么 OpenClaw 是 Paperclip 的事实标准执行层OpenClaw 在 Paperclip 架构中的地位类似于 Linux 里的systemd——它不是唯一的选项但已成为事实上的默认。原因在于它精准击中了本地 AI 工具链的几个痛点统一输入抽象无论你传进来的是 PDF、Markdown、Obsidian 笔记、甚至一段终端命令输出OpenClaw 都能将其标准化为结构化文本流。它内置的pdf子命令使用mupdf引擎比pdfjs-dist更快更准obsidian子命令能直接读取.md文件并解析 frontmattercode子命令支持 30 编程语言的语法高亮与上下文提取。这意味着 Paperclip 的 Node.js 层无需为每种文件类型写解析逻辑只需调用openclaw type file即可。可预测的退出码与日志这是工程落地的生命线。OpenClaw 的每个子命令都遵循 POSIX 规范成功返回0输入错误返回1如文件不存在解析失败返回2如 PDF 损坏内存不足返回12。它的日志输出严格区分stdout结构化结果和stderr人类可读错误信息且stderr默认带时间戳和模块名如[pdf] failed to parse page 3: invalid xref table。相比之下很多 Python 脚本把错误和结果混在print()里Node.js 层根本无法可靠解析。极简的依赖模型OpenClaw 是一个静态链接的二进制文件Linux/macOS或 WSL2 兼容的 Windows EXE。安装就是curl -L https://github.com/openclaw/openclaw/releases/download/v0.8.2/openclaw-linux-amd64 -o /usr/local/bin/openclaw chmod x /usr/local/bin/openclaw。它不依赖 Python 环境、不依赖 Node.js、不依赖特定版本的 GCC。Paperclip 的 Dockerfile 里你只需要COPY openclaw /usr/local/bin/就能在 Alpine Linux 容器里跑起来。而如果换成一个 Python 脚本你得维护requirements.txt、处理pip install网络超时、规避setuptools版本冲突——这些琐事会吃掉 30% 的交付时间。我试过用自研的 Rust 工具替代 OpenClaw功能上完全 OK但客户运维反馈“你们这个二进制文件太大了42MB vs OpenClaw 的 18MB而且ldd ./mytool显示一堆libssl.so.3依赖我们 CentOS 7 的 OpenSSL 太老没法用。” —— 这就是 OpenClaw 赢在细节的地方它用musl编译天生兼容老旧系统。3. Paperclip 架构的核心细节解析与实操要点从概念到可运行的每一步3.1 Node.js 胶水层不只是express()而是流式管道的精密编排Paperclip 的 Node.js 层看似简单实则暗藏玄机。一个典型的server.js并非教科书式的 CRUD 示例而是一个围绕child_process.spawn构建的流式管道。下面这段代码是我在线上环境稳定运行 8 个月的精简版核心逻辑const express require(express); const { spawn } require(child_process); const path require(path); const fs require(fs).promises; const app express(); app.use(express.json({ limit: 10mb })); app.use(express.urlencoded({ extended: true, limit: 10mb })); // 关键配置必须显式设置 max-old-space-size否则 OpenClaw 大文件解析时 Node.js 进程会 OOM const NODE_OPTIONS --max-old-space-size8192; process.env.NODE_OPTIONS NODE_OPTIONS; // POST /api/paperclip/summarize 处理上传的 PDF 并流式返回 Claude 摘要 app.post(/api/paperclip/summarize, async (req, res) { try { // 1. 校验确保是 multipart/form-data 且包含 file 字段 if (!req.files || !req.files.file) { return res.status(400).json({ error: Missing file }); } const file req.files.file; // 2. 安全检查仅允许 PDF且大小不超过 50MBOpenClaw 处理大 PDF 内存消耗巨大 if (file.mimetype ! application/pdf) { return res.status(400).json({ error: Only PDF files are allowed }); } if (file.size 50 * 1024 * 1024) { return res.status(400).json({ error: File too large (50MB) }); } // 3. 创建唯一临时文件路径避免并发冲突 const tempDir /tmp/paperclip; await fs.mkdir(tempDir, { recursive: true }); const tempFilePath path.join(tempDir, ${Date.now()}-${Math.random().toString(36).substr(2, 9)}.pdf); // 4. 将上传的 Buffer 写入临时文件注意不能用 file.mv()它不支持流式 await fs.writeFile(tempFilePath, file.data); // 5. 设置响应头启用流式传输 res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no // Nginx 关键配置禁用缓冲 }); // 6. 启动 OpenClaw 子进程提取 PDF 文本 const openclaw spawn(openclaw, [pdf, --extract-text, tempFilePath], { stdio: [pipe, pipe, pipe], // stdin, stdout, stderr env: { ...process.env, OPENCLAW_LOG_LEVEL: warn } // 降低 OpenClaw 日志噪音 }); // 7. 启动 Claude 子进程接收 OpenClaw 输出并生成摘要 // 注意这里用的是本地 Ollama 的 Claude 模型而非 Anthropic API const claude spawn(ollama, [run, claude-3-haiku], { stdio: [pipe, pipe, pipe], env: { ...process.env, OLLAMA_NO_CUDA: 1 } // 强制 CPU 模式避免 GPU 冲突 }); // 8. 构建流式管道openclaw.stdout - claude.stdin - res openclaw.stdout.pipe(claude.stdin); claude.stdout.on(data, (chunk) { // 将 Claude 输出按行分割包装成 SSE 格式 const lines chunk.toString().split(\n).filter(line line.trim()); lines.forEach(line { res.write(data: ${JSON.stringify({ type: chunk, content: line })}\n\n); }); }); // 9. 错误处理任一进程失败立即终止并清理 const cleanup () { openclaw.kill(SIGTERM); claude.kill(SIGTERM); fs.unlink(tempFilePath).catch(() {}); res.end(); }; openclaw.on(error, (err) { console.error(OpenClaw spawn error:, err); res.write(data: ${JSON.stringify({ type: error, message: OpenClaw failed to start })}\n\n); cleanup(); }); claude.on(error, (err) { console.error(Claude spawn error:, err); res.write(data: ${JSON.stringify({ type: error, message: Claude failed to start })}\n\n); cleanup(); }); openclaw.on(close, (code) { if (code ! 0) { console.error(OpenClaw exited with code ${code}); res.write(data: ${JSON.stringify({ type: error, message: PDF parsing failed })}\n\n); cleanup(); } }); claude.on(close, (code) { if (code ! 0) { console.error(Claude exited with code ${code}); res.write(data: ${JSON.stringify({ type: error, message: Summary generation failed })}\n\n); } cleanup(); }); } catch (err) { console.error(Summarize handler error:, err); res.status(500).json({ error: Internal server error }); } }); app.listen(3001, 0.0.0.0, () { console.log(Paperclip server running on http://localhost:3001); });这段代码里藏着几个必须掌握的实操要点提示res.write()发送的必须是严格符合 SSEServer-Sent Events规范的字符串即以data:开头后跟换行符\n\n。少一个\n前端 EventSource 就会静默失败且没有任何错误提示。我踩过这个坑在 Chrome DevTools 的 Network 标签页里看到响应体是data: {type:chunk,content:hello}\n只有一个\n导致前端一直等待第二个\n最终超时。解决方案是永远用res.write(data: ...\n\n)不要拼接。注意openclaw pdf --extract-text的输出是纯文本流但 Claude 的ollama run命令期望的是一个完整的 prompt。所以实际生产中你需要在openclaw.stdout.pipe(claude.stdin)之前插入一个 Transform Stream 来包装 prompt。例如const { Transform } require(stream); const promptTransform new Transform({ transform(chunk, encoding, callback) { const text chunk.toString(); const prompt 请用中文用不超过 200 字总结以下技术文档的核心内容\n\n${text}; callback(null, Buffer.from(prompt)); } }); openclaw.stdout.pipe(promptTransform).pipe(claude.stdin);这个 Transform 是 Paperclip 的“提示词注入点”所有业务逻辑如角色设定、输出格式约束、敏感词过滤都应在此处实现而非硬编码在前端或 Claude 模型里。提示child_process.spawn的stdio选项至关重要。[pipe, pipe, pipe]表示子进程的 stdin/stdout/stderr 都被 Node.js 管道接管。如果你写成[ignore, pipe, pipe]那么子进程的 stdin 就是/dev/nullClaude 就收不到 OpenClaw 的输出整个管道就断了。线上曾发生过一次事故运维同学为了“减少日志”把stdio改成了[ignore, pipe, ignore]结果所有摘要请求都返回空排查了两小时才发现是stderr被忽略导致错误信息丢失。3.2 React 前端如何让流式响应在浏览器里“活”起来React 层的挑战不在于功能实现而在于用户体验的打磨。一个“流式摘要”功能如果只是把 Claude 的每一行输出console.log出来那和命令行没什么区别。Paperclip 的 React 实现核心是三个 Hook 的协同useSSE封装EventSource处理连接、重连、错误。useStreamingText管理流式文本的状态支持追加、清除、暂停。useFileUpload处理文件选择、预览、进度条。下面是一个精简但生产可用的useSSEHookimport { useState, useEffect, useRef } from react; interface SSEMessage { type: chunk | error | done; content: string; } export function useSSE(url: string) { const [messages, setMessages] useStateSSEMessage[]([]); const [isConnected, setIsConnected] useState(false); const [error, setError] useStatestring | null(null); const eventSourceRef useRefEventSource | null(null); useEffect(() { // 创建 EventSource const es new EventSource(url, { withCredentials: true // 如果需要携带 cookie }); eventSourceRef.current es; es.onopen () { console.log(SSE connected); setIsConnected(true); setError(null); }; es.onmessage (event) { try { const data JSON.parse(event.data) as SSEMessage; setMessages(prev [...prev, data]); } catch (e) { console.error(Failed to parse SSE data:, event.data); } }; es.onerror (err) { console.error(SSE error:, err); setIsConnected(false); setError(Connection lost. Retrying...); // 关键手动重连逻辑因为 EventSource 的自动重连在某些网络环境下不可靠 setTimeout(() { if (es.readyState EventSource.CLOSED) { console.log(Manually reconnecting...); es.close(); // 重新创建新的 EventSource const newEs new EventSource(url, { withCredentials: true }); eventSourceRef.current newEs; } }, 3000); }; // 清理函数 return () { if (es es.readyState ! EventSource.CLOSED) { es.close(); } }; }, [url]); const clearMessages () { setMessages([]); }; return { messages, isConnected, error, clearMessages, // 提供一个手动重连方法供 UI 按钮调用 reconnect: () { if (eventSourceRef.current) { eventSourceRef.current.close(); } // 触发 useEffect 重新执行 } }; }这个 Hook 解决了三个关键问题重连可靠性原生EventSource的retry机制在遇到 DNS 故障或代理中断时经常失效。Paperclip 的做法是监听onerror并在 3 秒后主动关闭旧连接、创建新连接确保用户感知不到中断。错误隔离onmessage里用try/catch包裹JSON.parse防止后端偶尔发送的非 JSON 数据如调试日志导致整个 EventSource 崩溃。这是线上环境的真实需求——OpenClaw 在 debug 模式下可能往stderr写一些诊断信息如果没拦截就会污染onmessage流。状态管理messages是一个数组每次setMessages([...prev, data])都会触发 React 重渲染。但要注意如果 Claude 返回速度很快比如每秒 10 行频繁的setState会导致 UI 卡顿。Paperclip 的优化是添加一个防抖队列每 100ms 批量更新一次const [pendingMessages, setPendingMessages] useStateSSEMessage[]([]); // 在 onmessage 里 setPendingMessages(prev [...prev, data]); // 使用 useEffect 每 100ms 刷新一次 UI useEffect(() { const timer setTimeout(() { if (pendingMessages.length 0) { setMessages(prev [...prev, ...pendingMessages]); setPendingMessages([]); } }, 100); return () clearTimeout(timer); }, [pendingMessages]);提示EventSource在 Safari 上有一个著名 Bug当 URL 包含查询参数如/api/paperclip/stream?fileId123时Safari 会缓存第一个响应后续请求都返回旧数据。Paperclip 的解决方案是在 URL 后添加一个随机时间戳参数t${Date.now()}或者更优雅地使用POSTfetchReadableStream但需要后端配合且兼容性不如 SSE。3.3 OpenClaw 与 Claude 的本地化部署绕开云服务的“最后一公里”Paperclip 的灵魂在于“本地化”这意味着 OpenClaw 和 Claude 必须能在客户内网服务器上稳定运行。以下是我在 Ubuntu 22.04 和 Windows 11WSL2上验证过的最小可行部署方案Ubuntu 22.04 部署 OpenClaw OllamaClaude安装 OpenClaw# 下载最新稳定版v0.8.2 curl -L https://github.com/openclaw/openclaw/releases/download/v0.8.2/openclaw-linux-amd64 -o /usr/local/bin/openclaw chmod x /usr/local/bin/openclaw # 验证安装 openclaw --version # 输出openclaw v0.8.2 (built with rustc 1.78.0)安装 Ollama运行 Claude 模型# 官方一键安装 curl -fsSL https://ollama.com/install.sh | sh # 拉取 Claude 模型注意官方模型库中是 claude-3-haiku不是 anthropic/claude-3-haiku ollama pull claude-3-haiku # 验证模型可用 echo Hello | ollama run claude-3-haiku # 应该输出类似 Hello! How can I help you today?关键系统配置内存限制Ollama 默认使用全部可用内存但在 8GB 内存的服务器上Claude-3-Haiku 可能占满内存导致系统卡死。必须编辑/etc/ollama/ollama.ymlhost: 0.0.0.0:11434 # 限制最大内存为 4GB num_ctx: 4096 # 限制线程数为 4 num_thread: 4Swap 分区强烈建议启用至少 4GB 的 swap 分区防止 OOM Killer 杀死关键进程。sudo fallocate -l 4G /swapfile sudo mkswap /swapfile sudo swapon /swapfile。Windows 11WSL2部署启用 WSL2 并安装 Ubuntu 22.04# PowerShell 以管理员身份运行 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart # 重启电脑 wsl --install wsl --set-default-version 2 wsl --install -d Ubuntu-22.04在 WSL2 中安装 OpenClaw 和 Ollama步骤与 Ubuntu 完全相同。Windows 主机访问 WSL2 服务Node.js 服务监听0.0.0.0:3001不是localhost:3001。在 Windows 浏览器中访问http://localhost:3001即可WSL2 的localhost会自动映射到 Windows 主机。注意claude-desktop或claude-code这类桌面应用Paperclip 架构中明确不推荐使用。原因有三① 它们是闭源 Electron 应用无法嵌入到现有 React 界面中② 它们的本地模型运行在 Windows 原生环境与 OpenClaw 的 WSL2 环境隔离无法共享文件系统③ 它们的 API 不开放无法进行细粒度的流式控制和错误处理。Paperclip 坚持“命令行优先”所有能力都必须能通过openclaw xxx和ollama run xxx在终端里复现。4. Paperclip 架构的实操过程与核心环节实现从零开始搭建一个可运行的 Demo4.1 环境准备一份可复制粘贴的初始化清单在开始编码前务必完成以下环境检查。这是 Paperclip 能否跑起来的基石跳过任何一个步骤后面都会遇到难以排查的诡异问题。检查项命令期望输出不通过的后果Node.js 版本node --versionv18.20.420.x/22.x 版本在流式处理上存在已知 bug会导致 SSE 连接不稳定npm 权限npm config get prefix/usr/local非/home/user/.npm-global如果是用户目录npm install -g安装的全局包可能不在 PATH 中OpenClaw 可执行性openclaw --help | head -n 5显示帮助信息包含pdf,obsidian,code等子命令说明 OpenClaw 未正确安装或权限不足Ollama 服务状态systemctl is-active ollamaUbuntu或ollama listWSL2active或列出claude-3-haikuOllama 未运行Claude 模型无法调用临时目录权限ls -ld /tmp/paperclipdrwxrwxrwtsticky bit/tmp目录权限错误会导致文件写入失败提示/tmp目录的 sticky bitt是必须的否则不同用户的临时文件会互相覆盖。如果ls -ld /tmp显示-rwxrwxrwx没有t请立即执行sudo chmod 1777 /tmp。这是一个 Linux 系统常识但在容器化部署时容易被忽略。4.2 初始化项目结构一个最小但完整的 Paperclip 项目创建一个名为paperclip-demo的文件夹结构如下paperclip-demo/ ├── client/ # React 前端 │ ├── public/ │ └── src/ │ ├── hooks/ │ │ └── useSSE.ts │ ├── App.tsx │ └── main.tsx ├── server/ # Node.js 胶水层 │ ├── server.js │ ├── package.json │ └── Dockerfile ├── .gitignore └── README.mdStep 1初始化 Node.js 服务cd server npm init -y npm install express cors multer npm install --save-dev nodemonpackage.json的scripts部分{ scripts: { start: node server.js, dev: nodemon --watch server.js server.js } }Step 2编写server.js基于 3.1 节的精简版将 3.1 节的代码保存为server.js并修改tempFilePath的路径为/tmp/paperclip/确保该目录存在。Step 3启动服务并验证# 在 server/ 目录下 npm run dev # 应看到 Paperclip server running on http://localhost:3001 # 手动测试 OpenClaw Claude 管道 echo This is a test document. /tmp/test.txt openclaw text --input /tmp/test.txt | ollama run claude-3-haiku # 应该输出对 This is a test document. 的回应Step 4初始化 React 前端cd ../client npx create-react-app . --template typescript npm install将 3.2 节的useSSEHook 放入src/hooks/useSSE.ts。修改src/App.tsximport React, { useState } from react; import { useSSE } from ./hooks/useSSE; function App() { const [file, setFile] useStateFile | null(null); const [isUploading, setIsUploading] useState(false); const { messages
阅读完成 · 觉得有帮助?
咨询建站