panrouter 1.3.0 → 1.4.1
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 +20 -19
- package/daemon.mjs +148 -0
- package/package.json +3 -3
- package/tray-daemon.ps1 +58 -210
- package/tray-launcher.vbs +0 -14
package/cli.mjs
CHANGED
|
@@ -138,41 +138,42 @@ async function startServer() {
|
|
|
138
138
|
console.log("\n 现在可以运行: \x1b[33mclaude \"你好\"\x1b[0m\n");
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// ─── 4.
|
|
141
|
+
// ─── 4. 以托盘模式启动 ──────────────────────────────────────────────────
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
* 启动策略 (参考 9Router):
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
144
|
+
* 启动策略 (参考 9Router Windows 托盘):
|
|
145
|
+
*
|
|
146
|
+
* cli.mjs ─spawn(detached)──→ node daemon.mjs (无窗口, 常驻)
|
|
147
|
+
* ├─ cmd /c start /B → node server.mjs (隐藏)
|
|
148
|
+
* └─ spawn(pipe) ───→ powershell tray-daemon.ps1
|
|
149
|
+
* └─ NotifyIcon ✓
|
|
150
|
+
*
|
|
151
|
+
* 关键: PS 必须用 pipe 连接(不 detached), 保持与 daemon 的 Window Station
|
|
152
|
+
* 连接, 否则无法创建通知区图标。
|
|
152
153
|
*/
|
|
153
154
|
function startTray() {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
log("!!", "未找到 tray-launcher.vbs", "red");
|
|
155
|
+
const daemonPath = path.join(__dirname, "daemon.mjs");
|
|
156
|
+
if (!fs.existsSync(daemonPath)) {
|
|
157
|
+
log("!!", "未找到 daemon.mjs", "red");
|
|
158
158
|
process.exit(1);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
log("..", "正在以托盘模式启动 Pan Router...", "yellow");
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
|
|
163
|
+
const child = spawn(process.execPath, [
|
|
164
|
+
daemonPath,
|
|
165
|
+
`--serverPath="${path.join(__dirname, "server.mjs")}"`,
|
|
166
|
+
`--trayPsPath="${path.join(__dirname, "tray-daemon.ps1")}"`,
|
|
167
167
|
], {
|
|
168
168
|
stdio: "ignore",
|
|
169
|
-
detached: true,
|
|
170
169
|
windowsHide: true,
|
|
170
|
+
detached: true,
|
|
171
171
|
shell: false,
|
|
172
172
|
});
|
|
173
173
|
child.unref();
|
|
174
174
|
|
|
175
|
-
log("OK", "Pan Router
|
|
175
|
+
log("OK", "Pan Router 托盘已启动", "green");
|
|
176
|
+
console.log(" 图标在任务栏右下角, 右键菜单可退出");
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
// ─── 主流程 ──────────────────────────────────────────────────────────────
|
package/daemon.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pan Router Daemon — 后台守护进程
|
|
5
|
+
*
|
|
6
|
+
* 用法 (由 cli.mjs --tray 调用):
|
|
7
|
+
* node daemon.mjs --serverPath="..." --trayPsPath="..."
|
|
8
|
+
*
|
|
9
|
+
* 架构 (参考 9Router):
|
|
10
|
+
* daemon.mjs (无窗口, 常驻)
|
|
11
|
+
* ├─ spawn server.mjs (hidden, UseShellExecute=true)
|
|
12
|
+
* └─ spawn powershell tray-daemon.ps1 (pipe, NOT detached)
|
|
13
|
+
* └─ NotifyIcon (右下角)
|
|
14
|
+
*
|
|
15
|
+
* 关键: PS 子进程不 detached, 保持与 daemon 的 session 连接,
|
|
16
|
+
* 否则失去 Window Station 无法创建 UI 通知区图标。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawn, execSync } from "node:child_process";
|
|
20
|
+
import { createInterface } from "node:readline";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import http from "node:http";
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
|
|
28
|
+
// ─── 解析参数 ────────────────────────────────────
|
|
29
|
+
const serverPath = process.argv.find(a => a.startsWith("--serverPath="))?.split("=")[1];
|
|
30
|
+
const trayPsPath = process.argv.find(a => a.startsWith("--trayPsPath="))?.split("=")[1];
|
|
31
|
+
if (!serverPath || !trayPsPath) { process.exit(1); }
|
|
32
|
+
|
|
33
|
+
const appDir = path.dirname(serverPath);
|
|
34
|
+
const LOG = path.join(process.env.TEMP || "/tmp", "panrouter-daemon.log");
|
|
35
|
+
|
|
36
|
+
function log(msg) {
|
|
37
|
+
try { fs.appendFileSync(LOG, `${new Date().toISOString().slice(11,19)} ${msg}\n`); } catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
log("=== PanRouter Daemon (v2) ===");
|
|
41
|
+
log(`serverPath=${serverPath}`);
|
|
42
|
+
|
|
43
|
+
// ─── 健康检查 ─────────────────────────────────────
|
|
44
|
+
function isOnline() {
|
|
45
|
+
return new Promise(rs => {
|
|
46
|
+
const req = http.get("http://127.0.0.1:50816/health", () => {});
|
|
47
|
+
req.on("response", () => { req.destroy(); rs(true); });
|
|
48
|
+
req.on("error", () => rs(false));
|
|
49
|
+
req.setTimeout(1500, () => { req.destroy(); rs(false); });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── 启动隐藏的 server.mjs ────────────────────────
|
|
54
|
+
const serverProcess = (() => {
|
|
55
|
+
// 先杀旧的
|
|
56
|
+
try {
|
|
57
|
+
const wmic = spawn("wmic", [
|
|
58
|
+
"process", "where", "name='node.exe'", "get", "ProcessId,CommandLine", "/format:csv"
|
|
59
|
+
], { stdio: ["ignore", "pipe", "ignore"] });
|
|
60
|
+
let out = "";
|
|
61
|
+
wmic.stdout.on("data", d => out += d.toString());
|
|
62
|
+
wmic.on("close", () => {
|
|
63
|
+
for (const line of out.split("\n")) {
|
|
64
|
+
if (line.includes("server.mjs")) {
|
|
65
|
+
const m = line.match(/(\d+),.*?server\.mjs/);
|
|
66
|
+
if (m) { try { process.kill(parseInt(m[1]), "SIGKILL"); } catch {} }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
} catch {}
|
|
71
|
+
|
|
72
|
+
log("Starting server...");
|
|
73
|
+
// 用 node 直接 spawn 子进程, 避免 cmd /c 的额外 shell
|
|
74
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
75
|
+
cwd: appDir,
|
|
76
|
+
stdio: "ignore",
|
|
77
|
+
windowsHide: true,
|
|
78
|
+
detached: true,
|
|
79
|
+
shell: false,
|
|
80
|
+
});
|
|
81
|
+
child.unref();
|
|
82
|
+
log(`Server started PID=${child.pid}`);
|
|
83
|
+
return child;
|
|
84
|
+
})();
|
|
85
|
+
|
|
86
|
+
// ─── 等服务器就绪 ─────────────────────────────────
|
|
87
|
+
(async () => {
|
|
88
|
+
let ready = false;
|
|
89
|
+
for (let i = 0; i < 20; i++) {
|
|
90
|
+
if (await isOnline()) { ready = true; break; }
|
|
91
|
+
await new Promise(r => setTimeout(r, 500));
|
|
92
|
+
}
|
|
93
|
+
log(`Server ready=${ready}`);
|
|
94
|
+
|
|
95
|
+
// ─── 启动 PS 托盘 (pipe, 不 detached!) ──────────
|
|
96
|
+
// 关键: 9Router 的做法 - PS 必须与 daemon 同 session
|
|
97
|
+
log("Starting PS tray...");
|
|
98
|
+
const psProcess = spawn("powershell.exe", [
|
|
99
|
+
"-NoProfile",
|
|
100
|
+
"-ExecutionPolicy", "Bypass",
|
|
101
|
+
"-WindowStyle", "Hidden",
|
|
102
|
+
"-File", trayPsPath,
|
|
103
|
+
], {
|
|
104
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
shell: false,
|
|
107
|
+
// ⚠ 没有 detached: true — PS 进程保持子进程身份
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// IPC 读取 PS 事件
|
|
111
|
+
createInterface({ input: psProcess.stdout }).on("line", (line) => {
|
|
112
|
+
try {
|
|
113
|
+
const evt = JSON.parse(line);
|
|
114
|
+
log(`PS event: ${evt.type} idx=${evt.index}`);
|
|
115
|
+
if (evt.type === "click") {
|
|
116
|
+
if (evt.index === 2) { // 退出
|
|
117
|
+
log("Exit from PS menu");
|
|
118
|
+
psSend({ action: "kill" });
|
|
119
|
+
setTimeout(() => process.exit(0), 500);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (e) { log(`PS parse err: ${e.message}`); }
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
psProcess.on("error", err => log(`PS error: ${err.message}`));
|
|
126
|
+
psProcess.stderr.on("data", d => log(`[ps] ${d.toString().trim()}`));
|
|
127
|
+
psProcess.on("exit", code => {
|
|
128
|
+
log(`PS exited code=${code}`);
|
|
129
|
+
process.exit(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── 发送菜单配置到 PS ──────────────────────────
|
|
133
|
+
function psSend(cmd) {
|
|
134
|
+
if (psProcess?.stdin?.writable) {
|
|
135
|
+
psProcess.stdin.write(JSON.stringify(cmd) + "\n");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
psSend({ action: "add-item", index: 0, title: "Pan Router - :50816", enabled: false });
|
|
139
|
+
psSend({ action: "add-item", index: 1, title: "─".repeat(19), enabled: false });
|
|
140
|
+
psSend({ action: "add-item", index: 2, title: "退出", enabled: true });
|
|
141
|
+
psSend({ action: "set-tooltip", text: "Pan Router | 端口 50816" });
|
|
142
|
+
|
|
143
|
+
log("Daemon running — keeping session alive");
|
|
144
|
+
process.stdin.resume(); // keep alive
|
|
145
|
+
})();
|
|
146
|
+
|
|
147
|
+
process.on("SIGTERM", () => { log("SIGTERM"); process.exit(0); });
|
|
148
|
+
process.on("SIGINT", () => { log("SIGINT"); process.exit(0); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panrouter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
11
|
+
"daemon.mjs",
|
|
11
12
|
"server.mjs",
|
|
12
|
-
"tray-daemon.ps1"
|
|
13
|
-
"tray-launcher.vbs"
|
|
13
|
+
"tray-daemon.ps1"
|
|
14
14
|
],
|
|
15
15
|
"license": "MIT"
|
|
16
16
|
}
|
package/tray-daemon.ps1
CHANGED
|
@@ -1,38 +1,28 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
Pan Router
|
|
3
|
+
Pan Router 托盘进程 — 纯 NotifyIcon IPC 包装器
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- 隐藏启动 server.mjs (node)
|
|
7
|
-
- 右下角 NotifyIcon
|
|
8
|
-
- 右键菜单: 开机自启动 / 退出
|
|
9
|
-
- 30秒健康检查, 自动重启
|
|
10
|
-
- 日志到 %TEMP%\panrouter-tray.log
|
|
5
|
+
IPC: stdin JSON 命令, stdout JSON 事件
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
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"}
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
事件 (stdout):
|
|
14
|
+
{"type":"started"}
|
|
15
|
+
{"type":"click","index":0}
|
|
16
|
+
{"type":"error","message":"..."}
|
|
17
17
|
#>
|
|
18
18
|
|
|
19
|
-
$logFile = "$env:TEMP\panrouter-tray.log"
|
|
20
|
-
function Write-Log($m) { "$(Get-Date -Format 'HH:mm:ss') $m" | Out-File -Append -Encoding utf8 $logFile }
|
|
21
|
-
|
|
22
|
-
Write-Log "=== PanRouter Tray Daemon ==="
|
|
23
|
-
|
|
24
|
-
# ─── 路径 (与脚本同目录) ────────────────────────
|
|
25
|
-
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
|
26
|
-
$serverPath = Join-Path $scriptDir "server.mjs"
|
|
27
|
-
|
|
28
|
-
# ─── 加载 WinForms ─────────────────────────────
|
|
29
19
|
Add-Type -AssemblyName System.Windows.Forms
|
|
30
20
|
Add-Type -AssemblyName System.Drawing
|
|
31
|
-
Write-Log "Assemblies loaded"
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
$ErrorActionPreference = "Stop"
|
|
23
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
24
|
+
|
|
25
|
+
# ─── 生成图标 (蓝色 P, 纯内存) ────────────────────
|
|
36
26
|
$bmp = New-Object System.Drawing.Bitmap(16, 16)
|
|
37
27
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
38
28
|
$g.SmoothingMode = 'HighQuality'
|
|
@@ -46,206 +36,64 @@ $font.Dispose(); $fg.Dispose(); $brush.Dispose(); $g.Dispose()
|
|
|
46
36
|
$hIcon = $bmp.GetHicon()
|
|
47
37
|
$icon = [System.Drawing.Icon]::FromHandle($hIcon)
|
|
48
38
|
$bmp.Dispose()
|
|
49
|
-
Write-Log "Icon created"
|
|
50
39
|
|
|
51
|
-
#
|
|
52
|
-
# 2. NotifyIcon
|
|
53
|
-
# ═══════════════════════════════════════════════════════════
|
|
40
|
+
# ─── NotifyIcon ──────────────────────────────────
|
|
54
41
|
$notifyIcon = New-Object System.Windows.Forms.NotifyIcon
|
|
55
42
|
$notifyIcon.Icon = $icon
|
|
56
43
|
$notifyIcon.Text = "Pan Router | 端口 50816"
|
|
57
44
|
$notifyIcon.Visible = $true
|
|
58
|
-
Write-Log "NotifyIcon visible"
|
|
59
|
-
|
|
60
|
-
# ═══════════════════════════════════════════════════════════
|
|
61
|
-
# 3. 隐藏启动 server.mjs
|
|
62
|
-
# ═══════════════════════════════════════════════════════════
|
|
63
|
-
function Start-Server {
|
|
64
|
-
# 杀掉旧 server
|
|
65
|
-
try {
|
|
66
|
-
$old = wmic process where "name='node.exe'" get ProcessId,CommandLine /format:csv 2>$null
|
|
67
|
-
foreach ($line in $old) {
|
|
68
|
-
if ($line -match '(\d+),.*?server\.mjs') {
|
|
69
|
-
$pid = $Matches[1]
|
|
70
|
-
Write-Log "Kill old server PID=$pid"
|
|
71
|
-
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
} catch {}
|
|
75
|
-
|
|
76
|
-
# UseShellExecute=$true 避免输出缓冲死锁
|
|
77
|
-
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
78
|
-
$psi.FileName = "node"
|
|
79
|
-
$psi.Arguments = "`"$serverPath`""
|
|
80
|
-
$psi.WorkingDirectory = $scriptDir
|
|
81
|
-
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
|
82
|
-
$psi.CreateNoWindow = $true
|
|
83
|
-
$psi.UseShellExecute = $true
|
|
84
|
-
try {
|
|
85
|
-
$p = [System.Diagnostics.Process]::Start($psi)
|
|
86
|
-
Write-Log "Server started PID=$($p.Id)"
|
|
87
|
-
return $p
|
|
88
|
-
} catch {
|
|
89
|
-
Write-Log "Server start FAILED: $_"
|
|
90
|
-
return $null
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
# ═══════════════════════════════════════════════════════════
|
|
95
|
-
# 4. 健康检查
|
|
96
|
-
# ═══════════════════════════════════════════════════════════
|
|
97
|
-
function Test-Online {
|
|
98
|
-
try {
|
|
99
|
-
$req = [System.Net.WebRequest]::Create("http://127.0.0.1:50816/health")
|
|
100
|
-
$req.Timeout = 1500
|
|
101
|
-
$resp = $req.GetResponse()
|
|
102
|
-
$resp.Close()
|
|
103
|
-
return $true
|
|
104
|
-
} catch { return $false }
|
|
105
|
-
}
|
|
106
45
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
$autostartName = "PanRouter"
|
|
111
|
-
$runKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
|
|
46
|
+
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
47
|
+
$notifyIcon.ContextMenuStrip = $menu
|
|
48
|
+
$items = @()
|
|
112
49
|
|
|
113
|
-
function
|
|
50
|
+
function Write-Event($obj) {
|
|
114
51
|
try {
|
|
115
|
-
$
|
|
116
|
-
|
|
117
|
-
} catch {
|
|
52
|
+
[Console]::Out.WriteLine(($obj | ConvertTo-Json -Compress))
|
|
53
|
+
[Console]::Out.Flush()
|
|
54
|
+
} catch {}
|
|
118
55
|
}
|
|
119
56
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
57
|
+
function Add-MenuItem($index, $title, $enabled) {
|
|
58
|
+
$item = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
59
|
+
$item.Text = $title
|
|
60
|
+
$item.Enabled = $enabled
|
|
61
|
+
$idx = $index
|
|
62
|
+
$item.Add_Click({ Write-Event @{type="click"; index=$idx} }.GetNewClosure())
|
|
63
|
+
$menu.Items.Add($item) | Out-Null
|
|
64
|
+
$items += $item
|
|
129
65
|
}
|
|
130
66
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# 启动服务器
|
|
136
|
-
$serverProcess = Start-Server
|
|
137
|
-
|
|
138
|
-
# 等待就绪 (10秒)
|
|
139
|
-
$ready = $false
|
|
140
|
-
for ($i = 0; $i -lt 20; $i++) {
|
|
141
|
-
Start-Sleep -Milliseconds 500
|
|
142
|
-
if (Test-Online) { $ready = $true; break }
|
|
143
|
-
}
|
|
144
|
-
Write-Log "Server ready=$ready"
|
|
145
|
-
if ($ready) {
|
|
146
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已就绪 (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
147
|
-
} else {
|
|
148
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器启动失败, 查看日志", [System.Windows.Forms.ToolTipIcon]::Error)
|
|
67
|
+
function Set-Tooltip($text) {
|
|
68
|
+
if ($text.Length -gt 63) { $text = $text.Substring(0, 63) }
|
|
69
|
+
$notifyIcon.Text = $text
|
|
149
70
|
}
|
|
150
71
|
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
$
|
|
155
|
-
|
|
156
|
-
# 标题
|
|
157
|
-
$titleItem = New-Object System.Windows.Forms.ToolStripMenuItem("Pan Router - :50816")
|
|
158
|
-
$titleItem.Enabled = $false
|
|
159
|
-
$titleItem.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
|
|
160
|
-
$menu.Items.Add($titleItem)
|
|
161
|
-
$menu.Items.Add("-")
|
|
162
|
-
|
|
163
|
-
# 开机自启动
|
|
164
|
-
$autoItem = New-Object System.Windows.Forms.ToolStripMenuItem("开机自启动")
|
|
165
|
-
$autoItem.Checked = Get-Autostart
|
|
166
|
-
$autoItem.Add_Click({
|
|
167
|
-
if ($autoItem.Checked) {
|
|
168
|
-
Set-Autostart $false
|
|
169
|
-
$autoItem.Checked = $false
|
|
170
|
-
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已关闭", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
171
|
-
} else {
|
|
172
|
-
Set-Autostart $true
|
|
173
|
-
$autoItem.Checked = $true
|
|
174
|
-
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "开机自启动已开启 ✓", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
175
|
-
}
|
|
176
|
-
})
|
|
177
|
-
$menu.Items.Add($autoItem)
|
|
178
|
-
$menu.Items.Add("-")
|
|
179
|
-
|
|
180
|
-
# 退出
|
|
181
|
-
$exitItem = New-Object System.Windows.Forms.ToolStripMenuItem("退出")
|
|
182
|
-
$exitItem.Add_Click({
|
|
183
|
-
Write-Log "Exit clicked"
|
|
184
|
-
$notifyIcon.Visible = $false
|
|
185
|
-
[System.Windows.Forms.Application]::Exit()
|
|
186
|
-
})
|
|
187
|
-
$menu.Items.Add($exitItem)
|
|
188
|
-
|
|
189
|
-
$notifyIcon.ContextMenuStrip = $menu
|
|
190
|
-
|
|
191
|
-
# ═══════════════════════════════════════════════════════════
|
|
192
|
-
# ══ 左键: 状态气泡 ══
|
|
193
|
-
# ═══════════════════════════════════════════════════════════
|
|
194
|
-
$notifyIcon.Add_MouseClick({
|
|
195
|
-
if ($_.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
|
|
196
|
-
if (Test-Online) {
|
|
197
|
-
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "运行正常 ✓ (端口 50816)", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
198
|
-
} else {
|
|
199
|
-
$notifyIcon.ShowBalloonTip(2000, "Pan Router", "服务器未响应 ⚠", [System.Windows.Forms.ToolTipIcon]::Error)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
# ═══════════════════════════════════════════════════════════
|
|
205
|
-
# ══ 定时健康检查 (30秒) ══
|
|
206
|
-
# ═══════════════════════════════════════════════════════════
|
|
207
|
-
$healthTimer = New-Object System.Windows.Forms.Timer
|
|
208
|
-
$healthTimer.Interval = 30000
|
|
209
|
-
$healthTimer.Add_Tick({
|
|
210
|
-
if (-not (Test-Online)) {
|
|
211
|
-
Write-Log "Health check FAILED, restarting server..."
|
|
212
|
-
Start-Server
|
|
213
|
-
Start-Sleep -Seconds 3
|
|
214
|
-
if (Test-Online) {
|
|
215
|
-
$notifyIcon.ShowBalloonTip(3000, "Pan Router", "服务器已自动重启", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
})
|
|
219
|
-
$healthTimer.Start()
|
|
220
|
-
|
|
221
|
-
# ═══════════════════════════════════════════════════════════
|
|
222
|
-
# ══ 退出清理 ══
|
|
223
|
-
# ═══════════════════════════════════════════════════════════
|
|
224
|
-
[System.Windows.Forms.Application]::ApplicationExit += {
|
|
225
|
-
Write-Log "AppExit cleanup"
|
|
226
|
-
$healthTimer.Stop()
|
|
227
|
-
try {
|
|
228
|
-
if ($serverProcess -and !$serverProcess.HasExited) {
|
|
229
|
-
$serverProcess.Kill()
|
|
230
|
-
$serverProcess.WaitForExit(3000)
|
|
231
|
-
}
|
|
232
|
-
} catch {}
|
|
233
|
-
# 补刀: 杀残留
|
|
72
|
+
# ─── stdin 轮询 (9Router 做法: 100ms Timer) ──────
|
|
73
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
74
|
+
$timer.Interval = 100
|
|
75
|
+
$timer.Add_Tick({
|
|
234
76
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if ($line
|
|
77
|
+
while ([Console]::In.Peek() -ne -1) {
|
|
78
|
+
$line = [Console]::In.ReadLine()
|
|
79
|
+
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
|
80
|
+
$cmd = $line | ConvertFrom-Json
|
|
81
|
+
switch ($cmd.action) {
|
|
82
|
+
"add-item" { Add-MenuItem $cmd.index $cmd.title $cmd.enabled }
|
|
83
|
+
"update-item" { }
|
|
84
|
+
"set-tooltip" { Set-Tooltip $cmd.text }
|
|
85
|
+
"kill" {
|
|
86
|
+
$notifyIcon.Visible = $false
|
|
87
|
+
$notifyIcon.Dispose()
|
|
88
|
+
$icon.Dispose()
|
|
89
|
+
[System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
|
|
90
|
+
[System.Windows.Forms.Application]::Exit()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
238
93
|
}
|
|
239
|
-
} catch {}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
$icon.Dispose()
|
|
243
|
-
Write-Log "Cleanup done"
|
|
244
|
-
}
|
|
94
|
+
} catch { Write-Event @{type="error"; message=$_.Exception.Message} }
|
|
95
|
+
})
|
|
96
|
+
$timer.Start()
|
|
245
97
|
|
|
246
|
-
|
|
247
|
-
# ══ 消息循环 ══
|
|
248
|
-
# ═══════════════════════════════════════════════════════════
|
|
249
|
-
Write-Log "Entering message loop"
|
|
98
|
+
Write-Event @{type="started"}
|
|
250
99
|
[System.Windows.Forms.Application]::Run()
|
|
251
|
-
Write-Log "Message loop exited"
|
package/tray-launcher.vbs
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
' Pan Router Tray Launcher
|
|
2
|
-
' Windows 原生无窗口启动器(VBScript 内置, 无需任何运行时)
|
|
3
|
-
' 用法: wscript.exe tray-launcher.vbs
|
|
4
|
-
' 或 cscript.exe //nologo tray-launcher.vbs
|
|
5
|
-
|
|
6
|
-
Dim WshShell, FSO, ScriptDir
|
|
7
|
-
Set WshShell = CreateObject("WScript.Shell")
|
|
8
|
-
Set FSO = CreateObject("Scripting.FileSystemObject")
|
|
9
|
-
|
|
10
|
-
ScriptDir = FSO.GetParentFolderName(WScript.ScriptFullName)
|
|
11
|
-
PS1Path = ScriptDir & "\tray-daemon.ps1"
|
|
12
|
-
|
|
13
|
-
' 用 PowerShell 启动托盘守护进程 (0 = 隐藏窗口, False = 不等待返回)
|
|
14
|
-
WshShell.Run "powershell -ExecutionPolicy Bypass -WindowStyle Hidden -STA -File """ & PS1Path & """", 0, False
|