shipup 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/LICENSE +14 -0
- package/README.md +112 -0
- package/README.zh-CN.md +105 -0
- package/SECURITY.md +32 -0
- package/creds.example.yaml +62 -0
- package/dist/adapters/android/honor/api.js +360 -0
- package/dist/adapters/android/honor/api.js.map +1 -0
- package/dist/adapters/android/honor/index.js +122 -0
- package/dist/adapters/android/honor/index.js.map +1 -0
- package/dist/adapters/android/huawei/api.js +389 -0
- package/dist/adapters/android/huawei/api.js.map +1 -0
- package/dist/adapters/android/huawei/index.js +128 -0
- package/dist/adapters/android/huawei/index.js.map +1 -0
- package/dist/adapters/android/meizu/api.js +367 -0
- package/dist/adapters/android/meizu/api.js.map +1 -0
- package/dist/adapters/android/meizu/index.js +129 -0
- package/dist/adapters/android/meizu/index.js.map +1 -0
- package/dist/adapters/android/oppo/api.js +397 -0
- package/dist/adapters/android/oppo/api.js.map +1 -0
- package/dist/adapters/android/oppo/index.js +135 -0
- package/dist/adapters/android/oppo/index.js.map +1 -0
- package/dist/adapters/android/qq/api.js +184 -0
- package/dist/adapters/android/qq/api.js.map +1 -0
- package/dist/adapters/android/qq/index.js +80 -0
- package/dist/adapters/android/qq/index.js.map +1 -0
- package/dist/adapters/android/samsung/api.js +355 -0
- package/dist/adapters/android/samsung/api.js.map +1 -0
- package/dist/adapters/android/samsung/index.js +111 -0
- package/dist/adapters/android/samsung/index.js.map +1 -0
- package/dist/adapters/android/specs.js +106 -0
- package/dist/adapters/android/specs.js.map +1 -0
- package/dist/adapters/android/stub.js +27 -0
- package/dist/adapters/android/stub.js.map +1 -0
- package/dist/adapters/android/vivo/api.js +236 -0
- package/dist/adapters/android/vivo/api.js.map +1 -0
- package/dist/adapters/android/vivo/index.js +121 -0
- package/dist/adapters/android/vivo/index.js.map +1 -0
- package/dist/adapters/android/xiaomi/api.js +163 -0
- package/dist/adapters/android/xiaomi/api.js.map +1 -0
- package/dist/adapters/android/xiaomi/index.js +108 -0
- package/dist/adapters/android/xiaomi/index.js.map +1 -0
- package/dist/adapters/ios/appstore/asc.js +280 -0
- package/dist/adapters/ios/appstore/asc.js.map +1 -0
- package/dist/adapters/ios/appstore/index.js +323 -0
- package/dist/adapters/ios/appstore/index.js.map +1 -0
- package/dist/adapters/registry.js +34 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/util.js +26 -0
- package/dist/adapters/util.js.map +1 -0
- package/dist/cli/index.js +479 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.js +65 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/core/exit.js +20 -0
- package/dist/core/exit.js.map +1 -0
- package/dist/core/orchestrator.js +24 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/types.js +20 -0
- package/dist/core/types.js.map +1 -0
- package/dist/creds/load.js +68 -0
- package/dist/creds/load.js.map +1 -0
- package/dist/creds/schema.js +30 -0
- package/dist/creds/schema.js.map +1 -0
- package/dist/infra/crypto.js +12 -0
- package/dist/infra/crypto.js.map +1 -0
- package/dist/infra/http.js +87 -0
- package/dist/infra/http.js.map +1 -0
- package/dist/pkginfo/apk-icon.js +89 -0
- package/dist/pkginfo/apk-icon.js.map +1 -0
- package/dist/pkginfo/icon.js +83 -0
- package/dist/pkginfo/icon.js.map +1 -0
- package/dist/pkginfo/index.js +33 -0
- package/dist/pkginfo/index.js.map +1 -0
- package/docs/android-markets.md +190 -0
- package/docs/credentials.md +72 -0
- package/docs/providers.md +60 -0
- package/lib/common.mjs +265 -0
- package/lib/harmony.mjs +190 -0
- package/lib/huawei.mjs +153 -0
- package/lib/ios.mjs +477 -0
- package/package.json +70 -0
- package/shipup.mjs +160 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// 通用类型:渠道适配统一接口 + 归一化状态。两端(android/ios)共用。
|
|
2
|
+
/** Remove credentials, signed query values, private keys, and long opaque tokens from diagnostics. */
|
|
3
|
+
export function redactSensitive(value) {
|
|
4
|
+
return String(value ?? '')
|
|
5
|
+
.replace(/-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g, '<redacted-private-key>')
|
|
6
|
+
.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?[^\s,}\]]+/gi, '$1<redacted>')
|
|
7
|
+
.replace(/(["']?(?:access_?token|refresh_?token|client_?secret|access_?secret|private_?key|password|authCode|token)["']?\s*[:=]\s*["']?)([^"'\s,&}\]]+)/gi, '$1<redacted>')
|
|
8
|
+
.replace(/([?&](?:access_?token|client_?secret|access_?secret|password|authCode|signature|sign)=)[^&\s]+/gi, '$1<redacted>')
|
|
9
|
+
.replace(/\b[A-Za-z0-9_-]{80,}\b/g, '<redacted-opaque-value>');
|
|
10
|
+
}
|
|
11
|
+
/** 各市场抛出的业务错误:携带原始码 */
|
|
12
|
+
export class UploadError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(redactSensitive(message));
|
|
16
|
+
this.name = 'UploadError';
|
|
17
|
+
this.code = String(code);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAuF3C,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;SACvB,OAAO,CAAC,mDAAmD,EAAE,wBAAwB,CAAC;SACtF,OAAO,CAAC,qDAAqD,EAAE,cAAc,CAAC;SAC9E,OAAO,CACN,iJAAiJ,EACjJ,cAAc,CACf;SACA,OAAO,CAAC,kGAAkG,EAAE,cAAc,CAAC;SAC3H,OAAO,CAAC,yBAAyB,EAAE,yBAAyB,CAAC,CAAC;AACnE,CAAC;AAED,uBAAuB;AACvB,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,CAAS;IACtB,YAAY,IAAqB,EAAE,OAAe;QAChD,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;CACF"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 读取 creds 文件并解析每个值:字面量 / ${ENV_VAR} / @path。
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
5
|
+
import { CredsError, InputError } from '../core/exit.js';
|
|
6
|
+
function resolveValue(value, baseDir) {
|
|
7
|
+
if (typeof value !== 'string')
|
|
8
|
+
return value;
|
|
9
|
+
const v = value.trim();
|
|
10
|
+
const env = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(v);
|
|
11
|
+
if (env) {
|
|
12
|
+
const name = env[1];
|
|
13
|
+
const got = process.env[name];
|
|
14
|
+
if (got === undefined || got === '') {
|
|
15
|
+
throw new CredsError(`环境变量未设置: ${name}`);
|
|
16
|
+
}
|
|
17
|
+
return got;
|
|
18
|
+
}
|
|
19
|
+
if (v.startsWith('@')) {
|
|
20
|
+
const p = resolvePath(baseDir, v.slice(1));
|
|
21
|
+
if (!existsSync(p))
|
|
22
|
+
throw new CredsError(`creds 引用的文件不存在: ${p}`);
|
|
23
|
+
return readFileSync(p, 'utf8').replace(/\n+$/, '');
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function resolveDeep(obj, baseDir) {
|
|
28
|
+
if (Array.isArray(obj))
|
|
29
|
+
return obj.map((x) => resolveDeep(x, baseDir));
|
|
30
|
+
if (obj && typeof obj === 'object') {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [k, val] of Object.entries(obj))
|
|
33
|
+
out[k] = resolveDeep(val, baseDir);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
return resolveValue(obj, baseDir);
|
|
37
|
+
}
|
|
38
|
+
// Earlier shipup credential files allowed `key: @./file` even though `@` must
|
|
39
|
+
// be quoted in standard YAML. Normalize that legacy block-scalar form before
|
|
40
|
+
// parsing so existing signing setups continue to work unchanged.
|
|
41
|
+
function normalizeLegacyFileReferences(text) {
|
|
42
|
+
return text.replace(/^(\s*[\w.-]+\s*:\s*)(@[^#\r\n]*?)(\s*(?:#.*)?)$/gm, (_match, prefix, reference, suffix) => `${prefix}${JSON.stringify(reference.trim())}${suffix}`);
|
|
43
|
+
}
|
|
44
|
+
/** 读取并解析 creds 文件,返回值已解析为明文。 */
|
|
45
|
+
export function loadCreds(path, section) {
|
|
46
|
+
if (!existsSync(path))
|
|
47
|
+
throw new InputError(`creds 文件不存在: ${path}`);
|
|
48
|
+
if (process.platform !== 'win32') {
|
|
49
|
+
const mode = statSync(path).mode & 0o777;
|
|
50
|
+
if ((mode & 0o077) !== 0) {
|
|
51
|
+
console.error(`[shipup] 警告:凭证文件权限为 ${mode.toString(8)},建议 chmod 600 ${path}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = parseYaml(normalizeLegacyFileReferences(readFileSync(path, 'utf8')));
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
throw new CredsError(`creds 文件解析失败: ${e.message}`);
|
|
60
|
+
}
|
|
61
|
+
const selected = section && parsed?.[section] ? parsed[section] : parsed;
|
|
62
|
+
return resolveDeep(selected, dirname(resolvePath(path)));
|
|
63
|
+
}
|
|
64
|
+
/** 取某渠道的凭证(android creds 的 channels.<channel>)。 */
|
|
65
|
+
export function getChannelCreds(creds, channel) {
|
|
66
|
+
return (creds?.channels?.[channel] ?? {});
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/creds/load.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// 各渠道 creds 必填字段(与现有 config.dart 对齐),用于上传前校验。
|
|
2
|
+
import { CredsError } from '../core/exit.js';
|
|
3
|
+
export const ANDROID_REQUIRED = {
|
|
4
|
+
huawei: ['app_id', 'client_id', 'client_secret'],
|
|
5
|
+
honor: ['app_id', 'client_id', 'client_secret'],
|
|
6
|
+
oppo: ['app_id', 'client_id', 'client_secret'],
|
|
7
|
+
vivo: ['app_id', 'access_key', 'access_secret'],
|
|
8
|
+
xiaomi: ['user_name', 'password', 'rsa_modulus'],
|
|
9
|
+
samsung: ['app_id', 'service_account', 'private_key'],
|
|
10
|
+
qq: ['app_id', 'user_id', 'access_secret'],
|
|
11
|
+
meizu: ['access_key', 'access_secret'],
|
|
12
|
+
};
|
|
13
|
+
export const ANDROID_CHANNELS = Object.keys(ANDROID_REQUIRED);
|
|
14
|
+
export function validateChannelCreds(channel, creds) {
|
|
15
|
+
const required = ANDROID_REQUIRED[channel];
|
|
16
|
+
if (!required)
|
|
17
|
+
throw new CredsError(`未知渠道: ${channel}`);
|
|
18
|
+
const missing = required.filter((k) => !creds[k]);
|
|
19
|
+
if (missing.length) {
|
|
20
|
+
throw new CredsError(`渠道 ${channel} 缺少凭证字段: ${missing.join(', ')}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const IOS_REQUIRED = ['key_id', 'issuer_id', 'private_key'];
|
|
24
|
+
export function validateIosCreds(creds) {
|
|
25
|
+
const missing = IOS_REQUIRED.filter((k) => !creds[k]);
|
|
26
|
+
if (missing.length) {
|
|
27
|
+
throw new CredsError(`iOS ASC 缺少凭证字段: ${missing.join(', ')}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/creds/schema.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,MAAM,CAAC,MAAM,gBAAgB,GAA6B;IACxD,MAAM,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC;IAChD,KAAK,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC;IAC/C,IAAI,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC;IAC9C,IAAI,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,eAAe,CAAC;IAC/C,MAAM,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,aAAa,CAAC;IAChD,OAAO,EAAE,CAAC,QAAQ,EAAE,iBAAiB,EAAE,aAAa,CAAC;IACrD,EAAE,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,eAAe,CAAC;IAC1C,KAAK,EAAE,CAAC,YAAY,EAAE,eAAe,CAAC;CACvC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;AAE9D,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,KAAmB;IACvE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,UAAU,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,UAAU,CAAC,MAAM,OAAO,YAAY,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;AAEnE,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,UAAU,CAAC,mBAAmB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// 各市场签名所需的原生加密原语(Node crypto)。
|
|
2
|
+
import { createHmac, createHash } from 'node:crypto';
|
|
3
|
+
export function hmacSha256Hex(key, data) {
|
|
4
|
+
return createHmac('sha256', key).update(data, 'utf8').digest('hex');
|
|
5
|
+
}
|
|
6
|
+
export function sha256Hex(data) {
|
|
7
|
+
return createHash('sha256').update(data, 'utf8').digest('hex');
|
|
8
|
+
}
|
|
9
|
+
export function md5Hex(data) {
|
|
10
|
+
return createHash('md5').update(data).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/infra/crypto.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,IAAY;IACrD,OAAO,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAyB;IAC9C,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// 轻量 HTTP 封装:原生 fetch + 超时(AbortSignal.timeout)+ 瞬时失败重试。
|
|
2
|
+
import { UploadError } from '../core/types.js';
|
|
3
|
+
/** 是否可重试:5xx / 429 限流 / 网络错误 / 超时(这些通常意味着请求未被服务端处理)。 */
|
|
4
|
+
export function isRetryable(e) {
|
|
5
|
+
if (e instanceof UploadError) {
|
|
6
|
+
const c = Number(e.code);
|
|
7
|
+
return c >= 500 || c === 429;
|
|
8
|
+
}
|
|
9
|
+
if (e instanceof Error) {
|
|
10
|
+
return (e.name === 'TimeoutError' ||
|
|
11
|
+
e.name === 'TypeError' ||
|
|
12
|
+
/fetch failed|ECONN|ETIMEDOUT|EAI_AGAIN|socket|network/i.test(e.message));
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 对瞬时失败做有限次指数退避重试(带 jitter);非可重试错误(4xx/签名/解析等)立即抛。
|
|
18
|
+
* 仅用于幂等或「失败即未生效」的请求;提交审核类非幂等操作慎用(默认 postForm/putBytes 仅在
|
|
19
|
+
* 5xx/429/网络/超时 时重试,这类失败一般未被服务端处理,重试安全)。
|
|
20
|
+
*/
|
|
21
|
+
export async function withRetry(fn, opts) {
|
|
22
|
+
const retries = opts?.retries ?? 2;
|
|
23
|
+
const baseMs = opts?.baseMs ?? 1000;
|
|
24
|
+
let lastErr;
|
|
25
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
26
|
+
try {
|
|
27
|
+
return await fn();
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
lastErr = e;
|
|
31
|
+
if (attempt === retries || !isRetryable(e))
|
|
32
|
+
throw e;
|
|
33
|
+
const delay = baseMs * 2 ** attempt + Math.floor(Math.random() * 250);
|
|
34
|
+
console.error(`[retry] ${opts?.label ?? 'http'} 第 ${attempt + 1}/${retries} 次失败,${delay}ms 后重试: ${e instanceof Error ? e.message : String(e)}`);
|
|
35
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw lastErr;
|
|
39
|
+
}
|
|
40
|
+
function hostOf(url) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(url).host;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return url.slice(0, 40);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* POST application/x-www-form-urlencoded,返回解析后的 JSON。
|
|
50
|
+
* 默认对瞬时失败重试;**非幂等的提交类调用应传 retries=0**(避免重复提交)。
|
|
51
|
+
*/
|
|
52
|
+
export async function postForm(url, params, timeoutMs, retries = 2) {
|
|
53
|
+
return withRetry(async () => {
|
|
54
|
+
const res = await fetch(url, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
57
|
+
body: new URLSearchParams(params).toString(),
|
|
58
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
59
|
+
});
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new UploadError(res.status, `http ${res.status}: ${text.slice(0, 500)}`);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(text);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new UploadError('parse', `非 JSON 响应: ${text.slice(0, 500)}`);
|
|
69
|
+
}
|
|
70
|
+
}, { label: `POST ${hostOf(url)}`, retries });
|
|
71
|
+
}
|
|
72
|
+
/** PUT 原始字节(如腾讯云 COS 预签名直传)。瞬时失败自动重试(重传整文件,幂等)。 */
|
|
73
|
+
export async function putBytes(url, bytes, timeoutMs, contentType = 'application/octet-stream') {
|
|
74
|
+
return withRetry(async () => {
|
|
75
|
+
const res = await fetch(url, {
|
|
76
|
+
method: 'PUT',
|
|
77
|
+
headers: { 'Content-Type': contentType },
|
|
78
|
+
body: bytes,
|
|
79
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const text = await res.text().catch(() => '');
|
|
83
|
+
throw new UploadError(res.status, `put 失败 ${res.status}: ${text.slice(0, 300)}`);
|
|
84
|
+
}
|
|
85
|
+
}, { label: `PUT ${hostOf(url)}` });
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/infra/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// 从 APK 提取启动图标(aapt 软依赖)。
|
|
2
|
+
// 取 aapt badging 里各密度的 application-icon-<dpi> 中**最高密度的 PNG/WEBP 栅格回退图**,
|
|
3
|
+
// 跳过自适应图标 XML(anydpi-v26 的 ic_launcher.xml)——这正是早先用 app-info-parser 取错图(喇叭图)的根因。
|
|
4
|
+
// 仅在「需要图标且未提供 --icon」时调用;找不到 aapt 时给出清晰错误,提示改用 --icon。
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import AdmZip from 'adm-zip';
|
|
11
|
+
import { InputError } from '../core/exit.js';
|
|
12
|
+
import { resizeBufferToPng } from './icon.js';
|
|
13
|
+
const execFileP = promisify(execFile);
|
|
14
|
+
/** 候选 aapt 可执行:PATH 上的 aapt/aapt2 + ANDROID_HOME/ANDROID_SDK_ROOT 下最新 build-tools。 */
|
|
15
|
+
function aaptCandidates() {
|
|
16
|
+
const list = ['aapt', 'aapt2'];
|
|
17
|
+
const home = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || '';
|
|
18
|
+
if (home) {
|
|
19
|
+
try {
|
|
20
|
+
const bt = join(home, 'build-tools');
|
|
21
|
+
for (const v of readdirSync(bt).sort().reverse()) {
|
|
22
|
+
for (const name of ['aapt', 'aapt2']) {
|
|
23
|
+
const p = join(bt, v, name);
|
|
24
|
+
if (existsSync(p))
|
|
25
|
+
list.push(p);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* build-tools 不存在则忽略 */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return list;
|
|
34
|
+
}
|
|
35
|
+
async function dumpBadging(apkPath) {
|
|
36
|
+
let lastErr;
|
|
37
|
+
for (const aapt of aaptCandidates()) {
|
|
38
|
+
try {
|
|
39
|
+
const { stdout } = await execFileP(aapt, ['dump', 'badging', apkPath], {
|
|
40
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
41
|
+
});
|
|
42
|
+
if (stdout.includes('application'))
|
|
43
|
+
return stdout;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
lastErr = e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw lastErr ?? new Error('aapt not found');
|
|
50
|
+
}
|
|
51
|
+
/** aapt 解析 APK 启动图标,返回最高密度 PNG/WEBP 字节。无 aapt 或仅自适应 XML 时抛 InputError。 */
|
|
52
|
+
export async function extractApkIconBytes(apkPath) {
|
|
53
|
+
let stdout;
|
|
54
|
+
try {
|
|
55
|
+
stdout = await dumpBadging(apkPath);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new InputError('未提供 --icon 且未找到 aapt(需 Android SDK build-tools / ANDROID_HOME):请用 --icon 指定图标,或在含 aapt 的环境运行');
|
|
59
|
+
}
|
|
60
|
+
const entries = [...stdout.matchAll(/application-icon-(\d+):'([^']+)'/g)]
|
|
61
|
+
.map((m) => ({ dpi: Number(m[1]), path: m[2] }))
|
|
62
|
+
.filter((e) => /\.(png|webp)$/i.test(e.path));
|
|
63
|
+
if (entries.length === 0) {
|
|
64
|
+
// 退路:application 行的 icon(部分包仅此处给 PNG)。
|
|
65
|
+
const m = stdout.match(/application:[^\n]*icon='([^']+\.(?:png|webp))'/i);
|
|
66
|
+
if (m)
|
|
67
|
+
entries.push({ dpi: 0, path: m[1] });
|
|
68
|
+
}
|
|
69
|
+
if (entries.length === 0) {
|
|
70
|
+
throw new InputError('aapt 未从 APK 解析到 PNG/WEBP 图标(可能仅含自适应 XML 图标):请用 --icon 指定');
|
|
71
|
+
}
|
|
72
|
+
entries.sort((a, b) => b.dpi - a.dpi);
|
|
73
|
+
const zip = new AdmZip(apkPath);
|
|
74
|
+
const entry = zip.getEntry(entries[0].path);
|
|
75
|
+
if (!entry)
|
|
76
|
+
throw new InputError(`APK 内未找到图标资源 ${entries[0].path}`);
|
|
77
|
+
return entry.getData();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 解析图标字节:优先 --icon 文件,否则用 aapt 从 APK 提取(最高密度 PNG),
|
|
81
|
+
* 再按渠道尺寸 + 大小上限缩放压缩。两条来源都没有(无 aapt 且无 --icon)则抛 InputError。
|
|
82
|
+
*/
|
|
83
|
+
export async function resolveIconBytes(opts) {
|
|
84
|
+
const input = opts.iconPath
|
|
85
|
+
? await readFile(opts.iconPath)
|
|
86
|
+
: await extractApkIconBytes(opts.apkPath);
|
|
87
|
+
return resizeBufferToPng(input, opts.size, opts.maxBytes);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=apk-icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apk-icon.js","sourceRoot":"","sources":["../../src/pkginfo/apk-icon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// 图标处理:把外部提供的图标文件缩放为各市场要求的尺寸(纯 JS,jimp,无原生依赖)。
|
|
2
|
+
// 注意:不再从 APK 自动提取图标(app-info-parser 对 adaptive icon/多密度不可靠,会取错图)。
|
|
3
|
+
// 图标一律由调用方通过 --icon 显式提供;调用方用 aapt 提取正确图标后传入。
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { Jimp } from 'jimp';
|
|
6
|
+
/** 解析 base64 data-uri 图标为字节。取不到返回 undefined。 */
|
|
7
|
+
export function decodeIconDataUri(dataUri) {
|
|
8
|
+
if (!dataUri)
|
|
9
|
+
return undefined;
|
|
10
|
+
const comma = dataUri.indexOf(',');
|
|
11
|
+
const b64 = comma >= 0 ? dataUri.slice(comma + 1) : dataUri;
|
|
12
|
+
if (!b64)
|
|
13
|
+
return undefined;
|
|
14
|
+
try {
|
|
15
|
+
return Buffer.from(b64, 'base64');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** 把图标字节缩放为 width×height(默认正方形)的 PNG 字节。 */
|
|
22
|
+
export async function resizeToPng(input, width, height = width) {
|
|
23
|
+
const img = await Jimp.read(input);
|
|
24
|
+
img.resize({ w: width, h: height });
|
|
25
|
+
return img.getBuffer('image/png');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 把图标字节缩放为 size×size 的 PNG(图标正方形)。传 maxBytes 时若超限则逐步降尺寸压缩
|
|
29
|
+
* (vivo ≤50KB、应用宝/荣耀 ≤200KB 等较严的渠道用)。供 --icon 文件与 APK 提取的图标共用。
|
|
30
|
+
*/
|
|
31
|
+
export async function resizeBufferToPng(input, size, maxBytes) {
|
|
32
|
+
const encode = async (d) => {
|
|
33
|
+
const img = await Jimp.read(input);
|
|
34
|
+
img.resize({ w: d, h: d });
|
|
35
|
+
return img.getBuffer('image/png');
|
|
36
|
+
};
|
|
37
|
+
let buf = await encode(size);
|
|
38
|
+
if (maxBytes && buf.length > maxBytes) {
|
|
39
|
+
// 图标多为扁平设计,缩小边长后 PNG 体积明显下降;降到 96px 仍超则用最小结果并告警。
|
|
40
|
+
let scale = 0.85;
|
|
41
|
+
while (buf.length > maxBytes && Math.round(size * scale) >= 96) {
|
|
42
|
+
buf = await encode(Math.round(size * scale));
|
|
43
|
+
scale -= 0.15;
|
|
44
|
+
}
|
|
45
|
+
if (buf.length > maxBytes) {
|
|
46
|
+
console.error(`[warn] 图标压缩后仍 ${buf.length} 字节,超过上限 ${maxBytes};建议提供更简单/更小的图标`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return buf;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 读取本地图标文件并缩放为 width×width 的 PNG(图标正方形,height 仅兼容旧签名)。
|
|
53
|
+
* 各市场尺寸:华为 216;荣耀及其余 512。压缩见 resizeBufferToPng。
|
|
54
|
+
*/
|
|
55
|
+
export async function resizeIconFileToPng(path, width, _height = width, maxBytes) {
|
|
56
|
+
const input = await readFile(path);
|
|
57
|
+
return resizeBufferToPng(input, width, maxBytes);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 读取本地截图文件并按渠道首选格式编码(jpg/png)。
|
|
61
|
+
* 不强制改尺寸(截图比例/内容需保留);仅在超过 maxBytes 时对 jpg 逐步降质量压缩。
|
|
62
|
+
*/
|
|
63
|
+
export async function prepareScreenshot(path, format, maxBytes) {
|
|
64
|
+
const img = await Jimp.read(path);
|
|
65
|
+
if (format === 'jpg') {
|
|
66
|
+
let quality = 92;
|
|
67
|
+
let buf = await img.getBuffer('image/jpeg', { quality });
|
|
68
|
+
while (maxBytes && buf.length > maxBytes && quality > 40) {
|
|
69
|
+
quality -= 15;
|
|
70
|
+
buf = await img.getBuffer('image/jpeg', { quality });
|
|
71
|
+
}
|
|
72
|
+
if (maxBytes && buf.length > maxBytes) {
|
|
73
|
+
console.error(`[warn] 截图 ${path} 压缩后仍超过上限 ${maxBytes} 字节`);
|
|
74
|
+
}
|
|
75
|
+
return buf;
|
|
76
|
+
}
|
|
77
|
+
const buf = await img.getBuffer('image/png');
|
|
78
|
+
if (maxBytes && buf.length > maxBytes) {
|
|
79
|
+
console.error(`[warn] 截图 ${path} 为 ${buf.length} 字节,超过 PNG 上限 ${maxBytes};建议改用 jpg 或压缩`);
|
|
80
|
+
}
|
|
81
|
+
return buf;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icon.js","sourceRoot":"","sources":["../../src/pkginfo/icon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// 包元数据:用 app-info-parser 解析 APK/IPA(含 icon),用 adm-zip 读 ABI 判断 32 位。
|
|
2
|
+
// 去掉对 aapt / ANDROID_HOME 的依赖。
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import AdmZip from 'adm-zip';
|
|
6
|
+
import { InputError } from '../core/exit.js';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
export async function parsePackage(path) {
|
|
9
|
+
if (!existsSync(path))
|
|
10
|
+
throw new InputError(`包文件不存在: ${path}`);
|
|
11
|
+
const AppInfoParser = require('app-info-parser');
|
|
12
|
+
const r = await new AppInfoParser(path).parse();
|
|
13
|
+
return {
|
|
14
|
+
packageName: String(r.package ?? r.packageName ?? r.CFBundleIdentifier ?? ''),
|
|
15
|
+
versionName: String(r.versionName ?? r.CFBundleShortVersionString ?? ''),
|
|
16
|
+
versionCode: String(r.versionCode ?? r.CFBundleVersion ?? ''),
|
|
17
|
+
icon: typeof r.icon === 'string' ? r.icon : undefined,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const ABI_32 = new Set(['armeabi', 'armeabi-v7a', 'x86', 'mips']);
|
|
21
|
+
/** apk 内是否含 32 位 so(应用宝 apk32/apk64 槽位判定)。无 native so 时按 64 位。 */
|
|
22
|
+
export function detectCompat32(apkPath) {
|
|
23
|
+
if (!existsSync(apkPath))
|
|
24
|
+
throw new InputError(`包文件不存在: ${apkPath}`);
|
|
25
|
+
const zip = new AdmZip(apkPath);
|
|
26
|
+
for (const e of zip.getEntries()) {
|
|
27
|
+
const m = /^lib\/([^/]+)\//.exec(e.entryName);
|
|
28
|
+
if (m && m[1] && ABI_32.has(m[1]))
|
|
29
|
+
return true; // 含 32 位 ABI
|
|
30
|
+
}
|
|
31
|
+
return false; // 无 32 位 ABI(含「有 native 但仅 64 位」与「无 native」)→ 按 64 位
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pkginfo/index.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,+BAA+B;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAS/C,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;IAC/D,MAAM,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjD,MAAM,CAAC,GAAQ,MAAM,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;IACrD,OAAO;QACL,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,kBAAkB,IAAI,EAAE,CAAC;QAC7E,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,0BAA0B,IAAI,EAAE,CAAC;QACxE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,eAAe,IAAI,EAAE,CAAC;QAC7D,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;KACtD,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;AAElE,kEAAkE;AAClE,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,WAAW,OAAO,EAAE,CAAC,CAAC;IACrE,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,aAAa;IAC/D,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,qDAAqD;AAErE,CAAC"}
|