scripts/butler-deploy.sh
Rama main · réplica de lectura del repositorio original.
#!/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 <<EOF
Butler v4 统一部署工具
Usage: $(basename "$0") <command> [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 "$@"