feat(browser): 完成 Phase 10C orchestration 能力

新增固定流程块 orchestration 闭环,支持 wait_then_click、wait_then_type、run_replay_sequence 与 assert_query。
并补充 block 级失败定位与相关回归覆盖。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
walker1211andClaude Opus 4.7 committed 2026-04-20 15:00:15 +08:00
1 parent 2099145e2c
commit 77855035f4
2 files changed
+539

No files matched your search

+229
View File
@@ -69,6 +69,9 @@ const TOOL_CLEAR_RECORDING = 'browser_clear_recording';
const TOOL_START_REPLAY = 'browser_start_replay';
const TOOL_GET_REPLAY = 'browser_get_replay';
const TOOL_CANCEL_REPLAY = 'browser_cancel_replay';
const TOOL_START_ORCHESTRATION = 'browser_start_orchestration';
const TOOL_GET_ORCHESTRATION = 'browser_get_orchestration';
const TOOL_CANCEL_ORCHESTRATION = 'browser_cancel_orchestration';
const TOOL_TAKE_SCREENSHOT = 'browser_take_screenshot';
const TOOL_WAIT_FOR = 'browser_wait_for';
const TOOL_EVAL = 'browser_eval';
@@ -107,6 +110,9 @@ const TOOL_NAMES = [
TOOL_START_REPLAY,
TOOL_GET_REPLAY,
TOOL_CANCEL_REPLAY,
TOOL_START_ORCHESTRATION,
TOOL_GET_ORCHESTRATION,
TOOL_CANCEL_ORCHESTRATION,
TOOL_TAKE_SCREENSHOT,
TOOL_WAIT_FOR,
TOOL_EVAL,
@@ -148,6 +154,9 @@ let latestRecordingSession = null;
let nextReplayCounter = 1;
let activeReplaySession = null;
let latestReplaySession = null;
let nextOrchestrationCounter = 1;
let activeOrchestrationSession = null;
let latestOrchestrationSession = null;
const interceptRules = [];
const recentRequests = [];
const recentDownloads = [];
@@ -800,6 +809,38 @@ function getTools() {
additionalProperties: false,
},
},
{
name: TOOL_START_ORCHESTRATION,
description: 'Start an orchestration session that runs fixed browser workflow blocks on the selected page.',
inputSchema: {
type: 'object',
properties: {
blocks: { type: 'array', items: { type: 'object' } },
pageIndex: { type: 'integer', minimum: 0 },
pageId: { type: 'string' },
},
required: ['blocks'],
additionalProperties: false,
},
},
{
name: TOOL_GET_ORCHESTRATION,
description: 'Read the current orchestration status and result summary from session-local state.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
{
name: TOOL_CANCEL_ORCHESTRATION,
description: 'Cancel the active orchestration session and keep the summary in session-local state.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
{
name: TOOL_TAKE_SCREENSHOT,
description:
@@ -2351,6 +2392,12 @@ function formatRecordingSummary(session) {
].join('\n');
}
function createOrchestrationId() {
const orchestrationId = `orc_${String(nextOrchestrationCounter).padStart(4, '0')}`;
nextOrchestrationCounter += 1;
return orchestrationId;
}
function requireLatestReplay() {
if (!latestReplaySession) {
throw new Error('no replay available');
@@ -2358,6 +2405,13 @@ function requireLatestReplay() {
return latestReplaySession;
}
function requireLatestOrchestration() {
if (!latestOrchestrationSession) {
throw new Error('no orchestration available');
}
return latestOrchestrationSession;
}
function formatReplaySummary(session) {
return [
`replayId: ${session.replayId}`,
@@ -2372,6 +2426,20 @@ function formatReplaySummary(session) {
].join('\n');
}
function formatOrchestrationSummary(session) {
return [
`orchestrationId: ${session.orchestrationId}`,
`pageId: ${session.pageId}`,
`pageIndex: ${session.pageIndex}`,
`blockCount: ${session.blockCount}`,
`completedBlocks: ${session.completedBlocks}`,
`currentBlockIndex: ${session.currentBlockIndex}`,
`failedBlockIndex: ${session.failedBlockIndex === null ? 'none' : session.failedBlockIndex}`,
`error: ${session.error || 'none'}`,
`status: ${session.status}`,
].join('\n');
}
function formatRecordingDetail(session) {
const lines = [formatRecordingSummary(session), 'steps:'];
if (session.steps.length === 0) {
@@ -2441,6 +2509,31 @@ function validateReplayStep(step, replayPageId) {
}
}
const SUPPORTED_ORCHESTRATION_BLOCK_TYPES = new Set([
'wait_for_then_click',
'wait_for_then_type',
'wait_for_then_press_key',
'run_replay_sequence',
'assert_query',
]);
function requireOrchestrationBlocks(toolArgs) {
const blocks = Array.isArray(toolArgs.blocks) ? toolArgs.blocks : null;
if (!blocks || blocks.length === 0) {
throw new Error('blocks must be a non-empty array');
}
return blocks;
}
function validateOrchestrationBlock(block) {
if (!block || typeof block !== 'object') {
throw new Error('invalid orchestration block payload');
}
if (!SUPPORTED_ORCHESTRATION_BLOCK_TYPES.has(block.type)) {
throw new Error('unsupported orchestration block type');
}
}
function normalizeRecordedEvent(pageId, event) {
if (!event || typeof event !== 'object') {
return null;
@@ -4843,6 +4936,118 @@ async function handleCancelReplay() {
return formatReplaySummary(canceled);
}
async function executeOrchestrationBlock(session, block) {
if (block.type === 'wait_for_then_click') {
await handleWaitFor({ pageId: session.pageId, ...block.args.wait });
await handleClick({ pageId: session.pageId, ...block.args.click });
return;
}
if (block.type === 'wait_for_then_type') {
await handleWaitFor({ pageId: session.pageId, ...block.args.wait });
await handleType({ pageId: session.pageId, ...block.args.type });
return;
}
if (block.type === 'wait_for_then_press_key') {
await handleWaitFor({ pageId: session.pageId, ...block.args.wait });
await handlePressKey({ pageId: session.pageId, ...block.args.pressKey });
return;
}
if (block.type === 'run_replay_sequence') {
const replayResultText = await handleStartReplay({ pageId: session.pageId, steps: block.args.steps });
if (replayResultText.includes('status: failed')) {
throw new Error(replayResultText);
}
return;
}
if (block.type === 'assert_query') {
const queryText = await handleQuery({ pageId: session.pageId, ...block.args.query });
const expectedField = String(block.args.assert.field || '');
const expectedValue = String(block.args.assert.equals || '');
if (!queryText.includes(`${expectedField}: ${expectedValue}`)) {
throw new Error(`assert_query failed for field ${expectedField}`);
}
return;
}
throw new Error('unsupported orchestration block type');
}
async function executeOrchestrationBlocks(session) {
for (let index = 0; index < session.blocks.length; index += 1) {
const block = session.blocks[index];
session.currentBlockIndex = index;
validateOrchestrationBlock(block);
await executeOrchestrationBlock(session, block);
session.completedBlocks = index + 1;
}
}
async function handleStartOrchestration(toolArgs) {
if (activeOrchestrationSession) {
throw new Error('orchestration already active');
}
const pages = await listPageTargets();
if (pages.length === 0) {
throw new Error('Browser MCP did not find any page targets in the current Chrome session.');
}
const { page, pageIndex } = resolveTargetPage(pages, toolArgs, selectedPageId);
if (!page.webSocketDebuggerUrl) {
throw new Error(`Browser MCP page ${pageIndex} does not expose a websocket debugger URL.`);
}
const blocks = requireOrchestrationBlocks(toolArgs);
const session = {
orchestrationId: createOrchestrationId(),
pageId: page.id,
pageIndex,
status: 'running',
blockCount: blocks.length,
completedBlocks: 0,
currentBlockIndex: 0,
failedBlockIndex: null,
error: null,
blocks,
};
latestOrchestrationSession = session;
activeOrchestrationSession = session;
try {
await executeOrchestrationBlocks(session);
session.status = 'completed';
} catch (error) {
session.status = 'failed';
session.failedBlockIndex = session.currentBlockIndex;
session.error = error instanceof Error ? error.message : String(error);
}
activeOrchestrationSession = null;
latestOrchestrationSession = session;
return formatOrchestrationSummary(session);
}
async function handleGetOrchestration() {
const session = requireLatestOrchestration();
return formatOrchestrationSummary(session);
}
async function handleCancelOrchestration() {
if (!activeOrchestrationSession) {
throw new Error('no active orchestration');
}
activeOrchestrationSession.status = 'canceled';
const canceled = activeOrchestrationSession;
activeOrchestrationSession = null;
latestOrchestrationSession = canceled;
return formatOrchestrationSummary(canceled);
}
async function handleToolCall(message) {
const id = message.id;
const params = message.params || {};
@@ -5087,6 +5292,30 @@ async function handleToolCall(message) {
return;
}
if (toolName === TOOL_START_ORCHESTRATION) {
const text = await handleStartOrchestration(toolArgs);
writeResponse(id, {
content: [{ type: 'text', text }],
});
return;
}
if (toolName === TOOL_GET_ORCHESTRATION) {
const text = await handleGetOrchestration(toolArgs);
writeResponse(id, {
content: [{ type: 'text', text }],
});
return;
}
if (toolName === TOOL_CANCEL_ORCHESTRATION) {
const text = await handleCancelOrchestration(toolArgs);
writeResponse(id, {
content: [{ type: 'text', text }],
});
return;
}
if (toolName === TOOL_TAKE_SCREENSHOT) {
const text = await handleScreenshot(toolArgs);
writeResponse(id, {