@applift/core 0.1.3 → 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 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.toml;gitignore 由用户决定:私有仓可提交,公开仓 ignore) */
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.toml(就近,逐键优先)→ 全局 ~/.applift/(逐键回退)。
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.toml;不存在返回空 profiles;强制 0600 */
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
- * 更新商店应用信息:简介/描述走 v3 app-language-info(briefInfo/appDesc)。
148
- * 图标/截图:华为 v3 app-file-info 的 fileType 取值未在官方文档确证(软件包=5 已验证,图标清单值不确定),
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(android-publish oppo_query.py: 1=待提审 2=审核中 3=通过 4=驳回)。
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
- "1002": string;
221
- "46001": string;
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
  };
@@ -343,11 +364,11 @@ export declare class AscChannel implements Channel {
343
364
  stage(ctx: ChannelContext): Promise<StageResult>;
344
365
  /**
345
366
  * 提审 App Store —— 按 ASC 官方文档新实现,未经真实链路验证。
346
- * 文档: appStoreVersionSubmissions / Create a Version Submission
367
+ * 文档: reviewSubmissions / Create a Review Submission / Submit a Review Submission
347
368
  * https://developer.apple.com/documentation/appstoreconnectapi
348
369
  * 链路: 查 app(bundleId → 数字 id)→ 查最新已处理(VALID)构建 → 读取/创建 appStoreVersion
349
- * (releaseType 默认 MANUAL,ctx.releaseTime 提供时 SCHEDULED + earlyReleaseDate)
350
- * → 绑定构建到版本 → POST appStoreVersionSubmissions 进入审核
370
+ * (releaseType 默认 MANUAL,ctx.releaseTime 提供时 SCHEDULED + earliestReleaseDate)
371
+ * → 绑定构建到版本 → reviewSubmissions 三步流提交进入审核
351
372
  */
352
373
  submit(ctx: ChannelContext): Promise<SubmitResult>;
353
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.toml(0600,按 profile 组织,永不进项目目录/git)。
32
- * 加载用社区主流 c12(TS 配置执行 + 多层 merge),TOML 用 smol-toml,非自研。
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.toml");
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.toml;gitignore 由用户决定:私有仓可提交,公开仓 ignore) */
106
+ /** 项目级凭据目录(<project>/.applift/credentials.json;gitignore 由用户决定:私有仓可提交,公开仓 ignore) */
107
107
  function projectCredentialsPath(projectRoot) {
108
- return path.join(projectRoot, ".applift", "credentials.toml");
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.toml(就近,逐键优先)→ 全局 ~/.applift/(逐键回退)。
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.toml"));
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.toml;不存在返回空 profiles;强制 0600 */
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, credentialsToToml(data), { mode: 384 });
178
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
195
179
  fs.chmodSync(filePath, 384);
196
180
  }
197
181
  //#endregion
@@ -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
- * 更新商店应用信息:简介/描述走 v3 app-language-info(briefInfo/appDesc)。
753
- * 图标/截图:华为 v3 app-file-info 的 fileType 取值未在官方文档确证(软件包=5 已验证,图标清单值不确定),
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/v3/app-language-info?appId=${encodeURIComponent(appId)}`, {
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
- * 未传字段如 icon/privacyUrl 不覆盖线上值;无法只传包不提审)。
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/distribute/doc/details?pId=1134
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
- /** MI_PASSWORD 为主键名(applift 沿用);兼容 python 权威键名 MI_PRIVATE_KEY(_mi_common.get_mi_credentials) */
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 发布(submit)
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, { pkgName: auth.pkgName }, "GET", "查询应用详情"), "查询应用详情").data ?? {};
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(android-publish oppo_query.py: 1=待提审 2=审核中 3=通过 4=驳回)。
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 2: return { state: "reviewing" };
1402
- case 3: return {
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 4: {
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 1: return {
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: "待提审(audit_status=1)"
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
- "1002": "参数缺失或不合法(常见:更新说明少于 5 字)",
1467
- "46001": "访问频率超限(正式环境每接口 50 次/天)"
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
- await honorPostQ("/openapi/v1/publish/update-language-info", appId, token, { newFeature: ctx.updateDescription }, "更新文案");
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 appId = required$2(ctx, "appId", "审核状态查询必需");
1821
- const appIdNum = Number(appId);
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-audit-result 无数据: ${JSON.stringify(json).slice(0, 200)}`
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: "非审核状态(auditResult=3)"
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(校验应用 + 取线上版本)→ get_file_upload_info(pkg_name/app_id/file_type=apk/file_name → COS 预签名 URL + serial_number)
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 appInfo = (await yybApi("/query_app_detail", {
2025
+ const detail = await yybApi("/query_app_detail", {
1932
2026
  userId,
1933
2027
  accessSecret
1934
- }, { app_id: appId })).app_info ?? {};
2028
+ }, {
2029
+ pkg_name: pkgName,
2030
+ app_id: appId
2031
+ });
1935
2032
  ctx.emit({
1936
2033
  type: "progress",
1937
- detail: `${appInfo.app_name ?? ""} 线上版本 ${appInfo.version ?? "?"}`
2034
+ detail: `应用「${detail.app_name ?? appId}」校验通过`
1938
2035
  });
