feat: 添加 WebUI 静态资源包提示和相关错误处理逻辑

This commit is contained in:
DrSmoothl
2026-04-05 12:05:30 +08:00
parent 56d1071c01
commit ead90cbdf3
7 changed files with 56 additions and 279 deletions

View File

@@ -80,6 +80,7 @@
"startup.webui_auto_build_tool_missing": "❌ No supported frontend build tool was found, cannot auto-build WebUI", "startup.webui_auto_build_tool_missing": "❌ No supported frontend build tool was found, cannot auto-build WebUI",
"startup.webui_cors_configured": "✅ CORS middleware configured", "startup.webui_cors_configured": "✅ CORS middleware configured",
"startup.webui_dashboard_source_missing": "❌ WebUI frontend source directory not found: {dashboard_root}", "startup.webui_dashboard_source_missing": "❌ WebUI frontend source directory not found: {dashboard_root}",
"startup.webui_dashboard_package_hint": "💡 Install the prebuilt WebUI static asset package first, for example: {command}",
"startup.webui_disabled": "WebUI is disabled", "startup.webui_disabled": "WebUI is disabled",
"startup.webui_index_missing": "❌ index.html not found: {index_path}", "startup.webui_index_missing": "❌ index.html not found: {index_path}",
"startup.webui_manual_build_hint": "💡 Automatic recovery could not restore the frontend assets. Install dependencies and build manually in the dashboard directory: {command}", "startup.webui_manual_build_hint": "💡 Automatic recovery could not restore the frontend assets. Install dependencies and build manually in the dashboard directory: {command}",
@@ -87,6 +88,7 @@
"startup.webui_robots_route_register_failed": "❌ Failed to register robots.txt route: {error}", "startup.webui_robots_route_register_failed": "❌ Failed to register robots.txt route: {error}",
"startup.webui_robots_route_registered": "✅ robots.txt route registered", "startup.webui_robots_route_registered": "✅ robots.txt route registered",
"startup.webui_server_init_failed": "Failed to initialize WebUI server: {error}", "startup.webui_server_init_failed": "Failed to initialize WebUI server: {error}",
"startup.webui_static_assets_unavailable": "❌ No usable WebUI static assets were found (installed maibot-dashboard package or local dashboard/dist)",
"startup.webui_static_assets_try_auto_build": "⚠️ WebUI static assets are unavailable, attempting to auto-build the frontend...", "startup.webui_static_assets_try_auto_build": "⚠️ WebUI static assets are unavailable, attempting to auto-build the frontend...",
"startup.webui_static_dir_missing": "❌ WebUI static directory does not exist", "startup.webui_static_dir_missing": "❌ WebUI static directory does not exist",
"startup.webui_static_dir_missing_with_path": "❌ WebUI static directory does not exist: {static_path}", "startup.webui_static_dir_missing_with_path": "❌ WebUI static directory does not exist: {static_path}",

View File

