From 1782e0fe4012b9c52056a23f8ab0e1a07f560c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=90=AF=E7=81=8F?= Date: Thu, 2 Apr 2026 02:25:00 +0800 Subject: [PATCH 1/2] fix(doubao-app): connect to correct CDP target instead of background page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doubao desktop app exposes multiple CDP targets. The scoring logic picked the background page (doubao-background) over the actual chat page because its URL-as-title contained "doubao", boosting its score above the real chat page (title "豆包"). This caused all commands (send, ask, read) to fail with "No textarea found". - Add `targetFilter` field to ElectronAppEntry for per-app preferred target - Set doubao-app targetFilter to 'doubao-chat/chat' - Penalize background/new-tab-page URLs and URL-like titles in scoring - Thread cdpTargetFilter through execution → runtime → CDPBridge Closes #634, closes #506 Co-Authored-By: Claude Opus 4.6 --- src/browser/cdp.ts | 8 ++++---- src/electron-apps.ts | 4 +++- src/execution.ts | 6 ++++-- src/runtime.ts | 5 +++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/browser/cdp.ts b/src/browser/cdp.ts index 94e23a77..bee0b3d3 100644 --- a/src/browser/cdp.ts +++ b/src/browser/cdp.ts @@ -46,7 +46,7 @@ export class CDPBridge implements IBrowserFactory { private _pending = new Map void; reject: (err: Error) => void; timer: ReturnType }>(); private _eventListeners = new Map void>>(); - async connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string }): Promise { + async connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string; cdpTargetFilter?: string }): Promise { if (this._ws) throw new Error('CDPBridge is already connected. Call close() before reconnecting.'); const endpoint = opts?.cdpEndpoint ?? process.env.OPENCLI_CDP_ENDPOINT; @@ -55,7 +55,7 @@ export class CDPBridge implements IBrowserFactory { let wsUrl = endpoint; if (endpoint.startsWith('http')) { const targets = await fetchJsonDirect(`${endpoint.replace(/\/$/, '')}/json`) as CDPTarget[]; - const target = selectCDPTarget(targets); + const target = selectCDPTarget(targets, opts?.cdpTargetFilter); if (!target || !target.webSocketDebuggerUrl) { throw new Error('No inspectable targets found at CDP endpoint'); } @@ -250,8 +250,8 @@ function matchesCookieDomain(cookieDomain: string, targetDomain: string): boolea || normalizedTargetDomain.endsWith(`.${normalizedCookieDomain}`); } -function selectCDPTarget(targets: CDPTarget[]): CDPTarget | undefined { - const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET); +function selectCDPTarget(targets: CDPTarget[], appTargetFilter?: string): CDPTarget | undefined { + const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET ?? appTargetFilter); const ranked = targets .map((target, index) => ({ target, index, score: scoreCDPTarget(target, preferredPattern) })) diff --git a/src/electron-apps.ts b/src/electron-apps.ts index 5818bbd3..c49e8404 100644 --- a/src/electron-apps.ts +++ b/src/electron-apps.ts @@ -21,6 +21,8 @@ export interface ElectronAppEntry { displayName?: string; /** Additional launch args beyond --remote-debugging-port */ extraArgs?: string[]; + /** URL pattern to prefer when selecting a CDP target (e.g. 'doubao-chat/chat') */ + targetFilter?: string; } export const builtinApps: Record = { @@ -29,7 +31,7 @@ export const builtinApps: Record = { chatwise: { port: 9228, processName: 'ChatWise', bundleId: 'com.chatwise.app', displayName: 'ChatWise' }, notion: { port: 9230, processName: 'Notion', bundleId: 'notion.id', displayName: 'Notion' }, 'discord-app': { port: 9232, processName: 'Discord', bundleId: 'com.discord.app', displayName: 'Discord' }, - 'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao' }, + 'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao', targetFilter: 'doubao-chat/chat' }, antigravity: { port: 9234, processName: 'Antigravity', bundleId: 'dev.antigravity.app', displayName: 'Antigravity' }, chatgpt: { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' }, }; diff --git a/src/execution.ts b/src/execution.ts index 145505b1..e14cc297 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -20,7 +20,7 @@ import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMM import { emitHook, type HookContext } from './hooks.js'; import { checkDaemonStatus } from './browser/discover.js'; import { log } from './logger.js'; -import { isElectronApp } from './electron-apps.js'; +import { isElectronApp, getElectronApp } from './electron-apps.js'; import { resolveElectronEndpoint } from './launcher.js'; const _loadedModules = new Set(); @@ -173,10 +173,12 @@ export async function executeCommand( if (shouldUseBrowserSession(cmd)) { const electron = isElectronApp(cmd.site); let cdpEndpoint: string | undefined; + let cdpTargetFilter: string | undefined; if (electron) { // Electron apps: auto-detect, prompt restart if needed, launch with CDP cdpEndpoint = await resolveElectronEndpoint(cmd.site); + cdpTargetFilter = getElectronApp(cmd.site)?.targetFilter; } else { // Browser Bridge: fail-fast when daemon is up but extension is missing. // 300ms timeout avoids a full 2s wait on cold-start. @@ -212,7 +214,7 @@ export async function executeCommand( timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd), }); - }, { workspace: `site:${cmd.site}`, cdpEndpoint }); + }, { workspace: `site:${cmd.site}`, cdpEndpoint, cdpTargetFilter }); } else { // Non-browser commands: apply timeout only when explicitly configured. const timeout = cmd.timeoutSeconds; diff --git a/src/runtime.ts b/src/runtime.ts index d96b095f..834ae094 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -63,14 +63,14 @@ export function withTimeoutMs( /** Interface for browser factory (BrowserBridge or test mocks) */ export interface IBrowserFactory { - connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string }): Promise; + connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string; cdpTargetFilter?: string }): Promise; close(): Promise; } export async function browserSession( BrowserFactory: new () => IBrowserFactory, fn: (page: IPage) => Promise, - opts: { workspace?: string; cdpEndpoint?: string } = {}, + opts: { workspace?: string; cdpEndpoint?: string; cdpTargetFilter?: string } = {}, ): Promise { const browser = new BrowserFactory(); try { @@ -78,6 +78,7 @@ export async function browserSession( timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, workspace: opts.workspace, cdpEndpoint: opts.cdpEndpoint, + cdpTargetFilter: opts.cdpTargetFilter, }); return await fn(page); } finally { From 37bf7beca39f1124ee39e2fa685b6d6cd18433e0 Mon Sep 17 00:00:00 2001 From: jackwener Date: Fri, 3 Apr 2026 02:25:55 +0800 Subject: [PATCH 2/2] refactor(cdp): exclude background targets instead of targetFilter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the targetFilter plumbing (4 files, new interface field) with a single-line fix: exclude `background_page` and `service_worker` type targets from CDP selection entirely. Background pages should never be connection targets — they have no visible DOM and all selectors will fail. This is the root cause of #506/#634 (doubao-app connecting to empty background page). Simpler fix: 1 line added vs 4 files modified. No new interface fields, no per-app configuration needed. --- src/browser/cdp.ts | 9 +++++---- src/electron-apps.ts | 4 +--- src/execution.ts | 6 ++---- src/runtime.ts | 5 ++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/browser/cdp.ts b/src/browser/cdp.ts index bee0b3d3..9770ca8c 100644 --- a/src/browser/cdp.ts +++ b/src/browser/cdp.ts @@ -46,7 +46,7 @@ export class CDPBridge implements IBrowserFactory { private _pending = new Map void; reject: (err: Error) => void; timer: ReturnType }>(); private _eventListeners = new Map void>>(); - async connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string; cdpTargetFilter?: string }): Promise { + async connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string }): Promise { if (this._ws) throw new Error('CDPBridge is already connected. Call close() before reconnecting.'); const endpoint = opts?.cdpEndpoint ?? process.env.OPENCLI_CDP_ENDPOINT; @@ -55,7 +55,7 @@ export class CDPBridge implements IBrowserFactory { let wsUrl = endpoint; if (endpoint.startsWith('http')) { const targets = await fetchJsonDirect(`${endpoint.replace(/\/$/, '')}/json`) as CDPTarget[]; - const target = selectCDPTarget(targets, opts?.cdpTargetFilter); + const target = selectCDPTarget(targets); if (!target || !target.webSocketDebuggerUrl) { throw new Error('No inspectable targets found at CDP endpoint'); } @@ -250,8 +250,8 @@ function matchesCookieDomain(cookieDomain: string, targetDomain: string): boolea || normalizedTargetDomain.endsWith(`.${normalizedCookieDomain}`); } -function selectCDPTarget(targets: CDPTarget[], appTargetFilter?: string): CDPTarget | undefined { - const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET ?? appTargetFilter); +function selectCDPTarget(targets: CDPTarget[]): CDPTarget | undefined { + const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET); const ranked = targets .map((target, index) => ({ target, index, score: scoreCDPTarget(target, preferredPattern) })) @@ -274,6 +274,7 @@ function scoreCDPTarget(target: CDPTarget, preferredPattern?: RegExp): number { if (!haystack.trim() && !type) return Number.NEGATIVE_INFINITY; if (haystack.includes('devtools')) return Number.NEGATIVE_INFINITY; + if (type === 'background_page' || type === 'service_worker') return Number.NEGATIVE_INFINITY; let score = 0; diff --git a/src/electron-apps.ts b/src/electron-apps.ts index c49e8404..5818bbd3 100644 --- a/src/electron-apps.ts +++ b/src/electron-apps.ts @@ -21,8 +21,6 @@ export interface ElectronAppEntry { displayName?: string; /** Additional launch args beyond --remote-debugging-port */ extraArgs?: string[]; - /** URL pattern to prefer when selecting a CDP target (e.g. 'doubao-chat/chat') */ - targetFilter?: string; } export const builtinApps: Record = { @@ -31,7 +29,7 @@ export const builtinApps: Record = { chatwise: { port: 9228, processName: 'ChatWise', bundleId: 'com.chatwise.app', displayName: 'ChatWise' }, notion: { port: 9230, processName: 'Notion', bundleId: 'notion.id', displayName: 'Notion' }, 'discord-app': { port: 9232, processName: 'Discord', bundleId: 'com.discord.app', displayName: 'Discord' }, - 'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao', targetFilter: 'doubao-chat/chat' }, + 'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao' }, antigravity: { port: 9234, processName: 'Antigravity', bundleId: 'dev.antigravity.app', displayName: 'Antigravity' }, chatgpt: { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' }, }; diff --git a/src/execution.ts b/src/execution.ts index e14cc297..145505b1 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -20,7 +20,7 @@ import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMM import { emitHook, type HookContext } from './hooks.js'; import { checkDaemonStatus } from './browser/discover.js'; import { log } from './logger.js'; -import { isElectronApp, getElectronApp } from './electron-apps.js'; +import { isElectronApp } from './electron-apps.js'; import { resolveElectronEndpoint } from './launcher.js'; const _loadedModules = new Set(); @@ -173,12 +173,10 @@ export async function executeCommand( if (shouldUseBrowserSession(cmd)) { const electron = isElectronApp(cmd.site); let cdpEndpoint: string | undefined; - let cdpTargetFilter: string | undefined; if (electron) { // Electron apps: auto-detect, prompt restart if needed, launch with CDP cdpEndpoint = await resolveElectronEndpoint(cmd.site); - cdpTargetFilter = getElectronApp(cmd.site)?.targetFilter; } else { // Browser Bridge: fail-fast when daemon is up but extension is missing. // 300ms timeout avoids a full 2s wait on cold-start. @@ -214,7 +212,7 @@ export async function executeCommand( timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd), }); - }, { workspace: `site:${cmd.site}`, cdpEndpoint, cdpTargetFilter }); + }, { workspace: `site:${cmd.site}`, cdpEndpoint }); } else { // Non-browser commands: apply timeout only when explicitly configured. const timeout = cmd.timeoutSeconds; diff --git a/src/runtime.ts b/src/runtime.ts index 834ae094..d96b095f 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -63,14 +63,14 @@ export function withTimeoutMs( /** Interface for browser factory (BrowserBridge or test mocks) */ export interface IBrowserFactory { - connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string; cdpTargetFilter?: string }): Promise; + connect(opts?: { timeout?: number; workspace?: string; cdpEndpoint?: string }): Promise; close(): Promise; } export async function browserSession( BrowserFactory: new () => IBrowserFactory, fn: (page: IPage) => Promise, - opts: { workspace?: string; cdpEndpoint?: string; cdpTargetFilter?: string } = {}, + opts: { workspace?: string; cdpEndpoint?: string } = {}, ): Promise { const browser = new BrowserFactory(); try { @@ -78,7 +78,6 @@ export async function browserSession( timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, workspace: opts.workspace, cdpEndpoint: opts.cdpEndpoint, - cdpTargetFilter: opts.cdpTargetFilter, }); return await fn(page); } finally {