@autobest-ui/agent 1.0.23 → 1.0.25

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.
@@ -1,6 +1,6 @@
1
1
  # trace-mcp-recorder
2
2
 
3
- `trace-mcp-recorder` 是基于 `@modelcontextprotocol/sdk` 和 `StdioServerTransport` 的 Node.js MCP Server。它打开非无头 Chromium,并在页面左上角注入可收缩的半透明控制栏,供测试人员手动开始录制、截图、停止录制和切换设备;默认是 PC 端,也可切换为 iPhone 14 Pro Max 移动端模拟。页面先正常打开并等待,点击开始后先启动 Trace、刷新一次当前 SPA 页面并等待 DOM、CSS、字体和图片稳定,再进入正式录制;普通点击、输入和滚动不会生成自定义 Action,只有点击“截图”才会把当前渲染画面作为 Trace Group 写入官方 Viewer,截图时控制栏会临时隐藏。空闲超过 2 分钟自动结束,然后上传原生 `trace.zip` 和浏览器元数据,并返回私有后端的 Trace Viewer 链接。同一进程同一时间只允许一个录制任务。
3
+ `trace-mcp-recorder` 是基于 `@modelcontextprotocol/sdk` 和 `StdioServerTransport` 的 Node.js MCP Server。它打开非无头 Chromium,PC 端默认 viewport 为 1400×740,并在页面右下角固定显示半透明的 2×2 控制栏:第一行是“开始录制”“停止录制”,第二行是“切换移动端”“截图”。四个按钮始终显示,不伸缩,统一高度 32px。默认是 PC 端,也可切换为 iPhone 14 Pro Max 移动端模拟。页面先正常打开并等待,点击开始后先启动 Trace,临时禁用 Chromium 缓存并刷新一次当前 SPA 页面,等待 DOM、CSS、字体和图片稳定后恢复缓存,再进入正式录制;普通点击、输入和滚动不会生成自定义 Action,只有点击“截图”才会把当前渲染画面作为 Trace Group 写入官方 Viewer,截图时控制栏会临时隐藏。空闲超过 2 分钟自动结束,然后上传原生 `trace.zip` 和浏览器元数据,并返回私有后端的 Trace Viewer 链接。同一进程同一时间只允许一个录制任务。
4
4
 
5
5
  ## 安装与启动
6
6
 
@@ -15,6 +15,7 @@ import { z } from 'zod';
15
15
  const DEFAULT_MAX_RECORD_DURATION = 1_800_000;
16
16
  const USER_ACTIVITY_DEBOUNCE_DELAY = 800;
17
17
  const IDLE_STOP_DELAY = 120_000;
