diff --git a/server/routes.ts b/server/routes.ts index 778891c..4496682 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -16,6 +16,12 @@ import { tokenFromRequest, tokenMatches } from './auth.ts' import type { ConfigObject } from '../vendor/config-editor/index.ts' import type { Env } from './env.ts' +/** + * 延迟测试。名字里可能有斜杠和中文,所以按前缀匹配而不是列进上面那个精确白名单。 + * 它是 GET 但会真的发起网络连接,属于「读」里比较重的一类,单独列出来提醒这一点。 + */ +const DELAY_PATH = /^\/(proxies|group)\/.+\/delay$/ + /** 允许透传的管理接口路径。白名单而不是黑名单——多开一个口子要有意为之。 */ const READABLE_KERNEL_PATHS = new Set([ '/version', @@ -118,7 +124,7 @@ export function createRouter(env: Env, token: string) { if (path.startsWith('/api/kernel/') && req.method === 'GET') { const target = path.slice('/api/kernel'.length) - if (!READABLE_KERNEL_PATHS.has(target)) { + if (!READABLE_KERNEL_PATHS.has(target) && !DELAY_PATH.test(target)) { return json({ error: `不允许透传这个路径:${target}` }, 403) } const upstream = await proxyGet(env, target + url.search) diff --git a/web/App.vue b/web/App.vue index 7a61cc5..11c561f 100644 --- a/web/App.vue +++ b/web/App.vue @@ -15,11 +15,19 @@ const providerLines = ref([]) const runtime = ref>({}) const needsToken = ref(!getToken()) const loadError = ref('') -const loading = ref(false) +const booted = ref(false) +const refreshing = ref(false) const active = ref('router') +/** + * 重新读一遍状态。 + * + * 关键在于**不能把页面拆掉重建**:之前每次刷新都会先切到「读取中」, + * 于是路由页被卸载、选中的那一路入站丢失,改完一个设置还要重新找回原来的位置。 + * 所以只有首次加载显示占位,之后都是原地更新。 + */ async function load() { - loading.value = true + refreshing.value = true loadError.value = '' try { const [config, status] = await Promise.all([api.readConfig(), api.kernel()]) @@ -37,7 +45,8 @@ async function load() { loadError.value = error instanceof Error ? error.message : String(error) } } finally { - loading.value = false + refreshing.value = false + booted.value = true } } @@ -62,6 +71,17 @@ async function readRuntime(): Promise> { } } +/** + * 切换策略组时先在本地改掉,不等内核回话。 + * + * 这一步不写配置文件,失败率极低;而等一个往返再更新会让点击感觉发木。 + * 真失败了随后的刷新会把状态纠正回来。 + */ +function optimisticSelect(group: string, member: string) { + const info = runtime.value[group] + if (info) info.now = member +} + function submitToken(value: string) { setToken(value) void load() @@ -119,7 +139,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
-

读取中

+

读取中

读不到配置

@@ -144,6 +164,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value)) :provider-lines="providerLines" :runtime="runtime" @changed="load" + @optimistic-select="optimisticSelect" />
diff --git a/web/api.ts b/web/api.ts index 3bd852a..0ddc688 100644 --- a/web/api.ts +++ b/web/api.ts @@ -8,6 +8,9 @@ import type { ConfigDiagnostic, ConfigObject } from '../vendor/config-editor/ind const TOKEN_KEY = 'patchbay.token' +/** 测延迟用的目标。204 空响应,体积最小,全球都有部署。 */ +const TEST_URL = 'https://www.gstatic.com/generate_204' + export function getToken(): string { return localStorage.getItem(TOKEN_KEY) ?? '' } @@ -134,6 +137,20 @@ export const api = { name: member, }), + /** 测一条线路的延迟。返回的是这一次的结果,同时也会被记进内核的历史里。 */ + testLine: (name: string, url = TEST_URL, timeout = 5000) => + request<{ delay: number }>( + 'GET', + `/api/kernel/proxies/${encodeURIComponent(name)}/delay?url=${encodeURIComponent(url)}&timeout=${String(timeout)}`, + ), + + /** 测一整个策略组,内核会并发测所有成员。返回每个成员的延迟。 */ + testGroup: (group: string, url = TEST_URL, timeout = 5000) => + request>( + 'GET', + `/api/kernel/group/${encodeURIComponent(group)}/delay?url=${encodeURIComponent(url)}&timeout=${String(timeout)}`, + ), + /** 订阅里当前有哪些线路。配置文件给不出这个答案,只能问运行中的内核。 */ providers: () => request<{ providers: Record }>( diff --git a/web/views/InboundDetail.vue b/web/views/InboundDetail.vue index 24182e6..72e4194 100644 --- a/web/views/InboundDetail.vue +++ b/web/views/InboundDetail.vue @@ -6,7 +6,7 @@ import type { ProxyInfo } from '../api' import IconOffline from '~icons/ri/cloud-off-line' import IconEdit from '~icons/ri/edit-line' import IconDelete from '~icons/ri/delete-bin-line' -import IconConvert from '~icons/ri/swap-line' +import IconFlash from '~icons/ri/flashlight-line' const props = defineProps<{ inbound: Inbound @@ -19,14 +19,16 @@ const props = defineProps<{ runtime: Record kernelOnline: boolean saving: boolean + /** 正在测延迟 */ + testing: boolean }>() const emit = defineEmits<{ pick: [mode: OutboundMode] edit: [] remove: [] - convert: [] select: [group: string, member: string] + test: [group: string] }>() /** @@ -175,7 +177,18 @@ function delayClass(ms: number | null): string {