gf-packages-cli 1.0.6 → 1.0.7
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/bin/cli.js +6 -0
- package/lib/common/appDownload.js +187 -0
- package/package.json +1 -1
- package/src/downloadPackage.js +57 -0
- package/src/index.js +19 -1
- package/src/testActivation.js +315 -0
- package/tmp_activation/_getGaid.js +10 -0
package/bin/cli.js
CHANGED
|
@@ -5,6 +5,8 @@ const pkg = require('../package.json');
|
|
|
5
5
|
const showMenu = require('../src/index.js');
|
|
6
6
|
const exportCommand = require('../src/exportPackage.js');
|
|
7
7
|
const deleteCommand = require('../src/deletePackage.js');
|
|
8
|
+
const testActivationCommand = require('../src/testActivation.js');
|
|
9
|
+
const downloadPackageCommand = require('../src/downloadPackage.js');
|
|
8
10
|
const autoUpdate = require('../lib/common/updateCheck.js');
|
|
9
11
|
|
|
10
12
|
program
|
|
@@ -20,6 +22,10 @@ program.hook('preAction', async () => {
|
|
|
20
22
|
program.addCommand(exportCommand);
|
|
21
23
|
// 注册 delete 子命令
|
|
22
24
|
program.addCommand(deleteCommand);
|
|
25
|
+
// 注册 test-activation 子命令
|
|
26
|
+
program.addCommand(testActivationCommand);
|
|
27
|
+
// 注册 download-pkg 子命令
|
|
28
|
+
program.addCommand(downloadPackageCommand);
|
|
23
29
|
// 无子命令时,显示菜单
|
|
24
30
|
program
|
|
25
31
|
.action(() => {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// ===================== 云控下载 + 安装(公共模块) =====================
|
|
2
|
+
// 供 test-activation 与 download-pkg 两个功能复用
|
|
3
|
+
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const chalk = require('chalk').default;
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const AdmZip = require('adm-zip');
|
|
9
|
+
const inquirer = require('inquirer');
|
|
10
|
+
const { runAdbSync } = require('../tools/adb.js');
|
|
11
|
+
|
|
12
|
+
/** 以可见进度执行 adb 命令(spawn 继承 stdio,显示 adb 原生进度条)
|
|
13
|
+
* @param {string|null} deviceId 设备序列号
|
|
14
|
+
* @param {string[]} args adb 参数数组(不含 -s 和 adb 前缀)
|
|
15
|
+
*/
|
|
16
|
+
function runAdbLive(deviceId, args) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const base = deviceId ? ['-s', deviceId] : [];
|
|
19
|
+
const child = spawn('adb', [...base, ...args], { stdio: 'inherit' });
|
|
20
|
+
child.on('error', reject);
|
|
21
|
+
child.on('close', code => {
|
|
22
|
+
if (code === 0) resolve();
|
|
23
|
+
else reject(new Error(`adb ${args[0]} 退出码 ${code}`));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ===================== 云控下载参数(固定值) =====================
|
|
29
|
+
|
|
30
|
+
const DEFAULT_CLOUD_SERVER = 'http://43.155.24.172';
|
|
31
|
+
const SERVER_PARAMS = {
|
|
32
|
+
cloudServerUrl: DEFAULT_CLOUD_SERVER,
|
|
33
|
+
countryCode: 'US',
|
|
34
|
+
type: '1',
|
|
35
|
+
appMarketType: 'google',
|
|
36
|
+
offerType: '5',
|
|
37
|
+
taskType: '5001'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
async function getServerConfig() {
|
|
41
|
+
console.log(chalk.gray(`使用云控参数: ${JSON.stringify(SERVER_PARAMS)}`));
|
|
42
|
+
return { ...SERVER_PARAMS };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ===================== 获取下载信息 =====================
|
|
46
|
+
|
|
47
|
+
/** 获取设备SN(对应 deviceUtil.getSerialNo:getprop ro.boot.serialno) */
|
|
48
|
+
async function getDeviceSn(deviceId) {
|
|
49
|
+
try {
|
|
50
|
+
const out = await runAdbSync(deviceId, 'shell getprop ro.boot.serialno');
|
|
51
|
+
if (out.trim()) return out.trim();
|
|
52
|
+
} catch { }
|
|
53
|
+
throw new Error('无法获取设备SN');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 获取应用下载信息;ftp_url 为相对路径时通过 ftpAccount 接口拼接完整下载地址 */
|
|
57
|
+
async function getDownloadInfo(pkgName, server, deviceId) {
|
|
58
|
+
const params = new URLSearchParams({
|
|
59
|
+
name: pkgName,
|
|
60
|
+
countryCode: server.countryCode,
|
|
61
|
+
type: server.type,
|
|
62
|
+
appMarketType: server.appMarketType,
|
|
63
|
+
offerType: server.offerType,
|
|
64
|
+
taskType: server.taskType,
|
|
65
|
+
version: ''
|
|
66
|
+
});
|
|
67
|
+
const url = `${server.cloudServerUrl}/api/v1/apk/package/V3?${params.toString()}`;
|
|
68
|
+
console.log(chalk.gray(`获取应用信息: ${url}`));
|
|
69
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
70
|
+
if (!res.ok) throw new Error(`云控接口返回 HTTP ${res.status}`);
|
|
71
|
+
const data = await res.json();
|
|
72
|
+
const info = data && data.data;
|
|
73
|
+
if (!info || !info.ftp_url) throw new Error(`未获取到下载地址,返回: ${JSON.stringify(data)}`);
|
|
74
|
+
|
|
75
|
+
// V3 返回的是相对路径(如 /cloudcontrol/prod/...),需调 ftpAccount/get 拿服务器 host 拼接(对应 downloadBigFile/getFilerResource)
|
|
76
|
+
if (!/^https?:\/\//i.test(info.ftp_url)) {
|
|
77
|
+
const sn = await getDeviceSn(deviceId);
|
|
78
|
+
const ftpUrl = `${server.cloudServerUrl}/api/v1/ftpAccount/get?platform=1&type=0&sn=${encodeURIComponent(sn)}`;
|
|
79
|
+
console.log(chalk.gray(`获取FTP服务器: ${ftpUrl}`));
|
|
80
|
+
const ftpRes = await fetch(ftpUrl, { signal: AbortSignal.timeout(30000) });
|
|
81
|
+
const ftpData = await ftpRes.json();
|
|
82
|
+
const host = ftpData && ftpData.data && ftpData.data.host;
|
|
83
|
+
if (!host) throw new Error(`未获取到FTP服务器地址,返回: ${JSON.stringify(ftpData)}`);
|
|
84
|
+
info.ftp_url = `http://${host}${info.ftp_url}`;
|
|
85
|
+
console.log(chalk.gray(`拼接下载地址: ${info.ftp_url}`));
|
|
86
|
+
}
|
|
87
|
+
return info;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ===================== 下载文件 =====================
|
|
91
|
+
|
|
92
|
+
async function downloadFile(url, destDir) {
|
|
93
|
+
if (!/^https?:\/\//.test(url)) {
|
|
94
|
+
const { direct } = await inquirer.prompt([{
|
|
95
|
+
type: 'input',
|
|
96
|
+
name: 'direct',
|
|
97
|
+
message: `下载地址不是直链(${url}),请粘贴直链URL:`,
|
|
98
|
+
validate: input => /^https?:\/\//.test(input.trim()) ? true : '请输入http(s)直链'
|
|
99
|
+
}]);
|
|
100
|
+
url = direct.trim();
|
|
101
|
+
}
|
|
102
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
const extMatch = url.match(/\.(\w+)$/);
|
|
105
|
+
const ext = extMatch ? extMatch[1].toLowerCase() : 'apk';
|
|
106
|
+
const filePath = path.join(destDir, `pkg.${ext}`);
|
|
107
|
+
|
|
108
|
+
console.log(chalk.gray(`开始下载: ${url}`));
|
|
109
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(300000) });
|
|
110
|
+
if (!res.ok) throw new Error(`下载失败,HTTP ${res.status}`);
|
|
111
|
+
|
|
112
|
+
// 流式下载并实时显示进度(百分比 + 速度)
|
|
113
|
+
const total = Number(res.headers.get('content-length')) || 0;
|
|
114
|
+
const chunks = [];
|
|
115
|
+
let received = 0;
|
|
116
|
+
let lastPct = -1;
|
|
117
|
+
const started = Date.now();
|
|
118
|
+
for await (const chunk of res.body) {
|
|
119
|
+
chunks.push(Buffer.from(chunk));
|
|
120
|
+
received += chunk.length;
|
|
121
|
+
if (total) {
|
|
122
|
+
const pct = Math.floor((received / total) * 100);
|
|
123
|
+
if (pct !== lastPct) {
|
|
124
|
+
lastPct = pct;
|
|
125
|
+
const speed = received / 1024 / 1024 / Math.max(1, (Date.now() - started) / 1000);
|
|
126
|
+
process.stdout.write(`\r下载进度: ${(received / 1024 / 1024).toFixed(1)}/${(total / 1024 / 1024).toFixed(1)} MB (${pct}%) ${speed.toFixed(1)}MB/s`);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
process.stdout.write(`\r已下载: ${(received / 1024 / 1024).toFixed(1)} MB`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
process.stdout.write('\n');
|
|
133
|
+
const buf = Buffer.concat(chunks);
|
|
134
|
+
fs.writeFileSync(filePath, buf);
|
|
135
|
+
console.log(chalk.green(`下载完成: ${filePath} (${(buf.length / 1024 / 1024).toFixed(2)} MB)`));
|
|
136
|
+
return { filePath, ext };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ===================== 安装并打开 =====================
|
|
140
|
+
|
|
141
|
+
async function installAndOpen(deviceId, pkgName, filePath, ext) {
|
|
142
|
+
// 关闭安装校验(对应原脚本 downloadUtil 的 verifier 设置)
|
|
143
|
+
await runAdbSync(deviceId, 'shell settings put global verifier_verify_adb_installs 0');
|
|
144
|
+
await runAdbSync(deviceId, 'shell settings put global package_verifier_enable 0');
|
|
145
|
+
|
|
146
|
+
if (ext === 'apk') {
|
|
147
|
+
// 直接安装(--streaming 实时显示 adb 安装进度)
|
|
148
|
+
await runAdbLive(deviceId, ['install', '--streaming', '-r', '-d', filePath]);
|
|
149
|
+
} else {
|
|
150
|
+
// apks/xapk/zip:解压后批量安装分包
|
|
151
|
+
console.log(chalk.gray('解压分包...'));
|
|
152
|
+
const tmpDir = path.join(path.dirname(filePath), 'unpack');
|
|
153
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
154
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
155
|
+
new AdmZip(filePath).extractAllTo(tmpDir, true);
|
|
156
|
+
|
|
157
|
+
const apkFiles = [];
|
|
158
|
+
const walk = (dir) => {
|
|
159
|
+
for (const f of fs.readdirSync(dir)) {
|
|
160
|
+
const full = path.join(dir, f);
|
|
161
|
+
if (fs.statSync(full).isDirectory()) walk(full);
|
|
162
|
+
else if (f.toLowerCase().endsWith('.apk')) apkFiles.push(full);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
walk(tmpDir);
|
|
166
|
+
if (apkFiles.length === 0) throw new Error('解压后未找到任何APK分包');
|
|
167
|
+
console.log(chalk.gray(`找到 ${apkFiles.length} 个APK分包,开始安装...`));
|
|
168
|
+
await runAdbLive(deviceId, ['install-multiple', '-r', '-d', ...apkFiles]);
|
|
169
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 推送副本到设备 /sdcard/Download(与删除功能清理路径一致,显示 push 进度)
|
|
173
|
+
await runAdbLive(deviceId, ['push', filePath, `/sdcard/Download/${pkgName}.${ext}`]);
|
|
174
|
+
|
|
175
|
+
// 打开应用
|
|
176
|
+
await runAdbSync(deviceId, `shell monkey -p ${pkgName} -c android.intent.category.LAUNCHER 1`);
|
|
177
|
+
console.log(chalk.green(`已安装并打开应用: ${pkgName}`));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
getServerConfig,
|
|
182
|
+
getDeviceSn,
|
|
183
|
+
getDownloadInfo,
|
|
184
|
+
downloadFile,
|
|
185
|
+
installAndOpen,
|
|
186
|
+
runAdbLive
|
|
187
|
+
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const chalk = require('chalk').default;
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { getDeviceList, selectDevice } = require('../lib/tools/adb.js');
|
|
7
|
+
const { getServerConfig, getDownloadInfo, downloadFile, installAndOpen } = require('../lib/common/appDownload.js');
|
|
8
|
+
|
|
9
|
+
async function runDownloadPackage(options = {}) {
|
|
10
|
+
const { device, pkg } = options;
|
|
11
|
+
try {
|
|
12
|
+
// 1. 设备选择
|
|
13
|
+
const allDevices = await getDeviceList();
|
|
14
|
+
const targetDevice = device || await selectDevice(allDevices);
|
|
15
|
+
|
|
16
|
+
// 2. 输入包名
|
|
17
|
+
let pkgName = pkg;
|
|
18
|
+
if (!pkgName) {
|
|
19
|
+
const ans = await inquirer.prompt([{
|
|
20
|
+
type: 'input',
|
|
21
|
+
name: 'pkgName',
|
|
22
|
+
message: '请输入要下载的包名:',
|
|
23
|
+
validate: input => input.trim() ? true : '包名不能为空'
|
|
24
|
+
}]);
|
|
25
|
+
pkgName = ans.pkgName;
|
|
26
|
+
}
|
|
27
|
+
pkgName = pkgName.trim();
|
|
28
|
+
|
|
29
|
+
// 3. 获取下载地址
|
|
30
|
+
const server = await getServerConfig();
|
|
31
|
+
const info = await getDownloadInfo(pkgName, server, targetDevice);
|
|
32
|
+
|
|
33
|
+
// 4. 下载
|
|
34
|
+
const destDir = path.resolve('./tmp_download');
|
|
35
|
+
const { filePath, ext } = await downloadFile(info.ftp_url, destDir);
|
|
36
|
+
|
|
37
|
+
// 5. 安装并打开
|
|
38
|
+
await installAndOpen(targetDevice, pkgName, filePath, ext);
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
|
|
41
|
+
console.log(chalk.bold.cyan('\n===== 下载安装完成 ====='));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.log(chalk.red(`\n程序异常: ${err.message}`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const downloadPackageCommand = new Command('download-pkg')
|
|
49
|
+
.description('下载并安装指定包(云控)')
|
|
50
|
+
.option('-d, --device <id>', '设备序列号(单设备时自动识别)')
|
|
51
|
+
.option('-p, --pkg <pkg>', '要下载的包名')
|
|
52
|
+
.action(async (options) => {
|
|
53
|
+
await runDownloadPackage(options);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
module.exports = downloadPackageCommand;
|
|
57
|
+
module.exports.run = runDownloadPackage;
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,8 @@ const inquirer = require('inquirer');
|
|
|
2
2
|
const chalk = require('chalk').default;
|
|
3
3
|
const { run: runExport } = require('./exportPackage');
|
|
4
4
|
const { run: runDelete } = require('./deletePackage');
|
|
5
|
+
const { run: runTestActivation } = require('./testActivation');
|
|
6
|
+
const { run: runDownloadPackage } = require('./downloadPackage');
|
|
5
7
|
async function showMenu() {
|
|
6
8
|
console.log('\n📦 APP包工具\n');
|
|
7
9
|
|
|
@@ -21,6 +23,16 @@ async function showMenu() {
|
|
|
21
23
|
description: '删除设备指定APP及文件',
|
|
22
24
|
value: 'delete'
|
|
23
25
|
},
|
|
26
|
+
{
|
|
27
|
+
name: '🔑 一键测试激活',
|
|
28
|
+
description: '处理激活链接并下载安装包',
|
|
29
|
+
value: 'testActivation'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: '📥 下载并安装指定包',
|
|
33
|
+
description: '从云控下载指定包并安装打开',
|
|
34
|
+
value: 'downloadPkg'
|
|
35
|
+
},
|
|
24
36
|
{
|
|
25
37
|
name: '❌ 退出',
|
|
26
38
|
value: 'exit'
|
|
@@ -35,7 +47,13 @@ async function showMenu() {
|
|
|
35
47
|
break;
|
|
36
48
|
case 'delete':
|
|
37
49
|
await runDelete();
|
|
38
|
-
break;
|
|
50
|
+
break;
|
|
51
|
+
case 'testActivation':
|
|
52
|
+
await runTestActivation();
|
|
53
|
+
break;
|
|
54
|
+
case 'downloadPkg':
|
|
55
|
+
await runDownloadPackage();
|
|
56
|
+
break;
|
|
39
57
|
case 'exit':
|
|
40
58
|
console.log(chalk.gray('已退出'));
|
|
41
59
|
process.exit(0);
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const chalk = require('chalk').default;
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { getDeviceList, selectDevice, runAdbSync } = require('../lib/tools/adb.js');
|
|
7
|
+
const { getServerConfig, getDownloadInfo, downloadFile, installAndOpen } = require('../lib/common/appDownload.js');
|
|
8
|
+
|
|
9
|
+
// ===================== 读取设备 GAID =====================
|
|
10
|
+
|
|
11
|
+
const AUTOJS_PKG_CANDIDATES = [
|
|
12
|
+
'org.autojs.autoxjs',
|
|
13
|
+
'org.autojs.autoxjs.v7',
|
|
14
|
+
'org.autojs.autox6',
|
|
15
|
+
'org.autojs.autox6.v6',
|
|
16
|
+
'org.autojs.autojs',
|
|
17
|
+
'com.stardust.autojs',
|
|
18
|
+
'com.cloudcontrol'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// 通过设备上的 Auto.js 运行时读取 GAID(对应 deviceUtil.getDeviceGaId)
|
|
22
|
+
async function getGaIdByAutoJs(deviceId) {
|
|
23
|
+
try {
|
|
24
|
+
const out = await runAdbSync(deviceId, 'shell pm list packages');
|
|
25
|
+
const pkgs = out.split(/\r?\n/)
|
|
26
|
+
.map(l => l.replace('package:', '').trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
const autoJsPkg = AUTOJS_PKG_CANDIDATES.find(p => pkgs.includes(p)) || pkgs.find(p => /autojs|autox/i.test(p));
|
|
29
|
+
if (!autoJsPkg) return null;
|
|
30
|
+
|
|
31
|
+
// 自包含脚本:调用 stardust SDK 的 requestGoogle 取 GAID 并写入结果文件
|
|
32
|
+
const script = [
|
|
33
|
+
'var adId = 0, status = 0;',
|
|
34
|
+
'for (var i = 0; i < 5; i++) {',
|
|
35
|
+
' try {',
|
|
36
|
+
' com.stardust.sdk.AutoJsApi.requestGoogle({ onCallback: function (gaid) { adId = gaid; status = 1; } });',
|
|
37
|
+
' } catch (e) { status = 1; }',
|
|
38
|
+
' var n = 0;',
|
|
39
|
+
' while (n < 20 && status === 0) { sleep(500); n++; }',
|
|
40
|
+
' if (adId !== 0) break;',
|
|
41
|
+
'}',
|
|
42
|
+
"files.write('/sdcard/Download/_gaid_result.txt', String(adId));"
|
|
43
|
+
].join('\n');
|
|
44
|
+
|
|
45
|
+
const localScript = path.resolve('./tmp_activation/_getGaid.js');
|
|
46
|
+
const dir = path.dirname(localScript);
|
|
47
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
fs.writeFileSync(localScript, script, 'utf-8');
|
|
49
|
+
await runAdbSync(deviceId, `push "${localScript}" /sdcard/Download/_getGaid.js`);
|
|
50
|
+
|
|
51
|
+
// 尝试多种方式触发 Auto.js 运行脚本(intent filter 要求 application/x-javascript 类型)
|
|
52
|
+
const components = [
|
|
53
|
+
`${autoJsPkg}/org.autojs.autojs.external.open.RunIntentActivity`,
|
|
54
|
+
`${autoJsPkg}/org.autojs.autojs.external.open.OpenIntentActivity`,
|
|
55
|
+
`${autoJsPkg}/com.stardust.autojs.external.open.RunIntentActivity`
|
|
56
|
+
];
|
|
57
|
+
let ran = false;
|
|
58
|
+
for (const comp of components) {
|
|
59
|
+
try {
|
|
60
|
+
await runAdbSync(deviceId, `shell am start -n ${comp} -d "file:///sdcard/Download/_getGaid.js" -t "application/x-javascript"`);
|
|
61
|
+
ran = true;
|
|
62
|
+
break;
|
|
63
|
+
} catch { }
|
|
64
|
+
}
|
|
65
|
+
if (!ran) {
|
|
66
|
+
// 兜底:隐式 VIEW 打开 .js(AutoX.js 注册了该 scheme)
|
|
67
|
+
try {
|
|
68
|
+
await runAdbSync(deviceId, 'shell am start -a android.intent.action.VIEW -d "file:///sdcard/Download/_getGaid.js" -t "application/x-javascript"');
|
|
69
|
+
ran = true;
|
|
70
|
+
} catch { }
|
|
71
|
+
}
|
|
72
|
+
if (!ran) return null;
|
|
73
|
+
|
|
74
|
+
// 轮询结果文件(脚本最多约 50s 重试,通常几秒内完成)
|
|
75
|
+
let id = '';
|
|
76
|
+
for (let i = 0; i < 20; i++) {
|
|
77
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
78
|
+
try {
|
|
79
|
+
const res = await runAdbSync(deviceId, 'shell cat /sdcard/Download/_gaid_result.txt');
|
|
80
|
+
const candidate = res.trim();
|
|
81
|
+
if (/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(candidate)) { id = candidate; break; }
|
|
82
|
+
} catch { }
|
|
83
|
+
}
|
|
84
|
+
await runAdbSync(deviceId, 'shell rm -f /sdcard/Download/_getGaid.js /sdcard/Download/_gaid_result.txt');
|
|
85
|
+
if (id) return id;
|
|
86
|
+
} catch { }
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function getGaId(deviceId) {
|
|
91
|
+
// 1. 设备 Auto.js 运行时读取(本生态正规方式)
|
|
92
|
+
const autoJsId = await getGaIdByAutoJs(deviceId);
|
|
93
|
+
if (autoJsId) {
|
|
94
|
+
console.log(chalk.gray(`读取到设备GAID: ${autoJsId}`));
|
|
95
|
+
return autoJsId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. 部分 Android 12+ ROM:cmd advertising_id
|
|
99
|
+
try {
|
|
100
|
+
const out = await runAdbSync(deviceId, 'shell cmd advertising_id get-id');
|
|
101
|
+
const id = (out.trim() || '').split(/\s+/).pop();
|
|
102
|
+
if (/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(id)) {
|
|
103
|
+
console.log(chalk.gray(`读取到设备GAID: ${id}`));
|
|
104
|
+
return id;
|
|
105
|
+
}
|
|
106
|
+
} catch { }
|
|
107
|
+
|
|
108
|
+
// 3. root 设备:读取 Google Play 服务配置里的 adid
|
|
109
|
+
try {
|
|
110
|
+
const out = await runAdbSync(deviceId, 'shell su -c "grep adid_key /data/data/com.google.android.gms/shared_prefs/adid_settings.xml"');
|
|
111
|
+
const m = out.match(/adid_key">([^<]+)</);
|
|
112
|
+
if (m) {
|
|
113
|
+
console.log(chalk.gray(`读取到设备GAID: ${m[1]}`));
|
|
114
|
+
return m[1];
|
|
115
|
+
}
|
|
116
|
+
} catch { }
|
|
117
|
+
|
|
118
|
+
// 4. 手动输入兜底
|
|
119
|
+
const { gaid } = await inquirer.prompt([{
|
|
120
|
+
type: 'input',
|
|
121
|
+
name: 'gaid',
|
|
122
|
+
message: '无法自动读取GAID,请手动输入设备GAID:',
|
|
123
|
+
validate: input => input.trim() ? true : 'GAID不能为空'
|
|
124
|
+
}]);
|
|
125
|
+
return gaid.trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ===================== 处理激活链接(移植 本地.js buildUrl) =====================
|
|
129
|
+
|
|
130
|
+
async function buildUrl(url, gaid) {
|
|
131
|
+
console.log(chalk.gray(`本机gaid: ${gaid}`));
|
|
132
|
+
|
|
133
|
+
// 通用:所有 {gaid} 占位 → 本机 gaid
|
|
134
|
+
url = url.replace(/\{gaid\}/g, gaid);
|
|
135
|
+
|
|
136
|
+
if (url.indexOf('aff_sub') === -1) {
|
|
137
|
+
// 第一种:movablead 型,aff_site_id 随机 4 位数
|
|
138
|
+
const affId = Math.floor(1000 + Math.random() * 9000);
|
|
139
|
+
url = url.replace(/aff_site_id=[^&]+/, 'aff_site_id=' + affId);
|
|
140
|
+
console.log(chalk.gray(`随机 aff_site_id: ${affId}`));
|
|
141
|
+
} else {
|
|
142
|
+
// 第二种:hotrk0 型,剩余 {xxx} 占位逐个询问是否添加
|
|
143
|
+
const remain = url.match(/\{[^}]+\}/g) || [];
|
|
144
|
+
for (const ph of remain) {
|
|
145
|
+
const name = ph.slice(1, -1);
|
|
146
|
+
const { want } = await inquirer.prompt([{
|
|
147
|
+
type: 'confirm',
|
|
148
|
+
name: 'want',
|
|
149
|
+
message: `链接中存在参数 {${name}},是否添加?`,
|
|
150
|
+
default: false
|
|
151
|
+
}]);
|
|
152
|
+
if (want) {
|
|
153
|
+
const { val } = await inquirer.prompt([{
|
|
154
|
+
type: 'input',
|
|
155
|
+
name: 'val',
|
|
156
|
+
message: `请输入 ${name} 的值:`
|
|
157
|
+
}]);
|
|
158
|
+
url = url.split(ph).join(val);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(chalk.gray(`处理后的链接: ${url}`));
|
|
164
|
+
return url;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ===================== 跳转判断(移植 本地.js openChromeClick) =====================
|
|
168
|
+
|
|
169
|
+
/** 轮询直到 checkFn 返回 true 或超时
|
|
170
|
+
* @param {number} count 次数
|
|
171
|
+
* @param {number} interval 间隔ms
|
|
172
|
+
*/
|
|
173
|
+
async function pollUntil(deviceId, count, interval, checkFn) {
|
|
174
|
+
for (let i = 0; i < count; i++) {
|
|
175
|
+
try {
|
|
176
|
+
if (await checkFn()) return true;
|
|
177
|
+
} catch { }
|
|
178
|
+
await new Promise(r => setTimeout(r, interval));
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 打开链接并判断是否跳转成功(目标包到前台)
|
|
184
|
+
* @param {string} url 链接
|
|
185
|
+
* @param {string} targetPkg 跳转目标包名,默认 com.android.vending 谷歌商城
|
|
186
|
+
* @returns {boolean} 是否跳转成功
|
|
187
|
+
*/
|
|
188
|
+
async function openChromeClick(deviceId, url, targetPkg) {
|
|
189
|
+
targetPkg = targetPkg || 'com.android.vending';
|
|
190
|
+
try {
|
|
191
|
+
// 先清掉 Chrome,避免已运行状态下 URL 落到地址栏编辑框而不导航
|
|
192
|
+
await runAdbSync(deviceId, 'shell am force-stop com.android.chrome');
|
|
193
|
+
await new Promise(r => setTimeout(r, 500));
|
|
194
|
+
|
|
195
|
+
// 与本地.js 一致:用 Chrome 打开。
|
|
196
|
+
// 关键:& 转义成 \&。Windows 下 cmd.exe 会先剥掉双引号,若 URL 里的 & 不做转义,
|
|
197
|
+
// 会被设备 shell 当成后台符,命令在第一个 & 处截断(offer_id 等参数丢失 → 广告提示不在线)。
|
|
198
|
+
const escapedUrl = url.replace(/&/g, '\\&');
|
|
199
|
+
const cmd = `shell am start -n com.android.chrome/com.google.android.apps.chrome.Main -a android.intent.action.VIEW -d "${escapedUrl}"`;
|
|
200
|
+
console.log(chalk.gray(`打开Chrome命令: ${cmd}`));
|
|
201
|
+
await runAdbSync(deviceId, cmd);
|
|
202
|
+
|
|
203
|
+
// 轮询:等待跳转到目标包(前台应用窗口包含目标包名)
|
|
204
|
+
// 注意:不依赖 host 端 grep(Windows 用户终端 PATH 可能没有 grep),
|
|
205
|
+
// 拉全量 dumpsys window 后在 Node 里过滤 mCurrentFocus 行,任何环境都可运行。
|
|
206
|
+
const jumped = await pollUntil(deviceId, 30, 1000, async () => {
|
|
207
|
+
const out = await runAdbSync(deviceId, 'shell dumpsys window');
|
|
208
|
+
const focusLine = (out.split(/\r?\n/) || []).find(l => l.includes('mCurrentFocus'));
|
|
209
|
+
console.log(chalk.gray(`当前前台应用: ${focusLine ? focusLine.trim() : 'null'}`));
|
|
210
|
+
return focusLine ? focusLine.includes(targetPkg) : false;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (jumped) {
|
|
214
|
+
console.log(chalk.green(`跳转成功,已进入目标: ${targetPkg}`));
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
console.log(chalk.yellow('超时未跳转'));
|
|
218
|
+
return false;
|
|
219
|
+
} catch (e) {
|
|
220
|
+
console.log(chalk.yellow(`跳转异常: ${e.message}`));
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ===================== 核心流程 =====================
|
|
226
|
+
|
|
227
|
+
async function runTestActivation(options = {}) {
|
|
228
|
+
const { device, url, pkg } = options;
|
|
229
|
+
try {
|
|
230
|
+
// 1. 设备选择
|
|
231
|
+
const allDevices = await getDeviceList();
|
|
232
|
+
const targetDevice = device || await selectDevice(allDevices);
|
|
233
|
+
|
|
234
|
+
// 2. 输入激活链接
|
|
235
|
+
let activationUrl = url;
|
|
236
|
+
if (!activationUrl) {
|
|
237
|
+
const ans = await inquirer.prompt([{
|
|
238
|
+
type: 'input',
|
|
239
|
+
name: 'activationUrl',
|
|
240
|
+
message: '请输入激活链接:',
|
|
241
|
+
validate: input => input.trim() ? true : '链接不能为空'
|
|
242
|
+
}]);
|
|
243
|
+
activationUrl = ans.activationUrl;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 3. 读取GAID并处理链接(替换参数)
|
|
247
|
+
const gaid = await getGaId(targetDevice);
|
|
248
|
+
const builtUrl = await buildUrl(activationUrl.trim(), gaid);
|
|
249
|
+
|
|
250
|
+
// 4. 循环 3 次打开链接并判断跳转是否成功
|
|
251
|
+
let jumpOkCount = 0;
|
|
252
|
+
for (let i = 1; i <= 3; i++) {
|
|
253
|
+
console.log(chalk.gray(`\n===== 第 ${i}/3 次跳转开始 =====`));
|
|
254
|
+
const ok = await openChromeClick(targetDevice, builtUrl);
|
|
255
|
+
if (ok) {
|
|
256
|
+
jumpOkCount++;
|
|
257
|
+
console.log(chalk.green(`第 ${i}/3 次跳转成功`));
|
|
258
|
+
} else {
|
|
259
|
+
console.log(chalk.yellow(`第 ${i}/3 次跳转失败`));
|
|
260
|
+
}
|
|
261
|
+
if (i < 3) {
|
|
262
|
+
await new Promise(r => setTimeout(r, 3000 + Math.random() * 2000)); // 两次之间停 3~5 秒
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
console.log(chalk.gray(`\n===== 3 次跳转执行完毕,成功 ${jumpOkCount} 次 =====`));
|
|
266
|
+
|
|
267
|
+
// 5. 跳转未成功则显示失败,不再进行后续步骤
|
|
268
|
+
if (jumpOkCount === 0) {
|
|
269
|
+
console.log(chalk.red('\n❌ 激活失败:3 次跳转均未成功,不再进行后续操作'));
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 6. 询问下载的包
|
|
274
|
+
let pkgName = pkg;
|
|
275
|
+
if (!pkgName) {
|
|
276
|
+
const ans = await inquirer.prompt([{
|
|
277
|
+
type: 'input',
|
|
278
|
+
name: 'pkgName',
|
|
279
|
+
message: '请输入要下载的包名:',
|
|
280
|
+
validate: input => input.trim() ? true : '包名不能为空'
|
|
281
|
+
}]);
|
|
282
|
+
pkgName = ans.pkgName;
|
|
283
|
+
}
|
|
284
|
+
pkgName = pkgName.trim();
|
|
285
|
+
|
|
286
|
+
// 7. 从云控获取下载地址并下载
|
|
287
|
+
const server = await getServerConfig();
|
|
288
|
+
const info = await getDownloadInfo(pkgName, server, targetDevice);
|
|
289
|
+
const destDir = path.resolve('./tmp_activation');
|
|
290
|
+
const { filePath, ext } = await downloadFile(info.ftp_url, destDir);
|
|
291
|
+
|
|
292
|
+
// 8. 安装并打开
|
|
293
|
+
await installAndOpen(targetDevice, pkgName, filePath, ext);
|
|
294
|
+
|
|
295
|
+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
|
|
296
|
+
console.log(chalk.bold.cyan('\n===== 一键测试激活完成 ====='));
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.log(chalk.red(`\n程序异常: ${err.message}`));
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ===================== Commander 命令定义 =====================
|
|
304
|
+
|
|
305
|
+
const testActivationCommand = new Command('test-activation')
|
|
306
|
+
.description('一键测试激活:处理激活链接并下载安装包')
|
|
307
|
+
.option('-d, --device <id>', '设备序列号(单设备时自动识别)')
|
|
308
|
+
.option('-u, --url <url>', '激活链接')
|
|
309
|
+
.option('-p, --pkg <pkg>', '要下载的包名')
|
|
310
|
+
.action(async (options) => {
|
|
311
|
+
await runTestActivation(options);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
module.exports = testActivationCommand;
|
|
315
|
+
module.exports.run = runTestActivation;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
var adId = 0, status = 0;
|
|
2
|
+
for (var i = 0; i < 5; i++) {
|
|
3
|
+
try {
|
|
4
|
+
com.stardust.sdk.AutoJsApi.requestGoogle({ onCallback: function (gaid) { adId = gaid; status = 1; } });
|
|
5
|
+
} catch (e) { status = 1; }
|
|
6
|
+
var n = 0;
|
|
7
|
+
while (n < 20 && status === 0) { sleep(500); n++; }
|
|
8
|
+
if (adId !== 0) break;
|
|
9
|
+
}
|
|
10
|
+
files.write('/sdcard/Download/_gaid_result.txt', String(adId));
|