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
+79 -19
View File
@@ -710,7 +710,8 @@
<table>
<thead>
<tr>
<th> (Tag)</th>
<th>解析名 (Tag)</th>
<th>代理名</th>
<th>协议 (Type)</th>
<th>服务器 (Server)</th>
<th>端口 (Port)</th>
@@ -719,7 +720,7 @@
</thead>
<tbody id="proxies-list">
<tr>
<td colspan="5" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
请先点击右上角 “一键拉取” 按钮解析节点
</td>
</tr>
@@ -857,14 +858,28 @@
let subscriptions = [];
let proxies = []; // In-memory editable proxies
let originalDns = { clash: '', singbox: '' };
let appConfig = { publicBaseUrl: '' };
// On Load
window.addEventListener('DOMContentLoaded', () => {
window.addEventListener('DOMContentLoaded', async () => {
lucide.createIcons();
await loadAppConfig();
loadSubscriptions();
loadDnsTemplates();
});
async function loadAppConfig() {
try {
const res = await fetch('/api/app-config');
if (res.ok) {
const data = await res.json();
appConfig.publicBaseUrl = (data.publicBaseUrl || '').replace(/\/+$/, '');
}
} catch (e) {
console.warn('Failed to load app config:', e);
}
}
// Logging helper
function log(message, type = 'info') {
const logs = document.getElementById('console-logs');
@@ -1094,6 +1109,24 @@
}
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, ch => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[ch]);
}
function getParsedName(node) {
return node.name || node.tag || '';
}
function getProxyName(node) {
return node.proxyName || node.originalName || getParsedName(node);
}
function renderProxiesTable() {
const list = document.getElementById('proxies-list');
const counter = document.getElementById('proxies-counter');
@@ -1104,7 +1137,7 @@
if (proxies.length === 0) {
list.innerHTML = `
<tr>
<td colspan="5" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
未提取到任何代理节点
</td>
</tr>
@@ -1114,11 +1147,17 @@
proxies.forEach((node, idx) => {
const tr = document.createElement('tr');
const parsedName = getParsedName(node);
const proxyName = getProxyName(node);
const nodeType = node.type || '';
const server = node.server || '';
const port = node.port || node.server_port || '';
tr.innerHTML = `
<td style="font-weight: 500;">${node.name || node.tag}</td>
<td><span class="node-proto proto-${(node.type || '').toLowerCase()}">${node.type}</span></td>
<td style="font-family: var(--font-mono); font-size: 0.85rem;">${node.server}</td>
<td style="font-family: var(--font-mono); font-size: 0.85rem;">${node.port || node.server_port}</td>
<td style="font-weight: 500;" title="${escapeHtml(parsedName)}">${escapeHtml(parsedName)}</td>
<td title="${escapeHtml(proxyName)}">${escapeHtml(proxyName)}</td>
<td><span class="node-proto proto-${escapeHtml(nodeType.toLowerCase())}">${escapeHtml(nodeType)}</span></td>
<td style="font-family: var(--font-mono); font-size: 0.85rem;">${escapeHtml(server)}</td>
<td style="font-family: var(--font-mono); font-size: 0.85rem;">${escapeHtml(port)}</td>
<td>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="openNodeDrawer(${idx})">
@@ -1136,7 +1175,7 @@
}
function deleteProxy(idx) {
const nodeName = proxies[idx].name || proxies[idx].tag;
const nodeName = getParsedName(proxies[idx]);
if (confirm(`确定要删除代理节点 "${nodeName}" 吗?\n(注:这仅会从当前页面内存中删除。如需应用更改,请在删除后点击右上角“保存并生成配置”按钮重新生成配置文件)`)) {
log(`在内存中删除代理节点: ${nodeName}`);
proxies.splice(idx, 1);
@@ -1155,17 +1194,18 @@
const container = document.getElementById('node-edit-form');
const overlay = document.getElementById('node-drawer-overlay');
title.textContent = `修改节点: ${node.name || node.tag}`;
title.textContent = `修改节点: ${getParsedName(node)}`;
container.innerHTML = '';
const commonFields = [
{ name: 'name', label: '节点 Tag/Name', val: node.name || node.tag, type: 'text' },
{ name: 'name', label: '解析名 (Tag)', val: getParsedName(node), type: 'text' },
{ name: 'proxyName', label: '代理名', val: getProxyName(node), type: 'text', readonly: true },
{ name: 'server', label: '服务器 IP/域名', val: node.server, type: 'text' },
{ name: 'port', label: '端口', val: node.port || node.server_port, type: 'number' }
];
commonFields.forEach(f => {
container.appendChild(createFormField(f.name, f.label, f.val, f.type));
container.appendChild(createFormField(f.name, f.label, f.val, f.type, { readonly: f.readonly }));
});
const t = (node.type || '').toLowerCase();
@@ -1206,12 +1246,13 @@
overlay.classList.add('active');
}
function createFormField(name, label, value, type) {
function createFormField(name, label, value, type, options = {}) {
const div = document.createElement('div');
div.className = 'form-group';
const readonlyAttr = options.readonly ? ' readonly aria-readonly="true"' : '';
div.innerHTML = `
<label for="edit-${name}">${label}</label>
<input type="${type}" id="edit-${name}" class="form-control" value="${value !== undefined ? value : ''}">
<label for="edit-${escapeHtml(name)}">${escapeHtml(label)}</label>
<input type="${escapeHtml(type)}" id="edit-${escapeHtml(name)}" class="form-control" value="${escapeHtml(value !== undefined ? value : '')}"${readonlyAttr}>
`;
return div;
}
@@ -1402,6 +1443,20 @@
}
}
function getClientImportOrigin() {
if (appConfig.publicBaseUrl) {
return appConfig.publicBaseUrl;
}
const localHosts = new Set(['localhost', '127.0.0.1', '::1']);
if (!localHosts.has(window.location.hostname)) {
return window.location.origin;
}
const port = window.location.port ? `:${window.location.port}` : '';
return `${window.location.protocol}//127.0.0.1${port}`;
}
function renderHistoryList(list) {
const tbody = document.getElementById('history-list');
tbody.innerHTML = '';
@@ -1418,14 +1473,19 @@
}
const origin = window.location.origin;
const clientOrigin = getClientImportOrigin();
list.forEach(item => {
const yamlDownloadUrl = `${origin}/api/download?file=${item.yamlFile}`;
const jsonDownloadUrl = `${origin}/api/download?file=${item.jsonFile}`;
const yamlFileParam = encodeURIComponent(item.yamlFile);
const jsonFileParam = encodeURIComponent(item.jsonFile);
const yamlDownloadUrl = `${origin}/api/download?file=${yamlFileParam}`;
const jsonDownloadUrl = `${origin}/api/download?file=${jsonFileParam}`;
const yamlImportProfileUrl = `${clientOrigin}/api/profile?file=${yamlFileParam}`;
const jsonImportProfileUrl = `${clientOrigin}/api/profile?file=${jsonFileParam}`;
// Construct client URI schemes
const clashImportUrl = `clash://install-config?url=${encodeURIComponent(yamlDownloadUrl)}&name=${encodeURIComponent('CatMata-' + item.timeStr.replace(/[: ]/g, '-'))}`;
const singboxImportUrl = `sing-box://import-remote-profile?url=${encodeURIComponent(jsonDownloadUrl)}#${encodeURIComponent('CatMata-' + item.timeStr.replace(/[: ]/g, '-'))}`;
const clashImportUrl = `clash://install-config?url=${encodeURIComponent(yamlImportProfileUrl)}&name=${encodeURIComponent('CatMata-' + item.timeStr.replace(/[: ]/g, '-'))}`;
const singboxImportUrl = `sing-box://import-remote-profile?url=${encodeURIComponent(jsonImportProfileUrl)}#${encodeURIComponent('CatMata-' + item.timeStr.replace(/[: ]/g, '-'))}`;
const tr = document.createElement('tr');
tr.innerHTML = `