feat(view): Add AnyTLS protocol support and enhance node configuration options

This commit is contained in:
web
2025-07-06 04:56:00 -07:00
parent 9235fef3c6
commit bcfb10a6c7
15 changed files with 900 additions and 580 deletions
@@ -136,14 +136,14 @@ export function Register() {
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<TableRow className='flex flex-col'>
<TableCell className='w-full'>
<Label>{t('trialSubscribePlan')}</Label>
<p className='text-muted-foreground text-xs'>
{t('trialSubscribePlanDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<TableCell className='max-w-96'>
<EnhancedInput
placeholder={t('trialDuration')}
type='number'
+14 -2
View File
@@ -1,6 +1,6 @@
import { z } from 'zod';
export const protocols = ['shadowsocks', 'vmess', 'vless', 'trojan', 'hysteria2', 'tuic'];
export const protocols = ['shadowsocks', 'vmess', 'vless', 'trojan', 'hysteria2', 'tuic', 'anytls'];
const nullableString = z.string().nullish();
const portSchema = z.number().max(65535).nullish();
@@ -58,7 +58,15 @@ const hysteria2Schema = z.object({
const tuicSchema = z.object({
port: portSchema,
security: z.string(),
disable_sni: z.boolean().default(false),
reduce_rtt: z.boolean().default(false),
udp_relay_mode: z.string().default('native'),
congestion_controller: z.string().default('bbr'),
security_config: securityConfigSchema,
});
const anytlsSchema = z.object({
port: portSchema,
security_config: securityConfigSchema,
});
@@ -87,6 +95,10 @@ const protocolConfigSchema = z.discriminatedUnion('protocol', [
protocol: z.literal('tuic'),
config: tuicSchema,
}),
z.object({
protocol: z.literal('anytls'),
config: anytlsSchema,
}),
]);
const baseFormSchema = z.object({
+311 -194
View File
@@ -110,7 +110,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
{trigger}
</Button>
</SheetTrigger>
<SheetContent className='w-[520px] max-w-full md:max-w-screen-md'>
<SheetContent className='w-[580px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
@@ -290,7 +290,7 @@ export default function NodeForm<T extends { [x: string]: any }>({
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
if (['trojan', 'hysteria2', 'tuic'].includes(value)) {
if (['trojan', 'hysteria2', 'tuic', 'anytls'].includes(value)) {
form.setValue('config.security', 'tls');
}
}}
@@ -402,11 +402,11 @@ export default function NodeForm<T extends { [x: string]: any }>({
</div>
)}
{['vmess', 'vless', 'trojan', 'hysteria2', 'tuic'].includes(protocol) && (
{['vmess', 'vless', 'trojan', 'hysteria2', 'tuic', 'anytls'].includes(protocol) && (
<div className='grid gap-4'>
<div
className={cn('flex gap-4 *:flex-1', {
'grid grid-cols-2': ['hysteria2'].includes(protocol),
'grid grid-cols-2': ['hysteria2', 'tuic'].includes(protocol),
})}
>
<FormField
@@ -524,6 +524,109 @@ export default function NodeForm<T extends { [x: string]: any }>({
/>
</>
)}
{protocol === 'tuic' && (
<>
<FormField
control={form.control}
name='config.udp_relay_mode'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.udpRelayMode')}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='native'>Native</SelectItem>
<SelectItem value='quic'>QUIC</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.congestion_controller'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.congestionController')}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='bbr'>BBR</SelectItem>
<SelectItem value='cubic'>Cubic</SelectItem>
<SelectItem value='reno'>Reno</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className='flex gap-2'>
<FormField
control={form.control}
name='config.disable_sni'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.disableSni')}</FormLabel>
<FormControl>
<div className='pt-2'>
<Switch
checked={field.value}
onCheckedChange={(checked) => {
form.setValue(field.name, checked);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.reduce_rtt'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.reduceRtt')}</FormLabel>
<FormControl>
<div className='pt-2'>
<Switch
checked={field.value}
onCheckedChange={(checked) => {
form.setValue(field.name, checked);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</>
)}
</div>
{['vmess', 'vless', 'trojan'].includes(protocol) && (
<Card>
@@ -630,171 +733,17 @@ export default function NodeForm<T extends { [x: string]: any }>({
)}
</Card>
)}
<Card>
<CardHeader className='flex flex-row items-center justify-between p-3'>
<CardTitle>{t('form.securityConfig')}</CardTitle>
<FormField
control={form.control}
name='config.security'
render={({ field }) => (
<FormItem className='!mt-0 min-w-32'>
<Select
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
{['vmess', 'vless'].includes(protocol) && (
<SelectItem value='none'>NONE</SelectItem>
)}
<SelectItem value='tls'>TLS</SelectItem>
{protocol === 'vless' && (
<SelectItem value='reality'>Reality</SelectItem>
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</CardHeader>
{security !== 'none' && (
<CardContent className='grid grid-cols-2 gap-4 p-3'>
<FormField
control={form.control}
name='config.security_config.sni'
render={({ field }) => (
<FormItem>
<FormLabel>Server Name(SNI)</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{protocol === 'vless' && security === 'reality' && (
<>
<FormField
control={form.control}
name='config.security_config.reality_server_addr'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.serverAddress')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t(
'form.security_config.serverAddressPlaceholder',
)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_config.reality_server_port'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.serverPort')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
type='number'
min={1}
max={65535}
placeholder={t('form.security_config.serverPortPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_config.reality_private_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.privateKey')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('form.security_config.privateKeyPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_config.reality_public_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.publicKey')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('form.security_config.publicKeyPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_config.reality_short_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.shortId')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('form.security_config.shortIdPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{protocol === 'vless' && (
{(['vmess', 'vless', 'trojan'].includes(protocol) ||
['anytls', 'tuic', 'hysteria2'].includes(protocol)) && (
<Card>
<CardHeader className='flex flex-row items-center justify-between p-3'>
<CardTitle>{t('form.securityConfig')}</CardTitle>
{['vmess', 'vless', 'trojan'].includes(protocol) && (
<FormField
control={form.control}
name='config.security_config.fingerprint'
name='config.security'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.fingerprint')}</FormLabel>
<FormItem className='!mt-0 min-w-32'>
<Select
value={field.value}
onValueChange={(value) => {
@@ -807,45 +756,213 @@ export default function NodeForm<T extends { [x: string]: any }>({
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='chrome'>Chrome</SelectItem>
<SelectItem value='firefox'>Firefox</SelectItem>
<SelectItem value='safari'>Safari</SelectItem>
<SelectItem value='ios'>IOS</SelectItem>
<SelectItem value='android'>Android</SelectItem>
<SelectItem value='edge'>edge</SelectItem>
<SelectItem value='360'>360</SelectItem>
<SelectItem value='qq'>QQ</SelectItem>
{['vmess', 'vless'].includes(protocol) && (
<SelectItem value='none'>NONE</SelectItem>
)}
<SelectItem value='tls'>TLS</SelectItem>
{protocol === 'vless' && (
<SelectItem value='reality'>Reality</SelectItem>
)}
</SelectContent>
</Select>
<FormMessage />
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name='config.security_config.allow_insecure'
render={({ field }) => (
<FormItem>
<FormLabel>Allow Insecure</FormLabel>
<FormControl>
<div className='pt-2'>
<Switch
checked={!!field.value}
onCheckedChange={(value) => {
</CardHeader>
{(['anytls', 'tuic', 'hysteria2'].includes(protocol) ||
(['vmess', 'vless', 'trojan'].includes(protocol) &&
security !== 'none')) && (
<CardContent className='grid grid-cols-2 gap-4 p-3'>
<FormField
control={form.control}
name='config.security_config.sni'
render={({ field }) => (
<FormItem>
<FormLabel>Server Name(SNI)</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Reality 特殊配置只在 vless + reality 时显示 */}
{protocol === 'vless' && security === 'reality' && (
<>
<FormField
control={form.control}
name='config.security_config.reality_server_addr'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.serverAddress')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t(
'form.security_config.serverAddressPlaceholder',
)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_config.reality_server_port'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.serverPort')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
type='number'
min={1}
max={65535}
placeholder={t(
'form.security_config.serverPortPlaceholder',
)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_config.reality_private_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.privateKey')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t(
'form.security_config.privateKeyPlaceholder',
)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_config.reality_public_key'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.publicKey')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('form.security_config.publicKeyPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='config.security_config.reality_short_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.shortId')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('form.security_config.shortIdPlaceholder')}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
/>
</CardContent>
)}
</Card>
{protocol === 'vless' && (
<FormField
control={form.control}
name='config.security_config.fingerprint'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.security_config.fingerprint')}</FormLabel>
<Select
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('form.pleaseSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='chrome'>Chrome</SelectItem>
<SelectItem value='firefox'>Firefox</SelectItem>
<SelectItem value='safari'>Safari</SelectItem>
<SelectItem value='ios'>IOS</SelectItem>
<SelectItem value='android'>Android</SelectItem>
<SelectItem value='edge'>edge</SelectItem>
<SelectItem value='360'>360</SelectItem>
<SelectItem value='qq'>QQ</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name='config.security_config.allow_insecure'
render={({ field }) => (
<FormItem>
<FormLabel>Allow Insecure</FormLabel>
<FormControl>
<div className='pt-2'>
<Switch
checked={!!field.value}
onCheckedChange={(checked) => {
form.setValue(field.name, checked);
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
)}
</Card>
)}
</div>
)}
@@ -87,6 +87,7 @@ export default function NodeTable() {
'bg-yellow-500': row.original.protocol === 'trojan',
'bg-purple-500': row.original.protocol === 'hysteria2',
'bg-cyan-500': row.original.protocol === 'tuic',
'bg-gray-500': row.original.protocol === 'anytls',
})}
>
{row.getValue('id')}
+11 -10
View File
@@ -9,20 +9,20 @@ import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';
import { PublicEnvScript } from 'next-runtime-env';
import { unstable_noStore as noStore } from 'next/cache';
import { Geist, Geist_Mono } from 'next/font/google';
// import { Geist, Geist_Mono } from 'next/font/google';
import { cookies } from 'next/headers';
import NextTopLoader from 'nextjs-toploader';
import React from 'react';
const fontSans = Geist({
subsets: ['latin'],
variable: '--font-sans',
});
// const fontSans = Geist({
// subsets: ['latin'],
// variable: '--font-sans',
// });
const fontMono = Geist_Mono({
subsets: ['latin'],
variable: '--font-mono',
});
// const fontMono = Geist_Mono({
// subsets: ['latin'],
// variable: '--font-mono',
// });
export async function generateMetadata(): Promise<Metadata> {
noStore();
@@ -111,7 +111,8 @@ export default async function RootLayout({
</head>
<body
suppressHydrationWarning
className={`${fontSans.variable} ${fontMono.variable} size-full min-h-[calc(100dvh-env(safe-area-inset-top))] font-sans antialiased`}
// ${fontSans.variable} ${fontMono.variable}
className={`size-full min-h-[calc(100dvh-env(safe-area-inset-top))] font-sans antialiased`}
>
<NextIntlClientProvider messages={messages}>
<NextTopLoader showSpinner={false} />
+5
View File
@@ -68,11 +68,14 @@
"cancel": "Cancel",
"city": "City",
"confirm": "Confirm",
"congestionController": "Congestion Controller",
"country": "Country",
"disableSni": "Disable SNI",
"edit": "Edit",
"editSecurity": "Edit Security Configuration",
"enableTLS": "Enable TLS",
"encryptionMethod": "Encryption Method",
"fingerprint": "Fingerprint",
"flow": "Flow Control Algorithm",
"groupId": "Node Group",
"hopInterval": "Hop Interval",
@@ -85,6 +88,7 @@
"pleaseSelect": "Please Select",
"port": "Server Port",
"protocol": "Protocol",
"reduceRtt": "Reduce RTT",
"relayHost": "Relay Host",
"relayMode": "Relay Mode",
"relayModeOptions": {
@@ -128,6 +132,7 @@
"transport": "Transport Protocol Configuration",
"transportConfig": "Transport Protocol Configuration",
"transportHost": "Transport Server Address",
"udpRelayMode": "UDP Relay Mode",
"transportPath": "Transport Path",
"transportServerName": "Transport Server Name"
},
+5
View File
@@ -68,11 +68,14 @@
"cancel": "取消",
"city": "城市",
"confirm": "确认",
"congestionController": "拥塞控制",
"country": "国家",
"disableSni": "禁用SNI",
"edit": "编辑",
"editSecurity": "编辑安全性配置",
"enableTLS": "启用TLS",
"encryptionMethod": "加密方法",
"fingerprint": "指纹",
"flow": "流控算法",
"groupId": "节点组",
"hopInterval": "跳跃间隔",
@@ -85,6 +88,7 @@
"pleaseSelect": "请选择",
"port": "服务端口",
"protocol": "协议",
"reduceRtt": "减少RTT",
"relayHost": "中转地址",
"relayMode": "中转模式",
"relayModeOptions": {
@@ -128,6 +132,7 @@
"transport": "传输协议",
"transportConfig": "传输协议配置",
"transportHost": "传输服务地址",
"udpRelayMode": "UDP中继模式",
"transportPath": "传输路径",
"transportServerName": "传输服务名称"
},
+51 -3
View File
@@ -28,6 +28,11 @@ declare namespace API {
updated_at: number;
};
type AnyTLS = {
port: number;
security_config: SecurityConfig;
};
type Application = {
id: number;
icon: string;
@@ -75,6 +80,40 @@ declare namespace API {
is_default: boolean;
};
type AppUserSubcbribe = {
id: number;
name: string;
upload: number;
traffic: number;
download: number;
device_limit: number;
start_time: string;
expire_time: string;
list: AppUserSubscbribeNode[];
};
type AppUserSubscbribeNode = {
id: number;
name: string;
uuid: string;
protocol: string;
relay_mode: string;
relay_node: string;
server_addr: string;
speed_limit: number;
tags: string[];
traffic: number;
traffic_ratio: number;
upload: number;
config: string;
country: string;
city: string;
latitude: string;
longitude: string;
created_at: number;
download: number;
};
type AuthConfig = {
mobile: MobileAuthenticateConfig;
email: EmailAuthticateConfig;
@@ -598,7 +637,7 @@ declare namespace API {
type GetNodeListParams = {
page: number;
size: number;
tag?: string;
tags?: string;
group_id?: number;
search?: string;
};
@@ -610,7 +649,7 @@ declare namespace API {
type GetNodeServerListRequest = {
page: number;
size: number;
tag?: string;
tags?: string;
group_id?: number;
search?: string;
};
@@ -1077,7 +1116,7 @@ declare namespace API {
type PurchaseOrderRequest = {
subscribe_id: number;
quantity: number;
payment: number;
payment?: number;
coupon?: string;
};
@@ -1130,6 +1169,11 @@ declare namespace API {
total: number;
};
type QueryUserAffiliateCountResponse = {
registers: number;
total_commission: number;
};
type QueryUserAffiliateListRequest = {
page: number;
size: number;
@@ -1455,6 +1499,10 @@ declare namespace API {
type Tuic = {
port: number;
disable_sni: boolean;
reduce_rtt: boolean;
udp_relay_mode: string;
congestion_controller: string;
security_config: SecurityConfig;
};
+1 -1
View File
@@ -1,5 +1,5 @@
// @ts-ignore
/* eslint-disable */
import request from '@/utils/request';
/** Apple Login Callback POST /v1/auth/oauth/callback/apple */
+49 -1
View File
@@ -28,6 +28,11 @@ declare namespace API {
updated_at: number;
};
type AnyTLS = {
port: number;
security_config: SecurityConfig;
};
type AppleLoginCallbackRequest = {
code: string;
id_token: string;
@@ -81,6 +86,40 @@ declare namespace API {
is_default: boolean;
};
type AppUserSubcbribe = {
id: number;
name: string;
upload: number;
traffic: number;
download: number;
device_limit: number;
start_time: string;
expire_time: string;
list: AppUserSubscbribeNode[];
};
type AppUserSubscbribeNode = {
id: number;
name: string;
uuid: string;
protocol: string;
relay_mode: string;
relay_node: string;
server_addr: string;
speed_limit: number;
tags: string[];
traffic: number;
traffic_ratio: number;
upload: number;
config: string;
country: string;
city: string;
latitude: string;
longitude: string;
created_at: number;
download: number;
};
type AuthConfig = {
mobile: MobileAuthenticateConfig;
email: EmailAuthticateConfig;
@@ -473,7 +512,7 @@ declare namespace API {
type PurchaseOrderRequest = {
subscribe_id: number;
quantity: number;
payment: number;
payment?: number;
coupon?: string;
};
@@ -526,6 +565,11 @@ declare namespace API {
total: number;
};
type QueryUserAffiliateCountResponse = {
registers: number;
total_commission: number;
};
type QueryUserAffiliateListRequest = {
page: number;
size: number;
@@ -830,6 +874,10 @@ declare namespace API {
type Tuic = {
port: number;
disable_sni: boolean;
reduce_rtt: boolean;
udp_relay_mode: string;
congestion_controller: string;
security_config: SecurityConfig;
};