Skip to content

Architecture zh

Hermes Agent edited this page Oct 1, 2026 · 2 revisions

架构总览

English | 中文 | 日本語 | 한국어 | Español | Português | Русский

browsa 是 Chrome MV3 扩展,侧边栏对话 UI。扩展源码零构建——所有 JS 由 Chrome 直接加载,唯一的构建步骤(build/build.mjs)只负责用 esbuild 打包 vendor 库。

一条消息的旅程

页面 content scripts(MAIN world 拦截 / ISOLATED world 划词条)
        │  chrome.runtime.sendMessage / port
        ▼
background.js(service worker,单一消息路由器)
        │  handle() 大 switch —— case 体大都在 lib/handlers/* 模块
        ▼
侧边栏 sidepanel.js(UI 编排者)
        ▲
        ├── browsa-nav      长驻端口:NAVIGATED / XHS 推送 / SELECTION_ACTION
        ├── browsa-chat     每回合新开:主聊天流(chunk 推送协议)
        └── browsa-subchat  每次发送新开:追问卡流
  • background.js 是唯一的 chrome.runtime.onMessage 路由器:一个 handle() switch 仍是测试导入的单一出口,最大的 case 体拆在 lib/handlers/(chat / subchat / session / attach 系列 / mermaid-repair / provider-resolver / approval-relay / agent-stream-session / prompt-assembly / site-cache-store 等)。
  • 流式状态(streamPorts / streamState / chatControllers / 待审批 Map 等)单份住在 lib/state.js,background.js re-export——测试 import { ... } from '../background.js' 拿到的是同一批 Map 实例。

端口生命周期:长驻 vs 每回合(故意不同,勿混)

  • browsa-nav 长驻:面板 init 时连一次,SW 重启后经 connectNavPort() 模式重连(1s 退避;新 port 对象必须重新挂全部 onMessage 监听)。它代表「面板当前在看哪个 tab」,切 tab 时经 NAV_FOLLOW 改注册即可,符合语义。
  • browsa-chat / browsa-subchat 每回合/每次发送新开:一回合 = 一个端口。SW 睡死导致的断连不需要自愈——下一次发送自然开新的。这是故意设计:一回合一条生命周期,状态不会跨回合泄漏。

响应信封(历史 bug 家族,读层要分清)

  • 多数 handler:成功 { ok: true, data } / 抛错 { ok: false, error, code, hint }。
  • 少数 handler(APPROVAL_RESPOND/CLARIFY_RESPOND 等)内部再包一层 { ok, ... },因为它们自己 catch 中继失败而不抛。
  • 判断某个 case 属于哪种,读那个 case 的实现,别猜。错层读信封是 2026-08 全库排雷踩过的一族真 bug(如 LOAD_SESSION 未命中返回 -1、res.data.ok 才是内层成败)。

后台流跨会话切换(2026-09-24)

切走会话不再取消进行中的回合。核心路由规则一句话:面板正看着这条流时回合写入 LIVE history(bg === false);一旦后台化就写入 origin 会话快照。persistTurnEntry 是唯一写入者;REASSIGN_STREAM_SESSION(切走自动保存时)与 STREAM_PEEK(切回重挂)是仅有的两个改 bg 的消息。中止带流文本的回合按 { interrupted: true } 收尸进 origin(Esc/空闲超时/断网),显式清空历史才 salvage:false。

MV3 service worker 的坑(写 SW 侧代码前必读)

  • SW 空闲 ~30s 睡眠。模块级 Map 每次重启清零——持久状态不许放 Map。
  • SW 内 setTimeout 在消息处理返回后不可靠——用 chrome.alarms 或 chrome.storage.session。
  • 监听器按需注册是本库的常态化纪律:chrome.webNavigation 三监听只在有 navPort 时注册、tabs.onRemoved 按需注册、流 GC alarm 只在 streamState 非空时存在——否则每次导航/关 tab 都冷启动 SW(644KB 模块解析)去空跑。
  • chrome.storage.session 跨 SW 重启存活(同浏览器会话内),用于站点缓存 restore、pending SELECTION_ACTION 等。

Tab 切换

chrome.tabs.onActivated 不许碰 DOM——Chrome 让侧边栏 document 跨 tab 存活。只更新 currentTabId、页眉文字,并向 navPort 发 NAV_FOLLOW。流中止(STREAM_ABORT)按流自己的 tab 寻址(streamTabIdOf()),不是面板当前 tab。

模块地图

路径 职责
background.js SW;handle() 分发 + 内联小 case(ATTACH_PAGE 等)
lib/handlers/* 大 case 体:chat / subchat / session / attach-* / approval-relay / provider-resolver / stream-dispatch / agent-stream-session / site-cache-store / attach-store / attach-modes
lib/state.js 流式状态 Maps + pushChunk 协议 + 终态墓碑
lib/llm-client.js wire protocol 层:四条流共用 openSseStream() 骨架
lib/message-builder.js 四种 provider 形状的请求构造 + ageStaleAttachments
lib/agent-turn.js / lib/image-budget.js agent 回合公共层(文本/图片预算/回填)
lib/prompt-assembly.js CAPABILITY_HINTS_ENTRIES 单一文本源(渲染契约不分叉)
lib/storage.js chrome.storage.local 包装;全局 history + 会话拆键
lib/sidepanel/* UI 侧 26 个模块(渲染管线、会话抽屉、追问卡、时间线……见渲染管线页)
lib/content-scripts/* MAIN world 站点拦截器 + ISOLATED 划词条;站点知识库 SITES.md
lib/page-extractor.js 等 附加抽取层(reader/dom/full/auto 级联 + 站点 fast-path + PDF/Office/ASR 交接)

源头:AGENTS.md「Message flow」「Port lifecycle」「MV3 service worker gotchas」「Background streams across session switches」等节;CONTEXT.md 词汇表。同步于 2026-10-01。

Clone this wiki locally