feat(site): 并入 Gin 官网与文档服务,对齐桌面产品形态
CI / Lint & TypeCheck (push) Canceled after 0s
CI / Test (push) Canceled after 0s
CI / Tauri Foundation Acceptance (push) Canceled after 0s
CI / Build (macos-latest) (push) Canceled after 0s
CI / Build (ubuntu-latest) (push) Canceled after 0s
CI / Build (windows-latest) (push) Canceled after 0s

- apps/server:Gin embed 官网 + /docs + /health + /download(无操作 API)
- run.sh 重写 web/gui 场景;.gitignore 排除 server 二进制与挂载的安装包
- README/AGENTS/CLAUDE 同步「浏览器只看官网、操作走桌面」的产品形态说明
- 终端视口背景变量与全宽终端边缘 hairline 修正(serial/terminal/i18n 微调)
This commit is contained in:
Logan.Li committed 2026-10-10 13:35:01 +08:00
1 parent b18214e96a
commit 5eb11826db
36 files changed
+3676 -144

No files matched your search

+309 -36
View File
@@ -1,17 +1,43 @@
#!/bin/bash
set -e
#!/usr/bin/env bash
# OpsXShell launcher — scene profiles(对齐 opsxdb / opsxapi)
#
# ./run.sh # = web:仅 Gin 官网 + docs(:8789)
# ./run.sh web # 同上(浏览器不当操作客户端)
# ./run.sh gui # 桌面唯一操作入口:Tauri;默认也起 Gin
# ./run.sh all # 同 gui(兼容旧命令)
# ./run.sh server|desktop
# ./run.sh stop|restart|status|help
#
# 产品形态:浏览器 = 官网/docs;操作 = 桌面 Tauri(src/ + src-tauri/)。
# Vite :1420 仅作桌面 WebView 源,不要用浏览器当工作台。
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
set -euo pipefail
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
PROJECT_NAME="OpsXShell"
SERVER_PORT="${OPSXSHELL_SERVER_PORT:-8789}"
WEB_PORT="${OPSXSHELL_WEB_PORT:-1420}"
SERVER_DIR="apps/server"
SERVER_BIN="opsxshell-server"
SERVER_LABEL="com.opsx.shell.server"
WEB_LABEL="com.opsx.shell.web"
DESKTOP_LABEL="com.opsx.shell.desktop"
PID_FILE="/tmp/opsxshell.pid"
# gui 默认是否顺带起 Gin(可用 OPSXSHELL_GUI_WITH_SERVER=0 关掉)
GUI_WITH_SERVER="${OPSXSHELL_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"
# Same path as src-tauri infrastructure/file_log.rs (dirs::data_local_dir).
if [ "$(uname -s)" = "Darwin" ]; then
@@ -20,34 +46,281 @@ else
APP_LOG="${XDG_DATA_HOME:-$HOME/.local/share}/opsxshell/logs/opsxshell.log"
fi
echo -e "${CYAN}🦐 启动 OpsXShell...${NC}"
export PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/.cargo/bin:$PATH"
# 1. 清理旧进程
echo -e "${YELLOW}🧹 清理旧进程...${NC}"
pkill -f "vite" 2>/dev/null || true
pkill -f "opsxshell" 2>/dev/null || true
sleep 1
NODE_BIN_DIR=""
detect_node() {
if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
NODE_BIN_DIR="$(cd "$(dirname "$(command -v node)")" && pwd)"
return 0
fi
local roots=() d v best=""
[ -n "${NVM_DIR:-}" ] && roots+=("$NVM_DIR/versions/node")
roots+=("${HOME}/.nvm/versions/node")
for d in "${roots[@]}"; do
[ -d "$d" ] || continue
v="$(ls -1 "$d" 2>/dev/null | sort -V | tail -n 1)"
if [ -n "$v" ] && [ -x "$d/$v/bin/node" ]; then
best="$d/$v/bin"
break
fi
done
if [ -z "$best" ]; then
for d in /opt/homebrew/bin /usr/local/bin; do
if [ -x "$d/node" ]; then
best="$d"
break
fi
done
fi
if [ -z "$best" ]; then
error "未找到 node/npm。请先安装 Node.js 或 nvm。"
exit 1
fi
NODE_BIN_DIR="$best"
export PATH="$NODE_BIN_DIR:$PATH"
info "使用 Node.js: $NODE_BIN_DIR"
}
# 2. 检查依赖
if [ ! -d "node_modules" ]; then
echo -e "${YELLOW}📦 安装依赖...${NC}"
npm install
fi
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
}
# 3. 启动(原生 tracing 写入应用日志目录;Vite 输出留在本终端)
echo -e "${GREEN}⚡ 启动 OpsXShell...${NC}"
npm run dev &
VITE_PID=$!
echo $VITE_PID >"$PID_FILE"
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
}
# 4. 等待启动
sleep 3
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
}
echo ""
echo -e "${GREEN}🎉 OpsXShell 已启动!${NC}"
echo -e "${CYAN}📄 应用日志: tail -f \"${APP_LOG}\"${NC}"
echo -e "${CYAN}📦 轮转: 单文件 ≤10MB,最多 opsxshell.log + .1 + .2(正式版同路径)${NC}"
echo -e "${CYAN}🔍 DevTools: 菜单「视图 → 开发者工具」,或 Cmd+Option+I / F12(前端 console)${NC}"
ensure_npm() {
detect_node
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
warning "缺少 node_modules,正在 npm install..."
npm install
fi
}
# 等待子进程
wait $VITE_PID 2>/dev/null || true
build_server() {
info "编译 Gin 官网/docs..."
(
cd "$SCRIPT_DIR/$SERVER_DIR"
export GOPROXY="${GOPROXY:-https://goproxy.io,direct}"
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" \
-o "$SCRIPT_DIR/$SERVER_DIR/bin/$SERVER_BIN" ./cmd/opsxshell-site
)
}
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 curl -fsS "http://127.0.0.1:$SERVER_PORT/health" >/dev/null 2>&1; then
success "官网服务已在运行 :$SERVER_PORT"
return 0
fi
info "启动官网/docs :$SERVER_PORT ..."
: >"$SERVER_LOG"
kill_port "$SERVER_PORT"
build_server
local bind_env="OPSXSHELL_ADDR=127.0.0.1:$SERVER_PORT"
if [ -n "${OPSXSHELL_ADDR:-}" ]; then
bind_env="OPSXSHELL_ADDR=$OPSXSHELL_ADDR"
elif [ "${OPSXSHELL_BIND_PUBLIC:-0}" = "1" ] || [ "${OPSXSHELL_BIND_PUBLIC:-}" = "true" ]; then
bind_env="OPSXSHELL_BIND_PUBLIC=1"
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 GIN_MODE=debug \"./bin/$SERVER_BIN\""
wait_http "http://127.0.0.1:$SERVER_PORT/health" "官网服务"
success "官网 UP http://127.0.0.1:$SERVER_PORT/ · docs → http://127.0.0.1:$SERVER_PORT/docs"
}
stop_server() {
stop_detached "$SERVER_LABEL"
kill_port "$SERVER_PORT"
success "官网服务已停止"
}
# 仅供桌面 Tauri devUrl;不是浏览器产品入口
start_vite_for_desktop() {
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"
ensure_npm
kill_port "$WEB_PORT"
start_detached "$WEB_LABEL" "$WEB_LOG" \
"export PATH=\"$NODE_BIN_DIR:/opt/homebrew/bin:/usr/local/bin:\$PATH\"; cd \"$SCRIPT_DIR\" && exec npm run dev:ui"
wait_http "http://127.0.0.1:$WEB_PORT/" "Vite" 80
success "桌面 UI(Vite) UP http://127.0.0.1:$WEB_PORT(请用 ./run.sh gui)"
}
stop_web() {
stop_detached "$WEB_LABEL"
kill_port "$WEB_PORT"
success "Vite 已停止"
}
start_desktop() {
info "启动桌面 OpsXShell ..."
: >"$DESKTOP_LOG"
ensure_npm
# 清理旧进程
pkill -f 'target/debug/opsxshell' 2>/dev/null || true
pkill -f 'target/debug/OpsXShell' 2>/dev/null || true
stop_detached "$DESKTOP_LABEL"
# tauri dev 会自己起 Vite;detached 跑 npm run dev
start_detached "$DESKTOP_LABEL" "$DESKTOP_LOG" \
"export PATH=\"$NODE_BIN_DIR:/opt/homebrew/bin:/usr/local/bin:\$HOME/.cargo/bin:\$PATH\"; cd \"$SCRIPT_DIR\" && exec npm run dev"
local i=0
while true; do
if pgrep -f 'target/debug/opsxshell' >/dev/null 2>&1 \
|| pgrep -f 'target/debug/OpsXShell' >/dev/null 2>&1; then
success "桌面 OpsXShell 已启动"
info "应用日志: tail -f \"$APP_LOG\""
return 0
fi
if grep -Eiq 'error:|panicked|could not compile' "$DESKTOP_LOG" 2>/dev/null; then
error "桌面启动失败,见 $DESKTOP_LOG"
tail -n 40 "$DESKTOP_LOG" || true
return 1
fi
i=$((i + 1))
if [ "$i" -ge 180 ]; then
error "桌面启动超时,见 $DESKTOP_LOG"
tail -n 40 "$DESKTOP_LOG" || true
return 1
fi
sleep 1
done
}
stop_desktop() {
stop_detached "$DESKTOP_LABEL"
pkill -f 'target/debug/opsxshell' 2>/dev/null || true
pkill -f 'target/debug/OpsXShell' 2>/dev/null || true
pkill -f 'node_modules/.bin/tauri' 2>/dev/null || true
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 pgrep -f 'target/debug/opsxshell' >/dev/null 2>&1 \
|| pgrep -f 'target/debug/OpsXShell' >/dev/null 2>&1; then
success "gui OpsXShell UP"
else
warning "gui OpsXShell DOWN"
fi
}
usage() {
cat <<USAGE
$PROJECT_NAME 启动脚本(产品形态:浏览器=官网/docs · 操作=桌面)
./run.sh # 同 web
./run.sh web # 仅 Gin:官网 + docs → http://127.0.0.1:$SERVER_PORT/
./run.sh gui # 桌面唯一操作入口(Tauri;默认也起 Gin)
./run.sh all # 同 gui(兼容旧名)
./run.sh server # 只 Gin(官网/docs)
./run.sh desktop # 只 Desktop
./run.sh stop | restart | status | help
端口: Gin/官网 $SERVER_PORT · 桌面 Vite 源 $WEB_PORT(不对外当客户端)
日志: logs/{server,web,desktop}.log · 应用日志见 $APP_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(OPSXSHELL_GUI_WITH_SERVER=0)"
fi
start_desktop
echo
success "GUI 场景就绪 → 桌面窗口 OpsXShell"
}
start_all_scene() {
start_gui_scene
}
stop_all() {
stop_desktop || true
stop_web || true
stop_server || true
}
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