diff --git a/assets/gui/css/advanced-material-alas.css b/assets/gui/css/advanced-material-alas.css index 3eef0883b..323f7d069 100644 --- a/assets/gui/css/advanced-material-alas.css +++ b/assets/gui/css/advanced-material-alas.css @@ -31,13 +31,11 @@ 1. CSS 变量定义 ========================================== */ -/* 引入 Google Fonts 字体:Inter,包含常规、中等和半粗字重 */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'); - :root { /* === 色彩系统 === */ --alas-apple-bg: #f5f5f7; - --alas-apple-bg-image: url("https://api.yppp.net/api.php"); + /* 主题样式通过 WebSocket 内联注入,资源路径相对页面解析。 */ + --alas-apple-bg-image: url("static/assets/gui/css/bj.jpg"); --alas-apple-bg-noise: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); --alas-apple-card-bg: rgba(255, 255, 255, 0.72); --alas-apple-card-bg-solid: #ffffff; diff --git a/assets/gui/css/alas.css b/assets/gui/css/alas.css index 4deadce92..4ebbf1e8c 100644 --- a/assets/gui/css/alas.css +++ b/assets/gui/css/alas.css @@ -1,6 +1,6 @@ @font-face { font-family: 'MiSans'; - src: url('/static/assets/spa/MiSans-Demibold.ttf') format('truetype'); + src: url('../../spa/MiSans-Demibold.ttf') format('truetype'); font-weight: 450; font-style: normal; font-display: optional; @@ -8,7 +8,7 @@ @font-face { font-family: 'JetBrains Mono NL'; - src: url('/static/assets/spa/JetBrainsMonoNL-Regular.ttf') format('truetype'); + src: url('../../spa/JetBrainsMonoNL-Regular.ttf') format('truetype'); font-weight: normal; font-style: normal; } @@ -1120,7 +1120,7 @@ body.alas-login-page::after, width: 160px; height: 160px; margin: 0 auto; - background: url('/static/doc/logo.webp') center / contain no-repeat; + background: url('../../../doc/logo.webp') center / contain no-repeat; } .alas-login-page #input-container .card-body { diff --git a/assets/spa/manifest.json b/assets/spa/manifest.json index 17d6993f6..61e805e2a 100644 --- a/assets/spa/manifest.json +++ b/assets/spa/manifest.json @@ -1,20 +1,21 @@ { "name": "Alas Web UI", "short_name": "Alas", - "start_url": "/?app=index", + "start_url": "../../../?app=index", + "scope": "../../../", "display": "standalone", "background_color": "#ffffff", "theme_color": "#000000", "icons": [ { - "src": "/static/assets/spa/spa-icon-192x192.png", + "src": "spa-icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/static/assets/spa/spa-icon-512x512.png", + "src": "spa-icon-512x512.png", "sizes": "512x512", "type": "image/png" } ] -} \ No newline at end of file +} diff --git a/deploy/Windows/config.py b/deploy/Windows/config.py index f0e80f5da..2da15ea4c 100644 --- a/deploy/Windows/config.py +++ b/deploy/Windows/config.py @@ -4,12 +4,14 @@ import sys from typing import Optional, Union +from deploy.geo import get_country_code from deploy.Windows.logger import logger from deploy.Windows.utils import DEPLOY_CONFIG, DEPLOY_TEMPLATE, cached_property, poor_yaml_read, poor_yaml_write GIT_OVER_CDN_REPOSITORY = 'git://git.pull/AzurPilot' GIT_OVER_CDN_FALLBACK_REPOSITORY = 'https://gitcode.com/ddl2/AzurLaneAutoScript' +GITHUB_REPOSITORY = 'https://github.com/wess09/AzurPilot' class ExecutionError(Exception): @@ -18,7 +20,7 @@ class ExecutionError(Exception): class ConfigModel: # Git 配置 - Repository: str = "https://github.com/wess09/AzurPilot" + Repository: str = GITHUB_REPOSITORY Branch: str = "master" GitExecutable: str = "./.venv/Scripts/git/cmd/git.exe" GitProxy: Optional[str] = None @@ -86,6 +88,7 @@ def __init__(self, file=DEPLOY_CONFIG): self.file = file self.config = {} self.config_template = {} + self._github_location_checked = False self.read() self.show_config() @@ -125,6 +128,7 @@ def config_redirect(self): 每次 `read()` 之后必须调用。 """ self.config.pop('AutoUpdate', None) + self._redirect_github_repository() # 绕过 webui.config.DeployConfig.__setattr__(),不写入 deploy.yaml super().__setattr__('GitOverCdn', self.Repository in ['cn', GIT_OVER_CDN_REPOSITORY]) if self.Repository in ['global']: @@ -132,6 +136,22 @@ def config_redirect(self): if self.Repository in ['cn', GIT_OVER_CDN_REPOSITORY]: super().__setattr__('Repository', GIT_OVER_CDN_FALLBACK_REPOSITORY) + def _redirect_github_repository(self): + """为官方 GitHub 源一次性选择适合当前网络的更新镜像。""" + if self._github_location_checked or self.Repository != GITHUB_REPOSITORY: + return + + self._github_location_checked = True + country_code = get_country_code() + if country_code == 'cn': + logger.info('检测到中国大陆网络,切换至国内 Git 更新源') + self.Repository = GIT_OVER_CDN_REPOSITORY + self.config['Repository'] = GIT_OVER_CDN_REPOSITORY + elif country_code is None: + logger.warning('无法检测网络所在国家,保留 GitHub 更新源') + else: + logger.info('当前网络不在中国大陆,保留 GitHub 更新源') + def filepath(self, path): """获取绝对文件路径。 diff --git a/deploy/config.py b/deploy/config.py index 05a11f13c..60caa2391 100644 --- a/deploy/config.py +++ b/deploy/config.py @@ -2,12 +2,14 @@ import sys from typing import Optional, Union +from deploy.geo import get_country_code from deploy.logger import logger from deploy.utils import * GIT_OVER_CDN_REPOSITORY = 'git://git.pull/AzurPilot' GIT_OVER_CDN_FALLBACK_REPOSITORY = 'https://gitcode.com/ddl2/AzurLaneAutoScript' +GITHUB_REPOSITORY = 'https://github.com/wess09/AzurPilot' class ExecutionError(Exception): @@ -16,7 +18,7 @@ class ExecutionError(Exception): class ConfigModel: # Git 配置 - Repository: str = "https://github.com/wess09/AzurPilot" + Repository: str = GITHUB_REPOSITORY Branch: str = "master" GitExecutable: str = "./.venv/Scripts/git/cmd/git.exe" if sys.platform == "win32" else "./.venv/bin/git" GitProxy: Optional[str] = None @@ -85,6 +87,7 @@ def __init__(self, file=DEPLOY_CONFIG): self.template_file = get_deploy_template() self.config = {} self.config_template = {} + self._github_location_checked = False self.read() self.show_config() @@ -125,6 +128,7 @@ def config_redirect(self): 每次 `read()` 之后必须调用。 """ self.config.pop('AutoUpdate', None) + self._redirect_github_repository() if self.Repository in [ 'https://gitee.com/LmeSzinc/AzurLaneAutoScript', 'https://gitee.com/lmeszinc/azur-lane-auto-script-mirror', @@ -160,6 +164,22 @@ def config_redirect(self): if self.Repository in ['cn']: super().__setattr__('Repository', GIT_OVER_CDN_REPOSITORY) + def _redirect_github_repository(self): + """为官方 GitHub 源一次性选择适合当前网络的更新镜像。""" + if self._github_location_checked or self.Repository != GITHUB_REPOSITORY: + return + + self._github_location_checked = True + country_code = get_country_code() + if country_code == 'cn': + logger.info('检测到中国大陆网络,切换至国内 Git 更新源') + self.Repository = GIT_OVER_CDN_REPOSITORY + self.config['Repository'] = GIT_OVER_CDN_REPOSITORY + elif country_code is None: + logger.warning('无法检测网络所在国家,保留 GitHub 更新源') + else: + logger.info('当前网络不在中国大陆,保留 GitHub 更新源') + def filepath(self, key): """根据配置键获取绝对文件路径。 diff --git a/deploy/geo.py b/deploy/geo.py new file mode 100644 index 000000000..21a0dd24f --- /dev/null +++ b/deploy/geo.py @@ -0,0 +1,32 @@ +from typing import Optional + +import requests + + +IP9_LOCATION_URL = 'https://ip9.com.cn/get' + + +def get_country_code(timeout=5) -> Optional[str]: + """查询当前公网 IP 所在国家的 ISO 代码。 + + 查询失败或响应格式不符合预期时返回 ``None``,调用方应保留当前更新源。 + """ + try: + response = requests.get( + IP9_LOCATION_URL, + timeout=timeout, + headers={'User-Agent': 'AzurPilot'}, + ) + response.raise_for_status() + payload = response.json() + except (requests.RequestException, ValueError): + return None + + try: + country_code = payload['data']['country_code'] + except (KeyError, TypeError): + return None + + if isinstance(country_code, str): + return country_code.lower() + return None diff --git a/module/webui/app.py b/module/webui/app.py index e8ccf7a83..f7ffab606 100644 --- a/module/webui/app.py +++ b/module/webui/app.py @@ -7,6 +7,9 @@ 该模块是 WebUI 的顶层入口,被 gui.py 启动时引用。 """ +from hashlib import sha256 +from pathlib import Path + from module.webui.app_dashboard import DashboardMixin from module.webui.app_dependencies import ( Dict, @@ -29,7 +32,6 @@ local, logger, login, - os, popup, run_js, set_env, @@ -76,7 +78,16 @@ from module.webui.app_task_config import TaskConfigMixin -INITIAL_WEBUI_CSS = "/static/assets/gui/css/alas.css" +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +def _versioned_static_asset(relative_path: str) -> str: + """返回带内容哈希的相对静态资源地址。""" + digest = sha256((PROJECT_ROOT / relative_path).read_bytes()).hexdigest()[:12] + return f"static/{relative_path}?v={digest}" + + +INITIAL_WEBUI_CSS = _versioned_static_asset("assets/gui/css/alas.css") class AlasGUI( @@ -167,7 +178,10 @@ def app(): from deploy.atomic import atomic_failure_cleanup atomic_failure_cleanup("./config") - static_path = os.getcwd() + static_mounts = { + "/static/assets": str(PROJECT_ROOT / "assets"), + "/static/doc": str(PROJECT_ROOT / "doc"), + } def _block_restricted_device() -> bool: if is_demo_mode(): @@ -231,7 +245,7 @@ def manage() -> None: application = asgi_app( applications=[index, manage], cdn=cdn, - static_dir=static_path, + static_mounts=static_mounts, debug=True, on_startup=[ startup, diff --git a/module/webui/app_home.py b/module/webui/app_home.py index 192a87cb3..b42ac5ae9 100644 --- a/module/webui/app_home.py +++ b/module/webui/app_home.py @@ -251,13 +251,13 @@ def _load_deferred_client_assets(self) -> None: "}" "if (!document.querySelector('link[rel=\"manifest\"]')) {" "var manifest=document.createElement('link');" - "manifest.rel='manifest';manifest.href='/static/assets/spa/manifest.json';" + "manifest.rel='manifest';manifest.href='static/assets/spa/manifest.json';" "document.head.appendChild(manifest);" "}" "if (!document.getElementById('alas-utils-script')) {" "var script=document.createElement('script');" "script.id='alas-utils-script';script.async=true;" - "script.src='/static/assets/gui/js/alas-utils.js';" + "script.src='static/assets/gui/js/alas-utils.js';" "document.head.appendChild(script);" "}" "}" diff --git a/module/webui/fastapi.py b/module/webui/fastapi.py index 52bc6c3b4..ec27b2b77 100644 --- a/module/webui/fastapi.py +++ b/module/webui/fastapi.py @@ -5,6 +5,7 @@ import asyncio import logging import os +from collections.abc import Mapping from typing import Any, cast import uvicorn @@ -34,7 +35,7 @@ logger = logging.getLogger(__name__) -STATIC_ASSET_CACHE_CONTROL = "public, max-age=86400, must-revalidate" +STATIC_ASSET_CACHE_CONTROL = "no-cache" NO_CACHE_CONTROL = "no-cache" HTTP_GZIP_MINIMUM_SIZE = 1024 HTTP_GZIP_COMPRESS_LEVEL = 5 @@ -51,7 +52,7 @@ async def dispatch(self, request, call_next): 200 <= response.status_code < 300 or response.status_code == 304 ) if request.method in {"GET", "HEAD"} and is_static_asset and is_cacheable_response: - # 静态资源路径未使用内容哈希,缓存一天后仍通过 ETag 重新验证。 + # 部分静态资源没有内容哈希,必须在每次使用前重新验证。 response.headers["Cache-Control"] = STATIC_ASSET_CACHE_CONTROL else: response.headers["Cache-Control"] = NO_CACHE_CONTROL @@ -116,11 +117,12 @@ def patch_pywebio_websocket_connection(): def asgi_app( applications, - cdn: str | bool = True, + cdn: str | bool = False, static_dir=None, debug: bool = False, allowed_origins=None, check_origin=None, + static_mounts: Mapping[str, str] | None = None, **starlette_settings, ): debug = bool(os.environ.get("PYWEBIO_DEBUG", debug)) @@ -137,6 +139,9 @@ def asgi_app( check_origin=check_origin, ) routes.insert(0, Route("/robots.txt", robots_txt, methods=["GET", "HEAD"])) + if static_mounts: + for mount_path, directory in static_mounts.items(): + routes.append(Mount(mount_path, app=StaticFiles(directory=directory))) if static_dir: routes.append( Mount("/static", app=StaticFiles(directory=static_dir), name="static") @@ -176,13 +181,14 @@ def start_server( applications, port=0, host="", - cdn: str | bool = True, + cdn: str | bool = False, static_dir=None, remote_access=False, debug=False, allowed_origins=None, check_origin=None, auto_open_webbrowser=False, + static_mounts: Mapping[str, str] | None = None, **uvicorn_settings, ): @@ -190,6 +196,7 @@ def start_server( applications, cdn=cdn, static_dir=static_dir, + static_mounts=static_mounts, debug=debug, allowed_origins=allowed_origins, check_origin=check_origin, diff --git a/module/webui/obs_overlay.html b/module/webui/obs_overlay.html index 758635e7a..557ff0051 100644 --- a/module/webui/obs_overlay.html +++ b/module/webui/obs_overlay.html @@ -6,14 +6,12 @@ AzurPilot 侵蚀一实况监控