From 6f82842c92ef5e81ec9319221749a422440a64d9 Mon Sep 17 00:00:00 2001 From: go0p Date: Mon, 10 Feb 2025 15:04:14 +0800 Subject: [PATCH] use indexedDB manager proxySwitch --- public/background.js | 12 + public/db/db.js | 116 +++++ public/db/proxy-store.js | 137 ++++++ public/manifest.json | 5 +- public/proxy.js | 270 ++++++----- public/proxy/options.js | 1 - public/proxy/proxy-auth.js | 124 ++--- public/proxy/proxy-logs.js | 226 ++++++--- public/proxy/proxy-manager.js | 87 ---- public/proxy/proxy-settings.js | 40 +- public/types/action.js | 8 +- src/components/ProxySwitch/index.tsx | 80 +++- .../components/ProxyLogs/LogDetail.tsx | 166 +++++++ .../components/ProxyLogs/index.tsx | 131 ++++++ .../components/ProxySettings/index.tsx | 165 +++++++ .../OptionsPage/hooks/useProxyConfigs.ts | 177 ++++++++ src/pages/OptionsPage/hooks/useProxyLogs.ts | 59 +++ src/pages/OptionsPage/index.css | 13 +- src/pages/OptionsPage/index.tsx | 429 ++---------------- src/types/action.ts | 6 +- src/types/chrome.d.ts | 10 + src/types/proxy.ts | 27 +- tsconfig.json | 6 +- 23 files changed, 1542 insertions(+), 753 deletions(-) create mode 100644 public/db/db.js create mode 100644 public/db/proxy-store.js delete mode 100644 public/proxy/proxy-manager.js create mode 100644 src/pages/OptionsPage/components/ProxyLogs/LogDetail.tsx create mode 100644 src/pages/OptionsPage/components/ProxyLogs/index.tsx create mode 100644 src/pages/OptionsPage/components/ProxySettings/index.tsx create mode 100644 src/pages/OptionsPage/hooks/useProxyConfigs.ts create mode 100644 src/pages/OptionsPage/hooks/useProxyLogs.ts create mode 100644 src/types/chrome.d.ts diff --git a/public/background.js b/public/background.js index 1ed3c9a..2d1d708 100644 --- a/public/background.js +++ b/public/background.js @@ -9,6 +9,18 @@ const websocketManager = new WebSocketManager(); // 设置代理处理器 setupProxyHandlers(); +// 添加点击事件处理 +chrome.action.onClicked.addListener((tab) => { + // 打开侧边栏 + chrome.sidePanel.open({ windowId: tab.windowId }); +}); + +// 可选:设置默认打开状态 +chrome.sidePanel.setOptions({ + enabled: true, + path: 'index.html' +}); + chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) { console.log("msg", msg) switch (msg.action) { diff --git a/public/db/db.js b/public/db/db.js new file mode 100644 index 0000000..98d1088 --- /dev/null +++ b/public/db/db.js @@ -0,0 +1,116 @@ +class Database { + constructor() { + this.DB_NAME = 'yaklang_extension'; + this.DB_VERSION = 1; + this.stores = { + PROXY_LOGS: 'proxy_logs', + PROXY_CONFIGS: 'proxy_configs', + CURRENT_PROXY: 'current_proxy', + PROXY_AUTH: 'proxy_auth' + }; + } + + async initDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.DB_NAME, this.DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + // 代理日志存储 + if (!db.objectStoreNames.contains(this.stores.PROXY_LOGS)) { + const logsStore = db.createObjectStore(this.stores.PROXY_LOGS, { keyPath: 'id' }); + logsStore.createIndex('timestamp', 'timestamp'); + logsStore.createIndex('resourceType', 'resourceType'); + logsStore.createIndex('status', 'status'); + } + + // 代理配置列表存储 + if (!db.objectStoreNames.contains(this.stores.PROXY_CONFIGS)) { + const configsStore = db.createObjectStore(this.stores.PROXY_CONFIGS, { keyPath: 'id' }); + configsStore.createIndex('name', 'name'); + configsStore.createIndex('enabled', 'enabled'); + } + + // 当前代理配置存储 + if (!db.objectStoreNames.contains(this.stores.CURRENT_PROXY)) { + db.createObjectStore(this.stores.CURRENT_PROXY); + } + + // 代理认证信息存储 + if (!db.objectStoreNames.contains(this.stores.PROXY_AUTH)) { + const authStore = db.createObjectStore(this.stores.PROXY_AUTH, { keyPath: 'id' }); + authStore.createIndex('host', 'host'); + } + }; + }); + } + + async getStore(storeName, mode = 'readonly') { + const db = await this.initDB(); + const tx = db.transaction(storeName, mode); + return tx.objectStore(storeName); + } + + // 通用的 CRUD 操作 + async get(storeName, key) { + const store = await this.getStore(storeName); + return new Promise((resolve, reject) => { + const request = store.get(key); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + async getAll(storeName) { + const store = await this.getStore(storeName); + return new Promise((resolve, reject) => { + const request = store.getAll(); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + } + + async put(storeName, value, key = undefined) { + try { + const store = await this.getStore(storeName, 'readwrite'); + return new Promise((resolve, reject) => { + const request = key ? store.put(value, key) : store.put(value); + request.onsuccess = () => { + console.log(`Successfully put data in ${storeName}:`, value); + resolve(request.result); + }; + request.onerror = () => { + console.error(`Error putting data in ${storeName}:`, request.error); + reject(request.error); + }; + }); + } catch (error) { + console.error(`Error in put operation for ${storeName}:`, error); + throw error; + } + } + + async delete(storeName, key) { + const store = await this.getStore(storeName, 'readwrite'); + return new Promise((resolve, reject) => { + const request = store.delete(key); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + async clear(storeName) { + const store = await this.getStore(storeName, 'readwrite'); + return new Promise((resolve, reject) => { + const request = store.clear(); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } +} + +export const db = new Database(); \ No newline at end of file diff --git a/public/db/proxy-store.js b/public/db/proxy-store.js new file mode 100644 index 0000000..0cf28f0 --- /dev/null +++ b/public/db/proxy-store.js @@ -0,0 +1,137 @@ +import { db } from './db.js'; + +class ProxyStore { + constructor() { + this.MAX_LOGS = 1000; + } + + // 代理配置相关操作 + async getProxyConfigs() { + return await db.getAll(db.stores.PROXY_CONFIGS); + } + + async saveProxyConfigs(configs) { + try { + console.log('Saving proxy configs:', configs); + + // 确保配置数组有效 + if (!Array.isArray(configs)) { + throw new Error('配置必须是数组'); + } + + // 开始事务 + const store = await db.getStore(db.stores.PROXY_CONFIGS, 'readwrite'); + + // 清除现有配置 + await store.clear(); + + // 保存新配置 + for (const config of configs) { + await store.put(config); + } + + console.log('Proxy configs saved successfully'); + this.notifyConfigUpdate(); + return true; + } catch (error) { + console.error('Error saving proxy configs:', error); + throw error; + } + } + + async getCurrentProxy() { + return await db.get(db.stores.CURRENT_PROXY, 'current'); + } + + async setCurrentProxy(proxy) { + await db.put(db.stores.CURRENT_PROXY, proxy, 'current'); + } + + async clearCurrentProxy() { + await db.delete(db.stores.CURRENT_PROXY, 'current'); + } + + // 代理日志相关操作 + async getLogs() { + const logs = await db.getAll(db.stores.PROXY_LOGS); + return logs.sort((a, b) => b.timestamp - a.timestamp); + } + + async addLog(log) { + await db.put(db.stores.PROXY_LOGS, log); + await this.cleanOldLogs(); + } + + async clearLogs() { + await db.clear(db.stores.PROXY_LOGS); + } + + async cleanOldLogs() { + const store = await db.getStore(db.stores.PROXY_LOGS, 'readwrite'); + const countRequest = store.count(); + + countRequest.onsuccess = () => { + if (countRequest.result > this.MAX_LOGS) { + const excess = countRequest.result - this.MAX_LOGS; + const cursorRequest = store.index('timestamp').openCursor(); + let deleted = 0; + + cursorRequest.onsuccess = (event) => { + const cursor = event.target.result; + if (cursor && deleted < excess) { + cursor.delete(); + deleted++; + cursor.continue(); + } + }; + } + }; + } + + // 代理认证相关操作 + async getAuthHandlers() { + return await db.getAll(db.stores.PROXY_AUTH); + } + + async saveAuthHandler(handler) { + await db.put(db.stores.PROXY_AUTH, handler); + } + + async deleteAuthHandler(id) { + await db.delete(db.stores.PROXY_AUTH, id); + } + + async clearAuthHandlers() { + await db.clear(db.stores.PROXY_AUTH); + } + + async getErrors() { + return await db.get(db.stores.PROXY_AUTH, 'errors') || []; + } + + async saveErrors(errors) { + await db.put(db.stores.PROXY_AUTH, errors, 'errors'); + } + + async getAuth() { + return await db.get(db.stores.PROXY_AUTH, 'auth'); + } + + async saveAuth(auth) { + await db.put(db.stores.PROXY_AUTH, auth, 'auth'); + } + + async clearAuth() { + await db.delete(db.stores.PROXY_AUTH, 'auth'); + } + + notifyConfigUpdate() { + chrome.runtime.sendMessage({ + action: 'PROXY_CONFIGS_UPDATED' + }).catch(() => { + // 忽略接收者不存在的错误 + }); + } +} + +export const proxyStore = new ProxyStore(); \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index 0bb866c..b8fc865 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -16,7 +16,7 @@ } }, "side_panel": { - "default_path" : "index.html" + "default_path": "index.html" }, "background": { "service_worker": "background.js", @@ -26,7 +26,8 @@ "proxy", "storage", "sidePanel", - "webRequest" + "webRequest", + "declarativeNetRequest" ], "host_permissions": [ "" diff --git a/public/proxy.js b/public/proxy.js index 7bf7699..e052de0 100644 --- a/public/proxy.js +++ b/public/proxy.js @@ -1,69 +1,44 @@ -import { ProxyManager } from './proxy/proxy-manager.js'; import { ProxySettings } from './proxy/proxy-settings.js'; import { ProxyAuth } from './proxy/proxy-auth.js'; import { ProxyActionType } from './types/action.js'; -import { ProxyLogs } from './proxy/proxy-logs.js'; +import { proxyLogs } from './proxy/proxy-logs.js'; +import { proxyStore } from './db/proxy-store.js'; -// 记录代理日志 -async function logProxyRequest(details, proxyConfig, error = null) { - try { - // 只记录主文档和 XHR 请求 - if (!['main_frame', 'xmlhttprequest'].includes(details.type)) { - return; - } - - const log = { - id: Date.now().toString(), - timestamp: Date.now(), - url: details.url, - proxyId: proxyConfig.id, - proxyName: proxyConfig.name, - type: details.type, - status: error ? 'error' : 'success', - errorMessage: error?.message - }; - - // 使用 storage 存储日志 - const result = await chrome.storage.local.get('proxyLogs'); - const logs = result.proxyLogs || []; - const updatedLogs = [log, ...logs].slice(0, 100); // 只保留最新的 100 条 - await chrome.storage.local.set({ proxyLogs: updatedLogs }); - - } catch (error) { - console.error('Error logging proxy request:', error); - } +// 修改代理状态获取函数为 Promise 形式 +function getProxySettings() { + return new Promise((resolve) => { + chrome.proxy.settings.get({}, resolve); + }); } async function handleSetProxyConfig(config, sendResponse) { try { // 处理直接连接的情况 if (config.proxyType === 'direct') { - await chrome.proxy.settings.set({ - value: { mode: "direct" }, - scope: 'regular' + await new Promise((resolve) => { + chrome.proxy.settings.set({ + value: { mode: "direct" }, + scope: 'regular' + }, resolve); }); - const settings = await chrome.proxy.settings.get({}); + const settings = await getProxySettings(); const isSuccess = settings.value.mode === "direct"; if (isSuccess) { // 更新存储 - await chrome.storage.local.set({ - currentProxy: { - ...config, - timestamp: Date.now() - } + await proxyStore.setCurrentProxy({ + ...config, + timestamp: Date.now() }); // 更新代理列表状态 - const result = await chrome.storage.local.get('proxyConfigs'); - if (result.proxyConfigs) { - const updatedConfigs = result.proxyConfigs.map(c => ({ - ...c, - enabled: c.id === config.id - })); - await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - } + const configs = await proxyStore.getProxyConfigs(); + const updatedConfigs = configs.map(c => ({ + ...c, + enabled: c.id === config.id + })); + await proxyStore.saveProxyConfigs(updatedConfigs); console.log('Direct connection set successfully'); sendResponse({ success: true }); @@ -98,32 +73,30 @@ async function handleSetProxyConfig(config, sendResponse) { } }; - await chrome.proxy.settings.set({ - value: proxyConfig, - scope: 'regular' + await new Promise((resolve) => { + chrome.proxy.settings.set({ + value: proxyConfig, + scope: 'regular' + }, resolve); }); - const settings = await chrome.proxy.settings.get({}); + 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 chrome.storage.local.set({ - currentProxy: { - ...config, - timestamp: Date.now() - } + await proxyStore.setCurrentProxy({ + ...config, + timestamp: Date.now() }); - const result = await chrome.storage.local.get('proxyConfigs'); - if (result.proxyConfigs) { - const updatedConfigs = result.proxyConfigs.map(c => ({ - ...c, - enabled: c.id === config.id - })); - await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - } + 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 }); @@ -145,32 +118,31 @@ async function handleSetProxyConfig(config, sendResponse) { async function handleClearProxyConfig(sendResponse) { try { - await chrome.proxy.settings.clear({ - scope: 'regular' + await new Promise((resolve) => { + chrome.proxy.settings.clear({ + scope: 'regular' + }, resolve); }); - await chrome.proxy.settings.set({ - value: { mode: "system" }, - scope: 'regular' + await new Promise((resolve) => { + chrome.proxy.settings.set({ + value: { mode: "system" }, + scope: 'regular' + }, resolve); }); // 只移除当前代理配置,保留代理列表 - await chrome.storage.local.remove([ - 'currentProxy', - 'proxyAuthHandlers' - ]); + await proxyStore.clearCurrentProxy(); // 更新所有代理的启用状态 - const result = await chrome.storage.local.get('proxyConfigs'); - if (result.proxyConfigs) { - const updatedConfigs = result.proxyConfigs.map(config => ({ - ...config, - enabled: false - })); - await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - } + const configs = await proxyStore.getProxyConfigs(); + const updatedConfigs = configs.map(config => ({ + ...config, + enabled: false + })); + await proxyStore.saveProxyConfigs(updatedConfigs); - const settings = await chrome.proxy.settings.get({}); + const settings = await getProxySettings(); const isSuccess = settings.value.mode === "system"; if (isSuccess) { @@ -194,12 +166,12 @@ async function handleClearProxyConfig(sendResponse) { async function handleGetProxyStatus(sendResponse) { try { - const settings = await chrome.proxy.settings.get({}); - const currentProxy = await chrome.storage.local.get('currentProxy'); + const settings = await getProxySettings(); + const currentProxy = await proxyStore.getCurrentProxy(); const status = { enabled: settings.value.mode === "fixed_servers", - config: currentProxy.currentProxy || null, + config: currentProxy || null, mode: settings.value.mode }; @@ -245,25 +217,24 @@ function setupProxyRequestListener() { // 使用队列处理日志 async function queueProxyLog(details, error = null) { - // 检查是否是扩展自身的请求 - if (details.url.startsWith('chrome-extension://')) { - return; - } + try { + // 检查代理状态 + const settings = await getProxySettings(); + if (settings.value.mode !== "fixed_servers") { + return; + } - // 检查代理状态 - const settings = await chrome.proxy.settings.get({}); - if (settings.value.mode !== "fixed_servers") { - return; - } + // 获取当前代理配置 + const currentProxy = await proxyStore.getCurrentProxy(); + if (!currentProxy) { + return; + } - // 获取当前代理配置 - const { currentProxy } = await chrome.storage.local.get('currentProxy'); - if (!currentProxy) { - return; + // 记录日志 + await proxyLogs.logRequest(details, currentProxy, error); + } catch (error) { + console.error('Error in queueProxyLog:', error); } - - // 记录日志 - await logProxyRequest(details, currentProxy, error); } // 导出代理处理器设置函数 @@ -292,18 +263,86 @@ export function setupProxyHandlers() { handleGetProxyStatus(sendResponse); return true; - case ProxyActionType.CLEAR_PROXY_LOGS: - chrome.storage.local.set({ proxyLogs: [] }, () => { - if (chrome.runtime.lastError) { - sendResponse({ - success: false, - error: chrome.runtime.lastError.message - }); - } else { - sendResponse({ success: true }); - } + case ProxyActionType.GET_PROXY_LOGS: + proxyLogs.getLogs().then(logs => { + sendResponse({ + success: true, + data: logs + }); + }).catch(error => { + sendResponse({ + success: false, + error: error.message + }); }); return true; + + case ProxyActionType.CLEAR_PROXY_LOGS: + proxyLogs.clearLogs().then(() => { + sendResponse({ success: true }); + }).catch(error => { + sendResponse({ + success: false, + error: error.message + }); + }); + return true; + + case ProxyActionType.GET_PROXY_CONFIGS: + proxyStore.getProxyConfigs().then(configs => { + sendResponse({ + success: true, + data: configs + }); + }).catch(error => { + sendResponse({ + success: false, + error: error.message + }); + }); + return true; + + case ProxyActionType.ADD_PROXY_CONFIG: + proxyStore.getProxyConfigs().then(async configs => { + const newConfigs = [...configs, msg.config]; + await proxyStore.saveProxyConfigs(newConfigs); + sendResponse({ success: true }); + }).catch(error => { + sendResponse({ + success: false, + error: error.message + }); + }); + return true; + + case ProxyActionType.UPDATE_PROXY_CONFIG: + (async () => { // 使用立即执行的异步函数 + try { + if (!msg.configs || !Array.isArray(msg.configs)) { + throw new Error('无效的配置数据'); + } + + console.log('Updating proxy configs:', msg.configs); + await proxyStore.saveProxyConfigs(msg.configs); + + // 获取最新的配置 + const updatedConfigs = await proxyStore.getProxyConfigs(); + console.log('Configs updated successfully:', updatedConfigs); + + // 发送响应 + sendResponse({ + success: true, + data: updatedConfigs + }); + } catch (error) { + console.error('Error updating proxy configs:', error); + sendResponse({ + success: false, + error: error.message || '更新代理配置失败' + }); + } + })(); + return true; // 保持消息端口打开 } }); @@ -318,15 +357,6 @@ export function setupProxyHandlers() { // 设置认证监听 await ProxyAuth.setupAuthListener(); - - // 初始化存储 - const storage = await chrome.storage.local.get(['proxyConfigs', 'proxyLogs']); - if (!storage.proxyConfigs) { - await chrome.storage.local.set({ proxyConfigs: [] }); - } - if (!storage.proxyLogs) { - await chrome.storage.local.set({ proxyLogs: [] }); - } } catch (error) { console.error('Error during installation:', error); } diff --git a/public/proxy/options.js b/public/proxy/options.js index d9100c9..5c9fc3a 100644 --- a/public/proxy/options.js +++ b/public/proxy/options.js @@ -1,7 +1,6 @@ console.log('Options page script loaded'); import { ProxySettings } from './proxy-settings.js'; -import { ProxyManager } from './proxy-manager.js'; let currentConfigs = []; diff --git a/public/proxy/proxy-auth.js b/public/proxy/proxy-auth.js index a91d7fd..bc3229c 100644 --- a/public/proxy/proxy-auth.js +++ b/public/proxy/proxy-auth.js @@ -1,72 +1,91 @@ // 代理认证管理 +import { proxyStore } from '../db/proxy-store.js'; + export class ProxyAuth { static async setupAuthListener() { - try { - // 保存认证信息到 storage - const saveAuth = async (config) => { - await chrome.storage.local.set({ - proxyAuth: { - username: config.username, - password: config.password, - timestamp: Date.now() + // 使用 chrome.webRequest.onAuthRequired 的非阻塞版本 + chrome.webRequest.onAuthRequired.addListener( + async (details) => { + try { + // 获取认证处理器 + const handlers = await proxyStore.getAuthHandlers(); + const handler = handlers.find(h => + details.challenger?.host === h.host + ); + + if (handler) { + // 使用 declarativeNetRequest 规则来处理认证 + await chrome.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: [handler.id], + addRules: [{ + id: parseInt(handler.id), + priority: 1, + action: { + type: 'modifyHeaders', + requestHeaders: [ + { + header: 'Proxy-Authorization', + operation: 'set', + value: 'Basic ' + btoa(`${handler.username}:${handler.password}`) + } + ] + }, + condition: { + domains: [handler.host], + resourceTypes: ['main_frame', 'sub_frame', 'stylesheet', 'script', 'image', 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', 'media', 'websocket', 'other'] + } + }] + }); } - }); - }; + } catch (error) { + console.error('Auth error:', error); + } + }, + { urls: [""] } + ); + } - // 获取认证信息 - const getAuth = async () => { - const result = await chrome.storage.local.get('proxyAuth'); - return result.proxyAuth; - }; + static async saveAuthHandler(host, username, password) { + const handler = { + id: Date.now().toString(), + host, + username, + password + }; + await proxyStore.saveAuthHandler(handler); + await this.setupAuthListener(); // 重新设置认证规则 + } - // 清除认证信息 - const clearAuth = async () => { - await chrome.storage.local.remove('proxyAuth'); - }; - - return { - saveAuth, - getAuth, - clearAuth - }; - } catch (error) { - console.error('Error setting up auth listener:', error); - return null; + static async removeAuthHandler(host) { + const handlers = await proxyStore.getAuthHandlers(); + const handler = handlers.find(h => h.host === host); + if (handler) { + await proxyStore.deleteAuthHandler(handler.id); + // 移除对应的认证规则 + await chrome.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: [parseInt(handler.id)] + }); } } static setupErrorHandler() { - // 在 Manifest V3 中,我们不能使用 chrome.proxy.onProxyError - // 所以我们只记录错误到 storage - try { - const logError = async (error) => { - const errors = await chrome.storage.local.get('proxyErrors') || []; + // 使用 storage 记录错误 + return { + logError: async (error) => { + const errors = await proxyStore.getErrors() || []; errors.push({ timestamp: Date.now(), error: error.message || error }); - await chrome.storage.local.set({ - proxyErrors: errors.slice(-100) // 只保留最近100条错误记录 - }); - }; - - return { logError }; - } catch (error) { - console.error('Error setting up error handler:', error); - return null; - } + await proxyStore.saveErrors(errors.slice(-100)); // 只保留最近100条错误记录 + } + }; } // 设置代理认证信息 static async setProxyAuth(username, password) { try { - await chrome.storage.local.set({ - proxyAuth: { - username, - password, - timestamp: Date.now() - } - }); + await proxyStore.saveAuth({ username, password, timestamp: Date.now() }); return true; } catch (error) { console.error('Error setting proxy auth:', error); @@ -77,8 +96,7 @@ export class ProxyAuth { // 获取代理认证信息 static async getProxyAuth() { try { - const result = await chrome.storage.local.get('proxyAuth'); - return result.proxyAuth || null; + return await proxyStore.getAuth(); } catch (error) { console.error('Error getting proxy auth:', error); return null; @@ -88,7 +106,7 @@ export class ProxyAuth { // 清除代理认证信息 static async clearProxyAuth() { try { - await chrome.storage.local.remove('proxyAuth'); + await proxyStore.clearAuth(); return true; } catch (error) { console.error('Error clearing proxy auth:', error); diff --git a/public/proxy/proxy-logs.js b/public/proxy/proxy-logs.js index 5bda8b6..b187dff 100644 --- a/public/proxy/proxy-logs.js +++ b/public/proxy/proxy-logs.js @@ -1,12 +1,17 @@ -// 日志数据库管理 -export class ProxyLogs { - static DB_NAME = 'yakit_proxy_logs'; - static STORE_NAME = 'logs'; - static VERSION = 1; +import { proxyStore } from '../db/proxy-store.js'; - static async openDB() { +// 日志数据库管理 +class ProxyLogs { + constructor() { + this.DB_NAME = 'proxy_extension'; + this.STORE_NAME = 'proxy_logs'; + this.DB_VERSION = 1; + this.MAX_LOGS = 1000; // 最多保存1000条日志 + } + + async initDB() { return new Promise((resolve, reject) => { - const request = indexedDB.open(this.DB_NAME, this.VERSION); + const request = indexedDB.open(this.DB_NAME, this.DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); @@ -17,82 +22,161 @@ export class ProxyLogs { const store = db.createObjectStore(this.STORE_NAME, { keyPath: 'id' }); // 创建索引 store.createIndex('timestamp', 'timestamp'); - store.createIndex('url', 'url'); - store.createIndex('proxyId', 'proxyId'); + store.createIndex('resourceType', 'resourceType'); store.createIndex('status', 'status'); } }; }); } - static async addLog(log) { - const db = await this.openDB(); - return new Promise((resolve, reject) => { - const transaction = db.transaction([this.STORE_NAME], 'readwrite'); - const store = transaction.objectStore(this.STORE_NAME); - - // 添加新日志 - const request = store.add(log); - - request.onsuccess = () => { - // 删除旧日志,只保留最新的 100 条 - const countRequest = store.count(); - countRequest.onsuccess = () => { - if (countRequest.result > 100) { - const index = store.index('timestamp'); - const cursorRequest = index.openCursor(); - let deleteCount = countRequest.result - 100; - - cursorRequest.onsuccess = (event) => { - const cursor = event.target.result; - if (cursor && deleteCount > 0) { - store.delete(cursor.primaryKey); - deleteCount--; - cursor.continue(); - } - }; + async getResourceType(details) { + try { + // 首先检查请求类型 + if (details.type) { + // 直接使用 Chrome 提供的类型 + switch (details.type) { + case 'main_frame': return 'page'; + case 'xmlhttprequest': { + // 检查请求头来区分 XHR 和 Fetch + const isFetch = details.requestHeaders?.some( + header => header.name.toLowerCase() === 'sec-fetch-mode' && + header.value === 'cors' + ); + return isFetch ? 'fetch' : 'xhr'; } - }; - resolve(); - }; - - request.onerror = () => reject(request.error); - }); - } - - static async getLogs(limit = 100) { - const db = await this.openDB(); - return new Promise((resolve, reject) => { - const transaction = db.transaction([this.STORE_NAME], 'readonly'); - const store = transaction.objectStore(this.STORE_NAME); - const index = store.index('timestamp'); - - const request = index.openCursor(null, 'prev'); - const logs = []; - - request.onsuccess = (event) => { - const cursor = event.target.result; - if (cursor && logs.length < limit) { - logs.push(cursor.value); - cursor.continue(); - } else { - resolve(logs); + case 'script': return 'script'; + case 'stylesheet': return 'stylesheet'; + case 'image': return 'image'; + case 'media': return 'media'; + case 'font': return 'font'; + case 'websocket': return 'websocket'; } + } + + // 根据文件扩展名和内容类型判断 + const contentType = details.requestHeaders?.find( + header => header.name.toLowerCase() === 'content-type' + )?.value || ''; + + const url = new URL(details.url); + const pathname = url.pathname.toLowerCase(); + + // 检查文件扩展名 + if (pathname.endsWith('.js')) return 'script'; + if (pathname.endsWith('.css')) return 'stylesheet'; + if (/\.(png|jpg|jpeg|gif|webp|svg|ico)$/.test(pathname)) return 'image'; + if (/\.(mp3|mp4|wav|ogg|webm)$/.test(pathname)) return 'media'; + if (/\.(woff|woff2|ttf|eot|otf)$/.test(pathname)) return 'font'; + + // 根据内容类型判断 + if (contentType) { + if (contentType.includes('javascript')) return 'script'; + if (contentType.includes('css')) return 'stylesheet'; + if (contentType.includes('image/')) return 'image'; + if (contentType.includes('audio/') || contentType.includes('video/')) return 'media'; + if (contentType.includes('font/') || contentType.includes('application/font')) return 'font'; + if (contentType.includes('application/json')) return 'xhr'; + if (contentType.includes('application/x-www-form-urlencoded')) return 'xhr'; + } + + // 检查 Accept 头 + const acceptHeader = details.requestHeaders?.find( + header => header.name.toLowerCase() === 'accept' + )?.value || ''; + + if (acceptHeader) { + if (acceptHeader.includes('application/json')) return 'xhr'; + if (acceptHeader.includes('text/javascript')) return 'script'; + if (acceptHeader.includes('text/css')) return 'stylesheet'; + if (acceptHeader.includes('image/')) return 'image'; + } + + console.log('Resource type detection:', { + url: details.url, + type: details.type, + contentType, + acceptHeader, + headers: details.requestHeaders + }); + + return 'other'; + } catch (error) { + console.error('Error determining resource type:', error); + return 'other'; + } + } + + async logRequest(details, proxyConfig, error = null) { + try { + // 检查是否是扩展自身的请求 + if (details.url.startsWith('chrome-extension://')) { + return; + } + + // 获取资源类型 + const resourceType = await this.getResourceType(details); + + const log = { + id: Date.now().toString(), + timestamp: Date.now(), + url: details.url, + proxyId: proxyConfig.id, + proxyName: proxyConfig.name, + status: error ? 'error' : 'success', + errorMessage: error?.message, + method: details.method, + requestHeaders: details.requestHeaders?.reduce((acc, header) => { + acc[header.name] = header.value; + return acc; + }, {}), + requestBody: details.requestBody?.raw?.[0]?.bytes + ? decodeURIComponent(String.fromCharCode.apply(null, new Uint8Array(details.requestBody.raw[0].bytes))) + : null, + responseHeaders: details.responseHeaders?.reduce((acc, header) => { + acc[header.name] = header.value; + return acc; + }, {}), + timing: { + startTime: details.timeStamp, + endTime: Date.now(), + duration: Date.now() - details.timeStamp + }, + protocol: details.protocol || details.type, + ip: details.ip, + fromCache: details.fromCache, + resourceType }; - request.onerror = () => reject(request.error); - }); + // 使用 proxyStore 存储日志 + await proxyStore.addLog(log); + this.notifyLogUpdate(); + } catch (error) { + console.error('Error logging proxy request:', error); + } } - static async clearLogs() { - const db = await this.openDB(); - return new Promise((resolve, reject) => { - const transaction = db.transaction([this.STORE_NAME], 'readwrite'); - const store = transaction.objectStore(this.STORE_NAME); - const request = store.clear(); + async getLogs() { + return await proxyStore.getLogs(); + } - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); + async clearLogs() { + await proxyStore.clearLogs(); + this.notifyLogUpdate(); + } + + notifyLogUpdate() { + // 通知前端日志已更新 + chrome.runtime.sendMessage({ + action: 'PROXY_LOGS_UPDATED' + }).catch(() => { + // 忽略接收者不存在的错误 }); } -} \ No newline at end of file +} + +// 导出实例而不是直接使用顶层 await +export const proxyLogs = new ProxyLogs(); + +// 移除这些顶层 await 语句 +// await proxyStore.addLog({...}); +// const logs = await proxyStore.getLogs(); \ No newline at end of file diff --git a/public/proxy/proxy-manager.js b/public/proxy/proxy-manager.js deleted file mode 100644 index f35cf24..0000000 --- a/public/proxy/proxy-manager.js +++ /dev/null @@ -1,87 +0,0 @@ -// 代理配置管理 -export class ProxyManager { - static async setProxy(config) { - try { - const proxyConfig = { - mode: "fixed_servers", - rules: { - singleProxy: { - scheme: config.scheme, - host: config.host, - port: config.port - }, - bypassList: ["localhost", "127.0.0.1"] - } - }; - - await chrome.proxy.settings.set({ - value: proxyConfig, - scope: 'regular' - }); - - // 保存当前配置 - await chrome.storage.local.set({ - currentProxy: { - ...config, - timestamp: Date.now() - } - }); - - return true; - } catch (error) { - console.error('Error setting proxy:', error); - return false; - } - } - - static async clearProxy() { - try { - await chrome.proxy.settings.clear({scope: 'regular'}); - await chrome.storage.local.remove('currentProxy'); - return true; - } catch (error) { - console.error('Error clearing proxy:', error); - return false; - } - } - - static async getProxyStatus() { - try { - const settings = await chrome.proxy.settings.get({}); - const currentProxy = await chrome.storage.local.get('currentProxy'); - return { - enabled: settings.value.mode === "fixed_servers", - config: currentProxy.currentProxy || null - }; - } catch (error) { - console.error('Error getting proxy status:', error); - return { - enabled: false, - config: null - }; - } - } - - static async deleteProxy(proxyId) { - try { - // 获取当前代理配置 - const result = await chrome.storage.local.get(['proxyConfigs', 'currentProxy']); - const configs = result.proxyConfigs || []; - const currentProxy = result.currentProxy; - - // 如果要删除的代理正在使用中,先清除代理设置 - if (currentProxy && currentProxy.id === proxyId) { - await clearProxyConfig(); - } - - // 从配置列表中删除代理 - const updatedConfigs = configs.filter(config => config.id !== proxyId); - await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - - return true; - } catch (error) { - console.error('Error deleting proxy:', error); - return false; - } - } -} \ No newline at end of file diff --git a/public/proxy/proxy-settings.js b/public/proxy/proxy-settings.js index 6c07b94..4261b05 100644 --- a/public/proxy/proxy-settings.js +++ b/public/proxy/proxy-settings.js @@ -1,41 +1,43 @@ +import { proxyStore } from '../db/proxy-store.js'; + // 代理配置存储和管理 -export const ProxySettings = { - async importSettings(settings) { +export class ProxySettings { + static async importSettings(settings) { try { if (Array.isArray(settings) && settings.every(s => s.proxyType)) { - await chrome.storage.local.set({proxyConfigs: settings}); + await proxyStore.saveProxyConfigs(settings); return {success: true}; } return {success: false, error: "Invalid settings format"}; } catch (error) { return {success: false, error: error.message}; } - }, + } - async exportSettings() { + static async exportSettings() { try { - const {proxyConfigs} = await chrome.storage.local.get('proxyConfigs'); - return {success: true, settings: proxyConfigs || []}; + const configs = await proxyStore.getProxyConfigs(); + return {success: true, settings: configs || []}; } catch (error) { return {success: false, error: error.message}; } - }, + } - async setDefaultConfigs() { - const result = await chrome.storage.local.get('proxyConfigs'); - if (!result.proxyConfigs) { - const defaultConfigs = [{ + static async setDefaultConfigs() { + const configs = await proxyStore.getProxyConfigs(); + if (!configs || configs.length === 0) { + // 设置默认的直接连接配置 + await proxyStore.saveProxyConfigs([{ id: 'direct', name: '直接连接', proxyType: 'direct', enabled: false - }]; - await chrome.storage.local.set({ proxyConfigs: defaultConfigs }); + }]); } - // 确保 proxyLogs 存在 - const logsResult = await chrome.storage.local.get('proxyLogs'); - if (!logsResult.proxyLogs) { - await chrome.storage.local.set({ proxyLogs: [] }); + // 确保日志存储已初始化 + const logs = await proxyStore.getLogs(); // 使用 getLogs 而不是 getProxyLogs + if (!logs || logs.length === 0) { + await proxyStore.clearLogs(); // 初始化日志存储 } } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/public/types/action.js b/public/types/action.js index b8b4850..b9adfa0 100644 --- a/public/types/action.js +++ b/public/types/action.js @@ -1,6 +1,12 @@ +// 这个是插件 background 中使用的 action 类型 +// 和前端的 action 要保持一致 export const ProxyActionType = { SET_PROXY_CONFIG: "SET_PROXY_CONFIG", CLEAR_PROXY_CONFIG: "CLEAR_PROXY_CONFIG", GET_PROXY_STATUS: "GET_PROXY_STATUS", - CLEAR_PROXY_LOGS: "CLEAR_PROXY_LOGS" + GET_PROXY_LOGS: "GET_PROXY_LOGS", + CLEAR_PROXY_LOGS: "CLEAR_PROXY_LOGS", + GET_PROXY_CONFIGS: "GET_PROXY_CONFIGS", + ADD_PROXY_CONFIG: "ADD_PROXY_CONFIG", + UPDATE_PROXY_CONFIG: "UPDATE_PROXY_CONFIG" }; \ No newline at end of file diff --git a/src/components/ProxySwitch/index.tsx b/src/components/ProxySwitch/index.tsx index 608df67..41d119a 100644 --- a/src/components/ProxySwitch/index.tsx +++ b/src/components/ProxySwitch/index.tsx @@ -1,17 +1,24 @@ import React, {useEffect, useState} from "react"; -import {Select, Button, Tooltip, Modal, Form, Input, Radio, Space, message} from "antd"; +import {Select, Button, Tooltip, Modal, Form, Input, Radio, Space, message, Switch} from "antd"; import {PlusOutlined, SettingOutlined} from "@ant-design/icons"; import {ProxyConfig} from "@/types/proxy"; import {StorageChanges} from "@/types/chrome"; +import {ProxyActionType} from '@/types/action'; import "./index.css"; -export const ProxySwitch: React.FC = () => { +interface ProxySwitchProps { + onChange?: (checked: boolean) => void; +} + +export const ProxySwitch: React.FC = ({ onChange }) => { const [currentMode, setCurrentMode] = useState("direct"); const [proxyConfigs, setProxyConfigs] = useState([]); const [isModalVisible, setIsModalVisible] = useState(false); const [form] = Form.useForm(); const [proxyHost, setProxyHost] = useState(''); const [proxyPort, setProxyPort] = useState(''); + const [enabled, setEnabled] = useState(false); + const [loading, setLoading] = useState(true); useEffect(() => { loadConfigs().catch(error => { @@ -44,6 +51,10 @@ export const ProxySwitch: React.FC = () => { return () => chrome.storage.onChanged.removeListener(handleStorageChange); }, [currentMode]); + useEffect(() => { + loadProxyStatus(); + }, []); + const loadConfigs = async () => { try { const result = await chrome.storage.local.get('proxyConfigs'); @@ -188,6 +199,65 @@ export const ProxySwitch: React.FC = () => { } }; + const proxyType: ProxyConfig['proxyType'] = proxyConfigs.find((c: ProxyConfig) => c.id === currentMode)?.proxyType || 'direct'; + + const loadProxyStatus = async () => { + try { + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.GET_PROXY_STATUS + }); + + if (response.success) { + setEnabled(response.data.enabled); + } + setLoading(false); + } catch (error) { + console.error('Error loading proxy status:', error); + setLoading(false); + } + }; + + const handleChange = async (checked: boolean) => { + try { + if (checked) { + // 获取配置列表 + const configResponse = await chrome.runtime.sendMessage({ + action: ProxyActionType.GET_PROXY_CONFIGS + }); + + if (!configResponse.success) { + throw new Error(configResponse.error || '获取代理配置失败'); + } + + const configs = configResponse.data || []; + const defaultConfig = configs.find((c: ProxyConfig) => c.id === 'direct'); + + if (defaultConfig) { + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.SET_PROXY_CONFIG, + config: defaultConfig + }); + + if (response.success) { + setEnabled(true); + onChange?.(true); + } + } + } else { + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.CLEAR_PROXY_CONFIG + }); + + if (response.success) { + setEnabled(false); + onChange?.(false); + } + } + } catch (error) { + console.error('Error toggling proxy:', error); + } + }; + return (
@@ -283,6 +353,12 @@ export const ProxySwitch: React.FC = () => { + +
); }; \ No newline at end of file diff --git a/src/pages/OptionsPage/components/ProxyLogs/LogDetail.tsx b/src/pages/OptionsPage/components/ProxyLogs/LogDetail.tsx new file mode 100644 index 0000000..547f466 --- /dev/null +++ b/src/pages/OptionsPage/components/ProxyLogs/LogDetail.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { Modal, Button, Space, Descriptions, Tabs, Card, Typography, message } from 'antd'; +import { ProxyLog } from '@/types/proxy'; + +const { Text, Paragraph } = Typography; + +interface LogDetailProps { + log: ProxyLog | null; + onClose: () => void; +} + +export const LogDetail: React.FC = ({ log, onClose }) => { + const renderHttpRequest = (log: ProxyLog) => { + if (!log) return ''; + + // 构建请求头 + const headers = Object.entries(log.requestHeaders || {}) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + + // 构建完整的 HTTP 请求 + return `${log.method || 'GET'} ${log.url} ${log.protocol || 'HTTP/1.1'} +${headers} + +${log.requestBody || ''}`; + }; + + const renderHttpResponse = (log: ProxyLog) => { + if (!log || !log.responseHeaders) return ''; + + // 构建响应头 + const headers = Object.entries(log.responseHeaders) + .map(([key, value]) => `${key}: ${value}`) + .join('\n'); + + // 构建完整的 HTTP 响应 + return `HTTP/1.1 ${log.status === 'success' ? '200 OK' : '500 Error'} +${headers} + +${log.responseBody || ''}`; + }; + + const handleCopyRaw = () => { + if (!log) return; + navigator.clipboard.writeText(renderHttpRequest(log)) + .then(() => message.success('已复制到剪贴板')) + .catch(() => message.error('复制失败')); + }; + + const formatProxyInfo = (log: ProxyLog) => { + if (!log) return ''; + return `${log.proxyName}${log.host ? ` - ${log.host}:${log.port}` : ''}`; + }; + + return ( + + 复制原始数据 + , + + ]} + > + {log && ( + + + + {new Date(log.timestamp).toLocaleString()} + + + {formatProxyInfo(log)} + + + + {log.status === 'success' ? '成功' : '失败'} + + + + {log.timing?.duration}ms + + {log.errorMessage && ( + + {log.errorMessage} + + )} + + + +
+                                            {renderHttpRequest(log)}
+                                        
+ + ) + }, + { + key: 'response', + label: '响应数据', + children: ( + +
+                                            {renderHttpResponse(log)}
+                                        
+
+ ) + }, + { + key: 'timing', + label: '性能数据', + children: ( + + + + {new Date(log.timing?.startTime || 0).toLocaleString()} + + + {new Date(log.timing?.endTime || 0).toLocaleString()} + + + {log.timing?.duration}ms + + + {log.ip || '-'} + + + {log.protocol || '-'} + + + {log.fromCache ? '是' : '否'} + + + + ) + } + ]} + /> +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/pages/OptionsPage/components/ProxyLogs/index.tsx b/src/pages/OptionsPage/components/ProxyLogs/index.tsx new file mode 100644 index 0000000..f6b4e32 --- /dev/null +++ b/src/pages/OptionsPage/components/ProxyLogs/index.tsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import { Table, Button, Space } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { DeleteOutlined, FilterFilled } from '@ant-design/icons'; +import { ProxyLog } from '@/types/proxy'; +import { LogDetail } from './LogDetail'; + +interface ProxyLogsProps { + logs: ProxyLog[]; + onClearLogs: () => void; +} + +export const ProxyLogs: React.FC = ({ + logs, + onClearLogs +}) => { + const [selectedLog, setSelectedLog] = useState(null); + const [resourceFilter, setResourceFilter] = useState([]); + + const columns: ColumnsType = [ + { + title: '时间', + dataIndex: 'timestamp', + key: 'timestamp', + render: (timestamp: number) => new Date(timestamp).toLocaleString() + }, + { + title: 'URL', + dataIndex: 'url', + key: 'url', + ellipsis: true + }, + { + title: ( + + 类型 + {resourceFilter.length > 0 && } + + ), + dataIndex: 'resourceType', + key: 'resourceType', + render: (type: string) => { + const typeMap: Record = { + xhr: 'XHR', + fetch: 'Fetch', + script: 'JS', + stylesheet: 'CSS', + image: 'Image', + other: 'Other' + }; + return typeMap[type] || 'Other'; + }, + filters: [ + { text: 'XHR', value: 'xhr' }, + { text: 'Fetch', value: 'fetch' }, + { text: 'JS', value: 'script' }, + { text: 'CSS', value: 'stylesheet' }, + { text: 'Image', value: 'image' }, + { text: 'Other', value: 'other' } + ], + filterMode: 'menu' as const, + filtered: resourceFilter.length > 0, + onFilter: (value: string, record: ProxyLog) => record.resourceType === value + }, + { + title: '使用代理', + dataIndex: 'proxyName', + key: 'proxyName' + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + + {status === 'success' ? '成功' : '失败'} + + ) + }, + { + title: '错误信息', + dataIndex: 'errorMessage', + key: 'errorMessage', + ellipsis: true + } + ]; + + const filteredLogs = logs.filter(log => { + if (resourceFilter.length === 0) return true; + return resourceFilter.includes(log.resourceType || 'other'); + }); + + return ( + +
+ +
+ + ({ + onClick: () => setSelectedLog(record), + style: { cursor: 'pointer' } + })} + pagination={{ + pageSize: 10, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total) => `共 ${total} 条`, + pageSizeOptions: ['10', '20', '50', '100'] + }} + rowKey="id" + /> + setSelectedLog(null)} + /> + + ); +}; \ No newline at end of file diff --git a/src/pages/OptionsPage/components/ProxySettings/index.tsx b/src/pages/OptionsPage/components/ProxySettings/index.tsx new file mode 100644 index 0000000..fdbc5a3 --- /dev/null +++ b/src/pages/OptionsPage/components/ProxySettings/index.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import { Card, Input, Space, Button, Select, InputNumber, Form, Table } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import { ProxyConfig } from '@/types/proxy'; + +interface ProxySettingsProps { + proxyConfigs: ProxyConfig[]; + onAdd: () => void; + onChange: (configId: string, field: keyof ProxyConfig, value: any) => void; + onDelete: (configId: string) => void; + onApply: (configId: string) => Promise; + onClear: (configId: string) => Promise; +} + +export const ProxySettings: React.FC = ({ + proxyConfigs, + onAdd, + onChange, + onDelete, + onApply, + onClear +}) => { + const columns: ColumnsType = [ + { + title: '名称', + dataIndex: 'name', + key: 'name', + render: (text: string, record: ProxyConfig) => ( + onChange(record.id, 'name', e.target.value)} + disabled={record.id === 'direct'} + /> + ) + }, + { + title: '类型', + dataIndex: 'proxyType', + key: 'proxyType', + render: (text: string, record: ProxyConfig) => ( + onChange(record.id, 'scheme', value)} + style={{ width: 100 }} + options={[ + { label: 'HTTP', value: 'http' }, + { label: 'HTTPS', value: 'https' }, + { label: 'SOCKS4', value: 'socks4' }, + { label: 'SOCKS5', value: 'socks5' } + ]} + /> + ) + ) + }, + { + title: '主机', + dataIndex: 'host', + key: 'host', + render: (text: string, record: ProxyConfig) => ( + record.proxyType === 'fixed_server' && ( + onChange(record.id, 'host', e.target.value)} + placeholder="127.0.0.1" + /> + ) + ) + }, + { + title: '端口', + dataIndex: 'port', + key: 'port', + render: (text: number, record: ProxyConfig) => ( + record.proxyType === 'fixed_server' && ( + onChange(record.id, 'port', value)} + min={1} + max={65535} + /> + ) + ) + }, + { + title: 'PAC 脚本', + dataIndex: 'pacScript', + key: 'pacScript', + render: (text: string, record: ProxyConfig) => ( + record.proxyType === 'pac_script' && ( + onChange(record.id, 'pacScript', e.target.value)} + rows={4} + placeholder="输入 PAC 脚本" + /> + ) + ) + }, + { + title: '操作', + key: 'action', + render: (_, record: ProxyConfig) => ( + + + {record.id !== 'direct' && ( + + +
+ + ); +}; \ No newline at end of file diff --git a/src/pages/OptionsPage/hooks/useProxyConfigs.ts b/src/pages/OptionsPage/hooks/useProxyConfigs.ts new file mode 100644 index 0000000..cfc46c7 --- /dev/null +++ b/src/pages/OptionsPage/hooks/useProxyConfigs.ts @@ -0,0 +1,177 @@ +import { useState, useEffect } from 'react'; +import { ProxyConfig } from '@/types/proxy'; +import { ProxyActionType } from '@/types/action'; +import { message } from 'antd'; + +export const useProxyConfigs = () => { + const [proxyConfigs, setProxyConfigs] = useState([]); + + useEffect(() => { + loadConfigs(); + + // 监听配置更新 + const handleConfigUpdate = () => { + loadConfigs(); + }; + + chrome.runtime.onMessage.addListener((message) => { + if (message.action === 'PROXY_CONFIGS_UPDATED') { + handleConfigUpdate(); + } + }); + + return () => { + chrome.runtime.onMessage.removeListener(handleConfigUpdate); + }; + }, []); + + const loadConfigs = async () => { + try { + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.GET_PROXY_CONFIGS + }); + if (response.success) { + setProxyConfigs(response.data || []); + } + } catch (error) { + console.error('Error loading configs:', error); + message.error('加载配置时发生错误'); + } + }; + + const handleAddProxy = async (config: ProxyConfig) => { + try { + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.ADD_PROXY_CONFIG, + config: config + }); + if (response.success) { + message.success('添加代理成功'); + loadConfigs(); + } else { + message.error(response.error || '添加代理失败'); + } + } catch (error) { + console.error('Error adding proxy:', error); + message.error('添加代理时发生错误'); + } + }; + + const handleConfigChange = async (configId: string, field: keyof ProxyConfig, value: any) => { + try { + const updatedConfigs = proxyConfigs.map(config => + config.id === configId ? { ...config, [field]: value } : config + ); + + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.UPDATE_PROXY_CONFIG, + configs: updatedConfigs + }); + + if (response?.success) { + setProxyConfigs(response.data || updatedConfigs); + message.success('更新配置成功'); + } else { + message.error(response?.error || '更新配置失败'); + await loadConfigs(); + } + } catch (error) { + console.error('Error updating config:', error); + message.error('更新配置时发生错误'); + await loadConfigs(); + } + }; + + const handleDeleteProxy = async (configId: string) => { + try { + console.log('Deleting proxy:', configId); + const updatedConfigs = proxyConfigs.filter(config => config.id !== configId); + console.log('Updated configs after delete:', updatedConfigs); + + // 使用 Promise 包装消息发送 + const response = await new Promise((resolve) => { + chrome.runtime.sendMessage({ + action: ProxyActionType.UPDATE_PROXY_CONFIG, + configs: updatedConfigs + }, (result) => { + console.log('Delete response received:', result); + resolve(result); + }); + }); + + console.log('Delete response:', response); + + if (response?.success) { + setProxyConfigs(response.data || updatedConfigs); + message.success('删除代理成功'); + } else { + console.error('Delete failed:', response?.error); + message.error(response?.error || '删除代理失败'); + await loadConfigs(); + } + } catch (error) { + console.error('Error deleting proxy:', error); + message.error('删除代理时发生错误'); + await loadConfigs(); + } + }; + + const handleApplyConfig = async (configId: string) => { + try { + const config = proxyConfigs.find(c => c.id === configId); + if (!config) return; + + // 先取消当前启用的代理 + const currentEnabled = proxyConfigs.find(c => c.enabled); + if (currentEnabled && currentEnabled.id !== configId) { + // 如果当前启用的不是直接连接,需要先清除代理设置 + if (currentEnabled.id !== 'direct') { + await chrome.runtime.sendMessage({ + action: ProxyActionType.CLEAR_PROXY_CONFIG + }); + } + } + + // 应用新的代理设置 + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.SET_PROXY_CONFIG, + config: config + }); + + if (response.success) { + message.success('代理设置已应用'); + } else { + message.error(response.error || '应用代理设置失败'); + } + } catch (error) { + console.error('Error applying proxy:', error); + message.error('应用代理设置时发生错误'); + } + }; + + const handleClearProxy = async () => { + try { + const response = await chrome.runtime.sendMessage({ + action: ProxyActionType.CLEAR_PROXY_CONFIG + }); + + if (response.success) { + message.success('已切换至直接连接'); + } else { + message.error(response.error || '清除代理设置失败'); + } + } catch (error) { + console.error('Error clearing proxy:', error); + message.error('清除代理设置时发生错误'); + } + }; + + return { + proxyConfigs, + handleAddProxy, + handleConfigChange, + handleDeleteProxy, + handleApplyConfig, + handleClearProxy + }; +}; \ No newline at end of file diff --git a/src/pages/OptionsPage/hooks/useProxyLogs.ts b/src/pages/OptionsPage/hooks/useProxyLogs.ts new file mode 100644 index 0000000..c3f9a1c --- /dev/null +++ b/src/pages/OptionsPage/hooks/useProxyLogs.ts @@ -0,0 +1,59 @@ +import { useState, useEffect } from 'react'; +import { ProxyLog } from '@/types/proxy'; +import { ProxyActionType } from '@/types/action'; + +export const useProxyLogs = () => { + const [proxyLogs, setProxyLogs] = useState([]); + + useEffect(() => { + loadLogs(); + + // 监听日志更新 + const handleLogsUpdate = () => { + loadLogs(); + }; + + chrome.runtime.onMessage.addListener((message) => { + if (message.action === 'PROXY_LOGS_UPDATED') { + handleLogsUpdate(); + } + }); + + return () => { + chrome.runtime.onMessage.removeListener(handleLogsUpdate); + }; + }, []); + + const loadLogs = async () => { + try { + console.log('Fetching proxy logs...'); + const response = await chrome.runtime.sendMessage({ + action: 'GET_PROXY_LOGS' + }); + console.log('Received response:', response); + if (response.success) { + setProxyLogs(response.data || []); + } else { + console.error('Failed to load logs:', response.error); + } + } catch (error) { + console.error('Error loading logs:', error); + } + }; + + const handleClearLogs = async () => { + try { + await chrome.runtime.sendMessage({ + action: ProxyActionType.CLEAR_PROXY_LOGS + }); + setProxyLogs([]); + } catch (error) { + console.error('Error clearing logs:', error); + } + }; + + return { + proxyLogs, + handleClearLogs + }; +}; \ No newline at end of file diff --git a/src/pages/OptionsPage/index.css b/src/pages/OptionsPage/index.css index ca5208c..97978c3 100644 --- a/src/pages/OptionsPage/index.css +++ b/src/pages/OptionsPage/index.css @@ -1,5 +1,6 @@ .options-page { - min-height: 100vh; + height: 100vh; + overflow: auto; background: #f0f2f5; } @@ -97,4 +98,14 @@ .options-page .ant-select-item-option-selected:not(.ant-select-item-option-disabled) { background-color: var(--yakit-primary-5); +} + +/* 防止 Modal 出现时页面跳动 */ +.ant-modal-wrap { + overflow: hidden; +} + +.ant-modal-content { + max-height: 90vh; + overflow: auto; } \ No newline at end of file diff --git a/src/pages/OptionsPage/index.tsx b/src/pages/OptionsPage/index.tsx index 4e92b80..4eae451 100644 --- a/src/pages/OptionsPage/index.tsx +++ b/src/pages/OptionsPage/index.tsx @@ -1,59 +1,25 @@ -import React, { useEffect, useState } from "react"; -import { Layout, Button, Card, Input, Select, InputNumber, Space, Typography, Modal, Switch, Tabs, Table, Form, App } from "antd"; -import { PlusOutlined, DeleteOutlined, ImportOutlined, ExportOutlined } from "@ant-design/icons"; -import { ProxyConfig } from "@/types/proxy"; -import { StorageChanges } from "@/types/chrome"; -import './index.css'; -import { ProxyActionType } from '@/types/action'; +import React from 'react'; +import { Layout, Tabs } from 'antd'; +import { ProxySettings } from './components/ProxySettings'; +import { ProxyLogs } from './components/ProxyLogs'; +import { useProxyConfigs } from './hooks/useProxyConfigs'; +import { useProxyLogs } from './hooks/useProxyLogs'; +import { ProxyConfig } from '@/types/proxy'; -const { Header, Content } = Layout; -const { Title } = Typography; -const { TextArea } = Input; - -const headerStyle = { - background: '#fff', - padding: '0 24px', - borderBottom: '1px solid #f0f0f0' -}; - -const contentStyle = { - padding: '24px', - background: '#f0f2f5', - minHeight: '100vh' -}; - -const titleStyle = { - margin: '16px 0', - color: '#31343F' -}; - -interface ProxyLog { - id: string; - timestamp: number; - url: string; - proxyId: string; - proxyName: string; - status: 'success' | 'error'; - errorMessage?: string; -} +const { Content } = Layout; export const OptionsPage: React.FC = () => { - const { message } = App.useApp(); - const [proxyConfigs, setProxyConfigs] = useState([]); - const [proxyLogs, setProxyLogs] = useState([]); - const [activeTab, setActiveTab] = useState('settings'); - const [currentConfigId, setCurrentConfigId] = useState(''); + const { + proxyConfigs, + handleAddProxy, + handleConfigChange, + handleDeleteProxy, + handleApplyConfig, + handleClearProxy + } = useProxyConfigs(); + const { proxyLogs, handleClearLogs } = useProxyLogs(); - useEffect(() => { - loadConfigs(); - }, []); - - const loadConfigs = async () => { - const result = await chrome.storage.local.get('proxyConfigs'); - setProxyConfigs(result.proxyConfigs || []); - }; - - const handleAddProxy = () => { + const handleAdd = () => { const newConfig: ProxyConfig = { id: Date.now().toString(), name: '新建代理', @@ -63,360 +29,37 @@ export const OptionsPage: React.FC = () => { port: 8080, enabled: false }; - const updatedConfigs = [...proxyConfigs, newConfig]; - chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - setProxyConfigs(updatedConfigs); - }; - - const handleConfigChange = (configId: string, field: keyof ProxyConfig, value: any) => { - const updatedConfigs = proxyConfigs.map(config => { - if (config.id === configId) { - return { ...config, [field]: value }; - } - return config; - }); - chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - setProxyConfigs(updatedConfigs); - }; - - const handleDeleteProxy = (configId: string) => { - Modal.confirm({ - title: '确认删除', - content: '确定要删除这个代理配置吗?', - onOk: () => { - const updatedConfigs = proxyConfigs.filter(config => config.id !== configId); - chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - setProxyConfigs(updatedConfigs); - } - }); - }; - - useEffect(() => { - const handleStorageChange = (changes: StorageChanges) => { - if (changes.proxyConfigs) { - setProxyConfigs(changes.proxyConfigs.newValue || []); - } - }; - - chrome.storage.onChanged.addListener(handleStorageChange); - return () => chrome.storage.onChanged.removeListener(handleStorageChange); - }, []); - - useEffect(() => { - // 加载日志 - const loadLogs = async () => { - const result = await chrome.storage.local.get('proxyLogs'); - setProxyLogs(result.proxyLogs || []); - }; - - loadLogs(); - - // 监听存储变化 - const handleStorageChange = (changes: StorageChanges) => { - if (changes.proxyLogs) { - setProxyLogs(changes.proxyLogs.newValue || []); - } - }; - - chrome.storage.onChanged.addListener(handleStorageChange); - return () => chrome.storage.onChanged.removeListener(handleStorageChange); - }, []); - - const columns = [ - { - title: '时间', - dataIndex: 'timestamp', - key: 'timestamp', - render: (timestamp: number) => new Date(timestamp).toLocaleString() - }, - { - title: 'URL', - dataIndex: 'url', - key: 'url', - ellipsis: true, - render: (url: string) => ( - { - e.preventDefault(); - chrome.tabs.create({ url }); - }} - > - {url} - - ) - }, - { - title: '使用代理', - dataIndex: 'proxyName', - key: 'proxyName', - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - render: (status: string) => ( - - {status === 'success' ? '成功' : '失败'} - - ) - }, - { - title: '错误信息', - dataIndex: 'errorMessage', - key: 'errorMessage', - ellipsis: true, - } - ]; - - const handleApplyConfig = async (configId: string) => { - const config = proxyConfigs.find(c => c.id === configId); - if (config) { - try { - const response = await new Promise((resolve) => { - chrome.runtime.sendMessage({ - action: ProxyActionType.SET_PROXY_CONFIG, - config: { - ...config, - scheme: config.scheme || 'http', - host: config.host || '127.0.0.1', - port: Number(config.port) || 8080, - } - }, resolve); - }); - - if (response && response.success) { - const updatedConfigs = proxyConfigs.map(c => ({ - ...c, - enabled: c.id === configId - })); - await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - setProxyConfigs(updatedConfigs); - message.success('代理设置已应用'); - } else { - message.error((response && response.error) || '代理设置失败'); - } - } catch (error) { - console.error('Failed to apply proxy config:', error); - message.error('操作失败'); - } - } - }; - - const handleClearProxy = async (configId: string) => { - try { - const response = await new Promise((resolve) => { - chrome.runtime.sendMessage({ - action: ProxyActionType.CLEAR_PROXY_CONFIG - }, resolve); - }); - - if (response && response.success) { - const updatedConfigs = proxyConfigs.map(c => ({ - ...c, - enabled: false - })); - await chrome.storage.local.set({ proxyConfigs: updatedConfigs }); - setProxyConfigs(updatedConfigs); - message.success('代理已取消'); - } else { - message.error((response && response.error) || '取消代理失败'); - } - } catch (error) { - console.error('Error clearing proxy:', error); - message.error('操作失败'); - } - }; - - const handleClearLogs = () => { - chrome.runtime.sendMessage({ - action: ProxyActionType.CLEAR_PROXY_LOGS - }, (response) => { - if (chrome.runtime.lastError) { - message.error(chrome.runtime.lastError.message || '清除日志失败'); - return; - } - if (response?.success) { - message.success('日志已清除'); - } else { - message.error(response?.error || '清除日志失败'); - } - }); + handleAddProxy(newConfig); }; return ( - - + + - - - - - ) - }} + defaultActiveKey="1" items={[ { - key: 'settings', + key: '1', label: '代理设置', children: ( - - {proxyConfigs.map(config => ( - handleConfigChange(config.id, 'name', e.target.value)} - disabled={config.id === 'direct'} - variant="borderless" - style={{ fontSize: '16px', padding: 0 }} - /> - } - extra={ - - - {config.id !== 'direct' && ( -