334 lines
9.7 KiB
Bash
Executable File
334 lines
9.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================
|
||
# 大时代 生产环境启动脚本
|
||
#
|
||
# 用法:
|
||
# ./start.sh # 构建前端 + 后台启动全部服务 (默认)
|
||
# ./start.sh --no-build # 跳过前端构建
|
||
# ./start.sh --no-daemon # 前台运行 (不使用 nohup)
|
||
# ./start.sh --gateway-only # 仅启动 Gateway (配合 nginx)
|
||
# ./start.sh stop # 停止所有后台服务
|
||
# ./start.sh status # 查看服务状态
|
||
#
|
||
# 环境变量:
|
||
# WORKERS=2 # uvicorn worker 数 (默认: 2)
|
||
# GATEWAY_HOST=0.0.0.0 # Gateway 绑定地址
|
||
# GATEWAY_PORT=8765 # Gateway 端口
|
||
# FRONTEND_PORT=80 # 前端服务端口 (默认: 80)
|
||
# ============================================================
|
||
set -euo pipefail
|
||
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
cd "${SCRIPT_DIR}"
|
||
|
||
WORKERS="${WORKERS:-2}"
|
||
GATEWAY_HOST="${GATEWAY_HOST:-0.0.0.0}"
|
||
GATEWAY_PORT="${GATEWAY_PORT:-8765}"
|
||
FRONTEND_PORT="${FRONTEND_PORT:-80}"
|
||
PID_DIR="${SCRIPT_DIR}/.pids"
|
||
LOG_DIR="${SCRIPT_DIR}/logs"
|
||
FRONTEND_DIST="${SCRIPT_DIR}/frontend/dist"
|
||
|
||
DAEMON=true
|
||
BUILD_FRONTEND=true
|
||
GATEWAY_ONLY=false
|
||
ACTION="start"
|
||
|
||
for arg in "$@"; do
|
||
case "$arg" in
|
||
--no-daemon) DAEMON=false ;;
|
||
--no-build) BUILD_FRONTEND=false ;;
|
||
--gateway-only) GATEWAY_ONLY=true ;;
|
||
stop) ACTION="stop" ;;
|
||
status) ACTION="status" ;;
|
||
*) echo -e "${YELLOW}忽略未知参数: ${arg}${NC}" ;;
|
||
esac
|
||
done
|
||
|
||
ensure_dirs() {
|
||
mkdir -p "${PID_DIR}" "${LOG_DIR}"
|
||
}
|
||
|
||
save_pid() {
|
||
local name="$1" pid="$2"
|
||
echo "${pid}" > "${PID_DIR}/${name}.pid"
|
||
}
|
||
|
||
read_pid() {
|
||
local name="$1"
|
||
local pidfile="${PID_DIR}/${name}.pid"
|
||
if [ -f "${pidfile}" ]; then
|
||
cat "${pidfile}"
|
||
fi
|
||
}
|
||
|
||
is_running() {
|
||
local pid="$1"
|
||
[ -n "${pid}" ] && kill -0 "${pid}" 2>/dev/null
|
||
}
|
||
|
||
stop_service() {
|
||
local name="$1"
|
||
local pid
|
||
pid="$(read_pid "${name}")"
|
||
if is_running "${pid}"; then
|
||
echo -e " ${YELLOW}停止${NC} ${name} (PID: ${pid})"
|
||
kill "${pid}" 2>/dev/null || true
|
||
local count=0
|
||
while is_running "${pid}" && [ "${count}" -lt 20 ]; do
|
||
sleep 0.5
|
||
count=$((count + 1))
|
||
done
|
||
if is_running "${pid}"; then
|
||
echo -e " ${RED}强制终止${NC} ${name}"
|
||
kill -9 "${pid}" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
rm -f "${PID_DIR}/${name}.pid"
|
||
}
|
||
|
||
print_status() {
|
||
local name="$1" port="$2"
|
||
local pid
|
||
pid="$(read_pid "${name}")"
|
||
if is_running "${pid}"; then
|
||
echo -e " ${GREEN}●${NC} ${name} (PID: ${pid}, 端口: ${port})"
|
||
else
|
||
echo -e " ${RED}○${NC} ${name} (未运行)"
|
||
fi
|
||
}
|
||
|
||
load_env() {
|
||
if [ -f .env ]; then
|
||
set -a
|
||
# shellcheck disable=SC1091
|
||
source .env
|
||
set +a
|
||
else
|
||
echo -e "${YELLOW}警告: 未检测到 .env,将使用环境变量或默认值${NC}"
|
||
fi
|
||
}
|
||
|
||
check_prereqs() {
|
||
# Resolve the Python interpreter to use. Prefer the project venv.
|
||
if [ -x "${SCRIPT_DIR}/.venv/bin/python" ]; then
|
||
PYTHON="${SCRIPT_DIR}/.venv/bin/python"
|
||
else
|
||
PYTHON="$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true)"
|
||
fi
|
||
|
||
if [ -z "${PYTHON}" ]; then
|
||
echo -e "${RED}未找到 python${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
echo -e "${GREEN}使用 Python: ${PYTHON}${NC}"
|
||
|
||
command -v lsof >/dev/null 2>&1 || {
|
||
echo -e "${RED}未找到 lsof${NC}"
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
kill_port() {
|
||
local port="$1"
|
||
local pids
|
||
pids="$(lsof -ti :"${port}" 2>/dev/null || true)"
|
||
if [ -n "${pids}" ]; then
|
||
echo -e "${YELLOW}端口 ${port} 已被占用,清理 PID:${NC} ${pids}"
|
||
echo "${pids}" | xargs kill -9 2>/dev/null || true
|
||
sleep 0.5
|
||
fi
|
||
}
|
||
|
||
do_stop() {
|
||
echo -e "${CYAN}停止所有服务...${NC}"
|
||
for svc in frontend agent_service trading_service news_service runtime_service openclaw_service; do
|
||
stop_service "${svc}"
|
||
done
|
||
echo -e "${GREEN}已停止${NC}"
|
||
}
|
||
|
||
do_status() {
|
||
echo ""
|
||
echo -e "${CYAN}服务状态${NC}"
|
||
print_status "agent_service" 8000
|
||
print_status "trading_service" 8001
|
||
print_status "news_service" 8002
|
||
print_status "runtime_service" 8003
|
||
print_status "openclaw_service" 8004
|
||
print_status "frontend" "${FRONTEND_PORT}"
|
||
echo ""
|
||
echo -e " ${CYAN}ℹ${NC} Gateway 由 runtime_service 管理,通过前端启动任务触发"
|
||
echo ""
|
||
|
||
if [ -d "${FRONTEND_DIST}" ]; then
|
||
echo -e " ${GREEN}✔${NC} 前端已构建: ${FRONTEND_DIST}"
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} 前端未构建,运行: cd frontend && npm run build"
|
||
fi
|
||
echo ""
|
||
}
|
||
|
||
build_frontend() {
|
||
if ! ${BUILD_FRONTEND}; then
|
||
return
|
||
fi
|
||
echo ""
|
||
echo -e "${CYAN}构建前端...${NC}"
|
||
if [ -d "frontend" ] && command -v npm >/dev/null 2>&1; then
|
||
if [ -f "frontend/package-lock.json" ]; then
|
||
(cd frontend && npm ci && npm run build)
|
||
else
|
||
(cd frontend && npm install && npm run build)
|
||
fi
|
||
echo -e "${GREEN}前端构建完成: ${FRONTEND_DIST}${NC}"
|
||
else
|
||
echo -e "${RED}前端构建失败: 需要 npm 和 frontend 目录${NC}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
start_single_daemon() {
|
||
local name="$1" app_path="$2" port="$3"
|
||
echo -e " ${GREEN}▶${NC} ${name} → :${port} (${WORKERS} workers)"
|
||
nohup env SERVICE_NAME="${name}" "${PYTHON}" -m uvicorn "${app_path}" \
|
||
--host 0.0.0.0 \
|
||
--port "${port}" \
|
||
--workers "${WORKERS}" \
|
||
--log-level warning \
|
||
--no-access-log \
|
||
>> "${LOG_DIR}/${name}.log" 2>&1 &
|
||
save_pid "${name}" $!
|
||
}
|
||
|
||
start_daemon() {
|
||
if ! ${GATEWAY_ONLY}; then
|
||
start_single_daemon "agent_service" "backend.apps.agent_service:app" 8000
|
||
start_single_daemon "trading_service" "backend.apps.trading_service:app" 8001
|
||
start_single_daemon "news_service" "backend.apps.news_service:app" 8002
|
||
start_single_daemon "runtime_service" "backend.apps.runtime_service:app" 8003
|
||
start_single_daemon "openclaw_service" "backend.apps.openclaw_service:app" 8004
|
||
fi
|
||
|
||
echo -e " ${GREEN}▶${NC} frontend → http://0.0.0.0:${FRONTEND_PORT}"
|
||
nohup env SERVICE_NAME="frontend" "${PYTHON}" -m uvicorn "backend.apps.frontend_service:app" \
|
||
--host 0.0.0.0 \
|
||
--port "${FRONTEND_PORT}" \
|
||
--workers "${WORKERS}" \
|
||
--log-level warning \
|
||
--no-access-log \
|
||
>> "${LOG_DIR}/frontend.log" 2>&1 &
|
||
save_pid "frontend" $!
|
||
|
||
echo ""
|
||
echo -e "${GREEN}所有服务已在后台启动${NC}"
|
||
echo " 日志目录: ${LOG_DIR}/"
|
||
echo " PID 目录: ${PID_DIR}/"
|
||
echo ""
|
||
echo " 查看状态: ./start.sh status"
|
||
echo " 查看日志: tail -f ${LOG_DIR}/gateway.log"
|
||
echo " 停止服务: ./start.sh stop"
|
||
echo ""
|
||
}
|
||
|
||
PIDS=()
|
||
|
||
cleanup_foreground() {
|
||
echo ""
|
||
echo -e "${YELLOW}正在停止所有服务...${NC}"
|
||
if [ "${#PIDS[@]}" -gt 0 ]; then
|
||
kill "${PIDS[@]}" 2>/dev/null || true
|
||
wait "${PIDS[@]}" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
start_single_foreground() {
|
||
local name="$1" app_path="$2" port="$3"
|
||
echo -e " ${GREEN}▶${NC} ${name} → :${port}"
|
||
env SERVICE_NAME="${name}" "${PYTHON}" -m uvicorn "${app_path}" \
|
||
--host 0.0.0.0 \
|
||
--port "${port}" \
|
||
--log-level warning \
|
||
--no-access-log &
|
||
PIDS+=($!)
|
||
}
|
||
|
||
start_foreground() {
|
||
trap cleanup_foreground EXIT INT TERM
|
||
|
||
if ! ${GATEWAY_ONLY}; then
|
||
start_single_foreground "agent_service" "backend.apps.agent_service:app" 8000
|
||
start_single_foreground "trading_service" "backend.apps.trading_service:app" 8001
|
||
start_single_foreground "news_service" "backend.apps.news_service:app" 8002
|
||
start_single_foreground "runtime_service" "backend.apps.runtime_service:app" 8003
|
||
start_single_foreground "openclaw_service" "backend.apps.openclaw_service:app" 8004
|
||
fi
|
||
|
||
echo -e " ${GREEN}▶${NC} frontend → http://0.0.0.0:${FRONTEND_PORT}"
|
||
env SERVICE_NAME="frontend" "${PYTHON}" -m uvicorn "backend.apps.frontend_service:app" \
|
||
--host 0.0.0.0 \
|
||
--port "${FRONTEND_PORT}" \
|
||
--log-level warning \
|
||
--no-access-log &
|
||
PIDS+=($!)
|
||
|
||
echo ""
|
||
echo -e "${GREEN}服务以前台模式运行。按 Ctrl+C 停止。${NC}"
|
||
wait
|
||
}
|
||
|
||
do_start() {
|
||
ensure_dirs
|
||
check_prereqs
|
||
load_env
|
||
|
||
export TRADING_SERVICE_URL="${TRADING_SERVICE_URL:-http://localhost:8001}"
|
||
export NEWS_SERVICE_URL="${NEWS_SERVICE_URL:-http://localhost:8002}"
|
||
export RUNTIME_SERVICE_URL="${RUNTIME_SERVICE_URL:-http://localhost:8003}"
|
||
export OPENCLAW_SERVICE_URL="${OPENCLAW_SERVICE_URL:-http://localhost:8004}"
|
||
|
||
build_frontend
|
||
|
||
echo ""
|
||
echo -e "${CYAN}停止已有服务...${NC}"
|
||
for svc in frontend agent_service trading_service news_service runtime_service openclaw_service; do
|
||
stop_service "${svc}"
|
||
done
|
||
|
||
echo ""
|
||
echo -e "${CYAN}══════════════════════════════════════════${NC}"
|
||
echo -e "${CYAN} 大时代 · 生产环境启动${NC}"
|
||
echo -e "${CYAN}══════════════════════════════════════════${NC}"
|
||
echo ""
|
||
|
||
if ${DAEMON}; then
|
||
start_daemon
|
||
else
|
||
start_foreground
|
||
fi
|
||
}
|
||
|
||
case "${ACTION}" in
|
||
start)
|
||
do_start
|
||
;;
|
||
stop)
|
||
do_stop
|
||
;;
|
||
status)
|
||
do_status
|
||
;;
|
||
*)
|
||
echo -e "${RED}未知动作: ${ACTION}${NC}"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|