可切换的那一层改成最后一个 Selector,不是最后一个有成员的组

原来的规则会落在 AUTO 上,而 AUTO 是 url-test 类型——它按延迟自己挑,手动选它没有
意义,点完会被下一轮延迟测试算回去。内核里只有 Selector 是「由人来选」的
(constant/adapters.go:240-248 列了全部五种),URLTest 挑最快、Fallback 挑第一个可用、
LoadBalance 分摊,Relay 已被移除。

链条上现在每一跳都标出类型(PROXY Selector → AUTO URLTest → 线路 Vless),并明说
自动决定的那几层不接受手动指定——不说清楚的话,用户看到链条里有个组却点不了,
只会以为是坏了。
This commit is contained in:
im0o
2026-08-10 18:38:59 +08:00
parent c5ac59ecf9
commit 5049879a70
3 changed files with 126 additions and 26 deletions
+37 -5
View File
@@ -11,12 +11,26 @@
/** 运行时状态里跟解析链路有关的部分。完整定义在 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'
}
/**
* 从落脚的出站一路跟着「当前选中」往下走。
*
@@ -44,11 +58,13 @@ export interface SwitchableGroup {
}
/**
* 链条上最后一个还是策略组的环节。
* 链条上最后一个能由人来选的策略组。
*
* 那是用户能真正做出选择的地方:往前的环节只是层层转交,往后已经是具体线路了。
* 取最后一个而不是第一个,是因为换最靠近线路的那一层才是「换线路」;换前面的层
* 换的是「走哪套策略」,那是另一回事。
* 取最后一个而不是第一个,是因为越靠近线路的那一层越接近「换线路」这个意图;
* 前面的层换的是「走哪套策略」,那是另一回事。
*
* 跳过自动决定的组:让人去点一个 URLTest 组的成员,点完会被延迟测试算回去,
* 那不是选择,是错觉。
*/
export function lastSwitchableGroup(
runtime: Record<string, RuntimeNode>,
@@ -57,7 +73,23 @@ export function lastSwitchableGroup(
for (let i = chain.length - 1; i >= 0; i -= 1) {
const name = chain[i]!
const node = runtime[name]
if (node?.all?.length) return { group: name, members: node.all, now: node.now ?? '' }
if (!node?.all?.length || !acceptsManualChoice(node.type)) continue
return { group: name, members: node.all, now: node.now ?? '' }
}
return null
}
/**
* 链条上那些自动决定选中结果的组。
*
* 界面用它来解释「为什么这一层点不了」——不说清楚的话,用户看到链条里有个组却没法
* 操作,只会以为是坏了。
*/
export function autoDecidedHops(
runtime: Record<string, RuntimeNode>,
chain: string[],
): Array<{ name: string; type: string }> {
return chain
.map((name) => ({ name, type: runtime[name]?.type ?? '' }))
.filter((hop) => hop.type !== '' && !acceptsManualChoice(hop.type) && runtime[hop.name]?.all)
}