init: init proj

This commit is contained in:
2026-08-25 11:35:05 +08:00
commit bf7f2df67e
80 changed files with 9936 additions and 0 deletions

131
server/src/services/ir.ts Normal file
View File

@@ -0,0 +1,131 @@
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<string, string>; // 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<string, string> = {};
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,
};
}