上半部分收敛成一条规则:草稿和已保存一致时显示内核现状;不一致时显示 按草稿保存后的样子,将移除的画删除线、新增的虚线描边,即将被移除的当前 选中用危险色警示。点击语义只看卡片:现在就存在的成员点了立刻生效,保存 后才加入的点了是预选,保存成功后再作为运行时动作选上。预览的集合运算在 web/member-preview.ts,组件里只留接线。 其余:订阅选择器重写成可搜索多选(原平铺标签订阅一多就是一面墙);删除 入站换成页面内确认对话框;列表右列解析到最终出口并标注链式;「高级配置」 更名「配置」;表单重置改按内容比较(按引用的话任何一次运行时刷新都会冲掉 未保存的草稿,watch 传返回数组的 getter 同样触发这个问题);保存后先刷 运行时再换配置数据,避免卡片区闪空。
164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
/**
|
||
* 内核的运行时状态:有哪些出站、每个是什么协议、策略组当前选中谁、最近一次延迟。
|
||
*
|
||
* 这些数据配置文件里没有,只有运行中的内核知道;而且界面上有三层组件要用它。
|
||
* 原本它住在根组件里靠 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
|
||
}
|