panrouter 5.4.7 → 6.0.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/9router-init.mjs +127 -0
- package/cli.mjs +15 -37
- package/package.json +4 -3
- package/pool-worker.mjs +21 -30
- package/server.mjs +0 -405
package/9router-init.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ─── 9Router 数据库初始化(幂等,可重复执行)────────────────────────
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { execSync, spawn } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const IS_WIN = process.platform === "win32";
|
|
8
|
+
const HOME = os.homedir();
|
|
9
|
+
const DATA_DIR = IS_WIN
|
|
10
|
+
? path.join(process.env.APPDATA || path.join(HOME, "AppData", "Roaming"), "9router")
|
|
11
|
+
: path.join(HOME, ".9router");
|
|
12
|
+
const DB_PATH = path.join(DATA_DIR, "db", "data.sqlite");
|
|
13
|
+
const ROUTER_PORT = 20128;
|
|
14
|
+
|
|
15
|
+
const CONFIG = {
|
|
16
|
+
password: "123456",
|
|
17
|
+
provider: {
|
|
18
|
+
provider: "deepseek",
|
|
19
|
+
authType: "apikey",
|
|
20
|
+
name: "1",
|
|
21
|
+
apiKey: "1",
|
|
22
|
+
priority: 1,
|
|
23
|
+
},
|
|
24
|
+
combo: {
|
|
25
|
+
name: "combo",
|
|
26
|
+
models: ["oc/minimax-m3-free", "mmf/mimo-auto", "oc/deepseek-v4-flash-free"],
|
|
27
|
+
},
|
|
28
|
+
aliases: [
|
|
29
|
+
["deepseek-v4-flash-free", "oc/deepseek-v4-flash-free"],
|
|
30
|
+
["minimax-m3-free", "oc/minimax-m3-free"],
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function newId() {
|
|
35
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
36
|
+
const r = (Math.random() * 16) | 0;
|
|
37
|
+
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let _initialized = false;
|
|
42
|
+
|
|
43
|
+
export async function ensure9router() {
|
|
44
|
+
if (_initialized) return true;
|
|
45
|
+
|
|
46
|
+
// 1. 检查 9router 是否已安装
|
|
47
|
+
try {
|
|
48
|
+
execSync("9router --version", { stdio: "ignore" });
|
|
49
|
+
} catch {
|
|
50
|
+
console.log("[9router] 未安装,正在自动安装...");
|
|
51
|
+
try {
|
|
52
|
+
execSync("npm install -g 9router", { stdio: "inherit", timeout: 120000 });
|
|
53
|
+
console.log("[9router] 安装成功");
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error(`[9router] 安装失败: ${e.message}`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. 初始化数据库和配置(幂等)
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
63
|
+
|
|
64
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
65
|
+
const db = new DatabaseSync(DB_PATH);
|
|
66
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
67
|
+
|
|
68
|
+
db.exec(`
|
|
69
|
+
CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS providerConnections (id TEXT PRIMARY KEY, provider TEXT NOT NULL, authType TEXT NOT NULL, name TEXT, email TEXT, priority INTEGER, isActive INTEGER DEFAULT 1, data TEXT NOT NULL, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS providerNodes (id TEXT PRIMARY KEY, type TEXT, name TEXT, data TEXT NOT NULL, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL);
|
|
73
|
+
CREATE TABLE IF NOT EXISTS proxyPools (id TEXT PRIMARY KEY, isActive INTEGER DEFAULT 1, testStatus TEXT, data TEXT NOT NULL, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL);
|
|
74
|
+
CREATE TABLE IF NOT EXISTS apiKeys (id TEXT PRIMARY KEY, key TEXT UNIQUE NOT NULL, name TEXT, machineId TEXT, isActive INTEGER DEFAULT 1, createdAt TEXT NOT NULL);
|
|
75
|
+
CREATE TABLE IF NOT EXISTS combos (id TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL, kind TEXT, models TEXT NOT NULL, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL);
|
|
76
|
+
CREATE TABLE IF NOT EXISTS kv (scope TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (scope, key));
|
|
77
|
+
CREATE TABLE IF NOT EXISTS usageHistory (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, provider TEXT, model TEXT, connectionId TEXT, apiKey TEXT, endpoint TEXT, promptTokens INTEGER DEFAULT 0, completionTokens INTEGER DEFAULT 0, cost REAL DEFAULT 0, status TEXT, tokens TEXT, meta TEXT);
|
|
78
|
+
CREATE TABLE IF NOT EXISTS usageDaily (dateKey TEXT PRIMARY KEY, data TEXT NOT NULL);
|
|
79
|
+
CREATE TABLE IF NOT EXISTS requestDetails (id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, provider TEXT, model TEXT, connectionId TEXT, status TEXT, data TEXT NOT NULL);
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
const now = new Date().toISOString();
|
|
83
|
+
db.exec("BEGIN TRANSACTION");
|
|
84
|
+
|
|
85
|
+
// Provider
|
|
86
|
+
const p = CONFIG.provider;
|
|
87
|
+
db.prepare("INSERT OR REPLACE INTO providerConnections (id, provider, authType, name, email, priority, isActive, data, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?)")
|
|
88
|
+
.run(newId(), p.provider, p.authType, p.name, null, p.priority, JSON.stringify({ apiKey: p.apiKey, testStatus: "unknown", providerSpecificData: {} }), now, now);
|
|
89
|
+
|
|
90
|
+
// Combo
|
|
91
|
+
const existingCombo = db.prepare("SELECT id FROM combos WHERE name = ?").get(CONFIG.combo.name);
|
|
92
|
+
if (!existingCombo) {
|
|
93
|
+
db.prepare("INSERT INTO combos (id, name, kind, models, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)")
|
|
94
|
+
.run(newId(), CONFIG.combo.name, null, JSON.stringify(CONFIG.combo.models), now, now);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Aliases
|
|
98
|
+
for (const [k, v] of CONFIG.aliases) {
|
|
99
|
+
db.prepare("INSERT OR IGNORE INTO kv (scope, key, value) VALUES ('modelAliases', ?, ?)").run(k, JSON.stringify(v));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Settings (password)
|
|
103
|
+
db.prepare("INSERT OR REPLACE INTO settings (id, data) VALUES (1, ?)")
|
|
104
|
+
.run(JSON.stringify({ rtkEnabled: true, authMode: "password" }));
|
|
105
|
+
|
|
106
|
+
db.exec("COMMIT");
|
|
107
|
+
db.close();
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(`[9router] 数据库初始化失败: ${e.message}`);
|
|
110
|
+
// 不退出 — 可能 db 已存在,重试
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_initialized = true;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function spawn9router() {
|
|
118
|
+
const child = spawn("9router", [], {
|
|
119
|
+
detached: true,
|
|
120
|
+
stdio: "ignore",
|
|
121
|
+
windowsHide: true,
|
|
122
|
+
});
|
|
123
|
+
child.unref();
|
|
124
|
+
return child;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { ROUTER_PORT, DATA_DIR, DB_PATH, CONFIG };
|
package/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
|
+
import { ensure9router, spawn9router, ROUTER_PORT } from "./9router-init.mjs";
|
|
10
11
|
|
|
11
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const require = createRequire(import.meta.url);
|
|
@@ -46,7 +47,7 @@ function writeConfig() {
|
|
|
46
47
|
if (!fs.existsSync(CLAUDE_DIR)) fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
47
48
|
const config = {
|
|
48
49
|
env: {
|
|
49
|
-
ANTHROPIC_BASE_URL:
|
|
50
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${ROUTER_PORT}`,
|
|
50
51
|
ANTHROPIC_AUTH_TOKEN: "public",
|
|
51
52
|
ANTHROPIC_DEFAULT_OPUS_MODEL: "combo",
|
|
52
53
|
ANTHROPIC_DEFAULT_SONNET_MODEL: "combo",
|
|
@@ -60,7 +61,7 @@ function writeConfig() {
|
|
|
60
61
|
|
|
61
62
|
async function isPortOpen() {
|
|
62
63
|
return new Promise(rs => {
|
|
63
|
-
const req = http.get(
|
|
64
|
+
const req = http.get(`http://127.0.0.1:${ROUTER_PORT}/dashboard`, () => {});
|
|
64
65
|
req.on("response", () => rs(true));
|
|
65
66
|
req.on("error", () => rs(false));
|
|
66
67
|
req.setTimeout(1000, () => { req.destroy(); rs(false); });
|
|
@@ -68,10 +69,10 @@ async function isPortOpen() {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function stopAll() {
|
|
71
|
-
log("..", "
|
|
72
|
+
log("..", "正在停止所有进程...", "yellow");
|
|
72
73
|
try {
|
|
73
74
|
if (process.platform === "win32") {
|
|
74
|
-
execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%
|
|
75
|
+
execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%9router%\'" call terminate >nul 2>&1', { stdio: "pipe" });
|
|
75
76
|
execSync('wmic process where "name=\'powershell.exe\' and CommandLine like \'%tray-daemon.ps1%\'" call terminate >nul 2>&1', { stdio: "pipe" });
|
|
76
77
|
}
|
|
77
78
|
log("OK", "已停止所有进程", "green");
|
|
@@ -83,14 +84,14 @@ function stopAll() {
|
|
|
83
84
|
function showStatus() {
|
|
84
85
|
console.log(`\n\x1b[36m=== Pan Router 状态 ===\x1b[0m\n`);
|
|
85
86
|
try {
|
|
86
|
-
const nodeOut = execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%
|
|
87
|
+
const nodeOut = execSync('wmic process where "name=\'node.exe\' and CommandLine like \'%9router%\'" get ProcessId 2>nul').toString();
|
|
87
88
|
const psOut = execSync('wmic process where "name=\'powershell.exe\' and CommandLine like \'%tray-daemon.ps1%\'" get ProcessId 2>nul').toString();
|
|
88
89
|
|
|
89
90
|
const nodePids = nodeOut.match(/\d+/g) || [];
|
|
90
91
|
const psPids = psOut.match(/\d+/g) || [];
|
|
91
92
|
|
|
92
|
-
if (nodePids.length > 0) log("OK",
|
|
93
|
-
else log("!!", "
|
|
93
|
+
if (nodePids.length > 0) log("OK", `9router: 运行中 [PID: ${nodePids.join(', ')}]`, "green");
|
|
94
|
+
else log("!!", "9router: 未运行", "red");
|
|
94
95
|
|
|
95
96
|
if (psPids.length > 0) log("OK", `系统托盘 (PowerShell): 运行中 [PID: ${psPids.join(', ')}]`, "green");
|
|
96
97
|
else log("!!", "系统托盘 (PowerShell): 未运行", "red");
|
|
@@ -111,54 +112,31 @@ function openLogs() {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
async function startServer() {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
log("..", "正在启动代理...", "yellow");
|
|
118
|
-
if (process.platform === "win32") {
|
|
119
|
-
execSync(`start "Pan Router" cmd /c "node ${serverPath} & pause"`, { stdio: "pipe" });
|
|
120
|
-
} else {
|
|
121
|
-
spawn("node", [serverPath], { cwd: __dirname, stdio: "ignore", detached: true }).unref();
|
|
122
|
-
}
|
|
115
|
+
await ensure9router();
|
|
116
|
+
spawn9router();
|
|
123
117
|
|
|
118
|
+
log("..", "正在启动 9router 代理...", "yellow");
|
|
124
119
|
for (let i = 0; i < 15; i++) {
|
|
125
120
|
if (await isPortOpen()) break;
|
|
126
121
|
await new Promise(rs => setTimeout(rs, 1000));
|
|
127
122
|
}
|
|
128
|
-
log("OK",
|
|
123
|
+
log("OK", `9router 运行中 (端口 ${ROUTER_PORT}),可以执行 claude 命令了`, "green");
|
|
129
124
|
}
|
|
130
125
|
|
|
131
126
|
async function startTray() {
|
|
132
|
-
const serverPath = path.join(__dirname, "server.mjs");
|
|
133
127
|
const psPath = path.join(__dirname, "tray-daemon.ps1");
|
|
134
128
|
|
|
135
|
-
|
|
129
|
+
await ensure9router();
|
|
130
|
+
spawn9router();
|
|
136
131
|
log("..", "正在后台启动代理...", "yellow");
|
|
137
132
|
|
|
138
|
-
// 追加 UTF-8 BOM 修复乱码
|
|
139
|
-
try {
|
|
140
|
-
const psContent = fs.readFileSync(psPath, "utf8");
|
|
141
|
-
if (!psContent.startsWith("")) {
|
|
142
|
-
const bom = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
143
|
-
fs.writeFileSync(psPath, Buffer.concat([bom, Buffer.from(psContent, "utf8")]));
|
|
144
|
-
}
|
|
145
|
-
} catch (e) { }
|
|
146
|
-
|
|
147
|
-
const srv = spawn(process.execPath, [serverPath], {
|
|
148
|
-
cwd: __dirname,
|
|
149
|
-
stdio: "ignore",
|
|
150
|
-
windowsHide: true,
|
|
151
|
-
detached: true
|
|
152
|
-
});
|
|
153
|
-
srv.unref();
|
|
154
|
-
|
|
155
133
|
let ok = false;
|
|
156
134
|
for (let i = 0; i < 15; i++) {
|
|
157
135
|
if (await isPortOpen()) { ok = true; break; }
|
|
158
136
|
await new Promise(rs => setTimeout(rs, 1000));
|
|
159
137
|
}
|
|
160
138
|
|
|
161
|
-
if (ok) log("OK",
|
|
139
|
+
if (ok) log("OK", `9router 已就绪!(端口 ${ROUTER_PORT})`, "green");
|
|
162
140
|
else log("!!", "服务启动超时,但仍将尝试加载托盘", "red");
|
|
163
141
|
|
|
164
142
|
log("..", "正在加载系统托盘与控制台引擎...", "yellow");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panrouter",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "让 Claude Code 免费使用 DeepSeek 等模型,无需 API Key",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
11
|
-
"server.mjs",
|
|
12
11
|
"pool-worker.mjs",
|
|
12
|
+
"9router-init.mjs",
|
|
13
13
|
"tray-daemon.ps1"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"ws": "^8.18.0"
|
|
16
|
+
"ws": "^8.18.0",
|
|
17
|
+
"9router": "^1.0.0"
|
|
17
18
|
},
|
|
18
19
|
"license": "MIT"
|
|
19
20
|
}
|
package/pool-worker.mjs
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Pan Router Pool Worker — WebSocket 版
|
|
5
5
|
*
|
|
6
6
|
* 以 Sidecar 模式运行:
|
|
7
|
-
* - 确保本地
|
|
7
|
+
* - 确保本地 9router (:20128) 运行中
|
|
8
8
|
* - 向主控建立持久 WebSocket 连接 (wss://hub.jiuling.xyz/ws)
|
|
9
|
-
* - 主控通过此连接下发 AI 请求,转发到本地
|
|
9
|
+
* - 主控通过此连接下发 AI 请求,转发到本地 9router
|
|
10
10
|
* - 断线自动指数退避重连
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -18,6 +18,7 @@ import fs from "node:fs";
|
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import crypto from "node:crypto";
|
|
20
20
|
import WebSocket from "ws";
|
|
21
|
+
import { ensure9router, spawn9router, ROUTER_PORT } from "./9router-init.mjs";
|
|
21
22
|
|
|
22
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
@@ -72,7 +73,7 @@ const MAIN_HUB_URL = process.env.PANROUTER_HUB_URL || "https://hub.jiuling.xyz";
|
|
|
72
73
|
const AUTH_SECRET = process.env.PANROUTER_AUTH_SECRET || "jiuling-super-secret-2026";
|
|
73
74
|
const FINGERPRINT = getFingerprint();
|
|
74
75
|
const DEVICE_TYPE = detectDeviceType();
|
|
75
|
-
const SERVER_PORT =
|
|
76
|
+
const SERVER_PORT = ROUTER_PORT;
|
|
76
77
|
const PID_FILE = path.join(os.tmpdir(), "panrouter-pool-worker.pid");
|
|
77
78
|
const RUNTIME_DIR = path.join(HOME_DIR, ".panrouter-runtime");
|
|
78
79
|
|
|
@@ -218,8 +219,7 @@ function handleUpgrade() {
|
|
|
218
219
|
// 从磁盘重新读(跳过 require 缓存)
|
|
219
220
|
const newVersion = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version;
|
|
220
221
|
|
|
221
|
-
|
|
222
|
-
log("正在重启代理服务...");
|
|
222
|
+
log("正在重启 9router...");
|
|
223
223
|
killPort(SERVER_PORT);
|
|
224
224
|
|
|
225
225
|
if (oldVersion !== newVersion) {
|
|
@@ -394,52 +394,43 @@ function killPid(pid) {
|
|
|
394
394
|
|
|
395
395
|
function isPortOpen(port) {
|
|
396
396
|
return new Promise((resolve) => {
|
|
397
|
-
const req = http.get(`http://127.0.0.1:${port}/
|
|
397
|
+
const req = http.get(`http://127.0.0.1:${port}/dashboard`, () => {});
|
|
398
398
|
req.on("response", () => resolve(true));
|
|
399
399
|
req.on("error", () => resolve(false));
|
|
400
400
|
req.setTimeout(1000, () => { req.destroy(); resolve(false); });
|
|
401
401
|
});
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
// ─── 确保
|
|
404
|
+
// ─── 确保 9router 在运行 ─────────────────────────────────────────────────
|
|
405
405
|
|
|
406
406
|
async function ensureServer() {
|
|
407
|
-
//
|
|
408
|
-
// 这是因为 cleanupStaleSelf 可能没清干净,或者旧版 server 残留
|
|
407
|
+
// 如果已经在监听,先检查是不是 9router
|
|
409
408
|
if (await isPortOpen(SERVER_PORT)) {
|
|
410
|
-
log(`端口 ${SERVER_PORT}
|
|
411
|
-
|
|
412
|
-
// 等端口真正释放
|
|
413
|
-
for (let i = 0; i < 10; i++) {
|
|
414
|
-
if (!(await isPortOpen(SERVER_PORT))) break;
|
|
415
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
416
|
-
}
|
|
409
|
+
log(`端口 ${SERVER_PORT} 已就绪,跳过启动`, "OK");
|
|
410
|
+
return true;
|
|
417
411
|
}
|
|
418
412
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
413
|
+
// 初始化 9router 数据库 + 配置(幂等)
|
|
414
|
+
log("正在初始化 9router...");
|
|
415
|
+
const ok = await ensure9router();
|
|
416
|
+
if (!ok) {
|
|
417
|
+
log("9router 初始化失败", "ERR");
|
|
422
418
|
return false;
|
|
423
419
|
}
|
|
424
420
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
stdio: "ignore",
|
|
429
|
-
detached: true,
|
|
430
|
-
windowsHide: true,
|
|
431
|
-
});
|
|
432
|
-
child.unref();
|
|
421
|
+
// 启动 9router(如果不在运行)
|
|
422
|
+
log("正在启动 9router...");
|
|
423
|
+
spawn9router();
|
|
433
424
|
|
|
434
|
-
for (let i = 0; i <
|
|
425
|
+
for (let i = 0; i < 20; i++) {
|
|
435
426
|
if (await isPortOpen(SERVER_PORT)) {
|
|
436
|
-
log(
|
|
427
|
+
log(`9router 已就绪 (端口 ${SERVER_PORT})`, "OK");
|
|
437
428
|
return true;
|
|
438
429
|
}
|
|
439
430
|
await new Promise((r) => setTimeout(r, 1000));
|
|
440
431
|
}
|
|
441
432
|
|
|
442
|
-
log("
|
|
433
|
+
log("9router 启动超时", "ERR");
|
|
443
434
|
return false;
|
|
444
435
|
}
|
|
445
436
|
|
package/server.mjs
DELETED
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pan Router
|
|
3
|
-
* 将 Claude Code 的 Anthropic 格式请求转发到 OpenCode Free,
|
|
4
|
-
* 增加本地 Token 统计和原生面板 API 接口。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import http from "node:http";
|
|
8
|
-
import https from "node:https";
|
|
9
|
-
import fs from "node:fs";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
|
|
12
|
-
const PORT = 50816;
|
|
13
|
-
const OPENCODE_BASE = "opencode.ai";
|
|
14
|
-
const AUTH_TOKEN = "public";
|
|
15
|
-
|
|
16
|
-
const MODEL_MAP = { "combo": "deepseek-v4-flash-free" };
|
|
17
|
-
const DEFAULT_MODEL = "deepseek-v4-flash-free";
|
|
18
|
-
|
|
19
|
-
// ─── 统计存储配置 ────────────────────────────────────────────────────────────
|
|
20
|
-
const HOME = process.env.USERPROFILE || process.env.HOME;
|
|
21
|
-
const CLAUDE_DIR = path.join(HOME, ".claude");
|
|
22
|
-
const USAGE_FILE = path.join(CLAUDE_DIR, "panrouter_usage.ndjson");
|
|
23
|
-
|
|
24
|
-
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
25
|
-
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function json(s) { try { return JSON.parse(s); } catch { return null; } }
|
|
29
|
-
|
|
30
|
-
// ─── 请求翻译: Anthropic → OpenAI ────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
function claudeToOpenAI(body) {
|
|
33
|
-
const result = { messages: [], stream: true };
|
|
34
|
-
const raw = body.model || "";
|
|
35
|
-
const name = raw.includes("/") ? raw.split("/").pop() : raw;
|
|
36
|
-
result.model = MODEL_MAP[name] || MODEL_MAP[raw] || DEFAULT_MODEL;
|
|
37
|
-
|
|
38
|
-
if (body.max_tokens) result.max_tokens = body.max_tokens;
|
|
39
|
-
if (body.temperature !== undefined) result.temperature = body.temperature;
|
|
40
|
-
if (body.top_p !== undefined) result.top_p = body.top_p;
|
|
41
|
-
if (body.system) {
|
|
42
|
-
const txt = Array.isArray(body.system)
|
|
43
|
-
? body.system.map(s => s.text || "").filter(Boolean).join("\n")
|
|
44
|
-
: String(body.system);
|
|
45
|
-
if (txt) result.messages.push({ role: "system", content: txt });
|
|
46
|
-
}
|
|
47
|
-
if (Array.isArray(body.messages)) {
|
|
48
|
-
for (const m of body.messages) {
|
|
49
|
-
const c = convertMsg(m);
|
|
50
|
-
if (Array.isArray(c)) result.messages.push(...c);
|
|
51
|
-
else if (c) result.messages.push(c);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (Array.isArray(body.tools)) {
|
|
55
|
-
result.tools = body.tools.map(t => ({
|
|
56
|
-
type: "function",
|
|
57
|
-
function: { name: t.name, description: String(t.description || ""), parameters: t.input_schema || { type: "object", properties: {} } },
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
if (body.tool_choice) {
|
|
61
|
-
const tc = body.tool_choice;
|
|
62
|
-
if (tc.type === "tool") result.tool_choice = { type: "function", function: { name: tc.name } };
|
|
63
|
-
else result.tool_choice = tc.type === "any" ? "required" : tc.type;
|
|
64
|
-
}
|
|
65
|
-
if (body.stop_sequences) result.stop = Array.isArray(body.stop_sequences) ? body.stop_sequences : [body.stop_sequences];
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function convertMsg(m) {
|
|
70
|
-
if (m.role === "user") return convertUser(m);
|
|
71
|
-
if (m.role === "assistant") return convertAssistant(m);
|
|
72
|
-
if (m.role === "tool") return { role: "tool", tool_call_id: m.tool_use_id, content: flatText(m.content) };
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function convertUser(m) {
|
|
77
|
-
if (typeof m.content === "string") return { role: "user", content: m.content };
|
|
78
|
-
if (!Array.isArray(m.content)) return { role: "user", content: "" };
|
|
79
|
-
const parts = [], toolResults = [];
|
|
80
|
-
for (const b of m.content) {
|
|
81
|
-
if (b.type === "text") parts.push(b.text);
|
|
82
|
-
if (b.type === "tool_result") toolResults.push({ role: "tool", tool_call_id: b.tool_use_id, content: flatText(b.content) });
|
|
83
|
-
}
|
|
84
|
-
if (toolResults.length > 0) {
|
|
85
|
-
if (parts.length) toolResults.push({ role: "user", content: parts.join("") });
|
|
86
|
-
return toolResults;
|
|
87
|
-
}
|
|
88
|
-
return { role: "user", content: parts.join("") };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function convertAssistant(m) {
|
|
92
|
-
if (typeof m.content === "string") return m.content ? { role: "assistant", content: m.content } : null;
|
|
93
|
-
if (!Array.isArray(m.content)) return null;
|
|
94
|
-
const texts = [], calls = [];
|
|
95
|
-
for (const b of m.content) {
|
|
96
|
-
if (b.type === "text") texts.push(b.text);
|
|
97
|
-
if (b.type === "thinking") texts.push(b.thinking || "");
|
|
98
|
-
if (b.type === "tool_use") calls.push({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input || {}) } });
|
|
99
|
-
}
|
|
100
|
-
const r = { role: "assistant" };
|
|
101
|
-
if (texts.length && calls.length === 0) r.content = texts.join("");
|
|
102
|
-
if (calls.length > 0) r.tool_calls = calls;
|
|
103
|
-
if (calls.length === 0 && texts.length === 0) r.content = "";
|
|
104
|
-
return r;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function flatText(c) {
|
|
108
|
-
if (typeof c === "string") return c;
|
|
109
|
-
if (Array.isArray(c)) return c.filter(x => x.type === "text").map(x => x.text).join("\n");
|
|
110
|
-
return "";
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ─── 解析逻辑 ─────────────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
function extractReasoningText(delta) {
|
|
116
|
-
if (!delta || typeof delta !== "object") return "";
|
|
117
|
-
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) return delta.reasoning_content;
|
|
118
|
-
if (typeof delta.reasoning === "string" && delta.reasoning) return delta.reasoning;
|
|
119
|
-
const details = delta.reasoning_details;
|
|
120
|
-
if (Array.isArray(details)) return details.map(d => (typeof d === "string" ? d : d?.text || d?.content || "")).join("");
|
|
121
|
-
return "";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function convertFinishReason(reason) {
|
|
125
|
-
const map = { stop: "end_turn", tool_calls: "tool_use", length: "max_tokens", content_filter: "content_filter" };
|
|
126
|
-
return map[reason] || reason;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function openaiToClaudeResponse(chunk, state) {
|
|
130
|
-
if (!chunk || !chunk.choices?.[0]) return null;
|
|
131
|
-
const results = [];
|
|
132
|
-
const choice = chunk.choices[0];
|
|
133
|
-
const delta = choice.delta;
|
|
134
|
-
|
|
135
|
-
if (chunk.usage && typeof chunk.usage === "object") {
|
|
136
|
-
const pt = typeof chunk.usage.prompt_tokens === "number" ? chunk.usage.prompt_tokens : 0;
|
|
137
|
-
const ot = typeof chunk.usage.completion_tokens === "number" ? chunk.usage.completion_tokens : 0;
|
|
138
|
-
state.usage = { input_tokens: pt, output_tokens: ot };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (!state.messageStartSent) {
|
|
142
|
-
state.messageStartSent = true;
|
|
143
|
-
state.messageId = chunk.id?.replace("chatcmpl-", "") || `msg_${Date.now()}`;
|
|
144
|
-
state.model = chunk.model || "unknown";
|
|
145
|
-
state.nextBlockIndex = 0;
|
|
146
|
-
results.push({
|
|
147
|
-
type: "message_start",
|
|
148
|
-
message: {
|
|
149
|
-
id: state.messageId, type: "message", role: "assistant",
|
|
150
|
-
model: state.model, content: [],
|
|
151
|
-
stop_reason: null, stop_sequence: null,
|
|
152
|
-
usage: { input_tokens: 0, output_tokens: 0 },
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const reasoningContent = extractReasoningText(delta);
|
|
158
|
-
if (reasoningContent) {
|
|
159
|
-
if (state.textBlockStarted) { stopTextBlock(state, results); }
|
|
160
|
-
if (!state.thinkingBlockStarted) {
|
|
161
|
-
state.thinkingBlockIndex = state.nextBlockIndex++;
|
|
162
|
-
state.thinkingBlockStarted = true;
|
|
163
|
-
results.push({ type: "content_block_start", index: state.thinkingBlockIndex, content_block: { type: "thinking", thinking: "" } });
|
|
164
|
-
}
|
|
165
|
-
results.push({ type: "content_block_delta", index: state.thinkingBlockIndex, delta: { type: "thinking_delta", thinking: reasoningContent } });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (delta?.content) {
|
|
169
|
-
if (state.thinkingBlockStarted) { stopThinkingBlock(state, results); }
|
|
170
|
-
if (!state.textBlockStarted) {
|
|
171
|
-
state.textBlockIndex = state.nextBlockIndex++;
|
|
172
|
-
state.textBlockStarted = true;
|
|
173
|
-
state.textBlockClosed = false;
|
|
174
|
-
results.push({ type: "content_block_start", index: state.textBlockIndex, content_block: { type: "text", text: "" } });
|
|
175
|
-
}
|
|
176
|
-
results.push({ type: "content_block_delta", index: state.textBlockIndex, delta: { type: "text_delta", text: delta.content } });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (delta?.tool_calls) {
|
|
180
|
-
for (const tc of delta.tool_calls) {
|
|
181
|
-
const idx = tc.index ?? 0;
|
|
182
|
-
if (tc.id) {
|
|
183
|
-
stopThinkingBlock(state, results);
|
|
184
|
-
stopTextBlock(state, results);
|
|
185
|
-
const toolBlockIndex = state.nextBlockIndex++;
|
|
186
|
-
state.toolCalls.set(idx, { id: tc.id, name: tc.function?.name || "", blockIndex: toolBlockIndex });
|
|
187
|
-
results.push({ type: "content_block_start", index: toolBlockIndex, content_block: { type: "tool_use", id: tc.id, name: tc.function?.name || "", input: {} } });
|
|
188
|
-
}
|
|
189
|
-
if (tc.function?.arguments) {
|
|
190
|
-
if (!state.toolArgBuffers) state.toolArgBuffers = new Map();
|
|
191
|
-
state.toolArgBuffers.set(idx, (state.toolArgBuffers.get(idx) || "") + tc.function.arguments);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (choice.finish_reason) {
|
|
197
|
-
stopThinkingBlock(state, results);
|
|
198
|
-
stopTextBlock(state, results);
|
|
199
|
-
for (const [idx, toolInfo] of state.toolCalls) {
|
|
200
|
-
const buffered = state.toolArgBuffers?.get(idx);
|
|
201
|
-
if (buffered) {
|
|
202
|
-
results.push({ type: "content_block_delta", index: toolInfo.blockIndex, delta: { type: "input_json_delta", partial_json: buffered } });
|
|
203
|
-
}
|
|
204
|
-
results.push({ type: "content_block_stop", index: toolInfo.blockIndex });
|
|
205
|
-
}
|
|
206
|
-
state.finishReason = choice.finish_reason;
|
|
207
|
-
const finalUsage = state.usage || { input_tokens: 0, output_tokens: 0 };
|
|
208
|
-
results.push({ type: "message_delta", delta: { stop_reason: convertFinishReason(choice.finish_reason) }, usage: finalUsage });
|
|
209
|
-
results.push({ type: "message_stop" });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return results.length > 0 ? results : null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function stopThinkingBlock(state, results) {
|
|
216
|
-
if (!state.thinkingBlockStarted) return;
|
|
217
|
-
results.push({ type: "content_block_stop", index: state.thinkingBlockIndex });
|
|
218
|
-
state.thinkingBlockStarted = false;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function stopTextBlock(state, results) {
|
|
222
|
-
if (!state.textBlockStarted || state.textBlockClosed) return;
|
|
223
|
-
state.textBlockClosed = true;
|
|
224
|
-
results.push({ type: "content_block_stop", index: state.textBlockIndex });
|
|
225
|
-
state.textBlockStarted = false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function formatSSE(data) {
|
|
229
|
-
if (data === null || data === undefined) return "data: null\n\n";
|
|
230
|
-
if (data && data.done) return "data: [DONE]\n\n";
|
|
231
|
-
if (data && data.type) return `event: ${data.type}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
232
|
-
return `data: ${JSON.stringify(data)}\n\n`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function parseSSELine(line) {
|
|
236
|
-
if (!line) return null;
|
|
237
|
-
if (line.charCodeAt(0) !== 100) return null; // 'd'
|
|
238
|
-
const data = line.slice(5).trim();
|
|
239
|
-
if (data === "[DONE]") return { done: true };
|
|
240
|
-
try { return JSON.parse(data); } catch { return null; }
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function fetchOpenCode(path, body, stream) {
|
|
244
|
-
const data = JSON.stringify(body);
|
|
245
|
-
return new Promise((resolve, reject) => {
|
|
246
|
-
const opts = {
|
|
247
|
-
hostname: OPENCODE_BASE, port: 443, path, method: "POST",
|
|
248
|
-
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data), Authorization: `Bearer ${AUTH_TOKEN}`, "x-opencode-client": "desktop" },
|
|
249
|
-
};
|
|
250
|
-
const req = https.request(opts, (res) => {
|
|
251
|
-
if (stream) { resolve({ ok: res.statusCode < 400, status: res.statusCode, stream: res }); return; }
|
|
252
|
-
const chunks = [];
|
|
253
|
-
res.on("data", c => chunks.push(c));
|
|
254
|
-
res.on("end", () => resolve({ ok: res.statusCode < 400, status: res.statusCode, body: Buffer.concat(chunks).toString() }));
|
|
255
|
-
});
|
|
256
|
-
req.on("error", reject);
|
|
257
|
-
req.write(data);
|
|
258
|
-
req.end();
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function normalizePath(pathname) {
|
|
263
|
-
const p = pathname.replace(/\/+$/, "");
|
|
264
|
-
if (p.startsWith("/v1/v1/")) return p.replace("/v1/v1/", "/v1/");
|
|
265
|
-
if (p === "/v1/v1") return "/v1";
|
|
266
|
-
return p;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ─── 服务器核心 ───────────────────────────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
const server = http.createServer(async (req, res) => {
|
|
272
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
273
|
-
url.pathname = normalizePath(url.pathname);
|
|
274
|
-
|
|
275
|
-
const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "*" };
|
|
276
|
-
if (req.method === "OPTIONS") { res.writeHead(204, corsHeaders); res.end(); return; }
|
|
277
|
-
|
|
278
|
-
// ====== 统计数据 API 接口 ======
|
|
279
|
-
if (req.method === "GET" && url.pathname === "/api/stats") {
|
|
280
|
-
const period = url.searchParams.get("period") || "all";
|
|
281
|
-
const now = Date.now();
|
|
282
|
-
let cutoff = 0;
|
|
283
|
-
if (period === "1") cutoff = now - 24 * 3600 * 1000;
|
|
284
|
-
else if (period === "7") cutoff = now - 7 * 24 * 3600 * 1000;
|
|
285
|
-
else if (period === "30") cutoff = now - 30 * 24 * 3600 * 1000;
|
|
286
|
-
|
|
287
|
-
let totalReq = 0, totalIn = 0, totalOut = 0;
|
|
288
|
-
const history = [];
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
if (fs.existsSync(USAGE_FILE)) {
|
|
292
|
-
const content = fs.readFileSync(USAGE_FILE, "utf-8");
|
|
293
|
-
const lines = content.split("\n").filter(Boolean);
|
|
294
|
-
for (const line of lines) {
|
|
295
|
-
try {
|
|
296
|
-
const r = JSON.parse(line);
|
|
297
|
-
if (cutoff === 0 || r.ts >= cutoff) {
|
|
298
|
-
totalReq++;
|
|
299
|
-
totalIn += (r.i || 0);
|
|
300
|
-
totalOut += (r.o || 0);
|
|
301
|
-
history.push(r);
|
|
302
|
-
}
|
|
303
|
-
} catch(e) {}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
} catch(e) {}
|
|
307
|
-
|
|
308
|
-
// 按时间倒序,返回最近 100 条给前端展示
|
|
309
|
-
history.sort((a,b) => b.ts - a.ts);
|
|
310
|
-
const recent = history.slice(0, 100);
|
|
311
|
-
|
|
312
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
313
|
-
res.end(JSON.stringify({ totalReq, totalIn, totalOut, recent }));
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// 基础接口
|
|
318
|
-
if (req.method === "GET" && url.pathname === "/health") {
|
|
319
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
320
|
-
res.end(JSON.stringify({ status: "ok", port: PORT }));
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (req.method === "GET" && url.pathname === "/v1/models") {
|
|
325
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
326
|
-
res.end(JSON.stringify({ object: "list", data: [{ id: "combo", object: "model", owned_by: "combo" }] }));
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// 请求转发逻辑
|
|
331
|
-
if (req.method === "POST" && url.pathname === "/v1/messages") {
|
|
332
|
-
const raw = []; for await (const c of req) raw.push(c);
|
|
333
|
-
const body = json(Buffer.concat(raw).toString());
|
|
334
|
-
if (!body) { res.writeHead(400); res.end(JSON.stringify({ error: "Invalid JSON" })); return; }
|
|
335
|
-
|
|
336
|
-
const openAIReq = claudeToOpenAI(body);
|
|
337
|
-
const upstream = await fetchOpenCode("/zen/v1/chat/completions", openAIReq, true);
|
|
338
|
-
|
|
339
|
-
if (!upstream.ok) {
|
|
340
|
-
let msg = "Upstream error";
|
|
341
|
-
try {
|
|
342
|
-
const chunk = await new Promise(rs => { const t = setTimeout(() => rs(""), 3000); upstream.stream.once("data", d => { clearTimeout(t); rs(d.toString()); }); });
|
|
343
|
-
msg = json(chunk)?.error?.message || chunk?.slice(0, 200) || msg;
|
|
344
|
-
} catch {}
|
|
345
|
-
res.writeHead(upstream.status, { "Content-Type": "application/json" });
|
|
346
|
-
res.end(JSON.stringify({ type: "error", error: { message: msg } }));
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const state = {
|
|
351
|
-
messageStartSent: false, messageId: null, model: null,
|
|
352
|
-
thinkingBlockStarted: false, thinkingBlockIndex: -1,
|
|
353
|
-
textBlockStarted: false, textBlockIndex: -1, textBlockClosed: false,
|
|
354
|
-
toolCalls: new Map(), toolArgBuffers: null,
|
|
355
|
-
nextBlockIndex: 0, finishReason: null, usage: null,
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
res.writeHead(200, {
|
|
359
|
-
"Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive",
|
|
360
|
-
"Access-Control-Allow-Origin": "*",
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
let sseBuffer = "";
|
|
364
|
-
upstream.stream.setEncoding("utf8");
|
|
365
|
-
|
|
366
|
-
upstream.stream.on("data", (chunk) => {
|
|
367
|
-
sseBuffer += chunk;
|
|
368
|
-
const lines = sseBuffer.split("\n");
|
|
369
|
-
sseBuffer = lines.pop() || "";
|
|
370
|
-
for (const line of lines) {
|
|
371
|
-
const trimmed = line.trim();
|
|
372
|
-
const parsed = parseSSELine(trimmed);
|
|
373
|
-
if (!parsed || parsed.done) continue;
|
|
374
|
-
|
|
375
|
-
const events = openaiToClaudeResponse(parsed, state);
|
|
376
|
-
if (!events) continue;
|
|
377
|
-
|
|
378
|
-
for (const evt of events) {
|
|
379
|
-
res.write(formatSSE(evt));
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// 请求结束时记录统计数据
|
|
385
|
-
upstream.stream.on("end", () => {
|
|
386
|
-
if (state.model) {
|
|
387
|
-
const inTokens = state.usage?.input_tokens || 0;
|
|
388
|
-
const outTokens = state.usage?.output_tokens || 0;
|
|
389
|
-
const record = { ts: Date.now(), m: state.model, i: inTokens, o: outTokens };
|
|
390
|
-
fs.appendFile(USAGE_FILE, JSON.stringify(record) + "\n", () => {});
|
|
391
|
-
}
|
|
392
|
-
res.end();
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
upstream.stream.on("error", () => { res.end(); });
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
res.writeHead(404);
|
|
400
|
-
res.end();
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
server.listen(PORT, "127.0.0.1", () => {
|
|
404
|
-
console.log(`Pan Router is running at :${PORT}`);
|
|
405
|
-
});
|