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