Machine-translated draft — terminology + flow still being reviewed.

KAS Submit — design

状态:设计。Submitter interface + 4 个 driver 骨架在 keel/cloud/internal/submit/。每个 driver 的真实 API 接入是后续工作。

Submit 是 Keel 5 层架构里对位 EAS Submit 的那层:拿到一个构 建好的 ipa / apk 之后,自动submit到应用marketreview

EAS Submit 只对接 Apple App Store / Google Play Store。Keel Submit 当前的目标:

marketpath状态
Apple App Store (iOS)App Store Connect API + fastlane已知方案,复用 fastlane
Huawei AppGalleryAGC Connect Publish API完整 REST,hand-roll
OPPO 开放平台完整 RESThand-roll
vivo 开放平台完整 RESThand-roll
Xiaomiapp storeinternal API 需申请 → fallback browser-assisted混合
腾讯Tencent MyAppHub API 部分feature混合

5 个domestic / mainland China Android market(Huawei AGC / Xiaomi / OPPO / vivo / Tencent MyApp)+ iOS App Store + Google Play Store——7 个 target。每个一个 driver。


1. user视角

# 凭据通过 ~/.keel/credentials.yml 或 CI secret configuration——见 §4
# (当前没有 `keel submit configure` 引导命令;直接编辑 yaml)

# 单一marketsubmit for review
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

# 多marketconcurrencysubmit for review(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 修复login闪退'

CLI 输出每个market的 SubmissionID + 当前 Status。状态包括: uploaded / under_review / approved / rejected{reason} / published


2. user必须做的前置工作(Keel 帮不了)

每个market都需要user搞定(每家 1-3 天 + 部分要 30 天software copyright):

  1. enterprisedeveloper账号:vivo / OPPO / Tencent MyApp / Xiaomi必须enterprise; Huawei接受个人但有上架限制。
  2. 资质review + protocol签:每家走一次实名 + 资质上传 + protocol签字。
  3. software copyright(software copyright):5 domestic / mainland Chinamarket都要software copyright编号才能上架。中国 版权保护中心申请,~30 天review期,¥300-600/份。
  4. 创建应用条目:每家marketbackend新建一个应用,填基本信息(名称、 logo、描述、不同尺寸截图)。Keel 不能代你做这些(涉及商标 + 法 律内容)。
  5. 凭据获取:developerbackend生成 AppID + Client/AccessKey / OAuth secret,存进 ~/.keel/submit/credentials.yml 或 CI secret。

KAS Submit 帮的是上述都完成后,一键传 ipa/apk + submit for review + 拉状态 这一步。


3. 架构

                  keel submit android --to=huawei,oppo,vivo,xiaomi


                  ┌───────────────────────┐
                  │   submit.Coordinator  │  concurrency fan-out + 收集
                  └──────┬────────────────┘

        ┌────────┬───────┼───────┬──────────┬────────┐
        ▼        ▼       ▼       ▼          ▼        ▼
     huawei   oppo    vivo   xiaomi   tencent_yyb  ios_appstore
     driver  driver  driver  driver    driver       driver
        │       │       │       │          │           │
        ▼       ▼       ▼       ▼          ▼           ▼
   REST API + 每家凭据 + 每家review SLA + 各自 retry / poll loop

每个 driver 是独立的 Go package,实现 submit.Submitter interface:

// keel/cloud/internal/submit/submit.go
type Submitter interface {
    Name() string
    RequiresCredentials() []string  // documentation化每个 driver 要的 key
    Submit(ctx, target SubmissionInput) (SubmissionID, error)
    Status(ctx, sid SubmissionID) (SubmissionStatus, error)
}

Coordinator 不知道 driver 实现细节——只通过 interface 调用、按 namespace+driver_name 拿对应凭据、concurrency 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 domestic / mainland China markets are portal-only and do NOT implement Cancellable. Coordinator type-asserts at call time; unsupported markets return ErrCancelNotSupported.


4. 凭据存储

本地 dev~/.keel/credentials.yml,permission 0600,每 driver 一个 顶级 key、internal字段名直接对应code里的 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:每个market一组 GitHub Secrets / aliyun ACR secrets,名字按 <DRIVER>_<FIELD> 大写化(e.g. IOS_APPSTORE_ISSUER_IDANDROID_GOOGLEPLAY_PACKAGE_NAME)。keel-submit 二进制读 --credentials=<path> flag;CI 通常生成临时 yaml 后传入。

完整字段索引见 §11 Driver 凭据完整 schema


5. 每个 driver 的实现path

5.1 Huawei 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 submitreview
  • GET /agc/api/v1/publish/audit-info 拉状态

