feat: browser-side subscription fetch (bypass CORS with fallback)

This commit is contained in:
2026-06-14 16:57:49 +08:00
parent e93f15f670
commit 37e52cf186
3 changed files with 89 additions and 8 deletions
+41 -8
View File
@@ -431,22 +431,55 @@
if (!url) return toast('请输入订阅链接', 'error');
try {
toast('正在获取订阅...', 'info');
// First fetch and validate
const res = await api('POST', '/api/subscription/import', { url });
if (res.error) throw new Error(res.error);
toast('订阅获取成功,正在应用...', 'info');
toast('正在从浏览器获取订阅...', 'info');
// Then apply to mihomo
const applyRes = await api('POST', '/api/subscription/apply', { url });
// Fetch subscription from browser side (avoids server network issues)
const resp = await fetch(url, {
headers: { 'User-Agent': 'ClashForAndroid/2.5.12' },
mode: 'cors',
});
if (!resp.ok) throw new Error('HTTP ' + resp.status);
let content = await resp.text();
// Try base64 decode
try {
const decoded = atob(content.trim());
if (decoded.includes('://') || decoded.includes('proxies')) content = decoded;
} catch {}
if (!content.includes('proxies') && !content.includes('port:')) {
throw new Error('无效的订阅内容');
}
toast('订阅获取成功,正在推送到 mihomo...', 'info');
// Send content to mihomo via our server API
const applyRes = await api('POST', '/api/subscription/push', { content, url });
if (applyRes.error) throw new Error(applyRes.error);
toast('🎉 订阅导入并应用成功!', 'success');
document.getElementById('sub-url').value = '';
document.getElementById('sub-name').value = '';
loadSubscriptions();
loadDashboard();
} catch(e) {
toast('导入失败: ' + e.message, 'error');
// CORS blocked - fallback to server-side fetch
if (e.message.includes('Failed to fetch') || e.message.includes('NetworkError')) {
toast('浏览器无法直接获取(CORS),改用服务器获取...', 'info');
try {
const res = await api('POST', '/api/subscription/import', { url });
if (res.error) throw new Error(res.error);
const applyRes = await api('POST', '/api/subscription/apply', { url });
if (applyRes.error) throw new Error(applyRes.error);
toast('🎉 订阅导入成功!', 'success');
document.getElementById('sub-url').value = '';
loadSubscriptions();
} catch(e2) {
toast('导入失败: ' + e2.message, 'error');
}
} else {
toast('导入失败: ' + e.message, 'error');
}
}
}
+41
View File
@@ -355,6 +355,47 @@ async function handleApi(req, res, apiPath) {
}
}
// POST /api/subscription/push - Push subscription content directly to mihomo
if (apiPath === '/api/subscription/push' && method === 'POST') {
const body = JSON.parse(await readBody(req));
const { content, url } = body;
if (!content) return sendError(res, 'Missing content', 400);
try {
// Write config to mihomo directory
const configDir = path.dirname(CONFIG.mihomoConfig);
const subConfigPath = path.join(configDir, 'sub-config.yaml');
fs.writeFileSync(subConfigPath, content, 'utf-8');
// Inject our settings
let configContent = content;
configContent = configContent.replace(/external-controller:\s*'[^']*'/, "external-controller: '0.0.0.0:9097'");
configContent = configContent.replace(/secret:\s*"[^"]*"/, 'secret: "orangepi"');
fs.writeFileSync(subConfigPath, configContent, 'utf-8');
// Reload mihomo with new config
await mihomoApi('PUT', '/configs', { path: subConfigPath });
// Save subscription URL if provided
if (url) {
const subFile = path.join(configDir, 'subscriptions.json');
let subscriptions = [];
try { subscriptions = JSON.parse(fs.readFileSync(subFile, 'utf-8')); } catch {}
const existing = subscriptions.find(s => s.url === url);
if (existing) {
existing.updatedAt = new Date().toISOString();
} else {
subscriptions.push({ url, addedAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
}
fs.writeFileSync(subFile, JSON.stringify(subscriptions, null, 2));
}
return sendJson(res, { ok: true, message: 'Subscription pushed and applied' });
} catch (e) {
return sendError(res, 'Failed to push subscription: ' + e.message);
}
}
// GET /api/subscriptions - List saved subscriptions
if (apiPath === '/api/subscriptions' && method === 'GET') {
const subFile = '/etc/mihomo/subscriptions.json';
+7
View File
@@ -0,0 +1,7 @@
Set-Location "f:\Orangepi\Orange Pi RV\OrangePi-Mihomo-UI"
$env:MIHOMO_API = "http://192.168.10.1:9097"
$env:MIHOMO_SECRET = "orangepi"
$env:MIHOMO_CONFIG = ""
$env:YACD_PATH = "f:\Orangepi\Orange Pi RV\OrangePi-Mihomo-UI\yacd-meta"
$env:PORT = "8899"
node server.js