订阅详情可以立刻检查线路
走内核的 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:
+7
-2
@@ -184,8 +184,13 @@ export async function updateGeoDatabases(env: Env): Promise<ValidateResult> {
|
|||||||
* 持续吐 JSON 的分块流(内核源码 hub/route/server.go:380-383),给它们设超时等于
|
* 持续吐 JSON 的分块流(内核源码 hub/route/server.go:380-383),给它们设超时等于
|
||||||
* 每隔一会儿就把连接掐断一次。
|
* 每隔一会儿就把连接掐断一次。
|
||||||
*/
|
*/
|
||||||
export async function proxyGet(env: Env, path: string, stream = false): Promise<Response> {
|
export async function proxyGet(
|
||||||
return callApi(env, path, stream ? {} : { signal: AbortSignal.timeout(15_000) })
|
env: Env,
|
||||||
|
path: string,
|
||||||
|
stream = false,
|
||||||
|
timeoutMs = 15_000,
|
||||||
|
): Promise<Response> {
|
||||||
|
return callApi(env, path, stream ? {} : { signal: AbortSignal.timeout(timeoutMs) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 透传一个写操作。目前只有断开连接和刷新订阅两处。 */
|
/** 透传一个写操作。目前只有断开连接和刷新订阅两处。 */
|
||||||
|
|||||||
+9
-1
@@ -366,7 +366,15 @@ function makeRoute(env: Env, token: string) {
|
|||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return json({ message: `不允许访问该路径:${target}` }, 403)
|
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, {
|
return new Response(upstream.body, {
|
||||||
status: upstream.status,
|
status: upstream.status,
|
||||||
headers: { 'Content-Type': upstream.headers.get('Content-Type') ?? 'application/json' },
|
headers: { 'Content-Type': upstream.headers.get('Content-Type') ?? 'application/json' },
|
||||||
|
|||||||
+13
@@ -310,6 +310,19 @@ export const api = {
|
|||||||
refreshProvider: (name: string) =>
|
refreshProvider: (name: string) =>
|
||||||
request<unknown>('PUT', `/api/kernel/providers/proxies/${encodeURIComponent(name)}`),
|
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`,
|
||||||
|
),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每个规则组的运行时状态:装了多少条规则、上次什么时候更新。
|
* 每个规则组的运行时状态:装了多少条规则、上次什么时候更新。
|
||||||
* 配置文件里只有声明,这两个数字只有运行中的内核知道。
|
* 配置文件里只有声明,这两个数字只有运行中的内核知道。
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ const formEditing = ref<Subscription | null>(null)
|
|||||||
const removing = ref<Subscription | null>(null)
|
const removing = ref<Subscription | null>(null)
|
||||||
/** 正在等内核重新拉取的订阅名 */
|
/** 正在等内核重新拉取的订阅名 */
|
||||||
const refreshing = ref<string | 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)
|
const refreshingAll = ref(false)
|
||||||
|
|
||||||
/** 挨个让内核重新拉取所有订阅。失败的点名,成功的不逐个报。 */
|
/** 挨个让内核重新拉取所有订阅。失败的点名,成功的不逐个报。 */
|
||||||
@@ -309,10 +334,12 @@ async function refreshAll() {
|
|||||||
:info="providerInfo[selected.name]"
|
:info="providerInfo[selected.name]"
|
||||||
:online="kernel.online"
|
:online="kernel.online"
|
||||||
:refreshing="refreshing === selected.name"
|
:refreshing="refreshing === selected.name"
|
||||||
|
:checking="checking === selected.name"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
@edit="openEdit"
|
@edit="openEdit"
|
||||||
@remove="askRemove"
|
@remove="askRemove"
|
||||||
@refresh="refresh"
|
@refresh="refresh"
|
||||||
|
@check="check"
|
||||||
@toggle-health-check="toggleHealthCheck"
|
@toggle-health-check="toggleHealthCheck"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import IconDelete from '~icons/ri/delete-bin-line'
|
|||||||
import IconRefresh from '~icons/ri/refresh-line'
|
import IconRefresh from '~icons/ri/refresh-line'
|
||||||
import IconOffline from '~icons/ri/cloud-off-line'
|
import IconOffline from '~icons/ri/cloud-off-line'
|
||||||
import IconPulse from '~icons/ri/pulse-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<{
|
const props = defineProps<{
|
||||||
subscription: Subscription
|
subscription: Subscription
|
||||||
@@ -25,6 +27,8 @@ const props = defineProps<{
|
|||||||
online: boolean
|
online: boolean
|
||||||
/** 正在等内核重新拉取 */
|
/** 正在等内核重新拉取 */
|
||||||
refreshing: boolean
|
refreshing: boolean
|
||||||
|
/** 正在等内核测完这个订阅的线路 */
|
||||||
|
checking: boolean
|
||||||
/** 配置正在保存 */
|
/** 配置正在保存 */
|
||||||
saving: boolean
|
saving: boolean
|
||||||
}>()
|
}>()
|
||||||
@@ -34,6 +38,8 @@ const emit = defineEmits<{
|
|||||||
remove: []
|
remove: []
|
||||||
/** 让内核立刻重新拉取。运行时动作,不写配置文件 */
|
/** 让内核立刻重新拉取。运行时动作,不写配置文件 */
|
||||||
refresh: []
|
refresh: []
|
||||||
|
/** 让内核立刻测一遍这个订阅的线路。运行时动作,不写配置文件 */
|
||||||
|
check: []
|
||||||
/** 开关健康检查。写配置文件,走保存链路 */
|
/** 开关健康检查。写配置文件,走保存链路 */
|
||||||
toggleHealthCheck: []
|
toggleHealthCheck: []
|
||||||
}>()
|
}>()
|
||||||
@@ -86,7 +92,19 @@ function delayClass(ms: number | null): string {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="block">
|
<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-if="!online" class="hint"><IconOffline /> 内核未连接</p>
|
||||||
<p v-else-if="lines.length === 0" class="hint">这个订阅现在没有线路</p>
|
<p v-else-if="lines.length === 0" class="hint">这个订阅现在没有线路</p>
|
||||||
@@ -195,6 +213,8 @@ function delayClass(ms: number | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
font-size: var(--t-sm);
|
font-size: var(--t-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--c-text);
|
color: var(--c-text);
|
||||||
@@ -202,6 +222,38 @@ h3 {
|
|||||||
margin-bottom: var(--s-3);
|
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 {
|
.hint {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user