@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
@@ -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
 
@@ -6,6 +6,7 @@ import os from 'node:os';
6
6
  export const CONFIG_DIR = path.join(os.homedir(), '.webauto');
7
7
  export const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
8
8
  export const CONFIG_FILE = path.join(CONFIG_DIR, 'camo-cli.json');
9
+ export const PROFILE_META_FILE = 'camo-profile.json';
9
10
  export const BROWSER_SERVICE_URL = process.env.WEBAUTO_BROWSER_URL || 'http://127.0.0.1:7704';
10
11
 
11
12
  export function ensureDir(p) {
@@ -55,13 +56,13 @@ export function createProfile(profileId) {
55
56
  if (!isValidProfileId(profileId)) {
56
57
  throw new Error('Invalid profileId. Use only letters, numbers, dot, underscore, dash.');
57
58
  }
58
- const profileDir = path.join(PROFILES_DIR, profileId);
59
+ const profileDir = getProfileDir(profileId);
59
60
  if (fs.existsSync(profileDir)) throw new Error(`Profile already exists: ${profileId}`);
60
61
  ensureDir(profileDir);
61
62
  }
62
63
 
63
64
  export function deleteProfile(profileId) {
64
- const profileDir = path.join(PROFILES_DIR, profileId);
65
+ const profileDir = getProfileDir(profileId);
65
66
  if (!fs.existsSync(profileDir)) throw new Error(`Profile not found: ${profileId}`);
66
67
  fs.rmSync(profileDir, { recursive: true, force: true });
67
68
  }
@@ -82,6 +83,61 @@ export function getDefaultProfile() {
82
83
  return loadConfig().defaultProfile;
83
84
  }
84
85
 
86
+ export function getProfileDir(profileId) {
87
+ return path.join(PROFILES_DIR, String(profileId || '').trim());
88
+ }
89
+
90
+ export function getProfileMetaFile(profileId) {
91
+ return path.join(getProfileDir(profileId), PROFILE_META_FILE);
92
+ }
93
+
94
+ function loadProfileMeta(profileId) {
95
+ if (!isValidProfileId(profileId)) return {};
96
+ return readJson(getProfileMetaFile(profileId)) || {};
97
+ }
98
+
99
+ function saveProfileMeta(profileId, patch) {
100
+ if (!isValidProfileId(profileId)) return null;
101
+ const profileDir = getProfileDir(profileId);
102
+ ensureDir(profileDir);
103
+ const current = loadProfileMeta(profileId);
104
+ const next = {
105
+ ...current,
106
+ ...patch,
107
+ updatedAt: Date.now(),
108
+ };
109
+ writeJson(getProfileMetaFile(profileId), next);
110
+ return next;
111
+ }
112
+
113
+ export function getProfileWindowSize(profileId) {
114
+ const meta = loadProfileMeta(profileId);
115
+ const width = Number(meta?.window?.width);
116
+ const height = Number(meta?.window?.height);
117
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
118
+ if (width < 320 || height < 240) return null;
119
+ return {
120
+ width: Math.floor(width),
121
+ height: Math.floor(height),
122
+ updatedAt: Number(meta?.window?.updatedAt) || Number(meta?.updatedAt) || null,
123
+ };
124
+ }
125
+
126
+ export function setProfileWindowSize(profileId, width, height) {
127
+ const parsedWidth = Number(width);
128
+ const parsedHeight = Number(height);
129
+ if (!Number.isFinite(parsedWidth) || !Number.isFinite(parsedHeight)) return null;
130
+ if (parsedWidth < 320 || parsedHeight < 240) return null;
131
+ const now = Date.now();
132
+ return saveProfileMeta(profileId, {
133
+ window: {
134
+ width: Math.floor(parsedWidth),
135
+ height: Math.floor(parsedHeight),
136
+ updatedAt: now,
137
+ },
138
+ });
139
+ }
140
+
85
141
  const START_SCRIPT_REL = path.join('runtime', 'infra', 'utils', 'scripts', 'service', 'start-browser-service.mjs');
86
142
 
87
143
  export function hasStartScript(root) {
@@ -24,7 +24,7 @@ CONFIG:
24
24
 
25
25
  BROWSER CONTROL:
26
26
  init Ensure camoufox + ensure browser-service daemon
27
- start [profileId] [--url <url>] [--headless]
27
+ start [profileId] [--url <url>] [--headless] [--width <w> --height <h>]
28
28
  stop [profileId]
29
29
  status [profileId]
30
30
  list Alias of status
@@ -91,6 +91,7 @@ EXAMPLES:
91
91
  camo profile create myprofile
92
92
  camo profile default myprofile
93
93
  camo start --url https://example.com
94
+ camo start myprofile --width 1920 --height 1020
94
95
  camo goto https://www.xiaohongshu.com
95
96
  camo scroll --down --amount 500
96
97
  camo click "#search-input"
@@ -115,13 +116,36 @@ EXAMPLES:
115
116
  camo stop
116
117
 
117
118
  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
119
+ container init [--source <dir>] [--force] Initialize subscription dir + migrate container sets
120
+ container sets [--site <siteKey>] List migrated subscription sets
121
+ container register [profileId] <setId...> Register targets (path / url+dom markers) for profile
122
+ container targets [profileId] Show registered subscription targets
123
+ container filter [profileId] <selector...> Filter DOM elements by CSS selector
124
+ container watch [profileId] [--selector <css>] Watch for element changes (or use registered selectors)
125
+ container list [profileId] List visible elements in viewport
126
+
127
+ AUTOSCRIPT (STRATEGY LAYER):
128
+ autoscript scaffold xhs-unified [--output <file>] Generate xiaohongshu unified-harvest autoscript template
129
+ autoscript validate <file> Validate autoscript schema and references
130
+ autoscript explain <file> Print normalized graph and defaults
131
+ autoscript snapshot <jsonl-file> [--out <snapshot-file>] Build resumable snapshot from run JSONL
132
+ autoscript replay <jsonl-file> [--summary-file <path>] Rebuild summary from run JSONL
133
+ autoscript run <file> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>] Run autoscript runtime
134
+ autoscript resume <file> --snapshot <snapshot-file> [--from-node <nodeId>] [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]
135
+ autoscript mock-run <file> --fixture <fixture.json> [--profile <id>] [--jsonl-file <path>] [--summary-file <path>]
136
+
137
+ PROGRESS EVENTS:
138
+ events serve [--host 127.0.0.1] [--port 7788] Start progress websocket server (/events)
139
+ events tail [filters...] Tail progress events via websocket
140
+ events recent [--limit 50] Show recent persisted events
141
+ events emit --event <name> Emit a manual test event
142
+ (non-events commands auto-start daemon by default)
121
143
 
122
144
  ENV:
123
145
  WEBAUTO_BROWSER_URL Default: http://127.0.0.1:7704
124
146
  WEBAUTO_REPO_ROOT Optional explicit webauto repo root
147
+ CAMO_PROGRESS_EVENTS_FILE Optional path override for progress jsonl
148
+ CAMO_PROGRESS_WS_HOST / CAMO_PROGRESS_WS_PORT Progress daemon host/port (defaults: 127.0.0.1:7788)
125
149
  `);
126
150
  }
127
151