@applift/core 0.5.0 → 0.6.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
@@ -1,4 +1,4 @@
1
- import { $ as electronAdapter, A as ReleaseConfigSchema, B as ChannelCapabilities, C as CredentialsProfile, D as ProjectConfig, E as GlobalConfigSchema, F as VersionCodeConfig, G as ReviewStatus, H as ChannelEvent, I as VersionCodeConfigSchema, J as BundleIds, K as StageResult, L as VersionConfig, M as StoreConfigSchema, N as Stores, O as ProjectConfigSchema, P as StoresSchema, Q as detectFramework, R as VersionConfigSchema, S as CredentialsFileSchema, T as GlobalConfig, U as ChannelRegistry, V as ChannelContext, W as ReleasePlanEntry, X as Platform, Y as FrameworkAdapter, Z as detectBundleIds, _ as appendEntry, a as QypubUserConfig, at as rnAdapter, b as BetaConfigSchema, c as PipelineInput, d as runStage, et as findArtifacts, f as runSubmit, g as HistoryQuery, h as HistoryEvent, i as QypubPlugin, it as nativeIosAdapter, j as StoreConfig, k as ReleaseConfig, l as PipelineSummary, m as HistoryEntry, n as PluginHookName, nt as nativeAndroidAdapter, o as defineConfig, ot as tauriAdapter, p as HISTORY_PATH, q as SubmitResult, r as PluginHost, rt as nativeHarmonyAdapter, s as resolvePlugins, t as HookContext, tt as flutterAdapter, u as PipelineSummaryEntry, v as readEntries, w as CredentialsProfileSchema, x as CredentialsFile, y as BetaConfig, z as Channel } from "./plugin-CTt0LO8c.mjs";
1
+ import { $ as electronAdapter, A as ReleaseConfigSchema, B as ChannelCapabilities, C as CredentialsProfile, D as ProjectConfig, E as GlobalConfigSchema, F as VersionCodeConfig, G as ReviewStatus, H as ChannelEvent, I as VersionCodeConfigSchema, J as BundleIds, K as StageResult, L as VersionConfig, M as StoreConfigSchema, N as Stores, O as ProjectConfigSchema, P as StoresSchema, Q as detectFramework, R as VersionConfigSchema, S as CredentialsFileSchema, T as GlobalConfig, U as ChannelRegistry, V as ChannelContext, W as ReleasePlanEntry, X as Platform, Y as FrameworkAdapter, Z as detectBundleIds, _ as appendEntry, a as PluginHost, at as rnAdapter, b as BetaConfigSchema, c as PipelineInput, d as runStage, et as findArtifacts, f as runSubmit, g as HistoryQuery, h as HistoryEvent, i as PluginHookName, it as nativeIosAdapter, j as StoreConfig, k as ReleaseConfig, l as PipelineSummary, m as HistoryEntry, n as AppliftUserConfig, nt as nativeAndroidAdapter, o as defineConfig, ot as tauriAdapter, p as HISTORY_PATH, q as SubmitResult, r as HookContext, rt as nativeHarmonyAdapter, s as resolvePlugins, t as AppliftPlugin, tt as flutterAdapter, u as PipelineSummaryEntry, v as readEntries, w as CredentialsProfileSchema, x as CredentialsFile, y as BetaConfig, z as Channel } from "./plugin-B2GGlI_2.mjs";
2
2
  //#region src/registry.d.ts
