@huoye/app-ship 0.1.1 → 0.2.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/dist/cli.js CHANGED
@@ -7,25 +7,519 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/utils/upload-manager.ts
10
- import path9 from "path";
10
+ import path12 from "path";
11
+
12
+ // node_modules/chalk/source/vendor/ansi-styles/index.js
13
+ var ANSI_BACKGROUND_OFFSET = 10;
14
+ var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
15
+ var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
16
+ var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
17
+ var styles = {
18
+ modifier: {
19
+ reset: [0, 0],
20
+ // 21 isn't widely supported and 22 does the same thing
21
+ bold: [1, 22],
22
+ dim: [2, 22],
23
+ italic: [3, 23],
24
+ underline: [4, 24],
25
+ overline: [53, 55],
26
+ inverse: [7, 27],
27
+ hidden: [8, 28],
28
+ strikethrough: [9, 29]
29
+ },
30
+ color: {
31
+ black: [30, 39],
32
+ red: [31, 39],
33
+ green: [32, 39],
34
+ yellow: [33, 39],
35
+ blue: [34, 39],
36
+ magenta: [35, 39],
37
+ cyan: [36, 39],
38
+ white: [37, 39],
39
+ // Bright color
40
+ blackBright: [90, 39],
41
+ gray: [90, 39],
42
+ // Alias of `blackBright`
43
+ grey: [90, 39],
44
+ // Alias of `blackBright`
45
+ redBright: [91, 39],
46
+ greenBright: [92, 39],
47
+ yellowBright: [93, 39],
48
+ blueBright: [94, 39],
49
+ magentaBright: [95, 39],
50
+ cyanBright: [96, 39],
51
+ whiteBright: [97, 39]
52
+ },
53
+ bgColor: {
54
+ bgBlack: [40, 49],
55
+ bgRed: [41, 49],
56
+ bgGreen: [42, 49],
57
+ bgYellow: [43, 49],
58
+ bgBlue: [44, 49],
59
+ bgMagenta: [45, 49],
60
+ bgCyan: [46, 49],
61
+ bgWhite: [47, 49],
62
+ // Bright color
63
+ bgBlackBright: [100, 49],
64
+ bgGray: [100, 49],
65
+ // Alias of `bgBlackBright`
66
+ bgGrey: [100, 49],
67
+ // Alias of `bgBlackBright`
68
+ bgRedBright: [101, 49],
69
+ bgGreenBright: [102, 49],
70
+ bgYellowBright: [103, 49],
71
+ bgBlueBright: [104, 49],
72
+ bgMagentaBright: [105, 49],
73
+ bgCyanBright: [106, 49],
74
+ bgWhiteBright: [107, 49]
75
+ }
76
+ };
77
+ var modifierNames = Object.keys(styles.modifier);
78
+ var foregroundColorNames = Object.keys(styles.color);
79
+ var backgroundColorNames = Object.keys(styles.bgColor);
80
+ var colorNames = [...foregroundColorNames, ...backgroundColorNames];
81
+ function assembleStyles() {
82
+ const codes = /* @__PURE__ */ new Map();
83
+ for (const [groupName, group] of Object.entries(styles)) {
84
+ for (const [styleName, style] of Object.entries(group)) {
85
+ styles[styleName] = {
86
+ open: `\x1B[${style[0]}m`,
87
+ close: `\x1B[${style[1]}m`
88
+ };
89
+ group[styleName] = styles[styleName];
90
+ codes.set(style[0], style[1]);
91
+ }
92
+ Object.defineProperty(styles, groupName, {
93
+ value: group,
94
+ enumerable: false
95
+ });
96
+ }
97
+ Object.defineProperty(styles, "codes", {
98
+ value: codes,
99
+ enumerable: false
100
+ });
101
+ styles.color.close = "\x1B[39m";
102
+ styles.bgColor.close = "\x1B[49m";
103
+ styles.color.ansi = wrapAnsi16();
104
+ styles.color.ansi256 = wrapAnsi256();
105
+ styles.color.ansi16m = wrapAnsi16m();
106
+ styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
107
+ styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
108
+ styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
109
+ Object.defineProperties(styles, {
110
+ rgbToAnsi256: {
111
+ value(red, green, blue) {
112
+ if (red === green && green === blue) {
113
+ if (red < 8) {
114
+ return 16;
115
+ }
116
+ if (red > 248) {
117
+ return 231;
118
+ }
119
+ return Math.round((red - 8) / 247 * 24) + 232;
120
+ }
121
+ return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
122
+ },
123
+ enumerable: false
124
+ },
125
+ hexToRgb: {
126
+ value(hex) {
127
+ const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
128
+ if (!matches) {
129
+ return [0, 0, 0];
130
+ }
131
+ let [colorString] = matches;
132
+ if (colorString.length === 3) {
133
+ colorString = [...colorString].map((character) => character + character).join("");
134
+ }
135
+ const integer = Number.parseInt(colorString, 16);
136
+ return [
137
+ /* eslint-disable no-bitwise */
138
+ integer >> 16 & 255,
139
+ integer >> 8 & 255,
140
+ integer & 255
141
+ /* eslint-enable no-bitwise */
142
+ ];
143
+ },
144
+ enumerable: false
145
+ },
146
+ hexToAnsi256: {
147
+ value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
148
+ enumerable: false
149
+ },
150
+ ansi256ToAnsi: {
151
+ value(code) {
152
+ if (code < 8) {
153
+ return 30 + code;
154
+ }
155
+ if (code < 16) {
156
+ return 90 + (code - 8);
157
+ }
158
+ let red;
159
+ let green;
160
+ let blue;
161
+ if (code >= 232) {
162
+ red = ((code - 232) * 10 + 8) / 255;
163
+ green = red;
164
+ blue = red;
165
+ } else {
166
+ code -= 16;
167
+ const remainder = code % 36;
168
+ red = Math.floor(code / 36) / 5;
169
+ green = Math.floor(remainder / 6) / 5;
170
+ blue = remainder % 6 / 5;
171
+ }
172
+ const value = Math.max(red, green, blue) * 2;
173
+ if (value === 0) {
174
+ return 30;
175
+ }
176
+ let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
177
+ if (value === 2) {
178
+ result += 60;
179
+ }
180
+ return result;
181
+ },
182
+ enumerable: false
183
+ },
184
+ rgbToAnsi: {
185
+ value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
186
+ enumerable: false
187
+ },
188
+ hexToAnsi: {
189
+ value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
190
+ enumerable: false
191
+ }
192
+ });
193
+ return styles;
194
+ }
195
+ var ansiStyles = assembleStyles();
196
+ var ansi_styles_default = ansiStyles;
197
+
198
+ // node_modules/chalk/source/vendor/supports-color/index.js
199
+ import process2 from "process";
200
+ import os from "os";
201
+ import tty from "tty";
202
+ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
203
+ const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
204
+ const position = argv.indexOf(prefix + flag);
205
+ const terminatorPosition = argv.indexOf("--");
206
+ return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
207
+ }
208
+ var { env } = process2;
209
+ var flagForceColor;
210
+ if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
211
+ flagForceColor = 0;
212
+ } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
213
+ flagForceColor = 1;
214
+ }
215
+ function envForceColor() {
216
+ if ("FORCE_COLOR" in env) {
217
+ if (env.FORCE_COLOR === "true") {
218
+ return 1;
219
+ }
220
+ if (env.FORCE_COLOR === "false") {
221
+ return 0;
222
+ }
223
+ return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
224
+ }
225
+ }
226
+ function translateLevel(level) {
227
+ if (level === 0) {
228
+ return false;
229
+ }
230
+ return {
231
+ level,
232
+ hasBasic: true,
233
+ has256: level >= 2,
234
+ has16m: level >= 3
235
+ };
236
+ }
237
+ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
238
+ const noFlagForceColor = envForceColor();
239
+ if (noFlagForceColor !== void 0) {
240
+ flagForceColor = noFlagForceColor;
241
+ }
242
+ const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
243
+ if (forceColor === 0) {
244
+ return 0;
245
+ }
246
+ if (sniffFlags) {
247
+ if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
248
+ return 3;
249
+ }
250
+ if (hasFlag("color=256")) {
251
+ return 2;
252
+ }
253
+ }
254
+ if ("TF_BUILD" in env && "AGENT_NAME" in env) {
255
+ return 1;
256
+ }
257
+ if (haveStream && !streamIsTTY && forceColor === void 0) {
258
+ return 0;
259
+ }
260
+ const min = forceColor || 0;
261
+ if (env.TERM === "dumb") {
262
+ return min;
263
+ }
264
+ if (process2.platform === "win32") {
265
+ const osRelease = os.release().split(".");
266
+ if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
267
+ return Number(osRelease[2]) >= 14931 ? 3 : 2;
268
+ }
269
+ return 1;
270
+ }
271
+ if ("CI" in env) {
272
+ if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
273
+ return 3;
274
+ }
275
+ if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
276
+ return 1;
277
+ }
278
+ return min;
279
+ }
280
+ if ("TEAMCITY_VERSION" in env) {
281
+ return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
282
+ }
283
+ if (env.COLORTERM === "truecolor") {
284
+ return 3;
285
+ }
286
+ if (env.TERM === "xterm-kitty") {
287
+ return 3;
288
+ }
289
+ if (env.TERM === "xterm-ghostty") {
290
+ return 3;
291
+ }
292
+ if (env.TERM === "wezterm") {
293
+ return 3;
294
+ }
295
+ if ("TERM_PROGRAM" in env) {
296
+ const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
297
+ switch (env.TERM_PROGRAM) {
298
+ case "iTerm.app": {
299
+ return version >= 3 ? 3 : 2;
300
+ }
301
+ case "Apple_Terminal": {
302
+ return 2;
303
+ }
304
+ }
305
+ }
306
+ if (/-256(color)?$/i.test(env.TERM)) {
307
+ return 2;
308
+ }
309
+ if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
310
+ return 1;
311
+ }
312
+ if ("COLORTERM" in env) {
313
+ return 1;
314
+ }
315
+ return min;
316
+ }
317
+ function createSupportsColor(stream, options = {}) {
318
+ const level = _supportsColor(stream, {
319
+ streamIsTTY: stream && stream.isTTY,
320
+ ...options
321
+ });
322
+ return translateLevel(level);
323
+ }
324
+ var supportsColor = {
325
+ stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
326
+ stderr: createSupportsColor({ isTTY: tty.isatty(2) })
327
+ };
328
+ var supports_color_default = supportsColor;
329
+
330
+ // node_modules/chalk/source/utilities.js
331
+ function stringReplaceAll(string, substring, replacer) {
332
+ let index = string.indexOf(substring);
333
+ if (index === -1) {
334
+ return string;
335
+ }
336
+ const substringLength = substring.length;
337
+ let endIndex = 0;
338
+ let returnValue = "";
339
+ do {
340
+ returnValue += string.slice(endIndex, index) + substring + replacer;
341
+ endIndex = index + substringLength;
342
+ index = string.indexOf(substring, endIndex);
343
+ } while (index !== -1);
344
+ returnValue += string.slice(endIndex);
345
+ return returnValue;
346
+ }
347
+ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
348
+ let endIndex = 0;
349
+ let returnValue = "";
350
+ do {
351
+ const gotCR = string[index - 1] === "\r";
352
+ returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
353
+ endIndex = index + 1;
354
+ index = string.indexOf("\n", endIndex);
355
+ } while (index !== -1);
356
+ returnValue += string.slice(endIndex);
357
+ return returnValue;
358
+ }
359
+
360
+ // node_modules/chalk/source/index.js
361
+ var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
362
+ var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
363
+ var STYLER = /* @__PURE__ */ Symbol("STYLER");
364
+ var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
365
+ var levelMapping = [
366
+ "ansi",
367
+ "ansi",
368
+ "ansi256",
369
+ "ansi16m"
370
+ ];
371
+ var styles2 = /* @__PURE__ */ Object.create(null);
372
+ var applyOptions = (object, options = {}) => {
373
+ if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
374
+ throw new Error("The `level` option should be an integer from 0 to 3");
375
+ }
376
+ const colorLevel = stdoutColor ? stdoutColor.level : 0;
377
+ object.level = options.level === void 0 ? colorLevel : options.level;
378
+ };
379
+ var chalkFactory = (options) => {
380
+ const chalk2 = (...strings) => strings.join(" ");
381
+ applyOptions(chalk2, options);
382
+ Object.setPrototypeOf(chalk2, createChalk.prototype);
383
+ return chalk2;
384
+ };
385
+ function createChalk(options) {
386
+ return chalkFactory(options);
387
+ }
388
+ Object.setPrototypeOf(createChalk.prototype, Function.prototype);
389
+ for (const [styleName, style] of Object.entries(ansi_styles_default)) {
390
+ styles2[styleName] = {
391
+ get() {
392
+ const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
393
+ Object.defineProperty(this, styleName, { value: builder });
394
+ return builder;
395
+ }
396
+ };
397
+ }
398
+ styles2.visible = {
399
+ get() {
400
+ const builder = createBuilder(this, this[STYLER], true);
401
+ Object.defineProperty(this, "visible", { value: builder });
402
+ return builder;
403
+ }
404
+ };
405
+ var getModelAnsi = (model, level, type, ...arguments_) => {
406
+ if (model === "rgb") {
407
+ if (level === "ansi16m") {
408
+ return ansi_styles_default[type].ansi16m(...arguments_);
409
+ }
410
+ if (level === "ansi256") {
411
+ return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_));
412
+ }
413
+ return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_));
414
+ }
415
+ if (model === "hex") {
416
+ return getModelAnsi("rgb", level, type, ...ansi_styles_default.hexToRgb(...arguments_));
417
+ }
418
+ return ansi_styles_default[type][model](...arguments_);
419
+ };
420
+ var usedModels = ["rgb", "hex", "ansi256"];
421
+ for (const model of usedModels) {
422
+ styles2[model] = {
423
+ get() {
424
+ const { level } = this;
425
+ return function(...arguments_) {
426
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansi_styles_default.color.close, this[STYLER]);
427
+ return createBuilder(this, styler, this[IS_EMPTY]);
428
+ };
429
+ }
430
+ };
431
+ const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
432
+ styles2[bgModel] = {
433
+ get() {
434
+ const { level } = this;
435
+ return function(...arguments_) {
436
+ const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansi_styles_default.bgColor.close, this[STYLER]);
437
+ return createBuilder(this, styler, this[IS_EMPTY]);
438
+ };
439
+ }
440
+ };
441
+ }
442
+ var proto = Object.defineProperties(() => {
443
+ }, {
444
+ ...styles2,
445
+ level: {
446
+ enumerable: true,
447
+ get() {
448
+ return this[GENERATOR].level;
449
+ },
450
+ set(level) {
451
+ this[GENERATOR].level = level;
452
+ }
453
+ }
454
+ });
455
+ var createStyler = (open, close, parent) => {
456
+ let openAll;
457
+ let closeAll;
458
+ if (parent === void 0) {
459
+ openAll = open;
460
+ closeAll = close;
461
+ } else {
462
+ openAll = parent.openAll + open;
463
+ closeAll = close + parent.closeAll;
464
+ }
465
+ return {
466
+ open,
467
+ close,
468
+ openAll,
469
+ closeAll,
470
+ parent
471
+ };
472
+ };
473
+ var createBuilder = (self, _styler, _isEmpty) => {
474
+ const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
475
+ Object.setPrototypeOf(builder, proto);
476
+ builder[GENERATOR] = self;
477
+ builder[STYLER] = _styler;
478
+ builder[IS_EMPTY] = _isEmpty;
479
+ return builder;
480
+ };
481
+ var applyStyle = (self, string) => {
482
+ if (self.level <= 0 || !string) {
483
+ return self[IS_EMPTY] ? "" : string;
484
+ }
485
+ let styler = self[STYLER];
486
+ if (styler === void 0) {
487
+ return string;
488
+ }
489
+ const { openAll, closeAll } = styler;
490
+ if (string.includes("\x1B")) {
491
+ while (styler !== void 0) {
492
+ string = stringReplaceAll(string, styler.close, styler.open);
493
+ styler = styler.parent;
494
+ }
495
+ }
496
+ const lfIndex = string.indexOf("\n");
497
+ if (lfIndex !== -1) {
498
+ string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
499
+ }
500
+ return openAll + string + closeAll;
501
+ };
502
+ Object.defineProperties(createChalk.prototype, styles2);
503
+ var chalk = createChalk();
504
+ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
505
+ var source_default = chalk;
11
506
 
12
507
  // src/utils/logger.ts
