mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
feat(browser): 完成 Phase 10A 录制动作规范化
补齐 recording 安装阶段的真实页面监听与注入失败回滚,避免空录制和损坏会话状态。 并完善高层 step 规范化与相关回归覆盖。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
17a3434056
commit
5288387151
2 files changed
+107
-7
No files matched your search
@@ -4364,10 +4364,86 @@ async function handleDragFiles(toolArgs) {
|
|||||||
function buildRecordingInstallExpression(recordingPayload) {
|
function buildRecordingInstallExpression(recordingPayload) {
|
||||||
return `(() => {
|
return `(() => {
|
||||||
const recordingPayload = JSON.parse(${JSON.stringify(JSON.stringify(recordingPayload))});
|
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__ = {
|
globalThis.__CCS_BROWSER_RECORDING_RECORDER__ = {
|
||||||
installed: true,
|
installed: true,
|
||||||
events: Array.isArray(recordingPayload.events) ? recordingPayload.events : [],
|
events,
|
||||||
warnings: Array.isArray(recordingPayload.warnings) ? recordingPayload.warnings : [],
|
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 };
|
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.`);
|
throw new Error(`Browser MCP page ${pageIndex} does not expose a websocket debugger URL.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
activeRecordingSession = {
|
const session = {
|
||||||
recordingId: createRecordingId(),
|
recordingId: createRecordingId(),
|
||||||
pageId: page.id,
|
pageId: page.id,
|
||||||
pageIndex,
|
pageIndex,
|
||||||
@@ -4445,14 +4521,16 @@ async function handleStartRecording(toolArgs) {
|
|||||||
steps: [],
|
steps: [],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
rawEvents: [],
|
rawEvents: [],
|
||||||
|
pageWebSocketDebuggerUrl: page.webSocketDebuggerUrl,
|
||||||
|
captureInstalled: false,
|
||||||
};
|
};
|
||||||
latestRecordingSession = activeRecordingSession;
|
|
||||||
|
|
||||||
await installRecorderAndCapture(page);
|
await installRecorderAndCapture(page);
|
||||||
activeRecordingSession.pageWebSocketDebuggerUrl = page.webSocketDebuggerUrl;
|
session.captureInstalled = true;
|
||||||
activeRecordingSession.captureInstalled = true;
|
activeRecordingSession = session;
|
||||||
|
latestRecordingSession = session;
|
||||||
|
|
||||||
return formatRecordingSummary(activeRecordingSession);
|
return formatRecordingSummary(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStopRecording() {
|
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');
|
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 () => {
|
it('normalizes type, press_key, scroll, and warnings in a recording result', async () => {
|
||||||
const responses = await runMcpRequests(
|
const responses = await runMcpRequests(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in new issue
Block a user