panrouter 1.0.4 → 1.1.1
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 +71 -14
- package/package.json +3 -2
- package/tray-manager.ps1 +223 -0
package/cli.mjs
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* Pan Router CLI — 一键安装 + 启动
|
|
5
5
|
*
|
|
6
6
|
* 用法:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* panrouter # 一键安装 + 前台启动
|
|
8
|
+
* panrouter --install # 只安装配置
|
|
9
|
+
* panrouter --server # 只启动代理(前台)
|
|
10
|
+
* panrouter --tray # 以托盘模式启动(隐藏窗口)
|
|
11
|
+
* panrouter --tray-install # 安装 + 配置 + 托盘启动
|
|
12
|
+
* panrouter --help # 帮助
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
import { execSync, spawn } from "node:child_process";
|
|
@@ -95,6 +98,15 @@ function writeConfig() {
|
|
|
95
98
|
|
|
96
99
|
// ─── 3. 启动代理服务器(后台运行) ───────────────────────────────────────
|
|
97
100
|
|
|
101
|
+
async function isPortOpen() {
|
|
102
|
+
return new Promise(rs => {
|
|
103
|
+
const req = http.get("http://127.0.0.1:50816/health", () => {});
|
|
104
|
+
req.on("response", () => rs(true));
|
|
105
|
+
req.on("error", () => rs(false));
|
|
106
|
+
req.setTimeout(1000, () => { req.destroy(); rs(false); });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
async function startServer() {
|
|
99
111
|
const serverPath = path.join(__dirname, "server.mjs");
|
|
100
112
|
|
|
@@ -109,7 +121,6 @@ async function startServer() {
|
|
|
109
121
|
|
|
110
122
|
log("..", "正在启动 Pan Router(端口 50816)...", "yellow");
|
|
111
123
|
|
|
112
|
-
// Windows: 用 start 命令开新窗口,不依赖 node 后台进程
|
|
113
124
|
if (process.platform === "win32") {
|
|
114
125
|
execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
|
|
115
126
|
} else {
|
|
@@ -119,12 +130,7 @@ async function startServer() {
|
|
|
119
130
|
|
|
120
131
|
// 等待服务启动
|
|
121
132
|
for (let i = 0; i < 15; i++) {
|
|
122
|
-
|
|
123
|
-
const req = http.get("http://127.0.0.1:50816/health", (res) => {});
|
|
124
|
-
req.on("error", () => {});
|
|
125
|
-
const check = await new Promise(rs => { req.on("response", () => rs(true)); req.on("error", () => rs(false)); setTimeout(() => rs(false), 1000); });
|
|
126
|
-
if (check) { break; }
|
|
127
|
-
} catch {}
|
|
133
|
+
if (await isPortOpen()) { break; }
|
|
128
134
|
await new Promise(rs => setTimeout(rs, 1000));
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -132,6 +138,36 @@ async function startServer() {
|
|
|
132
138
|
console.log("\n 现在可以运行: \x1b[33mclaude \"你好\"\x1b[0m\n");
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
// ─── 4. 以托盘模式启动(隐藏窗口 + 系统托盘图标) ────────────────────────
|
|
142
|
+
|
|
143
|
+
function startTray() {
|
|
144
|
+
const serverPath = path.join(__dirname, "server.mjs");
|
|
145
|
+
const trayScript = path.join(__dirname, "tray-manager.ps1");
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(trayScript)) {
|
|
148
|
+
log("!!", "未找到 tray-manager.ps1", "red");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
log("..", "正在以托盘模式启动 Pan Router...", "yellow");
|
|
153
|
+
|
|
154
|
+
// 无窗口运行托盘管理器
|
|
155
|
+
const child = spawn("powershell", [
|
|
156
|
+
"-ExecutionPolicy", "Bypass",
|
|
157
|
+
"-WindowStyle", "Hidden",
|
|
158
|
+
"-STA",
|
|
159
|
+
"-File", trayScript,
|
|
160
|
+
"-ServerPath", serverPath,
|
|
161
|
+
], {
|
|
162
|
+
cwd: __dirname,
|
|
163
|
+
stdio: "ignore",
|
|
164
|
+
detached: true,
|
|
165
|
+
});
|
|
166
|
+
child.unref();
|
|
167
|
+
|
|
168
|
+
log("OK", "Pan Router 托盘已启动(图标在任务栏右下角)", "green");
|
|
169
|
+
}
|
|
170
|
+
|
|
135
171
|
// ─── 主流程 ──────────────────────────────────────────────────────────────
|
|
136
172
|
|
|
137
173
|
function printBanner() {
|
|
@@ -151,10 +187,18 @@ async function main() {
|
|
|
151
187
|
pan-router — Claude Code 免费 AI 路由代理
|
|
152
188
|
|
|
153
189
|
\x1b[33m用法:\x1b[0m
|
|
154
|
-
panrouter
|
|
155
|
-
panrouter --install
|
|
156
|
-
panrouter --server
|
|
157
|
-
panrouter --
|
|
190
|
+
panrouter 一键安装 + 前台启动
|
|
191
|
+
panrouter --install 只安装配置
|
|
192
|
+
panrouter --server 只启动代理(前台窗口)
|
|
193
|
+
panrouter --tray 以托盘模式启动(右下角隐藏图标)
|
|
194
|
+
panrouter --tray-install 安装配置 + 托盘启动
|
|
195
|
+
panrouter --help 显示帮助
|
|
196
|
+
|
|
197
|
+
\x1b[33m托盘模式:\x1b[0m
|
|
198
|
+
在系统通知区(右下角)显示图标,右键菜单:
|
|
199
|
+
- 开关 开机自启动
|
|
200
|
+
- 退出 关闭服务器和托盘
|
|
201
|
+
左键点击图标查看运行状态
|
|
158
202
|
|
|
159
203
|
\x1b[33m配置:\x1b[0m
|
|
160
204
|
代理运行在 http://127.0.0.1:50816
|
|
@@ -176,6 +220,19 @@ async function main() {
|
|
|
176
220
|
return;
|
|
177
221
|
}
|
|
178
222
|
|
|
223
|
+
if (args.includes("--tray") || args.includes("-t")) {
|
|
224
|
+
startTray();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (args.includes("--tray-install") || args.includes("-ti")) {
|
|
229
|
+
printBanner();
|
|
230
|
+
if (!installClaudeCode()) process.exit(1);
|
|
231
|
+
writeConfig();
|
|
232
|
+
startTray();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
179
236
|
// 默认:全流程
|
|
180
237
|
printBanner();
|
|
181
238
|
if (!installClaudeCode()) process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panrouter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
11
|
-
"server.mjs"
|
|
11
|
+
"server.mjs",
|
|
12
|
+
"tray-manager.ps1"
|
|
12
13
|
],
|
|
13
14
|
"license": "MIT"
|
|
14
15
|
}
|
package/tray-manager.ps1
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pan Router 系统托盘管理器
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
在通知区域(右下角)显示图标,隐藏命令窗口。
|
|
6
|
+
右键菜单: 状态, 开机自启动开关, 退出
|
|
7
|
+
|
|
8
|
+
用法:
|
|
9
|
+
powershell -ExecutionPolicy Bypass -STA -File tray-manager.ps1 -ServerPath "server.mjs"
|
|
10
|
+
#>
|
|
11
|
+
|
|
12
|
+
param(
|
|
13
|
+
[string]$ServerPath
|
|
14
|
+
)
|
|
15
|
+
|
|
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 }
|
|
19
|
+
|
|
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
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
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
|
|
86
|
+
}
|
|
87
|
+
} catch { Write-Log "Stop-OldServer: $_" }
|
|
88
|
+
}
|
|
89
|
+
|
|
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
|
+
}
|
|
100
|
+
|
|
101
|
+
# ─── 启动流程 ──────────────────────────────
|
|
102
|
+
Stop-OldServer
|
|
103
|
+
$serverProcess = Start-ServerNohup -ServerPath $ServerPath
|
|
104
|
+
|
|
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
|
+
}
|
|
115
|
+
|
|
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"
|
|
167
|
+
} 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"
|
|
173
|
+
}
|
|
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
|
+
}
|
|
206
|
+
|
|
207
|
+
[System.Windows.Forms.Application]::ApplicationExit += {
|
|
208
|
+
& $cleanup
|
|
209
|
+
$notifyIcon.Dispose()
|
|
210
|
+
[System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
|
|
211
|
+
$icon.Dispose()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Write-Log "Entering message loop"
|
|
215
|
+
[System.Windows.Forms.Application]::Run()
|
|
216
|
+
Write-Log "Message loop exited"
|
|
217
|
+
& $cleanup
|
|
218
|
+
|
|
219
|
+
} catch {
|
|
220
|
+
$errMsg = "UNHANDLED ERROR: $_"
|
|
221
|
+
Write-Log $errMsg
|
|
222
|
+
try { [System.Windows.Forms.MessageBox]::Show($errMsg, "Pan Router Error") } catch {}
|
|
223
|
+
}
|