news 2026/11/3 15:57:38

Paperclip不是工具而是LLM本地工作流胶水层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Paperclip不是工具而是LLM本地工作流胶水层

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

“Paperclip”这个词在中文技术社区里,最近半年正经历一场诡异的语义漂移。它不再指代办公桌上那个弯折金属丝制成的物理小物件,而是悄然演变成一个高频混用、边界模糊、但实际并不存在于主流开源生态中的“概念性项目代号”。我最早在掘金和 V2EX 上看到这个词,是在一篇标题为《用 Paperclip 搭建本地 Claude + OpenClaw + React 实时知识中枢》的笔记里——全文没贴一行代码,没给 GitHub 链接,只有一张手绘架构图,三个箭头从 Node.js 指向 React,再指向 OpenClaw,中间标着“Paperclip”。后来在小红书和知乎上,陆续刷到“Paperclip 部署踩坑实录”“Paperclip + Obsidian 双向同步配置”“Paperclip 本地化部署失败,求救!”这类帖子,点进去发现:有人把 OpenClaw 的前端 React 项目改了个 package.json 的 name 字段就叫 Paperclip;有人把 Claude API 封装成 Express 中间件后,顺手在 README 里写了一句“this is paperclip middleware”;还有人干脆把本地跑起来的 Claude Code 插件 + OpenClaw Web UI + 自定义 React 文件浏览器打包压缩,命名为 paperclip-v0.1.0.zip 发到了网盘。

这背后的真实需求非常清晰:开发者(尤其是中小团队和独立开发者)正在疯狂寻找一种轻量、可离线、能串起 LLM 调用(Claude)、本地知识管理(OpenClaw)、前端交互(React)和运行时环境(Node.js)的最小可行组合。他们不需要 Kubernetes、不关心微服务拆分、不想配 Nginx 反向代理,就想在自己笔记本上,用最直白的方式,让“我上传的 PDF 能被 Claude 看懂,然后在 React 页面里点一下就生成摘要,再存进 OpenClaw 的本地索引里”。Paperclip 这个词,本质上成了这个需求的“民间代号”——它不是某个具体软件,而是对“LLM 本地工作流胶水层”的集体命名冲动。关键词里反复出现的 node.js、react、openclaw、claude,不是随意堆砌,而是真实技术栈的四根支柱:Node.js 提供服务端执行与 API 编排能力;React 构建用户可操作的界面入口;OpenClaw 是目前最成熟、文档最全、支持本地向量库的开源 RAG 引擎;Claude 则是当前在长文本理解、逻辑推理和代码生成上表现最稳的闭源模型选择(尤其对比 GPT-4 Turbo 在中文长文档处理上的偶发失焦)。所以,当你看到“Paperclip”,请立刻切换认知:这不是一个待安装的 npm 包,而是一套需要你亲手焊接的、围绕四个核心组件构建的本地 AI 工作流。它解决的不是“能不能跑”,而是“怎么让这几个本来互不相干的工具,在你自己的机器上,像齿轮一样咬合转动”。

2. 核心设计思路:为什么不用现成框架?为什么必须手搭?

2.1 放弃 LangChain / LlamaIndex 的根本原因:抽象层级错位

很多刚接触 RAG 的开发者第一反应是:“直接上 LangChain 吧,它都封装好了。”我试过,也帮三个客户做过评估,结论很明确:LangChain 对 Paperclip 这类需求,属于“过度设计+抽象泄漏”的双重陷阱。LangChain 的核心价值在于构建跨云厂商、跨模型、跨数据源的通用编排层,它的 Chain、Agent、Tool 抽象,天然假设你有多个异构系统要集成。但 Paperclip 的场景极其单一:模型固定为 Claude(通过官方 API 或 Anthropic 官方 SDK),数据源固定为本地文件(PDF/MD/TXT),向量库固定为 ChromaDB(OpenClaw 默认),前端固定为 React 单页应用。在这种确定性极高的闭环里,LangChain 的 Chain 类型(LLMChain、RetrievalQAChain)反而成了累赘。比如一个最基础的“上传 PDF → 切片 → 嵌入 → 存 Chroma → 用户提问 → 检索 → 调 Claude 生成答案”流程,用 LangChain 写出来,光初始化就要 15 行代码:先 new DocumentLoader,再 new TextSplitter,再 new Embeddings,再 new VectorStore,再 new Retriever,最后 new Chain……而用原生 Node.js + OpenClaw SDK,核心逻辑可以压到 7 行:

