@web-auto/camo 0.2.0 → 0.2.2

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 (114) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +586 -586
  3. package/bin/browser-service.mjs +11 -11
  4. package/bin/camo.mjs +22 -22
  5. package/package.json +48 -48
  6. package/scripts/build.mjs +19 -19
  7. package/scripts/bump-version.mjs +34 -34
  8. package/scripts/check-file-size.mjs +80 -80
  9. package/scripts/file-size-policy.json +12 -2
  10. package/scripts/install.mjs +76 -76
  11. package/scripts/release.sh +54 -54
  12. package/src/autoscript/action-providers/index.mjs +6 -6
  13. package/src/autoscript/impact-engine.mjs +78 -78
  14. package/src/autoscript/runtime.mjs +1017 -1017
  15. package/src/autoscript/schema.mjs +376 -376
  16. package/src/cli.mjs +405 -405
  17. package/src/commands/attach.mjs +141 -141
  18. package/src/commands/autoscript.mjs +1011 -1011
  19. package/src/commands/browser.mjs +1255 -1255
  20. package/src/commands/container.mjs +401 -401
  21. package/src/commands/cookies.mjs +69 -69
  22. package/src/commands/create.mjs +98 -98
  23. package/src/commands/devtools.mjs +349 -349
  24. package/src/commands/events.mjs +152 -152
  25. package/src/commands/highlight-mode.mjs +24 -24
  26. package/src/commands/init.mjs +68 -68
  27. package/src/commands/lifecycle.mjs +275 -275
  28. package/src/commands/mouse.mjs +45 -45
  29. package/src/commands/profile.mjs +46 -46
  30. package/src/commands/record.mjs +115 -115
  31. package/src/commands/system.mjs +14 -14
  32. package/src/commands/window.mjs +123 -123
  33. package/src/container/change-notifier.mjs +362 -362
  34. package/src/container/element-filter.mjs +143 -143
  35. package/src/container/index.mjs +3 -3
  36. package/src/container/runtime-core/checkpoint.mjs +209 -209
  37. package/src/container/runtime-core/index.mjs +21 -21
  38. package/src/container/runtime-core/operations/index.mjs +774 -774
  39. package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
  40. package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
  41. package/src/container/runtime-core/operations/viewport.mjs +189 -189
  42. package/src/container/runtime-core/search.mjs +190 -190
  43. package/src/container/runtime-core/subscription.mjs +224 -224
  44. package/src/container/runtime-core/utils.mjs +94 -94
  45. package/src/container/runtime-core/validation.mjs +127 -127
  46. package/src/container/runtime-core.mjs +1 -1
  47. package/src/container/subscription-registry.mjs +459 -459
  48. package/src/core/actions.mjs +561 -561
  49. package/src/core/browser.mjs +266 -266
  50. package/src/core/index.mjs +52 -52
  51. package/src/core/utils.mjs +91 -91
  52. package/src/events/daemon-entry.mjs +33 -33
  53. package/src/events/daemon.mjs +80 -80
  54. package/src/events/progress-log.mjs +109 -109
  55. package/src/events/ws-server.mjs +239 -239
  56. package/src/lib/client.mjs +200 -200
  57. package/src/lifecycle/cleanup.mjs +83 -83
  58. package/src/lifecycle/lock.mjs +126 -126
  59. package/src/lifecycle/session-registry.mjs +279 -279
  60. package/src/lifecycle/session-view.mjs +76 -76
  61. package/src/lifecycle/session-watchdog.mjs +281 -281
  62. package/src/services/browser-service/index.js +671 -671
  63. package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
  64. package/src/services/browser-service/internal/BrowserSession.js +325 -304
  65. package/src/services/browser-service/internal/ElementRegistry.js +60 -60
  66. package/src/services/browser-service/internal/ProfileLock.js +84 -84
  67. package/src/services/browser-service/internal/SessionManager.js +184 -184
  68. package/src/services/browser-service/internal/SessionManager.test.js +39 -39
  69. package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
  70. package/src/services/browser-service/internal/browser-session/input-ops.js +222 -222
  71. package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
  72. package/src/services/browser-service/internal/browser-session/logging.js +46 -46
  73. package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
  74. package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
  75. package/src/services/browser-service/internal/browser-session/page-management.js +302 -302
  76. package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
  77. package/src/services/browser-service/internal/browser-session/recording.js +198 -198
  78. package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
  79. package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
  80. package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
  81. package/src/services/browser-service/internal/browser-session/types.js +14 -14
  82. package/src/services/browser-service/internal/browser-session/utils.js +95 -95
  83. package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
  84. package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
  85. package/src/services/browser-service/internal/container-matcher.js +851 -851
  86. package/src/services/browser-service/internal/container-registry.js +182 -182
  87. package/src/services/browser-service/internal/engine-manager.js +259 -259
  88. package/src/services/browser-service/internal/fingerprint.js +203 -203
  89. package/src/services/browser-service/internal/heartbeat.js +137 -137
  90. package/src/services/browser-service/internal/logging.js +46 -46
  91. package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
  92. package/src/services/browser-service/internal/pageRuntime.js +28 -28
  93. package/src/services/browser-service/internal/runtimeInjector.js +31 -31
  94. package/src/services/browser-service/internal/service-process-logger.js +140 -140
  95. package/src/services/browser-service/internal/state-bus.js +45 -45
  96. package/src/services/browser-service/internal/storage-paths.js +42 -42
  97. package/src/services/browser-service/internal/ws-server.js +1194 -1194
  98. package/src/services/browser-service/internal/ws-server.test.js +58 -58
  99. package/src/services/browser-service/server.mjs +6 -6
  100. package/src/services/controller/cli-bridge.js +93 -93
  101. package/src/services/controller/container-index.js +50 -50
  102. package/src/services/controller/container-storage.js +36 -36
  103. package/src/services/controller/controller-actions.js +207 -207
  104. package/src/services/controller/controller.js +1138 -1138
  105. package/src/services/controller/selectors.js +54 -54
  106. package/src/services/controller/transport.js +125 -125
  107. package/src/utils/args.mjs +26 -26
  108. package/src/utils/browser-service.mjs +544 -544
  109. package/src/utils/command-log.mjs +64 -64
  110. package/src/utils/config.mjs +214 -214
  111. package/src/utils/fingerprint.mjs +181 -181
  112. package/src/utils/help.mjs +216 -216
  113. package/src/utils/js-policy.mjs +13 -13
  114. package/src/utils/ws-client.mjs +30 -30
