chore: initial commit of ai site platform

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 10:19:22 +08:00
commit 6366859bb3
222 changed files with 47313 additions and 0 deletions

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

@@ -0,0 +1,402 @@
# 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() {
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
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() {
compose_cmd down --remove-orphans 2>/dev/null || true
}
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
}
git_pull_hard() {
local branch="${GIT_BRANCH:-master}"
echo "拉取代码branch=$branch..."
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "错误: 当前不是 git 仓库,跳过拉取。" >&2
return 1
fi
git fetch origin --progress
if [ "${FORCE_GIT_RESET:-1}" = "1" ]; then
git reset --hard "origin/$branch"
else
git checkout "$branch" 2>/dev/null || true
git merge --ff-only "origin/$branch"
fi
echo "当前: $(git rev-parse --short HEAD) $(git log -1 --pretty=%s)"
}