diff --git a/web/frps/src/types/client.ts b/web/frps/src/types/client.ts
index 31d846a7..85413ff0 100644
--- a/web/frps/src/types/client.ts
+++ b/web/frps/src/types/client.ts
@@ -4,6 +4,7 @@ export interface ClientInfoData {
clientID: string
runID: string
version?: string
+ wireProtocol?: string
hostname: string
clientIP?: string
metas?: Record
diff --git a/web/frps/src/utils/client.ts b/web/frps/src/utils/client.ts
index d7f95a3f..8d26eeb8 100644
--- a/web/frps/src/utils/client.ts
+++ b/web/frps/src/utils/client.ts
@@ -7,6 +7,7 @@ export class Client {
clientID: string
runID: string
version: string
+ wireProtocol: string
hostname: string
ip: string
metas: Map
@@ -21,6 +22,7 @@ export class Client {
this.clientID = data.clientID
this.runID = data.runID
this.version = data.version || ''
+ this.wireProtocol = data.wireProtocol || ''
this.hostname = data.hostname
this.ip = data.clientIP || ''
this.metas = new Map()
@@ -48,6 +50,11 @@ export class Client {
return this.runID.substring(0, 8)
}
+ get wireProtocolLabel(): string {
+ if (!this.wireProtocol) return ''
+ return `Protocol ${this.wireProtocol}`
+ }
+
get firstConnectedAgo(): string {
return formatDistanceToNow(this.firstConnectedAt)
}
@@ -80,6 +87,7 @@ export class Client {
this.user.toLowerCase().includes(search) ||
this.clientID.toLowerCase().includes(search) ||
this.runID.toLowerCase().includes(search) ||
+ this.wireProtocol.toLowerCase().includes(search) ||
this.hostname.toLowerCase().includes(search)
)
}
diff --git a/web/frps/src/views/ClientDetail.vue b/web/frps/src/views/ClientDetail.vue
index 1468a243..42c9a25e 100644
--- a/web/frps/src/views/ClientDetail.vue
+++ b/web/frps/src/views/ClientDetail.vue
@@ -27,6 +27,9 @@
v{{ client.version }}
+
+ {{ client.wireProtocolLabel }}
+
First Connected
{{ client.firstConnectedAgo }}
diff --git a/web/frps/src/views/Proxies.vue b/web/frps/src/views/Proxies.vue
index 70617292..b7fdc700 100644
--- a/web/frps/src/views/Proxies.vue
+++ b/web/frps/src/views/Proxies.vue
@@ -77,8 +77,9 @@
@@ -129,12 +130,14 @@ const route = useRoute()
const router = useRouter()
const proxyTypes = [
+ { label: 'All', value: 'all' },
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
{ label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' },
{ label: 'TCPMUX', value: 'tcpmux' },
{ label: 'STCP', value: 'stcp' },
+ { label: 'XTCP', value: 'xtcp' },
{ label: 'SUDP', value: 'sudp' },
]
@@ -200,15 +203,48 @@ const filteredProxies = computed(() => {
)
}
- // Filter by search text
+ // Filter by search text across multiple fields
if (searchText.value) {
const search = searchText.value.toLowerCase()
- result = result.filter((p) => p.name.toLowerCase().includes(search))
+ result = result.filter((p) => {
+ const fields: unknown[] = [
+ p.name,
+ p.type,
+ p.clientID,
+ p.user,
+ p.addr,
+ p.port,
+ p.customDomains,
+ p.subdomain,
+ ]
+ return fields.some((v) => matchesSearch(v, search))
+ })
}
return result
})
+// Normalize a field of unknown shape (string / number / array / null) to a
+// lowercase string for case-insensitive substring matching. Arrays are joined
+// so e.g. customDomains: ["A.com","B.com"] is searchable as one blob.
+const matchesSearch = (value: unknown, needle: string): boolean => {
+ if (value === null || value === undefined) return false
+ let str: string
+ if (Array.isArray(value)) {
+ str = value
+ .filter((v) => v !== null && v !== undefined)
+ .map((v) => String(v))
+ .join(' ')
+ } else if (typeof value === 'number') {
+ if (value === 0) return false
+ str = String(value)
+ } else {
+ str = String(value)
+ }
+ if (!str) return false
+ return str.toLowerCase().includes(needle)
+}
+
const onClientFilterChange = (key: string) => {
if (key) {
const client = clientOptions.value.find((c) => c.key === key)
@@ -249,45 +285,88 @@ const fetchServerInfo = async () => {
return serverInfo
}
+const convertProxies = async (
+ type: string,
+ json: any,
+): Promise => {
+ if (type === 'tcp') {
+ return json.proxies.map((p: any) => new TCPProxy(p))
+ }
+ if (type === 'udp') {
+ return json.proxies.map((p: any) => new UDPProxy(p))
+ }
+ if (type === 'http') {
+ const info = await fetchServerInfo()
+ if (info && info.vhostHTTPPort) {
+ return json.proxies.map(
+ (p: any) => new HTTPProxy(p, info.vhostHTTPPort, info.subdomainHost),
+ )
+ }
+ return []
+ }
+ if (type === 'https') {
+ const info = await fetchServerInfo()
+ if (info && info.vhostHTTPSPort) {
+ return json.proxies.map(
+ (p: any) => new HTTPSProxy(p, info.vhostHTTPSPort, info.subdomainHost),
+ )
+ }
+ return []
+ }
+ if (type === 'tcpmux') {
+ const info = await fetchServerInfo()
+ if (info && info.tcpmuxHTTPConnectPort) {
+ return json.proxies.map(
+ (p: any) =>
+ new TCPMuxProxy(p, info.tcpmuxHTTPConnectPort, info.subdomainHost),
+ )
+ }
+ return []
+ }
+ if (type === 'stcp') {
+ return json.proxies.map((p: any) => new STCPProxy(p))
+ }
+ if (type === 'sudp') {
+ return json.proxies.map((p: any) => new SUDPProxy(p))
+ }
+ // Fallback for types without a dedicated class (e.g. xtcp). Matches the
+ // pattern in ProxyDetail.vue so the type tag and meta render correctly.
+ return json.proxies.map((p: any) => {
+ const bp = new BaseProxy(p)
+ bp.type = type
+ return bp
+ })
+}
+
+const allProxyTypes = [
+ 'tcp',
+ 'udp',
+ 'http',
+ 'https',
+ 'tcpmux',
+ 'stcp',
+ 'xtcp',
+ 'sudp',
+]
+
const fetchData = async () => {
loading.value = true
proxies.value = []
try {
const type = activeType.value
- const json = await getProxiesByType(type)
- if (type === 'tcp') {
- proxies.value = json.proxies.map((p: any) => new TCPProxy(p))
- } else if (type === 'udp') {
- proxies.value = json.proxies.map((p: any) => new UDPProxy(p))
- } else if (type === 'http') {
- const info = await fetchServerInfo()
- if (info && info.vhostHTTPPort) {
- proxies.value = json.proxies.map(
- (p: any) => new HTTPProxy(p, info.vhostHTTPPort, info.subdomainHost),
- )
- }
- } else if (type === 'https') {
- const info = await fetchServerInfo()
- if (info && info.vhostHTTPSPort) {
- proxies.value = json.proxies.map(
- (p: any) =>
- new HTTPSProxy(p, info.vhostHTTPSPort, info.subdomainHost),
- )
- }
- } else if (type === 'tcpmux') {
- const info = await fetchServerInfo()
- if (info && info.tcpmuxHTTPConnectPort) {
- proxies.value = json.proxies.map(
- (p: any) =>
- new TCPMuxProxy(p, info.tcpmuxHTTPConnectPort, info.subdomainHost),
- )
- }
- } else if (type === 'stcp') {
- proxies.value = json.proxies.map((p: any) => new STCPProxy(p))
- } else if (type === 'sudp') {
- proxies.value = json.proxies.map((p: any) => new SUDPProxy(p))
+ if (type === 'all') {
+ const results = await Promise.all(
+ allProxyTypes.map(async (t) => {
+ const json = await getProxiesByType(t)
+ return convertProxies(t, json)
+ }),
+ )
+ proxies.value = results.flat()
+ } else {
+ const json = await getProxiesByType(type)
+ proxies.value = await convertProxies(type, json)
}
} catch (error: any) {
ElMessage({
diff --git a/web/package-lock.json b/web/package-lock.json
index bd274637..d9a66bc8 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -5753,9 +5753,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
- "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "version": "8.5.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
+ "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
"funding": [
{
"type": "opencollective",
@@ -5770,7 +5770,6 @@
"url": "https://github.com/sponsors/ai"
}
],
- "license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",