DeepChat Agent 浏览器画中画:从后台渲染宿主到 NativeKit 原生 PiP 面板的实现解析
2026/9/17 9:15:12 网站建设 项目流程

DeepChat Agent 浏览器画中画:从后台渲染宿主到 NativeKit 原生 PiP 面板的实现解析

【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat

本文围绕 DeepChat 的 Agent 浏览器预览规格(docs/features/agent-browser-pip/spec.md)展开,讲解当 Agent 会话在后台操作网页时,DeepChat 如何用「单一实时页面 + 后台渲染宿主 + 原生只读 PiP 面板」三者协作,让用户在不打开侧边栏的情况下看到 Agent 正在访问的页面。读完后你可以理解页面 reparent 的约束模型、捕获帧的序列化管线、原生覆盖层的进程级所有权仲裁,以及面板交接(panel handoff)与 run 级关闭的完整生命周期规则,并能对照源码定位到每个契约的落点。

背景:为什么需要一套独立的 Agent 浏览器呈现层

DeepChat 内置了名为 YoBrowser 的嵌入式浏览器能力,Agent 可以通过load_urlcdp_send等工具直接导航页面并执行 CDP 命令。当 Agent 在后台默默操作网页时,用户在聊天窗口里是「盲操作」的:看不到 Agent 正在加载哪个页面、滚动到哪、点击了哪里。PiP(Picture-in-Picture)预览正是为此设计的呈现层:

  • 右侧面板保持关闭时,符合条件的 Agent 会话可以弹出一个原生只读预览,展示 Agent 页面的实时画面;
  • 如果右侧已经打开了任何面板(或原生能力不可用),Agent 的浏览器动作会走既有的 Browser 侧边栏,而不是强行弹出 PiP;
  • 预览是只读图像,不是第二个浏览器、更不是页面的另一个原生父级;打开面板就是把同一个实时页面「搬」进面板。

规格文档明确当前状态:单页 Agent 浏览器预览与面板交接已实现;可见多标签条、Fit desktop 和完整响应式/展开页面工作流被推迟到 docs/features/agent-browser-pip/plan.md 与 docs/features/agent-browser-pip/tasks.md 中继续跟踪;原生呈现层的平台交互与性能验证仍是开放项。原生的窗口拖拽、工作区钳制与 z-order 由 NativeKit 定义,见 docs/architecture/nativekit-agent-browser-pip/spec.md。此外,Computer Use 快照 与 Browser 共享同一份「进程全局呈现所有权」,但两者的功能契约并不合并——Browser 仍然独立拥有自己的页面、捕获循环、面板交接和 run 级关闭。

组件职责图:谁拥有什么

规格中的「Current Ownership」一节把职责切得很干净,源码逐条对得上:

组件路径职责
YoBrowserPresentersrc/main/desktop/browser/YoBrowserPresenter.ts主进程侧唯一持有者:每个聊天会话一份SessionBrowserState,包含实时WebContentsView、页面封装、owner(agent/user)、Agent run 标识、放置信息、预览宿主、捕获状态
YoBrowserToolHandlersrc/main/desktop/browser/YoBrowserToolHandler.ts接受来自 Agent 执行侧的 run 标识并转发给 presenter;工具事件携带 owner/source 与 run 标识
ChatSidePanel.vuesrc/renderer/src/components/sidepanel/ChatSidePanel.vue为「显式用户导航」或「面板已打开」打开 Browser;单独的 Agent open 请求不会强行打开关闭中的面板
BrowserPanel.vuesrc/renderer/src/components/sidepanel/BrowserPanel.vue提供稳定的可见 bounds,通过类型化客户端管理页面附着
AgentBrowserPiP.vuesrc/renderer/src/components/browser/AgentBrowserPiP.vue推导当前会话/工作 run/窗口/面板资格,并对预览模式请求做合并(coalescing)
AgentPreviewCoordinatorsrc/main/desktop/preview/AgentPreviewCoordinator.ts拥有跨 Browser 与 Computer Use 的「唯一可见原生呈现」;NativeKit 拥有拖拽、显示工作区钳制与原生 z-order;渲染进程只收到来源特定的动作与 surface 状态,原生图像帧不经过渲染进程 IPC
yoBrowserSession.tssrc/main/desktop/browser/yoBrowserSession.ts从捆绑运行时推导桌面 Chromium 用户代理(process.versions.chrome),窄页面 bounds 不会把浏览器切换为移动端/触摸语义

