【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
标题
165、【Agent】【OpenCode】TuiThreadCmd(代理 Fetch 实现)
背景
上篇 blog
【Agent】【OpenCode】TuiThreadCmd(工厂设计对比)
分析了 RPC 泛型推导是"造数据/造类型 ",而 createWorkerFetch 是"造函数/造行为 ",它们都用了工厂模式,但解决的问题和类型系统的玩法完全不同,并对比了它们的核心差异,最后总结两者都是工厂,但 RPC 泛型工厂的核心价值是 "类型推导 "------让 TS 知道有什么方法;createWorkerFetch 的核心价值是 "接口兼容 "------让代码感觉不到底层被换了 ;接着对比了 C 语言对这种设计模式的支持,C 语言能实现"工厂返回函数 "这个运行时行为,但完全无法实现 typeof fetch 这种编译时契约,并分析了 C 语言做不到的部分,TS 的 createWorkerFetch 之所以优雅,是因为箭头函数自动把 client 关进闭包,返回的函数可以直接当 fetch 用,createWorkerFetch 的设计精髓是返回的东西和原生 fetch 签名一模一样,C 因为没有闭包,要么破坏签名(加 ctx 参数),要么引入全局状态。两条路都违背了"透明替换"的设计目标,下面继续分析
OpenCode
上篇 blog 提到了 C 语言必须精确匹配 typedef,无鸭子类型 ,"鸭子类型"这个名字来自一句著名的俗语:
"如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。"
(If it walks like a duck and quacks like a duck, then it must be a duck.)
在编程中,它的核心含义是:只关心对象"能做什么"(有什么方法/属性),不关心对象"是什么"(属于哪个类/接口)。
🦆 用 fetch 例子来理解
TypeScript(有鸭子类型 = 结构化类型)
javascript
// 原生 fetch 的签名
type FetchFn = (input: string) => Promise<Response>
// 自己写的函数,只要参数和返回值形状一样,就能直接赋值
const myFetch = async (url: string) => new Response("ok")
const fn: FetchFn = myFetch // ✅ 通过!没继承、没 implements,纯靠形状匹配
TS 编译器不看 myFetch 的"出身 ",只看它有没有 (string) => Promise<Response> 这个形状。这就是鸭子类型。
C 语言(无鸭子类型 = 名义类型)
c
typedef Response* (*FetchFn)(const char* url);
Response* myFetch(const char* url) { return NULL; }
FetchFn fn = myFetch; // ⚠️ 虽然签名一模一样,但必须精确匹配 typedef
// 如果参数用了 char* 而不是 const char*,直接报错
C 要求名义上的精确匹配 。哪怕两个函数行为完全相同,只要类型定义的名字或修饰符有一丝不同,就不兼容 。它不认"形状 ",只认"身份证"。
📊 一张表看懂区别
| 鸭子类型 (结构化) | 非鸭子类型 (名义化) | |
|---|---|---|
| 判断依据 | 有没有对应的方法/属性 | 是否声明了同一个类/接口名 |
| 代表语言 | TypeScript, Go, Python, Ruby | C, Java, C#, Rust |
| 类比 | 看能力发上岗证 | 看毕业证发上岗证 |
| 灵活性 | 高:不同来源的对象可互换 | 低:必须显式声明关系 |
| 安全性 | 运行时可能翻车(Python) 编译时安全(TS/Go) | 编译期严格保证 |
💡 为什么这对 createWorkerFetch 很重要?
createWorkerFetch 的设计目标是透明替换原生 fetch。这意味着:
- 有鸭子类型的语言 :返回的假 fetch 只要"长得像 "真 fetch,所有第三方库(axios、React Query 等)都会无条件接受它。不需要改任何第三方代码。
- 没有鸭子类型的语言 :必须让假 fetch 和真 fetch 共享同一个显式声明的类型/接口 。如果第三方库用的是自己的函数指针
typedef,就得写适配层 或者修改第三方源码。
一句话总结
鸭子类型就是 "以貌取人 "------不管你是谁,只要长得符合这个岗位的要求,就可以上岗。C 语言是 "查户口"------能力再强,户口本上没写这个名字,就不认。
下面看这个代理 fetch 的详细内容

这个代理 fetch 的核心功能是:将标准的 HTTP Fetch 请求,翻译并转发到自定义的 RPC 通道中执行 ,其本质上是一个协议适配器 ,让上层代码可以用熟悉的 fetch API 发请求,但底层实际走的是 RpcClient 定义的私有通信协议,下面逐层拆解功能:

- 请求归一化
javascript
const request = new Request(input, init)
- 功能 :利用浏览器/Node.js 内置的 Request 构造函数,将各种输入格式 (字符串 URL、URL 对象、已有 Request 对象)统一解析为一个标准的 Request 实例。
- 目的 :避免手动处理 input 和 init 合并的复杂逻辑(如 headers 合并、method 默认值等),直接复用平台能力完成请求预处理。
- 请求体序列化
javascript
const body = request.body ? await request.text() : undefined
- 功能 :读取请求体内容,将其转换为纯文本字符串;若无请求体则传 undefined。
- 目的 :RPC 通道通常不支持流式传输或二进制 Blob ,需要将请求体序列化为可跨进程/网络传输的简单数据类型。
⚠️ 代价:所有请求体被全量加载到内存,丧失流式上传能力(其实也不需要流式,RPC 作为进程间通信,直接通信就好了)。
- RPC 转发
javascript
const result = await client.call("fetch", {
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
body,
})
- 功能 :将标准 HTTP 请求的关键字段(URL、方法、Headers、Body)提取为普通 JSON 对象,通过
client.call发送到远端服务。 - 目的:完成从 HTTP 语义到 RPC 调用参数的映射。远端服务收到后会代为发起真实 HTTP 请求,并将结果返回。
关键点 :Object.fromEntries(request.headers.entries()) 将 Headers 对象转为普通键值对,因为 Headers 对象无法直接跨 RPC 序列化。
- 响应重构
javascript
return new Response(result.body, {
status: result.status,
headers: result.headers,
})
- 功能:将 RPC 返回的原始结果重新包装为标准 Response 对象。
- 目的 :确保返回值与原生 fetch 完全一致,调用方可以正常使用
.json()、.text()、.status等所有标准 API。
📌 整体数据流总结
bash
调用方 fetch(url, options)
↓
[1] 签名兼容 → 接收标准参数
↓
[2] Request 归一化 → 统一请求对象
↓
[3] Body 序列化 → 文本字符串
↓
[4] RPC 转发 → client.call("fetch", {...})
↓
(远端执行真实 HTTP 请求)
↓
[5] Response 重构 → 标准响应对象
↓
调用方拿到标准 Response
💡 一句话功能定义
HTTP-over-RPC 透明代理 :对外暴露标准 Fetch API,对内将请求序列化后通过 RPC 通道转发,再将远端响应反序列化回标准 Response 对象,实现"用 fetch 的语法,走 RPC 的通道"。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog