@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,77 @@
1
+ import { callAPI } from '../../../utils/browser-service.mjs';
2
+
3
+ export function withOperationHighlight(script, color = '#ff7a00') {
4
+ return `(() => {
5
+ const flashNode = (node, duration = 420) => {
6
+ if (!(node instanceof HTMLElement)) return;
7
+ const prevOutline = node.style.outline;
8
+ const prevOffset = node.style.outlineOffset;
9
+ const prevTransition = node.style.transition;
10
+ node.style.transition = 'outline 80ms ease';
11
+ node.style.outline = '2px solid ${color}';
12
+ node.style.outlineOffset = '2px';
13
+ setTimeout(() => {
14
+ node.style.outline = prevOutline;
15
+ node.style.outlineOffset = prevOffset;
16
+ node.style.transition = prevTransition;
17
+ }, duration);
18
+ };
19
+ const flashViewport = (duration = 420) => {
20
+ const root = document.documentElement;
21
+ if (!(root instanceof HTMLElement)) return;
22
+ const prevShadow = root.style.boxShadow;
23
+ const prevTransition = root.style.transition;
24
+ root.style.transition = 'box-shadow 80ms ease';
25
+ root.style.boxShadow = 'inset 0 0 0 3px ${color}';
26
+ setTimeout(() => {
27
+ root.style.boxShadow = prevShadow;
28
+ root.style.transition = prevTransition;
29
+ }, duration);
30
+ };
31
+ flashViewport();
32
+ const target = document.activeElement instanceof HTMLElement
33
+ ? document.activeElement
34
+ : (document.body || document.documentElement);
35
+ flashNode(target);
36
+ return (${script});
37
+ })()`;
38
+ }
39
+
40
+ export async function runEvaluateScript({ profileId, script, highlight = true }) {
41
+ const wrappedScript = highlight ? withOperationHighlight(script) : script;
42
+ return callAPI('evaluate', { profileId, script: wrappedScript });
43
+ }
44
+
45
+ export function extractEvaluateResultData(payload) {
46
+ if (!payload || typeof payload !== 'object') return null;
47
+ if ('result' in payload) return payload.result;
48
+ if (payload.data && typeof payload.data === 'object' && 'result' in payload.data) {
49
+ return payload.data.result;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ export function extractScreenshotBase64(payload) {
55
+ if (!payload || typeof payload !== 'object') return '';
56
+ if (typeof payload.data === 'string' && payload.data) return payload.data;
57
+ if (payload.result && typeof payload.result === 'object' && typeof payload.result.data === 'string') {
58
+ return payload.result.data;
59
+ }
60
+ if (payload.data && typeof payload.data === 'object' && typeof payload.data.data === 'string') {
61
+ return payload.data.data;
62
+ }
63
+ return '';
64
+ }
65
+
66
+ export async function evaluateWithScript({ profileId, script, message, highlight }) {
67
+ const result = await runEvaluateScript({ profileId, script, highlight });
68
+ return { ok: true, code: 'OPERATION_DONE', message, data: result };
69
+ }
70
+
71
+ export function createEvaluateHandler(message, buildScript) {
72
+ return async ({ profileId, params }) => {
73
+ const script = buildScript(params);
74
+ const highlight = params.highlight !== false;
75
+ return evaluateWithScript({ profileId, script, message, highlight });
76
+ };
77
+ }
@@ -0,0 +1,181 @@
1
+ export function buildDetailHarvestScript() {
2
+ return `(async () => {
3
+ const state = window.__camoXhsState || (window.__camoXhsState = {});
4
+ const scroller = document.querySelector('.note-scroller')
5
+ || document.querySelector('.comments-el')
6
+ || document.scrollingElement
7
+ || document.documentElement;
8
+ for (let i = 0; i < 3; i += 1) {
9
+ scroller.scrollBy({ top: 360, behavior: 'auto' });
10
+ await new Promise((resolve) => setTimeout(resolve, 120));
11
+ }
12
+ const title = (document.querySelector('.note-title') || {}).textContent || '';
13
+ const content = (document.querySelector('.note-content') || {}).textContent || '';
14
+ state.lastDetail = {
15
+ title: String(title).trim().slice(0, 200),
16
+ contentLength: String(content).trim().length,
17
+ capturedAt: new Date().toISOString(),
18
+ };
19
+ return { harvested: true, detail: state.lastDetail };
20
+ })()`;
21
+ }
22
+
23
+ export function buildExpandRepliesScript() {
24
+ return `(async () => {
25
+ const buttons = Array.from(document.querySelectorAll([
26
+ '.note-detail-mask .show-more',
27
+ '.note-detail-mask .reply-expand',
28
+ '.note-detail-mask [class*="expand"]',
29
+ '.note-detail-page .show-more',
30
+ '.note-detail-page .reply-expand',
31
+ '.note-detail-page [class*="expand"]',
32
+ ].join(',')));
33
+ let clicked = 0;
34
+ for (const button of buttons.slice(0, 8)) {
35
+ if (!(button instanceof HTMLElement)) continue;
36
+ const text = (button.textContent || '').trim();
37
+ if (!text) continue;
38
+ button.scrollIntoView({ behavior: 'auto', block: 'center' });
39
+ await new Promise((resolve) => setTimeout(resolve, 60));
40
+ button.click();
41
+ clicked += 1;
42
+ await new Promise((resolve) => setTimeout(resolve, 120));
43
+ }
44
+ return { expanded: clicked, scanned: buttons.length };
45
+ })()`;
46
+ }
47
+
48
+ export function buildCloseDetailScript() {
49
+ return `(async () => {
50
+ const state = window.__camoXhsState || (window.__camoXhsState = {});
51
+ const metrics = state.metrics && typeof state.metrics === 'object' ? state.metrics : {};
52
+ state.metrics = metrics;
53
+ metrics.searchCount = Number(metrics.searchCount || 0);
54
+ metrics.rollbackCount = Number(metrics.rollbackCount || 0);
55
+ metrics.returnToSearchCount = Number(metrics.returnToSearchCount || 0);
56
+ const harvest = state.lastCommentsHarvest && typeof state.lastCommentsHarvest === 'object'
57
+ ? state.lastCommentsHarvest
58
+ : null;
59
+ const exitMeta = {
60
+ pageExitReason: String(harvest?.exitReason || 'close_without_harvest').trim(),
61
+ reachedBottom: typeof harvest?.reachedBottom === 'boolean' ? harvest.reachedBottom : null,
62
+ commentsCollected: Number.isFinite(Number(harvest?.collected)) ? Number(harvest.collected) : null,
63
+ expectedCommentsCount: Number.isFinite(Number(harvest?.expectedCommentsCount)) ? Number(harvest.expectedCommentsCount) : null,
64
+ commentCoverageRate: Number.isFinite(Number(harvest?.commentCoverageRate)) ? Number(harvest.commentCoverageRate) : null,
65
+ scrollRecoveries: Number.isFinite(Number(harvest?.recoveries)) ? Number(harvest.recoveries) : 0,
66
+ harvestRounds: Number.isFinite(Number(harvest?.rounds)) ? Number(harvest.rounds) : null,
67
+ };
68
+ const detailSelectors = [
69
+ '.note-detail-mask',
70
+ '.note-detail-page',
71
+ '.note-detail-dialog',
72
+ '.note-detail-mask .detail-container',
73
+ '.note-detail-mask .media-container',
74
+ '.note-detail-mask .note-scroller',
75
+ '.note-detail-mask .note-content',
76
+ '.note-detail-mask .interaction-container',
77
+ '.note-detail-mask .comments-container',
78
+ ];
79
+ const searchSelectors = ['.note-item', '.search-result-list', '#search-input', '.feeds-page'];
80
+ const hasVisible = (selectors) => selectors.some((selector) => {
81
+ const node = document.querySelector(selector);
82
+ if (!node || !(node instanceof HTMLElement)) return false;
83
+ const style = window.getComputedStyle(node);
84
+ if (!style) return false;
85
+ if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity || '1') === 0) return false;
86
+ const rect = node.getBoundingClientRect();
87
+ return rect.width > 1 && rect.height > 1;
88
+ });
89
+ const isDetailVisible = () => hasVisible(detailSelectors);
90
+ const isSearchVisible = () => hasVisible(searchSelectors);
91
+ const dispatchEscape = () => {
92
+ const target = document.activeElement || document.body || document.documentElement;
93
+ const opts = { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true };
94
+ target.dispatchEvent(new KeyboardEvent('keydown', opts));
95
+ target.dispatchEvent(new KeyboardEvent('keyup', opts));
96
+ document.dispatchEvent(new KeyboardEvent('keydown', opts));
97
+ document.dispatchEvent(new KeyboardEvent('keyup', opts));
98
+ };
99
+ const waitForCloseAnimation = async () => {
100
+ for (let i = 0; i < 45; i += 1) {
101
+ if (!isDetailVisible() && isSearchVisible()) return true;
102
+ await new Promise((resolve) => setTimeout(resolve, 120));
103
+ }
104
+ return !isDetailVisible() && isSearchVisible();
105
+ };
106
+ const buildCounterMeta = (returnedToSearch) => ({
107
+ searchCount: Number(metrics.searchCount || 0),
108
+ rollbackCount: Number(metrics.rollbackCount || 0),
109
+ returnToSearchCount: Number(metrics.returnToSearchCount || 0),
110
+ returnedToSearch: Boolean(returnedToSearch),
111
+ });
112
+
113
+ if (!isDetailVisible()) {
114
+ return {
115
+ closed: true,
116
+ via: 'already_closed',
117
+ searchVisible: isSearchVisible(),
118
+ ...buildCounterMeta(false),
119
+ ...exitMeta,
120
+ };
121
+ }
122
+
123
+ metrics.rollbackCount += 1;
124
+ metrics.lastRollbackAt = new Date().toISOString();
125
+
126
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
127
+ dispatchEscape();
128
+ await new Promise((resolve) => setTimeout(resolve, 220));
129
+ if (await waitForCloseAnimation()) {
130
+ const searchVisible = isSearchVisible();
131
+ if (searchVisible) {
132
+ metrics.returnToSearchCount += 1;
133
+ metrics.lastReturnToSearchAt = new Date().toISOString();
134
+ }
135
+ return {
136
+ closed: true,
137
+ via: 'escape',
138
+ attempts: attempt,
139
+ searchVisible,
140
+ ...buildCounterMeta(searchVisible),
141
+ ...exitMeta,
142
+ };
143
+ }
144
+ }
145
+
146
+ const selectors = ['.note-detail-mask .close-box', '.note-detail-mask .close-circle', '.close-box', '.close-circle'];
147
+ for (const selector of selectors) {
148
+ const target = document.querySelector(selector);
149
+ if (!target || !(target instanceof HTMLElement)) continue;
150
+ target.scrollIntoView({ behavior: 'auto', block: 'center' });
151
+ await new Promise((resolve) => setTimeout(resolve, 100));
152
+ target.click();
153
+ await new Promise((resolve) => setTimeout(resolve, 220));
154
+ if (await waitForCloseAnimation()) {
155
+ const searchVisible = isSearchVisible();
156
+ if (searchVisible) {
157
+ metrics.returnToSearchCount += 1;
158
+ metrics.lastReturnToSearchAt = new Date().toISOString();
159
+ }
160
+ return {
161
+ closed: true,
162
+ via: selector,
163
+ attempts: 5,
164
+ searchVisible,
165
+ ...buildCounterMeta(searchVisible),
166
+ ...exitMeta,
167
+ };
168
+ }
169
+ }
170
+
171
+ return {
172
+ closed: false,
173
+ via: 'escape_failed',
174
+ detailVisible: isDetailVisible(),
175
+ searchVisible: isSearchVisible(),
176
+ ...buildCounterMeta(false),
177
+ ...exitMeta,
178
+ };
179
+ })()`;
180
+ }
181
+