panrouter 1.1.1 → 1.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 +21 -12
- package/daemon.mjs +178 -0
- package/package.json +2 -1
- package/tray-manager.ps1 +93 -205
package/cli.mjs
CHANGED
|
@@ -138,30 +138,39 @@ async function startServer() {
|
|
|
138
138
|
console.log("\n 现在可以运行: \x1b[33mclaude \"你好\"\x1b[0m\n");
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// ─── 4.
|
|
141
|
+
// ─── 4. 以托盘模式启动(后台守护进程 + 系统托盘) ──────────────────────────
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* 启动策略:
|
|
145
|
+
* 1. 启动 daemon.mjs (detached, 无窗口)
|
|
146
|
+
* → daemon 启动隐藏的 server.mjs
|
|
147
|
+
* → daemon 启动 tray-manager.ps1 (pipe IPC)
|
|
148
|
+
* → daemon 保持存活直到用户点"退出"
|
|
149
|
+
* 2. cli.mjs 进程立刻退出
|
|
150
|
+
*/
|
|
143
151
|
function startTray() {
|
|
144
|
-
const
|
|
145
|
-
const trayScript = path.join(__dirname, "tray-manager.ps1");
|
|
152
|
+
const daemonPath = path.join(__dirname, "daemon.mjs");
|
|
146
153
|
|
|
147
|
-
if (!fs.existsSync(
|
|
148
|
-
log("!!", "未找到
|
|
154
|
+
if (!fs.existsSync(daemonPath)) {
|
|
155
|
+
log("!!", "未找到 daemon.mjs", "red");
|
|
149
156
|
process.exit(1);
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
log("..", "正在以托盘模式启动 Pan Router...", "yellow");
|
|
153
160
|
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
+
const serverPath = path.join(__dirname, "server.mjs");
|
|
162
|
+
const trayPath = path.join(__dirname, "tray-manager.ps1");
|
|
163
|
+
|
|
164
|
+
const child = spawn(process.execPath, [
|
|
165
|
+
daemonPath,
|
|
166
|
+
`--serverPath="${serverPath}"`,
|
|
167
|
+
`--trayPath="${trayPath}"`,
|
|
161
168
|
], {
|
|
162
169
|
cwd: __dirname,
|
|
163
170
|
stdio: "ignore",
|
|
164
171
|
detached: true,
|
|
172
|
+
windowsHide: true,
|
|
173
|
+
shell: false,
|
|
165
174
|
});
|
|
166
175
|
child.unref();
|
|
167
176
|
|
package/daemon.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pan Router Daemon — 后台守护进程
|
|
5
|
+
*
|
|
6
|
+
* 由 cli.mjs 以 --tray 参数启动 (detached, hidden)。
|
|
7
|
+
* 管理 server.mjs + tray-manager.ps1, 通过 stdin/stdout JSON 通信。
|
|
8
|
+
*
|
|
9
|
+
* 用法(由 cli.mjs 调用):
|
|
10
|
+
* node daemon.mjs --serverPath="..." --trayPath="..."
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn, execSync } from "node:child_process";
|
|
14
|
+
import { createInterface } from "node:readline";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import http from "node:http";
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
// ─── 解析参数 ────────────────────────────────────
|
|
23
|
+
const serverPath = process.argv.find(a => a.startsWith("--serverPath="))?.split("=")[1];
|
|
24
|
+
const trayPath = process.argv.find(a => a.startsWith("--trayPath="))?.split("=")[1];
|
|
25
|
+
if (!serverPath || !trayPath) { process.exit(1); }
|
|
26
|
+
|
|
27
|
+
const appDir = path.dirname(serverPath);
|
|
28
|
+
const AUTOSTART_NAME = "PanRouter";
|
|
29
|
+
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
30
|
+
|
|
31
|
+
let serverProcess = null;
|
|
32
|
+
let psProcess = null;
|
|
33
|
+
|
|
34
|
+
// ─── 日志 ────────────────────────────────────────
|
|
35
|
+
const LOG = path.join(process.env.TEMP || "/tmp", "panrouter-daemon.log");
|
|
36
|
+
function log(msg) {
|
|
37
|
+
try { fs.appendFileSync(LOG, `${new Date().toISOString().slice(11,19)} ${msg}\n`); } catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── 健康检查 ─────────────────────────────────────
|
|
41
|
+
function isOnline() {
|
|
42
|
+
return new Promise(rs => {
|
|
43
|
+
const req = http.get("http://127.0.0.1:50816/health", () => {});
|
|
44
|
+
req.on("response", () => { req.destroy(); rs(true); });
|
|
45
|
+
req.on("error", () => rs(false));
|
|
46
|
+
req.setTimeout(1500, () => { req.destroy(); rs(false); });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── 杀掉旧 server 进程 ──────────────────────────
|
|
51
|
+
function killOldServers() {
|
|
52
|
+
try {
|
|
53
|
+
const wmic = spawn("wmic", [
|
|
54
|
+
"process", "where", "name='node.exe'", "get", "ProcessId,CommandLine", "/format:csv"
|
|
55
|
+
], { stdio: ["ignore", "pipe", "ignore"] });
|
|
56
|
+
let out = "";
|
|
57
|
+
wmic.stdout.on("data", d => out += d.toString());
|
|
58
|
+
wmic.on("close", () => {
|
|
59
|
+
for (const line of out.split("\n")) {
|
|
60
|
+
if (line.includes("server.mjs")) {
|
|
61
|
+
const m = line.match(/(\d+),.*?server\.mjs/);
|
|
62
|
+
if (m) { try { process.kill(parseInt(m[1]), "SIGKILL"); } catch {} }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── 隐藏启动 server.mjs ─────────────────────────
|
|
70
|
+
function startServer() {
|
|
71
|
+
killOldServers();
|
|
72
|
+
serverProcess = spawn("node", [serverPath], {
|
|
73
|
+
cwd: appDir, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, shell: false,
|
|
74
|
+
});
|
|
75
|
+
serverProcess.stdout.on("data", d => log("[srv] " + d.toString().trim()));
|
|
76
|
+
serverProcess.stderr.on("data", d => log("[srv-err] " + d.toString().trim()));
|
|
77
|
+
serverProcess.on("exit", code => log(`Server exited code=${code}`));
|
|
78
|
+
log(`Server started PID=${serverProcess.pid}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── 命令 → PS 托盘 ─────────────────────────────
|
|
82
|
+
function psSend(cmd) {
|
|
83
|
+
if (psProcess?.stdin?.writable) {
|
|
84
|
+
psProcess.stdin.write(JSON.stringify(cmd) + "\n");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── 开机自启动操作 (reg.exe) ────────────────────
|
|
89
|
+
function toggleAutostart() {
|
|
90
|
+
try {
|
|
91
|
+
const out = execSync(`reg query ${RUN_KEY} /v ${AUTOSTART_NAME} 2>nul`, {
|
|
92
|
+
encoding: "utf8", windowsHide: true, timeout: 3000,
|
|
93
|
+
});
|
|
94
|
+
const isOn = !out.toLowerCase().includes("error");
|
|
95
|
+
if (isOn) {
|
|
96
|
+
execSync(`reg delete ${RUN_KEY} /v ${AUTOSTART_NAME} /f 2>nul`, { windowsHide: true, timeout: 3000 });
|
|
97
|
+
psSend({ action: "update-item", index: 2, title: "开机自启动", enabled: true });
|
|
98
|
+
log("Autostart OFF");
|
|
99
|
+
} else {
|
|
100
|
+
const exe = process.execPath;
|
|
101
|
+
const daemon = path.join(__dirname, "daemon.mjs");
|
|
102
|
+
const cmd = `"${exe}" "${daemon}" --serverPath="${serverPath}" --trayPath="${trayPath}"`;
|
|
103
|
+
execSync(`reg add ${RUN_KEY} /v ${AUTOSTART_NAME} /t REG_SZ /d "${cmd}" /f 2>nul`, { windowsHide: true, timeout: 3000 });
|
|
104
|
+
psSend({ action: "update-item", index: 2, title: "✓ 开机自启动", enabled: true });
|
|
105
|
+
log("Autostart ON");
|
|
106
|
+
}
|
|
107
|
+
} catch (e) { log(`Autostart error: ${e.message}`); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── 退出清理 ────────────────────────────────────
|
|
111
|
+
function cleanup() {
|
|
112
|
+
log("Cleanup...");
|
|
113
|
+
try { if (serverProcess && !serverProcess.killed) serverProcess.kill("SIGKILL"); } catch {}
|
|
114
|
+
killOldServers();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ═══════════════ 主流程 ═══════════════
|
|
118
|
+
|
|
119
|
+
log("=== PanRouter Daemon ===");
|
|
120
|
+
log(`serverPath=${serverPath}`);
|
|
121
|
+
log(`trayPath=${trayPath}`);
|
|
122
|
+
|
|
123
|
+
// 1. 启动服务器
|
|
124
|
+
startServer();
|
|
125
|
+
|
|
126
|
+
// 2. 等服务器就绪, 启动托盘
|
|
127
|
+
(async () => {
|
|
128
|
+
let ready = false;
|
|
129
|
+
for (let i = 0; i < 20; i++) {
|
|
130
|
+
if (await isOnline()) { ready = true; break; }
|
|
131
|
+
await new Promise(r => setTimeout(r, 500));
|
|
132
|
+
}
|
|
133
|
+
log(`Server ready=${ready}`);
|
|
134
|
+
|
|
135
|
+
// 3. 启动 PS 托盘 (pipe 连接, 保持 IPC)
|
|
136
|
+
psProcess = spawn("powershell.exe", [
|
|
137
|
+
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
|
138
|
+
"-WindowStyle", "Hidden",
|
|
139
|
+
"-File", trayPath,
|
|
140
|
+
], {
|
|
141
|
+
stdio: ["pipe", "pipe", "pipe"], windowsHide: true, shell: false,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 读取 PS 事件
|
|
145
|
+
createInterface({ input: psProcess.stdout }).on("line", (line) => {
|
|
146
|
+
try {
|
|
147
|
+
const evt = JSON.parse(line);
|
|
148
|
+
log(`PS: ${line}`);
|
|
149
|
+
if (evt.type === "started") {
|
|
150
|
+
// 发送菜单配置
|
|
151
|
+
psSend({ action: "add-item", index: 0, title: "Pan Router - :50816", enabled: false });
|
|
152
|
+
psSend({ action: "add-item", index: 1, title: "─".repeat(19), enabled: false });
|
|
153
|
+
psSend({ action: "add-item", index: 2, title: "开机自启动", enabled: true });
|
|
154
|
+
psSend({ action: "add-item", index: 3, title: "─".repeat(19), enabled: false });
|
|
155
|
+
psSend({ action: "add-item", index: 4, title: "退出", enabled: true });
|
|
156
|
+
psSend({ action: "set-tooltip", text: "Pan Router | 端口 50816" });
|
|
157
|
+
log("Menu configured");
|
|
158
|
+
}
|
|
159
|
+
if (evt.type === "click" && evt.index === 2) toggleAutostart();
|
|
160
|
+
if (evt.type === "click" && evt.index === 4) {
|
|
161
|
+
log("Exit requested");
|
|
162
|
+
cleanup();
|
|
163
|
+
psSend({ action: "kill" });
|
|
164
|
+
setTimeout(() => process.exit(0), 500);
|
|
165
|
+
}
|
|
166
|
+
} catch (e) { log(`PS parse error: ${e.message}`); }
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
psProcess.on("error", err => log(`PS error: ${err.message}`));
|
|
170
|
+
psProcess.stderr.on("data", d => log(`[ps-err] ${d.toString().trim()}`));
|
|
171
|
+
psProcess.on("exit", code => { log(`PS exited code=${code}`); process.exit(0); });
|
|
172
|
+
|
|
173
|
+
log("Daemon ready");
|
|
174
|
+
process.stdin.resume(); // 保持进程存活
|
|
175
|
+
})();
|
|
176
|
+
|
|
177
|
+
process.on("SIGTERM", () => { cleanup(); setTimeout(() => process.exit(0), 300); });
|
|
178
|
+
process.on("SIGINT", () => { cleanup(); setTimeout(() => process.exit(0), 300); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panrouter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
11
|
+
"daemon.mjs",
|
|
11
12
|
"server.mjs",
|
|
12
13
|
"tray-manager.ps1"
|
|
13
14
|
],
|
package/tray-manager.ps1
CHANGED
|
@@ -1,223 +1,111 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
Pan Router
|
|
3
|
+
Pan Router 托盘管理器 (IPC 版)
|
|
4
4
|
.DESCRIPTION
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
纯 NotifyIcon 包装器, 通过 stdin/stdout JSON 与父进程通信
|
|
6
|
+
|
|
7
|
+
支持命令 (stdin):
|
|
8
|
+
{"action":"add-item","index":0,"title":"...","enabled":true}
|
|
9
|
+
{"action":"update-item","index":0,"title":"...","enabled":true}
|
|
10
|
+
{"action":"set-tooltip","text":"..."}
|
|
11
|
+
{"action":"kill"}
|
|
12
|
+
|
|
13
|
+
事件 (stdout):
|
|
14
|
+
{"type":"started"}
|
|
15
|
+
{"type":"ready"} <- PS 就绪 + STA 已确认
|
|
16
|
+
{"type":"click","index":0}
|
|
17
|
+
{"type":"error","message":"..."}
|
|
10
18
|
#>
|
|
11
19
|
|
|
12
|
-
param(
|
|
13
|
-
[string]$ServerPath
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
# ─── 日志辅助 ──────────────────────────────────
|
|
17
|
-
$logFile = "$env:TEMP\panrouter-tray.log"
|
|
18
|
-
function Write-Log { param([string]$m) "$([DateTime]::Now.ToString('HH:mm:ss')) $m" | Out-File -Append -Encoding utf8 $logFile }
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
Write-Log "==== Pan Router Tray Started ===="
|
|
22
|
-
Write-Log "ServerPath: $ServerPath"
|
|
23
|
-
Write-Log "PSVersion: $($PSVersionTable.PSVersion)"
|
|
24
|
-
|
|
25
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
26
|
-
Add-Type -AssemblyName System.Drawing
|
|
20
|
+
param()
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
$autostartName = "PanRouter"
|
|
31
|
-
$runKeyPath = "Software\Microsoft\Windows\CurrentVersion\Run"
|
|
22
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
23
|
+
Add-Type -AssemblyName System.Drawing
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
36
|
-
$g.SmoothingMode = 'HighQuality'
|
|
37
|
-
$g.Clear([System.Drawing.Color]::Transparent)
|
|
38
|
-
$brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(0, 120, 215))
|
|
39
|
-
$g.FillEllipse($brush, 0, 0, 15, 15)
|
|
40
|
-
$font = New-Object System.Drawing.Font("Segoe UI", 8.5, [System.Drawing.FontStyle]::Bold)
|
|
41
|
-
$fg = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
|
|
42
|
-
$g.DrawString("P", $font, $fg, 3, 1.5)
|
|
43
|
-
$font.Dispose(); $fg.Dispose(); $brush.Dispose(); $g.Dispose()
|
|
44
|
-
$hIcon = $bmp.GetHicon()
|
|
45
|
-
$icon = [System.Drawing.Icon]::FromHandle($hIcon)
|
|
46
|
-
$bmp.Dispose()
|
|
47
|
-
Write-Log "Icon created"
|
|
25
|
+
$ErrorActionPreference = "Stop"
|
|
26
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
48
27
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Write-Log "NotifyIcon created and visible"
|
|
28
|
+
$script:notifyIcon = New-Object System.Windows.Forms.NotifyIcon
|
|
29
|
+
$script:notifyIcon.Visible = $true
|
|
30
|
+
$script:menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
31
|
+
$script:notifyIcon.ContextMenuStrip = $script:menu
|
|
32
|
+
$script:items = @()
|
|
55
33
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
$
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
$psi.Arguments = "`"$ServerPath`""
|
|
64
|
-
$psi.WorkingDirectory = $serverDir
|
|
65
|
-
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
66
|
-
$psi.CreateNoWindow = $true
|
|
67
|
-
# UseShellExecute=$true 避免重定向缓冲死锁
|
|
68
|
-
$psi.UseShellExecute = $true
|
|
69
|
-
$proc = [System.Diagnostics.Process]::Start($psi)
|
|
70
|
-
Write-Log "Server started PID=$($proc.Id)"
|
|
71
|
-
return $proc
|
|
72
|
-
} catch {
|
|
73
|
-
Write-Log "Server start FAILED: $_"
|
|
74
|
-
return $null
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
# ─── 杀掉旧进程 (wmic 兼容 PS5.1) ──────────
|
|
79
|
-
function Stop-OldServer {
|
|
80
|
-
try {
|
|
81
|
-
$old = Get-WmiObject Win32_Process -Filter "Name='node.exe'" -ErrorAction Stop |
|
|
82
|
-
Where-Object { $_.CommandLine -match "server\.mjs" }
|
|
83
|
-
foreach ($p in $old) {
|
|
84
|
-
Write-Log "Killing old server PID=$($p.ProcessId)"
|
|
85
|
-
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
|
86
|
-
}
|
|
87
|
-
} catch { Write-Log "Stop-OldServer: $_" }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
# ─── 健康检查 ──────────────────────────────
|
|
91
|
-
function Test-Online {
|
|
92
|
-
try {
|
|
93
|
-
$req = [System.Net.WebRequest]::Create("http://127.0.0.1:50816/health")
|
|
94
|
-
$req.Timeout = 1500
|
|
95
|
-
$resp = $req.GetResponse()
|
|
96
|
-
$resp.Close()
|
|
97
|
-
return $true
|
|
98
|
-
} catch { return $false }
|
|
99
|
-
}
|
|
34
|
+
function Write-Event($obj) {
|
|
35
|
+
$json = $obj | ConvertTo-Json -Compress
|
|
36
|
+
try {
|
|
37
|
+
[Console]::Out.WriteLine($json)
|
|
38
|
+
[Console]::Out.Flush()
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
100
41
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
42
|
+
# ─── 生成蓝色 P 图标 (纯内存, 无需 .ico 文件) ──
|
|
43
|
+
$bmp = New-Object System.Drawing.Bitmap(16, 16)
|
|
44
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
45
|
+
$g.SmoothingMode = 'HighQuality'
|
|
46
|
+
$g.Clear([System.Drawing.Color]::Transparent)
|
|
47
|
+
$brush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(0, 120, 215))
|
|
48
|
+
$g.FillEllipse($brush, 0, 0, 15, 15)
|
|
49
|
+
$font = New-Object System.Drawing.Font("Segoe UI", 8.5, [System.Drawing.FontStyle]::Bold)
|
|
50
|
+
$fg = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
|
|
51
|
+
$g.DrawString("P", $font, $fg, 3, 1.5)
|
|
52
|
+
$font.Dispose(); $fg.Dispose(); $brush.Dispose(); $g.Dispose()
|
|
53
|
+
$hIcon = $bmp.GetHicon()
|
|
54
|
+
$icon = [System.Drawing.Icon]::FromHandle($hIcon)
|
|
55
|
+
$bmp.Dispose()
|
|
56
|
+
|
|
57
|
+
$script:notifyIcon.Icon = $icon
|
|
58
|
+
$script:notifyIcon.Text = "Pan Router"
|
|
59
|
+
|
|
60
|
+
function Add-MenuItem($index, $title, $enabled) {
|
|
61
|
+
$item = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
62
|
+
$item.Text = $title
|
|
63
|
+
$item.Enabled = $enabled
|
|
64
|
+
$idx = $index
|
|
65
|
+
$item.Add_Click({ Write-Event @{type="click"; index=$idx} }.GetNewClosure())
|
|
66
|
+
$script:menu.Items.Add($item) | Out-Null
|
|
67
|
+
$script:items += $item
|
|
68
|
+
}
|
|
104
69
|
|
|
105
|
-
|
|
106
|
-
$
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (Test-Online) { $ready = $true; break }
|
|
110
|
-
}
|
|
111
|
-
Write-Log "Server ready=$ready"
|
|
112
|
-
if ($ready) {
|
|
113
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已就绪 (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
70
|
+
function Update-MenuItem($index, $title, $enabled) {
|
|
71
|
+
if ($index -lt $script:items.Count) {
|
|
72
|
+
$script:items[$index].Text = $title
|
|
73
|
+
$script:items[$index].Enabled = $enabled
|
|
114
74
|
}
|
|
75
|
+
}
|
|
115
76
|
|
|
116
|
-
|
|
117
|
-
$
|
|
118
|
-
$
|
|
119
|
-
|
|
120
|
-
if (-not (Test-Online)) {
|
|
121
|
-
Write-Log "Health check FAILED, restarting..."
|
|
122
|
-
Stop-OldServer
|
|
123
|
-
$script:serverProcess = Start-ServerNohup -ServerPath $ServerPath
|
|
124
|
-
Start-Sleep -Seconds 3
|
|
125
|
-
if (Test-Online) {
|
|
126
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已自动重启", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
$healthTimer.Start()
|
|
131
|
-
Write-Log "Health timer started"
|
|
77
|
+
function Set-Tooltip($text) {
|
|
78
|
+
if ($text.Length -gt 63) { $text = $text.Substring(0, 63) }
|
|
79
|
+
$script:notifyIcon.Text = $text
|
|
80
|
+
}
|
|
132
81
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
82
|
+
# ─── stdin 轮询 (100ms 间隔) ────────────────────
|
|
83
|
+
$script:timer = New-Object System.Windows.Forms.Timer
|
|
84
|
+
$script:timer.Interval = 100
|
|
85
|
+
$script:timer.Add_Tick({
|
|
86
|
+
try {
|
|
87
|
+
while ([Console]::In.Peek() -ne -1) {
|
|
88
|
+
$line = [Console]::In.ReadLine()
|
|
89
|
+
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
|
90
|
+
$cmd = $line | ConvertFrom-Json
|
|
91
|
+
switch ($cmd.action) {
|
|
92
|
+
"add-item" { Add-MenuItem $cmd.index $cmd.title $cmd.enabled }
|
|
93
|
+
"update-item" { Update-MenuItem $cmd.index $cmd.title $cmd.enabled }
|
|
94
|
+
"set-tooltip" { Set-Tooltip $cmd.text }
|
|
95
|
+
"kill" {
|
|
96
|
+
$script:notifyIcon.Visible = $false
|
|
97
|
+
$script:notifyIcon.Dispose()
|
|
98
|
+
$icon.Dispose()
|
|
99
|
+
[System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
|
|
100
|
+
[System.Windows.Forms.Application]::Exit()
|
|
101
|
+
}
|
|
141
102
|
}
|
|
142
103
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
# ─── 右键菜单 ──────────────────────────────
|
|
146
|
-
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
147
|
-
|
|
148
|
-
$titleItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
149
|
-
$titleItem.Text = "Pan Router - :50816"
|
|
150
|
-
$titleItem.Enabled = $false
|
|
151
|
-
$titleItem.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
152
|
-
$menu.Items.Add($titleItem)
|
|
153
|
-
$menu.Items.Add("-")
|
|
154
|
-
|
|
155
|
-
$autoStartItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
156
|
-
$autoStartItem.Text = "开机自启动"
|
|
157
|
-
$rk = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
|
|
158
|
-
$autoStartItem.Checked = ($rk.GetValue($autostartName) -ne $null)
|
|
159
|
-
$rk.Close()
|
|
160
|
-
$autoStartItem.Add_Click({
|
|
161
|
-
$rk2 = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($runKeyPath, $true)
|
|
162
|
-
if ($autoStartItem.Checked) {
|
|
163
|
-
$rk2.DeleteValue($autostartName, $false)
|
|
164
|
-
$autoStartItem.Checked = $false
|
|
165
|
-
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
166
|
-
Write-Log "Autostart OFF"
|
|
167
|
-
} else {
|
|
168
|
-
$cmd = "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File `"$scriptPath`" -ServerPath `"$ServerPath`""
|
|
169
|
-
$rk2.SetValue($autostartName, $cmd)
|
|
170
|
-
$autoStartItem.Checked = $true
|
|
171
|
-
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
172
|
-
Write-Log "Autostart ON"
|
|
173
|
-
}
|
|
174
|
-
$rk2.Close()
|
|
175
|
-
})
|
|
176
|
-
$menu.Items.Add($autoStartItem)
|
|
177
|
-
$menu.Items.Add("-")
|
|
178
|
-
|
|
179
|
-
$exitItem = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
180
|
-
$exitItem.Text = "退出"
|
|
181
|
-
$exitItem.Add_Click({
|
|
182
|
-
Write-Log "Exit clicked"
|
|
183
|
-
$notifyIcon.Visible = $false
|
|
184
|
-
[System.Windows.Forms.Application]::Exit()
|
|
185
|
-
})
|
|
186
|
-
$menu.Items.Add($exitItem)
|
|
187
|
-
|
|
188
|
-
$notifyIcon.ContextMenuStrip = $menu
|
|
189
|
-
|
|
190
|
-
# ─── 退出清理 ──────────────────────────────
|
|
191
|
-
$cleanup = {
|
|
192
|
-
try {
|
|
193
|
-
Write-Log "Cleanup started"
|
|
194
|
-
$healthTimer.Stop()
|
|
195
|
-
$healthTimer.Dispose()
|
|
196
|
-
if (-not $serverProcess.HasExited) {
|
|
197
|
-
$serverProcess.Kill()
|
|
198
|
-
$serverProcess.WaitForExit(3000)
|
|
199
|
-
$serverProcess.Dispose()
|
|
200
|
-
Write-Log "Server killed"
|
|
201
|
-
}
|
|
202
|
-
Stop-OldServer
|
|
203
|
-
Write-Log "Cleanup done"
|
|
204
|
-
} catch { Write-Log "Cleanup error: $_" }
|
|
104
|
+
} catch {
|
|
105
|
+
Write-Event @{type="error"; message=$_.Exception.Message}
|
|
205
106
|
}
|
|
107
|
+
})
|
|
108
|
+
$script:timer.Start()
|
|
206
109
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
$notifyIcon.Dispose()
|
|
210
|
-
[System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
|
|
211
|
-
$icon.Dispose()
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
Write-Log "Entering message loop"
|
|
215
|
-
[System.Windows.Forms.Application]::Run()
|
|
216
|
-
Write-Log "Message loop exited"
|
|
217
|
-
& $cleanup
|
|
218
|
-
|
|
219
|
-
} catch {
|
|
220
|
-
$errMsg = "UNHANDLED ERROR: $_"
|
|
221
|
-
Write-Log $errMsg
|
|
222
|
-
try { [System.Windows.Forms.MessageBox]::Show($errMsg, "Pan Router Error") } catch {}
|
|
223
|
-
}
|
|
110
|
+
Write-Event @{type="started"}
|
|
111
|
+
[System.Windows.Forms.Application]::Run()
|