# apkgo
**Repository Path**: ForTheDream/apkgo
## Basic Information
- **Project Name**: apkgo
- **Description**: 快速将apk更新至各个平台
- **Primary Language**: Go
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: https://apkgo.com.cn
- **GVP Project**: No
## Statistics
- **Stars**: 18
- **Forks**: 0
- **Created**: 2023-02-21
- **Last Updated**: 2026-09-24
## Categories & Tags
**Categories**: ci
**Tags**: None
## README
# apkgo
   [](./LICENSE) [](https://skills.sh/KevinGong2013/apkgo)
🌐 **Language / 语言**: [English](./README.en.md) · **简体中文**
一行命令,将 APK 发布到所有主流安卓应用商店。
> **不想搭 CI、不想碰命令行?** 试试托管版 [**apkgo cloud**](https://apkgo.baici.tech) —— 浏览器打开就能发版,凭证云端托管、多人协作、发布历史可追溯,免装免运维,运营和产品同事也能独立上手。除命令行版覆盖的安卓各大商店外,云端还支持 **iOS(App Store)** 与 **鸿蒙(HarmonyOS)** 上架。
## 安装
**macOS / Linux** — 一行脚本自动识别 OS/架构、下载、校验 SHA-256:
```bash
curl -fsSL https://apkgo.com.cn/install.sh | sh
```
> 默认装到 `/usr/local/bin`,不可写时会提示用 `sudo` 或 `APKGO_INSTALL_DIR=$HOME/.local/bin sh`。
> 锁版本:`APKGO_VERSION=v3.1.0 sh`。
**其他方式:**
```bash
# AI Agent Skill (支持 Claude Code、Cursor、Windsurf 等 40+ agent)
npx skills add KevinGong2013/apkgo
# Go
go install github.com/KevinGong2013/apkgo@latest
# Docker
docker pull ghcr.io/kevingong2013/apkgo:latest
```
手动下载 / Windows
```bash
# macOS (Apple Silicon)
curl -fsSL https://github.com/KevinGong2013/apkgo/releases/latest/download/apkgo_Darwin_arm64.tar.gz | tar xz -C /usr/local/bin apkgo
# macOS (Intel)
curl -fsSL https://github.com/KevinGong2013/apkgo/releases/latest/download/apkgo_Darwin_x86_64.tar.gz | tar xz -C /usr/local/bin apkgo
# Linux (x86_64)
curl -fsSL https://github.com/KevinGong2013/apkgo/releases/latest/download/apkgo_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin apkgo
# Linux (arm64)
curl -fsSL https://github.com/KevinGong2013/apkgo/releases/latest/download/apkgo_Linux_arm64.tar.gz | tar xz -C /usr/local/bin apkgo
# Windows (PowerShell)
# 从 https://github.com/KevinGong2013/apkgo/releases/latest 下载 apkgo_Windows_x86_64.zip
# 解压后将 apkgo.exe 添加到 PATH
```
## 快速开始
```bash
# 1. 生成配置文件
apkgo init
# 2. 填写商店凭证
vim apkgo.yaml
# 3. 上传
apkgo upload -f app.apk
```
## 用法
### 上传
```bash
# 上传到所有配置的商店
apkgo upload -f app.apk
# 上传到指定商店
apkgo upload -f app.apk --store huawei,xiaomi
# 带更新日志
apkgo upload -f app.apk --notes "修复了登录问题"
apkgo upload -f app.apk --notes-file CHANGELOG.md
# 分架构包
apkgo upload -f app-arm32.apk --file64 app-arm64.apk
# 只验证不上传
apkgo upload -f app.apk --dry-run
# vivo 走沙箱,其他渠道只验证不上传
apkgo upload -f app.apk --sandbox
# 直接用 http(s) 地址(自动下载到临时文件;私有地址可加请求头,可重复)
apkgo upload -f https://artifacts.example.com/app-v1.apk --store huawei
apkgo upload -f https://private.example.com/app.apk --fetch-header "Authorization: Bearer xxx"
# 定时发布(RFC3339 时间)
apkgo upload -f app.apk --release-time 2026-06-20T10:00:00+08:00
# Google Play 上传 AAB(.aab 只会发到 googleplay)
apkgo upload -f app-release.aab -s googleplay
```
- `-f` 为 URL 时,huawei / honor / vivo 直接让商店从该地址拉包(honor 仅对 ≥ `url_push_min_mb`,默认 100MB 的文件),其他商店由 apkgo 下载后上传。
- `--release-time` 支持 huawei、harmony、honor、xiaomi、oppo、vivo、samsung、tencent,其他商店立即发布。
- 文件类型:`.apk` 发往所有安卓商店;`.aab` 仅 googleplay;鸿蒙 `.app` 仅 harmony。
#### 鸿蒙(HarmonyOS)上架
```bash
# 上传 DevEco Studio 打出的签名 .app 整包到华为应用市场(鸿蒙)
apkgo upload -f demo-default-signed.app -s harmony --notes "适配 HarmonyOS 5"
apkgo audit -f demo-default-signed.app -s harmony # 查询鸿蒙审核状态
apkgo doctor -f demo-default-signed.app -s harmony # 体检凭证 / 应用 ID / 上架权限
```
`harmony` 商店走 AppGallery Connect 的 HarmonyOS Publishing API,**复用 huawei 的 Service Account 凭证**(同一个 AGC 账号同时覆盖安卓与鸿蒙应用),流程为:按 bundleName 查 appId(`packageTypes=7`)→ `upload-url/for-obs` 取签名上传地址 → PUT 上传 `.app` → 绑定软件包 → 等待编译解析 → 提交审核。只接受 **`.app` 整包**(AGC 不接受单个 `.hap` 模块);`-f` 为鸿蒙包时会自动跳过安卓商店(不传 `-s` 时),显式指定安卓商店则直接报错。支持 `--release-time` 定时发布,不支持 `--file64` 与 URL 直传。
`--sandbox` 与 `--dry-run` 互斥。目前只有 vivo 支持沙箱:vivo 会真实调用沙箱 API,其他目标渠道执行与 `--dry-run` 相同的本地校验。沙箱运行不执行 hooks、不写上传历史、不触发生命周期事件回调,也不上报上传遥测。结果顶层包含 `"sandbox": true`,vivo 结果包含 `"sandbox": true`,其他渠道包含 `"dry_run": true`。
vivo 的[沙箱环境](https://dev.vivo.com.cn/documentCenter/doc/327#s-l67kfh1m)与正式环境的应用数据、`access_key` 和密钥完全隔离;请按[在线测试环境说明](https://dev.vivo.com.cn/documentCenter/doc/327#s-b9qi52f4)先在沙箱创建应用并申请独立凭据。测试环境限制每个接口 100 次/天。
### 查询审核状态
上传成功只代表「已提交(审核中)」,审核结果用 `audit` 单独查询:
```bash
apkgo audit -p com.example.app
apkgo audit -f app.apk -s tencent,huawei
# 持续轮询,直到每个商店都出结果(通过 / 驳回 / 撤回)或达到 -t 超时
apkgo audit -p com.example.app --watch --interval 1m -t 1h
```
目前支持 huawei、harmony、honor、xiaomi、oppo、vivo、meizu、tencent、samsung。
### 初始化配置
```bash
# 生成包含所有商店的配置
apkgo init
# 只生成指定商店的配置
apkgo init --store huawei,xiaomi
# 指定配置文件路径
apkgo init -c production.yaml
```
### 查看支持的商店
```bash
apkgo stores
# 使用内置 YAML 解析器,只输出当前配置中的商店名,不输出凭据
apkgo -c apkgo.yaml stores --configured
```
### 体检(验证商店配置)
`doctor` 在不上传文件的前提下,校验已配置商店的凭证和权限是否到位,避免真实上传到一半才发现问题:
```bash
# 校验所有已配置商店的凭证
apkgo doctor
# 只校验指定商店
apkgo doctor -s huawei
# 提供包名后会跑更深入的检查(包名映射、发布权限等)
apkgo doctor -s huawei -p com.example.app
apkgo doctor -s huawei -f app.apk # 从 APK 自动取包名
```
任一探针失败时,退出码为 1。除 googleplay 与 script 外的商店均已支持,未实现的会标记为 `doctor not implemented`。
## 配置文件
`apkgo.yaml`:
```yaml
# 可选:检查新版本的频率,如 "30d"、"7d","0" 关闭
# update_check: "7d"
# hooks 为可选配置,不需要可以不写
hooks:
before: "./scripts/validate.sh" # 所有上传前执行
after: "./scripts/notify.sh" # 所有上传后执行
stores:
huawei:
# 推荐:服务账号(PS256 JWT)
service_account_file: "/secure/path/huawei-sa.json"
# 或者: service_account: ""
# app_id: "" # 可选,不填则自动通过包名查询
before: "./scripts/before-huawei.sh" # 可选,该商店上传前执行
after: "./scripts/after-huawei.sh" # 可选,该商店上传后执行
timeout: 8m # 可选,该商店单独超时(覆盖全局 --timeout)
harmony:
# 鸿蒙(HarmonyOS)应用:凭证与 huawei 相同,可直接复用同一份 Service Account
service_account_file: "/secure/path/huawei-sa.json"
# app_id: "" # 可选,不填则按 bundleName 自动查询(仅匹配鸿蒙应用)
# lang: "zh-CN" # 可选,更新日志写入的语言,默认取 AGC 里应用的默认语言
# chinese_mainland_flag: "1" # 可选,开发者注册地非中国大陆时 AGC 要求填写
xiaomi:
email: "your@email.com"
private_key: "your-private-key" # 小米后台的「接口密钥」(被 SDK 当作 password 使用)
# cert_file: "/secure/path/xiaomi-pubkey.cer" # 可选,默认使用内置公钥证书(也支持 cert: 或 cert: )
oppo:
client_id: "your-client-id" # 19 位数字
client_secret: "your-client-secret"
vivo:
access_key: "your-access-key"
access_secret: "your-access-secret"
sandbox_access_key: "your-sandbox-access-key" # --sandbox 时使用
sandbox_access_secret: "your-sandbox-access-secret" # --sandbox 时使用
honor:
client_id: "your-client-id"
client_secret: "your-client-secret"
# app_id: "" # 可选,不填则按包名自动查询
# url_push_min_mb: "100" # 可选,-f 为 URL 时 ≥ 该大小(MB)才让荣耀从 URL 拉包
meizu:
client_id: "your-client-id" # 魅族开放平台「客户端凭证」
client_secret: "your-client-secret"
tencent:
user_id: "your-user-id"
access_secret: "your-access-secret"
app_id: "your-app-id"
# 多 app: app_id_map: '{"com.foo":"111","com.bar":"222"}'
samsung:
service_account_id: "your-service-account-id" # Seller Portal 服务账号 ID
private_key: "-----BEGIN PRIVATE KEY-----..." # Seller Portal 下载的 RSA 私钥(PEM)
content_id: "your-content-id" # Galaxy Store 中应用的 content ID
googleplay:
json_key_file: "/secure/path/play-sa.json" # 服务账号 JSON 密钥
package_name: "com.example.app"
# track: "production" # 可选:production(默认)/ beta / alpha / internal
pgyer:
api_key: "your-pgyer-api-key"
fir:
api_token: "your-fir-api-token"
# 单个脚本
script:
command: "./deploy.sh"
# 多个脚本实例 (script.实例名)
script.cdn-upload:
command: "./upload-cdn.sh"
script.dingtalk:
command: "./notify-dingtalk.sh"
```
#### Hooks 说明
Hooks 是可选功能,不配置则不生效。Hook 脚本通过 stdin 接收 JSON 上下文,通过退出码控制流程:
- `before` hook 失败(非零退出码)→ 中止上传
- `after` hook 失败 → 仅记录警告,不影响结果
- 自动注入环境变量:`APKGO_STORE`、`APKGO_PACKAGE`、`APKGO_VERSION`
- stderr 输出作为错误信息
- 通过 `sh -c` 执行(Windows 为 `cmd /C`)
- `apk.platform` 为 `android`(APK/AAB)或 `harmony`(鸿蒙 `.app`)
**全局 before hook** (`hooks.before`) stdin:
```json
{
"file_path": "/path/to/app.apk",
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1, "app_name": "MyApp"},
"stores": ["huawei", "xiaomi"]
}
```
**全局 after hook** (`hooks.after`) stdin:
```json
{
"file_path": "/path/to/app.apk",
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1, "app_name": "MyApp"},
"results": [
{"store": "huawei", "success": true, "category": "success", "duration_ms": 12300},
{"store": "xiaomi", "success": false, "error": "auth failed", "category": "auth_failed", "duration_ms": 400}
]
}
```
**商店 before hook** (`stores..before`) stdin:
```json
{
"file_path": "/path/to/app.apk",
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1, "app_name": "MyApp"},
"store": "huawei"
}
```
**商店 after hook** (`stores..after`) stdin:
```json
{
"file_path": "/path/to/app.apk",
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1, "app_name": "MyApp"},
"store": "huawei",
"result": {"store": "huawei", "success": true, "category": "success", "duration_ms": 12300}
}
```
Hook 脚本示例(上传完成后发送钉钉通知):
```bash
#!/bin/bash
input=$(cat)
results=$(echo "$input" | jq -r '.results[] | "\(.store): \(if .success then "✓" else "✗ "+.error end)"')
curl -s -X POST "$DINGTALK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"APK 发布完成 v${APKGO_VERSION}\n${results}\"}}"
```
### 环境变量
CI/CD 环境中可通过环境变量配置凭证,无需配置文件:
```bash
# 格式: APKGO_<商店名>_<字段名>=值
export APKGO_HUAWEI_SERVICE_ACCOUNT="$(base64 -w0 huawei-sa.json)" # 推荐
export APKGO_XIAOMI_EMAIL="your@email.com"
export APKGO_XIAOMI_PRIVATE_KEY="your-接口密钥"
export APKGO_XIAOMI_CERT="$(base64 -w0 xiaomi-pubkey.cer)"
export APKGO_OPPO_CLIENT_ID="your-19-digit-id"
export APKGO_OPPO_CLIENT_SECRET="your-secret"
export APKGO_VIVO_ACCESS_KEY="your-key"
export APKGO_VIVO_ACCESS_SECRET="your-secret"
export APKGO_MEIZU_CLIENT_ID="your-client-id"
export APKGO_MEIZU_CLIENT_SECRET="your-secret"
export APKGO_TENCENT_USER_ID="your-user-id"
export APKGO_TENCENT_ACCESS_SECRET="your-secret"
export APKGO_TENCENT_APP_ID="your-app-id"
# 多 app: APKGO_TENCENT_APP_ID_MAP='{"com.foo":"111","com.bar":"222"}'
export APKGO_SAMSUNG_SERVICE_ACCOUNT_ID="your-sa-id"
export APKGO_SAMSUNG_PRIVATE_KEY="$(cat samsung-private-key.pem)"
export APKGO_SAMSUNG_CONTENT_ID="your-content-id"
export APKGO_GOOGLEPLAY_JSON_KEY_FILE="/secure/path/play-sa.json"
export APKGO_GOOGLEPLAY_PACKAGE_NAME="com.example.app"
export APKGO_PGYER_API_KEY="your-pgyer-key"
export APKGO_FIR_API_TOKEN="your-fir-token"
# 环境变量会覆盖配置文件中的同名字段
# 如果没有配置文件,完全通过环境变量配置也可以
apkgo upload -f app.apk --store huawei
```
### 凭证不落盘(cloud worker / CI / 多租户)
`--creds-from` 让 apkgo 从非磁盘源读取 JSON 格式的凭证。orchestrator 把凭证从 secrets manager(Vault / AWS SM / GCP SM 等)取出后通过 stdin 或文件描述符直接注入子进程,**全程不写盘、不进 env**:
```bash
# 方式 A: stdin
vault read -format=json secret/apkgo | jq .data \
| apkgo upload -f app.apk --creds-from=stdin
# 方式 B: 文件描述符(适合 stdin 已经被占用的场景)
apkgo upload -f app.apk --creds-from=fd:3 3<<<"$(vault-creds-as-json)"
```
JSON 格式跟 yaml 一一对应:
```json
{
"stores": {
"huawei": {"service_account": ""},
"tencent": {
"user_id": "...",
"access_secret": "...",
"app_id_map": "{\"com.foo\":\"111\"}"
}
},
"hooks": {"before": "...", "after": "..."}
}
```
apkgo 解析完立刻把输入字节 zero-out,避免 secret 留在缓冲区。设了 `--creds-from` 时 `--config` 和 `APKGO_*` env 都被忽略。
## AI Agent 集成
apkgo 的输出格式专为 AI Agent 和自动化场景设计:
**结构化 JSON 输出** (stdout):
```json
{
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1},
"results": [
{"store": "huawei", "success": true, "category": "success", "duration_ms": 12300},
{"store": "oppo", "success": true, "category": "already_done", "duration_ms": 3200},
{"store": "xiaomi", "success": false, "category": "policy_block", "error": "签名不一致...", "duration_ms": 400}
]
}
```
**Category(重试决策提示)**:每个 result 带一个 `category` 字段,把各家千奇百怪的错误码归成 cloud orchestrator 友好的几个桶,避免父进程解析中文错误。可能的值:
| Category | 含义 | 建议处理 |
|---|---|---|
| `success` | 上传成功 | mark done |
| `already_done` | 该版本已在商店侧(如 OPPO 911215 应用审核中) | mark done,不重试 |
| `auth_failed` | 凭证错 | 让用户改 secret,不重试 |
| `network_retry` | 网络超时 / 5xx | 退避后重试 |
| `store_busy` | 商店限流 / 上次任务还没完 | 等几分钟再重试 |
| `policy_block` | 签名不一致、审核驳回等业务规则拒绝 | 让用户处理,不重试 |
| `config_invalid` | 后台元数据缺失(intro / 分类 / publisher entity) | 让用户去后台填,不重试 |
| `unknown` | 还没分类到 | 默认按"不可重试"处理 |
**语义化退出码**:
| Code | 含义 |
|------|------|
| 0 | 全部成功 |
| 1 | 部分失败 |
| 2 | 全部失败 |
| 3 | 输入错误 |
**可发现的配置 schema**:
```bash
apkgo stores # 返回每个商店需要的配置字段
```
**非交互**: 无 prompt、无确认,适合无人值守环境。
**实时进度流(NDJSON)**:父进程 fork apkgo 想实时拿进度时,加 `--progress-stream`,stdout 变成每行一个 JSON 事件:
```bash
apkgo upload -f app.apk --progress-stream
```
```json
{"type":"start","apk":{"package":"...","version_name":"1.2.0","version_code":120},"stores":["huawei","xiaomi"]}
{"type":"phase","store":"huawei","phase":"auth"}
{"type":"phase","store":"huawei","phase":"uploading"}
{"type":"total","store":"huawei","total_bytes":62914560}
{"type":"bytes","store":"huawei","sent":7045120,"total":62914560}
{"type":"bytes","store":"huawei","sent":23560192,"total":62914560}
{"type":"phase","store":"huawei","phase":"submitting"}
{"type":"result","store":"huawei","success":true,"duration_ms":34570}
{"type":"done","apk":{...},"results":[...]}
```
`bytes` 事件每 ~100ms 一条(throttled),多家并发各自一条流,按 `store` 字段区分。Go 父进程消费示例:
```go
cmd := exec.CommandContext(ctx, "apkgo", "upload", "-f", apkPath, "--progress-stream")
out, _ := cmd.StdoutPipe()
cmd.Start()
sc := bufio.NewScanner(out)
for sc.Scan() {
var evt map[string]any
json.Unmarshal(sc.Bytes(), &evt)
switch evt["type"] {
case "bytes":
ui.UpdateProgress(evt["store"].(string), evt["sent"].(float64), evt["total"].(float64))
case "result":
ui.MarkStoreDone(evt["store"].(string), evt["success"].(bool))
case "done":
ui.Finish(evt["results"])
}
}
cmd.Wait()
```
## 全部命令
```
apkgo init [-s store1,store2] [-c config.yaml]
apkgo upload -f [--file64 ] [-s stores] [-n notes] [--notes-file path]
[--release-time ] [--fetch-header "Name: value"] [--dry-run | --sandbox]
[--progress-stream] [-t timeout]
apkgo audit [-s stores] (-f | -p ) [--watch] [--interval 30s]
apkgo doctor [-s stores] [-f | -p ]
apkgo config export --out
apkgo config import
apkgo stores [--configured] [-o json|text]
apkgo history [-n limit]
apkgo upgrade
apkgo version [-o json|text]
```
## 全局参数
```
-c, --config 配置文件路径 (默认: apkgo.yaml)
--creds-from 从 stdin 或 fd:N 读取 JSON 凭证(覆盖 --config)
-o, --output 输出格式: json 或 text (默认: json)
-t, --timeout 全局超时 (默认: 10m)
-v, --verbose 详细日志输出到 stderr
```
## License
[PolyForm Noncommercial License 1.0.0](./LICENSE) —— 仅限非商业用途免费使用,商业使用请联系作者授权。