fix(user-center): fallback legacy agent node endpoint
This commit is contained in:
parent
41760a0227
commit
d8d95a14d3
@ -14,28 +14,7 @@ import {
|
||||
VlessNode,
|
||||
VlessTransport,
|
||||
} from '../lib/vless'
|
||||
|
||||
async function fetcher(url: string): Promise<VlessNode[]> {
|
||||
const res = await fetch(url, { credentials: 'include', cache: 'no-store' })
|
||||
const payload = await res.json().catch(() => null)
|
||||
|
||||
if (!res.ok) {
|
||||
const message =
|
||||
(payload && typeof payload.message === 'string' && payload.message) ||
|
||||
(payload && typeof payload.error === 'string' && payload.error) ||
|
||||
`Request failed (${res.status})`
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
return payload as VlessNode[]
|
||||
}
|
||||
if (payload && Array.isArray((payload as { nodes?: unknown }).nodes)) {
|
||||
return (payload as { nodes: VlessNode[] }).nodes
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
import { fetchAgentNodes } from '../lib/fetchAgentNodes'
|
||||
|
||||
export type VlessQrCopy = {
|
||||
label: string
|
||||
@ -65,7 +44,7 @@ export default function VlessQrCard({
|
||||
allowSandboxFallbackNode = false,
|
||||
boundNodeAddress,
|
||||
}: VlessQrCardProps) {
|
||||
const { data: allNodes, error: nodesError } = useSWR<VlessNode[]>('/api/agent-server/v1/nodes', fetcher)
|
||||
const { data: allNodes, error: nodesError } = useSWR<VlessNode[]>('user-center-agent-nodes', fetchAgentNodes)
|
||||
|
||||
const nodes = useMemo(() => {
|
||||
return (allNodes ?? []).filter((node) => {
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { fetchAgentNodes } from './fetchAgentNodes'
|
||||
|
||||
describe('fetchAgentNodes', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('uses the primary endpoint when it returns a node array', async () => {
|
||||
const fetchMock = vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
||||
new Response(JSON.stringify([{ name: 'JP', address: 'jp-xhttp.svc.plus' }]), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(fetchAgentNodes()).resolves.toEqual([{ name: 'JP', address: 'jp-xhttp.svc.plus' }])
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/agent-server/v1/nodes',
|
||||
expect.objectContaining({
|
||||
cache: 'no-store',
|
||||
credentials: 'include',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to the legacy endpoint when the primary route is unavailable', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ error: 'not_found' }), {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify([{ name: 'US', address: 'us-xhttp.svc.plus' }]), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(fetchAgentNodes()).resolves.toEqual([{ name: 'US', address: 'us-xhttp.svc.plus' }])
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
expect(fetchMock.mock.calls[1]?.[0]).toBe('/api/agent/nodes')
|
||||
})
|
||||
|
||||
it('falls back when the primary route returns an unexpected success payload', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(global, 'fetch')
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ nodes: [{ name: 'HK', address: 'hk-xhttp.svc.plus' }] }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(fetchAgentNodes()).resolves.toEqual([{ name: 'HK', address: 'hk-xhttp.svc.plus' }])
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
expect(fetchMock.mock.calls[1]?.[0]).toBe('/api/agent/nodes')
|
||||
})
|
||||
|
||||
it('preserves non-fallback errors from the primary endpoint', async () => {
|
||||
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ error: 'invalid_session' }), {
|
||||
status: 401,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await expect(fetchAgentNodes()).rejects.toThrow('invalid_session')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
|
||||
import type { VlessNode } from './vless'
|
||||
|
||||
const PRIMARY_ENDPOINT = '/api/agent-server/v1/nodes'
|
||||
const FALLBACK_ENDPOINT = '/api/agent/nodes'
|
||||
|
||||
type AgentNodePayload = {
|
||||
nodes?: unknown
|
||||
message?: unknown
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
type AgentNodesError = Error & {
|
||||
status?: number
|
||||
}
|
||||
|
||||
function extractMessage(payload: AgentNodePayload | null, status: number): string {
|
||||
if (payload && typeof payload.message === 'string' && payload.message.trim().length > 0) {
|
||||
return payload.message
|
||||
}
|
||||
if (payload && typeof payload.error === 'string' && payload.error.trim().length > 0) {
|
||||
return payload.error
|
||||
}
|
||||
return `Request failed (${status})`
|
||||
}
|
||||
|
||||
async function requestAgentNodes(url: string): Promise<VlessNode[]> {
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as AgentNodePayload | VlessNode[] | null
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(extractMessage(Array.isArray(payload) ? null : payload, response.status)) as AgentNodesError
|
||||
error.status = response.status
|
||||
throw error
|
||||
}
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
return payload as VlessNode[]
|
||||
}
|
||||
if (payload && Array.isArray((payload as AgentNodePayload).nodes)) {
|
||||
return (payload as { nodes: VlessNode[] }).nodes
|
||||
}
|
||||
|
||||
throw new Error('unexpected_agent_nodes_payload')
|
||||
}
|
||||
|
||||
function shouldFallback(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
return true
|
||||
}
|
||||
if ((error as AgentNodesError).status && [404, 405, 502].includes((error as AgentNodesError).status as number)) {
|
||||
return true
|
||||
}
|
||||
return [
|
||||
'unexpected_agent_nodes_payload',
|
||||
'upstream_unreachable',
|
||||
].includes(error.message)
|
||||
}
|
||||
|
||||
export async function fetchAgentNodes(): Promise<VlessNode[]> {
|
||||
try {
|
||||
return await requestAgentNodes(PRIMARY_ENDPOINT)
|
||||
} catch (error) {
|
||||
if (!shouldFallback(error)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return requestAgentNodes(FALLBACK_ENDPOINT)
|
||||
}
|
||||
@ -4,37 +4,11 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import Card from '../../components/Card'
|
||||
import { fetchAgentNodes } from '../../lib/fetchAgentNodes'
|
||||
import type { VlessNode } from '../../lib/vless'
|
||||
|
||||
async function fetcher(url: string): Promise<VlessNode[]> {
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => null)
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
(payload && typeof payload.message === 'string' && payload.message) ||
|
||||
(payload && typeof payload.error === 'string' && payload.error) ||
|
||||
`Request failed (${response.status})`
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
return payload as VlessNode[]
|
||||
}
|
||||
if (payload && Array.isArray((payload as { nodes?: unknown }).nodes)) {
|
||||
return (payload as { nodes: VlessNode[] }).nodes
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export default function SandboxNodeBindingPanel() {
|
||||
const { data: nodes, error, isLoading } = useSWR<VlessNode[]>('/api/agent-server/v1/nodes', fetcher, {
|
||||
const { data: nodes, error, isLoading } = useSWR<VlessNode[]>('user-center-agent-nodes', fetchAgentNodes, {
|
||||
revalidateOnFocus: false,
|
||||
})
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
|
||||
@ -8,6 +8,7 @@ import Breadcrumbs from '@/app/panel/components/Breadcrumbs'
|
||||
import { useLanguage } from '@i18n/LanguageProvider'
|
||||
import { translations } from '@i18n/translations'
|
||||
import { useUserStore } from '@lib/userStore'
|
||||
import { fetchAgentNodes } from '../lib/fetchAgentNodes'
|
||||
import { fetchSandboxNodeBinding } from '../lib/sandboxNodeBinding'
|
||||
|
||||
|
||||
@ -35,33 +36,11 @@ function isDisplayableNode(node: VlessNode): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
async function fetcher(url: string): Promise<VlessNode[]> {
|
||||
const res = await fetch(url, { credentials: 'include', cache: 'no-store' })
|
||||
|
||||
const payload = await res.json().catch(() => null)
|
||||
if (!res.ok) {
|
||||
const message =
|
||||
(payload && typeof payload.message === 'string' && payload.message) ||
|
||||
(payload && typeof payload.error === 'string' && payload.error) ||
|
||||
`Request failed (${res.status})`
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
return payload as VlessNode[]
|
||||
}
|
||||
if (payload && Array.isArray((payload as { nodes?: unknown }).nodes)) {
|
||||
return (payload as { nodes: VlessNode[] }).nodes
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export default function UserCenterAgentRoute() {
|
||||
const { language } = useLanguage()
|
||||
const t = translations[language].userCenter
|
||||
const user = useUserStore((state) => state.user)
|
||||
const { data: nodes, error, isLoading, mutate } = useSWR<VlessNode[]>('/api/agent-server/v1/nodes', fetcher)
|
||||
const { data: nodes, error, isLoading, mutate } = useSWR<VlessNode[]>('user-center-agent-nodes', fetchAgentNodes)
|
||||
const [boundNode, setBoundNode] = useState<VlessNode | null>(null)
|
||||
const normalizedEmail = user?.email?.toLowerCase() ?? ''
|
||||
const isGuestSandboxReadOnly = Boolean(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user