'use client'; import { useState } from 'react'; import { buildRoutingJson, type DomainStrategy, type RoutingInput, type RuleNetwork, type Strategy, } from '@/lib/xray/routing'; import { ToolFrame } from './tool-frame'; import { TextField, SelectField } from './shared/fields'; import { OutputBlock } from './shared/output-block'; interface BalancerRow { tag: string; selector: string; strategy: Strategy; fallbackTag: string; } interface RuleRow { domain: string; ip: string; port: string; network: string; inboundTag: string; targetKind: 'outbound' | 'balancer'; targetTag: string; } const STRATEGIES: readonly Strategy[] = ['random', 'roundRobin', 'leastPing', 'leastLoad']; const NETWORKS = ['any', 'tcp', 'udp', 'tcp,udp']; const TARGET_KINDS = ['outbound', 'balancer']; const DOMAIN_STRATEGIES: readonly DomainStrategy[] = ['AsIs', 'IPIfNonMatch', 'IPOnDemand']; const DEFAULT_BALANCERS: BalancerRow[] = [ { tag: 'balancer', selector: 'proxy', strategy: 'leastPing', fallbackTag: '' }, ]; const DEFAULT_RULES: RuleRow[] = [ { domain: 'geosite:category-ads-all', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'block' }, { domain: '', ip: 'geoip:private', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'direct' }, ]; function list(s: string): string[] { return s .split(',') .map((x) => x.trim()) .filter(Boolean); } const addBtn = 'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground'; export function RoutingBuilder() { const [domainStrategy, setDomainStrategy] = useState('IPIfNonMatch'); const [balancers, setBalancers] = useState(DEFAULT_BALANCERS); const [rules, setRules] = useState(DEFAULT_RULES); function patchBalancer(i: number, patch: Partial) { setBalancers((prev) => prev.map((b, j) => (i === j ? { ...b, ...patch } : b))); } function patchRule(i: number, patch: Partial) { setRules((prev) => prev.map((r, j) => (i === j ? { ...r, ...patch } : r))); } const input: RoutingInput = { domainStrategy, balancers: balancers .filter((b) => b.tag.trim()) .map((b) => ({ tag: b.tag.trim(), selector: list(b.selector), strategy: b.strategy, fallbackTag: b.fallbackTag.trim() || undefined, })), rules: rules .filter((r) => r.targetTag.trim()) .map((r) => ({ domain: list(r.domain), ip: list(r.ip), port: r.port.trim() || undefined, network: r.network === 'any' ? undefined : (r.network as RuleNetwork), inboundTag: list(r.inboundTag), target: { kind: r.targetKind, tag: r.targetTag.trim() }, })), }; function reset() { setDomainStrategy('IPIfNonMatch'); setBalancers(DEFAULT_BALANCERS); setRules(DEFAULT_RULES); } return (
setDomainStrategy(v as DomainStrategy)} options={DOMAIN_STRATEGIES} />

Balancers

{balancers.map((b, i) => (
patchBalancer(i, { tag: v })} /> patchBalancer(i, { selector: v })} /> patchBalancer(i, { strategy: v as Strategy })} options={STRATEGIES} /> patchBalancer(i, { fallbackTag: v })} placeholder="optional" />
))}

Rules

{rules.map((r, i) => (
patchRule(i, { domain: v })} placeholder="geosite:google, example.com" /> patchRule(i, { ip: v })} placeholder="geoip:cn, 1.1.1.1" /> patchRule(i, { port: v })} placeholder="443 or 1000-2000" /> patchRule(i, { network: v })} options={NETWORKS} /> patchRule(i, { inboundTag: v })} placeholder="optional" /> patchRule(i, { targetKind: v as 'outbound' | 'balancer' })} options={TARGET_KINDS} /> patchRule(i, { targetTag: v })} />
))}
); }