chore: track Linux deploy scripts with forced git pull

Ship start/restart/pull-and-restart and deploy lib so production can one-line update; git_pull_hard uses checkout -f, restart --pull aligns then restarts.
This commit is contained in:
whm
2026-08-05 14:45:06 +08:00
parent 3c9fe46359
commit f90245db9c
6 changed files with 580 additions and 8 deletions

11
.gitignore vendored
View File

@@ -22,13 +22,7 @@ ai-service/.env
# 本地意见稿 / 不入库
database_fastapi修改意见.md
# 一键部署脚本(本机/运维侧保留,不推远程)
start.sh
stop.sh
restart.sh
pull-and-restart.sh
reload-config.sh
deploy-menu.sh
# Windows 本机启动脚本不入库Linux 一键脚本入库以便生产 pull 更新
start.bat
start.ps1
stop.bat
@@ -36,6 +30,7 @@ stop.ps1
restart.bat
restart.ps1
start-host.ps1
scripts/lib-aijz-deploy.sh
deploy-menu.sh
reload-config.sh
scripts/linux/

13
pull-and-restart.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# AI 建站 — 拉取代码并重启(生产一键更新)
# 用法:./pull-and-restart.sh
# 等价:./restart.sh --pull
# 默认仓库https://gitea.yuxindazhineng.com/whm/ai_site.git
# 环境变量:
# AIJZ_GIT_URL 覆盖仓库地址
# GIT_BRANCH 分支(默认 main
# FORCE_GIT_RESET 默认 1硬对齐并丢弃已跟踪文件的本地改动保留 .env 等未跟踪文件)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
exec "$ROOT/restart.sh" --pull

48
restart.sh Normal file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# AI 建站 — 重启
# 用法:
# ./restart.sh # 不拉代码:停 → 构建 → 起
# ./restart.sh --pull # 强制对齐远程后再重启(生产一键更新)
# ./restart.sh --no-rebuild # 重启但不强制重建镜像
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
# shellcheck disable=SC1091
. "$ROOT/scripts/lib-aijz-deploy.sh"
REBUILD=1
DO_PULL=0
for a in "$@"; do
case "$a" in
--pull|--update) DO_PULL=1 ;;
--no-pull) DO_PULL=0 ;;
--no-rebuild) REBUILD=0 ;;
-h|--help)
echo "用法: $0 [--pull|--update] [--no-rebuild]"
echo " 默认不拉代码;--pull 时 git fetch + checkout -f + reset --hard 对齐远程后重启"
exit 0
;;
esac
done
echo "========================================"
echo " AI 建站 重启"
echo " 路径: $ROOT"
if [ "$DO_PULL" = "1" ]; then
echo " 拉取: 是(强制对齐 ${GIT_BRANCH:-main}"
else
echo " 拉取: 否(加 --pull 可拉代码)"
fi
echo "========================================"
ensure_docker
ensure_env_file
export_defaults
if [ "$DO_PULL" = "1" ]; then
git_pull_hard
fi
stack_down
sleep 1
stack_up "$REBUILD"
healthcheck
print_endpoints

454
scripts/lib-aijz-deploy.sh Normal file
View File

