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 { z } from 'zod';
|
||||||
import { networkInterfaces } from 'os';
|
import { networkInterfaces } from 'os';
|
||||||
|
|
||||||
export const systemHandler = os.route({
|
// Schema for IP info
|
||||||
get_local_ip: os.handler(async () => {
|
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 nets = networkInterfaces();
|
||||||
const results: string[] = [];
|
const results: { address: string; name: string; isRecommended: boolean }[] = [];
|
||||||
|
|
||||||
for (const name of Object.keys(nets)) {
|
for (const name of Object.keys(nets)) {
|
||||||
for (const net of nets[name] || []) {
|
for (const net of nets[name] || []) {
|
||||||
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
const family = net.family;
|
||||||
if (net.family === 'IPv4' && !net.internal) {
|
const isIPv4 = family === 'IPv4';
|
||||||
results.push(net.address);
|
|
||||||
|
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 { t } = useTranslation();
|
||||||
const { config, isLoading, saveConfig } = useAppConfig();
|
const { config, isLoading, saveConfig } = useAppConfig();
|
||||||
|
|
||||||
const { data: localIp } = useQuery({
|
// Query all available local IPs
|
||||||
queryKey: ['system', 'localIp'],
|
const { data: localIps } = useQuery({
|
||||||
|
queryKey: ['system', 'localIps'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// IPC might not be ready on first render, handled by query retry or lazy loading implication
|
try {
|
||||||
// But ipc is imported statically now.
|
const ips = await ipc.client.system.get_local_ips();
|
||||||
return (await ipc.client.system.get_local_ip()) as string;
|
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
|
// Local state for proxyConfig editing
|
||||||
const [proxyConfig, setProxyConfig] = useState<ProxyConfig | undefined>(undefined);
|
const [proxyConfig, setProxyConfig] = useState<ProxyConfig | undefined>(undefined);
|
||||||
const [isRegenerateDialogOpen, setIsRegenerateDialogOpen] = useState(false);
|
const [isRegenerateDialogOpen, setIsRegenerateDialogOpen] = useState(false);
|
||||||
const [showKey, setShowKey] = 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(() => {
|
useEffect(() => {
|
||||||
if (config) {
|
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]);
|
}, [config]);
|
||||||
|
|
||||||
@@ -175,8 +213,23 @@ print(response.choices[0].message.content)`;
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="font-semibold">{t('proxy.config.local_access', 'Local Access:')}</div>
|
<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">
|
<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>
|
</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>
|
</div>
|
||||||
{!proxyConfig.api_key && (
|
{!proxyConfig.api_key && (
|
||||||
<div className="flex items-center gap-2 text-xs font-medium text-amber-600 dark:text-amber-400">
|
<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