pansetup 1.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/bin/cli.js +191 -0
- package/package.json +16 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const { execSync, spawn } = require("child_process");
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// ANSI 色彩与 UI 辅助
|
|
10
|
+
// ============================================================================
|
|
11
|
+
const c = { reset: "\x1b[0m", bold: "\x1b[1m", green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", red: "\x1b[31m", gray: "\x1b[90m" };
|
|
12
|
+
const log = {
|
|
13
|
+
info: (msg) => console.log(`${c.cyan}[~]${c.reset} ${msg}`),
|
|
14
|
+
ok: (msg) => console.log(`${c.green}[OK]${c.reset} ${msg}`),
|
|
15
|
+
warn: (msg) => console.log(`${c.yellow}[!]${c.reset} ${msg}`),
|
|
16
|
+
error: (msg) => console.error(`${c.red}[X]${c.reset} ${msg}`)
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// 核心配置 (部署参数)
|
|
21
|
+
// ============================================================================
|
|
22
|
+
const CONFIG = {
|
|
23
|
+
port: 20128,
|
|
24
|
+
password: "123456",
|
|
25
|
+
provider: { provider: "deepseek", authType: "apikey", name: "1", apiKey: "1", priority: 1 },
|
|
26
|
+
combo: { name: "combo", models: ["oc/minimax-m3-free", "mmf/mimo-auto", "oc/deepseek-v4-flash-free"] },
|
|
27
|
+
aliases: [["deepseek-v4-flash-free", "oc/deepseek-v4-flash-free"], ["minimax-m3-free", "oc/minimax-m3-free"]],
|
|
28
|
+
claudeToken: "sk_9router"
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const IS_WIN = process.platform === "win32";
|
|
32
|
+
const HOME = os.homedir();
|
|
33
|
+
const DB_PATH = IS_WIN
|
|
34
|
+
? path.join(process.env.APPDATA || path.join(HOME, "AppData", "Roaming"), "9router", "db", "data.sqlite")
|
|
35
|
+
: path.join(HOME, ".9router", "db", "data.sqlite");
|
|
36
|
+
const CLAUDE_SETTINGS = path.join(HOME, ".claude", "settings.json");
|
|
37
|
+
|
|
38
|
+
function newId() {
|
|
39
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
40
|
+
const r = (Math.random() * 16) | 0; return (ch === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const delay = (ms) => new Promise(r => setTimeout(r, ms));
|
|
44
|
+
|
|
45
|
+
function installWithProgress(pkgName) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const npmCmd = IS_WIN ? "npm.cmd" : "npm";
|
|
48
|
+
const child = spawn(npmCmd, ["install", "-g", pkgName, "--prefer-online"], { stdio: "ignore" });
|
|
49
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
50
|
+
let i = 0; const startTime = Date.now();
|
|
51
|
+
const timer = setInterval(() => {
|
|
52
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
53
|
+
process.stdout.write(`\r ${c.cyan}拼命下载 ${pkgName} 中 ${frames[i]} (耗时: ${elapsed}s)...${c.reset} `);
|
|
54
|
+
i = (i + 1) % frames.length;
|
|
55
|
+
}, 100);
|
|
56
|
+
child.on("close", (code) => {
|
|
57
|
+
clearInterval(timer); process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
58
|
+
if (code === 0) resolve(); else reject(new Error(`Exit code ${code}`));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// 主流程
|
|
65
|
+
// ============================================================================
|
|
66
|
+
async function main() {
|
|
67
|
+
console.log(`\n${c.bold}${c.cyan}=========================================${c.reset}`);
|
|
68
|
+
console.log(` 🚀 ${c.bold}9Router & Claude 终极环境部署工具${c.reset}`);
|
|
69
|
+
console.log(`${c.bold}${c.cyan}=========================================${c.reset}\n`);
|
|
70
|
+
|
|
71
|
+
const nodeVer = process.versions.node.split('.').map(Number);
|
|
72
|
+
if (nodeVer[0] < 22 || (nodeVer[0] === 22 && nodeVer[1] < 5)) {
|
|
73
|
+
log.error(`Node.js 版本需 v22.5.0+。当前: ${process.version}`); process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try { execSync("9router --version", { stdio: "ignore" }); log.ok(`9router 已就绪。`); }
|
|
77
|
+
catch (e) {
|
|
78
|
+
log.warn(`未检测到 9router,正在自动安装全局包...`);
|
|
79
|
+
try { await installWithProgress("9router"); log.ok(`9router 安装成功!`); }
|
|
80
|
+
catch (err) { log.error(`9router 安装失败!`); process.exit(1); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try { execSync("claude --version", { stdio: "ignore" }); log.ok(`Claude Code 已就绪。`); }
|
|
84
|
+
catch (e) {
|
|
85
|
+
log.warn(`未检测到 Claude Code,正在自动安装...`);
|
|
86
|
+
try { await installWithProgress("@anthropic-ai/claude-code"); log.ok(`Claude Code 安装成功!`); }
|
|
87
|
+
catch (err) { log.warn(`Claude Code 安装失败,跳过。`); }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 【健壮性补丁 1】:嗅探并清理正在运行的 9router 进程,防止 SQLITE_BUSY 文件锁死!
|
|
91
|
+
log.info(`探测端口冲突与释放文件锁...`);
|
|
92
|
+
try {
|
|
93
|
+
if (IS_WIN) {
|
|
94
|
+
const out = execSync(`netstat -ano | findstr :${CONFIG.port}`, { encoding: "utf8" });
|
|
95
|
+
const pid = out.split('\n').find(l => l.includes('LISTENING'))?.trim()?.split(/\s+/)?.pop();
|
|
96
|
+
if (pid) {
|
|
97
|
+
log.warn(`检测到 9router 正在运行 (PID: ${pid}),正在安全终止以注入新配置...`);
|
|
98
|
+
execSync(`taskkill /F /PID ${pid} 2>nul`);
|
|
99
|
+
await delay(1500); // 必须等待 1.5 秒,让系统彻底释放 data.sqlite 的 wal 文件锁
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
execSync(`lsof -ti:${CONFIG.port} | xargs kill -9 2>/dev/null || true`);
|
|
103
|
+
await delay(1000);
|
|
104
|
+
}
|
|
105
|
+
} catch(e) {} // 没查到端口说明没在运行,直接跳过
|
|
106
|
+
|
|
107
|
+
// 数据库操作
|
|
108
|
+
let db;
|
|
109
|
+
try {
|
|
110
|
+
log.info(`正在解析并物理直写数据库...`);
|
|
111
|
+
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
112
|
+
|
|
113
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
114
|
+
db = new DatabaseSync(DB_PATH);
|
|
115
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
116
|
+
|
|
117
|
+
db.exec(`
|
|
118
|
+
CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
119
|
+
CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL);
|
|
120
|
+
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);
|
|
121
|
+
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);
|
|
122
|
+
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);
|
|
123
|
+
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);
|
|
124
|
+
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);
|
|
125
|
+
CREATE TABLE IF NOT EXISTS kv (scope TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY (scope, key));
|
|
126
|
+
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);
|
|
127
|
+
CREATE TABLE IF NOT EXISTS usageDaily (dateKey TEXT PRIMARY KEY, data TEXT NOT NULL);
|
|
128
|
+
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);
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
db.exec("BEGIN TRANSACTION");
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
|
+
const p = CONFIG.provider; const c = CONFIG.combo;
|
|
134
|
+
|
|
135
|
+
// 【修复Bug】:改为通过 provider 和 name 联合判断是否已存在配置,避免查不存在的列
|
|
136
|
+
const pData = JSON.stringify({ apiKey: p.apiKey, testStatus: "unknown", providerSpecificData: {} });
|
|
137
|
+
if (!db.prepare("SELECT id FROM providerConnections WHERE provider = ? AND name = ?").get(p.provider, p.name)) {
|
|
138
|
+
db.prepare("INSERT INTO providerConnections (id, provider, authType, name, email, priority, isActive, data, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?)").run(newId(), p.provider, p.authType, p.name, null, p.priority, pData, now, now);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!db.prepare("SELECT id FROM combos WHERE name = ?").get(c.name)) {
|
|
142
|
+
db.prepare("INSERT INTO combos (id, name, kind, models, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)").run(newId(), c.name, null, JSON.stringify(c.models), now, now);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const stmtAlias = db.prepare("INSERT OR IGNORE INTO kv (scope, key, value) VALUES ('modelAliases', ?, ?)");
|
|
146
|
+
for (const [k, v] of CONFIG.aliases) stmtAlias.run(k, JSON.stringify(v));
|
|
147
|
+
|
|
148
|
+
// 【健壮性补丁 3】:INSERT OR IGNORE 绝不覆盖老用户的自定义密码
|
|
149
|
+
db.prepare("INSERT OR IGNORE INTO settings (id, data) VALUES (1, ?)").run(JSON.stringify({ rtkEnabled: true, authMode: "password" }));
|
|
150
|
+
|
|
151
|
+
db.exec("COMMIT"); db.close();
|
|
152
|
+
log.ok(`数据库校验与注入完成!`);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (db) { try { db.exec("ROLLBACK"); db.close(); } catch(err){} }
|
|
155
|
+
log.error(`数据库操作失败: ${e.message}`); process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
log.info(`修改 Claude Code 网络重定向...`);
|
|
160
|
+
fs.mkdirSync(path.dirname(CLAUDE_SETTINGS), { recursive: true });
|
|
161
|
+
let claudeConfig = { hasCompletedOnboarding: true, env: {} };
|
|
162
|
+
if (fs.existsSync(CLAUDE_SETTINGS)) {
|
|
163
|
+
try { claudeConfig = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, "utf8").replace(/^/, "")); } catch (e) {}
|
|
164
|
+
}
|
|
165
|
+
claudeConfig.env = {
|
|
166
|
+
...(claudeConfig.env || {}),
|
|
167
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${CONFIG.port}/v1`,
|
|
168
|
+
ANTHROPIC_AUTH_TOKEN: CONFIG.claudeToken,
|
|
169
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: CONFIG.combo.name,
|
|
170
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: CONFIG.combo.name,
|
|
171
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: CONFIG.combo.name,
|
|
172
|
+
};
|
|
173
|
+
fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(claudeConfig, null, 2), "utf8");
|
|
174
|
+
log.ok(`Claude 流量已劫持至本地。`);
|
|
175
|
+
} catch (e) {}
|
|
176
|
+
|
|
177
|
+
console.log(`\n${c.bold}${c.green}=========================================${c.reset}`);
|
|
178
|
+
console.log(` 🎉 ${c.bold}全自动部署与配置圆满完成!${c.reset}`);
|
|
179
|
+
console.log(`${c.bold}${c.green}=========================================${c.reset}`);
|
|
180
|
+
console.log(` 🌐 9Router UI: ${c.cyan}http://127.0.0.1:${CONFIG.port}/dashboard${c.reset}`);
|
|
181
|
+
console.log(` 🤖 Claude 调用: 在任意终端输入 ${c.cyan}claude${c.reset} 即可`);
|
|
182
|
+
console.log(`${c.gray}-----------------------------------------${c.reset}\n`);
|
|
183
|
+
|
|
184
|
+
if (IS_WIN) {
|
|
185
|
+
log.info(`正在拉起 9Router...`);
|
|
186
|
+
const child = spawn("cmd.exe", ["/c", "start", "cmd.exe", "/k", "9router"], { detached: true, stdio: "ignore" });
|
|
187
|
+
child.unref();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main().catch(e => log.error("致命错误: " + e.message));
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pansetup",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "9Router & Claude Code 一键部署工具",
|
|
5
|
+
"bin": {
|
|
6
|
+
"pansetup": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["9router", "claude", "setup", "deploy"],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"type": "commonjs",
|
|
15
|
+
"dependencies": {}
|
|
16
|
+
}
|