@web-auto/camo 0.1.3 → 0.1.5

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.
Files changed (51) hide show
  1. package/README.md +142 -1
  2. package/package.json +2 -1
  3. package/scripts/check-file-size.mjs +80 -0
  4. package/scripts/file-size-policy.json +8 -0
  5. package/src/autoscript/action-providers/index.mjs +9 -0
  6. package/src/autoscript/action-providers/xhs/comments.mjs +412 -0
  7. package/src/autoscript/action-providers/xhs/common.mjs +77 -0
  8. package/src/autoscript/action-providers/xhs/detail.mjs +181 -0
  9. package/src/autoscript/action-providers/xhs/interaction.mjs +466 -0
  10. package/src/autoscript/action-providers/xhs/like-rules.mjs +57 -0
  11. package/src/autoscript/action-providers/xhs/persistence.mjs +167 -0
  12. package/src/autoscript/action-providers/xhs/search.mjs +174 -0
  13. package/src/autoscript/action-providers/xhs.mjs +133 -0
  14. package/src/autoscript/impact-engine.mjs +78 -0
  15. package/src/autoscript/runtime.mjs +1015 -0
  16. package/src/autoscript/schema.mjs +370 -0
  17. package/src/autoscript/xhs-unified-template.mjs +931 -0
  18. package/src/cli.mjs +185 -79
  19. package/src/commands/autoscript.mjs +1100 -0
  20. package/src/commands/browser.mjs +215 -5
  21. package/src/commands/container.mjs +298 -75
  22. package/src/commands/events.mjs +152 -0
  23. package/src/commands/lifecycle.mjs +17 -3
  24. package/src/commands/window.mjs +94 -2
  25. package/src/container/change-notifier.mjs +165 -24
  26. package/src/container/element-filter.mjs +51 -5
  27. package/src/container/runtime-core/checkpoint.mjs +195 -0
  28. package/src/container/runtime-core/index.mjs +21 -0
  29. package/src/container/runtime-core/operations/index.mjs +351 -0
  30. package/src/container/runtime-core/operations/selector-scripts.mjs +68 -0
  31. package/src/container/runtime-core/operations/tab-pool.mjs +544 -0
  32. package/src/container/runtime-core/operations/viewport.mjs +143 -0
  33. package/src/container/runtime-core/subscription.mjs +87 -0
  34. package/src/container/runtime-core/utils.mjs +94 -0
  35. package/src/container/runtime-core/validation.mjs +127 -0
  36. package/src/container/runtime-core.mjs +1 -0
  37. package/src/container/subscription-registry.mjs +459 -0
  38. package/src/core/actions.mjs +573 -0
  39. package/src/core/browser.mjs +270 -0
  40. package/src/core/index.mjs +53 -0
  41. package/src/core/utils.mjs +87 -0
  42. package/src/events/daemon-entry.mjs +33 -0
  43. package/src/events/daemon.mjs +80 -0
  44. package/src/events/progress-log.mjs +109 -0
  45. package/src/events/ws-server.mjs +239 -0
  46. package/src/lib/client.mjs +8 -5
  47. package/src/lifecycle/session-registry.mjs +8 -4
  48. package/src/lifecycle/session-watchdog.mjs +267 -0
  49. package/src/utils/browser-service.mjs +232 -9
  50. package/src/utils/config.mjs +58 -2
  51. package/src/utils/help.mjs +28 -4
@@ -1,9 +1,144 @@
1
1
  import fs from 'node:fs';
2
- import { listProfiles, getDefaultProfile } from '../utils/config.mjs';
2
+ import {
3
+ listProfiles,
4
+ getDefaultProfile,
5
+ getProfileWindowSize,
6
+ setProfileWindowSize,
7
+ } from '../utils/config.mjs';
3
8
  import { callAPI, ensureCamoufox, ensureBrowserService, getSessionByProfile, checkBrowserService } from '../utils/browser-service.mjs';
4
9
  import { resolveProfileId, ensureUrlScheme, looksLikeUrlToken, getPositionals } from '../utils/args.mjs';
