panrouter 1.1.0 → 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.
- package/package.json +3 -2
- package/tray-app.cs +324 -0
- package/tray-manager.ps1 +116 -155
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panrouter",
|
|
3
|
-
"version": "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,168 +5,120 @@
|
|
|
5
5
|
在通知区域(右下角)显示图标,隐藏命令窗口。
|
|
6
6
|
右键菜单: 状态, 开机自启动开关, 退出
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
powershell -STA -File tray-manager.ps1 -ServerPath "server.mjs"
|
|
8
|
+
用法(必须 STA):
|
|
9
|
+
powershell -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File tray-manager.ps1 -ServerPath "server.mjs"
|
|
10
|
+
|
|
11
|
+
注意: 不能在脚本中弹任何对话框(隐藏窗口下没人点)
|
|
10
12
|
#>
|
|
11
13
|
|
|
12
|
-
param(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
$r = [System.Windows.Forms.MessageBox]::Show(
|
|
19
|
-
"Pan Router 需要 STA 模式运行。`n是否自动以 STA 模式重新启动?",
|
|
20
|
-
"Pan Router",
|
|
21
|
-
"YesNo",
|
|
22
|
-
"Warning"
|
|
23
|
-
)
|
|
24
|
-
if ($r -eq "Yes") {
|
|
25
|
-
powershell -STA -File $MyInvocation.MyCommand.Path -ServerPath $ServerPath
|
|
26
|
-
}
|
|
27
|
-
exit 1
|
|
28
|
-
}
|
|
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 }
|
|
29
20
|
|
|
21
|
+
Log "ServerPath=$ServerPath"
|
|
22
|
+
Log "Args=$([environment]::CommandLine)"
|
|
23
|
+
|
|
24
|
+
# ─── 加载 WinForms ─────────────────────────────
|
|
30
25
|
Add-Type -AssemblyName System.Windows.Forms
|
|
31
26
|
Add-Type -AssemblyName System.Drawing
|
|
27
|
+
Log "Assemblies loaded"
|
|
32
28
|
|
|
33
29
|
# ─── 常量 ──────────────────────────────────────
|
|
34
|
-
$scriptPath = $MyInvocation.MyCommand.Path
|
|
35
30
|
$autostartName = "PanRouter"
|
|
36
31
|
$runKeyPath = "Software\Microsoft\Windows\CurrentVersion\Run"
|
|
37
|
-
|
|
38
|
-
# 取 npm 全局安装目录作为图标标题提示
|
|
39
|
-
$pkgDir = Split-Path (Split-Path $ServerPath -Parent) -Parent
|
|
40
|
-
$pkgName = Split-Path $pkgDir -Leaf
|
|
32
|
+
$appDir = Split-Path $ServerPath -Parent
|
|
41
33
|
$tooltipText = "Pan Router`n端口 50816 | 运行中"
|
|
42
34
|
|
|
43
|
-
# ───
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
$bmp.Dispose()
|
|
66
|
-
return $icon, $hIcon
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
# ─── 启动隐藏的 Node 服务器 ─────────────────────
|
|
70
|
-
function Start-ServerHidden {
|
|
71
|
-
param([string]$ServerPath)
|
|
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"
|
|
72
57
|
|
|
73
|
-
|
|
58
|
+
# ─── 启动隐藏服务器 ────────────────────────────
|
|
59
|
+
function Start-Server {
|
|
74
60
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
75
61
|
$psi.FileName = "node"
|
|
76
62
|
$psi.Arguments = "`"$ServerPath`""
|
|
77
|
-
$psi.WorkingDirectory = $
|
|
63
|
+
$psi.WorkingDirectory = $appDir
|
|
78
64
|
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
79
65
|
$psi.CreateNoWindow = $true
|
|
80
|
-
$psi.UseShellExecute = $
|
|
81
|
-
$psi.RedirectStandardOutput = $true
|
|
82
|
-
$psi.RedirectStandardError = $true
|
|
83
|
-
|
|
66
|
+
$psi.UseShellExecute = $true # 关键: 不重定向输出, 无缓冲死锁
|
|
84
67
|
try {
|
|
85
|
-
$
|
|
86
|
-
|
|
68
|
+
$p = [System.Diagnostics.Process]::Start($psi)
|
|
69
|
+
Log "Server started PID=$($p.Id)"
|
|
70
|
+
return $p
|
|
87
71
|
} catch {
|
|
72
|
+
Log "Server start FAILED: $_"
|
|
88
73
|
return $null
|
|
89
74
|
}
|
|
90
75
|
}
|
|
91
76
|
|
|
92
|
-
# ───
|
|
93
|
-
function
|
|
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
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch { Log "KillOld: $_" }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# ─── 健康检查 ──────────────────────────────────
|
|
92
|
+
function Test-Online {
|
|
94
93
|
try {
|
|
95
|
-
$req = [System.Net.
|
|
96
|
-
$req.Method = "GET"
|
|
94
|
+
$req = [System.Net.WebRequest]::Create("http://127.0.0.1:50816/health")
|
|
97
95
|
$req.Timeout = 1500
|
|
98
96
|
$resp = $req.GetResponse()
|
|
99
97
|
$resp.Close()
|
|
100
98
|
return $true
|
|
101
|
-
} catch {
|
|
102
|
-
return $false
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
# ─── 杀掉旧 Pan Router 进程 (WMI 方式, 兼容 PS5.1) ──────────
|
|
107
|
-
function Stop-OldServer {
|
|
108
|
-
try {
|
|
109
|
-
$existing = Get-CimInstance -ClassName Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue |
|
|
110
|
-
Where-Object { $_.CommandLine -match "server\.mjs" }
|
|
111
|
-
foreach ($p in $existing) {
|
|
112
|
-
$pid = $p.ProcessId
|
|
113
|
-
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
|
114
|
-
}
|
|
115
|
-
} catch {}
|
|
99
|
+
} catch { return $false }
|
|
116
100
|
}
|
|
117
101
|
|
|
118
|
-
#
|
|
119
|
-
# 主流程
|
|
120
|
-
# ═══════════════════════════════════════════════
|
|
102
|
+
# ═══════════════ 主流程 ═══════════════
|
|
121
103
|
|
|
122
|
-
# 1.
|
|
104
|
+
# 1. 杀旧 + 启动
|
|
123
105
|
Stop-OldServer
|
|
106
|
+
$serverProcess = Start-Server
|
|
124
107
|
|
|
125
|
-
# 2.
|
|
126
|
-
$
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
$
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
$
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
$serverProcess = Start-ServerHidden -ServerPath $ServerPath
|
|
136
|
-
|
|
137
|
-
# 5. 等待服务器就绪(后台检查,不阻塞 UI)
|
|
138
|
-
$checkTimer = New-Object System.Windows.Forms.Timer
|
|
139
|
-
$checkTimer.Interval = 500
|
|
140
|
-
$checkCount = 0
|
|
141
|
-
$checkTimer.Add_Tick({
|
|
142
|
-
$checkCount++
|
|
143
|
-
if (Test-ServerOnline) {
|
|
144
|
-
$checkTimer.Stop()
|
|
145
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已就绪 (端口 50816)", "Info")
|
|
146
|
-
} elseif ($checkCount -ge 10) {
|
|
147
|
-
$checkTimer.Stop()
|
|
148
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器启动可能较慢,尝试访问中...", "Warning")
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
$checkTimer.Start()
|
|
152
|
-
|
|
153
|
-
# 6. 左键点击: 弹气泡提示状态
|
|
154
|
-
$notifyIcon.Add_MouseClick({
|
|
155
|
-
param($sender, $e)
|
|
156
|
-
if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
|
|
157
|
-
$online = Test-ServerOnline
|
|
158
|
-
if ($online) {
|
|
159
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "运行正常 ✓ (端口 50816)", "Info")
|
|
160
|
-
} else {
|
|
161
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器未响应 ⚠", "Error")
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
})
|
|
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
|
+
}
|
|
165
118
|
|
|
166
|
-
#
|
|
119
|
+
# 3. 右键菜单
|
|
167
120
|
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
168
121
|
|
|
169
|
-
# 标题行
|
|
170
122
|
$titleItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
171
123
|
$titleItem.Text = "Pan Router - :50816"
|
|
172
124
|
$titleItem.Enabled = $false
|
|
@@ -174,34 +126,34 @@ $titleItem.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.
|
|
|
174
126
|
$menu.Items.Add($titleItem)
|
|
175
127
|
$menu.Items.Add("-")
|
|
176
128
|
|
|
177
|
-
# 开机自启动
|
|
178
129
|
$autoStartItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
179
130
|
$autoStartItem.Text = "开机自启动"
|
|
180
|
-
$
|
|
181
|
-
$autoStartItem.Checked = ($
|
|
182
|
-
$
|
|
131
|
+
$rk = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
|
|
132
|
+
$autoStartItem.Checked = ($rk.GetValue($autostartName) -ne $null)
|
|
133
|
+
$rk.Close()
|
|
183
134
|
$autoStartItem.Add_Click({
|
|
184
|
-
$
|
|
135
|
+
$rk2 = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
|
|
185
136
|
if ($autoStartItem.Checked) {
|
|
186
|
-
$
|
|
137
|
+
$rk2.DeleteValue($autostartName, $false)
|
|
187
138
|
$autoStartItem.Checked = $false
|
|
188
|
-
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭",
|
|
139
|
+
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
140
|
+
Log "Autostart OFF"
|
|
189
141
|
} else {
|
|
190
|
-
$cmd = "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File `"$
|
|
191
|
-
$
|
|
142
|
+
$cmd = "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File `"$($MyInvocation.MyCommand.Path)`" -ServerPath `"$ServerPath`""
|
|
143
|
+
$rk2.SetValue($autostartName, $cmd)
|
|
192
144
|
$autoStartItem.Checked = $true
|
|
193
|
-
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓",
|
|
145
|
+
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
146
|
+
Log "Autostart ON"
|
|
194
147
|
}
|
|
195
|
-
$
|
|
148
|
+
$rk2.Close()
|
|
196
149
|
})
|
|
197
150
|
$menu.Items.Add($autoStartItem)
|
|
198
|
-
|
|
199
151
|
$menu.Items.Add("-")
|
|
200
152
|
|
|
201
|
-
# 退出
|
|
202
153
|
$exitItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
203
154
|
$exitItem.Text = "退出"
|
|
204
155
|
$exitItem.Add_Click({
|
|
156
|
+
Log "Exit clicked"
|
|
205
157
|
$notifyIcon.Visible = $false
|
|
206
158
|
[System.Windows.Forms.Application]::Exit()
|
|
207
159
|
})
|
|
@@ -209,29 +161,38 @@ $menu.Items.Add($exitItem)
|
|
|
209
161
|
|
|
210
162
|
$notifyIcon.ContextMenuStrip = $menu
|
|
211
163
|
|
|
212
|
-
#
|
|
213
|
-
$
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
$
|
|
219
|
-
$serverProcess.Dispose()
|
|
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)
|
|
169
|
+
} else {
|
|
170
|
+
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "服务器未响应 ⚠", [System.Windows.Forms.ToolTipIcon]::Error)
|
|
220
171
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
} catch {}
|
|
224
|
-
}
|
|
172
|
+
}
|
|
173
|
+
})
|
|
225
174
|
|
|
175
|
+
# 5. 退出清理
|
|
226
176
|
[System.Windows.Forms.Application]::ApplicationExit += {
|
|
227
|
-
|
|
177
|
+
Log "AppExit cleanup"
|
|
178
|
+
if ($serverProcess -and !$serverProcess.HasExited) {
|
|
179
|
+
$serverProcess.Kill()
|
|
180
|
+
$serverProcess.WaitForExit(3000)
|
|
181
|
+
Log "Server killed"
|
|
182
|
+
}
|
|
183
|
+
Stop-OldServer
|
|
228
184
|
$notifyIcon.Dispose()
|
|
229
185
|
[System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
|
|
230
186
|
$icon.Dispose()
|
|
231
187
|
}
|
|
232
188
|
|
|
233
|
-
#
|
|
189
|
+
# 6. 运行消息循环
|
|
190
|
+
Log "Entering message loop"
|
|
234
191
|
[System.Windows.Forms.Application]::Run()
|
|
192
|
+
Log "Message loop exited"
|
|
235
193
|
|
|
236
|
-
#
|
|
237
|
-
|
|
194
|
+
# 7. 循环结束后再清理一次
|
|
195
|
+
try {
|
|
196
|
+
if ($serverProcess -and !$serverProcess.HasExited) { $serverProcess.Kill() }
|
|
197
|
+
Stop-OldServer
|
|
198
|
+
} catch {}
|