@@ -80,6 +80,7 @@
"startup.webui_auto_build_tool_missing": "❌ 利用可能なフロントエンドビルドツールが見つからず、WebUI を自動ビルドできません", "startup.webui_auto_build_tool_missing": "❌ 利用可能なフロントエンドビルドツールが見つからず、WebUI を自動ビルドできません",
"startup.webui_cors_configured": "✅ CORS ミドルウェアを設定しました", "startup.webui_cors_configured": "✅ CORS ミドルウェアを設定しました",
"startup.webui_dashboard_source_missing": "❌ WebUI フロントエンドのソースディレクトリが見つかりません: {dashboard_root}", "startup.webui_dashboard_source_missing": "❌ WebUI フロントエンドのソースディレクトリが見つかりません: {dashboard_root}",
"startup.webui_dashboard_package_hint": "💡 事前ビルド済みの WebUI 静的リソースパッケージを先にインストールしてください。例: {command}",
"startup.webui_disabled": "WebUI は無効です", "startup.webui_disabled": "WebUI は無効です",
"startup.webui_index_missing": "❌ index.html が見つかりません: {index_path}", "startup.webui_index_missing": "❌ index.html が見つかりません: {index_path}",
"startup.webui_manual_build_hint": "💡 自動復旧でフロントエンド資産を復旧できなかったため、dashboard ディレクトリで依存関係をインストールして手動ビルドしてください: {command}", "startup.webui_manual_build_hint": "💡 自動復旧でフロントエンド資産を復旧できなかったため、dashboard ディレクトリで依存関係をインストールして手動ビルドしてください: {command}",
@@ -87,6 +88,7 @@
"startup.webui_robots_route_register_failed": "❌ robots.txt ルートの登録に失敗しました: {error}", "startup.webui_robots_route_register_failed": "❌ robots.txt ルートの登録に失敗しました: {error}",
"startup.webui_robots_route_registered": "✅ robots.txt ルートを登録しました", "startup.webui_robots_route_registered": "✅ robots.txt ルートを登録しました",
"startup.webui_server_init_failed": "WebUI サーバーの初期化に失敗しました: {error}", "startup.webui_server_init_failed": "WebUI サーバーの初期化に失敗しました: {error}",
"startup.webui_static_assets_unavailable": "❌ 利用可能な WebUI 静的アセットが見つかりません(インストール済みの maibot-dashboard パッケージまたはローカルの dashboard/dist",
"startup.webui_static_assets_try_auto_build": "⚠️ WebUI の静的アセットが利用できないため、フロントエンドの自動ビルドを試行します...", "startup.webui_static_assets_try_auto_build": "⚠️ WebUI の静的アセットが利用できないため、フロントエンドの自動ビルドを試行します...",
"startup.webui_static_dir_missing": "❌ WebUI の静的ディレクトリが存在しません", "startup.webui_static_dir_missing": "❌ WebUI の静的ディレクトリが存在しません",
"startup.webui_static_dir_missing_with_path": "❌ WebUI の静的ディレクトリが存在しません: {static_path}", "startup.webui_static_dir_missing_with_path": "❌ WebUI の静的ディレクトリが存在しません: {static_path}",

View File

@@ -80,6 +80,7 @@
"startup.webui_auto_build_tool_missing": "❌ 사용 가능한 프론트엔드 빌드 도구를 찾을 수 없어 WebUI를 자동으로 빌드할 수 없습니다", "startup.webui_auto_build_tool_missing": "❌ 사용 가능한 프론트엔드 빌드 도구를 찾을 수 없어 WebUI를 자동으로 빌드할 수 없습니다",
"startup.webui_cors_configured": "✅ CORS 미들웨어 설정 완료", "startup.webui_cors_configured": "✅ CORS 미들웨어 설정 완료",
"startup.webui_dashboard_source_missing": "❌ WebUI 프론트엔드 소스 디렉터리를 찾을 수 없습니다: {dashboard_root}", "startup.webui_dashboard_source_missing": "❌ WebUI 프론트엔드 소스 디렉터리를 찾을 수 없습니다: {dashboard_root}",
"startup.webui_dashboard_package_hint": "💡 미리 빌드된 WebUI 정적 리소스 패키지를 먼저 설치하세요. 예: {command}",
"startup.webui_disabled": "WebUI가 비활성화되었습니다", "startup.webui_disabled": "WebUI가 비활성화되었습니다",
"startup.webui_index_missing": "❌ index.html을 찾을 수 없습니다: {index_path}", "startup.webui_index_missing": "❌ index.html을 찾을 수 없습니다: {index_path}",
"startup.webui_manual_build_hint": "💡 자동 복구로 프론트엔드 리소스를 수정하지 못했습니다. dashboard 디렉터리에서 의존성을 설치하고 수동으로 빌드하세요: {command}", "startup.webui_manual_build_hint": "💡 자동 복구로 프론트엔드 리소스를 수정하지 못했습니다. dashboard 디렉터리에서 의존성을 설치하고 수동으로 빌드하세요: {command}",
@@ -87,6 +88,7 @@
"startup.webui_robots_route_register_failed": "❌ robots.txt 라우트 등록 실패: {error}", "startup.webui_robots_route_register_failed": "❌ robots.txt 라우트 등록 실패: {error}",
"startup.webui_robots_route_registered": "✅ robots.txt 라우트 등록 완료", "startup.webui_robots_route_registered": "✅ robots.txt 라우트 등록 완료",
"startup.webui_server_init_failed": "WebUI 서버 초기화 실패: {error}", "startup.webui_server_init_failed": "WebUI 서버 초기화 실패: {error}",
"startup.webui_static_assets_unavailable": "❌ 사용할 수 있는 WebUI 정적 리소스를 찾을 수 없습니다(설치된 maibot-dashboard 패키지 또는 로컬 dashboard/dist)",
"startup.webui_static_assets_try_auto_build": "⚠️ WebUI 정적 리소스를 사용할 수 없어 프론트엔드 자동 빌드를 시도합니다...", "startup.webui_static_assets_try_auto_build": "⚠️ WebUI 정적 리소스를 사용할 수 없어 프론트엔드 자동 빌드를 시도합니다...",
"startup.webui_static_dir_missing": "❌ WebUI 정적 파일 디렉터리가 존재하지 않습니다", "startup.webui_static_dir_missing": "❌ WebUI 정적 파일 디렉터리가 존재하지 않습니다",
"startup.webui_static_dir_missing_with_path": "❌ WebUI 정적 파일 디렉터리가 존재하지 않습니다: {static_path}", "startup.webui_static_dir_missing_with_path": "❌ WebUI 정적 파일 디렉터리가 존재하지 않습니다: {static_path}",

