@autobest-ui/agent 1.0.11 → 1.0.13
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/README.md +20 -6
- package/bin/sync-assets.test.mjs +2 -0
- package/mcp/trace-mcp-recorder/.env.example +6 -0
- package/mcp/trace-mcp-recorder/README.md +60 -0
- package/mcp/trace-mcp-recorder/config.toml.example +11 -0
- package/mcp/trace-mcp-recorder/index.js +594 -0
- package/mcp/trace-mcp-recorder/index.test.js +189 -0
- package/mcp/trace-mcp-recorder/package-lock.json +2142 -0
- package/mcp/trace-mcp-recorder/package.json +26 -0
- package/package.json +9 -2
- package/skills/README.md +1 -0
- package/skills/common/trace-recorder/SKILL.md +25 -0
- package/skills/common/trace-recorder/agents/openai.yaml +11 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
import { PNG } from 'pngjs';
|
|
9
|
+
import { pixelDifference, TraceRecorder } from './index.js';
|
|
10
|
+
|
|
11
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const sessionUuid = '123e4567-e89b-42d3-a456-426614174000';
|
|
13
|
+
|
|
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
|
+
function createFixture(options = {}) {
|
|
24
|
+
const calls = [];
|
|
25
|
+
const browser = new EventEmitter();
|
|
26
|
+
const tracing = {
|
|
27
|
+
start: async options => calls.push(['trace-start', options]),
|
|
28
|
+
stop: async options => calls.push(['trace-stop', options])
|
|
29
|
+
};
|
|
30
|
+
const page = {
|
|
31
|
+
goto: async url => calls.push(['goto', url]),
|
|
32
|
+
evaluate: async () => 'Fixture User Agent',
|
|
33
|
+
viewportSize: () => ({ width: 1280, height: 720 }),
|
|
34
|
+
close: async () => calls.push(['page-close'])
|
|
35
|
+
};
|
|
36
|
+
const context = {
|
|
37
|
+
tracing,
|
|
38
|
+
newPage: async () => page,
|
|
39
|
+
close: async () => calls.push(['context-close'])
|
|
40
|
+
};
|
|
41
|
+
browser.newContext = async () => context;
|
|
42
|
+
browser.version = () => 'Fixture Chromium 1';
|
|
43
|
+
browser.close = async () => calls.push(['browser-close']);
|
|
44
|
+
|
|
45
|
+
const http = {
|
|
46
|
+
post: async (endpoint, body) => {
|
|
47
|
+
calls.push(['post', endpoint, body]);
|
|
48
|
+
if (endpoint === '/api/trace-sessions' && body !== form) return { data: { uuid: sessionUuid } };
|
|
49
|
+
if (options.uploadError) throw options.uploadError;
|
|
50
|
+
return { data: { fileUrl: '/private/traces/trace.zip' } };
|
|
51
|
+
},
|
|
52
|
+
patch: async (endpoint, body) => {
|
|
53
|
+
calls.push(['patch', endpoint, body]);
|
|
54
|
+
return { data: {} };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const form = {
|
|
58
|
+
append: (...args) => calls.push(['form-append', ...args]),
|
|
59
|
+
getHeaders: () => ({ 'content-type': 'multipart/form-data; boundary=fixture' })
|
|
60
|
+
};
|
|
61
|
+
const timer = { unref() {} };
|
|
62
|
+
const recorder = new TraceRecorder({
|
|
63
|
+
environment: {
|
|
64
|
+
BACKEND_BASE_URL: 'https://trace.example.test/root/',
|
|
65
|
+
MAX_RECORD_DURATION: '1000'
|
|
66
|
+
},
|
|
67
|
+
launchChromium: async options => {
|
|
68
|
+
calls.push(['launch', options]);
|
|
69
|
+
return browser;
|
|
70
|
+
},
|
|
71
|
+
createHttpClient: baseUrl => {
|
|
72
|
+
calls.push(['http-client', baseUrl]);
|
|
73
|
+
return http;
|
|
74
|
+
},
|
|
75
|
+
createFormData: () => form,
|
|
76
|
+
createReadStream: tracePath => ({ tracePath }),
|
|
77
|
+
makeTemporaryDirectory: async () => '/tmp/trace-fixture',
|
|
78
|
+
removeTemporaryDirectory: async temporaryDirectory => calls.push(['remove', temporaryDirectory]),
|
|
79
|
+
setTimer: (callback, duration) => {
|
|
80
|
+
calls.push(['timer', duration]);
|
|
81
|
+
timer.callback = callback;
|
|
82
|
+
return timer;
|
|
83
|
+
},
|
|
84
|
+
clearTimer: value => calls.push(['clear-timer', value]),
|
|
85
|
+
platformInfo: () => ({ platform: 'fixture-os' })
|
|
86
|
+
});
|
|
87
|
+
return { browser, calls, recorder, timer };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test('starts, records metadata, uploads, completes and cleans up one session', async () => {
|
|
91
|
+
const { calls, recorder } = createFixture();
|
|
92
|
+
const started = await recorder.start(
|
|
93
|
+
{
|
|
94
|
+
url: 'https://app.example.test/login',
|
|
95
|
+
version: 'v2.1.0',
|
|
96
|
+
title: 'bug3452',
|
|
97
|
+
operator: '传入操作人'
|
|
98
|
+
},
|
|
99
|
+
{ authInfo: { extra: { operator: '上下文操作人' } } }
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
assert.equal(started.uuid, undefined);
|
|
103
|
+
assert.equal(started.metadata.operator, '上下文操作人');
|
|
104
|
+
assert.equal(started.metadata.browserVersion, 'Fixture Chromium 1');
|
|
105
|
+
await assert.rejects(
|
|
106
|
+
recorder.start({ url: 'https://app.example.test/another' }),
|
|
107
|
+
/已有录制任务/
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const confirmation = await recorder.stop();
|
|
111
|
+
assert.match(confirmation.message, /是否上传/);
|
|
112
|
+
const stopped = await recorder.stop({ confirm: 'upload' });
|
|
113
|
+
assert.equal(
|
|
114
|
+
stopped.replayLink,
|
|
115
|
+
'https://trace.example.test/root/trace-viewer/?trace=/private/traces/trace.zip'
|
|
116
|
+
);
|
|
117
|
+
assert.equal(stopped.metadata.title, 'bug3452');
|
|
118
|
+
assert.ok(calls.some(call => call[0] === 'trace-start' && call[1].sources === true));
|
|
119
|
+
assert.ok(calls.some(call => call[0] === 'post' && call[1] === '/api/trace-sessions'));
|
|
120
|
+
assert.ok(calls.some(call => call[0] === 'form-append' && call[1] === 'target_url'));
|
|
121
|
+
assert.ok(calls.some(call => call[0] === 'remove' && call[1] === '/tmp/trace-fixture'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('rejects missing configuration before contacting the backend', async () => {
|
|
125
|
+
const recorder = new TraceRecorder({ environment: {} });
|
|
126
|
+
await assert.rejects(recorder.start({ url: 'https://app.example.test' }), /BACKEND_BASE_URL/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('cleans up when Chromium launch fails without a backend session', async () => {
|
|
130
|
+
const { calls, recorder } = createFixture();
|
|
131
|
+
recorder.launchChromium = async () => {
|
|
132
|
+
throw new Error('launch failed');
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
await assert.rejects(recorder.start({ url: 'https://app.example.test' }), /launch failed/);
|
|
136
|
+
assert.equal(calls.some(call => call[0] === 'post'), false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('marks the session failed and removes the local trace when upload fails', async () => {
|
|
140
|
+
const { calls, recorder } = createFixture({ uploadError: new Error('upload failed') });
|
|
141
|
+
await recorder.start({ url: 'https://app.example.test' });
|
|
142
|
+
|
|
143
|
+
await recorder.stop();
|
|
144
|
+
await assert.rejects(recorder.stop({ confirm: 'upload' }), /upload failed/);
|
|
145
|
+
assert.ok(calls.some(call => call[0] === 'remove' && call[1] === '/tmp/trace-fixture'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('automatically stops on timeout and returns that result on the next stop call', async () => {
|
|
149
|
+
const { recorder, timer } = createFixture();
|
|
150
|
+
await recorder.start({ url: 'https://app.example.test' });
|
|
151
|
+
timer.callback();
|
|
152
|
+
|
|
153
|
+
while (recorder.activeSession) await new Promise(resolve => setImmediate(resolve));
|
|
154
|
+
const result = await recorder.stop();
|
|
155
|
+
assert.equal(result.stoppedBy, 'timeout');
|
|
156
|
+
await assert.rejects(recorder.stop(), /当前没有正在录制/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('exposes both tools and the bundled skill over stdio', async () => {
|
|
160
|
+
const client = new Client({ name: 'trace-mcp-recorder-test', version: '1.0.0' });
|
|
161
|
+
const transport = new StdioClientTransport({
|
|
162
|
+
command: process.execPath,
|
|
163
|
+
args: [path.join(directory, 'index.js')],
|
|
164
|
+
cwd: directory,
|
|
165
|
+
env: { ...process.env },
|
|
166
|
+
stderr: 'pipe'
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await client.connect(transport);
|
|
171
|
+
assert.equal(client.getServerVersion()?.name, 'trace-mcp-recorder');
|
|
172
|
+
const { tools } = await client.listTools();
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
tools.map(tool => tool.name),
|
|
175
|
+
['start_trace_recording', 'stop_trace_recording']
|
|
176
|
+
);
|
|
177
|
+
const { resources } = await client.listResources();
|
|
178
|
+
assert.ok(resources.some(resource => resource.uri === 'skill://trace-recorder/SKILL.md'));
|
|
179
|
+
|
|
180
|
+
const result = await client.callTool({
|
|
181
|
+
name: 'start_trace_recording',
|
|
182
|
+
arguments: { url: 'https://app.example.test' }
|
|
183
|
+
});
|
|
184
|
+
assert.equal(result.isError, true);
|
|
185
|
+
assert.match(result.content[0].text, /BACKEND_BASE_URL/);
|
|
186
|
+
} finally {
|
|
187
|
+
await client.close();
|
|
188
|
+
}
|
|
189
|
+
});
|