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

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