5
10
  import { acquireLock, releaseLock, isLocked, getLockInfo, cleanupStaleLocks } from '../lifecycle/lock.mjs';
6
11
  import { registerSession, updateSession, getSessionInfo, unregisterSession, listRegisteredSessions, markSessionClosed, cleanupStaleSessions, recoverSession } from '../lifecycle/session-registry.mjs';
12
+ import { startSessionWatchdog, stopAllSessionWatchdogs, stopSessionWatchdog } from '../lifecycle/session-watchdog.mjs';
13
+
14
+ const START_WINDOW_MIN_WIDTH = 960;
15
+ const START_WINDOW_MIN_HEIGHT = 700;
16
+ const START_WINDOW_MAX_RESERVE = 240;
17
+ const START_WINDOW_DEFAULT_RESERVE = 72;
18
+
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+
23
+ function parseNumber(value, fallback = 0) {
24
+ const parsed = Number(value);
25
+ return Number.isFinite(parsed) ? parsed : fallback;
26
+ }
27
+
28
+ function clamp(value, min, max) {
29
+ return Math.min(Math.max(value, min), max);
30
+ }
31
+
32
+ export function computeTargetViewportFromWindowMetrics(measured) {
33
+ const innerWidth = Math.max(320, parseNumber(measured?.innerWidth, 0));
34
+ const innerHeight = Math.max(240, parseNumber(measured?.innerHeight, 0));
35
+ const outerWidth = Math.max(320, parseNumber(measured?.outerWidth, innerWidth));
36
+ const outerHeight = Math.max(240, parseNumber(measured?.outerHeight, innerHeight));
37
+
38
+ const rawDeltaW = Math.max(0, outerWidth - innerWidth);
39
+ const rawDeltaH = Math.max(0, outerHeight - innerHeight);
40
+ const frameW = rawDeltaW > 400 ? 16 : Math.min(rawDeltaW, 120);
41
+ const frameH = rawDeltaH > 400 ? 88 : Math.min(rawDeltaH, 180);
42
+
43
+ return {
44
+ width: Math.max(320, outerWidth - frameW),
45
+ height: Math.max(240, outerHeight - frameH),
46
+ frameW,
47
+ frameH,
48
+ innerWidth,
49
+ innerHeight,
50
+ outerWidth,
51
+ outerHeight,
52
+ };
53
+ }
54
+
55
+ export function computeStartWindowSize(metrics, options = {}) {
56
+ const display = metrics?.metrics || metrics || {};
57
+ const reserveFromEnv = parseNumber(process.env.CAMO_DEFAULT_WINDOW_VERTICAL_RESERVE, START_WINDOW_DEFAULT_RESERVE);
58
+ const reserve = clamp(
59
+ parseNumber(options.reservePx, reserveFromEnv),
60
+ 0,
61
+ START_WINDOW_MAX_RESERVE,
62
+ );
63
+
64
+ const workWidth = parseNumber(display.workWidth, 0);
65
+ const workHeight = parseNumber(display.workHeight, 0);
66
+ const width = parseNumber(display.width, 0);
67
+ const height = parseNumber(display.height, 0);
68
+ const baseW = Math.floor(workWidth > 0 ? workWidth : width);
69
+ const baseH = Math.floor(workHeight > 0 ? workHeight : height);
70
+
71
+ if (baseW <= 0 || baseH <= 0) {
72
+ return {
73
+ width: 1920,
74
+ height: 1000,
75
+ reservePx: reserve,
76
+ source: 'fallback',
77
+ };
78
+ }
79
+
80
+ return {
81
+ width: Math.max(START_WINDOW_MIN_WIDTH, baseW),
82
+ height: Math.max(START_WINDOW_MIN_HEIGHT, baseH - reserve),
83
+ reservePx: reserve,
84
+ source: workWidth > 0 || workHeight > 0 ? 'workArea' : 'screen',
85
+ };
86
+ }
87
+
88
+ async function probeWindowMetrics(profileId) {
89
+ const measured = await callAPI('evaluate', {
90
+ profileId,
91
+ script: '({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight })',
92
+ });
93
+ return measured?.result || {};
94
+ }
95
+
96
+ export async function syncWindowViewportAfterResize(profileId, width, height, options = {}) {
97
+ const settleMs = Math.max(40, parseNumber(options.settleMs, 120));
98
+ const attempts = Math.max(1, Math.min(8, Math.floor(parseNumber(options.attempts, 4))));
99
+ const tolerancePx = Math.max(0, parseNumber(options.tolerancePx, 3));
100
+
101
+ const windowResult = await callAPI('window:resize', { profileId, width, height });
102
+ await sleep(settleMs);
103
+
104
+ let measured = {};
105
+ let verified = {};
106
+ let viewport = null;
107
+ let matched = false;
108
+ let target = { width: 1280, height: 720, frameW: 16, frameH: 88 };
109
+
110
+ for (let i = 0; i < attempts; i += 1) {
111
+ measured = await probeWindowMetrics(profileId);
112
+ target = computeTargetViewportFromWindowMetrics(measured);
113
+ viewport = await callAPI('page:setViewport', {
114
+ profileId,
115
+ width: target.width,
116
+ height: target.height,
117
+ });
118
+ await sleep(settleMs);
119
+ verified = await probeWindowMetrics(profileId);
120
+ const dw = Math.abs(parseNumber(verified?.innerWidth, 0) - target.width);
121
+ const dh = Math.abs(parseNumber(verified?.innerHeight, 0) - target.height);
122
+ if (dw <= tolerancePx && dh <= tolerancePx) {
123
+ matched = true;
124
+ break;
125
+ }
126
+ }
127
+
128
+ return {
129
+ window: windowResult,
130
+ measured,
131
+ verified,
132
+ targetViewport: {
133
+ width: target.width,
134
+ height: target.height,
135
+ frameW: target.frameW,
136
+ frameH: target.frameH,
137
+ matched,
138
+ },
139
+ viewport,
140
+ };
141
+ }
7
142
 
