feat: 添加 WebUI 静态资源包提示和相关错误处理逻辑
This commit is contained in:
@@ -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}",
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
223
src/webui/app.py
223
src/webui/app.py
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user