2026-05-09 09:29:34 +08:00
|
|
|
#!/usr/bin/env sh
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
if (set -o pipefail) >/dev/null 2>&1; then
|
|
|
|
|
set -o pipefail
|
|
|
|
|
fi
|
2026-05-06 17:43:47 +08:00
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
export MSYS_NO_PATHCONV=1
|
|
|
|
|
|
2026-05-09 09:29:34 +08:00
|
|
|
SCRIPT_PATH="$0"
|
|
|
|
|
case "$SCRIPT_PATH" in
|
|
|
|
|
/*) ;;
|
|
|
|
|
*) SCRIPT_PATH="$(pwd)/$SCRIPT_PATH" ;;
|
|
|
|
|
esac
|
|
|
|
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
2026-05-06 17:43:47 +08:00
|
|
|
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'
|
|
|
|
|
|
2026-05-09 09:29:34 +08:00
|
|
|
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
|
2026-05-06 17:43:47 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-05-09 09:29:34 +08:00
|
|
|
ENV_OVERRIDE_SERVER_HOST_SET=false
|
|
|
|
|
ENV_OVERRIDE_POSTGRES_HOST_SET=false
|
|
|
|
|
ENV_OVERRIDE_DATABASE_URL_SET=false
|
2026-05-09 05:59:46 +00:00
|
|
|
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
|
2026-05-09 07:29:49 +00:00
|
|
|
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
|
2026-05-09 09:29:34 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-05-09 07:29:49 +00:00
|
|
|
if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_ENABLED+x}" = x ]; then
|
2026-05-09 05:59:46 +00:00
|
|
|
ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=true
|
|
|
|
|
ENV_OVERRIDE_ONLYOFFICE_ENABLED="$ONLYOFFICE_ENABLED"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-09 07:29:49 +00:00
|
|
|
if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_PUBLIC_URL+x}" = x ]; then
|
2026-05-09 05:59:46 +00:00
|
|
|
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=true
|
|
|
|
|
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL="$ONLYOFFICE_PUBLIC_URL"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-09 07:29:49 +00:00
|
|
|
if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_BACKEND_URL+x}" = x ]; then
|
2026-05-09 05:59:46 +00:00
|
|
|
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=true
|
|
|
|
|
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL="$ONLYOFFICE_BACKEND_URL"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-09 07:29:49 +00:00
|
|
|
if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_JWT_SECRET+x}" = x ]; then
|
2026-05-09 05:59:46 +00:00
|
|
|
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=true
|
|
|
|
|
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET="$ONLYOFFICE_JWT_SECRET"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-06 17:43:47 +08:00
|
|
|
set -a
|
|
|
|
|
. "$ROOT_ENV_FILE"
|
|
|
|
|
set +a
|
|
|
|
|
|
2026-05-09 09:29:34 +08:00
|
|
|
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
|
|
|
|
|
|
2026-05-09 05:59:46 +00:00
|
|
|
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
|
|
|
|
|
|
2026-05-08 10:52:54 +08:00
|
|
|
SERVER_HOST="${SERVER_HOST:-0.0.0.0}"
|
2026-05-06 17:43:47 +08:00
|
|
|
SERVER_PORT="${SERVER_PORT:-8000}"
|
2026-05-08 08:56:52 +08:00
|
|
|
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
|
|
|
|
|
|
2026-05-17 08:38:41 +00:00
|
|
|
if is_container; then
|
|
|
|
|
DEFAULT_SERVER_RELOAD="false"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-08 08:56:52 +08:00
|
|
|
SERVER_RELOAD="${SERVER_RELOAD:-$DEFAULT_SERVER_RELOAD}"
|
2026-05-06 17:43:47 +08:00
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
needs_windows_python() {
|
|
|
|
|
is_msys || is_wsl
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 17:43:47 +08:00
|
|
|
find_unix_python() {
|
2026-05-07 11:50:10 +08:00
|
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
|
|
|
echo "python3"
|
2026-05-06 17:43:47 +08:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
if command -v python >/dev/null 2>&1; then
|
|
|
|
|
echo "python"
|
2026-05-06 17:43:47 +08:00
|
|
|
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-17 08:38:41 +00:00
|
|
|
"$PYTHON_BIN" -c "import alembic, dotenv, email_validator, fastapi, jwt, lightrag, multipart, psycopg, pydantic_settings, qdrant_client, sqlalchemy, uvicorn" >/dev/null 2>&1
|
2026-05-06 17:43:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pip_ready() {
|
|
|
|
|
"$PYTHON_BIN" -m pip --version >/dev/null 2>&1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
create_venv() {
|
2026-05-09 09:29:34 +08:00
|
|
|
info "Creating virtual environment at $VENV_DISPLAY_PATH"
|
2026-05-06 17:43:47 +08:00
|
|
|
if [ -d "$VENV_DIR" ]; then
|
|
|
|
|
rm -rf "$VENV_DIR"
|
|
|
|
|
fi
|
2026-05-09 09:29:34 +08:00
|
|
|
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"
|
2026-05-06 17:43:47 +08:00
|
|
|
|
|
|
|
|
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() {
|
2026-05-09 09:29:34 +08:00
|
|
|
PYTHON_BOOTSTRAP_IS_WINDOWS="false"
|
|
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
if needs_windows_python; then
|
2026-05-06 17:43:47 +08:00
|
|
|
if find_windows_python >/dev/null 2>&1; then
|
|
|
|
|
PYTHON_BOOTSTRAP="$(find_windows_python)"
|
2026-05-09 09:29:34 +08:00
|
|
|
PYTHON_BOOTSTRAP_IS_WINDOWS="true"
|
|
|
|
|
info "Detected Windows bash environment, using Windows Python"
|
2026-05-06 17:43:47 +08:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if find_unix_python >/dev/null 2>&1; then
|
|
|
|
|
PYTHON_BOOTSTRAP="$(find_unix_python)"
|
2026-05-07 11:50:10 +08:00
|
|
|
warn "Windows Python not found, falling back to system Python"
|
2026-05-06 17:43:47 +08:00
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
error "Python is not available in PATH."
|
2026-05-06 17:43:47 +08:00
|
|
|
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
|
|
|
|
|
|
2026-05-09 09:29:34 +08:00
|
|
|
if is_container && [ "$VENV_DIR" != "$DEFAULT_VENV_DIR" ]; then
|
|
|
|
|
info "Docker runtime detected, using isolated Python environment at $VENV_DISPLAY_PATH"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-06 17:43:47 +08:00
|
|
|
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 ""
|
|
|
|
|
|
2026-05-07 11:50:10 +08:00
|
|
|
if [ "$SERVER_RELOAD" = "true" ]; then
|
|
|
|
|
exec "$PYTHON_BIN" -m uvicorn app.main:app --reload --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
exec "$PYTHON_BIN" -m uvicorn app.main:app --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT"
|
2026-05-06 17:43:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "$MODE" in
|
|
|
|
|
deps)
|
|
|
|
|
ensure_dependencies
|
|
|
|
|
;;
|
|
|
|
|
start)
|
|
|
|
|
ensure_dependencies
|
|
|
|
|
start_server
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
error "Unknown mode: $MODE. Use one of: deps, start"
|
|
|
|
|
;;
|
|
|
|
|
esac
|