@aiscene/shared 8.0.3 → 8.0.4
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/dist/es/agent-tools/agent-behavior-init-args.mjs +44 -0
- package/dist/es/agent-tools/base-tools.mjs +163 -0
- package/dist/es/agent-tools/chrome-path.mjs +50 -0
- package/dist/es/agent-tools/cli-report-session.mjs +78 -0
- package/dist/es/agent-tools/error-formatter.mjs +106 -0
- package/dist/es/agent-tools/index.mjs +9 -0
- package/dist/es/agent-tools/init-arg-utils.mjs +38 -0
- package/dist/es/agent-tools/observation-artifact.mjs +5 -0
- package/dist/es/agent-tools/observation-record.mjs +248 -0
- package/dist/es/agent-tools/tool-defaults.mjs +54 -0
- package/dist/es/agent-tools/tool-generator.mjs +529 -0
- package/dist/es/agent-tools/types.mjs +3 -0
- package/dist/es/agent-tools/user-prompt.mjs +66 -0
- package/dist/es/cli/interrupt.mjs +116 -0
- package/dist/es/cli/record-command.mjs +130 -0
- package/dist/es/cli/screenshot-file.mjs +24 -0
- package/dist/es/cli/verbose-ai-act.mjs +230 -0
- package/dist/es/cli/verbose-screenshot.mjs +134 -0
- package/dist/es/cli/verbose.mjs +404 -0
- package/dist/es/env/parse-model-config.mjs +1 -1
- package/dist/es/env/types.mjs +3 -1
- package/dist/es/img/info.mjs +45 -1
- package/dist/es/img/photon-loader.mjs +4 -0
- package/dist/es/recorder.mjs +249 -0
- package/dist/lib/agent-tools/agent-behavior-init-args.js +87 -0
- package/dist/lib/agent-tools/base-tools.js +197 -0
- package/dist/lib/agent-tools/chrome-path.js +87 -0
- package/dist/lib/agent-tools/cli-report-session.js +121 -0
- package/dist/lib/agent-tools/error-formatter.js +149 -0
- package/dist/lib/agent-tools/index.js +114 -0
- package/dist/lib/agent-tools/init-arg-utils.js +78 -0
- package/dist/lib/agent-tools/observation-artifact.js +42 -0
- package/dist/lib/agent-tools/observation-record.js +297 -0
- package/dist/lib/agent-tools/tool-defaults.js +97 -0
- package/dist/lib/agent-tools/tool-generator.js +569 -0
- package/dist/lib/agent-tools/types.js +40 -0
- package/dist/lib/agent-tools/user-prompt.js +103 -0
- package/dist/lib/cli/interrupt.js +156 -0
- package/dist/lib/cli/record-command.js +164 -0
- package/dist/lib/cli/screenshot-file.js +58 -0
- package/dist/lib/cli/verbose-ai-act.js +273 -0
- package/dist/lib/cli/verbose-screenshot.js +177 -0
- package/dist/lib/cli/verbose.js +465 -0
- package/dist/lib/env/parse-model-config.js +1 -1
- package/dist/lib/env/types.js +5 -0
- package/dist/lib/img/info.js +48 -1
- package/dist/lib/img/photon-loader.js +38 -0
- package/dist/lib/recorder.js +307 -0
- package/dist/types/agent-tools/agent-behavior-init-args.d.ts +17 -0
- package/dist/types/agent-tools/base-tools.d.ts +158 -0
- package/dist/types/agent-tools/chrome-path.d.ts +2 -0
- package/dist/types/agent-tools/cli-report-session.d.ts +12 -0
- package/dist/types/agent-tools/error-formatter.d.ts +30 -0
- package/dist/types/agent-tools/index.d.ts +9 -0
- package/dist/types/agent-tools/init-arg-utils.d.ts +13 -0
- package/dist/types/agent-tools/observation-artifact.d.ts +10 -0
- package/dist/types/agent-tools/observation-record.d.ts +38 -0
- package/dist/types/agent-tools/tool-defaults.d.ts +63 -0
- package/dist/types/agent-tools/tool-generator.d.ts +13 -0
- package/dist/types/agent-tools/types.d.ts +213 -0
- package/dist/types/agent-tools/user-prompt.d.ts +13 -0
- package/dist/types/cli/interrupt.d.ts +49 -0
- package/dist/types/cli/record-command.d.ts +3 -0
- package/dist/types/cli/screenshot-file.d.ts +10 -0
- package/dist/types/cli/verbose-ai-act.d.ts +44 -0
- package/dist/types/cli/verbose-screenshot.d.ts +10 -0
- package/dist/types/cli/verbose.d.ts +40 -0
- package/dist/types/env/types.d.ts +4 -3
- package/dist/types/img/info.d.ts +2 -0
- package/dist/types/img/photon-loader.d.ts +2 -0
- package/dist/types/mcp/types.d.ts +1 -0
- package/dist/types/recorder.d.ts +113 -0
- package/package.json +1 -1
- package/src/agent-tools/agent-behavior-init-args.ts +109 -0
- package/src/agent-tools/base-tools.ts +399 -0
- package/src/agent-tools/chrome-path.ts +74 -0
- package/src/agent-tools/cli-report-session.ts +130 -0
- package/src/agent-tools/error-formatter.ts +177 -0
- package/src/agent-tools/index.ts +9 -0
- package/src/agent-tools/init-arg-utils.ts +105 -0
- package/src/agent-tools/observation-artifact.ts +29 -0
- package/src/agent-tools/observation-record.ts +331 -0
- package/src/agent-tools/tool-defaults.ts +119 -0
- package/src/agent-tools/tool-generator.ts +866 -0
- package/src/agent-tools/types.ts +250 -0
- package/src/agent-tools/user-prompt.ts +102 -0
- package/src/cli/interrupt.ts +207 -0
- package/src/cli/record-command.ts +177 -0
- package/src/cli/screenshot-file.ts +61 -0
- package/src/cli/verbose-ai-act.ts +387 -0
- package/src/cli/verbose-screenshot.ts +269 -0
- package/src/cli/verbose.ts +753 -0
- package/src/env/types.ts +2 -0
- package/src/img/index.ts +12 -0
- package/src/img/info.ts +61 -0
- package/src/img/photon-loader.ts +5 -0
- package/src/img/transform.ts +261 -2
- package/src/mcp/types.ts +2 -0
- package/src/recorder.ts +625 -0
package/src/recorder.ts
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
export type MidsceneRecorderEventType =
|
|
2
|
+
| 'click'
|
|
3
|
+
| 'drag'
|
|
4
|
+
| 'scroll'
|
|
5
|
+
| 'input'
|
|
6
|
+
| 'navigation'
|
|
7
|
+
| 'setViewport'
|
|
8
|
+
| 'keydown';
|
|
9
|
+
|
|
10
|
+
export type MidsceneRecorderSourceKind =
|
|
11
|
+
| 'studio-preview'
|
|
12
|
+
| 'unsupported'
|
|
13
|
+
| (string & {});
|
|
14
|
+
|
|
15
|
+
export type MidsceneRecorderPlatformId =
|
|
16
|
+
| 'web'
|
|
17
|
+
| 'android'
|
|
18
|
+
| 'ios'
|
|
19
|
+
| 'computer'
|
|
20
|
+
| 'harmony'
|
|
21
|
+
| (string & {});
|
|
22
|
+
|
|
23
|
+
export interface MidsceneRecorderElementRect {
|
|
24
|
+
left?: number;
|
|
25
|
+
top?: number;
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
x?: number;
|
|
29
|
+
y?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MidsceneRecorderPageInfo {
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A screenshot stored outside the recording event payload.
|
|
39
|
+
*
|
|
40
|
+
* Recorder events are persisted in the Studio renderer. Keeping full data
|
|
41
|
+
* URLs there makes long recordings retain every screenshot in the renderer
|
|
42
|
+
* heap, so screenshot bytes live in the Playground run directory instead.
|
|
43
|
+
*/
|
|
44
|
+
export interface MidsceneRecorderScreenshotAssetRef {
|
|
45
|
+
id: string;
|
|
46
|
+
mimeType: string;
|
|
47
|
+
bytes: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MidsceneRecorderEvent {
|
|
51
|
+
type: MidsceneRecorderEventType;
|
|
52
|
+
source?: MidsceneRecorderSourceKind;
|
|
53
|
+
actionType?: string;
|
|
54
|
+
rawPayload?: Record<string, unknown>;
|
|
55
|
+
url?: string;
|
|
56
|
+
title?: string;
|
|
57
|
+
value?: string;
|
|
58
|
+
elementRect?: MidsceneRecorderElementRect;
|
|
59
|
+
pageInfo: MidsceneRecorderPageInfo;
|
|
60
|
+
screenshotBefore?: string;
|
|
61
|
+
screenshotAfter?: string;
|
|
62
|
+
/** The single screenshot retained for AI description and Markdown export. */
|
|
63
|
+
screenshotAsset?: MidsceneRecorderScreenshotAssetRef;
|
|
64
|
+
semantic?: MidsceneRecorderSemantic;
|
|
65
|
+
elementDescription?: string;
|
|
66
|
+
descriptionLoading?: boolean;
|
|
67
|
+
screenshotWithBox?: string;
|
|
68
|
+
timestamp: number;
|
|
69
|
+
hashId: string;
|
|
70
|
+
mergedHashIds?: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type MidsceneRecorderSemanticSource =
|
|
74
|
+
| 'aiDescribe'
|
|
75
|
+
| 'recorderAI'
|
|
76
|
+
| 'heuristic';
|
|
77
|
+
|
|
78
|
+
export type MidsceneRecorderSemanticStatus = 'pending' | 'ready' | 'failed';
|
|
79
|
+
|
|
80
|
+
export type MidsceneRecorderSemanticConfidence = 'high' | 'medium' | 'low';
|
|
81
|
+
|
|
82
|
+
export interface MidsceneRecorderSemanticAiDescribe {
|
|
83
|
+
verifyPrompt: boolean;
|
|
84
|
+
verifyPassed?: boolean;
|
|
85
|
+
deepLocate?: boolean;
|
|
86
|
+
centerDistance?: number;
|
|
87
|
+
expectedCenter?: [number, number];
|
|
88
|
+
actualCenter?: [number, number];
|
|
89
|
+
annotatedScreenshotPath?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface MidsceneRecorderSemantic {
|
|
93
|
+
source: MidsceneRecorderSemanticSource;
|
|
94
|
+
status: MidsceneRecorderSemanticStatus;
|
|
95
|
+
elementDescription?: string;
|
|
96
|
+
replayInstruction?: string;
|
|
97
|
+
actionSummary?: string;
|
|
98
|
+
confidence?: MidsceneRecorderSemanticConfidence;
|
|
99
|
+
error?: string;
|
|
100
|
+
aiDescribe?: MidsceneRecorderSemanticAiDescribe;
|
|
101
|
+
fallbackFrom?: MidsceneRecorderSemantic;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface MidsceneRecorderSemanticAction {
|
|
105
|
+
type: MidsceneRecorderEventType;
|
|
106
|
+
actionType?: string;
|
|
107
|
+
value?: string;
|
|
108
|
+
url?: string;
|
|
109
|
+
scrollDestinationDescription?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface MidsceneRecorderTarget {
|
|
113
|
+
platformId: MidsceneRecorderPlatformId;
|
|
114
|
+
deviceId?: string;
|
|
115
|
+
label?: string;
|
|
116
|
+
values: Record<string, string | number | boolean>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface MidsceneRecorderGeneratedCode {
|
|
120
|
+
markdown?: string;
|
|
121
|
+
yaml?: string;
|
|
122
|
+
playwright?: string;
|
|
123
|
+
updatedAt?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface MidsceneRecorderMarkdownScreenshotAsset {
|
|
127
|
+
eventIndex: number;
|
|
128
|
+
eventHashId: string;
|
|
129
|
+
eventType: MidsceneRecorderEventType;
|
|
130
|
+
relativePath: string;
|
|
131
|
+
dataUrl: string;
|
|
132
|
+
base64Data: string;
|
|
133
|
+
mimeType: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface MidsceneRecorderMarkdownScreenshotOptions {
|
|
137
|
+
baseDir?: string;
|
|
138
|
+
maxScreenshots?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const DEFAULT_MIDSCENE_RECORDER_MARKDOWN_MAX_SCREENSHOTS = 20;
|
|
142
|
+
|
|
143
|
+
function isMidsceneRecorderPendingDescription(value?: string) {
|
|
144
|
+
return value?.trim() === 'AI is analyzing element...';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getMidsceneRecorderSemantic(
|
|
148
|
+
event: Pick<MidsceneRecorderEvent, 'semantic'>,
|
|
149
|
+
) {
|
|
150
|
+
return event.semantic;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getRecorderPointerActionVerb(actionType?: string) {
|
|
154
|
+
switch (actionType) {
|
|
155
|
+
case 'Tap':
|
|
156
|
+
return 'Tap';
|
|
157
|
+
case 'DoubleClick':
|
|
158
|
+
return 'Double click';
|
|
159
|
+
case 'LongPress':
|
|
160
|
+
return 'Long press';
|
|
161
|
+
case 'RightClick':
|
|
162
|
+
return 'Right click';
|
|
163
|
+
default:
|
|
164
|
+
return 'Click';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getRecorderDragActionVerb(actionType?: string) {
|
|
169
|
+
switch (actionType) {
|
|
170
|
+
case 'Swipe':
|
|
171
|
+
return 'Swipe';
|
|
172
|
+
case 'DragAndDrop':
|
|
173
|
+
return 'Drag';
|
|
174
|
+
default:
|
|
175
|
+
return 'Drag';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function buildMidsceneRecorderReplayInstruction(
|
|
180
|
+
event: MidsceneRecorderSemanticAction,
|
|
181
|
+
elementDescription: string,
|
|
182
|
+
) {
|
|
183
|
+
switch (event.type) {
|
|
184
|
+
case 'navigation':
|
|
185
|
+
if (event.actionType === 'Stop') {
|
|
186
|
+
return 'Stop loading the current page.';
|
|
187
|
+
}
|
|
188
|
+
if (event.actionType === 'GoBack') {
|
|
189
|
+
return 'Go back in the browser.';
|
|
190
|
+
}
|
|
191
|
+
if (event.actionType === 'GoForward') {
|
|
192
|
+
return 'Go forward in the browser.';
|
|
193
|
+
}
|
|
194
|
+
if (event.actionType === 'Reload') {
|
|
195
|
+
return 'Reload the current page.';
|
|
196
|
+
}
|
|
197
|
+
if (event.actionType === 'NavigationChanged' && event.url) {
|
|
198
|
+
return `Wait for navigation to complete at \`${event.url}\`.`;
|
|
199
|
+
}
|
|
200
|
+
return event.url
|
|
201
|
+
? `Navigate to \`${event.url}\`.`
|
|
202
|
+
: `Navigate using ${elementDescription}.`;
|
|
203
|
+
case 'scroll':
|
|
204
|
+
return event.scrollDestinationDescription
|
|
205
|
+
? `Scroll the page/region with description "${elementDescription}" by value "${event.value || 'down'}" until "${event.scrollDestinationDescription}" is visible.`
|
|
206
|
+
: `Scroll the page/region with description "${elementDescription}" by value "${event.value || 'down'}".`;
|
|
207
|
+
case 'drag': {
|
|
208
|
+
const verb = getRecorderDragActionVerb(event.actionType);
|
|
209
|
+
return `${verb} through the area described as "${elementDescription}".`;
|
|
210
|
+
}
|
|
211
|
+
case 'input':
|
|
212
|
+
return `Input "${event.value || ''}" into the element described as "${elementDescription}".`;
|
|
213
|
+
case 'keydown':
|
|
214
|
+
return `Press "${event.value || 'the recorded key'}" on the element described as "${elementDescription}".`;
|
|
215
|
+
default: {
|
|
216
|
+
const verb = getRecorderPointerActionVerb(event.actionType);
|
|
217
|
+
if (verb === 'Long press') {
|
|
218
|
+
return `${verb} the element described as "${elementDescription}".`;
|
|
219
|
+
}
|
|
220
|
+
return `${verb} on the element described as "${elementDescription}".`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function buildMidsceneRecorderActionSummary(
|
|
226
|
+
event: MidsceneRecorderSemanticAction,
|
|
227
|
+
elementDescription: string,
|
|
228
|
+
) {
|
|
229
|
+
switch (event.type) {
|
|
230
|
+
case 'navigation':
|
|
231
|
+
if (event.actionType === 'Stop') {
|
|
232
|
+
return 'Stop page loading';
|
|
233
|
+
}
|
|
234
|
+
if (event.actionType === 'GoBack') {
|
|
235
|
+
return 'Go back';
|
|
236
|
+
}
|
|
237
|
+
if (event.actionType === 'GoForward') {
|
|
238
|
+
return 'Go forward';
|
|
239
|
+
}
|
|
240
|
+
if (event.actionType === 'Reload') {
|
|
241
|
+
return 'Reload page';
|
|
242
|
+
}
|
|
243
|
+
if (event.actionType === 'NavigationChanged' && event.url) {
|
|
244
|
+
return `Wait for navigation to complete at ${event.url}`;
|
|
245
|
+
}
|
|
246
|
+
return event.url ? `Navigate to ${event.url}` : 'Navigate';
|
|
247
|
+
case 'scroll':
|
|
248
|
+
return event.scrollDestinationDescription
|
|
249
|
+
? `Scroll ${elementDescription} toward ${event.scrollDestinationDescription}`
|
|
250
|
+
: `Scroll ${elementDescription}`;
|
|
251
|
+
case 'drag':
|
|
252
|
+
return `${getRecorderDragActionVerb(event.actionType)} ${elementDescription}`;
|
|
253
|
+
case 'input':
|
|
254
|
+
return `Input into ${elementDescription}`;
|
|
255
|
+
case 'keydown':
|
|
256
|
+
return `Press ${event.value || 'key'} on ${elementDescription}`;
|
|
257
|
+
default:
|
|
258
|
+
return `${getRecorderPointerActionVerb(event.actionType)} ${elementDescription}`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function getMidsceneRecorderEventDescription(
|
|
263
|
+
event: MidsceneRecorderEvent,
|
|
264
|
+
) {
|
|
265
|
+
const semantic = getMidsceneRecorderSemantic(event);
|
|
266
|
+
if (
|
|
267
|
+
semantic?.actionSummary &&
|
|
268
|
+
!isMidsceneRecorderPendingDescription(semantic.actionSummary)
|
|
269
|
+
) {
|
|
270
|
+
return semantic.actionSummary;
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
semantic?.elementDescription &&
|
|
274
|
+
!isMidsceneRecorderPendingDescription(semantic.elementDescription)
|
|
275
|
+
) {
|
|
276
|
+
return semantic.elementDescription;
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
semantic?.replayInstruction &&
|
|
280
|
+
!isMidsceneRecorderPendingDescription(semantic.replayInstruction)
|
|
281
|
+
) {
|
|
282
|
+
return semantic.replayInstruction;
|
|
283
|
+
}
|
|
284
|
+
if (event.type === 'navigation' && event.url) {
|
|
285
|
+
return `Navigate to ${event.url}`;
|
|
286
|
+
}
|
|
287
|
+
if (event.value) {
|
|
288
|
+
return event.actionType
|
|
289
|
+
? `${event.actionType} ${event.value}`
|
|
290
|
+
: event.value;
|
|
291
|
+
}
|
|
292
|
+
if (
|
|
293
|
+
event.elementRect?.x !== undefined &&
|
|
294
|
+
event.elementRect?.y !== undefined
|
|
295
|
+
) {
|
|
296
|
+
const prefix = event.actionType || event.type;
|
|
297
|
+
return `${prefix} (${Math.round(event.elementRect.x)}, ${Math.round(
|
|
298
|
+
event.elementRect.y,
|
|
299
|
+
)})`;
|
|
300
|
+
}
|
|
301
|
+
return event.actionType || event.type;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function getMidsceneRecorderScreenshotsForLLM(
|
|
305
|
+
events: MidsceneRecorderEvent[],
|
|
306
|
+
maxScreenshots = 1,
|
|
307
|
+
) {
|
|
308
|
+
return selectRecorderScreenshotCandidates(
|
|
309
|
+
getRecorderScreenshotCandidates(events),
|
|
310
|
+
maxScreenshots,
|
|
311
|
+
).map((candidate) => candidate.screenshot);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function sanitizeMidsceneRecorderFileName(value: string) {
|
|
315
|
+
return (
|
|
316
|
+
value
|
|
317
|
+
.trim()
|
|
318
|
+
.replace(/[\\/:*?"<>|]+/g, '-')
|
|
319
|
+
.replace(/\s+/g, '-')
|
|
320
|
+
.replace(/-+/g, '-')
|
|
321
|
+
.replace(/^-|-$/g, '')
|
|
322
|
+
.toLowerCase() || 'midscene-recording'
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function normalizeMarkdownAssetBaseDir(baseDir?: string) {
|
|
327
|
+
const value = (baseDir || './screenshots').replace(/\/+$/g, '');
|
|
328
|
+
if (value.startsWith('./') || value.startsWith('../')) {
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
return `./${value}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function padEventIndex(index: number) {
|
|
335
|
+
return String(index + 1).padStart(3, '0');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function parseScreenshotDataUrl(value: string):
|
|
339
|
+
| {
|
|
340
|
+
dataUrl: string;
|
|
341
|
+
base64Data: string;
|
|
342
|
+
mimeType: string;
|
|
343
|
+
extension: string;
|
|
344
|
+
}
|
|
345
|
+
| undefined {
|
|
346
|
+
const dataUrlMatch = value.match(
|
|
347
|
+
/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/,
|
|
348
|
+
);
|
|
349
|
+
if (dataUrlMatch) {
|
|
350
|
+
const mimeType = dataUrlMatch[1];
|
|
351
|
+
const extension = mimeType.includes('jpeg')
|
|
352
|
+
? 'jpg'
|
|
353
|
+
: mimeType.split('/')[1]?.replace(/[^a-zA-Z0-9]/g, '') || 'png';
|
|
354
|
+
return {
|
|
355
|
+
dataUrl: value,
|
|
356
|
+
base64Data: dataUrlMatch[2],
|
|
357
|
+
mimeType,
|
|
358
|
+
extension,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (/^[a-zA-Z0-9+/=\s]+$/.test(value) && value.trim().length > 0) {
|
|
363
|
+
const base64Data = value.replace(/\s+/g, '');
|
|
364
|
+
return {
|
|
365
|
+
dataUrl: `data:image/png;base64,${base64Data}`,
|
|
366
|
+
base64Data,
|
|
367
|
+
mimeType: 'image/png',
|
|
368
|
+
extension: 'png',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function getRecorderEventScreenshot(event: MidsceneRecorderEvent) {
|
|
376
|
+
return (
|
|
377
|
+
event.screenshotWithBox || event.screenshotAfter || event.screenshotBefore
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function hasCoordinateFallback(event: MidsceneRecorderEvent) {
|
|
382
|
+
const semantic = getMidsceneRecorderSemantic(event);
|
|
383
|
+
return (
|
|
384
|
+
!semantic?.elementDescription &&
|
|
385
|
+
event.elementRect?.x !== undefined &&
|
|
386
|
+
event.elementRect?.y !== undefined
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function shouldIncludeMarkdownScreenshot(
|
|
391
|
+
event: MidsceneRecorderEvent,
|
|
392
|
+
eventIndex: number,
|
|
393
|
+
lastEventIndex: number,
|
|
394
|
+
) {
|
|
395
|
+
const semantic = getMidsceneRecorderSemantic(event);
|
|
396
|
+
return (
|
|
397
|
+
eventIndex === 0 ||
|
|
398
|
+
eventIndex === lastEventIndex ||
|
|
399
|
+
event.type === 'navigation' ||
|
|
400
|
+
event.type === 'scroll' ||
|
|
401
|
+
event.type === 'input' ||
|
|
402
|
+
Boolean(event.screenshotWithBox) ||
|
|
403
|
+
!semantic?.elementDescription ||
|
|
404
|
+
hasCoordinateFallback(event)
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
interface RecorderScreenshotCandidate {
|
|
409
|
+
event: MidsceneRecorderEvent;
|
|
410
|
+
eventIndex: number;
|
|
411
|
+
screenshot: string;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function getRecorderScreenshotCandidatePriority(
|
|
415
|
+
candidate: RecorderScreenshotCandidate,
|
|
416
|
+
firstEventIndex: number,
|
|
417
|
+
lastEventIndex: number,
|
|
418
|
+
) {
|
|
419
|
+
const event = candidate.event;
|
|
420
|
+
let priority = 0;
|
|
421
|
+
|
|
422
|
+
if (candidate.eventIndex === firstEventIndex) {
|
|
423
|
+
priority += 100;
|
|
424
|
+
}
|
|
425
|
+
if (candidate.eventIndex === lastEventIndex) {
|
|
426
|
+
priority += 95;
|
|
427
|
+
}
|
|
428
|
+
if (event.type === 'navigation') {
|
|
429
|
+
priority += 80;
|
|
430
|
+
}
|
|
431
|
+
if (event.screenshotWithBox) {
|
|
432
|
+
priority += 70;
|
|
433
|
+
}
|
|
434
|
+
const semantic = getMidsceneRecorderSemantic(event);
|
|
435
|
+
if (
|
|
436
|
+
semantic?.source === 'heuristic' ||
|
|
437
|
+
semantic?.confidence === 'low' ||
|
|
438
|
+
semantic?.error
|
|
439
|
+
) {
|
|
440
|
+
priority += 60;
|
|
441
|
+
}
|
|
442
|
+
if (event.type === 'input' || event.type === 'scroll') {
|
|
443
|
+
priority += 40;
|
|
444
|
+
}
|
|
445
|
+
if (!semantic?.elementDescription || hasCoordinateFallback(event)) {
|
|
446
|
+
priority += 30;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return priority;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function selectEvenlyDistributedCandidates<
|
|
453
|
+
T extends RecorderScreenshotCandidate,
|
|
454
|
+
>(candidates: T[], count: number) {
|
|
455
|
+
if (count <= 0) {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
if (candidates.length <= count) {
|
|
459
|
+
return candidates;
|
|
460
|
+
}
|
|
461
|
+
if (count === 1) {
|
|
462
|
+
return [candidates[Math.floor((candidates.length - 1) / 2)]];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return Array.from({ length: count }, (_, index) => {
|
|
466
|
+
const candidateIndex = Math.round(
|
|
467
|
+
(index * (candidates.length - 1)) / (count - 1),
|
|
468
|
+
);
|
|
469
|
+
return candidates[candidateIndex];
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function selectRecorderScreenshotCandidates<
|
|
474
|
+
T extends RecorderScreenshotCandidate,
|
|
475
|
+
>(candidates: T[], maxScreenshots: number): T[] {
|
|
476
|
+
if (maxScreenshots <= 0 || candidates.length === 0) {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
if (candidates.length <= maxScreenshots) {
|
|
480
|
+
return candidates;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const selected = new Map<number, T>();
|
|
484
|
+
const firstEventIndex = candidates[0].eventIndex;
|
|
485
|
+
const lastEventIndex = candidates[candidates.length - 1].eventIndex;
|
|
486
|
+
const addCandidate = (candidate: T | undefined) => {
|
|
487
|
+
if (!candidate || selected.size >= maxScreenshots) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
selected.set(candidate.eventIndex, candidate);
|
|
491
|
+
};
|
|
492
|
+
const addEvenly = (pool: T[]) => {
|
|
493
|
+
const remaining = maxScreenshots - selected.size;
|
|
494
|
+
if (remaining <= 0) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const unselected = pool.filter(
|
|
498
|
+
(candidate) => !selected.has(candidate.eventIndex),
|
|
499
|
+
);
|
|
500
|
+
for (const candidate of selectEvenlyDistributedCandidates(
|
|
501
|
+
unselected,
|
|
502
|
+
remaining,
|
|
503
|
+
)) {
|
|
504
|
+
addCandidate(candidate);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
addCandidate(candidates[0]);
|
|
509
|
+
addCandidate(candidates[candidates.length - 1]);
|
|
510
|
+
|
|
511
|
+
addEvenly(
|
|
512
|
+
candidates.filter(
|
|
513
|
+
(candidate) =>
|
|
514
|
+
getRecorderScreenshotCandidatePriority(
|
|
515
|
+
candidate,
|
|
516
|
+
firstEventIndex,
|
|
517
|
+
lastEventIndex,
|
|
518
|
+
) >= 60,
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
addEvenly(
|
|
522
|
+
candidates.filter(
|
|
523
|
+
(candidate) =>
|
|
524
|
+
getRecorderScreenshotCandidatePriority(
|
|
525
|
+
candidate,
|
|
526
|
+
firstEventIndex,
|
|
527
|
+
lastEventIndex,
|
|
528
|
+
) >= 40,
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
addEvenly(candidates);
|
|
532
|
+
|
|
533
|
+
return Array.from(selected.values()).sort(
|
|
534
|
+
(left, right) => left.eventIndex - right.eventIndex,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function getRecorderScreenshotCandidates(
|
|
539
|
+
events: MidsceneRecorderEvent[],
|
|
540
|
+
): RecorderScreenshotCandidate[] {
|
|
541
|
+
const candidates: RecorderScreenshotCandidate[] = [];
|
|
542
|
+
const seenScreenshots = new Set<string>();
|
|
543
|
+
const lastEventIndex = events.length - 1;
|
|
544
|
+
|
|
545
|
+
for (let eventIndex = 0; eventIndex < events.length; eventIndex += 1) {
|
|
546
|
+
const event = events[eventIndex];
|
|
547
|
+
if (!shouldIncludeMarkdownScreenshot(event, eventIndex, lastEventIndex)) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const screenshot = getRecorderEventScreenshot(event);
|
|
552
|
+
if (!screenshot || seenScreenshots.has(screenshot)) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
seenScreenshots.add(screenshot);
|
|
557
|
+
candidates.push({
|
|
558
|
+
event,
|
|
559
|
+
eventIndex,
|
|
560
|
+
screenshot,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return candidates;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function createMidsceneRecorderMarkdownScreenshotAssets(
|
|
568
|
+
events: MidsceneRecorderEvent[],
|
|
569
|
+
options: MidsceneRecorderMarkdownScreenshotOptions = {},
|
|
570
|
+
): MidsceneRecorderMarkdownScreenshotAsset[] {
|
|
571
|
+
const baseDir = normalizeMarkdownAssetBaseDir(options.baseDir);
|
|
572
|
+
const maxScreenshots =
|
|
573
|
+
options.maxScreenshots ??
|
|
574
|
+
DEFAULT_MIDSCENE_RECORDER_MARKDOWN_MAX_SCREENSHOTS;
|
|
575
|
+
const candidates: Array<
|
|
576
|
+
RecorderScreenshotCandidate & {
|
|
577
|
+
parsedScreenshot: NonNullable<ReturnType<typeof parseScreenshotDataUrl>>;
|
|
578
|
+
}
|
|
579
|
+
> = [];
|
|
580
|
+
|
|
581
|
+
for (const candidate of getRecorderScreenshotCandidates(events)) {
|
|
582
|
+
const parsedScreenshot = parseScreenshotDataUrl(candidate.screenshot);
|
|
583
|
+
if (parsedScreenshot) {
|
|
584
|
+
candidates.push({
|
|
585
|
+
...candidate,
|
|
586
|
+
parsedScreenshot,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return selectRecorderScreenshotCandidates(candidates, maxScreenshots).map(
|
|
592
|
+
({ event, eventIndex, parsedScreenshot }) => {
|
|
593
|
+
const safeType = event.type.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
594
|
+
const fileName = `event-${padEventIndex(eventIndex)}-${safeType}.${parsedScreenshot.extension}`;
|
|
595
|
+
return {
|
|
596
|
+
eventIndex,
|
|
597
|
+
eventHashId: event.hashId,
|
|
598
|
+
eventType: event.type,
|
|
599
|
+
relativePath: `${baseDir}/${fileName}`,
|
|
600
|
+
dataUrl: parsedScreenshot.dataUrl,
|
|
601
|
+
base64Data: parsedScreenshot.base64Data,
|
|
602
|
+
mimeType: parsedScreenshot.mimeType,
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function scalarToYaml(value: string | number | boolean) {
|
|
609
|
+
return JSON.stringify(value);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function stringifyMidsceneRecorderTargetBlock(
|
|
613
|
+
target: MidsceneRecorderTarget,
|
|
614
|
+
) {
|
|
615
|
+
const lines = [`${target.platformId}:`];
|
|
616
|
+
const values = Object.entries(target.values);
|
|
617
|
+
if (values.length === 0) {
|
|
618
|
+
lines.push(' {}');
|
|
619
|
+
return lines.join('\n');
|
|
620
|
+
}
|
|
621
|
+
for (const [key, value] of values) {
|
|
622
|
+
lines.push(` ${key}: ${scalarToYaml(value)}`);
|
|
623
|
+
}
|
|
624
|
+
return lines.join('\n');
|
|
625
|
+
}
|