Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 95 additions & 50 deletions packages/cli/src/commands/trace.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
/**
* trace 命令 - 录制用户操作
* trace 命令 - 录制操作 + 网络请求的统一时间线
*/

import type { Request } from "@bb-browser/shared";
import type { Request, TraceEntry } from "@bb-browser/shared";
import { sendCommand } from "../client.js";

interface TraceOptions {
json?: boolean;
tabId?: string | number;
since?: number | string;
type?: string;
filter?: string;
limit?: number;
requestId?: string;
}

export async function traceCommand(
subCommand: 'start' | 'stop' | 'status',
subCommand: 'start' | 'stop' | 'status' | 'events' | 'body',
options: TraceOptions = {}
): Promise<void> {
const response = await sendCommand({
const request: Record<string, unknown> = {
method: "trace",
traceCommand: subCommand,
tabId: options.tabId,
} as Request);
};

// events-specific params
if (subCommand === 'events') {
if (options.since !== undefined) request.since = Number(options.since);
if (options.type) request.traceType = options.type;
if (options.filter) request.filter = options.filter;
if (options.limit) request.limit = Number(options.limit);
}

// body-specific params
if (subCommand === 'body') {
request.requestId = options.requestId;
}

const response = await sendCommand(request as Request);

if (options.json) {
console.log(JSON.stringify(response));
Expand All @@ -34,70 +54,95 @@ export async function traceCommand(
switch (subCommand) {
case "start": {
const status = data?.traceStatus;
console.log("开始录制用户操作");
console.log(`标签页 ID: ${status?.tabId || 'N/A'}`);
console.log("\n在浏览器中进行操作,完成后运行 'bb-browser trace stop' 停止录制");
console.log("Trace started");
if (status?.tracedTabs?.length) {
console.log(`Tracing tabs: ${status.tracedTabs.join(', ')}`);
}
console.log("\nOperate the browser, then run 'bb-browser trace events' to see the timeline.");
break;
}

case "stop": {
const events = data?.traceEvents || [];

console.log(`录制完成,共 ${events.length} 个事件\n`);

const status = data?.traceStatus;
console.log(`Trace stopped (${status?.eventCount ?? 0} events recorded)`);
console.log("Data preserved — use 'trace events' to query, 'trace start' to begin a new session.");
break;
}

case "status": {
const status = data?.traceStatus;
if (status?.recording) {
console.log(`Recording (${status.eventCount} events)`);
if (status.tracedTabs?.length) {
console.log(`Tabs: ${status.tracedTabs.join(', ')}`);
}
} else if (status?.eventCount) {
console.log(`Stopped (${status.eventCount} events in buffer)`);
} else {
console.log("No active trace session");
}
break;
}

case "events": {
const events = (data?.traceEvents || []) as TraceEntry[];
const cursor = (data as Record<string, unknown>)?.cursor;

if (events.length === 0) {
console.log("没有录制到任何操作");
console.log("No events");
break;
}

for (let i = 0; i < events.length; i++) {
const event = events[i];
const refStr = event.ref !== undefined ? `@${event.ref}` : '';

switch (event.type) {
case 'navigation':
console.log(`${i + 1}. 导航到: ${event.url}`);
break;
case 'click':
console.log(`${i + 1}. 点击 ${refStr} [${event.elementRole}] "${event.elementName || ''}"`);
break;
case 'fill':
console.log(`${i + 1}. 填充 ${refStr} [${event.elementRole}] "${event.elementName || ''}" <- "${event.value}"`);
break;
case 'select':
console.log(`${i + 1}. 选择 ${refStr} [${event.elementRole}] "${event.elementName || ''}" <- "${event.value}"`);

// Print timeline
for (const e of events) {
const tabStr = `[${e.tab}]`;
switch (e.type) {
case 'action': {
const src = e.source === 'human' ? ' (human)' : '';
const ref = e.ref !== undefined ? ` ref=${e.ref}` : '';
const val = e.value ? ` "${e.value}"` : '';
const key = e.key ? ` ${e.key}` : '';
const info = e.text ? ` "${e.text}"` : '';
const role = e.role ? ` [${e.role}]` : '';
console.log(` ${e.seq} ${tabStr} action${src} ${e.action}${ref}${role}${info}${val}${key}`);
break;
case 'check':
console.log(`${i + 1}. ${event.checked ? '勾选' : '取消勾选'} ${refStr} [${event.elementRole}] "${event.elementName || ''}"`);
}
case 'request': {
const trigger = e.triggerSeq ? ` trigger:${e.triggerSeq}` : '';
const body = e.body ? ` (body: ${e.body.length}B)` : '';
console.log(` ${e.seq} ${tabStr} request ${e.method} ${e.url}${body}${trigger}`);
break;
case 'press':
console.log(`${i + 1}. 按键 ${event.key}`);
}
case 'response': {
const size = e.bodySize ? ` ${e.bodySize}B` : '';
const mime = e.mimeType ? ` ${e.mimeType}` : '';
console.log(` ${e.seq} ${tabStr} response ${e.requestId} → ${e.status}${mime}${size}`);
break;
case 'scroll':
console.log(`${i + 1}. 滚动 ${event.direction} ${event.pixels}px`);
}
case 'navigation': {
const from = e.from ? ` (from: ${e.from})` : '';
console.log(` ${e.seq} ${tabStr} navigation ${e.url}${from}`);
break;
default:
console.log(`${i + 1}. ${event.type}`);
}
}
}

const status = data?.traceStatus;
console.log(`\n状态: ${status?.recording ? '录制中' : '已停止'}`);

console.log(`\n${events.length} events, cursor: ${cursor}`);
break;
}

case "status": {
const status = data?.traceStatus;
if (status?.recording) {
console.log(`录制中 (标签页 ${status.tabId})`);
console.log(`已录制 ${status.eventCount} 个事件`);
case "body": {
const body = data?.traceBody;
if (!body) {
console.error("No body data returned");
break;
}
if (body.base64Encoded) {
console.log(`[base64 encoded, ${body.body.length} chars]`);
} else {
console.log("未在录制");
console.log(body.body);
}
break;
}

default:
throw new Error(`未知的 trace 子命令: ${subCommand}`);
}
}
54 changes: 48 additions & 6 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const TAB_REQUIRED_COMMANDS = new Set([
"snap", "screenshot", "get",
"click", "hover", "fill", "type", "check", "uncheck", "select",
"press", "scroll", "back", "forward", "reload", "close",
"frame", "dialog", "network", "console", "errors", "trace",
"frame", "dialog", "network", "console", "errors",
]);

// eval requires --tab unless --domain is provided
Expand Down Expand Up @@ -95,7 +95,11 @@ bb-browser - AI Agent 浏览器自动化工具
network requests [filter] 查看网络请求 (--tab required)
console [--clear] 查看/清空控制台 (--tab required)
errors [--clear] 查看/清空 JS 错误 (--tab required)
trace start|stop|status 录制用户操作 (--tab required)
trace start [--tab <id>] 录制操作+网络的统一时间线
trace events [--type action|request] 查看时间线(支持 --since/--filter/--tab)
trace body <requestId> 获取某个请求的 response body
trace stop 停止录制(数据保留)
trace status 查看录制状态
daemon [start|status|stop] 管理 daemon

选项:
Expand All @@ -108,6 +112,11 @@ bb-browser - AI Agent 浏览器自动化工具
-d, --depth <n> 限制树深度(snap 命令)
-s, --selector <sel> 限定 CSS 选择器范围(snap 命令)
--tab <tabId> 指定操作的标签页 ID
--since <seq> 增量查询(network/console/errors/trace events)
--type <t> 过滤事件类型(trace events: action/request/response)
--filter <str> URL 或文字过滤(trace events/network requests)
--limit <n> 限制返回条数
--request-id <id> 请求 ID(trace body)
--help, -h 显示帮助信息
--version, -v 显示版本号
`.trim();
Expand Down Expand Up @@ -220,6 +229,30 @@ function parseArgs(argv: string[]): ParsedArgs {
skipNext = true;
} else if (arg === "--status") {
skipNext = true;
} else if (arg === "--type") {
skipNext = true;
const nextIdx = args.indexOf(arg) + 1;
if (nextIdx < args.length) {
(result.flags as Record<string, unknown>).type = args[nextIdx];
}
} else if (arg === "--filter") {
skipNext = true;
const nextIdx = args.indexOf(arg) + 1;
if (nextIdx < args.length) {
(result.flags as Record<string, unknown>).filter = args[nextIdx];
}
} else if (arg === "--limit") {
skipNext = true;
const nextIdx = args.indexOf(arg) + 1;
if (nextIdx < args.length) {
(result.flags as Record<string, unknown>).limit = parseInt(args[nextIdx], 10);
}
} else if (arg === "--request-id") {
skipNext = true;
const nextIdx = args.indexOf(arg) + 1;
if (nextIdx < args.length) {
(result.flags as Record<string, unknown>)["request-id"] = args[nextIdx];
}
} else if (arg.startsWith("-")) {
// Unknown flags, ignore
} else if (result.command === null) {
Expand Down Expand Up @@ -559,12 +592,21 @@ async function main(): Promise<void> {
}

case "trace": {
const subCmd = parsed.args[0] as 'start' | 'stop' | 'status' | undefined;
if (!subCmd || !['start', 'stop', 'status'].includes(subCmd)) {
console.error("用法:bb-browser trace <start|stop|status> --tab <tabId>");
const subCmd = parsed.args[0] as 'start' | 'stop' | 'status' | 'events' | 'body' | undefined;
if (!subCmd || !['start', 'stop', 'status', 'events', 'body'].includes(subCmd)) {
console.error("用法:bb-browser trace <start|stop|status|events|body> [--tab <tabId>]");
process.exit(1);
}
await traceCommand(subCmd, { json: parsed.flags.json, tabId: globalTabId });
const f = parsed.flags as Record<string, unknown>;
await traceCommand(subCmd, {
json: parsed.flags.json,
tabId: globalTabId,
since: globalSince,
type: f.type as string | undefined,
filter: f.filter as string | undefined,
limit: f.limit as number | undefined,
requestId: (f["request-id"] as string | undefined) || parsed.args[1],
});
break;
}

Expand Down
Loading
Loading