欢迎光临
我们一直在努力

167、【Agent】【OpenCode】TuiThreadCmd(EvenSource)

【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除

标题

167、【Agent】【OpenCode】TuiThreadCmd(EvenSource)

背景

上篇 blog 【Agent】【OpenCode】TuiThreadCmd(流式传输&二进制Blob) 分析了流式传输和二进制 Blob 两个概念,这是理解为什么代码里要写 await request.text() 的关键,接着解释了什么是二进制 Blob,Blob = Binary Large Object(二进制大对象),可以把它理解为 “一团原始的、未解释的字节数据”,RPC 通道通常只能传输 JSON 可序列化的数据。而 JSON 只支持字符串、数字、布尔、数组、对象这几种类型,所以 fetch 代理不能传文件,图片被 .text() 强行当 UTF-8 解码后会变成乱码,这也是这段代码最危险的地方:它不是“禁止传文件”,而是 “假装能传,然后默默把数据毁掉”。接着解释了什么是流式传输,流式传输 = 数据像水流一样“边产生边发送”,而不是“攒够了再一次性发”,最后提到如果场景只涉及 JSON API 请求(body 都是小文本),这个妥协完全没问题;但如果要传文件或大数据量,就需要升级 RPC 通道本身的设计,下面继续分析

OpenCode

之前 blog 还有个点

关键点:Object.fromEntries(request.headers.entries()) 将 Headers 对象转为普通键值对,因为 Headers 对象无法直接跨 RPC 序列化。

这句话的意思是:Headers 是一个特殊的浏览器/Node.js 内置对象,它不能直接被 JSON 序列化,所以必须先把它“拆”成普通的 { key: value } 对象,才能通过 RPC 发送,下面详细分析下,分三步拆解:


  • Headers 对象长什么样?
  • const headers = new Headers({
    "Content-Type": "application/json",
    "Authorization": "Bearer xxx"
    })

    它不是普通对象 {},而是一个有内部状态的类实例。直接打印或序列化它:

    console.log(JSON.stringify(headers)) // "{}" ← 空的!字段全丢了
    console.log(Object.keys(headers)) // [] ← 也是空的

    因为 Headers 把数据存在了内部私有槽位里,而不是挂在可枚举的属性上。JSON 序列化器只能看到对象的自有可枚举属性,所以什么都读不到。


  • .entries() 做了什么?
  • headers.entries()
    // → Iterator [["content-type", "application/json"], ["authorization", "Bearer xxx"]]

    它返回一个迭代器,逐个吐出 [key, value] 键值对数组。这是 Headers 提供的标准读取接口,能正确访问到内部存储的数据。


  • Object.fromEntries() 做了什么?
  • Object.fromEntries(headers.entries())
    // → { "content-type": "application/json", "authorization": "Bearer xxx" }

    它把迭代器里的每一对 [key, value] 重新组装成一个纯普通对象。这个对象:

    • ✅ 没有特殊原型链
    • ✅ 所有属性都可枚举
    • ✅ JSON.stringify() 能完整序列化
    • ✅ 可以安全地作为 client.call() 的参数跨 RPC 传输

    📌 整条链路一图看懂

    Headers 实例(不可序列化)
    ↓ .entries()
    Iterator<[key, value]>(仍不可序列化)
    ↓ Object.fromEntries()
    Plain Object { key: value }(✅ 可 JSON 序列化)
    ↓ JSON.stringify / RPC 编码
    发送到远端

    一句话总结:Headers 是个“带锁的保险箱”,JSON 序列化器打不开它。.entries() 是钥匙,把内容取出来;Object.fromEntries() 是把取出来的东西装进一个“透明塑料袋”(普通对象),这样 RPC 通道才能安全运输。


    OK,下面继续分析

    在这里插入图片描述

    这个函数的功能是:创建一个基于 RPC 通道的“伪 EventSource”对象,用来替代浏览器原生的 EventSource(SSE)。它和上一个 createWorkerFetch 是同一套设计思路:对外暴露熟悉的 API,对内走 RPC 通道。但这次适配的不是 HTTP 请求,而是服务端推送事件。


    🔍 下面逐行拆解

    function createEventSource(client: RpcClient): EventSource {
    return {
    // 1️⃣ 订阅事件:把 handler 注册到 RPC client 的 "event" 消息上
    on: (handler) => client.on<Event>("event", handler),

    // 2️⃣ 切换工作区:通过 RPC 调用通知服务端改变当前 workspace
    setWorkspace: (workspaceID) => {
    void client.call("setWorkspace", { workspaceID })
    },
    }
    }

    on(handler) — 事件订阅

    • 原生 EventSource:通过 addEventListener("message", cb) 接收 SSE 推送
    • 这里:改为 client.on("event", handler),从 RPC 通道监听服务端主动推过来的 “event” 消息

    本质:把 SSE 的“HTTP 长连接推送”替换成了“RPC 通道的事件订阅”


    setWorkspace(workspaceID) — 控制指令

    这不是标准 EventSource 的 API,是业务扩展,通过 RPC 单向调用告诉服务端:“现在要切换到这个 workspace,后续推送的事件都应该是这个 workspace 的”

    void 表示不关心返回值,这是一个“发了就行”的通知型调用


    📌 和原生 EventSource 的对比

    原生 EventSource这个代理 EventSource
    传输通道 HTTP SSE 长连接 RPC 通道(WebSocket/IPC 等)
    订阅方式 addEventListener() client.on("event", handler)
    连接管理 浏览器自动重连 由 RpcClient 内部管理
    业务扩展 ❌ 不支持 ✅ setWorkspace()
    返回类型 真正的 EventSource 实例 只是形状相似的普通对象 { on, setWorkspace }

    ⚠️ 注意:这不是真正的 EventSource

    返回类型标注为 EventSource,但实际上只是一个结构兼容的子集(Duck Typing)。它没有:

    • readyState、url、withCredentials 等属性
    • close()、dispatchEvent() 等方法
    • 浏览器的自动重连机制

    代码能正常工作的前提是:消费方只用到了 on() 和 setWorkspace() 这两个方法。如果某处代码调用了 eventSource.close(),运行时会直接报错。


    💡 一句话总结 它是一个 RPC 版的事件推送适配器:用 client.on 替代 SSE 长连接来接收服务端推送,同时扩展了 setWorkspace 业务能力,让上层代码可以用类 EventSource 的方式消费 RPC 事件流。


    OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog

    赞(0)
    未经允许不得转载:171主机测评 » 167、【Agent】【OpenCode】TuiThreadCmd(EvenSource)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址