8
143
  export async function handleStartCommand(args) {
9
144
  ensureCamoufox();
@@ -13,6 +148,19 @@ export async function handleStartCommand(args) {
13
148
 
14
149
  const urlIdx = args.indexOf('--url');
15
150
  const explicitUrl = urlIdx >= 0 ? args[urlIdx + 1] : undefined;
151
+ const widthIdx = args.indexOf('--width');
152
+ const heightIdx = args.indexOf('--height');
153
+ const explicitWidth = widthIdx >= 0 ? parseNumber(args[widthIdx + 1], NaN) : NaN;
154
+ const explicitHeight = heightIdx >= 0 ? parseNumber(args[heightIdx + 1], NaN) : NaN;
155
+ const hasExplicitWidth = Number.isFinite(explicitWidth);
156
+ const hasExplicitHeight = Number.isFinite(explicitHeight);
157
+ if (hasExplicitWidth !== hasExplicitHeight) {
158
+ throw new Error('Usage: camo start [profileId] [--url <url>] [--headless] [--width <w> --height <h>]');
159
+ }
160
+ if ((hasExplicitWidth && explicitWidth < START_WINDOW_MIN_WIDTH) || (hasExplicitHeight && explicitHeight < START_WINDOW_MIN_HEIGHT)) {
161
+ throw new Error(`Window size too small. Minimum is ${START_WINDOW_MIN_WIDTH}x${START_WINDOW_MIN_HEIGHT}`);
162
+ }
163
+ const hasExplicitWindowSize = hasExplicitWidth && hasExplicitHeight;
16
164
  const profileSet = new Set(listProfiles());
17
165
  let implicitUrl;
18
166
 
@@ -20,6 +168,7 @@ export async function handleStartCommand(args) {
20
168
  for (let i = 1; i < args.length; i++) {
21
169
  const arg = args[i];
22
170
  if (arg === '--url') { i++; continue; }
171
+ if (arg === '--width' || arg === '--height') { i++; continue; }
23
172
  if (arg === '--headless') continue;
24
173
  if (arg.startsWith('--')) continue;
25
174
 
@@ -56,6 +205,7 @@ export async function handleStartCommand(args) {
56
205
  message: 'Session already running',
57
206
  url: existing.current_url,
58
207
  }, null, 2));
208
+ startSessionWatchdog(profileId);
59
209
  return;
60
210
  }
61
211
 
@@ -84,6 +234,53 @@ export async function handleStartCommand(args) {
84
234
  url: targetUrl,
85
235
  headless,
86
236
  });
