@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
@@ -26,7 +26,230 @@ export async function callAPI(action, payload = {}) {
26
26
 
27
27
  export async function getSessionByProfile(profileId) {
28
28
  const status = await callAPI('getStatus', {});
29
- return status?.sessions?.find((s) => s.profileId === profileId) || null;
29
+ const activeSession = status?.sessions?.find((s) => s.profileId === profileId) || null;
30
+ if (activeSession) {
31
+ return activeSession;
32
+ }
33
+ if (!profileId) {
34
+ return null;
35
+ }
36
+
37
+ // Some browser-service builds do not populate getStatus.sessions reliably.
38
+ // Fallback to page:list so runtime can still attach to an active profile tab set.
39
+ try {
40
+ const pagePayload = await callAPI('page:list', { profileId });
41
+ const pages = Array.isArray(pagePayload?.pages)
42
+ ? pagePayload.pages
43
+ : Array.isArray(pagePayload?.data?.pages)
44
+ ? pagePayload.data.pages
45
+ : [];
46
+ if (!pages.length) return null;
47
+ const activeIndex = Number(pagePayload?.activeIndex ?? pagePayload?.data?.activeIndex);
48
+ const activePage = Number.isFinite(activeIndex)
49
+ ? pages.find((page) => Number(page?.index) === activeIndex)
50
+ : (pages.find((page) => page?.active) || pages[0]);
51
+ return {
52
+ profileId,
53
+ session_id: profileId,
54
+ sessionId: profileId,
55
+ current_url: activePage?.url || null,
56
+ recoveredFromPages: true,
57
+ };
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function buildDomSnapshotScript(maxDepth, maxChildren) {
64
+ return `(() => {
65
+ const MAX_DEPTH = ${maxDepth};
66
+ const MAX_CHILDREN = ${maxChildren};
67
+ const viewportWidth = Number(window.innerWidth || 0);
68
+ const viewportHeight = Number(window.innerHeight || 0);
69
+
70
+ const normalizeRect = (rect) => {
71
+ if (!rect) return null;
72
+ const left = Number(rect.left ?? rect.x ?? 0);
73
+ const top = Number(rect.top ?? rect.y ?? 0);
74
+ const width = Number(rect.width ?? 0);
75
+ const height = Number(rect.height ?? 0);
76
+ return {
77
+ left,
78
+ top,
79
+ right: left + width,
80
+ bottom: top + height,
81
+ x: left,
82
+ y: top,
83
+ width,
84
+ height,
85
+ };
86
+ };
87
+
88
+ const sanitizeClasses = (el) => {
89
+ const classAttr = typeof el.className === 'string'
90
+ ? el.className
91
+ : (el.getAttribute && el.getAttribute('class')) || '';
92
+ return classAttr.split(/\\s+/).filter(Boolean).slice(0, 24);
93
+ };
94
+
95
+ const collectAttrs = (el) => {
96
+ if (!el || !el.getAttribute) return null;
97
+ const keys = [
98
+ 'href',
99
+ 'src',
100
+ 'name',
101
+ 'type',
102
+ 'value',
103
+ 'placeholder',
104
+ 'role',
105
+ 'aria-label',
106
+ 'aria-hidden',
107
+ 'title',
108
+ ];
109
+ const attrs = {};
110
+ for (const key of keys) {
111
+ const value = el.getAttribute(key);
112
+ if (value === null || value === undefined || value === '') continue;
113
+ attrs[key] = String(value).slice(0, 400);
114
+ }
115
+ return Object.keys(attrs).length > 0 ? attrs : null;
116
+ };
117
+
118
+ const inViewport = (rect) => {
119
+ if (!rect) return false;
120
+ if (rect.width <= 0 || rect.height <= 0) return false;
121
+ return (
122
+ rect.right > 0
123
+ && rect.bottom > 0
124
+ && rect.left < viewportWidth
125
+ && rect.top < viewportHeight
126
+ );
127
+ };
128
+
129
+ const isRendered = (el) => {
130
+ try {
131
+ const style = window.getComputedStyle(el);
132
+ if (!style) return false;
133
+ if (style.display === 'none') return false;
134
+ if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
135
+ const opacity = Number.parseFloat(String(style.opacity || '1'));
136
+ if (Number.isFinite(opacity) && opacity <= 0.01) return false;
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ };
142
+
143
+ const clampPoint = (value, max) => {
144
+ if (!Number.isFinite(value)) return 0;
145
+ if (max <= 1) return 0;
146
+ return Math.max(0, Math.min(max - 1, value));
147
+ };
148
+
149
+ const hitTestVisible = (el, rect) => {
150
+ if (!rect || viewportWidth <= 0 || viewportHeight <= 0) return false;
151
+ const samplePoints = [
152
+ [rect.left + rect.width * 0.5, rect.top + rect.height * 0.5],
153
+ [rect.left + rect.width * 0.2, rect.top + rect.height * 0.2],
154
+ [rect.left + rect.width * 0.8, rect.top + rect.height * 0.8],
155
+ ];
156
+ for (const [rawX, rawY] of samplePoints) {
157
+ const x = clampPoint(rawX, viewportWidth);
158
+ const y = clampPoint(rawY, viewportHeight);
159
+ const topEl = document.elementFromPoint(x, y);
160
+ if (!topEl) continue;
161
+ if (topEl === el) return true;
162
+ if (el.contains && el.contains(topEl)) return true;
163
+ if (topEl.contains && topEl.contains(el)) return true;
164
+ }
165
+ return false;
166
+ };
167
+
168
+ const collect = (el, depth = 0, path = 'root') => {
169
+ if (!el || depth > MAX_DEPTH) return null;
170
+ const classes = sanitizeClasses(el);
171
+ const rect = normalizeRect(el.getBoundingClientRect ? el.getBoundingClientRect() : null);
172
+ const tag = String(el.tagName || el.nodeName || '').toLowerCase();
173
+ const id = el.id || null;
174
+ const text = typeof el.textContent === 'string'
175
+ ? el.textContent.replace(/\\s+/g, ' ').trim()
176
+ : '';
177
+ const selector = tag
178
+ ? \`\${tag}\${id ? '#' + id : ''}\${classes.length ? '.' + classes.slice(0, 3).join('.') : ''}\`
179
+ : null;
180
+
181
+ const node = {
182
+ tag,
183
+ id,
184
+ classes,
185
+ selector,
186
+ path,
187
+ };
188
+ const attrs = collectAttrs(el);
189
+ if (attrs) node.attrs = attrs;
190
+ if (attrs && attrs.href) node.href = attrs.href;
191
+ if (rect) node.rect = rect;
192
+ if (text) node.textSnippet = text.slice(0, 120);
193
+ if (rect) {
194
+ const rendered = isRendered(el);
195
+ const withinViewport = inViewport(rect);
196
+ const visible = rendered && withinViewport && hitTestVisible(el, rect);
197
+ node.visible = visible;
198
+ } else {
199
+ node.visible = false;
200
+ }
201
+
202
+ const children = Array.from(el.children || []);
203
+ if (children.length > 0 && depth < MAX_DEPTH) {
204
+ node.children = [];
205
+ const limit = Math.min(children.length, MAX_CHILDREN);
206
+ for (let i = 0; i < limit; i += 1) {
207
+ const child = collect(children[i], depth + 1, \`\${path}/\${i}\`);
208
+ if (child) node.children.push(child);
209
+ }
210
+ }
211
+
212
+ return node;
213
+ };
214
+
215
+ const root = collect(document.body || document.documentElement, 0, 'root');
216
+ return {
217
+ dom_tree: root,
218
+ viewport: {
219
+ width: viewportWidth,
220
+ height: viewportHeight,
221
+ },
222
+ };
223
+ })()`;
224
+ }
225
+
226
+ export async function getDomSnapshotByProfile(profileId, options = {}) {
227
+ const maxDepth = Math.max(1, Math.min(20, Number(options.maxDepth) || 10));
228
+ const maxChildren = Math.max(1, Math.min(500, Number(options.maxChildren) || 120));
229
+ const response = await callAPI('evaluate', {
230
+ profileId,
231
+ script: buildDomSnapshotScript(maxDepth, maxChildren),
232
+ });
233
+ const payload = response?.result || response || {};
234
+ const tree = payload.dom_tree || null;
235
+ if (tree && payload.viewport && typeof payload.viewport === 'object') {
236
+ tree.__viewport = {
237
+ width: Number(payload.viewport.width) || 0,
238
+ height: Number(payload.viewport.height) || 0,
239
+ };
240
+ }
241
+ return tree;
242
+ }
243
+
244
+ export async function getViewportByProfile(profileId) {
245
+ const response = await callAPI('evaluate', {
246
+ profileId,
247
+ script: `(() => ({ width: Number(window.innerWidth || 0), height: Number(window.innerHeight || 0) }))()`,
248
+ });
249
+ const viewport = response?.result || response?.viewport || {};
250
+ const width = Number(viewport?.width) || 1280;
251
+ const height = Number(viewport?.height) || 720;
252
+ return { width, height };
30
253
  }
31
254
 
32
255
  export async function checkBrowserService() {
@@ -89,15 +312,15 @@ function scanCommonRepoRoots() {
89
312
  const roots = [
90
313
  path.join(home, 'Documents', 'github'),
91
314
  path.join(home, 'github'),
92
- path.join(home, 'code'),
315
+ path.join(home, 'code'),
93
316
  path.join(home, 'projects'),
94
- path.join('/Volumes', 'extension', 'code'),
95
- path.join('C:', 'code'),
96
- path.join('D:', 'code'),
97
- path.join('C:', 'projects'),
98
- path.join('D:', 'projects'),
99
- path.join('C:', 'Users', os.userInfo().username, 'code'),
100
- path.join('C:', 'Users', os.userInfo().username, 'projects'),
317
+ path.join('/Volumes', 'extension', 'code'),
318
+ path.join('C:', 'code'),
319
+ path.join('D:', 'code'),
320
+ path.join('C:', 'projects'),
321
+ path.join('D:', 'projects'),
322
+ path.join('C:', 'Users', os.userInfo().username, 'code'),
323
+ path.join('C:', 'Users', os.userInfo().username, 'projects'),
101
324
  path.join('C:', 'Users', os.userInfo().username, 'Documents', 'github'),
102
325
  ].filter(Boolean);
103
326
 
@@ -115,13 +115,36 @@ EXAMPLES:
115
115
  camo stop
116
116
 
117
117
  CONTAINER FILTER & SUBSCRIPTION:
118
- container filter <selector> [--profile <id>] Filter DOM elements by CSS selector
119
- container watch --selector <css> Watch for element changes
120
- container list List visible elements in viewport
118
+ container init [--source <dir>] [--force] Initialize subscription dir + migrate container sets
119
+ container sets [--site <siteKey>] List migrated subscription sets
120
+ container register [profileId] <setId...> Register targets (path / url+dom markers) for profile
121
+ container targets [profileId] Show registered subscription targets
122
+ container filter [profileId] <selector...> Filter DOM elements by CSS selector
123
+ container watch [profileId] [--selector <css>] Watch for element changes (or use registered selectors)
124
+ container list [profileId] List visible elements in viewport
125
+
126
+ AUTOSCRIPT (STRATEGY LAYER):
127
+ autoscript scaffold xhs-unified [--output <file>] Generate xiaohongshu unified-harvest autoscript template
128
+ autoscript validate <file> Validate autoscript schema and references
129
+ autoscript explain <file> Print normalized graph and defaults
130
+ autoscript snapshot <jsonl-file> [--out <snapshot-file>] Build resumable snapshot from run JSONL
131
+ autoscript replay <jsonl-file> [--summary-file <path>] Rebuild summary from run JSONL
132
+ autoscript run <file> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>] Run autoscript runtime
133
+ autoscript resume <file> --snapshot <snapshot-file> [--from-node <nodeId>] [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]
134
+ autoscript mock-run <file> --fixture <fixture.json> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]
135
+
136
+ PROGRESS EVENTS:
137
+ events serve [--host 127.0.0.1] [--port 7788] Start progress websocket server (/events)
138
+ events tail [filters...] Tail progress events via websocket
139
+ events recent [--limit 50] Show recent persisted events
140
+ events emit --event <name> Emit a manual test event
141
+ (non-events commands auto-start daemon by default)
121
142
 
122
143
  ENV:
123
144
  WEBAUTO_BROWSER_URL Default: http://127.0.0.1:7704
124
145
  WEBAUTO_REPO_ROOT Optional explicit webauto repo root
146
+ CAMO_PROGRESS_EVENTS_FILE Optional path override for progress jsonl
147
+ CAMO_PROGRESS_WS_HOST / CAMO_PROGRESS_WS_PORT Progress daemon host/port (defaults: 127.0.0.1:7788)
125
148
  `);
126
149
  }
127
150