订阅详情可以立刻检查线路

走内核的 provider healthcheck:逐条 URLTest,测通与否和延迟一起记进历史
(adapter/provider/healthcheck.go:123-189)。用的是订阅自己的
health-check.url——这个接口不收 url 参数,设置页那个测速地址传不进去。

它是同步的,测完全部线路才回话,一批 10 条、每条最多 5 秒
(healthcheck.go:132、181、201),线路多又都测不通时远超透传默认的
15 秒,所以 proxyGet 的超时改成可传,健康检查这条路径放到 120 秒。
This commit is contained in:
2026-08-14 14:05:24 +08:00
parent 56546b20c5
commit 23aa28fbea
5 changed files with 109 additions and 4 deletions
+7 -2
View File
@@ -184,8 +184,13 @@ export async function updateGeoDatabases(env: Env): Promise<ValidateResult> {
* 持续吐 JSON 的分块流(内核源码 hub/route/server.go:380-383),给它们设超时等于
* 每隔一会儿就把连接掐断一次。
*/
export async function proxyGet(env: Env, path: string, stream = false): Promise<Response> {
return callApi(env, path, stream ? {} : { signal: AbortSignal.timeout(15_000) })
export async function proxyGet(
env: Env,
path: string,
stream = false,
timeoutMs = 15_000,
): Promise<Response> {
return callApi(env, path, stream ? {} : { signal: AbortSignal.timeout(timeoutMs) })
}
/** 透传一个写操作。目前只有断开连接和刷新订阅两处。 */
+9 -1
View File
@@ -366,7 +366,15 @@ function makeRoute(env: Env, token: string) {
if (!allowed) {
return json({ message: `不允许访问该路径:${target}` }, 403)
}
const upstream = await proxyGet(env, target + url.search, STREAM_PATHS.has(target))
// 健康检查是同步的:内核测完才回话,一批 10 条、每条最多 5 秒
// (内核源码 adapter/provider/healthcheck.go:132、181、201),
// 整条订阅测且线路多又都测不通时会远超默认的 15 秒
const upstream = await proxyGet(
env,
target + url.search,
STREAM_PATHS.has(target),
HEALTHCHECK_PATH.test(target) ? 120_000 : undefined,
)
return new Response(upstream.body, {
status: upstream.status,
headers: { 'Content-Type': upstream.headers.get('Content-Type') ?? 'application/json' },
+13
View File
@@ -310,6 +310,19 @@ export const api = {
refreshProvider: (name: string) =>
request<unknown>('PUT', `/api/kernel/providers/proxies/${encodeURIComponent(name)}`),
/**
* 让内核立刻对一个订阅的全部线路做一次健康检查:逐条 URLTest,测通与否和延迟一起
* 记进历史(内核源码 adapter/provider/healthcheck.go:123-189)。
*
* 用的是这个订阅 `health-check.url` 里的地址而不是设置页的测速地址——内核这个接口
* 不收 url 参数。它是同步的,测完全部线路才回话,所以可能要等上几十秒。
*/
checkProviderLines: (name: string) =>
request<unknown>(
'GET',
`/api/kernel/providers/proxies/${encodeURIComponent(name)}/healthcheck`,
),
/**
* 每个规则组的运行时状态:装了多少条规则、上次什么时候更新。
* 配置文件里只有声明,这两个数字只有运行中的内核知道。
+27
View File
@@ -50,6 +50,8 @@ const formEditing = ref<Subscription | null>(null)
const removing = ref<Subscription | null>(null)
/** 正在等内核重新拉取的订阅名 */
const refreshing = ref<string | null>(null)
/** 正在等内核测完线路的订阅名 */
const checking = ref<string | null>(null)
/**
* 每个订阅的运行时数据:当前线路、上次更新时间。
@@ -247,6 +249,29 @@ async function refresh() {
}
}
/**
* 让内核立刻测一遍选中订阅的线路。运行时动作,不写配置文件,所以不走 commit()。
*
* 内核是测完全部线路才回话的,线路多又都测不通时要等上几十秒,期间按钮转着。
*/
async function check() {
const target = selected.value
if (!target || checking.value !== null) return
checking.value = target.name
try {
await api.checkProviderLines(target.name)
await reloadProviders()
toast(`已检查 ${target.name} 的线路`)
} catch (error) {
toast(error instanceof ApiError ? error.message : String(error), {
kind: 'error',
hint: '该操作仅修改内核运行状态,配置文件未被修改。',
})
} finally {
checking.value = null
}
}
const refreshingAll = ref(false)
/** 挨个让内核重新拉取所有订阅。失败的点名,成功的不逐个报。 */
@@ -309,10 +334,12 @@ async function refreshAll() {
:info="providerInfo[selected.name]"
:online="kernel.online"
:refreshing="refreshing === selected.name"
:checking="checking === selected.name"
:saving="saving"
@edit="openEdit"
@remove="askRemove"
@refresh="refresh"
@check="check"
@toggle-health-check="toggleHealthCheck"
/>
</div>
+53 -1
View File
@@ -17,6 +17,8 @@ import IconDelete from '~icons/ri/delete-bin-line'
import IconRefresh from '~icons/ri/refresh-line'
import IconOffline from '~icons/ri/cloud-off-line'
import IconPulse from '~icons/ri/pulse-line'
import IconSync from '~icons/ri/loop-right-line'
import IconLoad from '~icons/ri/loader-4-line'
const props = defineProps<{
subscription: Subscription
@@ -25,6 +27,8 @@ const props = defineProps<{
online: boolean
/** 正在等内核重新拉取 */
refreshing: boolean
/** 正在等内核测完这个订阅的线路 */
checking: boolean
/** 配置正在保存 */
saving: boolean
}>()
@@ -34,6 +38,8 @@ const emit = defineEmits<{
remove: []
/** 让内核立刻重新拉取。运行时动作,不写配置文件 */
refresh: []
/** 让内核立刻测一遍这个订阅的线路。运行时动作,不写配置文件 */
check: []
/** 开关健康检查。写配置文件,走保存链路 */
toggleHealthCheck: []
}>()
@@ -86,7 +92,19 @@ function delayClass(ms: number | null): string {
</header>
<section class="block">
<h3>线路</h3>
<h3>
线路
<button
type="button"
class="check"
:title="checking ? '检查中' : '检查全部线路的延迟'"
:disabled="!online || checking || lines.length === 0"
@click="emit('check')"
>
<IconLoad v-if="checking" class="spin" />
<IconSync v-else />
</button>
</h3>
<p v-if="!online" class="hint"><IconOffline /> 内核未连接</p>
<p v-else-if="lines.length === 0" class="hint">这个订阅现在没有线路</p>
@@ -195,6 +213,8 @@ function delayClass(ms: number | null): string {
}
h3 {
display: flex;
align-items: center;
font-size: var(--t-sm);
font-weight: 600;
color: var(--c-text);
@@ -202,6 +222,38 @@ h3 {
margin-bottom: var(--s-3);
}
.check {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
width: 22px;
height: 22px;
border: none;
border-radius: var(--r-sm);
background: none;
color: var(--c-text-faint);
cursor: pointer;
}
.check:hover:not(:disabled) {
background: var(--c-surface-sunken);
color: var(--c-text);
}
.check:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.spin {
animation: spin 0.9s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.hint {
display: flex;
align-items: center;