Files
zk-data-agent/scripts/deploy-ubuntu.sh
T
2026-05-07 12:03:00 +08:00

273 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Ubuntu 一键部署/更新脚本。
# - 首次执行会交互式生成 .env.deploy,本机保存,不提交 git。
# - 后续执行会拉取代码、更新依赖、构建前端、安装/重启用户级 systemd 服务。
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${ROOT_DIR}/.env.deploy"
PYTHON_VERSION="${PYTHON_VERSION:-3.10.14}"
BACKEND_SERVICE="zk-data-agent-backend"
FRONTEND_SERVICE="zk-data-agent-frontend"
USER_SYSTEMD_DIR="${HOME}/.config/systemd/user"
BRANCH=""
FORCE=0
SKIP_GIT=0
usage() {
cat <<EOF
Usage: bash scripts/deploy-ubuntu.sh [branch] [--force] [--skip-git]
Options:
branch 要部署的 git 分支;不传则使用当前分支
--force 使用 git reset --hard origin/<branch> 覆盖本地改动
--skip-git 跳过 git 拉取,只部署当前工作区
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--force)
FORCE=1
shift
;;
--skip-git)
SKIP_GIT=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
if [[ -n "${BRANCH}" ]]; then
echo "Unexpected argument: $1" >&2
usage >&2
exit 1
fi
BRANCH="$1"
shift
;;
esac
done
log() {
printf "\n\033[1;34m==>\033[0m %s\n" "$1"
}
warn() {
printf "\033[1;33mWARN\033[0m %s\n" "$1" >&2
}
fail() {
printf "\033[1;31mERROR\033[0m %s\n" "$1" >&2
exit 1
}
require_command() {
local name="$1"
local hint="$2"
if ! command -v "${name}" >/dev/null 2>&1; then
fail "缺少命令:${name}${hint}"
fi
}
prompt_value() {
local var_name="$1"
local prompt="$2"
local default_value="${3:-}"
local secret="${4:-0}"
local value
if [[ "${secret}" == "1" ]]; then
read -r -s -p "${prompt}" value
printf "\n"
else
if [[ -n "${default_value}" ]]; then
read -r -p "${prompt} [${default_value}]: " value
else
read -r -p "${prompt}: " value
fi
fi
value="${value:-${default_value}}"
printf -v "${var_name}" "%s" "${value}"
}
write_env_file() {
local api_key="$1"
local base_url="$2"
local model="$3"
local backend_host="$4"
local backend_port="$5"
local frontend_host="$6"
local frontend_port="$7"
local claw_api_url="http://${backend_host}:${backend_port}"
umask 077
cat >"${ENV_FILE}" <<EOF
# ZK Data Agent 本机部署配置。
# 该文件包含敏感信息,只保存在部署机器本地,不要提交到 git。
export OPENAI_API_KEY=$(printf "%q" "${api_key}")
export OPENAI_BASE_URL=$(printf "%q" "${base_url}")
export OPENAI_MODEL=$(printf "%q" "${model}")
export CLAW_BACKEND_HOST=$(printf "%q" "${backend_host}")
export CLAW_BACKEND_PORT=$(printf "%q" "${backend_port}")
export CLAW_FRONTEND_HOST=$(printf "%q" "${frontend_host}")
export CLAW_FRONTEND_PORT=$(printf "%q" "${frontend_port}")
export CLAW_API_URL=$(printf "%q" "${claw_api_url}")
EOF
chmod 600 "${ENV_FILE}"
}
ensure_env_file() {
if [[ -f "${ENV_FILE}" ]]; then
# shellcheck disable=SC1090
source "${ENV_FILE}"
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
fail "${ENV_FILE} 缺少 OPENAI_API_KEY,请编辑该文件补齐。"
fi
log "使用本机部署配置:${ENV_FILE}"
echo "OPENAI_BASE_URL=${OPENAI_BASE_URL:-}"
echo "OPENAI_MODEL=${OPENAI_MODEL:-}"
echo "OPENAI_API_KEY=已配置"
return
fi
if [[ ! -t 0 ]]; then
fail "首次部署需要交互式输入 OPENAI_API_KEY。请登录机器后执行:bash scripts/deploy-ubuntu.sh"
fi
log "首次部署:生成本机配置 ${ENV_FILE}"
echo "配置会写入 ${ENV_FILE},该文件已被 .gitignore 忽略。"
local api_key base_url model backend_host backend_port frontend_host frontend_port
prompt_value api_key "请输入 OPENAI_API_KEY: " "" 1
[[ -n "${api_key}" ]] || fail "OPENAI_API_KEY 不能为空。"
prompt_value base_url "请输入 OPENAI_BASE_URL" "http://model.mify.ai.srv/v1"
prompt_value model "请输入 OPENAI_MODEL" "xiaomi/mimo-v2-flash"
prompt_value backend_host "请输入后端监听地址" "127.0.0.1"
prompt_value backend_port "请输入后端端口" "8765"
prompt_value frontend_host "请输入前端监听地址" "0.0.0.0"
prompt_value frontend_port "请输入前端端口" "3000"
write_env_file "${api_key}" "${base_url}" "${model}" "${backend_host}" "${backend_port}" "${frontend_host}" "${frontend_port}"
echo "已写入 ${ENV_FILE},后续修改配置请编辑这个文件。"
}
update_git() {
if [[ "${SKIP_GIT}" == "1" ]]; then
log "跳过 git 拉取,使用当前工作区"
return
fi
require_command git "请先安装 git。"
cd "${ROOT_DIR}"
[[ -d .git ]] || fail "${ROOT_DIR} 不是 git 仓库。"
if [[ -z "${BRANCH}" ]]; then
BRANCH="$(git branch --show-current)"
fi
[[ -n "${BRANCH}" ]] || fail "无法判断当前分支,请显式传入分支名。"
log "更新代码:origin/${BRANCH}"
git fetch origin
git checkout "${BRANCH}"
if [[ "${FORCE}" == "1" ]]; then
git reset --hard "origin/${BRANCH}"
else
if [[ -n "$(git status --porcelain)" ]]; then
fail "工作区有未提交改动。请先处理,或使用 --force 覆盖。"
fi
git pull --ff-only origin "${BRANCH}"
fi
}
ensure_python() {
require_command pyenv "请先安装 pyenv,并安装 Python ${PYTHON_VERSION}"
log "准备 Python 环境:${PYTHON_VERSION}"
cd "${ROOT_DIR}"
if ! pyenv versions --bare | grep -Fx "${PYTHON_VERSION}" >/dev/null 2>&1; then
echo "pyenv 未发现 Python ${PYTHON_VERSION},开始安装。"
pyenv install "${PYTHON_VERSION}"
fi
PYENV_VERSION="${PYTHON_VERSION}" pyenv exec python -m venv "${ROOT_DIR}/.venv"
"${ROOT_DIR}/.venv/bin/python" -m pip install --upgrade pip setuptools wheel
"${ROOT_DIR}/.venv/bin/python" -m pip install -e .
}
build_frontend() {
require_command npm "请先安装 Node.js 和 npm,建议 Node.js 20 或 22。"
log "安装并构建前端"
cd "${ROOT_DIR}/frontend/app"
if [[ -f package-lock.json ]]; then
npm ci
else
npm install
fi
npm run build
}
install_systemd_service() {
local service_name="$1"
local template_path="$2"
local target_path="${USER_SYSTEMD_DIR}/${service_name}.service"
local tmp_path
tmp_path="$(mktemp)"
mkdir -p "${USER_SYSTEMD_DIR}"
sed \
-e "s#__PROJECT_ROOT__#${ROOT_DIR}#g" \
"${template_path}" >"${tmp_path}"
install -m 0644 "${tmp_path}" "${target_path}"
rm -f "${tmp_path}"
}
install_services() {
require_command systemctl "当前系统不支持 systemd,无法安装服务。"
log "安装/更新用户级 systemd 服务"
install_systemd_service "${BACKEND_SERVICE}" "${ROOT_DIR}/deploy/systemd/${BACKEND_SERVICE}.service.template"
install_systemd_service "${FRONTEND_SERVICE}" "${ROOT_DIR}/deploy/systemd/${FRONTEND_SERVICE}.service.template"
systemctl --user daemon-reload
systemctl --user enable "${BACKEND_SERVICE}" "${FRONTEND_SERVICE}"
}
restart_services() {
log "重启服务"
systemctl --user restart "${BACKEND_SERVICE}"
systemctl --user restart "${FRONTEND_SERVICE}"
}
health_check() {
# shellcheck disable=SC1090
source "${ENV_FILE}"
local backend_url="http://${CLAW_BACKEND_HOST:-127.0.0.1}:${CLAW_BACKEND_PORT:-8765}/api/state"
local frontend_url="http://127.0.0.1:${CLAW_FRONTEND_PORT:-3000}/"
log "健康检查"
for _ in {1..40}; do
if curl -fsS "${backend_url}" >/dev/null 2>&1 && curl -fsS "${frontend_url}" >/dev/null 2>&1; then
echo "backend: ${backend_url}"
echo "frontend: http://<机器IP>:${CLAW_FRONTEND_PORT:-3000}/"
echo "服务已启动。"
return
fi
sleep 0.5
done
warn "健康检查失败,输出最近日志。"
journalctl --user -u "${BACKEND_SERVICE}" -n 80 --no-pager || true
journalctl --user -u "${FRONTEND_SERVICE}" -n 80 --no-pager || true
exit 1
}
main() {
log "ZK Data Agent Ubuntu 部署"
update_git
ensure_env_file
ensure_python
build_frontend
install_services
restart_services
health_check
}
main "$@"