入站详情可以测常用 AI 服务的可访问性
release / release (push) Successful in 29s

判定按状态码白名单而不是只认 200:探测不带鉴权,OpenAI 回 401、Anthropic 回 405
才是正常应答,说明 TLS 握手、路由、目标服务全都正常。清单和白名单照 sub2api 的
代理质量检查抄来。

失败文案不照抄 Bun:本机端口没人监听时它给的是「Unable to connect. Is the
computer able to access the url?」,把矛头指向目标网址,而真正的原因在本机。

对话框留在 InboundDetail 而不是提到 RouterPage——它不写配置文件也不改内核状态,
不需要走那条 commit 链路。firstPort 挪进 shared/inbounds.ts,两个页面都要用它了。
This commit is contained in:
2026-08-31 03:59:08 +08:00
parent 7d2a3afff0
commit 3d7304dd22
12 changed files with 716 additions and 34 deletions
+33
View File
@@ -11,6 +11,7 @@
* 两半的反馈方式必须不一样。它们长得一样的时候,用户分不清哪个动作要等、哪个不用。
*/
import { computed, nextTick, ref, watch } from 'vue'
import { firstPort } from '@shared/inbounds.ts'
import type { Inbound } from '@shared/inbounds.ts'
import {
autoGroupName,
@@ -29,8 +30,10 @@ import { isChainedLine, kernel, linesFrom, membersOf, providers, runtime } from
import { mergeMemberPreview } from '../member-preview'
import type { MemberPreview } from '../member-preview'
import OutboundCard from '../components/OutboundCard.vue'
import ReachDialog from '../components/ReachDialog.vue'
import SourcePicker from '../components/SourcePicker.vue'
import IconEdit from '~icons/ri/edit-line'
import IconReach from '~icons/ri/test-tube-line'
import IconDelete from '~icons/ri/delete-bin-line'
import IconUpgrade from '~icons/ri/arrow-up-circle-line'
import IconRight from '~icons/ri/arrow-right-s-line'
@@ -96,6 +99,24 @@ const subMatchAt = computed(() => firstMatchIndex(props.subRules))
/** legacy 入站由配置顶层的端口字段声明,没有 proxy 字段可写,也不能删,只能升级。 */
const locked = computed(() => props.inbound.isDefault)
/**
* 可访问性测试走本机的哪个端口,测不了就是 null。
*
* 服务端拿这个端口当 HTTP 代理去访问目标,所以只有说 HTTP 的入站可以测;
* 纯 socks 端口这条路走不通(同公网 IP 检测,server/host.ts)。
*/
const reachPort = computed(() =>
props.inbound.type === 'http' || props.inbound.type === 'mixed'
? firstPort(props.inbound.port)
: null,
)
/**
* 对话框留在这里而不是像别的对话框那样提到 RouterPage:它不写配置文件也不改内核状态,
* 不需要经过父组件那条 commit 链路,提上去只会多出一组只为传端口而存在的 props。
*/
const reachOpen = ref(false)
const activeTab = ref('')
watch(
() => props.panel.groups,
@@ -460,6 +481,9 @@ const complete = computed(
<span class="port mono">{{ inbound.port }}</span>
<span class="name">{{ inbound.name }}</span>
<div class="acts">
<button v-if="reachPort !== null" type="button" @click="reachOpen = true">
<IconReach /> 测试
</button>
<template v-if="!locked">
<button type="button" @click="emit('edit')"><IconEdit /> 编辑</button>
<button type="button" class="danger" @click="emit('remove')">
@@ -802,6 +826,15 @@ const complete = computed(
</template>
</div>
</Transition>
<!-- 放在 Transition 外面:切换入站时详情整块换掉,对话框不该跟着淡入淡出 -->
<ReachDialog
v-if="reachPort !== null"
:open="reachOpen"
:name="inbound.name"
:port="reachPort"
@close="reachOpen = false"
/>
</aside>
</template>