@skrillex1224/android-toolkit 1.0.3 → 1.0.6
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 +7 -8
- package/browser.d.ts +12 -0
- package/dist/browser.js +21 -4
- package/dist/browser.js.map +3 -3
- package/dist/index.cjs +376 -370
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +375 -369
- package/dist/index.js.map +4 -4
- package/index.d.ts +11 -7
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,7 @@ var import_node_fs = __toESM(require("node:fs"), 1);
|
|
|
40
40
|
var constants_exports = {};
|
|
41
41
|
__export(constants_exports, {
|
|
42
42
|
ActorInfo: () => ActorInfo,
|
|
43
|
+
AppiumSettings: () => AppiumSettings,
|
|
43
44
|
Code: () => Code,
|
|
44
45
|
Status: () => Status,
|
|
45
46
|
UnicodeIme: () => UnicodeIme
|
|
@@ -56,7 +57,7 @@ var Code = Object.freeze({
|
|
|
56
57
|
ContentUnavailable: 30010003,
|
|
57
58
|
SourceExtractionFailed: 30010004,
|
|
58
59
|
AutomationFailed: 30010005,
|
|
59
|
-
|
|
60
|
+
DataAccessUnavailable: 30010008,
|
|
60
61
|
AppNotInstalled: 30010009
|
|
61
62
|
});
|
|
62
63
|
var Status = Object.freeze({
|
|
@@ -66,7 +67,12 @@ var Status = Object.freeze({
|
|
|
66
67
|
var UnicodeIme = Object.freeze({
|
|
67
68
|
packageName: "io.appium.settings",
|
|
68
69
|
component: "io.appium.settings/.UnicodeIME",
|
|
69
|
-
|
|
70
|
+
inputAction: "ADB_INPUT_TEXT"
|
|
71
|
+
});
|
|
72
|
+
var AppiumSettings = Object.freeze({
|
|
73
|
+
packageName: "io.appium.settings",
|
|
74
|
+
clipboardReceiver: "io.appium.settings/.receivers.ClipboardReceiver",
|
|
75
|
+
clipboardGetAction: "io.appium.settings.clipboard.get"
|
|
70
76
|
});
|
|
71
77
|
var normalizeShare = (share) => {
|
|
72
78
|
const source = share && typeof share === "object" ? share : {};
|
|
@@ -111,7 +117,6 @@ function createAndroidContext(input = {}, defaults = {}) {
|
|
|
111
117
|
serial: firstNonEmpty(defaults.serial, device.serial),
|
|
112
118
|
packageName: firstNonEmpty(defaults.packageName, device.packageName),
|
|
113
119
|
adbPath: firstNonEmpty(defaults.adbPath, process.env.ANDROID_TOOLKIT_ADB_PATH),
|
|
114
|
-
fridaPath: firstNonEmpty(defaults.fridaPath, process.env.ANDROID_TOOLKIT_FRIDA_PATH),
|
|
115
120
|
appVersion: firstNonEmpty(defaults.appVersion, device.appVersion),
|
|
116
121
|
slotKey: firstNonEmpty(defaults.slotKey, device.slotKey),
|
|
117
122
|
instanceId: firstNonEmpty(defaults.instanceId, device.instanceId),
|
|
@@ -297,6 +302,8 @@ var Device = {
|
|
|
297
302
|
screenshotPng,
|
|
298
303
|
screenshotBase64,
|
|
299
304
|
dumpUiXml,
|
|
305
|
+
readClipboard,
|
|
306
|
+
clearClipboard,
|
|
300
307
|
wakeAndUnlock,
|
|
301
308
|
hideKeyboard,
|
|
302
309
|
isKeyboardVisible,
|
|
@@ -428,10 +435,12 @@ async function pressEnter(ctx) {
|
|
|
428
435
|
}
|
|
429
436
|
async function typeText(ctx, text2) {
|
|
430
437
|
await ensureUnicodeIme(ctx);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
438
|
+
const value = String(text2 ?? "");
|
|
439
|
+
if (!value) return;
|
|
440
|
+
await adbShell(ctx, ["input", "text", shellSingleQuote(encodeUnicodeImeText(value))], {
|
|
441
|
+
timeoutMs: Math.max(15e3, value.length * 300)
|
|
434
442
|
});
|
|
443
|
+
await sleep(1200);
|
|
435
444
|
}
|
|
436
445
|
async function ensureUnicodeIme(ctx) {
|
|
437
446
|
const out = await adbShell(ctx, ["ime", "list", "-s"], { timeoutMs: 15e3 });
|
|
@@ -457,26 +466,71 @@ async function screenshotPng(ctx) {
|
|
|
457
466
|
async function screenshotBase64(ctx) {
|
|
458
467
|
return `data:image/png;base64,${(await screenshotPng(ctx)).toString("base64")}`;
|
|
459
468
|
}
|
|
460
|
-
async function
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
469
|
+
async function readClipboard(ctx) {
|
|
470
|
+
const out = await adbShell(ctx, [
|
|
471
|
+
"am",
|
|
472
|
+
"broadcast",
|
|
473
|
+
"-n",
|
|
474
|
+
AppiumSettings.clipboardReceiver,
|
|
475
|
+
"-a",
|
|
476
|
+
AppiumSettings.clipboardGetAction
|
|
477
|
+
], {
|
|
478
|
+
timeoutMs: 8e3,
|
|
479
|
+
maxBuffer: 1024 * 1024
|
|
480
|
+
}).catch((error) => {
|
|
481
|
+
throw new CrawlerError({
|
|
482
|
+
message: `source_extraction_failed: Appium Settings clipboard receiver unavailable ${error?.message || String(error)}`,
|
|
483
|
+
code: Code.SourceExtractionFailed,
|
|
484
|
+
context: { receiver: AppiumSettings.clipboardReceiver }
|
|
485
|
+
});
|
|
467
486
|
});
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
487
|
+
const text2 = normalizeAppiumClipboardBroadcast(out);
|
|
488
|
+
if (!text2) {
|
|
489
|
+
throw new CrawlerError({
|
|
490
|
+
message: "source_extraction_failed: ADB clipboard is empty",
|
|
491
|
+
code: Code.SourceExtractionFailed,
|
|
492
|
+
context: { receiver: AppiumSettings.clipboardReceiver, output: String(out || "").slice(0, 1e3) }
|
|
472
493
|
});
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
494
|
+
}
|
|
495
|
+
return text2;
|
|
496
|
+
}
|
|
497
|
+
async function clearClipboard(ctx) {
|
|
498
|
+
Logger.info("Device.clearClipboard skipped", {
|
|
499
|
+
reason: "Android shell clipboard set is not portable; captureLink validates by link change."
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function dumpUiXml(ctx) {
|
|
503
|
+
let lastError = null;
|
|
504
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
505
|
+
const dumpPath = `/data/local/tmp/android-toolkit-window-${Date.now()}-${Math.random().toString(16).slice(2)}.xml`;
|
|
477
506
|
await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
|
|
478
507
|
});
|
|
508
|
+
try {
|
|
509
|
+
await adbShell(ctx, ["uiautomator", "dump", dumpPath], {
|
|
510
|
+
timeoutMs: 3e4,
|
|
511
|
+
maxBuffer: 4 * 1024 * 1024
|
|
512
|
+
});
|
|
513
|
+
const raw = await adbShell(ctx, ["cat", dumpPath], {
|
|
514
|
+
timeoutMs: 3e4,
|
|
515
|
+
maxBuffer: 16 * 1024 * 1024
|
|
516
|
+
});
|
|
517
|
+
const start = String(raw || "").indexOf("<?xml");
|
|
518
|
+
if (start < 0) throw new CrawlerError({ message: `automation_failed: uiautomator dump missing xml: ${raw}`, code: Code.AutomationFailed });
|
|
519
|
+
return String(raw || "").slice(start).trim();
|
|
520
|
+
} catch (error) {
|
|
521
|
+
lastError = error;
|
|
522
|
+
Logger.warn("uiautomator dump retry", { attempt, message: error?.message || String(error) });
|
|
523
|
+
await sleep(350 * attempt);
|
|
524
|
+
} finally {
|
|
525
|
+
await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
|
|
526
|
+
});
|
|
527
|
+
}
|
|
479
528
|
}
|
|
529
|
+
throw CrawlerError.isCrawlerError(lastError) ? lastError : new CrawlerError({
|
|
530
|
+
message: `automation_failed: uiautomator dump failed ${lastError?.message || String(lastError || "")}`.trim(),
|
|
531
|
+
code: Code.AutomationFailed,
|
|
532
|
+
context: { lastError: lastError?.message || String(lastError || "") }
|
|
533
|
+
});
|
|
480
534
|
}
|
|
481
535
|
async function wakeAndUnlock(ctx) {
|
|
482
536
|
await pressKey(ctx, "KEYCODE_WAKEUP").catch(() => {
|
|
@@ -514,11 +568,51 @@ function requireValue(value, name) {
|
|
|
514
568
|
}
|
|
515
569
|
function isAdbUnavailableError(error) {
|
|
516
570
|
const message = String(error?.message || error || "");
|
|
517
|
-
return /ENOENT|spawn .*adb|
|
|
571
|
+
return /ENOENT|spawn .*adb|device .* not found|no devices|offline/i.test(message);
|
|
518
572
|
}
|
|
519
573
|
function compactArgs(args) {
|
|
520
574
|
return args.map(String).join(" ").slice(0, 500);
|
|
521
575
|
}
|
|
576
|
+
function encodeUnicodeImeText(text2) {
|
|
577
|
+
let out = "";
|
|
578
|
+
let buffer = "";
|
|
579
|
+
const flush = () => {
|
|
580
|
+
if (!buffer) return;
|
|
581
|
+
const bytes = Buffer.alloc(buffer.length * 2);
|
|
582
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
583
|
+
bytes.writeUInt16BE(buffer.charCodeAt(index), index * 2);
|
|
584
|
+
}
|
|
585
|
+
out += `&${bytes.toString("base64").replace(/\//g, ",").replace(/=+$/g, "")}-`;
|
|
586
|
+
buffer = "";
|
|
587
|
+
};
|
|
588
|
+
for (const char of String(text2 || "")) {
|
|
589
|
+
const code = char.charCodeAt(0);
|
|
590
|
+
if (code >= 32 && code <= 126) {
|
|
591
|
+
flush();
|
|
592
|
+
out += char === "&" ? "&-" : char;
|
|
593
|
+
} else {
|
|
594
|
+
buffer += char;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
flush();
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
function shellSingleQuote(value) {
|
|
601
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
602
|
+
}
|
|
603
|
+
function normalizeAppiumClipboardBroadcast(value) {
|
|
604
|
+
const out = String(value || "");
|
|
605
|
+
if (!/Broadcast completed:\s*result=-1\b/.test(out)) return "";
|
|
606
|
+
const match = /data="([^"]*)"/.exec(out) || /data=([^\s]+)/.exec(out);
|
|
607
|
+
const base64 = match ? String(match[1] || "").trim() : "";
|
|
608
|
+
if (!base64) return "";
|
|
609
|
+
try {
|
|
610
|
+
return Buffer.from(base64, "base64").toString("utf8").trim();
|
|
611
|
+
} catch {
|
|
612
|
+
return "";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
var $DeviceInternalsForTest = Object.freeze({});
|
|
522
616
|
|
|
523
617
|
// src/apify-kit.js
|
|
524
618
|
var instance = null;
|
|
@@ -711,16 +805,19 @@ async function waitFor(ctx, selector, options = {}) {
|
|
|
711
805
|
const intervalMs = Number(options.intervalMs || options.pollIntervalMs || 500);
|
|
712
806
|
const deadline = Date.now() + timeoutMs;
|
|
713
807
|
let lastError = null;
|
|
808
|
+
let sawCleanPoll = false;
|
|
714
809
|
while (Date.now() < deadline) {
|
|
715
810
|
try {
|
|
716
811
|
const node = await find(ctx, selector, { ...options, optional: true });
|
|
812
|
+
sawCleanPoll = true;
|
|
813
|
+
lastError = null;
|
|
717
814
|
if (node) return node;
|
|
718
815
|
} catch (error) {
|
|
719
816
|
lastError = error;
|
|
720
817
|
}
|
|
721
818
|
await sleep(intervalMs);
|
|
722
819
|
}
|
|
723
|
-
if (lastError) throw lastError;
|
|
820
|
+
if (lastError && !sawCleanPoll) throw lastError;
|
|
724
821
|
throw new CrawlerError({
|
|
725
822
|
message: `automation_failed: \u7B49\u5F85 View \u8D85\u65F6 ${selectorLabel(selector)}`,
|
|
726
823
|
code: Code.AutomationFailed,
|
|
@@ -907,9 +1004,27 @@ async function click(ctx, selectorOrPoint, options = {}) {
|
|
|
907
1004
|
return { target, actual, point };
|
|
908
1005
|
}
|
|
909
1006
|
async function fill(ctx, selector, text2, options = {}) {
|
|
910
|
-
|
|
911
|
-
await
|
|
1007
|
+
const value = String(text2 ?? "");
|
|
1008
|
+
const clicked = await click(ctx, selector, {
|
|
1009
|
+
...options,
|
|
1010
|
+
settleMs: Number(options.focusSettleMs || 350)
|
|
1011
|
+
});
|
|
1012
|
+
try {
|
|
1013
|
+
await Device.typeText(ctx, value);
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
throw new CrawlerError({
|
|
1016
|
+
message: `automation_failed: View \u5199\u5165\u6587\u672C\u5931\u8D25 ${JSON.stringify(selector)}`,
|
|
1017
|
+
code: Code.AutomationFailed,
|
|
1018
|
+
context: { selector, error: error?.message || String(error) }
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
Logger.info("DeviceInput.fill", {
|
|
1022
|
+
selector,
|
|
1023
|
+
target: simplifyNode(clicked.actual || clicked.target),
|
|
1024
|
+
chars: value.length
|
|
1025
|
+
});
|
|
912
1026
|
await sleep(Number(options.settleMs || 350));
|
|
1027
|
+
return { target: clicked.actual || clicked.target, chars: value.length };
|
|
913
1028
|
}
|
|
914
1029
|
async function press(ctx, key) {
|
|
915
1030
|
await Device.pressKey(ctx, key);
|
|
@@ -997,160 +1112,18 @@ function simplifyNode(node) {
|
|
|
997
1112
|
};
|
|
998
1113
|
}
|
|
999
1114
|
|
|
1000
|
-
// src/
|
|
1115
|
+
// src/device-sqlite.js
|
|
1001
1116
|
var import_node_child_process2 = require("node:child_process");
|
|
1002
|
-
var
|
|
1117
|
+
var import_promises = require("node:fs/promises");
|
|
1003
1118
|
var import_node_os = require("node:os");
|
|
1004
1119
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const pid = await resolvePid(ctx, options.packageName || ctx.packageName);
|
|
1010
|
-
const scriptPath = writeTempScript(source, label);
|
|
1011
|
-
const args = [
|
|
1012
|
-
"-q",
|
|
1013
|
-
"-t",
|
|
1014
|
-
String(Number(options.fridaTimeoutSeconds || 8)),
|
|
1015
|
-
"-D",
|
|
1016
|
-
ctx.serial,
|
|
1017
|
-
"-p",
|
|
1018
|
-
pid,
|
|
1019
|
-
"-l",
|
|
1020
|
-
scriptPath
|
|
1021
|
-
];
|
|
1022
|
-
Logger.info("frida script start", { label, pid, timeoutMs: Number(options.timeoutMs || 12e3) });
|
|
1023
|
-
try {
|
|
1024
|
-
return await runFridaProcess(ctx.fridaPath, args, {
|
|
1025
|
-
marker,
|
|
1026
|
-
timeoutMs: Number(options.timeoutMs || 12e3),
|
|
1027
|
-
maxLines: Number(options.maxLines || 1500)
|
|
1028
|
-
});
|
|
1029
|
-
} finally {
|
|
1030
|
-
(0, import_node_fs2.rmSync)(import_node_path.default.dirname(scriptPath), { recursive: true, force: true });
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
async function resolveFridaPid(ctx, packageName = ctx.packageName) {
|
|
1034
|
-
return resolvePid(ctx, packageName);
|
|
1035
|
-
}
|
|
1036
|
-
async function assertFridaReady(ctx) {
|
|
1037
|
-
if (!ctx?.fridaPath) {
|
|
1038
|
-
throw new CrawlerError({
|
|
1039
|
-
message: "frida_unavailable: ANDROID_TOOLKIT_FRIDA_PATH is required",
|
|
1040
|
-
code: Code.FridaUnavailable
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
if (!ctx?.serial) {
|
|
1044
|
-
throw new CrawlerError({
|
|
1045
|
-
message: "frida_unavailable: device serial is required",
|
|
1046
|
-
code: Code.FridaUnavailable
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
async function resolvePid(ctx, packageName = ctx.packageName) {
|
|
1051
|
-
const target = String(packageName || "").trim();
|
|
1052
|
-
if (!target) {
|
|
1053
|
-
throw new CrawlerError({
|
|
1054
|
-
message: "frida_unavailable: packageName is required",
|
|
1055
|
-
code: Code.FridaUnavailable
|
|
1056
|
-
});
|
|
1057
|
-
}
|
|
1058
|
-
const out = await Device.adbShell(ctx, ["pidof", target], { timeoutMs: 8e3 }).catch(() => "");
|
|
1059
|
-
const pid = String(out || "").trim().split(/\s+/).find(Boolean);
|
|
1060
|
-
if (!pid) {
|
|
1061
|
-
throw new CrawlerError({
|
|
1062
|
-
message: `frida_unavailable: target app pid not found ${target}`,
|
|
1063
|
-
code: Code.FridaUnavailable,
|
|
1064
|
-
context: { packageName: target }
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
return pid;
|
|
1068
|
-
}
|
|
1069
|
-
function runFridaProcess(fridaPath, args, options) {
|
|
1070
|
-
return new Promise((resolve, reject) => {
|
|
1071
|
-
const events = [];
|
|
1072
|
-
const lines = [];
|
|
1073
|
-
let buffer = "";
|
|
1074
|
-
let finished = false;
|
|
1075
|
-
const child = (0, import_node_child_process2.spawn)(fridaPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1076
|
-
const timer = setTimeout(() => {
|
|
1077
|
-
if (finished) return;
|
|
1078
|
-
finished = true;
|
|
1079
|
-
child.kill("SIGTERM");
|
|
1080
|
-
reject(new CrawlerError({
|
|
1081
|
-
message: `frida_unavailable: frida script timeout ${options.timeoutMs}ms`,
|
|
1082
|
-
code: Code.FridaUnavailable,
|
|
1083
|
-
context: { lines: lines.slice(-40) }
|
|
1084
|
-
}));
|
|
1085
|
-
}, options.timeoutMs);
|
|
1086
|
-
const consumeLine = (line) => {
|
|
1087
|
-
const text2 = String(line || "").trim();
|
|
1088
|
-
if (!text2) return;
|
|
1089
|
-
lines.push(text2);
|
|
1090
|
-
if (lines.length > options.maxLines) lines.shift();
|
|
1091
|
-
const markerIndex = text2.indexOf(options.marker);
|
|
1092
|
-
if (markerIndex < 0) return;
|
|
1093
|
-
const jsonText = text2.slice(markerIndex + options.marker.length).trim();
|
|
1094
|
-
try {
|
|
1095
|
-
events.push(JSON.parse(jsonText));
|
|
1096
|
-
} catch (error) {
|
|
1097
|
-
events.push({ type: "parse_error", error: error?.message || String(error), line: text2 });
|
|
1098
|
-
}
|
|
1099
|
-
};
|
|
1100
|
-
const onChunk = (chunk) => {
|
|
1101
|
-
buffer += chunk.toString("utf8");
|
|
1102
|
-
let index = buffer.indexOf("\n");
|
|
1103
|
-
while (index >= 0) {
|
|
1104
|
-
consumeLine(buffer.slice(0, index));
|
|
1105
|
-
buffer = buffer.slice(index + 1);
|
|
1106
|
-
index = buffer.indexOf("\n");
|
|
1107
|
-
}
|
|
1108
|
-
};
|
|
1109
|
-
child.stdout.on("data", onChunk);
|
|
1110
|
-
child.stderr.on("data", onChunk);
|
|
1111
|
-
child.on("error", (error) => {
|
|
1112
|
-
if (finished) return;
|
|
1113
|
-
finished = true;
|
|
1114
|
-
clearTimeout(timer);
|
|
1115
|
-
reject(new CrawlerError({
|
|
1116
|
-
message: `frida_unavailable: ${error?.message || String(error)}`,
|
|
1117
|
-
code: Code.FridaUnavailable
|
|
1118
|
-
}));
|
|
1119
|
-
});
|
|
1120
|
-
child.on("close", () => {
|
|
1121
|
-
if (finished) return;
|
|
1122
|
-
finished = true;
|
|
1123
|
-
clearTimeout(timer);
|
|
1124
|
-
if (buffer.trim()) consumeLine(buffer);
|
|
1125
|
-
const parsed = events.filter((event) => event?.type !== "parse_error");
|
|
1126
|
-
if (parsed.length === 0) {
|
|
1127
|
-
reject(new CrawlerError({
|
|
1128
|
-
message: "frida_unavailable: script emitted no event",
|
|
1129
|
-
code: Code.FridaUnavailable,
|
|
1130
|
-
context: { lines: lines.slice(-40), events }
|
|
1131
|
-
}));
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
resolve(parsed.at(-1));
|
|
1135
|
-
});
|
|
1136
|
-
});
|
|
1137
|
-
}
|
|
1138
|
-
function writeTempScript(source, label) {
|
|
1139
|
-
const dir = (0, import_node_fs2.mkdtempSync)(import_node_path.default.join((0, import_node_os.tmpdir)(), `android-toolkit-${safeName(label)}-`));
|
|
1140
|
-
const scriptPath = import_node_path.default.join(dir, "script.js");
|
|
1141
|
-
(0, import_node_fs2.writeFileSync)(scriptPath, String(source || ""), "utf8");
|
|
1142
|
-
return scriptPath;
|
|
1143
|
-
}
|
|
1144
|
-
function safeName(value) {
|
|
1145
|
-
return String(value || "script").replace(/[^a-z0-9_-]+/gi, "-").slice(0, 80);
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// src/frida-client.js
|
|
1149
|
-
var Frida = {
|
|
1150
|
-
querySQLite,
|
|
1120
|
+
var import_node_util2 = require("node:util");
|
|
1121
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
1122
|
+
var DeviceSQLite = {
|
|
1123
|
+
query,
|
|
1151
1124
|
health
|
|
1152
1125
|
};
|
|
1153
|
-
async function
|
|
1126
|
+
async function query(ctx, options = {}) {
|
|
1154
1127
|
if (!options.sql) {
|
|
1155
1128
|
throw new CrawlerError({
|
|
1156
1129
|
message: "invalid_request: sql is required",
|
|
@@ -1165,118 +1138,186 @@ async function querySQLite(ctx, options = {}) {
|
|
|
1165
1138
|
dbNameExcludes: normalizeStringArray(options.dbNameExcludes),
|
|
1166
1139
|
sql: String(options.sql || ""),
|
|
1167
1140
|
args: normalizeStringArray(options.args),
|
|
1168
|
-
maxRows: Math.max(1, Number(options.maxRows || 200))
|
|
1141
|
+
maxRows: Math.max(1, Number(options.maxRows || 200)),
|
|
1142
|
+
label: String(options.label || "query-sqlite")
|
|
1169
1143
|
};
|
|
1170
|
-
|
|
1171
|
-
label: options.label || "query-sqlite",
|
|
1172
|
-
packageName: options.packageName || ctx.packageName,
|
|
1173
|
-
timeoutMs: options.timeoutMs || 3e4,
|
|
1174
|
-
fridaTimeoutSeconds: options.fridaTimeoutSeconds || 25,
|
|
1175
|
-
maxLines: options.maxLines || 4e3
|
|
1176
|
-
});
|
|
1177
|
-
if (!event.ok) {
|
|
1144
|
+
if (!config.dbPath && !config.dbDir) {
|
|
1178
1145
|
throw new CrawlerError({
|
|
1179
|
-
message:
|
|
1180
|
-
code: Code.
|
|
1181
|
-
|
|
1146
|
+
message: "invalid_request: dbPath or dbDir is required",
|
|
1147
|
+
code: Code.InvalidRequest
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
const startedAt = Date.now();
|
|
1151
|
+
const dir = await (0, import_promises.mkdtemp)(import_node_path.default.join((0, import_node_os.tmpdir)(), "android-toolkit-sqlite-"));
|
|
1152
|
+
try {
|
|
1153
|
+
const dbPaths = await resolveDeviceDbPaths(ctx, config);
|
|
1154
|
+
if (dbPaths.length === 0) {
|
|
1155
|
+
throw new CrawlerError({
|
|
1156
|
+
message: "data_access_unavailable: sqlite database not found",
|
|
1157
|
+
code: Code.DataAccessUnavailable,
|
|
1158
|
+
context: { dbDir: config.dbDir, dbPath: config.dbPath, dbNamePrefix: config.dbNamePrefix }
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
const databases = [];
|
|
1162
|
+
for (const dbPath of dbPaths) {
|
|
1163
|
+
const localDbPath = await pullDatabaseSnapshot(ctx, dbPath, dir);
|
|
1164
|
+
const result = await queryLocalSQLite(localDbPath, config);
|
|
1165
|
+
databases.push({
|
|
1166
|
+
dbPath,
|
|
1167
|
+
columns: result.columns,
|
|
1168
|
+
rows: result.rows,
|
|
1169
|
+
rowCount: result.rows.length,
|
|
1170
|
+
truncated: result.truncated
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
Logger.info("DeviceSQLite.query", {
|
|
1174
|
+
label: config.label,
|
|
1175
|
+
databaseCount: databases.length,
|
|
1176
|
+
rowCount: databases.reduce((total, item) => total + item.rowCount, 0),
|
|
1177
|
+
duration: Logger.duration(startedAt)
|
|
1178
|
+
});
|
|
1179
|
+
return { ok: true, databases, rows: databases.length === 1 ? databases[0].rows : [] };
|
|
1180
|
+
} finally {
|
|
1181
|
+
await (0, import_promises.rm)(dir, { recursive: true, force: true }).catch(() => {
|
|
1182
1182
|
});
|
|
1183
1183
|
}
|
|
1184
|
-
return event;
|
|
1185
1184
|
}
|
|
1186
|
-
async function health(ctx) {
|
|
1187
|
-
|
|
1185
|
+
async function health(ctx, options = {}) {
|
|
1186
|
+
const packageName = String(options.packageName || ctx?.packageName || "").trim();
|
|
1187
|
+
if (!packageName) {
|
|
1188
1188
|
throw new CrawlerError({
|
|
1189
|
-
message: "
|
|
1190
|
-
code: Code.
|
|
1189
|
+
message: "invalid_request: packageName is required",
|
|
1190
|
+
code: Code.InvalidRequest
|
|
1191
1191
|
});
|
|
1192
1192
|
}
|
|
1193
|
-
|
|
1193
|
+
await adbSuShell(ctx, `test -d ${shellQuote(`/data/data/${packageName}`)}`, {
|
|
1194
|
+
timeoutMs: Number(options.timeoutMs || 1e4)
|
|
1195
|
+
});
|
|
1196
|
+
return { ok: true, packageName };
|
|
1197
|
+
}
|
|
1198
|
+
async function resolveDeviceDbPaths(ctx, config) {
|
|
1199
|
+
if (config.dbPath) return [config.dbPath];
|
|
1200
|
+
const command = `cd ${shellQuote(config.dbDir)} || exit 1; for file in *; do [ -f "$file" ] || continue; case "$file" in *-wal|*-shm) continue ;; esac; printf "%s\\n" "$file"; done`;
|
|
1201
|
+
const out = await adbSuShell(ctx, command, {
|
|
1202
|
+
timeoutMs: 15e3,
|
|
1203
|
+
maxBuffer: 2 * 1024 * 1024
|
|
1204
|
+
}).catch((error) => {
|
|
1194
1205
|
throw new CrawlerError({
|
|
1195
|
-
message:
|
|
1196
|
-
code: Code.
|
|
1206
|
+
message: `data_access_unavailable: sqlite dbDir unavailable ${error?.message || String(error)}`,
|
|
1207
|
+
code: Code.DataAccessUnavailable,
|
|
1208
|
+
context: { dbDir: config.dbDir }
|
|
1197
1209
|
});
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
1217
|
-
function matchesName(name) {
|
|
1218
|
-
if (config.dbNamePrefix && name.indexOf(config.dbNamePrefix) !== 0) return false;
|
|
1219
|
-
if (config.dbNameIncludes && name.indexOf(config.dbNameIncludes) < 0) return false;
|
|
1220
|
-
for (var i = 0; i < config.dbNameExcludes.length; i++) {
|
|
1221
|
-
if (String(name).indexOf(config.dbNameExcludes[i]) >= 0) return false;
|
|
1222
|
-
}
|
|
1223
|
-
return true;
|
|
1224
|
-
}
|
|
1225
|
-
function dbPaths() {
|
|
1226
|
-
if (config.dbPath) return [config.dbPath];
|
|
1227
|
-
var File = Java.use('java.io.File');
|
|
1228
|
-
var dir = File.$new(config.dbDir);
|
|
1229
|
-
var files = dir.listFiles();
|
|
1230
|
-
var out = [];
|
|
1231
|
-
if (!files) return out;
|
|
1232
|
-
for (var i = 0; i < files.length; i++) {
|
|
1233
|
-
var name = String(files[i].getName());
|
|
1234
|
-
if (matchesName(name)) out.push(String(files[i].getAbsolutePath()));
|
|
1235
|
-
}
|
|
1236
|
-
return out;
|
|
1237
|
-
}
|
|
1238
|
-
function queryOne(dbPath) {
|
|
1239
|
-
var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase');
|
|
1240
|
-
var db = SQLiteDatabase.openDatabase(JavaString.$new(dbPath), null, 1);
|
|
1241
|
-
try {
|
|
1242
|
-
var cursor = db.rawQuery(JavaString.$new(config.sql), stringArray(config.args));
|
|
1243
|
-
var columns = [];
|
|
1244
|
-
var columnCount = cursor.getColumnCount();
|
|
1245
|
-
for (var c = 0; c < columnCount; c++) columns.push(String(cursor.getColumnName(c)));
|
|
1246
|
-
var rows = [];
|
|
1247
|
-
var truncated = false;
|
|
1248
|
-
while (cursor.moveToNext()) {
|
|
1249
|
-
if (rows.length >= config.maxRows) { truncated = true; break; }
|
|
1250
|
-
var row = {};
|
|
1251
|
-
for (var i = 0; i < columns.length; i++) row[columns[i]] = safeString(cursor, i);
|
|
1252
|
-
rows.push(row);
|
|
1253
|
-
}
|
|
1254
|
-
cursor.close();
|
|
1255
|
-
return { dbPath: dbPath, columns: columns, rows: rows, rowCount: rows.length, truncated: truncated };
|
|
1256
|
-
} finally {
|
|
1257
|
-
db.close();
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1210
|
+
});
|
|
1211
|
+
return String(out || "").split("\n").map((line) => line.trim()).filter(Boolean).filter((name) => matchesName(name, config)).map((name) => `${config.dbDir.replace(/\/+$/, "")}/${name}`).sort();
|
|
1212
|
+
}
|
|
1213
|
+
async function pullDatabaseSnapshot(ctx, dbPath, localDir) {
|
|
1214
|
+
const serial = requireNonEmpty2(ctx?.serial, "serial");
|
|
1215
|
+
const adbPath = requireNonEmpty2(ctx?.adbPath, "adbPath");
|
|
1216
|
+
const baseName = import_node_path.default.basename(dbPath);
|
|
1217
|
+
const remoteDir = `/data/local/tmp/android-toolkit-sqlite-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1218
|
+
const remoteDb = `${remoteDir}/${baseName}`;
|
|
1219
|
+
const localDb = import_node_path.default.join(localDir, baseName);
|
|
1220
|
+
const copyCommand = [
|
|
1221
|
+
`rm -rf ${shellQuote(remoteDir)}`,
|
|
1222
|
+
`mkdir -p ${shellQuote(remoteDir)}`,
|
|
1223
|
+
`cp ${shellQuote(dbPath)} ${shellQuote(remoteDb)}`,
|
|
1224
|
+
`[ -f ${shellQuote(`${dbPath}-wal`)} ] && cp ${shellQuote(`${dbPath}-wal`)} ${shellQuote(`${remoteDb}-wal`)} || true`,
|
|
1225
|
+
`[ -f ${shellQuote(`${dbPath}-shm`)} ] && cp ${shellQuote(`${dbPath}-shm`)} ${shellQuote(`${remoteDb}-shm`)} || true`,
|
|
1226
|
+
`chmod 644 ${shellQuote(remoteDir)}/*`
|
|
1227
|
+
].join(" && ");
|
|
1260
1228
|
try {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1229
|
+
await adbSuShell(ctx, copyCommand, { timeoutMs: 3e4, maxBuffer: 4 * 1024 * 1024 });
|
|
1230
|
+
await adbPull(adbPath, serial, remoteDb, localDb).catch((error) => {
|
|
1231
|
+
throw new CrawlerError({
|
|
1232
|
+
message: `data_access_unavailable: sqlite snapshot pull failed ${error?.message || String(error)}`,
|
|
1233
|
+
code: Code.DataAccessUnavailable,
|
|
1234
|
+
context: { dbPath }
|
|
1235
|
+
});
|
|
1236
|
+
});
|
|
1237
|
+
await adbPullOptional(adbPath, serial, `${remoteDb}-wal`, `${localDb}-wal`);
|
|
1238
|
+
await adbPullOptional(adbPath, serial, `${remoteDb}-shm`, `${localDb}-shm`);
|
|
1239
|
+
return localDb;
|
|
1240
|
+
} finally {
|
|
1241
|
+
await adbSuShell(ctx, `rm -rf ${shellQuote(remoteDir)}`, { timeoutMs: 1e4 }).catch(() => {
|
|
1242
|
+
});
|
|
1268
1243
|
}
|
|
1269
|
-
}
|
|
1244
|
+
}
|
|
1245
|
+
async function adbSuShell(ctx, command, options = {}) {
|
|
1246
|
+
return Device.adbShell(ctx, [`su -c ${shellQuote(command)}`], options);
|
|
1247
|
+
}
|
|
1248
|
+
async function queryLocalSQLite(dbPath, config) {
|
|
1249
|
+
const payloadPath = `${dbPath}.query.json`;
|
|
1250
|
+
const outputPath = `${dbPath}.rows.json`;
|
|
1251
|
+
await (0, import_promises.writeFile)(payloadPath, JSON.stringify({
|
|
1252
|
+
dbPath,
|
|
1253
|
+
sql: config.sql,
|
|
1254
|
+
args: config.args,
|
|
1255
|
+
maxRows: config.maxRows,
|
|
1256
|
+
outputPath
|
|
1257
|
+
}), "utf8");
|
|
1258
|
+
const code = `
|
|
1259
|
+
const fs = require('node:fs');
|
|
1260
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
1261
|
+
const payload = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
1262
|
+
const db = new DatabaseSync(payload.dbPath, { open: true, readOnly: true });
|
|
1263
|
+
try {
|
|
1264
|
+
const stmt = db.prepare(payload.sql);
|
|
1265
|
+
const columns = stmt.columns().map((column) => String(column.name || ''));
|
|
1266
|
+
const rows = [];
|
|
1267
|
+
let truncated = false;
|
|
1268
|
+
for (const row of stmt.iterate(...payload.args)) {
|
|
1269
|
+
if (rows.length >= payload.maxRows) { truncated = true; break; }
|
|
1270
|
+
const out = {};
|
|
1271
|
+
for (const column of columns) out[column] = row[column] == null ? '' : String(row[column]);
|
|
1272
|
+
rows.push(out);
|
|
1273
|
+
}
|
|
1274
|
+
fs.writeFileSync(payload.outputPath, JSON.stringify({ columns, rows, truncated }), 'utf8');
|
|
1275
|
+
} finally {
|
|
1276
|
+
db.close();
|
|
1277
|
+
}
|
|
1270
1278
|
`;
|
|
1279
|
+
await execFileAsync2(process.execPath, ["-e", code, payloadPath], {
|
|
1280
|
+
timeout: 3e4,
|
|
1281
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1282
|
+
encoding: "utf8"
|
|
1283
|
+
});
|
|
1284
|
+
return JSON.parse(await import("node:fs/promises").then((fs3) => fs3.readFile(outputPath, "utf8")));
|
|
1285
|
+
}
|
|
1286
|
+
async function adbPull(adbPath, serial, remotePath, localPath) {
|
|
1287
|
+
await execFileAsync2(adbPath, ["-s", serial, "pull", remotePath, localPath], {
|
|
1288
|
+
timeout: 3e4,
|
|
1289
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
1290
|
+
encoding: "utf8"
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
async function adbPullOptional(adbPath, serial, remotePath, localPath) {
|
|
1294
|
+
await adbPull(adbPath, serial, remotePath, localPath).catch(() => {
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
function matchesName(name, config) {
|
|
1298
|
+
if (config.dbNamePrefix && !name.startsWith(config.dbNamePrefix)) return false;
|
|
1299
|
+
if (config.dbNameIncludes && !name.includes(config.dbNameIncludes)) return false;
|
|
1300
|
+
for (const item of config.dbNameExcludes || []) {
|
|
1301
|
+
if (item && name.includes(item)) return false;
|
|
1302
|
+
}
|
|
1303
|
+
return true;
|
|
1271
1304
|
}
|
|
1272
1305
|
function normalizeStringArray(value) {
|
|
1273
1306
|
if (Array.isArray(value)) return value.map((item) => String(item ?? ""));
|
|
1274
1307
|
if (value == null) return [];
|
|
1275
1308
|
return [String(value)];
|
|
1276
1309
|
}
|
|
1310
|
+
function requireNonEmpty2(value, name) {
|
|
1311
|
+
const clean = String(value ?? "").trim();
|
|
1312
|
+
if (!clean) throw new CrawlerError({ message: `invalid_request: ${name} is required`, code: Code.InvalidRequest });
|
|
1313
|
+
return clean;
|
|
1314
|
+
}
|
|
1315
|
+
function shellQuote(value) {
|
|
1316
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1317
|
+
}
|
|
1277
1318
|
|
|
1278
1319
|
// src/launch.js
|
|
1279
|
-
var
|
|
1320
|
+
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
1280
1321
|
|
|
1281
1322
|
// src/mutation.js
|
|
1282
1323
|
var MUTATION_MONITOR_MODE = Object.freeze({
|
|
@@ -1311,8 +1352,24 @@ async function waitForStable(ctx, selectors, options = {}) {
|
|
|
1311
1352
|
let stableSince = 0;
|
|
1312
1353
|
let mutationCount = 0;
|
|
1313
1354
|
let wasPaused = false;
|
|
1355
|
+
let snapshotErrorCount = 0;
|
|
1356
|
+
let lastSnapshotError = null;
|
|
1314
1357
|
while (Date.now() < deadline) {
|
|
1315
|
-
const snapshot2 = await captureSnapshot(ctx, selectorList, options)
|
|
1358
|
+
const snapshot2 = await captureSnapshot(ctx, selectorList, options).catch((error) => {
|
|
1359
|
+
snapshotErrorCount += 1;
|
|
1360
|
+
lastSnapshotError = error;
|
|
1361
|
+
if (snapshotErrorCount === 1 || snapshotErrorCount % 5 === 0) {
|
|
1362
|
+
Logger.warn("Mutation.waitForStable snapshot skipped", {
|
|
1363
|
+
snapshotErrorCount,
|
|
1364
|
+
message: error?.message || String(error)
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
return null;
|
|
1368
|
+
});
|
|
1369
|
+
if (!snapshot2) {
|
|
1370
|
+
await sleep(pollIntervalMs);
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1316
1373
|
if (!foundInitial) {
|
|
1317
1374
|
if (snapshot2.found || Date.now() >= initialDeadline) {
|
|
1318
1375
|
foundInitial = true;
|
|
@@ -1360,7 +1417,7 @@ async function waitForStable(ctx, selectors, options = {}) {
|
|
|
1360
1417
|
}
|
|
1361
1418
|
await sleep(pollIntervalMs);
|
|
1362
1419
|
}
|
|
1363
|
-
throw new Error(`Mutation.waitForStable \u8D85\u65F6 (${timeout}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316`);
|
|
1420
|
+
throw new Error(`Mutation.waitForStable \u8D85\u65F6 (${timeout}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316, snapshotErrorCount=${snapshotErrorCount}, lastSnapshotError=${lastSnapshotError?.message || ""}`);
|
|
1364
1421
|
}
|
|
1365
1422
|
async function waitForStableAcrossRoots(ctx, selectors, options = {}) {
|
|
1366
1423
|
return waitForStable(ctx, selectors, options);
|
|
@@ -1567,12 +1624,8 @@ var compressImageBuffer = async (buffer, compression) => {
|
|
|
1567
1624
|
let smallest = null;
|
|
1568
1625
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1569
1626
|
const candidate = await encodeJpeg(sourceImage, compression, scale, quality);
|
|
1570
|
-
if (!smallest || candidate.bytes < smallest.bytes)
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
if (candidate.bytes <= compression.maxBytes) {
|
|
1574
|
-
return { ...candidate, withinLimit: true };
|
|
1575
|
-
}
|
|
1627
|
+
if (!smallest || candidate.bytes < smallest.bytes) smallest = candidate;
|
|
1628
|
+
if (candidate.bytes <= compression.maxBytes) return { ...candidate, withinLimit: true };
|
|
1576
1629
|
if (quality > minQuality) {
|
|
1577
1630
|
quality = Math.max(minQuality, Math.floor(quality * 0.75));
|
|
1578
1631
|
continue;
|
|
@@ -1582,9 +1635,7 @@ var compressImageBuffer = async (buffer, compression) => {
|
|
|
1582
1635
|
compression.minScale,
|
|
1583
1636
|
Math.min(scale * 0.85, scale * ratio * 0.94)
|
|
1584
1637
|
);
|
|
1585
|
-
if (nextScale >= scale * 0.99 || scale <= compression.minScale)
|
|
1586
|
-
break;
|
|
1587
|
-
}
|
|
1638
|
+
if (nextScale >= scale * 0.99 || scale <= compression.minScale) break;
|
|
1588
1639
|
scale = nextScale;
|
|
1589
1640
|
}
|
|
1590
1641
|
const finalCandidate = await encodeJpeg(sourceImage, compression, compression.minScale, minQuality);
|
|
@@ -1600,9 +1651,7 @@ var compressImageBufferToBase64 = async (buffer, compression) => {
|
|
|
1600
1651
|
Logger.warn("captureScreen \u538B\u7F29\u5931\u8D25\uFF0C\u8FD4\u56DE\u539F\u56FE", { message: error?.message || String(error) });
|
|
1601
1652
|
return null;
|
|
1602
1653
|
});
|
|
1603
|
-
if (!result?.buffer)
|
|
1604
|
-
return buffer.toString("base64");
|
|
1605
|
-
}
|
|
1654
|
+
if (!result?.buffer) return buffer.toString("base64");
|
|
1606
1655
|
if (result.withinLimit) {
|
|
1607
1656
|
Logger.info("captureScreen \u5DF2\u538B\u7F29", {
|
|
1608
1657
|
originalBytes,
|
|
@@ -1690,21 +1739,24 @@ async function captureLink(ctx, options = {}) {
|
|
|
1690
1739
|
const deadline = Date.now() + timeoutMs;
|
|
1691
1740
|
const prefix = String(share.prefix || "").trim();
|
|
1692
1741
|
Logger.start("Share.captureLink", { actor: actorInfo.key, prefix, timeoutMs });
|
|
1693
|
-
await
|
|
1694
|
-
|
|
1695
|
-
});
|
|
1742
|
+
const beforeEvent = await readClipboard2(ctx).catch(() => null);
|
|
1743
|
+
const beforeLink = beforeEvent ? selectLink(beforeEvent, prefix) : "";
|
|
1744
|
+
if (beforeLink) Logger.info("Share.captureLink baseline", { beforeLink });
|
|
1696
1745
|
if (typeof options.performActions === "function") {
|
|
1697
1746
|
await options.performActions();
|
|
1698
1747
|
}
|
|
1699
1748
|
let lastEvent = null;
|
|
1700
1749
|
while (Date.now() < deadline) {
|
|
1701
|
-
const event = await
|
|
1750
|
+
const event = await readClipboard2(ctx).catch((error) => ({ ok: false, error: error?.message || String(error) }));
|
|
1702
1751
|
lastEvent = event;
|
|
1703
1752
|
const link = selectLink(event, prefix);
|
|
1704
|
-
if (link) {
|
|
1753
|
+
if (link && link !== beforeLink) {
|
|
1705
1754
|
Logger.success("Share.captureLink", { link });
|
|
1706
1755
|
return { link, source: event.source || "clipboard", payloadSnapshot: event.payloadSnapshot || "" };
|
|
1707
1756
|
}
|
|
1757
|
+
if (link && link === beforeLink) {
|
|
1758
|
+
lastEvent = { ...event, rejected: "same_as_baseline", beforeLink };
|
|
1759
|
+
}
|
|
1708
1760
|
await sleep(pollIntervalMs);
|
|
1709
1761
|
}
|
|
1710
1762
|
throw new CrawlerError({
|
|
@@ -1713,77 +1765,20 @@ async function captureLink(ctx, options = {}) {
|
|
|
1713
1765
|
context: { prefix, lastEvent }
|
|
1714
1766
|
});
|
|
1715
1767
|
}
|
|
1716
|
-
async function
|
|
1717
|
-
await
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
function clipboardScript(mode) {
|
|
1732
|
-
return `
|
|
1733
|
-
Java.perform(function () {
|
|
1734
|
-
function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
|
|
1735
|
-
var mode = ${JSON.stringify(mode)};
|
|
1736
|
-
var JavaObject = Java.use('java.lang.Object');
|
|
1737
|
-
function text(value) {
|
|
1738
|
-
if (value === null || value === undefined) return '';
|
|
1739
|
-
try { return Java.cast(value, JavaObject).toString() + ''; } catch (_) {}
|
|
1740
|
-
try { return value.toString.overload().call(value) + ''; } catch (_) {}
|
|
1741
|
-
try { return value.toString() + ''; } catch (_) {}
|
|
1742
|
-
return '';
|
|
1743
|
-
}
|
|
1744
|
-
function addCandidate(out, source, value) {
|
|
1745
|
-
var s = text(value);
|
|
1746
|
-
if (!s) return;
|
|
1747
|
-
var matches = s.match(/https?:\\/\\/[^\\s"'<>\uFF0C\u3002]+/g) || [];
|
|
1748
|
-
for (var i = 0; i < matches.length; i++) {
|
|
1749
|
-
var link = matches[i].replace(/[)\\].,\uFF0C\u3002\uFF1B;!?\uFF01\uFF1F]+$/g, '');
|
|
1750
|
-
out.push({ source: source, link: link, payload: s.slice(0, 1000) });
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
try {
|
|
1754
|
-
var ActivityThread = Java.use('android.app.ActivityThread');
|
|
1755
|
-
var ClipData = Java.use('android.content.ClipData');
|
|
1756
|
-
var app = ActivityThread.currentApplication();
|
|
1757
|
-
var manager = app ? app.getSystemService('clipboard') : null;
|
|
1758
|
-
if (!manager) { emit({ ok: false, error: 'clipboard manager unavailable' }); return; }
|
|
1759
|
-
if (mode === 'clear') {
|
|
1760
|
-
manager.setPrimaryClip(ClipData.newPlainText('android-toolkit', ''));
|
|
1761
|
-
emit({ ok: true, source: 'clipboard.clear' });
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
var candidates = [];
|
|
1765
|
-
var clip = manager.getPrimaryClip();
|
|
1766
|
-
var count = clip ? clip.getItemCount() : 0;
|
|
1767
|
-
for (var i = 0; i < count; i++) {
|
|
1768
|
-
var item = clip.getItemAt(i);
|
|
1769
|
-
try { addCandidate(candidates, 'clipboard.text', item.getText()); } catch (_) {}
|
|
1770
|
-
try { addCandidate(candidates, 'clipboard.html', item.getHtmlText()); } catch (_) {}
|
|
1771
|
-
try { addCandidate(candidates, 'clipboard.uri', item.getUri()); } catch (_) {}
|
|
1772
|
-
try { addCandidate(candidates, 'clipboard.intent', item.getIntent()); } catch (_) {}
|
|
1773
|
-
try { addCandidate(candidates, 'clipboard.coerceToText', item.coerceToText(app)); } catch (_) {}
|
|
1774
|
-
}
|
|
1775
|
-
emit({
|
|
1776
|
-
ok: candidates.length > 0,
|
|
1777
|
-
link: candidates.length > 0 ? candidates[0].link : '',
|
|
1778
|
-
source: candidates.length > 0 ? candidates[0].source : 'clipboard',
|
|
1779
|
-
candidates: candidates,
|
|
1780
|
-
payloadSnapshot: candidates.length > 0 ? String(candidates[0].payload || '').slice(0, 500) : ''
|
|
1781
|
-
});
|
|
1782
|
-
} catch (error) {
|
|
1783
|
-
emit({ ok: false, error: String(error), stack: String(error.stack || '') });
|
|
1784
|
-
}
|
|
1785
|
-
});
|
|
1786
|
-
`;
|
|
1768
|
+
async function readClipboard2(ctx) {
|
|
1769
|
+
const payload = await Device.readClipboard(ctx);
|
|
1770
|
+
const candidates = extractLinks(payload).map((link) => ({
|
|
1771
|
+
source: "adb.clipboard",
|
|
1772
|
+
link,
|
|
1773
|
+
payload: payload.slice(0, 1e3)
|
|
1774
|
+
}));
|
|
1775
|
+
return {
|
|
1776
|
+
ok: candidates.length > 0,
|
|
1777
|
+
link: candidates[0]?.link || "",
|
|
1778
|
+
source: "adb.clipboard",
|
|
1779
|
+
candidates,
|
|
1780
|
+
payloadSnapshot: payload.slice(0, 500)
|
|
1781
|
+
};
|
|
1787
1782
|
}
|
|
1788
1783
|
async function composeSprite(buffers, options = {}) {
|
|
1789
1784
|
if (!buffers.length) {
|
|
@@ -1828,6 +1823,11 @@ function selectLink(event, prefix) {
|
|
|
1828
1823
|
}
|
|
1829
1824
|
return candidates.find((link) => !prefix || String(link).startsWith(prefix)) || "";
|
|
1830
1825
|
}
|
|
1826
|
+
function extractLinks(value) {
|
|
1827
|
+
const text2 = String(value || "");
|
|
1828
|
+
const matches = text2.match(/https?:\/\/[^\s"'<>,。]+/g) || [];
|
|
1829
|
+
return matches.map((link) => link.replace(/[)\].,,。;;!?!?]+$/g, ""));
|
|
1830
|
+
}
|
|
1831
1831
|
|
|
1832
1832
|
// src/launch.js
|
|
1833
1833
|
var DEFAULT_INPUT_PATH = "/apify_storage/input.json";
|
|
@@ -1839,7 +1839,7 @@ async function run(handler, options = {}) {
|
|
|
1839
1839
|
const startedAt = Date.now();
|
|
1840
1840
|
const inputPath = pathOption(options.inputPath, DEFAULT_INPUT_PATH);
|
|
1841
1841
|
const outputPath = pathOption(options.outputPath, DEFAULT_OUTPUT_PATH);
|
|
1842
|
-
const input = options.input || JSON.parse(
|
|
1842
|
+
const input = options.input || JSON.parse(import_node_fs2.default.readFileSync(inputPath, "utf8"));
|
|
1843
1843
|
const ctx = options.ctx || Context.createAndroidContext(input, options.contextDefaults || {});
|
|
1844
1844
|
ctx.runId = ctx.runId || input.run_id || input.runId || input.runtime?.run_id || input.runtime?.runId || "";
|
|
1845
1845
|
ctx.actorKey = ctx.actorKey || options.actorKey || input.actorKey || input.actor_name || input.actorName || "";
|
|
@@ -1856,7 +1856,7 @@ async function run(handler, options = {}) {
|
|
|
1856
1856
|
Device,
|
|
1857
1857
|
DeviceInput,
|
|
1858
1858
|
DeviceView,
|
|
1859
|
-
|
|
1859
|
+
DeviceSQLite,
|
|
1860
1860
|
Share,
|
|
1861
1861
|
Mutation,
|
|
1862
1862
|
Logger,
|
|
@@ -1894,20 +1894,26 @@ function pathOption(value, fallback) {
|
|
|
1894
1894
|
}
|
|
1895
1895
|
|
|
1896
1896
|
// entrys/node.js
|
|
1897
|
-
var
|
|
1898
|
-
|
|
1899
|
-
ApifyKit,
|
|
1900
|
-
DeviceInput,
|
|
1901
|
-
DeviceView,
|
|
1902
|
-
Device,
|
|
1903
|
-
Mutation,
|
|
1904
|
-
Share,
|
|
1905
|
-
Frida,
|
|
1906
|
-
Constants: constants_exports,
|
|
1907
|
-
Errors: errors_exports,
|
|
1908
|
-
Logger,
|
|
1909
|
-
Context
|
|
1897
|
+
var ToolkitMode = Object.freeze({
|
|
1898
|
+
default: "default"
|
|
1910
1899
|
});
|
|
1900
|
+
var useAndroidToolKit = () => {
|
|
1901
|
+
return {
|
|
1902
|
+
Launch,
|
|
1903
|
+
ApifyKit,
|
|
1904
|
+
DeviceInput,
|
|
1905
|
+
DeviceView,
|
|
1906
|
+
DeviceSQLite,
|
|
1907
|
+
Device,
|
|
1908
|
+
Mutation,
|
|
1909
|
+
Share,
|
|
1910
|
+
Constants: constants_exports,
|
|
1911
|
+
Errors: errors_exports,
|
|
1912
|
+
Logger,
|
|
1913
|
+
Context
|
|
1914
|
+
};
|
|
1915
|
+
};
|
|
1916
|
+
useAndroidToolKit.Mode = ToolkitMode;
|
|
1911
1917
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1912
1918
|
0 && (module.exports = {
|
|
1913
1919
|
useAndroidToolKit
|