KAS Submit — design
状态:设计。Submitter interface + 4 个 driver 骨架在
keel/cloud/internal/submit/。每个 driver 的真实 API 接入是后续工作。
Submit 是 Keel 5 层架构里对位 EAS Submit 的那层:拿到一个构 建好的 ipa / apk 之后,自动提交到应用市场审核。
EAS Submit 只对接 Apple App Store / Google Play Store。Keel Submit 当前的目标:
| 市场 | 路径 | 状态 |
|---|---|---|
| Apple App Store (iOS) | App Store Connect API + fastlane | 已知方案,复用 fastlane |
| 华为 AppGallery | AGC Connect Publish API | 完整 REST,hand-roll |
| OPPO 开放平台 | 完整 REST | hand-roll |
| vivo 开放平台 | 完整 REST | hand-roll |
| 小米应用商店 | 内部 API 需申请 → fallback browser-assisted | 混合 |
| 腾讯应用宝 | Hub API 部分功能 | 混合 |
5 个国内 Android 市场(华为 AGC / 小米 / OPPO / vivo / 应用宝)+ iOS App Store + Google Play Store——7 个 target。每个一个 driver。
1. 用户视角
# 凭据通过 ~/.keel/credentials.yml 或 CI secret 配置——见 §4
# (当前没有 `keel submit configure` 引导命令;直接编辑 yaml)
# 单一市场提审
keel submit ios \
--to=ios_appstore \
--build=builds/myapp-1.2.3.ipa \
--app-id=com.mycorp.app \
--version=1.2.3 \
--build-number=42
# 多市场并发提审(fan-out)
keel submit android \
--to=huawei,oppo,vivo,xiaomi,yyb \
--build=builds/myapp-1.2.3.apk \
--app-id=com.mycorp.app \
--version=1.2.3 \
--build-number=42 \
--release-notes='1.2.3 修复登录闪退'
CLI 输出每个市场的 SubmissionID + 当前 Status。状态包括:
uploaded / under_review / approved / rejected{reason} /
published。
2. 用户必须做的前置工作(Keel 帮不了)
每个市场都需要用户先搞定(每家 1-3 天 + 部分要 30 天软著):
- 企业开发者账号:vivo / OPPO / 应用宝 / 小米必须企业; 华为接受个人但有上架限制。
- 资质审核 + 协议签:每家走一次实名 + 资质上传 + 协议签字。
- 软件著作权(软著):5 国内市场都要软著编号才能上架。中国 版权保护中心申请,~30 天审核期,¥300-600/份。
- 创建应用条目:每家市场后台新建一个应用,填基本信息(名称、 logo、描述、不同尺寸截图)。Keel 不能代你做这些(涉及商标 + 法 律内容)。
- 凭据获取:开发者后台生成 AppID + Client/AccessKey / OAuth
secret,存进
~/.keel/submit/credentials.yml或 CI secret。
KAS Submit 帮的是上述都完成后,一键传 ipa/apk + 提审 + 拉状态 这一步。
3. 架构
keel submit android --to=huawei,oppo,vivo,xiaomi
│
▼
┌───────────────────────┐
│ submit.Coordinator │ 并发 fan-out + 收集
└──────┬────────────────┘
│
┌────────┬───────┼───────┬──────────┬────────┐
▼ ▼ ▼ ▼ ▼ ▼
huawei oppo vivo xiaomi tencent_yyb ios_appstore
driver driver driver driver driver driver
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
REST API + 每家凭据 + 每家审核 SLA + 各自 retry / poll loop
每个 driver 是独立的 Go package,实现 submit.Submitter interface:
// keel/cloud/internal/submit/submit.go
type Submitter interface {
Name() string
RequiresCredentials() []string // 文档化每个 driver 要的 key
Submit(ctx, target SubmissionInput) (SubmissionID, error)
Status(ctx, sid SubmissionID) (SubmissionStatus, error)
}
Coordinator 不知道 driver 实现细节——只通过 interface 调用、按 namespace+driver_name 拿对应凭据、并发 fan-out。
Optional capabilities: drivers can implement Cancellable (single
method Cancel(ctx, id) error) — Apple App Store + Google Play
support it via their APIs; the 5 国内 markets are portal-only and do
NOT implement Cancellable. Coordinator type-asserts at call time;
unsupported markets return ErrCancelNotSupported.
4. 凭据存储
本地 dev:~/.keel/credentials.yml,权限 0600,每 driver 一个
顶级 key、内部字段名直接对应代码里的 Cred* 常量。
ios_appstore:
issuer_id: "1234abcd-..."
key_id: "ABC123"
p8_path: "/path/to/AuthKey_ABC123.p8"
bundle_id: "com.mycorp.app"
android_googleplay:
service_account_json: "/path/to/gcp-svc-account.json"
package_name: "com.mycorp.app"
huawei:
app_id: "100000123"
client_id: "abcdef123456"
client_secret: "<long-string>"
project_id: "987654321"
xiaomi:
app_id: "20000000"
account: "you@mycorp.com"
private_key_pem: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
xiaomi_public_pem: |
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
oppo:
app_id: "30000000"
api_key: "<key>"
api_secret: "<secret>"
vivo:
app_id: "40000000"
access_key: "<key>"
secret_key: "<secret>"
yyb:
mode: "hub"
app_id: "50000000"
hub_access_key: "<key>"
hub_secret: "<secret>"
CI:每个市场一组 GitHub Secrets / aliyun ACR secrets,名字按
<DRIVER>_<FIELD> 大写化(e.g. IOS_APPSTORE_ISSUER_ID,
ANDROID_GOOGLEPLAY_PACKAGE_NAME)。keel-submit 二进制读
--credentials=<path> flag;CI 通常生成临时 yaml 后传入。
完整字段索引见 §11 Driver 凭据完整 schema。
5. 每个 driver 的实现路径
5.1 华为 AppGallery Connect
- OAuth 2.0 client_credentials grant →
access_token - POST
/api/oauth2/v1/token拿 token - POST
/agc/api/v1/publish/upload-url拿 OBS 上传 URL - PUT apk 到 OBS
- POST
/agc/api/v1/publish/upload-result报上传完成 - POST
/agc/api/v1/publish/submit提交审核 - GET
/agc/api/v1/publish/audit-info拉状态
文档:https://developer.huawei.com/consumer/cn/agconnect/
5.2 OPPO 开放平台
- 通过
developers.oppomobile.com拿 access_token - POST
/api/publish/apk/upload直接上传 apk - POST
/api/publish/apk/submit提审 - GET
/api/publish/apk/info拉状态
文档:https://open.oppomobile.com/wiki/
5.3 vivo 开放平台
- 走 https://dev.vivo.com.cn 的 API,签名走 HMAC-SHA256
- 类似 OPPO 的 upload + submit + status 三段式
5.4 小米
两条路径:
- Path A(有 API 权限):申请「应用商店内部 API」权限后,POST
/dev_open_api/upload_apk+ submit + status。审批耗时 1-2 周。 - Path B(无权限):driver 启动一个本地 Playwright session,登录 小米开发者后台 → 跳到上传页 → 预填表单字段 → 提示用户手动 点击”提交”。不爬虫,不绕过 ToS——只是”打开浏览器替你 填好”。
Path A 是首选;Path B 作为 fallback 让 KAS Submit 首次用就能跑。
5.5 腾讯应用宝(YYB)
- 应用宝 Hub API 文档零散,部分操作走 https://hub.tencent.com
- 同小米:能 API 走 API,不能就 browser-assisted
5.6 Apple App Store (iOS)
复用 fastlane:
- driver 内部 spawn
fastlane deliver --verbose子进程 - 通过 stdin 喂凭据(App Store Connect API key 的 issuer_id + key_id + p8 file path)
- 解析 fastlane stdout 拿 SubmissionID
- 后续状态拉取走 App Store Connect API(fastlane 不擅长这步)
fastlane 已被 Apple 自家工具 + Xcode Cloud 半官方化,稳定性最高。 hand-roll App Store Connect API 不值得。
6. 错误模式 + 重试策略
| 错误类型 | driver 行为 |
|---|---|
| 401 token 过期 | 自动 refresh access_token,重试一次 |
| 429 限流 | 退避 + jitter,最多 3 次 |
| 5xx | 退避 + jitter,最多 5 次 |
| 4xx(业务错误:图标尺寸错、描述违规等) | 不重试,把错误透传给用户 |
| 网络错误 | 退避 + jitter,最多 3 次 |
| 上传中断 | 部分市场支持断点续传(华为 OBS);其他重传整个 apk |
7. 状态轮询
Submit 是异步的(市场审核 1-7 天)。状态轮询有两种模式:
keel submit watch <build-id>:阻塞直到所有市场都终态 (approved / rejected / published)。CI 中常用——CI job 等 Submit 完成再发 Slack 通知。keel submit status <build-id>:单次查询所有市场,立即 返回当前状态。Dashboard 用。
driver 内部最小 poll interval:10 分钟(市场都是人工审核, sub-minute 轮询白费配额)。
8. CLI 命令清单
当前实装单条 fan-out 命令,没有独立的 configure / status / watch / list / show 子命令——凭据走 yaml 文件,状态走 driver Status() API
(CLI 后续封装)。
keel submit <platform> \
--to=<csv> \
--build=<path> \
--app-id=<id> \
--version=<x.y.z> \
--build-number=<n> \
[--release-notes=<text>] \
[--credentials=<path>]
参数:
| flag | 必填 | 说明 |
|---|---|---|
<platform> | ✓ | ios 或 android(positional) |
--to | ✓ | 逗号分隔的 driver 名:ios_appstore / android_googleplay / huawei / xiaomi / oppo / vivo / yyb |
--build | ✓ | 本地 .ipa / .apk / .aab 路径 |
--app-id | ✓ | iOS bundle id 或 Android package name |
--version | ✓ | 用户可见版本号,e.g. 1.2.3 |
--build-number | ✓ | monotonic build counter(CFBundleVersion / versionCode) |
--release-notes | 发布说明(UTF-8) | |
--credentials | credentials.yml 路径(默认 ~/.keel/credentials.yml) |
输出每个 driver 一行:<driver> <Status> <SubmissionID>。后续状态查询 +
watch loop 走 driver Status() API;keel submit status CLI
包装规划中。
9. 范围
当前范围(this design doc 的目标范围):
- Submitter interface + Coordinator 骨架
- 凭据存储(本地 yaml + CI env)
- 7 个 driver 占位(实现签到 + Submit() 报 ErrNotImplemented)
- CLI 命令 stub(接受 flags,调 Go 后端)
接下来(真正实现 driver):
- 华为(最完整的 API,先做)
- OPPO + vivo(同形,复用 patterns)
- Apple App Store via fastlane
- 小米 Path B(browser-assisted)
- 应用宝混合
后续规划:
- 小米 Path A(API 权限申请通过后)
- 应用宝 API 全功能
- 各市场的”灰度发布”配置(不是首次上架,而是版本更新策略)
10. 不在范围内
- 审核失败自动改 + 重提——审核驳回时 Keel 把驳回原因展示给 用户,让用户自己改。不根据 AI 改图标 / 改描述(那是另一 个产品)。
- 应用条目首次创建——必须用户在每家市场后台手工建。Keel 只 做”已有条目的版本提交”。
- 跨市场凭据共享——每家市场凭据隔离,driver 间不共享。
- 审核加速——某些市场(华为)支持加急审核,需付费。规划中。
11. Driver 凭据完整 schema
每 driver 的 RequiresCredentials() 返回的 Cred* 常量索引——按
driver 拆表,列出字段名、来源、是否必填、加密敏感度。Coordinator
在 Submit() 之前会对每个声明的 key 做 preflight 检查,缺字段就直接
返回 ErrMissingCredential 不走网络。
11.1 ios_appstore — Apple App Store Connect
| 字段 | 来源 | 必填 | 敏感 | 说明 |
|---|---|---|---|---|
issuer_id | App Store Connect → 用户和访问 → 密钥 → API Issuer ID | ✓ | 中 | UUID 格式,所有 API 调用的发行方 |
key_id | 同上,单个 API Key 的 ID(10 字符) | ✓ | 中 | 跟 p8_path 配对 |
p8_path | 同上下载的 AuthKey_<KEY_ID>.p8 本地路径 | ✓ | 高 | ECDSA P-256 私钥;用于 ES256 JWT 签名 |
bundle_id | App Store Connect 上 App 的 Bundle Identifier | ✓ | 低 | e.g. com.mycorp.app |
Submit() flow: 用 p8 私钥 mint ES256 JWT(20 min TTL) → POST /v1/betaAppReviewSubmissions 拿 submission UUID。binary 上传走
xcrun altool(仅 macOS host)或 caller 预上传后传 Metadata [build_id]。
11.2 android_googleplay — Google Play Console
| 字段 | 来源 | 必填 | 敏感 | 说明 |
|---|---|---|---|---|
service_account_json | GCP Console → IAM → Service Accounts,导出 JSON 本地路径 | ✓ | 高 | 内含 RSA private_key + client_email + token_uri |
package_name | Play Console 上 App 的 applicationId | ✓ | 低 | e.g. com.mycorp.app |
服务账号必须授予 Play Console 项目的 Service Account User + Release
Manager 角色。Submit() 走 OAuth2 jwt-bearer 换 access_token → 发
edit → 上传 .aab → 绑定到 track → commit。
可选 Metadata 字段(不在 Cred* 但影响行为):
track—internal(default) /alpha/beta/production
11.3 huawei — 华为 AppGallery Connect
| 字段 | 来源 | 必填 | 敏感 | 说明 |
|---|---|---|---|---|
app_id | AGC Connect → App 详情卡上的 “app_id” 数字 | ✓ | 低 | 例 100000123 |
client_id | AGC Connect → 我的项目 → 凭据 → API client ID | ✓ | 中 | OAuth 2.0 client_credentials grant |
client_secret | 同上,紧邻 client_id 的 secret | ✓ | 高 | |
project_id | AGC Connect 项目级 ID(不同于 app_id) | ✓ | 低 | 多 App 同 project 时显式区分 |
5 步 pipeline:oauth/token → upload-url → PUT OBS → upload-result →
app-submit。结果是 releaseId。
11.4 xiaomi — 小米应用商店
| 字段 | 来源 | 必填 | 敏感 | 说明 |
|---|---|---|---|---|
app_id | 小米开放平台 → App 详情 → “app_id” | ✓ | 低 | |
account | 开发者账号邮箱 | ✓ | 低 | 用作请求里的 UserName |
private_key_pem | 你自己的 RSA 私钥(PKCS#1 或 PKCS#8 PEM) | ✓ | 高 | 你生成 keypair 后把公钥提交给小米后台备案;私钥本地保留 |
xiaomi_public_pem | 小米后台下载的小米平台公钥(SPKI PEM) | ✓ | 低 | 用来 RSA-encrypt 你 per-request 生成的 AES-128 key |
加密混合:每请求生成临时 AES-128 key → AES-CBC 加 body → RSA-encrypt
AES key 放 SecretKey 字段。多上传 1 次会产生新的 AES key(forward
secrecy)。
11.5 oppo — OPPO 开放平台 / Heytap
| 字段 | 来源 | 必填 | 敏感 | 说明 |
|---|---|---|---|---|
app_id | OPPO 开放平台 → App 详情 | ✓ | 低 | |
api_key | 开放平台 → 凭据 → API Key | ✓ | 中 | 用作请求 query 参数 |
api_secret | 同上的 API Secret | ✓ | 高 | HMAC-SHA1 签名 key |
签名:canonical k=v&k=v 排序串 → HMAC-SHA1(secret, canonical) → 接
&sign=<hex>。
11.6 vivo — vivo 开放平台
| 字段 | 来源 | 必填 | 敏感 | 说明 |
|---|---|---|---|---|
app_id | vivo 开放平台 → App 详情 | ✓ | 低 | 用作 target_app_key |
access_key | 开放平台 → 凭据 → AccessKey | ✓ | 中 | |
secret_key | 同上的 SecretKey | ✓ | 高 | HMAC-SHA256 签名 key |
签名:与 OPPO 类似的 canonical 排序串,但 HMAC-SHA256(更新)。
11.7 yyb — 腾讯应用宝
| 字段 | 来源 | 必填 | 敏感 | 说明 |
|---|---|---|---|---|
mode | 字面 "hub" 或 "browser" | ✓ | 低 | 当前仅 hub 实装;browser 走 Playwright 留待后续 |
app_id | 腾讯应用宝开放平台 → App 详情 | ✓ | 低 | |
hub_access_key | Hub API 凭据 → AccessKey | hub 模式必填 | 中 | |
hub_secret | 同上的 Secret | hub 模式必填 | 高 | HMAC-SHA256 签名 key |
cookie_file | Playwright session cookies 导出文件路径 | browser 模式必填 | 高 | 后续才用 |
Hub API 二步:upload-apk 拿 upload_id → audit-submit 拿
audit_id。
字段类型 + 敏感度公约
- “敏感”分三档:低=可写入 README 例子;中=别写 README 但提交进 GitHub Issues OK;高=只能本地 / OS keyring / CI Secret,绝 不要 commit / 不要打日志。
- 所有”高”敏感字段在
keel-submit启动时会校验文件权限,发现 pem / json 文件 mode >0600时打 warn(后续改 hard-fail)。 - Coordinator preflight 失败的错误信息只暴露 driver name + key name,不打 key 值——防止 log scraper 顺手把凭据捞走。
凭据轮换
| Driver | 轮换难度 | 备注 |
|---|---|---|
| ios_appstore | 中 | 创建新 API Key + 改 p8_path;旧 key 可 revoke |
| android_googleplay | 中 | 在 GCP 创建新 service account;旧 SA 可禁用 |
| huawei | 易 | AGC 上 reissue secret;旧 secret 自动失效 |
| xiaomi | 难 | 改 RSA keypair 需提交新公钥等小米后台审核(人工,工作日 ≤ 24h) |
| oppo / vivo / yyb | 易 | 后台一键 reissue |
凭据丢失 / 怀疑泄漏的应急流程:立即从 ~/.keel/credentials.yml 删除受影响 driver;登录该市场后台 revoke 旧凭据 + 签发新一组;commit keel submit configure 重新写入;轮换完成后 24h 内审计本周所有 submit 任务的来源 IP
(规划中;现在按各 driver 文档手动操作)。