feat: Update server configuration translations for multiple languages

- Added new keys for server configuration actions and descriptions in Russian, Thai, Turkish, Ukrainian, Vietnamese, Chinese (Simplified and Traditional).
- Removed outdated configuration structure and replaced it with a more organized server_config object.
- Enhanced the dynamic Inputs component to support textarea input type for better user experience.
This commit is contained in:
web
2025-09-24 06:05:24 -07:00
parent bb6671c14f
commit fc43de16f0
31 changed files with 2130 additions and 734 deletions
@@ -41,14 +41,10 @@ export default function DynamicMultiplier() {
}, [periodsResp]);
async function savePeriods() {
try {
await setNodeMultiplier({ periods: timeSlots });
await refetchPeriods();
toast.success(t('config.saveSuccess'));
setOpen(false);
} catch (error) {
toast.error(t('config.saveError'));
}
await setNodeMultiplier({ periods: timeSlots });
await refetchPeriods();
toast.success(t('server_config.saveSuccess'));
setOpen(false);
}
return (
@@ -62,9 +58,9 @@ export default function DynamicMultiplier() {
<Icon icon='mdi:clock-time-eight' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('config.dynamicMultiplier')}</p>
<p className='font-medium'>{t('server_config.dynamic_multiplier')}</p>
<p className='text-muted-foreground truncate text-sm'>
{t('config.dynamicMultiplierDescription')}
{t('server_config.dynamic_multiplier_desc')}
</p>
</div>
</div>
@@ -76,18 +72,18 @@ export default function DynamicMultiplier() {
<SheetContent className='w-[600px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{t('config.dynamicMultiplier')}</SheetTitle>
<SheetDescription>{t('config.dynamicMultiplierDescription')}</SheetDescription>
<SheetTitle>{t('server_config.dynamic_multiplier')}</SheetTitle>
<SheetDescription>{t('server_config.dynamic_multiplier_desc')}</SheetDescription>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-60px-env(safe-area-inset-top))] px-6'>
<div className='space-y-4 pt-4'>
<ArrayInput<API.TimePeriod>
fields={[
{ name: 'start_time', prefix: t('config.startTime'), type: 'time' },
{ name: 'end_time', prefix: t('config.endTime'), type: 'time' },
{ name: 'start_time', prefix: t('server_config.fields.start_time'), type: 'time' },
{ name: 'end_time', prefix: t('server_config.fields.end_time'), type: 'time' },
{
name: 'multiplier',
prefix: t('config.multiplier'),
prefix: t('server_config.fields.multiplier'),
type: 'number',
placeholder: '0',
},
@@ -100,13 +96,13 @@ export default function DynamicMultiplier() {
<SheetFooter className='flex-row justify-between pt-3'>
<Button variant='outline' onClick={() => setTimeSlots(periodsResp || [])}>
{t('config.reset')}
{t('server_config.fields.reset')}
</Button>
<div className='flex gap-2'>
<Button variant='outline' onClick={() => setOpen(false)}>
{t('config.actions.cancel')}
{t('actions.cancel')}
</Button>
<Button onClick={savePeriods}>{t('config.actions.save')}</Button>
<Button onClick={savePeriods}>{t('actions.save')}</Button>
</div>
</SheetFooter>
</SheetContent>
@@ -60,6 +60,7 @@ export const TRANSPORTS = {
} as const;
export const SECURITY = {
shadowsocks: ['none', 'http', 'tls'] as const,
vmess: ['none', 'tls'] as const,
vless: ['none', 'tls', 'reality'] as const,
trojan: ['tls'] as const,
@@ -91,6 +92,8 @@ export const FINGERPRINTS = [
'qq',
] as const;
export const CERT_MODES = ['none', 'http', 'dns', 'self'] as const;
export const multiplexLevels = ['none', 'low', 'middle', 'high'] as const;
export function getLabel(value: string): string {
@@ -10,36 +10,85 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
port: null,
cipher: 'chacha20-ietf-poly1305',
server_key: null,
obfs: 'none',
obfs_host: null,
obfs_path: null,
security: 'none',
host: null,
path: null,
sni: null,
allow_insecure: null,
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
case 'vmess':
return {
type: 'vmess',
enable: false,
host: null,
port: null,
transport: 'tcp',
security: 'none',
path: null,
service_name: null,
sni: null,
allow_insecure: null,
fingerprint: 'chrome',
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
case 'vless':
return {
type: 'vless',
enable: false,
host: null,
port: null,
transport: 'tcp',
security: 'none',
flow: 'none',
path: null,
service_name: null,
sni: null,
allow_insecure: null,
fingerprint: 'chrome',
reality_server_addr: null,
reality_server_port: null,
reality_private_key: null,
reality_public_key: null,
reality_short_id: null,
xhttp_mode: XHTTP_MODES[0], // 'auto'
xhttp_extra: null,
encryption: 'none',
encryption_mode: null,
encryption_rtt: null,
encryption_ticket: null,
encryption_server_padding: null,
encryption_private_key: null,
encryption_client_padding: null,
encryption_password: null,
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
case 'trojan':
return {
type: 'trojan',
enable: false,
host: null,
port: null,
transport: 'tcp',
security: 'tls',
path: null,
service_name: null,
sni: null,
allow_insecure: null,
fingerprint: 'chrome',
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
case 'hysteria2':
return {
@@ -53,6 +102,13 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
security: 'tls',
up_mbps: null,
down_mbps: null,
sni: null,
allow_insecure: null,
fingerprint: 'chrome',
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
case 'tuic':
return {
@@ -67,12 +123,17 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
sni: null,
allow_insecure: false,
fingerprint: 'chrome',
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
case 'socks':
return {
type: 'socks',
enable: false,
port: null,
ratio: 1,
} as any;
case 'naive':
return {
@@ -80,6 +141,13 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
enable: false,
port: null,
security: 'none',
sni: null,
allow_insecure: null,
fingerprint: 'chrome',
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
case 'http':
return {
@@ -87,6 +155,13 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
enable: false,
port: null,
security: 'none',
sni: null,
allow_insecure: null,
fingerprint: 'chrome',
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
case 'mieru':
return {
@@ -106,6 +181,10 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
sni: null,
allow_insecure: false,
fingerprint: 'chrome',
cert_mode: 'none',
cert_dns_provider: null,
cert_dns_env: null,
ratio: 1,
} as any;
default:
return {} as any;
@@ -5,6 +5,7 @@ import {
generateRealityShortId,
} from '../generate';
import {
CERT_MODES,
ENCRYPTION_MODES,
ENCRYPTION_RTT,
ENCRYPTION_TYPES,
@@ -22,11 +23,20 @@ import type { FieldConfig } from './types';
export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
shadowsocks: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -55,36 +65,85 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
].includes(p.cipher),
},
{
name: 'obfs',
name: 'security',
type: 'select',
label: 'obfs',
options: ['none', 'http', 'tls'],
label: 'security',
options: SECURITY.shadowsocks,
defaultValue: 'none',
group: 'obfs',
group: 'security',
},
{
name: 'obfs_host',
name: 'host',
type: 'input',
label: 'obfs_host',
label: 'host',
placeholder: 'e.g. www.bing.com',
group: 'obfs',
condition: (p) => p.obfs && p.obfs !== 'none',
group: 'security',
condition: (p) => p.security && p.security !== 'none',
},
{
name: 'obfs_path',
name: 'path',
type: 'input',
label: 'obfs_path',
label: 'path',
placeholder: 'e.g. /path/to/obfs',
group: 'obfs',
condition: (p) => p.obfs && p.obfs !== 'none',
group: 'security',
condition: (p) => p.security && p.security !== 'none',
},
{
name: 'sni',
type: 'input',
label: 'security_sni',
group: 'security',
condition: (p) => p.security === 'tls',
},
{
name: 'allow_insecure',
type: 'switch',
label: 'security_allow_insecure',
group: 'security',
condition: (p) => p.security === 'tls',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
condition: (p) => p.security === 'tls',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
],
vmess: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -151,13 +210,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
group: 'security',
condition: (p) => p.security !== 'none',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
condition: (p) => p.security === 'tls',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
],
vless: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -374,13 +468,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
group: 'encryption',
condition: (p) => p.encryption === 'mlkem768x25519plus',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
condition: (p) => p.security === 'tls',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
],
trojan: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -447,13 +576,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
group: 'security',
condition: (p) => p.security !== 'none',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
condition: (p) => p.security === 'tls',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
],
hysteria2: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -470,7 +634,7 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
type: 'number',
label: 'hop_interval',
placeholder: 'e.g. 300',
min: 0,
min: 1,
suffix: 'S',
group: 'basic',
},
@@ -497,7 +661,7 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
name: 'up_mbps',
type: 'number',
label: 'up_mbps',
min: 0,
min: 1,
placeholder: (t) => t('bandwidth_placeholder'),
suffix: 'Mbps',
group: 'basic',
@@ -506,7 +670,7 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
name: 'down_mbps',
type: 'number',
label: 'down_mbps',
min: 0,
min: 1,
placeholder: (t) => t('bandwidth_placeholder'),
suffix: 'Mbps',
group: 'basic',
@@ -521,13 +685,47 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
defaultValue: 'chrome',
group: 'security',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.cert_mode === 'dns',
},
],
tuic: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -560,24 +758,67 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
defaultValue: 'chrome',
group: 'security',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.cert_mode === 'dns',
},
],
socks: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
},
],
naive: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -613,13 +854,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
group: 'security',
condition: (p) => p.security !== 'none',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
condition: (p) => p.security === 'tls',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
],
http: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -655,13 +931,48 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
group: 'security',
condition: (p) => p.security !== 'none',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
condition: (p) => p.security === 'tls',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.security === 'tls' && p.cert_mode === 'dns',
},
],
mieru: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -684,11 +995,20 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
},
],
anytls: [
{
name: 'ratio',
type: 'number',
label: 'traffic_ratio',
min: 1,
step: 0.01,
defaultValue: 1,
group: 'basic',
},
{
name: 'port',
type: 'number',
label: 'port',
min: 0,
min: 1,
max: 65535,
placeholder: '1-65535',
group: 'basic',
@@ -710,5 +1030,30 @@ export const PROTOCOL_FIELDS: Record<string, FieldConfig[]> = {
defaultValue: 'chrome',
group: 'security',
},
{
name: 'cert_mode',
type: 'select',
label: 'cert_mode',
options: CERT_MODES,
defaultValue: 'none',
group: 'security',
},
{
name: 'cert_dns_provider',
type: 'input',
label: 'cert_dns_provider',
placeholder: 'e.g. cloudflare, aliyun',
group: 'security',
condition: (p) => p.cert_mode === 'dns',
},
{
name: 'cert_dns_env',
type: 'textarea',
label: 'cert_dns_env',
placeholder:
'CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret',
group: 'security',
condition: (p) => p.cert_mode === 'dns',
},
],
};
@@ -1,5 +1,6 @@
import { z } from 'zod';
import {
CERT_MODES,
ENCRYPTION_MODES,
ENCRYPTION_RTT,
ENCRYPTION_TYPES,
@@ -16,20 +17,25 @@ import {
const nullableString = z.string().nullish();
const nullableBool = z.boolean().nullish();
const nullablePort = z.number().int().min(0).max(65535).nullish();
const nullableRatio = z.number().min(1).nullish();
const ss = z.object({
ratio: nullableRatio,
type: z.literal('shadowsocks'),
enable: nullableBool,
host: nullableString,
port: nullablePort,
cipher: z.enum(SS_CIPHERS).nullish(),
server_key: nullableString,
obfs: z.enum(['none', 'http', 'tls'] as const).nullish(),
obfs_host: nullableString,
obfs_path: nullableString,
security: z.enum(SECURITY.shadowsocks).nullish(),
host: nullableString,
path: nullableString,
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const vmess = z.object({
ratio: nullableRatio,
type: z.literal('vmess'),
enable: nullableBool,
host: nullableString,
@@ -41,9 +47,13 @@ const vmess = z.object({
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const vless = z.object({
ratio: nullableRatio,
type: z.literal('vless'),
enable: nullableBool,
host: nullableString,
@@ -71,9 +81,13 @@ const vless = z.object({
encryption_private_key: nullableString,
encryption_client_padding: nullableString,
encryption_password: nullableString,
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const trojan = z.object({
ratio: nullableRatio,
type: z.literal('trojan'),
enable: nullableBool,
host: nullableString,
@@ -85,9 +99,13 @@ const trojan = z.object({
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const hysteria2 = z.object({
ratio: nullableRatio,
type: z.literal('hysteria2'),
enable: nullableBool,
hop_ports: nullableString,
@@ -101,9 +119,13 @@ const hysteria2 = z.object({
fingerprint: nullableString,
up_mbps: z.number().nullish(),
down_mbps: z.number().nullish(),
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const tuic = z.object({
ratio: nullableRatio,
type: z.literal('tuic'),
enable: nullableBool,
host: nullableString,
@@ -116,9 +138,13 @@ const tuic = z.object({
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const anytls = z.object({
ratio: nullableRatio,
type: z.literal('anytls'),
enable: nullableBool,
port: nullablePort,
@@ -127,15 +153,20 @@ const anytls = z.object({
allow_insecure: nullableBool,
fingerprint: nullableString,
padding_scheme: nullableString,
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const socks = z.object({
ratio: nullableRatio,
type: z.literal('socks'),
enable: nullableBool,
port: nullablePort,
});
const naive = z.object({
ratio: nullableRatio,
type: z.literal('naive'),
enable: nullableBool,
port: nullablePort,
@@ -143,9 +174,13 @@ const naive = z.object({
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const http = z.object({
ratio: nullableRatio,
type: z.literal('http'),
enable: nullableBool,
port: nullablePort,
@@ -153,9 +188,13 @@ const http = z.object({
sni: nullableString,
allow_insecure: nullableBool,
fingerprint: nullableString,
cert_mode: z.enum(CERT_MODES).nullish(),
cert_dns_provider: nullableString,
cert_dns_env: nullableString,
});
const mieru = z.object({
ratio: nullableRatio,
type: z.literal('mieru'),
enable: nullableBool,
port: nullablePort,
@@ -182,6 +221,5 @@ export const formSchema = z.object({
address: z.string().min(1),
country: z.string().optional(),
city: z.string().optional(),
ratio: z.number().default(1),
protocols: z.array(protocolApiScheme),
});
@@ -15,6 +15,13 @@ import {
FormMessage,
} from '@workspace/ui/components/form';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import {
Sheet,
SheetContent,
@@ -23,6 +30,9 @@ import {
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { Textarea } from '@workspace/ui/components/textarea';
import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { DicesIcon } from 'lucide-react';
@@ -33,10 +43,46 @@ import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const dnsConfigSchema = z.object({
proto: z.enum(['tcp', 'udp', 'tls', 'https', 'quic']),
address: z.string(),
domains: z.array(z.string()),
});
const outboundConfigSchema = z.object({
name: z.string(),
protocol: z.enum([
'http',
'https',
'socks5',
'shadowsocks',
'vmess',
'vless',
'trojan',
'hysteria2',
'tuic',
'naive',
'brook',
'snell',
'wireguard',
'direct',
'reject',
]),
address: z.string(),
port: z.number(),
password: z.string().optional(),
rules: z.array(z.string()).optional(),
});
const nodeConfigSchema = z.object({
node_secret: z.string().optional(),
node_pull_interval: z.number().optional(),
node_push_interval: z.number().optional(),
traffic_report_threshold: z.number().optional(),
ip_strategy: z.enum(['prefer_ipv4', 'prefer_ipv6']).optional(),
dns: z.array(dnsConfigSchema).optional(),
block: z.array(z.string()).optional(),
outbound: z.array(outboundConfigSchema).optional(),
});
type NodeConfigFormData = z.infer<typeof nodeConfigSchema>;
@@ -60,6 +106,11 @@ export default function ServerConfig() {
node_secret: '',
node_pull_interval: undefined,
node_push_interval: undefined,
traffic_report_threshold: undefined,
ip_strategy: undefined,
dns: [],
block: [],
outbound: [],
},
});
@@ -69,6 +120,11 @@ export default function ServerConfig() {
node_secret: cfgResp.node_secret ?? '',
node_pull_interval: cfgResp.node_pull_interval as number | undefined,
node_push_interval: cfgResp.node_push_interval as number | undefined,
traffic_report_threshold: (cfgResp as any).traffic_report_threshold as number | undefined,
ip_strategy: (cfgResp as any).ip_strategy as 'prefer_ipv4' | 'prefer_ipv6' | undefined,
dns: (cfgResp as any).dns || [],
block: (cfgResp as any).block || [],
outbound: (cfgResp as any).outbound || [],
});
}
}, [cfgResp, form]);
@@ -77,7 +133,7 @@ export default function ServerConfig() {
setSaving(true);
try {
await updateNodeConfig(values as API.NodeConfig);
toast.success(t('config.saveSuccess'));
toast.success(t('server_config.saveSuccess'));
await refetchCfg();
setOpen(false);
} finally {
@@ -96,9 +152,9 @@ export default function ServerConfig() {
<Icon icon='mdi:resistor-nodes' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('config.title')}</p>
<p className='font-medium'>{t('server_config.title')}</p>
<p className='text-muted-foreground truncate text-sm'>
{t('config.description')}
{t('server_config.description')}
</p>
</div>
</div>
@@ -110,102 +166,348 @@ export default function ServerConfig() {
<SheetContent className='w-[720px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle></SheetTitle>
<SheetTitle>{t('server_config.title')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-6'>
<Form {...form}>
<form
id='server-config-form'
onSubmit={form.handleSubmit(onSubmit)}
className='space-y-4 pt-4'
>
<FormField
control={form.control}
name='node_secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('config.communicationKey')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('config.inputPlaceholder')}
value={field.value || ''}
onValueChange={field.onChange}
suffix={
<div className='bg-muted flex h-9 items-center px-3'>
<DicesIcon
onClick={() => {
const id = uid(32).toLowerCase();
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
form.setValue('node_secret', formatted);
}}
className='cursor-pointer'
/>
</div>
}
/>
</FormControl>
<FormDescription>{t('config.communicationKeyDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Tabs defaultValue='basic' className='pt-4'>
<TabsList className='grid w-full grid-cols-4'>
<TabsTrigger value='basic'>{t('server_config.tabs.basic')}</TabsTrigger>
<TabsTrigger value='dns'>{t('server_config.tabs.dns')}</TabsTrigger>
<TabsTrigger value='outbound'>{t('server_config.tabs.outbound')}</TabsTrigger>
<TabsTrigger value='block'>{t('server_config.tabs.block')}</TabsTrigger>
</TabsList>
<FormField
control={form.control}
name='node_pull_interval'
render={({ field }) => (
<FormItem>
<FormLabel>{t('config.nodePullInterval')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
min={0}
suffix='S'
value={field.value as any}
onValueChange={field.onChange}
placeholder={t('config.inputPlaceholder')}
/>
</FormControl>
<FormDescription>{t('config.nodePullIntervalDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Form {...form}>
<form id='server-config-form' onSubmit={form.handleSubmit(onSubmit)} className='mt-4'>
<TabsContent value='basic' className='space-y-4'>
<FormField
control={form.control}
name='node_secret'
render={({ field }) => (
<FormItem>
<FormLabel>{t('server_config.fields.communication_key')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('server_config.fields.communication_key_placeholder')}
value={field.value || ''}
onValueChange={field.onChange}
suffix={
<div className='bg-muted flex h-9 items-center px-3'>
<DicesIcon
onClick={() => {
const id = uid(32).toLowerCase();
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
form.setValue('node_secret', formatted);
}}
className='cursor-pointer'
/>
</div>
}
/>
</FormControl>
<FormDescription>
{t('server_config.fields.communication_key_desc')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='node_push_interval'
render={({ field }) => (
<FormItem>
<FormLabel>{t('config.nodePushInterval')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
min={0}
suffix='S'
step={0.1}
value={field.value as any}
onValueChange={field.onChange}
placeholder={t('config.inputPlaceholder')}
/>
</FormControl>
<FormDescription>{t('config.nodePushIntervalDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<FormField
control={form.control}
name='node_pull_interval'
render={({ field }) => (
<FormItem>
<FormLabel>{t('server_config.fields.node_pull_interval')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
min={0}
suffix='S'
value={field.value as any}
onValueChange={field.onChange}
placeholder={t('server_config.fields.communication_key_placeholder')}
/>
</FormControl>
<FormDescription>
{t('server_config.fields.node_pull_interval_desc')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='node_push_interval'
render={({ field }) => (
<FormItem>
<FormLabel>{t('server_config.fields.node_push_interval')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
min={0}
suffix='S'
step={0.1}
value={field.value as any}
onValueChange={field.onChange}
placeholder={t('server_config.fields.communication_key_placeholder')}
/>
</FormControl>
<FormDescription>
{t('server_config.fields.node_push_interval_desc')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='traffic_report_threshold'
render={({ field }) => (
<FormItem>
<FormLabel>{t('server_config.fields.traffic_report_threshold')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
min={0}
suffix='MB'
value={field.value as any}
onValueChange={field.onChange}
placeholder='1'
/>
</FormControl>
<FormDescription>
{t('server_config.fields.traffic_report_threshold_desc')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value='dns' className='space-y-4'>
<FormField
control={form.control}
name='ip_strategy'
render={({ field }) => (
<FormItem>
<FormLabel>{t('server_config.fields.ip_strategy')}</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t('server_config.fields.ip_strategy_placeholder')}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='prefer_ipv4'>
{t('server_config.fields.ip_strategy_ipv4')}
</SelectItem>
<SelectItem value='prefer_ipv6'>
{t('server_config.fields.ip_strategy_ipv6')}
</SelectItem>
</SelectContent>
</Select>
<FormDescription>
{t('server_config.fields.ip_strategy_desc')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='dns'
render={({ field }) => (
<FormItem>
<FormLabel>{t('server_config.fields.dns_config')}</FormLabel>
<FormControl>
<ArrayInput
className='grid grid-cols-2 gap-2'
fields={[
{
name: 'proto',
type: 'select',
placeholder: t('server_config.fields.dns_proto_placeholder'),
options: [
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
{ label: 'TLS', value: 'tls' },
{ label: 'HTTPS', value: 'https' },
{ label: 'QUIC', value: 'quic' },
],
},
{ name: 'address', type: 'text', placeholder: '8.8.8.8:53' },
{
name: 'domains',
type: 'textarea',
className: 'col-span-2',
placeholder: t('server_config.fields.dns_domains_placeholder'),
},
]}
value={(field.value || []).map((item) => ({
...item,
domains: Array.isArray(item.domains) ? item.domains.join('\n') : '',
}))}
onChange={(values) => {
// 转换 domains 字符串为数组
const converted = values.map((item: any) => ({
proto: item.proto,
address: item.address,
domains:
typeof item.domains === 'string'
? item.domains
.split('\n')
.map((d: string) => d.trim())
.filter(Boolean)
: item.domains || [],
}));
field.onChange(converted);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value='outbound' className='space-y-4'>
<FormField
control={form.control}
name='outbound'
render={({ field }) => (
<FormItem>
<FormControl>
<ArrayInput
className='grid grid-cols-2 gap-2'
fields={[
{
name: 'name',
type: 'text',
className: 'col-span-2',
placeholder: t('server_config.fields.outbound_name_placeholder'),
},
{
name: 'protocol',
type: 'select',
placeholder: t(
'server_config.fields.outbound_protocol_placeholder',
),
options: [
{ label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' },
{ label: 'SOCKS5', value: 'socks5' },
{ label: 'Shadowsocks', value: 'shadowsocks' },
{ label: 'VMess', value: 'vmess' },
{ label: 'VLESS', value: 'vless' },
{ label: 'Trojan', value: 'trojan' },
{ label: 'Hysteria2', value: 'hysteria2' },
{ label: 'TUIC', value: 'tuic' },
{ label: 'NaiveProxy', value: 'naive' },
{ label: 'Brook', value: 'brook' },
{ label: 'Snell', value: 'snell' },
{ label: 'WireGuard', value: 'wireguard' },
{ label: 'Direct', value: 'direct' },
{ label: 'Reject', value: 'reject' },
],
},
{
name: 'address',
type: 'text',
placeholder: t('server_config.fields.outbound_address_placeholder'),
},
{
name: 'port',
type: 'number',
placeholder: t('server_config.fields.outbound_port_placeholder'),
},
{
name: 'password',
type: 'text',
placeholder: t(
'server_config.fields.outbound_password_placeholder',
),
},
{
name: 'rules',
type: 'textarea',
className: 'col-span-2',
placeholder: t('server_config.fields.outbound_rules_placeholder'),
},
]}
value={(field.value || []).map((item) => ({
...item,
rules: Array.isArray(item.rules) ? item.rules.join('\n') : '',
}))}
onChange={(values) => {
// 转换 rules 字符串为数组
const converted = values.map((item: any) => ({
name: item.name,
protocol: item.protocol,
address: item.address,
port: item.port,
password: item.password,
rules:
typeof item.rules === 'string'
? item.rules
.split('\n')
.map((r: string) => r.trim())
.filter(Boolean)
: item.rules || [],
}));
field.onChange(converted);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value='block' className='space-y-4'>
<FormField
control={form.control}
name='block'
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
placeholder={t('server_config.fields.block_rules_placeholder')}
value={(field.value || []).join('\n')}
onChange={(e) => {
const lines = e.target.value
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
field.onChange(lines);
}}
rows={10}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
</form>
</Form>
</Tabs>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={saving} onClick={() => setOpen(false)}>
{t('config.actions.cancel')}
{t('actions.cancel')}
</Button>
<Button disabled={saving} type='submit' form='server-config-form'>
<Icon icon='mdi:loading' className={saving ? 'mr-2 animate-spin' : 'hidden'} />
{t('config.actions.save')}
{t('actions.save')}
</Button>
</SheetFooter>
</SheetContent>
@@ -338,7 +338,6 @@ export default function ServerForm(props: {
address: '',
country: '',
city: '',
ratio: 1,
protocols: [] as any[],
...initialValues,
},
@@ -375,7 +374,6 @@ export default function ServerForm(props: {
name: values.name,
country: values.country,
city: values.city,
ratio: values.ratio || 1,
address: values.address,
protocols: filteredProtocols,
};
@@ -399,7 +397,6 @@ export default function ServerForm(props: {
address: '',
country: '',
city: '',
ratio: 1,
protocols: full,
});
}
@@ -416,7 +413,7 @@ export default function ServerForm(props: {
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<Form {...form}>
<form className='grid grid-cols-1 gap-2 px-6 pt-4'>
<div className='grid grid-cols-3 gap-2'>
<div className='grid grid-cols-2 gap-2 md:grid-cols-4'>
<FormField
control={control}
name='name'
@@ -430,6 +427,23 @@ export default function ServerForm(props: {
</FormItem>
)}
/>
<FormField
control={control}
name='address'
render={({ field }) => (
<FormItem>
<FormLabel>{t('address')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('address_placeholder')}
onValueChange={(v) => field.onChange(v)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name='country'
@@ -457,44 +471,6 @@ export default function ServerForm(props: {
)}
/>
</div>
<div className='grid grid-cols-2 gap-2'>
<FormField
control={control}
name='address'
render={({ field }) => (
<FormItem>
<FormLabel>{t('address')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
placeholder={t('address_placeholder')}
onValueChange={(v) => field.onChange(v)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name='ratio'
render={({ field }) => (
<FormItem>
<FormLabel>{t('traffic_ratio')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
type='number'
step={0.1}
min={0}
onValueChange={(v) => field.onChange(v)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='my-3'>
<h3 className='text-foreground text-sm font-semibold'>
{t('protocol_configurations')}