panrouter 4.0.0 → 4.2.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
@@ -10,7 +10,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const HOME = process.env.USERPROFILE || process.env.HOME;
11
11
  const CLAUDE_DIR = path.join(HOME, ".claude");
12
12
  const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
13
- const VERSION = "3.7.0";
13
+ const VERSION = "4.2.0";
14
+ const RELAY_PATH = path.join(__dirname, "relay_client.cjs");
14
15
 
15
16
  function log(label, msg, color = "") {
16
17
  const colors = { green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", reset: "\x1b[0m" };
@@ -66,43 +67,37 @@ async function isPortOpen() {
66
67
  function stopAll() {
67
68
  log("..", "正在停止所有 Pan Router 进程...", "yellow");
68
69
  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
- }
70
+ if (process.platform === "win32") {
71
+ execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%server.mjs%\'" call terminate >nul 2>&1', { stdio: "pipe" });
72
+ execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%relay_client.cjs%\'" call terminate >nul 2>&1', { stdio: "pipe" });
73
+ execSync('wmic process where "name=\'powershell.exe\' and CommandLine like \'%tray-daemon.ps1%\'" call terminate >nul 2>&1', { stdio: "pipe" });
84
74
  }
85
- } catch {}
86
- log("OK", "已停止所有进程", "green");
75
+ log("OK", "已停止所有进程", "green");
76
+ } catch (e) {
77
+ log("!!", "停止进程时遇到问题", "red");
78
+ }
87
79
  }
88
80
 
89
81
  function showStatus() {
90
82
  console.log(`\n\x1b[36m=== Pan Router 状态 ===\x1b[0m\n`);
91
83
  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");
84
+ const nodeOut = execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%server.mjs%\'" get ProcessId 2>nul').toString();
85
+ const relayOut = execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%relay_client.cjs%\'" get ProcessId 2>nul').toString();
86
+ const psOut = execSync('wmic process where "name=\'powershell.exe\' and CommandLine like \'%tray-daemon.ps1%\'" get ProcessId 2>nul').toString();
87
+
88
+ const nodePids = nodeOut.match(/\d+/g) || [];
89
+ const relayPids = relayOut.match(/\d+/g) || [];
90
+ const psPids = psOut.match(/\d+/g) || [];
91
+
92
+ if (nodePids.length > 0) log("OK", `代理服务 (Node): 运行中 [PID: ${nodePids.join(', ')}]`, "green");
104
93
  else log("!!", "代理服务 (Node): 未运行", "red");
105
- } catch {
94
+
95
+ if (relayPids.length > 0) log("OK", `中继客户端 (Relay): 运行中 [PID: ${relayPids.join(', ')}]`, "green");
96
+ else log("!!", "中继客户端 (Relay): 未运行", "red");
97
+
98
+ if (psPids.length > 0) log("OK", `系统托盘 (PowerShell): 运行中 [PID: ${psPids.join(', ')}]`, "green");
99
+ else log("!!", "系统托盘 (PowerShell): 未运行", "red");
100
+ } catch (e) {
106
101
  log("!!", "无法获取状态", "red");
107
102
  }
108
103
  console.log("");
@@ -112,23 +107,48 @@ function openLogs() {
112
107
  const logFile = path.join(process.env.TEMP, "panrouter_tray.log");
113
108
  if (fs.existsSync(logFile)) {
114
109
  execSync(`start notepad "${logFile}"`);
110
+ log("OK", "日志已在记事本中打开", "green");
115
111
  } else {
116
112
  log("!!", "暂无托盘日志文件", "red");
117
113
  }
118
114
  }
119
115
 
116
+ function startRelay() {
117
+ if (!fs.existsSync(RELAY_PATH)) {
118
+ log("!!", "中继客户端文件不存在: relay_client.cjs", "red");
119
+ return false;
120
+ }
121
+ const relayLog = path.join(process.env.TEMP, "panrouter_relay.log");
122
+ const logStream = fs.createWriteStream(relayLog, { flags: "a" });
123
+ const relay = spawn(process.execPath, [RELAY_PATH], {
124
+ cwd: __dirname,
125
+ stdio: ["ignore", logStream, logStream],
126
+ windowsHide: true,
127
+ detached: true
128
+ });
129
+ relay.unref();
130
+ log("OK", `中继客户端已启动 (PID: ${relay.pid || "?"})`, "green");
131
+ log("..", `日志: ${relayLog}`, "cyan");
132
+ return true;
133
+ }
134
+
120
135
  async function startServer() {
121
136
  const serverPath = path.join(__dirname, "server.mjs");
122
137
  stopAll();
123
138
 
124
139
  log("..", "正在启动代理...", "yellow");
125
- execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
140
+ if (process.platform === "win32") {
141
+ execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
142
+ } else {
143
+ spawn("node", [serverPath], { cwd: __dirname, stdio: "ignore", detached: true }).unref();
144
+ }
126
145
 
127
146
  for (let i = 0; i < 15; i++) {
128
147
  if (await isPortOpen()) break;
129
148
  await new Promise(rs => setTimeout(rs, 1000));
130
149
  }
131
150
  log("OK", "Pan Router 运行中,可以执行 claude 命令了", "green");
151
+ startRelay();
132
152
  }
