)
【聲明】本博客所有內(nèi)容均為個(gè)人業(yè)余時(shí)間創(chuàng)作所述技術(shù)案例均來(lái)自公開(kāi)開(kāi)源項(xiàng)目如GithubApache基金會(huì)不涉及任何企業(yè)機(jī)密或未公開(kāi)技術(shù)如有侵權(quán)請(qǐng)聯(lián)系刪除標(biāo)題168、【Agent】【OpenCode】TuiThreadCmdSSE背景上篇 blog【Agent】【OpenCode】TuiThreadCmdEvenSource分析了 Headers 是一個(gè)特殊的瀏覽器/Node.js 內(nèi)置對(duì)象它不能直接被 JSON 序列化所以必須先把它“拆”成普通的{ key: value }對(duì)象才能通過(guò) RPC 發(fā)送因?yàn)?Headers 把數(shù)據(jù)存在了內(nèi)部私有槽位里而不是掛在可枚舉的屬性上。JSON 序列化器只能看到對(duì)象的自有可枚舉屬性所以什么都讀不到而.entries()是 Headers 提供的標(biāo)準(zhǔn)讀取接口能正確訪問(wèn)到內(nèi)部存儲(chǔ)的數(shù)據(jù)再由Object.fromEntries()把迭代器里的每一對(duì)[key, value]重新組裝成一個(gè)純普通對(duì)象接著分析了基于 RPC 通道的“偽 EventSource”對(duì)象它和上一個(gè)createWorkerFetch是同一套設(shè)計(jì)思路對(duì)外暴露熟悉的 API對(duì)內(nèi)走 RPC 通道。但這次適配的不是 HTTP 請(qǐng)求而是服務(wù)端推送事件下面繼續(xù)分析OpenCode上篇 blog 提到了 SSE 的概念下面澄清下SSE Server-Sent Events服務(wù)器發(fā)送事件它是瀏覽器原生提供的一種 “服務(wù)端向客戶端單向推送數(shù)據(jù)” 的標(biāo)準(zhǔn)協(xié)議。用最簡(jiǎn)單的類比理解通信方式類比方向普通 HTTP打電話問(wèn)老師問(wèn)題老師回答后掛斷一問(wèn)一答WebSocket和老師接通電話雙方可以隨時(shí)說(shuō)話雙向?qū)崟r(shí)SSE老師打開(kāi)廣播頻道只能聽(tīng)不能說(shuō)話服務(wù)端 → 客戶端 單向推送SSE 就是那個(gè)“廣播頻道”服務(wù)端持續(xù)往客戶端推數(shù)據(jù)客戶端只需要監(jiān)聽(tīng)不需要反復(fù)發(fā)請(qǐng)求。瀏覽器里怎么用// 瀏覽器原生 API就這么簡(jiǎn)單constsourcenewEventSource(/api/events)// 此 EventSource 非 opencode EvenSource方法不一樣source.onmessage(event){console.log(收到推送:,event.data)}source.onerror(err){console.log(連接出錯(cuò)瀏覽器會(huì)自動(dòng)重連)}瀏覽器會(huì)自動(dòng)建立一個(gè) HTTP 長(zhǎng)連接服務(wù)端返回的響應(yīng)頭是 Content-Type: text/event-stream然后數(shù)據(jù)就像水流一樣持續(xù)推過(guò)來(lái)。SSE 的數(shù)據(jù)格式服務(wù)端推送的內(nèi)容是純文本格式非常簡(jiǎn)單data:{user:alice,action:login}data:{user:bob,action:upload}event:notificationdata:{title:新消息,body:你好}每條消息以data:開(kāi)頭空行表示一條消息結(jié)束可選的event:字段區(qū)分消息類型SSE vs WebSocketSSEWebSocket方向單向服務(wù)端→客戶端雙向協(xié)議普通 HTTP獨(dú)立協(xié)議 (ws://)自動(dòng)重連? 瀏覽器內(nèi)置? 需自己實(shí)現(xiàn)數(shù)據(jù)格式純文本文本或二進(jìn)制復(fù)雜度極低較高適用場(chǎng)景通知推送、實(shí)時(shí)日志、AI 流式輸出聊天室、游戲、協(xié)作編輯為什么 AI 對(duì)話都用 SSEChatGPT 等產(chǎn)品的“打字機(jī)效果”就是 SSE模型生成一個(gè) token服務(wù)端就通過(guò) SSE 推一個(gè) token前端逐字渲染。不需要雙向通信SSE 比 WebSocket 簡(jiǎn)單得多。回到代碼functioncreateEventSource(client:RpcClient):EventSource{return{on:(handler)client.onEvent(event,handler),// ...}}這里做的事情就是把原本應(yīng)該走 HTTP SSE 長(zhǎng)連接的推送替換成了走 RPC 通道的事件訂閱。原來(lái)瀏覽器new EventSource(url)→ HTTP 長(zhǎng)連接 → 服務(wù)端推 SSE 文本流現(xiàn)在client.on(event, handler)→ RPC 通道WebSocket/IPC→ 服務(wù)端推結(jié)構(gòu)化事件對(duì)象調(diào)用handler通知好處是不需要額外維護(hù)一個(gè) SSE 端點(diǎn)所有通信都走同一個(gè) RPC 通道代價(jià)是失去了瀏覽器原生的自動(dòng)重連等能力需要自己在 RpcClient 里實(shí)現(xiàn)。另外這里的 “event” 就是一個(gè)硬編碼的字符串字面量。這意味著所有 handler 都綁定在同一個(gè)通道上無(wú)論注冊(cè)多少次on(handler)底層都是在執(zhí)行client.on(event, handler)。所有的回調(diào)函數(shù)都被注冊(cè)到了 RPC Client 內(nèi)部名為 “event” 的這唯一一個(gè)事件監(jiān)聽(tīng)器列表中。服務(wù)端推送時(shí)沒(méi)有區(qū)分度當(dāng)服務(wù)端通過(guò) RPC 發(fā)送消息時(shí)它只能發(fā)類似這樣的結(jié)構(gòu)client.emit(event,payload)它無(wú)法直接指定“這條消息只給某個(gè)特定的 handler”。RPC Client 收到 “event” 消息后會(huì)廣播式地觸發(fā)所有注冊(cè)在該通道上的 handler。那如何區(qū)分不同類型的事件既然通道只有一個(gè)事件的分類邏輯就下沉到了 payload 內(nèi)部。通常 Event 類型會(huì)長(zhǎng)這樣interfaceEvent{type:workspaceChanged|fileUpdated|notificationdata:unknown}客戶端消費(fèi)方需要在自己的 handler 里做二次分發(fā)eventSource.on((event){if(event.typefileUpdated){handleFileUpdate(event.data)}elseif(event.typenotification){showNotification(event.data)}})為什么這么設(shè)計(jì)這是一種 “單通道 消息自描述” 的模式好處是RPC 接口極簡(jiǎn)服務(wù)端只需要暴露一個(gè)emit(event, ...)方法不用為每種事件定義單獨(dú)的 RPC 消息名統(tǒng)一中間件可以在 “event” 這一個(gè)點(diǎn)上統(tǒng)一做日志、鑒權(quán)、序列化等處理與 SSE 語(yǔ)義對(duì)齊原生 SSE 本質(zhì)上也是一個(gè) HTTP 連接上的單一文本流所有事件都走同一個(gè)管道靠event: type字段區(qū)分類型。這個(gè)設(shè)計(jì)是對(duì) SSE 模型的忠實(shí)映射??潛在風(fēng)險(xiǎn)如果業(yè)務(wù)事件種類很多、頻率很高所有 handler 都會(huì)被頻繁觸發(fā)然后各自過(guò)濾存在一定的性能開(kāi)銷。當(dāng)事件量大到成為瓶頸時(shí)就需要拆分為多個(gè)命名通道如client.on(fileUpdated, ...)但在那之前單通道模式是更簡(jiǎn)單、更合理的選擇OK本篇先到這里如有疑問(wèn)歡迎評(píng)論區(qū)留言討論祝各位功力大漲技術(shù)更上一層樓更多內(nèi)容見(jiàn)下篇 blog【Agent】【OpenCode】TuiThreadCmdworkspaceworker路徑