panrouter 1.2.0 → 1.4.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
@@ -138,19 +138,21 @@ 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
- * 启动策略:
145
- * 1. 启动 daemon.mjs (detached, 无窗口)
146
- * daemon 启动隐藏的 server.mjs
147
- * daemon 启动 tray-manager.ps1 (pipe IPC)
148
- * daemon 保持存活直到用户点"退出"
149
- * 2. cli.mjs 进程立刻退出
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
+ * 连接, 否则无法创建通知区图标。
150
153
  */
151
154
  function startTray() {
152
155
  const daemonPath = path.join(__dirname, "daemon.mjs");
153
-
154
156
  if (!fs.existsSync(daemonPath)) {
155
157
  log("!!", "未找到 daemon.mjs", "red");
156
158
  process.exit(1);
@@ -158,23 +160,20 @@ function startTray() {
158
160
 
159
161
  log("..", "正在以托盘模式启动 Pan Router...", "yellow");
160
162
 
161
- const serverPath = path.join(__dirname, "server.mjs");
162
- const trayPath = path.join(__dirname, "tray-manager.ps1");
163
-
164
163
  const child = spawn(process.execPath, [
165
164
  daemonPath,
166
- `--serverPath="${serverPath}"`,
167
- `--trayPath="${trayPath}"`,
165
+ `--serverPath="${path.join(__dirname, "server.mjs")}"`,
166
+ `--trayPsPath="${path.join(__dirname, "tray-daemon.ps1")}"`,
168
167
  ], {
169
- cwd: __dirname,
170
168
  stdio: "ignore",
171
- detached: true,
172
169
  windowsHide: true,
170
+ detached: true,
173
171
  shell: false,
174
172
  });
175
173
  child.unref();
176
174
 
177
- log("OK", "Pan Router 托盘已启动(图标在任务栏右下角)", "green");
175
+ log("OK", "Pan Router 托盘已启动", "green");
176
+ console.log(" 图标在任务栏右下角, 右键菜单可退出");
178
177
  }
179
178
 
180
179
  // ─── 主流程 ──────────────────────────────────────────────────────────────
package/daemon.mjs CHANGED
@@ -3,11 +3,17 @@
3
3
  /**
4
4
  * Pan Router Daemon — 后台守护进程
5
5
  *
6
- * 由 cli.mjs --tray 参数启动 (detached, hidden)
7
- * 管理 server.mjs + tray-manager.ps1, 通过 stdin/stdout JSON 通信。
6
+ * 用法 (由 cli.mjs --tray 调用):
7
+ * node daemon.mjs --serverPath="..." --trayPsPath="..."
8
8
  *
9
- * 用法( cli.mjs 调用):
10
- * node daemon.mjs --serverPath="..." --trayPath="..."
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 通知区图标。
11
17
  */
12
18
 
13
19
  import { spawn, execSync } from "node:child_process";
@@ -21,22 +27,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
27
 
22
28
  // ─── 解析参数 ────────────────────────────────────
23
29
  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); }
30
+ const trayPsPath = process.argv.find(a => a.startsWith("--trayPsPath="))?.split("=")[1];
31
+ if (!serverPath || !trayPsPath) { process.exit(1); }
26
32
 
27
33
  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
34
  const LOG = path.join(process.env.TEMP || "/tmp", "panrouter-daemon.log");
35
+
36
36
  function log(msg) {
37
37
  try { fs.appendFileSync(LOG, `${new Date().toISOString().slice(11,19)} ${msg}\n`); } catch {}
38
38
  }
39
39
 
40
+ log("=== PanRouter Daemon (v2) ===");
41
+ log(`serverPath=${serverPath}`);
42
+
40
43
  // ─── 健康检查 ─────────────────────────────────────
