opensteer 0.4.13 → 0.5.0
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/README.md +5 -0
- package/bin/opensteer.mjs +42 -6
- package/dist/{chunk-QTGJO7RC.js → chunk-QHZFY3ZK.js} +9 -0
- package/dist/{chunk-V4OOJO4S.js → chunk-SPHS6YWD.js} +1 -1
- package/dist/{chunk-JSH3VLMH.js → chunk-SXPIGCSD.js} +1143 -939
- package/dist/{chunk-UIUDSWZV.js → chunk-YIQDOALV.js} +1 -1
- package/dist/cli/server.cjs +1454 -1176
- package/dist/cli/server.js +90 -24
- package/dist/{extractor-I6TJPTXV.js → extractor-CZFCFUME.js} +2 -2
- package/dist/index.cjs +1372 -1161
- package/dist/index.d.cts +24 -31
- package/dist/index.d.ts +24 -31
- package/dist/index.js +4 -6
- package/dist/{resolver-HVZJQZ32.js → resolver-ZREUOOTV.js} +2 -2
- package/package.json +3 -3
|
@@ -2,6 +2,232 @@ import {
|
|
|
2
2
|
flattenExtractionDataToFieldPlan
|
|
3
3
|
} from "./chunk-3H5RRIMZ.js";
|
|
4
4
|
|
|
5
|
+
// src/error-normalization.ts
|
|
6
|
+
function extractErrorMessage(error, fallback = "Unknown error.") {
|
|
7
|
+
if (error instanceof Error) {
|
|
8
|
+
const message = error.message.trim();
|
|
9
|
+
if (message) return message;
|
|
10
|
+
const name = error.name.trim();
|
|
11
|
+
if (name) return name;
|
|
12
|
+
}
|
|
13
|
+
if (typeof error === "string" && error.trim()) {
|
|
14
|
+
return error.trim();
|
|
15
|
+
}
|
|
16
|
+
const record = asRecord(error);
|
|
17
|
+
const recordMessage = toNonEmptyString(record?.message) || toNonEmptyString(record?.error);
|
|
18
|
+
if (recordMessage) {
|
|
19
|
+
return recordMessage;
|
|
20
|
+
}
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
function normalizeError(error, fallback = "Unknown error.", maxCauseDepth = 2) {
|
|
24
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
25
|
+
return normalizeErrorInternal(error, fallback, maxCauseDepth, seen);
|
|
26
|
+
}
|
|
27
|
+
function normalizeErrorInternal(error, fallback, depthRemaining, seen) {
|
|
28
|
+
const record = asRecord(error);
|
|
29
|
+
if (record) {
|
|
30
|
+
if (seen.has(record)) {
|
|
31
|
+
return {
|
|
32
|
+
message: extractErrorMessage(error, fallback)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
seen.add(record);
|
|
36
|
+
}
|
|
37
|
+
const message = extractErrorMessage(error, fallback);
|
|
38
|
+
const code = extractCode(error);
|
|
39
|
+
const name = extractName(error);
|
|
40
|
+
const details = extractDetails(error);
|
|
41
|
+
if (depthRemaining <= 0) {
|
|
42
|
+
return compactErrorInfo({
|
|
43
|
+
message,
|
|
44
|
+
...code ? { code } : {},
|
|
45
|
+
...name ? { name } : {},
|
|
46
|
+
...details ? { details } : {}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const cause = extractCause(error);
|
|
50
|
+
if (!cause) {
|
|
51
|
+
return compactErrorInfo({
|
|
52
|
+
message,
|
|
53
|
+
...code ? { code } : {},
|
|
54
|
+
...name ? { name } : {},
|
|
55
|
+
...details ? { details } : {}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const normalizedCause = normalizeErrorInternal(
|
|
59
|
+
cause,
|
|
60
|
+
"Caused by an unknown error.",
|
|
61
|
+
depthRemaining - 1,
|
|
62
|
+
seen
|
|
63
|
+
);
|
|
64
|
+
return compactErrorInfo({
|
|
65
|
+
message,
|
|
66
|
+
...code ? { code } : {},
|
|
67
|
+
...name ? { name } : {},
|
|
68
|
+
...details ? { details } : {},
|
|
69
|
+
cause: normalizedCause
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function compactErrorInfo(info) {
|
|
73
|
+
const safeDetails = toJsonSafeRecord(info.details);
|
|
74
|
+
return {
|
|
75
|
+
message: info.message,
|
|
76
|
+
...info.code ? { code: info.code } : {},
|
|
77
|
+
...info.name ? { name: info.name } : {},
|
|
78
|
+
...safeDetails ? { details: safeDetails } : {},
|
|
79
|
+
...info.cause ? { cause: info.cause } : {}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function extractCode(error) {
|
|
83
|
+
const record = asRecord(error);
|
|
84
|
+
const raw = record?.code;
|
|
85
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
86
|
+
return raw.trim();
|
|
87
|
+
}
|
|
88
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
89
|
+
return String(raw);
|
|
90
|
+
}
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
function extractName(error) {
|
|
94
|
+
if (error instanceof Error && error.name.trim()) {
|
|
95
|
+
return error.name.trim();
|
|
96
|
+
}
|
|
97
|
+
const record = asRecord(error);
|
|
98
|
+
return toNonEmptyString(record?.name);
|
|
99
|
+
}
|
|
100
|
+
function extractDetails(error) {
|
|
101
|
+
const record = asRecord(error);
|
|
102
|
+
if (!record) return void 0;
|
|
103
|
+
const details = {};
|
|
104
|
+
const rawDetails = asRecord(record.details);
|
|
105
|
+
if (rawDetails) {
|
|
106
|
+
Object.assign(details, rawDetails);
|
|
107
|
+
}
|
|
108
|
+
const action = toNonEmptyString(record.action);
|
|
109
|
+
if (action) {
|
|
110
|
+
details.action = action;
|
|
111
|
+
}
|
|
112
|
+
const selectorUsed = toNonEmptyString(record.selectorUsed);
|
|
113
|
+
if (selectorUsed) {
|
|
114
|
+
details.selectorUsed = selectorUsed;
|
|
115
|
+
}
|
|
116
|
+
if (typeof record.status === "number" && Number.isFinite(record.status)) {
|
|
117
|
+
details.status = record.status;
|
|
118
|
+
}
|
|
119
|
+
const failure = asRecord(record.failure);
|
|
120
|
+
if (failure) {
|
|
121
|
+
const failureCode = toNonEmptyString(failure.code);
|
|
122
|
+
const classificationSource = toNonEmptyString(
|
|
123
|
+
failure.classificationSource
|
|
124
|
+
);
|
|
125
|
+
const failureDetails = asRecord(failure.details);
|
|
126
|
+
if (failureCode || classificationSource || failureDetails) {
|
|
127
|
+
details.actionFailure = {
|
|
128
|
+
...failureCode ? { code: failureCode } : {},
|
|
129
|
+
...classificationSource ? { classificationSource } : {},
|
|
130
|
+
...failureDetails ? { details: failureDetails } : {}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Object.keys(details).length ? details : void 0;
|
|
135
|
+
}
|
|
136
|
+
function extractCause(error) {
|
|
137
|
+
if (error instanceof Error) {
|
|
138
|
+
return error.cause;
|
|
139
|
+
}
|
|
140
|
+
const record = asRecord(error);
|
|
141
|
+
return record?.cause;
|
|
142
|
+
}
|
|
143
|
+
function asRecord(value) {
|
|
144
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
function toNonEmptyString(value) {
|
|
150
|
+
if (typeof value !== "string") return void 0;
|
|
151
|
+
const normalized = value.trim();
|
|
152
|
+
return normalized.length ? normalized : void 0;
|
|
153
|
+
}
|
|
154
|
+
function toJsonSafeRecord(value) {
|
|
155
|
+
if (!value) return void 0;
|
|
156
|
+
const sanitized = toJsonSafeValue(value, /* @__PURE__ */ new WeakSet());
|
|
157
|
+
if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) {
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
const record = sanitized;
|
|
161
|
+
return Object.keys(record).length > 0 ? record : void 0;
|
|
162
|
+
}
|
|
163
|
+
function toJsonSafeValue(value, seen) {
|
|
164
|
+
if (value === null) return null;
|
|
165
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
if (typeof value === "number") {
|
|
169
|
+
return Number.isFinite(value) ? value : null;
|
|
170
|
+
}
|
|
171
|
+
if (typeof value === "bigint") {
|
|
172
|
+
return value.toString();
|
|
173
|
+
}
|
|
174
|
+
if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
|
|
175
|
+
return void 0;
|
|
176
|
+
}
|
|
177
|
+
if (value instanceof Date) {
|
|
178
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
179
|
+
}
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
if (seen.has(value)) return "[Circular]";
|
|
182
|
+
seen.add(value);
|
|
183
|
+
const output = value.map((item) => {
|
|
184
|
+
const next = toJsonSafeValue(item, seen);
|
|
185
|
+
return next === void 0 ? null : next;
|
|
186
|
+
});
|
|
187
|
+
seen.delete(value);
|
|
188
|
+
return output;
|
|
189
|
+
}
|
|
190
|
+
if (value instanceof Set) {
|
|
191
|
+
if (seen.has(value)) return "[Circular]";
|
|
192
|
+
seen.add(value);
|
|
193
|
+
const output = Array.from(value, (item) => {
|
|
194
|
+
const next = toJsonSafeValue(item, seen);
|
|
195
|
+
return next === void 0 ? null : next;
|
|
196
|
+
});
|
|
197
|
+
seen.delete(value);
|
|
198
|
+
return output;
|
|
199
|
+
}
|
|
200
|
+
if (value instanceof Map) {
|
|
201
|
+
if (seen.has(value)) return "[Circular]";
|
|
202
|
+
seen.add(value);
|
|
203
|
+
const output = {};
|
|
204
|
+
for (const [key, item] of value.entries()) {
|
|
205
|
+
const normalizedKey = String(key);
|
|
206
|
+
const next = toJsonSafeValue(item, seen);
|
|
207
|
+
if (next !== void 0) {
|
|
208
|
+
output[normalizedKey] = next;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
seen.delete(value);
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
if (typeof value === "object") {
|
|
215
|
+
const objectValue = value;
|
|
216
|
+
if (seen.has(objectValue)) return "[Circular]";
|
|
217
|
+
seen.add(objectValue);
|
|
218
|
+
const output = {};
|
|
219
|
+
for (const [key, item] of Object.entries(objectValue)) {
|
|
220
|
+
const next = toJsonSafeValue(item, seen);
|
|
221
|
+
if (next !== void 0) {
|
|
222
|
+
output[key] = next;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
seen.delete(objectValue);
|
|
226
|
+
return output;
|
|
227
|
+
}
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
5
231
|
// src/storage/namespace.ts
|
|
6
232
|
import path from "path";
|
|
7
233
|
var DEFAULT_NAMESPACE = "default";
|
|
@@ -607,9 +833,11 @@ import path2 from "path";
|
|
|
607
833
|
var LocalSelectorStorage = class {
|
|
608
834
|
rootDir;
|
|
609
835
|
namespace;
|
|
610
|
-
|
|
836
|
+
debug;
|
|
837
|
+
constructor(rootDir, namespace, options = {}) {
|
|
611
838
|
this.rootDir = rootDir;
|
|
612
839
|
this.namespace = normalizeNamespace(namespace);
|
|
840
|
+
this.debug = options.debug === true;
|
|
613
841
|
}
|
|
614
842
|
getRootDir() {
|
|
615
843
|
return this.rootDir;
|
|
@@ -643,7 +871,16 @@ var LocalSelectorStorage = class {
|
|
|
643
871
|
try {
|
|
644
872
|
const raw = fs.readFileSync(file, "utf8");
|
|
645
873
|
return JSON.parse(raw);
|
|
646
|
-
} catch {
|
|
874
|
+
} catch (error) {
|
|
875
|
+
const message = extractErrorMessage(
|
|
876
|
+
error,
|
|
877
|
+
"Unable to parse selector registry JSON."
|
|
878
|
+
);
|
|
879
|
+
if (this.debug) {
|
|
880
|
+
console.warn(
|
|
881
|
+
`[opensteer] failed to read selector registry "${file}": ${message}`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
647
884
|
return createEmptyRegistry(this.namespace);
|
|
648
885
|
}
|
|
649
886
|
}
|
|
@@ -660,7 +897,16 @@ var LocalSelectorStorage = class {
|
|
|
660
897
|
try {
|
|
661
898
|
const raw = fs.readFileSync(file, "utf8");
|
|
662
899
|
return JSON.parse(raw);
|
|
663
|
-
} catch {
|
|
900
|
+
} catch (error) {
|
|
901
|
+
const message = extractErrorMessage(
|
|
902
|
+
error,
|
|
903
|
+
"Unable to parse selector file JSON."
|
|
904
|
+
);
|
|
905
|
+
if (this.debug) {
|
|
906
|
+
console.warn(
|
|
907
|
+
`[opensteer] failed to read selector file "${file}": ${message}`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
664
910
|
return null;
|
|
665
911
|
}
|
|
666
912
|
}
|
|
@@ -946,9 +1192,6 @@ var ENSURE_NAME_SHIM_SCRIPT = `
|
|
|
946
1192
|
`;
|
|
947
1193
|
var OS_FRAME_TOKEN_KEY = "__opensteerFrameToken";
|
|
948
1194
|
var OS_INSTANCE_TOKEN_KEY = "__opensteerInstanceToken";
|
|
949
|
-
var OS_COUNTER_OWNER_KEY = "__opensteerCounterOwner";
|
|
950
|
-
var OS_COUNTER_VALUE_KEY = "__opensteerCounterValue";
|
|
951
|
-
var OS_COUNTER_NEXT_KEY = "__opensteerCounterNext";
|
|
952
1195
|
|
|
953
1196
|
// src/element-path/build.ts
|
|
954
1197
|
var MAX_ATTRIBUTE_VALUE_LENGTH = 300;
|
|
@@ -2739,567 +2982,179 @@ function cleanForAction(html) {
|
|
|
2739
2982
|
return compactHtml(htmlOut);
|
|
2740
2983
|
}
|
|
2741
2984
|
|
|
2742
|
-
// src/
|
|
2743
|
-
|
|
2744
|
-
function
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2985
|
+
// src/html/pipeline.ts
|
|
2986
|
+
import * as cheerio3 from "cheerio";
|
|
2987
|
+
function applyCleaner(mode, html) {
|
|
2988
|
+
switch (mode) {
|
|
2989
|
+
case "clickable":
|
|
2990
|
+
return cleanForClickable(html);
|
|
2991
|
+
case "scrollable":
|
|
2992
|
+
return cleanForScrollable(html);
|
|
2993
|
+
case "extraction":
|
|
2994
|
+
return cleanForExtraction(html);
|
|
2995
|
+
case "full":
|
|
2996
|
+
return cleanForFull(html);
|
|
2997
|
+
case "action":
|
|
2998
|
+
default:
|
|
2999
|
+
return cleanForAction(html);
|
|
2755
3000
|
}
|
|
2756
|
-
const text = rawText.replace(/\s+/g, " ").trim();
|
|
2757
|
-
return text || null;
|
|
2758
3001
|
}
|
|
2759
|
-
function
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
const
|
|
2766
|
-
|
|
2767
|
-
|
|
3002
|
+
async function assignCounters(page, html, nodePaths, nodeMeta) {
|
|
3003
|
+
const $ = cheerio3.load(html, { xmlMode: false });
|
|
3004
|
+
const counterIndex = /* @__PURE__ */ new Map();
|
|
3005
|
+
let nextCounter = 1;
|
|
3006
|
+
const assignedByNodeId = /* @__PURE__ */ new Map();
|
|
3007
|
+
$("*").each(function() {
|
|
3008
|
+
const el = $(this);
|
|
3009
|
+
const nodeId = el.attr(OS_NODE_ID_ATTR);
|
|
3010
|
+
if (!nodeId) return;
|
|
3011
|
+
const counter = nextCounter++;
|
|
3012
|
+
assignedByNodeId.set(nodeId, counter);
|
|
3013
|
+
const path5 = nodePaths.get(nodeId);
|
|
3014
|
+
el.attr("c", String(counter));
|
|
3015
|
+
el.removeAttr(OS_NODE_ID_ATTR);
|
|
3016
|
+
if (path5) {
|
|
3017
|
+
counterIndex.set(counter, cloneElementPath(path5));
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
try {
|
|
3021
|
+
await syncLiveCounters(page, nodeMeta, assignedByNodeId);
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
await clearLiveCounters(page);
|
|
3024
|
+
throw error;
|
|
2768
3025
|
}
|
|
2769
|
-
|
|
3026
|
+
$(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
|
|
3027
|
+
return {
|
|
3028
|
+
html: $.html(),
|
|
3029
|
+
counterIndex
|
|
3030
|
+
};
|
|
2770
3031
|
}
|
|
2771
|
-
function
|
|
2772
|
-
|
|
2773
|
-
if (!
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
(
|
|
2784
|
-
);
|
|
2785
|
-
if (densityCandidates.length) {
|
|
2786
|
-
return densityCandidates.reduce(
|
|
2787
|
-
(best, candidate) => candidate.density > best.density ? candidate : best
|
|
2788
|
-
).url;
|
|
3032
|
+
async function syncLiveCounters(page, nodeMeta, assignedByNodeId) {
|
|
3033
|
+
await clearLiveCounters(page);
|
|
3034
|
+
if (!assignedByNodeId.size) return;
|
|
3035
|
+
const groupedByFrame = /* @__PURE__ */ new Map();
|
|
3036
|
+
for (const [nodeId, counter] of assignedByNodeId.entries()) {
|
|
3037
|
+
const meta = nodeMeta.get(nodeId);
|
|
3038
|
+
if (!meta?.frameToken) continue;
|
|
3039
|
+
const list = groupedByFrame.get(meta.frameToken) || [];
|
|
3040
|
+
list.push({
|
|
3041
|
+
nodeId,
|
|
3042
|
+
counter
|
|
3043
|
+
});
|
|
3044
|
+
groupedByFrame.set(meta.frameToken, list);
|
|
2789
3045
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
if (!url) continue;
|
|
2804
|
-
index = skipWhitespace(text, index);
|
|
2805
|
-
const descriptors = [];
|
|
2806
|
-
while (index < text.length && text[index] !== ",") {
|
|
2807
|
-
const descriptorToken = readDescriptorToken(text, index);
|
|
2808
|
-
if (!descriptorToken.value) {
|
|
2809
|
-
index = descriptorToken.nextIndex;
|
|
2810
|
-
continue;
|
|
3046
|
+
if (!groupedByFrame.size) return;
|
|
3047
|
+
const failures = [];
|
|
3048
|
+
const framesByToken = await mapFramesByToken(page);
|
|
3049
|
+
for (const [frameToken, entries] of groupedByFrame.entries()) {
|
|
3050
|
+
const frame = framesByToken.get(frameToken);
|
|
3051
|
+
if (!frame) {
|
|
3052
|
+
for (const entry of entries) {
|
|
3053
|
+
failures.push({
|
|
3054
|
+
nodeId: entry.nodeId,
|
|
3055
|
+
counter: entry.counter,
|
|
3056
|
+
frameToken,
|
|
3057
|
+
reason: "frame_missing"
|
|
3058
|
+
});
|
|
2811
3059
|
}
|
|
2812
|
-
|
|
2813
|
-
index = descriptorToken.nextIndex;
|
|
2814
|
-
index = skipWhitespace(text, index);
|
|
2815
|
-
}
|
|
2816
|
-
if (index < text.length && text[index] === ",") {
|
|
2817
|
-
index += 1;
|
|
3060
|
+
continue;
|
|
2818
3061
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
3062
|
+
try {
|
|
3063
|
+
const unresolved = await frame.evaluate(
|
|
3064
|
+
({ entries: entries2, nodeAttr }) => {
|
|
3065
|
+
const index = /* @__PURE__ */ new Map();
|
|
3066
|
+
const unresolved2 = [];
|
|
3067
|
+
const walk = (root) => {
|
|
3068
|
+
const children = Array.from(root.children);
|
|
3069
|
+
for (const child of children) {
|
|
3070
|
+
const nodeId = child.getAttribute(nodeAttr);
|
|
3071
|
+
if (nodeId) {
|
|
3072
|
+
const list = index.get(nodeId) || [];
|
|
3073
|
+
list.push(child);
|
|
3074
|
+
index.set(nodeId, list);
|
|
3075
|
+
}
|
|
3076
|
+
walk(child);
|
|
3077
|
+
if (child.shadowRoot) {
|
|
3078
|
+
walk(child.shadowRoot);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
};
|
|
3082
|
+
walk(document);
|
|
3083
|
+
for (const entry of entries2) {
|
|
3084
|
+
const matches = index.get(entry.nodeId) || [];
|
|
3085
|
+
if (matches.length !== 1) {
|
|
3086
|
+
unresolved2.push({
|
|
3087
|
+
nodeId: entry.nodeId,
|
|
3088
|
+
counter: entry.counter,
|
|
3089
|
+
matches: matches.length
|
|
3090
|
+
});
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
matches[0].setAttribute("c", String(entry.counter));
|
|
3094
|
+
}
|
|
3095
|
+
return unresolved2;
|
|
3096
|
+
},
|
|
3097
|
+
{
|
|
3098
|
+
entries,
|
|
3099
|
+
nodeAttr: OS_NODE_ID_ATTR
|
|
2829
3100
|
}
|
|
2830
|
-
|
|
3101
|
+
);
|
|
3102
|
+
for (const entry of unresolved) {
|
|
3103
|
+
failures.push({
|
|
3104
|
+
nodeId: entry.nodeId,
|
|
3105
|
+
counter: entry.counter,
|
|
3106
|
+
frameToken,
|
|
3107
|
+
reason: "match_count",
|
|
3108
|
+
matches: entry.matches
|
|
3109
|
+
});
|
|
2831
3110
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
3111
|
+
} catch {
|
|
3112
|
+
for (const entry of entries) {
|
|
3113
|
+
failures.push({
|
|
3114
|
+
nodeId: entry.nodeId,
|
|
3115
|
+
counter: entry.counter,
|
|
3116
|
+
frameToken,
|
|
3117
|
+
reason: "frame_unavailable"
|
|
3118
|
+
});
|
|
2838
3119
|
}
|
|
2839
3120
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3121
|
+
}
|
|
3122
|
+
if (failures.length) {
|
|
3123
|
+
const preview = failures.slice(0, 3).map((failure) => {
|
|
3124
|
+
const base = `counter ${failure.counter} (nodeId "${failure.nodeId}") in frame "${failure.frameToken}"`;
|
|
3125
|
+
if (failure.reason === "frame_missing") {
|
|
3126
|
+
return `${base} could not be synchronized because the frame is missing.`;
|
|
3127
|
+
}
|
|
3128
|
+
if (failure.reason === "frame_unavailable") {
|
|
3129
|
+
return `${base} could not be synchronized because frame evaluation failed.`;
|
|
3130
|
+
}
|
|
3131
|
+
return `${base} expected exactly one live node but found ${failure.matches ?? 0}.`;
|
|
2844
3132
|
});
|
|
3133
|
+
const remaining = failures.length > 3 ? ` (+${failures.length - 3} more)` : "";
|
|
3134
|
+
throw new Error(
|
|
3135
|
+
`Failed to synchronize snapshot counters with the live DOM: ${preview.join(" ")}${remaining}`
|
|
3136
|
+
);
|
|
2845
3137
|
}
|
|
2846
|
-
return out;
|
|
2847
3138
|
}
|
|
2848
|
-
function
|
|
2849
|
-
const
|
|
2850
|
-
|
|
2851
|
-
|
|
3139
|
+
async function clearLiveCounters(page) {
|
|
3140
|
+
for (const frame of page.frames()) {
|
|
3141
|
+
try {
|
|
3142
|
+
await frame.evaluate(() => {
|
|
3143
|
+
const walk = (root) => {
|
|
3144
|
+
const children = Array.from(root.children);
|
|
3145
|
+
for (const child of children) {
|
|
3146
|
+
child.removeAttribute("c");
|
|
3147
|
+
walk(child);
|
|
3148
|
+
if (child.shadowRoot) {
|
|
3149
|
+
walk(child.shadowRoot);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
};
|
|
3153
|
+
walk(document);
|
|
3154
|
+
});
|
|
3155
|
+
} catch {
|
|
3156
|
+
}
|
|
2852
3157
|
}
|
|
2853
|
-
const text = String(raw || "");
|
|
2854
|
-
const start = skipSeparators(text, 0);
|
|
2855
|
-
if (start >= text.length) return null;
|
|
2856
|
-
const firstToken = readUrlToken(text, start).value.trim();
|
|
2857
|
-
return firstToken || null;
|
|
2858
|
-
}
|
|
2859
|
-
function skipWhitespace(value, index) {
|
|
2860
|
-
let cursor = index;
|
|
2861
|
-
while (cursor < value.length && /\s/.test(value[cursor])) {
|
|
2862
|
-
cursor += 1;
|
|
2863
|
-
}
|
|
2864
|
-
return cursor;
|
|
2865
|
-
}
|
|
2866
|
-
function skipSeparators(value, index) {
|
|
2867
|
-
let cursor = skipWhitespace(value, index);
|
|
2868
|
-
while (cursor < value.length && value[cursor] === ",") {
|
|
2869
|
-
cursor += 1;
|
|
2870
|
-
cursor = skipWhitespace(value, cursor);
|
|
2871
|
-
}
|
|
2872
|
-
return cursor;
|
|
2873
|
-
}
|
|
2874
|
-
function readUrlToken(value, index) {
|
|
2875
|
-
let cursor = index;
|
|
2876
|
-
let out = "";
|
|
2877
|
-
const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
|
|
2878
|
-
while (cursor < value.length) {
|
|
2879
|
-
const char = value[cursor];
|
|
2880
|
-
if (/\s/.test(char)) {
|
|
2881
|
-
break;
|
|
2882
|
-
}
|
|
2883
|
-
if (char === "," && !isDataUrl) {
|
|
2884
|
-
break;
|
|
2885
|
-
}
|
|
2886
|
-
out += char;
|
|
2887
|
-
cursor += 1;
|
|
2888
|
-
}
|
|
2889
|
-
if (isDataUrl && out.endsWith(",") && cursor < value.length) {
|
|
2890
|
-
out = out.slice(0, -1);
|
|
2891
|
-
}
|
|
2892
|
-
return {
|
|
2893
|
-
value: out,
|
|
2894
|
-
nextIndex: cursor
|
|
2895
|
-
};
|
|
2896
|
-
}
|
|
2897
|
-
function readDescriptorToken(value, index) {
|
|
2898
|
-
let cursor = skipWhitespace(value, index);
|
|
2899
|
-
let out = "";
|
|
2900
|
-
while (cursor < value.length) {
|
|
2901
|
-
const char = value[cursor];
|
|
2902
|
-
if (char === "," || /\s/.test(char)) {
|
|
2903
|
-
break;
|
|
2904
|
-
}
|
|
2905
|
-
out += char;
|
|
2906
|
-
cursor += 1;
|
|
2907
|
-
}
|
|
2908
|
-
return {
|
|
2909
|
-
value: out.trim(),
|
|
2910
|
-
nextIndex: cursor
|
|
2911
|
-
};
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
// src/html/counter-runtime.ts
|
|
2915
|
-
var CounterResolutionError = class extends Error {
|
|
2916
|
-
code;
|
|
2917
|
-
constructor(code, message) {
|
|
2918
|
-
super(message);
|
|
2919
|
-
this.name = "CounterResolutionError";
|
|
2920
|
-
this.code = code;
|
|
2921
|
-
}
|
|
2922
|
-
};
|
|
2923
|
-
async function ensureLiveCounters(page, nodeMeta, nodeIds) {
|
|
2924
|
-
const out = /* @__PURE__ */ new Map();
|
|
2925
|
-
if (!nodeIds.length) return out;
|
|
2926
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
2927
|
-
for (const nodeId of nodeIds) {
|
|
2928
|
-
const meta = nodeMeta.get(nodeId);
|
|
2929
|
-
if (!meta) {
|
|
2930
|
-
throw new CounterResolutionError(
|
|
2931
|
-
"ERR_COUNTER_STALE_OR_NOT_FOUND",
|
|
2932
|
-
`Missing metadata for node ${nodeId}. Run snapshot() again.`
|
|
2933
|
-
);
|
|
2934
|
-
}
|
|
2935
|
-
const list = grouped.get(meta.frameToken) || [];
|
|
2936
|
-
list.push({
|
|
2937
|
-
nodeId,
|
|
2938
|
-
instanceToken: meta.instanceToken
|
|
2939
|
-
});
|
|
2940
|
-
grouped.set(meta.frameToken, list);
|
|
2941
|
-
}
|
|
2942
|
-
const framesByToken = await mapFramesByToken(page);
|
|
2943
|
-
let nextCounter = await readGlobalNextCounter(page);
|
|
2944
|
-
const usedCounters = /* @__PURE__ */ new Map();
|
|
2945
|
-
for (const [frameToken, entries] of grouped.entries()) {
|
|
2946
|
-
const frame = framesByToken.get(frameToken);
|
|
2947
|
-
if (!frame) {
|
|
2948
|
-
throw new CounterResolutionError(
|
|
2949
|
-
"ERR_COUNTER_FRAME_UNAVAILABLE",
|
|
2950
|
-
`Counter frame ${frameToken} is unavailable. Run snapshot() again.`
|
|
2951
|
-
);
|
|
2952
|
-
}
|
|
2953
|
-
const result = await frame.evaluate(
|
|
2954
|
-
({
|
|
2955
|
-
entries: entries2,
|
|
2956
|
-
nodeAttr,
|
|
2957
|
-
instanceTokenKey,
|
|
2958
|
-
counterOwnerKey,
|
|
2959
|
-
counterValueKey,
|
|
2960
|
-
startCounter
|
|
2961
|
-
}) => {
|
|
2962
|
-
const helpers = {
|
|
2963
|
-
pushNode(map, node) {
|
|
2964
|
-
const nodeId = node.getAttribute(nodeAttr);
|
|
2965
|
-
if (!nodeId) return;
|
|
2966
|
-
const list = map.get(nodeId) || [];
|
|
2967
|
-
list.push(node);
|
|
2968
|
-
map.set(nodeId, list);
|
|
2969
|
-
},
|
|
2970
|
-
walk(map, root) {
|
|
2971
|
-
const children = Array.from(root.children);
|
|
2972
|
-
for (const child of children) {
|
|
2973
|
-
helpers.pushNode(map, child);
|
|
2974
|
-
helpers.walk(map, child);
|
|
2975
|
-
if (child.shadowRoot) {
|
|
2976
|
-
helpers.walk(map, child.shadowRoot);
|
|
2977
|
-
}
|
|
2978
|
-
}
|
|
2979
|
-
},
|
|
2980
|
-
buildNodeIndex() {
|
|
2981
|
-
const map = /* @__PURE__ */ new Map();
|
|
2982
|
-
helpers.walk(map, document);
|
|
2983
|
-
return map;
|
|
2984
|
-
}
|
|
2985
|
-
};
|
|
2986
|
-
const index = helpers.buildNodeIndex();
|
|
2987
|
-
const assigned = [];
|
|
2988
|
-
const failures = [];
|
|
2989
|
-
let next = Math.max(1, Number(startCounter || 1));
|
|
2990
|
-
for (const entry of entries2) {
|
|
2991
|
-
const matches = index.get(entry.nodeId) || [];
|
|
2992
|
-
if (!matches.length) {
|
|
2993
|
-
failures.push({
|
|
2994
|
-
nodeId: entry.nodeId,
|
|
2995
|
-
reason: "missing"
|
|
2996
|
-
});
|
|
2997
|
-
continue;
|
|
2998
|
-
}
|
|
2999
|
-
if (matches.length !== 1) {
|
|
3000
|
-
failures.push({
|
|
3001
|
-
nodeId: entry.nodeId,
|
|
3002
|
-
reason: "ambiguous"
|
|
3003
|
-
});
|
|
3004
|
-
continue;
|
|
3005
|
-
}
|
|
3006
|
-
const target = matches[0];
|
|
3007
|
-
if (target[instanceTokenKey] !== entry.instanceToken) {
|
|
3008
|
-
failures.push({
|
|
3009
|
-
nodeId: entry.nodeId,
|
|
3010
|
-
reason: "instance_mismatch"
|
|
3011
|
-
});
|
|
3012
|
-
continue;
|
|
3013
|
-
}
|
|
3014
|
-
const owned = target[counterOwnerKey] === true;
|
|
3015
|
-
const runtimeCounter = Number(target[counterValueKey] || 0);
|
|
3016
|
-
if (owned && Number.isFinite(runtimeCounter) && runtimeCounter > 0) {
|
|
3017
|
-
target.setAttribute("c", String(runtimeCounter));
|
|
3018
|
-
assigned.push({
|
|
3019
|
-
nodeId: entry.nodeId,
|
|
3020
|
-
counter: runtimeCounter
|
|
3021
|
-
});
|
|
3022
|
-
continue;
|
|
3023
|
-
}
|
|
3024
|
-
const counter = next++;
|
|
3025
|
-
target.setAttribute("c", String(counter));
|
|
3026
|
-
Object.defineProperty(target, counterOwnerKey, {
|
|
3027
|
-
value: true,
|
|
3028
|
-
writable: true,
|
|
3029
|
-
configurable: true
|
|
3030
|
-
});
|
|
3031
|
-
Object.defineProperty(target, counterValueKey, {
|
|
3032
|
-
value: counter,
|
|
3033
|
-
writable: true,
|
|
3034
|
-
configurable: true
|
|
3035
|
-
});
|
|
3036
|
-
assigned.push({ nodeId: entry.nodeId, counter });
|
|
3037
|
-
}
|
|
3038
|
-
return {
|
|
3039
|
-
assigned,
|
|
3040
|
-
failures,
|
|
3041
|
-
nextCounter: next
|
|
3042
|
-
};
|
|
3043
|
-
},
|
|
3044
|
-
{
|
|
3045
|
-
entries,
|
|
3046
|
-
nodeAttr: OS_NODE_ID_ATTR,
|
|
3047
|
-
instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
|
|
3048
|
-
counterOwnerKey: OS_COUNTER_OWNER_KEY,
|
|
3049
|
-
counterValueKey: OS_COUNTER_VALUE_KEY,
|
|
3050
|
-
startCounter: nextCounter
|
|
3051
|
-
}
|
|
3052
|
-
);
|
|
3053
|
-
if (result.failures.length) {
|
|
3054
|
-
const first = result.failures[0];
|
|
3055
|
-
throw buildCounterFailureError(first.nodeId, first.reason);
|
|
3056
|
-
}
|
|
3057
|
-
nextCounter = result.nextCounter;
|
|
3058
|
-
for (const item of result.assigned) {
|
|
3059
|
-
const existingNode = usedCounters.get(item.counter);
|
|
3060
|
-
if (existingNode && existingNode !== item.nodeId) {
|
|
3061
|
-
throw new CounterResolutionError(
|
|
3062
|
-
"ERR_COUNTER_AMBIGUOUS",
|
|
3063
|
-
`Counter ${item.counter} is assigned to multiple nodes (${existingNode}, ${item.nodeId}). Run snapshot() again.`
|
|
3064
|
-
);
|
|
3065
|
-
}
|
|
3066
|
-
usedCounters.set(item.counter, item.nodeId);
|
|
3067
|
-
out.set(item.nodeId, item.counter);
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
await writeGlobalNextCounter(page, nextCounter);
|
|
3071
|
-
return out;
|
|
3072
|
-
}
|
|
3073
|
-
async function resolveCounterElement(page, snapshot, counter) {
|
|
3074
|
-
const binding = readBinding(snapshot, counter);
|
|
3075
|
-
const framesByToken = await mapFramesByToken(page);
|
|
3076
|
-
const frame = framesByToken.get(binding.frameToken);
|
|
3077
|
-
if (!frame) {
|
|
3078
|
-
throw new CounterResolutionError(
|
|
3079
|
-
"ERR_COUNTER_FRAME_UNAVAILABLE",
|
|
3080
|
-
`Counter ${counter} frame is unavailable. Run snapshot() again.`
|
|
3081
|
-
);
|
|
3082
|
-
}
|
|
3083
|
-
const status = await frame.evaluate(
|
|
3084
|
-
({
|
|
3085
|
-
nodeId,
|
|
3086
|
-
instanceToken,
|
|
3087
|
-
counter: counter2,
|
|
3088
|
-
nodeAttr,
|
|
3089
|
-
instanceTokenKey,
|
|
3090
|
-
counterOwnerKey,
|
|
3091
|
-
counterValueKey
|
|
3092
|
-
}) => {
|
|
3093
|
-
const helpers = {
|
|
3094
|
-
walk(map, root) {
|
|
3095
|
-
const children = Array.from(root.children);
|
|
3096
|
-
for (const child of children) {
|
|
3097
|
-
const id = child.getAttribute(nodeAttr);
|
|
3098
|
-
if (id) {
|
|
3099
|
-
const list = map.get(id) || [];
|
|
3100
|
-
list.push(child);
|
|
3101
|
-
map.set(id, list);
|
|
3102
|
-
}
|
|
3103
|
-
helpers.walk(map, child);
|
|
3104
|
-
if (child.shadowRoot) {
|
|
3105
|
-
helpers.walk(map, child.shadowRoot);
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
},
|
|
3109
|
-
buildNodeIndex() {
|
|
3110
|
-
const map = /* @__PURE__ */ new Map();
|
|
3111
|
-
helpers.walk(map, document);
|
|
3112
|
-
return map;
|
|
3113
|
-
}
|
|
3114
|
-
};
|
|
3115
|
-
const matches = helpers.buildNodeIndex().get(nodeId) || [];
|
|
3116
|
-
if (!matches.length) return "missing";
|
|
3117
|
-
if (matches.length !== 1) return "ambiguous";
|
|
3118
|
-
const target = matches[0];
|
|
3119
|
-
if (target[instanceTokenKey] !== instanceToken) {
|
|
3120
|
-
return "instance_mismatch";
|
|
3121
|
-
}
|
|
3122
|
-
if (target[counterOwnerKey] !== true) {
|
|
3123
|
-
return "instance_mismatch";
|
|
3124
|
-
}
|
|
3125
|
-
if (Number(target[counterValueKey] || 0) !== counter2) {
|
|
3126
|
-
return "instance_mismatch";
|
|
3127
|
-
}
|
|
3128
|
-
if (target.getAttribute("c") !== String(counter2)) {
|
|
3129
|
-
return "instance_mismatch";
|
|
3130
|
-
}
|
|
3131
|
-
return "ok";
|
|
3132
|
-
},
|
|
3133
|
-
{
|
|
3134
|
-
nodeId: binding.nodeId,
|
|
3135
|
-
instanceToken: binding.instanceToken,
|
|
3136
|
-
counter,
|
|
3137
|
-
nodeAttr: OS_NODE_ID_ATTR,
|
|
3138
|
-
instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
|
|
3139
|
-
counterOwnerKey: OS_COUNTER_OWNER_KEY,
|
|
3140
|
-
counterValueKey: OS_COUNTER_VALUE_KEY
|
|
3141
|
-
}
|
|
3142
|
-
);
|
|
3143
|
-
if (status !== "ok") {
|
|
3144
|
-
throw buildCounterFailureError(binding.nodeId, status);
|
|
3145
|
-
}
|
|
3146
|
-
const handle = await frame.evaluateHandle(
|
|
3147
|
-
({ nodeId, nodeAttr }) => {
|
|
3148
|
-
const helpers = {
|
|
3149
|
-
walk(matches, root) {
|
|
3150
|
-
const children = Array.from(root.children);
|
|
3151
|
-
for (const child of children) {
|
|
3152
|
-
if (child.getAttribute(nodeAttr) === nodeId) {
|
|
3153
|
-
matches.push(child);
|
|
3154
|
-
}
|
|
3155
|
-
helpers.walk(matches, child);
|
|
3156
|
-
if (child.shadowRoot) {
|
|
3157
|
-
helpers.walk(matches, child.shadowRoot);
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
},
|
|
3161
|
-
findUniqueNode() {
|
|
3162
|
-
const matches = [];
|
|
3163
|
-
helpers.walk(matches, document);
|
|
3164
|
-
if (matches.length !== 1) return null;
|
|
3165
|
-
return matches[0];
|
|
3166
|
-
}
|
|
3167
|
-
};
|
|
3168
|
-
return helpers.findUniqueNode();
|
|
3169
|
-
},
|
|
3170
|
-
{
|
|
3171
|
-
nodeId: binding.nodeId,
|
|
3172
|
-
nodeAttr: OS_NODE_ID_ATTR
|
|
3173
|
-
}
|
|
3174
|
-
);
|
|
3175
|
-
const element = handle.asElement();
|
|
3176
|
-
if (!element) {
|
|
3177
|
-
await handle.dispose();
|
|
3178
|
-
throw new CounterResolutionError(
|
|
3179
|
-
"ERR_COUNTER_STALE_OR_NOT_FOUND",
|
|
3180
|
-
`Counter ${counter} became stale. Run snapshot() again.`
|
|
3181
|
-
);
|
|
3182
|
-
}
|
|
3183
|
-
return element;
|
|
3184
|
-
}
|
|
3185
|
-
async function resolveCountersBatch(page, snapshot, requests) {
|
|
3186
|
-
const out = {};
|
|
3187
|
-
if (!requests.length) return out;
|
|
3188
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
3189
|
-
for (const request of requests) {
|
|
3190
|
-
const binding = readBinding(snapshot, request.counter);
|
|
3191
|
-
const list = grouped.get(binding.frameToken) || [];
|
|
3192
|
-
list.push({
|
|
3193
|
-
...request,
|
|
3194
|
-
...binding
|
|
3195
|
-
});
|
|
3196
|
-
grouped.set(binding.frameToken, list);
|
|
3197
|
-
}
|
|
3198
|
-
const framesByToken = await mapFramesByToken(page);
|
|
3199
|
-
for (const [frameToken, entries] of grouped.entries()) {
|
|
3200
|
-
const frame = framesByToken.get(frameToken);
|
|
3201
|
-
if (!frame) {
|
|
3202
|
-
throw new CounterResolutionError(
|
|
3203
|
-
"ERR_COUNTER_FRAME_UNAVAILABLE",
|
|
3204
|
-
`Counter frame ${frameToken} is unavailable. Run snapshot() again.`
|
|
3205
|
-
);
|
|
3206
|
-
}
|
|
3207
|
-
const result = await frame.evaluate(
|
|
3208
|
-
({
|
|
3209
|
-
entries: entries2,
|
|
3210
|
-
nodeAttr,
|
|
3211
|
-
instanceTokenKey,
|
|
3212
|
-
counterOwnerKey,
|
|
3213
|
-
counterValueKey
|
|
3214
|
-
}) => {
|
|
3215
|
-
const values = [];
|
|
3216
|
-
const failures = [];
|
|
3217
|
-
const helpers = {
|
|
3218
|
-
walk(map, root) {
|
|
3219
|
-
const children = Array.from(root.children);
|
|
3220
|
-
for (const child of children) {
|
|
3221
|
-
const id = child.getAttribute(nodeAttr);
|
|
3222
|
-
if (id) {
|
|
3223
|
-
const list = map.get(id) || [];
|
|
3224
|
-
list.push(child);
|
|
3225
|
-
map.set(id, list);
|
|
3226
|
-
}
|
|
3227
|
-
helpers.walk(map, child);
|
|
3228
|
-
if (child.shadowRoot) {
|
|
3229
|
-
helpers.walk(map, child.shadowRoot);
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
},
|
|
3233
|
-
buildNodeIndex() {
|
|
3234
|
-
const map = /* @__PURE__ */ new Map();
|
|
3235
|
-
helpers.walk(map, document);
|
|
3236
|
-
return map;
|
|
3237
|
-
},
|
|
3238
|
-
readRawValue(element, attribute) {
|
|
3239
|
-
if (attribute) {
|
|
3240
|
-
return element.getAttribute(attribute);
|
|
3241
|
-
}
|
|
3242
|
-
return element.textContent;
|
|
3243
|
-
}
|
|
3244
|
-
};
|
|
3245
|
-
const index = helpers.buildNodeIndex();
|
|
3246
|
-
for (const entry of entries2) {
|
|
3247
|
-
const matches = index.get(entry.nodeId) || [];
|
|
3248
|
-
if (!matches.length) {
|
|
3249
|
-
failures.push({
|
|
3250
|
-
nodeId: entry.nodeId,
|
|
3251
|
-
reason: "missing"
|
|
3252
|
-
});
|
|
3253
|
-
continue;
|
|
3254
|
-
}
|
|
3255
|
-
if (matches.length !== 1) {
|
|
3256
|
-
failures.push({
|
|
3257
|
-
nodeId: entry.nodeId,
|
|
3258
|
-
reason: "ambiguous"
|
|
3259
|
-
});
|
|
3260
|
-
continue;
|
|
3261
|
-
}
|
|
3262
|
-
const target = matches[0];
|
|
3263
|
-
if (target[instanceTokenKey] !== entry.instanceToken || target[counterOwnerKey] !== true || Number(target[counterValueKey] || 0) !== entry.counter || target.getAttribute("c") !== String(entry.counter)) {
|
|
3264
|
-
failures.push({
|
|
3265
|
-
nodeId: entry.nodeId,
|
|
3266
|
-
reason: "instance_mismatch"
|
|
3267
|
-
});
|
|
3268
|
-
continue;
|
|
3269
|
-
}
|
|
3270
|
-
values.push({
|
|
3271
|
-
key: entry.key,
|
|
3272
|
-
value: helpers.readRawValue(target, entry.attribute)
|
|
3273
|
-
});
|
|
3274
|
-
}
|
|
3275
|
-
return {
|
|
3276
|
-
values,
|
|
3277
|
-
failures
|
|
3278
|
-
};
|
|
3279
|
-
},
|
|
3280
|
-
{
|
|
3281
|
-
entries,
|
|
3282
|
-
nodeAttr: OS_NODE_ID_ATTR,
|
|
3283
|
-
instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
|
|
3284
|
-
counterOwnerKey: OS_COUNTER_OWNER_KEY,
|
|
3285
|
-
counterValueKey: OS_COUNTER_VALUE_KEY
|
|
3286
|
-
}
|
|
3287
|
-
);
|
|
3288
|
-
if (result.failures.length) {
|
|
3289
|
-
const first = result.failures[0];
|
|
3290
|
-
throw buildCounterFailureError(first.nodeId, first.reason);
|
|
3291
|
-
}
|
|
3292
|
-
const attributeByKey = new Map(
|
|
3293
|
-
entries.map((entry) => [entry.key, entry.attribute])
|
|
3294
|
-
);
|
|
3295
|
-
for (const item of result.values) {
|
|
3296
|
-
out[item.key] = normalizeExtractedValue(
|
|
3297
|
-
item.value,
|
|
3298
|
-
attributeByKey.get(item.key)
|
|
3299
|
-
);
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
3302
|
-
return out;
|
|
3303
3158
|
}
|
|
3304
3159
|
async function mapFramesByToken(page) {
|
|
3305
3160
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -3321,182 +3176,6 @@ async function readFrameToken(frame) {
|
|
|
3321
3176
|
return null;
|
|
3322
3177
|
}
|
|
3323
3178
|
}
|
|
3324
|
-
async function readGlobalNextCounter(page) {
|
|
3325
|
-
const current = await page.mainFrame().evaluate((counterNextKey) => {
|
|
3326
|
-
const win = window;
|
|
3327
|
-
return Number(win[counterNextKey] || 0);
|
|
3328
|
-
}, OS_COUNTER_NEXT_KEY).catch(() => 0);
|
|
3329
|
-
if (Number.isFinite(current) && current > 0) {
|
|
3330
|
-
return current;
|
|
3331
|
-
}
|
|
3332
|
-
let max = 0;
|
|
3333
|
-
for (const frame of page.frames()) {
|
|
3334
|
-
try {
|
|
3335
|
-
const frameMax = await frame.evaluate(
|
|
3336
|
-
({ nodeAttr, counterOwnerKey, counterValueKey }) => {
|
|
3337
|
-
let localMax = 0;
|
|
3338
|
-
const helpers = {
|
|
3339
|
-
walk(root) {
|
|
3340
|
-
const children = Array.from(
|
|
3341
|
-
root.children
|
|
3342
|
-
);
|
|
3343
|
-
for (const child of children) {
|
|
3344
|
-
const candidate = child;
|
|
3345
|
-
const hasNodeId = child.hasAttribute(nodeAttr);
|
|
3346
|
-
const owned = candidate[counterOwnerKey] === true;
|
|
3347
|
-
if (hasNodeId && owned) {
|
|
3348
|
-
const value = Number(
|
|
3349
|
-
candidate[counterValueKey] || 0
|
|
3350
|
-
);
|
|
3351
|
-
if (Number.isFinite(value) && value > localMax) {
|
|
3352
|
-
localMax = value;
|
|
3353
|
-
}
|
|
3354
|
-
}
|
|
3355
|
-
helpers.walk(child);
|
|
3356
|
-
if (child.shadowRoot) {
|
|
3357
|
-
helpers.walk(child.shadowRoot);
|
|
3358
|
-
}
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
};
|
|
3362
|
-
helpers.walk(document);
|
|
3363
|
-
return localMax;
|
|
3364
|
-
},
|
|
3365
|
-
{
|
|
3366
|
-
nodeAttr: OS_NODE_ID_ATTR,
|
|
3367
|
-
counterOwnerKey: OS_COUNTER_OWNER_KEY,
|
|
3368
|
-
counterValueKey: OS_COUNTER_VALUE_KEY
|
|
3369
|
-
}
|
|
3370
|
-
);
|
|
3371
|
-
if (frameMax > max) {
|
|
3372
|
-
max = frameMax;
|
|
3373
|
-
}
|
|
3374
|
-
} catch {
|
|
3375
|
-
}
|
|
3376
|
-
}
|
|
3377
|
-
const next = max + 1;
|
|
3378
|
-
await writeGlobalNextCounter(page, next);
|
|
3379
|
-
return next;
|
|
3380
|
-
}
|
|
3381
|
-
async function writeGlobalNextCounter(page, nextCounter) {
|
|
3382
|
-
await page.mainFrame().evaluate(
|
|
3383
|
-
({ counterNextKey, nextCounter: nextCounter2 }) => {
|
|
3384
|
-
const win = window;
|
|
3385
|
-
win[counterNextKey] = nextCounter2;
|
|
3386
|
-
},
|
|
3387
|
-
{
|
|
3388
|
-
counterNextKey: OS_COUNTER_NEXT_KEY,
|
|
3389
|
-
nextCounter
|
|
3390
|
-
}
|
|
3391
|
-
).catch(() => void 0);
|
|
3392
|
-
}
|
|
3393
|
-
function readBinding(snapshot, counter) {
|
|
3394
|
-
if (!snapshot.counterBindings) {
|
|
3395
|
-
throw new CounterResolutionError(
|
|
3396
|
-
"ERR_COUNTER_NOT_FOUND",
|
|
3397
|
-
`Counter ${counter} is unavailable because this snapshot has no counter bindings. Run snapshot() with counters first.`
|
|
3398
|
-
);
|
|
3399
|
-
}
|
|
3400
|
-
const binding = snapshot.counterBindings.get(counter);
|
|
3401
|
-
if (!binding) {
|
|
3402
|
-
throw new CounterResolutionError(
|
|
3403
|
-
"ERR_COUNTER_NOT_FOUND",
|
|
3404
|
-
`Counter ${counter} was not found in the current snapshot. Run snapshot() again.`
|
|
3405
|
-
);
|
|
3406
|
-
}
|
|
3407
|
-
if (binding.sessionId !== snapshot.snapshotSessionId) {
|
|
3408
|
-
throw new CounterResolutionError(
|
|
3409
|
-
"ERR_COUNTER_STALE_OR_NOT_FOUND",
|
|
3410
|
-
`Counter ${counter} is stale for this snapshot session. Run snapshot() again.`
|
|
3411
|
-
);
|
|
3412
|
-
}
|
|
3413
|
-
return binding;
|
|
3414
|
-
}
|
|
3415
|
-
function buildCounterFailureError(nodeId, reason) {
|
|
3416
|
-
if (reason === "ambiguous") {
|
|
3417
|
-
return new CounterResolutionError(
|
|
3418
|
-
"ERR_COUNTER_AMBIGUOUS",
|
|
3419
|
-
`Counter target is ambiguous for node ${nodeId}. Run snapshot() again.`
|
|
3420
|
-
);
|
|
3421
|
-
}
|
|
3422
|
-
return new CounterResolutionError(
|
|
3423
|
-
"ERR_COUNTER_STALE_OR_NOT_FOUND",
|
|
3424
|
-
`Counter target is stale or missing for node ${nodeId}. Run snapshot() again.`
|
|
3425
|
-
);
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
// src/html/pipeline.ts
|
|
3429
|
-
import * as cheerio3 from "cheerio";
|
|
3430
|
-
import { randomUUID } from "crypto";
|
|
3431
|
-
function applyCleaner(mode, html) {
|
|
3432
|
-
switch (mode) {
|
|
3433
|
-
case "clickable":
|
|
3434
|
-
return cleanForClickable(html);
|
|
3435
|
-
case "scrollable":
|
|
3436
|
-
return cleanForScrollable(html);
|
|
3437
|
-
case "extraction":
|
|
3438
|
-
return cleanForExtraction(html);
|
|
3439
|
-
case "full":
|
|
3440
|
-
return cleanForFull(html);
|
|
3441
|
-
case "action":
|
|
3442
|
-
default:
|
|
3443
|
-
return cleanForAction(html);
|
|
3444
|
-
}
|
|
3445
|
-
}
|
|
3446
|
-
async function assignCounters(page, html, nodePaths, nodeMeta, snapshotSessionId) {
|
|
3447
|
-
const $ = cheerio3.load(html, { xmlMode: false });
|
|
3448
|
-
const counterIndex = /* @__PURE__ */ new Map();
|
|
3449
|
-
const counterBindings = /* @__PURE__ */ new Map();
|
|
3450
|
-
const orderedNodeIds = [];
|
|
3451
|
-
$("*").each(function() {
|
|
3452
|
-
const el = $(this);
|
|
3453
|
-
const nodeId = el.attr(OS_NODE_ID_ATTR);
|
|
3454
|
-
if (!nodeId) return;
|
|
3455
|
-
orderedNodeIds.push(nodeId);
|
|
3456
|
-
});
|
|
3457
|
-
const countersByNodeId = await ensureLiveCounters(
|
|
3458
|
-
page,
|
|
3459
|
-
nodeMeta,
|
|
3460
|
-
orderedNodeIds
|
|
3461
|
-
);
|
|
3462
|
-
$("*").each(function() {
|
|
3463
|
-
const el = $(this);
|
|
3464
|
-
const nodeId = el.attr(OS_NODE_ID_ATTR);
|
|
3465
|
-
if (!nodeId) return;
|
|
3466
|
-
const path5 = nodePaths.get(nodeId);
|
|
3467
|
-
const meta = nodeMeta.get(nodeId);
|
|
3468
|
-
const counter = countersByNodeId.get(nodeId);
|
|
3469
|
-
if (counter == null || !Number.isFinite(counter)) {
|
|
3470
|
-
throw new Error(
|
|
3471
|
-
`Counter assignment failed for node ${nodeId}. Run snapshot() again.`
|
|
3472
|
-
);
|
|
3473
|
-
}
|
|
3474
|
-
if (counterBindings.has(counter) && counterBindings.get(counter)?.nodeId !== nodeId) {
|
|
3475
|
-
throw new Error(
|
|
3476
|
-
`Counter ${counter} was assigned to multiple nodes. Run snapshot() again.`
|
|
3477
|
-
);
|
|
3478
|
-
}
|
|
3479
|
-
el.attr("c", String(counter));
|
|
3480
|
-
el.removeAttr(OS_NODE_ID_ATTR);
|
|
3481
|
-
if (path5) {
|
|
3482
|
-
counterIndex.set(counter, cloneElementPath(path5));
|
|
3483
|
-
}
|
|
3484
|
-
if (meta) {
|
|
3485
|
-
counterBindings.set(counter, {
|
|
3486
|
-
sessionId: snapshotSessionId,
|
|
3487
|
-
frameToken: meta.frameToken,
|
|
3488
|
-
nodeId,
|
|
3489
|
-
instanceToken: meta.instanceToken
|
|
3490
|
-
});
|
|
3491
|
-
}
|
|
3492
|
-
});
|
|
3493
|
-
$(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
|
|
3494
|
-
return {
|
|
3495
|
-
html: $.html(),
|
|
3496
|
-
counterIndex,
|
|
3497
|
-
counterBindings
|
|
3498
|
-
};
|
|
3499
|
-
}
|
|
3500
3179
|
function stripNodeIds(html) {
|
|
3501
3180
|
if (!html.includes(OS_NODE_ID_ATTR)) return html;
|
|
3502
3181
|
const $ = cheerio3.load(html, { xmlMode: false });
|
|
@@ -3504,7 +3183,6 @@ function stripNodeIds(html) {
|
|
|
3504
3183
|
return $.html();
|
|
3505
3184
|
}
|
|
3506
3185
|
async function prepareSnapshot(page, options = {}) {
|
|
3507
|
-
const snapshotSessionId = randomUUID();
|
|
3508
3186
|
const mode = options.mode ?? "action";
|
|
3509
3187
|
const withCounters = options.withCounters ?? true;
|
|
3510
3188
|
const shouldMarkInteractive = options.markInteractive ?? true;
|
|
@@ -3517,18 +3195,15 @@ async function prepareSnapshot(page, options = {}) {
|
|
|
3517
3195
|
const reducedHtml = applyCleaner(mode, processedHtml);
|
|
3518
3196
|
let cleanedHtml = reducedHtml;
|
|
3519
3197
|
let counterIndex = null;
|
|
3520
|
-
let counterBindings = null;
|
|
3521
3198
|
if (withCounters) {
|
|
3522
3199
|
const counted = await assignCounters(
|
|
3523
3200
|
page,
|
|
3524
3201
|
reducedHtml,
|
|
3525
3202
|
serialized.nodePaths,
|
|
3526
|
-
serialized.nodeMeta
|
|
3527
|
-
snapshotSessionId
|
|
3203
|
+
serialized.nodeMeta
|
|
3528
3204
|
);
|
|
3529
3205
|
cleanedHtml = counted.html;
|
|
3530
3206
|
counterIndex = counted.counterIndex;
|
|
3531
|
-
counterBindings = counted.counterBindings;
|
|
3532
3207
|
} else {
|
|
3533
3208
|
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
3534
3209
|
}
|
|
@@ -3537,15 +3212,13 @@ async function prepareSnapshot(page, options = {}) {
|
|
|
3537
3212
|
cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
|
|
3538
3213
|
}
|
|
3539
3214
|
return {
|
|
3540
|
-
snapshotSessionId,
|
|
3541
3215
|
mode,
|
|
3542
3216
|
url: page.url(),
|
|
3543
3217
|
rawHtml,
|
|
3544
3218
|
processedHtml,
|
|
3545
3219
|
reducedHtml,
|
|
3546
3220
|
cleanedHtml,
|
|
3547
|
-
counterIndex
|
|
3548
|
-
counterBindings
|
|
3221
|
+
counterIndex
|
|
3549
3222
|
};
|
|
3550
3223
|
}
|
|
3551
3224
|
|
|
@@ -3674,13 +3347,41 @@ function buildTargetNotFoundMessage(domPath, diagnostics) {
|
|
|
3674
3347
|
return `${base} Tried ${Math.max(diagnostics.length, 0)} candidates.`;
|
|
3675
3348
|
return `${base} Target depth ${depth}. Candidate counts: ${sample}.`;
|
|
3676
3349
|
}
|
|
3677
|
-
function selectInDocument(selectors) {
|
|
3350
|
+
function selectInDocument(selectors) {
|
|
3351
|
+
let fallback = null;
|
|
3352
|
+
for (const selector of selectors) {
|
|
3353
|
+
if (!selector) continue;
|
|
3354
|
+
let count = 0;
|
|
3355
|
+
try {
|
|
3356
|
+
count = document.querySelectorAll(selector).length;
|
|
3357
|
+
} catch {
|
|
3358
|
+
count = 0;
|
|
3359
|
+
}
|
|
3360
|
+
if (count === 1) {
|
|
3361
|
+
return {
|
|
3362
|
+
selector,
|
|
3363
|
+
count,
|
|
3364
|
+
mode: "unique"
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
if (count > 1 && !fallback) {
|
|
3368
|
+
fallback = {
|
|
3369
|
+
selector,
|
|
3370
|
+
count,
|
|
3371
|
+
mode: "fallback"
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
return fallback;
|
|
3376
|
+
}
|
|
3377
|
+
function selectInRoot(root, selectors) {
|
|
3378
|
+
if (!(root instanceof ShadowRoot)) return null;
|
|
3678
3379
|
let fallback = null;
|
|
3679
3380
|
for (const selector of selectors) {
|
|
3680
3381
|
if (!selector) continue;
|
|
3681
3382
|
let count = 0;
|
|
3682
3383
|
try {
|
|
3683
|
-
count =
|
|
3384
|
+
count = root.querySelectorAll(selector).length;
|
|
3684
3385
|
} catch {
|
|
3685
3386
|
count = 0;
|
|
3686
3387
|
}
|
|
@@ -3701,9 +3402,23 @@ function selectInDocument(selectors) {
|
|
|
3701
3402
|
}
|
|
3702
3403
|
return fallback;
|
|
3703
3404
|
}
|
|
3704
|
-
function
|
|
3705
|
-
|
|
3706
|
-
|
|
3405
|
+
function countInDocument(selectors) {
|
|
3406
|
+
const out = [];
|
|
3407
|
+
for (const selector of selectors) {
|
|
3408
|
+
if (!selector) continue;
|
|
3409
|
+
let count = 0;
|
|
3410
|
+
try {
|
|
3411
|
+
count = document.querySelectorAll(selector).length;
|
|
3412
|
+
} catch {
|
|
3413
|
+
count = 0;
|
|
3414
|
+
}
|
|
3415
|
+
out.push({ selector, count });
|
|
3416
|
+
}
|
|
3417
|
+
return out;
|
|
3418
|
+
}
|
|
3419
|
+
function countInRoot(root, selectors) {
|
|
3420
|
+
if (!(root instanceof ShadowRoot)) return [];
|
|
3421
|
+
const out = [];
|
|
3707
3422
|
for (const selector of selectors) {
|
|
3708
3423
|
if (!selector) continue;
|
|
3709
3424
|
let count = 0;
|
|
@@ -3712,73 +3427,432 @@ function selectInRoot(root, selectors) {
|
|
|
3712
3427
|
} catch {
|
|
3713
3428
|
count = 0;
|
|
3714
3429
|
}
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3430
|
+
out.push({ selector, count });
|
|
3431
|
+
}
|
|
3432
|
+
return out;
|
|
3433
|
+
}
|
|
3434
|
+
function isPathDebugEnabled() {
|
|
3435
|
+
const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
|
|
3436
|
+
if (!value) return false;
|
|
3437
|
+
const normalized = value.trim().toLowerCase();
|
|
3438
|
+
return normalized === "1" || normalized === "true";
|
|
3439
|
+
}
|
|
3440
|
+
function debugPath(message, data) {
|
|
3441
|
+
if (!isPathDebugEnabled()) return;
|
|
3442
|
+
if (data !== void 0) {
|
|
3443
|
+
console.log(`[opensteer:path] ${message}`, data);
|
|
3444
|
+
} else {
|
|
3445
|
+
console.log(`[opensteer:path] ${message}`);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
async function disposeHandle(handle) {
|
|
3449
|
+
if (!handle) return;
|
|
3450
|
+
try {
|
|
3451
|
+
await handle.dispose();
|
|
3452
|
+
} catch {
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// src/extract-value-normalization.ts
|
|
3457
|
+
var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
|
|
3458
|
+
function normalizeExtractedValue(raw, attribute) {
|
|
3459
|
+
if (raw == null) return null;
|
|
3460
|
+
const rawText = String(raw);
|
|
3461
|
+
if (!rawText.trim()) return null;
|
|
3462
|
+
const normalizedAttribute = String(attribute || "").trim().toLowerCase();
|
|
3463
|
+
if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
|
|
3464
|
+
const singleValue = pickSingleListAttributeValue(
|
|
3465
|
+
normalizedAttribute,
|
|
3466
|
+
rawText
|
|
3467
|
+
).trim();
|
|
3468
|
+
return singleValue || null;
|
|
3469
|
+
}
|
|
3470
|
+
const text = rawText.replace(/\s+/g, " ").trim();
|
|
3471
|
+
return text || null;
|
|
3472
|
+
}
|
|
3473
|
+
function pickSingleListAttributeValue(attribute, raw) {
|
|
3474
|
+
if (attribute === "ping") {
|
|
3475
|
+
const firstUrl = raw.trim().split(/\s+/)[0] || "";
|
|
3476
|
+
return firstUrl.trim();
|
|
3477
|
+
}
|
|
3478
|
+
if (attribute === "srcset" || attribute === "imagesrcset") {
|
|
3479
|
+
const picked = pickBestSrcsetCandidate(raw);
|
|
3480
|
+
if (picked) return picked;
|
|
3481
|
+
return pickFirstSrcsetToken(raw) || "";
|
|
3482
|
+
}
|
|
3483
|
+
return raw.trim();
|
|
3484
|
+
}
|
|
3485
|
+
function pickBestSrcsetCandidate(raw) {
|
|
3486
|
+
const candidates = parseSrcsetCandidates(raw);
|
|
3487
|
+
if (!candidates.length) return null;
|
|
3488
|
+
const widthCandidates = candidates.filter(
|
|
3489
|
+
(candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
|
|
3490
|
+
);
|
|
3491
|
+
if (widthCandidates.length) {
|
|
3492
|
+
return widthCandidates.reduce(
|
|
3493
|
+
(best, candidate) => candidate.width > best.width ? candidate : best
|
|
3494
|
+
).url;
|
|
3495
|
+
}
|
|
3496
|
+
const densityCandidates = candidates.filter(
|
|
3497
|
+
(candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
|
|
3498
|
+
);
|
|
3499
|
+
if (densityCandidates.length) {
|
|
3500
|
+
return densityCandidates.reduce(
|
|
3501
|
+
(best, candidate) => candidate.density > best.density ? candidate : best
|
|
3502
|
+
).url;
|
|
3503
|
+
}
|
|
3504
|
+
return candidates[0]?.url || null;
|
|
3505
|
+
}
|
|
3506
|
+
function parseSrcsetCandidates(raw) {
|
|
3507
|
+
const text = String(raw || "").trim();
|
|
3508
|
+
if (!text) return [];
|
|
3509
|
+
const out = [];
|
|
3510
|
+
let index = 0;
|
|
3511
|
+
while (index < text.length) {
|
|
3512
|
+
index = skipSeparators(text, index);
|
|
3513
|
+
if (index >= text.length) break;
|
|
3514
|
+
const urlToken = readUrlToken(text, index);
|
|
3515
|
+
index = urlToken.nextIndex;
|
|
3516
|
+
const url = urlToken.value.trim();
|
|
3517
|
+
if (!url) continue;
|
|
3518
|
+
index = skipWhitespace(text, index);
|
|
3519
|
+
const descriptors = [];
|
|
3520
|
+
while (index < text.length && text[index] !== ",") {
|
|
3521
|
+
const descriptorToken = readDescriptorToken(text, index);
|
|
3522
|
+
if (!descriptorToken.value) {
|
|
3523
|
+
index = descriptorToken.nextIndex;
|
|
3524
|
+
continue;
|
|
3525
|
+
}
|
|
3526
|
+
descriptors.push(descriptorToken.value);
|
|
3527
|
+
index = descriptorToken.nextIndex;
|
|
3528
|
+
index = skipWhitespace(text, index);
|
|
3529
|
+
}
|
|
3530
|
+
if (index < text.length && text[index] === ",") {
|
|
3531
|
+
index += 1;
|
|
3532
|
+
}
|
|
3533
|
+
let width = null;
|
|
3534
|
+
let density = null;
|
|
3535
|
+
for (const descriptor of descriptors) {
|
|
3536
|
+
const token = descriptor.trim().toLowerCase();
|
|
3537
|
+
if (!token) continue;
|
|
3538
|
+
const widthMatch = token.match(/^(\d+)w$/);
|
|
3539
|
+
if (widthMatch) {
|
|
3540
|
+
const parsed = Number.parseInt(widthMatch[1], 10);
|
|
3541
|
+
if (Number.isFinite(parsed)) {
|
|
3542
|
+
width = parsed;
|
|
3543
|
+
}
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3546
|
+
const densityMatch = token.match(/^(\d*\.?\d+)x$/);
|
|
3547
|
+
if (densityMatch) {
|
|
3548
|
+
const parsed = Number.parseFloat(densityMatch[1]);
|
|
3549
|
+
if (Number.isFinite(parsed)) {
|
|
3550
|
+
density = parsed;
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
out.push({
|
|
3555
|
+
url,
|
|
3556
|
+
width,
|
|
3557
|
+
density
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
return out;
|
|
3561
|
+
}
|
|
3562
|
+
function pickFirstSrcsetToken(raw) {
|
|
3563
|
+
const candidate = parseSrcsetCandidates(raw)[0];
|
|
3564
|
+
if (candidate?.url) {
|
|
3565
|
+
return candidate.url;
|
|
3566
|
+
}
|
|
3567
|
+
const text = String(raw || "");
|
|
3568
|
+
const start = skipSeparators(text, 0);
|
|
3569
|
+
if (start >= text.length) return null;
|
|
3570
|
+
const firstToken = readUrlToken(text, start).value.trim();
|
|
3571
|
+
return firstToken || null;
|
|
3572
|
+
}
|
|
3573
|
+
function skipWhitespace(value, index) {
|
|
3574
|
+
let cursor = index;
|
|
3575
|
+
while (cursor < value.length && /\s/.test(value[cursor])) {
|
|
3576
|
+
cursor += 1;
|
|
3577
|
+
}
|
|
3578
|
+
return cursor;
|
|
3579
|
+
}
|
|
3580
|
+
function skipSeparators(value, index) {
|
|
3581
|
+
let cursor = skipWhitespace(value, index);
|
|
3582
|
+
while (cursor < value.length && value[cursor] === ",") {
|
|
3583
|
+
cursor += 1;
|
|
3584
|
+
cursor = skipWhitespace(value, cursor);
|
|
3585
|
+
}
|
|
3586
|
+
return cursor;
|
|
3587
|
+
}
|
|
3588
|
+
function readUrlToken(value, index) {
|
|
3589
|
+
let cursor = index;
|
|
3590
|
+
let out = "";
|
|
3591
|
+
const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
|
|
3592
|
+
while (cursor < value.length) {
|
|
3593
|
+
const char = value[cursor];
|
|
3594
|
+
if (/\s/.test(char)) {
|
|
3595
|
+
break;
|
|
3596
|
+
}
|
|
3597
|
+
if (char === "," && !isDataUrl) {
|
|
3598
|
+
break;
|
|
3599
|
+
}
|
|
3600
|
+
out += char;
|
|
3601
|
+
cursor += 1;
|
|
3602
|
+
}
|
|
3603
|
+
if (isDataUrl && out.endsWith(",") && cursor < value.length) {
|
|
3604
|
+
out = out.slice(0, -1);
|
|
3605
|
+
}
|
|
3606
|
+
return {
|
|
3607
|
+
value: out,
|
|
3608
|
+
nextIndex: cursor
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
function readDescriptorToken(value, index) {
|
|
3612
|
+
let cursor = skipWhitespace(value, index);
|
|
3613
|
+
let out = "";
|
|
3614
|
+
while (cursor < value.length) {
|
|
3615
|
+
const char = value[cursor];
|
|
3616
|
+
if (char === "," || /\s/.test(char)) {
|
|
3617
|
+
break;
|
|
3618
|
+
}
|
|
3619
|
+
out += char;
|
|
3620
|
+
cursor += 1;
|
|
3621
|
+
}
|
|
3622
|
+
return {
|
|
3623
|
+
value: out.trim(),
|
|
3624
|
+
nextIndex: cursor
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// src/html/counter-runtime.ts
|
|
3629
|
+
var CounterResolutionError = class extends Error {
|
|
3630
|
+
code;
|
|
3631
|
+
constructor(code, message) {
|
|
3632
|
+
super(message);
|
|
3633
|
+
this.name = "CounterResolutionError";
|
|
3634
|
+
this.code = code;
|
|
3635
|
+
}
|
|
3636
|
+
};
|
|
3637
|
+
async function resolveCounterElement(page, counter) {
|
|
3638
|
+
const normalized = normalizeCounter(counter);
|
|
3639
|
+
if (normalized == null) {
|
|
3640
|
+
throw buildCounterNotFoundError(counter);
|
|
3641
|
+
}
|
|
3642
|
+
const scan = await scanCounterOccurrences(page, [normalized]);
|
|
3643
|
+
const entry = scan.get(normalized);
|
|
3644
|
+
if (!entry || entry.count <= 0 || !entry.frame) {
|
|
3645
|
+
throw buildCounterNotFoundError(counter);
|
|
3646
|
+
}
|
|
3647
|
+
if (entry.count > 1) {
|
|
3648
|
+
throw buildCounterAmbiguousError(counter);
|
|
3649
|
+
}
|
|
3650
|
+
const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
|
|
3651
|
+
const element = handle.asElement();
|
|
3652
|
+
if (!element) {
|
|
3653
|
+
await handle.dispose();
|
|
3654
|
+
throw buildCounterNotFoundError(counter);
|
|
3655
|
+
}
|
|
3656
|
+
return element;
|
|
3657
|
+
}
|
|
3658
|
+
async function resolveCountersBatch(page, requests) {
|
|
3659
|
+
const out = {};
|
|
3660
|
+
if (!requests.length) return out;
|
|
3661
|
+
const counters = dedupeCounters(requests);
|
|
3662
|
+
const scan = await scanCounterOccurrences(page, counters);
|
|
3663
|
+
for (const counter of counters) {
|
|
3664
|
+
const entry = scan.get(counter);
|
|
3665
|
+
if (entry.count > 1) {
|
|
3666
|
+
throw buildCounterAmbiguousError(counter);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
const valueCache = /* @__PURE__ */ new Map();
|
|
3670
|
+
for (const request of requests) {
|
|
3671
|
+
const normalized = normalizeCounter(request.counter);
|
|
3672
|
+
if (normalized == null) {
|
|
3673
|
+
out[request.key] = null;
|
|
3674
|
+
continue;
|
|
3675
|
+
}
|
|
3676
|
+
const entry = scan.get(normalized);
|
|
3677
|
+
if (!entry || entry.count <= 0 || !entry.frame) {
|
|
3678
|
+
out[request.key] = null;
|
|
3679
|
+
continue;
|
|
3721
3680
|
}
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3681
|
+
const cacheKey = `${normalized}:${request.attribute || ""}`;
|
|
3682
|
+
if (valueCache.has(cacheKey)) {
|
|
3683
|
+
out[request.key] = valueCache.get(cacheKey);
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
const read = await readCounterValueInFrame(
|
|
3687
|
+
entry.frame,
|
|
3688
|
+
normalized,
|
|
3689
|
+
request.attribute
|
|
3690
|
+
);
|
|
3691
|
+
if (read.status === "ambiguous") {
|
|
3692
|
+
throw buildCounterAmbiguousError(normalized);
|
|
3693
|
+
}
|
|
3694
|
+
if (read.status === "missing") {
|
|
3695
|
+
valueCache.set(cacheKey, null);
|
|
3696
|
+
out[request.key] = null;
|
|
3697
|
+
continue;
|
|
3728
3698
|
}
|
|
3699
|
+
const normalizedValue = normalizeExtractedValue(
|
|
3700
|
+
read.value ?? null,
|
|
3701
|
+
request.attribute
|
|
3702
|
+
);
|
|
3703
|
+
valueCache.set(cacheKey, normalizedValue);
|
|
3704
|
+
out[request.key] = normalizedValue;
|
|
3729
3705
|
}
|
|
3730
|
-
return
|
|
3706
|
+
return out;
|
|
3731
3707
|
}
|
|
3732
|
-
function
|
|
3708
|
+
function dedupeCounters(requests) {
|
|
3709
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3733
3710
|
const out = [];
|
|
3734
|
-
for (const
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
} catch {
|
|
3740
|
-
count = 0;
|
|
3741
|
-
}
|
|
3742
|
-
out.push({ selector, count });
|
|
3711
|
+
for (const request of requests) {
|
|
3712
|
+
const normalized = normalizeCounter(request.counter);
|
|
3713
|
+
if (normalized == null || seen.has(normalized)) continue;
|
|
3714
|
+
seen.add(normalized);
|
|
3715
|
+
out.push(normalized);
|
|
3743
3716
|
}
|
|
3744
3717
|
return out;
|
|
3745
3718
|
}
|
|
3746
|
-
function
|
|
3747
|
-
if (!(
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3719
|
+
function normalizeCounter(counter) {
|
|
3720
|
+
if (!Number.isFinite(counter)) return null;
|
|
3721
|
+
if (!Number.isInteger(counter)) return null;
|
|
3722
|
+
if (counter <= 0) return null;
|
|
3723
|
+
return counter;
|
|
3724
|
+
}
|
|
3725
|
+
async function scanCounterOccurrences(page, counters) {
|
|
3726
|
+
const out = /* @__PURE__ */ new Map();
|
|
3727
|
+
for (const counter of counters) {
|
|
3728
|
+
out.set(counter, {
|
|
3729
|
+
count: 0,
|
|
3730
|
+
frame: null
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
if (!counters.length) return out;
|
|
3734
|
+
for (const frame of page.frames()) {
|
|
3735
|
+
let frameCounts;
|
|
3752
3736
|
try {
|
|
3753
|
-
|
|
3737
|
+
frameCounts = await frame.evaluate((candidates) => {
|
|
3738
|
+
const keys = new Set(candidates.map((value) => String(value)));
|
|
3739
|
+
const counts = {};
|
|
3740
|
+
for (const key of keys) {
|
|
3741
|
+
counts[key] = 0;
|
|
3742
|
+
}
|
|
3743
|
+
const walk = (root) => {
|
|
3744
|
+
const children = Array.from(root.children);
|
|
3745
|
+
for (const child of children) {
|
|
3746
|
+
const value = child.getAttribute("c");
|
|
3747
|
+
if (value && keys.has(value)) {
|
|
3748
|
+
counts[value] = (counts[value] || 0) + 1;
|
|
3749
|
+
}
|
|
3750
|
+
walk(child);
|
|
3751
|
+
if (child.shadowRoot) {
|
|
3752
|
+
walk(child.shadowRoot);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
walk(document);
|
|
3757
|
+
return counts;
|
|
3758
|
+
}, counters);
|
|
3754
3759
|
} catch {
|
|
3755
|
-
|
|
3760
|
+
continue;
|
|
3761
|
+
}
|
|
3762
|
+
for (const [rawCounter, rawCount] of Object.entries(frameCounts)) {
|
|
3763
|
+
const counter = Number.parseInt(rawCounter, 10);
|
|
3764
|
+
if (!Number.isFinite(counter)) continue;
|
|
3765
|
+
const count = Number(rawCount || 0);
|
|
3766
|
+
if (!Number.isFinite(count) || count <= 0) continue;
|
|
3767
|
+
const entry = out.get(counter);
|
|
3768
|
+
entry.count += count;
|
|
3769
|
+
if (!entry.frame) {
|
|
3770
|
+
entry.frame = frame;
|
|
3771
|
+
}
|
|
3756
3772
|
}
|
|
3757
|
-
out.push({ selector, count });
|
|
3758
3773
|
}
|
|
3759
3774
|
return out;
|
|
3760
3775
|
}
|
|
3761
|
-
function
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3776
|
+
async function resolveUniqueHandleInFrame(frame, counter) {
|
|
3777
|
+
return frame.evaluateHandle((targetCounter) => {
|
|
3778
|
+
const matches = [];
|
|
3779
|
+
const walk = (root) => {
|
|
3780
|
+
const children = Array.from(root.children);
|
|
3781
|
+
for (const child of children) {
|
|
3782
|
+
if (child.getAttribute("c") === targetCounter) {
|
|
3783
|
+
matches.push(child);
|
|
3784
|
+
}
|
|
3785
|
+
walk(child);
|
|
3786
|
+
if (child.shadowRoot) {
|
|
3787
|
+
walk(child.shadowRoot);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
};
|
|
3791
|
+
walk(document);
|
|
3792
|
+
if (matches.length !== 1) {
|
|
3793
|
+
return null;
|
|
3794
|
+
}
|
|
3795
|
+
return matches[0];
|
|
3796
|
+
}, String(counter));
|
|
3774
3797
|
}
|
|
3775
|
-
async function
|
|
3776
|
-
if (!handle) return;
|
|
3798
|
+
async function readCounterValueInFrame(frame, counter, attribute) {
|
|
3777
3799
|
try {
|
|
3778
|
-
await
|
|
3800
|
+
return await frame.evaluate(
|
|
3801
|
+
({ targetCounter, attribute: attribute2 }) => {
|
|
3802
|
+
const matches = [];
|
|
3803
|
+
const walk = (root) => {
|
|
3804
|
+
const children = Array.from(root.children);
|
|
3805
|
+
for (const child of children) {
|
|
3806
|
+
if (child.getAttribute("c") === targetCounter) {
|
|
3807
|
+
matches.push(child);
|
|
3808
|
+
}
|
|
3809
|
+
walk(child);
|
|
3810
|
+
if (child.shadowRoot) {
|
|
3811
|
+
walk(child.shadowRoot);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
};
|
|
3815
|
+
walk(document);
|
|
3816
|
+
if (!matches.length) {
|
|
3817
|
+
return {
|
|
3818
|
+
status: "missing"
|
|
3819
|
+
};
|
|
3820
|
+
}
|
|
3821
|
+
if (matches.length > 1) {
|
|
3822
|
+
return {
|
|
3823
|
+
status: "ambiguous"
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
const target = matches[0];
|
|
3827
|
+
const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
|
|
3828
|
+
return {
|
|
3829
|
+
status: "ok",
|
|
3830
|
+
value
|
|
3831
|
+
};
|
|
3832
|
+
},
|
|
3833
|
+
{
|
|
3834
|
+
targetCounter: String(counter),
|
|
3835
|
+
attribute
|
|
3836
|
+
}
|
|
3837
|
+
);
|
|
3779
3838
|
} catch {
|
|
3839
|
+
return {
|
|
3840
|
+
status: "missing"
|
|
3841
|
+
};
|
|
3780
3842
|
}
|
|
3781
3843
|
}
|
|
3844
|
+
function buildCounterNotFoundError(counter) {
|
|
3845
|
+
return new CounterResolutionError(
|
|
3846
|
+
"ERR_COUNTER_NOT_FOUND",
|
|
3847
|
+
`Counter ${counter} was not found in the live DOM.`
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3850
|
+
function buildCounterAmbiguousError(counter) {
|
|
3851
|
+
return new CounterResolutionError(
|
|
3852
|
+
"ERR_COUNTER_AMBIGUOUS",
|
|
3853
|
+
`Counter ${counter} matches multiple live elements.`
|
|
3854
|
+
);
|
|
3855
|
+
}
|
|
3782
3856
|
|
|
3783
3857
|
// src/actions/actionability-probe.ts
|
|
3784
3858
|
async function probeActionabilityState(element) {
|
|
@@ -3906,7 +3980,7 @@ function defaultActionFailureMessage(action) {
|
|
|
3906
3980
|
function classifyActionFailure(input) {
|
|
3907
3981
|
const typed = classifyTypedError(input.error);
|
|
3908
3982
|
if (typed) return typed;
|
|
3909
|
-
const message =
|
|
3983
|
+
const message = extractErrorMessage2(input.error, input.fallbackMessage);
|
|
3910
3984
|
const fromCallLog = classifyFromPlaywrightMessage(message, input.probe);
|
|
3911
3985
|
if (fromCallLog) return fromCallLog;
|
|
3912
3986
|
const fromProbe = classifyFromProbe(input.probe);
|
|
@@ -3915,7 +3989,7 @@ function classifyActionFailure(input) {
|
|
|
3915
3989
|
if (fromHeuristic) return fromHeuristic;
|
|
3916
3990
|
return buildFailure({
|
|
3917
3991
|
code: "UNKNOWN",
|
|
3918
|
-
message: ensureMessage(input.fallbackMessage
|
|
3992
|
+
message: ensureMessage(message, input.fallbackMessage),
|
|
3919
3993
|
classificationSource: "unknown"
|
|
3920
3994
|
});
|
|
3921
3995
|
}
|
|
@@ -3971,13 +4045,6 @@ function classifyTypedError(error) {
|
|
|
3971
4045
|
classificationSource: "typed_error"
|
|
3972
4046
|
});
|
|
3973
4047
|
}
|
|
3974
|
-
if (error.code === "ERR_COUNTER_FRAME_UNAVAILABLE") {
|
|
3975
|
-
return buildFailure({
|
|
3976
|
-
code: "TARGET_UNAVAILABLE",
|
|
3977
|
-
message: error.message,
|
|
3978
|
-
classificationSource: "typed_error"
|
|
3979
|
-
});
|
|
3980
|
-
}
|
|
3981
4048
|
if (error.code === "ERR_COUNTER_AMBIGUOUS") {
|
|
3982
4049
|
return buildFailure({
|
|
3983
4050
|
code: "TARGET_AMBIGUOUS",
|
|
@@ -3985,13 +4052,6 @@ function classifyTypedError(error) {
|
|
|
3985
4052
|
classificationSource: "typed_error"
|
|
3986
4053
|
});
|
|
3987
4054
|
}
|
|
3988
|
-
if (error.code === "ERR_COUNTER_STALE_OR_NOT_FOUND") {
|
|
3989
|
-
return buildFailure({
|
|
3990
|
-
code: "TARGET_STALE",
|
|
3991
|
-
message: error.message,
|
|
3992
|
-
classificationSource: "typed_error"
|
|
3993
|
-
});
|
|
3994
|
-
}
|
|
3995
4055
|
}
|
|
3996
4056
|
return null;
|
|
3997
4057
|
}
|
|
@@ -4175,13 +4235,22 @@ function defaultRetryableForCode(code) {
|
|
|
4175
4235
|
return true;
|
|
4176
4236
|
}
|
|
4177
4237
|
}
|
|
4178
|
-
function
|
|
4238
|
+
function extractErrorMessage2(error, fallbackMessage) {
|
|
4179
4239
|
if (error instanceof Error && error.message.trim()) {
|
|
4180
4240
|
return error.message;
|
|
4181
4241
|
}
|
|
4182
4242
|
if (typeof error === "string" && error.trim()) {
|
|
4183
4243
|
return error.trim();
|
|
4184
4244
|
}
|
|
4245
|
+
if (error && typeof error === "object" && !Array.isArray(error)) {
|
|
4246
|
+
const record = error;
|
|
4247
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
4248
|
+
return record.message.trim();
|
|
4249
|
+
}
|
|
4250
|
+
if (typeof record.error === "string" && record.error.trim()) {
|
|
4251
|
+
return record.error.trim();
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4185
4254
|
return ensureMessage(fallbackMessage, "Action failed.");
|
|
4186
4255
|
}
|
|
4187
4256
|
function ensureMessage(value, fallback) {
|
|
@@ -5140,7 +5209,8 @@ function withTokenQuery(wsUrl, token) {
|
|
|
5140
5209
|
// src/cloud/local-cache-sync.ts
|
|
5141
5210
|
import fs2 from "fs";
|
|
5142
5211
|
import path3 from "path";
|
|
5143
|
-
function collectLocalSelectorCacheEntries(storage) {
|
|
5212
|
+
function collectLocalSelectorCacheEntries(storage, options = {}) {
|
|
5213
|
+
const debug = options.debug === true;
|
|
5144
5214
|
const namespace = storage.getNamespace();
|
|
5145
5215
|
const namespaceDir = storage.getNamespaceDir();
|
|
5146
5216
|
if (!fs2.existsSync(namespaceDir)) return [];
|
|
@@ -5149,7 +5219,7 @@ function collectLocalSelectorCacheEntries(storage) {
|
|
|
5149
5219
|
for (const fileName of fileNames) {
|
|
5150
5220
|
if (fileName === "index.json" || !fileName.endsWith(".json")) continue;
|
|
5151
5221
|
const filePath = path3.join(namespaceDir, fileName);
|
|
5152
|
-
const selector = readSelectorFile(filePath);
|
|
5222
|
+
const selector = readSelectorFile(filePath, debug);
|
|
5153
5223
|
if (!selector) continue;
|
|
5154
5224
|
const descriptionHash = normalizeDescriptionHash(selector.id);
|
|
5155
5225
|
const method = normalizeMethod(selector.method);
|
|
@@ -5174,11 +5244,20 @@ function collectLocalSelectorCacheEntries(storage) {
|
|
|
5174
5244
|
}
|
|
5175
5245
|
return dedupeNewest(entries);
|
|
5176
5246
|
}
|
|
5177
|
-
function readSelectorFile(filePath) {
|
|
5247
|
+
function readSelectorFile(filePath, debug) {
|
|
5178
5248
|
try {
|
|
5179
5249
|
const raw = fs2.readFileSync(filePath, "utf8");
|
|
5180
5250
|
return JSON.parse(raw);
|
|
5181
|
-
} catch {
|
|
5251
|
+
} catch (error) {
|
|
5252
|
+
const message = extractErrorMessage(
|
|
5253
|
+
error,
|
|
5254
|
+
"Unable to parse selector cache file JSON."
|
|
5255
|
+
);
|
|
5256
|
+
if (debug) {
|
|
5257
|
+
console.warn(
|
|
5258
|
+
`[opensteer] failed to read local selector cache file "${filePath}": ${message}`
|
|
5259
|
+
);
|
|
5260
|
+
}
|
|
5182
5261
|
return null;
|
|
5183
5262
|
}
|
|
5184
5263
|
}
|
|
@@ -7323,7 +7402,7 @@ function sleep3(ms) {
|
|
|
7323
7402
|
}
|
|
7324
7403
|
|
|
7325
7404
|
// src/opensteer.ts
|
|
7326
|
-
import { createHash, randomUUID
|
|
7405
|
+
import { createHash, randomUUID } from "crypto";
|
|
7327
7406
|
|
|
7328
7407
|
// src/browser/pool.ts
|
|
7329
7408
|
import {
|
|
@@ -7824,11 +7903,12 @@ function dotenvFileOrder(nodeEnv) {
|
|
|
7824
7903
|
files.push(".env");
|
|
7825
7904
|
return files;
|
|
7826
7905
|
}
|
|
7827
|
-
function loadDotenvValues(rootDir, baseEnv) {
|
|
7906
|
+
function loadDotenvValues(rootDir, baseEnv, options = {}) {
|
|
7828
7907
|
const values = {};
|
|
7829
7908
|
if (parseBool(baseEnv.OPENSTEER_DISABLE_DOTENV_AUTOLOAD) === true) {
|
|
7830
7909
|
return values;
|
|
7831
7910
|
}
|
|
7911
|
+
const debug = options.debug ?? parseBool(baseEnv.OPENSTEER_DEBUG) === true;
|
|
7832
7912
|
const baseDir = path4.resolve(rootDir);
|
|
7833
7913
|
const nodeEnv = baseEnv.NODE_ENV?.trim() || "";
|
|
7834
7914
|
for (const filename of dotenvFileOrder(nodeEnv)) {
|
|
@@ -7842,15 +7922,24 @@ function loadDotenvValues(rootDir, baseEnv) {
|
|
|
7842
7922
|
values[key] = value;
|
|
7843
7923
|
}
|
|
7844
7924
|
}
|
|
7845
|
-
} catch {
|
|
7925
|
+
} catch (error) {
|
|
7926
|
+
const message = extractErrorMessage(
|
|
7927
|
+
error,
|
|
7928
|
+
"Unable to read or parse dotenv file."
|
|
7929
|
+
);
|
|
7930
|
+
if (debug) {
|
|
7931
|
+
console.warn(
|
|
7932
|
+
`[opensteer] failed to load dotenv file "${filePath}": ${message}`
|
|
7933
|
+
);
|
|
7934
|
+
}
|
|
7846
7935
|
continue;
|
|
7847
7936
|
}
|
|
7848
7937
|
}
|
|
7849
7938
|
return values;
|
|
7850
7939
|
}
|
|
7851
|
-
function resolveEnv(rootDir) {
|
|
7940
|
+
function resolveEnv(rootDir, options = {}) {
|
|
7852
7941
|
const baseEnv = process.env;
|
|
7853
|
-
const dotenvValues = loadDotenvValues(rootDir, baseEnv);
|
|
7942
|
+
const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
|
|
7854
7943
|
return {
|
|
7855
7944
|
...dotenvValues,
|
|
7856
7945
|
...baseEnv
|
|
@@ -7894,13 +7983,22 @@ function assertNoLegacyRuntimeConfig(source, config) {
|
|
|
7894
7983
|
);
|
|
7895
7984
|
}
|
|
7896
7985
|
}
|
|
7897
|
-
function loadConfigFile(rootDir) {
|
|
7986
|
+
function loadConfigFile(rootDir, options = {}) {
|
|
7898
7987
|
const configPath = path4.join(rootDir, ".opensteer", "config.json");
|
|
7899
7988
|
if (!fs3.existsSync(configPath)) return {};
|
|
7900
7989
|
try {
|
|
7901
7990
|
const raw = fs3.readFileSync(configPath, "utf8");
|
|
7902
7991
|
return JSON.parse(raw);
|
|
7903
|
-
} catch {
|
|
7992
|
+
} catch (error) {
|
|
7993
|
+
const message = extractErrorMessage(
|
|
7994
|
+
error,
|
|
7995
|
+
"Unable to read or parse config file."
|
|
7996
|
+
);
|
|
7997
|
+
if (options.debug) {
|
|
7998
|
+
console.warn(
|
|
7999
|
+
`[opensteer] failed to load config file "${configPath}": ${message}`
|
|
8000
|
+
);
|
|
8001
|
+
}
|
|
7904
8002
|
return {};
|
|
7905
8003
|
}
|
|
7906
8004
|
}
|
|
@@ -8032,6 +8130,8 @@ function resolveCloudSelection(config, env = process.env) {
|
|
|
8032
8130
|
};
|
|
8033
8131
|
}
|
|
8034
8132
|
function resolveConfig(input = {}) {
|
|
8133
|
+
const processEnv = process.env;
|
|
8134
|
+
const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
|
|
8035
8135
|
const initialRootDir = input.storage?.rootDir ?? process.cwd();
|
|
8036
8136
|
const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
|
|
8037
8137
|
storage: {
|
|
@@ -8040,12 +8140,16 @@ function resolveConfig(input = {}) {
|
|
|
8040
8140
|
});
|
|
8041
8141
|
assertNoLegacyAiConfig("Opensteer constructor config", input);
|
|
8042
8142
|
assertNoLegacyRuntimeConfig("Opensteer constructor config", input);
|
|
8043
|
-
const fileConfig = loadConfigFile(initialRootDir
|
|
8143
|
+
const fileConfig = loadConfigFile(initialRootDir, {
|
|
8144
|
+
debug: debugHint
|
|
8145
|
+
});
|
|
8044
8146
|
assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
|
|
8045
8147
|
assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
|
|
8046
8148
|
const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
|
|
8047
8149
|
const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
|
|
8048
|
-
const env = resolveEnv(envRootDir
|
|
8150
|
+
const env = resolveEnv(envRootDir, {
|
|
8151
|
+
debug: debugHint
|
|
8152
|
+
});
|
|
8049
8153
|
if (env.OPENSTEER_AI_MODEL) {
|
|
8050
8154
|
throw new Error(
|
|
8051
8155
|
"OPENSTEER_AI_MODEL is no longer supported. Use OPENSTEER_MODEL instead."
|
|
@@ -9579,7 +9683,9 @@ var Opensteer = class _Opensteer {
|
|
|
9579
9683
|
this.aiExtract = this.createLazyExtractCallback(model);
|
|
9580
9684
|
const rootDir = resolved.storage?.rootDir || process.cwd();
|
|
9581
9685
|
this.namespace = resolveNamespace(resolved, rootDir);
|
|
9582
|
-
this.storage = new LocalSelectorStorage(rootDir, this.namespace
|
|
9686
|
+
this.storage = new LocalSelectorStorage(rootDir, this.namespace, {
|
|
9687
|
+
debug: Boolean(resolved.debug)
|
|
9688
|
+
});
|
|
9583
9689
|
this.pool = new BrowserPool(resolved.browser || {});
|
|
9584
9690
|
if (cloudSelection.cloud) {
|
|
9585
9691
|
const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
|
|
@@ -9598,12 +9704,20 @@ var Opensteer = class _Opensteer {
|
|
|
9598
9704
|
this.cloud = null;
|
|
9599
9705
|
}
|
|
9600
9706
|
}
|
|
9707
|
+
logDebugError(context, error) {
|
|
9708
|
+
if (!this.config.debug) return;
|
|
9709
|
+
const normalized = normalizeError(error, "Unknown error.");
|
|
9710
|
+
const codeSuffix = normalized.code && normalized.code.trim() ? ` [${normalized.code.trim()}]` : "";
|
|
9711
|
+
console.warn(
|
|
9712
|
+
`[opensteer] ${context}: ${normalized.message}${codeSuffix}`
|
|
9713
|
+
);
|
|
9714
|
+
}
|
|
9601
9715
|
createLazyResolveCallback(model) {
|
|
9602
9716
|
let resolverPromise = null;
|
|
9603
9717
|
return async (...args) => {
|
|
9604
9718
|
try {
|
|
9605
9719
|
if (!resolverPromise) {
|
|
9606
|
-
resolverPromise = import("./resolver-
|
|
9720
|
+
resolverPromise = import("./resolver-ZREUOOTV.js").then(
|
|
9607
9721
|
(m) => m.createResolveCallback(model)
|
|
9608
9722
|
);
|
|
9609
9723
|
}
|
|
@@ -9620,7 +9734,7 @@ var Opensteer = class _Opensteer {
|
|
|
9620
9734
|
const extract = async (args) => {
|
|
9621
9735
|
try {
|
|
9622
9736
|
if (!extractorPromise) {
|
|
9623
|
-
extractorPromise = import("./extractor-
|
|
9737
|
+
extractorPromise = import("./extractor-CZFCFUME.js").then(
|
|
9624
9738
|
(m) => m.createExtractCallback(model)
|
|
9625
9739
|
);
|
|
9626
9740
|
}
|
|
@@ -9688,7 +9802,8 @@ var Opensteer = class _Opensteer {
|
|
|
9688
9802
|
let tabs;
|
|
9689
9803
|
try {
|
|
9690
9804
|
tabs = await this.invokeCloudAction("tabs", {});
|
|
9691
|
-
} catch {
|
|
9805
|
+
} catch (error) {
|
|
9806
|
+
this.logDebugError("cloud page reference sync (tabs lookup) failed", error);
|
|
9692
9807
|
return;
|
|
9693
9808
|
}
|
|
9694
9809
|
if (!tabs.length) {
|
|
@@ -9826,12 +9941,7 @@ var Opensteer = class _Opensteer {
|
|
|
9826
9941
|
try {
|
|
9827
9942
|
await this.syncLocalSelectorCacheToCloud();
|
|
9828
9943
|
} catch (error) {
|
|
9829
|
-
|
|
9830
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9831
|
-
console.warn(
|
|
9832
|
-
`[opensteer] cloud selector cache sync failed: ${message}`
|
|
9833
|
-
);
|
|
9834
|
-
}
|
|
9944
|
+
this.logDebugError("cloud selector cache sync failed", error);
|
|
9835
9945
|
}
|
|
9836
9946
|
localRunId = this.cloud.localRunId || buildLocalRunId(this.namespace);
|
|
9837
9947
|
this.cloud.localRunId = localRunId;
|
|
@@ -9863,7 +9973,12 @@ var Opensteer = class _Opensteer {
|
|
|
9863
9973
|
this.cloud.actionClient = actionClient;
|
|
9864
9974
|
this.cloud.sessionId = sessionId;
|
|
9865
9975
|
this.cloud.cloudSessionUrl = session2.cloudSessionUrl;
|
|
9866
|
-
await this.syncCloudPageRef().catch(() =>
|
|
9976
|
+
await this.syncCloudPageRef().catch((error) => {
|
|
9977
|
+
this.logDebugError(
|
|
9978
|
+
"cloud page reference sync after launch failed",
|
|
9979
|
+
error
|
|
9980
|
+
);
|
|
9981
|
+
});
|
|
9867
9982
|
this.announceCloudSession({
|
|
9868
9983
|
sessionId: session2.sessionId,
|
|
9869
9984
|
workspaceId: session2.cloudSession.workspaceId,
|
|
@@ -9950,7 +10065,9 @@ var Opensteer = class _Opensteer {
|
|
|
9950
10065
|
}
|
|
9951
10066
|
async syncLocalSelectorCacheToCloud() {
|
|
9952
10067
|
if (!this.cloud) return;
|
|
9953
|
-
const entries = collectLocalSelectorCacheEntries(this.storage
|
|
10068
|
+
const entries = collectLocalSelectorCacheEntries(this.storage, {
|
|
10069
|
+
debug: Boolean(this.config.debug)
|
|
10070
|
+
});
|
|
9954
10071
|
if (!entries.length) return;
|
|
9955
10072
|
await this.cloud.sessionClient.importSelectorCache({
|
|
9956
10073
|
entries
|
|
@@ -9959,9 +10076,12 @@ var Opensteer = class _Opensteer {
|
|
|
9959
10076
|
async goto(url, options) {
|
|
9960
10077
|
if (this.cloud) {
|
|
9961
10078
|
await this.invokeCloudActionAndResetCache("goto", { url, options });
|
|
9962
|
-
await this.syncCloudPageRef({ expectedUrl: url }).catch(
|
|
9963
|
-
(
|
|
9964
|
-
|
|
10079
|
+
await this.syncCloudPageRef({ expectedUrl: url }).catch((error) => {
|
|
10080
|
+
this.logDebugError(
|
|
10081
|
+
"cloud page reference sync after goto failed",
|
|
10082
|
+
error
|
|
10083
|
+
);
|
|
10084
|
+
});
|
|
9965
10085
|
return;
|
|
9966
10086
|
}
|
|
9967
10087
|
const { waitUntil = "domcontentloaded", ...rest } = options ?? {};
|
|
@@ -10062,7 +10182,7 @@ var Opensteer = class _Opensteer {
|
|
|
10062
10182
|
let persistPath = null;
|
|
10063
10183
|
try {
|
|
10064
10184
|
if (storageKey && resolution.shouldPersist) {
|
|
10065
|
-
persistPath = await this.
|
|
10185
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10066
10186
|
handle,
|
|
10067
10187
|
"hover",
|
|
10068
10188
|
resolution.counter
|
|
@@ -10161,7 +10281,7 @@ var Opensteer = class _Opensteer {
|
|
|
10161
10281
|
let persistPath = null;
|
|
10162
10282
|
try {
|
|
10163
10283
|
if (storageKey && resolution.shouldPersist) {
|
|
10164
|
-
persistPath = await this.
|
|
10284
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10165
10285
|
handle,
|
|
10166
10286
|
"input",
|
|
10167
10287
|
resolution.counter
|
|
@@ -10264,7 +10384,7 @@ var Opensteer = class _Opensteer {
|
|
|
10264
10384
|
let persistPath = null;
|
|
10265
10385
|
try {
|
|
10266
10386
|
if (storageKey && resolution.shouldPersist) {
|
|
10267
|
-
persistPath = await this.
|
|
10387
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10268
10388
|
handle,
|
|
10269
10389
|
"select",
|
|
10270
10390
|
resolution.counter
|
|
@@ -10374,7 +10494,7 @@ var Opensteer = class _Opensteer {
|
|
|
10374
10494
|
let persistPath = null;
|
|
10375
10495
|
try {
|
|
10376
10496
|
if (storageKey && resolution.shouldPersist) {
|
|
10377
|
-
persistPath = await this.
|
|
10497
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10378
10498
|
handle,
|
|
10379
10499
|
"scroll",
|
|
10380
10500
|
resolution.counter
|
|
@@ -10473,7 +10593,12 @@ var Opensteer = class _Opensteer {
|
|
|
10473
10593
|
}
|
|
10474
10594
|
);
|
|
10475
10595
|
await this.syncCloudPageRef({ expectedUrl: result.url }).catch(
|
|
10476
|
-
() =>
|
|
10596
|
+
(error) => {
|
|
10597
|
+
this.logDebugError(
|
|
10598
|
+
"cloud page reference sync after newTab failed",
|
|
10599
|
+
error
|
|
10600
|
+
);
|
|
10601
|
+
}
|
|
10477
10602
|
);
|
|
10478
10603
|
return result;
|
|
10479
10604
|
}
|
|
@@ -10485,7 +10610,12 @@ var Opensteer = class _Opensteer {
|
|
|
10485
10610
|
async switchTab(index) {
|
|
10486
10611
|
if (this.cloud) {
|
|
10487
10612
|
await this.invokeCloudActionAndResetCache("switchTab", { index });
|
|
10488
|
-
await this.syncCloudPageRef().catch(() =>
|
|
10613
|
+
await this.syncCloudPageRef().catch((error) => {
|
|
10614
|
+
this.logDebugError(
|
|
10615
|
+
"cloud page reference sync after switchTab failed",
|
|
10616
|
+
error
|
|
10617
|
+
);
|
|
10618
|
+
});
|
|
10489
10619
|
return;
|
|
10490
10620
|
}
|
|
10491
10621
|
const page = await switchTab(this.context, index);
|
|
@@ -10495,7 +10625,12 @@ var Opensteer = class _Opensteer {
|
|
|
10495
10625
|
async closeTab(index) {
|
|
10496
10626
|
if (this.cloud) {
|
|
10497
10627
|
await this.invokeCloudActionAndResetCache("closeTab", { index });
|
|
10498
|
-
await this.syncCloudPageRef().catch(() =>
|
|
10628
|
+
await this.syncCloudPageRef().catch((error) => {
|
|
10629
|
+
this.logDebugError(
|
|
10630
|
+
"cloud page reference sync after closeTab failed",
|
|
10631
|
+
error
|
|
10632
|
+
);
|
|
10633
|
+
});
|
|
10499
10634
|
return;
|
|
10500
10635
|
}
|
|
10501
10636
|
const newPage = await closeTab(this.context, this.page, index);
|
|
@@ -10655,22 +10790,28 @@ var Opensteer = class _Opensteer {
|
|
|
10655
10790
|
const handle = await this.resolveCounterHandle(resolution.counter);
|
|
10656
10791
|
try {
|
|
10657
10792
|
if (storageKey && resolution.shouldPersist) {
|
|
10658
|
-
const persistPath = await this.
|
|
10793
|
+
const persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10659
10794
|
handle,
|
|
10660
10795
|
method,
|
|
10661
10796
|
resolution.counter
|
|
10662
10797
|
);
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10798
|
+
if (persistPath) {
|
|
10799
|
+
this.persistPath(
|
|
10800
|
+
storageKey,
|
|
10801
|
+
method,
|
|
10802
|
+
options.description,
|
|
10803
|
+
persistPath
|
|
10804
|
+
);
|
|
10805
|
+
}
|
|
10669
10806
|
}
|
|
10670
10807
|
return await counterFn(handle);
|
|
10671
10808
|
} catch (err) {
|
|
10672
|
-
|
|
10673
|
-
|
|
10809
|
+
if (err instanceof Error) {
|
|
10810
|
+
throw err;
|
|
10811
|
+
}
|
|
10812
|
+
throw new Error(
|
|
10813
|
+
`${method} failed. ${extractErrorMessage(err, "Unknown error.")}`
|
|
10814
|
+
);
|
|
10674
10815
|
} finally {
|
|
10675
10816
|
await handle.dispose();
|
|
10676
10817
|
}
|
|
@@ -10699,7 +10840,7 @@ var Opensteer = class _Opensteer {
|
|
|
10699
10840
|
let persistPath = null;
|
|
10700
10841
|
try {
|
|
10701
10842
|
if (storageKey && resolution.shouldPersist) {
|
|
10702
|
-
persistPath = await this.
|
|
10843
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10703
10844
|
handle,
|
|
10704
10845
|
"uploadFile",
|
|
10705
10846
|
resolution.counter
|
|
@@ -10797,6 +10938,9 @@ var Opensteer = class _Opensteer {
|
|
|
10797
10938
|
if (this.cloud) {
|
|
10798
10939
|
return await this.invokeCloudAction("extract", options);
|
|
10799
10940
|
}
|
|
10941
|
+
if (options.schema !== void 0) {
|
|
10942
|
+
assertValidExtractSchemaRoot(options.schema);
|
|
10943
|
+
}
|
|
10800
10944
|
const storageKey = this.resolveStorageKey(options.description);
|
|
10801
10945
|
const schemaHash = options.schema ? computeSchemaHash(options.schema) : null;
|
|
10802
10946
|
const stored = storageKey ? this.storage.readSelector(storageKey) : null;
|
|
@@ -10805,7 +10949,7 @@ var Opensteer = class _Opensteer {
|
|
|
10805
10949
|
try {
|
|
10806
10950
|
payload = normalizePersistedExtractPayload(stored.path);
|
|
10807
10951
|
} catch (err) {
|
|
10808
|
-
const message = err
|
|
10952
|
+
const message = extractErrorMessage(err, "Unknown error.");
|
|
10809
10953
|
const selectorFile = storageKey ? this.storage.getSelectorPath(storageKey) : "unknown selector file";
|
|
10810
10954
|
throw new Error(
|
|
10811
10955
|
`Cached extraction selector is invalid for the current schema at "${selectorFile}". Delete the cached selector and rerun extraction. ${message}`
|
|
@@ -10822,7 +10966,16 @@ var Opensteer = class _Opensteer {
|
|
|
10822
10966
|
fields.push(...schemaFields);
|
|
10823
10967
|
}
|
|
10824
10968
|
if (!fields.length) {
|
|
10825
|
-
|
|
10969
|
+
let planResult;
|
|
10970
|
+
try {
|
|
10971
|
+
planResult = await this.parseAiExtractPlan(options);
|
|
10972
|
+
} catch (error) {
|
|
10973
|
+
const message = extractErrorMessage(error, "Unknown error.");
|
|
10974
|
+
const contextMessage = options.schema ? "Schema extraction did not resolve deterministic field targets, so Opensteer attempted AI extraction planning." : "Opensteer attempted AI extraction planning.";
|
|
10975
|
+
throw new Error(`${contextMessage} ${message}`, {
|
|
10976
|
+
cause: error
|
|
10977
|
+
});
|
|
10978
|
+
}
|
|
10826
10979
|
if (planResult.fields.length) {
|
|
10827
10980
|
fields.push(...planResult.fields);
|
|
10828
10981
|
} else if (planResult.data !== void 0) {
|
|
@@ -10963,7 +11116,7 @@ var Opensteer = class _Opensteer {
|
|
|
10963
11116
|
let persistPath = null;
|
|
10964
11117
|
try {
|
|
10965
11118
|
if (storageKey && resolution.shouldPersist) {
|
|
10966
|
-
persistPath = await this.
|
|
11119
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10967
11120
|
handle,
|
|
10968
11121
|
"click",
|
|
10969
11122
|
resolution.counter
|
|
@@ -11059,17 +11212,6 @@ var Opensteer = class _Opensteer {
|
|
|
11059
11212
|
}
|
|
11060
11213
|
}
|
|
11061
11214
|
if (options.element != null) {
|
|
11062
|
-
const pathFromElement = await this.tryBuildPathFromCounter(
|
|
11063
|
-
options.element
|
|
11064
|
-
);
|
|
11065
|
-
if (pathFromElement) {
|
|
11066
|
-
return {
|
|
11067
|
-
path: pathFromElement,
|
|
11068
|
-
counter: null,
|
|
11069
|
-
shouldPersist: Boolean(storageKey),
|
|
11070
|
-
source: "element"
|
|
11071
|
-
};
|
|
11072
|
-
}
|
|
11073
11215
|
return {
|
|
11074
11216
|
path: null,
|
|
11075
11217
|
counter: options.element,
|
|
@@ -11097,17 +11239,6 @@ var Opensteer = class _Opensteer {
|
|
|
11097
11239
|
options.description
|
|
11098
11240
|
);
|
|
11099
11241
|
if (resolved?.counter != null) {
|
|
11100
|
-
const pathFromAiCounter = await this.tryBuildPathFromCounter(
|
|
11101
|
-
resolved.counter
|
|
11102
|
-
);
|
|
11103
|
-
if (pathFromAiCounter) {
|
|
11104
|
-
return {
|
|
11105
|
-
path: pathFromAiCounter,
|
|
11106
|
-
counter: null,
|
|
11107
|
-
shouldPersist: Boolean(storageKey),
|
|
11108
|
-
source: "ai"
|
|
11109
|
-
};
|
|
11110
|
-
}
|
|
11111
11242
|
return {
|
|
11112
11243
|
path: null,
|
|
11113
11244
|
counter: resolved.counter,
|
|
@@ -11189,23 +11320,22 @@ var Opensteer = class _Opensteer {
|
|
|
11189
11320
|
try {
|
|
11190
11321
|
const builtPath = await buildElementPathFromHandle(handle);
|
|
11191
11322
|
if (builtPath) {
|
|
11192
|
-
|
|
11323
|
+
const withFrameContext = await this.withHandleIframeContext(
|
|
11324
|
+
handle,
|
|
11325
|
+
builtPath
|
|
11326
|
+
);
|
|
11327
|
+
return this.withIndexedIframeContext(
|
|
11328
|
+
withFrameContext,
|
|
11329
|
+
indexedPath
|
|
11330
|
+
);
|
|
11193
11331
|
}
|
|
11194
11332
|
return indexedPath;
|
|
11195
11333
|
} finally {
|
|
11196
11334
|
await handle.dispose();
|
|
11197
11335
|
}
|
|
11198
11336
|
}
|
|
11199
|
-
async tryBuildPathFromCounter(counter) {
|
|
11200
|
-
try {
|
|
11201
|
-
return await this.buildPathFromElement(counter);
|
|
11202
|
-
} catch {
|
|
11203
|
-
return null;
|
|
11204
|
-
}
|
|
11205
|
-
}
|
|
11206
11337
|
async resolveCounterHandle(element) {
|
|
11207
|
-
|
|
11208
|
-
return resolveCounterElement(this.page, snapshot, element);
|
|
11338
|
+
return resolveCounterElement(this.page, element);
|
|
11209
11339
|
}
|
|
11210
11340
|
async resolveCounterHandleForAction(action, description, element) {
|
|
11211
11341
|
try {
|
|
@@ -11229,8 +11359,12 @@ var Opensteer = class _Opensteer {
|
|
|
11229
11359
|
const indexedPath = await this.readPathFromCounterIndex(counter);
|
|
11230
11360
|
const builtPath = await buildElementPathFromHandle(handle);
|
|
11231
11361
|
if (builtPath) {
|
|
11362
|
+
const withFrameContext = await this.withHandleIframeContext(
|
|
11363
|
+
handle,
|
|
11364
|
+
builtPath
|
|
11365
|
+
);
|
|
11232
11366
|
const normalized = this.withIndexedIframeContext(
|
|
11233
|
-
|
|
11367
|
+
withFrameContext,
|
|
11234
11368
|
indexedPath
|
|
11235
11369
|
);
|
|
11236
11370
|
if (normalized.nodes.length) return normalized;
|
|
@@ -11240,15 +11374,34 @@ var Opensteer = class _Opensteer {
|
|
|
11240
11374
|
`Unable to build element path from counter ${counter} during ${action}.`
|
|
11241
11375
|
);
|
|
11242
11376
|
}
|
|
11377
|
+
async tryBuildPathFromResolvedHandle(handle, action, counter) {
|
|
11378
|
+
try {
|
|
11379
|
+
return await this.buildPathFromResolvedHandle(handle, action, counter);
|
|
11380
|
+
} catch (error) {
|
|
11381
|
+
this.logDebugError(
|
|
11382
|
+
`path persistence skipped for ${action} counter ${counter}`,
|
|
11383
|
+
error
|
|
11384
|
+
);
|
|
11385
|
+
return null;
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
11243
11388
|
withIndexedIframeContext(builtPath, indexedPath) {
|
|
11244
11389
|
const normalizedBuilt = this.normalizePath(builtPath);
|
|
11245
11390
|
if (!indexedPath) return normalizedBuilt;
|
|
11246
11391
|
const iframePrefix = collectIframeContextPrefix(indexedPath);
|
|
11247
11392
|
if (!iframePrefix.length) return normalizedBuilt;
|
|
11393
|
+
const builtContext = cloneContextHops(normalizedBuilt.context);
|
|
11394
|
+
const overlap = measureContextOverlap(iframePrefix, builtContext);
|
|
11395
|
+
const missingPrefix = cloneContextHops(
|
|
11396
|
+
iframePrefix.slice(0, iframePrefix.length - overlap)
|
|
11397
|
+
);
|
|
11398
|
+
if (!missingPrefix.length) {
|
|
11399
|
+
return normalizedBuilt;
|
|
11400
|
+
}
|
|
11248
11401
|
const merged = {
|
|
11249
11402
|
context: [
|
|
11250
|
-
...
|
|
11251
|
-
...
|
|
11403
|
+
...missingPrefix,
|
|
11404
|
+
...builtContext
|
|
11252
11405
|
],
|
|
11253
11406
|
nodes: cloneElementPath(normalizedBuilt).nodes
|
|
11254
11407
|
};
|
|
@@ -11258,9 +11411,48 @@ var Opensteer = class _Opensteer {
|
|
|
11258
11411
|
if (fallback.nodes.length) return fallback;
|
|
11259
11412
|
return normalizedBuilt;
|
|
11260
11413
|
}
|
|
11414
|
+
async withHandleIframeContext(handle, path5) {
|
|
11415
|
+
const ownFrame = await handle.ownerFrame();
|
|
11416
|
+
if (!ownFrame) {
|
|
11417
|
+
return this.normalizePath(path5);
|
|
11418
|
+
}
|
|
11419
|
+
let frame = ownFrame;
|
|
11420
|
+
let prefix = [];
|
|
11421
|
+
while (frame && frame !== this.page.mainFrame()) {
|
|
11422
|
+
const parent = frame.parentFrame();
|
|
11423
|
+
if (!parent) break;
|
|
11424
|
+
const frameElement = await frame.frameElement().catch(() => null);
|
|
11425
|
+
if (!frameElement) break;
|
|
11426
|
+
try {
|
|
11427
|
+
const frameElementPath = await buildElementPathFromHandle(frameElement);
|
|
11428
|
+
if (frameElementPath?.nodes.length) {
|
|
11429
|
+
const segment = [
|
|
11430
|
+
...cloneContextHops(frameElementPath.context),
|
|
11431
|
+
{
|
|
11432
|
+
kind: "iframe",
|
|
11433
|
+
host: cloneElementPath(frameElementPath).nodes
|
|
11434
|
+
}
|
|
11435
|
+
];
|
|
11436
|
+
prefix = [...segment, ...prefix];
|
|
11437
|
+
}
|
|
11438
|
+
} finally {
|
|
11439
|
+
await frameElement.dispose().catch(() => void 0);
|
|
11440
|
+
}
|
|
11441
|
+
frame = parent;
|
|
11442
|
+
}
|
|
11443
|
+
if (!prefix.length) {
|
|
11444
|
+
return this.normalizePath(path5);
|
|
11445
|
+
}
|
|
11446
|
+
return this.normalizePath({
|
|
11447
|
+
context: [...prefix, ...cloneContextHops(path5.context)],
|
|
11448
|
+
nodes: cloneElementPath(path5).nodes
|
|
11449
|
+
});
|
|
11450
|
+
}
|
|
11261
11451
|
async readPathFromCounterIndex(counter) {
|
|
11262
|
-
|
|
11263
|
-
|
|
11452
|
+
if (!this.snapshotCache || this.snapshotCache.url !== this.page.url() || !this.snapshotCache.counterIndex) {
|
|
11453
|
+
return null;
|
|
11454
|
+
}
|
|
11455
|
+
const indexed = this.snapshotCache.counterIndex.get(counter);
|
|
11264
11456
|
if (!indexed) return null;
|
|
11265
11457
|
const normalized = this.normalizePath(indexed);
|
|
11266
11458
|
if (!normalized.nodes.length) return null;
|
|
@@ -11271,15 +11463,6 @@ var Opensteer = class _Opensteer {
|
|
|
11271
11463
|
if (!path5) return null;
|
|
11272
11464
|
return this.normalizePath(path5);
|
|
11273
11465
|
}
|
|
11274
|
-
async ensureSnapshotWithCounters() {
|
|
11275
|
-
if (!this.snapshotCache || !this.snapshotCache.counterBindings || this.snapshotCache.url !== this.page.url()) {
|
|
11276
|
-
await this.snapshot({
|
|
11277
|
-
mode: "full",
|
|
11278
|
-
withCounters: true
|
|
11279
|
-
});
|
|
11280
|
-
}
|
|
11281
|
-
return this.snapshotCache;
|
|
11282
|
-
}
|
|
11283
11466
|
persistPath(id, method, description, path5) {
|
|
11284
11467
|
const now = Date.now();
|
|
11285
11468
|
const safeFile = this.storage.getSelectorFileName(id);
|
|
@@ -11484,12 +11667,6 @@ var Opensteer = class _Opensteer {
|
|
|
11484
11667
|
};
|
|
11485
11668
|
}
|
|
11486
11669
|
async buildFieldTargetsFromSchema(schema) {
|
|
11487
|
-
if (!schema || typeof schema !== "object") {
|
|
11488
|
-
return [];
|
|
11489
|
-
}
|
|
11490
|
-
if (Array.isArray(schema)) {
|
|
11491
|
-
return [];
|
|
11492
|
-
}
|
|
11493
11670
|
const fields = [];
|
|
11494
11671
|
await this.collectFieldTargetsFromSchemaObject(
|
|
11495
11672
|
schema,
|
|
@@ -11535,17 +11712,6 @@ var Opensteer = class _Opensteer {
|
|
|
11535
11712
|
return;
|
|
11536
11713
|
}
|
|
11537
11714
|
if (normalized.element != null) {
|
|
11538
|
-
const path5 = await this.tryBuildPathFromCounter(
|
|
11539
|
-
normalized.element
|
|
11540
|
-
);
|
|
11541
|
-
if (path5) {
|
|
11542
|
-
fields.push({
|
|
11543
|
-
key: fieldKey,
|
|
11544
|
-
path: path5,
|
|
11545
|
-
attribute: normalized.attribute
|
|
11546
|
-
});
|
|
11547
|
-
return;
|
|
11548
|
-
}
|
|
11549
11715
|
fields.push({
|
|
11550
11716
|
key: fieldKey,
|
|
11551
11717
|
counter: normalized.element,
|
|
@@ -11563,6 +11729,10 @@ var Opensteer = class _Opensteer {
|
|
|
11563
11729
|
path: path5,
|
|
11564
11730
|
attribute: normalized.attribute
|
|
11565
11731
|
});
|
|
11732
|
+
} else {
|
|
11733
|
+
throw new Error(
|
|
11734
|
+
`Extraction schema field "${fieldKey}" uses selector "${normalized.selector}", but no matching element path could be built from the current page snapshot.`
|
|
11735
|
+
);
|
|
11566
11736
|
}
|
|
11567
11737
|
return;
|
|
11568
11738
|
}
|
|
@@ -11586,15 +11756,6 @@ var Opensteer = class _Opensteer {
|
|
|
11586
11756
|
continue;
|
|
11587
11757
|
}
|
|
11588
11758
|
if (fieldPlan.element != null) {
|
|
11589
|
-
const path6 = await this.tryBuildPathFromCounter(fieldPlan.element);
|
|
11590
|
-
if (path6) {
|
|
11591
|
-
fields.push({
|
|
11592
|
-
key,
|
|
11593
|
-
path: path6,
|
|
11594
|
-
attribute: fieldPlan.attribute
|
|
11595
|
-
});
|
|
11596
|
-
continue;
|
|
11597
|
-
}
|
|
11598
11759
|
fields.push({
|
|
11599
11760
|
key,
|
|
11600
11761
|
counter: fieldPlan.element,
|
|
@@ -11649,12 +11810,7 @@ var Opensteer = class _Opensteer {
|
|
|
11649
11810
|
}
|
|
11650
11811
|
}
|
|
11651
11812
|
if (counterRequests.length) {
|
|
11652
|
-
const
|
|
11653
|
-
const counterValues = await resolveCountersBatch(
|
|
11654
|
-
this.page,
|
|
11655
|
-
snapshot,
|
|
11656
|
-
counterRequests
|
|
11657
|
-
);
|
|
11813
|
+
const counterValues = await resolveCountersBatch(this.page, counterRequests);
|
|
11658
11814
|
Object.assign(result, counterValues);
|
|
11659
11815
|
}
|
|
11660
11816
|
if (pathFields.length) {
|
|
@@ -11684,7 +11840,7 @@ var Opensteer = class _Opensteer {
|
|
|
11684
11840
|
const path5 = await this.buildPathFromElement(field.counter);
|
|
11685
11841
|
if (!path5) {
|
|
11686
11842
|
throw new Error(
|
|
11687
|
-
`Unable to
|
|
11843
|
+
`Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
|
|
11688
11844
|
);
|
|
11689
11845
|
}
|
|
11690
11846
|
resolved.push({
|
|
@@ -11730,6 +11886,33 @@ function collectIframeContextPrefix(path5) {
|
|
|
11730
11886
|
if (lastIframeIndex < 0) return [];
|
|
11731
11887
|
return cloneContextHops(context.slice(0, lastIframeIndex + 1));
|
|
11732
11888
|
}
|
|
11889
|
+
function measureContextOverlap(indexedPrefix, builtContext) {
|
|
11890
|
+
const maxOverlap = Math.min(indexedPrefix.length, builtContext.length);
|
|
11891
|
+
for (let size = maxOverlap; size > 0; size -= 1) {
|
|
11892
|
+
if (matchesContextPrefix(indexedPrefix, builtContext, size, true)) {
|
|
11893
|
+
return size;
|
|
11894
|
+
}
|
|
11895
|
+
}
|
|
11896
|
+
for (let size = maxOverlap; size > 0; size -= 1) {
|
|
11897
|
+
if (matchesContextPrefix(indexedPrefix, builtContext, size, false)) {
|
|
11898
|
+
return size;
|
|
11899
|
+
}
|
|
11900
|
+
}
|
|
11901
|
+
return 0;
|
|
11902
|
+
}
|
|
11903
|
+
function matchesContextPrefix(indexedPrefix, builtContext, size, strictHost) {
|
|
11904
|
+
for (let idx = 0; idx < size; idx += 1) {
|
|
11905
|
+
const left = indexedPrefix[indexedPrefix.length - size + idx];
|
|
11906
|
+
const right = builtContext[idx];
|
|
11907
|
+
if (left.kind !== right.kind) {
|
|
11908
|
+
return false;
|
|
11909
|
+
}
|
|
11910
|
+
if (strictHost && JSON.stringify(left.host) !== JSON.stringify(right.host)) {
|
|
11911
|
+
return false;
|
|
11912
|
+
}
|
|
11913
|
+
}
|
|
11914
|
+
return true;
|
|
11915
|
+
}
|
|
11733
11916
|
function normalizeSchemaValue(value) {
|
|
11734
11917
|
if (!value) return null;
|
|
11735
11918
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -11913,13 +12096,28 @@ function countNonNullLeaves(value) {
|
|
|
11913
12096
|
function isPrimitiveLike(value) {
|
|
11914
12097
|
return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
11915
12098
|
}
|
|
12099
|
+
function assertValidExtractSchemaRoot(schema) {
|
|
12100
|
+
if (!schema || typeof schema !== "object") {
|
|
12101
|
+
throw new Error(
|
|
12102
|
+
"Invalid extraction schema: expected a JSON object at the top level."
|
|
12103
|
+
);
|
|
12104
|
+
}
|
|
12105
|
+
if (Array.isArray(schema)) {
|
|
12106
|
+
throw new Error(
|
|
12107
|
+
'Invalid extraction schema: top-level arrays are not supported. Wrap array fields in an object (for example {"items":[...]}).'
|
|
12108
|
+
);
|
|
12109
|
+
}
|
|
12110
|
+
}
|
|
11916
12111
|
function parseAiExtractResponse(response) {
|
|
11917
12112
|
if (typeof response === "string") {
|
|
11918
12113
|
const trimmed = stripCodeFence(response);
|
|
11919
12114
|
try {
|
|
11920
12115
|
return JSON.parse(trimmed);
|
|
11921
12116
|
} catch {
|
|
11922
|
-
|
|
12117
|
+
const preview = summarizeForError(trimmed);
|
|
12118
|
+
throw new Error(
|
|
12119
|
+
`LLM extraction returned a non-JSON response.${preview ? ` Preview: "${preview}"` : ""}`
|
|
12120
|
+
);
|
|
11923
12121
|
}
|
|
11924
12122
|
}
|
|
11925
12123
|
if (response && typeof response === "object") {
|
|
@@ -11944,6 +12142,12 @@ function stripCodeFence(input) {
|
|
|
11944
12142
|
if (lastFence === -1) return withoutHeader.trim();
|
|
11945
12143
|
return withoutHeader.slice(0, lastFence).trim();
|
|
11946
12144
|
}
|
|
12145
|
+
function summarizeForError(input, maxLength = 180) {
|
|
12146
|
+
const compact = input.replace(/\s+/g, " ").trim();
|
|
12147
|
+
if (!compact) return "";
|
|
12148
|
+
if (compact.length <= maxLength) return compact;
|
|
12149
|
+
return `${compact.slice(0, maxLength)}...`;
|
|
12150
|
+
}
|
|
11947
12151
|
function getScrollDelta2(options) {
|
|
11948
12152
|
const amount = typeof options.amount === "number" ? options.amount : 600;
|
|
11949
12153
|
const absoluteAmount = Math.abs(amount);
|
|
@@ -11966,10 +12170,11 @@ function isInternalOrBlankPageUrl(url) {
|
|
|
11966
12170
|
}
|
|
11967
12171
|
function buildLocalRunId(namespace) {
|
|
11968
12172
|
const normalized = namespace.trim() || "default";
|
|
11969
|
-
return `${normalized}-${Date.now().toString(36)}-${
|
|
12173
|
+
return `${normalized}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
11970
12174
|
}
|
|
11971
12175
|
|
|
11972
12176
|
export {
|
|
12177
|
+
normalizeError,
|
|
11973
12178
|
normalizeNamespace,
|
|
11974
12179
|
resolveNamespaceDir,
|
|
11975
12180
|
waitForVisualStability,
|
|
@@ -11995,13 +12200,12 @@ export {
|
|
|
11995
12200
|
cleanForClickable,
|
|
11996
12201
|
cleanForScrollable,
|
|
11997
12202
|
cleanForAction,
|
|
11998
|
-
CounterResolutionError,
|
|
11999
|
-
ensureLiveCounters,
|
|
12000
|
-
resolveCounterElement,
|
|
12001
|
-
resolveCountersBatch,
|
|
12002
12203
|
prepareSnapshot,
|
|
12003
12204
|
ElementPathError,
|
|
12004
12205
|
resolveElementPath,
|
|
12206
|
+
CounterResolutionError,
|
|
12207
|
+
resolveCounterElement,
|
|
12208
|
+
resolveCountersBatch,
|
|
12005
12209
|
performClick,
|
|
12006
12210
|
performHover,
|
|
12007
12211
|
performInput,
|