import db from '../db.js'; /** * @param whitelist When provided (guest links), only nodes whose name is in the * set are included, independent of their global enabled state. When omitted, * the normal enabled-only behavior applies. */ export function generateSurgeConfig(hostUrl: string, whitelist?: Set): string { // Get first enabled subscription's raw_config as base template const sub = db.prepare( 'SELECT raw_config FROM subscriptions WHERE enabled = 1 AND raw_config IS NOT NULL ORDER BY id LIMIT 1' ).get() as any; if (!sub?.raw_config) { return '# No subscription config available. Add and fetch a subscription first.'; } // Collect fetched nodes (exclude vless — Surge doesn't support it; tuic uses `tuic-v5`) const fetchedNodes = db.prepare( whitelist ? "SELECT surge_line FROM fetched_nodes WHERE type != 'vless' ORDER BY subscription_id, sort_order, id" : "SELECT surge_line FROM fetched_nodes WHERE enabled = 1 AND type != 'vless' ORDER BY subscription_id, sort_order, id" ).all() as any[]; // Collect static nodes (these go FIRST, exclude vless) const staticNodes = db.prepare( whitelist ? "SELECT surge_line FROM static_nodes WHERE type != 'vless' ORDER BY sort_order, id" : "SELECT surge_line FROM static_nodes WHERE enabled = 1 AND type != 'vless' ORDER BY sort_order, id" ).all() as any[]; // Collect enabled rules const userRules = db.prepare( 'SELECT type, value, action, comment FROM rules WHERE enabled = 1 ORDER BY sort_order, id' ).all() as any[]; // Static nodes first, then fetched nodes const lineName = (l: string) => l.split(' = ')[0].trim(); const keep = (l: string) => !whitelist || whitelist.has(lineName(l)); const staticLines = staticNodes.map((n: any) => n.surge_line).filter(keep); const fetchedLines = fetchedNodes.map((n: any) => n.surge_line).filter(keep); const allNodeLines = [...staticLines, ...fetchedLines]; const allNodeNames = allNodeLines.map((l: string) => l.split(' = ')[0].trim()); // Build rule lines const ruleLines = userRules.map((r: any) => { const line = `${r.type},${r.value},${r.action}`; return r.comment ? `${line} // ${r.comment}` : line; }); let config = sub.raw_config; // Replace [Proxy] section with only enabled nodes config = rebuildProxySection(config, staticLines, fetchedLines); // Rebuild [Proxy Group] select groups with only enabled node names config = rebuildProxyGroup(config, allNodeNames); // Inject user rules at the beginning of [Rule] section if (ruleLines.length > 0) { config = injectRules(config, ruleLines); } // Rewrite MANAGED-CONFIG URL config = config.replace( /^#!MANAGED-CONFIG\s+\S+/m, `#!MANAGED-CONFIG ${hostUrl}` ); return config; } /** * Replace the entire [Proxy] section content with only enabled nodes. * Static nodes go first, then fetched nodes. */ function rebuildProxySection(config: string, staticLines: string[], fetchedLines: string[]): string { const lines = config.split('\n'); const result: string[] = []; let inProxySection = false; let proxyHeaderEmitted = false; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('[') && trimmed.endsWith(']')) { if (inProxySection && !proxyHeaderEmitted) { // Emit our rebuilt proxy content before leaving the section emitProxyContent(result, staticLines, fetchedLines); proxyHeaderEmitted = true; } inProxySection = trimmed === '[Proxy]'; result.push(line); if (inProxySection) { // Emit all enabled nodes right after [Proxy] header emitProxyContent(result, staticLines, fetchedLines); proxyHeaderEmitted = true; } continue; } if (inProxySection) { // Skip original proxy lines (we replaced them) continue; } result.push(line); } // If [Proxy] was the last section if (inProxySection && !proxyHeaderEmitted) { emitProxyContent(result, staticLines, fetchedLines); } return result.join('\n'); } function emitProxyContent(result: string[], staticLines: string[], fetchedLines: string[]) { if (staticLines.length > 0) { result.push('# --- 自定义节点 ---'); staticLines.forEach(l => result.push(l)); result.push(''); } if (fetchedLines.length > 0) { result.push('# --- 订阅节点 ---'); fetchedLines.forEach(l => result.push(l)); result.push(''); } } /** * Rebuild [Proxy Group] select groups to contain only the enabled node names. */ function rebuildProxyGroup(config: string, allNodeNames: string[]): string { if (allNodeNames.length === 0) return config; const lines = config.split('\n'); const result: string[] = []; let inProxyGroupSection = false; let handled = false; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('[') && trimmed.endsWith(']')) { inProxyGroupSection = trimmed === '[Proxy Group]'; } if (inProxyGroupSection && !handled && trimmed.includes('= select,')) { // Rebuild: keep group name and "= select," prefix, replace node list const eqSelect = trimmed.indexOf('= select,'); const prefix = trimmed.slice(0, eqSelect + '= select,'.length); result.push(prefix + ' ' + allNodeNames.join(', ')); handled = true; continue; } result.push(line); } return result.join('\n'); } function injectRules(config: string, ruleLines: string[]): string { const lines = config.split('\n'); const result: string[] = []; for (const line of lines) { result.push(line); if (line.trim() === '[Rule]') { result.push('# --- 自定义规则 ---'); ruleLines.forEach(r => result.push(r)); result.push(''); } } return result.join('\n'); }