41
44
  function isOnline() {
42
45
  return new Promise(rs => {
@@ -47,8 +50,9 @@ function isOnline() {
47
50
  });
48
51
  }
49
52
 
50
- // ─── 杀掉旧 server 进程 ──────────────────────────
51
- function killOldServers() {
53
+ // ─── 启动隐藏的 server.mjs ────────────────────────
54
+ const serverProcess = (() => {
55
+ // 先杀旧的
52
56
  try {
53
57
  const wmic = spawn("wmic", [
54
58
  "process", "where", "name='node.exe'", "get", "ProcessId,CommandLine", "/format:csv"
@@ -64,66 +68,20 @@ function killOldServers() {
64
68
  }
65
69
  });
66
70
  } catch {}
67
- }
68
71
 
69
- // ─── 隐藏启动 server.mjs ─────────────────────────
70
- function startServer() {
71
- killOldServers();
72
- serverProcess = spawn("node", [serverPath], {
73
- cwd: appDir, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, shell: false,
72
+ log("Starting server...");
73
+ const child = spawn("cmd.exe", ["/c", "start", "/B", "node", serverPath], {
74
+ cwd: appDir,
75
+ stdio: "ignore",
76
+ windowsHide: true,
77
+ shell: false,
74
78
  });
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();
79
+ child.unref();
80
+ log(`Server started via cmd /c start /B`);
81
+ return child;
82
+ })();
125
83
 
126
- // 2. 等服务器就绪, 启动托盘
84
+ // ─── 等服务器就绪 ─────────────────────────────────
127
85
  (async () => {
128
86
  let ready = false;
129
87
  for (let i = 0; i < 20; i++) {
@@ -132,47 +90,58 @@ startServer();
132
90
  }
133
91
  log(`Server ready=${ready}`);
134
92
 
135
- // 3. 启动 PS 托盘 (pipe 连接, 保持 IPC)
136
- psProcess = spawn("powershell.exe", [
137
- "-NoProfile", "-ExecutionPolicy", "Bypass",
93
+ // ─── 启动 PS 托盘 (pipe, detached!) ──────────
94
+ // 关键: 9Router 的做法 - PS 必须与 daemon 同 session
95
+ log("Starting PS tray...");
96
+ const psProcess = spawn("powershell.exe", [
97
+ "-NoProfile",
98
+ "-ExecutionPolicy", "Bypass",
138
99
  "-WindowStyle", "Hidden",
139
- "-File", trayPath,
100
+ "-File", trayPsPath,
140
101
  ], {
141
- stdio: ["pipe", "pipe", "pipe"], windowsHide: true, shell: false,
102
+ stdio: ["pipe", "pipe", "pipe"],
103
+ windowsHide: true,
104
+ shell: false,
105
+ // ⚠ 没有 detached: true — PS 进程保持子进程身份
142
106
  });
143
107
 
144
- // 读取 PS 事件
108
+ // IPC 读取 PS 事件
145
109
  createInterface({ input: psProcess.stdout }).on("line", (line) => {
146
110
  try {
147
111
  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);
112
+ log(`PS event: ${evt.type} idx=${evt.index}`);
113
+ if (evt.type === "click") {
114
+ if (evt.index === 2) { // 退出
115
+ log("Exit from PS menu");
116
+ try { if (serverProcess && !serverProcess.killed) serverProcess.kill(); } catch {}
117
+ psSend({ action: "kill" });
118
+ setTimeout(() => process.exit(0), 500);
119
+ }
165
120
  }
166
- } catch (e) { log(`PS parse error: ${e.message}`); }
121
+ } catch (e) { log(`PS parse err: ${e.message}`); }
167
122
  });
168
123
 
169
124
  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); });
125
+ psProcess.stderr.on("data", d => log(`[ps] ${d.toString().trim()}`));
126
+ psProcess.on("exit", code => {
127
+ log(`PS exited code=${code}`);
128
+ process.exit(0);
129
+ });
130
+
131
+ // ─── 发送菜单配置到 PS ──────────────────────────
132
+ function psSend(cmd) {
133
+ if (psProcess?.stdin?.writable) {
134
+ psProcess.stdin.write(JSON.stringify(cmd) + "\n");
135
+ }
136
+ }
137
+ psSend({ action: "add-item", index: 0, title: "Pan Router - :50816", enabled: false });
138
+ psSend({ action: "add-item", index: 1, title: "─".repeat(19), enabled: false });
139
+ psSend({ action: "add-item", index: 2, title: "退出", enabled: true });
140
+ psSend({ action: "set-tooltip", text: "Pan Router | 端口 50816" });
172
141
 
173
- log("Daemon ready");
174
- process.stdin.resume(); // 保持进程存活
142
+ log("Daemon running — keeping session alive");
143
+ process.stdin.resume(); // keep alive
175
144
  })();
176
145
 
177
- process.on("SIGTERM", () => { cleanup(); setTimeout(() => process.exit(0), 300); });
178
- process.on("SIGINT", () => { cleanup(); setTimeout(() => process.exit(0), 300); });
146
+ process.on("SIGTERM", () => { log("SIGTERM"); process.exit(0); });
147
+ 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.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "cli.mjs",
11
11
  "daemon.mjs",
12
12
  "server.mjs",
13
- "tray-manager.ps1"
13
+ "tray-daemon.ps1"
14
14
  ],
15
15
  "license": "MIT"
16
16
  }
@@ -1,10 +1,10 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Pan Router 托盘管理器 (IPC 版)
4
- .DESCRIPTION
5
- 纯 NotifyIcon 包装器, 通过 stdin/stdout JSON 与父进程通信
3
+ Pan Router 托盘进程 — 纯 NotifyIcon IPC 包装器
6
4
 
7
- 支持命令 (stdin):
5
+ IPC: stdin JSON 命令, stdout JSON 事件
6
+
7
+ 命令 (stdin):
8
8
  {"action":"add-item","index":0,"title":"...","enabled":true}
9
9
  {"action":"update-item","index":0,"title":"...","enabled":true}
10
10
  {"action":"set-tooltip","text":"..."}
@@ -12,34 +12,17 @@
12
12
 
