mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
feat(browser): 完成 Phase 10A 录制动作规范化
补齐 recording 安装阶段的真实页面监听与注入失败回滚,避免空录制和损坏会话状态。 并完善高层 step 规范化与相关回归覆盖。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
17a3434056
commit
5288387151
@@ -4364,10 +4364,86 @@ async function handleDragFiles(toolArgs) {
|
||||
function buildRecordingInstallExpression(recordingPayload) {
|
||||
return `(() => {
|
||||
const recordingPayload = JSON.parse(${JSON.stringify(JSON.stringify(recordingPayload))});
|
||||
const existing = globalThis.__CCS_BROWSER_RECORDING_RECORDER__;
|
||||
if (existing && existing.installed === true) {
|
||||
return { installed: true };
|
||||
}
|
||||
|
||||
const events = Array.isArray(recordingPayload.events) ? [...recordingPayload.events] : [];
|
||||
const warnings = Array.isArray(recordingPayload.warnings) ? [...recordingPayload.warnings] : [];
|
||||
const getSelector = (element) => {
|
||||
if (!(element instanceof Element)) {
|
||||
return undefined;
|
||||
}
|
||||
if (element.id) {
|
||||
return '#' + element.id;
|
||||
}
|
||||
const attr = typeof element.getAttribute === 'function' ? element.getAttribute('data-testid') : '';
|
||||
if (attr) {
|
||||
return '[data-testid="' + attr + '"]';
|
||||
}
|
||||
return element.tagName ? element.tagName.toLowerCase() : undefined;
|
||||
};
|
||||
const pushEvent = (event) => {
|
||||
events.push(event);
|
||||
};
|
||||
const onClick = (event) => {
|
||||
pushEvent({
|
||||
kind: 'click',
|
||||
selector: getSelector(event.target),
|
||||
button: event.button === 1 ? 'middle' : event.button === 2 ? 'right' : 'left',
|
||||
clickCount: event.detail || 1,
|
||||
offsetX: typeof event.offsetX === 'number' ? event.offsetX : undefined,
|
||||
offsetY: typeof event.offsetY === 'number' ? event.offsetY : undefined,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
const onInput = (event) => {
|
||||
const target = event.target;
|
||||
let text = '';
|
||||
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
||||
text = target.value;
|
||||
} else if (target && target.isContentEditable === true) {
|
||||
text = target.textContent || '';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
pushEvent({ kind: 'type', selector: getSelector(target), text, timestamp: Date.now() });
|
||||
};
|
||||
const onKeyDown = (event) => {
|
||||
const modifiers = [];
|
||||
if (event.altKey) modifiers.push('Alt');
|
||||
if (event.ctrlKey) modifiers.push('Control');
|
||||
if (event.metaKey) modifiers.push('Meta');
|
||||
if (event.shiftKey) modifiers.push('Shift');
|
||||
pushEvent({ kind: 'press_key', key: event.key, modifiers, timestamp: Date.now() });
|
||||
};
|
||||
const onScroll = (event) => {
|
||||
const target = event.target === document ? document.scrollingElement || document.documentElement : event.target;
|
||||
pushEvent({
|
||||
kind: 'scroll',
|
||||
selector: getSelector(target),
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('click', onClick, true);
|
||||
document.addEventListener('input', onInput, true);
|
||||
document.addEventListener('keydown', onKeyDown, true);
|
||||
document.addEventListener('scroll', onScroll, true);
|
||||
|
||||
globalThis.__CCS_BROWSER_RECORDING_RECORDER__ = {
|
||||
installed: true,
|
||||
events: Array.isArray(recordingPayload.events) ? recordingPayload.events : [],
|
||||
warnings: Array.isArray(recordingPayload.warnings) ? recordingPayload.warnings : [],
|
||||
events,
|
||||
warnings,
|
||||
teardown: () => {
|
||||
document.removeEventListener('click', onClick, true);
|
||||
document.removeEventListener('input', onInput, true);
|
||||
document.removeEventListener('keydown', onKeyDown, true);
|
||||
document.removeEventListener('scroll', onScroll, true);
|
||||
},
|
||||
};
|
||||
return { installed: true };
|
||||
})()`;
|
||||
@@ -4435,7 +4511,7 @@ async function handleStartRecording(toolArgs) {
|
||||
throw new Error(`Browser MCP page ${pageIndex} does not expose a websocket debugger URL.`);
|
||||
}
|
||||
|
||||
activeRecordingSession = {
|
||||
const session = {
|
||||
recordingId: createRecordingId(),
|
||||
pageId: page.id,
|
||||
pageIndex,
|
||||
@@ -4445,14 +4521,16 @@ async function handleStartRecording(toolArgs) {
|
||||
steps: [],
|
||||
warnings: [],
|
||||
rawEvents: [],
|
||||
pageWebSocketDebuggerUrl: page.webSocketDebuggerUrl,
|
||||
captureInstalled: false,
|
||||
};
|
||||
latestRecordingSession = activeRecordingSession;
|
||||
|
||||
await installRecorderAndCapture(page);
|
||||
activeRecordingSession.pageWebSocketDebuggerUrl = page.webSocketDebuggerUrl;
|
||||
activeRecordingSession.captureInstalled = true;
|
||||
session.captureInstalled = true;
|
||||
activeRecordingSession = session;
|
||||
latestRecordingSession = session;
|
||||
|
||||
return formatRecordingSummary(activeRecordingSession);
|
||||
return formatRecordingSummary(session);
|
||||
}
|
||||
|
||||
async function handleStopRecording() {
|
||||
|
||||
@@ -5695,6 +5695,28 @@ describe('ccs-browser MCP server', () => {
|
||||
expect(getResponseText(responses.find((message) => message.id === 1019))).toContain('pageIndex and pageId cannot be used together');
|
||||
});
|
||||
|
||||
it('rolls back recording state when recorder injection fails', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[
|
||||
{
|
||||
id: 'page-1',
|
||||
title: 'Broken Recording Page',
|
||||
currentUrl: 'https://example.com/broken-recording',
|
||||
recording: {
|
||||
injectionError: 'recording injection failed',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{ jsonrpc: '2.0', id: 1020, method: 'tools/call', params: { name: 'browser_start_recording', arguments: {} } },
|
||||
{ jsonrpc: '2.0', id: 1020_1, method: 'tools/call', params: { name: 'browser_get_recording', arguments: {} } },
|
||||
]
|
||||
);
|
||||
|
||||
expect(getResponseText(responses.find((message) => message.id === 1020))).toContain('recording injection failed');
|
||||
expect(getResponseText(responses.find((message) => message.id === 1020_1))).toContain('no recording available');
|
||||
});
|
||||
|
||||
it('normalizes type, press_key, scroll, and warnings in a recording result', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user