#!/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}" DEPLOY_INSTANCE="${CLAW_DEPLOY_INSTANCE:-zk-data-agent}" BACKEND_SERVICE="${CLAW_BACKEND_SERVICE:-${DEPLOY_INSTANCE}-backend}" FRONTEND_SERVICE="${CLAW_FRONTEND_SERVICE:-${DEPLOY_INSTANCE}-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 } configure_instance_names() { DEPLOY_INSTANCE="${CLAW_DEPLOY_INSTANCE:-${DEPLOY_INSTANCE:-zk-data-agent}}" BACKEND_SERVICE="${CLAW_BACKEND_SERVICE:-${DEPLOY_INSTANCE}-backend}" FRONTEND_SERVICE="${CLAW_FRONTEND_SERVICE:-${DEPLOY_INSTANCE}-frontend}" } require_command() { local name="$1" local hint="$2" if ! command -v "${name}" >/dev/null 2>&1; then fail "缺少命令:${name}。${hint}" fi } resolve_npm_bin() { if [[ -n "${CLAW_NODE_BIN_DIR:-}" ]]; then export PATH="${CLAW_NODE_BIN_DIR}:${PATH}" fi local npm_bin="${CLAW_NPM_BIN:-$(command -v npm || true)}" if [[ -z "${npm_bin}" || ! -x "${npm_bin}" ]]; then fail "缺少命令:npm。请先安装 Node.js 和 npm,建议 Node.js 20 或 22。" fi local node_bin_dir node_bin_dir="$(dirname "${npm_bin}")" export PATH="${node_bin_dir}:${PATH}" if ! "${npm_bin}" --version >/dev/null 2>&1; then fail "npm 不可用:${npm_bin}。请确认同目录下存在 node,或重新安装 Node.js/npm。" fi printf "%s\n" "${npm_bin}" } bootstrap_system_packages() { if [[ "${BOOTSTRAP_SYSTEM}" != "1" && -f "${ENV_FILE}" ]]; 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() { local npm_bin npm_bin="$(resolve_npm_bin)" export PATH="$(dirname "${npm_bin}"):${PATH}" log "安装并构建前端" cd "${ROOT_DIR}/frontend/app" if [[ -f package-lock.json ]]; then "${npm_bin}" ci else "${npm_bin}" install fi "${npm_bin}" run build } remember_node_runtime() { local npm_bin node_bin node_bin_dir tmp_file npm_bin="$(resolve_npm_bin)" node_bin_dir="$(dirname "${npm_bin}")" export PATH="${node_bin_dir}:${PATH}" node_bin="$(PATH="${node_bin_dir}:${PATH}" command -v node || true)" tmp_file="$(mktemp)" grep -v -E '^(# systemd 用户服务不会读取交互式 shell 配置|export CLAW_(NPM_BIN|NODE_BIN|NODE_BIN_DIR)=)' "${ENV_FILE}" >"${tmp_file}" || true cat >>"${tmp_file}" <"${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 configure_instance_names ensure_python build_frontend remember_node_runtime install_services restart_services health_check } main "$@"