diff --git a/apps/admin/app/dashboard/servers/dynamic-multiplier.tsx b/apps/admin/app/dashboard/servers/dynamic-multiplier.tsx index c26dcf2..8a889a4 100644 --- a/apps/admin/app/dashboard/servers/dynamic-multiplier.tsx +++ b/apps/admin/app/dashboard/servers/dynamic-multiplier.tsx @@ -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() {
-

{t('config.dynamicMultiplier')}

+

{t('server_config.dynamic_multiplier')}

- {t('config.dynamicMultiplierDescription')} + {t('server_config.dynamic_multiplier_desc')}

@@ -76,18 +72,18 @@ export default function DynamicMultiplier() { - {t('config.dynamicMultiplier')} - {t('config.dynamicMultiplierDescription')} + {t('server_config.dynamic_multiplier')} + {t('server_config.dynamic_multiplier_desc')}
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() {
- +
diff --git a/apps/admin/app/dashboard/servers/form-schema/constants.ts b/apps/admin/app/dashboard/servers/form-schema/constants.ts index 7a967d2..1afe64e 100644 --- a/apps/admin/app/dashboard/servers/form-schema/constants.ts +++ b/apps/admin/app/dashboard/servers/form-schema/constants.ts @@ -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 { diff --git a/apps/admin/app/dashboard/servers/form-schema/defaults.ts b/apps/admin/app/dashboard/servers/form-schema/defaults.ts index 9c38969..023a4cd 100644 --- a/apps/admin/app/dashboard/servers/form-schema/defaults.ts +++ b/apps/admin/app/dashboard/servers/form-schema/defaults.ts @@ -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; diff --git a/apps/admin/app/dashboard/servers/form-schema/fields.ts b/apps/admin/app/dashboard/servers/form-schema/fields.ts index f3a31d6..4b45f87 100644 --- a/apps/admin/app/dashboard/servers/form-schema/fields.ts +++ b/apps/admin/app/dashboard/servers/form-schema/fields.ts @@ -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 = { 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 = { ].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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { }, ], 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 = { 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', + }, ], }; diff --git a/apps/admin/app/dashboard/servers/form-schema/schemas.ts b/apps/admin/app/dashboard/servers/form-schema/schemas.ts index 7c06900..343c869 100644 --- a/apps/admin/app/dashboard/servers/form-schema/schemas.ts +++ b/apps/admin/app/dashboard/servers/form-schema/schemas.ts @@ -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), }); diff --git a/apps/admin/app/dashboard/servers/server-config.tsx b/apps/admin/app/dashboard/servers/server-config.tsx index 6f06810..118fded 100644 --- a/apps/admin/app/dashboard/servers/server-config.tsx +++ b/apps/admin/app/dashboard/servers/server-config.tsx @@ -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; @@ -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() {
-

{t('config.title')}

+

{t('server_config.title')}

- {t('config.description')} + {t('server_config.description')}

@@ -110,102 +166,348 @@ export default function ServerConfig() { - 节点配置 + {t('server_config.title')} -
- - ( - - {t('config.communicationKey')} - - - { - 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' - /> - - } - /> - - {t('config.communicationKeyDescription')} - - - )} - /> + + + {t('server_config.tabs.basic')} + {t('server_config.tabs.dns')} + {t('server_config.tabs.outbound')} + {t('server_config.tabs.block')} + - ( - - {t('config.nodePullInterval')} - - - - {t('config.nodePullIntervalDescription')} - - - )} - /> + + + + ( + + {t('server_config.fields.communication_key')} + + + { + 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' + /> + + } + /> + + + {t('server_config.fields.communication_key_desc')} + + + + )} + /> - ( - - {t('config.nodePushInterval')} - - - - {t('config.nodePushIntervalDescription')} - - - )} - /> - - + ( + + {t('server_config.fields.node_pull_interval')} + + + + + {t('server_config.fields.node_pull_interval_desc')} + + + + )} + /> + + ( + + {t('server_config.fields.node_push_interval')} + + + + + {t('server_config.fields.node_push_interval_desc')} + + + + )} + /> + + ( + + {t('server_config.fields.traffic_report_threshold')} + + + + + {t('server_config.fields.traffic_report_threshold_desc')} + + + + )} + /> + + + + ( + + {t('server_config.fields.ip_strategy')} + + + {t('server_config.fields.ip_strategy_desc')} + + + + )} + /> + + ( + + {t('server_config.fields.dns_config')} + + ({ + ...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); + }} + /> + + + + )} + /> + + + + ( + + + ({ + ...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); + }} + /> + + + + )} + /> + + + + ( + + +