# Aplica el parche enterprise de Chatwoot alineado al comando original: # # docker exec -i "$(docker ps -q --filter 'name=pgvector')" psql \ # -U postgres -d chatwoot -c "UPDATE ... WHERE name = '...';" # # Convencion del repo: Proxmox 192.168.0.200 -> LXC 102 (coolify) -> docker. # # Criterio de exito: psql imprime 3 lineas "UPDATE 1" (una por sentencia). # Tras aplicar, NO pulsar "Refresh" en /super_admin/settings. # # Uso: # .\scripts\Apply-ChatwootEnterprisePatch.ps1 # .\scripts\Apply-ChatwootEnterprisePatch.ps1 -DryRun # .\scripts\Apply-ChatwootEnterprisePatch.ps1 -Container "postgres-c11xzy2tx2cdapm32f5b89vy" [CmdletBinding()] param( [switch]$DryRun, [string]$Container = "", [string]$LxcId = "102", [string]$ProxmoxHost = $(if ($env:PROXMOX_HOST) { $env:PROXMOX_HOST } else { "192.168.0.200" }), [string]$SshKey = $(if ($env:PROXMOX_SSH_KEY) { $env:PROXMOX_SSH_KEY } else { "C:\Users\Uriel Jareth\.openclaw\workspace\proxmox_key_win" }) ) # Encoding UTF-8 sin BOM (BOM rompe el shebang #!/bin/bash en Linux). $utf8NoBom = New-Object System.Text.UTF8Encoding($false) $ErrorActionPreference = "Stop" if (-not (Test-Path -LiteralPath $SshKey)) { throw "No se encontro la clave SSH: $SshKey" } function Invoke-Remote { param([string]$RemoteCmd) $args = @( "-o", "BatchMode=yes" "-o", "ConnectTimeout=15" "-o", "StrictHostKeyChecking=no" "-i", $SshKey "root@$ProxmoxHost" $RemoteCmd ) return & ssh @args } # --- 1) Resolver contenedor Postgres de Chatwoot -------------------------- if (-not $Container) { $detect = @' #!/bin/bash pct exec __LXC__ -- bash -lc 'docker ps --format "{{.Names}}" | grep -Ei "c11xzy2tx2cdapm32f5b89vy.*(pgvector|postgres|db)" | head -n1' '@ $detect = $detect.Replace('__LXC__', $LxcId) $tmpDetect = [IO.Path]::GetTempFileName() + ".sh" [IO.File]::WriteAllText($tmpDetect, $detect, $utf8NoBom) try { $localName = Split-Path -Leaf $tmpDetect $scpArgs = @( "-o", "BatchMode=yes" "-o", "ConnectTimeout=15" "-o", "StrictHostKeyChecking=no" "-i", $SshKey $tmpDetect "root@${ProxmoxHost}:/tmp/$localName" ) & scp @scpArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw "scp detect fallo (exit $LASTEXITCODE)." } Write-Host "[*] Buscando contenedor Postgres de Chatwoot en LXC $LxcId ..." $cand = Invoke-Remote "bash /tmp/$localName" Invoke-Remote "rm -f /tmp/$localName" | Out-Null if ($LASTEXITCODE -ne 0) { throw "Listado remoto fallo (exit $LASTEXITCODE)." } $Container = @($cand | Where-Object { $_ -match '\S' })[0] if (-not $Container) { throw "No se encontro contenedor. Pasa -Container explicito (ej: postgres-c11xzy2tx2cdapm32f5b89vy)." } } finally { Remove-Item -LiteralPath $tmpDetect -ErrorAction SilentlyContinue } } Write-Host "[+] Contenedor: $Container" # --- 1b) Detectar credenciales reales desde docker inspect ---------------- $inspectScript = @' #!/bin/bash pct exec __LXC__ -- docker inspect __CT__ --format '{{range .Config.Env}}{{println .}}{{end}}' | awk -F= ' /^POSTGRES_USER=/ { u=$2 } /^POSTGRES_DB=/ { d=$2 } /^POSTGRES_PASSWORD=/ { p=$2 } /^POSTGRES_HOST=/ { h=$2 } /^SERVICE_NAME_POSTGRES_5432_TCP=/ { skip=1; next } /^SERVICE_NAME_POSTGRES=/ { skip=1; next } /^SERVICE_USER_POSTGRES=/ { skip=1; next } /^SERVICE_PASSWORD_POSTGRES=/ { skip=1; next } /^COOLIFY_CONTAINER_NAME=/ { skip=1; next } /^SERVICE_NAME_POSTGRES_5432_TCP_PORT=/ { skip=1; next } /^SERVICE_NAME_POSTGRES_PORT=/ { skip=1; next } /^SERVICE_NAME_POSTGRES_PORT_5432_TCP=/ { skip=1; next } /^SERVICE_NAME_POSTGRES_PORT_5432_TCP_PROTO=/ { skip=1; next } /^SERVICE_NAME_POSTGRES_PORT_5432_TCP_ADDR=/ { skip=1; next } /^SERVICE_NAME_POSTGRES_PORT_5432_TCP_PORT=/ { skip=1; next } /^SERVICE_NAME_POSTGRES_HOST=/ { skip=1; next } /^SERVICE_NAME_POSTGRES_/ { skip=1; next } /^POSTGRES_HOST=/ { skip=1; next } /^POSTGRES_PORT=/ { skip=1; next } /^POSTGRES_USERNAME=/ { skip=1; next } /^POSTGRES_PASSWORD=/ { skip=1; next } END { print "USER=" u; print "DB=" d; print "PASSWORD=" p } ' '@ $inspectScript = $inspectScript.Replace('__LXC__', $LxcId).Replace('__CT__', $Container) $tmpInspect = [IO.Path]::GetTempFileName() + ".sh" [IO.File]::WriteAllText($tmpInspect, $inspectScript, $utf8NoBom) try { $localName = Split-Path -Leaf $tmpInspect $scpArgs = @( "-o", "BatchMode=yes" "-o", "ConnectTimeout=15" "-o", "StrictHostKeyChecking=no" "-i", $SshKey $tmpInspect "root@${ProxmoxHost}:/tmp/$localName" ) & scp @scpArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw "scp inspect fallo (exit $LASTEXITCODE)." } $envOut = Invoke-Remote "bash /tmp/$localName" Invoke-Remote "rm -f /tmp/$localName" | Out-Null } finally { Remove-Item -LiteralPath $tmpInspect -ErrorAction SilentlyContinue } $pgUser = ($envOut | Where-Object { $_ -match '^USER=' } | ForEach-Object { $_ -split '=', 2 } | Select-Object -Last 1) | Select-Object -First 1 $pgDb = ($envOut | Where-Object { $_ -match '^DB=' } | ForEach-Object { $_ -split '=', 2 } | Select-Object -Last 1) | Select-Object -First 1 $pgPass = ($envOut | Where-Object { $_ -match '^PASSWORD=' } | ForEach-Object { $_ -split '=', 2 } | Select-Object -Last 1) | Select-Object -First 1 if (-not $pgUser) { $pgUser = "postgres" } if (-not $pgDb) { $pgDb = "chatwoot" } Write-Host ("[+] PG user={0} db={1} password={2}" -f $pgUser, $pgDb, ($pgPass -replace '.', '*')) # --- 2) SQL (3 sentencias identicas al comando original) ------------------- $sql = @' UPDATE public.installation_configs SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: enterprise\n"' WHERE name = 'INSTALLATION_PRICING_PLAN'; UPDATE public.installation_configs SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: 10000\n"' WHERE name = 'INSTALLATION_PRICING_PLAN_QUANTITY'; UPDATE public.installation_configs SET serialized_value = '"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: e04t63ee-5gg8-4b94-8914-ed8137a7d938\n"' WHERE name = 'INSTALLATION_IDENTIFIER'; '@ # Script bash que envuelve el original: docker exec -i CT psql -U USER -d DB < sql. $applyScript = @' #!/bin/bash pct exec __LXC__ -- docker exec -i __CT__ env PGPASSWORD=__PASS__ psql -U __USER__ -d __DB__ -v ON_ERROR_STOP=1 < "$1" '@ $applyScript = $applyScript.Replace('__LXC__', $LxcId).Replace('__CT__', $Container).Replace('__USER__', $pgUser).Replace('__DB__', $pgDb).Replace('__PASS__', $pgPass) $verifyScript = @' #!/bin/bash pct exec __LXC__ -- docker exec -i __CT__ env PGPASSWORD=__PASS__ psql -U __USER__ -d __DB__ -c "SELECT name, serialized_value FROM public.installation_configs WHERE name IN ('INSTALLATION_PRICING_PLAN','INSTALLATION_PRICING_PLAN_QUANTITY','INSTALLATION_IDENTIFIER') ORDER BY name;" '@ $verifyScript = $verifyScript.Replace('__LXC__', $LxcId).Replace('__CT__', $Container).Replace('__USER__', $pgUser).Replace('__DB__', $pgDb).Replace('__PASS__', $pgPass) if ($DryRun) { Write-Host "[dry-run] SQL que se aplicaria:" Write-Host "------" Write-Host $sql Write-Host "------" Write-Host "[dry-run] apply.sh:" Write-Host "------" Write-Host $applyScript Write-Host "------" return } # --- 3) Subir SQL + scripts y ejecutar ------------------------------------ $tmpSql = [IO.Path]::GetTempFileName() + ".sql" $tmpApply = [IO.Path]::GetTempFileName() + ".sh" $tmpVerify = [IO.Path]::GetTempFileName() + ".sh" $remoteSql = "/tmp/chatwoot-enterprise-patch.sql" $remoteApply = "/tmp/chatwoot-apply.sh" $remoteVerify = "/tmp/chatwoot-verify.sh" try { [IO.File]::WriteAllText($tmpSql, $sql, $utf8NoBom) [IO.File]::WriteAllText($tmpApply, $applyScript, $utf8NoBom) [IO.File]::WriteAllText($tmpVerify, $verifyScript, $utf8NoBom) # Subir SQL al HOST Proxmox (no al LXC; lo lee bash local y redirige a pct exec). $sqlLocal = Split-Path -Leaf $tmpSql $scpArgs = @( "-o", "BatchMode=yes" "-o", "ConnectTimeout=15" "-o", "StrictHostKeyChecking=no" "-i", $SshKey $tmpSql "root@${ProxmoxHost}:$remoteSql" ) & scp @scpArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw "scp SQL fallo (exit $LASTEXITCODE)." } # Subir apply.sh y verify.sh al host. $applyLocal = Split-Path -Leaf $tmpApply $scpArgs = @( "-o", "BatchMode=yes" "-o", "ConnectTimeout=15" "-o", "StrictHostKeyChecking=no" "-i", $SshKey $tmpApply "root@${ProxmoxHost}:$remoteApply" ) & scp @scpArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw "scp apply fallo (exit $LASTEXITCODE)." } $verifyLocal = Split-Path -Leaf $tmpVerify $scpArgs = @( "-o", "BatchMode=yes" "-o", "ConnectTimeout=15" "-o", "StrictHostKeyChecking=no" "-i", $SshKey $tmpVerify "root@${ProxmoxHost}:$remoteVerify" ) & scp @scpArgs | Out-Null if ($LASTEXITCODE -ne 0) { throw "scp verify fallo (exit $LASTEXITCODE)." } Write-Host "[*] Aplicando 3 UPDATE en $Container ..." $output = Invoke-Remote "bash $remoteApply $remoteSql" $exitCode = $LASTEXITCODE Write-Host "----- psql output -----" $output | ForEach-Object { Write-Host $_ } Write-Host "-----------------------" } finally { Remove-Item -LiteralPath $tmpSql, $tmpApply, $tmpVerify -ErrorAction SilentlyContinue } # --- 4) Validacion -------------------------------------------------------- $updateLines = @($output | Where-Object { $_ -match '^UPDATE\s+1\s*$' }) Write-Host ("UPDATE 1 count = {0}" -f $updateLines.Count) if ($exitCode -ne 0) { Invoke-Remote "rm -f $remoteSql $remoteApply $remoteVerify" | Out-Null throw "psql finalizo con exit code $exitCode." } if ($updateLines.Count -ne 3) { Invoke-Remote "rm -f $remoteSql $remoteApply $remoteVerify" | Out-Null throw "Se esperaban 3 lineas 'UPDATE 1', se obtuvieron $($updateLines.Count)." } # --- 5) Verificacion SELECT ----------------------------------------------- Write-Host "[*] Verificando valores finales ..." $verify = Invoke-Remote "bash $remoteVerify" $verify | ForEach-Object { Write-Host $_ } # Limpieza final. Invoke-Remote "rm -f $remoteSql $remoteApply $remoteVerify" | Out-Null Write-Host "" Write-Host "[OK] Parche enterprise aplicado correctamente (3/3 UPDATE 1)." Write-Host " NO pulsar 'Refresh' en /super_admin/settings."