@huoye/app-ship 0.1.2 → 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/index.js CHANGED
@@ -552,16 +552,18 @@ var ConfigValidator = class {
552
552
  logger.error(`Config file not found: ${filePath}`);
553
553
  return null;
554
554
  }
555
- let content = fs.readFileSync(filePath, "utf8");
556
- const envRegex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
557
- const matches = [...content.matchAll(envRegex)];
558
- const requiredEnvs = Array.from(new Set(matches.map((m) => m[1])));
559
- if (!EnvValidator.validate(requiredEnvs)) {
560
- return null;
561
- }
562
- content = content.replace(envRegex, (_, varName) => process.env[varName] || "");
563
555
  try {
564
- const config = yaml.load(content);
556
+ const content = fs.readFileSync(filePath, "utf8");
557
+ const rawConfig = yaml.load(content);
558
+ if (!rawConfig || typeof rawConfig !== "object") {
559
+ logger.error("Config file must contain a YAML object");
560
+ return null;
561
+ }
562
+ const requiredEnvs = this.collectRequiredEnvVars(rawConfig);
563
+ if (!EnvValidator.validate(requiredEnvs)) {
564
+ return null;
565
+ }
566
+ const config = this.resolveEnvVars(rawConfig);
565
567
  if (!this.validateSchema(config)) {
566
568
  return null;
567
569
  }
@@ -571,15 +573,71 @@ var ConfigValidator = class {
571
573
  return null;
572
574
  }
573
575
  }
576
+ /**
577
+ * 只收集本次实际启用配置中的环境变量,避免禁用平台阻塞其它上传任务。
578
+ */
579
+ static collectRequiredEnvVars(config) {
580
+ const activeConfig = {
581
+ ...config,
582
+ publish: Object.fromEntries(
583
+ Object.entries(config.publish || {}).filter(([, value]) => value?.enable === true)
584
+ )
585
+ };
586
+ const requiredEnvs = /* @__PURE__ */ new Set();
587
+ const envRegex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
588
+ const visit = (value) => {
589
+ if (typeof value === "string") {
590
+ for (const match of value.matchAll(envRegex)) {
591
+ requiredEnvs.add(match[1]);
592
+ }
593
+ return;
594
+ }
595
+ if (Array.isArray(value)) {
596
+ value.forEach(visit);
597
+ return;
598
+ }
599
+ if (value && typeof value === "object") {
600
+ Object.values(value).forEach(visit);
601
+ }
602
+ };
603
+ visit(activeConfig);
604
+ return Array.from(requiredEnvs);
605
+ }
606
+ /** 在内存中递归解析环境变量,不重新拼接 YAML,避免密钥字符破坏 YAML 结构。 */
607
+ static resolveEnvVars(value) {
608
+ if (typeof value === "string") {
609
+ return value.replace(
610
+ /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g,
611
+ (_, varName) => process.env[varName] || ""
612
+ );
613
+ }
614
+ if (Array.isArray(value)) {
615
+ return value.map((item) => this.resolveEnvVars(item));
616
+ }
617
+ if (value && typeof value === "object") {
618
+ return Object.fromEntries(
619
+ Object.entries(value).map(([key, item]) => [key, this.resolveEnvVars(item)])
620
+ );
621
+ }
622
+ return value;
623
+ }
574
624
  /**
575
625
  * 验证配置 Schema
576
626
  * @param config 配置对象
577
627
  */
578
628
  static validateSchema(config) {
579
629
  if (config.publish?.app_store?.enable) {
630
+ const mode = config.publish.app_store.mode || "app_store";
580
631
  const auth = config.publish.app_store.auth;
581
- if (!auth?.apiKeyId || !auth?.apiIssuerId) {
582
- logger.error("App Store enabled but missing auth info (apiKeyId, apiIssuerId)");
632
+ const hasCliAuth = Boolean(auth?.appleId && auth?.appPassword);
633
+ const hasApiAuth = Boolean(
634
+ auth?.apiKeyId && auth?.apiIssuerId && (auth?.privateKeyPath || auth?.privateKeyContent)
635
+ );
636
+ const hasRequiredAuth = mode === "adhoc" || (mode === "testflight" ? hasApiAuth : hasCliAuth || hasApiAuth);
637
+ if (!hasRequiredAuth) {
638
+ logger.error(
639
+ mode === "testflight" ? "TestFlight enabled but missing complete API key credentials" : "App Store enabled but missing Apple ID credentials or complete API key credentials"
640
+ );
583
641
  return false;
584
642
  }
585
643
  }
@@ -625,6 +683,20 @@ var ConfigValidator = class {
625
683
  return false;
626
684
  }
627
685
  }
686
+ if (config.publish?.google_play?.enable) {
687
+ const googlePlay = config.publish.google_play;
688
+ const auth = googlePlay.auth;
689
+ if (!googlePlay.packageName || !auth?.serviceAccountJsonPath && !auth?.serviceAccountJsonContent) {
690
+ logger.error(
691
+ "Google Play enabled but missing packageName or service account JSON credentials"
692
+ );
693
+ return false;
694
+ }
695
+ if (googlePlay.track && googlePlay.track !== "internal") {
696
+ logger.error("Google Play first release only supports the internal track");
697
+ return false;
698
+ }
699
+ }
628
700
  return true;
629
701
  }
630
702
  };
@@ -966,6 +1038,7 @@ var UploadManager = class {
966
1038
  this.registerUploader("oppo", new OppoUploader());
967
1039
  this.registerUploader("vivo", new VivoUploader());
968
1040
  this.registerUploader("tencent", new TencentUploader());
1041
+ this.registerUploader("google_play", new GooglePlayUploader());
969
1042
  }