@@ -1,852 +1,852 @@
1
- import { ContainerRegistry, } from './container-registry.js';
2
- export class ContainerMatcher {
3
- registry;
4
- constructor(registry = new ContainerRegistry()) {
5
- this.registry = registry;
6
- }
7
- async matchRoot(session, pageContext) {
8
- const url = pageContext?.url;
9
- if (!url) {
10
- throw new Error('page_context.url is required');
11
- }
12
- const containers = this.registry.getContainersForUrl(url);
13
- if (!containers || !Object.keys(containers).length) {
14
- return null;
15
- }
16
- const page = await session.ensurePage(url);
17
- await this.waitForStableDom(page);
18
- const currentUrl = page.url() || url;
19
- const pagePath = this.safePathname(currentUrl);
20
- const rootContainers = Object.entries(containers)
21
- .filter(([containerId]) => !containerId.includes('.'))
22
- .sort((a, b) => this.scoreContainer(b[1]) - this.scoreContainer(a[1]));
23
- for (let attempt = 0; attempt < 3; attempt++) {
24
- for (const [containerId, containerDef] of rootContainers) {
25
- const match = await this.matchContainer(page, containerId, containerDef, currentUrl, pagePath);
26
- if (match) {
27
- return match;
28
- }
29
- }
30
- try {
31
- await page.waitForTimeout(300);
32
- }
33
- catch {
34
- break;
35
- }
36
- }
37
- return null;
38
- }
39
- async inspectTree(session, pageContext, options = {}) {
40
- const url = pageContext?.url;
41
- if (!url) {
42
- throw new Error('page_context.url is required');
43
- }
44
- const containers = this.registry.getContainersForUrl(url);
45
- if (!containers || !Object.keys(containers).length) {
46
- throw new Error('No container definitions available for this URL');
47
- }
48
- const timings = [];
49
- const totalStart = Date.now();
50
- const page = await session.ensurePage(url);
51
- const waitStart = Date.now();
52
- await this.waitForStableDom(page);
53
- timings.push({ step: 'wait_for_dom', duration_ms: Date.now() - waitStart });
54
- const currentUrl = page.url() || url;
55
- const pagePath = this.safePathname(currentUrl);
56
- const maxDepth = this.clampNumber(options.max_depth ?? options.maxDepth ?? 4, 1, 6);
57
- const maxChildren = this.clampNumber(options.max_children ?? options.maxChildren ?? 6, 1, 12);
58
- const preferredRootId = options.root_container_id || options.root_id;
59
- const preferredSelector = options.root_selector;
60
- let rootMatch = null;
61
- const matchStart = Date.now();
62
- if (preferredRootId && containers[preferredRootId]) {
63
- rootMatch = await this.matchContainer(page, preferredRootId, containers[preferredRootId], currentUrl, pagePath);
64
- }
65
- if (!rootMatch) {
66
- rootMatch = await this.matchRoot(session, pageContext);
67
- }
68
- if (!rootMatch) {
69
- throw new Error('No DOM elements matched known containers');
70
- }
71
- timings.push({ step: 'match_root', duration_ms: Date.now() - matchStart });
72
- const effectiveSelector = preferredSelector || rootMatch.container?.matched_selector;
73
- const collectStart = Date.now();
74
- // 仅收集以根容器为起点的子树内的匹配结果,避免对当前页面无关的容器做全量扫描
75
- const subtreeIds = this.collectSubtreeIds(containers, rootMatch.container.id);
76
- const matchMap = await this.collectContainerMatches(page, containers, effectiveSelector, undefined, subtreeIds);
77
- timings.push({ step: 'collect_container_matches', duration_ms: Date.now() - collectStart });
78
- const buildTreeStart = Date.now();
79
- const containerTree = this.buildContainerTree(containers, rootMatch.container.id, matchMap);
80
- timings.push({ step: 'build_container_tree', duration_ms: Date.now() - buildTreeStart });
81
- const domCaptureStart = Date.now();
82
- const matchedPaths = [];
83
- if (matchMap) {
84
- for (const result of Object.values(matchMap)) {
85
- if (result.nodes && Array.isArray(result.nodes)) {
86
- for (const node of result.nodes) {
87
- if (node.dom_path)
88
- matchedPaths.push(node.dom_path);
89
- }
90
- }
91
- }
92
- }
93
- const domTree = await this.captureDomTreeWithRetry(page, effectiveSelector, maxDepth, maxChildren, matchedPaths);
94
- timings.push({ step: 'capture_dom_tree', duration_ms: Date.now() - domCaptureStart });
95
- const annotateStart = Date.now();
96
- const annotations = this.buildDomAnnotations(matchMap);
97
- this.attachDomAnnotations(domTree, annotations);
98
- timings.push({ step: 'annotate_dom', duration_ms: Date.now() - annotateStart });
99
- timings.push({ step: 'total', duration_ms: Date.now() - totalStart });
100
- return {
101
- root_match: rootMatch,
102
- container_tree: containerTree,
103
- dom_tree: domTree,
104
- matches: matchMap,
105
- metadata: {
106
- captured_at: Date.now(),
107
- max_depth: maxDepth,
108
- max_children: maxChildren,
109
- root_selector: effectiveSelector || null,
110
- root_container_id: rootMatch.container.id || null,
111
- page_url: currentUrl,
112
- page_path: pagePath,
113
- timings,
114
- },
115
- };
116
- }
117
- async inspectDomBranch(session, pageContext, options = {}) {
118
- const url = pageContext?.url;
119
- if (!url) {
120
- throw new Error('page_context.url is required');
121
- }
122
- const path = this.normalizeDomPath(options.path || options.dom_path || options.node_path);
123
- if (!path) {
124
- throw new Error('DOM path is required');
125
- }
126
- const page = await session.ensurePage(url);
127
- await this.waitForStableDom(page);
128
- const maxDepth = this.clampNumber(options.max_depth ?? options.maxDepth ?? 4, 1, 6);
129
- const maxChildren = this.clampNumber(options.max_children ?? options.maxChildren ?? 6, 1, 20);
130
- const containers = this.registry.getContainersForUrl(url);
131
- const preferredRootId = options.root_container_id || options.root_id;
132
- const rootSelector = options.root_selector ||
133
- (await this.resolveRootSelector(session, pageContext, containers, preferredRootId));
134
- if (!rootSelector) {
135
- throw new Error('无法确定根容器选择器');
136
- }
137
- const branch = await this.captureDomBranch(page, rootSelector, path, maxDepth, maxChildren);
138
- if (!branch) {
139
- throw new Error('无法捕获 DOM 分支');
140
- }
141
- let annotations = {};
142
- if (containers && Object.keys(containers).length) {
143
- const matchSummary = await this.collectContainerMatches(page, containers, rootSelector, 8);
144
- annotations = this.buildDomAnnotations(matchSummary);
145
- }
146
- this.attachDomAnnotations(branch, annotations);
147
- return {
148
- path,
149
- node: branch,
150
- metadata: {
151
- captured_at: Date.now(),
152
- max_depth: maxDepth,
153
- max_children: maxChildren,
154
- root_selector: rootSelector,
155
- },
156
- };
157
- }
158
- async waitForStableDom(page) {
159
- try {
160
- await page.waitForLoadState('domcontentloaded', { timeout: 20000 });
161
- await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { });
162
- await page.waitForFunction(() => {
163
- const app = document.querySelector('#app');
164
- if (app && app.children.length > 0) {
165
- return true;
166
- }
167
- return document.body?.children?.length > 2;
168
- }, { timeout: 12000 }).catch(() => { });
169
- }
170
- catch { }
171
- }
172
- async matchContainer(page, containerId, container, url, pagePath) {
173
- if (!this.matchesPagePatterns(container, url, pagePath)) {
174
- return null;
175
- }
176
- const selectors = container.selectors || [];
177
- if (!selectors.length) {
178
- console.warn('[container-matcher] container has no selectors', containerId);
179
- return null;
180
- }
181
- for (const selector of selectors) {
182
- const css = this.selectorToCss(selector);
183
- if (!css)
184
- continue;
185
- let handles = [];
186
- try {
187
- handles = await page.$$(css);
188
- }
189
- catch (err) {
190
- console.warn('[container-matcher] selector failed', css, err);
191
- continue;
192
- }
193
- const count = handles.length;
194
- if (!count) {
195
- console.warn('[container-matcher] selector matched 0 nodes', css);
196
- continue;
197
- }
198
- const guards = container.metadata || {};
199
- if (!(await this.evaluateGuards(handles[0], guards))) {
200
- await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
201
- continue;
202
- }
203
- const payload = {
204
- container: {
205
- id: container.id || containerId,
206
- name: container.name,
207
- type: container.type,
208
- matched_selector: css,
209
- match_count: count,
210
- definition: container,
211
- },
212
- match_details: {
213
- container_id: containerId,
214
- selector_variant: selector.variant || 'primary',
215
- selector_classes: selector.classes || [],
216
- matched_selector: css,
217
- page_url: url,
218
- match_count: count,
219
- },
220
- };
221
- await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
222
- return payload;
223
- }
224
- return null;
225
- }
226
- scoreContainer(container) {
227
- const meta = container.metadata || {};
228
- const req = Array.isArray(meta.required_descendants_any) ? meta.required_descendants_any.length : 0;
229
- const excl = Array.isArray(meta.excluded_descendants_any) ? meta.excluded_descendants_any.length : 0;
230
- const selectors = container.selectors || [];
231
- const specificSelector = selectors.some((s) => {
232
- const css = s.css || '';
233
- return css && css !== '#app';
234
- });
235
- return req * 2 + excl + (specificSelector ? 1 : 0);
236
- }
237
- selectorToCss(selector) {
238
- if (selector.css) {
239
- return selector.css;
240
- }
241
- if (selector.id) {
242
- return `#${selector.id}`;
243
- }
244
- if (selector.classes && selector.classes.length) {
245
- return selector.classes.map((cls) => `.${cls}`).join('');
246
- }
247
- return null;
248
- }
249
- matchesPagePatterns(container, pageUrl, pagePath) {
250
- const host = this.safeHostname(pageUrl);
251
- const patterns = container.page_patterns || container.pagePatterns;
252
- if (!patterns || !patterns.length) {
253
- return true;
254
- }
255
- const includes = [];
256
- const excludes = [];
257
- for (const pattern of patterns) {
258
- if (typeof pattern !== 'string')
259
- continue;
260
- if (pattern.startsWith('!')) {
261
- excludes.push(pattern.slice(1));
262
- }
263
- else {
264
- includes.push(pattern);
265
- }
266
- }
267
- for (const pattern of excludes) {
268
- if (this.valueMatchesPattern(pageUrl, pattern) ||
269
- this.valueMatchesPattern(pagePath, pattern) ||
270
- this.valueMatchesPattern(host, pattern)) {
271
- return false;
272
- }
273
- }
274
- if (!includes.length) {
275
- return true;
276
- }
277
- for (const pattern of includes) {
278
- if (this.valueMatchesPattern(pageUrl, pattern) ||
279
- this.valueMatchesPattern(pagePath, pattern) ||
280
- this.valueMatchesPattern(host, pattern)) {
281
- return true;
282
- }
283
- }
284
- return false;
285
- }
286
- patternMatch(value, pattern) {
287
- if (!pattern)
288
- return false;
289
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
290
- const regex = new RegExp(`^${escaped}$`);
291
- return regex.test(value);
292
- }
293
- valueMatchesPattern(value, pattern) {
294
- if (this.patternMatch(value, pattern)) {
295
- return true;
296
- }
297
- if (!pattern.includes('*') && value.includes(pattern)) {
298
- return true;
299
- }
300
- return false;
301
- }
302
- async evaluateGuards(handle, metadata) {
303
- const req = Array.isArray(metadata?.required_descendants_any)
304
- ? metadata.required_descendants_any
305
- : [];
306
- const excl = Array.isArray(metadata?.excluded_descendants_any)
307
- ? metadata.excluded_descendants_any
308
- : [];
309
- if (!req.length && !excl.length) {
310
- return true;
311
- }
312
- try {
313
- return await handle.evaluate((element, guards) => {
314
- const { reqSelectors, exclSelectors } = guards;
315
- if (Array.isArray(reqSelectors) && reqSelectors.length) {
316
- const hasRequired = reqSelectors.some((sel) => {
317
- try {
318
- return !!element.querySelector(sel);
319
- }
320
- catch {
321
- return false;
322
- }
323
- });
324
- if (!hasRequired) {
325
- return false;
326
- }
327
- }
328
- if (Array.isArray(exclSelectors) && exclSelectors.length) {
329
- const hasExcluded = exclSelectors.some((sel) => {
330
- try {
331
- return !!element.querySelector(sel);
332
- }
333
- catch {
334
- return false;
335
- }
336
- });
337
- if (hasExcluded) {
338
- return false;
339
- }
340
- }
341
- return true;
342
- }, {
343
- reqSelectors: req,
344
- exclSelectors: excl,
345
- });
346
- }
347
- catch {
348
- return false;
349
- }
350
- }
351
- clampNumber(value, min, max) {
352
- if (Number.isNaN(value))
353
- return min;
354
- return Math.max(min, Math.min(max, value));
355
- }
356
- async collectContainerMatches(page, containers, rootSelector, maxNodes = 4, onlyContainerIds) {
357
- const summary = {};
358
- for (const [containerId, container] of Object.entries(containers)) {
359
- if (onlyContainerIds && !onlyContainerIds.has(containerId)) {
360
- continue;
361
- }
362
- const selectors = [];
363
- const nodes = [];
364
- let matchCount = 0;
365
- for (const selector of container.selectors || []) {
366
- const css = this.selectorToCss(selector);
367
- if (!css)
368
- continue;
369
- let handles = [];
370
- try {
371
- handles = await page.$$(css);
372
- }
373
- catch {
374
- continue;
375
- }
376
- const count = handles.length;
377
- if (!count) {
378
- await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
379
- continue;
380
- }
381
- selectors.push(css);
382
- matchCount += count;
383
- for (const handle of handles.slice(0, maxNodes)) {
384
- const info = await this.describeElement(handle, rootSelector);
385
- if (info) {
386
- info.selector = css;
387
- nodes.push(info);
388
- }
389
- }
390
- await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
391
- if (nodes.length >= maxNodes) {
392
- break;
393
- }
394
- }
395
- summary[containerId] = {
396
- container: {
397
- id: container.id || containerId,
398
- name: container.name,
399
- type: container.type,
400
- },
401
- selectors,
402
- match_count: matchCount,
403
- nodes,
404
- };
405
- }
406
- return summary;
407
- }
408
- collectSubtreeIds(containers, rootId) {
409
- const result = new Set();
410
- const visit = (containerId) => {
411
- if (result.has(containerId))
412
- return;
413
- const container = containers[containerId];
414
- if (!container)
415
- return;
416
- result.add(containerId);
417
- const childIds = this.resolveChildIds(containerId, container, containers);
418
- for (const childId of childIds) {
419
- visit(childId);
420
- }
421
- };
422
- visit(rootId);
423
- return result;
424
- }
425
- buildContainerTree(containers, rootId, matchMap) {
426
- const targetRoot = containers[rootId] ? rootId : this.inferFallbackRoot(containers, rootId);
427
- if (!targetRoot) {
428
- return null;
429
- }
430
- const build = (containerId) => {
431
- const container = containers[containerId];
432
- if (!container)
433
- return null;
434
- const childIds = this.resolveChildIds(containerId, container, containers);
435
- const node = {
436
- id: container.id || containerId,
437
- name: container.name,
438
- type: container.type,
439
- capabilities: container.capabilities || [],
440
- // 保留容器定义中的 metadata,供上层 UI 使用(例如 source_dom_path / 设计时信息)
441
- metadata: container.metadata ? { ...container.metadata } : undefined,
442
- selectors: (container.selectors || []).map((sel) => ({
443
- ...sel,
444
- })),
445
- // 将容器定义中的 operations 透传给前端,用于浮窗编辑和演练。
446
- operations: Array.isArray(container.operations)
447
- ? container.operations.map((op) => ({ ...op }))
448
- : [],
449
- match: this.summarizeMatchPayload(containerId, matchMap),
450
- children: [],
451
- };
452
- for (const childId of childIds) {
453
- const childNode = build(childId);
454
- if (childNode) {
455
- node.children.push(childNode);
456
- }
457
- }
458
- return node;
459
- };
460
- return build(targetRoot);
461
- }
462
- resolveChildIds(containerId, container, containers) {
463
- const declared = Array.isArray(container.children) ? container.children : [];
464
- const explicit = declared.filter((child) => Boolean(containers[child]));
465
- if (explicit.length) {
466
- return explicit;
467
- }
468
- const prefix = `${containerId}.`;
469
- const targetDepth = containerId.split('.').length;
470
- const fallback = [];
471
- for (const key of Object.keys(containers)) {
472
- if (!key.startsWith(prefix))
473
- continue;
474
- if (key.split('.').length === targetDepth + 1) {
475
- fallback.push(key);
476
- }
477
- }
478
- return fallback.sort();
479
- }
480
- inferFallbackRoot(containers, preferredId) {
481
- if (preferredId && containers[preferredId]) {
482
- return preferredId;
483
- }
484
- const topLevel = Object.keys(containers).filter((id) => !id.includes('.')).sort();
485
- if (topLevel.length)
486
- return topLevel[0];
487
- const keys = Object.keys(containers).sort();
488
- return keys[0] || null;
489
- }
490
- summarizeMatchPayload(containerId, matchMap) {
491
- const payload = matchMap[containerId] || {};
492
- return {
493
- match_count: payload.match_count || 0,
494
- selectors: payload.selectors || [],
495
- nodes: payload.nodes || [],
496
- };
497
- }
498
- async captureDomTreeWithRetry(page, selector, maxDepth, maxChildren, forcePaths) {
499
- const attempts = [];
500
- if (selector)
501
- attempts.push(selector);
502
- attempts.push('#app', 'body', null);
503
- const tried = new Set();
504
- for (const candidate of attempts) {
505
- const key = candidate ?? '__root__';
506
- if (tried.has(key))
507
- continue;
508
- tried.add(key);
509
- const retries = candidate && candidate === selector ? 5 : 3;
510
- for (let i = 0; i < retries; i++) {
511
- const outline = await this.captureDomTree(page, candidate || undefined, maxDepth, maxChildren, forcePaths);
512
- if (outline) {
513
- return outline;
514
- }
515
- await page.waitForTimeout(250).catch(() => { });
516
- }
517
- }
518
- return this.captureFallbackDomTree(page, maxDepth, maxChildren);
519
- }
520
- async captureDomTree(page, selector, maxDepth, maxChildren, forcePaths) {
521
- const runtimeTree = await page
522
- .evaluate((config) => {
523
- const runtime = window.__camoRuntime;
524
- if (!runtime?.dom?.getBranch) {
525
- return null;
526
- }
527
- return runtime.dom.getBranch('root', {
528
- rootSelector: config.selector || null,
529
- maxDepth: config.maxDepth,
530
- maxChildren: config.maxChildren,
531
- forcePaths: config.forcePaths,
532
- });
533
- }, { selector, maxDepth, maxChildren, forcePaths })
534
- .catch(() => null);
535
- if (runtimeTree?.node) {
536
- const normalized = this.normalizeRuntimeNode(runtimeTree.node);
537
- if (normalized) {
538
- return normalized;
539
- }
540
- }
541
- return this.captureDomTreeLegacy(page, selector, maxDepth, maxChildren);
542
- }
543
- async captureDomTreeLegacy(page, selector, maxDepth, maxChildren) {
544
- try {
545
- return await page.evaluate((config) => {
546
- const target = config.selector ? document.querySelector(config.selector) : document.body;
547
- if (!target)
548
- return null;
549
- const walk = (element, path, depth) => {
550
- const meta = {
551
- path: path.join('/'),
552
- tag: element.tagName,
553
- id: element.id || null,
554
- classes: Array.from(element.classList || []),
555
- childCount: element.children?.length || 0,
556
- textSnippet: (element.textContent || '').trim().slice(0, 80),
557
- children: [],
558
- };
559
- if (depth >= config.maxDepth) {
560
- return meta;
561
- }
562
- const children = Array.from(element.children || []).slice(0, config.maxChildren);
563
- meta.children = children.map((child, idx) => walk(child, path.concat(String(idx)), depth + 1));
564
- return meta;
565
- };
566
- return walk(target, ['root'], 0);
567
- }, {
568
- selector,
569
- maxDepth,
570
- maxChildren,
571
- });
572
- }
573
- catch {
574
- return null;
575
- }
576
- }
577
- async captureFallbackDomTree(page, maxDepth, maxChildren) {
578
- try {
579
- return await page.evaluate((config) => {
580
- const root = document.body || document.documentElement;
581
- if (!root)
582
- return null;
583
- const walk = (element, path, depth) => {
584
- const meta = {
585
- path: path.join('/'),
586
- tag: element.tagName,
587
- id: element.id || null,
588
- classes: Array.from(element.classList || []),
589
- childCount: element.children?.length || 0,
590
- textSnippet: (element.textContent || '').trim().slice(0, 80),
591
- children: [],
592
- };
593
- if (depth >= config.maxDepth) {
594
- return meta;
595
- }
596
- const kids = Array.from(element.children || []).slice(0, config.maxChildren);
597
- meta.children = kids.map((child, idx) => walk(child, path.concat(String(idx)), depth + 1));
598
- return meta;
599
- };
600
- return walk(root, ['root'], 0);
601
- }, {
602
- maxDepth: Math.max(1, Math.min(Number(maxDepth) || 4, 8)),
603
- maxChildren: Math.max(1, Math.min(Number(maxChildren) || 8, 40)),
604
- });
605
- }
606
- catch {
607
- return null;
608
- }
609
- }
610
- buildDomAnnotations(matchMap) {
611
- const annotations = {};
612
- for (const [containerId, payload] of Object.entries(matchMap)) {
613
- const nodes = payload.nodes || [];
614
- for (const node of nodes) {
615
- const path = node.dom_path;
616
- if (!path)
617
- continue;
618
- annotations[path] = annotations[path] || [];
619
- annotations[path].push({
620
- container_id: containerId,
621
- container_name: payload.container?.name || payload.container?.id,
622
- selector: node.selector,
623
- });
624
- }
625
- }
626
- return annotations;
627
- }
628
- attachDomAnnotations(domTree, annotations) {
629
- if (!domTree)
630
- return;
631
- const visit = (node) => {
632
- node.containers = annotations[node.path] || [];
633
- for (const child of node.children || []) {
634
- visit(child);
635
- }
636
- };
637
- visit(domTree);
638
- }
639
- async captureDomBranch(page, selector, path, maxDepth, maxChildren) {
640
- const runtimeBranch = await page
641
- .evaluate((config) => {
642
- const runtime = window.__camoRuntime;
643
- if (!runtime?.dom?.getBranch) {
644
- return null;
645
- }
646
- return runtime.dom.getBranch(config.path, {
647
- rootSelector: config.selector,
648
- maxDepth: config.maxDepth,
649
- maxChildren: config.maxChildren,
650
- });
651
- }, { selector, path, maxDepth, maxChildren })
652
- .catch(() => null);
653
- if (runtimeBranch?.node) {
654
- return this.normalizeRuntimeNode(runtimeBranch.node);
655
- }
656
- return this.captureDomBranchLegacy(page, selector, path, maxDepth, maxChildren);
657
- }
658
- async captureDomBranchLegacy(page, selector, path, maxDepth, maxChildren) {
659
- try {
660
- return await page.evaluate((config) => {
661
- const normalizePath = (raw) => {
662
- if (!raw)
663
- return ['root'];
664
- const tokens = raw.split('/').filter((token) => token.length);
665
- if (!tokens.length || tokens[0] === '__root__') {
666
- tokens[0] = 'root';
667
- }
668
- if (tokens[0] !== 'root') {
669
- tokens.unshift('root');
670
- }
671
- return tokens;
672
- };
673
- const pathParts = normalizePath(config.path);
674
- const root = document.querySelector(config.selector);
675
- if (!root)
676
- return null;
677
- const resolvePath = (node, parts, index) => {
678
- if (!node)
679
- return null;
680
- if (index >= parts.length)
681
- return node;
682
- const targetIdx = Number(parts[index]);
683
- const childNodes = Array.from(node.children ?? []);
684
- if (!Number.isFinite(targetIdx) || targetIdx < 0 || targetIdx >= childNodes.length) {
685
- return null;
686
- }
687
- return resolvePath(childNodes[targetIdx], parts, index + 1);
688
- };
689
- const startNode = resolvePath(root, pathParts, 1);
690
- if (!startNode)
691
- return null;
692
- const walk = (element, pathTokens, depth) => {
693
- const meta = {
694
- path: pathTokens.join('/'),
695
- tag: element.tagName,
696
- id: element.id || null,
697
- classes: Array.from(element.classList || []),
698
- childCount: element.children?.length || 0,
699
- textSnippet: (element.textContent || '').trim().slice(0, 80),
700
- children: [],
701
- };
702
- if (depth >= config.maxDepth) {
703
- return meta;
704
- }
705
- const children = Array.from(element.children || []).slice(0, config.maxChildren);
706
- meta.children = children.map((child, idx) => walk(child, pathTokens.concat(String(idx)), depth + 1));
707
- return meta;
708
- };
709
- return walk(startNode, pathParts, 0);
710
- }, {
711
- selector,
712
- path,
713
- maxDepth,
714
- maxChildren,
715
- });
716
- }
717
- catch {
718
- return null;
719
- }
720
- }
721
- async describeElement(handle, rootSelector) {
722
- try {
723
- return await handle.evaluate((element, selector) => {
724
- const resolvedRoot = selector ? document.querySelector(selector) : null;
725
- const computePath = (root) => {
726
- const indices = [];
727
- let current = element;
728
- let guard = 0;
729
- let foundRoot = false;
730
- while (current && guard < 80) {
731
- if (root && current === root) {
732
- foundRoot = true;
733
- break;
734
- }
735
- const parent = current.parentElement;
736
- if (!parent)
737
- break;
738
- const idx = Array.prototype.indexOf.call(parent.children || [], current);
739
- indices.unshift(String(idx));
740
- current = parent;
741
- guard += 1;
742
- }
743
- return {
744
- path: ['root', ...indices].join('/'),
745
- foundRoot,
746
- };
747
- };
748
- const rootPathInfo = resolvedRoot ? computePath(resolvedRoot) : null;
749
- const useRoot = Boolean(rootPathInfo?.foundRoot);
750
- const domPath = useRoot ? rootPathInfo?.path : null;
751
- const classes = Array.from(element.classList || []);
752
- const snippet = (element.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120);
753
- return {
754
- dom_path: domPath,
755
- dom_root_selector: useRoot ? selector : null,
756
- tag: element.tagName,
757
- id: element.id || null,
758
- classes,
759
- textSnippet: snippet,
760
- };
761
- }, rootSelector || null);
762
- }
763
- catch {
764
- return null;
765
- }
766
- finally {
767
- try {
768
- await handle.dispose();
769
- }
770
- catch {
771
- // ignore
772
- }
773
- }
774
- }
775
- safePathname(raw) {
776
- try {
777
- return new URL(raw).pathname || '/';
778
- }
779
- catch {
780
- return raw;
781
- }
782
- }
783
- safeHostname(raw) {
784
- try {
785
- return new URL(raw).hostname || '';
786
- }
787
- catch {
788
- return '';
789
- }
790
- }
791
- normalizeDomPath(path) {
792
- if (!path)
793
- return null;
794
- const tokens = path.split('/').filter((token) => token.length);
795
- if (!tokens.length)
796
- return 'root';
797
- if (tokens[0] === '__root__') {
798
- tokens[0] = 'root';
799
- }
800
- if (tokens[0] !== 'root') {
801
- tokens.unshift('root');
802
- }
803
- return tokens.join('/');
804
- }
805
- normalizeRuntimeNode(node) {
806
- if (!node)
807
- return null;
808
- const normalizedPath = this.normalizeDomPath(node.path || 'root') || 'root';
809
- const normalized = {
810
- path: normalizedPath,
811
- tag: node.tag ? String(node.tag).toUpperCase() : 'DIV',
812
- id: node.id || null,
813
- classes: Array.isArray(node.classes) ? [...node.classes] : [],
814
- childCount: typeof node.childCount === 'number'
815
- ? node.childCount
816
- : Array.isArray(node.children)
817
- ? node.children.length
818
- : 0,
819
- textSnippet: node.textSnippet || node.text || '',
820
- selector: node.selector || null,
821
- containers: Array.isArray(node.containers) ? [...node.containers] : [],
822
- children: [],
823
- };
824
- if (Array.isArray(node.children)) {
825
- normalized.children = node.children
826
- .map((child, index) => {
827
- if (!child.path) {
828
- child.path = `${normalizedPath}/${index}`;
829
- }
830
- return this.normalizeRuntimeNode(child);
831
- })
832
- .filter(Boolean);
833
- }
834
- return normalized;
835
- }
836
- async resolveRootSelector(session, pageContext, containers, preferredId) {
837
- if (!containers || !Object.keys(containers).length) {
838
- const match = await this.matchRoot(session, pageContext);
839
- return match?.container?.matched_selector || null;
840
- }
841
- if (preferredId && containers[preferredId]) {
842
- const page = await session.ensurePage(pageContext.url);
843
- const match = await this.matchContainer(page, preferredId, containers[preferredId], pageContext.url, this.safePathname(pageContext.url));
844
- if (match?.container?.matched_selector) {
845
- return match.container.matched_selector;
846
- }
847
- }
848
- const rootMatch = await this.matchRoot(session, pageContext);
849
- return rootMatch?.container?.matched_selector || null;
850
- }
851
- }
1
+ import { ContainerRegistry, } from './container-registry.js';
2
+ export class ContainerMatcher {
3
+ registry;
4
+ constructor(registry = new ContainerRegistry()) {
5
+ this.registry = registry;
6
+ }
7
+ async matchRoot(session, pageContext) {
8
+ const url = pageContext?.url;
9
+ if (!url) {
10
+ throw new Error('page_context.url is required');
11
+ }
12
+ const containers = this.registry.getContainersForUrl(url);
13
+ if (!containers || !Object.keys(containers).length) {
14
+ return null;
15
+ }
16
+ const page = await session.ensurePage(url);
17
+ await this.waitForStableDom(page);
18
+ const currentUrl = page.url() || url;
19
+ const pagePath = this.safePathname(currentUrl);
20
+ const rootContainers = Object.entries(containers)
21
+ .filter(([containerId]) => !containerId.includes('.'))
22
+ .sort((a, b) => this.scoreContainer(b[1]) - this.scoreContainer(a[1]));
23
+ for (let attempt = 0; attempt < 3; attempt++) {
24
+ for (const [containerId, containerDef] of rootContainers) {
25
+ const match = await this.matchContainer(page, containerId, containerDef, currentUrl, pagePath);
26
+ if (match) {
27
+ return match;
28
+ }
29
+ }
30
+ try {
31
+ await page.waitForTimeout(300);
32
+ }
33
+ catch {
34
+ break;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ async inspectTree(session, pageContext, options = {}) {
40
+ const url = pageContext?.url;
41
+ if (!url) {
42
+ throw new Error('page_context.url is required');
43
+ }
44
+ const containers = this.registry.getContainersForUrl(url);
45
+ if (!containers || !Object.keys(containers).length) {
46
+ throw new Error('No container definitions available for this URL');
47
+ }
48
+ const timings = [];
49
+ const totalStart = Date.now();
50
+ const page = await session.ensurePage(url);
51
+ const waitStart = Date.now();
52
+ await this.waitForStableDom(page);
53
+ timings.push({ step: 'wait_for_dom', duration_ms: Date.now() - waitStart });
54
+ const currentUrl = page.url() || url;
55
+ const pagePath = this.safePathname(currentUrl);
56
+ const maxDepth = this.clampNumber(options.max_depth ?? options.maxDepth ?? 4, 1, 6);
57
+ const maxChildren = this.clampNumber(options.max_children ?? options.maxChildren ?? 6, 1, 12);
58
+ const preferredRootId = options.root_container_id || options.root_id;
59
+ const preferredSelector = options.root_selector;
60
+ let rootMatch = null;
61
+ const matchStart = Date.now();
62
+ if (preferredRootId && containers[preferredRootId]) {
63
+ rootMatch = await this.matchContainer(page, preferredRootId, containers[preferredRootId], currentUrl, pagePath);
64
+ }
65
+ if (!rootMatch) {
66
+ rootMatch = await this.matchRoot(session, pageContext);
67
+ }
68
+ if (!rootMatch) {
69
+ throw new Error('No DOM elements matched known containers');
70
+ }
71
+ timings.push({ step: 'match_root', duration_ms: Date.now() - matchStart });
72
+ const effectiveSelector = preferredSelector || rootMatch.container?.matched_selector;
73
+ const collectStart = Date.now();
74
+ // 仅收集以根容器为起点的子树内的匹配结果,避免对当前页面无关的容器做全量扫描
75
+ const subtreeIds = this.collectSubtreeIds(containers, rootMatch.container.id);
76
+ const matchMap = await this.collectContainerMatches(page, containers, effectiveSelector, undefined, subtreeIds);
77
+ timings.push({ step: 'collect_container_matches', duration_ms: Date.now() - collectStart });
78
+ const buildTreeStart = Date.now();
79
+ const containerTree = this.buildContainerTree(containers, rootMatch.container.id, matchMap);
80
+ timings.push({ step: 'build_container_tree', duration_ms: Date.now() - buildTreeStart });
81
+ const domCaptureStart = Date.now();
82
+ const matchedPaths = [];
83
+ if (matchMap) {
84
+ for (const result of Object.values(matchMap)) {
85
+ if (result.nodes && Array.isArray(result.nodes)) {
86
+ for (const node of result.nodes) {
87
+ if (node.dom_path)
88
+ matchedPaths.push(node.dom_path);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ const domTree = await this.captureDomTreeWithRetry(page, effectiveSelector, maxDepth, maxChildren, matchedPaths);
94
+ timings.push({ step: 'capture_dom_tree', duration_ms: Date.now() - domCaptureStart });
95
+ const annotateStart = Date.now();
96
+ const annotations = this.buildDomAnnotations(matchMap);
97
+ this.attachDomAnnotations(domTree, annotations);
98
+ timings.push({ step: 'annotate_dom', duration_ms: Date.now() - annotateStart });
99
+ timings.push({ step: 'total', duration_ms: Date.now() - totalStart });
100
+ return {
101
+ root_match: rootMatch,
102
+ container_tree: containerTree,
103
+ dom_tree: domTree,
104
+ matches: matchMap,
105
+ metadata: {
106
+ captured_at: Date.now(),
107
+ max_depth: maxDepth,
108
+ max_children: maxChildren,
109
+ root_selector: effectiveSelector || null,
110
+ root_container_id: rootMatch.container.id || null,
111
+ page_url: currentUrl,
112
+ page_path: pagePath,
113
+ timings,
114
+ },
115
+ };
116
+ }
117
+ async inspectDomBranch(session, pageContext, options = {}) {
118
+ const url = pageContext?.url;
119
+ if (!url) {
120
+ throw new Error('page_context.url is required');
121
+ }
122
+ const path = this.normalizeDomPath(options.path || options.dom_path || options.node_path);
123
+ if (!path) {
124
+ throw new Error('DOM path is required');
125
+ }
126
+ const page = await session.ensurePage(url);
127
+ await this.waitForStableDom(page);
128
+ const maxDepth = this.clampNumber(options.max_depth ?? options.maxDepth ?? 4, 1, 6);
129
+ const maxChildren = this.clampNumber(options.max_children ?? options.maxChildren ?? 6, 1, 20);
130
+ const containers = this.registry.getContainersForUrl(url);
131
+ const preferredRootId = options.root_container_id || options.root_id;
132
+ const rootSelector = options.root_selector ||
133
+ (await this.resolveRootSelector(session, pageContext, containers, preferredRootId));
134
+ if (!rootSelector) {
135
+ throw new Error('无法确定根容器选择器');
136
+ }
137
+ const branch = await this.captureDomBranch(page, rootSelector, path, maxDepth, maxChildren);
138
+ if (!branch) {
139
+ throw new Error('无法捕获 DOM 分支');
140
+ }
141
+ let annotations = {};
142
+ if (containers && Object.keys(containers).length) {
143
+ const matchSummary = await this.collectContainerMatches(page, containers, rootSelector, 8);
144
+ annotations = this.buildDomAnnotations(matchSummary);
145
+ }
146
+ this.attachDomAnnotations(branch, annotations);
147
+ return {
148
+ path,
149
+ node: branch,
150
+ metadata: {
151
+ captured_at: Date.now(),
152
+ max_depth: maxDepth,
153
+ max_children: maxChildren,
154
+ root_selector: rootSelector,
155
+ },
156
+ };
157
+ }
158
+ async waitForStableDom(page) {
159
+ try {
160
+ await page.waitForLoadState('domcontentloaded', { timeout: 20000 });
161
+ await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { });
162
+ await page.waitForFunction(() => {
163
+ const app = document.querySelector('#app');
164
+ if (app && app.children.length > 0) {
165
+ return true;
166
+ }
167
+ return document.body?.children?.length > 2;
168
+ }, { timeout: 12000 }).catch(() => { });
169
+ }
170
+ catch { }
171
+ }
172
+ async matchContainer(page, containerId, container, url, pagePath) {
173
+ if (!this.matchesPagePatterns(container, url, pagePath)) {
174
+ return null;
175
+ }
176
+ const selectors = container.selectors || [];
177
+ if (!selectors.length) {
178
+ console.warn('[container-matcher] container has no selectors', containerId);
179
+ return null;
180
+ }
181
+ for (const selector of selectors) {
182
+ const css = this.selectorToCss(selector);
183
+ if (!css)
184
+ continue;
185
+ let handles = [];
186
+ try {
187
+ handles = await page.$$(css);
188
+ }
189
+ catch (err) {
190
+ console.warn('[container-matcher] selector failed', css, err);
191
+ continue;
192
+ }
193
+ const count = handles.length;
194
+ if (!count) {
195
+ console.warn('[container-matcher] selector matched 0 nodes', css);
196
+ continue;
197
+ }
198
+ const guards = container.metadata || {};
199
+ if (!(await this.evaluateGuards(handles[0], guards))) {
200
+ await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
201
+ continue;
202
+ }
203
+ const payload = {
204
+ container: {
205
+ id: container.id || containerId,
206
+ name: container.name,
207
+ type: container.type,
208
+ matched_selector: css,
209
+ match_count: count,
210
+ definition: container,
211
+ },
212
+ match_details: {
213
+ container_id: containerId,
214
+ selector_variant: selector.variant || 'primary',
215
+ selector_classes: selector.classes || [],
216
+ matched_selector: css,
217
+ page_url: url,
218
+ match_count: count,
219
+ },
220
+ };
221
+ await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
222
+ return payload;
223
+ }
224
+ return null;
225
+ }
226
+ scoreContainer(container) {
227
+ const meta = container.metadata || {};
228
+ const req = Array.isArray(meta.required_descendants_any) ? meta.required_descendants_any.length : 0;
229
+ const excl = Array.isArray(meta.excluded_descendants_any) ? meta.excluded_descendants_any.length : 0;
230
+ const selectors = container.selectors || [];
231
+ const specificSelector = selectors.some((s) => {
232
+ const css = s.css || '';
233
+ return css && css !== '#app';
234
+ });
235
+ return req * 2 + excl + (specificSelector ? 1 : 0);
236
+ }
237
+ selectorToCss(selector) {
238
+ if (selector.css) {
239
+ return selector.css;
240
+ }
241
+ if (selector.id) {
242
+ return `#${selector.id}`;
243
+ }
244
+ if (selector.classes && selector.classes.length) {
245
+ return selector.classes.map((cls) => `.${cls}`).join('');
246
+ }
247
+ return null;
248
+ }
249
+ matchesPagePatterns(container, pageUrl, pagePath) {
250
+ const host = this.safeHostname(pageUrl);
251
+ const patterns = container.page_patterns || container.pagePatterns;
252
+ if (!patterns || !patterns.length) {
253
+ return true;
254
+ }
255
+ const includes = [];
256
+ const excludes = [];
257
+ for (const pattern of patterns) {
258
+ if (typeof pattern !== 'string')
259
+ continue;
260
+ if (pattern.startsWith('!')) {
261
+ excludes.push(pattern.slice(1));
262
+ }
263
+ else {
264
+ includes.push(pattern);
265
+ }
266
+ }
267
+ for (const pattern of excludes) {
268
+ if (this.valueMatchesPattern(pageUrl, pattern) ||
269
+ this.valueMatchesPattern(pagePath, pattern) ||
270
+ this.valueMatchesPattern(host, pattern)) {
271
+ return false;
272
+ }
273
+ }
274
+ if (!includes.length) {
275
+ return true;
276
+ }
277
+ for (const pattern of includes) {
278
+ if (this.valueMatchesPattern(pageUrl, pattern) ||
279
+ this.valueMatchesPattern(pagePath, pattern) ||
280
+ this.valueMatchesPattern(host, pattern)) {
281
+ return true;
282
+ }
283
+ }
284
+ return false;
285
+ }
286
+ patternMatch(value, pattern) {
287
+ if (!pattern)
288
+ return false;
289
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
290
+ const regex = new RegExp(`^${escaped}$`);
291
+ return regex.test(value);
292
+ }
293
+ valueMatchesPattern(value, pattern) {
294
+ if (this.patternMatch(value, pattern)) {
295
+ return true;
296
+ }
297
+ if (!pattern.includes('*') && value.includes(pattern)) {
298
+ return true;
299
+ }
300
+ return false;
301
+ }
302
+ async evaluateGuards(handle, metadata) {
303
+ const req = Array.isArray(metadata?.required_descendants_any)
304
+ ? metadata.required_descendants_any
305
+ : [];
306
+ const excl = Array.isArray(metadata?.excluded_descendants_any)
307
+ ? metadata.excluded_descendants_any
308
+ : [];
309
+ if (!req.length && !excl.length) {
310
+ return true;
311
+ }
312
+ try {
313
+ return await handle.evaluate((element, guards) => {
314
+ const { reqSelectors, exclSelectors } = guards;
315
+ if (Array.isArray(reqSelectors) && reqSelectors.length) {
316
+ const hasRequired = reqSelectors.some((sel) => {
317
+ try {
318
+ return !!element.querySelector(sel);
319
+ }
320
+ catch {
321
+ return false;
322
+ }
323
+ });
324
+ if (!hasRequired) {
325
+ return false;
326
+ }
327
+ }
328
+ if (Array.isArray(exclSelectors) && exclSelectors.length) {
329
+ const hasExcluded = exclSelectors.some((sel) => {
330
+ try {
331
+ return !!element.querySelector(sel);
332
+ }
333
+ catch {
334
+ return false;
335
+ }
336
+ });
337
+ if (hasExcluded) {
338
+ return false;
339
+ }
340
+ }
341
+ return true;
342
+ }, {
343
+ reqSelectors: req,
344
+ exclSelectors: excl,
345
+ });
346
+ }
347
+ catch {
348
+ return false;
349
+ }
350
+ }
351
+ clampNumber(value, min, max) {
352
+ if (Number.isNaN(value))
353
+ return min;
354
+ return Math.max(min, Math.min(max, value));
355
+ }
356
+ async collectContainerMatches(page, containers, rootSelector, maxNodes = 4, onlyContainerIds) {
357
+ const summary = {};
358
+ for (const [containerId, container] of Object.entries(containers)) {
359
+ if (onlyContainerIds && !onlyContainerIds.has(containerId)) {
360
+ continue;
361
+ }
362
+ const selectors = [];
363
+ const nodes = [];
364
+ let matchCount = 0;
365
+ for (const selector of container.selectors || []) {
366
+ const css = this.selectorToCss(selector);
367
+ if (!css)
368
+ continue;
369
+ let handles = [];
370
+ try {
371
+ handles = await page.$$(css);
372
+ }
373
+ catch {
374
+ continue;
375
+ }
376
+ const count = handles.length;
377
+ if (!count) {
378
+ await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
379
+ continue;
380
+ }
381
+ selectors.push(css);
382
+ matchCount += count;
383
+ for (const handle of handles.slice(0, maxNodes)) {
384
+ const info = await this.describeElement(handle, rootSelector);
385
+ if (info) {
386
+ info.selector = css;
387
+ nodes.push(info);
388
+ }
389
+ }
390
+ await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
391
+ if (nodes.length >= maxNodes) {
392
+ break;
393
+ }
394
+ }
395
+ summary[containerId] = {
396
+ container: {
397
+ id: container.id || containerId,
398
+ name: container.name,
399
+ type: container.type,
400
+ },
401
+ selectors,
402
+ match_count: matchCount,
403
+ nodes,
404
+ };
405
+ }
406
+ return summary;
407
+ }
408
+ collectSubtreeIds(containers, rootId) {
409
+ const result = new Set();
410
+ const visit = (containerId) => {
411
+ if (result.has(containerId))
412
+ return;
413
+ const container = containers[containerId];
414
+ if (!container)
415
+ return;
416
+ result.add(containerId);
417
+ const childIds = this.resolveChildIds(containerId, container, containers);
418
+ for (const childId of childIds) {
419
+ visit(childId);
420
+ }
421
+ };
422
+ visit(rootId);
423
+ return result;
424
+ }
425
+ buildContainerTree(containers, rootId, matchMap) {
426
+ const targetRoot = containers[rootId] ? rootId : this.inferFallbackRoot(containers, rootId);
427
+ if (!targetRoot) {
428
+ return null;
429
+ }
430
+ const build = (containerId) => {
431
+ const container = containers[containerId];
432
+ if (!container)
433
+ return null;
434
+ const childIds = this.resolveChildIds(containerId, container, containers);
435
+ const node = {
436
+ id: container.id || containerId,
437
+ name: container.name,
438
+ type: container.type,
439
+ capabilities: container.capabilities || [],
440
+ // 保留容器定义中的 metadata,供上层 UI 使用(例如 source_dom_path / 设计时信息)
441
+ metadata: container.metadata ? { ...container.metadata } : undefined,
442
+ selectors: (container.selectors || []).map((sel) => ({
443
+ ...sel,
444
+ })),
445
+ // 将容器定义中的 operations 透传给前端,用于浮窗编辑和演练。
446
+ operations: Array.isArray(container.operations)
447
+ ? container.operations.map((op) => ({ ...op }))
448
+ : [],
449
+ match: this.summarizeMatchPayload(containerId, matchMap),
450
+ children: [],
451
+ };
452
+ for (const childId of childIds) {
453
+ const childNode = build(childId);
454
+ if (childNode) {
455
+ node.children.push(childNode);
456
+ }
457
+ }
458
+ return node;
459
+ };
460
+ return build(targetRoot);
461
+ }
462
+ resolveChildIds(containerId, container, containers) {
463
+ const declared = Array.isArray(container.children) ? container.children : [];
464
+ const explicit = declared.filter((child) => Boolean(containers[child]));
465
+ if (explicit.length) {
466
+ return explicit;
467
+ }
468
+ const prefix = `${containerId}.`;
469
+ const targetDepth = containerId.split('.').length;
470
+ const fallback = [];
471
+ for (const key of Object.keys(containers)) {
472
+ if (!key.startsWith(prefix))
473
+ continue;
474
+ if (key.split('.').length === targetDepth + 1) {
475
+ fallback.push(key);
476
+ }
477
+ }
478
+ return fallback.sort();
479
+ }
480
+ inferFallbackRoot(containers, preferredId) {
481
+ if (preferredId && containers[preferredId]) {
482
+ return preferredId;
483
+ }
484
+ const topLevel = Object.keys(containers).filter((id) => !id.includes('.')).sort();
485
+ if (topLevel.length)
486
+ return topLevel[0];
487
+ const keys = Object.keys(containers).sort();
488
+ return keys[0] || null;
489
+ }
490
+ summarizeMatchPayload(containerId, matchMap) {
491
+ const payload = matchMap[containerId] || {};
492
+ return {
493
+ match_count: payload.match_count || 0,
494
+ selectors: payload.selectors || [],
495
+ nodes: payload.nodes || [],
496
+ };
497
+ }
498
+ async captureDomTreeWithRetry(page, selector, maxDepth, maxChildren, forcePaths) {
499
+ const attempts = [];
500
+ if (selector)
501
+ attempts.push(selector);
502
+ attempts.push('#app', 'body', null);
503
+ const tried = new Set();
504
+ for (const candidate of attempts) {
505
+ const key = candidate ?? '__root__';
506
+ if (tried.has(key))
507
+ continue;
508
+ tried.add(key);
509
+ const retries = candidate && candidate === selector ? 5 : 3;
510
+ for (let i = 0; i < retries; i++) {
511
+ const outline = await this.captureDomTree(page, candidate || undefined, maxDepth, maxChildren, forcePaths);
512
+ if (outline) {
513
+ return outline;
514
+ }
515
+ await page.waitForTimeout(250).catch(() => { });
516
+ }
517
+ }
518
+ return this.captureFallbackDomTree(page, maxDepth, maxChildren);
519
+ }
520
+ async captureDomTree(page, selector, maxDepth, maxChildren, forcePaths) {
521
+ const runtimeTree = await page
522
+ .evaluate((config) => {
523
+ const runtime = window.__camoRuntime;
524
+ if (!runtime?.dom?.getBranch) {
525
+ return null;
526
+ }
527
+ return runtime.dom.getBranch('root', {
528
+ rootSelector: config.selector || null,
529
+ maxDepth: config.maxDepth,
530
+ maxChildren: config.maxChildren,
531
+ forcePaths: config.forcePaths,
532
+ });
533
+ }, { selector, maxDepth, maxChildren, forcePaths })
534
+ .catch(() => null);
535
+ if (runtimeTree?.node) {
536
+ const normalized = this.normalizeRuntimeNode(runtimeTree.node);
537
+ if (normalized) {
538
+ return normalized;
539
+ }
540
+ }
541
+ return this.captureDomTreeLegacy(page, selector, maxDepth, maxChildren);
542
+ }
543
+ async captureDomTreeLegacy(page, selector, maxDepth, maxChildren) {
544
+ try {
545
+ return await page.evaluate((config) => {
546
+ const target = config.selector ? document.querySelector(config.selector) : document.body;
547
+ if (!target)
548
+ return null;
549
+ const walk = (element, path, depth) => {
550
+ const meta = {
551
+ path: path.join('/'),
552
+ tag: element.tagName,
553
+ id: element.id || null,
554
+ classes: Array.from(element.classList || []),
555
+ childCount: element.children?.length || 0,
556
+ textSnippet: (element.textContent || '').trim().slice(0, 80),
557
+ children: [],
558
+ };
559
+ if (depth >= config.maxDepth) {
560
+ return meta;
561
+ }
562
+ const children = Array.from(element.children || []).slice(0, config.maxChildren);
563
+ meta.children = children.map((child, idx) => walk(child, path.concat(String(idx)), depth + 1));
564
+ return meta;
565
+ };
566
+ return walk(target, ['root'], 0);
567
+ }, {
568
+ selector,
569
+ maxDepth,
570
+ maxChildren,
571
+ });
572
+ }
573
+ catch {
574
+ return null;
575
+ }
576
+ }
577
+ async captureFallbackDomTree(page, maxDepth, maxChildren) {
578
+ try {
579
+ return await page.evaluate((config) => {
580
+ const root = document.body || document.documentElement;
581
+ if (!root)
582
+ return null;
583
+ const walk = (element, path, depth) => {
584
+ const meta = {
585
+ path: path.join('/'),
586
+ tag: element.tagName,
587
+ id: element.id || null,
588
+ classes: Array.from(element.classList || []),
589
+ childCount: element.children?.length || 0,
590
+ textSnippet: (element.textContent || '').trim().slice(0, 80),
591
+ children: [],
592
+ };
593
+ if (depth >= config.maxDepth) {
594
+ return meta;
595
+ }
596
+ const kids = Array.from(element.children || []).slice(0, config.maxChildren);
597
+ meta.children = kids.map((child, idx) => walk(child, path.concat(String(idx)), depth + 1));
598
+ return meta;
599
+ };
600
+ return walk(root, ['root'], 0);
601
+ }, {
602
+ maxDepth: Math.max(1, Math.min(Number(maxDepth) || 4, 8)),
603
+ maxChildren: Math.max(1, Math.min(Number(maxChildren) || 8, 40)),
604
+ });
605
+ }
606
+ catch {
607
+ return null;
608
+ }
609
+ }
610
+ buildDomAnnotations(matchMap) {
611
+ const annotations = {};
612
+ for (const [containerId, payload] of Object.entries(matchMap)) {
613
+ const nodes = payload.nodes || [];
614
+ for (const node of nodes) {
615
+ const path = node.dom_path;
616
+ if (!path)
617
+ continue;
618
+ annotations[path] = annotations[path] || [];
619
+ annotations[path].push({
620
+ container_id: containerId,
621
+ container_name: payload.container?.name || payload.container?.id,
622
+ selector: node.selector,
623
+ });
624
+ }
625
+ }
626
+ return annotations;
627
+ }
628
+ attachDomAnnotations(domTree, annotations) {
629
+ if (!domTree)
630
+ return;
631
+ const visit = (node) => {
632
+ node.containers = annotations[node.path] || [];
633
+ for (const child of node.children || []) {
634
+ visit(child);
635
+ }
636
+ };
637
+ visit(domTree);
638
+ }
639
+ async captureDomBranch(page, selector, path, maxDepth, maxChildren) {
640
+ const runtimeBranch = await page
641
+ .evaluate((config) => {
642
+ const runtime = window.__camoRuntime;
643
+ if (!runtime?.dom?.getBranch) {
644
+ return null;
645
+ }
646
+ return runtime.dom.getBranch(config.path, {
647
+ rootSelector: config.selector,
648
+ maxDepth: config.maxDepth,
649
+ maxChildren: config.maxChildren,
650
+ });
651
+ }, { selector, path, maxDepth, maxChildren })
652
+ .catch(() => null);
653
+ if (runtimeBranch?.node) {
654
+ return this.normalizeRuntimeNode(runtimeBranch.node);
655
+ }
656
+ return this.captureDomBranchLegacy(page, selector, path, maxDepth, maxChildren);
657
+ }
658
+ async captureDomBranchLegacy(page, selector, path, maxDepth, maxChildren) {
659
+ try {
660
+ return await page.evaluate((config) => {
661
+ const normalizePath = (raw) => {
662
+ if (!raw)
663
+ return ['root'];
664
+ const tokens = raw.split('/').filter((token) => token.length);
665
+ if (!tokens.length || tokens[0] === '__root__') {
666
+ tokens[0] = 'root';
667
+ }
668
+ if (tokens[0] !== 'root') {
669
+ tokens.unshift('root');
670
+ }
671
+ return tokens;
672
+ };
673
+ const pathParts = normalizePath(config.path);
674
+ const root = document.querySelector(config.selector);
675
+ if (!root)
676
+ return null;
677
+ const resolvePath = (node, parts, index) => {
678
+ if (!node)
679
+ return null;
680
+ if (index >= parts.length)
681
+ return node;
682
+ const targetIdx = Number(parts[index]);
683
+ const childNodes = Array.from(node.children ?? []);
684
+ if (!Number.isFinite(targetIdx) || targetIdx < 0 || targetIdx >= childNodes.length) {
685
+ return null;
686
+ }
687
+ return resolvePath(childNodes[targetIdx], parts, index + 1);
688
+ };
689
+ const startNode = resolvePath(root, pathParts, 1);
690
+ if (!startNode)
691
+ return null;
692
+ const walk = (element, pathTokens, depth) => {
693
+ const meta = {
694
+ path: pathTokens.join('/'),
695
+ tag: element.tagName,
696
+ id: element.id || null,
697
+ classes: Array.from(element.classList || []),
698
+ childCount: element.children?.length || 0,
699
+ textSnippet: (element.textContent || '').trim().slice(0, 80),
700
+ children: [],
701
+ };
702
+ if (depth >= config.maxDepth) {
703
+ return meta;
704
+ }
705
+ const children = Array.from(element.children || []).slice(0, config.maxChildren);
706
+ meta.children = children.map((child, idx) => walk(child, pathTokens.concat(String(idx)), depth + 1));
707
+ return meta;
708
+ };
709
+ return walk(startNode, pathParts, 0);
710
+ }, {
711
+ selector,
712
+ path,
713
+ maxDepth,
714
+ maxChildren,
715
+ });
716
+ }
717
+ catch {
718
+ return null;
719
+ }
720
+ }
721
+ async describeElement(handle, rootSelector) {
722
+ try {
723
+ return await handle.evaluate((element, selector) => {
724
+ const resolvedRoot = selector ? document.querySelector(selector) : null;
725
+ const computePath = (root) => {
726
+ const indices = [];
727
+ let current = element;
728
+ let guard = 0;
729
+ let foundRoot = false;
730
+ while (current && guard < 80) {
731
+ if (root && current === root) {
732
+ foundRoot = true;
733
+ break;
734
+ }
735
+ const parent = current.parentElement;
736
+ if (!parent)
737
+ break;
738
+ const idx = Array.prototype.indexOf.call(parent.children || [], current);
739
+ indices.unshift(String(idx));
740
+ current = parent;
741
+ guard += 1;
742
+ }
743
+ return {
744
+ path: ['root', ...indices].join('/'),
745
+ foundRoot,
746
+ };
747
+ };
748
+ const rootPathInfo = resolvedRoot ? computePath(resolvedRoot) : null;
749
+ const useRoot = Boolean(rootPathInfo?.foundRoot);
750
+ const domPath = useRoot ? rootPathInfo?.path : null;
751
+ const classes = Array.from(element.classList || []);
752
+ const snippet = (element.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120);
753
+ return {
754
+ dom_path: domPath,
755
+ dom_root_selector: useRoot ? selector : null,
756
+ tag: element.tagName,
757
+ id: element.id || null,
758
+ classes,
759
+ textSnippet: snippet,
760
+ };
761
+ }, rootSelector || null);
762
+ }
763
+ catch {
764
+ return null;
765
+ }
766
+ finally {
767
+ try {
768
+ await handle.dispose();
769
+ }
770
+ catch {
771
+ // ignore
772
+ }
773
+ }
774
+ }
775
+ safePathname(raw) {
776
+ try {
777
+ return new URL(raw).pathname || '/';
778
+ }
779
+ catch {
780
+ return raw;
781
+ }
782
+ }
783
+ safeHostname(raw) {
784
+ try {
785
+ return new URL(raw).hostname || '';
786
+ }
787
+ catch {
788
+ return '';
789
+ }
790
+ }
791
+ normalizeDomPath(path) {
792
+ if (!path)
793
+ return null;
794
+ const tokens = path.split('/').filter((token) => token.length);
795
+ if (!tokens.length)
796
+ return 'root';
797
+ if (tokens[0] === '__root__') {
798
+ tokens[0] = 'root';
799
+ }
800
+ if (tokens[0] !== 'root') {
801
+ tokens.unshift('root');
802
+ }
803
+ return tokens.join('/');
804
+ }
805
+ normalizeRuntimeNode(node) {
806
+ if (!node)
807
+ return null;
808
+ const normalizedPath = this.normalizeDomPath(node.path || 'root') || 'root';
809
+ const normalized = {
810
+ path: normalizedPath,
811
+ tag: node.tag ? String(node.tag).toUpperCase() : 'DIV',
812
+ id: node.id || null,
813
+ classes: Array.isArray(node.classes) ? [...node.classes] : [],
814
+ childCount: typeof node.childCount === 'number'
815
+ ? node.childCount
816
+ : Array.isArray(node.children)
817
+ ? node.children.length
818
+ : 0,
819
+ textSnippet: node.textSnippet || node.text || '',
820
+ selector: node.selector || null,
821
+ containers: Array.isArray(node.containers) ? [...node.containers] : [],
822
+ children: [],
823
+ };
824
+ if (Array.isArray(node.children)) {
825
+ normalized.children = node.children
826
+ .map((child, index) => {
827
+ if (!child.path) {
828
+ child.path = `${normalizedPath}/${index}`;
829
+ }
830
+ return this.normalizeRuntimeNode(child);
831
+ })
832
+ .filter(Boolean);
833
+ }
834
+ return normalized;
835
+ }
836
+ async resolveRootSelector(session, pageContext, containers, preferredId) {
837
+ if (!containers || !Object.keys(containers).length) {
838
+ const match = await this.matchRoot(session, pageContext);
839
+ return match?.container?.matched_selector || null;
840
+ }
841
+ if (preferredId && containers[preferredId]) {
842
+ const page = await session.ensurePage(pageContext.url);
843
+ const match = await this.matchContainer(page, preferredId, containers[preferredId], pageContext.url, this.safePathname(pageContext.url));
844
+ if (match?.container?.matched_selector) {
845
+ return match.container.matched_selector;
846
+ }
847
+ }
848
+ const rootMatch = await this.matchRoot(session, pageContext);
849
+ return rootMatch?.container?.matched_selector || null;
850
+ }
851
+ }
852
852
  //# sourceMappingURL=container-matcher.js.map