/** * 内核的运行时状态:有哪些出站、每个是什么协议、策略组当前选中谁、最近一次延迟。 * * 这些数据配置文件里没有,只有运行中的内核知道;而且界面上有三层组件要用它。 * 原本它住在根组件里靠 props 一层层往下传,中间有两层完全不碰它、纯粹为了转发而 * 声明了参数。做成模块级单例之后那两层就不用再知道它的存在了。 * * 用的是 web/prefs.ts 已有的模式(模块级 ref),不是新抽象。 */ import { ref } from 'vue' import { api } from './api' import type { ProxyInfo } from './api' /** 出站名字 → 运行时信息。内核没连接时是空的。 */ export const runtime = ref>({}) /** 订阅带来的线路,按订阅返回的顺序。 */ export const providerLines = ref([]) /** 订阅线路 → 它所属的订阅名。测它的延迟必须从所属订阅进去。 */ export const lineProvider = ref>({}) export const kernel = ref<{ online: boolean; version?: string }>({ online: false }) /** * 重新读一遍运行时状态。 * * 要读两处:`/proxies` 只包含内核全局名字表里的东西(内置出站、自有线路、策略组), * 订阅线路不在那张表上,它们的协议和延迟只能从 `/providers/proxies` 拿。合成一张表 * 之后,界面就不用关心某个名字是从哪来的了。 * * 任何一处取不到都不算错误——界面照常显示,只是少了延迟数字。内核没起来的时候正是 * 最需要这个界面的时候。 */ export async function reloadRuntime(): Promise { kernel.value = await api.kernel().catch(() => ({ online: false })) if (!kernel.value.online) { runtime.value = {} providerLines.value = [] lineProvider.value = {} return } const merged: Record = {} try { Object.assign(merged, (await api.proxies()).proxies) } catch { // 拿不到就当没有 } const lines: string[] = [] const owner: Record = {} try { const { providers } = await api.providers() for (const [name, provider] of Object.entries(providers)) { // 内核会给每个用了 proxies 的策略组造一个内部 provider,那些不是订阅 if (provider.vehicleType === 'Compatible') continue for (const line of provider.proxies) { merged[line.name] = line lines.push(line.name) owner[line.name] = name } } } catch { // 同上 } runtime.value = merged providerLines.value = lines lineProvider.value = owner } /** * 切换策略组选中谁时先在本地改掉,不等内核回话。 * * 这一步不写配置文件、失败率极低,而等一个往返再更新会让点击感觉发木。 * 真失败了随后的刷新会把状态纠正回来。 */ export function optimisticSelect(group: string, member: string): void { const node = runtime.value[group] if (node) node.now = member }