feat: support NAS imports and proxy names
This commit is contained in:
+71
-7
@@ -314,6 +314,7 @@ function parseSingboxNodes(content) {
|
||||
const usedNames = new Set();
|
||||
|
||||
function renameNode(node, subName, isClash = true) {
|
||||
const originalName = isClash ? (node.name || node.tag || '') : (node.tag || node.name || '');
|
||||
const typeStr = (node.type || '').toLowerCase();
|
||||
const protoClean = getProtoName(typeStr);
|
||||
const baseName = `${subName}-${protoClean}`;
|
||||
@@ -326,6 +327,7 @@ function renameNode(node, subName, isClash = true) {
|
||||
}
|
||||
|
||||
usedNames.add(newName.toLowerCase());
|
||||
node.proxyName = originalName || newName;
|
||||
|
||||
if (isClash) {
|
||||
node.name = newName;
|
||||
@@ -380,6 +382,12 @@ function detectTemplates() {
|
||||
return { yamlTpl, jsonTpl };
|
||||
}
|
||||
|
||||
function stripUiOnlyNodeFields(node) {
|
||||
const cleanNode = JSON.parse(JSON.stringify(node));
|
||||
delete cleanNode.proxyName;
|
||||
return cleanNode;
|
||||
}
|
||||
|
||||
function replaceNodeRefs(data, oldNodesSet, fallbackNode) {
|
||||
if (data && typeof data === 'object') {
|
||||
if (Array.isArray(data)) {
|
||||
@@ -403,7 +411,10 @@ function replaceNodeRefs(data, oldNodesSet, fallbackNode) {
|
||||
// Configuration paths
|
||||
const subsFile = 'subs.json';
|
||||
const outDir = 'out';
|
||||
const PORT = 3000;
|
||||
const PORT = Number.parseInt(process.env.PORT || '3000', 10);
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
const PUBLIC_BASE_URL = String(process.env.PUBLIC_BASE_URL || '').trim().replace(/\/+$/, '');
|
||||
const AUTO_OPEN = !['0', 'false', 'no', 'off'].includes(String(process.env.AUTO_OPEN || '1').toLowerCase());
|
||||
|
||||
// Central configuration generation function
|
||||
function generateConfigs(rawClashNodes, dns = null) {
|
||||
@@ -411,10 +422,11 @@ function generateConfigs(rawClashNodes, dns = null) {
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
const clashNodes = rawClashNodes.map(stripUiOnlyNodeFields);
|
||||
|
||||
// 1. Process Clash Config
|
||||
const clashConfig = yaml.load(fs.readFileSync(yamlTpl, 'utf-8')) || {};
|
||||
clashConfig.proxies = rawClashNodes;
|
||||
clashConfig.proxies = clashNodes;
|
||||
|
||||
if (dns && dns.clash) {
|
||||
clashConfig.dns = yaml.load(dns.clash);
|
||||
@@ -422,7 +434,7 @@ function generateConfigs(rawClashNodes, dns = null) {
|
||||
|
||||
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);
|
||||
const newNodeNames = clashNodes.map(n => n.name);
|
||||
|
||||
for (const group of (clashConfig['proxy-groups'] || [])) {
|
||||
const oldProxies = group.proxies || [];
|
||||
@@ -438,7 +450,7 @@ function generateConfigs(rawClashNodes, dns = null) {
|
||||
|
||||
// 2. Process Sing-box Config
|
||||
const sbConfig = JSON.parse(fs.readFileSync(jsonTpl, 'utf-8')) || {};
|
||||
const allSingboxNodes = rawClashNodes.map(n => {
|
||||
const allSingboxNodes = clashNodes.map(n => {
|
||||
const nodeCopy = JSON.parse(JSON.stringify(n));
|
||||
return clashToSingbox(nodeCopy);
|
||||
});
|
||||
@@ -571,6 +583,20 @@ function startServer() {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: GET /api/health
|
||||
if (pathname === '/api/health' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: GET /api/app-config
|
||||
if (pathname === '/api/app-config' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ publicBaseUrl: PUBLIC_BASE_URL }));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: GET /api/subs
|
||||
if (pathname === '/api/subs' && req.method === 'GET') {
|
||||
if (fs.existsSync(subsFile)) {
|
||||
@@ -653,6 +679,7 @@ function startServer() {
|
||||
renameNode(sbNode, subName, false);
|
||||
|
||||
const cNode = singboxToClash(sbNode);
|
||||
cNode.proxyName = sbNode.proxyName || cNode.name;
|
||||
allClashNodes.push(cNode);
|
||||
}
|
||||
}
|
||||
@@ -733,6 +760,39 @@ function startServer() {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: GET /api/profile
|
||||
// Remote import clients should receive the profile body directly, not as a browser attachment.
|
||||
if (pathname === '/api/profile' && req.method === 'GET') {
|
||||
const fileName = parsedUrl.searchParams.get('file');
|
||||
if (!fileName) {
|
||||
res.writeHead(400);
|
||||
res.end('Missing file parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
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 ? 'text/yaml; charset=utf-8' : 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.end(fs.readFileSync(filePath));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('File not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// API: POST /api/history/delete
|
||||
if (pathname === '/api/history/delete' && req.method === 'POST') {
|
||||
try {
|
||||
@@ -791,10 +851,14 @@ function startServer() {
|
||||
res.end('Not Found');
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
server.listen(PORT, HOST, () => {
|
||||
const displayHost = HOST === '0.0.0.0' || HOST === '::' ? 'localhost' : HOST;
|
||||
console.log(`CatMata Sub2Proxy 控制面板已启动!`);
|
||||
console.log(`本地访问地址: http://localhost:${PORT}`);
|
||||
if (process.env.HEADLESS !== 'true') {
|
||||
console.log(`访问地址: http://${displayHost}:${PORT}`);
|
||||
if (PUBLIC_BASE_URL) {
|
||||
console.log(`客户端导入地址基准: ${PUBLIC_BASE_URL}`);
|
||||
}
|
||||
if (AUTO_OPEN) {
|
||||
console.log(`正在自动打开默认浏览器...`);
|
||||
openBrowser(`http://localhost:${PORT}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user