(null)
+ const [saveSuccess, setSaveSuccess] = useState(false)
+
+ const fetchConfig = useCallback(async () => {
+ setIsLoading(true)
+ try {
+ const res = await fetch('/api/config')
+ if (res.ok) {
+ const data = (await res.json()) as KLALBControllerConfig
+ setConfig(data)
+ setError(null)
+ } else {
+ setError('获取配置失败: 服务器返回异常状态')
+ }
+ } catch (err: unknown) {
+ const msg = err instanceof Error ? err.message : String(err)
+ setError(`无法连接到配置接口: ${msg}`)
+ } finally {
+ setIsLoading(false)
+ }
+ }, [])
+
+ const saveConfig = useCallback(
+ async (newConfig: KLALBControllerConfig): Promise<{ success: boolean; message?: string }> => {
+ setIsSaving(true)
+ setSaveSuccess(false)
+ setError(null)
+ try {
+ const res = await fetch('/api/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newConfig),
+ })
+ const data = await res.json()
+ if (res.ok && (data.success || data.success === undefined)) {
+ setConfig(newConfig)
+ setSaveSuccess(true)
+ setTimeout(() => setSaveSuccess(false), 3000)
+ return { success: true }
+ }
+ const errMsg = data.error || data.message || '保存配置失败'
+ setError(errMsg)
+ return { success: false, message: errMsg }
+ } catch (err: unknown) {
+ const msg = err instanceof Error ? err.message : String(err)
+ setError(msg)
+ return { success: false, message: msg }
+ } finally {
+ setIsSaving(false)
+ }
+ },
+ []
+ )
+
+ useEffect(() => {
+ fetchConfig()
+ }, [fetchConfig])
+
+ return {
+ config,
+ isLoading,
+ isSaving,
+ error,
+ saveSuccess,
+ fetchConfig,
+ saveConfig,
+ }
+}
diff --git a/src/pages/overview.tsx b/src/pages/overview.tsx
index 2fec8bc..7d8d5ad 100644
--- a/src/pages/overview.tsx
+++ b/src/pages/overview.tsx
@@ -65,7 +65,7 @@ export function OverviewPage({ status, links, isConnected }: OverviewPageProps)
const isHealthy = lossRate === 0 && ecnRate === 0
// TUN device status check
- const isTunDisabled = !status?.tunName || status.tunName.trim() === ''
+ const isTunDisabled = status?.enableTUN === false || !status?.tunName || status.tunName.trim() === ''
return (
@@ -94,9 +94,6 @@ export function OverviewPage({ status, links, isConnected }: OverviewPageProps)
{status?.deviceName || 'KLALB SRv6 Node'}
-
- {isConnected ? '在线运行' : '离线'}
-
{status?.deviceDescription || '无节点描述信息'}
diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx
new file mode 100644
index 0000000..dd602c6
--- /dev/null
+++ b/src/pages/settings.tsx
@@ -0,0 +1,1288 @@
+import { useState } from 'react'
+import {
+ Settings,
+ Server,
+ Network,
+ Cpu,
+ Save,
+ RotateCcw,
+ AlertCircle,
+ Dice5,
+ Plus,
+ Trash2,
+ Layers,
+} from 'lucide-react'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Switch } from '@/components/ui/switch'
+import { Slider } from '@/components/ui/slider'
+import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from '@/components/ui/tabs'
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select'
+import {
+ Field,
+ FieldContent,
+ FieldDescription,
+ FieldGroup,
+ FieldLabel,
+} from '@/components/ui/field'
+import { toast } from '@/components/ui/toast'
+import { useKlalbConfig } from '@/hooks/use-klalb-config'
+import type { KLALBControllerConfig } from '@/types/api'
+
+const LANGUAGE_ITEMS = [
+ { value: 'ZH_CN', label: '简体中文 (ZH_CN)' },
+ { value: 'EN_US', label: 'English (EN_US)' },
+]
+
+const CONGESTION_ITEMS = [
+ { value: 'BBR', label: 'BBR (高吞吐低延迟,推荐)' },
+ { value: 'Vegas2', label: 'Vegas2 (基于延迟的平滑算法)' },
+ { value: 'DCTCP', label: 'DCTCP (数据中心低时延拥塞控制)' },
+]
+
+const PERFORMANCE_ITEMS = [
+ { value: 'multifill', label: '多核按顺序填充 (multifill - 推荐通用)' },
+ { value: 'multiscatter', label: '多核均匀打散 (multiscatter - 适合多路NUMA)' },
+ { value: 'singlecore', label: '单核节能优先 (singlecore - 低功耗/云机)' },
+]
+
+// Random IPv6 generator compatible with KLALB format
+function generateRandomIPv6(): string {
+ const hex = () => Math.floor(Math.random() * 0x10000).toString(16)
+ return `2486:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}`
+}
+
+// Helper to safely extract address string whether it's a string or an object with host/port
+function toAddrString(item: unknown): string {
+ if (!item) return ''
+ if (typeof item === 'string') return item
+ if (typeof item === 'object' && item !== null) {
+ const obj = item as Record
+ if (obj.host && obj.port) {
+ const proto = obj.protocol ? `${obj.protocol}://` : ''
+ return `${proto}${obj.host}:${obj.port}`
+ }
+ if (obj.name && typeof obj.name === 'string') return obj.name
+ if (obj.address && typeof obj.address === 'string') return obj.address
+ }
+ return String(item)
+}
+
+interface ConnectionItem {
+ id: string
+ address: string
+ autoConnect: boolean
+}
+
+function parseConnectionsFromConfig(config: KLALBControllerConfig | null): ConnectionItem[] {
+ if (!config) return []
+ const lineTable = (
+ Array.isArray(config.openConnections)
+ ? config.openConnections
+ : Array.isArray(config.LineTable)
+ ? config.LineTable
+ : []
+ ).map(toAddrString)
+ const connectTable = (
+ Array.isArray(config.autoConnections)
+ ? config.autoConnections
+ : Array.isArray(config.ConnectLineTable)
+ ? config.ConnectLineTable
+ : []
+ ).map(toAddrString)
+
+ const items: ConnectionItem[] = []
+ // openConnections (autoConnect: false)
+ lineTable.forEach((addr, index) => {
+ if (addr && addr.trim() !== '') {
+ items.push({
+ id: `open-${index}-${addr}`,
+ address: addr,
+ autoConnect: false,
+ })
+ }
+ })
+ // autoConnections (autoConnect: true)
+ connectTable.forEach((addr, index) => {
+ if (addr && addr.trim() !== '') {
+ items.push({
+ id: `auto-${index}-${addr}`,
+ address: addr,
+ autoConnect: true,
+ })
+ }
+ })
+ return items
+}
+
+function SettingsForm({
+ initialConfig,
+ onSave,
+ onReset,
+ isSaving,
+}: {
+ initialConfig: KLALBControllerConfig
+ onSave: (config: KLALBControllerConfig) => Promise
+ onReset: () => void
+ isSaving: boolean
+}) {
+ const [form, setForm] = useState(() => ({
+ ...initialConfig,
+ TCPListen: toAddrString(initialConfig.TCPListen),
+ UDPListen: toAddrString(initialConfig.UDPListen),
+ }))
+ const [tunEnabled, setTunEnabled] = useState(() =>
+ initialConfig.enableTUN ??
+ (initialConfig.TUNName !== null &&
+ initialConfig.TUNName !== undefined &&
+ initialConfig.TUNName.trim() !== '')
+ )
+
+ const [connections, setConnections] = useState(() =>
+ parseConnectionsFromConfig(initialConfig)
+ )
+
+ const [ntpServers, setNtpServers] = useState(() =>
+ (
+ Array.isArray(initialConfig.ntpServers)
+ ? initialConfig.ntpServers
+ : Array.isArray(initialConfig.ntpServerTable)
+ ? initialConfig.ntpServerTable
+ : []
+ ).map(toAddrString)
+ )
+
+ const [dnsList, setDnsList] = useState(() =>
+ (Array.isArray(initialConfig.DNS) ? initialConfig.DNS : []).map(toAddrString)
+ )
+
+ const [extraRoutes, setExtraRoutes] = useState(() =>
+ (Array.isArray(initialConfig.ExtraRoutes) ? initialConfig.ExtraRoutes : []).map(toAddrString)
+ )
+
+ const [interfaceExcepts, setInterfaceExcepts] = useState(() =>
+ (
+ Array.isArray(initialConfig.NetworkInterfaceExcepts)
+ ? initialConfig.NetworkInterfaceExcepts
+ : []
+ ).map(toAddrString)
+ )
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ const cleanConnections = connections.filter((c) => c.address.trim() !== '')
+ // 互斥分离:选中自动连接的进 autoConnections,未选中的进 openConnections
+ const updatedOpenConnections = cleanConnections
+ .filter((c) => !c.autoConnect)
+ .map((c) => c.address.trim())
+ const updatedAutoConnections = cleanConnections
+ .filter((c) => c.autoConnect)
+ .map((c) => c.address.trim())
+
+ const updatedConfig: Record = {
+ ...form,
+ openConnections: updatedOpenConnections,
+ autoConnections: updatedAutoConnections,
+ LineTable: updatedOpenConnections,
+ ConnectLineTable: updatedAutoConnections,
+ ntpServers: ntpServers.filter((s) => s.trim() !== ''),
+ ntpServerTable: ntpServers.filter((s) => s.trim() !== ''),
+ DNS: dnsList.filter((s) => s.trim() !== ''),
+ ExtraRoutes: extraRoutes.filter((s) => s.trim() !== ''),
+ NetworkInterfaceExcepts: interfaceExcepts.filter((s) => s.trim() !== ''),
+ enableTUN: tunEnabled,
+ TUNName: form.TUNName?.trim() || 'KLALB_SRv6',
+ denyConnectionQuery: form.denyConnectionQuery ?? form.denyLineTableQuery ?? false,
+ denyConnectionBroadcast: form.denyConnectionBroadcast ?? form.denyLineTableBroadcast ?? false,
+ Type: 'KLALBController',
+ }
+
+ await onSave(updatedConfig as KLALBControllerConfig)
+ }
+
+ // --- Dynamic Connection Items Handlers ---
+ const addConnection = () => {
+ setConnections((prev) => [
+ ...prev,
+ {
+ id: `conn-${Date.now()}`,
+ address: '',
+ autoConnect: true,
+ },
+ ])
+ }
+
+ const updateConnection = (
+ index: number,
+ field: 'address' | 'autoConnect',
+ value: string | boolean
+ ) => {
+ setConnections((prev) => {
+ const next = [...prev]
+ next[index] = { ...next[index], [field]: value }
+ return next
+ })
+ }
+
+ const removeConnection = (index: number) => {
+ setConnections((prev) => prev.filter((_, i) => i !== index))
+ }
+
+ // --- Generic Dynamic List Handlers ---
+ const addListItem = (setter: React.Dispatch>) => {
+ setter((prev) => [...prev, ''])
+ }
+
+ const updateListItem = (
+ index: number,
+ value: string,
+ setter: React.Dispatch>
+ ) => {
+ setter((prev) => {
+ const next = [...prev]
+ next[index] = value
+ return next
+ })
+ }
+
+ const removeListItem = (
+ index: number,
+ setter: React.Dispatch>
+ ) => {
+ setter((prev) => prev.filter((_, i) => i !== index))
+ }
+
+ return (
+
+ )
+}
+
+export function SettingsPage() {
+ const {
+ config,
+ isLoading,
+ isSaving,
+ fetchConfig,
+ saveConfig,
+ } = useKlalbConfig()
+
+ const [versionKey, setVersionKey] = useState(0)
+
+ const handleReset = () => {
+ fetchConfig()
+ setVersionKey((prev) => prev + 1)
+ }
+
+ const handleSave = async (updatedConfig: KLALBControllerConfig) => {
+ const res = await saveConfig(updatedConfig)
+ if (res.success) {
+ toast.add({
+ title: '配置已保存',
+ type: 'success',
+ })
+ setVersionKey((prev) => prev + 1)
+ } else {
+ toast.add({
+ title: '保存配置失败',
+ description: res.message || '请检查配置参数或后端服务状态',
+ type: 'error',
+ })
+ }
+ }
+
+ if (isLoading || !config) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/src/types/api.ts b/src/types/api.ts
index c421ba6..379128e 100644
--- a/src/types/api.ts
+++ b/src/types/api.ts
@@ -30,6 +30,7 @@ export interface SystemStatus {
onlineDevices?: number
deviceName?: string
deviceDescription?: string
+ enableTUN?: boolean
tunName?: string
congestionAlgorithm?: string
performanceStrategy?: string
@@ -98,3 +99,41 @@ export interface NetworkInterfaceInfo {
displayName: string
addresses: string[]
}
+
+export interface KLALBControllerConfig {
+ DeviceName?: string
+ DeviceDescription?: string
+ language?: string
+ VirtualAddress?: string
+ VirtualASN?: number
+ enableTUN?: boolean
+ TUNName?: string | null
+ webUI?: boolean
+ webPort?: number
+ nogui?: boolean
+ TCPListen?: string
+ UDPListen?: string
+ VirtualSocketName?: string
+ openConnections?: string[]
+ autoConnections?: string[]
+ ntpServers?: string[]
+ LineTable?: string[]
+ ConnectLineTable?: string[]
+ ntpServerTable?: string[]
+ DNS?: string[]
+ ExtraRoutes?: string[]
+ NetworkInterfaceExcepts?: string[]
+ denyConnectionQuery?: boolean
+ denyConnectionBroadcast?: boolean
+ denyLineTableQuery?: boolean
+ denyLineTableBroadcast?: boolean
+ congestionAlgorithm?: string
+ performanceStrategy?: string
+ linkConnectionsCount?: number
+ burstLimit?: number
+ delayUpperBound?: number
+ delayLowerBound?: number
+ nagleDelayTime?: number
+ linkNagleDelayTime?: number
+ Type?: string
+}