diff --git a/server/scripts/bootstrap_paddleocr_mobile.sh b/server/scripts/bootstrap_paddleocr_mobile.sh index b4bab69..bbfa603 100644 --- a/server/scripts/bootstrap_paddleocr_mobile.sh +++ b/server/scripts/bootstrap_paddleocr_mobile.sh @@ -3,17 +3,17 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" OCR_VENV_DIR="${ROOT_DIR}/.venv-ocr312" -PYTHON_BIN="${PYTHON_BIN:-python3.12}" -PADDLEPADDLE_VERSION="${PADDLEPADDLE_VERSION:-3.3.1}" +PYTHON_BIN="${PYTHON_BIN:-python3}" +PADDLEPADDLE_VERSION="${PADDLEPADDLE_VERSION:-3.2.2}" PADDLEOCR_VERSION="${PADDLEOCR_VERSION:-3.6.0}" if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then - echo "python3.12 不存在,请先安装 Python 3.12。" >&2 + echo "${PYTHON_BIN} 不存在,请先安装 Python 3。" >&2 exit 1 fi apt-get update -apt-get install -y libgl1 libglib2.0-0 +apt-get install -y --no-install-recommends libgl1 libglib2.0-0 poppler-utils "${PYTHON_BIN}" -m venv "${OCR_VENV_DIR}" "${OCR_VENV_DIR}/bin/pip" install --upgrade pip diff --git a/server/server_start.sh b/server/server_start.sh index a932ecc..991c938 100755 --- a/server/server_start.sh +++ b/server/server_start.sh @@ -88,8 +88,11 @@ if [ ! -f "$ROOT_ENV_FILE" ]; then fi ENV_OVERRIDE_SERVER_HOST_SET=false +ENV_OVERRIDE_SERVER_PORT_SET=false ENV_OVERRIDE_POSTGRES_HOST_SET=false ENV_OVERRIDE_DATABASE_URL_SET=false +ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED_SET=false +ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED_SET=false ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=false ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=false ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=false @@ -107,6 +110,11 @@ if [ "${SERVER_HOST+x}" = x ]; then ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST" fi +if [ "${SERVER_PORT+x}" = x ]; then + ENV_OVERRIDE_SERVER_PORT_SET=true + ENV_OVERRIDE_SERVER_PORT="$SERVER_PORT" +fi + if [ "${POSTGRES_HOST+x}" = x ]; then ENV_OVERRIDE_POSTGRES_HOST_SET=true ENV_OVERRIDE_POSTGRES_HOST="$POSTGRES_HOST" @@ -117,6 +125,16 @@ if [ "${DATABASE_URL+x}" = x ]; then ENV_OVERRIDE_DATABASE_URL="$DATABASE_URL" fi +if [ "${STARTUP_BOOTSTRAP_ENABLED+x}" = x ]; then + ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED_SET=true + ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED="$STARTUP_BOOTSTRAP_ENABLED" +fi + +if [ "${BACKGROUND_SCHEDULERS_ENABLED+x}" = x ]; then + ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED_SET=true + ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED="$BACKGROUND_SCHEDULERS_ENABLED" +fi + if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_ENABLED+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=true ENV_OVERRIDE_ONLYOFFICE_ENABLED="$ONLYOFFICE_ENABLED" @@ -145,6 +163,10 @@ if [ "$ENV_OVERRIDE_SERVER_HOST_SET" = true ]; then SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST" fi +if [ "$ENV_OVERRIDE_SERVER_PORT_SET" = true ]; then + SERVER_PORT="$ENV_OVERRIDE_SERVER_PORT" +fi + if [ "$ENV_OVERRIDE_POSTGRES_HOST_SET" = true ]; then POSTGRES_HOST="$ENV_OVERRIDE_POSTGRES_HOST" fi @@ -153,6 +175,14 @@ if [ "$ENV_OVERRIDE_DATABASE_URL_SET" = true ]; then DATABASE_URL="$ENV_OVERRIDE_DATABASE_URL" fi +if [ "$ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED_SET" = true ]; then + STARTUP_BOOTSTRAP_ENABLED="$ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED" +fi + +if [ "$ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED_SET" = true ]; then + BACKGROUND_SCHEDULERS_ENABLED="$ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED" +fi + if [ "$ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET" = true ]; then ONLYOFFICE_ENABLED="$ENV_OVERRIDE_ONLYOFFICE_ENABLED" fi diff --git a/server/src/app/core/config.py b/server/src/app/core/config.py index 83006ac..c9398c3 100644 --- a/server/src/app/core/config.py +++ b/server/src/app/core/config.py @@ -43,6 +43,8 @@ class Settings(BaseSettings): app_port: int = Field(default=8000, alias="SERVER_PORT") server_workers: int = Field(default=1, alias="SERVER_WORKERS") web_concurrency: int | None = Field(default=None, alias="WEB_CONCURRENCY") + startup_bootstrap_enabled: bool = Field(default=True, alias="STARTUP_BOOTSTRAP_ENABLED") + startup_cache_warmup_enabled: bool = Field(default=False, alias="STARTUP_CACHE_WARMUP_ENABLED") background_schedulers_enabled: bool = Field( default=True, alias="BACKGROUND_SCHEDULERS_ENABLED", diff --git a/server/src/app/main.py b/server/src/app/main.py index ff09717..89cb975 100644 --- a/server/src/app/main.py +++ b/server/src/app/main.py @@ -2,6 +2,8 @@ from __future__ import annotations from collections.abc import AsyncIterator from contextlib import asynccontextmanager +from logging import Logger +import threading from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -10,11 +12,13 @@ from app.api.router import api_router from app.core.config import get_settings from app.core.logging import get_logger, setup_logging from app.core.openapi import API_DESCRIPTION, OPENAPI_TAGS +from app.db.session import get_session_factory from app.middleware.logging import AccessLogMiddleware from app.schemas.common import RootStatusRead from app.services.agent_foundation import prepare_agent_foundation from app.services.digital_employee_reminder_scheduler import digital_employee_reminder_scheduler from app.services.employee import prepare_employee_directory +from app.services.employee import EmployeeService from app.services.employee_profile_scheduler import employee_profile_scheduler from app.services.finance_dashboard_scheduler import finance_dashboard_scheduler from app.services.finance_report_scheduler import finance_report_scheduler @@ -23,6 +27,8 @@ from app.services.knowledge import prepare_knowledge_library from app.services.knowledge_index_tasks import knowledge_index_task_manager from app.services.knowledge_rag import shutdown_knowledge_rag_runtime from app.services.knowledge_scheduler import knowledge_index_scheduler +from app.services.settings import SettingsService +from app.services.user_session_metrics import UserSessionMetricService def _effective_server_workers(settings: object) -> int: @@ -42,15 +48,55 @@ def _should_start_background_schedulers(settings: object) -> bool: return _effective_server_workers(settings) <= 1 +def _run_startup_bootstrap(logger: Logger) -> None: + steps = ( + ("employee_directory", prepare_employee_directory), + ("agent_foundation", prepare_agent_foundation), + ("knowledge_library", prepare_knowledge_library), + ("hermes_skills", sync_repository_hermes_skills), + ) + for name, step in steps: + try: + step() + except Exception: + logger.exception("Startup bootstrap step failed; continuing degraded name=%s", name) + + +def _warm_startup_caches(logger: Logger) -> None: + try: + session_factory = get_session_factory() + with session_factory() as db: + SettingsService(db).ensure_settings_ready() + EmployeeService(db).ensure_directory_ready() + UserSessionMetricService(db).ensure_storage_ready() + logger.info("Startup cache warmup complete") + except Exception: + logger.exception("Startup cache warmup failed; continuing without warm cache") + + +def _start_cache_warmup_thread(logger: Logger) -> None: + thread = threading.Thread( + target=_warm_startup_caches, + args=(logger,), + name="x-financial-startup-cache-warmup", + daemon=True, + ) + thread.start() + + @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncIterator[None]: settings = get_settings() logger = get_logger("app.main") - prepare_employee_directory() - prepare_agent_foundation() - prepare_knowledge_library() - sync_repository_hermes_skills() + if settings.startup_bootstrap_enabled: + _run_startup_bootstrap(logger) + else: + logger.warning("Startup bootstrap skipped because STARTUP_BOOTSTRAP_ENABLED=false") + + if settings.startup_cache_warmup_enabled: + _start_cache_warmup_thread(logger) + schedulers_started = _should_start_background_schedulers(settings) if schedulers_started: knowledge_index_scheduler.start() diff --git a/server/tests/test_env_file_precedence.py b/server/tests/test_env_file_precedence.py index 67d5fa4..d3ac159 100644 --- a/server/tests/test_env_file_precedence.py +++ b/server/tests/test_env_file_precedence.py @@ -79,3 +79,63 @@ def test_root_start_can_prefer_env_file_over_inherited_onlyoffice_values(tmp_pat assert result.returncode == 0, result.stderr assert "ONLYOFFICE_ENABLED=true" in result.stdout assert "ONLYOFFICE_PUBLIC_URL=http://10.10.10.122:8082" in result.stdout + + +def test_web_start_preserves_inherited_runtime_ports(tmp_path: Path) -> None: + result = _run_script_prefix( + tmp_path, + "web/web_start.sh", + env_file_content=( + "WEB_HOST=10.10.10.122\n" + "WEB_PORT=5273\n" + "SERVER_HOST=10.10.10.122\n" + "SERVER_PORT=9000\n" + "POSTGRES_HOST=10.10.10.189\n" + ), + env={ + "WEB_HOST": "0.0.0.0", + "WEB_PORT": "5173", + "SERVER_HOST": "0.0.0.0", + "SERVER_PORT": "8000", + "POSTGRES_HOST": "www.caoxiaozhu.com", + }, + output_vars=["WEB_HOST", "WEB_PORT", "SERVER_HOST", "SERVER_PORT", "POSTGRES_HOST"], + ) + + assert result.returncode == 0, result.stderr + assert "WEB_HOST=0.0.0.0" in result.stdout + assert "WEB_PORT=5173" in result.stdout + assert "SERVER_HOST=0.0.0.0" in result.stdout + assert "SERVER_PORT=8000" in result.stdout + assert "POSTGRES_HOST=www.caoxiaozhu.com" in result.stdout + + +def test_server_start_preserves_inherited_runtime_guards(tmp_path: Path) -> None: + result = _run_script_prefix( + tmp_path, + "server/server_start.sh", + env_file_content=( + "SERVER_HOST=10.10.10.122\n" + "SERVER_PORT=9000\n" + "STARTUP_BOOTSTRAP_ENABLED=true\n" + "BACKGROUND_SCHEDULERS_ENABLED=true\n" + ), + env={ + "SERVER_HOST": "0.0.0.0", + "SERVER_PORT": "8000", + "STARTUP_BOOTSTRAP_ENABLED": "false", + "BACKGROUND_SCHEDULERS_ENABLED": "false", + }, + output_vars=[ + "SERVER_HOST", + "SERVER_PORT", + "STARTUP_BOOTSTRAP_ENABLED", + "BACKGROUND_SCHEDULERS_ENABLED", + ], + ) + + assert result.returncode == 0, result.stderr + assert "SERVER_HOST=0.0.0.0" in result.stdout + assert "SERVER_PORT=8000" in result.stdout + assert "STARTUP_BOOTSTRAP_ENABLED=false" in result.stdout + assert "BACKGROUND_SCHEDULERS_ENABLED=false" in result.stdout diff --git a/server/tests/test_startup_bootstrap.py b/server/tests/test_startup_bootstrap.py new file mode 100644 index 0000000..d40dc8e --- /dev/null +++ b/server/tests/test_startup_bootstrap.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from fastapi.testclient import TestClient + +from app.core.config import get_settings +import app.main as main_module + + +def _set_bootstrap_step( + monkeypatch, + name: str, + calls: list[str], + *, + should_fail: bool = False, +) -> None: + def step() -> None: + calls.append(name) + if should_fail: + raise RuntimeError(f"{name} failed") + + monkeypatch.setattr(main_module, name, step) + + +def test_lifespan_can_skip_startup_bootstrap(monkeypatch) -> None: + calls: list[str] = [] + monkeypatch.setenv("STARTUP_BOOTSTRAP_ENABLED", "false") + monkeypatch.setenv("BACKGROUND_SCHEDULERS_ENABLED", "false") + get_settings.cache_clear() + + for name in ( + "prepare_employee_directory", + "prepare_agent_foundation", + "prepare_knowledge_library", + "sync_repository_hermes_skills", + ): + _set_bootstrap_step(monkeypatch, name, calls) + + try: + with TestClient(main_module.create_app()) as client: + response = client.get("/") + finally: + get_settings.cache_clear() + + assert response.status_code == 200 + assert calls == [] + + +def test_lifespan_continues_when_startup_bootstrap_fails(monkeypatch) -> None: + calls: list[str] = [] + monkeypatch.setenv("STARTUP_BOOTSTRAP_ENABLED", "true") + monkeypatch.setenv("BACKGROUND_SCHEDULERS_ENABLED", "false") + get_settings.cache_clear() + + steps: tuple[tuple[str, bool], ...] = ( + ("prepare_employee_directory", True), + ("prepare_agent_foundation", False), + ("prepare_knowledge_library", False), + ("sync_repository_hermes_skills", False), + ) + for name, should_fail in steps: + _set_bootstrap_step(monkeypatch, name, calls, should_fail=should_fail) + + try: + with TestClient(main_module.create_app()) as client: + response = client.get("/") + finally: + get_settings.cache_clear() + + assert response.status_code == 200 + assert calls == [name for name, _ in steps] diff --git a/web/web_start.sh b/web/web_start.sh index e83a53c..e4baf9d 100755 --- a/web/web_start.sh +++ b/web/web_start.sh @@ -29,7 +29,9 @@ error() { printf '%b\n' "${RED}[ERROR]${NC} $*"; exit 1; } if [ -f "$ROOT_ENV_FILE" ]; then ENV_OVERRIDE_WEB_HOST_SET=false + ENV_OVERRIDE_WEB_PORT_SET=false ENV_OVERRIDE_SERVER_HOST_SET=false + ENV_OVERRIDE_SERVER_PORT_SET=false ENV_OVERRIDE_POSTGRES_HOST_SET=false if [ "${WEB_HOST+x}" = x ]; then @@ -37,11 +39,21 @@ if [ -f "$ROOT_ENV_FILE" ]; then ENV_OVERRIDE_WEB_HOST="$WEB_HOST" fi + if [ "${WEB_PORT+x}" = x ]; then + ENV_OVERRIDE_WEB_PORT_SET=true + ENV_OVERRIDE_WEB_PORT="$WEB_PORT" + fi + if [ "${SERVER_HOST+x}" = x ]; then ENV_OVERRIDE_SERVER_HOST_SET=true ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST" fi + if [ "${SERVER_PORT+x}" = x ]; then + ENV_OVERRIDE_SERVER_PORT_SET=true + ENV_OVERRIDE_SERVER_PORT="$SERVER_PORT" + fi + if [ "${POSTGRES_HOST+x}" = x ]; then ENV_OVERRIDE_POSTGRES_HOST_SET=true ENV_OVERRIDE_POSTGRES_HOST="$POSTGRES_HOST" @@ -55,10 +67,18 @@ if [ -f "$ROOT_ENV_FILE" ]; then WEB_HOST="$ENV_OVERRIDE_WEB_HOST" fi + if [ "$ENV_OVERRIDE_WEB_PORT_SET" = true ]; then + WEB_PORT="$ENV_OVERRIDE_WEB_PORT" + fi + if [ "$ENV_OVERRIDE_SERVER_HOST_SET" = true ]; then SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST" fi + if [ "$ENV_OVERRIDE_SERVER_PORT_SET" = true ]; then + SERVER_PORT="$ENV_OVERRIDE_SERVER_PORT" + fi + if [ "$ENV_OVERRIDE_POSTGRES_HOST_SET" = true ]; then POSTGRES_HOST="$ENV_OVERRIDE_POSTGRES_HOST" fi