panrouter 3.5.0 → 3.7.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.
Files changed (3) hide show
  1. package/cli.mjs +171 -84
  2. package/package.json +1 -1
  3. package/tray-daemon.ps1 +4 -1
package/cli.mjs CHANGED
@@ -10,6 +10,7 @@ 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 BACKUP_PATH = path.join(CLAUDE_DIR, "settings.json.panrouter.backup");
13
14
 
14
15
  function log(label, msg, color = "") {
15
16
  const colors = { green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", reset: "\x1b[0m" };
@@ -17,28 +18,13 @@ function log(label, msg, color = "") {
17
18
  console.log(`${c}[${label}]${colors.reset} ${msg}`);
18
19
  }
19
20
 
20
- function installClaudeCode() {
21
- log("..", "正在检查 Claude Code...", "yellow");
22
- try {
23
- execSync("claude --version", { stdio: "pipe" });
24
- log("OK", "Claude Code 已就绪", "green");
25
- return true;
26
- } catch {
27
- log("..", "正在安装 Claude Code...", "yellow");
28
- try {
29
- execSync("npm install -g @anthropic-ai/claude-code", { stdio: "inherit" });
30
- log("OK", "Claude Code 安装成功", "green");
31
- return true;
32
- } catch {
33
- log("!!", "Claude Code 安装失败", "red");
34
- return false;
35
- }
36
- }
37
- }
21
+ // ─── 配置 ────────────────────────────────────────────────
38
22
 
39
23
  function writeConfig() {
40
24
  log("..", "正在配置 Claude Code 路由...", "yellow");
41
25
  if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
26
+ if (fs.existsSync(SETTINGS_PATH)) fs.copyFileSync(SETTINGS_PATH, BACKUP_PATH);
27
+
42
28
  const config = {
43
29
  env: {
44
30
  ANTHROPIC_BASE_URL: "http://127.0.0.1:50816",
@@ -53,6 +39,27 @@ function writeConfig() {
53
39
  log("OK", "配置完成", "green");
54
40
  }
55
41
 
42
+ // ─── 安装 Claude Code ────────────────────────────────────
43
+
44
+ function ensureClaudeCode() {
45
+ try {
46
+ execSync("claude --version", { stdio: "pipe" });
47
+ return true; // 已装
48
+ } catch {
49
+ log("..", "正在安装 Claude Code...", "yellow");
50
+ try {
51
+ execSync("npm install -g @anthropic-ai/claude-code", { stdio: "inherit", timeout: 120000 });
52
+ log("OK", "Claude Code 安装成功", "green");
53
+ return true;
54
+ } catch {
55
+ log("!!", "Claude Code 安装失败", "red");
56
+ return false;
57
+ }
58
+ }
59
+ }
60
+
61
+ // ─── 端口检测 ────────────────────────────────────────────
62
+
56
63
  async function isPortOpen() {
57
64
  return new Promise(rs => {
58
65
  const req = http.get("http://127.0.0.1:50816/health", () => {});
@@ -62,125 +69,205 @@ async function isPortOpen() {
62
69
  });
63
70
  }
64
71
 
72
+ // ─── 启动前台服务 ────────────────────────────────────────
73
+
65
74
  async function startServer() {
66
75
  const serverPath = path.join(__dirname, "server.mjs");
67
76
  try {
68
- if (process.platform === "win32") {
69
- execSync('taskkill /f /fi "WINDOWTITLE eq Pan Router*" >nul 2>&1', { stdio: "pipe" });
70
- } else {
71
- execSync("pkill -f 'node.*server.mjs' 2>/dev/null", { stdio: "pipe" });
72
- }
77
+ execSync('taskkill /f /fi "WINDOWTITLE eq Pan Router*" >nul 2>&1', { stdio: "pipe" });
73
78
  } catch {}
74
79
 
75
80
  log("..", "正在启动代理...", "yellow");
76
- if (process.platform === "win32") {
77
- execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
78
- } else {
79
- spawn("node", [serverPath], { cwd: __dirname, stdio: "ignore", detached: true }).unref();
80
- }
81
+ execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
81
82
 
82
83
  for (let i = 0; i < 15; i++) {
83
84
  if (await isPortOpen()) break;
84
85
  await new Promise(rs => setTimeout(rs, 1000));
85
86
  }
86
- log("OK", "Pan Router 运行中,可以执行 claude 命令了", "green");
87
+ log("OK", "Pan Router 运行中", "green");
87
88
  }
88
89
 
90
+ // ─── 启动托盘(后台) ────────────────────────────────────
91
+
89
92
  async function startTray() {
90
93
  const serverPath = path.join(__dirname, "server.mjs");
91
94
  const psPath = path.join(__dirname, "tray-daemon.ps1");
92
- log("..", "正在后台启动代理...", "yellow");
93
95
 
94
- // 【核心修复】:直接在原文件上追加 UTF-8 BOM 头,彻底修复乱码并保持原目录路径不变
96
+ // PS 脚本加 BOM(中文系统兼容)
95
97
  try {
96
- const psContent = fs.readFileSync(psPath, "utf8");
97
- if (!psContent.startsWith("")) {
98
+ const content = fs.readFileSync(psPath, "utf8");
99
+ if (content.charCodeAt(0) !== 0xFEFF) {
98
100
  const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
99
- fs.writeFileSync(psPath, Buffer.concat([bom, Buffer.from(psContent, "utf8")]));
100
- }
101
- } catch (e) {
102
- // 忽略权限问题
103
- }
104
-
105
- try {
106
- if (process.platform === "win32") {
107
- execSync('taskkill /f /fi "WINDOWTITLE eq Pan Router*" >nul 2>&1', { stdio: "pipe" });
101
+ fs.writeFileSync(psPath, Buffer.concat([bom, Buffer.from(content, "utf8")]));
108
102
  }
109
103
  } catch {}
110
104
 
105
+ // 清理旧进程
106
+ try { execSync('taskkill /f /fi "WINDOWTITLE eq Pan Router*" >nul 2>&1', { stdio: "pipe" }); } catch {}
107
+
108
+ // 启动 server(隐藏)
111
109
  const srv = spawn(process.execPath, [serverPath], {
112
- cwd: __dirname,
113
- stdio: "ignore",
114
- windowsHide: true,
115
- detached: true
110
+ cwd: __dirname, stdio: "ignore", windowsHide: true, detached: true,
116
111
  });
117
112
  srv.unref();
118
113
 
114
+ // 启动托盘(隐藏)
115
+ const tray = spawn("powershell.exe", [
116
+ "-NoProfile", "-STA", "-ExecutionPolicy", "Bypass",
117
+ "-WindowStyle", "Hidden", "-File", `"${psPath}"`,
118
+ ], {
119
+ cwd: __dirname, stdio: "ignore", windowsHide: true, shell: true,
120
+ env: { ...process.env, PANROUTER_NODE: process.execPath },
121
+ });
122
+ tray.unref();
123
+
124
+ // 验证端口
119
125
  let ok = false;
120
126
  for (let i = 0; i < 15; i++) {
121
- if (await isPortOpen()) {
122
- ok = true;
123
- break;
124
- }
127
+ if (await isPortOpen()) { ok = true; break; }
125
128
  await new Promise(rs => setTimeout(rs, 1000));
126
129
  }
127
130
 
128
131
  if (ok) {
129
- log("OK", "代理服务已就绪!(端口 50816)", "green");
132
+ log("OK", "Pan Router 已在后台运行(托盘在右下角)", "green");
130
133
  } else {
131
- log("!!", "服务启动超时,但仍将尝试加载托盘", "red");
134
+ log("!!", "代理启动超时,请检查端口 50816", "red");
132
135
  }
136
+ }
133
137
 
134
- log("..", "正在加载系统托盘...", "yellow");
138
+ // ─── 停止 ────────────────────────────────────────────────
135
139
 
136
- const tray = spawn("powershell.exe", [
137
- "-NoProfile",
138
- "-STA",
139
- "-ExecutionPolicy", "Bypass",
140
- "-WindowStyle", "Hidden",
141
- "-File", `"${psPath}"`
142
- ], {
143
- cwd: __dirname,
144
- stdio: ['ignore', 'pipe', 'pipe'],
145
- windowsHide: true,
146
- shell: true,
147
- env: { ...process.env, PANROUTER_NODE: process.execPath }
148
- });
140
+ function stopAll() {
141
+ log("..", "正在停止...", "yellow");
142
+ try { execSync('taskkill /f /fi "WINDOWTITLE eq Pan Router*" >nul 2>&1', { stdio: "pipe" }); } catch {}
143
+ try { execSync("taskkill /f /im powershell.exe >nul 2>&1", { stdio: "pipe" }); } catch {}
144
+ // 杀 server.mjs 相关 node 进程
145
+ try {
146
+ const out = execSync(
147
+ 'wmic process where "name=\'node.exe\'" get ProcessId,CommandLine /format:csv 2>nul',
148
+ { encoding: "utf8", windowsHide: true, timeout: 3000 }
149
+ );
150
+ for (const line of out.split("\n")) {
151
+ if (line.includes("server.mjs")) {
152
+ const m = line.match(/(\d+),.*?server\.mjs/);
153
+ if (m) try { process.kill(parseInt(m[1]), "SIGKILL"); } catch {}
154
+ }
155
+ }
156
+ } catch {}
157
+ log("OK", "已停止", "green");
158
+ }
159
+
160
+ // ─── 状态 ────────────────────────────────────────────────
149
161
 
150
- let psOutput = "";
151
- tray.stdout.on("data", d => psOutput += d.toString());
152
- tray.stderr.on("data", d => psOutput += d.toString());
162
+ async function showStatus() {
163
+ const online = await isPortOpen();
164
+ const version = "3.6.0";
165
+ console.log(`\n Pan Router v${version}`);
166
+ console.log(` 端口 50816: ${online ? "✓ 运行中" : "✗ 未启动"}`);
167
+ if (online) {
168
+ try {
169
+ const out = execSync(
170
+ 'wmic process where "name=\'node.exe\'" get ProcessId,CommandLine /format:csv 2>nul',
171
+ { encoding: "utf8", windowsHide: true, timeout: 3000 }
172
+ );
173
+ for (const line of out.split("\n")) {
174
+ if (line.includes("server.mjs")) {
175
+ const m = line.match(/(\d+),.*?server\.mjs/);
176
+ if (m) console.log(` PID: ${m[1]}`);
177
+ }
178
+ }
179
+ } catch {}
180
+ }
181
+ console.log("");
182
+ }
183
+
184
+ // ─── 版本 ────────────────────────────────────────────────
185
+
186
+ function showVersion() {
187
+ console.log("3.6.0");
188
+ }
153
189
 
154
- await new Promise(rs => setTimeout(rs, 2500));
190
+ // ─── 日志 ────────────────────────────────────────────────
155
191
 
156
- if (tray.exitCode !== null) {
157
- log("!!", "托盘进程未能驻留,发生闪退!", "red");
158
- console.log(`\n\x1b[31m=== PowerShell 启动失败原因 ===\x1b[0m\n${psOutput || "(无输出)"}\n\x1b[31m===============================\x1b[0m\n`);
192
+ function openLogs() {
193
+ const logPath = path.join(process.env.TEMP || "/tmp", "panrouter_tray.log");
194
+ if (fs.existsSync(logPath)) {
195
+ execSync(`notepad "${logPath}"`, { stdio: "inherit" });
159
196
  } else {
160
- tray.unref();
161
- console.log(" 托盘图标应该已在右下角显示。");
197
+ log("!!", "日志文件不存在", "red");
162
198
  }
163
199
  }
164
200
 
201
+ // ─── 主流程 ──────────────────────────────────────────────
202
+
165
203
  async function main() {
166
204
  const args = process.argv.slice(2);
205
+
206
+ // --help
167
207
  if (args.includes("--help") || args.includes("-h")) {
168
- console.log("用法:\n panrouter --server (带命令行窗口运行)\n panrouter --tray (后台隐藏运行 + 托盘)\n panrouter --tray-install (安装配置 + 托盘)");
208
+ console.log(`
209
+ Pan Router — Claude Code 免费路由代理
210
+
211
+ 用法:
212
+ panrouter 安装/配置 → 启动托盘(最常用)
213
+ panrouter --setup 只配置 Claude Code 路由
214
+ panrouter --server 前台窗口模式启动
215
+ panrouter --stop 停止所有
216
+ panrouter --status 查看运行状态
217
+ panrouter --logs 查看日志
218
+ panrouter --version 版本号
219
+ panrouter --help 帮助
220
+ `);
169
221
  return;
170
222
  }
171
223
 
172
- if (args.includes("--server") || args.includes("-s")) { await startServer(); return; }
173
- if (args.includes("--tray") || args.includes("-t")) { await startTray(); return; }
224
+ // --version
225
+ if (args.includes("--version") || args.includes("-v")) {
226
+ showVersion();
227
+ return;
228
+ }
174
229
 
175
- console.log(`\n\x1b[36m=== Pan Router - Claude Code ===\x1b[0m\n`);
176
- if (!installClaudeCode()) return process.exit(1);
177
- writeConfig();
230
+ // --status
231
+ if (args.includes("--status")) {
232
+ await showStatus();
233
+ return;
234
+ }
178
235
 
179
- if (args.includes("--tray-install") || args.includes("-ti")) {
180
- await startTray();
181
- } else {
236
+ // --logs
237
+ if (args.includes("--logs")) {
238
+ openLogs();
239
+ return;
240
+ }
241
+
242
+ // --stop
243
+ if (args.includes("--stop")) {
244
+ stopAll();
245
+ return;
246
+ }
247
+
248
+ // --setup(只配置)
249
+ if (args.includes("--setup")) {
250
+ writeConfig();
251
+ return;
252
+ }
253
+
254
+ // --server(前台窗口)
255
+ if (args.includes("--server") || args.includes("-s")) {
182
256
  await startServer();
257
+ return;
258
+ }
259
+
260
+ // --tray(显式指定,兼容旧指令)
261
+ if (args.includes("--tray") || args.includes("-t")) {
262
+ writeConfig();
263
+ await startTray();
264
+ return;
183
265
  }
266
+
267
+ // 默认:安装 + 配置 + 托盘
268
+ if (!ensureClaudeCode()) process.exit(1);
269
+ writeConfig();
270
+ await startTray();
184
271
  }
185
272
 
186
273
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panrouter",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
5
5
  "type": "module",
6
6
  "bin": {
package/tray-daemon.ps1 CHANGED
@@ -15,6 +15,8 @@ try {
15
15
 
16
16
  $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
17
17
  $serverPath = Join-Path $scriptDir "server.mjs"
18
+ # 【修复1】:在全局作用域提前把准确的脚本路径存下来,防止在 Click 事件块内丢失
19
+ $trayScriptPath = $MyInvocation.MyCommand.Path
18
20
 
19
21
  $nodePath = $env:PANROUTER_NODE
20
22
  if ([string]::IsNullOrWhiteSpace($nodePath)) {
@@ -102,7 +104,8 @@ try {
102
104
  reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v PanRouter /f 2>&1 | Out-Null
103
105
  $autoItem.Checked = $false
104
106
  } else {
105
- $cmd = "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`""
107
+ # 【修复2】:写入注册表时,务必加上 -STA 参数防止 UI 闪退,并使用刚刚存好的 $trayScriptPath
108
+ $cmd = "powershell.exe -WindowStyle Hidden -STA -ExecutionPolicy Bypass -File `"$trayScriptPath`""
106
109
  reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v PanRouter /t REG_SZ /d $cmd /f 2>&1 | Out-Null
107
110
  $autoItem.Checked = $true
108
111
  }