View File

@@ -80,6 +80,7 @@
"startup.webui_auto_build_tool_missing": "❌ 未找到可用的前端构建工具,无法自动构建 WebUI", "startup.webui_auto_build_tool_missing": "❌ 未找到可用的前端构建工具,无法自动构建 WebUI",
"startup.webui_cors_configured": "✅ CORS 中间件已配置", "startup.webui_cors_configured": "✅ CORS 中间件已配置",
"startup.webui_dashboard_source_missing": "❌ 未找到 WebUI 前端源码目录: {dashboard_root}", "startup.webui_dashboard_source_missing": "❌ 未找到 WebUI 前端源码目录: {dashboard_root}",
"startup.webui_dashboard_package_hint": "💡 请安装预构建的 WebUI 静态资源包,例如: {command}",
"startup.webui_disabled": "WebUI 已禁用", "startup.webui_disabled": "WebUI 已禁用",
"startup.webui_index_missing": "❌ 未找到 index.html: {index_path}", "startup.webui_index_missing": "❌ 未找到 index.html: {index_path}",
"startup.webui_manual_build_hint": "💡 自动恢复未能修复前端资源,请在 dashboard 目录安装依赖并手动构建: {command}", "startup.webui_manual_build_hint": "💡 自动恢复未能修复前端资源,请在 dashboard 目录安装依赖并手动构建: {command}",
@@ -87,6 +88,7 @@
"startup.webui_robots_route_register_failed": "❌ 注册 robots.txt 路由失败: {error}", "startup.webui_robots_route_register_failed": "❌ 注册 robots.txt 路由失败: {error}",
"startup.webui_robots_route_registered": "✅ robots.txt 路由已注册", "startup.webui_robots_route_registered": "✅ robots.txt 路由已注册",
"startup.webui_server_init_failed": "初始化 WebUI 服务器失败: {error}", "startup.webui_server_init_failed": "初始化 WebUI 服务器失败: {error}",
"startup.webui_static_assets_unavailable": "❌ 未找到可用的 WebUI 静态资源(已安装的 maibot-dashboard 包或本地 dashboard/dist",
"startup.webui_static_assets_try_auto_build": "⚠️ WebUI 静态资源不可用,尝试自动构建前端...", "startup.webui_static_assets_try_auto_build": "⚠️ WebUI 静态资源不可用,尝试自动构建前端...",
"startup.webui_static_dir_missing": "❌ WebUI 静态文件目录不存在", "startup.webui_static_dir_missing": "❌ WebUI 静态文件目录不存在",
"startup.webui_static_dir_missing_with_path": "❌ WebUI 静态文件目录不存在: {static_path}", "startup.webui_static_dir_missing_with_path": "❌ WebUI 静态文件目录不存在: {static_path}",

View File

