import { asc, eq } from 'drizzle-orm'; import { db } from '../db/index.js'; import { groupMembers, nodes, policyGroups, rules, rulesets, settings } from '../db/schema.js'; import type { NodeDto } from '@proxy-station/shared'; import { rowToDto } from '../routes/nodes.js'; export interface GroupIR { name: string; type: 'select' | 'url-test' | 'fallback'; testUrl: string | null; interval: number | null; tolerance: number | null; filterRegex: string | null; includeAllNodes: boolean; /** 有序成员:节点名 / 组名 / DIRECT 等内置策略 */ members: { kind: 'node' | 'group' | 'builtin'; name: string }[]; } export interface RuleIR { type: string; value: string | null; policy: string; params: string[]; ruleset: { id: string; name: string; surgeUrl: string; clashUrl: string | null; clashBehavior: string; clashFormat: string; needsConvert: boolean; } | null; } export interface ProfileIR { nodes: NodeDto[]; // 仅启用节点,按 sortOrder groups: GroupIR[]; rules: RuleIR[]; // 仅启用规则,按 seq sections: Record; // Surge 原文段落(General/Host/…) } export function getSetting(key: string): string { const row = db.select().from(settings).where(eq(settings.key, key)).get(); return row?.value ?? ''; } export function setSetting(key: string, value: string) { db.insert(settings) .values({ key, value }) .onConflictDoUpdate({ target: settings.key, set: { value } }) .run(); } /** * @param opts.nodeIds 订阅分配的节点范围;为空或未提供时下发全部启用节点 */ export function buildProfileIR(opts: { nodeIds?: string[] } = {}): ProfileIR { const allowed = opts.nodeIds && opts.nodeIds.length > 0 ? new Set(opts.nodeIds) : null; const nodeRows = db .select() .from(nodes) .where(eq(nodes.enabled, true)) .orderBy(asc(nodes.sortOrder), asc(nodes.name)) .all() .map(rowToDto) .filter((n) => !allowed || allowed.has(n.id)); const nodeById = new Map(nodeRows.map((n) => [n.id, n])); const memberRows = db.select().from(groupMembers).orderBy(asc(groupMembers.sortOrder)).all(); const allGroupRows = db.select().from(policyGroups).orderBy(asc(policyGroups.sortOrder)).all(); // 停用的组从输出中剔除;引用它的成员与规则一并跳过,保证产物有效 const groupRows = allGroupRows.filter((g) => g.enabled); const liveNames = new Set(groupRows.map((g) => g.name)); const BUILTIN_POLICIES = new Set(['DIRECT', 'REJECT', 'REJECT-DROP', 'REJECT-NO-DROP', 'REJECT-TINYGIF']); /** 组已被删除或停用时,引用它的成员/规则应当跳过 */ const isDeadRef = (name: string) => !liveNames.has(name) && !BUILTIN_POLICIES.has(name); const groups: GroupIR[] = groupRows.map((g) => ({ name: g.name, type: g.type, testUrl: g.testUrl, interval: g.interval, tolerance: g.tolerance, filterRegex: g.filterRegex, includeAllNodes: g.includeAllNodes, members: memberRows .filter((m) => m.groupId === g.id) .flatMap((m): GroupIR['members'] => { if (m.memberKind === 'node') { const node = m.nodeId ? nodeById.get(m.nodeId) : undefined; return node ? [{ kind: 'node', name: node.name }] : []; // 被禁用/删除的节点成员跳过 } if (!m.refName) return []; if (m.memberKind === 'group' && isDeadRef(m.refName)) return []; return [{ kind: m.memberKind, name: m.refName }]; }), })); const setRows = new Map(db.select().from(rulesets).all().map((r) => [r.id, r])); const ruleRows = db .select() .from(rules) .where(eq(rules.enabled, true)) .orderBy(asc(rules.seq)) .all() .filter((r) => r.type === 'FINAL' || !isDeadRef(r.policy)) // FINAL 必须保留:其策略组被停用或删除时兜底 DIRECT .map((r) => (r.type === 'FINAL' && isDeadRef(r.policy) ? { ...r, policy: 'DIRECT' } : r)); const ruleIRs: RuleIR[] = ruleRows.map((r) => { const rs = r.value ? setRows.get(r.value) : undefined; return { type: r.type, value: r.value, policy: r.policy, params: JSON.parse(r.params), ruleset: rs ?? null, }; }); const sections: Record = {}; for (const key of ['General', 'Host', 'URL Rewrite', 'Header Rewrite', 'MITM', 'Script']) { const v = getSetting(`surge:${key}`); if (v) sections[key] = v; } return { nodes: nodeRows, groups, rules: ruleIRs, sections, }; }