@web-auto/camo 0.1.3 → 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.
Files changed (50) hide show
  1. package/README.md +137 -0
  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 +20 -4
  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 +32 -1
  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 +220 -0
  49. package/src/utils/browser-service.mjs +232 -9
  50. package/src/utils/help.mjs +26 -3
@@ -0,0 +1,573 @@
1
+ /**
2
+ * Browser actions module - Playwright-based page operations
3
+ * No external browser-service dependency
4
+ */
5
+ import { getActiveBrowser, getCurrentPage, isBrowserRunning } from './browser.mjs';
6
+
7
+ /**
8
+ * Navigate to URL
9
+ */
10
+ export async function navigateTo(profileId, url) {
11
+ if (!isBrowserRunning(profileId)) {
12
+ throw new Error(`No browser running for profile: ${profileId}`);
13
+ }
14
+
15
+ const page = await getCurrentPage(profileId);
16
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
17
+
18
+ return {
19
+ ok: true,
20
+ profileId,
21
+ url: page.url(),
22
+ title: await page.title(),
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Go back
28
+ */
29
+ export async function goBack(profileId) {
30
+ if (!isBrowserRunning(profileId)) {
31
+ throw new Error(`No browser running for profile: ${profileId}`);
32
+ }
33
+
34
+ const page = await getCurrentPage(profileId);
35
+ await page.goBack({ waitUntil: 'domcontentloaded' });
36
+
37
+ return {
38
+ ok: true,
39
+ profileId,
40
+ url: page.url(),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Take screenshot
46
+ */
47
+ export async function takeScreenshot(profileId, options = {}) {
48
+ if (!isBrowserRunning(profileId)) {
49
+ throw new Error(`No browser running for profile: ${profileId}`);
50
+ }
51
+
52
+ const page = await getCurrentPage(profileId);
53
+ const buffer = await page.screenshot({
54
+ fullPage: options.fullPage || false,
55
+ type: 'png',
56
+ });
57
+
58
+ return {
59
+ ok: true,
60
+ profileId,
61
+ data: buffer.toString('base64'),
62
+ width: options.fullPage ? undefined : page.viewportSize()?.width,
63
+ height: options.fullPage ? undefined : page.viewportSize()?.height,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Scroll page
69
+ */
70
+ export async function scrollPage(profileId, options = {}) {
71
+ if (!isBrowserRunning(profileId)) {
72
+ throw new Error(`No browser running for profile: ${profileId}`);
73
+ }
74
+
75
+ const page = await getCurrentPage(profileId);
76
+ const direction = options.direction || 'down';
77
+ const amount = options.amount || 300;
78
+
79
+ const deltaX = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
80
+ const deltaY = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
81
+
82
+ await page.mouse.wheel(deltaX, deltaY);
83
+
84
+ return {
85
+ ok: true,
86
+ profileId,
87
+ direction,
88
+ amount,
89
+ scrolled: true,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Click element
95
+ */
96
+ export async function clickElement(profileId, selector, options = {}) {
97
+ if (!isBrowserRunning(profileId)) {
98
+ throw new Error(`No browser running for profile: ${profileId}`);
99
+ }
100
+
101
+ const page = await getCurrentPage(profileId);
102
+
103
+ // Wait for element to be visible
104
+ await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
105
+
106
+ // Scroll into view if needed
107
+ await page.locator(selector).scrollIntoViewIfNeeded();
108
+
109
+ // Click with system-level mouse
110
+ const element = await page.$(selector);
111
+ const box = await element.boundingBox();
112
+
113
+ if (!box) {
114
+ throw new Error(`Element not visible: ${selector}`);
115
+ }
116
+
117
+ const x = box.x + box.width / 2;
118
+ const y = box.y + box.height / 2;
119
+
120
+ await page.mouse.move(x, y);
121
+ await page.mouse.click(x, y, { button: options.button || 'left', clickCount: options.clickCount || 1 });
122
+
123
+ return {
124
+ ok: true,
125
+ profileId,
126
+ selector,
127
+ clicked: true,
128
+ position: { x, y },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Type text
134
+ */
135
+ export async function typeText(profileId, selector, text, options = {}) {
136
+ if (!isBrowserRunning(profileId)) {
137
+ throw new Error(`No browser running for profile: ${profileId}`);
138
+ }
139
+
140
+ const page = await getCurrentPage(profileId);
141
+
142
+ // Wait for element to be visible
143
+ await page.waitForSelector(selector, { state: 'visible', timeout: options.timeout || 10000 });
144
+
145
+ // Scroll into view if needed
146
+ await page.locator(selector).scrollIntoViewIfNeeded();
147
+
148
+ // Focus and type
149
+ const element = await page.$(selector);
150
+ await element.focus();
151
+ await element.fill('');
152
+
153
+ if (options.slowly) {
154
+ await element.type(text, { delay: 50 });
155
+ } else {
156
+ await element.fill(text);
157
+ }
158
+
159
+ if (options.pressEnter) {
160
+ await page.keyboard.press('Enter');
161
+ }
162
+
163
+ return {
164
+ ok: true,
165
+ profileId,
166
+ selector,
167
+ typed: text.length,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Press key
173
+ */
174
+ export async function pressKey(profileId, key) {
175
+ if (!isBrowserRunning(profileId)) {
176
+ throw new Error(`No browser running for profile: ${profileId}`);
177
+ }
178
+
179
+ const page = await getCurrentPage(profileId);
180
+ await page.keyboard.press(key);
181
+
182
+ return {
183
+ ok: true,
184
+ profileId,
185
+ key,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Highlight element
191
+ */
192
+ export async function highlightElement(profileId, selector, options = {}) {
193
+ if (!isBrowserRunning(profileId)) {
194
+ throw new Error(`No browser running for profile: ${profileId}`);
195
+ }
196
+
197
+ const page = await getCurrentPage(profileId);
198
+
199
+ const result = await page.evaluate((sel) => {
200
+ const el = document.querySelector(sel);
201
+ if (!el) return null;
202
+
203
+ const prev = el.style.outline;
204
+ el.style.outline = '3px solid #ff4444';
205
+
206
+ const rect = el.getBoundingClientRect();
207
+ return {
208
+ highlighted: true,
209
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
210
+ prevOutline: prev,
211
+ };
212
+ }, selector);
213
+
214
+ if (!result) {
215
+ throw new Error(`Element not found: ${selector}`);
216
+ }
217
+
218
+ // Auto-clear highlight after duration
219
+ const duration = options.duration || 2000;
220
+ setTimeout(async () => {
221
+ try {
222
+ const currentPage = await getCurrentPage(profileId);
223
+ await currentPage.evaluate((sel) => {
224
+ const el = document.querySelector(sel);
225
+ if (el) el.style.outline = '';
226
+ }, selector);
227
+ } catch {
228
+ // Browser may have been closed
229
+ }
230
+ }, duration);
231
+
232
+ return {
233
+ ok: true,
234
+ profileId,
235
+ selector,
236
+ ...result,
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Clear all highlights
242
+ */
243
+ export async function clearHighlights(profileId) {
244
+ if (!isBrowserRunning(profileId)) {
245
+ throw new Error(`No browser running for profile: ${profileId}`);
246
+ }
247
+
248
+ const page = await getCurrentPage(profileId);
249
+ await page.evaluate(() => {
250
+ document.querySelectorAll('[style*="outline"]').forEach((el) => {
251
+ el.style.outline = '';
252
+ });
253
+ });
254
+
255
+ return {
256
+ ok: true,
257
+ profileId,
258
+ cleared: true,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Set viewport size
264
+ */
265
+ export async function setViewport(profileId, width, height) {
266
+ if (!isBrowserRunning(profileId)) {
267
+ throw new Error(`No browser running for profile: ${profileId}`);
268
+ }
269
+
270
+ const page = await getCurrentPage(profileId);
271
+ await page.setViewportSize({ width, height });
272
+
273
+ return {
274
+ ok: true,
275
+ profileId,
276
+ viewport: { width, height },
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Get page info (URL, title, etc)
282
+ */
283
+ export async function getPageInfo(profileId) {
284
+ if (!isBrowserRunning(profileId)) {
285
+ throw new Error(`No browser running for profile: ${profileId}`);
286
+ }
287
+
288
+ const page = await getCurrentPage(profileId);
289
+
290
+ return {
291
+ ok: true,
292
+ profileId,
293
+ url: page.url(),
294
+ title: await page.title(),
295
+ viewport: page.viewportSize(),
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Get DOM snapshot
301
+ */
302
+ export async function getDOMSnapshot(profileId, options = {}) {
303
+ if (!isBrowserRunning(profileId)) {
304
+ throw new Error(`No browser running for profile: ${profileId}`);
305
+ }
306
+
307
+ const page = await getCurrentPage(profileId);
308
+
309
+ const snapshot = await page.evaluate((opts) => {
310
+ const collectNodes = (node, depth = 0, path = 'root') => {
311
+ if (depth > 10) return null;
312
+ if (!node) return null;
313
+
314
+ const result = {
315
+ tag: node.tagName?.toLowerCase() || node.nodeName?.toLowerCase(),
316
+ id: node.id || null,
317
+ classes: node.className ? node.className.split(' ').filter(Boolean) : [],
318
+ path,
319
+ };
320
+
321
+ const rect = node.getBoundingClientRect?.() || null;
322
+ if (rect) {
323
+ result.rect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
324
+ }
325
+
326
+ const text = node.textContent?.trim().slice(0, 50) || '';
327
+ if (text && node.children?.length === 0) {
328
+ result.text = text;
329
+ }
330
+
331
+ if (node.children && node.children.length > 0) {
332
+ result.children = [];
333
+ for (let i = 0; i < node.children.length; i++) {
334
+ const child = collectNodes(node.children[i], depth + 1, `${path}/${i}`);
335
+ if (child) result.children.push(child);
336
+ }
337
+ }
338
+
339
+ return result;
340
+ };
341
+
342
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
343
+ const root = collectNodes(document.body, 0, 'root');
344
+
345
+ return { viewport, dom_tree: root };
346
+ }, options);
347
+
348
+ return {
349
+ ok: true,
350
+ profileId,
351
+ ...snapshot,
352
+ };
353
+ }
354
+
355
+ /**
356
+ * Query elements
357
+ */
358
+ export async function queryElements(profileId, selector, options = {}) {
359
+ if (!isBrowserRunning(profileId)) {
360
+ throw new Error(`No browser running for profile: ${profileId}`);
361
+ }
362
+
363
+ const page = await getCurrentPage(profileId);
364
+
365
+ const elements = await page.evaluate((sel) => {
366
+ const nodes = document.querySelectorAll(sel);
367
+ return Array.from(nodes).map((el, idx) => {
368
+ const rect = el.getBoundingClientRect();
369
+ return {
370
+ index: idx,
371
+ tag: el.tagName.toLowerCase(),
372
+ id: el.id || null,
373
+ classes: el.className.split(' ').filter(Boolean),
374
+ text: el.textContent?.trim().slice(0, 100) || '',
375
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
376
+ visible: rect.width > 0 && rect.height > 0,
377
+ };
378
+ });
379
+ }, selector);
380
+
381
+ return {
382
+ ok: true,
383
+ profileId,
384
+ selector,
385
+ count: elements.length,
386
+ elements,
387
+ };
388
+ }
389
+
390
+ /**
391
+ * Execute JavaScript
392
+ */
393
+ export async function evaluateJS(profileId, script) {
394
+ if (!isBrowserRunning(profileId)) {
395
+ throw new Error(`No browser running for profile: ${profileId}`);
396
+ }
397
+
398
+ const page = await getCurrentPage(profileId);
399
+ const result = await page.evaluate(script);
400
+
401
+ return {
402
+ ok: true,
403
+ profileId,
404
+ result,
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Create new page
410
+ */
411
+ export async function createNewPage(profileId, options = {}) {
412
+ const browser = getActiveBrowser(profileId);
413
+ if (!browser) {
414
+ throw new Error(`No browser running for profile: ${profileId}`);
415
+ }
416
+
417
+ const context = browser.pwBrowser?.contexts()[0];
418
+ if (!context) {
419
+ throw new Error('No browser context available');
420
+ }
421
+
422
+ const page = await context.newPage();
423
+ if (options.url) {
424
+ await page.goto(options.url, { waitUntil: 'domcontentloaded' });
425
+ }
426
+
427
+ browser.pages.push(page);
428
+ browser.currentPage = page;
429
+
430
+ return {
431
+ ok: true,
432
+ profileId,
433
+ pageIndex: browser.pages.length - 1,
434
+ url: options.url || 'about:blank',
435
+ };
436
+ }
437
+
438
+ /**
439
+ * List pages
440
+ */
441
+ export async function listPages(profileId) {
442
+ const browser = getActiveBrowser(profileId);
443
+ if (!browser) {
444
+ throw new Error(`No browser running for profile: ${profileId}`);
445
+ }
446
+
447
+ const context = browser.pwBrowser?.contexts()[0];
448
+ if (!context) {
449
+ throw new Error('No browser context available');
450
+ }
451
+
452
+ const pages = context.pages();
453
+ const pageInfos = await Promise.all(pages.map(async (page, idx) => ({
454
+ index: idx,
455
+ url: page.url(),
456
+ title: await page.title().catch(() => ''),
457
+ })));
458
+
459
+ return {
460
+ ok: true,
461
+ profileId,
462
+ count: pageInfos.length,
463
+ pages: pageInfos,
464
+ currentPage: browser.currentPage?._guid || 0,
465
+ };
466
+ }
467
+
468
+ /**
469
+ * Switch page
470
+ */
471
+ export async function switchPage(profileId, pageIndex) {
472
+ const browser = getActiveBrowser(profileId);
473
+ if (!browser) {
474
+ throw new Error(`No browser running for profile: ${profileId}`);
475
+ }
476
+
477
+ const context = browser.pwBrowser?.contexts()[0];
478
+ if (!context) {
479
+ throw new Error('No browser context available');
480
+ }
481
+
482
+ const pages = context.pages();
483
+ if (pageIndex < 0 || pageIndex >= pages.length) {
484
+ throw new Error(`Invalid page index: ${pageIndex}`);
485
+ }
486
+
487
+ browser.currentPage = pages[pageIndex];
488
+ await pages[pageIndex].bringToFront();
489
+
490
+ return {
491
+ ok: true,
492
+ profileId,
493
+ pageIndex,
494
+ url: pages[pageIndex].url(),
495
+ };
496
+ }
497
+
498
+ /**
499
+ * Close page
500
+ */
501
+ export async function closePage(profileId, pageIndex) {
502
+ const browser = getActiveBrowser(profileId);
503
+ if (!browser) {
504
+ throw new Error(`No browser running for profile: ${profileId}`);
505
+ }
506
+
507
+ const context = browser.pwBrowser?.contexts()[0];
508
+ if (!context) {
509
+ throw new Error('No browser context available');
510
+ }
511
+
512
+ const pages = context.pages();
513
+ const idx = pageIndex !== undefined ? pageIndex : pages.length - 1;
514
+
515
+ if (idx < 0 || idx >= pages.length) {
516
+ throw new Error(`Invalid page index: ${idx}`);
517
+ }
518
+
519
+ await pages[idx].close();
520
+ browser.pages = pages.filter((_, i) => i !== idx);
521
+
522
+ // Update current page if needed
523
+ if (browser.currentPage === pages[idx]) {
524
+ browser.currentPage = browser.pages[0] || null;
525
+ }
526
+
527
+ return {
528
+ ok: true,
529
+ profileId,
530
+ closedIndex: idx,
531
+ remaining: browser.pages.length,
532
+ };
533
+ }
534
+
535
+ /**
536
+ * Mouse operations
537
+ */
538
+ export async function mouseMove(profileId, x, y, options = {}) {
539
+ if (!isBrowserRunning(profileId)) {
540
+ throw new Error(`No browser running for profile: ${profileId}`);
541
+ }
542
+
543
+ const page = await getCurrentPage(profileId);
544
+ await page.mouse.move(x, y, { steps: options.steps || 1 });
545
+
546
+ return { ok: true, profileId, x, y };
547
+ }
548
+
549
+ export async function mouseClick(profileId, x, y, options = {}) {
550
+ if (!isBrowserRunning(profileId)) {
551
+ throw new Error(`No browser running for profile: ${profileId}`);
552
+ }
553
+
554
+ const page = await getCurrentPage(profileId);
555
+ await page.mouse.click(x, y, {
556
+ button: options.button || 'left',
557
+ clickCount: options.clickCount || 1,
558
+ delay: options.delay || 0,
559
+ });
560
+
561
+ return { ok: true, profileId, x, y, button: options.button || 'left' };
562
+ }
563
+
564
+ export async function mouseWheel(profileId, deltaX, deltaY) {
565
+ if (!isBrowserRunning(profileId)) {
566
+ throw new Error(`No browser running for profile: ${profileId}`);
567
+ }
568
+
569
+ const page = await getCurrentPage(profileId);
570
+ await page.mouse.wheel(deltaX, deltaY);
571
+
572
+ return { ok: true, profileId, deltaX, deltaY };
573
+ }