@bryce3310687963/android-toolkit 0.0.0-stage → 1.0.50

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,3270 @@
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
+ import cycle from "json-cycle";
10
+
11
+ // src/constants.js
12
+ var constants_exports = {};
13
+ __export(constants_exports, {
14
+ ActorInfo: () => ActorInfo,
15
+ ActorKey: () => ActorKey,
16
+ AppiumSettings: () => AppiumSettings,
17
+ Code: () => Code,
18
+ Status: () => Status,
19
+ StoragePath: () => StoragePath,
20
+ UnicodeIme: () => UnicodeIme,
21
+ WebViewBridge: () => WebViewBridge
22
+ });
23
+ var Code = Object.freeze({
24
+ Success: 0,
25
+ EmptySuccess: 1,
26
+ UnknownError: -1,
27
+ NotLogin: 30000001,
28
+ Timeout: 30000003,
29
+ InitialTimeout: 30000004,
30
+ OverallTimeout: 30000005,
31
+ AccountFreeze: 30000006,
32
+ InvalidRequest: 30010001,
33
+ AdbUnavailable: 30010002,
34
+ ContentUnavailable: 30010003,
35
+ SourceExtractionFailed: 30010004,
36
+ AutomationFailed: 30010005,
37
+ AppRuntimeUnavailable: 30010008,
38
+ AppNotInstalled: 30010009
39
+ });
40
+ var Status = Object.freeze({
41
+ Success: "SUCCESS",
42
+ Failed: "FAILED"
43
+ });
44
+ var StoragePath = Object.freeze({
45
+ Input: "/apify_storage/input.json",
46
+ Output: "/apify_storage/output.json"
47
+ });
48
+ var UnicodeIme = Object.freeze({
49
+ packageName: "io.appium.settings",
50
+ component: "io.appium.settings/.UnicodeIME",
51
+ inputAction: "ADB_INPUT_TEXT"
52
+ });
53
+ var AppiumSettings = Object.freeze({
54
+ packageName: "io.appium.settings",
55
+ clipboardReceiver: "io.appium.settings/.receivers.ClipboardReceiver",
56
+ clipboardGetAction: "io.appium.settings.clipboard.get"
57
+ });
58
+ var WebViewBridge = Object.freeze({
59
+ packageName: "com.heartbit.apify.android.bridge",
60
+ actionCommand: "com.heartbit.apify.android.bridge.COMMAND",
61
+ cacheDir: "apify-webview-bridge",
62
+ hookConfigFile: "hook.json",
63
+ commandDir: "commands",
64
+ resultDir: "results"
65
+ });
66
+ var ActorKey = Object.freeze({
67
+ DoubaoAndroid: "doubao.android",
68
+ ErineAndroid: "erine.android",
69
+ WechatAndroid: "wechat.android",
70
+ QwenAndroid: "qwen.android",
71
+ YuanbaoAndroid: "yuanbao.android",
72
+ XhsAndroid: "xhs.android"
73
+ });
74
+ var normalizeShare = (share) => {
75
+ const source = share && typeof share === "object" ? share : {};
76
+ return {
77
+ mode: "clipboard",
78
+ prefix: String(source.prefix || "").trim(),
79
+ xurl: Array.isArray(source.xurl) ? source.xurl : []
80
+ };
81
+ };
82
+ var createActorInfo = (info) => {
83
+ const key = String(info.key || "").trim();
84
+ return Object.freeze({
85
+ key,
86
+ name: String(info.name || key),
87
+ icon: String(info.icon || `https://static.heartbitai.com/general/actors/${key}.png`),
88
+ webviews: Object.freeze(Array.isArray(info.webviews) ? info.webviews : []),
89
+ share: Object.freeze(normalizeShare(info.share))
90
+ });
91
+ };
92
+ var ActorInfo = Object.freeze({
93
+ [ActorKey.DoubaoAndroid]: createActorInfo({
94
+ key: ActorKey.DoubaoAndroid,
95
+ name: "\u8C46\u5305 Android",
96
+ webviews: [{
97
+ name: [
98
+ "android.webkit.WebView",
99
+ "com.baidu.webkit.sdk.WebView",
100
+ "com.tencent.smtt.sdk.WebView",
101
+ "com.tencent.xweb.WebView",
102
+ "com.tencent.xweb.pinus.PSWebview"
103
+ ],
104
+ value: ["www.doubao.com"]
105
+ }],
106
+ share: {
107
+ prefix: "https://www.doubao.com/thread/",
108
+ xurl: []
109
+ }
110
+ }),
111
+ [ActorKey.ErineAndroid]: createActorInfo({
112
+ key: ActorKey.ErineAndroid,
113
+ name: "\u6587\u5C0F\u8A00 Android",
114
+ webviews: [{
115
+ name: [
116
+ "android.webkit.WebView",
117
+ "com.baidu.webkit.sdk.WebView",
118
+ "com.tencent.smtt.sdk.WebView",
119
+ "com.tencent.xweb.WebView",
120
+ "com.tencent.xweb.pinus.PSWebview"
121
+ ],
122
+ value: ["yiyanapp.baidu.com"]
123
+ }],
124
+ share: {
125
+ prefix: "https://mr.baidu.com/r/",
126
+ xurl: []
127
+ }
128
+ }),
129
+ [ActorKey.WechatAndroid]: createActorInfo({
130
+ key: ActorKey.WechatAndroid,
131
+ name: "\u5FAE\u4FE1 Android",
132
+ webviews: [{
133
+ name: [
134
+ "android.webkit.WebView",
135
+ "com.baidu.webkit.sdk.WebView",
136
+ "com.tencent.smtt.sdk.WebView",
137
+ "com.tencent.xweb.WebView",
138
+ "com.tencent.xweb.pinus.PSWebview"
139
+ ],
140
+ value: ["websearch/template"]
141
+ }],
142
+ share: {
143
+ prefix: "",
144
+ xurl: []
145
+ }
146
+ }),
147
+ [ActorKey.QwenAndroid]: createActorInfo({
148
+ key: ActorKey.QwenAndroid,
149
+ name: "\u5343\u95EE Android",
150
+ webviews: [{
151
+ name: [
152
+ "android.webkit.WebView",
153
+ "com.baidu.webkit.sdk.WebView",
154
+ "com.tencent.smtt.sdk.WebView",
155
+ "com.tencent.xweb.WebView",
156
+ "com.tencent.xweb.pinus.PSWebview"
157
+ ],
158
+ value: ["www.qianwen.com", "chat2.qianwen.com"]
159
+ }],
160
+ share: {
161
+ prefix: "",
162
+ xurl: []
163
+ }
164
+ }),
165
+ [ActorKey.XhsAndroid]: createActorInfo({
166
+ key: ActorKey.XhsAndroid,
167
+ name: "\u5C0F\u7EA2\u4E66 Android",
168
+ webviews: [{
169
+ name: [
170
+ "android.webkit.WebView",
171
+ "com.baidu.webkit.sdk.WebView",
172
+ "com.tencent.smtt.sdk.WebView",
173
+ "com.tencent.xweb.WebView",
174
+ "com.tencent.xweb.pinus.PSWebview"
175
+ ],
176
+ value: ["xiaohongshu.com", "xhslink.com"]
177
+ }],
178
+ share: {
179
+ prefix: "",
180
+ xurl: []
181
+ }
182
+ }),
183
+ [ActorKey.YuanbaoAndroid]: createActorInfo({
184
+ key: ActorKey.YuanbaoAndroid,
185
+ name: "\u5143\u5B9D Android",
186
+ webviews: [{
187
+ name: [
188
+ "android.webkit.WebView",
189
+ "com.baidu.webkit.sdk.WebView",
190
+ "com.tencent.smtt.sdk.WebView",
191
+ "com.tencent.xweb.WebView",
192
+ "com.tencent.xweb.pinus.PSWebview"
193
+ ],
194
+ value: ["yuanbao.tencent.com", "hunyuan.tencent.com", "yb.tencent.com"]
195
+ }],
196
+ share: {
197
+ prefix: "https://yb.tencent.com/s/",
198
+ xurl: []
199
+ }
200
+ })
201
+ });
202
+
203
+ // src/context.js
204
+ var Context = {
205
+ createAndroidContext,
206
+ firstNonEmpty,
207
+ requireNonEmpty
208
+ };
209
+ function createAndroidContext(input = {}, defaults = {}) {
210
+ const runtime = input.runtime && typeof input.runtime === "object" ? input.runtime : {};
211
+ const device = runtime.device && typeof runtime.device === "object" ? runtime.device : {};
212
+ return {
213
+ runId: firstNonEmpty(defaults.runId, runtime.runId, input.runId),
214
+ query: String(firstNonEmpty(defaults.query, input.query) || "").trim(),
215
+ serial: firstNonEmpty(defaults.serial, device.serial),
216
+ packageName: firstNonEmpty(defaults.packageName, device.packageName),
217
+ adbPath: firstNonEmpty(defaults.adbPath, process.env.ANDROID_TOOLKIT_ADB_PATH),
218
+ appVersion: firstNonEmpty(defaults.appVersion, device.appVersion),
219
+ slotKey: firstNonEmpty(defaults.slotKey, device.slotKey),
220
+ instanceId: firstNonEmpty(defaults.instanceId, device.instanceId),
221
+ externalIp: firstNonEmpty(defaults.externalIp, device.externalIp)
222
+ };
223
+ }
224
+ function firstNonEmpty(...values) {
225
+ for (const value of values) {
226
+ const clean = String(value ?? "").trim();
227
+ if (clean) return clean;
228
+ }
229
+ return "";
230
+ }
231
+ function requireNonEmpty(value, name) {
232
+ const clean = String(value ?? "").trim();
233
+ if (!clean) throw new Error(`${name} is required`);
234
+ return clean;
235
+ }
236
+
237
+ // src/device.js
238
+ import { execFile } from "node:child_process";
239
+ import { promisify } from "node:util";
240
+
241
+ // src/errors.js
242
+ var errors_exports = {};
243
+ __export(errors_exports, {
244
+ CrawlerError: () => CrawlerError,
245
+ serializeError: () => serializeError
246
+ });
247
+ var CrawlerError = class _CrawlerError extends Error {
248
+ constructor(input = {}, options = {}) {
249
+ const payload = typeof input === "string" ? { message: input } : input;
250
+ super(payload.message || "Android crawler error", options);
251
+ this.name = "CrawlerError";
252
+ this.code = payload.code || Code.UnknownError;
253
+ this.context = payload.context || {};
254
+ }
255
+ static isCrawlerError(error) {
256
+ return error instanceof _CrawlerError || error?.name === "CrawlerError";
257
+ }
258
+ static from(error, fallback = {}) {
259
+ if (_CrawlerError.isCrawlerError(error)) return error;
260
+ return new _CrawlerError({
261
+ message: error?.message || String(error),
262
+ code: fallback.code || Code.UnknownError,
263
+ context: fallback.context || {}
264
+ });
265
+ }
266
+ };
267
+ function serializeError(error) {
268
+ if (!error) return { message: "" };
269
+ return {
270
+ name: error.name || "Error",
271
+ message: error.message || String(error),
272
+ stack: error.stack || "",
273
+ code: error.code || "",
274
+ context: error.context || void 0
275
+ };
276
+ }
277
+
278
+ // src/logger.js
279
+ var defaultLogger = {
280
+ info: (...args) => console.error(...args),
281
+ warning: (...args) => console.error(...args),
282
+ warn: (...args) => console.error(...args),
283
+ error: (...args) => console.error(...args),
284
+ debug: (...args) => console.error(...args)
285
+ };
286
+ var activeLogger = defaultLogger;
287
+ var ANSI = {
288
+ reset: "\x1B[0m",
289
+ gray: "\x1B[90m",
290
+ red: "\x1B[31m",
291
+ green: "\x1B[32m",
292
+ yellow: "\x1B[33m",
293
+ blue: "\x1B[34m",
294
+ cyan: "\x1B[36m"
295
+ };
296
+ var STEP_PREFIX = "\u6B65\u9AA4:";
297
+ var STEP_SEPARATOR = " | ";
298
+ var LOG_TAG_PREFIX = "[#log:";
299
+ var LOG_TAG_SUFFIX = "]";
300
+ var STEP_ICONS = [
301
+ { match: "\u4EFB\u52A1", icon: "\u{1F9ED}" },
302
+ { match: "Launch", icon: "\u{1F680}" },
303
+ { match: "Step", icon: "\u{1F9F1}" },
304
+ { match: "\u8F93\u5165\u6CD5", icon: "\u2328\uFE0F" },
305
+ { match: "ADB", icon: "\u{1F50C}" },
306
+ { match: "Mutation", icon: "\u{1F4E1}" },
307
+ { match: "WebView", icon: "\u{1F310}" },
308
+ { match: "\u622A\u56FE", icon: "\u{1F5BC}\uFE0F" },
309
+ { match: "\u5206\u4EAB", icon: "\u{1F517}" },
310
+ { match: "\u6570\u636E", icon: "\u{1F4E6}" }
311
+ ];
312
+ var STATUS_ICONS = [
313
+ { match: "\u5F00\u59CB", icon: "\u{1F680}" },
314
+ { match: "\u5B8C\u6210", icon: "\u2705" },
315
+ { match: "\u6210\u529F", icon: "\u2705" },
316
+ { match: "\u5931\u8D25", icon: "\u274C" },
317
+ { match: "\u8DF3\u8FC7", icon: "\u23ED\uFE0F" },
318
+ { match: "\u91CD\u8BD5", icon: "\u{1F501}" },
319
+ { match: "\u7A33\u5B9A", icon: "\u{1F7E2}" }
320
+ ];
321
+ var ATTENTION_RANK = {
322
+ low: 1,
323
+ medium: 2,
324
+ high: 3,
325
+ critical: 4
326
+ };
327
+ var DEFAULT_ATTENTION_RANK = ATTENTION_RANK.high;
328
+ var toErrorMessage = (error) => {
329
+ if (!error) return "";
330
+ if (error instanceof Error) return error.message;
331
+ if (typeof error === "string") return error;
332
+ try {
333
+ return JSON.stringify(error);
334
+ } catch {
335
+ return String(error);
336
+ }
337
+ };
338
+ var detailToParts = (detail) => {
339
+ if (detail == null || detail === "") return [];
340
+ if (detail instanceof Error) return [`err=${detail.message}`];
341
+ if (typeof detail === "string") return [detail];
342
+ if (Array.isArray(detail)) return detail.flatMap(detailToParts).filter(Boolean);
343
+ if (typeof detail === "object") {
344
+ return Object.entries(detail).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${key}=${typeof value === "object" ? safeJson(value) : String(value)}`);
345
+ }
346
+ return [String(detail)];
347
+ };
348
+ var LOG_DEFINITIONS = [
349
+ {
350
+ key: "task_start",
351
+ method: "taskStart",
352
+ label: "\u4EFB\u52A1\u5F00\u59CB",
353
+ group: "\u4EFB\u52A1",
354
+ step: "\u4EFB\u52A1",
355
+ status: "\u5F00\u59CB",
356
+ level: "start",
357
+ attention: "low",
358
+ buildDetails: (detail) => detailToParts(detail)
359
+ },
360
+ {
361
+ key: "task_success",
362
+ method: "taskSuccess",
363
+ label: "\u4EFB\u52A1\u5B8C\u6210",
364
+ group: "\u4EFB\u52A1",
365
+ step: "\u4EFB\u52A1",
366
+ status: "\u5B8C\u6210",
367
+ level: "success",
368
+ attention: "high",
369
+ buildDetails: (detail) => detailToParts(detail)
370
+ },
371
+ {
372
+ key: "task_fail",
373
+ method: "taskFail",
374
+ label: "\u4EFB\u52A1\u5931\u8D25",
375
+ group: "\u4EFB\u52A1",
376
+ step: "\u4EFB\u52A1",
377
+ status: "\u5931\u8D25",
378
+ level: "error",
379
+ attention: "critical",
380
+ buildDetails: (detail, error) => [...detailToParts(detail), error ? `err=${toErrorMessage(error)}` : ""]
381
+ },
382
+ {
383
+ key: "launch_start",
384
+ method: "launchStart",
385
+ label: "Launch \u5F00\u59CB",
386
+ group: "Launch",
387
+ step: "Launch.run",
388
+ status: "\u5F00\u59CB",
389
+ level: "start",
390
+ attention: "low",
391
+ buildDetails: (detail) => detailToParts(detail)
392
+ },
393
+ {
394
+ key: "launch_success",
395
+ method: "launchSuccess",
396
+ label: "Launch \u5B8C\u6210",
397
+ group: "Launch",
398
+ step: "Launch.run",
399
+ status: "\u5B8C\u6210",
400
+ level: "success",
401
+ attention: "high",
402
+ buildDetails: (detail) => detailToParts(detail)
403
+ },
404
+ {
405
+ key: "launch_fail",
406
+ method: "launchFail",
407
+ label: "Launch \u5931\u8D25",
408
+ group: "Launch",
409
+ step: "Launch.run",
410
+ status: "\u5931\u8D25",
411
+ level: "error",
412
+ attention: "critical",
413
+ buildDetails: (detail, error) => [...detailToParts(detail), error ? `err=${toErrorMessage(error)}` : ""]
414
+ },
415
+ {
416
+ key: "input_method_snapshot_fail",
417
+ method: "inputMethodSnapshotFail",
418
+ label: "\u8F93\u5165\u6CD5\u5FEB\u7167\u5931\u8D25",
419
+ group: "\u8F93\u5165\u6CD5",
420
+ step: "\u8F93\u5165\u6CD5\u5FEB\u7167",
421
+ status: "\u5931\u8D25",
422
+ level: "warning",
423
+ attention: "medium",
424
+ buildDetails: (error) => [error ? `err=${toErrorMessage(error)}` : ""]
425
+ },
426
+ {
427
+ key: "input_method_restore_fail",
428
+ method: "inputMethodRestoreFail",
429
+ label: "\u8F93\u5165\u6CD5\u6062\u590D\u5931\u8D25",
430
+ group: "\u8F93\u5165\u6CD5",
431
+ step: "\u8F93\u5165\u6CD5\u6062\u590D",
432
+ status: "\u5931\u8D25",
433
+ level: "warning",
434
+ attention: "medium",
435
+ buildDetails: (error) => [error ? `err=${toErrorMessage(error)}` : ""]
436
+ },
437
+ {
438
+ key: "step_start",
439
+ method: "stepStart",
440
+ label: "\u6B65\u9AA4\u5F00\u59CB",
441
+ group: "Step",
442
+ step: "Step",
443
+ status: "\u5F00\u59CB",
444
+ level: "start",
445
+ attention: "low",
446
+ buildDetails: (step, detail) => [step ? `name=${step}` : "", ...detailToParts(detail)]
447
+ },
448
+ {
449
+ key: "step_success",
450
+ method: "stepSuccess",
451
+ label: "\u6B65\u9AA4\u5B8C\u6210",
452
+ group: "Step",
453
+ step: "Step",
454
+ status: "\u5B8C\u6210",
455
+ level: "success",
456
+ attention: "high",
457
+ buildDetails: (step, detail) => [step ? `name=${step}` : "", ...detailToParts(detail)]
458
+ },
459
+ {
460
+ key: "step_fail",
461
+ method: "stepFail",
462
+ label: "\u6B65\u9AA4\u5931\u8D25",
463
+ group: "Step",
464
+ step: "Step",
465
+ status: "\u5931\u8D25",
466
+ level: "error",
467
+ attention: "critical",
468
+ buildDetails: (step, error) => [step ? `name=${step}` : "", error ? `err=${toErrorMessage(error)}` : ""]
469
+ },
470
+ {
471
+ key: "step_retry",
472
+ method: "stepRetry",
473
+ label: "\u6B65\u9AA4\u91CD\u8BD5",
474
+ group: "Step",
475
+ step: "Step",
476
+ status: "\u91CD\u8BD5",
477
+ level: "warning",
478
+ attention: "medium",
479
+ buildDetails: (step, attempt) => [step ? `name=${step}` : "", attempt !== void 0 ? `attempt=${attempt}` : ""]
480
+ },
481
+ {
482
+ key: "adb_exec",
483
+ method: "adbExec",
484
+ label: "ADB \u6267\u884C",
485
+ group: "ADB",
486
+ step: "ADB",
487
+ status: "\u5B8C\u6210",
488
+ level: "debug",
489
+ attention: "low",
490
+ buildDetails: (detail) => detailToParts(detail)
491
+ },
492
+ {
493
+ key: "adb_retry",
494
+ method: "adbRetry",
495
+ label: "ADB \u91CD\u8BD5",
496
+ group: "ADB",
497
+ step: "ADB",
498
+ status: "\u91CD\u8BD5",
499
+ level: "warning",
500
+ attention: "medium",
501
+ buildDetails: (detail) => detailToParts(detail)
502
+ },
503
+ {
504
+ key: "mutation_wait_start",
505
+ method: "mutationWaitStart",
506
+ label: "Mutation \u7B49\u5F85\u5F00\u59CB",
507
+ group: "Mutation",
508
+ step: "Mutation",
509
+ status: "\u5F00\u59CB",
510
+ level: "start",
511
+ attention: "low",
512
+ buildDetails: (detail) => detailToParts(detail)
513
+ },
514
+ {
515
+ key: "mutation_wait_success",
516
+ method: "mutationWaitSuccess",
517
+ label: "Mutation \u7A33\u5B9A",
518
+ group: "Mutation",
519
+ step: "Mutation",
520
+ status: "\u7A33\u5B9A",
521
+ level: "success",
522
+ attention: "medium",
523
+ buildDetails: (detail) => detailToParts(detail)
524
+ },
525
+ {
526
+ key: "mutation_wait_warn",
527
+ method: "mutationWaitWarn",
528
+ label: "Mutation \u7B49\u5F85\u544A\u8B66",
529
+ group: "Mutation",
530
+ step: "Mutation",
531
+ status: "\u8DF3\u8FC7",
532
+ level: "warning",
533
+ attention: "medium",
534
+ buildDetails: (detail) => detailToParts(detail)
535
+ },
536
+ {
537
+ key: "webview_command",
538
+ method: "webViewCommand",
539
+ label: "WebView \u547D\u4EE4",
540
+ group: "WebView",
541
+ step: "WebView",
542
+ status: "\u5F00\u59CB",
543
+ level: "start",
544
+ attention: "low",
545
+ buildDetails: (detail) => detailToParts(detail)
546
+ },
547
+ {
548
+ key: "webview_success",
549
+ method: "webViewSuccess",
550
+ label: "WebView \u5B8C\u6210",
551
+ group: "WebView",
552
+ step: "WebView",
553
+ status: "\u5B8C\u6210",
554
+ level: "success",
555
+ attention: "medium",
556
+ buildDetails: (detail) => detailToParts(detail)
557
+ },
558
+ {
559
+ key: "webview_warn",
560
+ method: "webViewWarn",
561
+ label: "WebView \u544A\u8B66",
562
+ group: "WebView",
563
+ step: "WebView",
564
+ status: "\u8DF3\u8FC7",
565
+ level: "warning",
566
+ attention: "medium",
567
+ buildDetails: (detail) => detailToParts(detail)
568
+ },
569
+ {
570
+ key: "webview_fail",
571
+ method: "webViewFail",
572
+ label: "WebView \u5931\u8D25",
573
+ group: "WebView",
574
+ step: "WebView",
575
+ status: "\u5931\u8D25",
576
+ level: "error",
577
+ attention: "high",
578
+ buildDetails: (error) => [error ? `err=${toErrorMessage(error)}` : ""]
579
+ },
580
+ {
581
+ key: "screenshot_start",
582
+ method: "screenshotStart",
583
+ label: "\u622A\u56FE\u5F00\u59CB",
584
+ group: "\u622A\u56FE",
585
+ step: "\u622A\u56FE",
586
+ status: "\u5F00\u59CB",
587
+ level: "start",
588
+ attention: "low",
589
+ buildDetails: (detail) => detailToParts(detail)
590
+ },
591
+ {
592
+ key: "screenshot_success",
593
+ method: "screenshotDone",
594
+ label: "\u622A\u56FE\u6210\u529F",
595
+ group: "\u622A\u56FE",
596
+ step: "\u622A\u56FE",
597
+ status: "\u6210\u529F",
598
+ level: "success",
599
+ attention: "high",
600
+ buildDetails: (detail) => detailToParts(detail)
601
+ },
602
+ {
603
+ key: "share_start",
604
+ method: "shareStart",
605
+ label: "\u5206\u4EAB\u5F00\u59CB",
606
+ group: "\u5206\u4EAB",
607
+ step: "\u5206\u4EAB",
608
+ status: "\u5F00\u59CB",
609
+ level: "start",
610
+ attention: "low",
611
+ buildDetails: (detail) => detailToParts(detail)
612
+ },
613
+ {
614
+ key: "share_success",
615
+ method: "shareSuccess",
616
+ label: "\u5206\u4EAB\u6210\u529F",
617
+ group: "\u5206\u4EAB",
618
+ step: "\u5206\u4EAB",
619
+ status: "\u6210\u529F",
620
+ level: "success",
621
+ attention: "high",
622
+ buildDetails: (detail) => detailToParts(detail)
623
+ },
624
+ {
625
+ key: "share_fail",
626
+ method: "shareFail",
627
+ label: "\u5206\u4EAB\u5931\u8D25",
628
+ group: "\u5206\u4EAB",
629
+ step: "\u5206\u4EAB",
630
+ status: "\u5931\u8D25",
631
+ level: "warning",
632
+ attention: "high",
633
+ buildDetails: (error) => [error ? `err=${toErrorMessage(error)}` : ""]
634
+ },
635
+ {
636
+ key: "data_push_success",
637
+ method: "dataPush",
638
+ label: "\u6570\u636E\u63A8\u9001\u6210\u529F",
639
+ group: "\u6570\u636E",
640
+ step: "\u6570\u636E\u63A8\u9001",
641
+ status: "\u6210\u529F",
642
+ level: "success",
643
+ attention: "high",
644
+ buildDetails: (detail) => detailToParts(detail)
645
+ },
646
+ {
647
+ key: "data_push_fail",
648
+ method: "dataPushFail",
649
+ label: "\u6570\u636E\u63A8\u9001\u5931\u8D25",
650
+ group: "\u6570\u636E",
651
+ step: "\u6570\u636E\u63A8\u9001",
652
+ status: "\u5931\u8D25",
653
+ level: "error",
654
+ attention: "high",
655
+ buildDetails: (error) => [error ? `err=${toErrorMessage(error)}` : ""]
656
+ }
657
+ ];
658
+ var buildLogTag = (key) => `${LOG_TAG_PREFIX}${key}${LOG_TAG_SUFFIX}`;
659
+ var escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
660
+ var buildStepPattern = (step, status) => {
661
+ if (!step) return null;
662
+ let pattern = `${STEP_PREFIX} .*${escapeRegExp(step)}`;
663
+ if (status) pattern += `.*${escapeRegExp(status)}`;
664
+ return new RegExp(pattern);
665
+ };
666
+ var LOG_TEMPLATES = Object.freeze(LOG_DEFINITIONS.map((definition) => {
667
+ const attention = definition.attention || "medium";
668
+ const attentionRank = ATTENTION_RANK[attention] || ATTENTION_RANK.medium;
669
+ const patterns = [new RegExp(`\\[#log:${escapeRegExp(definition.key)}\\]`)];
670
+ const fallback = buildStepPattern(definition.step, definition.status);
671
+ if (fallback) patterns.push(fallback);
672
+ return {
673
+ key: definition.key,
674
+ label: definition.label,
675
+ group: definition.group,
676
+ attention,
677
+ patterns,
678
+ defaultSelected: attentionRank >= DEFAULT_ATTENTION_RANK
679
+ };
680
+ }));
681
+ var Logger = {
682
+ setLogger(logger) {
683
+ activeLogger = logger || defaultLogger;
684
+ },
685
+ createLogger(scope) {
686
+ const prefix = scope ? String(scope) : "";
687
+ return {
688
+ start: (message, detail) => createBaseLogger(prefix).start(formatMessage(message, detail)),
689
+ success: (message, detail) => createBaseLogger(prefix).success(formatMessage(message, detail)),
690
+ fail: (message, error) => createBaseLogger(prefix).error(formatMessage(message, error?.message || error)),
691
+ warn: (message, detail) => createBaseLogger(prefix).warning(formatMessage(message, detail)),
692
+ warning: (message, detail) => createBaseLogger(prefix).warning(formatMessage(message, detail)),
693
+ info: (message, detail) => createBaseLogger(prefix).info(formatMessage(message, detail)),
694
+ debug: (message, detail) => createBaseLogger(prefix).debug(formatMessage(message, detail)),
695
+ useTemplate: () => createTemplateLogger(createBaseLogger(prefix))
696
+ };
697
+ },
698
+ useTemplate(logger) {
699
+ if (logger) return createTemplateLogger(createBaseLogger("", logger));
700
+ return createTemplateLogger(createBaseLogger(""));
701
+ },
702
+ start(message, detail = "") {
703
+ createBaseLogger("").start(formatMessage(message, detail));
704
+ },
705
+ success(message, detail = "") {
706
+ createBaseLogger("").success(formatMessage(message, detail));
707
+ },
708
+ fail(message, error = "") {
709
+ createBaseLogger("").error(formatMessage(message, error?.message || error));
710
+ },
711
+ warn(message, detail = "") {
712
+ createBaseLogger("").warning(formatMessage(message, detail));
713
+ },
714
+ warning(message, detail = "") {
715
+ Logger.warn(message, detail);
716
+ },
717
+ error(message, detail = "") {
718
+ createBaseLogger("").error(formatMessage(message, detail));
719
+ },
720
+ info(message, detail = "") {
721
+ createBaseLogger("").info(formatMessage(message, detail));
722
+ },
723
+ debug(message, detail = "") {
724
+ createBaseLogger("").debug(formatMessage(message, detail));
725
+ },
726
+ duration(startedAt) {
727
+ return `${((Date.now() - startedAt) / 1e3).toFixed(2)}s`;
728
+ }
729
+ };
730
+ function stripAnsi(value) {
731
+ return String(value || "").replace(/\u001b\[[0-9;]*m/g, "");
732
+ }
733
+ function sleep(ms) {
734
+ return new Promise((resolve) => setTimeout(resolve, ms));
735
+ }
736
+ function createBaseLogger(prefix = "", explicitLogger = null) {
737
+ const name = prefix ? String(prefix) : "";
738
+ const dispatch = (methodName, icon, message, color) => {
739
+ const timestamp = colorize(`[${formatTimestamp()}]`, ANSI.gray);
740
+ const line = formatLine(name, icon, message);
741
+ const coloredLine = colorize(line, color);
742
+ const writer = resolveLogMethod(resolveLogger(explicitLogger), methodName);
743
+ writer(`${timestamp} ${coloredLine}`.trim());
744
+ };
745
+ return {
746
+ info: (message) => dispatch("info", "\u{1F4D6}", message, ANSI.cyan),
747
+ success: (message) => dispatch("info", "\u2705", message, ANSI.green),
748
+ warning: (message) => dispatch("warning", "\u26A0\uFE0F", message, ANSI.yellow),
749
+ warn: (message) => dispatch("warning", "\u26A0\uFE0F", message, ANSI.yellow),
750
+ error: (message) => dispatch("error", "\u274C", message, ANSI.red),
751
+ debug: (message) => dispatch("debug", "\u{1F539}", message, ANSI.gray),
752
+ start: (message) => dispatch("info", "\u{1F537}", message, ANSI.blue)
753
+ };
754
+ }
755
+ function createTemplateLogger(baseLogger) {
756
+ const handlers = {
757
+ info: (line) => baseLogger.info(line),
758
+ success: (line) => baseLogger.success(line),
759
+ warning: (line) => baseLogger.warning(line),
760
+ warn: (line) => baseLogger.warning(line),
761
+ error: (line) => baseLogger.error(line),
762
+ debug: (line) => baseLogger.debug(line),
763
+ start: (line) => baseLogger.start(line)
764
+ };
765
+ const emitDefinition = (definition, details = []) => {
766
+ const handler = handlers[definition.level] || handlers.info;
767
+ handler(buildStepLine(definition.step, definition.status, [...details, buildLogTag(definition.key)]));
768
+ };
769
+ const definitionMethods = {};
770
+ for (const definition of LOG_DEFINITIONS) {
771
+ if (!definition.method) continue;
772
+ definitionMethods[definition.method] = (...args) => {
773
+ const details = definition.buildDetails ? definition.buildDetails(...args) : [];
774
+ emitDefinition(definition, details.filter(Boolean));
775
+ };
776
+ }
777
+ return {
778
+ step: (step, status, details, level = "info") => {
779
+ const handler = handlers[level] || handlers.info;
780
+ handler(buildStepLine(step, status, detailToParts(details)));
781
+ },
782
+ info: (message, detail) => baseLogger.info(formatMessage(message, detail)),
783
+ success: (message, detail) => baseLogger.success(formatMessage(message, detail)),
784
+ warning: (message, detail) => baseLogger.warning(formatMessage(message, detail)),
785
+ warn: (message, detail) => baseLogger.warning(formatMessage(message, detail)),
786
+ error: (message, detail) => baseLogger.error(formatMessage(message, detail)),
787
+ debug: (message, detail) => baseLogger.debug(formatMessage(message, detail)),
788
+ start: (message, detail) => baseLogger.start(formatMessage(message, detail)),
789
+ ...definitionMethods
790
+ };
791
+ }
792
+ function buildStepLine(step, status, details = []) {
793
+ const parts = [];
794
+ const decoratedStep = step ? decorateLabel(step, STEP_ICONS) : "";
795
+ parts.push(decoratedStep ? `${STEP_PREFIX} ${decoratedStep}` : STEP_PREFIX);
796
+ if (status) parts.push(decorateLabel(status, STATUS_ICONS));
797
+ const detailParts = details.flatMap(detailToParts).filter(Boolean);
798
+ parts.push(...detailParts);
799
+ return parts.join(STEP_SEPARATOR);
800
+ }
801
+ function decorateLabel(label, mappings) {
802
+ if (!label) return "";
803
+ const mapping = mappings.find((item) => String(label).includes(item.match));
804
+ if (!mapping) return label;
805
+ return `${mapping.icon} ${label}`;
806
+ }
807
+ function resolveLogger(explicitLogger) {
808
+ if (explicitLogger && typeof explicitLogger.info === "function") return explicitLogger;
809
+ return activeLogger || defaultLogger;
810
+ }
811
+ function resolveLogMethod(logger, name) {
812
+ if (logger && typeof logger[name] === "function") return logger[name].bind(logger);
813
+ if (name === "warning" && logger && typeof logger.warn === "function") return logger.warn.bind(logger);
814
+ return defaultLogger[name] || defaultLogger.info;
815
+ }
816
+ function formatLine(prefix, icon, message) {
817
+ const parts = [];
818
+ if (prefix) parts.push(`[${prefix}]`);
819
+ if (icon) parts.push(icon);
820
+ if (message) parts.push(message);
821
+ return parts.join(" ").trim();
822
+ }
823
+ function formatMessage(message, detail = "") {
824
+ const detailText = formatDetail(detail);
825
+ return detailText ? `${message} ${detailText}` : String(message || "");
826
+ }
827
+ function formatTimestamp(date = /* @__PURE__ */ new Date()) {
828
+ const pad = (value, size = 2) => String(value).padStart(size, "0");
829
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
830
+ }
831
+ function formatDetail(detail) {
832
+ return detailToParts(detail).join(" ");
833
+ }
834
+ function colorize(text2, color) {
835
+ if (!text2 || !color) return text2;
836
+ return `${color}${text2}${ANSI.reset}`;
837
+ }
838
+ function safeJson(value) {
839
+ try {
840
+ return JSON.stringify(value);
841
+ } catch {
842
+ return String(value);
843
+ }
844
+ }
845
+
846
+ // src/device.js
847
+ var execFileAsync = promisify(execFile);
848
+ var DEFAULT_ADB_ATTEMPTS = 3;
849
+ var DEFAULT_ADB_RETRY_DELAY_MS = 500;
850
+ var Device = {
851
+ adbExec,
852
+ adbShell,
853
+ writeCacheFile,
854
+ forceStopApp,
855
+ clearAppCache,
856
+ startActivity,
857
+ startLauncher,
858
+ resolveLauncherActivity,
859
+ focusedActivity,
860
+ waitForActivity,
861
+ waitForForeground,
862
+ screenSize,
863
+ screenDensity,
864
+ tapAbsolute,
865
+ swipe,
866
+ pressKey,
867
+ pressBack,
868
+ pressEnter,
869
+ typeText,
870
+ screenshotPng,
871
+ screenshotBase64,
872
+ dumpUiXml,
873
+ wakeAndUnlock,
874
+ hideKeyboard,
875
+ isKeyboardVisible,
876
+ sleep,
877
+ ensureUnicodeIme,
878
+ currentInputMethod,
879
+ restoreInputMethod
880
+ };
881
+ async function adbExec(ctx, args, options = {}) {
882
+ const adbPath = requireDeviceField(ctx, "adbPath");
883
+ const startedAt = Date.now();
884
+ try {
885
+ const { stdout } = await execAdbWithRetry(adbPath, args, options);
886
+ Logger.useTemplate().adbExec({ duration: Logger.duration(startedAt), args: compactArgs(args) });
887
+ return stdout;
888
+ } catch (error) {
889
+ if (isAdbUnavailableError(error)) {
890
+ throw new CrawlerError({
891
+ message: `adb_unavailable: ${error?.message || String(error)}`,
892
+ code: Code.AdbUnavailable,
893
+ context: { args: compactArgs(args), adbPath }
894
+ });
895
+ }
896
+ throw error;
897
+ }
898
+ }
899
+ async function adbShell(ctx, args, options = {}) {
900
+ const serial = requireDeviceField(ctx, "serial");
901
+ return adbExec(ctx, ["-s", serial, "shell", ...args], options);
902
+ }
903
+ async function writeCacheFile(ctx, options = {}) {
904
+ const dataDir = appDataDir(ctx);
905
+ const dirPath = `${dataDir}/cache/${cleanCachePathName(options.dirName, "dirName")}`;
906
+ const filePath = `${dirPath}/${cleanCachePathName(options.fileName, "fileName")}`;
907
+ const command = [
908
+ `owner=$(stat -c %u:%g ${shellQuoteForRemote(dataDir)})`,
909
+ `mkdir -p ${shellQuoteForRemote(dirPath)}`,
910
+ `printf %s ${shellQuoteForRemote(String(options.text ?? ""))} > ${shellQuoteForRemote(filePath)}`,
911
+ `chown "$owner" ${shellQuoteForRemote(dirPath)} ${shellQuoteForRemote(filePath)}`,
912
+ `chmod 2775 ${shellQuoteForRemote(dirPath)}`,
913
+ `chmod 664 ${shellQuoteForRemote(filePath)}`
914
+ ].join(" && ");
915
+ await Device.adbShell(ctx, [`su -c ${shellQuoteForRemote(command)}`], {
916
+ timeoutMs: Number(options.timeoutMs || 1e4),
917
+ maxBuffer: Number(options.maxBuffer || 1024)
918
+ });
919
+ return filePath;
920
+ }
921
+ async function adbInput(ctx, args, options = {}) {
922
+ return adbShell(ctx, ["input", ...args], options);
923
+ }
924
+ async function forceStopApp(ctx, packageName = ctx.packageName) {
925
+ await adbShell(ctx, ["am", "force-stop", requireValue(packageName, "packageName")], { timeoutMs: 15e3 });
926
+ }
927
+ async function clearAppCache(ctx, packageName = ctx.packageName) {
928
+ const targetPackage = requireValue(packageName, "packageName");
929
+ if (!/^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+$/.test(targetPackage)) {
930
+ throw new CrawlerError({ message: "invalid_request: invalid packageName for cache cleanup", code: Code.InvalidRequest });
931
+ }
932
+ const dataDir = `/data/data/${targetPackage}`;
933
+ const cacheDirs = [`${dataDir}/cache`, `${dataDir}/code_cache`, `/sdcard/Android/data/${targetPackage}/cache`];
934
+ const command = [
935
+ "set -e",
936
+ `test -d ${shellQuoteForRemote(dataDir)}`,
937
+ `for cache_dir in ${cacheDirs.map(shellQuoteForRemote).join(" ")}; do`,
938
+ ' if [ -L "$cache_dir" ]; then echo "refusing symlink cache directory: $cache_dir" >&2; exit 1; fi',
939
+ ' if [ -d "$cache_dir" ]; then',
940
+ ` find "$cache_dir" -mindepth 1 -maxdepth 1 ! -name ${shellQuoteForRemote(WebViewBridge.cacheDir)} ! -name 'network-capture' -exec rm -rf -- {} +`,
941
+ " fi",
942
+ "done"
943
+ ].join("\n");
944
+ await Device.adbShell(ctx, [`su -c ${shellQuoteForRemote(command)}`], {
945
+ timeoutMs: 3e4,
946
+ maxBuffer: 4096
947
+ }).catch((error) => {
948
+ throw CrawlerError.from(error, { code: Code.AutomationFailed, context: { packageName: targetPackage } });
949
+ });
950
+ Logger.useTemplate().info("App \u4E34\u65F6\u7F13\u5B58\u5DF2\u6E05\u7406", { packageName: targetPackage });
951
+ }
952
+ async function startActivity(ctx, component, args = []) {
953
+ const target = requireValue(component, "component");
954
+ const output = await adbShell(ctx, ["am", "start", "-W", "-n", target, ...args], { timeoutMs: 2e4 });
955
+ assertActivityStartOk(output, target);
956
+ await sleep(1500);
957
+ return output;
958
+ }
959
+ async function startLauncher(ctx, packageName = ctx.packageName, launcherActivity) {
960
+ const targetPackage = requireValue(packageName, "packageName");
961
+ const component = launcherActivity ? `${targetPackage}/${requireValue(launcherActivity, "launcherActivity")}` : await resolveLauncherActivity(ctx, targetPackage);
962
+ return startActivity(ctx, component, [
963
+ "-a",
964
+ "android.intent.action.MAIN",
965
+ "-c",
966
+ "android.intent.category.LAUNCHER"
967
+ ]);
968
+ }
969
+ async function resolveLauncherActivity(ctx, packageName = ctx.packageName) {
970
+ const targetPackage = requireValue(packageName, "packageName");
971
+ const output = await adbShell(ctx, [
972
+ "cmd",
973
+ "package",
974
+ "resolve-activity",
975
+ "--brief",
976
+ "-a",
977
+ "android.intent.action.MAIN",
978
+ "-c",
979
+ "android.intent.category.LAUNCHER",
980
+ targetPackage
981
+ ], { timeoutMs: 15e3 });
982
+ const component = parseResolvedLauncherComponent(output, targetPackage);
983
+ if (!component) {
984
+ throw new CrawlerError({
985
+ message: `automation_failed: cannot resolve launcher activity packageName=${targetPackage} output=${compactOutput(output)}`,
986
+ code: Code.AutomationFailed,
987
+ context: { packageName: targetPackage, output: compactOutput(output, 2e3) }
988
+ });
989
+ }
990
+ return component;
991
+ }
992
+ async function focusedActivity(ctx) {
993
+ const resumed = await resumedActivity(ctx).catch(() => "");
994
+ if (resumed) return resumed;
995
+ const out = await adbShell(ctx, ["dumpsys", "window"], { timeoutMs: 15e3, maxBuffer: 12 * 1024 * 1024 });
996
+ for (const line of String(out || "").split("\n")) {
997
+ const trimmed = line.trim();
998
+ if (trimmed.includes("mCurrentFocus=Window") && !trimmed.includes("=null")) return trimmed;
999
+ }
1000
+ for (const line of String(out || "").split("\n")) {
1001
+ const trimmed = line.trim();
1002
+ if (trimmed.includes("mFocusedApp=AppWindowToken") && !trimmed.includes("=null")) return trimmed;
1003
+ }
1004
+ return "";
1005
+ }
1006
+ async function resumedActivity(ctx) {
1007
+ const out = await adbShell(ctx, ["dumpsys", "activity", "activities"], {
1008
+ timeoutMs: 15e3,
1009
+ maxBuffer: 12 * 1024 * 1024
1010
+ });
1011
+ let fallback = "";
1012
+ for (const line of String(out || "").split("\n")) {
1013
+ const trimmed = line.trim();
1014
+ if (!trimmed.includes("ActivityRecord") || !trimmed.includes("/")) continue;
1015
+ if (trimmed.includes("mTopResumedActivity") || trimmed.includes("topResumedActivity") || trimmed.startsWith("ResumedActivity:") || trimmed.startsWith("Resumed:")) {
1016
+ return trimmed;
1017
+ }
1018
+ if (trimmed.includes("mResumedActivity")) fallback = trimmed;
1019
+ }
1020
+ return fallback;
1021
+ }
1022
+ async function waitForActivity(ctx, predicate, timeoutMs = 15e3, intervalMs = 700) {
1023
+ const deadline = Date.now() + Number(timeoutMs);
1024
+ let lastFocused = "";
1025
+ while (Date.now() < deadline) {
1026
+ lastFocused = await focusedActivity(ctx).catch((error) => `ERROR: ${error?.message || String(error)}`);
1027
+ if (predicate(lastFocused)) return lastFocused;
1028
+ await sleep(intervalMs);
1029
+ }
1030
+ throw new CrawlerError({
1031
+ message: `automation_failed: waitForActivity timeout focused=${lastFocused}`,
1032
+ code: Code.AutomationFailed
1033
+ });
1034
+ }
1035
+ async function waitForForeground(ctx, packageName = ctx.packageName, timeoutMs = 6e3) {
1036
+ const target = requireValue(packageName, "packageName");
1037
+ return waitForActivity(ctx, (value) => String(value || "").includes(target), timeoutMs, 500);
1038
+ }
1039
+ async function screenSize(ctx) {
1040
+ const out = await adbShell(ctx, ["wm", "size"], { timeoutMs: 15e3 });
1041
+ const match = /(\d+)x(\d+)/.exec(String(out || ""));
1042
+ if (!match) throw new CrawlerError({ message: `adb_unavailable: cannot parse screen size: ${out}`, code: Code.AdbUnavailable });
1043
+ return { width: Number(match[1]), height: Number(match[2]) };
1044
+ }
1045
+ async function screenDensity(ctx) {
1046
+ const out = await adbShell(ctx, ["wm", "density"], { timeoutMs: 15e3 });
1047
+ const match = /(\d+)/.exec(String(out || ""));
1048
+ if (!match) throw new CrawlerError({ message: `adb_unavailable: cannot parse screen density: ${out}`, code: Code.AdbUnavailable });
1049
+ return Number(match[1]);
1050
+ }
1051
+ async function tapAbsolute(ctx, x, y) {
1052
+ await adbInput(ctx, ["tap", Math.round(Number(x)), Math.round(Number(y))], { timeoutMs: 15e3 });
1053
+ }
1054
+ async function swipe(ctx, from, to, durationMs = 360) {
1055
+ const args = [
1056
+ "swipe",
1057
+ Math.round(Number(from?.x)),
1058
+ Math.round(Number(from?.y)),
1059
+ Math.round(Number(to?.x)),
1060
+ Math.round(Number(to?.y)),
1061
+ Math.round(Number(durationMs))
1062
+ ];
1063
+ await adbInput(ctx, args, { timeoutMs: 3e4 });
1064
+ }
1065
+ async function pressKey(ctx, key) {
1066
+ await adbInput(ctx, ["keyevent", String(key)], { timeoutMs: 15e3 });
1067
+ }
1068
+ async function pressBack(ctx) {
1069
+ await pressKey(ctx, "BACK");
1070
+ }
1071
+ async function pressEnter(ctx) {
1072
+ await pressKey(ctx, "KEYCODE_ENTER");
1073
+ }
1074
+ async function typeText(ctx, text2) {
1075
+ await ensureUnicodeIme(ctx);
1076
+ const value = String(text2 ?? "");
1077
+ if (!value) return;
1078
+ await sendUnicodeImeText(ctx, encodeUnicodeImeText(value), {
1079
+ timeoutMs: Math.max(15e3, value.length * 300)
1080
+ });
1081
+ await sleep(1200);
1082
+ }
1083
+ async function ensureUnicodeIme(ctx) {
1084
+ const out = await adbShell(ctx, ["ime", "list", "-s"], { timeoutMs: 15e3 });
1085
+ if (!String(out || "").split(/\s+/).includes(UnicodeIme.component)) {
1086
+ throw new CrawlerError({
1087
+ message: `adb_unavailable: UnicodeIME \u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528 ${UnicodeIme.component}`,
1088
+ code: Code.AdbUnavailable,
1089
+ context: { component: UnicodeIme.component }
1090
+ });
1091
+ }
1092
+ await adbShell(ctx, ["ime", "set", UnicodeIme.component], { timeoutMs: 15e3 });
1093
+ }
1094
+ async function currentInputMethod(ctx) {
1095
+ const value = await adbShell(ctx, ["settings", "get", "secure", "default_input_method"], { timeoutMs: 15e3 });
1096
+ return String(value || "").trim();
1097
+ }
1098
+ async function restoreInputMethod(ctx, component) {
1099
+ const value = String(component || "").trim();
1100
+ if (!value || value === "null" || value === UnicodeIme.component) return false;
1101
+ await adbShell(ctx, ["ime", "set", value], { timeoutMs: 15e3 });
1102
+ return true;
1103
+ }
1104
+ async function screenshotPng(ctx) {
1105
+ const serial = requireDeviceField(ctx, "serial");
1106
+ const adbPath = requireDeviceField(ctx, "adbPath");
1107
+ try {
1108
+ const { stdout } = await execAdbWithRetry(adbPath, ["-s", serial, "exec-out", "screencap", "-p"], {
1109
+ timeoutMs: 3e4,
1110
+ maxBuffer: 40 * 1024 * 1024,
1111
+ encoding: "buffer",
1112
+ label: "screencap"
1113
+ });
1114
+ return Buffer.from(stdout);
1115
+ } catch (error) {
1116
+ if (isAdbUnavailableError(error)) {
1117
+ throw new CrawlerError({
1118
+ message: `adb_unavailable: screencap failed ${error?.message || String(error)}`,
1119
+ code: Code.AdbUnavailable,
1120
+ context: { serial, adbPath }
1121
+ });
1122
+ }
1123
+ throw new CrawlerError({
1124
+ message: `source_extraction_failed: screencap failed ${error?.message || String(error)}`,
1125
+ code: Code.SourceExtractionFailed,
1126
+ context: { serial }
1127
+ });
1128
+ }
1129
+ }
1130
+ async function screenshotBase64(ctx) {
1131
+ return `data:image/png;base64,${(await screenshotPng(ctx)).toString("base64")}`;
1132
+ }
1133
+ async function readClipboardForShare(ctx) {
1134
+ const out = await adbShell(ctx, [
1135
+ "am",
1136
+ "broadcast",
1137
+ "-n",
1138
+ AppiumSettings.clipboardReceiver,
1139
+ "-a",
1140
+ AppiumSettings.clipboardGetAction
1141
+ ], {
1142
+ timeoutMs: 8e3,
1143
+ maxBuffer: 1024 * 1024
1144
+ }).catch((error) => {
1145
+ throw new CrawlerError({
1146
+ message: `source_extraction_failed: Appium Settings clipboard receiver unavailable ${error?.message || String(error)}`,
1147
+ code: Code.SourceExtractionFailed,
1148
+ context: { receiver: AppiumSettings.clipboardReceiver }
1149
+ });
1150
+ });
1151
+ const text2 = normalizeAppiumClipboardBroadcast(out);
1152
+ if (!text2) {
1153
+ throw new CrawlerError({
1154
+ message: "source_extraction_failed: ADB clipboard is empty",
1155
+ code: Code.SourceExtractionFailed,
1156
+ context: { receiver: AppiumSettings.clipboardReceiver, output: String(out || "").slice(0, 1e3) }
1157
+ });
1158
+ }
1159
+ return text2;
1160
+ }
1161
+ async function dumpUiXml(ctx) {
1162
+ let lastError = null;
1163
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
1164
+ const dumpPath = `/data/local/tmp/android-toolkit-window-${Date.now()}-${Math.random().toString(16).slice(2)}.xml`;
1165
+ await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
1166
+ });
1167
+ try {
1168
+ await adbShell(ctx, ["uiautomator", "dump", dumpPath], {
1169
+ timeoutMs: 3e4,
1170
+ maxBuffer: 4 * 1024 * 1024
1171
+ });
1172
+ const raw = await adbShell(ctx, ["cat", dumpPath], {
1173
+ timeoutMs: 3e4,
1174
+ maxBuffer: 16 * 1024 * 1024
1175
+ });
1176
+ const start = String(raw || "").indexOf("<?xml");
1177
+ if (start < 0) throw new CrawlerError({ message: `automation_failed: uiautomator dump missing xml: ${raw}`, code: Code.AutomationFailed });
1178
+ return String(raw || "").slice(start).trim();
1179
+ } catch (error) {
1180
+ lastError = error;
1181
+ Logger.useTemplate().adbRetry({ label: "uiautomator dump", attempt, message: error?.message || String(error) });
1182
+ await sleep(350 * attempt);
1183
+ } finally {
1184
+ await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
1185
+ });
1186
+ }
1187
+ }
1188
+ throw CrawlerError.isCrawlerError(lastError) ? lastError : new CrawlerError({
1189
+ message: `automation_failed: uiautomator dump failed ${lastError?.message || String(lastError || "")}`.trim(),
1190
+ code: Code.AutomationFailed,
1191
+ context: { lastError: lastError?.message || String(lastError || "") }
1192
+ });
1193
+ }
1194
+ async function wakeAndUnlock(ctx) {
1195
+ await pressKey(ctx, "KEYCODE_WAKEUP").catch(() => {
1196
+ });
1197
+ await sleep(300);
1198
+ await pressKey(ctx, "MENU").catch(() => {
1199
+ });
1200
+ await sleep(300);
1201
+ const size = await screenSize(ctx);
1202
+ 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(() => {
1203
+ });
1204
+ await sleep(600);
1205
+ }
1206
+ async function isKeyboardVisible(ctx) {
1207
+ const out = await adbShell(ctx, ["dumpsys", "input_method"], { timeoutMs: 15e3, maxBuffer: 4 * 1024 * 1024 });
1208
+ return /mInputShown=true|mIsInputViewShown=true|inputShown=true/i.test(String(out || ""));
1209
+ }
1210
+ async function hideKeyboard(ctx, options = {}) {
1211
+ const attempts = Math.max(1, Math.trunc(Number(options.attempts || 2)));
1212
+ for (let i = 0; i < attempts; i += 1) {
1213
+ if (!await isKeyboardVisible(ctx).catch(() => false)) return true;
1214
+ await pressBack(ctx).catch(() => {
1215
+ });
1216
+ await sleep(Number(options.settleMs || 400));
1217
+ }
1218
+ return !await isKeyboardVisible(ctx).catch(() => false);
1219
+ }
1220
+ async function execAdbWithRetry(adbPath, args, options = {}) {
1221
+ const attempts = Math.max(1, Math.trunc(Number(options.attempts || DEFAULT_ADB_ATTEMPTS)));
1222
+ const delayMs = Math.max(0, Number(options.retryDelayMs || DEFAULT_ADB_RETRY_DELAY_MS));
1223
+ const commandArgs = args.map(String);
1224
+ let lastError = null;
1225
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1226
+ try {
1227
+ return await execFileAsync(adbPath, commandArgs, {
1228
+ timeout: Number(options.timeoutMs || 15e3),
1229
+ maxBuffer: Number(options.maxBuffer || 12 * 1024 * 1024),
1230
+ encoding: options.encoding || "utf8"
1231
+ });
1232
+ } catch (error) {
1233
+ lastError = error;
1234
+ if (attempt >= attempts || !isRetryableAdbError(error)) {
1235
+ throw error;
1236
+ }
1237
+ Logger.useTemplate().adbRetry({
1238
+ label: options.label || "adb",
1239
+ attempt,
1240
+ attempts,
1241
+ args: compactArgs(args),
1242
+ message: error?.message || String(error)
1243
+ });
1244
+ await sleep(delayMs * attempt);
1245
+ }
1246
+ }
1247
+ throw lastError;
1248
+ }
1249
+ function requireDeviceField(ctx, key) {
1250
+ return requireValue(ctx?.[key], key);
1251
+ }
1252
+ function appDataDir(ctx) {
1253
+ return `/data/data/${requireDeviceField(ctx, "packageName")}`;
1254
+ }
1255
+ function cleanCachePathName(value, name) {
1256
+ const text2 = requireValue(value, name);
1257
+ if (text2.includes("/") || text2.includes("\\") || text2 === "." || text2 === "..") {
1258
+ throw new CrawlerError({ message: `invalid_request: invalid cache ${name}`, code: Code.InvalidRequest });
1259
+ }
1260
+ return text2;
1261
+ }
1262
+ function requireValue(value, name) {
1263
+ const clean = String(value ?? "").trim();
1264
+ if (!clean) throw new CrawlerError({ message: `invalid_request: ${name} is required`, code: Code.InvalidRequest });
1265
+ return clean;
1266
+ }
1267
+ function isAdbUnavailableError(error) {
1268
+ const message = String(error?.message || error || "");
1269
+ return /ENOENT|spawn .*adb|device .* not found|no devices|offline/i.test(message);
1270
+ }
1271
+ function isAdbInputTimeout(error) {
1272
+ const message = String(error?.message || error || "");
1273
+ return /timed out|timeout|SIGTERM|killed/i.test(message) || error?.signal === "SIGTERM" || error?.killed === true;
1274
+ }
1275
+ function isRetryableAdbError(error) {
1276
+ const message = String(error?.message || error || "");
1277
+ return isAdbInputTimeout(error) || /device .* not found|no devices|offline|closed|broken pipe|protocol fault|transport/i.test(message);
1278
+ }
1279
+ function compactArgs(args) {
1280
+ return args.map(String).join(" ").slice(0, 500);
1281
+ }
1282
+ function parseResolvedLauncherComponent(output, packageName) {
1283
+ const prefix = `${packageName}/`;
1284
+ const lines = String(output || "").replace(/\r/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
1285
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1286
+ const line = lines[index];
1287
+ if (line.startsWith(prefix) && /^[^\s/]+\/[^\s/]+$/.test(line)) return line;
1288
+ }
1289
+ return "";
1290
+ }
1291
+ function assertActivityStartOk(output, component) {
1292
+ const text2 = String(output || "");
1293
+ const status = /^\s*Status:\s*([^\s]+)/im.exec(text2)?.[1];
1294
+ const failed = /(^|\n)\s*(Error type \d+|Error:|Exception occurred|SecurityException|java\.[A-Za-z.]*Exception)/i.test(text2) || status && status.toLowerCase() !== "ok";
1295
+ if (!failed) return;
1296
+ throw new CrawlerError({
1297
+ message: `automation_failed: am start failed component=${component} output=${compactOutput(text2)}`,
1298
+ code: Code.AutomationFailed,
1299
+ context: { component, output: compactOutput(text2, 2e3) }
1300
+ });
1301
+ }
1302
+ function compactOutput(value, maxLength = 500) {
1303
+ return String(value || "").replace(/\s+/g, " ").trim().slice(0, maxLength);
1304
+ }
1305
+ function encodeUnicodeImeText(text2) {
1306
+ let out = "";
1307
+ let buffer = "";
1308
+ const flush = () => {
1309
+ if (!buffer) return;
1310
+ const bytes = Buffer.alloc(buffer.length * 2);
1311
+ for (let index = 0; index < buffer.length; index += 1) {
1312
+ bytes.writeUInt16BE(buffer.charCodeAt(index), index * 2);
1313
+ }
1314
+ out += `&${bytes.toString("base64").replace(/\//g, ",").replace(/=+$/g, "")}-`;
1315
+ buffer = "";
1316
+ };
1317
+ for (const char of String(text2 || "")) {
1318
+ const code = char.charCodeAt(0);
1319
+ if (code >= 32 && code <= 126) {
1320
+ flush();
1321
+ out += char === "&" ? "&-" : char;
1322
+ } else {
1323
+ buffer += char;
1324
+ }
1325
+ }
1326
+ flush();
1327
+ return out;
1328
+ }
1329
+ async function sendUnicodeImeText(ctx, encoded, options = {}) {
1330
+ await adbShell(ctx, ["input", "text", shellQuoteForRemote(encoded)], options);
1331
+ }
1332
+ function shellQuoteForRemote(value) {
1333
+ return `'${String(value ?? "").replace(/'/g, `'\\''`)}'`;
1334
+ }
1335
+ function normalizeAppiumClipboardBroadcast(value) {
1336
+ const out = String(value || "");
1337
+ if (!/Broadcast completed:\s*result=-1\b/.test(out)) return "";
1338
+ const match = /data="([^"]*)"/.exec(out) || /data=([^\s]+)/.exec(out);
1339
+ const base64 = match ? String(match[1] || "").trim() : "";
1340
+ if (!base64) return "";
1341
+ try {
1342
+ return Buffer.from(base64, "base64").toString("utf8").trim();
1343
+ } catch {
1344
+ return "";
1345
+ }
1346
+ }
1347
+ var $DeviceInternalsForTest = Object.freeze({
1348
+ isAdbInputTimeout,
1349
+ isRetryableAdbError,
1350
+ encodeUnicodeImeText,
1351
+ shellQuoteForRemote
1352
+ });
1353
+
1354
+ // src/apify-kit.js
1355
+ var { decycle } = cycle;
1356
+ var instance = null;
1357
+ var ApifyKit = {
1358
+ useApifyKit
1359
+ };
1360
+ async function useApifyKit(options = {}) {
1361
+ if (!instance || options.reset) {
1362
+ instance = createApifyKit(options);
1363
+ }
1364
+ return instance;
1365
+ }
1366
+ function createApifyKit(options = {}) {
1367
+ const inputPath = StoragePath.Input;
1368
+ const outputPath = options.outputPath || StoragePath.Output;
1369
+ const input = options.input || JSON.parse(fs.readFileSync(inputPath, "utf8"));
1370
+ const ctx = options.ctx || Context.createAndroidContext(input, options.contextDefaults || {});
1371
+ const logTemplate = Logger.useTemplate();
1372
+ let pushed = false;
1373
+ return {
1374
+ ctx,
1375
+ input,
1376
+ inputPath,
1377
+ outputPath,
1378
+ async runStep(step, target, actionFn, options2 = {}) {
1379
+ const { failActor = true, retry = {} } = options2;
1380
+ const retryTimes = Math.max(0, Number(retry.times || 0));
1381
+ const startedAt = Date.now();
1382
+ let lastError = null;
1383
+ for (let attempt = 0; attempt <= retryTimes; attempt += 1) {
1384
+ const attemptLabel = attempt > 0 ? ` (\u91CD\u8BD5 #${attempt})` : "";
1385
+ if (attempt > 0) logTemplate.stepRetry(step, attempt);
1386
+ logTemplate.stepStart(`${step}${attemptLabel}`, stepDetail(target));
1387
+ try {
1388
+ const result = await actionFn();
1389
+ logTemplate.stepSuccess(`${step}${attemptLabel}`, { duration: Logger.duration(startedAt) });
1390
+ return result;
1391
+ } catch (error) {
1392
+ lastError = error;
1393
+ logTemplate.stepFail(`${step}${attemptLabel}`, error);
1394
+ if (attempt < retryTimes) {
1395
+ if (typeof retry.before === "function") {
1396
+ await retry.before(ctx, attempt + 1);
1397
+ } else {
1398
+ await sleep(3e3);
1399
+ }
1400
+ }
1401
+ }
1402
+ }
1403
+ if (failActor) {
1404
+ let base64 = "";
1405
+ try {
1406
+ base64 = await Device.screenshotBase64(ctx);
1407
+ } catch (error) {
1408
+ base64 = `\u622A\u56FE\u5931\u8D25: ${error?.message || String(error)}`;
1409
+ }
1410
+ await this.pushFailed(lastError, { step, base64, retryAttempts: retryTimes });
1411
+ }
1412
+ throw lastError;
1413
+ },
1414
+ async runStepLoose(step, target, actionFn, options2 = {}) {
1415
+ return this.runStep(step, target, actionFn, { ...options2, failActor: false });
1416
+ },
1417
+ async pushSuccess(data = {}, options2 = {}) {
1418
+ pushed = true;
1419
+ appendOutput(outputPath, {
1420
+ code: Number.isFinite(Number(options2.code)) ? Number(options2.code) : Code.Success,
1421
+ status: Status.Success,
1422
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1423
+ data: sanitizeData(data),
1424
+ ...options2.meta ? { meta: sanitizeData(options2.meta) } : {}
1425
+ });
1426
+ logTemplate.dataPush({ type: "success", outputPath, keys: Object.keys(data || {}).join(",") });
1427
+ },
1428
+ async pushArtifact(data = {}) {
1429
+ appendOutput(outputPath, sanitizeData(data));
1430
+ logTemplate.dataPush({ type: "artifact", outputPath, keys: Object.keys(data || {}).join(",") });
1431
+ },
1432
+ async pushFailed(error, meta = {}) {
1433
+ const normalized = CrawlerError.isCrawlerError(error) ? error : CrawlerError.from(error, { code: Code.UnknownError, context: meta });
1434
+ pushed = true;
1435
+ appendOutput(outputPath, {
1436
+ code: normalized.code,
1437
+ status: Status.Failed,
1438
+ error: sanitizeData(serializeError(normalized)),
1439
+ meta: sanitizeData(meta),
1440
+ context: sanitizeData(normalized.context),
1441
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1442
+ });
1443
+ logTemplate.dataPush({ type: "failed", outputPath, code: normalized.code, message: normalized.message });
1444
+ },
1445
+ hasPushed() {
1446
+ return pushed;
1447
+ }
1448
+ };
1449
+ }
1450
+ function appendOutput(outputPath, item) {
1451
+ const items = readOutputArray(outputPath);
1452
+ items.push(item);
1453
+ fs.writeFileSync(outputPath, JSON.stringify(items, null, 2) + "\n", "utf8");
1454
+ }
1455
+ function readOutputArray(outputPath) {
1456
+ if (!fs.existsSync(outputPath)) return [];
1457
+ const raw = fs.readFileSync(outputPath, "utf8").trim();
1458
+ if (!raw) return [];
1459
+ const parsed = JSON.parse(raw);
1460
+ if (!Array.isArray(parsed)) throw new Error(`Android output must be a JSON array: ${outputPath}`);
1461
+ return parsed;
1462
+ }
1463
+ function stepDetail(target) {
1464
+ if (!target) return {};
1465
+ if (typeof target === "string") return { target };
1466
+ if (typeof target === "object") {
1467
+ return {
1468
+ serial: target.serial,
1469
+ packageName: target.packageName,
1470
+ runId: target.runId
1471
+ };
1472
+ }
1473
+ return { target: String(target) };
1474
+ }
1475
+ function sanitizeData(value) {
1476
+ return decycle(prepareForJson(value, /* @__PURE__ */ new WeakMap()));
1477
+ }
1478
+ function prepareForJson(value, seen) {
1479
+ if (value == null) return value;
1480
+ const type = typeof value;
1481
+ if (type === "string" || type === "number" || type === "boolean") return value;
1482
+ if (type === "bigint") return value.toString();
1483
+ if (type === "function" || type === "symbol") return void 0;
1484
+ if (type !== "object") return String(value);
1485
+ if (seen.has(value)) return seen.get(value);
1486
+ if (value instanceof Date) return value.toISOString();
1487
+ if (value instanceof Error) {
1488
+ const out2 = {};
1489
+ seen.set(value, out2);
1490
+ copyEntries(out2, serializeError(value), seen);
1491
+ return out2;
1492
+ }
1493
+ if (Array.isArray(value)) {
1494
+ const out2 = [];
1495
+ seen.set(value, out2);
1496
+ for (const item of value) {
1497
+ const safe = prepareForJson(item, seen);
1498
+ out2.push(safe === void 0 ? null : safe);
1499
+ }
1500
+ return out2;
1501
+ }
1502
+ if (value instanceof Map) {
1503
+ const out2 = [];
1504
+ seen.set(value, out2);
1505
+ for (const [key, item] of value.entries()) {
1506
+ out2.push([prepareForJson(key, seen), prepareForJson(item, seen)]);
1507
+ }
1508
+ return out2;
1509
+ }
1510
+ if (value instanceof Set) {
1511
+ const out2 = [];
1512
+ seen.set(value, out2);
1513
+ for (const item of value.values()) {
1514
+ const safe = prepareForJson(item, seen);
1515
+ out2.push(safe === void 0 ? null : safe);
1516
+ }
1517
+ return out2;
1518
+ }
1519
+ const out = {};
1520
+ seen.set(value, out);
1521
+ copyEntries(out, value, seen);
1522
+ return out;
1523
+ }
1524
+ function copyEntries(out, value, seen) {
1525
+ for (const [key, item] of Object.entries(value)) {
1526
+ const safe = prepareForJson(item, seen);
1527
+ if (safe !== void 0) out[key] = safe;
1528
+ }
1529
+ }
1530
+
1531
+ // src/device-input.js
1532
+ import { createHash as createHash2 } from "node:crypto";
1533
+
1534
+ // src/device-view.js
1535
+ import { createHash } from "node:crypto";
1536
+ import { XMLParser } from "fast-xml-parser";
1537
+ var parser = new XMLParser({
1538
+ ignoreAttributes: false,
1539
+ attributeNamePrefix: "",
1540
+ allowBooleanAttributes: true
1541
+ });
1542
+ var DeviceView = {
1543
+ snapshot,
1544
+ find,
1545
+ findAll,
1546
+ waitFor,
1547
+ bounds,
1548
+ text,
1549
+ exists,
1550
+ isScrollable,
1551
+ hashNode,
1552
+ normalizeSelector
1553
+ };
1554
+ async function snapshot(ctx, options = {}) {
1555
+ const xml = options.xml || await Device.dumpUiXml(ctx);
1556
+ const parsed = parser.parse(xml);
1557
+ const roots = toArray(parsed?.hierarchy?.node).map((node) => normalizeNode(node, null, 0));
1558
+ const flat = [];
1559
+ for (const root of roots) collectFlat(root, flat);
1560
+ return { xml, tree: roots, roots, flat, nodes: flat };
1561
+ }
1562
+ async function find(ctx, selector, options = {}) {
1563
+ const nodes = await findAll(ctx, selector, options);
1564
+ if (nodes.length > 0) return nodes[0];
1565
+ if (options.optional) return null;
1566
+ throw new CrawlerError({
1567
+ message: `automation_failed: View \u672A\u627E\u5230 ${selectorLabel(selector)}`,
1568
+ code: Code.AutomationFailed,
1569
+ context: { selector }
1570
+ });
1571
+ }
1572
+ async function findAll(ctx, selector, options = {}) {
1573
+ const view = options.snapshot || await snapshot(ctx, options);
1574
+ const normalized = normalizeSelector(selector);
1575
+ return view.flat.filter((node) => matchesSelector(node, normalized, options));
1576
+ }
1577
+ async function waitFor(ctx, selector, options = {}) {
1578
+ const timeoutMs = Number(options.timeoutMs || options.timeout || 3e4);
1579
+ const intervalMs = Number(options.intervalMs || options.pollIntervalMs || 500);
1580
+ const deadline = Date.now() + timeoutMs;
1581
+ let lastError = null;
1582
+ let sawCleanPoll = false;
1583
+ while (Date.now() < deadline) {
1584
+ try {
1585
+ const node = await find(ctx, selector, { ...options, optional: true });
1586
+ sawCleanPoll = true;
1587
+ lastError = null;
1588
+ if (node) return node;
1589
+ } catch (error) {
1590
+ lastError = error;
1591
+ }
1592
+ await sleep(intervalMs);
1593
+ }
1594
+ if (lastError && !sawCleanPoll) throw lastError;
1595
+ throw new CrawlerError({
1596
+ message: `automation_failed: \u7B49\u5F85 View \u8D85\u65F6 ${selectorLabel(selector)}`,
1597
+ code: Code.AutomationFailed,
1598
+ context: { selector, timeoutMs }
1599
+ });
1600
+ }
1601
+ async function bounds(ctx, selector, options = {}) {
1602
+ return (await find(ctx, selector, options)).bounds;
1603
+ }
1604
+ async function text(ctx, selector, options = {}) {
1605
+ const node = await find(ctx, selector, options);
1606
+ return node.text || node.contentDesc || "";
1607
+ }
1608
+ async function exists(ctx, selector, options = {}) {
1609
+ return Boolean(await find(ctx, selector, { ...options, optional: true }));
1610
+ }
1611
+ async function isScrollable(ctx, selector, options = {}) {
1612
+ const node = await find(ctx, selector, options);
1613
+ return Boolean(node.scrollable);
1614
+ }
1615
+ function normalizeSelector(selector) {
1616
+ if (!selector || typeof selector !== "object" || Array.isArray(selector)) {
1617
+ throw new CrawlerError({
1618
+ message: "invalid_request: DeviceView selector must be an object",
1619
+ code: Code.InvalidRequest,
1620
+ context: { selector }
1621
+ });
1622
+ }
1623
+ const out = {};
1624
+ for (const key of ["id", "text", "textContains", "contentDesc", "className"]) {
1625
+ if (selector[key] != null && String(selector[key]).trim()) out[key] = String(selector[key]).trim();
1626
+ }
1627
+ if (Object.keys(out).length === 0) {
1628
+ throw new CrawlerError({
1629
+ message: "invalid_request: DeviceView selector requires id/text/textContains/contentDesc/className",
1630
+ code: Code.InvalidRequest,
1631
+ context: { selector }
1632
+ });
1633
+ }
1634
+ return out;
1635
+ }
1636
+ function hashNode(node) {
1637
+ return createHash("sha256").update(stableNodeString(node)).digest("hex");
1638
+ }
1639
+ function normalizeNode(raw, parent, index) {
1640
+ const childrenRaw = toArray(raw?.node);
1641
+ const bounds2 = parseBounds(raw?.bounds);
1642
+ const node = {
1643
+ resourceId: String(raw?.["resource-id"] || ""),
1644
+ id: shortResourceId(raw?.["resource-id"]),
1645
+ text: String(raw?.text || ""),
1646
+ contentDesc: String(raw?.["content-desc"] || ""),
1647
+ className: String(raw?.class || ""),
1648
+ packageName: String(raw?.package || ""),
1649
+ bounds: bounds2,
1650
+ clickable: toBoolean(raw?.clickable),
1651
+ enabled: toBoolean(raw?.enabled),
1652
+ focusable: toBoolean(raw?.focusable),
1653
+ focused: toBoolean(raw?.focused),
1654
+ scrollable: toBoolean(raw?.scrollable),
1655
+ visible: bounds2.right > bounds2.left && bounds2.bottom > bounds2.top,
1656
+ index: Number(raw?.index ?? index),
1657
+ parent: null,
1658
+ children: []
1659
+ };
1660
+ node.parent = parent || null;
1661
+ node.children = childrenRaw.map((child, childIndex) => normalizeNode(child, node, childIndex));
1662
+ return node;
1663
+ }
1664
+ function matchesSelector(node, selector, options = {}) {
1665
+ if (options.visibleOnly !== false && !node.visible) return false;
1666
+ if (options.enabledOnly === true && !node.enabled) return false;
1667
+ if (selector.id && !matchesId(node, selector.id)) return false;
1668
+ if (selector.text && node.text !== selector.text) return false;
1669
+ if (selector.textContains && !node.text.includes(selector.textContains)) return false;
1670
+ if (selector.contentDesc && node.contentDesc !== selector.contentDesc) return false;
1671
+ if (selector.className && node.className !== selector.className) return false;
1672
+ return true;
1673
+ }
1674
+ function matchesId(node, expected) {
1675
+ const actual = node.resourceId || "";
1676
+ if (!actual) return false;
1677
+ if (actual === expected) return true;
1678
+ return actual.endsWith(`:id/${expected}`) || node.id === expected;
1679
+ }
1680
+ function collectFlat(node, out) {
1681
+ out.push(node);
1682
+ for (const child of node.children) collectFlat(child, out);
1683
+ }
1684
+ function toArray(value) {
1685
+ if (!value) return [];
1686
+ return Array.isArray(value) ? value : [value];
1687
+ }
1688
+ function parseBounds(value) {
1689
+ const match = /\[(\d+),(\d+)]\[(\d+),(\d+)]/.exec(String(value || ""));
1690
+ if (!match) return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0, centerX: 0, centerY: 0 };
1691
+ const left = Number(match[1]);
1692
+ const top = Number(match[2]);
1693
+ const right = Number(match[3]);
1694
+ const bottom = Number(match[4]);
1695
+ return {
1696
+ left,
1697
+ top,
1698
+ right,
1699
+ bottom,
1700
+ width: Math.max(0, right - left),
1701
+ height: Math.max(0, bottom - top),
1702
+ centerX: (left + right) / 2,
1703
+ centerY: (top + bottom) / 2
1704
+ };
1705
+ }
1706
+ function shortResourceId(value) {
1707
+ const raw = String(value || "");
1708
+ const marker = ":id/";
1709
+ const index = raw.indexOf(marker);
1710
+ return index >= 0 ? raw.slice(index + marker.length) : raw;
1711
+ }
1712
+ function toBoolean(value) {
1713
+ return value === true || String(value).toLowerCase() === "true";
1714
+ }
1715
+ function stableNodeString(node) {
1716
+ if (!node) return "";
1717
+ return JSON.stringify({
1718
+ resourceId: node.resourceId,
1719
+ text: node.text,
1720
+ contentDesc: node.contentDesc,
1721
+ className: node.className,
1722
+ bounds: node.bounds,
1723
+ clickable: node.clickable,
1724
+ enabled: node.enabled,
1725
+ focusable: node.focusable,
1726
+ focused: node.focused,
1727
+ scrollable: node.scrollable,
1728
+ children: node.children.map((child) => JSON.parse(stableNodeString(child) || "{}"))
1729
+ });
1730
+ }
1731
+ function selectorLabel(selector) {
1732
+ try {
1733
+ return JSON.stringify(selector);
1734
+ } catch {
1735
+ return String(selector);
1736
+ }
1737
+ }
1738
+
1739
+ // src/device-input.js
1740
+ var SCROLL_MAX_SWIPES = 24;
1741
+ var SCROLL_STABLE_ROUNDS = 1;
1742
+ var SCROLL_SWIPE_RATIO = 0.72;
1743
+ var SCROLL_DURATION_MS = 360;
1744
+ var SCROLL_SETTLE_MS = 500;
1745
+ var SCROLL_HASH_SOURCE = "screenshot";
1746
+ var SCROLL_IDLE_SETTLE_MS = 900;
1747
+ var DeviceInput = {
1748
+ click,
1749
+ tap: click,
1750
+ fill,
1751
+ type: fill,
1752
+ press,
1753
+ pressEnter: pressEnter2,
1754
+ scroll,
1755
+ scrollStep,
1756
+ scrollToEnd,
1757
+ scrollToTop
1758
+ };
1759
+ async function click(ctx, selectorOrPoint, options = {}) {
1760
+ if (isPoint(selectorOrPoint)) {
1761
+ await Device.tapAbsolute(ctx, selectorOrPoint.x, selectorOrPoint.y);
1762
+ await sleep(Number(options.settleMs || 250));
1763
+ return { point: selectorOrPoint };
1764
+ }
1765
+ if (isBounds(selectorOrPoint)) {
1766
+ const point2 = centerOf(selectorOrPoint);
1767
+ await Device.tapAbsolute(ctx, point2.x, point2.y);
1768
+ await sleep(Number(options.settleMs || 250));
1769
+ return { point: point2 };
1770
+ }
1771
+ const target = await DeviceView.find(ctx, selectorOrPoint, options);
1772
+ const actual = nearestClickable(target);
1773
+ if (!actual) {
1774
+ throw new CrawlerError({
1775
+ message: `automation_failed: View \u4E0D\u53EF\u70B9\u51FB ${JSON.stringify(selectorOrPoint)}`,
1776
+ code: Code.AutomationFailed,
1777
+ context: { selector: selectorOrPoint, target: simplifyNode(target) }
1778
+ });
1779
+ }
1780
+ const point = centerOf(actual.bounds);
1781
+ Logger.info("DeviceInput.click", {
1782
+ selector: selectorOrPoint,
1783
+ target: simplifyNode(target),
1784
+ actual: simplifyNode(actual),
1785
+ point
1786
+ });
1787
+ await Device.tapAbsolute(ctx, point.x, point.y);
1788
+ await sleep(Number(options.settleMs || 250));
1789
+ return { target, actual, point };
1790
+ }
1791
+ async function fill(ctx, selector, text2, options = {}) {
1792
+ const value = String(text2 ?? "");
1793
+ const clicked = await click(ctx, selector, {
1794
+ ...options,
1795
+ settleMs: Number(options.focusSettleMs || 350)
1796
+ });
1797
+ try {
1798
+ await Device.typeText(ctx, value);
1799
+ } catch (error) {
1800
+ throw new CrawlerError({
1801
+ message: `automation_failed: View \u5199\u5165\u6587\u672C\u5931\u8D25 ${JSON.stringify(selector)}`,
1802
+ code: Code.AutomationFailed,
1803
+ context: { selector, error: error?.message || String(error) }
1804
+ });
1805
+ }
1806
+ Logger.info("DeviceInput.fill", {
1807
+ selector,
1808
+ target: simplifyNode(clicked.actual || clicked.target),
1809
+ chars: value.length
1810
+ });
1811
+ await sleep(Number(options.settleMs || 350));
1812
+ return { target: clicked.actual || clicked.target, chars: value.length };
1813
+ }
1814
+ async function press(ctx, key) {
1815
+ await Device.pressKey(ctx, key);
1816
+ }
1817
+ async function pressEnter2(ctx) {
1818
+ await Device.pressEnter(ctx);
1819
+ }
1820
+ async function scroll(ctx, selector, direction, options = {}) {
1821
+ const node = await DeviceView.find(ctx, selector, options);
1822
+ await scrollBounds(ctx, node.bounds, direction, {});
1823
+ }
1824
+ async function scrollStep(ctx, selector, direction, options = {}) {
1825
+ const node = await DeviceView.find(ctx, selector, options);
1826
+ await scrollBounds(ctx, node.bounds, direction, options);
1827
+ }
1828
+ async function scrollBounds(ctx, bounds2, direction, options = {}) {
1829
+ const box = bounds2;
1830
+ const ratio = Number(options.swipeRatio || SCROLL_SWIPE_RATIO);
1831
+ const durationMs = Number(options.durationMs || SCROLL_DURATION_MS);
1832
+ const x = box.centerX;
1833
+ const distance = Math.max(40, box.height * ratio);
1834
+ const down = String(direction || "").toLowerCase() === "down";
1835
+ const from = {
1836
+ x,
1837
+ y: down ? box.top + box.height * 0.25 : box.bottom - box.height * 0.25
1838
+ };
1839
+ const to = {
1840
+ x,
1841
+ y: down ? Math.min(box.bottom - 10, from.y + distance) : Math.max(box.top + 10, from.y - distance)
1842
+ };
1843
+ await Device.swipe(ctx, from, to, durationMs);
1844
+ await sleep(Number(options.settleMs || SCROLL_SETTLE_MS));
1845
+ }
1846
+ async function scrollToEnd(ctx, selector, options = {}) {
1847
+ return scrollUntilStable(ctx, selector, "up", options);
1848
+ }
1849
+ async function scrollToTop(ctx, selector, options = {}) {
1850
+ return scrollUntilStable(ctx, selector, "down", options);
1851
+ }
1852
+ async function scrollUntilStable(ctx, selector, direction, options = {}) {
1853
+ const maxSwipes = SCROLL_MAX_SWIPES;
1854
+ const stableRoundsTarget = SCROLL_STABLE_ROUNDS;
1855
+ const node = await DeviceView.find(ctx, selector, options);
1856
+ const bounds2 = node.bounds;
1857
+ let lastHash = await scrollStateHash(ctx);
1858
+ let stableRounds = 0;
1859
+ for (let index = 0; index < maxSwipes; index += 1) {
1860
+ await scrollBounds(ctx, bounds2, direction, options);
1861
+ const currentHash = await scrollStateHash(ctx);
1862
+ if (currentHash === lastHash) {
1863
+ stableRounds += 1;
1864
+ if (stableRounds >= stableRoundsTarget) {
1865
+ return { swipes: index + 1, stableRounds, hash: currentHash, hashSource: SCROLL_HASH_SOURCE, capped: false };
1866
+ }
1867
+ } else {
1868
+ stableRounds = 0;
1869
+ lastHash = currentHash;
1870
+ }
1871
+ }
1872
+ const idle = await confirmScrollIdle(ctx, lastHash, options);
1873
+ if (idle.stable) {
1874
+ return {
1875
+ swipes: maxSwipes,
1876
+ stableRounds: stableRoundsTarget,
1877
+ hash: idle.hash,
1878
+ hashSource: `${SCROLL_HASH_SOURCE}:idle`,
1879
+ capped: false,
1880
+ inferredBoundary: true
1881
+ };
1882
+ }
1883
+ return { swipes: maxSwipes, stableRounds, hash: lastHash, hashSource: SCROLL_HASH_SOURCE, capped: true };
1884
+ }
1885
+ async function scrollStateHash(ctx) {
1886
+ const png = await Device.screenshotPng(ctx);
1887
+ return createHash2("sha256").update(png).digest("hex");
1888
+ }
1889
+ async function confirmScrollIdle(ctx, previousHash, options = {}) {
1890
+ const settleMs = Number(options.idleSettleMs || SCROLL_IDLE_SETTLE_MS);
1891
+ await sleep(settleMs);
1892
+ const hash = await scrollStateHash(ctx);
1893
+ if (hash !== previousHash) return { stable: false, hash };
1894
+ return { stable: true, hash };
1895
+ }
1896
+ function nearestClickable(node) {
1897
+ let current = node;
1898
+ while (current) {
1899
+ if (current.visible && current.enabled && current.clickable) return current;
1900
+ current = current.parent;
1901
+ }
1902
+ return null;
1903
+ }
1904
+ function isPoint(value) {
1905
+ return value && typeof value === "object" && Number.isFinite(Number(value.x)) && Number.isFinite(Number(value.y));
1906
+ }
1907
+ function isBounds(value) {
1908
+ 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));
1909
+ }
1910
+ function centerOf(bounds2) {
1911
+ return {
1912
+ x: (Number(bounds2.left) + Number(bounds2.right)) / 2,
1913
+ y: (Number(bounds2.top) + Number(bounds2.bottom)) / 2
1914
+ };
1915
+ }
1916
+ function simplifyNode(node) {
1917
+ if (!node) return null;
1918
+ return {
1919
+ id: node.id,
1920
+ resourceId: node.resourceId,
1921
+ text: node.text,
1922
+ contentDesc: node.contentDesc,
1923
+ className: node.className,
1924
+ clickable: node.clickable,
1925
+ enabled: node.enabled,
1926
+ bounds: node.bounds
1927
+ };
1928
+ }
1929
+
1930
+ // src/device-sqlite.js
1931
+ import { execFile as execFile2 } from "node:child_process";
1932
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
1933
+ import { tmpdir } from "node:os";
1934
+ import path from "node:path";
1935
+ import { promisify as promisify2 } from "node:util";
1936
+ var execFileAsync2 = promisify2(execFile2);
1937
+ var DeviceSQLite = {
1938
+ query,
1939
+ health
1940
+ };
1941
+ async function query(ctx, options = {}) {
1942
+ if (!options.sql) {
1943
+ throw new CrawlerError({
1944
+ message: "invalid_request: sql is required",
1945
+ code: Code.InvalidRequest
1946
+ });
1947
+ }
1948
+ const config = {
1949
+ dbPath: String(options.dbPath || ""),
1950
+ dbDir: String(options.dbDir || ""),
1951
+ dbNamePrefix: String(options.dbNamePrefix || ""),
1952
+ dbNameIncludes: String(options.dbNameIncludes || ""),
1953
+ dbNameExcludes: normalizeStringArray(options.dbNameExcludes),
1954
+ sql: String(options.sql || ""),
1955
+ args: normalizeStringArray(options.args),
1956
+ maxRows: Math.max(1, Number(options.maxRows || 200)),
1957
+ label: String(options.label || "query-sqlite")
1958
+ };
1959
+ if (!config.dbPath && !config.dbDir) {
1960
+ throw new CrawlerError({
1961
+ message: "invalid_request: dbPath or dbDir is required",
1962
+ code: Code.InvalidRequest
1963
+ });
1964
+ }
1965
+ const startedAt = Date.now();
1966
+ const dir = await mkdtemp(path.join(tmpdir(), "android-toolkit-sqlite-"));
1967
+ try {
1968
+ const dbPaths = await resolveDeviceDbPaths(ctx, config);
1969
+ if (dbPaths.length === 0) {
1970
+ throw new CrawlerError({
1971
+ message: "app_runtime_unavailable: sqlite database not found",
1972
+ code: Code.AppRuntimeUnavailable,
1973
+ context: { dbDir: config.dbDir, dbPath: config.dbPath, dbNamePrefix: config.dbNamePrefix }
1974
+ });
1975
+ }
1976
+ const databases = [];
1977
+ for (const dbPath of dbPaths) {
1978
+ const localDbPath = await pullDatabaseSnapshot(ctx, dbPath, dir);
1979
+ const result = await queryLocalSQLite(localDbPath, config);
1980
+ databases.push({
1981
+ dbPath,
1982
+ columns: result.columns,
1983
+ rows: result.rows,
1984
+ rowCount: result.rows.length,
1985
+ truncated: result.truncated
1986
+ });
1987
+ }
1988
+ Logger.info("DeviceSQLite.query", {
1989
+ label: config.label,
1990
+ databaseCount: databases.length,
1991
+ rowCount: databases.reduce((total, item) => total + item.rowCount, 0),
1992
+ duration: Logger.duration(startedAt)
1993
+ });
1994
+ return { ok: true, databases, rows: databases.length === 1 ? databases[0].rows : [] };
1995
+ } finally {
1996
+ await rm(dir, { recursive: true, force: true }).catch(() => {
1997
+ });
1998
+ }
1999
+ }
2000
+ async function health(ctx, options = {}) {
2001
+ const packageName = String(options.packageName || ctx?.packageName || "").trim();
2002
+ if (!packageName) {
2003
+ throw new CrawlerError({
2004
+ message: "invalid_request: packageName is required",
2005
+ code: Code.InvalidRequest
2006
+ });
2007
+ }
2008
+ await adbSuShell(ctx, `test -d ${shellQuote(`/data/data/${packageName}`)}`, {
2009
+ timeoutMs: Number(options.timeoutMs || 1e4)
2010
+ });
2011
+ return { ok: true, packageName };
2012
+ }
2013
+ async function resolveDeviceDbPaths(ctx, config) {
2014
+ if (config.dbPath) return [config.dbPath];
2015
+ 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`;
2016
+ const out = await adbSuShell(ctx, command, {
2017
+ timeoutMs: 15e3,
2018
+ maxBuffer: 2 * 1024 * 1024
2019
+ }).catch((error) => {
2020
+ throw new CrawlerError({
2021
+ message: `app_runtime_unavailable: sqlite dbDir unavailable ${error?.message || String(error)}`,
2022
+ code: Code.AppRuntimeUnavailable,
2023
+ context: { dbDir: config.dbDir }
2024
+ });
2025
+ });
2026
+ return String(out || "").split("\n").map((line) => line.trim()).filter(Boolean).filter((name) => matchesName(name, config)).map((name) => `${config.dbDir.replace(/\/+$/, "")}/${name}`).sort();
2027
+ }
2028
+ async function pullDatabaseSnapshot(ctx, dbPath, localDir) {
2029
+ const baseName = path.basename(dbPath);
2030
+ const remoteDir = `/data/local/tmp/android-toolkit-sqlite-${Date.now()}-${Math.random().toString(16).slice(2)}`;
2031
+ const remoteDb = `${remoteDir}/${baseName}`;
2032
+ const localDb = path.join(localDir, baseName);
2033
+ const copyCommand = [
2034
+ `rm -rf ${shellQuote(remoteDir)}`,
2035
+ `mkdir -p ${shellQuote(remoteDir)}`,
2036
+ `cp ${shellQuote(dbPath)} ${shellQuote(remoteDb)}`,
2037
+ `[ -f ${shellQuote(`${dbPath}-wal`)} ] && cp ${shellQuote(`${dbPath}-wal`)} ${shellQuote(`${remoteDb}-wal`)} || true`,
2038
+ `[ -f ${shellQuote(`${dbPath}-shm`)} ] && cp ${shellQuote(`${dbPath}-shm`)} ${shellQuote(`${remoteDb}-shm`)} || true`,
2039
+ `chmod 644 ${shellQuote(remoteDir)}/*`
2040
+ ].join(" && ");
2041
+ try {
2042
+ await adbSuShell(ctx, copyCommand, { timeoutMs: 3e4, maxBuffer: 4 * 1024 * 1024 });
2043
+ await adbPull(ctx, remoteDb, localDb).catch((error) => {
2044
+ throw new CrawlerError({
2045
+ message: `app_runtime_unavailable: sqlite snapshot pull failed ${error?.message || String(error)}`,
2046
+ code: Code.AppRuntimeUnavailable,
2047
+ context: { dbPath }
2048
+ });
2049
+ });
2050
+ await adbPullOptional(ctx, `${remoteDb}-wal`, `${localDb}-wal`);
2051
+ await adbPullOptional(ctx, `${remoteDb}-shm`, `${localDb}-shm`);
2052
+ return localDb;
2053
+ } finally {
2054
+ await adbSuShell(ctx, `rm -rf ${shellQuote(remoteDir)}`, { timeoutMs: 1e4 }).catch(() => {
2055
+ });
2056
+ }
2057
+ }
2058
+ async function adbSuShell(ctx, command, options = {}) {
2059
+ return Device.adbShell(ctx, [`su -c ${shellQuote(command)}`], options);
2060
+ }
2061
+ async function queryLocalSQLite(dbPath, config) {
2062
+ const payloadPath = `${dbPath}.query.json`;
2063
+ const outputPath = `${dbPath}.rows.json`;
2064
+ await writeFile(payloadPath, JSON.stringify({
2065
+ dbPath,
2066
+ sql: config.sql,
2067
+ args: config.args,
2068
+ maxRows: config.maxRows,
2069
+ outputPath
2070
+ }), "utf8");
2071
+ const code = `
2072
+ const fs = require('node:fs');
2073
+ const { DatabaseSync } = require('node:sqlite');
2074
+ const payload = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
2075
+ const db = new DatabaseSync(payload.dbPath, { open: true, readOnly: true });
2076
+ try {
2077
+ const stmt = db.prepare(payload.sql);
2078
+ const columns = stmt.columns().map((column) => String(column.name || ''));
2079
+ const rows = [];
2080
+ let truncated = false;
2081
+ for (const row of stmt.iterate(...payload.args)) {
2082
+ if (rows.length >= payload.maxRows) { truncated = true; break; }
2083
+ const out = {};
2084
+ for (const column of columns) out[column] = row[column] == null ? '' : String(row[column]);
2085
+ rows.push(out);
2086
+ }
2087
+ fs.writeFileSync(payload.outputPath, JSON.stringify({ columns, rows, truncated }), 'utf8');
2088
+ } finally {
2089
+ db.close();
2090
+ }
2091
+ `;
2092
+ await execFileAsync2(process.execPath, ["-e", code, payloadPath], {
2093
+ timeout: 3e4,
2094
+ maxBuffer: 4 * 1024 * 1024,
2095
+ encoding: "utf8"
2096
+ });
2097
+ return JSON.parse(await import("node:fs/promises").then((fs3) => fs3.readFile(outputPath, "utf8")));
2098
+ }
2099
+ async function adbPull(ctx, remotePath, localPath) {
2100
+ const serial = requireNonEmpty2(ctx?.serial, "serial");
2101
+ await Device.adbExec(ctx, ["-s", serial, "pull", remotePath, localPath], {
2102
+ timeoutMs: 3e4,
2103
+ maxBuffer: 8 * 1024 * 1024,
2104
+ label: "adb pull"
2105
+ });
2106
+ }
2107
+ async function adbPullOptional(ctx, remotePath, localPath) {
2108
+ await adbPull(ctx, remotePath, localPath).catch(() => {
2109
+ });
2110
+ }
2111
+ function matchesName(name, config) {
2112
+ if (config.dbNamePrefix && !name.startsWith(config.dbNamePrefix)) return false;
2113
+ if (config.dbNameIncludes && !name.includes(config.dbNameIncludes)) return false;
2114
+ for (const item of config.dbNameExcludes || []) {
2115
+ if (item && name.includes(item)) return false;
2116
+ }
2117
+ return true;
2118
+ }
2119
+ function normalizeStringArray(value) {
2120
+ if (Array.isArray(value)) return value.map((item) => String(item ?? ""));
2121
+ if (value == null) return [];
2122
+ return [String(value)];
2123
+ }
2124
+ function requireNonEmpty2(value, name) {
2125
+ const clean = String(value ?? "").trim();
2126
+ if (!clean) throw new CrawlerError({ message: `invalid_request: ${name} is required`, code: Code.InvalidRequest });
2127
+ return clean;
2128
+ }
2129
+ function shellQuote(value) {
2130
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
2131
+ }
2132
+
2133
+ // src/launch.js
2134
+ import fs2 from "node:fs";
2135
+
2136
+ // src/webview.js
2137
+ import { randomUUID, createHash as createHash3 } from "node:crypto";
2138
+ var DEFAULT_TIMEOUT_MS = 1e4;
2139
+ var DEFAULT_STABLE_TIME_MS = 5e3;
2140
+ var DEFAULT_POLL_INTERVAL_MS = 500;
2141
+ var WebView = {
2142
+ configureHook,
2143
+ evaluate,
2144
+ list,
2145
+ snapshot: snapshot2,
2146
+ waitForStable,
2147
+ click: click2
2148
+ };
2149
+ async function configureHook(ctx, actorInfo, options = {}) {
2150
+ const webviews = Array.isArray(actorInfo?.webviews) ? actorInfo.webviews : [];
2151
+ if (webviews.length === 0) return false;
2152
+ const packageName = requireNonEmpty3(ctx?.packageName, "packageName");
2153
+ const dataDir = appDataDir2(packageName);
2154
+ const baseDir = `${dataDir}/cache/${WebViewBridge.cacheDir}`;
2155
+ const configPath = `${baseDir}/${WebViewBridge.hookConfigFile}`;
2156
+ const configJson = JSON.stringify({ webviews });
2157
+ await Device.writeCacheFile(ctx, {
2158
+ dirName: WebViewBridge.cacheDir,
2159
+ fileName: WebViewBridge.hookConfigFile,
2160
+ text: configJson,
2161
+ timeoutMs: Math.max(5e3, Number(options.timeoutMs || DEFAULT_TIMEOUT_MS)),
2162
+ maxBuffer: 2 * 1024 * 1024
2163
+ }).catch((error) => {
2164
+ throw new CrawlerError({
2165
+ message: `app_runtime_unavailable: WebView hook \u914D\u7F6E\u5199\u5165\u5931\u8D25 ${error?.message || String(error)}`,
2166
+ code: Code.AppRuntimeUnavailable,
2167
+ context: { configPath }
2168
+ });
2169
+ });
2170
+ return true;
2171
+ }
2172
+ async function list(ctx, options = {}) {
2173
+ const command = {
2174
+ id: String(options.id || randomUUID()),
2175
+ action: "list",
2176
+ urlIncludes: normalizeStringArray2(options.webviews || options.urlIncludes || options.actorInfo?.webviews),
2177
+ timeoutMs: Math.max(1, Number(options.timeoutMs || DEFAULT_TIMEOUT_MS))
2178
+ };
2179
+ const result = normalizeBridgeResult(await runBridgeCommand(ctx, command, options));
2180
+ if (!result.ok) {
2181
+ throw new CrawlerError({
2182
+ message: `app_runtime_unavailable: WebView bridge \u5217\u8868\u8BFB\u53D6\u5931\u8D25 ${result.error || "unknown error"}`,
2183
+ code: Code.AppRuntimeUnavailable,
2184
+ context: { id: command.id }
2185
+ });
2186
+ }
2187
+ const value = parseBridgeValue(result.value);
2188
+ return Array.isArray(value) ? value : [];
2189
+ }
2190
+ async function evaluate(ctx, js, options = {}) {
2191
+ const command = {
2192
+ id: String(options.id || randomUUID()),
2193
+ action: "evaluate",
2194
+ urlIncludes: normalizeStringArray2(options.webviews || options.urlIncludes || options.actorInfo?.webviews),
2195
+ js: String(js || ""),
2196
+ timeoutMs: Math.max(1, Number(options.timeoutMs || DEFAULT_TIMEOUT_MS))
2197
+ };
2198
+ if (!command.js.trim()) {
2199
+ throw new CrawlerError({
2200
+ message: "invalid_request: WebView.evaluate \u7F3A\u5C11 js",
2201
+ code: Code.InvalidRequest
2202
+ });
2203
+ }
2204
+ const result = normalizeBridgeResult(await runBridgeCommand(ctx, command, options));
2205
+ if (!result.ok) {
2206
+ throw new CrawlerError({
2207
+ message: `app_runtime_unavailable: WebView bridge \u6267\u884C\u5931\u8D25 ${result.error || "unknown error"}`,
2208
+ code: Code.AppRuntimeUnavailable,
2209
+ context: { id: command.id, url: result.url || "", title: result.title || "" }
2210
+ });
2211
+ }
2212
+ return result;
2213
+ }
2214
+ async function snapshot2(ctx, options = {}) {
2215
+ const result = await evaluate(ctx, `(() => ({
2216
+ url: location.href,
2217
+ title: document.title,
2218
+ text: document.body ? document.body.innerText : '',
2219
+ html: document.documentElement ? document.documentElement.outerHTML : ''
2220
+ }))()`, options);
2221
+ const value = parseBridgeValue(result.value);
2222
+ const out = value && typeof value === "object" ? value : {};
2223
+ return {
2224
+ url: String(out.url || result.url || ""),
2225
+ title: String(out.title || result.title || ""),
2226
+ text: String(out.text || ""),
2227
+ html: String(out.html || ""),
2228
+ raw: result
2229
+ };
2230
+ }
2231
+ async function waitForStable(ctx, options = {}) {
2232
+ const timeout = Math.max(0, Number(options.timeout ?? options.timeoutMs ?? 12e4));
2233
+ const stableTime = Math.max(0, Number(options.stableTime ?? DEFAULT_STABLE_TIME_MS));
2234
+ const pollIntervalMs = Math.max(1, Number(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS));
2235
+ const startedAt = Date.now();
2236
+ const deadline = timeout === 0 ? Infinity : startedAt + timeout;
2237
+ let lastSnapshot = null;
2238
+ let stableSince = 0;
2239
+ let mutationCount = 0;
2240
+ let snapshotErrorCount = 0;
2241
+ let lastSnapshotError = null;
2242
+ const logTemplate = Logger.useTemplate();
2243
+ logTemplate.webViewCommand({
2244
+ action: "waitForStable",
2245
+ webviews: normalizeStringArray2(options.webviews || options.urlIncludes || options.actorInfo?.webviews).join(","),
2246
+ stableTime,
2247
+ timeout,
2248
+ pollIntervalMs
2249
+ });
2250
+ while (Date.now() < deadline) {
2251
+ const current = await snapshot2(ctx, options).catch((error) => {
2252
+ snapshotErrorCount += 1;
2253
+ lastSnapshotError = error;
2254
+ if (snapshotErrorCount === 1 || snapshotErrorCount % 5 === 0) {
2255
+ logTemplate.webViewWarn({
2256
+ action: "waitForStable.snapshot",
2257
+ snapshotErrorCount,
2258
+ message: error?.message || String(error)
2259
+ });
2260
+ }
2261
+ return null;
2262
+ });
2263
+ if (!current) {
2264
+ await sleep(pollIntervalMs);
2265
+ continue;
2266
+ }
2267
+ const hash = hashSnapshot(current);
2268
+ const changed = !lastSnapshot || hash !== lastSnapshot.hash;
2269
+ if (changed) {
2270
+ if (lastSnapshot) mutationCount += 1;
2271
+ lastSnapshot = { ...current, hash };
2272
+ stableSince = Date.now();
2273
+ } else if (Date.now() - stableSince >= stableTime) {
2274
+ const result = {
2275
+ mutationCount,
2276
+ stableTime,
2277
+ html: current.html,
2278
+ text: current.text,
2279
+ url: current.url,
2280
+ title: current.title,
2281
+ mutationNodes: [{
2282
+ html: current.html,
2283
+ text: current.text,
2284
+ mutationType: "stable",
2285
+ url: current.url,
2286
+ title: current.title
2287
+ }]
2288
+ };
2289
+ logTemplate.webViewSuccess({
2290
+ action: "waitForStable",
2291
+ duration: Logger.duration(startedAt),
2292
+ mutationCount,
2293
+ textChars: current.text.length,
2294
+ htmlChars: current.html.length
2295
+ });
2296
+ return result;
2297
+ }
2298
+ await sleep(pollIntervalMs);
2299
+ }
2300
+ throw new CrawlerError({
2301
+ message: `app_runtime_unavailable: WebView DOM \u7A33\u5B9A\u7B49\u5F85\u8D85\u65F6 (${timeout}ms), snapshotErrorCount=${snapshotErrorCount}, lastSnapshotError=${lastSnapshotError?.message || ""}`,
2302
+ code: Code.AppRuntimeUnavailable
2303
+ });
2304
+ }
2305
+ async function click2(ctx, target, options = {}) {
2306
+ const request = target && typeof target === "object" && !Array.isArray(target) ? target : { selector: target };
2307
+ const js = request.js || options.js || buildClickScript(request.selector, {
2308
+ textContains: request.textContains || options.textContains,
2309
+ last: request.last ?? options.last
2310
+ });
2311
+ const result = await evaluate(ctx, js, options);
2312
+ const value = parseBridgeValue(result.value);
2313
+ if (value === false || value?.ok === false) {
2314
+ throw new CrawlerError({
2315
+ message: `app_runtime_unavailable: WebView \u70B9\u51FB\u5931\u8D25 ${value?.reason || value?.error || options.label || request.selector || ""}`.trim(),
2316
+ code: Code.AppRuntimeUnavailable,
2317
+ context: { value }
2318
+ });
2319
+ }
2320
+ Logger.useTemplate().webViewSuccess({
2321
+ action: "click",
2322
+ label: options.label || request.selector || "",
2323
+ value: typeof value === "object" ? value : void 0
2324
+ });
2325
+ return value ?? result.value;
2326
+ }
2327
+ async function runBridgeCommand(ctx, command, options = {}) {
2328
+ if (typeof options.commandRunner === "function") return options.commandRunner(ctx, command);
2329
+ const packageName = requireNonEmpty3(ctx?.packageName, "packageName");
2330
+ const dataDir = appDataDir2(packageName);
2331
+ const baseDir = `${dataDir}/cache/${WebViewBridge.cacheDir}`;
2332
+ const commandPath = `${baseDir}/${WebViewBridge.commandDir}/${command.id}.json`;
2333
+ const resultPath = `${baseDir}/${WebViewBridge.resultDir}/${command.id}.json`;
2334
+ const commandJson = JSON.stringify(command);
2335
+ await adbSuShell2(ctx, [
2336
+ `owner=$(stat -c %u:%g ${shellQuote2(dataDir)})`,
2337
+ `mkdir -p ${shellQuote2(`${baseDir}/${WebViewBridge.commandDir}`)} ${shellQuote2(`${baseDir}/${WebViewBridge.resultDir}`)}`,
2338
+ `chown "$owner" ${shellQuote2(baseDir)} ${shellQuote2(`${baseDir}/${WebViewBridge.commandDir}`)} ${shellQuote2(`${baseDir}/${WebViewBridge.resultDir}`)}`,
2339
+ `chmod 2775 ${shellQuote2(baseDir)} ${shellQuote2(`${baseDir}/${WebViewBridge.commandDir}`)} ${shellQuote2(`${baseDir}/${WebViewBridge.resultDir}`)}`,
2340
+ `rm -f ${shellQuote2(resultPath)}`,
2341
+ `printf %s ${shellQuote2(commandJson)} > ${shellQuote2(commandPath)}`,
2342
+ `chown "$owner" ${shellQuote2(commandPath)}`,
2343
+ `chmod 664 ${shellQuote2(commandPath)}`
2344
+ ].join(" && "), {
2345
+ timeoutMs: Math.max(5e3, Number(options.timeoutMs || DEFAULT_TIMEOUT_MS)),
2346
+ maxBuffer: 2 * 1024 * 1024
2347
+ }).catch((error) => {
2348
+ throw new CrawlerError({
2349
+ message: `app_runtime_unavailable: WebView bridge \u547D\u4EE4\u5199\u5165\u5931\u8D25 ${error?.message || String(error)}`,
2350
+ code: Code.AppRuntimeUnavailable,
2351
+ context: { commandPath }
2352
+ });
2353
+ });
2354
+ await Device.adbShell(ctx, [
2355
+ "am",
2356
+ "broadcast",
2357
+ "-a",
2358
+ WebViewBridge.actionCommand,
2359
+ "--es",
2360
+ "id",
2361
+ command.id,
2362
+ "-p",
2363
+ packageName
2364
+ ], {
2365
+ timeoutMs: 8e3,
2366
+ maxBuffer: 1024 * 1024
2367
+ }).catch((error) => {
2368
+ throw new CrawlerError({
2369
+ message: `app_runtime_unavailable: WebView bridge \u5E7F\u64AD\u5931\u8D25 ${error?.message || String(error)}`,
2370
+ code: Code.AppRuntimeUnavailable,
2371
+ context: { action: WebViewBridge.actionCommand, id: command.id }
2372
+ });
2373
+ });
2374
+ const deadline = Date.now() + Math.max(1, Number(options.timeoutMs || command.timeoutMs || DEFAULT_TIMEOUT_MS));
2375
+ let lastRaw = "";
2376
+ while (Date.now() < deadline) {
2377
+ lastRaw = await adbSuShell2(ctx, `[ -f ${shellQuote2(resultPath)} ] && cat ${shellQuote2(resultPath)} || true`, {
2378
+ timeoutMs: 5e3,
2379
+ maxBuffer: Number(options.maxBuffer || 4 * 1024 * 1024)
2380
+ }).catch(() => "");
2381
+ if (String(lastRaw || "").trim()) {
2382
+ await adbSuShell2(ctx, `rm -f ${shellQuote2(commandPath)} ${shellQuote2(resultPath)}`, { timeoutMs: 5e3 }).catch(() => {
2383
+ });
2384
+ try {
2385
+ return JSON.parse(lastRaw);
2386
+ } catch (error) {
2387
+ throw new CrawlerError({
2388
+ message: `app_runtime_unavailable: WebView bridge \u8FD4\u56DE\u4E86\u65E0\u6548 JSON ${error?.message || String(error)}`,
2389
+ code: Code.AppRuntimeUnavailable,
2390
+ context: { id: command.id, resultPath, raw: String(lastRaw || "").slice(0, 1e3) }
2391
+ });
2392
+ }
2393
+ }
2394
+ await sleep(Math.max(100, Number(options.resultPollIntervalMs || 250)));
2395
+ }
2396
+ throw new CrawlerError({
2397
+ message: `app_runtime_unavailable: WebView bridge \u672A\u8FD4\u56DE\u7ED3\u679C (${command.timeoutMs}ms)`,
2398
+ code: Code.AppRuntimeUnavailable,
2399
+ context: { id: command.id, resultPath, lastRaw: String(lastRaw || "").slice(0, 500) }
2400
+ });
2401
+ }
2402
+ function buildClickScript(selector, options = {}) {
2403
+ const selectorJson = JSON.stringify(String(selector || 'button,a,[role="button"],[onclick],[aria-label],[title]'));
2404
+ const textJson = JSON.stringify(String(options.textContains || ""));
2405
+ const useLast = options.last !== false;
2406
+ return `(() => {
2407
+ const selector = ${selectorJson};
2408
+ const textContains = ${textJson};
2409
+ const visible = (el) => {
2410
+ const rect = el.getBoundingClientRect();
2411
+ const style = getComputedStyle(el);
2412
+ return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
2413
+ };
2414
+ const inViewport = (el) => {
2415
+ const rect = el.getBoundingClientRect();
2416
+ return rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth;
2417
+ };
2418
+ const label = (el) => [el.innerText, el.textContent, el.getAttribute('aria-label'), el.getAttribute('title'), el.getAttribute('class')]
2419
+ .filter(Boolean).join(' ').trim();
2420
+ const elements = Array.from(document.querySelectorAll(selector))
2421
+ .map((el) => el.closest('button,a,[role="button"],[onclick]') || el)
2422
+ .filter((el, index, arr) => el && arr.indexOf(el) === index && visible(el))
2423
+ .filter((el) => !textContains || label(el).includes(textContains));
2424
+ const el = elements[${useLast ? "elements.length - 1" : "0"}];
2425
+ if (!el) return { ok: false, reason: 'element_not_found', selector, textContains, candidateCount: elements.length };
2426
+ el.scrollIntoView({ block: 'center', inline: 'center' });
2427
+ if (!inViewport(el)) return { ok: false, reason: 'element_out_of_viewport', selector, textContains, candidateCount: elements.length };
2428
+ el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
2429
+ if (typeof el.click === 'function') el.click();
2430
+ const rect = el.getBoundingClientRect();
2431
+ return { ok: true, selector, text: label(el).slice(0, 120), rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height }, candidateCount: elements.length };
2432
+ })()`;
2433
+ }
2434
+ function normalizeBridgeResult(value) {
2435
+ const result = value && typeof value === "object" ? value : {};
2436
+ return {
2437
+ id: String(result.id || ""),
2438
+ ok: result.ok !== false,
2439
+ url: String(result.url || ""),
2440
+ title: String(result.title || ""),
2441
+ value: result.value,
2442
+ error: String(result.error || "")
2443
+ };
2444
+ }
2445
+ function parseBridgeValue(value) {
2446
+ let current = value;
2447
+ for (let i = 0; i < 2; i += 1) {
2448
+ if (typeof current !== "string") return current;
2449
+ const text2 = current.trim();
2450
+ if (!text2) return "";
2451
+ try {
2452
+ current = JSON.parse(text2);
2453
+ } catch {
2454
+ return current;
2455
+ }
2456
+ }
2457
+ return current;
2458
+ }
2459
+ function hashSnapshot(value) {
2460
+ return createHash3("sha256").update(String(value?.url || "")).update("\n").update(String(value?.title || "")).update("\n").update(String(value?.html || value?.text || "")).digest("hex");
2461
+ }
2462
+ async function adbSuShell2(ctx, command, options = {}) {
2463
+ return Device.adbShell(ctx, [`su -c ${shellQuote2(command)}`], options);
2464
+ }
2465
+ function appDataDir2(packageName) {
2466
+ return `/data/data/${String(packageName || "").trim()}`;
2467
+ }
2468
+ function normalizeStringArray2(value) {
2469
+ const result = [];
2470
+ const add = (item) => {
2471
+ if (Array.isArray(item)) {
2472
+ item.forEach(add);
2473
+ return;
2474
+ }
2475
+ if (item && typeof item === "object") {
2476
+ add(item.value);
2477
+ return;
2478
+ }
2479
+ const text2 = String(item ?? "").trim();
2480
+ if (text2) result.push(text2);
2481
+ };
2482
+ add(value);
2483
+ return result;
2484
+ }
2485
+ function requireNonEmpty3(value, name) {
2486
+ const clean = String(value ?? "").trim();
2487
+ if (!clean) throw new CrawlerError({ message: `invalid_request: ${name} is required`, code: Code.InvalidRequest });
2488
+ return clean;
2489
+ }
2490
+ function shellQuote2(value) {
2491
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
2492
+ }
2493
+
2494
+ // src/mutation.js
2495
+ var MUTATION_MONITOR_MODE = Object.freeze({
2496
+ Added: "added",
2497
+ Changed: "changed",
2498
+ All: "all"
2499
+ });
2500
+ var Mutation = {
2501
+ Mode: MUTATION_MONITOR_MODE,
2502
+ waitForStable: waitForStable2,
2503
+ waitForStableAcrossRoots,
2504
+ useMonitor
2505
+ };
2506
+ async function waitForStable2(ctx, selectors, options = {}) {
2507
+ if (options.mode === "webView") {
2508
+ return WebView.waitForStable(ctx, options);
2509
+ }
2510
+ const selectorList = normalizeSelectors(selectors);
2511
+ const initialTimeout = Math.max(0, Number(options.initialTimeout ?? 3e4));
2512
+ const stableTime = Math.max(0, Number(options.stableTime ?? 5e3));
2513
+ const timeout = Math.max(0, Number(options.timeout ?? 12e4));
2514
+ const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs ?? 500));
2515
+ const onMutation = typeof options.onMutation === "function" ? options.onMutation : null;
2516
+ const startedAt = Date.now();
2517
+ const deadline = timeout === 0 ? Infinity : startedAt + timeout;
2518
+ const initialDeadline = initialTimeout === 0 ? startedAt : startedAt + initialTimeout;
2519
+ const logTemplate = Logger.useTemplate();
2520
+ logTemplate.mutationWaitStart({
2521
+ selectors: selectorList.map(selectorLabel2).join(","),
2522
+ stableTime,
2523
+ timeout,
2524
+ pollIntervalMs
2525
+ });
2526
+ let foundInitial = false;
2527
+ let lastSnapshot = null;
2528
+ let stableSince = 0;
2529
+ let mutationCount = 0;
2530
+ let wasPaused = false;
2531
+ let snapshotErrorCount = 0;
2532
+ let lastSnapshotError = null;
2533
+ while (Date.now() < deadline) {
2534
+ const snapshot3 = await captureSnapshot(ctx, selectorList, options).catch((error) => {
2535
+ snapshotErrorCount += 1;
2536
+ lastSnapshotError = error;
2537
+ if (snapshotErrorCount === 1 || snapshotErrorCount % 5 === 0) {
2538
+ logTemplate.mutationWaitWarn({
2539
+ snapshotErrorCount,
2540
+ message: error?.message || String(error)
2541
+ });
2542
+ }
2543
+ return null;
2544
+ });
2545
+ if (!snapshot3) {
2546
+ await sleep(pollIntervalMs);
2547
+ continue;
2548
+ }
2549
+ if (!foundInitial) {
2550
+ if (snapshot3.found || Date.now() >= initialDeadline) {
2551
+ foundInitial = true;
2552
+ if (!snapshot3.found) logTemplate.mutationWaitWarn({ message: "\u521D\u59CB\u7B49\u5F85\u672A\u627E\u5230\u8282\u70B9", initialTimeout });
2553
+ } else {
2554
+ await sleep(pollIntervalMs);
2555
+ continue;
2556
+ }
2557
+ }
2558
+ const changed = !lastSnapshot || snapshot3.hash !== lastSnapshot.hash;
2559
+ if (changed) {
2560
+ if (lastSnapshot) mutationCount += 1;
2561
+ lastSnapshot = snapshot3;
2562
+ }
2563
+ let paused = false;
2564
+ if ((changed || !stableSince) && onMutation) {
2565
+ const signal = await onMutation({
2566
+ mutationCount,
2567
+ html: snapshot3.html,
2568
+ text: snapshot3.text,
2569
+ mutationNodes: snapshot3.mutationNodes
2570
+ });
2571
+ paused = !(signal === null || signal === void 0);
2572
+ wasPaused = wasPaused || paused;
2573
+ }
2574
+ if (paused) {
2575
+ stableSince = 0;
2576
+ } else if (changed || !stableSince) {
2577
+ stableSince = Date.now();
2578
+ } else if (Date.now() - stableSince >= stableTime) {
2579
+ const result = {
2580
+ mutationCount,
2581
+ stableTime,
2582
+ wasPaused,
2583
+ html: snapshot3.html,
2584
+ text: snapshot3.text,
2585
+ mutationNodes: snapshot3.mutationNodes
2586
+ };
2587
+ logTemplate.mutationWaitSuccess({
2588
+ duration: Logger.duration(startedAt),
2589
+ mutationCount,
2590
+ wasPaused
2591
+ });
2592
+ return result;
2593
+ }
2594
+ await sleep(pollIntervalMs);
2595
+ }
2596
+ throw new Error(`Mutation.waitForStable \u8D85\u65F6 (${timeout}ms), \u5DF2\u68C0\u6D4B\u5230 ${mutationCount} \u6B21\u53D8\u5316, snapshotErrorCount=${snapshotErrorCount}, lastSnapshotError=${lastSnapshotError?.message || ""}`);
2597
+ }
2598
+ async function waitForStableAcrossRoots(ctx, selectors, options = {}) {
2599
+ return waitForStable2(ctx, selectors, options);
2600
+ }
2601
+ async function useMonitor(ctx, selectors, options = {}) {
2602
+ const selectorList = normalizeSelectors(selectors);
2603
+ const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs ?? 500));
2604
+ const onMutation = typeof options.onMutation === "function" ? options.onMutation : null;
2605
+ let stopped = false;
2606
+ let totalMutations = 0;
2607
+ let lastHash = "";
2608
+ const loop = async () => {
2609
+ while (!stopped) {
2610
+ const snapshot3 = await captureSnapshot(ctx, selectorList, options).catch(() => null);
2611
+ if (snapshot3 && snapshot3.hash !== lastHash) {
2612
+ if (lastHash) totalMutations += 1;
2613
+ lastHash = snapshot3.hash;
2614
+ if (onMutation) {
2615
+ await onMutation({
2616
+ mutationCount: totalMutations,
2617
+ html: snapshot3.html,
2618
+ text: snapshot3.text,
2619
+ mutationNodes: snapshot3.mutationNodes
2620
+ });
2621
+ }
2622
+ }
2623
+ await sleep(pollIntervalMs);
2624
+ }
2625
+ };
2626
+ loop();
2627
+ return {
2628
+ stop: async () => {
2629
+ stopped = true;
2630
+ return { totalMutations };
2631
+ }
2632
+ };
2633
+ }
2634
+ async function captureSnapshot(ctx, selectors, options = {}) {
2635
+ const view = await DeviceView.snapshot(ctx, options);
2636
+ const matched = selectors.length ? selectors.flatMap((selector) => view.flat.filter((node) => matchesSelector2(node, selector, options))) : view.flat;
2637
+ const unique = uniqueNodes(matched);
2638
+ const mutationNodes = unique.map((node) => ({
2639
+ html: nodeToText(node),
2640
+ text: [node.text, node.contentDesc].filter(Boolean).join(" ").trim(),
2641
+ mutationType: "stable",
2642
+ resourceId: node.resourceId,
2643
+ className: node.className,
2644
+ bounds: node.bounds
2645
+ }));
2646
+ const html = mutationNodes.map((node) => node.html).join("\n");
2647
+ const text2 = mutationNodes.map((node) => node.text).filter(Boolean).join("\n");
2648
+ return {
2649
+ found: unique.length > 0,
2650
+ html,
2651
+ text: text2,
2652
+ mutationNodes,
2653
+ hash: unique.map((node) => DeviceView.hashNode(node)).join("|")
2654
+ };
2655
+ }
2656
+ function normalizeSelectors(selectors) {
2657
+ if (selectors == null) return [];
2658
+ const values = Array.isArray(selectors) ? selectors : [selectors];
2659
+ return values.map(normalizeSelector2).filter(Boolean);
2660
+ }
2661
+ function normalizeSelector2(selector) {
2662
+ if (selector === "*" || selector == null) return null;
2663
+ if (typeof selector === "object") return selector;
2664
+ const text2 = String(selector || "").trim();
2665
+ if (!text2) return null;
2666
+ return { id: text2.replace(/^#/, "") };
2667
+ }
2668
+ function matchesSelector2(node, selector, options) {
2669
+ if (!selector) return true;
2670
+ if (options.visibleOnly !== false && !node.visible) return false;
2671
+ if (selector.id) {
2672
+ const id = String(selector.id || "");
2673
+ if (node.resourceId !== id && node.id !== id && !node.resourceId.endsWith(`:id/${id}`)) return false;
2674
+ }
2675
+ if (selector.text && node.text !== selector.text) return false;
2676
+ if (selector.textContains && !node.text.includes(selector.textContains)) return false;
2677
+ if (selector.contentDesc && node.contentDesc !== selector.contentDesc) return false;
2678
+ return true;
2679
+ }
2680
+ function uniqueNodes(nodes) {
2681
+ const seen = /* @__PURE__ */ new Set();
2682
+ const out = [];
2683
+ for (const node of nodes) {
2684
+ const key = `${node.resourceId}|${node.bounds.left},${node.bounds.top},${node.bounds.right},${node.bounds.bottom}|${node.text}|${node.contentDesc}`;
2685
+ if (seen.has(key)) continue;
2686
+ seen.add(key);
2687
+ out.push(node);
2688
+ }
2689
+ return out;
2690
+ }
2691
+ function nodeToText(node) {
2692
+ return JSON.stringify({
2693
+ resourceId: node.resourceId,
2694
+ text: node.text,
2695
+ contentDesc: node.contentDesc,
2696
+ className: node.className,
2697
+ bounds: node.bounds,
2698
+ children: node.children?.length || 0
2699
+ });
2700
+ }
2701
+ function selectorLabel2(selector) {
2702
+ try {
2703
+ return JSON.stringify(selector);
2704
+ } catch {
2705
+ return String(selector);
2706
+ }
2707
+ }
2708
+
2709
+ // src/network-capture.js
2710
+ var CAPTURE_DIR_NAME = "network-capture";
2711
+ var RULES_FILE = "rules.json";
2712
+ var DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
2713
+ var NetworkCapture = {
2714
+ path: path2,
2715
+ clear,
2716
+ writeRules,
2717
+ readText
2718
+ };
2719
+ function path2(ctx, fileName = "") {
2720
+ const base = `${appDataDir3(ctx)}/cache/${CAPTURE_DIR_NAME}`;
2721
+ return fileName ? `${base}/${cleanFileName(fileName)}` : base;
2722
+ }
2723
+ async function clear(ctx, options = {}) {
2724
+ const dirPath = options.dirPath || path2(ctx);
2725
+ await adbSuShell3(ctx, `rm -rf ${shellQuote3(dirPath)}`, {
2726
+ timeoutMs: 1e4,
2727
+ maxBuffer: 1024
2728
+ });
2729
+ return true;
2730
+ }
2731
+ async function writeRules(ctx, { rulesJson } = {}) {
2732
+ const text2 = typeof rulesJson === "string" ? rulesJson : JSON.stringify(rulesJson || { rules: [] });
2733
+ await Device.writeCacheFile(ctx, {
2734
+ dirName: CAPTURE_DIR_NAME,
2735
+ fileName: RULES_FILE,
2736
+ text: text2,
2737
+ timeoutMs: 1e4,
2738
+ maxBuffer: 2 * 1024 * 1024
2739
+ });
2740
+ return true;
2741
+ }
2742
+ async function readText(ctx, fileName, options = {}) {
2743
+ return adbSuShell3(ctx, `cat ${shellQuote3(path2(ctx, fileName))} 2>/dev/null || true`, {
2744
+ timeoutMs: 1e4,
2745
+ maxBuffer: Number(options.maxBuffer || DEFAULT_MAX_BYTES)
2746
+ }).catch(() => "");
2747
+ }
2748
+ async function adbSuShell3(ctx, command, options = {}) {
2749
+ return Device.adbShell(ctx, [`su -c ${shellQuote3(command)}`], options);
2750
+ }
2751
+ function appDataDir3(ctx) {
2752
+ const packageName = String(ctx?.packageName || "").trim();
2753
+ if (!packageName) throw new Error("packageName is required");
2754
+ return `/data/data/${packageName}`;
2755
+ }
2756
+ function cleanFileName(value) {
2757
+ const text2 = String(value || "").trim();
2758
+ if (!text2 || text2.includes("/") || text2.includes("\\") || text2 === "." || text2 === "..") {
2759
+ throw new Error("invalid network capture file name");
2760
+ }
2761
+ return text2;
2762
+ }
2763
+ function shellQuote3(value) {
2764
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
2765
+ }
2766
+
2767
+ // src/share.js
2768
+ import { createHash as createHash4 } from "node:crypto";
2769
+ import { Jimp as Jimp2, JimpMime as JimpMime2 } from "jimp";
2770
+
2771
+ // src/internals/compression.js
2772
+ import { Jimp, JimpMime, ResizeStrategy } from "jimp";
2773
+ var DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
2774
+ var DEFAULT_SCREENSHOT_OUTPUT_TYPE = "jpeg";
2775
+ var DEFAULT_SCREENSHOT_QUALITY = 0.72;
2776
+ var DEFAULT_SCREENSHOT_MIN_QUALITY = 0.38;
2777
+ var DEFAULT_SCREENSHOT_MIN_SCALE = 0.25;
2778
+ var SUPPORTED_SCREENSHOT_OUTPUT_TYPES = /* @__PURE__ */ new Set(["jpeg"]);
2779
+ var toPositiveInteger = (value, fallback = 0) => {
2780
+ const number = Math.floor(Number(value) || 0);
2781
+ return number > 0 ? number : fallback;
2782
+ };
2783
+ var normalizeQuality = (value, fallback) => {
2784
+ const number = Number(value);
2785
+ if (!Number.isFinite(number) || number <= 0) return fallback;
2786
+ const normalized = number > 1 ? number / 100 : number;
2787
+ return Math.min(1, Math.max(0.01, normalized));
2788
+ };
2789
+ var normalizeScale = (value, fallback) => {
2790
+ const number = Number(value);
2791
+ if (!Number.isFinite(number) || number <= 0) return fallback;
2792
+ return Math.min(1, Math.max(0.05, number));
2793
+ };
2794
+ var normalizeScreenshotOutputType = (value) => {
2795
+ const raw = String(value || DEFAULT_SCREENSHOT_OUTPUT_TYPE).trim().toLowerCase();
2796
+ const normalized = raw === "jpg" ? "jpeg" : raw;
2797
+ return SUPPORTED_SCREENSHOT_OUTPUT_TYPES.has(normalized) ? normalized : DEFAULT_SCREENSHOT_OUTPUT_TYPE;
2798
+ };
2799
+ var getBase64BytesFromBuffer = (buffer) => Math.ceil(buffer.length / 3) * 4;
2800
+ var toJpegQuality = (value) => Math.round(normalizeQuality(value, DEFAULT_SCREENSHOT_QUALITY) * 100);
2801
+ var resolveImageCompression = (options = {}) => {
2802
+ const explicit = options.compression;
2803
+ const source = explicit && typeof explicit === "object" && !Array.isArray(explicit) ? explicit : {};
2804
+ const enabled = explicit !== false && source.enabled !== false && options.compress !== false;
2805
+ const quality = normalizeQuality(
2806
+ source.quality ?? options.quality,
2807
+ DEFAULT_SCREENSHOT_QUALITY
2808
+ );
2809
+ const minQuality = Math.min(
2810
+ quality,
2811
+ normalizeQuality(source.minQuality ?? options.minQuality, DEFAULT_SCREENSHOT_MIN_QUALITY)
2812
+ );
2813
+ return {
2814
+ enabled,
2815
+ maxBytes: toPositiveInteger(
2816
+ source.maxBytes ?? source.maxBase64Bytes ?? options.maxBytes ?? options.maxBase64Bytes,
2817
+ DEFAULT_SCREENSHOT_MAX_BYTES
2818
+ ),
2819
+ outputType: normalizeScreenshotOutputType(
2820
+ source.type ?? source.outputType ?? options.type ?? options.outputType
2821
+ ),
2822
+ quality,
2823
+ minQuality,
2824
+ minScale: normalizeScale(
2825
+ source.minScale ?? options.minScale,
2826
+ DEFAULT_SCREENSHOT_MIN_SCALE
2827
+ )
2828
+ };
2829
+ };
2830
+ var encodeJpeg = async (sourceImage, compression, scale, quality) => {
2831
+ const width = Math.max(1, Math.round(sourceImage.bitmap.width * scale));
2832
+ const height = Math.max(1, Math.round(sourceImage.bitmap.height * scale));
2833
+ const image = sourceImage.clone();
2834
+ if (scale < 0.999) {
2835
+ image.resize({
2836
+ w: width,
2837
+ h: height,
2838
+ mode: ResizeStrategy.BILINEAR
2839
+ });
2840
+ }
2841
+ const buffer = await image.getBuffer(JimpMime.jpeg, { quality });
2842
+ return {
2843
+ buffer,
2844
+ bytes: getBase64BytesFromBuffer(buffer),
2845
+ width,
2846
+ height,
2847
+ quality,
2848
+ scale: Number(scale.toFixed(3)),
2849
+ format: compression.outputType
2850
+ };
2851
+ };
2852
+ var rawPngResult = (buffer) => ({
2853
+ buffer,
2854
+ bytes: getBase64BytesFromBuffer(buffer),
2855
+ format: "png",
2856
+ mimeType: "image/png",
2857
+ withinLimit: true
2858
+ });
2859
+ var mimeTypeForFormat = (format) => format === "jpeg" ? "image/jpeg" : "image/png";
2860
+ var compressImageBuffer = async (buffer, compression) => {
2861
+ const sourceImage = await Jimp.read(buffer);
2862
+ const maxQuality = toJpegQuality(compression.quality);
2863
+ const minQuality = Math.min(maxQuality, toJpegQuality(compression.minQuality));
2864
+ let quality = maxQuality;
2865
+ let scale = 1;
2866
+ let smallest = null;
2867
+ for (let attempt = 0; attempt < 12; attempt += 1) {
2868
+ const candidate = await encodeJpeg(sourceImage, compression, scale, quality);
2869
+ if (!smallest || candidate.bytes < smallest.bytes) smallest = candidate;
2870
+ if (candidate.bytes <= compression.maxBytes) {
2871
+ return { ...candidate, mimeType: mimeTypeForFormat(candidate.format), withinLimit: true };
2872
+ }
2873
+ if (quality > minQuality) {
2874
+ quality = Math.max(minQuality, Math.floor(quality * 0.75));
2875
+ continue;
2876
+ }
2877
+ const ratio = Math.sqrt(compression.maxBytes / Math.max(1, candidate.bytes));
2878
+ const nextScale = Math.max(
2879
+ compression.minScale,
2880
+ Math.min(scale * 0.85, scale * ratio * 0.94)
2881
+ );
2882
+ if (nextScale >= scale * 0.99 || scale <= compression.minScale) break;
2883
+ scale = nextScale;
2884
+ }
2885
+ const finalCandidate = await encodeJpeg(sourceImage, compression, compression.minScale, minQuality);
2886
+ const fallback = !smallest || finalCandidate.bytes < smallest.bytes ? finalCandidate : smallest;
2887
+ return {
2888
+ ...fallback,
2889
+ mimeType: mimeTypeForFormat(fallback.format),
2890
+ withinLimit: fallback.bytes <= compression.maxBytes
2891
+ };
2892
+ };
2893
+ var compressImageBufferToResult = async (buffer, compression) => {
2894
+ const originalBytes = getBase64BytesFromBuffer(buffer);
2895
+ if (!compression.enabled || originalBytes <= compression.maxBytes) {
2896
+ const raw = rawPngResult(buffer);
2897
+ return {
2898
+ ...raw,
2899
+ base64: raw.buffer.toString("base64")
2900
+ };
2901
+ }
2902
+ const result = await compressImageBuffer(buffer, compression).catch((error) => {
2903
+ Logger.warn("captureScreen \u538B\u7F29\u5931\u8D25\uFF0C\u8FD4\u56DE\u539F\u56FE", { message: error?.message || String(error) });
2904
+ return null;
2905
+ });
2906
+ if (!result?.buffer) {
2907
+ const raw = rawPngResult(buffer);
2908
+ return {
2909
+ ...raw,
2910
+ base64: raw.buffer.toString("base64")
2911
+ };
2912
+ }
2913
+ if (result.withinLimit) {
2914
+ Logger.info("captureScreen \u5DF2\u538B\u7F29", {
2915
+ originalBytes,
2916
+ outputBytes: result.bytes,
2917
+ format: result.format,
2918
+ quality: result.quality,
2919
+ scale: result.scale,
2920
+ size: `${result.width}x${result.height}`
2921
+ });
2922
+ } else {
2923
+ Logger.warn("captureScreen \u538B\u7F29\u540E\u4ECD\u8D85\u8FC7\u76EE\u6807", {
2924
+ originalBytes,
2925
+ outputBytes: result.bytes,
2926
+ maxBytes: compression.maxBytes,
2927
+ format: result.format,
2928
+ quality: result.quality,
2929
+ scale: result.scale
2930
+ });
2931
+ }
2932
+ return {
2933
+ ...result,
2934
+ base64: result.buffer.toString("base64")
2935
+ };
2936
+ };
2937
+
2938
+ // src/share.js
2939
+ var DEFAULT_CAPTURE_COUNT = 9;
2940
+ var DEFAULT_COLUMNS = 3;
2941
+ var DEFAULT_SETTLE_MS = 550;
2942
+ var DEFAULT_TIMEOUT_MS2 = 5e4;
2943
+ var DEFAULT_POLL_INTERVAL_MS2 = 800;
2944
+ var CAPTURE_SCROLL_STEP_RATIO = 0.56;
2945
+ var CAPTURE_SCROLL_DURATION_MS = 360;
2946
+ var SPRITE_GAP = 16;
2947
+ var FRAME_SIGNATURE_WIDTH = 320;
2948
+ var FRAME_SIGNATURE_QUALITY = 35;
2949
+ var Share = {
2950
+ captureScreen,
2951
+ captureLink
2952
+ };
2953
+ async function captureScreen(ctx, options = {}) {
2954
+ const startedAt = Date.now();
2955
+ const logTemplate = Logger.useTemplate();
2956
+ const frameBuffers = [];
2957
+ const seen = /* @__PURE__ */ new Set();
2958
+ const targetSelector = options.selector || { id: "message_list" };
2959
+ const stopSelector = options.stopSelector || null;
2960
+ const resetToTop = options.resetToTop !== false;
2961
+ const maxFrames = clampInt(options.maxFrames, 1, DEFAULT_CAPTURE_COUNT, DEFAULT_CAPTURE_COUNT);
2962
+ const direction = String(options.direction || "up").toLowerCase() === "down" ? "down" : "up";
2963
+ let topResult = null;
2964
+ let stoppedBySelector = false;
2965
+ logTemplate.screenshotStart({ selector: targetSelector, resetToTop, direction, stopSelector });
2966
+ if (resetToTop) {
2967
+ topResult = await DeviceInput.scrollToTop(ctx, targetSelector, { settleMs: 400 });
2968
+ Logger.info("captureScreen scrollToTop", {
2969
+ swipes: topResult?.swipes,
2970
+ stableRounds: topResult?.stableRounds,
2971
+ hashSource: topResult?.hashSource,
2972
+ capped: topResult?.capped === true,
2973
+ inferredBoundary: topResult?.inferredBoundary === true
2974
+ });
2975
+ if (topResult?.capped === true && options.requireTop === true) {
2976
+ throw new CrawlerError({
2977
+ message: `source_extraction_failed: \u622A\u56FE\u524D\u672A\u80FD\u786E\u8BA4\u56DE\u5230\u9876\u90E8 swipes=${topResult.swipes} stableRounds=${topResult.stableRounds}`,
2978
+ code: Code.SourceExtractionFailed,
2979
+ context: {
2980
+ selector: targetSelector,
2981
+ swipes: topResult.swipes,
2982
+ stableRounds: topResult.stableRounds,
2983
+ hashSource: topResult.hashSource
2984
+ }
2985
+ });
2986
+ } else if (topResult?.capped === true) {
2987
+ Logger.warn("captureScreen \u672A\u80FD\u786E\u8BA4\u56DE\u5230\u9876\u90E8\uFF0C\u7EE7\u7EED\u91C7\u96C6\u5F53\u524D\u9875\u9762", {
2988
+ swipes: topResult.swipes,
2989
+ stableRounds: topResult.stableRounds,
2990
+ hashSource: topResult.hashSource
2991
+ });
2992
+ }
2993
+ }
2994
+ for (let index = 0; index < maxFrames; index += 1) {
2995
+ await sleep(DEFAULT_SETTLE_MS);
2996
+ const target = await DeviceView.find(ctx, targetSelector, { optional: true }).catch(() => null);
2997
+ const png = await Device.screenshotPng(ctx);
2998
+ const hash = await frameSignature(png, target?.bounds);
2999
+ if (seen.has(hash)) break;
3000
+ seen.add(hash);
3001
+ frameBuffers.push(png);
3002
+ if (stopSelector && await DeviceView.exists(ctx, stopSelector, { timeoutMs: 300 }).catch(() => false)) {
3003
+ stoppedBySelector = true;
3004
+ break;
3005
+ }
3006
+ if (index >= maxFrames - 1) break;
3007
+ await DeviceInput.scrollStep(ctx, targetSelector, direction, {
3008
+ swipeRatio: CAPTURE_SCROLL_STEP_RATIO,
3009
+ durationMs: CAPTURE_SCROLL_DURATION_MS,
3010
+ settleMs: DEFAULT_SETTLE_MS
3011
+ }).catch(() => {
3012
+ });
3013
+ }
3014
+ const spriteBuffers = options.reverseFrames ? [...frameBuffers].reverse() : frameBuffers;
3015
+ const sprite = await composeSprite(spriteBuffers, { columns: DEFAULT_COLUMNS, gap: SPRITE_GAP });
3016
+ const compression = resolveImageCompression(options);
3017
+ const imageResult = await compressImageBufferToResult(sprite, compression);
3018
+ logTemplate.screenshotDone({
3019
+ duration: Logger.duration(startedAt),
3020
+ frameCount: frameBuffers.length,
3021
+ scrollStepRatio: CAPTURE_SCROLL_STEP_RATIO,
3022
+ resetToTop,
3023
+ topCapped: topResult?.capped === true,
3024
+ topInferredBoundary: topResult?.inferredBoundary === true,
3025
+ format: imageResult.format,
3026
+ base64Bytes: Math.ceil(imageResult.base64.length),
3027
+ uniqueFrameCount: seen.size,
3028
+ stoppedBySelector
3029
+ });
3030
+ return `data:${imageResult.mimeType};base64,${imageResult.base64}`;
3031
+ }
3032
+ async function captureLink(ctx, options = {}) {
3033
+ const logTemplate = Logger.useTemplate();
3034
+ const actorInfo = resolveActorInfo(options.actorInfo || options.actorKey || ctx.actorKey);
3035
+ const share = actorInfo?.share || {};
3036
+ if (share.mode !== "clipboard") {
3037
+ throw new CrawlerError({
3038
+ message: `source_extraction_failed: unsupported Android share mode ${share.mode || ""}`,
3039
+ code: Code.SourceExtractionFailed
3040
+ });
3041
+ }
3042
+ const timeoutMs = Number(options.timeoutMs || DEFAULT_TIMEOUT_MS2);
3043
+ const prefix = String(share.prefix || "").trim();
3044
+ logTemplate.shareStart({ actor: actorInfo.key, prefix, timeoutMs });
3045
+ const beforeEvent = await readClipboard(ctx).catch(() => null);
3046
+ const beforeLink = beforeEvent ? selectLink(beforeEvent, prefix) : "";
3047
+ if (beforeLink) Logger.info("Share.captureLink baseline", { beforeLink });
3048
+ if (typeof options.performActions === "function") {
3049
+ await options.performActions();
3050
+ }
3051
+ const deadline = Date.now() + timeoutMs;
3052
+ let lastEvent = null;
3053
+ while (Date.now() < deadline) {
3054
+ const event = await readClipboard(ctx).catch((error) => ({ ok: false, error: error?.message || String(error) }));
3055
+ lastEvent = event;
3056
+ const link = selectLink(event, prefix);
3057
+ if (link) {
3058
+ if (link === beforeLink) {
3059
+ Logger.info("Share.captureLink ignore baseline link", { link });
3060
+ await sleep(DEFAULT_POLL_INTERVAL_MS2);
3061
+ continue;
3062
+ }
3063
+ logTemplate.shareSuccess({ link, source: event.source || "clipboard" });
3064
+ return { link, source: event.source || "clipboard", payloadSnapshot: event.payloadSnapshot || "" };
3065
+ }
3066
+ await sleep(DEFAULT_POLL_INTERVAL_MS2);
3067
+ }
3068
+ throw new CrawlerError({
3069
+ message: "source_extraction_failed: \u672A\u6355\u83B7\u5206\u4EAB\u94FE\u63A5",
3070
+ code: Code.SourceExtractionFailed,
3071
+ context: { prefix, lastEvent }
3072
+ });
3073
+ }
3074
+ async function readClipboard(ctx) {
3075
+ const payload = await readClipboardForShare(ctx);
3076
+ const candidates = extractLinks(payload).map((link) => ({
3077
+ source: "adb.clipboard",
3078
+ link,
3079
+ payload: payload.slice(0, 1e3)
3080
+ }));
3081
+ return {
3082
+ ok: candidates.length > 0,
3083
+ link: candidates[0]?.link || "",
3084
+ source: "adb.clipboard",
3085
+ candidates,
3086
+ payloadSnapshot: payload.slice(0, 500)
3087
+ };
3088
+ }
3089
+ async function composeSprite(buffers, options = {}) {
3090
+ if (!buffers.length) {
3091
+ throw new CrawlerError({ message: "source_extraction_failed: no screenshots captured", code: Code.SourceExtractionFailed });
3092
+ }
3093
+ const images = [];
3094
+ for (const buffer of buffers) images.push(await Jimp2.read(buffer));
3095
+ const columns = Math.max(1, Number(options.columns || DEFAULT_COLUMNS));
3096
+ const gap = Math.max(0, Number(options.gap || 0));
3097
+ const cellWidth = Math.max(...images.map((image) => image.bitmap.width));
3098
+ const cellHeight = Math.max(...images.map((image) => image.bitmap.height));
3099
+ const rows = Math.ceil(images.length / columns);
3100
+ const width = columns * cellWidth + (columns - 1) * gap;
3101
+ const height = rows * cellHeight + (rows - 1) * gap;
3102
+ const sprite = new Jimp2({ width, height, color: 4294967295 });
3103
+ images.forEach((image, index) => {
3104
+ const x = index % columns * (cellWidth + gap);
3105
+ const y = Math.floor(index / columns) * (cellHeight + gap);
3106
+ sprite.composite(image, x, y);
3107
+ });
3108
+ return sprite.getBuffer(JimpMime2.png);
3109
+ }
3110
+ async function frameSignature(buffer, bounds2) {
3111
+ const image = await Jimp2.read(buffer);
3112
+ const area = normalizeCaptureBounds(bounds2, image.bitmap.width, image.bitmap.height);
3113
+ const target = image.crop(area);
3114
+ const scale = Math.min(1, FRAME_SIGNATURE_WIDTH / Math.max(1, target.bitmap.width));
3115
+ if (scale < 1) {
3116
+ target.resize({
3117
+ w: Math.max(1, Math.round(target.bitmap.width * scale)),
3118
+ h: Math.max(1, Math.round(target.bitmap.height * scale))
3119
+ });
3120
+ }
3121
+ const normalized = await target.getBuffer(JimpMime2.jpeg, { quality: FRAME_SIGNATURE_QUALITY });
3122
+ return createHash4("sha256").update(normalized).digest("hex");
3123
+ }
3124
+ function normalizeCaptureBounds(bounds2, imageWidth, imageHeight) {
3125
+ const left = clampInt(bounds2?.left, 0, imageWidth - 1, 0);
3126
+ const top = clampInt(bounds2?.top, 0, imageHeight - 1, 0);
3127
+ const right = clampInt(bounds2?.right, left + 1, imageWidth, imageWidth);
3128
+ const bottom = clampInt(bounds2?.bottom, top + 1, imageHeight, imageHeight);
3129
+ return {
3130
+ x: left,
3131
+ y: top,
3132
+ w: Math.max(1, right - left),
3133
+ h: Math.max(1, bottom - top)
3134
+ };
3135
+ }
3136
+ function clampInt(value, min, max, fallback) {
3137
+ const number = Math.round(Number(value));
3138
+ if (!Number.isFinite(number)) return fallback;
3139
+ return Math.min(max, Math.max(min, number));
3140
+ }
3141
+ function resolveActorInfo(value) {
3142
+ if (value && typeof value === "object") return value;
3143
+ const key = String(value || "").trim();
3144
+ const info = ActorInfo[key];
3145
+ if (!info) {
3146
+ throw new CrawlerError({
3147
+ message: `invalid_request: unknown actorInfo ${key}`,
3148
+ code: Code.InvalidRequest
3149
+ });
3150
+ }
3151
+ return info;
3152
+ }
3153
+ function selectLink(event, prefix) {
3154
+ const candidates = [];
3155
+ if (event?.link) candidates.push(event.link);
3156
+ if (Array.isArray(event?.candidates)) {
3157
+ for (const candidate of event.candidates) {
3158
+ if (candidate?.link) candidates.push(candidate.link);
3159
+ }
3160
+ }
3161
+ return candidates.find((link) => !prefix || String(link).startsWith(prefix)) || "";
3162
+ }
3163
+ function extractLinks(value) {
3164
+ const text2 = String(value || "");
3165
+ const matches = text2.match(/https?:\/\/[^\s"'<>,。]+/g) || [];
3166
+ return matches.map((link) => link.replace(/[)\].,,。;;!?!?]+$/g, ""));
3167
+ }
3168
+
3169
+ // src/launch.js
3170
+ var Launch = {
3171
+ run
3172
+ };
3173
+ async function run(handler, options = {}) {
3174
+ const startedAt = Date.now();
3175
+ const logTemplate = Logger.useTemplate();
3176
+ const inputPath = StoragePath.Input;
3177
+ const outputPath = StoragePath.Output;
3178
+ const input = options.input || JSON.parse(fs2.readFileSync(inputPath, "utf8"));
3179
+ const ctx = options.ctx || Context.createAndroidContext(input, options.contextDefaults || {});
3180
+ ctx.runId = ctx.runId || input.run_id || input.runId || input.runtime?.run_id || input.runtime?.runId || "";
3181
+ ctx.actorKey = ctx.actorKey || options.actorKey || input.actorKey || input.actor_name || input.actorName || "";
3182
+ const actorInfo = options.actorInfo || ActorInfo[ctx.actorKey] || null;
3183
+ const originalInputMethod = await Device.currentInputMethod(ctx).catch((error) => {
3184
+ logTemplate.inputMethodSnapshotFail(error);
3185
+ return "";
3186
+ });
3187
+ const apifyKit = await ApifyKit.useApifyKit({
3188
+ reset: true,
3189
+ input,
3190
+ ctx,
3191
+ inputPath,
3192
+ outputPath
3193
+ });
3194
+ const kit = {
3195
+ Launch,
3196
+ ApifyKit: apifyKit,
3197
+ Device,
3198
+ DeviceInput,
3199
+ DeviceView,
3200
+ DeviceSQLite,
3201
+ Share,
3202
+ Mutation,
3203
+ NetworkCapture,
3204
+ WebView,
3205
+ Logger,
3206
+ Context
3207
+ };
3208
+ try {
3209
+ logTemplate.launchStart({
3210
+ inputPath,
3211
+ outputPath,
3212
+ runId: ctx.runId,
3213
+ serial: ctx.serial,
3214
+ packageName: ctx.packageName,
3215
+ queryChars: ctx.query.length
3216
+ });
3217
+ if (await WebView.configureHook(ctx, actorInfo)) {
3218
+ await Device.forceStopApp(ctx);
3219
+ }
3220
+ if (typeof options.hooks?.preNavigation === "function") {
3221
+ await options.hooks.preNavigation({ input, ctx, kit, ApifyKit: apifyKit });
3222
+ }
3223
+ await handler({ input, ctx, kit, ApifyKit: apifyKit });
3224
+ if (typeof options.hooks?.postRun === "function") {
3225
+ await options.hooks.postRun({ input, ctx, kit, ApifyKit: apifyKit });
3226
+ }
3227
+ logTemplate.launchSuccess({ duration: Logger.duration(startedAt) });
3228
+ } catch (error) {
3229
+ logTemplate.launchFail({ duration: Logger.duration(startedAt) }, error);
3230
+ if (typeof options.hooks?.onError === "function") {
3231
+ await options.hooks.onError(error, { input, ctx, kit, ApifyKit: apifyKit });
3232
+ }
3233
+ if (!apifyKit.hasPushed()) await apifyKit.pushFailed(error);
3234
+ throw error;
3235
+ } finally {
3236
+ await Device.restoreInputMethod(ctx, originalInputMethod).catch((error) => {
3237
+ logTemplate.inputMethodRestoreFail(error);
3238
+ });
3239
+ }
3240
+ }
3241
+
3242
+ // entrys/node.js
3243
+ var ToolkitMode = Object.freeze({
3244
+ default: "default"
3245
+ });
3246
+ var useAndroidToolKit = () => {
3247
+ return {
3248
+ Launch,
3249
+ ApifyKit,
3250
+ DeviceInput,
3251
+ DeviceView,
3252
+ DeviceSQLite,
3253
+ Device,
3254
+ Mutation,
3255
+ NetworkCapture,
3256
+ Share,
3257
+ WebView,
3258
+ Constants: constants_exports,
3259
+ Errors: errors_exports,
3260
+ Logger,
3261
+ LOG_TEMPLATES,
3262
+ $Internals: { LOG_TEMPLATES, stripAnsi },
3263
+ Context
3264
+ };
3265
+ };
3266
+ useAndroidToolKit.Mode = ToolkitMode;
3267
+ export {
3268
+ useAndroidToolKit
3269
+ };
3270
+ //# sourceMappingURL=index.js.map