From 4e582dc4a5b816ec5e24096b59eefb393995591b Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 7 Oct 2026 16:43:28 +0800 Subject: [PATCH] Release 0.4.7: ProgramData ACL fix and tray menu polish. Allow non-admin reconfigure of .env, harden setup/tray flows, and refresh the context menu with hover selection. Co-authored-by: Cursor --- packaging/_common.ps1 | 72 +++++- packaging/installer/netx.iss | 87 +++---- packaging/manifest.example.json | 10 +- packaging/netx_tray.ps1 | 439 ++++++++++++++++++++++++++++---- packaging/setup_first_run.ps1 | 104 +++++--- pyproject.toml | 2 +- 6 files changed, 583 insertions(+), 131 deletions(-) diff --git a/packaging/_common.ps1 b/packaging/_common.ps1 index a73f544..6c2c9dd 100644 --- a/packaging/_common.ps1 +++ b/packaging/_common.ps1 @@ -1,4 +1,4 @@ -# Shared path helpers for Windows packaging scripts. +# Shared path helpers for Windows packaging scripts. # Dot-source: . "$PSScriptRoot\_common.ps1" $ErrorActionPreference = "Stop" @@ -114,7 +114,33 @@ function Write-DotEnvValue { if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } - Set-Content -Path $Path -Value ($lines -join "`r`n") -Encoding utf8 + $text = ($lines -join "`r`n") + try { + Set-Content -LiteralPath $Path -Value $text -Encoding utf8 + } catch { + # Admin-created .env is often Users:RX only — repair ACL then retry once. + $root = $dir + if ((Split-Path -Leaf $dir) -eq "NetX" -or $dir -match '\\NetX$') { + $root = $dir + } else { + $parent = Split-Path -Parent $dir + if ($parent -and ((Split-Path -Leaf $parent) -eq "NetX")) { $root = $parent } + } + $null = Ensure-NetxDataAcl -DataRoot $root -Quiet + try { + # Reset .env ACL explicitly if still blocked. + $icacls = Join-Path $env:SystemRoot "System32\icacls.exe" + if (Test-Path -LiteralPath $icacls) { + & $icacls $Path /grant "*S-1-5-32-545:M" /C /Q 2>$null | Out-Null + } + Set-Content -LiteralPath $Path -Value $text -Encoding utf8 + } catch { + throw (New-Object System.UnauthorizedAccessException( + ("access_denied: cannot write {0}. Run Start Menu → NetX → Reconfigure database as Administrator, or: icacls `"{1}`" /grant Users:(OI)(CI)M /T" -f $Path, $root), + $_.Exception + )) + } + } } function Get-DbMode { @@ -169,8 +195,48 @@ function New-NetxFernetKey { return [Convert]::ToBase64String($bytes).Replace('+', '-').Replace('/', '_') } +function Ensure-NetxDataAcl { + param( + [Parameter(Mandatory = $true)][string]$DataRoot, + [switch]$Quiet = $false + ) + # Installer creates %ProgramData%\NetX as Administrators. Tray / normal users must + # be able to update .env and runtime files when reconfiguring DB. + if (-not (Test-Path -LiteralPath $DataRoot)) { return $false } + try { + $acl = Get-Acl -LiteralPath $DataRoot + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "BUILTIN\Users", + "Modify", + "ContainerInherit,ObjectInherit", + "None", + "Allow" + ) + $acl.SetAccessRule($rule) + Set-Acl -LiteralPath $DataRoot -AclObject $acl + + # Also fix already-created files that only inherited RX for Users. + $icacls = Join-Path $env:SystemRoot "System32\icacls.exe" + if (Test-Path -LiteralPath $icacls) { + & $icacls $DataRoot /grant "*S-1-5-32-545:(OI)(CI)M" /T /C /Q 2>$null | Out-Null + } + if (-not $Quiet) { + Write-Host "==> Data ACL: BUILTIN\Users Modify on $DataRoot" -ForegroundColor DarkGray + } + return $true + } catch { + if (-not $Quiet) { + Write-Host "[WARN] Ensure-NetxDataAcl: $($_.Exception.Message)" -ForegroundColor Yellow + } + return $false + } +} + function Ensure-NetxDataDirectories { param([string]$DataRoot) + if (-not (Test-Path -LiteralPath $DataRoot)) { + New-Item -ItemType Directory -Path $DataRoot -Force | Out-Null + } $subs = @( "data", "data\auth", "data\runtime", "data\biz_state_spool", "data\ne_collections", "data\ne_exec_jobs", "data\webcrt", @@ -182,6 +248,8 @@ function Ensure-NetxDataDirectories { New-Item -ItemType Directory -Path $p -Force | Out-Null } } + # Best-effort; succeeds when running elevated (installer / first-run as admin). + $null = Ensure-NetxDataAcl -DataRoot $DataRoot -Quiet } function Test-NetxTcpPortFree { diff --git a/packaging/installer/netx.iss b/packaging/installer/netx.iss index 335ce0c..c9acd89 100644 --- a/packaging/installer/netx.iss +++ b/packaging/installer/netx.iss @@ -1,11 +1,11 @@ -; NetX Windows installer (Inno Setup 6+) +; NetX Windows installer (Inno Setup 6+) ; Compile after: packaging\build_release.ps1 ; ISCC.exe packaging\installer\netx.iss ; Encoding: UTF-8 with BOM (required for Chinese CustomMessages) #define MyAppName "NetX" #ifndef MyAppVersion - #define MyAppVersion "0.4.6" + #define MyAppVersion "0.4.7" #endif #define MyAppPublisher "NetX" #define MyAppURL "https://github.com/hansjone/netx" @@ -39,7 +39,7 @@ ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible PrivilegesRequired=admin ; Data lives under {commonappdata}\NetX - not overwritten by upgrades. -; Do NOT use CloseApplications=yes — it can freeze/beep on the Tasks/Ready +; Do NOT use CloseApplications=yes ¡ª it can freeze/beep on the Tasks/Ready ; pages while scanning locked NetX tray/service processes with no visible dialog. CloseApplications=no ShowLanguageDialog=yes @@ -66,45 +66,45 @@ english.DbUser=User english.DbPassword=Password english.DbName=Database english.CredKey=Credential key -english.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY (optional — empty = auto-generate) +english.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY (optional ¡ª empty = auto-generate) english.DbFieldsRequired=Please fill in host, port, user, password, and database name. -english.DbTesting=Testing PostgreSQL connection, please wait… +english.DbTesting=Testing PostgreSQL connection, please wait¡­ english.DbTestFailed=Cannot connect to PostgreSQL with these settings.%n%n%1%n%nFix the settings and try again. english.DbTestExtractFailed=Could not extract PostgreSQL client tools for connection test. english.DbSilentExternalMissing=Silent install with external DB requires /DbHost /DbUser /DbPassword /DbName (optional /DbPort). -english.DbApplyFailed=Database configuration failed after file install (exit %1).%n%nLog: %2%n%nFix the problem, then use Start Menu → Reconfigure database — or reinstall. +english.DbApplyFailed=Database configuration failed after file install (exit %1).%n%nLog: %2%n%nFix the problem, then use Start Menu ¡ú Reconfigure database ¡ª or reinstall. english.DbApplyExecFailed=Could not start database configuration script. english.DbBundledMissing=Built-in PostgreSQL files are missing from the install folder:%n%1%n%nThe Setup package is incomplete. Re-download NetX-Setup and install again. english.DbEnvMissing=Database configuration did not write %ProgramData%\NetX\.env. english.ReconfigureDb=Reconfigure database -chinesesimplified.CreateDesktopIcon=创建桌面快捷方式 -chinesesimplified.SetupOptions=安装完成后: -chinesesimplified.StartTrayNow=立即启动 NetX 托盘 -chinesesimplified.EnableAutostart=开机时自动启动 NetX 托盘 -chinesesimplified.EnableAutoUpdate=启用每日静默自动更新(以后需要联网) -chinesesimplified.InstallService=安装为 Windows 服务(使用已捆绑的 WinSW,可离线) -chinesesimplified.UninstallDeleteData=是否同时删除 NetX 数据(%ProgramData%\NetX)?%n%n是 = 删除数据库、配置和密钥%n否 = 保留数据以便以后重装 -chinesesimplified.DbPageCaption=数据库 -chinesesimplified.DbPageDescription=选择内置或外置 PostgreSQL。可选粘贴已有 NETX_CREDENTIAL_SECRET_KEY;留空则自动生成。 -chinesesimplified.DbBundled=内置 PostgreSQL(便携,可离线) -chinesesimplified.DbExternal=外置 PostgreSQL(已有服务器) -chinesesimplified.DbHost=主机 -chinesesimplified.DbPort=端口 -chinesesimplified.DbUser=用户 -chinesesimplified.DbPassword=密码 -chinesesimplified.DbName=数据库名 -chinesesimplified.CredKey=凭据密钥 -chinesesimplified.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY(可选,留空=自动生成) -chinesesimplified.DbFieldsRequired=请填写主机、端口、用户、密码和数据库名。 -chinesesimplified.DbTesting=正在测试 PostgreSQL 连接,请稍候… -chinesesimplified.DbTestFailed=无法用当前设置连接 PostgreSQL。%n%n%1%n%n请改正后重试。 -chinesesimplified.DbTestExtractFailed=无法解压 PostgreSQL 客户端以测试连接。 -chinesesimplified.DbSilentExternalMissing=静默安装外置库需要参数 /DbHost /DbUser /DbPassword /DbName(可选 /DbPort)。 -chinesesimplified.DbApplyFailed=文件安装后数据库自动配置失败(退出码 %1)。%n%n日志: %2%n%n请处理后使用开始菜单 → 重新配置数据库,或重新安装。 -chinesesimplified.DbApplyExecFailed=无法启动数据库配置脚本。 -chinesesimplified.DbBundledMissing=安装目录中缺少内置 PostgreSQL 文件:%n%1%n%n安装包不完整,请重新下载 NetX-Setup 后再装。 -chinesesimplified.DbEnvMissing=数据库配置未写入 %ProgramData%\NetX\.env。 -chinesesimplified.ReconfigureDb=重新配置数据库 +chinesesimplified.CreateDesktopIcon=´´½¨×ÀÃæ¿ì½ݷ½ʽ +chinesesimplified.SetupOptions=°²װÍê³ɺó: +chinesesimplified.StartTrayNow=Á¢¼´Æô¶¯ NetX ÍÐÅÌ +chinesesimplified.EnableAutostart=¿ª»úʱ×Զ¯Æô¶¯ NetX ÍÐÅÌ +chinesesimplified.EnableAutoUpdate=ÆôÓÃÿÈվ²Ĭ×Զ¯¸üУ¨ÒԺóÐèҪÁªÍø£© +chinesesimplified.InstallService=°²װΪ Windows ·þÎñ£¨ʹÓÃÒÑÀ¦°óµÄ WinSW£¬¿ÉÀëÏߣ© +chinesesimplified.UninstallDeleteData=ÊǷñͬʱɾ³ý NetX Êý¾ݣ¨%ProgramData%\NetX£©£¿%n%nÊÇ = ɾ³ýÊý¾ݿ⡢ÅäÖúÍÃÜԿ%n·ñ = ±£ÁôÊý¾ÝÒԱãÒԺóÖØװ +chinesesimplified.DbPageCaption=Êý¾ݿâ +chinesesimplified.DbPageDescription=ѡÔñÄÚÖûòÍâÖà PostgreSQL¡£¿ÉѡճÌùÒÑÓÐ NETX_CREDENTIAL_SECRET_KEY£»Áô¿ÕÔò×Զ¯Éú³ɡ£ +chinesesimplified.DbBundled=ÄÚÖà PostgreSQL£¨±ãЯ£¬¿ÉÀëÏߣ© +chinesesimplified.DbExternal=ÍâÖà PostgreSQL£¨ÒÑÓзþÎñÆ÷£© +chinesesimplified.DbHost=Ö÷»ú +chinesesimplified.DbPort=¶˿Ú +chinesesimplified.DbUser=Óû§ +chinesesimplified.DbPassword=ÃÜÂë +chinesesimplified.DbName=Êý¾ݿâÃû +chinesesimplified.CredKey=ƾ¾ÝÃÜԿ +chinesesimplified.CredKeyHint=NETX_CREDENTIAL_SECRET_KEY£¨¿Éѡ£¬Áô¿Õ=×Զ¯Éú³ɣ© +chinesesimplified.DbFieldsRequired=ÇëÌîдÖ÷»ú¡¢¶˿ڡ¢Óû§¡¢ÃÜÂëºÍÊý¾ݿâÃû¡£ +chinesesimplified.DbTesting=ÕýÔڲâÊÔ PostgreSQL Á¬½ӣ¬ÇëÉԺò¡­ +chinesesimplified.DbTestFailed=Î޷¨Óõ±ǰÉèÖÃÁ¬½Ó PostgreSQL¡£%n%n%1%n%nÇë¸ÄÕýºóÖØÊԡ£ +chinesesimplified.DbTestExtractFailed=Î޷¨½âѹ PostgreSQL ¿ͻ§¶ËÒԲâÊÔÁ¬½ӡ£ +chinesesimplified.DbSilentExternalMissing=¾²Ĭ°²װÍâÖÿâÐèҪ²ÎÊý /DbHost /DbUser /DbPassword /DbName£¨¿Éѡ /DbPort£©¡£ +chinesesimplified.DbApplyFailed=Îļþ°²װºóÊý¾ݿâ×Զ¯ÅäÖÃʧ°ܣ¨Í˳öÂë %1£©¡£%n%nÈÕ־: %2%n%nÇ봦ÀíºóʹÓÿªʼ²˵¥ ¡ú ÖØÐÂÅäÖÃÊý¾ݿ⣬»òÖØÐ°²װ¡£ +chinesesimplified.DbApplyExecFailed=Î޷¨Æô¶¯Êý¾ݿâÅäÖýű¾¡£ +chinesesimplified.DbBundledMissing=°²װĿ¼ÖÐȱÉÙÄÚÖà PostgreSQL Îļþ£º%n%1%n%n°²װ°ü²»ÍêÕû£¬ÇëÖØÐÂÏÂÔØ NetX-Setup ºóÔÙװ¡£ +chinesesimplified.DbEnvMissing=Êý¾ݿâÅäÖÃδдÈë %ProgramData%\NetX\.env¡£ +chinesesimplified.ReconfigureDb=ÖØÐÂÅäÖÃÊý¾ݿâ [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce @@ -128,18 +128,19 @@ Source: "{#StageDir}\postgres\pgsql\bin\libxml2.dll"; Flags: dontcopy Source: "{#StageDir}\postgres\pgsql\bin\libxslt.dll"; Flags: dontcopy [Dirs] -Name: "{commonappdata}\NetX" -Name: "{commonappdata}\NetX\data" -Name: "{commonappdata}\NetX\data\auth" -Name: "{commonappdata}\NetX\data\runtime" -Name: "{commonappdata}\NetX\pgdata" -Name: "{commonappdata}\NetX\backups" +; users-modify so tray / non-admin reconfigure can update .env and runtime files. +Name: "{commonappdata}\NetX"; Permissions: users-modify +Name: "{commonappdata}\NetX\data"; Permissions: users-modify +Name: "{commonappdata}\NetX\data\auth"; Permissions: users-modify +Name: "{commonappdata}\NetX\data\runtime"; Permissions: users-modify +Name: "{commonappdata}\NetX\pgdata"; Permissions: users-modify +Name: "{commonappdata}\NetX\backups"; Permissions: users-modify [InstallDelete] ; Remove legacy desktop shortcuts from older Setup builds (keep a single NetX icon). Type: files; Name: "{commondesktop}\NetX Start.lnk" Type: files; Name: "{commondesktop}\NetX First-time setup.lnk" -Type: files; Name: "{commondesktop}\首次配置(内置或外置数据库).lnk" +Type: files; Name: "{commondesktop}\Ê״ÎÅäÖã¨ÄÚÖûòÍâÖÃÊý¾ݿ⣩.lnk" [Icons] ; Prefer .cmd targets so Server 2012 / classic Start Menu shows normal program entries. @@ -505,7 +506,7 @@ begin GDbConnOk := False; { Always show DB page so choosing built-in always re-runs auto-configure. Skipping when .env exists caused upgrades/reinstalls to keep a broken or - external config and still launch tray → confusing "first-time" prompts. } + external config and still launch tray ¡ú confusing "first-time" prompts. } GSkipDbPage := False; DbPage := CreateCustomPage(wpSelectDir, @@ -678,7 +679,7 @@ begin end; { Connection test here so bad settings never reach Tasks/Ready. - Show busy state — ExtractTemporaryFile + psql can take a few seconds. } + Show busy state ¡ª ExtractTemporaryFile + psql can take a few seconds. } SetWizardBusy(True, ExpandConstant('{cm:DbTesting}')); try Result := ValidateExternalDbSaved(ErrMsg); diff --git a/packaging/manifest.example.json b/packaging/manifest.example.json index 1d23d3e..2dccc21 100644 --- a/packaging/manifest.example.json +++ b/packaging/manifest.example.json @@ -1,11 +1,11 @@ -{ +{ "channel": "stable", - "latest": "0.4.5", + "latest": "0.4.7", "min_compatible": "0.3.0", - "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.5", + "notes_url": "https://github.com/hansjone/netx/releases/tag/v0.4.7", "windows": { - "url": "https://github.com/hansjone/netx/releases/download/v0.4.5/NetX-0.4.5-win64.zip", - "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.5/NetX-Setup-0.4.5.exe", + "url": "https://github.com/hansjone/netx/releases/download/v0.4.7/NetX-0.4.7-win64.zip", + "setup_url": "https://github.com/hansjone/netx/releases/download/v0.4.7/NetX-Setup-0.4.7.exe", "sha256": "", "size": 0 } diff --git a/packaging/netx_tray.ps1 b/packaging/netx_tray.ps1 index 769352f..e1d31c6 100644 --- a/packaging/netx_tray.ps1 +++ b/packaging/netx_tray.ps1 @@ -6,8 +6,7 @@ [switch]$AutoUpdate = $false ) -# Simple system-tray controller for NetX (Windows packaged installs). -# Start / Stop / Open UI / Check update / Exit +# System-tray controller for NetX (Windows packaged installs). $ErrorActionPreference = "Stop" . "$PSScriptRoot\_common.ps1" @@ -15,6 +14,74 @@ $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing +# Custom tray menu: hover/selected highlight + flat light chrome (default OS menu looks dead). +if (-not ("NetxMenuRenderer" -as [type])) { + Add-Type -ReferencedAssemblies System.Windows.Forms, System.Drawing -TypeDefinition @" +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +public sealed class NetxMenuColorTable : ProfessionalColorTable { + static readonly Color Hover = Color.FromArgb(232, 240, 252); + static readonly Color Press = Color.FromArgb(214, 228, 248); + static readonly Color Border = Color.FromArgb(170, 198, 240); + static readonly Color Edge = Color.FromArgb(210, 214, 220); + static readonly Color Sep = Color.FromArgb(228, 230, 235); + static readonly Color Bg = Color.FromArgb(252, 252, 253); + + public override Color MenuItemSelected { get { return Hover; } } + public override Color MenuItemSelectedGradientBegin { get { return Hover; } } + public override Color MenuItemSelectedGradientEnd { get { return Hover; } } + public override Color MenuItemBorder { get { return Border; } } + public override Color MenuItemPressedGradientBegin { get { return Press; } } + public override Color MenuItemPressedGradientMiddle { get { return Press; } } + public override Color MenuItemPressedGradientEnd { get { return Press; } } + public override Color MenuBorder { get { return Edge; } } + public override Color ToolStripDropDownBackground { get { return Bg; } } + public override Color ImageMarginGradientBegin { get { return Bg; } } + public override Color ImageMarginGradientMiddle { get { return Bg; } } + public override Color ImageMarginGradientEnd { get { return Bg; } } + public override Color SeparatorDark { get { return Sep; } } + public override Color SeparatorLight { get { return Sep; } } +} + +public sealed class NetxMenuRenderer : ToolStripProfessionalRenderer { + public NetxMenuRenderer() : base(new NetxMenuColorTable()) { + RoundedEdges = false; + } + + protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) { + ToolStripMenuItem item = e.Item as ToolStripMenuItem; + if (item == null || !item.Selected || !item.Enabled) { + base.OnRenderMenuItemBackground(e); + return; + } + Rectangle r = new Rectangle(2, 0, e.Item.Width - 4, e.Item.Height); + using (SolidBrush brush = new SolidBrush(Color.FromArgb(232, 240, 252))) + using (Pen pen = new Pen(Color.FromArgb(170, 198, 240))) { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + e.Graphics.FillRectangle(brush, r); + e.Graphics.DrawRectangle(pen, r.X, r.Y, r.Width - 1, r.Height - 1); + } + } + + protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) { + Rectangle r = new Rectangle(0, 0, e.AffectedBounds.Width - 1, e.AffectedBounds.Height - 1); + using (Pen pen = new Pen(Color.FromArgb(210, 214, 220))) { + e.Graphics.DrawRectangle(pen, r); + } + } + + protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) { + int y = e.Item.Height / 2; + using (Pen pen = new Pen(Color.FromArgb(228, 230, 235))) { + e.Graphics.DrawLine(pen, 10, y, e.Item.Width - 10, y); + } + } +} +"@ +} + $prog = Get-NetxProgramRoot -Override $ProgramRoot $data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot $hostBind = "127.0.0.1" @@ -35,6 +102,12 @@ if ($autoVal -match '^(1|true|yes|on)$') { } $uiUrl = "http://${hostBind}:${port}/" $ver = Get-NetxVersion -ProgramRoot $prog +$dbMode = Get-DbMode -EnvMap $map +$dbModeLabel = if ($dbMode -eq "bundled") { + if (([cultureinfo]::CurrentUICulture.Name -match '^(zh|zh-)')) { "内置库" } else { "built-in DB" } +} else { + if (([cultureinfo]::CurrentUICulture.Name -match '^(zh|zh-)')) { "外置库" } else { "external DB" } +} $zh = ([cultureinfo]::CurrentUICulture.Name -match '^(zh|zh-)') function T([string]$En, [string]$Zh) { if ($zh) { return $Zh } else { return $En } @@ -74,6 +147,7 @@ try { $startPs1 = Join-Path $PSScriptRoot "start_netx_app.ps1" $stopPs1 = Join-Path $PSScriptRoot "stop_netx_app.ps1" $checkPs1 = Join-Path $PSScriptRoot "check_update.ps1" +$setupPs1 = Join-Path $PSScriptRoot "setup_first_run.ps1" function Invoke-NetxScript { param( @@ -85,6 +159,52 @@ function Invoke-NetxScript { -WorkingDirectory $prog -WindowStyle Hidden -PassThru -Wait:$Wait } +function Update-NetxTrayTip { + $running = Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 1 + $state = if ($running) { + (T "Running" "运行中") + } else { + (T "Stopped" "已停止") + } + $tip = "NetX $ver · $state · $dbModeLabel`n$uiUrl" + if ($tip.Length -gt 63) { + # NotifyIcon.Text max ~63 chars on older Windows. + $tip = "NetX $ver · $state · $dbModeLabel" + } + $notify.Text = $tip +} + +function Close-NetxTrayMenu { + # ContextMenuStrip stays modal if Click handlers block; close before dialogs/waits. + try { $menu.Close() } catch {} + try { [System.Windows.Forms.Application]::DoEvents() } catch {} +} + +function Start-NetxTrayDeferred { + param([scriptblock]$Work) + Close-NetxTrayMenu + $timer = New-Object System.Windows.Forms.Timer + $timer.Interval = 50 + $script:netxTrayDeferredWork = $Work + $timer.add_Tick({ + $t = $script:netxTrayDeferredTimer + try { if ($t) { $t.Stop(); $t.Dispose() } } catch {} + $script:netxTrayDeferredTimer = $null + $w = $script:netxTrayDeferredWork + $script:netxTrayDeferredWork = $null + if ($w) { & $w } + }) + $script:netxTrayDeferredTimer = $timer + $timer.Start() +} + +function Test-NetxSetupCancelled { + param([AllowNull()][Nullable[int]]$ExitCode) + if ($null -eq $ExitCode) { return $false } + # STATUS_CONTROL_C_EXIT (0xC000013A): console closed / Ctrl+C — not a setup failure. + return ([int]$ExitCode -eq -1073741510) +} + function Open-NetxUiWhenReady { param([int]$TimeoutSec = 180) $notify.ShowBalloonTip( @@ -95,14 +215,16 @@ function Open-NetxUiWhenReady { ) if (Wait-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec $TimeoutSec) { try { Start-Process $uiUrl } catch {} + Update-NetxTrayTip return } $notify.ShowBalloonTip( 8000, "NetX", - (T "UI not ready yet. Use tray → Open UI when the icon appears, or check data\runtime\netx.err.log." "界面尚未就绪。就绪后请用托盘「打开界面」,或查看 data\runtime\netx.err.log。"), + (T "UI not ready yet. Use tray → Open UI when ready, or check data\runtime\netx.err.log." "界面尚未就绪。就绪后请用托盘「打开界面」,或查看 data\runtime\netx.err.log。"), [System.Windows.Forms.ToolTipIcon]::Warning ) + Update-NetxTrayTip } $form = New-Object System.Windows.Forms.Form @@ -113,7 +235,6 @@ $form.Visible = $false $form.Size = New-Object System.Drawing.Size(0, 0) $notify = New-Object System.Windows.Forms.NotifyIcon -$notify.Text = "NetX $ver" $notify.Visible = $true $iconPath = Join-Path $PSScriptRoot "assets\netx.ico" try { @@ -125,68 +246,286 @@ try { } catch { try { $notify.Icon = [System.Drawing.SystemIcons]::Application } catch {} } +Update-NetxTrayTip + +function New-NetxMenuItem { + param( + [string]$Text, + [switch]$Header, + [switch]$Primary, + [switch]$Muted + ) + $item = New-Object System.Windows.Forms.ToolStripMenuItem + $item.Text = $Text + $item.AutoSize = $true + $item.Padding = New-Object System.Windows.Forms.Padding(10, 5, 28, 5) + $item.Margin = New-Object System.Windows.Forms.Padding(0) + if ($Header) { + $item.Enabled = $false + $item.Font = New-Object System.Drawing.Font("Segoe UI", 9.0, [System.Drawing.FontStyle]::Bold) + $item.ForeColor = [System.Drawing.Color]::FromArgb(55, 65, 80) + $item.Padding = New-Object System.Windows.Forms.Padding(10, 6, 28, 2) + } elseif ($Muted) { + $item.Enabled = $false + $item.Font = New-Object System.Drawing.Font("Segoe UI", 8.25) + $item.ForeColor = [System.Drawing.Color]::FromArgb(120, 128, 140) + $item.Padding = New-Object System.Windows.Forms.Padding(10, 1, 28, 6) + } elseif ($Primary) { + $item.Font = New-Object System.Drawing.Font("Segoe UI", 9.0, [System.Drawing.FontStyle]::Bold) + $item.ForeColor = [System.Drawing.Color]::FromArgb(20, 40, 70) + } else { + $item.Font = New-Object System.Drawing.Font("Segoe UI", 9.0) + $item.ForeColor = [System.Drawing.Color]::FromArgb(35, 40, 48) + } + return $item +} + +function New-NetxMenuSeparator { + $sep = New-Object System.Windows.Forms.ToolStripSeparator + $sep.Margin = New-Object System.Windows.Forms.Padding(0, 3, 0, 3) + $sep.Padding = New-Object System.Windows.Forms.Padding(0) + return $sep +} $menu = New-Object System.Windows.Forms.ContextMenuStrip +$menu.ShowImageMargin = $false +$menu.ShowCheckMargin = $false +$menu.Font = New-Object System.Drawing.Font("Segoe UI", 9.0) +$menu.BackColor = [System.Drawing.Color]::FromArgb(252, 252, 253) +$menu.ForeColor = [System.Drawing.Color]::FromArgb(35, 40, 48) +$menu.Padding = New-Object System.Windows.Forms.Padding(4, 4, 4, 4) +$menu.Renderer = New-Object NetxMenuRenderer -$miOpen = $menu.Items.Add((T "Open UI ($uiUrl)" "打开界面 ($uiUrl)")) -$miOpen.add_Click({ Start-Process $uiUrl }) +# --- header (info only) --- +$miHeader = New-NetxMenuItem -Text "NetX $ver" -Header +[void]$menu.Items.Add($miHeader) -$miStart = $menu.Items.Add((T "Start NetX" "启动 NetX")) +$miSub = New-NetxMenuItem -Text "$dbModeLabel · ${hostBind}:$port" -Muted +[void]$menu.Items.Add($miSub) + +[void]$menu.Items.Add((New-NetxMenuSeparator)) + +# --- primary --- +$miOpen = New-NetxMenuItem -Text (T "Open UI" "打开界面") -Primary +$miOpen.add_Click({ + Start-NetxTrayDeferred { + if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) { + Start-Process $uiUrl + } else { + $notify.ShowBalloonTip(4000, "NetX", (T "NetX is not running. Starting…" "NetX 未运行,正在启动…"), [System.Windows.Forms.ToolTipIcon]::Info) + Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null + Open-NetxUiWhenReady + } + } +}) +[void]$menu.Items.Add($miOpen) + +[void]$menu.Items.Add((New-NetxMenuSeparator)) + +# --- service control --- +$miStart = New-NetxMenuItem -Text (T "Start" "启动") $miStart.add_Click({ - Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null - Open-NetxUiWhenReady + Start-NetxTrayDeferred { + Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null + Open-NetxUiWhenReady + } }) +[void]$menu.Items.Add($miStart) -$miStop = $menu.Items.Add((T "Stop NetX (keep tray)" "停止 NetX(保留托盘)")) +$miStop = New-NetxMenuItem -Text (T "Stop" "停止") $miStop.add_Click({ - $notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info) - Invoke-NetxScript -File $stopPs1 | Out-Null + Start-NetxTrayDeferred { + $notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info) + Invoke-NetxScript -File $stopPs1 | Out-Null + Update-NetxTrayTip + } }) +[void]$menu.Items.Add($miStop) -$miStopExit = $menu.Items.Add((T "Stop NetX and exit tray" "停止 NetX 并退出托盘")) -$miStopExit.add_Click({ - $notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info) - Invoke-NetxScript -File $stopPs1 -Wait | Out-Null +[void]$menu.Items.Add((New-NetxMenuSeparator)) + +# --- setup / update --- +$miReconfig = New-NetxMenuItem -Text (T "Reconfigure database…" "重新配置数据库…") +$miReconfig.add_Click({ + Start-NetxTrayDeferred { + $ans = [System.Windows.Forms.MessageBox]::Show( + (T ` + "NetX will stop while you reconfigure the database.`n`nChoose built-in or external PostgreSQL in the console window.`nContinue?" ` + "重新配置数据库时会先停止 NetX。`n`n请在随后的控制台窗口中选择内置或外置 PostgreSQL。`n是否继续?"), + (T "Reconfigure database" "重新配置数据库"), + [System.Windows.Forms.MessageBoxButtons]::YesNo, + [System.Windows.Forms.MessageBoxIcon]::Question + ) + if ($ans -ne [System.Windows.Forms.DialogResult]::Yes) { return } + + $notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info) + Invoke-NetxScript -File $stopPs1 -Wait | Out-Null + + # ProgramData\.env is often admin-owned after Setup; repair ACL before reconfigure. + $null = Ensure-NetxDataAcl -DataRoot $data -Quiet + + try { + $p = Start-NetxPowerShell -File $setupPs1 ` + -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) ` + -WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru + if (Test-NetxSetupCancelled -ExitCode $p.ExitCode) { + $notify.ShowBalloonTip( + 4000, "NetX", + (T "Database setup cancelled." "已取消数据库配置。"), + [System.Windows.Forms.ToolTipIcon]::Info + ) + Update-NetxTrayTip + return + } + if ($null -ne $p.ExitCode -and $p.ExitCode -ne 0) { + # Access denied → offer elevated re-run (UAC). + $elev = [System.Windows.Forms.MessageBox]::Show( + (T ` + "Database setup failed (exit $($p.ExitCode)).`n`nIf this was a permission error on %ProgramData%\NetX\.env, click Yes to retry as Administrator." ` + "数据库配置失败(退出码 $($p.ExitCode))。`n`n若是 %ProgramData%\NetX\.env 权限问题,点「是」将以管理员身份重试。"), + "NetX", + [System.Windows.Forms.MessageBoxButtons]::YesNo, + [System.Windows.Forms.MessageBoxIcon]::Warning + ) + if ($elev -eq [System.Windows.Forms.DialogResult]::Yes) { + $arg = "-NoProfile -ExecutionPolicy Bypass -File `"$setupPs1`" -ProgramRoot `"$prog`" -DataRoot `"$data`"" + $ep = Start-Process -FilePath "powershell.exe" -ArgumentList $arg ` + -WorkingDirectory $prog -Verb RunAs -Wait -PassThru + if (Test-NetxSetupCancelled -ExitCode $ep.ExitCode) { + Update-NetxTrayTip + return + } + if ($null -ne $ep.ExitCode -and $ep.ExitCode -ne 0) { + [System.Windows.Forms.MessageBox]::Show( + (T "Elevated database setup still failed (exit $($ep.ExitCode))." "管理员配置仍失败(退出码 $($ep.ExitCode))。"), + "NetX", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Error + ) + Update-NetxTrayTip + return + } + } else { + Update-NetxTrayTip + return + } + } + } catch { + [System.Windows.Forms.MessageBox]::Show( + (T "Database setup failed: $($_.Exception.Message)" "数据库配置失败:$($_.Exception.Message)"), + "NetX", + [System.Windows.Forms.MessageBoxButtons]::OK, + [System.Windows.Forms.MessageBoxIcon]::Error + ) + Update-NetxTrayTip + return + } + + # Reload .env after reconfigure (host/port/mode may change). + if (Test-Path $envPath) { + $map = Read-DotEnv -Path $envPath + if ($map["NETX_HOST"]) { $hostBind = $map["NETX_HOST"] } + if ($map["NETX_PORT"]) { try { $port = [int]$map["NETX_PORT"] } catch {} } + $uiUrl = "http://${hostBind}:${port}/" + $dbMode = Get-DbMode -EnvMap $map + $dbModeLabel = if ($dbMode -eq "bundled") { + (T "built-in DB" "内置库") + } else { + (T "external DB" "外置库") + } + $miSub.Text = "$dbModeLabel · ${hostBind}:$port" + } + + $restart = [System.Windows.Forms.MessageBox]::Show( + (T "Database configuration saved.`nStart NetX now?" "数据库配置已保存。`n是否立即启动 NetX?"), + "NetX", + [System.Windows.Forms.MessageBoxButtons]::YesNo, + [System.Windows.Forms.MessageBoxIcon]::Information + ) + if ($restart -eq [System.Windows.Forms.DialogResult]::Yes) { + Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null + Open-NetxUiWhenReady + } else { + Update-NetxTrayTip + } + } +}) +[void]$menu.Items.Add($miReconfig) + +$miUpdate = New-NetxMenuItem -Text (T "Check for updates…" "检查更新…") +$miUpdate.add_Click({ + Start-NetxTrayDeferred { + try { + $p = Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) ` + -WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru + if (Test-NetxSetupCancelled -ExitCode $p.ExitCode) { return } + if ($p.ExitCode -eq 10) { + $ans = [System.Windows.Forms.MessageBox]::Show( + (T "A newer NetX is available. Download and apply now?" "发现新版本,是否立即下载并更新?"), + (T "NetX update" "NetX 更新"), + [System.Windows.Forms.MessageBoxButtons]::YesNo, + [System.Windows.Forms.MessageBoxIcon]::Question + ) + if ($ans -eq [System.Windows.Forms.DialogResult]::Yes) { + Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply") ` + -WorkingDirectory $prog -WindowStyle Normal | Out-Null + } + } elseif ($p.ExitCode -eq 0) { + [System.Windows.Forms.MessageBox]::Show( + (T "NetX is up to date ($ver)." "已是最新版本 ($ver)。"), + (T "NetX update" "NetX 更新") + ) + } + } catch { + [System.Windows.Forms.MessageBox]::Show( + (T "Update check failed: $($_.Exception.Message)" "检查更新失败:$($_.Exception.Message)"), + "NetX" + ) + } + } +}) +[void]$menu.Items.Add($miUpdate) + +[void]$menu.Items.Add((New-NetxMenuSeparator)) + +# --- exit --- +$miExit = New-NetxMenuItem -Text (T "Exit tray" "退出托盘") +$miExit.ToolTipText = (T "Leave NetX services running" "NetX 服务继续运行") +$miExit.add_Click({ + Close-NetxTrayMenu $notify.Visible = $false $form.Close() [System.Windows.Forms.Application]::Exit() }) +[void]$menu.Items.Add($miExit) -$miUpdate = $menu.Items.Add((T "Check for updates" "检查更新")) -$miUpdate.add_Click({ - try { - $p = Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data) ` - -WorkingDirectory $prog -WindowStyle Normal -Wait -PassThru - if ($p.ExitCode -eq 10) { - $ans = [System.Windows.Forms.MessageBox]::Show( - (T "A newer NetX is available. Download and apply now?" "发现新版本,是否立即下载并更新?"), - (T "NetX update" "NetX 更新"), - [System.Windows.Forms.MessageBoxButtons]::YesNo, - [System.Windows.Forms.MessageBoxIcon]::Question - ) - if ($ans -eq [System.Windows.Forms.DialogResult]::Yes) { - Start-NetxPowerShell -File $checkPs1 -Arguments @("-ProgramRoot", $prog, "-DataRoot", $data, "-Apply") ` - -WorkingDirectory $prog -WindowStyle Normal | Out-Null - } - } elseif ($p.ExitCode -eq 0) { - [System.Windows.Forms.MessageBox]::Show((T "NetX is up to date ($ver)." "已是最新版本 ($ver)。"), (T "NetX update" "NetX 更新")) - } - } catch { - [System.Windows.Forms.MessageBox]::Show((T "Update check failed: $($_.Exception.Message)" "检查更新失败:$($_.Exception.Message)"), "NetX") +$miStopExit = New-NetxMenuItem -Text (T "Stop and exit" "停止并退出") +$miStopExit.add_Click({ + Start-NetxTrayDeferred { + $notify.ShowBalloonTip(3000, "NetX", (T "Stopping…" "正在停止…"), [System.Windows.Forms.ToolTipIcon]::Info) + Invoke-NetxScript -File $stopPs1 -Wait | Out-Null + $notify.Visible = $false + $form.Close() + [System.Windows.Forms.Application]::Exit() + } +}) +[void]$menu.Items.Add($miStopExit) + +$notify.ContextMenuStrip = $menu +$notify.add_DoubleClick({ + if (Test-NetxApiHealthy -HostName $hostBind -Port $port -TimeoutSec 2) { + Start-Process $uiUrl + } else { + Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null + Open-NetxUiWhenReady } }) -[void]$menu.Items.Add("-") - -$miExit = $menu.Items.Add((T "Exit tray (services keep running)" "退出托盘(服务继续运行)")) -$miExit.add_Click({ - $notify.Visible = $false - $form.Close() - [System.Windows.Forms.Application]::Exit() -}) - -$notify.ContextMenuStrip = $menu -$notify.add_DoubleClick({ Start-Process $uiUrl }) +# Refresh tooltip periodically (running / stopped). +$script:tipTimer = New-Object System.Windows.Forms.Timer +$script:tipTimer.Interval = 5000 +$script:tipTimer.add_Tick({ Update-NetxTrayTip }) +$script:tipTimer.Start() $form.add_Shown({ if ($AutoUpdate) { @@ -200,7 +539,6 @@ $form.add_Shown({ -WorkingDirectory $prog -WindowStyle Hidden | Out-Null } if ($StartOnLaunch) { - # Detach start so the tray message pump stays alive; poll /health before browser. Invoke-NetxScript -File $startPs1 -ExtraArgs @("-SkipBrowser") | Out-Null $script:netxUiWaitTicks = 0 $script:netxUiWaitMax = 360 # 360 * 500ms = 180s @@ -212,6 +550,7 @@ $form.add_Shown({ $script:netxUiTimer.Stop() $script:netxUiTimer.Dispose() try { Start-Process $uiUrl } catch {} + Update-NetxTrayTip return } if ($script:netxUiWaitTicks -eq 1) { @@ -231,6 +570,7 @@ $form.add_Shown({ (T "UI not ready yet. Use tray → Open UI later, or check data\runtime\netx.err.log." "界面尚未就绪。请稍后用托盘「打开界面」,或查看 data\runtime\netx.err.log。"), [System.Windows.Forms.ToolTipIcon]::Warning ) + Update-NetxTrayTip } }) $script:netxUiTimer.Start() @@ -238,6 +578,7 @@ $form.add_Shown({ }) $form.add_FormClosing({ + try { $script:tipTimer.Stop(); $script:tipTimer.Dispose() } catch {} $notify.Visible = $false $notify.Dispose() }) diff --git a/packaging/setup_first_run.ps1 b/packaging/setup_first_run.ps1 index c865723..8c58bf5 100644 --- a/packaging/setup_first_run.ps1 +++ b/packaging/setup_first_run.ps1 @@ -25,6 +25,8 @@ 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") @@ -72,34 +74,14 @@ if (Test-Path (Join-Path $distAbs "index.html")) { $dataEnv = Get-NetxDataEnvMap -DataRoot $data foreach ($k in $dataEnv.Keys) { $values[$k] = $dataEnv[$k] } -# Fernet key for managed-NE password encryption (required to save SSH passwords). -# Priority: -CredentialSecretKey(File) > interactive prompt > existing .env > generate. +# 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 (-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 -} if ($DbMode -eq "bundled") { $pgsql = Join-Path $prog "postgres\pgsql" @@ -230,11 +212,14 @@ if ($DbMode -eq "bundled") { $ExternalDatabaseUrl = [IO.File]::ReadAllText($ExternalDatabaseUrlFile).Trim() } if (-not $ExternalDatabaseUrl) { - if ($existing.ContainsKey("NETX_DATABASE_URL") -and $existing["NETX_DATABASE_URL"]) { - $ExternalDatabaseUrl = $existing["NETX_DATABASE_URL"] - } elseif ($NonInteractive) { - throw "external_url_required" + 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):" ` @@ -243,7 +228,21 @@ if ($DbMode -eq "bundled") { Write-Host (T ` "Example: postgresql+psycopg://netx:secret@10.0.0.8:5432/netx" ` "示例: postgresql+psycopg://netx:secret@10.0.0.8:5432/netx") - $ExternalDatabaseUrl = Read-Host "NETX_DATABASE_URL" + 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) { @@ -266,29 +265,61 @@ if ($DbMode -eq "bundled") { $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) { - Write-Host "==> Probing external PostgreSQL..." + $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 + -PgHost $h -Port $po -User $u -Password $pw -Database $dbn ` + -PsqlPath $psqlProbe -OutErrFile $probeErr if ($LASTEXITCODE -ne 0) { - throw "external_db_probe_failed" + $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 "external_db_probe_failed: $ping" + 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 @@ -327,3 +358,14 @@ if (-not $NonInteractive) { } 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 +} diff --git a/pyproject.toml b/pyproject.toml index 3e59513..d16a36f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netx-ops" -version = "0.4.6" +version = "0.4.7" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" readme = "README.md" requires-python = ">=3.11"