3
3
  export declare class DefaultChannelRegistry implements ChannelRegistry {
4
4
  private readonly channels;
@@ -30,8 +30,13 @@ export declare function loadCredentials(filePath?: string): CredentialsFile;
30
30
  * knownKeys 提供时其裸键名(如 PGYER_API_KEY)也认(向后兼容),未提供的裸大写键不捡(避免误吸无关变量)。
31
31
  */
32
32
  export declare function resolveProfile(creds: CredentialsFile, profile: string, knownKeys?: string[]): Record<string, string>;
33
- /** 全局配置装载(缺失/损坏降级默认值并警告,不阻断) */
34
- export declare function loadGlobalConfig(filePath?: string): GlobalConfig;
33
+ export interface GlobalConfigLoadResult {
34
+ config: GlobalConfig;
35
+ /** 配置文件损坏降级默认值时的警告(调用方决定呈现方式;core 保持 headless 不写 stderr) */
36
+ warning?: string;
37
+ }
38
+ /** 全局配置装载(缺失/损坏降级默认值;警告经返回值交给调用方,不阻断) */
39
+ export declare function loadGlobalConfig(filePath?: string): GlobalConfigLoadResult;
35
40
  /** 写凭据(目录自动创建,文件强制 0600) */
36
41
  export declare function saveCredentials(data: CredentialsFile, filePath?: string): void;
37
42
  //#endregion
@@ -42,7 +47,7 @@ export interface LoadedProjectConfig {
42
47
  * c12 原始配置(保留 plugins 等 schema 外键)——插件发现(resolvePlugins)用;
43
48
  * 业务字段一律以 config(zod 解析后)为准。
44
49
  */
45
- raw: QypubUserConfig;
50
+ raw: AppliftUserConfig;
46
51
  /** 配置文件所在目录(解析相对路径用),无配置文件时为 cwd */
47
52
  cwd: string;
48
53
  }
@@ -80,6 +85,10 @@ export declare class PgyerChannel implements Channel {
80
85
  readonly id = "pgyer";
81
86
  readonly label = "蒲公英";
82
87
  readonly capabilities: ChannelCapabilities;
88
+ /** 平台错误码 → 人话(1247:源码注释已证 = buildKey 尚未就绪/COS 仍在入库) */
89
+ readonly errorHints: {
90
+ "1247": string;
91
+ };
83
92
  plan(ctx: ChannelContext): Promise<ReleasePlanEntry>;
84
93
  stage(ctx: ChannelContext): Promise<StageResult>;
85
94
  submit(): Promise<SubmitResult>;
@@ -172,6 +181,11 @@ export declare class XiaomiChannel implements Channel {
172
181
  readonly id = "xiaomi";
173
182
  readonly label = "小米应用商店";
174
183
  readonly capabilities: ChannelCapabilities;
184
+ /** 平台错误码 → 人话(result 码:依据为本仓测试已固化的真实失败路径文案) */
185
+ readonly errorHints: {
186
+ "401": string;
187
+ "1005": string;
188
+ };
175
189
  plan(_ctx: ChannelContext): Promise<ReleasePlanEntry>;
176
190
  /** 小米不支持只传包:push 接口上传即提审 */
177
191
  stage(_ctx: ChannelContext): Promise<StageResult>;
@@ -286,6 +300,10 @@ export declare class YybChannel implements Channel {
286
300
  readonly id = "yyb";
287
301
  readonly label = "腾讯应用宝";
288
302
  readonly capabilities: ChannelCapabilities;
303
+ /** 平台错误码 → 人话(ret 码:依据为本仓测试已固化的真实失败路径) */
304
+ readonly errorHints: {
305
+ "10010": string;
306
+ };
289
307
  /** stage 产物暂存(serial_number + md5),submit 按 appId 取回 */
290
308
  private readonly staged;
291
309
  private required;
@@ -384,11 +402,24 @@ export declare class GooglePlayChannel implements Channel {
384
402
  readonly id = "googleplay";
385
403
  readonly label = "Google Play";
386
404
  readonly capabilities: ChannelCapabilities;
405
+ /** 平台错误码 → 人话(HTTP 状态:依据为本仓测试已固化的失败路径) */
406
+ readonly errorHints: {
407
+ "403": string;
408
+ };
387
409
  plan(_ctx: ChannelContext): Promise<ReleasePlanEntry>;
388
410
  stage(ctx: ChannelContext): Promise<StageResult>;
389
- submit(ctx: ChannelContext): Promise<SubmitResult>;
411
+ submit(ctx: ChannelContext, stagedRef?: string): Promise<SubmitResult>;
412
+ /** 复用 stage 已上传的 versionCode:开新 edit(仍需 auth)→ 部署 production → commit,跳过上传 */
413
+ private assignExisting;
390
414
  /** 上传 + 部署到指定轨道 + commit,返回 `editId:versionCode` */
391
415
  private uploadAndAssign;
416
+ /**
417
+ * 部署 versionCode 到指定轨道并 commit:
418
+ * 先 GET 轨道现有 releases 再合并 PUT——目标 release(同 status=completed 配置)追加新 versionCode,
419
+ * 其余 releases 原样保留(status/userFraction 不变),避免整轨覆盖把旧版本从轨道剔除;
420
+ * :commit 冒号语法(对拍 apkgo googleplay.go 权威;对 production 含提交审核语义,internal 即生效)
421
+ */
422
+ private deployAndCommit;
392
423
  status(ctx: Pick<ChannelContext, "credentials" | "target">): Promise<ReviewStatus>;
393
424
  private requirePkgName;
394
425
  }
@@ -400,9 +431,19 @@ export declare function sha256File(filePath: string): string;
400
431
  //#region src/channels/shared/error-codes.d.ts
401
432
  /**
402
433
  * 错误码人话化:消费各渠道自带 errorHints(平台内聚——表与渠道实现同文件)。
434
+ * 网络类故障(AbortSignal 超时/断连/DNS)渠道层不转译,统一在此兜底提示。
403
435
  */
404
436
  export declare function humanizeError(hints: Record<string, string> | undefined, message: string): string;
405
437
  //#endregion
438
+ //#region src/channels/shared/redact.d.ts
439
+ /**
440
+ * 文本脱敏(三层层叠,逐层尝试):
441
+ * 1) http(s) URL 带 query/userinfo → query 按名脱敏 + 剥 userinfo
442
+ * 2) { / [ 开头且整串为合法 JSON → 递归按键名脱敏
443
+ * 3) 其余 → name=value 正则扫(错误消息等无结构文本)
444
+ */
445
+ export declare function redactText(v: string): string;
446
+ //#endregion
406
447
  //#region src/version/project.d.ts
407
448
  export type VersionFileKind = "gradle" | "pbxproj" | "app-json5" | "pubspec" | "package-json" | "tauri" | "expo-app-json";
408
449
  export interface PlatformVersionInfo {
@@ -589,4 +630,4 @@ export interface ArtifactVersion {
589
630
  }
590
631
  export declare function readArtifactVersion(artifactPath: string): ArtifactVersion | undefined;
591
632
  //#endregion
592
- export { BetaConfig, BetaConfigSchema, BundleIds, Channel, ChannelCapabilities, ChannelContext, ChannelEvent, ChannelRegistry, CredentialsFile, CredentialsFileSchema, CredentialsProfile, CredentialsProfileSchema, FrameworkAdapter, GlobalConfig, GlobalConfigSchema, HISTORY_PATH, HistoryEntry, HistoryEvent, HistoryQuery, HookContext, PipelineInput, PipelineSummary, PipelineSummaryEntry, Platform, PluginHookName, PluginHost, ProjectConfig, ProjectConfigSchema, QypubPlugin, QypubUserConfig, ReleaseConfig, ReleaseConfigSchema, ReleasePlanEntry, ReviewStatus, StageResult, StoreConfig, StoreConfigSchema, Stores, StoresSchema, SubmitResult, VersionCodeConfig, VersionCodeConfigSchema, VersionConfig, VersionConfigSchema, appendEntry, defineConfig, detectBundleIds, detectFramework, electronAdapter, findArtifacts, flutterAdapter, nativeAndroidAdapter, nativeHarmonyAdapter, nativeIosAdapter, readEntries, resolvePlugins, rnAdapter, runStage, runSubmit, tauriAdapter };
633
+ export { AppliftPlugin, AppliftUserConfig, BetaConfig, BetaConfigSchema, BundleIds, Channel, ChannelCapabilities, ChannelContext, ChannelEvent, ChannelRegistry, CredentialsFile, CredentialsFileSchema, CredentialsProfile, CredentialsProfileSchema, FrameworkAdapter, GlobalConfig, GlobalConfigSchema, HISTORY_PATH, HistoryEntry, HistoryEvent, HistoryQuery, HookContext, PipelineInput, PipelineSummary, PipelineSummaryEntry, Platform, PluginHookName, PluginHost, ProjectConfig, ProjectConfigSchema, ReleaseConfig, ReleaseConfigSchema, ReleasePlanEntry, ReviewStatus, StageResult, StoreConfig, StoreConfigSchema, Stores, StoresSchema, SubmitResult, VersionCodeConfig, VersionCodeConfigSchema, VersionConfig, VersionConfigSchema, appendEntry, defineConfig, detectBundleIds, detectFramework, electronAdapter, findArtifacts, flutterAdapter, nativeAndroidAdapter, nativeHarmonyAdapter, nativeIosAdapter, readEntries, resolvePlugins, rnAdapter, runStage, runSubmit, tauriAdapter };
package/dist/index.mjs CHANGED
@@ -41,15 +41,23 @@ profiles: z.record(z.string(), CredentialsProfileSchema) });
41
41
  /** 单渠道配置(appId、pkgName 等渠道侧键) */
42
42
  const StoreConfigSchema = z.record(z.string(), z.string());
43
43
  /**
44
- * stores 三种等价写法,统一规范化为 record:
44
+ * stores 等价写法,统一规范化为 record:
45
45
  * ① 对象:{ huawei: { appId: 'x' } }(原有写法,原样保留)
46
46
  * ② 数组:['oppo', 'vivo'] → { oppo: {}, vivo: {} }(无配置渠道)
47
47
  * ③ 字符串值:{ huawei: '10xxx' } → { huawei: { appId: '10xxx' } }(appId 简写)
48
+ * ④ 混排数组(README 首例):['huawei', { yyb: 'appId值' }] → 元素对象须单键 { 渠道: 值 },
49
+ * 值走 ③ 同款 appId 简写归一
48
50
  */
49
- const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z.string()])), z.array(z.string())]).transform((stores) => {
50
- if (Array.isArray(stores)) return Object.fromEntries(stores.map((id) => [id, {}]));
51
+ const storesNormalize = (stores) => {
52
+ if (Array.isArray(stores)) return Object.fromEntries(stores.map((el) => typeof el === "string" ? [el, {}] : (() => {
53
+ const entries = Object.entries(el);
54
+ if (entries.length !== 1) throw new Error(`stores 数组内的对象元素必须形如 { 渠道id: appId简写 }(单键),收到: ${JSON.stringify(el)}`);
55
+ const [id, cfg] = entries[0];
56
+ return [id, typeof cfg === "string" ? { appId: cfg } : cfg];
57
+ })()));
51
58
  return Object.fromEntries(Object.entries(stores).map(([id, cfg]) => [id, typeof cfg === "string" ? { appId: cfg } : cfg]));
52
- }).default({});
59
+ };
60
+ const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z.string()])), z.array(z.union([z.string(), StoreConfigSchema]))]).transform(storesNormalize).default({});
53
61
  /** 项目级配置(applift.config.ts 的形状);beta 段见上方 BetaConfigSchema */
