Files
Z User e45b68bb8b 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
2026-10-27 22:50:50 +00:00

327 lines
11 KiB
PowerShell
Executable File

#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 "============================================"