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
package/dist/index.cjs
CHANGED
|
@@ -109,6 +109,15 @@ function resolveProviderInfo(modelStr) {
|
|
|
109
109
|
return info;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
const slash = modelStr.indexOf("/");
|
|
113
|
+
if (slash > 0) {
|
|
114
|
+
const provider = modelStr.slice(0, slash).trim().toLowerCase();
|
|
115
|
+
if (provider) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Unsupported model provider prefix "${provider}" in model "${modelStr}". Use one of: openai, anthropic, google, xai, groq.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
112
121
|
return { pkg: "@ai-sdk/openai", providerFn: "openai" };
|
|
113
122
|
}
|
|
114
123
|
function stripProviderPrefix(modelStr) {
|
|
@@ -396,7 +405,6 @@ __export(index_exports, {
|
|
|
396
405
|
createExtractCallback: () => createExtractCallback,
|
|
397
406
|
createResolveCallback: () => createResolveCallback,
|
|
398
407
|
createTab: () => createTab,
|
|
399
|
-
ensureLiveCounters: () => ensureLiveCounters,
|
|
400
408
|
exportCookies: () => exportCookies,
|
|
401
409
|
extractArrayRowsWithPaths: () => extractArrayRowsWithPaths,
|
|
402
410
|
extractArrayWithPaths: () => extractArrayWithPaths,
|
|
@@ -436,7 +444,7 @@ __export(index_exports, {
|
|
|
436
444
|
module.exports = __toCommonJS(index_exports);
|
|
437
445
|
|
|
438
446
|
// src/opensteer.ts
|
|
439
|
-
var
|
|
447
|
+
var import_crypto = require("crypto");
|
|
440
448
|
|
|
441
449
|
// src/browser/pool.ts
|
|
442
450
|
var import_playwright = require("playwright");
|
|
@@ -906,6 +914,232 @@ var import_path3 = __toESM(require("path"), 1);
|
|
|
906
914
|
var import_url = require("url");
|
|
907
915
|
var import_dotenv = require("dotenv");
|
|
908
916
|
|
|
917
|
+
// src/error-normalization.ts
|
|
918
|
+
function extractErrorMessage(error, fallback = "Unknown error.") {
|
|
919
|
+
if (error instanceof Error) {
|
|
920
|
+
const message = error.message.trim();
|
|
921
|
+
if (message) return message;
|
|
922
|
+
const name = error.name.trim();
|
|
923
|
+
if (name) return name;
|
|
924
|
+
}
|
|
925
|
+
if (typeof error === "string" && error.trim()) {
|
|
926
|
+
return error.trim();
|
|
927
|
+
}
|
|
928
|
+
const record = asRecord(error);
|
|
929
|
+
const recordMessage = toNonEmptyString(record?.message) || toNonEmptyString(record?.error);
|
|
930
|
+
if (recordMessage) {
|
|
931
|
+
return recordMessage;
|
|
932
|
+
}
|
|
933
|
+
return fallback;
|
|
934
|
+
}
|
|
935
|
+
function normalizeError(error, fallback = "Unknown error.", maxCauseDepth = 2) {
|
|
936
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
937
|
+
return normalizeErrorInternal(error, fallback, maxCauseDepth, seen);
|
|
938
|
+
}
|
|
939
|
+
function normalizeErrorInternal(error, fallback, depthRemaining, seen) {
|
|
940
|
+
const record = asRecord(error);
|
|
941
|
+
if (record) {
|
|
942
|
+
if (seen.has(record)) {
|
|
943
|
+
return {
|
|
944
|
+
message: extractErrorMessage(error, fallback)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
seen.add(record);
|
|
948
|
+
}
|
|
949
|
+
const message = extractErrorMessage(error, fallback);
|
|
950
|
+
const code = extractCode(error);
|
|
951
|
+
const name = extractName(error);
|
|
952
|
+
const details = extractDetails(error);
|
|
953
|
+
if (depthRemaining <= 0) {
|
|
954
|
+
return compactErrorInfo({
|
|
955
|
+
message,
|
|
956
|
+
...code ? { code } : {},
|
|
957
|
+
...name ? { name } : {},
|
|
958
|
+
...details ? { details } : {}
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
const cause = extractCause(error);
|
|
962
|
+
if (!cause) {
|
|
963
|
+
return compactErrorInfo({
|
|
964
|
+
message,
|
|
965
|
+
...code ? { code } : {},
|
|
966
|
+
...name ? { name } : {},
|
|
967
|
+
...details ? { details } : {}
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
const normalizedCause = normalizeErrorInternal(
|
|
971
|
+
cause,
|
|
972
|
+
"Caused by an unknown error.",
|
|
973
|
+
depthRemaining - 1,
|
|
974
|
+
seen
|
|
975
|
+
);
|
|
976
|
+
return compactErrorInfo({
|
|
977
|
+
message,
|
|
978
|
+
...code ? { code } : {},
|
|
979
|
+
...name ? { name } : {},
|
|
980
|
+
...details ? { details } : {},
|
|
981
|
+
cause: normalizedCause
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function compactErrorInfo(info) {
|
|
985
|
+
const safeDetails = toJsonSafeRecord(info.details);
|
|
986
|
+
return {
|
|
987
|
+
message: info.message,
|
|
988
|
+
...info.code ? { code: info.code } : {},
|
|
989
|
+
...info.name ? { name: info.name } : {},
|
|
990
|
+
...safeDetails ? { details: safeDetails } : {},
|
|
991
|
+
...info.cause ? { cause: info.cause } : {}
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function extractCode(error) {
|
|
995
|
+
const record = asRecord(error);
|
|
996
|
+
const raw = record?.code;
|
|
997
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
998
|
+
return raw.trim();
|
|
999
|
+
}
|
|
1000
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
1001
|
+
return String(raw);
|
|
1002
|
+
}
|
|
1003
|
+
return void 0;
|
|
1004
|
+
}
|
|
1005
|
+
function extractName(error) {
|
|
1006
|
+
if (error instanceof Error && error.name.trim()) {
|
|
1007
|
+
return error.name.trim();
|
|
1008
|
+
}
|
|
1009
|
+
const record = asRecord(error);
|
|
1010
|
+
return toNonEmptyString(record?.name);
|
|
1011
|
+
}
|
|
1012
|
+
function extractDetails(error) {
|
|
1013
|
+
const record = asRecord(error);
|
|
1014
|
+
if (!record) return void 0;
|
|
1015
|
+
const details = {};
|
|
1016
|
+
const rawDetails = asRecord(record.details);
|
|
1017
|
+
if (rawDetails) {
|
|
1018
|
+
Object.assign(details, rawDetails);
|
|
1019
|
+
}
|
|
1020
|
+
const action = toNonEmptyString(record.action);
|
|
1021
|
+
if (action) {
|
|
1022
|
+
details.action = action;
|
|
1023
|
+
}
|
|
1024
|
+
const selectorUsed = toNonEmptyString(record.selectorUsed);
|
|
1025
|
+
if (selectorUsed) {
|
|
1026
|
+
details.selectorUsed = selectorUsed;
|
|
1027
|
+
}
|
|
1028
|
+
if (typeof record.status === "number" && Number.isFinite(record.status)) {
|
|
1029
|
+
details.status = record.status;
|
|
1030
|
+
}
|
|
1031
|
+
const failure = asRecord(record.failure);
|
|
1032
|
+
if (failure) {
|
|
1033
|
+
const failureCode = toNonEmptyString(failure.code);
|
|
1034
|
+
const classificationSource = toNonEmptyString(
|
|
1035
|
+
failure.classificationSource
|
|
1036
|
+
);
|
|
1037
|
+
const failureDetails = asRecord(failure.details);
|
|
1038
|
+
if (failureCode || classificationSource || failureDetails) {
|
|
1039
|
+
details.actionFailure = {
|
|
1040
|
+
...failureCode ? { code: failureCode } : {},
|
|
1041
|
+
...classificationSource ? { classificationSource } : {},
|
|
1042
|
+
...failureDetails ? { details: failureDetails } : {}
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return Object.keys(details).length ? details : void 0;
|
|
1047
|
+
}
|
|
1048
|
+
function extractCause(error) {
|
|
1049
|
+
if (error instanceof Error) {
|
|
1050
|
+
return error.cause;
|
|
1051
|
+
}
|
|
1052
|
+
const record = asRecord(error);
|
|
1053
|
+
return record?.cause;
|
|
1054
|
+
}
|
|
1055
|
+
function asRecord(value) {
|
|
1056
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
return value;
|
|
1060
|
+
}
|
|
1061
|
+
function toNonEmptyString(value) {
|
|
1062
|
+
if (typeof value !== "string") return void 0;
|
|
1063
|
+
const normalized = value.trim();
|
|
1064
|
+
return normalized.length ? normalized : void 0;
|
|
1065
|
+
}
|
|
1066
|
+
function toJsonSafeRecord(value) {
|
|
1067
|
+
if (!value) return void 0;
|
|
1068
|
+
const sanitized = toJsonSafeValue(value, /* @__PURE__ */ new WeakSet());
|
|
1069
|
+
if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) {
|
|
1070
|
+
return void 0;
|
|
1071
|
+
}
|
|
1072
|
+
const record = sanitized;
|
|
1073
|
+
return Object.keys(record).length > 0 ? record : void 0;
|
|
1074
|
+
}
|
|
1075
|
+
function toJsonSafeValue(value, seen) {
|
|
1076
|
+
if (value === null) return null;
|
|
1077
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
1078
|
+
return value;
|
|
1079
|
+
}
|
|
1080
|
+
if (typeof value === "number") {
|
|
1081
|
+
return Number.isFinite(value) ? value : null;
|
|
1082
|
+
}
|
|
1083
|
+
if (typeof value === "bigint") {
|
|
1084
|
+
return value.toString();
|
|
1085
|
+
}
|
|
1086
|
+
if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
|
|
1087
|
+
return void 0;
|
|
1088
|
+
}
|
|
1089
|
+
if (value instanceof Date) {
|
|
1090
|
+
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
1091
|
+
}
|
|
1092
|
+
if (Array.isArray(value)) {
|
|
1093
|
+
if (seen.has(value)) return "[Circular]";
|
|
1094
|
+
seen.add(value);
|
|
1095
|
+
const output = value.map((item) => {
|
|
1096
|
+
const next = toJsonSafeValue(item, seen);
|
|
1097
|
+
return next === void 0 ? null : next;
|
|
1098
|
+
});
|
|
1099
|
+
seen.delete(value);
|
|
1100
|
+
return output;
|
|
1101
|
+
}
|
|
1102
|
+
if (value instanceof Set) {
|
|
1103
|
+
if (seen.has(value)) return "[Circular]";
|
|
1104
|
+
seen.add(value);
|
|
1105
|
+
const output = Array.from(value, (item) => {
|
|
1106
|
+
const next = toJsonSafeValue(item, seen);
|
|
1107
|
+
return next === void 0 ? null : next;
|
|
1108
|
+
});
|
|
1109
|
+
seen.delete(value);
|
|
1110
|
+
return output;
|
|
1111
|
+
}
|
|
1112
|
+
if (value instanceof Map) {
|
|
1113
|
+
if (seen.has(value)) return "[Circular]";
|
|
1114
|
+
seen.add(value);
|
|
1115
|
+
const output = {};
|
|
1116
|
+
for (const [key, item] of value.entries()) {
|
|
1117
|
+
const normalizedKey = String(key);
|
|
1118
|
+
const next = toJsonSafeValue(item, seen);
|
|
1119
|
+
if (next !== void 0) {
|
|
1120
|
+
output[normalizedKey] = next;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
seen.delete(value);
|
|
1124
|
+
return output;
|
|
1125
|
+
}
|
|
1126
|
+
if (typeof value === "object") {
|
|
1127
|
+
const objectValue = value;
|
|
1128
|
+
if (seen.has(objectValue)) return "[Circular]";
|
|
1129
|
+
seen.add(objectValue);
|
|
1130
|
+
const output = {};
|
|
1131
|
+
for (const [key, item] of Object.entries(objectValue)) {
|
|
1132
|
+
const next = toJsonSafeValue(item, seen);
|
|
1133
|
+
if (next !== void 0) {
|
|
1134
|
+
output[key] = next;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
seen.delete(objectValue);
|
|
1138
|
+
return output;
|
|
1139
|
+
}
|
|
1140
|
+
return void 0;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
909
1143
|
// src/storage/namespace.ts
|
|
910
1144
|
var import_path2 = __toESM(require("path"), 1);
|
|
911
1145
|
var DEFAULT_NAMESPACE = "default";
|
|
@@ -969,11 +1203,12 @@ function dotenvFileOrder(nodeEnv) {
|
|
|
969
1203
|
files.push(".env");
|
|
970
1204
|
return files;
|
|
971
1205
|
}
|
|
972
|
-
function loadDotenvValues(rootDir, baseEnv) {
|
|
1206
|
+
function loadDotenvValues(rootDir, baseEnv, options = {}) {
|
|
973
1207
|
const values = {};
|
|
974
1208
|
if (parseBool(baseEnv.OPENSTEER_DISABLE_DOTENV_AUTOLOAD) === true) {
|
|
975
1209
|
return values;
|
|
976
1210
|
}
|
|
1211
|
+
const debug = options.debug ?? parseBool(baseEnv.OPENSTEER_DEBUG) === true;
|
|
977
1212
|
const baseDir = import_path3.default.resolve(rootDir);
|
|
978
1213
|
const nodeEnv = baseEnv.NODE_ENV?.trim() || "";
|
|
979
1214
|
for (const filename of dotenvFileOrder(nodeEnv)) {
|
|
@@ -987,15 +1222,24 @@ function loadDotenvValues(rootDir, baseEnv) {
|
|
|
987
1222
|
values[key] = value;
|
|
988
1223
|
}
|
|
989
1224
|
}
|
|
990
|
-
} catch {
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
const message = extractErrorMessage(
|
|
1227
|
+
error,
|
|
1228
|
+
"Unable to read or parse dotenv file."
|
|
1229
|
+
);
|
|
1230
|
+
if (debug) {
|
|
1231
|
+
console.warn(
|
|
1232
|
+
`[opensteer] failed to load dotenv file "${filePath}": ${message}`
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
991
1235
|
continue;
|
|
992
1236
|
}
|
|
993
1237
|
}
|
|
994
1238
|
return values;
|
|
995
1239
|
}
|
|
996
|
-
function resolveEnv(rootDir) {
|
|
1240
|
+
function resolveEnv(rootDir, options = {}) {
|
|
997
1241
|
const baseEnv = process.env;
|
|
998
|
-
const dotenvValues = loadDotenvValues(rootDir, baseEnv);
|
|
1242
|
+
const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
|
|
999
1243
|
return {
|
|
1000
1244
|
...dotenvValues,
|
|
1001
1245
|
...baseEnv
|
|
@@ -1039,13 +1283,22 @@ function assertNoLegacyRuntimeConfig(source, config) {
|
|
|
1039
1283
|
);
|
|
1040
1284
|
}
|
|
1041
1285
|
}
|
|
1042
|
-
function loadConfigFile(rootDir) {
|
|
1286
|
+
function loadConfigFile(rootDir, options = {}) {
|
|
1043
1287
|
const configPath = import_path3.default.join(rootDir, ".opensteer", "config.json");
|
|
1044
1288
|
if (!import_fs.default.existsSync(configPath)) return {};
|
|
1045
1289
|
try {
|
|
1046
1290
|
const raw = import_fs.default.readFileSync(configPath, "utf8");
|
|
1047
1291
|
return JSON.parse(raw);
|
|
1048
|
-
} catch {
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
const message = extractErrorMessage(
|
|
1294
|
+
error,
|
|
1295
|
+
"Unable to read or parse config file."
|
|
1296
|
+
);
|
|
1297
|
+
if (options.debug) {
|
|
1298
|
+
console.warn(
|
|
1299
|
+
`[opensteer] failed to load config file "${configPath}": ${message}`
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1049
1302
|
return {};
|
|
1050
1303
|
}
|
|
1051
1304
|
}
|
|
@@ -1177,6 +1430,8 @@ function resolveCloudSelection(config, env = process.env) {
|
|
|
1177
1430
|
};
|
|
1178
1431
|
}
|
|
1179
1432
|
function resolveConfig(input = {}) {
|
|
1433
|
+
const processEnv = process.env;
|
|
1434
|
+
const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
|
|
1180
1435
|
const initialRootDir = input.storage?.rootDir ?? process.cwd();
|
|
1181
1436
|
const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
|
|
1182
1437
|
storage: {
|
|
@@ -1185,12 +1440,16 @@ function resolveConfig(input = {}) {
|
|
|
1185
1440
|
});
|
|
1186
1441
|
assertNoLegacyAiConfig("Opensteer constructor config", input);
|
|
1187
1442
|
assertNoLegacyRuntimeConfig("Opensteer constructor config", input);
|
|
1188
|
-
const fileConfig = loadConfigFile(initialRootDir
|
|
1443
|
+
const fileConfig = loadConfigFile(initialRootDir, {
|
|
1444
|
+
debug: debugHint
|
|
1445
|
+
});
|
|
1189
1446
|
assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
|
|
1190
1447
|
assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
|
|
1191
1448
|
const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
|
|
1192
1449
|
const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
|
|
1193
|
-
const env = resolveEnv(envRootDir
|
|
1450
|
+
const env = resolveEnv(envRootDir, {
|
|
1451
|
+
debug: debugHint
|
|
1452
|
+
});
|
|
1194
1453
|
if (env.OPENSTEER_AI_MODEL) {
|
|
1195
1454
|
throw new Error(
|
|
1196
1455
|
"OPENSTEER_AI_MODEL is no longer supported. Use OPENSTEER_MODEL instead."
|
|
@@ -1874,9 +2133,11 @@ function createEmptyRegistry(name) {
|
|
|
1874
2133
|
var LocalSelectorStorage = class {
|
|
1875
2134
|
rootDir;
|
|
1876
2135
|
namespace;
|
|
1877
|
-
|
|
2136
|
+
debug;
|
|
2137
|
+
constructor(rootDir, namespace, options = {}) {
|
|
1878
2138
|
this.rootDir = rootDir;
|
|
1879
2139
|
this.namespace = normalizeNamespace(namespace);
|
|
2140
|
+
this.debug = options.debug === true;
|
|
1880
2141
|
}
|
|
1881
2142
|
getRootDir() {
|
|
1882
2143
|
return this.rootDir;
|
|
@@ -1910,7 +2171,16 @@ var LocalSelectorStorage = class {
|
|
|
1910
2171
|
try {
|
|
1911
2172
|
const raw = import_fs2.default.readFileSync(file, "utf8");
|
|
1912
2173
|
return JSON.parse(raw);
|
|
1913
|
-
} catch {
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
const message = extractErrorMessage(
|
|
2176
|
+
error,
|
|
2177
|
+
"Unable to parse selector registry JSON."
|
|
2178
|
+
);
|
|
2179
|
+
if (this.debug) {
|
|
2180
|
+
console.warn(
|
|
2181
|
+
`[opensteer] failed to read selector registry "${file}": ${message}`
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
1914
2184
|
return createEmptyRegistry(this.namespace);
|
|
1915
2185
|
}
|
|
1916
2186
|
}
|
|
@@ -1927,7 +2197,16 @@ var LocalSelectorStorage = class {
|
|
|
1927
2197
|
try {
|
|
1928
2198
|
const raw = import_fs2.default.readFileSync(file, "utf8");
|
|
1929
2199
|
return JSON.parse(raw);
|
|
1930
|
-
} catch {
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
const message = extractErrorMessage(
|
|
2202
|
+
error,
|
|
2203
|
+
"Unable to parse selector file JSON."
|
|
2204
|
+
);
|
|
2205
|
+
if (this.debug) {
|
|
2206
|
+
console.warn(
|
|
2207
|
+
`[opensteer] failed to read selector file "${file}": ${message}`
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
1931
2210
|
return null;
|
|
1932
2211
|
}
|
|
1933
2212
|
}
|
|
@@ -1947,7 +2226,6 @@ var LocalSelectorStorage = class {
|
|
|
1947
2226
|
|
|
1948
2227
|
// src/html/pipeline.ts
|
|
1949
2228
|
var cheerio3 = __toESM(require("cheerio"), 1);
|
|
1950
|
-
var import_crypto = require("crypto");
|
|
1951
2229
|
|
|
1952
2230
|
// src/html/serializer.ts
|
|
1953
2231
|
var cheerio = __toESM(require("cheerio"), 1);
|
|
@@ -2220,9 +2498,6 @@ var ENSURE_NAME_SHIM_SCRIPT = `
|
|
|
2220
2498
|
`;
|
|
2221
2499
|
var OS_FRAME_TOKEN_KEY = "__opensteerFrameToken";
|
|
2222
2500
|
var OS_INSTANCE_TOKEN_KEY = "__opensteerInstanceToken";
|
|
2223
|
-
var OS_COUNTER_OWNER_KEY = "__opensteerCounterOwner";
|
|
2224
|
-
var OS_COUNTER_VALUE_KEY = "__opensteerCounterValue";
|
|
2225
|
-
var OS_COUNTER_NEXT_KEY = "__opensteerCounterNext";
|
|
2226
2501
|
|
|
2227
2502
|
// src/element-path/build.ts
|
|
2228
2503
|
var MAX_ATTRIBUTE_VALUE_LENGTH = 300;
|
|
@@ -4012,567 +4287,178 @@ function cleanForAction(html) {
|
|
|
4012
4287
|
return compactHtml(htmlOut);
|
|
4013
4288
|
}
|
|
4014
4289
|
|
|
4015
|
-
// src/
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4290
|
+
// src/html/pipeline.ts
|
|
4291
|
+
function applyCleaner(mode, html) {
|
|
4292
|
+
switch (mode) {
|
|
4293
|
+
case "clickable":
|
|
4294
|
+
return cleanForClickable(html);
|
|
4295
|
+
case "scrollable":
|
|
4296
|
+
return cleanForScrollable(html);
|
|
4297
|
+
case "extraction":
|
|
4298
|
+
return cleanForExtraction(html);
|
|
4299
|
+
case "full":
|
|
4300
|
+
return cleanForFull(html);
|
|
4301
|
+
case "action":
|
|
4302
|
+
default:
|
|
4303
|
+
return cleanForAction(html);
|
|
4028
4304
|
}
|
|
4029
|
-
const text = rawText.replace(/\s+/g, " ").trim();
|
|
4030
|
-
return text || null;
|
|
4031
4305
|
}
|
|
4032
|
-
function
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
const
|
|
4039
|
-
|
|
4040
|
-
|
|
4306
|
+
async function assignCounters(page, html, nodePaths, nodeMeta) {
|
|
4307
|
+
const $ = cheerio3.load(html, { xmlMode: false });
|
|
4308
|
+
const counterIndex = /* @__PURE__ */ new Map();
|
|
4309
|
+
let nextCounter = 1;
|
|
4310
|
+
const assignedByNodeId = /* @__PURE__ */ new Map();
|
|
4311
|
+
$("*").each(function() {
|
|
4312
|
+
const el = $(this);
|
|
4313
|
+
const nodeId = el.attr(OS_NODE_ID_ATTR);
|
|
4314
|
+
if (!nodeId) return;
|
|
4315
|
+
const counter = nextCounter++;
|
|
4316
|
+
assignedByNodeId.set(nodeId, counter);
|
|
4317
|
+
const path5 = nodePaths.get(nodeId);
|
|
4318
|
+
el.attr("c", String(counter));
|
|
4319
|
+
el.removeAttr(OS_NODE_ID_ATTR);
|
|
4320
|
+
if (path5) {
|
|
4321
|
+
counterIndex.set(counter, cloneElementPath(path5));
|
|
4322
|
+
}
|
|
4323
|
+
});
|
|
4324
|
+
try {
|
|
4325
|
+
await syncLiveCounters(page, nodeMeta, assignedByNodeId);
|
|
4326
|
+
} catch (error) {
|
|
4327
|
+
await clearLiveCounters(page);
|
|
4328
|
+
throw error;
|
|
4041
4329
|
}
|
|
4042
|
-
|
|
4330
|
+
$(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
|
|
4331
|
+
return {
|
|
4332
|
+
html: $.html(),
|
|
4333
|
+
counterIndex
|
|
4334
|
+
};
|
|
4043
4335
|
}
|
|
4044
|
-
function
|
|
4045
|
-
|
|
4046
|
-
if (!
|
|
4047
|
-
const
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
(
|
|
4057
|
-
);
|
|
4058
|
-
if (densityCandidates.length) {
|
|
4059
|
-
return densityCandidates.reduce(
|
|
4060
|
-
(best, candidate) => candidate.density > best.density ? candidate : best
|
|
4061
|
-
).url;
|
|
4336
|
+
async function syncLiveCounters(page, nodeMeta, assignedByNodeId) {
|
|
4337
|
+
await clearLiveCounters(page);
|
|
4338
|
+
if (!assignedByNodeId.size) return;
|
|
4339
|
+
const groupedByFrame = /* @__PURE__ */ new Map();
|
|
4340
|
+
for (const [nodeId, counter] of assignedByNodeId.entries()) {
|
|
4341
|
+
const meta = nodeMeta.get(nodeId);
|
|
4342
|
+
if (!meta?.frameToken) continue;
|
|
4343
|
+
const list = groupedByFrame.get(meta.frameToken) || [];
|
|
4344
|
+
list.push({
|
|
4345
|
+
nodeId,
|
|
4346
|
+
counter
|
|
4347
|
+
});
|
|
4348
|
+
groupedByFrame.set(meta.frameToken, list);
|
|
4062
4349
|
}
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
const
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
if (!url) continue;
|
|
4077
|
-
index = skipWhitespace(text, index);
|
|
4078
|
-
const descriptors = [];
|
|
4079
|
-
while (index < text.length && text[index] !== ",") {
|
|
4080
|
-
const descriptorToken = readDescriptorToken(text, index);
|
|
4081
|
-
if (!descriptorToken.value) {
|
|
4082
|
-
index = descriptorToken.nextIndex;
|
|
4083
|
-
continue;
|
|
4350
|
+
if (!groupedByFrame.size) return;
|
|
4351
|
+
const failures = [];
|
|
4352
|
+
const framesByToken = await mapFramesByToken(page);
|
|
4353
|
+
for (const [frameToken, entries] of groupedByFrame.entries()) {
|
|
4354
|
+
const frame = framesByToken.get(frameToken);
|
|
4355
|
+
if (!frame) {
|
|
4356
|
+
for (const entry of entries) {
|
|
4357
|
+
failures.push({
|
|
4358
|
+
nodeId: entry.nodeId,
|
|
4359
|
+
counter: entry.counter,
|
|
4360
|
+
frameToken,
|
|
4361
|
+
reason: "frame_missing"
|
|
4362
|
+
});
|
|
4084
4363
|
}
|
|
4085
|
-
|
|
4086
|
-
index = descriptorToken.nextIndex;
|
|
4087
|
-
index = skipWhitespace(text, index);
|
|
4088
|
-
}
|
|
4089
|
-
if (index < text.length && text[index] === ",") {
|
|
4090
|
-
index += 1;
|
|
4364
|
+
continue;
|
|
4091
4365
|
}
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4366
|
+
try {
|
|
4367
|
+
const unresolved = await frame.evaluate(
|
|
4368
|
+
({ entries: entries2, nodeAttr }) => {
|
|
4369
|
+
const index = /* @__PURE__ */ new Map();
|
|
4370
|
+
const unresolved2 = [];
|
|
4371
|
+
const walk = (root) => {
|
|
4372
|
+
const children = Array.from(root.children);
|
|
4373
|
+
for (const child of children) {
|
|
4374
|
+
const nodeId = child.getAttribute(nodeAttr);
|
|
4375
|
+
if (nodeId) {
|
|
4376
|
+
const list = index.get(nodeId) || [];
|
|
4377
|
+
list.push(child);
|
|
4378
|
+
index.set(nodeId, list);
|
|
4379
|
+
}
|
|
4380
|
+
walk(child);
|
|
4381
|
+
if (child.shadowRoot) {
|
|
4382
|
+
walk(child.shadowRoot);
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
};
|
|
4386
|
+
walk(document);
|
|
4387
|
+
for (const entry of entries2) {
|
|
4388
|
+
const matches = index.get(entry.nodeId) || [];
|
|
4389
|
+
if (matches.length !== 1) {
|
|
4390
|
+
unresolved2.push({
|
|
4391
|
+
nodeId: entry.nodeId,
|
|
4392
|
+
counter: entry.counter,
|
|
4393
|
+
matches: matches.length
|
|
4394
|
+
});
|
|
4395
|
+
continue;
|
|
4396
|
+
}
|
|
4397
|
+
matches[0].setAttribute("c", String(entry.counter));
|
|
4398
|
+
}
|
|
4399
|
+
return unresolved2;
|
|
4400
|
+
},
|
|
4401
|
+
{
|
|
4402
|
+
entries,
|
|
4403
|
+
nodeAttr: OS_NODE_ID_ATTR
|
|
4102
4404
|
}
|
|
4103
|
-
|
|
4405
|
+
);
|
|
4406
|
+
for (const entry of unresolved) {
|
|
4407
|
+
failures.push({
|
|
4408
|
+
nodeId: entry.nodeId,
|
|
4409
|
+
counter: entry.counter,
|
|
4410
|
+
frameToken,
|
|
4411
|
+
reason: "match_count",
|
|
4412
|
+
matches: entry.matches
|
|
4413
|
+
});
|
|
4104
4414
|
}
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4415
|
+
} catch {
|
|
4416
|
+
for (const entry of entries) {
|
|
4417
|
+
failures.push({
|
|
4418
|
+
nodeId: entry.nodeId,
|
|
4419
|
+
counter: entry.counter,
|
|
4420
|
+
frameToken,
|
|
4421
|
+
reason: "frame_unavailable"
|
|
4422
|
+
});
|
|
4111
4423
|
}
|
|
4112
4424
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4425
|
+
}
|
|
4426
|
+
if (failures.length) {
|
|
4427
|
+
const preview = failures.slice(0, 3).map((failure) => {
|
|
4428
|
+
const base = `counter ${failure.counter} (nodeId "${failure.nodeId}") in frame "${failure.frameToken}"`;
|
|
4429
|
+
if (failure.reason === "frame_missing") {
|
|
4430
|
+
return `${base} could not be synchronized because the frame is missing.`;
|
|
4431
|
+
}
|
|
4432
|
+
if (failure.reason === "frame_unavailable") {
|
|
4433
|
+
return `${base} could not be synchronized because frame evaluation failed.`;
|
|
4434
|
+
}
|
|
4435
|
+
return `${base} expected exactly one live node but found ${failure.matches ?? 0}.`;
|
|
4117
4436
|
});
|
|
4437
|
+
const remaining = failures.length > 3 ? ` (+${failures.length - 3} more)` : "";
|
|
4438
|
+
throw new Error(
|
|
4439
|
+
`Failed to synchronize snapshot counters with the live DOM: ${preview.join(" ")}${remaining}`
|
|
4440
|
+
);
|
|
4118
4441
|
}
|
|
4119
|
-
return out;
|
|
4120
4442
|
}
|
|
4121
|
-
function
|
|
4122
|
-
const
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
const text = String(raw || "");
|
|
4127
|
-
const start = skipSeparators(text, 0);
|
|
4128
|
-
if (start >= text.length) return null;
|
|
4129
|
-
const firstToken = readUrlToken(text, start).value.trim();
|
|
4130
|
-
return firstToken || null;
|
|
4131
|
-
}
|
|
4132
|
-
function skipWhitespace(value, index) {
|
|
4133
|
-
let cursor = index;
|
|
4134
|
-
while (cursor < value.length && /\s/.test(value[cursor])) {
|
|
4135
|
-
cursor += 1;
|
|
4136
|
-
}
|
|
4137
|
-
return cursor;
|
|
4138
|
-
}
|
|
4139
|
-
function skipSeparators(value, index) {
|
|
4140
|
-
let cursor = skipWhitespace(value, index);
|
|
4141
|
-
while (cursor < value.length && value[cursor] === ",") {
|
|
4142
|
-
cursor += 1;
|
|
4143
|
-
cursor = skipWhitespace(value, cursor);
|
|
4144
|
-
}
|
|
4145
|
-
return cursor;
|
|
4146
|
-
}
|
|
4147
|
-
function readUrlToken(value, index) {
|
|
4148
|
-
let cursor = index;
|
|
4149
|
-
let out = "";
|
|
4150
|
-
const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
|
|
4151
|
-
while (cursor < value.length) {
|
|
4152
|
-
const char = value[cursor];
|
|
4153
|
-
if (/\s/.test(char)) {
|
|
4154
|
-
break;
|
|
4155
|
-
}
|
|
4156
|
-
if (char === "," && !isDataUrl) {
|
|
4157
|
-
break;
|
|
4158
|
-
}
|
|
4159
|
-
out += char;
|
|
4160
|
-
cursor += 1;
|
|
4161
|
-
}
|
|
4162
|
-
if (isDataUrl && out.endsWith(",") && cursor < value.length) {
|
|
4163
|
-
out = out.slice(0, -1);
|
|
4164
|
-
}
|
|
4165
|
-
return {
|
|
4166
|
-
value: out,
|
|
4167
|
-
nextIndex: cursor
|
|
4168
|
-
};
|
|
4169
|
-
}
|
|
4170
|
-
function readDescriptorToken(value, index) {
|
|
4171
|
-
let cursor = skipWhitespace(value, index);
|
|
4172
|
-
let out = "";
|
|
4173
|
-
while (cursor < value.length) {
|
|
4174
|
-
const char = value[cursor];
|
|
4175
|
-
if (char === "," || /\s/.test(char)) {
|
|
4176
|
-
break;
|
|
4177
|
-
}
|
|
4178
|
-
out += char;
|
|
4179
|
-
cursor += 1;
|
|
4180
|
-
}
|
|
4181
|
-
return {
|
|
4182
|
-
value: out.trim(),
|
|
4183
|
-
nextIndex: cursor
|
|
4184
|
-
};
|
|
4185
|
-
}
|
|
4186
|
-
|
|
4187
|
-
// src/html/counter-runtime.ts
|
|
4188
|
-
var CounterResolutionError = class extends Error {
|
|
4189
|
-
code;
|
|
4190
|
-
constructor(code, message) {
|
|
4191
|
-
super(message);
|
|
4192
|
-
this.name = "CounterResolutionError";
|
|
4193
|
-
this.code = code;
|
|
4194
|
-
}
|
|
4195
|
-
};
|
|
4196
|
-
async function ensureLiveCounters(page, nodeMeta, nodeIds) {
|
|
4197
|
-
const out = /* @__PURE__ */ new Map();
|
|
4198
|
-
if (!nodeIds.length) return out;
|
|
4199
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
4200
|
-
for (const nodeId of nodeIds) {
|
|
4201
|
-
const meta = nodeMeta.get(nodeId);
|
|
4202
|
-
if (!meta) {
|
|
4203
|
-
throw new CounterResolutionError(
|
|
4204
|
-
"ERR_COUNTER_STALE_OR_NOT_FOUND",
|
|
4205
|
-
`Missing metadata for node ${nodeId}. Run snapshot() again.`
|
|
4206
|
-
);
|
|
4207
|
-
}
|
|
4208
|
-
const list = grouped.get(meta.frameToken) || [];
|
|
4209
|
-
list.push({
|
|
4210
|
-
nodeId,
|
|
4211
|
-
instanceToken: meta.instanceToken
|
|
4212
|
-
});
|
|
4213
|
-
grouped.set(meta.frameToken, list);
|
|
4214
|
-
}
|
|
4215
|
-
const framesByToken = await mapFramesByToken(page);
|
|
4216
|
-
let nextCounter = await readGlobalNextCounter(page);
|
|
4217
|
-
const usedCounters = /* @__PURE__ */ new Map();
|
|
4218
|
-
for (const [frameToken, entries] of grouped.entries()) {
|
|
4219
|
-
const frame = framesByToken.get(frameToken);
|
|
4220
|
-
if (!frame) {
|
|
4221
|
-
throw new CounterResolutionError(
|
|
4222
|
-
"ERR_COUNTER_FRAME_UNAVAILABLE",
|
|
4223
|
-
`Counter frame ${frameToken} is unavailable. Run snapshot() again.`
|
|
4224
|
-
);
|
|
4225
|
-
}
|
|
4226
|
-
const result = await frame.evaluate(
|
|
4227
|
-
({
|
|
4228
|
-
entries: entries2,
|
|
4229
|
-
nodeAttr,
|
|
4230
|
-
instanceTokenKey,
|
|
4231
|
-
counterOwnerKey,
|
|
4232
|
-
counterValueKey,
|
|
4233
|
-
startCounter
|
|
4234
|
-
}) => {
|
|
4235
|
-
const helpers = {
|
|
4236
|
-
pushNode(map, node) {
|
|
4237
|
-
const nodeId = node.getAttribute(nodeAttr);
|
|
4238
|
-
if (!nodeId) return;
|
|
4239
|
-
const list = map.get(nodeId) || [];
|
|
4240
|
-
list.push(node);
|
|
4241
|
-
map.set(nodeId, list);
|
|
4242
|
-
},
|
|
4243
|
-
walk(map, root) {
|
|
4244
|
-
const children = Array.from(root.children);
|
|
4245
|
-
for (const child of children) {
|
|
4246
|
-
helpers.pushNode(map, child);
|
|
4247
|
-
helpers.walk(map, child);
|
|
4248
|
-
if (child.shadowRoot) {
|
|
4249
|
-
helpers.walk(map, child.shadowRoot);
|
|
4250
|
-
}
|
|
4251
|
-
}
|
|
4252
|
-
},
|
|
4253
|
-
buildNodeIndex() {
|
|
4254
|
-
const map = /* @__PURE__ */ new Map();
|
|
4255
|
-
helpers.walk(map, document);
|
|
4256
|
-
return map;
|
|
4257
|
-
}
|
|
4258
|
-
};
|
|
4259
|
-
const index = helpers.buildNodeIndex();
|
|
4260
|
-
const assigned = [];
|
|
4261
|
-
const failures = [];
|
|
4262
|
-
let next = Math.max(1, Number(startCounter || 1));
|
|
4263
|
-
for (const entry of entries2) {
|
|
4264
|
-
const matches = index.get(entry.nodeId) || [];
|
|
4265
|
-
if (!matches.length) {
|
|
4266
|
-
failures.push({
|
|
4267
|
-
nodeId: entry.nodeId,
|
|
4268
|
-
reason: "missing"
|
|
4269
|
-
});
|
|
4270
|
-
continue;
|
|
4271
|
-
}
|
|
4272
|
-
if (matches.length !== 1) {
|
|
4273
|
-
failures.push({
|
|
4274
|
-
nodeId: entry.nodeId,
|
|
4275
|
-
reason: "ambiguous"
|
|
4276
|
-
});
|
|
4277
|
-
continue;
|
|
4278
|
-
}
|
|
4279
|
-
const target = matches[0];
|
|
4280
|
-
if (target[instanceTokenKey] !== entry.instanceToken) {
|
|
4281
|
-
failures.push({
|
|
4282
|
-
nodeId: entry.nodeId,
|
|
4283
|
-
reason: "instance_mismatch"
|
|
4284
|
-
});
|
|
4285
|
-
continue;
|
|
4286
|
-
}
|
|
4287
|
-
const owned = target[counterOwnerKey] === true;
|
|
4288
|
-
const runtimeCounter = Number(target[counterValueKey] || 0);
|
|
4289
|
-
if (owned && Number.isFinite(runtimeCounter) && runtimeCounter > 0) {
|
|
4290
|
-
target.setAttribute("c", String(runtimeCounter));
|
|
4291
|
-
assigned.push({
|
|
4292
|
-
nodeId: entry.nodeId,
|
|
4293
|
-
counter: runtimeCounter
|
|
4294
|
-
});
|
|
4295
|
-
continue;
|
|
4296
|
-
}
|
|
4297
|
-
const counter = next++;
|
|
4298
|
-
target.setAttribute("c", String(counter));
|
|
4299
|
-
Object.defineProperty(target, counterOwnerKey, {
|
|
4300
|
-
value: true,
|
|
4301
|
-
writable: true,
|
|
4302
|
-
configurable: true
|
|
4303
|
-
});
|
|
4304
|
-
Object.defineProperty(target, counterValueKey, {
|
|
4305
|
-
value: counter,
|
|
4306
|
-
writable: true,
|
|
4307
|
-
configurable: true
|
|
4308
|
-
});
|
|
4309
|
-
assigned.push({ nodeId: entry.nodeId, counter });
|
|
4310
|
-
}
|
|
4311
|
-
return {
|
|
4312
|
-
assigned,
|
|
4313
|
-
failures,
|
|
4314
|
-
nextCounter: next
|
|
4315
|
-
};
|
|
4316
|
-
},
|
|
4317
|
-
{
|
|
4318
|
-
entries,
|
|
4319
|
-
nodeAttr: OS_NODE_ID_ATTR,
|
|
4320
|
-
instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
|
|
4321
|
-
counterOwnerKey: OS_COUNTER_OWNER_KEY,
|
|
4322
|
-
counterValueKey: OS_COUNTER_VALUE_KEY,
|
|
4323
|
-
startCounter: nextCounter
|
|
4324
|
-
}
|
|
4325
|
-
);
|
|
4326
|
-
if (result.failures.length) {
|
|
4327
|
-
const first = result.failures[0];
|
|
4328
|
-
throw buildCounterFailureError(first.nodeId, first.reason);
|
|
4329
|
-
}
|
|
4330
|
-
nextCounter = result.nextCounter;
|
|
4331
|
-
for (const item of result.assigned) {
|
|
4332
|
-
const existingNode = usedCounters.get(item.counter);
|
|
4333
|
-
if (existingNode && existingNode !== item.nodeId) {
|
|
4334
|
-
throw new CounterResolutionError(
|
|
4335
|
-
"ERR_COUNTER_AMBIGUOUS",
|
|
4336
|
-
`Counter ${item.counter} is assigned to multiple nodes (${existingNode}, ${item.nodeId}). Run snapshot() again.`
|
|
4337
|
-
);
|
|
4338
|
-
}
|
|
4339
|
-
usedCounters.set(item.counter, item.nodeId);
|
|
4340
|
-
out.set(item.nodeId, item.counter);
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
await writeGlobalNextCounter(page, nextCounter);
|
|
4344
|
-
return out;
|
|
4345
|
-
}
|
|
4346
|
-
async function resolveCounterElement(page, snapshot, counter) {
|
|
4347
|
-
const binding = readBinding(snapshot, counter);
|
|
4348
|
-
const framesByToken = await mapFramesByToken(page);
|
|
4349
|
-
const frame = framesByToken.get(binding.frameToken);
|
|
4350
|
-
if (!frame) {
|
|
4351
|
-
throw new CounterResolutionError(
|
|
4352
|
-
"ERR_COUNTER_FRAME_UNAVAILABLE",
|
|
4353
|
-
`Counter ${counter} frame is unavailable. Run snapshot() again.`
|
|
4354
|
-
);
|
|
4355
|
-
}
|
|
4356
|
-
const status = await frame.evaluate(
|
|
4357
|
-
({
|
|
4358
|
-
nodeId,
|
|
4359
|
-
instanceToken,
|
|
4360
|
-
counter: counter2,
|
|
4361
|
-
nodeAttr,
|
|
4362
|
-
instanceTokenKey,
|
|
4363
|
-
counterOwnerKey,
|
|
4364
|
-
counterValueKey
|
|
4365
|
-
}) => {
|
|
4366
|
-
const helpers = {
|
|
4367
|
-
walk(map, root) {
|
|
4368
|
-
const children = Array.from(root.children);
|
|
4369
|
-
for (const child of children) {
|
|
4370
|
-
const id = child.getAttribute(nodeAttr);
|
|
4371
|
-
if (id) {
|
|
4372
|
-
const list = map.get(id) || [];
|
|
4373
|
-
list.push(child);
|
|
4374
|
-
map.set(id, list);
|
|
4375
|
-
}
|
|
4376
|
-
helpers.walk(map, child);
|
|
4377
|
-
if (child.shadowRoot) {
|
|
4378
|
-
helpers.walk(map, child.shadowRoot);
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
},
|
|
4382
|
-
buildNodeIndex() {
|
|
4383
|
-
const map = /* @__PURE__ */ new Map();
|
|
4384
|
-
helpers.walk(map, document);
|
|
4385
|
-
return map;
|
|
4386
|
-
}
|
|
4387
|
-
};
|
|
4388
|
-
const matches = helpers.buildNodeIndex().get(nodeId) || [];
|
|
4389
|
-
if (!matches.length) return "missing";
|
|
4390
|
-
if (matches.length !== 1) return "ambiguous";
|
|
4391
|
-
const target = matches[0];
|
|
4392
|
-
if (target[instanceTokenKey] !== instanceToken) {
|
|
4393
|
-
return "instance_mismatch";
|
|
4394
|
-
}
|
|
4395
|
-
if (target[counterOwnerKey] !== true) {
|
|
4396
|
-
return "instance_mismatch";
|
|
4397
|
-
}
|
|
4398
|
-
if (Number(target[counterValueKey] || 0) !== counter2) {
|
|
4399
|
-
return "instance_mismatch";
|
|
4400
|
-
}
|
|
4401
|
-
if (target.getAttribute("c") !== String(counter2)) {
|
|
4402
|
-
return "instance_mismatch";
|
|
4403
|
-
}
|
|
4404
|
-
return "ok";
|
|
4405
|
-
},
|
|
4406
|
-
{
|
|
4407
|
-
nodeId: binding.nodeId,
|
|
4408
|
-
instanceToken: binding.instanceToken,
|
|
4409
|
-
counter,
|
|
4410
|
-
nodeAttr: OS_NODE_ID_ATTR,
|
|
4411
|
-
instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
|
|
4412
|
-
counterOwnerKey: OS_COUNTER_OWNER_KEY,
|
|
4413
|
-
counterValueKey: OS_COUNTER_VALUE_KEY
|
|
4414
|
-
}
|
|
4415
|
-
);
|
|
4416
|
-
if (status !== "ok") {
|
|
4417
|
-
throw buildCounterFailureError(binding.nodeId, status);
|
|
4418
|
-
}
|
|
4419
|
-
const handle = await frame.evaluateHandle(
|
|
4420
|
-
({ nodeId, nodeAttr }) => {
|
|
4421
|
-
const helpers = {
|
|
4422
|
-
walk(matches, root) {
|
|
4443
|
+
async function clearLiveCounters(page) {
|
|
4444
|
+
for (const frame of page.frames()) {
|
|
4445
|
+
try {
|
|
4446
|
+
await frame.evaluate(() => {
|
|
4447
|
+
const walk = (root) => {
|
|
4423
4448
|
const children = Array.from(root.children);
|
|
4424
4449
|
for (const child of children) {
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
}
|
|
4428
|
-
helpers.walk(matches, child);
|
|
4450
|
+
child.removeAttribute("c");
|
|
4451
|
+
walk(child);
|
|
4429
4452
|
if (child.shadowRoot) {
|
|
4430
|
-
|
|
4431
|
-
}
|
|
4432
|
-
}
|
|
4433
|
-
},
|
|
4434
|
-
findUniqueNode() {
|
|
4435
|
-
const matches = [];
|
|
4436
|
-
helpers.walk(matches, document);
|
|
4437
|
-
if (matches.length !== 1) return null;
|
|
4438
|
-
return matches[0];
|
|
4439
|
-
}
|
|
4440
|
-
};
|
|
4441
|
-
return helpers.findUniqueNode();
|
|
4442
|
-
},
|
|
4443
|
-
{
|
|
4444
|
-
nodeId: binding.nodeId,
|
|
4445
|
-
nodeAttr: OS_NODE_ID_ATTR
|
|
4446
|
-
}
|
|
4447
|
-
);
|
|
4448
|
-
const element = handle.asElement();
|
|
4449
|
-
if (!element) {
|
|
4450
|
-
await handle.dispose();
|
|
4451
|
-
throw new CounterResolutionError(
|
|
4452
|
-
"ERR_COUNTER_STALE_OR_NOT_FOUND",
|
|
4453
|
-
`Counter ${counter} became stale. Run snapshot() again.`
|
|
4454
|
-
);
|
|
4455
|
-
}
|
|
4456
|
-
return element;
|
|
4457
|
-
}
|
|
4458
|
-
async function resolveCountersBatch(page, snapshot, requests) {
|
|
4459
|
-
const out = {};
|
|
4460
|
-
if (!requests.length) return out;
|
|
4461
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
4462
|
-
for (const request of requests) {
|
|
4463
|
-
const binding = readBinding(snapshot, request.counter);
|
|
4464
|
-
const list = grouped.get(binding.frameToken) || [];
|
|
4465
|
-
list.push({
|
|
4466
|
-
...request,
|
|
4467
|
-
...binding
|
|
4468
|
-
});
|
|
4469
|
-
grouped.set(binding.frameToken, list);
|
|
4470
|
-
}
|
|
4471
|
-
const framesByToken = await mapFramesByToken(page);
|
|
4472
|
-
for (const [frameToken, entries] of grouped.entries()) {
|
|
4473
|
-
const frame = framesByToken.get(frameToken);
|
|
4474
|
-
if (!frame) {
|
|
4475
|
-
throw new CounterResolutionError(
|
|
4476
|
-
"ERR_COUNTER_FRAME_UNAVAILABLE",
|
|
4477
|
-
`Counter frame ${frameToken} is unavailable. Run snapshot() again.`
|
|
4478
|
-
);
|
|
4479
|
-
}
|
|
4480
|
-
const result = await frame.evaluate(
|
|
4481
|
-
({
|
|
4482
|
-
entries: entries2,
|
|
4483
|
-
nodeAttr,
|
|
4484
|
-
instanceTokenKey,
|
|
4485
|
-
counterOwnerKey,
|
|
4486
|
-
counterValueKey
|
|
4487
|
-
}) => {
|
|
4488
|
-
const values = [];
|
|
4489
|
-
const failures = [];
|
|
4490
|
-
const helpers = {
|
|
4491
|
-
walk(map, root) {
|
|
4492
|
-
const children = Array.from(root.children);
|
|
4493
|
-
for (const child of children) {
|
|
4494
|
-
const id = child.getAttribute(nodeAttr);
|
|
4495
|
-
if (id) {
|
|
4496
|
-
const list = map.get(id) || [];
|
|
4497
|
-
list.push(child);
|
|
4498
|
-
map.set(id, list);
|
|
4499
|
-
}
|
|
4500
|
-
helpers.walk(map, child);
|
|
4501
|
-
if (child.shadowRoot) {
|
|
4502
|
-
helpers.walk(map, child.shadowRoot);
|
|
4503
|
-
}
|
|
4453
|
+
walk(child.shadowRoot);
|
|
4504
4454
|
}
|
|
4505
|
-
},
|
|
4506
|
-
buildNodeIndex() {
|
|
4507
|
-
const map = /* @__PURE__ */ new Map();
|
|
4508
|
-
helpers.walk(map, document);
|
|
4509
|
-
return map;
|
|
4510
|
-
},
|
|
4511
|
-
readRawValue(element, attribute) {
|
|
4512
|
-
if (attribute) {
|
|
4513
|
-
return element.getAttribute(attribute);
|
|
4514
|
-
}
|
|
4515
|
-
return element.textContent;
|
|
4516
|
-
}
|
|
4517
|
-
};
|
|
4518
|
-
const index = helpers.buildNodeIndex();
|
|
4519
|
-
for (const entry of entries2) {
|
|
4520
|
-
const matches = index.get(entry.nodeId) || [];
|
|
4521
|
-
if (!matches.length) {
|
|
4522
|
-
failures.push({
|
|
4523
|
-
nodeId: entry.nodeId,
|
|
4524
|
-
reason: "missing"
|
|
4525
|
-
});
|
|
4526
|
-
continue;
|
|
4527
|
-
}
|
|
4528
|
-
if (matches.length !== 1) {
|
|
4529
|
-
failures.push({
|
|
4530
|
-
nodeId: entry.nodeId,
|
|
4531
|
-
reason: "ambiguous"
|
|
4532
|
-
});
|
|
4533
|
-
continue;
|
|
4534
4455
|
}
|
|
4535
|
-
const target = matches[0];
|
|
4536
|
-
if (target[instanceTokenKey] !== entry.instanceToken || target[counterOwnerKey] !== true || Number(target[counterValueKey] || 0) !== entry.counter || target.getAttribute("c") !== String(entry.counter)) {
|
|
4537
|
-
failures.push({
|
|
4538
|
-
nodeId: entry.nodeId,
|
|
4539
|
-
reason: "instance_mismatch"
|
|
4540
|
-
});
|
|
4541
|
-
continue;
|
|
4542
|
-
}
|
|
4543
|
-
values.push({
|
|
4544
|
-
key: entry.key,
|
|
4545
|
-
value: helpers.readRawValue(target, entry.attribute)
|
|
4546
|
-
});
|
|
4547
|
-
}
|
|
4548
|
-
return {
|
|
4549
|
-
values,
|
|
4550
|
-
failures
|
|
4551
4456
|
};
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
nodeAttr: OS_NODE_ID_ATTR,
|
|
4556
|
-
instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
|
|
4557
|
-
counterOwnerKey: OS_COUNTER_OWNER_KEY,
|
|
4558
|
-
counterValueKey: OS_COUNTER_VALUE_KEY
|
|
4559
|
-
}
|
|
4560
|
-
);
|
|
4561
|
-
if (result.failures.length) {
|
|
4562
|
-
const first = result.failures[0];
|
|
4563
|
-
throw buildCounterFailureError(first.nodeId, first.reason);
|
|
4564
|
-
}
|
|
4565
|
-
const attributeByKey = new Map(
|
|
4566
|
-
entries.map((entry) => [entry.key, entry.attribute])
|
|
4567
|
-
);
|
|
4568
|
-
for (const item of result.values) {
|
|
4569
|
-
out[item.key] = normalizeExtractedValue(
|
|
4570
|
-
item.value,
|
|
4571
|
-
attributeByKey.get(item.key)
|
|
4572
|
-
);
|
|
4457
|
+
walk(document);
|
|
4458
|
+
});
|
|
4459
|
+
} catch {
|
|
4573
4460
|
}
|
|
4574
4461
|
}
|
|
4575
|
-
return out;
|
|
4576
4462
|
}
|
|
4577
4463
|
async function mapFramesByToken(page) {
|
|
4578
4464
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -4594,539 +4480,760 @@ async function readFrameToken(frame) {
|
|
|
4594
4480
|
return null;
|
|
4595
4481
|
}
|
|
4596
4482
|
}
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
if (Number.isFinite(current) && current > 0) {
|
|
4603
|
-
return current;
|
|
4604
|
-
}
|
|
4605
|
-
let max = 0;
|
|
4606
|
-
for (const frame of page.frames()) {
|
|
4607
|
-
try {
|
|
4608
|
-
const frameMax = await frame.evaluate(
|
|
4609
|
-
({ nodeAttr, counterOwnerKey, counterValueKey }) => {
|
|
4610
|
-
let localMax = 0;
|
|
4611
|
-
const helpers = {
|
|
4612
|
-
walk(root) {
|
|
4613
|
-
const children = Array.from(
|
|
4614
|
-
root.children
|
|
4615
|
-
);
|
|
4616
|
-
for (const child of children) {
|
|
4617
|
-
const candidate = child;
|
|
4618
|
-
const hasNodeId = child.hasAttribute(nodeAttr);
|
|
4619
|
-
const owned = candidate[counterOwnerKey] === true;
|
|
4620
|
-
if (hasNodeId && owned) {
|
|
4621
|
-
const value = Number(
|
|
4622
|
-
candidate[counterValueKey] || 0
|
|
4623
|
-
);
|
|
4624
|
-
if (Number.isFinite(value) && value > localMax) {
|
|
4625
|
-
localMax = value;
|
|
4626
|
-
}
|
|
4627
|
-
}
|
|
4628
|
-
helpers.walk(child);
|
|
4629
|
-
if (child.shadowRoot) {
|
|
4630
|
-
helpers.walk(child.shadowRoot);
|
|
4631
|
-
}
|
|
4632
|
-
}
|
|
4633
|
-
}
|
|
4634
|
-
};
|
|
4635
|
-
helpers.walk(document);
|
|
4636
|
-
return localMax;
|
|
4637
|
-
},
|
|
4638
|
-
{
|
|
4639
|
-
nodeAttr: OS_NODE_ID_ATTR,
|
|
4640
|
-
counterOwnerKey: OS_COUNTER_OWNER_KEY,
|
|
4641
|
-
counterValueKey: OS_COUNTER_VALUE_KEY
|
|
4642
|
-
}
|
|
4643
|
-
);
|
|
4644
|
-
if (frameMax > max) {
|
|
4645
|
-
max = frameMax;
|
|
4646
|
-
}
|
|
4647
|
-
} catch {
|
|
4648
|
-
}
|
|
4649
|
-
}
|
|
4650
|
-
const next = max + 1;
|
|
4651
|
-
await writeGlobalNextCounter(page, next);
|
|
4652
|
-
return next;
|
|
4653
|
-
}
|
|
4654
|
-
async function writeGlobalNextCounter(page, nextCounter) {
|
|
4655
|
-
await page.mainFrame().evaluate(
|
|
4656
|
-
({ counterNextKey, nextCounter: nextCounter2 }) => {
|
|
4657
|
-
const win = window;
|
|
4658
|
-
win[counterNextKey] = nextCounter2;
|
|
4659
|
-
},
|
|
4660
|
-
{
|
|
4661
|
-
counterNextKey: OS_COUNTER_NEXT_KEY,
|
|
4662
|
-
nextCounter
|
|
4663
|
-
}
|
|
4664
|
-
).catch(() => void 0);
|
|
4483
|
+
function stripNodeIds(html) {
|
|
4484
|
+
if (!html.includes(OS_NODE_ID_ATTR)) return html;
|
|
4485
|
+
const $ = cheerio3.load(html, { xmlMode: false });
|
|
4486
|
+
$(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
|
|
4487
|
+
return $.html();
|
|
4665
4488
|
}
|
|
4666
|
-
function
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
);
|
|
4489
|
+
async function prepareSnapshot(page, options = {}) {
|
|
4490
|
+
const mode = options.mode ?? "action";
|
|
4491
|
+
const withCounters = options.withCounters ?? true;
|
|
4492
|
+
const shouldMarkInteractive = options.markInteractive ?? true;
|
|
4493
|
+
if (shouldMarkInteractive) {
|
|
4494
|
+
await markInteractiveElements(page);
|
|
4672
4495
|
}
|
|
4673
|
-
const
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4496
|
+
const serialized = await serializePageHTML(page);
|
|
4497
|
+
const rawHtml = serialized.html;
|
|
4498
|
+
const processedHtml = rawHtml;
|
|
4499
|
+
const reducedHtml = applyCleaner(mode, processedHtml);
|
|
4500
|
+
let cleanedHtml = reducedHtml;
|
|
4501
|
+
let counterIndex = null;
|
|
4502
|
+
if (withCounters) {
|
|
4503
|
+
const counted = await assignCounters(
|
|
4504
|
+
page,
|
|
4505
|
+
reducedHtml,
|
|
4506
|
+
serialized.nodePaths,
|
|
4507
|
+
serialized.nodeMeta
|
|
4678
4508
|
);
|
|
4509
|
+
cleanedHtml = counted.html;
|
|
4510
|
+
counterIndex = counted.counterIndex;
|
|
4511
|
+
} else {
|
|
4512
|
+
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
4679
4513
|
}
|
|
4680
|
-
if (
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
`Counter ${counter} is stale for this snapshot session. Run snapshot() again.`
|
|
4684
|
-
);
|
|
4514
|
+
if (mode === "extraction") {
|
|
4515
|
+
const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
|
|
4516
|
+
cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
|
|
4685
4517
|
}
|
|
4686
|
-
return
|
|
4518
|
+
return {
|
|
4519
|
+
mode,
|
|
4520
|
+
url: page.url(),
|
|
4521
|
+
rawHtml,
|
|
4522
|
+
processedHtml,
|
|
4523
|
+
reducedHtml,
|
|
4524
|
+
cleanedHtml,
|
|
4525
|
+
counterIndex
|
|
4526
|
+
};
|
|
4687
4527
|
}
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4528
|
+
|
|
4529
|
+
// src/element-path/errors.ts
|
|
4530
|
+
var ElementPathError = class extends Error {
|
|
4531
|
+
code;
|
|
4532
|
+
constructor(code, message) {
|
|
4533
|
+
super(message);
|
|
4534
|
+
this.name = "ElementPathError";
|
|
4535
|
+
this.code = code;
|
|
4536
|
+
}
|
|
4537
|
+
};
|
|
4538
|
+
|
|
4539
|
+
// src/element-path/resolver.ts
|
|
4540
|
+
async function resolveElementPath(page, rawPath) {
|
|
4541
|
+
const path5 = sanitizeElementPath(rawPath);
|
|
4542
|
+
let frame = page.mainFrame();
|
|
4543
|
+
let rootHandle = null;
|
|
4544
|
+
for (const hop of path5.context) {
|
|
4545
|
+
const host = await resolveDomPath(frame, hop.host, rootHandle);
|
|
4546
|
+
if (!host) {
|
|
4547
|
+
await disposeHandle(rootHandle);
|
|
4548
|
+
throw new ElementPathError(
|
|
4549
|
+
"ERR_PATH_CONTEXT_HOST_NOT_FOUND",
|
|
4550
|
+
"Unable to resolve context host from stored match selectors."
|
|
4551
|
+
);
|
|
4552
|
+
}
|
|
4553
|
+
if (hop.kind === "iframe") {
|
|
4554
|
+
const nextFrame = await host.element.contentFrame();
|
|
4555
|
+
await host.element.dispose();
|
|
4556
|
+
await disposeHandle(rootHandle);
|
|
4557
|
+
rootHandle = null;
|
|
4558
|
+
if (!nextFrame) {
|
|
4559
|
+
throw new ElementPathError(
|
|
4560
|
+
"ERR_PATH_IFRAME_UNAVAILABLE",
|
|
4561
|
+
"Iframe is unavailable or inaccessible for this path."
|
|
4562
|
+
);
|
|
4563
|
+
}
|
|
4564
|
+
frame = nextFrame;
|
|
4565
|
+
continue;
|
|
4566
|
+
}
|
|
4567
|
+
const shadowRoot = await host.element.evaluateHandle(
|
|
4568
|
+
(element) => element.shadowRoot
|
|
4569
|
+
);
|
|
4570
|
+
await host.element.dispose();
|
|
4571
|
+
const isMissing = await shadowRoot.evaluate((value) => value == null);
|
|
4572
|
+
if (isMissing) {
|
|
4573
|
+
await shadowRoot.dispose();
|
|
4574
|
+
await disposeHandle(rootHandle);
|
|
4575
|
+
throw new ElementPathError(
|
|
4576
|
+
"ERR_PATH_SHADOW_ROOT_UNAVAILABLE",
|
|
4577
|
+
"Shadow root is unavailable for this path."
|
|
4578
|
+
);
|
|
4579
|
+
}
|
|
4580
|
+
await disposeHandle(rootHandle);
|
|
4581
|
+
rootHandle = shadowRoot;
|
|
4582
|
+
}
|
|
4583
|
+
const target = await resolveDomPath(frame, path5.nodes, rootHandle);
|
|
4584
|
+
if (!target) {
|
|
4585
|
+
const diagnostics = await collectCandidateDiagnostics(
|
|
4586
|
+
frame,
|
|
4587
|
+
path5.nodes,
|
|
4588
|
+
rootHandle
|
|
4589
|
+
);
|
|
4590
|
+
await disposeHandle(rootHandle);
|
|
4591
|
+
throw new ElementPathError(
|
|
4592
|
+
"ERR_PATH_TARGET_NOT_FOUND",
|
|
4593
|
+
buildTargetNotFoundMessage(path5.nodes, diagnostics)
|
|
4693
4594
|
);
|
|
4694
4595
|
}
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4596
|
+
await disposeHandle(rootHandle);
|
|
4597
|
+
if (isPathDebugEnabled()) {
|
|
4598
|
+
debugPath("resolved", {
|
|
4599
|
+
selector: target.selector,
|
|
4600
|
+
mode: target.mode,
|
|
4601
|
+
count: target.count,
|
|
4602
|
+
targetDepth: path5.nodes.length
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
return {
|
|
4606
|
+
element: target.element,
|
|
4607
|
+
usedSelector: target.selector || buildPathSelectorHint(path5)
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
async function resolveDomPath(frame, domPath, rootHandle) {
|
|
4611
|
+
const candidates = buildPathCandidates(domPath);
|
|
4612
|
+
if (!candidates.length) return null;
|
|
4613
|
+
if (isPathDebugEnabled()) {
|
|
4614
|
+
debugPath("trying selectors", { candidates });
|
|
4615
|
+
}
|
|
4616
|
+
const selected = rootHandle ? await rootHandle.evaluate(selectInRoot, candidates) : await frame.evaluate(selectInDocument, candidates);
|
|
4617
|
+
if (!selected || !selected.selector) return null;
|
|
4618
|
+
const handle = rootHandle ? await rootHandle.evaluateHandle((root, selector) => {
|
|
4619
|
+
if (!(root instanceof ShadowRoot)) return null;
|
|
4620
|
+
return root.querySelector(selector);
|
|
4621
|
+
}, selected.selector) : await frame.evaluateHandle(
|
|
4622
|
+
(selector) => document.querySelector(selector),
|
|
4623
|
+
selected.selector
|
|
4698
4624
|
);
|
|
4625
|
+
const element = handle.asElement();
|
|
4626
|
+
if (!element) {
|
|
4627
|
+
await handle.dispose();
|
|
4628
|
+
return null;
|
|
4629
|
+
}
|
|
4630
|
+
return {
|
|
4631
|
+
element,
|
|
4632
|
+
selector: selected.selector,
|
|
4633
|
+
mode: selected.mode,
|
|
4634
|
+
count: selected.count
|
|
4635
|
+
};
|
|
4699
4636
|
}
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4637
|
+
async function collectCandidateDiagnostics(frame, domPath, rootHandle) {
|
|
4638
|
+
const candidates = buildPathCandidates(domPath);
|
|
4639
|
+
if (!candidates.length) return [];
|
|
4640
|
+
const diagnostics = rootHandle ? await rootHandle.evaluate(countInRoot, candidates) : await frame.evaluate(countInDocument, candidates);
|
|
4641
|
+
return Array.isArray(diagnostics) ? diagnostics.map((item) => ({
|
|
4642
|
+
selector: String(item?.selector || ""),
|
|
4643
|
+
count: Number(item?.count || 0)
|
|
4644
|
+
})).filter((item) => item.selector) : [];
|
|
4645
|
+
}
|
|
4646
|
+
function buildTargetNotFoundMessage(domPath, diagnostics) {
|
|
4647
|
+
const depth = Array.isArray(domPath) ? domPath.length : 0;
|
|
4648
|
+
const sample = diagnostics.slice(0, 4).map((item) => `"${item.selector}" => ${item.count}`).join(", ");
|
|
4649
|
+
const base = "Element path resolution failed (ERR_PATH_TARGET_NOT_FOUND): no selector candidate matched the current DOM.";
|
|
4650
|
+
if (!sample)
|
|
4651
|
+
return `${base} Tried ${Math.max(diagnostics.length, 0)} candidates.`;
|
|
4652
|
+
return `${base} Target depth ${depth}. Candidate counts: ${sample}.`;
|
|
4653
|
+
}
|
|
4654
|
+
function selectInDocument(selectors) {
|
|
4655
|
+
let fallback = null;
|
|
4656
|
+
for (const selector of selectors) {
|
|
4657
|
+
if (!selector) continue;
|
|
4658
|
+
let count = 0;
|
|
4659
|
+
try {
|
|
4660
|
+
count = document.querySelectorAll(selector).length;
|
|
4661
|
+
} catch {
|
|
4662
|
+
count = 0;
|
|
4663
|
+
}
|
|
4664
|
+
if (count === 1) {
|
|
4665
|
+
return {
|
|
4666
|
+
selector,
|
|
4667
|
+
count,
|
|
4668
|
+
mode: "unique"
|
|
4669
|
+
};
|
|
4670
|
+
}
|
|
4671
|
+
if (count > 1 && !fallback) {
|
|
4672
|
+
fallback = {
|
|
4673
|
+
selector,
|
|
4674
|
+
count,
|
|
4675
|
+
mode: "fallback"
|
|
4676
|
+
};
|
|
4677
|
+
}
|
|
4715
4678
|
}
|
|
4679
|
+
return fallback;
|
|
4716
4680
|
}
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
const
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
});
|
|
4728
|
-
const countersByNodeId = await ensureLiveCounters(
|
|
4729
|
-
page,
|
|
4730
|
-
nodeMeta,
|
|
4731
|
-
orderedNodeIds
|
|
4732
|
-
);
|
|
4733
|
-
$("*").each(function() {
|
|
4734
|
-
const el = $(this);
|
|
4735
|
-
const nodeId = el.attr(OS_NODE_ID_ATTR);
|
|
4736
|
-
if (!nodeId) return;
|
|
4737
|
-
const path5 = nodePaths.get(nodeId);
|
|
4738
|
-
const meta = nodeMeta.get(nodeId);
|
|
4739
|
-
const counter = countersByNodeId.get(nodeId);
|
|
4740
|
-
if (counter == null || !Number.isFinite(counter)) {
|
|
4741
|
-
throw new Error(
|
|
4742
|
-
`Counter assignment failed for node ${nodeId}. Run snapshot() again.`
|
|
4743
|
-
);
|
|
4681
|
+
function selectInRoot(root, selectors) {
|
|
4682
|
+
if (!(root instanceof ShadowRoot)) return null;
|
|
4683
|
+
let fallback = null;
|
|
4684
|
+
for (const selector of selectors) {
|
|
4685
|
+
if (!selector) continue;
|
|
4686
|
+
let count = 0;
|
|
4687
|
+
try {
|
|
4688
|
+
count = root.querySelectorAll(selector).length;
|
|
4689
|
+
} catch {
|
|
4690
|
+
count = 0;
|
|
4744
4691
|
}
|
|
4745
|
-
if (
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4692
|
+
if (count === 1) {
|
|
4693
|
+
return {
|
|
4694
|
+
selector,
|
|
4695
|
+
count,
|
|
4696
|
+
mode: "unique"
|
|
4697
|
+
};
|
|
4749
4698
|
}
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4699
|
+
if (count > 1 && !fallback) {
|
|
4700
|
+
fallback = {
|
|
4701
|
+
selector,
|
|
4702
|
+
count,
|
|
4703
|
+
mode: "fallback"
|
|
4704
|
+
};
|
|
4754
4705
|
}
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4706
|
+
}
|
|
4707
|
+
return fallback;
|
|
4708
|
+
}
|
|
4709
|
+
function countInDocument(selectors) {
|
|
4710
|
+
const out = [];
|
|
4711
|
+
for (const selector of selectors) {
|
|
4712
|
+
if (!selector) continue;
|
|
4713
|
+
let count = 0;
|
|
4714
|
+
try {
|
|
4715
|
+
count = document.querySelectorAll(selector).length;
|
|
4716
|
+
} catch {
|
|
4717
|
+
count = 0;
|
|
4762
4718
|
}
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
return
|
|
4766
|
-
html: $.html(),
|
|
4767
|
-
counterIndex,
|
|
4768
|
-
counterBindings
|
|
4769
|
-
};
|
|
4719
|
+
out.push({ selector, count });
|
|
4720
|
+
}
|
|
4721
|
+
return out;
|
|
4770
4722
|
}
|
|
4771
|
-
function
|
|
4772
|
-
if (!
|
|
4773
|
-
const
|
|
4774
|
-
|
|
4775
|
-
|
|
4723
|
+
function countInRoot(root, selectors) {
|
|
4724
|
+
if (!(root instanceof ShadowRoot)) return [];
|
|
4725
|
+
const out = [];
|
|
4726
|
+
for (const selector of selectors) {
|
|
4727
|
+
if (!selector) continue;
|
|
4728
|
+
let count = 0;
|
|
4729
|
+
try {
|
|
4730
|
+
count = root.querySelectorAll(selector).length;
|
|
4731
|
+
} catch {
|
|
4732
|
+
count = 0;
|
|
4733
|
+
}
|
|
4734
|
+
out.push({ selector, count });
|
|
4735
|
+
}
|
|
4736
|
+
return out;
|
|
4776
4737
|
}
|
|
4777
|
-
|
|
4778
|
-
const
|
|
4779
|
-
|
|
4780
|
-
const
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4738
|
+
function isPathDebugEnabled() {
|
|
4739
|
+
const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
|
|
4740
|
+
if (!value) return false;
|
|
4741
|
+
const normalized = value.trim().toLowerCase();
|
|
4742
|
+
return normalized === "1" || normalized === "true";
|
|
4743
|
+
}
|
|
4744
|
+
function debugPath(message, data) {
|
|
4745
|
+
if (!isPathDebugEnabled()) return;
|
|
4746
|
+
if (data !== void 0) {
|
|
4747
|
+
console.log(`[opensteer:path] ${message}`, data);
|
|
4748
|
+
} else {
|
|
4749
|
+
console.log(`[opensteer:path] ${message}`);
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
async function disposeHandle(handle) {
|
|
4753
|
+
if (!handle) return;
|
|
4754
|
+
try {
|
|
4755
|
+
await handle.dispose();
|
|
4756
|
+
} catch {
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
// src/actions/actionability-probe.ts
|
|
4761
|
+
async function probeActionabilityState(element) {
|
|
4762
|
+
try {
|
|
4763
|
+
return await element.evaluate((target) => {
|
|
4764
|
+
if (!(target instanceof Element)) {
|
|
4765
|
+
return {
|
|
4766
|
+
connected: false,
|
|
4767
|
+
visible: null,
|
|
4768
|
+
enabled: null,
|
|
4769
|
+
editable: null,
|
|
4770
|
+
blocker: null
|
|
4771
|
+
};
|
|
4772
|
+
}
|
|
4773
|
+
const connected = target.isConnected;
|
|
4774
|
+
if (!connected) {
|
|
4775
|
+
return {
|
|
4776
|
+
connected: false,
|
|
4777
|
+
visible: null,
|
|
4778
|
+
enabled: null,
|
|
4779
|
+
editable: null,
|
|
4780
|
+
blocker: null
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
const style = window.getComputedStyle(target);
|
|
4784
|
+
const rect = target.getBoundingClientRect();
|
|
4785
|
+
const hasBox = rect.width > 0 && rect.height > 0;
|
|
4786
|
+
const opacity = Number.parseFloat(style.opacity || "1");
|
|
4787
|
+
const isVisible = hasBox && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && (!Number.isFinite(opacity) || opacity > 0);
|
|
4788
|
+
let enabled = null;
|
|
4789
|
+
if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLOptionElement || target instanceof HTMLOptGroupElement || target instanceof HTMLFieldSetElement) {
|
|
4790
|
+
enabled = !target.disabled;
|
|
4791
|
+
}
|
|
4792
|
+
let editable = null;
|
|
4793
|
+
if (target instanceof HTMLInputElement) {
|
|
4794
|
+
editable = !target.readOnly && !target.disabled;
|
|
4795
|
+
} else if (target instanceof HTMLTextAreaElement) {
|
|
4796
|
+
editable = !target.readOnly && !target.disabled;
|
|
4797
|
+
} else if (target instanceof HTMLSelectElement) {
|
|
4798
|
+
editable = !target.disabled;
|
|
4799
|
+
} else if (target instanceof HTMLElement && target.isContentEditable) {
|
|
4800
|
+
editable = true;
|
|
4801
|
+
}
|
|
4802
|
+
let blocker = null;
|
|
4803
|
+
if (hasBox && window.innerWidth > 0 && window.innerHeight > 0) {
|
|
4804
|
+
const x = Math.min(
|
|
4805
|
+
Math.max(rect.left + rect.width / 2, 0),
|
|
4806
|
+
window.innerWidth - 1
|
|
4807
|
+
);
|
|
4808
|
+
const y = Math.min(
|
|
4809
|
+
Math.max(rect.top + rect.height / 2, 0),
|
|
4810
|
+
window.innerHeight - 1
|
|
4811
|
+
);
|
|
4812
|
+
const top = document.elementFromPoint(x, y);
|
|
4813
|
+
if (top && top !== target && !target.contains(top)) {
|
|
4814
|
+
const classes = String(top.className || "").split(/\s+/).map((value) => value.trim()).filter(Boolean).slice(0, 5);
|
|
4815
|
+
blocker = {
|
|
4816
|
+
tag: top.tagName.toLowerCase(),
|
|
4817
|
+
id: top.id || null,
|
|
4818
|
+
classes,
|
|
4819
|
+
role: top.getAttribute("role"),
|
|
4820
|
+
text: (top.textContent || "").trim().slice(0, 80) || null
|
|
4821
|
+
};
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
return {
|
|
4825
|
+
connected,
|
|
4826
|
+
visible: isVisible,
|
|
4827
|
+
enabled,
|
|
4828
|
+
editable,
|
|
4829
|
+
blocker
|
|
4830
|
+
};
|
|
4831
|
+
});
|
|
4832
|
+
} catch {
|
|
4833
|
+
return null;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
// src/extract-value-normalization.ts
|
|
4838
|
+
var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
|
|
4839
|
+
function normalizeExtractedValue(raw, attribute) {
|
|
4840
|
+
if (raw == null) return null;
|
|
4841
|
+
const rawText = String(raw);
|
|
4842
|
+
if (!rawText.trim()) return null;
|
|
4843
|
+
const normalizedAttribute = String(attribute || "").trim().toLowerCase();
|
|
4844
|
+
if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
|
|
4845
|
+
const singleValue = pickSingleListAttributeValue(
|
|
4846
|
+
normalizedAttribute,
|
|
4847
|
+
rawText
|
|
4848
|
+
).trim();
|
|
4849
|
+
return singleValue || null;
|
|
4850
|
+
}
|
|
4851
|
+
const text = rawText.replace(/\s+/g, " ").trim();
|
|
4852
|
+
return text || null;
|
|
4853
|
+
}
|
|
4854
|
+
function pickSingleListAttributeValue(attribute, raw) {
|
|
4855
|
+
if (attribute === "ping") {
|
|
4856
|
+
const firstUrl = raw.trim().split(/\s+/)[0] || "";
|
|
4857
|
+
return firstUrl.trim();
|
|
4858
|
+
}
|
|
4859
|
+
if (attribute === "srcset" || attribute === "imagesrcset") {
|
|
4860
|
+
const picked = pickBestSrcsetCandidate(raw);
|
|
4861
|
+
if (picked) return picked;
|
|
4862
|
+
return pickFirstSrcsetToken(raw) || "";
|
|
4863
|
+
}
|
|
4864
|
+
return raw.trim();
|
|
4865
|
+
}
|
|
4866
|
+
function pickBestSrcsetCandidate(raw) {
|
|
4867
|
+
const candidates = parseSrcsetCandidates(raw);
|
|
4868
|
+
if (!candidates.length) return null;
|
|
4869
|
+
const widthCandidates = candidates.filter(
|
|
4870
|
+
(candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
|
|
4871
|
+
);
|
|
4872
|
+
if (widthCandidates.length) {
|
|
4873
|
+
return widthCandidates.reduce(
|
|
4874
|
+
(best, candidate) => candidate.width > best.width ? candidate : best
|
|
4875
|
+
).url;
|
|
4876
|
+
}
|
|
4877
|
+
const densityCandidates = candidates.filter(
|
|
4878
|
+
(candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
|
|
4879
|
+
);
|
|
4880
|
+
if (densityCandidates.length) {
|
|
4881
|
+
return densityCandidates.reduce(
|
|
4882
|
+
(best, candidate) => candidate.density > best.density ? candidate : best
|
|
4883
|
+
).url;
|
|
4884
|
+
}
|
|
4885
|
+
return candidates[0]?.url || null;
|
|
4886
|
+
}
|
|
4887
|
+
function parseSrcsetCandidates(raw) {
|
|
4888
|
+
const text = String(raw || "").trim();
|
|
4889
|
+
if (!text) return [];
|
|
4890
|
+
const out = [];
|
|
4891
|
+
let index = 0;
|
|
4892
|
+
while (index < text.length) {
|
|
4893
|
+
index = skipSeparators(text, index);
|
|
4894
|
+
if (index >= text.length) break;
|
|
4895
|
+
const urlToken = readUrlToken(text, index);
|
|
4896
|
+
index = urlToken.nextIndex;
|
|
4897
|
+
const url = urlToken.value.trim();
|
|
4898
|
+
if (!url) continue;
|
|
4899
|
+
index = skipWhitespace(text, index);
|
|
4900
|
+
const descriptors = [];
|
|
4901
|
+
while (index < text.length && text[index] !== ",") {
|
|
4902
|
+
const descriptorToken = readDescriptorToken(text, index);
|
|
4903
|
+
if (!descriptorToken.value) {
|
|
4904
|
+
index = descriptorToken.nextIndex;
|
|
4905
|
+
continue;
|
|
4906
|
+
}
|
|
4907
|
+
descriptors.push(descriptorToken.value);
|
|
4908
|
+
index = descriptorToken.nextIndex;
|
|
4909
|
+
index = skipWhitespace(text, index);
|
|
4910
|
+
}
|
|
4911
|
+
if (index < text.length && text[index] === ",") {
|
|
4912
|
+
index += 1;
|
|
4913
|
+
}
|
|
4914
|
+
let width = null;
|
|
4915
|
+
let density = null;
|
|
4916
|
+
for (const descriptor of descriptors) {
|
|
4917
|
+
const token = descriptor.trim().toLowerCase();
|
|
4918
|
+
if (!token) continue;
|
|
4919
|
+
const widthMatch = token.match(/^(\d+)w$/);
|
|
4920
|
+
if (widthMatch) {
|
|
4921
|
+
const parsed = Number.parseInt(widthMatch[1], 10);
|
|
4922
|
+
if (Number.isFinite(parsed)) {
|
|
4923
|
+
width = parsed;
|
|
4924
|
+
}
|
|
4925
|
+
continue;
|
|
4926
|
+
}
|
|
4927
|
+
const densityMatch = token.match(/^(\d*\.?\d+)x$/);
|
|
4928
|
+
if (densityMatch) {
|
|
4929
|
+
const parsed = Number.parseFloat(densityMatch[1]);
|
|
4930
|
+
if (Number.isFinite(parsed)) {
|
|
4931
|
+
density = parsed;
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
out.push({
|
|
4936
|
+
url,
|
|
4937
|
+
width,
|
|
4938
|
+
density
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
return out;
|
|
4942
|
+
}
|
|
4943
|
+
function pickFirstSrcsetToken(raw) {
|
|
4944
|
+
const candidate = parseSrcsetCandidates(raw)[0];
|
|
4945
|
+
if (candidate?.url) {
|
|
4946
|
+
return candidate.url;
|
|
4947
|
+
}
|
|
4948
|
+
const text = String(raw || "");
|
|
4949
|
+
const start = skipSeparators(text, 0);
|
|
4950
|
+
if (start >= text.length) return null;
|
|
4951
|
+
const firstToken = readUrlToken(text, start).value.trim();
|
|
4952
|
+
return firstToken || null;
|
|
4953
|
+
}
|
|
4954
|
+
function skipWhitespace(value, index) {
|
|
4955
|
+
let cursor = index;
|
|
4956
|
+
while (cursor < value.length && /\s/.test(value[cursor])) {
|
|
4957
|
+
cursor += 1;
|
|
4958
|
+
}
|
|
4959
|
+
return cursor;
|
|
4960
|
+
}
|
|
4961
|
+
function skipSeparators(value, index) {
|
|
4962
|
+
let cursor = skipWhitespace(value, index);
|
|
4963
|
+
while (cursor < value.length && value[cursor] === ",") {
|
|
4964
|
+
cursor += 1;
|
|
4965
|
+
cursor = skipWhitespace(value, cursor);
|
|
4966
|
+
}
|
|
4967
|
+
return cursor;
|
|
4968
|
+
}
|
|
4969
|
+
function readUrlToken(value, index) {
|
|
4970
|
+
let cursor = index;
|
|
4971
|
+
let out = "";
|
|
4972
|
+
const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
|
|
4973
|
+
while (cursor < value.length) {
|
|
4974
|
+
const char = value[cursor];
|
|
4975
|
+
if (/\s/.test(char)) {
|
|
4976
|
+
break;
|
|
4977
|
+
}
|
|
4978
|
+
if (char === "," && !isDataUrl) {
|
|
4979
|
+
break;
|
|
4980
|
+
}
|
|
4981
|
+
out += char;
|
|
4982
|
+
cursor += 1;
|
|
4784
4983
|
}
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
const processedHtml = rawHtml;
|
|
4788
|
-
const reducedHtml = applyCleaner(mode, processedHtml);
|
|
4789
|
-
let cleanedHtml = reducedHtml;
|
|
4790
|
-
let counterIndex = null;
|
|
4791
|
-
let counterBindings = null;
|
|
4792
|
-
if (withCounters) {
|
|
4793
|
-
const counted = await assignCounters(
|
|
4794
|
-
page,
|
|
4795
|
-
reducedHtml,
|
|
4796
|
-
serialized.nodePaths,
|
|
4797
|
-
serialized.nodeMeta,
|
|
4798
|
-
snapshotSessionId
|
|
4799
|
-
);
|
|
4800
|
-
cleanedHtml = counted.html;
|
|
4801
|
-
counterIndex = counted.counterIndex;
|
|
4802
|
-
counterBindings = counted.counterBindings;
|
|
4803
|
-
} else {
|
|
4804
|
-
cleanedHtml = stripNodeIds(cleanedHtml);
|
|
4984
|
+
if (isDataUrl && out.endsWith(",") && cursor < value.length) {
|
|
4985
|
+
out = out.slice(0, -1);
|
|
4805
4986
|
}
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4987
|
+
return {
|
|
4988
|
+
value: out,
|
|
4989
|
+
nextIndex: cursor
|
|
4990
|
+
};
|
|
4991
|
+
}
|
|
4992
|
+
function readDescriptorToken(value, index) {
|
|
4993
|
+
let cursor = skipWhitespace(value, index);
|
|
4994
|
+
let out = "";
|
|
4995
|
+
while (cursor < value.length) {
|
|
4996
|
+
const char = value[cursor];
|
|
4997
|
+
if (char === "," || /\s/.test(char)) {
|
|
4998
|
+
break;
|
|
4999
|
+
}
|
|
5000
|
+
out += char;
|
|
5001
|
+
cursor += 1;
|
|
4809
5002
|
}
|
|
4810
5003
|
return {
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
url: page.url(),
|
|
4814
|
-
rawHtml,
|
|
4815
|
-
processedHtml,
|
|
4816
|
-
reducedHtml,
|
|
4817
|
-
cleanedHtml,
|
|
4818
|
-
counterIndex,
|
|
4819
|
-
counterBindings
|
|
5004
|
+
value: out.trim(),
|
|
5005
|
+
nextIndex: cursor
|
|
4820
5006
|
};
|
|
4821
5007
|
}
|
|
4822
5008
|
|
|
4823
|
-
// src/
|
|
4824
|
-
var
|
|
5009
|
+
// src/html/counter-runtime.ts
|
|
5010
|
+
var CounterResolutionError = class extends Error {
|
|
4825
5011
|
code;
|
|
4826
5012
|
constructor(code, message) {
|
|
4827
5013
|
super(message);
|
|
4828
|
-
this.name = "
|
|
5014
|
+
this.name = "CounterResolutionError";
|
|
4829
5015
|
this.code = code;
|
|
4830
5016
|
}
|
|
4831
5017
|
};
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
let frame = page.mainFrame();
|
|
4837
|
-
let rootHandle = null;
|
|
4838
|
-
for (const hop of path5.context) {
|
|
4839
|
-
const host = await resolveDomPath(frame, hop.host, rootHandle);
|
|
4840
|
-
if (!host) {
|
|
4841
|
-
await disposeHandle(rootHandle);
|
|
4842
|
-
throw new ElementPathError(
|
|
4843
|
-
"ERR_PATH_CONTEXT_HOST_NOT_FOUND",
|
|
4844
|
-
"Unable to resolve context host from stored match selectors."
|
|
4845
|
-
);
|
|
4846
|
-
}
|
|
4847
|
-
if (hop.kind === "iframe") {
|
|
4848
|
-
const nextFrame = await host.element.contentFrame();
|
|
4849
|
-
await host.element.dispose();
|
|
4850
|
-
await disposeHandle(rootHandle);
|
|
4851
|
-
rootHandle = null;
|
|
4852
|
-
if (!nextFrame) {
|
|
4853
|
-
throw new ElementPathError(
|
|
4854
|
-
"ERR_PATH_IFRAME_UNAVAILABLE",
|
|
4855
|
-
"Iframe is unavailable or inaccessible for this path."
|
|
4856
|
-
);
|
|
4857
|
-
}
|
|
4858
|
-
frame = nextFrame;
|
|
4859
|
-
continue;
|
|
4860
|
-
}
|
|
4861
|
-
const shadowRoot = await host.element.evaluateHandle(
|
|
4862
|
-
(element) => element.shadowRoot
|
|
4863
|
-
);
|
|
4864
|
-
await host.element.dispose();
|
|
4865
|
-
const isMissing = await shadowRoot.evaluate((value) => value == null);
|
|
4866
|
-
if (isMissing) {
|
|
4867
|
-
await shadowRoot.dispose();
|
|
4868
|
-
await disposeHandle(rootHandle);
|
|
4869
|
-
throw new ElementPathError(
|
|
4870
|
-
"ERR_PATH_SHADOW_ROOT_UNAVAILABLE",
|
|
4871
|
-
"Shadow root is unavailable for this path."
|
|
4872
|
-
);
|
|
4873
|
-
}
|
|
4874
|
-
await disposeHandle(rootHandle);
|
|
4875
|
-
rootHandle = shadowRoot;
|
|
5018
|
+
async function resolveCounterElement(page, counter) {
|
|
5019
|
+
const normalized = normalizeCounter(counter);
|
|
5020
|
+
if (normalized == null) {
|
|
5021
|
+
throw buildCounterNotFoundError(counter);
|
|
4876
5022
|
}
|
|
4877
|
-
const
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
path5.nodes,
|
|
4882
|
-
rootHandle
|
|
4883
|
-
);
|
|
4884
|
-
await disposeHandle(rootHandle);
|
|
4885
|
-
throw new ElementPathError(
|
|
4886
|
-
"ERR_PATH_TARGET_NOT_FOUND",
|
|
4887
|
-
buildTargetNotFoundMessage(path5.nodes, diagnostics)
|
|
4888
|
-
);
|
|
4889
|
-
}
|
|
4890
|
-
await disposeHandle(rootHandle);
|
|
4891
|
-
if (isPathDebugEnabled()) {
|
|
4892
|
-
debugPath("resolved", {
|
|
4893
|
-
selector: target.selector,
|
|
4894
|
-
mode: target.mode,
|
|
4895
|
-
count: target.count,
|
|
4896
|
-
targetDepth: path5.nodes.length
|
|
4897
|
-
});
|
|
5023
|
+
const scan = await scanCounterOccurrences(page, [normalized]);
|
|
5024
|
+
const entry = scan.get(normalized);
|
|
5025
|
+
if (!entry || entry.count <= 0 || !entry.frame) {
|
|
5026
|
+
throw buildCounterNotFoundError(counter);
|
|
4898
5027
|
}
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
usedSelector: target.selector || buildPathSelectorHint(path5)
|
|
4902
|
-
};
|
|
4903
|
-
}
|
|
4904
|
-
async function resolveDomPath(frame, domPath, rootHandle) {
|
|
4905
|
-
const candidates = buildPathCandidates(domPath);
|
|
4906
|
-
if (!candidates.length) return null;
|
|
4907
|
-
if (isPathDebugEnabled()) {
|
|
4908
|
-
debugPath("trying selectors", { candidates });
|
|
5028
|
+
if (entry.count > 1) {
|
|
5029
|
+
throw buildCounterAmbiguousError(counter);
|
|
4909
5030
|
}
|
|
4910
|
-
const
|
|
4911
|
-
if (!selected || !selected.selector) return null;
|
|
4912
|
-
const handle = rootHandle ? await rootHandle.evaluateHandle((root, selector) => {
|
|
4913
|
-
if (!(root instanceof ShadowRoot)) return null;
|
|
4914
|
-
return root.querySelector(selector);
|
|
4915
|
-
}, selected.selector) : await frame.evaluateHandle(
|
|
4916
|
-
(selector) => document.querySelector(selector),
|
|
4917
|
-
selected.selector
|
|
4918
|
-
);
|
|
5031
|
+
const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
|
|
4919
5032
|
const element = handle.asElement();
|
|
4920
5033
|
if (!element) {
|
|
4921
5034
|
await handle.dispose();
|
|
4922
|
-
|
|
5035
|
+
throw buildCounterNotFoundError(counter);
|
|
4923
5036
|
}
|
|
4924
|
-
return
|
|
4925
|
-
element,
|
|
4926
|
-
selector: selected.selector,
|
|
4927
|
-
mode: selected.mode,
|
|
4928
|
-
count: selected.count
|
|
4929
|
-
};
|
|
4930
|
-
}
|
|
4931
|
-
async function collectCandidateDiagnostics(frame, domPath, rootHandle) {
|
|
4932
|
-
const candidates = buildPathCandidates(domPath);
|
|
4933
|
-
if (!candidates.length) return [];
|
|
4934
|
-
const diagnostics = rootHandle ? await rootHandle.evaluate(countInRoot, candidates) : await frame.evaluate(countInDocument, candidates);
|
|
4935
|
-
return Array.isArray(diagnostics) ? diagnostics.map((item) => ({
|
|
4936
|
-
selector: String(item?.selector || ""),
|
|
4937
|
-
count: Number(item?.count || 0)
|
|
4938
|
-
})).filter((item) => item.selector) : [];
|
|
4939
|
-
}
|
|
4940
|
-
function buildTargetNotFoundMessage(domPath, diagnostics) {
|
|
4941
|
-
const depth = Array.isArray(domPath) ? domPath.length : 0;
|
|
4942
|
-
const sample = diagnostics.slice(0, 4).map((item) => `"${item.selector}" => ${item.count}`).join(", ");
|
|
4943
|
-
const base = "Element path resolution failed (ERR_PATH_TARGET_NOT_FOUND): no selector candidate matched the current DOM.";
|
|
4944
|
-
if (!sample)
|
|
4945
|
-
return `${base} Tried ${Math.max(diagnostics.length, 0)} candidates.`;
|
|
4946
|
-
return `${base} Target depth ${depth}. Candidate counts: ${sample}.`;
|
|
5037
|
+
return element;
|
|
4947
5038
|
}
|
|
4948
|
-
function
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
5039
|
+
async function resolveCountersBatch(page, requests) {
|
|
5040
|
+
const out = {};
|
|
5041
|
+
if (!requests.length) return out;
|
|
5042
|
+
const counters = dedupeCounters(requests);
|
|
5043
|
+
const scan = await scanCounterOccurrences(page, counters);
|
|
5044
|
+
for (const counter of counters) {
|
|
5045
|
+
const entry = scan.get(counter);
|
|
5046
|
+
if (entry.count > 1) {
|
|
5047
|
+
throw buildCounterAmbiguousError(counter);
|
|
4957
5048
|
}
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
5049
|
+
}
|
|
5050
|
+
const valueCache = /* @__PURE__ */ new Map();
|
|
5051
|
+
for (const request of requests) {
|
|
5052
|
+
const normalized = normalizeCounter(request.counter);
|
|
5053
|
+
if (normalized == null) {
|
|
5054
|
+
out[request.key] = null;
|
|
5055
|
+
continue;
|
|
4964
5056
|
}
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
mode: "fallback"
|
|
4970
|
-
};
|
|
5057
|
+
const entry = scan.get(normalized);
|
|
5058
|
+
if (!entry || entry.count <= 0 || !entry.frame) {
|
|
5059
|
+
out[request.key] = null;
|
|
5060
|
+
continue;
|
|
4971
5061
|
}
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
if (!(root instanceof ShadowRoot)) return null;
|
|
4977
|
-
let fallback = null;
|
|
4978
|
-
for (const selector of selectors) {
|
|
4979
|
-
if (!selector) continue;
|
|
4980
|
-
let count = 0;
|
|
4981
|
-
try {
|
|
4982
|
-
count = root.querySelectorAll(selector).length;
|
|
4983
|
-
} catch {
|
|
4984
|
-
count = 0;
|
|
5062
|
+
const cacheKey = `${normalized}:${request.attribute || ""}`;
|
|
5063
|
+
if (valueCache.has(cacheKey)) {
|
|
5064
|
+
out[request.key] = valueCache.get(cacheKey);
|
|
5065
|
+
continue;
|
|
4985
5066
|
}
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
5067
|
+
const read = await readCounterValueInFrame(
|
|
5068
|
+
entry.frame,
|
|
5069
|
+
normalized,
|
|
5070
|
+
request.attribute
|
|
5071
|
+
);
|
|
5072
|
+
if (read.status === "ambiguous") {
|
|
5073
|
+
throw buildCounterAmbiguousError(normalized);
|
|
4992
5074
|
}
|
|
4993
|
-
if (
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
mode: "fallback"
|
|
4998
|
-
};
|
|
5075
|
+
if (read.status === "missing") {
|
|
5076
|
+
valueCache.set(cacheKey, null);
|
|
5077
|
+
out[request.key] = null;
|
|
5078
|
+
continue;
|
|
4999
5079
|
}
|
|
5080
|
+
const normalizedValue = normalizeExtractedValue(
|
|
5081
|
+
read.value ?? null,
|
|
5082
|
+
request.attribute
|
|
5083
|
+
);
|
|
5084
|
+
valueCache.set(cacheKey, normalizedValue);
|
|
5085
|
+
out[request.key] = normalizedValue;
|
|
5000
5086
|
}
|
|
5001
|
-
return
|
|
5087
|
+
return out;
|
|
5002
5088
|
}
|
|
5003
|
-
function
|
|
5089
|
+
function dedupeCounters(requests) {
|
|
5090
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5004
5091
|
const out = [];
|
|
5005
|
-
for (const
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
} catch {
|
|
5011
|
-
count = 0;
|
|
5012
|
-
}
|
|
5013
|
-
out.push({ selector, count });
|
|
5092
|
+
for (const request of requests) {
|
|
5093
|
+
const normalized = normalizeCounter(request.counter);
|
|
5094
|
+
if (normalized == null || seen.has(normalized)) continue;
|
|
5095
|
+
seen.add(normalized);
|
|
5096
|
+
out.push(normalized);
|
|
5014
5097
|
}
|
|
5015
5098
|
return out;
|
|
5016
5099
|
}
|
|
5017
|
-
function
|
|
5018
|
-
if (!(
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5100
|
+
function normalizeCounter(counter) {
|
|
5101
|
+
if (!Number.isFinite(counter)) return null;
|
|
5102
|
+
if (!Number.isInteger(counter)) return null;
|
|
5103
|
+
if (counter <= 0) return null;
|
|
5104
|
+
return counter;
|
|
5105
|
+
}
|
|
5106
|
+
async function scanCounterOccurrences(page, counters) {
|
|
5107
|
+
const out = /* @__PURE__ */ new Map();
|
|
5108
|
+
for (const counter of counters) {
|
|
5109
|
+
out.set(counter, {
|
|
5110
|
+
count: 0,
|
|
5111
|
+
frame: null
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
5114
|
+
if (!counters.length) return out;
|
|
5115
|
+
for (const frame of page.frames()) {
|
|
5116
|
+
let frameCounts;
|
|
5023
5117
|
try {
|
|
5024
|
-
|
|
5118
|
+
frameCounts = await frame.evaluate((candidates) => {
|
|
5119
|
+
const keys = new Set(candidates.map((value) => String(value)));
|
|
5120
|
+
const counts = {};
|
|
5121
|
+
for (const key of keys) {
|
|
5122
|
+
counts[key] = 0;
|
|
5123
|
+
}
|
|
5124
|
+
const walk = (root) => {
|
|
5125
|
+
const children = Array.from(root.children);
|
|
5126
|
+
for (const child of children) {
|
|
5127
|
+
const value = child.getAttribute("c");
|
|
5128
|
+
if (value && keys.has(value)) {
|
|
5129
|
+
counts[value] = (counts[value] || 0) + 1;
|
|
5130
|
+
}
|
|
5131
|
+
walk(child);
|
|
5132
|
+
if (child.shadowRoot) {
|
|
5133
|
+
walk(child.shadowRoot);
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
};
|
|
5137
|
+
walk(document);
|
|
5138
|
+
return counts;
|
|
5139
|
+
}, counters);
|
|
5025
5140
|
} catch {
|
|
5026
|
-
|
|
5141
|
+
continue;
|
|
5142
|
+
}
|
|
5143
|
+
for (const [rawCounter, rawCount] of Object.entries(frameCounts)) {
|
|
5144
|
+
const counter = Number.parseInt(rawCounter, 10);
|
|
5145
|
+
if (!Number.isFinite(counter)) continue;
|
|
5146
|
+
const count = Number(rawCount || 0);
|
|
5147
|
+
if (!Number.isFinite(count) || count <= 0) continue;
|
|
5148
|
+
const entry = out.get(counter);
|
|
5149
|
+
entry.count += count;
|
|
5150
|
+
if (!entry.frame) {
|
|
5151
|
+
entry.frame = frame;
|
|
5152
|
+
}
|
|
5027
5153
|
}
|
|
5028
|
-
out.push({ selector, count });
|
|
5029
5154
|
}
|
|
5030
5155
|
return out;
|
|
5031
5156
|
}
|
|
5032
|
-
function
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
}
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5157
|
+
async function resolveUniqueHandleInFrame(frame, counter) {
|
|
5158
|
+
return frame.evaluateHandle((targetCounter) => {
|
|
5159
|
+
const matches = [];
|
|
5160
|
+
const walk = (root) => {
|
|
5161
|
+
const children = Array.from(root.children);
|
|
5162
|
+
for (const child of children) {
|
|
5163
|
+
if (child.getAttribute("c") === targetCounter) {
|
|
5164
|
+
matches.push(child);
|
|
5165
|
+
}
|
|
5166
|
+
walk(child);
|
|
5167
|
+
if (child.shadowRoot) {
|
|
5168
|
+
walk(child.shadowRoot);
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
};
|
|
5172
|
+
walk(document);
|
|
5173
|
+
if (matches.length !== 1) {
|
|
5174
|
+
return null;
|
|
5175
|
+
}
|
|
5176
|
+
return matches[0];
|
|
5177
|
+
}, String(counter));
|
|
5052
5178
|
}
|
|
5053
|
-
|
|
5054
|
-
// src/actions/actionability-probe.ts
|
|
5055
|
-
async function probeActionabilityState(element) {
|
|
5179
|
+
async function readCounterValueInFrame(frame, counter, attribute) {
|
|
5056
5180
|
try {
|
|
5057
|
-
return await
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
visible: null,
|
|
5072
|
-
enabled: null,
|
|
5073
|
-
editable: null,
|
|
5074
|
-
blocker: null
|
|
5181
|
+
return await frame.evaluate(
|
|
5182
|
+
({ targetCounter, attribute: attribute2 }) => {
|
|
5183
|
+
const matches = [];
|
|
5184
|
+
const walk = (root) => {
|
|
5185
|
+
const children = Array.from(root.children);
|
|
5186
|
+
for (const child of children) {
|
|
5187
|
+
if (child.getAttribute("c") === targetCounter) {
|
|
5188
|
+
matches.push(child);
|
|
5189
|
+
}
|
|
5190
|
+
walk(child);
|
|
5191
|
+
if (child.shadowRoot) {
|
|
5192
|
+
walk(child.shadowRoot);
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5075
5195
|
};
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
const opacity = Number.parseFloat(style.opacity || "1");
|
|
5081
|
-
const isVisible = hasBox && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && (!Number.isFinite(opacity) || opacity > 0);
|
|
5082
|
-
let enabled = null;
|
|
5083
|
-
if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLOptionElement || target instanceof HTMLOptGroupElement || target instanceof HTMLFieldSetElement) {
|
|
5084
|
-
enabled = !target.disabled;
|
|
5085
|
-
}
|
|
5086
|
-
let editable = null;
|
|
5087
|
-
if (target instanceof HTMLInputElement) {
|
|
5088
|
-
editable = !target.readOnly && !target.disabled;
|
|
5089
|
-
} else if (target instanceof HTMLTextAreaElement) {
|
|
5090
|
-
editable = !target.readOnly && !target.disabled;
|
|
5091
|
-
} else if (target instanceof HTMLSelectElement) {
|
|
5092
|
-
editable = !target.disabled;
|
|
5093
|
-
} else if (target instanceof HTMLElement && target.isContentEditable) {
|
|
5094
|
-
editable = true;
|
|
5095
|
-
}
|
|
5096
|
-
let blocker = null;
|
|
5097
|
-
if (hasBox && window.innerWidth > 0 && window.innerHeight > 0) {
|
|
5098
|
-
const x = Math.min(
|
|
5099
|
-
Math.max(rect.left + rect.width / 2, 0),
|
|
5100
|
-
window.innerWidth - 1
|
|
5101
|
-
);
|
|
5102
|
-
const y = Math.min(
|
|
5103
|
-
Math.max(rect.top + rect.height / 2, 0),
|
|
5104
|
-
window.innerHeight - 1
|
|
5105
|
-
);
|
|
5106
|
-
const top = document.elementFromPoint(x, y);
|
|
5107
|
-
if (top && top !== target && !target.contains(top)) {
|
|
5108
|
-
const classes = String(top.className || "").split(/\s+/).map((value) => value.trim()).filter(Boolean).slice(0, 5);
|
|
5109
|
-
blocker = {
|
|
5110
|
-
tag: top.tagName.toLowerCase(),
|
|
5111
|
-
id: top.id || null,
|
|
5112
|
-
classes,
|
|
5113
|
-
role: top.getAttribute("role"),
|
|
5114
|
-
text: (top.textContent || "").trim().slice(0, 80) || null
|
|
5196
|
+
walk(document);
|
|
5197
|
+
if (!matches.length) {
|
|
5198
|
+
return {
|
|
5199
|
+
status: "missing"
|
|
5115
5200
|
};
|
|
5116
5201
|
}
|
|
5202
|
+
if (matches.length > 1) {
|
|
5203
|
+
return {
|
|
5204
|
+
status: "ambiguous"
|
|
5205
|
+
};
|
|
5206
|
+
}
|
|
5207
|
+
const target = matches[0];
|
|
5208
|
+
const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
|
|
5209
|
+
return {
|
|
5210
|
+
status: "ok",
|
|
5211
|
+
value
|
|
5212
|
+
};
|
|
5213
|
+
},
|
|
5214
|
+
{
|
|
5215
|
+
targetCounter: String(counter),
|
|
5216
|
+
attribute
|
|
5117
5217
|
}
|
|
5118
|
-
|
|
5119
|
-
connected,
|
|
5120
|
-
visible: isVisible,
|
|
5121
|
-
enabled,
|
|
5122
|
-
editable,
|
|
5123
|
-
blocker
|
|
5124
|
-
};
|
|
5125
|
-
});
|
|
5218
|
+
);
|
|
5126
5219
|
} catch {
|
|
5127
|
-
return
|
|
5220
|
+
return {
|
|
5221
|
+
status: "missing"
|
|
5222
|
+
};
|
|
5128
5223
|
}
|
|
5129
5224
|
}
|
|
5225
|
+
function buildCounterNotFoundError(counter) {
|
|
5226
|
+
return new CounterResolutionError(
|
|
5227
|
+
"ERR_COUNTER_NOT_FOUND",
|
|
5228
|
+
`Counter ${counter} was not found in the live DOM.`
|
|
5229
|
+
);
|
|
5230
|
+
}
|
|
5231
|
+
function buildCounterAmbiguousError(counter) {
|
|
5232
|
+
return new CounterResolutionError(
|
|
5233
|
+
"ERR_COUNTER_AMBIGUOUS",
|
|
5234
|
+
`Counter ${counter} matches multiple live elements.`
|
|
5235
|
+
);
|
|
5236
|
+
}
|
|
5130
5237
|
|
|
5131
5238
|
// src/actions/failure-classifier.ts
|
|
5132
5239
|
var ACTION_FAILURE_CODES = [
|
|
@@ -5177,7 +5284,7 @@ function defaultActionFailureMessage(action) {
|
|
|
5177
5284
|
function classifyActionFailure(input) {
|
|
5178
5285
|
const typed = classifyTypedError(input.error);
|
|
5179
5286
|
if (typed) return typed;
|
|
5180
|
-
const message =
|
|
5287
|
+
const message = extractErrorMessage2(input.error, input.fallbackMessage);
|
|
5181
5288
|
const fromCallLog = classifyFromPlaywrightMessage(message, input.probe);
|
|
5182
5289
|
if (fromCallLog) return fromCallLog;
|
|
5183
5290
|
const fromProbe = classifyFromProbe(input.probe);
|
|
@@ -5186,7 +5293,7 @@ function classifyActionFailure(input) {
|
|
|
5186
5293
|
if (fromHeuristic) return fromHeuristic;
|
|
5187
5294
|
return buildFailure({
|
|
5188
5295
|
code: "UNKNOWN",
|
|
5189
|
-
message: ensureMessage(input.fallbackMessage
|
|
5296
|
+
message: ensureMessage(message, input.fallbackMessage),
|
|
5190
5297
|
classificationSource: "unknown"
|
|
5191
5298
|
});
|
|
5192
5299
|
}
|
|
@@ -5242,13 +5349,6 @@ function classifyTypedError(error) {
|
|
|
5242
5349
|
classificationSource: "typed_error"
|
|
5243
5350
|
});
|
|
5244
5351
|
}
|
|
5245
|
-
if (error.code === "ERR_COUNTER_FRAME_UNAVAILABLE") {
|
|
5246
|
-
return buildFailure({
|
|
5247
|
-
code: "TARGET_UNAVAILABLE",
|
|
5248
|
-
message: error.message,
|
|
5249
|
-
classificationSource: "typed_error"
|
|
5250
|
-
});
|
|
5251
|
-
}
|
|
5252
5352
|
if (error.code === "ERR_COUNTER_AMBIGUOUS") {
|
|
5253
5353
|
return buildFailure({
|
|
5254
5354
|
code: "TARGET_AMBIGUOUS",
|
|
@@ -5256,13 +5356,6 @@ function classifyTypedError(error) {
|
|
|
5256
5356
|
classificationSource: "typed_error"
|
|
5257
5357
|
});
|
|
5258
5358
|
}
|
|
5259
|
-
if (error.code === "ERR_COUNTER_STALE_OR_NOT_FOUND") {
|
|
5260
|
-
return buildFailure({
|
|
5261
|
-
code: "TARGET_STALE",
|
|
5262
|
-
message: error.message,
|
|
5263
|
-
classificationSource: "typed_error"
|
|
5264
|
-
});
|
|
5265
|
-
}
|
|
5266
5359
|
}
|
|
5267
5360
|
return null;
|
|
5268
5361
|
}
|
|
@@ -5446,13 +5539,22 @@ function defaultRetryableForCode(code) {
|
|
|
5446
5539
|
return true;
|
|
5447
5540
|
}
|
|
5448
5541
|
}
|
|
5449
|
-
function
|
|
5542
|
+
function extractErrorMessage2(error, fallbackMessage) {
|
|
5450
5543
|
if (error instanceof Error && error.message.trim()) {
|
|
5451
5544
|
return error.message;
|
|
5452
5545
|
}
|
|
5453
5546
|
if (typeof error === "string" && error.trim()) {
|
|
5454
5547
|
return error.trim();
|
|
5455
5548
|
}
|
|
5549
|
+
if (error && typeof error === "object" && !Array.isArray(error)) {
|
|
5550
|
+
const record = error;
|
|
5551
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
5552
|
+
return record.message.trim();
|
|
5553
|
+
}
|
|
5554
|
+
if (typeof record.error === "string" && record.error.trim()) {
|
|
5555
|
+
return record.error.trim();
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5456
5558
|
return ensureMessage(fallbackMessage, "Action failed.");
|
|
5457
5559
|
}
|
|
5458
5560
|
function ensureMessage(value, fallback) {
|
|
@@ -7772,7 +7874,8 @@ function withTokenQuery(wsUrl, token) {
|
|
|
7772
7874
|
// src/cloud/local-cache-sync.ts
|
|
7773
7875
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
7774
7876
|
var import_path5 = __toESM(require("path"), 1);
|
|
7775
|
-
function collectLocalSelectorCacheEntries(storage) {
|
|
7877
|
+
function collectLocalSelectorCacheEntries(storage, options = {}) {
|
|
7878
|
+
const debug = options.debug === true;
|
|
7776
7879
|
const namespace = storage.getNamespace();
|
|
7777
7880
|
const namespaceDir = storage.getNamespaceDir();
|
|
7778
7881
|
if (!import_fs3.default.existsSync(namespaceDir)) return [];
|
|
@@ -7781,7 +7884,7 @@ function collectLocalSelectorCacheEntries(storage) {
|
|
|
7781
7884
|
for (const fileName of fileNames) {
|
|
7782
7885
|
if (fileName === "index.json" || !fileName.endsWith(".json")) continue;
|
|
7783
7886
|
const filePath = import_path5.default.join(namespaceDir, fileName);
|
|
7784
|
-
const selector = readSelectorFile(filePath);
|
|
7887
|
+
const selector = readSelectorFile(filePath, debug);
|
|
7785
7888
|
if (!selector) continue;
|
|
7786
7889
|
const descriptionHash = normalizeDescriptionHash(selector.id);
|
|
7787
7890
|
const method = normalizeMethod(selector.method);
|
|
@@ -7806,11 +7909,20 @@ function collectLocalSelectorCacheEntries(storage) {
|
|
|
7806
7909
|
}
|
|
7807
7910
|
return dedupeNewest(entries);
|
|
7808
7911
|
}
|
|
7809
|
-
function readSelectorFile(filePath) {
|
|
7912
|
+
function readSelectorFile(filePath, debug) {
|
|
7810
7913
|
try {
|
|
7811
7914
|
const raw = import_fs3.default.readFileSync(filePath, "utf8");
|
|
7812
7915
|
return JSON.parse(raw);
|
|
7813
|
-
} catch {
|
|
7916
|
+
} catch (error) {
|
|
7917
|
+
const message = extractErrorMessage(
|
|
7918
|
+
error,
|
|
7919
|
+
"Unable to parse selector cache file JSON."
|
|
7920
|
+
);
|
|
7921
|
+
if (debug) {
|
|
7922
|
+
console.warn(
|
|
7923
|
+
`[opensteer] failed to read local selector cache file "${filePath}": ${message}`
|
|
7924
|
+
);
|
|
7925
|
+
}
|
|
7814
7926
|
return null;
|
|
7815
7927
|
}
|
|
7816
7928
|
}
|
|
@@ -10018,7 +10130,9 @@ var Opensteer = class _Opensteer {
|
|
|
10018
10130
|
this.aiExtract = this.createLazyExtractCallback(model);
|
|
10019
10131
|
const rootDir = resolved.storage?.rootDir || process.cwd();
|
|
10020
10132
|
this.namespace = resolveNamespace(resolved, rootDir);
|
|
10021
|
-
this.storage = new LocalSelectorStorage(rootDir, this.namespace
|
|
10133
|
+
this.storage = new LocalSelectorStorage(rootDir, this.namespace, {
|
|
10134
|
+
debug: Boolean(resolved.debug)
|
|
10135
|
+
});
|
|
10022
10136
|
this.pool = new BrowserPool(resolved.browser || {});
|
|
10023
10137
|
if (cloudSelection.cloud) {
|
|
10024
10138
|
const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
|
|
@@ -10037,6 +10151,14 @@ var Opensteer = class _Opensteer {
|
|
|
10037
10151
|
this.cloud = null;
|
|
10038
10152
|
}
|
|
10039
10153
|
}
|
|
10154
|
+
logDebugError(context, error) {
|
|
10155
|
+
if (!this.config.debug) return;
|
|
10156
|
+
const normalized = normalizeError(error, "Unknown error.");
|
|
10157
|
+
const codeSuffix = normalized.code && normalized.code.trim() ? ` [${normalized.code.trim()}]` : "";
|
|
10158
|
+
console.warn(
|
|
10159
|
+
`[opensteer] ${context}: ${normalized.message}${codeSuffix}`
|
|
10160
|
+
);
|
|
10161
|
+
}
|
|
10040
10162
|
createLazyResolveCallback(model) {
|
|
10041
10163
|
let resolverPromise = null;
|
|
10042
10164
|
return async (...args) => {
|
|
@@ -10127,7 +10249,8 @@ var Opensteer = class _Opensteer {
|
|
|
10127
10249
|
let tabs;
|
|
10128
10250
|
try {
|
|
10129
10251
|
tabs = await this.invokeCloudAction("tabs", {});
|
|
10130
|
-
} catch {
|
|
10252
|
+
} catch (error) {
|
|
10253
|
+
this.logDebugError("cloud page reference sync (tabs lookup) failed", error);
|
|
10131
10254
|
return;
|
|
10132
10255
|
}
|
|
10133
10256
|
if (!tabs.length) {
|
|
@@ -10265,12 +10388,7 @@ var Opensteer = class _Opensteer {
|
|
|
10265
10388
|
try {
|
|
10266
10389
|
await this.syncLocalSelectorCacheToCloud();
|
|
10267
10390
|
} catch (error) {
|
|
10268
|
-
|
|
10269
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
10270
|
-
console.warn(
|
|
10271
|
-
`[opensteer] cloud selector cache sync failed: ${message}`
|
|
10272
|
-
);
|
|
10273
|
-
}
|
|
10391
|
+
this.logDebugError("cloud selector cache sync failed", error);
|
|
10274
10392
|
}
|
|
10275
10393
|
localRunId = this.cloud.localRunId || buildLocalRunId(this.namespace);
|
|
10276
10394
|
this.cloud.localRunId = localRunId;
|
|
@@ -10302,7 +10420,12 @@ var Opensteer = class _Opensteer {
|
|
|
10302
10420
|
this.cloud.actionClient = actionClient;
|
|
10303
10421
|
this.cloud.sessionId = sessionId;
|
|
10304
10422
|
this.cloud.cloudSessionUrl = session2.cloudSessionUrl;
|
|
10305
|
-
await this.syncCloudPageRef().catch(() =>
|
|
10423
|
+
await this.syncCloudPageRef().catch((error) => {
|
|
10424
|
+
this.logDebugError(
|
|
10425
|
+
"cloud page reference sync after launch failed",
|
|
10426
|
+
error
|
|
10427
|
+
);
|
|
10428
|
+
});
|
|
10306
10429
|
this.announceCloudSession({
|
|
10307
10430
|
sessionId: session2.sessionId,
|
|
10308
10431
|
workspaceId: session2.cloudSession.workspaceId,
|
|
@@ -10389,7 +10512,9 @@ var Opensteer = class _Opensteer {
|
|
|
10389
10512
|
}
|
|
10390
10513
|
async syncLocalSelectorCacheToCloud() {
|
|
10391
10514
|
if (!this.cloud) return;
|
|
10392
|
-
const entries = collectLocalSelectorCacheEntries(this.storage
|
|
10515
|
+
const entries = collectLocalSelectorCacheEntries(this.storage, {
|
|
10516
|
+
debug: Boolean(this.config.debug)
|
|
10517
|
+
});
|
|
10393
10518
|
if (!entries.length) return;
|
|
10394
10519
|
await this.cloud.sessionClient.importSelectorCache({
|
|
10395
10520
|
entries
|
|
@@ -10398,9 +10523,12 @@ var Opensteer = class _Opensteer {
|
|
|
10398
10523
|
async goto(url, options) {
|
|
10399
10524
|
if (this.cloud) {
|
|
10400
10525
|
await this.invokeCloudActionAndResetCache("goto", { url, options });
|
|
10401
|
-
await this.syncCloudPageRef({ expectedUrl: url }).catch(
|
|
10402
|
-
(
|
|
10403
|
-
|
|
10526
|
+
await this.syncCloudPageRef({ expectedUrl: url }).catch((error) => {
|
|
10527
|
+
this.logDebugError(
|
|
10528
|
+
"cloud page reference sync after goto failed",
|
|
10529
|
+
error
|
|
10530
|
+
);
|
|
10531
|
+
});
|
|
10404
10532
|
return;
|
|
10405
10533
|
}
|
|
10406
10534
|
const { waitUntil = "domcontentloaded", ...rest } = options ?? {};
|
|
@@ -10501,7 +10629,7 @@ var Opensteer = class _Opensteer {
|
|
|
10501
10629
|
let persistPath = null;
|
|
10502
10630
|
try {
|
|
10503
10631
|
if (storageKey && resolution.shouldPersist) {
|
|
10504
|
-
persistPath = await this.
|
|
10632
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10505
10633
|
handle,
|
|
10506
10634
|
"hover",
|
|
10507
10635
|
resolution.counter
|
|
@@ -10600,7 +10728,7 @@ var Opensteer = class _Opensteer {
|
|
|
10600
10728
|
let persistPath = null;
|
|
10601
10729
|
try {
|
|
10602
10730
|
if (storageKey && resolution.shouldPersist) {
|
|
10603
|
-
persistPath = await this.
|
|
10731
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10604
10732
|
handle,
|
|
10605
10733
|
"input",
|
|
10606
10734
|
resolution.counter
|
|
@@ -10703,7 +10831,7 @@ var Opensteer = class _Opensteer {
|
|
|
10703
10831
|
let persistPath = null;
|
|
10704
10832
|
try {
|
|
10705
10833
|
if (storageKey && resolution.shouldPersist) {
|
|
10706
|
-
persistPath = await this.
|
|
10834
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10707
10835
|
handle,
|
|
10708
10836
|
"select",
|
|
10709
10837
|
resolution.counter
|
|
@@ -10813,7 +10941,7 @@ var Opensteer = class _Opensteer {
|
|
|
10813
10941
|
let persistPath = null;
|
|
10814
10942
|
try {
|
|
10815
10943
|
if (storageKey && resolution.shouldPersist) {
|
|
10816
|
-
persistPath = await this.
|
|
10944
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
10817
10945
|
handle,
|
|
10818
10946
|
"scroll",
|
|
10819
10947
|
resolution.counter
|
|
@@ -10912,7 +11040,12 @@ var Opensteer = class _Opensteer {
|
|
|
10912
11040
|
}
|
|
10913
11041
|
);
|
|
10914
11042
|
await this.syncCloudPageRef({ expectedUrl: result.url }).catch(
|
|
10915
|
-
() =>
|
|
11043
|
+
(error) => {
|
|
11044
|
+
this.logDebugError(
|
|
11045
|
+
"cloud page reference sync after newTab failed",
|
|
11046
|
+
error
|
|
11047
|
+
);
|
|
11048
|
+
}
|
|
10916
11049
|
);
|
|
10917
11050
|
return result;
|
|
10918
11051
|
}
|
|
@@ -10924,7 +11057,12 @@ var Opensteer = class _Opensteer {
|
|
|
10924
11057
|
async switchTab(index) {
|
|
10925
11058
|
if (this.cloud) {
|
|
10926
11059
|
await this.invokeCloudActionAndResetCache("switchTab", { index });
|
|
10927
|
-
await this.syncCloudPageRef().catch(() =>
|
|
11060
|
+
await this.syncCloudPageRef().catch((error) => {
|
|
11061
|
+
this.logDebugError(
|
|
11062
|
+
"cloud page reference sync after switchTab failed",
|
|
11063
|
+
error
|
|
11064
|
+
);
|
|
11065
|
+
});
|
|
10928
11066
|
return;
|
|
10929
11067
|
}
|
|
10930
11068
|
const page = await switchTab(this.context, index);
|
|
@@ -10934,7 +11072,12 @@ var Opensteer = class _Opensteer {
|
|
|
10934
11072
|
async closeTab(index) {
|
|
10935
11073
|
if (this.cloud) {
|
|
10936
11074
|
await this.invokeCloudActionAndResetCache("closeTab", { index });
|
|
10937
|
-
await this.syncCloudPageRef().catch(() =>
|
|
11075
|
+
await this.syncCloudPageRef().catch((error) => {
|
|
11076
|
+
this.logDebugError(
|
|
11077
|
+
"cloud page reference sync after closeTab failed",
|
|
11078
|
+
error
|
|
11079
|
+
);
|
|
11080
|
+
});
|
|
10938
11081
|
return;
|
|
10939
11082
|
}
|
|
10940
11083
|
const newPage = await closeTab(this.context, this.page, index);
|
|
@@ -11094,22 +11237,28 @@ var Opensteer = class _Opensteer {
|
|
|
11094
11237
|
const handle = await this.resolveCounterHandle(resolution.counter);
|
|
11095
11238
|
try {
|
|
11096
11239
|
if (storageKey && resolution.shouldPersist) {
|
|
11097
|
-
const persistPath = await this.
|
|
11240
|
+
const persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
11098
11241
|
handle,
|
|
11099
11242
|
method,
|
|
11100
11243
|
resolution.counter
|
|
11101
11244
|
);
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11245
|
+
if (persistPath) {
|
|
11246
|
+
this.persistPath(
|
|
11247
|
+
storageKey,
|
|
11248
|
+
method,
|
|
11249
|
+
options.description,
|
|
11250
|
+
persistPath
|
|
11251
|
+
);
|
|
11252
|
+
}
|
|
11108
11253
|
}
|
|
11109
11254
|
return await counterFn(handle);
|
|
11110
11255
|
} catch (err) {
|
|
11111
|
-
|
|
11112
|
-
|
|
11256
|
+
if (err instanceof Error) {
|
|
11257
|
+
throw err;
|
|
11258
|
+
}
|
|
11259
|
+
throw new Error(
|
|
11260
|
+
`${method} failed. ${extractErrorMessage(err, "Unknown error.")}`
|
|
11261
|
+
);
|
|
11113
11262
|
} finally {
|
|
11114
11263
|
await handle.dispose();
|
|
11115
11264
|
}
|
|
@@ -11138,7 +11287,7 @@ var Opensteer = class _Opensteer {
|
|
|
11138
11287
|
let persistPath = null;
|
|
11139
11288
|
try {
|
|
11140
11289
|
if (storageKey && resolution.shouldPersist) {
|
|
11141
|
-
persistPath = await this.
|
|
11290
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
11142
11291
|
handle,
|
|
11143
11292
|
"uploadFile",
|
|
11144
11293
|
resolution.counter
|
|
@@ -11236,6 +11385,9 @@ var Opensteer = class _Opensteer {
|
|
|
11236
11385
|
if (this.cloud) {
|
|
11237
11386
|
return await this.invokeCloudAction("extract", options);
|
|
11238
11387
|
}
|
|
11388
|
+
if (options.schema !== void 0) {
|
|
11389
|
+
assertValidExtractSchemaRoot(options.schema);
|
|
11390
|
+
}
|
|
11239
11391
|
const storageKey = this.resolveStorageKey(options.description);
|
|
11240
11392
|
const schemaHash = options.schema ? computeSchemaHash(options.schema) : null;
|
|
11241
11393
|
const stored = storageKey ? this.storage.readSelector(storageKey) : null;
|
|
@@ -11244,7 +11396,7 @@ var Opensteer = class _Opensteer {
|
|
|
11244
11396
|
try {
|
|
11245
11397
|
payload = normalizePersistedExtractPayload(stored.path);
|
|
11246
11398
|
} catch (err) {
|
|
11247
|
-
const message = err
|
|
11399
|
+
const message = extractErrorMessage(err, "Unknown error.");
|
|
11248
11400
|
const selectorFile = storageKey ? this.storage.getSelectorPath(storageKey) : "unknown selector file";
|
|
11249
11401
|
throw new Error(
|
|
11250
11402
|
`Cached extraction selector is invalid for the current schema at "${selectorFile}". Delete the cached selector and rerun extraction. ${message}`
|
|
@@ -11261,7 +11413,16 @@ var Opensteer = class _Opensteer {
|
|
|
11261
11413
|
fields.push(...schemaFields);
|
|
11262
11414
|
}
|
|
11263
11415
|
if (!fields.length) {
|
|
11264
|
-
|
|
11416
|
+
let planResult;
|
|
11417
|
+
try {
|
|
11418
|
+
planResult = await this.parseAiExtractPlan(options);
|
|
11419
|
+
} catch (error) {
|
|
11420
|
+
const message = extractErrorMessage(error, "Unknown error.");
|
|
11421
|
+
const contextMessage = options.schema ? "Schema extraction did not resolve deterministic field targets, so Opensteer attempted AI extraction planning." : "Opensteer attempted AI extraction planning.";
|
|
11422
|
+
throw new Error(`${contextMessage} ${message}`, {
|
|
11423
|
+
cause: error
|
|
11424
|
+
});
|
|
11425
|
+
}
|
|
11265
11426
|
if (planResult.fields.length) {
|
|
11266
11427
|
fields.push(...planResult.fields);
|
|
11267
11428
|
} else if (planResult.data !== void 0) {
|
|
@@ -11402,7 +11563,7 @@ var Opensteer = class _Opensteer {
|
|
|
11402
11563
|
let persistPath = null;
|
|
11403
11564
|
try {
|
|
11404
11565
|
if (storageKey && resolution.shouldPersist) {
|
|
11405
|
-
persistPath = await this.
|
|
11566
|
+
persistPath = await this.tryBuildPathFromResolvedHandle(
|
|
11406
11567
|
handle,
|
|
11407
11568
|
"click",
|
|
11408
11569
|
resolution.counter
|
|
@@ -11498,17 +11659,6 @@ var Opensteer = class _Opensteer {
|
|
|
11498
11659
|
}
|
|
11499
11660
|
}
|
|
11500
11661
|
if (options.element != null) {
|
|
11501
|
-
const pathFromElement = await this.tryBuildPathFromCounter(
|
|
11502
|
-
options.element
|
|
11503
|
-
);
|
|
11504
|
-
if (pathFromElement) {
|
|
11505
|
-
return {
|
|
11506
|
-
path: pathFromElement,
|
|
11507
|
-
counter: null,
|
|
11508
|
-
shouldPersist: Boolean(storageKey),
|
|
11509
|
-
source: "element"
|
|
11510
|
-
};
|
|
11511
|
-
}
|
|
11512
11662
|
return {
|
|
11513
11663
|
path: null,
|
|
11514
11664
|
counter: options.element,
|
|
@@ -11536,17 +11686,6 @@ var Opensteer = class _Opensteer {
|
|
|
11536
11686
|
options.description
|
|
11537
11687
|
);
|
|
11538
11688
|
if (resolved?.counter != null) {
|
|
11539
|
-
const pathFromAiCounter = await this.tryBuildPathFromCounter(
|
|
11540
|
-
resolved.counter
|
|
11541
|
-
);
|
|
11542
|
-
if (pathFromAiCounter) {
|
|
11543
|
-
return {
|
|
11544
|
-
path: pathFromAiCounter,
|
|
11545
|
-
counter: null,
|
|
11546
|
-
shouldPersist: Boolean(storageKey),
|
|
11547
|
-
source: "ai"
|
|
11548
|
-
};
|
|
11549
|
-
}
|
|
11550
11689
|
return {
|
|
11551
11690
|
path: null,
|
|
11552
11691
|
counter: resolved.counter,
|
|
@@ -11628,23 +11767,22 @@ var Opensteer = class _Opensteer {
|
|
|
11628
11767
|
try {
|
|
11629
11768
|
const builtPath = await buildElementPathFromHandle(handle);
|
|
11630
11769
|
if (builtPath) {
|
|
11631
|
-
|
|
11770
|
+
const withFrameContext = await this.withHandleIframeContext(
|
|
11771
|
+
handle,
|
|
11772
|
+
builtPath
|
|
11773
|
+
);
|
|
11774
|
+
return this.withIndexedIframeContext(
|
|
11775
|
+
withFrameContext,
|
|
11776
|
+
indexedPath
|
|
11777
|
+
);
|
|
11632
11778
|
}
|
|
11633
11779
|
return indexedPath;
|
|
11634
11780
|
} finally {
|
|
11635
11781
|
await handle.dispose();
|
|
11636
11782
|
}
|
|
11637
11783
|
}
|
|
11638
|
-
async tryBuildPathFromCounter(counter) {
|
|
11639
|
-
try {
|
|
11640
|
-
return await this.buildPathFromElement(counter);
|
|
11641
|
-
} catch {
|
|
11642
|
-
return null;
|
|
11643
|
-
}
|
|
11644
|
-
}
|
|
11645
11784
|
async resolveCounterHandle(element) {
|
|
11646
|
-
|
|
11647
|
-
return resolveCounterElement(this.page, snapshot, element);
|
|
11785
|
+
return resolveCounterElement(this.page, element);
|
|
11648
11786
|
}
|
|
11649
11787
|
async resolveCounterHandleForAction(action, description, element) {
|
|
11650
11788
|
try {
|
|
@@ -11668,8 +11806,12 @@ var Opensteer = class _Opensteer {
|
|
|
11668
11806
|
const indexedPath = await this.readPathFromCounterIndex(counter);
|
|
11669
11807
|
const builtPath = await buildElementPathFromHandle(handle);
|
|
11670
11808
|
if (builtPath) {
|
|
11809
|
+
const withFrameContext = await this.withHandleIframeContext(
|
|
11810
|
+
handle,
|
|
11811
|
+
builtPath
|
|
11812
|
+
);
|
|
11671
11813
|
const normalized = this.withIndexedIframeContext(
|
|
11672
|
-
|
|
11814
|
+
withFrameContext,
|
|
11673
11815
|
indexedPath
|
|
11674
11816
|
);
|
|
11675
11817
|
if (normalized.nodes.length) return normalized;
|
|
@@ -11679,15 +11821,34 @@ var Opensteer = class _Opensteer {
|
|
|
11679
11821
|
`Unable to build element path from counter ${counter} during ${action}.`
|
|
11680
11822
|
);
|
|
11681
11823
|
}
|
|
11824
|
+
async tryBuildPathFromResolvedHandle(handle, action, counter) {
|
|
11825
|
+
try {
|
|
11826
|
+
return await this.buildPathFromResolvedHandle(handle, action, counter);
|
|
11827
|
+
} catch (error) {
|
|
11828
|
+
this.logDebugError(
|
|
11829
|
+
`path persistence skipped for ${action} counter ${counter}`,
|
|
11830
|
+
error
|
|
11831
|
+
);
|
|
11832
|
+
return null;
|
|
11833
|
+
}
|
|
11834
|
+
}
|
|
11682
11835
|
withIndexedIframeContext(builtPath, indexedPath) {
|
|
11683
11836
|
const normalizedBuilt = this.normalizePath(builtPath);
|
|
11684
11837
|
if (!indexedPath) return normalizedBuilt;
|
|
11685
11838
|
const iframePrefix = collectIframeContextPrefix(indexedPath);
|
|
11686
11839
|
if (!iframePrefix.length) return normalizedBuilt;
|
|
11840
|
+
const builtContext = cloneContextHops(normalizedBuilt.context);
|
|
11841
|
+
const overlap = measureContextOverlap(iframePrefix, builtContext);
|
|
11842
|
+
const missingPrefix = cloneContextHops(
|
|
11843
|
+
iframePrefix.slice(0, iframePrefix.length - overlap)
|
|
11844
|
+
);
|
|
11845
|
+
if (!missingPrefix.length) {
|
|
11846
|
+
return normalizedBuilt;
|
|
11847
|
+
}
|
|
11687
11848
|
const merged = {
|
|
11688
11849
|
context: [
|
|
11689
|
-
...
|
|
11690
|
-
...
|
|
11850
|
+
...missingPrefix,
|
|
11851
|
+
...builtContext
|
|
11691
11852
|
],
|
|
11692
11853
|
nodes: cloneElementPath(normalizedBuilt).nodes
|
|
11693
11854
|
};
|
|
@@ -11697,9 +11858,48 @@ var Opensteer = class _Opensteer {
|
|
|
11697
11858
|
if (fallback.nodes.length) return fallback;
|
|
11698
11859
|
return normalizedBuilt;
|
|
11699
11860
|
}
|
|
11861
|
+
async withHandleIframeContext(handle, path5) {
|
|
11862
|
+
const ownFrame = await handle.ownerFrame();
|
|
11863
|
+
if (!ownFrame) {
|
|
11864
|
+
return this.normalizePath(path5);
|
|
11865
|
+
}
|
|
11866
|
+
let frame = ownFrame;
|
|
11867
|
+
let prefix = [];
|
|
11868
|
+
while (frame && frame !== this.page.mainFrame()) {
|
|
11869
|
+
const parent = frame.parentFrame();
|
|
11870
|
+
if (!parent) break;
|
|
11871
|
+
const frameElement = await frame.frameElement().catch(() => null);
|
|
11872
|
+
if (!frameElement) break;
|
|
11873
|
+
try {
|
|
11874
|
+
const frameElementPath = await buildElementPathFromHandle(frameElement);
|
|
11875
|
+
if (frameElementPath?.nodes.length) {
|
|
11876
|
+
const segment = [
|
|
11877
|
+
...cloneContextHops(frameElementPath.context),
|
|
11878
|
+
{
|
|
11879
|
+
kind: "iframe",
|
|
11880
|
+
host: cloneElementPath(frameElementPath).nodes
|
|
11881
|
+
}
|
|
11882
|
+
];
|
|
11883
|
+
prefix = [...segment, ...prefix];
|
|
11884
|
+
}
|
|
11885
|
+
} finally {
|
|
11886
|
+
await frameElement.dispose().catch(() => void 0);
|
|
11887
|
+
}
|
|
11888
|
+
frame = parent;
|
|
11889
|
+
}
|
|
11890
|
+
if (!prefix.length) {
|
|
11891
|
+
return this.normalizePath(path5);
|
|
11892
|
+
}
|
|
11893
|
+
return this.normalizePath({
|
|
11894
|
+
context: [...prefix, ...cloneContextHops(path5.context)],
|
|
11895
|
+
nodes: cloneElementPath(path5).nodes
|
|
11896
|
+
});
|
|
11897
|
+
}
|
|
11700
11898
|
async readPathFromCounterIndex(counter) {
|
|
11701
|
-
|
|
11702
|
-
|
|
11899
|
+
if (!this.snapshotCache || this.snapshotCache.url !== this.page.url() || !this.snapshotCache.counterIndex) {
|
|
11900
|
+
return null;
|
|
11901
|
+
}
|
|
11902
|
+
const indexed = this.snapshotCache.counterIndex.get(counter);
|
|
11703
11903
|
if (!indexed) return null;
|
|
11704
11904
|
const normalized = this.normalizePath(indexed);
|
|
11705
11905
|
if (!normalized.nodes.length) return null;
|
|
@@ -11710,15 +11910,6 @@ var Opensteer = class _Opensteer {
|
|
|
11710
11910
|
if (!path5) return null;
|
|
11711
11911
|
return this.normalizePath(path5);
|
|
11712
11912
|
}
|
|
11713
|
-
async ensureSnapshotWithCounters() {
|
|
11714
|
-
if (!this.snapshotCache || !this.snapshotCache.counterBindings || this.snapshotCache.url !== this.page.url()) {
|
|
11715
|
-
await this.snapshot({
|
|
11716
|
-
mode: "full",
|
|
11717
|
-
withCounters: true
|
|
11718
|
-
});
|
|
11719
|
-
}
|
|
11720
|
-
return this.snapshotCache;
|
|
11721
|
-
}
|
|
11722
11913
|
persistPath(id, method, description, path5) {
|
|
11723
11914
|
const now = Date.now();
|
|
11724
11915
|
const safeFile = this.storage.getSelectorFileName(id);
|
|
@@ -11923,12 +12114,6 @@ var Opensteer = class _Opensteer {
|
|
|
11923
12114
|
};
|
|
11924
12115
|
}
|
|
11925
12116
|
async buildFieldTargetsFromSchema(schema) {
|
|
11926
|
-
if (!schema || typeof schema !== "object") {
|
|
11927
|
-
return [];
|
|
11928
|
-
}
|
|
11929
|
-
if (Array.isArray(schema)) {
|
|
11930
|
-
return [];
|
|
11931
|
-
}
|
|
11932
12117
|
const fields = [];
|
|
11933
12118
|
await this.collectFieldTargetsFromSchemaObject(
|
|
11934
12119
|
schema,
|
|
@@ -11974,17 +12159,6 @@ var Opensteer = class _Opensteer {
|
|
|
11974
12159
|
return;
|
|
11975
12160
|
}
|
|
11976
12161
|
if (normalized.element != null) {
|
|
11977
|
-
const path5 = await this.tryBuildPathFromCounter(
|
|
11978
|
-
normalized.element
|
|
11979
|
-
);
|
|
11980
|
-
if (path5) {
|
|
11981
|
-
fields.push({
|
|
11982
|
-
key: fieldKey,
|
|
11983
|
-
path: path5,
|
|
11984
|
-
attribute: normalized.attribute
|
|
11985
|
-
});
|
|
11986
|
-
return;
|
|
11987
|
-
}
|
|
11988
12162
|
fields.push({
|
|
11989
12163
|
key: fieldKey,
|
|
11990
12164
|
counter: normalized.element,
|
|
@@ -12002,6 +12176,10 @@ var Opensteer = class _Opensteer {
|
|
|
12002
12176
|
path: path5,
|
|
12003
12177
|
attribute: normalized.attribute
|
|
12004
12178
|
});
|
|
12179
|
+
} else {
|
|
12180
|
+
throw new Error(
|
|
12181
|
+
`Extraction schema field "${fieldKey}" uses selector "${normalized.selector}", but no matching element path could be built from the current page snapshot.`
|
|
12182
|
+
);
|
|
12005
12183
|
}
|
|
12006
12184
|
return;
|
|
12007
12185
|
}
|
|
@@ -12025,15 +12203,6 @@ var Opensteer = class _Opensteer {
|
|
|
12025
12203
|
continue;
|
|
12026
12204
|
}
|
|
12027
12205
|
if (fieldPlan.element != null) {
|
|
12028
|
-
const path6 = await this.tryBuildPathFromCounter(fieldPlan.element);
|
|
12029
|
-
if (path6) {
|
|
12030
|
-
fields.push({
|
|
12031
|
-
key,
|
|
12032
|
-
path: path6,
|
|
12033
|
-
attribute: fieldPlan.attribute
|
|
12034
|
-
});
|
|
12035
|
-
continue;
|
|
12036
|
-
}
|
|
12037
12206
|
fields.push({
|
|
12038
12207
|
key,
|
|
12039
12208
|
counter: fieldPlan.element,
|
|
@@ -12088,12 +12257,7 @@ var Opensteer = class _Opensteer {
|
|
|
12088
12257
|
}
|
|
12089
12258
|
}
|
|
12090
12259
|
if (counterRequests.length) {
|
|
12091
|
-
const
|
|
12092
|
-
const counterValues = await resolveCountersBatch(
|
|
12093
|
-
this.page,
|
|
12094
|
-
snapshot,
|
|
12095
|
-
counterRequests
|
|
12096
|
-
);
|
|
12260
|
+
const counterValues = await resolveCountersBatch(this.page, counterRequests);
|
|
12097
12261
|
Object.assign(result, counterValues);
|
|
12098
12262
|
}
|
|
12099
12263
|
if (pathFields.length) {
|
|
@@ -12123,7 +12287,7 @@ var Opensteer = class _Opensteer {
|
|
|
12123
12287
|
const path5 = await this.buildPathFromElement(field.counter);
|
|
12124
12288
|
if (!path5) {
|
|
12125
12289
|
throw new Error(
|
|
12126
|
-
`Unable to
|
|
12290
|
+
`Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
|
|
12127
12291
|
);
|
|
12128
12292
|
}
|
|
12129
12293
|
resolved.push({
|
|
@@ -12145,7 +12309,7 @@ var Opensteer = class _Opensteer {
|
|
|
12145
12309
|
}
|
|
12146
12310
|
resolveStorageKey(description) {
|
|
12147
12311
|
if (!description) return null;
|
|
12148
|
-
return (0,
|
|
12312
|
+
return (0, import_crypto.createHash)("sha256").update(description).digest("hex").slice(0, 16);
|
|
12149
12313
|
}
|
|
12150
12314
|
normalizePath(path5) {
|
|
12151
12315
|
return sanitizeElementPath(path5);
|
|
@@ -12169,6 +12333,33 @@ function collectIframeContextPrefix(path5) {
|
|
|
12169
12333
|
if (lastIframeIndex < 0) return [];
|
|
12170
12334
|
return cloneContextHops(context.slice(0, lastIframeIndex + 1));
|
|
12171
12335
|
}
|
|
12336
|
+
function measureContextOverlap(indexedPrefix, builtContext) {
|
|
12337
|
+
const maxOverlap = Math.min(indexedPrefix.length, builtContext.length);
|
|
12338
|
+
for (let size = maxOverlap; size > 0; size -= 1) {
|
|
12339
|
+
if (matchesContextPrefix(indexedPrefix, builtContext, size, true)) {
|
|
12340
|
+
return size;
|
|
12341
|
+
}
|
|
12342
|
+
}
|
|
12343
|
+
for (let size = maxOverlap; size > 0; size -= 1) {
|
|
12344
|
+
if (matchesContextPrefix(indexedPrefix, builtContext, size, false)) {
|
|
12345
|
+
return size;
|
|
12346
|
+
}
|
|
12347
|
+
}
|
|
12348
|
+
return 0;
|
|
12349
|
+
}
|
|
12350
|
+
function matchesContextPrefix(indexedPrefix, builtContext, size, strictHost) {
|
|
12351
|
+
for (let idx = 0; idx < size; idx += 1) {
|
|
12352
|
+
const left = indexedPrefix[indexedPrefix.length - size + idx];
|
|
12353
|
+
const right = builtContext[idx];
|
|
12354
|
+
if (left.kind !== right.kind) {
|
|
12355
|
+
return false;
|
|
12356
|
+
}
|
|
12357
|
+
if (strictHost && JSON.stringify(left.host) !== JSON.stringify(right.host)) {
|
|
12358
|
+
return false;
|
|
12359
|
+
}
|
|
12360
|
+
}
|
|
12361
|
+
return true;
|
|
12362
|
+
}
|
|
12172
12363
|
function normalizeSchemaValue(value) {
|
|
12173
12364
|
if (!value) return null;
|
|
12174
12365
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -12190,7 +12381,7 @@ function normalizeExtractSource(source) {
|
|
|
12190
12381
|
}
|
|
12191
12382
|
function computeSchemaHash(schema) {
|
|
12192
12383
|
const stable = stableStringify(schema);
|
|
12193
|
-
return (0,
|
|
12384
|
+
return (0, import_crypto.createHash)("sha256").update(stable).digest("hex");
|
|
12194
12385
|
}
|
|
12195
12386
|
function buildPathMap(fields) {
|
|
12196
12387
|
const out = {};
|
|
@@ -12352,13 +12543,28 @@ function countNonNullLeaves(value) {
|
|
|
12352
12543
|
function isPrimitiveLike(value) {
|
|
12353
12544
|
return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
12354
12545
|
}
|
|
12546
|
+
function assertValidExtractSchemaRoot(schema) {
|
|
12547
|
+
if (!schema || typeof schema !== "object") {
|
|
12548
|
+
throw new Error(
|
|
12549
|
+
"Invalid extraction schema: expected a JSON object at the top level."
|
|
12550
|
+
);
|
|
12551
|
+
}
|
|
12552
|
+
if (Array.isArray(schema)) {
|
|
12553
|
+
throw new Error(
|
|
12554
|
+
'Invalid extraction schema: top-level arrays are not supported. Wrap array fields in an object (for example {"items":[...]}).'
|
|
12555
|
+
);
|
|
12556
|
+
}
|
|
12557
|
+
}
|
|
12355
12558
|
function parseAiExtractResponse(response) {
|
|
12356
12559
|
if (typeof response === "string") {
|
|
12357
12560
|
const trimmed = stripCodeFence2(response);
|
|
12358
12561
|
try {
|
|
12359
12562
|
return JSON.parse(trimmed);
|
|
12360
12563
|
} catch {
|
|
12361
|
-
|
|
12564
|
+
const preview = summarizeForError(trimmed);
|
|
12565
|
+
throw new Error(
|
|
12566
|
+
`LLM extraction returned a non-JSON response.${preview ? ` Preview: "${preview}"` : ""}`
|
|
12567
|
+
);
|
|
12362
12568
|
}
|
|
12363
12569
|
}
|
|
12364
12570
|
if (response && typeof response === "object") {
|
|
@@ -12383,6 +12589,12 @@ function stripCodeFence2(input) {
|
|
|
12383
12589
|
if (lastFence === -1) return withoutHeader.trim();
|
|
12384
12590
|
return withoutHeader.slice(0, lastFence).trim();
|
|
12385
12591
|
}
|
|
12592
|
+
function summarizeForError(input, maxLength = 180) {
|
|
12593
|
+
const compact = input.replace(/\s+/g, " ").trim();
|
|
12594
|
+
if (!compact) return "";
|
|
12595
|
+
if (compact.length <= maxLength) return compact;
|
|
12596
|
+
return `${compact.slice(0, maxLength)}...`;
|
|
12597
|
+
}
|
|
12386
12598
|
function getScrollDelta2(options) {
|
|
12387
12599
|
const amount = typeof options.amount === "number" ? options.amount : 600;
|
|
12388
12600
|
const absoluteAmount = Math.abs(amount);
|
|
@@ -12405,7 +12617,7 @@ function isInternalOrBlankPageUrl(url) {
|
|
|
12405
12617
|
}
|
|
12406
12618
|
function buildLocalRunId(namespace) {
|
|
12407
12619
|
const normalized = namespace.trim() || "default";
|
|
12408
|
-
return `${normalized}-${Date.now().toString(36)}-${(0,
|
|
12620
|
+
return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
|
|
12409
12621
|
}
|
|
12410
12622
|
|
|
12411
12623
|
// src/ai/index.ts
|
|
@@ -12460,7 +12672,6 @@ init_model();
|
|
|
12460
12672
|
createExtractCallback,
|
|
12461
12673
|
createResolveCallback,
|
|
12462
12674
|
createTab,
|
|
12463
|
-
ensureLiveCounters,
|
|
12464
12675
|
exportCookies,
|
|
12465
12676
|
extractArrayRowsWithPaths,
|
|
12466
12677
|
extractArrayWithPaths,
|