@skrillex1224/android-toolkit 1.0.2 → 1.0.5
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 +361 -359
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +360 -358
- 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,14 +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
|
}
|
|
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
|
+
});
|
|
486
|
+
});
|
|
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) }
|
|
493
|
+
});
|
|
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
|
+
}
|
|
460
502
|
async function dumpUiXml(ctx) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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`;
|
|
506
|
+
await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
|
|
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
|
+
}
|
|
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 || "") }
|
|
464
533
|
});
|
|
465
|
-
const start = String(raw || "").indexOf("<?xml");
|
|
466
|
-
if (start < 0) throw new CrawlerError({ message: `automation_failed: uiautomator dump missing xml: ${raw}`, code: Code.AutomationFailed });
|
|
467
|
-
return String(raw || "").slice(start).trim();
|
|
468
534
|
}
|
|
469
535
|
async function wakeAndUnlock(ctx) {
|
|
470
536
|
await pressKey(ctx, "KEYCODE_WAKEUP").catch(() => {
|
|
@@ -502,11 +568,51 @@ function requireValue(value, name) {
|
|
|
502
568
|
}
|
|
503
569
|
function isAdbUnavailableError(error) {
|
|
504
570
|
const message = String(error?.message || error || "");
|
|
505
|
-
return /ENOENT|spawn .*adb|
|
|
571
|
+
return /ENOENT|spawn .*adb|device .* not found|no devices|offline/i.test(message);
|
|
506
572
|
}
|
|
507
573
|
function compactArgs(args) {
|
|
508
574
|
return args.map(String).join(" ").slice(0, 500);
|
|
509
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({});
|
|
510
616
|
|
|
511
617
|
// src/apify-kit.js
|
|
512
618
|
var instance = null;
|
|
@@ -699,16 +805,19 @@ async function waitFor(ctx, selector, options = {}) {
|
|
|
699
805
|
const intervalMs = Number(options.intervalMs || options.pollIntervalMs || 500);
|
|
700
806
|
const deadline = Date.now() + timeoutMs;
|
|
701
807
|
let lastError = null;
|
|
808
|
+
let sawCleanPoll = false;
|
|
702
809
|
while (Date.now() < deadline) {
|
|
703
810
|
try {
|
|
704
811
|
const node = await find(ctx, selector, { ...options, optional: true });
|
|
812
|
+
sawCleanPoll = true;
|
|
813
|
+
lastError = null;
|
|
705
814
|
if (node) return node;
|
|
706
815
|
} catch (error) {
|
|
707
816
|
lastError = error;
|
|
708
817
|
}
|
|
709
818
|
await sleep(intervalMs);
|
|
710
819
|
}
|
|
711
|
-
if (lastError) throw lastError;
|
|
820
|
+
if (lastError && !sawCleanPoll) throw lastError;
|
|
712
821
|
throw new CrawlerError({
|
|
713
822
|
message: `automation_failed: \u7B49\u5F85 View \u8D85\u65F6 ${selectorLabel(selector)}`,
|
|
714
823
|
code: Code.AutomationFailed,
|
|
@@ -895,9 +1004,27 @@ async function click(ctx, selectorOrPoint, options = {}) {
|
|
|
895
1004
|
return { target, actual, point };
|
|
896
1005
|
}
|
|
897
1006
|
async function fill(ctx, selector, text2, options = {}) {
|
|
898
|
-
|
|
899
|
-
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
|
+
});
|
|
900
1026
|
await sleep(Number(options.settleMs || 350));
|
|
1027
|
+
return { target: clicked.actual || clicked.target, chars: value.length };
|
|
901
1028
|
}
|
|
902
1029
|
async function press(ctx, key) {
|
|
903
1030
|
await Device.pressKey(ctx, key);
|
|
@@ -985,160 +1112,18 @@ function simplifyNode(node) {
|
|
|
985
1112
|
};
|
|
986
1113
|
}
|
|
987
1114
|
|
|
988
|
-
// src/
|
|
1115
|
+
// src/device-sqlite.js
|
|
989
1116
|
var import_node_child_process2 = require("node:child_process");
|
|
990
|
-
var
|
|
1117
|
+
var import_promises = require("node:fs/promises");
|
|
991
1118
|
var import_node_os = require("node:os");
|
|
992
1119
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const pid = await resolvePid(ctx, options.packageName || ctx.packageName);
|
|
998
|
-
const scriptPath = writeTempScript(source, label);
|
|
999
|
-
const args = [
|
|
1000
|
-
"-q",
|
|
1001
|
-
"-t",
|
|
1002
|
-
String(Number(options.fridaTimeoutSeconds || 8)),
|
|
1003
|
-
"-D",
|
|
1004
|
-
ctx.serial,
|
|
1005
|
-
"-p",
|
|
1006
|
-
pid,
|
|
1007
|
-
"-l",
|
|
1008
|
-
scriptPath
|
|
1009
|
-
];
|
|
1010
|
-
Logger.info("frida script start", { label, pid, timeoutMs: Number(options.timeoutMs || 12e3) });
|
|
1011
|
-
try {
|
|
1012
|
-
return await runFridaProcess(ctx.fridaPath, args, {
|
|
1013
|
-
marker,
|
|
1014
|
-
timeoutMs: Number(options.timeoutMs || 12e3),
|
|
1015
|
-
maxLines: Number(options.maxLines || 1500)
|
|
1016
|
-
});
|
|
1017
|
-
} finally {
|
|
1018
|
-
(0, import_node_fs2.rmSync)(import_node_path.default.dirname(scriptPath), { recursive: true, force: true });
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
async function resolveFridaPid(ctx, packageName = ctx.packageName) {
|
|
1022
|
-
return resolvePid(ctx, packageName);
|
|
1023
|
-
}
|
|
1024
|
-
async function assertFridaReady(ctx) {
|
|
1025
|
-
if (!ctx?.fridaPath) {
|
|
1026
|
-
throw new CrawlerError({
|
|
1027
|
-
message: "frida_unavailable: ANDROID_TOOLKIT_FRIDA_PATH is required",
|
|
1028
|
-
code: Code.FridaUnavailable
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
if (!ctx?.serial) {
|
|
1032
|
-
throw new CrawlerError({
|
|
1033
|
-
message: "frida_unavailable: device serial is required",
|
|
1034
|
-
code: Code.FridaUnavailable
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
async function resolvePid(ctx, packageName = ctx.packageName) {
|
|
1039
|
-
const target = String(packageName || "").trim();
|
|
1040
|
-
if (!target) {
|
|
1041
|
-
throw new CrawlerError({
|
|
1042
|
-
message: "frida_unavailable: packageName is required",
|
|
1043
|
-
code: Code.FridaUnavailable
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
const out = await Device.adbShell(ctx, ["pidof", target], { timeoutMs: 8e3 }).catch(() => "");
|
|
1047
|
-
const pid = String(out || "").trim().split(/\s+/).find(Boolean);
|
|
1048
|
-
if (!pid) {
|
|
1049
|
-
throw new CrawlerError({
|
|
1050
|
-
message: `frida_unavailable: target app pid not found ${target}`,
|
|
1051
|
-
code: Code.FridaUnavailable,
|
|
1052
|
-
context: { packageName: target }
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
return pid;
|
|
1056
|
-
}
|
|
1057
|
-
function runFridaProcess(fridaPath, args, options) {
|
|
1058
|
-
return new Promise((resolve, reject) => {
|
|
1059
|
-
const events = [];
|
|
1060
|
-
const lines = [];
|
|
1061
|
-
let buffer = "";
|
|
1062
|
-
let finished = false;
|
|
1063
|
-
const child = (0, import_node_child_process2.spawn)(fridaPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1064
|
-
const timer = setTimeout(() => {
|
|
1065
|
-
if (finished) return;
|
|
1066
|
-
finished = true;
|
|
1067
|
-
child.kill("SIGTERM");
|
|
1068
|
-
reject(new CrawlerError({
|
|
1069
|
-
message: `frida_unavailable: frida script timeout ${options.timeoutMs}ms`,
|
|
1070
|
-
code: Code.FridaUnavailable,
|
|
1071
|
-
context: { lines: lines.slice(-40) }
|
|
1072
|
-
}));
|
|
1073
|
-
}, options.timeoutMs);
|
|
1074
|
-
const consumeLine = (line) => {
|
|
1075
|
-
const text2 = String(line || "").trim();
|
|
1076
|
-
if (!text2) return;
|
|
1077
|
-
lines.push(text2);
|
|
1078
|
-
if (lines.length > options.maxLines) lines.shift();
|
|
1079
|
-
const markerIndex = text2.indexOf(options.marker);
|
|
1080
|
-
if (markerIndex < 0) return;
|
|
1081
|
-
const jsonText = text2.slice(markerIndex + options.marker.length).trim();
|
|
1082
|
-
try {
|
|
1083
|
-
events.push(JSON.parse(jsonText));
|
|
1084
|
-
} catch (error) {
|
|
1085
|
-
events.push({ type: "parse_error", error: error?.message || String(error), line: text2 });
|
|
1086
|
-
}
|
|
1087
|
-
};
|
|
1088
|
-
const onChunk = (chunk) => {
|
|
1089
|
-
buffer += chunk.toString("utf8");
|
|
1090
|
-
let index = buffer.indexOf("\n");
|
|
1091
|
-
while (index >= 0) {
|
|
1092
|
-
consumeLine(buffer.slice(0, index));
|
|
1093
|
-
buffer = buffer.slice(index + 1);
|
|
1094
|
-
index = buffer.indexOf("\n");
|
|
1095
|
-
}
|
|
1096
|
-
};
|
|
1097
|
-
child.stdout.on("data", onChunk);
|
|
1098
|
-
child.stderr.on("data", onChunk);
|
|
1099
|
-
child.on("error", (error) => {
|
|
1100
|
-
if (finished) return;
|
|
1101
|
-
finished = true;
|
|
1102
|
-
clearTimeout(timer);
|
|
1103
|
-
reject(new CrawlerError({
|
|
1104
|
-
message: `frida_unavailable: ${error?.message || String(error)}`,
|
|
1105
|
-
code: Code.FridaUnavailable
|
|
1106
|
-
}));
|
|
1107
|
-
});
|
|
1108
|
-
child.on("close", () => {
|
|
1109
|
-
if (finished) return;
|
|
1110
|
-
finished = true;
|
|
1111
|
-
clearTimeout(timer);
|
|
1112
|
-
if (buffer.trim()) consumeLine(buffer);
|
|
1113
|
-
const parsed = events.filter((event) => event?.type !== "parse_error");
|
|
1114
|
-
if (parsed.length === 0) {
|
|
1115
|
-
reject(new CrawlerError({
|
|
1116
|
-
message: "frida_unavailable: script emitted no event",
|
|
1117
|
-
code: Code.FridaUnavailable,
|
|
1118
|
-
context: { lines: lines.slice(-40), events }
|
|
1119
|
-
}));
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
resolve(parsed.at(-1));
|
|
1123
|
-
});
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
function writeTempScript(source, label) {
|
|
1127
|
-
const dir = (0, import_node_fs2.mkdtempSync)(import_node_path.default.join((0, import_node_os.tmpdir)(), `android-toolkit-${safeName(label)}-`));
|
|
1128
|
-
const scriptPath = import_node_path.default.join(dir, "script.js");
|
|
1129
|
-
(0, import_node_fs2.writeFileSync)(scriptPath, String(source || ""), "utf8");
|
|
1130
|
-
return scriptPath;
|
|
1131
|
-
}
|
|
1132
|
-
function safeName(value) {
|
|
1133
|
-
return String(value || "script").replace(/[^a-z0-9_-]+/gi, "-").slice(0, 80);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// src/frida-client.js
|
|
1137
|
-
var Frida = {
|
|
1138
|
-
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,
|
|
1139
1124
|
health
|
|
1140
1125
|
};
|
|
1141
|
-
async function
|
|
1126
|
+
async function query(ctx, options = {}) {
|
|
1142
1127
|
if (!options.sql) {
|
|
1143
1128
|
throw new CrawlerError({
|
|
1144
1129
|
message: "invalid_request: sql is required",
|
|
@@ -1153,118 +1138,186 @@ async function querySQLite(ctx, options = {}) {
|
|
|
1153
1138
|
dbNameExcludes: normalizeStringArray(options.dbNameExcludes),
|
|
1154
1139
|
sql: String(options.sql || ""),
|
|
1155
1140
|
args: normalizeStringArray(options.args),
|
|
1156
|
-
maxRows: Math.max(1, Number(options.maxRows || 200))
|
|
1141
|
+
maxRows: Math.max(1, Number(options.maxRows || 200)),
|
|
1142
|
+
label: String(options.label || "query-sqlite")
|
|
1157
1143
|
};
|
|
1158
|
-
|
|
1159
|
-
label: options.label || "query-sqlite",
|
|
1160
|
-
packageName: options.packageName || ctx.packageName,
|
|
1161
|
-
timeoutMs: options.timeoutMs || 3e4,
|
|
1162
|
-
fridaTimeoutSeconds: options.fridaTimeoutSeconds || 25,
|
|
1163
|
-
maxLines: options.maxLines || 4e3
|
|
1164
|
-
});
|
|
1165
|
-
if (!event.ok) {
|
|
1144
|
+
if (!config.dbPath && !config.dbDir) {
|
|
1166
1145
|
throw new CrawlerError({
|
|
1167
|
-
message:
|
|
1168
|
-
code: Code.
|
|
1169
|
-
|
|
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(() => {
|
|
1170
1182
|
});
|
|
1171
1183
|
}
|
|
1172
|
-
return event;
|
|
1173
1184
|
}
|
|
1174
|
-
async function health(ctx) {
|
|
1175
|
-
|
|
1185
|
+
async function health(ctx, options = {}) {
|
|
1186
|
+
const packageName = String(options.packageName || ctx?.packageName || "").trim();
|
|
1187
|
+
if (!packageName) {
|
|
1176
1188
|
throw new CrawlerError({
|
|
1177
|
-
message: "
|
|
1178
|
-
code: Code.
|
|
1189
|
+
message: "invalid_request: packageName is required",
|
|
1190
|
+
code: Code.InvalidRequest
|
|
1179
1191
|
});
|
|
1180
1192
|
}
|
|
1181
|
-
|
|
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) => {
|
|
1182
1205
|
throw new CrawlerError({
|
|
1183
|
-
message:
|
|
1184
|
-
code: Code.
|
|
1206
|
+
message: `data_access_unavailable: sqlite dbDir unavailable ${error?.message || String(error)}`,
|
|
1207
|
+
code: Code.DataAccessUnavailable,
|
|
1208
|
+
context: { dbDir: config.dbDir }
|
|
1185
1209
|
});
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
function matchesName(name) {
|
|
1206
|
-
if (config.dbNamePrefix && name.indexOf(config.dbNamePrefix) !== 0) return false;
|
|
1207
|
-
if (config.dbNameIncludes && name.indexOf(config.dbNameIncludes) < 0) return false;
|
|
1208
|
-
for (var i = 0; i < config.dbNameExcludes.length; i++) {
|
|
1209
|
-
if (String(name).indexOf(config.dbNameExcludes[i]) >= 0) return false;
|
|
1210
|
-
}
|
|
1211
|
-
return true;
|
|
1212
|
-
}
|
|
1213
|
-
function dbPaths() {
|
|
1214
|
-
if (config.dbPath) return [config.dbPath];
|
|
1215
|
-
var File = Java.use('java.io.File');
|
|
1216
|
-
var dir = File.$new(config.dbDir);
|
|
1217
|
-
var files = dir.listFiles();
|
|
1218
|
-
var out = [];
|
|
1219
|
-
if (!files) return out;
|
|
1220
|
-
for (var i = 0; i < files.length; i++) {
|
|
1221
|
-
var name = String(files[i].getName());
|
|
1222
|
-
if (matchesName(name)) out.push(String(files[i].getAbsolutePath()));
|
|
1223
|
-
}
|
|
1224
|
-
return out;
|
|
1225
|
-
}
|
|
1226
|
-
function queryOne(dbPath) {
|
|
1227
|
-
var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase');
|
|
1228
|
-
var db = SQLiteDatabase.openDatabase(JavaString.$new(dbPath), null, 1);
|
|
1229
|
-
try {
|
|
1230
|
-
var cursor = db.rawQuery(JavaString.$new(config.sql), stringArray(config.args));
|
|
1231
|
-
var columns = [];
|
|
1232
|
-
var columnCount = cursor.getColumnCount();
|
|
1233
|
-
for (var c = 0; c < columnCount; c++) columns.push(String(cursor.getColumnName(c)));
|
|
1234
|
-
var rows = [];
|
|
1235
|
-
var truncated = false;
|
|
1236
|
-
while (cursor.moveToNext()) {
|
|
1237
|
-
if (rows.length >= config.maxRows) { truncated = true; break; }
|
|
1238
|
-
var row = {};
|
|
1239
|
-
for (var i = 0; i < columns.length; i++) row[columns[i]] = safeString(cursor, i);
|
|
1240
|
-
rows.push(row);
|
|
1241
|
-
}
|
|
1242
|
-
cursor.close();
|
|
1243
|
-
return { dbPath: dbPath, columns: columns, rows: rows, rowCount: rows.length, truncated: truncated };
|
|
1244
|
-
} finally {
|
|
1245
|
-
db.close();
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
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(" && ");
|
|
1248
1228
|
try {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
+
});
|
|
1256
1243
|
}
|
|
1257
|
-
}
|
|
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
|
+
}
|
|
1258
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;
|
|
1259
1304
|
}
|
|
1260
1305
|
function normalizeStringArray(value) {
|
|
1261
1306
|
if (Array.isArray(value)) return value.map((item) => String(item ?? ""));
|
|
1262
1307
|
if (value == null) return [];
|
|
1263
1308
|
return [String(value)];
|
|
1264
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
|
+
}
|
|
1265
1318
|
|
|
1266
1319
|
// src/launch.js
|
|
1267
|
-
var
|
|
1320
|
+
var import_node_fs2 = __toESM(require("node:fs"), 1);
|
|
1268
1321
|
|
|
1269
1322
|
// src/mutation.js
|
|
1270
1323
|
var MUTATION_MONITOR_MODE = Object.freeze({
|
|
@@ -1555,12 +1608,8 @@ var compressImageBuffer = async (buffer, compression) => {
|
|
|
1555
1608
|
let smallest = null;
|
|
1556
1609
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1557
1610
|
const candidate = await encodeJpeg(sourceImage, compression, scale, quality);
|
|
1558
|
-
if (!smallest || candidate.bytes < smallest.bytes)
|
|
1559
|
-
|
|
1560
|
-
}
|
|
1561
|
-
if (candidate.bytes <= compression.maxBytes) {
|
|
1562
|
-
return { ...candidate, withinLimit: true };
|
|
1563
|
-
}
|
|
1611
|
+
if (!smallest || candidate.bytes < smallest.bytes) smallest = candidate;
|
|
1612
|
+
if (candidate.bytes <= compression.maxBytes) return { ...candidate, withinLimit: true };
|
|
1564
1613
|
if (quality > minQuality) {
|
|
1565
1614
|
quality = Math.max(minQuality, Math.floor(quality * 0.75));
|
|
1566
1615
|
continue;
|
|
@@ -1570,9 +1619,7 @@ var compressImageBuffer = async (buffer, compression) => {
|
|
|
1570
1619
|
compression.minScale,
|
|
1571
1620
|
Math.min(scale * 0.85, scale * ratio * 0.94)
|
|
1572
1621
|
);
|
|
1573
|
-
if (nextScale >= scale * 0.99 || scale <= compression.minScale)
|
|
1574
|
-
break;
|
|
1575
|
-
}
|
|
1622
|
+
if (nextScale >= scale * 0.99 || scale <= compression.minScale) break;
|
|
1576
1623
|
scale = nextScale;
|
|
1577
1624
|
}
|
|
1578
1625
|
const finalCandidate = await encodeJpeg(sourceImage, compression, compression.minScale, minQuality);
|
|
@@ -1588,9 +1635,7 @@ var compressImageBufferToBase64 = async (buffer, compression) => {
|
|
|
1588
1635
|
Logger.warn("captureScreen \u538B\u7F29\u5931\u8D25\uFF0C\u8FD4\u56DE\u539F\u56FE", { message: error?.message || String(error) });
|
|
1589
1636
|
return null;
|
|
1590
1637
|
});
|
|
1591
|
-
if (!result?.buffer)
|
|
1592
|
-
return buffer.toString("base64");
|
|
1593
|
-
}
|
|
1638
|
+
if (!result?.buffer) return buffer.toString("base64");
|
|
1594
1639
|
if (result.withinLimit) {
|
|
1595
1640
|
Logger.info("captureScreen \u5DF2\u538B\u7F29", {
|
|
1596
1641
|
originalBytes,
|
|
@@ -1678,21 +1723,24 @@ async function captureLink(ctx, options = {}) {
|
|
|
1678
1723
|
const deadline = Date.now() + timeoutMs;
|
|
1679
1724
|
const prefix = String(share.prefix || "").trim();
|
|
1680
1725
|
Logger.start("Share.captureLink", { actor: actorInfo.key, prefix, timeoutMs });
|
|
1681
|
-
await
|
|
1682
|
-
|
|
1683
|
-
});
|
|
1726
|
+
const beforeEvent = await readClipboard2(ctx).catch(() => null);
|
|
1727
|
+
const beforeLink = beforeEvent ? selectLink(beforeEvent, prefix) : "";
|
|
1728
|
+
if (beforeLink) Logger.info("Share.captureLink baseline", { beforeLink });
|
|
1684
1729
|
if (typeof options.performActions === "function") {
|
|
1685
1730
|
await options.performActions();
|
|
1686
1731
|
}
|
|
1687
1732
|
let lastEvent = null;
|
|
1688
1733
|
while (Date.now() < deadline) {
|
|
1689
|
-
const event = await
|
|
1734
|
+
const event = await readClipboard2(ctx).catch((error) => ({ ok: false, error: error?.message || String(error) }));
|
|
1690
1735
|
lastEvent = event;
|
|
1691
1736
|
const link = selectLink(event, prefix);
|
|
1692
|
-
if (link) {
|
|
1737
|
+
if (link && link !== beforeLink) {
|
|
1693
1738
|
Logger.success("Share.captureLink", { link });
|
|
1694
1739
|
return { link, source: event.source || "clipboard", payloadSnapshot: event.payloadSnapshot || "" };
|
|
1695
1740
|
}
|
|
1741
|
+
if (link && link === beforeLink) {
|
|
1742
|
+
lastEvent = { ...event, rejected: "same_as_baseline", beforeLink };
|
|
1743
|
+
}
|
|
1696
1744
|
await sleep(pollIntervalMs);
|
|
1697
1745
|
}
|
|
1698
1746
|
throw new CrawlerError({
|
|
@@ -1701,77 +1749,20 @@ async function captureLink(ctx, options = {}) {
|
|
|
1701
1749
|
context: { prefix, lastEvent }
|
|
1702
1750
|
});
|
|
1703
1751
|
}
|
|
1704
|
-
async function
|
|
1705
|
-
await
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
function clipboardScript(mode) {
|
|
1720
|
-
return `
|
|
1721
|
-
Java.perform(function () {
|
|
1722
|
-
function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
|
|
1723
|
-
var mode = ${JSON.stringify(mode)};
|
|
1724
|
-
var JavaObject = Java.use('java.lang.Object');
|
|
1725
|
-
function text(value) {
|
|
1726
|
-
if (value === null || value === undefined) return '';
|
|
1727
|
-
try { return Java.cast(value, JavaObject).toString() + ''; } catch (_) {}
|
|
1728
|
-
try { return value.toString.overload().call(value) + ''; } catch (_) {}
|
|
1729
|
-
try { return value.toString() + ''; } catch (_) {}
|
|
1730
|
-
return '';
|
|
1731
|
-
}
|
|
1732
|
-
function addCandidate(out, source, value) {
|
|
1733
|
-
var s = text(value);
|
|
1734
|
-
if (!s) return;
|
|
1735
|
-
var matches = s.match(/https?:\\/\\/[^\\s"'<>\uFF0C\u3002]+/g) || [];
|
|
1736
|
-
for (var i = 0; i < matches.length; i++) {
|
|
1737
|
-
var link = matches[i].replace(/[)\\].,\uFF0C\u3002\uFF1B;!?\uFF01\uFF1F]+$/g, '');
|
|
1738
|
-
out.push({ source: source, link: link, payload: s.slice(0, 1000) });
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
try {
|
|
1742
|
-
var ActivityThread = Java.use('android.app.ActivityThread');
|
|
1743
|
-
var ClipData = Java.use('android.content.ClipData');
|
|
1744
|
-
var app = ActivityThread.currentApplication();
|
|
1745
|
-
var manager = app ? app.getSystemService('clipboard') : null;
|
|
1746
|
-
if (!manager) { emit({ ok: false, error: 'clipboard manager unavailable' }); return; }
|
|
1747
|
-
if (mode === 'clear') {
|
|
1748
|
-
manager.setPrimaryClip(ClipData.newPlainText('android-toolkit', ''));
|
|
1749
|
-
emit({ ok: true, source: 'clipboard.clear' });
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
var candidates = [];
|
|
1753
|
-
var clip = manager.getPrimaryClip();
|
|
1754
|
-
var count = clip ? clip.getItemCount() : 0;
|
|
1755
|
-
for (var i = 0; i < count; i++) {
|
|
1756
|
-
var item = clip.getItemAt(i);
|
|
1757
|
-
try { addCandidate(candidates, 'clipboard.text', item.getText()); } catch (_) {}
|
|
1758
|
-
try { addCandidate(candidates, 'clipboard.html', item.getHtmlText()); } catch (_) {}
|
|
1759
|
-
try { addCandidate(candidates, 'clipboard.uri', item.getUri()); } catch (_) {}
|
|
1760
|
-
try { addCandidate(candidates, 'clipboard.intent', item.getIntent()); } catch (_) {}
|
|
1761
|
-
try { addCandidate(candidates, 'clipboard.coerceToText', item.coerceToText(app)); } catch (_) {}
|
|
1762
|
-
}
|
|
1763
|
-
emit({
|
|
1764
|
-
ok: candidates.length > 0,
|
|
1765
|
-
link: candidates.length > 0 ? candidates[0].link : '',
|
|
1766
|
-
source: candidates.length > 0 ? candidates[0].source : 'clipboard',
|
|
1767
|
-
candidates: candidates,
|
|
1768
|
-
payloadSnapshot: candidates.length > 0 ? String(candidates[0].payload || '').slice(0, 500) : ''
|
|
1769
|
-
});
|
|
1770
|
-
} catch (error) {
|
|
1771
|
-
emit({ ok: false, error: String(error), stack: String(error.stack || '') });
|
|
1772
|
-
}
|
|
1773
|
-
});
|
|
1774
|
-
`;
|
|
1752
|
+
async function readClipboard2(ctx) {
|
|
1753
|
+
const payload = await Device.readClipboard(ctx);
|
|
1754
|
+
const candidates = extractLinks(payload).map((link) => ({
|
|
1755
|
+
source: "adb.clipboard",
|
|
1756
|
+
link,
|
|
1757
|
+
payload: payload.slice(0, 1e3)
|
|
1758
|
+
}));
|
|
1759
|
+
return {
|
|
1760
|
+
ok: candidates.length > 0,
|
|
1761
|
+
link: candidates[0]?.link || "",
|
|
1762
|
+
source: "adb.clipboard",
|
|
1763
|
+
candidates,
|
|
1764
|
+
payloadSnapshot: payload.slice(0, 500)
|
|
1765
|
+
};
|
|
1775
1766
|
}
|
|
1776
1767
|
async function composeSprite(buffers, options = {}) {
|
|
1777
1768
|
if (!buffers.length) {
|
|
@@ -1816,6 +1807,11 @@ function selectLink(event, prefix) {
|
|
|
1816
1807
|
}
|
|
1817
1808
|
return candidates.find((link) => !prefix || String(link).startsWith(prefix)) || "";
|
|
1818
1809
|
}
|
|
1810
|
+
function extractLinks(value) {
|
|
1811
|
+
const text2 = String(value || "");
|
|
1812
|
+
const matches = text2.match(/https?:\/\/[^\s"'<>,。]+/g) || [];
|
|
1813
|
+
return matches.map((link) => link.replace(/[)\].,,。;;!?!?]+$/g, ""));
|
|
1814
|
+
}
|
|
1819
1815
|
|
|
1820
1816
|
// src/launch.js
|
|
1821
1817
|
var DEFAULT_INPUT_PATH = "/apify_storage/input.json";
|
|
@@ -1827,7 +1823,7 @@ async function run(handler, options = {}) {
|
|
|
1827
1823
|
const startedAt = Date.now();
|
|
1828
1824
|
const inputPath = pathOption(options.inputPath, DEFAULT_INPUT_PATH);
|
|
1829
1825
|
const outputPath = pathOption(options.outputPath, DEFAULT_OUTPUT_PATH);
|
|
1830
|
-
const input = options.input || JSON.parse(
|
|
1826
|
+
const input = options.input || JSON.parse(import_node_fs2.default.readFileSync(inputPath, "utf8"));
|
|
1831
1827
|
const ctx = options.ctx || Context.createAndroidContext(input, options.contextDefaults || {});
|
|
1832
1828
|
ctx.runId = ctx.runId || input.run_id || input.runId || input.runtime?.run_id || input.runtime?.runId || "";
|
|
1833
1829
|
ctx.actorKey = ctx.actorKey || options.actorKey || input.actorKey || input.actor_name || input.actorName || "";
|
|
@@ -1844,7 +1840,7 @@ async function run(handler, options = {}) {
|
|
|
1844
1840
|
Device,
|
|
1845
1841
|
DeviceInput,
|
|
1846
1842
|
DeviceView,
|
|
1847
|
-
|
|
1843
|
+
DeviceSQLite,
|
|
1848
1844
|
Share,
|
|
1849
1845
|
Mutation,
|
|
1850
1846
|
Logger,
|
|
@@ -1882,20 +1878,26 @@ function pathOption(value, fallback) {
|
|
|
1882
1878
|
}
|
|
1883
1879
|
|
|
1884
1880
|
// entrys/node.js
|
|
1885
|
-
var
|
|
1886
|
-
|
|
1887
|
-
ApifyKit,
|
|
1888
|
-
DeviceInput,
|
|
1889
|
-
DeviceView,
|
|
1890
|
-
Device,
|
|
1891
|
-
Mutation,
|
|
1892
|
-
Share,
|
|
1893
|
-
Frida,
|
|
1894
|
-
Constants: constants_exports,
|
|
1895
|
-
Errors: errors_exports,
|
|
1896
|
-
Logger,
|
|
1897
|
-
Context
|
|
1881
|
+
var ToolkitMode = Object.freeze({
|
|
1882
|
+
default: "default"
|
|
1898
1883
|
});
|
|
1884
|
+
var useAndroidToolKit = () => {
|
|
1885
|
+
return {
|
|
1886
|
+
Launch,
|
|
1887
|
+
ApifyKit,
|
|
1888
|
+
DeviceInput,
|
|
1889
|
+
DeviceView,
|
|
1890
|
+
DeviceSQLite,
|
|
1891
|
+
Device,
|
|
1892
|
+
Mutation,
|
|
1893
|
+
Share,
|
|
1894
|
+
Constants: constants_exports,
|
|
1895
|
+
Errors: errors_exports,
|
|
1896
|
+
Logger,
|
|
1897
|
+
Context
|
|
1898
|
+
};
|
|
1899
|
+
};
|
|
1900
|
+
useAndroidToolKit.Mode = ToolkitMode;
|
|
1899
1901
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1900
1902
|
0 && (module.exports = {
|
|
1901
1903
|
useAndroidToolKit
|