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.
Files changed (3) hide show
  1. package/cli.mjs +71 -14
  2. package/package.json +3 -2
  3. package/tray-manager.ps1 +223 -0
package/cli.mjs CHANGED
@@ -4,9 +4,12 @@
4
4
  * Pan Router CLI — 一键安装 + 启动
5
5
  *
6
6
  * 用法:
7
- * npx pan-router # 一键安装 + 启动
8
- * npx pan-router --install # 只安装配置
9
- * npx pan-router --server # 只启动代理
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
- try {
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 --help 显示帮助
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.0.4",
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
  }
@@ -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
+ }