news 2026/11/3 22:58:49

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 的 PDF,RSC 的 request body 限制直接报错;而且流式返回 token 到客户端,需要复杂的 Suspense 边界和 custom renderer,调试成本远超业务价值。第三个是封装好的“AI Provider”组件库,提供 和 ,但实际使用中,开发者发现 70% 的时间花在 patch 它们的默认行为上——比如默认把 error message 直接 toast 出来,而客户要求记录到 Sentry 并触发人工审核流程。

这些失败共同指向一个结论:强行把 AI 能力抽象成“框架级原语”,会因过度泛化而丧失对具体部署场景的适应力。OpenClaw 在 Ubuntu 上跑 Docker,endpoint 是 http://localhost:3000/v1/chat/completions;Claude 的 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 中间层:

  1. CORS 死结:Claude 官方 API 明确禁止浏览器直连(Access-Control-Allow-Origin: * 不开放),OpenClaw 默认也关闭 CORS(出于安全,默认只允许 localhost:3000)。你不能在 React 的 useEffect 里 fetch('https://api.anthropic.com/...'),浏览器会直接拦截。Node.js 服务则天然不受限,它可以自由发起任何 HTTP 请求。

  2. 敏感凭证隔离:Claude 的 API Key、OpenClaw 的 admin token,绝不能硬编码在前端 bundle 里。Node.js 服务可以安全地从环境变量读取,并在转发请求时注入,前端只传递业务参数。

  3. 流式响应的可靠中继:浏览器 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] = useState<ReviewResult | 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 端口还是 8080?
  • Claude 的 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: Map<string, 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) => Promise<RequestInit>; // responseHandler: 如何把 HTTP 响应转成前端友好的数据 responseHandler: (response: Response) => Promise<PaperclipResult>; // healthCheck: 如何探测服务是否存活 healthCheck?: () => Promise<boolean>; }

为什么需要注册表?因为 OpenClaw 和 Claude 的 endpoint 格式天差地别:

  • OpenClaw:http://localhost:3000/v1/chat/completions
  • Claude: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: Record<string, any>; // outputFormat: 期望返回格式,影响 responseHandler 行为 outputFormat?: 'text' | 'json' | 'stream'; // metadata: 透传元数据,用于日志追踪、权限校验 metadata?: Record<string, 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: Record<string, 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.13(CUDA 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 驱动(注意:不是 535,535 与 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 容器内的用户是nobody(UID 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:3000(React 开发服务器)和http://localhost:5173(Vite)。修改方式:

# 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 (contact@myapp.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-team@client.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 vite@latest 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: Record<string, any>; outputFormat?: 'text' | 'json' | 'stream'; metadata?: Record<string, string>; } export interface ExecuteOptions { provider: string; fallback?: string; timeout?: number; signal?: AbortSignal; } export type PaperclipResult = | { status: 'success'; data: any; metadata: Record<string, 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) => Promise<RequestInit>; responseHandler: (response: Response) => Promise<PaperclipResult>; healthCheck?: () => Promise<boolean>; } export class PaperclipClient { private providers: Map<string, 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): Promise<PaperclipResult> { 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: Promise<PaperclipResult> | 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 ): Promise<PaperclipResult> { 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): Promise<PaperclipResult> => { 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) { // 忽略解析错误的行 }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 22:58:30

Paperclip不是npm包:Claude集成的通信契约与React+Node.js工程化实践

1. 项目概述&#xff1a;Paperclip 不是回形针&#xff0c;而是一个被严重误读的 AI 工程化枢纽“Paperclip”这个词在中文技术社区里最近频繁出现&#xff0c;但绝大多数人点进去后都愣住了——既没看到回形针图标&#xff0c;也没找到任何文档说它能夹纸。我第一次在掘金看到…

作者头像 李华
网站建设 2026/11/3 22:55:45

嵌入式系统中蜂鸣器PWM驱动与低延迟提示音实现

我无法基于当前输入生成符合要求的博文。原因如下&#xff1a;输入中仅提供了项目标题"buzz"&#xff0c;以及空的“相关热搜词”和“最新网络热词”字段&#xff08;内容为&#xff09;&#xff0c;未提供任何实质性的项目正文、关键词列表或摘要描述&#xff1b;根…

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

洁净车间智能定员系统:3D视觉与边缘计算实践

1. 项目概述&#xff1a;洁净车间智能定员系统的核心价值在制药、电子制造、食品加工等对生产环境有严苛要求的行业&#xff0c;洁净车间的人员管控一直是管理难点。"动恰3DV6丨动恰智能定员系统"通过三维视觉识别技术&#xff0c;实现了对洁净车间人员数量的实时精准…

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

DWG数据交换方案选型:版本魔数、商业内核SDK与开源解析对比

做图纸管理系统、算量工具、审图平台的人&#xff0c;几乎都会在某个节点撞上同一堵墙&#xff1a;甲方扔过来一个压缩包&#xff0c;里面三百个 DWG 文件&#xff0c;版本从 R14 横跨到最新格式&#xff0c;还夹着一堆外部参照、自定义实体和缺字体的文字样式。这时候“DWG 数…

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

从零手搓AI工程:NumPy实现神经网络与反向传播实战

1. 从零手搓AI工程&#xff1a;为什么我不建议你直接调包第一次看到ai-engineering-from-scratch这个项目名的时候&#xff0c;我脑子里蹦出来的画面是&#xff1a;一个人坐在终端前&#xff0c;从矩阵乘法开始&#xff0c;一行一行把Transformer搭出来&#xff0c;中间不碰任何…

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

AI 热点日报 · 2026 年 11 月 2 日

&#x1f4cc; 今日导读 今天最该看的两件事&#xff0c;一件是 ChatGPT 的分享链接——点开就能接管你的账号&#xff0c;八天前才修好&#xff1b;另一件是 Jepsen 把一家卖"全 ACID"的数据库测出 96 个问题&#xff0c;其中一条是 WHERE 4 5 能查出数据。同一天…

作者头像 李华