每路入站配一组专属策略组,换线不再改配置文件
原本换一条线要改 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。
This commit is contained in:
+66
-86
@@ -7,24 +7,24 @@
|
||||
* 运行时动作(切换选中、测延迟)不经过它,因为它们不写文件也不需要校验。
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { collectInbounds } from '@shared/inbounds.ts'
|
||||
import type { Inbound } from '@shared/inbounds.ts'
|
||||
import {
|
||||
collectInbounds,
|
||||
collectOutboundNames,
|
||||
outboundLabel,
|
||||
outboundToProxyField,
|
||||
resolveDestination,
|
||||
} from '@shared/inbounds.ts'
|
||||
import type { Inbound, OutboundMode } from '@shared/inbounds.ts'
|
||||
applySetup,
|
||||
chainGroupName,
|
||||
groupUses,
|
||||
panelGroups,
|
||||
pruneGenerated,
|
||||
syncGenerated,
|
||||
readSetup,
|
||||
userProviders,
|
||||
} from '@shared/inbound-groups.ts'
|
||||
import type { OutboundSetup } from '@shared/inbound-groups.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 type { GroupView } from '../runtime'
|
||||
import { optimisticSelect, providerLines, reloadRuntime, runtime } from '../runtime'
|
||||
import { useDelayTest } from '../delay-test'
|
||||
import { density } from '../prefs'
|
||||
import InboundList from '../components/InboundList.vue'
|
||||
@@ -38,11 +38,12 @@ const props = defineProps<{ data: ConfigObject; revision: string }>()
|
||||
const emit = defineEmits<{ saved: [] }>()
|
||||
|
||||
const inbounds = computed(() => collectInbounds(props.data))
|
||||
const outbounds = computed(() => {
|
||||
const names = collectOutboundNames(props.data)
|
||||
// 生成的筛选组不出现在可选列表里:它们是「固定到某条线路」的实现细节,
|
||||
// 用户从没建过,让他在策略组那一栏看到只会莫名其妙。
|
||||
return { ...names, groups: names.groups.filter((g) => !isGeneratedGroup(g)) }
|
||||
const subscriptions = computed(() => userProviders(props.data))
|
||||
|
||||
/** 刚接管一路入站时给它什么:订阅全给上,不筛。三个组由 applySetup 一并建出来。 */
|
||||
const defaultSetup = (): OutboundSetup => ({
|
||||
proxy: { sources: [...subscriptions.value], filter: null },
|
||||
chain: null,
|
||||
})
|
||||
|
||||
const selectedId = ref<string | null>(null)
|
||||
@@ -55,8 +56,6 @@ const notice = ref('')
|
||||
const fields = ref<ListenerFields | null>(null)
|
||||
const formOpen = ref(false)
|
||||
const formEditing = ref<Inbound | null>(null)
|
||||
/** 正在提交的那个出站的名字。只有它显示忙碌,其余卡片照常能点 */
|
||||
const pendingOutbound = ref<string | null>(null)
|
||||
|
||||
const { testing, test } = useDelayTest(
|
||||
() => void reloadRuntime(),
|
||||
@@ -85,13 +84,32 @@ watch(
|
||||
const selected = computed(() => inbounds.value.find((i) => i.id === selectedId.value) ?? null)
|
||||
const takenNames = computed(() => inbounds.value.map((i) => i.name))
|
||||
|
||||
/** 面板上能操作哪些策略组、各自能不能点。这个问题只在 panelGroups 里回答一次。 */
|
||||
const panel = computed(() =>
|
||||
selected.value
|
||||
? panelGroups(props.data, runtime.value, selected.value)
|
||||
: { groups: [], shared: false },
|
||||
)
|
||||
|
||||
/** 共用的组是别人的,改不了它的来源,所以高级配置也不给。 */
|
||||
const setup = computed<OutboundSetup | null>(() =>
|
||||
selected.value && !panel.value.shared ? readSetup(props.data, selected.value.name) : null,
|
||||
)
|
||||
|
||||
const chainView = computed<GroupView | null>(() => {
|
||||
const target = selected.value
|
||||
if (!target || !setup.value?.chain) return null
|
||||
const name = chainGroupName(target.name)
|
||||
return { name, uses: groupUses(props.data, name) ?? [] }
|
||||
})
|
||||
|
||||
const clone = () => JSON.parse(JSON.stringify(props.data)) as ConfigObject
|
||||
|
||||
/** 所有改配置文件的动作都从这里走。 */
|
||||
async function commit(raw: ConfigObject, success: string) {
|
||||
// 顺手清掉没人再引用的生成组。不清理的话,改几次出站文件里就攒下一堆
|
||||
// 用户从没主动建过、也不知道是干嘛的组。
|
||||
const next = pruneGeneratedGroups(raw)
|
||||
// 先把生成的东西按当前规则重建一遍,再清掉没人再引用的。不清理的话,改几次文件里就
|
||||
// 攒下一堆用户从没主动建过的东西,而没人用的订阅副本还会按 interval 继续联网。
|
||||
const next = pruneGenerated(syncGenerated(raw, collectInbounds(raw)))
|
||||
saving.value = true
|
||||
failure.value = null
|
||||
done.value = ''
|
||||
@@ -110,62 +128,18 @@ async function commit(raw: ConfigObject, success: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function pickOutbound(mode: OutboundMode) {
|
||||
/** 保存高级配置:改的是这一路的组从哪些订阅取成员、筛掉哪些。 */
|
||||
async function saveSetup(next: OutboundSetup) {
|
||||
const target = selected.value
|
||||
if (!target || target.listenerIndex === null || pendingOutbound.value) return
|
||||
pendingOutbound.value = outboundLabel(mode)
|
||||
|
||||
const next = clone()
|
||||
const entry = (next.listeners as ConfigObject[])[target.listenerIndex]!
|
||||
const proxy = outboundToProxyField(mode)
|
||||
// RULE 是「没有这个字段」而不是「字段为空」,所以是删掉而不是置空。
|
||||
if (proxy === undefined) delete entry.proxy
|
||||
else entry.proxy = proxy
|
||||
|
||||
try {
|
||||
await commit(next, `${target.name} 的出站已设为 ${outboundLabel(mode)}`)
|
||||
} finally {
|
||||
pendingOutbound.value = null
|
||||
}
|
||||
if (!target || saving.value) return
|
||||
await commit(applySetup(clone(), target.name, next), `${target.name} 的代理配置已保存`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前这一路入站固定到某条订阅线路。
|
||||
*
|
||||
* 订阅线路不能被入站直接引用,所以要先建一个只装它的策略组再指过去(决策记录 0005)。
|
||||
* 这一步对用户是透明的:他看到的只是「这个口固定走这条线」。
|
||||
*/
|
||||
async function pinLine(line: string) {
|
||||
/** 把一路还没接管的入站改成专属代理组模式。 */
|
||||
async function adopt() {
|
||||
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
|
||||
}
|
||||
if (!target || saving.value) return
|
||||
await commit(applySetup(clone(), target.name, defaultSetup()), `${target.name} 已改为逐条选线路`)
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
@@ -188,7 +162,12 @@ async function submitForm(entry: ConfigObject) {
|
||||
else listeners[editingIndex] = entry
|
||||
next.listeners = listeners
|
||||
|
||||
await commit(next, editingIndex === null ? `已新增 ${String(entry.name)}` : '已保存')
|
||||
// 新增的入站直接进专属组模式。否则新建完看到的是一块「这一路不由 Patchbay 管理」
|
||||
// 的提示,还得再点一次才能用。
|
||||
const name = String(entry.name)
|
||||
const withGroup = editingIndex === null ? applySetup(next, name, defaultSetup()) : next
|
||||
|
||||
await commit(withGroup, editingIndex === null ? `已新增 ${name}` : '已保存')
|
||||
// 失败时不能关对话框——关了再打开就是空表单,用户填的名称、端口、类型全没了
|
||||
if (failure.value) return
|
||||
formOpen.value = false
|
||||
@@ -238,8 +217,8 @@ async function removeSelected() {
|
||||
<span class="sum-k">入站</span>
|
||||
</div>
|
||||
<div class="sum">
|
||||
<span class="sum-n">{{ outbounds.groups.length }}</span>
|
||||
<span class="sum-k">策略组</span>
|
||||
<span class="sum-n">{{ subscriptions.length }}</span>
|
||||
<span class="sum-k">订阅</span>
|
||||
</div>
|
||||
<div class="sum">
|
||||
<span class="sum-n">{{ providerLines.length }}</span>
|
||||
@@ -266,17 +245,18 @@ async function removeSelected() {
|
||||
<InboundDetail
|
||||
v-if="selected"
|
||||
:inbound="selected"
|
||||
:groups="outbounds.groups"
|
||||
:own-proxies="outbounds.ownProxies"
|
||||
:destination="resolveDestination(data, selected)"
|
||||
:pending-outbound="pendingOutbound"
|
||||
:panel="panel"
|
||||
:chain="chainView"
|
||||
:subscriptions="subscriptions"
|
||||
:setup="setup"
|
||||
:testing="testing"
|
||||
@pick="pickOutbound"
|
||||
:saving="saving"
|
||||
@edit="openEdit"
|
||||
@remove="removeSelected"
|
||||
@select="selectMember"
|
||||
@test="(name: string) => test(name, lineProvider[name])"
|
||||
@pin="pinLine"
|
||||
@test="test"
|
||||
@save="saveSetup"
|
||||
@adopt="adopt"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user