Files
X-Financial/server/server_start.sh

381 lines
9.7 KiB
Bash
Raw Normal View History

#!/usr/bin/env sh
set -eu
if (set -o pipefail) >/dev/null 2>&1; then
set -o pipefail
fi
export MSYS_NO_PATHCONV=1
SCRIPT_PATH="$0"
case "$SCRIPT_PATH" in
/*) ;;
*) SCRIPT_PATH="$(pwd)/$SCRIPT_PATH" ;;
esac
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
cd "$SCRIPT_DIR"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ROOT_ENV_FILE="$ROOT_DIR/.env"
ROOT_ENV_EXAMPLE_FILE="$ROOT_DIR/.env.example"
MODE="${1:-start}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { printf '%b\n' "${GREEN}[INFO]${NC} $*"; }
warn() { printf '%b\n' "${YELLOW}[WARN]${NC} $*"; }
error() { printf '%b\n' "${RED}[ERROR]${NC} $*"; exit 1; }
is_container() {
[ -f "/.dockerenv" ] && return 0
if [ -r /proc/1/cgroup ] && grep -Eq "(docker|containerd|kubepods)" /proc/1/cgroup 2>/dev/null; then
return 0
fi
return 1
}
is_wsl() {
grep -qi microsoft /proc/version 2>/dev/null
}
is_msys() {
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) return 0 ;;
*) return 1 ;;
esac
}
to_windows_path() {
target_path="$1"
if is_wsl && command -v wslpath >/dev/null 2>&1; then
wslpath -w "$target_path"
return 0
fi
if is_msys && command -v cygpath >/dev/null 2>&1; then
cygpath -aw "$target_path"
return 0
fi
printf '%s\n' "$target_path"
}
DEFAULT_VENV_DIR="$SCRIPT_DIR/.venv"
DEFAULT_VENV_DISPLAY_PATH="./server/.venv"
if [ -n "${SERVER_VENV_DIR:-}" ]; then
VENV_DIR="$SERVER_VENV_DIR"
VENV_DISPLAY_PATH="$SERVER_VENV_DIR"
elif is_container; then
VENV_DIR="/tmp/x-financial-server-venv"
VENV_DISPLAY_PATH="$VENV_DIR"
else
VENV_DIR="$DEFAULT_VENV_DIR"
VENV_DISPLAY_PATH="$DEFAULT_VENV_DISPLAY_PATH"
fi
if [ ! -f "$ROOT_ENV_FILE" ]; then
if [ -f "$ROOT_ENV_EXAMPLE_FILE" ]; then
warn "Root .env not found. Creating it from .env.example"
cp "$ROOT_ENV_EXAMPLE_FILE" "$ROOT_ENV_FILE"
else
error "Root .env and .env.example are both missing."
fi
fi
ENV_OVERRIDE_SERVER_HOST_SET=false
ENV_OVERRIDE_POSTGRES_HOST_SET=false
ENV_OVERRIDE_DATABASE_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=false
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=false
PREFER_ENV_FILE_FOR_ONLYOFFICE=false
case "${X_FINANCIAL_PREFER_ENV_FILE:-false}" in
1|true|TRUE|yes|YES|on|ON)
PREFER_ENV_FILE_FOR_ONLYOFFICE=true
;;
esac
if [ "${SERVER_HOST+x}" = x ]; then
ENV_OVERRIDE_SERVER_HOST_SET=true
ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST"
fi
if [ "${POSTGRES_HOST+x}" = x ]; then
ENV_OVERRIDE_POSTGRES_HOST_SET=true
ENV_OVERRIDE_POSTGRES_HOST="$POSTGRES_HOST"
fi
if [ "${DATABASE_URL+x}" = x ]; then
ENV_OVERRIDE_DATABASE_URL_SET=true
ENV_OVERRIDE_DATABASE_URL="$DATABASE_URL"
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"
fi
if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_PUBLIC_URL+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=true
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL="$ONLYOFFICE_PUBLIC_URL"
fi
if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_BACKEND_URL+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=true
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL="$ONLYOFFICE_BACKEND_URL"
fi
if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_JWT_SECRET+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=true
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET="$ONLYOFFICE_JWT_SECRET"
fi
set -a
. "$ROOT_ENV_FILE"
set +a
if [ "$ENV_OVERRIDE_SERVER_HOST_SET" = true ]; then
SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST"
fi
if [ "$ENV_OVERRIDE_POSTGRES_HOST_SET" = true ]; then
POSTGRES_HOST="$ENV_OVERRIDE_POSTGRES_HOST"
fi
if [ "$ENV_OVERRIDE_DATABASE_URL_SET" = true ]; then
DATABASE_URL="$ENV_OVERRIDE_DATABASE_URL"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET" = true ]; then
ONLYOFFICE_ENABLED="$ENV_OVERRIDE_ONLYOFFICE_ENABLED"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET" = true ]; then
ONLYOFFICE_PUBLIC_URL="$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET" = true ]; then
ONLYOFFICE_BACKEND_URL="$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET" = true ]; then
ONLYOFFICE_JWT_SECRET="$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET"
fi
feat: 启用后端自动启动与 Setup 引导流程增强 主要修改点: 1. 网络绑定扩展至 Server - .env.example: SERVER_HOST/VITE_SERVER_HOST 从 127.0.0.1 改为 0.0.0.0 - server/src/app/core/config.py: 默认 app_host 从 127.0.0.1 改为 0.0.0.0 2. 启动脚本重构(重命名以区分职责) - server/start.sh → server/server_start.sh - web/start.sh → web/web_start.sh - 根目录 start.sh: 更新调用路径(./start.sh → ./server_start.sh, ./web_start.sh) - 根目录 start.sh: 新增 setup_ready() 检测函数 - 根目录 start.sh: server_probe_host() 支持 0.0.0.0 探测转换为 127.0.0.1 - 根目录 start.sh: start_setup_web() 新增 X_FINANCIAL_FORCE_SETUP=true 3. API URL 动态化 (web/src/services/api.js) - 从 localStorage 持久化读取 API Base URL - 新增 setRuntimeApiBaseUrl() / getRuntimeApiBaseUrl() 导出函数 - buildUrl() 改用运行时 runtimeApiBaseUrl 4. 浏览器 Host 智能解析 (web/vite.config.js) - 新增 resolveBrowserApiHost(): 根据 server_host/web_host 配置决定浏览器端使用的 API Host - buildApiBaseUrl() 改用 resolveBrowserApiHost() - 新增后端启动状态管理: backendStartState / cloneBackendStartState() / updateBackendStep() - 后端启动分 5 步追踪: config → deps → server → health → done - 新增 backendStartPromise 避免重复启动 5. Setup 表单逻辑增强 (web/src/composables/useSetupView.js) - 新增 shouldExposeServerHost(): 判断浏览器 host 是否非本地 - 新增 resolveInitialServerHost(): 当浏览器访问且 server_host 为本地时暴露 0.0.0.0 - buildPayload(): 根据 shouldExposeServerHost() 自动将 127.0.0.1/localhost 转为 0.0.0.0 6. Bootstrap API 扩展 (web/src/services/bootstrap.js) - 新增 startBootstrapBackend(): POST /bootstrap/backend 触发后端启动 - 新增 fetchBootstrapBackendStatus(): GET /bootstrap/backend 查询后端状态 7. Session 导航增强 (web/src/composables/useSystemState.js) - 新增 resolveBrowserApiBaseUrl(): 智能解析浏览器端 API Base URL - installSessionNavigation(): 调用 fetchBootstrapState() 同步引导状态 - 新增 reconcileEntryRoute(): 根据引导状态协调路由 - VITE_SERVER_HOST 默认值从 127.0.0.1 改为 0.0.0.0 8. Setup 视图增强 (web/src/views/SetupRouteView.vue) - 向 SetupView 传递启动进度相关 props: startupCountdownSeconds, startupLog, startupSteps, startupVisible, progressMessage 9. CSS 新增 (web/src/assets/styles/views/setup-view.css) - .setup-complete-progress: 进度文字样式 - .setup-modal-backdrop: 模态框遮罩 - .setup-startup-modal: 启动模态框容器 - .setup-startup-head / .setup-startup-body / .setup-startup-spinner: 模态框头部/内容/加载动画 - .setup-startup-steps / .setup-startup-step: 步骤列表及单个步骤 - .setup-startup-step.is-running / .is-success / .is-failed: 步骤状态样式 - .setup-startup-log: 启动日志区域
2026-05-08 10:52:54 +08:00
SERVER_HOST="${SERVER_HOST:-0.0.0.0}"
SERVER_PORT="${SERVER_PORT:-8000}"
DEFAULT_SERVER_RELOAD="false"
case "${APP_ENV:-local}" in
local|dev|development)
DEFAULT_SERVER_RELOAD="true"
;;
esac
if [ "${APP_DEBUG:-true}" = "true" ]; then
DEFAULT_SERVER_RELOAD="true"
fi
if is_container; then
DEFAULT_SERVER_RELOAD="false"
fi
SERVER_RELOAD="${SERVER_RELOAD:-$DEFAULT_SERVER_RELOAD}"
SERVER_WORKERS="${SERVER_WORKERS:-${WEB_CONCURRENCY:-1}}"
export SERVER_WORKERS
needs_windows_python() {
is_msys || is_wsl
}
find_unix_python() {
if command -v python3 >/dev/null 2>&1; then
echo "python3"
return 0
fi
if command -v python >/dev/null 2>&1; then
echo "python"
return 0
fi
return 1
}
find_windows_python() {
if command -v py.exe >/dev/null 2>&1; then
echo "py.exe -3"
return 0
fi
if command -v python.exe >/dev/null 2>&1; then
echo "python.exe"
return 0
fi
return 1
}
venv_python_path() {
if [ -x "$VENV_DIR/Scripts/python.exe" ]; then
echo "$VENV_DIR/Scripts/python.exe"
return 0
fi
if [ -x "$VENV_DIR/bin/python" ]; then
echo "$VENV_DIR/bin/python"
return 0
fi
return 1
}
run_bootstrap_python() {
$PYTHON_BOOTSTRAP "$@"
}
dependencies_ready() {
2026-05-18 02:53:06 +00:00
"$PYTHON_BIN" -c "import alembic, dotenv, email_validator, fastapi, jwt, lightrag, multipart, openpyxl, psycopg, pydantic_settings, qdrant_client, sqlalchemy, uvicorn" >/dev/null 2>&1
}
pip_ready() {
"$PYTHON_BIN" -m pip --version >/dev/null 2>&1
}
create_venv() {
info "Creating virtual environment at $VENV_DISPLAY_PATH"
if [ -d "$VENV_DIR" ]; then
rm -rf "$VENV_DIR"
fi
mkdir -p "$(dirname "$VENV_DIR")"
venv_target="$VENV_DIR"
if [ "$PYTHON_BOOTSTRAP_IS_WINDOWS" = "true" ]; then
venv_target="$(to_windows_path "$VENV_DIR")"
fi
run_bootstrap_python -m venv "$venv_target"
if ! PYTHON_BIN="$(venv_python_path)"; then
error "Virtual environment was not created successfully."
fi
}
ensure_pip() {
if pip_ready; then
return 0
fi
warn "pip is missing in .venv, attempting repair"
if "$PYTHON_BIN" -m ensurepip --upgrade >/dev/null 2>&1 && pip_ready; then
info "pip restored successfully"
return 0
fi
warn "Recreating .venv because pip repair failed"
rm -rf "$VENV_DIR"
create_venv
if "$PYTHON_BIN" -m ensurepip --upgrade >/dev/null 2>&1 && pip_ready; then
info "pip restored after recreating .venv"
return 0
fi
error "pip could not be created inside .venv. Install Python with venv and pip support, then rerun the script."
}
ensure_python_bootstrap() {
PYTHON_BOOTSTRAP_IS_WINDOWS="false"
if needs_windows_python; then
if find_windows_python >/dev/null 2>&1; then
PYTHON_BOOTSTRAP="$(find_windows_python)"
PYTHON_BOOTSTRAP_IS_WINDOWS="true"
info "Detected Windows bash environment, using Windows Python"
return 0
fi
if find_unix_python >/dev/null 2>&1; then
PYTHON_BOOTSTRAP="$(find_unix_python)"
warn "Windows Python not found, falling back to system Python"
return 0
fi
error "Python is not available in PATH."
fi
if ! PYTHON_BOOTSTRAP="$(find_unix_python)"; then
error "Python is not installed or not available in PATH. Install Python 3.11+ first so the script can create server/.venv automatically."
fi
}
ensure_dependencies() {
ensure_python_bootstrap
if is_container && [ "$VENV_DIR" != "$DEFAULT_VENV_DIR" ]; then
info "Docker runtime detected, using isolated Python environment at $VENV_DISPLAY_PATH"
fi
if ! PYTHON_BIN="$(venv_python_path)"; then
warn "Python virtual environment not found"
create_venv
fi
ensure_pip
if dependencies_ready; then
info "Server dependencies are ready."
return 0
fi
warn "Server dependencies are missing or incomplete"
info "Running .venv Python dependency installation"
"$PYTHON_BIN" -m pip install --upgrade pip
"$PYTHON_BIN" -m pip install -e ".[dev]"
if ! dependencies_ready; then
error "Server dependencies are still incomplete after installation."
fi
info "Server dependencies are ready."
}
start_server() {
info "Starting FastAPI server..."
info "Access: http://$SERVER_HOST:$SERVER_PORT"
echo ""
if [ "$SERVER_RELOAD" = "true" ]; then
exec "$PYTHON_BIN" -m uvicorn app.main:app --reload --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT"
fi
if [ "$SERVER_WORKERS" -gt 1 ] 2>/dev/null; then
BACKGROUND_SCHEDULERS_ENABLED="${BACKGROUND_SCHEDULERS_ENABLED:-false}"
export BACKGROUND_SCHEDULERS_ENABLED
exec "$PYTHON_BIN" -m uvicorn app.main:app --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT" --workers "$SERVER_WORKERS"
fi
exec "$PYTHON_BIN" -m uvicorn app.main:app --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT"
}
case "$MODE" in
deps)
ensure_dependencies
;;
start)
ensure_dependencies
start_server
;;
*)
error "Unknown mode: $MODE. Use one of: deps, start"
;;
esac