关键设计点是:当前公开状态是每会话一个页面,而不是推迟中的多标签工作区模型。SessionBrowserState在 YoBrowserPresenter.ts 中定义,字段包含previewHost(后台BaseWindow)、previewMode(三种预览模式)、previewSurfacenative-overlay/renderer-canvas/none)、previewEpochpreviewClaimSequence等——后两者正是下文「epoch 失效」与「进程级 claim」机制的载体。

实时页面与后台渲染宿主:一个页面,两种父级

这是整个方案的骨架。面板关闭期间,Agent 页面停在一个无焦点的后台渲染宿主BaseWindow中;该宿主只为 Chromium 提供显示表面,透明、位于屏幕外,且不接收任何用户输入。后台页面的 viewport 固定为1280 × 800,与预览图尺寸无关:

Background render host +-- Agent WebContentsView at 1280 x 800 +-- capture -> resize/encode -> NativeKit read-only panel Chat BrowserWindow +-- trusted chat renderer +-- Browser panel: the same Agent WebContentsView when visible

约束是「每页视图的原生父级数 <= 1」:

panel => 父级是聊天宿主 contentView render-host => 父级是后台宿主 contentView(1280 x 800) detached => 无原生父级

打开面板时,是把这同一个页面视图 reparent 进可见面板,URL、DOM、滚动位置、cookie、可聚焦页面状态、CDP 身份全部原样保留——不重新导航、不复制状态、不新建 CDP target。父级切换与捕获状态迁移都归主进程所有,切换规则是「先移除旧父级再加新父级」,并用当前 epoch 使过期的异步捕获工作失效。

ensurePreviewHost的实现印证了这些约束(YoBrowserPresenter.ts):

