feat: support LAN network
This commit is contained in:
1 parent
098cbe3eb9
commit
f5a85e3436
2 files changed
+112
-15
No files matched your search
@@ -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;
|
||||
}),
|
||||
});
|
||||
+61
-8
@@ -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<string>('');
|
||||
|
||||
// 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<ProxyConfig | undefined>(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)`;
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-semibold">{t('proxy.config.local_access', 'Local Access:')}</div>
|
||||
<code className="rounded bg-blue-100 px-1.5 py-0.5 font-mono select-all dark:bg-blue-900/50">
|
||||
http://{localIp || 'localhost'}:{proxyConfig.port}/v1
|
||||
http://{selectedIp || 'localhost'}:{proxyConfig.port}/v1
|
||||
</code>
|
||||
{/* IP Selector Dropdown */}
|
||||
{localIps && localIps.length > 1 && (
|
||||
<Select value={selectedIp} onValueChange={setSelectedIp}>
|
||||
<SelectTrigger className="ml-2 h-7 w-auto min-w-[180px] text-xs">
|
||||
<SelectValue placeholder="Select IP" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{localIps.map((ip) => (
|
||||
<SelectItem key={ip.address} value={ip.address} className="text-xs">
|
||||
{ip.address} ({ip.name}){ip.isRecommended && ' ★'}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
{!proxyConfig.api_key && (
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-amber-600 dark:text-amber-400">
|
||||
|
||||
Reference in new issue
Block a user