feat: add /sub/clash/latest and /sub/singbox/latest public subscription APIs and display them on the web dashboard UI
This commit is contained in:
@@ -804,6 +804,43 @@
|
||||
<i data-lucide="refresh-cw"></i>刷新历史
|
||||
</button>
|
||||
</div>
|
||||
<!-- Latest Subscription Links -->
|
||||
<div style="margin-bottom: 1.5rem; background: rgba(255, 255, 255, 0.02); border: 1px solid var(--border-color); border-radius: 12px; padding: 1.25rem; display: flex; flex-direction: column; gap: 1rem;">
|
||||
<div style="font-size: 0.95rem; font-weight: 600; color: var(--text-primary); display: flex; align-items: center; gap: 0.5rem;">
|
||||
<i data-lucide="link" style="width: 16px; height: 16px; color: var(--accent-purple);"></i>最新订阅地址 (无Token/持久链接)
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem;">
|
||||
<div style="flex: 1; min-width: 280px; display: flex; align-items: center; justify-content: space-between; background: rgba(255, 255, 255, 0.01); border: 1px solid var(--border-color); border-radius: 8px; padding: 0.75rem 1rem;">
|
||||
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
||||
<span style="font-size: 0.75rem; font-weight: 600; color: #a78bfa; text-transform: uppercase;">Clash (YAML)</span>
|
||||
<span style="font-size: 0.85rem; font-family: var(--font-mono); color: var(--text-secondary); word-break: break-all;" id="latest-clash-link">加载中...</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button type="button" class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;" onclick="copyLatestLink('clash')">
|
||||
<i data-lucide="copy" style="width: 12px; height: 12px;"></i>复制
|
||||
</button>
|
||||
<a class="btn btn-primary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem; text-decoration: none; background: linear-gradient(135deg, #10B981 0%, #059669 100%);" id="latest-clash-import" href="#">
|
||||
<i data-lucide="zap" style="width: 12px; height: 12px;"></i>导入 Clash
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1; min-width: 280px; display: flex; align-items: center; justify-content: space-between; background: rgba(255, 255, 255, 0.01); border: 1px solid var(--border-color); border-radius: 8px; padding: 0.75rem 1rem;">
|
||||
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
||||
<span style="font-size: 0.75rem; font-weight: 600; color: #f472b6; text-transform: uppercase;">Sing-box (JSON)</span>
|
||||
<span style="font-size: 0.85rem; font-family: var(--font-mono); color: var(--text-secondary); word-break: break-all;" id="latest-singbox-link">加载中...</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button type="button" class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;" onclick="copyLatestLink('singbox')">
|
||||
<i data-lucide="copy" style="width: 12px; height: 12px;"></i>复制
|
||||
</button>
|
||||
<a class="btn btn-primary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem; text-decoration: none; background: linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);" id="latest-singbox-import" href="#">
|
||||
<i data-lucide="zap" style="width: 12px; height: 12px;"></i>导入 Sing-box
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -898,6 +935,7 @@
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
lucide.createIcons();
|
||||
await loadAppConfig();
|
||||
updateLatestUrls();
|
||||
loadSubscriptions();
|
||||
loadDnsTemplates();
|
||||
});
|
||||
@@ -1512,6 +1550,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
let latestClashUrl = '';
|
||||
let latestSingboxUrl = '';
|
||||
|
||||
function updateLatestUrls() {
|
||||
const clientOrigin = getClientImportOrigin();
|
||||
latestClashUrl = `${clientOrigin}/sub/clash/latest`;
|
||||
latestSingboxUrl = `${clientOrigin}/sub/singbox/latest`;
|
||||
|
||||
const clashLinkEl = document.getElementById('latest-clash-link');
|
||||
const singboxLinkEl = document.getElementById('latest-singbox-link');
|
||||
if (clashLinkEl) clashLinkEl.textContent = latestClashUrl;
|
||||
if (singboxLinkEl) singboxLinkEl.textContent = latestSingboxUrl;
|
||||
|
||||
const clashImportEl = document.getElementById('latest-clash-import');
|
||||
const singboxImportEl = document.getElementById('latest-singbox-import');
|
||||
if (clashImportEl) {
|
||||
clashImportEl.href = `clash://install-config?url=${encodeURIComponent(latestClashUrl)}&name=${encodeURIComponent('CatMata-Latest')}`;
|
||||
}
|
||||
if (singboxImportEl) {
|
||||
singboxImportEl.href = `sing-box://import-remote-profile?url=${encodeURIComponent(latestSingboxUrl)}#${encodeURIComponent('CatMata-Latest')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function copyLatestLink(type) {
|
||||
const link = type === 'clash' ? latestClashUrl : latestSingboxUrl;
|
||||
copyLinkToClipboard(link);
|
||||
}
|
||||
|
||||
|
||||
function renderHistoryList(list) {
|
||||
const tbody = document.getElementById('history-list');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
@@ -262,6 +262,32 @@
|
||||
<div class="muted" id="summary">正在加载...</div>
|
||||
</div>
|
||||
</header>
|
||||
<div style="padding: 20px 24px; border-bottom: 1px solid var(--line); background: #fafbfc; display: flex; flex-direction: column; gap: 12px;">
|
||||
<h3 style="margin: 0; font-size: 0.95rem; font-weight: 700; color: #374151;">最新订阅地址 (无Token/持久链接)</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 16px;">
|
||||
<div style="flex: 1; min-width: 280px; display: flex; align-items: center; justify-content: space-between; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 10px 14px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<span style="font-size: 0.78rem; font-weight: 750; color: var(--primary); text-transform: uppercase;">Clash (YAML)</span>
|
||||
<span style="font-size: 0.85rem; font-family: monospace; color: var(--muted); word-break: break-all;" id="latest-clash-link">加载中...</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<button type="button" class="btn btn-secondary" onclick="copyLatestLink('clash')">复制</button>
|
||||
<a class="btn btn-green" id="latest-clash-import" href="#">导入 Clash</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1; min-width: 280px; display: flex; align-items: center; justify-content: space-between; background: #fff; border: 1px solid var(--line); border-radius: 6px; padding: 10px 14px;">
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<span style="font-size: 0.78rem; font-weight: 750; color: var(--primary); text-transform: uppercase;">Sing-box (JSON)</span>
|
||||
<span style="font-size: 0.85rem; font-family: monospace; color: var(--muted); word-break: break-all;" id="latest-singbox-link">加载中...</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<button type="button" class="btn btn-secondary" onclick="copyLatestLink('singbox')">复制</button>
|
||||
<a class="btn btn-primary" id="latest-singbox-import" href="#">导入 Sing-box</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -339,6 +365,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
let latestClashUrl = '';
|
||||
let latestSingboxUrl = '';
|
||||
|
||||
function updateLatestUrls() {
|
||||
const clientOrigin = getClientImportOrigin();
|
||||
latestClashUrl = `${clientOrigin}/sub/clash/latest`;
|
||||
latestSingboxUrl = `${clientOrigin}/sub/singbox/latest`;
|
||||
|
||||
const clashLinkEl = document.getElementById('latest-clash-link');
|
||||
const singboxLinkEl = document.getElementById('latest-singbox-link');
|
||||
if (clashLinkEl) clashLinkEl.textContent = latestClashUrl;
|
||||
if (singboxLinkEl) singboxLinkEl.textContent = latestSingboxUrl;
|
||||
|
||||
const clashImportEl = document.getElementById('latest-clash-import');
|
||||
const singboxImportEl = document.getElementById('latest-singbox-import');
|
||||
if (clashImportEl) {
|
||||
clashImportEl.href = `clash://install-config?url=${encodeURIComponent(latestClashUrl)}&name=${encodeURIComponent('CatMata-Latest')}`;
|
||||
}
|
||||
if (singboxImportEl) {
|
||||
singboxImportEl.href = `sing-box://import-remote-profile?url=${encodeURIComponent(latestSingboxUrl)}#${encodeURIComponent('CatMata-Latest')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function copyLatestLink(type) {
|
||||
const link = type === 'clash' ? latestClashUrl : latestSingboxUrl;
|
||||
copyLinkToClipboard(link);
|
||||
}
|
||||
|
||||
|
||||
async function loadAppConfig() {
|
||||
try {
|
||||
const res = await fetch('/api/app-config', { credentials: 'same-origin' });
|
||||
@@ -469,6 +524,7 @@
|
||||
|
||||
(async function init() {
|
||||
await loadAppConfig();
|
||||
updateLatestUrls();
|
||||
if (await checkSession()) {
|
||||
showHistory();
|
||||
await loadHistory();
|
||||
|
||||
@@ -707,6 +707,64 @@ function startServer() {
|
||||
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
||||
const pathname = parsedUrl.pathname;
|
||||
|
||||
if (pathname === '/sub/clash/latest' && req.method === 'GET') {
|
||||
const filePath = path.join(outDir, 'CatMata-sub.yaml');
|
||||
if (fs.existsSync(filePath)) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/yaml; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.end(fs.readFileSync(filePath));
|
||||
} else {
|
||||
const history = getHistoryList();
|
||||
if (history.length > 0 && history[0].yamlFile) {
|
||||
const histPath = path.join(outDir, 'history', history[0].yamlFile);
|
||||
if (fs.existsSync(histPath)) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/yaml; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.end(fs.readFileSync(histPath));
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end('Latest Clash profile not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/sub/singbox/latest' && req.method === 'GET') {
|
||||
const filePath = path.join(outDir, 'CatMata-sub.json');
|
||||
if (fs.existsSync(filePath)) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.end(fs.readFileSync(filePath));
|
||||
} else {
|
||||
const history = getHistoryList();
|
||||
if (history.length > 0 && history[0].jsonFile) {
|
||||
const histPath = path.join(outDir, 'history', history[0].jsonFile);
|
||||
if (fs.existsSync(histPath)) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
});
|
||||
res.end(fs.readFileSync(histPath));
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end('Latest Sing-box profile not found');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/login' || pathname === '/login.html') {
|
||||
serveHtml(res, 'login.html');
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user