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>刷新历史
|
<i data-lucide="refresh-cw"></i>刷新历史
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div class="table-container">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -898,6 +935,7 @@
|
|||||||
window.addEventListener('DOMContentLoaded', async () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
await loadAppConfig();
|
await loadAppConfig();
|
||||||
|
updateLatestUrls();
|
||||||
loadSubscriptions();
|
loadSubscriptions();
|
||||||
loadDnsTemplates();
|
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) {
|
function renderHistoryList(list) {
|
||||||
const tbody = document.getElementById('history-list');
|
const tbody = document.getElementById('history-list');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
|
|||||||
@@ -262,6 +262,32 @@
|
|||||||
<div class="muted" id="summary">正在加载...</div>
|
<div class="muted" id="summary">正在加载...</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<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() {
|
async function loadAppConfig() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/app-config', { credentials: 'same-origin' });
|
const res = await fetch('/api/app-config', { credentials: 'same-origin' });
|
||||||
@@ -469,6 +524,7 @@
|
|||||||
|
|
||||||
(async function init() {
|
(async function init() {
|
||||||
await loadAppConfig();
|
await loadAppConfig();
|
||||||
|
updateLatestUrls();
|
||||||
if (await checkSession()) {
|
if (await checkSession()) {
|
||||||
showHistory();
|
showHistory();
|
||||||
await loadHistory();
|
await loadHistory();
|
||||||
|
|||||||
@@ -707,6 +707,64 @@ function startServer() {
|
|||||||
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
||||||
const pathname = parsedUrl.pathname;
|
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') {
|
if (pathname === '/login' || pathname === '/login.html') {
|
||||||
serveHtml(res, 'login.html');
|
serveHtml(res, 'login.html');
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user