host = new BaseWindow({ x: -10000, y: -10000, // 屏幕外 width: PREVIEW_VIEWPORT.width, // 1280 height: PREVIEW_VIEWPORT.height, // 800 show: !isMac, opacity: 0, focusable: false, skipTaskbar: true, hiddenInMissionControl: true, frame: false, transparent: true }) host.setIgnoreMouseEvents(true)

值得注意的一个平台细节写在源码注释里:macOS 上隐藏宿主再「解除节流」会破坏捕获(引用了 Electron PR 行为),所以 macOS 走host.showInactive()保持帧活跃,而其它平台在宿主隐藏时才调用setBackgroundThrottling(false)。宿主closed事件里会同步释放 claim、复位previewModestopped、递增previewEpoch并恢复背景节流——这就是「主进程不等渲染进程清理就处理宿主销毁」的落地方式。

预览模式、资格与捕获管线

预览把「捕获」与「页面渲染」解耦为三种模式,setPreviewMode(YoBrowserPresenter.ts)是它们的唯一入口:

  • capturing:维护渲染宿主,在资格满足时发布有界帧;
  • rendering:页面继续对 Agent 工具可用,但抑制预览捕获(例如用户正要点「在面板中打开」);
  • stopped:停止捕获并按页面生命周期释放预览宿主。

资格条件(全部满足才捕获):页面归当前 Agent run 所有、会话处于工作(working)状态、拥有前台的聊天窗口、右侧面板关闭、当前 run 未被用户关闭,并且持有当前进程级预览 claim。刷新图像不会从另一个预览来源(如 Computer Use)手中抢走所有权。

「run 相关性」规则也很明确:Agent run 在第一次浏览器动作之后才变得相关——run 一启动就展示空 PiP 是不允许的;权限/提问暂停不是终态结果,暂停、恢复、取消、被顶替(supersession)全程 run 标识与会话状态必须保持一致。

捕获常量集中在 YoBrowserPresenter.ts,是规格「有界捕获」条款的直接落地:

const PREVIEW_VIEWPORT = { width: 1280, height: 800 } // 后台页面 viewport const PREVIEW_FRAME = { width: 400, height: 250 } // 预览输出尺寸 const PREVIEW_ACTIVE_INTERVAL_MS = 500 // 活跃期节奏 const PREVIEW_IDLE_INTERVAL_MS = 2000 // 空闲期节奏 const PREVIEW_MAX_BYTES = 512 * 1024 // 编码输出上限 512 KiB

capturePreviewFrame(YoBrowserPresenter.ts)的单轮流程是:

  1. 校验previewMode === 'capturing'、epoch 未变、run 与 claim 仍是当前值、页面未销毁,任一失败直接放弃本轮;
  2. capturePage(1280×800 区域, { stayHidden: true })抓帧 →resize(400×250)toJPEG(72)
  3. 仅当 JPEG 字节数 ≤ 512 KiB 才推送;推给native-overlaypreviewCoordinator.present,推给renderer-canvas走渲染进程 IPC 事件browser.preview.frame
  4. 整轮结束后才安排下一轮 tick,活跃(页面加载中或处于 1.5 秒活动爆发期)用 500ms,否则用 2000ms。

由此实现规格里的三条硬约束:同一时刻最多一个在途捕获previewCapture单槽位 +finally里才安排下一个 tick);不合格或已停止的目标不做空闲捕获(下一轮调度前再次isCurrent校验);编码超界的帧被丢弃而不是截断。epoch 机制则保证任何「捕获/换父/停捕」发生时,先前在途的异步帧要么因 epoch 不匹配被丢弃、要么因isCurrent校验失败被拒收——这正是「用当前 epoch 使过期异步捕获工作失效」的实现。

渲染进程侧的 AgentBrowserPiP.vue 负责资格推导与模式合并:eligible组合了requiresRendering(working + owner=agent + 有 run 标识 + 页面已初始化)、窗口聚焦、面板关闭、未关闭当前 run 四个条件(AgentBrowserPiP.vue);previewMode计算属性把「面板打开 / 页面已可见 / 不合格」映射为rendering,把「合格且非紧凑」映射为capturing。所有模式请求并不逐条透传,而是写进pendingPreviewRequestdrainPreviewRequests串行合并发出(AgentBrowserPiP.vue)——这就是规格中「coalesces preview-mode requests」的落点。会话切换时会把旧会话的stopped请求发出,且任何帧解码前的身份/epoch/序列号校验(frameDecodeVersionlatestFrameSequence)确保切换到别的会话时,绝不会短暂暴露上一个会话的帧、URL 或标题

进程级呈现所有权:AgentPreviewCoordinator

AgentPreviewCoordinator(src/main/desktop/preview/AgentPreviewCoordinator.ts)是 Browser 与 Computer Use 共享的原生呈现仲裁器,封装 NativeKit 的 overlay(@zerob13/nativekit)。其目标身份AgentPreviewTarget是六元组:source / windowId / sessionId / runId / epoch / claimSequence——任何一维不匹配,帧推送就被isCurrent拒收。

核心机制有四个:

  • claim(所有权声明)claim()按会话发号(nextClaimSequence),同一会话的新 run 顶替旧 run 的 claim;matchesClaim要求 claim 的 source、run、序号三方一致。「刷新图像不抢所有权」由这条链路保证。
  • dismissedRuns(run 级关闭)dismiss()sessionId -> runId记入dismissedRuns,该 run 之后的任何prepare都直接返回none;用户关闭 PiP 只压制当前 run,不关页面、不取消工具、不中断 run。
  • 平台门控isPublishedTarget(AgentPreviewCoordinator.ts)仅放行darwin:arm64 / darwin:x64 / win32:x64 / linux:arm64 / linux:x64,其余平台标记unavailable并记录unsupported_platform,随后同一激活路径退回 Browser 侧面板。
  • 宿主同步:overlay 通过attachHost绑定聊天窗口(锚定在窗口后缘,偏移 16px,AgentPreviewCoordinator.ts);窗口 focus/blur/show/hide/minimize/move/resize/closed 事件驱动「模糊即隐藏、恢复即重新同步 bounds」(50ms 防抖),显示器增删/度量变化触发重新钳制。

面板工具栏由协调器统一配置(AgentPreviewCoordinator.ts):browser 来源提供Open in side panel(control idopen-panel,触发activate动作)与Close(触发dismiss)两个按钮;Computer Use 来源只有Close。双触发通道是:overlay 的activate(宿主区域被点)与control事件,都经emitAction派发到对应来源注册的 handler——Browser 侧的 handler 是 YoBrowserPresenter.ts 里的handleNativePreviewAction,它在处理前先做 windowId/sessionId/runId/epoch/claimSequence 五元组校验,随后停止捕获、把模式切到rendering、发browser.preview.action事件让渲染进程打开面板。

坐标与性能预算方面,协调器还携带一组常量(AgentPreviewCoordinator.ts):PREVIEW_MAX_EDGE = 360(overlay 尺寸上限)、HOST_ANCHOR_OFFSET = 16HOST_SYNC_DELAY_MS = 50SLOW_PUSH_WARNING_MS = 25(单次pushImage超过 25ms 会打一条限频告警,60 秒内最多一条)。帧推送以presentationIdagent-preview:browser:<windowId>:<sessionId>)保持稳定身份,图像替换不重置用户拖拽过的原点——窗口移动/缩放同步与图像替换是两条独立路径。

