Files
logan 68a933e6fb 端口规划落地:Mock 默认挪 8798,8788 让给 OpsxDB;/home 落地页别名
- 本机端口约定:8787=OpsxAPI / 8788=OpsxDB / 8789=OpsXShell;mock 独立监听默认改 0.0.0.0:8798(run.sh、main.go、api.go 三处同步,README 4 处引用更新)
- mountHomepage 增加 /home、/index.html → / 别名:静态站导航里的 /home 链接不再 404
- 三站资产扫描零断链(含 opsxshell 下载 dmg 已就位)
2026-10-16 03:11:50 +08:00

324 lines
11 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 &
fi
}
stop_detached() {
local label="$1"
if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/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 </dev/null &
wait_http "http://127.0.0.1:$WEB_PORT/" "Vite" 80 || { tail -n 20 "$WEB_LOG"; return 1; }
success "桌面 UI(Vite) UP http://127.0.0.1:${WEB_PORT}(请用 ./run.sh gui)"
}
start_web() { start_vite_for_desktop; }
stop_web() {
stop_detached "$WEB_LABEL"
kill_port "$WEB_PORT"
success "Vite 已停止"
}
start_desktop() {
start_vite_for_desktop
info "启动桌面 OpsxAPI(临时 .app + 当前图标)..."
: >"$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 <<USAGE
$PROJECT_NAME 启动脚本(场景 profile)
./run.sh # 同 web
./run.sh web # 仅 Gin:官网 + docs → http://127.0.0.1:$SERVER_PORT/
./run.sh gui # 桌面:临时 .app(Dock=OpsxAPI / icon.icns;默认也起 Gin)
./run.sh all # 同 gui
./run.sh server # 只 Gin(官网/docs + 代理 API)
./run.sh web-only # 只 Vite(桌面源,不是浏览器工作台)
./run.sh desktop # 只 Desktop(会确保 Vite)
./run.sh stop | restart | status | help
端口: server $SERVER_PORT / web $WEB_PORT
日志: logs/{server,web,desktop}.log
USAGE
}
start_web_scene() {
start_server
stop_web || true
echo
success "官网/文档就绪 → http://127.0.0.1:$SERVER_PORT/ · docs → http://127.0.0.1:$SERVER_PORT/docs"
info "操作请用 ./run.sh gui(桌面)。:$WEB_PORT 不是产品入口。"
}
start_gui_scene() {
if [ "$GUI_WITH_SERVER" = "1" ]; then
start_server
else
info "跳过 Gin(OPSXAPI_GUI_WITH_SERVER=0);桌面 HTTP 走 tauri-native"
fi
start_desktop
echo
success "GUI 场景就绪 → 桌面窗口 OpsxAPI"
}
start_all_scene() {
start_gui_scene
}
stop_all() {
stop_desktop || true
stop_web || true
stop_server
}
cmd="${1:-web}"
case "$cmd" in
web|start) start_web_scene ;;
gui|desktop-scene) start_gui_scene ;;
all) start_all_scene ;;
server) start_server ;;
web-only|vite)
warning "Vite 仅桌面源,不是浏览器工作台;请用: ./run.sh gui"
start_vite_for_desktop
;;
desktop|gui-only) start_desktop ;;
stop) stop_all ;;
restart)
stop_all || true
start_web_scene
;;
status) status ;;
help|-h|--help) usage ;;
*) error "未知命令: $cmd"; usage; exit 1 ;;
esac