shipup 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/CHANGELOG.md +25 -0
- package/LICENSE +14 -0
- package/README.md +112 -0
- package/README.zh-CN.md +105 -0
- package/SECURITY.md +32 -0
- package/creds.example.yaml +62 -0
- package/dist/adapters/android/honor/api.js +360 -0
- package/dist/adapters/android/honor/api.js.map +1 -0
- package/dist/adapters/android/honor/index.js +122 -0
- package/dist/adapters/android/honor/index.js.map +1 -0
- package/dist/adapters/android/huawei/api.js +389 -0
- package/dist/adapters/android/huawei/api.js.map +1 -0
- package/dist/adapters/android/huawei/index.js +128 -0
- package/dist/adapters/android/huawei/index.js.map +1 -0
- package/dist/adapters/android/meizu/api.js +367 -0
- package/dist/adapters/android/meizu/api.js.map +1 -0
- package/dist/adapters/android/meizu/index.js +129 -0
- package/dist/adapters/android/meizu/index.js.map +1 -0
- package/dist/adapters/android/oppo/api.js +397 -0
- package/dist/adapters/android/oppo/api.js.map +1 -0
- package/dist/adapters/android/oppo/index.js +135 -0
- package/dist/adapters/android/oppo/index.js.map +1 -0
- package/dist/adapters/android/qq/api.js +184 -0
- package/dist/adapters/android/qq/api.js.map +1 -0
- package/dist/adapters/android/qq/index.js +80 -0
- package/dist/adapters/android/qq/index.js.map +1 -0
- package/dist/adapters/android/samsung/api.js +355 -0
- package/dist/adapters/android/samsung/api.js.map +1 -0
- package/dist/adapters/android/samsung/index.js +111 -0
- package/dist/adapters/android/samsung/index.js.map +1 -0
- package/dist/adapters/android/specs.js +106 -0
- package/dist/adapters/android/specs.js.map +1 -0
- package/dist/adapters/android/stub.js +27 -0
- package/dist/adapters/android/stub.js.map +1 -0
- package/dist/adapters/android/vivo/api.js +236 -0
- package/dist/adapters/android/vivo/api.js.map +1 -0
- package/dist/adapters/android/vivo/index.js +121 -0
- package/dist/adapters/android/vivo/index.js.map +1 -0
- package/dist/adapters/android/xiaomi/api.js +163 -0
- package/dist/adapters/android/xiaomi/api.js.map +1 -0
- package/dist/adapters/android/xiaomi/index.js +108 -0
- package/dist/adapters/android/xiaomi/index.js.map +1 -0
- package/dist/adapters/ios/appstore/asc.js +280 -0
- package/dist/adapters/ios/appstore/asc.js.map +1 -0
- package/dist/adapters/ios/appstore/index.js +323 -0
- package/dist/adapters/ios/appstore/index.js.map +1 -0
- package/dist/adapters/registry.js +34 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/util.js +26 -0
- package/dist/adapters/util.js.map +1 -0
- package/dist/cli/index.js +479 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.js +65 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/core/exit.js +20 -0
- package/dist/core/exit.js.map +1 -0
- package/dist/core/orchestrator.js +24 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/types.js +20 -0
- package/dist/core/types.js.map +1 -0
- package/dist/creds/load.js +68 -0
- package/dist/creds/load.js.map +1 -0
- package/dist/creds/schema.js +30 -0
- package/dist/creds/schema.js.map +1 -0
- package/dist/infra/crypto.js +12 -0
- package/dist/infra/crypto.js.map +1 -0
- package/dist/infra/http.js +87 -0
- package/dist/infra/http.js.map +1 -0
- package/dist/pkginfo/apk-icon.js +89 -0
- package/dist/pkginfo/apk-icon.js.map +1 -0
- package/dist/pkginfo/icon.js +83 -0
- package/dist/pkginfo/icon.js.map +1 -0
- package/dist/pkginfo/index.js +33 -0
- package/dist/pkginfo/index.js.map +1 -0
- package/docs/android-markets.md +190 -0
- package/docs/credentials.md +72 -0
- package/docs/providers.md +60 -0
- package/lib/common.mjs +265 -0
- package/lib/harmony.mjs +190 -0
- package/lib/huawei.mjs +153 -0
- package/lib/ios.mjs +477 -0
- package/package.json +70 -0
- package/shipup.mjs +160 -0
package/lib/harmony.mjs
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// AppGallery Connect Publishing API v3(HarmonyOS 应用/元服务 .app 包)。
|
|
2
|
+
// 链路:预签名 OBS 传包 → app-package-info 登记 → 轮询编译解析 → (可选 newFeatures)→ app-submit 提审。
|
|
3
|
+
// 依据官方 2026-06-25 版文档(agc-help-publish-api-guide-0000002271134665)。
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFileSync, statSync, createReadStream, existsSync } from "node:fs";
|
|
6
|
+
import { resolve, basename } from "node:path";
|
|
7
|
+
import { request as httpsRequest } from "node:https";
|
|
8
|
+
import {
|
|
9
|
+
CliError, ExitCode, log, agc, agcAuth, sleep, remainingMs, resolveValue, readZipEntry,
|
|
10
|
+
} from "./common.mjs";
|
|
11
|
+
|
|
12
|
+
// releaseState(releaseType=1 全网)→ 归一化状态
|
|
13
|
+
const RELEASE_STATE = {
|
|
14
|
+
0: ["published", "已上架"],
|
|
15
|
+
1: ["rejected", "上架审核不通过"],
|
|
16
|
+
2: ["offline", "已下架(含强制下架)"],
|
|
17
|
+
3: ["approved", "待上架(预约上架)"],
|
|
18
|
+
4: ["pending_review", "审核中"],
|
|
19
|
+
5: ["pending_review", "升级审核中"],
|
|
20
|
+
6: ["pending_review", "申请下架(下架审核中)"],
|
|
21
|
+
7: ["draft", "草稿"],
|
|
22
|
+
8: ["rejected", "升级审核不通过"],
|
|
23
|
+
9: ["rejected", "下架审核不通过"],
|
|
24
|
+
10: ["offline", "应用被开发者下架"],
|
|
25
|
+
11: ["skipped", "撤销上架"],
|
|
26
|
+
12: ["pending_review", "预审中"],
|
|
27
|
+
13: ["rejected", "预审不通过"],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function requireAppId(creds) {
|
|
31
|
+
if (!creds.app_id) throw new CliError(ExitCode.CREDS, "harmony 凭证缺少 app_id(AGC → 我的APP → 查看应用信息)");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mapReleaseState(state) {
|
|
35
|
+
return RELEASE_STATE[state] ?? ["failed", `未知 releaseState=${state}`];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readPackInfo(filePath) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = readZipEntry(readFileSync(filePath), "pack.info");
|
|
41
|
+
const app = raw && JSON.parse(raw.toString("utf8"))?.summary?.app;
|
|
42
|
+
return app ? { bundleName: app.bundleName, versionName: app.version?.name, versionCode: app.version?.code } : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 预签名 OBS PUT:流式上传,原样回放 urlInfo.headers
|
|
49
|
+
function obsUpload(urlInfo, filePath, size, deadline) {
|
|
50
|
+
return new Promise((resolvePromise, reject) => {
|
|
51
|
+
const req = httpsRequest(new URL(urlInfo.url), {
|
|
52
|
+
method: urlInfo.method || "PUT",
|
|
53
|
+
headers: { ...(urlInfo.headers ?? {}), "Content-Length": size },
|
|
54
|
+
timeout: remainingMs(deadline),
|
|
55
|
+
}, (res) => {
|
|
56
|
+
let body = "";
|
|
57
|
+
res.on("data", (c) => (body += c));
|
|
58
|
+
res.on("end", () => {
|
|
59
|
+
if (res.statusCode === 200) resolvePromise();
|
|
60
|
+
else reject(new CliError(ExitCode.FAIL, `文件上传失败 HTTP ${res.statusCode}: ${body.slice(0, 300)}`, String(res.statusCode)));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
req.on("timeout", () => req.destroy(new Error("upload timeout")));
|
|
64
|
+
req.on("error", (e) => reject(new CliError(ExitCode.FAIL, `文件上传失败: ${e.message}`)));
|
|
65
|
+
createReadStream(filePath).on("error", (e) => req.destroy(e)).pipe(req);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function updateReleaseNote(creds, auth, opts, deadline) {
|
|
70
|
+
const lang = opts.lang || creds.default_lang || "zh-CN";
|
|
71
|
+
const note = resolveValue(opts.releaseNote, process.cwd());
|
|
72
|
+
if (note.length > 500) throw new CliError(ExitCode.USAGE, `release note 超长(${note.length} > 500)`);
|
|
73
|
+
await agc("PUT", "/publish/v3/app-language-info", {
|
|
74
|
+
auth,
|
|
75
|
+
query: { appId: creds.app_id },
|
|
76
|
+
body: { lang, newFeatures: note },
|
|
77
|
+
}, deadline);
|
|
78
|
+
log(`✓ 已更新 newFeatures(${lang},${note.length} 字)`);
|
|
79
|
+
return { lang, length: note.length };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function upload(creds, opts, deadline) {
|
|
83
|
+
requireAppId(creds);
|
|
84
|
+
const pkgPath = resolve(opts.package);
|
|
85
|
+
if (!existsSync(pkgPath)) throw new CliError(ExitCode.MISSING_INPUT, `软件包不存在: ${pkgPath}`);
|
|
86
|
+
const size = statSync(pkgPath).size;
|
|
87
|
+
const info = readPackInfo(pkgPath);
|
|
88
|
+
if (info) {
|
|
89
|
+
log(`软件包: ${basename(pkgPath)}(${(size / 1024 / 1024).toFixed(1)}MB)`);
|
|
90
|
+
log(` bundleName=${info.bundleName} versionName=${info.versionName} versionCode=${info.versionCode}`);
|
|
91
|
+
if (creds.package_name && info.bundleName !== creds.package_name) {
|
|
92
|
+
throw new CliError(ExitCode.MISSING_INPUT,
|
|
93
|
+
`包名不匹配: 软件包是 ${info.bundleName},凭证 package_name 是 ${creds.package_name}(防止传错 app)`);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
log(`软件包: ${basename(pkgPath)}(${(size / 1024 / 1024).toFixed(1)}MB)— pack.info 解析失败,跳过包名校验`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (opts.dryRun) {
|
|
100
|
+
log(`[dry-run] 将上传到 appId=${creds.app_id},不发请求`);
|
|
101
|
+
return { status: "skipped", message: "dry-run", ...info };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
log("计算 SHA256 …");
|
|
105
|
+
const sha256 = createHash("sha256").update(readFileSync(pkgPath)).digest("hex");
|
|
106
|
+
const auth = await agcAuth(creds, deadline);
|
|
107
|
+
|
|
108
|
+
log("获取上传地址 …");
|
|
109
|
+
const query = { appId: creds.app_id, fileName: basename(pkgPath), contentLength: String(size), sha256 };
|
|
110
|
+
if (creds.chinese_mainland_flag) query.chineseMainlandFlag = creds.chinese_mainland_flag;
|
|
111
|
+
const { urlInfo } = await agc("GET", "/publish/v2/upload-url/for-obs", { auth, query }, deadline);
|
|
112
|
+
|
|
113
|
+
log("上传软件包 …");
|
|
114
|
+
await obsUpload(urlInfo, pkgPath, size, deadline);
|
|
115
|
+
log("✓ 上传完成");
|
|
116
|
+
|
|
117
|
+
const { packageId } = await agc("PUT", "/publish/v3/app-package-info", {
|
|
118
|
+
auth,
|
|
119
|
+
query: { appId: creds.app_id },
|
|
120
|
+
body: { fileName: basename(pkgPath), objectId: urlInfo.objectId },
|
|
121
|
+
}, deadline);
|
|
122
|
+
log(`✓ 软件包已登记 packageId=${packageId}`);
|
|
123
|
+
|
|
124
|
+
if (!opts.noWait) {
|
|
125
|
+
log("等待编译解析(约 2 分钟)…");
|
|
126
|
+
for (;;) {
|
|
127
|
+
await sleep(15_000, deadline);
|
|
128
|
+
const { pkgStateList } = await agc("GET", "/publish/v3/package/compile/status", {
|
|
129
|
+
auth,
|
|
130
|
+
query: { appId: creds.app_id, pkgIds: packageId },
|
|
131
|
+
}, deadline);
|
|
132
|
+
const state = pkgStateList?.[0]?.successStatus;
|
|
133
|
+
if (state === 0) { log("✓ 解析正常,可提交审核"); break; }
|
|
134
|
+
if (state === 2) throw new CliError(ExitCode.FAIL, "软件包解析失败(successStatus=2),该包不可用", "compile_failed");
|
|
135
|
+
log(" … 解析中");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let noteInfo;
|
|
140
|
+
if (opts.releaseNote) noteInfo = await updateReleaseNote(creds, auth, opts, deadline);
|
|
141
|
+
|
|
142
|
+
return { status: "uploaded", packageId, ...info, releaseNote: noteInfo };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function submit(creds, opts, deadline) {
|
|
146
|
+
requireAppId(creds);
|
|
147
|
+
const body = { releaseType: 1, releasePhase: opts.phased ? 3 : 0 };
|
|
148
|
+
if (opts.remark) body.remark = opts.remark;
|
|
149
|
+
if (opts.releaseTime) body.releaseTime = opts.releaseTime;
|
|
150
|
+
if (opts.phased) body.phasedReleaseDescription = opts.phasedDesc;
|
|
151
|
+
|
|
152
|
+
if (opts.dryRun) {
|
|
153
|
+
log(`[dry-run] 将提交发布 appId=${creds.app_id} body=${JSON.stringify(body)}`);
|
|
154
|
+
return { status: "skipped", message: "dry-run" };
|
|
155
|
+
}
|
|
156
|
+
const auth = await agcAuth(creds, deadline);
|
|
157
|
+
let noteInfo;
|
|
158
|
+
if (opts.releaseNote) noteInfo = await updateReleaseNote(creds, auth, opts, deadline);
|
|
159
|
+
|
|
160
|
+
await agc("POST", "/publish/v3/app-submit", { auth, query: { appId: creds.app_id }, body }, deadline);
|
|
161
|
+
log(`✓ 已提交审核(${opts.phased ? "分阶段发布" : "全网发布"}${opts.releaseTime ? `,上架时间 ${opts.releaseTime}` : ""})`);
|
|
162
|
+
return { status: "submitted", phased: !!opts.phased, releaseNote: noteInfo };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function status(creds, opts, deadline) {
|
|
166
|
+
requireAppId(creds);
|
|
167
|
+
if (opts.dryRun) return { status: "skipped", message: "dry-run" };
|
|
168
|
+
const auth = await agcAuth(creds, deadline);
|
|
169
|
+
const res = await agc("GET", "/publish/v3/app-info", {
|
|
170
|
+
auth,
|
|
171
|
+
query: { appId: creds.app_id, lang: opts.lang || creds.default_lang || "zh-CN" },
|
|
172
|
+
}, deadline);
|
|
173
|
+
const a = res.appInfo ?? {};
|
|
174
|
+
const [normalized, label] = mapReleaseState(a.releaseState);
|
|
175
|
+
const auditOpinion = res.auditInfo?.auditOpinion || "";
|
|
176
|
+
log(`releaseState=${a.releaseState}(${label})`);
|
|
177
|
+
log(` 提审版本: ${a.versionNumber ?? "-"}(versionCode=${a.versionCode ?? "-"}) 在架版本: ${a.onShelfVersionNumber ?? "-"}`);
|
|
178
|
+
if (auditOpinion) log(` 审核意见: ${auditOpinion}`);
|
|
179
|
+
return {
|
|
180
|
+
status: normalized,
|
|
181
|
+
message: label,
|
|
182
|
+
releaseState: a.releaseState,
|
|
183
|
+
versionName: a.versionNumber,
|
|
184
|
+
versionCode: a.versionCode,
|
|
185
|
+
onShelfVersionName: a.onShelfVersionNumber,
|
|
186
|
+
reviewState: a.reviewState,
|
|
187
|
+
auditOpinion,
|
|
188
|
+
releaseTime: a.releaseTime,
|
|
189
|
+
};
|
|
190
|
+
}
|
package/lib/huawei.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// 华为应用市场(AppGallery,安卓 APK),Publish API v2。
|
|
2
|
+
// 发布链路:
|
|
3
|
+
// 取上传地址(upload-url?suffix=apk) → multipart 直传(authCode)→ app-file-info 绑定(fileType=5)
|
|
4
|
+
// →(可选 newFeatures)→(可选 --submit-review:app-submit,204144727 处理中自动重试 ≤30 次)。
|
|
5
|
+
// 已知坑:取地址返回的字段拼写是 fileDestUlr,绑定接口要的是 fileDestUrl,序列化时统一修正。
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { resolve, basename } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
CliError, ExitCode, log, agc, agcAuth, fetchJson, sleep, resolveValue, AGC_API_BASE,
|
|
10
|
+
} from "./common.mjs";
|
|
11
|
+
|
|
12
|
+
const V2 = "/publish/v2";
|
|
13
|
+
|
|
14
|
+
// 华为安卓 releaseState → 归一化
|
|
15
|
+
export function mapReleaseState(state) {
|
|
16
|
+
switch (state) {
|
|
17
|
+
case 0: return ["published", "已上架"];
|
|
18
|
+
case 1: case 8: case 9: case 13: return ["rejected", "审核不通过"];
|
|
19
|
+
case 3: case 4: case 5: case 12: return ["pending_review", "审核/预审中"];
|
|
20
|
+
case 2: case 6: case 10: case 11: return ["offline", "已下架/撤销"];
|
|
21
|
+
case 7: return ["uploaded", "草稿"];
|
|
22
|
+
default: return ["submitted", `releaseState=${state}(暂无更细状态)`];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireAppId(creds) {
|
|
27
|
+
if (!creds.app_id) throw new CliError(ExitCode.CREDS, "huawei 凭证缺少 app_id(AGC → 我的APP,安卓应用的 ID)");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** multipart 直传文件到华为文件服务器,返回 fileInfoList。 */
|
|
31
|
+
async function uploadFileMultipart(auth, creds, bytes, fileName, deadline) {
|
|
32
|
+
const suffix = fileName.split(".").pop();
|
|
33
|
+
const { uploadUrl, authCode } = await agc("GET", `${V2}/upload-url`, {
|
|
34
|
+
auth,
|
|
35
|
+
query: { appId: creds.app_id, suffix },
|
|
36
|
+
}, deadline);
|
|
37
|
+
if (!uploadUrl || !authCode) throw new CliError(ExitCode.FAIL, "取上传地址响应缺少 uploadUrl/authCode");
|
|
38
|
+
|
|
39
|
+
const form = new FormData();
|
|
40
|
+
form.append("authCode", authCode);
|
|
41
|
+
form.append("fileCount", "1");
|
|
42
|
+
form.append("parseType", "0");
|
|
43
|
+
form.append("file", new Blob([bytes]), fileName);
|
|
44
|
+
|
|
45
|
+
// 不手动设 Content-Type,交给 fetch 生成 multipart boundary
|
|
46
|
+
const data = await fetchJson(uploadUrl, { method: "POST", headers: { ...auth }, body: form }, deadline);
|
|
47
|
+
const rsp = data?.result?.UploadFileRsp;
|
|
48
|
+
if (!rsp || !Array.isArray(rsp.fileInfoList) || rsp.fileInfoList.length === 0) {
|
|
49
|
+
throw new CliError(ExitCode.FAIL, `上传响应缺少 fileInfoList: ${JSON.stringify(rsp ?? data).slice(0, 300)}`);
|
|
50
|
+
}
|
|
51
|
+
rsp.fileInfoList[0].fileName = fileName;
|
|
52
|
+
return rsp.fileInfoList;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** app-file-info 绑定(华为接口要 fileDestUrl,上游返回的是拼写错误的 fileDestUlr,需修正)。 */
|
|
56
|
+
async function bindFileInfo(auth, creds, fileType, files, deadline) {
|
|
57
|
+
const serialized = JSON.stringify({ fileType, files, lang: "zh-CN" }).replace(/fileDestUlr/g, "fileDestUrl");
|
|
58
|
+
const res = await fetchJson(`${AGC_API_BASE}${V2}/app-file-info?appId=${creds.app_id}`, {
|
|
59
|
+
method: "PUT",
|
|
60
|
+
headers: { ...auth, "Content-Type": "application/json" },
|
|
61
|
+
body: serialized,
|
|
62
|
+
}, deadline);
|
|
63
|
+
if (res.ret && Number(res.ret.code) !== 0) {
|
|
64
|
+
throw new CliError(ExitCode.FAIL, `app-file-info 失败: [${res.ret.code}] ${res.ret.msg}`, String(res.ret.code));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function updateReleaseNote(creds, auth, opts, deadline) {
|
|
69
|
+
const lang = opts.lang || creds.default_lang || "zh-CN";
|
|
70
|
+
const note = resolveValue(opts.releaseNote, process.cwd());
|
|
71
|
+
if (note.length > 500) throw new CliError(ExitCode.USAGE, `release note 超长(${note.length} > 500)`);
|
|
72
|
+
await agc("PUT", `${V2}/app-language-info`, {
|
|
73
|
+
auth,
|
|
74
|
+
query: { appId: creds.app_id },
|
|
75
|
+
body: { lang, newFeatures: note },
|
|
76
|
+
}, deadline);
|
|
77
|
+
log(`✓ 已更新 newFeatures(${lang},${note.length} 字)`);
|
|
78
|
+
return { lang, length: note.length };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 提交审核:ret.code==0 成功;204144727 表示服务端处理中,2s 后重试(≤30 次)。 */
|
|
82
|
+
async function submitReview(creds, auth, deadline) {
|
|
83
|
+
const body = { requestId: creds.package_name || String(Date.now()) };
|
|
84
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
85
|
+
const res = await fetchJson(`${AGC_API_BASE}${V2}/app-submit?appId=${creds.app_id}`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { ...auth, "Content-Type": "application/json" },
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
}, deadline);
|
|
90
|
+
const code = Number(res?.ret?.code ?? -1);
|
|
91
|
+
if (code === 0) return;
|
|
92
|
+
if (code === 204144727) {
|
|
93
|
+
log(" … 服务端处理中,2s 后重试提审");
|
|
94
|
+
await sleep(2000, deadline);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
throw new CliError(ExitCode.FAIL, `提交审核失败: [${code}] ${res?.ret?.msg ?? ""}`, String(code));
|
|
98
|
+
}
|
|
99
|
+
throw new CliError(ExitCode.FAIL, "提交审核失败:处理中状态重试超过 30 次", "204144727");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function upload(creds, opts, deadline) {
|
|
103
|
+
requireAppId(creds);
|
|
104
|
+
const pkgPath = resolve(opts.package);
|
|
105
|
+
if (!existsSync(pkgPath)) throw new CliError(ExitCode.MISSING_INPUT, `APK 不存在: ${pkgPath}`);
|
|
106
|
+
if (!pkgPath.endsWith(".apk")) throw new CliError(ExitCode.MISSING_INPUT, "huawei upload 只接受 .apk");
|
|
107
|
+
const bytes = readFileSync(pkgPath);
|
|
108
|
+
log(`APK: ${basename(pkgPath)}(${(bytes.length / 1024 / 1024).toFixed(1)}MB)`);
|
|
109
|
+
|
|
110
|
+
if (opts.dryRun) {
|
|
111
|
+
log(`[dry-run] 将上传到 appId=${creds.app_id}${opts.submitReview ? " 并提交审核" : ""},不发请求`);
|
|
112
|
+
return { status: "skipped", message: "dry-run" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const auth = await agcAuth(creds, deadline);
|
|
116
|
+
log("上传 APK …");
|
|
117
|
+
const files = await uploadFileMultipart(auth, creds, bytes, basename(pkgPath), deadline);
|
|
118
|
+
await bindFileInfo(auth, creds, 5, files, deadline);
|
|
119
|
+
log("✓ APK 已上传并绑定");
|
|
120
|
+
|
|
121
|
+
let noteInfo;
|
|
122
|
+
if (opts.releaseNote) noteInfo = await updateReleaseNote(creds, auth, opts, deadline);
|
|
123
|
+
|
|
124
|
+
if (opts.submitReview) {
|
|
125
|
+
await submitReview(creds, auth, deadline);
|
|
126
|
+
log("✓ 已提交审核");
|
|
127
|
+
return { status: "submitted", releaseNote: noteInfo };
|
|
128
|
+
}
|
|
129
|
+
log("(未提审:加 --submit-review 或稍后在 AGC 网页提交)");
|
|
130
|
+
return { status: "uploaded", releaseNote: noteInfo };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function status(creds, opts, deadline) {
|
|
134
|
+
requireAppId(creds);
|
|
135
|
+
if (opts.dryRun) return { status: "skipped", message: "dry-run" };
|
|
136
|
+
const auth = await agcAuth(creds, deadline);
|
|
137
|
+
const res = await agc("GET", `${V2}/app-info`, { auth, query: { appId: creds.app_id } }, deadline);
|
|
138
|
+
const a = res?.appInfo ?? res ?? {};
|
|
139
|
+
const raw = a.releaseState === undefined || a.releaseState === null || a.releaseState === "" ? null : Number(a.releaseState);
|
|
140
|
+
const [normalized, label] = mapReleaseState(raw);
|
|
141
|
+
const auditOpinion = res?.auditInfo?.auditOpinion || "";
|
|
142
|
+
log(`releaseState=${raw}(${label})`);
|
|
143
|
+
log(` 提审版本: ${a.versionNumber ?? "-"} 在架版本: ${a.onShelfVersionNumber ?? "-"}`);
|
|
144
|
+
if (auditOpinion) log(` 审核意见: ${auditOpinion}`);
|
|
145
|
+
return {
|
|
146
|
+
status: normalized,
|
|
147
|
+
message: label,
|
|
148
|
+
releaseState: raw,
|
|
149
|
+
versionName: a.versionNumber,
|
|
150
|
+
onShelfVersionName: a.onShelfVersionNumber,
|
|
151
|
+
auditOpinion,
|
|
152
|
+
};
|
|
153
|
+
}
|