feat(deploy): 在线更新 - 新增后台检查、强制备份迁移与失败自动回退

This commit is contained in:
ddCat
2026-08-30 22:15:17 +08:00
parent 4ba9694f45
commit db66ac1182
60 changed files with 4124 additions and 93 deletions

121
scripts/install-host-updater.sh Executable file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -Eeuo pipefail
INSTALL_DIR="${INSTALL_DIR:-/opt/open-ai-canvas}"
REPOSITORY="${REPOSITORY:-ddcat-ai/open-ai-canvas}"
SOCKET_DIR="${CANVAS_UPDATER_SOCKET_DIR:-/run/open-ai-canvas-updater}"
UPDATER_BIN="/usr/local/bin/open-ai-canvas-host-updater"
UPDATER_ENV="/etc/open-ai-canvas-updater.env"
UPDATER_SERVICE="/etc/systemd/system/open-ai-canvas-updater.service"
fail() {
printf 'Host Updater 安装失败:%s\n' "$1" >&2
exit 1
}
require_root() {
[[ "${EUID}" -eq 0 ]] || fail "请使用 sudo 运行"
[[ "$(uname -s)" == "Linux" ]] || fail "仅支持 Linux 服务器"
command -v systemctl >/dev/null 2>&1 || fail "服务器必须使用 systemd"
command -v curl >/dev/null 2>&1 || fail "缺少 curl"
command -v sha256sum >/dev/null 2>&1 || fail "缺少 sha256sum"
command -v openssl >/dev/null 2>&1 || fail "缺少 openssl"
[[ -f "${INSTALL_DIR}/.env" ]] || fail "未找到 ${INSTALL_DIR}/.env"
[[ -f "${INSTALL_DIR}/docker-compose.deploy.yml" ]] || fail "未找到部署 Compose"
}
read_image_tag() {
local configured
configured="$(sed -n 's/^CANVAS_IMAGE_TAG=//p' "${INSTALL_DIR}/.env" | tail -n 1)"
[[ -n "$configured" && "$configured" != "latest" ]] || fail "请先把 CANVAS_IMAGE_TAG 固定为已发布版本"
if [[ "$configured" == v* ]]; then
RELEASE_TAG="$configured"
else
RELEASE_TAG="v${configured}"
fi
}
install_binary() {
local arch asset temporary checksum_file expected
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
aarch64|arm64) arch="arm64" ;;
*) fail "不支持的 CPU 架构:$(uname -m)" ;;
esac
asset="open-ai-canvas-host-updater-linux-${arch}"
temporary="$(mktemp)"
checksum_file="$(mktemp)"
curl -fsSL "https://github.com/${REPOSITORY}/releases/download/${RELEASE_TAG}/${asset}" -o "$temporary"
curl -fsSL "https://github.com/${REPOSITORY}/releases/download/${RELEASE_TAG}/SHA256SUMS" -o "$checksum_file"
expected="$(awk -v asset="$asset" '$2 == asset { print $1 }' "$checksum_file")"
[[ "$expected" =~ ^[a-f0-9]{64}$ ]] || fail "Release 校验清单缺少 ${asset}"
printf '%s %s\n' "$expected" "$temporary" | sha256sum -c - >/dev/null || fail "Host Updater SHA-256 校验失败"
install -m 0755 "$temporary" "$UPDATER_BIN"
rm -f "$temporary" "$checksum_file"
}
ensure_token() {
local token temporary
token="$(sed -n 's/^CANVAS_UPDATER_TOKEN=//p' "${INSTALL_DIR}/.env" | tail -n 1)"
if [[ -z "$token" ]]; then
token="$(openssl rand -hex 32)"
temporary="$(mktemp "${INSTALL_DIR}/.env.XXXXXX")"
awk -v token="$token" '
BEGIN { updated=0 }
/^CANVAS_UPDATER_TOKEN=/ { print "CANVAS_UPDATER_TOKEN=" token; updated=1; next }
{ print }
END { if (!updated) print "CANVAS_UPDATER_TOKEN=" token }
' "${INSTALL_DIR}/.env" > "$temporary"
chmod --reference="${INSTALL_DIR}/.env" "$temporary"
mv "$temporary" "${INSTALL_DIR}/.env"
fi
[[ ${#token} -ge 32 ]] || fail "CANVAS_UPDATER_TOKEN 长度不足"
umask 077
printf 'CANVAS_UPDATER_TOKEN=%s\nCANVAS_UPDATER_INSTALL_DIR=%s\nCANVAS_UPDATER_SOCKET=%s/updater.sock\n' "$token" "$INSTALL_DIR" "$SOCKET_DIR" > "$UPDATER_ENV"
}
install_service() {
local temporary_service
install -d -m 0755 "$SOCKET_DIR"
install -d -m 0700 /var/lib/open-ai-canvas-updater "${INSTALL_DIR}/backups"
temporary_service="$(mktemp)"
printf '%s\n' \
'[Unit]' \
'Description=Open AI Canvas Host Updater' \
'After=docker.service network-online.target' \
'Requires=docker.service' \
'Wants=network-online.target' \
'' \
'[Service]' \
'Type=simple' \
"EnvironmentFile=${UPDATER_ENV}" \
"ExecStart=${UPDATER_BIN}" \
'Restart=on-failure' \
'RestartSec=5s' \
'NoNewPrivileges=true' \
'PrivateTmp=true' \
'ProtectHome=true' \
'ProtectSystem=full' \
"ReadWritePaths=${INSTALL_DIR} /var/lib/open-ai-canvas-updater ${SOCKET_DIR} ${UPDATER_BIN}" \
'' \
'[Install]' \
'WantedBy=multi-user.target' > "$temporary_service"
install -m 0644 "$temporary_service" "$UPDATER_SERVICE"
rm -f "$temporary_service"
systemctl daemon-reload
systemctl enable --now open-ai-canvas-updater.service
systemctl restart open-ai-canvas-updater.service
}
main() {
require_root
read_image_tag
install_binary
ensure_token
install_service
printf 'Host Updater 已安装,Socket:%s/updater.sock\n' "$SOCKET_DIR"
printf '请重建 backend 容器,使 Token 与 Socket 挂载生效。\n'
}
main "$@"

View File

@@ -5,9 +5,12 @@ set -Eeuo pipefail
REPOSITORY_REF="${REPOSITORY_REF:-main}"
INSTALL_DIR="${INSTALL_DIR:-/opt/open-ai-canvas}"
CANVAS_HTTP_PORT="${CANVAS_HTTP_PORT:-3000}"
CANVAS_IMAGE_TAG="${CANVAS_IMAGE_TAG:-latest}"
REQUESTED_IMAGE_TAG="${CANVAS_IMAGE_TAG:-}"
CANVAS_IMAGE_TAG="${REQUESTED_IMAGE_TAG:-latest}"
CANVAS_IMAGE_TAG="${CANVAS_IMAGE_TAG#v}"
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"
@@ -91,9 +94,20 @@ prepare_environment() {
local configured_image_tag
configured_image_tag="$(sed -n 's/^CANVAS_IMAGE_TAG=//p' .env | tail -n 1)"
if [[ -n "$configured_image_tag" ]]; then
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}$ ]] || fail ".env 中的 CANVAS_IMAGE_TAG 无效"
CANVAS_IMAGE_TAG="$configured_image_tag"
CANVAS_IMAGE_TAG="${configured_image_tag#v}"
fi
return
fi
@@ -124,6 +138,19 @@ download_compose() {
mv "$temporary_file" "$COMPOSE_FILE"
}
install_host_updater() {
if [[ "$CANVAS_IMAGE_TAG" == "latest" ]]; then
printf '\n提示:CANVAS_IMAGE_TAG=latest,已跳过在线更新器安装。固定到具体发布版本后可再次运行本脚本。\n'
return
fi
step "安装宿主机在线更新服务"
local installer
installer="$(mktemp)"
curl -fsSL "$UPDATER_INSTALL_URL" -o "$installer"
INSTALL_DIR="$INSTALL_DIR" bash "$installer"
rm -f "$installer"
}
start_services() {
step "拉取并启动 GHCR 网页与后端镜像"
if ! docker compose --env-file .env -f "$COMPOSE_FILE" pull; then
@@ -152,6 +179,7 @@ main() {
login_ghcr
prepare_environment
download_compose
install_host_updater
start_services
print_result
}