feat: init proj
This commit is contained in:
167
server/src/services/generator.ts
Normal file
167
server/src/services/generator.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import db from '../db.js';
|
||||
|
||||
export function generateSurgeConfig(hostUrl: string): 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 enabled fetched nodes
|
||||
const fetchedNodes = db.prepare(
|
||||
'SELECT surge_line FROM fetched_nodes WHERE enabled = 1 ORDER BY subscription_id, id'
|
||||
).all() as any[];
|
||||
|
||||
// Collect enabled static nodes (these go FIRST)
|
||||
const staticNodes = db.prepare(
|
||||
'SELECT surge_line FROM static_nodes WHERE enabled = 1 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 staticLines = staticNodes.map((n: any) => n.surge_line);
|
||||
const fetchedLines = fetchedNodes.map((n: any) => n.surge_line);
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user