13
- import chalk from "chalk";
14
508
  var ConsoleLogger = class {
15
509
  info(message) {
16
- console.log(chalk.blue("\u2139\uFE0F " + message));
510
+ console.log(source_default.blue("\u2139\uFE0F " + message));
17
511
  }
18
512
  success(message) {
19
- console.log(chalk.green("\u2705 " + message));
513
+ console.log(source_default.green("\u2705 " + message));
20
514
  }
21
515
  warn(message) {
22
- console.log(chalk.yellow("\u26A0\uFE0F " + message));
516
+ console.log(source_default.yellow("\u26A0\uFE0F " + message));
23
517
  }
24
518
  error(message) {
25
- console.log(chalk.red("\u274C " + message));
519
+ console.log(source_default.red("\u274C " + message));
26
520
  }
27
521
  title(message) {
28
- console.log(chalk.bold.cyan("\n" + message + "\n"));
522
+ console.log(source_default.bold.cyan("\n" + message + "\n"));
29
523
  }
30
524
  newLine() {
31
525
  console.log("");
@@ -134,12 +628,7 @@ var RetryStrategy = class {
134
628
  logger.warn(`${operationName} failed (Attempt ${attempt}): ${errorMessage} [${category}]`);
135
629
  if (!this.shouldRetry(error, attempt)) {
136
630
  logger.error(`${operationName} failed with non-retryable error: ${errorMessage}`);
137
- return {
138
- success: false,
139
- error,
140
- totalAttempts: attempt,
141
- lastError: errorMessage
142
- };
631
+ throw error;
143
632
  }
144
633
  if (attempt <= this.config.maxRetries) {
145
634
  const delayMs = this.calculateDelay(attempt - 1);
@@ -150,12 +639,7 @@ var RetryStrategy = class {
150
639
  }
151
640
  }
152
641
  }
153
- return {
154
- success: false,
155
- error: lastError || new Error(`${operationName} failed after all retries`),
156
- totalAttempts: attempt,
157
- lastError: lastError?.message
158
- };
642
+ throw lastError || new Error(`${operationName} failed after all retries`);
159
643
  }
160
644
  };
161
645
  var DEFAULT_RETRY_CONFIG = {
@@ -481,6 +965,280 @@ async function parseHap(filePath) {
481
965
  return info;
482
966
  }
483
967
 
968
+ // src/utils/release-receipt.ts
969
+ import { createHash, randomUUID } from "crypto";
970
+ import { createReadStream } from "fs";
971
+ import { chmod, mkdir, readFile, realpath, rename, stat, writeFile } from "fs/promises";
972
+ import { homedir } from "os";
973
+ import path2 from "path";
974
+ var RELEASE_RECEIPT_SCHEMA_VERSION = 1;
975
+ var DEFAULT_RELEASE_RECEIPT_LIMIT = 100;
976
+ var REMOTE_IDENTIFIER_KEYS = [
977
+ "id",
978
+ "appId",
979
+ "buildId",
980
+ "versionId",
981
+ "releaseId",
982
+ "editId",
983
+ "versionCode",
984
+ "track",
985
+ "apiTrack",
986
+ "status",
987
+ "committed"
988
+ ];
989
+ function resolveReleaseProjectRoot(projectRoot, configPath) {
990
+ return path2.resolve(projectRoot || path2.dirname(path2.resolve(configPath)));
991
+ }
992
+ function sanitizeReleaseMessage(value, maxLength = 500) {
993
+ let message = typeof value === "string" ? value : String(value ?? "");
994
+ message = message.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, "[\u5DF2\u8131\u654F]").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [\u5DF2\u8131\u654F]").replace(
995
+ /(["']?(?:access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key|api[_-]?key|app[_-]?password|authorization|credential|service[_-]?account(?:[_-]?json)?[_-]?content)["']?\s*[:=]\s*)["']?[^\s,"'}]+["']?/gi,
996
+ "$1[\u5DF2\u8131\u654F]"
997
+ ).replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
998
+ return message.length > maxLength ? `${message.slice(0, maxLength - 1)}\u2026` : message;
999
+ }
1000
+ function createUploadTargets(results) {
1001
+ return results.map((result) => {
1002
+ const platform = sanitizeReleaseMessage(result.platform, 80) || "unknown";
1003
+ const data = isRecord(result.data) ? result.data : void 0;
1004
+ const isDraft = result.success && platform === "google_play" && String(data?.status || "").toLowerCase() === "draft";
1005
+ const remote = createRemoteReceipt(result, data);
1006
+ return {
1007
+ platform,
1008
+ state: result.success ? isDraft ? "draft_created" : "uploaded" : "failed",
1009
+ outcome: result.success ? "success" : "failed",
1010
+ message: sanitizeReleaseMessage(result.message) || (result.success ? "\u4E0A\u4F20\u6210\u529F" : "\u4E0A\u4F20\u5931\u8D25"),
1011
+ ...remote ? { remote } : {}
1012
+ };
1013
+ });
1014
+ }
1015
+ async function createReleaseReceipt(input) {
1016
+ const projectRoot = await canonicalizeProjectRoot(input.projectRoot);
1017
+ const startedAt = toDate(input.startedAt ?? Date.now());
1018
+ const finishedAt = toDate(input.finishedAt ?? Date.now());
1019
+ const targets = input.targets.map(sanitizeTarget);
1020
+ return {
1021
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1022
+ id: randomUUID(),
1023
+ projectKey: createProjectKey(projectRoot),
1024
+ projectRoot,
1025
+ kind: input.kind,
1026
+ ...cleanOptionalText(input.appName, 160) ? { appName: cleanOptionalText(input.appName, 160) } : {},
1027
+ ...cleanOptionalText(input.packageName, 240) ? { packageName: cleanOptionalText(input.packageName, 240) } : {},
1028
+ ...cleanOptionalText(input.versionName, 80) ? { versionName: cleanOptionalText(input.versionName, 80) } : {},
1029
+ ...input.buildNumber !== void 0 ? { buildNumber: cleanOptionalText(String(input.buildNumber), 80) } : {},
1030
+ ...cleanOptionalText(input.changelog, 4e3) ? { changelog: cleanOptionalText(input.changelog, 4e3) } : {},
1031
+ artifacts: await inspectArtifacts(input.artifactPaths),
1032
+ targets,
1033
+ outcome: input.outcome ?? inferOutcome(targets),
1034
+ startedAt: startedAt.toISOString(),
1035
+ finishedAt: finishedAt.toISOString(),
1036
+ durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime())
1037
+ };
1038
+ }
1039
+ function formatReleaseReceipt(receipt) {
1040
+ const lines = [
1041
+ `${receipt.appName || receipt.packageName || "\u5E94\u7528"} \xB7 ${receipt.kind === "build" ? "\u6784\u5EFA\u56DE\u6267" : "\u4E0A\u4F20\u56DE\u6267"}`,
1042
+ `\u7ED3\u679C\uFF1A${receipt.outcome}`,
1043
+ `\u7248\u672C\uFF1A${receipt.versionName || "-"} (${receipt.buildNumber || "-"})`,
1044
+ `\u5B8C\u6210\u65F6\u95F4\uFF1A${receipt.finishedAt}`
1045
+ ];
1046
+ if (receipt.packageName) lines.push(`\u5305\u540D\uFF1A${receipt.packageName}`);
1047
+ if (receipt.changelog) lines.push(`\u66F4\u65B0\u65E5\u5FD7\uFF1A${receipt.changelog}`);
1048
+ for (const artifact of receipt.artifacts) {
1049
+ lines.push(
1050
+ `\u4EA7\u7269\uFF1A${artifact.path}${artifact.sizeBytes !== void 0 ? ` \xB7 ${artifact.sizeBytes} bytes` : ""}${artifact.sha256 ? ` \xB7 SHA-256 ${artifact.sha256}` : ""}`
1051
+ );
1052
+ }
1053
+ for (const target of receipt.targets) {
1054
+ lines.push(`${target.platform}\uFF1A${target.state} \xB7 ${target.message}`);
1055
+ if (target.remote?.consoleUrl) lines.push(`\u540E\u53F0\uFF1A${target.remote.consoleUrl}`);
1056
+ }
1057
+ return lines.join("\n");
1058
+ }
1059
+ var ReleaseReceiptStore = class {
1060
+ storageRoot;
1061
+ maxEntries;
1062
+ constructor(options = {}) {
1063
+ this.storageRoot = path2.resolve(options.storageRoot || path2.join(homedir(), ".flu-cli", "release-receipts"));
1064
+ this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_RELEASE_RECEIPT_LIMIT);
1065
+ }
1066
+ async append(receipt) {
1067
+ await mkdir(this.storageRoot, { recursive: true, mode: 448 });
1068
+ await chmod(this.storageRoot, 448);
1069
+ const filePath = this.getFilePath(receipt.projectKey);
1070
+ const current = await this.readFile(filePath, receipt.projectKey);
1071
+ const receipts = [receipt, ...current.receipts.filter((item) => item.id !== receipt.id)].slice(
1072
+ 0,
1073
+ this.maxEntries
1074
+ );
1075
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
1076
+ await writeFile(
1077
+ tempPath,
1078
+ `${JSON.stringify({
1079
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1080
+ projectKey: receipt.projectKey,
1081
+ receipts
1082
+ }, null, 2)}
1083
+ `,
1084
+ { encoding: "utf8", mode: 384 }
1085
+ );
1086
+ await rename(tempPath, filePath);
1087
+ await chmod(filePath, 384);
1088
+ }
1089
+ async list(projectRoot, limit = 20) {
1090
+ const canonicalRoot = await canonicalizeProjectRoot(projectRoot);
1091
+ const projectKey = createProjectKey(canonicalRoot);
1092
+ const data = await this.readFile(this.getFilePath(projectKey), projectKey);
1093
+ return data.receipts.slice(0, Math.max(0, limit));
1094
+ }
1095
+ async save(input) {
1096
+ const receipt = await createReleaseReceipt(input);
1097
+ await this.append(receipt);
1098
+ return receipt;
1099
+ }
1100
+ getFilePath(projectKey) {
1101
+ return path2.join(this.storageRoot, `${projectKey}.json`);
1102
+ }
1103
+ async readFile(filePath, projectKey) {
1104
+ try {
1105
+ const raw = JSON.parse(await readFile(filePath, "utf8"));
1106
+ return {
1107
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1108
+ projectKey,
1109
+ receipts: Array.isArray(raw.receipts) ? raw.receipts : []
1110
+ };
1111
+ } catch (error) {
1112
+ if (error?.code === "ENOENT") {
1113
+ return { schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION, projectKey, receipts: [] };
1114
+ }
1115
+ throw new Error(`\u8BFB\u53D6\u53D1\u5E03\u56DE\u6267\u5931\u8D25: ${sanitizeReleaseMessage(error?.message || error)}`);
1116
+ }
1117
+ }
1118
+ };
1119
+ function createProjectKey(projectRoot) {
1120
+ return createHash("sha256").update(path2.resolve(projectRoot)).digest("hex").slice(0, 24);
1121
+ }
1122
+ function createRemoteReceipt(result, data) {
1123
+ const identifiers = {};
1124
+ for (const key of REMOTE_IDENTIFIER_KEYS) {
1125
+ const value = data?.[key];
1126
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1127
+ identifiers[key] = typeof value === "string" ? sanitizeReleaseMessage(value, 240) : value;
1128
+ }
1129
+ }
1130
+ const consoleUrl = cleanHttpUrl(result.consoleUrl);
1131
+ const downloadUrl = cleanHttpUrl(result.url || result.link);
1132
+ if (!consoleUrl && !downloadUrl && Object.keys(identifiers).length === 0) return void 0;
1133
+ return {
1134
+ ...consoleUrl ? { consoleUrl } : {},
1135
+ ...downloadUrl ? { downloadUrl } : {},
1136
+ ...Object.keys(identifiers).length > 0 ? { identifiers } : {}
1137
+ };
1138
+ }
1139
+ function sanitizeTarget(target) {
1140
+ return {
1141
+ platform: sanitizeReleaseMessage(target.platform, 80) || "unknown",
1142
+ state: target.state,
1143
+ outcome: target.outcome,
1144
+ message: sanitizeReleaseMessage(target.message),
1145
+ ...target.remote ? { remote: sanitizeRemote(target.remote) } : {},
1146
+ ...cleanOptionalText(target.lastKnownRemoteStatus, 120) ? { lastKnownRemoteStatus: cleanOptionalText(target.lastKnownRemoteStatus, 120) } : {},
1147
+ ...cleanOptionalText(target.checkedAt, 80) ? { checkedAt: cleanOptionalText(target.checkedAt, 80) } : {}
1148
+ };
1149
+ }
1150
+ function sanitizeRemote(remote) {
1151
+ const identifiers = {};
1152
+ for (const [key, value] of Object.entries(remote.identifiers || {})) {
1153
+ if (!REMOTE_IDENTIFIER_KEYS.includes(key)) continue;
1154
+ if (typeof value === "string") identifiers[key] = sanitizeReleaseMessage(value, 240);
1155
+ if (typeof value === "number" || typeof value === "boolean") identifiers[key] = value;
1156
+ }
1157
+ const consoleUrl = cleanHttpUrl(remote.consoleUrl);
1158
+ const downloadUrl = cleanHttpUrl(remote.downloadUrl);
1159
+ return {
1160
+ ...consoleUrl ? { consoleUrl } : {},
1161
+ ...downloadUrl ? { downloadUrl } : {},
1162
+ ...Object.keys(identifiers).length > 0 ? { identifiers } : {}
1163
+ };
1164
+ }
1165
+ async function inspectArtifacts(value) {
1166
+ const paths = Array.from(new Set(flattenArtifactPaths(value).map((item) => path2.resolve(item))));
1167
+ return Promise.all(paths.map(inspectArtifact));
1168
+ }
1169
+ async function inspectArtifact(filePath) {
1170
+ const base = { path: filePath, name: path2.basename(filePath) };
1171
+ try {
1172
+ const info = await stat(filePath);
1173
+ if (!info.isFile()) return { ...base, available: false };
1174
+ return {
1175
+ ...base,
1176
+ sizeBytes: info.size,
1177
+ sha256: await hashFile(filePath),
1178
+ available: true
1179
+ };
1180
+ } catch {
1181
+ return { ...base, available: false };
1182
+ }
1183
+ }
1184
+ function hashFile(filePath) {
1185
+ return new Promise((resolve, reject) => {
1186
+ const hash = createHash("sha256");
1187
+ const stream = createReadStream(filePath);
1188
+ stream.on("error", reject);
1189
+ stream.on("data", (chunk) => hash.update(chunk));
1190
+ stream.on("end", () => resolve(hash.digest("hex")));
1191
+ });
1192
+ }
1193
+ function flattenArtifactPaths(value) {
1194
+ if (typeof value === "string" && value.trim()) return [value];
1195
+ if (Array.isArray(value)) return value.flatMap(flattenArtifactPaths);
1196
+ if (isRecord(value)) return Object.values(value).flatMap(flattenArtifactPaths);
1197
+ return [];
1198
+ }
1199
+ function inferOutcome(targets) {
1200
+ if (targets.length === 0) return "failed";
1201
+ if (targets.every((target) => target.outcome === "cancelled")) return "cancelled";
1202
+ const successCount = targets.filter((target) => target.outcome === "success").length;
1203
+ if (successCount === targets.length) return "success";
1204
+ if (successCount > 0) return "partial";
1205
+ return "failed";
1206
+ }
1207
+ async function canonicalizeProjectRoot(projectRoot) {
1208
+ const resolved = path2.resolve(projectRoot);
1209
+ try {
1210
+ return await realpath(resolved);
1211
+ } catch {
1212
+ return resolved;
1213
+ }
1214
+ }
1215
+ function toDate(value) {
1216
+ const date = value instanceof Date ? value : new Date(value);
1217
+ return Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
1218
+ }
1219
+ function cleanOptionalText(value, maxLength) {
1220
+ const cleaned = sanitizeReleaseMessage(value, maxLength);
1221
+ return cleaned || void 0;
1222
+ }
1223
+ function cleanHttpUrl(value) {
1224
+ if (typeof value !== "string") return void 0;
1225
+ try {
1226
+ const url = new URL(value);
1227
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
1228
+ for (const key of Array.from(url.searchParams.keys())) {
1229
+ if (/token|secret|password|signature|credential|key/i.test(key)) {
1230
+ url.searchParams.set(key, "[\u5DF2\u8131\u654F]");
1231
+ }
1232
+ }
1233
+ return url.toString();
1234
+ } catch {
1235
+ return void 0;
1236
+ }
1237
+ }
1238
+ function isRecord(value) {
1239
+ return !!value && typeof value === "object" && !Array.isArray(value);
1240
+ }
1241
+
484
1242
  // src/adapters/pgyer_uploader.ts
