feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)

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
This commit is contained in:
Z User committed 2026-10-27 22:50:50 +00:00
1 parent 7acc0c5451
commit e45b68bb8b
1089 files changed
+260614

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Lightweight environment check for PDF skill.
# Exit 0 = all OK, exit 1 = missing dependencies.
# Also resolves and exports PDF_SKILL_DIR and FONT_DIR.
# Usage: source env_check.sh (preferred, exports vars to caller)
# or: bash env_check.sh [--quiet]
QUIET=false; [ "${1:-}" = "--quiet" ] && QUIET=true
FAIL=0
check() { local desc="$1"; shift; if ! "$@" &>/dev/null; then $QUIET || echo "MISSING: $desc"; FAIL=1; fi; }
# ── Resolve PDF_SKILL_DIR & FONT_DIR ──
_ENV_CHECK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PDF_SKILL_DIR="$(cd "$_ENV_CHECK_DIR/.." && pwd)"
export PDF_SKILL_DIR
if [ "$(uname -s)" = "Darwin" ]; then
FONT_DIR="${HOME}/Library/Fonts"
else
FONT_DIR="/usr/share/fonts"
fi
export FONT_DIR
check "python3" command -v python3
check "node" command -v node
check "pikepdf" python3 -c "import pikepdf"
check "pdfplumber" python3 -c "import pdfplumber"
check "pypdf" python3 -c "import pypdf"
check "reportlab" python3 -c "import reportlab"
check "PyMuPDF" python3 -c "import fitz"
check "playwright" node -e "require('playwright')"
# LaTeX engine (tectonic) — needed for Academic pipeline
if [ -x "$PDF_SKILL_DIR/scripts/tectonic" ]; then
: # bundled binary OK
elif command -v tectonic &>/dev/null; then
: # system tectonic OK
else
$QUIET || echo "MISSING: tectonic (needed for LaTeX/Academic PDFs)"
FAIL=1
fi
# Font check: verify CJK fonts are available
if command -v fc-list &>/dev/null; then
fc-list :lang=zh 2>/dev/null | grep -qi "noto\|simhei\|wenquanyi" || { $QUIET || echo "MISSING: CJK fonts"; FAIL=1; }
fi
$QUIET || echo "PDF_SKILL_DIR=$PDF_SKILL_DIR"
$QUIET || echo "FONT_DIR=$FONT_DIR"
return $FAIL 2>/dev/null || exit $FAIL
+82
View File
@@ -0,0 +1,82 @@
/usr/share/fonts/truetype/chinese/NotoSansSC[wght].ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-Regular.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-Bold.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-Italic.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-BoldItalic.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-Light.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-LightItalic.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-ExtraLight.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-ExtraLightItalic.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-SemiBold.ttf
/usr/share/fonts/truetype/chinese/SarasaMonoSC-SemiBoldItalic.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC[wght].ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-Thin.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-ExtraLight.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-Light.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-Regular.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-Medium.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-SemiBold.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-Bold.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-ExtraBold.ttf
/usr/share/fonts/truetype/chinese/NotoSansSC-Black.ttf
/usr/share/fonts/truetype/chinese/LiberationSans-Regular.ttf
/usr/share/fonts/truetype/chinese/LiberationMono-Regular.ttf
/usr/share/fonts/truetype/chinese/LiberationSerif-Regular.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-VariableFont_wght.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-VariableFont_wght.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-ExtraLight.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-Light.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-Regular.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-Medium.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-SemiBold.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-Bold.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-ExtraBold.ttf
/usr/share/fonts/truetype/noto-serif-sc/NotoSerifSC-Black.ttf
/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKai-Regular.ttf
/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKai-Medium.ttf
/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKai-Light.ttf
/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKaiMono-Regular.ttf
/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKaiMono-Medium.ttf
/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKaiMono-Light.ttf
/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Oblique.ttf
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-BoldOblique.ttf
/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf
/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf
/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf
/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf
/usr/share/fonts/truetype/liberation/LiberationSans-Italic.ttf
/usr/share/fonts/truetype/liberation/LiberationSans-BoldItalic.ttf
/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf
/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf
/usr/share/fonts/truetype/liberation/LiberationSerif-Italic.ttf
/usr/share/fonts/truetype/liberation/LiberationSerif-BoldItalic.ttf
/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf
/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf
/usr/share/fonts/truetype/liberation/LiberationMono-Italic.ttf
/usr/share/fonts/truetype/liberation/LiberationMono-BoldItalic.ttf
/usr/share/fonts/truetype/english/Tinos-Regular.ttf
/usr/share/fonts/truetype/english/Tinos-Bold.ttf
/usr/share/fonts/truetype/english/Tinos-Italic.ttf
/usr/share/fonts/truetype/english/Tinos-BoldItalic.ttf
/usr/share/fonts/truetype/english/Carlito-Regular.ttf
/usr/share/fonts/truetype/english/Carlito-Bold.ttf
/usr/share/fonts/truetype/english/Carlito-Italic.ttf
/usr/share/fonts/truetype/english/Carlito-BoldItalic.ttf
/usr/share/fonts/truetype/freefont/FreeSans.ttf
/usr/share/fonts/truetype/freefont/FreeSansBold.ttf
/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf
/usr/share/fonts/truetype/freefont/FreeSansBoldOblique.ttf
/usr/share/fonts/truetype/freefont/FreeSerif.ttf
/usr/share/fonts/truetype/freefont/FreeSerifBold.ttf
/usr/share/fonts/truetype/freefont/FreeSerifItalic.ttf
/usr/share/fonts/truetype/freefont/FreeSerifBoldItalic.ttf
/usr/share/fonts/truetype/freefont/FreeMono.ttf
/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf
/usr/share/fonts/truetype/freefont/FreeMonoOblique.ttf
/usr/share/fonts/truetype/freefont/FreeMonoBoldOblique.ttf
/usr/share/fonts/truetype/libreoffice/opens___.ttf
+212
View File
@@ -0,0 +1,212 @@
# PDF Skill — Environment Setup Guide
This document contains full platform-specific instructions for setting up the PDF skill environment.
The model should read this file when first-time setup is needed.
---
## Step 1: Platform Detection
Detect the OS and set core variables:
### macOS / Linux (bash/zsh)
```bash
OS="$(uname -s)" # Darwin = macOS, Linux = Linux
ARCH="$(uname -m)" # x86_64 or arm64
PDF_SKILL_DIR="<skill_directory>"
export PDF_SKILL_DIR
```
### Windows (PowerShell, Win10/Win11)
```powershell
$WinVer = [System.Environment]::OSVersion.Version
$Arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
$env:PDF_SKILL_DIR = "<skill_directory>"
```
---
## Step 2: Dependency Check & Install
Run the platform-appropriate setup script:
| Platform | Command |
|----------|---------|
| macOS / Linux | `bash "$PDF_SKILL_DIR/env_setup/setup_mac_linux.sh"` |
| Windows | `powershell -ExecutionPolicy Bypass -File "$env:PDF_SKILL_DIR\env_setup\setup_windows.ps1"` |
### Required Dependencies
| Category | Package | Purpose |
|----------|---------|---------|
| Runtime | Python 3 + pip | PDF generation and processing |
| Runtime | Node.js + npm | Playwright for Creative pipeline |
| Python pkg | pikepdf | PDF manipulation (merge/split/encrypt) |
| Python pkg | pdfplumber | Text/table extraction |
| Python pkg | pypdf | PDF reading/writing |
| Python pkg | reportlab | Report pipeline PDF generation |
| Python pkg | PyMuPDF (fitz) | Fast PDF rendering and extraction |
| npm pkg | playwright | HTML-to-PDF for Creative pipeline |
| Browser | Chromium (via Playwright) | Headless rendering |
| Optional | tectonic | LaTeX/Academic PDF compilation |
| Optional | LibreOffice | Office-to-PDF conversion |
| Font | CJK fonts (from CDN) | Chinese text in PDFs |
### Manual Install by Platform
#### macOS
```bash
brew install python3 node
python3 -m pip install pikepdf pdfplumber pypdf reportlab PyMuPDF
npm install -g playwright@1.50.0 && npx playwright install chromium
# Pinned to 1.50.0 — npmmirror has Chromium rev 1155 for this version
brew install tectonic # optional: LaTeX
brew install --cask libreoffice # optional: Office-to-PDF
```
#### Linux (Debian/Ubuntu)
```bash
sudo apt install python3 python3-pip nodejs npm
python3 -m pip install pikepdf pdfplumber pypdf reportlab PyMuPDF
npm install -g playwright@1.50.0 && npx playwright install chromium && npx playwright install-deps
# Pinned to 1.50.0 — npmmirror has Chromium rev 1155 for this version
# optional:
conda install -c conda-forge tectonic # use Tsinghua mirror: conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
sudo apt install libreoffice-core # LibreOffice
```
#### Windows (PowerShell)
```powershell
winget install Python.Python.3.11
winget install OpenJS.NodeJS.LTS
python -m pip install pikepdf pdfplumber pypdf reportlab PyMuPDF
npm install -g playwright@1.50.0
# Pinned to 1.50.0 — npmmirror has Chromium rev 1155 for this version
npx playwright install chromium
# optional:
scoop install tectonic # LaTeX
winget install TheDocumentFoundation.LibreOffice # Office-to-PDF
```
Alternative Windows package managers:
- `choco install python3 nodejs-lts`
- `scoop install python nodejs-lts`
---
## Step 3: Font Installation
Fonts are downloaded individually from CDN on first setup.
- **CDN base**: `https://z-cdn.chatglm.cn/office-skill/fonts/`
- **Font list**: `env_setup/font_list.txt` (78 fonts, one relative path per line)
- **Marker file**: `.office-skill-fonts-installed` in the user font directory prevents re-download
- Special characters in filenames (e.g., `[`, `]`) are URL-encoded automatically by the setup script
The setup scripts read `font_list.txt`, check which fonts are already installed, and download only the missing ones. Each font is saved flat (filename only) to the user font directory.
### CDN Directory Structure (78 fonts)
| Directory | Count | Description |
|-----------|-------|-------------|
| `truetype/lxgw-wenkai/` | 6 | LXGW WenKai — Chinese handwriting style |
| `truetype/noto-serif-sc/` | 9 | Noto Serif SC — Chinese serif (variable + 8 static weights) |
| `chinese/` | 14 | Noto Sans SC, Sarasa Mono SC, Liberation fallbacks |
| `dejavu/` | 8 | DejaVu Sans/Serif/Mono — Latin/symbol fallback |
| `emoji/` | 1 | Noto Color Emoji |
| `english/` | 12 | Tinos, Carlito, Calibri |
| `freefont/` | 12 | FreeSans/FreeSerif/FreeMono — open-source fallback |
| `liberation/` | 12 | Liberation Sans/Serif/Mono — MS-metric-compatible |
| `libreoffice/` | 1 | OpenSymbol |
| `noto/` | 1 | Noto Color Emoji (duplicate) |
| `wqy/` | 1 | WenQuanYi Zen Hei — CJK fallback |
| *(root)* | 1 | Japanese Gothic |
### Install Targets by Platform
| Platform | User Font Directory |
|----------|-------------------|
| macOS | `~/Library/Fonts/` |
| Linux | `~/.local/share/fonts/` (then run `fc-cache -f`) |
| Windows | `%LOCALAPPDATA%\Microsoft\Windows\Fonts` (per-user, no admin) |
### Manual Font Installation
If CDN is unreachable, download fonts manually. Example:
```bash
# Download a single font (use static-weight files, NOT variable fonts)
curl -fSLO "https://z-cdn.chatglm.cn/office-skill/fonts/chinese/NotoSansSC-Regular.ttf"
# Copy to user font dir
cp "NotoSansSC-Regular.ttf" ~/Library/Fonts/ # macOS
```
Or download all fonts listed in `font_list.txt`:
```bash
while read f; do
encoded=$(echo "$f" | sed 's/\[/%5B/g; s/\]/%5D/g')
curl -fSLO "https://z-cdn.chatglm.cn/office-skill/fonts/$encoded"
done < env_setup/font_list.txt
```
### Post-Install Variable
After font installation, `FONT_DIR` points to the user font directory:
| Platform | FONT_DIR |
|----------|---------------|
| macOS | `~/Library/Fonts` |
| Linux | `~/.local/share/fonts` |
| Windows | `%LOCALAPPDATA%\Microsoft\Windows\Fonts` |
---
## China Network Fallback
If default sources are unreachable, use China mirrors:
### pip (Tsinghua mirror)
```bash
python3 -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple \
--trusted-host pypi.tuna.tsinghua.edu.cn \
pikepdf pdfplumber pypdf reportlab PyMuPDF
```
### npm (npmmirror)
```bash
npm install -g playwright@1.50.0 --registry https://registry.npmmirror.com
# Pinned to 1.50.0 — npmmirror has Chromium rev 1155 for this version
```
### Playwright browser download (npmmirror)
```bash
PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright/ npx playwright install chromium
```
### Windows (PowerShell) China mirrors
```powershell
python -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn pikepdf pdfplumber pypdf reportlab PyMuPDF
npm install -g playwright@1.50.0 --registry https://registry.npmmirror.com
# Pinned to 1.50.0 — npmmirror has Chromium rev 1155 for this version
$env:PLAYWRIGHT_DOWNLOAD_HOST = "https://npmmirror.com/mirrors/playwright/"
npx playwright install chromium
```
### Installer downloads (China)
| Software | China Mirror |
|----------|-------------|
| Node.js | https://npmmirror.com/mirrors/node/ |
| Python | https://npmmirror.com/mirrors/python/ |
+350
View File
@@ -0,0 +1,350 @@
#!/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 "============================================"
+326
View File
@@ -0,0 +1,326 @@
#Requires -Version 5.1
<#
.SYNOPSIS
PDF Skill — Environment Setup for Windows (Win10/Win11)
.DESCRIPTION
Detects platform, checks and installs all dependencies for the PDF skill.
Supports China mirror fallback for pip/npm/playwright.
#>
param(
[switch]$UseChinaMirror
)
$ErrorActionPreference = "Continue"
function Write-Ok { param($msg) Write-Host " [OK] $msg" -ForegroundColor Green }
function Write-Fail { param($msg) Write-Host " [FAIL] $msg" -ForegroundColor Red }
function Write-Warn { param($msg) Write-Host " [WARN] $msg" -ForegroundColor Yellow }
function Write-Info { param($msg) Write-Host " [->] $msg" -ForegroundColor Cyan }
# ── Resolve PDF_SKILL_DIR ──
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$PDF_SKILL_DIR = Split-Path -Parent $ScriptDir
$env:PDF_SKILL_DIR = $PDF_SKILL_DIR
Write-Host "============================================"
Write-Host " PDF Skill - Environment Setup"
Write-Host " (Windows)"
Write-Host "============================================"
Write-Host ""
# ── Step 1: Platform Detection ──
$WinVer = [System.Environment]::OSVersion.Version
$WinName = if ($WinVer.Build -ge 22000) { "Windows 11" } elseif ($WinVer.Build -ge 10240) { "Windows 10" } else { "Windows (older)" }
Write-Host "Platform: $WinName (Build $($WinVer.Build)), $([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)"
Write-Host "PDF_SKILL_DIR=$PDF_SKILL_DIR"
Write-Host ""
# ── China mirror detection ──
$PipMirrorArgs = @()
$NpmMirrorArgs = @()
$PlaywrightHost = ""
if ($UseChinaMirror) {
$global:UseCN = $true
} else {
try {
$null = Invoke-WebRequest -Uri "https://pypi.org" -TimeoutSec 3 -UseBasicParsing -ErrorAction Stop
$global:UseCN = $false
} catch {
Write-Warn "pypi.org unreachable - enabling China mirrors"
$global:UseCN = $true
}
}
if ($global:UseCN) {
$PipMirrorArgs = @("-i", "https://pypi.tuna.tsinghua.edu.cn/simple", "--trusted-host", "pypi.tuna.tsinghua.edu.cn")
$NpmMirrorArgs = @("--registry", "https://registry.npmmirror.com")
$PlaywrightHost = "https://npmmirror.com/mirrors/playwright/"
Write-Info "China mirrors enabled (pip: tuna, npm: npmmirror, playwright: npmmirror)"
Write-Host ""
}
$Errors = 0
# ── Step 2a: Python 3 ──
Write-Host "--- [1/9] Python 3 ---"
$PyCmd = $null
foreach ($cmd in @("python3", "python", "py")) {
try {
$ver = & $cmd --version 2>&1
if ($ver -match "Python 3") {
$PyCmd = $cmd
Write-Ok "$cmd ($ver)"
break
}
} catch {}
}
if (-not $PyCmd) {
Write-Fail "Python 3 not found"
Write-Info "Install option 1: winget install Python.Python.3.11"
Write-Info "Install option 2: https://www.python.org/downloads/"
Write-Info "Install option 3: choco install python3"
if ($global:UseCN) {
Write-Info "China alt: https://npmmirror.com/mirrors/python/"
}
$Errors++
}
Write-Host ""
# ── Step 2b: pip ──
Write-Host "--- [2/9] pip ---"
if ($PyCmd) {
try {
$pipVer = & $PyCmd -m pip --version 2>&1
if ($pipVer -match "pip") {
Write-Ok "pip ($pipVer)"
} else { throw "no pip" }
} catch {
Write-Fail "pip not found"
Write-Info "Install: $PyCmd -m ensurepip --upgrade"
$Errors++
}
} else {
Write-Fail "pip - skipped (Python not found)"
$Errors++
}
Write-Host ""
# ── Step 2c: Python packages ──
Write-Host "--- [3/9] Python Packages (pikepdf, pdfplumber, pypdf, reportlab, PyMuPDF) ---"
$PyPkgs = @(
@{ Module = "pikepdf"; Package = "pikepdf" },
@{ Module = "pdfplumber"; Package = "pdfplumber" },
@{ Module = "pypdf"; Package = "pypdf" },
@{ Module = "reportlab"; Package = "reportlab" },
@{ Module = "fitz"; Package = "PyMuPDF" }
)
$MissingPy = @()
if ($PyCmd) {
foreach ($pkg in $PyPkgs) {
try {
$result = & $PyCmd -c "import $($pkg.Module); print(getattr($($pkg.Module), '__version__', getattr($($pkg.Module), 'version', 'ok')))" 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Ok "$($pkg.Package) ($result)"
} else { throw "not installed" }
} catch {
Write-Fail "$($pkg.Package) not installed"
$MissingPy += $pkg.Package
}
}
if ($MissingPy.Count -gt 0) {
Write-Info "Installing: $($MissingPy -join ', ')"
$installArgs = @("-m", "pip", "install") + $PipMirrorArgs + $MissingPy
try {
& $PyCmd @installArgs 2>&1 | Out-Null
Write-Ok "Installed: $($MissingPy -join ', ')"
} catch {
Write-Fail "pip install failed. Try: $PyCmd -m pip install $($PipMirrorArgs -join ' ') $($MissingPy -join ' ')"
$Errors++
}
}
} else {
Write-Fail "Python packages - skipped (Python not found)"
}
Write-Host ""
# ── Step 2d: Node.js + npm ──
Write-Host "--- [4/9] Node.js + npm ---"
try {
$nodeVer = & node --version 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Ok "node ($nodeVer)"
} else { throw "no node" }
} catch {
Write-Fail "node not found"
Write-Info "Install option 1: winget install OpenJS.NodeJS.LTS"
Write-Info "Install option 2: https://nodejs.org/"
Write-Info "Install option 3: choco install nodejs-lts"
if ($global:UseCN) {
Write-Info "China alt: https://npmmirror.com/mirrors/node/"
}
$Errors++
}
try {
$npmVer = & npm --version 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Ok "npm ($npmVer)"
} else { throw "no npm" }
} catch {
Write-Fail "npm not found (installed with Node.js)"
$Errors++
}
Write-Host ""
# ── Step 2e: Playwright + Chromium ──
Write-Host "--- [5/9] Playwright + Chromium ---"
try {
$null = & node -e "require('playwright')" 2>&1
if ($LASTEXITCODE -eq 0) {
$pwVer = & node -e "console.log(require('playwright/package.json').version)" 2>&1
Write-Ok "playwright ($pwVer)"
} else { throw "no pw" }
} catch {
Write-Fail "playwright not installed"
if ($global:UseCN) {
Write-Info "Install: npm install -g playwright@1.50.0 $($NpmMirrorArgs -join ' ')"
} else {
Write-Info "Install: npm install -g playwright@1.50.0"
}
$Errors++
}
$PwCache = Join-Path $env:LOCALAPPDATA "ms-playwright"
if (Test-Path "$PwCache\chromium-*") {
Write-Ok "chromium installed"
} else {
Write-Fail "chromium not installed"
if ($global:UseCN) {
Write-Info "Install: `$env:PLAYWRIGHT_DOWNLOAD_HOST='$PlaywrightHost'; npx playwright install chromium"
} else {
Write-Info "Install: npx playwright install chromium"
}
$Errors++
}
Write-Host ""
# ── Step 2f: Tectonic (optional) ──
Write-Host "--- [6/9] Tectonic (optional, LaTeX/Academic PDFs) ---"
try {
$tecVer = & tectonic --version 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Ok "tectonic ($tecVer)"
} else { throw "no tectonic" }
} catch {
Write-Warn "tectonic not installed (needed only for LaTeX/academic PDFs)"
Write-Info "Install option 1: scoop install tectonic"
Write-Info "Install option 2: choco install tectonic"
}
Write-Host ""
# ── Step 2g: LibreOffice (optional) ──
Write-Host "--- [7/9] LibreOffice (optional, Office->PDF conversion) ---"
$SofficePath = "C:\Program Files\LibreOffice\program\soffice.exe"
if (Test-Path $SofficePath) {
Write-Ok "libreoffice found"
} elseif (Get-Command soffice -ErrorAction SilentlyContinue) {
Write-Ok "libreoffice (in PATH)"
} else {
Write-Warn "libreoffice not installed (needed only for .docx/.xlsx -> PDF conversion)"
Write-Info "Install: winget install TheDocumentFoundation.LibreOffice"
Write-Info " or: https://www.libreoffice.org/download/"
}
Write-Host ""
# ── Step 2h: Font Installation (from CDN) ──
Write-Host "--- [8/9] Font Installation ---"
$FontCdnBase = "https://z-cdn.chatglm.cn/office-skill/fonts"
$FontList = Join-Path $ScriptDir "font_list.txt"
$UserFontDir = Join-Path $env:LOCALAPPDATA "Microsoft\Windows\Fonts"
if (-not (Test-Path $UserFontDir)) { New-Item -ItemType Directory -Path $UserFontDir -Force | Out-Null }
$Marker = Join-Path $UserFontDir ".office-skill-fonts-installed"
if (Test-Path $Marker) {
Write-Ok "Fonts already installed (marker found). To re-install, delete $Marker"
} else {
if (-not (Test-Path $FontList)) {
Write-Fail "Font list not found: $FontList"
$Errors++
} else {
$lines = Get-Content $FontList | Where-Object { $_.Trim() -ne "" }
$Total = $lines.Count
$Installed = 0; $Skipped = 0; $Failed = 0
Write-Info "Downloading $Total fonts from CDN..."
foreach ($relPath in $lines) {
$fname = Split-Path $relPath -Leaf
$dest = Join-Path $UserFontDir $fname
if (Test-Path $dest) {
$Skipped++
continue
}
$encoded = $relPath -replace '\[','%5B' -replace '\]','%5D' -replace ' ','%20'
$url = "$FontCdnBase/$encoded"
try {
Invoke-WebRequest -Uri $url -OutFile $dest -TimeoutSec 30 -ErrorAction Stop
$regPath = "HKCU:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
$null = New-ItemProperty -Path $regPath -Name $fname -Value $dest -PropertyType String -Force -ErrorAction SilentlyContinue
$Installed++
} catch {
Write-Warn "Failed to download: $relPath"
$Failed++
Remove-Item $dest -Force -ErrorAction SilentlyContinue
}
}
if ($Failed -eq 0) {
New-Item -ItemType File -Path $Marker -Force | Out-Null
Write-Ok "Fonts: $Installed newly installed, $Skipped already present (target: $UserFontDir)"
} else {
Write-Warn "Fonts: $Installed installed, $Skipped skipped, $Failed failed (marker not written, will retry next run)"
$Errors++
}
}
}
# Set PDF_FONTS_DIR to user font directory (fonts are now installed there)
$PDF_FONTS_DIR = $UserFontDir
$env:PDF_FONTS_DIR = $PDF_FONTS_DIR
Write-Host ""
# ── Step 2i: CJK Font Verification ──
Write-Host "--- [9/9] CJK Font Verification ---"
$CjkFound = $false
$FontsDir = Join-Path $env:WINDIR "Fonts"
$UserFontDir2 = Join-Path $env:LOCALAPPDATA "Microsoft\Windows\Fonts"
$CjkFonts = @("NotoSansSC[wght].ttf", "NotoSansSC[wght].ttf", "NotoSansSC[wght].ttf", "NotoSerifSC-Regular.ttf")
foreach ($f in $CjkFonts) {
if ((Test-Path (Join-Path $FontsDir $f)) -or (Test-Path (Join-Path $UserFontDir2 $f))) {
Write-Ok "CJK font found: $f"
$CjkFound = $true
break
}
}
if (-not $CjkFound) {
Write-Warn "No CJK font verified yet - fonts were just installed, restart may be needed"
}
Write-Host ""
# ── Summary ──
Write-Host "============================================"
if ($Errors -eq 0) {
Write-Host " All dependencies OK."
} else {
Write-Host " $Errors issue(s) found. Fix them above."
}
Write-Host " PDF_SKILL_DIR=$PDF_SKILL_DIR"
Write-Host "============================================"