970
1043
  /**
971
1044
  * 注册上传器
@@ -984,6 +1057,8 @@ var UploadManager = class {
984
1057
  if (config?.publish?.oppo?.enable) enabled.push("oppo");
985
1058
  if (config?.publish?.vivo?.enable) enabled.push("vivo");
986
1059
  if (config?.publish?.tencent?.enable) enabled.push("tencent");
1060
+ if (config?.publish?.harmony?.enable) enabled.push("harmony");
1061
+ if (config?.publish?.google_play?.enable) enabled.push("google_play");
987
1062
  if (config?.publish?.app_store?.enable) {
988
1063
  const mode = config.publish.app_store.mode || "app_store";
989
1064
  enabled.push(mode);
@@ -1127,7 +1202,7 @@ var UploadManager = class {
1127
1202
  artifactPath: artifactSelection
1128
1203
  };
1129
1204
  await this.validateArtifactSelection(platform, artifactSelection, effectiveMetadata);
1130
- const pkgInfo = await parsePackageInfo(filePath, {
1205
+ const pkgInfo = path2.extname(filePath).toLowerCase() === ".aab" ? null : await parsePackageInfo(filePath, {
1131
1206
  androidSdkPath: effectiveMetadata.globalSettings?.androidSdkPath
1132
1207
  });
1133
1208
  if (pkgInfo) {
@@ -1484,6 +1559,280 @@ var AppleJWTGenerator = class _AppleJWTGenerator {
1484
1559
  }
1485
1560
  };
1486
1561
 
1562
+ // src/utils/release-receipt.ts
1563
+ import { createHash, randomUUID } from "crypto";
1564
+ import { createReadStream } from "fs";
1565
+ import { chmod, mkdir, readFile, realpath, rename, stat, writeFile } from "fs/promises";
1566
+ import { homedir } from "os";
1567
+ import path3 from "path";
1568
+ var RELEASE_RECEIPT_SCHEMA_VERSION = 1;
1569
+ var DEFAULT_RELEASE_RECEIPT_LIMIT = 100;
1570
+ var REMOTE_IDENTIFIER_KEYS = [
1571
+ "id",
1572
+ "appId",
1573
+ "buildId",
1574
+ "versionId",
1575
+ "releaseId",
1576
+ "editId",
1577
+ "versionCode",
1578
+ "track",
1579
+ "apiTrack",
1580
+ "status",
1581
+ "committed"
1582
+ ];
1583
+ function resolveReleaseProjectRoot(projectRoot, configPath) {
1584
+ return path3.resolve(projectRoot || path3.dirname(path3.resolve(configPath)));
1585
+ }
1586
+ function sanitizeReleaseMessage(value, maxLength = 500) {
1587
+ let message = typeof value === "string" ? value : String(value ?? "");
1588
+ 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(
1589
+ /(["']?(?:access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key|api[_-]?key|app[_-]?password|authorization|credential|service[_-]?account(?:[_-]?json)?[_-]?content)["']?\s*[:=]\s*)["']?[^\s,"'}]+["']?/gi,
1590
+ "$1[\u5DF2\u8131\u654F]"
1591
+ ).replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
1592
+ return message.length > maxLength ? `${message.slice(0, maxLength - 1)}\u2026` : message;
1593
+ }
1594
+ function createUploadTargets(results) {
1595
+ return results.map((result) => {
1596
+ const platform = sanitizeReleaseMessage(result.platform, 80) || "unknown";
1597
+ const data = isRecord(result.data) ? result.data : void 0;
1598
+ const isDraft = result.success && platform === "google_play" && String(data?.status || "").toLowerCase() === "draft";
1599
+ const remote = createRemoteReceipt(result, data);
1600
+ return {
1601
+ platform,
1602
+ state: result.success ? isDraft ? "draft_created" : "uploaded" : "failed",
1603
+ outcome: result.success ? "success" : "failed",
1604
+ message: sanitizeReleaseMessage(result.message) || (result.success ? "\u4E0A\u4F20\u6210\u529F" : "\u4E0A\u4F20\u5931\u8D25"),
1605
+ ...remote ? { remote } : {}
1606
+ };
1607
+ });
1608
+ }
1609
+ async function createReleaseReceipt(input) {
1610
+ const projectRoot = await canonicalizeProjectRoot(input.projectRoot);
1611
+ const startedAt = toDate(input.startedAt ?? Date.now());
1612
+ const finishedAt = toDate(input.finishedAt ?? Date.now());
1613
+ const targets = input.targets.map(sanitizeTarget);
1614
+ return {
1615
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1616
+ id: randomUUID(),
1617
+ projectKey: createProjectKey(projectRoot),
1618
+ projectRoot,
1619
+ kind: input.kind,
1620
+ ...cleanOptionalText(input.appName, 160) ? { appName: cleanOptionalText(input.appName, 160) } : {},
1621
+ ...cleanOptionalText(input.packageName, 240) ? { packageName: cleanOptionalText(input.packageName, 240) } : {},
1622
+ ...cleanOptionalText(input.versionName, 80) ? { versionName: cleanOptionalText(input.versionName, 80) } : {},
1623
+ ...input.buildNumber !== void 0 ? { buildNumber: cleanOptionalText(String(input.buildNumber), 80) } : {},
1624
+ ...cleanOptionalText(input.changelog, 4e3) ? { changelog: cleanOptionalText(input.changelog, 4e3) } : {},
1625
+ artifacts: await inspectArtifacts(input.artifactPaths),
1626
+ targets,
1627
+ outcome: input.outcome ?? inferOutcome(targets),
1628
+ startedAt: startedAt.toISOString(),
1629
+ finishedAt: finishedAt.toISOString(),
1630
+ durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime())
1631
+ };
1632
+ }
1633
+ function formatReleaseReceipt(receipt) {
1634
+ const lines = [
1635
+ `${receipt.appName || receipt.packageName || "\u5E94\u7528"} \xB7 ${receipt.kind === "build" ? "\u6784\u5EFA\u56DE\u6267" : "\u4E0A\u4F20\u56DE\u6267"}`,
1636
+ `\u7ED3\u679C\uFF1A${receipt.outcome}`,
1637
+ `\u7248\u672C\uFF1A${receipt.versionName || "-"} (${receipt.buildNumber || "-"})`,
1638
+ `\u5B8C\u6210\u65F6\u95F4\uFF1A${receipt.finishedAt}`
1639
+ ];
1640
+ if (receipt.packageName) lines.push(`\u5305\u540D\uFF1A${receipt.packageName}`);
1641
+ if (receipt.changelog) lines.push(`\u66F4\u65B0\u65E5\u5FD7\uFF1A${receipt.changelog}`);
1642
+ for (const artifact of receipt.artifacts) {
1643
+ lines.push(
1644
+ `\u4EA7\u7269\uFF1A${artifact.path}${artifact.sizeBytes !== void 0 ? ` \xB7 ${artifact.sizeBytes} bytes` : ""}${artifact.sha256 ? ` \xB7 SHA-256 ${artifact.sha256}` : ""}`
1645
+ );
1646
+ }
1647
+ for (const target of receipt.targets) {
1648
+ lines.push(`${target.platform}\uFF1A${target.state} \xB7 ${target.message}`);
1649
+ if (target.remote?.consoleUrl) lines.push(`\u540E\u53F0\uFF1A${target.remote.consoleUrl}`);
1650
+ }
1651
+ return lines.join("\n");
1652
+ }
1653
+ var ReleaseReceiptStore = class {
1654
+ storageRoot;
1655
+ maxEntries;
1656
+ constructor(options = {}) {
1657
+ this.storageRoot = path3.resolve(options.storageRoot || path3.join(homedir(), ".flu-cli", "release-receipts"));
1658
+ this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_RELEASE_RECEIPT_LIMIT);
1659
+ }
1660
+ async append(receipt) {
1661
+ await mkdir(this.storageRoot, { recursive: true, mode: 448 });
1662
+ await chmod(this.storageRoot, 448);
1663
+ const filePath = this.getFilePath(receipt.projectKey);
1664
+ const current = await this.readFile(filePath, receipt.projectKey);
1665
+ const receipts = [receipt, ...current.receipts.filter((item) => item.id !== receipt.id)].slice(
1666
+ 0,
1667
+ this.maxEntries
1668
+ );
1669
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
1670
+ await writeFile(
1671
+ tempPath,
1672
+ `${JSON.stringify({
1673
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1674
+ projectKey: receipt.projectKey,
1675
+ receipts
1676
+ }, null, 2)}
1677
+ `,
1678
+ { encoding: "utf8", mode: 384 }
1679
+ );
1680
+ await rename(tempPath, filePath);
1681
+ await chmod(filePath, 384);
1682
+ }
1683
+ async list(projectRoot, limit = 20) {
1684
+ const canonicalRoot = await canonicalizeProjectRoot(projectRoot);
1685
+ const projectKey = createProjectKey(canonicalRoot);
1686
+ const data = await this.readFile(this.getFilePath(projectKey), projectKey);
1687
+ return data.receipts.slice(0, Math.max(0, limit));
1688
+ }
1689
+ async save(input) {
1690
+ const receipt = await createReleaseReceipt(input);
1691
+ await this.append(receipt);
1692
+ return receipt;
1693
+ }
1694
+ getFilePath(projectKey) {
1695
+ return path3.join(this.storageRoot, `${projectKey}.json`);
1696
+ }
1697
+ async readFile(filePath, projectKey) {
1698
+ try {
1699
+ const raw = JSON.parse(await readFile(filePath, "utf8"));
1700
+ return {
1701
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1702
+ projectKey,
1703
+ receipts: Array.isArray(raw.receipts) ? raw.receipts : []
1704
+ };
1705
+ } catch (error) {
1706
+ if (error?.code === "ENOENT") {
1707
+ return { schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION, projectKey, receipts: [] };
1708
+ }
1709
+ throw new Error(`\u8BFB\u53D6\u53D1\u5E03\u56DE\u6267\u5931\u8D25: ${sanitizeReleaseMessage(error?.message || error)}`);
1710
+ }
1711
+ }
1712
+ };
1713
+ function createProjectKey(projectRoot) {
1714
+ return createHash("sha256").update(path3.resolve(projectRoot)).digest("hex").slice(0, 24);
1715
+ }
1716
+ function createRemoteReceipt(result, data) {
1717
+ const identifiers = {};
1718
+ for (const key of REMOTE_IDENTIFIER_KEYS) {
1719
+ const value = data?.[key];
1720
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1721
+ identifiers[key] = typeof value === "string" ? sanitizeReleaseMessage(value, 240) : value;
1722
+ }
1723
+ }
1724
+ const consoleUrl = cleanHttpUrl(result.consoleUrl);
1725
+ const downloadUrl = cleanHttpUrl(result.url || result.link);
1726
+ if (!consoleUrl && !downloadUrl && Object.keys(identifiers).length === 0) return void 0;
1727
+ return {
1728
+ ...consoleUrl ? { consoleUrl } : {},
1729
+ ...downloadUrl ? { downloadUrl } : {},
1730
+ ...Object.keys(identifiers).length > 0 ? { identifiers } : {}
1731
+ };
1732
+ }
1733
+ function sanitizeTarget(target) {
1734
+ return {
1735
+ platform: sanitizeReleaseMessage(target.platform, 80) || "unknown",
1736
+ state: target.state,
1737
+ outcome: target.outcome,
1738
+ message: sanitizeReleaseMessage(target.message),
1739
+ ...target.remote ? { remote: sanitizeRemote(target.remote) } : {},
1740
+ ...cleanOptionalText(target.lastKnownRemoteStatus, 120) ? { lastKnownRemoteStatus: cleanOptionalText(target.lastKnownRemoteStatus, 120) } : {},
1741
+ ...cleanOptionalText(target.checkedAt, 80) ? { checkedAt: cleanOptionalText(target.checkedAt, 80) } : {}
1742
+ };
1743
+ }
1744
+ function sanitizeRemote(remote) {
1745
+ const identifiers = {};
1746
+ for (const [key, value] of Object.entries(remote.identifiers || {})) {
1747
+ if (!REMOTE_IDENTIFIER_KEYS.includes(key)) continue;
1748
+ if (typeof value === "string") identifiers[key] = sanitizeReleaseMessage(value, 240);
1749
+ if (typeof value === "number" || typeof value === "boolean") identifiers[key] = value;
1750
+ }
1751
+ const consoleUrl = cleanHttpUrl(remote.consoleUrl);
1752
+ const downloadUrl = cleanHttpUrl(remote.downloadUrl);
1753
+ return {
1754
+ ...consoleUrl ? { consoleUrl } : {},
1755
+ ...downloadUrl ? { downloadUrl } : {},
1756
+ ...Object.keys(identifiers).length > 0 ? { identifiers } : {}
1757
+ };
1758
+ }
1759
+ async function inspectArtifacts(value) {
1760
+ const paths = Array.from(new Set(flattenArtifactPaths(value).map((item) => path3.resolve(item))));
1761
+ return Promise.all(paths.map(inspectArtifact));
1762
+ }
1763
+ async function inspectArtifact(filePath) {
1764
+ const base = { path: filePath, name: path3.basename(filePath) };
1765
+ try {
1766
+ const info = await stat(filePath);
1767
+ if (!info.isFile()) return { ...base, available: false };
1768
+ return {
1769
+ ...base,
1770
+ sizeBytes: info.size,
1771
+ sha256: await hashFile(filePath),
1772
+ available: true
1773
+ };
1774
+ } catch {
1775
+ return { ...base, available: false };
1776
+ }
1777
+ }
1778
+ function hashFile(filePath) {
1779
+ return new Promise((resolve, reject) => {
1780
+ const hash = createHash("sha256");
1781
+ const stream = createReadStream(filePath);
1782
+ stream.on("error", reject);
1783
+ stream.on("data", (chunk) => hash.update(chunk));
1784
+ stream.on("end", () => resolve(hash.digest("hex")));
1785
+ });
1786
+ }
1787
+ function flattenArtifactPaths(value) {
1788
+ if (typeof value === "string" && value.trim()) return [value];
1789
+ if (Array.isArray(value)) return value.flatMap(flattenArtifactPaths);
1790
+ if (isRecord(value)) return Object.values(value).flatMap(flattenArtifactPaths);
1791
+ return [];
1792
+ }
1793
+ function inferOutcome(targets) {
1794
+ if (targets.length === 0) return "failed";
1795
+ if (targets.every((target) => target.outcome === "cancelled")) return "cancelled";
1796
+ const successCount = targets.filter((target) => target.outcome === "success").length;
1797
+ if (successCount === targets.length) return "success";
1798
+ if (successCount > 0) return "partial";
1799
+ return "failed";
1800
+ }
1801
+ async function canonicalizeProjectRoot(projectRoot) {
1802
+ const resolved = path3.resolve(projectRoot);
1803
+ try {
1804
+ return await realpath(resolved);
1805
+ } catch {
1806
+ return resolved;
1807
+ }
1808
+ }
1809
+ function toDate(value) {
1810
+ const date = value instanceof Date ? value : new Date(value);
1811
+ return Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
1812
+ }
1813
+ function cleanOptionalText(value, maxLength) {
1814
+ const cleaned = sanitizeReleaseMessage(value, maxLength);
1815
+ return cleaned || void 0;
1816
+ }
1817
+ function cleanHttpUrl(value) {
1818
+ if (typeof value !== "string") return void 0;
1819
+ try {
1820
+ const url = new URL(value);
1821
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
1822
+ for (const key of Array.from(url.searchParams.keys())) {
1823
+ if (/token|secret|password|signature|credential|key/i.test(key)) {
1824
+ url.searchParams.set(key, "[\u5DF2\u8131\u654F]");
1825
+ }
1826
+ }
1827
+ return url.toString();
1828
+ } catch {
1829
+ return void 0;
1830
+ }
1831
+ }
1832
+ function isRecord(value) {
1833
+ return !!value && typeof value === "object" && !Array.isArray(value);
1834
+ }
1835
+
1487
1836
  // src/adapters/pgyer_uploader.ts
1488
1837
  var PgyerUploader = class _PgyerUploader {
1489
1838
  name = "Pgyer";
@@ -1602,7 +1951,7 @@ var PgyerUploader = class _PgyerUploader {
1602
1951
 
1603
1952
  // src/adapters/huawei_uploader.ts
1604
1953
  import fs5 from "fs-extra";
1605
- import path3 from "path";
1954
+ import path4 from "path";
1606
1955
  import { promisify } from "util";
1607
1956
  import axios2 from "axios";
1608
1957
  import FormData2 from "form-data";
@@ -1681,7 +2030,7 @@ var HuaweiUploader = class _HuaweiUploader {
1681
2030
  const clientSecret = hwConfig.auth.clientSecret;
1682
2031
  const appId = hwConfig.appId;
1683
2032
  const releaseType = hwConfig.releaseType || "draft";
1684
- const suffix = path3.extname(filePath).substring(1);
2033
+ const suffix = path4.extname(filePath).substring(1);
1685
2034
  const retryConfig = config.retry;
1686
2035
  const tokenUrl = this.getTokenUrl(config);
1687
2036
  const publishApiRoot = this.getApiRoot(config);
@@ -1707,8 +2056,8 @@ var HuaweiUploader = class _HuaweiUploader {
1707
2056
  const uploadResults = [];
1708
2057
  const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
1709
2058
  if (!p || !fs5.existsSync(p)) return;
1710
- const currentSuffix = path3.extname(p).substring(1);
1711
- logger.info(`[Huawei] \u6B63\u5728\u4E0A\u4F20 ${abi} \u5305: ${path3.basename(p)}`);
2059
+ const currentSuffix = path4.extname(p).substring(1);
2060
+ logger.info(`[Huawei] \u6B63\u5728\u4E0A\u4F20 ${abi} \u5305: ${path4.basename(p)}`);
1712
2061
  const uploadInfo = await retryStrategy.execute(
1713
2062
  () => this.getUploadUrl(ctx, currentSuffix),
1714
2063
  `Huawei get upload URL for ${abi}`
@@ -1724,7 +2073,7 @@ var HuaweiUploader = class _HuaweiUploader {
1724
2073
  throw uploadFileRes.error || new Error(`File upload failed for ${abi}`);
1725
2074
  }
1726
2075
  uploadResults.push({
1727
- fileName: path3.basename(p),
2076
+ fileName: path4.basename(p),
1728
2077
  fileDestUrl: uploadFileRes.data[0].fileDestUrl
1729
2078
  });
1730
2079
  });
@@ -1896,7 +2245,7 @@ var HuaweiUploader = class _HuaweiUploader {
1896
2245
  }
1897
2246
  /** 通用文件上传(FormData 方式,无需 API 认证头) */
