Skip to content

Webview Bridge:gRPC-over-postMessage 协议

源码版本v4.0.10

职责

webview 是 React 应用,跑在独立的 iframe 沙箱里,和扩展宿主 (extension host) 之间只能通过 webview.postMessage / webview.onDidReceiveMessage 通信。Cline 在这层裸 API 上搭了一个「gRPC-over-postMessage」协议:webview 端把每次调用包装成 GrpcRequest (service + method + message + request_id),扩展端拿到后按 service/method 分发到具体 handler,handler 返回的结果再包成 grpc_response 推回去。这套协议在 apps/vscode/src/hosts/vscode/VscodeWebviewProvider.tsapps/vscode/src/core/controller/grpc-handler.ts 之间落地。

它做的事可以拆三层。最底一层是 webview HTML 注入和 CSP,Cline 在生产用打包好的 index.js + index.css,dev 模式连本地 Vite dev server 走 HMR。中间一层是消息收发,setWebviewMessageListener 注册 onDidReceiveMessage 回调,postMessageToWebview 把 ExtensionMessage 推回 webview。最上一层是协议分发,webview 发来的消息只有两类 (grpc_requestgrpc_request_cancel),所有具体业务 (askResponse、showTaskWithId、subscribeToState 等) 都被建模成 gRPC service.method 调用,由 serviceHandlers 注册表路由到具体 handler。

设计动机

  • 统一协议替代 switch-case:旧 Cline 用 type: "askResponse" | "showTaskWithId" | ... 的大 switch 分发,每加一个 webview 调用都要改 ExtensionMessage 类型 + switch + handler。改成 gRPC service/method 后 (GrpcRequest:7),新增调用只要在生成代码里注册一个 service.method handler,webview 端用 typed client 调,两端类型对齐。
  • request_id 关联请求与响应:GrpcRequest.request_id 是字符串 (request_id:11),webview 用它找到对应的 pending Promise,扩展端用它做取消和录制。GrpcResponse.request_id 原样带回,webview 收到后按 ID 派发。
  • streaming 用 is_streaming + sequence_number:handleStreamingRequest:113 创建 responseStream 回调,handler 可以多次调它推 partial 结果,每次带 is_streaming: true 和递增的 sequence_number,最后一次 isLast=true 关闭流。这适合「task 状态持续更新」「mcp server 列表流」这类场景。
  • 录制中间件:withRecordingMiddleware 包裹 postMessage (withRecordingMiddleware:23),每次有 grpc_response 都交给 GrpcRecorderBuilder.recordResponse,用于调试时回放完整的请求-响应序列。
  • CSP 严格但不挡 HMR:生产 CSP 用 nonce + script-src 'nonce-${nonce}' 'unsafe-eval',connect-src 只放行 posthog 和 cline.bot (CSP meta:113)。dev 模式 CSP 放开 localhost 和 ws,让 Vite HMR 能跑 (dev CSP:200)。
  • ExtensionMessage 只剩 grpc_response:ExtensionMessage 接口现在只有一个 type: "grpc_response" (ExtensionMessage:21),所有从扩展推到 webview 的内容 (state 更新、partial message、ask/say) 都包在某个 grpc_response.message 里,走的是「扩展主动调 streaming handler 推」的模式,不再有独立的 plusButtonClicked 之类消息类型。

关键文件

数据流

webview 发请求时,先在客户端用 typed client 包装成 GrpcRequest 然后 postMessage;扩展端 onDidReceiveMessage 收到后走 handleWebviewMessage:

typescript
// apps/vscode/src/hosts/vscode/VscodeWebviewProvider.ts
async handleWebviewMessage(message: WebviewMessage) {
    const postMessageToWebview = (response: ExtensionMessage) => this.postMessageToWebview(response)

    switch (message.type) {
        case "grpc_request": {
            if (message.grpc_request) {
                await handleGrpcRequest(this.controller, postMessageToWebview, message.grpc_request)
            }
            break
        }
        case "grpc_request_cancel": {
            if (message.grpc_request_cancel) {
                await handleGrpcRequestCancel(postMessageToWebview, message.grpc_request_cancel)
            }
            break
        }
        default: {
            Logger.error("Received unhandled WebviewMessage type:", JSON.stringify(message))
        }
    }
}

handleGrpcRequest 拿到 GrpcRequest 后,先 recordRequest 录制,然后用 withRecordingMiddleware 包裹 postMessageToWebview 让响应也走录制,再按 request.is_streaming 分流 (is_streaming branch:63)。unary 调 handler 一次,把结果包成 grpc_response.message 推回 (unary response:86);streaming 把 responseStream 回调交给 handler,handler 每调一次回调就推一条 grpc_responseis_streaming: !isLastsequence_number。webview 端按 request_id 派发到对应的 pending Promise,流式消息按 sequence 顺序合并。askResponse 这个具体业务就是 webview 调 cline.askResponse service 的 AskResponse method,handler 在 askResponse handler:14 里把 response 映射成 ClineAskResponse 后调 task.handleWebviewAskResponse,Task 里那个 pWaitFor 就被唤醒。

边界与失败

  • 未识别的消息类型:handleWebviewMessage 的 default 分支只记 error 不抛 (default error:177),旧版 webview 缓存里可能还在发老的 plusButtonClicked 类型,扩展容忍。
  • handler 抛错:unary handler 抛错时,catch 把 error.message 包成 grpc_response.error 推回 (unary error:96)。streaming handler 抛错时,在 catch 里推一条 is_streaming: false 的 error 响应关闭流 (streaming error:147)。
  • unknown service/method:getHandler 找不到 service 或 method 时抛 Unknown service: ${serviceName} (unknown service:196),这种错会被上层 catch 包成 grpc_response.error。
  • streaming 不主动关流:handler 正常结束后,代码注释明确说「不发最终消息,流要保持开」(keep stream open:142),等客户端发 grpc_request_cancel 或服务端显式调 isLast=true 才关。
  • webview 还未初始化时 postMessage:postMessageToWebview 直接 this.webview?.webview.postMessage(message),webview 为空时返回 undefined (postMessage undefined:189),调用方要容忍这个 falsy。
  • 配置变更触发 state push:监听 vscode.workspace.onDidChangeConfiguration,只要 cline.mcpMarketplace.enabled 变了就 postStateToWebview (config change:102),让 webview 跟上设置变化。
  • 录制失败不致命:withRecordingMiddlewarerecordRequest 都 try/catch 包了 (recordRequest try:42) ,录制挂了不影响主流程。

小结

webview bridge 把「扩展↔React 沙箱」的通信统一成 gRPC-over-postMessage,所有具体业务都是 service.method 调用,unary 一次推回,streaming 多次推 partial。这样 ExtensionMessage 只剩一种 type,新增调用只需要在生成代码里注册 handler,两端类型自动对齐。要看一个具体的 unary 调用怎么落地到 Task 上,转 /startup/controller;看 Task 内部 ask/say 怎么变成 webview 上的聊天消息,转 /agent-loop/task-class

对照官方资料:Cline 文档 · README