docs: update release-guide and app-update-system for v1.9.1
This commit is contained in:
+92
-43
@@ -197,11 +197,13 @@ ON CONFLICT (version) DO NOTHING;
|
||||
| 方法 | 路径 | 说明 | 认证 |
|
||||
|------|------|------|------|
|
||||
| POST | `/api/v1/update/check` | 检查应用更新 | 无需认证 |
|
||||
| GET | `/api/v1/update/download/{version}/{platform}` | 获取下载 URL 并记录 | 无需认证 |
|
||||
| GET | `/api/v1/update/download` | 统一下载入口:自动匹配最新版本和平台安装包 | 无需认证 |
|
||||
| POST | `/api/v1/update/releases` | 创建新版本发布 | 需要管理员认证 |
|
||||
| GET | `/api/v1/update/releases` | 获取所有版本列表 | 需要管理员认证 |
|
||||
| DELETE | `/api/v1/update/releases/{version}` | 删除版本发布 | 需要管理员认证 |
|
||||
|
||||
> 说明:`/download` 优先根据 `User-Agent` 识别平台(darwin / windows / linux)和架构(x86_64 / aarch64),也支持通过查询参数显式指定,例如 `?target=darwin&arch=aarch64`。返回 302 重定向到七牛云上的对应安装包。
|
||||
|
||||
### 3.2 数据模型
|
||||
|
||||
#### 3.2.1 SQLAlchemy 模型
|
||||
@@ -350,9 +352,10 @@ class DownloadResponse(BaseModel):
|
||||
文件位置:`python-api/app/api/v1/update.py`
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, HTTPException, Depends, status
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, Request, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
|
||||
from ...deps import get_db
|
||||
@@ -443,69 +446,115 @@ async def check_update(
|
||||
]
|
||||
)
|
||||
|
||||
@router.get("/download/{version}/{platform}", response_model=DownloadResponse)
|
||||
async def get_download_url(
|
||||
version: str,
|
||||
platform: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@router.get("/download")
|
||||
async def download_latest(
|
||||
request: Request,
|
||||
target: str | None = Query(None, description="平台:darwin / windows / linux"),
|
||||
arch: str | None = Query(None, description="架构:x86_64 / aarch64 / i686"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
获取下载 URL 并记录下载日志
|
||||
统一下载入口:自动匹配最新版本和当前环境安装包。
|
||||
|
||||
Args:
|
||||
version: 目标版本
|
||||
platform: 平台类型
|
||||
优先级:
|
||||
1. 查询参数 `target` + `arch`(最可靠,推荐应用内使用)
|
||||
2. `User-Agent` 解析(兜底,适合网页/文档中的固定链接)
|
||||
|
||||
Returns:
|
||||
包含下载 URL、文件大小和哈希的响应
|
||||
匹配规则:
|
||||
- 返回 302 重定向到七牛云上的安装包地址
|
||||
- 优先返回用户安装包(`.dmg` / `.exe` / `.msi` / `.AppImage`)
|
||||
- 其次返回 updater 用的 `.app.tar.gz`
|
||||
- 同一平台若找不到精确架构,会兜底返回同平台的其他架构包
|
||||
"""
|
||||
# 查询版本信息
|
||||
result = await db.execute(
|
||||
select(AppRelease).where(AppRelease.version == version)
|
||||
# 1. 确定平台与架构
|
||||
if target and arch:
|
||||
platform = target.lower()
|
||||
architecture = arch.lower()
|
||||
else:
|
||||
parsed = _parse_user_agent(request.headers.get("user-agent"))
|
||||
if not parsed:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="无法识别您的操作系统,请通过官网或应用商店下载对应版本",
|
||||
)
|
||||
release = result.scalar_one_or_none()
|
||||
platform, architecture = parsed
|
||||
|
||||
if not release:
|
||||
# 2. 查询最新版本
|
||||
result = await db.execute(
|
||||
select(AppRelease).order_by(AppRelease.release_date.desc()).limit(1)
|
||||
)
|
||||
latest: AppRelease | None = result.scalar_one_or_none()
|
||||
if not latest:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Release not found"
|
||||
detail="暂无可用下载",
|
||||
)
|
||||
|
||||
# 查询对应平台的包
|
||||
# 3. 查询该平台所有包(macOS 常用 universal 包会同时写入 x86_64/aarch64)
|
||||
result = await db.execute(
|
||||
select(ReleasePackage).where(
|
||||
and_(
|
||||
ReleasePackage.release_id == release.id,
|
||||
ReleasePackage.platform == platform
|
||||
ReleasePackage.release_id == latest.id,
|
||||
ReleasePackage.platform == platform,
|
||||
)
|
||||
)
|
||||
)
|
||||
package = result.scalar_one_or_none()
|
||||
platform_pkgs = list(result.scalars().all())
|
||||
|
||||
if not package:
|
||||
if not platform_pkgs:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Package not found for platform {platform}"
|
||||
detail=f"版本 {latest.version} 暂无可用的 {platform} 安装包",
|
||||
)
|
||||
|
||||
# 增加下载计数
|
||||
package.download_count += 1
|
||||
# 4. 优先选择用户安装包,而不是 updater 用的 .app.tar.gz
|
||||
def _install_pkg_priority(pkg: ReleasePackage) -> int:
|
||||
name = pkg.filename.lower()
|
||||
if name.endswith(".dmg"):
|
||||
return 1
|
||||
if name.endswith(".exe"):
|
||||
return 2
|
||||
if name.endswith(".msi"):
|
||||
return 3
|
||||
if name.endswith(".appimage"):
|
||||
return 4
|
||||
if name.endswith(".app.tar.gz"):
|
||||
return 10
|
||||
return 5
|
||||
|
||||
# 记录下载日志(可选,不阻塞主流程)
|
||||
download_log = UpdateDownload(
|
||||
release_id=release.id,
|
||||
platform=platform,
|
||||
app_version=version
|
||||
)
|
||||
db.add(download_log)
|
||||
exact_arch_pkgs = [p for p in platform_pkgs if p.architecture == architecture]
|
||||
candidate_pkgs = exact_arch_pkgs or platform_pkgs
|
||||
package = min(candidate_pkgs, key=_install_pkg_priority)
|
||||
|
||||
await db.commit()
|
||||
return RedirectResponse(url=package.file_url)
|
||||
|
||||
return DownloadResponse(
|
||||
download_url=package.file_url,
|
||||
file_size=package.file_size,
|
||||
file_hash=package.file_hash
|
||||
)
|
||||
|
||||
def _parse_user_agent(user_agent: str | None) -> tuple[str, str] | None:
|
||||
"""从 User-Agent 解析 Tauri 平台标识和架构。"""
|
||||
if not user_agent:
|
||||
return None
|
||||
|
||||
ua = user_agent.lower()
|
||||
|
||||
if "windows" in ua:
|
||||
platform = "windows"
|
||||
arch = "aarch64" if "arm64" in ua or "aarch64" in ua else "x86_64"
|
||||
return platform, arch
|
||||
|
||||
if "macintosh" in ua or "mac os x" in ua:
|
||||
platform = "darwin"
|
||||
if "arm64" in ua or "aarch64" in ua:
|
||||
arch = "aarch64"
|
||||
elif "intel" in ua:
|
||||
arch = "x86_64"
|
||||
else:
|
||||
arch = "aarch64"
|
||||
return platform, arch
|
||||
|
||||
if "linux" in ua:
|
||||
platform = "linux"
|
||||
arch = "aarch64" if "aarch64" in ua or "arm64" in ua else "x86_64"
|
||||
return platform, arch
|
||||
|
||||
return None
|
||||
|
||||
@router.post("/releases", response_model=ReleaseResponse)
|
||||
async def create_release(
|
||||
|
||||
+52
-13
@@ -7,14 +7,20 @@
|
||||
|
||||
## 前置条件
|
||||
|
||||
### 1. 签名密钥(已生成,只需确认存在)
|
||||
### 1. 签名密钥
|
||||
|
||||
当前使用 Tauri 自动更新签名密钥对,私钥用于构建时签名,公钥写入前端配置。
|
||||
|
||||
```bash
|
||||
# 确认密钥对存在(私钥文件名以你本地实际为准)
|
||||
ls ~/.tauri/meijiaka.key ~/.tauri/meijiaka.key.pub
|
||||
```
|
||||
|
||||
- 私钥 `~/.tauri/meijiaka.key`:构建时用于签名,**不要泄露**
|
||||
- 公钥内容:已写入 `tauri-app/src-tauri/tauri.conf.json` 的 `plugins.updater.pubkey`
|
||||
- **私钥**:构建时用于签名更新包,**不要泄露,不要提交到 Git**
|
||||
- **公钥**:内容已写入 `tauri-app/src-tauri/tauri.conf.json` 的 `plugins.updater.pubkey`
|
||||
|
||||
> **注意**:如果重新生成了签名密钥对,旧版本客户端将无法通过自动更新升级(旧公钥无法验证新私钥的签名),只能手动下载重装。详细说明见本文档末尾"密钥更换与手动下载"。
|
||||
|
||||
|
||||
### 2. 七牛云环境变量(复用素材上传配置)
|
||||
|
||||
@@ -45,26 +51,34 @@ curl https://dev.tapi.meijiaka.cn/api/v1/system/health
|
||||
|
||||
### 步骤 1:修改版本号
|
||||
|
||||
三个文件版本号必须完全一致:
|
||||
所有包含版本号的位置必须保持一致,建议统一修改后全局搜索确认:
|
||||
|
||||
```bash
|
||||
cd tauri-app
|
||||
# 项目根目录
|
||||
VERSION
|
||||
AGENTS.md
|
||||
|
||||
# 1. package.json
|
||||
npm version 1.6.0 --no-git-tag-version
|
||||
# 前端
|
||||
tauri-app/package.json
|
||||
tauri-app/src-tauri/Cargo.toml
|
||||
tauri-app/src-tauri/tauri.conf.json
|
||||
tauri-app/src-tauri/Cargo.lock # name = "tauri-app" 对应的 version
|
||||
tauri-app/AGENTS.md
|
||||
|
||||
# 2. Cargo.toml
|
||||
# 手动修改:src-tauri/Cargo.toml → version = "1.6.0"
|
||||
|
||||
# 3. tauri.conf.json
|
||||
# 手动修改:src-tauri/tauri.conf.json → "version": "1.6.0"
|
||||
# 后端
|
||||
python-api/pyproject.toml
|
||||
python-api/uv.lock # name = "meijiaka-ai-api" 对应的 version
|
||||
python-api/app/config.py # APP_VERSION 默认值
|
||||
```
|
||||
|
||||
> 当前版本示例:`1.9.1`。修改后可用 `grep -R "1.9.0"` 检查是否有遗漏。
|
||||
|
||||
### 步骤 2:构建
|
||||
|
||||
```bash
|
||||
cd tauri-app
|
||||
export TAURI_SIGNING_PRIVATE_KEY="$HOME/.tauri/meijiaka.key"
|
||||
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" # 若私钥有密码则填写
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
@@ -162,12 +176,37 @@ Tauri updater 插件已内置跨平台安装逻辑,前端代码无需区分平
|
||||
|
||||
---
|
||||
|
||||
## 密钥更换与手动下载
|
||||
|
||||
如果签名密钥对发生更换(例如旧私钥丢失、泄露或主动轮换):
|
||||
|
||||
1. **旧版本客户端无法自动更新**
|
||||
- 旧客户端内置的旧公钥无法验证新私钥签名的更新包
|
||||
- Tauri updater 会在签名验证阶段失败
|
||||
|
||||
2. **必须引导用户手动下载重装**
|
||||
- 后端已提供统一下载入口:`GET /api/v1/update/download`
|
||||
- 该接口根据 `User-Agent` 自动匹配最新版本和平台安装包
|
||||
- 也支持显式传参:`/api/v1/update/download?target=darwin&arch=x86_64`
|
||||
- 返回 **302 重定向**到对应安装包的七牛云 URL
|
||||
|
||||
3. **分发链接示例**
|
||||
- 网页/文档固定链接:`https://dev.tapi.meijiaka.cn/api/v1/update/download`
|
||||
- 应用内手动下载按钮:调用 Tauri `opener` 打开上述链接即可
|
||||
|
||||
4. **恢复自动更新**
|
||||
- 用户手动安装新版本后,新安装包中已包含新公钥
|
||||
- 后续版本可恢复正常的自动更新流程
|
||||
|
||||
---
|
||||
|
||||
## 文件清单
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `~/.tauri/meijiaka.key` | 私钥(签名用,勿泄露) |
|
||||
| `tauri-app/.tauri-signing-key.pub` | 公钥源文件 |
|
||||
| `tauri-app/src-tauri/tauri.conf.json` | updater 配置:公钥 + endpoint URL |
|
||||
| `python-api/scripts/publish_release.py` | 发版脚本(扫描 .sig → 上传七牛云 → 写数据库) |
|
||||
| `python-api/app/api/v1/update.py` | 后端更新检查 API |
|
||||
| `python-api/app/api/v1/update.py` | 后端更新检查 API + 统一下载入口 |
|
||||
| `python-api/app/models/update.py` | 数据库模型(`mjk_app_releases` / `mjk_app_release_packages`) |
|
||||
|
||||
Reference in New Issue
Block a user