documentation: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 submit for review
  • GET /api/publish/apk/info 拉状态

documentation:https://open.oppomobile.com/wiki/

5.3 vivo 开放平台

5.4 Xiaomi

两条path

  • Path A(有 API permission):申请「app storeinternal API」permission后,POST /dev_open_api/upload_apk + submit + status。审批耗时 1-2 周。
  • Path B(无permission):driver 启动一个本地 Playwright session,login Xiaomideveloperbackend → 跳到上传页 → 预填表单字段 → 提示user手动 点击”submit”不爬虫,不绕过 ToS——只是”打开浏览器替你 填好”。

Path A 是首选;Path B 作为 fallback 让 KAS Submit 首次用就能跑。

5.5 腾讯Tencent MyApp(YYB)

  • Tencent MyApp Hub API documentation零散,部分操作走 https://hub.tencent.com
  • 同Xiaomi:能 API 走 API,不能就 browser-assisted

5.6 Apple App Store (iOS)

复用 fastlane:

  • driver internal spawn fastlane deliver --verbose 子进程
  • 通过 stdin 喂凭据(App Store Connect API key 的 issuer_id + key_id + p8 file path)
  • 解析 fastlane stdout 拿 SubmissionID
  • 后续状态pull走 App Store Connect API(fastlane 不擅长这步)

fastlane 已被 Apple 自家工具 + Xcode Cloud 半官方化,稳定性最高。 hand-roll App Store Connect API 不值得。


6. 错误模式 + 重试策略

错误类型driver 行为
401 token 过期自动 refresh access_token,重试一次
429 rate limit退避 + jitter,最多 3 次
5xx退避 + jitter,最多 5 次
4xx(业务错误:图标尺寸错、描述违规等)不重试,把错误透传给user
网络错误退避 + jitter,最多 3 次
上传中断部分market支持断点续传(Huawei OBS);其他重传整个 apk

7. 状态轮询

Submit 是异步的(marketreview 1-7 天)。状态轮询有两种模式:

  • keel submit watch <build-id>:阻塞直到所有market都终态 (approved / rejected / published)。CI 中常用——CI job 等 Submit 完成再发 Slack 通知。
  • keel submit status <build-id>:单次查询所有market,立即 返回当前状态。Dashboard 用。

driver internal最小 poll interval:10 分钟(market都是人工review, sub-minute 轮询白费配额)。


8. CLI 命令清单

当前实装单条 fan-out 命令,没有独立的 configure / status / watch / list / show 子命令——凭据走 yaml file,状态走 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>iosandroid(positional)
--to逗号分隔的 driver 名:ios_appstore / android_googleplay / huawei / xiaomi / oppo / vivo / yyb
--build本地 .ipa / .apk / .aab path
--app-idiOS bundle id 或 Android package name
--versionuser可见version号,e.g. 1.2.3
--build-numbermonotonic build counter(CFBundleVersion / versionCode)
--release-notespublish说明(UTF-8)
--credentialscredentials.yml path(默认 ~/.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):

  • Huawei(最完整的 API,先做)
  • OPPO + vivo(同形,复用 patterns)
  • Apple App Store via fastlane
  • Xiaomi Path B(browser-assisted)
  • Tencent MyApp混合

后续规划:

  • Xiaomi Path A(API permission申请通过后)
  • Tencent MyApp API 全feature
  • 各market的”灰度publish”configuration(不是首次上架,而是versionupdate策略)

10. 不在范围内

  • reviewfailed自动改 + 重提——review驳回时 Keel 把驳回原因展示给 user,让user自己改。根据 AI 改图标 / 改描述(那是另一 个产品)。
  • 应用条目首次创建——必须user在每家marketbackend手工建。Keel 只 做”已有条目的versionsubmit”。
  • 跨market凭据共享——每家market凭据隔离,driver 间不共享。
  • review加速——某些market(Huawei)支持加急review,需paid。规划中。

11. Driver 凭据完整 schema

每 driver 的 RequiresCredentials() 返回的 Cred* 常量索引——按 driver 拆表,列出字段名、来源、是否必填、加密敏感度。Coordinator 在 Submit() 之前会对每个声明的 key 做 preflight 检查,缺字段就直接 返回 ErrMissingCredential 不走网络。

11.1 ios_appstore — Apple App Store Connect

