/** * 「这个端口现在到底走哪条线」——这个工具要回答的核心问题。 * * 答案往往不是一跳能到的:一路入站落在 `PROXY` 组上,`PROXY` 选中了 `AUTO`, * `AUTO` 又按延迟挑了某条线路。只显示第一层等于没回答。 * * 这里只做纯计算,输入是内核报上来的运行时状态,输出是名字数组。放在 shared 里 * 是为了能被 `bun test` 覆盖——它带防环处理,而防环这种事写在组件的 computed 里 * 就再也没人测得到。 */ /** 运行时状态里跟解析链路有关的部分。完整定义在 web/api.ts。 */ export interface RuntimeNode { /** 出站类型。策略组是 Selector / URLTest / Fallback / LoadBalance 之一 */ type?: string /** 策略组当前选中的成员。不是策略组就没有这个字段 */ now?: string /** 策略组的成员列表 */ all?: string[] } /** * 从落脚的出站一路跟着「当前选中」往下走。 * * 返回的数组第一项是起点,最后一项是走到头的那个名字。起点为 null 时返回空数组。 * 策略组理论上可以互相引用成环,遇到重复的名字就停下——内核自己会拒绝这种配置, * 但界面不能因为一份坏配置就转到卡死。 */ export function resolveChain( runtime: Record, start: string | null, ): string[] { const hops: string[] = [] let cursor = start while (cursor && !hops.includes(cursor)) { hops.push(cursor) cursor = runtime[cursor]?.now ?? null } return hops } export interface SwitchableGroup { group: string members: string[] now: string } /** * 链条上最后一个 `Selector` 组。这一路入站没被 Patchbay 接管时,界面上能点的就是它。 * * 取最后一个而不是第一个,是因为越靠近线路的那一层越接近「换线路」这个意图; * 前面的层换的是「走哪套策略」,那是另一回事。 * * 只认 `Selector`,尽管内核对 `URLTest` 和 `Fallback` 也接受手动指定 * (hub/route/proxies.go:85-90 检查的是 `SelectAble` 接口,不是类型)。原因是那两种的 * 手动指定会自己失效:`URLTest` 选中的线路测不通时临时让位给最快的 * (adapter/outboundgroup/urltest.go:103-116),`Fallback` 更狠,测不通一次就把手动指定 * 永久清掉(fallback.go:117)。把它们和「选了就一直是它」摆成同一个控件会骗人。 */ export function lastSwitchableGroup( runtime: Record, chain: string[], ): SwitchableGroup | null { for (let i = chain.length - 1; i >= 0; i -= 1) { const name = chain[i]! const node = runtime[name] if (!node?.all?.length || node.type !== 'Selector') continue return { group: name, members: node.all, now: node.now ?? '' } } return null }