feat(proxy): HTTP/SOCKS5 代理支持,按会话独立配置 | 15 files +483/-23 | 96/96 tests passing

功能:
- 代理类型定义 (ProxyType, ProxyConfig)
- 代理工具函数 (验证、解析、构建URL) + 24 测试
- SSH 连接通过 HTTP CONNECT / SOCKS5 隧道
- 数据库 proxy 列持久化 (JSON)
- SessionDialog 代理配置 UI (可折叠面板)
- 连接池传递 proxy 到 IPC
- sessionStore 类型添加 proxy 支持
This commit is contained in:
OpsXShell Dev committed 2026-04-25 05:14:27 +08:00
1 parent 3f636b1b0e
commit 07c4a5b14f
15 files changed
+757 -23

No files matched your search

+149 -2
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { X, Server, Key, Lock, User, Globe, Tag, Folder } from 'lucide-react'
import { X, Server, Key, Lock, User, Globe, Tag, Folder, Shield } from 'lucide-react'
import { useSessionStore, Session } from '../stores'
import type { AuthType } from '../../shared/types/session.types'
import type { AuthType, ProxyType } from '../../shared/types/session.types'
import { createLogger } from '../utils/logger'
const logger = createLogger('SessionDialog')
@@ -23,9 +23,17 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
password: '',
privateKey: '~/.ssh/id_rsa',
passphrase: '',
proxy: {
type: 'none' as ProxyType,
host: '',
port: 0,
username: '',
password: '',
},
group: 'default',
tags: '',
})
const [showProxySettings, setShowProxySettings] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
// 当编辑会话时,填充表单数据
@@ -40,9 +48,15 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
password: editSession.password || '',
privateKey: editSession.privateKey || '',
passphrase: editSession.passphrase || '',
proxy: editSession.proxy
? { type: editSession.proxy.type as ProxyType, host: editSession.proxy.host, port: editSession.proxy.port, username: editSession.proxy.username || '', password: editSession.proxy.password || '' }
: { type: 'none' as ProxyType, host: '', port: 0, username: '', password: '' },
group: editSession.group || 'default',
tags: editSession.tags?.join(', ') || '',
})
if (editSession.proxy && editSession.proxy.type !== 'none') {
setShowProxySettings(true)
}
} else {
// 重置表单
setFormData({
@@ -54,9 +68,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
password: '',
privateKey: '~/.ssh/id_rsa',
passphrase: '',
proxy: { type: 'none', host: '', port: 0, username: '', password: '' },
group: 'default',
tags: '',
})
setShowProxySettings(false)
}
}, [editSession])
@@ -69,6 +85,16 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
setIsSubmitting(true)
try {
const proxyConfig = formData.proxy.type !== 'none' && formData.proxy.host
? {
type: formData.proxy.type,
host: formData.proxy.host,
port: formData.proxy.port || (formData.proxy.type === 'http' ? 8080 : 1080),
username: formData.proxy.username || undefined,
password: formData.proxy.password || undefined,
}
: undefined
const sessionData = {
name: formData.name,
host: formData.host,
@@ -78,6 +104,7 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
password: formData.password,
privateKey: formData.authType === 'key' ? (formData.privateKey.trim() || '~/.ssh/id_rsa') : formData.privateKey,
passphrase: formData.passphrase,
proxy: proxyConfig,
group: formData.group,
tags: formData.tags.split(',').map(t => t.trim()).filter(Boolean),
}
@@ -102,9 +129,11 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
password: '',
privateKey: '',
passphrase: '',
proxy: { type: 'none', host: '', port: 0, username: '', password: '' },
group: 'default',
tags: '',
})
setShowProxySettings(false)
} catch (error) {
logger.error(isEditing ? 'Failed to update session:' : 'Failed to create session:', error)
} finally {
@@ -301,6 +330,124 @@ export default function SessionDialog({ isOpen, onClose, editSession }: SessionD
)}
</div>
{/* Proxy Settings */}
<div className="border-t border-dark-700 pt-4">
<button
type="button"
onClick={() => setShowProxySettings(!showProxySettings)}
className="flex items-center gap-2 text-sm font-medium text-gray-400 hover:text-gray-200 transition-colors"
>
<Shield className="w-4 h-4" />
代理设置
<span className="text-xs text-gray-600">
{formData.proxy.type !== 'none' && formData.proxy.host ? `(已配置 ${formData.proxy.type.toUpperCase()})` : '(未配置)'}
</span>
<svg
className={`w-3 h-3 transition-transform ${showProxySettings ? 'rotate-180' : ''}`}
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showProxySettings && (
<div className="mt-3 space-y-3 animate-fade-in">
{/* Proxy Type */}
<div className="flex gap-2">
{[
{ value: 'none', label: '不使用' },
{ value: 'http', label: 'HTTP' },
{ value: 'socks5', label: 'SOCKS5' },
].map((type) => (
<button
key={type.value}
type="button"
onClick={() => setFormData({
...formData,
proxy: {
...formData.proxy,
type: type.value as ProxyType,
port: formData.proxy.port || (type.value === 'http' ? 8080 : 1080),
}
})}
className={`flex-1 px-3 py-1.5 rounded-lg border text-xs transition-colors ${
formData.proxy.type === type.value
? 'border-accent-blue bg-accent-blue/20 text-accent-blue'
: 'border-dark-700 hover:border-dark-600 text-gray-500'
}`}
>
{type.label}
</button>
))}
</div>
{formData.proxy.type !== 'none' && (
<>
{/* Proxy Host & Port */}
<div className="grid grid-cols-3 gap-3">
<div className="col-span-2">
<label className="block text-xs text-gray-500 mb-1">代理地址</label>
<input
type="text"
value={formData.proxy.host}
onChange={(e) => setFormData({
...formData,
proxy: { ...formData.proxy, host: e.target.value }
})}
placeholder="proxy.example.com"
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">端口</label>
<input
type="number"
value={formData.proxy.port || ''}
onChange={(e) => setFormData({
...formData,
proxy: { ...formData.proxy, port: parseInt(e.target.value) || 0 }
})}
placeholder={formData.proxy.type === 'http' ? '8080' : '1080'}
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
/>
</div>
</div>
{/* Proxy Auth */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-500 mb-1">代理用户名 (可选)</label>
<input
type="text"
value={formData.proxy.username}
onChange={(e) => setFormData({
...formData,
proxy: { ...formData.proxy, username: e.target.value }
})}
placeholder="username"
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">代理密码 (可选)</label>
<input
type="password"
value={formData.proxy.password}
onChange={(e) => setFormData({
...formData,
proxy: { ...formData.proxy, password: e.target.value }
})}
placeholder="password"
className="w-full px-3 py-1.5 bg-dark-900 border border-dark-700 rounded-lg text-gray-200 text-sm placeholder-gray-600 focus:outline-none focus:border-accent-blue"
/>
</div>
</div>
</>
)}
</div>
)}
</div>
{/* Group and tags */}
<div className="border-t border-dark-700 pt-4 grid grid-cols-2 gap-3">
<div>