@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.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from "node:fs";
|
|
|
11
11
|
var constants_exports = {};
|
|
12
12
|
__export(constants_exports, {
|
|
13
13
|
ActorInfo: () => ActorInfo,
|
|
14
|
+
AppiumSettings: () => AppiumSettings,
|
|
14
15
|
Code: () => Code,
|
|
15
16
|
Status: () => Status,
|
|
16
17
|
UnicodeIme: () => UnicodeIme
|
|
@@ -27,7 +28,7 @@ var Code = Object.freeze({
|
|
|
27
28
|
ContentUnavailable: 30010003,
|
|
28
29
|
SourceExtractionFailed: 30010004,
|
|
29
30
|
AutomationFailed: 30010005,
|
|
30
|
-
|
|
31
|
+
DataAccessUnavailable: 30010008,
|
|
31
32
|
AppNotInstalled: 30010009
|
|
32
33
|
});
|
|
33
34
|
var Status = Object.freeze({
|
|
@@ -37,7 +38,12 @@ var Status = Object.freeze({
|
|
|
37
38
|
var UnicodeIme = Object.freeze({
|
|
38
39
|
packageName: "io.appium.settings",
|
|
39
40
|
component: "io.appium.settings/.UnicodeIME",
|
|
40
|
-
|
|
41
|
+
inputAction: "ADB_INPUT_TEXT"
|
|
42
|
+
});
|
|
43
|
+
var AppiumSettings = Object.freeze({
|
|
44
|
+
packageName: "io.appium.settings",
|
|
45
|
+
clipboardReceiver: "io.appium.settings/.receivers.ClipboardReceiver",
|
|
46
|
+
clipboardGetAction: "io.appium.settings.clipboard.get"
|
|
41
47
|
});
|
|
42
48
|
var normalizeShare = (share) => {
|
|
43
49
|
const source = share && typeof share === "object" ? share : {};
|
|
@@ -82,7 +88,6 @@ function createAndroidContext(input = {}, defaults = {}) {
|
|
|
82
88
|
serial: firstNonEmpty(defaults.serial, device.serial),
|
|
83
89
|
packageName: firstNonEmpty(defaults.packageName, device.packageName),
|
|
84
90
|
adbPath: firstNonEmpty(defaults.adbPath, process.env.ANDROID_TOOLKIT_ADB_PATH),
|
|
85
|
-
fridaPath: firstNonEmpty(defaults.fridaPath, process.env.ANDROID_TOOLKIT_FRIDA_PATH),
|
|
86
91
|
appVersion: firstNonEmpty(defaults.appVersion, device.appVersion),
|
|
87
92
|
slotKey: firstNonEmpty(defaults.slotKey, device.slotKey),
|
|
88
93
|
instanceId: firstNonEmpty(defaults.instanceId, device.instanceId),
|
|
@@ -268,6 +273,8 @@ var Device = {
|
|
|
268
273
|
screenshotPng,
|
|
269
274
|
screenshotBase64,
|
|
270
275
|
dumpUiXml,
|
|
276
|
+
readClipboard,
|
|
277
|
+
clearClipboard,
|
|
271
278
|
wakeAndUnlock,
|
|
272
279
|
hideKeyboard,
|
|
273
280
|
isKeyboardVisible,
|
|
@@ -399,10 +406,12 @@ async function pressEnter(ctx) {
|
|
|
399
406
|
}
|
|
400
407
|
async function typeText(ctx, text2) {
|
|
401
408
|
await ensureUnicodeIme(ctx);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
409
|
+
const value = String(text2 ?? "");
|
|
410
|
+
if (!value) return;
|
|
411
|
+
await adbShell(ctx, ["input", "text", shellSingleQuote(encodeUnicodeImeText(value))], {
|
|
412
|
+
timeoutMs: Math.max(15e3, value.length * 300)
|
|
405
413
|
});
|
|
414
|
+
await sleep(1200);
|
|
406
415
|
}
|
|
407
416
|
async function ensureUnicodeIme(ctx) {
|
|
408
417
|
const out = await adbShell(ctx, ["ime", "list", "-s"], { timeoutMs: 15e3 });
|
|
@@ -428,26 +437,71 @@ async function screenshotPng(ctx) {
|
|
|
428
437
|
async function screenshotBase64(ctx) {
|
|
429
438
|
return `data:image/png;base64,${(await screenshotPng(ctx)).toString("base64")}`;
|
|
430
439
|
}
|
|
431
|
-
async function
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
440
|
+
async function readClipboard(ctx) {
|
|
441
|
+
const out = await adbShell(ctx, [
|
|
442
|
+
"am",
|
|
443
|
+
"broadcast",
|
|
444
|
+
"-n",
|
|
445
|
+
AppiumSettings.clipboardReceiver,
|
|
446
|
+
"-a",
|
|
447
|
+
AppiumSettings.clipboardGetAction
|
|
448
|
+
], {
|
|
449
|
+
timeoutMs: 8e3,
|
|
450
|
+
maxBuffer: 1024 * 1024
|
|
451
|
+
}).catch((error) => {
|
|
452
|
+
throw new CrawlerError({
|
|
453
|
+
message: `source_extraction_failed: Appium Settings clipboard receiver unavailable ${error?.message || String(error)}`,
|
|
454
|
+
code: Code.SourceExtractionFailed,
|
|
455
|
+
context: { receiver: AppiumSettings.clipboardReceiver }
|
|
456
|
+
});
|
|
438
457
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
458
|
+
const text2 = normalizeAppiumClipboardBroadcast(out);
|
|
459
|
+
if (!text2) {
|
|
460
|
+
throw new CrawlerError({
|
|
461
|
+
message: "source_extraction_failed: ADB clipboard is empty",
|
|
462
|
+
code: Code.SourceExtractionFailed,
|
|
463
|
+
context: { receiver: AppiumSettings.clipboardReceiver, output: String(out || "").slice(0, 1e3) }
|
|
443
464
|
});
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
465
|
+
}
|
|
466
|
+
return text2;
|
|
467
|
+
}
|
|
468
|
+
async function clearClipboard(ctx) {
|
|
469
|
+
Logger.info("Device.clearClipboard skipped", {
|
|
470
|
+
reason: "Android shell clipboard set is not portable; captureLink validates by link change."
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async function dumpUiXml(ctx) {
|
|
474
|
+
let lastError = null;
|
|
475
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
476
|
+
const dumpPath = `/data/local/tmp/android-toolkit-window-${Date.now()}-${Math.random().toString(16).slice(2)}.xml`;
|
|
448
477
|
await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
|
|
449
478
|
});
|
|
479
|
+
try {
|
|
480
|
+
await adbShell(ctx, ["uiautomator", "dump", dumpPath], {
|
|
481
|
+
timeoutMs: 3e4,
|
|
482
|
+
maxBuffer: 4 * 1024 * 1024
|
|
483
|
+
});
|
|
484
|
+
const raw = await adbShell(ctx, ["cat", dumpPath], {
|
|
485
|
+
timeoutMs: 3e4,
|
|
486
|
+
maxBuffer: 16 * 1024 * 1024
|
|
487
|
+
});
|
|
488
|
+
const start = String(raw || "").indexOf("<?xml");
|
|
489
|
+
if (start < 0) throw new CrawlerError({ message: `automation_failed: uiautomator dump missing xml: ${raw}`, code: Code.AutomationFailed });
|
|
490
|
+
return String(raw || "").slice(start).trim();
|
|
491
|
+
} catch (error) {
|
|
492
|
+
lastError = error;
|
|
493
|
+
Logger.warn("uiautomator dump retry", { attempt, message: error?.message || String(error) });
|
|
494
|
+
await sleep(350 * attempt);
|
|
495
|
+
} finally {
|
|
496
|
+
await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
|
|
497
|
+
});
|
|
498
|
+
}
|
|
450
499
|
}
|
|
500
|
+
throw CrawlerError.isCrawlerError(lastError) ? lastError : new CrawlerError({
|
|
501
|
+
message: `automation_failed: uiautomator dump failed ${lastError?.message || String(lastError || "")}`.trim(),
|
|
502
|
+
code: Code.AutomationFailed,
|
|
503
|
+
context: { lastError: lastError?.message || String(lastError || "") }
|
|
504
|
+
});
|
|
451
505
|
}
|
|
452
506
|
async function wakeAndUnlock(ctx) {
|
|
453
507
|
await pressKey(ctx, "KEYCODE_WAKEUP").catch(() => {
|
|
@@ -485,11 +539,51 @@ function requireValue(value, name) {
|
|
|
485
539
|
}
|
|
486
540
|
function isAdbUnavailableError(error) {
|
|
487
541
|
const message = String(error?.message || error || "");
|
|
488
|
-
return /ENOENT|spawn .*adb|
|
|
542
|
+
return /ENOENT|spawn .*adb|device .* not found|no devices|offline/i.test(message);
|
|
489
543
|
}
|
|
490
544
|
function compactArgs(args) {
|
|
491
545
|
return args.map(String).join(" ").slice(0, 500);
|
|
492
546
|
}
|
|
547
|
+
function encodeUnicodeImeText(text2) {
|
|
548
|
+
let out = "";
|
|
549
|
+
let buffer = "";
|
|
550
|
+
const flush = () => {
|
|
551
|
+
if (!buffer) return;
|
|
552
|
+
const bytes = Buffer.alloc(buffer.length * 2);
|
|
553
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
554
|
+
bytes.writeUInt16BE(buffer.charCodeAt(index), index * 2);
|
|
555
|
+
}
|
|
556
|
+
out += `&${bytes.toString("base64").replace(/\//g, ",").replace(/=+$/g, "")}-`;
|
|
557
|
+
buffer = "";
|
|
558
|
+
};
|
|
559
|
+
for (const char of String(text2 || "")) {
|
|
560
|
+
const code = char.charCodeAt(0);
|
|
561
|
+
if (code >= 32 && code <= 126) {
|
|
562
|
+
flush();
|
|
563
|
+
out += char === "&" ? "&-" : char;
|
|
564
|
+
} else {
|
|
565
|
+
buffer += char;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
flush();
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
function shellSingleQuote(value) {
|
|
572
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
573
|
+
}
|
|
574
|
+
function normalizeAppiumClipboardBroadcast(value) {
|
|
575
|
+
const out = String(value || "");
|
|
576
|
+
if (!/Broadcast completed:\s*result=-1\b/.test(out)) return "";
|
|
577
|
+
const match = /data="([^"]*)"/.exec(out) || /data=([^\s]+)/.exec(out);
|
|
578
|
+
const base64 = match ? String(match[1] || "").trim() : "";
|
|
579
|
+
if (!base64) return "";
|
|
580
|
+
try {
|
|
581
|
+
return Buffer.from(base64, "base64").toString("utf8").trim();
|
|
582
|
+
} catch {
|
|
583
|
+
return "";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
var $DeviceInternalsForTest = Object.freeze({});
|
|
493
587
|
|
|
494
588
|
// src/apify-kit.js
|
|
495
589
|
var instance = null;
|
|
@@ -682,16 +776,19 @@ async function waitFor(ctx, selector, options = {}) {
|
|
|
682
776
|
const intervalMs = Number(options.intervalMs || options.pollIntervalMs || 500);
|
|
683
777
|
const deadline = Date.now() + timeoutMs;
|
|
684
778
|
let lastError = null;
|
|
779
|
+
let sawCleanPoll = false;
|
|
685
780
|
while (Date.now() < deadline) {
|
|
686
781
|
try {
|
|
687
782
|
const node = await find(ctx, selector, { ...options, optional: true });
|
|
783
|
+
sawCleanPoll = true;
|
|
784
|
+
lastError = null;
|
|
688
785
|
if (node) return node;
|
|
689
786
|
} catch (error) {
|
|
690
787
|
lastError = error;
|
|
691
788
|
}
|
|
692
789
|
await sleep(intervalMs);
|
|
693
790
|
}
|
|
694
|
-
if (lastError) throw lastError;
|
|
791
|
+
if (lastError && !sawCleanPoll) throw lastError;
|
|
695
792
|
throw new CrawlerError({
|
|
696
793
|
message: `automation_failed: \u7B49\u5F85 View \u8D85\u65F6 ${selectorLabel(selector)}`,
|
|
697
794
|
code: Code.AutomationFailed,
|
|
@@ -878,9 +975,27 @@ async function click(ctx, selectorOrPoint, options = {}) {
|
|
|
878
975
|
return { target, actual, point };
|
|
879
976
|
}
|
|
880
977
|
async function fill(ctx, selector, text2, options = {}) {
|
|
881
|
-
|
|
882
|
-
await
|
|
978
|
+
const value = String(text2 ?? "");
|
|
979
|
+
const clicked = await click(ctx, selector, {
|
|
980
|
+
...options,
|
|
981
|
+
settleMs: Number(options.focusSettleMs || 350)
|
|
982
|
+
});
|
|
983
|
+
try {
|
|
984
|
+
await Device.typeText(ctx, value);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
throw new CrawlerError({
|
|
987
|
+
message: `automation_failed: View \u5199\u5165\u6587\u672C\u5931\u8D25 ${JSON.stringify(selector)}`,
|
|
988
|
+
code: Code.AutomationFailed,
|
|
989
|
+
context: { selector, error: error?.message || String(error) }
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
Logger.info("DeviceInput.fill", {
|
|
993
|
+
selector,
|
|
994
|
+
target: simplifyNode(clicked.actual || clicked.target),
|
|
995
|
+
chars: value.length
|
|
996
|
+
});
|
|
883
997
|
await sleep(Number(options.settleMs || 350));
|
|
998
|
+
return { target: clicked.actual || clicked.target, chars: value.length };
|
|
884
999
|
}
|
|
885
1000
|
async function press(ctx, key) {
|
|
886
1001
|
await Device.pressKey(ctx, key);
|
|
@@ -968,160 +1083,18 @@ function simplifyNode(node) {
|
|
|
968
1083
|
};
|
|
969
1084
|
}
|
|
970
1085
|
|
|
971
|
-
// src/
|
|
972
|
-
import {
|
|
973
|
-
import {
|
|
1086
|
+
// src/device-sqlite.js
|
|
1087
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1088
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
974
1089
|
import { tmpdir } from "node:os";
|
|
975
1090
|
import path from "node:path";
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const pid = await resolvePid(ctx, options.packageName || ctx.packageName);
|
|
981
|
-
const scriptPath = writeTempScript(source, label);
|
|
982
|
-
const args = [
|
|
983
|
-
"-q",
|
|
984
|
-
"-t",
|
|
985
|
-
String(Number(options.fridaTimeoutSeconds || 8)),
|
|
986
|
-
"-D",
|
|
987
|
-
ctx.serial,
|
|
988
|
-
"-p",
|
|
989
|
-
pid,
|
|
990
|
-
"-l",
|
|
991
|
-
scriptPath
|
|
992
|
-
];
|
|
993
|
-
Logger.info("frida script start", { label, pid, timeoutMs: Number(options.timeoutMs || 12e3) });
|
|
994
|
-
try {
|
|
995
|
-
return await runFridaProcess(ctx.fridaPath, args, {
|
|
996
|
-
marker,
|
|
997
|
-
timeoutMs: Number(options.timeoutMs || 12e3),
|
|
998
|
-
maxLines: Number(options.maxLines || 1500)
|
|
999
|
-
});
|
|
1000
|
-
} finally {
|
|
1001
|
-
rmSync(path.dirname(scriptPath), { recursive: true, force: true });
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
async function resolveFridaPid(ctx, packageName = ctx.packageName) {
|
|
1005
|
-
return resolvePid(ctx, packageName);
|
|
1006
|
-
}
|
|
1007
|
-
async function assertFridaReady(ctx) {
|
|
1008
|
-
if (!ctx?.fridaPath) {
|
|
1009
|
-
throw new CrawlerError({
|
|
1010
|
-
message: "frida_unavailable: ANDROID_TOOLKIT_FRIDA_PATH is required",
|
|
1011
|
-
code: Code.FridaUnavailable
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
if (!ctx?.serial) {
|
|
1015
|
-
throw new CrawlerError({
|
|
1016
|
-
message: "frida_unavailable: device serial is required",
|
|
1017
|
-
code: Code.FridaUnavailable
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
async function resolvePid(ctx, packageName = ctx.packageName) {
|
|
1022
|
-
const target = String(packageName || "").trim();
|
|
1023
|
-
if (!target) {
|
|
1024
|
-
throw new CrawlerError({
|
|
1025
|
-
message: "frida_unavailable: packageName is required",
|
|
1026
|
-
code: Code.FridaUnavailable
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
const out = await Device.adbShell(ctx, ["pidof", target], { timeoutMs: 8e3 }).catch(() => "");
|
|
1030
|
-
const pid = String(out || "").trim().split(/\s+/).find(Boolean);
|
|
1031
|
-
if (!pid) {
|
|
1032
|
-
throw new CrawlerError({
|
|
1033
|
-
message: `frida_unavailable: target app pid not found ${target}`,
|
|
1034
|
-
code: Code.FridaUnavailable,
|
|
1035
|
-
context: { packageName: target }
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
return pid;
|
|
1039
|
-
}
|
|
1040
|
-
function runFridaProcess(fridaPath, args, options) {
|
|
1041
|
-
return new Promise((resolve, reject) => {
|
|
1042
|
-
const events = [];
|
|
1043
|
-
const lines = [];
|
|
1044
|
-
let buffer = "";
|
|
1045
|
-
let finished = false;
|
|
1046
|
-
const child = spawn(fridaPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1047
|
-
const timer = setTimeout(() => {
|
|
1048
|
-
if (finished) return;
|
|
1049
|
-
finished = true;
|
|
1050
|
-
child.kill("SIGTERM");
|
|
1051
|
-
reject(new CrawlerError({
|
|
1052
|
-
message: `frida_unavailable: frida script timeout ${options.timeoutMs}ms`,
|
|
1053
|
-
code: Code.FridaUnavailable,
|
|
1054
|
-
context: { lines: lines.slice(-40) }
|
|
1055
|
-
}));
|
|
1056
|
-
}, options.timeoutMs);
|
|
1057
|
-
const consumeLine = (line) => {
|
|
1058
|
-
const text2 = String(line || "").trim();
|
|
1059
|
-
if (!text2) return;
|
|
1060
|
-
lines.push(text2);
|
|
1061
|
-
if (lines.length > options.maxLines) lines.shift();
|
|
1062
|
-
const markerIndex = text2.indexOf(options.marker);
|
|
1063
|
-
if (markerIndex < 0) return;
|
|
1064
|
-
const jsonText = text2.slice(markerIndex + options.marker.length).trim();
|
|
1065
|
-
try {
|
|
1066
|
-
events.push(JSON.parse(jsonText));
|
|
1067
|
-
} catch (error) {
|
|
1068
|
-
events.push({ type: "parse_error", error: error?.message || String(error), line: text2 });
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
const onChunk = (chunk) => {
|
|
1072
|
-
buffer += chunk.toString("utf8");
|
|
1073
|
-
let index = buffer.indexOf("\n");
|
|
1074
|
-
while (index >= 0) {
|
|
1075
|
-
consumeLine(buffer.slice(0, index));
|
|
1076
|
-
buffer = buffer.slice(index + 1);
|
|
1077
|
-
index = buffer.indexOf("\n");
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1080
|
-
child.stdout.on("data", onChunk);
|
|
1081
|
-
child.stderr.on("data", onChunk);
|
|
1082
|
-
child.on("error", (error) => {
|
|
1083
|
-
if (finished) return;
|
|
1084
|
-
finished = true;
|
|
1085
|
-
clearTimeout(timer);
|
|
1086
|
-
reject(new CrawlerError({
|
|
1087
|
-
message: `frida_unavailable: ${error?.message || String(error)}`,
|
|
1088
|
-
code: Code.FridaUnavailable
|
|
1089
|
-
}));
|
|
1090
|
-
});
|
|
1091
|
-
child.on("close", () => {
|
|
1092
|
-
if (finished) return;
|
|
1093
|
-
finished = true;
|
|
1094
|
-
clearTimeout(timer);
|
|
1095
|
-
if (buffer.trim()) consumeLine(buffer);
|
|
1096
|
-
const parsed = events.filter((event) => event?.type !== "parse_error");
|
|
1097
|
-
if (parsed.length === 0) {
|
|
1098
|
-
reject(new CrawlerError({
|
|
1099
|
-
message: "frida_unavailable: script emitted no event",
|
|
1100
|
-
code: Code.FridaUnavailable,
|
|
1101
|
-
context: { lines: lines.slice(-40), events }
|
|
1102
|
-
}));
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
resolve(parsed.at(-1));
|
|
1106
|
-
});
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
function writeTempScript(source, label) {
|
|
1110
|
-
const dir = mkdtempSync(path.join(tmpdir(), `android-toolkit-${safeName(label)}-`));
|
|
1111
|
-
const scriptPath = path.join(dir, "script.js");
|
|
1112
|
-
writeFileSync(scriptPath, String(source || ""), "utf8");
|
|
1113
|
-
return scriptPath;
|
|
1114
|
-
}
|
|
1115
|
-
function safeName(value) {
|
|
1116
|
-
return String(value || "script").replace(/[^a-z0-9_-]+/gi, "-").slice(0, 80);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// src/frida-client.js
|
|
1120
|
-
var Frida = {
|
|
1121
|
-
querySQLite,
|
|
1091
|
+
import { promisify as promisify2 } from "node:util";
|
|
1092
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1093
|
+
var DeviceSQLite = {
|
|
1094
|
+
query,
|
|
1122
1095
|
health
|
|
1123
1096
|
};
|
|
1124
|
-
async function
|
|
1097
|
+
async function query(ctx, options = {}) {
|
|
1125
1098
|
if (!options.sql) {
|
|
1126
1099
|
throw new CrawlerError({
|
|
1127
1100
|
message: "invalid_request: sql is required",
|
|
@@ -1136,115 +1109,183 @@ async function querySQLite(ctx, options = {}) {
|
|
|
1136
1109
|
dbNameExcludes: normalizeStringArray(options.dbNameExcludes),
|
|
1137
1110
|
sql: String(options.sql || ""),
|
|
1138
1111
|
args: normalizeStringArray(options.args),
|
|
1139
|
-
maxRows: Math.max(1, Number(options.maxRows || 200))
|
|
1112
|
+
maxRows: Math.max(1, Number(options.maxRows || 200)),
|
|
1113
|
+
label: String(options.label || "query-sqlite")
|
|
1140
1114
|
};
|
|
1141
|
-
|
|
1142
|
-
label: options.label || "query-sqlite",
|
|
1143
|
-
packageName: options.packageName || ctx.packageName,
|
|
1144
|
-
timeoutMs: options.timeoutMs || 3e4,
|
|
1145
|
-
fridaTimeoutSeconds: options.fridaTimeoutSeconds || 25,
|
|
1146
|
-
maxLines: options.maxLines || 4e3
|
|
1147
|
-
});
|
|
1148
|
-
if (!event.ok) {
|
|
1115
|
+
if (!config.dbPath && !config.dbDir) {
|
|
1149
1116
|
throw new CrawlerError({
|
|
1150
|
-
message:
|
|
1151
|
-
code: Code.
|
|
1152
|
-
|
|
1117
|
+
message: "invalid_request: dbPath or dbDir is required",
|
|
1118
|
+
code: Code.InvalidRequest
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
const startedAt = Date.now();
|
|
1122
|
+
const dir = await mkdtemp(path.join(tmpdir(), "android-toolkit-sqlite-"));
|
|
1123
|
+
try {
|
|
1124
|
+
const dbPaths = await resolveDeviceDbPaths(ctx, config);
|
|
1125
|
+
if (dbPaths.length === 0) {
|
|
1126
|
+
throw new CrawlerError({
|
|
1127
|
+
message: "data_access_unavailable: sqlite database not found",
|
|
1128
|
+
code: Code.DataAccessUnavailable,
|
|
1129
|
+
context: { dbDir: config.dbDir, dbPath: config.dbPath, dbNamePrefix: config.dbNamePrefix }
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
const databases = [];
|
|
1133
|
+
for (const dbPath of dbPaths) {
|
|
1134
|
+
const localDbPath = await pullDatabaseSnapshot(ctx, dbPath, dir);
|
|
1135
|
+
const result = await queryLocalSQLite(localDbPath, config);
|
|
1136
|
+
databases.push({
|
|
1137
|
+
dbPath,
|
|
1138
|
+
columns: result.columns,
|
|
1139
|
+
rows: result.rows,
|
|
1140
|
+
rowCount: result.rows.length,
|
|
1141
|
+
truncated: result.truncated
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
Logger.info("DeviceSQLite.query", {
|
|
1145
|
+
label: config.label,
|
|
1146
|
+
databaseCount: databases.length,
|
|
1147
|
+
rowCount: databases.reduce((total, item) => total + item.rowCount, 0),
|
|
1148
|
+
duration: Logger.duration(startedAt)
|
|
1149
|
+
});
|
|
1150
|
+
return { ok: true, databases, rows: databases.length === 1 ? databases[0].rows : [] };
|
|
1151
|
+
} finally {
|
|
1152
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1153
1153
|
});
|
|
1154
1154
|
}
|
|
1155
|
-
return event;
|
|
1156
1155
|
}
|
|
1157
|
-
async function health(ctx) {
|
|
1158
|
-
|
|
1156
|
+
async function health(ctx, options = {}) {
|
|
1157
|
+
const packageName = String(options.packageName || ctx?.packageName || "").trim();
|
|
1158
|
+
if (!packageName) {
|
|
1159
1159
|
throw new CrawlerError({
|
|
1160
|
-
message: "
|
|
1161
|
-
code: Code.
|
|
1160
|
+
message: "invalid_request: packageName is required",
|
|
1161
|
+
code: Code.InvalidRequest
|
|
1162
1162
|
});
|
|
1163
1163
|
}
|
|
1164
|
-
|
|
1164
|
+
await adbSuShell(ctx, `test -d ${shellQuote(`/data/data/${packageName}`)}`, {
|
|
1165
|
+
timeoutMs: Number(options.timeoutMs || 1e4)
|
|
1166
|
+
});
|
|
1167
|
+
return { ok: true, packageName };
|
|
1168
|
+
}
|
|
1169
|
+
async function resolveDeviceDbPaths(ctx, config) {
|
|
1170
|
+
if (config.dbPath) return [config.dbPath];
|
|
1171
|
+
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`;
|
|
1172
|
+
const out = await adbSuShell(ctx, command, {
|
|
1173
|
+
timeoutMs: 15e3,
|
|
1174
|
+
maxBuffer: 2 * 1024 * 1024
|
|
1175
|
+
}).catch((error) => {
|
|
1165
1176
|
throw new CrawlerError({
|
|
1166
|
-
message:
|
|
1167
|
-
code: Code.
|
|
1177
|
+
message: `data_access_unavailable: sqlite dbDir unavailable ${error?.message || String(error)}`,
|
|
1178
|
+
code: Code.DataAccessUnavailable,
|
|
1179
|
+
context: { dbDir: config.dbDir }
|
|
1168
1180
|
});
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
}
|
|
1188
|
-
function matchesName(name) {
|
|
1189
|
-
if (config.dbNamePrefix && name.indexOf(config.dbNamePrefix) !== 0) return false;
|
|
1190
|
-
if (config.dbNameIncludes && name.indexOf(config.dbNameIncludes) < 0) return false;
|
|
1191
|
-
for (var i = 0; i < config.dbNameExcludes.length; i++) {
|
|
1192
|
-
if (String(name).indexOf(config.dbNameExcludes[i]) >= 0) return false;
|
|
1193
|
-
}
|
|
1194
|
-
return true;
|
|
1195
|
-
}
|
|
1196
|
-
function dbPaths() {
|
|
1197
|
-
if (config.dbPath) return [config.dbPath];
|
|
1198
|
-
var File = Java.use('java.io.File');
|
|
1199
|
-
var dir = File.$new(config.dbDir);
|
|
1200
|
-
var files = dir.listFiles();
|
|
1201
|
-
var out = [];
|
|
1202
|
-
if (!files) return out;
|
|
1203
|
-
for (var i = 0; i < files.length; i++) {
|
|
1204
|
-
var name = String(files[i].getName());
|
|
1205
|
-
if (matchesName(name)) out.push(String(files[i].getAbsolutePath()));
|
|
1206
|
-
}
|
|
1207
|
-
return out;
|
|
1208
|
-
}
|
|
1209
|
-
function queryOne(dbPath) {
|
|
1210
|
-
var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase');
|
|
1211
|
-
var db = SQLiteDatabase.openDatabase(JavaString.$new(dbPath), null, 1);
|
|
1212
|
-
try {
|
|
1213
|
-
var cursor = db.rawQuery(JavaString.$new(config.sql), stringArray(config.args));
|
|
1214
|
-
var columns = [];
|
|
1215
|
-
var columnCount = cursor.getColumnCount();
|
|
1216
|
-
for (var c = 0; c < columnCount; c++) columns.push(String(cursor.getColumnName(c)));
|
|
1217
|
-
var rows = [];
|
|
1218
|
-
var truncated = false;
|
|
1219
|
-
while (cursor.moveToNext()) {
|
|
1220
|
-
if (rows.length >= config.maxRows) { truncated = true; break; }
|
|
1221
|
-
var row = {};
|
|
1222
|
-
for (var i = 0; i < columns.length; i++) row[columns[i]] = safeString(cursor, i);
|
|
1223
|
-
rows.push(row);
|
|
1224
|
-
}
|
|
1225
|
-
cursor.close();
|
|
1226
|
-
return { dbPath: dbPath, columns: columns, rows: rows, rowCount: rows.length, truncated: truncated };
|
|
1227
|
-
} finally {
|
|
1228
|
-
db.close();
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1181
|
+
});
|
|
1182
|
+
return String(out || "").split("\n").map((line) => line.trim()).filter(Boolean).filter((name) => matchesName(name, config)).map((name) => `${config.dbDir.replace(/\/+$/, "")}/${name}`).sort();
|
|
1183
|
+
}
|
|
1184
|
+
async function pullDatabaseSnapshot(ctx, dbPath, localDir) {
|
|
1185
|
+
const serial = requireNonEmpty2(ctx?.serial, "serial");
|
|
1186
|
+
const adbPath = requireNonEmpty2(ctx?.adbPath, "adbPath");
|
|
1187
|
+
const baseName = path.basename(dbPath);
|
|
1188
|
+
const remoteDir = `/data/local/tmp/android-toolkit-sqlite-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1189
|
+
const remoteDb = `${remoteDir}/${baseName}`;
|
|
1190
|
+
const localDb = path.join(localDir, baseName);
|
|
1191
|
+
const copyCommand = [
|
|
1192
|
+
`rm -rf ${shellQuote(remoteDir)}`,
|
|
1193
|
+
`mkdir -p ${shellQuote(remoteDir)}`,
|
|
1194
|
+
`cp ${shellQuote(dbPath)} ${shellQuote(remoteDb)}`,
|
|
1195
|
+
`[ -f ${shellQuote(`${dbPath}-wal`)} ] && cp ${shellQuote(`${dbPath}-wal`)} ${shellQuote(`${remoteDb}-wal`)} || true`,
|
|
1196
|
+
`[ -f ${shellQuote(`${dbPath}-shm`)} ] && cp ${shellQuote(`${dbPath}-shm`)} ${shellQuote(`${remoteDb}-shm`)} || true`,
|
|
1197
|
+
`chmod 644 ${shellQuote(remoteDir)}/*`
|
|
1198
|
+
].join(" && ");
|
|
1231
1199
|
try {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1200
|
+
await adbSuShell(ctx, copyCommand, { timeoutMs: 3e4, maxBuffer: 4 * 1024 * 1024 });
|
|
1201
|
+
await adbPull(adbPath, serial, remoteDb, localDb).catch((error) => {
|
|
1202
|
+
throw new CrawlerError({
|
|
1203
|
+
message: `data_access_unavailable: sqlite snapshot pull failed ${error?.message || String(error)}`,
|
|
1204
|
+
code: Code.DataAccessUnavailable,
|
|
1205
|
+
context: { dbPath }
|
|
1206
|
+
});
|
|
1207
|
+
});
|
|
1208
|
+
await adbPullOptional(adbPath, serial, `${remoteDb}-wal`, `${localDb}-wal`);
|
|
1209
|
+
await adbPullOptional(adbPath, serial, `${remoteDb}-shm`, `${localDb}-shm`);
|
|
1210
|
+
return localDb;
|
|
1211
|
+
} finally {
|
|
1212
|
+
await adbSuShell(ctx, `rm -rf ${shellQuote(remoteDir)}`, { timeoutMs: 1e4 }).catch(() => {
|
|
1213
|
+
});
|
|
1239
1214
|
}
|
|
1240
|
-
}
|
|
1215
|
+
}
|
|
1216
|
+
async function adbSuShell(ctx, command, options = {}) {
|
|
1217
|
+
return Device.adbShell(ctx, [`su -c ${shellQuote(command)}`], options);
|
|
1218
|
+
}
|
|
1219
|
+
async function queryLocalSQLite(dbPath, config) {
|
|
1220
|
+
const payloadPath = `${dbPath}.query.json`;
|
|
1221
|
+
const outputPath = `${dbPath}.rows.json`;
|
|
1222
|
+
await writeFile(payloadPath, JSON.stringify({
|
|
1223
|
+
dbPath,
|
|
1224
|
+
sql: config.sql,
|
|
1225
|
+
args: config.args,
|
|
1226
|
+
maxRows: config.maxRows,
|
|
1227
|
+
outputPath
|
|
1228
|
+
}), "utf8");
|
|
1229
|
+
const code = `
|
|
1230
|
+
const fs = require('node:fs');
|
|
1231
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
1232
|
+
const payload = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
1233
|
+
const db = new DatabaseSync(payload.dbPath, { open: true, readOnly: true });
|
|
1234
|
+
try {
|
|
1235
|
+
const stmt = db.prepare(payload.sql);
|
|
1236
|
+
const columns = stmt.columns().map((column) => String(column.name || ''));
|
|
1237
|
+
const rows = [];
|
|
1238
|
+
let truncated = false;
|
|
1239
|
+
for (const row of stmt.iterate(...payload.args)) {
|
|
1240
|
+
if (rows.length >= payload.maxRows) { truncated = true; break; }
|
|
1241
|
+
const out = {};
|
|
1242
|
+
for (const column of columns) out[column] = row[column] == null ? '' : String(row[column]);
|
|
1243
|
+
rows.push(out);
|
|
1244
|
+
}
|
|
1245
|
+
fs.writeFileSync(payload.outputPath, JSON.stringify({ columns, rows, truncated }), 'utf8');
|
|
1246
|
+
} finally {
|
|
1247
|
+
db.close();
|
|
1248
|
+
}
|
|
1241
1249
|
`;
|
|
1250
|
+
await execFileAsync2(process.execPath, ["-e", code, payloadPath], {
|
|
1251
|
+
timeout: 3e4,
|
|
1252
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1253
|
+
encoding: "utf8"
|
|
1254
|
+
});
|
|
1255
|
+
return JSON.parse(await import("node:fs/promises").then((fs3) => fs3.readFile(outputPath, "utf8")));
|
|
1256
|
+
}
|
|
1257
|
+
async function adbPull(adbPath, serial, remotePath, localPath) {
|
|
1258
|
+
await execFileAsync2(adbPath, ["-s", serial, "pull", remotePath, localPath], {
|
|
1259
|
+
timeout: 3e4,
|
|
1260
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
1261
|
+
encoding: "utf8"
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
async function adbPullOptional(adbPath, serial, remotePath, localPath) {
|
|
1265
|
+
await adbPull(adbPath, serial, remotePath, localPath).catch(() => {
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
function matchesName(name, config) {
|
|
1269
|
+
if (config.dbNamePrefix && !name.startsWith(config.dbNamePrefix)) return false;
|
|
1270
|
+
if (config.dbNameIncludes && !name.includes(config.dbNameIncludes)) return false;
|
|
1271
|
+
for (const item of config.dbNameExcludes || []) {
|
|
1272
|
+
if (item && name.includes(item)) return false;
|
|
1273
|
+
}
|
|
1274
|
+
return true;
|
|
1242
1275
|
}
|
|
1243
1276
|
function normalizeStringArray(value) {
|
|
1244
1277
|
if (Array.isArray(value)) return value.map((item) => String(item ?? ""));
|
|
1245
1278
|
if (value == null) return [];
|
|
1246
1279
|
return [String(value)];
|
|
1247
1280
|
}
|
|
1281
|
+
function requireNonEmpty2(value, name) {
|
|
1282
|
+
const clean = String(value ?? "").trim();
|
|
1283
|
+
if (!clean) throw new CrawlerError({ message: `invalid_request: ${name} is required`, code: Code.InvalidRequest });
|
|
1284
|
+
return clean;
|
|
1285
|
+
}
|
|
1286
|
+
function shellQuote(value) {
|
|
1287
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1288
|
+
}
|
|
1248
1289
|
|
|
1249
1290
|
// src/launch.js
|
|
1250
1291
|
import fs2 from "node:fs";
|
|
@@ -1282,8 +1323,24 @@ async function waitForStable(ctx, selectors, options = {}) {
|
|
|
1282
1323
|
let stableSince = 0;
|
|
1283
1324
|
let mutationCount = 0;
|
|
1284
1325
|
let wasPaused = false;
|
|
1326
|
+
let snapshotErrorCount = 0;
|
|
1327
|
+
let lastSnapshotError = null;
|
|
1285
1328
|
while (Date.now() < deadline) {
|
|
1286
|
-
const snapshot2 = await captureSnapshot(ctx, selectorList, options)
|
|
1329
|
+
const snapshot2 = await captureSnapshot(ctx, selectorList, options).catch((error) => {
|
|
1330
|
+
snapshotErrorCount += 1;
|
|
1331
|
+
lastSnapshotError = error;
|
|
1332
|
+
if (snapshotErrorCount === 1 || snapshotErrorCount % 5 === 0) {
|
|
1333
|
+
Logger.warn("Mutation.waitForStable snapshot skipped", {
|
|
1334
|
+
snapshotErrorCount,
|
|
1335
|
+
message: error?.message || String(error)
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
});
|
|
1340
|
+
if (!snapshot2) {
|
|
1341
|
+
await sleep(pollIntervalMs);
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1287
1344
|
if (!foundInitial) {
|
|
1288
1345
|
if (snapshot2.found || Date.now() >= initialDeadline) {
|
|
1289
1346
|
foundInitial = true;
|
|
@@ -1331,7 +1388,7 @@ async function waitForStable(ctx, selectors, options = {}) {
|
|
|
1331
1388
|
}
|
|
1332
1389
|
await sleep(pollIntervalMs);
|
|
1333
1390
|
}
|
|
1334
|
-
throw new Error(`Mutation.waitForStable \u8D85\u65F6 (${timeout}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316`);
|
|
1391
|
+
throw new Error(`Mutation.waitForStable \u8D85\u65F6 (${timeout}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316, snapshotErrorCount=${snapshotErrorCount}, lastSnapshotError=${lastSnapshotError?.message || ""}`);
|
|
1335
1392
|
}
|
|
1336
1393
|
async function waitForStableAcrossRoots(ctx, selectors, options = {}) {
|
|
1337
1394
|
return waitForStable(ctx, selectors, options);
|
|
@@ -1538,12 +1595,8 @@ var compressImageBuffer = async (buffer, compression) => {
|
|
|
1538
1595
|
let smallest = null;
|
|
1539
1596
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1540
1597
|
const candidate = await encodeJpeg(sourceImage, compression, scale, quality);
|
|
1541
|
-
if (!smallest || candidate.bytes < smallest.bytes)
|
|
1542
|
-
|
|
1543
|
-
}
|
|
1544
|
-
if (candidate.bytes <= compression.maxBytes) {
|
|
1545
|
-
return { ...candidate, withinLimit: true };
|
|
1546
|
-
}
|
|
1598
|
+
if (!smallest || candidate.bytes < smallest.bytes) smallest = candidate;
|
|
1599
|
+
if (candidate.bytes <= compression.maxBytes) return { ...candidate, withinLimit: true };
|
|
1547
1600
|
if (quality > minQuality) {
|
|
1548
1601
|
quality = Math.max(minQuality, Math.floor(quality * 0.75));
|
|
1549
1602
|
continue;
|
|
@@ -1553,9 +1606,7 @@ var compressImageBuffer = async (buffer, compression) => {
|
|
|
1553
1606
|
compression.minScale,
|
|
1554
1607
|
Math.min(scale * 0.85, scale * ratio * 0.94)
|
|
1555
1608
|
);
|
|
1556
|
-
if (nextScale >= scale * 0.99 || scale <= compression.minScale)
|
|
1557
|
-
break;
|
|
1558
|
-
}
|
|
1609
|
+
if (nextScale >= scale * 0.99 || scale <= compression.minScale) break;
|
|
1559
1610
|
scale = nextScale;
|
|
1560
1611
|
}
|
|
1561
1612
|
const finalCandidate = await encodeJpeg(sourceImage, compression, compression.minScale, minQuality);
|
|
@@ -1571,9 +1622,7 @@ var compressImageBufferToBase64 = async (buffer, compression) => {
|
|
|
1571
1622
|
Logger.warn("captureScreen \u538B\u7F29\u5931\u8D25\uFF0C\u8FD4\u56DE\u539F\u56FE", { message: error?.message || String(error) });
|
|
1572
1623
|
return null;
|
|
1573
1624
|
});
|
|
1574
|
-
if (!result?.buffer)
|
|
1575
|
-
return buffer.toString("base64");
|
|
1576
|
-
}
|
|
1625
|
+
if (!result?.buffer) return buffer.toString("base64");
|
|
1577
1626
|
if (result.withinLimit) {
|
|
1578
1627
|
Logger.info("captureScreen \u5DF2\u538B\u7F29", {
|
|
1579
1628
|
originalBytes,
|
|
@@ -1661,21 +1710,24 @@ async function captureLink(ctx, options = {}) {
|
|
|
1661
1710
|
const deadline = Date.now() + timeoutMs;
|
|
1662
1711
|
const prefix = String(share.prefix || "").trim();
|
|
1663
1712
|
Logger.start("Share.captureLink", { actor: actorInfo.key, prefix, timeoutMs });
|
|
1664
|
-
await
|
|
1665
|
-
|
|
1666
|
-
});
|
|
1713
|
+
const beforeEvent = await readClipboard2(ctx).catch(() => null);
|
|
1714
|
+
const beforeLink = beforeEvent ? selectLink(beforeEvent, prefix) : "";
|
|
1715
|
+
if (beforeLink) Logger.info("Share.captureLink baseline", { beforeLink });
|
|
1667
1716
|
if (typeof options.performActions === "function") {
|
|
1668
1717
|
await options.performActions();
|
|
1669
1718
|
}
|
|
1670
1719
|
let lastEvent = null;
|
|
1671
1720
|
while (Date.now() < deadline) {
|
|
1672
|
-
const event = await
|
|
1721
|
+
const event = await readClipboard2(ctx).catch((error) => ({ ok: false, error: error?.message || String(error) }));
|
|
1673
1722
|
lastEvent = event;
|
|
1674
1723
|
const link = selectLink(event, prefix);
|
|
1675
|
-
if (link) {
|
|
1724
|
+
if (link && link !== beforeLink) {
|
|
1676
1725
|
Logger.success("Share.captureLink", { link });
|
|
1677
1726
|
return { link, source: event.source || "clipboard", payloadSnapshot: event.payloadSnapshot || "" };
|
|
1678
1727
|
}
|
|
1728
|
+
if (link && link === beforeLink) {
|
|
1729
|
+
lastEvent = { ...event, rejected: "same_as_baseline", beforeLink };
|
|
1730
|
+
}
|
|
1679
1731
|
await sleep(pollIntervalMs);
|
|
1680
1732
|
}
|
|
1681
1733
|
throw new CrawlerError({
|
|
@@ -1684,77 +1736,20 @@ async function captureLink(ctx, options = {}) {
|
|
|
1684
1736
|
context: { prefix, lastEvent }
|
|
1685
1737
|
});
|
|
1686
1738
|
}
|
|
1687
|
-
async function
|
|
1688
|
-
await
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
function clipboardScript(mode) {
|
|
1703
|
-
return `
|
|
1704
|
-
Java.perform(function () {
|
|
1705
|
-
function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
|
|
1706
|
-
var mode = ${JSON.stringify(mode)};
|
|
1707
|
-
var JavaObject = Java.use('java.lang.Object');
|
|
1708
|
-
function text(value) {
|
|
1709
|
-
if (value === null || value === undefined) return '';
|
|
1710
|
-
try { return Java.cast(value, JavaObject).toString() + ''; } catch (_) {}
|
|
1711
|
-
try { return value.toString.overload().call(value) + ''; } catch (_) {}
|
|
1712
|
-
try { return value.toString() + ''; } catch (_) {}
|
|
1713
|
-
return '';
|
|
1714
|
-
}
|
|
1715
|
-
function addCandidate(out, source, value) {
|
|
1716
|
-
var s = text(value);
|
|
1717
|
-
if (!s) return;
|
|
1718
|
-
var matches = s.match(/https?:\\/\\/[^\\s"'<>\uFF0C\u3002]+/g) || [];
|
|
1719
|
-
for (var i = 0; i < matches.length; i++) {
|
|
1720
|
-
var link = matches[i].replace(/[)\\].,\uFF0C\u3002\uFF1B;!?\uFF01\uFF1F]+$/g, '');
|
|
1721
|
-
out.push({ source: source, link: link, payload: s.slice(0, 1000) });
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
try {
|
|
1725
|
-
var ActivityThread = Java.use('android.app.ActivityThread');
|
|
1726
|
-
var ClipData = Java.use('android.content.ClipData');
|
|
1727
|
-
var app = ActivityThread.currentApplication();
|
|
1728
|
-
var manager = app ? app.getSystemService('clipboard') : null;
|
|
1729
|
-
if (!manager) { emit({ ok: false, error: 'clipboard manager unavailable' }); return; }
|
|
1730
|
-
if (mode === 'clear') {
|
|
1731
|
-
manager.setPrimaryClip(ClipData.newPlainText('android-toolkit', ''));
|
|
1732
|
-
emit({ ok: true, source: 'clipboard.clear' });
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
var candidates = [];
|
|
1736
|
-
var clip = manager.getPrimaryClip();
|
|
1737
|
-
var count = clip ? clip.getItemCount() : 0;
|
|
1738
|
-
for (var i = 0; i < count; i++) {
|
|
1739
|
-
var item = clip.getItemAt(i);
|
|
1740
|
-
try { addCandidate(candidates, 'clipboard.text', item.getText()); } catch (_) {}
|
|
1741
|
-
try { addCandidate(candidates, 'clipboard.html', item.getHtmlText()); } catch (_) {}
|
|
1742
|
-
try { addCandidate(candidates, 'clipboard.uri', item.getUri()); } catch (_) {}
|
|
1743
|
-
try { addCandidate(candidates, 'clipboard.intent', item.getIntent()); } catch (_) {}
|
|
1744
|
-
try { addCandidate(candidates, 'clipboard.coerceToText', item.coerceToText(app)); } catch (_) {}
|
|
1745
|
-
}
|
|
1746
|
-
emit({
|
|
1747
|
-
ok: candidates.length > 0,
|
|
1748
|
-
link: candidates.length > 0 ? candidates[0].link : '',
|
|
1749
|
-
source: candidates.length > 0 ? candidates[0].source : 'clipboard',
|
|
1750
|
-
candidates: candidates,
|
|
1751
|
-
payloadSnapshot: candidates.length > 0 ? String(candidates[0].payload || '').slice(0, 500) : ''
|
|
1752
|
-
});
|
|
1753
|
-
} catch (error) {
|
|
1754
|
-
emit({ ok: false, error: String(error), stack: String(error.stack || '') });
|
|
1755
|
-
}
|
|
1756
|
-
});
|
|
1757
|
-
`;
|
|
1739
|
+
async function readClipboard2(ctx) {
|
|
1740
|
+
const payload = await Device.readClipboard(ctx);
|
|
1741
|
+
const candidates = extractLinks(payload).map((link) => ({
|
|
1742
|
+
source: "adb.clipboard",
|
|
1743
|
+
link,
|
|
1744
|
+
payload: payload.slice(0, 1e3)
|
|
1745
|
+
}));
|
|
1746
|
+
return {
|
|
1747
|
+
ok: candidates.length > 0,
|
|
1748
|
+
link: candidates[0]?.link || "",
|
|
1749
|
+
source: "adb.clipboard",
|
|
1750
|
+
candidates,
|
|
1751
|
+
payloadSnapshot: payload.slice(0, 500)
|
|
1752
|
+
};
|
|
1758
1753
|
}
|
|
1759
1754
|
async function composeSprite(buffers, options = {}) {
|
|
1760
1755
|
if (!buffers.length) {
|
|
@@ -1799,6 +1794,11 @@ function selectLink(event, prefix) {
|
|
|
1799
1794
|
}
|
|
1800
1795
|
return candidates.find((link) => !prefix || String(link).startsWith(prefix)) || "";
|
|
1801
1796
|
}
|
|
1797
|
+
function extractLinks(value) {
|
|
1798
|
+
const text2 = String(value || "");
|
|
1799
|
+
const matches = text2.match(/https?:\/\/[^\s"'<>,。]+/g) || [];
|
|
1800
|
+
return matches.map((link) => link.replace(/[)\].,,。;;!?!?]+$/g, ""));
|
|
1801
|
+
}
|
|
1802
1802
|
|
|
1803
1803
|
// src/launch.js
|
|
1804
1804
|
var DEFAULT_INPUT_PATH = "/apify_storage/input.json";
|
|
@@ -1827,7 +1827,7 @@ async function run(handler, options = {}) {
|
|
|
1827
1827
|
Device,
|
|
1828
1828
|
DeviceInput,
|
|
1829
1829
|
DeviceView,
|
|
1830
|
-
|
|
1830
|
+
DeviceSQLite,
|
|
1831
1831
|
Share,
|
|
1832
1832
|
Mutation,
|
|
1833
1833
|
Logger,
|
|
@@ -1865,20 +1865,26 @@ function pathOption(value, fallback) {
|
|
|
1865
1865
|
}
|
|
1866
1866
|
|
|
1867
1867
|
// entrys/node.js
|
|
1868
|
-
var
|
|
1869
|
-
|
|
1870
|
-
ApifyKit,
|
|
1871
|
-
DeviceInput,
|
|
1872
|
-
DeviceView,
|
|
1873
|
-
Device,
|
|
1874
|
-
Mutation,
|
|
1875
|
-
Share,
|
|
1876
|
-
Frida,
|
|
1877
|
-
Constants: constants_exports,
|
|
1878
|
-
Errors: errors_exports,
|
|
1879
|
-
Logger,
|
|
1880
|
-
Context
|
|
1868
|
+
var ToolkitMode = Object.freeze({
|
|
1869
|
+
default: "default"
|
|
1881
1870
|
});
|
|
1871
|
+
var useAndroidToolKit = () => {
|
|
1872
|
+
return {
|
|
1873
|
+
Launch,
|
|
1874
|
+
ApifyKit,
|
|
1875
|
+
DeviceInput,
|
|
1876
|
+
DeviceView,
|
|
1877
|
+
DeviceSQLite,
|
|
1878
|
+
Device,
|
|
1879
|
+
Mutation,
|
|
1880
|
+
Share,
|
|
1881
|
+
Constants: constants_exports,
|
|
1882
|
+
Errors: errors_exports,
|
|
1883
|
+
Logger,
|
|
1884
|
+
Context
|
|
1885
|
+
};
|
|
1886
|
+
};
|
|
1887
|
+
useAndroidToolKit.Mode = ToolkitMode;
|
|
1882
1888
|
export {
|
|
1883
1889
|
useAndroidToolKit
|
|
1884
1890
|
};
|