mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
feat(browser): 增加 orchestration 执行策略增强
为 block 与 sequence step 增加最小 continueOnError 语义,并补充 failures 汇总输出。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
78f3879e19
commit
101396f728
@@ -2433,11 +2433,22 @@ function formatOrchestrationSummary(session) {
|
||||
`pageIndex: ${session.pageIndex}`,
|
||||
`blockCount: ${session.blockCount}`,
|
||||
`completedBlocks: ${session.completedBlocks}`,
|
||||
`failedCount: ${session.failedCount || 0}`,
|
||||
`currentBlockIndex: ${session.currentBlockIndex}`,
|
||||
`failedBlockIndex: ${session.failedBlockIndex === null ? 'none' : session.failedBlockIndex}`,
|
||||
`error: ${session.error || 'none'}`,
|
||||
`status: ${session.status}`,
|
||||
];
|
||||
if (Array.isArray(session.failures) && session.failures.length > 0) {
|
||||
session.failures.forEach((failure, index) => {
|
||||
lines.push(`failure[${index}].blockIndex: ${failure.blockIndex}`);
|
||||
lines.push(
|
||||
`failure[${index}].sequenceStepIndex: ${failure.sequenceStepIndex === null ? 'none' : failure.sequenceStepIndex}`
|
||||
);
|
||||
lines.push(`failure[${index}].type: ${failure.type}`);
|
||||
lines.push(`failure[${index}].message: ${failure.message}`);
|
||||
});
|
||||
}
|
||||
if (session.failedSequenceStepIndex !== null && session.failedSequenceStepIndex !== undefined) {
|
||||
lines.push(`failedSequenceStepIndex: ${session.failedSequenceStepIndex}`);
|
||||
}
|
||||
@@ -2656,6 +2667,12 @@ function formatOrchestrationError(error) {
|
||||
return { message };
|
||||
}
|
||||
|
||||
function pushOrchestrationFailure(session, details) {
|
||||
session.failures = session.failures || [];
|
||||
session.failures.push(details);
|
||||
session.failedCount = session.failures.length;
|
||||
}
|
||||
|
||||
function requireOrchestrationBlocks(toolArgs) {
|
||||
const blocks = Array.isArray(toolArgs.blocks) ? toolArgs.blocks : null;
|
||||
if (!blocks || blocks.length === 0) {
|
||||
@@ -5151,7 +5168,17 @@ async function executeSequenceSteps(session, block) {
|
||||
try {
|
||||
await executeOrchestrationBlock(session, step);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
pushOrchestrationFailure(session, {
|
||||
blockIndex: session.currentBlockIndex,
|
||||
sequenceStepIndex: index,
|
||||
type: step.type,
|
||||
message,
|
||||
});
|
||||
session.failedSequenceStepIndex = index;
|
||||
if (step.continueOnError === true) {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -5162,8 +5189,26 @@ async function executeOrchestrationBlocks(session) {
|
||||
const block = session.blocks[index];
|
||||
session.currentBlockIndex = index;
|
||||
validateOrchestrationBlock(block);
|
||||
await executeOrchestrationBlock(session, block);
|
||||
session.completedBlocks = index + 1;
|
||||
try {
|
||||
await executeOrchestrationBlock(session, block);
|
||||
session.completedBlocks = index + 1;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (session.failedSequenceStepIndex === null || session.failedSequenceStepIndex === undefined) {
|
||||
pushOrchestrationFailure(session, {
|
||||
blockIndex: index,
|
||||
sequenceStepIndex: null,
|
||||
type: block.type,
|
||||
message,
|
||||
});
|
||||
}
|
||||
if (block.continueOnError === true) {
|
||||
session.failedSequenceStepIndex = null;
|
||||
session.completedBlocks = index + 1;
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7009,6 +7009,275 @@ describe('ccs-browser MCP server', () => {
|
||||
expect(getResponseText(responses.find((message) => message.id === 1306))).toContain('status: completed');
|
||||
});
|
||||
|
||||
it('continues to the next top-level block when continueOnError is true', async () => {
|
||||
const pages: MockPageState[] = [
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Policy Page',
|
||||
currentUrl: 'https://example.com/policy',
|
||||
query: {
|
||||
'#status': {
|
||||
exists: true,
|
||||
connected: true,
|
||||
innerText: 'ready state',
|
||||
textContent: 'ready state',
|
||||
display: 'block',
|
||||
visibility: 'visible',
|
||||
opacity: '1',
|
||||
rect: {
|
||||
x: 20,
|
||||
y: 80,
|
||||
width: 100,
|
||||
height: 40,
|
||||
top: 80,
|
||||
right: 120,
|
||||
bottom: 120,
|
||||
left: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const responses = await runMcpRequests(pages, [
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1501,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_start_orchestration',
|
||||
arguments: {
|
||||
blocks: [
|
||||
createOrchestrationBlock({
|
||||
type: 'assert_query',
|
||||
continueOnError: true,
|
||||
args: {
|
||||
query: { selector: '#missing', fields: ['exists'] },
|
||||
assertions: [{ field: 'exists', op: 'equals', value: true }],
|
||||
},
|
||||
}),
|
||||
createOrchestrationBlock({
|
||||
type: 'assert_query',
|
||||
args: {
|
||||
query: { selector: '#status', fields: ['innerText'] },
|
||||
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1502,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_get_orchestration', arguments: {} },
|
||||
},
|
||||
]);
|
||||
|
||||
const text = getResponseText(responses.find((message) => message.id === 1502));
|
||||
expect(text).toContain('completedBlocks: 2');
|
||||
expect(text).toContain('failedCount: 1');
|
||||
expect(text).toContain('failure[0].blockIndex: 0');
|
||||
});
|
||||
|
||||
it('continues inside sequence when a step has continueOnError', async () => {
|
||||
const pages: MockPageState[] = [
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Sequence Policy Page',
|
||||
currentUrl: 'https://example.com/sequence-policy',
|
||||
query: {
|
||||
'#status': {
|
||||
exists: true,
|
||||
connected: true,
|
||||
innerText: 'ready state',
|
||||
textContent: 'ready state',
|
||||
display: 'block',
|
||||
visibility: 'visible',
|
||||
opacity: '1',
|
||||
rect: {
|
||||
x: 20,
|
||||
y: 80,
|
||||
width: 100,
|
||||
height: 40,
|
||||
top: 80,
|
||||
right: 120,
|
||||
bottom: 120,
|
||||
left: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const responses = await runMcpRequests(pages, [
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1503,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_start_orchestration',
|
||||
arguments: {
|
||||
blocks: [
|
||||
createOrchestrationBlock({
|
||||
type: 'sequence',
|
||||
args: {
|
||||
steps: [
|
||||
createOrchestrationBlock({
|
||||
type: 'assert_query',
|
||||
continueOnError: true,
|
||||
args: {
|
||||
query: { selector: '#missing', fields: ['exists'] },
|
||||
assertions: [{ field: 'exists', op: 'equals', value: true }],
|
||||
},
|
||||
}),
|
||||
createOrchestrationBlock({
|
||||
type: 'assert_query',
|
||||
args: {
|
||||
query: { selector: '#status', fields: ['innerText'] },
|
||||
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1504,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_get_orchestration', arguments: {} },
|
||||
},
|
||||
]);
|
||||
|
||||
const text = getResponseText(responses.find((message) => message.id === 1504));
|
||||
expect(text).toContain('completedBlocks: 1');
|
||||
expect(text).toContain('failedCount: 1');
|
||||
expect(text).toContain('failure[0].sequenceStepIndex: 0');
|
||||
});
|
||||
|
||||
it('still stops immediately when continueOnError is not enabled', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[{ id: 'page-1', title: 'Strict Stop Page', currentUrl: 'https://example.com/strict-stop' }],
|
||||
[
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1505,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_start_orchestration',
|
||||
arguments: {
|
||||
blocks: [
|
||||
createOrchestrationBlock({
|
||||
type: 'assert_query',
|
||||
args: {
|
||||
query: { selector: '#missing', fields: ['exists'] },
|
||||
assertions: [{ field: 'exists', op: 'equals', value: true }],
|
||||
},
|
||||
}),
|
||||
createOrchestrationBlock({
|
||||
type: 'assert_query',
|
||||
continueOnError: true,
|
||||
args: {
|
||||
query: { selector: '#status', fields: ['innerText'] },
|
||||
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const text = getResponseText(responses.find((message) => message.id === 1505));
|
||||
expect(text).toContain('status: failed');
|
||||
expect(text).toContain('completedBlocks: 0');
|
||||
});
|
||||
|
||||
it('continues to the next top-level block when a sequence block itself allows continueOnError', async () => {
|
||||
const pages: MockPageState[] = [
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Sequence Top-Level Policy Page',
|
||||
currentUrl: 'https://example.com/sequence-top-policy',
|
||||
query: {
|
||||
'#status': {
|
||||
exists: true,
|
||||
connected: true,
|
||||
innerText: 'ready state',
|
||||
textContent: 'ready state',
|
||||
display: 'block',
|
||||
visibility: 'visible',
|
||||
opacity: '1',
|
||||
rect: {
|
||||
x: 20,
|
||||
y: 80,
|
||||
width: 100,
|
||||
height: 40,
|
||||
top: 80,
|
||||
right: 120,
|
||||
bottom: 120,
|
||||
left: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const responses = await runMcpRequests(pages, [
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1506,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_start_orchestration',
|
||||
arguments: {
|
||||
blocks: [
|
||||
createOrchestrationBlock({
|
||||
type: 'sequence',
|
||||
continueOnError: true,
|
||||
args: {
|
||||
steps: [
|
||||
createOrchestrationBlock({
|
||||
type: 'assert_query',
|
||||
args: {
|
||||
query: { selector: '#missing', fields: ['exists'] },
|
||||
assertions: [{ field: 'exists', op: 'equals', value: true }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
createOrchestrationBlock({
|
||||
type: 'assert_query',
|
||||
args: {
|
||||
query: { selector: '#status', fields: ['innerText'] },
|
||||
assertions: [{ field: 'innerText', op: 'contains', value: 'ready' }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 1507,
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_get_orchestration', arguments: {} },
|
||||
},
|
||||
]);
|
||||
|
||||
const text = getResponseText(responses.find((message) => message.id === 1507));
|
||||
expect(text).toContain('completedBlocks: 2');
|
||||
expect(text).toContain('failedCount: 1');
|
||||
expect(text).toContain('failure[0].sequenceStepIndex: 0');
|
||||
});
|
||||
|
||||
it('drags local files onto normal, frame, and shadow dropzones', async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'ccs-browser-drag-files-'));
|
||||
const invoicePath = join(tempDir, 'invoice.pdf');
|
||||
|
||||
Reference in New Issue
Block a user