ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Paperclip模式:React+Node.js+OpenClaw构建可嵌入AI Agent

Paperclip模式:React+Node.js+OpenClaw构建可嵌入AI Agent 1. 项目概述Paperclip 不是回形针而是一个正在成型的 AI Agent 开发范式你搜“paperclip”第一反应可能是办公桌抽屉里那枚银色小金属片——但最近在 Node.js 和 React 开发者圈子里“Paperclip”已经悄悄变成一个技术代号指向一类轻量、可嵌入、以 React 组件为界面载体、以 Node.js 为执行底座的 AI Agent 实现模式。它不是某个开源库的官方名称也不是 npm 上已发布的包而是一种正在被高频复用的架构共识用 React 做 Agent 的“皮肤”用 Node.js 做 Agent 的“神经中枢”用 OpenClaw 这类工具链做它的“运动系统”。我从去年底开始在三个内部项目中落地这种模式从最初手动拼接 WebSocket Express React Context到后来抽象出统一的 agent-runtime 层再到最近把整套流程封装成 CLI 工具整个过程踩过的坑、调优的参数、绕开的依赖陷阱比写十个 CRUD 页面还烧脑。它解决的核心问题很实在当你的产品需要在已有 React 应用里快速集成一个能读文件、调 API、做决策、生成 Markdown 并实时渲染的智能体时你不需要重写整个后端也不必强推一套新框架——Paperclip 模式让你把 Agent 当作一个“可热插拔的 React Hook”来用。适合谁不是纯算法工程师也不是只写页面的前端同学而是那些既要懂组件生命周期、又要会写 REST 路由、还得理解 LLM token 流式返回节奏的全栈型业务开发者。它不承诺替代 LangChain 或 LlamaIndex但能让你在周五下班前把一个带上下文记忆的文档摘要 Agent 接进客户正在用的 CRM 系统里。2. 架构设计与选型逻辑为什么是 React Node.js OpenClaw 的三角组合2.1 核心思路拆解解耦“意图表达”、“决策执行”与“状态同步”Paperclip 模式的底层逻辑本质是对 AI Agent 三大能力的物理分层意图表达层React负责用户输入的结构化如表单校验、多步骤引导、结果的可视化渲染Markdown 高亮、代码块折叠、图表联动以及最关键的——交互状态的局部管理。这里不用 Redux 或 Zustand 全局 store而是用useReduceruseContext构建一个轻量级的 AgentStateProvider每个 Agent 实例独占一份状态树避免跨组件污染。我试过直接用 useState 管理复杂 Agent 状态结果在流式响应中途触发多次 rerender导致滚动条跳动、光标丢失最后发现必须把isStreaming、currentStep、errorStack这些字段收束到一个 reducer 里用dispatch({ type: STREAM_CHUNK, payload })统一更新。决策执行层Node.js承担所有不能放在浏览器里的重活LLM 调用避开 CORS 和密钥暴露、文件解析PDF/DOCX 解析依赖 native 模块、外部 API 调用带 auth header 的企业内网服务、长任务队列如批量处理 50 个 Excel 表格。这里的关键不是“用不用 Express”而是如何设计请求-响应的语义契约。我们最终放弃传统 RESTful 设计改用/agent/:id/run这种路径配合POSTbody 里传{ input: { fileId: xxx, prompt: 总结前三页 }, config: { model: qwen2.5-7b, maxTokens: 1024 } }。好处是语义清晰——不是“创建资源”而是“触发一次智能体运行”。Node.js 版本我们锁定在 20.18.1 LTS因为 OpenClaw 的openclaw/core在 21 版本里对worker_threads的初始化有兼容性问题这个坑我们在灰度发布时才发现回滚花了 3 小时。运动系统层OpenClaw它不是 Paperclip 的必需依赖但却是让模式真正“跑起来”的关键粘合剂。OpenClaw 提供了两样不可替代的东西一是标准化的 Agent 插件接口Tool类必须实现execute()和schema()二是内置的 session 管理机制。注意那个高频报错agent failed before reply: session file locked (timeout 60000ms)不是 OpenClaw 的 bug而是你没理解它的 session 设计哲学——它默认把每个 Agent 实例的中间状态如 tool call history、memory buffer持久化到本地文件用于断点续跑。当并发请求打进来多个进程同时尝试fs.writeFileSync(sessionPath, data)就会锁死。解决方案不是关掉 session而是改用 Redis 作为 backend在openclaw.config.js里配置session: { type: redis, url: redis://localhost:6379 }实测 QPS 从 3 提升到 47。2.2 为什么不是其他组合避坑对比分析对比维度Paperclip 模式ReactNodeOpenClawNext.js App Router Server ActionsVercel AI SDK Edge RuntimeElectron Python Backend开发调试效率前后端分离VS Code Chrome DevTools 双开调试React 组件热更新秒级生效Node.js 用nodemon监听路由文件变化Server Actions 调试需刷新整个页面错误堆栈常指向编译后代码定位困难Edge Runtime 日志不完整console.log常丢失本地模拟环境与生产行为不一致主进程与渲染进程通信链路长IPC 消息序列化开销大Chrome DevTools 无法直接 inspect Python文件操作能力Node.js 可直接fs.readFileSync()读取上传文件临时路径调用pdf-parse解析 PDFServer Actions 中req.file不存在需先存到 S3 再传 URL 给 AI延迟增加 800msEdge Runtime 不支持fs无法解析本地文件必须走第三方 APIPython 可完美解析但 Electron 打包后体积暴增 120MBMac M1 用户反馈启动慢Agent 状态管理OpenClaw session Redis支持跨请求 memory 持久化用户切 Tab 后回来仍保持对话上下文依赖cookies()或headers()传递 state token易被拦截且无法处理长对话createStreamableValue仅维持单次请求生命周期断网重连后状态全丢主进程全局变量管理但多窗口场景下状态不同步需额外实现广播机制部署成本两个 Docker 容器nginxreact、nodejs阿里云 ECS 2C4G 足够支撑 200 并发Vercel 免费额度够小项目但自定义域名 SSL 配置复杂冷启动超 2sVercel 边缘节点分布广但国内访问延迟高上海用户平均首字节 1.2s需分发安装包Windows 杀毒软件常误报客服反馈率高达 17%提示不要被“React 是前端框架”这个标签限制。Paperclip 模式里React 组件实际承担的是Agent 的 UI Protocol—— 它定义了用户如何与 Agent 交互输入框、按钮、拖拽区、Agent 如何向用户反馈流式文字、进度条、错误弹窗、以及双方如何协商下一步动作如 Agent 返回{action: ask_for_file, field: invoice_pdf}React 组件自动渲染文件上传控件。这比单纯渲染 HTML 复杂得多所以必须用 React 的声明式更新能力而不是 jQuery 式 DOM 操作。2.3 技术栈版本锁定策略稳定压倒一切我们团队制定了一套严格的版本控制清单不是为了守旧而是因为 AI 工具链的版本碎片化太严重Node.js固定使用20.18.1LTS原因有三① OpenClaw 的openclaw/toolkit依赖node-fetch3.3.2该版本在 Node.js 21 的globalThis环境下有内存泄漏②sharp图像处理库在 22 版本中默认启用 SIMD但某些 ARM 服务器如阿里云 gn7i会触发 SIGILL③bcrypt编译时要求 Python 3.10而 Ubuntu 22.04 默认 Python 3.10.12升级 Node.js 到 22.x 后需手动指定PYTHON/usr/bin/python3.10CI 流程变复杂。React锁定18.2.0禁用useTransition和useDeferredValue。这两个 Hook 在流式响应场景下会引发竞态条件——当 Agent 正在返回第 3 个 tokenuseDeferredValue却把第 1 个 token 的旧状态又推给 DOM导致内容闪烁。我们用useEffectref手动控制渲染时机虽然代码多 15 行但稳定性提升 100%。OpenClaw强制使用v0.12.4这是最后一个不强制要求openclaw/llm的版本。后续版本把 LLM client 抽成独立包导致npm install时出现 peer dependency conflictopenclaw/core0.13.0要求openclaw/llm^0.5.0但openclaw/llm0.5.0又要求openai4.29.0而我们的项目用的是openai4.32.0。解决办法是resolutions字段但在 pnpm 下不生效最终选择降级。3. 核心细节解析与实操要点从零搭建一个可运行的 Paperclip Agent3.1 前端 React 层构建 Agent 的“数字躯体”Paperclip 的 React 层不是静态页面而是一个具备“生命体征”的容器。核心在于三个自定义 Hook 的协同useAgentRuntime(agentId: string)封装了与 Node.js 后端的通信逻辑。它内部维护一个EventSource连接不是 WebSocket因为 SSE 更适合单向流式响应监听/api/agent/${agentId}/stream路径。关键细节EventSource的withCredentials: true必须开启否则跨域请求带不上 cookie重连机制要设置retry: 3000避免网络抖动导致连接中断收到data: {type:chunk,content:hello}时用JSON.parse(event.data)解析再 dispatch 到本地 reducer。useAgentUI(agentId: string)负责将 Agent 状态映射为 UI 元素。它返回一个对象{ InputArea, OutputArea, ControlBar }每个都是函数组件。例如OutputArea会根据state.status渲染不同状态idle显示欢迎语running显示流式文本加旋转图标error显示折叠的 error stack用useMemo缓存解析后的 stack trace避免每次 render 都split(\n)。useAgentMemory(agentId: string)管理 Agent 的短期记忆。它不依赖外部数据库而是用useState存储一个Mapstring, anykey 是sessionIdvalue 是{ messages: Array{role: user|assistant, content: string}, tools: Array{name: string, args: object} }。重点技巧当用户点击“清空对话”不是setState({})而是setMemory(prev { const newMap new Map(prev); newMap.delete(sessionId); return newMap; })避免触发整个 Map 的重新序列化。下面是一个精简但可运行的useAgentRuntime实现// hooks/useAgentRuntime.tsx import { useEffect, useRef, useState } from react; export function useAgentRuntime(agentId: string) { const [state, setState] useState({ status: idle as idle | running | error, currentContent: , fullResponse: , error: }); const eventSourceRef useRefEventSource | null(null); const abortControllerRef useRefAbortController | null(null); const run (input: Recordstring, any) { // 清理旧连接 if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current null; } // 创建新连接 const url /api/agent/${agentId}/run; const es new EventSource(url, { withCredentials: true }); eventSourceRef.current es; es.onmessage (event) { try { const data JSON.parse(event.data); if (data.type chunk) { setState(prev ({ ...prev, status: running, currentContent: data.content, fullResponse: prev.fullResponse data.content })); } else if (data.type done) { setState(prev ({ ...prev, status: idle })); } } catch (e) { setState(prev ({ ...prev, status: error, error: 解析流数据失败 })); } }; es.onerror () { setState(prev ({ ...prev, status: error, error: 连接中断请检查网络 })); if (eventSourceRef.current) { eventSourceRef.current.close(); } }; }; useEffect(() { return () { if (eventSourceRef.current) { eventSourceRef.current.close(); } if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); return { state, run }; }注意不要在useEffect里直接new EventSource()因为组件卸载时useEffect cleanup可能晚于es.onmessage触发导致setStateon unmounted component warning。正确做法是把EventSource实例存在ref里cleanup时显式close()。3.2 后端 Node.js 层打造 Agent 的“决策大脑”Node.js 层的核心是AgentRunner类它封装了 OpenClaw 的调用、session 管理、错误兜底。我们不用 Express 的app.use()全局中间件而是为每个 Agent 路由单独配置// routes/agentRoutes.js const express require(express); const { AgentRunner } require(../core/AgentRunner); const { createRedisSessionStore } require(../utils/sessionStore); const router express.Router(); // 初始化 Runner 实例避免每次请求都 new const runner new AgentRunner({ sessionStore: createRedisSessionStore(), llmClient: new OpenAIClient({ apiKey: process.env.OPENAI_API_KEY }), tools: [ new FileTool(), // 自定义工具实现 readPdf、parseExcel new WebSearchTool() // 调用 SerpAPI ] }); router.post(/:agentId/run, async (req, res) { const { agentId } req.params; const { input, config } req.body; try { // 验证输入合法性防止恶意 payload if (!input || typeof input ! object) { throw new Error(input must be an object); } // 创建唯一 session ID基于用户 ID agent ID 时间戳 const sessionId ${req.user.id}_${agentId}_${Date.now()}; // 启动流式响应 res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive }); // runner.run 返回一个 AsyncIterator逐个 yield chunk const stream await runner.run(agentId, input, { sessionId, ...config }); for await (const chunk of stream) { res.write(data: ${JSON.stringify(chunk)}\n\n); // 强制 flush避免 Nginx 缓存 res.flush(); } res.write(data: {type:done}\n\n); res.end(); } catch (error) { console.error(Agent ${agentId} failed:, error); res.status(500).json({ error: error.message }); } }); module.exports router;AgentRunner.run()方法的关键逻辑Session 加载从 Redis 获取session:${sessionId}如果不存在则创建空 sessionInput 注入把input合并进 session 的messages数组role 为userLLM 调用构造 prompt含 system message、history、tools schema调用llmClient.chat.completions.create()设置stream: trueChunk 解析监听response.body的data事件用正则/data: (.*)\n\n/g提取每个 chunkTool Call 处理当 LLM 返回{tool_calls: [...]}遍历 calls调用对应Tool.execute()把结果追加到 session循环判断如果本轮没有 tool call 或所有 tool call 完成则结束流否则把 tool result 加入 messages再次调用 LLM。实操心得OpenClaw 的tool.execute()返回 Promise但某些工具如pdf-parse在解析大文件时会阻塞主线程。我们用worker_threads把耗时操作移到子线程// tools/FileTool.js const { Worker, isMainThread, parentPort } require(worker_threads); class FileTool { async execute({ fileId }) { if (isMainThread) { return new Promise((resolve, reject) { const worker new Worker(__filename, { workerData: { fileId } }); worker.on(message, resolve); worker.on(error, reject); }); } else { // 子线程里执行 fs.readFileSync pdf-parse const data await parsePdf(workerData.fileId); parentPort.postMessage(data); } } }3.3 OpenClaw 配置与工具开发赋予 Agent “动手能力”OpenClaw 的Tool开发是 Paperclip 模式的价值放大器。一个合格的 Tool 必须满足三个条件可发现、可验证、可审计。可发现schema()方法返回的 JSON Schema 必须精确描述参数。例如WebSearchTool.schema()返回{ name: web_search, description: 搜索互联网获取最新信息当用户问题涉及实时数据如股价、天气、新闻时调用, parameters: { type: object, properties: { query: { type: string, description: 搜索关键词用中文不超过 20 字 } }, required: [query] } }这个 schema 会被 OpenClaw 注入到 LLM 的 system prompt 里直接影响 LLM 是否选择该 tool。可验证execute()内部必须做输入校验和错误分类。比如FileTool.execute()会检查fileId是否在白名单内防止路径遍历攻击调用pdf-parse后检查result.text.length 100否则抛出new ToolError(PDF 解析失败内容过短可能已损坏)。OpenClaw 会捕获ToolError并原样返回给 LLM让 LLM 知道“这个 tool 不行换一个”。可审计每个 tool call 的输入、输出、耗时必须记录到日志。我们用pino记录// utils/logger.js const logger pino({ transport: { target: pino-pretty, options: { colorize: true } }, level: info }); // 在 Tool.execute 里 const start Date.now(); try { const result await doSomething(input); logger.info({ tool: this.name, input, outputLength: result.length, durationMs: Date.now() - start }); return result; } catch (e) { logger.error({ tool: this.name, input, error: e.message, durationMs: Date.now() - start }); throw e; }我们开发了四个核心工具覆盖 80% 场景工具名调用场景关键实现技巧性能优化点FileReaderTool解析用户上传的 PDF/Excel/Word用pdf-parse解析 PDFxlsx解析 Excelmammoth解析 DOCX对 10MB 文件做分块解析每块 2MB避免内存溢出WebSearchTool查询实时信息调用 SerpAPI设置num: 3限制返回结果数缓存 1 小时内的相同 query用 RedisSETEXCodeExecutorTool运行用户提供的 Python/JS 代码用vm2沙箱执行 JSdocker run --rm -v $(pwd):/workspace python:3.11执行 Python限制 CPU 时间 3s内存 256MB超时强制 killDatabaseQueryTool查询内部 MySQL 数据库用mysql2连接池SQL 模板化禁止拼接 user input预编译常用查询pool.prepare(SELECT * FROM users WHERE id ?)4. 实操过程与核心环节实现手把手部署一个“合同条款审查 Agent”4.1 环境准备从零开始的 15 分钟搭建我们以“合同条款审查 Agent”为例演示完整部署流程。假设你有一台干净的 Ubuntu 22.04 服务器阿里云 ECS2C4GStep 1安装 Node.js 20.18.1# 下载二进制包避免 apt 源版本过旧 curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs # 验证版本 node -v # 应输出 v20.18.1 npm -v # 应输出 10.2.2 # 设置 npm registry国内加速 npm config set registry https://registry.npmmirror.comStep 2安装 Redis用于 OpenClaw sessionsudo apt update sudo apt install redis-server sudo systemctl enable redis-server sudo systemctl start redis-server # 验证 redis-cli ping # 应返回 PONGStep 3克隆并安装 Paperclip 模板git clone https://github.com/your-org/paperclip-template.git cd paperclip-template # 安装依赖pnpm 更快且自动 dedupe curl -fsSL https://get.pnpm.io/install.sh | sh - source ~/.pnpm-env pnpm install模板目录结构paperclip-template/ ├── client/ # React 前端 │ ├── src/ │ │ ├── hooks/ # useAgentRuntime 等 │ │ └── components/ │ │ └── ContractReviewer/ # 合同审查 Agent 组件 ├── server/ # Node.js 后端 │ ├── routes/ │ │ └── agentRoutes.js │ ├── core/ │ │ └── AgentRunner.js │ └── tools/ │ ├── FileReaderTool.js │ └── ContractReviewTool.js # 专用工具 ├── .env # 环境变量 └── docker-compose.ymlStep 4配置环境变量# 复制 .env.example 为 .env cp .env.example .env # 编辑 .env vim .env关键变量NODE_ENVproduction PORT3000 REDIS_URLredis://localhost:6379 OPENAI_API_KEYsk-... # 你的 OpenAI key CONTRACT_REVIEW_MODELqwen2.5-7b # 或 gpt-4o4.2 开发 ContractReviewTool让 Agent 真正“看懂”合同这个工具的目标是接收 PDF 合同返回结构化风险点如“违约金过高”、“管辖法院约定不明”。它不调用 LLM而是用规则引擎 小模型// server/tools/ContractReviewTool.js const { Tool } require(openclaw/core); const { extractTextFromPdf } require(./FileReaderTool); const { loadModel, predict } require(../ml/contractClassifier); // 自研轻量模型 class ContractReviewTool extends Tool { constructor() { super(); this.name contract_review; this.description 审查合同 PDF识别法律风险条款返回 JSON 格式的风险点列表; } schema() { return { name: this.name, description: this.description, parameters: { type: object, properties: { fileId: { type: string, description: 合同文件 ID由前端上传后返回 } }, required: [fileId] } }; } async execute({ fileId }) { // 1. 读取 PDF 文本 const text await extractTextFromPdf(fileId); // 2. 预处理去除页眉页脚、合并连续空行 const cleanText text .replace(/第\s*\d\s*页\s*\/\s*\d/g, ) .replace(/\n\s*\n/g, \n\n); // 3. 调用本地小模型比调用 GPT-4 便宜 90%且响应快 const model await loadModel(); // 模型加载一次缓存到内存 const risks await predict(model, cleanText); // 4. 后处理按风险等级排序截取前 5 条 risks.sort((a, b) b.confidence - a.confidence); return risks.slice(0, 5); } } module.exports ContractReviewTool;predict()函数使用 ONNX Runtime 加载量化后的 PyTorch 模型输入是文本 embedding用 sentence-transformers/all-MiniLM-L6-v2输出是 12 个风险类别的概率分布。模型训练数据来自公开的合同库如 SEC EDGAR标注了 2000 条风险条款。4.3 前端集成在 React 中调用 Agentclient/src/components/ContractReviewer/ContractReviewer.tsximport React, { useState } from react; import { useAgentRuntime } from ../../hooks/useAgentRuntime; export function ContractReviewer() { const [file, setFile] useStateFile | null(null); const [isUploading, setIsUploading] useState(false); const { state, run } useAgentRuntime(contract-reviewer); const handleUpload async (e: React.ChangeEventHTMLInputElement) { const selectedFile e.target.files?.[0]; if (!selectedFile) return; setIsUploading(true); try { // 1. 上传文件到 /api/upload获取 fileId const formData new FormData(); formData.append(file, selectedFile); const uploadRes await fetch(/api/upload, { method: POST, body: formData, credentials: include }); const { fileId } await uploadRes.json(); // 2. 启动 Agent run({ fileId }); } catch (error) { alert(上传失败 error.message); } finally { setIsUploading(false); } }; return ( div classNamecontract-reviewer h2合同条款智能审查/h2 div classNameupload-area onClick{() document.getElementById(file-input)?.click()} {file ? ( div已选择{file.name}/div ) : ( div点击上传合同 PDF≤10MB/div )} input idfile-input typefile accept.pdf onChange{handleUpload} classNamehidden / /div {state.status running ( div classNamestatus-indicator div classNamespinner/div spanAI 正在审查合同.../span /div )} {state.fullResponse ( div classNameoutput h3审查结果/h3 pre{JSON.stringify(JSON.parse(state.fullResponse), null, 2)}/pre /div )} /div ); }4.4 Docker 部署一键上线docker-compose.ymlversion: 3.8 services: nginx: image: nginx:alpine ports: - 80:80 volumes: - ./client/build:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/nginx.conf depends_on: - nodejs nodejs: build: . environment: - NODE_ENVproduction - PORT3000 - REDIS_URLredis://redis:6379 - OPENAI_API_KEY${OPENAI_API_KEY} ports: - 3000:3000 depends_on: - redis redis: image: redis:7-alpine command: redis-server --save 60 1 --loglevel warning volumes: - redis-data:/data volumes: redis-data:构建并启动# 构建前端 cd client npm run build cd .. # 构建并启动 docker-compose up -d --build # 查看日志 docker-compose logs -f nodejsNginx 配置 (nginx.conf) 关键点location /api/ { proxy_pass http://nodejs:3000/; 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_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; # 关键禁用 buffering确保 SSE 流式响应不被缓存 proxy_buffering off; proxy_cache off; }5. 常见问题与排查技巧实录那些凌晨三点的报错真相5.1 OpenClaw 相关高频问题速查表错误信息根本原因解决方案验证方法agent failed before reply: session file locked (timeout 60000ms)OpenClaw 默认 session store 用fs.writeFileSync并发写入触发文件锁在openclaw.config.js中配置session: { type: redis, url: redis://localhost:6379 }redis-cli keys session:*查看是否有大量 session keyTypeError: Cannot read properties of undefined (reading execute)OpenClaw 的Tool类未正确导出或tools数组里混入了 undefined检查server/tools/index.js的导出module.exports [new FileReaderTool(), new ContractReviewTool()]确保每个 new 都成功在 Node.js REPL 中require(./tools).map(t t.name)应返回工具名数组Error: Request failed with status code 401OpenAI API Key 无效或openclaw/llm版本与 OpenAI API 版本不匹配检查OPENAI_API_KEY环境变量是否漏设确认openai包版本Paperclip 模式要求4.32.0curl -H Authorization: Bearer YOUR_KEY https://api.openai.com/v1/models应返回 200Error: spawn ENOENTCodeExecutorTool调用docker run时宿主机未安装 Docker在服务器上执行which docker若无输出则需sudo apt install docker.iosudo docker run hello-world应输出欢迎信息RangeError: Maximum call stack size exceededLLM 返回的tool_calls形成无限循环如 tool A 调用 tool Btool B 又调用 tool A在AgentRunner.run()循环中加入最大迭代次数限制如maxIterations: 5在日志中搜索tool_call iteration #5若出现则说明被截断5.2 React 层典型问题与调试技巧问题流式响应卡在第一个 chunk后续 chunk 不触发 render原因useEffect里创建的EventSource没有正确处理onmessage或者setState被包裹在异步回调里导致闭包 stale。调试在onmessage回调里加console.log(received:, event.data)确认后端确实发送了多个 chunk检查setState是否在useCallback里被 memoized导致引用不变。修复用useRef存储最新setState在onmessage里调用setStateRef.current(...)。**问题上传大文件5
返回列表