mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 06:40:45 +08:00
Some checks failed
Release Windows / build (push) Has been cancelled
Allow non-admin reconfigure of .env, harden setup/tray flows, and refresh the context menu with hover selection. Co-authored-by: Cursor <cursoragent@cursor.com>
371 lines
17 KiB
PowerShell
371 lines
17 KiB
PowerShell
param(
|
||
[ValidateSet("bundled", "external", "")]
|
||
[string]$DbMode = "",
|
||
[string]$ProgramRoot = "",
|
||
[string]$DataRoot = "",
|
||
[string]$ExternalDatabaseUrl = "",
|
||
[string]$ExternalDatabaseUrlFile = "",
|
||
[string]$CredentialSecretKey = "",
|
||
[string]$CredentialSecretKeyFile = "",
|
||
[string]$BundledPassword = "",
|
||
[int]$BundledPort = 15432,
|
||
[switch]$NonInteractive = $false,
|
||
[switch]$SkipExternalProbe = $false
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
. "$PSScriptRoot\_common.ps1"
|
||
|
||
$prog = Get-NetxProgramRoot -Override $ProgramRoot
|
||
$data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot
|
||
$envPath = Join-Path $data ".env"
|
||
$zh = ([cultureinfo]::CurrentUICulture.Name -match '^(zh|zh-)')
|
||
|
||
function T([string]$En, [string]$Zh) {
|
||
if ($zh) { return $Zh } else { return $En }
|
||
}
|
||
|
||
try {
|
||
|
||
Write-Host ("==> " + (T "Program root:" "程序目录:") + " $prog")
|
||
Write-Host ("==> " + (T "Data root:" "数据目录:") + " $data")
|
||
Write-Host ("==> " + (T "Env file:" "环境文件:") + " $envPath")
|
||
|
||
Ensure-NetxDataDirectories -DataRoot $data
|
||
|
||
$existing = Read-DotEnv -Path $envPath
|
||
if (-not $DbMode) {
|
||
if ($NonInteractive) {
|
||
if ($existing.ContainsKey("NETX_DB_MODE")) {
|
||
$DbMode = $existing["NETX_DB_MODE"]
|
||
} elseif ($existing.ContainsKey("NETX_DATABASE_URL")) {
|
||
$DbMode = "external"
|
||
} else {
|
||
$DbMode = "bundled"
|
||
}
|
||
} else {
|
||
Write-Host ""
|
||
Write-Host (T "Choose database mode:" "选择数据库模式:") -ForegroundColor Cyan
|
||
Write-Host (T " 1) bundled — NetX portable PostgreSQL (offline / default)" " 1) bundled — NetX 内置便携 PostgreSQL(可离线,默认)")
|
||
Write-Host (T " 2) external — existing PostgreSQL (you provide connection URL)" " 2) external — 已有外置 PostgreSQL(需填写连接串)")
|
||
Write-Host ""
|
||
$choice = Read-Host (T "Enter 1 or 2" "请输入 1 或 2")
|
||
if ($choice -eq "2") { $DbMode = "external" } else { $DbMode = "bundled" }
|
||
}
|
||
}
|
||
$DbMode = $DbMode.Trim().ToLowerInvariant()
|
||
if ($DbMode -ne "bundled" -and $DbMode -ne "external") {
|
||
throw "invalid_db_mode: $DbMode"
|
||
}
|
||
|
||
$values = @{}
|
||
$values["NETX_DB_MODE"] = $DbMode
|
||
$values["NETX_HOST"] = "127.0.0.1"
|
||
$values["NETX_PORT"] = "8890"
|
||
# Absolute UI path when present; else relative for source trees.
|
||
$distAbs = Join-Path $prog "web\dist"
|
||
if (Test-Path (Join-Path $distAbs "index.html")) {
|
||
$values["NETX_UI_DIST_DIR"] = (ConvertTo-NetxFsPath $distAbs)
|
||
} else {
|
||
$values["NETX_UI_DIST_DIR"] = "web/dist"
|
||
}
|
||
|
||
# All runtime data under data root (never under Program Files).
|
||
$dataEnv = Get-NetxDataEnvMap -DataRoot $data
|
||
foreach ($k in $dataEnv.Keys) { $values[$k] = $dataEnv[$k] }
|
||
|
||
# Preload credential key from file/CLI only; interactive prompt comes AFTER DB settings
|
||
# so users are not left thinking the key alone finished setup.
|
||
if ($CredentialSecretKeyFile) {
|
||
if (-not (Test-Path -LiteralPath $CredentialSecretKeyFile)) {
|
||
throw "credential_secret_key_file_missing: $CredentialSecretKeyFile"
|
||
}
|
||
$CredentialSecretKey = [IO.File]::ReadAllText($CredentialSecretKeyFile).Trim()
|
||
}
|
||
|
||
if ($DbMode -eq "bundled") {
|
||
$pgsql = Join-Path $prog "postgres\pgsql"
|
||
if (-not (Test-Path (Join-Path $pgsql "bin\initdb.exe"))) {
|
||
$alt = Join-Path $PSScriptRoot "postgres\pgsql"
|
||
if (Test-Path (Join-Path $alt "bin\initdb.exe")) {
|
||
$pgsql = $alt
|
||
} else {
|
||
throw (T `
|
||
"bundled_postgres_missing: incomplete package (expected $pgsql). Offline Setup must include postgres; rebuild with build_release.ps1 on a machine that already ran download_postgres.ps1." `
|
||
"缺少内置 PostgreSQL(期望路径 $pgsql)。离线安装包必须自带数据库;请在已运行过 download_postgres.ps1 的机器上重新 build_release。")
|
||
}
|
||
}
|
||
if (-not $BundledPassword) {
|
||
if ($NonInteractive) {
|
||
$BundledPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 24 | ForEach-Object { [char]$_ })
|
||
} else {
|
||
$sec = Read-Host -Prompt (T "Password for bundled role 'netx' (empty = auto-generate)" "内置数据库用户 netx 的密码(回车=自动生成)") -AsSecureString
|
||
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec)
|
||
try {
|
||
$BundledPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
|
||
} finally {
|
||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||
}
|
||
if (-not $BundledPassword) {
|
||
$BundledPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 24 | ForEach-Object { [char]$_ })
|
||
Write-Host (T "Generated password (saved in .env only)." "已自动生成密码(仅保存在 .env)。")
|
||
}
|
||
}
|
||
}
|
||
$pgData = Join-Path $data "pgdata"
|
||
$values["NETX_BUNDLED_PG_PORT"] = "$BundledPort"
|
||
$values["NETX_BUNDLED_PG_DATA_DIR"] = (ConvertTo-NetxFsPath $pgData)
|
||
$pwFile = Join-Path $data "pg_netx.pw"
|
||
Set-Content -Path $pwFile -Value $BundledPassword -Encoding ascii -NoNewline
|
||
$encPw = [uri]::EscapeDataString($BundledPassword)
|
||
$values["NETX_DATABASE_URL"] = "postgresql+psycopg://netx:${encPw}@127.0.0.1:${BundledPort}/netx"
|
||
|
||
$initdb = Join-Path $pgsql "bin\initdb.exe"
|
||
$pgCtl = Join-Path $pgsql "bin\pg_ctl.exe"
|
||
$psql = Join-Path $pgsql "bin\psql.exe"
|
||
$sqlPw = ConvertTo-NetxSqlLiteral $BundledPassword
|
||
|
||
if (-not (Test-Path (Join-Path $pgData "PG_VERSION"))) {
|
||
Write-Host "==> initdb $pgData"
|
||
$pwSuper = Join-Path $data "pg_super.pw"
|
||
Set-Content -Path $pwSuper -Value $BundledPassword -Encoding ascii -NoNewline
|
||
& $initdb -D $pgData -U postgres -A password --pwfile=$pwSuper -E UTF8 --locale=C
|
||
if ($LASTEXITCODE -ne 0) { throw "initdb_failed" }
|
||
}
|
||
|
||
$conf = Join-Path $pgData "postgresql.conf"
|
||
$hba = Join-Path $pgData "pg_hba.conf"
|
||
if (Test-Path $conf) {
|
||
$confText = Get-Content -Raw -Path $conf
|
||
if ($confText -notmatch "(?m)^\s*port\s*=") {
|
||
Add-Content -Path $conf -Value "`nport = $BundledPort`nlisten_addresses = '127.0.0.1'`n"
|
||
} else {
|
||
$confText = $confText -replace '(?m)^\s*port\s*=\s*\d+', "port = $BundledPort"
|
||
if ($confText -notmatch "(?m)^\s*listen_addresses\s*=") {
|
||
$confText += "`nlisten_addresses = '127.0.0.1'`n"
|
||
} else {
|
||
$confText = $confText -replace "(?m)^\s*listen_addresses\s*=\s*'[^']*'", "listen_addresses = '127.0.0.1'"
|
||
}
|
||
Set-Content -Path $conf -Value $confText -Encoding utf8
|
||
}
|
||
}
|
||
if (Test-Path $hba) {
|
||
$hbaText = Get-Content -Raw -Path $hba
|
||
if ($hbaText -notmatch "127\.0\.0\.1/32") {
|
||
Add-Content -Path $hba -Value "`nhost all all 127.0.0.1/32 scram-sha-256`n"
|
||
}
|
||
}
|
||
|
||
$status = & $pgCtl -D $pgData status 2>&1 | Out-String
|
||
if ($status -notmatch "server is running") {
|
||
if (-not (Test-NetxTcpPortFree -HostName "127.0.0.1" -Port $BundledPort)) {
|
||
throw "port_in_use: 127.0.0.1:$BundledPort already occupied (bundled Postgres)"
|
||
}
|
||
Write-Host "==> Starting bundled PostgreSQL for bootstrap"
|
||
& $pgCtl -D $pgData -l (Join-Path $pgData "pg.log") start
|
||
if ($LASTEXITCODE -ne 0) { throw "pg_start_failed" }
|
||
Start-Sleep -Seconds 2
|
||
}
|
||
|
||
$env:PGPASSWORD = $BundledPassword
|
||
try {
|
||
function Invoke-NetxPsql {
|
||
param([string]$Sql, [string]$Database = "postgres")
|
||
$out = & $psql -h 127.0.0.1 -p $BundledPort -U postgres -d $Database -v ON_ERROR_STOP=1 -tAc $Sql 2>&1
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "psql_failed ($LASTEXITCODE): $Sql`n$out"
|
||
}
|
||
return (($out | Out-String).Trim())
|
||
}
|
||
|
||
$role = Invoke-NetxPsql -Sql "SELECT 1 FROM pg_roles WHERE rolname='netx'"
|
||
if ($role -eq "1") {
|
||
Invoke-NetxPsql -Sql "ALTER ROLE netx WITH LOGIN PASSWORD '$sqlPw'" | Out-Null
|
||
} else {
|
||
Invoke-NetxPsql -Sql "CREATE ROLE netx LOGIN PASSWORD '$sqlPw'" | Out-Null
|
||
}
|
||
$db = Invoke-NetxPsql -Sql "SELECT 1 FROM pg_database WHERE datname='netx'"
|
||
if ($db -ne "1") {
|
||
Invoke-NetxPsql -Sql "CREATE DATABASE netx OWNER netx" | Out-Null
|
||
}
|
||
Invoke-NetxPsql -Sql "GRANT ALL PRIVILEGES ON DATABASE netx TO netx" | Out-Null
|
||
# Verify login as app role (catches auth/hba mismatches early).
|
||
$prev = $env:PGPASSWORD
|
||
$env:PGPASSWORD = $BundledPassword
|
||
try {
|
||
$ping = & $psql -h 127.0.0.1 -p $BundledPort -U netx -d netx -v ON_ERROR_STOP=1 -tAc "SELECT 1" 2>&1
|
||
if ($LASTEXITCODE -ne 0 -or (($ping | Out-String).Trim()) -ne "1") {
|
||
throw "netx_role_login_failed: $ping"
|
||
}
|
||
} finally {
|
||
$env:PGPASSWORD = $prev
|
||
}
|
||
} finally {
|
||
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
|
||
}
|
||
Write-Host "==> Bundled Postgres ready on 127.0.0.1:$BundledPort" -ForegroundColor Green
|
||
} else {
|
||
if ($ExternalDatabaseUrlFile) {
|
||
if (-not (Test-Path -LiteralPath $ExternalDatabaseUrlFile)) {
|
||
throw "external_url_file_missing: $ExternalDatabaseUrlFile"
|
||
}
|
||
$ExternalDatabaseUrl = [IO.File]::ReadAllText($ExternalDatabaseUrlFile).Trim()
|
||
}
|
||
if (-not $ExternalDatabaseUrl) {
|
||
if ($NonInteractive) {
|
||
if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
||
$ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"]
|
||
} else {
|
||
throw "external_url_required"
|
||
}
|
||
} else {
|
||
# Interactive: always ask. Empty = keep existing URL (never silent).
|
||
Write-Host ""
|
||
Write-Host (T `
|
||
"Enter PostgreSQL URL (database/role must already exist):" `
|
||
"请输入 PostgreSQL 连接串(库和用户需事先建好):") -ForegroundColor Cyan
|
||
Write-Host " postgresql+psycopg://USER:PASSWORD@HOST:5432/DBNAME"
|
||
Write-Host (T `
|
||
"Example: postgresql+psycopg://netx:secret@10.0.0.8:5432/netx" `
|
||
"示例: postgresql+psycopg://netx:secret@10.0.0.8:5432/netx")
|
||
if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
||
Write-Host (T `
|
||
"Current: $($existing['NETX_DATABASE_URL'])" `
|
||
"当前: $($existing['NETX_DATABASE_URL'])") -ForegroundColor DarkGray
|
||
Write-Host (T `
|
||
"Press Enter to keep the current URL, or paste a new one." `
|
||
"直接回车 = 保留当前连接串;或粘贴新的连接串。")
|
||
}
|
||
$entered = (Read-Host "NETX_DATABASE_URL").Trim()
|
||
if ($entered) {
|
||
$ExternalDatabaseUrl = $entered
|
||
} elseif ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) {
|
||
$ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"]
|
||
Write-Host (T "==> Keeping existing NETX_DATABASE_URL" "==> 保留已有 NETX_DATABASE_URL") -ForegroundColor Green
|
||
}
|
||
}
|
||
}
|
||
if (-not $ExternalDatabaseUrl) {
|
||
throw "external_url_required"
|
||
}
|
||
$ExternalDatabaseUrl = $ExternalDatabaseUrl.Trim()
|
||
$values["NETX_DATABASE_URL"] = $ExternalDatabaseUrl
|
||
Write-Host "==> External Postgres configured" -ForegroundColor Green
|
||
|
||
if (-not $SkipExternalProbe) {
|
||
# Probe with bundled psql when available (wizard already tested; this covers CLI reconfigure).
|
||
$psqlProbe = Join-Path $prog "postgres\pgsql\bin\psql.exe"
|
||
$testHelper = Join-Path $PSScriptRoot "installer\test_pg_conn.ps1"
|
||
if (-not (Test-Path $testHelper)) {
|
||
$testHelper = Join-Path $PSScriptRoot "test_pg_conn.ps1"
|
||
}
|
||
if ((Test-Path $psqlProbe) -and ($ExternalDatabaseUrl -match '^(?:postgresql(?:\+psycopg)?|postgres)://([^:]+):([^@]*)@([^:/]+):?(\d+)?/([^?\s]+)')) {
|
||
$u = [uri]::UnescapeDataString($Matches[1])
|
||
$pw = [uri]::UnescapeDataString($Matches[2])
|
||
$h = $Matches[3]
|
||
$po = if ($Matches[4]) { [int]$Matches[4] } else { 5432 }
|
||
$dbn = [uri]::UnescapeDataString($Matches[5])
|
||
Write-Host "==> Probing external PostgreSQL ${h}:${po}/${dbn} ..."
|
||
if (Test-Path $testHelper) {
|
||
$probeErr = Join-Path $env:TEMP ("netx-pg-probe-" + [Guid]::NewGuid().ToString("n") + ".txt")
|
||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $testHelper `
|
||
-PgHost $h -Port $po -User $u -Password $pw -Database $dbn `
|
||
-PsqlPath $psqlProbe -OutErrFile $probeErr
|
||
if ($LASTEXITCODE -ne 0) {
|
||
$detail = if (Test-Path $probeErr) { (Get-Content -LiteralPath $probeErr -Raw) } else { "exit $LASTEXITCODE" }
|
||
Remove-Item -LiteralPath $probeErr -Force -ErrorAction SilentlyContinue
|
||
throw (T "external_db_probe_failed: $detail" "外置数据库连接失败: $detail")
|
||
}
|
||
Remove-Item -LiteralPath $probeErr -Force -ErrorAction SilentlyContinue
|
||
Write-Host "==> External PostgreSQL OK" -ForegroundColor Green
|
||
} else {
|
||
$env:PGPASSWORD = $pw
|
||
try {
|
||
$ping = & $psqlProbe -h $h -p $po -U $u -d $dbn -t -A -c "SELECT 1" 2>&1
|
||
if ($LASTEXITCODE -ne 0 -or (($ping | Out-String).Trim()) -notmatch '1') {
|
||
throw (T "external_db_probe_failed: $ping" "外置数据库连接失败: $ping")
|
||
}
|
||
} finally {
|
||
Remove-Item Env:PGPASSWORD -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
} else {
|
||
Write-Host (T `
|
||
"[WARN] Skipped connection probe (no bundled psql or URL parse failed)." `
|
||
"[WARN] 已跳过连接探测(缺少捆绑 psql 或连接串无法解析)。") -ForegroundColor Yellow
|
||
}
|
||
}
|
||
}
|
||
|
||
# Fernet key AFTER database prompts (interactive UX).
|
||
# Priority: CLI/file > interactive prompt > existing .env > generate.
|
||
if (-not $CredentialSecretKey -and -not $NonInteractive) {
|
||
Write-Host ""
|
||
Write-Host (T `
|
||
"Optional: paste NETX_CREDENTIAL_SECRET_KEY from an existing install" `
|
||
"可选: 粘贴已有安装的 NETX_CREDENTIAL_SECRET_KEY(便于沿用已加密凭据)") -ForegroundColor Cyan
|
||
Write-Host (T `
|
||
"Leave empty to keep existing .env key, or auto-generate if none." `
|
||
"留空则保留已有 .env 中的密钥;若没有则自动生成。")
|
||
$CredentialSecretKey = (Read-Host "NETX_CREDENTIAL_SECRET_KEY").Trim()
|
||
}
|
||
if ($CredentialSecretKey) {
|
||
$values["NETX_CREDENTIAL_SECRET_KEY"] = $CredentialSecretKey.Trim()
|
||
Write-Host "==> Using provided NETX_CREDENTIAL_SECRET_KEY" -ForegroundColor Green
|
||
} elseif ($existing.ContainsKey("NETX_CREDENTIAL_SECRET_KEY") -and $existing["NETX_CREDENTIAL_SECRET_KEY"]) {
|
||
$values["NETX_CREDENTIAL_SECRET_KEY"] = $existing["NETX_CREDENTIAL_SECRET_KEY"]
|
||
Write-Host "==> Keeping existing NETX_CREDENTIAL_SECRET_KEY from .env" -ForegroundColor Green
|
||
} else {
|
||
$values["NETX_CREDENTIAL_SECRET_KEY"] = New-NetxFernetKey
|
||
Write-Host "==> Generated NETX_CREDENTIAL_SECRET_KEY (saved in .env)" -ForegroundColor Green
|
||
}
|
||
|
||
Write-DotEnvValue -Path $envPath -Values $values
|
||
Write-Host ""
|
||
Write-Host "Wrote $envPath" -ForegroundColor Green
|
||
|
||
# Official Setup ships python/runtime + .venv (build_release -CreateVenv).
|
||
$venvPy = Join-Path $prog ".venv\Scripts\python.exe"
|
||
try {
|
||
$null = Ensure-NetxVenv -ProgramRoot $prog
|
||
Write-Host "==> Bundled Python venv OK: $venvPy" -ForegroundColor Green
|
||
} catch {
|
||
if (Test-Path $venvPy) {
|
||
throw
|
||
}
|
||
Write-Host "==> Bundled .venv missing — creating one (Setup should normally ship it)." -ForegroundColor Yellow
|
||
Write-Host "[WARN] $($_.Exception.Message)" -ForegroundColor Yellow
|
||
Write-Host " Install a Setup built with: packaging\build_release.ps1 -CreateVenv" -ForegroundColor Yellow
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host (T "Setup complete." "首次配置完成。") -ForegroundColor Green
|
||
if (-not $NonInteractive) {
|
||
Write-Host (T `
|
||
"Next: Start Menu → NetX Tray (or press Y below)." `
|
||
"下一步: 开始菜单 → NetX 托盘 (或在下方按 Y 立即启动)。")
|
||
$ans = Read-Host (T "Start NetX tray now? [Y/n]" "现在启动 NetX 托盘?[Y/n]")
|
||
if ($ans -notmatch '^[Nn]') {
|
||
try {
|
||
Start-NetxPowerShell -File (Join-Path $PSScriptRoot "netx_tray.ps1") `
|
||
-Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-StartOnLaunch") `
|
||
-WorkingDirectory $prog -WindowStyle Hidden | Out-Null
|
||
Write-Host (T "Tray started." "托盘已启动。") -ForegroundColor Green
|
||
} catch {
|
||
Write-Host "[WARN] $($_.Exception.Message)" -ForegroundColor Yellow
|
||
}
|
||
}
|
||
} else {
|
||
Write-Host "Next: .\packaging\start_netx_app.ps1 or NetX-Tray.cmd"
|
||
}
|
||
|
||
} catch {
|
||
Write-Host ""
|
||
Write-Host ("[ERR] " + $_.Exception.Message) -ForegroundColor Red
|
||
if (-not $NonInteractive) {
|
||
try {
|
||
[void](Read-Host (T "Press Enter to close" "按回车关闭窗口"))
|
||
} catch {}
|
||
}
|
||
exit 1
|
||
}
|