mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +08:00
use indexedDB manager proxySwitch
This commit is contained in:
@@ -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) {
|
||||
|
||||
+116
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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": [
|
||||
"<all_urls>"
|
||||
|
||||
+150
-120
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
console.log('Options page script loaded');
|
||||
|
||||
import { ProxySettings } from './proxy-settings.js';
|
||||
import { ProxyManager } from './proxy-manager.js';
|
||||
|
||||
let currentConfigs = [];
|
||||
|
||||
|
||||
+71
-53
@@ -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: ["<all_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);
|
||||
|
||||
+155
-71
@@ -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(() => {
|
||||
// 忽略接收者不存在的错误
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出实例而不是直接使用顶层 await
|
||||
export const proxyLogs = new ProxyLogs();
|
||||
|
||||
// 移除这些顶层 await 语句
|
||||
// await proxyStore.addLog({...});
|
||||
// const logs = await proxyStore.getLogs();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(); // 初始化日志存储
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
@@ -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<ProxySwitchProps> = ({ onChange }) => {
|
||||
const [currentMode, setCurrentMode] = useState<string>("direct");
|
||||
const [proxyConfigs, setProxyConfigs] = useState<ProxyConfig[]>([]);
|
||||
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 (
|
||||
<div className="proxy-switch">
|
||||
<div className="proxy-switch-header">
|
||||
@@ -283,6 +353,12 @@ export const ProxySwitch: React.FC = () => {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={handleChange}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<LogDetailProps> = ({ 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 (
|
||||
<Modal
|
||||
title="请求详情"
|
||||
open={!!log}
|
||||
onCancel={onClose}
|
||||
width={800}
|
||||
footer={[
|
||||
<Button key="copy" onClick={handleCopyRaw}>
|
||||
复制原始数据
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{log && (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="请求时间">
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="代理服务器">
|
||||
{formatProxyInfo(log)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Text type={log.status === 'success' ? 'success' : 'danger'}>
|
||||
{log.status === 'success' ? '成功' : '失败'}
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="响应时间">
|
||||
{log.timing?.duration}ms
|
||||
</Descriptions.Item>
|
||||
{log.errorMessage && (
|
||||
<Descriptions.Item label="错误信息" span={2}>
|
||||
<Text type="danger">{log.errorMessage}</Text>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'request',
|
||||
label: '请求数据',
|
||||
children: (
|
||||
<Card size="small">
|
||||
<pre style={{
|
||||
background: '#f5f5f5',
|
||||
padding: 16,
|
||||
borderRadius: 4,
|
||||
maxHeight: 400,
|
||||
overflow: 'auto',
|
||||
margin: 0
|
||||
}}>
|
||||
{renderHttpRequest(log)}
|
||||
</pre>
|
||||
</Card>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'response',
|
||||
label: '响应数据',
|
||||
children: (
|
||||
<Card size="small">
|
||||
<pre style={{
|
||||
background: '#f5f5f5',
|
||||
padding: 16,
|
||||
borderRadius: 4,
|
||||
maxHeight: 400,
|
||||
overflow: 'auto',
|
||||
margin: 0
|
||||
}}>
|
||||
{renderHttpResponse(log)}
|
||||
</pre>
|
||||
</Card>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'timing',
|
||||
label: '性能数据',
|
||||
children: (
|
||||
<Card size="small">
|
||||
<Descriptions bordered>
|
||||
<Descriptions.Item label="开始时间">
|
||||
{new Date(log.timing?.startTime || 0).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结束时间">
|
||||
{new Date(log.timing?.endTime || 0).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="总耗时">
|
||||
{log.timing?.duration}ms
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="IP地址">
|
||||
{log.ip || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="协议">
|
||||
{log.protocol || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="缓存">
|
||||
{log.fromCache ? '是' : '否'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -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<ProxyLogsProps> = ({
|
||||
logs,
|
||||
onClearLogs
|
||||
}) => {
|
||||
const [selectedLog, setSelectedLog] = useState<ProxyLog | null>(null);
|
||||
const [resourceFilter, setResourceFilter] = useState<string[]>([]);
|
||||
|
||||
const columns: ColumnsType<ProxyLog> = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
render: (timestamp: number) => new Date(timestamp).toLocaleString()
|
||||
},
|
||||
{
|
||||
title: 'URL',
|
||||
dataIndex: 'url',
|
||||
key: 'url',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Space>
|
||||
类型
|
||||
{resourceFilter.length > 0 && <FilterFilled style={{ color: '#f50' }} />}
|
||||
</Space>
|
||||
),
|
||||
dataIndex: 'resourceType',
|
||||
key: 'resourceType',
|
||||
render: (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
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) => (
|
||||
<span style={{ color: status === 'success' ? '#52c41a' : '#ff4d4f' }}>
|
||||
{status === 'success' ? '成功' : '失败'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<div style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
}}>
|
||||
<Button
|
||||
danger
|
||||
onClick={onClearLogs}
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
清除日志
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
dataSource={filteredLogs}
|
||||
columns={columns}
|
||||
onRow={(record) => ({
|
||||
onClick: () => setSelectedLog(record),
|
||||
style: { cursor: 'pointer' }
|
||||
})}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
}}
|
||||
rowKey="id"
|
||||
/>
|
||||
<LogDetail
|
||||
log={selectedLog}
|
||||
onClose={() => setSelectedLog(null)}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
@@ -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<void>;
|
||||
onClear: (configId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ProxySettings: React.FC<ProxySettingsProps> = ({
|
||||
proxyConfigs,
|
||||
onAdd,
|
||||
onChange,
|
||||
onDelete,
|
||||
onApply,
|
||||
onClear
|
||||
}) => {
|
||||
const columns: ColumnsType<ProxyConfig> = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string, record: ProxyConfig) => (
|
||||
<Input
|
||||
value={text}
|
||||
onChange={e => onChange(record.id, 'name', e.target.value)}
|
||||
disabled={record.id === 'direct'}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'proxyType',
|
||||
key: 'proxyType',
|
||||
render: (text: string, record: ProxyConfig) => (
|
||||
<Select
|
||||
value={text}
|
||||
onChange={value => onChange(record.id, 'proxyType', value)}
|
||||
style={{ width: 120 }}
|
||||
disabled={record.id === 'direct'}
|
||||
options={[
|
||||
{ label: '直接连接', value: 'direct' },
|
||||
{ label: '代理服务器', value: 'fixed_server' },
|
||||
{ label: 'PAC 脚本', value: 'pac_script' }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '协议',
|
||||
dataIndex: 'scheme',
|
||||
key: 'scheme',
|
||||
render: (text: string, record: ProxyConfig) => (
|
||||
record.proxyType === 'fixed_server' && (
|
||||
<Select
|
||||
value={text}
|
||||
onChange={value => 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' && (
|
||||
<Input
|
||||
value={text}
|
||||
onChange={e => 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' && (
|
||||
<InputNumber
|
||||
value={text}
|
||||
onChange={value => onChange(record.id, 'port', value)}
|
||||
min={1}
|
||||
max={65535}
|
||||
/>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'PAC 脚本',
|
||||
dataIndex: 'pacScript',
|
||||
key: 'pacScript',
|
||||
render: (text: string, record: ProxyConfig) => (
|
||||
record.proxyType === 'pac_script' && (
|
||||
<Input.TextArea
|
||||
value={text}
|
||||
onChange={e => onChange(record.id, 'pacScript', e.target.value)}
|
||||
rows={4}
|
||||
placeholder="输入 PAC 脚本"
|
||||
/>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_, record: ProxyConfig) => (
|
||||
<Space>
|
||||
<Button
|
||||
type={record.enabled ? "primary" : "default"}
|
||||
danger={record.enabled}
|
||||
onClick={() => record.enabled ?
|
||||
onClear(record.id) :
|
||||
onApply(record.id)
|
||||
}
|
||||
>
|
||||
{record.enabled ? '取消应用' : '应用选项'}
|
||||
</Button>
|
||||
{record.id !== 'direct' && (
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => onDelete(record.id)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16, justifyContent: 'flex-end', width: '100%' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onAdd}
|
||||
>
|
||||
添加代理
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
dataSource={proxyConfigs}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<ProxyConfig[]>([]);
|
||||
|
||||
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<any>((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
|
||||
};
|
||||
};
|
||||
@@ -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<ProxyLog[]>([]);
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
+36
-393
@@ -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<ProxyConfig[]>([]);
|
||||
const [proxyLogs, setProxyLogs] = useState<ProxyLog[]>([]);
|
||||
const [activeTab, setActiveTab] = useState('settings');
|
||||
const [currentConfigId, setCurrentConfigId] = useState<string>('');
|
||||
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) => (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
color: '#1890ff',
|
||||
textDecoration: 'none',
|
||||
maxWidth: '400px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'block'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
chrome.tabs.create({ url });
|
||||
}}
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '使用代理',
|
||||
dataIndex: 'proxyName',
|
||||
key: 'proxyName',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<span style={{ color: status === 'success' ? '#52c41a' : '#ff4d4f' }}>
|
||||
{status === 'success' ? '成功' : '失败'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
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<any>((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<any>((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 (
|
||||
<Layout className="options-page">
|
||||
<Content style={contentStyle}>
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
<Content style={{ padding: '24px' }}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className="proxy-tabs"
|
||||
tabBarExtraContent={{
|
||||
right: (
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleAddProxy}
|
||||
>
|
||||
添加代理
|
||||
</Button>
|
||||
<Button icon={<ImportOutlined />}>导入</Button>
|
||||
<Button icon={<ExportOutlined />}>导出</Button>
|
||||
</Space>
|
||||
)
|
||||
}}
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: 'settings',
|
||||
key: '1',
|
||||
label: '代理设置',
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{proxyConfigs.map(config => (
|
||||
<Card
|
||||
key={config.id}
|
||||
size="small"
|
||||
title={
|
||||
<Input
|
||||
placeholder="代理名称"
|
||||
value={config.name}
|
||||
onChange={e => handleConfigChange(config.id, 'name', e.target.value)}
|
||||
disabled={config.id === 'direct'}
|
||||
variant="borderless"
|
||||
style={{ fontSize: '16px', padding: 0 }}
|
||||
/>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
className="proxy-action-btn"
|
||||
type={config.enabled ? "primary" : "default"}
|
||||
danger={config.enabled}
|
||||
onClick={() => config.enabled ?
|
||||
handleClearProxy(config.id) :
|
||||
handleApplyConfig(config.id)
|
||||
}
|
||||
>
|
||||
{config.enabled ? '取消应用' : '应用选项'}
|
||||
</Button>
|
||||
{config.id !== 'direct' && (
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDeleteProxy(config.id)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
style={{ borderRadius: '4px' }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={config.proxyType}
|
||||
onChange={value => handleConfigChange(config.id, 'proxyType', value)}
|
||||
disabled={config.id === 'direct'}
|
||||
>
|
||||
<Select.Option value="direct">直接连接</Select.Option>
|
||||
<Select.Option value="fixed_server">代理服务器</Select.Option>
|
||||
<Select.Option value="pac_script">PAC 脚本</Select.Option>
|
||||
<Select.Option value="bypass_list">代理规则列表</Select.Option>
|
||||
</Select>
|
||||
|
||||
{config.proxyType === 'bypass_list' && (
|
||||
<TextArea
|
||||
rows={4}
|
||||
value={config.bypassList?.join('\n')}
|
||||
onChange={e => handleConfigChange(config.id, 'bypassList', e.target.value.split('\n'))}
|
||||
placeholder="每行一个规则,例如:
|
||||
*.example.com
|
||||
[::1]
|
||||
127.0.0.1"
|
||||
/>
|
||||
)}
|
||||
|
||||
{config.proxyType === 'fixed_server' && (
|
||||
<Space style={{ width: '100%' }}>
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
value={config.scheme}
|
||||
onChange={value => handleConfigChange(config.id, 'scheme', value)}
|
||||
>
|
||||
<Select.Option value="http">HTTP</Select.Option>
|
||||
<Select.Option value="https">HTTPS</Select.Option>
|
||||
<Select.Option value="socks4">SOCKS4</Select.Option>
|
||||
<Select.Option value="socks5">SOCKS5</Select.Option>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="代理服务器"
|
||||
value={config.host}
|
||||
onChange={e => handleConfigChange(config.id, 'host', e.target.value)}
|
||||
/>
|
||||
<InputNumber
|
||||
placeholder="端口"
|
||||
value={config.port}
|
||||
onChange={value => handleConfigChange(config.id, 'port', value)}
|
||||
style={{ width: 100 }}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{config.proxyType === 'pac_script' && (
|
||||
<TextArea
|
||||
rows={4}
|
||||
value={config.pacScript}
|
||||
onChange={e => handleConfigChange(config.id, 'pacScript', e.target.value)}
|
||||
placeholder="输入 PAC 脚本"
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
<ProxySettings
|
||||
proxyConfigs={proxyConfigs}
|
||||
onAdd={handleAdd}
|
||||
onChange={handleConfigChange}
|
||||
onDelete={handleDeleteProxy}
|
||||
onApply={handleApplyConfig}
|
||||
onClear={handleClearProxy}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
key: '2',
|
||||
label: '代理日志',
|
||||
children: (
|
||||
<>
|
||||
<div style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
}}>
|
||||
<Button
|
||||
danger
|
||||
onClick={handleClearLogs}
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
清除日志
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
dataSource={proxyLogs}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
}}
|
||||
rowKey="id"
|
||||
/>
|
||||
</>
|
||||
<ProxyLogs
|
||||
logs={proxyLogs}
|
||||
onClearLogs={handleClearLogs}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
|
||||
+5
-1
@@ -14,7 +14,11 @@ 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"
|
||||
CLEAR_PROXY_LOGS: "CLEAR_PROXY_LOGS",
|
||||
GET_PROXY_LOGS: "GET_PROXY_LOGS",
|
||||
GET_PROXY_CONFIGS: "GET_PROXY_CONFIGS",
|
||||
ADD_PROXY_CONFIG: "ADD_PROXY_CONFIG",
|
||||
UPDATE_PROXY_CONFIG: "UPDATE_PROXY_CONFIG"
|
||||
} as const;
|
||||
|
||||
export type ProxyActionType = typeof ProxyActionType[keyof typeof ProxyActionType];
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
declare namespace chrome.storage {
|
||||
interface StorageChange {
|
||||
oldValue?: any;
|
||||
newValue?: any;
|
||||
}
|
||||
|
||||
type StorageChanges = {
|
||||
[key: string]: StorageChange;
|
||||
};
|
||||
}
|
||||
+26
-1
@@ -7,5 +7,30 @@ export interface ProxyConfig {
|
||||
port?: number;
|
||||
scheme?: "http" | "https" | "socks4" | "socks5";
|
||||
pacScript?: string;
|
||||
bypassList?: string[];
|
||||
}
|
||||
|
||||
export interface ProxyLog {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
url: string;
|
||||
proxyId: string;
|
||||
proxyName: string;
|
||||
status: 'success' | 'error';
|
||||
errorMessage?: string;
|
||||
method?: string;
|
||||
requestHeaders?: Record<string, string>;
|
||||
requestBody?: string;
|
||||
responseHeaders?: Record<string, string>;
|
||||
responseBody?: string;
|
||||
timing?: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
};
|
||||
protocol?: string;
|
||||
ip?: string;
|
||||
fromCache?: boolean;
|
||||
host?: string;
|
||||
port?: number;
|
||||
resourceType?: 'xhr' | 'fetch' | 'script' | 'stylesheet' | 'image' | 'other';
|
||||
}
|
||||
+5
-1
@@ -15,7 +15,11 @@
|
||||
"@assets/*": ["./src/assets/*"],
|
||||
"@network/*": ["./src/network/*"],
|
||||
"@types/*": ["./src/types/*"]
|
||||
}
|
||||
},
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./src/types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
|
||||
Reference in New Issue
Block a user