Refine V2.1 source and Docker release

This commit is contained in:
Aimili
2026-08-26 23:42:00 +08:00
parent a9cf220500
commit 92910ab2d6
9 changed files with 209 additions and 97 deletions
+81 -22
View File
@@ -4,11 +4,25 @@ on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
release_tag:
description: Existing formal tag to publish, for example v2.1.0
required: true
default: v2.1.0
type: string
concurrency:
group: formal-release-${{ inputs.release_tag || github.ref_name }}
cancel-in-progress: false
permissions:
contents: write
packages: write
env:
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
jobs:
test:
name: Test Python ${{ matrix.python-version }}
@@ -19,12 +33,14 @@ jobs:
python-version: ["3.9", "3.11", "3.13"]
steps:
- uses: actions/checkout@v7
with:
ref: ${{ env.RELEASE_TAG }}
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Verify version tag
- name: Verify formal version tag
shell: bash
run: test "${GITHUB_REF_NAME}" = "v$(tr -d '\r\n' < VERSION)"
run: test "${RELEASE_TAG}" = "v$(tr -d '\r\n' < VERSION)"
- name: Compile Python sources
run: python -m py_compile vpngate_manager.py vpn_utils.py proxy_server.py snapshot_utils.py
- name: Validate installation script
@@ -34,36 +50,53 @@ jobs:
- name: Run unit tests
run: python -m unittest discover -s tests -v
release:
name: Build Linux release archives
docker-smoke:
name: Smoke test Docker ${{ matrix.slug }}
needs: test
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
slug: amd64
- platform: linux/386
slug: 386
- platform: linux/arm64
slug: arm64
- platform: linux/arm/v7
slug: armv7
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Build architecture-labelled archives
run: python scripts/build_release_archives.py --output-dir dist
- name: Create GitHub formal release
env:
GH_TOKEN: ${{ github.token }}
ref: ${{ env.RELEASE_TAG }}
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- name: Build architecture image for smoke test
uses: docker/build-push-action@v7
with:
context: .
load: true
platforms: ${{ matrix.platform }}
build-args: BUILD_VERSION=${{ env.RELEASE_TAG }}
tags: aimilivpn-smoke:${{ matrix.slug }}
cache-from: type=gha,scope=smoke-${{ matrix.slug }}
cache-to: type=gha,mode=max,scope=smoke-${{ matrix.slug }}
- name: Verify application imports in image
shell: bash
run: |
title="AimiliVPN V$(cut -d. -f1,2 VERSION) 正式版"
if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
gh release upload "${GITHUB_REF_NAME}" dist/* --clobber --repo "${GITHUB_REPOSITORY}"
gh release edit "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md
else
gh release create "${GITHUB_REF_NAME}" dist/* --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md --verify-tag
fi
docker run --rm --platform "${{ matrix.platform }}" \
"aimilivpn-smoke:${{ matrix.slug }}" \
python3 -c 'import vpngate_manager as app; print(app.APP_VERSION)'
docker:
name: Build multi-architecture Docker image
needs: test
name: Publish multi-architecture Docker image
needs: docker-smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ env.RELEASE_TAG }}
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
@@ -75,11 +108,12 @@ jobs:
id: version
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
version="${RELEASE_TAG#v}"
minor="$(printf '%s' "${version}" | cut -d. -f1,2)"
echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "minor=${minor}" >> "${GITHUB_OUTPUT}"
- uses: docker/build-push-action@v7
- name: Build and publish Docker manifest
uses: docker/build-push-action@v7
with:
context: .
push: true
@@ -93,3 +127,28 @@ jobs:
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true
release:
name: Publish universal Python source release
needs: docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ env.RELEASE_TAG }}
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Build universal Linux source archive
run: python scripts/build_release_archives.py --output-dir dist
- name: Create GitHub formal release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
title="AimiliVPN V$(cut -d. -f1,2 VERSION) 正式版"
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
gh release upload "${RELEASE_TAG}" dist/* --clobber --repo "${GITHUB_REPOSITORY}"
gh release edit "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md
else
gh release create "${RELEASE_TAG}" dist/* --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md --verify-tag
+1
View File
@@ -27,6 +27,7 @@ COPY mirror ./mirror
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
DEPLOYMENT_MODE=docker \
VPNGATE_DATA_DIR=/data \
UI_HOST=0.0.0.0 \
UI_PORT=8787 \
+32 -15
View File
@@ -27,23 +27,23 @@ V2.1 是项目启用正式版本标志后的首个稳定版本。仓库、安装
- **国家筛选**:支持带国旗和节点数量的实时多选筛选,选择范围保存到本机,并作用于手动更新和后台周期同步。
- **节点操作**:恢复单节点“检测”按钮,补齐收藏、检测、连接和断开状态逻辑。
- **镜像同步**GitHub Pages 每 15 分钟同步并校验官方节点快照,官方 API 被屏蔽时自动回退。
- **Web 更新检测**:页面顶部显示 `V2.1 正式版`可直接检查 GitHub 最新稳定 Release;只展示 `main` 和正式版下载入口
- **正式发布链路**GitHub 标签自动运行 Python 兼容测试、构建四类 Linux 发行包、生成 SHA-256 校验文件并发布多架构 Docker 镜像
- **Web 更新检测**:页面顶部显示 `V2.1 正式版`检查 GitHub 最新稳定 Release,并根据 Python 源码或 Docker 部署方式显示正确更新命令
- **正式发布链路**GitHub 标签或手动重跑会依次执行 Python 兼容测试、四架构 Docker 冒烟测试、GHCR 镜像发布,全部成功后才发布通用源码包与 SHA-256 校验文件
#### 系统与架构兼容性
| 类型 | 正式支持范围 | GitHub 发行标识 |
| 类型 | 正式支持范围 | 安装或镜像标识 |
| --- | --- | --- |
| Linux x64 | Intel/AMD 64 位 VPS | `linux-amd64` |
| Linux x86 | Intel/AMD 32 位系统 | `linux-386` |
| Linux ARM64 | AArch64、ARMv8 VPS/开发板 | `linux-arm64` |
| Linux ARM32 | ARMv7 设备 | `linux-armv7` |
| Linux x64 | Intel/AMD 64 位 VPS | 通用 Python 源码 / Docker `linux/amd64` |
| Linux x86 | Intel/AMD 32 位系统 | 通用 Python 源码 / Docker `linux/386` |
| Linux ARM64 | AArch64、ARMv8 VPS/开发板 | 通用 Python 源码 / Docker `linux/arm64` |
| Linux ARM32 | ARMv7 设备 | 通用 Python 源码 / Docker `linux/arm/v7` |
| Linux 发行版 | Debian、Ubuntu、CentOS、RHEL、Rocky、AlmaLinux、Fedora、Oracle Linux、Amazon Linux、Alpine | 使用同一正式核心 |
| Docker | Linux 主机上的 amd64、386、arm64、arm/v7 | GHCR 多架构镜像 |
> AimiliVPN 依赖 Linux 的 TUN、OpenVPN、iptables 和策略路由,因此不发布虚假的 Windows/macOS 原生兼容包。Windows 或 macOS 只能作为代理客户端使用,不能直接运行完整网关;Docker Desktop 同样不等同于具备宿主机 TUN 能力的 Linux 服务器。
项目由纯 Python 标准库组成,不需要为 CPU 编译不同的 Python 二进制。GitHub Actions 会为每种架构生成经过相同测试的正式发行包,并实际构建对应架构的 Docker 镜像。
项目由纯 Python 标准库组成,不需要为 CPU 编译不同的 Python 二进制。GitHub Release 只提供一个通用 Linux 源码包;GHCR 才会实际构建并发布四种 CPU 架构的 Docker 镜像。
---
@@ -82,21 +82,26 @@ bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/ma
[Releases 页面](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)提供以下文件:
- `aimilivpn-v2.1.0-linux-amd64.tar.gz`x64 / x86_64
- `aimilivpn-v2.1.0-linux-386.tar.gz`x86 32 位
- `aimilivpn-v2.1.0-linux-arm64.tar.gz`ARM64 / AArch64。
- `aimilivpn-v2.1.0-linux-armv7.tar.gz`ARMv7 32 位。
- `sha256sums.txt`:所有发行包的 SHA-256 校验值。
- `aimilivpn-v2.1.0-linux-source.tar.gz`适用于支持 Python 3 和项目系统依赖的 Linux x64、x86、ARM64、ARMv7 主机
- `sha256sums.txt`:源码包的 SHA-256 校验值
#### 方法三:Docker / Docker Compose
Docker 镜像地址:`ghcr.io/baoweise-bot/aimili-vpngate:2.1`。仓库中的 [`compose.yaml`](./compose.yaml) 已配置主机网络、`NET_ADMIN` 和 TUN 设备:
```bash
docker compose pull
docker compose up -d
docker logs -f aimilivpn
```
无法访问 GHCR 或需要自行审查构建过程时,也可以在 VPS 的仓库目录本地构建:
```bash
docker compose build
docker compose up -d
```
也可以直接运行:
```bash
@@ -105,12 +110,13 @@ docker run -d \
--restart unless-stopped \
--network host \
--cap-add NET_ADMIN \
--cap-add NET_RAW \
--device /dev/net/tun:/dev/net/tun \
-v aimilivpn-data:/data \
ghcr.io/baoweise-bot/aimili-vpngate:2.1
```
> Docker 方式只支持具备 `/dev/net/tun` 的 Linux 主机。管理页面默认端口为 `8787`,本机 HTTP/SOCKS5 代理默认端口为 `7928`。
> Docker 方式只支持具备 `/dev/net/tun` 的 Linux 主机,并需要 `NET_ADMIN`、`NET_RAW` 能力。管理页面默认端口为 `8787`,本机 HTTP/SOCKS5 代理默认端口为 `7928`。容器检测到新版本后会提示重新拉取镜像,不会在容器内执行 `git pull`。
---
@@ -157,7 +163,7 @@ docker run -d \
### 🛠️ 核心功能与操作说明
* **合并操作面板**:将“更新节点”与“立即检测补齐”合并,一键触发多线程拉取与测速。
* **正式版更新检测**:Web 顶部版本菜单可以检查 GitHub 最新稳定 Release,并提供 `main` 主分支和正式版下载入口
* **正式版更新检测**:Web 顶部版本菜单可以检查 GitHub 最新稳定 Release;源码部署提示 `ml update`,Docker 部署提示重新拉取并启动镜像
* **多国家发现范围**:节点表可实时勾选多个国家;点击“更新节点”后保存范围并影响后台周期拉取。
* **延迟来源区分**:实测延迟正常显示,官方 Ping 回退值使用弱化样式并标注为预估。
* **网关状态面板**
@@ -253,6 +259,17 @@ bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/ma
> 💡 **Quick Note**: Once installed, copy the printed URL from the terminal to access the Web UI. Type the `ml` command in the terminal to summon the interactive CLI management console.
#### Docker / Docker Compose
GitHub publishes prebuilt images for `linux/amd64`, `linux/386`, `linux/arm64`, and `linux/arm/v7` under `ghcr.io/baoweise-bot/aimili-vpngate:2.1`:
```bash
docker compose pull
docker compose up -d
```
To build natively on the VPS instead, run `docker compose build` before `docker compose up -d`. Docker requires a Linux host with `/dev/net/tun`, `NET_ADMIN`, and `NET_RAW` support.
---
### 💡 Quick Start Guide
+5 -4
View File
@@ -8,11 +8,12 @@ V2.1 是仅从 `main` 主分支发布的首个正式版本标志。
- 修复节点获取缓慢、连接断开和切换失败时误伤现有连接的问题。
- 恢复节点延迟列,区分本机实测值与 VPNGate 官方预估值。
- 加入国旗、实时多选国家筛选、国家范围持久化和单节点测试。
- Web 管理端加入正式版更新检测,只检查 GitHub 最新稳定 Release,并只保留 `main` 主分支入口
- Web 管理端加入正式版更新检测,只检查 GitHub 最新稳定 Release,并根据 Python 源码或 Docker 部署方式显示对应更新命令
- `install.sh``ml update` 统一只更新 `origin/main`
- GitHub Release 提供 Linux `amd64``386``arm64``armv7` 发行包与 SHA-256 校验文件。
- GHCR 提供相同四种架构的 Docker 镜像
- GitHub Release 提供一个适用于 Linux `amd64``386``arm64``armv7` 的通用 Python 源码包与 SHA-256 校验文件。
- GHCR 提供经过逐架构冒烟测试的 `amd64``386``arm64``arm/v7` Docker 镜像,并同时发布 `2.1.0``2.1``latest` 标签
- Docker 用户默认拉取 GitHub 预构建镜像,也可以使用仓库中的 Dockerfile 在 VPS 本地构建。
## 兼容范围
应用依赖 Linux TUN、OpenVPN、iptables 和策略路由,因此正式支持 Linux 主机。Docker 也必须运行在具备 `/dev/net/tun` 的 Linux 主机上,并授予 `NET_ADMIN` 能力。
应用依赖 Linux TUN、OpenVPN、iptables 和策略路由,因此正式支持 Linux 主机。Docker 也必须运行在具备 `/dev/net/tun` 的 Linux 主机上,并授予 `NET_ADMIN``NET_RAW` 能力。
+10
View File
@@ -1,13 +1,21 @@
services:
aimilivpn:
image: ghcr.io/baoweise-bot/aimili-vpngate:2.1
build:
context: .
args:
BUILD_VERSION: local
container_name: aimilivpn
network_mode: host
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
devices:
- /dev/net/tun:/dev/net/tun
environment:
DEPLOYMENT_MODE: docker
VPNGATE_DATA_DIR: /data
UI_HOST: "0.0.0.0"
UI_PORT: "8787"
@@ -16,6 +24,8 @@ services:
volumes:
- aimilivpn-data:/data
init: true
security_opt:
- no-new-privileges:true
restart: unless-stopped
volumes:
+1
View File
@@ -137,6 +137,7 @@ WorkingDirectory=${INSTALL_DIR}
ExecStart=/usr/bin/python3 vpngate_manager.py
Restart=always
RestartSec=5
Environment=DEPLOYMENT_MODE=source
EnvironmentFile=-/etc/default/aimilivpn
[Install]
+21 -54
View File
@@ -3,21 +3,13 @@ from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import tarfile
import tempfile
from pathlib import Path
TARGETS = {
"amd64": {"cpu": "x86_64 / x64", "docker_platform": "linux/amd64"},
"386": {"cpu": "x86 / i386 32-bit", "docker_platform": "linux/386"},
"arm64": {"cpu": "ARM64 / AArch64", "docker_platform": "linux/arm64"},
"armv7": {"cpu": "ARMv7 32-bit", "docker_platform": "linux/arm/v7"},
}
RELEASE_FILES = [
".dockerignore",
"VERSION",
"README.md",
"RELEASE_NOTES.md",
@@ -40,7 +32,7 @@ def sha256(path: Path) -> str:
return digest.hexdigest()
def build_archives(root: Path, output_dir: Path) -> list[Path]:
def build_archive(root: Path, output_dir: Path) -> Path:
version = (root / "VERSION").read_text(encoding="utf-8").strip()
version_parts = version.split(".")
if len(version_parts) not in (2, 3) or any(not part.isdigit() for part in version_parts):
@@ -53,63 +45,38 @@ def build_archives(root: Path, output_dir: Path) -> list[Path]:
raise FileNotFoundError("发行文件缺失: mirror")
output_dir.mkdir(parents=True, exist_ok=True)
archives: list[Path] = []
for stale_archive in output_dir.glob("aimilivpn-v*-linux-*.tar.gz"):
stale_archive.unlink()
checksum_path = output_dir / "sha256sums.txt"
checksum_path.unlink(missing_ok=True)
with tempfile.TemporaryDirectory(prefix="aimilivpn-release-") as temp_name:
temp_root = Path(temp_name)
for architecture, metadata in TARGETS.items():
package_name = f"aimilivpn-v{version}-linux-{architecture}"
package_root = temp_root / package_name
package_root.mkdir()
package_name = f"aimilivpn-v{version}-linux-source"
package_root = temp_root / package_name
package_root.mkdir()
for name in RELEASE_FILES:
shutil.copy2(root / name, package_root / name)
shutil.copytree(root / "mirror", package_root / "mirror")
build_info = {
"product": "AimiliVPN",
"version": version,
"release": f"V{'.'.join(version.split('.')[:2])} 正式版",
"branch": "main",
"operating_system": "Linux",
"architecture": architecture,
**metadata,
"supported_distributions": [
"Debian",
"Ubuntu",
"CentOS",
"RHEL",
"Rocky Linux",
"AlmaLinux",
"Fedora",
"Oracle Linux",
"Amazon Linux",
"Alpine Linux",
],
}
(package_root / "BUILD_INFO.json").write_text(
json.dumps(build_info, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
for name in RELEASE_FILES:
shutil.copy2(root / name, package_root / name)
shutil.copytree(root / "mirror", package_root / "mirror")
archive_path = output_dir / f"{package_name}.tar.gz"
with tarfile.open(archive_path, "w:gz") as archive:
archive.add(package_root, arcname=package_name)
archives.append(archive_path)
archive_path = output_dir / f"{package_name}.tar.gz"
with tarfile.open(archive_path, "w:gz") as archive:
archive.add(package_root, arcname=package_name)
checksum_lines = [f"{sha256(path)} {path.name}" for path in archives]
checksum_path = output_dir / "sha256sums.txt"
checksum_lines = [f"{sha256(archive_path)} {archive_path.name}"]
checksum_path.write_text("\n".join(checksum_lines) + "\n", encoding="ascii")
return archives
return archive_path
def main() -> int:
parser = argparse.ArgumentParser(description="构建 AimiliVPN Linux 多架构发行包")
parser = argparse.ArgumentParser(description="构建 AimiliVPN Linux 通用 Python 源码发行包")
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output-dir", type=Path, default=Path("dist"))
args = parser.parse_args()
archives = build_archives(args.root.resolve(), args.output_dir.resolve())
for archive in archives:
print(archive)
archive = build_archive(args.root.resolve(), args.output_dir.resolve())
print(archive)
return 0
+34
View File
@@ -403,6 +403,7 @@ class ManagerLogicTests(unittest.TestCase):
self.assertIn("/releases/latest", manager.INDEX_HTML)
self.assertNotIn("/tree/bate", manager.INDEX_HTML)
self.assertNotIn(">测试版<", manager.INDEX_HTML)
self.assertIn('id="deployment_mode_label"', manager.INDEX_HTML)
def test_installer_updates_only_from_main(self) -> None:
install_text = (manager.ROOT_DIR / "install.sh").read_text(encoding="utf-8")
@@ -447,6 +448,39 @@ class ManagerLogicTests(unittest.TestCase):
self.assertFalse(result["update_available"])
self.assertEqual("V2.1 正式版", result["current_version_label"])
def test_latest_release_check_reports_source_update_command(self) -> None:
release = {"tag_name": "v2.2.0", "draft": False, "prerelease": False}
with (
mock.patch.object(manager, "fetch_api_text", return_value=json.dumps(release)),
mock.patch.object(manager, "DEPLOYMENT_MODE", "source"),
mock.patch.object(manager, "DEPLOYMENT_MODE_LABEL", "Python 源码"),
mock.patch.object(manager, "UPDATE_COMMAND", "ml update"),
):
result = manager.check_latest_release()
self.assertEqual("source", result["deployment_mode"])
self.assertEqual("ml update", result["update_command"])
def test_latest_release_check_reports_docker_update_command(self) -> None:
release = {"tag_name": "v2.2.0", "draft": False, "prerelease": False}
with (
mock.patch.object(manager, "fetch_api_text", return_value=json.dumps(release)),
mock.patch.object(manager, "DEPLOYMENT_MODE", "docker"),
mock.patch.object(manager, "DEPLOYMENT_MODE_LABEL", "Docker 容器"),
mock.patch.object(
manager,
"UPDATE_COMMAND",
"docker compose pull && docker compose up -d",
),
):
result = manager.check_latest_release()
self.assertEqual("docker", result["deployment_mode"])
self.assertEqual(
"docker compose pull && docker compose up -d",
result["update_command"],
)
def test_fetch_uses_github_mirror_after_official_sources(self) -> None:
csv_text = valid_snapshot()
+24 -2
View File
@@ -117,6 +117,15 @@ LOCAL_PROXY_PORT = env_int("LOCAL_PROXY_PORT", 7928, 1, 65535)
UI_HOST = os.environ.get("UI_HOST", "::")
UI_PORT = env_int("UI_PORT", 8787, 1, 65535)
INVALID_BACKOFF_SECONDS = env_int("INVALID_BACKOFF_SECONDS", 30 * 60, 1)
DEPLOYMENT_MODE = os.environ.get("DEPLOYMENT_MODE", "source").strip().lower()
if DEPLOYMENT_MODE not in {"source", "docker"}:
DEPLOYMENT_MODE = "source"
DEPLOYMENT_MODE_LABEL = "Docker 容器" if DEPLOYMENT_MODE == "docker" else "Python 源码"
UPDATE_COMMAND = (
"docker compose pull && docker compose up -d"
if DEPLOYMENT_MODE == "docker"
else "ml update"
)
ROOT_DIR = Path(sys.executable).resolve().parent if globals().get("__compiled__") else Path(__file__).resolve().parent
DEFAULT_APP_VERSION = "2.1.0"
@@ -429,6 +438,8 @@ def get_state() -> dict[str, Any]:
state.setdefault("blacklisted_nodes", 0)
state["app_version"] = APP_VERSION
state["app_version_label"] = APP_VERSION_LABEL
state["deployment_mode"] = DEPLOYMENT_MODE
state["deployment_mode_label"] = DEPLOYMENT_MODE_LABEL
# Pre-populate settings inputs in UI
ui_cfg = load_ui_config()
@@ -765,6 +776,9 @@ def check_latest_release() -> dict[str, Any]:
"update_available": latest_version > current_version,
"release_url": release_url,
"main_branch_url": GITHUB_MAIN_BRANCH_URL,
"deployment_mode": DEPLOYMENT_MODE,
"deployment_mode_label": DEPLOYMENT_MODE_LABEL,
"update_command": UPDATE_COMMAND,
}
def is_certificate_verification_error(exc: BaseException) -> bool:
@@ -3915,7 +3929,7 @@ INDEX_HTML = r"""<!doctype html>
<div id="github_dropdown" class="dropdown-content github-dropdown">
<div class="version-current">
<div id="current_version_label" class="version-current-label">V2.1 正式版</div>
<div class="version-current-meta">唯一更新通道main 主分支</div>
<div id="deployment_mode_label" class="version-current-meta">Python 源码部署 · 更新通道main</div>
</div>
<button id="check_update_btn" type="button" onclick="checkForUpdate(event)">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" style="width:14px; height:14px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 1121.21 8H18.5" /></svg>
@@ -4634,6 +4648,10 @@ function render(){
const versionLabel = state.app_version_label || "V2.1 正式版";
if ($("github_version_label")) $("github_version_label").textContent = versionLabel;
if ($("current_version_label")) $("current_version_label").textContent = versionLabel;
if ($("deployment_mode_label")) {
const modeLabel = state.deployment_mode_label || "Python 源码";
$("deployment_mode_label").textContent = `${modeLabel}部署 · 更新通道main`;
}
const activeNodeId = state.active_openvpn_node_id;
const activeNode = nodes.find(n => n && (n.active || n.id === activeNodeId));
@@ -5166,7 +5184,11 @@ async function checkForUpdate(event) {
if (releaseLink && result.release_url) releaseLink.href = result.release_url;
if (result.update_available) {
statusBox.className = "update-check-status available";
statusBox.textContent = `发现正式版 ${result.latest_tag}当前为 ${result.current_version_label}请打开正式版下载页更新`;
if (result.deployment_mode === "docker") {
statusBox.textContent = `发现正式版 ${result.latest_tag}请在 VPS 执行${result.update_command}`;
} else {
statusBox.textContent = `发现正式版 ${result.latest_tag}请执行${result.update_command}`;
}
} else {
statusBox.className = "update-check-status current";
statusBox.textContent = `当前 ${result.current_version_label} 已是最新正式版`;