Files
proxy-station/server/src/services/ir.ts
2026-08-25 11:35:05 +08:00

132 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}