1898
2247
  async executeUploadRequest(uploadUrl, authCode, filePath) {
1899
- const fileName = path3.basename(filePath);
2248
+ const fileName = path4.basename(filePath);
1900
2249
  const fileSize = fs5.statSync(filePath).size;
1901
2250
  const form = new FormData2();
1902
2251
  form.append("file", fs5.createReadStream(filePath), {
@@ -2063,7 +2412,7 @@ var HuaweiUploader = class _HuaweiUploader {
2063
2412
  logger.warn(`Asset file not found, skipping: ${filePath}`);
2064
2413
  continue;
2065
2414
  }
2066
- const suffix = path3.extname(filePath).substring(1).toLowerCase();
2415
+ const suffix = path4.extname(filePath).substring(1).toLowerCase();
2067
2416
  if (!HUAWEI_SUPPORTED_SUFFIXES.image.includes(suffix)) {
2068
2417
  logger.warn(`Unsupported file type ${suffix}, skipping: ${filePath}`);
2069
2418
  continue;
@@ -2118,7 +2467,7 @@ var HuaweiUploader = class _HuaweiUploader {
2118
2467
 
2119
2468
  // src/adapters/xiaomi_uploader.ts
2120
2469
  import fs6 from "fs-extra";
2121
- import path4 from "path";
2470
+ import path5 from "path";
2122
2471
  import axios3 from "axios";
2123
2472
  import FormData3 from "form-data";
2124
2473
  import crypto3 from "crypto";
@@ -2446,14 +2795,14 @@ ${lines.join("\n")}
2446
2795
  if (arm64 && arm32) {
2447
2796
  mainPath = arm64[1];
2448
2797
  secondPath = arm32[1];
2449
- 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)}`);
2798
+ logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u67B6\u6784\u5206\u5305: \u4E3B\u5305(64\u4F4D)=${path5.basename(mainPath)}, \u6B21\u5305(32\u4F4D)=${path5.basename(secondPath)}`);
2450
2799
  } else {
2451
2800
  mainPath = artifactEntries[0][1];
2452
2801
  if (artifactEntries.length > 1) {
2453
2802
  secondPath = artifactEntries[1][1];
2454
- 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)}`);
2803
+ logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u4E2A\u5305\uFF08\u975E\u6807\u51C6\u540D\uFF09\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u987A\u5E8F: \u4E3B\u5305=${path5.basename(mainPath)}, \u6B21\u5305=${path5.basename(secondPath)}`);
2455
2804
  } else {
2456
- logger.info(`[Xiaomi] \u4F7F\u7528\u5355\u5305\u6A21\u5F0F: ${path4.basename(mainPath)}`);
2805
+ logger.info(`[Xiaomi] \u4F7F\u7528\u5355\u5305\u6A21\u5F0F: ${path5.basename(mainPath)}`);
2457
2806
  }
