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

走内核的 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
+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;