237
+ startSessionWatchdog(profileId);
238
+
239
+ if (!headless) {
240
+ let windowTarget = null;
241
+ if (hasExplicitWindowSize) {
242
+ windowTarget = {
243
+ width: Math.floor(explicitWidth),
244
+ height: Math.floor(explicitHeight),
245
+ source: 'explicit',
246
+ };
247
+ } else {
248
+ const rememberedWindow = getProfileWindowSize(profileId);
249
+ if (rememberedWindow) {
250
+ windowTarget = {
251
+ width: rememberedWindow.width,
252
+ height: rememberedWindow.height,
253
+ source: 'profile',
254
+ updatedAt: rememberedWindow.updatedAt,
255
+ };
256
+ } else {
257
+ const display = await callAPI('system:display', {}).catch(() => null);
258
+ windowTarget = computeStartWindowSize(display);
259
+ }
260
+ }
261
+
262
+ result.startWindow = {
263
+ width: windowTarget.width,
264
+ height: windowTarget.height,
265
+ source: windowTarget.source,
266
+ };
267
+
268
+ const syncResult = await syncWindowViewportAfterResize(
269
+ profileId,
270
+ windowTarget.width,
271
+ windowTarget.height,
272
+ ).catch((err) => ({ error: err?.message || String(err) }));
273
+ result.windowSync = syncResult;
274
+
275
+ const measuredOuterWidth = Number(syncResult?.verified?.outerWidth);
276
+ const measuredOuterHeight = Number(syncResult?.verified?.outerHeight);
277
+ const savedWindow = setProfileWindowSize(
278
+ profileId,
279
+ Number.isFinite(measuredOuterWidth) ? measuredOuterWidth : windowTarget.width,
280
+ Number.isFinite(measuredOuterHeight) ? measuredOuterHeight : windowTarget.height,
281
+ );
282
+ result.profileWindow = savedWindow?.window || null;
283
+ }
87
284
  }
88
285
  console.log(JSON.stringify(result, null, 2));
89
286
  }
@@ -92,10 +289,20 @@ export async function handleStopCommand(args) {
92
289
  await ensureBrowserService();
93
290
  const profileId = resolveProfileId(args, 1, getDefaultProfile);
94
291
  if (!profileId) throw new Error('Usage: camo stop [profileId]');
95
-
96
- const result = await callAPI('stop', { profileId });
97
- releaseLock(profileId);
98
- markSessionClosed(profileId);
292
+
293
+ let result = null;
294
+ let stopError = null;
295
+ try {
296
+ result = await callAPI('stop', { profileId });
297
+ } catch (err) {
298
+ stopError = err;
299
+ } finally {
300
+ stopSessionWatchdog(profileId);
301
+ releaseLock(profileId);
302
+ markSessionClosed(profileId);
303
+ }
304
+
305
+ if (stopError) throw stopError;
99
306
  console.log(JSON.stringify(result, null, 2));
100
307
  }
101
308
 
@@ -405,6 +612,7 @@ export async function handleShutdownCommand() {
405
612
  } catch {
406
613
  // Best effort cleanup
407
614
  }
615
+ stopSessionWatchdog(session.profileId);
408
616
  releaseLock(session.profileId);
409
617
  markSessionClosed(session.profileId);
410
618
  }
@@ -413,10 +621,12 @@ export async function handleShutdownCommand() {
413
621
  const registered = listRegisteredSessions();
414
622
  for (const reg of registered) {
415
623
  if (reg.status !== 'closed') {
624
+ stopSessionWatchdog(reg.profileId);
416
625
  markSessionClosed(reg.profileId);
417
626
  releaseLock(reg.profileId);
418
627
  }
419
628
  }
629
+ stopAllSessionWatchdogs();
420
630
 
421
631
  const result = await callAPI('service:shutdown', {});
422
632
  console.log(JSON.stringify(result, null, 2));