@@ -19,6 +19,7 @@ dependencies = [
"jieba>=0.42.1", "jieba>=0.42.1",
"json-repair>=0.47.6", "json-repair>=0.47.6",
"maim-message>=0.6.2", "maim-message>=0.6.2",
"maibot-dashboard==1.0.0.dev2026040439",
"maibot-plugin-sdk>=2.3.0", "maibot-plugin-sdk>=2.3.0",
"mcp", "mcp",
"msgpack>=1.1.2", "msgpack>=1.1.2",

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@@ -10,54 +11,42 @@ def test_ensure_static_path_ready_uses_existing_static_path(tmp_path) -> None:
static_path.mkdir() static_path.mkdir()
(static_path / "index.html").write_text("<html></html>", encoding="utf-8") (static_path / "index.html").write_text("<html></html>", encoding="utf-8")
with ( with patch.object(webui_app, "_resolve_static_path", return_value=static_path):
patch.object(webui_app, "_resolve_static_path", return_value=static_path),
patch.object(webui_app, "_try_build_dashboard") as build_mock,
):
result = webui_app._ensure_static_path_ready() result = webui_app._ensure_static_path_ready()
assert result == static_path assert result == static_path
build_mock.assert_not_called()
def test_ensure_static_path_ready_retries_after_auto_build(tmp_path) -> None: def test_ensure_static_path_ready_logs_install_hint_when_static_assets_are_missing() -> None:
static_path = tmp_path / "dist"
static_path.mkdir()
(static_path / "index.html").write_text("<html></html>", encoding="utf-8")
with (
patch.object(webui_app, "_resolve_static_path", side_effect=[None, static_path]),
patch.object(
webui_app,
"_try_build_dashboard",
return_value=webui_app.DashboardAutoRecoveryResult(succeeded=True),
) as build_mock,
):
result = webui_app._ensure_static_path_ready()
assert result == static_path
build_mock.assert_called_once_with()
def test_ensure_static_path_ready_logs_manual_hint_when_auto_build_fails() -> None:
with ( with (
patch.object(webui_app, "_resolve_static_path", return_value=None), patch.object(webui_app, "_resolve_static_path", return_value=None),
patch.object(
webui_app,
"_try_build_dashboard",
return_value=webui_app.DashboardAutoRecoveryResult(
succeeded=False,
manual_recovery_command=webui_app._MANUAL_BUILD_COMMAND,
),
),
patch.object(webui_app.logger, "warning") as warning_mock, patch.object(webui_app.logger, "warning") as warning_mock,
): ):
result = webui_app._ensure_static_path_ready() result = webui_app._ensure_static_path_ready()
assert result is None assert result is None
warning_mock.assert_any_call(webui_app.t("startup.webui_auto_recovery_failed")) warning_mock.assert_any_call(webui_app.t("startup.webui_static_assets_unavailable"))
warning_mock.assert_any_call( warning_mock.assert_any_call(
webui_app.t("startup.webui_manual_build_hint", command=webui_app._MANUAL_BUILD_COMMAND) webui_app.t("startup.webui_dashboard_package_hint", command=webui_app._MANUAL_INSTALL_COMMAND)
)
def test_ensure_static_path_ready_logs_index_error_when_static_path_is_invalid(tmp_path) -> None:
static_path = tmp_path / "dist"
static_path.mkdir()
with (
patch.object(webui_app, "_resolve_static_path", return_value=static_path),
patch.object(webui_app.logger, "warning") as warning_mock,
):
result = webui_app._ensure_static_path_ready()
assert result is None
warning_mock.assert_any_call(
webui_app.t("startup.webui_index_missing", index_path=static_path / "index.html")
)
warning_mock.assert_any_call(
webui_app.t("startup.webui_dashboard_package_hint", command=webui_app._MANUAL_INSTALL_COMMAND)
) )
@@ -73,43 +62,21 @@ def test_setup_static_files_does_not_duplicate_warning_when_static_path_is_unava
warning_mock.assert_not_called() warning_mock.assert_not_called()
def test_get_dashboard_build_command_defaults_to_npm(tmp_path) -> None: def test_resolve_static_path_prefers_installed_dashboard_package(monkeypatch, tmp_path) -> None:
(tmp_path / "package.json").write_text("{}", encoding="utf-8") package_dist = tmp_path / "site-packages" / "maibot_dashboard" / "dist"
package_dist.mkdir(parents=True)
with patch.object( class _DashboardModule:
webui_app.shutil, @staticmethod
"which", def get_dist_path() -> Path:
side_effect=lambda tool_name: "/usr/bin/npm" if tool_name == "npm" else None, return package_dist
):
command = webui_app._get_dashboard_build_command(tmp_path)
assert command == ["npm", "run", "build"] monkeypatch.setattr(webui_app, "_get_project_root", lambda: tmp_path)
with patch.object(webui_app, "import_module", return_value=_DashboardModule()):
resolved_path = webui_app._resolve_static_path()
def test_try_build_dashboard_installs_missing_dependencies_before_build(monkeypatch, tmp_path) -> None: assert resolved_path == package_dist
(tmp_path / "package.json").write_text("{}", encoding="utf-8")
run_results = [
webui_app.CompletedProcess(args=["npm", "install", "--no-package-lock"], returncode=0, stdout="", stderr=""),
webui_app.CompletedProcess(args=["npm", "run", "build"], returncode=0, stdout="", stderr=""),
]
monkeypatch.setattr(webui_app, "_get_dashboard_root", lambda: tmp_path)
monkeypatch.setattr(webui_app, "_should_auto_install_dashboard_dependencies", lambda dashboard_root: True)
with (
patch.object(webui_app, "_get_dashboard_build_command", return_value=["npm", "run", "build"]),
patch.object(webui_app, "run", side_effect=run_results) as run_mock,
):
result = webui_app._try_build_dashboard()
assert result.succeeded is True
assert run_mock.call_count == 2
install_call = run_mock.call_args_list[0]
build_call = run_mock.call_args_list[1]
assert install_call.args[0] == ["npm", "install", "--no-package-lock"]
assert install_call.kwargs["cwd"] == tmp_path
assert build_call.args[0] == ["npm", "run", "build"]
assert build_call.kwargs["cwd"] == tmp_path
def test_resolve_static_path_uses_dashboard_dist(monkeypatch, tmp_path) -> None: def test_resolve_static_path_uses_dashboard_dist(monkeypatch, tmp_path) -> None:

View File

@@ -1,12 +1,10 @@
"""FastAPI 应用工厂 - 创建和配置 WebUI 应用实例""" """FastAPI 应用工厂 - 创建和配置 WebUI 应用实例"""
import mimetypes
import shutil
from dataclasses import dataclass
from importlib import import_module from importlib import import_module
from pathlib import Path from pathlib import Path
from subprocess import CompletedProcess, TimeoutExpired, run from typing import Any, Dict, Tuple
from typing import Any, Dict, List, Tuple
import mimetypes
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -17,28 +15,8 @@ from src.common.logger import get_logger
logger = get_logger("webui.app") logger = get_logger("webui.app")
_DASHBOARD_BUILD_TIMEOUT_SECONDS = 300 _DASHBOARD_PACKAGE_NAME = "maibot-dashboard"
_DASHBOARD_INSTALL_TIMEOUT_SECONDS = 600 _MANUAL_INSTALL_COMMAND = f"pip install {_DASHBOARD_PACKAGE_NAME}"
_MANUAL_BUILD_COMMAND = "cd dashboard && npm install && npm run build"
_DASHBOARD_BUILD_COMMANDS = {
"bun": ["bun", "run", "build"],
"npm": ["npm", "run", "build"],
"pnpm": ["pnpm", "build"],
"yarn": ["yarn", "build"],
}
_DASHBOARD_INSTALL_COMMANDS = {
"bun": ["bun", "install"],
"npm": ["npm", "install", "--no-package-lock"],
"pnpm": ["pnpm", "install"],
"yarn": ["yarn", "install"],
}
@dataclass(frozen=True)
class DashboardAutoRecoveryResult:
succeeded: bool
manual_recovery_command: str | None = None
def _resolve_safe_static_file_path(static_path: Path, full_path: str) -> Path | None: def _resolve_safe_static_file_path(static_path: Path, full_path: str) -> Path | None:
@@ -58,15 +36,6 @@ def _get_project_root() -> Path:
return Path(__file__).resolve().parents[2] return Path(__file__).resolve().parents[2]
def _get_dashboard_root() -> Path:
return _get_project_root() / "dashboard"
def _format_dashboard_shell_commands(*commands: List[str]) -> str:
formatted_commands = " && ".join(" ".join(command) for command in commands)
return f"cd dashboard && {formatted_commands}"
def _validate_static_path(static_path: Path | None) -> Tuple[str, Dict[str, Any]] | None: def _validate_static_path(static_path: Path | None) -> Tuple[str, Dict[str, Any]] | None:
if static_path is None: if static_path is None:
return "startup.webui_static_dir_missing", {} return "startup.webui_static_dir_missing", {}
@@ -81,185 +50,16 @@ def _validate_static_path(static_path: Path | None) -> Tuple[str, Dict[str, Any]
return None return None
def _summarize_command_output(command_result: CompletedProcess[str] | TimeoutExpired) -> str:
output_chunks: List[str] = []
stdout = command_result.stdout
stderr = command_result.stderr
if isinstance(stdout, str) and stdout.strip():
output_chunks.append(stdout.strip())
if isinstance(stderr, str) and stderr.strip():
output_chunks.append(stderr.strip())
if not output_chunks:
return ""
combined_output = "\n".join(output_chunks)
max_output_length = 2000
if len(combined_output) <= max_output_length:
return combined_output
return combined_output[-max_output_length:]
def _get_preferred_dashboard_package_manager(dashboard_root: Path) -> str:
if (dashboard_root / "pnpm-lock.yaml").exists():
return "pnpm"
if (dashboard_root / "yarn.lock").exists():
return "yarn"
if (dashboard_root / "bun.lock").exists() or (dashboard_root / "bun.lockb").exists():
return "bun"
return "npm"
def _get_dashboard_build_command(dashboard_root: Path) -> List[str] | None:
if not (dashboard_root / "package.json").exists():
return None
preferred_package_manager = _get_preferred_dashboard_package_manager(dashboard_root)
package_managers = [
preferred_package_manager,
*[
package_manager
for package_manager in _DASHBOARD_BUILD_COMMANDS
if package_manager != preferred_package_manager
],
]
for package_manager in package_managers:
if shutil.which(package_manager):
return _DASHBOARD_BUILD_COMMANDS[package_manager]
return None
def _get_dashboard_manual_recovery_command(dashboard_root: Path, build_command: List[str] | None = None) -> str:
package_manager = (
build_command[0] if build_command is not None else _get_preferred_dashboard_package_manager(dashboard_root)
)
install_command = _DASHBOARD_INSTALL_COMMANDS.get(package_manager)
selected_build_command = _DASHBOARD_BUILD_COMMANDS.get(package_manager)
if install_command is None or selected_build_command is None:
return _MANUAL_BUILD_COMMAND
return _format_dashboard_shell_commands(install_command, selected_build_command)
def _should_auto_install_dashboard_dependencies(dashboard_root: Path) -> bool:
return not (dashboard_root / "node_modules").exists()
def _try_build_dashboard() -> DashboardAutoRecoveryResult:
dashboard_root = _get_dashboard_root()
if not dashboard_root.exists():
logger.warning(t("startup.webui_dashboard_source_missing", dashboard_root=dashboard_root))
return DashboardAutoRecoveryResult(succeeded=False)
build_command = _get_dashboard_build_command(dashboard_root)
if build_command is None:
logger.warning(t("startup.webui_auto_build_tool_missing"))
manual_recovery_command = _get_dashboard_manual_recovery_command(dashboard_root)
return DashboardAutoRecoveryResult(succeeded=False, manual_recovery_command=manual_recovery_command)
manual_recovery_command = _get_dashboard_manual_recovery_command(dashboard_root, build_command)
if _should_auto_install_dashboard_dependencies(dashboard_root):
install_command = _DASHBOARD_INSTALL_COMMANDS[build_command[0]]
logger.info(t("startup.webui_auto_install_started", command=" ".join(install_command)))
try:
install_result = run(
install_command,
capture_output=True,
check=False,
cwd=dashboard_root,
text=True,
timeout=_DASHBOARD_INSTALL_TIMEOUT_SECONDS,
)
except TimeoutExpired as exc:
logger.warning(
t("startup.webui_auto_install_timeout", timeout_seconds=_DASHBOARD_INSTALL_TIMEOUT_SECONDS),
)
install_output = _summarize_command_output(exc)
if install_output:
logger.warning(t("startup.webui_auto_install_failed_output", output=install_output))
return DashboardAutoRecoveryResult(succeeded=False, manual_recovery_command=manual_recovery_command)
except OSError as exc:
logger.warning(t("startup.webui_auto_install_exec_failed", error=exc))
return DashboardAutoRecoveryResult(succeeded=False, manual_recovery_command=manual_recovery_command)
if install_result.returncode != 0:
logger.warning(t("startup.webui_auto_install_failed", return_code=install_result.returncode))
install_output = _summarize_command_output(install_result)
if install_output:
logger.warning(t("startup.webui_auto_install_failed_output", output=install_output))
return DashboardAutoRecoveryResult(succeeded=False, manual_recovery_command=manual_recovery_command)
logger.info(t("startup.webui_auto_install_succeeded"))
logger.info(t("startup.webui_auto_build_started", command=" ".join(build_command)))
try:
build_result = run(
build_command,
capture_output=True,
check=False,
cwd=dashboard_root,
text=True,
timeout=_DASHBOARD_BUILD_TIMEOUT_SECONDS,
)
except TimeoutExpired as exc:
logger.warning(
t("startup.webui_auto_build_timeout", timeout_seconds=_DASHBOARD_BUILD_TIMEOUT_SECONDS),
)
build_output = _summarize_command_output(exc)
if build_output:
logger.warning(t("startup.webui_auto_build_failed_output", output=build_output))
return DashboardAutoRecoveryResult(succeeded=False, manual_recovery_command=manual_recovery_command)
except OSError as exc:
logger.warning(t("startup.webui_auto_build_exec_failed", error=exc))
return DashboardAutoRecoveryResult(succeeded=False, manual_recovery_command=manual_recovery_command)
if build_result.returncode != 0:
logger.warning(t("startup.webui_auto_build_failed", return_code=build_result.returncode))
build_output = _summarize_command_output(build_result)
if build_output:
logger.warning(t("startup.webui_auto_build_failed_output", output=build_output))
return DashboardAutoRecoveryResult(succeeded=False, manual_recovery_command=manual_recovery_command)
logger.info(t("startup.webui_auto_build_succeeded"))
return DashboardAutoRecoveryResult(succeeded=True, manual_recovery_command=manual_recovery_command)
def _ensure_static_path_ready() -> Path | None: def _ensure_static_path_ready() -> Path | None:
static_path = _resolve_static_path() static_path = _resolve_static_path()
validation_error = _validate_static_path(static_path) validation_error = _validate_static_path(static_path)
if validation_error is None: if validation_error is None:
return static_path return static_path
logger.info(t("startup.webui_static_assets_try_auto_build")) logger.warning(t("startup.webui_static_assets_unavailable"))
error_key, error_kwargs = validation_error
auto_recovery_result = _try_build_dashboard() logger.warning(t(error_key, **error_kwargs))
if auto_recovery_result.succeeded: logger.warning(t("startup.webui_dashboard_package_hint", command=_MANUAL_INSTALL_COMMAND))
static_path = _resolve_static_path()
validation_error = _validate_static_path(static_path)
if validation_error is None:
return static_path
logger.warning(t("startup.webui_auto_build_artifacts_invalid"))
error_key, error_kwargs = validation_error
logger.warning(t(error_key, **error_kwargs))
logger.warning(
t(
"startup.webui_manual_build_hint",
command=auto_recovery_result.manual_recovery_command or _MANUAL_BUILD_COMMAND,
)
)
return None
if auto_recovery_result.manual_recovery_command is not None:
logger.warning(t("startup.webui_auto_recovery_failed"))
logger.warning(t("startup.webui_manual_build_hint", command=auto_recovery_result.manual_recovery_command))
return None return None
@@ -371,12 +171,12 @@ def _setup_static_files(app: FastAPI):
if not static_path.exists(): if not static_path.exists():
logger.warning(t("startup.webui_static_dir_missing_with_path", static_path=static_path)) logger.warning(t("startup.webui_static_dir_missing_with_path", static_path=static_path))
logger.warning(t("startup.webui_manual_build_hint", command=_MANUAL_BUILD_COMMAND)) logger.warning(t("startup.webui_dashboard_package_hint", command=_MANUAL_INSTALL_COMMAND))
return return
if not (static_path / "index.html").exists(): if not (static_path / "index.html").exists():
logger.warning(t("startup.webui_index_missing", index_path=static_path / "index.html")) logger.warning(t("startup.webui_index_missing", index_path=static_path / "index.html"))
logger.warning(t("startup.webui_manual_build_hint", command=_MANUAL_BUILD_COMMAND)) logger.warning(t("startup.webui_dashboard_package_hint", command=_MANUAL_INSTALL_COMMAND))
return return
@app.get("/{full_path:path}", include_in_schema=False) @app.get("/{full_path:path}", include_in_schema=False)
@@ -415,6 +215,7 @@ def _resolve_static_path() -> Path | None:
except Exception: except Exception:
pass pass
# 开发环境允许复用仓库里的现成 dist但不再在用户机器上触发任何前端自愈构建。
base_dir = _get_project_root() base_dir = _get_project_root()
static_path = base_dir / "dashboard" / "dist" static_path = base_dir / "dashboard" / "dist"
if static_path.exists(): if static_path.exists():