@skrillex1224/android-toolkit 0.1.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +28 -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 -1068
- 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
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import {Jimp, JimpMime, ResizeStrategy} from 'jimp';
|
|
2
|
-
|
|
3
|
-
import {Logger} from '../logger.js';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_SCREENSHOT_MAX_BYTES = 5 * 1024 * 1024;
|
|
6
|
-
const DEFAULT_SCREENSHOT_OUTPUT_TYPE = 'jpeg';
|
|
7
|
-
const DEFAULT_SCREENSHOT_QUALITY = 0.72;
|
|
8
|
-
const DEFAULT_SCREENSHOT_MIN_QUALITY = 0.38;
|
|
9
|
-
const DEFAULT_SCREENSHOT_MIN_SCALE = 0.25;
|
|
10
|
-
const SUPPORTED_SCREENSHOT_OUTPUT_TYPES = new Set(['jpeg']);
|
|
11
|
-
|
|
12
|
-
const toPositiveInteger = (value, fallback = 0) => {
|
|
13
|
-
const number = Math.floor(Number(value) || 0);
|
|
14
|
-
return number > 0 ? number : fallback;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const normalizeQuality = (value, fallback) => {
|
|
18
|
-
const number = Number(value);
|
|
19
|
-
if (!Number.isFinite(number) || number <= 0) return fallback;
|
|
20
|
-
const normalized = number > 1 ? number / 100 : number;
|
|
21
|
-
return Math.min(1, Math.max(0.01, normalized));
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const normalizeScale = (value, fallback) => {
|
|
25
|
-
const number = Number(value);
|
|
26
|
-
if (!Number.isFinite(number) || number <= 0) return fallback;
|
|
27
|
-
return Math.min(1, Math.max(0.05, number));
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const normalizeScreenshotOutputType = (value) => {
|
|
31
|
-
const raw = String(value || DEFAULT_SCREENSHOT_OUTPUT_TYPE).trim().toLowerCase();
|
|
32
|
-
const normalized = raw === 'jpg' ? 'jpeg' : raw;
|
|
33
|
-
return SUPPORTED_SCREENSHOT_OUTPUT_TYPES.has(normalized)
|
|
34
|
-
? normalized
|
|
35
|
-
: DEFAULT_SCREENSHOT_OUTPUT_TYPE;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const getBase64BytesFromBuffer = (buffer) => Math.ceil(buffer.length / 3) * 4;
|
|
39
|
-
|
|
40
|
-
const toJpegQuality = (value) => Math.round(normalizeQuality(value, DEFAULT_SCREENSHOT_QUALITY) * 100);
|
|
41
|
-
|
|
42
|
-
export const resolveImageCompression = (options = {}) => {
|
|
43
|
-
const explicit = options.compression;
|
|
44
|
-
const source = explicit && typeof explicit === 'object' && !Array.isArray(explicit)
|
|
45
|
-
? explicit
|
|
46
|
-
: {};
|
|
47
|
-
|
|
48
|
-
const enabled = explicit !== false
|
|
49
|
-
&& source.enabled !== false
|
|
50
|
-
&& options.compress !== false;
|
|
51
|
-
|
|
52
|
-
const quality = normalizeQuality(
|
|
53
|
-
source.quality ?? options.quality,
|
|
54
|
-
DEFAULT_SCREENSHOT_QUALITY
|
|
55
|
-
);
|
|
56
|
-
const minQuality = Math.min(
|
|
57
|
-
quality,
|
|
58
|
-
normalizeQuality(source.minQuality ?? options.minQuality, DEFAULT_SCREENSHOT_MIN_QUALITY)
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
enabled,
|
|
63
|
-
maxBytes: toPositiveInteger(
|
|
64
|
-
source.maxBytes
|
|
65
|
-
?? source.maxBase64Bytes
|
|
66
|
-
?? options.maxBytes
|
|
67
|
-
?? options.maxBase64Bytes,
|
|
68
|
-
DEFAULT_SCREENSHOT_MAX_BYTES
|
|
69
|
-
),
|
|
70
|
-
outputType: normalizeScreenshotOutputType(
|
|
71
|
-
source.type
|
|
72
|
-
?? source.outputType
|
|
73
|
-
?? options.type
|
|
74
|
-
?? options.outputType
|
|
75
|
-
),
|
|
76
|
-
quality,
|
|
77
|
-
minQuality,
|
|
78
|
-
minScale: normalizeScale(
|
|
79
|
-
source.minScale ?? options.minScale,
|
|
80
|
-
DEFAULT_SCREENSHOT_MIN_SCALE
|
|
81
|
-
),
|
|
82
|
-
};
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const encodeJpeg = async (sourceImage, compression, scale, quality) => {
|
|
86
|
-
const width = Math.max(1, Math.round(sourceImage.bitmap.width * scale));
|
|
87
|
-
const height = Math.max(1, Math.round(sourceImage.bitmap.height * scale));
|
|
88
|
-
const image = sourceImage.clone();
|
|
89
|
-
|
|
90
|
-
if (scale < 0.999) {
|
|
91
|
-
image.resize({
|
|
92
|
-
w: width,
|
|
93
|
-
h: height,
|
|
94
|
-
mode: ResizeStrategy.BILINEAR,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const buffer = await image.getBuffer(JimpMime.jpeg, {quality});
|
|
99
|
-
return {
|
|
100
|
-
buffer,
|
|
101
|
-
bytes: getBase64BytesFromBuffer(buffer),
|
|
102
|
-
width,
|
|
103
|
-
height,
|
|
104
|
-
quality,
|
|
105
|
-
scale: Number(scale.toFixed(3)),
|
|
106
|
-
format: compression.outputType,
|
|
107
|
-
};
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const compressImageBuffer = async (buffer, compression) => {
|
|
111
|
-
const sourceImage = await Jimp.read(buffer);
|
|
112
|
-
const maxQuality = toJpegQuality(compression.quality);
|
|
113
|
-
const minQuality = Math.min(maxQuality, toJpegQuality(compression.minQuality));
|
|
114
|
-
let quality = maxQuality;
|
|
115
|
-
let scale = 1;
|
|
116
|
-
let smallest = null;
|
|
117
|
-
|
|
118
|
-
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
119
|
-
const candidate = await encodeJpeg(sourceImage, compression, scale, quality);
|
|
120
|
-
if (!smallest || candidate.bytes < smallest.bytes) {
|
|
121
|
-
smallest = candidate;
|
|
122
|
-
}
|
|
123
|
-
if (candidate.bytes <= compression.maxBytes) {
|
|
124
|
-
return {...candidate, withinLimit: true};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (quality > minQuality) {
|
|
128
|
-
quality = Math.max(minQuality, Math.floor(quality * 0.75));
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const ratio = Math.sqrt(compression.maxBytes / Math.max(1, candidate.bytes));
|
|
133
|
-
const nextScale = Math.max(
|
|
134
|
-
compression.minScale,
|
|
135
|
-
Math.min(scale * 0.85, scale * ratio * 0.94)
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
if (nextScale >= scale * 0.99 || scale <= compression.minScale) {
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
scale = nextScale;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const finalCandidate = await encodeJpeg(sourceImage, compression, compression.minScale, minQuality);
|
|
146
|
-
const fallback = !smallest || finalCandidate.bytes < smallest.bytes
|
|
147
|
-
? finalCandidate
|
|
148
|
-
: smallest;
|
|
149
|
-
return {...fallback, withinLimit: fallback.bytes <= compression.maxBytes};
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
export const compressImageBufferToBase64 = async (buffer, compression) => {
|
|
153
|
-
const originalBytes = getBase64BytesFromBuffer(buffer);
|
|
154
|
-
if (!compression.enabled || originalBytes <= compression.maxBytes) {
|
|
155
|
-
return buffer.toString('base64');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const result = await compressImageBuffer(buffer, compression).catch((error) => {
|
|
159
|
-
Logger.warn('captureScreen 压缩失败,返回原图', {message: error?.message || String(error)});
|
|
160
|
-
return null;
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
if (!result?.buffer) {
|
|
164
|
-
return buffer.toString('base64');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (result.withinLimit) {
|
|
168
|
-
Logger.info('captureScreen 已压缩', {
|
|
169
|
-
originalBytes,
|
|
170
|
-
outputBytes: result.bytes,
|
|
171
|
-
format: result.format,
|
|
172
|
-
quality: result.quality,
|
|
173
|
-
scale: result.scale,
|
|
174
|
-
size: `${result.width}x${result.height}`,
|
|
175
|
-
});
|
|
176
|
-
} else {
|
|
177
|
-
Logger.warn('captureScreen 压缩后仍超过目标', {
|
|
178
|
-
originalBytes,
|
|
179
|
-
outputBytes: result.bytes,
|
|
180
|
-
maxBytes: compression.maxBytes,
|
|
181
|
-
format: result.format,
|
|
182
|
-
quality: result.quality,
|
|
183
|
-
scale: result.scale,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return result.buffer.toString('base64');
|
|
188
|
-
};
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
const CONFIG = globalThis.__ANDROID_TOOLKIT_WEBVIEW_EVENT_CONFIG__ || {};
|
|
2
|
-
|
|
3
|
-
function getConfig() {
|
|
4
|
-
const webViewClasses = Array.isArray(CONFIG.webViewClasses)
|
|
5
|
-
? CONFIG.webViewClasses.map((item) => String(item || '').trim()).filter(Boolean)
|
|
6
|
-
: [];
|
|
7
|
-
if (webViewClasses.length === 0) {
|
|
8
|
-
throw new Error('android-toolkit webview_event_agent requires config.webViewClasses');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
webViewClasses,
|
|
13
|
-
eventNames: normalizeSet(CONFIG.eventNames),
|
|
14
|
-
keywords: Array.isArray(CONFIG.keywords)
|
|
15
|
-
? CONFIG.keywords.map((item) => String(item || '')).filter(Boolean)
|
|
16
|
-
: [],
|
|
17
|
-
// 调试开关:捕获所有能解析到的 WebView JS 回调,用于先确认业务事件名。
|
|
18
|
-
// 默认关闭,避免正式任务记录无关事件。
|
|
19
|
-
captureAllEvents: CONFIG.captureAllEvents === true,
|
|
20
|
-
includeRawJs: CONFIG.includeRawJs === true,
|
|
21
|
-
maxRawJsLength: Number(CONFIG.maxRawJsLength || 1200),
|
|
22
|
-
maxPayloadLength: Number(CONFIG.maxPayloadLength || 4 * 1024 * 1024)
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function normalizeSet(values) {
|
|
27
|
-
return new Set(Array.isArray(values)
|
|
28
|
-
? values.map((item) => String(item || '').trim()).filter(Boolean)
|
|
29
|
-
: []);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function emit(type, payload) {
|
|
33
|
-
const safe = payload || {};
|
|
34
|
-
safe.type = type;
|
|
35
|
-
safe.pid = Process.id;
|
|
36
|
-
safe.process = Process.name;
|
|
37
|
-
console.log(`ANDROID_WEBVIEW_EVENT_JSON ${JSON.stringify(safe)}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function clip(value, limit) {
|
|
41
|
-
const raw = String(value || '');
|
|
42
|
-
if (limit <= 0 || raw.length <= limit) return raw;
|
|
43
|
-
return `${raw.slice(0, limit)}...<${raw.length}>`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseJson(value) {
|
|
47
|
-
const raw = String(value || '').trim();
|
|
48
|
-
if (!raw) return null;
|
|
49
|
-
try {
|
|
50
|
-
return JSON.parse(raw);
|
|
51
|
-
} catch (_) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function extractCallPayload(rawSource) {
|
|
57
|
-
const source = String(rawSource || '').trim().replace(/^javascript:\s*/i, '').trim();
|
|
58
|
-
const patterns = [
|
|
59
|
-
{
|
|
60
|
-
kind: 'weixin_bridge',
|
|
61
|
-
re: /^(?:typeof\s+WeixinJSBridge\s*!==\s*['"]undefined['"]\s*&&\s*)?WeixinJSBridge\._handleMessageFromWeixin\(([\s\S]*)\)\s*;?\s*$/
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
kind: 'window_callback',
|
|
65
|
-
re: /^window\[['"]([^'"]+)['"]\]\s*&&\s*\1\(([\s\S]*)\)\s*;?\s*$/
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
kind: 'function_callback',
|
|
69
|
-
re: /^([A-Za-z_$][\w$]*)\(([\s\S]*)\)\s*;?\s*$/
|
|
70
|
-
}
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
for (const pattern of patterns) {
|
|
74
|
-
const match = source.match(pattern.re);
|
|
75
|
-
if (!match) continue;
|
|
76
|
-
if (pattern.kind === 'weixin_bridge') {
|
|
77
|
-
const payload = parseJson(match[1]);
|
|
78
|
-
const eventName = String(payload?.__event_id || '').trim();
|
|
79
|
-
return {kind: pattern.kind, eventName, payload};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const eventName = String(match[1] || '').trim();
|
|
83
|
-
const payload = parseJson(match[2]);
|
|
84
|
-
return {kind: pattern.kind, eventName, payload};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {kind: 'raw', eventName: '', payload: null};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function shouldCapture(config, rawJs, parsed) {
|
|
91
|
-
const eventName = String(parsed?.eventName || '').trim();
|
|
92
|
-
if (config.captureAllEvents && (eventName || parsed?.payload || config.includeRawJs)) return true;
|
|
93
|
-
if (eventName && config.eventNames.has(eventName)) return true;
|
|
94
|
-
if (config.keywords.length === 0) return false;
|
|
95
|
-
const raw = String(rawJs || '');
|
|
96
|
-
const payloadText = parsed?.payload ? JSON.stringify(parsed.payload) : '';
|
|
97
|
-
return config.keywords.some((keyword) => raw.includes(keyword) || payloadText.includes(keyword));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function safePayload(config, payload) {
|
|
101
|
-
if (payload == null) return null;
|
|
102
|
-
const raw = JSON.stringify(payload);
|
|
103
|
-
if (raw.length <= config.maxPayloadLength) return payload;
|
|
104
|
-
return {
|
|
105
|
-
__androidToolkitTruncated: true,
|
|
106
|
-
raw: clip(raw, config.maxPayloadLength)
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function callString(instance, methodName) {
|
|
111
|
-
try {
|
|
112
|
-
if (typeof instance[methodName] !== 'function') return '';
|
|
113
|
-
return String(instance[methodName]() || '');
|
|
114
|
-
} catch (_) {
|
|
115
|
-
return '';
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function findLoaders(className) {
|
|
120
|
-
const loaders = [];
|
|
121
|
-
Java.enumerateClassLoaders({
|
|
122
|
-
onMatch(loader) {
|
|
123
|
-
try {
|
|
124
|
-
loader.findClass(className);
|
|
125
|
-
loaders.push(loader);
|
|
126
|
-
} catch (_) { }
|
|
127
|
-
},
|
|
128
|
-
onComplete() { }
|
|
129
|
-
});
|
|
130
|
-
return loaders;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function hookWithFactory(loader, className, hooker) {
|
|
134
|
-
const oldLoader = Java.classFactory.loader;
|
|
135
|
-
try {
|
|
136
|
-
Java.classFactory.loader = loader;
|
|
137
|
-
const factory = Java.ClassFactory.get(loader);
|
|
138
|
-
const klass = factory.use(className);
|
|
139
|
-
hooker(klass);
|
|
140
|
-
emit('hook_installed', {className, loader: String(loader)});
|
|
141
|
-
} catch (error) {
|
|
142
|
-
emit('hook_error', {className, loader: String(loader), error: String(error)});
|
|
143
|
-
} finally {
|
|
144
|
-
Java.classFactory.loader = oldLoader;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const INSTALLED_HOOKS = new Set();
|
|
149
|
-
|
|
150
|
-
function installAllHooks(config) {
|
|
151
|
-
let installed = 0;
|
|
152
|
-
for (const className of config.webViewClasses) {
|
|
153
|
-
const loaders = findLoaders(className);
|
|
154
|
-
if (loaders.length > 0) {
|
|
155
|
-
emit('loaders', {
|
|
156
|
-
className,
|
|
157
|
-
loaderCount: loaders.length,
|
|
158
|
-
loaders: loaders.map((loader) => String(loader)).slice(0, 5)
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
for (const loader of loaders) {
|
|
162
|
-
const key = `${className}::${String(loader)}`;
|
|
163
|
-
if (INSTALLED_HOOKS.has(key)) continue;
|
|
164
|
-
INSTALLED_HOOKS.add(key);
|
|
165
|
-
hookWithFactory(loader, className, (klass) => {
|
|
166
|
-
// 通用层只 hook WebView 的“执行 JS 字符串”入口;业务事件名和 payload 解析规则由 config 决定。
|
|
167
|
-
hookStringMethod(config, klass, className, 'evaluateJavascript');
|
|
168
|
-
hookStringMethod(config, klass, className, 'evaluateJavaScript');
|
|
169
|
-
hookStringMethod(config, klass, className, 'loadUrl');
|
|
170
|
-
});
|
|
171
|
-
installed += 1;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return installed;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function hookStringMethod(config, klass, className, methodName) {
|
|
178
|
-
const method = klass[methodName];
|
|
179
|
-
if (!method || !method.overloads) return;
|
|
180
|
-
|
|
181
|
-
for (const overload of method.overloads) {
|
|
182
|
-
const signature = overload.argumentTypes.map((item) => item.className).join(',');
|
|
183
|
-
if (!signature.includes('java.lang.String')) continue;
|
|
184
|
-
|
|
185
|
-
overload.implementation = function (...args) {
|
|
186
|
-
const js = String(args[0] || '');
|
|
187
|
-
const parsed = extractCallPayload(js);
|
|
188
|
-
if (shouldCapture(config, js, parsed)) {
|
|
189
|
-
emit('webview_event', {
|
|
190
|
-
className,
|
|
191
|
-
methodName,
|
|
192
|
-
signature,
|
|
193
|
-
url: callString(this, 'getUrl'),
|
|
194
|
-
title: callString(this, 'getTitle'),
|
|
195
|
-
eventName: parsed.eventName,
|
|
196
|
-
eventKind: parsed.kind,
|
|
197
|
-
payload: safePayload(config, parsed.payload),
|
|
198
|
-
rawJs: config.includeRawJs ? clip(js, config.maxRawJsLength) : ''
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
return overload.apply(this, args);
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (typeof Java !== 'undefined' && Java.available) {
|
|
207
|
-
Java.perform(() => {
|
|
208
|
-
const config = getConfig();
|
|
209
|
-
emit('agent_ready', {
|
|
210
|
-
webViewClassCount: config.webViewClasses.length,
|
|
211
|
-
eventNames: Array.from(config.eventNames),
|
|
212
|
-
keywordCount: config.keywords.length
|
|
213
|
-
});
|
|
214
|
-
installAllHooks(config);
|
|
215
|
-
|
|
216
|
-
const pollIntervalMs = Number(CONFIG.loaderPollIntervalMs ?? 1000);
|
|
217
|
-
if (pollIntervalMs > 0) {
|
|
218
|
-
setInterval(() => {
|
|
219
|
-
try {
|
|
220
|
-
installAllHooks(config);
|
|
221
|
-
} catch (error) {
|
|
222
|
-
emit('poll_error', {error: String(error)});
|
|
223
|
-
}
|
|
224
|
-
}, pollIntervalMs);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
package/src/launch.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
|
|
3
|
-
import {ApifyKit} from './apify-kit.js';
|
|
4
|
-
import {createAndroidContext} from './context.js';
|
|
5
|
-
import {Device} from './device.js';
|
|
6
|
-
import {Frida} from './frida-client.js';
|
|
7
|
-
import {Logger} from './logger.js';
|
|
8
|
-
import {Share} from './share.js';
|
|
9
|
-
|
|
10
|
-
const DEFAULT_INPUT_PATH = '/apify_storage/input.json';
|
|
11
|
-
const DEFAULT_OUTPUT_PATH = '/apify_storage/output.json';
|
|
12
|
-
|
|
13
|
-
export const Launch = {
|
|
14
|
-
/**
|
|
15
|
-
* Android androider 的统一入口。
|
|
16
|
-
*
|
|
17
|
-
* 这层等价于普通 visitor 里的 Actor.init + Actor.getInput + requestHandler 的组合。
|
|
18
|
-
* 容器里默认使用 cluster 固定的 input/output 路径;本地调试仍可由
|
|
19
|
-
* main.js 透传 argv 覆盖。
|
|
20
|
-
*
|
|
21
|
-
* handler 内建议只写业务流程;输入、ctx、toolkit 模块都从参数里取。
|
|
22
|
-
*/
|
|
23
|
-
async run(handler, options = {}) {
|
|
24
|
-
const startedAt = Date.now();
|
|
25
|
-
const inputPath = pathOption(options.inputPath, DEFAULT_INPUT_PATH);
|
|
26
|
-
const outputPath = pathOption(options.outputPath, DEFAULT_OUTPUT_PATH);
|
|
27
|
-
const input = options.input || JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
|
28
|
-
const ctx = options.ctx || createAndroidContext(input, options.contextDefaults || {});
|
|
29
|
-
ctx.runId = ctx.runId || input.run_id || input.runId || input.runtime?.run_id || input.runtime?.runId || '';
|
|
30
|
-
const apifyKit = await ApifyKit.useApifyKit({
|
|
31
|
-
reset: true,
|
|
32
|
-
input,
|
|
33
|
-
ctx,
|
|
34
|
-
inputPath,
|
|
35
|
-
outputPath
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const kit = {
|
|
39
|
-
ApifyKit: apifyKit,
|
|
40
|
-
Device,
|
|
41
|
-
Frida,
|
|
42
|
-
Share,
|
|
43
|
-
Logger
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
Logger.start('Launch run', {
|
|
48
|
-
inputPath,
|
|
49
|
-
outputPath,
|
|
50
|
-
runId: ctx.runId,
|
|
51
|
-
serial: ctx.serial,
|
|
52
|
-
packageName: ctx.packageName,
|
|
53
|
-
queryChars: ctx.query.length
|
|
54
|
-
});
|
|
55
|
-
await handler({input, ctx, kit, ApifyKit: apifyKit});
|
|
56
|
-
Logger.success('Launch run', {duration: Logger.duration(startedAt)});
|
|
57
|
-
} catch (error) {
|
|
58
|
-
Logger.fail(`Launch handler failed duration=${Logger.duration(startedAt)}`, error);
|
|
59
|
-
if (!apifyKit.hasPushed()) {
|
|
60
|
-
await apifyKit.pushFailed(error);
|
|
61
|
-
}
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
function pathOption(value, fallback) {
|
|
68
|
-
const clean = String(value || '').trim();
|
|
69
|
-
return clean || fallback;
|
|
70
|
-
}
|
package/src/logger.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
const defaultLogger = {
|
|
2
|
-
info: (...args) => console.error(...args),
|
|
3
|
-
warning: (...args) => console.error(...args),
|
|
4
|
-
warn: (...args) => console.error(...args),
|
|
5
|
-
error: (...args) => console.error(...args),
|
|
6
|
-
debug: (...args) => console.error(...args)
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
let activeLogger = defaultLogger;
|
|
10
|
-
|
|
11
|
-
function write(level, message, detail = '') {
|
|
12
|
-
const formattedDetail = formatDetail(detail);
|
|
13
|
-
const line = formattedDetail
|
|
14
|
-
? `[${formatTimestamp()}] [android-toolkit] ${message} ${formattedDetail}`
|
|
15
|
-
: `[${formatTimestamp()}] [android-toolkit] ${message}`;
|
|
16
|
-
const writer = activeLogger[level] || activeLogger.warning || activeLogger.warn || activeLogger.info || console.error;
|
|
17
|
-
writer.call(activeLogger, line);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// 轻量 Logger,接口风格跟 playwright-toolkit 接近。
|
|
21
|
-
// 当前 android-toolkit 先不引入 crawlee/apify 依赖,避免 native actor 在本地运行时额外装包。
|
|
22
|
-
export const Logger = {
|
|
23
|
-
setLogger(logger) {
|
|
24
|
-
activeLogger = logger || defaultLogger;
|
|
25
|
-
},
|
|
26
|
-
start(message, detail = '') {
|
|
27
|
-
write('info', `▶ ${message}`, detail);
|
|
28
|
-
},
|
|
29
|
-
success(message, detail = '') {
|
|
30
|
-
write('info', `✓ ${message}`, detail);
|
|
31
|
-
},
|
|
32
|
-
fail(message, error = '') {
|
|
33
|
-
const detail = error?.message || String(error || '');
|
|
34
|
-
write('error', `✗ ${message}`, detail);
|
|
35
|
-
},
|
|
36
|
-
warn(message, detail = '') {
|
|
37
|
-
write('warn', `⚠ ${message}`, detail);
|
|
38
|
-
},
|
|
39
|
-
info(message, detail = '') {
|
|
40
|
-
write('info', message, detail);
|
|
41
|
-
},
|
|
42
|
-
debug(message, detail = '') {
|
|
43
|
-
write('debug', message, detail);
|
|
44
|
-
},
|
|
45
|
-
duration(startedAt) {
|
|
46
|
-
return `${((Date.now() - startedAt) / 1000).toFixed(2)}s`;
|
|
47
|
-
},
|
|
48
|
-
createLogger(scope) {
|
|
49
|
-
const prefix = scope ? `[${scope}] ` : '';
|
|
50
|
-
return {
|
|
51
|
-
start: (message, detail = '') => Logger.start(`${prefix}${message}`, detail),
|
|
52
|
-
success: (message, detail = '') => Logger.success(`${prefix}${message}`, detail),
|
|
53
|
-
fail: (message, error = '') => Logger.fail(`${prefix}${message}`, error),
|
|
54
|
-
warn: (message, detail = '') => Logger.warn(`${prefix}${message}`, detail),
|
|
55
|
-
info: (message, detail = '') => Logger.info(`${prefix}${message}`, detail),
|
|
56
|
-
debug: (message, detail = '') => Logger.debug(`${prefix}${message}`, detail)
|
|
57
|
-
};
|
|
58
|
-
},
|
|
59
|
-
useTemplate() {
|
|
60
|
-
return {
|
|
61
|
-
taskStart: (detail) => Logger.start('任务开始', detail),
|
|
62
|
-
taskSuccess: (detail) => Logger.success('任务完成', detail),
|
|
63
|
-
taskFail: (detail, error) => Logger.fail(`任务失败 ${detail || ''}`.trim(), error),
|
|
64
|
-
stepStart: (step) => Logger.start(`[Step] ${step}`),
|
|
65
|
-
stepSuccess: (step) => Logger.success(`[Step] ${step}`),
|
|
66
|
-
stepFail: (step, error) => Logger.fail(`[Step] ${step}`, error)
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export function sleep(ms) {
|
|
72
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function formatTimestamp(date = new Date()) {
|
|
76
|
-
const pad = (value, size = 2) => String(value).padStart(size, '0');
|
|
77
|
-
return [
|
|
78
|
-
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
|
|
79
|
-
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`
|
|
80
|
-
].join(' ');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function formatDetail(detail) {
|
|
84
|
-
if (detail == null || detail === '') return '';
|
|
85
|
-
if (typeof detail === 'string') return detail;
|
|
86
|
-
if (detail instanceof Error) return detail.message;
|
|
87
|
-
if (Array.isArray(detail)) {
|
|
88
|
-
return detail.map(formatDetail).filter(Boolean).join(' ');
|
|
89
|
-
}
|
|
90
|
-
if (typeof detail === 'object') {
|
|
91
|
-
const parts = [];
|
|
92
|
-
for (const [key, value] of Object.entries(detail)) {
|
|
93
|
-
if (value == null || value === '') continue;
|
|
94
|
-
if (typeof value === 'object') {
|
|
95
|
-
parts.push(`${key}=${safeJson(value)}`);
|
|
96
|
-
} else {
|
|
97
|
-
parts.push(`${key}=${String(value)}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return parts.join(' ');
|
|
101
|
-
}
|
|
102
|
-
return String(detail);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function safeJson(value) {
|
|
106
|
-
try {
|
|
107
|
-
return JSON.stringify(value);
|
|
108
|
-
} catch {
|
|
109
|
-
return String(value);
|
|
110
|
-
}
|
|
111
|
-
}
|