@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.
Files changed (51) hide show
  1. package/README.md +137 -0
  2. package/package.json +7 -3
  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 +190 -78
  19. package/src/commands/autoscript.mjs +1100 -0
  20. package/src/commands/browser.mjs +20 -4
  21. package/src/commands/container.mjs +401 -0
  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 +311 -0
  26. package/src/container/element-filter.mjs +143 -0
  27. package/src/container/index.mjs +3 -0
  28. package/src/container/runtime-core/checkpoint.mjs +195 -0
  29. package/src/container/runtime-core/index.mjs +21 -0
  30. package/src/container/runtime-core/operations/index.mjs +351 -0
  31. package/src/container/runtime-core/operations/selector-scripts.mjs +68 -0
  32. package/src/container/runtime-core/operations/tab-pool.mjs +544 -0
  33. package/src/container/runtime-core/operations/viewport.mjs +143 -0
  34. package/src/container/runtime-core/subscription.mjs +87 -0
  35. package/src/container/runtime-core/utils.mjs +94 -0
  36. package/src/container/runtime-core/validation.mjs +127 -0
  37. package/src/container/runtime-core.mjs +1 -0
  38. package/src/container/subscription-registry.mjs +459 -0
  39. package/src/core/actions.mjs +573 -0
  40. package/src/core/browser.mjs +270 -0
  41. package/src/core/index.mjs +53 -0
  42. package/src/core/utils.mjs +87 -0
  43. package/src/events/daemon-entry.mjs +33 -0
  44. package/src/events/daemon.mjs +80 -0
  45. package/src/events/progress-log.mjs +109 -0
  46. package/src/events/ws-server.mjs +239 -0
  47. package/src/lib/client.mjs +200 -0
  48. package/src/lifecycle/session-registry.mjs +8 -4
  49. package/src/lifecycle/session-watchdog.mjs +220 -0
  50. package/src/utils/browser-service.mjs +232 -9
  51. package/src/utils/help.mjs +28 -0
