# 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 }