Files
im0o bd087aabea 接入六个新页面,外壳加内核状态心跳
导航新增「规则」项;设置、配置、规则页接 saved 事件重读配置,仪表盘只
读传 data。心跳每 10 秒探一次内核在不在:kernel.online 原本只在 load()
里更新,用户停在任何页面期间它都是静态快照,内核重启一次,请求页和
仪表盘会锁死在「内核未连接」,各页等它回来的 watch 永远不可达。恢复
在线的那一次顺带拉新运行时数据。
2026-08-11 21:14:03 +08:00

169 lines
6.6 KiB
TypeScript
Raw Permalink 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 })
/** 只探测内核在不在线,不拉运行时数据。给外壳的定时心跳用,各页等内核回来的 watch 靠它可达。 */
export async function refreshKernel(): Promise<void> {
kernel.value = await api.kernel().catch(() => ({ online: false }))
}
/**
* 重新读一遍运行时状态。
*
* 要读两处:`/proxies` 只包含内核全局名字表里的东西(内置出站、自有线路、策略组),
* 订阅线路不在那张表上,它们的协议和延迟只能从 `/providers/proxies` 拿。
*
* 任何一处取不到都不算错误——界面照常显示,只是少了延迟数字。内核没起来的时候正是
* 最需要这个界面的时候。
*/
export async function reloadRuntime(): Promise<void> {
await refreshKernel()
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
}