把入站固定到某条订阅线路
订阅线路不在内核的全局名字表里,入站的 proxy 指不到它(决策记录 0005)。做法是自动建 一个只装它的策略组再指过去,用户看到的只是「这个口固定走这条线」,组是实现细节: 不出现在策略组列表里,列表和详情里都显示成线路本身的名字,没人再引用时自动删掉。 筛选条件必须做对两件事,都写了测试:转义正则元字符(线路名形如 [vmess]DMIT.LAX.Pro 里的方括号和点不转义会匹配错),以及前后加锚点(内核是部分匹配,不加锚点时「香港01」 会把「香港011」也捞进来)。线路名含反引号时表达不了——那是内核切分多个筛选条件的 分隔符——这种情况明说做不到而不是生成一个会静默匹配错的组。 实测:生成的配置 mihomo -t 通过;起一个临时内核问 /proxies,这个组解析出来的成员 恰好只有那一条线路,且确实进了名字表所以入站指得到。
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
* 只管显示和选中,不碰任何数据。
|
||||
*/
|
||||
import { inboundTypeLabel, isUnsetOutbound, outboundLabel } from '@shared/inbounds.ts'
|
||||
import { displayOutboundName } from '@shared/filter-groups.ts'
|
||||
import type { Inbound } from '@shared/inbounds.ts'
|
||||
|
||||
defineProps<{ inbounds: Inbound[] }>()
|
||||
@@ -26,7 +27,7 @@ const selected = defineModel<string | null>('selected', { required: true })
|
||||
<span v-if="inbound.isDefault" class="tag">默认</span>
|
||||
</span>
|
||||
<span :class="['out mono', isUnsetOutbound(inbound.outbound) && 'is-muted']">
|
||||
{{ outboundLabel(inbound.outbound) }}
|
||||
{{ displayOutboundName(outboundLabel(inbound.outbound)) }}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { computed } from 'vue'
|
||||
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
|
||||
import { lastSwitchableGroup, resolveChain } from '@shared/outbound-chain.ts'
|
||||
import { generatedGroupName } from '@shared/filter-groups.ts'
|
||||
import { latestDelay } from '../api'
|
||||
import { kernel, providerLines, runtime } from '../runtime'
|
||||
import OutboundCard from '../components/OutboundCard.vue'
|
||||
@@ -34,6 +35,8 @@ const emit = defineEmits<{
|
||||
remove: []
|
||||
select: [group: string, member: string]
|
||||
test: [name: string]
|
||||
/** 把这一路入站固定到某条订阅线路 */
|
||||
pin: [line: string]
|
||||
}>()
|
||||
|
||||
/**
|
||||
@@ -67,7 +70,11 @@ const BUILTINS: Array<{ mode: OutboundMode; label: string; sub: string }> = [
|
||||
{ mode: { kind: 'global' }, label: 'GLOBAL', sub: '跟随 GLOBAL 组' },
|
||||
]
|
||||
|
||||
const LINE_LOCKED = '订阅线路无法被入站直接引用,需要通过筛选组间接引入。该功能尚未实现。'
|
||||
/** 这一路入站是不是已经固定在某条订阅线路上。 */
|
||||
function isPinnedTo(line: string): boolean {
|
||||
const mode = props.inbound.outbound
|
||||
return mode.kind === 'named' && mode.name === generatedGroupName(line)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -192,10 +199,11 @@ const LINE_LOCKED = '订阅线路无法被入站直接引用,需要通过筛
|
||||
:name="line"
|
||||
:sub="describe(line)"
|
||||
:delay="delayOf(line)"
|
||||
:selected="false"
|
||||
:disabled-reason="LINE_LOCKED"
|
||||
:selected="isPinnedTo(line)"
|
||||
:pending="pendingOutbound === generatedGroupName(line)"
|
||||
:testing="isTesting(line)"
|
||||
testable
|
||||
@pick="emit('pin', line)"
|
||||
@test="emit('test', line)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,12 @@ import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
|
||||
import type { ConfigObject } from '../../vendor/config-editor/index.ts'
|
||||
import { ApiError, api } from '../api'
|
||||
import type { ListenerFields } from '../api'
|
||||
import {
|
||||
generatedGroupName,
|
||||
isGeneratedGroup,
|
||||
makeFilterGroup,
|
||||
pruneGeneratedGroups,
|
||||
} from '@shared/filter-groups.ts'
|
||||
import { lineProvider, optimisticSelect, providerLines, reloadRuntime } from '../runtime'
|
||||
import { useDelayTest } from '../delay-test'
|
||||
import { density } from '../prefs'
|
||||
@@ -32,7 +38,12 @@ const props = defineProps<{ data: ConfigObject; revision: string }>()
|
||||
const emit = defineEmits<{ saved: [] }>()
|
||||
|
||||
const inbounds = computed(() => collectInbounds(props.data))
|
||||
const outbounds = computed(() => collectOutboundNames(props.data))
|
||||
const outbounds = computed(() => {
|
||||
const names = collectOutboundNames(props.data)
|
||||
// 生成的筛选组不出现在可选列表里:它们是「固定到某条线路」的实现细节,
|
||||
// 用户从没建过,让他在策略组那一栏看到只会莫名其妙。
|
||||
return { ...names, groups: names.groups.filter((g) => !isGeneratedGroup(g)) }
|
||||
})
|
||||
|
||||
const selectedId = ref<string | null>(null)
|
||||
const saving = ref(false)
|
||||
@@ -77,7 +88,10 @@ const takenNames = computed(() => inbounds.value.map((i) => i.name))
|
||||
const clone = () => JSON.parse(JSON.stringify(props.data)) as ConfigObject
|
||||
|
||||
/** 所有改配置文件的动作都从这里走。 */
|
||||
async function commit(next: ConfigObject, success: string) {
|
||||
async function commit(raw: ConfigObject, success: string) {
|
||||
// 顺手清掉没人再引用的生成组。不清理的话,改几次出站文件里就攒下一堆
|
||||
// 用户从没主动建过、也不知道是干嘛的组。
|
||||
const next = pruneGeneratedGroups(raw)
|
||||
saving.value = true
|
||||
failure.value = null
|
||||
done.value = ''
|
||||
@@ -115,6 +129,45 @@ async function pickOutbound(mode: OutboundMode) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前这一路入站固定到某条订阅线路。
|
||||
*
|
||||
* 订阅线路不能被入站直接引用,所以要先建一个只装它的策略组再指过去(决策记录 0005)。
|
||||
* 这一步对用户是透明的:他看到的只是「这个口固定走这条线」。
|
||||
*/
|
||||
async function pinLine(line: string) {
|
||||
const target = selected.value
|
||||
if (!target || target.listenerIndex === null || pendingOutbound.value) return
|
||||
|
||||
const provider = lineProvider.value[line]
|
||||
if (!provider) {
|
||||
notice.value = `找不到 ${line} 属于哪个订阅,无法固定`
|
||||
return
|
||||
}
|
||||
|
||||
const group = makeFilterGroup(line, provider)
|
||||
if (!group) {
|
||||
notice.value = `${line} 的名字里含反引号,内核会把它当成筛选条件的分隔符,无法固定`
|
||||
return
|
||||
}
|
||||
|
||||
const groupName = generatedGroupName(line)
|
||||
pendingOutbound.value = groupName
|
||||
|
||||
const next = clone()
|
||||
const groups = Array.isArray(next['proxy-groups']) ? (next['proxy-groups'] as ConfigObject[]) : []
|
||||
// 同一条线路被两路入站固定时共用一个组,不重复建
|
||||
if (!groups.some((g) => g.name === groupName)) groups.push(group)
|
||||
next['proxy-groups'] = groups
|
||||
;(next.listeners as ConfigObject[])[target.listenerIndex]!.proxy = groupName
|
||||
|
||||
try {
|
||||
await commit(next, `${target.name} 已固定走 ${line}`)
|
||||
} finally {
|
||||
pendingOutbound.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
formEditing.value = null
|
||||
formOpen.value = true
|
||||
@@ -223,6 +276,7 @@ async function removeSelected() {
|
||||
@remove="removeSelected"
|
||||
@select="selectMember"
|
||||
@test="(name: string) => test(name, lineProvider[name])"
|
||||
@pin="pinLine"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user