Files
mihomo-patchbay/web/runtime.ts
T
im0o e9e2eb555d 每路入站配一组专属策略组,换线不再改配置文件
原本换一条线要改 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。
2026-08-11 03:10:38 +08:00

164 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 内核的运行时状态:有哪些出站、每个是什么协议、策略组当前选中谁、最近一次延迟。
*
* 这些数据配置文件里没有,只有运行中的内核知道;而且界面上有三层组件要用它。
* 原本它住在根组件里靠 props 一层层往下传,中间有两层完全不碰它、纯粹为了转发而
* 声明了参数。做成模块级单例之后那两层就不用再知道它的存在了。
*
* 用的是 web/prefs.ts 已有的模式(模块级 ref),不是新抽象。
*/
import { ref } from 'vue'
import { api } from './api'
import type { ProxyInfo } from './api'
import { isGenerated } from '@shared/inbound-groups.ts'
/**
* 出站名字 → 运行时信息。内核没连接时是空的。
*
* 合并了内核全局名字表和用户订阅里的线路。Patchbay 自己生成的订阅副本**不并进来**:
* 副本里的线路和原版同名(决策记录 0007 里那套复制订阅声明的做法),并进来只会互相
* 覆盖。要看副本里那一份,用下面的 `providers`。
*/
export const runtime = ref<Record<string, ProxyInfo>>({})
/** 订阅名 → 它当前提供的线路。含 Patchbay 生成的副本,按订阅隔开所以不会撞。 */
export const providers = ref<Record<string, ProxyInfo[]>>({})
/** 用户自己的订阅带来的线路,按订阅返回的顺序。不含生成的副本。 */
export const providerLines = ref<string[]>([])
/** 订阅线路 → 它所属的订阅名。测它的延迟必须从所属订阅进去。 */
export const lineProvider = ref<Record<string, string>>({})
export const kernel = ref<{ online: boolean; version?: string }>({ online: false })
/**
* 重新读一遍运行时状态。
*
* 要读两处:`/proxies` 只包含内核全局名字表里的东西(内置出站、自有线路、策略组),
* 订阅线路不在那张表上,它们的协议和延迟只能从 `/providers/proxies` 拿。
*
* 任何一处取不到都不算错误——界面照常显示,只是少了延迟数字。内核没起来的时候正是
* 最需要这个界面的时候。
*/
export async function reloadRuntime(): Promise<void> {
kernel.value = await api.kernel().catch(() => ({ online: false }))
if (!kernel.value.online) {
runtime.value = {}
providers.value = {}
providerLines.value = []
lineProvider.value = {}
return
}
const merged: Record<string, ProxyInfo> = {}
try {
Object.assign(merged, (await api.proxies()).proxies)
} catch {
// 拿不到就当没有
}
const byProvider: Record<string, ProxyInfo[]> = {}
const lines: string[] = []
const owner: Record<string, string> = {}
try {
const result = await api.providers()
for (const [name, provider] of Object.entries(result.providers)) {
// 内核会给每个用了 proxies 的策略组造一个内部 provider,那些不是订阅
if (provider.vehicleType === 'Compatible') continue
byProvider[name] = provider.proxies
if (isGenerated(name)) continue
for (const line of provider.proxies) {
merged[line.name] = line
lines.push(line.name)
owner[line.name] = name
}
}
} catch {
// 同上
}
runtime.value = merged
providers.value = byProvider
providerLines.value = lines
lineProvider.value = owner
}
/**
* 界面渲染一个策略组需要知道的最少信息。
*
* `uses` 来自配置文件,`now` 来自内核,两边缺一不可:光有名字查不到它从哪取成员,
* 而开了链式代理之后同名线路有两份,不知道来源就分不清该显示哪一份。
*/
export interface GroupView {
name: string
uses: string[]
}
/** 一条线路在界面上要显示的样子,外加测它的延迟该从哪个订阅进去。 */
export interface LineEntry {
name: string
/** 它来自哪个订阅。测延迟必须带上 */
provider: string
info: ProxyInfo | undefined
}
/**
* 这条线路是不是来自 Patchbay 生成的订阅副本。
*
* 副本整份挂着 `dialer-proxy`,所以来自副本就一定经过入口。点名加进组里的成员
* (`DIRECT`、AUTO 和 FALLBACK 那两个组)不属于任何订阅,不在副本里。
*/
export function isChainedLine(name: string): boolean {
return Object.entries(providers.value).some(
([provider, lines]) => isGenerated(provider) && lines.some((line) => line.name === name),
)
}
/**
* 这几个订阅里一共有哪些线路。
*
* 只给高级配置里那个「匹配到几条」的预览用:那时用户选的来源还没保存,内核也还不知道
* 有这么个组,只能自己按来源算一遍。已经存在的组要用下面的 `membersOf`。
*/
export function linesFrom(uses: string[]): string[] {
const seen = new Set<string>()
for (const provider of uses) {
for (const line of providers.value[provider] ?? []) seen.add(line.name)
}
return [...seen]
}
/**
* 一个策略组的成员,按内核给的顺序。
*
* 成员名单直接用内核算好的(`/proxies` 里那个 `all`),不自己从来源订阅里推——组上的
* `filter` 已经在内核那边生效过了,重推一遍就是把同一条规则实现两次,两边一旦不一致
* 界面显示的就是假的。
*
* 每个成员还要标出它来自哪个订阅,因为测延迟得从所属订阅进去。顺着组自己的 `use:` 找而
* 不是全局按名字查:开了链式代理之后同一个线路名在原订阅和副本里各有一份,只有顺着来源
* 才知道该用哪一份。副本那份挂着 `dialer-proxy`,测出来是走完两跳的延迟,这正是这一路
* 真实的延迟。点名加进来的成员(DIRECT、别的策略组)不属于任何订阅,`provider` 是空串。
*/
export function membersOf(view: GroupView): LineEntry[] {
return (runtime.value[view.name]?.all ?? []).map((name) => {
for (const provider of view.uses) {
const line = (providers.value[provider] ?? []).find((item) => item.name === name)
if (line) return { name, provider, info: line }
}
return { name, provider: '', info: runtime.value[name] }
})
}
/**
* 切换策略组选中谁时先在本地改掉,不等内核回话。
*
* 这一步不写配置文件、失败率极低,而等一个往返再更新会让点击感觉发木。
* 真失败了随后的刷新会把状态纠正回来。
*/
export function optimisticSelect(group: string, member: string): void {
const node = runtime.value[group]
if (node) node.now = member
}