13
13
  事件 (stdout):
14
14
  {"type":"started"}
15
- {"type":"ready"} <- PS 就绪 + STA 已确认
16
15
  {"type":"click","index":0}
17
16
  {"type":"error","message":"..."}
18
17
  #>
19
18
 
20
- param()
21
-
22
19
  Add-Type -AssemblyName System.Windows.Forms
23
20
  Add-Type -AssemblyName System.Drawing
24
21
 
25
22
  $ErrorActionPreference = "Stop"
26
23
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
27
24
 
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 = @()
33
-
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
- }
41
-
42
- # ─── 生成蓝色 P 图标 (纯内存, 无需 .ico 文件) ──
25
+ # ─── 生成图标 (蓝色 P, 纯内存) ────────────────────
43
26
  $bmp = New-Object System.Drawing.Bitmap(16, 16)
44
27
  $g = [System.Drawing.Graphics]::FromImage($bmp)
45
28
  $g.SmoothingMode = 'HighQuality'
@@ -54,8 +37,22 @@ $hIcon = $bmp.GetHicon()
54
37
  $icon = [System.Drawing.Icon]::FromHandle($hIcon)
55
38
  $bmp.Dispose()
56
39
 
57
- $script:notifyIcon.Icon = $icon
58
- $script:notifyIcon.Text = "Pan Router"
40
+ # ─── NotifyIcon ──────────────────────────────────
41
+ $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
42
+ $notifyIcon.Icon = $icon
43
+ $notifyIcon.Text = "Pan Router | 端口 50816"
44
+ $notifyIcon.Visible = $true
45
+
46
+ $menu = New-Object System.Windows.Forms.ContextMenuStrip
47
+ $notifyIcon.ContextMenuStrip = $menu
48
+ $items = @()
49
+
50
+ function Write-Event($obj) {
51
+ try {
52
+ [Console]::Out.WriteLine(($obj | ConvertTo-Json -Compress))
53
+ [Console]::Out.Flush()
54
+ } catch {}
55
+ }
59
56
 
60
57
  function Add-MenuItem($index, $title, $enabled) {
61
58
  $item = New-Object System.Windows.Forms.ToolStripMenuItem
@@ -63,26 +60,19 @@ function Add-MenuItem($index, $title, $enabled) {
63
60
  $item.Enabled = $enabled
64
61
  $idx = $index
65
62
  $item.Add_Click({ Write-Event @{type="click"; index=$idx} }.GetNewClosure())
66
- $script:menu.Items.Add($item) | Out-Null
67
- $script:items += $item
68
- }
69
-
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
74
- }
63
+ $menu.Items.Add($item) | Out-Null
64
+ $items += $item
75
65
  }
76
66
 
77
67
  function Set-Tooltip($text) {
78
68
  if ($text.Length -gt 63) { $text = $text.Substring(0, 63) }
79
- $script:notifyIcon.Text = $text
69
+ $notifyIcon.Text = $text
80
70
  }
81
71
 
82
- # ─── stdin 轮询 (100ms 间隔) ────────────────────
83
- $script:timer = New-Object System.Windows.Forms.Timer
84
- $script:timer.Interval = 100
85
- $script:timer.Add_Tick({
72
+ # ─── stdin 轮询 (9Router 做法: 100ms Timer) ──────
73
+ $timer = New-Object System.Windows.Forms.Timer
74
+ $timer.Interval = 100
75
+ $timer.Add_Tick({
86
76
  try {
87
77
  while ([Console]::In.Peek() -ne -1) {
88
78
  $line = [Console]::In.ReadLine()
@@ -90,22 +80,20 @@ $script:timer.Add_Tick({
90
80
  $cmd = $line | ConvertFrom-Json
91
81
  switch ($cmd.action) {
92
82
  "add-item" { Add-MenuItem $cmd.index $cmd.title $cmd.enabled }
93
- "update-item" { Update-MenuItem $cmd.index $cmd.title $cmd.enabled }
83
+ "update-item" { }
94
84
  "set-tooltip" { Set-Tooltip $cmd.text }
95
85
  "kill" {
96
- $script:notifyIcon.Visible = $false
97
- $script:notifyIcon.Dispose()
86
+ $notifyIcon.Visible = $false
87
+ $notifyIcon.Dispose()
98
88
  $icon.Dispose()
99
89
  [System.Runtime.InteropServices.Marshal]::DestroyIcon($hIcon)
100
90
  [System.Windows.Forms.Application]::Exit()
101
91
  }
102
92
  }
103
93
  }
104
- } catch {
105
- Write-Event @{type="error"; message=$_.Exception.Message}
106
- }
94
+ } catch { Write-Event @{type="error"; message=$_.Exception.Message} }
107
95
  })
108
- $script:timer.Start()
96
+ $timer.Start()
109
97
 
110
98
  Write-Event @{type="started"}
111
99
  [System.Windows.Forms.Application]::Run()