54
62
  /**
55
63
  * 内测分发配置(applift beta 无平台参数时并行发布的渠道开关):
@@ -94,16 +102,19 @@ const ProjectConfigSchema = z.object({
94
102
  /** 团队默认凭据 profile(个人用 applift use / --profile 覆盖),缺省 default */
95
103
  profile: z.string().default("default"),
96
104
  android: z.object({
97
- pkgName: z.string(),
105
+ /** 缺省经 detectBundleIds 从工程文件自动推导(README 首例即只有 stores 的最小配置) */
106
+ pkgName: z.string().optional(),
98
107
  /** 渠道 → 渠道侧配置(appId 等);stores 里列出的渠道才发 */
99
108
  stores: StoresSchema
100
109
  }).optional(),
101
110
  harmony: z.object({
102
- bundleName: z.string(),
111
+ /** 缺省自动推导(AppScope/app.json5) */
112
+ bundleName: z.string().optional(),
103
113
  appId: z.string().optional()
104
114
  }).optional(),
105
115
  ios: z.object({
106
- scheme: z.string(),
116
+ /** 缺省自动推导(工程 scheme 探测) */
117
+ scheme: z.string().optional(),
107
118
  workspace: z.string().optional(),
108
119
  /** App Store Connect 按 bundleId 定位 App(asc 渠道 submit 用) */
109
120
  bundleId: z.string().optional()
@@ -208,14 +219,16 @@ function resolveProfile(creds, profile, knownKeys) {
208
219
  for (const [envName, value] of Object.entries(process.env)) if (envName.startsWith("APPLIFT_") && value) merged[envName.slice(8)] = value;
209
220
  return merged;
210
221
  }
211
- /** 全局配置装载(缺失/损坏降级默认值并警告,不阻断) */
222
+ /** 全局配置装载(缺失/损坏降级默认值;警告经返回值交给调用方,不阻断) */
212
223
  function loadGlobalConfig(filePath = GLOBAL_CONFIG_PATH) {
213
- if (!fs.existsSync(filePath)) return GlobalConfigSchema.parse({});
224
+ if (!fs.existsSync(filePath)) return { config: GlobalConfigSchema.parse({}) };
214
225
  try {
215
- return GlobalConfigSchema.parse(parse(fs.readFileSync(filePath, "utf8")));
226
+ return { config: GlobalConfigSchema.parse(parse(fs.readFileSync(filePath, "utf8"))) };
216
227
  } catch (e) {
217
- process.stderr.write?.(`[applift] 全局配置解析失败,使用默认值: ${e.message}\n`);
218
- return GlobalConfigSchema.parse({});
228
+ return {
229
+ config: GlobalConfigSchema.parse({}),
230
+ warning: `全局配置解析失败,使用默认值: ${e.message}`
231
+ };
219
232
  }
220
233
  }
221
234
  /** 写凭据(目录自动创建,文件强制 0600) */
@@ -266,8 +279,13 @@ async function loadProjectConfig(cwd) {
266
279
  packageJson: false,
267
280
  defaults: ProjectConfigSchema.parse({})
268
281
  });
282
+ const parsed = ProjectConfigSchema.safeParse(result.config ?? {});
283
+ if (!parsed.success) {
284
+ const details = parsed.error.issues.map((i) => `${i.path.join(".") || "(根)"}: ${i.message}`).join(";");
285
+ throw new Error(`applift.config.ts 字段校验失败 → ${details}`);
286
+ }
269
287
  return {
270
- config: ProjectConfigSchema.parse(result.config ?? {}),
288
+ config: parsed.data,
271
289
  raw: result.config ?? {},
272
290
  cwd: result.cwd ?? cwd
273
291
  };
@@ -370,8 +388,19 @@ async function formPost(url, params, { timeout = 6e4 } = {}) {
370
388
  }
371
389
  }
372
390
  //#endregion
391
+ //#region src/channels/shared/file-blob.ts
392
+ /**
393
+ * 惰性文件 Blob:FormData multipart 上传的文件部件。
394
+ * 优先 fs.openAsBlob(Node ≥19.8,按需分片读,零整读内存);
395
+ * 能力检测不存在时回退整读(旧运行时兜底)。
396
+ */
397
+ async function fileBlob(p) {
398
+ if (typeof fs.openAsBlob === "function") return fs.openAsBlob(p);
399
+ return new Blob([await fs.promises.readFile(p)]);
400
+ }
401
+ //#endregion
373
402
  //#region src/util.ts
374
- /** core 内部共用小工具(不进公共 API) */
403
+ /** core 内部共用小工具(sleep/formatSize 仅内部使用;sha256File 经 index 导出,CLI 历史指纹用) */
375
404
  const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
376
405
  /** 分块计算文件 SHA256(产物可达上百 MB,8MB 分块避免整读内存,同 sign.ts md5File 先例) */
