#!/usr/bin/env bash # OpsxAPI launcher — scene profiles, not random process soup # # ./run.sh # = web:仅 Gin 官网 + docs(:8787,homepage/dist) # ./run.sh web # 同上(浏览器不当客户端) # ./run.sh gui # 桌面唯一操作入口:临时 .app(Dock 名 OpsxAPI)+ 默认 Gin # ./run.sh all # 同 gui # ./run.sh server|web-only|desktop # 单组件 # ./run.sh stop|restart|status|help # # 产品形态:浏览器 = 官网/docs(homepage/);操作 = 桌面 Tauri(src/ + src-tauri/)。 # Vite :5173 仅作桌面 WebView 源,不要用浏览器当工作台。 set -euo pipefail PROJECT_NAME="OpsxAPI" SERVER_PORT="${OPSXAPI_SERVER_PORT:-8787}" WEB_PORT="${OPSXAPI_WEB_PORT:-5173}" SERVER_DIR="server" SERVER_BIN="opsxapi-server" SERVER_LABEL="com.opsx.api.server" WEB_LABEL="com.opsx.api.web" DESKTOP_LABEL="com.opsx.api.desktop" # gui 默认是否顺带起 Gin(可用 OPSXAPI_GUI_WITH_SERVER=0 关掉) GUI_WITH_SERVER="${OPSXAPI_GUI_WITH_SERVER:-1}" RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[OK]${NC} $*"; } warning() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERR]${NC} $*"; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" LOG_DIR="$SCRIPT_DIR/logs" mkdir -p "$LOG_DIR" "$SCRIPT_DIR/$SERVER_DIR/bin" SERVER_LOG="$LOG_DIR/server.log" WEB_LOG="$LOG_DIR/web.log" DESKTOP_LOG="$LOG_DIR/desktop.log" # shellcheck source=scripts/gui-dev-app.sh source "$SCRIPT_DIR/scripts/gui-dev-app.sh" export PATH="/Users/logan/.nvm/versions/node/v24.19.0/bin:/opt/homebrew/bin:/usr/local/bin:${HOME}/.cargo/bin:$PATH" check_port() { lsof -ti:"$1" 2>/dev/null | head -1 || true; } mock_api_ready() { # /health and the original Mock route exist on older builds; require the access-control shape. curl -fsS --max-time 2 "http://127.0.0.1:$SERVER_PORT/api/mock/local" 2>/dev/null \ | grep -q '"bearerRequired"' } stop_owned_server() { stop_detached "$SERVER_LABEL" local pid cwd command_line process_name pid="$(check_port "$SERVER_PORT")" [ -z "$pid" ] && return 0 cwd="$(lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1)" command_line="$(ps -p "$pid" -o command= 2>/dev/null || true)" process_name="$(lsof -nP -a -p "$pid" -iTCP:"$SERVER_PORT" -sTCP:LISTEN -Fc 2>/dev/null | sed -n 's/^c//p' | head -1)" if [ "$process_name" = "opsxapi-server" ] && { [ "$cwd" = "$SCRIPT_DIR/server" ] || [ "$cwd" = "$SCRIPT_DIR/apps/server" ]; }; then info "停止本项目旧后端 (PID $pid)" kill "$pid" 2>/dev/null || { error "无法停止本项目后端 (PID $pid)"; return 1; } return 0 fi case "$command_line:$cwd" in *"$SCRIPT_DIR/server/bin/opsxapi-server"*|*"$SCRIPT_DIR/apps/server/bin/opsxapi-server"*|*"opsxapi-server:$SCRIPT_DIR/server"|*"opsxapi-server:$SCRIPT_DIR/apps/server") info "停止本项目旧后端 (PID $pid)" kill "$pid" 2>/dev/null || { error "无法停止本项目后端 (PID $pid)"; return 1; } return 0 ;; esac error "端口 $SERVER_PORT 被其他进程占用 (PID $pid),未停止该进程" return 1 } kill_port() { local pids pids="$(lsof -ti:"$1" 2>/dev/null || true)" if [ -n "$pids" ]; then info "释放端口 $1" # shellcheck disable=SC2086 kill -9 $pids 2>/dev/null || true sleep 0.3 fi } start_detached() { local label="$1" log_file="$2" command="$3" if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then launchctl remove "$label" 2>/dev/null || true launchctl submit -l "$label" -o "$log_file" -e "$log_file" -- /bin/bash -lc "$command" else nohup /bin/bash -lc "$command" >>"$log_file" 2>&1 /dev/null 2>&1; then launchctl remove "$label" 2>/dev/null || true fi } ensure_npm() { if [ ! -d "$SCRIPT_DIR/node_modules" ] || ! npm ls --depth=0 >/dev/null 2>&1; then warning "桌面依赖缺失或与 package.json 不一致,正在 npm install..." npm install || { error "桌面依赖安装失败,请检查 npm registry 与网络后运行 npm install"; return 1; } fi } build_server() { info "编译 Gin 服务..." (cd "$SCRIPT_DIR/$SERVER_DIR" && go build -o "$SCRIPT_DIR/$SERVER_DIR/bin/$SERVER_BIN" ./cmd/opsxapi) } wait_http() { local url="$1" name="$2" retries="${3:-60}" local i=0 while ! curl -fsS "$url" >/dev/null 2>&1; do i=$((i + 1)) if [ "$i" -ge "$retries" ]; then error "$name 启动超时:$url" return 1 fi sleep 0.5 done } start_server() { if mock_api_ready; then success "后端已在运行 :$SERVER_PORT" return 0 fi if [ -n "$(check_port "$SERVER_PORT")" ]; then warning "现有后端缺少 Mock 管理接口,准备更新本项目服务" stop_owned_server local retries=0 while [ -n "$(check_port "$SERVER_PORT")" ]; do retries=$((retries + 1)) if [ "$retries" -ge 20 ]; then error "旧后端未能释放端口 ${SERVER_PORT},请检查占用进程" return 1 fi sleep 0.25 done fi info "启动后端 :$SERVER_PORT ..." : >"$SERVER_LOG" build_server # 默认仅本机;对外需 OPSXAPI_BIND_PUBLIC=1(或显式 OPSXAPI_ADDR) local bind_env="" if [ -n "${OPSXAPI_ADDR:-}" ]; then bind_env="OPSXAPI_ADDR=$OPSXAPI_ADDR" elif [ "${OPSXAPI_BIND_PUBLIC:-0}" = "1" ] || [ "${OPSXAPI_BIND_PUBLIC:-}" = "true" ]; then bind_env="OPSXAPI_BIND_PUBLIC=1" else bind_env="OPSXAPI_ADDR=127.0.0.1:$SERVER_PORT" fi start_detached "$SERVER_LABEL" "$SERVER_LOG" \ "export PATH=\"/opt/homebrew/bin:/usr/local/bin:\$PATH\"; cd \"$SCRIPT_DIR/$SERVER_DIR\" && exec env $bind_env OPSXAPI_MOCK_ADDR=\"${OPSXAPI_MOCK_ADDR:-0.0.0.0:8798}\" OPSXAPI_MOCK_ALLOW_PUBLIC=\"${OPSXAPI_MOCK_ALLOW_PUBLIC:-0}\" GIN_MODE=debug \"./bin/$SERVER_BIN\"" wait_http "http://127.0.0.1:$SERVER_PORT/health" "后端" if ! mock_api_ready; then error "后端已启动,但 Mock 管理接口仍不可用,请检查 $SERVER_LOG" return 1 fi success "后端 UP http://127.0.0.1:$SERVER_PORT/health" } stop_server() { stop_owned_server || return 1 success "后端已停止" } start_vite_for_desktop() { ensure_npm if curl -fsS "http://127.0.0.1:$WEB_PORT/" >/dev/null 2>&1; then success "桌面 UI(Vite) 已在运行 :$WEB_PORT" return 0 fi info "启动桌面 UI 源 :${WEB_PORT}(仅本机给 Tauri,勿当浏览器客户端)..." : >"$WEB_LOG" kill_port "$WEB_PORT" nohup /bin/bash -lc "export PATH=\"/Users/logan/.nvm/versions/node/v24.19.0/bin:/opt/homebrew/bin:/usr/local/bin:\$PATH\"; cd \"$SCRIPT_DIR\" && exec npm run dev" >>"$WEB_LOG" 2>&1 "$DESKTOP_LOG" ensure_npm kill_opsxapi_gui ensure_gui_dev_app || return 1 # 注意:不能用 launchctl submit 跑 open —— submit 任务有自动重生语义, # open 立即退出会被 launchd 反复重启,导致桌面应用每 ~20s 多开一个实例。 # 这里直接同步 open(不带 -n:万一已有实例则聚焦,不再新开)。 open "$GUI_APP" 2>>"$DESKTOP_LOG" || { error "open 失败,见 $DESKTOP_LOG"; return 1; } local i=0 while true; do if gui_is_running; then success "桌面 OpsxAPI 已启动(临时 .app,Dock 用当前 icon.icns)" info "路径: $GUI_APP" return 0 fi i=$((i + 1)) if [ "$i" -ge 30 ]; then error "桌面启动超时,见 $DESKTOP_LOG" tail -n 40 "$DESKTOP_LOG" || true return 1 fi sleep 1 done } stop_desktop() { kill_opsxapi_gui success "桌面已停止" } status() { echo "$PROJECT_NAME 状态" if curl -fsS "http://127.0.0.1:$SERVER_PORT/health" >/dev/null 2>&1; then success "server :$SERVER_PORT UP $(curl -fsS "http://127.0.0.1:$SERVER_PORT/health")" else warning "server :$SERVER_PORT DOWN" fi if curl -fsS "http://127.0.0.1:$WEB_PORT/" >/dev/null 2>&1; then success "vite :$WEB_PORT UP(桌面源,非浏览器产品)" else warning "vite :$WEB_PORT DOWN" fi if curl -fsS "http://127.0.0.1:$SERVER_PORT/" >/dev/null 2>&1 \ || curl -fsS "http://127.0.0.1:$SERVER_PORT/docs" >/dev/null 2>&1; then success "site :$SERVER_PORT 官网/docs 可达" fi if gui_is_running; then success "gui OpsxAPI UP" else warning "gui OpsxAPI DOWN" fi } usage() { cat <