|
| 1 | +#Requires -Version 5.1 |
| 2 | + |
| 3 | +<# |
| 4 | +.SYNOPSIS |
| 5 | + Cliente PowerShell para a API Jurisprudencias.ai com cache local. |
| 6 | +.DESCRIPTION |
| 7 | + Fornece funcoes para buscar decisoes judiciais, consultar tribunais |
| 8 | + e resolver processos, com cache JSON para evitar o rate limit de 5 buscas/dia. |
| 9 | +#> |
| 10 | + |
| 11 | +$Script:CacheDir = if ($env:JURISPRUDENCIAS_CACHE_DIR) { $env:JURISPRUDENCIAS_CACHE_DIR } else { "$env:USERPROFILE\.jurisprudencias\cache" } |
| 12 | +$Script:CacheTTLHours = if ($env:JURISPRUDENCIAS_CACHE_TTL) { [int]$env:JURISPRUDENCIAS_CACHE_TTL } else { 24 } |
| 13 | +$Script:ApiBaseUrl = "https://jurisprudencias.ai/api/v1" |
| 14 | + |
| 15 | +$Script:Token = $env:JURISPRUDENCIAS_API_TOKEN |
| 16 | +if (-not $Script:Token) { |
| 17 | + Write-Warning "Variavel JURISPRUDENCIAS_API_TOKEN nao definida" |
| 18 | + Write-Warning "Configure com: `$env:JURISPRUDENCIAS_API_TOKEN = 'jur_...'" |
| 19 | +} |
| 20 | + |
| 21 | +function _remove-diacritics($s) { |
| 22 | + $normalized = $s.Normalize([System.text.NormalizationForm]::FormD) |
| 23 | + return [Text.RegularExpressions.Regex]::Replace($normalized, '\p{M}', '') |
| 24 | +} |
| 25 | + |
| 26 | +function _ensure-cache-dir { |
| 27 | + if (-not (Test-Path $Script:CacheDir)) { |
| 28 | + $null = New-Item -ItemType Directory -Path $Script:CacheDir -Force |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +function _cache-path($key) { |
| 33 | + $bytes = [text.encoding]::UTF8.GetBytes($key) |
| 34 | + $hashAlgo = [Security.Cryptography.SHA256]::Create() |
| 35 | + $hashBytes = $hashAlgo.ComputeHash($bytes) |
| 36 | + $hash = [System.BitConverter]::ToString($hashBytes) -replace '-', '' |
| 37 | + return Join-Path $Script:CacheDir "$hash.json" |
| 38 | +} |
| 39 | + |
| 40 | +function _cache-get($key) { |
| 41 | + _ensure-cache-dir |
| 42 | + $path = _cache-path $key |
| 43 | + if (-not (Test-Path $path)) { return $null } |
| 44 | + try { |
| 45 | + $o = Get-Content -Raw -Encoding UTF8 -LiteralPath $path | ConvertFrom-Json |
| 46 | + $age = [datetime]::Now - [datetime]::Parse($o._cached_at) |
| 47 | + if ($age.TotalHours -gt $Script:CacheTTLHours) { |
| 48 | + Remove-Item -LiteralPath $path -Force |
| 49 | + return $null |
| 50 | + } |
| 51 | + return $o.data |
| 52 | + } catch { return $null } |
| 53 | +} |
| 54 | + |
| 55 | +function _cache-set($key, $data) { |
| 56 | + _ensure-cache-dir |
| 57 | + $path = _cache-path $key |
| 58 | + $o = [PSCustomObject]@{ _cached_at = [datetime]::Now.ToString('o'); data = $data } |
| 59 | + $o | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $path -Encoding UTF8 |
| 60 | +} |
| 61 | + |
| 62 | +function _api-get($url) { |
| 63 | + $token = $Script:Token |
| 64 | + if (-not $token) { throw "Token nao disponivel. Defina JURISPRUDENCIAS_API_TOKEN" } |
| 65 | + try { |
| 66 | + $output = & "curl.exe" -s -w "%{http_code}" -H "Authorization: Bearer $token" $url 2>&1 |
| 67 | + $combined = $output -join "`n" |
| 68 | + $code = $combined.Substring($combined.Length - 3) |
| 69 | + $body = $combined.Substring(0, $combined.Length - 3) |
| 70 | + if ($code -eq "429") { |
| 71 | + Write-Warning "Rate limit excedido (429). Use cache ou aguarde reset a meia-noite." |
| 72 | + return $null |
| 73 | + } |
| 74 | + if ($code -eq "404") { return $null } |
| 75 | + if ($code -ne "200") { |
| 76 | + Write-Warning "HTTP $code -- $url" |
| 77 | + return $null |
| 78 | + } |
| 79 | + $trimmed = $body.Trim() |
| 80 | + if (-not $trimmed) { return @() } |
| 81 | + $parsed = $trimmed | ConvertFrom-Json |
| 82 | + if ($parsed.PSObject.Properties.Name -contains "data") { return $parsed.data } |
| 83 | + return $parsed |
| 84 | + } catch { |
| 85 | + Write-Warning "Erro na requisicao: $_" |
| 86 | + return $null |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +function Get-JurCourts { |
| 91 | + $url = "$($Script:ApiBaseUrl)/courts" |
| 92 | + $data = _api-get $url |
| 93 | + if (-not $data) { return } |
| 94 | + $data | ForEach-Object { |
| 95 | + [PSCustomObject]@{ Slug = $_.id; Nome = $_.name; Decisoes = $_.decisions_count } |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +function Search-JurDecisions { |
| 100 | + param( |
| 101 | + [Parameter(Mandatory)] [string] $Court, |
| 102 | + [Parameter(Mandatory)] [string] $Query, |
| 103 | + [int] $Page = 0, |
| 104 | + [string] $PubFrom, |
| 105 | + [string] $PubTo, |
| 106 | + [string] $TrialFrom, |
| 107 | + [string] $TrialTo, |
| 108 | + [switch] $Force, |
| 109 | + [switch] $NoCache |
| 110 | + ) |
| 111 | + $eq = [uri]::EscapeDataString($Query) |
| 112 | + $url = "{0}/courts/{1}/decisions?q={2}" -f $Script:ApiBaseUrl, $Court, $eq |
| 113 | + $url = "{0}{1}page={2}" -f $url, "&", $Page |
| 114 | + if ($PubFrom) { $url = "{0}{1}pub_from={2}" -f $url, "&", $PubFrom } |
| 115 | + if ($PubTo) { $url = "{0}{1}pub_to={2}" -f $url, "&", $PubTo } |
| 116 | + if ($TrialFrom) { $url = "{0}{1}trial_from={2}" -f $url, "&", $TrialFrom } |
| 117 | + if ($TrialTo) { $url = "{0}{1}trial_to={2}" -f $url, "&", $TrialTo } |
| 118 | + |
| 119 | + $ck = "{0}|{1}|p={2}|pub={3}-{4}|tri={5}-{6}" -f $Court, $Query, $Page, $PubFrom, $PubTo, $TrialFrom, $TrialTo |
| 120 | + |
| 121 | + if (-not $Force -and -not $NoCache) { |
| 122 | + $cached = _cache-get $ck |
| 123 | + if ($cached) { Write-Host "[cache]" -NoNewline -ForegroundColor DarkGray; return $cached } |
| 124 | + } |
| 125 | + $data = _api-get $url |
| 126 | + if ($null -eq $data) { return } |
| 127 | + if (-not $NoCache -and $data) { _cache-set $ck $data } |
| 128 | + return $data |
| 129 | +} |
| 130 | + |
| 131 | +function Get-JurDecision { |
| 132 | + param( |
| 133 | + [Parameter(Mandatory)] [string] $Court, |
| 134 | + [Parameter(Mandatory)] [string] $Number |
| 135 | + ) |
| 136 | + $en = [uri]::EscapeDataString($Number) |
| 137 | + $url = "{0}/courts/{1}/decisions/lookup?n={2}" -f $Script:ApiBaseUrl, $Court, $en |
| 138 | + $data = _api-get $url |
| 139 | + if (-not $data) { return } |
| 140 | + [PSCustomObject]@{ |
| 141 | + Processo = $data.process_number |
| 142 | + Ementa = $data.summary |
| 143 | + URL = $data.url |
| 144 | + Tribunal = $data.court |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +function Clear-JurCache { |
| 149 | + param([int] $OlderThanHours = 0) |
| 150 | + _ensure-cache-dir |
| 151 | + $files = Get-ChildItem "$($Script:CacheDir)\*.json" -ErrorAction SilentlyContinue |
| 152 | + if (-not $files) { Write-Host "Cache vazio."; return } |
| 153 | + $removed = 0 |
| 154 | + $files | ForEach-Object { |
| 155 | + if ($OlderThanHours -gt 0) { |
| 156 | + $age = ([datetime]::Now - $_.LastWriteTime).TotalHours |
| 157 | + if ($age -ge $OlderThanHours) { Remove-Item $_.FullName -Force; $removed++ } |
| 158 | + } else { Remove-Item $_.FullName -Force; $removed++ } |
| 159 | + } |
| 160 | + Write-Host "Cache limpo. Removidos $removed arquivos." -ForegroundColor Green |
| 161 | +} |
| 162 | + |
| 163 | +function Get-JurCacheStatus { |
| 164 | + _ensure-cache-dir |
| 165 | + $files = Get-ChildItem "$($Script:CacheDir)\*.json" -ErrorAction SilentlyContinue |
| 166 | + if (-not $files) { Write-Host "Cache vazio."; return } |
| 167 | + $total = @($files).Count |
| 168 | + $size = ($files | Measure-Object Length -Sum).Sum |
| 169 | + $now = [datetime]::Now |
| 170 | + $expiredCount = 0 |
| 171 | + $files | ForEach-Object { |
| 172 | + $age = ($now - $_.LastWriteTime).TotalHours |
| 173 | + if ($age -gt $Script:CacheTTLHours) { $expiredCount++ } |
| 174 | + $shortName = $_.Name.Substring(0, [Math]::Min(16, $_.Name.Length)) + ".." |
| 175 | + [PSCustomObject]@{ |
| 176 | + Arquivo = $shortName |
| 177 | + IdadeH = [math]::Round($age, 1) |
| 178 | + Status = if ($age -gt $Script:CacheTTLHours) { "expirado" } else { "valido" } |
| 179 | + Tamanho = "{0:N1}KB" -f ($_.Length / 1KB) |
| 180 | + } |
| 181 | + } | Sort-Object IdadeH -Descending | Format-Table -AutoSize |
| 182 | + $msg = "Total: {0} itens, {1:N1}KB, {2} expirados (TTL={3}h)" -f $total, ($size/1KB), $expiredCount, $Script:CacheTTLHours |
| 183 | + Write-Host $msg -ForegroundColor Cyan |
| 184 | +} |
| 185 | + |
| 186 | +function Search-JurCache { |
| 187 | + param( |
| 188 | + [Parameter(Mandatory)] [string] $Term, |
| 189 | + [string] $Court, |
| 190 | + [switch] $SimpleOutput |
| 191 | + ) |
| 192 | + _ensure-cache-dir |
| 193 | + $files = Get-ChildItem "$($Script:CacheDir)\*.json" -ErrorAction SilentlyContinue |
| 194 | + if (-not $files) { Write-Host "Cache vazio. Nada para buscar."; return } |
| 195 | + |
| 196 | + $results = [System.Collections.ArrayList]@() |
| 197 | + $termNorm = _remove-diacritics $Term.ToLower() |
| 198 | + |
| 199 | + foreach ($f in $files) { |
| 200 | + try { |
| 201 | + $o = Get-Content -Raw -Encoding UTF8 -LiteralPath $f.FullName | ConvertFrom-Json |
| 202 | + $entries = $o.data |
| 203 | + if (-not $entries) { continue } |
| 204 | + if ($entries -isnot [array]) { $entries = @($entries) } |
| 205 | + foreach ($entry in $entries) { |
| 206 | + $courtMatch = (-not $Court) -or ($entry.court -eq $Court) -or ($entry.court_slug -eq $Court) |
| 207 | + if (-not $courtMatch) { continue } |
| 208 | + |
| 209 | + $haystackParts = @( |
| 210 | + $entry.excerpt; $entry.process_number; $entry.summary |
| 211 | + $entry.process_type; $entry.rapporteur |
| 212 | + ) | Where-Object { $_ } | ForEach-Object { _remove-diacritics $_.ToLower() } |
| 213 | + $haystack = $haystackParts -join " " |
| 214 | + if ($haystack -match [regex]::Escape($termNorm)) { |
| 215 | + $null = $results.Add([PSCustomObject]@{ |
| 216 | + Processo = $entry.process_number |
| 217 | + Tribunal = $entry.court |
| 218 | + DataPublicacao = $entry.publication_date |
| 219 | + DataJulgamento = $entry.trial_date |
| 220 | + Relator = $entry.rapporteur |
| 221 | + Ementa = if ($entry.excerpt) { ($entry.excerpt -replace '\s+', ' ').Substring(0, [Math]::Min(200, $entry.excerpt.Length)) + "..." } else { "" } |
| 222 | + URL = $entry.url |
| 223 | + }) |
| 224 | + } |
| 225 | + } |
| 226 | + } catch { continue } |
| 227 | + } |
| 228 | + if ($results.Count -eq 0) { Write-Host "Nenhum resultado offline para '$Term'."; return } |
| 229 | + if ($SimpleOutput) { |
| 230 | + $results | Select-Object Processo, DataPublicacao | Format-Table -AutoSize |
| 231 | + } else { |
| 232 | + $results | Format-Table -Property Processo, Tribunal, DataPublicacao, Ementa -AutoSize -Wrap |
| 233 | + } |
| 234 | + Write-Host "($($results.Count) resultados offline)" -ForegroundColor Cyan |
| 235 | +} |
| 236 | + |
| 237 | +function Export-JurDocs { |
| 238 | + param( |
| 239 | + [string] $OutputPath = ".\jurisprudencias_offline.html", |
| 240 | + [string] $Court, |
| 241 | + [switch] $Open |
| 242 | + ) |
| 243 | + _ensure-cache-dir |
| 244 | + $files = Get-ChildItem "$($Script:CacheDir)\*.json" -ErrorAction SilentlyContinue |
| 245 | + if (-not $files) { Write-Host "Cache vazio."; return } |
| 246 | + |
| 247 | + $allDecisions = [System.Collections.ArrayList]@() |
| 248 | + foreach ($f in $files) { |
| 249 | + try { |
| 250 | + $o = Get-Content -Raw -Encoding UTF8 -LiteralPath $f.FullName | ConvertFrom-Json |
| 251 | + $entries = $o.data |
| 252 | + if (-not $entries -or ($entries -isnot [array])) { continue } |
| 253 | + foreach ($entry in $entries) { |
| 254 | + if ($Court -and $entry.court -ne $Court) { continue } |
| 255 | + $null = $allDecisions.Add($entry) |
| 256 | + } |
| 257 | + } catch { continue } |
| 258 | + } |
| 259 | + if ($allDecisions.Count -eq 0) { Write-Host "Nenhuma decisao no cache para exportar."; return } |
| 260 | + |
| 261 | + $count = $allDecisions.Count |
| 262 | + $rows = $allDecisions | Sort-Object publication_date -Descending | ForEach-Object { |
| 263 | + $num = $_.process_number; $date = $_.publication_date; $trial = $_.trial_date |
| 264 | + $court = $_.court; $rel = $_.rapporteur; $body = $_.excerpt; $url = $_.url |
| 265 | + $title = if ($num) { $num } else { "(sem numero)" } |
| 266 | + $meta = @() |
| 267 | + if ($court) { $meta += "Tribunal: $court" } |
| 268 | + if ($date) { $meta += "Publicacao: $date" } |
| 269 | + if ($trial) { $meta += "Julgamento: $trial" } |
| 270 | + if ($rel) { $meta += "Relator: $rel" } |
| 271 | + $metaStr = $meta -join " · " |
| 272 | + $bodyHtml = if ($body) { |
| 273 | + $escaped = [System.Net.WebUtility]::HtmlEncode(($body -replace '\s+', ' ').Trim()) |
| 274 | + "<p>$escaped</p>" |
| 275 | + } else { "" } |
| 276 | + $urlHtml = if ($url) { "<a href=`"$([System.Net.WebUtility]::HtmlEncode($url))`" target=`"_blank`">$([System.Net.WebUtility]::HtmlEncode($url))</a>" } else { "" } |
| 277 | +@" |
| 278 | +<div class="card"><div class="card-title"><a href="#$([System.Net.WebUtility]::HtmlEncode($num))">$([System.Net.WebUtility]::HtmlEncode($title))</a></div><div class="card-meta">$metaStr</div>$bodyHtml<div class="card-url">$urlHtml</div></div> |
| 279 | +"@ |
| 280 | + } |
| 281 | + |
| 282 | + $html = @" |
| 283 | +<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"> |
| 284 | +<title>Jurisprudencias - Offline</title> |
| 285 | +<style>*{box-sizing:border-box;margin:0;padding:0} |
| 286 | +body{font-family:'Segoe UI',system-ui,sans-serif;background:#f5f5f5;color:#222;line-height:1.6;padding:2rem} |
| 287 | +.container{max-width:960px;margin:0 auto} |
| 288 | +h1{font-size:1.75rem;font-weight:600;margin-bottom:0.25rem} |
| 289 | +.subtitle{color:#666;font-size:0.9rem;margin-bottom:2rem} |
| 290 | +.stats{display:flex;gap:1rem;margin-bottom:2rem;flex-wrap:wrap} |
| 291 | +.stat{background:#fff;border-radius:8px;padding:1rem 1.25rem;box-shadow:0 1px 3px rgba(0,0,0,0.08);flex:1;min-width:120px} |
| 292 | +.stat-num{font-size:1.5rem;font-weight:700;color:#1a56db} |
| 293 | +.stat-label{font-size:0.8rem;color:#666;text-transform:uppercase;letter-spacing:0.05em} |
| 294 | +.card{background:#fff;border-radius:8px;padding:1.25rem;margin-bottom:1rem;box-shadow:0 1px 3px rgba(0,0,0,0.08)} |
| 295 | +.card-title{font-weight:600;margin-bottom:0.35rem} |
| 296 | +.card-title a{color:#1a56db;text-decoration:none} |
| 297 | +.card-meta{font-size:0.82rem;color:#888;margin-bottom:0.75rem} |
| 298 | +.card p{font-size:0.92rem;color:#444;text-align:justify} |
| 299 | +.card-url{margin-top:0.5rem;font-size:0.82rem;word-break:break-all} |
| 300 | +.search-box{margin-bottom:1.5rem} |
| 301 | +.search-box input{width:100%;padding:0.65rem 1rem;border:1px solid #d0d0d0;border-radius:8px;font-size:1rem} |
| 302 | +.search-box input:focus{outline:none;border-color:#1a56db;box-shadow:0 0 0 3px rgba(26,86,219,0.15)} |
| 303 | +.footer{text-align:center;color:#999;font-size:0.8rem;margin-top:2rem;padding-top:1rem;border-top:1px solid #e0e0e0} |
| 304 | +</style></head><body><div class="container"> |
| 305 | +<h1>Jurisprudencias — Documentacao Offline</h1> |
| 306 | +<p class="subtitle">Gerado em $(Get-Date -Format "dd/MM/yyyy HH:mm") · Cache local</p> |
| 307 | +<div class="stats"> |
| 308 | +<div class="stat"><div class="stat-num">$count</div><div class="stat-label">Decisoes</div></div> |
| 309 | +<div class="stat"><div class="stat-num">$($(($allDecisions | ForEach-Object { $_.court } | Where-Object { $_ } | Select-Object -Unique).Count))</div><div class="stat-label">Tribunais</div></div> |
| 310 | +</div> |
| 311 | +<div class="search-box"><input type="text" id="filter" placeholder="Filtrar..." oninput="filterCards()"></div> |
| 312 | +<div id="cards">$rows</div> |
| 313 | +<div class="footer">Cache TTL: $($Script:CacheTTLHours)h</div> |
| 314 | +</div> |
| 315 | +<script>function filterCards(){var q=document.getElementById('filter').value.toLowerCase();document.querySelectorAll('.card').forEach(function(c){c.style.display=q===''||c.textContent.toLowerCase().includes(q)?'':'none'})}</script> |
| 316 | +</body></html> |
| 317 | +"@ |
| 318 | + |
| 319 | + $html | Set-Content -LiteralPath $OutputPath -Encoding UTF8 |
| 320 | + Write-Host "Documentacao gerada: $OutputPath ($count decisoes)" -ForegroundColor Green |
| 321 | + if ($Open) { Start-Process $OutputPath } |
| 322 | +} |
| 323 | + |
| 324 | +function Invoke-JurPreFetch { |
| 325 | + param( |
| 326 | + [Parameter(Mandatory)] [string] $Court, |
| 327 | + [string[]] $Terms = @("desapropriacao","precatorio","imissao na posse","indenizacao","utilidade publica","justa indenizacao","tombamento","direito de propriedade","posse","reintegracao de posse","usucapiao","bem publico","servidao administrativa","limitação administrativa"), |
| 328 | + [int] $MaxPages = 0 |
| 329 | + ) |
| 330 | + Write-Host "=== PRE-FETCH: $Court ===" -ForegroundColor Yellow |
| 331 | + $total = 0 |
| 332 | + foreach ($term in $Terms) { |
| 333 | + for ($p = 0; $p -le $MaxPages; $p++) { |
| 334 | + Write-Host "[$($Court)] '$term' pag.$p ... " -NoNewline |
| 335 | + try { |
| 336 | + $r = Search-JurDecisions -Court $Court -Query $term -Page $p -Force |
| 337 | + if ($r -and @($r).Count -gt 0) { Write-Host "$(@($r).Count) resultados" -ForegroundColor Green; $total += @($r).Count } |
| 338 | + else { Write-Host "0 resultados" -ForegroundColor DarkGray; break } |
| 339 | + } catch { Write-Host "ERRO: $_" -ForegroundColor Red; break } |
| 340 | + } |
| 341 | + } |
| 342 | + Write-Host "=== CONCLUIDO: $total decisoes cacheadas ===" -ForegroundColor Yellow |
| 343 | +} |
0 commit comments