@midscene/playground 1.10.4 → 1.10.5-beta-20260714101501.0
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/adapters/remote-execution.mjs +38 -1
- package/dist/es/adapters/remote-execution.mjs.map +1 -1
- package/dist/es/mjpeg-hub.mjs +7 -6
- package/dist/es/mjpeg-hub.mjs.map +1 -1
- package/dist/es/mjpeg-stream-handler.mjs +7 -1
- package/dist/es/mjpeg-stream-handler.mjs.map +1 -1
- package/dist/es/platform.mjs.map +1 -1
- package/dist/es/sdk/index.mjs +10 -0
- package/dist/es/sdk/index.mjs.map +1 -1
- package/dist/es/server.mjs +369 -35
- package/dist/es/server.mjs.map +1 -1
- package/dist/lib/adapters/remote-execution.js +38 -1
- package/dist/lib/adapters/remote-execution.js.map +1 -1
- package/dist/lib/mjpeg-hub.js +7 -6
- package/dist/lib/mjpeg-hub.js.map +1 -1
- package/dist/lib/mjpeg-stream-handler.js +7 -1
- package/dist/lib/mjpeg-stream-handler.js.map +1 -1
- package/dist/lib/platform.js.map +1 -1
- package/dist/lib/sdk/index.js +10 -0
- package/dist/lib/sdk/index.js.map +1 -1
- package/dist/lib/server.js +367 -33
- package/dist/lib/server.js.map +1 -1
- package/dist/types/adapters/remote-execution.d.ts +3 -0
- package/dist/types/platform.d.ts +1 -0
- package/dist/types/sdk/index.d.ts +3 -0
- package/dist/types/server.d.ts +18 -1
- package/dist/types/types.d.ts +1 -0
- package/package.json +3 -3
- package/static/index.html +1 -1
- package/static/static/css/index.2b6b43ad.css +2 -0
- package/static/static/css/index.2b6b43ad.css.map +1 -0
- package/static/static/js/{445.884e7c8e.js → 890.03bb43a9.js} +3 -3
- package/static/static/js/{445.884e7c8e.js.LICENSE.txt → 890.03bb43a9.js.LICENSE.txt} +0 -2
- package/static/static/js/890.03bb43a9.js.map +1 -0
- package/static/static/js/index.2a85361c.js +960 -0
- package/static/static/js/index.2a85361c.js.map +1 -0
- package/static/static/css/index.1293237f.css +0 -2
- package/static/static/css/index.1293237f.css.map +0 -1
- package/static/static/js/445.884e7c8e.js.map +0 -1
- package/static/static/js/index.babddf12.js +0 -960
- package/static/static/js/index.babddf12.js.map +0 -1
- /package/static/static/js/{index.babddf12.js.LICENSE.txt → index.2a85361c.js.LICENSE.txt} +0 -0
package/dist/lib/server.js
CHANGED
|
@@ -80,8 +80,28 @@ function _define_property(obj, key, value) {
|
|
|
80
80
|
}
|
|
81
81
|
const defaultPort = constants_namespaceObject.PLAYGROUND_SERVER_PORT;
|
|
82
82
|
const RECORDER_CAPTURE_AFTER_INTERACT_DELAY_MS = 250;
|
|
83
|
+
const RECORDER_TYPE_ONLY_INPUT_SETTLE_DELAY_MS = 750;
|
|
84
|
+
const RECORDER_NAVIGATION_OBSERVATION_INTERVAL_MS = 250;
|
|
85
|
+
const RECORDER_NAVIGATION_OBSERVATION_TIMEOUT_MS = 35000;
|
|
83
86
|
const RECORDER_AI_DESCRIBE_AFTER_INTERACT_TIMEOUT_MS = 30000;
|
|
87
|
+
const RECORDER_AI_DESCRIBE_VERIFY_PROMPT = false;
|
|
84
88
|
const RECORDER_AI_DESCRIBE_SCREENSHOT_DUMP_DIR = 'recorder-ai-describe-screenshots';
|
|
89
|
+
const RECORDER_SCREENSHOT_ASSET_DIR = 'recorder-screenshots';
|
|
90
|
+
const RECORDER_SCREENSHOT_ASSET_MAX_SESSION_BYTES = 134217728;
|
|
91
|
+
const RECORDER_SCREENSHOT_ASSET_MAX_TOTAL_BYTES = 536870912;
|
|
92
|
+
const recorderScreenshotAssetQuotaExceededSessions = new Set();
|
|
93
|
+
const recorderScreenshotAssetBytesBySession = new Map();
|
|
94
|
+
let recorderScreenshotAssetUsageInitialized = false;
|
|
95
|
+
function shouldPersistStudioPreviewRecorderScreenshot(actionType) {
|
|
96
|
+
return [
|
|
97
|
+
'Tap',
|
|
98
|
+
'DoubleClick',
|
|
99
|
+
'LongPress',
|
|
100
|
+
'RightClick',
|
|
101
|
+
'DragAndDrop',
|
|
102
|
+
'Input'
|
|
103
|
+
].includes(actionType);
|
|
104
|
+
}
|
|
85
105
|
function createElementDescriberRuntime(agent) {
|
|
86
106
|
const modelConfigManager = agent.modelConfigManager;
|
|
87
107
|
const missingModelRuntime = void 0;
|
|
@@ -144,8 +164,10 @@ function buildRecorderEventSummary(event) {
|
|
|
144
164
|
pageInfo: event.pageInfo
|
|
145
165
|
};
|
|
146
166
|
}
|
|
147
|
-
function
|
|
148
|
-
|
|
167
|
+
function sanitizeRecorderPathSegment(value, fallback, maxLength = 64) {
|
|
168
|
+
const normalized = 'string' == typeof value && value.trim() ? value.trim() : fallback;
|
|
169
|
+
const sanitized = normalized.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
170
|
+
return (sanitized || fallback).slice(0, maxLength);
|
|
149
171
|
}
|
|
150
172
|
function formatRecorderAiDescribeScreenshotPathParts() {
|
|
151
173
|
const date = new Date();
|
|
@@ -197,6 +219,141 @@ function writeRecorderAiDescribeScreenshot(imageBase64, suffix) {
|
|
|
197
219
|
mimeType
|
|
198
220
|
};
|
|
199
221
|
}
|
|
222
|
+
function getRecorderScreenshotAssetRoot() {
|
|
223
|
+
const root = (0, external_node_path_namespaceObject.resolve)((0, common_namespaceObject.getMidsceneRunSubDir)('output'), RECORDER_SCREENSHOT_ASSET_DIR);
|
|
224
|
+
(0, external_node_fs_namespaceObject.mkdirSync)(root, {
|
|
225
|
+
recursive: true
|
|
226
|
+
});
|
|
227
|
+
return root;
|
|
228
|
+
}
|
|
229
|
+
function assertRecorderScreenshotAssetId(assetId) {
|
|
230
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(assetId)) throw new Error('Invalid recorder screenshot asset id');
|
|
231
|
+
return assetId;
|
|
232
|
+
}
|
|
233
|
+
function recorderScreenshotAssetExtension(mimeType) {
|
|
234
|
+
return mimeType?.includes('jpeg') ? 'jpg' : 'png';
|
|
235
|
+
}
|
|
236
|
+
function recorderScreenshotAssetPath(assetId, mimeType) {
|
|
237
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
238
|
+
const safeAssetId = assertRecorderScreenshotAssetId(assetId);
|
|
239
|
+
return assertPathInsideDirectory(root, (0, external_node_path_namespaceObject.join)(root, `${safeAssetId}.${recorderScreenshotAssetExtension(mimeType)}`));
|
|
240
|
+
}
|
|
241
|
+
function getRecorderScreenshotAssetUsage(sessionId) {
|
|
242
|
+
initializeRecorderScreenshotAssetUsage();
|
|
243
|
+
const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
|
|
244
|
+
const sessionBytes = recorderScreenshotAssetBytesBySession.get(safeSessionId) || 0;
|
|
245
|
+
const totalBytes = Array.from(recorderScreenshotAssetBytesBySession.values()).reduce((total, bytes)=>total + bytes, 0);
|
|
246
|
+
return {
|
|
247
|
+
totalBytes,
|
|
248
|
+
sessionBytes
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function initializeRecorderScreenshotAssetUsage() {
|
|
252
|
+
if (recorderScreenshotAssetUsageInitialized) return;
|
|
253
|
+
recorderScreenshotAssetUsageInitialized = true;
|
|
254
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
255
|
+
try {
|
|
256
|
+
for (const entry of (0, external_node_fs_namespaceObject.readdirSync)(root, {
|
|
257
|
+
withFileTypes: true
|
|
258
|
+
})){
|
|
259
|
+
if (!entry.isFile()) continue;
|
|
260
|
+
const match = entry.name.match(/^(.*)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(?:png|jpg)$/i);
|
|
261
|
+
if (!match?.[1]) continue;
|
|
262
|
+
const sessionId = match[1];
|
|
263
|
+
const bytes = (0, external_node_fs_namespaceObject.statSync)((0, external_node_path_namespaceObject.join)(root, entry.name)).size;
|
|
264
|
+
recorderScreenshotAssetBytesBySession.set(sessionId, (recorderScreenshotAssetBytesBySession.get(sessionId) || 0) + bytes);
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
debugRecorderAssets('failed to initialize recorder screenshot asset usage:', error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function persistRecorderScreenshotAsset(sessionId, imageBase64) {
|
|
271
|
+
if (!imageBase64?.startsWith('data:')) return;
|
|
272
|
+
const { base64, mimeType } = extractBase64Payload(imageBase64);
|
|
273
|
+
const bytes = Buffer.from(base64, 'base64');
|
|
274
|
+
const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
|
|
275
|
+
const usage = getRecorderScreenshotAssetUsage(sessionId);
|
|
276
|
+
if (usage.sessionBytes + bytes.byteLength > RECORDER_SCREENSHOT_ASSET_MAX_SESSION_BYTES || usage.totalBytes + bytes.byteLength > RECORDER_SCREENSHOT_ASSET_MAX_TOTAL_BYTES) {
|
|
277
|
+
if (!recorderScreenshotAssetQuotaExceededSessions.has(safeSessionId)) {
|
|
278
|
+
recorderScreenshotAssetQuotaExceededSessions.add(safeSessionId);
|
|
279
|
+
debugRecorderAssets('recorder screenshot asset quota reached %o', {
|
|
280
|
+
sessionId: safeSessionId,
|
|
281
|
+
sessionBytes: usage.sessionBytes,
|
|
282
|
+
totalBytes: usage.totalBytes
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const assetId = `${safeSessionId}-${(0, external_node_crypto_namespaceObject.randomUUID)()}`;
|
|
288
|
+
const normalizedMimeType = mimeType || 'image/png';
|
|
289
|
+
const filePath = recorderScreenshotAssetPath(assetId, normalizedMimeType);
|
|
290
|
+
try {
|
|
291
|
+
(0, external_node_fs_namespaceObject.writeFileSync)(filePath, bytes);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
debugRecorderAssets('failed to persist recorder screenshot asset:', error);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
recorderScreenshotAssetBytesBySession.set(safeSessionId, usage.sessionBytes + bytes.byteLength);
|
|
297
|
+
return {
|
|
298
|
+
id: assetId,
|
|
299
|
+
mimeType: normalizedMimeType,
|
|
300
|
+
bytes: bytes.byteLength
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function readRecorderScreenshotAsset(asset) {
|
|
304
|
+
const filePath = recorderScreenshotAssetPath(asset.id, asset.mimeType);
|
|
305
|
+
if (!(0, external_node_fs_namespaceObject.existsSync)(filePath)) return;
|
|
306
|
+
return `data:${asset.mimeType};base64,${(0, external_node_fs_namespaceObject.readFileSync)(filePath).toString('base64')}`;
|
|
307
|
+
}
|
|
308
|
+
function findRecorderScreenshotAssetPath(assetId) {
|
|
309
|
+
const safeAssetId = assertRecorderScreenshotAssetId(assetId);
|
|
310
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
311
|
+
for (const extension of [
|
|
312
|
+
'png',
|
|
313
|
+
'jpg'
|
|
314
|
+
]){
|
|
315
|
+
const filePath = assertPathInsideDirectory(root, (0, external_node_path_namespaceObject.join)(root, `${safeAssetId}.${extension}`));
|
|
316
|
+
if ((0, external_node_fs_namespaceObject.existsSync)(filePath)) return filePath;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function removeRecorderScreenshotAssetsForSession(sessionId) {
|
|
320
|
+
const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
|
|
321
|
+
const prefix = `${safeSessionId}-`;
|
|
322
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
323
|
+
for (const entry of (0, external_node_fs_namespaceObject.readdirSync)(root, {
|
|
324
|
+
withFileTypes: true
|
|
325
|
+
}))if (entry.isFile() && entry.name.startsWith(prefix)) (0, external_node_fs_namespaceObject.rmSync)((0, external_node_path_namespaceObject.join)(root, entry.name), {
|
|
326
|
+
force: true
|
|
327
|
+
});
|
|
328
|
+
recorderScreenshotAssetQuotaExceededSessions.delete(safeSessionId);
|
|
329
|
+
recorderScreenshotAssetBytesBySession.delete(safeSessionId);
|
|
330
|
+
}
|
|
331
|
+
function pruneRecorderScreenshotAssetsForSession(sessionId, retainedAssetIds) {
|
|
332
|
+
const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
|
|
333
|
+
const prefix = `${safeSessionId}-`;
|
|
334
|
+
const retained = new Set(retainedAssetIds.map((assetId)=>{
|
|
335
|
+
const safeAssetId = assertRecorderScreenshotAssetId(assetId);
|
|
336
|
+
if (!safeAssetId.startsWith(prefix)) throw new Error('Recorder screenshot asset does not belong to session');
|
|
337
|
+
return safeAssetId;
|
|
338
|
+
}));
|
|
339
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
340
|
+
let sessionBytes = 0;
|
|
341
|
+
for (const entry of (0, external_node_fs_namespaceObject.readdirSync)(root, {
|
|
342
|
+
withFileTypes: true
|
|
343
|
+
})){
|
|
344
|
+
if (!entry.isFile() || !entry.name.startsWith(prefix)) continue;
|
|
345
|
+
const assetId = entry.name.replace(/\.(?:png|jpg)$/i, '');
|
|
346
|
+
const filePath = (0, external_node_path_namespaceObject.join)(root, entry.name);
|
|
347
|
+
if (!retained.has(assetId)) {
|
|
348
|
+
(0, external_node_fs_namespaceObject.rmSync)(filePath, {
|
|
349
|
+
force: true
|
|
350
|
+
});
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
sessionBytes += (0, external_node_fs_namespaceObject.statSync)(filePath).size;
|
|
354
|
+
}
|
|
355
|
+
recorderScreenshotAssetBytesBySession.set(safeSessionId, sessionBytes);
|
|
356
|
+
}
|
|
200
357
|
function calculateRecorderScreenshotAnnotation(event, imageSize, verifyResult) {
|
|
201
358
|
const pageWidth = event.pageInfo?.width;
|
|
202
359
|
const pageHeight = event.pageInfo?.height;
|
|
@@ -353,6 +510,9 @@ const debugMjpeg = (0, logger_namespaceObject.getDebug)('playground:mjpeg', {
|
|
|
353
510
|
const debugInteract = (0, logger_namespaceObject.getDebug)('playground:interact', {
|
|
354
511
|
console: true
|
|
355
512
|
});
|
|
513
|
+
const debugRecorderAssets = (0, logger_namespaceObject.getDebug)('playground:recorder-assets', {
|
|
514
|
+
console: true
|
|
515
|
+
});
|
|
356
516
|
class InteractParamsValidationError extends Error {
|
|
357
517
|
constructor(message){
|
|
358
518
|
super(message);
|
|
@@ -742,8 +902,10 @@ class PlaygroundServer {
|
|
|
742
902
|
};
|
|
743
903
|
}
|
|
744
904
|
resetRecorderState() {
|
|
905
|
+
this.clearPendingTypeOnlyRecorderInputFlushTimer();
|
|
745
906
|
this._recorderSessionId = null;
|
|
746
907
|
this._recorderEvents = [];
|
|
908
|
+
this._recorderPendingTypeOnlyInput = null;
|
|
747
909
|
this._recorderEventQueue = Promise.resolve();
|
|
748
910
|
this._studioPreviewRecorderLastTargetPoint = void 0;
|
|
749
911
|
this._studioPreviewRecorderLastScreenshot = void 0;
|
|
@@ -850,8 +1012,13 @@ class PlaygroundServer {
|
|
|
850
1012
|
const initialNavigationEvent = this.buildStudioPreviewInitialNavigationEvent(initialPageState, this._studioPreviewRecorderLastScreenshot);
|
|
851
1013
|
if (initialNavigationEvent) this._recorderEvents.push(initialNavigationEvent);
|
|
852
1014
|
}
|
|
1015
|
+
persistStudioPreviewRecorderScreenshot(screenshot) {
|
|
1016
|
+
if (!this._recorderSessionId) return;
|
|
1017
|
+
return persistRecorderScreenshotAsset(this._recorderSessionId, screenshot);
|
|
1018
|
+
}
|
|
853
1019
|
async storeStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
|
|
854
1020
|
if (!this._recorderSessionId) return;
|
|
1021
|
+
const sessionId = this._recorderSessionId;
|
|
855
1022
|
const before = snapshotBefore || await this.captureRecorderSnapshotBeforeInteract();
|
|
856
1023
|
const screenshotBefore = before?.screenshot;
|
|
857
1024
|
debugInteract('recorder capture scheduled after action %o', {
|
|
@@ -879,6 +1046,48 @@ class PlaygroundServer {
|
|
|
879
1046
|
this._studioPreviewRecorderLastScreenshot = screenshotAfter;
|
|
880
1047
|
this._studioPreviewRecorderLastPageState = pageStateAfter;
|
|
881
1048
|
this.queueStudioPreviewRecorderEventAppend(event, navigationEvent);
|
|
1049
|
+
if (!navigationEvent) this.observeStudioPreviewNavigationChange(payload, before?.pageState, event.hashId, sessionId);
|
|
1050
|
+
}
|
|
1051
|
+
observeStudioPreviewNavigationChange(payload, pageStateBefore, triggerEventHashId, sessionId) {
|
|
1052
|
+
const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
|
|
1053
|
+
const beforeUrl = pageStateBefore?.url;
|
|
1054
|
+
if (!actionType || !beforeUrl || BROWSER_CHROME_NAVIGATION_ACTIONS.has(actionType)) return;
|
|
1055
|
+
(async ()=>{
|
|
1056
|
+
const deadline = Date.now() + RECORDER_NAVIGATION_OBSERVATION_TIMEOUT_MS;
|
|
1057
|
+
while(this._recorderSessionId === sessionId && Date.now() < deadline){
|
|
1058
|
+
await (0, utils_namespaceObject.sleep)(RECORDER_NAVIGATION_OBSERVATION_INTERVAL_MS);
|
|
1059
|
+
if (this._recorderSessionId !== sessionId) return;
|
|
1060
|
+
const currentUrl = await this.getActivePageUrl();
|
|
1061
|
+
if (!currentUrl || currentUrl === beforeUrl) continue;
|
|
1062
|
+
const screenshot = await this.takeRecorderScreenshot();
|
|
1063
|
+
const pageStateAfter = await this.getActiveRecorderPageState();
|
|
1064
|
+
const navigationEvent = this.buildStudioPreviewNavigationChangeEvent(payload, pageStateBefore, pageStateAfter, screenshot);
|
|
1065
|
+
if (!navigationEvent) return;
|
|
1066
|
+
navigationEvent.rawPayload = {
|
|
1067
|
+
...navigationEvent.rawPayload,
|
|
1068
|
+
triggerEventHashId
|
|
1069
|
+
};
|
|
1070
|
+
if (this.appendStudioPreviewNavigationEventAfterTrigger(triggerEventHashId, navigationEvent)) {
|
|
1071
|
+
this._studioPreviewRecorderLastScreenshot = screenshot;
|
|
1072
|
+
this._studioPreviewRecorderLastPageState = pageStateAfter;
|
|
1073
|
+
}
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
})().catch((error)=>{
|
|
1077
|
+
debugInteract('delayed recorder navigation observation failed:', error);
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
appendStudioPreviewNavigationEventAfterTrigger(triggerEventHashId, navigationEvent) {
|
|
1081
|
+
const triggerIndex = this._recorderEvents.findIndex((event)=>event.hashId === triggerEventHashId);
|
|
1082
|
+
if (triggerIndex < 0) return false;
|
|
1083
|
+
const alreadyRecorded = this._recorderEvents.some((event)=>{
|
|
1084
|
+
if ('NavigationChanged' !== event.actionType) return false;
|
|
1085
|
+
const rawPayload = event.rawPayload;
|
|
1086
|
+
return rawPayload?.triggerEventHashId === triggerEventHashId;
|
|
1087
|
+
});
|
|
1088
|
+
if (alreadyRecorded) return false;
|
|
1089
|
+
this._recorderEvents.splice(triggerIndex + 1, 0, navigationEvent);
|
|
1090
|
+
return true;
|
|
882
1091
|
}
|
|
883
1092
|
async buildStudioPreviewRecorderEvent(payload, pageState, screenshotBefore, screenshotAfter) {
|
|
884
1093
|
const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
|
|
@@ -898,10 +1107,12 @@ class PlaygroundServer {
|
|
|
898
1107
|
const endX = 'number' == typeof payload.endX ? payload.endX : void 0;
|
|
899
1108
|
const endY = 'number' == typeof payload.endY ? payload.endY : void 0;
|
|
900
1109
|
const dragDescription = void 0 !== x && void 0 !== y && void 0 !== endX && void 0 !== endY ? `${Math.round(x)},${Math.round(y)} -> ${Math.round(endX)},${Math.round(endY)}` : void 0;
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1110
|
+
const shouldPersistScreenshot = shouldPersistStudioPreviewRecorderScreenshot(actionType);
|
|
1111
|
+
const retainedScreenshot = shouldPersistScreenshot ? screenshotBefore || screenshotAfter : void 0;
|
|
1112
|
+
const deferScreenshotPersistence = 'Input' === actionType && 'typeOnly' === payload.mode;
|
|
1113
|
+
const screenshotAsset = deferScreenshotPersistence ? void 0 : this.persistStudioPreviewRecorderScreenshot(retainedScreenshot);
|
|
1114
|
+
const shouldDiscardDataUrlScreenshot = Boolean(retainedScreenshot?.startsWith('data:'));
|
|
1115
|
+
const hasRetainedScreenshot = Boolean(screenshotAsset || deferScreenshotPersistence && retainedScreenshot || !shouldDiscardDataUrlScreenshot && retainedScreenshot);
|
|
905
1116
|
const base = {
|
|
906
1117
|
source: 'studio-preview',
|
|
907
1118
|
actionType,
|
|
@@ -909,13 +1120,18 @@ class PlaygroundServer {
|
|
|
909
1120
|
pageInfo,
|
|
910
1121
|
url,
|
|
911
1122
|
title,
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1123
|
+
...screenshotAsset ? {
|
|
1124
|
+
screenshotAsset
|
|
1125
|
+
} : deferScreenshotPersistence ? retainedScreenshot ? {
|
|
1126
|
+
screenshotWithBox: retainedScreenshot
|
|
1127
|
+
} : {} : shouldDiscardDataUrlScreenshot ? {} : {
|
|
1128
|
+
screenshotBefore,
|
|
1129
|
+
screenshotAfter
|
|
1130
|
+
},
|
|
1131
|
+
semantic: hasRetainedScreenshot ? {
|
|
916
1132
|
source: 'aiDescribe',
|
|
917
1133
|
status: 'pending'
|
|
918
|
-
},
|
|
1134
|
+
} : buildFailedAiDescribeRecorderSemantic('Recorder screenshot was not retained because asset storage is unavailable or its quota was reached.'),
|
|
919
1135
|
timestamp,
|
|
920
1136
|
hashId: `studio-preview-${actionType}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
|
|
921
1137
|
};
|
|
@@ -1016,7 +1232,7 @@ class PlaygroundServer {
|
|
|
1016
1232
|
async enrichStudioPreviewRecorderEventWithAiDescribe(event, agent) {
|
|
1017
1233
|
const startedAt = new Date();
|
|
1018
1234
|
const startedAtMs = Date.now();
|
|
1019
|
-
const eventScreenshot = this.getRecorderAiDescribeScreenshot(event);
|
|
1235
|
+
const eventScreenshot = await this.getRecorderAiDescribeScreenshot(event);
|
|
1020
1236
|
const traceBase = createRecorderAiDescribeTraceBase(event, eventScreenshot);
|
|
1021
1237
|
const finishTrace = async (status, extra = {})=>{
|
|
1022
1238
|
let screenshotRef;
|
|
@@ -1081,11 +1297,11 @@ class PlaygroundServer {
|
|
|
1081
1297
|
let elementDescription;
|
|
1082
1298
|
let deepLocate;
|
|
1083
1299
|
let verifyResult;
|
|
1300
|
+
const verifyPrompt = RECORDER_AI_DESCRIBE_VERIFY_PROMPT;
|
|
1084
1301
|
try {
|
|
1085
1302
|
if ('number' != typeof x || 'number' != typeof y) throw new Error('Skipped aiDescribe because the recorder event has no stable point target.');
|
|
1086
1303
|
if (!eventScreenshot) throw new Error('Skipped aiDescribe because the recorder event has no screenshot.');
|
|
1087
1304
|
if (!event.pageInfo?.width || !event.pageInfo?.height) throw new Error('Skipped aiDescribe because the recorder event has no pageInfo for coordinate mapping.');
|
|
1088
|
-
const verifyPrompt = shouldVerifyRecorderAiDescribeEvent(event);
|
|
1089
1305
|
modelCallStartedAt = Date.now();
|
|
1090
1306
|
const elementDescriber = createElementDescriberRuntime(agent);
|
|
1091
1307
|
const describeResult = await withTimeout((0, core_namespaceObject.describeElementAtPoint)(elementDescriber, [
|
|
@@ -1112,6 +1328,7 @@ class PlaygroundServer {
|
|
|
1112
1328
|
const trace = await finishTrace('ready', {
|
|
1113
1329
|
modelCallDurationMs,
|
|
1114
1330
|
elementDescription,
|
|
1331
|
+
verifyPrompt,
|
|
1115
1332
|
verifyPassed: false,
|
|
1116
1333
|
centerDistance: verifyResult.centerDistance,
|
|
1117
1334
|
verifyResult
|
|
@@ -1142,6 +1359,7 @@ class PlaygroundServer {
|
|
|
1142
1359
|
const trace = await finishTrace('ready', {
|
|
1143
1360
|
modelCallDurationMs,
|
|
1144
1361
|
elementDescription,
|
|
1362
|
+
verifyPrompt,
|
|
1145
1363
|
verifyPassed: verifyResult?.pass,
|
|
1146
1364
|
centerDistance: verifyResult?.centerDistance,
|
|
1147
1365
|
verifyResult
|
|
@@ -1175,6 +1393,7 @@ class PlaygroundServer {
|
|
|
1175
1393
|
error: reportedError instanceof Error ? reportedError.message : String(reportedError),
|
|
1176
1394
|
modelCallDurationMs,
|
|
1177
1395
|
elementDescription,
|
|
1396
|
+
verifyPrompt,
|
|
1178
1397
|
verifyPassed: verifyResult?.pass,
|
|
1179
1398
|
centerDistance: verifyResult?.centerDistance,
|
|
1180
1399
|
verifyResult
|
|
@@ -1182,7 +1401,7 @@ class PlaygroundServer {
|
|
|
1182
1401
|
debugInteract('recorder aiDescribe trace:', trace);
|
|
1183
1402
|
const aiDescribeDetails = verifyResult || trace.annotatedScreenshotRef ? {
|
|
1184
1403
|
aiDescribe: {
|
|
1185
|
-
verifyPrompt
|
|
1404
|
+
verifyPrompt,
|
|
1186
1405
|
verifyPassed: verifyResult?.pass,
|
|
1187
1406
|
deepLocate,
|
|
1188
1407
|
centerDistance: verifyResult?.centerDistance,
|
|
@@ -1203,16 +1422,92 @@ class PlaygroundServer {
|
|
|
1203
1422
|
};
|
|
1204
1423
|
}
|
|
1205
1424
|
}
|
|
1206
|
-
getRecorderAiDescribeScreenshot(event) {
|
|
1425
|
+
async getRecorderAiDescribeScreenshot(event) {
|
|
1426
|
+
if (event.screenshotAsset) {
|
|
1427
|
+
const screenshot = readRecorderScreenshotAsset(event.screenshotAsset);
|
|
1428
|
+
if (screenshot) return screenshot;
|
|
1429
|
+
}
|
|
1207
1430
|
if ('click' === event.type || 'input' === event.type || 'keydown' === event.type || 'drag' === event.type) return event.screenshotBefore || event.screenshotAfter;
|
|
1208
|
-
return event.screenshotAfter || event.screenshotBefore;
|
|
1431
|
+
return event.screenshotAfter || event.screenshotBefore || event.screenshotWithBox;
|
|
1209
1432
|
}
|
|
1210
1433
|
queueStudioPreviewRecorderEventAppend(event, navigationEvent) {
|
|
1211
1434
|
const sessionId = this._recorderSessionId;
|
|
1212
1435
|
if (!sessionId) return;
|
|
1436
|
+
if (this.isDeferredTypeOnlyRecorderInput(event) && !navigationEvent) {
|
|
1437
|
+
const pending = this._recorderPendingTypeOnlyInput;
|
|
1438
|
+
if (pending && this.canCoalesceDeferredTypeOnlyRecorderInput(pending, event)) this._recorderPendingTypeOnlyInput = this.mergeDeferredTypeOnlyRecorderInput(pending, event);
|
|
1439
|
+
else {
|
|
1440
|
+
this.flushPendingTypeOnlyRecorderInput();
|
|
1441
|
+
this._recorderPendingTypeOnlyInput = event;
|
|
1442
|
+
}
|
|
1443
|
+
this.schedulePendingTypeOnlyRecorderInputFlush();
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
this.flushPendingTypeOnlyRecorderInput();
|
|
1213
1447
|
this._recorderEvents.push(event);
|
|
1214
1448
|
if (navigationEvent) this._recorderEvents.push(navigationEvent);
|
|
1215
1449
|
}
|
|
1450
|
+
isDeferredTypeOnlyRecorderInput(event) {
|
|
1451
|
+
return 'studio-preview' === event.source && 'input' === event.type && 'Input' === event.actionType && event.rawPayload?.mode === 'typeOnly';
|
|
1452
|
+
}
|
|
1453
|
+
canCoalesceDeferredTypeOnlyRecorderInput(current, next) {
|
|
1454
|
+
return current.url === next.url && current.title === next.title && current.rawPayload?.x === next.rawPayload?.x && current.rawPayload?.y === next.rawPayload?.y;
|
|
1455
|
+
}
|
|
1456
|
+
mergeDeferredTypeOnlyRecorderInput(current, next) {
|
|
1457
|
+
const value = `${current.value || ''}${next.value || ''}`;
|
|
1458
|
+
return {
|
|
1459
|
+
...current,
|
|
1460
|
+
value,
|
|
1461
|
+
rawPayload: {
|
|
1462
|
+
...current.rawPayload,
|
|
1463
|
+
...next.rawPayload,
|
|
1464
|
+
value
|
|
1465
|
+
},
|
|
1466
|
+
pageInfo: next.pageInfo || current.pageInfo,
|
|
1467
|
+
screenshotWithBox: next.screenshotWithBox || next.screenshotAfter || next.screenshotBefore || current.screenshotWithBox,
|
|
1468
|
+
timestamp: next.timestamp,
|
|
1469
|
+
mergedHashIds: Array.from(new Set([
|
|
1470
|
+
current.hashId,
|
|
1471
|
+
...current.mergedHashIds || [],
|
|
1472
|
+
next.hashId,
|
|
1473
|
+
...next.mergedHashIds || []
|
|
1474
|
+
]))
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
flushPendingTypeOnlyRecorderInput() {
|
|
1478
|
+
this.clearPendingTypeOnlyRecorderInputFlushTimer();
|
|
1479
|
+
const pending = this._recorderPendingTypeOnlyInput;
|
|
1480
|
+
if (!pending) return;
|
|
1481
|
+
this._recorderPendingTypeOnlyInput = null;
|
|
1482
|
+
const screenshot = pending.screenshotWithBox || pending.screenshotAfter || pending.screenshotBefore;
|
|
1483
|
+
const screenshotAsset = this.persistStudioPreviewRecorderScreenshot(screenshot);
|
|
1484
|
+
if (screenshotAsset) return void this._recorderEvents.push({
|
|
1485
|
+
...pending,
|
|
1486
|
+
screenshotAsset,
|
|
1487
|
+
screenshotBefore: void 0,
|
|
1488
|
+
screenshotAfter: void 0,
|
|
1489
|
+
screenshotWithBox: void 0
|
|
1490
|
+
});
|
|
1491
|
+
this._recorderEvents.push({
|
|
1492
|
+
...pending,
|
|
1493
|
+
screenshotBefore: void 0,
|
|
1494
|
+
screenshotAfter: void 0,
|
|
1495
|
+
screenshotWithBox: void 0,
|
|
1496
|
+
semantic: buildFailedAiDescribeRecorderSemantic('Recorder screenshot was not retained because asset storage is unavailable or its quota was reached.')
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
schedulePendingTypeOnlyRecorderInputFlush() {
|
|
1500
|
+
this.clearPendingTypeOnlyRecorderInputFlushTimer();
|
|
1501
|
+
this._recorderPendingTypeOnlyInputFlushTimer = setTimeout(()=>{
|
|
1502
|
+
this._recorderPendingTypeOnlyInputFlushTimer = void 0;
|
|
1503
|
+
this.flushPendingTypeOnlyRecorderInput();
|
|
1504
|
+
}, RECORDER_TYPE_ONLY_INPUT_SETTLE_DELAY_MS);
|
|
1505
|
+
}
|
|
1506
|
+
clearPendingTypeOnlyRecorderInputFlushTimer() {
|
|
1507
|
+
if (!this._recorderPendingTypeOnlyInputFlushTimer) return;
|
|
1508
|
+
clearTimeout(this._recorderPendingTypeOnlyInputFlushTimer);
|
|
1509
|
+
this._recorderPendingTypeOnlyInputFlushTimer = void 0;
|
|
1510
|
+
}
|
|
1216
1511
|
queueStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
|
|
1217
1512
|
const sessionId = this._recorderSessionId;
|
|
1218
1513
|
if (!sessionId) return;
|
|
@@ -1252,8 +1547,6 @@ class PlaygroundServer {
|
|
|
1252
1547
|
url: afterUrl,
|
|
1253
1548
|
title: pageStateAfter.title,
|
|
1254
1549
|
value: afterUrl,
|
|
1255
|
-
screenshotBefore: screenshotAfter,
|
|
1256
|
-
screenshotAfter,
|
|
1257
1550
|
semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, afterUrl, 'high'),
|
|
1258
1551
|
timestamp,
|
|
1259
1552
|
hashId: `studio-preview-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
|
|
@@ -1277,26 +1570,11 @@ class PlaygroundServer {
|
|
|
1277
1570
|
url: pageState.url,
|
|
1278
1571
|
title: pageState.title,
|
|
1279
1572
|
value: pageState.url,
|
|
1280
|
-
screenshotBefore: screenshot,
|
|
1281
|
-
screenshotAfter: screenshot,
|
|
1282
1573
|
semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, pageState.url, 'high'),
|
|
1283
1574
|
timestamp,
|
|
1284
1575
|
hashId: `studio-preview-initial-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
|
|
1285
1576
|
};
|
|
1286
1577
|
}
|
|
1287
|
-
async createRecorderScreenshotWithMarker(screenshot, pageInfo, point) {
|
|
1288
|
-
if (!screenshot || !pageInfo.width || !pageInfo.height) return;
|
|
1289
|
-
try {
|
|
1290
|
-
return await (0, img_namespaceObject.compositePointMarkerImg)({
|
|
1291
|
-
inputImgBase64: screenshot,
|
|
1292
|
-
size: pageInfo,
|
|
1293
|
-
point
|
|
1294
|
-
});
|
|
1295
|
-
} catch (error) {
|
|
1296
|
-
debugScreenshot('recorder screenshot marker failed:', error);
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
1578
|
async destroyCurrentAgent({ preserveActiveStream = false } = {}) {
|
|
1301
1579
|
if (!this._activeConnection.agent) return;
|
|
1302
1580
|
try {
|
|
@@ -1654,9 +1932,12 @@ class PlaygroundServer {
|
|
|
1654
1932
|
error: 'Another task is already running',
|
|
1655
1933
|
currentTaskId: this.currentTaskId
|
|
1656
1934
|
});
|
|
1935
|
+
let abortController = null;
|
|
1657
1936
|
if (requestId) {
|
|
1658
1937
|
this.currentTaskId = requestId;
|
|
1659
1938
|
this.taskExecutionDumps[requestId] = null;
|
|
1939
|
+
abortController = new AbortController();
|
|
1940
|
+
this.taskAbortControllers[requestId] = abortController;
|
|
1660
1941
|
agent.onDumpUpdate = (_dump, executionDump)=>{
|
|
1661
1942
|
if (executionDump) this.taskExecutionDumps[requestId] = executionDump;
|
|
1662
1943
|
};
|
|
@@ -1679,10 +1960,12 @@ class PlaygroundServer {
|
|
|
1679
1960
|
params
|
|
1680
1961
|
};
|
|
1681
1962
|
response.result = await (0, external_common_js_namespaceObject.executeAction)(agent, type, actionSpace, value, {
|
|
1963
|
+
requestId,
|
|
1682
1964
|
deepLocate,
|
|
1683
1965
|
deepThink,
|
|
1684
1966
|
screenshotIncluded,
|
|
1685
1967
|
domIncluded,
|
|
1968
|
+
abortSignal: abortController?.signal,
|
|
1686
1969
|
deviceOptions,
|
|
1687
1970
|
reportDisplay
|
|
1688
1971
|
});
|
|
@@ -1718,6 +2001,7 @@ class PlaygroundServer {
|
|
|
1718
2001
|
else console.log(`handle request done after ${timeCost}ms: requestId: ${requestId}`);
|
|
1719
2002
|
if (requestId) {
|
|
1720
2003
|
delete this.taskExecutionDumps[requestId];
|
|
2004
|
+
delete this.taskAbortControllers[requestId];
|
|
1721
2005
|
if (this.currentTaskId === requestId) this.currentTaskId = null;
|
|
1722
2006
|
}
|
|
1723
2007
|
});
|
|
@@ -1749,6 +2033,8 @@ class PlaygroundServer {
|
|
|
1749
2033
|
} catch (error) {
|
|
1750
2034
|
console.warn('Failed to get execution data before cancel:', error);
|
|
1751
2035
|
}
|
|
2036
|
+
const abortController = this.taskAbortControllers[requestId];
|
|
2037
|
+
abortController?.abort(new Error(`Task cancelled by user: ${requestId}`));
|
|
1752
2038
|
try {
|
|
1753
2039
|
await this.recreateAgent({
|
|
1754
2040
|
preserveActiveStream: true
|
|
@@ -1757,6 +2043,7 @@ class PlaygroundServer {
|
|
|
1757
2043
|
console.warn('Failed to recreate agent during cancel:', error);
|
|
1758
2044
|
}
|
|
1759
2045
|
delete this.taskExecutionDumps[requestId];
|
|
2046
|
+
delete this.taskAbortControllers[requestId];
|
|
1760
2047
|
this.currentTaskId = null;
|
|
1761
2048
|
res.json({
|
|
1762
2049
|
status: 'cancelled',
|
|
@@ -1813,6 +2100,7 @@ class PlaygroundServer {
|
|
|
1813
2100
|
});
|
|
1814
2101
|
this._app.post('/recorder/stop', async (_req, res)=>{
|
|
1815
2102
|
await this.waitForRecorderIdle();
|
|
2103
|
+
this.flushPendingTypeOnlyRecorderInput();
|
|
1816
2104
|
this._recorderSessionId = null;
|
|
1817
2105
|
this._studioPreviewRecorderLastScreenshot = void 0;
|
|
1818
2106
|
this._studioPreviewRecorderLastPageState = void 0;
|
|
@@ -1821,6 +2109,7 @@ class PlaygroundServer {
|
|
|
1821
2109
|
});
|
|
1822
2110
|
});
|
|
1823
2111
|
this._app.get('/recorder/events', async (req, res)=>{
|
|
2112
|
+
if ('false' !== req.query.flushPending) this.flushPendingTypeOnlyRecorderInput();
|
|
1824
2113
|
const since = 'string' == typeof req.query.since ? Number.parseInt(req.query.since, 10) : 0;
|
|
1825
2114
|
const startIndex = Number.isFinite(since) && since > 0 ? since : 0;
|
|
1826
2115
|
res.json({
|
|
@@ -1828,6 +2117,48 @@ class PlaygroundServer {
|
|
|
1828
2117
|
nextIndex: this._recorderEvents.length
|
|
1829
2118
|
});
|
|
1830
2119
|
});
|
|
2120
|
+
this._app.get('/recorder/assets/:assetId', async (req, res)=>{
|
|
2121
|
+
try {
|
|
2122
|
+
const assetId = String(req.params.assetId || '');
|
|
2123
|
+
const filePath = findRecorderScreenshotAssetPath(assetId);
|
|
2124
|
+
if (!filePath) return res.status(404).json({
|
|
2125
|
+
error: 'Recorder screenshot not found'
|
|
2126
|
+
});
|
|
2127
|
+
res.type(filePath.endsWith('.jpg') ? 'image/jpeg' : 'image/png');
|
|
2128
|
+
return res.send((0, external_node_fs_namespaceObject.readFileSync)(filePath));
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
return res.status(400).json({
|
|
2131
|
+
error: error instanceof Error ? error.message : 'Invalid recorder screenshot request'
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
this._app.delete('/recorder/assets/session/:sessionId', async (req, res)=>{
|
|
2136
|
+
try {
|
|
2137
|
+
removeRecorderScreenshotAssetsForSession(String(req.params.sessionId || ''));
|
|
2138
|
+
return res.json({
|
|
2139
|
+
ok: true
|
|
2140
|
+
});
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
return res.status(400).json({
|
|
2143
|
+
ok: false,
|
|
2144
|
+
error: error instanceof Error ? error.message : 'Invalid recorder session id'
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
this._app.post('/recorder/assets/session/:sessionId/prune', async (req, res)=>{
|
|
2149
|
+
try {
|
|
2150
|
+
const assetIds = Array.isArray(req.body?.assetIds) ? req.body.assetIds.filter((assetId)=>'string' == typeof assetId) : [];
|
|
2151
|
+
pruneRecorderScreenshotAssetsForSession(String(req.params.sessionId || ''), assetIds);
|
|
2152
|
+
return res.json({
|
|
2153
|
+
ok: true
|
|
2154
|
+
});
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
return res.status(400).json({
|
|
2157
|
+
ok: false,
|
|
2158
|
+
error: error instanceof Error ? error.message : 'Invalid recorder screenshot cleanup request'
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
1831
2162
|
this._app.post('/recorder/describe-event', async (req, res)=>{
|
|
1832
2163
|
const event = req.body?.event;
|
|
1833
2164
|
if (!event || 'object' != typeof event) return res.status(400).json({
|
|
@@ -2175,6 +2506,7 @@ class PlaygroundServer {
|
|
|
2175
2506
|
_define_property(this, "sessionSetupState", 'ready');
|
|
2176
2507
|
_define_property(this, "sessionSetupBlockingReason", void 0);
|
|
2177
2508
|
_define_property(this, "currentTaskId", null);
|
|
2509
|
+
_define_property(this, "taskAbortControllers", {});
|
|
2178
2510
|
_define_property(this, "_agentReady", true);
|
|
2179
2511
|
_define_property(this, "_configDirty", false);
|
|
2180
2512
|
_define_property(this, "_lastAiConfigSignature", null);
|
|
@@ -2184,6 +2516,8 @@ class PlaygroundServer {
|
|
|
2184
2516
|
_define_property(this, "_baseSidecars", void 0);
|
|
2185
2517
|
_define_property(this, "_recorderSessionId", null);
|
|
2186
2518
|
_define_property(this, "_recorderEvents", []);
|
|
2519
|
+
_define_property(this, "_recorderPendingTypeOnlyInput", null);
|
|
2520
|
+
_define_property(this, "_recorderPendingTypeOnlyInputFlushTimer", void 0);
|
|
2187
2521
|
_define_property(this, "_recorderEventQueue", Promise.resolve());
|
|
2188
2522
|
_define_property(this, "_recorderPendingCaptures", 0);
|
|
2189
2523
|
_define_property(this, "_studioPreviewRecorderLastTargetPoint", void 0);
|