GitHub 加速
+ +用于控制台自身的版本检测与在线更新,保存后立即生效。
+ + +diff --git a/pyproject.toml b/pyproject.toml index 08c3a4b..a0eddc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "nonebot-plugin-mimo-console" -version = "0.1.2" +version = "0.1.3" description = "A glass-style WebUI management console for NoneBot2" readme = "README.md" requires-python = ">=3.10" @@ -54,7 +54,7 @@ requires = ["uv_build>=0.10.0,<0.12.0"] build-backend = "uv_build" [tool.bumpversion] -current_version = "0.1.2" +current_version = "0.1.3" commit = true message = "chore: 发布 v{new_version}" tag = true diff --git a/src/nonebot_plugin_mimo_console/api.py b/src/nonebot_plugin_mimo_console/api.py index d179977..405a6a2 100644 --- a/src/nonebot_plugin_mimo_console/api.py +++ b/src/nonebot_plugin_mimo_console/api.py @@ -7,6 +7,7 @@ from collections import defaultdict, deque from typing import Annotated, Any, Literal +import httpx from fastapi import APIRouter, Depends, HTTPException, Query, Request, UploadFile, status from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer @@ -19,7 +20,14 @@ from .security import AuthError, Session from .state import ConsoleState from .store import StoreError, _clean_output, build_self_update_command -from .version import PACKAGE_GIT_URL, PACKAGE_NAME, get_installed_version +from .version import ( + GITHUB_PROXY_PRESETS, + PACKAGE_NAME, + get_installed_version, + normalize_github_proxy, + resolve_git_url, + resolve_version_url, +) class SetupBody(BaseModel): @@ -45,6 +53,10 @@ class PluginActionBody(BaseModel): action: Literal["install", "update", "uninstall"] +class GithubProxyBody(BaseModel): + proxy: str = Field(default="", max_length=512) + + class AttemptLimiter: def __init__(self, limit: int = 10, window: int = 300) -> None: self.limit = limit @@ -312,9 +324,65 @@ async def system_version( session: Annotated[Session, Depends(require_session)], force: bool = Query(default=False), ) -> dict[str, Any]: - await state.release_cache.fetch(force=force) + await state.release_cache.fetch(force=force, proxy=state.config.mimo_console_github_proxy) return state.release_cache.snapshot(get_installed_version()) + @router.get("/api/system/github-proxy") + async def get_github_proxy( + session: Annotated[Session, Depends(require_session)], + ) -> dict[str, Any]: + return { + "proxy": state.config.mimo_console_github_proxy, + "presets": list(GITHUB_PROXY_PRESETS), + } + + @router.put("/api/system/github-proxy") + async def set_github_proxy( + body: GithubProxyBody, + session: Annotated[Session, Depends(require_session)], + ) -> dict[str, Any]: + try: + proxy = normalize_github_proxy(body.proxy) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + environment = str(getattr(get_driver().config, "environment", "prod")) + path = locate_env_file(state.config.project_root(), environment) + try: + await asyncio.to_thread( + update_env, path, {"MIMO_CONSOLE_GITHUB_PROXY": proxy}, state.backup_dir + ) + except (OSError, ValueError) as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + # 热更新内存配置,无需重启即可生效 + state.config.mimo_console_github_proxy = proxy + logger.info(f"[Mimo Console] GitHub 加速已设置为:{proxy or '直连'}") + return {"ok": True, "proxy": proxy} + + @router.post("/api/system/github-proxy/test") + async def test_github_proxy( + body: GithubProxyBody, + session: Annotated[Session, Depends(require_session)], + ) -> dict[str, Any]: + try: + proxy = normalize_github_proxy(body.proxy) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + url = resolve_version_url(proxy) + started = time.perf_counter() + try: + async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client: + response = await client.get(url, headers={"User-Agent": PACKAGE_NAME}) + latency = int((time.perf_counter() - started) * 1000) + except (httpx.HTTPError, OSError) as exc: + return {"ok": False, "latency_ms": None, "detail": f"连接失败:{exc}"} + if response.status_code != 200: + return { + "ok": False, + "latency_ms": latency, + "detail": f"加速地址返回 HTTP {response.status_code}", + } + return {"ok": True, "latency_ms": latency, "detail": ""} + @router.post("/api/system/update") async def system_update( session: Annotated[Session, Depends(require_session)], @@ -324,7 +392,7 @@ async def system_update( command = build_self_update_command( state.config.project_root(), PACKAGE_NAME, - PACKAGE_GIT_URL, + resolve_git_url(state.config.mimo_console_github_proxy), ) env = os.environ.copy() env.update({"NO_COLOR": "1", "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"}) diff --git a/src/nonebot_plugin_mimo_console/config.py b/src/nonebot_plugin_mimo_console/config.py index 73b1a47..a7a693d 100644 --- a/src/nonebot_plugin_mimo_console/config.py +++ b/src/nonebot_plugin_mimo_console/config.py @@ -4,6 +4,8 @@ from pydantic import BaseModel, Field, field_validator +from .version import normalize_github_proxy + class ConsoleConfig(BaseModel): """NoneBot 环境变量中的控制台配置。""" @@ -16,6 +18,7 @@ class ConsoleConfig(BaseModel): mimo_console_store_cache_seconds: int = Field(default=600, ge=60, le=86400) mimo_console_package_timeout: int = Field(default=300, ge=60, le=1800) mimo_console_background_url: str | None = None + mimo_console_github_proxy: str = "" @field_validator("mimo_console_path") @classmethod @@ -23,5 +26,10 @@ def normalize_path(cls, value: str) -> str: path = "/" + value.strip().strip("/") return path if path != "/" else "/mimo-console" + @field_validator("mimo_console_github_proxy") + @classmethod + def normalize_proxy(cls, value: str) -> str: + return normalize_github_proxy(value) + def project_root(self) -> Path: return (self.mimo_console_project_root or Path.cwd()).expanduser().resolve() diff --git a/src/nonebot_plugin_mimo_console/static/assets/app.js b/src/nonebot_plugin_mimo_console/static/assets/app.js index acaa0de..d708aa7 100644 --- a/src/nonebot_plugin_mimo_console/static/assets/app.js +++ b/src/nonebot_plugin_mimo_console/static/assets/app.js @@ -28,6 +28,7 @@ const state = { background: { source: "default", url: "" }, theme: null, version: null, + proxy: { proxy: "", presets: [] }, }; const $ = (selector, context = document) => context.querySelector(selector); @@ -186,6 +187,8 @@ function bindEvents() { $("#theme-reset").addEventListener("click", resetTheme); $("#restart-button").addEventListener("click", restartNonebot); $("#check-update-btn").addEventListener("click", checkUpdate); + $("#proxy-save").addEventListener("click", saveProxySettings); + $("#proxy-test-btn").addEventListener("click", testProxySettings); $$(".view-toggle button").forEach((button) => button.addEventListener("click", () => { $$(".view-toggle button").forEach((item) => item.classList.remove("active")); button.classList.add("active"); @@ -242,7 +245,7 @@ function enterApp(username) { clearTimers(); state.timers.push(setInterval(() => loadDashboard(false), 5000)); state.timers.push(setInterval(loadLogs, 2000)); - Promise.allSettled([loadDashboard(), loadPlugins(), loadLogs(), loadBackground(), loadVersion()]); + Promise.allSettled([loadDashboard(), loadPlugins(), loadLogs(), loadBackground(), loadVersion(), loadProxySettings()]); } async function signOut(requestLogout) { @@ -1146,6 +1149,106 @@ function renderVersion(data) { $("#version-current").textContent = data.current ? `v${data.current}` : "--"; } +const PROXY_CUSTOM_VALUE = "__custom__"; + +function presetLabel(url) { + return url.includes("cnb.cool") ? "CNB 镜像仓库" : "GitHub 加速代理"; +} + +async function loadProxySettings() { + try { + state.proxy = await api("/system/github-proxy"); + renderProxySettings(); + } catch (_) { /* 读取失败不影响使用 */ } +} + +function renderProxySettings() { + const container = $("#proxy-modes"); + if (!container) return; + const presets = state.proxy.presets || []; + const current = state.proxy.proxy || ""; + const options = [ + { value: "", title: "不使用 GitHub 加速", desc: "直连 GitHub" }, + ...presets.map((url) => ({ value: url, title: url, desc: presetLabel(url) })), + { value: PROXY_CUSTOM_VALUE, title: "自定义", desc: "填入自定义加速前缀或镜像仓库地址" }, + ]; + container.innerHTML = options + .map( + (opt) => ``, + ) + .join(""); + const mode = current === "" ? "" : presets.includes(current) ? current : PROXY_CUSTOM_VALUE; + $$('input[name="gh-proxy"]', container).forEach((input) => { + input.checked = input.value === mode; + }); + $("#proxy-custom-field").classList.toggle("hidden", mode !== PROXY_CUSTOM_VALUE); + if (mode === PROXY_CUSTOM_VALUE) $("#proxy-custom-input").value = current; + container.onchange = () => { + const checked = $('input[name="gh-proxy"]:checked', container); + $("#proxy-custom-field").classList.toggle( + "hidden", + !checked || checked.value !== PROXY_CUSTOM_VALUE, + ); + }; + updateProxyStatus(); +} + +function selectedProxyValue() { + const checked = $('input[name="gh-proxy"]:checked'); + if (!checked) return ""; + if (checked.value === PROXY_CUSTOM_VALUE) return ($("#proxy-custom-input").value || "").trim(); + return checked.value; +} + +function updateProxyStatus() { + const current = state.proxy.proxy || ""; + $("#proxy-status").textContent = current ? `当前:${current}` : "当前:直连"; +} + +async function saveProxySettings() { + const button = $("#proxy-save"); + button.disabled = true; + try { + const data = await api("/system/github-proxy", { + method: "PUT", + body: JSON.stringify({ proxy: selectedProxyValue() }), + }); + state.proxy.proxy = data.proxy || ""; + renderProxySettings(); + toast("GitHub 加速设置已保存,立即生效"); + } catch (error) { + toast(error.message, "error"); + } finally { + button.disabled = false; + } +} + +async function testProxySettings() { + const button = $("#proxy-test-btn"); + const original = button.textContent; + button.disabled = true; + button.textContent = "测试中…"; + try { + const data = await api("/system/github-proxy/test", { + method: "POST", + body: JSON.stringify({ proxy: selectedProxyValue() }), + }); + if (data.ok) { + toast(`连通正常(${data.latency_ms}ms)`); + } else { + toast(data.detail || "连接失败", "error"); + } + } catch (error) { + toast(error.message, "error"); + } finally { + button.disabled = false; + button.textContent = original; + } +} + async function checkUpdate() { const button = $("#check-update-btn"); const original = button.textContent; diff --git a/src/nonebot_plugin_mimo_console/static/assets/styles.css b/src/nonebot_plugin_mimo_console/static/assets/styles.css index 7575a69..fbcb826 100644 --- a/src/nonebot_plugin_mimo_console/static/assets/styles.css +++ b/src/nonebot_plugin_mimo_console/static/assets/styles.css @@ -770,6 +770,13 @@ a { color: inherit; text-decoration: none; } .bot-chip strong { display: block; font-size: 12px; } .bot-chip span, .empty-mini { color: var(--muted); font-size: 11px; } .recent-panel { margin-top: 12px; } +.proxy-panel { margin-top: 12px; padding: 22px; } +.proxy-panel .proxy-hint { margin: -8px 0 16px; font-size: 13px; line-height: 1.6; } +.proxy-panel .bg-modes { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); } +.proxy-panel .bg-mode-text { min-width: 0; } +.proxy-panel .bg-mode-text strong { overflow-wrap: anywhere; word-break: break-all; line-height: 1.45; } +.proxy-panel .bg-actions { align-items: center; } +.proxy-panel #proxy-status { font-size: 12px; overflow-wrap: anywhere; } .recent-logs { overflow: hidden; border: 1px solid var(--line); @@ -1719,6 +1726,7 @@ a { color: inherit; text-decoration: none; } .metric, .dash-grid > .panel, .recent-panel, +.proxy-panel, .toolbar, .tabs, .plugin-card, diff --git a/src/nonebot_plugin_mimo_console/static/index.html b/src/nonebot_plugin_mimo_console/static/index.html index 6ef8a54..a52b7ea 100644 --- a/src/nonebot_plugin_mimo_console/static/index.html +++ b/src/nonebot_plugin_mimo_console/static/index.html @@ -23,7 +23,7 @@ - +
@@ -115,7 +115,7 @@用于控制台自身的版本检测与在线更新,保存后立即生效。
+ + +