panrouter 1.0.4 → 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.
- package/cli.mjs +71 -14
- package/package.json +3 -2
- package/tray-manager.ps1 +237 -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.0
|
|
3
|
+
"version": "1.1.0",
|
|
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,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
|