
1. 项目概述Paperclip 不是回形针而是一个被严重误读的 AI 工程化枢纽“Paperclip”这个词一出来很多人第一反应是办公桌抽屉里那个银色金属小物件——回形针。但在这个技术语境下它根本不是物理实体而是当前 AI 应用层开发中一个正在快速成型、却尚未被中文社区系统梳理的轻量级本地 AI 编排框架原型。它不叫 Paperclip CLI也不叫 Paperclip Server更不是某个大厂开源的明星项目它是一组围绕OpenClaw Claude Code React Node.js四要素耦合演进而来的、高度实操导向的工程实践模式。我从去年底开始在三个内部项目中反复验证这套组合从最初手动拼接 OpenClaw 的 REST API、硬编码 Claude 的 SSE 流式响应、用 React 状态管理模拟 Agent 生命周期到最终沉淀出一套可复用的本地启动脚本、状态同步机制和 UI 响应协议——这个过程产物团队内部就叫它 “paperclip”取其“连接 disparate parts连接离散部件”的本义而非字面翻译。核心关键词 paperclip、Node.js、React、OpenClaw、Claude 在热搜中高频共现绝非偶然。它们共同指向一个真实痛点开发者想快速验证一个带 UI 的本地 AI 工作流比如文档摘要多轮问答代码生成但现有方案要么太重LangChain FastAPI Streamlit要么太散各自跑服务、手动 curl、React 端写一堆 ad-hoc fetch。Paperclip 就是为解决这个“最后一公里连接”而生的——它不替代 OpenClaw也不重写 Claude Code而是用极简的 Node.js 中间层做三件事统一端口暴露、标准化事件流格式、桥接 React 组件生命周期与 AI 执行状态。它甚至没有自己的 npm 包所有代码都藏在server/目录下不到 300 行 TypeScript 里。你搜不到它的 GitHub 主页因为它根本没打算开源但你能在掘金、知乎、V2EX 上看到大量零散提问“OpenClaw 怎么接 React”“Claude Code 本地怎么调用”“React SSE 轮询文件变化卡顿怎么办”——这些就是 paperclip 正在默默填平的沟壑。适合谁来参考不是刚学 JS 的新手也不是要建百人 AI 平台的架构师。它最适合三类人一是正在准备 2026 前端面试、需要手写一个“React Agent” demo 的候选人二是中小团队里那个既要写页面又要搭后端、被要求“三天内跑通本地 LLM 工作流”的全栈工程师三是 Obsidian 插件开发者想把 OpenClaw 的本地能力无缝注入笔记界面。它不承诺高并发、不保证企业级安全、不提供 SaaS 运营后台——它只承诺一件事让你在 12 分钟内从git clone到在浏览器里对着自己笔记本上的 PDF 文件问出“第三页第二段的核心论点是什么”且整个链路全部跑在本机无网络依赖、无 token 限制、无额外云费用。这就是 paperclip 的全部野心也是它被反复搜索却难觅全貌的根本原因它不是一个产品而是一套正在被集体实践、尚未被命名的共识性工作流。2. 整体设计思路为什么放弃 LangChain选择“胶水式”轻编排2.1 拒绝抽象层套娃LangChain 的“过度设计”陷阱去年 Q3 我接手一个客户项目需求很朴素给销售团队做一个本地 PDF 分析工具上传合同扫描件自动提取甲方义务条款、违约金计算逻辑、争议解决方式。团队第一反应是上 LangChain ——毕竟教程满天飞“RAG LLM VectorDB” 听起来就很专业。我们真这么干了装 ChromaDB、配 embedding model、写 prompt template、搭 FastAPI 接口、再用 React 做前端。结果呢光是让 ChromaDB 在 macOS M1 上编译成功就花了两天第一次跑通 RAG 流程时单次查询耗时 8.3 秒其中 6.2 秒花在向本地 Ollama 发送请求并等待响应更糟的是当用户上传一份 50 页的 PDFembedding 步骤直接吃光 16GB 内存Node.js 进程 OOM。这不是模型不行是整套抽象层在本地场景下成了负累。LangChain 的设计哲学是“面向云原生、服务化部署”它预设你的向量库在 AWSLLM 在 AzureAPI 网关有 WAF 防护。但 paperclip 的起点截然相反所有组件必须能npm run dev一键启动所有数据流必须走 localhost:3000 → localhost:3001 → localhost:3002 这样的直连管道且任意环节崩溃都不该导致整个流程不可恢复。它不要“可插拔的 chain”只要“可替换的 endpoint”。OpenClaw 是 endpointClaude Code 的本地 HTTP server 是 endpoint甚至你用 Python 写个flask脚本处理 OCR 也是 endpoint。paperclip 的 Node.js 层不碰任何业务逻辑只做三件事路由分发、流式透传、状态广播。这就像老式电话交换机——不理解通话内容只确保 A 的语音信号准确送到 B 的听筒。2.2 为什么选 OpenClaw 而非 Ollama 或 LM StudioOpenClaw 在中文开发者中热度飙升不是因为它有多先进而是它精准踩中了“本地 LLM 工具链的最后一块拼图”它把模型加载、推理、HTTP API 暴露、GPU 显存管理打包成一个开箱即用的二进制且对 Windows/macOS/Linux 全平台提供一键安装包.deb,.rpm,.exe,.dmg。对比 Ollama你需要先curl -fsSL https://ollama.com/install.sh | sh再ollama pull llama3再ollama run llama3最后还得自己写curl http://localhost:11434/api/chat的请求体LM Studio 更麻烦GUI 界面好看但 API 文档稀烂/v1/chat/completions的 request body 格式和 OpenAI 不完全兼容React 端改一行代码就要查半小时文档。OpenClaw 的/v1/chat/completions完全遵循 OpenAI 标准这意味着你写在 React 里的fetch(http://localhost:3001/v1/chat/completions, { method: POST, body: JSON.stringify({ model: qwen2, messages: [...] }) })明天换成 Claude Code 的本地服务只需改一个 URL其余代码零修改。paperclip 的 Node.js 层正是基于这个“协议一致性”构建的——它不关心背后是 Qwen、DeepSeek 还是 Claude只认/v1/chat/completions这个路径和标准 JSON Schema。这种设计让技术选型变得极其轻量今天用 OpenClaw 跑 qwen2:7b明天换 Claude Code 跑 claude-3-haiku后天切到本地部署的 DeepSeek-CoderNode.js 层配置文件只需改两行// config.ts export const AI_PROVIDER openclaw; // or claude-code, deepseek-local export const AI_ENDPOINT http://localhost:3001/v1/chat/completions;没有 SDK没有适配器没有中间转换层。这就是 paperclip 的“胶水”本质它不创造新协议只复用最广泛接受的协议并确保所有参与方都严格遵守。2.3 React 为何必须承担“状态中枢”角色很多团队尝试把 AI 状态管理全扔给后端Node.js 保存 session ID记录用户历史维护 conversation tree。这在 Web 应用里看似合理但一落地就崩。举个真实案例某教育 SaaS 用 Express Socket.IO 实现“AI 讲解课件”功能老师点击 PPT 下一页后端触发 LLM 生成讲解词再推送给前端。结果上课时网络抖动一次Socket 断连老师点下一页后端没收到指令但前端已翻页——师生看到的讲解词和当前页面完全错位。问题根源在于状态源头错了。PPT 页面索引、当前聚焦的文本框、用户刚输入的问题这些信息天然存在于 React 组件的 state 或 context 中强行让后端持有并同步等于在分布式系统里搞强一致性成本远高于收益。paperclip 的设计反其道而行之React 是唯一真相源Single Source of Truth。Node.js 层只做“请求代理”和“流式中继”不存储任何会话状态。当用户在 React 界面输入问题组件立即 dispatch 一个AI_REQUEST_STARTactionUI 进入 loading 状态同时发起 fetch 请求到http://localhost:3000/api/ai/chatNode.js 收到后不做任何加工直接fetch(AI_ENDPOINT)并用res.body.pipe(res)将原始流透传回前端React 端用ReadableStream接收 chunk逐段解析 JSON Lines实时更新useReducer管理的 message list。整个过程状态变更完全由 React 驱动Node.js 只是透明管道。这样做的好处是即使 Node.js 进程崩溃重启用户在前端输入的内容、已显示的对话历史、当前滚动位置全部毫发无损——因为它们根本不在后端。2.4 Node.js 的不可替代性为什么不能纯前端直连有人会问既然 OpenClaw 和 Claude Code 都暴露 HTTP APIReact 为什么不能直接fetch(http://localhost:3001/...)答案是浏览器同源策略CORS和流式响应支持度。OpenClaw 默认开启 CORS但仅限*这在开发环境 OK生产环境必须配具体域名Claude Code 的本地服务默认关闭 CORS需手动加--cors参数更致命的是SSEServer-Sent Events在现代浏览器支持良好但fetch的ReadableStream解析 JSON Lines 需要手动处理 chunk 边界而EventSource对非标准格式如 OpenAI-style streaming兼容性差。Node.js 层在这里扮演了“协议翻译器”角色它用node-fetch或axios调用后端 AI 服务不受 CORS 限制它将 OpenAI 标准的data: {id:...,choices:[{delta:{content:a}}]}流清洗为纯 JSON Lines去掉data:前缀合并多行 JSON它添加自定义 HTTP header如X-Paperclip-Version: 0.3.2便于前端识别服务版本它实现超时控制AbortController、错误重试指数退避、请求队列防并发打爆 GPU 显存。这些功能若全塞进 React会让组件逻辑臃肿不堪。Node.js 作为轻量胶水层恰好卡在“足够薄”和“足够用”之间——它不处理业务只确保管道畅通、信号干净、故障可控。3. 核心细节解析从零搭建 paperclip 的四步实操法3.1 环境准备避开 Node.js 版本陷阱的实战清单paperclip 对 Node.js 版本有明确要求必须 18.18.0推荐 18.20.4 LTS 或 20.11.1 LTS。为什么两个关键原因一是stream/webAPIReadableStream,TransformStream在 18.18 才稳定支持这是流式透传的基石二是fetch全局函数在 18.0.0 引入但早期版本存在内存泄漏18.18.0 是首个修复版。我见过太多人卡在这一步用nvm install 16.20.2跑起来发现fetch is not defined或用nvm install 22.12.0结果 OpenClaw 的某些 native binding如 CUDA 加速报Module did not self-register错误。正确操作流程如下以 macOS 为例Windows/Linux 同理仅命令微调卸载旧版 Node.js提示不要用brew uninstall node它可能残留node_modules和全局 bin。执行which node和which npm删除所有输出路径然后rm -rf ~/.nvm如果用 nvm或/usr/local/bin/node*如果用 pkg 安装。安装 nvm 并指定版本curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 重新打开终端执行 nvm install 18.20.4 nvm use 18.20.4 node -v # 必须输出 v18.20.4验证 OpenClaw 和 Claude Code 的端口冲突OpenClaw 默认监听http://localhost:3001Claude Code 默认http://localhost:3002。用lsof -i :3001检查端口是否空闲。若被占用修改 OpenClaw 启动参数openclaw --port 3003Claude Code 启动时加--port 3004。paperclip 的 Node.js 层会读取config.ts中的AI_ENDPOINT无需硬编码。初始化项目结构mkdir paperclip-demo cd paperclip-demo npm init -y npm install express cors helmet morgan npm install --save-dev typescript types/express types/cors npx tsc --init # 生成 tsconfig.json关键配置 # target: ES2020, module: CommonJS, lib: [ES2020, DOM], strict: true, skipLibCheck: true注意不要装types/nodepaperclip 的 Node.js 层运行在服务端globalThis下没有window但fetch是全局可用的Node.js 18 内置。装types/node会导致类型冲突fetch类型被覆盖为any。3.2 OpenClaw 本地部署Ubuntu/CentOS/Windows 三平台实操要点OpenClaw 的安装看似简单但不同平台的坑差异极大。以下是我在 12 台不同配置机器上踩坑后总结的“零失败”指南Ubuntu 22.04 / 24.04推荐# 下载最新 .deb 包截至 2024-10v0.8.3 wget https://github.com/openclaw/openclaw/releases/download/v0.8.3/openclaw_0.8.3_amd64.deb sudo dpkg -i openclaw_0.8.3_amd64.deb # 若提示依赖缺失执行 sudo apt-get install -f # 启动服务后台运行日志自动写入 /var/log/openclaw/ sudo systemctl start openclaw sudo systemctl enable openclaw # 验证 curl http://localhost:3001/health # 返回 {status:ok}CentOS 7.9老旧但常见CentOS 7 默认 glibc 版本过低2.17OpenClaw 二进制要求 2.28。强行安装会报GLIBC_2.28 not found。解决方案升级系统不推荐风险高使用 Docker推荐sudo yum install -y docker sudo systemctl start docker sudo docker run -d --name openclaw -p 3001:3001 -v /path/to/models:/app/models openclaw/openclaw:latest注意-v参数必须映射模型目录否则启动失败。模型下载地址见 OpenClaw 官网 Model Zoo。WindowsWSL2 用户注意OpenClaw Windows 版本要求启用“虚拟机平台Virtual Machine Platform”。若安装时报错Claudes workspace requires the virtual machine platform on windows. enable请按此顺序操作以管理员身份运行 PowerShell执行dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行wsl --update重启电脑在 WSL2 中安装 Ubuntu再按 Ubuntu 步骤部署。关键经验不要在 Windows 原生 cmd/powershell 中运行 OpenClaw性能极差且 GPU 加速失效。WSL2 NVIDIA Container Toolkit 是唯一可行路径。所有平台通用验证法# 测试流式响应必须看到逐字返回而非整块 JSON curl -N http://localhost:3001/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2:7b, messages: [{role: user, content: 你好}], stream: true } # 正常输出应类似 # data: {id:chatcmpl-...,choices:[{delta:{content:你},index:0,finish_reason:null}]} # data: {id:chatcmpl-...,choices:[{delta:{content:好},index:0,finish_reason:null}]} # ...3.3 React 前端手写一个可复用的 AI Chat Hookpaperclip 的 React 层核心是一个自定义 HookuseAIChat。它封装了请求发起、流式解析、错误处理、取消机制让业务组件只需关注 UI 渲染。以下是经过 3 个项目验证的精简版TypeScript// hooks/useAIChat.ts import { useState, useCallback, useRef, useEffect } from react; interface Message { id: string; role: user | assistant | system; content: string; } interface AIChatOptions { endpoint?: string; // paperclip 的 /api/ai/chat model?: string; // 传递给后端的模型名 } export function useAIChat({ endpoint /api/ai/chat, model qwen2:7b }: AIChatOptions {}) { const [messages, setMessages] useStateMessage[]([]); const [isLoading, setIsLoading] useState(false); const [error, setError] useStatestring | null(null); const abortControllerRef useRefAbortController | null(null); const sendMessage useCallback(async (userMessage: string) { if (!userMessage.trim()) return; // 添加用户消息 const newUserMsg: Message { id: Date.now().toString(), role: user, content: userMessage }; setMessages(prev [...prev, newUserMsg]); setIsLoading(true); setError(null); // 创建 AbortController 用于取消请求 abortControllerRef.current new AbortController(); try { const response await fetch(endpoint, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ messages: [...messages, newUserMsg], model, stream: true, }), signal: abortControllerRef.current.signal, }); if (!response.ok) { throw new Error(HTTP ${response.status}: ${response.statusText}); } // 处理流式响应 const reader response.body?.getReader(); if (!reader) throw new Error(Response body is not readable); let accumulatedContent ; while (true) { const { done, value } await reader.read(); if (done) break; // 将 Uint8Array 转为字符串 const chunk new TextDecoder().decode(value); // 按行分割OpenAI 标准流每行是 data: {...} const lines chunk.split(\n).filter(line line.trim() ! ); for (const line of lines) { if (line.startsWith(data: )) { try { const jsonStr line.slice(6).trim(); if (jsonStr [DONE]) continue; const data JSON.parse(jsonStr); const deltaContent data.choices?.[0]?.delta?.content || ; accumulatedContent deltaContent; // 实时更新 assistant 消息 setMessages(prev { const lastMsg prev[prev.length - 1]; if (lastMsg?.role assistant) { return [...prev.slice(0, -1), { ...lastMsg, content: accumulatedContent }]; } else { return [...prev, { id: Date.now().toString(), role: assistant, content: accumulatedContent }]; } }); } catch (e) { console.warn(Failed to parse stream line:, line, e); } } } } } catch (err) { if (err instanceof DOMException err.name AbortError) { console.log(Request aborted); } else { setError(err instanceof Error ? err.message : Unknown error); } } finally { setIsLoading(false); abortControllerRef.current null; } }, [endpoint, model, messages]); const abortRequest useCallback(() { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current null; } }, []); // 组件卸载时自动取消请求 useEffect(() { return () { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); return { messages, isLoading, error, sendMessage, abortRequest, }; }关键细节说明TextDecoder().decode(value)是处理Uint8Array的标准方式避免Buffer.from(value).toString()在某些 Node.js 版本下的乱码lines.filter(line line.trim() ! )去除空行防止JSON.parse()报错data.choices?.[0]?.delta?.content使用可选链兼容不同模型返回格式Qwen 返回delta.contentClaude 可能返回delta.textsetMessages的更新逻辑确保 assistant 消息始终是最后一条且内容实时追加而非覆盖——这是流式体验的核心。3.4 Node.js 中间层300 行代码实现的流式代理服务器paperclip 的 Node.js 层是整个架构的“心脏”但它异常简洁。以下是server/index.ts的完整实现已删减日志和错误处理保留核心逻辑import express from express; import cors from cors; import helmet from helmet; import morgan from morgan; import { Readable, Transform } from stream; import { pipeline } from stream/promises; import { fetch } from undici; // Node.js 18 内置 fetch 有 bug用 undici 更稳 const app express(); const PORT 3000; // 中间件 app.use(helmet()); app.use(cors({ origin: * })); // 开发环境允许所有来源 app.use(morgan(dev)); app.use(express.json({ limit: 10mb })); app.use(express.urlencoded({ extended: true })); // 配置 const AI_ENDPOINT process.env.AI_ENDPOINT || http://localhost:3001/v1/chat/completions; const TIMEOUT_MS 30000; // 流式代理核心 app.post(/api/ai/chat, async (req, res) { const { messages, model, stream true } req.body; try { // 构造 AI 服务请求 const aiReqBody JSON.stringify({ messages, model, stream, }); const aiResponse await fetch(AI_ENDPOINT, { method: POST, headers: { Content-Type: application/json, Accept: text/event-stream, // 明确告知后端要流式响应 }, body: aiReqBody, // 超时控制 dispatcher: new undici.TimeoutInterceptor({ timeout: TIMEOUT_MS }), }); if (!aiResponse.ok) { throw new Error(AI service error: ${aiResponse.status} ${aiResponse.statusText}); } // 设置响应头启用流式传输 res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no, // Nginx 关键配置 }); // 创建 Transform Stream 清洗数据 const cleanStream new Transform({ transform(chunk, encoding, callback) { try { const str chunk.toString(); // 移除 data: 前缀保留纯 JSON const cleaned str .split(\n) .map(line line.trim()) .filter(line line.startsWith(data: ) line ! data: [DONE]) .map(line line.slice(6).trim()) // 去掉 data: .join(\n); callback(null, cleaned ? cleaned \n : ); } catch (err) { callback(err); } } }); // 管道AI 响应流 → 清洗流 → HTTP 响应流 await pipeline( aiResponse.body!, cleanStream, res ); } catch (err) { console.error(Proxy error:, err); res.status(500).json({ error: err instanceof Error ? err.message : Internal server error }); } }); // 健康检查 app.get(/health, (req, res) { res.json({ status: ok, timestamp: new Date().toISOString() }); }); app.listen(PORT, () { console.log(Paperclip server running on http://localhost:${PORT}); });为什么用undici而不用内置fetchNode.js 18.18 的global.fetch在流式响应场景下存在内存泄漏尤其当请求被频繁取消时。undici是 Node.js 官方推荐的高性能 HTTP client其TimeoutInterceptor可精确控制请求超时且pipeline函数能优雅处理流中断。实测对比相同压力下undici内存占用稳定在 80MB内置fetch30 分钟后涨至 1.2GB。X-Accel-Buffering: no的作用当 paperclip 部署在 Nginx 后时Nginx 默认会缓冲响应直到整个 body 发送完毕才推给客户端这会彻底破坏流式体验。此 header 强制 Nginx 禁用缓冲逐 chunk 转发。4. 实操全流程从空白目录到可交互 AI 界面的 12 分钟4.1 第 1-3 分钟初始化与依赖安装打开终端执行以下命令假设已按 3.1 节准备好 Node.js 18.20.4mkdir my-paperclip cd my-paperclip npm init -y npm install express cors helmet morgan undici npm install --save-dev typescript types/express types/cors npx tsc --init # 修改 tsconfig.json确保 module: CommonJS, target: ES2020创建目录结构my-paperclip/ ├── server/ │ ├── index.ts │ └── config.ts ├── client/ │ ├── src/ │ │ ├── hooks/ │ │ │ └── useAIChat.ts │ │ ├── App.tsx │ │ └── main.tsx │ └── public/ │ └── index.html ├── package.json └── tsconfig.json4.2 第 4-6 分钟编写 Node.js 代理服务器server/config.tsexport const AI_ENDPOINT http://localhost:3001/v1/chat/completions; export const PORT 3000;server/index.ts粘贴 3.4 节的完整代码含undici导入和pipeline调用。package.json添加脚本scripts: { server: ts-node --esm server/index.ts, client: vite, dev: concurrently \npm run server\ \npm run client\ }安装concurrently和ts-nodenpm install --save-dev concurrently ts-node types/node4.3 第 7-9 分钟搭建 React 前端基础client/src/main.tsximport React from react; import ReactDOM from react-dom/client; import App from ./App; ReactDOM.createRoot(document.getElementById(root)!).render( React.StrictMode App / /React.StrictMode, );client/src/App.tsximport { useState } from react; import { useAIChat } from ./hooks/useAIChat; function App() { const [inputValue, setInputValue] useState(); const { messages, isLoading, error, sendMessage, abortRequest } useAIChat(); const handleSubmit (e: React.FormEvent) { e.preventDefault(); if (inputValue.trim()) { sendMessage(inputValue); setInputValue(); } }; return ( div style{{ padding: 20px, fontFamily: system-ui }} h1Paperclip AI Demo/h1 form onSubmit{handleSubmit} input typetext value{inputValue} onChange{(e) setInputValue(e.target.value)} placeholder输入问题... disabled{isLoading} style{{ width: 500px, padding: 10px, marginRight: 10px }} / button typesubmit disabled{isLoading} {isLoading ? 思考中... : 发送} /button {isLoading button typebutton onClick{abortRequest}取消/button} /form {error div style{{ color: red, marginTop: 10px }}错误{error}/div} div style{{ marginTop: 20px, maxHeight: 400px, overflowY: auto }} {messages.map((msg) ( div key{msg.id} style{{ marginBottom: 10px, fontWeight: msg.role user ? bold : normal }} strong{msg.role user ? 你 : AI}/strong {msg.content} /div ))} /div /div ); } export default App;4.4 第 10-12 分钟启动服务与首次交互启动 OpenClaw确保端口 3001 空闲openclaw --port 3001 --models-dir ./models首次运行会自动下载 qwen2:7b约 4.2GB耐心等待启动 paperclip在my-paperclip/目录下执行npm run dev终端应显示Paperclip server running on http://localhost:3000Vite server running on http://localhost:5173打开浏览器http://localhost:5173在输入框输入你好点击发送。你会看到UI 立即显示“你你好”几秒后“AI你好很高兴见到你。” 逐字出现打开浏览器开发者工具 Network 标签找到/api/ai/chat请求Preview 中能看到纯 JSON Lines 流。实测心得首次交互延迟主要来自 OpenClaw 加载模型约 8-12 秒后续请求均在 1.2 秒内返回首字节。若想加速可在openclaw启动时加--num-gpu 1Linux/macOS或--gpu-layers 20Windows WSL2显式启用 GPU 加速。5. 常见问题与排查技巧实录那些官方文档不会写的坑5.1 “React 页面白屏控制台报错ReferenceError: fetch is not defined”现象Vite 启动后页面空白浏览器控制台报错ReferenceError: fetch is not defined。根因Vite 默认在 SSR服务端渲染模式下运行而fetch是浏览器全局对象Node.js 环境下不存在。但你的useAIChatHook 在组件初始化时就调用了fetchSSR 时执行失败。解决方案强制禁用 SSR在vite.config.ts中添加export default defineConfig({ ssr: false, // 关键 // 其他配置... });经验paperclip 是纯客户端交互应用无需 SSR。开启 SSR 只会引入更多兼容性问题如window is not defined。5.2 “OpenClaw 启动报错CUDA out of memory但 GPU 显存明明充足”现象openclaw --gpu-layers 35启动失败日志显示CUDA out of memorynvidia-smi查看显存使用率仅 40%。根因OpenClaw 默认为每个模型实例分配固定显存池未考虑多任务共享。当系统有其他进程如 Chrome、VSCode占用显存时OpenClaw 申请失败。