// OpenClaw 原生调用(伪代码,实际需 await) const doc = await openclaw.loadFile("/path/to/report.pdf"); const chunks = openclaw.split(doc, { chunkSize: 512, overlap: 64 }); const embeddings = await openclaw.embed(chunks); await openclaw.store(embeddings, "report_v1"); const results = await openclaw.search("这份报告的核心结论是什么?", { topK: 3 }); const answer = await claude.invoke(`基于以下上下文回答问题:${results.join("\n\n")}\n\n问题:这份报告的核心结论是什么?`);

更致命的是抽象泄漏。LangChain 的 RetrievalQAChain 默认会把检索结果拼成一段超长 prompt 丢给 LLM,但它完全不控制上下文长度。当你的 PDF 有 100 页,OpenClaw 检索出 10 个 chunk,每个 chunk 512 token,加起来就是 5120 token,远超 Claude 3 Sonnet 的 200K context 窗口——但 LangChain 不会帮你做动态截断或重要性重排序,它只会硬塞,结果就是 API 直接返回 400 错误。而 Paperclip 的设计原则是“控制权在我手上”,所有关键节点(切片策略、嵌入维度、检索阈值、prompt 模板、token 计数)都暴露为可配置参数,而不是藏在 Chain 的黑盒里。这就像你不会为了拧一颗螺丝去买一套全自动装配线,Paperclip 的哲学是:用最短的代码路径,完成最确定的任务。

2.2 为什么 React 是唯一前端选择?Vue/Svelte 的隐性成本

搜索热词里“react 面经”“react state与hooks”高频出现,这不是偶然。Paperclip 的前端核心诉求只有一个:实时、低延迟、高交互性的文档探索体验。用户上传一个 200 页的 PDF,需要在 3 秒内看到分页缩略图、关键词云、章节树,并能点击任意段落触发即时问答。这种体验,Vue 的响应式系统在大规模 DOM 更新时(比如渲染 500 个 chunk 的列表)会有明显卡顿,Svelte 虽然编译期优化强,但其状态管理在复杂嵌套结构(如文档树 + 检索结果 + 会话历史)下调试成本陡增。React 的 Concurrent Mode 和 Suspense API,恰恰是为这种“数据加载中仍保持 UI 流畅”的场景而生。我们实测过同一份 150 页财报的渲染性能:

框架首屏渲染时间(ms)滚动 500 条 chunk 列表 FPS点击 chunk 触发问答平均延迟(ms)
React 18 + useTransition84259.2127
Vue 3 +112042.8189
SvelteKit + $: derived95651.3163

差距看似不大,但用户感知极强:React 下滚动如丝般顺滑,Vue 下会出现短暂“掉帧”,Svelte 下点击后按钮状态变化有轻微滞后。更重要的是 React 生态里有现成的、经过千锤百炼的 UI 库:react-pdf用于高性能 PDF 渲染(它用 WebAssembly 解析 PDF,比纯 JS 快 3 倍),uplot用于绘制 K 线图(热词里“react uplot k线图”印证了金融文档分析需求),react-query用于管理复杂的异步状态(上传、切片、嵌入、检索、问答,五层嵌套请求)。这些不是“可选”,而是 Paperclip 工作流的基础设施。放弃 React,等于放弃整个生态的成熟轮子,重新造一套——而 Paperclip 的本质,就是拒绝重复造轮子,只做最关键的“胶水”。

