panrouter 4.0.0 → 4.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 CHANGED
@@ -66,43 +66,31 @@ async function isPortOpen() {
66
66
  function stopAll() {
67
67
  log("..", "正在停止所有 Pan Router 进程...", "yellow");
68
68
  try {
69
- execSync('taskkill /f /fi "WINDOWTITLE eq Pan Router*" >nul 2>&1', { stdio: "pipe" });
70
- } catch {}
71
- try {
72
- execSync("taskkill /f /im powershell.exe >nul 2>&1", { stdio: "pipe" });
73
- } catch {}
74
- try {
75
- const out = execSync(
76
- 'wmic process where "name=\'node.exe\'" get ProcessId,CommandLine /format:csv 2>nul',
77
- { encoding: "utf8", windowsHide: true, timeout: 5000 }
78
- );
79
- for (const line of out.split("\n")) {
80
- if (line.includes("server.mjs")) {
81
- const m = line.match(/(\d+),.*?server\.mjs/);
82
- if (m) try { process.kill(parseInt(m[1]), "SIGKILL"); } catch {}
83
- }
69
+ if (process.platform === "win32") {
70
+ execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%server.mjs%\'" call terminate >nul 2>&1', { stdio: "pipe" });
71
+ execSync('wmic process where "name=\'powershell.exe\' and CommandLine like \'%tray-daemon.ps1%\'" call terminate >nul 2>&1', { stdio: "pipe" });
84
72
  }
85
- } catch {}
86
- log("OK", "已停止所有进程", "green");
73
+ log("OK", "已停止所有进程", "green");
74
+ } catch (e) {
75
+ log("!!", "停止进程时遇到问题", "red");
76
+ }
87
77
  }
88
78
 
89
79
  function showStatus() {
90
80
  console.log(`\n\x1b[36m=== Pan Router 状态 ===\x1b[0m\n`);
91
81
  try {
92
- const out = execSync(
93
- 'wmic process where "name=\'node.exe\'" get ProcessId,CommandLine /format:csv 2>nul',
94
- { encoding: "utf8", timeout: 5000 }
95
- );
96
- const nodePids = [];
97
- for (const line of out.split("\n")) {
98
- if (line.includes("server.mjs")) {
99
- const m = line.match(/(\d+),/);
100
- if (m) nodePids.push(m[1]);
101
- }
102
- }
103
- if (nodePids.length > 0) log("OK", `代理服务 (Node): 运行中 [PID: ${nodePids.join(", ")}]`, "green");
82
+ const nodeOut = execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%server.mjs%\'" get ProcessId 2>nul').toString();
83
+ const psOut = execSync('wmic process where "name=\'powershell.exe\' and CommandLine like \'%tray-daemon.ps1%\'" get ProcessId 2>nul').toString();
84
+
85
+ const nodePids = nodeOut.match(/\d+/g) || [];
86
+ const psPids = psOut.match(/\d+/g) || [];
87
+
88
+ if (nodePids.length > 0) log("OK", `代理服务 (Node): 运行中 [PID: ${nodePids.join(', ')}]`, "green");
104
89
  else log("!!", "代理服务 (Node): 未运行", "red");
105
- } catch {
90
+
91
+ if (psPids.length > 0) log("OK", `系统托盘 (PowerShell): 运行中 [PID: ${psPids.join(', ')}]`, "green");
92
+ else log("!!", "系统托盘 (PowerShell): 未运行", "red");
93
+ } catch (e) {
106
94
  log("!!", "无法获取状态", "red");
107
95
  }
108
96
  console.log("");
