参照 OpenAI Codex 桌面端 IAB 1:1 反编译实测 · 融合语雀 3-Backend 共存调研 · 适配 phoenix-pc 现有 BrowserPreview 与 packages/app/src/browser/runtime/ 体系
Author: 反编译 + 工程探索综合 · Branch: feat/invoke-dispatch · Date: 2026-05-15 · Status: DRAFT 待评审
apps/desktop/ 内基于 Electron WebContentsView 实现 Backend C(IAB),技术形态参考 Codex IAB 反编译实测;不重写 agent 协议,通过 IabTransport 把 webContents.debugger 适配成现有 ExtensionTransport 接口,直接复用 packages/app/src/browser/runtime/actions/* 全套 tier0 actions 与 SDK 剪枝管线。MVP 4 周交付,作为 Linux 兜底 + 无扩展用户 onboarding 补充 backend,不替代 A/B。
| 核心目标 | 给 Phoenix-PC 桌面端引入"自带 Chromium 内核 / 不依赖用户 Chrome 扩展"的浏览器 backend |
|---|---|
| 技术参考 | Codex.app v26.513.20950 反编译实测(app.asar 解包后逐函数验证) |
| 架构定位 | Backend C,与 Backend A(Desktop Relay+CDP)、Backend B(Web Extension Port)并行共存;复用顶层 Tool Schema / Snapshot Pruning / aria-tree parser / Ref Registry |
| 核心模块 | ① IAB BrowserSidebarManager ② CDP Bridge ③ Input 翻译器 ④ Lifecycle Registry ⑤ IabTransport ⑥ browser tool 'iab' 路由 |
| MVP 范围 | 侧边栏 + 独立窗口双形态 / agent 控制 snapshot+act+screenshot+navigate / 内嵌于主聊天窗口 |
| 不做 | Comment 标注层、外部 CLI 接入(native pipe + peer auth)、node_repl 范式、Confirmation Taxonomy 4 级、Tab claim/finalize/group |
| 工期 | MVP 4 周(单人) → alpha;P1+P2 跟进语雀 3-Backend 重构与 Codex 工具脚本接入 |
| 关键风险 | action 暂时复用 B 的 ExtensionTransport 接口,会让 action 实现"暂时变三套",P1 必须跟进抽 BrowserDriver |
本节基于直接探索 apps/desktop/src/main/browser/window.ts、packages/app/src/browser/runtime/、packages/sdk/src/vm/tools/function-tools/search/browser.ts、packages/sdk/src/browser/ 得出。
| 层 | 当前实现 | 规模 | 状态 |
|---|---|---|---|
| Tool 公开契约 | packages/sdk/src/vm/tools/function-tools/search/browser.ts — 双 backend 路由('sdk' Playwright / 'app' Extension+CDP) | 已成熟 | 复用,本次扩到三 backend |
| Backend A · Desktop | packages/sdk/src/browser/cdp/ + packages/sdk/src/browser/relay/ + packages/sdk/src/browser/playwright/;通过 WS Relay Server 接入用户本机 Chrome,经 chrome-extension/accio-browser-relay 落 chrome.debugger | 2891+2306+2636 行 | 本次不动 |
| Backend B · Web | packages/app/src/browser/runtime/ — AppBrowserRuntime + ExtensionTransport(chrome.runtime.connect external port)+ tier0-actions(snapshot/act/screenshot/navigate/scroll/console/errors/requests/waitFor/drag) | ~10 文件,actions/* 9 个 | 协议本次复用 |
| SDK 共享层 | packages/sdk/src/browser/snapshot/pruning/(intent/diff/enforce/ref-stab/shadow 5 阶段管线)+ aria-tree parser + ref-registry + state | 2521+3015 行 | 100% 复用 |
| 共用 Chrome 扩展 | assets/chrome-extension/accio-browser-relay/ — A/B 共享,MV3 | 多文件 | C 不依赖 |
| 独立 BrowserPreview(已存在!) | apps/desktop/src/main/browser/window.ts — 独立 BrowserWindow,WebContentsView 双 view(48px toolbar + content),partition persist:browser-preview,IPC browser-preview:*,支持 URL/HTML 加载、injectAuth、cookie 同步、前进后退/刷新/关闭 | 510 行 | 本次演化为 IAB 的 standalone 模式 |
Accessibility.getFullAXTree + ref=eN 这套(比 Codex IAB 自家的 in-page DOM walker 更标准);③ DOM 剪枝管线比 Codex 还成熟(语雀语)。本次 IAB 主要工作量是新写 transport + 改造窗口形态 + 接入 tool 路由,而不是从零写浏览器协议。
语雀调研 Q1 给出的结论(直接引用):
支持 C,作为「无登录态场景」+「Linux 兜底」+「无扩展用户的 onboarding 体验」补充 backend,不替代 A/B。
| 缺口场景 | 当前问题 | IAB 如何解决 |
|---|---|---|
| Linux 用户 | Backend A 依赖用户装 Chrome + 扩展,Backend B 依赖 Chrome 内打开;Linux 桌面 Chrome 装机率不稳定 | IAB 自带 Chromium(Electron 内置),零依赖 |
| 无扩展 onboarding | 用户首次使用时若未装 accio-browser-relay 扩展,无法立即体验 browser 能力 | IAB 立即可用,扩展引导可以放到后续 |
| 无登录态场景 | Backend A/B 借用户已登录的 Chrome session,某些场景需要 fresh session | IAB 默认 fresh session(单 partition,可清),agent 不污染用户登录态 |
| 对标 Codex/悟空 | 同类竞品都已有内置浏览器 | 能力对齐 |
navigate / snapshot / act / screenshot / scroll 等 tier0 全集persist:phoenix-iab,可显式 clear-storage'iab',可被 tool router 选中BrowserDriver 接口 + shared-actions/ 的 P0 重构 — IAB 与之并行,见 §13| # | 决策点 | 选定方向 | 理由 |
|---|---|---|---|
| D1 | 窗口形态 | 侧边栏 + 独立窗口双形态 | 侧边栏对齐 Codex IAB 体验;独立窗口复用现有 BrowserPreview 演化路径 |
| D2 | agent 通道(IPC vs CDP 命令风格) | 复用 ExtensionTransport 接口形态,新写 IabTransport | 零协议改造,自动复用 tier0-actions 全集与 SDK 剪枝管线 |
| D3 | 外部 CLI / 进程接入 | 不开放(MVP) | 仅服务桌面端内 agent runtime;若未来需要再加 native pipe 层 |
| D4 | Comment 标注层 | MVP 不做 | Codex 该层是 36MB React 包,价值高但与 agent 控制核心解耦,可独立后续做 |
| D5 | 重构顺序(C 先 vs P0 重构先) | 路线 X · 先做 C,actions 暂时复用 B | 用户原始诉求是"内置浏览器",路线 Y 工期 4-6 周且影响线上稳定性 ROI 太低;P1 跟进 BrowserDriver 抽象 |
| D6 | node_repl 范式 | 本次不实施,列入 §13 后续 Roadmap | 跨度涉及 SKILL.md / 模型路由 / 评测体系,应独立立项 |
BrowserDriver,会让 action 实现"暂时变三套"。本次方案选定的路线 X是:C 阶段不重写 action,通过 IabTransport 模拟 B 的 ExtensionTransport 接口,让 C 直接走 B 的 tier0-actions/* —— 这意味着 action 实现暂时还是两套(A 一套 + B/C 共用一套),P1 阶段跟进抽象。
BrowserSidebarManager ↔ phoenix IabSidebarManagerBrowserSessionRegistry ↔ phoenix IabSessionRegistrybrowser-use-iab-api capability ↔ phoenix browser tool 'iab' backendbrowser-use-iab-tab:${cdpTabId} ↔ phoenix 同款 tab id 命名persist:codex-browser-app-route:app ↔ phoenix persist:phoenix-iab(单 partition)data-browser-sidebar-conversation-id ↔ phoenix data-iab-conversation-idgP() in-page Input 翻译器 ↔ phoenix iabInputTranslator.js(从 Codex 抠出来移植)__codexIabExpectedInputTargetToken 元素 token ↔ phoenix __phoenixIabExpectedInputTargetToken
证据来源:/Applications/Codex.app/Contents/Resources/app.asar v26.513.20950 解包后逐函数验证(main-kSlb32Yb.js 1.21 MB / comment-preload.js 36.4 MB / app-session-O7kcZj7R.js 4.43 MB / bootstrap.js 3.7 KB / package.json)。
| # | 决策 | Codex 实测证据 | phoenix-pc 适配 |
|---|---|---|---|
| K1 | 嵌入容器:WebContentsView(非 <webview> tag,非 BrowserView) |
Codex 用的是 <webview> tag(webPreferences.webviewTag:true 命中 2 次,will-attach-webview 4 次,did-attach-webview 5 次) |
选 WebContentsView(主动偏离 Codex)。理由:① <webview> 已被 Electron 标记 deprecated,② phoenix-pc 现有 BrowserPreview 已用 WebContentsView,生态一致,③ 性能/稳定性更好,④ 主进程直接管理,无需中转。代价:preload 没有 sendToHost,所有事件走 main 中转(我们也走 main,符合 sidebar manager 是单一事实来源的设计) |
| K2 | CDP 接入方式 | e.webContents.debugger.attach('1.3')(命中 2),debugger.sendCommand(命中 4)。不开 9222 端口 |
同款。直接用 Electron 内建 webContents.debugger,不暴露 OS 端口 |
| K3 | 实际启用的 CDP 域 | main.js 字符串字面量 grep 完整集合:Page.navigate / reload / captureScreenshot / getLayoutMetrics / getNavigationHistory / navigateToHistoryEntry / close / navigationBlockedInput.dispatchKeyEvent / dispatchMouseEvent / insertText / synthesizeScrollGesture ← 实际被翻译,未真正发出Emulation.setDeviceMetricsOverride / clearDeviceMetricsOverride / setEmulatedMediaTarget.getTargets / closeTarget ← emulatedDOM/Accessibility/DOMSnapshot/Network/Runtime 域:0 命中 |
phoenix-pc 既然要复用 tier0-actions(actions/snapshot.ts 用 Accessibility.getFullAXTree),IAB transport 必须真启用 DOM + Accessibility 域。Codex 的"只 Page/Emulation/Target"是因为它走 in-page DOM walker 不依赖 CDP DOM,我们走 phoenix 现有协议必须把 DOM/Accessibility 域打开。不显式 enable(直接 sendCommand 即可,Electron debugger 默认配置就支持) |
| K4 | Input 命令:翻译为 in-page JS,不真正发 CDP Input.* | main.js 函数 executeTranslatedInputCommand + LL(e)=`(${gP.toString()})(${JSON.stringify(e)});` + 5 KB gP() in-page 翻译器(elementFromPoint + shadow DOM + same-origin iframe + cross-origin 抛错 + PointerEvent/MouseEvent 双发 + __codexIabExpectedInputTargetToken 元素 token 校验 + 自动 focus label.control / 最近 focusable 父节点)。实际错误信息:"input CDP commands are translated to JavaScript to preserve focus" |
1:1 移植。把 Codex 的 gP() 抠出来改写成 apps/desktop/src/main/browser/iab/input-translator.ts,IabTransport 拦截 Input.* 命令时走 webContents.executeJavaScript(translatorIife)。理由:Codex 选这个路径是为了"agent 操作期间 Owner 主聊天框不失焦",对 phoenix-pc 同样关键 |
| K5 | 截图双策略 | capturePage()(默认,2 处)+ Page.captureScreenshot(captureBeyondViewport 全页时,1 处)。函数 TL() 专门拆 viewport vs 全页路径,captureBeyondViewport 必须特判 |
同款。实现 iabScreenshot(targetId, opts):默认 capturePage(),opts.fullPage===true 走 Page.captureScreenshot({ captureBeyondViewport:true })。性能优势:capturePage 走 GPU compositor,比 CDP 截图快 2-3x |
| K6 | partition 隔离策略 | 单 partition:persist:codex-browser-app-route:app(常量 JL='persist:codex-browser-app-route:',Yp(e)=encodeURIComponent);conversationId 通过 data-browser-sidebar-conversation-id HTML 属性挂在 <webview> 上(常量 qL);存储数据通过 session.fromPartition().clearStorageData({ storages:['cookies'/'siteData'/'cache'] }) 显式清 |
同款:partition persist:phoenix-iab(单值);conversationId 用 contentView.webContents.executeJavaScript('document.documentElement.dataset.iabConversationId="..."') 或在 IabSessionRegistry 维护;提供 iab:clear-storage IPC 给用户。不按 conversation 分 partition(否则同站点要重复登录,UX 差) |
| K7 | CDP 命令必须包超时 | 函数 RL(promise, timeout, msg);字段 cdpCommandTimeoutMs |
同款。IabTransport 内置 cdpCommandTimeoutMs(默认 30s),所有 sendCommand 包 race |
| K8 | Page.captureScreenshot 的 captureBeyondViewport 必须特判 |
函数 TL() 显式拆分:viewport 截图与全页截图走两条路径,commandParams.captureBeyondViewport===true 时额外读 clip.width/height 校验 |
同款。IabCdpBridge 截图路由必须按 captureBeyondViewport 分流;不可统一走一条 |
| K9 | cross-origin iframe 显式抛错 | gP() 翻译器:碰到 cross-origin iframe 调用 d()=throw Error("Input targets inside cross-origin or inaccessible iframes are not currently supported in the in-app browser") |
同款。错误信息改写为 phoenix 命名空间;不静默吞掉,让 agent 能感知到失败原因 |
| K10 | 元素 token 校验防 race | __codexIabExpectedInputTargetToken:翻译器执行前先在目标元素打 token,翻译过程中读 elementFromPoint 拿到元素再校验 token,不一致就抛 "Focused input target no longer matches the resolved locator" |
同款。改名 __phoenixIabExpectedInputTargetToken。这是高速 agent 操作下"页面 mutate 导致点错"的核心防护 |
| K11 | will-attach-* 强制安全配置 |
Codex 在 non_mcp_app_sandbox_webview 分支显式:sandbox=true; contextIsolation=true; nodeIntegration=false; webviewTag=false(防 webview 套娃逃逸) |
WebContentsView 不需要 will-attach-webview 钩子,但创建 view 时 webPreferences 必须:sandbox:true, contextIsolation:true, nodeIntegration:false, webviewTag:false |
| K12 | navigation blocked 接管 | Page.navigationBlocked 事件 + 内部白名单(hide-local-server / unhide-local-server) |
MVP 实现 Page.navigationBlocked 监听 + 钓鱼站基础白名单;hide-local-server 推到 P2 |
| K13 | dialog auto-accept | auto-accept alert / beforeunload,confirm/prompt 显式处理(否则 agent 会卡死) |
同款。contentView.webContents.session.on('will-prevent-unload', ...) + setWindowOpenHandler |
| K14 | quit 时清理 | 每个 IAB tab 关闭时:debugger.detach() + cleanupDebuggerForTabBestEffort |
IabSessionRegistry 在 contentView.webContents destroy / app quit 时统一 detach |
| K15 | 命名约定 | IAB_LIFECYCLE(49 处日志)、browser-use-iab-api、browser-use-iab-tab:${cdpTabId}、tabIdsByPageKey(8)、routeKeysByTabId(7) |
phoenix 改名:PHOENIX_IAB_LIFECYCLE、phoenix-iab-api、phoenix-iab-tab:${cdpTabId}、tabIdsByPageKey、routeKeysByTabId(沿用 Codex 命名,因为表达力好) |
phoenix-pc/
├── apps/desktop/
│ └── src/
│ ├── main/
│ │ └── browser/
│ │ ├── window.ts # 现有(独立 BrowserPreview)
│ │ ├── normalize-url.ts # 现有
│ │ └── iab/ ★ 本次新增
│ │ ├── index.ts ★ 入口:registerIabSidebar()
│ │ ├── sidebar-manager.ts ★ IabSidebarManager(双形态切换)
│ │ ├── session-registry.ts ★ IabSessionRegistry(tabIdsByPageKey 等)
│ │ ├── cdp-bridge.ts ★ IabCdpBridge(debugger.attach/sendCommand)
│ │ ├── input-translator.ts ★ in-page 翻译器(从 Codex gP() 移植)
│ │ ├── iab-transport.ts ★ 适配 ExtensionTransport 接口
│ │ ├── screenshot.ts ★ capturePage / Page.captureScreenshot 双策略
│ │ ├── navigation-guard.ts ★ Page.navigationBlocked 接管
│ │ ├── dialog-handler.ts ★ alert/beforeunload auto-accept
│ │ ├── ipc-handlers.ts ★ 'phoenix-iab:*' channel 路由
│ │ └── constants.ts ★ partition 名 / token 名 / 超时常量
│ ├── preload/
│ │ └── browser/
│ │ ├── content.ts # 现有(BrowserPreview)
│ │ ├── toolbar.ts # 现有
│ │ └── iab/ ★ 本次新增
│ │ ├── iab-content.ts ★ guest webContents preload(轻量,只暴露 inspect token)
│ │ └── iab-toolbar.ts ★ toolbar webContents preload
│ └── renderer/
│ ├── browser/
│ │ ├── toolbar.html # 现有
│ │ └── toolbar.ts # 现有
│ └── iab/ ★ 本次新增
│ ├── sidebar.tsx ★ React sidebar 组件(嵌入主聊天 UI)
│ ├── toolbar.html ★ IAB toolbar HTML
│ ├── toolbar.tsx ★ IAB toolbar React 组件
│ └── styles.css ★ IAB UI 样式
│
├── packages/
│ ├── sdk/
│ │ └── src/
│ │ └── vm/tools/function-tools/search/
│ │ ├── browser.ts ⚙ 修改:增加 backend='iab' 路由
│ │ └── browser_utils/
│ │ ├── handle-iab-action.ts ★ 类似 handleRelayDirectAction 的 IAB 适配器
│ │ └── iab-runtime-bridge.ts ★ 主进程 ↔ tool 桥(走 ipcMain.handle)
│ │
│ └── app/
│ └── src/
│ └── browser/
│ └── runtime/
│ ├── extension-transport.ts # 现有(B 用)
│ ├── browser-runtime.ts # 现有
│ ├── tier0-actions.ts # 现有
│ ├── actions/ # 现有 9 个 action,本次 0 改动 ★
│ └── iab-transport.ts ★ 新增:IabTransport(实现 ExtensionTransport 接口)
│ # 注:与 main 进程的 iab-transport.ts 是配对关系,
│ # 这里是 renderer/agent 侧的桩,通过 IPC 转到 main
│
└── docs/browser/
├── embed-browser-research.md # 现有(已评审)
└── embed-browser-plan.html ★ 本文档
| 文件 | 改动 | 影响面 |
|---|---|---|
| apps/desktop/src/main/index.ts | 新增 registerIabSidebar({...}) 调用,注入 mainWindow / accessToken / cookie | 10 行内 |
| apps/desktop/src/preload/index.ts | 新增 iabBridge(contextBridge.exposeInMainWorld)给 Owner Renderer | 20 行内 |
| apps/desktop/electron.vite.config.ts | preload entry 增加 browser/iab/iab-content、browser/iab/iab-toolbar;renderer entry 增加 iab/toolbar | vite config block |
| packages/sdk/src/vm/tools/function-tools/search/browser.ts | 路由层增加 case 'iab': return handleIabAction(...) | 对应 backend selector 分支 |
| packages/sdk/src/vm/tools/function-tools/search/browser_shared/contracts.ts | BrowserBackend 类型增加 'iab' | 类型层 |
| 主聊天窗口 layout 组件(待定位) | 右侧布局留出 IAB sidebar 插槽,可拖拽宽度;支持 detach 按钮把 IAB 拆出独立窗口 | 需要确认 layout 组件路径 |
核心逻辑:
mainWindow.contentView.addChildView(toolbarView))。bounds 通过监听 mainWindow resize + Renderer 的 sidebar-resize IPC 同步previewWindow 形态;新创建 BrowserWindow 容纳 toolbar+content。从 sidebar 切换时:① 把 toolbarView/contentView 从 mainWindow detach,② 创建 standalone BrowserWindow,③ 把两个 view attach 过去,④ guest webContents 不重建,debugger 不 detach,会话不丢// apps/desktop/src/main/browser/iab/sidebar-manager.ts (骨架)
import { BrowserWindow, WebContentsView, session } from 'electron'
import { IAB_PARTITION, TOOLBAR_HEIGHT } from './constants'
export type IabMode = 'sidebar' | 'standalone'
export class IabSidebarManager {
private toolbarView: WebContentsView | null = null
private contentView: WebContentsView | null = null
private hostWindow: BrowserWindow | null = null
private mode: IabMode = 'sidebar'
private sidebarWidth = 480
constructor(private opts: {
mainWindowProvider: () => BrowserWindow | null
onTabReady: (tabId: string, webContents: Electron.WebContents) => void
}) {}
async open(url: string, conversationId: string, mode: IabMode = 'sidebar') {
this.ensureViews()
await this.setMode(mode)
this.setConversationId(conversationId)
await this.contentView!.webContents.loadURL(url)
}
private ensureViews() {
if (this.contentView) return
const sess = session.fromPartition(IAB_PARTITION)
this.toolbarView = new WebContentsView({
webPreferences: {
preload: getPreloadPath('browser/iab/iab-toolbar'),
sandbox: false, // toolbar 是我们自己的 UI,需要 IPC
contextIsolation: true,
nodeIntegration: false,
session: sess,
},
})
this.contentView = new WebContentsView({
webPreferences: {
preload: getPreloadPath('browser/iab/iab-content'),
sandbox: true, // guest 页面,严格沙盒
contextIsolation: true,
nodeIntegration: false,
webviewTag: false,
session: sess,
},
})
// CDP 桥与生命周期注册由 IabCdpBridge / IabSessionRegistry 接管
this.opts.onTabReady('iab-tab-1', this.contentView.webContents)
}
async setMode(mode: IabMode) {
if (this.mode === mode && this.hostWindow) return
// 1. detach from current host
if (this.hostWindow && !this.hostWindow.isDestroyed()) {
this.hostWindow.contentView.removeChildView(this.toolbarView!)
this.hostWindow.contentView.removeChildView(this.contentView!)
}
// 2. resolve new host
if (mode === 'sidebar') {
this.hostWindow = this.opts.mainWindowProvider()
} else {
this.hostWindow = this.createStandaloneWindow()
}
// 3. attach
this.hostWindow!.contentView.addChildView(this.toolbarView!)
this.hostWindow!.contentView.addChildView(this.contentView!)
this.mode = mode
this.layoutBounds()
}
private layoutBounds() {
if (!this.hostWindow) return
const b = this.hostWindow.getContentBounds()
if (this.mode === 'sidebar') {
const x = b.width - this.sidebarWidth
this.toolbarView!.setBounds({ x, y: 0, width: this.sidebarWidth, height: TOOLBAR_HEIGHT })
this.contentView!.setBounds({ x, y: TOOLBAR_HEIGHT, width: this.sidebarWidth, height: b.height - TOOLBAR_HEIGHT })
} else {
this.toolbarView!.setBounds({ x: 0, y: 0, width: b.width, height: TOOLBAR_HEIGHT })
this.contentView!.setBounds({ x: 0, y: TOOLBAR_HEIGHT, width: b.width, height: b.height - TOOLBAR_HEIGHT })
}
}
}
// apps/desktop/src/main/browser/iab/cdp-bridge.ts
import type { WebContents } from 'electron'
import { IAB_CDP_TIMEOUT_MS } from './constants'
import { translateInputCommand } from './input-translator'
export class IabCdpBridge {
constructor(private wc: WebContents) {}
async attach() {
if (!this.wc.debugger.isAttached()) {
this.wc.debugger.attach('1.3')
}
this.wc.debugger.on('message', (_e, method, params) => this.dispatchEvent(method, params))
}
async sendCommand(method: string, params: Record<string, unknown> = {}) {
// K4 · Input.* 翻译为 in-page JS
if (method.startsWith('Input.')) {
return translateInputCommand(this.wc, method, params)
}
// K5 · captureBeyondViewport 走真实 CDP
// K7 · 必须包超时
return withTimeout(
this.wc.debugger.sendCommand(method, params),
IAB_CDP_TIMEOUT_MS,
`Timed out running CDP command "${method}"`,
)
}
detach() {
if (this.wc.debugger.isAttached()) {
try { this.wc.debugger.detach() } catch {}
}
}
private cdpEventListeners = new Set<(method: string, params: unknown) => void>()
onCdpEvent(cb: (method: string, params: unknown) => void) {
this.cdpEventListeners.add(cb)
return () => this.cdpEventListeners.delete(cb)
}
private dispatchEvent(method: string, params: unknown) {
for (const cb of this.cdpEventListeners) cb(method, params)
}
}
function withTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T> {
return new Promise((resolve, reject) => {
let done = false
const t = setTimeout(() => { if (!done) { done = true; reject(new Error(msg)) } }, ms)
t.unref?.()
p.then(v => { if (!done) { done = true; clearTimeout(t); resolve(v) } },
e => { if (!done) { done = true; clearTimeout(t); reject(e) } })
})
}
来源:Codex main-kSlb32Yb.js 函数 gP(),完整 5KB,从反编译里直接抠出。本次方案原样移植(改名 / 去掉 Codex 业务命名空间)。
// apps/desktop/src/main/browser/iab/input-translator.ts
import type { WebContents } from 'electron'
const TRANSLATOR_IIFE = `(${iabInputTranslator.toString()})`
export async function translateInputCommand(
wc: WebContents,
method: string,
params: Record<string, unknown>,
) {
const expr = `${TRANSLATOR_IIFE}(${JSON.stringify({ method, params })});`
const r = await wc.executeJavaScript(expr, true /* userGesture */)
if (r && typeof r === 'object' && (r as any).ok === true) return {}
const err = (r && (r as any).error) || 'unknown translation failure'
throw new Error(`Unable to translate ${method} in IAB: ${err}`)
}
// 注入到 page 内执行的翻译器(不是真的从这里 export,需要改成字符串模板)
function iabInputTranslator(cmd: { method: string; params: any }) {
// ── 关键实现要点(完整版从 Codex gP() 1:1 抠出) ──
// 1. elementFromPoint(x, y) + shadow DOM 递归 + same-origin iframe 递归
// 2. cross-origin iframe 显式抛错(K9)
// 3. PointerEvent + MouseEvent 双发(modern + legacy 兼容)
// 4. mousedown → focus(label.control 或最近 focusable 父节点) → click 自动序列
// 5. wheel → 自定义滚动容器查找 + scrollBy({behavior:'auto'})
// 6. __phoenixIabExpectedInputTargetToken 元素 token 校验(K10)
// 7. 鼠标键映射:left=0 / middle=1 / right=2 / back=3 / forward=4
return { ok: true /* | error: 'msg' */ }
}
// apps/desktop/src/main/browser/iab/screenshot.ts
import type { WebContents } from 'electron'
export async function iabScreenshot(
wc: WebContents,
bridge: IabCdpBridge,
opts: { fullPage?: boolean; clip?: { x: number; y: number; width: number; height: number } } = {},
): Promise<Buffer> {
// K5 · 默认走 capturePage(),GPU compositor,2-3x 快
if (!opts.fullPage && !opts.clip) {
const img = await wc.capturePage()
return img.toPNG()
}
// K8 · 全页或带 clip 走真实 CDP,必须特判 captureBeyondViewport
const params: Record<string, unknown> = {
format: 'png',
captureBeyondViewport: opts.fullPage === true,
fromSurface: true,
}
if (opts.clip) params.clip = opts.clip
const r = await bridge.sendCommand('Page.captureScreenshot', params) as { data: string }
return Buffer.from(r.data, 'base64')
}
// apps/desktop/src/main/browser/iab/constants.ts
export const IAB_PARTITION = 'persist:phoenix-iab' // K6 · 单 partition
export const IAB_CDP_TIMEOUT_MS = 30_000 // K7
export const IAB_CONVERSATION_ATTR = 'iab-conversation-id' // 挂到 dataset 里
export const IAB_INPUT_TOKEN_KEY = '__phoenixIabExpectedInputTargetToken' // K10
export const TOOLBAR_HEIGHT = 48
设置 conversationId 不放在 <webview> data-* 上(我们用 WebContentsView 没有 webview 元素),改放在 IabSessionRegistry 的 routeKey 里,事件回流时通过 webContents.id 反查。partition 全局只有一个,登录态跨会话共享(对齐 Codex 体验)。
清理:
// 提供 IPC: 'phoenix-iab:clear-storage' { storages: ['cookies'|'siteData'|'cache'] }
import { session } from 'electron'
async function clearIabStorage(storages: string[]) {
const sess = session.fromPartition(IAB_PARTITION)
await Promise.all(storages.map(s => {
if (s === 'cache') return sess.clearCache()
return sess.clearStorageData({ storages: s === 'cookies' ? ['cookies'] : ['cookies', 'localstorage', 'indexdb', 'serviceworkers'] })
}))
}
对应 Codex 的 tabIdsByPageKey / routeKeysByTabId / debuggerListeners / cdpEventListeners 注册表,字段命名沿用 Codex(表达力好)。
// apps/desktop/src/main/browser/iab/session-registry.ts
import type { WebContents } from 'electron'
import { IabCdpBridge } from './cdp-bridge'
export interface IabTabState {
tabId: string // 'phoenix-iab-tab:1'
cdpTabId: string // 与 tabId 同义,用于对外暴露
conversationId: string
windowId: number // 宿主 BrowserWindow.id
routeKey: string // `${windowId}:${conversationId}`
webContents: WebContents
bridge: IabCdpBridge
url: string
title: string
faviconUrl: string | null
}
export class IabSessionRegistry {
private tabsById = new Map<string, IabTabState>()
private tabIdsByPageKey = new Map<string, string>() // routeKey → tabId
private routeKeysByTabId = new Map<string, string>()
private seq = 0
registerTab(conversationId: string, windowId: number, wc: WebContents, bridge: IabCdpBridge) {
const tabId = `phoenix-iab-tab:${++this.seq}`
const routeKey = `${windowId}:${conversationId}`
const state: IabTabState = {
tabId, cdpTabId: tabId, conversationId, windowId, routeKey,
webContents: wc, bridge, url: '', title: '', faviconUrl: null,
}
this.tabsById.set(tabId, state)
this.tabIdsByPageKey.set(routeKey, tabId)
this.routeKeysByTabId.set(tabId, routeKey)
wc.once('destroyed', () => this.unregisterTab(tabId))
return state
}
unregisterTab(tabId: string) {
const t = this.tabsById.get(tabId)
if (!t) return
t.bridge.detach()
this.tabsById.delete(tabId)
this.routeKeysByTabId.delete(tabId)
this.tabIdsByPageKey.delete(t.routeKey)
}
getTabByConversation(conversationId: string, windowId: number) {
const tabId = this.tabIdsByPageKey.get(`${windowId}:${conversationId}`)
return tabId ? this.tabsById.get(tabId) : null
}
getTab(tabId: string) { return this.tabsById.get(tabId) }
}
关键:让 C 复用 B 的 packages/app/src/browser/runtime/actions/* 不改一行代码。做法:在 packages/app/src/browser/runtime/iab-transport.ts 实现一个与 ExtensionTransport 同型的 transport,内部走 IPC 转发到 main 进程的 IabCdpBridge。
// packages/app/src/browser/runtime/iab-transport.ts
// 与 extension-transport.ts 同型 — 只在桌面端 main 进程或注入 IPC bridge 的 renderer 用
export interface IabTransportConfig {
send: (method: string, params: unknown) => Promise<unknown> // 由 main 进程注入
}
export class IabTransport {
isConnected = true
constructor(private cfg: IabTransportConfig) {}
/**
* tier0-actions 调用形态:
* transport.call('CDP.send', { targetId, cdpMethod:'Page.navigate', cdpParams:{url} })
* transport.call('snapshot', {...})
* transport.call('aria-snapshot', {...}) // 由 actions/snapshot.ts 决定
*/
async call(method: string, params: unknown): Promise<unknown> {
return this.cfg.send(method, params)
}
}
// main 进程注入的 send 实现:
// async function iabSendImpl(method, params) {
// if (method === 'CDP.send') {
// const { targetId, cdpMethod, cdpParams } = params as any
// const tab = registry.getTab(targetId)
// if (!tab) throw new Error(`Unknown IAB tab: ${targetId}`)
// return tab.bridge.sendCommand(cdpMethod, cdpParams ?? {})
// }
// throw new Error(`IAB transport: method '${method}' not supported`)
// }
BrowserDriver 接口(cdpSend / capturePage / clearStorage / ...),IabTransport 这一层就被替换为 EmbeddedDriver implements BrowserDriver。本次先打这个临时桥。
// packages/sdk/src/vm/tools/function-tools/search/browser_shared/contracts.ts (修改)
- export type BrowserBackend = 'sdk' | 'app'
+ export type BrowserBackend = 'sdk' | 'app' | 'iab'
// packages/sdk/src/vm/tools/function-tools/search/browser.ts (路由层修改)
const backend: BrowserBackend = resolveBackend(options, runtime)
switch (backend) {
case 'sdk':
return executePlaywrightAction(...)
case 'app':
return handleRelayDirectAction(...)
case 'iab': // ★ 新增
return handleIabAction(action, params, options) // ★
}
// packages/sdk/src/vm/tools/function-tools/search/browser_utils/handle-iab-action.ts (新增)
export async function handleIabAction(action, params, opts) {
// 通过 IPC 桥(IabRuntimeBridge)转发到 main 进程,main 进程内构造 IabTransport
// 再走 packages/app/src/browser/runtime/tier0-actions.ts 的 executeTier0Action()
return await iabRuntimeBridge.execute(action, params)
}
backend 选择策略(resolveBackend):
| 条件 | 选择 |
|---|---|
用户显式 backend: 'iab' | iab |
| Linux 平台(无扩展) | iab |
| 未装 accio-browser-relay 扩展 | iab(onboarding 兜底) |
| 有扩展且 desktop 客户端 | app(现状,优先) |
| web 端 | app(走扩展) |
| 其他 | sdk(Playwright) |
| Channel | 方向 | 载荷 | 说明 |
|---|---|---|---|
phoenix-iab:open | renderer → main · invoke | { url, conversationId, mode?: 'sidebar'\|'standalone' } | 打开/激活 IAB |
phoenix-iab:command | renderer → main · send | { type:'navigate'\|'back'\|'forward'\|'reload'\|'close'\|'clear-storage', ... } | toolbar 操作 + 维护命令 |
phoenix-iab:set-mode | renderer → main · send | { mode: 'sidebar'\|'standalone' } | 形态切换 |
phoenix-iab:resize-sidebar | renderer → main · send | { width:number } | 侧边栏宽度变更 |
phoenix-iab:state | main → renderer · send | { tabId, url, title, canGoBack, canGoForward, mode, isAgentControlling } | 状态推送给 toolbar / sidebar |
phoenix-iab:agent-execute | (in-process)main 内 · invoke | { action, params } | browser tool 调用 IAB 的内部桥(non-IPC,直接函数调用) |
phoenix-iab:navigation-blocked | main → renderer · send | { url, reason } | 导航拦截事件 |
当前 apps/desktop/src/main/browser/window.ts(510 行)是一个独立 BrowserWindow 形态的 BrowserPreview。本次 IAB 不删除它,而是按下面方式演化:
| 能力 | 当前 BrowserPreview | 本次 IAB(MVP) | 未来归宿 |
|---|---|---|---|
| 独立窗口形态 | ✅ 唯一形态 | ✅ standalone 模式 | 统一为 IAB standalone,删 window.ts 独立逻辑 |
| 侧边栏形态 | ❌ | ✅ sidebar 模式 | IAB 主形态 |
| partition | persist:browser-preview | persist:phoenix-iab | 合并为 persist:phoenix-iab;数据迁移可选 |
| injectAuth(注入 Bearer/Cookie) | ✅ | ✅ 复用同款 webRequest.onBeforeSendHeaders 逻辑 | 沉到 IAB sidebar-manager 内 |
| cookie 同步(defaultSession → 自有 partition) | ✅ | ✅ 复用 | 沉到 IAB |
| send-to-conversation(从 toolbar 把 URL/截图发回对话) | ✅ | P0 复用 | IAB toolbar 内置同款功能 |
| agent CDP 控制 | ❌ | ✅ 核心新增 | IAB 唯一来源 |
| IPC 命名空间 | browser-preview:* | phoenix-iab:* | 新代码全走 phoenix-iab,旧 channel 保留 1 个 release 周期向后兼容 |
injectAuth / syncCookies / normalizeUrl 等),不动 window.ts 主体;② IAB 直接 import 这些 helper;③ alpha 验证后再决定是否把 window.ts 整体改成调用 IAB standalone。
| Week | 交付物 | 关键工作 | 验收 |
|---|---|---|---|
| W1 骨架 |
IAB 能打开页面、双形态切换、toolbar 可用 |
① 创建 apps/desktop/src/main/browser/iab/ 目录与 8 个文件骨架 ② 实现 IabSidebarManager 双 view 创建 + sidebar/standalone 切换 + bounds 同步③ renderer/iab/sidebar.tsx 在主聊天右侧渲染容器(可拖宽 / detach 按钮) ④ toolbar HTML/TS(从现有 renderer/browser/toolbar.html 复用 + 扩展) ⑤ phoenix-iab:open / command / set-mode / resize-sidebar / state 5 个 IPC 通道
|
手动测试:聊天里点按钮 → 右侧出现 IAB 加载 google.com → 切 standalone → 切回 sidebar,会话保留 |
| W2 CDP 桥 + Input 翻译 |
agent 能通过 CLI/REPL 直接调 CDP 命令 |
① IabCdpBridge.attach + sendCommand + 超时包装 + event listener② Input 翻译器:把 Codex gP() 5KB 移植成 TS,改名 phoenix 命名空间③ 元素 token 机制(K10) ④ 截图双策略(K5/K8) ⑤ navigation-guard / dialog-handler / window-open ⑥ IabSessionRegistry(K15 命名)
|
node REPL 跑通:bridge.sendCommand('Page.navigate', {url}) / 'DOM.getDocument' / 'Accessibility.getFullAXTree' / 'Input.dispatchMouseEvent'(走翻译器) |
| W3 tool 接入 |
browser tool 'iab' backend 上线,模型可用 |
① BrowserBackend 类型扩 'iab',resolveBackend 加策略② IabTransport(packages/app)+ iabRuntimeBridge(packages/sdk)+ handleIabAction③ main 进程暴露 phoenix-iab:agent-execute 内部调用接口④ apps/desktop/src/main/index.ts 接入 registerIabSidebar⑤ Clean storage / agent-controlling 状态指示(toolbar 加锁图标) |
真实 agent 跑 5 个任务:Google 搜索 / 表单填写 / 多 tab(同一 IAB 标签内多次 navigate)/ 滚动加载 / 截图回流。结果跟 Backend B 走 Chrome Extension 等价 |
| W4 打磨 + 评测 |
alpha 可发布 |
① 错误路径:CDP 超时、debugger detach、cross-origin iframe 抛错信息 ② 性能: capturePage vs CDP 截图 benchmark / 多次 snapshot 内存稳定性③ Linux 平台跑通(electron-builder linux target) ④ 集成测试 4-5 条: apps/desktop/src/main/browser/iab/__tests__/ + 录屏回归⑤ docs/browser/embed-browser-plan.html 转成发布文档,补"用户首次使用引导" |
① CI 通过;② alpha 发版灰度 10 个内部用户;③ 跑通 30 个真实用户任务,与 Backend B 对比 ≥95% 一致(snapshot 字节、action 成功率) |
| 类别 | 标准 |
|---|---|
| 功能 | ① IAB 能在主聊天右侧正常渲染,可拖宽,可 detach 成独立窗口;② toolbar 前进/后退/刷新/关闭 / 地址栏可用;③ agent 通过 browser tool 'iab' backend 能跑通 navigate / snapshot / act / screenshot / scroll(tier0 全集) |
| 性能 | ① 截图 P95 ≤ 200ms(viewport)/ ≤ 800ms(全页);② snapshot P95 ≤ 1s(中等页面);③ 形态切换无白屏闪烁 |
| 稳定性 | ① 30 个真实任务跑完,IAB tab 内存稳定,debugger 不泄漏;② 关闭 sidebar 后再打开,partition 数据保留;③ Cmd+Q 退出时 debugger 全部 detach |
| 跨平台 | macOS arm64/x64 + Windows x64 + Linux x64 三平台 IAB 都可用 |
| 对齐 Backend B | 同样 30 个任务跑 Backend B vs Backend C,snapshot 字节差 ≤5%、action 成功率差 ≤2%、screenshot 像素对比 PSNR ≥ 30dB |
| 对齐 Codex 风格 | K1-K15 中除了 K1 主动偏离(用 WebContentsView 而非 webview)外全部对齐 |
| # | 风险 | 来源 | 缓解 |
|---|---|---|---|
| R1 | Input 命令翻译器对 cross-origin iframe 不支持 | Codex gP() 显式抛错 | 错误信息明确告知 agent;P2 评估是否补 OOPIF 路径(走真 CDP Input.dispatchMouseEvent,接受 focus 抢占代价) |
| R2 | WebContentsView 在某些 Electron 版本有 bounds 计算 bug | 电影业反馈 + Electron issue tracker | 锁定 Electron 版本;form 切换时强制 layoutBounds() 二次刷新(参考 BrowserPreview 现有 syncToolbarBounds 模式) |
| R3 | action 暂时变三套实现(SDK Playwright / B tier0-actions / C 借 B 同款 → 仍是两套但 transport 三套) | 语雀指出 | 明确 P1 跟进 BrowserDriver 抽象;在 IabTransport 注释里贴 ADR 链接 |
| R4 | partition 单值意味着所有会话共享 cookie/localStorage;某些场景需要"会话级隔离" | Codex 同款,UX 决策 | 提供 iab:clear-storage 显式清;后续如有强需求,加 partition-per-conversation 选项,但默认共享 |
| R5 | navigation 拦截白名单需要业务侧定义 | Codex 有 hide-local-server | MVP 用最小白名单(本机 dev server + 知名钓鱼站黑名单);P2 接入 phoenix 现有的 url 安全策略 |
| R6 | AccessibilityTree 在某些站点(如 SPA + 大量 generic 节点)体积爆炸 | 已知问题 | 复用 SDK snapshot/pruning/ 5 阶段管线;extreme case 走 ref-stab 阶段降级 |
| R7 | Electron debugger 不能并发:同一 webContents 同一时刻只能一个 debugger 客户端 | Electron 文档 | IabCdpBridge 设计为单实例 per webContents;主进程串行化命令(已通过 Promise 链保证) |
| R8 | guest 页面运行 service worker / 长 JS 阻塞会卡死 executeJavaScript | Codex 用 cdpCommandTimeoutMs 包了一层 | K7 已覆盖;超时后明确告知 agent,不让 agent 死等 |
| R9 | IAB 与现有 BrowserPreview 同时存在期间 partition 命名不同,登录态隔离 | 本方案演化决策 | W1 实施时先在 BrowserPreview 加迁移逻辑(读旧 partition cookie 写到新 partition);或显式文档说明"alpha 期间登录可能需要重新输入" |
| R10 | preload 路径在 build vs dev 不一致 | electron-vite 现状 | 统一用 __dirname + 相对路径,参考现有 main/browser/window.ts 的 getPreloadPath() 模式 |
| 方向 | 本次未选 | 原因 | 什么时候应该重新考虑 |
|---|---|---|---|
<webview> tag(完全照 Codex) | 选了 WebContentsView | ① Electron 已 deprecated;② phoenix-pc 现有 BrowserPreview 已用 WebContentsView;③ 性能 + 主进程统一管理 | 除非发现 WebContentsView 在 sidebar attach 模式下有不可解 bug |
| 路线 Y(先抽 BrowserDriver 再做 C) | 选了路线 X | 用户原始诉求是"内置浏览器",路线 Y 工期 4-6 周且影响 A/B 线上稳定性 | P1 阶段独立 RFC,届时把 IAB 一起重构进 BrowserDriver |
| node_repl + JS SDK 范式 | 本次不实施 | 跨度涉及 SKILL.md / 模型路由 / 评测体系;独立议题 | 独立立项,语雀 §4 已有完整 MVP 路径 |
| Comment 标注层(36MB React 包) | MVP 不做 | 价值高但与 agent 控制核心解耦,且 Codex 自己用了 30+MB 资源,phoenix 短期吞不下 | P2,作为产品化"用户标注网页元素发问"功能时 |
| 外部 CLI 接入(native pipe + peer auth) | 不开放 | phoenix-pc 没有独立 CLI 进程要远程操作 IAB | 如果未来出 Phoenix CLI 需要 |
| Codex 自家 in-page DOM walker(snapshot 不走 CDP) | 不采用 | phoenix tier0-actions/snapshot.ts 已用更标准的 Accessibility.getFullAXTree + 5 阶段剪枝管线,语雀评价"比 Codex 更强" | 不会重新考虑 |
| 多 partition(per-conversation) | 单 partition | 对齐 Codex,UX 更好(同站点不重复登录) | 有"会话强隔离"硬需求时,作为 opt-in 选项 |
| Sparkle/Squirrel/MSIX 自更新 | 沿用现有 phoenix-pc electron-builder 自更新 | phoenix-pc 已有 dist:beta/stable 全套 | 不重新考虑 |
这些来自语雀调研 §3.4 / §B.3,IAB 上线后跟进。本次 plan 不实施,但列出来让 reader 看到全景。
| 优先级 | 动作 | 来源 | 收益 |
|---|---|---|---|
| P1 | 抽 BrowserDriver 接口 + shared-actions/,把 A/B/C 的 action 实现统一为单一来源 |
语雀 §3.3,本次 R3 | 消除"action 实现两套(P1 后)/ 三套(本次 X 路线)"的代码漂移 |
| P1 | node_repl + JS SDK MVP(setupAtlasRuntime 等价物 + display() + 翻译 SKILL.md) | 语雀 §4 | LLM 调用降 50-70% / token 降 40%+ / 端到端耗时降 50%+ |
| P1 | 移植 Tab claim / finalize / group 协议(SDK 接口 + 扩展 chrome.tabs.group API) | 语雀 §B.2 / Codex SKILL.md § User Tab Claiming | UX 大幅提升;Backend A/B 受益,IAB 简化版即可 |
| P1 | 抄 Codex 三脚本:installed-browsers.js / chrome-is-running.js / open-chrome-window.js |
语雀 §B.1 | Backend A 初次启动体验完整闭环;~3 天工作量纯收益 |
| P1 | 翻译 Codex SKILL.md 60% 段落作为模型 system prompt | 语雀 §B.1 | 模型行为质量直接提升一档 |
| P2 | 合并 Codex generic/listitem/group collapse 启发式到 SDK enforce 阶段 | 语雀 §B.2 / Codex domSnapshot | snapshot 体积再降 ~20% |
| P2 | 暴露 Playwright locator 链给模型(getByRole/Text/Label) | 语雀 §B.2 / Codex Playwright shim | 消除"必须先 snapshot 拿 ref"的硬约束 |
| P2 | Confirmation Taxonomy 4 级文档化 + 映射到 Phoenix 现有 confirm 路径 | 语雀 §B.2 / Codex SKILL.md § Browser Use Confirmations Policy | 授权策略对外可解释 |
| P2 | IAB Comment 标注层(hover outline + 评论气泡 + anchor 注入 agent 上下文) | Codex comment-preload.js | 用户能直接在网页上标注问 agent;但与 agent 控制核心解耦 |
| P3 | 剪贴板 API(read/write/readText/writeText) | Codex tab.clipboard | 补漏 |
| P3 | iframe / frameLocator 支持 | Codex Playwright shim | 补漏 |
| P3 | 下载流(Browser.setDownloadBehavior) |
Codex downloadMedia | 补漏 |
| P3 | 外部 CLI 接入(unix socket + peer auth) | Codex native pipe + browser-use-peer-authorization.node | 如未来出 Phoenix CLI 需要 |
| P3 | Touch ID 二次确认(macOS LocalAuthentication) | Codex 预留接入点 | 支付/敏感场景 |
| 文件 | 角色 |
|---|---|
| apps/desktop/src/main/browser/window.ts | 独立 BrowserPreview(510 行),W1 拆 helper 复用 |
| apps/desktop/src/main/browser/normalize-url.ts | URL 归一化,IAB 直接复用 |
| apps/desktop/src/preload/browser/content.ts | BrowserPreview content preload(参考结构) |
| apps/desktop/src/preload/browser/toolbar.ts | BrowserPreview toolbar preload(参考结构) |
| apps/desktop/src/renderer/browser/toolbar.html | BrowserPreview toolbar HTML(参考样式) |
| apps/desktop/src/main/index.ts | 主进程入口,W3 接入 registerIabSidebar |
| apps/desktop/electron.vite.config.ts | 构建配置,W1 加 preload/renderer entry |
| packages/sdk/src/vm/tools/function-tools/search/browser.ts | tool 公开契约,W3 加 'iab' backend 路由 |
| packages/sdk/src/vm/tools/function-tools/search/browser_shared/contracts.ts | BrowserBackend 类型,W3 扩 'iab' |
| packages/app/src/browser/runtime/extension-transport.ts | ExtensionTransport 接口,W3 IabTransport 同型 |
| packages/app/src/browser/runtime/browser-runtime.ts | AppBrowserRuntime,W3 复用 |
| packages/app/src/browser/runtime/tier0-actions.ts | action 路由,W3 0 改动复用 |
| packages/app/src/browser/runtime/actions/*.ts | 9 个 action 实现,W3 0 改动复用 |
| packages/sdk/src/browser/snapshot/pruning/ | 5 阶段剪枝管线,自动复用 |
| docs/browser/embed-browser-research.md | 第一轮调研(已评审,部分结论已修正) |
| docs/app-browser-cloud-plan.md | App Browser 云端方案(背景) |
| docs/app-browser-playwright-migration.md | tier0-actions 优化背景 |
| docs/chrome-cdp-analysis.md | CDP 直连分析(背景) |
# 1) parse asar pickle header(无需 npm 安装,纯 python)
python3 - <<'EOF'
import struct, json
with open("/Applications/Codex.app/Contents/Resources/app.asar","rb") as f:
head = f.read(16)
outer = struct.unpack('<I', head[4:8])[0]
jlen = struct.unpack('<I', head[12:16])[0]
f.seek(16); manifest = json.loads(f.read(jlen).decode('utf-8'))
print(list(manifest['files']['.vite']['files']['build']['files'].keys()))
EOF
# 输出包含:bootstrap.js / main-kSlb32Yb.js / app-session-O7kcZj7R.js /
# comment-preload.js / preload.js / sandbox-preload.js / ...
# 2) 关键字符串 grep(确认 K1-K15 全部命中)
grep -oE "webviewTag|will-attach-webview|debugger\.attach|capturePage|IAB_LIFECYCLE|\
browser-use-iab|tabIdsByPageKey|routeKeysByTabId|captureBeyondViewport|\
__codexIabExpectedInputTargetToken|persist:codex-browser-app-route|non_mcp_app_sandbox_webview" \
/tmp/codex-extract/main.js | sort | uniq -c
| Codex 名 | phoenix-pc 名 |
|---|---|
BrowserSidebarManager | IabSidebarManager |
BrowserSessionRegistry | IabSessionRegistry |
browser-use-iab-api | phoenix-iab-api |
browser-use-iab-tab:${cdpTabId} | phoenix-iab-tab:${cdpTabId} |
persist:codex-browser-app-route:app | persist:phoenix-iab |
data-browser-sidebar-conversation-id | (改用 IabSessionRegistry 维护,不挂 dataset) |
__codexIabExpectedInputTargetToken | __phoenixIabExpectedInputTargetToken |
IAB_LIFECYCLE 日志前缀 | PHOENIX_IAB_LIFECYCLE |
BROWSER_PREVIEW_OPEN_CHANNEL(现有) | phoenix-iab:open(新) |
本文档为技术方案 DRAFT,等待评审。落地实施前还需:① 主聊天窗口 layout 组件路径定位与改造方案确认;② phoenix-pc Electron 版本与 WebContentsView API 兼容性核对;③ 与 packages/sdk owner 同步 backend selector 策略变更。