Skip to content

Commit 5096cbf

Browse files
committed
sync: 版本发布体系/自动更新检查/Changelog/v2.5.0
1 parent d6321a7 commit 5096cbf

4 files changed

Lines changed: 180 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
# 更新日志
22

3-
## Release 2026-05-22 (v2.5.0)
3+
## Release 2026-05-25 (v2.5.0)
44

5-
### 🔧 优化
6-
- **ScrollableFrame 通用组件**: 提取 12+ 处重复的 Canvas + Scrollbar + Frame 模式到 `ui/scrollable_frame.py`,统一 40 行实现,`FinetunePanel` / `TrainingPanel` / `SettingsWindow` / `Dialogs` / `milestone_stats` / `trending_discovery` / `entry_tab` 共 7 个文件受益
7-
- **BaseTrainingPanel 基类**: 提取 `TrainingPanel``FinetunePanel` 共享逻辑到 `ui/training_base.py`,包含 `TrainingMonitor` 训练质量监控和 `BaseTrainingPanel` 通用训练生命周期管理
5+
### ✨ 新功能
6+
- **20 种新预测算法** (83 → 103):
7+
- 时间序列: NARX外生自回归、MSTL多重季节分解、TBATS季节分解、GARCH波动率
8+
- 深度学习: TIDE稠密编码器、TSMixer MLP混合器、DeepAR概率自回归、Chronos零样本、Mamba S6状态空间、iTransformer倒置、SCINet卷积交互、TimesFM谷歌、Time-MoE专家混合
9+
- 统计模型: DTW-kNN类比预测
10+
- 集成模型: NGBoost自然梯度提升、TabNet注意力特征网络
11+
- 高级分析: 频域分解、SIRD传染病传播模型、CausalImpact因果推断、层级贝叶斯
12+
- **9 个新 Torch 模型**: 全部新深度学习算法实现 PyTorch 模型,接入 `try_torch_predict` 降级链
13+
- **DirectML 推理加速**: 支持 Intel NPU (AI Boost) / GPU 通过 DirectML 运行 PyTorch 推理
14+
- **CUDA 冒烟测试**: `get_device()` 自动验证 GPU 实际可用,失败降级 CPU
15+
- **自动更新检查**: 启动时异步检测 GitHub Release,弹窗展示 changelog
816

9-
### 📐 架构
10-
- **代码消除**: 移除约 90 行重复的滚动容器样板代码
11-
- **单一职责**: `ScrollableFrame` 封装了 Canvas 的创建、Scrollbar 绑定、鼠标滚轮支持、`<Configure>` 自适应和 `inner` 属性访问,调用方只需 3 行即可获得完整可滚动容器
17+
### 🐛 修复
18+
- `change_point_detection.py`: 斜率计算改用 `np.polyfit` 避免大数溢出
19+
- `hf_loader.py`: transformers 元数据异常时正确降级
20+
- `main_gui.py`: 移除不存在的 `open_algorithm_comparison` 调用
21+
22+
### 📚 文档
23+
- README 更新至 103 种算法,新增 DirectML/XPU/NPU 安装指南
24+
- ALGORITHMS.md 新增 20 种算法详细说明 + 14 篇参考论文
1225

1326
## Release 2026-05-20 (v2.4.0)
1427

