@autobest-ui/agent 1.0.22 → 1.0.23

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,并在页面左下角注入半透明的“开始录制”“停止录制”和设备切换按钮供测试人员手动控制 Trace;默认是 PC 端,也可切换为 iPhone 14 Pro Max 移动端模拟。页面先正常打开并等待,点击开始后先启动 Trace、刷新一次当前 SPA 页面并等待 DOM、CSS、字体和图片稳定,再进入正式录制;后续用户活动经过防抖后以 `步骤N | 页面标题 | 事件类型` 的 Trace Group 写入轻量 Playwright Action,空闲超过 2 分钟自动结束,然后上传原生 `trace.zip` 和浏览器元数据,并返回私有后端的 Trace Viewer 链接。同一进程同一时间只允许一个录制任务。
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 链接。同一进程同一时间只允许一个录制任务。
4
4
 
5
5
  ## 安装与启动
6
6
 
@@ -141,8 +141,8 @@ function createRecorderPageScript() {
141
141
  :host { all: initial; }
142
142
  .toolbar {
143
143
  position: fixed;
144
- left: 16px;
145
- bottom: 16px;
144
+ left: 0;
145
+ top: 16px;
146
146
  z-index: 2147483647;
147
147
  display: flex;
148
148
  gap: 8px;
@@ -153,6 +153,12 @@ function createRecorderPageScript() {
153
153
  box-shadow: 0 4px 16px rgba(0, 0, 0, .2);
154
154
  backdrop-filter: blur(6px);
155
155
  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);
156
162
  }
157
163
  button {
158
164
  box-sizing: border-box;
@@ -170,17 +176,28 @@ function createRecorderPageScript() {
170
176
  button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
171
177
  button:disabled { cursor: default; opacity: .42; }
172
178
  .start { background: rgba(22, 163, 74, .82); }
179
+ .screenshot { background: rgba(234, 88, 12, .82); }
173
180
  .stop { background: rgba(220, 38, 38, .82); }
174
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
+ }
175
189
  </style>
176
190
  <div class="toolbar" role="group" aria-label="Trace 录制控制">
191
+ <button class="toggle" type="button" aria-label="展开录制控制" data-trace-recorder-control>›</button>
177
192
  <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>
178
194
  <button class="stop" type="button" data-action="stop" data-trace-recorder-control>停止录制</button>
179
195
  <button class="device" type="button" data-action="toggle-device" data-trace-recorder-control>切换移动端</button>
180
196
  </div>
181
197
  \`;
182
198
  document.documentElement.append(host);
183
199
  const startButton = shadow.querySelector('[data-action="start"]');
200
+ const screenshotButton = shadow.querySelector('[data-action="screenshot"]');
184
201
  const stopButton = shadow.querySelector('[data-action="stop"]');
185
202
  const deviceButton = shadow.querySelector('[data-action="toggle-device"]');
186
203
 
@@ -189,6 +206,7 @@ function createRecorderPageScript() {
189
206
  const waiting = state === 'waiting_for_start';
190
207
  const preparing = state === 'starting';
191
208
  startButton.disabled = !waiting;
209
+ screenshotButton.disabled = !recording;
192
210
  stopButton.disabled = !recording && !waiting;
193
211
  deviceButton.disabled = state === 'starting' || state === 'stopping' || state === 'uploading';
194
212
  startButton.textContent = preparing ? '准备页面' : recording ? '录制中' : '开始录制';
@@ -197,6 +215,7 @@ function createRecorderPageScript() {
197
215
  };
198
216
  const control = async action => {
199
217
  startButton.disabled = true;
218
+ screenshotButton.disabled = true;
200
219
  stopButton.disabled = true;
201
220
  try {
202
221
  const result = await globalThis.__traceRecorderControl?.(action);
@@ -210,6 +229,11 @@ function createRecorderPageScript() {
210
229
  event.stopPropagation();
211
230
  void control('start');
212
231
  });
232
+ screenshotButton.addEventListener('click', event => {
233
+ event.preventDefault();
234
+ event.stopPropagation();
235
+ void control('screenshot');
236
+ });
213
237
  stopButton.addEventListener('click', event => {
214
238
  event.preventDefault();
215
239
  event.stopPropagation();
@@ -603,6 +627,10 @@ export class TraceRecorder {
603
627
  handlePageControl(session, action) {
604
628
  if (action === 'status') return { state: session.state, deviceMode: session.deviceMode };
605
629
  if (action === 'toggle-device') return this.toggleDeviceMode(session);
630
+ if (action === 'screenshot') {
631
+ if (session.state !== 'recording') return { state: session.state, captured: false };
632
+ return this.captureScreenshot(session).then(() => ({ state: session.state, captured: true }));
633
+ }
606
634
  if (action === 'start') {
607
635
  if (session.state === 'recording') return { state: session.state };
608
636
  if (session.state !== 'waiting_for_start') return { state: session.state };
@@ -626,24 +654,52 @@ export class TraceRecorder {
626
654
  return { state: session.state };
627
655
  }
628
656
 
657
+ async captureScreenshot(session) {
658
+ if (!session.tracingStarted || session.state !== 'recording') return;
659
+ session.stepIndex += 1;
660
+ let title = '未命名页面';
661
+ if (typeof session.page.title === 'function') {
662
+ title = await session.page.title().catch(() => title) || title;
663
+ }
664
+ const label = `步骤${session.stepIndex} | ${title}`;
665
+ await session.context.tracing.group(label);
666
+ let toolbarHidden = false;
667
+ try {
668
+ // Keep the recorder controls out of the user-selected page evidence.
669
+ await session.page.evaluate(() => {
670
+ const toolbar = document.querySelector('[data-trace-recorder-toolbar-host]');
671
+ if (toolbar) {
672
+ toolbar.setAttribute('data-trace-recorder-hidden', '');
673
+ toolbar.style.visibility = 'hidden';
674
+ }
675
+ });
676
+ toolbarHidden = true;
677
+ // page.screenshot is intentionally used inside the Trace group so the
678
+ // official Viewer records this deliberate visual checkpoint.
679
+ await session.page.screenshot({ fullPage: false });
680
+ await session.page.waitForTimeout(1);
681
+ session.lastActivityAt = Date.now();
682
+ this.armIdleTimer(session);
683
+ } finally {
684
+ if (toolbarHidden) {
685
+ await session.page.evaluate(() => {
686
+ const toolbar = document.querySelector('[data-trace-recorder-toolbar-host]');
687
+ if (toolbar) {
688
+ toolbar.style.visibility = '';
689
+ toolbar.removeAttribute('data-trace-recorder-hidden');
690
+ }
691
+ }).catch(() => {});
692
+ }
693
+ await session.context.tracing.groupEnd();
694
+ }
695
+ }
696
+
629
697
  handleUserActivity(session, activity = {}) {
630
698
  if (session.state !== 'recording') return undefined;
699
+ // Page activity is intentionally not recorded as an Action. The operator
700
+ // explicitly controls collection by pressing the Screenshot button.
631
701
  session.lastActivityAt = Date.now();
632
- session.pendingActivity = activity;
633
702
  this.armIdleTimer(session);
634
- if (session.activityDebounceTimer) this.clearTimer(session.activityDebounceTimer);
635
- session.activityDebounceTimer = this.setTimer(() => {
636
- session.activityDebounceTimer = undefined;
637
- const pendingActivity = session.pendingActivity;
638
- session.pendingActivity = undefined;
639
- session.activityRecordPromise = (session.activityRecordPromise ?? Promise.resolve())
640
- .then(() => this.recordActivityStep(session, pendingActivity))
641
- .catch(() => {})
642
- .finally(() => {
643
- session.activityRecordPromise = undefined;
644
- });
645
- }, USER_ACTIVITY_DEBOUNCE_DELAY);
646
- session.activityDebounceTimer?.unref?.();
647
703
  return undefined;
648
704
  }
649
705
 
@@ -652,7 +708,7 @@ export class TraceRecorder {
652
708
  session.stepIndex += 1;
653
709
  const type = typeof activity.type === 'string' && activity.type ? activity.type : 'unknown';
654
710
  const title = typeof activity.title === 'string' && activity.title ? activity.title : '未命名页面';
655
- const label = `步骤${session.stepIndex} | ${title} | ${type}`;
711
+ const label = `步骤${session.stepIndex} | ${title}`;
656
712
  await session.context.tracing.group(label);
657
713
  try {
658
714
  await session.page.waitForTimeout(1);
@@ -667,7 +723,7 @@ export class TraceRecorder {
667
723
  if (typeof session.page.title === 'function') {
668
724
  title = await session.page.title().catch(() => title) || title;
669
725
  }
670
- const label = `步骤${session.stepIndex} | ${title} | stop`;
726
+ const label = `步骤${session.stepIndex} | ${title}`;
671
727
  await session.context.tracing.group(label);
672
728
  try {
673
729
  await session.page.waitForTimeout(1);
@@ -25,6 +25,7 @@ function createFixture(options = {}) {
25
25
  reload: async options => calls.push(['reload', options]),
26
26
  evaluate: async () => 'Fixture User Agent',
27
27
  waitForTimeout: async duration => calls.push(['wait', duration]),
28
+ screenshot: async options => calls.push(['screenshot', options]),
28
29
  viewportSize: () => ({ width: 1280, height: 720 }),
29
30
  close: async () => calls.push(['page-close'])
30
31
  };
@@ -113,6 +114,10 @@ test('page controls start recording, record activity, upload and clean up one se
113
114
  });
114
115
  await recorder.activeSession.activityPromise;
115
116
  assert.equal(recorder.activeSession.state, 'recording');
117
+ const screenshotResult = await bindings.__traceRecorderControl({}, 'screenshot');
118
+ assert.deepEqual(screenshotResult, { state: 'recording', captured: true });
119
+ assert.ok(calls.some(call => call[0] === 'screenshot' && call[1].fullPage === false));
120
+ assert.ok(calls.some(call => call[0] === 'trace-group' && call[1] === '步骤1 | 未命名页面'));
116
121
  assert.ok(calls.some(call => call[0] === 'reload' && call[1].waitUntil === 'domcontentloaded'));
117
122
  assert.ok(calls.findIndex(call => call[0] === 'trace-start') < calls.findIndex(call => call[0] === 'reload'));
118
123
  recorder.handleUserActivity(recorder.activeSession, {
@@ -133,7 +138,7 @@ test('page controls start recording, record activity, upload and clean up one se
133
138
  );
134
139
  assert.equal(stopped.metadata.title, 'bug3452');
135
140
  assert.ok(calls.some(call => call[0] === 'trace-start' && call[1].sources === true));
136
- assert.ok(calls.some(call => call[0] === 'trace-group' && call[1] === '步骤1 | 首页功能 | click'));
141
+ assert.equal(calls.some(call => call[0] === 'trace-group' && call[1].includes('首页功能')), false);
137
142
  assert.ok(calls.some(call => call[0] === 'post' && call[1] === '/api/trace-sessions'));
138
143
  assert.ok(calls.some(call => call[0] === 'form-append' && call[1] === 'target_url'));
139
144
  assert.ok(calls.some(call => call[0] === 'remove' && call[1] === '/tmp/trace-fixture'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobest-ui/agent",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
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、字体和图片稳定,随后才进入正式录制计时。刷新期间不要操作页面;完成后点击“停止录制”。同一 BrowserContext 会保留 cookies、localStorage 和 sessionStorage,新打开的页面会继承当前设备模式。连续 2 分钟没有真实操作时服务会自动停止并上传。真实操作经防抖后会在官方 Trace Viewer 中以 `步骤N | 页面标题 | 事件类型` 的分组节点呈现。
17
+ 4. 告知用户浏览器已打开:默认是 PC 端;左上角控制栏默认收缩,悬停后展开,移出后自动收回。如需移动端测试,先展开并点击“切换移动端”(iPhone 14 Pro Max,430×932,DPR 3),再点击“开始录制”;点击开始后服务会先启动 Trace、刷新当前 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