需求是"改完 Android 端怎么送到手机上"。不发应用商店,也不想每次手动传包,
于是做了最小闭环:后端存一份安装包清单,App 启动时查、下载、交给系统安装器。
后端
- 新增 /app/release/latest 与 /app/release/download/{file},都免登录:
还没装上新包的客户端未必带着有效令牌,检查更新不该被登录挡住
- download_url 用相对路径,客户端拿自己配置的 API 地址拼——同一份清单要在
内网、公网、模拟器之间共用,写死域名只会让其中一个坏掉
- has_update 只比对 version_code,不比版本名字符串(0.10.0 与 0.9.0 的字典序是坑)
- 下载只认清单里登记的文件名,含 ..%2F 一律 9001
- scripts/publish_apk.py 负责复制 APK 并写 latest.json(sha256/size/published_at),
backend/static/apk/ 不进版本库
安卓
- AppUpdater:查清单 → 下到应用自己的 updates 目录(先写 .part 再改名)→
校验 sha256 → 经 FileProvider 交给系统安装器
- UpdateDialog:版本号、更新说明、下载进度、失败重试;「允许安装未知应用」
是系统开关,只跳设置页,不代点
- 启动时自动查一次,首页底部另有「检查更新」入口
- 图标换成和 Web 的 <Logo/> 同一套图形:墨绿圆角底、三条米白圆头线、陶土点;
同时出 mipmap 各密度 PNG、自适应图标与 512px 预览
- versionCode=2 / versionName=0.2.0;发布签名从 android/keystore.properties 读,
没有该文件时回落调试签名,保证别人 clone 下来也能 assembleRelease
文档
- API.md 第 7 节:两个接口、清单字段、签名与 sha256 的约定、9001
- DEPLOY.md 第 5 节:出包命令(JDK 17 + Gradle 8.9 + 干净的 GRADLE_USER_HOME)、
发布清单、传到服务器、验证
- ROADMAP 第 6 节记录本次范围与本机踩过的构建环境坑
验证
- 后端 pytest 103 passed;ruff check / format --check 全绿
- assembleRelease 成功,app-release.apk 7.4 MB,apksigner 验签显示 CN=Dadao Task
- 本地与生产 GET /app/release/latest?version_code=1 → has_update=true,
version_code=2 → false;下载返回 200 / application/vnd.android.package-archive
This commit is contained in:
+61
-3
@@ -544,8 +544,6 @@ Query:
|
||||
|
||||
### 6.3 附件错误码
|
||||
|
||||
### 6.3 附件错误码
|
||||
|
||||
| 码 | HTTP | 含义 |
|
||||
|---|---|---|
|
||||
| 8001 | 404 | 附件不存在,或不属于这个工作区 |
|
||||
@@ -562,7 +560,67 @@ Query:
|
||||
|
||||
---
|
||||
|
||||
## 7. 健康与版本
|
||||
## 7. 安卓发布与升级 `/app`
|
||||
|
||||
安装包不进版本库:`backend/static/apk/` 由 `publish_apk.py` 写入,里面有 APK 与一份
|
||||
`latest.json` 清单。接口只读这份清单,**没有清单就等于没发布过**(9001)。
|
||||
|
||||
### 7.1 `GET /app/release/latest`
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| `version_code` | 可选,客户端当前版本号;给了才会算 `has_update` |
|
||||
|
||||
免登录:老版本客户端可能连"登录"这一步都还没走到,检查更新不该需要令牌。
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"platform": "android",
|
||||
"version_code": 2,
|
||||
"version_name": "0.2.0",
|
||||
"notes": "新增应用内升级;图标换成和 Web 一样",
|
||||
"file": "dadaotask-0.2.0.apk",
|
||||
"size": 8123456,
|
||||
"sha256": "…",
|
||||
"min_sdk": 26,
|
||||
"published_at": "2026-10-31T03:20:00Z",
|
||||
"download_url": "/api/v1/app/release/download/dadaotask-0.2.0.apk",
|
||||
"has_update": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`download_url` 是**相对路径**:同一份清单要被内网、公网、模拟器三个入口共用,
|
||||
写死域名只会让其中一个坏掉;客户端用自己配置的 API 地址拼。
|
||||
|
||||
`has_update` 的判据只有 `version_code`,不看版本名字符串——字符串比较会把
|
||||
`0.10.0` 判成比 `0.9.0` 旧。
|
||||
|
||||
### 7.2 `GET /app/release/download/{file}`
|
||||
|
||||
只认清单里登记过的文件名,其它一律 9001(含 `..%2F` 这类路径穿越尝试)。
|
||||
返回 `application/vnd.android.package-archive`。同样免登录:下载地址本身就是
|
||||
清单里给出的,能拿到清单就说明已经过了一道。
|
||||
|
||||
### 7.3 发布包的约定
|
||||
|
||||
- APK 必须用**同一把证书**签名,否则新包装不上去(系统只会报"应用未安装")。
|
||||
证书放在发布者本机(`android/keystore.properties` 指向它),不进仓库。
|
||||
- 上传清单带 `sha256`,客户端下完先校验再交给系统安装器:内网代理缓存旧包时,
|
||||
这是唯一能挡住"装了个旧版本"的地方。
|
||||
- 「允许安装未知应用」是系统开关,App 只能把用户送到设置页,不能代点。
|
||||
|
||||
### 7.4 错误码
|
||||
|
||||
| 码 | HTTP | 含义 |
|
||||
|---|---|---|
|
||||
| 9001 | 404 | 还没发布过安装包,或下载的文件不在清单里 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 健康与版本
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
|
||||
+54
-2
@@ -100,7 +100,59 @@ curl -sI http://192.168.8.188:8200/ | head -1 # 200,text/html(前端产
|
||||
|
||||
浏览器验收:打开 `http://192.168.8.188:8200` → 手机号任意 + 验证码 `123456` 登录 → 任务/工单/知识各点一遍。
|
||||
|
||||
## 5. 运维
|
||||
## 5. 安卓安装包发布(OTA)
|
||||
|
||||
APK 不进版本库,也不走 `deploy.sh`:代码发布和"发一个安装包"是两件事,
|
||||
前者每次上线都做,后者只在安卓端有改动时做。
|
||||
|
||||
### 5.1 出包(本机)
|
||||
|
||||
```bash
|
||||
cd android
|
||||
export JAVA_HOME=<JDK 17> # AGP 8.7 不认 JDK 25
|
||||
export GRADLE_USER_HOME=<可写的 gradle 目录>
|
||||
gradle -Dorg.gradle.jvmargs="-Djava.io.tmpdir=<可写 tmp>" assembleRelease
|
||||
# 产物:android/app/build/outputs/apk/release/app-release.apk
|
||||
```
|
||||
|
||||
- 版本号在 `android/app/build.gradle.kts`:`versionCode` 是 OTA 的唯一判据,**每次发版必须 +1**,
|
||||
否则客户端查不到更新;`versionName` 只是给人看的。
|
||||
- 签名证书在 `android/keystore.properties`(本机文件,`.gitignore` 已排除)指向的 `.jks`。
|
||||
**换证书等于换应用**:装了旧包的手机再也装不上新包,只能卸载重装。
|
||||
- `dadao.apiBase`(`android/local.properties`)决定包里写死的后端地址。正式包指向
|
||||
`http://192.168.8.188:8200`,模拟器调试包指向 `http://10.0.2.2:8100`。
|
||||
|
||||
### 5.2 发布清单
|
||||
|
||||
```bash
|
||||
python backend/scripts/publish_apk.py android/app/build/outputs/apk/release/app-release.apk \
|
||||
--version-code 2 --version-name 0.2.0 --notes "新增应用内升级;图标与 Web 一致"
|
||||
```
|
||||
|
||||
脚本把 APK 复制成 `backend/static/apk/dadaotask-<版本>.apk`,并写 `latest.json`
|
||||
(含 `sha256`、`size`、`published_at`)。接口读的就是这份清单。
|
||||
|
||||
### 5.3 传到服务器
|
||||
|
||||
```bash
|
||||
scp backend/static/apk/* openclaw-01:~/app/dadaotask/backend/static/apk/
|
||||
```
|
||||
|
||||
`deploy.sh` 的 `git reset --hard` 不会删未跟踪文件,所以已发布的 APK 在服务器上能留住;
|
||||
但换了机器或清了目录就要重新传一次。**不需要重启服务**:接口每次请求都现读清单与文件。
|
||||
|
||||
### 5.4 验证
|
||||
|
||||
```bash
|
||||
curl -s "http://192.168.8.188:8200/api/v1/app/release/latest?version_code=1"
|
||||
# data.version_code 为新版号、has_update=true、download_url 可下载
|
||||
curl -sI http://192.168.8.188:8200/api/v1/app/release/download/dadaotask-0.2.0.apk | head -1 # 200
|
||||
```
|
||||
|
||||
手机端:装上新包 → 下次启动自动查一次;也可以在首页底部点「检查更新」。
|
||||
装完再启动,`version_code` 已是最新,接口返回 `has_update=false`,不会再弹。
|
||||
|
||||
## 6. 运维
|
||||
|
||||
```bash
|
||||
systemctl --user status dadaotask-api # 状态
|
||||
@@ -112,7 +164,7 @@ journalctl --user -u dadaotask-api -f # 日志(gunicorn 输出到 stdout
|
||||
- 回滚:`cd ~/app/dadaotask && git reset --hard <旧 commit> && bash deploy/prod/deploy.sh`;**数据库迁移不回滚**,回滚代码前确认旧代码兼容新表结构
|
||||
- 端口:`8200`(systemd 单元与 `deploy.sh` 的 `PORT` 默认值,两处要一致)
|
||||
|
||||
## 6. 已知限制(当前阶段)
|
||||
## 7. 已知限制(当前阶段)
|
||||
|
||||
| 限制 | 影响 | 解除方式 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -417,3 +417,34 @@
|
||||
- [x] 既有三套 e2e 回归通过(smoke 7 / tickets 8 / knowledge 7)
|
||||
- [x] `tsc -b` 0 错;`vite build` 主包 299.42 kB │ gzip 74.96 kB,Vditor 独立 chunk 385.24 kB │ gzip 136.05 kB(按需加载,只进详情抽屉)
|
||||
- [x] 安卓 `assembleDebug` 成功(APK 10.1 MB),模拟器上走通「详情 → 编辑正文 → 预览 → 保存」,截图 `2.Design/screens/android-editor{,-preview}.png`
|
||||
|
||||
## 6. 安卓分发:应用内升级(OTA,2026-10-31)
|
||||
|
||||
要解决的是"改完安卓端怎么送到手机上":不发商店,也不能每次让人手动传 APK。
|
||||
|
||||
### 6.1 后端
|
||||
- [x] `backend/app/api/app_release.py`:`GET /app/release/latest?version_code=` 读 `latest.json` 清单,`GET /app/release/download/{file}` 只认清单里登记的文件名(含 `..%2F` 一律 9001)
|
||||
- [x] 两个接口都免登录:还没装上新包的客户端未必带着有效令牌,检查更新不该被登录挡住
|
||||
- [x] `download_url` 是相对路径,客户端用自己配置的 API 地址拼接——同一份清单要在内网/公网/模拟器之间共用
|
||||
- [x] `has_update` 只比对 `version_code`,不看版本名字符串(`0.10.0` 与 `0.9.0` 的字典序是坑)
|
||||
- [x] `backend/scripts/publish_apk.py`:复制成 `dadaotask-<版本>.apk` 并写 `latest.json`(sha256/size/published_at);`backend/static/apk/` 已在 `.gitignore`
|
||||
- [x] `backend/tests/test_app_release.py` 5 个用例:未发布 9001、清单字段与 download_url、`has_update` 判据、下载字节与 content-type、路径穿越
|
||||
|
||||
### 6.2 安卓
|
||||
- [x] `data/AppUpdater.kt`:查清单 → 下到 `Android/data/<pkg>/files/updates/`(先写 `.part` 再改名)→ 校验 sha256 → 经 `FileProvider` 交给系统安装器
|
||||
- [x] `ui/UpdateDialog.kt`:当前/新版本号、更新说明、下载进度、失败重试;「允许安装未知应用」只跳系统设置,不代点
|
||||
- [x] 启动时自动查一次;首页底部有「检查更新」手动入口,结果用一句话回给用户
|
||||
- [x] 图标换成与 Web 的 `<Logo/>` 同一套图形(墨绿圆角底 + 三条米白圆头线 + 陶土点),生成 `mipmap-*/ic_launcher{,_round}.png` 与自适应图标 `mipmap-anydpi-v26/ic_launcher{,_round}.xml`,512px 预览存 `2.Design/screens/android-icon-512.png`
|
||||
- [x] 版本 `versionCode = 2` / `versionName = "0.2.0"`;发布签名从 `android/keystore.properties` 读,无该文件时回落调试签名(保证别人 clone 下来也能 `assembleRelease`)
|
||||
|
||||
### 6.3 构建环境(本机踩过的坑)
|
||||
- Gradle 8.7 被 AGP 8.7.3 拒绝(`Minimum supported Gradle version is 8.9`),用 **Gradle 8.9**(腾讯镜像下载)
|
||||
- 系统 `~/.gradle` 的 native 库坏了(`Failed to load native library 'native-platform.dll'`),必须 `GRADLE_USER_HOME` 指到一个干净目录
|
||||
- JDK 25 编译不过,用 **Temurin JDK 17**;Kotlin 编译还要把 `java.io.tmpdir` 指到工程内可写目录,否则报 `AccessDeniedException: kotlin-compiler-*.alive`
|
||||
- 完整命令见 `2.Design/DEPLOY.md` 第 5.1 节
|
||||
|
||||
### 6.4 验证
|
||||
- [x] 后端 `pytest` → **103 passed**;`ruff check` / `ruff format --check` 全绿
|
||||
- [x] `assembleRelease` 成功:`app-release.apk` 7.4 MB,`apksigner verify --print-certs` 显示证书 `CN=Dadao Task`
|
||||
- [x] 本地 `GET /api/v1/app/release/latest?version_code=1` → `has_update=true`、`download_url` 可下(200,7 762 912 字节),`version_code=2` → `false`
|
||||
- [x] 生产(`http://192.168.8.188:8200`)同上,安装包已随部署上传
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Reference in New Issue
Block a user