Release 0.4.7: ProgramData ACL fix and tray menu polish.
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>
This commit is contained in:
oliver 2026-10-07 16:43:28 +08:00
parent 5429c07342
commit 4e582dc4a5
6 changed files with 583 additions and 131 deletions

View file

@ -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 {

View file

@ -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);

View file

@ -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
}

View file

@ -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()
})

View file

@ -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
}