From b242ccc93a5d57a947ddfaef32dbce54a68151ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=B1=BC=E5=BC=80=E5=8F=91?= Date: Tue, 16 Jun 2026 17:28:26 +0800 Subject: [PATCH] docs: update release-guide and app-update-system for v1.9.1 --- docs/app-update-system.md | 151 +++++++++++++++++++++++++------------- docs/release-guide.md | 65 ++++++++++++---- 2 files changed, 152 insertions(+), 64 deletions(-) diff --git a/docs/app-update-system.md b/docs/app-update-system.md index 6e6558a..202cb55 100644 --- a/docs/app-update-system.md +++ b/docs/app-update-system.md @@ -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: 平台类型 - - Returns: - 包含下载 URL、文件大小和哈希的响应 + 统一下载入口:自动匹配最新版本和当前环境安装包。 + + 优先级: + 1. 查询参数 `target` + `arch`(最可靠,推荐应用内使用) + 2. `User-Agent` 解析(兜底,适合网页/文档中的固定链接) + + 匹配规则: + - 返回 302 重定向到七牛云上的安装包地址 + - 优先返回用户安装包(`.dmg` / `.exe` / `.msi` / `.AppImage`) + - 其次返回 updater 用的 `.app.tar.gz` + - 同一平台若找不到精确架构,会兜底返回同平台的其他架构包 """ - # 查询版本信息 + # 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="无法识别您的操作系统,请通过官网或应用商店下载对应版本", + ) + platform, architecture = parsed + + # 2. 查询最新版本 result = await db.execute( - select(AppRelease).where(AppRelease.version == version) + select(AppRelease).order_by(AppRelease.release_date.desc()).limit(1) ) - release = result.scalar_one_or_none() - - if not release: + 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() - - if not package: + platform_pkgs = list(result.scalars().all()) + + 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 - - # 记录下载日志(可选,不阻塞主流程) - download_log = UpdateDownload( - release_id=release.id, - platform=platform, - app_version=version - ) - db.add(download_log) - - await db.commit() - - return DownloadResponse( - download_url=package.file_url, - file_size=package.file_size, - file_hash=package.file_hash - ) + + # 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 + + 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) + + return RedirectResponse(url=package.file_url) + + +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( diff --git a/docs/release-guide.md b/docs/release-guide.md index a3fe0af..a1837b9 100644 --- a/docs/release-guide.md +++ b/docs/release-guide.md @@ -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`) |