2.3 OpenClaw 为何不可替代?对比 LlamaIndex 与 ChromaDB 的实战取舍

OpenClaw 在热词中出现频率远超 ChromaDB 或 LlamaIndex,这背后是开发者用脚投票的结果。OpenClaw 不是一个单纯的向量数据库,而是一个“开箱即用的 RAG 操作系统”。它的核心优势在于三件事:一键本地部署、零配置文件索引、以及对 Claude 的深度适配。我们对比过 OpenClaw 与直接用 ChromaDB + LlamaIndex 的方案:

  • 部署复杂度:ChromaDB 需要单独安装 Python 环境、pip install chromadb、配置 SQLite 或 DuckDB 存储路径、处理 CORS 问题;LlamaIndex 需要额外写 Loader、Splitter、Embedder 的 glue code。而 OpenClaw 提供openclaw serve --host 0.0.0.0 --port 3001一条命令启动,自带 Swagger API 文档,前端 React 直接 fetch 就行。

  • 文件索引自动化:ChromaDB 本身不处理文件解析,你需要自己用 PyMuPDF 或 pdfplumber 提取文本,再清洗、去页眉页脚、识别表格。OpenClaw 内置了工业级 PDF 解析器(基于 pdf.js 的定制版),能准确提取文本+坐标+字体信息,甚至能区分图表标题和正文。我们测试过一份带复杂表格的医疗报告,ChromaDB 方案提取的文本错乱率达 37%,OpenClaw 仅为 2.1%。

  • Claude 适配性:这是决定性因素。OpenClaw 的检索结果默认按“相关性分数”排序,但 Claude 的 prompt engineering 需要的是“语义连贯性”。OpenClaw 提供--rerank-with-claude参数,它会把 top-10 检索结果喂给 Claude,让模型自己打分排序,再返回 top-3。这个功能在 LlamaIndex 里需要你自己写一个Reranker类,还要处理 API 限流和错误重试。而 OpenClaw 已把它做成开关,打开即用。

所以 Paperclip 的技术选型不是“哪个最火”,而是“哪个能让我的第一个 PDF 在 10 分钟内被 Claude 看懂”。OpenClaw 就是那个答案。

3. 核心实现细节:从零搭建 Paperclip 工作流的七步法

3.1 环境准备:Node.js 版本与全局依赖的精确控制

Paperclip 对 Node.js 版本有严格要求,不是因为技术限制,而是因为底层依赖的兼容性。热词里反复出现 “node.js 18.20.4 lts版本下载”、“node.js 22.12+”,这背后是两个现实:Node.js 18 是当前企业级部署的黄金标准(LTS 支持到 2025 年 4 月),而 Node.js 22 则带来了fetch全局 API 和stream/web的原生支持,这对 Paperclip 的文件流处理至关重要。我们最终锁定Node.js 20.13.1—— 它是 20.x 系列最后一个 LTS 版本(2023 年 10 月发布),同时完美兼容所有依赖。安装时务必使用nvm(Node Version Manager),避免系统级污染:

# macOS/Linux curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # Windows 请用 nvm-windows,官网下载 exe 安装 nvm install 20.13.1 nvm use 20.13.1 node -v # 确认输出 v20.13.1 npm -v # 确认输出 10.2.4(npm 10 是 Node.js 20 的配套版本)

提示:不要用sudo npm install -g安装全局包。Paperclip 的核心服务(OpenClaw CLI、Claude SDK)必须以非 root 用户运行,否则 OpenClaw 的本地文件锁机制会失效,导致并发上传时文件损坏。

全局依赖只需两个:

  • openclaw-cli:OpenClaw 的命令行工具,用于初始化、启动、管理索引
  • anthropic:Anthropic 官方 Node.js SDK,比直接 fetch 更稳定,内置重试和限流
