@skrillex1224/android-toolkit 0.1.7 → 0.1.8
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 +34 -4
- package/entrys/node.js +2 -0
- package/index.d.ts +191 -0
- package/package.json +7 -2
- package/src/apify-kit.js +15 -0
- package/src/constants.js +50 -0
- package/src/context.js +2 -2
- package/src/device.js +385 -10
- package/src/internals/compression.js +188 -0
- package/src/launch.js +2 -0
- package/src/share.js +644 -0
package/README.md
CHANGED
|
@@ -20,13 +20,13 @@ await Launch.run(async ({ input, ctx, kit }) => {
|
|
|
20
20
|
await kit.ApifyKit.pushSuccess(result);
|
|
21
21
|
}, {
|
|
22
22
|
inputPath,
|
|
23
|
-
outputPath
|
|
24
|
-
contextDefaults: {
|
|
25
|
-
packageName: 'com.example.app'
|
|
26
|
-
}
|
|
23
|
+
outputPath
|
|
27
24
|
});
|
|
28
25
|
```
|
|
29
26
|
|
|
27
|
+
`ctx.packageName` 来自后端注入的 `input.runtime.device.packageName`。Android Toolkit
|
|
28
|
+
不把包名放进 `Constants.ActorInfo`,也不在业务包里维护包名真源。
|
|
29
|
+
|
|
30
30
|
## 保留模块
|
|
31
31
|
|
|
32
32
|
| 模块 | 说明 |
|
|
@@ -35,9 +35,39 @@ await Launch.run(async ({ input, ctx, kit }) => {
|
|
|
35
35
|
| `ApifyKit` | 提供 `runStep`、`runStepLoose`、`pushSuccess`、`pushFailed`,并统一补齐 `code/status/timestamp/data` |
|
|
36
36
|
| `Device` | ADB 操作层:启动、点击、滑动、输入中文、截图、Activity 检查 |
|
|
37
37
|
| `Frida` | Frida attach、短脚本执行、SQLite 查询、WebView event recorder |
|
|
38
|
+
| `Share` | 对齐 `playwright-toolkit` 的分享能力:高视口截图、分享链接捕获、截图压缩 |
|
|
38
39
|
| `Context` | 从显式 input/defaults 生成 Android 运行上下文 |
|
|
39
40
|
| `Errors` | `CrawlerError` 和错误序列化;失败码由业务显式抛出的 `CrawlerError` 决定 |
|
|
40
41
|
|
|
42
|
+
## Share
|
|
43
|
+
|
|
44
|
+
`Share.captureScreen(ctx, options)` 用于采集 Android 页面长图。实现策略是先用
|
|
45
|
+
Frida 扫描当前 Activity 的 View 树,读取所有可滚动 View 的
|
|
46
|
+
`computeVerticalScrollRange()`,取最大所需高度后临时执行 `wm size WxH`,再用
|
|
47
|
+
`screencap` 一次性截图。它不裁剪状态栏和导航栏;如果 View 树无法读取,会直接报错,
|
|
48
|
+
不会退回普通视口截图。默认压缩阈值与 `playwright-toolkit` 一致:base64 最大
|
|
49
|
+
5MiB,JPEG,质量 `0.72`,最低质量 `0.38`,最低缩放 `0.25`。
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
const screenshotBase64 = await Share.captureScreen(ctx, {
|
|
53
|
+
Frida,
|
|
54
|
+
actorInfo: Constants.ActorInfo['doubao.android'].key,
|
|
55
|
+
maxHeight: 8000
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Share.captureLink(ctx, options)` 负责按 `Constants.ActorInfo[actor].share.prefix`
|
|
60
|
+
校验分享链接。Android 当前稳定模式是 `clipboard`:业务层执行点击分享、复制链接,
|
|
61
|
+
toolkit 通过 `Frida.runScript` 轮询系统剪贴板候选内容并提取匹配 prefix 的 URL。
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const result = await Share.captureLink(ctx, {
|
|
65
|
+
Frida,
|
|
66
|
+
actorInfo: Constants.ActorInfo['doubao.android'].key,
|
|
67
|
+
performActions: async () => clickShareAndCopyLink()
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
41
71
|
## 当前边界
|
|
42
72
|
|
|
43
73
|
放进 toolkit:
|
package/entrys/node.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Launch } from '../src/launch.js';
|
|
|
2
2
|
import { ApifyKit } from '../src/apify-kit.js';
|
|
3
3
|
import { Device } from '../src/device.js';
|
|
4
4
|
import { Frida } from '../src/frida-client.js';
|
|
5
|
+
import { Share } from '../src/share.js';
|
|
5
6
|
import * as Constants from '../src/constants.js';
|
|
6
7
|
import * as Errors from '../src/errors.js';
|
|
7
8
|
import { Logger } from '../src/logger.js';
|
|
@@ -16,6 +17,7 @@ export const useAndroidToolKit = () => {
|
|
|
16
17
|
ApifyKit,
|
|
17
18
|
Device,
|
|
18
19
|
Frida,
|
|
20
|
+
Share,
|
|
19
21
|
Constants,
|
|
20
22
|
Errors,
|
|
21
23
|
Logger,
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export interface AndroidContext {
|
|
2
|
+
adbPath?: string;
|
|
3
|
+
fridaPath?: string;
|
|
4
|
+
fridaServerPath?: string;
|
|
5
|
+
fridaWebViewEventAgentScript?: string;
|
|
6
|
+
serial: string;
|
|
7
|
+
packageName: string;
|
|
8
|
+
query: string;
|
|
9
|
+
runId?: string;
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AndroidActorShareInfo {
|
|
14
|
+
mode: 'clipboard' | 'custom';
|
|
15
|
+
prefix: string;
|
|
16
|
+
xurl: Array<string | string[]>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AndroidActorInfoItem {
|
|
20
|
+
key: string;
|
|
21
|
+
name: string;
|
|
22
|
+
icon: string;
|
|
23
|
+
share: AndroidActorShareInfo;
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AndroidConstantsModule {
|
|
28
|
+
Code: Record<string, number>;
|
|
29
|
+
Status: Record<string, string>;
|
|
30
|
+
ActorInfo: Record<string, AndroidActorInfoItem>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AndroidCrawlerErrorInfo {
|
|
34
|
+
message: string;
|
|
35
|
+
code?: number;
|
|
36
|
+
context?: Record<string, any>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AndroidCrawlerError extends Error {
|
|
40
|
+
code: number;
|
|
41
|
+
context?: Record<string, any>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AndroidErrorsModule {
|
|
45
|
+
CrawlerError: new (info: AndroidCrawlerErrorInfo) => AndroidCrawlerError;
|
|
46
|
+
serializeError(error: unknown): Record<string, any>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AndroidLogger {
|
|
50
|
+
debug(message: string, meta?: Record<string, any>): void;
|
|
51
|
+
info(message: string, meta?: Record<string, any>): void;
|
|
52
|
+
warn(message: string, meta?: Record<string, any>): void;
|
|
53
|
+
success(message: string, meta?: Record<string, any>): void;
|
|
54
|
+
fail(message: string, error?: unknown): void;
|
|
55
|
+
start(message: string, meta?: Record<string, any>): void;
|
|
56
|
+
duration(startedAt: number): string;
|
|
57
|
+
createLogger(scope: string): AndroidLogger;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AndroidDeviceModule {
|
|
61
|
+
adbShell(ctx: AndroidContext, args: string[], options?: Record<string, any>): Promise<string>;
|
|
62
|
+
adbExec(ctx: AndroidContext, args: string[], options?: Record<string, any>): Promise<string>;
|
|
63
|
+
forceStopApp(ctx: AndroidContext, packageName?: string): Promise<void>;
|
|
64
|
+
startActivity(ctx: AndroidContext, component: string, args?: string[]): Promise<void>;
|
|
65
|
+
startLauncher(ctx: AndroidContext, packageName?: string, launcherActivity?: string): Promise<void>;
|
|
66
|
+
waitForForeground(ctx: AndroidContext, packageName?: string, timeoutMs?: number): Promise<boolean>;
|
|
67
|
+
screenSize(ctx: AndroidContext): Promise<{ width: number; height: number }>;
|
|
68
|
+
screenDensity(ctx: AndroidContext): Promise<number>;
|
|
69
|
+
overrideScreenSize(ctx: AndroidContext, width: number, height: number): Promise<void>;
|
|
70
|
+
resetScreenSize(ctx: AndroidContext): Promise<void>;
|
|
71
|
+
resetScreenDensity(ctx: AndroidContext): Promise<void>;
|
|
72
|
+
click(ctx: AndroidContext, pointOrX: any, y?: number): Promise<void>;
|
|
73
|
+
move(ctx: AndroidContext, from: { x: number; y: number }, to: { x: number; y: number }, durationMs?: number): Promise<void>;
|
|
74
|
+
pressBack(ctx: AndroidContext): Promise<void>;
|
|
75
|
+
wakeAndUnlock(ctx: AndroidContext): Promise<void>;
|
|
76
|
+
focusedActivity(ctx: AndroidContext): Promise<string>;
|
|
77
|
+
waitForActivity(ctx: AndroidContext, predicate: (focused: string) => boolean, timeoutMs?: number, intervalMs?: number): Promise<string>;
|
|
78
|
+
screenshotPng(ctx: AndroidContext): Promise<Buffer>;
|
|
79
|
+
screenshotBase64(ctx: AndroidContext): Promise<string>;
|
|
80
|
+
[key: string]: any;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface AndroidFridaModule {
|
|
84
|
+
runScript(ctx: AndroidContext, source: string, options?: Record<string, any>): Promise<any>;
|
|
85
|
+
querySQLite(ctx: AndroidContext, options?: Record<string, any>): Promise<any>;
|
|
86
|
+
startWebViewEventRecorder(ctx: AndroidContext, config?: Record<string, any>, options?: Record<string, any>): Promise<any>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface AndroidShareScreenCompressionOptions {
|
|
90
|
+
enabled?: boolean;
|
|
91
|
+
maxBytes?: number;
|
|
92
|
+
maxBase64Bytes?: number;
|
|
93
|
+
type?: 'jpeg' | 'jpg';
|
|
94
|
+
outputType?: 'jpeg' | 'jpg';
|
|
95
|
+
quality?: number;
|
|
96
|
+
minQuality?: number;
|
|
97
|
+
minScale?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface AndroidShareScreenCaptureOptions {
|
|
101
|
+
Frida?: AndroidFridaModule;
|
|
102
|
+
frida?: AndroidFridaModule;
|
|
103
|
+
actorInfo?: string | AndroidActorInfoItem;
|
|
104
|
+
actor?: string | AndroidActorInfoItem;
|
|
105
|
+
actorKey?: string | AndroidActorInfoItem;
|
|
106
|
+
restore?: boolean;
|
|
107
|
+
maxHeight?: number;
|
|
108
|
+
width?: number;
|
|
109
|
+
height?: number;
|
|
110
|
+
settleMs?: number;
|
|
111
|
+
scrollToTop?: boolean;
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
fridaTimeoutSeconds?: number;
|
|
114
|
+
maxBytes?: number;
|
|
115
|
+
maxBase64Bytes?: number;
|
|
116
|
+
type?: 'jpeg' | 'jpg';
|
|
117
|
+
outputType?: 'jpeg' | 'jpg';
|
|
118
|
+
quality?: number;
|
|
119
|
+
minQuality?: number;
|
|
120
|
+
minScale?: number;
|
|
121
|
+
compression?: boolean | AndroidShareScreenCompressionOptions;
|
|
122
|
+
compress?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface AndroidShareLinkCaptureOptions {
|
|
126
|
+
actorInfo?: string | AndroidActorInfoItem;
|
|
127
|
+
actor?: string | AndroidActorInfoItem;
|
|
128
|
+
actorKey?: string | AndroidActorInfoItem;
|
|
129
|
+
share?: Partial<AndroidActorShareInfo>;
|
|
130
|
+
timeoutMs?: number;
|
|
131
|
+
pollIntervalMs?: number;
|
|
132
|
+
payloadSnapshotMaxLen?: number;
|
|
133
|
+
signal?: AbortSignal;
|
|
134
|
+
performActions?: () => any | Promise<any>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface AndroidShareLinkCaptureResult {
|
|
138
|
+
link: string | null;
|
|
139
|
+
payloadText: string;
|
|
140
|
+
payloadSnapshot: string;
|
|
141
|
+
source: 'clipboard' | 'custom' | 'none' | string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface AndroidShareModule {
|
|
145
|
+
captureScreen(ctx: AndroidContext, options?: AndroidShareScreenCaptureOptions): Promise<string>;
|
|
146
|
+
captureLink(ctx: AndroidContext, options?: AndroidShareLinkCaptureOptions): Promise<AndroidShareLinkCaptureResult>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface AndroidApifyKitInstance {
|
|
150
|
+
runStep<T>(name: string, meta: any, fn: () => Promise<T> | T): Promise<T>;
|
|
151
|
+
runStepLoose<T>(name: string, meta: any, fn: () => Promise<T> | T): Promise<T>;
|
|
152
|
+
pushSuccess(data: any): Promise<void>;
|
|
153
|
+
pushFailed(error: unknown, context?: Record<string, any>): Promise<void>;
|
|
154
|
+
pushArtifact(data: any): Promise<void>;
|
|
155
|
+
hasPushed(): boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface AndroidApifyKitModule {
|
|
159
|
+
useApifyKit(options?: Record<string, any>): Promise<AndroidApifyKitInstance>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface AndroidLaunchRunHandlerArgs {
|
|
163
|
+
input: any;
|
|
164
|
+
ctx: AndroidContext;
|
|
165
|
+
kit: {
|
|
166
|
+
ApifyKit: AndroidApifyKitInstance;
|
|
167
|
+
Device: AndroidDeviceModule;
|
|
168
|
+
Frida: AndroidFridaModule;
|
|
169
|
+
Share: AndroidShareModule;
|
|
170
|
+
Logger: AndroidLogger;
|
|
171
|
+
};
|
|
172
|
+
ApifyKit: AndroidApifyKitInstance;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface AndroidLaunchModule {
|
|
176
|
+
run(handler: (args: AndroidLaunchRunHandlerArgs) => Promise<void> | void, options?: Record<string, any>): Promise<void>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface AndroidToolkit {
|
|
180
|
+
Launch: AndroidLaunchModule;
|
|
181
|
+
ApifyKit: AndroidApifyKitModule;
|
|
182
|
+
Device: AndroidDeviceModule;
|
|
183
|
+
Frida: AndroidFridaModule;
|
|
184
|
+
Share: AndroidShareModule;
|
|
185
|
+
Constants: AndroidConstantsModule;
|
|
186
|
+
Errors: AndroidErrorsModule;
|
|
187
|
+
Logger: AndroidLogger;
|
|
188
|
+
Context: Record<string, any>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function useAndroidToolKit(): AndroidToolkit;
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skrillex1224/android-toolkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": "./index.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"index.js",
|
|
12
|
+
"index.d.ts",
|
|
11
13
|
"entrys",
|
|
12
14
|
"src",
|
|
13
15
|
"scripts",
|
|
@@ -15,10 +17,13 @@
|
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
17
19
|
"postinstall": "node scripts/postinstall.js",
|
|
18
|
-
"check": "node --check index.js && node --check entrys/node.js && node --check src/launch.js && node --check src/apify-kit.js && node --check src/context.js && node --check src/constants.js && node --check src/device.js && node --check src/errors.js && node --check src/frida-client.js && node --check src/logger.js && node --check src/internals/frida/webview_event_agent.js",
|
|
20
|
+
"check": "node --check index.js && node --check entrys/node.js && node --check src/launch.js && node --check src/apify-kit.js && node --check src/context.js && node --check src/constants.js && node --check src/device.js && node --check src/errors.js && node --check src/frida-client.js && node --check src/logger.js && node --check src/share.js && node --check src/internals/compression.js && node --check src/internals/frida/webview_event_agent.js",
|
|
19
21
|
"test": "node --test test/*.test.js"
|
|
20
22
|
},
|
|
21
23
|
"engines": {
|
|
22
24
|
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"jimp": "^1.6.1"
|
|
23
28
|
}
|
|
24
29
|
}
|
package/src/apify-kit.js
CHANGED
|
@@ -86,6 +86,21 @@ async function createApifyKit(options = {}) {
|
|
|
86
86
|
});
|
|
87
87
|
},
|
|
88
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
|
+
|
|
89
104
|
/**
|
|
90
105
|
* 写失败 dataset。
|
|
91
106
|
*
|
package/src/constants.js
CHANGED
|
@@ -23,3 +23,53 @@ export const Status = {
|
|
|
23
23
|
Success: 'SUCCESS',
|
|
24
24
|
Failed: 'FAILED'
|
|
25
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
CHANGED
|
@@ -15,7 +15,7 @@ export const Context = {
|
|
|
15
15
|
* 创建 Android actor 的运行上下文。
|
|
16
16
|
*
|
|
17
17
|
* 这里刻意只接受 input/defaults 参数,不读取环境变量,也不写任何业务默认值:
|
|
18
|
-
* -
|
|
18
|
+
* - 具体 app 包名由后端注入 input.runtime.device.packageName;
|
|
19
19
|
* - 不知道当前 visitor 要 attach 哪些 WebView 类;
|
|
20
20
|
*
|
|
21
21
|
* ADB/Frida 是机器环境,不是业务 input。toolkit 只从 PATH 查找命令;
|
|
@@ -31,7 +31,7 @@ export function createAndroidContext(input = {}, defaults = {}) {
|
|
|
31
31
|
mode: firstNonEmpty(defaults.mode, 'ai_search'),
|
|
32
32
|
serial: firstNonEmpty(device.serial, defaults.serial),
|
|
33
33
|
adbPath: resolveAdbPath(defaults),
|
|
34
|
-
packageName: firstNonEmpty(
|
|
34
|
+
packageName: firstNonEmpty(device.packageName),
|
|
35
35
|
fridaPath: resolveFridaPath(defaults),
|
|
36
36
|
// Frida WebView event agent 是当前保留的唯一 Frida agent:
|
|
37
37
|
// 它 hook WebView 执行 JS 字符串的入口,业务方再从注入事件中解析数据。
|