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.
- package/cli.mjs +52 -84
- package/package.json +1 -1
- 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.
|
|
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 (
|
|
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", "配置文件已写入
|
|
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())
|
|
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
|
-
// ───
|
|
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
|
-
|
|
147
|
-
log("!!", "未找到 panrouter-tray.vbs", "red");
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
log("..", "正在以托盘模式启动 Pan Router...", "yellow");
|
|
121
|
+
log("..", "正在启动代理并加载托盘...", "yellow");
|
|
152
122
|
|
|
153
|
-
//
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
# ───
|
|
49
|
+
# ─── 保证服务在运行(用于应对开机自启动的情况) ───────────
|
|
50
|
+
$isRunning = $false
|
|
18
51
|
try {
|
|
19
|
-
$
|
|
20
|
-
foreach ($
|
|
21
|
-
if ($
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
$
|
|
31
|
-
|
|
32
|
-
$
|
|
33
|
-
|
|
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
|
-
"
|
|
81
|
+
"Icon drawing error: $($_.Exception.Message)" | Out-File $logFile -Append
|
|
82
|
+
Exit
|
|
36
83
|
}
|
|
37
84
|
|
|
38
|
-
# ───
|
|
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
|
|
88
|
+
$notifyIcon.Text = "Pan Router (端口: 50816)"
|
|
61
89
|
|
|
62
|
-
# ─── 菜单 ────────────────────────────────────────
|
|
63
90
|
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
64
|
-
|
|
65
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
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 {
|
|
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
|
-
}
|
|
89
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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()
|