@applift/core 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.d.mts +40 -12
- package/dist/index.mjs +296 -142
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -13,17 +13,17 @@ export declare const registry: DefaultChannelRegistry;
|
|
|
13
13
|
export declare const APPLIFT_HOME: string;
|
|
14
14
|
export declare const CREDENTIALS_PATH: string;
|
|
15
15
|
export declare const GLOBAL_CONFIG_PATH: string;
|
|
16
|
-
/** 项目级凭据目录(<project>/.applift/credentials.
|
|
16
|
+
/** 项目级凭据目录(<project>/.applift/credentials.json;gitignore 由用户决定:私有仓可提交,公开仓 ignore) */
|
|
17
17
|
export declare function projectCredentialsPath(projectRoot: string): string;
|
|
18
18
|
/** 从 startDir 逐层向上找第一个存在的路径(monorepo 子包向上借用根配置/凭据);找不到返回 null */
|
|
19
19
|
export declare function findUp(startDir: string, relative: string): string | null;
|
|
20
20
|
/**
|
|
21
|
-
* 逐级凭据加载:项目 <root>/.applift/credentials.
|
|
21
|
+
* 逐级凭据加载:项目 <root>/.applift/credentials.json(就近,逐键优先)→ 全局 ~/.applift/(逐键回退)。
|
|
22
22
|
* 场景:私有仓提交凭据进仓库;或公开仓 .applift/ 进 gitignore 由成员各自维护。
|
|
23
23
|
* profile 合并:项目同名 profile 逐键覆盖全局;仅全局有则用全局。
|
|
24
24
|
*/
|
|
25
25
|
export declare function loadCredentialsLayered(projectRoot?: string, globalPath?: string): CredentialsFile;
|
|
26
|
-
/** 读取并校验 credentials.
|
|
26
|
+
/** 读取并校验 credentials.json;不存在返回空 profiles;强制 0600 */
|
|
27
27
|
export declare function loadCredentials(filePath?: string): CredentialsFile;
|
|
28
28
|
/** 按项目 profile 取凭据;profile 不存在时报错并列出可用 profiles。
|
|
29
29
|
* 环境变量覆盖(CI 场景):APPLIFT_<KEY> 前缀优先(如 APPLIFT_PGYER_API_KEY);
|
|
@@ -144,8 +144,9 @@ export declare class HuaweiAndroidChannel implements Channel, ListingCapableChan
|
|
|
144
144
|
stage(ctx: ChannelContext): Promise<StageResult>;
|
|
145
145
|
submit(ctx: ChannelContext): Promise<SubmitResult>;
|
|
146
146
|
/**
|
|
147
|
-
* 更新商店应用信息:简介/描述走
|
|
148
|
-
*
|
|
147
|
+
* 更新商店应用信息:简介/描述走 v2 app-language-info(briefInfo/appDesc)。
|
|
148
|
+
* 注:v3 app-language-info 为鸿蒙(HarmonyOS)专属接口族,安卓现行文档只有 v2。
|
|
149
|
+
* 图标/截图:华为 app-file-info 的 fileType 取值未在官方文档确证(软件包=5 已验证,图标清单值不确定),
|
|
149
150
|
* 未经真机验证前只上传 OBS 不做 file-info 绑定(skip 并在结果中标注),避免误写损坏草稿。
|
|
150
151
|
*/
|
|
151
152
|
updateListing(ctx: Pick<ChannelContext, "credentials" | "target" | "emit">, listing: ListingInput): Promise<ListingResult>;
|
|
@@ -191,6 +192,9 @@ export declare class OppoChannel implements Channel {
|
|
|
191
192
|
"911215": string;
|
|
192
193
|
"911216": string;
|
|
193
194
|
"800002": string;
|
|
195
|
+
"910003": string;
|
|
196
|
+
"911045": string;
|
|
197
|
+
"300002": string;
|
|
194
198
|
};
|
|
195
199
|
/** stage 产物暂存(apkUrl/md5),submit 按 pkgName 取回 */
|
|
196
200
|
private readonly staged;
|
|
@@ -204,9 +208,19 @@ export declare class OppoChannel implements Channel {
|
|
|
204
208
|
*/
|
|
205
209
|
private uploadApk;
|
|
206
210
|
submit(ctx: ChannelContext): Promise<SubmitResult>;
|
|
211
|
+
/**
|
|
212
|
+
* app/upd 异步任务结果轮询: POST /resource/v1/app/task-state(业务参数 pkg_name+version_code,
|
|
213
|
+
* 现行文档「获取任务状态」)。task_state: 1=待处理(继续等)、2=处理成功、3=处理失败(err_msg 透出);
|
|
214
|
+
* 间隔 10s × 最多 5 次,超时视为已受理但异步结果未知(warn 提示,稍后可用 status 查询审核状态)。
|
|
215
|
+
*/
|
|
216
|
+
private pollTaskState;
|
|
207
217
|
/**
|
|
208
218
|
* 审核状态查询: GET /resource/v1/app/info(公共参数 + api_sign,同 stage)
|
|
209
|
-
* → data.audit_status
|
|
219
|
+
* → data.audit_status 按现行「审核状态对照表」映射(id=10998):
|
|
220
|
+
* 1=审核中、4=运营审核中 → reviewing;2=审核通过、6=运营通过 → approved;111=上线 → approved+live;
|
|
221
|
+
* 3=测试不通过、5=运营打回、444=审核不通过 → rejected(原因读 refuse_reason,原样透出不解析);
|
|
222
|
+
* 0=未发布、7=定时发布、22=已冻结 → unknown+raw;其余编号(00/11/-11 资质类、222 下架等)→ unknown。
|
|
223
|
+
* 旧映射(1=待提审 2=审核中 3=通过 4=驳回,出自 python oppo_query.py)与现行对照表整体错位,已重写。
|
|
210
224
|
*/
|
|
211
225
|
status(ctx: Pick<ChannelContext, "credentials" | "target">): Promise<ReviewStatus>;
|
|
212
226
|
}
|
|
@@ -217,8 +231,10 @@ export declare class VivoChannel implements Channel {
|
|
|
217
231
|
readonly label = "vivo 应用市场";
|
|
218
232
|
readonly capabilities: ChannelCapabilities;
|
|
219
233
|
readonly errorHints: {
|
|
220
|
-
"
|
|
221
|
-
"
|
|
234
|
+
"10001": string;
|
|
235
|
+
"10005": string;
|
|
236
|
+
"10011": string;
|
|
237
|
+
"12010": string;
|
|
222
238
|
};
|
|
223
239
|
/**
|
|
224
240
|
* stage→submit 之间传递上传结果:Channel 接口不透传 ref,
|
|
@@ -239,6 +255,7 @@ export declare class VivoChannel implements Channel {
|
|
|
239
255
|
* 审核状态查询: 复用 app.query.details 的 data.auditStatus(对拍 python vivo_query.py;
|
|
240
256
|
* 未确证的 app.audit.status method 已删——python/apkgo 均无此调用)。
|
|
241
257
|
* 映射 0=审核通过 1=审核中 2=驳回 沿自 v1,待真机校准;
|
|
258
|
+
* 注意:该编号映射未在现行中国站文档成文(国际站同型接口字典编号整体错一位),需真机校准;
|
|
242
259
|
* 交叉见证:apkgo vivo.go 读 data.status 且编号不同(1草稿/2待审核/3通过/4不通过/5撤销),冲突以真机为准。
|
|
243
260
|
*/
|
|
244
261
|
status(ctx: Pick<ChannelContext, "credentials" | "target">): Promise<ReviewStatus>;
|
|
@@ -252,6 +269,10 @@ export declare class HonorChannel implements Channel {
|
|
|
252
269
|
readonly label = "荣耀应用市场";
|
|
253
270
|
readonly capabilities: ChannelCapabilities;
|
|
254
271
|
readonly errorHints: {
|
|
272
|
+
"10003": string;
|
|
273
|
+
"20022": string;
|
|
274
|
+
"30006": string;
|
|
275
|
+
"30011": string;
|
|
255
276
|
"100030001": string;
|
|
256
277
|
"100030002": string;
|
|
257
278
|
};
|
|
@@ -311,10 +332,17 @@ export declare class AgcHarmonyChannel implements Channel {
|
|
|
311
332
|
stage(ctx: ChannelContext): Promise<StageResult>;
|
|
312
333
|
/** submit = v1 releaseHarmonyApp 完整链路:上传+绑定+文案 → 等编译轮询 → 提审(带重试) */
|
|
313
334
|
submit(ctx: ChannelContext): Promise<SubmitResult>;
|
|
335
|
+
/** 轮询软件包云端编译:successStatus 0=成功 2=拒绝(终态,多为签名/证书/bundleName 不匹配)其他=编译中 */
|
|
336
|
+
private awaitCompile;
|
|
314
337
|
status(): Promise<ReviewStatus>;
|
|
315
338
|
/** v1 releaseHarmonyApp step 1-4:.app 校验 → 上传地址 → PUT 整包 → v3 绑定(releaseType=1)→ 文案(不阻断) */
|
|
316
339
|
private uploadFormal;
|
|
317
|
-
/**
|
|
340
|
+
/**
|
|
341
|
+
* 测试发布(releaseType=6,2026-10 起走 Testing API——旧 app-package-info 直绑链路已被华为下线,
|
|
342
|
+
* 其 releaseType 枚举仅剩 1=全网 / 7=企业非公开):
|
|
343
|
+
* 上传 .hap → 新建测试版本(testType=3 邀请测试)→ 添加测试软件包 → 等云端编译 → 更新测试版本绑定;
|
|
344
|
+
* 不自动提交审核,测试群组/邀请码由控制台人工完成。
|
|
345
|
+
*/
|
|
318
346
|
private uploadBeta;
|
|
319
347
|
/** v1 更新文案段(releaseHarmonyApp step 4 / uploadToAgc 尾段):查默认语言 → 写 newFeatures,失败仅警告不阻断 */
|
|
320
348
|
private updateReleaseNotes;
|
|
@@ -336,11 +364,11 @@ export declare class AscChannel implements Channel {
|
|
|
336
364
|
stage(ctx: ChannelContext): Promise<StageResult>;
|
|
337
365
|
/**
|
|
338
366
|
* 提审 App Store —— 按 ASC 官方文档新实现,未经真实链路验证。
|
|
339
|
-
* 文档:
|
|
367
|
+
* 文档: reviewSubmissions / Create a Review Submission / Submit a Review Submission
|
|
340
368
|
* https://developer.apple.com/documentation/appstoreconnectapi
|
|
341
369
|
* 链路: 查 app(bundleId → 数字 id)→ 查最新已处理(VALID)构建 → 读取/创建 appStoreVersion
|
|
342
|
-
* (releaseType 默认 MANUAL,ctx.releaseTime 提供时 SCHEDULED +
|
|
343
|
-
* → 绑定构建到版本 →
|
|
370
|
+
* (releaseType 默认 MANUAL,ctx.releaseTime 提供时 SCHEDULED + earliestReleaseDate)
|
|
371
|
+
* → 绑定构建到版本 → reviewSubmissions 三步流提交进入审核
|
|
344
372
|
*/
|
|
345
373
|
submit(ctx: ChannelContext): Promise<SubmitResult>;
|
|
346
374
|
status(): Promise<ReviewStatus>;
|
package/dist/index.mjs
CHANGED
|
@@ -28,8 +28,8 @@ const registry = new DefaultChannelRegistry();
|
|
|
28
28
|
//#region src/config/schema.ts
|
|
29
29
|
/**
|
|
30
30
|
* 配置三层:项目 applift.config.ts > 内置默认(全局行为配置独立于 ~/.applift/config.toml)。
|
|
31
|
-
* 凭据独立存放 ~/.applift/credentials.
|
|
32
|
-
* 加载用社区主流 c12(TS 配置执行 + 多层 merge),
|
|
31
|
+
* 凭据独立存放 ~/.applift/credentials.json(0600,按 profile 组织,永不进项目目录/git)。
|
|
32
|
+
* 加载用社区主流 c12(TS 配置执行 + 多层 merge),config.toml 用 smol-toml 解析,非自研。
|
|
33
33
|
*/
|
|
34
34
|
const CredentialsProfileSchema = z.record(z.string(), z.string());
|
|
35
35
|
const CredentialsFileSchema = z.object({
|
|
@@ -101,11 +101,11 @@ const GlobalConfigSchema = z.object({
|
|
|
101
101
|
* 凭据与配置装载:文件定位、profile 解析、0600 权限。
|
|
102
102
|
*/
|
|
103
103
|
const APPLIFT_HOME = process.env.APPLIFT_HOME || path.join(os.homedir(), ".applift");
|
|
104
|
-
const CREDENTIALS_PATH = path.join(APPLIFT_HOME, "credentials.
|
|
104
|
+
const CREDENTIALS_PATH = path.join(APPLIFT_HOME, "credentials.json");
|
|
105
105
|
const GLOBAL_CONFIG_PATH = path.join(APPLIFT_HOME, "config.toml");
|
|
106
|
-
/** 项目级凭据目录(<project>/.applift/credentials.
|
|
106
|
+
/** 项目级凭据目录(<project>/.applift/credentials.json;gitignore 由用户决定:私有仓可提交,公开仓 ignore) */
|
|
107
107
|
function projectCredentialsPath(projectRoot) {
|
|
108
|
-
return path.join(projectRoot, ".applift", "credentials.
|
|
108
|
+
return path.join(projectRoot, ".applift", "credentials.json");
|
|
109
109
|
}
|
|
110
110
|
/** 从 startDir 逐层向上找第一个存在的路径(monorepo 子包向上借用根配置/凭据);找不到返回 null */
|
|
111
111
|
function findUp(startDir, relative) {
|
|
@@ -119,14 +119,14 @@ function findUp(startDir, relative) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
|
-
* 逐级凭据加载:项目 <root>/.applift/credentials.
|
|
122
|
+
* 逐级凭据加载:项目 <root>/.applift/credentials.json(就近,逐键优先)→ 全局 ~/.applift/(逐键回退)。
|
|
123
123
|
* 场景:私有仓提交凭据进仓库;或公开仓 .applift/ 进 gitignore 由成员各自维护。
|
|
124
124
|
* profile 合并:项目同名 profile 逐键覆盖全局;仅全局有则用全局。
|
|
125
125
|
*/
|
|
126
126
|
function loadCredentialsLayered(projectRoot, globalPath = CREDENTIALS_PATH) {
|
|
127
127
|
const global = loadCredentials(globalPath);
|
|
128
128
|
if (!projectRoot) return global;
|
|
129
|
-
const localPath = findUp(projectRoot, path.join(".applift", "credentials.
|
|
129
|
+
const localPath = findUp(projectRoot, path.join(".applift", "credentials.json"));
|
|
130
130
|
if (!localPath) return global;
|
|
131
131
|
const local = loadCredentials(localPath);
|
|
132
132
|
const merged = {};
|
|
@@ -138,11 +138,11 @@ function loadCredentialsLayered(projectRoot, globalPath = CREDENTIALS_PATH) {
|
|
|
138
138
|
return CredentialsFileSchema.parse({ profiles: merged });
|
|
139
139
|
}
|
|
140
140
|
const EMPTY_CREDENTIALS = { profiles: {} };
|
|
141
|
-
/** 读取并校验 credentials.
|
|
141
|
+
/** 读取并校验 credentials.json;不存在返回空 profiles;强制 0600 */
|
|
142
142
|
function loadCredentials(filePath = CREDENTIALS_PATH) {
|
|
143
143
|
if (!fs.existsSync(filePath)) return EMPTY_CREDENTIALS;
|
|
144
144
|
if (fs.statSync(filePath).mode & 63) fs.chmodSync(filePath, 384);
|
|
145
|
-
const raw = parse(fs.readFileSync(filePath, "utf8"));
|
|
145
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
146
146
|
return CredentialsFileSchema.parse(raw);
|
|
147
147
|
}
|
|
148
148
|
/** 按项目 profile 取凭据;profile 不存在时报错并列出可用 profiles。
|
|
@@ -172,26 +172,10 @@ function loadGlobalConfig(filePath = GLOBAL_CONFIG_PATH) {
|
|
|
172
172
|
return GlobalConfigSchema.parse({});
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
-
/** TOML 裸键合法字符集(含 - 与 _),超出则产出的 TOML 无法被解析回来 */
|
|
176
|
-
const TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
|
|
177
|
-
/** 凭据序列化为 TOML(值经 JSON.stringify 转义,可含引号/反斜杠/#;键名与 profile 名仅允许裸键字符集,非法即抛错防写坏文件) */
|
|
178
|
-
function credentialsToToml(data) {
|
|
179
|
-
const lines = [];
|
|
180
|
-
for (const [pname, kv] of Object.entries(data.profiles)) {
|
|
181
|
-
if (!TOML_BARE_KEY.test(pname)) throw new Error(`凭据 profile 名 "${pname}" 含 TOML 裸键非法字符(仅允许字母/数字/_/-)`);
|
|
182
|
-
lines.push(`[profiles.${pname}]`);
|
|
183
|
-
for (const [k, v] of Object.entries(kv)) {
|
|
184
|
-
if (!TOML_BARE_KEY.test(k)) throw new Error(`profile "${pname}" 的凭据键名 "${k}" 含 TOML 裸键非法字符(仅允许字母/数字/_/-)`);
|
|
185
|
-
lines.push(`${k} = ${JSON.stringify(String(v))}`);
|
|
186
|
-
}
|
|
187
|
-
lines.push("");
|
|
188
|
-
}
|
|
189
|
-
return `${lines.join("\n")}\n`;
|
|
190
|
-
}
|
|
191
175
|
/** 写凭据(目录自动创建,文件强制 0600) */
|
|
192
176
|
function saveCredentials(data, filePath = CREDENTIALS_PATH) {
|
|
193
177
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
194
|
-
fs.writeFileSync(filePath,
|
|
178
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
195
179
|
fs.chmodSync(filePath, 384);
|
|
196
180
|
}
|
|
197
181
|
//#endregion
|
|
@@ -447,7 +431,7 @@ var PgyerChannel = class {
|
|
|
447
431
|
phase: "wait",
|
|
448
432
|
message: "等待蒲公英处理"
|
|
449
433
|
});
|
|
450
|
-
const deadline = Date.now() +
|
|
434
|
+
const deadline = Date.now() + 9e5;
|
|
451
435
|
while (Date.now() < deadline) {
|
|
452
436
|
await sleep(3e3);
|
|
453
437
|
const info = await pgyerPost(HOSTS[0], "/apiv2/app/buildInfo", {
|
|
@@ -472,7 +456,7 @@ var PgyerChannel = class {
|
|
|
472
456
|
}
|
|
473
457
|
return {
|
|
474
458
|
ok: false,
|
|
475
|
-
error: "蒲公英
|
|
459
|
+
error: "蒲公英 15 分钟内未完成处理(大包云端处理慢属正常,可到 pgyer.com 控制台确认实际状态)"
|
|
476
460
|
};
|
|
477
461
|
}
|
|
478
462
|
async submit() {
|
|
@@ -677,10 +661,10 @@ var HuaweiAndroidChannel = class {
|
|
|
677
661
|
},
|
|
678
662
|
body: JSON.stringify({
|
|
679
663
|
fileType: 5,
|
|
680
|
-
files: {
|
|
664
|
+
files: [{
|
|
681
665
|
fileName,
|
|
682
666
|
fileDestUrl: urlInfo.objectId
|
|
683
|
-
}
|
|
667
|
+
}]
|
|
684
668
|
})
|
|
685
669
|
}, "登记软件包");
|
|
686
670
|
ctx.emit({
|
|
@@ -695,6 +679,11 @@ var HuaweiAndroidChannel = class {
|
|
|
695
679
|
}
|
|
696
680
|
async submit(ctx) {
|
|
697
681
|
const appId = required$5(ctx, "appId");
|
|
682
|
+
if (ctx.remark !== void 0 && ctx.remark !== "") {
|
|
683
|
+
const remarkLen = [...ctx.remark].length;
|
|
684
|
+
if (remarkLen < 10 || remarkLen > 300) throw new Error(`华为提审备注(remark)长度需在 10-300 字之间,当前 ${remarkLen} 字`);
|
|
685
|
+
}
|
|
686
|
+
if (ctx.updateDescription !== void 0 && [...ctx.updateDescription].length > 500) throw new Error(`华为更新说明(newFeatures)不能超过 500 字符,当前 ${[...ctx.updateDescription].length} 字符`);
|
|
698
687
|
const { headers } = await this.authHeaders(ctx);
|
|
699
688
|
if (ctx.updateDescription) try {
|
|
700
689
|
await hwJson(`${BASE$1}/publish/v2/app-language-info?appId=${encodeURIComponent(appId)}`, {
|
|
@@ -749,8 +738,9 @@ var HuaweiAndroidChannel = class {
|
|
|
749
738
|
};
|
|
750
739
|
}
|
|
751
740
|
/**
|
|
752
|
-
* 更新商店应用信息:简介/描述走
|
|
753
|
-
*
|
|
741
|
+
* 更新商店应用信息:简介/描述走 v2 app-language-info(briefInfo/appDesc)。
|
|
742
|
+
* 注:v3 app-language-info 为鸿蒙(HarmonyOS)专属接口族,安卓现行文档只有 v2。
|
|
743
|
+
* 图标/截图:华为 app-file-info 的 fileType 取值未在官方文档确证(软件包=5 已验证,图标清单值不确定),
|
|
754
744
|
* 未经真机验证前只上传 OBS 不做 file-info 绑定(skip 并在结果中标注),避免误写损坏草稿。
|
|
755
745
|
*/
|
|
756
746
|
async updateListing(ctx, listing) {
|
|
@@ -763,7 +753,7 @@ var HuaweiAndroidChannel = class {
|
|
|
763
753
|
const body = { lang: "zh-CN" };
|
|
764
754
|
if (effective.brief !== void 0) body.briefInfo = effective.brief;
|
|
765
755
|
if (effective.description !== void 0) body.appDesc = effective.description;
|
|
766
|
-
await hwJson(`${BASE$1}/publish/
|
|
756
|
+
await hwJson(`${BASE$1}/publish/v2/app-language-info?appId=${encodeURIComponent(appId)}`, {
|
|
767
757
|
method: "PUT",
|
|
768
758
|
headers: {
|
|
769
759
|
...headers,
|
|
@@ -825,7 +815,7 @@ var HuaweiAndroidChannel = class {
|
|
|
825
815
|
case 1:
|
|
826
816
|
case 8: return {
|
|
827
817
|
state: "rejected",
|
|
828
|
-
reason: json.appInfo?.auditDesc
|
|
818
|
+
reason: json.auditInfo?.auditOpinion ?? json.appInfo?.auditDesc
|
|
829
819
|
};
|
|
830
820
|
case 2: return {
|
|
831
821
|
state: "unknown",
|
|
@@ -865,9 +855,12 @@ function required$5(ctx, key, where) {
|
|
|
865
855
|
* sig = RSA_encrypt(证书公钥, JSON({sig: [{name, hash: MD5}...], password}))
|
|
866
856
|
* 分段: 1024bit key → 每段 117 字节(128 - PKCS1 padding 11)
|
|
867
857
|
* 链路: /dev/query 校验账号对包名权限 + 提审前版本回归预检 → /dev/push 上传即提审(synchroType=1 更新包,
|
|
868
|
-
*
|
|
858
|
+
* 无法只传包不提审)。现行文档(2026-09 更新)文件参数表 icon 标「必选」、appInfo.privacyUrl 标「必选」,
|
|
859
|
+
* 旧版「未传字段如 icon/privacyUrl 不覆盖线上值」表述已不被现行文档支持;synchroType=1 不带素材是否仍被
|
|
860
|
+
* 受理待真机验证,若被拒需补传 icon 文件(其 MD5 追加进 SIG 的 sig 数组)。
|
|
861
|
+
* 另 2026-02-04 起 testAccount 须用新结构化格式(如 {"zh_CN":{"accounts":[...]}}),当前未传暂不受影响。
|
|
869
862
|
* 移植自(MIT): AngChow/android-publish scripts/_mi_common.py + mi_submit.py
|
|
870
|
-
* 官方文档: https://dev.mi.com/
|
|
863
|
+
* 官方文档: https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1134
|
|
871
864
|
*/
|
|
872
865
|
const MI_API_BASE = "https://api.developer.xiaomi.com/devupload";
|
|
873
866
|
/**
|
|
@@ -1047,7 +1040,11 @@ var XiaomiChannel = class {
|
|
|
1047
1040
|
};
|
|
1048
1041
|
}
|
|
1049
1042
|
};
|
|
1050
|
-
/**
|
|
1043
|
+
/**
|
|
1044
|
+
* MI_PASSWORD 为主键名(applift 沿用);兼容 python 权威键名 MI_PRIVATE_KEY(_mi_common.get_mi_credentials)。
|
|
1045
|
+
* 值为小米开放平台的「API 私钥」(SIG 加密的 password 材料),非开发者账号登录密码 ——
|
|
1046
|
+
* 文档明确:访问密码即私钥,重置私钥后私钥变、小米分配的公钥不变。
|
|
1047
|
+
*/
|
|
1051
1048
|
function miPassword(ctx) {
|
|
1052
1049
|
const v = ctx.credentials.MI_PASSWORD ?? ctx.credentials.MI_PRIVATE_KEY;
|
|
1053
1050
|
if (!v) throw new Error("小米渠道缺少 MI_PASSWORD(兼容别名 MI_PRIVATE_KEY)");
|
|
@@ -1062,10 +1059,14 @@ function required$4(ctx, key, where) {
|
|
|
1062
1059
|
//#region src/channels/oppo.ts
|
|
1063
1060
|
/**
|
|
1064
1061
|
* OPPO 应用市场(安卓 APK)渠道 —— app/upd 发布字段集/参数名以 python/android-publish oppo_submit.py 为权威对拍。
|
|
1065
|
-
* v2 链路: GET token(48h 有效)→ get-upload-url → multipart 上传(stage)→ 查 app/info(复用线上字段)→ app/upd
|
|
1062
|
+
* v2 链路: GET token(48h 有效)→ get-upload-url → multipart 上传(stage)→ 查 app/info(复用线上字段)→ app/upd 发布
|
|
1063
|
+
* → task-state 轮询确认异步任务结果(submit;app/upd 为异步任务,errno=0 仅代表受理)
|
|
1066
1064
|
* 签名: HmacSHA256(公共参数 access_token + timestamp(秒) + api_sign)
|
|
1067
1065
|
*/
|
|
1068
1066
|
const API_BASE$3 = "https://oop-openapi-cn.heytapmobi.com";
|
|
1067
|
+
/** app/upd 异步任务结果轮询:间隔 10s(现行文档建议等待 ≥10s)× 最多 5 次 */
|
|
1068
|
+
const TASK_STATE_POLL_INTERVAL_MS = 1e4;
|
|
1069
|
+
const TASK_STATE_MAX_POLLS = 5;
|
|
1069
1070
|
/** 获取 OPPO access_token(GET,48h 有效)—— 来源: oppo.mjs getOppoToken */
|
|
1070
1071
|
async function getOppoToken(clientId, clientSecret) {
|
|
1071
1072
|
const qs = new URLSearchParams({
|
|
@@ -1195,7 +1196,7 @@ function buildUpdParams(appInfo, apkUrl, apkMd5, versionCode, updateDesc) {
|
|
|
1195
1196
|
app_name: infoField(appInfo, "app_name"),
|
|
1196
1197
|
second_category_id: infoField(appInfo, "second_category_id"),
|
|
1197
1198
|
third_category_id: infoField(appInfo, "third_category_id"),
|
|
1198
|
-
summary: infoField(appInfo, "summary").slice(0, 13),
|
|
1199
|
+
summary: infoField(appInfo, "summary").slice(0, 13).replace(/[\p{P}\p{Z}\s]/gu, ""),
|
|
1199
1200
|
detail_desc: infoField(appInfo, "detail_desc"),
|
|
1200
1201
|
update_desc: updateDesc,
|
|
1201
1202
|
privacy_source_url: infoField(appInfo, "privacy_source_url"),
|
|
@@ -1233,7 +1234,10 @@ var OppoChannel = class {
|
|
|
1233
1234
|
errorHints = {
|
|
1234
1235
|
"911215": "应用已有版本在审核中(等待审核完成或到控制台撤回后再提)",
|
|
1235
1236
|
"911216": "版本更新任务处理中(稍后用 status 查询结果)",
|
|
1236
|
-
"800002": "参数校验失败:常见于简介/截图等商店资料不符合规格"
|
|
1237
|
+
"800002": "参数校验失败:常见于简介/截图等商店资料不符合规格",
|
|
1238
|
+
"910003": "上传 sign 失效(sign 单次有效),重新发起上传即可重试",
|
|
1239
|
+
"911045": "client_id 与包名不匹配(检查 API 客户端与应用的归属关系)",
|
|
1240
|
+
"300002": "api_sign 签名错误(检查 client_secret 与系统时间偏移)"
|
|
1237
1241
|
};
|
|
1238
1242
|
/** stage 产物暂存(apkUrl/md5),submit 按 pkgName 取回 */
|
|
1239
1243
|
staged = /* @__PURE__ */ new Map();
|
|
@@ -1327,7 +1331,7 @@ var OppoChannel = class {
|
|
|
1327
1331
|
}
|
|
1328
1332
|
async submit(ctx) {
|
|
1329
1333
|
const auth = await this.auth(ctx);
|
|
1330
|
-
const appInfo = checkErrno(await oppoApi("/resource/v1/app/info", auth, {
|
|
1334
|
+
const appInfo = checkErrno(await oppoApi("/resource/v1/app/info", auth, { pkg_name: auth.pkgName }, "GET", "查询应用详情"), "查询应用详情").data ?? {};
|
|
1331
1335
|
let staged = this.staged.get(auth.pkgName);
|
|
1332
1336
|
if (!staged) {
|
|
1333
1337
|
ctx.emit({
|
|
@@ -1363,6 +1367,7 @@ var OppoChannel = class {
|
|
|
1363
1367
|
};
|
|
1364
1368
|
}
|
|
1365
1369
|
checkErrno(upd, "发布版本");
|
|
1370
|
+
await this.pollTaskState(ctx, auth, versionCode);
|
|
1366
1371
|
ctx.emit({
|
|
1367
1372
|
type: "phase",
|
|
1368
1373
|
phase: "done",
|
|
@@ -1374,8 +1379,46 @@ var OppoChannel = class {
|
|
|
1374
1379
|
};
|
|
1375
1380
|
}
|
|
1376
1381
|
/**
|
|
1382
|
+
* app/upd 异步任务结果轮询: POST /resource/v1/app/task-state(业务参数 pkg_name+version_code,
|
|
1383
|
+
* 现行文档「获取任务状态」)。task_state: 1=待处理(继续等)、2=处理成功、3=处理失败(err_msg 透出);
|
|
1384
|
+
* 间隔 10s × 最多 5 次,超时视为已受理但异步结果未知(warn 提示,稍后可用 status 查询审核状态)。
|
|
1385
|
+
*/
|
|
1386
|
+
async pollTaskState(ctx, auth, versionCode) {
|
|
1387
|
+
for (let attempt = 1; attempt <= TASK_STATE_MAX_POLLS; attempt++) {
|
|
1388
|
+
await sleep(TASK_STATE_POLL_INTERVAL_MS);
|
|
1389
|
+
const st = checkErrno(await oppoApi("/resource/v1/app/task-state", auth, {
|
|
1390
|
+
pkg_name: auth.pkgName,
|
|
1391
|
+
version_code: versionCode
|
|
1392
|
+
}, "POST", "查询任务状态"), "查询任务状态");
|
|
1393
|
+
const taskState = st.data?.["task_state"];
|
|
1394
|
+
if (taskState === 2) {
|
|
1395
|
+
ctx.emit({
|
|
1396
|
+
type: "progress",
|
|
1397
|
+
detail: `任务处理成功(task_state=2,第 ${attempt} 次轮询确认)`
|
|
1398
|
+
});
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (taskState === 3) {
|
|
1402
|
+
const errMsg = st.data?.["err_msg"];
|
|
1403
|
+
throw new Error(`OPPO 发布任务处理失败(task_state=3): ${typeof errMsg === "string" && errMsg ? errMsg : JSON.stringify(st.data).slice(0, 200)}`);
|
|
1404
|
+
}
|
|
1405
|
+
ctx.emit({
|
|
1406
|
+
type: "progress",
|
|
1407
|
+
detail: `任务处理中(task_state=${String(taskState)},第 ${attempt}/${TASK_STATE_MAX_POLLS} 次)`
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
ctx.emit({
|
|
1411
|
+
type: "warn",
|
|
1412
|
+
message: `任务状态轮询 ${TASK_STATE_MAX_POLLS} 次未到终态:app/upd 已受理但异步处理结果未知,请稍后用 status 查询审核状态`
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1377
1416
|
* 审核状态查询: GET /resource/v1/app/info(公共参数 + api_sign,同 stage)
|
|
1378
|
-
* → data.audit_status
|
|
1417
|
+
* → data.audit_status 按现行「审核状态对照表」映射(id=10998):
|
|
1418
|
+
* 1=审核中、4=运营审核中 → reviewing;2=审核通过、6=运营通过 → approved;111=上线 → approved+live;
|
|
1419
|
+
* 3=测试不通过、5=运营打回、444=审核不通过 → rejected(原因读 refuse_reason,原样透出不解析);
|
|
1420
|
+
* 0=未发布、7=定时发布、22=已冻结 → unknown+raw;其余编号(00/11/-11 资质类、222 下架等)→ unknown。
|
|
1421
|
+
* 旧映射(1=待提审 2=审核中 3=通过 4=驳回,出自 python oppo_query.py)与现行对照表整体错位,已重写。
|
|
1379
1422
|
*/
|
|
1380
1423
|
async status(ctx) {
|
|
1381
1424
|
const clientId = this.required(ctx, "OPPO_CLIENT_ID");
|
|
@@ -1385,7 +1428,7 @@ var OppoChannel = class {
|
|
|
1385
1428
|
clientId,
|
|
1386
1429
|
clientSecret,
|
|
1387
1430
|
token: await getOppoToken(clientId, clientSecret)
|
|
1388
|
-
}, { pkgName }, "GET", "查询审核状态"), "查询审核状态");
|
|
1431
|
+
}, { pkg_name: pkgName }, "GET", "查询审核状态"), "查询审核状态");
|
|
1389
1432
|
const data = info.data ?? {};
|
|
1390
1433
|
const auditStatus = data["audit_status"] ?? data["auditStatus"];
|
|
1391
1434
|
if (auditStatus === void 0) return {
|
|
@@ -1398,21 +1441,37 @@ var OppoChannel = class {
|
|
|
1398
1441
|
raw: `audit_status=${String(auditStatus)}`
|
|
1399
1442
|
};
|
|
1400
1443
|
switch (num) {
|
|
1401
|
-
case
|
|
1402
|
-
case
|
|
1444
|
+
case 1:
|
|
1445
|
+
case 4: return { state: "reviewing" };
|
|
1446
|
+
case 2:
|
|
1447
|
+
case 6: return {
|
|
1448
|
+
state: "approved",
|
|
1449
|
+
live: false
|
|
1450
|
+
};
|
|
1451
|
+
case 111: return {
|
|
1403
1452
|
state: "approved",
|
|
1404
1453
|
live: true
|
|
1405
1454
|
};
|
|
1406
|
-
case
|
|
1455
|
+
case 3:
|
|
1456
|
+
case 5:
|
|
1457
|
+
case 444: {
|
|
1407
1458
|
const reason = data["refuse_reason"];
|
|
1408
1459
|
return {
|
|
1409
1460
|
state: "rejected",
|
|
1410
1461
|
reason: typeof reason === "string" && reason ? reason : void 0
|
|
1411
1462
|
};
|
|
1412
1463
|
}
|
|
1413
|
-
case
|
|
1464
|
+
case 0: return {
|
|
1465
|
+
state: "unknown",
|
|
1466
|
+
raw: "未发布(audit_status=0)"
|
|
1467
|
+
};
|
|
1468
|
+
case 7: return {
|
|
1469
|
+
state: "unknown",
|
|
1470
|
+
raw: "定时发布(audit_status=7)"
|
|
1471
|
+
};
|
|
1472
|
+
case 22: return {
|
|
1414
1473
|
state: "unknown",
|
|
1415
|
-
raw: "
|
|
1474
|
+
raw: "已冻结(audit_status=22)"
|
|
1416
1475
|
};
|
|
1417
1476
|
default: return {
|
|
1418
1477
|
state: "unknown",
|
|
@@ -1463,8 +1522,10 @@ var VivoChannel = class {
|
|
|
1463
1522
|
status: true
|
|
1464
1523
|
};
|
|
1465
1524
|
errorHints = {
|
|
1466
|
-
"
|
|
1467
|
-
"
|
|
1525
|
+
"10001": "签名校验失败(检查 VIVO_ACCESS_SECRET 是否正确)",
|
|
1526
|
+
"10005": "时间戳失效(检查系统时间偏差)",
|
|
1527
|
+
"10011": "当天调用次数超限(约 50 次/天)",
|
|
1528
|
+
"12010": "应用审核中,不允许更新"
|
|
1468
1529
|
};
|
|
1469
1530
|
/**
|
|
1470
1531
|
* stage→submit 之间传递上传结果:Channel 接口不透传 ref,
|
|
@@ -1574,6 +1635,7 @@ var VivoChannel = class {
|
|
|
1574
1635
|
if (!serialnumber) throw new Error("vivo 提审缺少 serialnumber(上传未返回)");
|
|
1575
1636
|
const updateDesc = (ctx.updateDescription ?? "").trim();
|
|
1576
1637
|
if (updateDesc.length < 5) throw new Error(`vivo 提审 updateDesc 需 5-200 字(当前 ${updateDesc.length} 字),请提供更新说明`);
|
|
1638
|
+
if (updateDesc.length > 200) throw new Error(`vivo 提审 updateDesc 需 5-200 字(当前 ${updateDesc.length} 字,超出上限),请精简更新说明`);
|
|
1577
1639
|
const { accessKey, accessSecret } = this.creds(ctx);
|
|
1578
1640
|
ctx.emit({
|
|
1579
1641
|
type: "phase",
|
|
@@ -1602,6 +1664,7 @@ var VivoChannel = class {
|
|
|
1602
1664
|
* 审核状态查询: 复用 app.query.details 的 data.auditStatus(对拍 python vivo_query.py;
|
|
1603
1665
|
* 未确证的 app.audit.status method 已删——python/apkgo 均无此调用)。
|
|
1604
1666
|
* 映射 0=审核通过 1=审核中 2=驳回 沿自 v1,待真机校准;
|
|
1667
|
+
* 注意:该编号映射未在现行中国站文档成文(国际站同型接口字典编号整体错一位),需真机校准;
|
|
1605
1668
|
* 交叉见证:apkgo vivo.go 读 data.status 且编号不同(1草稿/2待审核/3通过/4不通过/5撤销),冲突以真机为准。
|
|
1606
1669
|
*/
|
|
1607
1670
|
async status(ctx) {
|
|
@@ -1701,6 +1764,24 @@ async function honorPost(apiPath, token, bodyObj, context) {
|
|
|
1701
1764
|
async function honorPostQ(apiPath, appId, token, bodyObj, context) {
|
|
1702
1765
|
return honorPost(`${apiPath}?${new URLSearchParams({ appId }).toString()}`, token, bodyObj, context);
|
|
1703
1766
|
}
|
|
1767
|
+
/** 带 appId query 的 GET(查询类接口,仅 Bearer 鉴权无 body) */
|
|
1768
|
+
async function honorGetQ(apiPath, appId, token, context) {
|
|
1769
|
+
const qs = new URLSearchParams({ appId }).toString();
|
|
1770
|
+
const res = await fetch(`${PUBLISH_BASE}${apiPath}?${qs}`, {
|
|
1771
|
+
method: "GET",
|
|
1772
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1773
|
+
signal: AbortSignal.timeout(6e4)
|
|
1774
|
+
});
|
|
1775
|
+
const text = await res.text();
|
|
1776
|
+
let json;
|
|
1777
|
+
try {
|
|
1778
|
+
json = JSON.parse(text);
|
|
1779
|
+
} catch {
|
|
1780
|
+
throw new Error(`荣耀 ${context}返回非 JSON(HTTP ${res.status}): ${text.slice(0, 300)}`);
|
|
1781
|
+
}
|
|
1782
|
+
if (json.code !== 0) throw new Error(`荣耀 ${context}失败 code=${json.code} ${json.msg ?? ""}: ${text.slice(0, 200)}`);
|
|
1783
|
+
return json;
|
|
1784
|
+
}
|
|
1704
1785
|
/** RFC3339 → 荣耀定时发布格式:偏移不带冒号(+08:00→+0800)、Z→+0000,如 2026-06-20T10:00:00+0800(形状参照 apkgo submitAudit) */
|
|
1705
1786
|
function honorReleaseTime(rfc3339) {
|
|
1706
1787
|
const converted = rfc3339.replace(/([+-]\d{2}):(\d{2})$/, "$1$2").replace(/Z$/, "+0000");
|
|
@@ -1717,6 +1798,10 @@ var HonorChannel = class {
|
|
|
1717
1798
|
status: true
|
|
1718
1799
|
};
|
|
1719
1800
|
errorHints = {
|
|
1801
|
+
"10003": "token 过期(CLI 每次现取,若出现请重试)",
|
|
1802
|
+
"20022": "已有审核中的版本,需等待审核完成后再提交",
|
|
1803
|
+
"30006": "版本号必须大于等于已上架版本",
|
|
1804
|
+
"30011": "提交过于频繁,请稍后再试",
|
|
1720
1805
|
"100030001": "认证失败:Client ID/Secret 无效或未开通 API 权限",
|
|
1721
1806
|
"100030002": "token 过期(CLI 每次现取,若出现请重试)"
|
|
1722
1807
|
};
|
|
@@ -1791,7 +1876,19 @@ var HonorChannel = class {
|
|
|
1791
1876
|
const appId = required$2(ctx, "appId");
|
|
1792
1877
|
const token = await this.fetchToken(ctx);
|
|
1793
1878
|
if (ctx.updateDescription) try {
|
|
1794
|
-
|
|
1879
|
+
if (ctx.updateDescription.length > 500) throw new Error(`更新文案超过 500 字(当前 ${ctx.updateDescription.length} 字)`);
|
|
1880
|
+
const detail = await honorPostQ("/openapi/v1/publish/get-app-detail", appId, token, {}, "查询应用详情");
|
|
1881
|
+
const { appName, intro } = detail.data ?? {};
|
|
1882
|
+
if (!appName || !intro) throw new Error(`应用详情缺 appName/intro: ${JSON.stringify(detail.data ?? {}).slice(0, 200)}`);
|
|
1883
|
+
await honorPostQ("/openapi/v1/publish/update-language-info", appId, token, {
|
|
1884
|
+
languageInfoList: [{
|
|
1885
|
+
languageId: "zh-CN",
|
|
1886
|
+
appName,
|
|
1887
|
+
intro,
|
|
1888
|
+
newFeature: ctx.updateDescription
|
|
1889
|
+
}],
|
|
1890
|
+
setAll: 0
|
|
1891
|
+
}, "更新文案");
|
|
1795
1892
|
} catch (e) {
|
|
1796
1893
|
ctx.emit({
|
|
1797
1894
|
type: "warn",
|
|
@@ -1817,14 +1914,11 @@ var HonorChannel = class {
|
|
|
1817
1914
|
};
|
|
1818
1915
|
}
|
|
1819
1916
|
async status(ctx) {
|
|
1820
|
-
const
|
|
1821
|
-
const
|
|
1822
|
-
if (!Number.isFinite(appIdNum)) throw new Error(`荣耀 appId 非数字: ${appId}(get-audit-result 要求 body 内为数字)`);
|
|
1823
|
-
const json = await honorPost("/openapi/v1/publish/get-audit-result", await this.fetchToken(ctx), { appId: [{ appId: appIdNum }] }, "查询审核结果");
|
|
1824
|
-
const item = json.data?.[0];
|
|
1917
|
+
const json = await honorGetQ("/openapi/v1/publish/get-app-current-release", required$2(ctx, "appId", "审核状态查询必需"), await this.fetchToken(ctx), "查询审核状态");
|
|
1918
|
+
const item = json.data;
|
|
1825
1919
|
if (!item || item.auditResult === void 0) return {
|
|
1826
1920
|
state: "unknown",
|
|
1827
|
-
raw: `get-
|
|
1921
|
+
raw: `get-app-current-release 无数据: ${JSON.stringify(json).slice(0, 200)}`
|
|
1828
1922
|
};
|
|
1829
1923
|
switch (item.auditResult) {
|
|
1830
1924
|
case 0: return { state: "reviewing" };
|
|
@@ -1838,7 +1932,7 @@ var HonorChannel = class {
|
|
|
1838
1932
|
};
|
|
1839
1933
|
case 3: return {
|
|
1840
1934
|
state: "unknown",
|
|
1841
|
-
raw: "
|
|
1935
|
+
raw: "未提交审核(auditResult=3)"
|
|
1842
1936
|
};
|
|
1843
1937
|
case 4: return {
|
|
1844
1938
|
state: "unknown",
|
|
@@ -1863,7 +1957,7 @@ function required$2(ctx, key, where) {
|
|
|
1863
1957
|
* 腾讯应用宝(安卓 APK)渠道。
|
|
1864
1958
|
* 权威: AngChow/android-publish scripts/_yyb_common.py + submit_apk.py + query_status.py(MIT),apkgo pkg/store/tencent 交叉验证
|
|
1865
1959
|
* 官方文档: https://wikinew.open.qq.com/index.html#/iwiki/4015262492
|
|
1866
|
-
* v2 链路: query_app_detail
|
|
1960
|
+
* v2 链路: query_app_detail(校验应用)→ get_file_upload_info(pkg_name/app_id/file_type=apk/file_name → COS 预签名 URL + serial_number)
|
|
1867
1961
|
* → PUT 预签名 URL 直传 COS(预签名自带鉴权,无需自签 sign)→ update_app 提审(serial_number + md5)
|
|
1868
1962
|
* 签名: HmacSHA256(公共参数 user_id + timestamp(秒) + 业务参数,按 key ASCII 升序)——仅 open API 需要,COS 上传不需要
|
|
1869
1963
|
* 限额: 上传 100 次/天、更新 50 次/天;审核状态 audit_status 1=审核中 2=驳回 3=通过 8=撤销
|
|
@@ -1928,13 +2022,16 @@ var YybChannel = class {
|
|
|
1928
2022
|
type: "progress",
|
|
1929
2023
|
detail: "查询应用详情"
|
|
1930
2024
|
});
|
|
1931
|
-
const
|
|
2025
|
+
const detail = await yybApi("/query_app_detail", {
|
|
1932
2026
|
userId,
|
|
1933
2027
|
accessSecret
|
|
1934
|
-
}, {
|
|
2028
|
+
}, {
|
|
2029
|
+
pkg_name: pkgName,
|
|
2030
|
+
app_id: appId
|
|
2031
|
+
});
|
|
1935
2032
|
ctx.emit({
|
|
1936
2033
|
type: "progress",
|
|
1937
|
-
detail:
|
|
2034
|
+
detail: `应用「${detail.app_name ?? appId}」校验通过`
|
|
1938
2035
|
});
|
|
1939
2036
|
const upInfo = await yybApi("/get_file_upload_info", {
|
|
1940
2037
|
userId,
|
|
@@ -2072,9 +2169,13 @@ var YybChannel = class {
|
|
|
2072
2169
|
* 5. GET /api/publish/v3/package/compile/status 轮询编译(0=成功 2=拒绝终态 其他=编译中)
|
|
2073
2170
|
* 6. POST /api/publish/v3/app-submit 提审(ret 204144719/204144727 = 编译中可重试)
|
|
2074
2171
|
*
|
|
2075
|
-
*
|
|
2076
|
-
* upload-url/for-obs
|
|
2077
|
-
*
|
|
2172
|
+
* 测试发布(ctx.target.releaseType === '6',2026-10 起走 Testing API;旧 releaseType=6 直绑 app-package-info 链路已下线,枚举仅剩 1/7):
|
|
2173
|
+
* 0. GET /api/publish/v2/upload-url/for-obs?releaseType=6 .hap 上传地址 → PUT OBS
|
|
2174
|
+
* 1. POST /api/publish/v2/test/app/version 新建测试版本(testType 3=邀请测试)→ versionId
|
|
2175
|
+
* 2. POST /api/publish/v2/test/version/pkg 添加测试软件包(distributeMode=1)→ pkgId
|
|
2176
|
+
* 3. GET /api/publish/v3/package/compile/status 轮询云端编译
|
|
2177
|
+
* 4. PUT /api/publish/v2/test/app/version {versionId, pkgId} 绑定;提交审核/测试群组由控制台人工完成。
|
|
2178
|
+
* 官方文档: https://developer.huawei.com/consumer/cn/doc/doccenter-connect-api/agc-help-test-api-guide-0000002236015562
|
|
2078
2179
|
*/
|
|
2079
2180
|
const BASE = "https://connect-api.cloud.huawei.com/api";
|
|
2080
2181
|
const COMPILE_TIMEOUT_MS = 9e5;
|
|
@@ -2100,11 +2201,16 @@ async function agcJson(url, init, context) {
|
|
|
2100
2201
|
* appId 缺省时由渠道内部调用,也可独立复用。
|
|
2101
2202
|
*/
|
|
2102
2203
|
async function getHarmonyAppId(opts) {
|
|
2103
|
-
const qs = new URLSearchParams({
|
|
2204
|
+
const qs = new URLSearchParams({
|
|
2205
|
+
packageTypes: "7",
|
|
2206
|
+
packageName: opts.bundleName
|
|
2207
|
+
});
|
|
2104
2208
|
const json = await agcJson(`${BASE}/publish/v2/appid-list?${qs}`, { headers: {
|
|
2105
2209
|
client_id: opts.clientId,
|
|
2106
2210
|
Authorization: `Bearer ${opts.token}`
|
|
2107
2211
|
} }, "查询应用列表");
|
|
2212
|
+
const fresh = json.appids?.[0]?.value;
|
|
2213
|
+
if (fresh) return fresh;
|
|
2108
2214
|
const list = json.appIds ?? json.appIdList ?? [];
|
|
2109
2215
|
const hit = (Array.isArray(list) ? list : []).find((e) => String(e.bundleName ?? e.BundleName ?? "").toLowerCase() === opts.bundleName.toLowerCase());
|
|
2110
2216
|
if (!hit) throw new Error(`AGC 未找到 bundleName=${opts.bundleName} 的鸿蒙应用(packageTypes=7)`);
|
|
@@ -2172,7 +2278,7 @@ var AgcHarmonyChannel = class {
|
|
|
2172
2278
|
artifactVersion: "",
|
|
2173
2279
|
action: "stage-only",
|
|
2174
2280
|
afterApproval: "none(beta)",
|
|
2175
|
-
notes: "
|
|
2281
|
+
notes: "Testing API 邀请测试:上传→新建测试版本→绑定软件包;提交审核与测试群组到控制台完成,不自动提审"
|
|
2176
2282
|
};
|
|
2177
2283
|
return {
|
|
2178
2284
|
channel: this.id,
|
|
@@ -2203,7 +2309,7 @@ var AgcHarmonyChannel = class {
|
|
|
2203
2309
|
async submit(ctx) {
|
|
2204
2310
|
if (ctx.target.releaseType === "6") return {
|
|
2205
2311
|
ok: false,
|
|
2206
|
-
error: "AGC 测试发布(releaseType=6)不做自动提审,请到 AGC
|
|
2312
|
+
error: "AGC 测试发布(releaseType=6)不做自动提审,请到 AGC 控制台「测试」提交测试版本并指定测试群组"
|
|
2207
2313
|
};
|
|
2208
2314
|
const { clientId, token, headers } = await this.authHeaders(ctx);
|
|
2209
2315
|
const appId = await this.resolveAppId(ctx, clientId, token);
|
|
@@ -2213,36 +2319,11 @@ var AgcHarmonyChannel = class {
|
|
|
2213
2319
|
message: "token 获取成功"
|
|
2214
2320
|
});
|
|
2215
2321
|
const { packageId } = await this.uploadFormal(ctx, appId, headers);
|
|
2216
|
-
if (packageId)
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
});
|
|
2222
|
-
const deadline = Date.now() + COMPILE_TIMEOUT_MS;
|
|
2223
|
-
for (;;) {
|
|
2224
|
-
const stQs = new URLSearchParams({
|
|
2225
|
-
appId,
|
|
2226
|
-
pkgIds: packageId
|
|
2227
|
-
});
|
|
2228
|
-
const st = await agcJson(`${BASE}/publish/v3/package/compile/status?${stQs}`, { headers }, "查询编译状态");
|
|
2229
|
-
const s = (st.pkgStateList?.find((p) => p.pkgId === packageId) ?? st.pkgStateList?.[0])?.successStatus;
|
|
2230
|
-
if (s === 0) {
|
|
2231
|
-
ctx.emit({
|
|
2232
|
-
type: "progress",
|
|
2233
|
-
detail: "软件包编译完成"
|
|
2234
|
-
});
|
|
2235
|
-
break;
|
|
2236
|
-
}
|
|
2237
|
-
if (s === 2) throw new Error(`AGC 编译拒绝(packageId ${packageId}):签名/证书/bundleName 不匹配,到 AGC 控制台核对`);
|
|
2238
|
-
if (Date.now() > deadline) throw new Error(`AGC 编译超时(${COMPILE_TIMEOUT_MS / 6e4} 分钟,packageId ${packageId})`);
|
|
2239
|
-
ctx.emit({
|
|
2240
|
-
type: "progress",
|
|
2241
|
-
detail: `编译中(successStatus=${s}),${COMPILE_POLL_MS / 1e3}s 后重查…`
|
|
2242
|
-
});
|
|
2243
|
-
await sleep(COMPILE_POLL_MS);
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2322
|
+
if (packageId) await this.awaitCompile(ctx, appId, packageId, headers);
|
|
2323
|
+
if (!ctx.updateDescription) ctx.emit({
|
|
2324
|
+
type: "warn",
|
|
2325
|
+
message: "未提供更新说明(--note):AGC 升级版本要求 newFeatures 必填,提审可能被拒"
|
|
2326
|
+
});
|
|
2246
2327
|
ctx.emit({
|
|
2247
2328
|
type: "phase",
|
|
2248
2329
|
phase: "submit"
|
|
@@ -2279,6 +2360,37 @@ var AgcHarmonyChannel = class {
|
|
|
2279
2360
|
return { ok: true };
|
|
2280
2361
|
}
|
|
2281
2362
|
}
|
|
2363
|
+
/** 轮询软件包云端编译:successStatus 0=成功 2=拒绝(终态,多为签名/证书/bundleName 不匹配)其他=编译中 */
|
|
2364
|
+
async awaitCompile(ctx, appId, pkgId, headers) {
|
|
2365
|
+
ctx.emit({
|
|
2366
|
+
type: "phase",
|
|
2367
|
+
phase: "wait",
|
|
2368
|
+
message: `等待 AGC 云端编译(pkgId: ${pkgId})`
|
|
2369
|
+
});
|
|
2370
|
+
const deadline = Date.now() + COMPILE_TIMEOUT_MS;
|
|
2371
|
+
for (;;) {
|
|
2372
|
+
const stQs = new URLSearchParams({
|
|
2373
|
+
appId,
|
|
2374
|
+
pkgIds: pkgId
|
|
2375
|
+
});
|
|
2376
|
+
const st = await agcJson(`${BASE}/publish/v3/package/compile/status?${stQs}`, { headers }, "查询编译状态");
|
|
2377
|
+
const s = (st.pkgStateList?.find((p) => p.pkgId === pkgId) ?? st.pkgStateList?.[0])?.successStatus;
|
|
2378
|
+
if (s === 0) {
|
|
2379
|
+
ctx.emit({
|
|
2380
|
+
type: "progress",
|
|
2381
|
+
detail: "软件包编译完成"
|
|
2382
|
+
});
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
if (s === 2) throw new Error(`AGC 编译拒绝(pkgId ${pkgId}):签名/证书/bundleName 不匹配,到 AGC 控制台核对`);
|
|
2386
|
+
if (Date.now() > deadline) throw new Error(`AGC 编译超时(${COMPILE_TIMEOUT_MS / 6e4} 分钟,pkgId ${pkgId})`);
|
|
2387
|
+
ctx.emit({
|
|
2388
|
+
type: "progress",
|
|
2389
|
+
detail: `编译中(successStatus=${s}),${COMPILE_POLL_MS / 1e3}s 后重查…`
|
|
2390
|
+
});
|
|
2391
|
+
await sleep(COMPILE_POLL_MS);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2282
2394
|
async status() {
|
|
2283
2395
|
return {
|
|
2284
2396
|
state: "unknown",
|
|
@@ -2355,15 +2467,21 @@ var AgcHarmonyChannel = class {
|
|
|
2355
2467
|
packageId
|
|
2356
2468
|
};
|
|
2357
2469
|
}
|
|
2358
|
-
/**
|
|
2470
|
+
/**
|
|
2471
|
+
* 测试发布(releaseType=6,2026-10 起走 Testing API——旧 app-package-info 直绑链路已被华为下线,
|
|
2472
|
+
* 其 releaseType 枚举仅剩 1=全网 / 7=企业非公开):
|
|
2473
|
+
* 上传 .hap → 新建测试版本(testType=3 邀请测试)→ 添加测试软件包 → 等云端编译 → 更新测试版本绑定;
|
|
2474
|
+
* 不自动提交审核,测试群组/邀请码由控制台人工完成。
|
|
2475
|
+
*/
|
|
2359
2476
|
async uploadBeta(ctx, appId, headers) {
|
|
2360
2477
|
const fileName = path.basename(ctx.artifactPath);
|
|
2361
2478
|
const contentLength = fs.statSync(ctx.artifactPath).size;
|
|
2362
2479
|
const sha256 = sha256File(ctx.artifactPath);
|
|
2480
|
+
if (path.extname(ctx.artifactPath).toLowerCase() !== ".app") throw new Error("AGC 测试发布需要 .app 整包(DevEco「构建 App」产物,发布签名);Testing API 不再接受 .hap(实测 204144641)");
|
|
2363
2481
|
ctx.emit({
|
|
2364
2482
|
type: "phase",
|
|
2365
2483
|
phase: "upload",
|
|
2366
|
-
message: `AGC 上传 ${fileName}(${formatSize(contentLength)}
|
|
2484
|
+
message: `AGC 上传 ${fileName}(${formatSize(contentLength)},测试发布)`
|
|
2367
2485
|
});
|
|
2368
2486
|
const qs = new URLSearchParams({
|
|
2369
2487
|
appId,
|
|
@@ -2376,6 +2494,7 @@ var AgcHarmonyChannel = class {
|
|
|
2376
2494
|
const urlInfo = urlJson.urlInfo ?? urlJson;
|
|
2377
2495
|
const objectId = urlInfo.objectId ?? "";
|
|
2378
2496
|
if (!urlInfo.url) throw new Error(`AGC 返回的上传地址为空: ${JSON.stringify(urlJson).slice(0, 300)}`);
|
|
2497
|
+
if (!objectId) throw new Error("AGC 上传响应缺 objectId(Testing API 添加软件包必需)");
|
|
2379
2498
|
ctx.emit({
|
|
2380
2499
|
type: "progress",
|
|
2381
2500
|
detail: "上传地址获取成功(5 分钟内有效)"
|
|
@@ -2399,46 +2518,59 @@ var AgcHarmonyChannel = class {
|
|
|
2399
2518
|
type: "progress",
|
|
2400
2519
|
detail: "文件上传完成"
|
|
2401
2520
|
});
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2521
|
+
const versionId = (await agcJson(`${BASE}/publish/v2/test/app/version?appId=${encodeURIComponent(appId)}`, {
|
|
2522
|
+
method: "POST",
|
|
2523
|
+
headers: {
|
|
2524
|
+
...headers,
|
|
2525
|
+
"Content-Type": "application/json"
|
|
2526
|
+
},
|
|
2527
|
+
body: JSON.stringify({
|
|
2528
|
+
releaseType: 6,
|
|
2529
|
+
testType: 3,
|
|
2530
|
+
testDesc: (ctx.updateDescription ?? "applift 测试发布").slice(0, 50)
|
|
2531
|
+
})
|
|
2532
|
+
}, "新建测试版本")).versionId ?? "";
|
|
2533
|
+
if (!versionId) throw new Error("AGC 新建测试版本未返回 versionId");
|
|
2534
|
+
ctx.emit({
|
|
2535
|
+
type: "phase",
|
|
2536
|
+
phase: "bind",
|
|
2537
|
+
message: `测试版本已创建(versionId: ${versionId})`
|
|
2538
|
+
});
|
|
2539
|
+
const pkgId = (await agcJson(`${BASE}/publish/v2/test/version/pkg?appId=${encodeURIComponent(appId)}`, {
|
|
2540
|
+
method: "POST",
|
|
2541
|
+
headers: {
|
|
2542
|
+
...headers,
|
|
2543
|
+
"Content-Type": "application/json"
|
|
2544
|
+
},
|
|
2545
|
+
body: JSON.stringify({
|
|
2546
|
+
distributeMode: 1,
|
|
2547
|
+
file: {
|
|
2414
2548
|
fileName,
|
|
2415
2549
|
objectId
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
});
|
|
2433
|
-
await this.updateReleaseNotes(ctx, appId, headers, langQs);
|
|
2550
|
+
}
|
|
2551
|
+
})
|
|
2552
|
+
}, "添加测试软件包")).pkgVersion?.[0] ?? "";
|
|
2553
|
+
if (!pkgId) throw new Error("AGC 添加测试软件包未返回 pkgVersion");
|
|
2554
|
+
await this.awaitCompile(ctx, appId, pkgId, headers);
|
|
2555
|
+
await agcJson(`${BASE}/publish/v2/test/app/version?appId=${encodeURIComponent(appId)}`, {
|
|
2556
|
+
method: "PUT",
|
|
2557
|
+
headers: {
|
|
2558
|
+
...headers,
|
|
2559
|
+
"Content-Type": "application/json"
|
|
2560
|
+
},
|
|
2561
|
+
body: JSON.stringify({
|
|
2562
|
+
versionId,
|
|
2563
|
+
pkgId
|
|
2564
|
+
})
|
|
2565
|
+
}, "绑定测试软件包");
|
|
2434
2566
|
ctx.emit({
|
|
2435
2567
|
type: "phase",
|
|
2436
2568
|
phase: "done",
|
|
2437
|
-
message: "
|
|
2569
|
+
message: "测试版本已就绪(未提交审核);到 AGC 控制台「测试」提交并指定测试群组"
|
|
2438
2570
|
});
|
|
2439
2571
|
return {
|
|
2440
2572
|
ok: true,
|
|
2441
|
-
ref:
|
|
2573
|
+
ref: versionId
|
|
2442
2574
|
};
|
|
2443
2575
|
}
|
|
2444
2576
|
/** v1 更新文案段(releaseHarmonyApp step 4 / uploadToAgc 尾段):查默认语言 → 写 newFeatures,失败仅警告不阻断 */
|
|
@@ -2544,6 +2676,8 @@ async function ascRequest(jwt, apiPath, method, body) {
|
|
|
2544
2676
|
*
|
|
2545
2677
|
* - stage: xcrun altool 上传 ipa(免 fastlane/Ruby 依赖,底层即 App Store Connect API)
|
|
2546
2678
|
* —— 自 v1 tools/publish/lib/asc.mjs 的 uploadIpaToAsc 迁移(逻辑不变)。
|
|
2679
|
+
* 注意:xcrun altool 上传通道已被 Apple 列为弃用(deprecated),后续迁移至
|
|
2680
|
+
* ASC API buildUploads 资源或 Transporter(altool 本身仍可用,暂保留)。
|
|
2547
2681
|
* 约定 p8 私钥:凭据 ASC_P8_PATH(支持 ~ 前缀)或 ~/.applift/private_keys/AuthKey_<ASC_KEY_ID>.p8
|
|
2548
2682
|
* (v1 放 tools/publish/private_keys/;altool 本身认 <cwd>/private_keys、~/private_keys、
|
|
2549
2683
|
* ~/.private_keys、~/.appstoreconnect/private_keys,见 xcrun altool --help)
|
|
@@ -2644,11 +2778,11 @@ var AscChannel = class {
|
|
|
2644
2778
|
}
|
|
2645
2779
|
/**
|
|
2646
2780
|
* 提审 App Store —— 按 ASC 官方文档新实现,未经真实链路验证。
|
|
2647
|
-
* 文档:
|
|
2781
|
+
* 文档: reviewSubmissions / Create a Review Submission / Submit a Review Submission
|
|
2648
2782
|
* https://developer.apple.com/documentation/appstoreconnectapi
|
|
2649
2783
|
* 链路: 查 app(bundleId → 数字 id)→ 查最新已处理(VALID)构建 → 读取/创建 appStoreVersion
|
|
2650
|
-
* (releaseType 默认 MANUAL,ctx.releaseTime 提供时 SCHEDULED +
|
|
2651
|
-
* → 绑定构建到版本 →
|
|
2784
|
+
* (releaseType 默认 MANUAL,ctx.releaseTime 提供时 SCHEDULED + earliestReleaseDate)
|
|
2785
|
+
* → 绑定构建到版本 → reviewSubmissions 三步流提交进入审核
|
|
2652
2786
|
*/
|
|
2653
2787
|
async submit(ctx) {
|
|
2654
2788
|
const { keyId, issuerId, p8Path } = this.resolveAuth(ctx);
|
|
@@ -2683,9 +2817,9 @@ var AscChannel = class {
|
|
|
2683
2817
|
const releaseType = ctx.releaseTime ? "SCHEDULED" : "MANUAL";
|
|
2684
2818
|
const releaseAttrs = ctx.releaseTime ? {
|
|
2685
2819
|
releaseType,
|
|
2686
|
-
|
|
2820
|
+
earliestReleaseDate: ctx.releaseTime
|
|
2687
2821
|
} : { releaseType };
|
|
2688
|
-
const versions = await ascGet(jwt, `/v1/apps/${app.id}/appStoreVersions?filter[platform]=IOS&filter[
|
|
2822
|
+
const versions = await ascGet(jwt, `/v1/apps/${app.id}/appStoreVersions?filter[platform]=IOS&filter[appVersionState]=PREPARE_FOR_SUBMISSION&limit=5`);
|
|
2689
2823
|
let versionId = versions.data[0]?.id;
|
|
2690
2824
|
if (versionId) {
|
|
2691
2825
|
await ascRequest(jwt, `/v1/appStoreVersions/${versionId}`, "PATCH", { data: {
|
|
@@ -2722,13 +2856,27 @@ var AscChannel = class {
|
|
|
2722
2856
|
type: "builds",
|
|
2723
2857
|
id: build.id
|
|
2724
2858
|
}] });
|
|
2725
|
-
const
|
|
2726
|
-
type: "
|
|
2859
|
+
const reviewSubmissionId = (await ascRequest(jwt, "/v1/reviewSubmissions", "POST", { data: {
|
|
2860
|
+
type: "reviewSubmissions",
|
|
2861
|
+
attributes: {},
|
|
2862
|
+
relationships: { app: { data: {
|
|
2863
|
+
type: "apps",
|
|
2864
|
+
id: app.id
|
|
2865
|
+
} } }
|
|
2866
|
+
} })).data?.id;
|
|
2867
|
+
if (!reviewSubmissionId) throw new Error("ASC 创建 reviewSubmission 未返回 id");
|
|
2868
|
+
await ascRequest(jwt, `/v1/reviewSubmissions/${reviewSubmissionId}/items`, "POST", { data: {
|
|
2869
|
+
type: "reviewSubmissionItems",
|
|
2870
|
+
attributes: {},
|
|
2727
2871
|
relationships: { appStoreVersion: { data: {
|
|
2728
2872
|
type: "appStoreVersions",
|
|
2729
2873
|
id: versionId
|
|
2730
2874
|
} } }
|
|
2731
2875
|
} });
|
|
2876
|
+
await ascRequest(jwt, `/v1/reviewSubmissions/${reviewSubmissionId}`, "PATCH", { data: {
|
|
2877
|
+
type: "reviewSubmissions",
|
|
2878
|
+
attributes: { action: "SUBMIT" }
|
|
2879
|
+
} });
|
|
2732
2880
|
ctx.emit({
|
|
2733
2881
|
type: "phase",
|
|
2734
2882
|
phase: "done",
|
|
@@ -2736,7 +2884,7 @@ var AscChannel = class {
|
|
|
2736
2884
|
});
|
|
2737
2885
|
return {
|
|
2738
2886
|
ok: true,
|
|
2739
|
-
ref:
|
|
2887
|
+
ref: reviewSubmissionId
|
|
2740
2888
|
};
|
|
2741
2889
|
}
|
|
2742
2890
|
async status() {
|
|
@@ -2939,6 +3087,7 @@ var GooglePlayChannel = class {
|
|
|
2939
3087
|
type: "progress",
|
|
2940
3088
|
detail: `上传成功(versionCode: ${versionCode})`
|
|
2941
3089
|
});
|
|
3090
|
+
const releases = [...(await apiFetch(`${API_BASE}/${pkgName}/edits/${editId}/tracks/${track}`, token, { method: "GET" }, `读取 ${track} 轨道现有 releases`)).releases ?? []];
|
|
2942
3091
|
const release = {
|
|
2943
3092
|
versionCodes: [versionCode],
|
|
2944
3093
|
status: "completed"
|
|
@@ -2947,9 +3096,14 @@ var GooglePlayChannel = class {
|
|
|
2947
3096
|
language: "zh-CN",
|
|
2948
3097
|
text: releaseNotes
|
|
2949
3098
|
}];
|
|
3099
|
+
const target = releases.find((r) => r.status === "completed");
|
|
3100
|
+
if (target) {
|
|
3101
|
+
target.versionCodes = [...target.versionCodes ?? [], versionCode];
|
|
3102
|
+
if (release.releaseNotes) target.releaseNotes = release.releaseNotes;
|
|
3103
|
+
} else releases.push(release);
|
|
2950
3104
|
await apiFetch(`${API_BASE}/${pkgName}/edits/${editId}/tracks/${track}`, token, {
|
|
2951
3105
|
method: "PUT",
|
|
2952
|
-
body: JSON.stringify({ releases
|
|
3106
|
+
body: JSON.stringify({ releases })
|
|
2953
3107
|
}, `部署 ${track} 轨道`);
|
|
2954
3108
|
ctx.emit({
|
|
2955
3109
|
type: "phase",
|
|
@@ -2967,7 +3121,7 @@ var GooglePlayChannel = class {
|
|
|
2967
3121
|
async status(ctx) {
|
|
2968
3122
|
const pkgName = this.requirePkgName(ctx);
|
|
2969
3123
|
const token = await getAccessToken(ctx.credentials);
|
|
2970
|
-
const release = (await apiFetch(`${API_BASE}/${pkgName}/
|
|
3124
|
+
const release = (await apiFetch(`${API_BASE}/${pkgName}/tracks/production`, token, { method: "GET" }, "查询 production 轨道")).releases?.[0];
|
|
2971
3125
|
if (!release || !release.status) return {
|
|
2972
3126
|
state: "unknown",
|
|
2973
3127
|
raw: "production 轨道无 release"
|