Files
mihomo-patchbay/web/runtime.ts
T
im0o a4d08892f7 路由页一轮交互细化:草稿预览、预选、若干修正
上半部分收敛成一条规则:草稿和已保存一致时显示内核现状;不一致时显示
按草稿保存后的样子,将移除的画删除线、新增的虚线描边,即将被移除的当前
选中用危险色警示。点击语义只看卡片:现在就存在的成员点了立刻生效,保存
后才加入的点了是预选,保存成功后再作为运行时动作选上。预览的集合运算在
web/member-preview.ts,组件里只留接线。

其余:订阅选择器重写成可搜索多选(原平铺标签订阅一多就是一面墙);删除
入站换成页面内确认对话框;列表右列解析到最终出口并标注链式;「高级配置」
更名「配置」;表单重置改按内容比较(按引用的话任何一次运行时刷新都会冲掉
未保存的草稿,watch 传返回数组的 getter 同样触发这个问题);保存后先刷
运行时再换配置数据,避免卡片区闪空。
2026-08-11 14:39:47 +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
}