订阅线路的类型、延迟和测速都改走 provider 接口
404 不是 bug。内核的 /proxies 只返回全局名字表(内置出站、自有线路、策略组),
订阅线路不在里面——跟「订阅线路不能被入站直接引用」是同一条约束,我没想到它同样
适用于运行时接口。所以 /proxies/{订阅线路}/delay 一律 404,而订阅线路的卡片也一直
没有协议和延迟,因为那些数据同样查不到。
现在运行时状态从两处合成:/proxies 给内置出站和策略组,/providers/proxies 给订阅线路
(它返回的线路本来就带完整的协议和延迟历史)。测速按线路归属选接口,订阅线路走
{订阅}/{线路}/healthcheck。实测该接口对同一条线路返回 200 和 325 毫秒。
This commit is contained in:
+11
-1
@@ -22,6 +22,14 @@ import type { Env } from './env.ts'
|
|||||||
*/
|
*/
|
||||||
const DELAY_PATH = /^\/(proxies|group)\/.+\/delay$/
|
const DELAY_PATH = /^\/(proxies|group)\/.+\/delay$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅线路的健康检查。
|
||||||
|
*
|
||||||
|
* 订阅线路不在内核的全局名字表里,所以 /proxies/{名字}/delay 对它们一律 404——
|
||||||
|
* 要测只能从它所属的订阅进去。整条订阅测用 {订阅}/healthcheck,单条用 {订阅}/{线路}/healthcheck。
|
||||||
|
*/
|
||||||
|
const HEALTHCHECK_PATH = /^\/providers\/proxies\/[^/]+(\/[^/]+)?\/healthcheck$/
|
||||||
|
|
||||||
/** 允许透传的管理接口路径。白名单而不是黑名单——多开一个口子要有意为之。 */
|
/** 允许透传的管理接口路径。白名单而不是黑名单——多开一个口子要有意为之。 */
|
||||||
const READABLE_KERNEL_PATHS = new Set([
|
const READABLE_KERNEL_PATHS = new Set([
|
||||||
'/version',
|
'/version',
|
||||||
@@ -124,7 +132,9 @@ export function createRouter(env: Env, token: string) {
|
|||||||
|
|
||||||
if (path.startsWith('/api/kernel/') && req.method === 'GET') {
|
if (path.startsWith('/api/kernel/') && req.method === 'GET') {
|
||||||
const target = path.slice('/api/kernel'.length)
|
const target = path.slice('/api/kernel'.length)
|
||||||
if (!READABLE_KERNEL_PATHS.has(target) && !DELAY_PATH.test(target)) {
|
const allowed =
|
||||||
|
READABLE_KERNEL_PATHS.has(target) || DELAY_PATH.test(target) || HEALTHCHECK_PATH.test(target)
|
||||||
|
if (!allowed) {
|
||||||
return json({ error: `不允许透传这个路径:${target}` }, 403)
|
return json({ error: `不允许透传这个路径:${target}` }, 403)
|
||||||
}
|
}
|
||||||
const upstream = await proxyGet(env, target + url.search)
|
const upstream = await proxyGet(env, target + url.search)
|
||||||
|
|||||||
+37
-16
@@ -12,6 +12,8 @@ import IconUnfold from '~icons/ri/sidebar-unfold-line'
|
|||||||
const snapshot = ref<ConfigSnapshot | null>(null)
|
const snapshot = ref<ConfigSnapshot | null>(null)
|
||||||
const kernel = ref<{ online: boolean; version?: string }>({ online: false })
|
const kernel = ref<{ online: boolean; version?: string }>({ online: false })
|
||||||
const providerLines = ref<string[]>([])
|
const providerLines = ref<string[]>([])
|
||||||
|
/** 订阅线路 → 它所属的订阅名。测延迟时要用它选对接口 */
|
||||||
|
const lineProvider = ref<Record<string, string>>({})
|
||||||
const runtime = ref<Record<string, ProxyInfo>>({})
|
const runtime = ref<Record<string, ProxyInfo>>({})
|
||||||
const needsToken = ref(!getToken())
|
const needsToken = ref(!getToken())
|
||||||
const loadError = ref('')
|
const loadError = ref('')
|
||||||
@@ -36,7 +38,6 @@ async function load() {
|
|||||||
needsToken.value = false
|
needsToken.value = false
|
||||||
// 订阅里有哪些线路只有运行中的内核知道,配置文件里只有一个订阅地址。
|
// 订阅里有哪些线路只有运行中的内核知道,配置文件里只有一个订阅地址。
|
||||||
// 拿不到不算错误——界面照常能编辑,只是订阅线路那一档不可选。
|
// 拿不到不算错误——界面照常能编辑,只是订阅线路那一档不可选。
|
||||||
providerLines.value = status.online ? await readProviderLines() : []
|
|
||||||
runtime.value = status.online ? await readRuntime() : {}
|
runtime.value = status.online ? await readRuntime() : {}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiError && error.unauthorized) {
|
if (error instanceof ApiError && error.unauthorized) {
|
||||||
@@ -50,25 +51,44 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 只取真正的订阅。内核会给每个用了 proxies 的策略组造一个内部 provider,那些不是订阅。 */
|
/**
|
||||||
async function readProviderLines(): Promise<string[]> {
|
* 出站的运行时状态:协议类型、策略组当前选中了谁、最近一次延迟。
|
||||||
|
*
|
||||||
|
* 要读两处。/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 {
|
try {
|
||||||
const { providers } = await api.providers()
|
const { providers } = await api.providers()
|
||||||
return Object.values(providers)
|
for (const [name, provider] of Object.entries(providers)) {
|
||||||
.filter((p) => p.vehicleType !== 'Compatible')
|
// 内核会给每个用了 proxies 的策略组造一个内部 provider,那些不是订阅
|
||||||
.flatMap((p) => p.proxies.map((line) => line.name))
|
if (provider.vehicleType === 'Compatible') continue
|
||||||
} catch {
|
for (const line of provider.proxies) {
|
||||||
return []
|
merged[line.name] = line
|
||||||
|
lines.push(line.name)
|
||||||
|
owner[line.name] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 同上
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** 出站的运行时状态:协议类型、策略组当前选中了谁、最近一次延迟。 */
|
providerLines.value = lines
|
||||||
async function readRuntime(): Promise<Record<string, ProxyInfo>> {
|
lineProvider.value = owner
|
||||||
try {
|
return merged
|
||||||
return (await api.proxies()).proxies
|
|
||||||
} catch {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,6 +182,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value))
|
|||||||
:revision="snapshot.revision"
|
:revision="snapshot.revision"
|
||||||
:kernel-online="kernel.online"
|
:kernel-online="kernel.online"
|
||||||
:provider-lines="providerLines"
|
:provider-lines="providerLines"
|
||||||
|
:line-provider="lineProvider"
|
||||||
:runtime="runtime"
|
:runtime="runtime"
|
||||||
@changed="load"
|
@changed="load"
|
||||||
@optimistic-select="optimisticSelect"
|
@optimistic-select="optimisticSelect"
|
||||||
|
|||||||
+22
-15
@@ -76,6 +76,8 @@ export interface ApplyFailure {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ProxyInfo {
|
export interface ProxyInfo {
|
||||||
|
/** 出站的名字。/proxies 用键名标识,provider 里的线路则带在对象上 */
|
||||||
|
name: string
|
||||||
/** 协议类型(Vmess、Vless…)或策略组类型(Selector、URLTest…) */
|
/** 协议类型(Vmess、Vless…)或策略组类型(Selector、URLTest…) */
|
||||||
type: string
|
type: string
|
||||||
/** 策略组当前选中的成员。不是策略组就没有这个字段 */
|
/** 策略组当前选中的成员。不是策略组就没有这个字段 */
|
||||||
@@ -137,23 +139,28 @@ export const api = {
|
|||||||
name: member,
|
name: member,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/** 测一条线路的延迟。返回的是这一次的结果,同时也会被记进内核的历史里。 */
|
/**
|
||||||
testLine: (name: string, url = TEST_URL, timeout = 5000) =>
|
* 测一条出站的延迟。
|
||||||
request<{ delay: number }>(
|
*
|
||||||
'GET',
|
* 走哪个接口取决于它在不在内核的全局名字表里:策略组和自有线路在表里,用 /proxies;
|
||||||
`/api/kernel/proxies/${encodeURIComponent(name)}/delay?url=${encodeURIComponent(url)}&timeout=${String(timeout)}`,
|
* 订阅线路不在表里(那正是它们无法被入站直接引用的原因),只能从所属订阅进去测。
|
||||||
),
|
*/
|
||||||
|
testLine: (name: string, provider?: string, timeout = 5000) => {
|
||||||
|
const q = `url=${encodeURIComponent(TEST_URL)}&timeout=${String(timeout)}`
|
||||||
|
const path = provider
|
||||||
|
? `/api/kernel/providers/proxies/${encodeURIComponent(provider)}/${encodeURIComponent(name)}/healthcheck?${q}`
|
||||||
|
: `/api/kernel/proxies/${encodeURIComponent(name)}/delay?${q}`
|
||||||
|
return request<{ delay: number }>('GET', path)
|
||||||
|
},
|
||||||
|
|
||||||
/** 测一整个策略组,内核会并发测所有成员。返回每个成员的延迟。 */
|
/**
|
||||||
testGroup: (group: string, url = TEST_URL, timeout = 5000) =>
|
* 订阅及其线路。配置文件里只有订阅地址,有哪些线路只有运行中的内核知道。
|
||||||
request<Record<string, number>>(
|
*
|
||||||
'GET',
|
* 返回的线路带完整的运行时信息(协议、延迟历史),因为这些线路不在 /proxies 那张
|
||||||
`/api/kernel/group/${encodeURIComponent(group)}/delay?url=${encodeURIComponent(url)}&timeout=${String(timeout)}`,
|
* 全局名字表里,这是拿到它们类型和延迟的唯一途径。
|
||||||
),
|
*/
|
||||||
|
|
||||||
/** 订阅里当前有哪些线路。配置文件给不出这个答案,只能问运行中的内核。 */
|
|
||||||
providers: () =>
|
providers: () =>
|
||||||
request<{ providers: Record<string, { vehicleType: string; proxies: { name: string }[] }> }>(
|
request<{ providers: Record<string, { vehicleType: string; proxies: ProxyInfo[] }> }>(
|
||||||
'GET',
|
'GET',
|
||||||
'/api/kernel/providers/proxies',
|
'/api/kernel/providers/proxies',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const props = defineProps<{
|
|||||||
revision: string
|
revision: string
|
||||||
kernelOnline: boolean
|
kernelOnline: boolean
|
||||||
providerLines: string[]
|
providerLines: string[]
|
||||||
|
/** 订阅线路 → 所属订阅 */
|
||||||
|
lineProvider: Record<string, string>
|
||||||
runtime: Record<string, ProxyInfo>
|
runtime: Record<string, ProxyInfo>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -182,6 +184,9 @@ function describeTestFailure(name: string, error: unknown): string {
|
|||||||
|
|
||||||
const detail = String(error.payload.message ?? '')
|
const detail = String(error.payload.message ?? '')
|
||||||
const generic = detail === '' || detail === 'An error occurred in the delay test'
|
const generic = detail === '' || detail === 'An error occurred in the delay test'
|
||||||
|
if (error.status === 404) {
|
||||||
|
return `${name} 不在内核的出站表里,无法测试。订阅线路需要从所属订阅进去测。`
|
||||||
|
}
|
||||||
if (error.status === 503) {
|
if (error.status === 503) {
|
||||||
return generic
|
return generic
|
||||||
? `${name} 无法建立连接,线路可能已失效,或本机到该服务器的网络不通`
|
? `${name} 无法建立连接,线路可能已失效,或本机到该服务器的网络不通`
|
||||||
@@ -195,7 +200,7 @@ async function testLine(name: string) {
|
|||||||
if (testing.value.includes(name)) return
|
if (testing.value.includes(name)) return
|
||||||
testing.value = [...testing.value, name]
|
testing.value = [...testing.value, name]
|
||||||
try {
|
try {
|
||||||
await api.testLine(name, undefined, TEST_TIMEOUT_MS)
|
await api.testLine(name, props.lineProvider[name], TEST_TIMEOUT_MS)
|
||||||
emit('changed')
|
emit('changed')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 测不通是关于线路的结论,不是 Patchbay 出了故障,所以不用错误框
|
// 测不通是关于线路的结论,不是 Patchbay 出了故障,所以不用错误框
|
||||||
|
|||||||
Reference in New Issue
Block a user