scripts/lib/butler-gateway-preflight.sh
Rama main · réplica de lectura del repositorio original.
# Shared preflight for Butler WeChat gateway (source, do not execute directly).
# Usage: ROOT=... source scripts/lib/butler-gateway-preflight.sh
# butler_gateway_preflight
# shellcheck source=scripts/lib/butler-source-env.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/butler-source-env.sh"
butler_gateway_preflight() {
local root="${ROOT:?ROOT not set}"
local errors=0
local warns=0
_bg_ok() { printf ' [ok] %s\n' "$1"; }
_bg_warn() { printf ' [warn] %s\n' "$1"; warns=$((warns + 1)); }
_bg_fail() { printf ' [fail] %s\n' "$1"; errors=$((errors + 1)); }
_bg_compact() {
printf '%s' "${1:-}" | tr -d '[:space:]'
}
echo "Butler gateway preflight ($root)"
if ! command -v python3 >/dev/null 2>&1; then
_bg_fail "python3 not in PATH"
else
_bg_ok "python3: $(command -v python3)"
fi
if [[ ! -f "$root/.env" ]]; then
_bg_fail "missing $root/.env (cp .env.example .env)"
else
_bg_ok ".env present"
butler_source_env "$root/.env" || {
_bg_fail "failed to source $root/.env (check unset vars under set -u, e.g. use \${LD_LIBRARY_PATH:-})"
errors=$((errors + 1))
}
if [[ -z "${MINIMAX_API_KEY:-}" && -z "${MINIMAX_CN_API_KEY:-}" ]]; then
_bg_warn "MINIMAX_API_KEY not set in .env (gateway LLM may fail)"
else
_bg_ok "MiniMax API key configured"
fi
fi
if ! PYTHONPATH="$root" python3 -c "
from butler.gateway.platforms.wechat import check_wechat_requirements
import sys
sys.exit(0 if check_wechat_requirements() else 1)
" 2>/dev/null; then
_bg_fail 'wechat extras missing: pip install -e ".[gateway]"'
else
_bg_ok "wechat Python deps"
fi
local _gw_extra_ok=1
PYTHONPATH="$root" python3 -c "import mcp" 2>/dev/null || _gw_extra_ok=0
if [[ "$_gw_extra_ok" -eq 0 ]]; then
_bg_fail 'mcp extra missing: pip install -e ".[gateway]"'
else
_bg_ok "mcp Python deps"
fi
_gw_extra_ok=1
PYTHONPATH="$root" python3 -c "import fastembed" 2>/dev/null || _gw_extra_ok=0
if [[ "$_gw_extra_ok" -eq 0 ]]; then
_bg_warn "fastembed missing — semantic memory degraded: pip install -e \".[gateway]\""
else
_bg_ok "fastembed (embeddings)"
fi
_gw_extra_ok=1
PYTHONPATH="$root" python3 -c "import chromadb" 2>/dev/null || _gw_extra_ok=0
if [[ "$_gw_extra_ok" -eq 0 ]]; then
_bg_warn "chromadb missing — vector store degraded: pip install -e \".[gateway]\""
else
_bg_ok "chromadb (vectors)"
fi
_gw_extra_ok=1
PYTHONPATH="$root" python3 -c "import trafilatura" 2>/dev/null || _gw_extra_ok=0
if [[ "$_gw_extra_ok" -eq 0 ]]; then
_bg_warn "trafilatura missing — web_fetch uses regex fallback: pip install -e \".[gateway]\""
else
_bg_ok "trafilatura (web_fetch)"
fi
if [[ "${BUTLER_MCP_ENABLED:-0}" =~ ^(1|true|yes|on)$ ]]; then
if ! command -v npx >/dev/null 2>&1; then
_bg_warn "BUTLER_MCP_ENABLED=1 but npx not in PATH (Firecrawl MCP needs Node 18+)"
else
_bg_ok "npx present (MCP stdio)"
fi
local mcp_cfg="${BUTLER_MCP_CONFIG:-$HOME/.butler/mcp.yaml}"
local max_srv="${BUTLER_MCP_MAX_SERVERS:-3}"
if [[ -f "$mcp_cfg" ]]; then
local cfg_n
cfg_n="$(PYTHONPATH="$root" MCP_CFG="$mcp_cfg" python3 - <<'PY'
import os, yaml
from pathlib import Path
p = Path(os.environ["MCP_CFG"])
data = yaml.safe_load(p.read_text(encoding="utf-8")) if p.is_file() else {}
s = data.get("servers") or {}
print(len(s) if isinstance(s, dict) else 0)
PY
)"
if [[ "${cfg_n:-0}" -gt "${max_srv}" ]]; then
_bg_warn "BUTLER_MCP_MAX_SERVERS=${max_srv} but mcp.yaml has ${cfg_n} servers — raise to ≥${cfg_n} (EXT-5 markitdown)"
else
_bg_ok "MCP_MAX_SERVERS=${max_srv} ≥ configured servers (${cfg_n:-0})"
fi
fi
fi
local butler_home="${BUTLER_HOME:-$HOME/.butler}"
if [[ ! -f "$butler_home/config.yaml" ]]; then
_bg_warn "~/.butler/config.yaml 缺失 — 可复制 docs/config/config.yaml.example"
else
_bg_ok "config.yaml present ($butler_home/config.yaml)"
fi
local accounts_dir="$butler_home/wechat/accounts"
if [[ ! -d "$accounts_dir" ]] || [[ -z "$(find "$accounts_dir" -maxdepth 1 -name '*.json' 2>/dev/null | head -1)" ]]; then
_bg_warn "no WeChat account in $accounts_dir — run: butler wechat-setup"
else
local n
n="$(find "$accounts_dir" -maxdepth 1 -name '*.json' 2>/dev/null | wc -l)"
_bg_ok "WeChat accounts: $n file(s) under $accounts_dir"
fi
if command -v loginctl >/dev/null 2>&1; then
local linger
linger="$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)"
if [[ "$linger" != "yes" ]]; then
_bg_warn "linger=no — user systemd stops at logout; run: sudo loginctl enable-linger $(id -un)"
else
_bg_ok "linger=yes (survives reboot without login)"
fi
fi
if systemctl --user cat hermes-gateway.service >/dev/null 2>&1; then
if systemctl --user is-active hermes-gateway.service >/dev/null 2>&1; then
_bg_warn "hermes-gateway.service is active — stop/disable to avoid Bot conflicts"
else
_bg_ok "hermes-gateway.service not running"
fi
fi
mkdir -p "$root/logs"
_bg_ok "logs dir: $root/logs"
local dm_policy="${WECHAT_DM_POLICY:-open}"
dm_policy="${dm_policy,,}"
local dm_allow_compact
dm_allow_compact="$(_bg_compact "${WECHAT_ALLOWED_USERS:-}")"
if [[ "$dm_policy" == "open" ]]; then
_bg_warn "WECHAT_DM_POLICY=open — set allowlist + WECHAT_ALLOWED_USERS before exposing the Bot"
elif [[ "$dm_policy" == "allowlist" ]]; then
if [[ -z "$dm_allow_compact" ]]; then
_bg_warn "WECHAT_DM_POLICY=allowlist but WECHAT_ALLOWED_USERS is empty"
else
_bg_ok "WeChat DM allowlist configured"
fi
else
_bg_ok "WeChat DM policy: $dm_policy"
fi
local group_policy="${WECHAT_GROUP_POLICY:-disabled}"
group_policy="${group_policy,,}"
local group_allow_compact
group_allow_compact="$(_bg_compact "${WECHAT_GROUP_ALLOWED_USERS:-}")"
if [[ "$group_policy" == "open" ]]; then
_bg_warn "WECHAT_GROUP_POLICY=open — group ingress is fully exposed; prefer disabled or allowlist"
elif [[ "$group_policy" == "allowlist" ]]; then
if [[ -z "$group_allow_compact" ]]; then
_bg_warn "WECHAT_GROUP_POLICY=allowlist but WECHAT_GROUP_ALLOWED_USERS is empty"
else
_bg_warn "WECHAT_GROUP_POLICY=allowlist — confirm WECHAT_GROUP_ALLOWED_USERS is minimal"
fi
else
_bg_ok "WeChat group policy: $group_policy"
fi
if [[ -z "${BUTLER_TOOL_SAFE_ROOT:-}" ]]; then
_bg_warn "BUTLER_TOOL_SAFE_ROOT unset — tools may fall back to process cwd without a project"
else
_bg_ok "BUTLER_TOOL_SAFE_ROOT set"
fi
if [[ -z "${BUTLER_DEFAULT_PROJECT:-}" ]]; then
_bg_warn "BUTLER_DEFAULT_PROJECT unset — new chats need /切换 before tool use"
else
_bg_ok "BUTLER_DEFAULT_PROJECT=${BUTLER_DEFAULT_PROJECT}"
fi
local owner_compact gateway_allow_compact
owner_compact="$(_bg_compact "${BUTLER_OWNER_WECHAT_ID:-}")"
gateway_allow_compact="$(_bg_compact "${BUTLER_GATEWAY_ALLOWLIST:-}")"
if [[ -z "$owner_compact" && -z "$gateway_allow_compact" && -z "$dm_allow_compact" ]]; then
_bg_warn "未配置 BUTLER_OWNER_WECHAT_ID / BUTLER_GATEWAY_ALLOWLIST / WECHAT_ALLOWED_USERS — 运行 butler doctor 或补 .env"
else
_bg_ok "Gateway owner/allowlist configured"
fi
local inbound_media="${BUTLER_WECHAT_INBOUND_MEDIA:-1}"
if [[ "$inbound_media" =~ ^(1|true|yes|on)$ ]]; then
if [[ -z "${MINIMAX_API_KEY:-}" && -z "${MINIMAX_CN_API_KEY:-}" ]]; then
_bg_warn "入站识图需 MINIMAX_API_KEY(BUTLER_WECHAT_INBOUND_MEDIA=1)"
fi
local vf="${BUTLER_WECHAT_VISION_FALLBACK:-openai,ocr}"
if [[ "$vf" == *ocr* ]]; then
if ! PYTHONPATH="$root" python3 -c "import pytesseract" 2>/dev/null; then
_bg_warn "VISION_FALLBACK 含 ocr 但未安装 — pip install -e \".[wechat-ocr]\"(及系统 tesseract)"
else
_bg_ok "pytesseract available (vision OCR fallback)"
fi
fi
if [[ "${BUTLER_WECHAT_STT_PROVIDER:-local}" == "local" ]]; then
if ! command -v ffmpeg >/dev/null 2>&1; then
_bg_warn "无 ffmpeg — 纯 .silk 语音无法转写(iLink 自带转写仍可用)"
else
_bg_ok "ffmpeg present (silk STT)"
fi
fi
fi
if [[ "${BUTLER_REASONING_TRACE:-1}" =~ ^(1|true|yes|on)$ ]]; then
_bg_ok "BUTLER_REASONING_TRACE enabled (transcript 推理摘要)"
else
_bg_warn "BUTLER_REASONING_TRACE=0 — /诊断 不展示推理摘要行"
fi
if [[ "${BUTLER_PLAN_REASON_GRAPH:-1}" =~ ^(1|true|yes|on)$ ]]; then
_bg_ok "BUTLER_PLAN_REASON_GRAPH enabled (规划 DoT-lite)"
else
_bg_warn "BUTLER_PLAN_REASON_GRAPH=0 — 规划模式不写推理图"
fi
if [[ "$errors" -gt 0 ]]; then
echo "Preflight: $errors error(s), $warns warning(s)"
return 1
fi
echo "Preflight: ok ($warns warning(s))"
return 0
}