@web-auto/camo 0.1.26 → 0.2.1
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/LICENSE +21 -21
- package/README.md +586 -586
- package/bin/browser-service.mjs +11 -11
- package/bin/camo.mjs +22 -22
- package/package.json +48 -48
- package/scripts/build.mjs +19 -19
- package/scripts/bump-version.mjs +34 -34
- package/scripts/check-file-size.mjs +80 -80
- package/scripts/file-size-policy.json +12 -2
- package/scripts/install.mjs +76 -76
- package/scripts/release.sh +54 -54
- package/src/autoscript/action-providers/index.mjs +6 -6
- package/src/autoscript/impact-engine.mjs +78 -78
- package/src/autoscript/runtime.mjs +1017 -1017
- package/src/autoscript/schema.mjs +376 -376
- package/src/cli.mjs +405 -405
- package/src/commands/attach.mjs +141 -141
- package/src/commands/autoscript.mjs +1011 -1011
- package/src/commands/browser.mjs +1255 -1257
- package/src/commands/container.mjs +401 -401
- package/src/commands/cookies.mjs +69 -69
- package/src/commands/create.mjs +98 -98
- package/src/commands/devtools.mjs +349 -349
- package/src/commands/events.mjs +152 -152
- package/src/commands/highlight-mode.mjs +24 -24
- package/src/commands/init.mjs +68 -68
- package/src/commands/lifecycle.mjs +275 -275
- package/src/commands/mouse.mjs +45 -45
- package/src/commands/profile.mjs +46 -46
- package/src/commands/record.mjs +115 -115
- package/src/commands/system.mjs +14 -14
- package/src/commands/window.mjs +123 -123
- package/src/container/change-notifier.mjs +362 -362
- package/src/container/element-filter.mjs +143 -143
- package/src/container/index.mjs +3 -3
- package/src/container/runtime-core/checkpoint.mjs +209 -209
- package/src/container/runtime-core/index.mjs +21 -21
- package/src/container/runtime-core/operations/index.mjs +774 -774
- package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
- package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
- package/src/container/runtime-core/operations/viewport.mjs +189 -189
- package/src/container/runtime-core/search.mjs +190 -190
- package/src/container/runtime-core/subscription.mjs +224 -224
- package/src/container/runtime-core/utils.mjs +94 -94
- package/src/container/runtime-core/validation.mjs +127 -184
- package/src/container/runtime-core.mjs +1 -1
- package/src/container/subscription-registry.mjs +459 -459
- package/src/core/actions.mjs +561 -561
- package/src/core/browser.mjs +266 -266
- package/src/core/index.mjs +52 -52
- package/src/core/utils.mjs +91 -91
- package/src/events/daemon-entry.mjs +33 -33
- package/src/events/daemon.mjs +80 -80
- package/src/events/progress-log.mjs +109 -109
- package/src/events/ws-server.mjs +239 -239
- package/src/lib/client.mjs +200 -200
- package/src/lifecycle/cleanup.mjs +83 -83
- package/src/lifecycle/lock.mjs +126 -126
- package/src/lifecycle/session-registry.mjs +279 -279
- package/src/lifecycle/session-view.mjs +76 -76
- package/src/lifecycle/session-watchdog.mjs +281 -281
- package/src/services/browser-service/index.js +671 -674
- package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
- package/src/services/browser-service/internal/BrowserSession.js +325 -336
- package/src/services/browser-service/internal/ElementRegistry.js +60 -60
- package/src/services/browser-service/internal/ProfileLock.js +84 -84
- package/src/services/browser-service/internal/SessionManager.js +184 -184
- package/src/services/browser-service/internal/SessionManager.test.js +39 -39
- package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
- package/src/services/browser-service/internal/browser-session/input-ops.js +222 -219
- package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
- package/src/services/browser-service/internal/browser-session/logging.js +46 -46
- package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
- package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
- package/src/services/browser-service/internal/browser-session/page-management.js +302 -336
- package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
- package/src/services/browser-service/internal/browser-session/recording.js +198 -198
- package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
- package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
- package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
- package/src/services/browser-service/internal/browser-session/types.js +14 -14
- package/src/services/browser-service/internal/browser-session/utils.js +95 -95
- package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
- package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
- package/src/services/browser-service/internal/container-matcher.js +851 -851
- package/src/services/browser-service/internal/container-registry.js +182 -182
- package/src/services/browser-service/internal/engine-manager.js +259 -259
- package/src/services/browser-service/internal/fingerprint.js +203 -203
- package/src/services/browser-service/internal/heartbeat.js +137 -137
- package/src/services/browser-service/internal/logging.js +46 -46
- package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
- package/src/services/browser-service/internal/pageRuntime.js +28 -28
- package/src/services/browser-service/internal/runtimeInjector.js +31 -31
- package/src/services/browser-service/internal/service-process-logger.js +140 -140
- package/src/services/browser-service/internal/state-bus.js +45 -45
- package/src/services/browser-service/internal/storage-paths.js +42 -42
- package/src/services/browser-service/internal/ws-server.js +1194 -1194
- package/src/services/browser-service/internal/ws-server.test.js +58 -58
- package/src/services/browser-service/server.mjs +6 -6
- package/src/services/controller/cli-bridge.js +93 -93
- package/src/services/controller/container-index.js +50 -50
- package/src/services/controller/container-storage.js +36 -36
- package/src/services/controller/controller-actions.js +207 -207
- package/src/services/controller/controller.js +1138 -1138
- package/src/services/controller/selectors.js +54 -54
- package/src/services/controller/transport.js +125 -125
- package/src/utils/args.mjs +26 -26
- package/src/utils/browser-service.mjs +544 -544
- package/src/utils/command-log.mjs +64 -64
- package/src/utils/config.mjs +214 -214
- package/src/utils/fingerprint.mjs +181 -181
- package/src/utils/help.mjs +216 -216
- package/src/utils/js-policy.mjs +13 -13
- package/src/utils/ws-client.mjs +30 -30
- package/src/container/runtime-core/operations/tab-pool.mjs.bak +0 -762
- package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +0 -762
- package/src/services/browser-service/index.js.bak +0 -671
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
import { getDomSnapshotByProfile } from '../../utils/browser-service.mjs';
|
|
2
|
-
import { ChangeNotifier } from '../change-notifier.mjs';
|
|
3
|
-
import { ensureActiveSession, getCurrentUrl, normalizeArray } from './utils.mjs';
|
|
4
|
-
|
|
5
|
-
function normalizeElementKeys(elements) {
|
|
6
|
-
return (Array.isArray(elements) ? elements : [])
|
|
7
|
-
.map((node) => String(node?.path || '').trim())
|
|
8
|
-
.filter(Boolean)
|
|
9
|
-
.sort();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function joinElementKeys(keys) {
|
|
13
|
-
return Array.isArray(keys) && keys.length > 0 ? keys.join('|') : 'none';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function buildEventKey(subscriptionId, type, presenceVersion, keys) {
|
|
17
|
-
return `${subscriptionId}:${type}:p${Math.max(0, Number(presenceVersion) || 0)}:k${joinElementKeys(keys)}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function isTransientSubscriptionError(error) {
|
|
21
|
-
const message = String(error?.message || error || '').trim().toLowerCase();
|
|
22
|
-
if (!message) return false;
|
|
23
|
-
return message.includes('execution context was destroyed')
|
|
24
|
-
|| message.includes('most likely because of a navigation')
|
|
25
|
-
|| message.includes('cannot find context with specified id')
|
|
26
|
-
|| message.includes('target closed');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function resolveFilterMode(input) {
|
|
30
|
-
const text = String(input || process.env.CAMO_FILTER_MODE || 'strict').trim().toLowerCase();
|
|
31
|
-
if (!text) return 'strict';
|
|
32
|
-
if (text === 'legacy') return 'legacy';
|
|
33
|
-
return 'strict';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function urlMatchesFilter(url, item) {
|
|
37
|
-
const href = String(url || '').trim();
|
|
38
|
-
const includes = normalizeArray(item?.pageUrlIncludes || item?.urlIncludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
39
|
-
const excludes = normalizeArray(item?.pageUrlExcludes || item?.urlExcludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
40
|
-
if (includes.length > 0 && !includes.some((token) => href.includes(token))) return false;
|
|
41
|
-
if (excludes.length > 0 && excludes.some((token) => href.includes(token))) return false;
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function watchSubscriptions({
|
|
46
|
-
profileId,
|
|
47
|
-
subscriptions,
|
|
48
|
-
throttle = 500,
|
|
49
|
-
filterMode = 'strict',
|
|
50
|
-
onEvent = () => {},
|
|
51
|
-
onError = () => {},
|
|
52
|
-
}) {
|
|
53
|
-
const session = await ensureActiveSession(profileId);
|
|
54
|
-
const resolvedProfile = session.profileId || profileId;
|
|
55
|
-
const notifier = new ChangeNotifier();
|
|
56
|
-
const effectiveFilterMode = resolveFilterMode(filterMode);
|
|
57
|
-
const strictFilter = effectiveFilterMode === 'strict';
|
|
58
|
-
const items = normalizeArray(subscriptions)
|
|
59
|
-
.map((item, index) => {
|
|
60
|
-
if (!item || typeof item !== 'object') return null;
|
|
61
|
-
const id = String(item.id || `sub_${index + 1}`);
|
|
62
|
-
const selector = String(item.selector || '').trim();
|
|
63
|
-
if (!selector) return null;
|
|
64
|
-
const events = normalizeArray(item.events).map((name) => String(name).trim()).filter(Boolean);
|
|
65
|
-
const pageUrlIncludes = normalizeArray(item.pageUrlIncludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
66
|
-
const pageUrlExcludes = normalizeArray(item.pageUrlExcludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
67
|
-
return {
|
|
68
|
-
id,
|
|
69
|
-
selector,
|
|
70
|
-
visible: strictFilter ? true : (item.visible !== false),
|
|
71
|
-
pageUrlIncludes,
|
|
72
|
-
pageUrlExcludes,
|
|
73
|
-
events: events.length > 0 ? new Set(events) : null,
|
|
74
|
-
};
|
|
75
|
-
})
|
|
76
|
-
.filter(Boolean);
|
|
77
|
-
|
|
78
|
-
const state = new Map(items.map((item) => [item.id, {
|
|
79
|
-
exists: false,
|
|
80
|
-
stateSig: '',
|
|
81
|
-
appearCount: 0,
|
|
82
|
-
presenceVersion: 0,
|
|
83
|
-
elementKeys: [],
|
|
84
|
-
}]));
|
|
85
|
-
const intervalMs = Math.max(100, Number(throttle) || 500);
|
|
86
|
-
let stopped = false;
|
|
87
|
-
|
|
88
|
-
const emit = async (payload) => {
|
|
89
|
-
try {
|
|
90
|
-
await onEvent(payload);
|
|
91
|
-
} catch (err) {
|
|
92
|
-
onError(err);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const poll = async () => {
|
|
97
|
-
if (stopped) return;
|
|
98
|
-
try {
|
|
99
|
-
const snapshot = await getDomSnapshotByProfile(resolvedProfile);
|
|
100
|
-
const currentUrl = String(snapshot?.__url || '') || await getCurrentUrl(resolvedProfile).catch(() => '');
|
|
101
|
-
const ts = new Date().toISOString();
|
|
102
|
-
for (const item of items) {
|
|
103
|
-
const prev = state.get(item.id) || {
|
|
104
|
-
exists: false,
|
|
105
|
-
stateSig: '',
|
|
106
|
-
appearCount: 0,
|
|
107
|
-
presenceVersion: 0,
|
|
108
|
-
elementKeys: [],
|
|
109
|
-
};
|
|
110
|
-
const urlMatched = urlMatchesFilter(currentUrl, item);
|
|
111
|
-
const elements = urlMatched
|
|
112
|
-
? notifier.findElements(snapshot, { css: item.selector, visible: item.visible })
|
|
113
|
-
: [];
|
|
114
|
-
const exists = elements.length > 0;
|
|
115
|
-
const elementKeys = normalizeElementKeys(elements);
|
|
116
|
-
const prevElementKeys = Array.isArray(prev.elementKeys) ? prev.elementKeys : [];
|
|
117
|
-
const prevElementKeySet = new Set(prevElementKeys);
|
|
118
|
-
const elementKeySet = new Set(elementKeys);
|
|
119
|
-
const appearedKeys = elementKeys.filter((key) => !prevElementKeySet.has(key));
|
|
120
|
-
const disappearedKeys = prevElementKeys.filter((key) => !elementKeySet.has(key));
|
|
121
|
-
const stateSig = elementKeys.join(',');
|
|
122
|
-
const changed = stateSig !== prev.stateSig;
|
|
123
|
-
const presenceVersion = prev.presenceVersion + (exists && !prev.exists ? 1 : 0);
|
|
124
|
-
const next = {
|
|
125
|
-
exists,
|
|
126
|
-
stateSig,
|
|
127
|
-
appearCount: prev.appearCount + (exists && !prev.exists ? 1 : 0),
|
|
128
|
-
presenceVersion,
|
|
129
|
-
elementKeys,
|
|
130
|
-
};
|
|
131
|
-
state.set(item.id, next);
|
|
132
|
-
|
|
133
|
-
const shouldEmit = (type) => !item.events || item.events.has(type);
|
|
134
|
-
if (exists && !prev.exists && shouldEmit('appear')) {
|
|
135
|
-
await emit({
|
|
136
|
-
type: 'appear',
|
|
137
|
-
profileId: resolvedProfile,
|
|
138
|
-
subscriptionId: item.id,
|
|
139
|
-
selector: item.selector,
|
|
140
|
-
count: elements.length,
|
|
141
|
-
elements,
|
|
142
|
-
pageUrl: currentUrl,
|
|
143
|
-
filterMode: effectiveFilterMode,
|
|
144
|
-
elementKeys,
|
|
145
|
-
presenceVersion,
|
|
146
|
-
stateKey: stateSig,
|
|
147
|
-
eventKey: buildEventKey(item.id, 'appear', presenceVersion, appearedKeys.length > 0 ? appearedKeys : elementKeys),
|
|
148
|
-
timestamp: ts,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (!exists && prev.exists && shouldEmit('disappear')) {
|
|
152
|
-
await emit({
|
|
153
|
-
type: 'disappear',
|
|
154
|
-
profileId: resolvedProfile,
|
|
155
|
-
subscriptionId: item.id,
|
|
156
|
-
selector: item.selector,
|
|
157
|
-
count: 0,
|
|
158
|
-
elements: [],
|
|
159
|
-
pageUrl: currentUrl,
|
|
160
|
-
filterMode: effectiveFilterMode,
|
|
161
|
-
elementKeys: [],
|
|
162
|
-
departedElementKeys: disappearedKeys,
|
|
163
|
-
presenceVersion: prev.presenceVersion,
|
|
164
|
-
stateKey: '',
|
|
165
|
-
eventKey: buildEventKey(item.id, 'disappear', prev.presenceVersion, disappearedKeys),
|
|
166
|
-
timestamp: ts,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
if (exists && shouldEmit('exist')) {
|
|
170
|
-
await emit({
|
|
171
|
-
type: 'exist',
|
|
172
|
-
profileId: resolvedProfile,
|
|
173
|
-
subscriptionId: item.id,
|
|
174
|
-
selector: item.selector,
|
|
175
|
-
count: elements.length,
|
|
176
|
-
elements,
|
|
177
|
-
pageUrl: currentUrl,
|
|
178
|
-
filterMode: effectiveFilterMode,
|
|
179
|
-
elementKeys,
|
|
180
|
-
presenceVersion,
|
|
181
|
-
stateKey: stateSig,
|
|
182
|
-
eventKey: buildEventKey(item.id, 'exist', presenceVersion, elementKeys),
|
|
183
|
-
timestamp: ts,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
if (changed && shouldEmit('change')) {
|
|
187
|
-
await emit({
|
|
188
|
-
type: 'change',
|
|
189
|
-
profileId: resolvedProfile,
|
|
190
|
-
subscriptionId: item.id,
|
|
191
|
-
selector: item.selector,
|
|
192
|
-
count: elements.length,
|
|
193
|
-
elements,
|
|
194
|
-
pageUrl: currentUrl,
|
|
195
|
-
filterMode: effectiveFilterMode,
|
|
196
|
-
elementKeys,
|
|
197
|
-
appearedElementKeys: appearedKeys,
|
|
198
|
-
departedElementKeys: disappearedKeys,
|
|
199
|
-
presenceVersion,
|
|
200
|
-
stateKey: stateSig,
|
|
201
|
-
eventKey: buildEventKey(item.id, 'change', presenceVersion, elementKeys),
|
|
202
|
-
timestamp: ts,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
await emit({ type: 'tick', profileId: resolvedProfile, timestamp: ts });
|
|
207
|
-
} catch (err) {
|
|
208
|
-
if (isTransientSubscriptionError(err)) return;
|
|
209
|
-
onError(err);
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const interval = setInterval(poll, intervalMs);
|
|
214
|
-
await poll();
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
stop: () => {
|
|
218
|
-
if (stopped) return;
|
|
219
|
-
stopped = true;
|
|
220
|
-
clearInterval(interval);
|
|
221
|
-
},
|
|
222
|
-
profileId: resolvedProfile,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
1
|
+
import { getDomSnapshotByProfile } from '../../utils/browser-service.mjs';
|
|
2
|
+
import { ChangeNotifier } from '../change-notifier.mjs';
|
|
3
|
+
import { ensureActiveSession, getCurrentUrl, normalizeArray } from './utils.mjs';
|
|
4
|
+
|
|
5
|
+
function normalizeElementKeys(elements) {
|
|
6
|
+
return (Array.isArray(elements) ? elements : [])
|
|
7
|
+
.map((node) => String(node?.path || '').trim())
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.sort();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function joinElementKeys(keys) {
|
|
13
|
+
return Array.isArray(keys) && keys.length > 0 ? keys.join('|') : 'none';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildEventKey(subscriptionId, type, presenceVersion, keys) {
|
|
17
|
+
return `${subscriptionId}:${type}:p${Math.max(0, Number(presenceVersion) || 0)}:k${joinElementKeys(keys)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isTransientSubscriptionError(error) {
|
|
21
|
+
const message = String(error?.message || error || '').trim().toLowerCase();
|
|
22
|
+
if (!message) return false;
|
|
23
|
+
return message.includes('execution context was destroyed')
|
|
24
|
+
|| message.includes('most likely because of a navigation')
|
|
25
|
+
|| message.includes('cannot find context with specified id')
|
|
26
|
+
|| message.includes('target closed');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveFilterMode(input) {
|
|
30
|
+
const text = String(input || process.env.CAMO_FILTER_MODE || 'strict').trim().toLowerCase();
|
|
31
|
+
if (!text) return 'strict';
|
|
32
|
+
if (text === 'legacy') return 'legacy';
|
|
33
|
+
return 'strict';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function urlMatchesFilter(url, item) {
|
|
37
|
+
const href = String(url || '').trim();
|
|
38
|
+
const includes = normalizeArray(item?.pageUrlIncludes || item?.urlIncludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
39
|
+
const excludes = normalizeArray(item?.pageUrlExcludes || item?.urlExcludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
40
|
+
if (includes.length > 0 && !includes.some((token) => href.includes(token))) return false;
|
|
41
|
+
if (excludes.length > 0 && excludes.some((token) => href.includes(token))) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function watchSubscriptions({
|
|
46
|
+
profileId,
|
|
47
|
+
subscriptions,
|
|
48
|
+
throttle = 500,
|
|
49
|
+
filterMode = 'strict',
|
|
50
|
+
onEvent = () => {},
|
|
51
|
+
onError = () => {},
|
|
52
|
+
}) {
|
|
53
|
+
const session = await ensureActiveSession(profileId);
|
|
54
|
+
const resolvedProfile = session.profileId || profileId;
|
|
55
|
+
const notifier = new ChangeNotifier();
|
|
56
|
+
const effectiveFilterMode = resolveFilterMode(filterMode);
|
|
57
|
+
const strictFilter = effectiveFilterMode === 'strict';
|
|
58
|
+
const items = normalizeArray(subscriptions)
|
|
59
|
+
.map((item, index) => {
|
|
60
|
+
if (!item || typeof item !== 'object') return null;
|
|
61
|
+
const id = String(item.id || `sub_${index + 1}`);
|
|
62
|
+
const selector = String(item.selector || '').trim();
|
|
63
|
+
if (!selector) return null;
|
|
64
|
+
const events = normalizeArray(item.events).map((name) => String(name).trim()).filter(Boolean);
|
|
65
|
+
const pageUrlIncludes = normalizeArray(item.pageUrlIncludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
66
|
+
const pageUrlExcludes = normalizeArray(item.pageUrlExcludes).map((token) => String(token || '').trim()).filter(Boolean);
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
selector,
|
|
70
|
+
visible: strictFilter ? true : (item.visible !== false),
|
|
71
|
+
pageUrlIncludes,
|
|
72
|
+
pageUrlExcludes,
|
|
73
|
+
events: events.length > 0 ? new Set(events) : null,
|
|
74
|
+
};
|
|
75
|
+
})
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
|
|
78
|
+
const state = new Map(items.map((item) => [item.id, {
|
|
79
|
+
exists: false,
|
|
80
|
+
stateSig: '',
|
|
81
|
+
appearCount: 0,
|
|
82
|
+
presenceVersion: 0,
|
|
83
|
+
elementKeys: [],
|
|
84
|
+
}]));
|
|
85
|
+
const intervalMs = Math.max(100, Number(throttle) || 500);
|
|
86
|
+
let stopped = false;
|
|
87
|
+
|
|
88
|
+
const emit = async (payload) => {
|
|
89
|
+
try {
|
|
90
|
+
await onEvent(payload);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
onError(err);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const poll = async () => {
|
|
97
|
+
if (stopped) return;
|
|
98
|
+
try {
|
|
99
|
+
const snapshot = await getDomSnapshotByProfile(resolvedProfile);
|
|
100
|
+
const currentUrl = String(snapshot?.__url || '') || await getCurrentUrl(resolvedProfile).catch(() => '');
|
|
101
|
+
const ts = new Date().toISOString();
|
|
102
|
+
for (const item of items) {
|
|
103
|
+
const prev = state.get(item.id) || {
|
|
104
|
+
exists: false,
|
|
105
|
+
stateSig: '',
|
|
106
|
+
appearCount: 0,
|
|
107
|
+
presenceVersion: 0,
|
|
108
|
+
elementKeys: [],
|
|
109
|
+
};
|
|
110
|
+
const urlMatched = urlMatchesFilter(currentUrl, item);
|
|
111
|
+
const elements = urlMatched
|
|
112
|
+
? notifier.findElements(snapshot, { css: item.selector, visible: item.visible })
|
|
113
|
+
: [];
|
|
114
|
+
const exists = elements.length > 0;
|
|
115
|
+
const elementKeys = normalizeElementKeys(elements);
|
|
116
|
+
const prevElementKeys = Array.isArray(prev.elementKeys) ? prev.elementKeys : [];
|
|
117
|
+
const prevElementKeySet = new Set(prevElementKeys);
|
|
118
|
+
const elementKeySet = new Set(elementKeys);
|
|
119
|
+
const appearedKeys = elementKeys.filter((key) => !prevElementKeySet.has(key));
|
|
120
|
+
const disappearedKeys = prevElementKeys.filter((key) => !elementKeySet.has(key));
|
|
121
|
+
const stateSig = elementKeys.join(',');
|
|
122
|
+
const changed = stateSig !== prev.stateSig;
|
|
123
|
+
const presenceVersion = prev.presenceVersion + (exists && !prev.exists ? 1 : 0);
|
|
124
|
+
const next = {
|
|
125
|
+
exists,
|
|
126
|
+
stateSig,
|
|
127
|
+
appearCount: prev.appearCount + (exists && !prev.exists ? 1 : 0),
|
|
128
|
+
presenceVersion,
|
|
129
|
+
elementKeys,
|
|
130
|
+
};
|
|
131
|
+
state.set(item.id, next);
|
|
132
|
+
|
|
133
|
+
const shouldEmit = (type) => !item.events || item.events.has(type);
|
|
134
|
+
if (exists && !prev.exists && shouldEmit('appear')) {
|
|
135
|
+
await emit({
|
|
136
|
+
type: 'appear',
|
|
137
|
+
profileId: resolvedProfile,
|
|
138
|
+
subscriptionId: item.id,
|
|
139
|
+
selector: item.selector,
|
|
140
|
+
count: elements.length,
|
|
141
|
+
elements,
|
|
142
|
+
pageUrl: currentUrl,
|
|
143
|
+
filterMode: effectiveFilterMode,
|
|
144
|
+
elementKeys,
|
|
145
|
+
presenceVersion,
|
|
146
|
+
stateKey: stateSig,
|
|
147
|
+
eventKey: buildEventKey(item.id, 'appear', presenceVersion, appearedKeys.length > 0 ? appearedKeys : elementKeys),
|
|
148
|
+
timestamp: ts,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (!exists && prev.exists && shouldEmit('disappear')) {
|
|
152
|
+
await emit({
|
|
153
|
+
type: 'disappear',
|
|
154
|
+
profileId: resolvedProfile,
|
|
155
|
+
subscriptionId: item.id,
|
|
156
|
+
selector: item.selector,
|
|
157
|
+
count: 0,
|
|
158
|
+
elements: [],
|
|
159
|
+
pageUrl: currentUrl,
|
|
160
|
+
filterMode: effectiveFilterMode,
|
|
161
|
+
elementKeys: [],
|
|
162
|
+
departedElementKeys: disappearedKeys,
|
|
163
|
+
presenceVersion: prev.presenceVersion,
|
|
164
|
+
stateKey: '',
|
|
165
|
+
eventKey: buildEventKey(item.id, 'disappear', prev.presenceVersion, disappearedKeys),
|
|
166
|
+
timestamp: ts,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (exists && shouldEmit('exist')) {
|
|
170
|
+
await emit({
|
|
171
|
+
type: 'exist',
|
|
172
|
+
profileId: resolvedProfile,
|
|
173
|
+
subscriptionId: item.id,
|
|
174
|
+
selector: item.selector,
|
|
175
|
+
count: elements.length,
|
|
176
|
+
elements,
|
|
177
|
+
pageUrl: currentUrl,
|
|
178
|
+
filterMode: effectiveFilterMode,
|
|
179
|
+
elementKeys,
|
|
180
|
+
presenceVersion,
|
|
181
|
+
stateKey: stateSig,
|
|
182
|
+
eventKey: buildEventKey(item.id, 'exist', presenceVersion, elementKeys),
|
|
183
|
+
timestamp: ts,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (changed && shouldEmit('change')) {
|
|
187
|
+
await emit({
|
|
188
|
+
type: 'change',
|
|
189
|
+
profileId: resolvedProfile,
|
|
190
|
+
subscriptionId: item.id,
|
|
191
|
+
selector: item.selector,
|
|
192
|
+
count: elements.length,
|
|
193
|
+
elements,
|
|
194
|
+
pageUrl: currentUrl,
|
|
195
|
+
filterMode: effectiveFilterMode,
|
|
196
|
+
elementKeys,
|
|
197
|
+
appearedElementKeys: appearedKeys,
|
|
198
|
+
departedElementKeys: disappearedKeys,
|
|
199
|
+
presenceVersion,
|
|
200
|
+
stateKey: stateSig,
|
|
201
|
+
eventKey: buildEventKey(item.id, 'change', presenceVersion, elementKeys),
|
|
202
|
+
timestamp: ts,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
await emit({ type: 'tick', profileId: resolvedProfile, timestamp: ts });
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (isTransientSubscriptionError(err)) return;
|
|
209
|
+
onError(err);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const interval = setInterval(poll, intervalMs);
|
|
214
|
+
await poll();
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
stop: () => {
|
|
218
|
+
if (stopped) return;
|
|
219
|
+
stopped = true;
|
|
220
|
+
clearInterval(interval);
|
|
221
|
+
},
|
|
222
|
+
profileId: resolvedProfile,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
import { callAPI, getSessionByProfile } from '../../utils/browser-service.mjs';
|
|
2
|
-
import { getDefaultProfile } from '../../utils/config.mjs';
|
|
3
|
-
import { ChangeNotifier } from '../change-notifier.mjs';
|
|
4
|
-
import { getRegisteredTargets } from '../subscription-registry.mjs';
|
|
5
|
-
|
|
6
|
-
export function asErrorPayload(code, message, data = {}) {
|
|
7
|
-
return { ok: false, code, message, data };
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function normalizeArray(value) {
|
|
11
|
-
return Array.isArray(value) ? value : [];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function extractPageList(payload) {
|
|
15
|
-
const pages = normalizeArray(payload?.pages || payload?.data?.pages)
|
|
16
|
-
.map((item, idx) => {
|
|
17
|
-
const rawIndex = Number(item?.index);
|
|
18
|
-
return {
|
|
19
|
-
...item,
|
|
20
|
-
index: Number.isFinite(rawIndex) ? rawIndex : idx,
|
|
21
|
-
};
|
|
22
|
-
})
|
|
23
|
-
.sort((a, b) => Number(a.index) - Number(b.index));
|
|
24
|
-
const activeIndex = Number(payload?.activeIndex ?? payload?.data?.activeIndex);
|
|
25
|
-
return {
|
|
26
|
-
pages,
|
|
27
|
-
activeIndex: Number.isFinite(activeIndex) ? activeIndex : null,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function getCurrentUrl(profileId) {
|
|
32
|
-
const res = await callAPI('evaluate', {
|
|
33
|
-
profileId,
|
|
34
|
-
script: 'window.location.href',
|
|
35
|
-
});
|
|
36
|
-
return String(res?.result || res?.data?.result || '');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function firstSelectorForContainer(profileId, containerId) {
|
|
40
|
-
const targets = getRegisteredTargets(profileId)?.profile;
|
|
41
|
-
const selectors = normalizeArray(targets?.selectors);
|
|
42
|
-
const direct = selectors.find((item) => item?.setId === containerId && typeof item?.css === 'string');
|
|
43
|
-
if (direct?.css) return direct.css;
|
|
44
|
-
|
|
45
|
-
const fallbackTarget = normalizeArray(targets?.targets).find(
|
|
46
|
-
(item) => item?.containerId === containerId && item?.markerType === 'url_dom' && item?.dom?.css,
|
|
47
|
-
);
|
|
48
|
-
if (fallbackTarget?.dom?.css) return fallbackTarget.dom.css;
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function maybeSelector({ profileId, containerId, selector }) {
|
|
53
|
-
if (selector && typeof selector === 'string') return selector;
|
|
54
|
-
if (containerId && typeof containerId === 'string') {
|
|
55
|
-
return firstSelectorForContainer(profileId, containerId);
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function buildSelectorCheck(snapshot, selector) {
|
|
61
|
-
if (!snapshot || !selector) return [];
|
|
62
|
-
const notifier = new ChangeNotifier();
|
|
63
|
-
if (typeof selector === 'string') {
|
|
64
|
-
return notifier.findElements(snapshot, { css: selector });
|
|
65
|
-
}
|
|
66
|
-
if (selector && typeof selector === 'object') {
|
|
67
|
-
return notifier.findElements(snapshot, selector);
|
|
68
|
-
}
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function isCheckpointRiskUrl(url) {
|
|
73
|
-
const lower = String(url || '').toLowerCase();
|
|
74
|
-
return (
|
|
75
|
-
lower.includes('/website-login/captcha')
|
|
76
|
-
|| lower.includes('verifyuuid=')
|
|
77
|
-
|| lower.includes('verifytype=')
|
|
78
|
-
|| lower.includes('verifybiz=')
|
|
79
|
-
|| lower.includes('/website-login/verify')
|
|
80
|
-
|| lower.includes('/website-login/security')
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function ensureActiveSession(profileId) {
|
|
85
|
-
const targetProfile = profileId || getDefaultProfile();
|
|
86
|
-
if (!targetProfile) {
|
|
87
|
-
throw new Error('profileId is required');
|
|
88
|
-
}
|
|
89
|
-
const session = await getSessionByProfile(targetProfile);
|
|
90
|
-
if (!session) {
|
|
91
|
-
throw new Error(`No active session for profile: ${targetProfile}`);
|
|
92
|
-
}
|
|
93
|
-
return session;
|
|
94
|
-
}
|
|
1
|
+
import { callAPI, getSessionByProfile } from '../../utils/browser-service.mjs';
|
|
2
|
+
import { getDefaultProfile } from '../../utils/config.mjs';
|
|
3
|
+
import { ChangeNotifier } from '../change-notifier.mjs';
|
|
4
|
+
import { getRegisteredTargets } from '../subscription-registry.mjs';
|
|
5
|
+
|
|
6
|
+
export function asErrorPayload(code, message, data = {}) {
|
|
7
|
+
return { ok: false, code, message, data };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizeArray(value) {
|
|
11
|
+
return Array.isArray(value) ? value : [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function extractPageList(payload) {
|
|
15
|
+
const pages = normalizeArray(payload?.pages || payload?.data?.pages)
|
|
16
|
+
.map((item, idx) => {
|
|
17
|
+
const rawIndex = Number(item?.index);
|
|
18
|
+
return {
|
|
19
|
+
...item,
|
|
20
|
+
index: Number.isFinite(rawIndex) ? rawIndex : idx,
|
|
21
|
+
};
|
|
22
|
+
})
|
|
23
|
+
.sort((a, b) => Number(a.index) - Number(b.index));
|
|
24
|
+
const activeIndex = Number(payload?.activeIndex ?? payload?.data?.activeIndex);
|
|
25
|
+
return {
|
|
26
|
+
pages,
|
|
27
|
+
activeIndex: Number.isFinite(activeIndex) ? activeIndex : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getCurrentUrl(profileId) {
|
|
32
|
+
const res = await callAPI('evaluate', {
|
|
33
|
+
profileId,
|
|
34
|
+
script: 'window.location.href',
|
|
35
|
+
});
|
|
36
|
+
return String(res?.result || res?.data?.result || '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function firstSelectorForContainer(profileId, containerId) {
|
|
40
|
+
const targets = getRegisteredTargets(profileId)?.profile;
|
|
41
|
+
const selectors = normalizeArray(targets?.selectors);
|
|
42
|
+
const direct = selectors.find((item) => item?.setId === containerId && typeof item?.css === 'string');
|
|
43
|
+
if (direct?.css) return direct.css;
|
|
44
|
+
|
|
45
|
+
const fallbackTarget = normalizeArray(targets?.targets).find(
|
|
46
|
+
(item) => item?.containerId === containerId && item?.markerType === 'url_dom' && item?.dom?.css,
|
|
47
|
+
);
|
|
48
|
+
if (fallbackTarget?.dom?.css) return fallbackTarget.dom.css;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function maybeSelector({ profileId, containerId, selector }) {
|
|
53
|
+
if (selector && typeof selector === 'string') return selector;
|
|
54
|
+
if (containerId && typeof containerId === 'string') {
|
|
55
|
+
return firstSelectorForContainer(profileId, containerId);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildSelectorCheck(snapshot, selector) {
|
|
61
|
+
if (!snapshot || !selector) return [];
|
|
62
|
+
const notifier = new ChangeNotifier();
|
|
63
|
+
if (typeof selector === 'string') {
|
|
64
|
+
return notifier.findElements(snapshot, { css: selector });
|
|
65
|
+
}
|
|
66
|
+
if (selector && typeof selector === 'object') {
|
|
67
|
+
return notifier.findElements(snapshot, selector);
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isCheckpointRiskUrl(url) {
|
|
73
|
+
const lower = String(url || '').toLowerCase();
|
|
74
|
+
return (
|
|
75
|
+
lower.includes('/website-login/captcha')
|
|
76
|
+
|| lower.includes('verifyuuid=')
|
|
77
|
+
|| lower.includes('verifytype=')
|
|
78
|
+
|| lower.includes('verifybiz=')
|
|
79
|
+
|| lower.includes('/website-login/verify')
|
|
80
|
+
|| lower.includes('/website-login/security')
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function ensureActiveSession(profileId) {
|
|
85
|
+
const targetProfile = profileId || getDefaultProfile();
|
|
86
|
+
if (!targetProfile) {
|
|
87
|
+
throw new Error('profileId is required');
|
|
88
|
+
}
|
|
89
|
+
const session = await getSessionByProfile(targetProfile);
|
|
90
|
+
if (!session) {
|
|
91
|
+
throw new Error(`No active session for profile: ${targetProfile}`);
|
|
92
|
+
}
|
|
93
|
+
return session;
|
|
94
|
+
}
|