feat: #53 支持本地代理 - 添加HTTP/SOCKS5代理配置和VFS缓存刷新功能

This commit is contained in:
VirtualHotBar
2026-06-02 12:55:26 +08:00
parent 79a2b4fde5
commit 8ebcdfd2c9
5 changed files with 171 additions and 2 deletions

View File

@@ -33,6 +33,21 @@ async function reupMount(noRefreshUI?: boolean) {
}
}
/**
* 清除所有已挂载存储的 VFS 目录缓存并刷新挂载列表
* 解决远程添加文件后本地不显示的问题
*/
async function refreshMountWithVfsCache(): Promise<void> {
try {
// 先清除所有 VFS 缓存,强制 rclone 重新读取远程目录
await mountRepository.forgetAllVfsCache()
// 再刷新挂载列表
await mountRepository.refreshMountList()
} catch (error) {
mountLogger.error('Failed to refresh mount with VFS cache', error as Error)
}
}
/**
* 获取挂载配置
*/
@@ -136,6 +151,7 @@ async function getAvailableDriveLetter(): Promise<string> {
export {
reupMount,
refreshMountWithVfsCache,
mountStorage,
unmountStorage,
addMountStorage,

View File

@@ -1,9 +1,9 @@
/**
* Advanced Settings Component
* 高级设置组件(启动参数)
* 高级设置组件(网络代理、启动参数)
*/
import { Button, Collapse, Form, Input, Message, Modal } from '@arco-design/web-react'
import { Button, Collapse, Form, Input, InputNumber, Message, Modal, Select } from '@arco-design/web-react'
import { useTranslation } from 'react-i18next'
import { nmConfig, saveNmConfig } from '../../../services/ConfigService'
import { useSettingsStore } from '../../../stores/useSettingsStore'
@@ -15,9 +15,99 @@ export function AdvancedSettings(): JSX.Element {
const { t } = useTranslation()
const { increment: incrementSettings } = useSettingsStore()
const proxy = nmConfig.settings.proxy
const proxyType = proxy?.type || 'no_proxy'
return (
<Form autoComplete="off" style={{ paddingRight: '0.8rem' }}>
<Collapse bordered={false} style={{ marginBottom: '0.75rem' }}>
<Collapse.Item name="proxy_settings" header={t('proxy_settings')}>
<FormItem label={t('proxy_type')}>
<Select
value={proxyType}
onChange={value => {
if (value === 'no_proxy') {
nmConfig.settings.proxy = undefined
} else {
nmConfig.settings.proxy = {
type: value as 'http' | 'socks5',
host: proxy?.host || '',
port: proxy?.port || (value === 'socks5' ? 1080 : 8080),
username: proxy?.username,
password: proxy?.password,
}
}
incrementSettings()
}}
style={{ width: '12rem' }}
>
<Select.Option value="no_proxy">{t('no_proxy')}</Select.Option>
<Select.Option value="http">HTTP</Select.Option>
<Select.Option value="socks5">SOCKS5</Select.Option>
</Select>
</FormItem>
{proxyType !== 'no_proxy' && (
<>
<FormItem label={t('proxy_host')}>
<Input
value={proxy?.host || ''}
placeholder={t('proxy_host_placeholder')}
onChange={value => {
if (nmConfig.settings.proxy) {
nmConfig.settings.proxy.host = value
incrementSettings()
}
}}
/>
</FormItem>
<FormItem label={t('proxy_port')}>
<InputNumber
value={proxy?.port}
min={1}
max={65535}
placeholder={t('proxy_port_placeholder')}
onChange={value => {
if (nmConfig.settings.proxy) {
nmConfig.settings.proxy.port = value || undefined
incrementSettings()
}
}}
style={{ width: '12rem' }}
/>
</FormItem>
<FormItem label={t('proxy_username')}>
<Input
value={proxy?.username || ''}
placeholder={t('optional')}
onChange={value => {
if (nmConfig.settings.proxy) {
nmConfig.settings.proxy.username = value || undefined
incrementSettings()
}
}}
/>
</FormItem>
<FormItem label={t('proxy_password')}>
<Input.Password
value={proxy?.password || ''}
placeholder={t('optional')}
onChange={value => {
if (nmConfig.settings.proxy) {
nmConfig.settings.proxy.password = value || undefined
incrementSettings()
}
}}
/>
</FormItem>
</>
)}
<div style={{ fontSize: '0.75rem', color: 'var(--color-text-3)', marginTop: '0.25rem', marginBottom: '0.75rem' }}>
{t('proxy_settings_hint')}
</div>
</Collapse.Item>
<Collapse.Item name="extra_startup_args" header={t('extra_startup_args')}>
<FormItem label={'Rclone'}>
<Input

View File

@@ -241,6 +241,28 @@ export async function performMount(mountInfo: MountListItem): Promise<void> {
await refreshMountList()
}
/**
* 清除所有已挂载存储的 VFS 目录缓存
* 用于刷新操作,强制 rclone 重新从远程读取目录列表
*/
export async function forgetAllVfsCache(): Promise<void> {
const mounts = rcloneInfo.mountList
if (mounts.length === 0) return
const forgetPromises = mounts.map(async (mount) => {
try {
await rclone_api_post('/vfs/forget', {
fs: convertStoragePath(mount.storageName) || mount.storageName,
}, true)
} catch {
// 忽略 - 非关键操作
}
})
await Promise.all(forgetPromises)
mountLogger.debug('VFS cache forgotten for all mounts', { count: mounts.length })
}
/**
* 执行卸载操作
* 卸载前先清理 VFS 缓存引用,卸载后清理残留临时文件

View File

@@ -25,6 +25,13 @@ interface NMConfig {
lockOnSleep?: boolean // 休眠时锁定
idleTimeoutMinutes?: number // 空闲超时锁定分钟0或undefined表示禁用
}
proxy?: {
type: 'no_proxy' | 'http' | 'socks5' // 代理类型
host?: string // 代理主机地址
port?: number // 代理端口
username?: string // 代理用户名(可选)
password?: string // 代理密码(可选)
}
}
notice?: Notice
framework: {

View File

@@ -12,6 +12,29 @@ import { netmountLogDir, rcloneConfigFile, rcloneLogFile } from '../netmountPath
import { restartSidecar, startSidecarAndWait, stopSidecarGracefully } from '../sidecarService'
import { parseExtraCliArgs } from '../cliArgs'
/**
* 构建代理URL
* 根据代理配置生成 rclone --http-proxy 参数值
* 支持 HTTP 和 SOCKS5 代理格式protocol://[user:pass@]host:port
*/
function buildProxyUrl(proxy: { type: string; host?: string; port?: number; username?: string; password?: string }): string | undefined {
if (proxy.type === 'no_proxy' || !proxy.host || !proxy.port) {
return undefined
}
const protocol = proxy.type === 'socks5' ? 'socks5' : 'http'
let auth = ''
if (proxy.username) {
auth = encodeURIComponent(proxy.username)
if (proxy.password) {
auth += ':' + encodeURIComponent(proxy.password)
}
auth += '@'
}
return `${protocol}://${auth}${proxy.host}:${proxy.port}`
}
async function startRclone() {
if (rcloneInfo.process.child) {
await stopRclone()
@@ -67,6 +90,17 @@ async function startRclone() {
if (nmConfig.framework.rclone.user === '') {
args.push('--rc-no-auth')
}
// 应用代理配置
const proxy = nmConfig.settings.proxy
if (proxy && proxy.type !== 'no_proxy') {
const proxyUrl = buildProxyUrl(proxy)
if (proxyUrl) {
args.push(`--http-proxy=${proxyUrl}`)
logger.info('Rclone proxy configured', 'Rclone', { type: proxy.type, host: proxy.host })
}
}
args.push(...parseExtraCliArgs(nmConfig.framework.rclone.extraArgs))
// 使用 Rust 端启动 sidecar确保由主进程创建