panrouter 1.2.0 → 1.3.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 +16 -18
- package/package.json +3 -3
- package/tray-daemon.ps1 +251 -0
- package/tray-launcher.vbs +14 -0
- package/daemon.mjs +0 -178
- package/tray-manager.ps1 +0 -111
package/cli.mjs
CHANGED
|
@@ -138,35 +138,33 @@ async function startServer() {
|
|
|
138
138
|
console.log("\n 现在可以运行: \x1b[33mclaude \"你好\"\x1b[0m\n");
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// ─── 4.
|
|
141
|
+
// ─── 4. 以托盘模式启动(VBS 启动器 + 自包含 PS 托盘) ─────────────────────
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
*
|
|
145
|
-
* 1. 启动 daemon.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
144
|
+
* 启动策略 (参考 9Router):
|
|
145
|
+
* 1. tray-launcher.vbs 启动 tray-daemon.ps1 (WshShell.Run, 完全隐藏)
|
|
146
|
+
* 2. tray-daemon.ps1 自包含:
|
|
147
|
+
* - 隐藏启动 server.mjs
|
|
148
|
+
* - NotifyIcon (右下角)
|
|
149
|
+
* - 右键菜单 (开机自启动 / 退出)
|
|
150
|
+
* - 30秒健康检查自动重启
|
|
151
|
+
* 3. cli.mjs 进程立刻退出
|
|
150
152
|
*/
|
|
151
153
|
function startTray() {
|
|
152
|
-
const
|
|
154
|
+
const launcherPath = path.join(__dirname, "tray-launcher.vbs");
|
|
153
155
|
|
|
154
|
-
if (!fs.existsSync(
|
|
155
|
-
log("!!", "未找到
|
|
156
|
+
if (!fs.existsSync(launcherPath)) {
|
|
157
|
+
log("!!", "未找到 tray-launcher.vbs", "red");
|
|
156
158
|
process.exit(1);
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
log("..", "正在以托盘模式启动 Pan Router...", "yellow");
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
daemonPath,
|
|
166
|
-
`--serverPath="${serverPath}"`,
|
|
167
|
-
`--trayPath="${trayPath}"`,
|
|
163
|
+
// wscript.exe //B = 无窗口, 自动退出
|
|
164
|
+
const child = spawn("wscript.exe", [
|
|
165
|
+
"//B", "//NoLogo",
|
|
166
|
+
launcherPath,
|
|
168
167
|
], {
|
|
169
|
-
cwd: __dirname,
|
|
170
168
|
stdio: "ignore",
|
|
171
169
|
detached: true,
|
|
172
170
|
windowsHide: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panrouter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
11
|
-
"daemon.mjs",
|
|
12
11
|
"server.mjs",
|
|
13
|
-
"tray-
|
|
12
|
+
"tray-daemon.ps1",
|
|
13
|
+
"tray-launcher.vbs"
|
|
14
14
|
],
|
|
15
15
|
"license": "MIT"
|
|
16
16
|
}
|
package/tray-daemon.ps1
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pan Router Tray Daemon — 完全自包含的托盘进程
|
|
4
|
+
|
|
5
|
+
功能:
|
|
6
|
+
- 隐藏启动 server.mjs (node)
|
|
7
|
+
- 右下角 NotifyIcon
|
|
8
|
+
- 右键菜单: 开机自启动 / 退出
|
|
9
|
+
- 30秒健康检查, 自动重启
|
|
10
|
+
- 日志到 %TEMP%\panrouter-tray.log
|
|
11
|
+
|
|
12
|
+
用法 (由 tray-launcher.vbs 调用):
|
|
13
|
+
powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File tray-daemon.ps1
|
|
14
|
+
|
|
15
|
+
也可直接测试:
|
|
16
|
+
powershell -ExecutionPolicy Bypass -STA -File tray-daemon.ps1
|
|
17
|
+
#>
|
|
18
|
+
|
|
19
|
+
$logFile = "$env:TEMP\panrouter-tray.log"
|
|
20
|
+
function Write-Log($m) { "$(Get-Date -Format 'HH:mm:ss') $m" | Out-File -Append -Encoding utf8 $logFile }
|
|
21
|
+
|
|
22
|
+
Write-Log "=== PanRouter Tray Daemon ==="
|
|
23
|
+
|
|
24
|
+
# ─── 路径 (与脚本同目录) ────────────────────────
|
|
25
|
+
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
|
26
|
+
$serverPath = Join-Path $scriptDir "server.mjs"
|
|
27
|
+
|
|
28
|
+
# ─── 加载 WinForms ─────────────────────────────
|
|
29
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
30
|
+
Add-Type -AssemblyName System.Drawing
|
|
31
|
+
Write-Log "Assemblies loaded"
|
|
32
|
+
|
|
33
|
+
# ═══════════════════════════════════════════════════════════
|
|
34
|
+
# 1. 生成托盘图标 (蓝色底 P)
|
|
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, 0, 0, 15, 15)
|
|
42
|
+
$font = New-Object System.Drawing.Font("Segoe UI", 8.5, [System.Drawing.FontStyle]::Bold)
|
|
43
|
+
$fg = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
|
|
44
|
+
$g.DrawString("P", $font, $fg, 3, 1.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
|
+
Write-Log "Icon created"
|
|
50
|
+
|
|
51
|
+
# ═══════════════════════════════════════════════════════════
|
|
52
|
+
# 2. NotifyIcon
|
|
53
|
+
# ═══════════════════════════════════════════════════════════
|
|
54
|
+
$notifyIcon = New-Object System.Windows.Forms.NotifyIcon
|
|
55
|
+
$notifyIcon.Icon = $icon
|
|
56
|
+
$notifyIcon.Text = "Pan Router | 端口 50816"
|
|
57
|
+
$notifyIcon.Visible = $true
|
|
58
|
+
Write-Log "NotifyIcon visible"
|
|
59
|
+
|
|
60
|
+
# ═══════════════════════════════════════════════════════════
|
|
61
|
+
# 3. 隐藏启动 server.mjs
|
|
62
|
+
# ═══════════════════════════════════════════════════════════
|
|
63
|
+
function Start-Server {
|
|
64
|
+
# 杀掉旧 server
|
|
65
|
+
try {
|
|
66
|
+
$old = wmic process where "name='node.exe'" get ProcessId,CommandLine /format:csv 2>$null
|
|
67
|
+
foreach ($line in $old) {
|
|
68
|
+
if ($line -match '(\d+),.*?server\.mjs') {
|
|
69
|
+
$pid = $Matches[1]
|
|
70
|
+
Write-Log "Kill old server PID=$pid"
|
|
71
|
+
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
|
|
76
|
+
# UseShellExecute=$true 避免输出缓冲死锁
|
|
77
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
78
|
+
$psi.FileName = "node"
|
|
79
|
+
$psi.Arguments = "`"$serverPath`""
|
|
80
|
+
$psi.WorkingDirectory = $scriptDir
|
|
81
|
+
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
82
|
+
$psi.CreateNoWindow = $true
|
|
83
|
+
$psi.UseShellExecute = $true
|
|
84
|
+
try {
|
|
85
|
+
$p = [System.Diagnostics.Process]::Start($psi)
|
|
86
|
+
Write-Log "Server started PID=$($p.Id)"
|
|
87
|
+
return $p
|
|
88
|
+
} catch {
|
|
89
|
+
Write-Log "Server start FAILED: $_"
|
|
90
|
+
return $null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ═══════════════════════════════════════════════════════════
|
|
95
|
+
# 4. 健康检查
|
|
96
|
+
# ═══════════════════════════════════════════════════════════
|
|
97
|
+
function Test-Online {
|
|
98
|
+
try {
|
|
99
|
+
$req = [System.Net.WebRequest]::Create("http://127.0.0.1:50816/health")
|
|
100
|
+
$req.Timeout = 1500
|
|
101
|
+
$resp = $req.GetResponse()
|
|
102
|
+
$resp.Close()
|
|
103
|
+
return $true
|
|
104
|
+
} catch { return $false }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# ═══════════════════════════════════════════════════════════
|
|
108
|
+
# 5. 开机自启动 (Registry)
|
|
109
|
+
# ═══════════════════════════════════════════════════════════
|
|
110
|
+
$autostartName = "PanRouter"
|
|
111
|
+
$runKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
|
|
112
|
+
|
|
113
|
+
function Get-Autostart {
|
|
114
|
+
try {
|
|
115
|
+
$val = Get-ItemProperty -Path $runKey -Name $autostartName -ErrorAction Stop
|
|
116
|
+
return $val.$autostartName -ne $null
|
|
117
|
+
} catch { return $false }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function Set-Autostart($enable) {
|
|
121
|
+
if ($enable) {
|
|
122
|
+
$vbsPath = Join-Path $scriptDir "tray-launcher.vbs"
|
|
123
|
+
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v $autostartName /t REG_SZ /d "wscript.exe //B `"$vbsPath`"" /f 2>&1 | Out-Null
|
|
124
|
+
Write-Log "Autostart ON: $vbsPath"
|
|
125
|
+
} else {
|
|
126
|
+
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v $autostartName /f 2>&1 | Out-Null
|
|
127
|
+
Write-Log "Autostart OFF"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# ═══════════════════════════════════════════════════════════
|
|
132
|
+
# ══ 启动流程 ══
|
|
133
|
+
# ═══════════════════════════════════════════════════════════
|
|
134
|
+
|
|
135
|
+
# 启动服务器
|
|
136
|
+
$serverProcess = Start-Server
|
|
137
|
+
|
|
138
|
+
# 等待就绪 (10秒)
|
|
139
|
+
$ready = $false
|
|
140
|
+
for ($i = 0; $i -lt 20; $i++) {
|
|
141
|
+
Start-Sleep -Milliseconds 500
|
|
142
|
+
if (Test-Online) { $ready = $true; break }
|
|
143
|
+
}
|
|
144
|
+
Write-Log "Server ready=$ready"
|
|
145
|
+
if ($ready) {
|
|
146
|
+
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已就绪 (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
147
|
+
} else {
|
|
148
|
+
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器启动失败, 查看日志", [System.Windows.Forms.ToolTipIcon]::Error)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# ═══════════════════════════════════════════════════════════
|
|
152
|
+
# ══ 右键菜单 ══
|
|
153
|
+
# ═══════════════════════════════════════════════════════════
|
|
154
|
+
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
155
|
+
|
|
156
|
+
# 标题
|
|
157
|
+
$titleItem = New-Object System.Windows.Forms.ToolStripMenuItem("Pan Router - :50816")
|
|
158
|
+
$titleItem.Enabled = $false
|
|
159
|
+
$titleItem.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
160
|
+
$menu.Items.Add($titleItem)
|
|
161
|
+
$menu.Items.Add("-")
|
|
162
|
+
|
|
163
|
+
# 开机自启动
|
|
164
|
+
$autoItem = New-Object System.Windows.Forms.ToolStripMenuItem("开机自启动")
|
|
165
|
+
$autoItem.Checked = Get-Autostart
|
|
166
|
+
$autoItem.Add_Click({
|
|
167
|
+
if ($autoItem.Checked) {
|
|
168
|
+
Set-Autostart $false
|
|
169
|
+
$autoItem.Checked = $false
|
|
170
|
+
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
171
|
+
} else {
|
|
172
|
+
Set-Autostart $true
|
|
173
|
+
$autoItem.Checked = $true
|
|
174
|
+
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
$menu.Items.Add($autoItem)
|
|
178
|
+
$menu.Items.Add("-")
|
|
179
|
+
|
|
180
|
+
# 退出
|
|
181
|
+
$exitItem = New-Object System.Windows.Forms.ToolStripMenuItem("退出")
|
|
182
|
+
$exitItem.Add_Click({
|
|
183
|
+
Write-Log "Exit clicked"
|
|
184
|
+
$notifyIcon.Visible = $false
|
|
185
|
+
[System.Windows.Forms.Application]::Exit()
|
|
186
|
+
})
|
|
187
|
+
$menu.Items.Add($exitItem)
|
|
188
|
+
|
|
189
|
+
$notifyIcon.ContextMenuStrip = $menu
|
|
190
|
+
|
|
191
|
+
# ═══════════════════════════════════════════════════════════
|
|
192
|
+
# ══ 左键: 状态气泡 ══
|
|
193
|
+
# ═══════════════════════════════════════════════════════════
|
|
194
|
+
$notifyIcon.Add_MouseClick({
|
|
195
|
+
if ($_.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
|
|
196
|
+
if (Test-Online) {
|
|
197
|
+
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "运行正常 ✓ (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
198
|
+
} else {
|
|
199
|
+
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "服务器未响应 ⚠", [System.Windows.Forms.ToolTipIcon]::Error)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
# ═══════════════════════════════════════════════════════════
|
|
205
|
+
# ══ 定时健康检查 (30秒) ══
|
|
206
|
+
# ═══════════════════════════════════════════════════════════
|
|
207
|
+
$healthTimer = New-Object System.Windows.Forms.Timer
|
|
208
|
+
$healthTimer.Interval = 30000
|
|
209
|
+
$healthTimer.Add_Tick({
|
|
210
|
+
if (-not (Test-Online)) {
|
|
211
|
+
Write-Log "Health check FAILED, restarting server..."
|
|
212
|
+
Start-Server
|
|
213
|
+
Start-Sleep -Seconds 3
|
|
214
|
+
if (Test-Online) {
|
|
215
|
+
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已自动重启", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
$healthTimer.Start()
|
|
220
|
+
|
|
221
|
+
# ═══════════════════════════════════════════════════════════
|
|
222
|
+
# ══ 退出清理 ══
|
|
223
|
+
# ═══════════════════════════════════════════════════════════
|
|
224
|
+
[System.Windows.Forms.Application]::ApplicationExit += {
|
|
225
|
+
Write-Log "AppExit cleanup"
|
|
226
|
+
$healthTimer.Stop()
|
|
227
|
+
try {
|
|
228
|
+
if ($serverProcess -and !$serverProcess.HasExited) {
|
|
229
|
+
$serverProcess.Kill()
|
|
230
|
+
$serverProcess.WaitForExit(3000)
|
|
231
|
+
}
|
|
232
|
+
} catch {}
|
|
233
|
+
# 补刀: 杀残留
|
|
234
|
+
try {
|
|
235
|
+
$old = wmic process where "name='node.exe'" get ProcessId,CommandLine /format:csv 2>$null
|
|
236
|
+
foreach ($line in $old) {
|
|
237
|
+
if ($line -match '(\d+),.*?server\.mjs') { Stop-Process -Id $Matches[1] -Force -ErrorAction SilentlyContinue }
|
|
238
|
+
}
|
|
239
|
+
} catch {}
|
|
240
|
+
$notifyIcon.Dispose()
|
|
241
|
+
[System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
|
|
242
|
+
$icon.Dispose()
|
|
243
|
+
Write-Log "Cleanup done"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# ═══════════════════════════════════════════════════════════
|
|
247
|
+
# ══ 消息循环 ══
|
|
248
|
+
# ═══════════════════════════════════════════════════════════
|
|
249
|
+
Write-Log "Entering message loop"
|
|
250
|
+
[System.Windows.Forms.Application]::Run()
|
|
251
|
+
Write-Log "Message loop exited"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
' Pan Router Tray Launcher
|
|
2
|
+
' Windows 原生无窗口启动器(VBScript 内置, 无需任何运行时)
|
|
3
|
+
' 用法: wscript.exe tray-launcher.vbs
|
|
4
|
+
' 或 cscript.exe //nologo tray-launcher.vbs
|
|
5
|
+
|
|
6
|
+
Dim WshShell, FSO, ScriptDir
|
|
7
|
+
Set WshShell = CreateObject("WScript.Shell")
|
|
8
|
+
Set FSO = CreateObject("Scripting.FileSystemObject")
|
|
9
|
+
|
|
10
|
+
ScriptDir = FSO.GetParentFolderName(WScript.ScriptFullName)
|
|
11
|
+
PS1Path = ScriptDir & "\tray-daemon.ps1"
|
|
12
|
+
|
|
13
|
+
' 用 PowerShell 启动托盘守护进程 (0 = 隐藏窗口, False = 不等待返回)
|
|
14
|
+
WshShell.Run "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File """ & PS1Path & """", 0, False
|
package/daemon.mjs
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Pan Router Daemon — 后台守护进程
|
|
5
|
-
*
|
|
6
|
-
* 由 cli.mjs 以 --tray 参数启动 (detached, hidden)。
|
|
7
|
-
* 管理 server.mjs + tray-manager.ps1, 通过 stdin/stdout JSON 通信。
|
|
8
|
-
*
|
|
9
|
-
* 用法(由 cli.mjs 调用):
|
|
10
|
-
* node daemon.mjs --serverPath="..." --trayPath="..."
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { spawn, execSync } from "node:child_process";
|
|
14
|
-
import { createInterface } from "node:readline";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import fs from "node:fs";
|
|
18
|
-
import http from "node:http";
|
|
19
|
-
|
|
20
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
|
|
22
|
-
// ─── 解析参数 ────────────────────────────────────
|
|
23
|
-
const serverPath = process.argv.find(a => a.startsWith("--serverPath="))?.split("=")[1];
|
|
24
|
-
const trayPath = process.argv.find(a => a.startsWith("--trayPath="))?.split("=")[1];
|
|
25
|
-
if (!serverPath || !trayPath) { process.exit(1); }
|
|
26
|
-
|
|
27
|
-
const appDir = path.dirname(serverPath);
|
|
28
|
-
const AUTOSTART_NAME = "PanRouter";
|
|
29
|
-
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
30
|
-
|
|
31
|
-
let serverProcess = null;
|
|
32
|
-
let psProcess = null;
|
|
33
|
-
|
|
34
|
-
// ─── 日志 ────────────────────────────────────────
|
|
35
|
-
const LOG = path.join(process.env.TEMP || "/tmp", "panrouter-daemon.log");
|
|
36
|
-
function log(msg) {
|
|
37
|
-
try { fs.appendFileSync(LOG, `${new Date().toISOString().slice(11,19)} ${msg}\n`); } catch {}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ─── 健康检查 ─────────────────────────────────────
|
|
41
|
-
function isOnline() {
|
|
42
|
-
return new Promise(rs => {
|
|
43
|
-
const req = http.get("http://127.0.0.1:50816/health", () => {});
|
|
44
|
-
req.on("response", () => { req.destroy(); rs(true); });
|
|
45
|
-
req.on("error", () => rs(false));
|
|
46
|
-
req.setTimeout(1500, () => { req.destroy(); rs(false); });
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ─── 杀掉旧 server 进程 ──────────────────────────
|
|
51
|
-
function killOldServers() {
|
|
52
|
-
try {
|
|
53
|
-
const wmic = spawn("wmic", [
|
|
54
|
-
"process", "where", "name='node.exe'", "get", "ProcessId,CommandLine", "/format:csv"
|
|
55
|
-
], { stdio: ["ignore", "pipe", "ignore"] });
|
|
56
|
-
let out = "";
|
|
57
|
-
wmic.stdout.on("data", d => out += d.toString());
|
|
58
|
-
wmic.on("close", () => {
|
|
59
|
-
for (const line of out.split("\n")) {
|
|
60
|
-
if (line.includes("server.mjs")) {
|
|
61
|
-
const m = line.match(/(\d+),.*?server\.mjs/);
|
|
62
|
-
if (m) { try { process.kill(parseInt(m[1]), "SIGKILL"); } catch {} }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
} catch {}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ─── 隐藏启动 server.mjs ─────────────────────────
|
|
70
|
-
function startServer() {
|
|
71
|
-
killOldServers();
|
|
72
|
-
serverProcess = spawn("node", [serverPath], {
|
|
73
|
-
cwd: appDir, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, shell: false,
|
|
74
|
-
});
|
|
75
|
-
serverProcess.stdout.on("data", d => log("[srv] " + d.toString().trim()));
|
|
76
|
-
serverProcess.stderr.on("data", d => log("[srv-err] " + d.toString().trim()));
|
|
77
|
-
serverProcess.on("exit", code => log(`Server exited code=${code}`));
|
|
78
|
-
log(`Server started PID=${serverProcess.pid}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ─── 命令 → PS 托盘 ─────────────────────────────
|
|
82
|
-
function psSend(cmd) {
|
|
83
|
-
if (psProcess?.stdin?.writable) {
|
|
84
|
-
psProcess.stdin.write(JSON.stringify(cmd) + "\n");
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ─── 开机自启动操作 (reg.exe) ────────────────────
|
|
89
|
-
function toggleAutostart() {
|
|
90
|
-
try {
|
|
91
|
-
const out = execSync(`reg query ${RUN_KEY} /v ${AUTOSTART_NAME} 2>nul`, {
|
|
92
|
-
encoding: "utf8", windowsHide: true, timeout: 3000,
|
|
93
|
-
});
|
|
94
|
-
const isOn = !out.toLowerCase().includes("error");
|
|
95
|
-
if (isOn) {
|
|
96
|
-
execSync(`reg delete ${RUN_KEY} /v ${AUTOSTART_NAME} /f 2>nul`, { windowsHide: true, timeout: 3000 });
|
|
97
|
-
psSend({ action: "update-item", index: 2, title: "开机自启动", enabled: true });
|
|
98
|
-
log("Autostart OFF");
|
|
99
|
-
} else {
|
|
100
|
-
const exe = process.execPath;
|
|
101
|
-
const daemon = path.join(__dirname, "daemon.mjs");
|
|
102
|
-
const cmd = `"${exe}" "${daemon}" --serverPath="${serverPath}" --trayPath="${trayPath}"`;
|
|
103
|
-
execSync(`reg add ${RUN_KEY} /v ${AUTOSTART_NAME} /t REG_SZ /d "${cmd}" /f 2>nul`, { windowsHide: true, timeout: 3000 });
|
|
104
|
-
psSend({ action: "update-item", index: 2, title: "✓ 开机自启动", enabled: true });
|
|
105
|
-
log("Autostart ON");
|
|
106
|
-
}
|
|
107
|
-
} catch (e) { log(`Autostart error: ${e.message}`); }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ─── 退出清理 ────────────────────────────────────
|
|
111
|
-
function cleanup() {
|
|
112
|
-
log("Cleanup...");
|
|
113
|
-
try { if (serverProcess && !serverProcess.killed) serverProcess.kill("SIGKILL"); } catch {}
|
|
114
|
-
killOldServers();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ═══════════════ 主流程 ═══════════════
|
|
118
|
-
|
|
119
|
-
log("=== PanRouter Daemon ===");
|
|
120
|
-
log(`serverPath=${serverPath}`);
|
|
121
|
-
log(`trayPath=${trayPath}`);
|
|
122
|
-
|
|
123
|
-
// 1. 启动服务器
|
|
124
|
-
startServer();
|
|
125
|
-
|
|
126
|
-
// 2. 等服务器就绪, 启动托盘
|
|
127
|
-
(async () => {
|
|
128
|
-
let ready = false;
|
|
129
|
-
for (let i = 0; i < 20; i++) {
|
|
130
|
-
if (await isOnline()) { ready = true; break; }
|
|
131
|
-
await new Promise(r => setTimeout(r, 500));
|
|
132
|
-
}
|
|
133
|
-
log(`Server ready=${ready}`);
|
|
134
|
-
|
|
135
|
-
// 3. 启动 PS 托盘 (pipe 连接, 保持 IPC)
|
|
136
|
-
psProcess = spawn("powershell.exe", [
|
|
137
|
-
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
|
138
|
-
"-WindowStyle", "Hidden",
|
|
139
|
-
"-File", trayPath,
|
|
140
|
-
], {
|
|
141
|
-
stdio: ["pipe", "pipe", "pipe"], windowsHide: true, shell: false,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// 读取 PS 事件
|
|
145
|
-
createInterface({ input: psProcess.stdout }).on("line", (line) => {
|
|
146
|
-
try {
|
|
147
|
-
const evt = JSON.parse(line);
|
|
148
|
-
log(`PS: ${line}`);
|
|
149
|
-
if (evt.type === "started") {
|
|
150
|
-
// 发送菜单配置
|
|
151
|
-
psSend({ action: "add-item", index: 0, title: "Pan Router - :50816", enabled: false });
|
|
152
|
-
psSend({ action: "add-item", index: 1, title: "─".repeat(19), enabled: false });
|
|
153
|
-
psSend({ action: "add-item", index: 2, title: "开机自启动", enabled: true });
|
|
154
|
-
psSend({ action: "add-item", index: 3, title: "─".repeat(19), enabled: false });
|
|
155
|
-
psSend({ action: "add-item", index: 4, title: "退出", enabled: true });
|
|
156
|
-
psSend({ action: "set-tooltip", text: "Pan Router | 端口 50816" });
|
|
157
|
-
log("Menu configured");
|
|
158
|
-
}
|
|
159
|
-
if (evt.type === "click" && evt.index === 2) toggleAutostart();
|
|
160
|
-
if (evt.type === "click" && evt.index === 4) {
|
|
161
|
-
log("Exit requested");
|
|
162
|
-
cleanup();
|
|
163
|
-
psSend({ action: "kill" });
|
|
164
|
-
setTimeout(() => process.exit(0), 500);
|
|
165
|
-
}
|
|
166
|
-
} catch (e) { log(`PS parse error: ${e.message}`); }
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
psProcess.on("error", err => log(`PS error: ${err.message}`));
|
|
170
|
-
psProcess.stderr.on("data", d => log(`[ps-err] ${d.toString().trim()}`));
|
|
171
|
-
psProcess.on("exit", code => { log(`PS exited code=${code}`); process.exit(0); });
|
|
172
|
-
|
|
173
|
-
log("Daemon ready");
|
|
174
|
-
process.stdin.resume(); // 保持进程存活
|
|
175
|
-
})();
|
|
176
|
-
|
|
177
|
-
process.on("SIGTERM", () => { cleanup(); setTimeout(() => process.exit(0), 300); });
|
|
178
|
-
process.on("SIGINT", () => { cleanup(); setTimeout(() => process.exit(0), 300); });
|
package/tray-manager.ps1
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
<#
|
|
2
|
-
.SYNOPSIS
|
|
3
|
-
Pan Router 托盘管理器 (IPC 版)
|
|
4
|
-
.DESCRIPTION
|
|
5
|
-
纯 NotifyIcon 包装器, 通过 stdin/stdout JSON 与父进程通信
|
|
6
|
-
|
|
7
|
-
支持命令 (stdin):
|
|
8
|
-
{"action":"add-item","index":0,"title":"...","enabled":true}
|
|
9
|
-
{"action":"update-item","index":0,"title":"...","enabled":true}
|
|
10
|
-
{"action":"set-tooltip","text":"..."}
|
|
11
|
-
{"action":"kill"}
|
|
12
|
-
|
|
13
|
-
事件 (stdout):
|
|
14
|
-
{"type":"started"}
|
|
15
|
-
{"type":"ready"} <- PS 就绪 + STA 已确认
|
|
16
|
-
{"type":"click","index":0}
|
|
17
|
-
{"type":"error","message":"..."}
|
|
18
|
-
#>
|
|
19
|
-
|
|
20
|
-
param()
|
|
21
|
-
|
|
22
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
23
|
-
Add-Type -AssemblyName System.Drawing
|
|
24
|
-
|
|
25
|
-
$ErrorActionPreference = "Stop"
|
|
26
|
-
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
27
|
-
|
|
28
|
-
$script:notifyIcon = New-Object System.Windows.Forms.NotifyIcon
|
|
29
|
-
$script:notifyIcon.Visible = $true
|
|
30
|
-
$script:menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
31
|
-
$script:notifyIcon.ContextMenuStrip = $script:menu
|
|
32
|
-
$script:items = @()
|
|
33
|
-
|
|
34
|
-
function Write-Event($obj) {
|
|
35
|
-
$json = $obj | ConvertTo-Json -Compress
|
|
36
|
-
try {
|
|
37
|
-
[Console]::Out.WriteLine($json)
|
|
38
|
-
[Console]::Out.Flush()
|
|
39
|
-
} catch {}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
# ─── 生成蓝色 P 图标 (纯内存, 无需 .ico 文件) ──
|
|
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
|
-
$script:notifyIcon.Icon = $icon
|
|
58
|
-
$script:notifyIcon.Text = "Pan Router"
|
|
59
|
-
|
|
60
|
-
function Add-MenuItem($index, $title, $enabled) {
|
|
61
|
-
$item = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
62
|
-
$item.Text = $title
|
|
63
|
-
$item.Enabled = $enabled
|
|
64
|
-
$idx = $index
|
|
65
|
-
$item.Add_Click({ Write-Event @{type="click"; index=$idx} }.GetNewClosure())
|
|
66
|
-
$script:menu.Items.Add($item) | Out-Null
|
|
67
|
-
$script:items += $item
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function Update-MenuItem($index, $title, $enabled) {
|
|
71
|
-
if ($index -lt $script:items.Count) {
|
|
72
|
-
$script:items[$index].Text = $title
|
|
73
|
-
$script:items[$index].Enabled = $enabled
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function Set-Tooltip($text) {
|
|
78
|
-
if ($text.Length -gt 63) { $text = $text.Substring(0, 63) }
|
|
79
|
-
$script:notifyIcon.Text = $text
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
# ─── stdin 轮询 (100ms 间隔) ────────────────────
|
|
83
|
-
$script:timer = New-Object System.Windows.Forms.Timer
|
|
84
|
-
$script:timer.Interval = 100
|
|
85
|
-
$script:timer.Add_Tick({
|
|
86
|
-
try {
|
|
87
|
-
while ([Console]::In.Peek() -ne -1) {
|
|
88
|
-
$line = [Console]::In.ReadLine()
|
|
89
|
-
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
|
90
|
-
$cmd = $line | ConvertFrom-Json
|
|
91
|
-
switch ($cmd.action) {
|
|
92
|
-
"add-item" { Add-MenuItem $cmd.index $cmd.title $cmd.enabled }
|
|
93
|
-
"update-item" { Update-MenuItem $cmd.index $cmd.title $cmd.enabled }
|
|
94
|
-
"set-tooltip" { Set-Tooltip $cmd.text }
|
|
95
|
-
"kill" {
|
|
96
|
-
$script:notifyIcon.Visible = $false
|
|
97
|
-
$script:notifyIcon.Dispose()
|
|
98
|
-
$icon.Dispose()
|
|
99
|
-
[System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
|
|
100
|
-
[System.Windows.Forms.Application]::Exit()
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
Write-Event @{type="error"; message=$_.Exception.Message}
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
$script:timer.Start()
|
|
109
|
-
|
|
110
|
-
Write-Event @{type="started"}
|
|
111
|
-
[System.Windows.Forms.Application]::Run()
|