Files

193 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMAGE_TAG="${IMAGE_TAG:-deerflow-backend-offline:20260505}"
CONTAINER_NAME="${CONTAINER_NAME:-deerflow-backend}"
HOST_PORT="${HOST_PORT:-8001}"
IMAGE_TAR="${IMAGE_TAR:-${SCRIPT_DIR}/deerflow-backend-offline-image-20260505.tar}"
EXTENSIONS_CONFIG_TEMPLATE="${SCRIPT_DIR}/extensions_config.json"
RUNTIME_EXTENSIONS_CONFIG="${SCRIPT_DIR}/data/config/extensions_config.json"
# Default timezone for the container. The offline image is built from a base
# layer that ships with UTC, which made scheduled tasks fire at the wrong wall
# clock and made log timestamps confusing for ops. ``Asia/Shanghai`` keeps the
# scheduler aligned with the operator's expectation; override with TZ=... when
# deploying to a different region.
CONTAINER_TZ="${CONTAINER_TZ:-${TZ:-Asia/Shanghai}}"
die() {
echo "ERROR: $*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "command not found: $1"
}
require_file() {
[ -f "$1" ] || die "required file not found: $1"
}
require_dir() {
[ -d "$1" ] || die "required directory not found: $1"
}
require_env_key() {
local key="$1"
grep -Eq "^[[:space:]]*${key}=" "${SCRIPT_DIR}/.env" || die "required .env key not found: ${key}"
}
read_optional_env_value() {
local key="$1"
awk -v key="${key}" '
$0 ~ "^[[:space:]]*" key "[[:space:]]*=" {
value = $0
sub("^[[:space:]]*" key "[[:space:]]*=[[:space:]]*", "", value)
sub(/\r$/, "", value)
}
END { if (value != "") print value }
' "${SCRIPT_DIR}/.env"
}
if [ ! -f "${SCRIPT_DIR}/.env" ]; then
if [ -f "${SCRIPT_DIR}/.env.example" ]; then
cp "${SCRIPT_DIR}/.env.example" "${SCRIPT_DIR}/.env"
echo "Generated .env from .env.example. Please edit .env and run this script again."
exit 1
fi
die "missing .env"
fi
if [ ! -f "${SCRIPT_DIR}/config.yaml" ]; then
if [ -f "${SCRIPT_DIR}/config.yaml.example" ]; then
cp "${SCRIPT_DIR}/config.yaml.example" "${SCRIPT_DIR}/config.yaml"
echo "Generated config.yaml from config.yaml.example. Please edit model/API settings and run this script again."
exit 1
fi
die "missing config.yaml"
fi
require_env_key "AUTH_JWT_SECRET"
require_env_key "SCHEDULED_TASKS_ENABLED"
require_env_key "ELEMENT_ENABLED"
require_file "${SCRIPT_DIR}/config.yaml"
require_file "${EXTENSIONS_CONFIG_TEMPLATE}"
require_file "${SCRIPT_DIR}/skill_evolution.json"
require_file "${SCRIPT_DIR}/backend/pyproject.toml"
require_file "${SCRIPT_DIR}/backend/app/gateway/app.py"
require_file "${SCRIPT_DIR}/backend/packages/harness/deerflow/runtime/scheduler/element.py"
require_dir "${SCRIPT_DIR}/skills"
require_dir "${SCRIPT_DIR}/wheelhouse"
# Knowledge base directory, mounted into the container at /mnt/knowledge_base.
require_dir "${SCRIPT_DIR}/backend/knowledge/knowledge_base"
# The Gateway's live RunManager and MemoryStreamBridge are process-local. A
# reconnect through Location/Last-Event-ID is a new HTTP request and uvicorn may
# dispatch it to a different worker, which has neither the live run nor its
# buffered events. Keep this launcher single-process until a shared stream
# bridge and live-run registry are implemented.
env_gateway_workers="$(read_optional_env_value GATEWAY_WORKERS)"
GATEWAY_WORKERS="${GATEWAY_WORKERS:-${env_gateway_workers:-1}}"
case "${GATEWAY_WORKERS}" in
''|*[!0-9]*) die "GATEWAY_WORKERS must be a positive integer (got: ${GATEWAY_WORKERS})" ;;
esac
[ "${GATEWAY_WORKERS}" -ge 1 ] || die "GATEWAY_WORKERS must be at least 1"
env_allow_unsafe_multiworker="$(read_optional_env_value DEERFLOW_ALLOW_UNSAFE_MULTIWORKER)"
ALLOW_UNSAFE_MULTIWORKER="${DEERFLOW_ALLOW_UNSAFE_MULTIWORKER:-${env_allow_unsafe_multiworker:-0}}"
if [ "${GATEWAY_WORKERS}" -gt 1 ]; then
if [ "${ALLOW_UNSAFE_MULTIWORKER}" != "1" ]; then
die "GATEWAY_WORKERS=${GATEWAY_WORKERS} is unsafe with the process-local MemoryStreamBridge/RunManager: SSE Location/join reconnects can land on another worker and lose the live stream. Use GATEWAY_WORKERS=1. Set DEERFLOW_ALLOW_UNSAFE_MULTIWORKER=1 only to acknowledge and accept this failure mode."
fi
echo "WARNING: UNSAFE multi-worker Gateway override enabled (GATEWAY_WORKERS=${GATEWAY_WORKERS})." >&2
echo "WARNING: SSE Location/join reconnects can lose live streams across workers." >&2
else
echo "Gateway workers: 1 (required by process-local live run/stream state)"
fi
require_cmd docker
mkdir -p "$(dirname "${RUNTIME_EXTENSIONS_CONFIG}")"
if [ -e "${RUNTIME_EXTENSIONS_CONFIG}" ] && [ ! -f "${RUNTIME_EXTENSIONS_CONFIG}" ]; then
die "runtime extensions config is not a regular file: ${RUNTIME_EXTENSIONS_CONFIG}"
fi
if [ ! -e "${RUNTIME_EXTENSIONS_CONFIG}" ]; then
cp -a "${EXTENSIONS_CONFIG_TEMPLATE}" "${RUNTIME_EXTENSIONS_CONFIG}"
echo "Initialized writable extensions config at ${RUNTIME_EXTENSIONS_CONFIG}"
fi
if ! docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
require_file "${IMAGE_TAR}"
echo "Loading Docker image from ${IMAGE_TAR}"
docker load -i "${IMAGE_TAR}"
fi
if docker ps -a --format '{{.Names}}' | awk -v name="${CONTAINER_NAME}" '$0 == name { found=1 } END { exit(found ? 0 : 1) }'; then
echo "Removing existing container ${CONTAINER_NAME}"
docker rm -f "${CONTAINER_NAME}" >/dev/null
fi
echo "Starting backend container ${CONTAINER_NAME}"
docker run -d \
--name "${CONTAINER_NAME}" \
--restart unless-stopped \
-p "${HOST_PORT}:8001" \
--env-file "${SCRIPT_DIR}/.env" \
-e GATEWAY_WORKERS="${GATEWAY_WORKERS}" \
-e TZ="${CONTAINER_TZ}" \
-e DEER_FLOW_PROJECT_ROOT=/app \
-e DEER_FLOW_HOME=/app/backend/.deer-flow \
-e DEER_FLOW_CONFIG_PATH=/app/config.yaml \
-e DEER_FLOW_EXTENSIONS_CONFIG_PATH=/app/backend/.deer-flow/config/extensions_config.json \
-e DEER_FLOW_SKILL_EVOLUTION_CONFIG_PATH=/app/skill_evolution.json \
-e DEER_FLOW_SKILLS_PATH=/app/skills \
-e UV_LINK_MODE=copy \
-v "${SCRIPT_DIR}/backend/app:/app/backend/app" \
-v "${SCRIPT_DIR}/backend/packages:/app/backend/packages" \
-v "${SCRIPT_DIR}/backend/scripts:/app/backend/scripts" \
-v "${SCRIPT_DIR}/backend/pyproject.toml:/app/backend/pyproject.toml:ro" \
-v "${SCRIPT_DIR}/backend/uv.lock:/app/backend/uv.lock:ro" \
-v "${SCRIPT_DIR}/backend/langgraph.json:/app/backend/langgraph.json:ro" \
-v "${SCRIPT_DIR}/config.yaml:/app/config.yaml:ro" \
-v "${SCRIPT_DIR}/skill_evolution.json:/app/skill_evolution.json:ro" \
-v "${SCRIPT_DIR}/skills:/app/skills:ro" \
-v "${SCRIPT_DIR}/wheelhouse:/app/wheelhouse:ro" \
-v "${SCRIPT_DIR}/backend/knowledge/knowledge_base:/mnt/knowledge_base:ro" \
-v "${SCRIPT_DIR}/data:/app/backend/.deer-flow:rw" \
"${IMAGE_TAG}" \
sh -c 'cd backend
# Apply timezone inside the container. ``TZ`` alone is enough for Python /
# uvicorn log timestamps, but linking /etc/localtime keeps C-level tools
# (e.g. anything that reads /etc/localtime directly) on the same clock.
if [ -n "${TZ:-}" ] && [ -f "/usr/share/zoneinfo/${TZ}" ]; then
ln -sf "/usr/share/zoneinfo/${TZ}" /etc/localtime 2>/dev/null || true
echo "${TZ}" > /etc/timezone 2>/dev/null || true
fi
if [ "${SCHEDULER_INSTALL_DEPS:-1}" != "0" ]; then
uv run --no-sync python -c "import croniter" >/dev/null 2>&1 || {
PYTHON_BIN="$(uv run --no-sync python -c "import sys; print(sys.executable)")"
uv pip install --python "${PYTHON_BIN}" --no-index --find-links /app/wheelhouse ${SCHEDULER_PIP_PACKAGES:-croniter==6.2.2}
}
fi
if [ "${HINDSIGHT_INSTALL_DEPS:-1}" != "0" ]; then
uv run --no-sync python -c "import hindsight_client" >/dev/null 2>&1 || {
PYTHON_BIN="$(uv run --no-sync python -c "import sys; print(sys.executable)")"
uv pip install --python "${PYTHON_BIN}" --no-index --find-links /app/wheelhouse ${HINDSIGHT_PIP_PACKAGES:-hindsight-client==0.6.2}
}
fi
if [ "${AIOMYSQL_INSTALL_DEPS:-1}" != "0" ]; then
uv run --no-sync python -c "import aiomysql, pymysql; import inspect; from sqlalchemy.dialects.mysql.aiomysql import AsyncAdapt_aiomysql_connection; assert inspect.signature(AsyncAdapt_aiomysql_connection.ping).parameters.get(\"reconnect\").default is False" >/dev/null 2>&1 || {
PYTHON_BIN="$(uv run --no-sync python -c "import sys; print(sys.executable)")"
uv pip install --python "${PYTHON_BIN}" --no-index --find-links /app/wheelhouse ${AIOMYSQL_PIP_PACKAGES:-aiomysql==0.3.2 PyMySQL==1.2.0 SQLAlchemy==2.0.51}
}
fi
uv run --no-sync python -c "import redis" >/dev/null 2>&1 || uv pip install --python "$(uv run --no-sync python -c "import sys; print(sys.executable)")" --no-index --find-links /app/wheelhouse ${REDIS_PIP_PACKAGES:-redis==5.2.1}
PYTHONPATH=. uv run --no-sync python -m uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 --workers "${GATEWAY_WORKERS:-1}"'
echo
echo "Backend started."
echo " URL: http://<server-ip>:${HOST_PORT}"
echo " Logs: docker logs -f ${CONTAINER_NAME}"
echo " Stop: ./stop_backend.sh"