@web-auto/webauto 0.1.4 → 0.1.6
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/apps/desktop-console/default-settings.json +2 -2
- package/apps/desktop-console/dist/main/index.mjs +915 -85
- package/apps/desktop-console/dist/main/preload.mjs +7 -0
- package/apps/desktop-console/dist/renderer/index.html +622 -50
- package/apps/desktop-console/dist/renderer/index.js +2415 -470
- package/apps/desktop-console/dist/renderer/run.mts +6 -5
- package/apps/desktop-console/entry/ui-cli.mjs +672 -0
- package/apps/desktop-console/entry/ui-console.mjs +416 -29
- package/apps/webauto/entry/account.mjs +89 -53
- package/apps/webauto/entry/browser-status.mjs +7 -10
- package/apps/webauto/entry/lib/account-detect.mjs +254 -28
- package/apps/webauto/entry/lib/account-store.mjs +219 -30
- package/apps/webauto/entry/lib/bus-publish.mjs +63 -0
- package/apps/webauto/entry/lib/camo-cli.mjs +93 -0
- package/apps/webauto/entry/lib/profilepool.mjs +14 -5
- package/apps/webauto/entry/lib/quota-status.mjs +23 -0
- package/apps/webauto/entry/lib/schedule-store.mjs +1068 -0
- package/apps/webauto/entry/profilepool.mjs +106 -17
- package/apps/webauto/entry/schedule.mjs +612 -0
- package/apps/webauto/entry/weibo-unified.mjs +134 -0
- package/apps/webauto/entry/xhs-install.mjs +236 -29
- package/apps/webauto/entry/xhs-status.mjs +5 -2
- package/apps/webauto/entry/xhs-unified.mjs +631 -98
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/comment_item/container.json +40 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_expand_button/container.json +38 -0
- package/apps/webauto/resources/container-library/weibo/weibo_detail_page/reply_list/container.json +37 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/container.json +8 -3
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/login_anchor/container.json +30 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_bar/container.json +47 -0
- package/apps/webauto/resources/container-library/weibo/weibo_search_page/search_button/container.json +39 -0
- package/bin/camoufox-cli.mjs +61 -0
- package/bin/webauto.mjs +301 -54
- package/dist/modules/camo-backend/src/index.js +49 -1
- package/dist/modules/camo-backend/src/internal/BrowserSession.js +572 -3
- package/dist/modules/camo-backend/src/internal/SessionManager.js +13 -1
- package/dist/modules/camo-backend/src/internal/storage-paths.js +6 -0
- package/dist/modules/collection-manager/bloom-filter.js +91 -0
- package/dist/modules/collection-manager/date-utils.js +275 -0
- package/dist/modules/collection-manager/index.js +258 -0
- package/dist/modules/collection-manager/storage.js +195 -0
- package/dist/modules/collection-manager/types.js +47 -0
- package/dist/modules/logging/src/index.js +1 -1
- package/dist/modules/process-registry/index.js +230 -0
- package/dist/modules/rate-limiter/index.js +242 -0
- package/dist/modules/workflow/blocks/ExecuteWeiboSearchBlock.js +128 -0
- package/dist/modules/workflow/blocks/PersistXhsNoteBlock.js +7 -3
- package/dist/modules/workflow/blocks/RenderMarkdown.js +4 -1
- package/dist/modules/workflow/blocks/WeiboCollectCommentsBlock.js +282 -0
- package/dist/modules/workflow/blocks/WeiboCollectFromLinksBlock.js +283 -0
- package/dist/modules/workflow/blocks/WeiboCollectSearchLinksBlock.js +208 -0
- package/dist/modules/workflow/blocks/WeiboCollectTimelineListBlock.js +128 -0
- package/dist/modules/workflow/blocks/WeiboCollectUserPostsListBlock.js +127 -0
- package/dist/modules/workflow/blocks/helpers/downloadPaths.js +21 -0
- package/dist/modules/workflow/config/workflowRegistry.js +2 -0
- package/dist/modules/workflow/definitions/weibo-search-workflow-v1.js +47 -0
- package/dist/modules/workflow/src/runner.js +6 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +4 -0
- package/dist/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +2 -2
- package/dist/modules/xiaohongshu/app/src/blocks/helpers/sharding.js +123 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/container-registry/src/index.js +184 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.d.ts +31 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/AnchorVerificationBlock.js +71 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/DetectPageStateBlock.js +259 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.d.ts +28 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/ErrorRecoveryBlock.js +319 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/WaitSearchPermitBlock.js +162 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.d.ts +36 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/containerAnchors.js +301 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/operationLogger.js +195 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.d.ts +25 -0
- package/dist/modules/xiaohongshu/app/src/workflow/blocks/helpers/searchPageState.js +164 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.d.ts +66 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/MatchCommentsBlock.js +139 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.d.ts +16 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1EnsureServicesBlock.js +36 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.d.ts +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1MonitorCookieBlock.js +213 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.d.ts +18 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.js +121 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2CollectLinksBlock.js +1249 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.d.ts +17 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase2SearchBlock.js +703 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.d.ts +15 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseDetailBlock.js +41 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.d.ts +26 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CloseTabsBlock.js +44 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.d.ts +29 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34CollectCommentsBlock.js +150 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.d.ts +38 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ExtractDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.d.ts +30 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenDetailBlock.js +102 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34OpenTabsBlock.js +109 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.d.ts +32 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.js +117 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.d.ts +35 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ProcessSingleNoteBlock.js +114 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.d.ts +34 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase34ValidateLinksBlock.js +90 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.d.ts +111 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase3InteractBlock.js +1009 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.d.ts +20 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/Phase4MultiTabHarvestBlock.js +233 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.d.ts +48 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/ReplyInteractBlock.js +291 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.d.ts +23 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/XhsDiscoverFallbackBlock.js +240 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.d.ts +55 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatchDsl.js +126 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.d.ts +21 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/commentMatcher.js +99 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.d.ts +5 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/evidence.js +27 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.d.ts +37 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/sharding.js +165 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.d.ts +33 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/blocks/helpers/xhsComments.js +270 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.d.ts +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/index.js +9 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.d.ts +50 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/checkpoints.js +222 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.d.ts +10 -0
- package/dist/modules/xiaohongshu/app/src/xiaohongshu/app/src/utils/controllerAction.js +43 -0
- package/dist/services/shared/serviceProcessLogger.js +1 -1
- package/dist/services/unified-api/server.js +105 -11
- package/modules/camo-backend/src/index.ts +46 -1
- package/modules/camo-backend/src/internal/BrowserSession.ts +619 -3
- package/modules/camo-backend/src/internal/SessionManager.ts +12 -1
- package/modules/camo-backend/src/internal/storage-paths.ts +5 -0
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/comments.mjs +38 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/interaction.mjs +47 -2
- package/modules/camo-runtime/src/autoscript/action-providers/xhs/search.mjs +94 -11
- package/modules/camo-runtime/src/autoscript/action-providers/xhs.mjs +208 -2
- package/modules/camo-runtime/src/autoscript/runtime.mjs +7 -1
- package/modules/camo-runtime/src/autoscript/xhs-unified-template.mjs +76 -43
- package/modules/camo-runtime/src/container/runtime-core/operations/index.mjs +75 -1
- package/modules/camo-runtime/src/container/runtime-core/operations/selector-scripts.mjs +71 -4
- package/modules/camo-runtime/src/container/runtime-core/operations/tab-pool.mjs +183 -27
- package/modules/collection-manager/bloom-filter.ts +112 -0
- package/modules/collection-manager/date-utils.ts +316 -0
- package/modules/collection-manager/index.ts +309 -0
- package/modules/collection-manager/package.json +10 -0
- package/modules/collection-manager/storage.ts +174 -0
- package/modules/collection-manager/types.ts +156 -0
- package/modules/logging/src/index.ts +1 -1
- package/modules/process-registry/index.ts +284 -0
- package/modules/rate-limiter/index.ts +322 -0
- package/modules/state/src/paths.ts +9 -1
- package/modules/task-scheduler/index.ts +293 -0
- package/modules/workflow/blocks/ExecuteWeiboSearchBlock.ts +167 -0
- package/modules/workflow/blocks/PersistXhsNoteBlock.ts +7 -3
- package/modules/workflow/blocks/RenderMarkdown.ts +4 -1
- package/modules/workflow/blocks/WeiboCollectCommentsBlock.ts +339 -0
- package/modules/workflow/blocks/WeiboCollectFromLinksBlock.ts +338 -0
- package/modules/workflow/blocks/helpers/downloadPaths.ts +16 -0
- package/modules/workflow/config/workflowRegistry.ts +2 -0
- package/modules/workflow/definitions/weibo-search-workflow-v1.ts +47 -0
- package/modules/workflow/src/runner.ts +6 -0
- package/modules/xiaohongshu/app/src/blocks/Phase1StartProfileBlock.ts +1 -1
- package/modules/xiaohongshu/app/src/blocks/Phase34PersistDetailBlock.ts +4 -0
- package/modules/xiaohongshu/app/src/blocks/Phase3InteractBlock.ts +2 -3
- package/modules/xiaohongshu/app/src/blocks/helpers/sharding.ts +152 -0
- package/package.json +13 -4
- package/scripts/postinstall-resources.mjs +62 -0
- package/scripts/test/run-coverage.mjs +76 -0
- package/scripts/weibo/search.ts +49 -0
- package/services/shared/serviceProcessLogger.ts +1 -1
- package/services/unified-api/server.ts +98 -12
|
@@ -9,7 +9,7 @@ import { logDebug } from '../../../../modules/logging/src/index.js';
|
|
|
9
9
|
import { getStateBus } from './state-bus.js';
|
|
10
10
|
import { loadOrGenerateFingerprint, applyFingerprint } from './fingerprint.js';
|
|
11
11
|
import { launchEngineContext } from './engine-manager.js';
|
|
12
|
-
import { resolveCookiesRoot, resolveProfilesRoot } from './storage-paths.js';
|
|
12
|
+
import { resolveCookiesRoot, resolveProfilesRoot, resolveRecordsRoot } from './storage-paths.js';
|
|
13
13
|
|
|
14
14
|
const stateBus = getStateBus();
|
|
15
15
|
|
|
@@ -23,6 +23,25 @@ export interface BrowserSessionOptions {
|
|
|
23
23
|
fingerprintPlatform?: 'windows' | 'macos' | null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface RecordingOptions {
|
|
27
|
+
name?: string;
|
|
28
|
+
outputPath?: string;
|
|
29
|
+
overlay?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RecordingState {
|
|
33
|
+
active: boolean;
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
name: string | null;
|
|
36
|
+
outputPath: string | null;
|
|
37
|
+
overlay: boolean;
|
|
38
|
+
startedAt: number | null;
|
|
39
|
+
endedAt: number | null;
|
|
40
|
+
eventCount: number;
|
|
41
|
+
lastEventAt: number | null;
|
|
42
|
+
lastError: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
export class BrowserSession {
|
|
27
46
|
private browser?: Browser;
|
|
28
47
|
private context?: BrowserContext;
|
|
@@ -35,8 +54,22 @@ export class BrowserSession {
|
|
|
35
54
|
private lastCookieSaveTs = 0;
|
|
36
55
|
private runtimeObservers = new Set<(event: any) => void>();
|
|
37
56
|
private bridgedPages = new WeakSet<Page>();
|
|
57
|
+
private recorderBridgePages = new WeakSet<Page>();
|
|
38
58
|
private lastViewport: { width: number; height: number } | null = null;
|
|
39
59
|
private fingerprint: any = null;
|
|
60
|
+
private recordingStream: fs.WriteStream | null = null;
|
|
61
|
+
private recording: RecordingState = {
|
|
62
|
+
active: false,
|
|
63
|
+
enabled: false,
|
|
64
|
+
name: null,
|
|
65
|
+
outputPath: null,
|
|
66
|
+
overlay: false,
|
|
67
|
+
startedAt: null,
|
|
68
|
+
endedAt: null,
|
|
69
|
+
eventCount: 0,
|
|
70
|
+
lastEventAt: null,
|
|
71
|
+
lastError: null,
|
|
72
|
+
};
|
|
40
73
|
|
|
41
74
|
onExit?: (profileId: string) => void;
|
|
42
75
|
private exitNotified = false;
|
|
@@ -73,6 +106,7 @@ export class BrowserSession {
|
|
|
73
106
|
current_url: this.getCurrentUrl(),
|
|
74
107
|
mode: this.mode,
|
|
75
108
|
headless: !!this.options.headless,
|
|
109
|
+
recording: this.getRecordingStatus(),
|
|
76
110
|
};
|
|
77
111
|
}
|
|
78
112
|
|
|
@@ -138,10 +172,17 @@ export class BrowserSession {
|
|
|
138
172
|
});
|
|
139
173
|
};
|
|
140
174
|
this.bindRuntimeBridge(page);
|
|
141
|
-
|
|
175
|
+
this.bindRecorderBridge(page);
|
|
176
|
+
page.on('domcontentloaded', () => {
|
|
177
|
+
ensure('domcontentloaded');
|
|
178
|
+
if (this.recording.active) {
|
|
179
|
+
void this.installRecorderRuntime(page, 'domcontentloaded');
|
|
180
|
+
}
|
|
181
|
+
});
|
|
142
182
|
page.on('framenavigated', (frame) => {
|
|
143
183
|
if (frame === page.mainFrame()) {
|
|
144
184
|
ensure('framenavigated');
|
|
185
|
+
this.recordPageVisit(page, 'framenavigated');
|
|
145
186
|
}
|
|
146
187
|
});
|
|
147
188
|
page.on('pageerror', (error) => {
|
|
@@ -154,6 +195,9 @@ export class BrowserSession {
|
|
|
154
195
|
});
|
|
155
196
|
|
|
156
197
|
ensure('initial');
|
|
198
|
+
if (this.recording.active) {
|
|
199
|
+
void this.installRecorderRuntime(page, 'initial');
|
|
200
|
+
}
|
|
157
201
|
}
|
|
158
202
|
|
|
159
203
|
addRuntimeEventObserver(observer: (event: any) => void): () => void {
|
|
@@ -216,6 +260,564 @@ export class BrowserSession {
|
|
|
216
260
|
});
|
|
217
261
|
}
|
|
218
262
|
|
|
263
|
+
private buildRecorderBootstrapScript(): string {
|
|
264
|
+
return `(() => {
|
|
265
|
+
const KEY = '__camoRecorderV1__';
|
|
266
|
+
if (window[KEY]) return window[KEY].getState();
|
|
267
|
+
|
|
268
|
+
const state = {
|
|
269
|
+
enabled: false,
|
|
270
|
+
overlay: false,
|
|
271
|
+
destroyed: false,
|
|
272
|
+
scrollAt: 0,
|
|
273
|
+
wheelAt: 0,
|
|
274
|
+
};
|
|
275
|
+
const listeners = [];
|
|
276
|
+
const OVERLAY_ID = '__camo_recorder_toggle__';
|
|
277
|
+
|
|
278
|
+
const now = () => Date.now();
|
|
279
|
+
const safeText = (value, max = 160) => {
|
|
280
|
+
if (typeof value !== 'string') return '';
|
|
281
|
+
return value.replace(/\\s+/g, ' ').trim().slice(0, max);
|
|
282
|
+
};
|
|
283
|
+
const SENSITIVE_TEXT_RE = /(pass(word)?|pwd|secret|token|otp|one[\\s_-]?time|验证码|校验码|短信|sms|手机|phone|mail|邮箱|email)/i;
|
|
284
|
+
const toNumber = (value, fallback = 0) => {
|
|
285
|
+
const n = Number(value);
|
|
286
|
+
return Number.isFinite(n) ? n : fallback;
|
|
287
|
+
};
|
|
288
|
+
const getAttr = (el, name) => {
|
|
289
|
+
if (!(el instanceof Element)) return '';
|
|
290
|
+
const value = el.getAttribute?.(name);
|
|
291
|
+
return typeof value === 'string' ? value : '';
|
|
292
|
+
};
|
|
293
|
+
const hasSensitiveHint = (value) => SENSITIVE_TEXT_RE.test(String(value || ''));
|
|
294
|
+
const isSensitiveElement = (el) => {
|
|
295
|
+
if (!(el instanceof Element)) return false;
|
|
296
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
297
|
+
const type = String((el instanceof HTMLInputElement ? el.type : getAttr(el, 'type')) || '').toLowerCase();
|
|
298
|
+
if (tag === 'input' && ['password', 'email', 'tel'].includes(type)) return true;
|
|
299
|
+
const autocomplete = String(getAttr(el, 'autocomplete') || '').toLowerCase();
|
|
300
|
+
if (autocomplete.includes('one-time-code') || autocomplete.includes('password')) return true;
|
|
301
|
+
const hint = [
|
|
302
|
+
el.id || '',
|
|
303
|
+
getAttr(el, 'name'),
|
|
304
|
+
getAttr(el, 'aria-label'),
|
|
305
|
+
getAttr(el, 'placeholder'),
|
|
306
|
+
autocomplete,
|
|
307
|
+
String(el.className || ''),
|
|
308
|
+
].join(' ');
|
|
309
|
+
return hasSensitiveHint(hint);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const isVisible = (el) => {
|
|
313
|
+
if (!(el instanceof Element)) return false;
|
|
314
|
+
const rect = el.getBoundingClientRect?.();
|
|
315
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
|
316
|
+
try {
|
|
317
|
+
const style = window.getComputedStyle(el);
|
|
318
|
+
if (!style) return false;
|
|
319
|
+
if (style.display === 'none') return false;
|
|
320
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
|
|
321
|
+
const opacity = Number.parseFloat(String(style.opacity || '1'));
|
|
322
|
+
if (Number.isFinite(opacity) && opacity <= 0.01) return false;
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const buildSelectorPath = (el) => {
|
|
330
|
+
if (!(el instanceof Element)) return null;
|
|
331
|
+
const parts = [];
|
|
332
|
+
let cursor = el;
|
|
333
|
+
let depth = 0;
|
|
334
|
+
while (cursor && depth < 8) {
|
|
335
|
+
const tag = String(cursor.tagName || '').toLowerCase();
|
|
336
|
+
if (!tag) break;
|
|
337
|
+
const id = cursor.id ? '#' + cursor.id : '';
|
|
338
|
+
const cls = Array.from(cursor.classList || []).slice(0, 2).join('.');
|
|
339
|
+
let piece = tag + id + (cls ? '.' + cls : '');
|
|
340
|
+
if (!id) {
|
|
341
|
+
const parent = cursor.parentElement;
|
|
342
|
+
if (parent) {
|
|
343
|
+
const siblings = Array.from(parent.children).filter((item) => item.tagName === cursor.tagName);
|
|
344
|
+
if (siblings.length > 1) {
|
|
345
|
+
const nth = siblings.indexOf(cursor) + 1;
|
|
346
|
+
piece += ':nth-of-type(' + nth + ')';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
parts.unshift(piece);
|
|
351
|
+
cursor = cursor.parentElement;
|
|
352
|
+
depth += 1;
|
|
353
|
+
if (id) break;
|
|
354
|
+
}
|
|
355
|
+
return parts.join(' > ');
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const resolveElement = (target) => {
|
|
359
|
+
if (target instanceof Element) return target;
|
|
360
|
+
if (target && target.scrollingElement instanceof Element) return target.scrollingElement;
|
|
361
|
+
if (document.activeElement instanceof Element) return document.activeElement;
|
|
362
|
+
if (document.scrollingElement instanceof Element) return document.scrollingElement;
|
|
363
|
+
return document.documentElement instanceof Element ? document.documentElement : null;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const buildElementPayload = (target) => {
|
|
367
|
+
const el = resolveElement(target);
|
|
368
|
+
if (!(el instanceof Element)) return null;
|
|
369
|
+
const rect = el.getBoundingClientRect?.();
|
|
370
|
+
const attrs = {};
|
|
371
|
+
['name', 'type', 'role', 'placeholder', 'aria-label'].forEach((key) => {
|
|
372
|
+
const value = el.getAttribute?.(key);
|
|
373
|
+
if (value) attrs[key] = String(value).slice(0, 120);
|
|
374
|
+
});
|
|
375
|
+
const sensitive = isSensitiveElement(el);
|
|
376
|
+
let valueSnippet = null;
|
|
377
|
+
const value = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el.value : null;
|
|
378
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
379
|
+
valueSnippet = sensitive ? '[REDACTED]' : value.slice(0, 120);
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
tag: String(el.tagName || '').toLowerCase(),
|
|
383
|
+
id: el.id || null,
|
|
384
|
+
classes: Array.from(el.classList || []).slice(0, 6),
|
|
385
|
+
selectorPath: buildSelectorPath(el),
|
|
386
|
+
textSnippet: safeText(el.textContent || '', 120),
|
|
387
|
+
attrs,
|
|
388
|
+
valueSnippet,
|
|
389
|
+
sensitive,
|
|
390
|
+
visible: isVisible(el),
|
|
391
|
+
rect: rect
|
|
392
|
+
? {
|
|
393
|
+
x: Math.round(toNumber(rect.x, 0)),
|
|
394
|
+
y: Math.round(toNumber(rect.y, 0)),
|
|
395
|
+
width: Math.round(toNumber(rect.width, 0)),
|
|
396
|
+
height: Math.round(toNumber(rect.height, 0)),
|
|
397
|
+
}
|
|
398
|
+
: null,
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const emit = (type, payload = {}) => {
|
|
403
|
+
if (typeof window.webauto_recorder_dispatch !== 'function') return;
|
|
404
|
+
try {
|
|
405
|
+
window.webauto_recorder_dispatch({
|
|
406
|
+
ts: now(),
|
|
407
|
+
type,
|
|
408
|
+
payload,
|
|
409
|
+
href: String(window.location?.href || ''),
|
|
410
|
+
title: safeText(String(document?.title || ''), 200),
|
|
411
|
+
});
|
|
412
|
+
} catch {
|
|
413
|
+
// ignore bridge errors
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const shouldRecord = (event) => {
|
|
418
|
+
if (state.destroyed || !state.enabled) return false;
|
|
419
|
+
if (!event) return false;
|
|
420
|
+
if (typeof event.isTrusted === 'boolean' && !event.isTrusted) return false;
|
|
421
|
+
return true;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const onClick = (event) => {
|
|
425
|
+
if (!shouldRecord(event)) return;
|
|
426
|
+
emit('interaction.click', {
|
|
427
|
+
button: Number(event.button || 0),
|
|
428
|
+
buttons: Number(event.buttons || 0),
|
|
429
|
+
element: buildElementPayload(event.target),
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const onKeyDown = (event) => {
|
|
434
|
+
if (!shouldRecord(event)) return;
|
|
435
|
+
const element = buildElementPayload(event.target || document.activeElement);
|
|
436
|
+
const isPrintable = typeof event.key === 'string' && event.key.length === 1;
|
|
437
|
+
const redactKey = !!element?.sensitive || (isPrintable && !event.ctrlKey && !event.metaKey && !event.altKey);
|
|
438
|
+
emit('interaction.keydown', {
|
|
439
|
+
key: redactKey ? '[REDACTED]' : String(event.key || ''),
|
|
440
|
+
code: String(event.code || ''),
|
|
441
|
+
ctrlKey: !!event.ctrlKey,
|
|
442
|
+
metaKey: !!event.metaKey,
|
|
443
|
+
altKey: !!event.altKey,
|
|
444
|
+
shiftKey: !!event.shiftKey,
|
|
445
|
+
redacted: redactKey,
|
|
446
|
+
element,
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const onInput = (event) => {
|
|
451
|
+
if (!shouldRecord(event)) return;
|
|
452
|
+
const element = buildElementPayload(event.target || document.activeElement);
|
|
453
|
+
const rawData = typeof event.data === 'string' ? event.data : '';
|
|
454
|
+
const redactData = !!element?.sensitive;
|
|
455
|
+
emit('interaction.input', {
|
|
456
|
+
inputType: String(event.inputType || ''),
|
|
457
|
+
data: redactData ? '[REDACTED]' : safeText(rawData, 80),
|
|
458
|
+
dataLength: rawData.length,
|
|
459
|
+
redacted: redactData,
|
|
460
|
+
element,
|
|
461
|
+
});
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const onWheel = (event) => {
|
|
465
|
+
if (!shouldRecord(event)) return;
|
|
466
|
+
const ts = now();
|
|
467
|
+
if (ts - state.wheelAt < 120) return;
|
|
468
|
+
state.wheelAt = ts;
|
|
469
|
+
emit('interaction.wheel', {
|
|
470
|
+
deltaX: toNumber(event.deltaX, 0),
|
|
471
|
+
deltaY: toNumber(event.deltaY, 0),
|
|
472
|
+
deltaMode: Number(event.deltaMode || 0),
|
|
473
|
+
element: buildElementPayload(event.target),
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const onScroll = (event) => {
|
|
478
|
+
if (!shouldRecord(event)) return;
|
|
479
|
+
const ts = now();
|
|
480
|
+
if (ts - state.scrollAt < 150) return;
|
|
481
|
+
state.scrollAt = ts;
|
|
482
|
+
const target = resolveElement(event.target || document.scrollingElement);
|
|
483
|
+
const scrollTop = target && typeof target.scrollTop === 'number'
|
|
484
|
+
? target.scrollTop
|
|
485
|
+
: (window.scrollY || document.documentElement.scrollTop || 0);
|
|
486
|
+
const scrollLeft = target && typeof target.scrollLeft === 'number'
|
|
487
|
+
? target.scrollLeft
|
|
488
|
+
: (window.scrollX || document.documentElement.scrollLeft || 0);
|
|
489
|
+
emit('interaction.scroll', {
|
|
490
|
+
scrollTop: Math.round(toNumber(scrollTop, 0)),
|
|
491
|
+
scrollLeft: Math.round(toNumber(scrollLeft, 0)),
|
|
492
|
+
element: buildElementPayload(target),
|
|
493
|
+
});
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const addListener = (target, type, handler, options) => {
|
|
497
|
+
target.addEventListener(type, handler, options);
|
|
498
|
+
listeners.push(() => {
|
|
499
|
+
try {
|
|
500
|
+
target.removeEventListener(type, handler, options);
|
|
501
|
+
} catch {
|
|
502
|
+
// ignore
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const getOverlayButton = () => document.getElementById(OVERLAY_ID);
|
|
508
|
+
const applyOverlay = () => {
|
|
509
|
+
const existing = getOverlayButton();
|
|
510
|
+
if (existing && !state.overlay) {
|
|
511
|
+
existing.remove();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!state.overlay) return;
|
|
515
|
+
const btn = existing || document.createElement('button');
|
|
516
|
+
btn.id = OVERLAY_ID;
|
|
517
|
+
btn.type = 'button';
|
|
518
|
+
btn.textContent = state.enabled ? 'REC ON' : 'REC OFF';
|
|
519
|
+
Object.assign(btn.style, {
|
|
520
|
+
position: 'fixed',
|
|
521
|
+
right: '16px',
|
|
522
|
+
bottom: '16px',
|
|
523
|
+
zIndex: '2147483647',
|
|
524
|
+
border: '0',
|
|
525
|
+
borderRadius: '999px',
|
|
526
|
+
background: state.enabled ? '#d63636' : '#5b6575',
|
|
527
|
+
color: '#fff',
|
|
528
|
+
padding: '8px 12px',
|
|
529
|
+
fontSize: '12px',
|
|
530
|
+
fontFamily: 'monospace',
|
|
531
|
+
cursor: 'pointer',
|
|
532
|
+
boxShadow: '0 4px 14px rgba(0,0,0,0.25)',
|
|
533
|
+
});
|
|
534
|
+
if (!existing) {
|
|
535
|
+
btn.addEventListener('click', () => {
|
|
536
|
+
state.enabled = !state.enabled;
|
|
537
|
+
applyOverlay();
|
|
538
|
+
emit('recording.toggled', { enabled: state.enabled, source: 'overlay' });
|
|
539
|
+
});
|
|
540
|
+
(document.body || document.documentElement || document).appendChild(btn);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
addListener(document, 'click', onClick, true);
|
|
545
|
+
addListener(document, 'keydown', onKeyDown, true);
|
|
546
|
+
addListener(document, 'input', onInput, true);
|
|
547
|
+
addListener(window, 'wheel', onWheel, { capture: true, passive: true });
|
|
548
|
+
addListener(window, 'scroll', onScroll, { capture: true, passive: true });
|
|
549
|
+
|
|
550
|
+
const api = {
|
|
551
|
+
setOptions(options = {}) {
|
|
552
|
+
if (typeof options.enabled === 'boolean') {
|
|
553
|
+
state.enabled = options.enabled;
|
|
554
|
+
}
|
|
555
|
+
if (typeof options.overlay === 'boolean') {
|
|
556
|
+
state.overlay = options.overlay;
|
|
557
|
+
}
|
|
558
|
+
applyOverlay();
|
|
559
|
+
return this.getState();
|
|
560
|
+
},
|
|
561
|
+
getState() {
|
|
562
|
+
return {
|
|
563
|
+
ok: true,
|
|
564
|
+
enabled: !!state.enabled,
|
|
565
|
+
overlay: !!state.overlay,
|
|
566
|
+
href: String(window.location?.href || ''),
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
destroy() {
|
|
570
|
+
state.destroyed = true;
|
|
571
|
+
while (listeners.length) {
|
|
572
|
+
const dispose = listeners.pop();
|
|
573
|
+
try {
|
|
574
|
+
dispose && dispose();
|
|
575
|
+
} catch {
|
|
576
|
+
// ignore
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const existing = getOverlayButton();
|
|
580
|
+
if (existing) existing.remove();
|
|
581
|
+
try {
|
|
582
|
+
delete window[KEY];
|
|
583
|
+
} catch {
|
|
584
|
+
window[KEY] = undefined;
|
|
585
|
+
}
|
|
586
|
+
return { ok: true };
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
window[KEY] = api;
|
|
591
|
+
applyOverlay();
|
|
592
|
+
emit('recording.runtime_ready', { enabled: state.enabled, overlay: state.overlay });
|
|
593
|
+
return api.getState();
|
|
594
|
+
})();`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private bindRecorderBridge(page: Page) {
|
|
598
|
+
if (this.recorderBridgePages.has(page)) return;
|
|
599
|
+
this.recorderBridgePages.add(page);
|
|
600
|
+
page.exposeFunction('webauto_recorder_dispatch', (evt: any) => {
|
|
601
|
+
this.handleRecorderEvent(page, evt);
|
|
602
|
+
}).catch((err) => {
|
|
603
|
+
console.warn(`[session:${this.id}] failed to expose webauto_recorder_dispatch`, err?.message || err);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private async installRecorderRuntime(page: Page, reason: string): Promise<void> {
|
|
608
|
+
if (!page || page.isClosed()) return;
|
|
609
|
+
try {
|
|
610
|
+
await page.evaluate(this.buildRecorderBootstrapScript());
|
|
611
|
+
} catch {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (this.recording.active) {
|
|
615
|
+
await this.syncRecorderStateToPage(page).catch(() => {});
|
|
616
|
+
this.recordPageVisit(page, reason);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private async syncRecorderStateToPage(page: Page): Promise<void> {
|
|
621
|
+
if (!page || page.isClosed()) return;
|
|
622
|
+
await page.evaluate(
|
|
623
|
+
(options) => {
|
|
624
|
+
const runtime = (window as any).__camoRecorderV1__;
|
|
625
|
+
if (!runtime || typeof runtime.setOptions !== 'function') return null;
|
|
626
|
+
return runtime.setOptions(options);
|
|
627
|
+
},
|
|
628
|
+
{ enabled: this.recording.enabled, overlay: this.recording.overlay },
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private async destroyRecorderRuntimeOnPage(page: Page): Promise<void> {
|
|
633
|
+
if (!page || page.isClosed()) return;
|
|
634
|
+
await page.evaluate(() => {
|
|
635
|
+
const runtime = (window as any).__camoRecorderV1__;
|
|
636
|
+
if (!runtime || typeof runtime.destroy !== 'function') return null;
|
|
637
|
+
return runtime.destroy();
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private normalizeRecordingName(raw?: string): string {
|
|
642
|
+
const text = String(raw || '').trim();
|
|
643
|
+
const fallback = `record-${this.id}`;
|
|
644
|
+
if (!text) return fallback;
|
|
645
|
+
return text.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 120) || fallback;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private buildRecordingFilename(name: string): string {
|
|
649
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
650
|
+
return `${stamp}-${name}.jsonl`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private resolveRecordingOutputPath(options: RecordingOptions): string {
|
|
654
|
+
const name = this.normalizeRecordingName(options?.name);
|
|
655
|
+
const rawOutput = String(options?.outputPath || '').trim();
|
|
656
|
+
if (!rawOutput) {
|
|
657
|
+
const root = path.join(resolveRecordsRoot(), this.id);
|
|
658
|
+
return path.join(root, this.buildRecordingFilename(name));
|
|
659
|
+
}
|
|
660
|
+
const absolute = path.isAbsolute(rawOutput) ? rawOutput : path.resolve(rawOutput);
|
|
661
|
+
if (absolute.endsWith(path.sep)) {
|
|
662
|
+
return path.join(absolute, this.buildRecordingFilename(name));
|
|
663
|
+
}
|
|
664
|
+
if (fs.existsSync(absolute) && fs.statSync(absolute).isDirectory()) {
|
|
665
|
+
return path.join(absolute, this.buildRecordingFilename(name));
|
|
666
|
+
}
|
|
667
|
+
return absolute;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private writeRecordingEvent(
|
|
671
|
+
type: string,
|
|
672
|
+
payload: any = {},
|
|
673
|
+
options: { pageUrl?: string; allowWhenDisabled?: boolean } = {},
|
|
674
|
+
) {
|
|
675
|
+
if (!this.recordingStream || !this.recording.active) return;
|
|
676
|
+
if (!this.recording.enabled && !options.allowWhenDisabled) return;
|
|
677
|
+
const eventTs = Date.now();
|
|
678
|
+
const entry = {
|
|
679
|
+
ts: eventTs,
|
|
680
|
+
profileId: this.id,
|
|
681
|
+
sessionId: this.id,
|
|
682
|
+
type,
|
|
683
|
+
url: options.pageUrl || this.getCurrentUrl() || null,
|
|
684
|
+
payload,
|
|
685
|
+
};
|
|
686
|
+
try {
|
|
687
|
+
this.recordingStream.write(`${JSON.stringify(entry)}\n`);
|
|
688
|
+
this.recording.eventCount += 1;
|
|
689
|
+
this.recording.lastEventAt = eventTs;
|
|
690
|
+
} catch (err) {
|
|
691
|
+
this.recording.lastError = (err as Error)?.message || String(err);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private handleRecorderEvent(page: Page, evt: any) {
|
|
696
|
+
const type = String(evt?.type || '').trim();
|
|
697
|
+
if (!type) return;
|
|
698
|
+
const pageUrl = String(evt?.href || page?.url?.() || this.getCurrentUrl() || '');
|
|
699
|
+
const payload = evt?.payload && typeof evt.payload === 'object' ? evt.payload : {};
|
|
700
|
+
|
|
701
|
+
if (type === 'recording.toggled') {
|
|
702
|
+
if (!this.recording.active) {
|
|
703
|
+
this.recording.enabled = false;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
this.recording.enabled = payload.enabled !== false;
|
|
707
|
+
this.writeRecordingEvent(type, payload, { pageUrl, allowWhenDisabled: true });
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (type === 'recording.runtime_ready') {
|
|
711
|
+
this.writeRecordingEvent(type, payload, { pageUrl, allowWhenDisabled: true });
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
this.writeRecordingEvent(type, payload, { pageUrl });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private recordPageVisit(page: Page, reason: string) {
|
|
718
|
+
const pageUrl = page?.url?.() || this.getCurrentUrl() || null;
|
|
719
|
+
if (!pageUrl) return;
|
|
720
|
+
this.lastKnownUrl = pageUrl;
|
|
721
|
+
this.writeRecordingEvent(
|
|
722
|
+
'page.visit',
|
|
723
|
+
{ reason, title: null },
|
|
724
|
+
{ pageUrl },
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
getRecordingStatus(): RecordingState {
|
|
729
|
+
return { ...this.recording };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async startRecording(options: RecordingOptions = {}): Promise<RecordingState> {
|
|
733
|
+
const outputPath = this.resolveRecordingOutputPath(options);
|
|
734
|
+
const name = this.normalizeRecordingName(options?.name);
|
|
735
|
+
const overlay = options?.overlay !== false;
|
|
736
|
+
|
|
737
|
+
if (this.recordingStream) {
|
|
738
|
+
await this.stopRecording({ reason: 'restart' });
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
742
|
+
const stream = fs.createWriteStream(outputPath, { flags: 'a', encoding: 'utf-8' });
|
|
743
|
+
stream.on('error', (err) => {
|
|
744
|
+
this.recording.lastError = (err as Error)?.message || String(err);
|
|
745
|
+
this.recording.enabled = false;
|
|
746
|
+
});
|
|
747
|
+
this.recordingStream = stream;
|
|
748
|
+
|
|
749
|
+
this.recording = {
|
|
750
|
+
active: true,
|
|
751
|
+
enabled: true,
|
|
752
|
+
name,
|
|
753
|
+
outputPath,
|
|
754
|
+
overlay,
|
|
755
|
+
startedAt: Date.now(),
|
|
756
|
+
endedAt: null,
|
|
757
|
+
eventCount: 0,
|
|
758
|
+
lastEventAt: null,
|
|
759
|
+
lastError: null,
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
if (this.context) {
|
|
763
|
+
const pages = this.context.pages().filter((p) => !p.isClosed());
|
|
764
|
+
for (const page of pages) {
|
|
765
|
+
this.bindRecorderBridge(page);
|
|
766
|
+
// eslint-disable-next-line no-await-in-loop
|
|
767
|
+
await this.installRecorderRuntime(page, 'recording_start').catch(() => {});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
this.writeRecordingEvent(
|
|
772
|
+
'recording.start',
|
|
773
|
+
{ name, outputPath, overlay },
|
|
774
|
+
{ allowWhenDisabled: true },
|
|
775
|
+
);
|
|
776
|
+
return this.getRecordingStatus();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async stopRecording(options: { reason?: string } = {}): Promise<RecordingState> {
|
|
780
|
+
if (!this.recordingStream) {
|
|
781
|
+
this.recording.active = false;
|
|
782
|
+
this.recording.enabled = false;
|
|
783
|
+
this.recording.overlay = false;
|
|
784
|
+
this.recording.endedAt = Date.now();
|
|
785
|
+
if (this.context) {
|
|
786
|
+
const pages = this.context.pages().filter((p) => !p.isClosed());
|
|
787
|
+
for (const page of pages) {
|
|
788
|
+
// eslint-disable-next-line no-await-in-loop
|
|
789
|
+
await this.destroyRecorderRuntimeOnPage(page).catch(() => {});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return this.getRecordingStatus();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
this.writeRecordingEvent(
|
|
796
|
+
'recording.stop',
|
|
797
|
+
{ reason: options.reason || 'manual' },
|
|
798
|
+
{ allowWhenDisabled: true },
|
|
799
|
+
);
|
|
800
|
+
this.recording.enabled = false;
|
|
801
|
+
this.recording.active = false;
|
|
802
|
+
this.recording.overlay = false;
|
|
803
|
+
this.recording.endedAt = Date.now();
|
|
804
|
+
|
|
805
|
+
if (this.context) {
|
|
806
|
+
const pages = this.context.pages().filter((p) => !p.isClosed());
|
|
807
|
+
for (const page of pages) {
|
|
808
|
+
// eslint-disable-next-line no-await-in-loop
|
|
809
|
+
await this.destroyRecorderRuntimeOnPage(page).catch(() => {});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const stream = this.recordingStream;
|
|
814
|
+
this.recordingStream = null;
|
|
815
|
+
await new Promise<void>((resolve) => {
|
|
816
|
+
stream.end(() => resolve());
|
|
817
|
+
});
|
|
818
|
+
return this.getRecordingStatus();
|
|
819
|
+
}
|
|
820
|
+
|
|
219
821
|
private getActivePage(): Page | null {
|
|
220
822
|
if (this.page && !this.page.isClosed()) {
|
|
221
823
|
return this.page;
|
|
@@ -363,7 +965,20 @@ export class BrowserSession {
|
|
|
363
965
|
await new Promise((r) => setTimeout(r, 250));
|
|
364
966
|
}
|
|
365
967
|
|
|
366
|
-
|
|
968
|
+
let after = ctx.pages().filter((p) => !p.isClosed()).length;
|
|
969
|
+
if (!page || after <= before) {
|
|
970
|
+
try {
|
|
971
|
+
page = await ctx.newPage();
|
|
972
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 8000 }).catch((): any => null);
|
|
973
|
+
} catch {
|
|
974
|
+
// ignore fallback errors
|
|
975
|
+
}
|
|
976
|
+
after = ctx.pages().filter((p) => !p.isClosed()).length;
|
|
977
|
+
if (!page && after > before) {
|
|
978
|
+
const pagesNow = ctx.pages().filter((p) => !p.isClosed());
|
|
979
|
+
page = pagesNow[pagesNow.length - 1] || null;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
367
982
|
if (!page || after <= before) {
|
|
368
983
|
throw new Error('new_tab_failed');
|
|
369
984
|
}
|
|
@@ -844,6 +1459,7 @@ export class BrowserSession {
|
|
|
844
1459
|
|
|
845
1460
|
async close(): Promise<void> {
|
|
846
1461
|
try {
|
|
1462
|
+
await this.stopRecording({ reason: 'session_close' }).catch(() => {});
|
|
847
1463
|
await this.context?.close();
|
|
848
1464
|
} finally {
|
|
849
1465
|
await this.browser?.close();
|