#!/usr/bin/env bash # Butler v4 — 统一部署入口 # 自动检测环境类型(全新 / 已有),支持 init/update/status/rollback/langfuse-only 五种模式。 # # Usage: # bash scripts/butler-deploy.sh init # 全新环境部署 # bash scripts/butler-deploy.sh update # 增量更新(git pull + deps + restart + verify) # bash scripts/butler-deploy.sh status # 全栈状态检查 # bash scripts/butler-deploy.sh rollback # 回滚到上一版本 # LangFuse 栈独立运维: ~/gongju/langfuse/ops.sh set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BUTLER_HOME="${BUTLER_HOME:-$HOME/.butler}" GONGJU_LANGFUSE="${GONGJU_LANGFUSE:-$HOME/gongju/langfuse}" LANGFUSE_OPS="$GONGJU_LANGFUSE/ops.sh" UNIT=butler-gateway.service LOG="$ROOT/logs/butler-gateway.log" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' _info() { echo -e "${CYAN}[INFO]${NC} $*"; } _ok() { echo -e "${GREEN}[OK]${NC} $*"; } _warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } _err() { echo -e "${RED}[ERROR]${NC} $*" >&2; } # shellcheck source=scripts/lib/butler-pip-extras.sh source "$ROOT/scripts/lib/butler-pip-extras.sh" _usage() { cat < [options] Commands: init [--systemd] [--with-langfuse] 全新环境部署(默认不启 LangFuse 栈) update [--with-langfuse] [--skip-reindex] [--skip-benchmark] 增量更新 status 全栈状态(Butler + LangFuse 可达性 + systemd) rollback [--steps N] 回滚到上一版本(git + deps + restart) LangFuse 栈: cd ~/gongju/langfuse && ./ops.sh {up|down|status|logs|pull} Examples: bash scripts/butler-deploy.sh init bash scripts/butler-deploy.sh update bash scripts/butler-deploy.sh status bash scripts/butler-deploy.sh rollback EOF } # ─── LangFuse 管理 ─── _langfuse_health() { local host="${LANGFUSE_HOST:-http://localhost:3000}" curl -sf "${host%/}/api/public/health" > /dev/null 2>&1 } _langfuse_up() { if [[ ! -x "$LANGFUSE_OPS" ]]; then _warn "LangFuse ops 未找到: $LANGFUSE_OPS" return 1 fi _info "启动 LangFuse($GONGJU_LANGFUSE)..." bash "$LANGFUSE_OPS" up } _observability_status() { echo " === 观测演化 (L7) ===" local env_file="$ROOT/.env" if [[ -f "$env_file" ]]; then # shellcheck disable=SC1090 set -a; source "$env_file" 2>/dev/null || true; set +a fi local lf="${BUTLER_LANGFUSE_ENABLED:-0}" local emb="${BUTLER_EMBEDDING_PROVIDER:-local}" local sem="${BUTLER_SEMANTIC_MEMORY:-0}" if [[ "$lf" == "1" || "$lf" == "true" ]]; then _ok "BUTLER_LANGFUSE_ENABLED=1" else _warn "BUTLER_LANGFUSE_ENABLED 未启用" fi echo " Embedding: provider=$emb semantic=$sem" if PYTHONPATH="$ROOT" python3 -c " from butler.ops.embedding_health import check_embedding_recall r = check_embedding_recall(min_recall=0.5) print(f' Recall@3: {r.recall_at_3:.0%} ({r.hits}/{r.total}) — {r.message}') import sys; sys.exit(0 if not r.degraded else 1) " 2>/dev/null; then : else _warn "Embedder 可能已降级到 HashingEmbedder" fi echo "" } _langfuse_status() { echo " === LangFuse(外部栈: $GONGJU_LANGFUSE)===" if _langfuse_health; then _ok "LangFuse API 健康 (${LANGFUSE_HOST:-http://localhost:3000})" else _warn "LangFuse API 未响应" echo " 启动: cd $GONGJU_LANGFUSE && ./ops.sh up" fi if [[ -x "$LANGFUSE_OPS" ]]; then bash "$LANGFUSE_OPS" status 2>/dev/null || true fi } # ─── Butler 状态 ─── _butler_status() { echo " === Butler 进程 ===" local active active=$(systemctl --user is-active "$UNIT" 2>/dev/null || echo "inactive") if [[ "$active" == "active" ]]; then _ok "Gateway: active" systemctl --user --no-pager status "$UNIT" 2>&1 | head -8 || true else echo " Gateway: $active" fi echo "" pgrep -af 'butler.main' 2>/dev/null || echo " (无 Butler 进程)" echo "" echo " === 版本信息 ===" local git_sha git_sha=$(cd "$ROOT" && git rev-parse --short HEAD 2>/dev/null || echo "unknown") echo " Git HEAD: $git_sha" PYTHONPATH="$ROOT" python3 -c " from butler import get_build_identity info = get_build_identity() print(f\" Butler: v{info['version']} (commit={info['git_sha']}, python={info['python']})\") " 2>/dev/null || echo " Butler: (import failed)" echo "" echo " === 数据目录 ===" if [[ -d "$BUTLER_HOME" ]]; then echo " $BUTLER_HOME" du -sh "$BUTLER_HOME" 2>/dev/null | awk '{print " 总大小: " $1}' else echo " $BUTLER_HOME: 不存在" fi echo "" echo " === Python 环境 ===" echo " venv: ${VIRTUAL_ENV:-none}" python3 --version 2>/dev/null || echo " python3: not found" echo "" } # ─── Init(全新部署) ─── _cmd_init() { local install_systemd=0 local with_langfuse=0 while [[ $# -gt 0 ]]; do case "$1" in --systemd) install_systemd=1 ;; --with-langfuse) with_langfuse=1 ;; --skip-langfuse) with_langfuse=0 ;; *) _err "Unknown init option: $1"; _usage; exit 1 ;; esac shift done echo "" echo "╔══════════════════════════════════════╗" echo "║ Butler v4 — 全新环境部署 ║" echo "╚══════════════════════════════════════╝" echo "" local extra_args="" [[ "$install_systemd" -eq 1 ]] && extra_args="$extra_args --systemd" [[ "$with_langfuse" -eq 1 ]] && extra_args="$extra_args --langfuse" bash "$ROOT/scripts/deploy-new-env.sh" $extra_args if [[ "$with_langfuse" -eq 1 ]]; then _info "配置观测演化生产默认值..." bash "$ROOT/scripts/butler-observability-provision.sh" 2>/dev/null || _warn "观测配置跳过" if [[ -f "$ROOT/scripts/install-butler-eval-sync-timer.sh" ]]; then bash "$ROOT/scripts/install-butler-eval-sync-timer.sh" --no-enable 2>/dev/null || true fi fi echo "" _ok "全新部署完成" echo "" echo " 下一步:" echo " 1. 编辑 $ROOT/.env 填入 API keys" echo " 2. 运行 bash scripts/butler-deploy.sh status 检查状态" } # ─── Update(增量更新) ─── _cmd_update() { local with_langfuse=0 local skip_reindex=0 local skip_benchmark=0 while [[ $# -gt 0 ]]; do case "$1" in --with-langfuse) with_langfuse=1 ;; --skip-langfuse) with_langfuse=0 ;; --skip-reindex) skip_reindex=1 ;; --skip-benchmark) skip_benchmark=1 ;; *) _err "Unknown update option: $1"; _usage; exit 1 ;; esac shift done echo "" echo "╔══════════════════════════════════════╗" echo "║ Butler v4 — 增量更新 ║" echo "╚══════════════════════════════════════╝" echo "" local prev_sha prev_sha=$(cd "$ROOT" && git rev-parse --short HEAD 2>/dev/null || echo "unknown") # Step 1: Git pull _info "[1/7] 拉取代码..." cd "$ROOT" if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git pull --ff-only 2>&1 || { _warn "git pull 失败(可能有本地修改)"; } else _warn "非 git 仓库,跳过 pull" fi local new_sha new_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") if [[ "$prev_sha" != "$new_sha" ]]; then _ok "代码更新: $prev_sha → $new_sha" else _info "代码无变化: $new_sha" fi # Step 2: Clean stale caches _info "[2/7] 清理 __pycache__..." find "$ROOT" -path '*/butler*/__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true find "$ROOT" -path '*/tests*/__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true # Step 3: Sync dependencies _info "[3/7] 同步依赖(profile=$(butler_resolve_pip_profile), extras=[$(butler_pip_extra_spec)])..." if [[ -n "${VIRTUAL_ENV:-}" ]]; then butler_pip_install_extras "$ROOT" || _warn "pip install 失败(非致命)" else if [[ -d "$ROOT/.venv" ]]; then # shellcheck source=/dev/null source "$ROOT/.venv/bin/activate" butler_pip_install_extras "$ROOT" || _warn "pip install 失败(非致命)" else _warn "无 venv,跳过 pip install" fi fi # Step 4: Restart Butler gateway _info "[4/7] 重启 Butler 服务..." if systemctl --user is-active "$UNIT" >/dev/null 2>&1; then bash "$ROOT/scripts/install-butler-gateway-service.sh" 2>/dev/null || true bash "$ROOT/scripts/install-butler-ops-bundle.sh" --no-enable 2>/dev/null || true sleep 2 _ok "Butler 服务已重启" else _info "Gateway 未运行,跳过重启" fi # Step 5: LangFuse(可选;默认栈在 ~/gongju/langfuse) if [[ "$with_langfuse" -eq 1 ]]; then _info "[5/7] 更新 LangFuse 栈..." if [[ -x "$LANGFUSE_OPS" ]]; then bash "$LANGFUSE_OPS" pull 2>/dev/null || _langfuse_up || _warn "LangFuse 更新失败(非致命)" else _warn "LangFuse ops 未找到: $LANGFUSE_OPS" fi _info "同步观测演化配置..." bash "$ROOT/scripts/butler-observability-provision.sh" 2>/dev/null || _warn "观测配置跳过" else _info "[5/7] 跳过 LangFuse 栈(独立运维: $GONGJU_LANGFUSE)" fi # Step 6: Benchmark regression gate (O7) if [[ "$skip_benchmark" -eq 0 ]]; then _info "[6/7] 基准回归门 (B1–B8 / MB1–MB7)..." local bench_args="" if _langfuse_health 2>/dev/null; then bench_args="--sync-dataset" fi if bash "$ROOT/scripts/butler-eval-regression.sh" $bench_args 2>&1; then _ok "基准回归通过" else _warn "基准回归未完全通过(见上方输出)" fi else _info "[6/7] 跳过基准回归(--skip-benchmark)" fi # Step 7: Verify _info "[7/7] 部署验证..." cd "$ROOT" if systemctl --user is-active "$UNIT" >/dev/null 2>&1; then bash "$ROOT/scripts/butler-gateway-ops.sh" verify 2>&1 || _warn "验证不完全通过" else PYTHONPATH="$ROOT" python3 -c " from butler import get_build_identity info = get_build_identity() print(f' Butler v{info[\"version\"]} (commit={info[\"git_sha\"]})') " 2>/dev/null || _warn "Butler import 验证失败" fi if [[ "$skip_reindex" -eq 0 ]]; then if [[ -f "$ROOT/scripts/butler-memory-reindex.sh" ]]; then bash "$ROOT/scripts/butler-memory-reindex.sh" --project "灵文1号" 2>/dev/null || true fi fi echo "" _ok "增量更新完成 ($prev_sha → $new_sha)" } # ─── Rollback ─── _cmd_rollback() { local steps=1 while [[ $# -gt 0 ]]; do case "$1" in --steps) steps="$2"; shift ;; *) _err "Unknown rollback option: $1"; exit 1 ;; esac shift done echo "" echo "╔══════════════════════════════════════╗" echo "║ Butler v4 — 回滚 ║" echo "╚══════════════════════════════════════╝" echo "" cd "$ROOT" local current_sha current_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") _info "当前版本: $current_sha" _info "回滚 $steps 个提交..." git log --oneline -"$((steps + 1))" 2>/dev/null || true echo "" local target_sha target_sha=$(git rev-parse --short "HEAD~$steps" 2>/dev/null) if [[ -z "$target_sha" ]]; then _err "无法计算回滚目标" exit 1 fi _warn "将回滚到: $target_sha" read -r -p "确认回滚? [y/N] " confirm if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then echo "取消回滚" exit 0 fi git checkout "$target_sha" -- . 2>/dev/null || { _err "git checkout 失败" exit 1 } _info "同步依赖(profile=$(butler_resolve_pip_profile))..." butler_pip_install_extras "$ROOT" || true if systemctl --user is-active "$UNIT" >/dev/null 2>&1; then _info "重启服务..." systemctl --user restart "$UNIT" sleep 2 fi _ok "回滚完成: $current_sha → $target_sha" echo " 如需恢复: git checkout main" } # ─── Status ─── _cmd_status() { echo "" echo "╔══════════════════════════════════════╗" echo "║ Butler v4 — 全栈状态 ║" echo "╚══════════════════════════════════════╝" echo "" _butler_status _langfuse_status _observability_status echo "" echo " === Systemd Timers ===" systemctl --user list-timers --all 2>/dev/null | grep -i butler || echo " (无 Butler 定时器)" echo "" } # ─── Main ─── main() { local cmd="${1:-}" shift || true case "$cmd" in init) _cmd_init "$@" ;; update|upgrade) _cmd_update "$@" ;; status) _cmd_status ;; rollback) _cmd_rollback "$@" ;; -h|--help|help|"") _usage ;; *) _err "Unknown command: $cmd" _usage exit 1 ;; esac } main "$@"