panrouter 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/package.json +3 -2
  2. package/tray-app.cs +324 -0
  3. package/tray-manager.ps1 +176 -201
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "files": [
10
10
  "cli.mjs",
11
11
  "server.mjs",
12
- "tray-manager.ps1"
12
+ "tray-manager.ps1",
13
+ "tray-app.cs"
13
14
  ],
14
15
  "license": "MIT"
15
16
  }
package/tray-app.cs ADDED
@@ -0,0 +1,324 @@
1
+ /* Pan Router Tray App
2
+ * 编译: csc /nologo /target:winexe /reference:System.Windows.Forms.dll /reference:System.Drawing.dll tray-app.cs
3
+ *
4
+ * 功能: 以系统托盘方式运行 panrouter 服务器, 无窗口
5
+ * 右键菜单: 状态, 开机自启动开关, 退出
6
+ */
7
+
8
+ using System;
9
+ using System.Diagnostics;
10
+ using System.Drawing;
11
+ using System.IO;
12
+ using System.Net;
13
+ using System.Reflection;
14
+ using System.Runtime.InteropServices;
15
+ using System.Text;
16
+ using System.Threading;
17
+ using System.Windows.Forms;
18
+
19
+ public class PanRouterTrayApp
20
+ {
21
+ private static NotifyIcon _notifyIcon;
22
+ private static Process _serverProcess;
23
+ private static string _serverPath;
24
+ private static string _appDir;
25
+ private static System.Threading.Timer _healthTimer;
26
+ private static StreamWriter _log;
27
+ private static readonly string _logPath = Path.Combine(Path.GetTempPath(), "panrouter-tray.log");
28
+
29
+ // ─── Win32 API: 隐藏窗口 ──────────────────────
30
+ [DllImport("user32.dll")]
31
+ private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
32
+ private const int SW_HIDE = 0;
33
+
34
+ [STAThread]
35
+ public static void Main(string[] args)
36
+ {
37
+ // Parse args
38
+ foreach (var a in args)
39
+ {
40
+ if (a.StartsWith("--serverPath:", StringComparison.OrdinalIgnoreCase))
41
+ _serverPath = a.Substring("--serverPath:".Length).Trim('"');
42
+ }
43
+ if (string.IsNullOrEmpty(_serverPath) || !File.Exists(_serverPath))
44
+ {
45
+ MessageBox.Show("Pan Router: 找不到 server.mjs", "Pan Router", MessageBoxButtons.OK, MessageBoxIcon.Error);
46
+ return;
47
+ }
48
+ _appDir = Path.GetDirectoryName(_serverPath);
49
+
50
+ // Open log
51
+ try
52
+ {
53
+ _log = new StreamWriter(_logPath, false, Encoding.UTF8);
54
+ _log.AutoFlush = true;
55
+ Log("=== PanRouter Tray App v1 ===");
56
+ Log("ServerPath: " + _serverPath);
57
+ }
58
+ catch { }
59
+
60
+ // Kill old servers
61
+ KillOldServers();
62
+
63
+ // Start hidden server
64
+ StartServer();
65
+
66
+ // Wait for ready
67
+ bool ready = false;
68
+ for (int i = 0; i < 20; i++)
69
+ {
70
+ Thread.Sleep(500);
71
+ if (IsOnline()) { ready = true; break; }
72
+ }
73
+ Log("Server ready=" + ready);
74
+
75
+ // Create tray icon
76
+ CreateTrayIcon();
77
+
78
+ // Show balloon
79
+ if (ready)
80
+ _notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已就绪 (端口 50816)", ToolTipIcon.Info);
81
+ else
82
+ _notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器启动异常,查看日志", ToolTipIcon.Warning);
83
+
84
+ // Health check every 30s
85
+ _healthTimer = new System.Threading.Timer(HealthCheck, null, 30000, 30000);
86
+
87
+ // Message loop
88
+ Application.Run();
89
+ }
90
+
91
+ private static void Log(string msg)
92
+ {
93
+ try { _log?.WriteLine(DateTime.Now.ToString("HH:mm:ss") + " " + msg); } catch { }
94
+ }
95
+
96
+ // ─── 杀掉旧服务器 ────────────────────────────
97
+ private static void KillOldServers()
98
+ {
99
+ try
100
+ {
101
+ // Use WMIC via command line to find node.exe processes running server.mjs
102
+ var psi = new ProcessStartInfo("wmic", "process where \"name='node.exe'\" get ProcessId,CommandLine /format:csv")
103
+ {
104
+ CreateNoWindow = true,
105
+ WindowStyle = ProcessWindowStyle.Hidden,
106
+ UseShellExecute = false,
107
+ RedirectStandardOutput = true,
108
+ RedirectStandardError = true
109
+ };
110
+ using (var p = Process.Start(psi))
111
+ {
112
+ string output = p.StandardOutput.ReadToEnd();
113
+ p.WaitForExit(2000);
114
+
115
+ foreach (string line in output.Split('\n'))
116
+ {
117
+ if (line.Contains("server.mjs"))
118
+ {
119
+ string[] parts = line.Split(',');
120
+ if (parts.Length >= 3 && int.TryParse(parts[2].Trim(), out int pid))
121
+ {
122
+ Log("Killing old server PID=" + pid);
123
+ try { Process.GetProcessById(pid)?.Kill(); } catch { }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ catch (Exception ex) { Log("KillOld error: " + ex.Message); }
130
+ }
131
+
132
+ // ─── 启动隐藏服务器 ───────────────────────────
133
+ private static void StartServer()
134
+ {
135
+ try
136
+ {
137
+ _serverProcess = new Process();
138
+ _serverProcess.StartInfo.FileName = "node";
139
+ _serverProcess.StartInfo.Arguments = "\"" + _serverPath + "\"";
140
+ _serverProcess.StartInfo.WorkingDirectory = _appDir;
141
+ _serverProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
142
+ _serverProcess.StartInfo.CreateNoWindow = true;
143
+ _serverProcess.StartInfo.UseShellExecute = false;
144
+ // Read output so buffer never blocks
145
+ _serverProcess.StartInfo.RedirectStandardOutput = true;
146
+ _serverProcess.StartInfo.RedirectStandardError = true;
147
+ _serverProcess.OutputDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) Log("[srv] " + e.Data); };
148
+ _serverProcess.ErrorDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) Log("[srv-err] " + e.Data); };
149
+ _serverProcess.Start();
150
+ _serverProcess.BeginOutputReadLine();
151
+ _serverProcess.BeginErrorReadLine();
152
+ Log("Server started PID=" + _serverProcess.Id);
153
+ }
154
+ catch (Exception ex)
155
+ {
156
+ Log("StartServer FAILED: " + ex.Message);
157
+ }
158
+ }
159
+
160
+ // ─── 健康检查 ────────────────────────────────
161
+ private static bool IsOnline()
162
+ {
163
+ try
164
+ {
165
+ var req = WebRequest.CreateHttp("http://127.0.0.1:50816/health");
166
+ req.Timeout = 1500;
167
+ using (var resp = req.GetResponse()) { return true; }
168
+ }
169
+ catch { return false; }
170
+ }
171
+
172
+ private static void HealthCheck(object state)
173
+ {
174
+ try
175
+ {
176
+ if (!IsOnline())
177
+ {
178
+ Log("Health check FAILED, restarting...");
179
+ KillOldServers();
180
+ StartServer();
181
+ Thread.Sleep(3000);
182
+ if (IsOnline())
183
+ _notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已自动重启 ✓", ToolTipIcon.Info);
184
+ else
185
+ _notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器重启失败 ⚠", ToolTipIcon.Error);
186
+ }
187
+ }
188
+ catch (Exception ex) { Log("HealthCheck error: " + ex.Message); }
189
+ }
190
+
191
+ // ─── 创建托盘图标 ────────────────────────────
192
+ private static void CreateTrayIcon()
193
+ {
194
+ var bmp = new Bitmap(16, 16);
195
+ using (var g = Graphics.FromImage(bmp))
196
+ {
197
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
198
+ g.Clear(Color.Transparent);
199
+ using (var brush = new SolidBrush(Color.FromArgb(0, 120, 215)))
200
+ g.FillEllipse(brush, 0, 0, 15, 15);
201
+ using (var font = new Font("Segoe UI", 8.5f, FontStyle.Bold))
202
+ using (var fg = new SolidBrush(Color.White))
203
+ g.DrawString("P", font, fg, 3, 1.5f);
204
+ }
205
+ var icon = Icon.FromHandle(bmp.GetHicon());
206
+
207
+ _notifyIcon = new NotifyIcon();
208
+ _notifyIcon.Icon = icon;
209
+ _notifyIcon.Text = "Pan Router\n端口 50816 | 运行中";
210
+ _notifyIcon.Visible = true;
211
+ Log("Tray icon created");
212
+
213
+ // Left click: balloon status
214
+ _notifyIcon.MouseClick += (s, e) =>
215
+ {
216
+ if (e.Button == MouseButtons.Left)
217
+ {
218
+ bool online = IsOnline();
219
+ _notifyIcon.ShowBalloonTip(2000, "Pan Router",
220
+ online ? "运行正常 ✓ (端口 50816)" : "服务器未响应 ⚠",
221
+ online ? ToolTipIcon.Info : ToolTipIcon.Error);
222
+ }
223
+ };
224
+
225
+ // Right-click menu
226
+ var menu = new ContextMenuStrip();
227
+
228
+ var titleItem = new ToolStripMenuItem("Pan Router - :50816") { Enabled = false };
229
+ titleItem.Font = new Font("Segoe UI", 9, FontStyle.Bold);
230
+ menu.Items.Add(titleItem);
231
+ menu.Items.Add(new ToolStripSeparator());
232
+
233
+ // Autostart
234
+ var autoItem = new ToolStripMenuItem("开机自启动");
235
+ autoItem.Checked = IsAutostart();
236
+ autoItem.Click += (s, e) =>
237
+ {
238
+ if (autoItem.Checked)
239
+ {
240
+ SetAutostart(false);
241
+ autoItem.Checked = false;
242
+ _notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", ToolTipIcon.Info);
243
+ Log("Autostart OFF");
244
+ }
245
+ else
246
+ {
247
+ SetAutostart(true);
248
+ autoItem.Checked = true;
249
+ _notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", ToolTipIcon.Info);
250
+ Log("Autostart ON");
251
+ }
252
+ };
253
+ menu.Items.Add(autoItem);
254
+ menu.Items.Add(new ToolStripSeparator());
255
+
256
+ // Exit
257
+ var exitItem = new ToolStripMenuItem("退出");
258
+ exitItem.Click += (s, e) =>
259
+ {
260
+ Log("Exit clicked");
261
+ Cleanup();
262
+ Application.Exit();
263
+ };
264
+ menu.Items.Add(exitItem);
265
+
266
+ _notifyIcon.ContextMenuStrip = menu;
267
+
268
+ // Cleanup on exit
269
+ Application.ApplicationExit += (s, e) => { Cleanup(); bmp.Dispose(); };
270
+ }
271
+
272
+ // ─── 开机自启动 ──────────────────────────────
273
+ private static bool IsAutostart()
274
+ {
275
+ try
276
+ {
277
+ using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run"))
278
+ return key?.GetValue("PanRouter") != null;
279
+ }
280
+ catch { return false; }
281
+ }
282
+
283
+ private static void SetAutostart(bool enable)
284
+ {
285
+ try
286
+ {
287
+ using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true))
288
+ {
289
+ if (enable)
290
+ {
291
+ var exePath = Assembly.GetExecutingAssembly().Location;
292
+ key.SetValue("PanRouter",
293
+ $"\"{exePath}\" --serverPath:\"{_serverPath}\"");
294
+ }
295
+ else
296
+ {
297
+ key.DeleteValue("PanRouter", false);
298
+ }
299
+ }
300
+ }
301
+ catch (Exception ex) { Log("Autostart error: " + ex.Message); }
302
+ }
303
+
304
+ // ─── 退出清理 ────────────────────────────────
305
+ private static void Cleanup()
306
+ {
307
+ try
308
+ {
309
+ Log("Cleanup starting...");
310
+ _healthTimer?.Dispose();
311
+ if (_serverProcess != null && !_serverProcess.HasExited)
312
+ {
313
+ Log("Killing server PID=" + _serverProcess.Id);
314
+ _serverProcess.Kill();
315
+ _serverProcess.WaitForExit(3000);
316
+ _serverProcess.Dispose();
317
+ Log("Server killed");
318
+ }
319
+ KillOldServers();
320
+ try { _log?.Close(); } catch { }
321
+ }
322
+ catch (Exception ex) { try { _log?.WriteLine("Cleanup error: " + ex.Message); } catch { } }
323
+ }
324
+ }
package/tray-manager.ps1 CHANGED
@@ -5,219 +5,194 @@
5
5
  在通知区域(右下角)显示图标,隐藏命令窗口。
