init: init proj
This commit is contained in:
131
server/src/services/ir.ts
Normal file
131
server/src/services/ir.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user