@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/es/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve as external_node_path_resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { ReportActionDump, describeElementAtPoint, runConnectivityTest } from "@midscene/core";
|
|
@@ -9,7 +9,7 @@ import { getMidsceneRunSubDir } from "@midscene/shared/common";
|
|
|
9
9
|
import { PLAYGROUND_SERVER_PORT } from "@midscene/shared/constants";
|
|
10
10
|
import { ModelConfigManager, globalModelConfigManager, overrideAIConfig } from "@midscene/shared/env";
|
|
11
11
|
import { generateElementByPoint } from "@midscene/shared/extractor";
|
|
12
|
-
import { annotateRects,
|
|
12
|
+
import { annotateRects, imageInfoOfBase64 } from "@midscene/shared/img";
|
|
13
13
|
import { getDebug } from "@midscene/shared/logger";
|
|
14
14
|
import { buildMidsceneRecorderActionSummary, buildMidsceneRecorderReplayInstruction } from "@midscene/shared/recorder";
|
|
15
15
|
import { uuid as utils_uuid } from "@midscene/shared/utils";
|
|
@@ -31,8 +31,28 @@ function _define_property(obj, key, value) {
|
|
|
31
31
|
}
|
|
32
32
|
const defaultPort = PLAYGROUND_SERVER_PORT;
|
|
33
33
|
const RECORDER_CAPTURE_AFTER_INTERACT_DELAY_MS = 250;
|
|
34
|
+
const RECORDER_TYPE_ONLY_INPUT_SETTLE_DELAY_MS = 750;
|
|
35
|
+
const RECORDER_NAVIGATION_OBSERVATION_INTERVAL_MS = 250;
|
|
36
|
+
const RECORDER_NAVIGATION_OBSERVATION_TIMEOUT_MS = 35000;
|
|
34
37
|
const RECORDER_AI_DESCRIBE_AFTER_INTERACT_TIMEOUT_MS = 30000;
|
|
38
|
+
const RECORDER_AI_DESCRIBE_VERIFY_PROMPT = false;
|
|
35
39
|
const RECORDER_AI_DESCRIBE_SCREENSHOT_DUMP_DIR = 'recorder-ai-describe-screenshots';
|
|
40
|
+
const RECORDER_SCREENSHOT_ASSET_DIR = 'recorder-screenshots';
|
|
41
|
+
const RECORDER_SCREENSHOT_ASSET_MAX_SESSION_BYTES = 134217728;
|
|
42
|
+
const RECORDER_SCREENSHOT_ASSET_MAX_TOTAL_BYTES = 536870912;
|
|
43
|
+
const recorderScreenshotAssetQuotaExceededSessions = new Set();
|
|
44
|
+
const recorderScreenshotAssetBytesBySession = new Map();
|
|
45
|
+
let recorderScreenshotAssetUsageInitialized = false;
|
|
46
|
+
function shouldPersistStudioPreviewRecorderScreenshot(actionType) {
|
|
47
|
+
return [
|
|
48
|
+
'Tap',
|
|
49
|
+
'DoubleClick',
|
|
50
|
+
'LongPress',
|
|
51
|
+
'RightClick',
|
|
52
|
+
'DragAndDrop',
|
|
53
|
+
'Input'
|
|
54
|
+
].includes(actionType);
|
|
55
|
+
}
|
|
36
56
|
function createElementDescriberRuntime(agent) {
|
|
37
57
|
const modelConfigManager = agent.modelConfigManager;
|
|
38
58
|
const missingModelRuntime = void 0;
|
|
@@ -95,8 +115,10 @@ function buildRecorderEventSummary(event) {
|
|
|
95
115
|
pageInfo: event.pageInfo
|
|
96
116
|
};
|
|
97
117
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
118
|
+
function sanitizeRecorderPathSegment(value, fallback, maxLength = 64) {
|
|
119
|
+
const normalized = 'string' == typeof value && value.trim() ? value.trim() : fallback;
|
|
120
|
+
const sanitized = normalized.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
121
|
+
return (sanitized || fallback).slice(0, maxLength);
|
|
100
122
|
}
|
|
101
123
|
function formatRecorderAiDescribeScreenshotPathParts() {
|
|
102
124
|
const date = new Date();
|
|
@@ -148,6 +170,141 @@ function writeRecorderAiDescribeScreenshot(imageBase64, suffix) {
|
|
|
148
170
|
mimeType
|
|
149
171
|
};
|
|
150
172
|
}
|
|
173
|
+
function getRecorderScreenshotAssetRoot() {
|
|
174
|
+
const root = external_node_path_resolve(getMidsceneRunSubDir('output'), RECORDER_SCREENSHOT_ASSET_DIR);
|
|
175
|
+
mkdirSync(root, {
|
|
176
|
+
recursive: true
|
|
177
|
+
});
|
|
178
|
+
return root;
|
|
179
|
+
}
|
|
180
|
+
function assertRecorderScreenshotAssetId(assetId) {
|
|
181
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(assetId)) throw new Error('Invalid recorder screenshot asset id');
|
|
182
|
+
return assetId;
|
|
183
|
+
}
|
|
184
|
+
function recorderScreenshotAssetExtension(mimeType) {
|
|
185
|
+
return mimeType?.includes('jpeg') ? 'jpg' : 'png';
|
|
186
|
+
}
|
|
187
|
+
function recorderScreenshotAssetPath(assetId, mimeType) {
|
|
188
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
189
|
+
const safeAssetId = assertRecorderScreenshotAssetId(assetId);
|
|
190
|
+
return assertPathInsideDirectory(root, join(root, `${safeAssetId}.${recorderScreenshotAssetExtension(mimeType)}`));
|
|
191
|
+
}
|
|
192
|
+
function getRecorderScreenshotAssetUsage(sessionId) {
|
|
193
|
+
initializeRecorderScreenshotAssetUsage();
|
|
194
|
+
const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
|
|
195
|
+
const sessionBytes = recorderScreenshotAssetBytesBySession.get(safeSessionId) || 0;
|
|
196
|
+
const totalBytes = Array.from(recorderScreenshotAssetBytesBySession.values()).reduce((total, bytes)=>total + bytes, 0);
|
|
197
|
+
return {
|
|
198
|
+
totalBytes,
|
|
199
|
+
sessionBytes
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function initializeRecorderScreenshotAssetUsage() {
|
|
203
|
+
if (recorderScreenshotAssetUsageInitialized) return;
|
|
204
|
+
recorderScreenshotAssetUsageInitialized = true;
|
|
205
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
206
|
+
try {
|
|
207
|
+
for (const entry of readdirSync(root, {
|
|
208
|
+
withFileTypes: true
|
|
209
|
+
})){
|
|
210
|
+
if (!entry.isFile()) continue;
|
|
211
|
+
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);
|
|
212
|
+
if (!match?.[1]) continue;
|
|
213
|
+
const sessionId = match[1];
|
|
214
|
+
const bytes = statSync(join(root, entry.name)).size;
|
|
215
|
+
recorderScreenshotAssetBytesBySession.set(sessionId, (recorderScreenshotAssetBytesBySession.get(sessionId) || 0) + bytes);
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
debugRecorderAssets('failed to initialize recorder screenshot asset usage:', error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function persistRecorderScreenshotAsset(sessionId, imageBase64) {
|
|
222
|
+
if (!imageBase64?.startsWith('data:')) return;
|
|
223
|
+
const { base64, mimeType } = extractBase64Payload(imageBase64);
|
|
224
|
+
const bytes = Buffer.from(base64, 'base64');
|
|
225
|
+
const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
|
|
226
|
+
const usage = getRecorderScreenshotAssetUsage(sessionId);
|
|
227
|
+
if (usage.sessionBytes + bytes.byteLength > RECORDER_SCREENSHOT_ASSET_MAX_SESSION_BYTES || usage.totalBytes + bytes.byteLength > RECORDER_SCREENSHOT_ASSET_MAX_TOTAL_BYTES) {
|
|
228
|
+
if (!recorderScreenshotAssetQuotaExceededSessions.has(safeSessionId)) {
|
|
229
|
+
recorderScreenshotAssetQuotaExceededSessions.add(safeSessionId);
|
|
230
|
+
debugRecorderAssets('recorder screenshot asset quota reached %o', {
|
|
231
|
+
sessionId: safeSessionId,
|
|
232
|
+
sessionBytes: usage.sessionBytes,
|
|
233
|
+
totalBytes: usage.totalBytes
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const assetId = `${safeSessionId}-${randomUUID()}`;
|
|
239
|
+
const normalizedMimeType = mimeType || 'image/png';
|
|
240
|
+
const filePath = recorderScreenshotAssetPath(assetId, normalizedMimeType);
|
|
241
|
+
try {
|
|
242
|
+
writeFileSync(filePath, bytes);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
debugRecorderAssets('failed to persist recorder screenshot asset:', error);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
recorderScreenshotAssetBytesBySession.set(safeSessionId, usage.sessionBytes + bytes.byteLength);
|
|
248
|
+
return {
|
|
249
|
+
id: assetId,
|
|
250
|
+
mimeType: normalizedMimeType,
|
|
251
|
+
bytes: bytes.byteLength
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function readRecorderScreenshotAsset(asset) {
|
|
255
|
+
const filePath = recorderScreenshotAssetPath(asset.id, asset.mimeType);
|
|
256
|
+
if (!existsSync(filePath)) return;
|
|
257
|
+
return `data:${asset.mimeType};base64,${readFileSync(filePath).toString('base64')}`;
|
|
258
|
+
}
|
|
259
|
+
function findRecorderScreenshotAssetPath(assetId) {
|
|
260
|
+
const safeAssetId = assertRecorderScreenshotAssetId(assetId);
|
|
261
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
262
|
+
for (const extension of [
|
|
263
|
+
'png',
|
|
264
|
+
'jpg'
|
|
265
|
+
]){
|
|
266
|
+
const filePath = assertPathInsideDirectory(root, join(root, `${safeAssetId}.${extension}`));
|
|
267
|
+
if (existsSync(filePath)) return filePath;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function removeRecorderScreenshotAssetsForSession(sessionId) {
|
|
271
|
+
const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
|
|
272
|
+
const prefix = `${safeSessionId}-`;
|
|
273
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
274
|
+
for (const entry of readdirSync(root, {
|
|
275
|
+
withFileTypes: true
|
|
276
|
+
}))if (entry.isFile() && entry.name.startsWith(prefix)) rmSync(join(root, entry.name), {
|
|
277
|
+
force: true
|
|
278
|
+
});
|
|
279
|
+
recorderScreenshotAssetQuotaExceededSessions.delete(safeSessionId);
|
|
280
|
+
recorderScreenshotAssetBytesBySession.delete(safeSessionId);
|
|
281
|
+
}
|
|
282
|
+
function pruneRecorderScreenshotAssetsForSession(sessionId, retainedAssetIds) {
|
|
283
|
+
const safeSessionId = sanitizeRecorderPathSegment(sessionId, 'session');
|
|
284
|
+
const prefix = `${safeSessionId}-`;
|
|
285
|
+
const retained = new Set(retainedAssetIds.map((assetId)=>{
|
|
286
|
+
const safeAssetId = assertRecorderScreenshotAssetId(assetId);
|
|
287
|
+
if (!safeAssetId.startsWith(prefix)) throw new Error('Recorder screenshot asset does not belong to session');
|
|
288
|
+
return safeAssetId;
|
|
289
|
+
}));
|
|
290
|
+
const root = getRecorderScreenshotAssetRoot();
|
|
291
|
+
let sessionBytes = 0;
|
|
292
|
+
for (const entry of readdirSync(root, {
|
|
293
|
+
withFileTypes: true
|
|
294
|
+
})){
|
|
295
|
+
if (!entry.isFile() || !entry.name.startsWith(prefix)) continue;
|
|
296
|
+
const assetId = entry.name.replace(/\.(?:png|jpg)$/i, '');
|
|
297
|
+
const filePath = join(root, entry.name);
|
|
298
|
+
if (!retained.has(assetId)) {
|
|
299
|
+
rmSync(filePath, {
|
|
300
|
+
force: true
|
|
301
|
+
});
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
sessionBytes += statSync(filePath).size;
|
|
305
|
+
}
|
|
306
|
+
recorderScreenshotAssetBytesBySession.set(safeSessionId, sessionBytes);
|
|
307
|
+
}
|
|
151
308
|
function calculateRecorderScreenshotAnnotation(event, imageSize, verifyResult) {
|
|
152
309
|
const pageWidth = event.pageInfo?.width;
|
|
153
310
|
const pageHeight = event.pageInfo?.height;
|
|
@@ -304,6 +461,9 @@ const debugMjpeg = getDebug('playground:mjpeg', {
|
|
|
304
461
|
const debugInteract = getDebug('playground:interact', {
|
|
305
462
|
console: true
|
|
306
463
|
});
|
|
464
|
+
const debugRecorderAssets = getDebug('playground:recorder-assets', {
|
|
465
|
+
console: true
|
|
466
|
+
});
|
|
307
467
|
class InteractParamsValidationError extends Error {
|
|
308
468
|
constructor(message){
|
|
309
469
|
super(message);
|
|
@@ -693,8 +853,10 @@ class PlaygroundServer {
|
|
|
693
853
|
};
|
|
694
854
|
}
|
|
695
855
|
resetRecorderState() {
|
|
856
|
+
this.clearPendingTypeOnlyRecorderInputFlushTimer();
|
|
696
857
|
this._recorderSessionId = null;
|
|
697
858
|
this._recorderEvents = [];
|
|
859
|
+
this._recorderPendingTypeOnlyInput = null;
|
|
698
860
|
this._recorderEventQueue = Promise.resolve();
|
|
699
861
|
this._studioPreviewRecorderLastTargetPoint = void 0;
|
|
700
862
|
this._studioPreviewRecorderLastScreenshot = void 0;
|
|
@@ -801,8 +963,13 @@ class PlaygroundServer {
|
|
|
801
963
|
const initialNavigationEvent = this.buildStudioPreviewInitialNavigationEvent(initialPageState, this._studioPreviewRecorderLastScreenshot);
|
|
802
964
|
if (initialNavigationEvent) this._recorderEvents.push(initialNavigationEvent);
|
|
803
965
|
}
|
|
966
|
+
persistStudioPreviewRecorderScreenshot(screenshot) {
|
|
967
|
+
if (!this._recorderSessionId) return;
|
|
968
|
+
return persistRecorderScreenshotAsset(this._recorderSessionId, screenshot);
|
|
969
|
+
}
|
|
804
970
|
async storeStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
|
|
805
971
|
if (!this._recorderSessionId) return;
|
|
972
|
+
const sessionId = this._recorderSessionId;
|
|
806
973
|
const before = snapshotBefore || await this.captureRecorderSnapshotBeforeInteract();
|
|
807
974
|
const screenshotBefore = before?.screenshot;
|
|
808
975
|
debugInteract('recorder capture scheduled after action %o', {
|
|
@@ -830,6 +997,48 @@ class PlaygroundServer {
|
|
|
830
997
|
this._studioPreviewRecorderLastScreenshot = screenshotAfter;
|
|
831
998
|
this._studioPreviewRecorderLastPageState = pageStateAfter;
|
|
832
999
|
this.queueStudioPreviewRecorderEventAppend(event, navigationEvent);
|
|
1000
|
+
if (!navigationEvent) this.observeStudioPreviewNavigationChange(payload, before?.pageState, event.hashId, sessionId);
|
|
1001
|
+
}
|
|
1002
|
+
observeStudioPreviewNavigationChange(payload, pageStateBefore, triggerEventHashId, sessionId) {
|
|
1003
|
+
const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
|
|
1004
|
+
const beforeUrl = pageStateBefore?.url;
|
|
1005
|
+
if (!actionType || !beforeUrl || BROWSER_CHROME_NAVIGATION_ACTIONS.has(actionType)) return;
|
|
1006
|
+
(async ()=>{
|
|
1007
|
+
const deadline = Date.now() + RECORDER_NAVIGATION_OBSERVATION_TIMEOUT_MS;
|
|
1008
|
+
while(this._recorderSessionId === sessionId && Date.now() < deadline){
|
|
1009
|
+
await sleep(RECORDER_NAVIGATION_OBSERVATION_INTERVAL_MS);
|
|
1010
|
+
if (this._recorderSessionId !== sessionId) return;
|
|
1011
|
+
const currentUrl = await this.getActivePageUrl();
|
|
1012
|
+
if (!currentUrl || currentUrl === beforeUrl) continue;
|
|
1013
|
+
const screenshot = await this.takeRecorderScreenshot();
|
|
1014
|
+
const pageStateAfter = await this.getActiveRecorderPageState();
|
|
1015
|
+
const navigationEvent = this.buildStudioPreviewNavigationChangeEvent(payload, pageStateBefore, pageStateAfter, screenshot);
|
|
1016
|
+
if (!navigationEvent) return;
|
|
1017
|
+
navigationEvent.rawPayload = {
|
|
1018
|
+
...navigationEvent.rawPayload,
|
|
1019
|
+
triggerEventHashId
|
|
1020
|
+
};
|
|
1021
|
+
if (this.appendStudioPreviewNavigationEventAfterTrigger(triggerEventHashId, navigationEvent)) {
|
|
1022
|
+
this._studioPreviewRecorderLastScreenshot = screenshot;
|
|
1023
|
+
this._studioPreviewRecorderLastPageState = pageStateAfter;
|
|
1024
|
+
}
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
})().catch((error)=>{
|
|
1028
|
+
debugInteract('delayed recorder navigation observation failed:', error);
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
appendStudioPreviewNavigationEventAfterTrigger(triggerEventHashId, navigationEvent) {
|
|
1032
|
+
const triggerIndex = this._recorderEvents.findIndex((event)=>event.hashId === triggerEventHashId);
|
|
1033
|
+
if (triggerIndex < 0) return false;
|
|
1034
|
+
const alreadyRecorded = this._recorderEvents.some((event)=>{
|
|
1035
|
+
if ('NavigationChanged' !== event.actionType) return false;
|
|
1036
|
+
const rawPayload = event.rawPayload;
|
|
1037
|
+
return rawPayload?.triggerEventHashId === triggerEventHashId;
|
|
1038
|
+
});
|
|
1039
|
+
if (alreadyRecorded) return false;
|
|
1040
|
+
this._recorderEvents.splice(triggerIndex + 1, 0, navigationEvent);
|
|
1041
|
+
return true;
|
|
833
1042
|
}
|
|
834
1043
|
async buildStudioPreviewRecorderEvent(payload, pageState, screenshotBefore, screenshotAfter) {
|
|
835
1044
|
const actionType = 'string' == typeof payload.actionType ? payload.actionType : void 0;
|
|
@@ -849,10 +1058,12 @@ class PlaygroundServer {
|
|
|
849
1058
|
const endX = 'number' == typeof payload.endX ? payload.endX : void 0;
|
|
850
1059
|
const endY = 'number' == typeof payload.endY ? payload.endY : void 0;
|
|
851
1060
|
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;
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1061
|
+
const shouldPersistScreenshot = shouldPersistStudioPreviewRecorderScreenshot(actionType);
|
|
1062
|
+
const retainedScreenshot = shouldPersistScreenshot ? screenshotBefore || screenshotAfter : void 0;
|
|
1063
|
+
const deferScreenshotPersistence = 'Input' === actionType && 'typeOnly' === payload.mode;
|
|
1064
|
+
const screenshotAsset = deferScreenshotPersistence ? void 0 : this.persistStudioPreviewRecorderScreenshot(retainedScreenshot);
|
|
1065
|
+
const shouldDiscardDataUrlScreenshot = Boolean(retainedScreenshot?.startsWith('data:'));
|
|
1066
|
+
const hasRetainedScreenshot = Boolean(screenshotAsset || deferScreenshotPersistence && retainedScreenshot || !shouldDiscardDataUrlScreenshot && retainedScreenshot);
|
|
856
1067
|
const base = {
|
|
857
1068
|
source: 'studio-preview',
|
|
858
1069
|
actionType,
|
|
@@ -860,13 +1071,18 @@ class PlaygroundServer {
|
|
|
860
1071
|
pageInfo,
|
|
861
1072
|
url,
|
|
862
1073
|
title,
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1074
|
+
...screenshotAsset ? {
|
|
1075
|
+
screenshotAsset
|
|
1076
|
+
} : deferScreenshotPersistence ? retainedScreenshot ? {
|
|
1077
|
+
screenshotWithBox: retainedScreenshot
|
|
1078
|
+
} : {} : shouldDiscardDataUrlScreenshot ? {} : {
|
|
1079
|
+
screenshotBefore,
|
|
1080
|
+
screenshotAfter
|
|
1081
|
+
},
|
|
1082
|
+
semantic: hasRetainedScreenshot ? {
|
|
867
1083
|
source: 'aiDescribe',
|
|
868
1084
|
status: 'pending'
|
|
869
|
-
},
|
|
1085
|
+
} : buildFailedAiDescribeRecorderSemantic('Recorder screenshot was not retained because asset storage is unavailable or its quota was reached.'),
|
|
870
1086
|
timestamp,
|
|
871
1087
|
hashId: `studio-preview-${actionType}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
|
|
872
1088
|
};
|
|
@@ -967,7 +1183,7 @@ class PlaygroundServer {
|
|
|
967
1183
|
async enrichStudioPreviewRecorderEventWithAiDescribe(event, agent) {
|
|
968
1184
|
const startedAt = new Date();
|
|
969
1185
|
const startedAtMs = Date.now();
|
|
970
|
-
const eventScreenshot = this.getRecorderAiDescribeScreenshot(event);
|
|
1186
|
+
const eventScreenshot = await this.getRecorderAiDescribeScreenshot(event);
|
|
971
1187
|
const traceBase = createRecorderAiDescribeTraceBase(event, eventScreenshot);
|
|
972
1188
|
const finishTrace = async (status, extra = {})=>{
|
|
973
1189
|
let screenshotRef;
|
|
@@ -1032,11 +1248,11 @@ class PlaygroundServer {
|
|
|
1032
1248
|
let elementDescription;
|
|
1033
1249
|
let deepLocate;
|
|
1034
1250
|
let verifyResult;
|
|
1251
|
+
const verifyPrompt = RECORDER_AI_DESCRIBE_VERIFY_PROMPT;
|
|
1035
1252
|
try {
|
|
1036
1253
|
if ('number' != typeof x || 'number' != typeof y) throw new Error('Skipped aiDescribe because the recorder event has no stable point target.');
|
|
1037
1254
|
if (!eventScreenshot) throw new Error('Skipped aiDescribe because the recorder event has no screenshot.');
|
|
1038
1255
|
if (!event.pageInfo?.width || !event.pageInfo?.height) throw new Error('Skipped aiDescribe because the recorder event has no pageInfo for coordinate mapping.');
|
|
1039
|
-
const verifyPrompt = shouldVerifyRecorderAiDescribeEvent(event);
|
|
1040
1256
|
modelCallStartedAt = Date.now();
|
|
1041
1257
|
const elementDescriber = createElementDescriberRuntime(agent);
|
|
1042
1258
|
const describeResult = await withTimeout(describeElementAtPoint(elementDescriber, [
|
|
@@ -1063,6 +1279,7 @@ class PlaygroundServer {
|
|
|
1063
1279
|
const trace = await finishTrace('ready', {
|
|
1064
1280
|
modelCallDurationMs,
|
|
1065
1281
|
elementDescription,
|
|
1282
|
+
verifyPrompt,
|
|
1066
1283
|
verifyPassed: false,
|
|
1067
1284
|
centerDistance: verifyResult.centerDistance,
|
|
1068
1285
|
verifyResult
|
|
@@ -1093,6 +1310,7 @@ class PlaygroundServer {
|
|
|
1093
1310
|
const trace = await finishTrace('ready', {
|
|
1094
1311
|
modelCallDurationMs,
|
|
1095
1312
|
elementDescription,
|
|
1313
|
+
verifyPrompt,
|
|
1096
1314
|
verifyPassed: verifyResult?.pass,
|
|
1097
1315
|
centerDistance: verifyResult?.centerDistance,
|
|
1098
1316
|
verifyResult
|
|
@@ -1126,6 +1344,7 @@ class PlaygroundServer {
|
|
|
1126
1344
|
error: reportedError instanceof Error ? reportedError.message : String(reportedError),
|
|
1127
1345
|
modelCallDurationMs,
|
|
1128
1346
|
elementDescription,
|
|
1347
|
+
verifyPrompt,
|
|
1129
1348
|
verifyPassed: verifyResult?.pass,
|
|
1130
1349
|
centerDistance: verifyResult?.centerDistance,
|
|
1131
1350
|
verifyResult
|
|
@@ -1133,7 +1352,7 @@ class PlaygroundServer {
|
|
|
1133
1352
|
debugInteract('recorder aiDescribe trace:', trace);
|
|
1134
1353
|
const aiDescribeDetails = verifyResult || trace.annotatedScreenshotRef ? {
|
|
1135
1354
|
aiDescribe: {
|
|
1136
|
-
verifyPrompt
|
|
1355
|
+
verifyPrompt,
|
|
1137
1356
|
verifyPassed: verifyResult?.pass,
|
|
1138
1357
|
deepLocate,
|
|
1139
1358
|
centerDistance: verifyResult?.centerDistance,
|
|
@@ -1154,16 +1373,92 @@ class PlaygroundServer {
|
|
|
1154
1373
|
};
|
|
1155
1374
|
}
|
|
1156
1375
|
}
|
|
1157
|
-
getRecorderAiDescribeScreenshot(event) {
|
|
1376
|
+
async getRecorderAiDescribeScreenshot(event) {
|
|
1377
|
+
if (event.screenshotAsset) {
|
|
1378
|
+
const screenshot = readRecorderScreenshotAsset(event.screenshotAsset);
|
|
1379
|
+
if (screenshot) return screenshot;
|
|
1380
|
+
}
|
|
1158
1381
|
if ('click' === event.type || 'input' === event.type || 'keydown' === event.type || 'drag' === event.type) return event.screenshotBefore || event.screenshotAfter;
|
|
1159
|
-
return event.screenshotAfter || event.screenshotBefore;
|
|
1382
|
+
return event.screenshotAfter || event.screenshotBefore || event.screenshotWithBox;
|
|
1160
1383
|
}
|
|
1161
1384
|
queueStudioPreviewRecorderEventAppend(event, navigationEvent) {
|
|
1162
1385
|
const sessionId = this._recorderSessionId;
|
|
1163
1386
|
if (!sessionId) return;
|
|
1387
|
+
if (this.isDeferredTypeOnlyRecorderInput(event) && !navigationEvent) {
|
|
1388
|
+
const pending = this._recorderPendingTypeOnlyInput;
|
|
1389
|
+
if (pending && this.canCoalesceDeferredTypeOnlyRecorderInput(pending, event)) this._recorderPendingTypeOnlyInput = this.mergeDeferredTypeOnlyRecorderInput(pending, event);
|
|
1390
|
+
else {
|
|
1391
|
+
this.flushPendingTypeOnlyRecorderInput();
|
|
1392
|
+
this._recorderPendingTypeOnlyInput = event;
|
|
1393
|
+
}
|
|
1394
|
+
this.schedulePendingTypeOnlyRecorderInputFlush();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
this.flushPendingTypeOnlyRecorderInput();
|
|
1164
1398
|
this._recorderEvents.push(event);
|
|
1165
1399
|
if (navigationEvent) this._recorderEvents.push(navigationEvent);
|
|
1166
1400
|
}
|
|
1401
|
+
isDeferredTypeOnlyRecorderInput(event) {
|
|
1402
|
+
return 'studio-preview' === event.source && 'input' === event.type && 'Input' === event.actionType && event.rawPayload?.mode === 'typeOnly';
|
|
1403
|
+
}
|
|
1404
|
+
canCoalesceDeferredTypeOnlyRecorderInput(current, next) {
|
|
1405
|
+
return current.url === next.url && current.title === next.title && current.rawPayload?.x === next.rawPayload?.x && current.rawPayload?.y === next.rawPayload?.y;
|
|
1406
|
+
}
|
|
1407
|
+
mergeDeferredTypeOnlyRecorderInput(current, next) {
|
|
1408
|
+
const value = `${current.value || ''}${next.value || ''}`;
|
|
1409
|
+
return {
|
|
1410
|
+
...current,
|
|
1411
|
+
value,
|
|
1412
|
+
rawPayload: {
|
|
1413
|
+
...current.rawPayload,
|
|
1414
|
+
...next.rawPayload,
|
|
1415
|
+
value
|
|
1416
|
+
},
|
|
1417
|
+
pageInfo: next.pageInfo || current.pageInfo,
|
|
1418
|
+
screenshotWithBox: next.screenshotWithBox || next.screenshotAfter || next.screenshotBefore || current.screenshotWithBox,
|
|
1419
|
+
timestamp: next.timestamp,
|
|
1420
|
+
mergedHashIds: Array.from(new Set([
|
|
1421
|
+
current.hashId,
|
|
1422
|
+
...current.mergedHashIds || [],
|
|
1423
|
+
next.hashId,
|
|
1424
|
+
...next.mergedHashIds || []
|
|
1425
|
+
]))
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
flushPendingTypeOnlyRecorderInput() {
|
|
1429
|
+
this.clearPendingTypeOnlyRecorderInputFlushTimer();
|
|
1430
|
+
const pending = this._recorderPendingTypeOnlyInput;
|
|
1431
|
+
if (!pending) return;
|
|
1432
|
+
this._recorderPendingTypeOnlyInput = null;
|
|
1433
|
+
const screenshot = pending.screenshotWithBox || pending.screenshotAfter || pending.screenshotBefore;
|
|
1434
|
+
const screenshotAsset = this.persistStudioPreviewRecorderScreenshot(screenshot);
|
|
1435
|
+
if (screenshotAsset) return void this._recorderEvents.push({
|
|
1436
|
+
...pending,
|
|
1437
|
+
screenshotAsset,
|
|
1438
|
+
screenshotBefore: void 0,
|
|
1439
|
+
screenshotAfter: void 0,
|
|
1440
|
+
screenshotWithBox: void 0
|
|
1441
|
+
});
|
|
1442
|
+
this._recorderEvents.push({
|
|
1443
|
+
...pending,
|
|
1444
|
+
screenshotBefore: void 0,
|
|
1445
|
+
screenshotAfter: void 0,
|
|
1446
|
+
screenshotWithBox: void 0,
|
|
1447
|
+
semantic: buildFailedAiDescribeRecorderSemantic('Recorder screenshot was not retained because asset storage is unavailable or its quota was reached.')
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
schedulePendingTypeOnlyRecorderInputFlush() {
|
|
1451
|
+
this.clearPendingTypeOnlyRecorderInputFlushTimer();
|
|
1452
|
+
this._recorderPendingTypeOnlyInputFlushTimer = setTimeout(()=>{
|
|
1453
|
+
this._recorderPendingTypeOnlyInputFlushTimer = void 0;
|
|
1454
|
+
this.flushPendingTypeOnlyRecorderInput();
|
|
1455
|
+
}, RECORDER_TYPE_ONLY_INPUT_SETTLE_DELAY_MS);
|
|
1456
|
+
}
|
|
1457
|
+
clearPendingTypeOnlyRecorderInputFlushTimer() {
|
|
1458
|
+
if (!this._recorderPendingTypeOnlyInputFlushTimer) return;
|
|
1459
|
+
clearTimeout(this._recorderPendingTypeOnlyInputFlushTimer);
|
|
1460
|
+
this._recorderPendingTypeOnlyInputFlushTimer = void 0;
|
|
1461
|
+
}
|
|
1167
1462
|
queueStudioPreviewRecorderEvent(payload, snapshotBefore, agent) {
|
|
1168
1463
|
const sessionId = this._recorderSessionId;
|
|
1169
1464
|
if (!sessionId) return;
|
|
@@ -1203,8 +1498,6 @@ class PlaygroundServer {
|
|
|
1203
1498
|
url: afterUrl,
|
|
1204
1499
|
title: pageStateAfter.title,
|
|
1205
1500
|
value: afterUrl,
|
|
1206
|
-
screenshotBefore: screenshotAfter,
|
|
1207
|
-
screenshotAfter,
|
|
1208
1501
|
semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, afterUrl, 'high'),
|
|
1209
1502
|
timestamp,
|
|
1210
1503
|
hashId: `studio-preview-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
|
|
@@ -1228,26 +1521,11 @@ class PlaygroundServer {
|
|
|
1228
1521
|
url: pageState.url,
|
|
1229
1522
|
title: pageState.title,
|
|
1230
1523
|
value: pageState.url,
|
|
1231
|
-
screenshotBefore: screenshot,
|
|
1232
|
-
screenshotAfter: screenshot,
|
|
1233
1524
|
semantic: buildReadyRecorderSemantic('heuristic', semanticEvent, pageState.url, 'high'),
|
|
1234
1525
|
timestamp,
|
|
1235
1526
|
hashId: `studio-preview-initial-navigation-${timestamp}-${Math.random().toString(36).slice(2, 8)}`
|
|
1236
1527
|
};
|
|
1237
1528
|
}
|
|
1238
|
-
async createRecorderScreenshotWithMarker(screenshot, pageInfo, point) {
|
|
1239
|
-
if (!screenshot || !pageInfo.width || !pageInfo.height) return;
|
|
1240
|
-
try {
|
|
1241
|
-
return await compositePointMarkerImg({
|
|
1242
|
-
inputImgBase64: screenshot,
|
|
1243
|
-
size: pageInfo,
|
|
1244
|
-
point
|
|
1245
|
-
});
|
|
1246
|
-
} catch (error) {
|
|
1247
|
-
debugScreenshot('recorder screenshot marker failed:', error);
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
1529
|
async destroyCurrentAgent({ preserveActiveStream = false } = {}) {
|
|
1252
1530
|
if (!this._activeConnection.agent) return;
|
|
1253
1531
|
try {
|
|
@@ -1605,9 +1883,12 @@ class PlaygroundServer {
|
|
|
1605
1883
|
error: 'Another task is already running',
|
|
1606
1884
|
currentTaskId: this.currentTaskId
|
|
1607
1885
|
});
|
|
1886
|
+
let abortController = null;
|
|
1608
1887
|
if (requestId) {
|
|
1609
1888
|
this.currentTaskId = requestId;
|
|
1610
1889
|
this.taskExecutionDumps[requestId] = null;
|
|
1890
|
+
abortController = new AbortController();
|
|
1891
|
+
this.taskAbortControllers[requestId] = abortController;
|
|
1611
1892
|
agent.onDumpUpdate = (_dump, executionDump)=>{
|
|
1612
1893
|
if (executionDump) this.taskExecutionDumps[requestId] = executionDump;
|
|
1613
1894
|
};
|
|
@@ -1630,10 +1911,12 @@ class PlaygroundServer {
|
|
|
1630
1911
|
params
|
|
1631
1912
|
};
|
|
1632
1913
|
response.result = await executeAction(agent, type, actionSpace, value, {
|
|
1914
|
+
requestId,
|
|
1633
1915
|
deepLocate,
|
|
1634
1916
|
deepThink,
|
|
1635
1917
|
screenshotIncluded,
|
|
1636
1918
|
domIncluded,
|
|
1919
|
+
abortSignal: abortController?.signal,
|
|
1637
1920
|
deviceOptions,
|
|
1638
1921
|
reportDisplay
|
|
1639
1922
|
});
|
|
@@ -1669,6 +1952,7 @@ class PlaygroundServer {
|
|
|
1669
1952
|
else console.log(`handle request done after ${timeCost}ms: requestId: ${requestId}`);
|
|
1670
1953
|
if (requestId) {
|
|
1671
1954
|
delete this.taskExecutionDumps[requestId];
|
|
1955
|
+
delete this.taskAbortControllers[requestId];
|
|
1672
1956
|
if (this.currentTaskId === requestId) this.currentTaskId = null;
|
|
1673
1957
|
}
|
|
1674
1958
|
});
|
|
@@ -1700,6 +1984,8 @@ class PlaygroundServer {
|
|
|
1700
1984
|
} catch (error) {
|
|
1701
1985
|
console.warn('Failed to get execution data before cancel:', error);
|
|
1702
1986
|
}
|
|
1987
|
+
const abortController = this.taskAbortControllers[requestId];
|
|
1988
|
+
abortController?.abort(new Error(`Task cancelled by user: ${requestId}`));
|
|
1703
1989
|
try {
|
|
1704
1990
|
await this.recreateAgent({
|
|
1705
1991
|
preserveActiveStream: true
|
|
@@ -1708,6 +1994,7 @@ class PlaygroundServer {
|
|
|
1708
1994
|
console.warn('Failed to recreate agent during cancel:', error);
|
|
1709
1995
|
}
|
|
1710
1996
|
delete this.taskExecutionDumps[requestId];
|
|
1997
|
+
delete this.taskAbortControllers[requestId];
|
|
1711
1998
|
this.currentTaskId = null;
|
|
1712
1999
|
res.json({
|
|
1713
2000
|
status: 'cancelled',
|
|
@@ -1764,6 +2051,7 @@ class PlaygroundServer {
|
|
|
1764
2051
|
});
|
|
1765
2052
|
this._app.post('/recorder/stop', async (_req, res)=>{
|
|
1766
2053
|
await this.waitForRecorderIdle();
|
|
2054
|
+
this.flushPendingTypeOnlyRecorderInput();
|
|
1767
2055
|
this._recorderSessionId = null;
|
|
1768
2056
|
this._studioPreviewRecorderLastScreenshot = void 0;
|
|
1769
2057
|
this._studioPreviewRecorderLastPageState = void 0;
|
|
@@ -1772,6 +2060,7 @@ class PlaygroundServer {
|
|
|
1772
2060
|
});
|
|
1773
2061
|
});
|
|
1774
2062
|
this._app.get('/recorder/events', async (req, res)=>{
|
|
2063
|
+
if ('false' !== req.query.flushPending) this.flushPendingTypeOnlyRecorderInput();
|
|
1775
2064
|
const since = 'string' == typeof req.query.since ? Number.parseInt(req.query.since, 10) : 0;
|
|
1776
2065
|
const startIndex = Number.isFinite(since) && since > 0 ? since : 0;
|
|
1777
2066
|
res.json({
|
|
@@ -1779,6 +2068,48 @@ class PlaygroundServer {
|
|
|
1779
2068
|
nextIndex: this._recorderEvents.length
|
|
1780
2069
|
});
|
|
1781
2070
|
});
|
|
2071
|
+
this._app.get('/recorder/assets/:assetId', async (req, res)=>{
|
|
2072
|
+
try {
|
|
2073
|
+
const assetId = String(req.params.assetId || '');
|
|
2074
|
+
const filePath = findRecorderScreenshotAssetPath(assetId);
|
|
2075
|
+
if (!filePath) return res.status(404).json({
|
|
2076
|
+
error: 'Recorder screenshot not found'
|
|
2077
|
+
});
|
|
2078
|
+
res.type(filePath.endsWith('.jpg') ? 'image/jpeg' : 'image/png');
|
|
2079
|
+
return res.send(readFileSync(filePath));
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
return res.status(400).json({
|
|
2082
|
+
error: error instanceof Error ? error.message : 'Invalid recorder screenshot request'
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
this._app.delete('/recorder/assets/session/:sessionId', async (req, res)=>{
|
|
2087
|
+
try {
|
|
2088
|
+
removeRecorderScreenshotAssetsForSession(String(req.params.sessionId || ''));
|
|
2089
|
+
return res.json({
|
|
2090
|
+
ok: true
|
|
2091
|
+
});
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
return res.status(400).json({
|
|
2094
|
+
ok: false,
|
|
2095
|
+
error: error instanceof Error ? error.message : 'Invalid recorder session id'
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
this._app.post('/recorder/assets/session/:sessionId/prune', async (req, res)=>{
|
|
2100
|
+
try {
|
|
2101
|
+
const assetIds = Array.isArray(req.body?.assetIds) ? req.body.assetIds.filter((assetId)=>'string' == typeof assetId) : [];
|
|
2102
|
+
pruneRecorderScreenshotAssetsForSession(String(req.params.sessionId || ''), assetIds);
|
|
2103
|
+
return res.json({
|
|
2104
|
+
ok: true
|
|
2105
|
+
});
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
return res.status(400).json({
|
|
2108
|
+
ok: false,
|
|
2109
|
+
error: error instanceof Error ? error.message : 'Invalid recorder screenshot cleanup request'
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
1782
2113
|
this._app.post('/recorder/describe-event', async (req, res)=>{
|
|
1783
2114
|
const event = req.body?.event;
|
|
1784
2115
|
if (!event || 'object' != typeof event) return res.status(400).json({
|
|
@@ -2126,6 +2457,7 @@ class PlaygroundServer {
|
|
|
2126
2457
|
_define_property(this, "sessionSetupState", 'ready');
|
|
2127
2458
|
_define_property(this, "sessionSetupBlockingReason", void 0);
|
|
2128
2459
|
_define_property(this, "currentTaskId", null);
|
|
2460
|
+
_define_property(this, "taskAbortControllers", {});
|
|
2129
2461
|
_define_property(this, "_agentReady", true);
|
|
2130
2462
|
_define_property(this, "_configDirty", false);
|
|
2131
2463
|
_define_property(this, "_lastAiConfigSignature", null);
|
|
@@ -2135,6 +2467,8 @@ class PlaygroundServer {
|
|
|
2135
2467
|
_define_property(this, "_baseSidecars", void 0);
|
|
2136
2468
|
_define_property(this, "_recorderSessionId", null);
|
|
2137
2469
|
_define_property(this, "_recorderEvents", []);
|
|
2470
|
+
_define_property(this, "_recorderPendingTypeOnlyInput", null);
|
|
2471
|
+
_define_property(this, "_recorderPendingTypeOnlyInputFlushTimer", void 0);
|
|
2138
2472
|
_define_property(this, "_recorderEventQueue", Promise.resolve());
|
|
2139
2473
|
_define_property(this, "_recorderPendingCaptures", 0);
|
|
2140
2474
|
_define_property(this, "_studioPreviewRecorderLastTargetPoint", void 0);
|