@@ -0,0 +1,21 @@
1
+ export {
2
+ ensureActiveSession,
3
+ asErrorPayload,
4
+ normalizeArray,
5
+ extractPageList,
6
+ getCurrentUrl,
7
+ maybeSelector,
8
+ buildSelectorCheck,
9
+ isCheckpointRiskUrl,
10
+ } from './utils.mjs';
11
+
12
+ export {
13
+ XHS_CHECKPOINTS,
14
+ detectCheckpoint,
15
+ captureCheckpoint,
16
+ restoreCheckpoint,
17
+ } from './checkpoint.mjs';
18
+
19
+ export { validateOperation } from './validation.mjs';
20
+ export { executeOperation } from './operations/index.mjs';
21
+ export { watchSubscriptions } from './subscription.mjs';
@@ -0,0 +1,351 @@
1
+ import { callAPI, getDomSnapshotByProfile } from '../../../utils/browser-service.mjs';
2
+ import { executeTabPoolOperation } from './tab-pool.mjs';
3
+ import {
4
+ buildSelectorClickScript,
5
+ buildSelectorScrollIntoViewScript,
6
+ buildSelectorTypeScript,
7
+ } from './selector-scripts.mjs';
8
+ import { executeViewportOperation } from './viewport.mjs';
9
+ import {
10
+ asErrorPayload,
11
+ buildSelectorCheck,
12
+ ensureActiveSession,
13
+ extractPageList,
14
+ getCurrentUrl,
15
+ maybeSelector,
16
+ normalizeArray,
17
+ } from '../utils.mjs';
18
+
19
+ const TAB_ACTIONS = new Set([
20
+ 'ensure_tab_pool',
21
+ 'tab_pool_switch_next',
22
+ 'tab_pool_switch_slot',
23
+ ]);
24
+
25
+ const VIEWPORT_ACTIONS = new Set([
26
+ 'sync_window_viewport',
27
+ 'get_current_url',
28
+ ]);
29
+
30
+ async function executeExternalOperationIfAny({
31
+ profileId,
32
+ action,
33
+ params,
34
+ operation,
35
+ context,
36
+ }) {
37
+ const executor = context?.executeExternalOperation;
38
+ if (typeof executor !== 'function') return null;
39
+ const result = await executor({
40
+ profileId,
41
+ action,
42
+ params,
43
+ operation,
44
+ context,
45
+ });
46
+ if (result === null || result === undefined) return null;
47
+ if (result && typeof result === 'object' && typeof result.ok === 'boolean') {
48
+ return result;
49
+ }
50
+ return asErrorPayload('OPERATION_FAILED', 'external operation executor returned invalid payload', {
51
+ action,
52
+ resultType: typeof result,
53
+ });
54
+ }
55
+
56
+ async function flashOperationViewport(profileId, params = {}) {
57
+ if (params.highlight === false) return;
58
+ try {
59
+ await callAPI('evaluate', {
60
+ profileId,
61
+ script: `(() => {
62
+ const root = document.documentElement;
63
+ if (!(root instanceof HTMLElement)) return { ok: false };
64
+ const prevShadow = root.style.boxShadow;
65
+ const prevTransition = root.style.transition;
66
+ root.style.transition = 'box-shadow 80ms ease';
67
+ root.style.boxShadow = 'inset 0 0 0 3px #ff7a00';
68
+ setTimeout(() => {
69
+ root.style.boxShadow = prevShadow;
70
+ root.style.transition = prevTransition;
71
+ }, 260);
72
+ return { ok: true };
73
+ })()`,
74
+ });
75
+ } catch {
76
+ // highlight failure should never block action execution
77
+ }
78
+ }
79
+
80
+ async function executeSelectorOperation({ profileId, action, operation, params }) {
81
+ const selector = maybeSelector({
82
+ profileId,
83
+ containerId: params.containerId || operation?.containerId || null,
84
+ selector: params.selector || operation?.selector || null,
85
+ });
86
+ if (!selector) return asErrorPayload('CONTAINER_NOT_FOUND', `${action} requires selector/containerId`);
87
+
88
+ const highlight = params.highlight !== false;
89
+ if (action === 'scroll_into_view') {
90
+ const script = buildSelectorScrollIntoViewScript({ selector, highlight });
91
+ const result = await callAPI('evaluate', {
92
+ profileId,
93
+ script,
94
+ });
95
+ return { ok: true, code: 'OPERATION_DONE', message: 'scroll_into_view done', data: result };
96
+ }
97
+
98
+ const typeText = String(params.text ?? params.value ?? '');
99
+ const script = action === 'click'
100
+ ? buildSelectorClickScript({ selector, highlight })
101
+ : buildSelectorTypeScript({ selector, highlight, text: typeText });
102
+ const result = await callAPI('evaluate', { profileId, script });
103
+ return { ok: true, code: 'OPERATION_DONE', message: `${action} done`, data: result };
104
+ }
105
+
106
+ async function executeVerifySubscriptions({ profileId, params }) {
107
+ const defaultVisible = params.visible !== false;
108
+ const defaultMinCount = Math.max(0, Number(params.minCount ?? 1) || 1);
109
+ const selectorItems = normalizeArray(params.subscriptions || params.selectors)
110
+ .map((item, idx) => {
111
+ if (typeof item === 'string') {
112
+ return {
113
+ id: `selector_${idx + 1}`,
114
+ selector: item,
115
+ visible: defaultVisible,
116
+ minCount: defaultMinCount,
117
+ };
118
+ }
119
+ if (!item || typeof item !== 'object') return null;
120
+ const selector = String(item.selector || '').trim();
121
+ if (!selector) return null;
122
+ const visible = item.visible !== undefined
123
+ ? item.visible !== false
124
+ : defaultVisible;
125
+ const minCount = Math.max(0, Number(item.minCount ?? defaultMinCount) || defaultMinCount);
126
+ return {
127
+ id: String(item.id || `selector_${idx + 1}`),
128
+ selector,
129
+ visible,
130
+ minCount,
131
+ };
132
+ })
133
+ .filter(Boolean);
134
+ if (selectorItems.length === 0) {
135
+ return asErrorPayload('OPERATION_FAILED', 'verify_subscriptions requires params.selectors');
136
+ }
137
+
138
+ const acrossPages = params.acrossPages === true;
139
+ const settleMs = Math.max(0, Number(params.settleMs ?? 280) || 280);
140
+
141
+ const collectForCurrentPage = async () => {
142
+ const snapshot = await getDomSnapshotByProfile(profileId);
143
+ const url = await getCurrentUrl(profileId);
144
+ const matches = selectorItems.map((item) => ({
145
+ id: item.id,
146
+ selector: item.selector,
147
+ visible: item.visible,
148
+ minCount: item.minCount,
149
+ count: buildSelectorCheck(snapshot, {
150
+ css: item.selector,
151
+ visible: item.visible,
152
+ }).length,
153
+ }));
154
+ return { url, matches };
155
+ };
156
+
157
+ let pagesResult = [];
158
+ let overallOk = true;
159
+ if (!acrossPages) {
160
+ const current = await collectForCurrentPage();
161
+ overallOk = current.matches.every((item) => item.count >= item.minCount);
162
+ pagesResult = [{ index: null, ...current }];
163
+ } else {
164
+ const listed = await callAPI('page:list', { profileId });
165
+ const { pages, activeIndex } = extractPageList(listed);
166
+ for (const page of pages) {
167
+ const pageIndex = Number(page.index);
168
+ if (Number.isFinite(activeIndex) && activeIndex !== pageIndex) {
169
+ await callAPI('page:switch', { profileId, index: pageIndex });
170
+ if (settleMs > 0) await new Promise((resolve) => setTimeout(resolve, settleMs));
171
+ }
172
+ const current = await collectForCurrentPage();
173
+ const pageOk = current.matches.every((item) => item.count >= item.minCount);
174
+ overallOk = overallOk && pageOk;
175
+ pagesResult.push({ index: pageIndex, ...current, ok: pageOk });
176
+ }
177
+ if (Number.isFinite(activeIndex)) {
178
+ await callAPI('page:switch', { profileId, index: activeIndex });
179
+ }
180
+ }
181
+
182
+ if (!overallOk) {
183
+ return asErrorPayload('SUBSCRIPTION_MISMATCH', 'subscription selectors missing on one or more pages', {
184
+ acrossPages,
185
+ pages: pagesResult,
186
+ });
187
+ }
188
+
189
+ return {
190
+ ok: true,
191
+ code: 'OPERATION_DONE',
192
+ message: 'verify_subscriptions done',
193
+ data: { acrossPages, pages: pagesResult },
194
+ };
195
+ }
196
+
197
+ export async function executeOperation({ profileId, operation, context = {} }) {
198
+ try {
199
+ const session = await ensureActiveSession(profileId);
200
+ const resolvedProfile = session.profileId || profileId;
201
+ const action = String(operation?.action || '').trim();
202
+ const params = operation?.params || operation?.config || {};
203
+
204
+ if (!action) {
205
+ return asErrorPayload('OPERATION_FAILED', 'operation.action is required');
206
+ }
207
+
208
+ if (action !== 'wait') {
209
+ await flashOperationViewport(resolvedProfile, params);
210
+ }
211
+
212
+ if (TAB_ACTIONS.has(action)) {
213
+ return await executeTabPoolOperation({
214
+ profileId: resolvedProfile,
215
+ action,
216
+ params,
217
+ context,
218
+ });
219
+ }
220
+
221
+ if (VIEWPORT_ACTIONS.has(action)) {
222
+ return await executeViewportOperation({
223
+ profileId: resolvedProfile,
224
+ action,
225
+ params,
226
+ });
227
+ }
228
+
229
+ if (action === 'goto') {
230
+ const url = String(params.url || params.value || '').trim();
231
+ if (!url) return asErrorPayload('OPERATION_FAILED', 'goto requires params.url');
232
+ const result = await callAPI('goto', { profileId: resolvedProfile, url });
233
+ return { ok: true, code: 'OPERATION_DONE', message: 'goto done', data: result };
234
+ }
235
+
236
+ if (action === 'back') {
237
+ const result = await callAPI('page:back', { profileId: resolvedProfile });
238
+ return { ok: true, code: 'OPERATION_DONE', message: 'back done', data: result };
239
+ }
240
+
241
+ if (action === 'list_pages') {
242
+ const result = await callAPI('page:list', { profileId: resolvedProfile });
243
+ const { pages, activeIndex } = extractPageList(result);
244
+ return { ok: true, code: 'OPERATION_DONE', message: 'list_pages done', data: { pages, activeIndex, raw: result } };
245
+ }
246
+
247
+ if (action === 'new_page') {
248
+ const rawUrl = String(params.url || params.value || '').trim();
249
+ const payload = rawUrl ? { profileId: resolvedProfile, url: rawUrl } : { profileId: resolvedProfile };
250
+ const result = await callAPI('newPage', payload);
251
+ return { ok: true, code: 'OPERATION_DONE', message: 'new_page done', data: result };
252
+ }
253
+
254
+ if (action === 'switch_page') {
255
+ const index = Number(params.index ?? params.value);
256
+ if (!Number.isFinite(index)) {
257
+ return asErrorPayload('OPERATION_FAILED', 'switch_page requires params.index');
258
+ }
259
+ const result = await callAPI('page:switch', { profileId: resolvedProfile, index });
260
+ return { ok: true, code: 'OPERATION_DONE', message: 'switch_page done', data: result };
261
+ }
262
+
263
+ if (action === 'wait') {
264
+ const ms = Math.max(0, Number(params.ms ?? params.value ?? 0));
265
+ await new Promise((resolve) => setTimeout(resolve, ms));
266
+ return { ok: true, code: 'OPERATION_DONE', message: 'wait done', data: { ms } };
267
+ }
268
+
269
+ if (action === 'scroll') {
270
+ const amount = Math.max(1, Number(params.amount ?? params.value ?? 300) || 300);
271
+ const direction = String(params.direction || 'down').toLowerCase();
272
+ let deltaX = 0;
273
+ let deltaY = amount;
274
+ if (direction === 'up') deltaY = -amount;
275
+ else if (direction === 'left') {
276
+ deltaX = -amount;
277
+ deltaY = 0;
278
+ } else if (direction === 'right') {
279
+ deltaX = amount;
280
+ deltaY = 0;
281
+ }
282
+ const result = await callAPI('mouse:wheel', { profileId: resolvedProfile, deltaX, deltaY });
283
+ return {
284
+ ok: true,
285
+ code: 'OPERATION_DONE',
286
+ message: 'scroll done',
287
+ data: { direction, amount, deltaX, deltaY, result },
288
+ };
289
+ }
290
+
291
+ if (action === 'press_key') {
292
+ const key = String(params.key || params.value || '').trim();
293
+ if (!key) return asErrorPayload('OPERATION_FAILED', 'press_key requires params.key');
294
+ const result = await callAPI('evaluate', {
295
+ profileId: resolvedProfile,
296
+ script: `(async () => {
297
+ const target = document.activeElement || document.body || document.documentElement;
298
+ const key = ${JSON.stringify(key)};
299
+ const code = key.length === 1 ? 'Key' + key.toUpperCase() : key;
300
+ const opts = { key, code, bubbles: true, cancelable: true };
301
+ target.dispatchEvent(new KeyboardEvent('keydown', opts));
302
+ target.dispatchEvent(new KeyboardEvent('keypress', opts));
303
+ target.dispatchEvent(new KeyboardEvent('keyup', opts));
304
+ if (key === 'Escape') {
305
+ const closeButton = document.querySelector('.note-detail-mask .close-box, .note-detail-mask .close-circle');
306
+ if (closeButton instanceof HTMLElement) closeButton.click();
307
+ }
308
+ if (key === 'Enter' && target instanceof HTMLInputElement && target.form) {
309
+ if (typeof target.form.requestSubmit === 'function') target.form.requestSubmit();
310
+ else target.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
311
+ }
312
+ return { key, targetTag: target?.tagName || null };
313
+ })()`,
314
+ });
315
+ return { ok: true, code: 'OPERATION_DONE', message: 'press_key done', data: result };
316
+ }
317
+
318
+ if (action === 'verify_subscriptions') {
319
+ return executeVerifySubscriptions({ profileId: resolvedProfile, params });
320
+ }
321
+
322
+ if (action === 'evaluate') {
323
+ const script = String(params.script || '').trim();
324
+ if (!script) return asErrorPayload('OPERATION_FAILED', 'evaluate requires params.script');
325
+ const result = await callAPI('evaluate', { profileId: resolvedProfile, script });
326
+ return { ok: true, code: 'OPERATION_DONE', message: 'evaluate done', data: result };
327
+ }
328
+
329
+ if (action === 'click' || action === 'type' || action === 'scroll_into_view') {
330
+ return executeSelectorOperation({
331
+ profileId: resolvedProfile,
332
+ action,
333
+ operation,
334
+ params,
335
+ });
336
+ }
337
+
338
+ const externalResult = await executeExternalOperationIfAny({
339
+ profileId: resolvedProfile,
340
+ action,
341
+ params,
342
+ operation,
343
+ context,
344
+ });
345
+ if (externalResult) return externalResult;
346
+
347
+ return asErrorPayload('UNSUPPORTED_OPERATION', `Unsupported operation action: ${action}`);
348
+ } catch (err) {
349
+ return asErrorPayload('OPERATION_FAILED', err?.message || String(err), { context });
350
+ }
351
+ }
@@ -0,0 +1,68 @@
1
+ function asBoolLiteral(value) {
2
+ return value ? 'true' : 'false';
3
+ }
4
+
5
+ export function buildSelectorScrollIntoViewScript({ selector, highlight }) {
6
+ const selectorLiteral = JSON.stringify(selector);
7
+ const highlightLiteral = asBoolLiteral(highlight);
8
+ return `(async () => {
9
+ const el = document.querySelector(${selectorLiteral});
10
+ if (!el) throw new Error('Element not found: ' + ${selectorLiteral});
11
+ const restoreOutline = el instanceof HTMLElement ? el.style.outline : '';
12
+ if (${highlightLiteral} && el instanceof HTMLElement) {
13
+ el.style.outline = '2px solid #ff4d4f';
14
+ }
15
+ el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' });
16
+ await new Promise((r) => setTimeout(r, 120));
17
+ if (${highlightLiteral} && el instanceof HTMLElement) {
18
+ el.style.outline = restoreOutline;
19
+ }
20
+ return { ok: true, selector: ${selectorLiteral} };
21
+ })()`;
22
+ }
23
+
24
+ export function buildSelectorClickScript({ selector, highlight }) {
25
+ const selectorLiteral = JSON.stringify(selector);
26
+ const highlightLiteral = asBoolLiteral(highlight);
27
+ return `(async () => {
28
+ const el = document.querySelector(${selectorLiteral});
29
+ if (!el) throw new Error('Element not found: ' + ${selectorLiteral});
30
+ const restoreOutline = el instanceof HTMLElement ? el.style.outline : '';
31
+ if (${highlightLiteral} && el instanceof HTMLElement) {
32
+ el.style.outline = '2px solid #ff4d4f';
33
+ }
34
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
35
+ await new Promise((r) => setTimeout(r, 150));
36
+ el.click();
37
+ if (${highlightLiteral} && el instanceof HTMLElement) {
38
+ setTimeout(() => { el.style.outline = restoreOutline; }, 260);
39
+ }
40
+ return { ok: true, selector: ${selectorLiteral}, action: 'click', highlight: ${highlightLiteral} };
41
+ })()`;
42
+ }
43
+
44
+ export function buildSelectorTypeScript({ selector, highlight, text }) {
45
+ const selectorLiteral = JSON.stringify(selector);
46
+ const highlightLiteral = asBoolLiteral(highlight);
47
+ const textLiteral = JSON.stringify(String(text || ''));
48
+ const textLength = String(text || '').length;
49
+
50
+ return `(async () => {
51
+ const el = document.querySelector(${selectorLiteral});
52
+ if (!el) throw new Error('Element not found: ' + ${selectorLiteral});
53
+ const restoreOutline = el instanceof HTMLElement ? el.style.outline : '';
54
+ if (${highlightLiteral} && el instanceof HTMLElement) {
55
+ el.style.outline = '2px solid #ff4d4f';
56
+ }
57
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
58
+ await new Promise((r) => setTimeout(r, 150));
59
+ el.focus();
60
+ el.value = ${textLiteral};
61
+ el.dispatchEvent(new Event('input', { bubbles: true }));
62
+ el.dispatchEvent(new Event('change', { bubbles: true }));
63
+ if (${highlightLiteral} && el instanceof HTMLElement) {
64
+ setTimeout(() => { el.style.outline = restoreOutline; }, 260);
65
+ }
66
+ return { ok: true, selector: ${selectorLiteral}, action: 'type', length: ${textLength}, highlight: ${highlightLiteral} };
67
+ })()`;
68
+ }