@skrillex1224/android-toolkit 0.1.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1880 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/apify-kit.js
8
+ import fs from "node:fs";
9
+
10
+ // src/constants.js
11
+ var constants_exports = {};
12
+ __export(constants_exports, {
13
+ ActorInfo: () => ActorInfo,
14
+ Code: () => Code,
15
+ Status: () => Status,
16
+ UnicodeIme: () => UnicodeIme
17
+ });
18
+ var Code = Object.freeze({
19
+ Success: 0,
20
+ UnknownError: -1,
21
+ NotLogin: 30000001,
22
+ Timeout: 30000003,
23
+ InitialTimeout: 30000004,
24
+ OverallTimeout: 30000005,
25
+ InvalidRequest: 30010001,
26
+ AdbUnavailable: 30010002,
27
+ ContentUnavailable: 30010003,
28
+ SourceExtractionFailed: 30010004,
29
+ AutomationFailed: 30010005,
30
+ FridaUnavailable: 30010008,
31
+ AppNotInstalled: 30010009
32
+ });
33
+ var Status = Object.freeze({
34
+ Success: "SUCCESS",
35
+ Failed: "FAILED"
36
+ });
37
+ var UnicodeIme = Object.freeze({
38
+ packageName: "io.appium.settings",
39
+ component: "io.appium.settings/.UnicodeIME",
40
+ inputTextAction: "ADB_INPUT_TEXT"
41
+ });
42
+ var normalizeShare = (share) => {
43
+ const source = share && typeof share === "object" ? share : {};
44
+ return {
45
+ mode: "clipboard",
46
+ prefix: String(source.prefix || "").trim(),
47
+ xurl: Array.isArray(source.xurl) ? source.xurl : []
48
+ };
49
+ };
50
+ var createActorInfo = (info) => {
51
+ const key = String(info.key || "").trim();
52
+ return Object.freeze({
53
+ key,
54
+ name: String(info.name || key),
55
+ icon: String(info.icon || `https://static.heartbitai.com/general/actors/${key}.png`),
56
+ share: Object.freeze(normalizeShare(info.share))
57
+ });
58
+ };
59
+ var ActorInfo = Object.freeze({
60
+ "doubao.android": createActorInfo({
61
+ key: "doubao.android",
62
+ name: "\u8C46\u5305 Android",
63
+ share: {
64
+ prefix: "https://www.doubao.com/thread/",
65
+ xurl: []
66
+ }
67
+ })
68
+ });
69
+
70
+ // src/context.js
71
+ var Context = {
72
+ createAndroidContext,
73
+ firstNonEmpty,
74
+ requireNonEmpty
75
+ };
76
+ function createAndroidContext(input = {}, defaults = {}) {
77
+ const runtime = input.runtime && typeof input.runtime === "object" ? input.runtime : {};
78
+ const device = runtime.device && typeof runtime.device === "object" ? runtime.device : {};
79
+ return {
80
+ runId: firstNonEmpty(defaults.runId, runtime.runId, input.runId),
81
+ query: String(firstNonEmpty(defaults.query, input.query) || "").trim(),
82
+ serial: firstNonEmpty(defaults.serial, device.serial),
83
+ packageName: firstNonEmpty(defaults.packageName, device.packageName),
84
+ adbPath: firstNonEmpty(defaults.adbPath, process.env.ANDROID_TOOLKIT_ADB_PATH),
85
+ fridaPath: firstNonEmpty(defaults.fridaPath, process.env.ANDROID_TOOLKIT_FRIDA_PATH),
86
+ appVersion: firstNonEmpty(defaults.appVersion, device.appVersion),
87
+ slotKey: firstNonEmpty(defaults.slotKey, device.slotKey),
88
+ instanceId: firstNonEmpty(defaults.instanceId, device.instanceId),
89
+ externalIp: firstNonEmpty(defaults.externalIp, device.externalIp)
90
+ };
91
+ }
92
+ function firstNonEmpty(...values) {
93
+ for (const value of values) {
94
+ const clean = String(value ?? "").trim();
95
+ if (clean) return clean;
96
+ }
97
+ return "";
98
+ }
99
+ function requireNonEmpty(value, name) {
100
+ const clean = String(value ?? "").trim();
101
+ if (!clean) throw new Error(`${name} is required`);
102
+ return clean;
103
+ }
104
+
105
+ // src/device.js
106
+ import { execFile } from "node:child_process";
107
+ import { promisify } from "node:util";
108
+
109
+ // src/errors.js
110
+ var errors_exports = {};
111
+ __export(errors_exports, {
112
+ CrawlerError: () => CrawlerError,
113
+ serializeError: () => serializeError
114
+ });
115
+ var CrawlerError = class _CrawlerError extends Error {
116
+ constructor(input = {}, options = {}) {
117
+ const payload = typeof input === "string" ? { message: input } : input;
118
+ super(payload.message || "Android crawler error", options);
119
+ this.name = "CrawlerError";
120
+ this.code = payload.code || Code.UnknownError;
121
+ this.context = payload.context || {};
122
+ }
123
+ static isCrawlerError(error) {
124
+ return error instanceof _CrawlerError || error?.name === "CrawlerError";
125
+ }
126
+ static from(error, fallback = {}) {
127
+ if (_CrawlerError.isCrawlerError(error)) return error;
128
+ return new _CrawlerError({
129
+ message: error?.message || String(error),
130
+ code: fallback.code || Code.UnknownError,
131
+ context: fallback.context || {}
132
+ });
133
+ }
134
+ };
135
+ function serializeError(error) {
136
+ if (!error) return { message: "" };
137
+ return {
138
+ name: error.name || "Error",
139
+ message: error.message || String(error),
140
+ stack: error.stack || "",
141
+ code: error.code || "",
142
+ context: error.context || void 0
143
+ };
144
+ }
145
+
146
+ // src/logger.js
147
+ var defaultLogger = {
148
+ info: (...args) => console.error(...args),
149
+ warning: (...args) => console.error(...args),
150
+ warn: (...args) => console.error(...args),
151
+ error: (...args) => console.error(...args),
152
+ debug: (...args) => console.error(...args)
153
+ };
154
+ var activeLogger = defaultLogger;
155
+ var LOG_TEMPLATES = Object.freeze({
156
+ stepStart: "[Step] start",
157
+ stepSuccess: "[Step] success",
158
+ stepFail: "[Step] failed",
159
+ mutationStable: "[Mutation] stable",
160
+ shareStart: "[Share] start",
161
+ screenshotDone: "[Share] screenshot done",
162
+ dataPush: "[Dataset] push"
163
+ });
164
+ var Logger = {
165
+ setLogger(logger) {
166
+ activeLogger = logger || defaultLogger;
167
+ },
168
+ createLogger(scope) {
169
+ const prefix = scope ? `[${scope}] ` : "";
170
+ return {
171
+ start: (message, detail) => Logger.start(`${prefix}${message}`, detail),
172
+ success: (message, detail) => Logger.success(`${prefix}${message}`, detail),
173
+ fail: (message, error) => Logger.fail(`${prefix}${message}`, error),
174
+ warn: (message, detail) => Logger.warn(`${prefix}${message}`, detail),
175
+ info: (message, detail) => Logger.info(`${prefix}${message}`, detail),
176
+ debug: (message, detail) => Logger.debug(`${prefix}${message}`, detail)
177
+ };
178
+ },
179
+ useTemplate() {
180
+ return {
181
+ taskStart: (detail) => Logger.start("\u4EFB\u52A1\u5F00\u59CB", detail),
182
+ taskSuccess: (detail) => Logger.success("\u4EFB\u52A1\u5B8C\u6210", detail),
183
+ taskFail: (detail, error) => Logger.fail(`\u4EFB\u52A1\u5931\u8D25 ${detail || ""}`.trim(), error),
184
+ stepStart: (step, detail) => Logger.start(`[Step] ${step}`, detail),
185
+ stepSuccess: (step, detail) => Logger.success(`[Step] ${step}`, detail),
186
+ stepFail: (step, error) => Logger.fail(`[Step] ${step}`, error),
187
+ mutationStable: (detail) => Logger.success("[Mutation] stable", detail),
188
+ shareStart: (detail) => Logger.start("[Share] capture", detail),
189
+ screenshotDone: (detail) => Logger.success("[Share] screenshot", detail),
190
+ dataPush: (detail) => Logger.success("[Dataset] push", detail)
191
+ };
192
+ },
193
+ start(message, detail = "") {
194
+ write("info", `\u25B6 ${message}`, detail);
195
+ },
196
+ success(message, detail = "") {
197
+ write("info", `\u2713 ${message}`, detail);
198
+ },
199
+ fail(message, error = "") {
200
+ write("error", `\u2717 ${message}`, error?.message || error);
201
+ },
202
+ warn(message, detail = "") {
203
+ write("warn", `\u26A0 ${message}`, detail);
204
+ },
205
+ warning(message, detail = "") {
206
+ Logger.warn(message, detail);
207
+ },
208
+ info(message, detail = "") {
209
+ write("info", message, detail);
210
+ },
211
+ debug(message, detail = "") {
212
+ write("debug", message, detail);
213
+ },
214
+ duration(startedAt) {
215
+ return `${((Date.now() - startedAt) / 1e3).toFixed(2)}s`;
216
+ }
217
+ };
218
+ function stripAnsi(value) {
219
+ return String(value || "").replace(/\u001b\[[0-9;]*m/g, "");
220
+ }
221
+ function sleep(ms) {
222
+ return new Promise((resolve) => setTimeout(resolve, ms));
223
+ }
224
+ function write(level, message, detail = "") {
225
+ const formattedDetail = formatDetail(detail);
226
+ const line = formattedDetail ? `[${formatTimestamp()}] [android-toolkit] ${message} ${formattedDetail}` : `[${formatTimestamp()}] [android-toolkit] ${message}`;
227
+ const writer = activeLogger[level] || activeLogger.warning || activeLogger.warn || activeLogger.info || console.error;
228
+ writer.call(activeLogger, line);
229
+ }
230
+ function formatTimestamp(date = /* @__PURE__ */ new Date()) {
231
+ const pad = (value, size = 2) => String(value).padStart(size, "0");
232
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
233
+ }
234
+ function formatDetail(detail) {
235
+ if (detail == null || detail === "") return "";
236
+ if (detail instanceof Error) return detail.message;
237
+ if (typeof detail === "string") return detail;
238
+ if (Array.isArray(detail)) return detail.map(formatDetail).filter(Boolean).join(" ");
239
+ if (typeof detail === "object") {
240
+ return Object.entries(detail).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${key}=${typeof value === "object" ? safeJson(value) : String(value)}`).join(" ");
241
+ }
242
+ return String(detail);
243
+ }
244
+ function safeJson(value) {
245
+ try {
246
+ return JSON.stringify(value);
247
+ } catch {
248
+ return String(value);
249
+ }
250
+ }
251
+
252
+ // src/device.js
253
+ var execFileAsync = promisify(execFile);
254
+ var Device = {
255
+ adbExec,
256
+ adbShell,
257
+ forceStopApp,
258
+ startActivity,
259
+ startLauncher,
260
+ focusedActivity,
261
+ waitForActivity,
262
+ waitForForeground,
263
+ screenSize,
264
+ screenDensity,
265
+ tapAbsolute,
266
+ swipe,
267
+ pressKey,
268
+ pressBack,
269
+ pressEnter,
270
+ typeText,
271
+ screenshotPng,
272
+ screenshotBase64,
273
+ dumpUiXml,
274
+ wakeAndUnlock,
275
+ hideKeyboard,
276
+ isKeyboardVisible,
277
+ sleep,
278
+ ensureUnicodeIme
279
+ };
280
+ async function adbExec(ctx, args, options = {}) {
281
+ const adbPath = requireDeviceField(ctx, "adbPath");
282
+ const startedAt = Date.now();
283
+ try {
284
+ const { stdout } = await execFileAsync(adbPath, args.map(String), {
285
+ timeout: Number(options.timeoutMs || 15e3),
286
+ maxBuffer: Number(options.maxBuffer || 12 * 1024 * 1024),
287
+ encoding: options.encoding || "utf8"
288
+ });
289
+ Logger.debug("adbExec", { duration: Logger.duration(startedAt), args: compactArgs(args) });
290
+ return stdout;
291
+ } catch (error) {
292
+ if (isAdbUnavailableError(error)) {
293
+ throw new CrawlerError({
294
+ message: `adb_unavailable: ${error?.message || String(error)}`,
295
+ code: Code.AdbUnavailable,
296
+ context: { args: compactArgs(args), adbPath }
297
+ });
298
+ }
299
+ throw error;
300
+ }
301
+ }
302
+ async function adbShell(ctx, args, options = {}) {
303
+ const serial = requireDeviceField(ctx, "serial");
304
+ return adbExec(ctx, ["-s", serial, "shell", ...args], options);
305
+ }
306
+ async function adbInput(ctx, args, options = {}) {
307
+ return adbShell(ctx, ["input", ...args], options);
308
+ }
309
+ async function forceStopApp(ctx, packageName = ctx.packageName) {
310
+ await adbShell(ctx, ["am", "force-stop", requireValue(packageName, "packageName")], { timeoutMs: 15e3 });
311
+ }
312
+ async function startActivity(ctx, component, args = []) {
313
+ await adbShell(ctx, ["am", "start", "-n", requireValue(component, "component"), ...args], { timeoutMs: 2e4 });
314
+ await sleep(1500);
315
+ }
316
+ async function startLauncher(ctx, packageName = ctx.packageName, launcherActivity) {
317
+ await startActivity(ctx, `${requireValue(packageName, "packageName")}/${requireValue(launcherActivity, "launcherActivity")}`, [
318
+ "-a",
319
+ "android.intent.action.MAIN",
320
+ "-c",
321
+ "android.intent.category.LAUNCHER"
322
+ ]);
323
+ }
324
+ async function focusedActivity(ctx) {
325
+ const resumed = await resumedActivity(ctx).catch(() => "");
326
+ if (resumed) return resumed;
327
+ const out = await adbShell(ctx, ["dumpsys", "window"], { timeoutMs: 15e3, maxBuffer: 12 * 1024 * 1024 });
328
+ for (const line of String(out || "").split("\n")) {
329
+ const trimmed = line.trim();
330
+ if (trimmed.includes("mCurrentFocus=Window") && !trimmed.includes("=null")) return trimmed;
331
+ }
332
+ for (const line of String(out || "").split("\n")) {
333
+ const trimmed = line.trim();
334
+ if (trimmed.includes("mFocusedApp=AppWindowToken") && !trimmed.includes("=null")) return trimmed;
335
+ }
336
+ return String(out || "").trim();
337
+ }
338
+ async function resumedActivity(ctx) {
339
+ const out = await adbShell(ctx, ["dumpsys", "activity", "activities"], {
340
+ timeoutMs: 15e3,
341
+ maxBuffer: 12 * 1024 * 1024
342
+ });
343
+ let fallback = "";
344
+ for (const line of String(out || "").split("\n")) {
345
+ const trimmed = line.trim();
346
+ if (!trimmed.includes("ActivityRecord") || !trimmed.includes("/")) continue;
347
+ if (trimmed.includes("mTopResumedActivity") || trimmed.includes("topResumedActivity")) return trimmed;
348
+ if (trimmed.includes("mResumedActivity")) fallback = trimmed;
349
+ }
350
+ return fallback;
351
+ }
352
+ async function waitForActivity(ctx, predicate, timeoutMs = 15e3, intervalMs = 700) {
353
+ const deadline = Date.now() + Number(timeoutMs);
354
+ let lastFocused = "";
355
+ while (Date.now() < deadline) {
356
+ lastFocused = await focusedActivity(ctx).catch((error) => `ERROR: ${error?.message || String(error)}`);
357
+ if (predicate(lastFocused)) return lastFocused;
358
+ await sleep(intervalMs);
359
+ }
360
+ throw new CrawlerError({
361
+ message: `automation_failed: waitForActivity timeout focused=${lastFocused}`,
362
+ code: Code.AutomationFailed
363
+ });
364
+ }
365
+ async function waitForForeground(ctx, packageName = ctx.packageName, timeoutMs = 6e3) {
366
+ const target = requireValue(packageName, "packageName");
367
+ return waitForActivity(ctx, (value) => String(value || "").includes(target), timeoutMs, 500);
368
+ }
369
+ async function screenSize(ctx) {
370
+ const out = await adbShell(ctx, ["wm", "size"], { timeoutMs: 15e3 });
371
+ const match = /(\d+)x(\d+)/.exec(String(out || ""));
372
+ if (!match) throw new CrawlerError({ message: `adb_unavailable: cannot parse screen size: ${out}`, code: Code.AdbUnavailable });
373
+ return { width: Number(match[1]), height: Number(match[2]) };
374
+ }
375
+ async function screenDensity(ctx) {
376
+ const out = await adbShell(ctx, ["wm", "density"], { timeoutMs: 15e3 });
377
+ const match = /(\d+)/.exec(String(out || ""));
378
+ if (!match) throw new CrawlerError({ message: `adb_unavailable: cannot parse screen density: ${out}`, code: Code.AdbUnavailable });
379
+ return Number(match[1]);
380
+ }
381
+ async function tapAbsolute(ctx, x, y) {
382
+ await adbInput(ctx, ["tap", Math.round(Number(x)), Math.round(Number(y))], { timeoutMs: 15e3 });
383
+ }
384
+ async function swipe(ctx, from, to, durationMs = 360) {
385
+ await adbInput(ctx, [
386
+ "swipe",
387
+ Math.round(Number(from?.x)),
388
+ Math.round(Number(from?.y)),
389
+ Math.round(Number(to?.x)),
390
+ Math.round(Number(to?.y)),
391
+ Math.round(Number(durationMs))
392
+ ], { timeoutMs: 15e3 });
393
+ }
394
+ async function pressKey(ctx, key) {
395
+ await adbInput(ctx, ["keyevent", String(key)], { timeoutMs: 15e3 });
396
+ }
397
+ async function pressBack(ctx) {
398
+ await pressKey(ctx, "BACK");
399
+ }
400
+ async function pressEnter(ctx) {
401
+ await pressKey(ctx, "KEYCODE_ENTER");
402
+ }
403
+ async function typeText(ctx, text2) {
404
+ await ensureUnicodeIme(ctx);
405
+ await adbShell(ctx, ["am", "broadcast", "-a", UnicodeIme.inputTextAction, "--es", "msg", String(text2 ?? "")], {
406
+ timeoutMs: 3e4,
407
+ maxBuffer: 4 * 1024 * 1024
408
+ });
409
+ }
410
+ async function ensureUnicodeIme(ctx) {
411
+ const out = await adbShell(ctx, ["ime", "list", "-s"], { timeoutMs: 15e3 });
412
+ if (!String(out || "").split(/\s+/).includes(UnicodeIme.component)) {
413
+ throw new CrawlerError({
414
+ message: `adb_unavailable: UnicodeIME \u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528 ${UnicodeIme.component}`,
415
+ code: Code.AdbUnavailable,
416
+ context: { component: UnicodeIme.component }
417
+ });
418
+ }
419
+ await adbShell(ctx, ["ime", "set", UnicodeIme.component], { timeoutMs: 15e3 });
420
+ }
421
+ async function screenshotPng(ctx) {
422
+ const serial = requireDeviceField(ctx, "serial");
423
+ const adbPath = requireDeviceField(ctx, "adbPath");
424
+ const { stdout } = await execFileAsync(adbPath, ["-s", serial, "exec-out", "screencap", "-p"], {
425
+ timeout: 3e4,
426
+ maxBuffer: 40 * 1024 * 1024,
427
+ encoding: "buffer"
428
+ });
429
+ return Buffer.from(stdout);
430
+ }
431
+ async function screenshotBase64(ctx) {
432
+ return `data:image/png;base64,${(await screenshotPng(ctx)).toString("base64")}`;
433
+ }
434
+ async function dumpUiXml(ctx) {
435
+ const raw = await adbShell(ctx, ["uiautomator", "dump", "/dev/tty"], {
436
+ timeoutMs: 3e4,
437
+ maxBuffer: 16 * 1024 * 1024
438
+ });
439
+ const start = String(raw || "").indexOf("<?xml");
440
+ if (start < 0) throw new CrawlerError({ message: `automation_failed: uiautomator dump missing xml: ${raw}`, code: Code.AutomationFailed });
441
+ return String(raw || "").slice(start).trim();
442
+ }
443
+ async function wakeAndUnlock(ctx) {
444
+ await pressKey(ctx, "KEYCODE_WAKEUP").catch(() => {
445
+ });
446
+ await sleep(300);
447
+ await pressKey(ctx, "MENU").catch(() => {
448
+ });
449
+ await sleep(300);
450
+ const size = await screenSize(ctx);
451
+ await swipe(ctx, { x: size.width * 0.5, y: size.height * 0.82 }, { x: size.width * 0.5, y: size.height * 0.25 }, 350).catch(() => {
452
+ });
453
+ await sleep(600);
454
+ }
455
+ async function isKeyboardVisible(ctx) {
456
+ const out = await adbShell(ctx, ["dumpsys", "input_method"], { timeoutMs: 15e3, maxBuffer: 4 * 1024 * 1024 });
457
+ return /mInputShown=true|mIsInputViewShown=true|inputShown=true/i.test(String(out || ""));
458
+ }
459
+ async function hideKeyboard(ctx, options = {}) {
460
+ const attempts = Math.max(1, Math.trunc(Number(options.attempts || 2)));
461
+ for (let i = 0; i < attempts; i += 1) {
462
+ if (!await isKeyboardVisible(ctx).catch(() => false)) return true;
463
+ await pressBack(ctx).catch(() => {
464
+ });
465
+ await sleep(Number(options.settleMs || 400));
466
+ }
467
+ return !await isKeyboardVisible(ctx).catch(() => false);
468
+ }
469
+ function requireDeviceField(ctx, key) {
470
+ return requireValue(ctx?.[key], key);
471
+ }
472
+ function requireValue(value, name) {
473
+ const clean = String(value ?? "").trim();
474
+ if (!clean) throw new CrawlerError({ message: `invalid_request: ${name} is required`, code: Code.InvalidRequest });
475
+ return clean;
476
+ }
477
+ function isAdbUnavailableError(error) {
478
+ const message = String(error?.message || error || "");
479
+ return /ENOENT|spawn .*adb|not found|No such file|device .* not found|no devices|offline/i.test(message);
480
+ }
481
+ function compactArgs(args) {
482
+ return args.map(String).join(" ").slice(0, 500);
483
+ }
484
+
485
+ // src/apify-kit.js
486
+ var instance = null;
487
+ var ApifyKit = {
488
+ useApifyKit
489
+ };
490
+ async function useApifyKit(options = {}) {
491
+ if (!instance || options.reset) {
492
+ instance = createApifyKit(options);
493
+ }
494
+ return instance;
495
+ }
496
+ function createApifyKit(options = {}) {
497
+ const inputPath = String(options.inputPath || "").trim();
498
+ const outputPath = requiredOption(options.outputPath, "outputPath");
499
+ const input = options.input || (inputPath ? JSON.parse(fs.readFileSync(inputPath, "utf8")) : {});
500
+ const ctx = options.ctx || Context.createAndroidContext(input, options.contextDefaults || {});
501
+ let pushed = false;
502
+ return {
503
+ ctx,
504
+ input,
505
+ inputPath,
506
+ outputPath,
507
+ async runStep(step, target, actionFn, options2 = {}) {
508
+ const { failActor = true, retry = {} } = options2;
509
+ const retryTimes = Math.max(0, Number(retry.times || 0));
510
+ const startedAt = Date.now();
511
+ let lastError = null;
512
+ for (let attempt = 0; attempt <= retryTimes; attempt += 1) {
513
+ const attemptLabel = attempt > 0 ? ` (\u91CD\u8BD5 #${attempt})` : "";
514
+ Logger.start(`[Step] ${step}${attemptLabel}`, stepDetail(target));
515
+ try {
516
+ const result = await actionFn();
517
+ Logger.success(`[Step] ${step}${attemptLabel}`, { duration: Logger.duration(startedAt) });
518
+ return result;
519
+ } catch (error) {
520
+ lastError = error;
521
+ Logger.fail(`[Step] ${step}${attemptLabel}`, error);
522
+ if (attempt < retryTimes) {
523
+ if (typeof retry.before === "function") {
524
+ await retry.before(ctx, attempt + 1);
525
+ } else {
526
+ await sleep(3e3);
527
+ }
528
+ }
529
+ }
530
+ }
531
+ if (failActor) {
532
+ let base64 = "";
533
+ try {
534
+ base64 = await Device.screenshotBase64(ctx);
535
+ } catch (error) {
536
+ base64 = `\u622A\u56FE\u5931\u8D25: ${error?.message || String(error)}`;
537
+ }
538
+ await this.pushFailed(lastError, { step, base64, retryAttempts: retryTimes });
539
+ }
540
+ throw lastError;
541
+ },
542
+ async runStepLoose(step, target, actionFn, options2 = {}) {
543
+ return this.runStep(step, target, actionFn, { ...options2, failActor: false });
544
+ },
545
+ async pushSuccess(data = {}, options2 = {}) {
546
+ pushed = true;
547
+ appendOutput(outputPath, {
548
+ code: Code.Success,
549
+ status: Status.Success,
550
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
551
+ data: sanitizeData(data),
552
+ ...options2.meta ? { meta: sanitizeData(options2.meta) } : {}
553
+ });
554
+ Logger.success("pushSuccess", { outputPath, keys: Object.keys(data || {}).join(",") });
555
+ },
556
+ async pushArtifact(data = {}) {
557
+ appendOutput(outputPath, sanitizeData(data));
558
+ Logger.success("pushArtifact", { outputPath, keys: Object.keys(data || {}).join(",") });
559
+ },
560
+ async pushFailed(error, meta = {}) {
561
+ const normalized = CrawlerError.isCrawlerError(error) ? error : CrawlerError.from(error, { code: Code.UnknownError, context: meta });
562
+ pushed = true;
563
+ appendOutput(outputPath, {
564
+ code: normalized.code,
565
+ status: Status.Failed,
566
+ error: serializeError(normalized),
567
+ meta: sanitizeData(meta),
568
+ context: sanitizeData(normalized.context),
569
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
570
+ });
571
+ Logger.success("pushFailed", { outputPath, code: normalized.code, message: normalized.message });
572
+ },
573
+ hasPushed() {
574
+ return pushed;
575
+ }
576
+ };
577
+ }
578
+ function appendOutput(outputPath, item) {
579
+ const items = readOutputArray(outputPath);
580
+ items.push(item);
581
+ fs.writeFileSync(outputPath, JSON.stringify(items, null, 2) + "\n", "utf8");
582
+ }
583
+ function readOutputArray(outputPath) {
584
+ if (!fs.existsSync(outputPath)) return [];
585
+ const raw = fs.readFileSync(outputPath, "utf8").trim();
586
+ if (!raw) return [];
587
+ const parsed = JSON.parse(raw);
588
+ if (!Array.isArray(parsed)) throw new Error(`Android output must be a JSON array: ${outputPath}`);
589
+ return parsed;
590
+ }
591
+ function requiredOption(value, name) {
592
+ const clean = String(value || "").trim();
593
+ if (!clean) throw new Error(`ApifyKit.useApifyKit requires ${name}`);
594
+ return clean;
595
+ }
596
+ function stepDetail(target) {
597
+ if (!target) return {};
598
+ if (typeof target === "string") return { target };
599
+ if (typeof target === "object") {
600
+ return {
601
+ serial: target.serial,
602
+ packageName: target.packageName,
603
+ runId: target.runId
604
+ };
605
+ }
606
+ return { target: String(target) };
607
+ }
608
+ function sanitizeData(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
609
+ if (depth > 8) return "[MaxDepth]";
610
+ if (value == null) return value;
611
+ const type = typeof value;
612
+ if (type === "string" || type === "number" || type === "boolean") return value;
613
+ if (type === "bigint") return value.toString();
614
+ if (type === "function" || type === "symbol") return void 0;
615
+ if (value instanceof Error) return serializeError(value);
616
+ if (Array.isArray(value)) return value.map((item) => sanitizeData(item, depth + 1, seen)).filter((item) => item !== void 0);
617
+ if (type !== "object") return String(value);
618
+ if (seen.has(value)) return "[Circular]";
619
+ seen.add(value);
620
+ const out = {};
621
+ for (const [key, item] of Object.entries(value)) {
622
+ const safe = sanitizeData(item, depth + 1, seen);
623
+ if (safe !== void 0) out[key] = safe;
624
+ }
625
+ return out;
626
+ }
627
+
628
+ // src/device-view.js
629
+ import { createHash } from "node:crypto";
630
+ import { XMLParser } from "fast-xml-parser";
631
+ var parser = new XMLParser({
632
+ ignoreAttributes: false,
633
+ attributeNamePrefix: "",
634
+ allowBooleanAttributes: true
635
+ });
636
+ var DeviceView = {
637
+ snapshot,
638
+ find,
639
+ findAll,
640
+ waitFor,
641
+ bounds,
642
+ text,
643
+ exists,
644
+ isScrollable,
645
+ hashNode,
646
+ normalizeSelector
647
+ };
648
+ async function snapshot(ctx, options = {}) {
649
+ const xml = options.xml || await Device.dumpUiXml(ctx);
650
+ const parsed = parser.parse(xml);
651
+ const roots = toArray(parsed?.hierarchy?.node).map((node) => normalizeNode(node, null, 0));
652
+ const flat = [];
653
+ for (const root of roots) collectFlat(root, flat);
654
+ return { xml, tree: roots, roots, flat, nodes: flat };
655
+ }
656
+ async function find(ctx, selector, options = {}) {
657
+ const nodes = await findAll(ctx, selector, options);
658
+ if (nodes.length > 0) return nodes[0];
659
+ if (options.optional) return null;
660
+ throw new CrawlerError({
661
+ message: `automation_failed: View \u672A\u627E\u5230 ${selectorLabel(selector)}`,
662
+ code: Code.AutomationFailed,
663
+ context: { selector }
664
+ });
665
+ }
666
+ async function findAll(ctx, selector, options = {}) {
667
+ const view = options.snapshot || await snapshot(ctx, options);
668
+ const normalized = normalizeSelector(selector);
669
+ return view.flat.filter((node) => matchesSelector(node, normalized, options));
670
+ }
671
+ async function waitFor(ctx, selector, options = {}) {
672
+ const timeoutMs = Number(options.timeoutMs || options.timeout || 3e4);
673
+ const intervalMs = Number(options.intervalMs || options.pollIntervalMs || 500);
674
+ const deadline = Date.now() + timeoutMs;
675
+ let lastError = null;
676
+ while (Date.now() < deadline) {
677
+ try {
678
+ const node = await find(ctx, selector, { ...options, optional: true });
679
+ if (node) return node;
680
+ } catch (error) {
681
+ lastError = error;
682
+ }
683
+ await sleep(intervalMs);
684
+ }
685
+ if (lastError) throw lastError;
686
+ throw new CrawlerError({
687
+ message: `automation_failed: \u7B49\u5F85 View \u8D85\u65F6 ${selectorLabel(selector)}`,
688
+ code: Code.AutomationFailed,
689
+ context: { selector, timeoutMs }
690
+ });
691
+ }
692
+ async function bounds(ctx, selector, options = {}) {
693
+ return (await find(ctx, selector, options)).bounds;
694
+ }
695
+ async function text(ctx, selector, options = {}) {
696
+ const node = await find(ctx, selector, options);
697
+ return node.text || node.contentDesc || "";
698
+ }
699
+ async function exists(ctx, selector, options = {}) {
700
+ return Boolean(await find(ctx, selector, { ...options, optional: true }));
701
+ }
702
+ async function isScrollable(ctx, selector, options = {}) {
703
+ const node = await find(ctx, selector, options);
704
+ return Boolean(node.scrollable);
705
+ }
706
+ function normalizeSelector(selector) {
707
+ if (!selector || typeof selector !== "object" || Array.isArray(selector)) {
708
+ throw new CrawlerError({
709
+ message: "invalid_request: DeviceView selector must be an object",
710
+ code: Code.InvalidRequest,
711
+ context: { selector }
712
+ });
713
+ }
714
+ const out = {};
715
+ for (const key of ["id", "text", "textContains", "contentDesc"]) {
716
+ if (selector[key] != null && String(selector[key]).trim()) out[key] = String(selector[key]).trim();
717
+ }
718
+ if (Object.keys(out).length === 0) {
719
+ throw new CrawlerError({
720
+ message: "invalid_request: DeviceView selector requires id/text/textContains/contentDesc",
721
+ code: Code.InvalidRequest,
722
+ context: { selector }
723
+ });
724
+ }
725
+ return out;
726
+ }
727
+ function hashNode(node) {
728
+ return createHash("sha256").update(stableNodeString(node)).digest("hex");
729
+ }
730
+ function normalizeNode(raw, parent, index) {
731
+ const childrenRaw = toArray(raw?.node);
732
+ const bounds2 = parseBounds(raw?.bounds);
733
+ const node = {
734
+ resourceId: String(raw?.["resource-id"] || ""),
735
+ id: shortResourceId(raw?.["resource-id"]),
736
+ text: String(raw?.text || ""),
737
+ contentDesc: String(raw?.["content-desc"] || ""),
738
+ className: String(raw?.class || ""),
739
+ packageName: String(raw?.package || ""),
740
+ bounds: bounds2,
741
+ clickable: toBoolean(raw?.clickable),
742
+ enabled: toBoolean(raw?.enabled),
743
+ focusable: toBoolean(raw?.focusable),
744
+ focused: toBoolean(raw?.focused),
745
+ scrollable: toBoolean(raw?.scrollable),
746
+ visible: bounds2.right > bounds2.left && bounds2.bottom > bounds2.top,
747
+ index: Number(raw?.index ?? index),
748
+ parent: null,
749
+ children: []
750
+ };
751
+ node.parent = parent || null;
752
+ node.children = childrenRaw.map((child, childIndex) => normalizeNode(child, node, childIndex));
753
+ return node;
754
+ }
755
+ function matchesSelector(node, selector, options = {}) {
756
+ if (options.visibleOnly !== false && !node.visible) return false;
757
+ if (options.enabledOnly === true && !node.enabled) return false;
758
+ if (selector.id && !matchesId(node, selector.id)) return false;
759
+ if (selector.text && node.text !== selector.text) return false;
760
+ if (selector.textContains && !node.text.includes(selector.textContains)) return false;
761
+ if (selector.contentDesc && node.contentDesc !== selector.contentDesc) return false;
762
+ return true;
763
+ }
764
+ function matchesId(node, expected) {
765
+ const actual = node.resourceId || "";
766
+ if (!actual) return false;
767
+ if (actual === expected) return true;
768
+ return actual.endsWith(`:id/${expected}`) || node.id === expected;
769
+ }
770
+ function collectFlat(node, out) {
771
+ out.push(node);
772
+ for (const child of node.children) collectFlat(child, out);
773
+ }
774
+ function toArray(value) {
775
+ if (!value) return [];
776
+ return Array.isArray(value) ? value : [value];
777
+ }
778
+ function parseBounds(value) {
779
+ const match = /\[(\d+),(\d+)]\[(\d+),(\d+)]/.exec(String(value || ""));
780
+ if (!match) return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0, centerX: 0, centerY: 0 };
781
+ const left = Number(match[1]);
782
+ const top = Number(match[2]);
783
+ const right = Number(match[3]);
784
+ const bottom = Number(match[4]);
785
+ return {
786
+ left,
787
+ top,
788
+ right,
789
+ bottom,
790
+ width: Math.max(0, right - left),
791
+ height: Math.max(0, bottom - top),
792
+ centerX: (left + right) / 2,
793
+ centerY: (top + bottom) / 2
794
+ };
795
+ }
796
+ function shortResourceId(value) {
797
+ const raw = String(value || "");
798
+ const marker = ":id/";
799
+ const index = raw.indexOf(marker);
800
+ return index >= 0 ? raw.slice(index + marker.length) : raw;
801
+ }
802
+ function toBoolean(value) {
803
+ return value === true || String(value).toLowerCase() === "true";
804
+ }
805
+ function stableNodeString(node) {
806
+ if (!node) return "";
807
+ return JSON.stringify({
808
+ resourceId: node.resourceId,
809
+ text: node.text,
810
+ contentDesc: node.contentDesc,
811
+ className: node.className,
812
+ bounds: node.bounds,
813
+ clickable: node.clickable,
814
+ enabled: node.enabled,
815
+ focusable: node.focusable,
816
+ focused: node.focused,
817
+ scrollable: node.scrollable,
818
+ children: node.children.map((child) => JSON.parse(stableNodeString(child) || "{}"))
819
+ });
820
+ }
821
+ function selectorLabel(selector) {
822
+ try {
823
+ return JSON.stringify(selector);
824
+ } catch {
825
+ return String(selector);
826
+ }
827
+ }
828
+
829
+ // src/device-input.js
830
+ var DeviceInput = {
831
+ click,
832
+ tap: click,
833
+ fill,
834
+ type: fill,
835
+ press,
836
+ pressEnter: pressEnter2,
837
+ scroll,
838
+ scrollToEnd,
839
+ scrollToTop
840
+ };
841
+ async function click(ctx, selectorOrPoint, options = {}) {
842
+ if (isPoint(selectorOrPoint)) {
843
+ await Device.tapAbsolute(ctx, selectorOrPoint.x, selectorOrPoint.y);
844
+ return { point: selectorOrPoint };
845
+ }
846
+ if (isBounds(selectorOrPoint)) {
847
+ const point2 = centerOf(selectorOrPoint);
848
+ await Device.tapAbsolute(ctx, point2.x, point2.y);
849
+ return { point: point2 };
850
+ }
851
+ const target = await DeviceView.find(ctx, selectorOrPoint, options);
852
+ const actual = nearestClickable(target);
853
+ if (!actual) {
854
+ throw new CrawlerError({
855
+ message: `automation_failed: View \u4E0D\u53EF\u70B9\u51FB ${JSON.stringify(selectorOrPoint)}`,
856
+ code: Code.AutomationFailed,
857
+ context: { selector: selectorOrPoint, target: simplifyNode(target) }
858
+ });
859
+ }
860
+ const point = centerOf(actual.bounds);
861
+ Logger.info("DeviceInput.click", {
862
+ selector: selectorOrPoint,
863
+ target: simplifyNode(target),
864
+ actual: simplifyNode(actual),
865
+ point
866
+ });
867
+ await Device.tapAbsolute(ctx, point.x, point.y);
868
+ await sleep(Number(options.settleMs || 250));
869
+ return { target, actual, point };
870
+ }
871
+ async function fill(ctx, selector, text2, options = {}) {
872
+ await click(ctx, selector, options);
873
+ await Device.typeText(ctx, text2);
874
+ await sleep(Number(options.settleMs || 350));
875
+ }
876
+ async function press(ctx, key) {
877
+ await Device.pressKey(ctx, key);
878
+ }
879
+ async function pressEnter2(ctx) {
880
+ await Device.pressEnter(ctx);
881
+ }
882
+ async function scroll(ctx, selector, direction, options = {}) {
883
+ const node = await DeviceView.find(ctx, selector, options);
884
+ const box = node.bounds;
885
+ const ratio = Number(options.swipeRatio || 0.72);
886
+ const durationMs = Number(options.durationMs || 360);
887
+ const x = box.centerX;
888
+ const distance = Math.max(40, box.height * ratio);
889
+ const down = String(direction || "").toLowerCase() === "down";
890
+ const from = {
891
+ x,
892
+ y: down ? box.top + box.height * 0.25 : box.bottom - box.height * 0.25
893
+ };
894
+ const to = {
895
+ x,
896
+ y: down ? Math.min(box.bottom - 10, from.y + distance) : Math.max(box.top + 10, from.y - distance)
897
+ };
898
+ await Device.swipe(ctx, from, to, durationMs);
899
+ await sleep(Number(options.settleMs || 500));
900
+ }
901
+ async function scrollToEnd(ctx, selector, options = {}) {
902
+ return scrollUntilStable(ctx, selector, "up", options);
903
+ }
904
+ async function scrollToTop(ctx, selector, options = {}) {
905
+ return scrollUntilStable(ctx, selector, "down", options);
906
+ }
907
+ async function scrollUntilStable(ctx, selector, direction, options = {}) {
908
+ const maxSwipes = Math.max(1, Number(options.maxSwipes || 20));
909
+ const stableRoundsTarget = Math.max(1, Number(options.stableRounds || 2));
910
+ let lastHash = "";
911
+ let stableRounds = 0;
912
+ for (let index = 0; index < maxSwipes; index += 1) {
913
+ const node = await DeviceView.find(ctx, selector, options);
914
+ const currentHash = DeviceView.hashNode(node);
915
+ if (currentHash === lastHash) {
916
+ stableRounds += 1;
917
+ if (stableRounds >= stableRoundsTarget) {
918
+ return { swipes: index, stableRounds, hash: currentHash };
919
+ }
920
+ } else {
921
+ stableRounds = 0;
922
+ lastHash = currentHash;
923
+ }
924
+ await scroll(ctx, selector, direction, options);
925
+ }
926
+ return { swipes: maxSwipes, stableRounds, hash: lastHash };
927
+ }
928
+ function nearestClickable(node) {
929
+ let current = node;
930
+ while (current) {
931
+ if (current.visible && current.enabled && current.clickable) return current;
932
+ current = current.parent;
933
+ }
934
+ return null;
935
+ }
936
+ function isPoint(value) {
937
+ return value && typeof value === "object" && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
938
+ }
939
+ function isBounds(value) {
940
+ return value && typeof value === "object" && Number.isFinite(Number(value.left)) && Number.isFinite(Number(value.top)) && Number.isFinite(Number(value.right)) && Number.isFinite(Number(value.bottom));
941
+ }
942
+ function centerOf(bounds2) {
943
+ return {
944
+ x: (Number(bounds2.left) + Number(bounds2.right)) / 2,
945
+ y: (Number(bounds2.top) + Number(bounds2.bottom)) / 2
946
+ };
947
+ }
948
+ function simplifyNode(node) {
949
+ if (!node) return null;
950
+ return {
951
+ id: node.id,
952
+ resourceId: node.resourceId,
953
+ text: node.text,
954
+ contentDesc: node.contentDesc,
955
+ className: node.className,
956
+ clickable: node.clickable,
957
+ enabled: node.enabled,
958
+ bounds: node.bounds
959
+ };
960
+ }
961
+
962
+ // src/internals/frida-script.js
963
+ import { spawn } from "node:child_process";
964
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
965
+ import { tmpdir } from "node:os";
966
+ import path from "node:path";
967
+ async function runFridaScriptInternal(ctx, source, options = {}) {
968
+ await assertFridaReady(ctx);
969
+ const label = String(options.label || "frida-script");
970
+ const marker = String(options.marker || "ANDROID_TOOLKIT_SCRIPT_JSON ");
971
+ const pid = await resolvePid(ctx, options.packageName || ctx.packageName);
972
+ const scriptPath = writeTempScript(source, label);
973
+ const args = [
974
+ "-q",
975
+ "-t",
976
+ String(Number(options.fridaTimeoutSeconds || 8)),
977
+ "-D",
978
+ ctx.serial,
979
+ "-p",
980
+ pid,
981
+ "-l",
982
+ scriptPath
983
+ ];
984
+ Logger.info("frida script start", { label, pid, timeoutMs: Number(options.timeoutMs || 12e3) });
985
+ try {
986
+ return await runFridaProcess(ctx.fridaPath, args, {
987
+ marker,
988
+ timeoutMs: Number(options.timeoutMs || 12e3),
989
+ maxLines: Number(options.maxLines || 1500)
990
+ });
991
+ } finally {
992
+ rmSync(path.dirname(scriptPath), { recursive: true, force: true });
993
+ }
994
+ }
995
+ async function resolveFridaPid(ctx, packageName = ctx.packageName) {
996
+ return resolvePid(ctx, packageName);
997
+ }
998
+ async function assertFridaReady(ctx) {
999
+ if (!ctx?.fridaPath) {
1000
+ throw new CrawlerError({
1001
+ message: "frida_unavailable: ANDROID_TOOLKIT_FRIDA_PATH is required",
1002
+ code: Code.FridaUnavailable
1003
+ });
1004
+ }
1005
+ if (!ctx?.serial) {
1006
+ throw new CrawlerError({
1007
+ message: "frida_unavailable: device serial is required",
1008
+ code: Code.FridaUnavailable
1009
+ });
1010
+ }
1011
+ }
1012
+ async function resolvePid(ctx, packageName = ctx.packageName) {
1013
+ const target = String(packageName || "").trim();
1014
+ if (!target) {
1015
+ throw new CrawlerError({
1016
+ message: "frida_unavailable: packageName is required",
1017
+ code: Code.FridaUnavailable
1018
+ });
1019
+ }
1020
+ const out = await Device.adbShell(ctx, ["pidof", target], { timeoutMs: 8e3 }).catch(() => "");
1021
+ const pid = String(out || "").trim().split(/\s+/).find(Boolean);
1022
+ if (!pid) {
1023
+ throw new CrawlerError({
1024
+ message: `frida_unavailable: target app pid not found ${target}`,
1025
+ code: Code.FridaUnavailable,
1026
+ context: { packageName: target }
1027
+ });
1028
+ }
1029
+ return pid;
1030
+ }
1031
+ function runFridaProcess(fridaPath, args, options) {
1032
+ return new Promise((resolve, reject) => {
1033
+ const events = [];
1034
+ const lines = [];
1035
+ let buffer = "";
1036
+ let finished = false;
1037
+ const child = spawn(fridaPath, args, { stdio: ["ignore", "pipe", "pipe"] });
1038
+ const timer = setTimeout(() => {
1039
+ if (finished) return;
1040
+ finished = true;
1041
+ child.kill("SIGTERM");
1042
+ reject(new CrawlerError({
1043
+ message: `frida_unavailable: frida script timeout ${options.timeoutMs}ms`,
1044
+ code: Code.FridaUnavailable,
1045
+ context: { lines: lines.slice(-40) }
1046
+ }));
1047
+ }, options.timeoutMs);
1048
+ const consumeLine = (line) => {
1049
+ const text2 = String(line || "").trim();
1050
+ if (!text2) return;
1051
+ lines.push(text2);
1052
+ if (lines.length > options.maxLines) lines.shift();
1053
+ const markerIndex = text2.indexOf(options.marker);
1054
+ if (markerIndex < 0) return;
1055
+ const jsonText = text2.slice(markerIndex + options.marker.length).trim();
1056
+ try {
1057
+ events.push(JSON.parse(jsonText));
1058
+ } catch (error) {
1059
+ events.push({ type: "parse_error", error: error?.message || String(error), line: text2 });
1060
+ }
1061
+ };
1062
+ const onChunk = (chunk) => {
1063
+ buffer += chunk.toString("utf8");
1064
+ let index = buffer.indexOf("\n");
1065
+ while (index >= 0) {
1066
+ consumeLine(buffer.slice(0, index));
1067
+ buffer = buffer.slice(index + 1);
1068
+ index = buffer.indexOf("\n");
1069
+ }
1070
+ };
1071
+ child.stdout.on("data", onChunk);
1072
+ child.stderr.on("data", onChunk);
1073
+ child.on("error", (error) => {
1074
+ if (finished) return;
1075
+ finished = true;
1076
+ clearTimeout(timer);
1077
+ reject(new CrawlerError({
1078
+ message: `frida_unavailable: ${error?.message || String(error)}`,
1079
+ code: Code.FridaUnavailable
1080
+ }));
1081
+ });
1082
+ child.on("close", () => {
1083
+ if (finished) return;
1084
+ finished = true;
1085
+ clearTimeout(timer);
1086
+ if (buffer.trim()) consumeLine(buffer);
1087
+ const parsed = events.filter((event) => event?.type !== "parse_error");
1088
+ if (parsed.length === 0) {
1089
+ reject(new CrawlerError({
1090
+ message: "frida_unavailable: script emitted no event",
1091
+ code: Code.FridaUnavailable,
1092
+ context: { lines: lines.slice(-40), events }
1093
+ }));
1094
+ return;
1095
+ }
1096
+ resolve(parsed.at(-1));
1097
+ });
1098
+ });
1099
+ }
1100
+ function writeTempScript(source, label) {
1101
+ const dir = mkdtempSync(path.join(tmpdir(), `android-toolkit-${safeName(label)}-`));
1102
+ const scriptPath = path.join(dir, "script.js");
1103
+ writeFileSync(scriptPath, String(source || ""), "utf8");
1104
+ return scriptPath;
1105
+ }
1106
+ function safeName(value) {
1107
+ return String(value || "script").replace(/[^a-z0-9_-]+/gi, "-").slice(0, 80);
1108
+ }
1109
+
1110
+ // src/frida-client.js
1111
+ var Frida = {
1112
+ querySQLite,
1113
+ health
1114
+ };
1115
+ async function querySQLite(ctx, options = {}) {
1116
+ if (!options.sql) {
1117
+ throw new CrawlerError({
1118
+ message: "invalid_request: sql is required",
1119
+ code: Code.InvalidRequest
1120
+ });
1121
+ }
1122
+ const config = {
1123
+ dbPath: String(options.dbPath || ""),
1124
+ dbDir: String(options.dbDir || ""),
1125
+ dbNamePrefix: String(options.dbNamePrefix || ""),
1126
+ dbNameIncludes: String(options.dbNameIncludes || ""),
1127
+ dbNameExcludes: normalizeStringArray(options.dbNameExcludes),
1128
+ sql: String(options.sql || ""),
1129
+ args: normalizeStringArray(options.args),
1130
+ maxRows: Math.max(1, Number(options.maxRows || 200))
1131
+ };
1132
+ const event = await runFridaScriptInternal(ctx, sqliteScript(config), {
1133
+ label: options.label || "query-sqlite",
1134
+ packageName: options.packageName || ctx.packageName,
1135
+ timeoutMs: options.timeoutMs || 3e4,
1136
+ fridaTimeoutSeconds: options.fridaTimeoutSeconds || 25,
1137
+ maxLines: options.maxLines || 4e3
1138
+ });
1139
+ if (!event.ok) {
1140
+ throw new CrawlerError({
1141
+ message: `content_unavailable: sqlite query failed ${event.error || ""}`.trim(),
1142
+ code: Code.ContentUnavailable,
1143
+ context: event
1144
+ });
1145
+ }
1146
+ return event;
1147
+ }
1148
+ async function health(ctx) {
1149
+ if (!ctx?.fridaPath) {
1150
+ throw new CrawlerError({
1151
+ message: "frida_unavailable: ANDROID_TOOLKIT_FRIDA_PATH is required",
1152
+ code: Code.FridaUnavailable
1153
+ });
1154
+ }
1155
+ if (!ctx?.serial) {
1156
+ throw new CrawlerError({
1157
+ message: "frida_unavailable: device serial is required",
1158
+ code: Code.FridaUnavailable
1159
+ });
1160
+ }
1161
+ const pid = await resolveFridaPid(ctx, ctx.packageName);
1162
+ return { ok: true, pid };
1163
+ }
1164
+ function sqliteScript(config) {
1165
+ return `
1166
+ Java.perform(function () {
1167
+ function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
1168
+ var config = ${JSON.stringify(config)};
1169
+ var JavaString = Java.use('java.lang.String');
1170
+ function safeString(cursor, index) {
1171
+ try { return cursor.isNull(index) ? '' : String(cursor.getString(index)); } catch (_) { return ''; }
1172
+ }
1173
+ function stringArray(values) {
1174
+ if (!values || values.length === 0) return null;
1175
+ var out = [];
1176
+ for (var i = 0; i < values.length; i++) out.push(String(values[i]));
1177
+ return Java.array('java.lang.String', out);
1178
+ }
1179
+ function matchesName(name) {
1180
+ if (config.dbNamePrefix && name.indexOf(config.dbNamePrefix) !== 0) return false;
1181
+ if (config.dbNameIncludes && name.indexOf(config.dbNameIncludes) < 0) return false;
1182
+ for (var i = 0; i < config.dbNameExcludes.length; i++) {
1183
+ if (String(name).indexOf(config.dbNameExcludes[i]) >= 0) return false;
1184
+ }
1185
+ return true;
1186
+ }
1187
+ function dbPaths() {
1188
+ if (config.dbPath) return [config.dbPath];
1189
+ var File = Java.use('java.io.File');
1190
+ var dir = File.$new(config.dbDir);
1191
+ var files = dir.listFiles();
1192
+ var out = [];
1193
+ if (!files) return out;
1194
+ for (var i = 0; i < files.length; i++) {
1195
+ var name = String(files[i].getName());
1196
+ if (matchesName(name)) out.push(String(files[i].getAbsolutePath()));
1197
+ }
1198
+ return out;
1199
+ }
1200
+ function queryOne(dbPath) {
1201
+ var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase');
1202
+ var db = SQLiteDatabase.openDatabase(JavaString.$new(dbPath), null, 1);
1203
+ try {
1204
+ var cursor = db.rawQuery(JavaString.$new(config.sql), stringArray(config.args));
1205
+ var columns = [];
1206
+ var columnCount = cursor.getColumnCount();
1207
+ for (var c = 0; c < columnCount; c++) columns.push(String(cursor.getColumnName(c)));
1208
+ var rows = [];
1209
+ var truncated = false;
1210
+ while (cursor.moveToNext()) {
1211
+ if (rows.length >= config.maxRows) { truncated = true; break; }
1212
+ var row = {};
1213
+ for (var i = 0; i < columns.length; i++) row[columns[i]] = safeString(cursor, i);
1214
+ rows.push(row);
1215
+ }
1216
+ cursor.close();
1217
+ return { dbPath: dbPath, columns: columns, rows: rows, rowCount: rows.length, truncated: truncated };
1218
+ } finally {
1219
+ db.close();
1220
+ }
1221
+ }
1222
+ try {
1223
+ var paths = dbPaths();
1224
+ if (paths.length === 0) { emit({ ok: false, error: 'sqlite database not found', config: config }); return; }
1225
+ var databases = [];
1226
+ for (var d = 0; d < paths.length; d++) databases.push(queryOne(paths[d]));
1227
+ emit({ ok: true, databases: databases, rows: databases.length === 1 ? databases[0].rows : [] });
1228
+ } catch (error) {
1229
+ emit({ ok: false, error: String(error), stack: String(error.stack || '') });
1230
+ }
1231
+ });
1232
+ `;
1233
+ }
1234
+ function normalizeStringArray(value) {
1235
+ if (Array.isArray(value)) return value.map((item) => String(item ?? ""));
1236
+ if (value == null) return [];
1237
+ return [String(value)];
1238
+ }
1239
+
1240
+ // src/launch.js
1241
+ import fs2 from "node:fs";
1242
+
1243
+ // src/mutation.js
1244
+ var MUTATION_MONITOR_MODE = Object.freeze({
1245
+ Added: "added",
1246
+ Changed: "changed",
1247
+ All: "all"
1248
+ });
1249
+ var Mutation = {
1250
+ Mode: MUTATION_MONITOR_MODE,
1251
+ waitForStable,
1252
+ waitForStableAcrossRoots,
1253
+ useMonitor
1254
+ };
1255
+ async function waitForStable(ctx, selectors, options = {}) {
1256
+ const selectorList = normalizeSelectors(selectors);
1257
+ const initialTimeout = Math.max(0, Number(options.initialTimeout ?? 3e4));
1258
+ const stableTime = Math.max(0, Number(options.stableTime ?? 5e3));
1259
+ const timeout = Math.max(0, Number(options.timeout ?? 12e4));
1260
+ const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs ?? 500));
1261
+ const onMutation = typeof options.onMutation === "function" ? options.onMutation : null;
1262
+ const startedAt = Date.now();
1263
+ const deadline = timeout === 0 ? Infinity : startedAt + timeout;
1264
+ const initialDeadline = initialTimeout === 0 ? startedAt : startedAt + initialTimeout;
1265
+ Logger.start("Mutation.waitForStable", {
1266
+ selectors: selectorList.map(selectorLabel2).join(","),
1267
+ stableTime,
1268
+ timeout,
1269
+ pollIntervalMs
1270
+ });
1271
+ let foundInitial = false;
1272
+ let lastSnapshot = null;
1273
+ let stableSince = 0;
1274
+ let mutationCount = 0;
1275
+ let wasPaused = false;
1276
+ while (Date.now() < deadline) {
1277
+ const snapshot2 = await captureSnapshot(ctx, selectorList, options);
1278
+ if (!foundInitial) {
1279
+ if (snapshot2.found || Date.now() >= initialDeadline) {
1280
+ foundInitial = true;
1281
+ if (!snapshot2.found) Logger.warn("Mutation.waitForStable \u521D\u59CB\u7B49\u5F85\u672A\u627E\u5230\u8282\u70B9", { initialTimeout });
1282
+ } else {
1283
+ await sleep(pollIntervalMs);
1284
+ continue;
1285
+ }
1286
+ }
1287
+ const changed = !lastSnapshot || snapshot2.hash !== lastSnapshot.hash;
1288
+ if (changed) {
1289
+ if (lastSnapshot) mutationCount += 1;
1290
+ lastSnapshot = snapshot2;
1291
+ }
1292
+ let paused = false;
1293
+ if ((changed || !stableSince) && onMutation) {
1294
+ const signal = await onMutation({
1295
+ mutationCount,
1296
+ html: snapshot2.html,
1297
+ text: snapshot2.text,
1298
+ mutationNodes: snapshot2.mutationNodes
1299
+ });
1300
+ paused = !(signal === null || signal === void 0);
1301
+ wasPaused = wasPaused || paused;
1302
+ }
1303
+ if (paused) {
1304
+ stableSince = 0;
1305
+ } else if (changed || !stableSince) {
1306
+ stableSince = Date.now();
1307
+ } else if (Date.now() - stableSince >= stableTime) {
1308
+ const result = {
1309
+ mutationCount,
1310
+ stableTime,
1311
+ wasPaused,
1312
+ html: snapshot2.html,
1313
+ text: snapshot2.text,
1314
+ mutationNodes: snapshot2.mutationNodes
1315
+ };
1316
+ Logger.success("Mutation.waitForStable", {
1317
+ duration: Logger.duration(startedAt),
1318
+ mutationCount,
1319
+ wasPaused
1320
+ });
1321
+ return result;
1322
+ }
1323
+ await sleep(pollIntervalMs);
1324
+ }
1325
+ throw new Error(`Mutation.waitForStable \u8D85\u65F6 (${timeout}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316`);
1326
+ }
1327
+ async function waitForStableAcrossRoots(ctx, selectors, options = {}) {
1328
+ return waitForStable(ctx, selectors, options);
1329
+ }
1330
+ async function useMonitor(ctx, selectors, options = {}) {
1331
+ const selectorList = normalizeSelectors(selectors);
1332
+ const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs ?? 500));
1333
+ const onMutation = typeof options.onMutation === "function" ? options.onMutation : null;
1334
+ let stopped = false;
1335
+ let totalMutations = 0;
1336
+ let lastHash = "";
1337
+ const loop = async () => {
1338
+ while (!stopped) {
1339
+ const snapshot2 = await captureSnapshot(ctx, selectorList, options).catch(() => null);
1340
+ if (snapshot2 && snapshot2.hash !== lastHash) {
1341
+ if (lastHash) totalMutations += 1;
1342
+ lastHash = snapshot2.hash;
1343
+ if (onMutation) {
1344
+ await onMutation({
1345
+ mutationCount: totalMutations,
1346
+ html: snapshot2.html,
1347
+ text: snapshot2.text,
1348
+ mutationNodes: snapshot2.mutationNodes
1349
+ });
1350
+ }
1351
+ }
1352
+ await sleep(pollIntervalMs);
1353
+ }
1354
+ };
1355
+ loop();
1356
+ return {
1357
+ stop: async () => {
1358
+ stopped = true;
1359
+ return { totalMutations };
1360
+ }
1361
+ };
1362
+ }
1363
+ async function captureSnapshot(ctx, selectors, options = {}) {
1364
+ const view = await DeviceView.snapshot(ctx, options);
1365
+ const matched = selectors.length ? selectors.flatMap((selector) => view.flat.filter((node) => matchesSelector2(node, selector, options))) : view.flat;
1366
+ const unique = uniqueNodes(matched);
1367
+ const mutationNodes = unique.map((node) => ({
1368
+ html: nodeToText(node),
1369
+ text: [node.text, node.contentDesc].filter(Boolean).join(" ").trim(),
1370
+ mutationType: "stable",
1371
+ resourceId: node.resourceId,
1372
+ className: node.className,
1373
+ bounds: node.bounds
1374
+ }));
1375
+ const html = mutationNodes.map((node) => node.html).join("\n");
1376
+ const text2 = mutationNodes.map((node) => node.text).filter(Boolean).join("\n");
1377
+ return {
1378
+ found: unique.length > 0,
1379
+ html,
1380
+ text: text2,
1381
+ mutationNodes,
1382
+ hash: unique.map((node) => DeviceView.hashNode(node)).join("|")
1383
+ };
1384
+ }
1385
+ function normalizeSelectors(selectors) {
1386
+ if (selectors == null) return [];
1387
+ const values = Array.isArray(selectors) ? selectors : [selectors];
1388
+ return values.map(normalizeSelector2).filter(Boolean);
1389
+ }
1390
+ function normalizeSelector2(selector) {
1391
+ if (selector === "*" || selector == null) return null;
1392
+ if (typeof selector === "object") return selector;
1393
+ const text2 = String(selector || "").trim();
1394
+ if (!text2) return null;
1395
+ return { id: text2.replace(/^#/, "") };
1396
+ }
1397
+ function matchesSelector2(node, selector, options) {
1398
+ if (!selector) return true;
1399
+ if (options.visibleOnly !== false && !node.visible) return false;
1400
+ if (selector.id) {
1401
+ const id = String(selector.id || "");
1402
+ if (node.resourceId !== id && node.id !== id && !node.resourceId.endsWith(`:id/${id}`)) return false;
1403
+ }
1404
+ if (selector.text && node.text !== selector.text) return false;
1405
+ if (selector.textContains && !node.text.includes(selector.textContains)) return false;
1406
+ if (selector.contentDesc && node.contentDesc !== selector.contentDesc) return false;
1407
+ return true;
1408
+ }
1409
+ function uniqueNodes(nodes) {
1410
+ const seen = /* @__PURE__ */ new Set();
1411
+ const out = [];
1412
+ for (const node of nodes) {
1413
+ const key = `${node.resourceId}|${node.bounds.left},${node.bounds.top},${node.bounds.right},${node.bounds.bottom}|${node.text}|${node.contentDesc}`;
1414
+ if (seen.has(key)) continue;
1415
+ seen.add(key);
1416
+ out.push(node);
1417
+ }
1418
+ return out;
1419
+ }
1420
+ function nodeToText(node) {
1421
+ return JSON.stringify({
1422
+ resourceId: node.resourceId,
1423
+ text: node.text,
1424
+ contentDesc: node.contentDesc,
1425
+ className: node.className,
1426
+ bounds: node.bounds,
1427
+ children: node.children?.length || 0
1428
+ });
1429
+ }
1430
+ function selectorLabel2(selector) {
1431
+ try {
1432
+ return JSON.stringify(selector);
1433
+ } catch {
1434
+ return String(selector);
1435
+ }
1436
+ }
1437
+
1438
+ // src/share.js
1439
+ import { createHash as createHash2 } from "node:crypto";
1440
+ import { Jimp as Jimp2, JimpMime as JimpMime2 } from "jimp";
1441
+
1442
+ // src/internals/compression.js
1443
+ import { Jimp, JimpMime, ResizeStrategy } from "jimp";
1444
+ var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
1445
+ var DEFAULT_SCREENSHOT_OUTPUT_TYPE = "jpeg";
1446
+ var DEFAULT_SCREENSHOT_QUALITY = 0.72;
1447
+ var DEFAULT_SCREENSHOT_MIN_QUALITY = 0.38;
1448
+ var DEFAULT_SCREENSHOT_MIN_SCALE = 0.25;
1449
+ var SUPPORTED_SCREENSHOT_OUTPUT_TYPES = /* @__PURE__ */ new Set(["jpeg"]);
1450
+ var toPositiveInteger = (value, fallback = 0) => {
1451
+ const number = Math.floor(Number(value) || 0);
1452
+ return number > 0 ? number : fallback;
1453
+ };
1454
+ var normalizeQuality = (value, fallback) => {
1455
+ const number = Number(value);
1456
+ if (!Number.isFinite(number) || number <= 0) return fallback;
1457
+ const normalized = number > 1 ? number / 100 : number;
1458
+ return Math.min(1, Math.max(0.01, normalized));
1459
+ };
1460
+ var normalizeScale = (value, fallback) => {
1461
+ const number = Number(value);
1462
+ if (!Number.isFinite(number) || number <= 0) return fallback;
1463
+ return Math.min(1, Math.max(0.05, number));
1464
+ };
1465
+ var normalizeScreenshotOutputType = (value) => {
1466
+ const raw = String(value || DEFAULT_SCREENSHOT_OUTPUT_TYPE).trim().toLowerCase();
1467
+ const normalized = raw === "jpg" ? "jpeg" : raw;
1468
+ return SUPPORTED_SCREENSHOT_OUTPUT_TYPES.has(normalized) ? normalized : DEFAULT_SCREENSHOT_OUTPUT_TYPE;
1469
+ };
1470
+ var getBase64BytesFromBuffer = (buffer) => Math.ceil(buffer.length / 3) * 4;
1471
+ var toJpegQuality = (value) => Math.round(normalizeQuality(value, DEFAULT_SCREENSHOT_QUALITY) * 100);
1472
+ var resolveImageCompression = (options = {}) => {
1473
+ const explicit = options.compression;
1474
+ const source = explicit && typeof explicit === "object" && !Array.isArray(explicit) ? explicit : {};
1475
+ const enabled = explicit !== false && source.enabled !== false && options.compress !== false;
1476
+ const quality = normalizeQuality(
1477
+ source.quality ?? options.quality,
1478
+ DEFAULT_SCREENSHOT_QUALITY
1479
+ );
1480
+ const minQuality = Math.min(
1481
+ quality,
1482
+ normalizeQuality(source.minQuality ?? options.minQuality, DEFAULT_SCREENSHOT_MIN_QUALITY)
1483
+ );
1484
+ return {
1485
+ enabled,
1486
+ maxBytes: toPositiveInteger(
1487
+ source.maxBytes ?? source.maxBase64Bytes ?? options.maxBytes ?? options.maxBase64Bytes,
1488
+ DEFAULT_SCREENSHOT_MAX_BYTES
1489
+ ),
1490
+ outputType: normalizeScreenshotOutputType(
1491
+ source.type ?? source.outputType ?? options.type ?? options.outputType
1492
+ ),
1493
+ quality,
1494
+ minQuality,
1495
+ minScale: normalizeScale(
1496
+ source.minScale ?? options.minScale,
1497
+ DEFAULT_SCREENSHOT_MIN_SCALE
1498
+ )
1499
+ };
1500
+ };
1501
+ var encodeJpeg = async (sourceImage, compression, scale, quality) => {
1502
+ const width = Math.max(1, Math.round(sourceImage.bitmap.width * scale));
1503
+ const height = Math.max(1, Math.round(sourceImage.bitmap.height * scale));
1504
+ const image = sourceImage.clone();
1505
+ if (scale < 0.999) {
1506
+ image.resize({
1507
+ w: width,
1508
+ h: height,
1509
+ mode: ResizeStrategy.BILINEAR
1510
+ });
1511
+ }
1512
+ const buffer = await image.getBuffer(JimpMime.jpeg, { quality });
1513
+ return {
1514
+ buffer,
1515
+ bytes: getBase64BytesFromBuffer(buffer),
1516
+ width,
1517
+ height,
1518
+ quality,
1519
+ scale: Number(scale.toFixed(3)),
1520
+ format: compression.outputType
1521
+ };
1522
+ };
1523
+ var compressImageBuffer = async (buffer, compression) => {
1524
+ const sourceImage = await Jimp.read(buffer);
1525
+ const maxQuality = toJpegQuality(compression.quality);
1526
+ const minQuality = Math.min(maxQuality, toJpegQuality(compression.minQuality));
1527
+ let quality = maxQuality;
1528
+ let scale = 1;
1529
+ let smallest = null;
1530
+ for (let attempt = 0; attempt < 12; attempt += 1) {
1531
+ const candidate = await encodeJpeg(sourceImage, compression, scale, quality);
1532
+ if (!smallest || candidate.bytes < smallest.bytes) {
1533
+ smallest = candidate;
1534
+ }
1535
+ if (candidate.bytes <= compression.maxBytes) {
1536
+ return { ...candidate, withinLimit: true };
1537
+ }
1538
+ if (quality > minQuality) {
1539
+ quality = Math.max(minQuality, Math.floor(quality * 0.75));
1540
+ continue;
1541
+ }
1542
+ const ratio = Math.sqrt(compression.maxBytes / Math.max(1, candidate.bytes));
1543
+ const nextScale = Math.max(
1544
+ compression.minScale,
1545
+ Math.min(scale * 0.85, scale * ratio * 0.94)
1546
+ );
1547
+ if (nextScale >= scale * 0.99 || scale <= compression.minScale) {
1548
+ break;
1549
+ }
1550
+ scale = nextScale;
1551
+ }
1552
+ const finalCandidate = await encodeJpeg(sourceImage, compression, compression.minScale, minQuality);
1553
+ const fallback = !smallest || finalCandidate.bytes < smallest.bytes ? finalCandidate : smallest;
1554
+ return { ...fallback, withinLimit: fallback.bytes <= compression.maxBytes };
1555
+ };
1556
+ var compressImageBufferToBase64 = async (buffer, compression) => {
1557
+ const originalBytes = getBase64BytesFromBuffer(buffer);
1558
+ if (!compression.enabled || originalBytes <= compression.maxBytes) {
1559
+ return buffer.toString("base64");
1560
+ }
1561
+ const result = await compressImageBuffer(buffer, compression).catch((error) => {
1562
+ Logger.warn("captureScreen \u538B\u7F29\u5931\u8D25\uFF0C\u8FD4\u56DE\u539F\u56FE", { message: error?.message || String(error) });
1563
+ return null;
1564
+ });
1565
+ if (!result?.buffer) {
1566
+ return buffer.toString("base64");
1567
+ }
1568
+ if (result.withinLimit) {
1569
+ Logger.info("captureScreen \u5DF2\u538B\u7F29", {
1570
+ originalBytes,
1571
+ outputBytes: result.bytes,
1572
+ format: result.format,
1573
+ quality: result.quality,
1574
+ scale: result.scale,
1575
+ size: `${result.width}x${result.height}`
1576
+ });
1577
+ } else {
1578
+ Logger.warn("captureScreen \u538B\u7F29\u540E\u4ECD\u8D85\u8FC7\u76EE\u6807", {
1579
+ originalBytes,
1580
+ outputBytes: result.bytes,
1581
+ maxBytes: compression.maxBytes,
1582
+ format: result.format,
1583
+ quality: result.quality,
1584
+ scale: result.scale
1585
+ });
1586
+ }
1587
+ return result.buffer.toString("base64");
1588
+ };
1589
+
1590
+ // src/share.js
1591
+ var DEFAULT_CAPTURE_COUNT = 9;
1592
+ var DEFAULT_COLUMNS = 3;
1593
+ var DEFAULT_SETTLE_MS = 550;
1594
+ var DEFAULT_TIMEOUT_MS = 5e4;
1595
+ var DEFAULT_POLL_INTERVAL_MS = 800;
1596
+ var Share = {
1597
+ captureScreen,
1598
+ captureLink
1599
+ };
1600
+ async function captureScreen(ctx, options = {}) {
1601
+ const startedAt = Date.now();
1602
+ const frameBuffers = [];
1603
+ const seen = /* @__PURE__ */ new Set();
1604
+ const targetSelector = options.selector || { id: "message_list" };
1605
+ await Device.hideKeyboard(ctx, { attempts: 2, settleMs: 350 }).catch(() => {
1606
+ });
1607
+ await DeviceInput.scrollToTop(ctx, targetSelector, {
1608
+ maxSwipes: Number(options.scrollToTopMaxSwipes || 12),
1609
+ stableRounds: 2,
1610
+ settleMs: 400
1611
+ }).catch((error) => {
1612
+ Logger.warn("captureScreen scrollToTop skipped", { message: error?.message || String(error) });
1613
+ });
1614
+ for (let index = 0; index < DEFAULT_CAPTURE_COUNT; index += 1) {
1615
+ await Device.hideKeyboard(ctx, { attempts: 1, settleMs: 250 }).catch(() => {
1616
+ });
1617
+ await sleep(DEFAULT_SETTLE_MS);
1618
+ const png = await Device.screenshotPng(ctx);
1619
+ const hash = createHash2("sha256").update(png).digest("hex");
1620
+ if (seen.has(hash)) break;
1621
+ seen.add(hash);
1622
+ frameBuffers.push(png);
1623
+ if (index >= DEFAULT_CAPTURE_COUNT - 1) break;
1624
+ await DeviceInput.scroll(ctx, targetSelector, "up", {
1625
+ swipeRatio: 0.72,
1626
+ durationMs: 360,
1627
+ settleMs: DEFAULT_SETTLE_MS
1628
+ }).catch(() => {
1629
+ });
1630
+ }
1631
+ const sprite = await composeSprite(frameBuffers, { columns: DEFAULT_COLUMNS, gap: 16 });
1632
+ const compression = resolveImageCompression(options);
1633
+ const base64 = await compressImageBufferToBase64(sprite, compression);
1634
+ Logger.success("Share.captureScreen", {
1635
+ duration: Logger.duration(startedAt),
1636
+ frameCount: frameBuffers.length,
1637
+ base64Bytes: Math.ceil(base64.length)
1638
+ });
1639
+ return `data:image/jpeg;base64,${base64}`;
1640
+ }
1641
+ async function captureLink(ctx, options = {}) {
1642
+ const actorInfo = resolveActorInfo(options.actorInfo || options.actorKey || ctx.actorKey);
1643
+ const share = actorInfo?.share || {};
1644
+ if (share.mode !== "clipboard") {
1645
+ throw new CrawlerError({
1646
+ message: `source_extraction_failed: unsupported Android share mode ${share.mode || ""}`,
1647
+ code: Code.SourceExtractionFailed
1648
+ });
1649
+ }
1650
+ const timeoutMs = Number(options.timeoutMs || DEFAULT_TIMEOUT_MS);
1651
+ const pollIntervalMs = Number(options.pollIntervalMs || DEFAULT_POLL_INTERVAL_MS);
1652
+ const deadline = Date.now() + timeoutMs;
1653
+ const prefix = String(share.prefix || "").trim();
1654
+ Logger.start("Share.captureLink", { actor: actorInfo.key, prefix, timeoutMs });
1655
+ await clearClipboard(ctx).catch((error) => {
1656
+ Logger.warn("Share.captureLink clearClipboard failed", { message: error?.message || String(error) });
1657
+ });
1658
+ if (typeof options.performActions === "function") {
1659
+ await options.performActions();
1660
+ }
1661
+ let lastEvent = null;
1662
+ while (Date.now() < deadline) {
1663
+ const event = await readClipboard(ctx).catch((error) => ({ ok: false, error: error?.message || String(error) }));
1664
+ lastEvent = event;
1665
+ const link = selectLink(event, prefix);
1666
+ if (link) {
1667
+ Logger.success("Share.captureLink", { link });
1668
+ return { link, source: event.source || "clipboard", payloadSnapshot: event.payloadSnapshot || "" };
1669
+ }
1670
+ await sleep(pollIntervalMs);
1671
+ }
1672
+ throw new CrawlerError({
1673
+ message: "source_extraction_failed: \u672A\u6355\u83B7\u5206\u4EAB\u94FE\u63A5",
1674
+ code: Code.SourceExtractionFailed,
1675
+ context: { prefix, lastEvent }
1676
+ });
1677
+ }
1678
+ async function clearClipboard(ctx) {
1679
+ await runFridaScriptInternal(ctx, clipboardScript("clear"), {
1680
+ label: "share-clear-clipboard",
1681
+ timeoutMs: 8e3,
1682
+ fridaTimeoutSeconds: 8
1683
+ });
1684
+ }
1685
+ async function readClipboard(ctx) {
1686
+ return runFridaScriptInternal(ctx, clipboardScript("read"), {
1687
+ label: "share-read-clipboard",
1688
+ timeoutMs: 9e3,
1689
+ fridaTimeoutSeconds: 8,
1690
+ maxLines: 1500
1691
+ });
1692
+ }
1693
+ function clipboardScript(mode) {
1694
+ return `
1695
+ Java.perform(function () {
1696
+ function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
1697
+ var mode = ${JSON.stringify(mode)};
1698
+ var JavaObject = Java.use('java.lang.Object');
1699
+ function text(value) {
1700
+ if (value === null || value === undefined) return '';
1701
+ try { return Java.cast(value, JavaObject).toString() + ''; } catch (_) {}
1702
+ try { return value.toString.overload().call(value) + ''; } catch (_) {}
1703
+ try { return value.toString() + ''; } catch (_) {}
1704
+ return '';
1705
+ }
1706
+ function addCandidate(out, source, value) {
1707
+ var s = text(value);
1708
+ if (!s) return;
1709
+ var matches = s.match(/https?:\\/\\/[^\\s"'<>\uFF0C\u3002]+/g) || [];
1710
+ for (var i = 0; i < matches.length; i++) {
1711
+ var link = matches[i].replace(/[)\\].,\uFF0C\u3002\uFF1B;!?\uFF01\uFF1F]+$/g, '');
1712
+ out.push({ source: source, link: link, payload: s.slice(0, 1000) });
1713
+ }
1714
+ }
1715
+ try {
1716
+ var ActivityThread = Java.use('android.app.ActivityThread');
1717
+ var ClipData = Java.use('android.content.ClipData');
1718
+ var app = ActivityThread.currentApplication();
1719
+ var manager = app ? app.getSystemService('clipboard') : null;
1720
+ if (!manager) { emit({ ok: false, error: 'clipboard manager unavailable' }); return; }
1721
+ if (mode === 'clear') {
1722
+ manager.setPrimaryClip(ClipData.newPlainText('android-toolkit', ''));
1723
+ emit({ ok: true, source: 'clipboard.clear' });
1724
+ return;
1725
+ }
1726
+ var candidates = [];
1727
+ var clip = manager.getPrimaryClip();
1728
+ var count = clip ? clip.getItemCount() : 0;
1729
+ for (var i = 0; i < count; i++) {
1730
+ var item = clip.getItemAt(i);
1731
+ try { addCandidate(candidates, 'clipboard.text', item.getText()); } catch (_) {}
1732
+ try { addCandidate(candidates, 'clipboard.html', item.getHtmlText()); } catch (_) {}
1733
+ try { addCandidate(candidates, 'clipboard.uri', item.getUri()); } catch (_) {}
1734
+ try { addCandidate(candidates, 'clipboard.intent', item.getIntent()); } catch (_) {}
1735
+ try { addCandidate(candidates, 'clipboard.coerceToText', item.coerceToText(app)); } catch (_) {}
1736
+ }
1737
+ emit({
1738
+ ok: candidates.length > 0,
1739
+ link: candidates.length > 0 ? candidates[0].link : '',
1740
+ source: candidates.length > 0 ? candidates[0].source : 'clipboard',
1741
+ candidates: candidates,
1742
+ payloadSnapshot: candidates.length > 0 ? String(candidates[0].payload || '').slice(0, 500) : ''
1743
+ });
1744
+ } catch (error) {
1745
+ emit({ ok: false, error: String(error), stack: String(error.stack || '') });
1746
+ }
1747
+ });
1748
+ `;
1749
+ }
1750
+ async function composeSprite(buffers, options = {}) {
1751
+ if (!buffers.length) {
1752
+ throw new CrawlerError({ message: "source_extraction_failed: no screenshots captured", code: Code.SourceExtractionFailed });
1753
+ }
1754
+ const images = [];
1755
+ for (const buffer of buffers) images.push(await Jimp2.read(buffer));
1756
+ const columns = Math.max(1, Number(options.columns || DEFAULT_COLUMNS));
1757
+ const gap = Math.max(0, Number(options.gap || 0));
1758
+ const cellWidth = Math.max(...images.map((image) => image.bitmap.width));
1759
+ const cellHeight = Math.max(...images.map((image) => image.bitmap.height));
1760
+ const rows = Math.ceil(images.length / columns);
1761
+ const width = columns * cellWidth + (columns - 1) * gap;
1762
+ const height = rows * cellHeight + (rows - 1) * gap;
1763
+ const sprite = new Jimp2({ width, height, color: 4294967295 });
1764
+ images.forEach((image, index) => {
1765
+ const x = index % columns * (cellWidth + gap);
1766
+ const y = Math.floor(index / columns) * (cellHeight + gap);
1767
+ sprite.composite(image, x, y);
1768
+ });
1769
+ return sprite.getBuffer(JimpMime2.png);
1770
+ }
1771
+ function resolveActorInfo(value) {
1772
+ if (value && typeof value === "object") return value;
1773
+ const key = String(value || "").trim();
1774
+ const info = ActorInfo[key];
1775
+ if (!info) {
1776
+ throw new CrawlerError({
1777
+ message: `invalid_request: unknown actorInfo ${key}`,
1778
+ code: Code.InvalidRequest
1779
+ });
1780
+ }
1781
+ return info;
1782
+ }
1783
+ function selectLink(event, prefix) {
1784
+ const candidates = [];
1785
+ if (event?.link) candidates.push(event.link);
1786
+ if (Array.isArray(event?.candidates)) {
1787
+ for (const candidate of event.candidates) {
1788
+ if (candidate?.link) candidates.push(candidate.link);
1789
+ }
1790
+ }
1791
+ return candidates.find((link) => !prefix || String(link).startsWith(prefix)) || "";
1792
+ }
1793
+
1794
+ // src/launch.js
1795
+ var DEFAULT_INPUT_PATH = "/apify_storage/input.json";
1796
+ var DEFAULT_OUTPUT_PATH = "/apify_storage/output.json";
1797
+ var Launch = {
1798
+ run
1799
+ };
1800
+ async function run(handler, options = {}) {
1801
+ const startedAt = Date.now();
1802
+ const inputPath = pathOption(options.inputPath, DEFAULT_INPUT_PATH);
1803
+ const outputPath = pathOption(options.outputPath, DEFAULT_OUTPUT_PATH);
1804
+ const input = options.input || JSON.parse(fs2.readFileSync(inputPath, "utf8"));
1805
+ const ctx = options.ctx || Context.createAndroidContext(input, options.contextDefaults || {});
1806
+ ctx.runId = ctx.runId || input.run_id || input.runId || input.runtime?.run_id || input.runtime?.runId || "";
1807
+ ctx.actorKey = ctx.actorKey || options.actorKey || input.actorKey || input.actor_name || input.actorName || "";
1808
+ const apifyKit = await ApifyKit.useApifyKit({
1809
+ reset: true,
1810
+ input,
1811
+ ctx,
1812
+ inputPath,
1813
+ outputPath
1814
+ });
1815
+ const kit = {
1816
+ Launch,
1817
+ ApifyKit: apifyKit,
1818
+ Device,
1819
+ DeviceInput,
1820
+ DeviceView,
1821
+ Frida,
1822
+ Share,
1823
+ Mutation,
1824
+ Logger,
1825
+ Context
1826
+ };
1827
+ try {
1828
+ Logger.start("Launch.run", {
1829
+ inputPath,
1830
+ outputPath,
1831
+ runId: ctx.runId,
1832
+ serial: ctx.serial,
1833
+ packageName: ctx.packageName,
1834
+ queryChars: ctx.query.length
1835
+ });
1836
+ if (typeof options.hooks?.preNavigation === "function") {
1837
+ await options.hooks.preNavigation({ input, ctx, kit, ApifyKit: apifyKit });
1838
+ }
1839
+ await handler({ input, ctx, kit, ApifyKit: apifyKit });
1840
+ if (typeof options.hooks?.postRun === "function") {
1841
+ await options.hooks.postRun({ input, ctx, kit, ApifyKit: apifyKit });
1842
+ }
1843
+ Logger.success("Launch.run", { duration: Logger.duration(startedAt) });
1844
+ } catch (error) {
1845
+ Logger.fail(`Launch.run duration=${Logger.duration(startedAt)}`, error);
1846
+ if (typeof options.hooks?.onError === "function") {
1847
+ await options.hooks.onError(error, { input, ctx, kit, ApifyKit: apifyKit });
1848
+ }
1849
+ if (!apifyKit.hasPushed()) await apifyKit.pushFailed(error);
1850
+ throw error;
1851
+ }
1852
+ }
1853
+ function pathOption(value, fallback) {
1854
+ const clean = String(value || "").trim();
1855
+ return clean || fallback;
1856
+ }
1857
+
1858
+ // entrys/node.js
1859
+ var useAndroidToolKit = () => ({
1860
+ Launch,
1861
+ ApifyKit,
1862
+ DeviceInput,
1863
+ DeviceView,
1864
+ Device,
1865
+ Mutation,
1866
+ Share,
1867
+ Frida,
1868
+ Constants: constants_exports,
1869
+ Errors: errors_exports,
1870
+ Logger,
1871
+ Context,
1872
+ $Internals: {
1873
+ LOG_TEMPLATES,
1874
+ stripAnsi
1875
+ }
1876
+ });
1877
+ export {
1878
+ useAndroidToolKit
1879
+ };
1880
+ //# sourceMappingURL=index.js.map