游客链接:可建多个带独立 token 的订阅,按节点名勾选白名单 (抗重新抓取),/surge|/clash|/ssr 复用同一路径回落匹配 guest token,三个 generator 接收可选 whitelist 过滤生成。 节点排序:fetched_nodes 新增 sort_order,重抓时按名保留顺序, 选择/节点面板支持原生拖拽排序,输出按此顺序排列。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
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>): 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');
|
|
}
|