顺序是刻意的。前端一行测试都没有,直接挪代码挪坏了 bun test 不会红,所以先把纯逻辑 抽到能被测试的位置并补上用例,最后才动组件。测试从 37 条增加到 77 条。 抽出来的四块: - shared/outbound-chain.ts 「这个端口现在走哪条线」的链路解析,带防环,9 条测试 - web/delay-test.ts 延迟测试失败时说什么,6 个分支原本一条测试都没有 - web/runtime.ts 运行时状态的模块级单例,照 prefs.ts 已有的模式 - web/components/InboundList.vue 入站列表 出站的文本原本散成四份(读一份、显示一份、写回一份、表单又抄一份),加一种出站模式 要同时改四处,漏一个就是显示与写回不一致。合并成 OUTBOUND_PRESETS 一张表。 给认证和透传白名单补了 20 条测试。两者都是安全边界、都是纯函数、都不需要内核在线, 之前却只靠需要活内核的冒烟脚本间接覆盖。其中「形似但不是健康检查的路径」几条确认了 正则没写松。 另外:配置快照不再把整份文件原文下发(前端从不读,白白让响应体积翻倍); yaml-cosmetics 的测试改走生产路径,之前它自己拼了一遍三步流程,落盘链路漏掉其中一步 测试照样会绿。
83 lines
3.0 KiB
TypeScript
83 lines
3.0 KiB
TypeScript
/**
|
|
* 内核的运行时状态:有哪些出站、每个是什么协议、策略组当前选中谁、最近一次延迟。
|
|
*
|
|
* 这些数据配置文件里没有,只有运行中的内核知道;而且界面上有三层组件要用它。
|
|
* 原本它住在根组件里靠 props 一层层往下传,中间有两层完全不碰它、纯粹为了转发而
|
|
* 声明了参数。做成模块级单例之后那两层就不用再知道它的存在了。
|
|
*
|
|
* 用的是 web/prefs.ts 已有的模式(模块级 ref),不是新抽象。
|
|
*/
|
|
import { ref } from 'vue'
|
|
import { api } from './api'
|
|
import type { ProxyInfo } from './api'
|
|
|
|
/** 出站名字 → 运行时信息。内核没连接时是空的。 */
|
|
export const runtime = 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 = {}
|
|
providerLines.value = []
|
|
lineProvider.value = {}
|
|
return
|
|
}
|
|
|
|
const merged: Record<string, ProxyInfo> = {}
|
|
try {
|
|
Object.assign(merged, (await api.proxies()).proxies)
|
|
} catch {
|
|
// 拿不到就当没有
|
|
}
|
|
|
|
const lines: string[] = []
|
|
const owner: Record<string, string> = {}
|
|
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
|
|
}
|