mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat: add console observation command
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SessionManager } from "@/session-manager/manager";
|
||||
import type { CdpRunner } from "@/tools/shared";
|
||||
import type { ConsoleResult } from "@/transport/types";
|
||||
import { handleConsole } from "../console";
|
||||
|
||||
function fakeAgentWindow(ids: number[]) {
|
||||
let i = 0;
|
||||
return {
|
||||
create: vi.fn(async () => {
|
||||
const id = ids[i++];
|
||||
if (id === undefined) throw new Error("ran out of fake ids");
|
||||
return id;
|
||||
}),
|
||||
remove: vi.fn(async () => {}),
|
||||
ensureActiveTab: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeDeps(
|
||||
opts: {
|
||||
get?: (tabId: number) => Promise<chrome.tabs.Tab>;
|
||||
query?: (query: chrome.tabs.QueryInfo) => Promise<chrome.tabs.Tab[]>;
|
||||
result?: ConsoleResult;
|
||||
} = {},
|
||||
) {
|
||||
const result =
|
||||
opts.result ??
|
||||
({
|
||||
tab_id: 7,
|
||||
entries: [],
|
||||
next_since: 0,
|
||||
truncated: false,
|
||||
} satisfies ConsoleResult);
|
||||
const ensureConsoleCapture = vi.fn(async () => {});
|
||||
const consoleEntriesSince = vi.fn(() => result);
|
||||
const cdp = {
|
||||
send: vi.fn(),
|
||||
ensureConsoleCapture,
|
||||
consoleEntriesSince,
|
||||
} as unknown as CdpRunner;
|
||||
const tabsApi = {
|
||||
get:
|
||||
opts.get ??
|
||||
vi.fn(
|
||||
async (tabId: number) => ({ id: tabId, windowId: 100, active: true }) as chrome.tabs.Tab,
|
||||
),
|
||||
query:
|
||||
opts.query ?? vi.fn(async () => [{ id: 7, windowId: 100, active: true } as chrome.tabs.Tab]),
|
||||
};
|
||||
return { cdp, tabsApi, ensureConsoleCapture, consoleEntriesSince };
|
||||
}
|
||||
|
||||
describe("handleConsole", () => {
|
||||
it("reads the Agent Window active tab with safe defaults", async () => {
|
||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||
await sm.start("aa11");
|
||||
const deps = makeDeps({
|
||||
result: {
|
||||
tab_id: 7,
|
||||
entries: [{ sequence: 3, kind: "console", level: "log", text: "hello", truncated: false }],
|
||||
next_since: 3,
|
||||
truncated: false,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await handleConsole(sm, { session_id: "aa11" }, deps);
|
||||
|
||||
if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`);
|
||||
expect(res.entries[0].text).toBe("hello");
|
||||
expect(deps.ensureConsoleCapture).toHaveBeenCalledWith(7);
|
||||
expect(deps.consoleEntriesSince).toHaveBeenCalledWith(7, undefined, 50, 1000, false);
|
||||
});
|
||||
|
||||
it("reads an explicit tab and forwards bounded options", async () => {
|
||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||
await sm.start("aa11");
|
||||
const deps = makeDeps({
|
||||
get: vi.fn(async () => ({ id: 9, windowId: 200, active: true }) as chrome.tabs.Tab),
|
||||
});
|
||||
|
||||
await handleConsole(
|
||||
sm,
|
||||
{
|
||||
session_id: "aa11",
|
||||
tab_id: 9,
|
||||
since: 12,
|
||||
limit: 500,
|
||||
max_text_chars: 9999,
|
||||
include_stack: true,
|
||||
},
|
||||
deps,
|
||||
);
|
||||
|
||||
expect(deps.ensureConsoleCapture).toHaveBeenCalledWith(9);
|
||||
expect(deps.consoleEntriesSince).toHaveBeenCalledWith(9, 12, 200, 4096, true);
|
||||
});
|
||||
|
||||
it("rejects invalid bounds before touching CDP", async () => {
|
||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||
await sm.start("aa11");
|
||||
const deps = makeDeps();
|
||||
|
||||
const res = await handleConsole(sm, { session_id: "aa11", limit: 0 }, deps);
|
||||
|
||||
expect(res).toMatchObject({ code: "invalid_params", message: /limit/ });
|
||||
expect(deps.ensureConsoleCapture).not.toHaveBeenCalled();
|
||||
expect(deps.consoleEntriesSince).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hides other sessions' Agent Window tabs", async () => {
|
||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100, 101]) });
|
||||
await sm.start("aa11");
|
||||
await sm.start("bb22");
|
||||
const deps = makeDeps({
|
||||
get: vi.fn(async () => ({ id: 9, windowId: 101, active: true }) as chrome.tabs.Tab),
|
||||
});
|
||||
|
||||
const res = await handleConsole(sm, { session_id: "aa11", tab_id: 9 }, deps);
|
||||
|
||||
expect(res).toMatchObject({ code: "not_found" });
|
||||
expect(deps.ensureConsoleCapture).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,12 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { SessionManager } from "@/session-manager/manager";
|
||||
import type { ConnectionStateHandler, FrameHandler, Transport } from "@/transport/transport";
|
||||
import type { ConnectionState, ProtocolFrame, RequestFrame } from "@/transport/types";
|
||||
import type {
|
||||
ConnectionState,
|
||||
ConsoleResult,
|
||||
ProtocolFrame,
|
||||
RequestFrame,
|
||||
} from "@/transport/types";
|
||||
import { ToolDispatcher } from "../dispatcher";
|
||||
|
||||
function fakeTransport() {
|
||||
@@ -36,6 +41,10 @@ function makeRequest(method: string, params: unknown): RequestFrame {
|
||||
}
|
||||
|
||||
describe("ToolDispatcher", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("routes tool.session_start to the SessionManager and replies with the window id", async () => {
|
||||
const { transport, sent, deliver } = fakeTransport();
|
||||
const sessions = new SessionManager({
|
||||
@@ -78,6 +87,55 @@ describe("ToolDispatcher", () => {
|
||||
expect(sent[0]).toEqual({ id: "r-1", result: {} });
|
||||
});
|
||||
|
||||
it("routes tool.console through the CDP console buffer", async () => {
|
||||
vi.stubGlobal("chrome", {
|
||||
tabs: {
|
||||
query: vi.fn(async () => [{ id: 7, windowId: 4242, active: true }]),
|
||||
},
|
||||
});
|
||||
const { transport, sent, deliver } = fakeTransport();
|
||||
const sessions = new SessionManager({
|
||||
agentWindow: {
|
||||
create: vi.fn(async () => 4242),
|
||||
remove: vi.fn(async () => {}),
|
||||
ensureActiveTab: vi.fn(async () => {}),
|
||||
},
|
||||
});
|
||||
await sessions.start("aa11");
|
||||
const cdp = {
|
||||
send: vi.fn(),
|
||||
detachSession: vi.fn(async () => {}),
|
||||
ensureConsoleCapture: vi.fn(async () => {}),
|
||||
consoleEntriesSince: vi.fn(
|
||||
() =>
|
||||
({
|
||||
tab_id: 7,
|
||||
entries: [
|
||||
{ sequence: 1, kind: "console", level: "log", text: "hello", truncated: false },
|
||||
],
|
||||
next_since: 1,
|
||||
truncated: false,
|
||||
}) satisfies ConsoleResult,
|
||||
),
|
||||
};
|
||||
const dispatcher = new ToolDispatcher({ transport, sessions, cdp });
|
||||
dispatcher.start();
|
||||
|
||||
deliver(makeRequest("tool.console", { session_id: "aa11" }));
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(cdp.ensureConsoleCapture).toHaveBeenCalledWith(7);
|
||||
expect(sent[0]).toEqual({
|
||||
id: "r-1",
|
||||
result: {
|
||||
tab_id: 7,
|
||||
entries: [{ sequence: 1, kind: "console", level: "log", text: "hello", truncated: false }],
|
||||
next_since: 1,
|
||||
truncated: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("detaches CDP state before stopping a session", async () => {
|
||||
const { transport, sent, deliver } = fakeTransport();
|
||||
const sessions = new SessionManager({
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { ChromiumCdp } from "@/browser-driver/chromium-cdp";
|
||||
import type { SessionManager } from "@/session-manager/manager";
|
||||
import type { ConsoleParams, ConsoleResult, RpcError } from "@/transport/types";
|
||||
import {
|
||||
type CdpRunner,
|
||||
type ChromeTabsApi,
|
||||
chromeTabsApi,
|
||||
isRpcError,
|
||||
lookupSession,
|
||||
resolveTargetTab,
|
||||
} from "./shared";
|
||||
|
||||
const DEFAULT_CONSOLE_LIMIT = 50;
|
||||
const MAX_CONSOLE_LIMIT = 200;
|
||||
const DEFAULT_MAX_TEXT_CHARS = 1000;
|
||||
const MAX_TEXT_CHARS = 4096;
|
||||
|
||||
export interface ConsoleDeps {
|
||||
cdp: CdpRunner;
|
||||
tabsApi: ChromeTabsApi;
|
||||
}
|
||||
|
||||
function defaultConsoleDeps(): ConsoleDeps {
|
||||
return {
|
||||
cdp: new ChromiumCdp(),
|
||||
tabsApi: chromeTabsApi,
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleConsole(
|
||||
manager: SessionManager,
|
||||
params: ConsoleParams,
|
||||
deps: ConsoleDeps = defaultConsoleDeps(),
|
||||
): Promise<ConsoleResult | RpcError> {
|
||||
const ctxOrErr = lookupSession(manager, params, "console");
|
||||
if (isRpcError(ctxOrErr)) return ctxOrErr;
|
||||
const bounds = parseConsoleBounds(params);
|
||||
if (isRpcError(bounds)) return bounds;
|
||||
const target = await resolveTargetTab(manager, ctxOrErr, params.tab_id, deps.tabsApi);
|
||||
if (isRpcError(target)) return target;
|
||||
if (!deps.cdp.ensureConsoleCapture || !deps.cdp.consoleEntriesSince) {
|
||||
return { code: "cdp_failed", message: "console capture requires CDP console support" };
|
||||
}
|
||||
|
||||
try {
|
||||
await deps.cdp.ensureConsoleCapture(target.tabId);
|
||||
deps.cdp.trackSessionTab?.(ctxOrErr.sessionId, target.tabId);
|
||||
return deps.cdp.consoleEntriesSince(
|
||||
target.tabId,
|
||||
bounds.since,
|
||||
bounds.limit,
|
||||
bounds.maxTextChars,
|
||||
bounds.includeStack,
|
||||
);
|
||||
} catch (err) {
|
||||
return {
|
||||
code: "cdp_failed",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseConsoleBounds(params: ConsoleParams):
|
||||
| {
|
||||
since: number | undefined;
|
||||
limit: number;
|
||||
maxTextChars: number;
|
||||
includeStack: boolean;
|
||||
}
|
||||
| RpcError {
|
||||
const since = params.since;
|
||||
if (since !== undefined && (!Number.isSafeInteger(since) || since < 0)) {
|
||||
return { code: "invalid_params", message: "since must be a non-negative integer" };
|
||||
}
|
||||
const limit = boundedOptionalInteger(
|
||||
params.limit,
|
||||
DEFAULT_CONSOLE_LIMIT,
|
||||
MAX_CONSOLE_LIMIT,
|
||||
"limit",
|
||||
);
|
||||
if (isRpcError(limit)) return limit;
|
||||
const maxTextChars = boundedOptionalInteger(
|
||||
params.max_text_chars,
|
||||
DEFAULT_MAX_TEXT_CHARS,
|
||||
MAX_TEXT_CHARS,
|
||||
"max_text_chars",
|
||||
);
|
||||
if (isRpcError(maxTextChars)) return maxTextChars;
|
||||
return {
|
||||
since,
|
||||
limit,
|
||||
maxTextChars,
|
||||
includeStack: params.include_stack === true,
|
||||
};
|
||||
}
|
||||
|
||||
function boundedOptionalInteger(
|
||||
value: number | undefined,
|
||||
defaultValue: number,
|
||||
maxValue: number,
|
||||
field: string,
|
||||
): number | RpcError {
|
||||
if (value === undefined) return defaultValue;
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
return { code: "invalid_params", message: `${field} must be a positive integer` };
|
||||
}
|
||||
return Math.min(value, maxValue);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { OVERLAY_AUTOMATION_BYPASS } from "@/lib/overlay-bridge";
|
||||
import type { SessionManager } from "@/session-manager/manager";
|
||||
import type { Transport } from "@/transport/transport";
|
||||
import type {
|
||||
ClickParams,
|
||||
ConsoleParams,
|
||||
EvaluateParams,
|
||||
FillParams,
|
||||
GetHtmlParams,
|
||||
@@ -11,17 +13,19 @@ import type {
|
||||
PressParams,
|
||||
ProtocolFrame,
|
||||
ReloadParams,
|
||||
RequestHelpParams,
|
||||
RequestFrame,
|
||||
RequestHelpParams,
|
||||
ResponseFrame,
|
||||
RpcError,
|
||||
SelectParams,
|
||||
ScreenshotParams,
|
||||
SelectParams,
|
||||
SnapshotParams,
|
||||
WaitForNavigationParams,
|
||||
} from "@/transport/types";
|
||||
import { isRequestFrame } from "@/transport/types";
|
||||
import { handleConsole } from "./console";
|
||||
import { handleEvaluate } from "./evaluate";
|
||||
import { defaultWatchTabNavigation, handleRequestHelp } from "./human-loop";
|
||||
import { handleClick, handleFill, handlePress, handleSelect } from "./interaction";
|
||||
import {
|
||||
handleNavigate,
|
||||
@@ -42,16 +46,15 @@ import {
|
||||
type SessionStartParams,
|
||||
type SessionStopParams,
|
||||
} from "./session";
|
||||
import { OVERLAY_AUTOMATION_BYPASS } from "@/lib/overlay-bridge";
|
||||
import { chromeTabsApi } from "./shared";
|
||||
import {
|
||||
type BorrowConfirmationApprover,
|
||||
handleTabBorrow,
|
||||
handleTabClose,
|
||||
handleTabCreate,
|
||||
handleTabList,
|
||||
handleTabReturn,
|
||||
handleTabSelect,
|
||||
type BorrowConfirmationApprover,
|
||||
type TabBorrowParams,
|
||||
type TabCloseParams,
|
||||
type TabCreateParams,
|
||||
@@ -59,7 +62,6 @@ import {
|
||||
type TabReturnParams,
|
||||
type TabSelectParams,
|
||||
} from "./tabs";
|
||||
import { defaultWatchTabNavigation, handleRequestHelp } from "./human-loop";
|
||||
import { handleWaitForNavigation } from "./waits";
|
||||
|
||||
export interface DispatcherDeps {
|
||||
@@ -277,6 +279,12 @@ export class ToolDispatcher {
|
||||
? { cdp: this.cdp, tabsApi: chromeTabsCaptureApi, captureApi: chromeTabsCaptureApi }
|
||||
: undefined,
|
||||
);
|
||||
case "tool.console":
|
||||
return handleConsole(
|
||||
this.sessions,
|
||||
req.params as ConsoleParams,
|
||||
this.cdp ? { cdp: this.cdp, tabsApi: chromeTabsApi } : undefined,
|
||||
);
|
||||
case "tool.snapshot":
|
||||
return handleSnapshot(
|
||||
this.sessions,
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
// exactly the same sandbox + visibility rules as the M6 observation
|
||||
// handlers (review parity).
|
||||
|
||||
import { normaliseRef } from "@/session-manager/ref-store";
|
||||
import type { SessionContext, SessionManager } from "@/session-manager/manager";
|
||||
import type { RpcError } from "@/transport/types";
|
||||
import type { JavaScriptDialogInfo } from "@/transport/types";
|
||||
import type { DialogCursor } from "@/browser-driver/chromium-cdp";
|
||||
import type { SessionContext, SessionManager } from "@/session-manager/manager";
|
||||
import { normaliseRef } from "@/session-manager/ref-store";
|
||||
import type { ConsoleResult, JavaScriptDialogInfo, RpcError } from "@/transport/types";
|
||||
import { rpcError } from "./errors";
|
||||
|
||||
/**
|
||||
@@ -46,6 +45,14 @@ export interface CdpRunner {
|
||||
};
|
||||
dialogCursor?(tabId: number): DialogCursor;
|
||||
dialogsSince?(tabId: number, cursor: DialogCursor): JavaScriptDialogInfo[];
|
||||
ensureConsoleCapture?(tabId: number): Promise<void>;
|
||||
consoleEntriesSince?(
|
||||
tabId: number,
|
||||
since: number | undefined,
|
||||
limit: number,
|
||||
maxTextChars: number,
|
||||
includeStack: boolean,
|
||||
): ConsoleResult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user