链条回到纯名字。类型标注和「某某组自己决定选中哪一条」那句解释是我上一版加的, 用来说明为什么那一层点不了——但不能点的东西本来就没画出来,解释一个看不见的东西 只是噪音。 默认入站那条压成一句,内核未连接那条压成四个字,没有附加规则时整行不显示而不是 显示一个破折号。shared 里跟着没人用的 autoDecidedHops 一并删掉。
82 lines
3.0 KiB
TypeScript
82 lines
3.0 KiB
TypeScript
/**
|
||
* 「这个端口现在到底走哪条线」——这个工具要回答的核心问题。
|
||
*
|
||
* 答案往往不是一跳能到的:一路入站落在 `PROXY` 组上,`PROXY` 选中了 `AUTO`,
|
||
* `AUTO` 又按延迟挑了某条线路。只显示第一层等于没回答。
|
||
*
|
||
* 这里只做纯计算,输入是内核报上来的运行时状态,输出是名字数组。放在 shared 里
|
||
* 是为了能被 `bun test` 覆盖——它带防环处理,而防环这种事写在组件的 computed 里
|
||
* 就再也没人测得到。
|
||
*/
|
||
|
||
/** 运行时状态里跟解析链路有关的部分。完整定义在 web/api.ts。 */
|
||
export interface RuntimeNode {
|
||
/** 出站类型。策略组是 Selector / URLTest / Fallback / LoadBalance 之一 */
|
||
type?: string
|
||
/** 策略组当前选中的成员。不是策略组就没有这个字段 */
|
||
now?: string
|
||
/** 策略组的成员列表 */
|
||
all?: string[]
|
||
}
|
||
|
||
/**
|
||
* 这个类型的策略组接不接受手动选择。
|
||
*
|
||
* 内核里只有 `Selector` 是「由人来选」的(内核源码 constant/adapters.go:240-248 列出
|
||
* 全部类型)。`URLTest` 按延迟挑最快的、`Fallback` 挑第一个可用的、`LoadBalance` 在成员
|
||
* 之间分摊——它们的选中结果是算出来的,界面上给一个能点的列表只会让人困惑:点完可能
|
||
* 立刻被算回去。`Relay` 已经被内核移除。
|
||
*/
|
||
export function acceptsManualChoice(type: string | undefined): boolean {
|
||
return type === 'Selector'
|
||
}
|
||
|
||
/**
|
||
* 从落脚的出站一路跟着「当前选中」往下走。
|
||
*
|
||
* 返回的数组第一项是起点,最后一项是走到头的那个名字。起点为 null 时返回空数组。
|
||
* 策略组理论上可以互相引用成环,遇到重复的名字就停下——内核自己会拒绝这种配置,
|
||
* 但界面不能因为一份坏配置就转到卡死。
|
||
*/
|
||
export function resolveChain(
|
||
runtime: Record<string, RuntimeNode>,
|
||
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
|
||
}
|
||
|
||
/**
|
||
* 链条上最后一个能由人来选的策略组。
|
||
*
|
||
* 取最后一个而不是第一个,是因为越靠近线路的那一层越接近「换线路」这个意图;
|
||
* 前面的层换的是「走哪套策略」,那是另一回事。
|
||
*
|
||
* 跳过自动决定的组:让人去点一个 URLTest 组的成员,点完会被延迟测试算回去,
|
||
* 那不是选择,是错觉。
|
||
*/
|
||
export function lastSwitchableGroup(
|
||
runtime: Record<string, RuntimeNode>,
|
||
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 || !acceptsManualChoice(node.type)) continue
|
||
return { group: name, members: node.all, now: node.now ?? '' }
|
||
}
|
||
return null
|
||
}
|
||
|