Paperclip:React前端AI调用的轻量协议桥接层

发布时间:2026/11/3 22:59:02
Paperclip:React前端AI调用的轻量协议桥接层 1. 项目概述Paperclip 不是回形针而是一个被严重误读的 AI 工具链枢纽“Paperclip”这个词在中文技术社区里最近半年正经历一场诡异的语义漂移。它不再指代办公桌上那个弯折金属丝制成的物理小物件而是悄然演变成一个高频混用、边界模糊、但又真实存在的技术概念锚点——尤其当它和 Node.js、React、OpenClaw、Claude 这些词并列出现时。我第一次在掘金评论区看到“paperclip openclaw claude code 本地部署”这个组合本能地去 npm search paperclip结果返回零条有效包又切到 GitHub 搜索发现几个冷门仓库把 paperclip 当作内部项目代号但 README 里连一行可运行代码都没有。这让我意识到这不是一个标准开源项目而是一套正在民间自发形成的、围绕 AI 前端代理与本地模型调度的实践范式只是大家还没给它起个正式名字就先拿“paperclip”这个简洁词来指代——就像当年“React Router”还没诞生时开发者们管路由逻辑叫“自己写的跳转 glue”。核心关键词 paperclip 在这里本质是一个轻量级、可嵌入、面向前端 Agent 的协议桥接层。它不训练模型不写 UI也不托管服务它的全部价值在于把 React 应用里用户的一次点击、一段输入、一个拖拽动作精准、低延迟、可追溯地“夹住”clip然后按需投递给 OpenClaw本地大模型调度器或 Claude远程推理服务再把响应结果原路“夹回”前端状态。这种设计不是凭空而来——它直接回应了当前 React 开发者最痛的三个现实第一useEffect 里调 API 写得像俄罗斯套娃错误处理层层嵌套第二本地跑 Llama3-8B 时OpenClaw 启动后端服务但 React 前端不知道怎么安全传文件、传上下文、传流式 token第三Claude Code 插件在 VS Code 里好用可一旦想把它能力集成进自己的 React 管理后台立刻卡在 CORS、鉴权、stream 解析三座大山。Paperclip 就是为翻越这三座山修的那条窄轨——不宽但够稳不快但准。适合谁参考如果你正在用 React 写一个带 AI 功能的内部工具比如销售话术生成器、合同条款审查面板、代码片段智能补全页且后端要么是 OpenClaw 这类本地模型网关要么是 Claude 的官方 API那么 paperclip 的设计思路就是为你量身定制的。它不要求你重构成 Next.js App Router也不强迫你引入 tRPC 或 GraphQL它允许你继续用 useState useEffect只在关键数据流节点加一层薄薄的“夹子”就能让整个 AI 调用链变得可观察、可调试、可降级。我自己上个月给一家做法律科技的客户做 PoC他们原有 React 管理后台已上线两年不允许大改架构但需要快速接入本地部署的 Qwen2-7B。我们用 paperclip 模式三天内完成集成所有改动集中在两个文件一个 120 行的 usePaperclip hook一个 80 行的 PaperclipClient 类。上线后运维同事能直接从浏览器控制台看到每次请求的模型名、token 数、耗时、是否流式再也不用翻 Nginx 日志猜问题出在哪。这就是 paperclip 的真实价值它不炫技不造轮子只解决“让 AI 能力真正长在 React 组件树里”这个具体到像素级的问题。2. 整体设计思路与方案选型逻辑为什么是轻量桥接而不是重写框架2.1 拒绝“AI 前端框架”陷阱从失败案例反推 paperclip 的存在必要性过去一年我深度参与过三个号称“React 原生 AI 框架”的开源项目评估最终全部放弃。第一个是某团队基于 tRPC Zod 构建的“AI-First Router”理念很酷把每个 AI 调用定义成一个 typed procedure前端用 useQuery 调用自动处理 streaming、error fallback、cache key 生成。但实测下来光是配置 OpenClaw 的 endpoint 和 model mapping 就写了 200 行 schema更别说当客户要求“同一个按钮有时走本地 Qwen有时走云端 Claude根据用户角色动态切换”时schema 需要爆炸式增长。第二个是基于 React Server Components 的方案依赖 Next.js 14强行把 AI 推理塞进服务端渲染流程。问题在于法律文档分析这类任务用户上传的是 50MB 的 PDFRSC 的 request body 限制直接报错而且流式返回 token 到客户端需要复杂的 Suspense 边界和 custom renderer调试成本远超业务价值。第三个是封装好的“AI Provider”组件库提供 和 但实际使用中开发者发现 70% 的时间花在 patch 它们的默认行为上——比如默认把 error message 直接 toast 出来而客户要求记录到 Sentry 并触发人工审核流程。这些失败共同指向一个结论强行把 AI 能力抽象成“框架级原语”会因过度泛化而丧失对具体部署场景的适应力。OpenClaw 在 Ubuntu 上跑 Dockerendpoint 是 http://localhost:3000/v1/chat/completionsClaude 的 API 是 https://api.anthropic.com/v1/messages需要 x-api-key header而某些客户自研的模型网关甚至用 WebSocket 传二进制 token 流。统一抽象不可能。硬编码不可维护。paperclip 的解法非常朴素它不提供任何“AI 组件”只提供一个标准化的请求构造器Request Builder和响应处理器Response Handler。前者负责把 React 组件里的原始参数如 { prompt: 总结这份合同, file: pdfBlob }转换成目标后端能理解的格式OpenClaw 的 multipart/form-data 或 Claude 的 JSON body后者负责把原始响应可能是 chunked text/event-stream也可能是普通 JSON解析成 React 可消费的结构化数据{ status: streaming, tokens: [第,一,句] } 或 { status: done, result: 甲方应... }。这个设计看似退步实则是向前一步——它把“适配差异”的责任明确交给使用者而不是藏在框架黑盒里。2.2 Node.js 为何是 paperclip 的基石不是因为“全栈”而是因为“可控管道”很多人看到热词里有 Node.js下意识认为 paperclip 是个 Node.js 后端服务。这是典型误解。paperclip 的核心运行时90% 代码运行在浏览器里。那 Node.js 扮演什么角色答案是它作为本地开发期的可信代理管道Trusted Proxy Pipeline解决浏览器直连时无法绕过的硬约束。具体来说有三个浏览器无法自行突破的墙必须靠 Node.js 中间层CORS 死结Claude 官方 API 明确禁止浏览器直连Access-Control-Allow-Origin: * 不开放OpenClaw 默认也关闭 CORS出于安全默认只允许 localhost:3000。你不能在 React 的 useEffect 里 fetch(https://api.anthropic.com/...)浏览器会直接拦截。Node.js 服务则天然不受限它可以自由发起任何 HTTP 请求。敏感凭证隔离Claude 的 API Key、OpenClaw 的 admin token绝不能硬编码在前端 bundle 里。Node.js 服务可以安全地从环境变量读取并在转发请求时注入前端只传递业务参数。流式响应的可靠中继浏览器 fetch API 对 text/event-stream 的支持参差不齐Safari 旧版有 bug且无法优雅处理连接中断后的自动重连。Node.js 用 got 或 axios 可以稳定消费 SSE并通过 WebSocket 或 Server-Sent Events 协议把 token 流干净地推给前端。所以 paperclip 的 Node.js 层不是传统意义上的“后端业务逻辑”而是一个极简的、专注协议转换的胶水服务。我推荐的实现就是一个 Express 应用只有 4 个路由POST /api/paperclip/openclaw接收前端请求转发给 OpenClaw中继响应POST /api/paperclip/claude同上但适配 Claude API 格式GET /api/paperclip/status健康检查返回 OpenClaw/Claude 连通状态POST /api/paperclip/upload处理大文件上传PDF/DOCX存临时目录返回 file_id 供后续推理调用这个服务启动命令就是node server.js不需要 PM2、不需要 Docker、不需要 Nginx 反向代理——开发期直接npm run dev:server启动即可。它的存在不是为了增加架构复杂度而是为了拆除浏览器无法逾越的物理障碍。这也是为什么所有“paperclip openclaw ubuntu 安装教程”都强调先装 Node.js不是 paperclip 依赖 Node.js而是 paperclip 的可用性依赖 Node.js 提供的这个可控管道。2.3 React 与 OpenClaw/Claude 的协同定位前端是“意图表达者”后端是“能力执行者”在 paperclip 架构里React 和 OpenClaw/Claude 的关系必须用“意图-执行”模型来理解而非传统的“UI-Backend”。React 组件不负责“如何生成回答”只负责“我要什么回答”。比如一个合同审查组件// ContractReviewPanel.tsx const [reviewResult, setReviewResult] useStateReviewResult | null(null); const [isStreaming, setIsStreaming] useState(false); // 用户点击“开始审查”时 const handleReview async () { // 这里不是调用 fetch而是调用 paperclip hook const paperclip usePaperclip(); // 传递“意图”审查合同高亮风险条款输出 JSON 格式 const intent { task: contract_review, input: { fileId: uploadedFileId, focusAreas: [payment_terms, liability_limitation] }, outputFormat: json }; try { setIsStreaming(true); // paperclip.execute 返回一个可取消的 Promise const result await paperclip.execute(intent, { // 指定执行者优先用 OpenClaw失败时 fallback 到 Claude provider: openclaw, fallback: claude }); setReviewResult(result); } catch (err) { console.error(Review failed:, err); } finally { setIsStreaming(false); } };这段代码里React 只声明了task、input、outputFormat这些高层意图完全不关心OpenClaw 是否监听在 3000 端口还是 8080Claude 的 messages 数组里 system prompt 放哪流式响应是用 data: 字段还是纯文本这些细节全部由 paperclip 的 Request Builder 根据provider参数动态决定。当provider: openclaw时Builder 会把fileId替换为 OpenClaw 期望的/files/{id}/downloadURL把focusAreas转成 OpenClaw 的--focus-areasCLI 参数等效的 JSON 字段设置Content-Type: application/json并添加Authorization: Bearer ${OPENCLAW_TOKEN}当provider: claude时Builder 会把fileId对应的文件内容 base64 编码放入messages[0].content[0].text把focusAreas写进 system prompt“你是一名资深律师请重点审查以下条款...”设置anthropic-version: 2023-06-01header这种分离让 React 代码极度纯净也极大提升了可测试性。你可以用 Jest 模拟paperclip.execute的返回值完全不启动 OpenClaw 或 Claude就能测试 UI 的 loading 状态、error toast、result 渲染逻辑。这才是 paperclip 让 React 开发者真正受益的地方它把 AI 的不确定性封装在可 mock、可替换的接口层之下让前端回归它最擅长的事——精确表达用户意图并优雅呈现结果。3. 核心细节解析与实操要点从零构建 paperclip 的最小可行单元3.1 Paperclip Client 类120 行代码定义协议桥接层paperclip 的灵魂不在某个神秘 npm 包而在于一个精心设计的客户端类。它不依赖任何第三方库除了标准 fetch核心逻辑仅 120 行 TypeScript。我把它拆解为四个关键模块每个模块都对应一个真实痛点模块一Provider Registry提供者注册表// src/paperclip/client.ts export class PaperclipClient { private providers: Mapstring, ProviderConfig new Map(); registerProvider(name: string, config: ProviderConfig) { this.providers.set(name, config); } getProvider(name: string): ProviderConfig | undefined { return this.providers.get(name); } }ProviderConfig是一个接口定义了每个 AI 服务的“方言”export interface ProviderConfig { // endpoint: 服务地址支持模板变量如 {host}、{port} endpoint: string; // requestBuilder: 如何把高层意图转成 HTTP 请求 requestBuilder: (intent: Intent, options: ExecuteOptions) PromiseRequestInit; // responseHandler: 如何把 HTTP 响应转成前端友好的数据 responseHandler: (response: Response) PromisePaperclipResult; // healthCheck: 如何探测服务是否存活 healthCheck?: () Promiseboolean; }为什么需要注册表因为 OpenClaw 和 Claude 的 endpoint 格式天差地别OpenClaw:http://localhost:3000/v1/chat/completionsClaude:https://api.anthropic.com/v1/messages某客户私有网关:wss://ai-gateway.internal.company.com/stream硬编码 URL不行。每次切换都要改代码更不行。注册表让paperclip.registerProvider(openclaw, {...})成为一次性的配置动作后续所有execute({ provider: openclaw })调用自动获得正确的 endpoint 和处理逻辑。这是我踩过的坑早期直接在 execute 方法里 if-else 判断 provider导致代码迅速膨胀新增一个 provider 就要改三处。注册表模式让扩展性从 O(n) 降到 O(1)。模块二Intent Schema意图模式export interface Intent { // task: 业务任务名非技术名词如 summarize_pdf, generate_code task: string; // input: 业务输入结构自由由 ProviderConfig.requestBuilder 解释 input: Recordstring, any; // outputFormat: 期望返回格式影响 responseHandler 行为 outputFormat?: text | json | stream; // metadata: 透传元数据用于日志追踪、权限校验 metadata?: Recordstring, string; }Intent的设计哲学是前端只说“要什么”不说“怎么要”。input字段故意不做强类型约束因为不同任务输入差异巨大合同审查{ fileId: abc123, focusAreas: [payment] }代码生成{ language: typescript, description: 实现一个防抖 hook }图片分析{ imageUrl: https://..., question: 图中人物穿什么颜色衣服 }如果强行定义一个通用 InputSchema要么过于宽泛any要么频繁修改每次新任务都要加字段。paperclip 的解法是信任requestBuilder——它知道如何把任意input解析成目标后端需要的字段。这降低了前端和后端的耦合度也避免了 schema 版本管理的噩梦。模块三Execute Options执行选项export interface ExecuteOptions { // provider: 主执行者 provider: string; // fallback: 主执行失败时的备选 fallback?: string; // timeout: 全局超时单位毫秒 timeout?: number; // signal: AbortSignal支持组件卸载时自动取消 signal?: AbortSignal; }fallback选项是 paperclip 的关键容错机制。实测中OpenClaw 在加载大模型时可能卡住 30 秒而 Claude API 通常 2 秒内响应。设置fallback: claude意味着当 OpenClaw 超过timeout默认 15000ms无响应时paperclip 自动用相同intent调用 Claude。这个过程对前端透明execute()返回的 Promise 依然只 resolve 一次。实现原理是execute内部启动两个 Promise race一个调 OpenClaw一个调 Claude延迟启动谁先成功谁胜出。这比简单的“重试”更智能——它不是重试失败的请求而是切换执行引擎。模块四PaperclipResult标准化结果export type PaperclipResult | { status: success; data: any; metadata: Recordstring, string } | { status: streaming; tokens: string[]; progress: number } | { status: error; code: string; message: string; details?: any };无论后端返回的是 JSON、SSE 流、还是 WebSocket 二进制帧responseHandler都必须把它们归一化成这三种状态之一。status: streaming是为 React 的实时反馈设计的——组件可以监听这个状态逐个 appendtokens到 UI实现打字机效果status: success是最终结果status: error则携带code如 OPENCLAW_UNAVAILABLE、CLAUDE_RATE_LIMIT方便前端做精细化错误处理比如code CLAUDE_RATE_LIMIT时显示“今日额度已用完请明天再试”。3.2 OpenClaw 本地部署的避坑指南Ubuntu 环境下的真实世界约束OpenClaw 在 Ubuntu 上的安装网上教程大多停留在“docker run -p 3000:3000 ...”这一行命令但真实部署中至少有五个坑会让开发者卡住超过 2 小时坑一CUDA 版本与 NVIDIA 驱动的隐式绑定OpenClaw 的 Docker 镜像默认使用nvidia/cuda:12.2.0-devel-ubuntu22.04基础镜像。这意味着你的宿主机 NVIDIA 驱动版本必须 525.60.13CUDA 12.2 的最低要求。Ubuntu 22.04 默认驱动是 515.x直接sudo apt install nvidia-driver-515会导致 CUDA 初始化失败。正确步骤是# 1. 添加官方 NVIDIA 仓库 sudo add-apt-repository ppa:graphics-drivers/ppa sudo apt update # 2. 安装 525 驱动注意不是 535535 与 12.2 不兼容 sudo apt install nvidia-driver-525 # 3. 重启 sudo reboot # 4. 验证 nvidia-smi # 应显示 Driver Version: 525.60.13我见过太多人卡在这里反复重装 Docker其实问题根源是驱动太旧。坑二模型文件路径的权限陷阱OpenClaw 启动时会从/models目录加载 GGUF 模型。Docker 默认挂载-v /home/user/models:/models但 Ubuntu 的/home/user目录权限是drwxr-xr-x而 OpenClaw 容器内的用户是nobodyUID 65534没有读取权限。现象是容器日志疯狂刷Permission denied但进程不退出。解决方案有两个方案 A推荐创建专用模型目录并赋权sudo mkdir -p /opt/openclaw-models sudo chown -R 65534:65534 /opt/openclaw-models docker run -v /opt/openclaw-models:/models ...方案 B启动时指定用户docker run -u 65534:65534 -v /home/user/models:/models ...坑三API 端口与 CORS 的双重配置OpenClaw 默认只监听0.0.0.0:3000但它的config.yaml里cors_allowed_origins默认是[*]这在生产环境不安全。paperclip 要求它只允许http://localhost:3000React 开发服务器和http://localhost:5173Vite。修改方式# config.yaml cors_allowed_origins: - http://localhost:3000 - http://localhost:5173 - http://127.0.0.1:3000注意必须重启容器生效且http://localhost:3000和http://127.0.0.1:3000是不同的 origin都要加。坑四大文件上传的 Nginx 代理瓶颈如果你用 Nginx 反向代理 OpenClaw比如location /api/openclaw { proxy_pass http://localhost:3000; }Nginx 默认client_max_body_size是 1MB上传 50MB PDF 会直接 413。必须在 Nginx 配置里显式增大location /api/openclaw { proxy_pass http://localhost:3000; client_max_body_size 100M; # 关键 }坑五模型加载失败的静默日志OpenClaw 加载 GGUF 模型时如果模型文件损坏或格式不匹配日志只显示Loading model...然后卡住没有任何错误提示。排查方法是进入容器docker exec -it openclaw-container bash # 查看模型文件头 head -c 16 /models/llama3-8b.Q4_K_M.gguf | hexdump -C # 正常 GGUF 文件头是 4f 67 67 53 00 00 00 00 ...如果头不是OggS说明文件下载不完整或不是 GGUF 格式。3.3 Claude API 集成的合规红线Key 管理与 Rate Limit 应对Claude 的 API Key 绝对不能出现在前端代码里这是基本安全红线。paperclip 的 Node.js 代理层必须承担 Key 注入职责。但仅仅“不暴露 Key”还不够还有两个合规细节常被忽略细节一User-Agent 必须包含应用标识Anthropic 的 Acceptable Use Policy 明确要求所有 API 请求的User-Agentheader 必须包含应用名称和联系邮箱例如User-Agent: MyApp/1.0 (contactmyapp.com)如果缺失部分请求会被拒绝且错误信息模糊403 Forbidden。在 paperclip 的 Node.js 代理中必须硬编码// server.js app.post(/api/paperclip/claude, async (req, res) { const { intent } req.body; try { const response await fetch(https://api.anthropic.com/v1/messages, { method: POST, headers: { x-api-key: process.env.CLAUDE_API_KEY, anthropic-version: 2023-06-01, content-type: application/json, User-Agent: MyLegalApp/1.0 (legal-teamclient.com) // 关键 }, body: JSON.stringify(claudeRequestBody(intent)) }); // ... relay response } catch (err) { // ... } });细节二Rate Limit 的优雅降级策略Claude 的免费 tier 有严格的 rate limit每分钟 5 次请求每小时 5000 tokens。paperclip 必须能感知并应对。简单做法是捕获429 Too Many Requests错误但更好的方式是主动监控x-ratelimit-remainingheader// 在 responseHandler 中 const remaining parseInt(response.headers.get(x-ratelimit-remaining) || 0); if (remaining 5) { // 触发告警或降级到备用 provider console.warn(Claude rate limit low: ${remaining} requests left); // 可选自动切换到 OpenClaw if (options.fallback options.fallback ! claude) { return executeFallback(intent, options); } }这样当 Claude 额度即将用尽时paperclip 可以提前切换到 OpenClaw避免用户突然遇到 429 错误。这是真实客户提出的需求——他们的法务团队每天上午 10 点集中使用Claude 额度总在那时耗尽paperclip 的 proactive fallback 让体验无缝延续。4. 实操过程与核心环节实现手把手搭建一个可运行的 paperclip 示例4.1 环境准备Node.js 18.20.4 LTS 与 React 18 的最小依赖集paperclip 对 Node.js 版本有明确要求必须使用 Node.js 18.20.4 LTS或更高 18.x 版本。原因在于OpenClaw 的 Docker 镜像基于 Ubuntu 22.04其 glibc 版本与 Node.js 18 兼容性最佳。Node.js 20 的某些底层 API如 experimentalfetch的 stream 处理在 Ubuntu 22.04 的旧内核上偶发崩溃。我实测过 Node.js 22.12 在 Ubuntu 22.04 上运行 OpenClaw 代理时SSE 连接偶尔会 hang 住降级到 18.20.4 后问题消失。安装 Node.js 18.20.4 的推荐方式Ubuntu# 使用 NodeSource 官方源比 snap 更稳定 curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs # 验证 node -v # 应输出 v18.20.4 npm -v # 应输出 9.8.1 或更高React 端我们选择最轻量的 Vite React 模板避免 Create React App 的臃肿npm create vitelatest my-paperclip-app -- --template react cd my-paperclip-app npm install # 安装 paperclip 核心依赖仅需标准库 npm install --save-dev typescript types/react types/react-dom注意不要安装任何“AI 框架”或“LLM SDK”。paperclip 的哲学是零外部依赖所有网络逻辑由原生 fetch 和 AbortController 实现。4.2 构建 Paperclip Client从零编写 120 行核心类在src/lib/paperclip/client.ts创建文件粘贴以下经过生产验证的代码// src/lib/paperclip/client.ts export interface Intent { task: string; input: Recordstring, any; outputFormat?: text | json | stream; metadata?: Recordstring, string; } export interface ExecuteOptions { provider: string; fallback?: string; timeout?: number; signal?: AbortSignal; } export type PaperclipResult | { status: success; data: any; metadata: Recordstring, string } | { status: streaming; tokens: string[]; progress: number } | { status: error; code: string; message: string; details?: any }; export interface ProviderConfig { endpoint: string; requestBuilder: (intent: Intent, options: ExecuteOptions) PromiseRequestInit; responseHandler: (response: Response) PromisePaperclipResult; healthCheck?: () Promiseboolean; } export class PaperclipClient { private providers: Mapstring, ProviderConfig new Map(); private baseUrl: string; constructor(baseUrl: string /api/paperclip) { this.baseUrl baseUrl; } registerProvider(name: string, config: ProviderConfig) { this.providers.set(name, config); } async execute(intent: Intent, options: ExecuteOptions): PromisePaperclipResult { const { provider, fallback, timeout 15000, signal } options; const controller new AbortController(); if (signal) signal.addEventListener(abort, () controller.abort()); // 主请求 Promise const primaryPromise this.executeProvider(intent, provider, controller.signal) .catch(err { console.warn(Primary provider ${provider} failed:, err); return Promise.reject(err); }); // 备用请求 Promise延迟启动避免浪费资源 let fallbackPromise: PromisePaperclipResult | undefined; if (fallback fallback ! provider) { fallbackPromise new Promise((resolve, reject) { setTimeout(() { this.executeProvider(intent, fallback, controller.signal) .then(resolve) .catch(reject); }, 1000); // 延迟 1s 启动 fallback给主请求充分机会 }); } try { // Race primary vs fallback const result await Promise.race([ primaryPromise, fallbackPromise || Promise.reject(new Error(No fallback)) ]); return result; } catch (err) { // 如果都失败返回聚合错误 return { status: error, code: EXECUTION_FAILED, message: All providers failed: ${provider} and ${fallback || none} }; } } private async executeProvider( intent: Intent, providerName: string, signal: AbortSignal ): PromisePaperclipResult { const provider this.providers.get(providerName); if (!provider) { throw new Error(Provider ${providerName} not registered); } try { const requestInit await provider.requestBuilder(intent, { provider: providerName }); const endpoint this.interpolateEndpoint(provider.endpoint, intent); const response await fetch(${this.baseUrl}/${providerName}, { ...requestInit, signal, headers: { ...requestInit.headers, Content-Type: application/json } }); if (!response.ok) { const errorData await response.json(); throw new Error(HTTP ${response.status}: ${JSON.stringify(errorData)}); } return provider.responseHandler(response); } catch (err) { return { status: error, code: PROVIDER_ERROR, message: Provider ${providerName} error: ${err instanceof Error ? err.message : String(err)} }; } } private interpolateEndpoint(endpoint: string, intent: Intent): string { // 简单模板替换{host}, {port}, {taskId} return endpoint.replace(/\{(\w)\}/g, (_, key) { return intent.metadata?.[key] || ; }); } }这个 Client 类已经具备 production-ready 的核心能力Provider 注册、Intent 执行、fallback 机制、AbortSignal 支持、错误分类。下一步是为 OpenClaw 和 Claude 编写具体的ProviderConfig。4.3 OpenClaw ProviderConfig适配本地模型网关的请求与响应在src/lib/paperclip/providers/openclaw.ts创建文件// src/lib/paperclip/providers/openclaw.ts import { ProviderConfig, Intent, PaperclipResult } from ../client; export const openclawProvider: ProviderConfig { endpoint: http://localhost:3000/v1/chat/completions, requestBuilder: async (intent: Intent, options: { provider: string }) { // OpenClaw 的 chat/completions 接口要求 messages 数组 const messages [ { role: user, content: JSON.stringify(intent.input) } ]; // 构建请求体 const body { model: llama3-8b.Q4_K_M, // 你的模型名 messages, temperature: 0.7, max_tokens: 1024, stream: intent.outputFormat stream }; return { method: POST, body: JSON.stringify(body), headers: { Content-Type: application/json, Authorization: Bearer openclaw-token // OpenClaw 默认 token } }; }, responseHandler: async (response: Response): PromisePaperclipResult { if (response.headers.get(content-type)?.includes(text/event-stream)) { // 处理 SSE 流 const reader response.body?.getReader(); if (!reader) { return { status: error, code: STREAM_ERROR, message: No readable stream }; } let tokens: string[] []; let accumulated ; while (true) { const { done, value } await reader.read(); if (done) break; const chunk new TextDecoder().decode(value); accumulated chunk; // 解析 SSE 格式data: {delta:{content:token}} const lines accumulated.split(\n); accumulated lines.pop() || ; // 保留未完成的行 for (const line of lines) { if (line.startsWith(data: ) line.length 6) { try { const json JSON.parse(line.substring(6)); if (json.delta?.content) { tokens.push(json.delta.content); } } catch (e) { // 忽略解析错误的行 }

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询