#!/usr/bin/env bash
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
log_info() { echo -e "${CYAN}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_err() { echo -e "${RED}[ERR]${NC} $*"; }
section() {
echo ""
echo -e "${BOLD}============================================${NC}"
echo -e "${BOLD} $*${NC}"
echo -e "${BOLD}============================================${NC}"
}
POLYMIND_DIR="$HOME/.polymind"
ENV_FILE="$POLYMIND_DIR/.env"
PROFILE_FILE="$POLYMIND_DIR/.profile"
BACKEND_PORT="${BACKEND_PORT:-8000}"
FRONTEND_PORT="${FRONTEND_PORT:-3000}"
NEXTJS_UPSTREAM_PORT="${NEXTJS_UPSTREAM_PORT:-3001}"
CMD_BACKEND="witty-service"
CMD_FRONTEND="polymind"
PID_FILE="$POLYMIND_DIR/runtime.pid"
NGINX_DIR="$POLYMIND_DIR/nginx"
NGINX_CONF_TEMPLATE="$NGINX_DIR/nginx.conf.template"
NGINX_CONF="$NGINX_DIR/nginx.conf"
ARG_STOP=false
ARG_STATUS=false
usage() {
echo "用法: $0 [OPTIONS]"
echo ""
echo "选项:"
echo " --stop 停止所有运行中的 PolyMind 服务"
echo " --status 查看服务运行状态"
echo " -h, --help 显示此帮助信息"
exit 0
}
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--stop) ARG_STOP=true ;;
--status) ARG_STATUS=true ;;
-h|--help) usage ;;
*)
log_err "未知参数: $1"
usage
;;
esac
shift
done
}
parse_args "$@"
check_port() {
local port=$1
if command -v ss &> /dev/null; then
if ss -tuln | grep -q ":$port "; then
return 1
fi
elif command -v lsof &> /dev/null; then
if lsof -Pi ":$port" -sTCP:LISTEN -t &> /dev/null; then
return 1
fi
elif command -v netstat &> /dev/null; then
if netstat -tuln | grep -q ":$port "; then
return 1
fi
else
return 0
fi
return 0
}
get_port_pid() {
local port=$1
if command -v ss &> /dev/null; then
ss -tulnp | grep ":$port " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1
elif command -v lsof &> /dev/null; then
lsof -Pi ":$port" -sTCP:LISTEN -t 2>/dev/null | head -1
elif command -v netstat &> /dev/null; then
netstat -tulnp 2>/dev/null | grep ":$port " | awk '{print $NF}' | sed 's/\/.*//' | head -1
fi
}
kill_port_process() {
local port=$1
local pid
pid=$(get_port_pid "$port")
if [ -n "$pid" ]; then
log_warn "端口 $port 被进程 PID=$pid 占用,正在终止..."
kill "$pid" 2>/dev/null || true
sleep 1
if kill -0 "$pid" 2>/dev/null; then
log_warn "进程未响应,强制终止..."
kill -9 "$pid" 2>/dev/null || true
sleep 1
fi
if check_port "$port"; then
log_ok "端口 $port 已释放"
else
log_err "端口 $port 释放失败"
return 1
fi
fi
return 0
}
save_pids() {
mkdir -p "$POLYMIND_DIR"
echo "BACKEND_PID=$BACKEND_PID" > "$PID_FILE"
echo "FRONTEND_PID=$FRONTEND_PID" >> "$PID_FILE"
echo "NGINX_PID=$NGINX_PID" >> "$PID_FILE"
echo "BACKEND_PORT=$BACKEND_PORT" >> "$PID_FILE"
echo "FRONTEND_PORT=$FRONTEND_PORT" >> "$PID_FILE"
}
load_pids() {
if [ -f "$PID_FILE" ]; then
BACKEND_PID=$(grep '^BACKEND_PID=' "$PID_FILE" | cut -d'=' -f2)
FRONTEND_PID=$(grep '^FRONTEND_PID=' "$PID_FILE" | cut -d'=' -f2)
NGINX_PID=$(grep '^NGINX_PID=' "$PID_FILE" | cut -d'=' -f2)
BACKEND_PORT=$(grep '^BACKEND_PORT=' "$PID_FILE" | cut -d'=' -f2)
FRONTEND_PORT=$(grep '^FRONTEND_PORT=' "$PID_FILE" | cut -d'=' -f2)
fi
}
generate_nginx_config() {
if [ ! -f "$NGINX_CONF_TEMPLATE" ]; then
log_err "nginx 配置模板不存在: $NGINX_CONF_TEMPLATE"
log_err "请重新运行 install.sh 生成模板"
return 1
fi
sed \
-e "s|{{POLYMIND_DIR}}|$POLYMIND_DIR|g" \
-e "s|{{FRONTEND_PORT}}|$FRONTEND_PORT|g" \
-e "s|{{BACKEND_PORT}}|$BACKEND_PORT|g" \
-e "s|{{NEXTJS_UPSTREAM_PORT}}|$NEXTJS_UPSTREAM_PORT|g" \
"$NGINX_CONF_TEMPLATE" > "$NGINX_CONF"
sed -i "1i user $(whoami);" "$NGINX_CONF"
mkdir -p "$NGINX_DIR/tmp/client_body" "$NGINX_DIR/tmp/proxy" "$NGINX_DIR/tmp/fastcgi" "$NGINX_DIR/tmp/uwsgi" "$NGINX_DIR/tmp/scgi"
log_ok "nginx 配置已生成: $NGINX_CONF"
}
start_nginx() {
generate_nginx_config || return 1
log_info "测试 nginx 配置..."
if ! nginx -t -c "$NGINX_CONF" 2>&1; then
log_err "nginx 配置测试失败"
return 1
fi
log_info "启动 nginx (端口 $FRONTEND_PORT)..."
nginx -c "$NGINX_CONF"
sleep 1
NGINX_PID=$(cat "$NGINX_DIR/nginx.pid" 2>/dev/null || true)
if [ -n "$NGINX_PID" ] && kill -0 "$NGINX_PID" 2>/dev/null; then
log_ok "nginx 已启动 PID=$NGINX_PID 端口=$FRONTEND_PORT"
return 0
else
log_err "nginx 启动失败"
return 1
fi
}
stop_nginx() {
log_info "停止 nginx..."
if [ -f "$NGINX_DIR/nginx.pid" ]; then
local pid
pid=$(cat "$NGINX_DIR/nginx.pid" 2>/dev/null || true)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
nginx -c "$NGINX_CONF" -s quit 2>/dev/null || true
sleep 1
if kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null || true
fi
log_ok "nginx 已停止 (PID=$pid)"
fi
rm -f "$NGINX_DIR/nginx.pid"
else
local pid
pid=$(get_port_pid "$FRONTEND_PORT")
if [ -n "$pid" ]; then
nginx -s quit 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
log_ok "nginx 已停止 (端口 $FRONTEND_PORT)"
else
log_info "nginx 未在运行"
fi
fi
}
show_status() {
echo ""
echo -e "${BOLD}PolyMind 服务状态${NC}"
echo ""
load_pids
if [ -n "${BACKEND_PID:-}" ] && kill -0 "$BACKEND_PID" 2>/dev/null; then
echo -e " 后端 (witty-service): ${GREEN}运行中${NC} PID=$BACKEND_PID http://127.0.0.1:${BACKEND_PORT:-8000}"
else
echo -e " 后端 (witty-service): ${RED}未运行${NC}"
fi
if [ -n "${FRONTEND_PID:-}" ] && kill -0 "$FRONTEND_PID" 2>/dev/null; then
echo -e " 前端 (polymind): ${GREEN}运行中${NC} PID=$FRONTEND_PID 内部端口=${NEXTJS_UPSTREAM_PORT:-3001}"
else
echo -e " 前端 (polymind): ${RED}未运行${NC}"
fi
if [ -n "${NGINX_PID:-}" ] && kill -0 "$NGINX_PID" 2>/dev/null; then
echo -e " 代理 (nginx): ${GREEN}运行中${NC} PID=$NGINX_PID 端口=${FRONTEND_PORT:-3000}"
else
echo -e " 代理 (nginx): ${RED}未运行${NC}"
fi
echo ""
local backend_port="${BACKEND_PORT:-8000}"
local frontend_port="${FRONTEND_PORT:-3000}"
local nextjs_port="${NEXTJS_UPSTREAM_PORT:-3001}"
if [ -z "${BACKEND_PID:-}" ] && ! check_port "$backend_port"; then
echo -e " ${YELLOW}注意: 端口 $backend_port 被占用${NC}"
fi
if [ -z "${NGINX_PID:-}" ] && ! check_port "$frontend_port"; then
echo -e " ${YELLOW}注意: 端口 $frontend_port 被占用${NC}"
fi
if [ -z "${FRONTEND_PID:-}" ] && ! check_port "$nextjs_port"; then
echo -e " ${YELLOW}注意: 端口 $nextjs_port 被占用${NC}"
fi
}
stop_services() {
section "停止 PolyMind 服务"
load_pids
if [ -n "${FRONTEND_PID:-}" ] && kill -0 "$FRONTEND_PID" 2>/dev/null; then
log_info "停止前端 (PID=$FRONTEND_PID)..."
kill "$FRONTEND_PID" 2>/dev/null || true
sleep 1
kill -0 "$FRONTEND_PID" 2>/dev/null && kill -9 "$FRONTEND_PID" 2>/dev/null || true
log_ok "前端已停止"
else
log_info "前端未在运行"
fi
if [ -n "${BACKEND_PID:-}" ] && kill -0 "$BACKEND_PID" 2>/dev/null; then
log_info "停止后端 (PID=$BACKEND_PID)..."
kill "$BACKEND_PID" 2>/dev/null || true
sleep 1
kill -0 "$BACKEND_PID" 2>/dev/null && kill -9 "$BACKEND_PID" 2>/dev/null || true
log_ok "后端已停止"
else
log_info "后端未在运行"
fi
stop_nginx
local ports=("$BACKEND_PORT" "$FRONTEND_PORT" "$NEXTJS_UPSTREAM_PORT")
for p in "${ports[@]}"; do
if ! check_port "$p"; then
kill_port_process "$p" || true
fi
done
rm -f "$PID_FILE"
log_ok "所有服务已停止"
}
activate_environment() {
local _saved_path="$PATH"
if [ -s "$PROFILE_FILE" ]; then
source "$PROFILE_FILE"
export PATH="$PATH:$_saved_path"
else
log_warn "隔离环境配置文件不存在: $PROFILE_FILE"
log_warn "建议先运行 install.sh 以创建隔离环境"
fi
}
run_precheck() {
section "0/4 启动前检查"
activate_environment
local missing=0
if ! command -v "$CMD_BACKEND" &> /dev/null; then
log_err "$CMD_BACKEND 未安装,请先运行 install.sh"
missing=1
else
log_ok "$CMD_BACKEND 已就绪"
fi
if ! command -v "$CMD_FRONTEND" &> /dev/null; then
log_err "$CMD_FRONTEND 未安装,请先运行 install.sh"
missing=1
else
log_ok "$CMD_FRONTEND 已就绪"
fi
if ! command -v nginx &> /dev/null; then
log_err "nginx 未安装,请先运行 install.sh"
missing=1
else
log_ok "nginx $(nginx -v 2>&1 | cut -d'/' -f2) 已就绪"
fi
if [ -f "$ENV_FILE" ]; then
log_ok "配置文件: $ENV_FILE"
else
log_warn "配置文件不存在,将使用默认配置"
fi
if [ "$missing" -eq 1 ]; then
exit 1
fi
}
configure_network() {
section "1/4 网络配置"
if [ -f "$ENV_FILE" ] && grep -q '^BACKEND_HOST=' "$ENV_FILE" 2>/dev/null; then
BACKEND_HOST=$(grep '^BACKEND_HOST=' "$ENV_FILE" | head -1 | cut -d'=' -f2-)
else
BACKEND_HOST="127.0.0.1"
fi
log_info "nginx 反向代理模式(同源访问,无需 CORS)"
log_info "后端地址: $BACKEND_HOST:$BACKEND_PORT"
}
prepare_ports() {
if ! check_port "$BACKEND_PORT"; then
kill_port_process "$BACKEND_PORT" || exit 1
fi
if ! check_port "$FRONTEND_PORT"; then
kill_port_process "$FRONTEND_PORT" || exit 1
fi
if ! check_port "$NEXTJS_UPSTREAM_PORT"; then
kill_port_process "$NEXTJS_UPSTREAM_PORT" || exit 1
fi
}
start_backend() {
section "2/4 启动后端"
log_info "启动后端 $CMD_BACKEND (端口 $BACKEND_PORT) ..."
setsid $CMD_BACKEND --host 127.0.0.1 --port "$BACKEND_PORT" > "$POLYMIND_DIR/backend.log" 2>&1 &
BACKEND_PID=$!
sleep 2
if kill -0 "$BACKEND_PID" 2>/dev/null; then
log_ok "后端已启动 PID=$BACKEND_PID http://127.0.0.1:$BACKEND_PORT (日志: $POLYMIND_DIR/backend.log)"
else
log_err "后端启动失败,请检查日志: $POLYMIND_DIR/backend.log"
exit 1
fi
}
start_frontend() {
section "3/4 启动前端"
log_info "启动前端 $CMD_FRONTEND (内部端口 $NEXTJS_UPSTREAM_PORT) ..."
BACKEND_HOST="${BACKEND_HOST:-127.0.0.1}" \
BACKEND_PORT="$BACKEND_PORT" \
FRONTEND_PORT="$FRONTEND_PORT" \
setsid $CMD_FRONTEND --port "$NEXTJS_UPSTREAM_PORT" --host "127.0.0.1" > "$POLYMIND_DIR/frontend.log" 2>&1 &
FRONTEND_PID=$!
sleep 2
if kill -0 "$FRONTEND_PID" 2>/dev/null; then
log_ok "前端已启动 PID=$FRONTEND_PID 内部端口=$NEXTJS_UPSTREAM_PORT"
else
log_err "前端启动失败,请检查日志: $POLYMIND_DIR/frontend.log"
kill "$BACKEND_PID" 2>/dev/null
exit 1
fi
}
print_done() {
section "4/4 启动完成"
echo ""
echo -e " 访问地址: ${BOLD}http://localhost:$FRONTEND_PORT${NC}"
echo -e " 代理模式: ${GREEN}nginx 反向代理${NC}"
echo -e " / → Next.js (127.0.0.1:$NEXTJS_UPSTREAM_PORT)"
echo -e " /api/* → witty-service (127.0.0.1:$BACKEND_PORT)"
echo ""
echo -e " nginx PID: $NGINX_PID"
echo -e " 后端 PID: $BACKEND_PID"
echo -e " 前端 PID: $FRONTEND_PID"
echo ""
echo -e " 停止服务: ${YELLOW}bash start.sh --stop${NC}"
echo -e " 查看状态: ${YELLOW}bash start.sh --status${NC}"
echo -e " 修改配置: ${YELLOW}$ENV_FILE${NC}"
echo ""
}
main() {
if $ARG_STATUS; then
show_status
exit 0
fi
if $ARG_STOP; then
stop_services
exit 0
fi
run_precheck
configure_network
prepare_ports
start_backend
start_nginx || {
log_err "nginx 启动失败"
kill "$BACKEND_PID" 2>/dev/null
exit 1
}
start_frontend
save_pids
print_done
}
main