panrouter 1.0.2 → 1.1.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.
@@ -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,7 +98,16 @@ function writeConfig() {
95
98
 
96
99
  // ─── 3. 启动代理服务器(后台运行) ───────────────────────────────────────
97
100
 
98
- function startServer() {
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
+
110
+ async function startServer() {
99
111
  const serverPath = path.join(__dirname, "server.mjs");
100
112
 
101
113
  // 关掉旧的 Pan Router
@@ -109,9 +121,8 @@ function startServer() {
109
121
 
110
122
  log("..", "正在启动 Pan Router(端口 50816)...", "yellow");
111
123
 
112
- // Windows: 用 start 命令开新窗口,不依赖 node 后台进程
113
124
  if (process.platform === "win32") {
114
- execSync(`start "Pan Router" cmd /c "node "${serverPath}" & pause"`, { stdio: "pipe" });
125
+ execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
115
126
  } else {
116
127
  const child = spawn("node", [serverPath], { cwd: __dirname, stdio: "ignore", detached: true });
117
128
  child.unref();
@@ -119,12 +130,7 @@ 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 @@ 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() {
@@ -143,7 +179,7 @@ function printBanner() {
143
179
  `);
144
180
  }
145
181
 
146
- function main() {
182
+ async function main() {
147
183
  const args = process.argv.slice(2);
148
184
 
149
185
  if (args.includes("--help") || args.includes("-h")) {
@@ -151,10 +187,18 @@ 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
@@ -172,7 +216,20 @@ function main() {
172
216
  }
173
217
 
174
218
  if (args.includes("--server") || args.includes("-s")) {
175
- startServer();
219
+ await startServer();
220
+ return;
221
+ }
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();
176
233
  return;
177
234
  }
178
235
 
@@ -180,7 +237,7 @@ function main() {
180
237
  printBanner();
181
238
  if (!installClaudeCode()) process.exit(1);
182
239
  writeConfig();
183
- startServer();
240
+ await startServer();
184
241
  }
185
242
 
186
243
  main();
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
7
- "panrouter": "cli.js"
7
+ "panrouter": "cli.mjs"
8
8
  },
9
9
  "files": [
10
- "cli.js",
11
- "server.mjs"
10
+ "cli.mjs",
11
+ "server.mjs",
12
+ "tray-manager.ps1"
12
13
  ],
13
14
  "license": "MIT"
14
15
  }
@@ -0,0 +1,237 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pan Router 系统托盘管理器
4
+ .DESCRIPTION
5
+ 在通知区域(右下角)显示图标,隐藏命令窗口。
6
+ 右键菜单: 状态, 开机自启动开关, 退出
7
+
8
+ 用法:
9
+ powershell -STA -File tray-manager.ps1 -ServerPath "server.mjs"
10
+ #>
11
+
12
+ param(
13
+ [string]$ServerPath
14
+ )
15
+
16
+ # ─── STA 检查 ──────────────────────────────────
17
+ if ([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne "STA") {
18
+ $r = [System.Windows.Forms.MessageBox]::Show(
19
+ "Pan Router 需要 STA 模式运行。`n是否自动以 STA 模式重新启动?",
20
+ "Pan Router",
21
+ "YesNo",
22
+ "Warning"
23
+ )
24
+ if ($r -eq "Yes") {
25
+ powershell -STA -File $MyInvocation.MyCommand.Path -ServerPath $ServerPath
26
+ }
27
+ exit 1
28
+ }
29
+
30
+ Add-Type -AssemblyName System.Windows.Forms
31
+ Add-Type -AssemblyName System.Drawing
32
+
33
+ # ─── 常量 ──────────────────────────────────────
34
+ $scriptPath = $MyInvocation.MyCommand.Path
35
+ $autostartName = "PanRouter"
36
+ $runKeyPath = "Software\Microsoft\Windows\CurrentVersion\Run"
37
+
38
+ # 取 npm 全局安装目录作为图标标题提示
39
+ $pkgDir = Split-Path (Split-Path $ServerPath -Parent) -Parent
40
+ $pkgName = Split-Path $pkgDir -Leaf
41
+ $tooltipText = "Pan Router`n端口 50816 | 运行中"
42
+
43
+ # ─── 图标生成 ──────────────────────────────────
44
+ function New-TrayIcon {
45
+ $bmp = New-Object System.Drawing.Bitmap(16, 16)
46
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
47
+ $g.SmoothingMode = 'HighQuality'
48
+ $g.Clear([System.Drawing.Color]::Transparent)
49
+
50
+ # 蓝色圆底
51
+ $brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(0, 120, 215))
52
+ $g.FillEllipse($brush, 0, 0, 15, 15)
53
+
54
+ # 白色 "P" 字
55
+ $font = New-Object System.Drawing.Font("Segoe UI", 8.5, [System.Drawing.FontStyle]::Bold)
56
+ $fg = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
57
+ $g.DrawString("P", $font, $fg, 3, 1.5)
58
+ $font.Dispose()
59
+ $fg.Dispose()
60
+ $brush.Dispose()
61
+ $g.Dispose()
62
+
63
+ $hIcon = $bmp.GetHicon()
64
+ $icon = [System.Drawing.Icon]::FromHandle($hIcon)
65
+ $bmp.Dispose()
66
+ return $icon, $hIcon
67
+ }
68
+
69
+ # ─── 启动隐藏的 Node 服务器 ─────────────────────
70
+ function Start-ServerHidden {
71
+ param([string]$ServerPath)
72
+
73
+ $serverDir = Split-Path $ServerPath -Parent
74
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
75
+ $psi.FileName = "node"
76
+ $psi.Arguments = "`"$ServerPath`""
77
+ $psi.WorkingDirectory = $serverDir
78
+ $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
79
+ $psi.CreateNoWindow = $true
80
+ $psi.UseShellExecute = $false
81
+ $psi.RedirectStandardOutput = $true
82
+ $psi.RedirectStandardError = $true
83
+
84
+ try {
85
+ $proc = [System.Diagnostics.Process]::Start($psi)
86
+ return $proc
87
+ } catch {
88
+ return $null
89
+ }
90
+ }
91
+
92
+ # ─── 检查服务器是否在线 ─────────────────────────
93
+ function Test-ServerOnline {
94
+ try {
95
+ $req = [System.Net.HttpWebRequest]::Create("http://127.0.0.1:50816/health")
96
+ $req.Method = "GET"
97
+ $req.Timeout = 1500
98
+ $resp = $req.GetResponse()
99
+ $resp.Close()
100
+ return $true
101
+ } catch {
102
+ return $false
103
+ }
104
+ }
105
+
106
+ # ─── 杀掉旧 Pan Router 进程 (WMI 方式, 兼容 PS5.1) ──────────
107
+ function Stop-OldServer {
108
+ try {
109
+ $existing = Get-CimInstance -ClassName Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue |
110
+ Where-Object { $_.CommandLine -match "server\.mjs" }
111
+ foreach ($p in $existing) {
112
+ $pid = $p.ProcessId
113
+ Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
114
+ }
115
+ } catch {}
116
+ }
117
+
118
+ # ═══════════════════════════════════════════════
119
+ # 主流程
120
+ # ═══════════════════════════════════════════════
121
+
122
+ # 1. 杀掉旧服务器
123
+ Stop-OldServer
124
+
125
+ # 2. 生成图标
126
+ $icon, $hIcon = New-TrayIcon
127
+
128
+ # 3. 创建 NotifyIcon
129
+ $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
130
+ $notifyIcon.Icon = $icon
131
+ $notifyIcon.Text = $tooltipText
132
+ $notifyIcon.Visible = $true
133
+
134
+ # 4. 启动隐藏服务器进程
135
+ $serverProcess = Start-ServerHidden -ServerPath $ServerPath
136
+
137
+ # 5. 等待服务器就绪(后台检查,不阻塞 UI)
138
+ $checkTimer = New-Object System.Windows.Forms.Timer
139
+ $checkTimer.Interval = 500
140
+ $checkCount = 0
141
+ $checkTimer.Add_Tick({
142
+ $checkCount++
143
+ if (Test-ServerOnline) {
144
+ $checkTimer.Stop()
145
+ $notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已就绪 (端口 50816)", "Info")
146
+ } elseif ($checkCount -ge 10) {
147
+ $checkTimer.Stop()
148
+ $notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器启动可能较慢,尝试访问中...", "Warning")
149
+ }
150
+ })
151
+ $checkTimer.Start()
152
+
153
+ # 6. 左键点击: 弹气泡提示状态
154
+ $notifyIcon.Add_MouseClick({
155
+ param($sender, $e)
156
+ if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
157
+ $online = Test-ServerOnline
158
+ if ($online) {
159
+ $notifyIcon.ShowBalloonTip(3000, "Pan Router", "运行正常 ✓ (端口 50816)", "Info")
160
+ } else {
161
+ $notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器未响应 ⚠", "Error")
162
+ }
163
+ }
164
+ })
165
+
166
+ # 7. 右键菜单
167
+ $menu = New-Object System.Windows.Forms.ContextMenuStrip
168
+
169
+ # 标题行
170
+ $titleItem = New-Object System.Windows.Forms.ToolStripMenuItem
171
+ $titleItem.Text = "Pan Router - :50816"
172
+ $titleItem.Enabled = $false
173
+ $titleItem.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
174
+ $menu.Items.Add($titleItem)
175
+ $menu.Items.Add("-")
176
+
177
+ # 开机自启动
178
+ $autoStartItem = New-Object System.Windows.Forms.ToolStripMenuItem
179
+ $autoStartItem.Text = "开机自启动"
180
+ $runKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
181
+ $autoStartItem.Checked = ($runKey.GetValue($autostartName) -ne $null)
182
+ $runKey.Close()
183
+ $autoStartItem.Add_Click({
184
+ $rk = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
185
+ if ($autoStartItem.Checked) {
186
+ $rk.DeleteValue($autostartName, $false)
187
+ $autoStartItem.Checked = $false
188
+ $notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", "Info")
189
+ } else {
190
+ $cmd = "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File `"$scriptPath`" -ServerPath `"$ServerPath`""
191
+ $rk.SetValue($autostartName, $cmd)
192
+ $autoStartItem.Checked = $true
193
+ $notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", "Info")
194
+ }
195
+ $rk.Close()
196
+ })
197
+ $menu.Items.Add($autoStartItem)
198
+
199
+ $menu.Items.Add("-")
200
+
201
+ # 退出
202
+ $exitItem = New-Object System.Windows.Forms.ToolStripMenuItem
203
+ $exitItem.Text = "退出"
204
+ $exitItem.Add_Click({
205
+ $notifyIcon.Visible = $false
206
+ [System.Windows.Forms.Application]::Exit()
207
+ })
208
+ $menu.Items.Add($exitItem)
209
+
210
+ $notifyIcon.ContextMenuStrip = $menu
211
+
212
+ # 8. 退出清理
213
+ $cleanup = {
214
+ try {
215
+ if ($checkTimer) { $checkTimer.Stop(); $checkTimer.Dispose() }
216
+ if ($serverProcess -and !$serverProcess.HasExited) {
217
+ $serverProcess.Kill()
218
+ $serverProcess.WaitForExit(3000)
219
+ $serverProcess.Dispose()
220
+ }
221
+ # 再补一刀: 确保无残留
222
+ Stop-OldServer
223
+ } catch {}
224
+ }
225
+
226
+ [System.Windows.Forms.Application]::ApplicationExit += {
227
+ & $cleanup
228
+ $notifyIcon.Dispose()
229
+ [System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
230
+ $icon.Dispose()
231
+ }
232
+
233
+ # 9. 运行消息循环
234
+ [System.Windows.Forms.Application]::Run()
235
+
236
+ # 10. 循环结束后再次清理
237
+ & $cleanup