diff --git a/src/ipc/system/handler.ts b/src/ipc/system/handler.ts index 86daedc..e033d6b 100644 --- a/src/ipc/system/handler.ts +++ b/src/ipc/system/handler.ts @@ -2,19 +2,63 @@ import { os } from '@orpc/server'; import { z } from 'zod'; import { networkInterfaces } from 'os'; -export const systemHandler = os.route({ - get_local_ip: os.handler(async () => { +// Schema for IP info +const IpInfoSchema = z.object({ + address: z.string(), + name: z.string(), + isRecommended: z.boolean(), +}); + +export const systemHandler = os.router({ + // Get all available local IPs with their adapter names + get_local_ips: os.output(z.array(IpInfoSchema)).handler(async () => { const nets = networkInterfaces(); - const results: string[] = []; + const results: { address: string; name: string; isRecommended: boolean }[] = []; for (const name of Object.keys(nets)) { for (const net of nets[name] || []) { - // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses - if (net.family === 'IPv4' && !net.internal) { - results.push(net.address); + const family = net.family; + const isIPv4 = family === 'IPv4'; + + if (!isIPv4 || net.internal) { + continue; } + + const addr = net.address; + + // Skip non-LAN addresses + if (addr.startsWith('169.254.')) continue; // APIPA + if (addr.startsWith('198.18.')) continue; // CGNAT / VPN + + // Determine if this is a recommended (likely real LAN) address + let isRecommended = false; + const lowerName = name.toLowerCase(); + + if (addr.startsWith('192.168.') || addr.startsWith('10.')) { + if ( + lowerName.includes('wlan') || + lowerName.includes('wi-fi') || + lowerName.includes('wireless') || + lowerName === '以太网' || + lowerName === 'ethernet' || + lowerName.match(/^eth\d/) + ) { + isRecommended = true; + } + } + + results.push({ address: addr, name, isRecommended }); } } - return results.length > 0 ? results[0] : '127.0.0.1'; + + // Sort: recommended first, then by address + results.sort((a, b) => { + if (a.isRecommended !== b.isRecommended) { + return b.isRecommended ? 1 : -1; + } + return a.address.localeCompare(b.address); + }); + + return results; }), }); diff --git a/src/routes/proxy.tsx b/src/routes/proxy.tsx index edaa6c7..dea3b60 100644 --- a/src/routes/proxy.tsx +++ b/src/routes/proxy.tsx @@ -47,24 +47,62 @@ function ProxyPage() { const { t } = useTranslation(); const { config, isLoading, saveConfig } = useAppConfig(); - const { data: localIp } = useQuery({ - queryKey: ['system', 'localIp'], + // Query all available local IPs + const { data: localIps } = useQuery({ + queryKey: ['system', 'localIps'], queryFn: async () => { - // IPC might not be ready on first render, handled by query retry or lazy loading implication - // But ipc is imported statically now. - return (await ipc.client.system.get_local_ip()) as string; + try { + const ips = await ipc.client.system.get_local_ips(); + return ips as { address: string; name: string; isRecommended: boolean }[]; + } catch (e) { + console.error('Failed to get local IPs:', e); + return [{ address: '127.0.0.1', name: 'localhost', isRecommended: false }]; + } }, + staleTime: Infinity, + retry: 3, }); + // Selected IP for display (defaults to first recommended or first available) + const [selectedIp, setSelectedIp] = useState(''); + + // Set default selected IP when IPs are loaded + useEffect(() => { + if (localIps && localIps.length > 0 && !selectedIp) { + const recommended = localIps.find((ip) => ip.isRecommended); + setSelectedIp(recommended?.address || localIps[0].address); + } + }, [localIps, selectedIp]); + // Local state for proxyConfig editing const [proxyConfig, setProxyConfig] = useState(undefined); const [isRegenerateDialogOpen, setIsRegenerateDialogOpen] = useState(false); const [showKey, setShowKey] = useState(false); - // Sync config.proxy to local state when loaded + // Sync config.proxy to local state when loaded, and check actual server status useEffect(() => { if (config) { - setProxyConfig(config.proxy); + // Check actual server status and sync with config + const syncServerStatus = async () => { + try { + const status = await ipc.client.gateway.status(); + const actualEnabled = status.running; + + // If config says enabled but server not running, or vice versa, sync + if (config.proxy.enabled !== actualEnabled) { + const syncedConfig = { ...config.proxy, enabled: actualEnabled }; + setProxyConfig(syncedConfig); + // Also save the corrected state + await saveConfig({ ...config, proxy: syncedConfig }); + } else { + setProxyConfig(config.proxy); + } + } catch (e) { + // If status check fails, just use config value + setProxyConfig(config.proxy); + } + }; + syncServerStatus(); } }, [config]); @@ -175,8 +213,23 @@ print(response.choices[0].message.content)`;
{t('proxy.config.local_access', 'Local Access:')}
- http://{localIp || 'localhost'}:{proxyConfig.port}/v1 + http://{selectedIp || 'localhost'}:{proxyConfig.port}/v1 + {/* IP Selector Dropdown */} + {localIps && localIps.length > 1 && ( + + )}
{!proxyConfig.api_key && (