1 Commits
Author SHA1 Message Date
Aimili 97fc3374f1 fix: refill nodes after exhausted failover 2026-08-28 06:43:02 +08:00
6 changed files with 36 additions and 21 deletions
+2 -2
View File
@@ -7,9 +7,9 @@ on:
workflow_dispatch:
inputs:
release_tag:
description: Existing formal tag to publish, for example v2.1.4
description: Existing formal tag to publish, for example v2.1.5
required: true
default: v2.1.4
default: v2.1.5
type: string
concurrency:
+9 -6
View File
@@ -1,12 +1,15 @@
# AimiliVPN V2.1.4 正式版
# AimiliVPN V2.1.5 正式版
V2.1.4 包含 V2.1.3 的全部稳定性、安全性和节点识别改进,并修复正式版重新安装验收中发现的手动断开状态遗漏。
V2.1.5 包含 V2.1.4 的全部稳定性、安全性和节点识别改进,并修复正式版重新安装验收中发现的失败切换恢复遗漏。
## V2.1.4 热修复
## V2.1.5 热修复
- 修复点击“断开连接”后 OpenVPN、活动节点和 table 100 已清理,但 Web API 的 `tunnel_ready` / `proxy_ready` 仍短暂保留为 `true` 的问题。
- 手动断开现在统一复用完整连接状态清理流程;活动节点、连接中状态、隧道、代理出口、出口 IP 与路由规则会同步归零。
- 新增手动断开状态回归测试,并在真实 VPS 上完成断开与重新连接复验。
- 修复手动切换失败、原节点恢复失败且连续 3 个备用节点也失效后,日志提示后台重新加载但实际没有启动补位任务的问题。
- 备用节点耗尽后现在会启动唯一后台节点补齐线程,按退避间隔重新同步并尝试连接,避免一直等到下一轮周期采集。
- 新增自动切换耗尽回归测试,并在真实 VPS 上验证补位线程会创建且不会重复启动。
## IP 类型识别
@@ -37,7 +40,7 @@ V2.1.4 包含 V2.1.3 的全部稳定性、安全性和节点识别改进,并
## 验证与兼容性
- 本地 Python 单元测试 52/52 通过,Python 编译、Dashboard JavaScript 语法、安装脚本语法和补丁格式检查通过。
- 本地 Python 单元测试 53/53 通过,Python 编译、Dashboard JavaScript 语法、安装脚本语法和补丁格式检查通过。
- Ubuntu 22.04 x86_64 VPS 已完成真实卸载、重新安装、节点源、连接切换、代理出口、Web API、权限和路由残留验证。
- 发布流水线会在 Python 3.9、3.11、3.13 上运行完整测试。
- Docker 会分别构建验证 `linux/amd64``linux/386``linux/arm64``linux/arm/v7`,并发布统一多架构镜像清单。
@@ -50,8 +53,8 @@ V2.1.4 包含 V2.1.3 的全部稳定性、安全性和节点识别改进,并
## 下载与更新
- GitHub Release`aimilivpn-v2.1.4-linux-source.tar.gz``sha256sums.txt`
- GHCR`2.1.4``2.1``latest` 多架构镜像标签。
- GitHub Release`aimilivpn-v2.1.5-linux-source.tar.gz``sha256sums.txt`
- GHCR`2.1.5``2.1``latest` 多架构镜像标签。
Python 源码安装更新:
+1 -1
View File
@@ -1 +1 @@
2.1.4
2.1.5
+2 -2
View File
@@ -204,9 +204,9 @@ def get_app_version():
try:
with open(os.path.join(INSTALL_DIR, "VERSION"), "r", encoding="utf-8") as f:
version = f.read().strip().lstrip("vV")
return version or "2.1.4"
return version or "2.1.5"
except Exception:
return "2.1.4"
return "2.1.5"
def load_ui_cfg():
import json
+16 -6
View File
@@ -631,6 +631,16 @@ class ManagerLogicTests(unittest.TestCase):
connect_mock.assert_called_once_with("old-node")
auto_switch_mock.assert_not_called()
def test_auto_switch_exhaustion_schedules_background_refill(self) -> None:
with (
mock.patch.object(manager, "schedule_background_refill", return_value=True) as schedule_mock,
mock.patch.object(manager, "log_to_json") as log_mock,
):
manager.auto_switch_node(attempt=3)
schedule_mock.assert_called_once_with()
log_mock.assert_called_once_with("INFO", "Main", "连续自动切换失败,已启动唯一后台节点补齐任务")
def test_physical_interface_detection_is_cached(self) -> None:
original_cache = manager.vpn_utils.physical_interface_cache
manager.vpn_utils.physical_interface_cache = (None, 0.0)
@@ -828,8 +838,8 @@ class ManagerLogicTests(unittest.TestCase):
self.assertEqual(519, entries[-1]["index"])
def test_web_update_controls_only_expose_stable_main_channel(self) -> None:
self.assertEqual("2.1.4", manager.APP_VERSION)
self.assertEqual("V2.1.4 正式版", manager.APP_VERSION_LABEL)
self.assertEqual("2.1.5", manager.APP_VERSION)
self.assertEqual("V2.1.5 正式版", manager.APP_VERSION_LABEL)
self.assertIn("检测更新", manager.INDEX_HTML)
self.assertIn("/api/check_update", manager.INDEX_HTML)
self.assertIn("/tree/main", manager.INDEX_HTML)
@@ -874,7 +884,7 @@ class ManagerLogicTests(unittest.TestCase):
def test_release_workflow_uses_full_patch_version(self) -> None:
workflow_text = (manager.ROOT_DIR / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8")
self.assertIn("default: v2.1.4", workflow_text)
self.assertIn("default: v2.1.5", workflow_text)
self.assertIn("AimiliVPN V$(tr -d '\\r\\n' < VERSION) 正式版", workflow_text)
self.assertNotIn("cut -d. -f1,2 VERSION", workflow_text)
@@ -901,8 +911,8 @@ class ManagerLogicTests(unittest.TestCase):
def test_latest_release_check_reports_current_formal_version(self) -> None:
release = {
"tag_name": "v2.1.4",
"name": "AimiliVPN V2.1.4 正式版",
"tag_name": "v2.1.5",
"name": "AimiliVPN V2.1.5 正式版",
"draft": False,
"prerelease": False,
}
@@ -910,7 +920,7 @@ class ManagerLogicTests(unittest.TestCase):
result = manager.check_latest_release()
self.assertFalse(result["update_available"])
self.assertEqual("V2.1.4 正式版", result["current_version_label"])
self.assertEqual("V2.1.5 正式版", 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}
+6 -4
View File
@@ -141,7 +141,7 @@ UPDATE_COMMAND = (
)
ROOT_DIR = Path(sys.executable).resolve().parent if globals().get("__compiled__") else Path(__file__).resolve().parent
DEFAULT_APP_VERSION = "2.1.4"
DEFAULT_APP_VERSION = "2.1.5"
try:
_version_text = (ROOT_DIR / "VERSION").read_text(encoding="utf-8").strip()
except OSError:
@@ -2122,6 +2122,8 @@ def schedule_background_refill() -> bool:
def auto_switch_node(attempt: int = 0) -> None:
if attempt >= 3:
print("[自动切换] 连续切换失败已达 3 次,停止切换以防止主线程死锁,将在后台重新加载节点...", flush=True)
if schedule_background_refill():
log_to_json("INFO", "Main", "连续自动切换失败,已启动唯一后台节点补齐任务")
return
ui_cfg = load_ui_config()
@@ -4165,12 +4167,12 @@ INDEX_HTML = r"""<!doctype html>
<div class="dropdown">
<button id="github_btn" class="btn-primary" type="button" aria-expanded="false" aria-controls="github_dropdown" style="background: rgba(255, 255, 255, 0.08); border: 1px solid var(--border-color); color: var(--text-primary);">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" style="vertical-align: middle; margin-right: 4px;"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
<span id="github_version_label">V2.1.4 正式版</span>
<span id="github_version_label">V2.1.5 正式版</span>
<svg xmlns="http://www.w3.org/2000/svg" style="width:12px; height:12px; margin-left: 2px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
</button>
<div id="github_dropdown" class="dropdown-content github-dropdown">
<div class="version-current">
<div id="current_version_label" class="version-current-label">V2.1.4 正式版</div>
<div id="current_version_label" class="version-current-label">V2.1.5 正式版</div>
<div id="deployment_mode_label" class="version-current-meta">Python 源码部署 · 更新通道main</div>
</div>
<button id="check_update_btn" type="button" onclick="checkForUpdate(event)">
@@ -4997,7 +4999,7 @@ function stableSortNodes() {
}
function render(){
const versionLabel = state.app_version_label || "V2.1.4 正式版";
const versionLabel = state.app_version_label || "V2.1.5 正式版";
if ($("github_version_label")) $("github_version_label").textContent = versionLabel;
if ($("current_version_label")) $("current_version_label").textContent = versionLabel;
if ($("deployment_mode_label")) {