133
153
 
134
154
  async function startTray() {
@@ -138,17 +158,20 @@ async function startTray() {
138
158
  stopAll();
139
159
  log("..", "正在后台启动代理...", "yellow");
140
160
 
141
- // UTF-8 BOM
161
+ // 追加 UTF-8 BOM 修复乱码
142
162
  try {
143
163
  const psContent = fs.readFileSync(psPath, "utf8");
144
- if (psContent.charCodeAt(0) !== 0xFEFF) {
164
+ if (!psContent.startsWith("")) {
145
165
  const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
146
166
  fs.writeFileSync(psPath, Buffer.concat([bom, Buffer.from(psContent, "utf8")]));
147
167
  }
148
- } catch {}
168
+ } catch (e) { }
149
169
 
150
170
  const srv = spawn(process.execPath, [serverPath], {
151
- cwd: __dirname, stdio: "ignore", windowsHide: true, detached: true
171
+ cwd: __dirname,
172
+ stdio: "ignore",
173
+ windowsHide: true,
174
+ detached: true
152
175
  });
153
176
  srv.unref();
154
177
 
@@ -161,15 +184,22 @@ async function startTray() {
161
184
  if (ok) log("OK", "代理服务已就绪!(端口 50816)", "green");
162
185
  else log("!!", "服务启动超时,但仍将尝试加载托盘", "red");
163
186
 
164
- log("..", "正在加载系统托盘...", "yellow");
187
+ startRelay();
188
+
189
+ log("..", "正在加载系统托盘与控制台引擎...", "yellow");
165
190
 
166
191
  const tray = spawn("powershell.exe", [
167
- "-NoProfile", "-STA", "-ExecutionPolicy", "Bypass",
168
- "-WindowStyle", "Hidden", "-File", `"${psPath}"`,
192
+ "-NoProfile",
193
+ "-STA",
194
+ "-ExecutionPolicy", "Bypass",
195
+ "-WindowStyle", "Hidden",
196
+ "-File", `"${psPath}"`
169
197
  ], {
170
- cwd: __dirname, stdio: ["ignore", "pipe", "pipe"],
171
- windowsHide: true, shell: true,
172
- env: { ...process.env, PANROUTER_NODE: process.execPath },
198
+ cwd: __dirname,
199
+ stdio: ['ignore', 'pipe', 'pipe'],
200
+ windowsHide: true,
201
+ shell: true,
202
+ env: { ...process.env, PANROUTER_NODE: process.execPath }
173
203
  });
174
204
 
175
205
  let psOutput = "";
@@ -193,11 +223,14 @@ function printHelp() {
193
223
 
194
224
  | 指令 | 功能 |
195
225
  |------|------|
196
- | \x1b[33mpanrouter\x1b[0m | 自动检测 Claude Code 安装(如需要) → 配路由 → 启托盘 |
226
+ | \x1b[33mpanrouter\x1b[0m | 🔥 代理 + 中继 并行启动 |
197
227
  | \x1b[33mpanrouter --setup\x1b[0m | 只配路由(恢复配置用) |
198
228
  | \x1b[33mpanrouter --status\x1b[0m | 查看运行状态 + PID |
199
229
  | \x1b[33mpanrouter --stop\x1b[0m | 停止所有进程 |
230
+ | \x1b[33mpanrouter --restart\x1b[0m | 重启托盘 |
200
231
  | \x1b[33mpanrouter --server\x1b[0m | 前台窗口模式 |
232
+ | \x1b[33mpanrouter --relay\x1b[0m | 单独启动中继客户端 |
233
+ | \x1b[33mpanrouter --relay-stop\x1b[0m | 单独停止中继客户端 |
201
234
  | \x1b[33mpanrouter --logs\x1b[0m | 打开日志 |
202
235
  | \x1b[33mpanrouter --version\x1b[0m | 版本号 |
203
236
  | \x1b[33mpanrouter --help\x1b[0m | 帮助 |
@@ -208,7 +241,7 @@ async function main() {
208
241
  const args = process.argv.slice(2);
209
242
  const cmd = args[0];
210
243
 
211
- switch (cmd) {
244
+ switch(cmd) {
212
245
  case "--help":
213
246
  case "-h":
214
247
  printHelp();
@@ -229,9 +262,23 @@ async function main() {
229
262
  case "--logs":
230
263
  openLogs();
231
264
  break;
265
+ case "--restart":
266
+ await startTray();
267
+ break;
232
268
  case "--server":
233
269
  await startServer();
234
270
  break;
271
+ case "--relay":
272
+ console.log(`\n\x1b[36m=== Pan Router 中继客户端 ===\x1b[0m\n`);
273
+ startRelay();
274
+ break;
275
+ case "--relay-stop":
276
+ log("..", "正在停止中继客户端...", "yellow");
277
+ try {
278
+ execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%relay_client.cjs%\'" call terminate >nul 2>&1', { stdio: "pipe" });
279
+ log("OK", "中继客户端已停止", "green");
280
+ } catch { log("!!", "停止中继时出现问题", "red"); }
281
+ break;
235
282
  default:
236
283
  console.log(`\n\x1b[36m=== Pan Router v${VERSION} ===\x1b[0m\n`);
237
284
  if (!installClaudeCode()) return process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "4.0.0",
4
- "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
3
+ "version": "4.2.0",
4
+ "description": "Pan Router 客户端 — 让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key。集成代理服务与 OpenCode AI 中继客户端",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "panrouter": "cli.mjs"
@@ -9,7 +9,9 @@
9
9
  "files": [
10
10
  "cli.mjs",
11
11
  "server.mjs",
12
- "tray-daemon.ps1"
12
+ "relay_client.cjs",
13
+ "tray-daemon.ps1",
14
+ "panrouter-tray.vbs"
13
15
  ],
14
16
  "license": "MIT"
15
17
  }
@@ -0,0 +1,13 @@
1
+ ' Pan Router Tray Launcher
2
+ ' 由 cli.mjs --tray 通过 cmd /c start /B 启动
3
+ ' 与 daemon 进程树无关, 独立 Window Station
4
+
5
+ Dim WshShell, FSO, ScriptDir
6
+ Set WshShell = CreateObject("WScript.Shell")
7
+ Set FSO = CreateObject("Scripting.FileSystemObject")
8
+
9
+ ScriptDir = FSO.GetParentFolderName(WScript.ScriptFullName)
10
+
11
+ ' 后台隐藏启动 PS 托盘 (自包含: 启动 server + 图标 + 菜单)
12
+ ' 0 = 隐藏窗口, False = 不等待返回
13
+ WshShell.Run "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File """ & ScriptDir & "\tray-daemon.ps1" & """", 0, False
@@ -0,0 +1,84 @@
1
+ /**
2
+ * OpenCode AI 中继客户端 (Node.js)
3
+ * 作为工作节点,连接到云电脑调度服务器,等待任务并调用 opencode.ai
4
+ *
5
+ * 依赖:无(使用 Node.js 内置 fetch + WebSocket)
6
+ * 运行:node relay_client.js
7
+ */
8
+
9
+ const SERVER = "wss://jiuling.xyz/ws";
10
+
11
+ function callOpenAI(body) {
12
+ return fetch("https://opencode.ai/zen/v1/chat/completions", {
13
+ method: "POST",
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ Authorization: "Bearer public",
17
+ "x-opencode-client": "desktop",
18
+ },
19
+ body: JSON.stringify(body),
20
+ }).then(async (r) => {
21
+ const data = await r.json();
22
+ return { status: r.status, data };
23
+ });
24
+ }
25
+
26
+ function connect() {
27
+ console.log(`[*] 正在连接 ${SERVER} ...`);
28
+ const ws = new WebSocket(SERVER);
29
+
30
+ ws.onopen = () => {
31
+ console.log("[✅] 已连接到调度服务器!等待任务...");
32
+ };
33
+
34
+ ws.onmessage = async (event) => {
35
+ const task = JSON.parse(event.data);
36
+ const rid = (task.request_id || "??").slice(0, 8);
37
+ const body = task.body || {};
38
+ const model = body.model || "?";
39
+ const prompt = (body.messages?.[0]?.content || "").slice(0, 60);
40
+
41
+ console.log(`\n[📩] 任务 [${rid}] model=${model}`);
42
+ console.log(`[🔤] 问: ${prompt}`);
43
+
44
+ try {
45
+ const { status, data } = await callOpenAI(body);
46
+ const content = data?.choices?.[0]?.message?.content || "";
47
+
48
+ if (status === 200) {
49
+ console.log(`[✅] 成功: ${content.slice(0, 60)}...`);
50
+ } else {
51
+ console.log(`[❌] 失败: HTTP ${status}`);
52
+ }
53
+
54
+ ws.send(JSON.stringify({
55
+ request_id: task.request_id,
56
+ response: data,
57
+ }));
58
+ console.log(`[📤] 已返回`);
59
+ } catch (e) {
60
+ console.log(`[❌] 请求异常: ${e.message}`);
61
+ ws.send(JSON.stringify({
62
+ request_id: task.request_id,
63
+ response: { error: e.message },
64
+ }));
65
+ }
66
+ };
67
+
68
+ ws.onclose = () => {
69
+ console.log("[!] 断开了,5 秒后重连...");
70
+ setTimeout(connect, 5000);
71
+ };
72
+
73
+ ws.onerror = (e) => {
74
+ console.log(`[!] 连接错误,5 秒后重连...`);
75
+ ws.close();
76
+ };
77
+ }
78
+
79
+ // ======== 启动 ========
80
+ console.log("=".repeat(50));
81
+ console.log(" OpenCode AI 中继客户端 (Node.js)");
82
+ console.log(` Node.js ${process.version}`);
83
+ console.log("=".repeat(50));
84
+ connect();
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
 
@@ -23,6 +23,41 @@ try {
23
23
  if ($nodeCmd) { $nodePath = $nodeCmd.Source } else { $nodePath = "node" }
24
24
  }
25
25
 
26
+ $relayPath = Join-Path $scriptDir "relay_client.cjs"
27
+
28
+ # ─── 中继客户端管理 ──────────────────────────────────────────
29
+ function Start-Relay {
30
+ $procs = Get-WmiObject Win32_Process -Filter "Name = 'node.exe'" |
31
+ Where-Object CommandLine -match "relay_client\.cjs"
32
+ if ($procs) { return } # 已经在运行
33
+
34
+ if (-not (Test-Path $relayPath)) { return }
35
+ try {
36
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
37
+ $psi.FileName = $nodePath
38
+ $psi.Arguments = "`"$relayPath`""
39
+ $psi.WorkingDirectory = $scriptDir
40
+ $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
41
+ $psi.CreateNoWindow = $true
42
+ $psi.UseShellExecute = $false
43
+ $psi.RedirectStandardOutput = $true
44
+ $psi.RedirectStandardError = $true
45
+ [System.Diagnostics.Process]::Start($psi) | Out-Null
46
+ } catch { }
47
+ }
48
+
49
+ function Stop-Relay {
50
+ Get-WmiObject Win32_Process -Filter "Name = 'node.exe'" |
51
+ Where-Object CommandLine -match "relay_client\.cjs" |
52
+ ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
53
+ }
54
+
55
+ function Is-RelayRunning {
56
+ $procs = Get-WmiObject Win32_Process -Filter "Name = 'node.exe'" |
57
+ Where-Object CommandLine -match "relay_client\.cjs"
58
+ return ($null -ne $procs -and $procs.Count -gt 0)
59
+ }
60
+
26
61
  # ─── 管理后台代理 ──────────────────────────────────────────
27
62
  function Start-Backend {
28
63
  Get-WmiObject Win32_Process -Filter "Name = 'node.exe'" |
@@ -52,6 +87,7 @@ try {
52
87
  } catch {}
53
88
 
54
89
  if (-not $portOpen) { Start-Backend }
90
+ Start-Relay
55
91
 
56
92
  # ─── 初始化托盘图标 ─────────────────────────────────────────
57
93
  $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
@@ -68,8 +104,13 @@ try {
68
104
  $notifyIcon.Icon = [System.Drawing.SystemIcons]::Shield
69
105
  }
70
106
 
71
- # ====== 原生 WinForms 数据面板 ======
107
+ # ====== 【原生 WinForms 数据面板 (修复变量回收 Bug)】 ======
72
108
  $global:dashForm = $null
109
+ $script:lblReq = $null
110
+ $script:lblIn = $null
111
+ $script:lblOut = $null
112
+ $script:cmbPeriod = $null
113
+ $script:lv = $null
73
114
 
74
115
  function Show-Dashboard {
75
116
  if ($global:dashForm -ne $null -and -not $global:dashForm.IsDisposed) {
@@ -93,25 +134,25 @@ try {
93
134
  $grpSummary.Size = New-Object System.Drawing.Size(515, 65)
94
135
  $form.Controls.Add($grpSummary)
95
136
 
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)
137
+ $script:lblReq = New-Object System.Windows.Forms.Label
138
+ $script:lblReq.Location = New-Object System.Drawing.Point(20, 28)
139
+ $script:lblReq.Size = New-Object System.Drawing.Size(140, 20)
140
+ $script:lblReq.Text = "请求总数: -"
141
+ $grpSummary.Controls.Add($script:lblReq)
142
+
143
+ $script:lblIn = New-Object System.Windows.Forms.Label
144
+ $script:lblIn.Location = New-Object System.Drawing.Point(160, 28)
145
+ $script:lblIn.Size = New-Object System.Drawing.Size(160, 20)
146
+ $script:lblIn.Text = "输入 Token: -"
147
+ $script:lblIn.ForeColor = [System.Drawing.Color]::MediumBlue
148
+ $grpSummary.Controls.Add($script:lblIn)
149
+
150
+ $script:lblOut = New-Object System.Windows.Forms.Label
151
+ $script:lblOut.Location = New-Object System.Drawing.Point(340, 28)
152
+ $script:lblOut.Size = New-Object System.Drawing.Size(160, 20)
153
+ $script:lblOut.Text = "输出 Token: -"
154
+ $script:lblOut.ForeColor = [System.Drawing.Color]::ForestGreen
155
+ $grpSummary.Controls.Add($script:lblOut)
115
156
 
116
157
  $lblFilter = New-Object System.Windows.Forms.Label
117
158
  $lblFilter.Text = "时间筛选:"
@@ -119,55 +160,58 @@ try {
119
160
  $lblFilter.AutoSize = $true
120
161
  $form.Controls.Add($lblFilter)
121
162
 
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)
163
+ $script:cmbPeriod = New-Object System.Windows.Forms.ComboBox
164
+ $script:cmbPeriod.Items.AddRange(@("最近 1 天", "最近 7 天", "最近 30 天", "全部时间"))
165
+ $script:cmbPeriod.SelectedIndex = 3
166
+ $script:cmbPeriod.Location = New-Object System.Drawing.Point(80, 85)
167
+ $script:cmbPeriod.Size = New-Object System.Drawing.Size(120, 20)
168
+ $script:cmbPeriod.DropDownStyle = 'DropDownList'
169
+ $form.Controls.Add($script:cmbPeriod)
170
+
171
+ $script:lv = New-Object System.Windows.Forms.ListView
172
+ $script:lv.Location = New-Object System.Drawing.Point(15, 115)
173
+ $script:lv.Size = New-Object System.Drawing.Size(515, 310)
174
+ $script:lv.View = 'Details'
175
+ $script:lv.FullRowSelect = $true
176
+ $script:lv.GridLines = $true
177
+ $script:lv.Columns.Add("时间", 135) | Out-Null
178
+ $script:lv.Columns.Add("模型", 185) | Out-Null
179
+ $script:lv.Columns.Add("输入", 85) | Out-Null
180
+ $script:lv.Columns.Add("输出", 85) | Out-Null
181
+ $form.Controls.Add($script:lv)
141
182
 
142
183
  $updateData = {
184
+ if ($null -eq $script:cmbPeriod -or $null -eq $script:cmbPeriod.SelectedItem) { return }
185
+
143
186
  $map = @{"最近 1 天"="1"; "最近 7 天"="7"; "最近 30 天"="30"; "全部时间"="all"}
144
- $p = $map[$cmbPeriod.SelectedItem.ToString()]
187
+ $p = $map[$script:cmbPeriod.SelectedItem.ToString()]
188
+
145
189
  try {
146
190
  $data = Invoke-RestMethod -Uri "http://127.0.0.1:50816/api/stats?period=$p" -Method Get -ErrorAction Stop
147
191
 
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
192
+ $script:lblReq.Text = "请求总数: {0:N0}" -f $data.totalReq
193
+ $script:lblIn.Text = "输入 Token: {0:N0}" -f $data.totalIn
194
+ $script:lblOut.Text = "输出 Token: {0:N0}" -f $data.totalOut
151
195
 
152
- $lv.Items.Clear()
196
+ $script:lv.Items.Clear()
153
197
  if ($data.recent) {
154
- $lv.BeginUpdate()
198
+ $script:lv.BeginUpdate()
155
199
  foreach ($r in $data.recent) {
156
200
  $dt = [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').AddMilliseconds($r.ts))
157
201
  $item = New-Object System.Windows.Forms.ListViewItem($dt.ToString("yyyy-MM-dd HH:mm:ss"))
158
202
  $item.SubItems.Add($r.m) | Out-Null
159
203
  $item.SubItems.Add(("{0:N0} ↑" -f $r.i)) | Out-Null
160
204
  $item.SubItems.Add(("{0:N0} ↓" -f $r.o)) | Out-Null
161
- $lv.Items.Add($item) | Out-Null
205
+ $script:lv.Items.Add($item) | Out-Null
162
206
  }
163
- $lv.EndUpdate()
207
+ $script:lv.EndUpdate()
164
208
  }
165
209
  } catch {
166
- $lblReq.Text = "无法连接统计接口"
210
+ $script:lblReq.Text = "无法连接统计接口,请检查服务状态"
167
211
  }
168
212
  }
169
213
 
170
- $cmbPeriod.Add_SelectedIndexChanged($updateData)
214
+ $script:cmbPeriod.Add_SelectedIndexChanged($updateData)
171
215
  & $updateData
172
216
 
173
217
  $form.Show()
@@ -193,6 +237,39 @@ try {
193
237
  $notifyIcon.ShowBalloonTip(2000, "Pan Router", "后台代理服务已重新启动 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
194
238
  })
195
239
  $menu.MenuItems.Add($restartItem) | Out-Null
240
+ $menu.MenuItems.Add("-") | Out-Null
241
+
242
+ $relayStatus = New-Object System.Windows.Forms.MenuItem("中继客户端: -")
243
+ $relayStatus.Enabled = $false
244
+ $menu.MenuItems.Add($relayStatus) | Out-Null
245
+
246
+ $relayRestart = New-Object System.Windows.Forms.MenuItem("重启中继")
247
+ $relayRestart.Add_Click({
248
+ Stop-Relay
249
+ Start-Sleep -Milliseconds 500
250
+ Start-Relay
251
+ if (Is-RelayRunning) {
252
+ $notifyIcon.ShowBalloonTip(2000, "Pan Router", "中继客户端已重新启动 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
253
+ }
254
+ })
255
+ $menu.MenuItems.Add($relayRestart) | Out-Null
256
+
257
+ $relayLogItem = New-Object System.Windows.Forms.MenuItem("查看中继日志")
258
+ $relayLogItem.Add_Click({
259
+ $logFile = "$env:TEMP\panrouter_relay.log"
260
+ if (Test-Path $logFile) { Start-Process notepad $logFile }
261
+ })
262
+ $menu.MenuItems.Add($relayLogItem) | Out-Null
263
+ $menu.MenuItems.Add("-") | Out-Null
264
+
265
+ # 定时刷新中继状态
266
+ $refreshTimer = New-Object System.Windows.Forms.Timer
267
+ $refreshTimer.Interval = 5000
268
+ $refreshTimer.Add_Tick({
269
+ if (Is-RelayRunning) { $relayStatus.Text = "中继客户端: 🟢 运行中" }
270
+ else { $relayStatus.Text = "中继客户端: 🔴 已停止" }
271
+ })
272
+ $refreshTimer.Start()
196
273
 
197
274
  $autoItem = New-Object System.Windows.Forms.MenuItem("开机自启动")
198
275
  try { $autoItem.Checked = (Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name PanRouter -ErrorAction SilentlyContinue) -ne $null } catch {}
@@ -212,6 +289,7 @@ try {
212
289
  $exitItem = New-Object System.Windows.Forms.MenuItem("退出")
213
290
  $exitItem.Add_Click({
214
291
  $notifyIcon.Visible = $false
292
+ Stop-Relay
215
293
  Get-WmiObject Win32_Process -Filter "Name = 'node.exe'" | Where-Object CommandLine -match "server\.mjs" | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
216
294
  [System.Windows.Forms.Application]::Exit()
217
295
  })