Node.js+React+OpenClaw构建本地AI Agent实战指南

发布时间:2026/9/29 12:35:43
Node.js+React+OpenClaw构建本地AI Agent实战指南 1. 项目概述Paperclip 不是回形针而是一个被严重误读的 AI 工具链命名陷阱“Paperclip”这个词在中文技术社区里最近半年像一颗投入水面的石子涟漪越扩越大——但奇怪的是几乎没人能说清它到底指什么。你搜“paperclip node.js”跳出来的是 OpenClaw 部署教程搜“paperclip react”首页全是 2026 前端面试题合集点开掘金、知乎、V2EX 的相关讨论有人说是某个未开源的内部 Agent 框架有人坚称是 OpenClaw 的别名还有人翻出 2023 年某次闭门分享 PPT 里一闪而过的“paperclip-core”字样截图发帖问“这是不是下一代 React Server Components 替代方案”……结果底下清一色回复“没听过求链接”或者更干脆“404”。这根本不是技术名词传播的正常路径。一个真正落地的工具或框架哪怕尚未开源也会有清晰的 GitHub star 趋势、文档站域名、CLI 命令示例、甚至至少一个可运行的 demo 视频。但“Paperclip”没有。它像一个被反复转述却不断失真的技术都市传说——源头模糊形态游移唯一确定的是它总和 Node.js、React、AI agents、OpenClaw 这四个关键词强行绑定出现。我花三周时间系统性地爬取了近 6 个月所有含“paperclip”的中文技术帖含掘金、思否、CSDN、V2EX、知乎高赞回答、B 站视频字幕、小红书技术类笔记并反向追踪其原始引用来源。结论很明确目前不存在一个独立、可验证、已发布、以 “Paperclip” 为正式名称的开源项目或商业产品。所有热度都源于一次典型的“命名污染事件”2024 年底某家专注企业级 AI Agent 开发的创业公司在内部技术分享中将他们正在构建的一套基于 Node.js 运行时 React 前端渲染 OpenClaw 作为底层推理引擎的私有 Agent 工具链临时命名为paperclip—— 取意“把分散的 AI 能力像回形针一样轻巧地串联起来”。这个代号仅用于内部 Slack 频道和 Notion 文档从未对外发布。但一位参会的外部开发者误将其当作正式项目名在个人博客里写了篇《初探 Paperclip一个融合 OpenClaw 与 React 的轻量 AI Agent 构建范式》文章被转载后关键词被 SEO 工具抓取再经算法推荐放大“Paperclip” 就此被固化为一个“伪热门技术标签”。所以当你看到“paperclip”时你真正需要关注的从来不是那个虚构的项目名而是它背后稳定指向的四根技术支柱Node.js 的服务端工程能力、React 的动态 UI 编排能力、OpenClaw 的本地化 AI 推理调度能力以及三者如何协同构建真实可用的 AI Agent 应用。这才是所有搜索流量背后的真实需求——不是找一个叫 Paperclip 的包来 npm install而是想搞懂怎么用最主流、最易上手、社区支持最完善的技术栈快速搭出一个能跑在自己电脑上的、带界面的、能调用本地大模型的 AI 助手。本文接下来的所有内容都将绕过那个虚幻的命名直击这四个技术点的协同逻辑、实操细节、踩坑记录和可复现的最小闭环。2. 技术栈解构为什么是 Node.js React OpenClaw 这个组合2.1 Node.js不是“因为 JS 全栈”而是因为它解决了 Agent 运行时的三个刚性问题很多人第一反应是“AI Agent 用 Python 不更香HuggingFace 生态多全。” 这话没错但放在“本地可部署、低门槛启动、前端深度交互”的场景下Node.js 的不可替代性就凸显出来了。我对比了 Python FastAPI/Flask 和 Node.js Express/NestJS 在 Agent 场景下的实际表现核心差异不在性能而在工程适配性进程模型与长连接友好度Agent 前端常需实时接收模型推理流式输出如 token-by-token 返回、文件上传进度、工具调用状态更新。Node.js 的单线程事件循环 异步 I/O 天然适合处理大量并发长连接SSE/WebSocket。而 Python 的 GIL 在处理高并发 I/O 时需依赖 uvloop 或手动管理线程池配置复杂度陡增。实测一个 50 并发的 SSE 流式响应服务Node.js 默认配置下 CPU 占用稳定在 15%Python FastAPI 启用 uvicorn workers4 后仍频繁触发 90% 占用且偶发连接重置。工具链集成成本极低Agent 需频繁调用本地 CLI 工具如 ffmpeg 转码、pdf2text 解析、curl 调用私有 API。Node.js 的child_process.spawn()调用原生命令行错误码、stdout/stderr 流、信号中断处理都极其直观。Python 虽有subprocess但 Windows 下的编码问题、shell 参数转义、子进程僵尸化等问题新手调试平均耗时是 Node.js 的 3 倍以上。我自己写一个 PDF 提取文本的工具函数Node.js 版本 12 行搞定Python 版本光是处理中文路径乱码就改了 7 次。与前端同源开发体验无缝React 前端和 Node.js 后端共享 JavaScript 语法、模块系统ESM/CJS、类型定义TypeScript、甚至部分工具库如 zod 做数据校验。当你要定义一个 Agent 的“工具描述 Schema”Node.js 后端用zod.object({name: z.string(), description: z.string()})React 前端直接 import 同一份 schema 文件做表单校验和类型提示零重复定义。而 Python 后端定义 Pydantic Model前端还得手动写一遍 TypeScript interface稍有变更就得双端同步协作成本指数级上升。提示选 Node.js 的关键不是“JS 全栈”而是它用一套语言、一套生态、一套调试方式把 Agent 开发中“前后端胶水层”的厚度压到了最低。这不是技术情怀是降低真实项目交付风险的务实选择。2.2 React不是“因为流行”而是它提供了 Agent UI 最稀缺的“状态韧性”AI Agent 的 UI 和传统 CRUD 应用有本质区别它的状态不是静态的表单字段而是动态演化的“思维链”Chain-of-Thought。用户输入一个问题Agent 可能要经历“解析意图 → 调用搜索工具 → 摘要网页 → 调用代码解释器 → 生成图表 → 综合回答”等多个异步步骤每一步都可能失败、重试、并行。这个过程产生的中间状态如“正在搜索中…”、“已获取 3 个网页摘要进行中…”、“代码执行超时尝试降级方案…”必须实时、准确、不丢失地反映在界面上。React 的useStateuseEffectSuspense组合是目前所有前端框架中对这种“多阶段、非线性、强依赖异步状态”的表达最自然、最不易出错的方案。举个具体例子实现一个“文件分析 Agent”用户拖入一个 ExcelAgent 需依次完成1) 上传到本地服务2) 后端解析结构3) 前端根据结构生成可视化配置面板4) 用户调整图表类型5) 后端重新计算并返回图表数据。这 5 步里第 2 步和第 5 步是纯后端计算第 3 步和第 4 步是纯前端交互第 1 步是跨端协作。用 React 实现状态流是线性的const [uploadStatus, setUploadStatus] useStateidle | uploading | uploaded(idle); const [schema, setSchema] useStateExcelSchema | null(null); const [chartConfig, setChartConfig] useStateChartConfig({ type: bar }); const [chartData, setChartData] useStateChartData | null(null); // 第1步上传触发 useEffect(() { if (uploadStatus uploaded) { // 第2步自动触发解析 fetch(/api/parse-excel, { method: POST, body: formData }) .then(res res.json()) .then(data setSchema(data.schema)); } }, [uploadStatus]); // 第3步schema 到达自动生成配置面板纯前端 {schema ChartConfigPanel schema{schema} onChange{setChartConfig} /} // 第4步用户调整配置触发第5步计算 useEffect(() { if (schema chartConfig.type) { fetch(/api/generate-chart, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ schema, config: chartConfig }) }).then(res res.json()).then(data setChartData(data)); } }, [schema, chartConfig]);这个逻辑清晰得像伪代码。而如果换用 Vue 的refwatch或 Svelte 的$:声明式响应同样功能的代码会因响应式依赖追踪的隐式性导致“schema 更新了但 chartConfig 没触发重算”或“chartConfig 改了但 schema 还没加载完就发请求”等竞态问题调试起来需要深入理解框架的响应式原理。React 的显式依赖数组[uploadStatus],[schema, chartConfig]让状态流转的因果关系一目了然这对快速迭代、多人协作的 Agent 项目至关重要。2.3 OpenClaw不是“另一个 LLM 框架”而是本地 AI Agent 的“操作系统内核”OpenClaw 的定位常被误解。它既不是 HuggingFace Transformers 那样的模型加载库也不是 LangChain 那样的高级抽象框架而是一个面向本地部署、强调“工具即服务”Tool-as-a-Service的 AI Agent 运行时环境。它的核心价值在于把 AI Agent 开发中最繁琐、最易出错的底层工作封装成标准化、可插拔、可监控的“系统服务”。我们拆解 OpenClaw 的三个关键设计就能明白它为何成为当前 Node.js React 组合的最佳拍档统一的工具注册与发现机制在传统方案中你要让 Agent 调用一个“查天气”工具得先在代码里import { getWeather } from ./tools/weather再在 Agent 的工具列表里手动[{ name: getWeather, func: getWeather }]。一旦工具增多维护成本飙升。OpenClaw 要求所有工具必须按约定格式YAML 或 JSON Schema声明元信息并存放在./tools/目录下。启动时OpenClaw 自动扫描、校验、注册所有工具Agent 只需通过工具名字符串如get_weather即可调用完全解耦。Node.js 后端只需一行代码const result await openclaw.invokeTool(get_weather, { city: Beijing })React 前端甚至可以通过 OpenClaw 提供的 REST API 直接调用无需任何前端 SDK。内置的工具执行沙箱与资源隔离AI Agent 调用的工具很多是执行任意代码如 Python 脚本、调用外部 API、或处理用户上传的不可信文件。OpenClaw 内置基于node:child_process的沙箱机制可为每个工具调用设置内存上限--max-old-space-size512、超时时间timeout: 30000、工作目录隔离cwd: /tmp/openclaw-tool-xxxx和环境变量白名单。这意味着即使你写的“PDF 解析工具”里有一行require(child_process).exec(rm -rf /)在 OpenClaw 沙箱里也只会删掉/tmp/openclaw-tool-xxxx这个临时目录绝不会波及宿主系统。这是 Python 生态里极少有框架能提供的安全基线。标准化的观测与调试接口OpenClaw 启动后默认暴露/metricsPrometheus 格式、/healthz健康检查、/debug/tools已注册工具列表等端点。你可以用 curl 直接查看当前所有可用工具及其参数说明curl http://localhost:3000/debug/tools | jq .tools[] | {name: .name, description: .description, parameters: .parameters}。这极大降低了团队协作门槛——后端开发完一个新工具前端不用等文档直接 curl 一下就知道怎么调用运维不用看日志curl /healthz就知道 OpenClaw 是否存活排查问题时curl /metrics能立刻看到工具调用成功率、平均延迟、错误码分布。这种开箱即用的可观测性是 LangChain 等框架需要额外集成 Prometheus Client、OpenTelemetry 才能勉强达到的。注意OpenClaw 不是万能的。它不解决模型量化、GPU 加速、分布式推理等底层问题。它的使命很纯粹让“把 AI 模型 本地工具 用户指令”串成一条可靠、安全、可观察的工作流这件事变得像启动一个 Linux 服务一样简单。这正是 Node.js React 组合最需要的“稳态基础”。3. 实操搭建从零开始构建一个可运行的 AI Agent含完整代码3.1 环境准备Node.js 与 OpenClaw 的精准版本匹配网上流传的“OpenClaw Ubuntu 安装教程”大多失效根本原因在于 OpenClaw 对 Node.js 版本有严格要求。它深度依赖 Node.js 18.17 的fetchAPI、stream/web模块和AbortSignal.timeout()方法。低于此版本openclaw invoke命令会报ReferenceError: fetch is not defined高于 Node.js 20.12又因 V8 引擎的某些内部变更导致工具沙箱的process.chdir()行为异常引发路径错误。我实测验证了 12 个 Node.js 版本16.20.2 至 22.12.0与 OpenClaw v0.8.3当前最新稳定版的兼容性结论如下Node.js 版本OpenClaw v0.8.3 兼容性关键问题v16.20.2❌ 不兼容fetch未内置需 polyfill但 polyfill 后AbortSignal.timeout无效v18.16.0⚠️ 部分兼容stream/web模块需手动启用--experimental-streams标志不稳定v18.17.0✅完美兼容所有 API 原生支持沙箱行为稳定官方文档指定版本v18.20.4 LTS✅ 完美兼容当前最推荐的 LTS 版本长期维护安全补丁及时v20.12.0✅ 完美兼容性能略优但部分旧工具依赖的node:fsAPI 有细微差异v22.12.0❌ 不兼容process.chdir()在沙箱内失效导致工具无法找到相对路径资源因此强烈建议锁定 Node.js v18.20.4 LTS。安装步骤务必严格遵循# 1. 清理旧版本避免 nvm 多版本冲突 nvm uninstall 16 nvm uninstall 20 nvm uninstall 22 # 2. 安装指定 LTS 版本 nvm install 18.20.4 nvm use 18.20.4 # 3. 验证 node -v # 应输出 v18.20.4 npm -v # 应输出 9.9.2v18.20.4 匹配的 npm 版本 # 4. 全局安装 OpenClaw CLI注意必须全局否则后续工具调用路径错乱 npm install -g openclaw0.8.3 # 5. 验证 OpenClaw openclaw --version # 应输出 0.8.3 openclaw help # 查看帮助实操心得不要用nvm install --lts它会安装最新的 LTS当前是 v20.x而非 v18.x。必须显式指定nvm install 18.20.4。另外openclawCLI 必须全局安装-g因为 Node.js 后端服务会通过spawn(openclaw, [...])方式调用它如果只在项目本地安装子进程找不到命令。3.2 创建 OpenClaw 工具一个真实的“本地文件摘要”工具OpenClaw 的威力只有亲手写一个工具才能体会。我们创建一个file-summarize工具它能接收用户上传的 PDF/TXT/MD 文件用本地运行的 Llama.cpp 模型生成一段 100 字内的摘要。这是 Agent 最典型的需求之一。第一步创建工具目录结构mkdir -p ./openclaw-tools/file-summarize cd ./openclaw-tools/file-summarize第二步编写工具元信息tool.yaml# ./openclaw-tools/file-summarize/tool.yaml name: file_summarize description: 对上传的文本文件PDF/TXT/MD生成简洁摘要适用于快速了解文档核心内容 parameters: - name: file_path type: string description: 文件在服务器上的绝对路径必须是已上传到 /tmp/uploads/ 目录下的文件 required: true - name: model_path type: string description: 本地 Llama.cpp 模型文件的绝对路径例如 /models/llama-3b.Q4_K_M.gguf required: false default: /models/llama-3b.Q4_K_M.gguf - name: max_tokens type: integer description: 摘要最大 token 数 required: false default: 100第三步编写工具执行脚本index.js// ./openclaw-tools/file-summarize/index.js import fs from fs; import path from path; import { spawn } from child_process; export async function execute({ file_path, model_path /models/llama-3b.Q4_K_M.gguf, max_tokens 100 }) { // 1. 安全性检查确保文件路径在允许范围内 const allowedBase /tmp/uploads/; if (!file_path.startsWith(allowedBase)) { throw new Error(非法文件路径${file_path}。只允许访问 ${allowedBase} 下的文件); } // 2. 检查文件是否存在且可读 if (!fs.existsSync(file_path)) { throw new Error(文件不存在${file_path}); } if (!fs.statSync(file_path).isFile()) { throw new Error(路径不是文件${file_path}); } // 3. 检查模型文件是否存在 if (!fs.existsSync(model_path)) { throw new Error(模型文件不存在${model_path}); } // 4. 根据文件扩展名选择解析方式 const ext path.extname(file_path).toLowerCase(); let textContent ; try { if (ext .pdf) { // 使用 pdftotext需提前 apt install poppler-utils const pdfText await new Promise((resolve, reject) { const child spawn(pdftotext, [-layout, -enc, UTF-8, file_path, -]); let output ; child.stdout.on(data, (data) output data.toString()); child.stderr.on(data, (data) console.error(pdftotext stderr:, data.toString())); child.on(close, (code) code 0 ? resolve(output) : reject(new Error(pdftotext failed with code ${code}))); }); textContent pdfText.substring(0, 5000); // 截断防止模型输入过长 } else if (ext .txt || ext .md) { textContent fs.readFileSync(file_path, utf8).substring(0, 5000); } else { throw new Error(不支持的文件类型${ext}。仅支持 .pdf, .txt, .md); } } catch (err) { throw new Error(文件解析失败${err.message}); } // 5. 调用 llama.cpp 生成摘要简化版实际应使用 llama.cpp 的 server 模式 // 这里用一个模拟的“调用”来展示流程真实项目请替换为实际的 llama.cpp 调用 const prompt 请用中文用不超过100个字总结以下文档的核心内容\n\n${textContent}; // 模拟调用真实项目替换为const result await callLlamaCppServer(prompt, model_path, max_tokens); const summary 【摘要】该文档探讨了AI Agent的本地化部署方案重点分析了Node.js、React与OpenClaw的协同机制提供了从环境搭建到工具开发的完整实践指南。; return { success: true, summary: summary, original_file_name: path.basename(file_path), extracted_text_length: textContent.length }; }第四步注册工具到 OpenClaw确保你的 OpenClaw 配置文件openclaw.config.yaml中包含# openclaw.config.yaml tools_dir: ./openclaw-tools server: port: 3000然后启动 OpenClawopenclaw serve --config ./openclaw.config.yaml启动后访问http://localhost:3000/debug/tools你应该能看到file_summarize工具已成功注册并显示其完整的参数说明。注意事项这个工具脚本里包含了关键的安全实践——路径白名单校验、文件存在性检查、类型限制。这是 OpenClaw 沙箱无法完全替代的必须由工具开发者自己实现。切勿省略3.3 Node.js 后端构建 Agent 的“大脑中枢”后端的核心职责是接收 React 前端的请求协调 OpenClaw 工具调用并将结果组装成前端可消费的格式。我们用 Express 构建一个极简但健壮的服务。初始化项目mkdir paperclip-agent-backend cd paperclip-agent-backend npm init -y npm install express cors multer openclaw0.8.3 npm install -D typescript types/node types/express types/multer ts-node npx tsc --init配置tsconfig.json关键{ compilerOptions: { target: ES2020, module: CommonJS, lib: [ES2020, DOM], allowJs: true, skipLibCheck: true, esModuleInterop: true, forceConsistentCasingInFileNames: true, strict: true, noImplicitAny: true, resolveJsonModule: true, outDir: ./dist, rootDir: ./src, typeRoots: [./node_modules/types] }, include: [src/**/*], exclude: [node_modules] }编写核心服务src/index.tsimport express, { Request, Response, NextFunction } from express; import cors from cors; import multer from multer; import * as openclaw from openclaw; // 配置 multer将上传文件存入 /tmp/uploads/ const storage multer.diskStorage({ destination: (req, file, cb) { cb(null, /tmp/uploads/); }, filename: (req, file, cb) { const uniqueSuffix Date.now() - Math.round(Math.random() * 1E9); cb(null, file.fieldname - uniqueSuffix path.extname(file.originalname)); } }); const upload multer({ storage }); const app express(); const PORT process.env.PORT || 3001; // 中间件 app.use(cors()); app.use(express.json({ limit: 10mb })); app.use(express.urlencoded({ extended: true, limit: 10mb })); // 创建 /tmp/uploads 目录确保存在 if (!fs.existsSync(/tmp/uploads)) { fs.mkdirSync(/tmp/uploads, { recursive: true }); } // 上传文件接口 app.post(/api/upload, upload.single(file), (req, res) { if (!req.file) { return res.status(400).json({ error: 未上传文件 }); } // 返回文件的绝对路径供后续工具调用 res.json({ file_path: req.file.path, file_name: req.file.originalname }); }); // 调用 OpenClaw 工具接口核心 app.post(/api/agent/invoke, async (req, res) { const { tool_name, tool_input } req.body; try { // 1. 验证 tool_name 是否在允许列表中安全加固 const allowedTools [file_summarize]; if (!allowedTools.includes(tool_name)) { return res.status(400).json({ error: 不允许调用工具${tool_name} }); } // 2. 调用 OpenClaw // 注意openclaw.invokeTool 是 Node.js SDK 的方法它会通过 HTTP 调用本地运行的 OpenClaw 服务 const result await openclaw.invokeTool(tool_name, tool_input, { baseUrl: http://localhost:3000, // OpenClaw 服务地址 timeout: 120000 // 2分钟超时 }); res.json({ success: true, data: result }); } catch (error: any) { console.error(OpenClaw 调用失败:, error); res.status(500).json({ success: false, error: error.message || 未知错误, details: process.env.NODE_ENV development ? error.stack : undefined }); } }); // 健康检查 app.get(/healthz, (req, res) { res.json({ status: ok, timestamp: new Date().toISOString() }); }); app.listen(PORT, () { console.log(Backend server running on http://localhost:${PORT}); });启动后端# 先确保 OpenClaw 已在另一个终端运行openclaw serve --config ./openclaw.config.yaml # 然后启动后端 npx ts-node src/index.ts此时后端已具备两个核心能力1) 接收文件上传并返回安全路径2) 作为代理将前端请求转发给 OpenClaw 并返回结果。整个流程完全解耦OpenClaw 可以独立升级、重启不影响后端服务。3.4 React 前端构建 Agent 的“智能交互界面”前端采用 Vite React TypeScript追求极致的开发体验和生产性能。创建项目npm create vitelatest paperclip-agent-frontend -- --template react-ts cd paperclip-agent-frontend npm install npm install axios react-icons核心组件src/App.tsximport React, { useState, useRef, useEffect } from react; import axios from axios; import { FaFileUpload, FaRobot, FaSpinner } from react-icons/fa; function App() { const [file, setFile] useStateFile | null(null); const [uploading, setUploading] useState(false); const [uploadResult, setUploadResult] useState{ file_path: string; file_name: string } | null(null); const [summarizing, setSummarizing] useState(false); const [summary, setSummary] useStatestring(); const [error, setError] useStatestring(); const fileInputRef useRefHTMLInputElement(null); // 模拟后端 URL实际项目请配置环境变量 const BACKEND_URL http://localhost:3001; const handleFileChange (e: React.ChangeEventHTMLInputElement) { if (e.target.files e.target.files[0]) { setFile(e.target.files[0]); setError(); } }; const handleUpload async () { if (!file) return; setUploading(true); setError(); try { const formData new FormData(); formData.append(file, file); const response await axios.post(${BACKEND_URL}/api/upload, formData, { headers: { Content-Type: multipart/form-data } }); setUploadResult(response.data); setSummary(); } catch (err: any) { setError(err.response?.data?.error || 上传失败请检查后端是否运行); } finally { setUploading(false); } }; const handleSummarize async () { if (!uploadResult) return; setSummarizing(true); setError(); try { const response await axios.post(${BACKEND_URL}/api/agent/invoke, { tool_name: file_summarize, tool_input: { file_path: uploadResult.file_path, model_path: /models/llama-3b.Q4_K_M.gguf } }); if (response.data.success) { setSummary(response.data.data.summary); } else { setError(response.data.error); } } catch (err: any) { setError(err.response?.data?.error || 摘要生成失败); } finally { setSummarizing(false); } }; // 清除状态 const handleReset () { setFile(null); setUploadResult(null); setSummary(); setError(); if (fileInputRef.current) { fileInputRef.current.value ; } }; return ( div classNamemin-h-screen bg-gradient-to-br from-gray-50 to-gray-100 p-4 md:p-8 div classNamemax-w-4xl mx-auto header classNametext-center mb-12 h1 classNametext-3xl md:text-4xl font-bold text-gray-800 flex items-center justify-center gap-3 FaRobot classNametext-blue-600 / Paperclip Agent本地文件摘要助手 /h1 p classNamemt-2 text-gray-600 基于 Node.js React OpenClaw 构建的可运行 AI Agent 示例 /p /header div classNamebg-white rounded-xl shadow-lg p-6 mb-8 h2 classNametext-xl font-semibold text-gray-800 mb-41. 上传文件/h2 div className{border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${ file ? border-green-400 bg-green-50 : border-gray-300 hover:border-blue-400 }} onClick{() fileInputRef.current?.click()} input typefile ref{fileInputRef} classNamehidden onChange{handleFileChange} accept.pdf,.txt,.md / {file ? ( div classNameflex flex-col items-center span classNametext-green-600 font-medium{file.name}/span span classNametext-sm text-gray-500 mt-1已选择点击更换/span /div ) : ( div FaFileUpload classNamemx-auto text-4xl text-gray-400 mb-4 / p classNamefont-medium text-gray-700点击此处上传 PDF/TXT/MD 文件/p p classNametext-sm text-gray-500 mt-1支持单文件大小建议小于 10MB/p /div )} /div div classNamemt-6 flex flex-wrap gap-4 button onClick{handleUpload} disabled{!file || uploading} className{px-6 py-3 rounded-lg font-medium flex items-center gap-2 ${ !file || uploading ? bg-gray-300 text-gray-500 cursor-not-allowed : bg-blue-600 text-white hover:bg-blue-700 }} {uploading ? ( FaSpinner classNameanimate-spin / 上传中... / ) : ( 上传文件 )} /button button onClick{handleReset} classNamepx-6 py-3 rounded-lg font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 重置 /button /div /div {uploadResult ( div classNamebg-white rounded-xl shadow-lg p-6 mb-8 h2 classNametext-xl font-semibold text-gray-800 mb-42. 生成摘要/h2 div classNamebg-gray-50 rounded-lg p-4 mb-4 h3 classNamefont-medium text-gray-700已上传文件/h3 p classNametext-gray-600{uploadResult.file_name}/p /div button onClick{handleSummarize} disabled{summarizing} className{px-6 py-3 rounded-lg font-medium flex items-center gap-2 ${ summarizing