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