@@ -112,6 +100,7 @@ function openLogs() {
112
100
  const logFile = path.join(process.env.TEMP, "panrouter_tray.log");
113
101
  if (fs.existsSync(logFile)) {
114
102
  execSync(`start notepad "${logFile}"`);
103
+ log("OK", "日志已在记事本中打开", "green");
115
104
  } else {
116
105
  log("!!", "暂无托盘日志文件", "red");
117
106
  }
@@ -122,7 +111,11 @@ async function startServer() {
122
111
  stopAll();
123
112
 
124
113
  log("..", "正在启动代理...", "yellow");
125
- execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
114
+ if (process.platform === "win32") {
115
+ execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
116
+ } else {
117
+ spawn("node", [serverPath], { cwd: __dirname, stdio: "ignore", detached: true }).unref();
118
+ }
126
119
 
127
120
  for (let i = 0; i < 15; i++) {
128
121
  if (await isPortOpen()) break;
@@ -138,17 +131,20 @@ async function startTray() {
138
131
  stopAll();
139
132
  log("..", "正在后台启动代理...", "yellow");
140
133
 
141
- // UTF-8 BOM
134
+ // 追加 UTF-8 BOM 修复乱码
142
135
  try {
143
136
  const psContent = fs.readFileSync(psPath, "utf8");
144
- if (psContent.charCodeAt(0) !== 0xFEFF) {
137
+ if (!psContent.startsWith("")) {
145
138
  const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
146
139
  fs.writeFileSync(psPath, Buffer.concat([bom, Buffer.from(psContent, "utf8")]));
147
140
  }
148
- } catch {}
141
+ } catch (e) { }
149
142
 
150
143
  const srv = spawn(process.execPath, [serverPath], {
151
- cwd: __dirname, stdio: "ignore", windowsHide: true, detached: true
144
+ cwd: __dirname,
145
+ stdio: "ignore",
146
+ windowsHide: true,
147
+ detached: true
152
148
  });
153
149
  srv.unref();
154
150
 
@@ -161,15 +157,20 @@ async function startTray() {
161
157
  if (ok) log("OK", "代理服务已就绪!(端口 50816)", "green");
162
158
  else log("!!", "服务启动超时,但仍将尝试加载托盘", "red");
163
159
 
164
- log("..", "正在加载系统托盘...", "yellow");
160
+ log("..", "正在加载系统托盘与控制台引擎...", "yellow");
165
161
 
166
162
  const tray = spawn("powershell.exe", [
167
- "-NoProfile", "-STA", "-ExecutionPolicy", "Bypass",
168
- "-WindowStyle", "Hidden", "-File", `"${psPath}"`,
163
+ "-NoProfile",
164
+ "-STA",
165
+ "-ExecutionPolicy", "Bypass",
166
+ "-WindowStyle", "Hidden",
167
+ "-File", `"${psPath}"`
169
168
  ], {
170
- cwd: __dirname, stdio: ["ignore", "pipe", "pipe"],
171
- windowsHide: true, shell: true,
172
- env: { ...process.env, PANROUTER_NODE: process.execPath },
169
+ cwd: __dirname,
170
+ stdio: ['ignore', 'pipe', 'pipe'],
171
+ windowsHide: true,
172
+ shell: true,
173
+ env: { ...process.env, PANROUTER_NODE: process.execPath }
173
174
  });
174
175
 
175
176
  let psOutput = "";
@@ -193,10 +194,11 @@ function printHelp() {
193
194
 
194
195
  | 指令 | 功能 |
195
196
  |------|------|
196
- | \x1b[33mpanrouter\x1b[0m | 自动检测 Claude Code → 安装(如需要) → 配路由 → 启托盘 |
197
+ | \x1b[33mpanrouter\x1b[0m | 🔥 自动检测 Claude Code → 安装(如需要) → 配路由 → 启托盘 |
197
198
  | \x1b[33mpanrouter --setup\x1b[0m | 只配路由(恢复配置用) |
198
199
  | \x1b[33mpanrouter --status\x1b[0m | 查看运行状态 + PID |
199
200
  | \x1b[33mpanrouter --stop\x1b[0m | 停止所有进程 |
201
+ | \x1b[33mpanrouter --restart\x1b[0m | 重启托盘 |
200
202
  | \x1b[33mpanrouter --server\x1b[0m | 前台窗口模式 |
201
203
  | \x1b[33mpanrouter --logs\x1b[0m | 打开日志 |
202
204
  | \x1b[33mpanrouter --version\x1b[0m | 版本号 |
@@ -208,7 +210,7 @@ async function main() {
208
210
  const args = process.argv.slice(2);
209
211
  const cmd = args[0];
210
212
 
211
- switch (cmd) {
213
+ switch(cmd) {
212
214
  case "--help":
213
215
  case "-h":
214
216
  printHelp();
@@ -229,6 +231,9 @@ async function main() {
229
231
  case "--logs":
230
232
  openLogs();
231
233
  break;
234
+ case "--restart":
235
+ await startTray();
236
+ break;
232
237
  case "--server":
233
238
  await startServer();
234
239
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
package/server.mjs CHANGED
@@ -305,6 +305,7 @@ const server = http.createServer(async (req, res) => {
305
305
  }
306
306
  } catch(e) {}
307
307
 
308
+ // 按时间倒序,返回最近 100 条给前端展示
308
309
  history.sort((a,b) => b.ts - a.ts);
309
310
  const recent = history.slice(0, 100);
310
311
 
@@ -380,6 +381,7 @@ const server = http.createServer(async (req, res) => {
380
381
  }
381
382
  });
382
383
 
384
+ // 请求结束时记录统计数据
383
385
  upstream.stream.on("end", () => {
384
386
  if (state.model) {
385
387
  const inTokens = state.usage?.input_tokens || 0;
package/tray-daemon.ps1 CHANGED
@@ -1,6 +1,6 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Pan Router 托盘守护脚本 (原生桌面控制台版)
3
+ Pan Router 托盘守护脚本 (原生桌面控制台版 - 作用域修复)
4
4
  #>
5
5
  $ErrorActionPreference = "Stop"
6
6
 
@@ -68,8 +68,13 @@ try {
68
68
  $notifyIcon.Icon = [System.Drawing.SystemIcons]::Shield
69
69
  }
70
70
 
71
- # ====== 原生 WinForms 数据面板 ======
71
+ # ====== 【原生 WinForms 数据面板 (修复变量回收 Bug)】 ======
72
72
  $global:dashForm = $null
73
+ $script:lblReq = $null
74
+ $script:lblIn = $null
75
+ $script:lblOut = $null
76
+ $script:cmbPeriod = $null
77
+ $script:lv = $null
73
78
 
74
79
  function Show-Dashboard {
75
80
  if ($global:dashForm -ne $null -and -not $global:dashForm.IsDisposed) {
@@ -93,25 +98,25 @@ try {
93
98
  $grpSummary.Size = New-Object System.Drawing.Size(515, 65)
94
99
  $form.Controls.Add($grpSummary)
95
100
 
96
- $lblReq = New-Object System.Windows.Forms.Label
97
- $lblReq.Location = New-Object System.Drawing.Point(20, 28)
98
- $lblReq.Size = New-Object System.Drawing.Size(140, 20)
99
- $lblReq.Text = "请求总数: -"
100
- $grpSummary.Controls.Add($lblReq)
101
-
102
- $lblIn = New-Object System.Windows.Forms.Label
103
- $lblIn.Location = New-Object System.Drawing.Point(160, 28)
104
- $lblIn.Size = New-Object System.Drawing.Size(160, 20)
105
- $lblIn.Text = "输入 Token: -"
106
- $lblIn.ForeColor = [System.Drawing.Color]::MediumBlue
107
- $grpSummary.Controls.Add($lblIn)
108
-
109
- $lblOut = New-Object System.Windows.Forms.Label
110
- $lblOut.Location = New-Object System.Drawing.Point(340, 28)
111
- $lblOut.Size = New-Object System.Drawing.Size(160, 20)
112
- $lblOut.Text = "输出 Token: -"
113
- $lblOut.ForeColor = [System.Drawing.Color]::ForestGreen
114
- $grpSummary.Controls.Add($lblOut)
101
+ $script:lblReq = New-Object System.Windows.Forms.Label
102
+ $script:lblReq.Location = New-Object System.Drawing.Point(20, 28)
103
+ $script:lblReq.Size = New-Object System.Drawing.Size(140, 20)
104
+ $script:lblReq.Text = "请求总数: -"
105
+ $grpSummary.Controls.Add($script:lblReq)
106
+
107
+ $script:lblIn = New-Object System.Windows.Forms.Label
108
+ $script:lblIn.Location = New-Object System.Drawing.Point(160, 28)
109
+ $script:lblIn.Size = New-Object System.Drawing.Size(160, 20)
110
+ $script:lblIn.Text = "输入 Token: -"
111
+ $script:lblIn.ForeColor = [System.Drawing.Color]::MediumBlue
112
+ $grpSummary.Controls.Add($script:lblIn)
113
+
114
+ $script:lblOut = New-Object System.Windows.Forms.Label
115
+ $script:lblOut.Location = New-Object System.Drawing.Point(340, 28)
116
+ $script:lblOut.Size = New-Object System.Drawing.Size(160, 20)
117
+ $script:lblOut.Text = "输出 Token: -"
118
+ $script:lblOut.ForeColor = [System.Drawing.Color]::ForestGreen
119
+ $grpSummary.Controls.Add($script:lblOut)
115
120
 
116
121
  $lblFilter = New-Object System.Windows.Forms.Label
117
122
  $lblFilter.Text = "时间筛选:"
@@ -119,55 +124,58 @@ try {
119
124
  $lblFilter.AutoSize = $true
120
125
  $form.Controls.Add($lblFilter)
121
126
 
122
- $cmbPeriod = New-Object System.Windows.Forms.ComboBox
123
- $cmbPeriod.Items.AddRange(@("最近 1 天", "最近 7 天", "最近 30 天", "全部时间"))
124
- $cmbPeriod.SelectedIndex = 3
125
- $cmbPeriod.Location = New-Object System.Drawing.Point(80, 85)
126
- $cmbPeriod.Size = New-Object System.Drawing.Size(120, 20)
127
- $cmbPeriod.DropDownStyle = 'DropDownList'
128
- $form.Controls.Add($cmbPeriod)
129
-
130
- $lv = New-Object System.Windows.Forms.ListView
131
- $lv.Location = New-Object System.Drawing.Point(15, 115)
132
- $lv.Size = New-Object System.Drawing.Size(515, 310)
133
- $lv.View = 'Details'
134
- $lv.FullRowSelect = $true
135
- $lv.GridLines = $true
136
- $lv.Columns.Add("时间", 135) | Out-Null
137
- $lv.Columns.Add("模型", 185) | Out-Null
138
- $lv.Columns.Add("输入", 85) | Out-Null
139
- $lv.Columns.Add("输出", 85) | Out-Null
140
- $form.Controls.Add($lv)
127
+ $script:cmbPeriod = New-Object System.Windows.Forms.ComboBox
128
+ $script:cmbPeriod.Items.AddRange(@("最近 1 天", "最近 7 天", "最近 30 天", "全部时间"))
129
+ $script:cmbPeriod.SelectedIndex = 3
130
+ $script:cmbPeriod.Location = New-Object System.Drawing.Point(80, 85)
131
+ $script:cmbPeriod.Size = New-Object System.Drawing.Size(120, 20)
132
+ $script:cmbPeriod.DropDownStyle = 'DropDownList'
133
+ $form.Controls.Add($script:cmbPeriod)
134
+
135
+ $script:lv = New-Object System.Windows.Forms.ListView
136
+ $script:lv.Location = New-Object System.Drawing.Point(15, 115)
137
+ $script:lv.Size = New-Object System.Drawing.Size(515, 310)
138
+ $script:lv.View = 'Details'
139
+ $script:lv.FullRowSelect = $true
140
+ $script:lv.GridLines = $true
141
+ $script:lv.Columns.Add("时间", 135) | Out-Null
142
+ $script:lv.Columns.Add("模型", 185) | Out-Null
143
+ $script:lv.Columns.Add("输入", 85) | Out-Null
144
+ $script:lv.Columns.Add("输出", 85) | Out-Null
145
+ $form.Controls.Add($script:lv)
141
146
 
142
147
  $updateData = {
148
+ if ($null -eq $script:cmbPeriod -or $null -eq $script:cmbPeriod.SelectedItem) { return }
149
+
143
150
  $map = @{"最近 1 天"="1"; "最近 7 天"="7"; "最近 30 天"="30"; "全部时间"="all"}
144
- $p = $map[$cmbPeriod.SelectedItem.ToString()]
151
+ $p = $map[$script:cmbPeriod.SelectedItem.ToString()]
152
+
145
153
  try {
146
154
  $data = Invoke-RestMethod -Uri "http://127.0.0.1:50816/api/stats?period=$p" -Method Get -ErrorAction Stop
147
155
 
148
- $lblReq.Text = "请求总数: {0:N0}" -f $data.totalReq
149
- $lblIn.Text = "输入 Token: {0:N0}" -f $data.totalIn
150
- $lblOut.Text = "输出 Token: {0:N0}" -f $data.totalOut
156
+ $script:lblReq.Text = "请求总数: {0:N0}" -f $data.totalReq
157
+ $script:lblIn.Text = "输入 Token: {0:N0}" -f $data.totalIn
158
+ $script:lblOut.Text = "输出 Token: {0:N0}" -f $data.totalOut
151
159
 
152
- $lv.Items.Clear()
160
+ $script:lv.Items.Clear()
153
161
  if ($data.recent) {
154
- $lv.BeginUpdate()
162
+ $script:lv.BeginUpdate()
155
163
  foreach ($r in $data.recent) {
156
164
  $dt = [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').AddMilliseconds($r.ts))
157
165
  $item = New-Object System.Windows.Forms.ListViewItem($dt.ToString("yyyy-MM-dd HH:mm:ss"))
158
166
  $item.SubItems.Add($r.m) | Out-Null
159
167
  $item.SubItems.Add(("{0:N0} ↑" -f $r.i)) | Out-Null
160
168
  $item.SubItems.Add(("{0:N0} ↓" -f $r.o)) | Out-Null
161
- $lv.Items.Add($item) | Out-Null
169
+ $script:lv.Items.Add($item) | Out-Null
162
170
  }
163
- $lv.EndUpdate()
171
+ $script:lv.EndUpdate()
164
172
  }
165
173
  } catch {
166
- $lblReq.Text = "无法连接统计接口"
174
+ $script:lblReq.Text = "无法连接统计接口,请检查服务状态"
167
175
  }
168
176
  }
169
177
 
170
- $cmbPeriod.Add_SelectedIndexChanged($updateData)
178
+ $script:cmbPeriod.Add_SelectedIndexChanged($updateData)
171
179
  & $updateData
172
180
 
173
181
  $form.Show()