原本换一条线要改 config.yaml、跑校验、等内核重载,几秒起步;而换线恰恰是这个工具 最高频的动作,把它放在最重的路径上方向就错了。现在每一路配一个 select 组加 AUTO、 FALLBACK 两个,入站的 proxy 永久指向 select,换线只是调一次内核接口。这样做的前提是 选中不会丢:内核把它写进 cache 文件,重启后由 patchSelectGroup 恢复,而 profile.store-selected 默认为真。 链式代理靠给来源订阅复制一份声明、挂上 override.dialer-proxy 实现,因为这个字段写在 策略组上内核只会打条日志然后忽略。副本是 type: file 读原订阅已经落盘的缓存,而不是 自己再拉一次:自己拉的话订阅地址一不可达副本就是空的,那一路入站会一条线都不剩, 表现是端口还在监听却没有任何出口。 顺带把 shared/outbound-chain.ts 里「只有 Selector 接受手动选择」那条断言删了,它是 错的(内核检查的是 SelectAble 接口,URLTest 和 Fallback 也实现了)。界面仍然只让 Selector 可点,但理由换成了另外两种的手动指定会被它们自己撤销。 完整取舍见工作区决策记录 0007。
86 lines
3.6 KiB
TypeScript
86 lines
3.6 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
||
import { lastSwitchableGroup, resolveChain } from './outbound-chain.ts'
|
||
import type { RuntimeNode } from './outbound-chain.ts'
|
||
|
||
/**
|
||
* 仿照真实那台机器:PROXY 手动选,它选中了按延迟自动挑的 AUTO,AUTO 挑了一条订阅线路。
|
||
* PROXY 的成员是手写的 AUTO、DIRECT 加上订阅带进来的两条线路。
|
||
*/
|
||
const runtime: Record<string, RuntimeNode> = {
|
||
PROXY: { type: 'Selector', now: 'AUTO', all: ['AUTO', 'DIRECT', '[vmess]DMIT', '[vless]Gomami'] },
|
||
AUTO: { type: 'URLTest', now: '[vless]Gomami', all: ['[vmess]DMIT', '[vless]Gomami'] },
|
||
'[vless]Gomami': { type: 'Vless' },
|
||
'[vmess]DMIT': { type: 'Vmess' },
|
||
DIRECT: { type: 'Direct' },
|
||
}
|
||
|
||
describe('解析流量最终走到哪条线', () => {
|
||
it('一路跟着当前选中走到底', () => {
|
||
expect(resolveChain(runtime, 'PROXY')).toEqual(['PROXY', 'AUTO', '[vless]Gomami'])
|
||
})
|
||
|
||
it('起点就是线路时链条只有它自己', () => {
|
||
expect(resolveChain(runtime, '[vless]Gomami')).toEqual(['[vless]Gomami'])
|
||
})
|
||
|
||
it('没有落脚点时是空链条', () => {
|
||
expect(resolveChain(runtime, null)).toEqual([])
|
||
})
|
||
|
||
it('落脚在一个内核不认识的名字上时也只返回它自己', () => {
|
||
// 配置引用了一个不存在的出站时会走到这里,界面要能显示而不是崩掉
|
||
expect(resolveChain(runtime, '不存在的组')).toEqual(['不存在的组'])
|
||
})
|
||
|
||
it('策略组互相引用成环时停下来,不会转死', () => {
|
||
const looped: Record<string, RuntimeNode> = {
|
||
A: { type: 'Selector', now: 'B', all: ['B'] },
|
||
B: { type: 'Selector', now: 'A', all: ['A'] },
|
||
}
|
||
expect(resolveChain(looped, 'A')).toEqual(['A', 'B'])
|
||
})
|
||
})
|
||
|
||
describe('找出用户能换选中的那一层', () => {
|
||
it('跳过自动决定的组,落在最后一个 Selector 上', () => {
|
||
// 链条是 PROXY(Selector) → AUTO(URLTest) → 线路。能点的是 PROXY 不是 AUTO:
|
||
// 内核对 URLTest 也接受手动指定,但那条线测不通时指定会临时让位,
|
||
// 和「选了就一直是它」不是一回事,摆成同一个控件会骗人。
|
||
const chain = resolveChain(runtime, 'PROXY')
|
||
expect(lastSwitchableGroup(runtime, chain)).toEqual({
|
||
group: 'PROXY',
|
||
members: ['AUTO', 'DIRECT', '[vmess]DMIT', '[vless]Gomami'],
|
||
now: 'AUTO',
|
||
})
|
||
})
|
||
|
||
it('有多个 Selector 时取最靠近线路的那个', () => {
|
||
// 越靠近线路越接近「换线路」这个意图;前面的层换的是「走哪套策略」
|
||
const nested: Record<string, RuntimeNode> = {
|
||
外层: { type: 'Selector', now: '内层', all: ['内层'] },
|
||
内层: { type: 'Selector', now: '线路A', all: ['线路A', '线路B'] },
|
||
线路A: { type: 'Vmess' },
|
||
}
|
||
expect(lastSwitchableGroup(nested, resolveChain(nested, '外层'))?.group).toBe('内层')
|
||
})
|
||
|
||
it('整条链都不是 Selector 时返回空', () => {
|
||
const auto: Record<string, RuntimeNode> = {
|
||
AUTO: { type: 'URLTest', now: '线路A', all: ['线路A'] },
|
||
线路A: { type: 'Vmess' },
|
||
}
|
||
expect(lastSwitchableGroup(auto, resolveChain(auto, 'AUTO'))).toBeNull()
|
||
})
|
||
|
||
it('链条上一个策略组都没有时返回空', () => {
|
||
expect(lastSwitchableGroup(runtime, ['[vless]Gomami'])).toBeNull()
|
||
expect(lastSwitchableGroup(runtime, [])).toBeNull()
|
||
})
|
||
|
||
it('Selector 存在但成员是空的,不算可切换', () => {
|
||
// 筛选条件没匹配到任何线路时会出现这种组,给出一个点不动的列表只会让人困惑
|
||
expect(lastSwitchableGroup({ 空组: { type: 'Selector', all: [], now: '' } }, ['空组'])).toBeNull()
|
||
})
|
||
})
|
||
|