1939
2036
  const upInfo = await yybApi("/get_file_upload_info", {
1940
2037
  userId,
@@ -2223,6 +2320,10 @@ var AgcHarmonyChannel = class {
2223
2320
  });
2224
2321
  const { packageId } = await this.uploadFormal(ctx, appId, headers);
2225
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
+ });
2226
2327
  ctx.emit({
2227
2328
  type: "phase",
2228
2329
  phase: "submit"
@@ -2575,6 +2676,8 @@ async function ascRequest(jwt, apiPath, method, body) {
2575
2676
  *
2576
2677
  * - stage: xcrun altool 上传 ipa(免 fastlane/Ruby 依赖,底层即 App Store Connect API)
2577
2678
  * —— 自 v1 tools/publish/lib/asc.mjs 的 uploadIpaToAsc 迁移(逻辑不变)。
2679
+ * 注意:xcrun altool 上传通道已被 Apple 列为弃用(deprecated),后续迁移至
2680
+ * ASC API buildUploads 资源或 Transporter(altool 本身仍可用,暂保留)。
2578
2681
  * 约定 p8 私钥:凭据 ASC_P8_PATH(支持 ~ 前缀)或 ~/.applift/private_keys/AuthKey_<ASC_KEY_ID>.p8
2579
2682
  * (v1 放 tools/publish/private_keys/;altool 本身认 <cwd>/private_keys、~/private_keys、
2580
2683
  * ~/.private_keys、~/.appstoreconnect/private_keys,见 xcrun altool --help)
@@ -2675,11 +2778,11 @@ var AscChannel = class {
2675
2778
  }
2676
2779
  /**
2677
2780
  * 提审 App Store —— 按 ASC 官方文档新实现,未经真实链路验证。
2678
- * 文档: appStoreVersionSubmissions / Create a Version Submission
2781
+ * 文档: reviewSubmissions / Create a Review Submission / Submit a Review Submission
2679
2782
  * https://developer.apple.com/documentation/appstoreconnectapi
2680
2783
  * 链路: 查 app(bundleId → 数字 id)→ 查最新已处理(VALID)构建 → 读取/创建 appStoreVersion
2681
- * (releaseType 默认 MANUAL,ctx.releaseTime 提供时 SCHEDULED + earlyReleaseDate)
2682
- * → 绑定构建到版本 → POST appStoreVersionSubmissions 进入审核
2784
+ * (releaseType 默认 MANUAL,ctx.releaseTime 提供时 SCHEDULED + earliestReleaseDate)
2785
+ * → 绑定构建到版本 → reviewSubmissions 三步流提交进入审核
2683
2786
  */
2684
2787
  async submit(ctx) {
2685
2788
  const { keyId, issuerId, p8Path } = this.resolveAuth(ctx);
@@ -2714,9 +2817,9 @@ var AscChannel = class {
2714
2817
  const releaseType = ctx.releaseTime ? "SCHEDULED" : "MANUAL";
2715
2818
  const releaseAttrs = ctx.releaseTime ? {
2716
2819
  releaseType,
2717
- earlyReleaseDate: ctx.releaseTime
2820
+ earliestReleaseDate: ctx.releaseTime
2718
2821
  } : { releaseType };
2719
- const versions = await ascGet(jwt, `/v1/apps/${app.id}/appStoreVersions?filter[platform]=IOS&filter[appStoreState]=PREPARE_FOR_SUBMISSION,READY_FOR_SUBMISSION&limit=5`);
2822
+ const versions = await ascGet(jwt, `/v1/apps/${app.id}/appStoreVersions?filter[platform]=IOS&filter[appVersionState]=PREPARE_FOR_SUBMISSION&limit=5`);
2720
2823
  let versionId = versions.data[0]?.id;
2721
2824
  if (versionId) {
2722
2825
  await ascRequest(jwt, `/v1/appStoreVersions/${versionId}`, "PATCH", { data: {
@@ -2753,13 +2856,27 @@ var AscChannel = class {
2753
2856
  type: "builds",
2754
2857
  id: build.id
2755
2858
  }] });
2756
- const submission = await ascRequest(jwt, "/v1/appStoreVersionSubmissions", "POST", { data: {
2757
- type: "appStoreVersionSubmissions",
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: {},
2758
2871
  relationships: { appStoreVersion: { data: {
2759
2872
  type: "appStoreVersions",
2760
2873
  id: versionId
2761
2874
  } } }
2762
2875
  } });
2876
+ await ascRequest(jwt, `/v1/reviewSubmissions/${reviewSubmissionId}`, "PATCH", { data: {
2877
+ type: "reviewSubmissions",
2878
+ attributes: { action: "SUBMIT" }
2879
+ } });
2763
2880
  ctx.emit({
2764
2881
  type: "phase",
2765
2882
  phase: "done",
@@ -2767,7 +2884,7 @@ var AscChannel = class {
2767
2884
  });
2768
2885
  return {
2769
2886
  ok: true,
2770
- ref: submission.data?.id
2887
+ ref: reviewSubmissionId
2771
2888
  };
2772
2889
  }
2773
2890
  async status() {
@@ -2970,6 +3087,7 @@ var GooglePlayChannel = class {
2970
3087
  type: "progress",
2971
3088
  detail: `上传成功(versionCode: ${versionCode})`
2972
3089
  });
3090
+ const releases = [...(await apiFetch(`${API_BASE}/${pkgName}/edits/${editId}/tracks/${track}`, token, { method: "GET" }, `读取 ${track} 轨道现有 releases`)).releases ?? []];
2973
3091
  const release = {
2974
3092
  versionCodes: [versionCode],
2975
3093
  status: "completed"
@@ -2978,9 +3096,14 @@ var GooglePlayChannel = class {
2978
3096
  language: "zh-CN",
2979
3097
  text: releaseNotes
2980
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);
2981
3104
  await apiFetch(`${API_BASE}/${pkgName}/edits/${editId}/tracks/${track}`, token, {
2982
3105
  method: "PUT",
2983
- body: JSON.stringify({ releases: [release] })
3106
+ body: JSON.stringify({ releases })
2984
3107
  }, `部署 ${track} 轨道`);
2985
3108
  ctx.emit({
2986
3109
  type: "phase",
@@ -2998,7 +3121,7 @@ var GooglePlayChannel = class {
2998
3121
  async status(ctx) {
2999
3122
  const pkgName = this.requirePkgName(ctx);
3000
3123
  const token = await getAccessToken(ctx.credentials);
3001
- const release = (await apiFetch(`${API_BASE}/${pkgName}/edits/-/tracks/production`, token, { method: "GET" }, "查询 production 轨道")).releases?.[0];
3124
+ const release = (await apiFetch(`${API_BASE}/${pkgName}/tracks/production`, token, { method: "GET" }, "查询 production 轨道")).releases?.[0];
3002
3125
  if (!release || !release.status) return {
3003
3126
  state: "unknown",
3004
3127
  raw: "production 轨道无 release"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applift/core",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/hcg1023/applift.git",