@web-auto/camo 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/package.json +7 -3
- package/scripts/check-file-size.mjs +80 -0
- package/scripts/file-size-policy.json +8 -0
- package/src/autoscript/action-providers/index.mjs +9 -0
- package/src/autoscript/action-providers/xhs/comments.mjs +412 -0
- package/src/autoscript/action-providers/xhs/common.mjs +77 -0
- package/src/autoscript/action-providers/xhs/detail.mjs +181 -0
- package/src/autoscript/action-providers/xhs/interaction.mjs +466 -0
- package/src/autoscript/action-providers/xhs/like-rules.mjs +57 -0
- package/src/autoscript/action-providers/xhs/persistence.mjs +167 -0
- package/src/autoscript/action-providers/xhs/search.mjs +174 -0
- package/src/autoscript/action-providers/xhs.mjs +133 -0
- package/src/autoscript/impact-engine.mjs +78 -0
- package/src/autoscript/runtime.mjs +1015 -0
- package/src/autoscript/schema.mjs +370 -0
- package/src/autoscript/xhs-unified-template.mjs +931 -0
- package/src/cli.mjs +190 -78
- package/src/commands/autoscript.mjs +1100 -0
- package/src/commands/browser.mjs +20 -4
- package/src/commands/container.mjs +401 -0
- package/src/commands/events.mjs +152 -0
- package/src/commands/lifecycle.mjs +17 -3
- package/src/commands/window.mjs +32 -1
- package/src/container/change-notifier.mjs +311 -0
- package/src/container/element-filter.mjs +143 -0
- package/src/container/index.mjs +3 -0
- package/src/container/runtime-core/checkpoint.mjs +195 -0
- package/src/container/runtime-core/index.mjs +21 -0
- package/src/container/runtime-core/operations/index.mjs +351 -0
- package/src/container/runtime-core/operations/selector-scripts.mjs +68 -0
- package/src/container/runtime-core/operations/tab-pool.mjs +544 -0
- package/src/container/runtime-core/operations/viewport.mjs +143 -0
- package/src/container/runtime-core/subscription.mjs +87 -0
- package/src/container/runtime-core/utils.mjs +94 -0
- package/src/container/runtime-core/validation.mjs +127 -0
- package/src/container/runtime-core.mjs +1 -0
- package/src/container/subscription-registry.mjs +459 -0
- package/src/core/actions.mjs +573 -0
- package/src/core/browser.mjs +270 -0
- package/src/core/index.mjs +53 -0
- package/src/core/utils.mjs +87 -0
- package/src/events/daemon-entry.mjs +33 -0
- package/src/events/daemon.mjs +80 -0
- package/src/events/progress-log.mjs +109 -0
- package/src/events/ws-server.mjs +239 -0
- package/src/lib/client.mjs +200 -0
- package/src/lifecycle/session-registry.mjs +8 -4
- package/src/lifecycle/session-watchdog.mjs +220 -0
- package/src/utils/browser-service.mjs +232 -9
- package/src/utils/help.mjs +28 -0
package/src/commands/browser.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, che
|
|
|
4
4
|
import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
|
|
5
5
|
import { acquireLock, releaseLock, isLocked, getLockInfo, cleanupStaleLocks } from '../lifecycle/lock.mjs';
|
|
6
6
|
import { registerSession, updateSession, getSessionInfo, unregisterSession, listRegisteredSessions, markSessionClosed, cleanupStaleSessions, recoverSession } from '../lifecycle/session-registry.mjs';
|
|
7
|
+
import { startSessionWatchdog, stopAllSessionWatchdogs, stopSessionWatchdog } from '../lifecycle/session-watchdog.mjs';
|
|
7
8
|
|
|
8
9
|
export async function handleStartCommand(args) {
|
|
9
10
|
ensureCamoufox();
|
|
@@ -56,6 +57,7 @@ export async function handleStartCommand(args) {
|
|
|
56
57
|
message: 'Session already running',
|
|
57
58
|
url: existing.current_url,
|
|
58
59
|
}, null, 2));
|
|
60
|
+
startSessionWatchdog(profileId);
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -84,6 +86,7 @@ export async function handleStartCommand(args) {
|
|
|
84
86
|
url: targetUrl,
|
|
85
87
|
headless,
|
|
86
88
|
});
|
|
89
|
+
startSessionWatchdog(profileId);
|
|
87
90
|
}
|
|
88
91
|
console.log(JSON.stringify(result, null, 2));
|
|
89
92
|
}
|
|
@@ -92,10 +95,20 @@ export async function handleStopCommand(args) {
|
|
|
92
95
|
await ensureBrowserService();
|
|
93
96
|
const profileId = resolveProfileId(args, 1, getDefaultProfile);
|
|
94
97
|
if (!profileId) throw new Error('Usage: camo stop [profileId]');
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
|
|
99
|
+
let result = null;
|
|
100
|
+
let stopError = null;
|
|
101
|
+
try {
|
|
102
|
+
result = await callAPI('stop', { profileId });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
stopError = err;
|
|
105
|
+
} finally {
|
|
106
|
+
stopSessionWatchdog(profileId);
|
|
107
|
+
releaseLock(profileId);
|
|
108
|
+
markSessionClosed(profileId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (stopError) throw stopError;
|
|
99
112
|
console.log(JSON.stringify(result, null, 2));
|
|
100
113
|
}
|
|
101
114
|
|
|
@@ -405,6 +418,7 @@ export async function handleShutdownCommand() {
|
|
|
405
418
|
} catch {
|
|
406
419
|
// Best effort cleanup
|
|
407
420
|
}
|
|
421
|
+
stopSessionWatchdog(session.profileId);
|
|
408
422
|
releaseLock(session.profileId);
|
|
409
423
|
markSessionClosed(session.profileId);
|
|
410
424
|
}
|
|
@@ -413,10 +427,12 @@ export async function handleShutdownCommand() {
|
|
|
413
427
|
const registered = listRegisteredSessions();
|
|
414
428
|
for (const reg of registered) {
|
|
415
429
|
if (reg.status !== 'closed') {
|
|
430
|
+
stopSessionWatchdog(reg.profileId);
|
|
416
431
|
markSessionClosed(reg.profileId);
|
|
417
432
|
releaseLock(reg.profileId);
|
|
418
433
|
}
|
|
419
434
|
}
|
|
435
|
+
stopAllSessionWatchdogs();
|
|
420
436
|
|
|
421
437
|
const result = await callAPI('service:shutdown', {});
|
|
422
438
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// Container commands - filter, watch, and subscription targets
|
|
2
|
+
import { getDomSnapshotByProfile, getSessionByProfile, getViewportByProfile } from '../utils/browser-service.mjs';
|
|
3
|
+
import { getDefaultProfile } from '../utils/config.mjs';
|
|
4
|
+
import { getChangeNotifier } from '../container/change-notifier.mjs';
|
|
5
|
+
import { createElementFilter } from '../container/element-filter.mjs';
|
|
6
|
+
import {
|
|
7
|
+
getRegisteredTargets,
|
|
8
|
+
initContainerSubscriptionDirectory,
|
|
9
|
+
listSubscriptionSets,
|
|
10
|
+
registerSubscriptionTargets,
|
|
11
|
+
} from '../container/subscription-registry.mjs';
|
|
12
|
+
import { safeAppendProgressEvent } from '../events/progress-log.mjs';
|
|
13
|
+
|
|
14
|
+
const notifier = getChangeNotifier();
|
|
15
|
+
const elementFilter = createElementFilter();
|
|
16
|
+
|
|
17
|
+
const VALUE_FLAGS = new Set([
|
|
18
|
+
'--profile',
|
|
19
|
+
'-p',
|
|
20
|
+
'--selector',
|
|
21
|
+
'-s',
|
|
22
|
+
'--throttle',
|
|
23
|
+
'-t',
|
|
24
|
+
'--source',
|
|
25
|
+
'--site',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function readFlagValue(args, names) {
|
|
29
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
30
|
+
if (!names.includes(args[i])) continue;
|
|
31
|
+
const value = args[i + 1];
|
|
32
|
+
if (!value || String(value).startsWith('-')) return null;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectPositionals(args, startIndex = 2) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let i = startIndex; i < args.length; i += 1) {
|
|
41
|
+
const arg = args[i];
|
|
42
|
+
if (!arg) continue;
|
|
43
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
44
|
+
i += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (String(arg).startsWith('-')) continue;
|
|
48
|
+
out.push(arg);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function ensureSession(profileId) {
|
|
54
|
+
const session = await getSessionByProfile(profileId);
|
|
55
|
+
if (!session) {
|
|
56
|
+
throw new Error(`No active session for profile: ${profileId || 'default'}`);
|
|
57
|
+
}
|
|
58
|
+
return session;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveWatchProfileAndSelectors(args) {
|
|
62
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
63
|
+
const explicitSelector = readFlagValue(args, ['--selector', '-s']);
|
|
64
|
+
const positionals = collectPositionals(args);
|
|
65
|
+
|
|
66
|
+
if (explicitSelector) {
|
|
67
|
+
const profileId = explicitProfile || positionals[0] || getDefaultProfile();
|
|
68
|
+
return { profileId, selectors: [explicitSelector], source: 'manual' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (explicitProfile) {
|
|
72
|
+
return {
|
|
73
|
+
profileId: explicitProfile,
|
|
74
|
+
selectors: positionals,
|
|
75
|
+
source: positionals.length > 0 ? 'manual' : 'subscription',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (positionals.length >= 2) {
|
|
80
|
+
return { profileId: positionals[0], selectors: positionals.slice(1), source: 'manual' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (positionals.length === 1) {
|
|
84
|
+
const candidateProfile = getRegisteredTargets(positionals[0])?.profile;
|
|
85
|
+
if (candidateProfile) {
|
|
86
|
+
return { profileId: positionals[0], selectors: [], source: 'subscription' };
|
|
87
|
+
}
|
|
88
|
+
return { profileId: getDefaultProfile(), selectors: [positionals[0]], source: 'manual' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { profileId: getDefaultProfile(), selectors: [], source: 'subscription' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveProfileAndSelectors(args) {
|
|
95
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
96
|
+
const positionals = collectPositionals(args);
|
|
97
|
+
if (explicitProfile) {
|
|
98
|
+
return { profileId: explicitProfile, selectors: positionals };
|
|
99
|
+
}
|
|
100
|
+
if (positionals.length >= 2) {
|
|
101
|
+
return { profileId: positionals[0], selectors: positionals.slice(1) };
|
|
102
|
+
}
|
|
103
|
+
return { profileId: getDefaultProfile(), selectors: positionals };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveListProfile(args) {
|
|
107
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
108
|
+
if (explicitProfile) return explicitProfile;
|
|
109
|
+
const positionals = collectPositionals(args);
|
|
110
|
+
return positionals[0] || getDefaultProfile();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function handleContainerInitCommand(args) {
|
|
114
|
+
const source = readFlagValue(args, ['--source']);
|
|
115
|
+
const force = args.includes('--force');
|
|
116
|
+
const result = initContainerSubscriptionDirectory({
|
|
117
|
+
...(source ? { containerLibraryRoot: source } : {}),
|
|
118
|
+
force,
|
|
119
|
+
});
|
|
120
|
+
console.log(JSON.stringify(result, null, 2));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function handleContainerSetsCommand(args) {
|
|
124
|
+
const site = readFlagValue(args, ['--site']);
|
|
125
|
+
const result = listSubscriptionSets({ ...(site ? { site } : {}) });
|
|
126
|
+
console.log(JSON.stringify(result, null, 2));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function handleContainerRegisterCommand(args) {
|
|
130
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
131
|
+
const append = args.includes('--append');
|
|
132
|
+
const positionals = collectPositionals(args);
|
|
133
|
+
|
|
134
|
+
let profileId;
|
|
135
|
+
let setIds;
|
|
136
|
+
if (explicitProfile) {
|
|
137
|
+
profileId = explicitProfile;
|
|
138
|
+
setIds = positionals;
|
|
139
|
+
} else if (positionals.length >= 2) {
|
|
140
|
+
profileId = positionals[0];
|
|
141
|
+
setIds = positionals.slice(1);
|
|
142
|
+
} else {
|
|
143
|
+
profileId = getDefaultProfile();
|
|
144
|
+
setIds = positionals;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!profileId) {
|
|
148
|
+
throw new Error('Usage: camo container register [profileId] <setId...> [--append] [--profile <id>]');
|
|
149
|
+
}
|
|
150
|
+
if (!Array.isArray(setIds) || setIds.length === 0) {
|
|
151
|
+
throw new Error('Usage: camo container register [profileId] <setId...> [--append] [--profile <id>]');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = registerSubscriptionTargets(profileId, setIds, { append });
|
|
155
|
+
console.log(JSON.stringify(result, null, 2));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function handleContainerTargetsCommand(args) {
|
|
159
|
+
const explicitProfile = readFlagValue(args, ['--profile', '-p']);
|
|
160
|
+
const positionals = collectPositionals(args);
|
|
161
|
+
const profileId = explicitProfile || positionals[0] || null;
|
|
162
|
+
const result = getRegisteredTargets(profileId);
|
|
163
|
+
console.log(JSON.stringify(result, null, 2));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function handleContainerFilterCommand(args) {
|
|
167
|
+
const { profileId, selectors } = resolveProfileAndSelectors(args);
|
|
168
|
+
if (!profileId) {
|
|
169
|
+
throw new Error('Usage: camo container filter [profileId] <selector...> [--profile <id>]');
|
|
170
|
+
}
|
|
171
|
+
if (selectors.length === 0) {
|
|
172
|
+
throw new Error('Usage: camo container filter [profileId] <selector...> [--profile <id>]');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const session = await ensureSession(profileId);
|
|
176
|
+
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
177
|
+
|
|
178
|
+
const matched = [];
|
|
179
|
+
for (const selector of selectors) {
|
|
180
|
+
const elements = notifier.findElements(snapshot, { css: selector });
|
|
181
|
+
matched.push(
|
|
182
|
+
...elements.map((element) => ({
|
|
183
|
+
selector,
|
|
184
|
+
path: element.path,
|
|
185
|
+
tag: element.tag,
|
|
186
|
+
id: element.id,
|
|
187
|
+
classes: element.classes,
|
|
188
|
+
text: (element.textSnippet || element.text || '').slice(0, 80),
|
|
189
|
+
})),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(JSON.stringify({ ok: true, profileId: session.profileId || profileId, count: matched.length, elements: matched }, null, 2));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function handleContainerWatchCommand(args) {
|
|
197
|
+
const watchRequest = resolveWatchProfileAndSelectors(args);
|
|
198
|
+
const profileId = watchRequest.profileId;
|
|
199
|
+
if (!profileId) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'Usage: camo container watch [profileId] [--selector <css>|<selector...>] [--throttle ms] [--profile <id>]',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let selectors = Array.from(
|
|
206
|
+
new Set(
|
|
207
|
+
(watchRequest.selectors || [])
|
|
208
|
+
.map((item) => String(item || '').trim())
|
|
209
|
+
.filter(Boolean),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
let source = watchRequest.source;
|
|
213
|
+
|
|
214
|
+
if (selectors.length === 0) {
|
|
215
|
+
const registered = getRegisteredTargets(profileId)?.profile;
|
|
216
|
+
selectors = Array.from(
|
|
217
|
+
new Set(
|
|
218
|
+
(registered?.selectors || [])
|
|
219
|
+
.map((item) => item?.css)
|
|
220
|
+
.filter((css) => typeof css === 'string' && css.trim())
|
|
221
|
+
.map((css) => css.trim()),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
source = 'subscription';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (selectors.length === 0) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`No selectors found for profile: ${profileId}. Use --selector <css> or run camo container register ${profileId} <setId...> first.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const session = await ensureSession(profileId);
|
|
234
|
+
const throttleRaw = readFlagValue(args, ['--throttle', '-t']);
|
|
235
|
+
const throttle = Math.max(100, Number(throttleRaw) || 500);
|
|
236
|
+
|
|
237
|
+
console.log(JSON.stringify({
|
|
238
|
+
ok: true,
|
|
239
|
+
message: `Watching ${selectors.length} selector(s) from ${source}`,
|
|
240
|
+
selectors,
|
|
241
|
+
profileId: session.profileId || profileId,
|
|
242
|
+
throttle,
|
|
243
|
+
}));
|
|
244
|
+
safeAppendProgressEvent({
|
|
245
|
+
source: 'container.watch',
|
|
246
|
+
mode: 'normal',
|
|
247
|
+
profileId: session.profileId || profileId,
|
|
248
|
+
event: 'container.watch.start',
|
|
249
|
+
payload: {
|
|
250
|
+
selectors,
|
|
251
|
+
throttle,
|
|
252
|
+
source,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const interval = setInterval(async () => {
|
|
257
|
+
try {
|
|
258
|
+
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
259
|
+
notifier.processSnapshot(snapshot);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
262
|
+
}
|
|
263
|
+
}, throttle);
|
|
264
|
+
|
|
265
|
+
const watchers = selectors.map((selector) => notifier.watch({ css: selector }, {
|
|
266
|
+
onAppear: (elements) => {
|
|
267
|
+
console.log(JSON.stringify({ event: 'appear', selector, count: elements.length, elements }));
|
|
268
|
+
safeAppendProgressEvent({
|
|
269
|
+
source: 'container.watch',
|
|
270
|
+
mode: 'normal',
|
|
271
|
+
profileId: session.profileId || profileId,
|
|
272
|
+
event: 'container.appear',
|
|
273
|
+
payload: { selector, count: elements.length },
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
onDisappear: (elements) => {
|
|
277
|
+
console.log(JSON.stringify({ event: 'disappear', selector, count: elements.length }));
|
|
278
|
+
safeAppendProgressEvent({
|
|
279
|
+
source: 'container.watch',
|
|
280
|
+
mode: 'normal',
|
|
281
|
+
profileId: session.profileId || profileId,
|
|
282
|
+
event: 'container.disappear',
|
|
283
|
+
payload: { selector, count: elements.length },
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
onChange: ({ appeared, disappeared }) => {
|
|
287
|
+
console.log(JSON.stringify({ event: 'change', selector, appeared: appeared.length, disappeared: disappeared.length }));
|
|
288
|
+
safeAppendProgressEvent({
|
|
289
|
+
source: 'container.watch',
|
|
290
|
+
mode: 'normal',
|
|
291
|
+
profileId: session.profileId || profileId,
|
|
292
|
+
event: 'container.change',
|
|
293
|
+
payload: { selector, appeared: appeared.length, disappeared: disappeared.length },
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
throttle,
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
process.once('SIGINT', () => {
|
|
300
|
+
clearInterval(interval);
|
|
301
|
+
for (const stopWatch of watchers) stopWatch();
|
|
302
|
+
notifier.destroy();
|
|
303
|
+
console.log(JSON.stringify({ ok: true, message: 'Watch stopped' }));
|
|
304
|
+
safeAppendProgressEvent({
|
|
305
|
+
source: 'container.watch',
|
|
306
|
+
mode: 'normal',
|
|
307
|
+
profileId: session.profileId || profileId,
|
|
308
|
+
event: 'container.watch.stop',
|
|
309
|
+
payload: { selectors },
|
|
310
|
+
});
|
|
311
|
+
process.exit(0);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function handleContainerListCommand(args) {
|
|
316
|
+
const profileId = resolveListProfile(args);
|
|
317
|
+
if (!profileId) {
|
|
318
|
+
throw new Error('Usage: camo container list [profileId] [--profile <id>]');
|
|
319
|
+
}
|
|
320
|
+
const session = await ensureSession(profileId);
|
|
321
|
+
|
|
322
|
+
const snapshot = await getDomSnapshotByProfile(session.profileId || profileId);
|
|
323
|
+
const viewport = await getViewportByProfile(session.profileId || profileId);
|
|
324
|
+
|
|
325
|
+
const collectElements = (node, domPath = 'root') => {
|
|
326
|
+
const elements = [];
|
|
327
|
+
if (!node) return elements;
|
|
328
|
+
|
|
329
|
+
const rect = node.rect || node.bbox;
|
|
330
|
+
if (rect && viewport) {
|
|
331
|
+
const inViewport = elementFilter.isInViewport(rect, viewport);
|
|
332
|
+
const visibilityRatio = elementFilter.getVisibilityRatio(rect, viewport);
|
|
333
|
+
if (inViewport && visibilityRatio > 0.1) {
|
|
334
|
+
elements.push({
|
|
335
|
+
path: domPath,
|
|
336
|
+
tag: node.tag,
|
|
337
|
+
id: node.id,
|
|
338
|
+
classes: node.classes?.slice(0, 3),
|
|
339
|
+
visibilityRatio: Math.round(visibilityRatio * 100) / 100,
|
|
340
|
+
rect: { x: rect.left || rect.x, y: rect.top || rect.y, w: rect.width, h: rect.height },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (Array.isArray(node.children)) {
|
|
346
|
+
for (let i = 0; i < node.children.length; i += 1) {
|
|
347
|
+
elements.push(...collectElements(node.children[i], `${domPath}/${i}`));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return elements;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const elements = collectElements(snapshot);
|
|
355
|
+
console.log(JSON.stringify({
|
|
356
|
+
ok: true,
|
|
357
|
+
profileId: session.profileId || profileId,
|
|
358
|
+
viewport,
|
|
359
|
+
count: elements.length,
|
|
360
|
+
elements: elements.slice(0, 50),
|
|
361
|
+
}, null, 2));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function handleContainerCommand(args) {
|
|
365
|
+
const sub = args[1];
|
|
366
|
+
|
|
367
|
+
switch (sub) {
|
|
368
|
+
case 'init':
|
|
369
|
+
return handleContainerInitCommand(args);
|
|
370
|
+
case 'sets':
|
|
371
|
+
return handleContainerSetsCommand(args);
|
|
372
|
+
case 'register':
|
|
373
|
+
return handleContainerRegisterCommand(args);
|
|
374
|
+
case 'targets':
|
|
375
|
+
return handleContainerTargetsCommand(args);
|
|
376
|
+
case 'filter':
|
|
377
|
+
return handleContainerFilterCommand(args);
|
|
378
|
+
case 'watch':
|
|
379
|
+
return handleContainerWatchCommand(args);
|
|
380
|
+
case 'list':
|
|
381
|
+
return handleContainerListCommand(args);
|
|
382
|
+
default:
|
|
383
|
+
console.log(`Usage: camo container <init|sets|register|targets|filter|watch|list> [options]
|
|
384
|
+
|
|
385
|
+
Commands:
|
|
386
|
+
init [--source <container-library-dir>] [--force] Initialize subscription directory and migrate container sets
|
|
387
|
+
sets [--site <siteKey>] List migrated subscription sets
|
|
388
|
+
register [profileId] <setId...> [--append] Register subscription targets for profile
|
|
389
|
+
targets [profileId] Show registered targets
|
|
390
|
+
filter [profileId] <selector...> Filter DOM elements by CSS selector
|
|
391
|
+
watch [profileId] [--selector <css>] [--throttle <ms>] Watch for element changes (defaults to registered selectors)
|
|
392
|
+
list [profileId] List visible elements in viewport
|
|
393
|
+
|
|
394
|
+
Options:
|
|
395
|
+
--profile, -p <id> Profile to use
|
|
396
|
+
--selector, -s <css> Selector for watch
|
|
397
|
+
--throttle, -t <ms> Poll interval for watch (default: 500)
|
|
398
|
+
--site <siteKey> Filter set list by site
|
|
399
|
+
`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createProgressWsServer } from '../events/ws-server.mjs';
|
|
2
|
+
import { getProgressEventsFile, readRecentProgressEvents, safeAppendProgressEvent } from '../events/progress-log.mjs';
|
|
3
|
+
import { ensureProgressEventDaemon } from '../events/daemon.mjs';
|
|
4
|
+
|
|
5
|
+
function readFlagValue(args, names) {
|
|
6
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
7
|
+
if (!names.includes(args[i])) continue;
|
|
8
|
+
const value = args[i + 1];
|
|
9
|
+
if (!value || String(value).startsWith('-')) return null;
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasFlag(args, name) {
|
|
16
|
+
return args.includes(name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildQuery(args) {
|
|
20
|
+
const profileId = readFlagValue(args, ['--profile', '-p']);
|
|
21
|
+
const runId = readFlagValue(args, ['--run-id']);
|
|
22
|
+
const mode = readFlagValue(args, ['--mode']);
|
|
23
|
+
const events = readFlagValue(args, ['--events']);
|
|
24
|
+
const replay = Math.max(0, Number(readFlagValue(args, ['--replay']) ?? 50) || 50);
|
|
25
|
+
const qs = new URLSearchParams();
|
|
26
|
+
if (profileId) qs.set('profileId', profileId);
|
|
27
|
+
if (runId) qs.set('runId', runId);
|
|
28
|
+
if (mode) qs.set('mode', mode);
|
|
29
|
+
if (events) qs.set('events', events);
|
|
30
|
+
qs.set('replay', String(replay));
|
|
31
|
+
return qs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function handleEventsServe(args) {
|
|
35
|
+
const host = readFlagValue(args, ['--host']) || '127.0.0.1';
|
|
36
|
+
const port = Math.max(1, Number(readFlagValue(args, ['--port']) || 7788) || 7788);
|
|
37
|
+
const pollMs = Math.max(80, Number(readFlagValue(args, ['--poll-ms']) || 220) || 220);
|
|
38
|
+
const fromStart = hasFlag(args, '--from-start');
|
|
39
|
+
|
|
40
|
+
const server = createProgressWsServer({ host, port, pollMs, fromStart });
|
|
41
|
+
const info = await server.start();
|
|
42
|
+
console.log(JSON.stringify({
|
|
43
|
+
ok: true,
|
|
44
|
+
command: 'events.serve',
|
|
45
|
+
...info,
|
|
46
|
+
message: 'Progress WS server started. Press Ctrl+C to stop.',
|
|
47
|
+
}, null, 2));
|
|
48
|
+
|
|
49
|
+
const stop = async (reason = 'signal_interrupt') => {
|
|
50
|
+
await server.stop();
|
|
51
|
+
console.log(JSON.stringify({ ok: true, event: 'events.serve.stop', reason }));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
};
|
|
54
|
+
process.once('SIGINT', () => {
|
|
55
|
+
stop('SIGINT');
|
|
56
|
+
});
|
|
57
|
+
process.once('SIGTERM', () => {
|
|
58
|
+
stop('SIGTERM');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await new Promise(() => {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleEventsTail(args) {
|
|
65
|
+
const host = readFlagValue(args, ['--host']) || '127.0.0.1';
|
|
66
|
+
const port = Math.max(1, Number(readFlagValue(args, ['--port']) || 7788) || 7788);
|
|
67
|
+
await ensureProgressEventDaemon({ host, port });
|
|
68
|
+
const qs = buildQuery(args);
|
|
69
|
+
const wsUrl = `ws://${host}:${port}/events?${qs.toString()}`;
|
|
70
|
+
if (typeof WebSocket !== 'function') {
|
|
71
|
+
throw new Error('Global WebSocket is unavailable in this Node runtime');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const socket = new WebSocket(wsUrl);
|
|
75
|
+
socket.addEventListener('open', () => {
|
|
76
|
+
console.log(JSON.stringify({ ok: true, command: 'events.tail', wsUrl }));
|
|
77
|
+
});
|
|
78
|
+
socket.addEventListener('message', (event) => {
|
|
79
|
+
const text = typeof event.data === 'string' ? event.data : String(event.data);
|
|
80
|
+
console.log(text);
|
|
81
|
+
});
|
|
82
|
+
socket.addEventListener('close', () => {
|
|
83
|
+
process.exit(0);
|
|
84
|
+
});
|
|
85
|
+
socket.addEventListener('error', (err) => {
|
|
86
|
+
console.error(JSON.stringify({ ok: false, command: 'events.tail', wsUrl, error: err?.message || String(err) }));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
|
89
|
+
process.once('SIGINT', () => {
|
|
90
|
+
socket.close();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await new Promise(() => {});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleEventsRecent(args) {
|
|
97
|
+
const limit = Math.max(1, Number(readFlagValue(args, ['--limit', '-n']) || 50) || 50);
|
|
98
|
+
const rows = readRecentProgressEvents(limit);
|
|
99
|
+
console.log(JSON.stringify({
|
|
100
|
+
ok: true,
|
|
101
|
+
command: 'events.recent',
|
|
102
|
+
file: getProgressEventsFile(),
|
|
103
|
+
count: rows.length,
|
|
104
|
+
events: rows,
|
|
105
|
+
}, null, 2));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleEventsEmit(args) {
|
|
109
|
+
const eventName = readFlagValue(args, ['--event']) || 'manual.emit';
|
|
110
|
+
const mode = readFlagValue(args, ['--mode']) || 'normal';
|
|
111
|
+
const profileId = readFlagValue(args, ['--profile', '-p']) || null;
|
|
112
|
+
const runId = readFlagValue(args, ['--run-id']) || null;
|
|
113
|
+
const payloadRaw = readFlagValue(args, ['--payload']) || '{}';
|
|
114
|
+
let payload = null;
|
|
115
|
+
try {
|
|
116
|
+
payload = JSON.parse(payloadRaw);
|
|
117
|
+
} catch {
|
|
118
|
+
payload = { raw: payloadRaw };
|
|
119
|
+
}
|
|
120
|
+
const appended = safeAppendProgressEvent({
|
|
121
|
+
source: 'events.emit',
|
|
122
|
+
mode,
|
|
123
|
+
profileId,
|
|
124
|
+
runId,
|
|
125
|
+
event: eventName,
|
|
126
|
+
payload,
|
|
127
|
+
});
|
|
128
|
+
console.log(JSON.stringify({ ok: Boolean(appended), command: 'events.emit', event: appended }, null, 2));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function handleEventsCommand(args) {
|
|
132
|
+
const sub = args[1];
|
|
133
|
+
switch (sub) {
|
|
134
|
+
case 'serve':
|
|
135
|
+
return handleEventsServe(args);
|
|
136
|
+
case 'tail':
|
|
137
|
+
return handleEventsTail(args);
|
|
138
|
+
case 'recent':
|
|
139
|
+
return handleEventsRecent(args);
|
|
140
|
+
case 'emit':
|
|
141
|
+
return handleEventsEmit(args);
|
|
142
|
+
default:
|
|
143
|
+
console.log(`Usage: camo events <serve|tail|recent|emit> [options]
|
|
144
|
+
|
|
145
|
+
Commands:
|
|
146
|
+
serve [--host 127.0.0.1] [--port 7788] [--poll-ms 220] [--from-start]
|
|
147
|
+
tail [--host 127.0.0.1] [--port 7788] [--profile <id>] [--run-id <id>] [--mode <normal|autoscript>] [--events e1,e2] [--replay 50]
|
|
148
|
+
recent [--limit 50]
|
|
149
|
+
emit --event <name> [--mode <normal|autoscript>] [--profile <id>] [--run-id <id>] [--payload '{"k":"v"}']
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getSessionInfo, unregisterSession, markSessionClosed, cleanupStaleSessions,
|
|
11
11
|
listRegisteredSessions, registerSession, updateSession
|
|
12
12
|
} from '../lifecycle/session-registry.mjs';
|
|
13
|
+
import { stopAllSessionWatchdogs, stopSessionWatchdog } from '../lifecycle/session-watchdog.mjs';
|
|
13
14
|
|
|
14
15
|
export async function handleCleanupCommand(args) {
|
|
15
16
|
const sub = args[1];
|
|
@@ -36,14 +37,21 @@ export async function handleCleanupCommand(args) {
|
|
|
36
37
|
const sessions = Array.isArray(status?.sessions) ? status.sessions : [];
|
|
37
38
|
|
|
38
39
|
for (const session of sessions) {
|
|
40
|
+
let stopError = null;
|
|
39
41
|
try {
|
|
40
42
|
await callAPI('stop', { profileId: session.profileId });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
stopError = err;
|
|
45
|
+
} finally {
|
|
46
|
+
stopSessionWatchdog(session.profileId);
|
|
41
47
|
releaseLock(session.profileId);
|
|
42
48
|
markSessionClosed(session.profileId);
|
|
43
|
-
results.push({ profileId: session.profileId, ok: true });
|
|
44
|
-
} catch (err) {
|
|
45
|
-
results.push({ profileId: session.profileId, ok: false, error: err.message });
|
|
46
49
|
}
|
|
50
|
+
results.push(
|
|
51
|
+
stopError
|
|
52
|
+
? { profileId: session.profileId, ok: false, error: stopError.message }
|
|
53
|
+
: { profileId: session.profileId, ok: true },
|
|
54
|
+
);
|
|
47
55
|
}
|
|
48
56
|
} catch {}
|
|
49
57
|
}
|
|
@@ -51,6 +59,7 @@ export async function handleCleanupCommand(args) {
|
|
|
51
59
|
// Cleanup stale locks and sessions
|
|
52
60
|
const cleanedLocks = cleanupStaleLocks();
|
|
53
61
|
const cleanedSessions = cleanupStaleSessions();
|
|
62
|
+
stopAllSessionWatchdogs();
|
|
54
63
|
|
|
55
64
|
console.log(JSON.stringify({
|
|
56
65
|
ok: true,
|
|
@@ -74,6 +83,7 @@ export async function handleCleanupCommand(args) {
|
|
|
74
83
|
} catch {}
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
stopSessionWatchdog(profileId);
|
|
77
87
|
releaseLock(profileId);
|
|
78
88
|
markSessionClosed(profileId);
|
|
79
89
|
console.log(JSON.stringify({ ok: true, profileId }, null, 2));
|
|
@@ -86,11 +96,13 @@ export async function handleForceStopCommand(args) {
|
|
|
86
96
|
|
|
87
97
|
try {
|
|
88
98
|
const result = await callAPI('stop', { profileId, force: true });
|
|
99
|
+
stopSessionWatchdog(profileId);
|
|
89
100
|
releaseLock(profileId);
|
|
90
101
|
markSessionClosed(profileId);
|
|
91
102
|
console.log(JSON.stringify({ ok: true, profileId, ...result }, null, 2));
|
|
92
103
|
} catch (err) {
|
|
93
104
|
// Even if stop fails, cleanup local state
|
|
105
|
+
stopSessionWatchdog(profileId);
|
|
94
106
|
releaseLock(profileId);
|
|
95
107
|
markSessionClosed(profileId);
|
|
96
108
|
console.log(JSON.stringify({ ok: true, profileId, warning: 'Session stopped locally but remote stop failed: ' + err.message }, null, 2));
|
|
@@ -204,6 +216,7 @@ export async function handleRecoverCommand(args) {
|
|
|
204
216
|
|
|
205
217
|
if (!serviceUp) {
|
|
206
218
|
// Service is down - session cannot be recovered, clean up
|
|
219
|
+
stopSessionWatchdog(profileId);
|
|
207
220
|
unregisterSession(profileId);
|
|
208
221
|
releaseLock(profileId);
|
|
209
222
|
console.log(JSON.stringify({
|
|
@@ -240,6 +253,7 @@ export async function handleRecoverCommand(args) {
|
|
|
240
253
|
}, null, 2));
|
|
241
254
|
} else {
|
|
242
255
|
// Session not in browser service - clean up
|
|
256
|
+
stopSessionWatchdog(profileId);
|
|
243
257
|
unregisterSession(profileId);
|
|
244
258
|
releaseLock(profileId);
|
|
245
259
|
console.log(JSON.stringify({
|