6
6
  右键菜单: 状态, 开机自启动开关, 退出
7
7
 
8
- 用法:
9
- powershell -ExecutionPolicy Bypass -STA -File tray-manager.ps1 -ServerPath "server.mjs"
10
- #>
11
-
12
- param(
13
- [string]$ServerPath
14
- )
8
+ 用法(必须 STA):
9
+ powershell -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File tray-manager.ps1 -ServerPath "server.mjs"
15
10
 
16
- # ─── 日志辅助 ──────────────────────────────────
17
- $logFile = "$env:TEMP\panrouter-tray.log"
18
- function Write-Log { param([string]$m) "$([DateTime]::Now.ToString('HH:mm:ss')) $m" | Out-File -Append -Encoding utf8 $logFile }
11
+ 注意: 不能在脚本中弹任何对话框(隐藏窗口下没人点)
12
+ #>
19
13
 
20
- try {
21
- Write-Log "==== Pan Router Tray Started ===="
22
- Write-Log "ServerPath: $ServerPath"
23
- Write-Log "PSVersion: $($PSVersionTable.PSVersion)"
24
-
25
- Add-Type -AssemblyName System.Windows.Forms
26
- Add-Type -AssemblyName System.Drawing
27
-
28
- # ─── 常量 ──────────────────────────────────
29
- $scriptPath = $MyInvocation.MyCommand.Path
30
- $autostartName = "PanRouter"
31
- $runKeyPath = "Software\Microsoft\Windows\CurrentVersion\Run"
32
-
33
- # ─── 生成图标 ──────────────────────────────
34
- $bmp = New-Object System.Drawing.Bitmap(16, 16)
35
- $g = [System.Drawing.Graphics]::FromImage($bmp)
36
- $g.SmoothingMode = 'HighQuality'
37
- $g.Clear([System.Drawing.Color]::Transparent)
38
- $brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(0, 120, 215))
39
- $g.FillEllipse($brush, 0, 0, 15, 15)
40
- $font = New-Object System.Drawing.Font("Segoe UI", 8.5, [System.Drawing.FontStyle]::Bold)
41
- $fg = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
42
- $g.DrawString("P", $font, $fg, 3, 1.5)
43
- $font.Dispose(); $fg.Dispose(); $brush.Dispose(); $g.Dispose()
44
- $hIcon = $bmp.GetHicon()
45
- $icon = [System.Drawing.Icon]::FromHandle($hIcon)
46
- $bmp.Dispose()
47
- Write-Log "Icon created"
48
-
49
- # ─── NotifyIcon ────────────────────────────
50
- $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
51
- $notifyIcon.Icon = $icon
52
- $notifyIcon.Text = "Pan Router`n端口 50816 | 运行中"
53
- $notifyIcon.Visible = $true
54
- Write-Log "NotifyIcon created and visible"
55
-
56
- # ─── 用 UseShellExecute 启动服务器(避免输出缓冲死锁) ──
57
- function Start-ServerNohup {
58
- param([string]$ServerPath)
59
- $serverDir = Split-Path $ServerPath -Parent
60
- try {
61
- $psi = New-Object System.Diagnostics.ProcessStartInfo
62
- $psi.FileName = "node"
63
- $psi.Arguments = "`"$ServerPath`""
64
- $psi.WorkingDirectory = $serverDir
65
- $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
66
- $psi.CreateNoWindow = $true
67
- # UseShellExecute=$true 避免重定向缓冲死锁
68
- $psi.UseShellExecute = $true
69
- $proc = [System.Diagnostics.Process]::Start($psi)
70
- Write-Log "Server started PID=$($proc.Id)"
71
- return $proc
72
- } catch {
73
- Write-Log "Server start FAILED: $_"
74
- return $null
75
- }
14
+ param([string]$ServerPath)
15
+
16
+ # ─── 启动即写日志 ──────────────────────────────
17
+ $logPath = "$env:TEMP\panrouter-tray.log"
18
+ "===" + (Get-Date -Format "HH:mm:ss") + " PanRouter Tray===" | Out-File $logPath
19
+ function Log { param($m) (Get-Date -Format "HH:mm:ss") + " " + $m | Out-File -Append $logPath }
20
+
21
+ Log "ServerPath=$ServerPath"
22
+ Log "Args=$([environment]::CommandLine)"
23
+
24
+ # ─── 加载 WinForms ─────────────────────────────
25
+ Add-Type -AssemblyName System.Windows.Forms
26
+ Add-Type -AssemblyName System.Drawing
27
+ Log "Assemblies loaded"
28
+
29
+ # ─── 常量 ──────────────────────────────────────
30
+ $autostartName = "PanRouter"
31
+ $runKeyPath = "Software\Microsoft\Windows\CurrentVersion\Run"
32
+ $appDir = Split-Path $ServerPath -Parent
33
+ $tooltipText = "Pan Router`n端口 50816 | 运行中"
34
+
35
+ # ─── 生成图标 ──────────────────────────────────
36
+ $bmp = New-Object System.Drawing.Bitmap(16, 16)
37
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
38
+ $g.SmoothingMode = 'HighQuality'
39
+ $g.Clear([System.Drawing.Color]::Transparent)
40
+ $brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(0, 120, 215))
41
+ $g.FillEllipse($brush, 2, 2, 12, 12)
42
+ $font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Bold)
43
+ $fg = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
44
+ $g.DrawString("P", $font, $fg, 4, 2.5)
45
+ $font.Dispose(); $fg.Dispose(); $brush.Dispose(); $g.Dispose()
46
+ $hIcon = $bmp.GetHicon()
47
+ $icon = [System.Drawing.Icon]::FromHandle($hIcon)
48
+ $bmp.Dispose()
49
+ Log "Icon generated"
50
+
51
+ # ─── NotifyIcon ────────────────────────────────
52
+ $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
53
+ $notifyIcon.Icon = $icon
54
+ $notifyIcon.Text = $tooltipText
55
+ $notifyIcon.Visible = $true
56
+ Log "NotifyIcon visible"
57
+
58
+ # ─── 启动隐藏服务器 ────────────────────────────
59
+ function Start-Server {
60
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
61
+ $psi.FileName = "node"
62
+ $psi.Arguments = "`"$ServerPath`""
63
+ $psi.WorkingDirectory = $appDir
64
+ $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
65
+ $psi.CreateNoWindow = $true
66
+ $psi.UseShellExecute = $true # 关键: 不重定向输出, 无缓冲死锁
67
+ try {
68
+ $p = [System.Diagnostics.Process]::Start($psi)
69
+ Log "Server started PID=$($p.Id)"
70
+ return $p
71
+ } catch {
72
+ Log "Server start FAILED: $_"
73
+ return $null
76
74
  }
75
+ }
77
76
 
78
- # ─── 杀掉旧进程 (wmic 兼容 PS5.1) ──────────
79
- function Stop-OldServer {
80
- try {
81
- $old = Get-WmiObject Win32_Process -Filter "Name='node.exe'" -ErrorAction Stop |
82
- Where-Object { $_.CommandLine -match "server\.mjs" }
83
- foreach ($p in $old) {
84
- Write-Log "Killing old server PID=$($p.ProcessId)"
85
- Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
77
+ # ─── 杀掉旧服务器 (wmic, 不依赖 CIM/WMI 版本)
78
+ function Stop-OldServer {
79
+ try {
80
+ $wmicOut = & wmic process where "name='node.exe'" get ProcessId,CommandLine /format:csv 2>$null
81
+ foreach ($line in $wmicOut) {
82
+ if ($line -match '(\d+),.*?server\.mjs') {
83
+ $pid = $Matches[1]
84
+ Log "Kill old PID=$pid"
85
+ Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
86
86
  }
87
- } catch { Write-Log "Stop-OldServer: $_" }
88
- }
87
+ }
88
+ } catch { Log "KillOld: $_" }
89
+ }
89
90
 
90
- # ─── 健康检查 ──────────────────────────────
91
- function Test-Online {
92
- try {
93
- $req = [System.Net.WebRequest]::Create("http://127.0.0.1:50816/health")
94
- $req.Timeout = 1500
95
- $resp = $req.GetResponse()
96
- $resp.Close()
97
- return $true
98
- } catch { return $false }
99
- }
91
+ # ─── 健康检查 ──────────────────────────────────
92
+ function Test-Online {
93
+ try {
94
+ $req = [System.Net.WebRequest]::Create("http://127.0.0.1:50816/health")
95
+ $req.Timeout = 1500
96
+ $resp = $req.GetResponse()
97
+ $resp.Close()
98
+ return $true
99
+ } catch { return $false }
100
+ }
100
101
 
101
- # ─── 启动流程 ──────────────────────────────
102
- Stop-OldServer
103
- $serverProcess = Start-ServerNohup -ServerPath $ServerPath
102
+ # ═══════════════ 主流程 ═══════════════
104
103
 
105
- # 前台等待服务器就绪(超时 10 秒)
106
- $ready = $false
107
- for ($i = 0; $i -lt 20; $i++) {
108
- Start-Sleep -Milliseconds 500
109
- if (Test-Online) { $ready = $true; break }
110
- }
111
- Write-Log "Server ready=$ready"
112
- if ($ready) {
113
- $notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已就绪 (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
114
- }
104
+ # 1. 杀旧 + 启动
105
+ Stop-OldServer
106
+ $serverProcess = Start-Server
115
107
 
116
- # ─── 后台定时健康检查 ──────────────────────
117
- $healthTimer = New-Object System.Windows.Forms.Timer
118
- $healthTimer.Interval = 30000
119
- $healthTimer.Add_Tick({
120
- if (-not (Test-Online)) {
121
- Write-Log "Health check FAILED, restarting..."
122
- Stop-OldServer
123
- $script:serverProcess = Start-ServerNohup -ServerPath $ServerPath
124
- Start-Sleep -Seconds 3
125
- if (Test-Online) {
126
- $notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已自动重启", [System.Windows.Forms.ToolTipIcon]::Info)
127
- }
128
- }
129
- })
130
- $healthTimer.Start()
131
- Write-Log "Health timer started"
132
-
133
- # ─── 左键: 状态气泡 ────────────────────────
134
- $notifyIcon.Add_MouseClick({
135
- if ($_.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
136
- $online = Test-Online
137
- if ($online) {
138
- $notifyIcon.ShowBalloonTip(2000, "Pan Router", "运行正常 ✓ (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
139
- } else {
140
- $notifyIcon.ShowBalloonTip(2000, "Pan Router", "服务器未响应 ⚠", [System.Windows.Forms.ToolTipIcon]::Error)
141
- }
142
- }
143
- })
144
-
145
- # ─── 右键菜单 ──────────────────────────────
146
- $menu = New-Object System.Windows.Forms.ContextMenuStrip
147
-
148
- $titleItem = New-Object System.Windows.Forms.ToolStripMenuItem
149
- $titleItem.Text = "Pan Router - :50816"
150
- $titleItem.Enabled = $false
151
- $titleItem.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
152
- $menu.Items.Add($titleItem)
153
- $menu.Items.Add("-")
154
-
155
- $autoStartItem = New-Object System.Windows.Forms.ToolStripMenuItem
156
- $autoStartItem.Text = "开机自启动"
157
- $rk = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
158
- $autoStartItem.Checked = ($rk.GetValue($autostartName) -ne $null)
159
- $rk.Close()
160
- $autoStartItem.Add_Click({
161
- $rk2 = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
162
- if ($autoStartItem.Checked) {
163
- $rk2.DeleteValue($autostartName, $false)
164
- $autoStartItem.Checked = $false
165
- $notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", [System.Windows.Forms.ToolTipIcon]::Info)
166
- Write-Log "Autostart OFF"
108
+ # 2. 前台等待就绪 (10秒超时)
109
+ $ready = $false
110
+ for ($i = 0; $i -lt 20; $i++) {
111
+ Start-Sleep -Milliseconds 500
112
+ if (Test-Online) { $ready = $true; break }
113
+ }
114
+ Log "Server ready=$ready"
115
+ if ($ready) {
116
+ $notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已就绪 (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
117
+ }
118
+
119
+ # 3. 右键菜单
120
+ $menu = New-Object System.Windows.Forms.ContextMenuStrip
121
+
122
+ $titleItem = New-Object System.Windows.Forms.ToolStripMenuItem
123
+ $titleItem.Text = "Pan Router - :50816"
124
+ $titleItem.Enabled = $false
125
+ $titleItem.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
126
+ $menu.Items.Add($titleItem)
127
+ $menu.Items.Add("-")
128
+
129
+ $autoStartItem = New-Object System.Windows.Forms.ToolStripMenuItem
130
+ $autoStartItem.Text = "开机自启动"
131
+ $rk = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
132
+ $autoStartItem.Checked = ($rk.GetValue($autostartName) -ne $null)
133
+ $rk.Close()
134
+ $autoStartItem.Add_Click({
135
+ $rk2 = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
136
+ if ($autoStartItem.Checked) {
137
+ $rk2.DeleteValue($autostartName, $false)
138
+ $autoStartItem.Checked = $false
139
+ $notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", [System.Windows.Forms.ToolTipIcon]::Info)
140
+ Log "Autostart OFF"
141
+ } else {
142
+ $cmd = "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File `"$($MyInvocation.MyCommand.Path)`" -ServerPath `"$ServerPath`""
143
+ $rk2.SetValue($autostartName, $cmd)
144
+ $autoStartItem.Checked = $true
145
+ $notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
146
+ Log "Autostart ON"
147
+ }
148
+ $rk2.Close()
149
+ })
150
+ $menu.Items.Add($autoStartItem)
151
+ $menu.Items.Add("-")
152
+
153
+ $exitItem = New-Object System.Windows.Forms.ToolStripMenuItem
154
+ $exitItem.Text = "退出"
155
+ $exitItem.Add_Click({
156
+ Log "Exit clicked"
157
+ $notifyIcon.Visible = $false
158
+ [System.Windows.Forms.Application]::Exit()
159
+ })
160
+ $menu.Items.Add($exitItem)
161
+
162
+ $notifyIcon.ContextMenuStrip = $menu
163
+
164
+ # 4. 左键: 状态
165
+ $notifyIcon.Add_MouseClick({
166
+ if ($_.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
167
+ if (Test-Online) {
168
+ $notifyIcon.ShowBalloonTip(2000, "Pan Router", "运行正常 ✓ (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
167
169
  } else {
168
- $cmd = "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File `"$scriptPath`" -ServerPath `"$ServerPath`""
169
- $rk2.SetValue($autostartName, $cmd)
170
- $autoStartItem.Checked = $true
171
- $notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
172
- Write-Log "Autostart ON"
170
+ $notifyIcon.ShowBalloonTip(2000, "Pan Router", "服务器未响应 ", [System.Windows.Forms.ToolTipIcon]::Error)
173
171
  }
174
- $rk2.Close()
175
- })
176
- $menu.Items.Add($autoStartItem)
177
- $menu.Items.Add("-")
178
-
179
- $exitItem = New-Object System.Windows.Forms.ToolStripMenuItem
180
- $exitItem.Text = "退出"
181
- $exitItem.Add_Click({
182
- Write-Log "Exit clicked"
183
- $notifyIcon.Visible = $false
184
- [System.Windows.Forms.Application]::Exit()
185
- })
186
- $menu.Items.Add($exitItem)
187
-
188
- $notifyIcon.ContextMenuStrip = $menu
189
-
190
- # ─── 退出清理 ──────────────────────────────
191
- $cleanup = {
192
- try {
193
- Write-Log "Cleanup started"
194
- $healthTimer.Stop()
195
- $healthTimer.Dispose()
196
- if (-not $serverProcess.HasExited) {
197
- $serverProcess.Kill()
198
- $serverProcess.WaitForExit(3000)
199
- $serverProcess.Dispose()
200
- Write-Log "Server killed"
201
- }
202
- Stop-OldServer
203
- Write-Log "Cleanup done"
204
- } catch { Write-Log "Cleanup error: $_" }
205
172
  }
206
-
207
- [System.Windows.Forms.Application]::ApplicationExit += {
208
- & $cleanup
209
- $notifyIcon.Dispose()
210
- [System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
211
- $icon.Dispose()
173
+ })
174
+
175
+ # 5. 退出清理
176
+ [System.Windows.Forms.Application]::ApplicationExit += {
177
+ Log "AppExit cleanup"
178
+ if ($serverProcess -and !$serverProcess.HasExited) {
179
+ $serverProcess.Kill()
180
+ $serverProcess.WaitForExit(3000)
181
+ Log "Server killed"
212
182
  }
183
+ Stop-OldServer
184
+ $notifyIcon.Dispose()
185
+ [System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
186
+ $icon.Dispose()
187
+ }
213
188
 
214
- Write-Log "Entering message loop"
215
- [System.Windows.Forms.Application]::Run()
216
- Write-Log "Message loop exited"
217
- & $cleanup
189
+ # 6. 运行消息循环
190
+ Log "Entering message loop"
191
+ [System.Windows.Forms.Application]::Run()
192
+ Log "Message loop exited"
218
193
 
219
- } catch {
220
- $errMsg = "UNHANDLED ERROR: $_"
221
- Write-Log $errMsg
222
- try { [System.Windows.Forms.MessageBox]::Show($errMsg, "Pan Router Error") } catch {}
223
- }
194
+ # 7. 循环结束后再清理一次
195
+ try {
196
+ if ($serverProcess -and !$serverProcess.HasExited) { $serverProcess.Kill() }
197
+ Stop-OldServer
198
+ } catch {}