377
406
  function sha256File(filePath) {
@@ -413,7 +442,8 @@ async function pgyerPost(host, apiPath, form) {
413
442
  const res = await fetch(`https://${host}${apiPath}`, {
414
443
  method: "POST",
415
444
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
416
- body
445
+ body,
446
+ signal: AbortSignal.timeout(3e4)
417
447
  });
418
448
  const text = await res.text();
419
449
  try {
@@ -431,6 +461,8 @@ var PgyerChannel = class {
431
461
  scheduled: false,
432
462
  status: false
433
463
  };
464
+ /** 平台错误码 → 人话(1247:源码注释已证 = buildKey 尚未就绪/COS 仍在入库) */
465
+ errorHints = { "1247": "buildKey 尚未就绪(COS 上传仍在入库),属处理中状态,稍候即可" };
434
466
  async plan(ctx) {
435
467
  return {
436
468
  channel: this.id,
@@ -487,7 +519,7 @@ var PgyerChannel = class {
487
519
  form.append("x-cos-security-token", params["x-cos-security-token"]);
488
520
  form.append("key", params.key);
489
521
  form.append("x-cos-meta-file-name", path.basename(ctx.artifactPath));
490
- form.append("file", new Blob([fs.readFileSync(ctx.artifactPath)]), path.basename(ctx.artifactPath));
522
+ form.append("file", await fileBlob(ctx.artifactPath), path.basename(ctx.artifactPath));
491
523
  const upRes = await fetch(endpoint, {
492
524
  method: "POST",
493
525
  body: form,
@@ -970,6 +1002,11 @@ var XiaomiChannel = class {
970
1002
  scheduled: false,
971
1003
  status: true
972
1004
  };
1005
+ /** 平台错误码 → 人话(result 码:依据为本仓测试已固化的真实失败路径文案) */
1006
+ errorHints = {
1007
+ "401": "签名无效或账号无该包名权限(检查 MI_CLIENT_SECRET 与 /dev/query 权限)",
1008
+ "1005": "push SIG 校验失败(检查证书公钥与密码是否匹配)"
1009
+ };
973
1010
  async plan(_ctx) {
974
1011
  return {
975
1012
  channel: this.id,
@@ -1051,8 +1088,8 @@ var XiaomiChannel = class {
1051
1088
  const form = new FormData();
1052
1089
  form.append("RequestData", requestJson);
1053
1090
  form.append("SIG", sig);
1054
- form.append("apk", new Blob([fs.readFileSync(ctx.artifactPath)]), fileName);
1055
- form.append("icon", new Blob([fs.readFileSync(iconPath)]), path.basename(iconPath));
1091
+ form.append("apk", await fileBlob(ctx.artifactPath), fileName);
1092
+ form.append("icon", await fileBlob(iconPath), path.basename(iconPath));
1056
1093
  const res = await fetch(`${MI_API_BASE}/dev/push`, {
1057
1094
  method: "POST",
1058
1095
  body: form,
@@ -1375,7 +1412,7 @@ var OppoChannel = class {
1375
1412
  const form = new FormData();
1376
1413
  form.append("type", "apk");
1377
1414
  form.append("sign", sign);
1378
- form.append("file", new Blob([fs.readFileSync(ctx.artifactPath)]), fileName);
1415
+ form.append("file", await fileBlob(ctx.artifactPath), fileName);
1379
1416
  const upRes = await fetch(uploadUrl, {
1380
1417
  method: "POST",
1381
1418
  body: form,
@@ -1670,7 +1707,7 @@ var VivoChannel = class {
1670
1707
  params.sign = hmacSign(accessSecret, params);
1671
1708
  const form = new FormData();
1672
1709
  for (const [k, v] of Object.entries(params)) form.append(k, v);
1673
- form.append("file", new Blob([fs.readFileSync(ctx.artifactPath)]), fileName);
1710
+ form.append("file", await fileBlob(ctx.artifactPath), fileName);
1674
1711
  const upRes = await fetch(API_BASE$2, {
1675
1712
  method: "POST",
1676
1713
  body: form,
@@ -1925,7 +1962,7 @@ var HonorChannel = class {
1925
1962
  message: `上传 ${fileName}(${formatSize(fileSize)})`
1926
1963
  });
1927
1964
  const form = new FormData();
1928
- form.append("file", new Blob([fs.readFileSync(ctx.artifactPath)]), fileName);
1965
+ form.append("file", await fileBlob(ctx.artifactPath), fileName);
1929
1966
  const upRes = await fetch(uploadUrl, {
1930
1967
  method: "POST",
1931
1968
  headers: { Authorization: `Bearer ${token}` },
@@ -2117,6 +2154,8 @@ var YybChannel = class {
2117
2154
  scheduled: true,
2118
2155
  status: true
2119
2156
  };
2157
+ /** 平台错误码 → 人话(ret 码:依据为本仓测试已固化的真实失败路径) */
2158
+ errorHints = { "10010": "应用不存在(检查 YYB_APP_ID 与 user_id 是否匹配该应用)" };
2120
2159
  /** stage 产物暂存(serial_number + md5),submit 按 appId 取回 */
2121
2160
  staged = /* @__PURE__ */ new Map();
2122
2161
  required(ctx, key, where) {
@@ -2319,7 +2358,10 @@ const COMPILE_POLL_MS = 15e3;
2319
2358
  const APP_TEST_BASE = "https://connect-api.cloud.huawei.com/api/app-test/v1";
2320
2359
  /** AGC JSON API 封装:判 ret.code !== 0 抛错 —— v1 agc.mjs agcJson 原样 */
2321
2360
  async function agcJson(url, init, context) {
2322
- const res = await fetch(url, init);
2361
+ const res = await fetch(url, {
2362
+ ...init,
2363
+ signal: init.signal ?? AbortSignal.timeout(6e4)
2364
+ });
2323
2365
  let json;
2324
2366
  try {
2325
2367
  json = await res.json();
@@ -2779,10 +2821,13 @@ var AgcHarmonyChannel = class {
2779
2821
  current: String(current),
2780
2822
  pageSize: "50"
2781
2823
  });
2782
- const res = await fetch(`${APP_TEST_BASE}/test-group/list?${qs}`, { headers: {
2783
- ...headers,
2784
- appId
2785
- } });
2824
+ const res = await fetch(`${APP_TEST_BASE}/test-group/list?${qs}`, {
2825
+ headers: {
2826
+ ...headers,
2827
+ appId
2828
+ },
2829
+ signal: AbortSignal.timeout(6e4)
2830
+ });
2786
2831
  if (!res.ok) throw new Error(`AGC 查询测试群组失败 HTTP ${res.status}`);
2787
2832
  const json = await res.json();
2788
2833
  if (Number(json.rtnCode) !== 0) throw new Error(`AGC 查询测试群组失败: rtnCode=${json.rtnCode ?? "?"}${json.rtnDesc ? ` ${json.rtnDesc}` : ""}`);
@@ -3247,6 +3292,8 @@ var GooglePlayChannel = class {
3247
3292
  scheduled: false,
3248
3293
  status: true
3249
3294
  };
3295
+ /** 平台错误码 → 人话(HTTP 状态:依据为本仓测试已固化的失败路径) */
3296
+ errorHints = { "403": "权限不足(检查 Service Account 是否已关联 Play Console 且有发布权限)" };
3250
3297
  async plan(_ctx) {
3251
3298
  return {
3252
3299
  channel: this.id,
@@ -3263,12 +3310,40 @@ var GooglePlayChannel = class {
3263
3310
  ref: await this.uploadAndAssign(ctx, "internal", void 0)
3264
3311
  };
3265
3312
  }
3266
- async submit(ctx) {
3313
+ async submit(ctx, stagedRef) {
3314
+ const reusedVc = stagedRef?.split(":")[1];
3315
+ if (reusedVc !== void 0 && reusedVc !== "") return {
3316
+ ok: true,
3317
+ ref: await this.assignExisting(ctx, "production", Number(reusedVc))
3318
+ };
3267
3319
  return {
3268
3320
  ok: true,
3269
3321
  ref: await this.uploadAndAssign(ctx, "production", ctx.updateDescription)
3270
3322
  };
3271
3323
  }
3324
+ /** 复用 stage 已上传的 versionCode:开新 edit(仍需 auth)→ 部署 production → commit,跳过上传 */
3325
+ async assignExisting(ctx, track, versionCode) {
3326
+ const pkgName = this.requirePkgName(ctx);
3327
+ const token = await getAccessToken(ctx.credentials);
3328
+ ctx.emit({
3329
+ type: "phase",
3330
+ phase: "auth",
3331
+ message: "OAuth2 token 获取成功"
3332
+ });
3333
+ const edit = await apiFetch(`${API_BASE}/${pkgName}/edits`, token, {
3334
+ method: "POST",
3335
+ body: "{}"
3336
+ }, "创建 edit");
3337
+ if (!edit.id) throw new Error(`Google Play 创建 edit 响应缺少 id: ${JSON.stringify(edit).slice(0, 300)}`);
3338
+ const editId = edit.id;
3339
+ ctx.emit({
3340
+ type: "phase",
3341
+ phase: "upload",
3342
+ message: `复用 stage 上传的 versionCode ${versionCode}(跳过重复上传)`
3343
+ });
3344
+ await this.deployAndCommit(ctx, pkgName, token, editId, track, versionCode, ctx.updateDescription);
3345
+ return `${editId}:${versionCode}`;
3346
+ }
3272
3347
  /** 上传 + 部署到指定轨道 + commit,返回 `editId:versionCode` */
3273
3348
  async uploadAndAssign(ctx, track, releaseNotes) {
3274
3349
  const pkgName = this.requirePkgName(ctx);
@@ -3307,6 +3382,15 @@ var GooglePlayChannel = class {
3307
3382
  type: "progress",
3308
3383
  detail: `上传成功(versionCode: ${versionCode})`
3309
3384
  });
3385
+ return this.deployAndCommit(ctx, pkgName, token, editId, track, versionCode, releaseNotes);
3386
+ }
3387
+ /**
3388
+ * 部署 versionCode 到指定轨道并 commit:
3389
+ * 先 GET 轨道现有 releases 再合并 PUT——目标 release(同 status=completed 配置)追加新 versionCode,
3390
+ * 其余 releases 原样保留(status/userFraction 不变),避免整轨覆盖把旧版本从轨道剔除;
3391
+ * :commit 冒号语法(对拍 apkgo googleplay.go 权威;对 production 含提交审核语义,internal 即生效)
3392
+ */
3393
+ async deployAndCommit(ctx, pkgName, token, editId, track, versionCode, releaseNotes) {
3310
3394
  const releases = [...(await apiFetch(`${API_BASE}/${pkgName}/edits/${editId}/tracks/${track}`, token, { method: "GET" }, `读取 ${track} 轨道现有 releases`)).releases ?? []];
3311
3395
  const release = {
3312
3396
  versionCodes: [versionCode],
@@ -3374,6 +3458,11 @@ var GooglePlayChannel = class {
3374
3458
  };
3375
3459
  //#endregion
3376
3460
  //#region src/pipeline.ts
3461
+ /**
3462
+ * 发布编排引擎:构建计划 → 确认协议 → 并发执行 → 汇总。
3463
+ * headless:确认通过注入的 confirm 回调完成(CLI 用 clack/弹窗),core 不碰终端。
3464
+ * 插件:可选 PluginHost,在 plan/stage/submit 生命周期触发钩子(可拦截可修改)。
3465
+ */
3377
3466
  async function runLimited(items, limit, fn) {
3378
3467
  const queue = [...items];
3379
3468
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
@@ -3497,12 +3586,13 @@ async function runSubmit(input, options) {
3497
3586
  label: channel.label,
3498
3587
  ok: false,
3499
3588
  submitError: "被插件跳过",
3500
- plan: planFor(plans, basePlans, channel.id)
3589
+ plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
3501
3590
  });
3502
3591
  return;
3503
3592
  }
3504
3593
  effectiveCtx = r.value;
3505
3594
  }
3595
+ let stagedRef;
3506
3596
  if (!options.skipStage && channel.capabilities.stage) {
3507
3597
  const stageResult = await channel.stage(effectiveCtx);
3508
3598
  if (!stageResult.ok) {
@@ -3511,7 +3601,7 @@ async function runSubmit(input, options) {
3511
3601
  label: channel.label,
3512
3602
  ok: false,
3513
3603
  stageError: stageResult.error,
3514
- plan: planFor(plans, basePlans, channel.id)
3604
+ plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
3515
3605
  });
3516
3606
  input.historySink?.({
3517
3607
  channel: channel.id,
@@ -3521,6 +3611,7 @@ async function runSubmit(input, options) {
3521
3611
  });
3522
3612
  return;
3523
3613
  }
3614
+ stagedRef = stageResult.ref;
3524
3615
  input.historySink?.({
3525
3616
  channel: channel.id,
3526
3617
  action: "stage",
@@ -3529,13 +3620,13 @@ async function runSubmit(input, options) {
3529
3620
  artifactVersion: plans.find((p) => p.channel === channel.id)?.artifactVersion || void 0
3530
3621
  });
3531
3622
  }
3532
- const r = await channel.submit(effectiveCtx);
3623
+ const r = await channel.submit(effectiveCtx, stagedRef);
3533
3624
  summary.push({
3534
3625
  channel: channel.id,
3535
3626
  label: channel.label,
3536
3627
  ok: r.ok,
3537
3628
  submitError: r.error,
3538
- plan: planFor(plans, basePlans, channel.id)
3629
+ plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
3539
3630
  });
3540
3631
  input.historySink?.({
3541
3632
  channel: channel.id,
@@ -3550,7 +3641,7 @@ async function runSubmit(input, options) {
3550
3641
  label: channel.label,
3551
3642
  ok: false,
3552
3643
  submitError: e.message,
3553
- plan: planFor(plans, basePlans, channel.id)
3644
+ plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
3554
3645
  });
3555
3646
  input.historySink?.({
3556
3647
  channel: channel.id,
@@ -3573,22 +3664,29 @@ async function safePlan(channel, ctx) {
3573
3664
  try {
3574
3665
  return await channel.plan(ctx);
3575
3666
  } catch {
3576
- return fallbackPlan(channel.id);
3667
+ return fallbackPlan(channel.id, ctx.artifactPath);
3577
3668
  }
3578
3669
  }
3579
- /** plan 失败/被插件删除时的兜底条目(保 summary.plan 非可选契约) */
3580
- function fallbackPlan(channelId) {
3670
+ /** 按产物扩展名推断平台(兜底条目用):.ipa→ios,.hap→harmony,.apk/.aab 及其余→android */
3671
+ function platformOfArtifact(artifactPath) {
3672
+ const ext = path.extname(artifactPath).toLowerCase();
3673
+ if (ext === ".ipa") return "ios";
3674
+ if (ext === ".hap") return "harmony";
3675
+ return "android";
3676
+ }
3677
+ /** plan 失败/被插件删除时的兜底条目(保 summary.plan 非可选契约;平台按产物推断不再硬编码 android) */
3678
+ function fallbackPlan(channelId, artifactPath) {
3581
3679
  return {
3582
3680
  channel: channelId,
3583
- platform: "android",
3681
+ platform: platformOfArtifact(artifactPath),
3584
3682
  artifactVersion: "",
3585
3683
  action: "stage+submit",
3586
3684
  afterApproval: "auto-publish"
3587
3685
  };
3588
3686
  }
3589
3687
  /** 汇总条目取计划:plan:before 改写后的 plans 优先,被插件删除时回退改写前快照,永不返回 undefined */
3590
- function planFor(plans, base, id) {
3591
- return plans.find((p) => p.channel === id) ?? base.get(id) ?? fallbackPlan(id);
3688
+ function planFor(plans, base, id, artifactPath) {
3689
+ return plans.find((p) => p.channel === id) ?? base.get(id) ?? fallbackPlan(id, artifactPath);
3592
3690
  }
3593
3691
  //#endregion
3594
3692
  //#region src/plugin.ts
@@ -3599,6 +3697,8 @@ var PluginHost = class {
3599
3697
  }
3600
3698
  /**
3601
3699
  * 顺序执行某钩子;支持「链式替换」(返回对象则替换后继续传给下一插件)与「拦截」(返回 false 立即中止)。
3700
+ * 信任边界:返回值按 T 强转、不做形状校验(vite 同款取舍)——插件作者自负其责,
3701
+ * 坏形状会在后续消费点显式抛错,宿主不代为静默兜底。
3602
3702
  * @returns {[被拦截?, 最终值]} false 表示被某插件拦截
3603
3703
  */
3604
3704
  async apply(hook, value, ctx) {
@@ -4073,13 +4173,116 @@ function readEntries(query = {}, filePath = HISTORY_PATH) {
4073
4173
  //#region src/channels/shared/error-codes.ts
4074
4174
  /**
4075
4175
  * 错误码人话化:消费各渠道自带 errorHints(平台内聚——表与渠道实现同文件)。
4176
+ * 网络类故障(AbortSignal 超时/断连/DNS)渠道层不转译,统一在此兜底提示。
4076
4177
  */
4178
+ /** 网络故障特征 → 人话提示(命中即返回,优先于渠道码表) */
4179
+ const NETWORK_HINTS = [[/aborted due to timeout|TimeoutError/i, "请求超时(服务端无响应或网络不稳):可重跑;大包上传建议在稳定网络环境执行"], [/\b(?:ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|EPIPE)\b|socket hang up|fetch failed/i, "网络故障(连接被重置/DNS 解析失败等):检查网络或代理后重跑"]];
4180
+ /** 码匹配:纯数字码加数字边界(防 1003 误中 11003/10031);其余按包含匹配 */
4181
+ function matchesCode(message, code) {
4182
+ if (/^\d+$/.test(code)) return new RegExp(`(?<!\\d)${code}(?!\\d)`).test(message);
4183
+ return message.includes(code);
4184
+ }
4077
4185
  function humanizeError(hints, message) {
4186
+ for (const [re, hint] of NETWORK_HINTS) if (re.test(message)) return `${message}\n 提示: ${hint}`;
4078
4187
  if (!hints) return message;
4079
- for (const [code, hint] of Object.entries(hints)) if (message.includes(code)) return `${message}\n 提示: ${hint}`;
4188
+ for (const [code, hint] of Object.entries(hints)) if (matchesCode(message, code)) return `${message}\n 提示: ${hint}`;
4080
4189
  return message;
4081
4190
  }
4082
4191
  //#endregion
4192
+ //#region src/channels/shared/redact.ts
4193
+ /**
4194
+ * 凭据脱敏(对拍 apkgo pkg/httptrace/redact.go 权威)。
4195
+ * 判定依据是“名字”:JSON 键 / 表单字段 / query 参数 / header 名携带凭据语义即整值掩码为
4196
+ * [REDACTED <len>](保留长度线索),空串保持空串——“签名为空”本身就是排查线索。
4197
+ * 非敏感名下的值仍递归扫内容(signed URL / 内嵌 JSON / name=value 自由文本)。
4198
+ * 故意分歧:掩码不做 percent-encode(日志可读性优先);JSON 再序列化保持原键序(Go map 序随机)。
4199
+ * 挂接点:CLI / 错误消息层接入(core 先提供能力)。
4200
+ */
4201
+ /** 归一化后「包含即敏感」的词(对拍 redact.go sensitiveWords) */
4202
+ const SENSITIVE_WORDS = [
4203
+ "secret",
4204
+ "token",
4205
+ "password",
4206
+ "passwd",
4207
+ "authorization",
4208
+ "credential",
4209
+ "cookie",
4210
+ "privatekey",
4211
+ "apikey",
4212
+ "assertion",
4213
+ "jwt",
4214
+ "signature"
4215
+ ];
4216
+ /** 归一化后全等即敏感的名字(对拍 redact.go sensitiveNames) */
4217
+ const SENSITIVE_NAMES = {
4218
+ sig: true,
4219
+ sign: true,
4220
+ pwd: true,
4221
+ auth: true
4222
+ };
4223
+ /** 名字是否携带凭据语义(JSON 键 / 表单字段 / query 参数 / header 名通用;归一化 = 小写去非字母数字) */
4224
+ function isSensitiveName(name) {
4225
+ const n = name.toLowerCase().replace(/[^a-z0-9]/g, "");
4226
+ if (n in SENSITIVE_NAMES) return true;
4227
+ if (n === "tokentype" || n.startsWith("accesscontrol")) return false;
4228
+ if (n.endsWith("sign") && n !== "design") return true;
4229
+ return SENSITIVE_WORDS.some((w) => n.includes(w));
4230
+ }
4231
+ /** 整值掩码:保留长度;空串保持(“签名为空”是有效诊断信息)。3 处调用点需锁步一致 */
4232
+ function mask(v) {
4233
+ return v === "" ? "" : `[REDACTED ${v.length}]`;
4234
+ }
4235
+ /**
4236
+ * 按名字脱敏单个值:敏感名 → 整值掩码;Authorization 保留 scheme("Bearer [REDACTED 812]",
4237
+ * 对拍 redact.go redactHeaders);非敏感名 → 递归 redactText 扫值内凭据。
4238
+ */
4239
+ function redactValue(name, v) {
4240
+ if (/^authorization$/i.test(name) && v.includes(" ")) {
4241
+ const schemeEnd = v.indexOf(" ");
4242
+ return `${v.slice(0, schemeEnd)} ${mask(v.slice(schemeEnd + 1))}`;
4243
+ }
4244
+ return isSensitiveName(name) ? mask(v) : redactText(v);
4245
+ }
4246
+ /** redactURL:剥离 userinfo + query 参数按名脱敏,路径/fragment 原样保留 */
4247
+ function redactUrlString(v) {
4248
+ let out = v.replace(/^(https?:\/\/)[^/?#]*@/, "$1");
4249
+ const hashIdx = out.indexOf("#");
4250
+ const hash = hashIdx >= 0 ? out.slice(hashIdx) : "";
4251
+ if (hashIdx >= 0) out = out.slice(0, hashIdx);
4252
+ const qIdx = out.indexOf("?");
4253
+ if (qIdx < 0) return out + hash;
4254
+ const query = new URLSearchParams(out.slice(qIdx + 1));
4255
+ const pairs = [];
4256
+ for (const [k, val] of query) pairs.push(`${k}=${isSensitiveName(k) ? mask(val) : val}`);
4257
+ return `${out.slice(0, qIdx)}?${pairs.join("&")}${hash}`;
4258
+ }
4259
+ /** free 文本中的 name=value / "name":"value" 凭据对(对拍 redact.go scrubRe;掩码无长度) */
4260
+ const SCRUB_RE = /([\w.-]*(?:secret|token|password|passwd|signature|credential|authorization|api_?key|assertion)[\w.-]*"?\s*[=:]\s*"?)([^"&\s,;<}]+)/gi;
4261
+ /** JSON 树递归:敏感名下字符串掩码、数字/布尔置 [REDACTED],数组元素继承外层键名 */
4262
+ function redactTree(name, v) {
4263
+ if (v === null || typeof v !== "object") {
4264
+ if (typeof v === "string") return redactValue(name, v);
4265
+ return isSensitiveName(name) ? "[REDACTED]" : v;
4266
+ }
4267
+ if (Array.isArray(v)) return v.map((x) => redactTree(name, x));
4268
+ const out = {};
4269
+ for (const [k, val] of Object.entries(v)) out[k] = redactTree(k, val);
4270
+ return out;
4271
+ }
4272
+ /**
4273
+ * 文本脱敏(三层层叠,逐层尝试):
4274
+ * 1) http(s) URL 带 query/userinfo → query 按名脱敏 + 剥 userinfo
4275
+ * 2) { / [ 开头且整串为合法 JSON → 递归按键名脱敏
4276
+ * 3) 其余 → name=value 正则扫(错误消息等无结构文本)
4277
+ */
4278
+ function redactText(v) {
4279
+ if (/^https?:\/\//i.test(v) && (v.includes("?") || /^https?:\/\/[^/?#]*@/i.test(v))) return redactUrlString(v);
4280
+ if (v.length > 1 && (v.startsWith("{") || v.startsWith("["))) try {
4281
+ return JSON.stringify(redactTree("", JSON.parse(v)));
4282
+ } catch {}
4283
+ return v.replace(SCRUB_RE, "$1[REDACTED]");
4284
+ }
4285
+ //#endregion
4083
4286
  //#region src/version/project.ts
4084
4287
  /**
4085
4288
  * 工程文件版本读写(可接管模式:版本事实源永远是工程文件,applift 只代读代写)。
@@ -5039,4 +5242,4 @@ function fromAapt(artifactPath) {
5039
5242
  }
5040
5243
  }
5041
5244
  //#endregion
5042
- export { APPLIFT_HOME, AgcHarmonyChannel, AscChannel, BetaConfigSchema, CREDENTIALS_PATH, CredentialsFileSchema, CredentialsProfileSchema, DefaultChannelRegistry, GLOBAL_CONFIG_PATH, GlobalConfigSchema, GooglePlayChannel, HISTORY_PATH, HonorChannel, HuaweiAndroidChannel, OppoChannel, PROJECTS_MAP_PATH, PgyerChannel, PluginHost, ProjectConfigSchema, ReleaseConfigSchema, StoreConfigSchema, StoresSchema, VersionCodeConfigSchema, VersionConfigSchema, VivoChannel, WAIT_BEFORE_SUBMIT_MS, XiaomiChannel, YybChannel, appendEntry, applyBump, assertListingFiles, commitsSince, decideNextVersion, defineConfig, detectBundleIds, detectFramework, electronAdapter, evalVersionCodeFormula, findArtifacts, findUp, flutterAdapter, formPost, getAgcToken, getHarmonyAppId, git, hmacSign, humanizeError, inferBumpLevel, isGitRepo, latestSemverTag, loadCredentials, loadCredentialsLayered, loadGlobalConfig, loadListingFile, loadProjectConfig, loadProjectProfileMap, looseSemver, md5File, md5String, nativeAndroidAdapter, nativeHarmonyAdapter, nativeIosAdapter, nextVersionCode, planBump, projectCredentialsPath, readArtifactVersion, readEntries, readProjectVersions, registry, renderChangelogSection, resolveListing, resolvePlugins, resolveProfile, resolveProfileFor, rnAdapter, runStage, runSubmit, saveCredentials, saveProjectProfileMap, setProjectProfile, sha256File, sortedQueryString, statusShort, storeVersionName, tauriAdapter, toChangelogEntries, tsMillis, tsSeconds, upsertChangelog, writeProjectVersions };
5245
+ export { APPLIFT_HOME, AgcHarmonyChannel, AscChannel, BetaConfigSchema, CREDENTIALS_PATH, CredentialsFileSchema, CredentialsProfileSchema, DefaultChannelRegistry, GLOBAL_CONFIG_PATH, GlobalConfigSchema, GooglePlayChannel, HISTORY_PATH, HonorChannel, HuaweiAndroidChannel, OppoChannel, PROJECTS_MAP_PATH, PgyerChannel, PluginHost, ProjectConfigSchema, ReleaseConfigSchema, StoreConfigSchema, StoresSchema, VersionCodeConfigSchema, VersionConfigSchema, VivoChannel, WAIT_BEFORE_SUBMIT_MS, XiaomiChannel, YybChannel, appendEntry, applyBump, assertListingFiles, commitsSince, decideNextVersion, defineConfig, detectBundleIds, detectFramework, electronAdapter, evalVersionCodeFormula, findArtifacts, findUp, flutterAdapter, formPost, getAgcToken, getHarmonyAppId, git, hmacSign, humanizeError, inferBumpLevel, isGitRepo, latestSemverTag, loadCredentials, loadCredentialsLayered, loadGlobalConfig, loadListingFile, loadProjectConfig, loadProjectProfileMap, looseSemver, md5File, md5String, nativeAndroidAdapter, nativeHarmonyAdapter, nativeIosAdapter, nextVersionCode, planBump, projectCredentialsPath, readArtifactVersion, readEntries, readProjectVersions, redactText, registry, renderChangelogSection, resolveListing, resolvePlugins, resolveProfile, resolveProfileFor, rnAdapter, runStage, runSubmit, saveCredentials, saveProjectProfileMap, setProjectProfile, sha256File, sortedQueryString, statusShort, storeVersionName, tauriAdapter, toChangelogEntries, tsMillis, tsSeconds, upsertChangelog, writeProjectVersions };
@@ -46,16 +46,6 @@ interface BundleIds {
46
46
  declare function detectBundleIds(cwd: string): BundleIds;
47
47
  //#endregion
48
48
  //#region src/channel.d.ts
49
- /**
50
- * 渠道适配器统一接口 —— applift 可扩展性的核心。
51
- *
52
- * 每个渠道(六商店/AGC/ASC/蒲公英…)实现本接口并注册到 registry;
53
- * 编排引擎(pipeline)只面对本接口,不感知任何渠道细节。
54
- *
55
- * 两段式安全模型:
56
- * stage = 只把包送到渠道草稿区,不动线上(iOS 上传构建≈只进 TestFlight)
57
- * submit = 提审;除 iOS(默认 manual 发布)外,审核通过即自动上架 —— 不可逆,需计划确认
58
- */
59
49
  /** 渠道能力声明(编排与确认界面据此渲染/拦截) */
60
50
  interface ChannelCapabilities {
61
51
  /** 支持只传包不提审(小米 push 一体 → false,submit 即上传) */
@@ -145,8 +135,12 @@ interface Channel {
145
135
  plan(ctx: ChannelContext): Promise<ReleasePlanEntry>;
146
136
  /** 传包到草稿(capabilities.stage=false 的渠道返回明确错误) */
147
137
  stage(ctx: ChannelContext): Promise<StageResult>;
148
- /** 提审(=审核通过自动上架,L3 不可逆操作) */
149
- submit(ctx: ChannelContext): Promise<SubmitResult>;
138
+ /**
139
+ * 提审(=审核通过自动上架,L3 不可逆操作)。
140
+ * stagedRef = 同渠道 stage 成功返回的 ref(如 googleplay 的 `editId:versionCode`),
141
+ * 供实现复用 stage 已上传的草稿、避免重复上传;实现方可忽略。
142
+ */
143
+ submit(ctx: ChannelContext, stagedRef?: string): Promise<SubmitResult>;
150
144
  /** 查询审核状态(capabilities.status=false 时抛 NotSupported) */
151
145
  status(ctx: Pick<ChannelContext, "credentials" | "target">): Promise<ReviewStatus>;
152
146
  }
@@ -167,13 +161,7 @@ type CredentialsFile = z.infer<typeof CredentialsFileSchema>;
167
161
  /** 单渠道配置(appId、pkgName 等渠道侧键) */
168
162
  declare const StoreConfigSchema: z.ZodRecord<z.ZodString, z.ZodString>;
169
163
  type StoreConfig = z.infer<typeof StoreConfigSchema>;
170
- /**
171
- * stores 三种等价写法,统一规范化为 record:
172
- * ① 对象:{ huawei: { appId: 'x' } }(原有写法,原样保留)
173
- * ② 数组:['oppo', 'vivo'] → { oppo: {}, vivo: {} }(无配置渠道)
174
- * ③ 字符串值:{ huawei: '10xxx' } → { huawei: { appId: '10xxx' } }(appId 简写)
175
- */
176
- declare const StoresSchema: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodString>, z.ZodString]>>, z.ZodArray<z.ZodString>]>, z.ZodTransform<Record<string, Record<string, string>>, string[] | Record<string, string | Record<string, string>>>>>;
164
+ declare const StoresSchema: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodString>, z.ZodString]>>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>]>, z.ZodTransform<Record<string, Record<string, string>>, (string | Record<string, string>)[] | Record<string, string | Record<string, string>>>>>;
177
165
  type Stores = z.infer<typeof StoresSchema>;
178
166
  /** 项目级配置(applift.config.ts 的形状);beta 段见上方 BetaConfigSchema */
179
167
  /**
@@ -223,15 +211,15 @@ declare const ProjectConfigSchema: z.ZodObject<{
223
211
  name: z.ZodOptional<z.ZodString>;
224
212
  profile: z.ZodDefault<z.ZodString>;
225
213
  android: z.ZodOptional<z.ZodObject<{
226
- pkgName: z.ZodString;
227
- stores: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodString>, z.ZodString]>>, z.ZodArray<z.ZodString>]>, z.ZodTransform<Record<string, Record<string, string>>, string[] | Record<string, string | Record<string, string>>>>>;
214
+ pkgName: z.ZodOptional<z.ZodString>;
215
+ stores: z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodString>, z.ZodString]>>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>]>, z.ZodTransform<Record<string, Record<string, string>>, (string | Record<string, string>)[] | Record<string, string | Record<string, string>>>>>;
228
216
  }, z.core.$strip>>;
229
217
  harmony: z.ZodOptional<z.ZodObject<{
230
- bundleName: z.ZodString;
218
+ bundleName: z.ZodOptional<z.ZodString>;
231
219
  appId: z.ZodOptional<z.ZodString>;
232
220
  }, z.core.$strip>>;
233
221
  ios: z.ZodOptional<z.ZodObject<{
234
- scheme: z.ZodString;
222
+ scheme: z.ZodOptional<z.ZodString>;
235
223
  workspace: z.ZodOptional<z.ZodString>;
236
224
  bundleId: z.ZodOptional<z.ZodString>;
237
225
  }, z.core.$strip>>;
@@ -359,7 +347,7 @@ interface HookContext {
359
347
  /** 项目根目录 */
360
348
  cwd: string;
361
349
  }
362
- interface QypubPlugin {
350
+ interface AppliftPlugin {
363
351
  name: string;
364
352
  "config:resolved"?(config: ProjectConfig, ctx: HookContext): Promise<ProjectConfig | void> | ProjectConfig | void;
365
353
  "channel:register"?(registry: ChannelRegistry, ctx: HookContext): Promise<void> | void;
@@ -377,12 +365,14 @@ interface QypubPlugin {
377
365
  "submit:after"?(summary: PipelineSummary, ctx: HookContext): Promise<void> | void;
378
366
  notify?(summary: PipelineSummary, ctx: HookContext): Promise<void> | void;
379
367
  }
380
- type PluginHookName = Exclude<keyof QypubPlugin, "name">;
368
+ type PluginHookName = Exclude<keyof AppliftPlugin, "name">;
381
369
  declare class PluginHost {
382
370
  private readonly plugins;
383
- constructor(plugins: QypubPlugin[]);
371
+ constructor(plugins: AppliftPlugin[]);
384
372
  /**
385
373
  * 顺序执行某钩子;支持「链式替换」(返回对象则替换后继续传给下一插件)与「拦截」(返回 false 立即中止)。
374
+ * 信任边界:返回值按 T 强转、不做形状校验(vite 同款取舍)——插件作者自负其责,
375
+ * 坏形状会在后续消费点显式抛错,宿主不代为静默兜底。
386
376
  * @returns {[被拦截?, 最终值]} false 表示被某插件拦截
387
377
  */
388
378
  apply<T>(hook: PluginHookName, value: T, ctx: HookContext): Promise<{
@@ -396,11 +386,11 @@ declare class PluginHost {
396
386
  * defineConfig —— vite 同款体验:applift.config.ts 导出时提供类型提示。
397
387
  * plugins 中允许嵌套工厂函数(如 dingtalkNotify({...}) 直接返回插件对象)。
398
388
  */
399
- interface QypubUserConfig extends Partial<ProjectConfig> {
400
- plugins?: Array<QypubPlugin | (() => QypubPlugin)>;
389
+ interface AppliftUserConfig extends Partial<ProjectConfig> {
390
+ plugins?: Array<AppliftPlugin | (() => AppliftPlugin)>;
401
391
  }
402
- declare function defineConfig(config: QypubUserConfig): QypubUserConfig;
392
+ declare function defineConfig(config: AppliftUserConfig): AppliftUserConfig;
403
393
  /** 解析用户配置中的插件(工厂惰性求值),供 PluginHost 构造 */
404
- declare function resolvePlugins(userConfig: QypubUserConfig): QypubPlugin[];
394
+ declare function resolvePlugins(userConfig: AppliftUserConfig): AppliftPlugin[];
405
395
  //#endregion
406
- export { electronAdapter as $, ReleaseConfigSchema as A, ChannelCapabilities as B, CredentialsProfile as C, ProjectConfig as D, GlobalConfigSchema as E, VersionCodeConfig as F, ReviewStatus as G, ChannelEvent as H, VersionCodeConfigSchema as I, BundleIds as J, StageResult as K, VersionConfig as L, StoreConfigSchema as M, Stores as N, ProjectConfigSchema as O, StoresSchema as P, detectFramework as Q, VersionConfigSchema as R, CredentialsFileSchema as S, GlobalConfig as T, ChannelRegistry as U, ChannelContext as V, ReleasePlanEntry as W, Platform as X, FrameworkAdapter as Y, detectBundleIds as Z, appendEntry as _, QypubUserConfig as a, rnAdapter as at, BetaConfigSchema as b, PipelineInput as c, runStage as d, findArtifacts as et, runSubmit as f, HistoryQuery as g, HistoryEvent as h, QypubPlugin as i, nativeIosAdapter as it, StoreConfig as j, ReleaseConfig as k, PipelineSummary as l, HistoryEntry as m, PluginHookName as n, nativeAndroidAdapter as nt, defineConfig as o, tauriAdapter as ot, HISTORY_PATH as p, SubmitResult as q, PluginHost as r, nativeHarmonyAdapter as rt, resolvePlugins as s, HookContext as t, flutterAdapter as tt, PipelineSummaryEntry as u, readEntries as v, CredentialsProfileSchema as w, CredentialsFile as x, BetaConfig as y, Channel as z };
396
+ export { electronAdapter as $, ReleaseConfigSchema as A, ChannelCapabilities as B, CredentialsProfile as C, ProjectConfig as D, GlobalConfigSchema as E, VersionCodeConfig as F, ReviewStatus as G, ChannelEvent as H, VersionCodeConfigSchema as I, BundleIds as J, StageResult as K, VersionConfig as L, StoreConfigSchema as M, Stores as N, ProjectConfigSchema as O, StoresSchema as P, detectFramework as Q, VersionConfigSchema as R, CredentialsFileSchema as S, GlobalConfig as T, ChannelRegistry as U, ChannelContext as V, ReleasePlanEntry as W, Platform as X, FrameworkAdapter as Y, detectBundleIds as Z, appendEntry as _, PluginHost as a, rnAdapter as at, BetaConfigSchema as b, PipelineInput as c, runStage as d, findArtifacts as et, runSubmit as f, HistoryQuery as g, HistoryEvent as h, PluginHookName as i, nativeIosAdapter as it, StoreConfig as j, ReleaseConfig as k, PipelineSummary as l, HistoryEntry as m, AppliftUserConfig as n, nativeAndroidAdapter as nt, defineConfig as o, tauriAdapter as ot, HISTORY_PATH as p, SubmitResult as q, HookContext as r, nativeHarmonyAdapter as rt, resolvePlugins as s, AppliftPlugin as t, flutterAdapter as tt, PipelineSummaryEntry as u, readEntries as v, CredentialsProfileSchema as w, CredentialsFile as x, BetaConfig as y, Channel as z };
@@ -1,4 +1,4 @@
1
- import { i as QypubPlugin } from "../plugin-CTt0LO8c.mjs";
1
+ import { t as AppliftPlugin } from "../plugin-B2GGlI_2.mjs";
2
2
  //#region src/plugins/dingtalk-notify.d.ts
3
3
  interface DingtalkNotifyOptions {
4
4
  webhook: string;
@@ -6,6 +6,6 @@ interface DingtalkNotifyOptions {
6
6
  secret?: string;
7
7
  atMobiles?: string[];
8
8
  }
9
- export declare function dingtalkNotify(options: DingtalkNotifyOptions): QypubPlugin;
9
+ export declare function dingtalkNotify(options: DingtalkNotifyOptions): AppliftPlugin;
10
10
  //#endregion
11
11
  export type { DingtalkNotifyOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applift/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/hcg1023/applift.git",