生命周期:事件—呈现—页面行为对照表

规格给出的完整生命周期矩阵是验收契约的核心,此处完整保留:

事件呈现行为页面行为
面板打开时首个 Agent 动作不显示 PiP;激活 Browser在面板 bounds 稳定后附着同一页面
面板关闭时的首个合格 Agent 动作仅在拿到有效帧后显示页面留在渲染宿主中
原生能力不可用请求 Browser 侧面板激活通过常规面板路径附着同一页面
Browser 面板打开隐藏原生预览并停止捕获reparent 进面板
用户从 Browser 切回 Workspace面板保持打开期间不显示 PiP按活动 run 需要 detach 或保留后台渲染
合格 run 中面板关闭显示前先推一帧当前帧reparent 回渲染宿主
PiP 关闭压制当前 run 并停止捕获保留页面与活动工具
原生「在面板中打开」或双击聚焦宿主窗口并请求 Browser面板 bounds 稳定后 reparent
宿主失焦/隐藏/最小化同步隐藏仅当 Agent 工作需要时才保留渲染
合格宿主恢复前台重新评估所有权并显示当前帧不重新加载页面
run 进入终态停止捕获并移除呈现释放渲染宿主,保持常规页面生命周期
会话/页面销毁或宿主关闭移除呈现与目标释放所拥有的页面/宿主资源
应用退出只停一次原生 overlay走既有幂等的 presenter 清理(shutdown销毁全部会话浏览器并注销 preview handler)

「首帧先于可见」规则在两侧都有落点:协调器present()内只有在desiredVisible && isHostEligible时才setVisible(true);而捕获失败时保留上一张同目标有效图并在下一个有界 tick 重试,绝不闪空图或切目标。

失败处理与安全约束

规格对失败与安全的条款,在源码中逐条可查:

  • 初始化/宿主附着失败 ⇒ 进程级禁用原生能力并打开 Browser 面板disableNativetoolbar_update_failed/host_attach_failed路径)置unavailable = truestopNative();一次性「坏帧」不会改道到renderer-canvas——renderer-canvas只对其既有调用方可用,不是失败自动降级项(YoBrowserPresenter.ts 的openBrowserPanelForUnavailablePreview只在!isAvailable()时激活面板)。
  • 拿不到稳定面板 bounds ⇒ 保留/恢复预览并报告可恢复的交接失败,不重载页面、不重复附着。
  • 远端页面失败走既有 Browser 错误行为与工具错误YoBrowserUnavailableError,见 src/main/desktop/browser/YoBrowserToolHandler.ts),预览失败不打断正常 Agent 操作。
  • 执行隔离:页面保持在既有 sandbox/session 中执行(sandbox: true+ 独立分区persist:yo-browser,yoBrowserSession.ts);原生呈现层只拿到缩放后的 JPEG 图像,渲染进程或远端内容拿不到任何 NativeKit 对象或原生句柄。
  • 帧只在内存中:不进入日志、缓存、持久化或磁盘文件;日志只有有界的生命周期/性能元数据(如slow frame presentationdurationMs),不含图像字节、页面内容或密钥。
  • YoBrowserOverlayWindow与 PiP 是两层:前者仍是可见面板上的 Agent 活动图层(点击/滚动/键入的位置标注),独立于 PiP 存在。
  • 诚实性条款:源可见性、帧时序、原生移动与打包合成器行为有待平台 QA;配置了捕获间隔不等于对帧率的测量承诺

安全配置层面,独立分区会话还会拒绝所有权限请求(摄像头、地理位置等一律callback(false)),并用webRequest.onBeforeSendHeaders强制覆盖 UA——UA 来自process.versions.chrome的桌面形态(macOS/Windows/Linux 各有对应字符串),这解释了规格「窄页面 bounds 不切换移动/触摸语义」:整条链路上不存在按视口宽窄切换 UA 的逻辑。

推迟的多标签工作区契约

规格把「可见标签条 + 展开页面适配」明确排除在已交付的单页产品之外,但完整保留了其验收前置条件,供后续实现对照:

标签身份与归属:只有当主进程所有者、类型化路由、渲染端调用方与生命周期测试一起迁移时,才允许用「每会话标签集合 + 选中标签身份」替换单页状态;用户标签与主 Agent 自动化标签严格区分,不把用户标签静默变成 Agent 标签;同一会话复用一个主 Agent 标签跨 URL 加载与 run,不为每次load_url建标签;精确 run 标识要贯穿 immediate/batch/resumed/deferred 工具路径;关闭标签只销毁其页面并向并发操作返回可恢复的 page-closed 结果,关 PiP 不关标签;驻留/驱逐需要实测内存证据,禁止臆造的闲置驱逐或重启持久化。

响应式 chrome:以容器宽度(非应用视口)为准——≥640px 支撑完整标签/导航行,480–639px 用紧凑标签与溢出控件,更窄时保留地址栏与键盘可达命令;420px 面板必须可用。标签标题截断、标签滚动而非挤压、地址栏可收缩但不裁切导航命令:

Expanded panel +----------------------------------------------------------------+ | [User tab] [Agent tab *] [+] | | [Back] [Forward] [Reload] [URL ] [Fit] [Expand] | +----------------------------------------------------------------+ Compact panel +----------------------------------------------+ | [User] [Agent *] ... | | [Back] [Reload] [URL ] [...]| +----------------------------------------------+

页面呈现与展开:响应式渲染用真实原生内容 bounds、默认 100% 缩放,不按横向溢出静默缩放,更不假装窄表面是移动浏览器;Fit desktop是每标签的显式选择,必须在证明布局/媒体查询、可读性、用户输入、CDP 坐标、截图尺寸、每标签隔离与跨导航/reparent 的确定性复位之后才可用——同源 Chromium zoom 行为不构成「每标签 fit」的证明,坐标证明通过之前 Fit desktop 保持不可用;Expand复用既有侧面板/全屏外壳占据聊天内容区,配明确的 Restore chat 动作与焦点恢复,不新建窗口、页面或 CDP target。

回归保护与未决验证

回归测试的落点由规格指定且仓库中均存在:test/main/desktop/browser/YoBrowserPresenter.test.ts(presenter 的捕获/换父/生命周期)、test/main/desktop/preview/AgentPreviewCoordinator.test.ts(按来源的预览协调器)、test/renderer/components/AgentBrowserPiP.test.ts(渲染端 PiP 资格与合并)。实现必须持续保护的属性:页面/CDP 身份保持、来源/run 隔离、有界捕获、渲染端拿不到原生帧载荷、稳定原生位置、安全面板交接、run 级关闭、确定性拆除。

规格同时诚实列出了未决验证面:取消/失败/顶替与暂停/恢复路径;会话/路由/多窗口竞态;宿主 show/hide/minimize/restore;缩放/显示比例变化;页面与宿主崩溃;用户与 Agent 并发操作;物理平台的焦点、z-order、拖拽、键盘与无障碍行为。Windows/Linux 打包检查、受支持 macOS 检查、原生移动/帧延迟测量在证据记录前保持开放;任何实现变更仍须通过仓库格式、i18n、lint、typecheck 与相关聚焦测试。

非目标(明确不做)

  • 原生视频 PiP、documentPictureInPicture或全帧率共享纹理视频管线;
  • 多个同时可见 PiP,或仅由用户操作的浮动页面;
  • 向远端页面转发 PiP 输入,或让渲染进程持有远端 WebContents;
  • 隐藏页面的自动缩放、移动端 UA 切换、未测量的标签驱逐;
  • 持久化 PiP 几何、通用化独立浏览器标签架构、公开预览框架。

小结

DeepChat 的 Agent 浏览器 PiP 是一套「契约先行、单页现实、原生呈现」的分层设计:YoBrowserPresenter以每会话一份的SessionBrowserState保证页面身份唯一与父级数 ≤1;捕获管线用固定 viewport、有界 JPEG、单在途 tick 与 epoch 校验把帧流约束在可验证的预算内;AgentPreviewCoordinator用 claim + epoch + dismissedRuns 的六元组身份仲裁进程内唯一可见的原生呈现,并让 Browser 与 Computer Use 共享所有权而不合并契约。规格文档、NativeKit PiP 架构契约 与三个测试文件共同构成了从产品行为到实现证据的完整闭环。

【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询