diff --git a/src/components/ProxySwitch/index.css b/src/components/ProxySwitch/index.css index ef0df2f..ef27a43 100644 --- a/src/components/ProxySwitch/index.css +++ b/src/components/ProxySwitch/index.css @@ -349,4 +349,28 @@ body { .ant-menu .ant-menu-item.ant-menu-item-selected.active-item::after { color: white; +} + +.active-item { + background-color: #F28B44 !important; + color: white !important; + font-weight: 500; + transition: background-color 0.2s ease-in-out !important; +} + +.active-item img { + filter: brightness(0) invert(1); +} + +.active-item .anticon { + color: white !important; +} + +.proxy-menu .ant-menu-item { + transition: all 0.2s ease-in-out !important; +} + +/* 添加悬停效果 */ +.proxy-menu .ant-menu-item:hover { + background-color: rgba(242, 139, 68, 0.1) !important; } \ No newline at end of file diff --git a/src/components/ProxySwitch/index.tsx b/src/components/ProxySwitch/index.tsx index cb92c50..dcf8be8 100644 --- a/src/components/ProxySwitch/index.tsx +++ b/src/components/ProxySwitch/index.tsx @@ -5,6 +5,7 @@ import {browser,} from 'wxt/browser'; import type {MenuProps} from 'antd'; import type {ProxyConfig} from '@/types/proxy'; import {ContentActionType, ProxyActionType} from '@/types/action'; +import { getAllProxyConfigs, getCurrentProxy } from '@/utils/storage'; import './index.css'; @@ -58,6 +59,7 @@ export const ProxySwitch: React.FC = () => { const handleMessage = (message: any) => { if (message.action === ContentActionType.PROXY_CONFIGS_UPDATED && message.source !== 'proxy_switch') { loadCustomProxies(); + loadProxyStatus(); } }; @@ -80,18 +82,23 @@ export const ProxySwitch: React.FC = () => { // 获取当前代理状态 const loadProxyStatus = async () => { try { + // 先尝试从后台脚本获取当前代理状态 const response = await browser.runtime.sendMessage({ action: ProxyActionType.GET_PROXY_STATUS }); - if (!response) { - console.log('No response from background script'); + if (response && response.success) { + const activeMode = response.data.mode; + setCurrentMode(activeMode); return; } - if (response.success) { - const activeMode = response.data.mode; - setCurrentMode(activeMode); + // 如果后台脚本没有返回,则从存储中获取当前代理 + const currentProxy = await getCurrentProxy(); + if (currentProxy) { + setCurrentMode(currentProxy.id); + } else { + setCurrentMode('direct'); } } catch (error) { console.error('Error loading proxy status:', error); @@ -102,33 +109,12 @@ export const ProxySwitch: React.FC = () => { // 加载自定义代理配置 const loadCustomProxies = async () => { try { - const DB_NAME = 'yaklang_extension'; - const STORE_NAME = 'proxy_configs'; - - // 打开数据库 - const db = await new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, 1); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - }); - - // 从数据库读取代理配置 - const configs = await new Promise((resolve, reject) => { - try { - const transaction = db.transaction([STORE_NAME], 'readonly'); - const store = transaction.objectStore(STORE_NAME); - const request = store.getAll(); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result || []); - } catch (error) { - reject(error); - } - }); + // 使用存储API获取所有代理配置 + const configs = await getAllProxyConfigs(); // 处理代理配置 const proxies = configs - .filter((proxy: ProxyConfig) => !FIXED_MODES.some(mode => mode.key === proxy.id)) + .filter((proxy: ProxyConfig) => !['direct', 'system'].includes(proxy.id)) .map((proxy: ProxyConfig): CustomProxy => ({ key: proxy.id, name: proxy.name, @@ -138,6 +124,7 @@ export const ProxySwitch: React.FC = () => { })); setCustomProxies(proxies); + // 查找并设置已启用的代理 const enabledProxy = configs.find((proxy: ProxyConfig) => proxy.enabled); if (enabledProxy) { setCurrentMode(enabledProxy.id); @@ -182,6 +169,8 @@ export const ProxySwitch: React.FC = () => { } try { + // 先更新UI状态,避免闪烁 + setCurrentMode(mode); setIsLoading(true); // 发送切换代理请求 @@ -190,11 +179,17 @@ export const ProxySwitch: React.FC = () => { mode }); - if (response && response.success) { - setCurrentMode(mode); + if (!response || !response.success) { + // 如果失败,恢复原状态 + console.error('Failed to switch proxy mode'); + await loadProxyStatus(); // 重新加载正确的状态 + } else { + // 成功时刷新代理列表状态 + await loadCustomProxies(); } } catch (error) { console.error(`Error switching to proxy mode ${mode}:`, error); + await loadProxyStatus(); // 出错时重新加载正确的状态 } finally { setIsLoading(false); } @@ -218,7 +213,15 @@ export const ProxySwitch: React.FC = () => { ...customProxies.map(proxy => ({ key: proxy.key, label: proxy.name, - icon: YAK, + icon: YAK, className: `${currentMode === proxy.key ? 'active-item' : ''} menu-id-${proxy.key}`, })) ); @@ -229,7 +232,7 @@ export const ProxySwitch: React.FC = () => { items.push({ key: 'setting', label: '代理设置', - icon: , + icon: , className: 'menu-id-setting', }); @@ -237,7 +240,7 @@ export const ProxySwitch: React.FC = () => { items.push({ key: 'add', label: '添加代理', - icon: , + icon: , className: 'menu-id-add', }); diff --git a/src/entrypoints/background.ts b/src/entrypoints/background.ts index d615d78..e407478 100644 --- a/src/entrypoints/background.ts +++ b/src/entrypoints/background.ts @@ -1,11 +1,32 @@ import { browser, type Browser } from 'wxt/browser'; import {ContentActionType, ProxyActionType} from '@/types/action'; import { getCurrentProxyMode, switchProxyMode } from '@/utils/proxy'; +import { getProxyConfig, saveProxyConfig } from '@/utils/storage'; +import type { ProxyConfig } from '@/types/proxy'; + +// 固定的代理模式配置 +const FIXED_MODES = [ + { + id: 'direct', + name: '[直接连接]', + proxyType: 'direct', + enabled: false + }, + { + id: 'system', + name: '[系统代理]', + proxyType: 'system', + enabled: false + } +]; export default defineBackground({ type: 'module', - main() { + async main() { + // 初始化固定模式的代理配置 + await initializeFixedModes(); + // 初始化代理状态监听 browser.runtime.onMessage.addListener((message: any, sender: Browser.runtime.MessageSender, sendResponse: (response?: any) => void) => { if (message.action === ProxyActionType.GET_PROXY_STATUS) { @@ -34,3 +55,19 @@ export default defineBackground({ console.log('代理管理后台服务已启动'); }, }); + +// 初始化固定模式的代理配置 +async function initializeFixedModes() { + try { + // 确保固定模式的配置已保存到数据库 + for (const modeConfig of FIXED_MODES) { + const existingConfig = await getProxyConfig(modeConfig.id); + if (!existingConfig) { + console.log(`初始化固定模式配置: ${modeConfig.id}`); + await saveProxyConfig(modeConfig as ProxyConfig); + } + } + } catch (error) { + console.error('初始化固定模式配置失败:', error); + } +} diff --git a/src/entrypoints/options/App.css b/src/entrypoints/options/App.css index 089f81e..971ff8b 100644 --- a/src/entrypoints/options/App.css +++ b/src/entrypoints/options/App.css @@ -60,6 +60,26 @@ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } +.add-proxy-card .ant-modal-footer { + display: flex; + justify-content: flex-end; + padding: 10px 24px; + border-top: 1px solid #f0f0f0; +} + +.add-proxy-card .ant-modal-footer button { + margin-left: 8px; +} + +.required-label::before { + content: '* '; + color: #ff4d4f; +} + +.ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before { + display: none !important; +} + .ant-space { width: 100%; } \ No newline at end of file diff --git a/src/entrypoints/options/App.tsx b/src/entrypoints/options/App.tsx index 504ccb2..5ac4f82 100644 --- a/src/entrypoints/options/App.tsx +++ b/src/entrypoints/options/App.tsx @@ -47,20 +47,68 @@ export default function App() { try { setLoading(true); - const newProxy: ProxyConfig = { + const newProxy: Partial = { id: uuidv4(), name: values.name, - proxyType: "fixed_servers", - scheme: values.proxyType, - host: values.host, - port: Number(values.port), enabled: false }; + if (values.proxyType === 'fixed_servers') { + newProxy.proxyType = 'fixed_servers'; + newProxy.scheme = values.scheme; + newProxy.host = values.host; + newProxy.port = Number(values.port); + + // 处理不经过代理的地址 + if (values.bypassList) { + newProxy.bypassList = values.bypassList + .split('\n') + .map((line: string) => line.trim()) + .filter((line: string) => line.length > 0); + } else { + newProxy.bypassList = []; + } + } else if (values.proxyType === 'pac_script') { + newProxy.proxyType = 'pac_script'; + newProxy.mode = 'pac_script'; + + // 处理PAC脚本匹配域名 + if (values.matchList) { + newProxy.matchList = values.matchList + .split('\n') + .map((line: string) => line.trim()) + .filter((line: string) => line.length > 0); + } + + // 创建PAC脚本 + newProxy.pacScript = { + data: `function FindProxyForURL(url, host) { + // 匹配域名列表 + const domains = ${JSON.stringify(newProxy.matchList || [])}; + + // 检查是否匹配任何域名 + for (let i = 0; i < domains.length; i++) { + const domain = domains[i]; + // 支持通配符 + if (domain.startsWith('*.') && host.endsWith(domain.substring(1))) { + return 'PROXY ${values.host}:${values.port}'; + } else if (host === domain) { + return 'PROXY ${values.host}:${values.port}'; + } + } + + // 默认直接连接 + return 'DIRECT'; +}`, + mandatory: true + }; + } + + // 添加认证信息 if (values.username) newProxy.username = values.username; if (values.password) newProxy.password = values.password; - await saveProxyConfig(newProxy); + await saveProxyConfig(newProxy as ProxyConfig); // 重新加载代理列表 await loadProxies(); @@ -111,6 +159,12 @@ export default function App() { // 重新加载代理列表 await loadProxies(); + + // 通知后台脚本 + browser.runtime.sendMessage({ + action: ContentActionType.PROXY_CONFIGS_UPDATED, + source: 'options' + }); } catch (error) { console.error(`Error activating proxy ${id}:`, error); } @@ -179,9 +233,14 @@ export default function App() { key={proxy.id} actions={[ , @@ -207,7 +266,19 @@ export default function App() { open={isModalOpen} className='add-proxy-card' onCancel={handleCancel} - footer={null} + footer={[ + , + + ]} destroyOnClose >
名称} rules={[{ required: true, message: '请输入代理名称' }]} > - + 类型} + initialValue="fixed_servers" rules={[{ required: true, message: '请选择代理类型' }]} > - - - - - - - - - + prevValues.proxyType !== currentValues.proxyType} + > + {({ getFieldValue }) => { + const proxyType = getFieldValue('proxyType'); + if (proxyType === 'fixed_servers') { + return ( + <> + 协议} + initialValue="http" + rules={[{ required: true, message: '请选择代理协议' }]} + > + + + + 主机} + rules={[{ required: true, message: '请输入主机地址' }]} + > + + + + 端口} + rules={[{ required: true, message: '请输入端口' }]} + > + + + + + +
每行一个地址,支持通配符 *
+
+ + ); + } else if (proxyType === 'pac_script') { + return ( + <> + 匹配域名} + rules={[{ required: true, message: '请输入至少一个匹配域名' }]} + > + +
每行一个域名,支持通配符 *
+
+ + ); + } + return null; + }} +
- - - -
diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 7e661b9..072db3c 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -1,6 +1,6 @@ import { browser } from 'wxt/browser'; import type { ProxyConfig } from '../types/proxy'; -import { getAllProxyConfigs, getProxyConfig, enableProxyConfig, disableAllProxies } from './storage'; +import { getAllProxyConfigs, getProxyConfig, enableProxyConfig, disableAllProxies, getCurrentProxy, setCurrentProxy } from './storage'; /** * 获取当前激活的代理模式 @@ -8,10 +8,19 @@ import { getAllProxyConfigs, getProxyConfig, enableProxyConfig, disableAllProxie */ export async function getCurrentProxyMode(): Promise { try { + // 首先尝试从当前代理存储中获取 + const currentProxyConfig = await getCurrentProxy(); + if (currentProxyConfig) { + return currentProxyConfig.id; + } + + // 如果没有当前代理记录,则从配置列表查找已启用的代理 const configs = await getAllProxyConfigs(); const enabledProxy = configs.find(config => config.enabled); if (enabledProxy) { + // 如果找到已启用的代理,更新当前代理存储 + await setCurrentProxy(enabledProxy); return enabledProxy.id; } @@ -30,23 +39,6 @@ export async function getCurrentProxyMode(): Promise { */ export async function switchProxyMode(mode: string): Promise { try { - if (mode === 'direct') { - // 清除所有代理设置 - await disableAllProxies(); - await browser.proxy.settings.clear({}); - return true; - } - - if (mode === 'system') { - // 使用系统代理 - await disableAllProxies(); - await browser.proxy.settings.set({ - value: { mode: 'system' }, - scope: 'regular' - }); - return true; - } - // 使用自定义代理 const config = await getProxyConfig(mode); if (!config) { @@ -59,14 +51,16 @@ export async function switchProxyMode(mode: string): Promise { // 设置代理 if (config.proxyType === 'direct') { + // 直接连接模式 await browser.proxy.settings.clear({}); } else if (config.proxyType === 'system') { + // 系统代理模式 await browser.proxy.settings.set({ value: { mode: 'system' }, scope: 'regular' }); - } else { - // 构建代理配置 + } else if (config.proxyType === 'fixed_servers') { + // 固定服务器代理 const proxyConfig = { mode: 'fixed_servers', rules: {} @@ -74,7 +68,7 @@ export async function switchProxyMode(mode: string): Promise { // 添加代理规则 const proxyRule = { - scheme: config.proxyType, + scheme: config.scheme, host: config.host || '', port: config.port || 80 }; @@ -88,7 +82,7 @@ export async function switchProxyMode(mode: string): Promise { // 设置代理规则 proxyConfig.rules = { singleProxy: proxyRule, - bypassList: ['localhost', '127.0.0.1'] + bypassList: config.bypassList || ['localhost', '127.0.0.1'] }; // 应用代理设置 @@ -96,6 +90,18 @@ export async function switchProxyMode(mode: string): Promise { value: proxyConfig, scope: 'regular' }); + } else if (config.proxyType === 'pac_script') { + // PAC脚本代理 + const pacConfig = { + mode: 'pac_script', + pacScript: config.pacScript + }; + + // 应用PAC脚本设置 + await browser.proxy.settings.set({ + value: pacConfig, + scope: 'regular' + }); } return true; diff --git a/src/utils/storage.ts b/src/utils/storage.ts index a332ab6..c4cc7dd 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -2,21 +2,35 @@ import type { ProxyConfig } from '../types/proxy'; // 数据库名称和存储名称 const DB_NAME = 'yaklang_extension'; -const PROXY_STORE_NAME = 'proxy_configs'; +const STORES = { + PROXY_CONFIGS: 'proxy_configs', // 代理配置列表存储 + CURRENT_PROXY: 'current_proxy', // 当前代理配置存储 + PROXY_AUTH: 'proxy_auth' // 代理认证信息存储 +}; + +const DB_VERSION = 2; // 增加版本号以触发数据库升级 // 打开数据库 async function openDB(): Promise { return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, 1); + const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; - // 如果存储不存在,创建它 - if (!db.objectStoreNames.contains(PROXY_STORE_NAME)) { - db.createObjectStore(PROXY_STORE_NAME, { keyPath: 'id' }); + // 检查并创建各个存储对象 + if (!db.objectStoreNames.contains(STORES.PROXY_CONFIGS)) { + db.createObjectStore(STORES.PROXY_CONFIGS, { keyPath: 'id' }); + } + + if (!db.objectStoreNames.contains(STORES.CURRENT_PROXY)) { + db.createObjectStore(STORES.CURRENT_PROXY, { keyPath: 'id' }); + } + + if (!db.objectStoreNames.contains(STORES.PROXY_AUTH)) { + db.createObjectStore(STORES.PROXY_AUTH, { keyPath: 'id' }); } }; @@ -29,8 +43,8 @@ export async function getAllProxyConfigs(): Promise { try { const db = await openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction([PROXY_STORE_NAME], 'readonly'); - const store = transaction.objectStore(PROXY_STORE_NAME); + const transaction = db.transaction([STORES.PROXY_CONFIGS], 'readonly'); + const store = transaction.objectStore(STORES.PROXY_CONFIGS); const request = store.getAll(); request.onerror = () => reject(request.error); @@ -47,8 +61,8 @@ export async function getProxyConfig(id: string): Promise { try { const db = await openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction([PROXY_STORE_NAME], 'readonly'); - const store = transaction.objectStore(PROXY_STORE_NAME); + const transaction = db.transaction([STORES.PROXY_CONFIGS], 'readonly'); + const store = transaction.objectStore(STORES.PROXY_CONFIGS); const request = store.get(id); request.onerror = () => reject(request.error); @@ -65,12 +79,18 @@ export async function saveProxyConfig(config: ProxyConfig): Promise { try { const db = await openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction([PROXY_STORE_NAME], 'readwrite'); - const store = transaction.objectStore(PROXY_STORE_NAME); + const transaction = db.transaction([STORES.PROXY_CONFIGS], 'readwrite'); + const store = transaction.objectStore(STORES.PROXY_CONFIGS); const request = store.put(config); request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(true); + request.onsuccess = () => { + // 如果代理被启用,更新当前代理 + if (config.enabled) { + setCurrentProxy(config).catch(console.error); + } + resolve(true); + }; }); } catch (error) { console.error('Error saving proxy config:', error); @@ -78,13 +98,70 @@ export async function saveProxyConfig(config: ProxyConfig): Promise { } } +// 批量保存代理配置 +export async function saveProxyConfigs(configs: ProxyConfig[]): Promise { + try { + const db = await openDB(); + const transaction = db.transaction([STORES.PROXY_CONFIGS], 'readwrite'); + const store = transaction.objectStore(STORES.PROXY_CONFIGS); + + // 获取所有现有配置 + const existingConfigs = await getAllProxyConfigs(); + + // 保留固定模式配置(direct和system) + const fixedModeConfigs = existingConfigs.filter(config => + config.id === 'direct' || config.id === 'system' + ); + + // 确保新保存的配置不会覆盖固定模式的enabled状态 + const nonFixedConfigs = configs.filter(config => + config.id !== 'direct' && config.id !== 'system' + ); + + // 合并配置 + const allConfigs = [...fixedModeConfigs, ...nonFixedConfigs]; + + // 更新enabled状态 + const updatedConfigs = allConfigs.map(config => ({ + ...config, + enabled: configs.some(c => c.id === config.id && c.enabled) + })); + + // 清除所有配置 + await new Promise((resolve, reject) => { + const clearRequest = store.clear(); + clearRequest.onerror = () => reject(clearRequest.error); + clearRequest.onsuccess = () => resolve(); + }); + + // 保存所有配置 + for (const config of updatedConfigs) { + await new Promise((resolve, reject) => { + const putRequest = store.put(config); + putRequest.onerror = () => reject(putRequest.error); + putRequest.onsuccess = () => resolve(); + }); + + // 如果代理被启用,更新当前代理 + if (config.enabled) { + await setCurrentProxy(config); + } + } + + return true; + } catch (error) { + console.error('Error saving proxy configs:', error); + return false; + } +} + // 删除代理配置 export async function deleteProxyConfig(id: string): Promise { try { const db = await openDB(); return new Promise((resolve, reject) => { - const transaction = db.transaction([PROXY_STORE_NAME], 'readwrite'); - const store = transaction.objectStore(PROXY_STORE_NAME); + const transaction = db.transaction([STORES.PROXY_CONFIGS], 'readwrite'); + const store = transaction.objectStore(STORES.PROXY_CONFIGS); const request = store.delete(id); request.onerror = () => reject(request.error); @@ -96,16 +173,99 @@ export async function deleteProxyConfig(id: string): Promise { } } +// 设置当前代理 +export async function setCurrentProxy(proxy: ProxyConfig): Promise { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORES.CURRENT_PROXY], 'readwrite'); + const store = transaction.objectStore(STORES.CURRENT_PROXY); + const request = store.put({ ...proxy, id: 'current' }); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(true); + }); + } catch (error) { + console.error('Error setting current proxy:', error); + return false; + } +} + +// 获取当前代理 +export async function getCurrentProxy(): Promise { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORES.CURRENT_PROXY], 'readonly'); + const store = transaction.objectStore(STORES.CURRENT_PROXY); + const request = store.get('current'); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result || null); + }); + } catch (error) { + console.error('Error getting current proxy:', error); + return null; + } +} + +// 清除当前代理 +export async function clearCurrentProxy(): Promise { + try { + const db = await openDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORES.CURRENT_PROXY], 'readwrite'); + const store = transaction.objectStore(STORES.CURRENT_PROXY); + const request = store.delete('current'); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(true); + }); + } catch (error) { + console.error('Error clearing current proxy:', error); + return false; + } +} + // 启用指定的代理,禁用其他 export async function enableProxyConfig(id: string): Promise { try { + // 获取所有配置 const configs = await getAllProxyConfigs(); - for (const config of configs) { - const updated = { ...config, enabled: config.id === id }; - await saveProxyConfig(updated); + // 查找目标代理 + const targetProxy = configs.find(config => config.id === id); + if (!targetProxy) { + console.error(`找不到ID为 ${id} 的代理配置`); + return false; } + // 创建更新后的代理配置 + const updatedConfigs = configs.map(config => ({ + ...config, + enabled: config.id === id + })); + + // 保存到数据库 + const db = await openDB(); + const transaction = db.transaction([STORES.PROXY_CONFIGS], 'readwrite'); + const store = transaction.objectStore(STORES.PROXY_CONFIGS); + + // 逐个更新配置 + for (const config of updatedConfigs) { + await new Promise((resolve, reject) => { + const request = store.put(config); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(); + }); + } + + // 设置当前代理 + await setCurrentProxy({ + ...targetProxy, + enabled: true + }); + return true; } catch (error) { console.error(`Error enabling proxy config ${id}:`, error); @@ -117,13 +277,16 @@ export async function enableProxyConfig(id: string): Promise { export async function disableAllProxies(): Promise { try { const configs = await getAllProxyConfigs(); + const updatedConfigs = configs.map(config => ({ + ...config, + enabled: false + })); - for (const config of configs) { - if (config.enabled) { - const updated = { ...config, enabled: false }; - await saveProxyConfig(updated); - } - } + // 保存更新后的配置列表 + await saveProxyConfigs(updatedConfigs); + + // 清除当前代理 + await clearCurrentProxy(); return true; } catch (error) {