字段来源必填敏感说明
issuer_idApp Store Connect → user和访问 → 密钥 → API Issuer IDUUID 格式,所有 API 调用的发行方
key_id同上,单个 API Key 的 ID(10 字符)跟 p8_path 配对
p8_path同上下载的 AuthKey_<KEY_ID>.p8 本地pathECDSA P-256 私钥;用于 ES256 JWT 签名
bundle_idApp Store Connect 上 App 的 Bundle Identifiere.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_jsonGCP Console → IAM → Service Accounts,导出 JSON 本地path内含 RSA private_key + client_email + token_uri
package_namePlay Console 上 App 的 applicationIde.g. com.mycorp.app

service账号必须授予 Play Console 项目的 Service Account User + Release Manager 角色。Submit() 走 OAuth2 jwt-bearer 换 access_token → 发 edit → 上传 .aab → 绑定到 track → commit。

可选 Metadata 字段(不在 Cred* 但影响行为):

  • trackinternal (default) / alpha / beta / production

11.3 huawei — Huawei AppGallery Connect

字段来源必填敏感说明
app_idAGC Connect → App 详情卡上的 “app_id” 数字100000123
client_idAGC Connect → 我的项目 → 凭据 → API client IDOAuth 2.0 client_credentials grant
client_secret同上,紧邻 client_id 的 secret
project_idAGC Connect 项目级 ID(不同于 app_id)多 App 同 project 时显式区分

5 步 pipeline:oauth/token → upload-url → PUT OBS → upload-result → app-submit。结果是 releaseId

11.4 xiaomi — Xiaomiapp store

字段来源必填敏感说明
app_idXiaomi开放平台 → App 详情 → “app_id”
accountdeveloper账号邮箱用作request里的 UserName
private_key_pem自己的 RSA 私钥(PKCS#1 或 PKCS#8 PEM)你生成 keypair 后把公钥submit给XiaomibackendICP filing;私钥本地保留
xiaomi_public_pemXiaomibackend下载的Xiaomi平台公钥(SPKI PEM)用来 RSA-encrypt 你 per-request 生成的 AES-128 key

加密混合:每request生成临时 AES-128 key → AES-CBC 加 body → RSA-encrypt AES key 放 SecretKey 字段。多上传 1 次会产生新的 AES key(forward secrecy)。

11.5 oppo — OPPO 开放平台 / Heytap

字段来源必填敏感说明
app_idOPPO 开放平台 → App 详情
api_key开放平台 → 凭据 → API Key用作request query 参数
api_secret同上的 API SecretHMAC-SHA1 签名 key

签名:canonical k=v&k=v sort串 → HMAC-SHA1(secret, canonical) → 接 &sign=<hex>

11.6 vivo — vivo 开放平台

字段来源必填敏感说明
app_idvivo 开放平台 → App 详情用作 target_app_key
access_key开放平台 → 凭据 → AccessKey
secret_key同上的 SecretKeyHMAC-SHA256 签名 key

签名:与 OPPO 类似的 canonical sort串,但 HMAC-SHA256(update)。

11.7 yyb — 腾讯Tencent MyApp

字段来源必填敏感说明
mode字面 "hub""browser"当前仅 hub 实装;browser 走 Playwright 留待后续
app_id腾讯Tencent MyApp开放平台 → App 详情
hub_access_keyHub API 凭据 → AccessKeyhub 模式必填
hub_secret同上的 Secrethub 模式必填HMAC-SHA256 签名 key
cookie_filePlaywright session cookies 导出filepathbrowser 模式必填后续才用

Hub API 二步:upload-apk 拿 upload_id → audit-submit 拿 audit_id

字段类型 + 敏感度公约

  • “敏感”分三档:低=可写入 README 例子;中=别写 README 但submit进 GitHub Issues OK;=只能本地 / OS keyring / CI Secret,绝 不要 commit / 不要打日志
  • 所有”高”敏感字段在 keel-submit 启动时会校验filepermission,发现 pem / json file mode > 0600 时打 warn(后续改 hard-fail)。
  • Coordinator preflight failed的错误信息只暴露 driver name + key name不打 key 值——防止 log scraper 顺手把凭据捞走。

凭据轮换

Driver轮换难度备注
ios_appstore创建新 API Key + 改 p8_path;旧 key 可 revoke
android_googleplay在 GCP 创建新 service account;旧 SA 可禁用
huaweiAGC 上 reissue secret;旧 secret 自动失效
xiaomi改 RSA keypair 需submit新公钥等Xiaomibackendreview(人工,工作日 ≤ 24h)
oppo / vivo / yybbackend一键 reissue

凭据丢失 / 怀疑泄漏的应急流程:立即从 ~/.keel/credentials.yml 删除受影响 driver;login该marketbackend revoke 旧凭据 + 签发新一组;commit keel submit configure 重新写入;轮换完成后 24h 内审计本周所有 submit 任务的来源 IP (规划中;现在按各 driver documentation手动操作)。