@web-auto/camo 0.1.26 → 0.2.1
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.
- package/LICENSE +21 -21
- package/README.md +586 -586
- package/bin/browser-service.mjs +11 -11
- package/bin/camo.mjs +22 -22
- package/package.json +48 -48
- package/scripts/build.mjs +19 -19
- package/scripts/bump-version.mjs +34 -34
- package/scripts/check-file-size.mjs +80 -80
- package/scripts/file-size-policy.json +12 -2
- package/scripts/install.mjs +76 -76
- package/scripts/release.sh +54 -54
- package/src/autoscript/action-providers/index.mjs +6 -6
- package/src/autoscript/impact-engine.mjs +78 -78
- package/src/autoscript/runtime.mjs +1017 -1017
- package/src/autoscript/schema.mjs +376 -376
- package/src/cli.mjs +405 -405
- package/src/commands/attach.mjs +141 -141
- package/src/commands/autoscript.mjs +1011 -1011
- package/src/commands/browser.mjs +1255 -1257
- package/src/commands/container.mjs +401 -401
- package/src/commands/cookies.mjs +69 -69
- package/src/commands/create.mjs +98 -98
- package/src/commands/devtools.mjs +349 -349
- package/src/commands/events.mjs +152 -152
- package/src/commands/highlight-mode.mjs +24 -24
- package/src/commands/init.mjs +68 -68
- package/src/commands/lifecycle.mjs +275 -275
- package/src/commands/mouse.mjs +45 -45
- package/src/commands/profile.mjs +46 -46
- package/src/commands/record.mjs +115 -115
- package/src/commands/system.mjs +14 -14
- package/src/commands/window.mjs +123 -123
- package/src/container/change-notifier.mjs +362 -362
- package/src/container/element-filter.mjs +143 -143
- package/src/container/index.mjs +3 -3
- package/src/container/runtime-core/checkpoint.mjs +209 -209
- package/src/container/runtime-core/index.mjs +21 -21
- package/src/container/runtime-core/operations/index.mjs +774 -774
- package/src/container/runtime-core/operations/selector-scripts.mjs +277 -277
- package/src/container/runtime-core/operations/tab-pool.mjs +746 -746
- package/src/container/runtime-core/operations/viewport.mjs +189 -189
- package/src/container/runtime-core/search.mjs +190 -190
- package/src/container/runtime-core/subscription.mjs +224 -224
- package/src/container/runtime-core/utils.mjs +94 -94
- package/src/container/runtime-core/validation.mjs +127 -184
- package/src/container/runtime-core.mjs +1 -1
- package/src/container/subscription-registry.mjs +459 -459
- package/src/core/actions.mjs +561 -561
- package/src/core/browser.mjs +266 -266
- package/src/core/index.mjs +52 -52
- package/src/core/utils.mjs +91 -91
- package/src/events/daemon-entry.mjs +33 -33
- package/src/events/daemon.mjs +80 -80
- package/src/events/progress-log.mjs +109 -109
- package/src/events/ws-server.mjs +239 -239
- package/src/lib/client.mjs +200 -200
- package/src/lifecycle/cleanup.mjs +83 -83
- package/src/lifecycle/lock.mjs +126 -126
- package/src/lifecycle/session-registry.mjs +279 -279
- package/src/lifecycle/session-view.mjs +76 -76
- package/src/lifecycle/session-watchdog.mjs +281 -281
- package/src/services/browser-service/index.js +671 -674
- package/src/services/browser-service/internal/BrowserSession.input.test.js +389 -389
- package/src/services/browser-service/internal/BrowserSession.js +325 -336
- package/src/services/browser-service/internal/ElementRegistry.js +60 -60
- package/src/services/browser-service/internal/ProfileLock.js +84 -84
- package/src/services/browser-service/internal/SessionManager.js +184 -184
- package/src/services/browser-service/internal/SessionManager.test.js +39 -39
- package/src/services/browser-service/internal/browser-session/cookies.js +144 -144
- package/src/services/browser-service/internal/browser-session/input-ops.js +222 -219
- package/src/services/browser-service/internal/browser-session/input-pipeline.js +144 -144
- package/src/services/browser-service/internal/browser-session/logging.js +46 -46
- package/src/services/browser-service/internal/browser-session/navigation.js +38 -38
- package/src/services/browser-service/internal/browser-session/page-hooks.js +442 -442
- package/src/services/browser-service/internal/browser-session/page-management.js +302 -336
- package/src/services/browser-service/internal/browser-session/page-management.test.js +148 -148
- package/src/services/browser-service/internal/browser-session/recording.js +198 -198
- package/src/services/browser-service/internal/browser-session/runtime-events.js +61 -61
- package/src/services/browser-service/internal/browser-session/session-core.js +84 -84
- package/src/services/browser-service/internal/browser-session/session-state.js +38 -38
- package/src/services/browser-service/internal/browser-session/types.js +14 -14
- package/src/services/browser-service/internal/browser-session/utils.js +95 -95
- package/src/services/browser-service/internal/browser-session/viewport-manager.js +46 -46
- package/src/services/browser-service/internal/browser-session/viewport.js +215 -215
- package/src/services/browser-service/internal/container-matcher.js +851 -851
- package/src/services/browser-service/internal/container-registry.js +182 -182
- package/src/services/browser-service/internal/engine-manager.js +259 -259
- package/src/services/browser-service/internal/fingerprint.js +203 -203
- package/src/services/browser-service/internal/heartbeat.js +137 -137
- package/src/services/browser-service/internal/logging.js +46 -46
- package/src/services/browser-service/internal/page-runtime/runtime.js +1317 -1317
- package/src/services/browser-service/internal/pageRuntime.js +28 -28
- package/src/services/browser-service/internal/runtimeInjector.js +31 -31
- package/src/services/browser-service/internal/service-process-logger.js +140 -140
- package/src/services/browser-service/internal/state-bus.js +45 -45
- package/src/services/browser-service/internal/storage-paths.js +42 -42
- package/src/services/browser-service/internal/ws-server.js +1194 -1194
- package/src/services/browser-service/internal/ws-server.test.js +58 -58
- package/src/services/browser-service/server.mjs +6 -6
- package/src/services/controller/cli-bridge.js +93 -93
- package/src/services/controller/container-index.js +50 -50
- package/src/services/controller/container-storage.js +36 -36
- package/src/services/controller/controller-actions.js +207 -207
- package/src/services/controller/controller.js +1138 -1138
- package/src/services/controller/selectors.js +54 -54
- package/src/services/controller/transport.js +125 -125
- package/src/utils/args.mjs +26 -26
- package/src/utils/browser-service.mjs +544 -544
- package/src/utils/command-log.mjs +64 -64
- package/src/utils/config.mjs +214 -214
- package/src/utils/fingerprint.mjs +181 -181
- package/src/utils/help.mjs +216 -216
- package/src/utils/js-policy.mjs +13 -13
- package/src/utils/ws-client.mjs +30 -30
- package/src/container/runtime-core/operations/tab-pool.mjs.bak +0 -762
- package/src/container/runtime-core/operations/tab-pool.mjs.syntax-error +0 -762
- package/src/services/browser-service/index.js.bak +0 -671
|
@@ -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
|