Files
sub2proxy/sub2proxy.js
T

816 lines
25 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const http = require('http');
const { execSync, exec } = require('child_process');
// Auto-install js-yaml if missing
try {
require('js-yaml');
} catch (e) {
console.log('js-yaml module not found. Installing it via npm...');
try {
execSync('npm install js-yaml', { stdio: 'inherit' });
} catch (err) {
console.error('Failed to install js-yaml:', err.message);
process.exit(1);
}
}
const yaml = require('js-yaml');
function getProtoName(typeStr) {
const t = typeStr.toLowerCase();
if (t === 'hysteria2') return 'hy2';
if (t === 'ss' || t === 'shadowsocks') return 'ss';
return t;
}
function parseSpeed(speedStr) {
if (!speedStr) return null;
const m = String(speedStr).match(/(\d+)/);
return m ? parseInt(m[1], 10) : null;
}
function clashToSingbox(node) {
const sb = {};
const t = (node.type || '').toLowerCase();
sb.tag = node.name;
sb.server = node.server;
sb.server_port = parseInt(node.port || 0, 10);
if (t === 'hysteria2') {
sb.type = 'hysteria2';
sb.password = node.password;
const up = parseSpeed(node.up);
if (up !== null) sb.up_mbps = up;
const down = parseSpeed(node.down);
if (down !== null) sb.down_mbps = down;
const tls = { enabled: true };
if (node.sni) tls.server_name = node.sni;
if (node.hasOwnProperty('skip-cert-verify')) tls.insecure = !!node['skip-cert-verify'];
if (node.alpn) tls.alpn = node.alpn;
sb.tls = tls;
} else if (t === 'vless') {
sb.type = 'vless';
sb.uuid = node.uuid;
sb.flow = node.flow || '';
sb.network = node.network || 'tcp';
if (node['packet-encoding']) sb.packet_encoding = node['packet-encoding'];
const tls = { enabled: false };
if (node.tls) {
tls.enabled = true;
if (node.servername) tls.server_name = node.servername;
if (node.hasOwnProperty('skip-cert-verify')) tls.insecure = !!node['skip-cert-verify'];
if (node.alpn) tls.alpn = node.alpn;
if (node['reality-opts'] || (node.opts && node.opts.reality)) {
const ropts = node['reality-opts'] || {};
tls.reality = {
enabled: true,
public_key: ropts['public-key'] || '',
short_id: ropts['short-id'] || ''
};
}
if (node['client-fingerprint']) {
tls.utls = {
enabled: true,
fingerprint: node['client-fingerprint']
};
}
}
sb.tls = tls;
} else if (t === 'tuic') {
sb.type = 'tuic';
sb.uuid = node.uuid;
sb.password = node.password;
if (node['congestion-controller']) sb.congestion_control = node['congestion-controller'];
if (node['udp-relay-mode']) sb.udp_relay_mode = node['udp-relay-mode'];
if (node['reduce-rtt']) sb.zero_rtt_handshake = !!node['reduce-rtt'];
const tls = { enabled: true };
if (node.hasOwnProperty('disable-sni')) tls.disable_sni = !!node['disable-sni'];
if (node.hasOwnProperty('skip-cert-verify')) tls.insecure = !!node['skip-cert-verify'];
if (node.alpn) tls.alpn = node.alpn;
sb.tls = tls;
} else if (t === 'ss' || t === 'shadowsocks') {
sb.type = 'shadowsocks';
sb.method = node.cipher;
sb.password = node.password;
} else if (t === 'trojan') {
sb.type = 'trojan';
sb.password = node.password;
const tls = { enabled: true };
if (node.sni) tls.server_name = node.sni;
if (node.hasOwnProperty('skip-cert-verify')) tls.insecure = !!node['skip-cert-verify'];
if (node.alpn) tls.alpn = node.alpn;
sb.tls = tls;
} else if (t === 'vmess') {
sb.type = 'vmess';
sb.uuid = node.uuid;
sb.security = node.cipher || 'auto';
sb.alter_id = parseInt(node.alterId || 0, 10);
const tls = { enabled: false };
if (node.tls) {
tls.enabled = true;
if (node.servername) tls.server_name = node.servername;
if (node.hasOwnProperty('skip-cert-verify')) tls.insecure = !!node['skip-cert-verify'];
}
sb.tls = tls;
if (node.network === 'ws') {
const ws_opts = node['ws-opts'] || {};
sb.transport = {
type: 'ws',
path: ws_opts.path || '/',
headers: ws_opts.headers || {}
};
}
} else {
sb.type = t;
for (const k in node) {
if (!['name', 'port', 'type', 'server'].includes(k)) {
sb[k] = node[k];
}
}
}
return sb;
}
function singboxToClash(node) {
const c = {};
const t = (node.type || '').toLowerCase();
c.name = node.tag;
c.server = node.server;
c.port = parseInt(node.server_port || 0, 10);
c.udp = true;
if (t === 'hysteria2') {
c.type = 'hysteria2';
c.password = node.password;
if (node.up_mbps) c.up = `${node.up_mbps} Mbps`;
if (node.down_mbps) c.down = `${node.down_mbps} Mbps`;
const tls = node.tls || {};
if (tls.enabled) {
if (tls.server_name) c.sni = tls.server_name;
if (tls.hasOwnProperty('insecure')) c['skip-cert-verify'] = !!tls.insecure;
if (tls.alpn) c.alpn = tls.alpn;
}
} else if (t === 'vless') {
c.type = 'vless';
c.uuid = node.uuid;
c.flow = node.flow || '';
c.network = node.network || 'tcp';
if (node.packet_encoding) c['packet-encoding'] = node.packet_encoding;
const tls = node.tls || {};
if (tls.enabled) {
c.tls = true;
if (tls.server_name) c.servername = tls.server_name;
if (tls.hasOwnProperty('insecure')) c['skip-cert-verify'] = !!tls.insecure;
if (tls.alpn) c.alpn = tls.alpn;
const reality = tls.reality || {};
if (reality.enabled) {
c['reality-opts'] = {
'public-key': reality.public_key || '',
'short-id': reality.short_id || ''
};
}
const utls = tls.utls || {};
if (utls.enabled && utls.fingerprint) {
c['client-fingerprint'] = utls.fingerprint;
}
} else {
c.tls = false;
}
} else if (t === 'tuic') {
c.type = 'tuic';
c.version = 5;
c.uuid = node.uuid;
c.password = node.password;
if (node.congestion_control) c['congestion-controller'] = node.congestion_control;
if (node.udp_relay_mode) c['udp-relay-mode'] = node.udp_relay_mode;
if (node.hasOwnProperty('zero_rtt_handshake')) c['reduce-rtt'] = !!node.zero_rtt_handshake;
const tls = node.tls || {};
if (tls.enabled) {
if (tls.hasOwnProperty('disable_sni')) c['disable-sni'] = !!tls.disable_sni;
if (tls.hasOwnProperty('insecure')) c['skip-cert-verify'] = !!tls.insecure;
if (tls.alpn) c.alpn = tls.alpn;
}
} else if (t === 'shadowsocks') {
c.type = 'ss';
c.cipher = node.method;
c.password = node.password;
} else if (t === 'trojan') {
c.type = 'trojan';
c.password = node.password;
const tls = node.tls || {};
if (tls.enabled) {
if (tls.server_name) c.sni = tls.server_name;
if (tls.hasOwnProperty('insecure')) c['skip-cert-verify'] = !!tls.insecure;
if (tls.alpn) c.alpn = tls.alpn;
}
} else if (t === 'vmess') {
c.type = 'vmess';
c.uuid = node.uuid;
c.cipher = node.security || 'auto';
c.alterId = parseInt(node.alter_id || 0, 10);
const tls = node.tls || {};
if (tls.enabled) {
c.tls = true;
if (tls.server_name) c.servername = tls.server_name;
if (tls.hasOwnProperty('insecure')) c['skip-cert-verify'] = !!tls.insecure;
} else {
c.tls = false;
}
const trans = node.transport || {};
if (trans.type === 'ws') {
c.network = 'ws';
c['ws-opts'] = {
path: trans.path || '/',
headers: trans.headers || {}
};
}
} else {
c.type = t;
for (const k in node) {
if (!['tag', 'server_port', 'type', 'server'].includes(k)) {
c[k] = node[k];
}
}
}
return c;
}
async function loadSubscriptionContent(link) {
if (link.startsWith('http://') || link.startsWith('https://')) {
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), 15000);
const res = await fetch(link, {
headers: {
'User-Agent': 'clash-meta; sing-box; Mozilla/5.0'
},
signal: controller.signal
});
clearTimeout(id);
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return await res.text();
} catch (e) {
console.error(`Error fetching ${link}:`, e.message);
return null;
}
} else {
try {
return fs.readFileSync(link, 'utf-8');
} catch (e) {
console.error(`Error reading local file ${link}:`, e.message);
return null;
}
}
}
function parseClashNodes(content) {
try {
const data = yaml.load(content);
if (data && typeof data === 'object' && Array.isArray(data.proxies)) {
return data.proxies;
}
} catch (e) {
console.error(`Error parsing Clash YAML:`, e.message);
}
return [];
}
function parseSingboxNodes(content) {
try {
const data = JSON.parse(content);
if (data && typeof data === 'object') {
const outbounds = data.outbounds || [];
return outbounds.filter(o =>
o && o.type && !['selector', 'urltest', 'direct', 'block', 'dns'].includes(o.type)
);
}
} catch (e) {
console.error(`Error parsing Sing-box JSON:`, e.message);
}
return [];
}
const usedNames = new Set();
function renameNode(node, subName, isClash = true) {
const typeStr = (node.type || '').toLowerCase();
const protoClean = getProtoName(typeStr);
const baseName = `${subName}-${protoClean}`;
let newName = baseName;
let counter = 1;
while (usedNames.has(newName.toLowerCase())) {
newName = `${baseName}-${counter}`;
counter++;
}
usedNames.add(newName.toLowerCase());
if (isClash) {
node.name = newName;
} else {
node.tag = newName;
}
return newName;
}
function detectTemplates() {
const tplDir = 'templates';
let yamlTpl = path.join(tplDir, 'CatMata.optimized.yaml');
let jsonTpl = path.join(tplDir, 'CatMata.json');
if (!fs.existsSync(yamlTpl)) {
if (fs.existsSync(tplDir)) {
const yamls = fs.readdirSync(tplDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
const matched = yamls.find(f => f.includes('CatMata'));
if (matched) yamlTpl = path.join(tplDir, matched);
} else {
yamlTpl = 'CatMata.optimized.yaml';
if (!fs.existsSync(yamlTpl)) {
const yamls = fs.readdirSync('.').filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
const matched = yamls.find(f => f.includes('CatMata'));
if (matched) yamlTpl = matched;
}
}
}
if (!fs.existsSync(jsonTpl)) {
if (fs.existsSync(tplDir)) {
if (fs.existsSync(path.join(tplDir, 'CatMata-tun.json'))) {
jsonTpl = path.join(tplDir, 'CatMata-tun.json');
} else {
const jsons = fs.readdirSync(tplDir).filter(f => f.endsWith('.json'));
const matched = jsons.find(f => f.includes('CatMata'));
if (matched) jsonTpl = path.join(tplDir, matched);
}
} else {
jsonTpl = 'CatMata.json';
if (!fs.existsSync(jsonTpl)) {
if (fs.existsSync('CatMata-tun.json')) {
jsonTpl = 'CatMata-tun.json';
} else {
const jsons = fs.readdirSync('.').filter(f => f.endsWith('.json') && f !== 'subs.json');
const matched = jsons.find(f => f.includes('CatMata'));
if (matched) jsonTpl = matched;
}
}
}
}
return { yamlTpl, jsonTpl };
}
function replaceNodeRefs(data, oldNodesSet, fallbackNode) {
if (data && typeof data === 'object') {
if (Array.isArray(data)) {
for (const item of data) {
replaceNodeRefs(item, oldNodesSet, fallbackNode);
}
} else {
for (const k in data) {
if ((k === 'detour' || k === 'download_detour') && typeof data[k] === 'string') {
if (oldNodesSet.has(data[k])) {
data[k] = fallbackNode;
}
} else {
replaceNodeRefs(data[k], oldNodesSet, fallbackNode);
}
}
}
}
}
// Configuration paths
const subsFile = 'subs.json';
const outDir = 'out';
const PORT = 3000;
// Central configuration generation function
function generateConfigs(rawClashNodes, dns = null) {
const { yamlTpl, jsonTpl } = detectTemplates();
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
// 1. Process Clash Config
const clashConfig = yaml.load(fs.readFileSync(yamlTpl, 'utf-8')) || {};
clashConfig.proxies = rawClashNodes;
if (dns && dns.clash) {
clashConfig.dns = yaml.load(dns.clash);
}
const groupNames = new Set((clashConfig['proxy-groups'] || []).map(g => g.name));
const specialNames = new Set(['DIRECT', 'REJECT', 'PASS', 'COMPATIBLE']);
const newNodeNames = rawClashNodes.map(n => n.name);
for (const group of (clashConfig['proxy-groups'] || [])) {
const oldProxies = group.proxies || [];
const nonNodeItems = oldProxies.filter(p => groupNames.has(p) || specialNames.has(p));
const hasNodes = oldProxies.some(p => !groupNames.has(p) && !specialNames.has(p));
if (hasNodes || nonNodeItems.length === 0) {
group.proxies = [...nonNodeItems, ...newNodeNames];
}
}
const outYamlPath = path.join(outDir, 'CatMata-sub.yaml');
fs.writeFileSync(outYamlPath, yaml.dump(clashConfig, { noRefs: true, sortKeys: false }), 'utf-8');
// 2. Process Sing-box Config
const sbConfig = JSON.parse(fs.readFileSync(jsonTpl, 'utf-8')) || {};
const allSingboxNodes = rawClashNodes.map(n => {
const nodeCopy = JSON.parse(JSON.stringify(n));
return clashToSingbox(nodeCopy);
});
if (dns && dns.singbox) {
sbConfig.dns = JSON.parse(dns.singbox);
}
const oldNodesSet = new Set();
const preservedOutbounds = [];
for (const o of (sbConfig.outbounds || [])) {
if (['selector', 'urltest', 'direct', 'block', 'dns'].includes(o.type)) {
preservedOutbounds.push(o);
} else {
oldNodesSet.add(o.tag);
}
}
const groupTags = new Set(preservedOutbounds.filter(o => ['selector', 'urltest'].includes(o.type)).map(o => o.tag));
const specialTags = new Set(['direct', 'block', 'dns']);
const newNodeTags = allSingboxNodes.map(n => n.tag);
for (const o of preservedOutbounds) {
if (['selector', 'urltest'].includes(o.type)) {
const oldList = o.outbounds || [];
const nonNodeTags = oldList.filter(t => groupTags.has(t) || specialTags.has(t));
const hasNodes = oldList.some(t => !groupTags.has(t) && !specialTags.has(t));
if (hasNodes || nonNodeTags.length === 0) {
o.outbounds = [...nonNodeTags, ...newNodeTags];
}
if (o.default && oldNodesSet.has(o.default)) {
o.default = newNodeTags[0];
}
}
}
replaceNodeRefs(sbConfig, oldNodesSet, newNodeTags[0]);
sbConfig.outbounds = [...preservedOutbounds, ...allSingboxNodes];
const outJsonPath = path.join(outDir, 'CatMata-sub.json');
fs.writeFileSync(outJsonPath, JSON.stringify(sbConfig, null, 2), 'utf-8');
// Archive in history
const historyDir = path.join(outDir, 'history');
if (!fs.existsSync(historyDir)) {
fs.mkdirSync(historyDir, { recursive: true });
}
const now = new Date();
const pad = n => String(n).padStart(2, '0');
const timestampFile = `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
const timeStr = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
const histYamlName = `CatMata-sub-${timestampFile}.yaml`;
const histJsonName = `CatMata-sub-${timestampFile}.json`;
fs.writeFileSync(path.join(historyDir, histYamlName), fs.readFileSync(outYamlPath));
fs.writeFileSync(path.join(historyDir, histJsonName), fs.readFileSync(outJsonPath));
const historyJsonPath = path.join(historyDir, 'history.json');
let historyList = [];
if (fs.existsSync(historyJsonPath)) {
try {
historyList = JSON.parse(fs.readFileSync(historyJsonPath, 'utf-8'));
} catch (e) {
console.error("Failed to parse history.json, resetting:", e.message);
}
}
historyList.unshift({
timestamp: now.toISOString(),
timeStr: timeStr,
yamlFile: histYamlName,
jsonFile: histJsonName,
nodesCount: rawClashNodes.length
});
fs.writeFileSync(historyJsonPath, JSON.stringify(historyList, null, 2), 'utf-8');
return { yamlPath: outYamlPath, jsonPath: outJsonPath };
}
function openBrowser(url) {
let cmd;
if (process.platform === 'win32') {
cmd = `start "" "${url}"`;
} else if (process.platform === 'darwin') {
cmd = `open "${url}"`;
} else {
cmd = `xdg-open "${url}"`;
}
exec(cmd, err => {
if (err) console.error('Failed to open browser:', err);
});
}
function readJsonBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
try {
resolve(JSON.parse(body || '{}'));
} catch (e) {
reject(e);
}
});
req.on('error', err => reject(err));
});
}
// HTTP Server setup
function startServer() {
const server = http.createServer(async (req, res) => {
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
const pathname = parsedUrl.pathname;
// Static Index file
if (pathname === '/' || pathname === '/index.html') {
const htmlPath = path.join(__dirname, 'public', 'index.html');
if (fs.existsSync(htmlPath)) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(fs.readFileSync(htmlPath));
} else {
res.writeHead(404);
res.end('index.html not found');
}
return;
}
// API: GET /api/subs
if (pathname === '/api/subs' && req.method === 'GET') {
if (fs.existsSync(subsFile)) {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(fs.readFileSync(subsFile));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([]));
}
return;
}
// API: POST /api/subs
if (pathname === '/api/subs' && req.method === 'POST') {
try {
const payload = await readJsonBody(req);
fs.writeFileSync(subsFile, JSON.stringify(payload, null, 2), 'utf-8');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
} catch (e) {
res.writeHead(500);
res.end(e.message);
}
return;
}
// API: GET /api/dns
if (pathname === '/api/dns' && req.method === 'GET') {
try {
const { yamlTpl, jsonTpl } = detectTemplates();
const clashConfig = yaml.load(fs.readFileSync(yamlTpl, 'utf-8')) || {};
const sbConfig = JSON.parse(fs.readFileSync(jsonTpl, 'utf-8')) || {};
const clashDnsStr = yaml.dump(clashConfig.dns || {}, { noRefs: true, sortKeys: false });
const sbDnsStr = JSON.stringify(sbConfig.dns || {}, null, 2);
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ clash: clashDnsStr, singbox: sbDnsStr }));
} catch (e) {
res.writeHead(500);
res.end(e.message);
}
return;
}
// API: POST /api/fetch (Fetches and converts in-memory)
if (pathname === '/api/fetch' && req.method === 'POST') {
try {
if (!fs.existsSync(subsFile)) {
res.writeHead(400);
res.end('Subscription config subs.json is missing');
return;
}
const subs = JSON.parse(fs.readFileSync(subsFile, 'utf-8'));
const allClashNodes = [];
usedNames.clear();
for (const sub of subs) {
const subName = sub.name || 'Sub';
const subLink = sub.link;
const subType = (sub.type || 'clash').toLowerCase();
if (!subLink) continue;
const content = await loadSubscriptionContent(subLink);
if (!content) continue;
if (subType === 'clash') {
const nodes = parseClashNodes(content);
for (const node of nodes) {
const cNode = JSON.parse(JSON.stringify(node));
renameNode(cNode, subName, true);
allClashNodes.push(cNode);
}
} else if (subType === 'singbox') {
const nodes = parseSingboxNodes(content);
for (const node of nodes) {
const sbNode = JSON.parse(JSON.stringify(node));
renameNode(sbNode, subName, false);
const cNode = singboxToClash(sbNode);
allClashNodes.push(cNode);
}
}
}
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ proxies: allClashNodes }));
} catch (e) {
res.writeHead(500);
res.end(e.message);
}
return;
}
// API: POST /api/generate (Generates and writes configurations)
if (pathname === '/api/generate' && req.method === 'POST') {
try {
const { proxies: rawClashNodes, dns } = await readJsonBody(req);
if (!rawClashNodes || !Array.isArray(rawClashNodes) || rawClashNodes.length === 0) {
res.writeHead(400);
res.end('No proxies supplied');
return;
}
generateConfigs(rawClashNodes, dns);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
} catch (e) {
res.writeHead(500);
res.end(e.message);
}
return;
}
// API: GET /api/history
if (pathname === '/api/history' && req.method === 'GET') {
const historyJsonPath = path.join(outDir, 'history', 'history.json');
if (fs.existsSync(historyJsonPath)) {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(fs.readFileSync(historyJsonPath));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([]));
}
return;
}
// API: GET /api/download
if (pathname === '/api/download' && req.method === 'GET') {
const fileName = parsedUrl.searchParams.get('file');
if (!fileName) {
res.writeHead(400);
res.end('Missing file parameter');
return;
}
// Security check: prevent directory traversal
const safeRegex = /^CatMata-sub-\d{8}-\d{6}\.(yaml|json)$/;
if (!safeRegex.test(fileName)) {
res.writeHead(400);
res.end('Invalid file name');
return;
}
const filePath = path.join(outDir, 'history', fileName);
if (fs.existsSync(filePath)) {
const isYaml = fileName.endsWith('.yaml');
res.writeHead(200, {
'Content-Type': isYaml ? 'application/x-yaml' : 'application/json; charset=utf-8',
'Content-Disposition': `attachment; filename="${fileName}"`
});
res.end(fs.readFileSync(filePath));
} else {
res.writeHead(404);
res.end('File not found');
}
return;
}
res.writeHead(404);
res.end('Not Found');
});
server.listen(PORT, () => {
console.log(`CatMata Sub2Proxy 控制面板已启动!`);
console.log(`本地访问地址: http://localhost:${PORT}`);
console.log(`正在自动打开默认浏览器...`);
openBrowser(`http://localhost:${PORT}`);
});
}
// Legacy CLI Mode fallback
async function runCli() {
const detected = detectTemplates();
const yamlTpl = detected.yamlTpl;
const jsonTpl = detected.jsonTpl;
console.log(`CLI 编译模式:`);
console.log(`Clash 模板: ${yamlTpl}`);
console.log(`Sing-box 模板: ${jsonTpl}`);
if (!fs.existsSync(subsFile)) {
console.error(`Error: subs.json not found.`);
process.exit(1);
}
const subs = JSON.parse(fs.readFileSync(subsFile, 'utf-8'));
const allClashNodes = [];
usedNames.clear();
for (const sub of subs) {
const subName = sub.name || 'Sub';
const subLink = sub.link;
const subType = (sub.type || 'clash').toLowerCase();
if (!subLink) continue;
const content = await loadSubscriptionContent(subLink);
if (!content) continue;
if (subType === 'clash') {
const nodes = parseClashNodes(content);
for (const node of nodes) {
const cNode = JSON.parse(JSON.stringify(node));
const newName = renameNode(cNode, subName, true);
allClashNodes.push(cNode);
}
} else if (subType === 'singbox') {
const nodes = parseSingboxNodes(content);
for (const node of nodes) {
const sbNode = JSON.parse(JSON.stringify(node));
const newName = renameNode(sbNode, subName, false);
const cNode = singboxToClash(sbNode);
allClashNodes.push(cNode);
}
}
}
if (allClashNodes.length === 0) {
console.error("未拉取到任何代理节点。");
process.exit(1);
}
try {
const result = generateConfigs(allClashNodes);
console.log(`生成 Clash 配置成功: ${result.yamlPath}`);
console.log(`生成 Sing-box 配置成功: ${result.jsonPath}`);
} catch (e) {
console.error("生成配置失败:", e.message);
}
}
// Start Main Routine
const args = process.argv.slice(2);
if (args.includes('--cli') || args.includes('-c')) {
runCli();
} else {
startServer();
}