#!/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 BOOTSTRAP_SYSTEM=0 usage() { cat < 覆盖本地改动 --skip-git 跳过 git 拉取,只部署当前工作区 --bootstrap-system 首次安装时使用 sudo 安装 Ubuntu 系统依赖,应用仍部署在用户目录 EOF } while [[ $# -gt 0 ]]; do case "$1" in --force) FORCE=1 shift ;; --skip-git) SKIP_GIT=1 shift ;; --bootstrap-system) BOOTSTRAP_SYSTEM=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 } bootstrap_system_packages() { if [[ "${BOOTSTRAP_SYSTEM}" != "1" ]]; then return fi require_command sudo "首次安装系统依赖需要 sudo。" if ! command -v apt-get >/dev/null 2>&1; then warn "未发现 apt-get,跳过系统依赖安装。请手工安装 git、curl、pyenv、Node.js、npm 和 Python 编译依赖。" return fi log "首次初始化 Ubuntu 系统依赖" sudo apt-get update sudo apt-get install -y \ git curl ca-certificates build-essential \ libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \ libffi-dev liblzma-dev libncursesw5-dev xz-utils tk-dev \ systemd } 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}" </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 部署" bootstrap_system_packages update_git ensure_env_file ensure_python build_frontend install_services restart_services health_check } main "$@"