@@ -0,0 +1,454 @@
# shellcheck shell=bash
# AI 建站 Linux 部署公共函数。由 start/restart/stop/pull-and-restart 在 ROOT 下 source。
run_sudo() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
else
sudo "$@"
fi
}
ensure_runtime_dirs() {
mkdir -p "$ROOT/.runtime/pgdata" "$ROOT/.runtime/uploads" "$ROOT/.runtime/logs" \
"$ROOT/.runtime/logs/generate" "$ROOT/web/dist"
}
ensure_env_file() {
if [ ! -f "$ROOT/.env" ]; then
if [ -f "$ROOT/.env.example" ]; then
cp "$ROOT/.env.example" "$ROOT/.env"
echo "已从 .env.example 创建 .env请按需填写 API Key。"
else
cat >"$ROOT/.env" <<'EOF'
LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=
MINIMAX_API_KEY=
EOF
echo "已创建默认 .env"
fi
fi
sed -i 's/\r$//' "$ROOT/.env" 2>/dev/null || true
# 供 compose 变量替换与本脚本健康检查使用
set -a
# shellcheck disable=SC1091
[ -f "$ROOT/.env" ] && . "$ROOT/.env"
set +a
export AIJZ_WEB_PUBLISH="${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}"
export AIJZ_GATEWAY_PUBLISH="${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}"
export AIJZ_PLATFORM_PUBLISH="${AIJZ_PLATFORM_PUBLISH:-127.0.0.1:8888}"
export AIJZ_AI_PUBLISH="${AIJZ_AI_PUBLISH:-127.0.0.1:8001}"
export AIJZ_PG_PUBLISH="${AIJZ_PG_PUBLISH:-127.0.0.1:5432}"
export AIJZ_ENABLE_HOST_NGINX="${AIJZ_ENABLE_HOST_NGINX:-0}"
export AIJZ_DOMAIN="${AIJZ_DOMAIN:-}"
export AIJZ_PUBLIC_BASE_URL="${AIJZ_PUBLIC_BASE_URL:-}"
}
# 若 .env 设了 AIJZ_PUBLIC_BASE_URL同步到 platform.docker.yaml挂载文件restart platform 即生效)
apply_public_base_url() {
local base="${AIJZ_PUBLIC_BASE_URL:-}"
local pf="$ROOT/platform/etc/platform.docker.yaml"
[ -n "$base" ] || return 0
[ -f "$pf" ] || return 0
base="${base%/}"
local storage_base="${base}/api/v1/storage"
if grep -q '^PublicBaseURL:' "$pf"; then
sed -i "s|^PublicBaseURL:.*|PublicBaseURL: \"${base}\"|" "$pf"
fi
if grep -q '^ PublicBase:' "$pf"; then
sed -i "s|^ PublicBase:.*| PublicBase: \"${storage_base}\"|" "$pf"
fi
echo "已同步 PublicBaseURL -> ${base}platform/etc/platform.docker.yaml"
}
sync_ssl_certs() {
local domain="${AIJZ_DOMAIN:-}"
[ -n "$domain" ] || return 0
local ssl_dir="/etc/ssl/aijianzhan/$domain"
run_sudo mkdir -p "$ssl_dir"
if [ -f "$ROOT/nginx/$domain.pem" ] && [ -f "$ROOT/nginx/$domain.key" ]; then
run_sudo cp -f "$ROOT/nginx/$domain.pem" "$ssl_dir/fullchain.pem"
run_sudo cp -f "$ROOT/nginx/$domain.key" "$ssl_dir/privkey.pem"
elif [ -f "$ROOT/nginx/fullchain.pem" ] && [ -f "$ROOT/nginx/privkey.pem" ]; then
run_sudo cp -f "$ROOT/nginx/fullchain.pem" "$ROOT/nginx/privkey.pem" "$ssl_dir/"
elif [ -f "$ROOT/nginx/$domain/fullchain.pem" ] && [ -f "$ROOT/nginx/$domain/privkey.pem" ]; then
run_sudo cp -f "$ROOT/nginx/$domain/fullchain.pem" "$ROOT/nginx/$domain/privkey.pem" "$ssl_dir/"
else
echo "提示: 未找到 nginx/ 下证书,宿主机 HTTPS 需手动放入 $ssl_dir" >&2
return 0
fi
run_sudo chmod 644 "$ssl_dir/fullchain.pem" 2>/dev/null || true
run_sudo chmod 600 "$ssl_dir/privkey.pem" 2>/dev/null || true
echo "证书已同步 -> $ssl_dir"
}
host_nginx_online() {
command -v systemctl >/dev/null 2>&1 || return 1
systemctl is-active nginx >/dev/null 2>&1 && return 0
systemctl is-active nginx.service >/dev/null 2>&1 && return 0
return 1
}
ensure_host_nginx_started() {
command -v nginx >/dev/null 2>&1 || {
echo "错误: 未安装 nginx无法启用 AIJZ_ENABLE_HOST_NGINX" >&2
exit 1
}
if host_nginx_online; then
return 0
fi
echo "宿主机 Nginx 未在线,尝试启动..."
run_sudo systemctl start nginx 2>/dev/null || run_sudo systemctl start nginx.service
run_sudo systemctl enable nginx 2>/dev/null || true
host_nginx_online || {
echo "错误: 无法启动宿主机 nginx" >&2
exit 1
}
}
install_host_nginx_site_conf() {
local domain="${AIJZ_DOMAIN:-}"
[ -n "$domain" ] || return 0
[ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ] || return 0
local tpl="$ROOT/nginx/aijz.host.conf"
local out="/etc/nginx/conf.d/aijz_${domain}.conf"
local web_port
web_port="$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")"
[ -f "$tpl" ] || {
echo "未找到 $tpl,跳过宿主机 Nginx 配置" >&2
return 0
}
mkdir -p "$ROOT/verify-root"
sync_ssl_certs
sed -e "s|__DOMAIN__|${domain}|g" \
-e "s|__WEB_PORT__|${web_port}|g" \
-e "s|__VERIFY_ROOT__|${ROOT}/verify-root|g" \
"$tpl" | run_sudo tee "$out" >/dev/null
if ! run_sudo nginx -t 2>/dev/null; then
echo "错误: nginx -t 失败,请检查 $out" >&2
exit 1
fi
if host_nginx_online; then
run_sudo systemctl reload nginx 2>/dev/null && echo "宿主机 Nginx 已重载($out"
else
ensure_host_nginx_started
fi
}
reload_web_nginx() {
local cid
cid="$(compose_cmd ps -q web 2>/dev/null | head -1)"
if [ -z "$cid" ]; then
echo "web 容器未运行,跳过 reload" >&2
return 1
fi
run_sudo docker exec "$cid" nginx -t >/dev/null 2>&1 || {
echo "错误: 容器内 nginx -t 失败" >&2
return 1
}
run_sudo docker exec "$cid" nginx -s reload
echo "web 容器 nginx 已 reloadweb/nginx.conf"
}
restart_service() {
local svc="$1"
compose_cmd restart "$svc"
echo "已 restart $svc"
}
reload_host_nginx() {
[ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ] || {
echo "AIJZ_ENABLE_HOST_NGINX 未启用,跳过宿主机 Nginx" >&2
return 0
}
install_host_nginx_site_conf
}
reload_all_config() {
ensure_env_file
apply_public_base_url
reload_host_nginx || true
reload_web_nginx || true
restart_service platform
restart_service gateway
compose_cmd up -d --force-recreate ai >/dev/null 2>&1 || compose_cmd restart ai
echo "配置重载完成(未 rebuild 镜像、未重建 web/dist"
}
ensure_docker() {
if command -v docker >/dev/null 2>&1 && run_sudo docker info >/dev/null 2>&1; then
echo "Docker 已就绪."
return 0
fi
if command -v docker >/dev/null 2>&1; then
echo "Docker 守护进程未连接,尝试启动..."
run_sudo systemctl start docker 2>/dev/null || run_sudo systemctl start podman 2>/dev/null || true
if run_sudo docker info >/dev/null 2>&1; then
echo "Docker 已就绪."
return 0
fi
fi
echo "错误: Docker 不可用。请先安装并启动 docker 后再执行。" >&2
exit 1
}
resolve_compose_cmd() {
if run_sudo docker compose version >/dev/null 2>&1; then
echo "docker compose"
return
fi
if command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
return
fi
if [ -x /usr/local/bin/docker-compose ]; then
echo "/usr/local/bin/docker-compose"
return
fi
echo ""
}
COMPOSE_CMD=""
compose_cmd() {
# stack_down 可能早于 stack_up先补默认值避免 set -u 静默退出
export_defaults
: "${AIJZ_WEB_PUBLISH:=127.0.0.1:5173}"
: "${AIJZ_GATEWAY_PUBLISH:=127.0.0.1:8180}"
: "${AIJZ_PLATFORM_PUBLISH:=127.0.0.1:8888}"
: "${AIJZ_AI_PUBLISH:=127.0.0.1:8001}"
: "${AIJZ_PG_PUBLISH:=127.0.0.1:5432}"
if [ -z "$COMPOSE_CMD" ]; then
COMPOSE_CMD="$(resolve_compose_cmd)"
fi
if [ -z "$COMPOSE_CMD" ]; then
echo "错误: 找不到 docker compose / docker-compose" >&2
exit 1
fi
# shellcheck disable=SC2086
run_sudo env \
DOCKER_BASE_REGISTRY="${DOCKER_BASE_REGISTRY}" \
GOPROXY="${GOPROXY}" \
AIJZ_WEB_PUBLISH="${AIJZ_WEB_PUBLISH}" \
AIJZ_GATEWAY_PUBLISH="${AIJZ_GATEWAY_PUBLISH}" \
AIJZ_PLATFORM_PUBLISH="${AIJZ_PLATFORM_PUBLISH}" \
AIJZ_AI_PUBLISH="${AIJZ_AI_PUBLISH}" \
AIJZ_PG_PUBLISH="${AIJZ_PG_PUBLISH}" \
$COMPOSE_CMD "$@"
}
export_defaults() {
export DOCKER_BASE_REGISTRY="${DOCKER_BASE_REGISTRY:-docker.m.daocloud.io/library}"
export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
export REGISTRY_MIRROR="${REGISTRY_MIRROR:-docker.m.daocloud.io/library/}"
}
# 从 "127.0.0.1:5173" 或 "5173" 取出宿主机端口号
publish_host_port() {
local pub="$1"
echo "$pub" | awk -F: '{print $NF}'
}
port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
run_sudo ss -tlnH "sport = :$port" 2>/dev/null | grep -q . && return 0
ss -tlnH "sport = :$port" 2>/dev/null | grep -q . && return 0
return 1
fi
if command -v lsof >/dev/null 2>&1; then
lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0
return 1
fi
return 1
}
# 检查 AI 建站将占用的宿主机端口;若已被本项目容器占用则放行
check_aijz_ports() {
local pairs=(
"web:$AIJZ_WEB_PUBLISH"
"gateway:$AIJZ_GATEWAY_PUBLISH"
"platform:$AIJZ_PLATFORM_PUBLISH"
"ai:$AIJZ_AI_PUBLISH"
"postgres:$AIJZ_PG_PUBLISH"
)
echo "端口规划核对(相对 yh_webAI建站 5173/8180/8888/8001/5432宇恒 8088/9080/9081 + 宿主机 80/443。"
local name pub port conflict=0
local self_running=0
if compose_cmd ps -q 2>/dev/null | grep -q .; then
self_running=1
fi
for item in "${pairs[@]}"; do
name="${item%%:*}"
pub="${item#*:}"
port="$(publish_host_port "$pub")"
[ -z "$port" ] && continue
if port_in_use "$port"; then
if [ "$self_running" -eq 1 ]; then
echo " 提示: 端口 $port$name)已占用,疑为本栈,继续。"
else
echo " 错误: 宿主机端口 $port 已被占用(服务 $name$pub)。" >&2
echo " 请改 .env 中对应 AIJZ_*_PUBLISH或停掉占用进程。" >&2
conflict=1
fi
else
echo " OK $name$pub"
fi
done
[ "$conflict" -eq 0 ] || exit 1
}
# 构建 web/dist优先本机 npm否则用 Docker node 镜像
build_web_dist() {
echo "构建 web dist ..."
if command -v npm >/dev/null 2>&1; then
(
cd "$ROOT/web"
if [ -f package-lock.json ]; then
npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps
else
npm install --legacy-peer-deps
fi
npm run build
)
else
echo "本机无 npm使用 Docker node 构建..."
run_sudo docker run --rm \
-v "$ROOT/web:/app" \
-v "$ROOT/web/dist:/app/dist" \
-w /app \
"${REGISTRY_MIRROR}node:20-alpine" \
sh -c "(npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps) && npm run build"
fi
if [ ! -f "$ROOT/web/dist/index.html" ]; then
echo "错误: web/dist/index.html 不存在,构建失败。" >&2
exit 1
fi
# nginx 容器用户需可读;避免 Podman 下 Permission denied → 403
chmod -R a+rX "$ROOT/web/dist" 2>/dev/null || true
echo "web/dist 就绪."
}
stack_up() {
local rebuild="${1:-0}"
export_defaults
ensure_runtime_dirs
ensure_env_file
apply_public_base_url
check_aijz_ports
build_web_dist
if [ "$rebuild" = "1" ]; then
compose_cmd up -d --build --force-recreate
else
if ! compose_cmd up -d; then
echo "compose up 失败,尝试 --build ..."
compose_cmd up -d --build
fi
fi
install_host_nginx_site_conf || true
}
stack_down() {
# 不吞掉 unbound 等错误compose down 失败仍继续
if ! compose_cmd down --remove-orphans; then
echo "提示: compose down 未完全成功,继续启动..." >&2
fi
}
healthcheck() {
local ok=0 i
local gw_port web_port
gw_port="$(publish_host_port "${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}")"
web_port="$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")"
echo "等待 gateway(:$gw_port) / web(:$web_port) ..."
for i in $(seq 1 60); do
if curl -fsS --max-time 2 "http://127.0.0.1:${gw_port}/gateway/health" >/dev/null 2>&1 \
&& curl -fsS --max-time 2 "http://127.0.0.1:${web_port}/" >/dev/null 2>&1; then
ok=1
break
fi
sleep 2
done
if [ "$ok" -eq 1 ]; then
echo "健康检查通过."
else
echo "警告: 健康检查超时,请查看: docker compose -f $ROOT/docker-compose.yml logs" >&2
fi
compose_cmd exec -T postgres psql -U platform -d platform -c 'ALTER USER platform CREATEDB;' >/dev/null 2>&1 || true
}
print_endpoints() {
local pub_line=""
if [ -n "${AIJZ_DOMAIN:-}" ] && [ "${AIJZ_ENABLE_HOST_NGINX:-0}" = "1" ]; then
pub_line=" Public https://${AIJZ_DOMAIN}"
fi
cat <<EOF
========================================
Web http://127.0.0.1:$(publish_host_port "${AIJZ_WEB_PUBLISH:-127.0.0.1:5173}")
Gateway http://127.0.0.1:$(publish_host_port "${AIJZ_GATEWAY_PUBLISH:-127.0.0.1:8180}")
${pub_line}
Account demo / demo123
Data $ROOT/.runtime
Stop ./stop.sh
重载配置 ./reload-config.sh不整栈 rebuild
绑定 默认 127.0.0.1(与 yh_web 同机不抢 80/443/8088
========================================
EOF
}
# 默认代码源(可用 AIJZ_GIT_URL / GIT_BRANCH 覆盖)
AIJZ_GIT_URL_DEFAULT="https://gitea.yuxindazhineng.com/whm/ai_site.git"
ensure_git_origin() {
local url="${1:-${AIJZ_GIT_URL:-$AIJZ_GIT_URL_DEFAULT}}"
if ! command -v git >/dev/null 2>&1; then
echo "错误: 未安装 git无法拉取代码。请先安装 git。" >&2
return 1
fi
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "当前目录不是 git 仓库,初始化并关联 origin -> $url"
git init
git remote add origin "$url"
return 0
fi
if git remote get-url origin >/dev/null 2>&1; then
local cur
cur="$(git remote get-url origin)"
if [ "$cur" != "$url" ]; then
echo "更新 origin: $cur -> $url"
git remote set-url origin "$url"
fi
else
echo "添加 origin -> $url"
git remote add origin "$url"
fi
}
# 从 Gitea ai_site 拉取并与远程对齐(默认硬重置,保留未跟踪文件如 .env
# 环境变量:
# AIJZ_GIT_URL 仓库地址(默认 https://gitea.yuxindazhineng.com/whm/ai_site.git
# GIT_BRANCH 分支(默认 main
# FORCE_GIT_RESET 1=reset --hard 对齐远程(默认 10=仅快进合并
git_pull_hard() {
local url="${AIJZ_GIT_URL:-$AIJZ_GIT_URL_DEFAULT}"
local branch="${GIT_BRANCH:-main}"
echo "拉取代码($url branch=$branch..."
ensure_git_origin "$url" || return 1
if ! git fetch origin --progress; then
echo "错误: git fetch 失败。私有仓库请配置凭据,或设置 AIJZ_GIT_URL 为带 token 的地址。" >&2
return 1
fi
if ! git rev-parse --verify "origin/$branch" >/dev/null 2>&1; then
echo "错误: 远程不存在分支 origin/$branch" >&2
return 1
fi
if [ "${FORCE_GIT_RESET:-1}" = "1" ]; then
# -f丢弃已跟踪文件的本地改动避免 checkout 被挡住(.env 等未跟踪文件保留)
git checkout -f -B "$branch" "origin/$branch"
git reset --hard "origin/$branch"
else
git checkout "$branch" 2>/dev/null || git checkout -b "$branch" "origin/$branch"
git merge --ff-only "origin/$branch"
fi
echo "当前: $(git rev-parse --short HEAD) $(git log -1 --pretty=%s)"
}

49
start.sh Normal file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# AI 建站 — Linux 一键启动Docker Compose
# 用法cd 项目根 && ./start.sh [--rebuild] [--no-pull]
# 默认从 Gitea 强制对齐最新代码再启动(可用 --no-pull / AIJZ_SKIP_GIT_PULL=1 跳过)
# 生产一键更新也可用:./restart.sh --pull 或 ./pull-and-restart.sh
# 行尾LF
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
# shellcheck disable=SC1091
. "$ROOT/scripts/lib-aijz-deploy.sh"
REBUILD=0
DO_PULL=1
for a in "$@"; do
case "$a" in
--rebuild|-r) REBUILD=1 ;;
--no-pull) DO_PULL=0 ;;
--pull) DO_PULL=1 ;;
-h|--help)
echo "用法: $0 [--rebuild] [--no-pull|--pull]"
echo " 默认拉取并硬对齐: ${AIJZ_GIT_URL:-https://gitea.yuxindazhineng.com/whm/ai_site.git} 分支 ${GIT_BRANCH:-main}"
echo " 跳过拉取: --no-pull 或 AIJZ_SKIP_GIT_PULL=1"
echo " 仅更新重启: ./restart.sh --pull"
exit 0
;;
esac
done
if [ "${AIJZ_SKIP_GIT_PULL:-0}" = "1" ]; then
DO_PULL=0
fi
echo "========================================"
echo " AI 建站 启动"
echo " 路径: $ROOT"
echo "========================================"
if [ "$DO_PULL" = "1" ]; then
git_pull_hard
else
echo "已跳过代码拉取(--no-pull / AIJZ_SKIP_GIT_PULL"
fi
ensure_docker
ensure_env_file
export_defaults
stack_up "$REBUILD"
healthcheck
print_endpoints

13
stop.sh Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# AI 建站 — 停止 Docker 栈
# 用法:./stop.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
# shellcheck disable=SC1091
. "$ROOT/scripts/lib-aijz-deploy.sh"
echo "停止 AI 建站 ($ROOT) ..."
ensure_docker
stack_down
echo "已停止."