apkpub-cli 0.0.1-alpha.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/CHANGELOG.md +27 -0
- package/LICENSE +201 -0
- package/NOTICE +10 -0
- package/README.md +250 -0
- package/dist/apkpub.js +2527 -0
- package/package.json +89 -0
package/dist/apkpub.js
ADDED
|
@@ -0,0 +1,2527 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/apkpub.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/init.ts
|
|
7
|
+
import { input, confirm, checkbox } from "@inquirer/prompts";
|
|
8
|
+
|
|
9
|
+
// src/config/store.ts
|
|
10
|
+
import { mkdir, readFile, readdir, writeFile, chmod, access } from "fs/promises";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import os from "os";
|
|
13
|
+
|
|
14
|
+
// src/errors/ApkpubError.ts
|
|
15
|
+
var ApkpubError = class extends Error {
|
|
16
|
+
code;
|
|
17
|
+
channel;
|
|
18
|
+
step;
|
|
19
|
+
retryable;
|
|
20
|
+
cause;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
super(options.message);
|
|
23
|
+
this.name = "ApkpubError";
|
|
24
|
+
this.code = options.code;
|
|
25
|
+
this.channel = options.channel;
|
|
26
|
+
this.step = options.step;
|
|
27
|
+
this.retryable = options.retryable ?? isRetryableCode(options.code);
|
|
28
|
+
this.cause = options.cause;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function isRetryableCode(code) {
|
|
32
|
+
return ["NETWORK_ERROR" /* NETWORK_ERROR */, "TIMEOUT" /* TIMEOUT */, "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */].includes(code);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/config/schema.ts
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
var CURRENT_SCHEMA_VERSION = 1;
|
|
38
|
+
var channelParamSchema = z.object({
|
|
39
|
+
name: z.string(),
|
|
40
|
+
value: z.string().default("")
|
|
41
|
+
});
|
|
42
|
+
var marketChannelConfigSchema = z.object({
|
|
43
|
+
name: z.string(),
|
|
44
|
+
type: z.literal("market").default("market"),
|
|
45
|
+
enable: z.boolean().default(true),
|
|
46
|
+
params: z.array(channelParamSchema).default([])
|
|
47
|
+
});
|
|
48
|
+
var ossAuthSchema = z.discriminatedUnion("mode", [
|
|
49
|
+
z.object({
|
|
50
|
+
mode: z.literal("ak"),
|
|
51
|
+
accessKeyId: z.string(),
|
|
52
|
+
accessKeySecret: z.string()
|
|
53
|
+
}),
|
|
54
|
+
z.object({
|
|
55
|
+
mode: z.literal("sts"),
|
|
56
|
+
stsTokenUrl: z.string().url(),
|
|
57
|
+
signKey: z.string(),
|
|
58
|
+
contextB: z.string().default("{}")
|
|
59
|
+
})
|
|
60
|
+
]);
|
|
61
|
+
var customChannelConfigSchema = z.object({
|
|
62
|
+
name: z.string(),
|
|
63
|
+
type: z.literal("custom"),
|
|
64
|
+
enable: z.boolean().default(true),
|
|
65
|
+
uploadType: z.enum(["oss", "http"]),
|
|
66
|
+
fileNameIdentify: z.string().optional(),
|
|
67
|
+
endpoint: z.string().optional(),
|
|
68
|
+
bucket: z.string().optional(),
|
|
69
|
+
auth: ossAuthSchema.optional(),
|
|
70
|
+
uploadUrl: z.string().optional(),
|
|
71
|
+
method: z.enum(["PUT", "POST"]).optional(),
|
|
72
|
+
headers: z.record(z.string()).optional(),
|
|
73
|
+
formField: z.string().optional(),
|
|
74
|
+
objectKeyTemplate: z.string(),
|
|
75
|
+
downloadUrlTemplate: z.string(),
|
|
76
|
+
params: z.array(channelParamSchema).default([])
|
|
77
|
+
});
|
|
78
|
+
var channelConfigSchema = z.union([marketChannelConfigSchema, customChannelConfigSchema]);
|
|
79
|
+
var extensionSchema = z.object({
|
|
80
|
+
updateDesc: z.string().optional(),
|
|
81
|
+
apkDir: z.string().optional(),
|
|
82
|
+
urls: z.record(z.string()).optional(),
|
|
83
|
+
lastVersionCode: z.record(z.number()).optional(),
|
|
84
|
+
lastVersionName: z.record(z.string()).optional()
|
|
85
|
+
});
|
|
86
|
+
var appConfigSchema = z.object({
|
|
87
|
+
schemaVersion: z.number().default(CURRENT_SCHEMA_VERSION),
|
|
88
|
+
name: z.string(),
|
|
89
|
+
applicationId: z.string(),
|
|
90
|
+
createTime: z.number(),
|
|
91
|
+
enableChannel: z.boolean().default(true),
|
|
92
|
+
channels: z.array(channelConfigSchema).default([]),
|
|
93
|
+
extension: extensionSchema.default({})
|
|
94
|
+
});
|
|
95
|
+
function paramsToRecord(params) {
|
|
96
|
+
const record = {};
|
|
97
|
+
for (const p of params) {
|
|
98
|
+
record[p.name] = p.value;
|
|
99
|
+
}
|
|
100
|
+
return record;
|
|
101
|
+
}
|
|
102
|
+
function migrateConfig(raw) {
|
|
103
|
+
const obj = raw;
|
|
104
|
+
if (!obj.schemaVersion) {
|
|
105
|
+
obj.schemaVersion = 1;
|
|
106
|
+
}
|
|
107
|
+
return appConfigSchema.parse(obj);
|
|
108
|
+
}
|
|
109
|
+
function stripSecrets(config) {
|
|
110
|
+
const sensitiveKeys = [
|
|
111
|
+
"client_secret",
|
|
112
|
+
"privateKey",
|
|
113
|
+
"access_key_secret",
|
|
114
|
+
"accessKeySecret",
|
|
115
|
+
"signKey",
|
|
116
|
+
"password"
|
|
117
|
+
];
|
|
118
|
+
const stripped = structuredClone(config);
|
|
119
|
+
for (const ch of stripped.channels) {
|
|
120
|
+
if ("params" in ch) {
|
|
121
|
+
for (const p of ch.params) {
|
|
122
|
+
if (sensitiveKeys.some((k) => p.name.toLowerCase().includes(k.toLowerCase()))) {
|
|
123
|
+
p.value = "";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (ch.type === "custom" && ch.auth) {
|
|
128
|
+
if (ch.auth.mode === "ak") {
|
|
129
|
+
ch.auth.accessKeyId = "";
|
|
130
|
+
ch.auth.accessKeySecret = "";
|
|
131
|
+
} else {
|
|
132
|
+
ch.auth.signKey = "";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return stripped;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/utils/logger.ts
|
|
140
|
+
import pc from "picocolors";
|
|
141
|
+
|
|
142
|
+
// src/utils/redaction.ts
|
|
143
|
+
function redactMessage(message) {
|
|
144
|
+
return message.replace(/(client_secret|access_key_secret|privateKey|password|signKey)=[^&\s]+/gi, "$1=***").replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer ***");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/utils/logger.ts
|
|
148
|
+
var jsonMode = false;
|
|
149
|
+
var debugMode = false;
|
|
150
|
+
function setJsonMode(enabled) {
|
|
151
|
+
jsonMode = enabled;
|
|
152
|
+
}
|
|
153
|
+
function formatMessage(level, tag, message) {
|
|
154
|
+
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
155
|
+
const levelColors = {
|
|
156
|
+
debug: pc.gray,
|
|
157
|
+
info: pc.blue,
|
|
158
|
+
warn: pc.yellow,
|
|
159
|
+
error: pc.red
|
|
160
|
+
};
|
|
161
|
+
return `${pc.dim(time)} ${levelColors[level](level.toUpperCase())} [${tag}] ${redactMessage(message)}`;
|
|
162
|
+
}
|
|
163
|
+
function log(level, tag, message) {
|
|
164
|
+
if (jsonMode && level !== "error") return;
|
|
165
|
+
if (level === "debug" && !debugMode) return;
|
|
166
|
+
const stream = level === "error" ? process.stderr : process.stderr;
|
|
167
|
+
stream.write(formatMessage(level, tag, message) + "\n");
|
|
168
|
+
}
|
|
169
|
+
var logger = {
|
|
170
|
+
debug: (tag, message) => log("debug", tag, message),
|
|
171
|
+
info: (tag, message) => log("info", tag, message),
|
|
172
|
+
warn: (tag, message) => log("warn", tag, message),
|
|
173
|
+
error: (tag, message) => log("error", tag, message)
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/config/store.ts
|
|
177
|
+
var CONFIG_DIR_NAME = ".apkpub";
|
|
178
|
+
function getConfigRoot(debug = false) {
|
|
179
|
+
const base = path.join(os.homedir(), CONFIG_DIR_NAME);
|
|
180
|
+
return debug ? path.join(base, "debug") : base;
|
|
181
|
+
}
|
|
182
|
+
function getAppsDir(debug = false) {
|
|
183
|
+
return path.join(getConfigRoot(debug), "apps");
|
|
184
|
+
}
|
|
185
|
+
function getLogsDir(debug = false) {
|
|
186
|
+
return path.join(getConfigRoot(debug), "logs");
|
|
187
|
+
}
|
|
188
|
+
async function ensureConfigDirs(debug = false) {
|
|
189
|
+
const root = getConfigRoot(debug);
|
|
190
|
+
const apps = getAppsDir(debug);
|
|
191
|
+
const logs = getLogsDir(debug);
|
|
192
|
+
await mkdir(apps, { recursive: true, mode: 448 });
|
|
193
|
+
await mkdir(logs, { recursive: true, mode: 448 });
|
|
194
|
+
try {
|
|
195
|
+
await chmod(root, 448);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
await checkPermissions(root);
|
|
199
|
+
}
|
|
200
|
+
async function checkPermissions(dir) {
|
|
201
|
+
try {
|
|
202
|
+
const { statSync } = await import("fs");
|
|
203
|
+
const mode = statSync(dir).mode & 511;
|
|
204
|
+
if (mode > 448) {
|
|
205
|
+
logger.warn("config", `\u914D\u7F6E\u76EE\u5F55\u6743\u9650\u8FC7\u5BBD (${mode.toString(8)})\uFF0C\u5EFA\u8BAE chmod 700 ${dir}`);
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function configPath(applicationId, debug = false) {
|
|
211
|
+
const safe = applicationId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
212
|
+
return path.join(getAppsDir(debug), `${safe}.json`);
|
|
213
|
+
}
|
|
214
|
+
async function saveConfig(config, debug = false) {
|
|
215
|
+
await ensureConfigDirs(debug);
|
|
216
|
+
const filePath = configPath(config.applicationId, debug);
|
|
217
|
+
const content = JSON.stringify(config, null, 2);
|
|
218
|
+
await writeFile(filePath, content, { mode: 384 });
|
|
219
|
+
logger.debug("config", `\u5DF2\u4FDD\u5B58\u914D\u7F6E: ${filePath}`);
|
|
220
|
+
}
|
|
221
|
+
async function loadConfig(applicationId, debug = false) {
|
|
222
|
+
const filePath = configPath(applicationId, debug);
|
|
223
|
+
try {
|
|
224
|
+
await access(filePath);
|
|
225
|
+
} catch {
|
|
226
|
+
throw new ApkpubError({
|
|
227
|
+
code: "CONFIG_NOT_FOUND" /* CONFIG_NOT_FOUND */,
|
|
228
|
+
message: `\u672A\u627E\u5230\u5E94\u7528\u914D\u7F6E: ${applicationId}`,
|
|
229
|
+
retryable: false
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const content = await readFile(filePath, "utf8");
|
|
233
|
+
const raw = JSON.parse(content);
|
|
234
|
+
return migrateConfig(raw);
|
|
235
|
+
}
|
|
236
|
+
async function listConfigs(debug = false) {
|
|
237
|
+
await ensureConfigDirs(debug);
|
|
238
|
+
const dir = getAppsDir(debug);
|
|
239
|
+
let files;
|
|
240
|
+
try {
|
|
241
|
+
files = await readdir(dir);
|
|
242
|
+
} catch {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const configs = [];
|
|
246
|
+
for (const file of files) {
|
|
247
|
+
if (!file.endsWith(".json")) continue;
|
|
248
|
+
try {
|
|
249
|
+
const content = await readFile(path.join(dir, file), "utf8");
|
|
250
|
+
configs.push(migrateConfig(JSON.parse(content)));
|
|
251
|
+
} catch (err) {
|
|
252
|
+
logger.warn("config", `\u8DF3\u8FC7\u65E0\u6548\u914D\u7F6E ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return configs;
|
|
256
|
+
}
|
|
257
|
+
async function importConfig(filePath, debug = false) {
|
|
258
|
+
const content = await readFile(filePath, "utf8");
|
|
259
|
+
const raw = JSON.parse(content);
|
|
260
|
+
const config = migrateConfig(raw);
|
|
261
|
+
await saveConfig(config, debug);
|
|
262
|
+
return config;
|
|
263
|
+
}
|
|
264
|
+
async function exportConfig(applicationId, options = {}) {
|
|
265
|
+
const config = await loadConfig(applicationId, options.debug);
|
|
266
|
+
const output = options.includeSecrets ? config : stripSecrets(config);
|
|
267
|
+
return JSON.stringify(output, null, 2);
|
|
268
|
+
}
|
|
269
|
+
async function writeAuditLog(entry, debug = false) {
|
|
270
|
+
await ensureConfigDirs(debug);
|
|
271
|
+
const logFile = path.join(getLogsDir(debug), "audit.log");
|
|
272
|
+
const line = JSON.stringify({ ...entry, timestamp: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
|
|
273
|
+
const { appendFile } = await import("fs/promises");
|
|
274
|
+
await appendFile(logFile, line, { mode: 384 });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/channels/huawei/index.ts
|
|
278
|
+
import { z as z2 } from "zod";
|
|
279
|
+
import { createReadStream } from "fs";
|
|
280
|
+
import path2 from "path";
|
|
281
|
+
|
|
282
|
+
// src/utils/http.ts
|
|
283
|
+
import axios from "axios";
|
|
284
|
+
var PRIVATE_IP_PATTERNS = [
|
|
285
|
+
/^localhost$/i,
|
|
286
|
+
/^127\./,
|
|
287
|
+
/^10\./,
|
|
288
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
289
|
+
/^192\.168\./,
|
|
290
|
+
/^0\.0\.0\.0$/,
|
|
291
|
+
/^\[::1\]$/,
|
|
292
|
+
/^169\.254\./
|
|
293
|
+
];
|
|
294
|
+
function assertSafeUrl(url, label = "URL") {
|
|
295
|
+
let parsed;
|
|
296
|
+
try {
|
|
297
|
+
parsed = new URL(url);
|
|
298
|
+
} catch {
|
|
299
|
+
throw new ApkpubError({
|
|
300
|
+
code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
|
|
301
|
+
message: `\u65E0\u6548\u7684 ${label}: ${url}`,
|
|
302
|
+
retryable: false
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
306
|
+
throw new ApkpubError({
|
|
307
|
+
code: "SSRF_BLOCKED" /* SSRF_BLOCKED */,
|
|
308
|
+
message: `${label} \u4EC5\u652F\u6301 http/https \u534F\u8BAE`,
|
|
309
|
+
retryable: false
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const host = parsed.hostname;
|
|
313
|
+
if (PRIVATE_IP_PATTERNS.some((p) => p.test(host))) {
|
|
314
|
+
throw new ApkpubError({
|
|
315
|
+
code: "SSRF_BLOCKED" /* SSRF_BLOCKED */,
|
|
316
|
+
message: `${label} \u4E0D\u5141\u8BB8\u6307\u5411\u5185\u7F51\u5730\u5740: ${host}`,
|
|
317
|
+
retryable: false
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function createHttpClient(options) {
|
|
322
|
+
return axios.create({
|
|
323
|
+
timeout: options?.timeout ?? 6e4,
|
|
324
|
+
validateStatus: () => true
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function withRetry(fn, options = {}) {
|
|
328
|
+
const retries = options.retries ?? 3;
|
|
329
|
+
const delayMs = options.delayMs ?? 1e3;
|
|
330
|
+
let lastError;
|
|
331
|
+
for (let i = 0; i <= retries; i++) {
|
|
332
|
+
if (options.signal?.aborted) {
|
|
333
|
+
throw new ApkpubError({ code: "TIMEOUT" /* TIMEOUT */, message: "\u64CD\u4F5C\u5DF2\u53D6\u6D88", retryable: false });
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
return await fn();
|
|
337
|
+
} catch (err) {
|
|
338
|
+
lastError = err;
|
|
339
|
+
const retryable = err instanceof ApkpubError ? err.retryable : true;
|
|
340
|
+
if (!retryable || i === retries) break;
|
|
341
|
+
await sleep(delayMs * Math.pow(2, i), options.signal);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
throw lastError;
|
|
345
|
+
}
|
|
346
|
+
function sleep(ms, signal) {
|
|
347
|
+
return new Promise((resolve, reject) => {
|
|
348
|
+
const timer = setTimeout(resolve, ms);
|
|
349
|
+
signal?.addEventListener("abort", () => {
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
reject(new ApkpubError({ code: "TIMEOUT" /* TIMEOUT */, message: "\u64CD\u4F5C\u5DF2\u53D6\u6D88", retryable: false }));
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/channels/huawei/index.ts
|
|
357
|
+
var BASE_URL = "https://connect-api.cloud.huawei.com";
|
|
358
|
+
var credSchema = z2.object({
|
|
359
|
+
client_id: z2.string().min(1),
|
|
360
|
+
client_secret: z2.string().min(1)
|
|
361
|
+
});
|
|
362
|
+
async function getToken(clientId, clientSecret) {
|
|
363
|
+
const client = createHttpClient();
|
|
364
|
+
const resp = await client.post(`${BASE_URL}/api/oauth2/v1/token`, {
|
|
365
|
+
client_id: clientId,
|
|
366
|
+
client_secret: clientSecret,
|
|
367
|
+
grant_type: "client_credentials"
|
|
368
|
+
});
|
|
369
|
+
if (resp.data?.access_token) return resp.data.access_token;
|
|
370
|
+
throw new ApkpubError({
|
|
371
|
+
code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */,
|
|
372
|
+
channel: "huawei",
|
|
373
|
+
step: "getToken",
|
|
374
|
+
message: `\u83B7\u53D6 token \u5931\u8D25: ${JSON.stringify(resp.data)}`,
|
|
375
|
+
retryable: false
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async function getAppId(clientId, token, packageName) {
|
|
379
|
+
const client = createHttpClient();
|
|
380
|
+
const resp = await client.get(`${BASE_URL}/api/publish/v2/appid-list`, {
|
|
381
|
+
headers: { client_id: clientId, Authorization: `Bearer ${token}` },
|
|
382
|
+
params: { packageName }
|
|
383
|
+
});
|
|
384
|
+
const list = resp.data?.appids ?? resp.data?.list ?? [];
|
|
385
|
+
if (!list.length) {
|
|
386
|
+
throw new ApkpubError({
|
|
387
|
+
code: "CHANNEL_STATE_FAILED" /* CHANNEL_STATE_FAILED */,
|
|
388
|
+
channel: "huawei",
|
|
389
|
+
step: "getAppId",
|
|
390
|
+
message: `\u672A\u627E\u5230\u5305\u540D ${packageName} \u5BF9\u5E94\u7684\u5E94\u7528`,
|
|
391
|
+
retryable: false
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return list[0].value ?? list[0].id ?? list[0].appId;
|
|
395
|
+
}
|
|
396
|
+
async function getUploadUrl(clientId, token, appId, fileName, contentLength) {
|
|
397
|
+
const client = createHttpClient();
|
|
398
|
+
const resp = await client.get(`${BASE_URL}/api/publish/v2/upload-url/for-obs`, {
|
|
399
|
+
headers: { client_id: clientId, Authorization: `Bearer ${token}` },
|
|
400
|
+
params: { appId, fileName, contentLength }
|
|
401
|
+
});
|
|
402
|
+
const urlInfo = resp.data?.urlInfo ?? resp.data?.url;
|
|
403
|
+
if (!urlInfo?.url) {
|
|
404
|
+
throw new ApkpubError({
|
|
405
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
406
|
+
channel: "huawei",
|
|
407
|
+
step: "getUploadUrl",
|
|
408
|
+
message: `\u83B7\u53D6\u4E0A\u4F20\u5730\u5740\u5931\u8D25: ${JSON.stringify(resp.data)}`,
|
|
409
|
+
retryable: true
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const headers = {};
|
|
413
|
+
if (urlInfo.headers) {
|
|
414
|
+
for (const h of urlInfo.headers) {
|
|
415
|
+
headers[h.key ?? h.name] = h.value;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { url: urlInfo.url, objectId: urlInfo.objectId, headers };
|
|
419
|
+
}
|
|
420
|
+
async function uploadFile(uploadUrl, headers, filePath, onProgress) {
|
|
421
|
+
const client = createHttpClient({ timeout: 6e5 });
|
|
422
|
+
const stream = createReadStream(filePath);
|
|
423
|
+
const resp = await client.put(uploadUrl, stream, {
|
|
424
|
+
headers: { ...headers, "Content-Type": "application/octet-stream" },
|
|
425
|
+
maxBodyLength: Infinity,
|
|
426
|
+
onUploadProgress: (e) => {
|
|
427
|
+
if (e.total) onProgress(Math.round(e.loaded / e.total * 100));
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
431
|
+
throw new ApkpubError({
|
|
432
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
433
|
+
channel: "huawei",
|
|
434
|
+
step: "uploadFile",
|
|
435
|
+
message: `\u4E0A\u4F20\u5931\u8D25 HTTP ${resp.status}`,
|
|
436
|
+
retryable: resp.status >= 500
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function bindApk(clientId, token, appId, fileName, objectId) {
|
|
441
|
+
const client = createHttpClient();
|
|
442
|
+
const resp = await client.put(
|
|
443
|
+
`${BASE_URL}/api/publish/v2/app-file-info`,
|
|
444
|
+
{ fileType: 5, files: [{ fileName, fileDestUrl: objectId }] },
|
|
445
|
+
{ headers: { client_id: clientId, Authorization: `Bearer ${token}` }, params: { appId } }
|
|
446
|
+
);
|
|
447
|
+
const pkgId = resp.data?.pkgVersion?.[0]?.pkgId ?? resp.data?.pkgId;
|
|
448
|
+
if (!pkgId) {
|
|
449
|
+
throw new ApkpubError({
|
|
450
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
451
|
+
channel: "huawei",
|
|
452
|
+
step: "bindApk",
|
|
453
|
+
message: `\u7ED1\u5B9A APK \u5931\u8D25: ${JSON.stringify(resp.data)}`,
|
|
454
|
+
retryable: false
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return { pkgId };
|
|
458
|
+
}
|
|
459
|
+
async function waitApkReady(clientId, token, appId, pkgId, signal) {
|
|
460
|
+
const client = createHttpClient();
|
|
461
|
+
const start = Date.now();
|
|
462
|
+
const timeout = 3 * 60 * 1e3;
|
|
463
|
+
while (Date.now() - start < timeout) {
|
|
464
|
+
if (signal.aborted) throw new ApkpubError({ code: "TIMEOUT" /* TIMEOUT */, message: "\u7B49\u5F85\u7F16\u8BD1\u8D85\u65F6", retryable: false });
|
|
465
|
+
await new Promise((r) => setTimeout(r, 1e4));
|
|
466
|
+
const resp = await client.get(`${BASE_URL}/api/publish/v2/package/compile/status`, {
|
|
467
|
+
headers: { client_id: clientId, Authorization: `Bearer ${token}` },
|
|
468
|
+
params: { appId, pkgIds: pkgId }
|
|
469
|
+
});
|
|
470
|
+
const state = resp.data?.pkgStateList?.[0]?.pkgState ?? resp.data?.compileStatus;
|
|
471
|
+
if (state === 0 || state === "COMPILE_SUCCESS") return;
|
|
472
|
+
}
|
|
473
|
+
throw new ApkpubError({
|
|
474
|
+
code: "TIMEOUT" /* TIMEOUT */,
|
|
475
|
+
channel: "huawei",
|
|
476
|
+
step: "waitApkReady",
|
|
477
|
+
message: "\u7B49\u5F85 APK \u7F16\u8BD1\u8D85\u65F6",
|
|
478
|
+
retryable: true
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
var huaweiChannel = {
|
|
482
|
+
name: "huawei",
|
|
483
|
+
label: "\u534E\u4E3A",
|
|
484
|
+
type: "market",
|
|
485
|
+
fileNameIdentify: "HUAWEI",
|
|
486
|
+
credentialSchema: credSchema,
|
|
487
|
+
async getMarketState(appId, config) {
|
|
488
|
+
const creds = credSchema.parse(config);
|
|
489
|
+
const token = await getToken(creds.client_id, creds.client_secret);
|
|
490
|
+
const hwAppId = await getAppId(creds.client_id, token, appId);
|
|
491
|
+
const client = createHttpClient();
|
|
492
|
+
const resp = await client.get(`${BASE_URL}/api/publish/v2/app-info`, {
|
|
493
|
+
headers: { client_id: creds.client_id, Authorization: `Bearer ${token}` },
|
|
494
|
+
params: { appId: hwAppId }
|
|
495
|
+
});
|
|
496
|
+
const info = resp.data?.appInfo ?? resp.data;
|
|
497
|
+
const reviewState = mapReviewState(info?.releaseState ?? info?.status);
|
|
498
|
+
return {
|
|
499
|
+
reviewState,
|
|
500
|
+
enableSubmit: reviewState === "online" || reviewState === "rejected",
|
|
501
|
+
lastVersionCode: Number(info?.versionCode ?? 0),
|
|
502
|
+
lastVersionName: String(info?.versionNumber ?? info?.onShelfVersionNumber ?? info?.versionName ?? "0")
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
async validateCredentials(config) {
|
|
506
|
+
const creds = credSchema.parse(config);
|
|
507
|
+
await getToken(creds.client_id, creds.client_secret);
|
|
508
|
+
},
|
|
509
|
+
async upload(ctx) {
|
|
510
|
+
const creds = credSchema.parse(ctx.config);
|
|
511
|
+
const fileName = path2.basename(ctx.filePath);
|
|
512
|
+
ctx.onProgress({ step: "getToken" });
|
|
513
|
+
const token = await withRetry(() => getToken(creds.client_id, creds.client_secret));
|
|
514
|
+
ctx.onProgress({ step: "getAppId" });
|
|
515
|
+
const appId = await getAppId(creds.client_id, token, ctx.apkInfo.applicationId);
|
|
516
|
+
ctx.onProgress({ step: "getUploadUrl" });
|
|
517
|
+
const uploadInfo = await getUploadUrl(creds.client_id, token, appId, fileName, ctx.apkInfo.size);
|
|
518
|
+
ctx.onProgress({ step: "uploading", percent: 0 });
|
|
519
|
+
await uploadFile(
|
|
520
|
+
uploadInfo.url,
|
|
521
|
+
uploadInfo.headers,
|
|
522
|
+
ctx.filePath,
|
|
523
|
+
(p) => ctx.onProgress({ step: "uploading", percent: p })
|
|
524
|
+
);
|
|
525
|
+
ctx.onProgress({ step: "bindApk" });
|
|
526
|
+
const bind = await bindApk(creds.client_id, token, appId, fileName, uploadInfo.objectId);
|
|
527
|
+
ctx.onProgress({ step: "waitCompile" });
|
|
528
|
+
await waitApkReady(creds.client_id, token, appId, bind.pkgId, ctx.signal);
|
|
529
|
+
ctx.onProgress({ step: "updateDesc" });
|
|
530
|
+
const client = createHttpClient();
|
|
531
|
+
await client.put(
|
|
532
|
+
`${BASE_URL}/api/publish/v2/app-language-info`,
|
|
533
|
+
{ lang: "zh-CN", appDesc: ctx.desc },
|
|
534
|
+
{ headers: { client_id: creds.client_id, Authorization: `Bearer ${token}` }, params: { appId } }
|
|
535
|
+
);
|
|
536
|
+
ctx.onProgress({ step: "submit" });
|
|
537
|
+
await client.post(`${BASE_URL}/api/publish/v2/app-submit`, null, {
|
|
538
|
+
headers: { client_id: creds.client_id, Authorization: `Bearer ${token}` },
|
|
539
|
+
params: { appId }
|
|
540
|
+
});
|
|
541
|
+
ctx.onProgress({ step: "done", percent: 100 });
|
|
542
|
+
return { message: "\u63D0\u4EA4\u5BA1\u6838\u6210\u529F" };
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
function mapReviewState(state) {
|
|
546
|
+
const s = Number(state);
|
|
547
|
+
if (s === 0 || s === 7) return "online";
|
|
548
|
+
if (s === 4 || s === 5) return "reviewing";
|
|
549
|
+
if (s === 1 || s === 8 || s === 9) return "rejected";
|
|
550
|
+
return "unknown";
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/channels/honor/index.ts
|
|
554
|
+
import { z as z3 } from "zod";
|
|
555
|
+
import { createReadStream as createReadStream3 } from "fs";
|
|
556
|
+
import path4 from "path";
|
|
557
|
+
import FormData from "form-data";
|
|
558
|
+
|
|
559
|
+
// src/utils/files.ts
|
|
560
|
+
import { createHash } from "crypto";
|
|
561
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
562
|
+
import { readdir as readdir2, stat } from "fs/promises";
|
|
563
|
+
import path3 from "path";
|
|
564
|
+
async function fileMd5(filePath) {
|
|
565
|
+
return hashFile(filePath, "md5");
|
|
566
|
+
}
|
|
567
|
+
async function fileSha256(filePath) {
|
|
568
|
+
return hashFile(filePath, "sha256");
|
|
569
|
+
}
|
|
570
|
+
async function hashFile(filePath, algorithm) {
|
|
571
|
+
return new Promise((resolve, reject) => {
|
|
572
|
+
const hash = createHash(algorithm);
|
|
573
|
+
const stream = createReadStream2(filePath);
|
|
574
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
575
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
576
|
+
stream.on("error", reject);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async function listApkFiles(dir) {
|
|
580
|
+
const entries = await readdir2(dir);
|
|
581
|
+
const apks = [];
|
|
582
|
+
for (const entry of entries) {
|
|
583
|
+
const full = path3.join(dir, entry);
|
|
584
|
+
const info = await stat(full);
|
|
585
|
+
if (info.isFile() && entry.toLowerCase().endsWith(".apk")) {
|
|
586
|
+
apks.push(full);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return apks;
|
|
590
|
+
}
|
|
591
|
+
async function fileSize(filePath) {
|
|
592
|
+
const info = await stat(filePath);
|
|
593
|
+
return info.size;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/channels/honor/index.ts
|
|
597
|
+
var BASE_URL2 = "https://appmarket-openapi-drcn.cloud.honor.com";
|
|
598
|
+
var TOKEN_URL = "https://iam.developer.honor.com/auth/token";
|
|
599
|
+
var credSchema2 = z3.object({
|
|
600
|
+
client_id: z3.string().min(1),
|
|
601
|
+
client_secret: z3.string().min(1)
|
|
602
|
+
});
|
|
603
|
+
async function getToken2(clientId, clientSecret) {
|
|
604
|
+
const client = createHttpClient();
|
|
605
|
+
const resp = await client.post(
|
|
606
|
+
TOKEN_URL,
|
|
607
|
+
new URLSearchParams({
|
|
608
|
+
client_id: clientId,
|
|
609
|
+
client_secret: clientSecret,
|
|
610
|
+
grant_type: "client_credentials"
|
|
611
|
+
}).toString(),
|
|
612
|
+
{ headers: { "content-type": "application/x-www-form-urlencoded" } }
|
|
613
|
+
);
|
|
614
|
+
if (resp.data?.access_token) return resp.data.access_token;
|
|
615
|
+
throw new ApkpubError({
|
|
616
|
+
code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */,
|
|
617
|
+
channel: "honor",
|
|
618
|
+
step: "getToken",
|
|
619
|
+
message: `\u83B7\u53D6 token \u5931\u8D25`,
|
|
620
|
+
retryable: false
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
async function getAppId2(token, packageName) {
|
|
624
|
+
const client = createHttpClient();
|
|
625
|
+
const resp = await client.get(`${BASE_URL2}/openapi/v1/publish/get-app-id`, {
|
|
626
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
627
|
+
params: { pkgName: packageName }
|
|
628
|
+
});
|
|
629
|
+
checkHonorResult(resp.data, "getAppId");
|
|
630
|
+
const list = resp.data.data ?? [];
|
|
631
|
+
if (!list.length) {
|
|
632
|
+
throw new ApkpubError({
|
|
633
|
+
code: "CHANNEL_STATE_FAILED" /* CHANNEL_STATE_FAILED */,
|
|
634
|
+
channel: "honor",
|
|
635
|
+
message: `\u672A\u627E\u5230\u5305\u540D ${packageName}`,
|
|
636
|
+
retryable: false
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return list[0].appId;
|
|
640
|
+
}
|
|
641
|
+
function checkHonorResult(data, action) {
|
|
642
|
+
if (data.code !== 0) {
|
|
643
|
+
throw new ApkpubError({
|
|
644
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
645
|
+
channel: "honor",
|
|
646
|
+
step: action,
|
|
647
|
+
message: data.message ?? `\u8363\u8000 API \u5931\u8D25: code=${data.code}`,
|
|
648
|
+
retryable: false
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
var honorChannel = {
|
|
653
|
+
name: "honor",
|
|
654
|
+
label: "\u8363\u8000",
|
|
655
|
+
type: "market",
|
|
656
|
+
fileNameIdentify: "HONOR",
|
|
657
|
+
credentialSchema: credSchema2,
|
|
658
|
+
async getMarketState(appId, config) {
|
|
659
|
+
const creds = credSchema2.parse(config);
|
|
660
|
+
const token = await getToken2(creds.client_id, creds.client_secret);
|
|
661
|
+
const honorAppId = await getAppId2(token, appId);
|
|
662
|
+
const client = createHttpClient();
|
|
663
|
+
const resp = await client.get(`${BASE_URL2}/openapi/v1/publish/get-app-current-release`, {
|
|
664
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
665
|
+
params: { appId: honorAppId }
|
|
666
|
+
});
|
|
667
|
+
checkHonorResult(resp.data, "getReviewState");
|
|
668
|
+
const data = resp.data.data;
|
|
669
|
+
return {
|
|
670
|
+
reviewState: data?.auditStatus === 1 ? "online" : data?.auditStatus === 2 ? "reviewing" : "unknown",
|
|
671
|
+
enableSubmit: true,
|
|
672
|
+
lastVersionCode: Number(data?.versionCode ?? 0),
|
|
673
|
+
lastVersionName: String(data?.versionName ?? "0")
|
|
674
|
+
};
|
|
675
|
+
},
|
|
676
|
+
async validateCredentials(config) {
|
|
677
|
+
const creds = credSchema2.parse(config);
|
|
678
|
+
await getToken2(creds.client_id, creds.client_secret);
|
|
679
|
+
},
|
|
680
|
+
async upload(ctx) {
|
|
681
|
+
const creds = credSchema2.parse(ctx.config);
|
|
682
|
+
const fileName = path4.basename(ctx.filePath);
|
|
683
|
+
ctx.onProgress({ step: "getToken" });
|
|
684
|
+
const token = await getToken2(creds.client_id, creds.client_secret);
|
|
685
|
+
const auth = `Bearer ${token}`;
|
|
686
|
+
ctx.onProgress({ step: "getAppId" });
|
|
687
|
+
const appId = await getAppId2(token, ctx.apkInfo.applicationId);
|
|
688
|
+
const client = createHttpClient();
|
|
689
|
+
ctx.onProgress({ step: "getAppInfo" });
|
|
690
|
+
const appInfoResp = await client.get(`${BASE_URL2}/openapi/v1/publish/get-app-detail`, {
|
|
691
|
+
headers: { Authorization: auth },
|
|
692
|
+
params: { appId }
|
|
693
|
+
});
|
|
694
|
+
checkHonorResult(appInfoResp.data, "getAppInfo");
|
|
695
|
+
const langInfo = appInfoResp.data.data?.languageInfo?.[0] ?? {};
|
|
696
|
+
ctx.onProgress({ step: "getUploadUrl" });
|
|
697
|
+
const sha256 = await fileSha256(ctx.filePath);
|
|
698
|
+
const uploadUrlResp = await client.post(
|
|
699
|
+
`${BASE_URL2}/openapi/v1/publish/get-file-upload-url`,
|
|
700
|
+
[{ fileName, fileType: 100, fileSize: ctx.apkInfo.size, fileSha256: sha256 }],
|
|
701
|
+
{ headers: { Authorization: auth }, params: { appId } }
|
|
702
|
+
);
|
|
703
|
+
checkHonorResult(uploadUrlResp.data, "getUploadUrl");
|
|
704
|
+
const uploadInfo = uploadUrlResp.data.data[0];
|
|
705
|
+
ctx.onProgress({ step: "uploading", percent: 0 });
|
|
706
|
+
const form = new FormData();
|
|
707
|
+
form.append("file", createReadStream3(ctx.filePath), { filename: fileName });
|
|
708
|
+
const uploadResp = await client.post(uploadInfo.url, form, {
|
|
709
|
+
headers: { Authorization: auth, ...form.getHeaders() },
|
|
710
|
+
maxBodyLength: Infinity,
|
|
711
|
+
onUploadProgress: (e) => {
|
|
712
|
+
if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
if (uploadResp.data?.code !== 0 && uploadResp.status >= 400) {
|
|
716
|
+
throw new ApkpubError({
|
|
717
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
718
|
+
channel: "honor",
|
|
719
|
+
step: "uploadFile",
|
|
720
|
+
message: "\u4E0A\u4F20\u6587\u4EF6\u5931\u8D25",
|
|
721
|
+
retryable: true
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
ctx.onProgress({ step: "bindApk" });
|
|
725
|
+
await client.post(
|
|
726
|
+
`${BASE_URL2}/openapi/v1/publish/update-file-info`,
|
|
727
|
+
{ fileInfoList: [{ objectId: uploadInfo.objectId }] },
|
|
728
|
+
{ headers: { Authorization: auth }, params: { appId } }
|
|
729
|
+
);
|
|
730
|
+
ctx.onProgress({ step: "updateDesc" });
|
|
731
|
+
await client.post(
|
|
732
|
+
`${BASE_URL2}/openapi/v1/publish/update-language-info`,
|
|
733
|
+
{
|
|
734
|
+
languageInfoList: [{
|
|
735
|
+
appName: langInfo.appName,
|
|
736
|
+
intro: langInfo.intro,
|
|
737
|
+
desc: ctx.desc,
|
|
738
|
+
briefIntro: langInfo.briefIntro
|
|
739
|
+
}]
|
|
740
|
+
},
|
|
741
|
+
{ headers: { Authorization: auth }, params: { appId } }
|
|
742
|
+
);
|
|
743
|
+
ctx.onProgress({ step: "submit" });
|
|
744
|
+
await client.post(`${BASE_URL2}/openapi/v1/publish/submit-audit`, { releaseType: 1 }, {
|
|
745
|
+
headers: { Authorization: auth },
|
|
746
|
+
params: { appId }
|
|
747
|
+
});
|
|
748
|
+
ctx.onProgress({ step: "done", percent: 100 });
|
|
749
|
+
return { message: "\u63D0\u4EA4\u5BA1\u6838\u6210\u529F" };
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/channels/mi/index.ts
|
|
754
|
+
import { z as z4 } from "zod";
|
|
755
|
+
import { createReadStream as createReadStream4 } from "fs";
|
|
756
|
+
import FormData2 from "form-data";
|
|
757
|
+
|
|
758
|
+
// src/signers/rsa.ts
|
|
759
|
+
import { createPublicKey, publicEncrypt, constants } from "crypto";
|
|
760
|
+
var GROUP_SIZE = 128;
|
|
761
|
+
var ENCRYPT_GROUP_SIZE = GROUP_SIZE - 11;
|
|
762
|
+
function rsaEncrypt(content, publicKeyPem) {
|
|
763
|
+
const pem = normalizePublicKey(publicKeyPem);
|
|
764
|
+
const publicKey = createPublicKey(pem);
|
|
765
|
+
const data = Buffer.from(content, "utf8");
|
|
766
|
+
const chunks = [];
|
|
767
|
+
let offset = 0;
|
|
768
|
+
while (offset < data.length) {
|
|
769
|
+
const remain = data.length - offset;
|
|
770
|
+
const segSize = Math.min(remain, ENCRYPT_GROUP_SIZE);
|
|
771
|
+
const segment = data.subarray(offset, offset + segSize);
|
|
772
|
+
const encrypted = publicEncrypt(
|
|
773
|
+
{ key: publicKey, padding: constants.RSA_PKCS1_PADDING },
|
|
774
|
+
segment
|
|
775
|
+
);
|
|
776
|
+
chunks.push(encrypted);
|
|
777
|
+
offset += segSize;
|
|
778
|
+
}
|
|
779
|
+
return Buffer.concat(chunks).toString("hex");
|
|
780
|
+
}
|
|
781
|
+
function normalizePublicKey(key) {
|
|
782
|
+
const trimmed = key.trim();
|
|
783
|
+
if (trimmed.includes("BEGIN CERTIFICATE")) {
|
|
784
|
+
return trimmed;
|
|
785
|
+
}
|
|
786
|
+
if (trimmed.includes("BEGIN PUBLIC KEY")) {
|
|
787
|
+
return trimmed;
|
|
788
|
+
}
|
|
789
|
+
return `-----BEGIN CERTIFICATE-----
|
|
790
|
+
${trimmed}
|
|
791
|
+
-----END CERTIFICATE-----`;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/signers/md5.ts
|
|
795
|
+
import { createHash as createHash2 } from "crypto";
|
|
796
|
+
function md5Hex(input2) {
|
|
797
|
+
return createHash2("md5").update(input2, "utf8").digest("hex");
|
|
798
|
+
}
|
|
799
|
+
function md5Sign(data, signKey) {
|
|
800
|
+
return md5Hex(data + signKey);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/channels/mi/index.ts
|
|
804
|
+
var DOMAIN = "https://api.developer.xiaomi.com/devupload";
|
|
805
|
+
var QUERY_URL = `${DOMAIN}/dev/query`;
|
|
806
|
+
var PUSH_URL = `${DOMAIN}/dev/push`;
|
|
807
|
+
var credSchema3 = z4.object({
|
|
808
|
+
account: z4.string().min(1),
|
|
809
|
+
publicKey: z4.string().min(1),
|
|
810
|
+
privateKey: z4.string().min(1)
|
|
811
|
+
});
|
|
812
|
+
function buildSig(privateKey, hashes) {
|
|
813
|
+
const sig = JSON.stringify({
|
|
814
|
+
password: privateKey,
|
|
815
|
+
sig: hashes.map((h) => ({ name: h.name, hash: h.hash }))
|
|
816
|
+
});
|
|
817
|
+
return sig;
|
|
818
|
+
}
|
|
819
|
+
var miChannel = {
|
|
820
|
+
name: "mi",
|
|
821
|
+
label: "\u5C0F\u7C73",
|
|
822
|
+
type: "market",
|
|
823
|
+
fileNameIdentify: "MI",
|
|
824
|
+
credentialSchema: credSchema3,
|
|
825
|
+
async getMarketState(appId, config) {
|
|
826
|
+
const creds = credSchema3.parse(config);
|
|
827
|
+
const requestData = JSON.stringify({ userName: creds.account, packageName: appId });
|
|
828
|
+
const sigData = buildSig(creds.privateKey, [{ name: "RequestData", hash: md5Hex(requestData) }]);
|
|
829
|
+
const client = createHttpClient();
|
|
830
|
+
const form = new URLSearchParams();
|
|
831
|
+
form.set("RequestData", requestData);
|
|
832
|
+
form.set("SIG", rsaEncrypt(sigData, creds.publicKey));
|
|
833
|
+
const resp = await client.post(QUERY_URL, form.toString(), {
|
|
834
|
+
headers: { "content-type": "application/x-www-form-urlencoded" }
|
|
835
|
+
});
|
|
836
|
+
checkMiResult(resp.data, "\u83B7\u53D6App\u4FE1\u606F");
|
|
837
|
+
const info = resp.data.packageInfo ?? resp.data;
|
|
838
|
+
return {
|
|
839
|
+
reviewState: "online",
|
|
840
|
+
enableSubmit: true,
|
|
841
|
+
lastVersionCode: Number(info?.versionCode ?? 0),
|
|
842
|
+
lastVersionName: String(info?.versionName ?? "0")
|
|
843
|
+
};
|
|
844
|
+
},
|
|
845
|
+
async validateCredentials(config) {
|
|
846
|
+
credSchema3.parse(config);
|
|
847
|
+
},
|
|
848
|
+
async upload(ctx) {
|
|
849
|
+
const creds = credSchema3.parse(ctx.config);
|
|
850
|
+
ctx.onProgress({ step: "getAppInfo" });
|
|
851
|
+
const marketState = await miChannel.getMarketState(ctx.apkInfo.applicationId, ctx.config);
|
|
852
|
+
const requestData = JSON.stringify({
|
|
853
|
+
userName: creds.account,
|
|
854
|
+
synchroType: 1,
|
|
855
|
+
appInfo: {
|
|
856
|
+
appName: marketState?.lastVersionName ?? ctx.apkInfo.versionName,
|
|
857
|
+
packageName: ctx.apkInfo.applicationId,
|
|
858
|
+
updateDesc: ctx.desc
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
const apkHash = await fileMd5(ctx.filePath);
|
|
862
|
+
const sigData = buildSig(creds.privateKey, [
|
|
863
|
+
{ name: "RequestData", hash: md5Hex(requestData) },
|
|
864
|
+
{ name: "apk", hash: apkHash }
|
|
865
|
+
]);
|
|
866
|
+
ctx.onProgress({ step: "uploading", percent: 0 });
|
|
867
|
+
const form = new FormData2();
|
|
868
|
+
form.append("apk", createReadStream4(ctx.filePath), { filename: "app.apk" });
|
|
869
|
+
form.append("RequestData", requestData);
|
|
870
|
+
form.append("SIG", rsaEncrypt(sigData, creds.publicKey));
|
|
871
|
+
const client = createHttpClient({ timeout: 6e5 });
|
|
872
|
+
const resp = await client.post(PUSH_URL, form, {
|
|
873
|
+
headers: form.getHeaders(),
|
|
874
|
+
maxBodyLength: Infinity,
|
|
875
|
+
onUploadProgress: (e) => {
|
|
876
|
+
if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
checkMiResult(resp.data, "\u4E0A\u4F20Apk");
|
|
880
|
+
ctx.onProgress({ step: "done", percent: 100 });
|
|
881
|
+
return { message: "\u63D0\u4EA4\u6210\u529F" };
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
function checkMiResult(data, action) {
|
|
885
|
+
if (data.result !== 0) {
|
|
886
|
+
throw new ApkpubError({
|
|
887
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
888
|
+
channel: "mi",
|
|
889
|
+
message: `${action}\u5931\u8D25: ${data.message ?? "\u672A\u77E5\u9519\u8BEF"}`,
|
|
890
|
+
retryable: false
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/channels/oppo/index.ts
|
|
896
|
+
import { z as z5 } from "zod";
|
|
897
|
+
import { createReadStream as createReadStream5 } from "fs";
|
|
898
|
+
import FormData3 from "form-data";
|
|
899
|
+
|
|
900
|
+
// src/signers/hmac.ts
|
|
901
|
+
import { createHmac } from "crypto";
|
|
902
|
+
function hmacSha256(data, secret) {
|
|
903
|
+
return createHmac("sha256", secret).update(data, "utf8").digest("hex");
|
|
904
|
+
}
|
|
905
|
+
function signSortedParams(params, secret) {
|
|
906
|
+
const keys = Object.keys(params).sort();
|
|
907
|
+
const pairs = keys.filter((k) => params[k] !== void 0 && params[k] !== null).map((k) => `${k}=${params[k]}`);
|
|
908
|
+
return hmacSha256(pairs.join("&"), secret);
|
|
909
|
+
}
|
|
910
|
+
function vivoSignParams(accessKey, accessSecret, method, originParams) {
|
|
911
|
+
const params = { ...originParams };
|
|
912
|
+
params.access_key = accessKey;
|
|
913
|
+
params.timestamp = String(Date.now());
|
|
914
|
+
params.method = method;
|
|
915
|
+
params.v = "1.0";
|
|
916
|
+
params.sign_method = "HMAC-SHA256";
|
|
917
|
+
params.format = "json";
|
|
918
|
+
params.target_app_key = "developer";
|
|
919
|
+
const keys = Object.keys(params).sort();
|
|
920
|
+
const data = keys.map((k) => `${k}=${params[k]}`).join("&");
|
|
921
|
+
params.sign = hmacSha256(data, accessSecret);
|
|
922
|
+
return params;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/channels/oppo/index.ts
|
|
926
|
+
var DOMAIN2 = "https://oop-openapi-cn.heytapmobi.com";
|
|
927
|
+
var credSchema4 = z5.object({
|
|
928
|
+
client_id: z5.string().min(1),
|
|
929
|
+
client_secret: z5.string().min(1)
|
|
930
|
+
});
|
|
931
|
+
function buildSignedUrl(originUrl, params, token, clientSecret, appendQuery) {
|
|
932
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
933
|
+
const allParams = { ...params, access_token: token, timestamp };
|
|
934
|
+
const url = new URL(originUrl);
|
|
935
|
+
if (appendQuery) {
|
|
936
|
+
for (const [k, v] of Object.entries(allParams)) {
|
|
937
|
+
url.searchParams.set(k, v);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
url.searchParams.set("access_token", token);
|
|
941
|
+
url.searchParams.set("timestamp", timestamp);
|
|
942
|
+
url.searchParams.set("api_sign", signSortedParams(allParams, clientSecret));
|
|
943
|
+
return url.toString();
|
|
944
|
+
}
|
|
945
|
+
var oppoChannel = {
|
|
946
|
+
name: "oppo",
|
|
947
|
+
label: "OPPO",
|
|
948
|
+
type: "market",
|
|
949
|
+
fileNameIdentify: "OPPO",
|
|
950
|
+
credentialSchema: credSchema4,
|
|
951
|
+
async getMarketState(appId, config) {
|
|
952
|
+
const creds = credSchema4.parse(config);
|
|
953
|
+
const token = await getToken3(creds);
|
|
954
|
+
const client = createHttpClient();
|
|
955
|
+
const url = buildSignedUrl(`${DOMAIN2}/resource/v1/app/info`, { pkg_name: appId }, token, creds.client_secret, true);
|
|
956
|
+
const resp = await client.get(url);
|
|
957
|
+
checkOppoResult(resp.data, "\u83B7\u53D6App\u4FE1\u606F");
|
|
958
|
+
const data = resp.data.data;
|
|
959
|
+
return {
|
|
960
|
+
reviewState: "online",
|
|
961
|
+
enableSubmit: true,
|
|
962
|
+
lastVersionCode: Number(data?.version_code ?? 0),
|
|
963
|
+
lastVersionName: String(data?.version_name ?? "0")
|
|
964
|
+
};
|
|
965
|
+
},
|
|
966
|
+
async validateCredentials(config) {
|
|
967
|
+
const creds = credSchema4.parse(config);
|
|
968
|
+
await getToken3(creds);
|
|
969
|
+
},
|
|
970
|
+
async upload(ctx) {
|
|
971
|
+
const creds = credSchema4.parse(ctx.config);
|
|
972
|
+
ctx.onProgress({ step: "getToken" });
|
|
973
|
+
const token = await getToken3(creds);
|
|
974
|
+
const client = createHttpClient();
|
|
975
|
+
ctx.onProgress({ step: "getAppInfo" });
|
|
976
|
+
const appInfoUrl = buildSignedUrl(
|
|
977
|
+
`${DOMAIN2}/resource/v1/app/info`,
|
|
978
|
+
{ pkg_name: ctx.apkInfo.applicationId },
|
|
979
|
+
token,
|
|
980
|
+
creds.client_secret,
|
|
981
|
+
true
|
|
982
|
+
);
|
|
983
|
+
const appInfoResp = await client.get(appInfoUrl);
|
|
984
|
+
checkOppoResult(appInfoResp.data, "\u83B7\u53D6App\u4FE1\u606F");
|
|
985
|
+
const appInfo = appInfoResp.data.data;
|
|
986
|
+
ctx.onProgress({ step: "getUploadUrl" });
|
|
987
|
+
const uploadUrlResp = await client.get(
|
|
988
|
+
buildSignedUrl(`${DOMAIN2}/resource/v1/upload/get-upload-url`, {}, token, creds.client_secret, true)
|
|
989
|
+
);
|
|
990
|
+
checkOppoResult(uploadUrlResp.data, "\u83B7\u53D6\u4E0A\u4F20url");
|
|
991
|
+
const { upload_url: rawUploadUrl, sign } = uploadUrlResp.data.data;
|
|
992
|
+
ctx.onProgress({ step: "uploading", percent: 0 });
|
|
993
|
+
const signedUploadUrl = buildSignedUrl(
|
|
994
|
+
rawUploadUrl,
|
|
995
|
+
{ type: "apk", sign },
|
|
996
|
+
token,
|
|
997
|
+
creds.client_secret,
|
|
998
|
+
false
|
|
999
|
+
);
|
|
1000
|
+
const form = new FormData3();
|
|
1001
|
+
form.append("file", createReadStream5(ctx.filePath), { filename: "app.apk" });
|
|
1002
|
+
form.append("type", "apk");
|
|
1003
|
+
form.append("sign", sign);
|
|
1004
|
+
const uploadResp = await client.post(signedUploadUrl, form, {
|
|
1005
|
+
headers: form.getHeaders(),
|
|
1006
|
+
maxBodyLength: Infinity,
|
|
1007
|
+
onUploadProgress: (e) => {
|
|
1008
|
+
if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
checkOppoResult(uploadResp.data, "\u4E0A\u4F20Apk");
|
|
1012
|
+
const apkResult = uploadResp.data.data;
|
|
1013
|
+
ctx.onProgress({ step: "submit" });
|
|
1014
|
+
const apkUrl = JSON.stringify([{ url: apkResult.url, md5: apkResult.md5, cpu_code: 0 }]);
|
|
1015
|
+
const submitParams = {
|
|
1016
|
+
pkg_name: ctx.apkInfo.applicationId,
|
|
1017
|
+
version_code: String(ctx.apkInfo.versionCode),
|
|
1018
|
+
apk_url: apkUrl,
|
|
1019
|
+
update_desc: ctx.desc,
|
|
1020
|
+
online_type: "1",
|
|
1021
|
+
second_category_id: String(appInfo.second_category_id ?? appInfo.secondCategory ?? ""),
|
|
1022
|
+
third_category_id: String(appInfo.third_category_id ?? appInfo.thirdCategory ?? ""),
|
|
1023
|
+
summary: appInfo.summary ?? "",
|
|
1024
|
+
detail_desc: appInfo.detail_desc ?? appInfo.detailDesc ?? "",
|
|
1025
|
+
privacy_source_url: appInfo.privacy_source_url ?? appInfo.privacyUrl ?? "",
|
|
1026
|
+
icon_url: appInfo.icon_url ?? appInfo.iconUrl ?? "",
|
|
1027
|
+
pic_url: appInfo.pic_url ?? appInfo.picUrl ?? "",
|
|
1028
|
+
test_desc: appInfo.test_desc ?? appInfo.testDesc ?? "",
|
|
1029
|
+
business_username: appInfo.business_username ?? appInfo.businessUsername ?? "",
|
|
1030
|
+
business_email: appInfo.business_email ?? appInfo.businessEmail ?? "",
|
|
1031
|
+
business_mobile: appInfo.business_mobile ?? appInfo.businessMobile ?? "",
|
|
1032
|
+
copyright_url: appInfo.copyright_url ?? appInfo.copyrightUrl ?? ""
|
|
1033
|
+
};
|
|
1034
|
+
const submitUrl = buildSignedUrl(`${DOMAIN2}/resource/v1/app/upd`, submitParams, token, creds.client_secret, false);
|
|
1035
|
+
const submitForm = new URLSearchParams(submitParams);
|
|
1036
|
+
const submitResp = await client.post(submitUrl, submitForm.toString(), {
|
|
1037
|
+
headers: { "content-type": "application/x-www-form-urlencoded" }
|
|
1038
|
+
});
|
|
1039
|
+
checkOppoResult(submitResp.data, "\u63D0\u4EA4\u7248\u672C");
|
|
1040
|
+
ctx.onProgress({ step: "done", percent: 100 });
|
|
1041
|
+
return { message: "\u63D0\u4EA4\u6210\u529F" };
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
async function getToken3(creds) {
|
|
1045
|
+
const client = createHttpClient();
|
|
1046
|
+
const resp = await client.get(`${DOMAIN2}/developer/v1/token`, {
|
|
1047
|
+
params: { client_id: creds.client_id, client_secret: creds.client_secret }
|
|
1048
|
+
});
|
|
1049
|
+
checkOppoResult(resp.data, "\u83B7\u53D6token");
|
|
1050
|
+
return resp.data.data.access_token;
|
|
1051
|
+
}
|
|
1052
|
+
function checkOppoResult(data, action) {
|
|
1053
|
+
if (data.errno !== 0) {
|
|
1054
|
+
throw new ApkpubError({
|
|
1055
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
1056
|
+
channel: "oppo",
|
|
1057
|
+
message: `${action}\u5931\u8D25: ${data.data?.message ?? "\u672A\u77E5\u9519\u8BEF"}`,
|
|
1058
|
+
retryable: false
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/channels/vivo/index.ts
|
|
1064
|
+
import { z as z6 } from "zod";
|
|
1065
|
+
import { createReadStream as createReadStream6 } from "fs";
|
|
1066
|
+
import FormData4 from "form-data";
|
|
1067
|
+
var DOMAIN3 = "https://developer-api.vivo.com.cn/router/rest";
|
|
1068
|
+
var credSchema5 = z6.object({
|
|
1069
|
+
access_key: z6.string().min(1),
|
|
1070
|
+
access_secret: z6.string().min(1)
|
|
1071
|
+
});
|
|
1072
|
+
function buildUrl(method, params, accessKey, accessSecret) {
|
|
1073
|
+
const signed = vivoSignParams(accessKey, accessSecret, method, params);
|
|
1074
|
+
const url = new URL(DOMAIN3);
|
|
1075
|
+
for (const [k, v] of Object.entries(signed)) {
|
|
1076
|
+
url.searchParams.set(k, v);
|
|
1077
|
+
}
|
|
1078
|
+
return url.toString();
|
|
1079
|
+
}
|
|
1080
|
+
var vivoChannel = {
|
|
1081
|
+
name: "vivo",
|
|
1082
|
+
label: "VIVO",
|
|
1083
|
+
type: "market",
|
|
1084
|
+
fileNameIdentify: "VIVO",
|
|
1085
|
+
credentialSchema: credSchema5,
|
|
1086
|
+
async getMarketState(appId, config) {
|
|
1087
|
+
const creds = credSchema5.parse(config);
|
|
1088
|
+
const client = createHttpClient();
|
|
1089
|
+
const url = buildUrl("app.query.details", { packageName: appId }, creds.access_key, creds.access_secret);
|
|
1090
|
+
const resp = await client.get(url);
|
|
1091
|
+
checkVivoResult(resp.data, "\u67E5\u8BE2\u5E94\u7528\u8BE6\u60C5");
|
|
1092
|
+
const data = resp.data.data;
|
|
1093
|
+
return {
|
|
1094
|
+
reviewState: "online",
|
|
1095
|
+
enableSubmit: true,
|
|
1096
|
+
lastVersionCode: Number(data?.versionCode ?? 0),
|
|
1097
|
+
lastVersionName: String(data?.versionName ?? "0")
|
|
1098
|
+
};
|
|
1099
|
+
},
|
|
1100
|
+
async validateCredentials(config) {
|
|
1101
|
+
credSchema5.parse(config);
|
|
1102
|
+
},
|
|
1103
|
+
async upload(ctx) {
|
|
1104
|
+
const creds = credSchema5.parse(ctx.config);
|
|
1105
|
+
const client = createHttpClient({ timeout: 6e5 });
|
|
1106
|
+
ctx.onProgress({ step: "getAppInfo" });
|
|
1107
|
+
const appInfoUrl = buildUrl(
|
|
1108
|
+
"app.query.details",
|
|
1109
|
+
{ packageName: ctx.apkInfo.applicationId },
|
|
1110
|
+
creds.access_key,
|
|
1111
|
+
creds.access_secret
|
|
1112
|
+
);
|
|
1113
|
+
const appInfoResp = await client.get(appInfoUrl);
|
|
1114
|
+
checkVivoResult(appInfoResp.data, "\u67E5\u8BE2\u5E94\u7528\u8BE6\u60C5");
|
|
1115
|
+
const appInfo = appInfoResp.data.data;
|
|
1116
|
+
ctx.onProgress({ step: "uploading", percent: 0 });
|
|
1117
|
+
const fileMd5Hash = await fileMd5(ctx.filePath);
|
|
1118
|
+
const uploadUrl = buildUrl(
|
|
1119
|
+
"app.upload.apk.app",
|
|
1120
|
+
{ packageName: ctx.apkInfo.applicationId, fileMd5: fileMd5Hash },
|
|
1121
|
+
creds.access_key,
|
|
1122
|
+
creds.access_secret
|
|
1123
|
+
);
|
|
1124
|
+
const form = new FormData4();
|
|
1125
|
+
form.append("file", createReadStream6(ctx.filePath), { filename: "app.apk" });
|
|
1126
|
+
const uploadResp = await client.post(uploadUrl, form, {
|
|
1127
|
+
headers: form.getHeaders(),
|
|
1128
|
+
maxBodyLength: Infinity,
|
|
1129
|
+
onUploadProgress: (e) => {
|
|
1130
|
+
if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
checkVivoResult(uploadResp.data, "\u4E0A\u4F20apk");
|
|
1134
|
+
const apkResult = uploadResp.data.data;
|
|
1135
|
+
ctx.onProgress({ step: "submit" });
|
|
1136
|
+
const submitUrl = buildUrl(
|
|
1137
|
+
"app.sync.update.app",
|
|
1138
|
+
{
|
|
1139
|
+
packageName: apkResult.packageName,
|
|
1140
|
+
versionCode: String(apkResult.versionCode),
|
|
1141
|
+
apk: apkResult.serialnumber,
|
|
1142
|
+
fileMd5: apkResult.fileMd5,
|
|
1143
|
+
onlineType: String(appInfo.onlineType ?? 1),
|
|
1144
|
+
updateDesc: ctx.desc
|
|
1145
|
+
},
|
|
1146
|
+
creds.access_key,
|
|
1147
|
+
creds.access_secret
|
|
1148
|
+
);
|
|
1149
|
+
const submitResp = await client.get(submitUrl);
|
|
1150
|
+
checkVivoResult(submitResp.data, "\u63D0\u4EA4\u66F4\u65B0");
|
|
1151
|
+
ctx.onProgress({ step: "done", percent: 100 });
|
|
1152
|
+
return { message: "\u63D0\u4EA4\u6210\u529F" };
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
function checkVivoResult(data, action) {
|
|
1156
|
+
const subCode = Number(data.subCode ?? -1);
|
|
1157
|
+
if (data.code !== 0 || subCode !== 0) {
|
|
1158
|
+
throw new ApkpubError({
|
|
1159
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
1160
|
+
channel: "vivo",
|
|
1161
|
+
message: `${action}\u5931\u8D25: ${data.msg ?? "\u672A\u77E5\u9519\u8BEF"}`,
|
|
1162
|
+
retryable: false
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/channels/custom/oss.ts
|
|
1168
|
+
import OSS from "ali-oss";
|
|
1169
|
+
import path5 from "path";
|
|
1170
|
+
|
|
1171
|
+
// src/utils/template.ts
|
|
1172
|
+
var PLACEHOLDER_RE = /\{(\w+)\}/g;
|
|
1173
|
+
function renderTemplate(template, ctx) {
|
|
1174
|
+
const rendered = template.replace(PLACEHOLDER_RE, (_, key) => {
|
|
1175
|
+
const map = {
|
|
1176
|
+
appId: ctx.appId,
|
|
1177
|
+
versionName: ctx.versionName,
|
|
1178
|
+
versionCode: ctx.versionCode,
|
|
1179
|
+
fileName: ctx.fileName,
|
|
1180
|
+
objectKey: ctx.objectKey ?? ""
|
|
1181
|
+
};
|
|
1182
|
+
const value = map[key];
|
|
1183
|
+
if (value === void 0) return `{${key}}`;
|
|
1184
|
+
return String(value);
|
|
1185
|
+
});
|
|
1186
|
+
return sanitizePath(rendered);
|
|
1187
|
+
}
|
|
1188
|
+
function sanitizePath(path8) {
|
|
1189
|
+
const normalized = path8.replace(/\\/g, "/");
|
|
1190
|
+
if (normalized.includes("..") || normalized.startsWith("/")) {
|
|
1191
|
+
throw new ApkpubError({
|
|
1192
|
+
code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
|
|
1193
|
+
message: `\u8DEF\u5F84\u6A21\u677F\u5305\u542B\u975E\u6CD5\u5B57\u7B26: ${path8}`,
|
|
1194
|
+
retryable: false
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return normalized.replace(/^\/+/, "");
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/channels/custom/oss.ts
|
|
1201
|
+
async function fetchStsToken(stsTokenUrl, signKey, contextB) {
|
|
1202
|
+
assertSafeUrl(stsTokenUrl, "STS Token URL");
|
|
1203
|
+
const sign = md5Sign(contextB, signKey);
|
|
1204
|
+
const client = createHttpClient();
|
|
1205
|
+
const response = await client.post(
|
|
1206
|
+
stsTokenUrl,
|
|
1207
|
+
new URLSearchParams({ c: JSON.stringify({ mode: "text" }), b: contextB, sign }).toString(),
|
|
1208
|
+
{ headers: { "content-type": "application/x-www-form-urlencoded" } }
|
|
1209
|
+
);
|
|
1210
|
+
if (response.status !== 200) {
|
|
1211
|
+
throw new ApkpubError({
|
|
1212
|
+
code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */,
|
|
1213
|
+
message: `\u83B7\u53D6 STS Token \u5931\u8D25: HTTP ${response.status}`,
|
|
1214
|
+
retryable: response.status >= 500
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
const data = response.data?.data;
|
|
1218
|
+
if (!data?.AccessKeyId) {
|
|
1219
|
+
throw new ApkpubError({
|
|
1220
|
+
code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */,
|
|
1221
|
+
message: "STS Token \u54CD\u5E94\u683C\u5F0F\u65E0\u6548",
|
|
1222
|
+
retryable: false
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
accessKeyId: data.AccessKeyId,
|
|
1227
|
+
accessKeySecret: data.AccessKeySecret,
|
|
1228
|
+
securityToken: data.SecurityToken,
|
|
1229
|
+
expiration: data.Expiration
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
async function createOssClient(config) {
|
|
1233
|
+
if (!config.endpoint || !config.bucket) {
|
|
1234
|
+
throw new ApkpubError({
|
|
1235
|
+
code: "CONFIG_INVALID" /* CONFIG_INVALID */,
|
|
1236
|
+
message: "OSS \u6E20\u9053\u9700\u8981\u914D\u7F6E endpoint \u548C bucket",
|
|
1237
|
+
retryable: false
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
assertSafeUrl(config.endpoint.startsWith("http") ? config.endpoint : `https://${config.endpoint}`, "OSS endpoint");
|
|
1241
|
+
if (!config.auth) {
|
|
1242
|
+
throw new ApkpubError({
|
|
1243
|
+
code: "CONFIG_INVALID" /* CONFIG_INVALID */,
|
|
1244
|
+
message: "OSS \u6E20\u9053\u9700\u8981\u914D\u7F6E auth",
|
|
1245
|
+
retryable: false
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
if (config.auth.mode === "ak") {
|
|
1249
|
+
return new OSS({
|
|
1250
|
+
region: extractRegion(config.endpoint),
|
|
1251
|
+
accessKeyId: config.auth.accessKeyId,
|
|
1252
|
+
accessKeySecret: config.auth.accessKeySecret,
|
|
1253
|
+
bucket: config.bucket,
|
|
1254
|
+
endpoint: config.endpoint
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
const sts = await fetchStsToken(config.auth.stsTokenUrl, config.auth.signKey, config.auth.contextB);
|
|
1258
|
+
return new OSS({
|
|
1259
|
+
region: extractRegion(config.endpoint),
|
|
1260
|
+
accessKeyId: sts.accessKeyId,
|
|
1261
|
+
accessKeySecret: sts.accessKeySecret,
|
|
1262
|
+
stsToken: sts.securityToken,
|
|
1263
|
+
bucket: config.bucket,
|
|
1264
|
+
endpoint: config.endpoint,
|
|
1265
|
+
refreshSTSToken: async () => {
|
|
1266
|
+
const auth = config.auth;
|
|
1267
|
+
if (!auth || auth.mode !== "sts") {
|
|
1268
|
+
throw new ApkpubError({ code: "CHANNEL_AUTH_FAILED" /* CHANNEL_AUTH_FAILED */, message: "STS \u914D\u7F6E\u65E0\u6548", retryable: false });
|
|
1269
|
+
}
|
|
1270
|
+
const refreshed = await fetchStsToken(auth.stsTokenUrl, auth.signKey, auth.contextB);
|
|
1271
|
+
return {
|
|
1272
|
+
accessKeyId: refreshed.accessKeyId,
|
|
1273
|
+
accessKeySecret: refreshed.accessKeySecret,
|
|
1274
|
+
stsToken: refreshed.securityToken
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
function extractRegion(endpoint) {
|
|
1280
|
+
const match = endpoint.match(/oss-([a-z0-9-]+)\./);
|
|
1281
|
+
return match?.[1] ?? "cn-beijing";
|
|
1282
|
+
}
|
|
1283
|
+
async function uploadToOss(ctx, config) {
|
|
1284
|
+
ctx.onProgress({ step: "connecting" });
|
|
1285
|
+
const fileName = path5.basename(ctx.filePath);
|
|
1286
|
+
const objectKey = renderTemplate(config.objectKeyTemplate, {
|
|
1287
|
+
appId: ctx.apkInfo.applicationId,
|
|
1288
|
+
versionName: ctx.apkInfo.versionName,
|
|
1289
|
+
versionCode: ctx.apkInfo.versionCode,
|
|
1290
|
+
fileName
|
|
1291
|
+
});
|
|
1292
|
+
const client = await createOssClient(config);
|
|
1293
|
+
ctx.onProgress({ step: "uploading", percent: 0 });
|
|
1294
|
+
await client.multipartUpload(objectKey, ctx.filePath, {
|
|
1295
|
+
progress: (p) => {
|
|
1296
|
+
ctx.onProgress({ step: "uploading", percent: Math.round(p * 100) });
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
const downloadUrl = renderTemplate(config.downloadUrlTemplate, {
|
|
1300
|
+
appId: ctx.apkInfo.applicationId,
|
|
1301
|
+
versionName: ctx.apkInfo.versionName,
|
|
1302
|
+
versionCode: ctx.apkInfo.versionCode,
|
|
1303
|
+
fileName,
|
|
1304
|
+
objectKey
|
|
1305
|
+
});
|
|
1306
|
+
ctx.onProgress({ step: "done", percent: 100 });
|
|
1307
|
+
return { downloadUrl, message: "\u4E0A\u4F20\u6210\u529F" };
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// src/channels/custom/http.ts
|
|
1311
|
+
import { createReadStream as createReadStream7 } from "fs";
|
|
1312
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1313
|
+
import path6 from "path";
|
|
1314
|
+
import FormData5 from "form-data";
|
|
1315
|
+
async function uploadToHttp(ctx, config) {
|
|
1316
|
+
if (!config.uploadUrl) {
|
|
1317
|
+
throw new ApkpubError({
|
|
1318
|
+
code: "CONFIG_INVALID" /* CONFIG_INVALID */,
|
|
1319
|
+
message: "HTTP \u6E20\u9053\u9700\u8981\u914D\u7F6E uploadUrl",
|
|
1320
|
+
retryable: false
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
const fileName = path6.basename(ctx.filePath);
|
|
1324
|
+
const objectKey = renderTemplate(config.objectKeyTemplate, {
|
|
1325
|
+
appId: ctx.apkInfo.applicationId,
|
|
1326
|
+
versionName: ctx.apkInfo.versionName,
|
|
1327
|
+
versionCode: ctx.apkInfo.versionCode,
|
|
1328
|
+
fileName
|
|
1329
|
+
});
|
|
1330
|
+
const uploadUrl = renderTemplate(config.uploadUrl, {
|
|
1331
|
+
appId: ctx.apkInfo.applicationId,
|
|
1332
|
+
versionName: ctx.apkInfo.versionName,
|
|
1333
|
+
versionCode: ctx.apkInfo.versionCode,
|
|
1334
|
+
fileName,
|
|
1335
|
+
objectKey
|
|
1336
|
+
});
|
|
1337
|
+
assertSafeUrl(uploadUrl, "\u4E0A\u4F20\u5730\u5740");
|
|
1338
|
+
const client = createHttpClient({ timeout: 3e5 });
|
|
1339
|
+
const method = config.method ?? "PUT";
|
|
1340
|
+
ctx.onProgress({ step: "uploading", percent: 0 });
|
|
1341
|
+
if (method === "PUT") {
|
|
1342
|
+
const data = await readFile2(ctx.filePath);
|
|
1343
|
+
const response = await client.request({
|
|
1344
|
+
method: "PUT",
|
|
1345
|
+
url: uploadUrl,
|
|
1346
|
+
data,
|
|
1347
|
+
headers: {
|
|
1348
|
+
"Content-Type": "application/vnd.android.package-archive",
|
|
1349
|
+
...config.headers
|
|
1350
|
+
},
|
|
1351
|
+
maxBodyLength: Infinity,
|
|
1352
|
+
signal: ctx.signal,
|
|
1353
|
+
onUploadProgress: (e) => {
|
|
1354
|
+
if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1358
|
+
throw new ApkpubError({
|
|
1359
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
1360
|
+
message: `HTTP \u4E0A\u4F20\u5931\u8D25: ${response.status}`,
|
|
1361
|
+
retryable: response.status >= 500
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
} else {
|
|
1365
|
+
const form = new FormData5();
|
|
1366
|
+
const field = config.formField ?? "file";
|
|
1367
|
+
form.append(field, createReadStream7(ctx.filePath), { filename: fileName });
|
|
1368
|
+
const response = await client.post(uploadUrl, form, {
|
|
1369
|
+
headers: { ...form.getHeaders(), ...config.headers },
|
|
1370
|
+
maxBodyLength: Infinity,
|
|
1371
|
+
signal: ctx.signal,
|
|
1372
|
+
onUploadProgress: (e) => {
|
|
1373
|
+
if (e.total) ctx.onProgress({ step: "uploading", percent: Math.round(e.loaded / e.total * 100) });
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
if (response.status < 200 || response.status >= 300) {
|
|
1377
|
+
throw new ApkpubError({
|
|
1378
|
+
code: "CHANNEL_UPLOAD_FAILED" /* CHANNEL_UPLOAD_FAILED */,
|
|
1379
|
+
message: `HTTP \u4E0A\u4F20\u5931\u8D25: ${response.status}`,
|
|
1380
|
+
retryable: response.status >= 500
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
const downloadUrl = renderTemplate(config.downloadUrlTemplate, {
|
|
1385
|
+
appId: ctx.apkInfo.applicationId,
|
|
1386
|
+
versionName: ctx.apkInfo.versionName,
|
|
1387
|
+
versionCode: ctx.apkInfo.versionCode,
|
|
1388
|
+
fileName,
|
|
1389
|
+
objectKey
|
|
1390
|
+
});
|
|
1391
|
+
ctx.onProgress({ step: "done", percent: 100 });
|
|
1392
|
+
return { downloadUrl, message: "\u4E0A\u4F20\u6210\u529F" };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/channels/custom/index.ts
|
|
1396
|
+
function createCustomChannel(config) {
|
|
1397
|
+
return {
|
|
1398
|
+
name: config.name,
|
|
1399
|
+
label: config.name,
|
|
1400
|
+
type: "custom",
|
|
1401
|
+
fileNameIdentify: config.fileNameIdentify ?? config.name,
|
|
1402
|
+
credentialSchema: customChannelConfigSchema,
|
|
1403
|
+
async upload(ctx) {
|
|
1404
|
+
if (config.uploadType === "oss") {
|
|
1405
|
+
return uploadToOss(ctx, config);
|
|
1406
|
+
}
|
|
1407
|
+
return uploadToHttp(ctx, config);
|
|
1408
|
+
},
|
|
1409
|
+
async validateCredentials() {
|
|
1410
|
+
customChannelConfigSchema.parse(config);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/channels/registry.ts
|
|
1416
|
+
var BUILTIN_MARKET_CHANNELS = [
|
|
1417
|
+
huaweiChannel,
|
|
1418
|
+
honorChannel,
|
|
1419
|
+
miChannel,
|
|
1420
|
+
oppoChannel,
|
|
1421
|
+
vivoChannel
|
|
1422
|
+
];
|
|
1423
|
+
function loadChannelsFromConfig(appConfig) {
|
|
1424
|
+
const channels = [];
|
|
1425
|
+
const builtinNames = new Set(BUILTIN_MARKET_CHANNELS.map((c) => c.name));
|
|
1426
|
+
for (const chConfig of appConfig.channels) {
|
|
1427
|
+
if (!chConfig.enable) continue;
|
|
1428
|
+
if (chConfig.type === "custom") {
|
|
1429
|
+
channels.push(createCustomChannel(chConfig));
|
|
1430
|
+
} else if (builtinNames.has(chConfig.name)) {
|
|
1431
|
+
const builtin = BUILTIN_MARKET_CHANNELS.find((c) => c.name === chConfig.name);
|
|
1432
|
+
if (builtin) channels.push(builtin);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return channels;
|
|
1436
|
+
}
|
|
1437
|
+
function resolveChannels(appConfig, channelNames) {
|
|
1438
|
+
const all = loadChannelsFromConfig(appConfig);
|
|
1439
|
+
if (!channelNames || channelNames.length === 0) {
|
|
1440
|
+
return all;
|
|
1441
|
+
}
|
|
1442
|
+
const resolved = [];
|
|
1443
|
+
for (const name of channelNames) {
|
|
1444
|
+
const ch = all.find((c) => c.name === name);
|
|
1445
|
+
if (!ch) {
|
|
1446
|
+
throw new ApkpubError({
|
|
1447
|
+
code: "CHANNEL_NOT_FOUND" /* CHANNEL_NOT_FOUND */,
|
|
1448
|
+
message: `\u6E20\u9053 "${name}" \u672A\u5728\u914D\u7F6E\u4E2D\u542F\u7528\u6216\u4E0D\u5B58\u5728`,
|
|
1449
|
+
retryable: false
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
resolved.push(ch);
|
|
1453
|
+
}
|
|
1454
|
+
return resolved;
|
|
1455
|
+
}
|
|
1456
|
+
var MARKET_CREDENTIALS = {
|
|
1457
|
+
huawei: [
|
|
1458
|
+
{ name: "client_id", required: true, description: "\u534E\u4E3A Connect API \u5BA2\u6237\u7AEF ID" },
|
|
1459
|
+
{ name: "client_secret", required: true, description: "\u534E\u4E3A Connect API \u5BC6\u94A5" }
|
|
1460
|
+
],
|
|
1461
|
+
honor: [
|
|
1462
|
+
{ name: "client_id", required: true, description: "\u8363\u8000\u5F00\u53D1\u8005\u51ED\u8BC1 ID" },
|
|
1463
|
+
{ name: "client_secret", required: true, description: "\u8363\u8000\u5F00\u53D1\u8005\u51ED\u8BC1\u5BC6\u94A5" }
|
|
1464
|
+
],
|
|
1465
|
+
mi: [
|
|
1466
|
+
{ name: "account", required: true, description: "\u5C0F\u7C73\u5F00\u53D1\u8005\u8D26\u53F7\uFF08\u90AE\u7BB1\uFF09" },
|
|
1467
|
+
{ name: "publicKey", required: true, description: "\u516C\u94A5\u8BC1\u4E66\u5185\u5BB9" },
|
|
1468
|
+
{ name: "privateKey", required: true, description: "\u79C1\u94A5" }
|
|
1469
|
+
],
|
|
1470
|
+
oppo: [
|
|
1471
|
+
{ name: "client_id", required: true, description: "OPPO \u670D\u52A1\u7AEF\u5E94\u7528 ID" },
|
|
1472
|
+
{ name: "client_secret", required: true, description: "OPPO \u670D\u52A1\u7AEF\u5E94\u7528\u5BC6\u94A5" }
|
|
1473
|
+
],
|
|
1474
|
+
vivo: [
|
|
1475
|
+
{ name: "access_key", required: true, description: "VIVO API Access Key" },
|
|
1476
|
+
{ name: "access_secret", required: true, description: "VIVO API Access Secret" }
|
|
1477
|
+
]
|
|
1478
|
+
};
|
|
1479
|
+
function getChannelMetas() {
|
|
1480
|
+
return BUILTIN_MARKET_CHANNELS.map((ch) => ({
|
|
1481
|
+
name: ch.name,
|
|
1482
|
+
label: ch.label,
|
|
1483
|
+
type: ch.type,
|
|
1484
|
+
fileNameIdentify: ch.fileNameIdentify,
|
|
1485
|
+
credentialFields: [
|
|
1486
|
+
...MARKET_CREDENTIALS[ch.name] ?? [],
|
|
1487
|
+
{ name: "fileNameIdentify", required: false, description: "\u591A\u6E20\u9053\u5305\u6587\u4EF6\u540D\u5339\u914D\u6807\u8BC6" }
|
|
1488
|
+
]
|
|
1489
|
+
}));
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// src/utils/output.ts
|
|
1493
|
+
import Table from "cli-table3";
|
|
1494
|
+
import pc2 from "picocolors";
|
|
1495
|
+
function printJson(data) {
|
|
1496
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
1497
|
+
}
|
|
1498
|
+
function printPublishResult(result) {
|
|
1499
|
+
const table = new Table({
|
|
1500
|
+
head: ["\u6E20\u9053", "\u72B6\u6001", "\u4E0B\u8F7D\u94FE\u63A5", "\u9519\u8BEF"],
|
|
1501
|
+
colWidths: [12, 10, 40, 30]
|
|
1502
|
+
});
|
|
1503
|
+
for (const r of result.results) {
|
|
1504
|
+
const statusColor = r.status === "success" ? pc2.green : r.status === "failed" ? pc2.red : r.status === "dry_run" ? pc2.yellow : pc2.gray;
|
|
1505
|
+
table.push([
|
|
1506
|
+
r.label,
|
|
1507
|
+
statusColor(r.status),
|
|
1508
|
+
r.downloadUrl ?? "-",
|
|
1509
|
+
r.error?.message ?? "-"
|
|
1510
|
+
]);
|
|
1511
|
+
}
|
|
1512
|
+
process.stderr.write("\n" + table.toString() + "\n");
|
|
1513
|
+
const { summary } = result;
|
|
1514
|
+
process.stderr.write(
|
|
1515
|
+
pc2.dim(`
|
|
1516
|
+
\u603B\u8BA1: ${summary.total} | \u6210\u529F: ${pc2.green(String(summary.success))} | \u5931\u8D25: ${pc2.red(String(summary.failed))} | \u8DF3\u8FC7: ${summary.skipped}
|
|
1517
|
+
`)
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
function printStatusResults(results) {
|
|
1521
|
+
const table = new Table({
|
|
1522
|
+
head: ["\u6E20\u9053", "\u5BA1\u6838\u72B6\u6001", "\u7248\u672C\u53F7", "\u7248\u672C\u540D", "\u5907\u6CE8"]
|
|
1523
|
+
});
|
|
1524
|
+
for (const r of results) {
|
|
1525
|
+
table.push([r.label, r.state ?? "-", r.versionCode ?? "-", r.versionName ?? "-", r.error ?? "-"]);
|
|
1526
|
+
}
|
|
1527
|
+
process.stderr.write("\n" + table.toString() + "\n");
|
|
1528
|
+
}
|
|
1529
|
+
function isInteractive() {
|
|
1530
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/core/result.ts
|
|
1534
|
+
function aggregateResults(results, dryRun = false) {
|
|
1535
|
+
const success = results.filter((r) => r.status === "success" || r.status === "dry_run").length;
|
|
1536
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
1537
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
1538
|
+
const ok = failed === 0;
|
|
1539
|
+
return {
|
|
1540
|
+
ok,
|
|
1541
|
+
dryRun,
|
|
1542
|
+
results,
|
|
1543
|
+
summary: { total: results.length, success, failed, skipped }
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
function getExitCode(result) {
|
|
1547
|
+
if (result.ok) return 0 /* SUCCESS */;
|
|
1548
|
+
const { success, failed } = result.summary;
|
|
1549
|
+
if (failed > 0 && success > 0) return 4 /* PARTIAL_FAILURE */;
|
|
1550
|
+
if (failed > 0 && success === 0) {
|
|
1551
|
+
const versionErrors = result.results.some((r) => r.error?.code === "VERSION_TOO_LOW" /* VERSION_TOO_LOW */);
|
|
1552
|
+
if (versionErrors && failed === result.results.length) return 3 /* VERSION_CHECK_FAILED */;
|
|
1553
|
+
return 5 /* ALL_FAILED */;
|
|
1554
|
+
}
|
|
1555
|
+
return 1 /* INTERNAL */;
|
|
1556
|
+
}
|
|
1557
|
+
function channelResultFromError(name, label, err) {
|
|
1558
|
+
const apkErr = err instanceof ApkpubError ? err : new ApkpubError({ code: "INTERNAL" /* INTERNAL */, message: String(err) });
|
|
1559
|
+
return {
|
|
1560
|
+
name,
|
|
1561
|
+
label,
|
|
1562
|
+
status: "failed",
|
|
1563
|
+
retryable: apkErr.retryable,
|
|
1564
|
+
error: {
|
|
1565
|
+
code: apkErr.code,
|
|
1566
|
+
message: apkErr.message,
|
|
1567
|
+
step: apkErr.step,
|
|
1568
|
+
retryable: apkErr.retryable
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/cli/init.ts
|
|
1574
|
+
function registerInitCommand(program2) {
|
|
1575
|
+
program2.command("init").description("\u521B\u5EFA\u5E94\u7528\u914D\u7F6E").option("--name <name>", "\u5E94\u7528\u663E\u793A\u540D\u79F0").option("--app <id>", "\u5E94\u7528\u5305\u540D").option("--channels <names>", "\u542F\u7528\u7684\u5E02\u573A\u6E20\u9053\uFF0C\u9017\u53F7\u5206\u9694").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
|
|
1576
|
+
try {
|
|
1577
|
+
await ensureConfigDirs();
|
|
1578
|
+
let name = options.name;
|
|
1579
|
+
let applicationId = options.app;
|
|
1580
|
+
let selectedChannels = options.channels?.split(",").map((s) => s.trim()) ?? [];
|
|
1581
|
+
if (isInteractive() && !options.name) {
|
|
1582
|
+
name = await input({ message: "\u5E94\u7528\u663E\u793A\u540D\u79F0:" });
|
|
1583
|
+
applicationId = await input({ message: "\u5E94\u7528\u5305\u540D (applicationId):" });
|
|
1584
|
+
const metas2 = getChannelMetas();
|
|
1585
|
+
selectedChannels = await checkbox({
|
|
1586
|
+
message: "\u9009\u62E9\u8981\u542F\u7528\u7684\u5E02\u573A\u6E20\u9053:",
|
|
1587
|
+
choices: metas2.map((m) => ({ name: m.label, value: m.name }))
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
if (!name || !applicationId) {
|
|
1591
|
+
throw new ApkpubError({
|
|
1592
|
+
code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
|
|
1593
|
+
message: "\u8BF7\u63D0\u4F9B --name \u548C --app\uFF0C\u6216\u5728\u4EA4\u4E92\u6A21\u5F0F\u4E0B\u8FD0\u884C",
|
|
1594
|
+
retryable: false
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
const metas = getChannelMetas();
|
|
1598
|
+
const channels = [];
|
|
1599
|
+
for (const meta of metas) {
|
|
1600
|
+
const enabled = selectedChannels.length === 0 || selectedChannels.includes(meta.name);
|
|
1601
|
+
if (!enabled && selectedChannels.length > 0) continue;
|
|
1602
|
+
if (selectedChannels.length > 0 && !selectedChannels.includes(meta.name)) continue;
|
|
1603
|
+
if (isInteractive() && (selectedChannels.length === 0 || selectedChannels.includes(meta.name))) {
|
|
1604
|
+
const enable = selectedChannels.length > 0 ? true : await confirm({ message: `\u542F\u7528 ${meta.label} \u6E20\u9053?`, default: false });
|
|
1605
|
+
if (!enable) continue;
|
|
1606
|
+
const params = [];
|
|
1607
|
+
for (const field of meta.credentialFields) {
|
|
1608
|
+
if (field.name === "fileNameIdentify") continue;
|
|
1609
|
+
const value = await input({
|
|
1610
|
+
message: `${meta.label} - ${field.name}${field.description ? ` (${field.description})` : ""}:`
|
|
1611
|
+
});
|
|
1612
|
+
params.push({ name: field.name, value });
|
|
1613
|
+
}
|
|
1614
|
+
params.push({ name: "fileNameIdentify", value: meta.fileNameIdentify });
|
|
1615
|
+
channels.push({ name: meta.name, type: "market", enable: true, params });
|
|
1616
|
+
} else if (selectedChannels.includes(meta.name)) {
|
|
1617
|
+
channels.push({
|
|
1618
|
+
name: meta.name,
|
|
1619
|
+
type: "market",
|
|
1620
|
+
enable: true,
|
|
1621
|
+
params: meta.credentialFields.filter((f) => f.name !== "fileNameIdentify").map((f) => ({ name: f.name, value: `\${${meta.name.toUpperCase()}_${f.name.toUpperCase()}}` }))
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const config = {
|
|
1626
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
1627
|
+
name,
|
|
1628
|
+
applicationId,
|
|
1629
|
+
createTime: Date.now(),
|
|
1630
|
+
enableChannel: true,
|
|
1631
|
+
channels,
|
|
1632
|
+
extension: {}
|
|
1633
|
+
};
|
|
1634
|
+
await saveConfig(config);
|
|
1635
|
+
if (options.json) {
|
|
1636
|
+
printJson({ ok: true, config: { applicationId, name, channels: channels.map((c) => c.name) } });
|
|
1637
|
+
} else {
|
|
1638
|
+
process.stderr.write(`\u5DF2\u521B\u5EFA\u5E94\u7528\u914D\u7F6E: ${applicationId}
|
|
1639
|
+
`);
|
|
1640
|
+
process.stderr.write(`\u914D\u7F6E\u6587\u4EF6: ~/.apkpub/apps/${applicationId}.json
|
|
1641
|
+
`);
|
|
1642
|
+
}
|
|
1643
|
+
process.exit(0 /* SUCCESS */);
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
const message = err instanceof ApkpubError ? err.message : String(err);
|
|
1646
|
+
if (options.json) {
|
|
1647
|
+
printJson({ ok: false, error: { message } });
|
|
1648
|
+
} else {
|
|
1649
|
+
process.stderr.write(`\u9519\u8BEF: ${message}
|
|
1650
|
+
`);
|
|
1651
|
+
}
|
|
1652
|
+
process.exit(2 /* INVALID_ARGUMENT */);
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/cli/config.ts
|
|
1658
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
1659
|
+
function registerConfigCommand(program2) {
|
|
1660
|
+
const config = program2.command("config").description("\u914D\u7F6E\u7BA1\u7406");
|
|
1661
|
+
config.command("list").description("\u5217\u51FA\u6240\u6709\u5E94\u7528\u914D\u7F6E").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
|
|
1662
|
+
const configs = await listConfigs();
|
|
1663
|
+
if (options.json) {
|
|
1664
|
+
printJson({ apps: configs.map((c) => ({ name: c.name, applicationId: c.applicationId })) });
|
|
1665
|
+
} else {
|
|
1666
|
+
for (const c of configs) {
|
|
1667
|
+
process.stderr.write(`${c.applicationId} - ${c.name}
|
|
1668
|
+
`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
process.exit(0 /* SUCCESS */);
|
|
1672
|
+
});
|
|
1673
|
+
config.command("get <appId>").description("\u83B7\u53D6\u5E94\u7528\u914D\u7F6E").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (appId, options) => {
|
|
1674
|
+
try {
|
|
1675
|
+
const cfg = await loadConfig(appId);
|
|
1676
|
+
if (options.json) {
|
|
1677
|
+
printJson(stripSecrets(cfg));
|
|
1678
|
+
} else {
|
|
1679
|
+
process.stderr.write(JSON.stringify(stripSecrets(cfg), null, 2) + "\n");
|
|
1680
|
+
}
|
|
1681
|
+
process.exit(0 /* SUCCESS */);
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
handleError(err, options.json);
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
config.command("export <appId>").description("\u5BFC\u51FA\u5E94\u7528\u914D\u7F6E\uFF08\u9ED8\u8BA4\u5265\u79BB\u5BC6\u94A5\uFF09").option("--include-secrets", "\u5305\u542B\u5BC6\u94A5\uFF08\u4E0D\u63A8\u8350\uFF09").option("-o, --output <file>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (appId, options) => {
|
|
1687
|
+
try {
|
|
1688
|
+
const content = await exportConfig(appId, { includeSecrets: options.includeSecrets });
|
|
1689
|
+
if (options.output) {
|
|
1690
|
+
await writeFile2(options.output, content, { mode: 384 });
|
|
1691
|
+
process.stderr.write(`\u5DF2\u5BFC\u51FA\u5230 ${options.output}
|
|
1692
|
+
`);
|
|
1693
|
+
} else {
|
|
1694
|
+
process.stdout.write(content + "\n");
|
|
1695
|
+
}
|
|
1696
|
+
process.exit(0 /* SUCCESS */);
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
handleError(err, false);
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
config.command("import <file>").description("\u5BFC\u5165\u5E94\u7528\u914D\u7F6E").action(async (file) => {
|
|
1702
|
+
try {
|
|
1703
|
+
const cfg = await importConfig(file);
|
|
1704
|
+
process.stderr.write(`\u5DF2\u5BFC\u5165: ${cfg.applicationId}
|
|
1705
|
+
`);
|
|
1706
|
+
process.exit(0 /* SUCCESS */);
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
handleError(err, false);
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
function handleError(err, json) {
|
|
1713
|
+
const message = err instanceof ApkpubError ? err.message : String(err);
|
|
1714
|
+
if (json) {
|
|
1715
|
+
printJson({ ok: false, error: { message } });
|
|
1716
|
+
} else {
|
|
1717
|
+
process.stderr.write(`\u9519\u8BEF: ${message}
|
|
1718
|
+
`);
|
|
1719
|
+
}
|
|
1720
|
+
process.exit(2 /* INVALID_ARGUMENT */);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// src/apk/ApkParser.ts
|
|
1724
|
+
import AppInfoParser from "app-info-parser";
|
|
1725
|
+
async function parseApk(filePath) {
|
|
1726
|
+
try {
|
|
1727
|
+
const parser = new AppInfoParser(filePath);
|
|
1728
|
+
const result = await parser.parse();
|
|
1729
|
+
const packageName = result.package;
|
|
1730
|
+
const versionCode = Number(result.versionCode);
|
|
1731
|
+
const versionName = String(result.versionName ?? "");
|
|
1732
|
+
if (!packageName) {
|
|
1733
|
+
throw new ApkpubError({
|
|
1734
|
+
code: "APK_PARSE_FAILED" /* APK_PARSE_FAILED */,
|
|
1735
|
+
message: "\u65E0\u6CD5\u4ECE APK \u4E2D\u8BFB\u53D6\u5305\u540D",
|
|
1736
|
+
retryable: false
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
const size = await fileSize(filePath);
|
|
1740
|
+
return {
|
|
1741
|
+
filePath,
|
|
1742
|
+
applicationId: packageName,
|
|
1743
|
+
versionCode,
|
|
1744
|
+
versionName,
|
|
1745
|
+
size
|
|
1746
|
+
};
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
if (err instanceof ApkpubError) throw err;
|
|
1749
|
+
throw new ApkpubError({
|
|
1750
|
+
code: "APK_PARSE_FAILED" /* APK_PARSE_FAILED */,
|
|
1751
|
+
message: `APK \u89E3\u6790\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`,
|
|
1752
|
+
retryable: false,
|
|
1753
|
+
cause: err
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// src/cli/info.ts
|
|
1759
|
+
function registerInfoCommand(program2) {
|
|
1760
|
+
program2.command("info <apk>").description("\u89E3\u6790 APK\uFF0C\u8F93\u51FA\u5305\u540D\u3001\u7248\u672C\u53F7\u7B49\u4FE1\u606F").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (apk, options) => {
|
|
1761
|
+
try {
|
|
1762
|
+
const info = await parseApk(apk);
|
|
1763
|
+
const output = {
|
|
1764
|
+
filePath: info.filePath,
|
|
1765
|
+
applicationId: info.applicationId,
|
|
1766
|
+
versionCode: info.versionCode,
|
|
1767
|
+
versionName: info.versionName,
|
|
1768
|
+
size: info.size,
|
|
1769
|
+
sizeMB: (info.size / 1024 / 1024).toFixed(2)
|
|
1770
|
+
};
|
|
1771
|
+
if (options.json) {
|
|
1772
|
+
printJson(output);
|
|
1773
|
+
} else {
|
|
1774
|
+
process.stderr.write(`\u5305\u540D: ${info.applicationId}
|
|
1775
|
+
`);
|
|
1776
|
+
process.stderr.write(`\u7248\u672C\u53F7: ${info.versionCode}
|
|
1777
|
+
`);
|
|
1778
|
+
process.stderr.write(`\u7248\u672C\u540D: ${info.versionName}
|
|
1779
|
+
`);
|
|
1780
|
+
process.stderr.write(`\u5927\u5C0F: ${output.sizeMB} MB
|
|
1781
|
+
`);
|
|
1782
|
+
}
|
|
1783
|
+
process.exit(0 /* SUCCESS */);
|
|
1784
|
+
} catch (err) {
|
|
1785
|
+
const message = err instanceof ApkpubError ? err.message : String(err);
|
|
1786
|
+
if (options.json) {
|
|
1787
|
+
printJson({ ok: false, error: { message, code: err instanceof ApkpubError ? err.code : "INTERNAL" /* INTERNAL */ } });
|
|
1788
|
+
} else {
|
|
1789
|
+
process.stderr.write(`\u9519\u8BEF: ${message}
|
|
1790
|
+
`);
|
|
1791
|
+
}
|
|
1792
|
+
process.exit(2 /* INVALID_ARGUMENT */);
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// src/core/Dispatcher.ts
|
|
1798
|
+
import pLimit from "p-limit";
|
|
1799
|
+
|
|
1800
|
+
// src/secrets/resolver.ts
|
|
1801
|
+
import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes, scryptSync } from "crypto";
|
|
1802
|
+
var ENV_PLACEHOLDER_RE = /^\$\{([A-Z_][A-Z0-9_]*)\}$/;
|
|
1803
|
+
async function resolveSecret(value, options = {}) {
|
|
1804
|
+
const envMatch = value.match(ENV_PLACEHOLDER_RE);
|
|
1805
|
+
if (envMatch) {
|
|
1806
|
+
const envVal = process.env[envMatch[1]];
|
|
1807
|
+
if (!envVal) {
|
|
1808
|
+
throw new ApkpubError({
|
|
1809
|
+
code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
|
|
1810
|
+
message: `\u73AF\u5883\u53D8\u91CF ${envMatch[1]} \u672A\u8BBE\u7F6E`,
|
|
1811
|
+
retryable: false
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
return { value: envVal, source: "env" };
|
|
1815
|
+
}
|
|
1816
|
+
if (value.startsWith("keychain:")) {
|
|
1817
|
+
const key = value.slice("keychain:".length);
|
|
1818
|
+
const resolved = await resolveFromKeychain(options.service ?? "apkpub-cli", key);
|
|
1819
|
+
return { value: resolved, source: "keychain" };
|
|
1820
|
+
}
|
|
1821
|
+
if (value.startsWith("enc:")) {
|
|
1822
|
+
const resolved = await decryptValue(value.slice("enc:".length));
|
|
1823
|
+
return { value: resolved, source: "encrypted" };
|
|
1824
|
+
}
|
|
1825
|
+
if (looksLikeSecret(value)) {
|
|
1826
|
+
logger.warn("secrets", "\u68C0\u6D4B\u5230\u660E\u6587\u5BC6\u94A5\uFF0C\u5EFA\u8BAE\u8FC1\u79FB\u5230\u73AF\u5883\u53D8\u91CF\u6216 keychain");
|
|
1827
|
+
}
|
|
1828
|
+
return { value, source: "plain" };
|
|
1829
|
+
}
|
|
1830
|
+
async function resolveConfigSecrets(params, options = {}) {
|
|
1831
|
+
const result = {};
|
|
1832
|
+
for (const [key, val] of Object.entries(params)) {
|
|
1833
|
+
if (typeof val === "string" && val.length > 0) {
|
|
1834
|
+
const resolved = await resolveSecret(val, { ...options, account: key });
|
|
1835
|
+
result[key] = resolved.value;
|
|
1836
|
+
} else {
|
|
1837
|
+
result[key] = val;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return result;
|
|
1841
|
+
}
|
|
1842
|
+
async function resolveFromKeychain(service, account) {
|
|
1843
|
+
try {
|
|
1844
|
+
const keytar = await import("keytar");
|
|
1845
|
+
const password = await keytar.getPassword(service, account);
|
|
1846
|
+
if (!password) {
|
|
1847
|
+
throw new ApkpubError({
|
|
1848
|
+
code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
|
|
1849
|
+
message: `keychain \u4E2D\u672A\u627E\u5230 ${service}/${account}`,
|
|
1850
|
+
retryable: false
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
return password;
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
if (err instanceof ApkpubError) throw err;
|
|
1856
|
+
throw new ApkpubError({
|
|
1857
|
+
code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
|
|
1858
|
+
message: `keychain \u4E0D\u53EF\u7528\uFF0C\u8BF7\u5B89\u88C5 keytar \u6216\u6539\u7528\u73AF\u5883\u53D8\u91CF: ${err instanceof Error ? err.message : String(err)}`,
|
|
1859
|
+
retryable: false,
|
|
1860
|
+
cause: err
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
async function decryptValue(encrypted) {
|
|
1865
|
+
const masterKey = process.env.APKPUB_MASTER_KEY;
|
|
1866
|
+
if (!masterKey) {
|
|
1867
|
+
throw new ApkpubError({
|
|
1868
|
+
code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
|
|
1869
|
+
message: "\u89E3\u5BC6\u9700\u8981\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF APKPUB_MASTER_KEY",
|
|
1870
|
+
retryable: false
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
const [ivHex, authTagHex, cipherHex] = encrypted.split(":");
|
|
1874
|
+
if (!ivHex || !authTagHex || !cipherHex) {
|
|
1875
|
+
throw new ApkpubError({
|
|
1876
|
+
code: "SECRET_RESOLVE_FAILED" /* SECRET_RESOLVE_FAILED */,
|
|
1877
|
+
message: "\u52A0\u5BC6\u503C\u683C\u5F0F\u65E0\u6548",
|
|
1878
|
+
retryable: false
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
const key = scryptSync(masterKey, "apkpub-salt", 32);
|
|
1882
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
|
|
1883
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
1884
|
+
const decrypted = Buffer.concat([
|
|
1885
|
+
decipher.update(Buffer.from(cipherHex, "hex")),
|
|
1886
|
+
decipher.final()
|
|
1887
|
+
]);
|
|
1888
|
+
return decrypted.toString("utf8");
|
|
1889
|
+
}
|
|
1890
|
+
function looksLikeSecret(value) {
|
|
1891
|
+
if (value.length < 16) return false;
|
|
1892
|
+
return /secret|key|token|password/i.test(value) === false && /^[A-Za-z0-9+/=_-]{16,}$/.test(value);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// src/core/versionCheck.ts
|
|
1896
|
+
function checkVersion(apkInfo, marketInfo, channelName) {
|
|
1897
|
+
if (!marketInfo) return;
|
|
1898
|
+
if (apkInfo.versionCode <= marketInfo.lastVersionCode) {
|
|
1899
|
+
throw new ApkpubError({
|
|
1900
|
+
code: "VERSION_TOO_LOW" /* VERSION_TOO_LOW */,
|
|
1901
|
+
channel: channelName,
|
|
1902
|
+
message: `\u8981\u63D0\u4EA4\u7684 APK \u7248\u672C\u53F7(${apkInfo.versionCode})\u9700\u5927\u4E8E\u7EBF\u4E0A\u6700\u65B0\u7248\u672C\u53F7(${marketInfo.lastVersionCode})`,
|
|
1903
|
+
retryable: false
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function checkPackageMatch(apkInfo, expectedAppId) {
|
|
1908
|
+
if (apkInfo.applicationId !== expectedAppId) {
|
|
1909
|
+
throw new ApkpubError({
|
|
1910
|
+
code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
|
|
1911
|
+
message: `APK \u5305\u540D(${apkInfo.applicationId})\u4E0E\u914D\u7F6E(${expectedAppId})\u4E0D\u5339\u914D`,
|
|
1912
|
+
retryable: false
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// src/core/TaskLauncher.ts
|
|
1918
|
+
import path7 from "path";
|
|
1919
|
+
import { stat as stat2 } from "fs/promises";
|
|
1920
|
+
var TaskLauncher = class {
|
|
1921
|
+
channel;
|
|
1922
|
+
channelConfig;
|
|
1923
|
+
apkPath;
|
|
1924
|
+
enableChannel;
|
|
1925
|
+
resolvedFile;
|
|
1926
|
+
apkInfo;
|
|
1927
|
+
config = {};
|
|
1928
|
+
constructor(options) {
|
|
1929
|
+
this.channel = options.channel;
|
|
1930
|
+
this.channelConfig = options.channelConfig;
|
|
1931
|
+
this.apkPath = options.apkPath;
|
|
1932
|
+
this.enableChannel = options.enableChannel;
|
|
1933
|
+
}
|
|
1934
|
+
/** 注入渠道配置参数 */
|
|
1935
|
+
injectConfig(resolvedParams) {
|
|
1936
|
+
if (this.channelConfig.type === "custom") {
|
|
1937
|
+
this.config = { ...this.channelConfig, ...resolvedParams };
|
|
1938
|
+
} else {
|
|
1939
|
+
this.config = { ...resolvedParams };
|
|
1940
|
+
const fileId = resolvedParams.fileNameIdentify ?? this.channel.fileNameIdentify;
|
|
1941
|
+
this.config.fileNameIdentify = fileId;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
/** 匹配并选择 APK 文件 */
|
|
1945
|
+
async selectFile() {
|
|
1946
|
+
const info = await stat2(this.apkPath);
|
|
1947
|
+
if (info.isFile()) {
|
|
1948
|
+
this.resolvedFile = this.apkPath;
|
|
1949
|
+
return this.resolvedFile;
|
|
1950
|
+
}
|
|
1951
|
+
if (!this.enableChannel) {
|
|
1952
|
+
throw new ApkpubError({
|
|
1953
|
+
code: "APK_NOT_FOUND" /* APK_NOT_FOUND */,
|
|
1954
|
+
channel: this.channel.name,
|
|
1955
|
+
message: "\u591A\u6E20\u9053\u6A21\u5F0F\u672A\u542F\u7528\uFF0C\u8BF7\u6307\u5B9A\u5355\u4E2A APK \u6587\u4EF6",
|
|
1956
|
+
retryable: false
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
const fileId = String(this.config.fileNameIdentify ?? this.channel.fileNameIdentify);
|
|
1960
|
+
const apks = await listApkFiles(this.apkPath);
|
|
1961
|
+
const matches = apks.filter((f) => path7.basename(f).toLowerCase().includes(fileId.toLowerCase()));
|
|
1962
|
+
if (matches.length === 0) {
|
|
1963
|
+
throw new ApkpubError({
|
|
1964
|
+
code: "APK_NOT_FOUND" /* APK_NOT_FOUND */,
|
|
1965
|
+
channel: this.channel.name,
|
|
1966
|
+
message: `\u627E\u4E0D\u5230\u6587\u4EF6\u540D\u4E2D\u5305\u542B "${fileId}" \u7684 APK \u6587\u4EF6`,
|
|
1967
|
+
retryable: false
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
if (matches.length > 1) {
|
|
1971
|
+
throw new ApkpubError({
|
|
1972
|
+
code: "APK_AMBIGUOUS" /* APK_AMBIGUOUS */,
|
|
1973
|
+
channel: this.channel.name,
|
|
1974
|
+
message: `\u5339\u914D\u5230\u591A\u4E2A APK \u6587\u4EF6\uFF0C\u8BF7\u786E\u4FDD\u552F\u4E00: ${matches.map((f) => path7.basename(f)).join(", ")}`,
|
|
1975
|
+
retryable: false
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
this.resolvedFile = matches[0];
|
|
1979
|
+
return this.resolvedFile;
|
|
1980
|
+
}
|
|
1981
|
+
/** 解析 APK 信息 */
|
|
1982
|
+
async prepare() {
|
|
1983
|
+
const file = this.resolvedFile ?? await this.selectFile();
|
|
1984
|
+
this.apkInfo = await parseApk(file);
|
|
1985
|
+
return this.apkInfo;
|
|
1986
|
+
}
|
|
1987
|
+
getFilePath() {
|
|
1988
|
+
if (!this.resolvedFile) {
|
|
1989
|
+
throw new ApkpubError({
|
|
1990
|
+
code: "APK_NOT_FOUND" /* APK_NOT_FOUND */,
|
|
1991
|
+
message: "\u5C1A\u672A\u9009\u62E9 APK \u6587\u4EF6",
|
|
1992
|
+
retryable: false
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
return this.resolvedFile;
|
|
1996
|
+
}
|
|
1997
|
+
getApkInfo() {
|
|
1998
|
+
if (!this.apkInfo) {
|
|
1999
|
+
throw new ApkpubError({
|
|
2000
|
+
code: "APK_PARSE_FAILED" /* APK_PARSE_FAILED */,
|
|
2001
|
+
message: "\u5C1A\u672A\u89E3\u6790 APK",
|
|
2002
|
+
retryable: false
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
return this.apkInfo;
|
|
2006
|
+
}
|
|
2007
|
+
getConfig() {
|
|
2008
|
+
return this.config;
|
|
2009
|
+
}
|
|
2010
|
+
/** 从渠道配置提取原始参数 */
|
|
2011
|
+
static getRawParams(channelConfig) {
|
|
2012
|
+
if (channelConfig.type === "custom") {
|
|
2013
|
+
return paramsToRecord(channelConfig.params ?? []);
|
|
2014
|
+
}
|
|
2015
|
+
return paramsToRecord(channelConfig.params);
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
|
|
2019
|
+
// src/core/Dispatcher.ts
|
|
2020
|
+
var Dispatcher = class {
|
|
2021
|
+
async dispatch(options) {
|
|
2022
|
+
const { appConfig, channels, apkPath, updateDesc, dryRun, parallel = 1, signal } = options;
|
|
2023
|
+
const limit = pLimit(Math.max(1, parallel));
|
|
2024
|
+
const tasks = channels.map((channel) => {
|
|
2025
|
+
const chConfig = appConfig.channels.find((c) => c.name === channel.name);
|
|
2026
|
+
if (!chConfig || !chConfig.enable) {
|
|
2027
|
+
return async () => ({
|
|
2028
|
+
name: channel.name,
|
|
2029
|
+
label: channel.label,
|
|
2030
|
+
status: "skipped"
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
return async () => {
|
|
2034
|
+
const launcher = new TaskLauncher({
|
|
2035
|
+
channel,
|
|
2036
|
+
channelConfig: chConfig,
|
|
2037
|
+
apkPath,
|
|
2038
|
+
enableChannel: appConfig.enableChannel
|
|
2039
|
+
});
|
|
2040
|
+
try {
|
|
2041
|
+
const rawParams = TaskLauncher.getRawParams(chConfig);
|
|
2042
|
+
const resolved = await resolveConfigSecrets(rawParams, {
|
|
2043
|
+
service: "apkpub-cli",
|
|
2044
|
+
account: `${appConfig.applicationId}/${channel.name}`
|
|
2045
|
+
});
|
|
2046
|
+
launcher.injectConfig(resolved);
|
|
2047
|
+
await launcher.selectFile();
|
|
2048
|
+
const apkInfo = await launcher.prepare();
|
|
2049
|
+
checkPackageMatch(apkInfo, appConfig.applicationId);
|
|
2050
|
+
if (channel.getMarketState) {
|
|
2051
|
+
const marketState = await channel.getMarketState(appConfig.applicationId, launcher.getConfig());
|
|
2052
|
+
checkVersion(apkInfo, marketState, channel.name);
|
|
2053
|
+
}
|
|
2054
|
+
if (dryRun) {
|
|
2055
|
+
logger.info(channel.name, `[dry-run] \u9884\u68C0\u901A\u8FC7\uFF0C\u5C06\u4E0A\u4F20 ${apkInfo.versionName}(${apkInfo.versionCode})`);
|
|
2056
|
+
return { name: channel.name, label: channel.label, status: "dry_run" };
|
|
2057
|
+
}
|
|
2058
|
+
const result = await channel.upload({
|
|
2059
|
+
apkInfo,
|
|
2060
|
+
filePath: launcher.getFilePath(),
|
|
2061
|
+
desc: updateDesc,
|
|
2062
|
+
config: launcher.getConfig(),
|
|
2063
|
+
onProgress: ({ step, percent }) => {
|
|
2064
|
+
options.onChannelProgress?.(channel.name, step, percent);
|
|
2065
|
+
},
|
|
2066
|
+
signal: signal ?? new AbortController().signal
|
|
2067
|
+
});
|
|
2068
|
+
await writeAuditLog({
|
|
2069
|
+
action: "publish",
|
|
2070
|
+
appId: appConfig.applicationId,
|
|
2071
|
+
channels: [channel.name],
|
|
2072
|
+
versionCode: apkInfo.versionCode,
|
|
2073
|
+
result: "success"
|
|
2074
|
+
});
|
|
2075
|
+
return {
|
|
2076
|
+
name: channel.name,
|
|
2077
|
+
label: channel.label,
|
|
2078
|
+
status: "success",
|
|
2079
|
+
downloadUrl: result.downloadUrl
|
|
2080
|
+
};
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
logger.error(channel.name, err instanceof Error ? err.message : String(err));
|
|
2083
|
+
await writeAuditLog({
|
|
2084
|
+
action: "publish",
|
|
2085
|
+
appId: appConfig.applicationId,
|
|
2086
|
+
channels: [channel.name],
|
|
2087
|
+
result: "failed"
|
|
2088
|
+
}).catch(() => {
|
|
2089
|
+
});
|
|
2090
|
+
return channelResultFromError(channel.name, channel.label, err);
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
});
|
|
2094
|
+
const results = await Promise.all(tasks.map((task) => limit(task)));
|
|
2095
|
+
return aggregateResults(results, dryRun);
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
// src/cli/publish.ts
|
|
2100
|
+
function registerPublishCommand(program2) {
|
|
2101
|
+
program2.command("publish").description("\u53D1\u5E03 APK \u5230\u6307\u5B9A\u6E20\u9053").requiredOption("--app <id>", "\u5E94\u7528\u5305\u540D").requiredOption("--apk <path>", "APK \u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84").option("--channels <names>", "\u6E20\u9053\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694", (v) => v.split(",").map((s) => s.trim())).option("--desc <text>", "\u66F4\u65B0\u63CF\u8FF0").option("--parallel [n]", "\u5E76\u884C\u4E0A\u4F20\u6570", "1").option("--dry-run", "\u4EC5\u9884\u68C0\uFF0C\u4E0D\u5B9E\u9645\u4E0A\u4F20").option("--yes", "\u8DF3\u8FC7\u786E\u8BA4").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").option("--no-progress", "\u7981\u7528\u8FDB\u5EA6\u663E\u793A").option("--debug", "\u8C03\u8BD5\u6A21\u5F0F").action(async (options) => {
|
|
2102
|
+
const jsonMode2 = options.json || !isInteractive();
|
|
2103
|
+
setJsonMode(jsonMode2);
|
|
2104
|
+
try {
|
|
2105
|
+
const appConfig = await loadConfig(options.app);
|
|
2106
|
+
const updateDesc = options.desc ?? appConfig.extension.updateDesc ?? "";
|
|
2107
|
+
if (!updateDesc.trim()) {
|
|
2108
|
+
throw new ApkpubError({
|
|
2109
|
+
code: "INVALID_ARGUMENT" /* INVALID_ARGUMENT */,
|
|
2110
|
+
message: "\u8BF7\u901A\u8FC7 --desc \u63D0\u4F9B\u66F4\u65B0\u63CF\u8FF0",
|
|
2111
|
+
retryable: false
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
const channelNames = options.channels;
|
|
2115
|
+
const channels = resolveChannels(appConfig, channelNames);
|
|
2116
|
+
if (channels.length === 0) {
|
|
2117
|
+
throw new ApkpubError({
|
|
2118
|
+
code: "CHANNEL_NOT_FOUND" /* CHANNEL_NOT_FOUND */,
|
|
2119
|
+
message: "\u6CA1\u6709\u53EF\u7528\u7684\u53D1\u5E03\u6E20\u9053",
|
|
2120
|
+
retryable: false
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
const dispatcher = new Dispatcher();
|
|
2124
|
+
const result = await dispatcher.dispatch({
|
|
2125
|
+
appConfig,
|
|
2126
|
+
channels,
|
|
2127
|
+
apkPath: options.apk,
|
|
2128
|
+
updateDesc: updateDesc.trim(),
|
|
2129
|
+
dryRun: options.dryRun,
|
|
2130
|
+
parallel: parseInt(options.parallel ?? "1", 10),
|
|
2131
|
+
onChannelProgress: (channel, step, percent) => {
|
|
2132
|
+
if (!jsonMode2 && options.progress !== false) {
|
|
2133
|
+
const pct = percent !== void 0 ? ` ${percent}%` : "";
|
|
2134
|
+
process.stderr.write(`[${channel}] ${step}${pct}
|
|
2135
|
+
`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
});
|
|
2139
|
+
if (jsonMode2) {
|
|
2140
|
+
printJson(result);
|
|
2141
|
+
} else {
|
|
2142
|
+
printPublishResult(result);
|
|
2143
|
+
}
|
|
2144
|
+
process.exit(getExitCode(result));
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
const apkErr = err instanceof ApkpubError ? err : new ApkpubError({ code: "INTERNAL" /* INTERNAL */, message: String(err) });
|
|
2147
|
+
if (options.json) {
|
|
2148
|
+
printJson({ ok: false, error: { code: apkErr.code, message: apkErr.message, retryable: apkErr.retryable } });
|
|
2149
|
+
} else {
|
|
2150
|
+
process.stderr.write(`\u9519\u8BEF: ${apkErr.message}
|
|
2151
|
+
`);
|
|
2152
|
+
}
|
|
2153
|
+
process.exit(2 /* INVALID_ARGUMENT */);
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// src/cli/status.ts
|
|
2159
|
+
function registerStatusCommand(program2) {
|
|
2160
|
+
program2.command("status").description("\u67E5\u8BE2\u5404\u5E02\u573A\u7EBF\u4E0A\u7248\u672C\u4E0E\u5BA1\u6838\u72B6\u6001").requiredOption("--app <id>", "\u5E94\u7528\u5305\u540D").option("--channels <names>", "\u6E20\u9053\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694", (v) => v.split(",").map((s) => s.trim())).option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
|
|
2161
|
+
setJsonMode(!!options.json);
|
|
2162
|
+
try {
|
|
2163
|
+
const appConfig = await loadConfig(options.app);
|
|
2164
|
+
const channels = resolveChannels(appConfig, options.channels);
|
|
2165
|
+
const results = [];
|
|
2166
|
+
for (const channel of channels) {
|
|
2167
|
+
if (!channel.getMarketState) {
|
|
2168
|
+
results.push({ name: channel.name, label: channel.label, state: "n/a", error: "\u81EA\u5B9A\u4E49\u6E20\u9053\u65E0\u5E02\u573A\u72B6\u6001" });
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
const chConfig = appConfig.channels.find((c) => c.name === channel.name);
|
|
2172
|
+
try {
|
|
2173
|
+
const rawParams = TaskLauncher.getRawParams(chConfig);
|
|
2174
|
+
const resolved = await resolveConfigSecrets(rawParams);
|
|
2175
|
+
const state = await channel.getMarketState(appConfig.applicationId, resolved);
|
|
2176
|
+
results.push({
|
|
2177
|
+
name: channel.name,
|
|
2178
|
+
label: channel.label,
|
|
2179
|
+
state: state?.reviewState ?? "unknown",
|
|
2180
|
+
versionCode: state?.lastVersionCode,
|
|
2181
|
+
versionName: state?.lastVersionName
|
|
2182
|
+
});
|
|
2183
|
+
} catch (err) {
|
|
2184
|
+
results.push({
|
|
2185
|
+
name: channel.name,
|
|
2186
|
+
label: channel.label,
|
|
2187
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (options.json) {
|
|
2192
|
+
printJson({ ok: true, results });
|
|
2193
|
+
} else {
|
|
2194
|
+
printStatusResults(results);
|
|
2195
|
+
}
|
|
2196
|
+
process.exit(0 /* SUCCESS */);
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
const message = err instanceof ApkpubError ? err.message : String(err);
|
|
2199
|
+
if (options.json) {
|
|
2200
|
+
printJson({ ok: false, error: { message } });
|
|
2201
|
+
} else {
|
|
2202
|
+
process.stderr.write(`\u9519\u8BEF: ${message}
|
|
2203
|
+
`);
|
|
2204
|
+
}
|
|
2205
|
+
process.exit(2 /* INVALID_ARGUMENT */);
|
|
2206
|
+
}
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// src/cli/channels.ts
|
|
2211
|
+
function registerChannelsCommand(program2) {
|
|
2212
|
+
program2.command("channels").description("\u5217\u51FA\u652F\u6301\u7684\u5185\u7F6E\u5E02\u573A\u6E20\u9053\u53CA\u6240\u9700\u51ED\u8BC1\u5B57\u6BB5").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action((options) => {
|
|
2213
|
+
const metas = getChannelMetas();
|
|
2214
|
+
if (options.json) {
|
|
2215
|
+
printJson({ channels: metas });
|
|
2216
|
+
} else {
|
|
2217
|
+
for (const ch of metas) {
|
|
2218
|
+
process.stderr.write(`
|
|
2219
|
+
${ch.label} (${ch.name})
|
|
2220
|
+
`);
|
|
2221
|
+
process.stderr.write(` \u6587\u4EF6\u540D\u6807\u8BC6: ${ch.fileNameIdentify}
|
|
2222
|
+
`);
|
|
2223
|
+
process.stderr.write(` \u51ED\u8BC1\u5B57\u6BB5:
|
|
2224
|
+
`);
|
|
2225
|
+
for (const f of ch.credentialFields) {
|
|
2226
|
+
process.stderr.write(` - ${f.name}${f.required ? " *" : ""}: ${f.description ?? ""}
|
|
2227
|
+
`);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
process.stderr.write("\n");
|
|
2231
|
+
}
|
|
2232
|
+
process.exit(0 /* SUCCESS */);
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// src/cli/doctor.ts
|
|
2237
|
+
function registerDoctorCommand(program2) {
|
|
2238
|
+
program2.command("doctor").description("\u4F53\u68C0\uFF1A\u6821\u9A8C\u914D\u7F6E\u5B8C\u6574\u6027\u4E0E\u5404\u6E20\u9053\u51ED\u8BC1\u53EF\u7528\u6027").requiredOption("--app <id>", "\u5E94\u7528\u5305\u540D").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(async (options) => {
|
|
2239
|
+
setJsonMode(!!options.json);
|
|
2240
|
+
try {
|
|
2241
|
+
const appConfig = await loadConfig(options.app);
|
|
2242
|
+
const channels = resolveChannels(appConfig);
|
|
2243
|
+
const checks = [];
|
|
2244
|
+
for (const channel of channels) {
|
|
2245
|
+
const chConfig = appConfig.channels.find((c) => c.name === channel.name);
|
|
2246
|
+
try {
|
|
2247
|
+
const rawParams = TaskLauncher.getRawParams(chConfig);
|
|
2248
|
+
const resolved = await resolveConfigSecrets(rawParams);
|
|
2249
|
+
if (channel.validateCredentials) {
|
|
2250
|
+
await channel.validateCredentials(resolved);
|
|
2251
|
+
}
|
|
2252
|
+
checks.push({ channel: channel.name, ok: true, message: "\u51ED\u8BC1\u6709\u6548" });
|
|
2253
|
+
} catch (err) {
|
|
2254
|
+
checks.push({
|
|
2255
|
+
channel: channel.name,
|
|
2256
|
+
ok: false,
|
|
2257
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
const allOk = checks.every((c) => c.ok);
|
|
2262
|
+
const output = { ok: allOk, app: options.app, checks };
|
|
2263
|
+
if (options.json) {
|
|
2264
|
+
printJson(output);
|
|
2265
|
+
} else {
|
|
2266
|
+
for (const c of checks) {
|
|
2267
|
+
const icon = c.ok ? "\u2713" : "\u2717";
|
|
2268
|
+
process.stderr.write(`${icon} [${c.channel}] ${c.message}
|
|
2269
|
+
`);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
process.exit(allOk ? 0 /* SUCCESS */ : 5 /* ALL_FAILED */);
|
|
2273
|
+
} catch (err) {
|
|
2274
|
+
const message = err instanceof ApkpubError ? err.message : String(err);
|
|
2275
|
+
if (options.json) {
|
|
2276
|
+
printJson({ ok: false, error: { message } });
|
|
2277
|
+
} else {
|
|
2278
|
+
process.stderr.write(`\u9519\u8BEF: ${message}
|
|
2279
|
+
`);
|
|
2280
|
+
}
|
|
2281
|
+
process.exit(2 /* INVALID_ARGUMENT */);
|
|
2282
|
+
}
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// src/cli/describe.ts
|
|
2287
|
+
var COMMANDS = [
|
|
2288
|
+
{
|
|
2289
|
+
name: "init",
|
|
2290
|
+
description: "\u521B\u5EFA\u5E94\u7528\u914D\u7F6E",
|
|
2291
|
+
options: [
|
|
2292
|
+
{ name: "--name", required: false, description: "\u5E94\u7528\u663E\u793A\u540D\u79F0" },
|
|
2293
|
+
{ name: "--app", required: false, description: "\u5E94\u7528\u5305\u540D" },
|
|
2294
|
+
{ name: "--channels", required: false, description: "\u542F\u7528\u7684\u5E02\u573A\u6E20\u9053" }
|
|
2295
|
+
]
|
|
2296
|
+
},
|
|
2297
|
+
{
|
|
2298
|
+
name: "publish",
|
|
2299
|
+
description: "\u53D1\u5E03 APK \u5230\u6307\u5B9A\u6E20\u9053",
|
|
2300
|
+
options: [
|
|
2301
|
+
{ name: "--app", required: true, description: "\u5E94\u7528\u5305\u540D" },
|
|
2302
|
+
{ name: "--apk", required: true, description: "APK \u6587\u4EF6\u6216\u76EE\u5F55" },
|
|
2303
|
+
{ name: "--channels", required: false, description: "\u6E20\u9053\u5217\u8868" },
|
|
2304
|
+
{ name: "--desc", required: false, description: "\u66F4\u65B0\u63CF\u8FF0" },
|
|
2305
|
+
{ name: "--parallel", required: false, description: "\u5E76\u884C\u6570" },
|
|
2306
|
+
{ name: "--dry-run", required: false, description: "\u4EC5\u9884\u68C0" },
|
|
2307
|
+
{ name: "--json", required: false, description: "JSON \u8F93\u51FA" }
|
|
2308
|
+
]
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
name: "status",
|
|
2312
|
+
description: "\u67E5\u8BE2\u5E02\u573A\u5BA1\u6838\u72B6\u6001",
|
|
2313
|
+
options: [
|
|
2314
|
+
{ name: "--app", required: true, description: "\u5E94\u7528\u5305\u540D" },
|
|
2315
|
+
{ name: "--channels", required: false, description: "\u6E20\u9053\u5217\u8868" },
|
|
2316
|
+
{ name: "--json", required: false, description: "JSON \u8F93\u51FA" }
|
|
2317
|
+
]
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
name: "info",
|
|
2321
|
+
description: "\u89E3\u6790 APK \u4FE1\u606F",
|
|
2322
|
+
options: [
|
|
2323
|
+
{ name: "<apk>", required: true, description: "APK \u6587\u4EF6\u8DEF\u5F84" },
|
|
2324
|
+
{ name: "--json", required: false, description: "JSON \u8F93\u51FA" }
|
|
2325
|
+
]
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
name: "doctor",
|
|
2329
|
+
description: "\u914D\u7F6E\u4E0E\u51ED\u8BC1\u4F53\u68C0",
|
|
2330
|
+
options: [
|
|
2331
|
+
{ name: "--app", required: true, description: "\u5E94\u7528\u5305\u540D" },
|
|
2332
|
+
{ name: "--json", required: false, description: "JSON \u8F93\u51FA" }
|
|
2333
|
+
]
|
|
2334
|
+
},
|
|
2335
|
+
{
|
|
2336
|
+
name: "mcp",
|
|
2337
|
+
description: "\u542F\u52A8 MCP server \u6A21\u5F0F",
|
|
2338
|
+
options: []
|
|
2339
|
+
}
|
|
2340
|
+
];
|
|
2341
|
+
function registerDescribeCommand(program2) {
|
|
2342
|
+
program2.command("describe").description("\u8F93\u51FA\u547D\u4EE4\u4E0E\u6E20\u9053\u7684\u673A\u5668\u53EF\u8BFB\u6E05\u5355\uFF08\u4F9B Agent \u81EA\u53D1\u73B0\uFF09").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA", true).action((options) => {
|
|
2343
|
+
const descriptor = {
|
|
2344
|
+
name: "apkpub-cli",
|
|
2345
|
+
version: "1.0.0",
|
|
2346
|
+
commands: COMMANDS,
|
|
2347
|
+
channels: getChannelMetas(),
|
|
2348
|
+
exitCodes: {
|
|
2349
|
+
0: "\u5168\u90E8\u6210\u529F",
|
|
2350
|
+
2: "\u53C2\u6570/\u914D\u7F6E\u9519\u8BEF",
|
|
2351
|
+
3: "\u7248\u672C\u6821\u9A8C\u5931\u8D25",
|
|
2352
|
+
4: "\u90E8\u5206\u6E20\u9053\u5931\u8D25",
|
|
2353
|
+
5: "\u5168\u90E8\u5931\u8D25"
|
|
2354
|
+
},
|
|
2355
|
+
jsonSchema: {
|
|
2356
|
+
publishResult: {
|
|
2357
|
+
ok: "boolean",
|
|
2358
|
+
dryRun: "boolean",
|
|
2359
|
+
results: [{ name: "string", status: "string", downloadUrl: "string?", error: "object?" }],
|
|
2360
|
+
summary: { total: "number", success: "number", failed: "number", skipped: "number" }
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
if (options.json !== false) {
|
|
2365
|
+
printJson(descriptor);
|
|
2366
|
+
}
|
|
2367
|
+
process.exit(0 /* SUCCESS */);
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// src/mcp/server.ts
|
|
2372
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2373
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2374
|
+
import {
|
|
2375
|
+
CallToolRequestSchema,
|
|
2376
|
+
ListToolsRequestSchema
|
|
2377
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
2378
|
+
async function startMcpServer() {
|
|
2379
|
+
const server = new Server(
|
|
2380
|
+
{ name: "apkpub-cli", version: "1.0.0" },
|
|
2381
|
+
{ capabilities: { tools: {} } }
|
|
2382
|
+
);
|
|
2383
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2384
|
+
tools: [
|
|
2385
|
+
{
|
|
2386
|
+
name: "apkpub_info",
|
|
2387
|
+
description: "\u89E3\u6790 APK \u6587\u4EF6\uFF0C\u83B7\u53D6\u5305\u540D\u3001\u7248\u672C\u53F7\u7B49\u4FE1\u606F",
|
|
2388
|
+
inputSchema: {
|
|
2389
|
+
type: "object",
|
|
2390
|
+
properties: { apk: { type: "string", description: "APK \u6587\u4EF6\u8DEF\u5F84" } },
|
|
2391
|
+
required: ["apk"]
|
|
2392
|
+
}
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
name: "apkpub_status",
|
|
2396
|
+
description: "\u67E5\u8BE2\u5E94\u7528\u5728\u5404\u5E02\u573A\u7684\u5BA1\u6838\u72B6\u6001\u4E0E\u7EBF\u4E0A\u7248\u672C",
|
|
2397
|
+
inputSchema: {
|
|
2398
|
+
type: "object",
|
|
2399
|
+
properties: {
|
|
2400
|
+
app: { type: "string", description: "\u5E94\u7528\u5305\u540D" },
|
|
2401
|
+
channels: { type: "array", items: { type: "string" }, description: "\u6E20\u9053\u5217\u8868" }
|
|
2402
|
+
},
|
|
2403
|
+
required: ["app"]
|
|
2404
|
+
}
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
name: "apkpub_publish",
|
|
2408
|
+
description: "\u53D1\u5E03 APK \u5230\u6307\u5B9A\u6E20\u9053",
|
|
2409
|
+
inputSchema: {
|
|
2410
|
+
type: "object",
|
|
2411
|
+
properties: {
|
|
2412
|
+
app: { type: "string", description: "\u5E94\u7528\u5305\u540D" },
|
|
2413
|
+
apk: { type: "string", description: "APK \u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84" },
|
|
2414
|
+
channels: { type: "array", items: { type: "string" }, description: "\u6E20\u9053\u5217\u8868" },
|
|
2415
|
+
desc: { type: "string", description: "\u66F4\u65B0\u63CF\u8FF0" },
|
|
2416
|
+
dryRun: { type: "boolean", description: "\u4EC5\u9884\u68C0" },
|
|
2417
|
+
parallel: { type: "number", description: "\u5E76\u884C\u6570" }
|
|
2418
|
+
},
|
|
2419
|
+
required: ["app", "apk"]
|
|
2420
|
+
}
|
|
2421
|
+
},
|
|
2422
|
+
{
|
|
2423
|
+
name: "apkpub_doctor",
|
|
2424
|
+
description: "\u4F53\u68C0\u5E94\u7528\u914D\u7F6E\u4E0E\u5404\u6E20\u9053\u51ED\u8BC1",
|
|
2425
|
+
inputSchema: {
|
|
2426
|
+
type: "object",
|
|
2427
|
+
properties: { app: { type: "string", description: "\u5E94\u7528\u5305\u540D" } },
|
|
2428
|
+
required: ["app"]
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
]
|
|
2432
|
+
}));
|
|
2433
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2434
|
+
const { name, arguments: args } = request.params;
|
|
2435
|
+
const params = args ?? {};
|
|
2436
|
+
try {
|
|
2437
|
+
switch (name) {
|
|
2438
|
+
case "apkpub_info": {
|
|
2439
|
+
const info = await parseApk(params.apk);
|
|
2440
|
+
return { content: [{ type: "text", text: JSON.stringify(info) }] };
|
|
2441
|
+
}
|
|
2442
|
+
case "apkpub_status": {
|
|
2443
|
+
const appConfig = await loadConfig(params.app);
|
|
2444
|
+
const channels = resolveChannels(appConfig, params.channels);
|
|
2445
|
+
const results = [];
|
|
2446
|
+
for (const channel of channels) {
|
|
2447
|
+
if (!channel.getMarketState) {
|
|
2448
|
+
results.push({ channel: channel.name, state: "n/a" });
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
const chConfig = appConfig.channels.find((c) => c.name === channel.name);
|
|
2452
|
+
const rawParams = TaskLauncher.getRawParams(chConfig);
|
|
2453
|
+
const resolved = await resolveConfigSecrets(rawParams);
|
|
2454
|
+
const state = await channel.getMarketState(appConfig.applicationId, resolved);
|
|
2455
|
+
results.push({ channel: channel.name, ...state });
|
|
2456
|
+
}
|
|
2457
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, results }) }] };
|
|
2458
|
+
}
|
|
2459
|
+
case "apkpub_publish": {
|
|
2460
|
+
const appConfig = await loadConfig(params.app);
|
|
2461
|
+
const channels = resolveChannels(appConfig, params.channels);
|
|
2462
|
+
const dispatcher = new Dispatcher();
|
|
2463
|
+
const result = await dispatcher.dispatch({
|
|
2464
|
+
appConfig,
|
|
2465
|
+
channels,
|
|
2466
|
+
apkPath: params.apk,
|
|
2467
|
+
updateDesc: params.desc ?? appConfig.extension.updateDesc ?? "",
|
|
2468
|
+
dryRun: params.dryRun,
|
|
2469
|
+
parallel: params.parallel ?? 1
|
|
2470
|
+
});
|
|
2471
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
2472
|
+
}
|
|
2473
|
+
case "apkpub_doctor": {
|
|
2474
|
+
const appConfig = await loadConfig(params.app);
|
|
2475
|
+
const channels = resolveChannels(appConfig);
|
|
2476
|
+
const checks = [];
|
|
2477
|
+
for (const channel of channels) {
|
|
2478
|
+
const chConfig = appConfig.channels.find((c) => c.name === channel.name);
|
|
2479
|
+
try {
|
|
2480
|
+
const rawParams = TaskLauncher.getRawParams(chConfig);
|
|
2481
|
+
const resolved = await resolveConfigSecrets(rawParams);
|
|
2482
|
+
if (channel.validateCredentials) await channel.validateCredentials(resolved);
|
|
2483
|
+
checks.push({ channel: channel.name, ok: true });
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
checks.push({ channel: channel.name, ok: false, error: String(err) });
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: checks.every((c) => c.ok), checks }) }] };
|
|
2489
|
+
}
|
|
2490
|
+
default:
|
|
2491
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `\u672A\u77E5\u5DE5\u5177: ${name}` }) }], isError: true };
|
|
2492
|
+
}
|
|
2493
|
+
} catch (err) {
|
|
2494
|
+
return {
|
|
2495
|
+
content: [{ type: "text", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) }],
|
|
2496
|
+
isError: true
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
const transport = new StdioServerTransport();
|
|
2501
|
+
await server.connect(transport);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/cli/mcp.ts
|
|
2505
|
+
function registerMcpCommand(program2) {
|
|
2506
|
+
program2.command("mcp").description("\u4EE5 MCP server \u6A21\u5F0F\u8FD0\u884C\uFF0C\u66B4\u9732 publish/status/info/doctor \u5DE5\u5177").action(async () => {
|
|
2507
|
+
await startMcpServer();
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// src/bin/apkpub.ts
|
|
2512
|
+
var program = new Command();
|
|
2513
|
+
program.name("apkpub").description("APK \u591A\u5E02\u573A\u5206\u53D1 CLI \u5DE5\u5177").version("1.0.0");
|
|
2514
|
+
registerInitCommand(program);
|
|
2515
|
+
registerConfigCommand(program);
|
|
2516
|
+
registerInfoCommand(program);
|
|
2517
|
+
registerPublishCommand(program);
|
|
2518
|
+
registerStatusCommand(program);
|
|
2519
|
+
registerChannelsCommand(program);
|
|
2520
|
+
registerDoctorCommand(program);
|
|
2521
|
+
registerDescribeCommand(program);
|
|
2522
|
+
registerMcpCommand(program);
|
|
2523
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
2524
|
+
process.stderr.write(`\u81F4\u547D\u9519\u8BEF: ${err instanceof Error ? err.message : String(err)}
|
|
2525
|
+
`);
|
|
2526
|
+
process.exit(1);
|
|
2527
|
+
});
|