485
1243
  var PgyerUploader = class _PgyerUploader {
486
1244
  name = "Pgyer";
@@ -599,7 +1357,7 @@ var PgyerUploader = class _PgyerUploader {
599
1357
 
600
1358
  // src/adapters/huawei_uploader.ts
601
1359
  import fs4 from "fs-extra";
602
- import path2 from "path";
1360
+ import path3 from "path";
603
1361
  import { promisify } from "util";
604
1362
  import axios2 from "axios";
605
1363
  import FormData2 from "form-data";
@@ -678,7 +1436,7 @@ var HuaweiUploader = class _HuaweiUploader {
678
1436
  const clientSecret = hwConfig.auth.clientSecret;
679
1437
  const appId = hwConfig.appId;
680
1438
  const releaseType = hwConfig.releaseType || "draft";
681
- const suffix = path2.extname(filePath).substring(1);
1439
+ const suffix = path3.extname(filePath).substring(1);
682
1440
  const retryConfig = config.retry;
683
1441
  const tokenUrl = this.getTokenUrl(config);
684
1442
  const publishApiRoot = this.getApiRoot(config);
@@ -699,27 +1457,39 @@ var HuaweiUploader = class _HuaweiUploader {
699
1457
  clientId,
700
1458
  appId
701
1459
  };
702
- logger.info("Requesting upload URL...");
703
- const uploadInfoResult = await retryStrategy.execute(
704
- () => this.getUploadUrl(ctx, suffix),
705
- "Huawei get upload URL"
706
- );
707
- if (!uploadInfoResult.success || !uploadInfoResult.data) {
708
- throw uploadInfoResult.error || new Error("Failed to get upload URL");
709
- }
710
- const uploadInfo = uploadInfoResult.data;
711
- logger.info("Uploading file (this may take a while)...");
712
- const uploadFileResult = await retryStrategy.execute(
713
- () => this.uploadFile(uploadInfo.uploadUrl, uploadInfo.authCode, filePath),
714
- "Huawei file upload"
715
- );
716
- if (!uploadFileResult.success || !uploadFileResult.data) {
717
- throw uploadFileResult.error || new Error("File upload failed");
1460
+ const artifacts = typeof metadata.artifactPath === "object" ? metadata.artifactPath : { universal: filePath || metadata.artifactPath || "" };
1461
+ logger.info(`[Huawei] \u5904\u7406\u4EA7\u7269\u6570\u91CF: ${Object.keys(artifacts).length}`);
1462
+ const uploadResults = [];
1463
+ const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
1464
+ if (!p || !fs4.existsSync(p)) return;
1465
+ const currentSuffix = path3.extname(p).substring(1);
1466
+ logger.info(`[Huawei] \u6B63\u5728\u4E0A\u4F20 ${abi} \u5305: ${path3.basename(p)}`);
1467
+ const uploadInfo = await retryStrategy.execute(
1468
+ () => this.getUploadUrl(ctx, currentSuffix),
1469
+ `Huawei get upload URL for ${abi}`
1470
+ );
1471
+ if (!uploadInfo.success || !uploadInfo.data) {
1472
+ throw uploadInfo.error || new Error(`Failed to get upload URL for ${abi}`);
1473
+ }
1474
+ const uploadFileRes = await retryStrategy.execute(
1475
+ () => this.executeUploadRequest(uploadInfo.data.uploadUrl, uploadInfo.data.authCode, p),
1476
+ `Huawei upload file for ${abi}`
1477
+ );
1478
+ if (!uploadFileRes.success || !uploadFileRes.data || uploadFileRes.data.length === 0) {
1479
+ throw uploadFileRes.error || new Error(`File upload failed for ${abi}`);
1480
+ }
1481
+ uploadResults.push({
1482
+ fileName: path3.basename(p),
1483
+ fileDestUrl: uploadFileRes.data[0].fileDestUrl
1484
+ });
1485
+ });
1486
+ await Promise.all(uploadPromises);
1487
+ if (uploadResults.length === 0) {
1488
+ throw new Error("No files were successfully uploaded");
718
1489
  }
719
- const fileDestUrl = uploadFileResult.data.fileDestUrl;
720
- logger.info("Linking file to app...");
1490
+ logger.info(`[Huawei] \u6B63\u5728\u5173\u8054 ${uploadResults.length} \u4E2A\u6587\u4EF6\u5230\u5E94\u7528...`);
721
1491
  const updateInfoResult = await retryStrategy.execute(
722
- () => this.updateAppFileInfo(ctx, fileDestUrl, filePath),
1492
+ () => this.updateAppFileInfo(ctx, uploadResults, filePath),
723
1493
  "Huawei update file info"
724
1494
  );
725
1495
  if (!updateInfoResult.success) {
@@ -881,7 +1651,7 @@ var HuaweiUploader = class _HuaweiUploader {
881
1651
  }
882
1652
  /** 通用文件上传(FormData 方式,无需 API 认证头) */
883
1653
  async executeUploadRequest(uploadUrl, authCode, filePath) {
884
- const fileName = path2.basename(filePath);
1654
+ const fileName = path3.basename(filePath);
885
1655
  const fileSize = fs4.statSync(filePath).size;
886
1656
  const form = new FormData2();
887
1657
  form.append("file", fs4.createReadStream(filePath), {
@@ -923,14 +1693,17 @@ var HuaweiUploader = class _HuaweiUploader {
923
1693
  throw new Error("Upload succeeded but fileDestUrl not found in response");
924
1694
  }
925
1695
  /** 更新应用文件信息 */
926
- async updateAppFileInfo(ctx, fileDestUrl, filePath) {
927
- const fileName = path2.basename(filePath);
928
- const fileType = path2.extname(filePath).toLowerCase() === ".aab" ? 7 : 5;
1696
+ async updateAppFileInfo(ctx, files, anyFilePath) {
1697
+ const hasAab = files.some((f) => f.fileName.toLowerCase().endsWith(".aab"));
1698
+ const fileType = hasAab ? 7 /* AAB */ : 5 /* APP_PACKAGE */;
929
1699
  const res = await axios2.put(
930
1700
  `${ctx.apiRoot}/app-file-info`,
931
1701
  {
932
1702
  fileType,
933
- files: [{ fileName, fileDestUrl }]
1703
+ files: files.map((f) => ({
1704
+ fileName: f.fileName,
1705
+ fileDestUrl: f.fileDestUrl
1706
+ }))
934
1707
  },
935
1708
  {
936
1709
  params: { appId: ctx.appId },
@@ -1045,7 +1818,7 @@ var HuaweiUploader = class _HuaweiUploader {
1045
1818
  logger.warn(`Asset file not found, skipping: ${filePath}`);
1046
1819
  continue;
1047
1820
  }
1048
- const suffix = path2.extname(filePath).substring(1).toLowerCase();
1821
+ const suffix = path3.extname(filePath).substring(1).toLowerCase();
1049
1822
  if (!HUAWEI_SUPPORTED_SUFFIXES.image.includes(suffix)) {
1050
1823
  logger.warn(`Unsupported file type ${suffix}, skipping: ${filePath}`);
1051
1824
  continue;
@@ -1100,6 +1873,7 @@ var HuaweiUploader = class _HuaweiUploader {
1100
1873
 
1101
1874
  // src/adapters/xiaomi_uploader.ts
1102
1875
  import fs5 from "fs-extra";
1876
+ import path4 from "path";
1103
1877
  import axios3 from "axios";
1104
1878
  import FormData3 from "form-data";
1105
1879
  import crypto3 from "crypto";
@@ -1350,80 +2124,39 @@ ${lines.join("\n")}
1350
2124
  const xiaomiConfig = config.publish?.xiaomi;
1351
2125
  const { userName, privateKey, publicKeyPath } = xiaomiConfig.auth;
1352
2126
  const updateDesc = metadata.changelog || xiaomiConfig?.description || "App Update";
1353
- const retryConfig = config.retry;
1354
- if (!fs5.existsSync(filePath)) {
1355
- return {
1356
- platform: "xiaomi",
1357
- success: false,
1358
- message: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`
1359
- };
1360
- }
2127
+ const artifacts = typeof metadata.artifactPath === "object" ? metadata.artifactPath : { universal: filePath || (typeof metadata.artifactPath === "string" ? metadata.artifactPath : "") };
1361
2128
  const resolvedPublicKey = await this.resolvePublicKey(publicKeyPath);
1362
2129
  if (!resolvedPublicKey) {
1363
- return {
1364
- platform: "xiaomi",
1365
- success: false,
1366
- message: "\u65E0\u6CD5\u83B7\u53D6 RSA \u516C\u94A5: publicKeyPath \u672A\u63D0\u4F9B\u6216\u6587\u4EF6\u4E0D\u5B58\u5728"
1367
- };
1368
- }
1369
- const packageName = xiaomiConfig.packageName || this.inferPackageName(filePath);
1370
- if (!packageName) {
1371
- return {
1372
- platform: "xiaomi",
1373
- success: false,
1374
- message: "\u7F3A\u5C11 packageName \u914D\u7F6E, \u4E14\u65E0\u6CD5\u4ECE\u6587\u4EF6\u540D\u63A8\u65AD\u5E94\u7528\u5305\u540D"
1375
- };
1376
- }
1377
- const appName = xiaomiConfig.appName || this.inferAppName(packageName);
1378
- if (!appName) {
1379
- return {
1380
- platform: "xiaomi",
1381
- success: false,
1382
- message: "\u7F3A\u5C11 appName \u914D\u7F6E, \u4E14\u65E0\u6CD5\u4ECE\u5305\u540D\u63A8\u65AD\u5E94\u7528\u540D\u79F0"
1383
- };
2130
+ throw new Error("\u65E0\u6CD5\u83B7\u53D6 RSA \u516C\u94A5");
1384
2131
  }
1385
- logger.info("\u5F00\u59CB\u4E0A\u4F20\u5230\u5C0F\u7C73\u5E94\u7528\u5546\u5E97...");
1386
- logger.info(`\u6587\u4EF6: ${filePath}`);
1387
- logger.info(`\u5305\u540D: ${packageName}`);
1388
- logger.info(`\u5E94\u7528\u540D: ${appName}`);
1389
2132
  const publicKeyObj = this.extractPublicKey(resolvedPublicKey);
1390
- const configUploadType = xiaomiConfig.uploadType || "auto";
1391
- let uploadType;
1392
- let existingAppInfo = null;
1393
- if (configUploadType === "auto") {
1394
- logger.info("\u6B63\u5728\u67E5\u8BE2\u5E94\u7528\u4FE1\u606F...");
1395
- const status = await this.queryAppStatus(userName, packageName, privateKey, publicKeyObj);
1396
- uploadType = status.type;
1397
- existingAppInfo = status.appInfo;
1398
- } else {
1399
- uploadType = configUploadType;
1400
- }
1401
- const uploadTypeDesc = uploadType === "update" ? "\u5E94\u7528\u66F4\u65B0 (synchroType=1)" : "\u65B0\u589E\u5E94\u7528 (synchroType=0)";
1402
- logger.info(`\u4E0A\u4F20\u7C7B\u578B: ${uploadTypeDesc}`);
1403
- if (uploadType === "new" && xiaomiConfig.uploadType === "new") {
1404
- const missing = [];
1405
- if (xiaomiConfig.category == null) missing.push("category (\u5206\u7C7BID)");
1406
- if (!xiaomiConfig.desc) missing.push("desc (\u5E94\u7528\u4ECB\u7ECD)");
1407
- if (missing.length > 0) {
1408
- return {
1409
- platform: "xiaomi",
1410
- success: false,
1411
- message: `\u9996\u6B21\u63D0\u5BA1\u6A21\u5F0F\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5: ${missing.join(", ")}\u3002\u8BF7\u5728\u540E\u53F0\u8865\u5168\u6216\u5728\u914D\u7F6E\u4E2D\u6DFB\u52A0\u3002`
1412
- };
1413
- }
1414
- }
2133
+ const anyPath = Object.values(artifacts)[0];
2134
+ const packageName = xiaomiConfig.packageName || this.inferPackageName(anyPath);
2135
+ if (!packageName) throw new Error("\u7F3A\u5C11 packageName");
2136
+ const appName = xiaomiConfig.appName || this.inferAppName(packageName);
1415
2137
  try {
1416
- const retryStrategy = createUploadRetryStrategy(retryConfig);
2138
+ logger.info(`\u5F00\u59CB\u4E0A\u4F20\u5230\u5C0F\u7C73\u5E94\u7528\u5546\u5E97 (\u5305\u542B ${Object.keys(artifacts).length} \u4E2A\u5305)...`);
2139
+ let uploadType = xiaomiConfig.uploadType || "auto";
2140
+ let finalUploadType;
2141
+ let existingAppInfo = null;
2142
+ if (uploadType === "auto") {
2143
+ const status = await this.queryAppStatus(userName, packageName, privateKey, publicKeyObj);
2144
+ finalUploadType = status.type;
2145
+ existingAppInfo = status.appInfo;
2146
+ } else {
2147
+ finalUploadType = uploadType;
2148
+ }
2149
+ const retryStrategy = createUploadRetryStrategy(config.retry);
1417
2150
  const result = await retryStrategy.execute(
1418
2151
  () => this.performUpload({
1419
- filePath,
2152
+ artifacts,
1420
2153
  userName,
1421
2154
  privateKey,
1422
2155
  publicKey: resolvedPublicKey,
2156
+ uploadType: finalUploadType,
1423
2157
  packageName,
1424
- appName,
2158
+ appName: appName || "",
1425
2159
  updateDesc,
1426
- uploadType,
1427
2160
  category: xiaomiConfig.category || existingAppInfo?.category,
1428
2161
  keyWords: xiaomiConfig.keyWords || existingAppInfo?.keyWords,
1429
2162
  desc: xiaomiConfig.desc || existingAppInfo?.desc,
@@ -1433,50 +2166,19 @@ ${lines.join("\n")}
1433
2166
  }),
1434
2167
  "Xiaomi push upload"
1435
2168
  );
1436
- if (!result.success) {
1437
- throw result.error || new Error("Upload failed");
1438
- }
1439
- logger.success("\u4E0A\u4F20\u5230\u5C0F\u7C73\u5E94\u7528\u5546\u5E97\u6210\u529F!");
1440
2169
  return {
1441
2170
  platform: "xiaomi",
1442
2171
  success: true,
1443
- message: "\u4E0A\u4F20\u6210\u529F, \u7B49\u5F85\u5BA1\u6838",
1444
- data: result.data,
2172
+ message: "\u4E0A\u4F20\u6210\u529F",
1445
2173
  consoleUrl: "https://dev.mi.com/distribute/app/details"
1446
2174
  };
1447
2175
  } catch (error) {
1448
- logger.error(`\u4E0A\u4F20\u5230\u5C0F\u7C73\u5931\u8D25: ${error.message}`);
1449
- if (error.response?.data) {
1450
- logger.info(`\u9519\u8BEF\u8BE6\u60C5: ${JSON.stringify(error.response.data)}`);
1451
- }
1452
- return {
1453
- platform: "xiaomi",
1454
- success: false,
1455
- message: error.message,
1456
- error
1457
- };
2176
+ logger.error(`\u5C0F\u7C73\u4E0A\u4F20\u5931\u8D25: ${error.message}`);
2177
+ return { platform: "xiaomi", success: false, message: error.message, error };
1458
2178
  }
1459
2179
  }
1460
- /**
1461
- * 执行实际的上传操作
1462
- */
1463
2180
  async performUpload(params) {
1464
- const {
1465
- filePath,
1466
- userName,
1467
- privateKey,
1468
- publicKey,
1469
- packageName,
1470
- appName,
1471
- updateDesc,
1472
- uploadType,
1473
- category,
1474
- keyWords,
1475
- desc,
1476
- brief,
1477
- privacyUrl,
1478
- assets
1479
- } = params;
2181
+ const { artifacts, userName, privateKey, publicKey, packageName, appName, updateDesc, uploadType, category, keyWords, desc, brief, privacyUrl, assets } = params;
1480
2182
  const publicKeyObj = this.extractPublicKey(publicKey);
1481
2183
  const requestData = this.buildRequestData({
1482
2184
  userName,
@@ -1490,67 +2192,55 @@ ${lines.join("\n")}
1490
2192
  brief,
1491
2193
  privacyUrl
1492
2194
  });
1493
- logger.info("RequestData \u5DF2\u6784\u5EFA");
1494
- logger.info(`RequestData: ${requestData}`);
1495
2195
  const requestDataMD5 = this.calculateStringMD5(requestData);
1496
- const apkFileMD5 = await this.calculateFileMD5(filePath);
1497
- logger.info(`APK MD5: ${apkFileMD5}`);
2196
+ let mainPath;
2197
+ let secondPath = void 0;
2198
+ const artifactEntries = Object.entries(artifacts);
2199
+ const arm64 = artifactEntries.find(([abi]) => abi === "arm64-v8a");
2200
+ const arm32 = artifactEntries.find(([abi]) => abi === "armeabi-v7a");
2201
+ if (arm64 && arm32) {
2202
+ mainPath = arm64[1];
2203
+ secondPath = arm32[1];
2204
+ logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u67B6\u6784\u5206\u5305: \u4E3B\u5305(64\u4F4D)=${path4.basename(mainPath)}, \u6B21\u5305(32\u4F4D)=${path4.basename(secondPath)}`);
2205
+ } else {
2206
+ mainPath = artifactEntries[0][1];
2207
+ if (artifactEntries.length > 1) {
2208
+ secondPath = artifactEntries[1][1];
2209
+ logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u4E2A\u5305\uFF08\u975E\u6807\u51C6\u540D\uFF09\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u987A\u5E8F: \u4E3B\u5305=${path4.basename(mainPath)}, \u6B21\u5305=${path4.basename(secondPath)}`);
2210
+ } else {
2211
+ logger.info(`[Xiaomi] \u4F7F\u7528\u5355\u5305\u6A21\u5F0F: ${path4.basename(mainPath)}`);
2212
+ }
2213
+ }
2214
+ const mainMD5 = await this.calculateFileMD5(mainPath);
1498
2215
  const extraSigItems = [];
1499
- let iconPath;
2216
+ if (secondPath) {
2217
+ const secondMD5 = await this.calculateFileMD5(secondPath);
2218
+ extraSigItems.push({ name: "secondApk", hash: secondMD5 });
2219
+ }
1500
2220
  if (assets?.icon && fs5.existsSync(assets.icon)) {
1501
- iconPath = assets.icon;
1502
- const iconMD5 = await this.calculateFileMD5(iconPath);
1503
- extraSigItems.push({ name: "icon", hash: iconMD5 });
1504
- }
1505
- const screenshotPaths = [];
1506
- if (assets?.screenshots) {
1507
- for (let i = 0; i < assets.screenshots.length && i < 4; i++) {
1508
- const ssPath = assets.screenshots[i];
1509
- if (fs5.existsSync(ssPath)) {
1510
- screenshotPaths.push(ssPath);
1511
- const ssMD5 = await this.calculateFileMD5(ssPath);
1512
- extraSigItems.push({ name: `screenshot_${i + 1}`, hash: ssMD5 });
1513
- }
1514
- }
2221
+ extraSigItems.push({ name: "icon", hash: await this.calculateFileMD5(assets.icon) });
1515
2222
  }
1516
- const sig = this.generateUploadSIG(
1517
- requestDataMD5,
1518
- apkFileMD5,
1519
- privateKey,
1520
- publicKeyObj,
1521
- extraSigItems.length > 0 ? extraSigItems : void 0
1522
- );
1523
- logger.info("SIG \u7B7E\u540D\u5DF2\u751F\u6210");
2223
+ const sig = this.generateUploadSIG(requestDataMD5, mainMD5, privateKey, publicKeyObj, extraSigItems);
1524
2224
  const form = new FormData3();
1525
2225
  form.append("RequestData", requestData);
1526
2226
  form.append("SIG", sig);
1527
- form.append("apk", fs5.createReadStream(filePath));
1528
- if (iconPath) {
1529
- form.append("icon", fs5.createReadStream(iconPath));
1530
- logger.info(`\u9644\u52A0 icon: ${iconPath}`);
2227
+ form.append("apk", fs5.createReadStream(mainPath));
2228
+ if (secondPath) {
2229
+ form.append("secondApk", fs5.createReadStream(secondPath));
1531
2230
  }
1532
- for (let i = 0; i < screenshotPaths.length; i++) {
1533
- form.append(`screenshot_${i + 1}`, fs5.createReadStream(screenshotPaths[i]));
1534
- logger.info(`\u9644\u52A0 screenshot_${i + 1}: ${screenshotPaths[i]}`);
2231
+ if (assets?.icon && fs5.existsSync(assets.icon)) {
2232
+ form.append("icon", fs5.createReadStream(assets.icon));
1535
2233
  }
1536
- logger.info(`POST ${_XiaomiUploader.PUSH_API}`);
1537
2234
  const response = await axios3.post(_XiaomiUploader.PUSH_API, form, {
1538
- headers: {
1539
- ...form.getHeaders()
1540
- },
2235
+ headers: form.getHeaders(),
1541
2236
  maxContentLength: Infinity,
1542
2237
  maxBodyLength: Infinity,
1543
- timeout: 6e4
1544
- // 60 秒请求超时
2238
+ timeout: 12e4
1545
2239
  });
1546
- const data = response.data;
1547
- logger.info(`\u5C0F\u7C73 API \u539F\u59CB\u54CD\u5E94: ${JSON.stringify(data)}`);
1548
- if (data?.result !== 0) {
1549
- const errMsg = data?.message || data?.msg || JSON.stringify(data);
1550
- throw new Error(`\u5C0F\u7C73 API \u8FD4\u56DE\u9519\u8BEF (result=${data?.result}): ${errMsg}`);
2240
+ if (response.data?.result !== 0) {
2241
+ throw new Error(`\u5C0F\u7C73 API \u9519\u8BEF: ${response.data?.message || JSON.stringify(response.data)}`);
1551
2242
  }
1552
- logger.info(`\u5C0F\u7C73 API \u54CD\u5E94: ${JSON.stringify(data)}`);
1553
- return data;
2243
+ return response.data;
1554
2244
  }
1555
2245
  /**
1556
2246
  * 解析公钥: 优先使用内容字段, 其次从文件路径读取
@@ -1592,7 +2282,7 @@ ${lines.join("\n")}
1592
2282
 
1593
2283
  // src/adapters/apple_store_uploader.ts
1594
2284
  import fs6 from "fs-extra";
1595
- import path3 from "path";
2285
+ import path5 from "path";
1596
2286
  import { exec as exec2 } from "child_process";
1597
2287
  import { promisify as promisify2 } from "util";
1598
2288
  import axios4 from "axios";
@@ -1758,7 +2448,7 @@ var AppleStoreUploader = class {
1758
2448
  appPassword,
1759
2449
  "-showTransferRate"
1760
2450
  ].join(" ");
1761
- logger.info(`[Transporter] Uploading: ${path3.basename(ipaPath)}`);
2451
+ logger.info(`[Transporter] Uploading: ${path5.basename(ipaPath)}`);
1762
2452
  try {
1763
2453
  const { stdout, stderr } = await execAsync(command, {
1764
2454
  timeout: 30 * 60 * 1e3,
@@ -1809,7 +2499,7 @@ Stderr: ${error.stderr || ""}`);
1809
2499
  });
1810
2500
  command = innerArgs.join(" ");
1811
2501
  }
1812
- logger.info(`[altool] Uploading: ${path3.basename(ipaPath)}`);
2502
+ logger.info(`[altool] Uploading: ${path5.basename(ipaPath)}`);
1813
2503
  logger.info(`[altool] Command prefix: ${tool.command}`);
1814
2504
  try {
1815
2505
  const { stdout, stderr } = await execAsync(command, {
@@ -2393,7 +3083,7 @@ var TestFlightUploader = class _TestFlightUploader {
2393
3083
 
2394
3084
  // src/adapters/adhoc_uploader.ts
2395
3085
  import fs8 from "fs-extra";
2396
- import path4 from "path";
3086
+ import path6 from "path";
2397
3087
  import { execSync } from "child_process";
2398
3088
  var AdHocUploader = class {
2399
3089
  name = "AdHoc";
@@ -2526,8 +3216,8 @@ var AdHocUploader = class {
2526
3216
  * @param changelog 更新日志
2527
3217
  */
2528
3218
  async createManifest(ipaPath, version, bundleId, changelog) {
2529
- const ipaFileName = path4.basename(ipaPath);
2530
- const appName = bundleId || path4.parse(ipaFileName).name;
3219
+ const ipaFileName = path6.basename(ipaPath);
3220
+ const appName = bundleId || path6.parse(ipaFileName).name;
2531
3221
  const ipaUrl = `itms-services://?action=download-manifest&url=https://your-distribution-server.com/manifests/${ipaFileName}.plist`;
2532
3222
  const manifestContent = `<?xml version="1.0" encoding="UTF-8"?>
2533
3223
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -2586,14 +3276,14 @@ var AdHocUploader = class {
2586
3276
  */
2587
3277
  async setupDistributionDirectory(ipaPath, manifestPath) {
2588
3278
  const projectRoot = process.cwd();
2589
- const distributionDir = path4.join(projectRoot, "build", "adhoc-distribution");
3279
+ const distributionDir = path6.join(projectRoot, "build", "adhoc-distribution");
2590
3280
  await fs8.ensureDir(distributionDir);
2591
- await fs8.ensureDir(path4.join(distributionDir, "apps"));
2592
- await fs8.ensureDir(path4.join(distributionDir, "manifests"));
2593
- await fs8.ensureDir(path4.join(distributionDir, "icons"));
2594
- const ipaFileName = path4.basename(ipaPath);
2595
- await fs8.copy(ipaPath, path4.join(distributionDir, "apps", ipaFileName));
2596
- await fs8.copy(manifestPath, path4.join(distributionDir, "manifests", `${ipaFileName}.plist`));
3281
+ await fs8.ensureDir(path6.join(distributionDir, "apps"));
3282
+ await fs8.ensureDir(path6.join(distributionDir, "manifests"));
3283
+ await fs8.ensureDir(path6.join(distributionDir, "icons"));
3284
+ const ipaFileName = path6.basename(ipaPath);
3285
+ await fs8.copy(ipaPath, path6.join(distributionDir, "apps", ipaFileName));
3286
+ await fs8.copy(manifestPath, path6.join(distributionDir, "manifests", `${ipaFileName}.plist`));
2597
3287
  logger.info(`Distribution directory: ${distributionDir}`);
2598
3288
  return distributionDir;
2599
3289
  }
@@ -2603,7 +3293,7 @@ var AdHocUploader = class {
2603
3293
  * @param bundleId Bundle ID
2604
3294
  */
2605
3295
  generateInstallUrl(distributionPath, bundleId) {
2606
- const manifestPath = path4.join(distributionPath, "manifests", `*.plist`);
3296
+ const manifestPath = path6.join(distributionPath, "manifests", `*.plist`);
2607
3297
  return `itms-services://?action=download-manifest&url=file://${manifestPath}`;
2608
3298
  }
2609
3299
  /**
@@ -2612,7 +3302,7 @@ var AdHocUploader = class {
2612
3302
  * @param distributionPath 分发目录
2613
3303
  */
2614
3304
  async generateQRCode(url, distributionPath) {
2615
- const qrCodePath = path4.join(distributionPath, "install-qrcode.png");
3305
+ const qrCodePath = path6.join(distributionPath, "install-qrcode.png");
2616
3306
  try {
2617
3307
  logger.info(`QR code would be saved to: ${qrCodePath}`);
2618
3308
  logger.info(`Install URL: ${url}`);
@@ -2639,8 +3329,8 @@ var AdHocUploader = class {
2639
3329
  logger.newLine();
2640
3330
  logger.info("\u{1F4E6} Distribution Files:");
2641
3331
  logger.info(` Directory: ${distributionPath}`);
2642
- logger.info(` IPA: ${path4.join(distributionPath, "apps", "*.ipa")}`);
2643
- logger.info(` Manifest: ${path4.join(distributionPath, "manifests", "*.plist")}`);
3332
+ logger.info(` IPA: ${path6.join(distributionPath, "apps", "*.ipa")}`);
3333
+ logger.info(` Manifest: ${path6.join(distributionPath, "manifests", "*.plist")}`);
2644
3334
  logger.info(` QR Code: ${qrCodePath}`);
2645
3335
  logger.newLine();
2646
3336
  logger.info("\u{1F517} Installation Link:");
@@ -2699,7 +3389,7 @@ var AdHocUploader = class {
2699
3389
 
2700
3390
  // src/adapters/harmony_uploader.ts
2701
3391
  import fs9 from "fs-extra";
2702
- import path5 from "path";
3392
+ import path7 from "path";
2703
3393
  import axios6 from "axios";
2704
3394
  import FormData5 from "form-data";
2705
3395
  var HarmonyUploader = class _HarmonyUploader {
@@ -2729,7 +3419,7 @@ var HarmonyUploader = class _HarmonyUploader {
2729
3419
  const clientSecret = harmonyConfig.auth.clientSecret;
2730
3420
  const appId = harmonyConfig.appId;
2731
3421
  const releaseType = harmonyConfig.releaseType || "draft";
2732
- const suffix = path5.extname(filePath).substring(1);
3422
+ const suffix = path7.extname(filePath).substring(1);
2733
3423
  const retryConfig = config.retry;
2734
3424
  logger.info("Starting HarmonyOS AppGallery upload...");
2735
3425
  try {
@@ -2846,7 +3536,7 @@ var HarmonyUploader = class _HarmonyUploader {
2846
3536
  * @param filePath 文件路径
2847
3537
  */
2848
3538
  async updateAppFileInfo(token, appId, authCode, filePath) {
2849
- const fileName = path5.basename(filePath);
3539
+ const fileName = path7.basename(filePath);
2850
3540
  const body = {
2851
3541
  fileType: 7,
2852
3542
  // 鸿蒙包的 fileType(需要根据华为最新文档确认)
@@ -2869,7 +3559,7 @@ var HarmonyUploader = class _HarmonyUploader {
2869
3559
 
2870
3560
  // src/adapters/oppo_uploader.ts
2871
3561
  import fs10 from "fs-extra";
2872
- import path6 from "path";
3562
+ import path8 from "path";
2873
3563
  import axios7 from "axios";
2874
3564
  import FormData6 from "form-data";
2875
3565
  import crypto4 from "crypto";
@@ -2910,7 +3600,7 @@ var OppoUploader = class _OppoUploader {
2910
3600
  * 尝试从文件名推断包名
2911
3601
  */
2912
3602
  inferPackageName(filePath) {
2913
- const fileName = path6.basename(filePath);
3603
+ const fileName = path8.basename(filePath);
2914
3604
  const match = fileName.match(/^([\w.]+?)(?:-(?:release|debug|unsigned))?\.apk$/i);
2915
3605
  if (match && match[1].includes(".")) {
2916
3606
  return match[1];
@@ -2928,110 +3618,47 @@ var OppoUploader = class _OppoUploader {
2928
3618
  }
2929
3619
  async upload(filePath, config, metadata) {
2930
3620
  const oppoConfig = config.publish.oppo;
2931
- const clientId = oppoConfig.auth.clientId;
2932
- const clientSecret = oppoConfig.auth.clientSecret;
3621
+ const accessToken = await this.getAccessToken(oppoConfig.auth.clientId, oppoConfig.auth.clientSecret);
2933
3622
  const packageName = oppoConfig.packageName || this.inferPackageName(filePath);
2934
- if (!packageName) {
2935
- return {
2936
- platform: "oppo",
2937
- success: false,
2938
- message: "\u65E0\u6CD5\u83B7\u53D6\u5305\u540D (packageName)\uFF0C\u8BF7\u5728\u914D\u7F6E\u4E2D\u6307\u5B9A\u6216\u786E\u4FDD\u6587\u4EF6\u540D\u5305\u542B\u5305\u540D",
2939
- consoleUrl: "https://open.oppomobile.com/new/ecology/app"
2940
- };
2941
- }
2942
- const retryConfig = config.retry;
2943
3623
  const consoleUrl = "https://open.oppomobile.com/new/ecology/app";
2944
- if (!fs10.existsSync(filePath)) {
2945
- return {
2946
- platform: "oppo",
2947
- success: false,
2948
- message: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`,
2949
- consoleUrl
2950
- };
3624
+ if (!packageName) {
3625
+ return { platform: "oppo", success: false, message: "\u65E0\u6CD5\u83B7\u53D6\u5305\u540D", consoleUrl };
2951
3626
  }
2952
- logger.info("\u5F00\u59CB\u4E0A\u4F20\u5230 OPPO \u5E94\u7528\u5546\u5E97...");
2953
- logger.info(`\u6587\u4EF6: ${filePath}`);
3627
+ const artifacts = typeof metadata.artifactPath === "object" ? metadata.artifactPath : { universal: filePath || (typeof metadata.artifactPath === "string" ? metadata.artifactPath : "") };
2954
3628
  try {
2955
- const retryStrategy = createUploadRetryStrategy(retryConfig);
2956
- logger.info("\u6B63\u5728\u83B7\u53D6 access_token...");
2957
- const tokenResult = await retryStrategy.execute(
2958
- () => this.getAccessToken(clientId, clientSecret),
2959
- "OPPO authentication"
2960
- );
2961
- if (!tokenResult.success || !tokenResult.data) {
2962
- throw tokenResult.error || new Error("Failed to get access token");
2963
- }
2964
- const accessToken = tokenResult.data;
2965
- logger.info("\u6B63\u5728\u83B7\u53D6\u73B0\u6709\u5E94\u7528\u8BE6\u60C5...");
2966
- const appInfoResult = await retryStrategy.execute(
2967
- () => this.getAppInfo(accessToken, clientSecret, packageName),
2968
- "OPPO get app info"
2969
- );
2970
- const onlineAppInfo = appInfoResult.data || {};
2971
- if (onlineAppInfo) {
2972
- logger.info(
2973
- `\u83B7\u53D6\u5230\u7EBF\u4E0A\u4FE1\u606F: ${onlineAppInfo.app_name} (Category: ${onlineAppInfo.second_category_id})`
2974
- );
2975
- logger.info(`[OPPO AppInfo Full] ${JSON.stringify(onlineAppInfo)}`);
2976
- }
2977
- logger.info("\u6B63\u5728\u83B7\u53D6\u4E0A\u4F20\u5730\u5740...");
2978
- const uploadUrlResult = await retryStrategy.execute(
2979
- () => this.getUploadUrl(accessToken, clientSecret),
2980
- "OPPO get upload URL"
2981
- );
2982
- if (!uploadUrlResult.success || !uploadUrlResult.data) {
2983
- throw uploadUrlResult.error || new Error("Failed to get upload URL");
2984
- }
2985
- const { uploadUrl, sign: uploadSign } = uploadUrlResult.data;
2986
- logger.info("\u6B63\u5728\u4E0A\u4F20 APK \u6587\u4EF6...");
2987
- const uploadFileResult = await retryStrategy.execute(
2988
- () => this.uploadFile(uploadUrl, uploadSign, filePath),
2989
- "OPPO file upload"
2990
- );
2991
- if (!uploadFileResult.success || !uploadFileResult.data) {
2992
- throw uploadFileResult.error || new Error("File upload failed");
2993
- }
2994
- const rawUrl = uploadFileResult.data.url;
2995
- const fileUrl = rawUrl.replace(/^http:\/\//i, "https://").replace("storedl1.nearme.com.cn", "storedl.nearme.com.cn");
2996
- const md5 = uploadFileResult.data.md5;
2997
- logger.info("\u6B63\u5728\u63D0\u4EA4\u7248\u672C\u4FE1\u606F...");
2998
- const submitResult = await retryStrategy.execute(
2999
- () => this.submitVersion({
3000
- accessToken,
3001
- clientSecret,
3002
- packageName,
3003
- appName: oppoConfig.appName || metadata.appName || onlineAppInfo.app_name || this.inferAppName(packageName),
3004
- fileUrl,
3005
- fileMd5: md5,
3006
- versionName: metadata.version,
3007
- versionCode: metadata.buildNumber,
3008
- updateDesc: metadata.changelog || "App Update",
3009
- onlineType: oppoConfig.onlineType,
3010
- // 从配置读取
3011
- onlineAppInfo
3012
- // 传入线上信息
3013
- }),
3014
- "OPPO submit version"
3015
- );
3016
- if (!submitResult.success) {
3017
- throw submitResult.error || new Error("Submit version failed");
3018
- }
3629
+ const retryStrategy = createUploadRetryStrategy(config.retry);
3630
+ const onlineAppInfo = await this.getAppInfo(accessToken, oppoConfig.auth.clientSecret, packageName) || {};
3631
+ const uploadResults = [];
3632
+ const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
3633
+ if (!p || !fs10.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
3634
+ logger.info(`[OPPO] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305...`);
3635
+ const { uploadUrl, sign } = await this.getUploadUrl(accessToken, oppoConfig.auth.clientSecret);
3636
+ const uploadFileResult = await this.uploadFile(uploadUrl, sign, p);
3637
+ const fileUrl = uploadFileResult.url.replace(/^http:\/\//i, "https://").replace("storedl1.nearme.com.cn", "storedl.nearme.com.cn");
3638
+ let cpu_code = 0;
3639
+ if (abi === "arm64-v8a") cpu_code = 3;
3640
+ else if (abi === "armeabi-v7a") cpu_code = 4;
3641
+ uploadResults.push({ url: fileUrl, md5: uploadFileResult.md5, cpu_code });
3642
+ });
3643
+ await Promise.all(uploadPromises);
3644
+ logger.info(`[OPPO] \u6B63\u5728\u63D0\u4EA4\u6D41\u6C34\u53F7\u7EC4: ${JSON.stringify(uploadResults)}`);
3645
+ await this.submitVersion({
3646
+ accessToken,
3647
+ clientSecret: oppoConfig.auth.clientSecret,
3648
+ packageName,
3649
+ appName: oppoConfig.appName || onlineAppInfo.app_name || metadata.appName || this.inferAppName(packageName),
3650
+ apkUrls: uploadResults,
3651
+ versionName: metadata.version,
3652
+ versionCode: metadata.buildNumber,
3653
+ updateDesc: metadata.changelog || "App Update",
3654
+ onlineType: oppoConfig.onlineType,
3655
+ onlineAppInfo
3656
+ });
3019
3657
  logger.success("\u4E0A\u4F20\u5230 OPPO \u5E94\u7528\u5546\u5E97\u6210\u529F!");
3020
- return {
3021
- platform: "oppo",
3022
- success: true,
3023
- message: "\u4E0A\u4F20\u6210\u529F, \u8BF7\u524D\u5F80\u540E\u53F0\u786E\u8BA4\u5E76\u63D0\u4EA4\u5BA1\u6838",
3024
- consoleUrl
3025
- };
3658
+ return { platform: "oppo", success: true, message: "\u4E0A\u4F20\u6210\u529F, \u8BF7\u786E\u8BA4", consoleUrl };
3026
3659
  } catch (error) {
3027
3660
  logger.error(`\u4E0A\u4F20\u5230 OPPO \u5931\u8D25: ${error.message}`);
3028
- return {
3029
- platform: "oppo",
3030
- success: false,
3031
- message: this.formatError(error),
3032
- error,
3033
- consoleUrl
3034
- };
3661
+ return { platform: "oppo", success: false, message: error.message, error, consoleUrl };
3035
3662
  }
3036
3663
  }
3037
3664
  /**
@@ -3152,16 +3779,7 @@ var OppoUploader = class _OppoUploader {
3152
3779
  const signParams = {
3153
3780
  access_token: params.accessToken,
3154
3781
  timestamp,
3155
- // OPPO 官方要求 apk_url 为 JSON 数组格式:[{url, md5, cpu_code}]
3156
- // cpu_code: 0=通用包, 3=arm64-v8a, 4=armeabi-v7a, 5=x86_64
3157
- apk_url: JSON.stringify([
3158
- {
3159
- url: params.fileUrl,
3160
- md5: params.fileMd5,
3161
- cpu_code: 0
3162
- // 通用包
3163
- }
3164
- ]),
3782
+ apk_url: JSON.stringify(params.apkUrls),
3165
3783
  version_name: params.versionName.split("+")[0],
3166
3784
  // 仅保留版本名部分
3167
3785
  version_code: vCode,
@@ -3263,7 +3881,7 @@ var OppoUploader = class _OppoUploader {
3263
3881
 
3264
3882
  // src/adapters/vivo_uploader.ts
3265
3883
  import fs11 from "fs-extra";
3266
- import path7 from "path";
3884
+ import path9 from "path";
3267
3885
  import axios8 from "axios";
3268
3886
  import FormData7 from "form-data";
3269
3887
  import crypto5 from "crypto";
@@ -3271,26 +3889,14 @@ var VivoUploader = class _VivoUploader {
3271
3889
  name = "vivo";
3272
3890
  /** vivo 开放 API 基础地址 */
3273
3891
  static API_BASE = "https://developer-api.vivo.com.cn/router/rest";
3274
- /**
3275
- * vivo API 签名生成
3276
- * 规则: 所有参数按 ASCII 码排序后拼接为 key=val&... 字符串,
3277
- * 以 accessSecret 为密钥进行 HMAC-SHA256 加密,结果转为小写 16 进制字符串
3278
- */
3279
3892
  generateSign(params, accessSecret) {
3280
3893
  const sorted = Object.keys(params).sort().filter((key) => params[key] !== void 0 && params[key] !== null).map((key) => `${key}=${params[key]}`).join("&");
3281
- const sign = crypto5.createHmac("sha256", accessSecret).update(sorted).digest("hex");
3282
- logger.info(`[VIVO DEBUG] \u5F85\u7B7E\u540D\u4E32: ${sorted}
3283
- -> \u7B7E\u540D\u7ED3\u679C: ${sign}`);
3284
- return sign;
3894
+ return crypto5.createHmac("sha256", accessSecret).update(sorted).digest("hex");
3285
3895
  }
3286
3896
  validateConfig(config) {
3287
3897
  if (!config?.publish?.vivo?.enable) return false;
3288
3898
  const vivo = config.publish.vivo;
3289
- if (!vivo.auth?.accessKey || !vivo.auth?.accessSecret) {
3290
- logger.error("vivo config missing: accessKey or accessSecret");
3291
- return false;
3292
- }
3293
- return true;
3899
+ return !!(vivo.auth?.accessKey && vivo.auth?.accessSecret);
3294
3900
  }
3295
3901
  canHandle(filePath) {
3296
3902
  const ext = filePath.split(".").pop()?.toLowerCase();
@@ -3300,67 +3906,51 @@ var VivoUploader = class _VivoUploader {
3300
3906
  const vivoConfig = config.publish.vivo;
3301
3907
  const accessKey = vivoConfig.auth.accessKey;
3302
3908
  const accessSecret = vivoConfig.auth.accessSecret;
3303
- const retryConfig = config.retry;
3304
3909
  const consoleUrl = "https://dev.vivo.com.cn/manage/app";
3305
- if (!fs11.existsSync(filePath)) {
3306
- return {
3307
- platform: "vivo",
3308
- success: false,
3309
- message: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`,
3310
- consoleUrl
3311
- };
3312
- }
3313
- const fileSize = fs11.statSync(filePath).size;
3314
- logger.info("\u5F00\u59CB\u4E0A\u4F20\u5230 vivo \u5E94\u7528\u5546\u5E97...");
3315
- logger.info(`\u6587\u4EF6: ${filePath} (${(fileSize / 1024 / 1024).toFixed(2)} MB)`);
3910
+ const artifacts = typeof metadata.artifactPath === "object" ? metadata.artifactPath : { universal: filePath || (typeof metadata.artifactPath === "string" ? metadata.artifactPath : "") };
3316
3911
  try {
3317
- const retryStrategy = createUploadRetryStrategy(retryConfig);
3318
- const fileMd5 = await this.calculateFileMD5(filePath);
3319
- logger.info("\u6B63\u5728\u4E0A\u4F20 APK \u6587\u4EF6...");
3320
- const packageName = vivoConfig.packageName || "";
3321
- const uploadFileResult = await retryStrategy.execute(
3322
- () => this.uploadApk(accessKey, accessSecret, filePath, packageName, fileMd5),
3323
- "vivo file upload"
3324
- );
3325
- if (!uploadFileResult.success || !uploadFileResult.data) {
3326
- throw uploadFileResult.error || new Error("File upload failed or no serialnumber returned");
3912
+ const uploadResults = {};
3913
+ const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
3914
+ if (!p || !fs11.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
3915
+ const md5 = await this.calculateFileMD5(p);
3916
+ logger.info(`[vivo] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${path9.basename(p)}`);
3917
+ const serialnumber = await this.uploadApk(accessKey, accessSecret, p, vivoConfig.packageName || "", md5);
3918
+ const pkgInfo = await parsePackageInfo(p, { androidSdkPath: config.globalSettings?.androidSdkPath });
3919
+ const nativeCodes = pkgInfo?.nativeCode || [];
3920
+ const has64Bit = nativeCodes.includes("arm64-v8a") || nativeCodes.includes("x86_64") || abi === "arm64-v8a";
3921
+ const has32Bit = nativeCodes.includes("armeabi-v7a") || nativeCodes.includes("armeabi") || abi === "armeabi-v7a";
3922
+ uploadResults[abi] = { serialnumber, md5, is64Bit: has64Bit && !has32Bit };
3923
+ });
3924
+ await Promise.all(uploadPromises);
3925
+ const arm64Result = uploadResults["arm64-v8a"] || Object.values(uploadResults).find((r) => r.is64Bit);
3926
+ const arm32Result = uploadResults["armeabi-v7a"] || Object.values(uploadResults).find((r) => !r.is64Bit);
3927
+ const submitData = {
3928
+ packageName: vivoConfig.packageName,
3929
+ versionCode: parseInt(metadata.buildNumber) || 1,
3930
+ updateDesc: metadata.changelog || "App Update"
3931
+ };
3932
+ if (arm64Result) {
3933
+ submitData.serialnumber = arm64Result.serialnumber;
3934
+ submitData.fileMd5 = arm64Result.md5;
3327
3935
  }
3328
- const serialnumber = uploadFileResult.data;
3329
- logger.info("\u6B63\u5728\u63D0\u4EA4\u7248\u672C\u4FE1\u606F...");
3330
- const submitResult = await retryStrategy.execute(
3331
- () => this.submitVersion(accessKey, accessSecret, {
3332
- serialnumber,
3333
- packageName: vivoConfig.packageName,
3334
- versionCode: parseInt(metadata.buildNumber) || 1,
3335
- fileMd5,
3336
- updateDesc: metadata.changelog || "App Update"
3337
- }),
3338
- "vivo submit version"
3339
- );
3340
- if (!submitResult.success) {
3341
- throw submitResult.error || new Error("Submit version failed");
3936
+ if (arm32Result) {
3937
+ submitData.apk32 = arm32Result.serialnumber;
3938
+ submitData.md532 = arm32Result.md5;
3342
3939
  }
3940
+ if (!submitData.serialnumber && !submitData.apk32) {
3941
+ const single = Object.values(uploadResults)[0];
3942
+ submitData.serialnumber = single.serialnumber;
3943
+ submitData.fileMd5 = single.md5;
3944
+ }
3945
+ logger.info(`[vivo] \u6B63\u5728\u63D0\u4EA4\u5BA1\u6838\u7533\u62A5: ${JSON.stringify(submitData)}`);
3946
+ await this.submitVersion(accessKey, accessSecret, submitData);
3343
3947
  logger.success("\u4E0A\u4F20\u5230 vivo \u5E94\u7528\u5546\u5E97\u6210\u529F!");
3344
- return {
3345
- platform: "vivo",
3346
- success: true,
3347
- message: "\u4E0A\u4F20\u6210\u529F, \u8BF7\u524D\u5F80\u540E\u53F0\u786E\u8BA4\u5E76\u63D0\u4EA4\u5BA1\u6838",
3348
- consoleUrl
3349
- };
3948
+ return { platform: "vivo", success: true, message: "\u4E0A\u4F20\u5E76\u63D0\u4EA4\u5BA1\u6838\u6210\u529F", consoleUrl };
3350
3949
  } catch (error) {
3351
3950
  logger.error(`\u4E0A\u4F20\u5230 vivo \u5931\u8D25: ${error.message}`);
3352
- return {
3353
- platform: "vivo",
3354
- success: false,
3355
- message: this.formatError(error),
3356
- error,
3357
- consoleUrl
3358
- };
3951
+ return { platform: "vivo", success: false, message: error.message, error, consoleUrl };
3359
3952
  }
3360
3953
  }
3361
- /**
3362
- * 计算文件 MD5
3363
- */
3364
3954
  calculateFileMD5(filePath) {
3365
3955
  return new Promise((resolve, reject) => {
3366
3956
  const hash = crypto5.createHash("md5");
@@ -3370,14 +3960,9 @@ var VivoUploader = class _VivoUploader {
3370
3960
  stream.on("error", reject);
3371
3961
  });
3372
3962
  }
3373
- /**
3374
- * 上传 APK 文件并获取流水号
3375
- * 将公共参数与业务参数放入一个 multipart 请求中直接上传至 API_BASE
3376
- */
3377
3963
  async uploadApk(accessKey, accessSecret, filePath, packageName, fileMd5) {
3378
3964
  const timestamp = String(Date.now());
3379
- const paramsForSign = {
3380
- // 公共参数
3965
+ const params = {
3381
3966
  access_key: accessKey,
3382
3967
  timestamp,
3383
3968
  format: "json",
@@ -3385,41 +3970,19 @@ var VivoUploader = class _VivoUploader {
3385
3970
  sign_method: "hmac",
3386
3971
  target_app_key: "developer",
3387
3972
  method: "app.upload.apk.app",
3388
- // 业务参数
3389
3973
  packageName,
3390
3974
  fileMd5
3391
3975
  };
3392
- const sign = this.generateSign(paramsForSign, accessSecret);
3976
+ const sign = this.generateSign(params, accessSecret);
3393
3977
  const form = new FormData7();
3394
- for (const [key, value] of Object.entries(paramsForSign)) {
3395
- form.append(key, value);
3396
- }
3978
+ Object.entries(params).forEach(([k, v]) => form.append(k, v));
3397
3979
  form.append("sign", sign);
3398
- form.append("file", fs11.createReadStream(filePath), {
3399
- filename: path7.basename(filePath),
3400
- contentType: "application/vnd.android.package-archive"
3401
- });
3402
- logger.info(
3403
- `[VIVO DEBUG] \u5F00\u59CB\u4E0A\u4F20 APK (API\u8BF7\u6C42) \u53C2\u6570(\u4E0D\u542B\u6587\u4EF6): ${JSON.stringify({ ...paramsForSign, sign })}`
3404
- );
3405
- const res = await axios8.post(_VivoUploader.API_BASE, form, {
3406
- headers: form.getHeaders(),
3407
- maxContentLength: Infinity,
3408
- maxBodyLength: Infinity,
3409
- timeout: 3e5
3410
- });
3411
- logger.info(`[VIVO DEBUG] \u4E0A\u4F20\u54CD\u5E94: ${JSON.stringify(res.data)}`);
3412
- if (res.data.code !== 0) {
3413
- throw new Error(`\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25: ${res.data.msg || JSON.stringify(res.data)}`);
3414
- }
3415
- const serialnumber = res.data.data?.serialnumber || res.data.data;
3416
- logger.info(`APK \u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210\uFF0C\u6D41\u6C34\u53F7: ${serialnumber}`);
3417
- return serialnumber;
3980
+ form.append("file", fs11.createReadStream(filePath), { filename: path9.basename(filePath) });
3981
+ const res = await axios8.post(_VivoUploader.API_BASE, form, { headers: form.getHeaders(), timeout: 3e5 });
3982
+ if (res.data.code !== 0) throw new Error(`\u4E0A\u4F20\u5305\u5931\u8D25: ${res.data.msg}`);
3983
+ return res.data.data?.serialnumber || res.data.data;
3418
3984
  }
3419
- /**
3420
- * 提交版本更新信息
3421
- */
3422
- async submitVersion(accessKey, accessSecret, params) {
3985
+ async submitVersion(accessKey, accessSecret, data) {
3423
3986
  const timestamp = String(Date.now());
3424
3987
  const reqParams = {
3425
3988
  access_key: accessKey,
@@ -3429,51 +3992,32 @@ var VivoUploader = class _VivoUploader {
3429
3992
  sign_method: "hmac",
3430
3993
  target_app_key: "developer",
3431
3994
  method: "app.sync.update.app",
3432
- packageName: params.packageName || "",
3433
- // 文档必填
3434
- versionCode: String(params.versionCode),
3435
- // 文档必填 (Integer)
3436
- apk: params.serialnumber,
3437
- // 文档中的 apk 字段即为上传接口返回的 serialnumber
3438
- fileMd5: params.fileMd5,
3439
- // 文档必填
3995
+ packageName: data.packageName || "",
3996
+ versionCode: String(data.versionCode),
3997
+ apk: data.serialnumber || "",
3998
+ fileMd5: data.fileMd5 || "",
3440
3999
  onlineType: "1",
3441
- // 1:立即上架 2:定时上架 (必填)
3442
4000
  compatibleDevice: "1",
3443
- // 1:手机 2:手机和平板 3:平板 (必填)
3444
- updateDesc: params.updateDesc
4001
+ updateDesc: data.updateDesc
3445
4002
  };
4003
+ if (data.apk32) {
4004
+ reqParams.apk32 = data.apk32;
4005
+ if (data.md532) reqParams.md532 = data.md532;
4006
+ }
3446
4007
  reqParams.sign = this.generateSign(reqParams, accessSecret);
3447
4008
  const formData = new URLSearchParams();
3448
- for (const [key, value] of Object.entries(reqParams)) {
3449
- formData.append(key, value);
3450
- }
4009
+ Object.entries(reqParams).forEach(([k, v]) => formData.append(k, v));
3451
4010
  const res = await axios8.post(_VivoUploader.API_BASE, formData.toString(), {
3452
- headers: {
3453
- "Content-Type": "application/x-www-form-urlencoded"
3454
- },
4011
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3455
4012
  timeout: 3e4
3456
4013
  });
3457
- if (res.data.code !== 0) {
3458
- throw new Error(`\u63D0\u4EA4\u7248\u672C\u5931\u8D25: ${res.data.msg || JSON.stringify(res.data)}`);
3459
- }
3460
- logger.info("\u7248\u672C\u4FE1\u606F\u5DF2\u63D0\u4EA4");
3461
- }
3462
- /**
3463
- * 格式化错误信息
3464
- */
3465
- formatError(error) {
3466
- if (error.response?.data) {
3467
- const data = error.response.data;
3468
- return data.msg || data.message || error.message;
3469
- }
3470
- return error.message;
4014
+ if (res.data.code !== 0) throw new Error(`\u63D0\u4EA4\u5931\u8D25: ${res.data.msg}`);
3471
4015
  }
3472
4016
  };
3473
4017
 
3474
4018
  // src/adapters/tencent_uploader.ts
3475
4019
  import fs12 from "fs-extra";
3476
- import path8 from "path";
4020
+ import path10 from "path";
3477
4021
  import axios9 from "axios";
3478
4022
  import crypto6 from "crypto";
3479
4023
  var TencentUploader = class _TencentUploader {
@@ -3505,54 +4049,71 @@ var TencentUploader = class _TencentUploader {
3505
4049
  async upload(filePath, config, metadata) {
3506
4050
  const tencentConfig = config.publish.tencent;
3507
4051
  const auth = tencentConfig.auth;
3508
- logger.info(`Tencent \u8FD0\u884C\u65F6\u914D\u7F6E: ${JSON.stringify(tencentConfig)}`);
3509
4052
  const consoleUrl = "https://app.open.qq.com";
3510
- if (!fs12.existsSync(filePath)) {
3511
- return { platform: "tencent", success: false, message: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`, consoleUrl };
3512
- }
4053
+ const pkgName = tencentConfig.packageName || "";
4054
+ const artifacts = typeof metadata.artifactPath === "object" ? metadata.artifactPath : { universal: filePath || (typeof metadata.artifactPath === "string" ? metadata.artifactPath : "") };
3513
4055
  try {
3514
- const stats = fs12.statSync(filePath);
3515
- const fileName = path8.basename(filePath);
3516
- logger.info("\u6B63\u5728\u83B7\u53D6\u9884\u7B7E\u540D\u4E0A\u4F20\u5730\u5740...");
3517
- const pkgName = tencentConfig.packageName || "";
3518
- const uploadInfo = await this.getFileUploadInfo(auth, stats.size, fileName, pkgName);
3519
- logger.info(`\u83B7\u53D6\u6587\u4EF6\u4E0A\u4F20\u4FE1\u606F\u7ED3\u679C ${JSON.stringify(uploadInfo)}`);
3520
- logger.info("\u6B63\u5728\u4E0A\u4F20\u6587\u4EF6\u5230\u817E\u8BAF\u4E91 COS (\u7ED5\u8FC7 API \u7F51\u5173\u9650\u5236)...");
3521
- await this.uploadToCos(uploadInfo.pre_sign_url, filePath, stats.size);
3522
- logger.info("\u6B63\u5728\u83B7\u53D6\u7EBF\u4E0A\u5E94\u7528\u8BE6\u60C5\u4EE5\u4FDD\u7559\u73B0\u6709\u914D\u7F6E...");
3523
- const pkgInfo = await parsePackageInfo(filePath, {
4056
+ const uploadResults = {};
4057
+ const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
4058
+ if (!p || !fs12.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
4059
+ const stats = fs12.statSync(p);
4060
+ const fileName = path10.basename(p);
4061
+ logger.info(`[Tencent] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${fileName}`);
4062
+ const uploadInfo = await this.getFileUploadInfo(auth, stats.size, fileName, pkgName);
4063
+ await this.uploadToCos(uploadInfo.pre_sign_url, p, stats.size);
4064
+ const pkgInfo = await parsePackageInfo(p, {
4065
+ androidSdkPath: config.globalSettings?.androidSdkPath
4066
+ });
4067
+ const nativeCodes = pkgInfo?.nativeCode || [];
4068
+ const has64Bit = nativeCodes.includes("arm64-v8a") || nativeCodes.includes("x86_64") || abi === "arm64-v8a";
4069
+ const has32Bit = nativeCodes.includes("armeabi-v7a") || nativeCodes.includes("armeabi") || nativeCodes.includes("x86") || abi === "armeabi-v7a";
4070
+ uploadResults[abi] = {
4071
+ serial_number: uploadInfo.serial_number,
4072
+ is64Bit: has64Bit && !has32Bit,
4073
+ md5: pkgInfo?.md5 || ""
4074
+ };
4075
+ });
4076
+ await Promise.all(uploadPromises);
4077
+ logger.info("[Tencent] \u6B63\u5728\u83B7\u53D6\u7EBF\u4E0A\u5E94\u7528\u8BE6\u60C5...");
4078
+ const anyPkgPath = Object.values(artifacts)[0];
4079
+ const samplePkgInfo = await parsePackageInfo(anyPkgPath, {
3524
4080
  androidSdkPath: config.globalSettings?.androidSdkPath
3525
4081
  });
3526
- const nativeCodes = pkgInfo?.nativeCode || [];
3527
- const has64Bit = nativeCodes.includes("arm64-v8a") || nativeCodes.includes("x86_64");
3528
- const has32Bit = nativeCodes.includes("armeabi-v7a") || nativeCodes.includes("armeabi") || nativeCodes.includes("x86");
3529
- const is64Bit = has64Bit && !has32Bit;
3530
- const md5 = pkgInfo?.md5;
3531
4082
  const onlineDetail = await this.queryAppDetail(
3532
4083
  auth,
3533
- tencentConfig.packageName || pkgInfo?.packageName || ""
3534
- );
3535
- logger.info(`\u83B7\u53D6\u7EBF\u4E0A\u8BE6\u60C5\u6210\u529F: ${onlineDetail.app_name || "\u672A\u547D\u540D"}`);
3536
- logger.info("\u6B63\u5728\u63D0\u4EA4\u6D41\u6C34\u53F7\u5B8C\u6210\u5BA1\u6838\u7533\u62A5...");
3537
- const submitResult = await this.submitModifyAppInfo(
3538
- auth,
3539
- {
3540
- ...onlineDetail,
3541
- // 以前序详情为蓝本
3542
- pkg_name: tencentConfig.packageName || pkgInfo?.packageName || onlineDetail.pkg_name || "",
3543
- serial_number: uploadInfo.serial_number,
3544
- update_desc: metadata.changelog || "\u7248\u672C\u66F4\u65B0",
3545
- is64Bit,
3546
- md5
3547
- },
3548
- { timeout: tencentConfig.timeout || 6e4 }
4084
+ pkgName || samplePkgInfo?.packageName || ""
3549
4085
  );
3550
- logger.info(`\u63D0\u4EA4\u6D41\u6C34\u53F7\u5B8C\u6210\u5BA1\u6838\u7533\u62A5\u7ED3\u679C: ${JSON.stringify(submitResult)}`);
4086
+ const submitData = {
4087
+ ...onlineDetail,
4088
+ pkg_name: pkgName || samplePkgInfo?.packageName || onlineDetail.pkg_name || "",
4089
+ update_desc: metadata.changelog || "\u7248\u672C\u66F4\u65B0"
4090
+ };
4091
+ const arm64Result = uploadResults["arm64-v8a"] || Object.values(uploadResults).find((r) => r.is64Bit);
4092
+ const arm32Result = uploadResults["armeabi-v7a"] || Object.values(uploadResults).find((r) => !r.is64Bit);
4093
+ if (arm64Result) {
4094
+ submitData.apk64_serial_number = arm64Result.serial_number;
4095
+ submitData.apk64_md5 = arm64Result.md5;
4096
+ }
4097
+ if (arm32Result) {
4098
+ submitData.apk32_serial_number = arm32Result.serial_number;
4099
+ submitData.apk32_md5 = arm32Result.md5;
4100
+ }
4101
+ if (Object.keys(uploadResults).length === 1 && !submitData.apk64_serial_number && !submitData.apk32_serial_number) {
4102
+ const single = Object.values(uploadResults)[0];
4103
+ if (single.is64Bit) {
4104
+ submitData.apk64_serial_number = single.serial_number;
4105
+ submitData.apk64_md5 = single.md5;
4106
+ } else {
4107
+ submitData.apk32_serial_number = single.serial_number;
4108
+ submitData.apk32_md5 = single.md5;
4109
+ }
4110
+ }
4111
+ logger.info("[Tencent] \u6B63\u5728\u63D0\u4EA4\u5BA1\u6838\u7533\u62A5...");
4112
+ await this.submitModifyAppInfo(auth, submitData, { timeout: tencentConfig.timeout || 6e4 });
3551
4113
  logger.success("\u817E\u8BAF\u5E94\u7528\u5B9D\u4E0A\u4F20\u5E76\u63D0\u4EA4\u6210\u529F!");
3552
4114
  return { platform: "tencent", success: true, message: "\u4E0A\u4F20\u5E76\u63D0\u4EA4\u5BA1\u6838\u6210\u529F", consoleUrl };
3553
4115
  } catch (error) {
3554
- logger.error(`\u817E\u8BAF\u4E0A\u4F20\u5931\u8D25: ${JSON.stringify(error)}`);
3555
- logger.error(`\u817E\u8BAF\u4E0A\u4F20\u5931\u8D25: ${error.stack}`);
4116
+ logger.error(`\u817E\u8BAF\u4E0A\u4F20\u5931\u8D25: ${error.message}`);
3556
4117
  return { platform: "tencent", success: false, message: error.message, error, consoleUrl };
3557
4118
  }
3558
4119
  }
@@ -3666,16 +4227,22 @@ var TencentUploader = class _TencentUploader {
3666
4227
  signParams[key] = String(data[key]);
3667
4228
  }
3668
4229
  });
3669
- if (data.is64Bit) {
3670
- signParams.apk64_file_serial_number = String(data.serial_number || "");
3671
- if (data.md5) signParams.apk64_file_md5 = data.md5;
3672
- delete signParams.apk32_file_serial_number;
3673
- delete signParams.apk32_file_md5;
3674
- } else {
3675
- signParams.apk32_file_serial_number = String(data.serial_number || "");
3676
- if (data.md5) signParams.apk32_file_md5 = data.md5;
3677
- delete signParams.apk64_file_serial_number;
3678
- delete signParams.apk64_file_md5;
4230
+ if (data.apk64_serial_number) {
4231
+ signParams.apk64_file_serial_number = String(data.apk64_serial_number);
4232
+ if (data.apk64_md5) signParams.apk64_file_md5 = data.apk64_md5;
4233
+ }
4234
+ if (data.apk32_serial_number) {
4235
+ signParams.apk32_file_serial_number = String(data.apk32_serial_number);
4236
+ if (data.apk32_md5) signParams.apk32_file_md5 = data.apk32_md5;
4237
+ }
4238
+ if (!data.apk64_serial_number && !data.apk32_serial_number && data.serial_number) {
4239
+ if (data.is64Bit) {
4240
+ signParams.apk64_file_serial_number = String(data.serial_number || "");
4241
+ if (data.md5) signParams.apk64_file_md5 = data.md5;
4242
+ } else {
4243
+ signParams.apk32_file_serial_number = String(data.serial_number || "");
4244
+ if (data.md5) signParams.apk32_file_md5 = data.md5;
4245
+ }
3679
4246
  }
3680
4247
  const sign = this.generateSign(signParams, accessSecret);
3681
4248
  const formParams = {
@@ -3694,7 +4261,227 @@ var TencentUploader = class _TencentUploader {
3694
4261
  }
3695
4262
  };
3696
4263
 
4264
+ // src/adapters/google_play_uploader.ts
4265
+ import fs13 from "fs-extra";
4266
+ import path11 from "path";
4267
+ import axios10 from "axios";
4268
+ import { GoogleAuth } from "google-auth-library";
4269
+ var ANDROID_PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher";
4270
+ var PUBLISH_API_ROOT = "https://androidpublisher.googleapis.com/androidpublisher/v3";
4271
+ var UPLOAD_API_ROOT = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3";
4272
+ var GOOGLE_PLAY_CONSOLE_URL = "https://play.google.com/console/developers";
4273
+ var GOOGLE_PLAY_INTERNAL_API_TRACK = "internal";
4274
+ var UPLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
4275
+ var GooglePlayUploader = class {
4276
+ name = "Google Play";
4277
+ httpClient;
4278
+ getAccessToken;
4279
+ constructor(dependencies = {}) {
4280
+ this.httpClient = dependencies.httpClient || axios10;
4281
+ this.getAccessToken = dependencies.getAccessToken || this.createAccessToken.bind(this);
4282
+ }
4283
+ validateConfig(config) {
4284
+ const googleConfig = config?.publish?.google_play;
4285
+ if (!googleConfig?.enable) return false;
4286
+ if (!googleConfig.packageName || typeof googleConfig.packageName !== "string") {
4287
+ logger.error("Google Play \u914D\u7F6E\u7F3A\u5C11 packageName");
4288
+ return false;
4289
+ }
4290
+ const track = googleConfig.track || "internal";
4291
+ if (track !== "internal") {
4292
+ logger.error("Google Play \u9996\u7248\u4EC5\u652F\u6301 internal \u8F68\u9053");
4293
+ return false;
4294
+ }
4295
+ const auth = googleConfig.auth;
4296
+ const hasCredentialContent = typeof auth?.serviceAccountJsonContent === "string" && auth.serviceAccountJsonContent.trim().length > 0;
4297
+ const credentialPath = auth?.serviceAccountJsonPath;
4298
+ const hasCredentialPath = typeof credentialPath === "string" && credentialPath.trim().length > 0 && fs13.existsSync(path11.resolve(credentialPath));
4299
+ if (!hasCredentialContent && !hasCredentialPath) {
4300
+ logger.error("Google Play \u914D\u7F6E\u7F3A\u5C11\u6709\u6548\u7684\u670D\u52A1\u8D26\u53F7 JSON \u6587\u4EF6\u6216\u5185\u5BB9");
4301
+ return false;
4302
+ }
4303
+ return true;
4304
+ }
4305
+ canHandle(filePath) {
4306
+ return path11.extname(filePath).toLowerCase() === ".aab";
4307
+ }
4308
+ async upload(filePath, config, _metadata) {
4309
+ if (!this.validateConfig(config)) {
4310
+ return this.failure("Google Play \u914D\u7F6E\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u5305\u540D\u3001internal \u8F68\u9053\u548C\u670D\u52A1\u8D26\u53F7\u6587\u4EF6");
4311
+ }
4312
+ if (!this.canHandle(filePath)) {
4313
+ return this.failure("Google Play \u4EC5\u63A5\u53D7 Android App Bundle\uFF08.aab\uFF09");
4314
+ }
4315
+ if (!fs13.existsSync(filePath)) {
4316
+ return this.failure(`\u672A\u627E\u5230\u5F85\u4E0A\u4F20\u7684 AAB\uFF1A${filePath}`);
4317
+ }
4318
+ const googleConfig = config.publish.google_play;
4319
+ const packageName = googleConfig.packageName.trim();
4320
+ const encodedPackageName = encodeURIComponent(packageName);
4321
+ let editId;
4322
+ let accessToken;
4323
+ let committed = false;
4324
+ let stage = "create_edit";
4325
+ logger.info(`\u51C6\u5907\u5C06 ${path11.basename(filePath)} \u4E0A\u4F20\u5230 Google Play internal \u8349\u7A3F`);
4326
+ try {
4327
+ accessToken = await this.getAccessToken(googleConfig.auth);
4328
+ const requestConfig = {
4329
+ headers: { Authorization: `Bearer ${accessToken}` }
4330
+ };
4331
+ const editResponse = await this.httpClient.post(
4332
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits`,
4333
+ {},
4334
+ requestConfig
4335
+ );
4336
+ editId = editResponse.data.id;
4337
+ if (!editId) {
4338
+ throw new Error("Google Play \u672A\u8FD4\u56DE editId");
4339
+ }
4340
+ stage = "upload_bundle";
4341
+ const encodedEditId = encodeURIComponent(editId);
4342
+ const bundleResponse = await this.httpClient.post(
4343
+ `${UPLOAD_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}/bundles?uploadType=media`,
4344
+ fs13.createReadStream(filePath),
4345
+ {
4346
+ timeout: UPLOAD_TIMEOUT_MS,
4347
+ maxBodyLength: Infinity,
4348
+ maxContentLength: Infinity,
4349
+ headers: {
4350
+ Authorization: `Bearer ${accessToken}`,
4351
+ "Content-Type": "application/octet-stream",
4352
+ "Content-Length": fs13.statSync(filePath).size
4353
+ }
4354
+ }
4355
+ );
4356
+ const versionCode = bundleResponse.data.versionCode;
4357
+ if (!Number.isInteger(versionCode)) {
4358
+ throw new Error("Google Play \u4E0A\u4F20\u6210\u529F\u4F46\u672A\u8FD4\u56DE\u6709\u6548\u7684 versionCode");
4359
+ }
4360
+ stage = "update_internal_track";
4361
+ await this.httpClient.put(
4362
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}/tracks/${GOOGLE_PLAY_INTERNAL_API_TRACK}`,
4363
+ {
4364
+ track: GOOGLE_PLAY_INTERNAL_API_TRACK,
4365
+ releases: [
4366
+ {
4367
+ versionCodes: [String(versionCode)],
4368
+ status: "draft"
4369
+ }
4370
+ ]
4371
+ },
4372
+ requestConfig
4373
+ );
4374
+ stage = "validate_edit";
4375
+ await this.httpClient.post(
4376
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}:validate`,
4377
+ {},
4378
+ requestConfig
4379
+ );
4380
+ stage = "commit_edit";
4381
+ await this.httpClient.post(
4382
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}:commit`,
4383
+ {},
4384
+ requestConfig
4385
+ );
4386
+ committed = true;
4387
+ logger.success(`Google Play internal \u8349\u7A3F\u5DF2\u521B\u5EFA\uFF08versionCode ${versionCode}\uFF09`);
4388
+ return {
4389
+ platform: "google_play",
4390
+ success: true,
4391
+ message: `AAB \u5DF2\u4E0A\u4F20\u5230 Google Play internal \u8349\u7A3F\uFF08versionCode ${versionCode}\uFF09`,
4392
+ consoleUrl: GOOGLE_PLAY_CONSOLE_URL,
4393
+ data: {
4394
+ editId,
4395
+ versionCode,
4396
+ track: "internal",
4397
+ apiTrack: GOOGLE_PLAY_INTERNAL_API_TRACK,
4398
+ status: "draft",
4399
+ committed: true
4400
+ }
4401
+ };
4402
+ } catch (error) {
4403
+ if (editId && accessToken && !committed) {
4404
+ await this.deleteEditBestEffort(encodedPackageName, editId, accessToken);
4405
+ }
4406
+ const message = this.describeError(error, stage);
4407
+ logger.error(`Google Play \u4E0A\u4F20\u5931\u8D25\uFF1A${message}`);
4408
+ return this.failure(message, error);
4409
+ }
4410
+ }
4411
+ async createAccessToken(authConfig) {
4412
+ let credentials;
4413
+ if (authConfig.serviceAccountJsonContent?.trim()) {
4414
+ try {
4415
+ credentials = JSON.parse(authConfig.serviceAccountJsonContent);
4416
+ } catch {
4417
+ throw new Error("\u670D\u52A1\u8D26\u53F7 JSON \u5185\u5BB9\u683C\u5F0F\u65E0\u6548");
4418
+ }
4419
+ }
4420
+ const keyFile = !credentials && authConfig.serviceAccountJsonPath ? path11.resolve(authConfig.serviceAccountJsonPath) : void 0;
4421
+ const auth = new GoogleAuth({
4422
+ scopes: [ANDROID_PUBLISHER_SCOPE],
4423
+ keyFile,
4424
+ credentials
4425
+ });
4426
+ const token = await auth.getAccessToken();
4427
+ if (!token) {
4428
+ throw new Error("Google Play \u8BA4\u8BC1\u672A\u8FD4\u56DE\u8BBF\u95EE\u4EE4\u724C");
4429
+ }
4430
+ return token;
4431
+ }
4432
+ async deleteEditBestEffort(encodedPackageName, editId, accessToken) {
4433
+ try {
4434
+ await this.httpClient.delete(
4435
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodeURIComponent(editId)}`,
4436
+ { headers: { Authorization: `Bearer ${accessToken}` } }
4437
+ );
4438
+ } catch {
4439
+ logger.warn("Google Play \u672A\u63D0\u4EA4 edit \u7684\u81EA\u52A8\u6E05\u7406\u5931\u8D25\uFF0C\u8BF7\u5728 Play Console \u4E2D\u68C0\u67E5\u8349\u7A3F\u72B6\u6001");
4440
+ }
4441
+ }
4442
+ describeError(error, stage) {
4443
+ const status = error?.response?.status;
4444
+ const rawMessage = error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || "\u672A\u77E5\u9519\u8BEF";
4445
+ const safeMessage = this.redactAndTruncate(String(rawMessage));
4446
+ if (/version\s*code|versionCode/i.test(safeMessage)) {
4447
+ return `Google Play \u62D2\u7EDD\u8BE5 versionCode\uFF0C\u8BF7\u63D0\u9AD8 Flutter \u6784\u5EFA\u53F7\u540E\u91CD\u65B0\u751F\u6210 AAB\uFF1A${safeMessage}`;
4448
+ }
4449
+ if (status === 401) {
4450
+ return "Google Play \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u8D26\u53F7 JSON \u662F\u5426\u6709\u6548";
4451
+ }
4452
+ if (status === 403) {
4453
+ return "Google Play \u62D2\u7EDD\u8BBF\u95EE\uFF0C\u8BF7\u786E\u8BA4\u670D\u52A1\u8D26\u53F7\u5DF2\u52A0\u5165 Play Console \u5E76\u5177\u6709\u5185\u90E8\u6D4B\u8BD5\u7248\u672C\u4E0A\u4F20\u6743\u9650";
4454
+ }
4455
+ if (status === 404) {
4456
+ if (stage === "update_internal_track") {
4457
+ return "Google Play \u4E2D\u672A\u627E\u5230 internal \u5185\u90E8\u6D4B\u8BD5\u8F68\u9053\uFF0C\u8BF7\u786E\u8BA4\u5E94\u7528\u5DF2\u5728 Play Console \u521B\u5EFA\u5185\u90E8\u6D4B\u8BD5\u8F68\u9053";
4458
+ }
4459
+ if (stage !== "create_edit") {
4460
+ return `Google Play API \u5728 ${stage} \u9636\u6BB5\u672A\u627E\u5230\u76EE\u6807\u8D44\u6E90\uFF1A${safeMessage}`;
4461
+ }
4462
+ return "Google Play \u4E2D\u672A\u627E\u5230\u53EF\u4F9B API \u7F16\u8F91\u7684\u5E94\u7528\u3002\u8BF7\u786E\u8BA4 packageName \u5B8C\u5168\u4E00\u81F4\uFF1B\u82E5\u8FD9\u662F\u9996\u6B21\u4E0A\u4F20\uFF0C\u8BF7\u5148\u5728 Play Console \u624B\u5DE5\u4E0A\u4F20\u5E76\u4FDD\u5B58\u4E00\u4E2A APK/AAB\uFF0C\u518D\u91CD\u8BD5 API \u4E0A\u4F20";
4463
+ }
4464
+ if (error?.code === "ECONNABORTED" || /timeout/i.test(safeMessage)) {
4465
+ return "Google Play \u4E0A\u4F20\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\uFF1B\u672C\u5730 AAB \u672A\u88AB\u5220\u9664";
4466
+ }
4467
+ return `Google Play API \u8BF7\u6C42\u5931\u8D25${status ? `\uFF08HTTP ${status}\uFF09` : ""}\uFF1A${safeMessage}`;
4468
+ }
4469
+ redactAndTruncate(message) {
4470
+ return message.replace(/-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/gi, "[\u5DF2\u8131\u654F\u79C1\u94A5]").replace(/Bearer\s+[^\s"']+/gi, "Bearer [\u5DF2\u8131\u654F]").replace(/"private_key"\s*:\s*"[^"]*"/gi, '"private_key":"[\u5DF2\u8131\u654F]"').replace(/"access_token"\s*:\s*"[^"]*"/gi, '"access_token":"[\u5DF2\u8131\u654F]"').slice(0, 500);
4471
+ }
4472
+ failure(message, error) {
4473
+ return {
4474
+ platform: "google_play",
4475
+ success: false,
4476
+ message,
4477
+ // 原始 Axios 错误可能包含 Authorization 请求头,只返回脱敏后的 Error。
4478
+ error: error ? new Error(message) : void 0
4479
+ };
4480
+ }
4481
+ };
4482
+
3697
4483
  // src/utils/upload-manager.ts
4484
+ var GROUPED_ARTIFACT_PLATFORMS = /* @__PURE__ */ new Set(["huawei", "xiaomi", "oppo", "vivo", "tencent"]);
3698
4485
  var UploadManager = class {
3699
4486
  uploaders;
3700
4487
  config;
@@ -3716,6 +4503,7 @@ var UploadManager = class {
3716
4503
  this.registerUploader("oppo", new OppoUploader());
3717
4504
  this.registerUploader("vivo", new VivoUploader());
3718
4505
  this.registerUploader("tencent", new TencentUploader());
4506
+ this.registerUploader("google_play", new GooglePlayUploader());
3719
4507
  }
3720
4508
  /**
3721
4509
  * 注册上传器
@@ -3734,6 +4522,8 @@ var UploadManager = class {
3734
4522
  if (config?.publish?.oppo?.enable) enabled.push("oppo");
3735
4523
  if (config?.publish?.vivo?.enable) enabled.push("vivo");
3736
4524
  if (config?.publish?.tencent?.enable) enabled.push("tencent");
4525
+ if (config?.publish?.harmony?.enable) enabled.push("harmony");
4526
+ if (config?.publish?.google_play?.enable) enabled.push("google_play");
3737
4527
  if (config?.publish?.app_store?.enable) {
3738
4528
  const mode = config.publish.app_store.mode || "app_store";
3739
4529
  enabled.push(mode);
@@ -3801,9 +4591,22 @@ var UploadManager = class {
3801
4591
  if (matchedFiles.length === 0) {
3802
4592
  logger.warn(`[${platform}] No compatible file found in provided files`);
3803
4593
  } else {
3804
- matchedFiles.forEach((file) => {
3805
- tasks.push({ platform, file });
3806
- });
4594
+ if (this.shouldGroupArtifacts(platform)) {
4595
+ const artifactSelection = this.resolveArtifactSelection(platform, matchedFiles, metadata);
4596
+ if (!artifactSelection) {
4597
+ logger.warn(`[${platform}] Failed to build grouped artifact selection`);
4598
+ return;
4599
+ }
4600
+ tasks.push({
4601
+ platform,
4602
+ file: this.getRepresentativeFilePath(artifactSelection),
4603
+ artifactSelection
4604
+ });
4605
+ } else {
4606
+ matchedFiles.forEach((file) => {
4607
+ tasks.push({ platform, file, artifactSelection: file });
4608
+ });
4609
+ }
3807
4610
  }
3808
4611
  });
3809
4612
  if (tasks.length === 0) {
@@ -3821,8 +4624,14 @@ var UploadManager = class {
3821
4624
  }
3822
4625
  logger.info(`Scheduled ${tasks.length} upload task(s) for ${validPlatforms.length} platform(s)`);
3823
4626
  const uploadPromises = tasks.map((task) => {
3824
- logger.info(`[${task.platform}] Matched file: ${path9.basename(task.file)}`);
3825
- return this.uploadToPlatform(task.platform, task.file, config, metadata);
4627
+ logger.info(`[${task.platform}] Matched file: ${path12.basename(task.file)}`);
4628
+ return this.uploadToPlatform(
4629
+ task.platform,
4630
+ task.file,
4631
+ config,
4632
+ metadata,
4633
+ task.artifactSelection
4634
+ );
3826
4635
  });
3827
4636
  const results = await Promise.all(uploadPromises);
3828
4637
  const report = this.aggregateResults(results, tasks.length, startTime);
@@ -3832,7 +4641,7 @@ var UploadManager = class {
3832
4641
  /**
3833
4642
  * 上传到单个平台
3834
4643
  */
3835
- async uploadToPlatform(platform, filePath, config, metadata) {
4644
+ async uploadToPlatform(platform, filePath, config, metadata, artifactSelection = filePath) {
3836
4645
  if (this.config.cancellationToken?.isCancelled) {
3837
4646
  logger.warn(`[${platform.toUpperCase()}] Upload skipped due to cancellation`);
3838
4647
  return {
@@ -3853,25 +4662,30 @@ var UploadManager = class {
3853
4662
  if (this.config.verbose) {
3854
4663
  logger.info(`[${platform.toUpperCase()}] Starting upload...`);
3855
4664
  }
3856
- const pkgInfo = await parsePackageInfo(filePath, {
3857
- androidSdkPath: metadata.globalSettings?.androidSdkPath
4665
+ const effectiveMetadata = {
4666
+ ...metadata,
4667
+ artifactPath: artifactSelection
4668
+ };
4669
+ await this.validateArtifactSelection(platform, artifactSelection, effectiveMetadata);
4670
+ const pkgInfo = path12.extname(filePath).toLowerCase() === ".aab" ? null : await parsePackageInfo(filePath, {
4671
+ androidSdkPath: effectiveMetadata.globalSettings?.androidSdkPath
3858
4672
  });
3859
4673
  if (pkgInfo) {
3860
4674
  if (pkgInfo.versionName) {
3861
- metadata.version = pkgInfo.versionName;
4675
+ effectiveMetadata.version = pkgInfo.versionName;
3862
4676
  }
3863
4677
  if (pkgInfo.versionCode) {
3864
- metadata.buildNumber = pkgInfo.versionCode;
4678
+ effectiveMetadata.buildNumber = pkgInfo.versionCode;
3865
4679
  }
3866
4680
  if (pkgInfo.packageName) {
3867
4681
  ;
3868
- metadata.packageName = pkgInfo.packageName;
4682
+ effectiveMetadata.packageName = pkgInfo.packageName;
3869
4683
  }
3870
4684
  logger.info(
3871
- `[${platform.toUpperCase()}] \u5DF2\u6821\u51C6\u7248\u672C\u4FE1\u606F: v${metadata.version}+${metadata.buildNumber}`
4685
+ `[${platform.toUpperCase()}] \u5DF2\u6821\u51C6\u7248\u672C\u4FE1\u606F: v${effectiveMetadata.version}+${effectiveMetadata.buildNumber}`
3872
4686
  );
3873
4687
  }
3874
- const result = await uploader.upload(filePath, config, metadata);
4688
+ const result = await uploader.upload(filePath, config, effectiveMetadata);
3875
4689
  return result;
3876
4690
  } catch (error) {
3877
4691
  const errorMessage = error.message || String(error);
@@ -3884,6 +4698,85 @@ var UploadManager = class {
3884
4698
  };
3885
4699
  }
3886
4700
  }
4701
+ shouldGroupArtifacts(platform) {
4702
+ return GROUPED_ARTIFACT_PLATFORMS.has(platform);
4703
+ }
4704
+ resolveArtifactSelection(platform, matchedFiles, metadata) {
4705
+ const explicitArtifacts = metadata.platformArtifacts?.[platform];
4706
+ if (explicitArtifacts) {
4707
+ if (typeof explicitArtifacts === "string") {
4708
+ return explicitArtifacts;
4709
+ }
4710
+ if (Object.keys(explicitArtifacts).length > 0) {
4711
+ return explicitArtifacts;
4712
+ }
4713
+ }
4714
+ if (matchedFiles.length === 0) {
4715
+ return null;
4716
+ }
4717
+ if (matchedFiles.length === 1) {
4718
+ return matchedFiles[0];
4719
+ }
4720
+ return this.buildArtifactMapFromFiles(matchedFiles);
4721
+ }
4722
+ buildArtifactMapFromFiles(files) {
4723
+ return files.reduce(
4724
+ (acc, filePath, index) => {
4725
+ acc[this.inferArtifactKey(filePath, index)] = filePath;
4726
+ return acc;
4727
+ },
4728
+ {}
4729
+ );
4730
+ }
4731
+ inferArtifactKey(filePath, index) {
4732
+ const fileName = path12.basename(filePath).toLowerCase();
4733
+ if (fileName.includes("arm64-v8a")) return "arm64-v8a";
4734
+ if (fileName.includes("armeabi-v7a")) return "armeabi-v7a";
4735
+ if (fileName.includes("x86_64")) return "x86_64";
4736
+ if (fileName.includes("universal")) return "universal";
4737
+ return `artifact_${index + 1}`;
4738
+ }
4739
+ getRepresentativeFilePath(artifactSelection) {
4740
+ if (typeof artifactSelection === "string") {
4741
+ return artifactSelection;
4742
+ }
4743
+ return Object.values(artifactSelection)[0];
4744
+ }
4745
+ async validateArtifactSelection(platform, artifactSelection, metadata) {
4746
+ if (typeof artifactSelection === "string") {
4747
+ return;
4748
+ }
4749
+ const artifactPaths = Object.values(artifactSelection).filter(Boolean);
4750
+ if (artifactPaths.length < 2) {
4751
+ return;
4752
+ }
4753
+ const parsedInfos = await Promise.all(
4754
+ artifactPaths.map(
4755
+ (artifactPath) => parsePackageInfo(artifactPath, {
4756
+ androidSdkPath: metadata.globalSettings?.androidSdkPath
4757
+ })
4758
+ )
4759
+ );
4760
+ const availableInfos = parsedInfos.filter((info) => !!info);
4761
+ if (availableInfos.length < 2) {
4762
+ logger.warn(
4763
+ `[${platform.toUpperCase()}] Unable to fully validate multi-artifact package info; continuing with upload.`
4764
+ );
4765
+ return;
4766
+ }
4767
+ const [baseInfo, ...restInfos] = availableInfos;
4768
+ for (const info of restInfos) {
4769
+ if (baseInfo?.packageName && info?.packageName && baseInfo.packageName !== info.packageName) {
4770
+ throw new Error("\u53CC\u5305\u4E0A\u4F20\u5931\u8D25\uFF1A\u6240\u9009\u5B89\u88C5\u5305\u7684 packageName \u4E0D\u4E00\u81F4");
4771
+ }
4772
+ if (baseInfo?.versionName && info?.versionName && baseInfo.versionName !== info.versionName) {
4773
+ throw new Error("\u53CC\u5305\u4E0A\u4F20\u5931\u8D25\uFF1A\u6240\u9009\u5B89\u88C5\u5305\u7684 versionName \u4E0D\u4E00\u81F4");
4774
+ }
4775
+ if (baseInfo?.versionCode && info?.versionCode && baseInfo.versionCode !== info.versionCode) {
4776
+ throw new Error("\u53CC\u5305\u4E0A\u4F20\u5931\u8D25\uFF1A\u6240\u9009\u5B89\u88C5\u5305\u7684 versionCode \u4E0D\u4E00\u81F4");
4777
+ }
4778
+ }
4779
+ }
3887
4780
  /**
3888
4781
  * 聚合上传结果
3889
4782
  */
@@ -3993,7 +4886,7 @@ var UploadManager = class {
3993
4886
  };
3994
4887
 
3995
4888
  // src/validators/config_validator.ts
3996
- import fs13 from "fs-extra";
4889
+ import fs14 from "fs-extra";
3997
4890
  import yaml from "js-yaml";
3998
4891
 
3999
4892
  // src/validators/env_validator.ts
@@ -4019,20 +4912,22 @@ var ConfigValidator = class {
4019
4912
  * @param filePath 配置文件路径
4020
4913
  */
4021
4914
  static loadAndValidate(filePath) {
4022
- if (!fs13.existsSync(filePath)) {
4915
+ if (!fs14.existsSync(filePath)) {
4023
4916
  logger.error(`Config file not found: ${filePath}`);
4024
4917
  return null;
4025
4918
  }
4026
- let content = fs13.readFileSync(filePath, "utf8");
4027
- const envRegex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
4028
- const matches = [...content.matchAll(envRegex)];
4029
- const requiredEnvs = Array.from(new Set(matches.map((m) => m[1])));
4030
- if (!EnvValidator.validate(requiredEnvs)) {
4031
- return null;
4032
- }
4033
- content = content.replace(envRegex, (_, varName) => process.env[varName] || "");
4034
4919
  try {
4035
- const config = yaml.load(content);
4920
+ const content = fs14.readFileSync(filePath, "utf8");
4921
+ const rawConfig = yaml.load(content);
4922
+ if (!rawConfig || typeof rawConfig !== "object") {
4923
+ logger.error("Config file must contain a YAML object");
4924
+ return null;
4925
+ }
4926
+ const requiredEnvs = this.collectRequiredEnvVars(rawConfig);
4927
+ if (!EnvValidator.validate(requiredEnvs)) {
4928
+ return null;
4929
+ }
4930
+ const config = this.resolveEnvVars(rawConfig);
4036
4931
  if (!this.validateSchema(config)) {
4037
4932
  return null;
4038
4933
  }
@@ -4042,15 +4937,71 @@ var ConfigValidator = class {
4042
4937
  return null;
4043
4938
  }
4044
4939
  }
4940
+ /**
4941
+ * 只收集本次实际启用配置中的环境变量,避免禁用平台阻塞其它上传任务。
4942
+ */
4943
+ static collectRequiredEnvVars(config) {
4944
+ const activeConfig = {
4945
+ ...config,
4946
+ publish: Object.fromEntries(
4947
+ Object.entries(config.publish || {}).filter(([, value]) => value?.enable === true)
4948
+ )
4949
+ };
4950
+ const requiredEnvs = /* @__PURE__ */ new Set();
4951
+ const envRegex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
4952
+ const visit = (value) => {
4953
+ if (typeof value === "string") {
4954
+ for (const match of value.matchAll(envRegex)) {
4955
+ requiredEnvs.add(match[1]);
4956
+ }
4957
+ return;
4958
+ }
4959
+ if (Array.isArray(value)) {
4960
+ value.forEach(visit);
4961
+ return;
4962
+ }
4963
+ if (value && typeof value === "object") {
4964
+ Object.values(value).forEach(visit);
4965
+ }
4966
+ };
4967
+ visit(activeConfig);
4968
+ return Array.from(requiredEnvs);
4969
+ }
4970
+ /** 在内存中递归解析环境变量,不重新拼接 YAML,避免密钥字符破坏 YAML 结构。 */
4971
+ static resolveEnvVars(value) {
4972
+ if (typeof value === "string") {
4973
+ return value.replace(
4974
+ /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g,
4975
+ (_, varName) => process.env[varName] || ""
4976
+ );
4977
+ }
4978
+ if (Array.isArray(value)) {
4979
+ return value.map((item) => this.resolveEnvVars(item));
4980
+ }
4981
+ if (value && typeof value === "object") {
4982
+ return Object.fromEntries(
4983
+ Object.entries(value).map(([key, item]) => [key, this.resolveEnvVars(item)])
4984
+ );
4985
+ }
4986
+ return value;
4987
+ }
4045
4988
  /**
4046
4989
  * 验证配置 Schema
4047
4990
  * @param config 配置对象
4048
4991
  */
4049
4992
  static validateSchema(config) {
4050
4993
  if (config.publish?.app_store?.enable) {
4994
+ const mode = config.publish.app_store.mode || "app_store";
4051
4995
  const auth = config.publish.app_store.auth;
4052
- if (!auth?.apiKeyId || !auth?.apiIssuerId) {
4053
- logger.error("App Store enabled but missing auth info (apiKeyId, apiIssuerId)");
4996
+ const hasCliAuth = Boolean(auth?.appleId && auth?.appPassword);
4997
+ const hasApiAuth = Boolean(
4998
+ auth?.apiKeyId && auth?.apiIssuerId && (auth?.privateKeyPath || auth?.privateKeyContent)
4999
+ );
5000
+ const hasRequiredAuth = mode === "adhoc" || (mode === "testflight" ? hasApiAuth : hasCliAuth || hasApiAuth);
5001
+ if (!hasRequiredAuth) {
5002
+ logger.error(
5003
+ mode === "testflight" ? "TestFlight enabled but missing complete API key credentials" : "App Store enabled but missing Apple ID credentials or complete API key credentials"
5004
+ );
4054
5005
  return false;
4055
5006
  }
4056
5007
  }
@@ -4096,23 +5047,38 @@ var ConfigValidator = class {
4096
5047
  return false;
4097
5048
  }
4098
5049
  }
5050
+ if (config.publish?.google_play?.enable) {
5051
+ const googlePlay = config.publish.google_play;
5052
+ const auth = googlePlay.auth;
5053
+ if (!googlePlay.packageName || !auth?.serviceAccountJsonPath && !auth?.serviceAccountJsonContent) {
5054
+ logger.error(
5055
+ "Google Play enabled but missing packageName or service account JSON credentials"
5056
+ );
5057
+ return false;
5058
+ }
5059
+ if (googlePlay.track && googlePlay.track !== "internal") {
5060
+ logger.error("Google Play first release only supports the internal track");
5061
+ return false;
5062
+ }
5063
+ }
4099
5064
  return true;
4100
5065
  }
4101
5066
  };
4102
5067
 
4103
5068
  // src/cli.ts
4104
- import path10 from "path";
4105
- import fs14 from "fs-extra";
5069
+ import path13 from "path";
5070
+ import fs15 from "fs-extra";
4106
5071
  import yaml2 from "js-yaml";
4107
- var VERSION = "0.1.0";
5072
+ var VERSION = "0.1.2";
4108
5073
  function printHelp() {
4109
5074
  console.log(`
4110
5075
  app-ship v${VERSION} \u2014 Multi-platform app store uploader
4111
5076
 
4112
5077
  Usage:
4113
- app-ship upload -c <config> -f <file> [--platform pgyer,huawei]
5078
+ app-ship upload -c <config> -f <file> [--platform pgyer,huawei] [--project <dir>]
4114
5079
  app-ship platforms List supported platforms
4115
5080
  app-ship init Generate sample config file
5081
+ app-ship history [--project <dir>] [--json]
4116
5082
  app-ship --help Show this help
4117
5083
  app-ship --version Show version
4118
5084
 
@@ -4123,7 +5089,13 @@ Upload Options:
4123
5089
  -v, --version <ver> Version string (default: from config or "1.0.0")
4124
5090
  -b, --build-number <n> Build number (default: "1")
4125
5091
  --changelog <text> Changelog / release notes
5092
+ --project <dir> Project directory recorded in the release receipt
5093
+ --confirm-google-play Confirm upload to Google Play internal draft
4126
5094
  --verbose Show detailed output
5095
+
5096
+ History Options:
5097
+ --project <dir> Project directory (default: current directory)
5098
+ --json Print structured receipts as JSON
4127
5099
  `);
4128
5100
  }
4129
5101
  function parseArgs(argv) {
@@ -4137,6 +5109,12 @@ function parseArgs(argv) {
4137
5109
  args.showVersion = true;
4138
5110
  } else if (arg === "--verbose") {
4139
5111
  args.verbose = true;
5112
+ } else if (arg === "--confirm-google-play") {
5113
+ args.confirmGooglePlay = true;
5114
+ } else if (arg === "--json") {
5115
+ args.json = true;
5116
+ } else if (arg === "--project" && argv[i + 1]) {
5117
+ args.project = argv[++i];
4140
5118
  } else if ((arg === "-c" || arg === "--config") && argv[i + 1]) {
4141
5119
  args.config = argv[++i];
4142
5120
  } else if ((arg === "-f" || arg === "--file") && argv[i + 1]) {
@@ -4167,9 +5145,9 @@ async function cmdUpload(args) {
4167
5145
  logger.error("Missing file path. Use: app-ship upload -c <config.yaml> -f <file>");
4168
5146
  process.exit(1);
4169
5147
  }
4170
- const absConfig = path10.resolve(configPath);
4171
- const absFile = path10.resolve(filePath);
4172
- if (!fs14.existsSync(absFile)) {
5148
+ const absConfig = path13.resolve(configPath);
5149
+ const absFile = path13.resolve(filePath);
5150
+ if (!fs15.existsSync(absFile)) {
4173
5151
  logger.error(`File not found: ${absFile}`);
4174
5152
  process.exit(1);
4175
5153
  }
@@ -4178,27 +5156,92 @@ async function cmdUpload(args) {
4178
5156
  logger.error("Config validation failed");
4179
5157
  process.exit(1);
4180
5158
  }
4181
- const platforms = args.platform ? args.platform.split(",").map((s) => s.trim()) : void 0;
5159
+ const platforms = args.platform ? Array.from(
5160
+ new Set(
5161
+ args.platform.split(",").map((platform) => platform.trim()).filter(Boolean)
5162
+ )
5163
+ ) : void 0;
5164
+ const manager = new UploadManager({
5165
+ maxConcurrent: 3,
5166
+ continueOnError: true,
5167
+ verbose: !!args.verbose
5168
+ });
5169
+ const effectivePlatforms = platforms || manager.getEnabledPlatforms(config);
5170
+ if (effectivePlatforms.length === 0) {
5171
+ logger.error("No upload platform is enabled. Enable one in config or pass --platform.");
5172
+ process.exit(1);
5173
+ }
4182
5174
  const metadata = {
4183
5175
  version: args.ver || config.version || "1.0.0",
4184
5176
  buildNumber: args.buildNumber || "1",
4185
- changelog: args.changelog || ""
5177
+ changelog: args.changelog || "",
5178
+ platform: effectivePlatforms
4186
5179
  };
4187
5180
  logger.title("App Ship \u2014 Starting Upload");
4188
5181
  logger.info(`File: ${absFile}`);
4189
5182
  logger.info(`Config: ${absConfig}`);
4190
- if (platforms) {
4191
- logger.info(`Platforms: ${platforms.join(", ")}`);
5183
+ logger.info(`Platforms: ${effectivePlatforms.join(", ") || "none"}`);
5184
+ if (effectivePlatforms.includes("google_play")) {
5185
+ if (path13.extname(absFile).toLowerCase() !== ".aab") {
5186
+ logger.error("Google Play only accepts Android App Bundle (.aab) files");
5187
+ process.exit(1);
5188
+ }
5189
+ logger.info(`Google Play package: ${config.publish?.google_play?.packageName || "unknown"}`);
5190
+ logger.info("Google Play target: internal draft");
5191
+ if (!args.confirmGooglePlay) {
5192
+ logger.error(
5193
+ "Google Play upload requires explicit confirmation. Review the preview and rerun with --confirm-google-play"
5194
+ );
5195
+ process.exit(1);
5196
+ }
4192
5197
  }
4193
5198
  logger.newLine();
4194
- const manager = new UploadManager({
4195
- maxConcurrent: 3,
4196
- continueOnError: true,
4197
- verbose: !!args.verbose
4198
- });
4199
- const report = await manager.uploadToMultiplePlatforms(absFile, config, metadata, platforms);
5199
+ const startedAt = Date.now();
5200
+ const report = await manager.uploadToMultiplePlatforms(
5201
+ absFile,
5202
+ config,
5203
+ metadata,
5204
+ effectivePlatforms
5205
+ );
5206
+ try {
5207
+ const receipt = await new ReleaseReceiptStore().save({
5208
+ projectRoot: resolveReleaseProjectRoot(args.project, absConfig),
5209
+ kind: "upload",
5210
+ appName: metadata.appName,
5211
+ packageName: resolvePackageName(config),
5212
+ versionName: metadata.version,
5213
+ buildNumber: metadata.buildNumber,
5214
+ changelog: metadata.changelog,
5215
+ artifactPaths: absFile,
5216
+ targets: createUploadTargets(report.results),
5217
+ startedAt
5218
+ });
5219
+ logger.info(`Receipt: ${receipt.id}`);
5220
+ } catch (error) {
5221
+ logger.warn(`Failed to save release receipt: ${error.message}`);
5222
+ }
4200
5223
  process.exit(report.failureCount > 0 ? 1 : 0);
4201
5224
  }
5225
+ async function cmdHistory(args) {
5226
+ const projectRoot = path13.resolve(args.project || process.cwd());
5227
+ const receipts = await new ReleaseReceiptStore().list(projectRoot);
5228
+ if (args.json) {
5229
+ console.log(JSON.stringify(receipts, null, 2));
5230
+ return;
5231
+ }
5232
+ if (receipts.length === 0) {
5233
+ logger.info(`No release receipts found for ${projectRoot}`);
5234
+ return;
5235
+ }
5236
+ logger.title(`Release Receipts \u2014 ${projectRoot}`);
5237
+ receipts.forEach((receipt, index) => {
5238
+ if (index > 0) console.log("");
5239
+ console.log(formatReleaseReceipt(receipt));
5240
+ });
5241
+ }
5242
+ function resolvePackageName(config) {
5243
+ return config.appId || config.publish?.google_play?.packageName || Object.values(config.publish || {}).find((value) => value?.packageName)?.packageName;
5244
+ }
4202
5245
  function cmdPlatforms() {
4203
5246
  const manager = new UploadManager();
4204
5247
  const platforms = manager.getAvailablePlatforms();
@@ -4211,7 +5254,7 @@ Total: ${platforms.length} platforms`);
4211
5254
  }
4212
5255
  function cmdInit() {
4213
5256
  const filename = "app-ship.yaml";
4214
- if (fs14.existsSync(filename)) {
5257
+ if (fs15.existsSync(filename)) {
4215
5258
  logger.warn(`${filename} already exists. Skipping.`);
4216
5259
  return;
4217
5260
  }
@@ -4219,7 +5262,7 @@ function cmdInit() {
4219
5262
  version: "1.0.0",
4220
5263
  publish: {
4221
5264
  pgyer: {
4222
- enable: true,
5265
+ enable: false,
4223
5266
  apiKey: "YOUR_PGYER_API_KEY"
4224
5267
  },
4225
5268
  huawei: {
@@ -4255,6 +5298,7 @@ function cmdInit() {
4255
5298
  tencent: {
4256
5299
  enable: false,
4257
5300
  auth: {
5301
+ userId: "",
4258
5302
  appId: "",
4259
5303
  accessSecret: ""
4260
5304
  }
@@ -4273,10 +5317,18 @@ function cmdInit() {
4273
5317
  appleId: "",
4274
5318
  appPassword: ""
4275
5319
  }
5320
+ },
5321
+ google_play: {
5322
+ enable: false,
5323
+ auth: {
5324
+ serviceAccountJsonPath: "./service-account.json"
5325
+ },
5326
+ packageName: "com.example.app",
5327
+ track: "internal"
4276
5328
  }
4277
5329
  }
4278
5330
  };
4279
- fs14.writeFileSync(filename, yaml2.dump(sample, { indent: 2 }), "utf8");
5331
+ fs15.writeFileSync(filename, yaml2.dump(sample, { indent: 2 }), "utf8");
4280
5332
  logger.success(`Created ${filename}`);
4281
5333
  logger.info("Edit the file to configure your platform credentials.");
4282
5334
  }
@@ -4301,6 +5353,9 @@ async function main() {
4301
5353
  case "init":
4302
5354
  cmdInit();
4303
5355
  break;
5356
+ case "history":
5357
+ await cmdHistory(args);
5358
+ break;
4304
5359
  default:
4305
5360
  printHelp();
4306
5361
  break;