Real IMAP connection via imapflow library: - Connects to IMAP server (IMAPFlow, TLS on port 993) - Fetches recent messages (configurable limit, default 20) - Extracts: from, fromEmail, subject, date, snippet (200 chars) - Thread-to-contact matching (PRD §6.1): 1. Match sender email against persons table 2. If no person match, extract domain and match against company.website 3. Returns matchedCompany + matchedPerson for each email - Audit-logged (action: crm.mailsync:imap) - Zod validated (host, port, user, password, folder, limit) - 502 on IMAP connection failure with detail - 400 on validation failure Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
351 lines
11 KiB
Bash
Executable File
351 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ---
|
|
# name: pdf-env-setup (macOS / Linux)
|
|
# description: Environment detection, dependency check & install for PDF skill on macOS and Linux.
|
|
# ---
|
|
set -euo pipefail
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
|
fail() { echo -e " ${RED}✗${NC} $1"; }
|
|
warn() { echo -e " ${YELLOW}○${NC} $1"; }
|
|
info() { echo -e " ${BLUE}→${NC} $1"; }
|
|
|
|
# ── Resolve PDF_SKILL_DIR ──
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PDF_SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
export PDF_SKILL_DIR
|
|
|
|
echo "============================================"
|
|
echo " PDF Skill — Environment Setup"
|
|
echo " (macOS / Linux)"
|
|
echo "============================================"
|
|
echo ""
|
|
|
|
# ── Step 1: Platform Detection ──
|
|
OS="$(uname -s)"
|
|
ARCH="$(uname -m)"
|
|
echo "Platform: $OS $ARCH"
|
|
|
|
if [ "$OS" = "Darwin" ]; then
|
|
PLATFORM="mac"
|
|
elif [ "$OS" = "Linux" ]; then
|
|
PLATFORM="linux"
|
|
else
|
|
echo "Unsupported platform: $OS. For Windows use env_setup/setup_windows.ps1"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Detected: $PLATFORM"
|
|
echo "PDF_SKILL_DIR=$PDF_SKILL_DIR"
|
|
echo ""
|
|
|
|
# ── China mirror detection & config ──
|
|
USE_CN_MIRROR=false
|
|
PIP_MIRROR_ARGS=""
|
|
NPM_MIRROR_ARGS=""
|
|
PLAYWRIGHT_HOST=""
|
|
|
|
if [ "${USE_CN_MIRROR_FORCE:-}" = "true" ]; then
|
|
USE_CN_MIRROR=true
|
|
elif curl -s --connect-timeout 3 https://pypi.org > /dev/null 2>&1; then
|
|
USE_CN_MIRROR=false
|
|
else
|
|
warn "pypi.org unreachable — enabling China mirrors"
|
|
USE_CN_MIRROR=true
|
|
fi
|
|
|
|
if [ "$USE_CN_MIRROR" = true ]; then
|
|
PIP_MIRROR_ARGS="-i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn"
|
|
NPM_MIRROR_ARGS="--registry https://registry.npmmirror.com"
|
|
PLAYWRIGHT_HOST="https://npmmirror.com/mirrors/playwright/"
|
|
info "China mirrors enabled (pip: tuna, npm: npmmirror, playwright: npmmirror)"
|
|
echo ""
|
|
fi
|
|
|
|
ERRORS=0
|
|
|
|
# ── Step 2a: Homebrew (macOS only) ──
|
|
if [ "$PLATFORM" = "mac" ]; then
|
|
echo "--- [1/10] Homebrew (macOS package manager) ---"
|
|
if command -v brew &>/dev/null; then
|
|
ok "brew installed"
|
|
else
|
|
fail "brew not found"
|
|
info "Install: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
|
|
ERRORS=$((ERRORS + 1))
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# ── Step 2b: Python 3 ──
|
|
echo "--- [2/10] Python 3 ---"
|
|
if command -v python3 &>/dev/null; then
|
|
PY_VER=$(python3 --version 2>&1)
|
|
ok "python3 ($PY_VER)"
|
|
if [ "$PLATFORM" = "mac" ]; then
|
|
PY_PATH=$(which python3 2>/dev/null)
|
|
if [[ "$PY_PATH" == "/usr/bin/python3" ]]; then
|
|
warn "Using macOS system Python (limited). Recommend: brew install python3"
|
|
fi
|
|
fi
|
|
else
|
|
fail "python3 not found"
|
|
case "$PLATFORM" in
|
|
mac) info "Install: brew install python3" ;;
|
|
linux) info "Install: sudo apt install python3 python3-pip (Debian/Ubuntu)"
|
|
info " sudo dnf install python3 python3-pip (Fedora/RHEL)" ;;
|
|
esac
|
|
ERRORS=$((ERRORS + 1))
|
|
fi
|
|
echo ""
|
|
|
|
# ── Step 2c: pip ──
|
|
echo "--- [3/10] pip ---"
|
|
if python3 -m pip --version &>/dev/null 2>&1; then
|
|
ok "pip installed"
|
|
else
|
|
fail "pip not found"
|
|
case "$PLATFORM" in
|
|
mac) info "Install: python3 -m ensurepip --upgrade" ;;
|
|
linux) info "Install: sudo apt install python3-pip" ;;
|
|
esac
|
|
ERRORS=$((ERRORS + 1))
|
|
fi
|
|
echo ""
|
|
|
|
# ── Step 2d: Python packages ──
|
|
echo "--- [4/10] Python Packages (pikepdf, pdfplumber, pypdf, reportlab, PyMuPDF) ---"
|
|
PY_PKGS=(
|
|
"pikepdf:pikepdf"
|
|
"pdfplumber:pdfplumber"
|
|
"pypdf:pypdf"
|
|
"reportlab:reportlab"
|
|
"pymupdf:PyMuPDF"
|
|
)
|
|
|
|
MISSING_PY=()
|
|
for entry in "${PY_PKGS[@]}"; do
|
|
mod="${entry%%:*}"
|
|
pkg="${entry##*:}"
|
|
# PyMuPDF imports as 'fitz'
|
|
if [ "$mod" = "pymupdf" ]; then mod="fitz"; fi
|
|
if python3 -c "import $mod" 2>/dev/null; then
|
|
ver=$(python3 -c "import $mod; print(getattr($mod, '__version__', getattr($mod, 'version', 'ok')))" 2>/dev/null)
|
|
ok "$pkg ($ver)"
|
|
else
|
|
fail "$pkg not installed"
|
|
MISSING_PY+=("$pkg")
|
|
fi
|
|
done
|
|
|
|
if [ ${#MISSING_PY[@]} -gt 0 ]; then
|
|
info "Installing missing Python packages: ${MISSING_PY[*]}"
|
|
# shellcheck disable=SC2086
|
|
python3 -m pip install -q $PIP_MIRROR_ARGS "${MISSING_PY[@]}" 2>/dev/null \
|
|
|| python3 -m pip install -q --user $PIP_MIRROR_ARGS "${MISSING_PY[@]}" 2>/dev/null \
|
|
|| python3 -m pip install -q --break-system-packages $PIP_MIRROR_ARGS "${MISSING_PY[@]}" 2>/dev/null \
|
|
|| { fail "pip install failed. Try: pip install $PIP_MIRROR_ARGS ${MISSING_PY[*]}"; ERRORS=$((ERRORS + 1)); }
|
|
ok "Installed: ${MISSING_PY[*]}"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Step 2e: Node.js + npm ──
|
|
echo "--- [5/10] Node.js + npm ---"
|
|
if command -v node &>/dev/null; then
|
|
ok "node ($(node --version))"
|
|
else
|
|
fail "node not found"
|
|
case "$PLATFORM" in
|
|
mac) info "Install: brew install node" ;;
|
|
linux) info "Install: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -"
|
|
info " sudo apt install -y nodejs"
|
|
if [ "$USE_CN_MIRROR" = true ]; then
|
|
info "China alt: https://npmmirror.com/mirrors/node/"
|
|
fi ;;
|
|
esac
|
|
ERRORS=$((ERRORS + 1))
|
|
fi
|
|
|
|
if command -v npm &>/dev/null; then
|
|
ok "npm ($(npm --version))"
|
|
else
|
|
fail "npm not found"
|
|
ERRORS=$((ERRORS + 1))
|
|
fi
|
|
echo ""
|
|
|
|
# ── Step 2f: Playwright + Chromium ──
|
|
echo "--- [6/10] Playwright + Chromium ---"
|
|
if node -e "require('playwright')" 2>/dev/null; then
|
|
PW_VER=$(node -e "console.log(require('playwright/package.json').version)" 2>/dev/null)
|
|
ok "playwright ($PW_VER)"
|
|
else
|
|
fail "playwright not installed"
|
|
if [ "$USE_CN_MIRROR" = true ]; then
|
|
info "Install: npm install -g playwright@1.50.0 $NPM_MIRROR_ARGS"
|
|
else
|
|
info "Install: npm install -g playwright@1.50.0"
|
|
fi
|
|
ERRORS=$((ERRORS + 1))
|
|
fi
|
|
|
|
if [ "$PLATFORM" = "mac" ]; then
|
|
PW_CACHE="$HOME/Library/Caches/ms-playwright"
|
|
else
|
|
PW_CACHE="$HOME/.cache/ms-playwright"
|
|
fi
|
|
|
|
if ls "$PW_CACHE"/chromium-* &>/dev/null 2>&1; then
|
|
ok "chromium installed"
|
|
else
|
|
fail "chromium not installed"
|
|
if [ "$USE_CN_MIRROR" = true ]; then
|
|
info "Install: PLAYWRIGHT_DOWNLOAD_HOST=$PLAYWRIGHT_HOST npx playwright install chromium"
|
|
else
|
|
info "Install: npx playwright install chromium"
|
|
fi
|
|
if [ "$PLATFORM" = "linux" ]; then
|
|
info " npx playwright install-deps (system libs, needs sudo)"
|
|
fi
|
|
ERRORS=$((ERRORS + 1))
|
|
fi
|
|
echo ""
|
|
|
|
# ── Step 2g: Tectonic (optional, LaTeX engine) ──
|
|
echo "--- [7/10] Tectonic (optional, LaTeX/Academic PDFs) ---"
|
|
BUNDLED="$PDF_SKILL_DIR/scripts/tectonic"
|
|
if [ -x "$BUNDLED" ] && [ "$OS" = "Darwin" ] && [ "$ARCH" = "arm64" ]; then
|
|
ok "tectonic (bundled, macOS arm64)"
|
|
elif command -v tectonic &>/dev/null; then
|
|
TEC_VER=$(tectonic --version 2>&1 | head -1)
|
|
ok "tectonic ($TEC_VER)"
|
|
elif [ -x "$HOME/tectonic" ]; then
|
|
ok "tectonic (~/tectonic)"
|
|
else
|
|
warn "tectonic not installed (needed only for LaTeX/academic PDFs)"
|
|
case "$PLATFORM" in
|
|
mac) info "Install: brew install tectonic" ;;
|
|
linux) info "Install: conda install -c conda-forge tectonic"
|
|
info " or: conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ && conda install tectonic" ;;
|
|
esac
|
|
fi
|
|
echo ""
|
|
|
|
# ── Step 2h: LibreOffice (optional) ──
|
|
echo "--- [8/10] LibreOffice (optional, Office→PDF) ---"
|
|
if command -v soffice &>/dev/null; then
|
|
LO_VER=$(soffice --version 2>/dev/null | head -1)
|
|
ok "libreoffice ($LO_VER)"
|
|
else
|
|
warn "libreoffice not installed (needed only for .docx/.xlsx→PDF conversion)"
|
|
case "$PLATFORM" in
|
|
mac) info "Install: brew install --cask libreoffice" ;;
|
|
linux) info "Install: sudo apt install libreoffice-core" ;;
|
|
esac
|
|
fi
|
|
echo ""
|
|
|
|
# ── Step 2i: Font Installation (from CDN) ──
|
|
echo "--- [9/10] Font Installation ---"
|
|
FONT_CDN_BASE="https://z-cdn.chatglm.cn/office-skill/fonts"
|
|
FONT_LIST="$SCRIPT_DIR/font_list.txt"
|
|
|
|
if [ "$PLATFORM" = "mac" ]; then
|
|
USER_FONT_DIR="$HOME/Library/Fonts"
|
|
else
|
|
USER_FONT_DIR="$HOME/.local/share/fonts"
|
|
fi
|
|
mkdir -p "$USER_FONT_DIR"
|
|
|
|
MARKER="$USER_FONT_DIR/.office-skill-fonts-installed"
|
|
if [ -f "$MARKER" ]; then
|
|
ok "Fonts already installed (marker found). To re-install, delete $MARKER"
|
|
else
|
|
if [ ! -f "$FONT_LIST" ]; then
|
|
fail "Font list not found: $FONT_LIST"
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
TOTAL=$(wc -l < "$FONT_LIST" | tr -d ' ')
|
|
INSTALLED=0
|
|
SKIPPED=0
|
|
FAILED=0
|
|
info "Downloading $TOTAL fonts from CDN..."
|
|
|
|
while IFS= read -r rel_path || [ -n "$rel_path" ]; do
|
|
[ -z "$rel_path" ] && continue
|
|
fname="$(basename "$rel_path")"
|
|
|
|
if [ -f "$USER_FONT_DIR/$fname" ]; then
|
|
SKIPPED=$((SKIPPED + 1))
|
|
continue
|
|
fi
|
|
|
|
# URL-encode special chars (brackets)
|
|
encoded=$(echo "$rel_path" | sed 's/\[/%5B/g; s/\]/%5D/g; s/ /%20/g')
|
|
url="$FONT_CDN_BASE/$encoded"
|
|
|
|
if curl -fSL --connect-timeout 15 --retry 2 -o "$USER_FONT_DIR/$fname" "$url" 2>/dev/null; then
|
|
INSTALLED=$((INSTALLED + 1))
|
|
else
|
|
warn "Failed to download: $rel_path"
|
|
FAILED=$((FAILED + 1))
|
|
rm -f "$USER_FONT_DIR/$fname" 2>/dev/null
|
|
fi
|
|
done < "$FONT_LIST"
|
|
|
|
if [ "$PLATFORM" = "linux" ] && [ $INSTALLED -gt 0 ]; then
|
|
fc-cache -f "$USER_FONT_DIR" 2>/dev/null
|
|
fi
|
|
|
|
if [ $FAILED -eq 0 ]; then
|
|
touch "$MARKER"
|
|
ok "Fonts: $INSTALLED newly installed, $SKIPPED already present (target: $USER_FONT_DIR)"
|
|
else
|
|
warn "Fonts: $INSTALLED installed, $SKIPPED skipped, $FAILED failed (marker not written, will retry next run)"
|
|
ERRORS=$((ERRORS + 1))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Set FONT_DIR to user font directory (fonts are now installed there)
|
|
FONT_DIR="$USER_FONT_DIR"
|
|
export FONT_DIR
|
|
echo ""
|
|
|
|
# ── Step 2j: CJK Font Verification ──
|
|
echo "--- [10/10] CJK Font Verification ---"
|
|
CJK_FOUND=false
|
|
|
|
if [ "$PLATFORM" = "mac" ]; then
|
|
if [ -f "$HOME/Library/Fonts/NotoSansSC[wght].ttf" ] || fc-list :lang=zh 2>/dev/null | head -1 | grep -q .; then
|
|
ok "CJK fonts available (Noto Sans SC or system)"
|
|
CJK_FOUND=true
|
|
fi
|
|
if [ -f "$HOME/Library/Fonts/NotoSansSC[wght].ttf" ]; then
|
|
ok "Noto Sans SC (user fonts)"
|
|
CJK_FOUND=true
|
|
fi
|
|
elif [ "$PLATFORM" = "linux" ]; then
|
|
if fc-list :lang=zh 2>/dev/null | head -1 | grep -q .; then
|
|
ok "CJK fonts available (fc-list)"
|
|
CJK_FOUND=true
|
|
fi
|
|
fi
|
|
|
|
if [ "$CJK_FOUND" = false ]; then
|
|
warn "No CJK font detected — fonts were downloaded and copied; rebuild font cache if needed"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Summary ──
|
|
echo "============================================"
|
|
if [ $ERRORS -eq 0 ]; then
|
|
echo " All dependencies OK."
|
|
else
|
|
echo " $ERRORS issue(s) found. Fix them above."
|
|
fi
|
|
echo " PDF_SKILL_DIR=$PDF_SKILL_DIR"
|
|
echo " FONT_DIR=$FONT_DIR (user font directory)"
|
|
echo "============================================"
|