240 lines
10 KiB
Bash
Executable File
240 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
set -Eeuo pipefail
|
||
|
||
REPOSITORY_REF="${REPOSITORY_REF:-main}"
|
||
INSTALL_DIR="${INSTALL_DIR:-/opt/open-ai-canvas}"
|
||
CANVAS_HTTP_PORT="${CANVAS_HTTP_PORT:-3000}"
|
||
REQUESTED_IMAGE_TAG="${CANVAS_IMAGE_TAG:-}"
|
||
CANVAS_IMAGE_TAG="${REQUESTED_IMAGE_TAG#v}"
|
||
IMAGE_REPOSITORY="${IMAGE_REPOSITORY:-ghcr.io/ddcat-ai/open-ai-canvas}"
|
||
CANVAS_BACKEND_IMAGE=""
|
||
CANVAS_WEB_IMAGE=""
|
||
COMPOSE_FILE="docker-compose.deploy.yml"
|
||
COMPOSE_URL="${COMPOSE_URL:-https://raw.githubusercontent.com/ddcat-ai/open-ai-canvas/${REPOSITORY_REF}/${COMPOSE_FILE}}"
|
||
UPDATER_INSTALL_URL="${UPDATER_INSTALL_URL:-https://raw.githubusercontent.com/ddcat-ai/open-ai-canvas/${REPOSITORY_REF}/scripts/install-host-updater.sh}"
|
||
|
||
step() {
|
||
printf '\n==> %s\n' "$1"
|
||
}
|
||
|
||
fail() {
|
||
printf '\n安装失败:%s\n' "$1" >&2
|
||
exit 1
|
||
}
|
||
|
||
require_root() {
|
||
if [[ "${EUID}" -ne 0 ]]; then
|
||
fail "请使用 README 中带 sudo 的一键安装命令"
|
||
fi
|
||
[[ "$(uname -s)" == "Linux" ]] || fail "一键部署脚本仅支持 Linux 服务器"
|
||
[[ "$CANVAS_HTTP_PORT" =~ ^[0-9]+$ ]] || fail "CANVAS_HTTP_PORT 必须是 1 到 65535 的数字"
|
||
((CANVAS_HTTP_PORT >= 1 && CANVAS_HTTP_PORT <= 65535)) || fail "CANVAS_HTTP_PORT 必须是 1 到 65535 的数字"
|
||
if [[ -n "$CANVAS_IMAGE_TAG" ]]; then
|
||
[[ "$CANVAS_IMAGE_TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || fail "CANVAS_IMAGE_TAG 不是有效的 Docker 镜像标签"
|
||
[[ "$CANVAS_IMAGE_TAG" != "latest" ]] || fail "生产部署必须固定到具体 Release,不能使用 latest"
|
||
fi
|
||
}
|
||
|
||
install_packages() {
|
||
local packages=(ca-certificates curl openssl)
|
||
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
apt-get update
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
|
||
elif command -v dnf >/dev/null 2>&1; then
|
||
dnf install -y "${packages[@]}"
|
||
elif command -v yum >/dev/null 2>&1; then
|
||
yum install -y "${packages[@]}"
|
||
else
|
||
fail "暂不支持当前 Linux 发行版,请先手动安装 Docker、curl 和 OpenSSL"
|
||
fi
|
||
}
|
||
|
||
install_docker() {
|
||
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||
return
|
||
fi
|
||
|
||
step "安装 Docker 和 Docker Compose"
|
||
local installer
|
||
installer="$(mktemp)"
|
||
curl -fsSL https://get.docker.com -o "$installer"
|
||
sh "$installer"
|
||
rm -f "$installer"
|
||
|
||
if command -v systemctl >/dev/null 2>&1; then
|
||
systemctl enable --now docker
|
||
elif command -v service >/dev/null 2>&1; then
|
||
service docker start
|
||
fi
|
||
|
||
docker compose version >/dev/null 2>&1 || fail "Docker Compose 安装失败"
|
||
}
|
||
|
||
login_ghcr() {
|
||
if [[ -n "${GHCR_USERNAME:-}" || -n "${GHCR_TOKEN:-}" ]]; then
|
||
[[ -n "${GHCR_USERNAME:-}" && -n "${GHCR_TOKEN:-}" ]] || fail "GHCR_USERNAME 和 GHCR_TOKEN 必须同时配置"
|
||
step "登录 GitHub Container Registry"
|
||
# token 只通过 stdin 交给 Docker,避免出现在命令参数和进程列表中。
|
||
printf '%s' "$GHCR_TOKEN" | docker login ghcr.io --username "$GHCR_USERNAME" --password-stdin
|
||
fi
|
||
}
|
||
|
||
prepare_environment() {
|
||
mkdir -p "$INSTALL_DIR"
|
||
cd "$INSTALL_DIR"
|
||
|
||
if [[ -f .env ]]; then
|
||
grep -Eq '^POSTGRES_PASSWORD=.+$' .env || fail "现有 .env 缺少 POSTGRES_PASSWORD"
|
||
grep -Eq '^DATABASE_URL=.+$' .env || fail "现有 .env 缺少 DATABASE_URL"
|
||
|
||
local configured_http_port
|
||
configured_http_port="$(sed -n 's/^CANVAS_HTTP_PORT=//p' .env | tail -n 1)"
|
||
if [[ -n "$configured_http_port" ]]; then
|
||
[[ "$configured_http_port" =~ ^[0-9]+$ ]] || fail ".env 中的 CANVAS_HTTP_PORT 无效"
|
||
((configured_http_port >= 1 && configured_http_port <= 65535)) || fail ".env 中的 CANVAS_HTTP_PORT 无效"
|
||
CANVAS_HTTP_PORT="$configured_http_port"
|
||
fi
|
||
|
||
local configured_image_tag
|
||
configured_image_tag="$(sed -n 's/^CANVAS_IMAGE_TAG=//p' .env | tail -n 1)"
|
||
if [[ -n "$REQUESTED_IMAGE_TAG" ]]; then
|
||
local temporary_env
|
||
temporary_env="$(mktemp "${INSTALL_DIR}/.env.XXXXXX")"
|
||
awk -v image_tag="$CANVAS_IMAGE_TAG" '
|
||
BEGIN { updated=0 }
|
||
/^CANVAS_IMAGE_TAG=/ { print "CANVAS_IMAGE_TAG=" image_tag; updated=1; next }
|
||
{ print }
|
||
END { if (!updated) print "CANVAS_IMAGE_TAG=" image_tag }
|
||
' .env > "$temporary_env"
|
||
chmod --reference=.env "$temporary_env"
|
||
mv "$temporary_env" .env
|
||
elif [[ -n "$configured_image_tag" ]]; then
|
||
[[ "$configured_image_tag" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ && "$configured_image_tag" != "latest" ]] || fail ".env 中的 CANVAS_IMAGE_TAG 必须固定为具体 Release"
|
||
CANVAS_IMAGE_TAG="${configured_image_tag#v}"
|
||
else
|
||
fail "请通过 CANVAS_IMAGE_TAG 指定具体 Release,例如 v1.5.7.1"
|
||
fi
|
||
CANVAS_BACKEND_IMAGE="${IMAGE_REPOSITORY}-backend:${CANVAS_IMAGE_TAG}"
|
||
CANVAS_WEB_IMAGE="${IMAGE_REPOSITORY}-web:${CANVAS_IMAGE_TAG}"
|
||
CANVAS_YINGCE_AGENT_IMAGE="${IMAGE_REPOSITORY}-yingce-agent:${CANVAS_IMAGE_TAG}"
|
||
set_env_value .env CANVAS_IMAGE_TAG "$CANVAS_IMAGE_TAG"
|
||
set_env_value .env CANVAS_BACKEND_IMAGE "$CANVAS_BACKEND_IMAGE"
|
||
set_env_value .env CANVAS_WEB_IMAGE "$CANVAS_WEB_IMAGE"
|
||
set_env_value .env CANVAS_YINGCE_AGENT_IMAGE "$CANVAS_YINGCE_AGENT_IMAGE"
|
||
if ! grep -Eq '^YINGCE_AGENT_TOKEN=.{32,}$' .env; then
|
||
set_env_value .env YINGCE_AGENT_TOKEN "$(openssl rand -hex 32)"
|
||
fi
|
||
return
|
||
fi
|
||
|
||
[[ -n "$CANVAS_IMAGE_TAG" ]] || fail "首次安装必须通过 CANVAS_IMAGE_TAG 指定具体 Release,例如 v1.5.7.1"
|
||
[[ "$CANVAS_IMAGE_TAG" != "latest" ]] || fail "生产部署必须固定到具体 Release,不能使用 latest"
|
||
|
||
step "生成 PostgreSQL 随机密码和部署配置"
|
||
local database_password
|
||
database_password="$(openssl rand -hex 32)"
|
||
CANVAS_BACKEND_IMAGE="${IMAGE_REPOSITORY}-backend:${CANVAS_IMAGE_TAG}"
|
||
CANVAS_WEB_IMAGE="${IMAGE_REPOSITORY}-web:${CANVAS_IMAGE_TAG}"
|
||
CANVAS_YINGCE_AGENT_IMAGE="${IMAGE_REPOSITORY}-yingce-agent:${CANVAS_IMAGE_TAG}"
|
||
umask 077
|
||
cat >.env <<EOF
|
||
POSTGRES_DB=open_ai_canvas
|
||
POSTGRES_USER=open_ai_canvas
|
||
POSTGRES_PASSWORD=${database_password}
|
||
DATABASE_URL=postgresql://open_ai_canvas:${database_password}@postgres:5432/open_ai_canvas?sslmode=disable
|
||
CANVAS_HTTP_PORT=${CANVAS_HTTP_PORT}
|
||
CANVAS_IMAGE_TAG=${CANVAS_IMAGE_TAG}
|
||
CANVAS_BACKEND_IMAGE=${IMAGE_REPOSITORY}-backend:${CANVAS_IMAGE_TAG}
|
||
CANVAS_WEB_IMAGE=${IMAGE_REPOSITORY}-web:${CANVAS_IMAGE_TAG}
|
||
CANVAS_YINGCE_AGENT_IMAGE=${IMAGE_REPOSITORY}-yingce-agent:${CANVAS_IMAGE_TAG}
|
||
YINGCE_AGENT_TOKEN=$(openssl rand -hex 32)
|
||
CANVAS_REGISTRATION_ENABLED=false
|
||
CANVAS_ALLOW_PRIVATE_UPSTREAMS=false
|
||
CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS=
|
||
CANVAS_CORS_ORIGINS=
|
||
EOF
|
||
}
|
||
|
||
set_env_value() {
|
||
local path="$1"
|
||
local key="$2"
|
||
local value="$3"
|
||
local temporary
|
||
temporary="$(mktemp "${path}.XXXXXX")"
|
||
awk -v key="$key" -v value="$value" '
|
||
BEGIN { updated=0 }
|
||
$0 ~ "^" key "=" { print key "=" value; updated=1; next }
|
||
{ print }
|
||
END { if (!updated) print key "=" value }
|
||
' "$path" > "$temporary"
|
||
chmod --reference="$path" "$temporary"
|
||
mv "$temporary" "$path"
|
||
}
|
||
|
||
download_compose() {
|
||
step "下载 GHCR 镜像部署配置"
|
||
local temporary_file
|
||
temporary_file="$(mktemp "${INSTALL_DIR}/.docker-compose.deploy.XXXXXX")"
|
||
curl -fsSL "$COMPOSE_URL" -o "$temporary_file"
|
||
mv "$temporary_file" "$COMPOSE_FILE"
|
||
}
|
||
|
||
install_host_updater() {
|
||
step "安装宿主机在线更新服务"
|
||
local installer
|
||
installer="$(mktemp)"
|
||
curl -fsSL "$UPDATER_INSTALL_URL" -o "$installer"
|
||
INSTALL_DIR="$INSTALL_DIR" bash "$installer"
|
||
rm -f "$installer"
|
||
|
||
step "重建 backend 以启用在线更新"
|
||
docker compose --env-file .env -f "$COMPOSE_FILE" up -d --no-deps --force-recreate --wait --wait-timeout 600 backend
|
||
}
|
||
|
||
start_services() {
|
||
step "拉取并启动 GHCR 网页与后端镜像"
|
||
if ! docker compose --env-file .env -f "$COMPOSE_FILE" pull; then
|
||
fail "GHCR 镜像拉取失败;如果容器包尚未公开,请通过 GHCR_USERNAME 和 GHCR_TOKEN 登录后重试"
|
||
fi
|
||
local backend_digest web_digest agent_digest
|
||
backend_digest="$(docker image inspect "$CANVAS_BACKEND_IMAGE" --format '{{range .RepoDigests}}{{println .}}{{end}}' | awk -v repository="${IMAGE_REPOSITORY}-backend" '$0 ~ "^" repository "@sha256:" { print; exit }')"
|
||
web_digest="$(docker image inspect "$CANVAS_WEB_IMAGE" --format '{{range .RepoDigests}}{{println .}}{{end}}' | awk -v repository="${IMAGE_REPOSITORY}-web" '$0 ~ "^" repository "@sha256:" { print; exit }')"
|
||
agent_digest="$(docker image inspect "$CANVAS_YINGCE_AGENT_IMAGE" --format '{{range .RepoDigests}}{{println .}}{{end}}' | awk -v repository="${IMAGE_REPOSITORY}-yingce-agent" '$0 ~ "^" repository "@sha256:" { print; exit }')"
|
||
[[ "$backend_digest" =~ ^${IMAGE_REPOSITORY//\//\/}-backend@sha256:[a-f0-9]{64}$ ]] || fail "后端镜像未返回可验证的仓库 digest"
|
||
[[ "$web_digest" =~ ^${IMAGE_REPOSITORY//\//\/}-web@sha256:[a-f0-9]{64}$ ]] || fail "Web 镜像未返回可验证的仓库 digest"
|
||
[[ "$agent_digest" =~ ^${IMAGE_REPOSITORY//\//\/}-yingce-agent@sha256:[a-f0-9]{64}$ ]] || fail "Agent 镜像未返回可验证的仓库 digest"
|
||
set_env_value .env CANVAS_BACKEND_IMAGE "$backend_digest"
|
||
set_env_value .env CANVAS_WEB_IMAGE "$web_digest"
|
||
set_env_value .env CANVAS_YINGCE_AGENT_IMAGE "$agent_digest"
|
||
docker compose --env-file .env -f "$COMPOSE_FILE" up -d --remove-orphans --wait --wait-timeout 600
|
||
}
|
||
|
||
print_result() {
|
||
local local_ip
|
||
local_ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
||
[[ -n "$local_ip" ]] || local_ip="服务器IP"
|
||
|
||
printf '\n部署完成。\n'
|
||
printf '访问地址:http://%s:%s\n' "$local_ip" "$CANVAS_HTTP_PORT"
|
||
printf '安装目录:%s\n' "$INSTALL_DIR"
|
||
printf '查看状态:cd %q && docker compose --env-file .env -f %s ps\n' "$INSTALL_DIR" "$COMPOSE_FILE"
|
||
printf '\n首次打开后注册的第一个账号会自动成为管理员。公网长期使用前请配置 HTTPS。\n'
|
||
}
|
||
|
||
main() {
|
||
require_root
|
||
step "安装服务器基础工具"
|
||
install_packages
|
||
install_docker
|
||
login_ghcr
|
||
prepare_environment
|
||
download_compose
|
||
start_services
|
||
install_host_updater
|
||
print_result
|
||
}
|
||
|
||
main "$@"
|