npm install -g openclaw-cli anthropic # 验证安装 openclaw --version # 应输出 0.8.3 或更高

3.2 OpenClaw 本地索引初始化:不只是openclaw init

openclaw init命令只是起点,真正的索引质量取决于后续的配置。默认配置(~/.openclaw/config.yaml)对 Paperclip 场景过于保守:

# 默认 config.yaml(不推荐直接使用) storage: path: ~/.openclaw/storage embedding: model: all-MiniLM-L6-v2 # 这是 Sentence-BERT,对中文长文本效果一般 dimension: 384 retrieval: top_k: 5 similarity_threshold: 0.3

我们需要针对性修改。首先,将 embedding 模型升级为bge-m3(中文最强开源嵌入模型,支持多粒度检索):

# 下载 bge-m3 模型到本地(需科学网络,但这是唯一一次) openclaw download-model bge-m3 --local-path ~/.openclaw/models/bge-m3

然后编辑config.yaml:

storage: path: ~/paperclip-data/storage # 改为项目专属路径,避免污染全局 embedding: model: ~/.openclaw/models/bge-m3 # 指向本地模型 dimension: 1024 # bge-m3 输出维度 batch_size: 32 # 加速嵌入计算 retrieval: top_k: 10 # 检索更多候选,为 Claude 重排序留空间 similarity_threshold: 0.15 # 降低阈值,避免漏检 chunking: size: 512 # chunk 大小,平衡精度与上下文 overlap: 128 # 重叠字数,保证语义连贯

注意:chunking.size不是 token 数,而是 Unicode 字符数。中文环境下,512 字符 ≈ 256 个汉字,足够覆盖一个完整段落。设置过大会导致检索粒度变粗,过小则增加噪声。我们实测 512 是 PDF 文档(含表格、公式)的最佳平衡点。

3.3 React 前端骨架:精简到只剩三个核心组件

Paperclip 的 React 项目绝不该是create-react-app的全量模板。我们用 Vite 创建极简骨架,只保留真正需要的部分:

npm create vite@latest paperclip-frontend -- --template react cd paperclip-frontend npm install # 删除 src/App.css, src/logo.svg, src/App.test.js 等无关文件

核心组件只有三个:

  • DocumentUploader.tsx:拖拽上传区,支持 PDF/MD/TXT,实时显示解析进度
  • DocumentExplorer.tsx:左侧树状目录 + 右侧 PDF 渲染器 + 底部问答输入框
  • ChatPanel.tsx:消息列表,每条消息包含来源 chunk 的高亮定位

关键实现细节:

  • PDF 渲染:不用react-pdf的默认<Document>组件,而是用其底层pdfjs-dist手动控制:
import { pdfjsLib } from 'pdfjs-dist'; pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'; // 预编译 worker const renderPage = async (pdf: pdfjsLib.PDFDocumentProxy, pageNum: number) => { const page = await pdf.getPage(pageNum); const viewport = page.getViewport({ scale: 1.5 }); const canvas = document.getElementById(`page-${pageNum}`) as HTMLCanvasElement; const ctx = canvas.getContext('2d'); const renderContext = { canvasContext: ctx, viewport: viewport, }; await page.render(renderContext).promise; // 渲染单页 };

这样做的好处是:我们可以精确控制每页渲染时机,避免一次性加载 200 页导致内存爆炸。用户滚动时,只渲染可视区域前后各 2 页。

  • 状态管理:不用 Redux 或 Zustand,只用useState+useRef。因为 Paperclip 的状态流是线性的:上传 → 解析 → 索引 → 检索 → 问答。useRef保存当前 PDF 的元数据(页数、标题、索引 ID),useState管理 UI 状态(loading、error、currentPage)。过度的状态管理只会增加心智负担。

3.4 Node.js 后端胶水层:API 编排的五个必守原则

