feat: init proj

This commit is contained in:
2026-03-31 13:11:54 +08:00
commit 8f75ea24d6
38 changed files with 6826 additions and 0 deletions

View 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');
}