@skrillex1224/android-toolkit 0.1.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 -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/src/apify-kit.js
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
|
|
3
|
-
import {Code, Status} from './constants.js';
|
|
4
|
-
import {createAndroidContext} from './context.js';
|
|
5
|
-
import {CrawlerError, serializeError} from './errors.js';
|
|
6
|
-
import {Logger} from './logger.js';
|
|
7
|
-
|
|
8
|
-
let instance = null;
|
|
9
|
-
|
|
10
|
-
async function createApifyKit(options = {}) {
|
|
11
|
-
const io = {
|
|
12
|
-
inputPath: options.inputPath || '',
|
|
13
|
-
outputPath: requiredOption(options.outputPath, 'outputPath')
|
|
14
|
-
};
|
|
15
|
-
const input = options.input || (io.inputPath ? JSON.parse(fs.readFileSync(io.inputPath, 'utf8')) : {});
|
|
16
|
-
const ctx = options.ctx || createAndroidContext(input, options.contextDefaults || {});
|
|
17
|
-
let pushed = false;
|
|
18
|
-
let lastDevice = null;
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
ctx,
|
|
22
|
-
input,
|
|
23
|
-
inputPath: io.inputPath,
|
|
24
|
-
outputPath: io.outputPath,
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Android 版 runStep。
|
|
28
|
-
*
|
|
29
|
-
* 设计上贴近 playwright-toolkit 的 ApifyKit.runStep:
|
|
30
|
-
* - step 是中文步骤名;
|
|
31
|
-
* - target 位置保留给 device/session,便于未来接入云真机 live view;
|
|
32
|
-
* - actionFn 是真正业务动作;
|
|
33
|
-
* - failActor=true 时会先 pushFailed,再把错误继续抛给 Launch。
|
|
34
|
-
*/
|
|
35
|
-
async runStep(step, target, actionFn, options = {}) {
|
|
36
|
-
if (target) lastDevice = target;
|
|
37
|
-
const {failActor = true} = options;
|
|
38
|
-
const startedAt = Date.now();
|
|
39
|
-
|
|
40
|
-
Logger.start(`[Step] ${step}`, stepDetail({target, failActor}));
|
|
41
|
-
try {
|
|
42
|
-
const result = await actionFn();
|
|
43
|
-
Logger.success(`[Step] ${step}`, {duration: Logger.duration(startedAt)});
|
|
44
|
-
return result;
|
|
45
|
-
} catch (error) {
|
|
46
|
-
Logger.fail(`[Step] ${step} duration=${Logger.duration(startedAt)}`, error);
|
|
47
|
-
if (failActor) {
|
|
48
|
-
await this.pushFailed(error, {step}, {target});
|
|
49
|
-
}
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 宽松版 runStep。
|
|
56
|
-
*
|
|
57
|
-
* 和 playwright-toolkit 一样:只负责打日志和抛错,不主动写失败 dataset。
|
|
58
|
-
* 业务层可以用它包“可选截图/可选关闭弹窗/可选诊断”这类步骤。
|
|
59
|
-
*/
|
|
60
|
-
async runStepLoose(step, target, fn, options = {}) {
|
|
61
|
-
return this.runStep(step, target, fn, {
|
|
62
|
-
...options,
|
|
63
|
-
failActor: false
|
|
64
|
-
});
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 写成功 dataset。
|
|
69
|
-
*
|
|
70
|
-
* 对齐 playwright-toolkit:业务 actor 只传业务数据,toolkit 统一补
|
|
71
|
-
* code/status/timestamp/data 外壳。这样 commander/transformer 看到的是同类结构。
|
|
72
|
-
*/
|
|
73
|
-
async pushSuccess(data = {}, options = {}) {
|
|
74
|
-
const dataSummary = summarizeDatasetData(data);
|
|
75
|
-
pushed = true;
|
|
76
|
-
appendOutput(io.outputPath, {
|
|
77
|
-
code: Code.Success,
|
|
78
|
-
status: Status.Success,
|
|
79
|
-
timestamp: new Date().toISOString(),
|
|
80
|
-
data: sanitizeData(data),
|
|
81
|
-
...(options.meta ? {meta: sanitizeData(options.meta)} : {})
|
|
82
|
-
});
|
|
83
|
-
Logger.success('pushSuccess', {
|
|
84
|
-
outputPath: io.outputPath,
|
|
85
|
-
...dataSummary
|
|
86
|
-
});
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 写补充 artifact dataset。
|
|
91
|
-
*
|
|
92
|
-
* 这类数据不是最终业务结果,只用于给 commander 的 ext/base64 链路消费,
|
|
93
|
-
* 例如 screenshotBase64、conversationShareLink。它不改变 hasPushed()
|
|
94
|
-
* 状态,避免 artifact 写入后主流程失败时吞掉失败 dataset。
|
|
95
|
-
*/
|
|
96
|
-
async pushArtifact(data = {}) {
|
|
97
|
-
appendOutput(io.outputPath, sanitizeData(data));
|
|
98
|
-
Logger.success('pushArtifact', {
|
|
99
|
-
outputPath: io.outputPath,
|
|
100
|
-
keys: Object.keys(data || {}).join(',')
|
|
101
|
-
});
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 写失败 dataset。
|
|
106
|
-
*
|
|
107
|
-
* 失败结构也对齐 playwright-toolkit:CrawlerError 决定 code/context,
|
|
108
|
-
* 普通 Error 统一按 UnknownError 处理,业务可预期失败必须显式抛 CrawlerError。
|
|
109
|
-
*/
|
|
110
|
-
async pushFailed(error, meta = {}) {
|
|
111
|
-
const normalized = CrawlerError.isCrawlerError(error)
|
|
112
|
-
? error
|
|
113
|
-
: CrawlerError.from(error, {code: Code.UnknownError, context: meta});
|
|
114
|
-
|
|
115
|
-
pushed = true;
|
|
116
|
-
appendOutput(io.outputPath, {
|
|
117
|
-
code: normalized.code,
|
|
118
|
-
status: Status.Failed,
|
|
119
|
-
error: serializeError(normalized),
|
|
120
|
-
meta: sanitizeData(meta),
|
|
121
|
-
context: sanitizeData(normalized.context),
|
|
122
|
-
timestamp: new Date().toISOString()
|
|
123
|
-
});
|
|
124
|
-
Logger.success('pushFailed', {
|
|
125
|
-
outputPath: io.outputPath,
|
|
126
|
-
code: normalized.code,
|
|
127
|
-
message: normalized.message
|
|
128
|
-
});
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
hasPushed() {
|
|
132
|
-
return pushed;
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
getLastDevice() {
|
|
136
|
-
return lastDevice;
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function useApifyKit(options = {}) {
|
|
142
|
-
if (!instance || options.reset) {
|
|
143
|
-
instance = await createApifyKit(options);
|
|
144
|
-
}
|
|
145
|
-
return instance;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export const ApifyKit = {
|
|
149
|
-
useApifyKit
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
function appendOutput(outputPath, item) {
|
|
153
|
-
const items = readOutputArray(outputPath);
|
|
154
|
-
items.push(item);
|
|
155
|
-
fs.writeFileSync(outputPath, JSON.stringify(items, null, 2) + '\n', 'utf8');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function readOutputArray(outputPath) {
|
|
159
|
-
if (!fs.existsSync(outputPath)) return [];
|
|
160
|
-
const raw = fs.readFileSync(outputPath, 'utf8').trim();
|
|
161
|
-
if (!raw) return [];
|
|
162
|
-
const parsed = JSON.parse(raw);
|
|
163
|
-
if (!Array.isArray(parsed)) {
|
|
164
|
-
throw new Error(`Android output must be a JSON array: ${outputPath}`);
|
|
165
|
-
}
|
|
166
|
-
return parsed;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function requiredOption(value, name) {
|
|
170
|
-
const clean = String(value || '').trim();
|
|
171
|
-
if (!clean) throw new Error(`ApifyKit.useApifyKit requires ${name}`);
|
|
172
|
-
return clean;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function stepDetail({target, failActor}) {
|
|
176
|
-
const detail = {failActor};
|
|
177
|
-
if (!target) return detail;
|
|
178
|
-
if (typeof target === 'string') detail.target = target;
|
|
179
|
-
else if (typeof target === 'object') {
|
|
180
|
-
detail.serial = target.serial;
|
|
181
|
-
detail.packageName = target.packageName;
|
|
182
|
-
detail.runId = target.runId;
|
|
183
|
-
}
|
|
184
|
-
return detail;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function summarizeDatasetData(data) {
|
|
188
|
-
const summary = {};
|
|
189
|
-
if (typeof data?.answer === 'string') summary.answerChars = data.answer.length;
|
|
190
|
-
if (Array.isArray(data?.sources)) summary.sourceCount = data.sources.length;
|
|
191
|
-
if (data?.imMessage?.message_id) summary.messageId = data.imMessage.message_id;
|
|
192
|
-
if (data?.imMessage?.local_message_id) summary.localMessageId = data.imMessage.local_message_id;
|
|
193
|
-
return summary;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function sanitizeData(value, depth = 0, seen = new WeakSet()) {
|
|
197
|
-
if (depth > 8) return '[MaxDepth]';
|
|
198
|
-
if (value == null) return value;
|
|
199
|
-
const valueType = typeof value;
|
|
200
|
-
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') return value;
|
|
201
|
-
if (valueType === 'bigint') return value.toString();
|
|
202
|
-
if (valueType === 'function' || valueType === 'symbol') return undefined;
|
|
203
|
-
if (value instanceof Error) return serializeError(value);
|
|
204
|
-
if (Array.isArray(value)) {
|
|
205
|
-
return value.map((item) => sanitizeData(item, depth + 1, seen)).filter((item) => item !== undefined);
|
|
206
|
-
}
|
|
207
|
-
if (valueType !== 'object') return String(value);
|
|
208
|
-
if (seen.has(value)) return '[Circular]';
|
|
209
|
-
seen.add(value);
|
|
210
|
-
|
|
211
|
-
const out = {};
|
|
212
|
-
for (const [key, item] of Object.entries(value)) {
|
|
213
|
-
const safe = sanitizeData(item, depth + 1, seen);
|
|
214
|
-
if (safe !== undefined) out[key] = safe;
|
|
215
|
-
}
|
|
216
|
-
return out;
|
|
217
|
-
}
|
package/src/constants.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
export const Code = {
|
|
2
|
-
Success: 0,
|
|
3
|
-
UnknownError: -1,
|
|
4
|
-
NotLogin: 30000001,
|
|
5
|
-
Chaptcha: 30000002,
|
|
6
|
-
Timeout: 30000003,
|
|
7
|
-
InitialTimeout: 30000004,
|
|
8
|
-
OverallTimeout: 30000005,
|
|
9
|
-
|
|
10
|
-
// Android-toolkit 扩展码从 300100xx 开始,避免改动 Playwright-toolkit 既有码值。
|
|
11
|
-
InvalidRequest: 30010001,
|
|
12
|
-
AdbUnavailable: 30010002,
|
|
13
|
-
ContentUnavailable: 30010003,
|
|
14
|
-
SourceExtractionFailed: 30010004,
|
|
15
|
-
AutomationFailed: 30010005,
|
|
16
|
-
RunnerFailed: 30010006,
|
|
17
|
-
AppiumUnavailable: 30010007,
|
|
18
|
-
FridaUnavailable: 30010008,
|
|
19
|
-
AppNotInstalled: 30010009
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const Status = {
|
|
23
|
-
Success: 'SUCCESS',
|
|
24
|
-
Failed: 'FAILED'
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const normalizePrefix = (value) => String(value || '').trim();
|
|
28
|
-
|
|
29
|
-
const normalizeShare = (value) => {
|
|
30
|
-
const raw = value && typeof value === 'object' ? value : {};
|
|
31
|
-
const modeRaw = String(raw.mode || 'clipboard').trim().toLowerCase();
|
|
32
|
-
const mode = ['clipboard', 'custom'].includes(modeRaw) ? modeRaw : 'clipboard';
|
|
33
|
-
return {
|
|
34
|
-
mode,
|
|
35
|
-
prefix: normalizePrefix(raw.prefix),
|
|
36
|
-
xurl: Array.isArray(raw.xurl) ? raw.xurl : []
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const createActorInfo = (info) => {
|
|
41
|
-
const key = String(info?.key || '').trim();
|
|
42
|
-
const share = normalizeShare(info?.share);
|
|
43
|
-
return {
|
|
44
|
-
...info,
|
|
45
|
-
key,
|
|
46
|
-
share,
|
|
47
|
-
get icon() {
|
|
48
|
-
if (info.icon) return info.icon;
|
|
49
|
-
return buildIcon(this.key);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const buildIcon = (key) => `https://static.heartbitai.com/general/actors/${key}.png`;
|
|
55
|
-
|
|
56
|
-
export const ActorInfo = {
|
|
57
|
-
'doubao.android': createActorInfo({
|
|
58
|
-
key: 'doubao.android',
|
|
59
|
-
name: '豆包 Android',
|
|
60
|
-
share: {
|
|
61
|
-
mode: 'clipboard',
|
|
62
|
-
prefix: 'https://www.doubao.com/thread/',
|
|
63
|
-
xurl: []
|
|
64
|
-
}
|
|
65
|
-
}),
|
|
66
|
-
'wechat.android': createActorInfo({
|
|
67
|
-
key: 'wechat.android',
|
|
68
|
-
name: '微信 Android',
|
|
69
|
-
share: {
|
|
70
|
-
mode: 'clipboard',
|
|
71
|
-
prefix: '',
|
|
72
|
-
xurl: []
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
};
|
package/src/context.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import {execFileSync} from 'node:child_process';
|
|
3
|
-
import {fileURLToPath} from 'node:url';
|
|
4
|
-
|
|
5
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const toolkitRoot = path.resolve(__dirname, '..');
|
|
7
|
-
|
|
8
|
-
export const Context = {
|
|
9
|
-
createAndroidContext,
|
|
10
|
-
firstNonEmpty,
|
|
11
|
-
toolkitPath
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 创建 Android actor 的运行上下文。
|
|
16
|
-
*
|
|
17
|
-
* 这里刻意只接受 input/defaults 参数,不读取环境变量,也不写任何业务默认值:
|
|
18
|
-
* - 具体 app 包名由后端注入 input.runtime.device.packageName;
|
|
19
|
-
* - 不知道当前 visitor 要 attach 哪些 WebView 类;
|
|
20
|
-
*
|
|
21
|
-
* ADB/Frida 是机器环境,不是业务 input。toolkit 只从 PATH 查找命令;
|
|
22
|
-
* 安装和 PATH 准备由 postinstall 或部署环境负责。
|
|
23
|
-
*/
|
|
24
|
-
export function createAndroidContext(input = {}, defaults = {}) {
|
|
25
|
-
const runtime = input.runtime || {};
|
|
26
|
-
const device = normalizeRuntimeDevice(runtime.device);
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
runId: firstNonEmpty(defaults.runId),
|
|
30
|
-
query: String(input.query || defaults.query || '').trim(),
|
|
31
|
-
mode: firstNonEmpty(defaults.mode, 'ai_search'),
|
|
32
|
-
serial: firstNonEmpty(device.serial, defaults.serial),
|
|
33
|
-
adbPath: resolveAdbPath(defaults),
|
|
34
|
-
packageName: firstNonEmpty(device.packageName),
|
|
35
|
-
fridaPath: resolveFridaPath(defaults),
|
|
36
|
-
// Frida WebView event agent 是当前保留的唯一 Frida agent:
|
|
37
|
-
// 它 hook WebView 执行 JS 字符串的入口,业务方再从注入事件中解析数据。
|
|
38
|
-
fridaWebViewEventAgentScript: firstNonEmpty(
|
|
39
|
-
defaults.fridaWebViewEventAgentScript,
|
|
40
|
-
toolkitPath('src/internals/frida/webview_event_agent.js')
|
|
41
|
-
),
|
|
42
|
-
appVersion: firstNonEmpty(defaults.appVersion),
|
|
43
|
-
slotKey: firstNonEmpty(defaults.slotKey),
|
|
44
|
-
instanceId: firstNonEmpty(defaults.instanceId),
|
|
45
|
-
externalIp: firstNonEmpty(defaults.externalIp)
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function toolkitPath(relativePath) {
|
|
50
|
-
return path.resolve(toolkitRoot, relativePath);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function firstNonEmpty(...values) {
|
|
54
|
-
for (const value of values) {
|
|
55
|
-
const clean = String(value ?? '').trim();
|
|
56
|
-
if (clean) return clean;
|
|
57
|
-
}
|
|
58
|
-
return '';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function normalizeRuntimeDevice(value) {
|
|
62
|
-
if (typeof value === 'string') return {serial: value};
|
|
63
|
-
if (value && typeof value === 'object') return value;
|
|
64
|
-
return {};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function resolveAdbPath(defaults = {}) {
|
|
68
|
-
return firstNonEmpty(
|
|
69
|
-
defaults.adbPath,
|
|
70
|
-
findCommand('adb'),
|
|
71
|
-
'adb'
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function resolveFridaPath(defaults = {}) {
|
|
76
|
-
return firstNonEmpty(
|
|
77
|
-
defaults.fridaPath,
|
|
78
|
-
findCommand('frida'),
|
|
79
|
-
'frida'
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function findCommand(command) {
|
|
84
|
-
try {
|
|
85
|
-
return String(execFileSync('which', [command], {encoding: 'utf8'})).trim();
|
|
86
|
-
} catch {
|
|
87
|
-
return '';
|
|
88
|
-
}
|
|
89
|
-
}
|