2458
2807
  }
2459
2808
  const mainMD5 = await this.calculateFileMD5(mainPath);
@@ -2527,7 +2876,7 @@ ${lines.join("\n")}
2527
2876
 
2528
2877
  // src/adapters/apple_store_uploader.ts
2529
2878
  import fs7 from "fs-extra";
2530
- import path5 from "path";
2879
+ import path6 from "path";
2531
2880
  import { exec as exec2 } from "child_process";
2532
2881
  import { promisify as promisify2 } from "util";
2533
2882
  import axios4 from "axios";
@@ -2693,7 +3042,7 @@ var AppleStoreUploader = class {
2693
3042
  appPassword,
2694
3043
  "-showTransferRate"
2695
3044
  ].join(" ");
2696
- logger.info(`[Transporter] Uploading: ${path5.basename(ipaPath)}`);
3045
+ logger.info(`[Transporter] Uploading: ${path6.basename(ipaPath)}`);
2697
3046
  try {
2698
3047
  const { stdout, stderr } = await execAsync(command, {
2699
3048
  timeout: 30 * 60 * 1e3,
@@ -2744,7 +3093,7 @@ Stderr: ${error.stderr || ""}`);
2744
3093
  });
2745
3094
  command = innerArgs.join(" ");
2746
3095
  }
2747
- logger.info(`[altool] Uploading: ${path5.basename(ipaPath)}`);
3096
+ logger.info(`[altool] Uploading: ${path6.basename(ipaPath)}`);
2748
3097
  logger.info(`[altool] Command prefix: ${tool.command}`);
2749
3098
  try {
2750
3099
  const { stdout, stderr } = await execAsync(command, {
@@ -3328,7 +3677,7 @@ var TestFlightUploader = class _TestFlightUploader {
3328
3677
 
3329
3678
  // src/adapters/adhoc_uploader.ts
3330
3679
  import fs9 from "fs-extra";
3331
- import path6 from "path";
3680
+ import path7 from "path";
3332
3681
  import { execSync } from "child_process";
3333
3682
  var AdHocUploader = class {
3334
3683
  name = "AdHoc";
@@ -3461,8 +3810,8 @@ var AdHocUploader = class {
3461
3810
  * @param changelog 更新日志
3462
3811
  */
3463
3812
  async createManifest(ipaPath, version, bundleId, changelog) {
3464
- const ipaFileName = path6.basename(ipaPath);
3465
- const appName = bundleId || path6.parse(ipaFileName).name;
3813
+ const ipaFileName = path7.basename(ipaPath);
3814
+ const appName = bundleId || path7.parse(ipaFileName).name;
3466
3815
  const ipaUrl = `itms-services://?action=download-manifest&url=https://your-distribution-server.com/manifests/${ipaFileName}.plist`;
3467
3816
  const manifestContent = `<?xml version="1.0" encoding="UTF-8"?>
3468
3817
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -3521,14 +3870,14 @@ var AdHocUploader = class {
3521
3870
  */
3522
3871
  async setupDistributionDirectory(ipaPath, manifestPath) {
3523
3872
  const projectRoot = process.cwd();
3524
- const distributionDir = path6.join(projectRoot, "build", "adhoc-distribution");
3873
+ const distributionDir = path7.join(projectRoot, "build", "adhoc-distribution");
3525
3874
  await fs9.ensureDir(distributionDir);
3526
- await fs9.ensureDir(path6.join(distributionDir, "apps"));
3527
- await fs9.ensureDir(path6.join(distributionDir, "manifests"));
3528
- await fs9.ensureDir(path6.join(distributionDir, "icons"));
3529
- const ipaFileName = path6.basename(ipaPath);
3530
- await fs9.copy(ipaPath, path6.join(distributionDir, "apps", ipaFileName));
3531
- await fs9.copy(manifestPath, path6.join(distributionDir, "manifests", `${ipaFileName}.plist`));
3875
+ await fs9.ensureDir(path7.join(distributionDir, "apps"));
3876
+ await fs9.ensureDir(path7.join(distributionDir, "manifests"));
3877
+ await fs9.ensureDir(path7.join(distributionDir, "icons"));
3878
+ const ipaFileName = path7.basename(ipaPath);
3879
+ await fs9.copy(ipaPath, path7.join(distributionDir, "apps", ipaFileName));
3880
+ await fs9.copy(manifestPath, path7.join(distributionDir, "manifests", `${ipaFileName}.plist`));
3532
3881
  logger.info(`Distribution directory: ${distributionDir}`);
3533
3882
  return distributionDir;
3534
3883
  }
@@ -3538,7 +3887,7 @@ var AdHocUploader = class {
3538
3887
  * @param bundleId Bundle ID
3539
3888
  */
3540
3889
  generateInstallUrl(distributionPath, bundleId) {
3541
- const manifestPath = path6.join(distributionPath, "manifests", `*.plist`);
3890
+ const manifestPath = path7.join(distributionPath, "manifests", `*.plist`);
3542
3891
  return `itms-services://?action=download-manifest&url=file://${manifestPath}`;
3543
3892
  }
3544
3893
  /**
@@ -3547,7 +3896,7 @@ var AdHocUploader = class {
3547
3896
  * @param distributionPath 分发目录
3548
3897
  */
3549
3898
  async generateQRCode(url, distributionPath) {
3550
- const qrCodePath = path6.join(distributionPath, "install-qrcode.png");
3899
+ const qrCodePath = path7.join(distributionPath, "install-qrcode.png");
3551
3900
  try {
3552
3901
  logger.info(`QR code would be saved to: ${qrCodePath}`);
3553
3902
  logger.info(`Install URL: ${url}`);
@@ -3574,8 +3923,8 @@ var AdHocUploader = class {
3574
3923
  logger.newLine();
3575
3924
  logger.info("\u{1F4E6} Distribution Files:");
3576
3925
  logger.info(` Directory: ${distributionPath}`);
3577
- logger.info(` IPA: ${path6.join(distributionPath, "apps", "*.ipa")}`);
3578
- logger.info(` Manifest: ${path6.join(distributionPath, "manifests", "*.plist")}`);
3926
+ logger.info(` IPA: ${path7.join(distributionPath, "apps", "*.ipa")}`);
3927
+ logger.info(` Manifest: ${path7.join(distributionPath, "manifests", "*.plist")}`);
3579
3928
  logger.info(` QR Code: ${qrCodePath}`);
3580
3929
  logger.newLine();
3581
3930
  logger.info("\u{1F517} Installation Link:");
@@ -3634,7 +3983,7 @@ var AdHocUploader = class {
3634
3983
 
3635
3984
  // src/adapters/harmony_uploader.ts
3636
3985
  import fs10 from "fs-extra";
3637
- import path7 from "path";
3986
+ import path8 from "path";
3638
3987
  import axios6 from "axios";
3639
3988
  import FormData5 from "form-data";
3640
3989
  var HarmonyUploader = class _HarmonyUploader {
@@ -3664,7 +4013,7 @@ var HarmonyUploader = class _HarmonyUploader {
3664
4013
  const clientSecret = harmonyConfig.auth.clientSecret;
3665
4014
  const appId = harmonyConfig.appId;
3666
4015
  const releaseType = harmonyConfig.releaseType || "draft";
3667
- const suffix = path7.extname(filePath).substring(1);
4016
+ const suffix = path8.extname(filePath).substring(1);
3668
4017
  const retryConfig = config.retry;
3669
4018
  logger.info("Starting HarmonyOS AppGallery upload...");
3670
4019
  try {
@@ -3781,7 +4130,7 @@ var HarmonyUploader = class _HarmonyUploader {
3781
4130
  * @param filePath 文件路径
3782
4131
  */
3783
4132
  async updateAppFileInfo(token, appId, authCode, filePath) {
3784
- const fileName = path7.basename(filePath);
4133
+ const fileName = path8.basename(filePath);
3785
4134
  const body = {
3786
4135
  fileType: 7,
3787
4136
  // 鸿蒙包的 fileType(需要根据华为最新文档确认)
@@ -3804,7 +4153,7 @@ var HarmonyUploader = class _HarmonyUploader {
3804
4153
 
3805
4154
  // src/adapters/oppo_uploader.ts
3806
4155
  import fs11 from "fs-extra";
3807
- import path8 from "path";
4156
+ import path9 from "path";
3808
4157
  import axios7 from "axios";
3809
4158
  import FormData6 from "form-data";
3810
4159
  import crypto4 from "crypto";
@@ -3845,7 +4194,7 @@ var OppoUploader = class _OppoUploader {
3845
4194
  * 尝试从文件名推断包名
3846
4195
  */
3847
4196
  inferPackageName(filePath) {
3848
- const fileName = path8.basename(filePath);
4197
+ const fileName = path9.basename(filePath);
3849
4198
  const match = fileName.match(/^([\w.]+?)(?:-(?:release|debug|unsigned))?\.apk$/i);
3850
4199
  if (match && match[1].includes(".")) {
3851
4200
  return match[1];
@@ -4126,7 +4475,7 @@ var OppoUploader = class _OppoUploader {
4126
4475
 
4127
4476
  // src/adapters/vivo_uploader.ts
4128
4477
  import fs12 from "fs-extra";
4129
- import path9 from "path";
4478
+ import path10 from "path";
4130
4479
  import axios8 from "axios";
4131
4480
  import FormData7 from "form-data";
4132
4481
  import crypto5 from "crypto";
@@ -4158,7 +4507,7 @@ var VivoUploader = class _VivoUploader {
4158
4507
  const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
4159
4508
  if (!p || !fs12.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
4160
4509
  const md5 = await this.calculateFileMD5(p);
4161
- logger.info(`[vivo] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${path9.basename(p)}`);
4510
+ logger.info(`[vivo] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${path10.basename(p)}`);
4162
4511
  const serialnumber = await this.uploadApk(accessKey, accessSecret, p, vivoConfig.packageName || "", md5);
4163
4512
  const pkgInfo = await parsePackageInfo(p, { androidSdkPath: config.globalSettings?.androidSdkPath });
4164
4513
  const nativeCodes = pkgInfo?.nativeCode || [];
@@ -4222,7 +4571,7 @@ var VivoUploader = class _VivoUploader {
4222
4571
  const form = new FormData7();
4223
4572
  Object.entries(params).forEach(([k, v]) => form.append(k, v));
4224
4573
  form.append("sign", sign);
4225
- form.append("file", fs12.createReadStream(filePath), { filename: path9.basename(filePath) });
4574
+ form.append("file", fs12.createReadStream(filePath), { filename: path10.basename(filePath) });
4226
4575
  const res = await axios8.post(_VivoUploader.API_BASE, form, { headers: form.getHeaders(), timeout: 3e5 });
4227
4576
  if (res.data.code !== 0) throw new Error(`\u4E0A\u4F20\u5305\u5931\u8D25: ${res.data.msg}`);
4228
4577
  return res.data.data?.serialnumber || res.data.data;
@@ -4262,7 +4611,7 @@ var VivoUploader = class _VivoUploader {
4262
4611
 
4263
4612
  // src/adapters/tencent_uploader.ts
4264
4613
  import fs13 from "fs-extra";
4265
- import path10 from "path";
4614
+ import path11 from "path";
4266
4615
  import axios9 from "axios";
4267
4616
  import crypto6 from "crypto";
4268
4617
  var TencentUploader = class _TencentUploader {
@@ -4302,7 +4651,7 @@ var TencentUploader = class _TencentUploader {
4302
4651
  const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
4303
4652
  if (!p || !fs13.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
4304
4653
  const stats = fs13.statSync(p);
4305
- const fileName = path10.basename(p);
4654
+ const fileName = path11.basename(p);
4306
4655
  logger.info(`[Tencent] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${fileName}`);
4307
4656
  const uploadInfo = await this.getFileUploadInfo(auth, stats.size, fileName, pkgName);
4308
4657
  await this.uploadToCos(uploadInfo.pre_sign_url, p, stats.size);
@@ -4505,26 +4854,255 @@ var TencentUploader = class _TencentUploader {
4505
4854
  return res.data;
4506
4855
  }
4507
4856
  };
4857
+
4858
+ // src/adapters/google_play_uploader.ts
4859
+ import fs14 from "fs-extra";
4860
+ import path12 from "path";
4861
+ import axios10 from "axios";
4862
+ import { GoogleAuth } from "google-auth-library";
4863
+ var ANDROID_PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher";
4864
+ var PUBLISH_API_ROOT = "https://androidpublisher.googleapis.com/androidpublisher/v3";
4865
+ var UPLOAD_API_ROOT = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3";
4866
+ var GOOGLE_PLAY_CONSOLE_URL = "https://play.google.com/console/developers";
4867
+ var GOOGLE_PLAY_INTERNAL_API_TRACK = "internal";
4868
+ var UPLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
4869
+ var GooglePlayUploader = class {
4870
+ name = "Google Play";
4871
+ httpClient;
4872
+ getAccessToken;
4873
+ constructor(dependencies = {}) {
4874
+ this.httpClient = dependencies.httpClient || axios10;
4875
+ this.getAccessToken = dependencies.getAccessToken || this.createAccessToken.bind(this);
4876
+ }
4877
+ validateConfig(config) {
4878
+ const googleConfig = config?.publish?.google_play;
4879
+ if (!googleConfig?.enable) return false;
4880
+ if (!googleConfig.packageName || typeof googleConfig.packageName !== "string") {
4881
+ logger.error("Google Play \u914D\u7F6E\u7F3A\u5C11 packageName");
4882
+ return false;
4883
+ }
4884
+ const track = googleConfig.track || "internal";
4885
+ if (track !== "internal") {
4886
+ logger.error("Google Play \u9996\u7248\u4EC5\u652F\u6301 internal \u8F68\u9053");
4887
+ return false;
4888
+ }
4889
+ const auth = googleConfig.auth;
4890
+ const hasCredentialContent = typeof auth?.serviceAccountJsonContent === "string" && auth.serviceAccountJsonContent.trim().length > 0;
4891
+ const credentialPath = auth?.serviceAccountJsonPath;
4892
+ const hasCredentialPath = typeof credentialPath === "string" && credentialPath.trim().length > 0 && fs14.existsSync(path12.resolve(credentialPath));
4893
+ if (!hasCredentialContent && !hasCredentialPath) {
4894
+ logger.error("Google Play \u914D\u7F6E\u7F3A\u5C11\u6709\u6548\u7684\u670D\u52A1\u8D26\u53F7 JSON \u6587\u4EF6\u6216\u5185\u5BB9");
4895
+ return false;
4896
+ }
4897
+ return true;
4898
+ }
4899
+ canHandle(filePath) {
4900
+ return path12.extname(filePath).toLowerCase() === ".aab";
4901
+ }
4902
+ async upload(filePath, config, _metadata) {
4903
+ if (!this.validateConfig(config)) {
4904
+ 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");
4905
+ }
4906
+ if (!this.canHandle(filePath)) {
4907
+ return this.failure("Google Play \u4EC5\u63A5\u53D7 Android App Bundle\uFF08.aab\uFF09");
4908
+ }
4909
+ if (!fs14.existsSync(filePath)) {
4910
+ return this.failure(`\u672A\u627E\u5230\u5F85\u4E0A\u4F20\u7684 AAB\uFF1A${filePath}`);
4911
+ }
4912
+ const googleConfig = config.publish.google_play;
4913
+ const packageName = googleConfig.packageName.trim();
4914
+ const encodedPackageName = encodeURIComponent(packageName);
4915
+ let editId;
4916
+ let accessToken;
4917
+ let committed = false;
4918
+ let stage = "create_edit";
4919
+ logger.info(`\u51C6\u5907\u5C06 ${path12.basename(filePath)} \u4E0A\u4F20\u5230 Google Play internal \u8349\u7A3F`);
4920
+ try {
4921
+ accessToken = await this.getAccessToken(googleConfig.auth);
4922
+ const requestConfig = {
4923
+ headers: { Authorization: `Bearer ${accessToken}` }
4924
+ };
4925
+ const editResponse = await this.httpClient.post(
4926
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits`,
4927
+ {},
4928
+ requestConfig
4929
+ );
4930
+ editId = editResponse.data.id;
4931
+ if (!editId) {
4932
+ throw new Error("Google Play \u672A\u8FD4\u56DE editId");
4933
+ }
4934
+ stage = "upload_bundle";
4935
+ const encodedEditId = encodeURIComponent(editId);
4936
+ const bundleResponse = await this.httpClient.post(
4937
+ `${UPLOAD_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}/bundles?uploadType=media`,
4938
+ fs14.createReadStream(filePath),
4939
+ {
4940
+ timeout: UPLOAD_TIMEOUT_MS,
4941
+ maxBodyLength: Infinity,
4942
+ maxContentLength: Infinity,
4943
+ headers: {
4944
+ Authorization: `Bearer ${accessToken}`,
4945
+ "Content-Type": "application/octet-stream",
4946
+ "Content-Length": fs14.statSync(filePath).size
4947
+ }
4948
+ }
4949
+ );
4950
+ const versionCode = bundleResponse.data.versionCode;
4951
+ if (!Number.isInteger(versionCode)) {
4952
+ throw new Error("Google Play \u4E0A\u4F20\u6210\u529F\u4F46\u672A\u8FD4\u56DE\u6709\u6548\u7684 versionCode");
4953
+ }
4954
+ stage = "update_internal_track";
4955
+ await this.httpClient.put(
4956
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}/tracks/${GOOGLE_PLAY_INTERNAL_API_TRACK}`,
4957
+ {
4958
+ track: GOOGLE_PLAY_INTERNAL_API_TRACK,
4959
+ releases: [
4960
+ {
4961
+ versionCodes: [String(versionCode)],
4962
+ status: "draft"
4963
+ }
4964
+ ]
4965
+ },
4966
+ requestConfig
4967
+ );
4968
+ stage = "validate_edit";
4969
+ await this.httpClient.post(
4970
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}:validate`,
4971
+ {},
4972
+ requestConfig
4973
+ );
4974
+ stage = "commit_edit";
4975
+ await this.httpClient.post(
4976
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}:commit`,
4977
+ {},
4978
+ requestConfig
4979
+ );
4980
+ committed = true;
4981
+ logger.success(`Google Play internal \u8349\u7A3F\u5DF2\u521B\u5EFA\uFF08versionCode ${versionCode}\uFF09`);
4982
+ return {
4983
+ platform: "google_play",
4984
+ success: true,
4985
+ message: `AAB \u5DF2\u4E0A\u4F20\u5230 Google Play internal \u8349\u7A3F\uFF08versionCode ${versionCode}\uFF09`,
4986
+ consoleUrl: GOOGLE_PLAY_CONSOLE_URL,
4987
+ data: {
4988
+ editId,
4989
+ versionCode,
4990
+ track: "internal",
4991
+ apiTrack: GOOGLE_PLAY_INTERNAL_API_TRACK,
4992
+ status: "draft",
4993
+ committed: true
4994
+ }
4995
+ };
4996
+ } catch (error) {
4997
+ if (editId && accessToken && !committed) {
4998
+ await this.deleteEditBestEffort(encodedPackageName, editId, accessToken);
4999
+ }
5000
+ const message = this.describeError(error, stage);
5001
+ logger.error(`Google Play \u4E0A\u4F20\u5931\u8D25\uFF1A${message}`);
5002
+ return this.failure(message, error);
5003
+ }
5004
+ }
5005
+ async createAccessToken(authConfig) {
5006
+ let credentials;
5007
+ if (authConfig.serviceAccountJsonContent?.trim()) {
5008
+ try {
5009
+ credentials = JSON.parse(authConfig.serviceAccountJsonContent);
5010
+ } catch {
5011
+ throw new Error("\u670D\u52A1\u8D26\u53F7 JSON \u5185\u5BB9\u683C\u5F0F\u65E0\u6548");
5012
+ }
5013
+ }
5014
+ const keyFile = !credentials && authConfig.serviceAccountJsonPath ? path12.resolve(authConfig.serviceAccountJsonPath) : void 0;
5015
+ const auth = new GoogleAuth({
5016
+ scopes: [ANDROID_PUBLISHER_SCOPE],
5017
+ keyFile,
5018
+ credentials
5019
+ });
5020
+ const token = await auth.getAccessToken();
5021
+ if (!token) {
5022
+ throw new Error("Google Play \u8BA4\u8BC1\u672A\u8FD4\u56DE\u8BBF\u95EE\u4EE4\u724C");
5023
+ }
5024
+ return token;
5025
+ }
5026
+ async deleteEditBestEffort(encodedPackageName, editId, accessToken) {
5027
+ try {
5028
+ await this.httpClient.delete(
5029
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodeURIComponent(editId)}`,
5030
+ { headers: { Authorization: `Bearer ${accessToken}` } }
5031
+ );
5032
+ } catch {
5033
+ 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");
5034
+ }
5035
+ }
5036
+ describeError(error, stage) {
5037
+ const status = error?.response?.status;
5038
+ const rawMessage = error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || "\u672A\u77E5\u9519\u8BEF";
5039
+ const safeMessage = this.redactAndTruncate(String(rawMessage));
5040
+ if (/version\s*code|versionCode/i.test(safeMessage)) {
5041
+ return `Google Play \u62D2\u7EDD\u8BE5 versionCode\uFF0C\u8BF7\u63D0\u9AD8 Flutter \u6784\u5EFA\u53F7\u540E\u91CD\u65B0\u751F\u6210 AAB\uFF1A${safeMessage}`;
5042
+ }
5043
+ if (status === 401) {
5044
+ return "Google Play \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u8D26\u53F7 JSON \u662F\u5426\u6709\u6548";
5045
+ }
5046
+ if (status === 403) {
5047
+ 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";
5048
+ }
5049
+ if (status === 404) {
5050
+ if (stage === "update_internal_track") {
5051
+ 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";
5052
+ }
5053
+ if (stage !== "create_edit") {
5054
+ return `Google Play API \u5728 ${stage} \u9636\u6BB5\u672A\u627E\u5230\u76EE\u6807\u8D44\u6E90\uFF1A${safeMessage}`;
5055
+ }
5056
+ 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";
5057
+ }
5058
+ if (error?.code === "ECONNABORTED" || /timeout/i.test(safeMessage)) {
5059
+ return "Google Play \u4E0A\u4F20\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\uFF1B\u672C\u5730 AAB \u672A\u88AB\u5220\u9664";
5060
+ }
5061
+ return `Google Play API \u8BF7\u6C42\u5931\u8D25${status ? `\uFF08HTTP ${status}\uFF09` : ""}\uFF1A${safeMessage}`;
5062
+ }
5063
+ redactAndTruncate(message) {
5064
+ 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);
5065
+ }
5066
+ failure(message, error) {
5067
+ return {
5068
+ platform: "google_play",
5069
+ success: false,
5070
+ message,
5071
+ // 原始 Axios 错误可能包含 Authorization 请求头,只返回脱敏后的 Error。
5072
+ error: error ? new Error(message) : void 0
5073
+ };
5074
+ }
5075
+ };
4508
5076
  export {
4509
5077
  AdHocUploader,
4510
5078
  AppleJWTGenerator,
4511
5079
  AppleStoreUploader,
4512
5080
  ConfigValidator,
5081
+ DEFAULT_RELEASE_RECEIPT_LIMIT,
4513
5082
  DEFAULT_RETRY_CONFIG,
4514
5083
  EnvValidator,
4515
5084
  ErrorCategory,
5085
+ GooglePlayUploader,
4516
5086
  HarmonyUploader,
4517
5087
  HuaweiUploader,
4518
5088
  OppoUploader,
4519
5089
  PgyerUploader,
5090
+ RELEASE_RECEIPT_SCHEMA_VERSION,
5091
+ ReleaseReceiptStore,
4520
5092
  RetryStrategy,
4521
5093
  TencentUploader,
4522
5094
  TestFlightUploader,
4523
5095
  UploadManager,
4524
5096
  VivoUploader,
4525
5097
  XiaomiUploader,
5098
+ createProjectKey,
5099
+ createReleaseReceipt,
4526
5100
  createUploadManager,
4527
5101
  createUploadRetryStrategy,
4528
- parsePackageInfo
5102
+ createUploadTargets,
5103
+ formatReleaseReceipt,
5104
+ parsePackageInfo,
5105
+ resolveReleaseProjectRoot,
5106
+ sanitizeReleaseMessage
4529
5107
  };
4530
5108
  //# sourceMappingURL=index.js.map