132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
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,
|
||
};
|
||
}
|