@autobest-ui/agent 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/trace-mcp-recorder/README.md +2 -2
- package/mcp/trace-mcp-recorder/index.js +111 -144
- package/mcp/trace-mcp-recorder/index.test.js +6 -12
- package/mcp/trace-mcp-recorder/package-lock.json +0 -14
- package/mcp/trace-mcp-recorder/package.json +0 -1
- package/package.json +1 -1
- package/skills/common/trace-recorder/SKILL.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# trace-mcp-recorder
|
|
2
2
|
|
|
3
|
-
`trace-mcp-recorder` 是基于 `@modelcontextprotocol/sdk` 和 `StdioServerTransport` 的 Node.js MCP Server。它打开非无头 Chromium
|
|
3
|
+
`trace-mcp-recorder` 是基于 `@modelcontextprotocol/sdk` 和 `StdioServerTransport` 的 Node.js MCP Server。它打开非无头 Chromium 供测试人员手动操作,页面加载后等待第一次真实用户操作才启动 Trace;后续用户活动经过防抖后写入轻量 Playwright Action,空闲超过 2 分钟自动结束,然后上传原生 `trace.zip` 和浏览器元数据,并返回私有后端的 Trace Viewer 链接。同一进程同一时间只允许一个录制任务。
|
|
4
4
|
|
|
5
5
|
## 安装与启动
|
|
6
6
|
|
|
@@ -43,7 +43,7 @@ npm start
|
|
|
43
43
|
|
|
44
44
|
录制超时后服务自动停止并自动上传(无需人工确认),随后第一次人工调用 `stop_trace_recording` 会取回自动停止的结果;再调用则返回没有活动会话。
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
Trace 在第一次真实用户操作后才开始计时。连续用户操作停止 800ms 后写入一个轻量 `waitForTimeout(1)` Trace Action;连续 2 分钟没有真实用户操作时自动停止并上传。停止录制时会补一个最终 Action。页面自身的轮播、动画和图片加载不会触发录制节点。
|
|
47
47
|
|
|
48
48
|
## 后端接口契约
|
|
49
49
|
|
|
@@ -8,16 +8,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import axios from 'axios';
|
|
9
9
|
import FormData from 'form-data';
|
|
10
10
|
import { chromium } from 'playwright';
|
|
11
|
-
import { PNG } from 'pngjs';
|
|
12
11
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
12
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
13
|
import { z } from 'zod';
|
|
15
14
|
|
|
16
15
|
const DEFAULT_MAX_RECORD_DURATION = 1_800_000;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const VISUAL_DEBOUNCE_DELAY = 800;
|
|
20
|
-
const VISUAL_MAX_PENDING_DELAY = 3_000;
|
|
16
|
+
const USER_ACTIVITY_DEBOUNCE_DELAY = 800;
|
|
17
|
+
const IDLE_STOP_DELAY = 120_000;
|
|
21
18
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
22
19
|
|
|
23
20
|
function readConfiguration(environment = process.env) {
|
|
@@ -104,30 +101,18 @@ function toolError(error) {
|
|
|
104
101
|
};
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (let offset = 0; offset < previous.data.length; offset += 4) {
|
|
118
|
-
if (
|
|
119
|
-
Math.abs(previous.data[offset] - current.data[offset]) > channelTolerance ||
|
|
120
|
-
Math.abs(previous.data[offset + 1] - current.data[offset + 1]) > channelTolerance ||
|
|
121
|
-
Math.abs(previous.data[offset + 2] - current.data[offset + 2]) > channelTolerance ||
|
|
122
|
-
Math.abs(previous.data[offset + 3] - current.data[offset + 3]) > channelTolerance
|
|
123
|
-
) {
|
|
124
|
-
changedPixels += 1;
|
|
125
|
-
}
|
|
104
|
+
// Only emit a signal for trusted user input. The Node side stores no action
|
|
105
|
+
// details; it uses the signal to control Trace timing and debounce checkpoints.
|
|
106
|
+
function createUserActivityScript() {
|
|
107
|
+
return `(() => {
|
|
108
|
+
const notify = () => Promise.resolve(globalThis.__traceRecorderUserActivity?.()).catch(() => {});
|
|
109
|
+
const options = { capture: true, passive: true };
|
|
110
|
+
for (const type of ['pointerdown', 'keydown', 'input', 'change', 'wheel', 'touchstart']) {
|
|
111
|
+
document.addEventListener(type, event => {
|
|
112
|
+
if (event.isTrusted) notify();
|
|
113
|
+
}, options);
|
|
126
114
|
}
|
|
127
|
-
|
|
128
|
-
} catch {
|
|
129
|
-
return 1;
|
|
130
|
-
}
|
|
115
|
+
})();`;
|
|
131
116
|
}
|
|
132
117
|
|
|
133
118
|
export class TraceRecorder {
|
|
@@ -148,21 +133,18 @@ export class TraceRecorder {
|
|
|
148
133
|
this.makeTemporaryDirectory =
|
|
149
134
|
options.makeTemporaryDirectory ?? (() => mkdtemp(path.join(os.tmpdir(), 'trace-mcp-recorder-')));
|
|
150
135
|
this.removeTemporaryDirectory =
|
|
151
|
-
options.removeTemporaryDirectory ??
|
|
136
|
+
options.removeTemporaryDirectory ??
|
|
137
|
+
(temporaryDirectory => rm(temporaryDirectory, { recursive: true, force: true }));
|
|
152
138
|
this.setTimer = options.setTimer ?? setTimeout;
|
|
153
139
|
this.clearTimer = options.clearTimer ?? clearTimeout;
|
|
154
|
-
this.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
type: os.type(),
|
|
163
|
-
release: os.release(),
|
|
164
|
-
architecture: os.arch()
|
|
165
|
-
}));
|
|
140
|
+
this.platformInfo =
|
|
141
|
+
options.platformInfo ??
|
|
142
|
+
(() => ({
|
|
143
|
+
platform: os.platform(),
|
|
144
|
+
type: os.type(),
|
|
145
|
+
release: os.release(),
|
|
146
|
+
architecture: os.arch()
|
|
147
|
+
}));
|
|
166
148
|
this.activeSession = undefined;
|
|
167
149
|
this.lastAutomaticResult = undefined;
|
|
168
150
|
this.lastAutomaticError = undefined;
|
|
@@ -194,7 +176,16 @@ export class TraceRecorder {
|
|
|
194
176
|
temporaryDirectory = await this.makeTemporaryDirectory();
|
|
195
177
|
browser = await this.launchChromium({ headless: false });
|
|
196
178
|
context = await browser.newContext();
|
|
197
|
-
|
|
179
|
+
let session;
|
|
180
|
+
if (typeof context.exposeBinding === 'function') {
|
|
181
|
+
await context.exposeBinding('__traceRecorderUserActivity', () => {
|
|
182
|
+
if (session) return this.handleUserActivity(session);
|
|
183
|
+
return undefined;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (typeof context.addInitScript === 'function') {
|
|
187
|
+
await context.addInitScript({ content: createUserActivityScript() });
|
|
188
|
+
}
|
|
198
189
|
page = await context.newPage();
|
|
199
190
|
await page.goto(targetUrl);
|
|
200
191
|
|
|
@@ -210,32 +201,29 @@ export class TraceRecorder {
|
|
|
210
201
|
operatingSystem: this.platformInfo(),
|
|
211
202
|
startedAt: new Date().toISOString()
|
|
212
203
|
};
|
|
213
|
-
|
|
204
|
+
session = {
|
|
214
205
|
browser,
|
|
215
206
|
context,
|
|
216
207
|
page,
|
|
217
208
|
metadata,
|
|
218
209
|
baseUrl: configuration.baseUrl,
|
|
210
|
+
maxRecordDuration: configuration.maxRecordDuration,
|
|
219
211
|
tracePath: path.join(temporaryDirectory, 'trace.zip'),
|
|
220
212
|
temporaryDirectory,
|
|
221
|
-
state: '
|
|
213
|
+
state: 'waiting_for_activity',
|
|
222
214
|
stopPromise: undefined,
|
|
223
215
|
uploadPromise: undefined,
|
|
224
216
|
timer: undefined,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
cdpSession: undefined,
|
|
231
|
-
previousProbeFrame: undefined,
|
|
232
|
-
lastRecordedFrameAt: Date.now()
|
|
217
|
+
idleTimer: undefined,
|
|
218
|
+
activityDebounceTimer: undefined,
|
|
219
|
+
activityPromise: undefined,
|
|
220
|
+
tracingStarted: false,
|
|
221
|
+
lastActivityAt: undefined
|
|
233
222
|
};
|
|
234
223
|
this.activeSession = session;
|
|
235
|
-
await this.startVisualCapture(session);
|
|
236
224
|
|
|
237
225
|
const failOnBrowserExit = () => {
|
|
238
|
-
if (session.state === 'recording') {
|
|
226
|
+
if (session.state === 'recording' || session.state === 'waiting_for_activity') {
|
|
239
227
|
session.stopPromise = this.failSession(session, new Error('Chromium 浏览器意外退出'));
|
|
240
228
|
void session.stopPromise.catch(failure => {
|
|
241
229
|
console.error('[trace-mcp-recorder] 浏览器异常退出:', errorMessage(failure));
|
|
@@ -243,16 +231,9 @@ export class TraceRecorder {
|
|
|
243
231
|
}
|
|
244
232
|
};
|
|
245
233
|
browser.once('disconnected', failOnBrowserExit);
|
|
246
|
-
session.timer = this.setTimer(() => {
|
|
247
|
-
void this.stop({ trigger: 'timeout', confirm: 'upload' }).catch(error => {
|
|
248
|
-
this.lastAutomaticError = error;
|
|
249
|
-
console.error('[trace-mcp-recorder] 自动停止录制失败:', errorMessage(error));
|
|
250
|
-
});
|
|
251
|
-
}, configuration.maxRecordDuration);
|
|
252
|
-
session.timer?.unref?.();
|
|
253
234
|
|
|
254
235
|
return {
|
|
255
|
-
message: '
|
|
236
|
+
message: '浏览器已打开,等待首次真实操作后开始录制;完成后调用 stop_trace_recording',
|
|
256
237
|
maxRecordDuration: configuration.maxRecordDuration,
|
|
257
238
|
metadata
|
|
258
239
|
};
|
|
@@ -283,10 +264,17 @@ export class TraceRecorder {
|
|
|
283
264
|
if (session.state === 'starting') {
|
|
284
265
|
throw new Error('录制会话正在启动,请等待 start_trace_recording 返回后再停止');
|
|
285
266
|
}
|
|
267
|
+
if (session.state === 'waiting_for_activity') {
|
|
268
|
+
await this.closeResources({ page: session.page, context: session.context, browser: session.browser });
|
|
269
|
+
if (this.activeSession === session) this.activeSession = undefined;
|
|
270
|
+
throw new Error('尚未检测到真实用户操作,未生成 trace');
|
|
271
|
+
}
|
|
286
272
|
if (session.state === 'recording') {
|
|
287
273
|
if (session.stopPromise) return session.stopPromise;
|
|
288
274
|
session.state = 'stopping';
|
|
289
275
|
this.clearTimer(session.timer);
|
|
276
|
+
this.clearTimer(session.idleTimer);
|
|
277
|
+
this.clearTimer(session.activityDebounceTimer);
|
|
290
278
|
session.stopPromise = this.prepareSession(session).finally(() => {
|
|
291
279
|
session.stopPromise = undefined;
|
|
292
280
|
});
|
|
@@ -310,7 +298,8 @@ export class TraceRecorder {
|
|
|
310
298
|
|
|
311
299
|
async prepareSession(session) {
|
|
312
300
|
try {
|
|
313
|
-
await
|
|
301
|
+
await session.activityPromise?.catch(() => {});
|
|
302
|
+
if (session.tracingStarted) await session.page.waitForTimeout(1).catch(() => {});
|
|
314
303
|
await session.context.tracing.stop({ path: session.tracePath });
|
|
315
304
|
await this.closeResources({ page: session.page, context: session.context, browser: session.browser });
|
|
316
305
|
session.state = 'awaiting_upload';
|
|
@@ -326,81 +315,53 @@ export class TraceRecorder {
|
|
|
326
315
|
}
|
|
327
316
|
}
|
|
328
317
|
|
|
329
|
-
async
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
session.
|
|
334
|
-
session.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
}).finally(() => {
|
|
341
|
-
session.visualProbePromise = undefined;
|
|
318
|
+
async beginRecording(session) {
|
|
319
|
+
if (session.tracingStarted || session.state !== 'waiting_for_activity') return;
|
|
320
|
+
await session.context.tracing.start({ screenshots: true, snapshots: true, sources: true });
|
|
321
|
+
session.tracingStarted = true;
|
|
322
|
+
session.state = 'recording';
|
|
323
|
+
session.lastActivityAt = Date.now();
|
|
324
|
+
session.timer = this.setTimer(() => {
|
|
325
|
+
void this.stop({ trigger: 'timeout', confirm: 'upload' }).catch(error => {
|
|
326
|
+
this.lastAutomaticError = error;
|
|
327
|
+
console.error('[trace-mcp-recorder] 自动停止录制失败:', errorMessage(error));
|
|
342
328
|
});
|
|
343
|
-
},
|
|
344
|
-
session.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
async captureProbeFrame(session) {
|
|
348
|
-
const result = await session.cdpSession.send('Page.captureScreenshot', {
|
|
349
|
-
format: 'png',
|
|
350
|
-
fromSurface: true
|
|
351
|
-
});
|
|
352
|
-
return Buffer.from(result.data, 'base64');
|
|
329
|
+
}, session.maxRecordDuration);
|
|
330
|
+
session.timer?.unref?.();
|
|
331
|
+
this.armIdleTimer(session);
|
|
332
|
+
await session.page.waitForTimeout(1);
|
|
353
333
|
}
|
|
354
334
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
335
|
+
handleUserActivity(session) {
|
|
336
|
+
if (session.state === 'waiting_for_activity') {
|
|
337
|
+
if (session.activityPromise) return session.activityPromise;
|
|
338
|
+
session.activityPromise = this.beginRecording(session).catch(error => {
|
|
339
|
+
session.state = 'failed';
|
|
340
|
+
throw error;
|
|
341
|
+
});
|
|
342
|
+
return session.activityPromise;
|
|
361
343
|
}
|
|
344
|
+
if (session.state !== 'recording') return undefined;
|
|
345
|
+
session.lastActivityAt = Date.now();
|
|
346
|
+
this.armIdleTimer(session);
|
|
347
|
+
if (session.activityDebounceTimer) this.clearTimer(session.activityDebounceTimer);
|
|
348
|
+
session.activityDebounceTimer = this.setTimer(() => {
|
|
349
|
+
session.activityDebounceTimer = undefined;
|
|
350
|
+
session.activityPromise = session.page.waitForTimeout(1).catch(() => {});
|
|
351
|
+
}, USER_ACTIVITY_DEBOUNCE_DELAY);
|
|
352
|
+
session.activityDebounceTimer?.unref?.();
|
|
353
|
+
return undefined;
|
|
362
354
|
}
|
|
363
355
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
session.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
session.visualDebounceTimer = this.setTimer(() => {
|
|
371
|
-
session.visualDebounceTimer = undefined;
|
|
372
|
-
session.visualRecordPromise = this.recordPendingVisual(session).finally(() => {
|
|
373
|
-
session.visualRecordPromise = undefined;
|
|
356
|
+
armIdleTimer(session) {
|
|
357
|
+
this.clearTimer(session.idleTimer);
|
|
358
|
+
session.idleTimer = this.setTimer(() => {
|
|
359
|
+
void this.stop({ trigger: 'idle', confirm: 'upload' }).catch(error => {
|
|
360
|
+
this.lastAutomaticError = error;
|
|
361
|
+
console.error('[trace-mcp-recorder] 空闲自动停止失败:', errorMessage(error));
|
|
374
362
|
});
|
|
375
|
-
},
|
|
376
|
-
session.
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async recordPendingVisual(session) {
|
|
380
|
-
if (session.state !== 'recording' || !session.visualPendingSince) return;
|
|
381
|
-
await session.page.screenshot();
|
|
382
|
-
session.lastRecordedFrameAt = Date.now();
|
|
383
|
-
session.visualPendingSince = undefined;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async stopVisualCapture(session, recordFinalFrame = false) {
|
|
387
|
-
if (!session.cdpSession) return;
|
|
388
|
-
if (session.visualProbeTimer) {
|
|
389
|
-
this.clearInterval(session.visualProbeTimer);
|
|
390
|
-
session.visualProbeTimer = undefined;
|
|
391
|
-
}
|
|
392
|
-
await session.visualProbePromise?.catch(() => {});
|
|
393
|
-
if (session.visualDebounceTimer) {
|
|
394
|
-
this.clearTimer(session.visualDebounceTimer);
|
|
395
|
-
session.visualDebounceTimer = undefined;
|
|
396
|
-
}
|
|
397
|
-
await session.visualRecordPromise?.catch(() => {});
|
|
398
|
-
session.visualPendingSince = undefined;
|
|
399
|
-
if (recordFinalFrame && session.page) {
|
|
400
|
-
await session.page.screenshot().catch(() => {});
|
|
401
|
-
}
|
|
402
|
-
await session.cdpSession?.detach().catch(() => {});
|
|
403
|
-
session.cdpSession = undefined;
|
|
363
|
+
}, IDLE_STOP_DELAY);
|
|
364
|
+
session.idleTimer?.unref?.();
|
|
404
365
|
}
|
|
405
366
|
|
|
406
367
|
async uploadSession(session, trigger) {
|
|
@@ -449,16 +410,18 @@ export class TraceRecorder {
|
|
|
449
410
|
session.state = 'completed';
|
|
450
411
|
if (trigger === 'timeout') this.lastAutomaticResult = result;
|
|
451
412
|
return result;
|
|
452
|
-
})()
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
413
|
+
})()
|
|
414
|
+
.catch(async error => {
|
|
415
|
+
session.state = 'failed';
|
|
416
|
+
if (trigger === 'timeout') this.lastAutomaticError = error;
|
|
417
|
+
await this.removeTemporaryDirectory(session.temporaryDirectory).catch(() => {});
|
|
418
|
+
if (this.activeSession === session) this.activeSession = undefined;
|
|
419
|
+
throw error;
|
|
420
|
+
})
|
|
421
|
+
.finally(async () => {
|
|
422
|
+
await this.removeTemporaryDirectory(session.temporaryDirectory).catch(() => {});
|
|
423
|
+
if (this.activeSession === session && session.state === 'completed') this.activeSession = undefined;
|
|
424
|
+
});
|
|
462
425
|
return session.uploadPromise;
|
|
463
426
|
}
|
|
464
427
|
|
|
@@ -470,10 +433,11 @@ export class TraceRecorder {
|
|
|
470
433
|
}
|
|
471
434
|
|
|
472
435
|
async failSession(session, error) {
|
|
473
|
-
if (session.state !== 'recording') return;
|
|
436
|
+
if (session.state !== 'recording' && session.state !== 'waiting_for_activity') return;
|
|
474
437
|
session.state = 'failed';
|
|
475
438
|
this.clearTimer(session.timer);
|
|
476
|
-
|
|
439
|
+
this.clearTimer(session.idleTimer);
|
|
440
|
+
this.clearTimer(session.activityDebounceTimer);
|
|
477
441
|
await this.closeResources(session);
|
|
478
442
|
await this.removeTemporaryDirectory(session.temporaryDirectory).catch(() => {});
|
|
479
443
|
if (this.activeSession === session) this.activeSession = undefined;
|
|
@@ -551,7 +515,10 @@ export function createServer(options = {}) {
|
|
|
551
515
|
title: '停止 Playwright Trace 录制',
|
|
552
516
|
description: '停止当前 trace 并询问上传;确认后上传 trace.zip 和浏览器元数据,返回回放链接。',
|
|
553
517
|
inputSchema: {
|
|
554
|
-
confirm: z
|
|
518
|
+
confirm: z
|
|
519
|
+
.enum(['upload', 'cancel'])
|
|
520
|
+
.optional()
|
|
521
|
+
.describe('首次停止不传;用户确认上传传 upload,拒绝上传传 cancel')
|
|
555
522
|
},
|
|
556
523
|
annotations: {
|
|
557
524
|
readOnlyHint: false,
|
|
@@ -5,21 +5,11 @@ import test from 'node:test';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
7
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
-
import {
|
|
9
|
-
import { pixelDifference, TraceRecorder } from './index.js';
|
|
8
|
+
import { TraceRecorder } from './index.js';
|
|
10
9
|
|
|
11
10
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
12
11
|
const sessionUuid = '123e4567-e89b-42d3-a456-426614174000';
|
|
13
12
|
|
|
14
|
-
test('calculates pixel differences for visual probes', () => {
|
|
15
|
-
const first = new PNG({ width: 2, height: 1 });
|
|
16
|
-
const second = new PNG({ width: 2, height: 1 });
|
|
17
|
-
first.data.fill(255);
|
|
18
|
-
second.data.fill(255);
|
|
19
|
-
second.data[0] = 0;
|
|
20
|
-
assert.equal(pixelDifference(PNG.sync.write(first), PNG.sync.write(second)), 0.5);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
13
|
function createFixture(options = {}) {
|
|
24
14
|
const calls = [];
|
|
25
15
|
const browser = new EventEmitter();
|
|
@@ -30,6 +20,7 @@ function createFixture(options = {}) {
|
|
|
30
20
|
const page = {
|
|
31
21
|
goto: async url => calls.push(['goto', url]),
|
|
32
22
|
evaluate: async () => 'Fixture User Agent',
|
|
23
|
+
waitForTimeout: async duration => calls.push(['wait', duration]),
|
|
33
24
|
viewportSize: () => ({ width: 1280, height: 720 }),
|
|
34
25
|
close: async () => calls.push(['page-close'])
|
|
35
26
|
};
|
|
@@ -78,7 +69,7 @@ function createFixture(options = {}) {
|
|
|
78
69
|
removeTemporaryDirectory: async temporaryDirectory => calls.push(['remove', temporaryDirectory]),
|
|
79
70
|
setTimer: (callback, duration) => {
|
|
80
71
|
calls.push(['timer', duration]);
|
|
81
|
-
timer.callback = callback;
|
|
72
|
+
if (duration === 1000) timer.callback = callback;
|
|
82
73
|
return timer;
|
|
83
74
|
},
|
|
84
75
|
clearTimer: value => calls.push(['clear-timer', value]),
|
|
@@ -102,6 +93,7 @@ test('starts, records metadata, uploads, completes and cleans up one session', a
|
|
|
102
93
|
assert.equal(started.uuid, undefined);
|
|
103
94
|
assert.equal(started.metadata.operator, '上下文操作人');
|
|
104
95
|
assert.equal(started.metadata.browserVersion, 'Fixture Chromium 1');
|
|
96
|
+
await recorder.handleUserActivity(recorder.activeSession);
|
|
105
97
|
await assert.rejects(
|
|
106
98
|
recorder.start({ url: 'https://app.example.test/another' }),
|
|
107
99
|
/已有录制任务/
|
|
@@ -139,6 +131,7 @@ test('cleans up when Chromium launch fails without a backend session', async ()
|
|
|
139
131
|
test('marks the session failed and removes the local trace when upload fails', async () => {
|
|
140
132
|
const { calls, recorder } = createFixture({ uploadError: new Error('upload failed') });
|
|
141
133
|
await recorder.start({ url: 'https://app.example.test' });
|
|
134
|
+
await recorder.handleUserActivity(recorder.activeSession);
|
|
142
135
|
|
|
143
136
|
await recorder.stop();
|
|
144
137
|
await assert.rejects(recorder.stop({ confirm: 'upload' }), /upload failed/);
|
|
@@ -148,6 +141,7 @@ test('marks the session failed and removes the local trace when upload fails', a
|
|
|
148
141
|
test('automatically stops on timeout and returns that result on the next stop call', async () => {
|
|
149
142
|
const { recorder, timer } = createFixture();
|
|
150
143
|
await recorder.start({ url: 'https://app.example.test' });
|
|
144
|
+
await recorder.handleUserActivity(recorder.activeSession);
|
|
151
145
|
timer.callback();
|
|
152
146
|
|
|
153
147
|
while (recorder.activeSession) await new Promise(resolve => setImmediate(resolve));
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"axios": "1.20.0",
|
|
13
13
|
"form-data": "4.0.6",
|
|
14
14
|
"playwright": "1.63.0",
|
|
15
|
-
"pngjs": "^7.0.0",
|
|
16
15
|
"uuid": "14.0.2",
|
|
17
16
|
"zod": "4.4.3"
|
|
18
17
|
},
|
|
@@ -961,14 +960,6 @@
|
|
|
961
960
|
"node": ">=20"
|
|
962
961
|
}
|
|
963
962
|
},
|
|
964
|
-
"node_modules/pngjs": {
|
|
965
|
-
"version": "7.0.0",
|
|
966
|
-
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
|
967
|
-
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
|
968
|
-
"engines": {
|
|
969
|
-
"node": ">=14.19.0"
|
|
970
|
-
}
|
|
971
|
-
},
|
|
972
963
|
"node_modules/proxy-addr": {
|
|
973
964
|
"version": "2.0.7",
|
|
974
965
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
|
@@ -1915,11 +1906,6 @@
|
|
|
1915
1906
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
|
|
1916
1907
|
"integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="
|
|
1917
1908
|
},
|
|
1918
|
-
"pngjs": {
|
|
1919
|
-
"version": "7.0.0",
|
|
1920
|
-
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
|
1921
|
-
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="
|
|
1922
|
-
},
|
|
1923
1909
|
"proxy-addr": {
|
|
1924
1910
|
"version": "2.0.7",
|
|
1925
1911
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ description: 通过 trace-mcp-recorder 启动或停止 Playwright Trace 人工
|
|
|
14
14
|
1. 从原话提取 `url`、`version`、`title`、`operator`。支持 `key=value` 和自然语言表达,不改写用户给出的值。
|
|
15
15
|
2. `url` 必填;缺少时只询问目标页面地址。其他字段缺失时不要阻断录制。
|
|
16
16
|
3. 调用 `start_trace_recording`,只传入用户明确提供的可选字段。服务端会优先使用 MCP 上下文中的操作人身份。
|
|
17
|
-
4.
|
|
17
|
+
4. 告知用户浏览器已打开、正在等待首次真实操作,并保留工具返回的 `uuid` 和最大录制时长;Trace 会在第一次真实操作时开始计时,不要代替用户操作页面。连续 2 分钟没有真实操作时服务会自动停止并上传。
|
|
18
18
|
|
|
19
19
|
示例:`trace-mcp-recorder 启动Playwright trace 访问https://cpd.dev.autobestdevops.com,version=v2.1.0 title=bug3452 operator=张三`。这里 `title=bug3452` 是本次录制的标题/功能描述,页面由 `url=https://cpd.dev.autobestdevops.com` 确定。
|
|
20
20
|
|