入站详情可以测常用 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
+120
View File
@@ -0,0 +1,120 @@
import { describe, expect, test } from 'bun:test'
import {
describeProbeFailure,
findTarget,
isChallenge,
judge,
rayId,
REACH_TARGETS,
} from './reachability.ts'
const openai = REACH_TARGETS[0]!
const gemini = REACH_TARGETS[2]!
const headers = (init: Record<string, string> = {}) => new Headers(init)
describe('REACH_TARGETS', () => {
test('标识不重名,否则接口按 id 取目标会取错', () => {
const ids = REACH_TARGETS.map((t) => t.id)
expect(new Set(ids).size).toBe(ids.length)
})
test('findTarget 认不出来的标识返回 null', () => {
expect(findTarget('openai')?.label).toBe('OpenAI')
expect(findTarget('nonesuch')).toBeNull()
})
})
describe('judge', () => {
test('无鉴权探测拿到 401 算可达,并说明这是预期结果', () => {
const { verdict, message } = judge(openai, 401, headers(), '')
expect(verdict).toBe('reachable')
expect(message).toBe('HTTP 401,目标可达')
})
test('2xx 不需要解释,只报状态码', () => {
expect(judge(gemini, 200, headers(), '{}')).toEqual({
verdict: 'reachable',
message: 'HTTP 200',
})
})
test('白名单之外的状态码算不通', () => {
const { verdict, message } = judge(gemini, 403, headers(), '')
expect(verdict).toBe('unreachable')
expect(message).toBe('HTTP 403,不是预期的状态码')
})
test('429 单独说是限流,不和连不上混在一起', () => {
expect(judge(openai, 429, headers(), '').verdict).toBe('throttled')
})
test('Cloudflare 拦截优先于状态码白名单', () => {
// 429 在 openai 的白名单外,但结论要是 challenge 而不是 throttled:
// 前者要换出口 IP,后者只要等
const { verdict, message } = judge(
openai,
429,
headers({ 'cf-mitigated': 'challenge', 'cf-ray': '8f0a1b2c3d4e5f60-LAX' }),
'',
)
expect(verdict).toBe('challenge')
expect(message).toContain('8f0a1b2c3d4e5f60-LAX')
})
})
describe('isChallenge', () => {
test('只有 403 和 429 才可能是拦截页', () => {
const body = '<html>window._cf_chl_opt</html>'
expect(isChallenge(403, headers(), body)).toBe(true)
expect(isChallenge(429, headers(), body)).toBe(true)
expect(isChallenge(200, headers(), body)).toBe(false)
})
test('没有 cf-mitigated 头时认正文里的标记', () => {
expect(isChallenge(403, headers(), '<title>Just a Moment...</title>')).toBe(true)
})
test('普通的 403 不是拦截页', () => {
expect(isChallenge(403, headers(), '{"error":"forbidden"}')).toBe(false)
})
test('正文只看前 4 KB,标记在那之后不算', () => {
expect(isChallenge(403, headers(), 'x'.repeat(5000) + 'just a moment')).toBe(false)
})
})
describe('rayId', () => {
test('优先取响应头', () => {
expect(rayId(headers({ 'cf-ray': ' abc123-LAX ' }), 'cf-ray: zzz')).toBe('abc123-LAX')
})
test('没有响应头时从正文里找', () => {
expect(rayId(headers(), 'Cloudflare Ray ID: <code>cf-ray: abc123</code>')).toBe('abc123')
})
test('都没有就是空串', () => {
expect(rayId(headers(), 'nothing here')).toBe('')
})
})
describe('describeProbeFailure', () => {
test('超时说等了多久', () => {
const error = new Error('timed out')
error.name = 'TimeoutError'
expect(describeProbeFailure(7890, 10_000, error)).toBe('10 秒内没有响应')
})
test('本机端口没人监听时指向内核,不照抄 Bun 那句指向目标网址的话', () => {
const error = Object.assign(new Error('Unable to connect. Is the computer able to access the url?'), {
code: 'ConnectionRefused',
})
expect(describeProbeFailure(7890, 10_000, error)).toBe('本机 7890 端口没有响应,内核可能没在运行')
})
test('其余原因照原文给出,不猜', () => {
expect(describeProbeFailure(7890, 10_000, new Error('TLS handshake failed'))).toBe(
'连接失败:TLS handshake failed',
)
})
})