diff --git a/public/background.js b/public/background.js index c8e427a..a9a25cb 100644 --- a/public/background.js +++ b/public/background.js @@ -25,7 +25,6 @@ chrome.sidePanel.setOptions({ }); chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - console.log("msg", msg) switch (msg.action) { case ActionType.CONNECT: console.info("Start to connect websocket") diff --git a/public/manifest.json b/public/manifest.json index f7a74bd..b70a88b 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -24,7 +24,7 @@ }, "content_scripts": [ { - "matches": ["https://github.com/*"], + "matches": ["http://mitm/"], "run_at": "document_start", "js": ["proxy/content.js"] } diff --git a/public/proxy.js b/public/proxy.js index 5ca555a..4a53572 100644 --- a/public/proxy.js +++ b/public/proxy.js @@ -13,26 +13,47 @@ function getProxySettings() { async function handleSetProxyConfig(config, sendResponse) { try { - // 处理直接连接的情况 - if (config.proxyType === 'direct') { + // 处理代理服务器的情况 + if (config.proxyType === 'fixed_servers') { + // 固定代理服务器模式需要验证 host 和 port + if (!config || !config.host || !config.port) { + sendResponse({ + success: false, + error: '无效的代理配置:缺少主机或端口' + }); + return; + } + + const proxyConfig = { + mode: "fixed_servers", + rules: { + singleProxy: { + scheme: config.scheme || 'http', + host: config.host, + port: parseInt(config.port) + }, + bypassList: config.bypassList || ["localhost", "127.0.0.1"] + } + }; + await new Promise((resolve) => { chrome.proxy.settings.set({ - value: {mode: "direct"}, + value: proxyConfig, scope: 'regular' }, resolve); }); const settings = await getProxySettings(); - const isSuccess = settings.value.mode === "direct"; + const isSuccess = settings.value.mode === "fixed_servers" && + settings.value.rules.singleProxy.host === config.host && + settings.value.rules.singleProxy.port === parseInt(config.port); if (isSuccess) { - // 更新存储 await proxyStore.setCurrentProxy({ ...config, timestamp: Date.now() }); - // 更新代理列表状态 const configs = await proxyStore.getProxyConfigs(); const updatedConfigs = configs.map(c => ({ ...c, @@ -40,136 +61,123 @@ async function handleSetProxyConfig(config, sendResponse) { })); await proxyStore.saveProxyConfigs(updatedConfigs); - console.log('Direct connection set successfully'); + console.log('Proxy successfully set:', settings.value); sendResponse({success: true}); - + // 通知所有 content scripts 更新 await notifyProxyStatusChanged(); } else { - console.error('Failed to set direct connection'); + console.error('Proxy settings verification failed'); sendResponse({ success: false, - error: '无法设置直接连接' + error: '代理设置验证失败' }); } return; - } - - // 添加系统代理的处理 - if (config.proxyType === 'system') { - try { - // 先清除当前的代理设置 - await new Promise((resolve) => { - chrome.proxy.settings.clear({ - scope: 'regular' - }, resolve); - }); - - // 然后设置为系统代理 - await new Promise((resolve) => { - chrome.proxy.settings.set({ - value: {mode: "system"}, - scope: 'regular' - }, resolve); - }); - - const settings = await getProxySettings(); - const isSuccess = settings.value.mode === "system"; - - if (isSuccess) { - // 更新存储 - await proxyStore.setCurrentProxy({ - ...config, - timestamp: Date.now() - }); - - // 更新代理列表状态 - const configs = await proxyStore.getProxyConfigs(); - const updatedConfigs = configs.map(c => ({ - ...c, - enabled: c.id === config.id - })); - await proxyStore.saveProxyConfigs(updatedConfigs); - - console.log('System proxy set successfully'); - sendResponse({success: true}); - - // 通知所有 content scripts 更新 - await notifyProxyStatusChanged(); - } else { - console.error('Failed to set system proxy'); - sendResponse({ - success: false, - error: '无法设置系统代理' - }); - } - } catch (error) { - console.error('Error setting system proxy:', error); + } else if (config.proxyType === 'pac_script') { + // PAC 脚本模式需要验证 pacScript + if (!config || !config.pacScript || !config.pacScript.data) { sendResponse({ success: false, - error: error.message || '设置系统代理时发生错误' + error: '无效的 PAC 脚本配置' + }); + return; + } + + const proxyConfig = { + mode: "pac_script", + pacScript: config.pacScript + }; + + await new Promise((resolve) => { + chrome.proxy.settings.set({ + value: proxyConfig, + scope: 'regular' + }, resolve); + }); + + const settings = await getProxySettings(); + const isSuccess = settings.value.mode === "pac_script" && + settings.value.pacScript && + settings.value.pacScript.data; + + if (isSuccess) { + await proxyStore.setCurrentProxy({ + ...config, + timestamp: Date.now() + }); + + const configs = await proxyStore.getProxyConfigs(); + const updatedConfigs = configs.map(c => ({ + ...c, + enabled: c.id === config.id + })); + await proxyStore.saveProxyConfigs(updatedConfigs); + + console.log('PAC script proxy successfully set:', settings.value); + sendResponse({success: true}); + + // 通知所有 content scripts 更新 + await notifyProxyStatusChanged(); + } else { + console.error('PAC script settings verification failed', { + expected: config, + actual: settings.value + }); + sendResponse({ + success: false, + error: '代理设置验证失败' }); } return; - } + } else if (config.proxyType === 'direct' || config.proxyType === 'system') { + // 直接连接或系统代理模式 + const proxyConfig = { + mode: config.proxyType + }; - // 处理代理服务器的情况 - if (!config || !config.host || !config.port) { - sendResponse({ - success: false, - error: '无效的代理配置' + await new Promise((resolve) => { + chrome.proxy.settings.set({ + value: proxyConfig, + scope: 'regular' + }, resolve); }); - return; - } - const proxyConfig = { - mode: "fixed_servers", - rules: { - singleProxy: { - scheme: config.scheme || 'http', - host: config.host, - port: parseInt(config.port) - }, - bypassList: ["localhost", "127.0.0.1"] + const settings = await getProxySettings(); + const isSuccess = settings.value.mode === config.proxyType; + + if (isSuccess) { + await proxyStore.setCurrentProxy({ + ...config, + timestamp: Date.now() + }); + + const configs = await proxyStore.getProxyConfigs(); + const updatedConfigs = configs.map(c => ({ + ...c, + enabled: c.id === config.id + })); + await proxyStore.saveProxyConfigs(updatedConfigs); + + console.log('Proxy successfully set:', settings.value); + sendResponse({success: true}); + + // 通知所有 content scripts 更新 + await notifyProxyStatusChanged(); + } else { + console.error('Proxy settings verification failed'); + sendResponse({ + success: false, + error: '代理设置验证失败' + }); } - }; - - await new Promise((resolve) => { - chrome.proxy.settings.set({ - value: proxyConfig, - scope: 'regular' - }, resolve); - }); - - const settings = await getProxySettings(); - const isSuccess = settings.value.mode === "fixed_servers" && - settings.value.rules.singleProxy.host === config.host && - settings.value.rules.singleProxy.port === parseInt(config.port); - - if (isSuccess) { - await proxyStore.setCurrentProxy({ - ...config, - timestamp: Date.now() - }); - - const configs = await proxyStore.getProxyConfigs(); - const updatedConfigs = configs.map(c => ({ - ...c, - enabled: c.id === config.id - })); - await proxyStore.saveProxyConfigs(updatedConfigs); - - console.log('Proxy successfully set:', settings.value); - sendResponse({success: true}); - - // 通知所有 content scripts 更新 - await notifyProxyStatusChanged(); + return; } else { - console.error('Proxy settings verification failed'); sendResponse({ success: false, - error: '代理设置验证失败' + error: '不支持的代理类型' }); + return; } } catch (error) { console.error('Error setting proxy:', error); @@ -192,9 +200,9 @@ async function handleClearProxyConfig(sendResponse) { enabled: false })); await proxyStore.saveProxyConfigs(updatedConfigs); - + sendResponse({ success: true }); - + // 通知所有 content scripts 更新 await notifyProxyStatusChanged(); } catch (error) { @@ -276,51 +284,73 @@ async function queueProxyLog(details, error = null) { } } -// 添加 URL 拦截处理 -function setupUrlInterceptor() { - chrome.webNavigation.onBeforeNavigate.addListener(async (details) => { - try { - const url = new URL(details.url); - - if (url.hostname === 'mitm') { - // 解析参数 - const host = url.searchParams.get('host'); - const port = parseInt(url.searchParams.get('port') || '0'); - const scheme = url.searchParams.get('scheme'); +// 添加检查和设置初始代理的函数 +async function checkAndSetInitialProxy() { + try { + // 确保默认配置存在 + await ProxySettings.setDefaultConfigs(); - if (host && port && scheme) { - // 创建新的代理配置 - const newConfig = { - id: Date.now().toString(), - name: "Yakit MITM", - proxyType: 'fixed_servers', - scheme: scheme, - host: host, - port: port, - enabled: true - }; + // 获取当前的代理设置 + const settings = await getProxySettings(); + console.log('Current proxy settings:', settings); - // 直接使用 proxyStore 实例的方法 - const result = await proxyStore.addAndEnableProxy(newConfig); - - if (result.success) { - // 重定向回 mitm 页面 - chrome.tabs.update(details.tabId, { - url: "http://mitm/" - }); - } else { - console.error('Failed to add and enable proxy:', result.error); - } - } + // 检查是否存在固定代理服务器设置 + if (settings.value.mode === "fixed_servers" && + settings.value.rules && + settings.value.rules.singleProxy) { + + const proxy = settings.value.rules.singleProxy; + + // 获取现有配置 + const existingConfigs = await proxyStore.getProxyConfigs(); + + // 检查是否已存在相同的 MITM 配置 + const existingMitm = existingConfigs.find(config => + config.host === proxy.host && + config.port === proxy.port && + config.scheme === proxy.scheme + ); + + if (!existingMitm) { + // 创建新的 MITM 配置 + const newConfig = { + id: Date.now().toString(), + name: "Yakit MITM", + proxyType: 'fixed_servers', + scheme: proxy.scheme || 'http', + host: proxy.host, + port: proxy.port, + enabled: true, + bypassList: ["localhost", "127.0.0.1"], + matchList: [] + }; + + // 添加到现有配置中 + const updatedConfigs = [...existingConfigs, newConfig]; + await proxyStore.saveProxyConfigs(updatedConfigs); + + // 启用新配置 + await handleSetProxyConfig(newConfig, () => {}); + + console.log('Added and enabled Yakit MITM config from existing proxy settings'); + return; } - } catch (error) { - console.error('Error handling proxy URL:', error); } - }, { - url: [{ - hostEquals: 'mitm' - }] - }); + + // 如果没有检测到代理或已存在配置,则设置为系统代理 + await handleSetProxyConfig({ + id: 'system', + name: '[系统代理]', + proxyType: 'system', + enabled: true + }, () => {}); + + // 设置认证监听 + await ProxyAuth.setupAuthListener(); + + } catch (error) { + console.error('Error during initialization:', error); + } } // 修改 setupProxyHandlers 函数 @@ -331,9 +361,6 @@ export function setupProxyHandlers() { // 设置代理请求监听器 setupProxyRequestListener(); - - // 设置 URL 拦截器 - setupUrlInterceptor(); // 消息监听器 chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { @@ -458,44 +485,17 @@ export function setupProxyHandlers() { }); sendResponse({ success: true }); return true; - return handleClearProxyConfig(sendResponse); } }); // 在扩展启动时初始化 chrome.runtime.onInstalled.addListener(async () => { - try { - // 确保默认配置存在 - await ProxySettings.setDefaultConfigs(); - - // 设置为系统代理 - await handleSetProxyConfig({ - id: 'system', - name: '[系统代理]', - proxyType: 'system', - enabled: true - }, () => {}); - - // 设置认证监听 - await ProxyAuth.setupAuthListener(); - } catch (error) { - console.error('Error during installation:', error); - } + await checkAndSetInitialProxy(); }); - // 添加启动时的初始化 + // 浏览器启动时初始化 chrome.runtime.onStartup.addListener(async () => { - try { - // 设置为系统代理 - await handleSetProxyConfig({ - id: 'system', - name: '[系统代理]', - proxyType: 'system', - enabled: true - }, () => {}); - } catch (error) { - console.error('Error during startup:', error); - } + await checkAndSetInitialProxy(); }); } @@ -509,4 +509,43 @@ async function notifyProxyStatusChanged() { // 忽略不支持的标签页 } } +} + +async function setProxyConfig(config) { + try { + let chromeProxyConfig; + + if (config.proxyType === 'pac_script') { + chromeProxyConfig = { + mode: "pac_script", + pacScript: config.pacScript + }; + } else if (config.proxyType === 'fixed_servers') { + chromeProxyConfig = { + mode: "fixed_servers", + rules: { + singleProxy: { + scheme: config.scheme, + host: config.host, + port: config.port + }, + bypassList: config.bypassList || [] + } + }; + } else { + chromeProxyConfig = { + mode: config.proxyType // direct, system, auto_detect + }; + } + + await chrome.proxy.settings.set({ + value: chromeProxyConfig, + scope: 'regular' + }); + + return { success: true }; + } catch (error) { + console.error('Failed to set proxy config:', error); + return { success: false, error: error.message }; + } } \ No newline at end of file diff --git a/public/proxy/content.js b/public/proxy/content.js index 9917a30..48aaf07 100644 --- a/public/proxy/content.js +++ b/public/proxy/content.js @@ -16,8 +16,7 @@ const YAK_ICON_URL = chrome.runtime.getURL('/images/yak.svg'); async function sendMessageWithRetry(message, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { - const response = await chrome.runtime.sendMessage(message); - return response; + return await chrome.runtime.sendMessage(message); } catch (error) { console.warn(`Attempt ${i + 1} failed:`, error); if (i === maxRetries - 1) { @@ -36,40 +35,23 @@ async function getCurrentProxy() { action: ProxyActionType.GET_PROXY_STATUS }); - if (!response) { - console.log('No response from background script'); - return { enable: false, proxy: '' }; - } - - // 检查响应格式 - if (!response.success) { - console.error('Error in proxy status response:', response.error); - return { enable: false, proxy: '' }; + if (!response || !response.success) { + console.log('No valid response from background script'); + return { enable: false, proxy: '', currentMode: 'direct' }; } const status = response.data; console.log('Proxy status from background:', status); - // 根据状态返回正确的格式 - if (status.mode === 'fixed_servers' && status.enabled && status.config) { - const config = status.config; - // 只在确实有配置时才返回代理信息 - if (config.scheme && config.host && config.port) { - return { - enable: true, - proxy: `${config.scheme}://${config.host}:${config.port}` - }; - } - } - - // 对于直连或系统代理,返回相应状态 + // 返回当前模式 return { - enable: false, - proxy: status.mode === 'system' ? 'system' : '' + enable: status.enabled, + proxy: status.mode === 'system' ? 'system' : '', + currentMode: status.mode || 'direct' // 使用 mode 来判断当前激活的代理 }; } catch (error) { console.error('Error getting proxy status:', error); - return { enable: false, proxy: '' }; + return { enable: false, proxy: '', currentMode: 'direct' }; } } @@ -99,12 +81,36 @@ async function getProxyConfigs() { async function switchProxy(config) { try { console.log('Switching proxy:', config); + + // 根据配置类型构建正确的配置对象 + let proxyConfig; + if (config.proxyType === 'system') { + proxyConfig = { + id: 'system', + name: '[系统代理]', + proxyType: 'system', + enabled: true + }; + } else if (config.proxyType === 'direct') { + proxyConfig = { + id: 'direct', + name: '[直接连接]', + proxyType: 'direct', + enabled: false + }; + } else { + proxyConfig = { + ...config, + enabled: true + }; + } + await sendMessageWithRetry({ action: ProxyActionType.SET_PROXY_CONFIG, - config: config + config: proxyConfig }); - await updatePanel(); + await PanelManager.updatePanel(); } catch (error) { console.error('Error switching proxy:', error); } @@ -124,410 +130,55 @@ async function clearProxy() { }); console.log('Clear proxy response:', response); - await updatePanel(); + // 使用 PanelManager 的 updatePanel 方法 + await PanelManager.updatePanel(); } catch (error) { console.error('Error clearing proxy:', error); } } -// 更新面板显示 -async function updatePanel() { - const panel = document.getElementById('yakit-proxy-panel')?.shadowRoot?.querySelector('.panel-content'); - if (!panel) return; - - const currentProxy = await getCurrentProxy(); - const configs = await getProxyConfigs(); +// 修改 PanelManager +const PanelManager = { + panel: null, + messageListener: null, + _updating: false, + _updateQueue: Promise.resolve(), + _currentState: null, // 用于跟踪当前状态 + _lastUpdate: null, // 添加最后更新时间戳 + _lastState: null, + _pollingInterval: null, - console.log('Current proxy status:', currentProxy); - - // 修改这里的判断逻辑 - let html = ` -
- 🟢 - [直接连接] - -
-
- ⚙️ - [系统代理] - -
-
- `; - - // 修改自定义代理配置的判断逻辑 - configs.forEach(config => { - if (config.id !== 'direct' && config.id !== 'system') { - const proxyUrl = `${config.scheme}://${config.host}:${config.port}`; - const isActive = currentProxy.enable && currentProxy.proxy === proxyUrl && config.enabled; - - // 添加 title 属性显示详细信息 - const tooltipText = `${config.scheme.toUpperCase()} ${config.host}:${config.port}`; - - html += ` -
- 🌐 - ${config.name} - -
- `; - } - }); - - // 修改添加操作按钮部分 - html += ` -
- - 添加代理... -
-
- 👨‍💻 - 选项 -
- `; - - panel.innerHTML = html; - - // 添加事件监听 - panel.querySelectorAll('.proxy-item').forEach(item => { - item.addEventListener('click', async () => { - const id = item.dataset.id; - if (id === 'direct') { - await clearProxy(); - } else if (id === 'system') { - await switchProxy({ - id: 'system', - name: '[系统代理]', - proxyType: 'system' - }); - } else { - const config = configs.find(c => c.id === id); - if (config) { - await switchProxy(config); - } - } - }); - }); - - // 修改添加代理和选项按钮的事件处理 - panel.querySelector('.add-proxy')?.addEventListener('click', async () => { - try { - // 通过发送消息给 background script 来处理添加代理 - await sendMessageWithRetry({ - action: 'OPEN_OPTIONS_PAGE', - triggerAdd: true // 标记需要触发添加代理 - }); - } catch (error) { - console.error('Error handling add proxy:', error); - } - }); - - panel.querySelector('.settings')?.addEventListener('click', async () => { - try { - // 通过发送消息给 background script 来打开选项页 - await sendMessageWithRetry({ - action: 'OPEN_OPTIONS_PAGE' - }); - } catch (error) { - console.error('Error opening options page:', error); - } - }); -} - -// 创建并注入悬浮框 -function createFloatingPanel() { - console.log("Creating floating panel..."); - - // 检查是否已存在面板 - if (document.getElementById('yakit-proxy-panel')) { - return; - } - - // 创建容器 - const container = document.createElement('div'); - container.id = 'yakit-proxy-panel'; - - // 创建 shadow DOM - const shadow = container.attachShadow({ mode: 'open' }); - - // 添加样式 - const style = document.createElement('style'); - style.textContent = ` - .floating-panel { - position: fixed; - top: 20px; - right: 0; - width: 180px; - background: white; - border-radius: 8px 0 0 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); - z-index: 2147483647; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - transition: transform 0.3s ease; + init() { + // 确保 document.body 存在 + if (!document.body) { + console.log('Body not ready, waiting...'); + this.waitForBody(); + return; } - .floating-panel.collapsed { - transform: translateX(100%); + if (this.panel) { + console.log('Panel already exists, updating...'); + this.updatePanel(); + return; + } + + console.log('Creating new panel...'); + this.createPanel(); + }, + + // 添加等待 body 的方法 + waitForBody() { + if (document.body) { + this.init(); + return; } - .panel-header { - padding: 4px; - border-bottom: 1px solid #eee; - border-radius: 8px 0 0 0; - background: #f8f9fa; - display: flex; - justify-content: space-between; - align-items: center; - } - - .collapse-trigger { - position: absolute; - left: -20px; - top: 0; - width: 20px; - height: 100%; - background: #f8f9fa; - border-radius: 8px 0 0 8px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - box-shadow: -2px 0 5px rgba(0,0,0,0.1); - } - - .collapse-trigger:hover { - background: #e9ecef; - } - - .collapse-trigger::after { - content: '◀'; - transition: transform 0.3s ease; - } - - .floating-panel.collapsed .collapse-trigger::after { - transform: rotate(180deg); - } - - .panel-content { - padding: 4px; - } - - .proxy-item { - position: relative; - display: flex; - align-items: center; - padding: 4px 10px; - cursor: pointer; - transition: all 0.2s; - color: #666; - border-left: 3px solid transparent; - overflow: hidden; - } - - .proxy-item > span { - position: relative; - z-index: 1; - } - - .watermark-icon { - position: absolute; - right: 0; - width: 100%; - height: 100%; - opacity: 0.3; - pointer-events: none; - display: none; - object-fit: contain; - object-position: right center; - padding: 4px; - } - - .proxy-item.active .watermark-icon { - display: block; - } - - .proxy-item.active { - color: #ff6b00 !important; - background: #fff7e6; - border-left: 3px solid #ff6b00; - } - - .proxy-item.active span:first-child { - color: #ff6b00 !important; - } - - .proxy-item span:first-child { - margin-right: 8px; - font-size: 16px; - color: #666; - transition: color 0.2s; - } - - .proxy-item:hover { - background: #f5f5f5; - } - - .proxy-item:hover span:first-child { - color: #ff6b00; - } - - .add-proxy { - display: flex; - align-items: center; - padding: 4px 10px; - color: #1890ff; - cursor: pointer; - border-top: 1px solid #eee; - transition: all 0.2s; - } - - .add-proxy:hover { - background: #f5f5f5; - } - - .settings { - padding: 4px 10px; - color: #666; - cursor: pointer; - border-top: 1px solid #eee; - transition: all 0.2s; - } - - .settings:hover { - background: #f5f5f5; - } - - .panel-header .header-content { - display: flex; - align-items: center; - gap: 8px; - padding: 0 4px; - } - - .yak-icon { - width: 24px; - height: 24px; - object-fit: contain; - } - - .divider { - height: 1px; - background-color: #eee; - margin: 4px 0; - } - `; - - // 创建面板内容 - const panel = document.createElement('div'); - panel.className = 'floating-panel'; - panel.innerHTML = ` -
-
-
- Yak - 代理设置 -
-
-
-
- 🟢 - [直接连接] -
-
- ⚙️ - [系统代理] -
-
-
- - [添加代理...] -
-
- 👨‍💻 - 选项 -
-
- `; - - // 将样式和面板添加到 shadow DOM - shadow.appendChild(style); - shadow.appendChild(panel); - - // 将容器添加到页面 - document.body.appendChild(container); - - // 添加事件监听器 - const addEventListeners = () => { - // 收起/展开功能 - panel.querySelector('.collapse-trigger')?.addEventListener('click', (e) => { - e.stopPropagation(); - panel.classList.toggle('collapsed'); - }); - - // 添加代理按钮 - panel.querySelector('.add-proxy')?.addEventListener('click', () => { - chrome.runtime.sendMessage({ action: 'OPEN_OPTIONS_PAGE' }); - }); - - // 设置按钮 - panel.querySelector('.settings')?.addEventListener('click', () => { - chrome.runtime.sendMessage({ action: 'OPEN_OPTIONS_PAGE' }); - }); - }; - - // 初始化事件监听器 - addEventListeners(); - - // 初始更新面板 - updatePanel(); - - // 添加代理变化监听 - let messageListener = (message) => { - if (message.action === 'PROXY_STATUS_CHANGED') { - updatePanel(); - } - }; - - // 确保只添加一次监听器 - chrome.runtime.onMessage.removeListener(messageListener); - chrome.runtime.onMessage.addListener(messageListener); - - // 验证面板是否成功创建 - console.log("Panel created:", { - containerExists: !!document.getElementById('yakit-proxy-panel'), - containerVisible: window.getComputedStyle(container).display !== 'none', - shadowRoot: !!container.shadowRoot, - panelElement: !!container.shadowRoot?.querySelector('.floating-panel') - }); -} - -// 使用 MutationObserver 确保在 DOM 准备好时创建面板 -function initPanel() { - if (document.body) { - console.log("Body found, creating panel"); - // 确保background script已经准备好 - sendMessageWithRetry({ action: 'PING' }) - .then(() => { - createFloatingPanel(); - }) - .catch(error => { - console.error('Failed to initialize panel:', error); - // 可以在这里添加重试逻辑 - setTimeout(initPanel, 1000); - }); - } else { - console.log("Body not found, waiting..."); + console.log('Setting up MutationObserver for body'); const observer = new MutationObserver((mutations, obs) => { if (document.body) { - console.log("Body found via observer"); + console.log('Body found via observer'); obs.disconnect(); - initPanel(); + this.init(); } }); @@ -535,18 +186,528 @@ function initPanel() { childList: true, subtree: true }); - } -} + }, + + createPanel() { + if (!document.body) { + console.log('Body not available during panel creation'); + return; + } -// 尝试多种方式来确保面板被创建 + // 创建容器 + const container = document.createElement('div'); + container.id = 'yakit-proxy-panel'; + + // 创建 shadow DOM + const shadow = container.attachShadow({ mode: 'open' }); + + // 添加样式 + const style = document.createElement('style'); + style.textContent = ` + .floating-panel { + position: fixed; + top: 20px; + right: 0; + width: 180px; + background: white; + border-radius: 8px 0 0 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + z-index: 2147483647; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + transition: transform 0.3s ease; + } + + .floating-panel.collapsed { + transform: translateX(100%); + } + + .panel-header { + padding: 4px; + border-bottom: 1px solid #eee; + border-radius: 8px 0 0 0; + background: #f8f9fa; + display: flex; + justify-content: space-between; + align-items: center; + } + + .collapse-trigger { + position: absolute; + left: -20px; + top: 0; + width: 20px; + height: 100%; + background: #f8f9fa; + border-radius: 8px 0 0 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: -2px 0 5px rgba(0,0,0,0.1); + } + + .collapse-trigger:hover { + background: #e9ecef; + } + + .collapse-trigger::after { + content: '◀'; + transition: transform 0.3s ease; + } + + .floating-panel.collapsed .collapse-trigger::after { + transform: rotate(180deg); + } + + .panel-content { + padding: 4px; + } + + .proxy-item { + position: relative; + display: flex; + align-items: center; + padding: 4px 10px; + cursor: pointer; + transition: all 0.2s; + color: #666; + border-left: 3px solid transparent; + overflow: hidden; + } + + .proxy-item > span { + position: relative; + z-index: 1; + } + + .watermark-icon { + position: absolute; + right: 0; + width: 100%; + height: 100%; + opacity: 0.3; + pointer-events: none; + display: none; + object-fit: contain; + object-position: right center; + padding: 4px; + } + + .proxy-item.active .watermark-icon { + display: block; + } + + .proxy-item.active { + color: #ff6b00 !important; + background: #fff7e6; + border-left: 3px solid #ff6b00; + } + + .proxy-item.active span:first-child { + color: #ff6b00 !important; + } + + .proxy-item span:first-child { + margin-right: 8px; + font-size: 16px; + color: #666; + transition: color 0.2s; + } + + .proxy-item:hover { + background: #f5f5f5; + } + + .proxy-item:hover span:first-child { + color: #ff6b00; + } + + .add-proxy { + display: flex; + align-items: center; + padding: 4px 10px; + color: #1890ff; + cursor: pointer; + border-top: 1px solid #eee; + transition: all 0.2s; + } + + .add-proxy:hover { + background: #f5f5f5; + } + + .settings { + padding: 4px 10px; + color: #666; + cursor: pointer; + border-top: 1px solid #eee; + transition: all 0.2s; + } + + .settings:hover { + background: #f5f5f5; + } + + .panel-header .header-content { + display: flex; + align-items: center; + gap: 8px; + padding: 0 4px; + } + + .yak-icon { + width: 24px; + height: 24px; + object-fit: contain; + } + + .divider { + height: 1px; + background-color: #eee; + margin: 4px 0; + } + `; + + // 创建面板内容 + const panel = document.createElement('div'); + panel.className = 'floating-panel'; + panel.innerHTML = ` +
+
+
+ Yak + 代理设置 +
+
+
+
+ 🟢 + [直接连接] +
+
+ ⚙️ + [系统代理] +
+
+
+ + [添加代理...] +
+
+ 👨‍💻 + 选项 +
+
+ `; + + // 将样式和面板添加到 shadow DOM + shadow.appendChild(style); + shadow.appendChild(panel); + + // 确保安全地添加到 body + try { + document.body.appendChild(container); + this.panel = container; + + // 添加折叠触发器的点击事件 + const floatingPanel = shadow.querySelector('.floating-panel'); + const collapseTrigger = shadow.querySelector('.collapse-trigger'); + + collapseTrigger.addEventListener('click', () => { + floatingPanel.classList.toggle('collapsed'); + }); + + // 设置消息监听和开始轮询 + this.setupMessageListener(); + + // 初始更新面板 + this.updatePanel(); + + // 添加页面卸载时的清理 + window.addEventListener('unload', () => { + if (this._pollingInterval) { + clearInterval(this._pollingInterval); + } + if (this.messageListener) { + chrome.runtime.onMessage.removeListener(this.messageListener); + } + }); + } catch (error) { + console.error('Error creating panel:', error); + } + }, + + setupMessageListener() { + if (this.messageListener) { + chrome.runtime.onMessage.removeListener(this.messageListener); + } + + this.messageListener = async (message, sender) => { + // 只检查消息是否来自同一个扩展 + if (sender.id !== chrome.runtime.id) { + return; + } + + // 处理状态更新消息 + if (message.action === 'PROXY_STATUS_CHANGED' || + message.action === 'PROXY_CONFIGS_UPDATED') { + console.log('Received update message:', message, 'from:', sender); + await this.updatePanel(); + } + }; + + chrome.runtime.onMessage.addListener(this.messageListener); + }, + + async updatePanel() { + if (!this.panel || !document.body.contains(this.panel)) { + console.log('Panel not in document, recreating...'); + this.createPanel(); + return; + } + + const panel = this.panel.shadowRoot?.querySelector('.panel-content'); + if (!panel) return; + + // 使用更新队列确保更新按顺序执行 + this._updateQueue = this._updateQueue.then(async () => { + if (this._updating) { + console.log('Update already in progress, skipping...'); + return; + } + + try { + this._updating = true; + + // 获取最新状态 + const [currentProxy, configs] = await Promise.all([ + getCurrentProxy(), + getProxyConfigs() + ]); + + // 状态没有变化时不更新 + const newState = JSON.stringify({ currentProxy, configs }); + if (this._currentState === newState) { + console.log('State unchanged, skipping update'); + return; + } + this._currentState = newState; + + // 再次检查面板状态 + if (!this.panel || !document.body.contains(this.panel)) { + console.log('Panel was removed during data fetch'); + return; + } + + console.log('Updating panel with:', { currentProxy, configs }); + + if (!Array.isArray(configs)) { + console.error('Invalid configs:', configs); + return; + } + + // 保存当前激活的项 + const currentActiveId = panel.querySelector('.proxy-item.active')?.dataset.id; + + // 构建新的 HTML + const newHtml = this._buildPanelHtml(currentProxy, configs); + + // 创建一个临时容器来比较内容 + const temp = document.createElement('div'); + temp.innerHTML = newHtml; + + // 只在内容真正改变时更新 + if (panel.innerHTML !== temp.innerHTML) { + requestAnimationFrame(() => { + panel.innerHTML = newHtml; + this._bindEventListeners(panel, configs, currentProxy); + + // 验证更新后的状态 + const newActiveId = panel.querySelector('.proxy-item.active')?.dataset.id; + if (currentActiveId !== newActiveId) { + console.log('Active state changed:', { + from: currentActiveId, + to: newActiveId + }); + } + }); + } + } catch (error) { + console.error('Error updating panel:', error); + } finally { + this._updating = false; + } + }).catch(error => { + console.error('Error in update queue:', error); + this._updating = false; + }); + + return this._updateQueue; + }, + + // 将 HTML 构建逻辑抽离成单独的方法 + _buildPanelHtml(currentProxy, configs) { + let html = ` +
+ 🟢 + [直接连接] + +
+
+ ⚙️ + [系统代理] + +
+
+ `; + + // 添加自定义代理配置 + configs.forEach(config => { + if (config.proxyType !== 'direct' && config.proxyType !== 'system') { + // 判断是否激活:当前模式为 fixed_servers 且配置已启用 + const isActive = currentProxy.currentMode === 'fixed_servers' && + config.enabled; + + const tooltipText = config.proxyType === 'pac_script' + ? 'PAC Script' + : `${(config.proxyType || 'HTTP').toUpperCase()} ${config.host || ''}:${config.port || ''}`; + + const proxyIcon = config.proxyType === 'pac_script' ? '📜' : '🌐'; + + html += ` +
+ ${proxyIcon} + ${config.name || '未命名代理'} + +
+ `; + } + }); + + // 添加操作按钮 + html += ` +
+ + 添加代理... +
+
+ 👨‍💻 + 选项 +
+ `; + + return html; + }, + + _bindEventListeners(panel, configs, currentProxy) { + // 代理项点击事件 + panel.querySelectorAll('.proxy-item').forEach(item => { + const id = item.dataset.id; + + // 使用事件委托来提高性能 + const clickHandler = async (e) => { + e.stopPropagation(); + + if (this._updating) { + console.log('Panel is updating, ignoring click'); + return; + } + + // 添加点击反馈 + const originalOpacity = item.style.opacity; + item.style.opacity = '0.7'; + + try { + // 立即更新 UI 状态,不等待响应 + panel.querySelectorAll('.proxy-item').forEach(i => { + i.classList.remove('active'); + i.querySelector('span').style.color = '#666'; + }); + item.classList.add('active'); + item.querySelector('span').style.color = '#ff6b00'; + + if (id === 'direct') { + await clearProxy(); + } else if (id === 'system') { + await switchProxy({ + id: 'system', + name: '[系统代理]', + proxyType: 'system' + }); + } else { + const config = configs.find(c => c.id === id); + if (config) { + await switchProxy(config); + } + } + } catch (error) { + console.error('Error handling proxy item click:', error); + // 发生错误时恢复原状 + await this.updatePanel(); + } finally { + item.style.opacity = originalOpacity; + } + }; + + // 使用 { once: true } 确保事件监听器不会重复 + item.addEventListener('click', clickHandler, { once: true }); + }); + + // 添加代理按钮 + panel.querySelector('.add-proxy')?.addEventListener('click', async () => { + try { + await sendMessageWithRetry({ + action: 'OPEN_OPTIONS_PAGE', + triggerAdd: true + }); + } catch (error) { + console.error('Error handling add proxy:', error); + } + }); + + // 设置按钮 + panel.querySelector('.settings')?.addEventListener('click', async () => { + try { + await sendMessageWithRetry({ + action: 'OPEN_OPTIONS_PAGE' + }); + } catch (error) { + console.error('Error opening options page:', error); + } + }); + } +}; + +// 修改初始化调用 console.log("Setting up initialization..."); +// 根据文档状态决定初始化方式 +if (document.readyState === 'loading') { + console.log('Document still loading, waiting for DOMContentLoaded'); + document.addEventListener('DOMContentLoaded', () => { + console.log('DOMContentLoaded fired'); + PanelManager.init(); + }); +} else { + console.log('Document already loaded, initializing immediately'); + PanelManager.init(); +} + +// 保留 load 事件作为备份 window.addEventListener('load', () => { console.log("Window load triggered"); - initPanel(); + if (!PanelManager.panel) { + PanelManager.init(); + } }); -// 在关键位置添加更多日志 +// 添加更详细的日志 console.log("Document readyState:", document.readyState); console.log("Document body exists:", !!document.body); console.log("Document documentElement exists:", !!document.documentElement); diff --git a/public/proxy/options.js b/public/proxy/options.js deleted file mode 100644 index 8d83f40..0000000 --- a/public/proxy/options.js +++ /dev/null @@ -1,167 +0,0 @@ -console.log('Options page script loaded'); - -import { ProxySettings } from './proxy-settings.js'; - -let currentConfigs = []; - -document.addEventListener('DOMContentLoaded', async () => { - try { - await initOptionsPage(); - setupEventListeners(); - } catch (error) { - console.error('Failed to initialize options page:', error); - showError(error.message); - } -}); - -async function initOptionsPage() { - const result = await ProxySettings.exportSettings(); - currentConfigs = result.settings || []; - renderProxyConfigs(); -} - -function renderProxyConfigs() { - const proxyList = document.getElementById('proxyList'); - const template = document.getElementById('proxyItemTemplate'); - - // 清除除了直接连接以外的所有配置 - const items = proxyList.querySelectorAll('.proxy-item:not([data-id="direct"])'); - items.forEach(item => item.remove()); - - // 渲染其他代理配置 - currentConfigs.forEach(config => { - if (config.id === 'direct') return; // 跳过直接连接 - - const clone = template.content.cloneNode(true); - const proxyItem = clone.querySelector('.proxy-item'); - - proxyItem.dataset.id = config.id; - proxyItem.querySelector('.proxy-name').value = config.name; - proxyItem.querySelector('.proxy-type-select').value = config.proxyType; - - updateProxyTypeSettings(proxyItem, config); - proxyList.appendChild(proxyItem); - }); -} - -function updateProxyTypeSettings(proxyItem, config) { - const serverSettings = proxyItem.querySelector('.proxy-server-settings'); - const pacSettings = proxyItem.querySelector('.pac-script-settings'); - - if (config.proxyType === 'fixed_servers') { - serverSettings.style.display = 'block'; - pacSettings.style.display = 'none'; - - proxyItem.querySelector('.proxy-scheme').value = config.scheme || 'http'; - proxyItem.querySelector('.proxy-host').value = config.host || ''; - proxyItem.querySelector('.proxy-port').value = config.port || ''; - } - else if (config.proxyType === 'pac_script') { - serverSettings.style.display = 'none'; - pacSettings.style.display = 'block'; - - proxyItem.querySelector('.pac-script').value = config.pacScript || ''; - } - else { - serverSettings.style.display = 'none'; - pacSettings.style.display = 'none'; - } -} - -function setupEventListeners() { - // 添加代理按钮 - document.getElementById('addProxy').addEventListener('click', addNewProxy); - - // 代理列表变更事件 - document.getElementById('proxyList').addEventListener('change', handleProxyChange); - - // 删除代理按钮 - document.getElementById('proxyList').addEventListener('click', handleProxyDelete); -} - -async function addNewProxy() { - const newConfig = { - id: Date.now().toString(), - name: '新建代理', - proxyType: 'fixed_servers', - scheme: 'http', - host: '127.0.0.1', - port: 8080 - }; - - currentConfigs = [...currentConfigs, newConfig]; - await ProxySettings.importSettings(currentConfigs); - renderProxyConfigs(); -} - -async function handleProxyChange(e) { - const proxyItem = e.target.closest('.proxy-item'); - if (!proxyItem) return; - - const configId = proxyItem.dataset.id; - const configIndex = currentConfigs.findIndex(c => c.id === configId); - if (configIndex === -1) return; - - const updatedConfig = { ...currentConfigs[configIndex] }; - - if (e.target.classList.contains('proxy-name')) { - updatedConfig.name = e.target.value; - } else if (e.target.classList.contains('proxy-type-select')) { - updatedConfig.proxyType = e.target.value; - // 如果切换到固定服务器模式,设置默认值 - if (updatedConfig.proxyType === 'fixed_servers' && !updatedConfig.scheme) { - updatedConfig.scheme = 'http'; - updatedConfig.host = '127.0.0.1'; - updatedConfig.port = 8080; - } - } else if (e.target.classList.contains('proxy-scheme')) { - updatedConfig.scheme = e.target.value; - } else if (e.target.classList.contains('proxy-host')) { - updatedConfig.host = e.target.value; - } else if (e.target.classList.contains('proxy-port')) { - updatedConfig.port = parseInt(e.target.value) || ''; - } else if (e.target.classList.contains('pac-script')) { - updatedConfig.pacScript = e.target.value; - } - - currentConfigs[configIndex] = updatedConfig; - await ProxySettings.importSettings(currentConfigs); - - // 如果代理类型改变,需要更新UI - if (e.target.classList.contains('proxy-type-select')) { - updateProxyTypeSettings(proxyItem, updatedConfig); - } -} - -async function handleProxyDelete(e) { - const deleteBtn = e.target.closest('.delete-btn'); - if (!deleteBtn) return; - - const proxyItem = deleteBtn.closest('.proxy-item'); - const configId = proxyItem.dataset.id; - - currentConfigs = currentConfigs.filter(c => c.id !== configId); - await ProxySettings.importSettings(currentConfigs); - renderProxyConfigs(); -} - -function showError(message) { - const container = document.querySelector('.container'); - const errorDiv = document.createElement('div'); - errorDiv.className = 'error-message'; - errorDiv.style.cssText = ` - color: #ff4d4f; - padding: 8px 16px; - margin-bottom: 16px; - background-color: #fff2f0; - border: 1px solid #ffccc7; - border-radius: 4px; - `; - errorDiv.textContent = message; - container.insertBefore(errorDiv, container.firstChild); - - // 5秒后自动移除错误信息 - setTimeout(() => { - errorDiv.remove(); - }, 3000); -} \ No newline at end of file diff --git a/public/types/action.js b/public/types/action.js index 0fb8da0..cbca12a 100644 --- a/public/types/action.js +++ b/public/types/action.js @@ -9,4 +9,5 @@ export const ProxyActionType = { GET_PROXY_CONFIGS: "GET_PROXY_CONFIGS", ADD_PROXY_CONFIG: "ADD_PROXY_CONFIG", UPDATE_PROXY_CONFIG: "UPDATE_PROXY_CONFIG", + OPEN_OPTIONS_PAGE: "OPEN_OPTIONS_PAGE", }; \ No newline at end of file diff --git a/src/components/ProxySwitch/index.tsx b/src/components/ProxySwitch/index.tsx index de927bb..c216ad2 100644 --- a/src/components/ProxySwitch/index.tsx +++ b/src/components/ProxySwitch/index.tsx @@ -57,12 +57,25 @@ export const ProxySwitch: React.FC = () => { const [customProxies, setCustomProxies] = useState([]); const [isLoading, setIsLoading] = useState(false); + // 修改存储变化监听 + useEffect(() => { + const handleMessage = (message: any) => { + if (message.action === 'PROXY_CONFIGS_UPDATED' && message.source !== 'proxy_switch') { + loadCustomProxies(); + } + }; + + chrome.runtime.onMessage.addListener(handleMessage); + return () => { + chrome.runtime.onMessage.removeListener(handleMessage); + }; + }, []); + useEffect(() => { const init = async () => { - await Promise.all([ - loadProxyStatus(), - loadCustomProxies() - ]); + // 修改初始化逻辑,避免并行请求 + await loadProxyStatus(); + await loadCustomProxies(); setInitialized(true); }; init(); @@ -93,39 +106,6 @@ export const ProxySwitch: React.FC = () => { const loadCustomProxies = async () => { try { - // 首先检查当前是否在 options 页面的上下文中 - const currentUrl = window.location.href; - const isInOptionsContext = currentUrl.includes('chrome-extension://') && currentUrl.includes('options.html'); - - if (isInOptionsContext) { - // 如果在 options 页面上下文中,直接使用消息通信 - const response = await chrome.runtime.sendMessage({ - action: ProxyActionType.GET_PROXY_CONFIGS - }); - - if (response?.success && response.data) { - const proxies = response.data - .filter((proxy: ProxyConfig) => !FIXED_MODES.some(mode => mode.key === proxy.id)) - .map((proxy: ProxyConfig): CustomProxy => ({ - key: proxy.id, - name: proxy.name, - color: '#1890ff', - config: proxy, - enabled: proxy.enabled - })); - setCustomProxies(proxies); - - const enabledProxy = response.data.find((proxy: ProxyConfig) => proxy.enabled); - if (enabledProxy) { - setCurrentMode(enabledProxy.id); - } else { - setCurrentMode('direct'); - } - return; - } - } - - // 如果不在 options 页面上下文中,使用 IndexedDB const DB_NAME = 'yaklang_extension'; const STORE_NAME = 'proxy_configs'; @@ -165,53 +145,49 @@ export const ProxySwitch: React.FC = () => { const enabledProxy = configs.find((proxy: ProxyConfig) => proxy.enabled); if (enabledProxy) { setCurrentMode(enabledProxy.id); - } else { - setCurrentMode('direct'); } - } catch (error) { console.error('Error loading custom proxies:', error); setCustomProxies([]); - setCurrentMode('direct'); } }; const handleModeChange = async (mode: string) => { - if (mode === 'setting') { - await chrome.runtime.openOptionsPage?.(); - return; - } - - if (mode === 'add') { - try { - const [activeTab] = await chrome.tabs.query({ - active: true, - currentWindow: true - }); - const optionsUrl = chrome.runtime.getURL('/proxy/options.html'); - - if (activeTab?.url === optionsUrl) { - chrome.tabs.sendMessage(activeTab.id!, { - action: 'TRIGGER_ADD_PROXY' + if (mode === 'setting' || mode === 'add') { + if (mode === 'setting') { + await chrome.runtime.openOptionsPage?.(); + } + if (mode === 'add') { + try { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true }); - } else { - const tab = await chrome.tabs.create({ - url: optionsUrl - }); - - const listener = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { - if (tabId === tab.id && changeInfo.status === 'complete') { - chrome.tabs.onUpdated.removeListener(listener); - chrome.tabs.sendMessage(tab.id!, { - action: 'TRIGGER_ADD_PROXY' - }); - } - }; + const optionsUrl = chrome.runtime.getURL('/proxy/options.html'); - chrome.tabs.onUpdated.addListener(listener); + if (activeTab?.url === optionsUrl) { + chrome.tabs.sendMessage(activeTab.id!, { + action: 'TRIGGER_ADD_PROXY' + }); + } else { + const tab = await chrome.tabs.create({ + url: optionsUrl + }); + + const listener = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + if (tabId === tab.id && changeInfo.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + chrome.tabs.sendMessage(tab.id!, { + action: 'TRIGGER_ADD_PROXY' + }); + } + }; + + chrome.tabs.onUpdated.addListener(listener); + } + } catch (error) { + console.error('Failed to get current tab:', error); } - } catch (error) { - console.error('Failed to get current tab:', error); } return; } @@ -227,8 +203,8 @@ export const ProxySwitch: React.FC = () => { return; } + // 立即更新UI状态 setCurrentMode(mode); - if (customProxy) { setCustomProxies(prev => prev.map(p => ({ ...p, @@ -238,16 +214,20 @@ export const ProxySwitch: React.FC = () => { const response = await chrome.runtime.sendMessage({ action: ProxyActionType.SET_PROXY_CONFIG, - config + config, + // 添加一个标志,表示这是从 ProxySwitch 发起的更改 + source: 'proxy_switch' }); if (response?.success === false) { throw new Error(response.error || '设置代理失败'); } - await loadCustomProxies(); + // 不需要重新加载,因为我们已经更新了本地状态 } catch (error) { console.error('Error applying proxy config:', error); + // 发生错误时才重新加载以确保状态正确 + await loadCustomProxies(); throw error; } finally { setIsLoading(false); @@ -265,13 +245,17 @@ export const ProxySwitch: React.FC = () => { { type: 'divider' }, ...customProxies.map(proxy => ({ key: proxy.key, - icon: , + icon: + {proxy.config.proxyType === 'pac_script' ? '📜' : } + , label: {proxy.name}, className: `${currentMode === proxy.key ? 'menu-item-selected' : ''} ${isLoading ? 'menu-item-loading' : ''}`, - title: `${proxy.config.scheme.toUpperCase()} ${proxy.config.host}:${proxy.config.port}` + title: proxy.config.scheme + ? `${proxy.config.scheme.toUpperCase()} ${proxy.config.host}:${proxy.config.port}` + : `${proxy.config.host}:${proxy.config.port}` })), { key: 'add', diff --git a/src/pages/OptionsPage/components/ProxySettings/index.tsx b/src/pages/OptionsPage/components/ProxySettings/index.tsx index 5c9f75c..f1aa480 100644 --- a/src/pages/OptionsPage/components/ProxySettings/index.tsx +++ b/src/pages/OptionsPage/components/ProxySettings/index.tsx @@ -4,6 +4,7 @@ import type { ColumnsType } from 'antd/es/table'; import { DeleteOutlined, PlusOutlined, EditOutlined, CheckOutlined } from '@ant-design/icons'; import { ProxyConfig } from '@/types/proxy'; import './index.css'; +import punycode from 'punycode'; interface ProxySettingsProps { proxyConfigs: ProxyConfig[]; @@ -14,7 +15,7 @@ interface ProxySettingsProps { onClear: (configId: string) => Promise; } -// 添加编辑表单的接口 +// 修改 EditFormData 接口 interface EditFormData { name: string; proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect"; @@ -22,6 +23,9 @@ interface EditFormData { host?: string; port?: number; pacScript?: string; + bypassList?: string; + matchList?: string; // 仅用于 UI 编辑 + proxyServer?: string; // 添加 proxyServer 字段,用于 PAC 脚本模式选择代理服务器 } // 或者更好的方式是创建一个专门的类型 @@ -48,7 +52,11 @@ export const ProxySettings: React.FC = ({ scheme: editingConfig.scheme, host: editingConfig.host, port: editingConfig.port, - pacScript: editingConfig.pacScript + bypassList: editingConfig.bypassList?.join('\n') || '', + matchList: editingConfig.matchList?.join('\n') || '', + proxyServer: editingConfig.host && editingConfig.port + ? `${editingConfig.host}:${editingConfig.port}` + : undefined }); } }, [editModalVisible, editingConfig, form]); @@ -73,6 +81,17 @@ export const ProxySettings: React.FC = ({ setEditModalVisible(true); }; + // 添加一个函数来获取可用的代理服务器列表 + const getAvailableProxies = (configs: ProxyConfig[]) => { + return configs + .filter(config => config.proxyType === 'fixed_servers') + .map(config => ({ + label: `${config.name} (${config.scheme}://${config.host}:${config.port})`, + value: `${config.host}:${config.port}`, + config + })); + }; + // 处理编辑保存 const handleEditSave = async () => { try { @@ -81,13 +100,108 @@ export const ProxySettings: React.FC = ({ if (editingConfig.enabled) { await onClear(editingConfig.id); } - - const updatedConfig: ProxyConfig = { - ...editingConfig, - ...values, - proxyType: values.proxyType as "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect", - scheme: values.scheme as "http" | "https" | "socks4" | "socks5" - }; + + let updatedConfig: ProxyConfig; + + if (values.proxyType === 'fixed_servers') { + // 处理固定代理服务器模式 + const bypassList = values.bypassList + ? values.bypassList.split('\n').map(line => line.trim()).filter(line => line.length > 0) + : ["localhost", "127.0.0.1"]; + + updatedConfig = { + id: editingConfig.id, + name: values.name, + enabled: editingConfig.enabled, + proxyType: 'fixed_servers', + scheme: values.scheme, + host: values.host, + port: values.port, + bypassList, + }; + } else if (values.proxyType === 'pac_script') { + const domains = values.matchList + ? values.matchList.split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0) + .map(domain => { + try { + // 如果域名包含非 ASCII 字符,转换为 Punycode + if (/[^\x00-\x7F]/.test(domain)) { + if (domain.startsWith('*.')) { + const suffix = domain.substring(2); + return '*.' + suffix.split('.').map(part => { + return /[^\x00-\x7F]/.test(part) ? 'xn--' + punycode.encode(part) : part; + }).join('.'); + } else { + return domain.split('.').map(part => { + return /[^\x00-\x7F]/.test(part) ? 'xn--' + punycode.encode(part) : part; + }).join('.'); + } + } + return domain; + } catch (error) { + console.error('Error encoding domain:', domain, error); + return domain; + } + }) + : []; + + // 从选择的代理服务器中获取配置 + const [host, port] = values.proxyServer.split(':'); + + // 生成 PAC 脚本 + const pacScriptContent = ` +function FindProxyForURL(url, host) { + // Convert host to lowercase for case-insensitive matching + host = host.toLowerCase(); + + // Define domain patterns + var domains = ${JSON.stringify(domains)}; + + // Check each domain pattern + for (var i = 0; i < domains.length; i++) { + var pattern = domains[i].toLowerCase(); + + if (pattern.startsWith('*.')) { + var suffix = pattern.substring(2); + if (host === suffix || host.endsWith('.' + suffix)) { + return 'PROXY ${host}:${port}'; + } + } else if (host === pattern) { + return 'PROXY ${host}:${port}'; + } + } + + return 'DIRECT'; +}`; + + updatedConfig = { + id: editingConfig.id, + name: values.name, + enabled: editingConfig.enabled, + proxyType: 'pac_script', + mode: 'pac_script', + // 保存代理服务器信息 + host, + port: parseInt(port), + // 保存匹配域名列表 + matchList: domains, + pacScript: { + data: pacScriptContent, + mandatory: true + } + }; + } else { + // 处理其他模式 + updatedConfig = { + id: editingConfig.id, + name: values.name, + enabled: editingConfig.enabled, + proxyType: values.proxyType, + bypassList: [], // 其他模式下设置为空数组 + }; + } if (!proxyConfigs.find(config => config.id === editingConfig.id)) { await onAdd(updatedConfig); @@ -232,7 +346,7 @@ export const ProxySettings: React.FC = ({ > - - - - - - - - - ) : proxyType === 'pac_script' ? ( - - - - ) : null; + if (proxyType === 'fixed_servers') { + return ( + <> + + + + + + + + + + + ); + } else if (proxyType === 'pac_script') { + const availableProxies = getAvailableProxies(proxyConfigs); + + return ( + <> + + + + +