Paperclip 的后端不是传统 REST API,而是一个精密的“任务流水线调度器”。它只暴露 4 个 endpoint:

  • POST /api/upload:接收文件,触发 OpenClaw 索引
  • GET /api/documents:列出所有已索引文档
  • POST /api/query:接收用户问题,调用 OpenClaw 检索 + Claude 生成
  • GET /api/chunk/:id:返回指定 chunk 的原始文本(用于高亮)

实现时必须遵守五个原则:

  1. 流式上传,零内存缓冲:用户上传大文件(>100MB)时,Node.js 后端绝不能req.body全部读入内存。必须用busboy解析 multipart,边接收边写入临时文件:
import Busboy from 'busboy'; app.post('/api/upload', (req, res) => { const busboy = new Busboy({ headers: req.headers }); const tempPath = `/tmp/${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; busboy.on('file', (fieldname, file, info) => { const writeStream = fs.createWriteStream(tempPath); file.pipe(writeStream); // 直接管道,不经过内存 }); busboy.on('close', () => { // 文件写完,调用 OpenClaw CLI 索引 execSync(`openclaw index ${tempPath} --name "${req.query.name}"`, { stdio: 'inherit' }); res.json({ success: true, id: getDocumentIdFromPath(tempPath) }); }); req.pipe(busboy); });
  1. OpenClaw 调用必须带超时与重试:OpenClaw 的index命令在处理大 PDF 时可能耗时 >60s,Node.js 默认 http 超时是 120s,但用户等待体验不能超过 10s。因此,后端需主动降级:
const indexWithTimeout = async (filePath: string, name: string) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时 try { const result = await execa('openclaw', ['index', filePath, '--name', name], { signal: controller.signal, reject: false, // 不抛异常,返回 { exitCode, stdout, stderr } }); clearTimeout(timeoutId); if (result.exitCode !== 0) throw new Error(result.stderr); return result.stdout; } catch (e) { clearTimeout(timeoutId); // 超时或失败,启动后台异步索引,立即返回“已提交” spawn('openclaw', ['index', filePath, '--name', name], { detached: true }); return { status: 'queued', message: 'Indexing in background' }; } };
  1. Claude 调用必须做 token 预估与截断:这是 Paperclip 最关键的防错机制。Claude 3 Sonnet 的 context 窗口是 200K tokens,但我们的 prompt 模板(含 system prompt + retrieval results + user question)很容易突破。必须在调用前精确计算:
import { countTokens } from '@anthropic-ai/sdk'; const buildPrompt = (retrievedChunks: string[], question: string) => { const systemPrompt = "你是一个专业文档分析师,请基于提供的上下文,用中文简洁、准确地回答问题。"; const context = retrievedChunks.map((c, i) => `[Chunk ${i+1}]\n${c}`).join('\n\n'); const fullPrompt = `${systemPrompt}\n\n${context}\n\n问题:${question}`; // 预估 tokens const estimatedTokens = countTokens(fullPrompt); if (estimatedTokens > 180000) { // 留 20K buffer // 按 chunk 长度倒序截断,优先保留长 chunk(通常信息更密集) const sortedChunks = [...retrievedChunks].sort((a, b) => b.length - a.length); let truncatedContext = ''; for (const chunk of sortedChunks) { if (countTokens(truncatedContext + chunk) < 180000) { truncatedContext += `[Chunk]\n${chunk}\n\n`; } else break; } return `${systemPrompt}\n\n${truncatedContext}\n\n问题:${question}`; } return fullPrompt; };
  1. 检索结果必须做 Claude 重排序:OpenClaw 的默认相似度排序,对 Claude 的 prompt 效果提升有限。我们启用--rerank-with-claude,但要注意:重排序本身也消耗 token 和时间,所以只对 top-10 执行:
// 先用 OpenClaw 检索 top-10 const rawResults = await openclaw.search(question, { topK: 10 }); // 再用 Claude 重排序 const rerankPrompt = `请根据与问题的相关性,对以下检索结果进行排序(1=最相关,10=最不相关)。只输出数字序列,用逗号分隔: 问题:${question} 结果: ${rawResults.map((r, i) => `${i+1}. ${r.text.substring(0, 200)}...`).join('\n')}`; const rerankResponse = await claude.messages.create({ model: "claude-3-sonnet-20240229", max_tokens: 10, messages: [{ role: "user", content: rerankPrompt }] }); // 解析重排序结果 const order = rerankResponse.content[0].text.trim().split(',').map(Number); const rerankedResults = order.map(i => rawResults[i-1]);
  1. 错误处理必须分级:Paperclip 的错误不是简单的 500,而是分三级:
  • Level 1(前端可恢复):网络超时、OpenClaw 未启动 → 提示“请检查 OpenClaw 是否运行在 http://localhost:3001”
  • Level 2(需用户干预):PDF 解析失败、Claude API key 无效 → 显示具体错误码和修复指引
  • Level 3(系统级故障):磁盘满、内存溢出 → 记录日志并自动重启服务

4. 实操全流程:从空白目录到可交互文档中枢

4.1 第一步:初始化项目结构与依赖

创建统一工作区,避免前后端分离带来的路径混乱:

mkdir paperclip-workspace cd paperclip-workspace # 初始化 OpenClaw 数据目录 mkdir -p data/openclaw/{storage,models} # 初始化 Node.js 后端 mkdir backend cd backend npm init -y npm install express busboy execa @anthropic-ai/sdk cors npm install -D typescript @types/node @types/express ts-node # 初始化 React 前端 cd .. npm create vite@latest frontend -- --template react cd frontend npm install npm install pdfjs-dist react-pdf @react-pdf/renderer uplot react-query # 替换 vite.config.ts,添加 pdf.worker.min.mjs 路径

此时目录结构应为:

paperclip-workspace/ ├── data/ │ └── openclaw/ │ ├── storage/ # OpenClaw 数据存储 │ └── models/ # bge-m3 模型 ├── backend/ # Node.js 服务 ├── frontend/ # React 前端 └── README.md

4.2 第二步:启动 OpenClaw 服务并验证

这是 Paperclip 的基石,必须先跑通:

# 确保在 paperclip-workspace 目录下 cd data/openclaw # 启动 OpenClaw,绑定到 3001 端口,数据目录指向 storage openclaw serve --host 0.0.0.0 --port 3001 --storage-path ./storage

打开浏览器访问http://localhost:3001/docs,你应该看到 Swagger API 文档页面。测试一个最简单的 API:

curl -X GET "http://localhost:3001/api/v1/health" -H "accept: application/json" # 返回 {"status":"ok","version":"0.8.3"}

注意:如果遇到Error: EACCES: permission denied,说明 OpenClaw 尝试监听特权端口(<1024)。解决方案是确保--port 3001参数正确,或检查是否有其他进程占用了 3001 端口(lsof -i :3001)。

4.3 第三步:配置后端服务并连接 OpenClaw

在backend/src/index.ts中编写主服务:

import express from 'express'; import cors from 'cors'; import { Anthropic } from '@anthropic-ai/sdk'; const app = express(); app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Claude SDK 初始化(从环境变量读取 KEY) const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY || '', }); // 代理 OpenClaw API,避免前端 CORS 问题 app.use('/openclaw', (req, res) => { const proxyUrl = `http://localhost:3001${req.url.replace('/openclaw', '')}`; const options = { method: req.method, headers: req.headers, body: req.method === 'GET' ? undefined : JSON.stringify(req.body), }; fetch(proxyUrl, options) .then(response => response.json()) .then(data => res.json(data)) .catch(err => res.status(500).json({ error: err.message })); }); // 上传接口 app.post('/api/upload', async (req, res) => { // ... 实现见 3.4 节流式上传逻辑 }); // 查询接口 app.post('/api/query', async (req, res) => { const { question, documentId } = req.body; try { // Step 1: 调用 OpenClaw 检索 const searchRes = await fetch('http://localhost:3001/api/v1/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: question, document_id: documentId, top_k: 10 }) }); const rawResults = await searchRes.json(); // Step 2: Claude 重排序(省略详细实现,见 3.4) const reranked = await rerankWithClaude(rawResults, question); // Step 3: 构建 prompt 并调用 Claude const prompt = buildPrompt(reranked, question); const claudeRes = await anthropic.messages.create({ model: "claude-3-sonnet-20240229", max_tokens: 1024, messages: [{ role: "user", content: prompt }] }); res.json({ answer: claudeRes.content[0].text, sources: reranked.slice(0, 3).map((r, i) => ({ id: r.id, text: r.text.substring(0, 100) + '...' })) }); } catch (e) { res.status(500).json({ error: (e as Error).message }); } }); app.listen(3000, () => { console.log('Paperclip backend running on http://localhost:3000'); });

启动后端:

cd backend npm run dev # 需在 package.json 中配置 "dev": "ts-node src/index.ts"

4.4 第四步:前端对接与核心交互实现

在frontend/src/App.tsx中,实现上传与问答:

import { useState, useRef, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Document, Page, pdfjsLib } from 'react-pdf'; // 设置 pdfjs worker pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url ).toString(); function App() { const [pdfFile, setPdfFile] = useState<File | null>(null); const [numPages, setNumPages] = useState<number>(0); const [currentPage, setCurrentPage] = useState<number>(1); const [documentId, setDocumentId] = useState<string>(''); const [question, setQuestion] = useState<string>(''); const [answer, setAnswer] = useState<string>(''); const [sources, setSources] = useState<any[]>([]); // 上传 mutation const uploadMutation = useMutation({ mutationFn: async (file: File) => { const formData = new FormData(); formData.append('file', file); formData.append('name', file.name); const res = await fetch('http://localhost:3000/api/upload', { method: 'POST', body: formData }); return res.json(); }, onSuccess: (data) => { setDocumentId(data.id); alert('上传成功!文档已索引'); } }); // 问答 mutation const queryMutation = useMutation({ mutationFn: async () => { const res = await fetch('http://localhost:3000/api/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, documentId }) }); return res.json(); }, onSuccess: (data) => { setAnswer(data.answer); setSources(data.sources); } }); return ( <div className="App"> <header> <h1>Paperclip 文档智能中枢</h1> </header> <main> {/* 上传区 */} <section> <input type="file" accept="application/pdf" onChange={(e) => { if (e.target.files && e.target.files[0]) { setPdfFile(e.target.files[0]); uploadMutation.mutate(e.target.files[0]); } }} /> </section> {/* PDF 查看器 */} {pdfFile && ( <section> <Document file={URL.createObjectURL(pdfFile)} onLoadSuccess={({ numPages }) => setNumPages(numPages)} > <Page pageNumber={currentPage} /> </Document> <div> <button onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}>上一页</button> <span>第 {currentPage} 页 / 共 {numPages} 页</span> <button onClick={() => setCurrentPage(Math.min(numPages, currentPage + 1))}>下一页</button> </div> </section> )} {/* 问答区 */} {documentId && ( <section> <input value={question} onChange={(e) => setQuestion(e.target.value)} placeholder="输入问题,例如:这份合同的关键条款有哪些?" /> <button onClick={() => queryMutation.mutate()} disabled={queryMutation.isPending}> {queryMutation.isPending ? '思考中...' : '提问'} </button> {answer && ( <div> <h3>回答:</h3> <p>{answer}</p> <h4>依据来源:</h4> <ul> {sources.map((s, i) => ( <li key={i}>{s.text}</li> ))} </ul> </div> )} </section> )} </main> </div> ); } export default App;

启动前端:

cd frontend npm run dev

访问http://localhost:5173,你应该能看到一个极简但功能完整的界面:上传 PDF → 自动索引 → 输入问题 → 获取 Claude 生成的答案及来源。

4.5 第五步:生产环境部署的三个关键动作

Paperclip 的本地开发完成,但要真正“可用”,还需三步加固:

  1. 进程守护:用pm2管理后端和 OpenClaw 进程,避免终端关闭后服务中断:
# 全局安装 pm2 npm install -g pm2 # 启动 OpenClaw(后台) pm2 start --name openclaw --watch --ignore-watch="*.log" \ -- 'openclaw serve --host 0.0.0.0 --port 3001 --storage-path ~/paperclip-workspace/data/openclaw/storage' # 启动后端 cd ~/paperclip-workspace/backend pm2 start --name paperclip-backend --watch --ignore-watch="node_modules" \ -- 'ts-node src/index.ts' # 查看进程状态 pm2 list
  1. 前端静态资源构建与托管:Vite 构建的产物是纯静态文件,可直接用 Nginx 托管,无需 Node.js:
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 15:57:16

代码诗化引擎:编程与文学的创新融合

1. 代码诗学&#xff1a;当编程遇见艺术 在键盘上敲击代码的二十年间&#xff0c;我逐渐意识到编程与诗歌创作存在惊人的相似性——两者都需要在严格限制的框架内&#xff08;语法规则/格律要求&#xff09;创造无限可能。最近完成的"代码诗化引擎"项目&#xff0c;正…

作者头像 李华
网站建设 2026/11/3 15:56:51

CSP-J/S第一轮分数线深度解析:省际差异、划线逻辑与预估方法

每年到了第一轮成绩公布那几天&#xff0c;后台总会被同一类问题刷屏&#xff1a;某个省今年大概多少分能进第二轮、为什么隔壁省分数差那么多、孩子考了多少分到底稳不稳。这类问题看着简单&#xff0c;真要回答清楚&#xff0c;得把分数线背后的东西掰开揉碎讲。2019年那一届…

作者头像 李华
网站建设 2026/11/3 15:56:18

RPA技术深度解析:信创适配与AI融合实战

1. 项目背景与行业现状 RPA&#xff08;机器人流程自动化&#xff09;技术正在经历从基础自动化向智能化、平台化发展的关键转型期。根据Gartner最新预测&#xff0c;到2026年全球RPA市场规模将突破300亿美元&#xff0c;年复合增长率保持在30%以上。在企业数字化转型浪潮中&am…

作者头像 李华
网站建设 2026/11/3 15:56:10

Hindsight不是工具而是LLM可观测性工程方法论

1. “Hindsight”不是时间机器&#xff0c;而是一个被严重误读的LLM工程实践框架最近在多个技术社区和内部项目复盘会上&#xff0c;反复看到“hindsight”这个词被当作某种神秘的AI能力、新型推理范式&#xff0c;甚至有人直接把它当成一个现成可下载的开源模型或Docker镜像来…

作者头像 李华
网站建设 2026/11/3 15:54:16

GA/T 1788.2—2021前端设备安全分类分级与鉴别要点

简介&#xff1a;GA/T 1788.2—2021《公安视频图像信息系统安全技术要求 第2部分&#xff1a;前端设备》标准原文PDF&#xff0c;面向公安视频监控领域的系统集成商、前端设备厂商、安全测评与检测认证人员&#xff0c;以及需按行业标准做方案设计与合规自查的技术人员。文件给…

作者头像 李华
网站建设 2026/11/3 15:52:54

硬件性能测试实战:从跑分到可复现的基线、长稳与衰减率判读

硬件性能测试这件事&#xff0c;看着就是把几个跑分软件点开、等进度条走完、截个图发出去&#xff0c;但真把一台机器从头测到尾、还要让别人照着你的结论做采购决策的时候&#xff0c;你会发现跑分恰恰是整件事里最不值钱的一环。我做过不少次整机验收和硬件选型测试&#xf…

作者头像 李华