feat: support NAS imports and proxy names

This commit is contained in:
jocayn
2026-06-13 16:19:52 +08:00
parent ec6b3cb93f
commit b231e78498
6 changed files with 212 additions and 47 deletions
+71 -7
View File
@@ -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}`);
}