512 lines
14 KiB
Bash
Executable File
512 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# 大时代 Development Startup Script
|
||
# ================================
|
||
#
|
||
# 启动模式说明:
|
||
# -------------
|
||
# 本脚本只支持当前开发主路径:
|
||
# 启动 4 个独立服务 + 由 runtime_service 托管的 Gateway
|
||
# - agent_service (端口 8000): Agent 生命周期管理
|
||
# - runtime_service (端口 8003): 运行时配置和 Pipeline 执行
|
||
# - trading_service (端口 8001): 市场数据和交易操作
|
||
# - news_service (端口 8002): 新闻采集和富化
|
||
# - gateway (端口 8765): 由 runtime_service 拉起的 WebSocket 网关
|
||
#
|
||
# 用法:
|
||
# ./start-dev.sh # 启动开发环境
|
||
# ./start-dev.sh --help # 显示帮助信息
|
||
#
|
||
|
||
set -euo pipefail
|
||
|
||
# ============================================
|
||
# 配置与常量
|
||
# ============================================
|
||
|
||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
readonly SCRIPT_NAME="$(basename "$0")"
|
||
|
||
# 服务端点配置
|
||
readonly AGENT_SERVICE_PORT=8000
|
||
readonly TRADING_SERVICE_PORT=8001
|
||
readonly NEWS_SERVICE_PORT=8002
|
||
readonly RUNTIME_SERVICE_PORT=8003
|
||
readonly GATEWAY_PORT=8765
|
||
|
||
# 颜色定义
|
||
readonly RED='\033[0;31m'
|
||
readonly GREEN='\033[0;32m'
|
||
readonly YELLOW='\033[1;33m'
|
||
readonly BLUE='\033[0;34m'
|
||
readonly CYAN='\033[0;36m'
|
||
readonly NC='\033[0m' # No Color
|
||
|
||
# 进程 ID 数组
|
||
PIDS=()
|
||
|
||
# ============================================
|
||
# 工具函数
|
||
# ============================================
|
||
|
||
log_info() {
|
||
echo -e "${GREEN}[INFO]${NC} $1"
|
||
}
|
||
|
||
log_warn() {
|
||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}[ERROR]${NC} $1"
|
||
}
|
||
|
||
log_step() {
|
||
echo -e "${CYAN}[STEP]${NC} $1"
|
||
}
|
||
|
||
log_debug() {
|
||
echo -e "${BLUE}[DEBUG]${NC} $1"
|
||
}
|
||
|
||
# ============================================
|
||
# 帮助信息
|
||
# ============================================
|
||
|
||
show_help() {
|
||
cat << 'EOF'
|
||
大时代 Development Startup Script
|
||
|
||
用法:
|
||
./start-dev.sh [选项]
|
||
|
||
选项:
|
||
--help, -h 显示此帮助信息
|
||
|
||
开发模式:
|
||
启动 4 个独立微服务 + 托管 Gateway,各服务独立进程,便于单独调试
|
||
- agent_service: http://localhost:8000 (Agent 生命周期)
|
||
- trading_service: http://localhost:8001 (市场数据)
|
||
- news_service: http://localhost:8002 (新闻服务)
|
||
- runtime_service: http://localhost:8003 (运行时管理)
|
||
- gateway: ws://localhost:8765 (由 runtime_service 托管)
|
||
|
||
环境要求:
|
||
- Python 3.9+
|
||
- 虚拟环境 (推荐 .venv)
|
||
- .env 文件 (可选但推荐)
|
||
|
||
示例:
|
||
./start-dev.sh # 启动开发环境
|
||
EOF
|
||
}
|
||
|
||
# ============================================
|
||
# 参数解析
|
||
# ============================================
|
||
|
||
parse_args() {
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--help|-h)
|
||
show_help
|
||
exit 0
|
||
;;
|
||
*)
|
||
log_warn "未知选项: $1"
|
||
log_info "使用 --help 查看帮助信息"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# ============================================
|
||
# 启动前检查
|
||
# ============================================
|
||
|
||
check_python_version() {
|
||
log_step "检查 Python 版本..."
|
||
|
||
if ! command -v python >/dev/null 2>&1; then
|
||
log_error "未找到 python 命令"
|
||
exit 1
|
||
fi
|
||
|
||
local python_version
|
||
python_version=$(python --version 2>&1 | awk '{print $2}')
|
||
log_debug "Python 版本: $python_version"
|
||
|
||
python - <<'PY' || {
|
||
import sys
|
||
if sys.version_info < (3, 9):
|
||
print(f"Python 3.9+ 是必需的,当前版本: {sys.version}")
|
||
sys.exit(1)
|
||
print(f"Python 版本检查通过: {sys.version.split()[0]}")
|
||
PY
|
||
log_error "Python 版本不符合要求 (需要 3.9+)"
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
check_virtual_env() {
|
||
log_step "检查虚拟环境..."
|
||
|
||
if [[ -n "${VIRTUAL_ENV:-}" ]]; then
|
||
log_info "已激活虚拟环境: $VIRTUAL_ENV"
|
||
return 0
|
||
fi
|
||
|
||
if [[ -f "$SCRIPT_DIR/.venv/bin/activate" ]]; then
|
||
log_warn "未激活虚拟环境,自动激活 .venv"
|
||
# shellcheck disable=SC1091
|
||
source "$SCRIPT_DIR/.venv/bin/activate"
|
||
log_info "虚拟环境已激活: $VIRTUAL_ENV"
|
||
else
|
||
log_warn "未找到虚拟环境,使用系统 Python"
|
||
fi
|
||
}
|
||
|
||
check_required_commands() {
|
||
log_step "检查必要命令..."
|
||
|
||
local missing=()
|
||
|
||
if ! command -v python >/dev/null 2>&1; then
|
||
missing+=("python")
|
||
fi
|
||
|
||
if ! command -v lsof >/dev/null 2>&1; then
|
||
missing+=("lsof")
|
||
fi
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
log_error "缺少必要命令: ${missing[*]}"
|
||
log_info "请安装缺失的命令后重试"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "所有必要命令已安装"
|
||
}
|
||
|
||
check_python_modules() {
|
||
log_step "检查 Python 依赖模块..."
|
||
|
||
local modules=("fastapi" "uvicorn" "websockets" "yaml" "dotenv")
|
||
local missing=()
|
||
|
||
for module in "${modules[@]}"; do
|
||
if ! python -c "import $module" 2>/dev/null; then
|
||
missing+=("$module")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
log_error "缺少 Python 模块: ${missing[*]}"
|
||
log_info "请安装依赖: uv pip install -e '.[dev]' 或 pip install -r requirements.txt"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "所有依赖模块已安装"
|
||
}
|
||
|
||
check_env_file() {
|
||
log_step "检查环境配置文件..."
|
||
|
||
if [[ -f "$SCRIPT_DIR/.env" ]]; then
|
||
log_info "加载环境变量: .env"
|
||
set -a
|
||
# shellcheck disable=SC1091
|
||
source "$SCRIPT_DIR/.env"
|
||
set +a
|
||
else
|
||
log_warn ".env 文件不存在,使用默认配置"
|
||
log_info "提示: 复制 env.template 到 .env 并配置您的 API 密钥"
|
||
fi
|
||
}
|
||
|
||
check_ports() {
|
||
log_step "检查端口占用情况..."
|
||
|
||
local ports=($AGENT_SERVICE_PORT $TRADING_SERVICE_PORT $NEWS_SERVICE_PORT $RUNTIME_SERVICE_PORT $GATEWAY_PORT)
|
||
|
||
local occupied=()
|
||
for port in "${ports[@]}"; do
|
||
if lsof -Pi :"$port" -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||
occupied+=("$port")
|
||
fi
|
||
done
|
||
|
||
if [[ ${#occupied[@]} -gt 0 ]]; then
|
||
log_warn "以下端口已被占用: ${occupied[*]}"
|
||
log_info "尝试释放端口..."
|
||
|
||
for port in "${occupied[@]}"; do
|
||
kill_port "$port"
|
||
done
|
||
else
|
||
log_info "所有端口可用"
|
||
fi
|
||
}
|
||
|
||
kill_port() {
|
||
local port="$1"
|
||
local pids
|
||
pids=$(lsof -ti :"$port" 2>/dev/null || true)
|
||
|
||
if [[ -n "$pids" ]]; then
|
||
log_warn "释放端口 $port (PID: $pids)"
|
||
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||
sleep 0.5
|
||
fi
|
||
}
|
||
|
||
check_optional_services() {
|
||
log_step "检查可选服务..."
|
||
|
||
# 检查 npm(用于前端)
|
||
if ! command -v npm >/dev/null 2>&1; then
|
||
log_warn "npm 未安装,前端启动功能不可用"
|
||
else
|
||
log_info "npm 已安装"
|
||
fi
|
||
|
||
# 检查 OpenClaw gateway
|
||
check_openclaw_gateway
|
||
}
|
||
|
||
check_openclaw_gateway() {
|
||
local target_host="127.0.0.1"
|
||
local target_port="18789"
|
||
|
||
if python - <<PY >/dev/null 2>&1; then
|
||
import socket
|
||
sock = socket.socket()
|
||
sock.settimeout(1.0)
|
||
sock.connect(("${target_host}", ${target_port}))
|
||
sock.close()
|
||
PY
|
||
log_info "OpenClaw gateway 可连接: ws://${target_host}:${target_port}"
|
||
else
|
||
log_warn "OpenClaw gateway 未启动: ws://${target_host}:${target_port}"
|
||
log_info " OpenClaw 面板功能将不可用"
|
||
fi
|
||
}
|
||
|
||
# ============================================
|
||
# 服务启动函数
|
||
# ============================================
|
||
|
||
start_service() {
|
||
local name="$1"
|
||
local app_path="$2"
|
||
local port="$3"
|
||
|
||
log_info "启动 ${name} (端口 ${port})..."
|
||
SERVICE_NAME="${name}" python -m uvicorn "${app_path}" \
|
||
--host 0.0.0.0 \
|
||
--port "${port}" \
|
||
--reload \
|
||
--reload-dir backend \
|
||
--log-level warning \
|
||
--no-access-log &
|
||
PIDS+=($!)
|
||
}
|
||
|
||
wait_for_runtime_service() {
|
||
log_step "等待 runtime_service 就绪..."
|
||
|
||
local runtime_url="http://127.0.0.1:${RUNTIME_SERVICE_PORT}/health"
|
||
local attempts=30
|
||
|
||
for ((i=1; i<=attempts; i++)); do
|
||
if python - <<PY >/dev/null 2>&1; then
|
||
import urllib.request
|
||
with urllib.request.urlopen("${runtime_url}", timeout=1.5) as resp:
|
||
raise SystemExit(0 if resp.status == 200 else 1)
|
||
PY
|
||
log_info "runtime_service 已就绪: ${runtime_url}"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
log_error "runtime_service 未在预期时间内就绪"
|
||
return 1
|
||
}
|
||
|
||
start_managed_runtime() {
|
||
log_step "通过 runtime_service 创建默认运行时..."
|
||
|
||
local runtime_api="http://127.0.0.1:${RUNTIME_SERVICE_PORT}/api/runtime/start"
|
||
|
||
if ! python - <<PY; then
|
||
import json
|
||
import os
|
||
import sys
|
||
import urllib.request
|
||
|
||
tickers_env = os.getenv("TICKERS", "")
|
||
tickers = [item.strip().upper() for item in tickers_env.split(",") if item.strip()]
|
||
if not tickers:
|
||
tickers = ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "AMD", "NFLX", "AVGO", "PLTR", "COIN"]
|
||
|
||
def _env_int(name: str, default: int) -> int:
|
||
value = os.getenv(name, "").strip()
|
||
return int(value) if value else default
|
||
|
||
def _env_float(name: str, default: float) -> float:
|
||
value = os.getenv(name, "").strip()
|
||
return float(value) if value else default
|
||
|
||
payload = {
|
||
"launch_mode": "fresh",
|
||
"tickers": tickers,
|
||
"schedule_mode": os.getenv("SCHEDULE_MODE", "daily").strip() or "daily",
|
||
"interval_minutes": _env_int("INTERVAL_MINUTES", 60),
|
||
"trigger_time": os.getenv("TRIGGER_TIME", "09:30").strip() or "09:30",
|
||
"max_comm_cycles": _env_int("MAX_COMM_CYCLES", 2),
|
||
"initial_cash": _env_float("INITIAL_CASH", 100000.0),
|
||
"margin_requirement": _env_float("MARGIN_REQUIREMENT", 0.0),
|
||
"enable_memory": os.getenv("ENABLE_MEMORY", "").strip().lower() in {"1", "true", "yes", "on"},
|
||
"mode": os.getenv("RUNTIME_MODE", "live").strip() or "live",
|
||
"poll_interval": _env_int("POLL_INTERVAL", 10),
|
||
}
|
||
|
||
data = json.dumps(payload).encode("utf-8")
|
||
req = urllib.request.Request(
|
||
"${runtime_api}",
|
||
data=data,
|
||
headers={"Content-Type": "application/json"},
|
||
method="POST",
|
||
)
|
||
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||
body = json.loads(resp.read().decode("utf-8"))
|
||
except Exception as exc:
|
||
print(f"FAILED: {exc}", file=sys.stderr)
|
||
raise
|
||
|
||
print(json.dumps(body, ensure_ascii=False))
|
||
PY
|
||
log_error "通过 runtime_service 创建运行时失败"
|
||
return 1
|
||
fi
|
||
|
||
log_info "默认运行时已创建,Gateway 将由 runtime_service 托管"
|
||
}
|
||
|
||
# ============================================
|
||
# 微服务模式启动
|
||
# ============================================
|
||
|
||
start_microservices_mode() {
|
||
log_step "启动微服务模式..."
|
||
echo ""
|
||
echo -e "${CYAN}==========================================${NC}"
|
||
echo -e "${CYAN} 服务端点 ${NC}"
|
||
echo -e "${CYAN}==========================================${NC}"
|
||
echo -e " agent_service: http://localhost:${AGENT_SERVICE_PORT}"
|
||
echo -e " runtime_service: http://localhost:${RUNTIME_SERVICE_PORT}"
|
||
echo -e " trading_service: http://localhost:${TRADING_SERVICE_PORT}"
|
||
echo -e " news_service: http://localhost:${NEWS_SERVICE_PORT}"
|
||
echo -e " gateway: ws://localhost:${GATEWAY_PORT} (由 runtime_service 拉起)"
|
||
echo -e "${CYAN}==========================================${NC}"
|
||
echo ""
|
||
|
||
# 设置服务 URL 环境变量
|
||
export TRADING_SERVICE_URL="${TRADING_SERVICE_URL:-http://localhost:${TRADING_SERVICE_PORT}}"
|
||
export NEWS_SERVICE_URL="${NEWS_SERVICE_URL:-http://localhost:${NEWS_SERVICE_PORT}}"
|
||
export RUNTIME_SERVICE_URL="${RUNTIME_SERVICE_URL:-http://localhost:${RUNTIME_SERVICE_PORT}}"
|
||
export OPENCLAW_SERVICE_URL="${OPENCLAW_SERVICE_URL:-http://localhost:18789}"
|
||
export ENABLE_DASHBOARD_COMPAT_EXPORTS="${ENABLE_DASHBOARD_COMPAT_EXPORTS:-true}"
|
||
|
||
log_debug "环境变量:"
|
||
log_debug " TRADING_SERVICE_URL=${TRADING_SERVICE_URL}"
|
||
log_debug " NEWS_SERVICE_URL=${NEWS_SERVICE_URL}"
|
||
log_debug " RUNTIME_SERVICE_URL=${RUNTIME_SERVICE_URL}"
|
||
log_debug " OPENCLAW_SERVICE_URL=${OPENCLAW_SERVICE_URL}"
|
||
echo ""
|
||
|
||
# 启动 4 个微服务
|
||
start_service "agent_service" "backend.apps.agent_service:app" "$AGENT_SERVICE_PORT"
|
||
start_service "runtime_service" "backend.apps.runtime_service:app" "$RUNTIME_SERVICE_PORT"
|
||
start_service "trading_service" "backend.apps.trading_service:app" "$TRADING_SERVICE_PORT"
|
||
start_service "news_service" "backend.apps.news_service:app" "$NEWS_SERVICE_PORT"
|
||
|
||
wait_for_runtime_service
|
||
start_managed_runtime
|
||
|
||
echo ""
|
||
log_info "所有服务已启动"
|
||
log_info "按 Ctrl+C 停止所有服务"
|
||
echo ""
|
||
}
|
||
|
||
# ============================================
|
||
# 清理与信号处理
|
||
# ============================================
|
||
|
||
cleanup() {
|
||
if [[ ${#PIDS[@]} -gt 0 ]]; then
|
||
echo ""
|
||
log_step "正在停止服务..."
|
||
|
||
for pid in "${PIDS[@]}"; do
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
kill "$pid" 2>/dev/null || true
|
||
fi
|
||
done
|
||
|
||
# 等待进程结束
|
||
wait "${PIDS[@]}" 2>/dev/null || true
|
||
log_info "所有服务已停止"
|
||
fi
|
||
}
|
||
|
||
trap cleanup EXIT INT TERM
|
||
|
||
# ============================================
|
||
# 主程序
|
||
# ============================================
|
||
|
||
main() {
|
||
# 解析命令行参数
|
||
parse_args "$@"
|
||
|
||
# 显示启动横幅
|
||
echo ""
|
||
echo -e "${CYAN}==========================================${NC}"
|
||
echo -e "${CYAN} 大时代 Development Environment ${NC}"
|
||
echo -e "${CYAN}==========================================${NC}"
|
||
echo ""
|
||
|
||
# 切换到项目根目录
|
||
cd "$SCRIPT_DIR"
|
||
log_debug "工作目录: $SCRIPT_DIR"
|
||
|
||
# 启动前检查
|
||
check_required_commands
|
||
check_python_version
|
||
check_virtual_env
|
||
check_python_modules
|
||
check_env_file
|
||
check_ports
|
||
check_optional_services
|
||
|
||
echo ""
|
||
echo -e "${GREEN}==========================================${NC}"
|
||
echo -e "${GREEN} 启动前检查完成 ${NC}"
|
||
echo -e "${GREEN}==========================================${NC}"
|
||
echo ""
|
||
|
||
start_microservices_mode
|
||
|
||
# 等待所有后台进程
|
||
wait
|
||
}
|
||
|
||
# 执行主程序
|
||
main "$@"
|