__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
- exports/: 导出目录
2626
"""
2727

28-
__version__ = "2.4.0"
28+
__version__ = "2.5.0"
2929
__author__ = "Bilibili Monitor Team"
3030

3131
from config import PROJECT_ROOT, DATA_DIR, COVER_DIR, EXPORT_DIR

ui/main_gui.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ def __init__(self, root=None):
126126
self._schedule_daily_push()
127127
self._start_auto_refresh()
128128
self._file_logger.start_midnight_checker(self.root)
129+
# 异步检查更新
130+
self.root.after(3000, self._check_update)
129131

130132
def _set_window_icon(self):
131133
try:
@@ -964,6 +966,63 @@ def _schedule_daily_push(self):
964966
self.root.after(delay_ms, self._daily_push)
965967
logger.info("已安排每日推送: %s", target.strftime("%Y-%m-%d %H:%M"))
966968

969+
def _check_update(self):
970+
"""异步检查 GitHub Release 更新,含 changelog 展示"""
971+
from utils.update_checker import check_for_update_async, format_changelog_for_display
972+
973+
def _on_result(has_update, latest, url, changelog):
974+
if has_update and latest:
975+
from __init__ import __version__
976+
self.root.after(0, lambda: self._sb("status", f"发现新版本 v{latest} (当前 v{__version__})", C["warning"]))
977+
logger.info("有新版本可用: v%s (当前 v%s), %s", latest, __version__, url)
978+
self.root.after(0, lambda: self._show_update_dialog(latest, __version__, url, changelog))
979+
980+
check_for_update_async(_on_result)
981+
982+
def _show_update_dialog(self, latest, current, url, changelog):
983+
"""显示更新弹窗(含 changelog)"""
984+
from utils.update_checker import format_changelog_for_display
985+
import webbrowser
986+
987+
dlg = tk.Toplevel(self.root)
988+
dlg.title("发现新版本")
989+
dlg.configure(bg=C["bg_base"])
990+
dlg.resizable(True, True)
991+
dlg.geometry("600x450")
992+
dlg.transient(self.root)
993+
dlg.grab_set()
994+
995+
# 标题
996+
tk.Label(dlg, text=f"新版本 v{latest} 可用!", font=("Microsoft YaHei UI", 14, "bold"),
997+
bg=C["bg_base"], fg=C["text_1"]).pack(pady=(16, 4))
998+
tk.Label(dlg, text=f"当前版本: v{current}", font=("Microsoft YaHei UI", 10),
999+
bg=C["bg_base"], fg=C["text_3"]).pack(pady=(0, 12))
1000+
1001+
# Changelog 区域
1002+
frame = tk.Frame(dlg, bg=C["bg_elevated"], highlightthickness=1, highlightbackground=C["border_sub"])
1003+
frame.pack(fill=tk.BOTH, expand=True, padx=16, pady=(0, 12))
1004+
1005+
tk.Label(frame, text="更新内容", font=("Microsoft YaHei UI", 10, "bold"),
1006+
bg=C["bg_elevated"], fg=C["text_2"]).pack(anchor="w", padx=8, pady=(8, 4))
1007+
1008+
text = tk.Text(frame, wrap=tk.WORD, font=("Consolas", 9),
1009+
bg=C["bg_surface"], fg=C["text_1"],
1010+
relief=tk.FLAT, borderwidth=0, padx=8, pady=8)
1011+
text.pack(fill=tk.BOTH, expand=True, padx=8, pady=(0, 8))
1012+
text.insert("1.0", format_changelog_for_display(changelog))
1013+
text.config(state=tk.DISABLED)
1014+
1015+
# 滚动条
1016+
scroll = tk.Scrollbar(text, command=text.yview)
1017+
scroll.pack(side=tk.RIGHT, fill=tk.Y)
1018+
text.config(yscrollcommand=scroll.set)
1019+
1020+
# 按钮
1021+
btn_frame = tk.Frame(dlg, bg=C["bg_base"])
1022+
btn_frame.pack(fill=tk.X, padx=16, pady=(0, 16))
1023+
ttk.Button(btn_frame, text="前往下载", command=lambda: (webbrowser.open(url), dlg.destroy())).pack(side=tk.RIGHT, padx=(8, 0))
1024+
ttk.Button(btn_frame, text="稍后提醒", command=dlg.destroy).pack(side=tk.RIGHT)
1025+
9671026
def _daily_push(self):
9681027
"""每日 23:50 自动推送日报"""
9691028
from core.notification import notification_manager

utils/update_checker.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
自动更新检查
3+
启动时异步检查 GitHub Release,有新版本时提示更新,含 changelog 展示
4+
"""
5+
6+
import logging
7+
import json
8+
import threading
9+
from typing import Optional, Tuple
10+
from datetime import datetime, timedelta
11+
from pathlib import Path
12+
13+
import requests
14+
15+
from config import DATA_DIR
16+
17+
logger = logging.getLogger(__name__)
18+
19+
GITHUB_API = "https://api.github.com/repos/jinyiwei2012/bilivideo_monitor/releases/latest"
20+
CACHE_FILE = Path(DATA_DIR) / ".update_cache.json"
21+
CACHE_TTL = timedelta(hours=24)
22+
23+
24+
def _get_local_version() -> str:
25+
try:
26+
from __init__ import __version__
27+
return __version__
28+
except Exception:
29+
return "0.0.0"
30+
31+
32+
def _load_cache() -> Optional[dict]:
33+
try:
34+
if CACHE_FILE.exists():
35+
data = json.loads(CACHE_FILE.read_text(encoding="utf-8"))
36+
cached_time = datetime.fromisoformat(data.get("cached_at", "2000-01-01"))
37+
if datetime.now() - cached_time < CACHE_TTL:
38+
return data
39+
except Exception:
40+
pass
41+
return None
42+
43+
44+
def _save_cache(data: dict):
45+
try:
46+
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
47+
data["cached_at"] = datetime.now().isoformat()
48+
CACHE_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
49+
except Exception as e:
50+
logger.debug("保存更新缓存失败: %s", e)
51+
52+
53+
def check_for_update() -> Tuple[bool, str, str, str]:
54+
"""检查更新。返回 (has_update, latest_version, download_url, changelog)"""
55+
cached = _load_cache()
56+
if cached:
57+
latest = cached.get("latest_version", "")
58+
local = _get_local_version()
59+
if latest:
60+
return latest != local, latest, cached.get("download_url", ""), cached.get("changelog", "")
61+
62+
try:
63+
resp = requests.get(GITHUB_API, timeout=10)
64+
if resp.status_code != 200:
65+
logger.debug("GitHub API 返回 %s", resp.status_code)
66+
return False, "", "", ""
67+
68+
data = resp.json()
69+
latest = data.get("tag_name", "").lstrip("v")
70+
download_url = data.get("html_url", "")
71+
changelog = data.get("body", "")
72+
73+
_save_cache({
74+
"latest_version": latest,
75+
"download_url": download_url,
76+
"changelog": changelog,
77+
})
78+
79+
local = _get_local_version()
80+
return latest != local, latest, download_url, changelog
81+
82+
except requests.RequestException as e:
83+
logger.debug("GitHub API 请求失败: %s", e)
84+
return False, "", "", ""
85+
86+
87+
def check_for_update_async(callback):
88+
"""异步检查更新,完成后调用 callback(has_update, latest_version, download_url, changelog)"""
89+
threading.Thread(target=lambda: callback(*check_for_update()), daemon=True).start()
90+
91+
92+
def format_changelog_for_display(changelog: str, max_lines: int = 30) -> str:
93+
"""截取 changelog 前 max_lines 行用于 UI 展示"""
94+
if not changelog:
95+
return "暂无更新说明"
96+
lines = changelog.strip().split("\n")
97+
display = "\n".join(lines[:max_lines])
98+
if len(lines) > max_lines:
99+
display += f"\n\n... 还有 {len(lines) - max_lines} 行,请前往 GitHub 查看完整内容"
100+
return display

0 commit comments

Comments
 (0)