按审查结论重构:先补测试网,再搬代码
顺序是刻意的。前端一行测试都没有,直接挪代码挪坏了 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 的测试改走生产路径,之前它自己拼了一遍三步流程,落盘链路漏掉其中一步 测试照样会绿。
This commit is contained in:
+10
-86
@@ -1,46 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ApiError, api, getToken, setToken } from './api'
|
||||
import type { ConfigSnapshot, ProxyInfo } from './api'
|
||||
import { ApiError, api, clearToken, getToken, setToken } from './api'
|
||||
import type { ConfigSnapshot } from './api'
|
||||
import { NAV } from './nav'
|
||||
import { density, toggleDensity } from './prefs'
|
||||
import { kernel, reloadRuntime } from './runtime'
|
||||
import RouterPage from './views/RouterPage.vue'
|
||||
import TokenGate from './views/TokenGate.vue'
|
||||
import IconFold from '~icons/ri/sidebar-fold-line'
|
||||
import IconUnfold from '~icons/ri/sidebar-unfold-line'
|
||||
|
||||
const snapshot = ref<ConfigSnapshot | null>(null)
|
||||
const kernel = ref<{ online: boolean; version?: string }>({ online: false })
|
||||
const providerLines = ref<string[]>([])
|
||||
/** 订阅线路 → 它所属的订阅名。测延迟时要用它选对接口 */
|
||||
const lineProvider = ref<Record<string, string>>({})
|
||||
const runtime = ref<Record<string, ProxyInfo>>({})
|
||||
const needsToken = ref(!getToken())
|
||||
const loadError = ref('')
|
||||
const booted = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const active = ref('router')
|
||||
const active = ref('dashboard')
|
||||
|
||||
/**
|
||||
* 重新读一遍状态。
|
||||
*
|
||||
* 关键在于**不能把页面拆掉重建**:之前每次刷新都会先切到「读取中」,
|
||||
* 于是路由页被卸载、选中的那一路入站丢失,改完一个设置还要重新找回原来的位置。
|
||||
* 所以只有首次加载显示占位,之后都是原地更新。
|
||||
* 关键在于**不能把页面拆掉重建**:之前每次刷新都会先切到「读取中」,于是路由页被卸载、
|
||||
* 选中的那一路入站丢失,改完一个设置还要重新找回原来的位置。所以只有首次加载显示占位,
|
||||
* 之后都是原地更新。
|
||||
*/
|
||||
async function load() {
|
||||
refreshing.value = true
|
||||
loadError.value = ''
|
||||
try {
|
||||
const [config, status] = await Promise.all([api.readConfig(), api.kernel()])
|
||||
snapshot.value = config
|
||||
kernel.value = status
|
||||
snapshot.value = await api.readConfig()
|
||||
needsToken.value = false
|
||||
// 订阅里有哪些线路只有运行中的内核知道,配置文件里只有一个订阅地址。
|
||||
// 拿不到不算错误——界面照常能编辑,只是订阅线路那一档不可选。
|
||||
runtime.value = status.online ? await readRuntime() : {}
|
||||
await reloadRuntime()
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.unauthorized) {
|
||||
clearToken()
|
||||
needsToken.value = true
|
||||
} else {
|
||||
loadError.value = error instanceof Error ? error.message : String(error)
|
||||
@@ -51,69 +44,6 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 出站的运行时状态:协议类型、策略组当前选中了谁、最近一次延迟。
|
||||
*
|
||||
* 要读两处。/proxies 只包含内核全局名字表里的东西——内置出站、自有线路、策略组;
|
||||
* 订阅线路不在那张表上,它们的类型和延迟只能从 /providers/proxies 拿。
|
||||
* 两边合成一张表,界面上就不用关心某个名字是从哪来的了。
|
||||
*
|
||||
* 顺带记下每条订阅线路属于哪个订阅:测它的延迟必须从所属订阅进去。
|
||||
*/
|
||||
async function readRuntime(): Promise<Record<string, ProxyInfo>> {
|
||||
const merged: Record<string, ProxyInfo> = {}
|
||||
const lines: string[] = []
|
||||
const owner: Record<string, string> = {}
|
||||
|
||||
try {
|
||||
Object.assign(merged, (await api.proxies()).proxies)
|
||||
} catch {
|
||||
// 拿不到就当没有,界面照常显示,只是没有延迟数字
|
||||
}
|
||||
|
||||
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 {
|
||||
// 同上
|
||||
}
|
||||
|
||||
providerLines.value = lines
|
||||
lineProvider.value = owner
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换策略组时先在本地改掉,不等内核回话。
|
||||
*
|
||||
* 这一步不写配置文件,失败率极低;而等一个往返再更新会让点击感觉发木。
|
||||
* 真失败了随后的刷新会把状态纠正回来。
|
||||
*/
|
||||
function optimisticSelect(group: string, member: string) {
|
||||
const info = runtime.value[group]
|
||||
if (info) info.now = member
|
||||
}
|
||||
|
||||
/**
|
||||
* 只重读运行时状态,不碰配置文件。
|
||||
*
|
||||
* 切换策略组、测一条线路的延迟,改变的只是内核的运行状态。走完整的 load() 会让服务端
|
||||
* 重读整份配置、重新解析、再跑一遍引用校验,外加两个多余的往返——最便宜的动作触发了
|
||||
* 最贵的操作。
|
||||
*/
|
||||
async function reloadRuntime() {
|
||||
if (!kernel.value.online) return
|
||||
runtime.value = await readRuntime()
|
||||
}
|
||||
|
||||
function submitToken(value: string) {
|
||||
setToken(value)
|
||||
void load()
|
||||
@@ -192,13 +122,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
||||
v-if="active === 'router'"
|
||||
:data="snapshot.data"
|
||||
:revision="snapshot.revision"
|
||||
:kernel-online="kernel.online"
|
||||
:provider-lines="providerLines"
|
||||
:line-provider="lineProvider"
|
||||
:runtime="runtime"
|
||||
@saved="load"
|
||||
@optimistic-select="optimisticSelect"
|
||||
@runtime-changed="reloadRuntime"
|
||||
/>
|
||||
|
||||
<div v-else class="note">
|
||||
|
||||
Reference in New Issue
Block a user