18
+ const DESKTOP_VIEWPORT = { width: 1400, height: 740 };
18
19
  const IPHONE_14_PRO_MAX_DEVICE = {
19
20
  name: 'iPhone 14 Pro Max',
20
21
  width: 430,
@@ -141,10 +142,11 @@ function createRecorderPageScript() {
141
142
  :host { all: initial; }
142
143
  .toolbar {
143
144
  position: fixed;
144
- left: 0;
145
- top: 16px;
145
+ right: 16px;
146
+ bottom: 16px;
146
147
  z-index: 2147483647;
147
- display: flex;
148
+ display: grid;
149
+ grid-template-columns: repeat(2, minmax(120px, 1fr));
148
150
  gap: 8px;
149
151
  padding: 7px;
150
152
  border: 1px solid rgba(255, 255, 255, .38);
@@ -153,17 +155,11 @@ function createRecorderPageScript() {
153
155
  box-shadow: 0 4px 16px rgba(0, 0, 0, .2);
154
156
  backdrop-filter: blur(6px);
155
157
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
156
- transform: translateX(calc(-100% + 30px));
157
- transition: transform .2s ease;
158
- }
159
- .toolbar:hover,
160
- .toolbar:focus-within {
161
- transform: translateX(0);
162
158
  }
163
159
  button {
164
160
  box-sizing: border-box;
165
- min-width: 88px;
166
- height: 34px;
161
+ min-width: 120px;
162
+ height: 32px;
167
163
  border: 1px solid rgba(255, 255, 255, .48);
168
164
  border-radius: 6px;
169
165
  color: #fff;
@@ -176,23 +172,15 @@ function createRecorderPageScript() {
176
172
  button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
177
173
  button:disabled { cursor: default; opacity: .42; }
178
174
  .start { background: rgba(22, 163, 74, .82); }
179
- .screenshot { background: rgba(234, 88, 12, .82); }
180
175
  .stop { background: rgba(220, 38, 38, .82); }
181
- .device { background: rgba(37, 99, 235, .82); min-width: 102px; }
182
- .toggle {
183
- flex: 0 0 24px;
184
- min-width: 24px;
185
- padding: 0;
186
- background: rgba(255, 255, 255, .16);
187
- font-size: 20px;
188
- }
176
+ .device { background: rgba(37, 99, 235, .82); }
177
+ .screenshot { background: rgba(234, 88, 12, .82); }
189
178
  </style>
190
179
  <div class="toolbar" role="group" aria-label="Trace 录制控制">
191
- <button class="toggle" type="button" aria-label="展开录制控制" data-trace-recorder-control>›</button>
192
180
  <button class="start" type="button" data-action="start" data-trace-recorder-control>开始录制</button>
193
- <button class="screenshot" type="button" data-action="screenshot" data-trace-recorder-control>截图</button>
194
181
  <button class="stop" type="button" data-action="stop" data-trace-recorder-control>停止录制</button>
195
182
  <button class="device" type="button" data-action="toggle-device" data-trace-recorder-control>切换移动端</button>
183
+ <button class="screenshot" type="button" data-action="screenshot" data-trace-recorder-control>截图</button>
196
184
  </div>
197
185
  \`;
198
186
  document.documentElement.append(host);
@@ -324,7 +312,7 @@ export class TraceRecorder {
324
312
  try {
325
313
  temporaryDirectory = await this.makeTemporaryDirectory();
326
314
  browser = await this.launchChromium({ headless: false });
327
- context = await browser.newContext();
315
+ context = await browser.newContext({ viewport: DESKTOP_VIEWPORT });
328
316
  let session;
329
317
  if (typeof context.exposeBinding === 'function') {
330
318
  await context.exposeBinding('__traceRecorderUserActivity', (_source, activity) => {
@@ -532,25 +520,44 @@ export class TraceRecorder {
532
520
  .evaluate(() => ({ x: window.scrollX, y: window.scrollY }))
533
521
  .catch(() => ({ x: 0, y: 0 }));
534
522
 
535
- await session.page.reload({ waitUntil: 'domcontentloaded' });
536
- // networkidle is intentionally best-effort: SPAs with polling/websockets
537
- // may never become idle, while the resource checks below remain bounded.
538
- if (typeof session.page.waitForLoadState === 'function') {
539
- await session.page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => {});
523
+ // The first page load may have populated Chromium's HTTP cache before
524
+ // tracing started. Disable cache only for this reload so CSS, images and
525
+ // fonts produce fresh response bodies inside trace.zip.
526
+ let cacheSession;
527
+ let cacheWasDisabled = false;
528
+ try {
529
+ if (typeof session.context.newCDPSession === 'function') {
530
+ cacheSession = session.cdpSessions.get(session.page) ?? await session.context.newCDPSession(session.page);
531
+ session.cdpSessions.set(session.page, cacheSession);
532
+ await cacheSession.send('Network.enable');
533
+ await cacheSession.send('Network.setCacheDisabled', { cacheDisabled: true });
534
+ cacheWasDisabled = true;
535
+ }
536
+
537
+ await session.page.reload({ waitUntil: 'domcontentloaded' });
538
+ // networkidle is intentionally best-effort: SPAs with polling/websockets
539
+ // may never become idle, while the resource checks below remain bounded.
540
+ if (typeof session.page.waitForLoadState === 'function') {
541
+ await session.page.waitForLoadState('networkidle', { timeout: 10_000 }).catch(() => {});
542
+ }
543
+ await session.page
544
+ .evaluate(async () => {
545
+ await document.fonts?.ready?.catch?.(() => {});
546
+ const pendingImages = Array.from(document.images).filter(image => !image.complete);
547
+ await Promise.race([
548
+ Promise.all(pendingImages.map(image => new Promise(resolve => {
549
+ image.addEventListener('load', resolve, { once: true });
550
+ image.addEventListener('error', resolve, { once: true });
551
+ }))),
552
+ new Promise(resolve => setTimeout(resolve, 10_000))
553
+ ]);
554
+ })
555
+ .catch(() => {});
556
+ } finally {
557
+ if (cacheWasDisabled) {
558
+ await cacheSession.send('Network.setCacheDisabled', { cacheDisabled: false }).catch(() => {});
559
+ }
540
560
  }
541
- await session.page
542
- .evaluate(async () => {
543
- await document.fonts?.ready?.catch?.(() => {});
544
- const pendingImages = Array.from(document.images).filter(image => !image.complete);
545
- await Promise.race([
546
- Promise.all(pendingImages.map(image => new Promise(resolve => {
547
- image.addEventListener('load', resolve, { once: true });
548
- image.addEventListener('error', resolve, { once: true });
549
- }))),
550
- new Promise(resolve => setTimeout(resolve, 10_000))
551
- ]);
552
- })
553
- .catch(() => {});
554
561
  if (currentUrl && typeof session.page.url === 'function' && session.page.url() !== currentUrl) {
555
562
  await session.page.goto(currentUrl, { waitUntil: 'domcontentloaded' });
556
563
  }
@@ -620,7 +627,7 @@ export class TraceRecorder {
620
627
  : session.desktopUserAgent;
621
628
  session.metadata.viewport = nextMode === 'mobile'
622
629
  ? { width: IPHONE_14_PRO_MAX_DEVICE.width, height: IPHONE_14_PRO_MAX_DEVICE.height }
623
- : session.page.viewportSize();
630
+ : { ...DESKTOP_VIEWPORT };
624
631
  return { state: session.state, deviceMode: session.deviceMode, deviceName: session.deviceName };
625
632
  }
626
633
 
@@ -120,6 +120,11 @@ test('page controls start recording, record activity, upload and clean up one se
120
120
  assert.ok(calls.some(call => call[0] === 'trace-group' && call[1] === '步骤1 | 未命名页面'));
121
121
  assert.ok(calls.some(call => call[0] === 'reload' && call[1].waitUntil === 'domcontentloaded'));
122
122
  assert.ok(calls.findIndex(call => call[0] === 'trace-start') < calls.findIndex(call => call[0] === 'reload'));
123
+ const cacheOnIndex = calls.findIndex(call => call[0] === 'cdp' && call[1] === 'Network.setCacheDisabled' && call[2].cacheDisabled === true);
124
+ const reloadIndex = calls.findIndex(call => call[0] === 'reload');
125
+ const cacheOffIndex = calls.findIndex(call => call[0] === 'cdp' && call[1] === 'Network.setCacheDisabled' && call[2].cacheDisabled === false);
126
+ assert.ok(cacheOnIndex < reloadIndex);
127
+ assert.ok(reloadIndex < cacheOffIndex);
123
128
  recorder.handleUserActivity(recorder.activeSession, {
124
129
  type: 'click',
125
130
  title: '首页功能'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobest-ui/agent",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "private": false,
5
5
  "description": "Autobest Agent skills/plugins/mcp assets + sync cli",
6
6
  "files": [
@@ -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. 告知用户浏览器已打开:默认是 PC 端;左上角控制栏默认收缩,悬停后展开,移出后自动收回。如需移动端测试,先展开并点击“切换移动端”(iPhone 14 Pro Max,430×932,DPR 3),再点击“开始录制”;点击开始后服务会先启动 Trace、刷新当前 SPA 页面一次并等待 DOM、CSS、字体和图片稳定,随后才进入正式录制计时。刷新期间不要操作页面;页面稳定后按需操作,只有点击“截图”才会保存当前渲染画面,普通点击、输入和滚动不会生成自定义 Action。截图时控制栏会临时隐藏。同一 BrowserContext 会保留 cookies、localStorage 和 sessionStorage,新打开的页面会继承当前设备模式。连续 2 分钟没有页面活动时服务会自动停止并上传。人工截图在官方 Trace Viewer 中显示为 `步骤N | 页面标题`。
17
+ 4. 告知用户浏览器已打开:默认是 PC 端(viewport 1400×740);右下角固定显示 2×2 半透明控制栏,第一行是“开始录制”“停止录制”,第二行是“切换移动端”“截图”,四个按钮始终显示且高度为 32px。如需移动端测试,先点击“切换移动端”(iPhone 14 Pro Max,430×932,DPR 3),再点击“开始录制”;点击开始后服务会先启动 Trace,临时禁用 Chromium 缓存并刷新当前 SPA 页面,等待 DOM、CSS、字体和图片稳定后恢复缓存,随后才进入正式录制计时。刷新期间不要操作页面;页面稳定后按需操作,只有点击“截图”才会保存当前渲染画面,普通点击、输入和滚动不会生成自定义 Action。截图时所有控制按钮会临时隐藏。同一 BrowserContext 会保留 cookies、localStorage 和 sessionStorage,新打开的页面会继承当前设备模式。连续 2 分钟没有页面活动时服务会自动停止并上传。人工截图在官方 Trace Viewer 中显示为 `步骤N | 页面标题`。
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