panrouter 1.8.0 → 2.0.0

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/cli.mjs +52 -84
  2. package/package.json +1 -1
  3. package/tray-daemon.ps1 +106 -77
package/cli.mjs CHANGED
@@ -2,14 +2,6 @@
2
2
 
3
3
  /**
4
4
  * Pan Router CLI — 一键安装 + 启动
5
- *
6
- * 用法:
7
- * panrouter # 一键安装 + 前台启动
8
- * panrouter --install # 只安装配置
9
- * panrouter --server # 只启动代理(前台)
10
- * panrouter --tray # 以托盘模式启动(隐藏窗口)
11
- * panrouter --tray-install # 安装 + 配置 + 托盘启动
12
- * panrouter --help # 帮助
13
5
  */
14
6
 
15
7
  import { execSync, spawn } from "node:child_process";
@@ -48,7 +40,7 @@ function runSilent(cmd) {
48
40
  }
49
41
  }
50
42
 
51
- // ─── 1. 检查 / 安装 Claude Code ──────────────────────────────────────────
43
+ // ─── 1. 安装与配置 ──────────────────────────────────────────
52
44
 
53
45
  function installClaudeCode() {
54
46
  log("..", "正在检查 Claude Code...", "yellow");
@@ -56,7 +48,6 @@ function installClaudeCode() {
56
48
  log("OK", "Claude Code 已就绪", "green");
57
49
  return true;
58
50
  }
59
-
60
51
  log("..", "正在安装 Claude Code...", "yellow");
61
52
  if (!run("npm install -g @anthropic-ai/claude-code")) {
62
53
  log("!!", "Claude Code 安装失败", "red");
@@ -66,20 +57,10 @@ function installClaudeCode() {
66
57
  return true;
67
58
  }
68
59
 
69
- // ─── 2. 写入配置文件 ─────────────────────────────────────────────────────
70
-
71
60
  function writeConfig() {
72
61
  log("..", "正在写入配置...", "yellow");
73
-
74
- if (!fs.existsSync(CLAUDE_DIR)) {
75
- fs.mkdirSync(CLAUDE_DIR, { recursive: true });
76
- }
77
-
78
- // 备份
79
- if (fs.existsSync(SETTINGS_PATH)) {
80
- fs.copyFileSync(SETTINGS_PATH, BACKUP_PATH);
81
- log("OK", "原配置已备份", "green");
82
- }
62
+ if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
63
+ if (fs.existsSync(SETTINGS_PATH)) fs.copyFileSync(SETTINGS_PATH, BACKUP_PATH);
83
64
 
84
65
  const config = {
85
66
  env: {
@@ -91,13 +72,10 @@ function writeConfig() {
91
72
  },
92
73
  hasCompletedOnboarding: true,
93
74
  };
94
-
95
75
  fs.writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2), "utf-8");
96
- log("OK", "配置文件已写入 ~/.claude/settings.json", "green");
76
+ log("OK", "配置文件已写入", "green");
97
77
  }
98
78
 
99
- // ─── 3. 启动代理服务器(后台运行) ───────────────────────────────────────
100
-
101
79
  async function isPortOpen() {
102
80
  return new Promise(rs => {
103
81
  const req = http.get("http://127.0.0.1:50816/health", () => {});
@@ -107,10 +85,10 @@ async function isPortOpen() {
107
85
  });
108
86
  }
109
87
 
88
+ // ─── 2. 启动服务(前台黑框) ────────────────────────────────
89
+
110
90
  async function startServer() {
111
91
  const serverPath = path.join(__dirname, "server.mjs");
112
-
113
- // 关掉旧的 Pan Router
114
92
  try {
115
93
  if (process.platform === "win32") {
116
94
  execSync('taskkill /f /fi "WINDOWTITLE eq Pan Router*" >nul 2>&1', { stdio: "pipe" });
@@ -120,7 +98,6 @@ async function startServer() {
120
98
  } catch {}
121
99
 
122
100
  log("..", "正在启动 Pan Router(端口 50816)...", "yellow");
123
-
124
101
  if (process.platform === "win32") {
125
102
  execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
126
103
  } else {
@@ -128,78 +105,71 @@ async function startServer() {
128
105
  child.unref();
129
106
  }
130
107
 
131
- // 等待服务启动
132
108
  for (let i = 0; i < 15; i++) {
133
- if (await isPortOpen()) { break; }
109
+ if (await isPortOpen()) break;
134
110
  await new Promise(rs => setTimeout(rs, 1000));
135
111
  }
136
-
137
- log("OK", "Pan Router 已启动(端口 50816)", "green");
138
- console.log("\n 现在可以运行: \x1b[33mclaude \"你好\"\x1b[0m\n");
112
+ log("OK", "Pan Router 已启动!现在可以运行 claude 了", "green");
139
113
  }
140
114
 
141
- // ─── 4. 以托盘模式启动 ──────────────────────────────────────────────────
115
+ // ─── 3. 托盘模式(后台静默) ────────────────────────────────
142
116
 
143
- function startTray() {
117
+ async function startTray() {
118
+ const serverPath = path.join(__dirname, "server.mjs");
144
119
  const vbsPath = path.join(__dirname, "panrouter-tray.vbs");
145
120
 
146
- if (!fs.existsSync(vbsPath)) {
147
- log("!!", "未找到 panrouter-tray.vbs", "red");
148
- process.exit(1);
149
- }
150
-
151
- log("..", "正在以托盘模式启动 Pan Router...", "yellow");
121
+ log("..", "正在启动代理并加载托盘...", "yellow");
152
122
 
153
- // cmd /c start /B VBS 在新进程树中运行, 有 Window Station
154
- const child = spawn("cmd.exe", ["/c", "start", "/B", "wscript.exe", "//B", "//NoLogo", vbsPath], {
155
- cwd: __dirname,
156
- stdio: "ignore",
157
- windowsHide: true,
158
- detached: true,
159
- shell: false,
123
+ // 【核心修复1】:由 Node 主进程直接启动 Server,确保 API 百分百可用
124
+ const srv = spawn(process.execPath, [serverPath], {
125
+ cwd: __dirname,
126
+ stdio: "ignore",
127
+ detached: true,
128
+ windowsHide: true
160
129
  });
161
- child.unref();
130
+ srv.unref();
131
+
132
+ // 【核心修复2】:将 Node 的绝对路径通过环境变量塞给 VBS/PowerShell
133
+ process.env.PANROUTER_NODE_PATH = process.execPath;
134
+
135
+ // 启动托盘 UI
136
+ if (fs.existsSync(vbsPath)) {
137
+ const tray = spawn("wscript.exe", ["//B", "//NoLogo", vbsPath], {
138
+ cwd: __dirname,
139
+ stdio: "ignore",
140
+ windowsHide: true,
141
+ detached: true,
142
+ env: process.env // 传递环境变量
143
+ });
144
+ tray.unref();
145
+ }
162
146
 
163
- log("OK", "Pan Router 托盘已启动", "green");
164
- console.log(" 图标在右下角, 右键可退出");
147
+ // 验证端口
148
+ let ok = false;
149
+ for (let i = 0; i < 15; i++) {
150
+ if (await isPortOpen()) { ok = true; break; }
151
+ await new Promise(rs => setTimeout(rs, 1000));
152
+ }
153
+
154
+ if (ok) {
155
+ log("OK", "Pan Router 代理已在后台运行 (端口: 50816)!", "green");
156
+ console.log(" 托盘图标应该已出现。如果系统原因导致图标没出来,代理依然可用。");
157
+ } else {
158
+ log("!!", "代理启动超时,请检查端口 50816 是否被占用", "red");
159
+ }
165
160
  }
166
161
 
167
- // ─── 主流程 ──────────────────────────────────────────────────────────────
162
+ // ─── 主流程 ──────────────────────────────────────────────────
168
163
 
169
164
  function printBanner() {
170
- console.log(`
171
- \x1b[36m============================================\x1b[0m
172
- \x1b[36m Pan Router - Claude Code 一键安装\x1b[0m
173
- \x1b[36m 无需 API Key,开箱即用\x1b[0m
174
- \x1b[36m============================================\x1b[0m
175
- `);
165
+ console.log(`\n\x1b[36m=== Pan Router - Claude Code 代理 ===\x1b[0m\n`);
176
166
  }
177
167
 
178
168
  async function main() {
179
169
  const args = process.argv.slice(2);
180
170
 
181
171
  if (args.includes("--help") || args.includes("-h")) {
182
- console.log(`
183
- pan-router — Claude Code 免费 AI 路由代理
184
-
185
- \x1b[33m用法:\x1b[0m
186
- panrouter 一键安装 + 前台启动
187
- panrouter --install 只安装配置
188
- panrouter --server 只启动代理(前台窗口)
189
- panrouter --tray 以托盘模式启动(右下角隐藏图标)
190
- panrouter --tray-install 安装配置 + 托盘启动
191
- panrouter --help 显示帮助
192
-
193
- \x1b[33m托盘模式:\x1b[0m
194
- 在系统通知区(右下角)显示图标,右键菜单:
195
- - 开关 开机自启动
196
- - 退出 关闭服务器和托盘
197
- 左键点击图标查看运行状态
198
-
199
- \x1b[33m配置:\x1b[0m
200
- 代理运行在 http://127.0.0.1:50816
201
- Claude Code 自动使用,无需额外设置
202
- `);
172
+ console.log("用法:\n panrouter --server (带命令行窗口运行)\n panrouter --tray (后台隐藏运行 + 托盘)\n panrouter --tray-install (安装配置 + 托盘)");
203
173
  return;
204
174
  }
205
175
 
@@ -207,7 +177,6 @@ async function main() {
207
177
  printBanner();
208
178
  if (!installClaudeCode()) process.exit(1);
209
179
  writeConfig();
210
- console.log("\n ✓ 安装完成,运行 \x1b[33mpanrouter --server\x1b[0m 启动代理\n");
211
180
  return;
212
181
  }
213
182
 
@@ -217,7 +186,7 @@ async function main() {
217
186
  }
218
187
 
219
188
  if (args.includes("--tray") || args.includes("-t")) {
220
- startTray();
189
+ await startTray();
221
190
  return;
222
191
  }
223
192
 
@@ -225,11 +194,10 @@ async function main() {
225
194
  printBanner();
226
195
  if (!installClaudeCode()) process.exit(1);
227
196
  writeConfig();
228
- startTray();
197
+ await startTray();
229
198
  return;
230
199
  }
231
200
 
232
- // 默认:全流程
233
201
  printBanner();
234
202
  if (!installClaudeCode()) process.exit(1);
235
203
  writeConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
package/tray-daemon.ps1 CHANGED
@@ -1,124 +1,153 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Pan Router 托盘 (自包含: 启动服务器 + 托盘图标 + 菜单)
4
- ⚠ 必须从 WScript.Shell.Run (VBS) 或正常控制台启动, 不能从 detached Node 进程启动
3
+ Pan Router 托盘守护脚本 (终极稳固版)
5
4
  #>
6
5
 
6
+ $ErrorActionPreference = "SilentlyContinue"
7
+
8
+ # 增加调试日志功能(记录在系统临时目录,遇到问题能查原因)
9
+ $logFile = "$env:TEMP\panrouter_tray_debug.log"
10
+ "--- $(Get-Date -Format 'HH:mm:ss') Tray Script Started ---" | Out-File $logFile -Append
11
+
12
+ try {
13
+ Add-Type -AssemblyName System.Windows.Forms
14
+ Add-Type -AssemblyName System.Drawing
15
+ } catch {
16
+ "Failed to load assemblies: $($_.Exception.Message)" | Out-File $logFile -Append
17
+ Exit
18
+ }
19
+
7
20
  $scriptPath = $MyInvocation.MyCommand.Path
8
21
  $scriptDir = Split-Path $scriptPath -Parent
9
22
  $serverPath = Join-Path $scriptDir "server.mjs"
10
- $logFile = "$env:TEMP\panrouter-tray.log"
11
- $now = Get-Date -Format "HH:mm:ss"
12
23
 
13
- "$now === PanRouter Tray START ===" | Out-File $logFile
14
- "$now PID=$pid" | Out-File -Append $logFile
15
- "$now serverPath=$serverPath" | Out-File -Append $logFile
24
+ # 接收从 Node CLI 传来的绝对路径,如果没有则降级使用 "node"
25
+ $nodePath = $env:PANROUTER_NODE_PATH
26
+ if ([string]::IsNullOrWhiteSpace($nodePath)) {
27
+ $nodePath = "node"
28
+ }
29
+ "Node Path identified as: $nodePath" | Out-File $logFile -Append
30
+
31
+ # ─── 定义重启服务的逻辑 ────────────────────────────────────
32
+ function Restart-Server {
33
+ "Executing Restart-Server..." | Out-File $logFile -Append
34
+ try {
35
+ # 使用更为稳健的 WMI 查询关闭进程
36
+ Get-WmiObject Win32_Process -Filter "Name = 'node.exe'" |
37
+ Where-Object { $_.CommandLine -match "server\.mjs" } |
38
+ ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
39
+ } catch {}
40
+
41
+ try {
42
+ Start-Process -FilePath $nodePath -ArgumentList "`"$serverPath`"" -WorkingDirectory $scriptDir -WindowStyle Hidden
43
+ "Server process spawned successfully." | Out-File $logFile -Append
44
+ } catch {
45
+ "Server start error: $($_.Exception.Message)" | Out-File $logFile -Append
46
+ }
47
+ }
16
48
 
17
- # ─── 杀掉旧 server ──────────────────────────────
49
+ # ─── 保证服务在运行(用于应对开机自启动的情况) ───────────
50
+ $isRunning = $false
18
51
  try {
19
- $old = wmic process where "name='node.exe'" get ProcessId,CommandLine /format:csv 2>$null
20
- foreach ($line in $old) {
21
- if ($line -match '(\d+),.*?server\.mjs') {
22
- Stop-Process -Id $Matches[1] -Force -ErrorAction SilentlyContinue
23
- "$now Killed old server PID=$($Matches[1])" | Out-File -Append $logFile
52
+ $procs = Get-WmiObject Win32_Process -Filter "Name = 'node.exe'"
53
+ foreach ($p in $procs) {
54
+ if ($p.CommandLine -match "server\.mjs") {
55
+ $isRunning = $true
56
+ break
24
57
  }
25
58
  }
26
59
  } catch {}
27
60
 
28
- # ─── 启动 server.mjs ( node 完整路径) ─────────
61
+ if (-not $isRunning) {
62
+ "Server not running on load, starting..." | Out-File $logFile -Append
63
+ Restart-Server
64
+ }
65
+
66
+ # ─── 绘制托盘图标 ──────────────────────────────────────────
29
67
  try {
30
- $nodePath = (Get-Command node).Source
31
- "$now nodePath=$nodePath" | Out-File -Append $logFile
32
- $p = Start-Process -FilePath $nodePath -ArgumentList "`"$serverPath`"" -WorkingDirectory $scriptDir -WindowStyle Hidden -PassThru -NoNewWindow
33
- if ($p -and $p.Id) { "$now Server started PID=$($p.Id)" | Out-File -Append $logFile }
68
+ $bmp = New-Object System.Drawing.Bitmap(16, 16)
69
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
70
+ $g.SmoothingMode = 'HighQuality'
71
+ $g.Clear([System.Drawing.Color]::Transparent)
72
+ $brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(0, 120, 215))
73
+ $g.FillEllipse($brush, 0, 0, 15, 15)
74
+ $font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Bold)
75
+ $fg = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
76
+ $g.DrawString("P", $font, $fg, 3, 2)
77
+
78
+ $hIcon = $bmp.GetHicon()
79
+ $icon = [System.Drawing.Icon]::FromHandle($hIcon)
34
80
  } catch {
35
- "$now Server start FAILED: $_" | Out-File -Append $logFile
81
+ "Icon drawing error: $($_.Exception.Message)" | Out-File $logFile -Append
82
+ Exit
36
83
  }
37
84
 
38
- # ─── WinForms ────────────────────────────────────
39
- Add-Type -AssemblyName System.Windows.Forms
40
- Add-Type -AssemblyName System.Drawing
41
-
42
- # ─── 图标 ────────────────────────────────────────
43
- $bmp = New-Object System.Drawing.Bitmap(16, 16)
44
- $g = [System.Drawing.Graphics]::FromImage($bmp)
45
- $g.SmoothingMode = 'HighQuality'
46
- $g.Clear([System.Drawing.Color]::Transparent)
47
- $brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(0, 120, 215))
48
- $g.FillEllipse($brush, 0, 0, 15, 15)
49
- $font = New-Object System.Drawing.Font("Segoe UI", 8.5, [System.Drawing.FontStyle]::Bold)
50
- $fg = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
51
- $g.DrawString("P", $font, $fg, 3, 1.5)
52
- $font.Dispose(); $fg.Dispose(); $brush.Dispose(); $g.Dispose()
53
- $hIcon = $bmp.GetHicon()
54
- $icon = [System.Drawing.Icon]::FromHandle($hIcon)
55
- $bmp.Dispose()
56
-
57
- # ─── NotifyIcon ──────────────────────────────────
85
+ # ─── 初始化托盘菜单 ────────────────────────────────────────
58
86
  $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
59
87
  $notifyIcon.Icon = $icon
60
- $notifyIcon.Text = "Pan Router | 端口 50816"
88
+ $notifyIcon.Text = "Pan Router (端口: 50816)"
61
89
 
62
- # ─── 菜单 ────────────────────────────────────────
63
90
  $menu = New-Object System.Windows.Forms.ContextMenuStrip
64
- [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripMenuItem("Pan Router - :50816") { Enabled = $false; Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold) }))
65
- [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
91
+
92
+ $titleItem = New-Object System.Windows.Forms.ToolStripMenuItem("Pan Router | :50816")
93
+ $titleItem.Enabled = $false
94
+ $titleItem.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
95
+ $menu.Items.Add($titleItem) | Out-Null
96
+ $menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
97
+
98
+ $restartItem = New-Object System.Windows.Forms.ToolStripMenuItem("重启后台服务")
99
+ $restartItem.Add_Click({
100
+ Restart-Server
101
+ $notifyIcon.ShowBalloonTip(2000, "Pan Router", "后台代理服务已重新启动 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
102
+ })
103
+ $menu.Items.Add($restartItem) | Out-Null
66
104
 
67
105
  $autoItem = New-Object System.Windows.Forms.ToolStripMenuItem("开机自启动")
68
- try { $autoItem.Checked = (Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name PanRouter -ErrorAction Stop).PanRouter -ne $null } catch {}
106
+ try {
107
+ $regKey = Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name PanRouter
108
+ $autoItem.Checked = ($regKey -ne $null)
109
+ } catch {}
110
+
69
111
  $autoItem.Add_Click({
70
112
  if ($autoItem.Checked) {
71
113
  reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v PanRouter /f 2>&1 | Out-Null
72
114
  $autoItem.Checked = $false
73
- $notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", [System.Windows.Forms.ToolTipIcon]::Info)
74
115
  } else {
75
- reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v PanRouter /t REG_SZ /d "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File `"$scriptPath`"" /f 2>&1 | Out-Null
116
+ $vbsPath = Join-Path $scriptDir "panrouter-tray.vbs"
117
+ $cmd = "wscript.exe //B //NoLogo `"$vbsPath`""
118
+ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v PanRouter /t REG_SZ /d $cmd /f 2>&1 | Out-Null
76
119
  $autoItem.Checked = $true
77
- $notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
78
120
  }
79
- }.GetNewClosure())
80
- [void]$menu.Items.Add($autoItem)
81
- [void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
121
+ })
122
+ $menu.Items.Add($autoItem) | Out-Null
123
+ $menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) | Out-Null
82
124
 
83
125
  $exitItem = New-Object System.Windows.Forms.ToolStripMenuItem("退出")
84
126
  $exitItem.Add_Click({
85
127
  $notifyIcon.Visible = $false
86
- try { $old = wmic process where "name='node.exe'" get ProcessId,CommandLine /format:csv 2>$null; foreach ($line in $old) { if ($line -match '(\d+),.*?server\.mjs') { Stop-Process -Id $Matches[1] -Force -ErrorAction SilentlyContinue } } } catch {}
128
+ try {
129
+ Get-WmiObject Win32_Process -Filter "Name = 'node.exe'" | Where-Object { $_.CommandLine -match "server\.mjs" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
130
+ } catch {}
87
131
  [System.Windows.Forms.Application]::Exit()
88
- }.GetNewClosure())
89
- [void]$menu.Items.Add($exitItem)
132
+ })
133
+ $menu.Items.Add($exitItem) | Out-Null
90
134
 
91
135
  $notifyIcon.ContextMenuStrip = $menu
92
136
 
93
- # ─── 左键 ────────────────────────────────────────
94
137
  $notifyIcon.Add_MouseClick({
95
138
  if ($_.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
96
- try { $req = [System.Net.WebRequest]::Create("http://127.0.0.1:50816/health"); $req.Timeout = 1500; $resp = $req.GetResponse(); $resp.Close(); $notifyIcon.ShowBalloonTip(2000, "Pan Router", "运行正常 ✓ (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info) }
97
- catch { $notifyIcon.ShowBalloonTip(2000, "Pan Router", "服务器未响应 ⚠", [System.Windows.Forms.ToolTipIcon]::Error) }
98
- }
99
- })
100
-
101
- # ─── 健康检查 ────────────────────────────────────
102
- $healthTimer = New-Object System.Windows.Forms.Timer
103
- $healthTimer.Interval = 30000
104
- $healthTimer.Add_Tick({
105
- try { $req = [System.Net.WebRequest]::Create("http://127.0.0.1:50816/health"); $req.Timeout = 1500; $resp = $req.GetResponse(); $resp.Close() }
106
- catch {
107
- try { $old = wmic process where "name='node.exe'" get ProcessId,CommandLine /format:csv 2>$null; foreach ($line in $old) { if ($line -match '(\d+),.*?server\.mjs') { Stop-Process -Id $Matches[1] -Force -ErrorAction SilentlyContinue } } } catch {}
108
- try { Start-Process -FilePath (Get-Command node).Source -ArgumentList "`"$serverPath`"" -WorkingDirectory $scriptDir -WindowStyle Hidden -NoNewWindow } catch {}
139
+ $mi = [System.Windows.Forms.NotifyIcon].GetMethod("ShowContextMenu", [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance)
140
+ $mi.Invoke($notifyIcon, $null)
109
141
  }
110
142
  })
111
- $healthTimer.Start()
112
- "$now Health timer started" | Out-File -Append $logFile
113
143
 
114
- # ═══ 进入消息循环 ═══
115
144
  $notifyIcon.Visible = $true
116
- "$now Icon visible, entering message loop" | Out-File -Append $logFile
117
- [System.Windows.Forms.Application]::Run()
118
- "$now Exited message loop" | Out-File -Append $logFile
119
145
 
120
- # 清理
121
- $healthTimer.Stop()
146
+ "UI setup completed, entering ApplicationContext loop." | Out-File $logFile -Append
147
+
148
+ $appContext = New-Object System.Windows.Forms.ApplicationContext
149
+ [System.Windows.Forms.Application]::Run($appContext)
150
+
151
+ "Exiting..." | Out-File $logFile -Append
152
+ $notifyIcon.Visible = $false
122
153
  $notifyIcon.Dispose()
123
- [System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
124
- $icon.Dispose()