Compare commits

...
20 Commits
Author SHA1 Message Date
go0p f463076def fix version 2025-03-04 16:14:13 +08:00
go0p 43743d729f Enhance webpack build script with production mode and progress tracking 2025-03-04 16:07:15 +08:00
go0p 8801de3441 update build script to use webpack directly 2025-03-04 16:07:15 +08:00
go0p 9a63f596b8 fix chrome doesn't give a damn about proxy on localhost 2025-03-04 16:07:15 +08:00
go0p 2253bdffb1 fix setting pac 2025-03-04 16:07:15 +08:00
go0p 322cd6dcd6 todo switch 2025-03-04 16:07:15 +08:00
go0p f0fb3f6cf0 fix popup options.html context 2025-03-04 16:07:15 +08:00
go0p 5066e9d89a add inject content ui 2025-03-04 16:07:15 +08:00
go0p 35184f4b25 yakit mitm config 2025-03-04 16:07:14 +08:00
go0p 1eee8217ec ui v0.0.2-beta 2025-03-04 16:07:14 +08:00
go0p 1149cfeb4d ui v0.0.1-beta 2025-03-04 16:07:14 +08:00
go0p cf10abc3f8 ui v0.0.1-beta 2025-03-04 16:07:14 +08:00
go0p 86dd6cd40c fix ui 2025-03-04 16:07:14 +08:00
go0p 06c053228c fix 2025-03-04 16:07:14 +08:00
go0p 6f82842c92 use indexedDB manager proxySwitch 2025-03-04 16:07:14 +08:00
go0p 1e25561fae add proxy switch demo 2025-03-04 16:07:14 +08:00
go0p 8c30607514 add proxy switch demo 2025-03-04 16:07:14 +08:00
go0p 692ab7760e fix inject 2025-03-04 16:07:14 +08:00
go0p 08796f91c4 feat(wsc) brute encrypt demo 2025-03-04 16:07:14 +08:00
go0p 7c3eb80872 feat(wsc) fuzzer demo 2025-03-04 16:07:14 +08:00
48 changed files with 15345 additions and 430 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ build.pem
# production
/build
/2.5.21_0
# misc
.DS_Store
.env.local
+3 -1
View File
@@ -30,6 +30,7 @@
"jest": "^27.4.3",
"jest-resolve": "^27.4.2",
"jest-watch-typeahead": "^1.0.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.4.5",
"postcss": "^8.4.4",
"postcss-flexbugs-fixes": "^5.0.2",
@@ -56,7 +57,7 @@
},
"scripts": {
"start": "node scripts/start.js",
"build": "cross-env NODE_ENV=production node scripts/build.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.js --mode production --progress --no-watch",
"watch": "cross-env NODE_ENV=development webpack --config webpack.config.js",
"test": "node scripts/test.js"
},
@@ -139,6 +140,7 @@
"@babel/preset-env": "^7.24.1",
"@babel/preset-react": "^7.24.1",
"@types/chrome": "^0.0.268",
"@types/lodash": "^4.17.15",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
+33 -37
View File
@@ -1,19 +1,39 @@
import {ActionType, WebSocketManager} from './socket.js';
import {ActionType, injectScriptAndSendMessage, WebSocketManager} from './socket.js';
import { setupProxyHandlers } from './proxy.js';
console.info("Chrome Extenstion Background is loaded")
console.info("Chrome Extension Background is loaded");
const websocketManager = new WebSocketManager();
// 设置代理处理器
setupProxyHandlers();
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
console.log("msg", msg)
// 添加点击事件处理
chrome.action.onClicked.addListener((tab) => {
// 打开侧边栏
chrome.sidePanel.open({windowId: tab.windowId}).catch(error => {
console.error('Error opening side panel:', error);
});
});
// 设置默认打开状态
chrome.sidePanel.setOptions({
enabled: true,
path: 'index.html'
}).catch(error => {
console.error('Error setting side panel options:', error);
});
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg.action) {
case ActionType.CONNECT:
console.info("Start to connect websocket")
const host = msg['host'] || "127.0.0.1"
const port = msg['port'] || 11212
websocketManager.connectWebsocket(`ws://${host}:${port}/?token=${"a"}`, port)
websocketManager.connectWebsocket(`ws://${host}:${port}/?token=chrome`, port)
break;
case ActionType.SEND_MESSAGE:
websocketManager.sendMessage(msg.message);
break;
case ActionType.DISCONNECT:
websocketManager.disconnectWebsocket();
@@ -27,7 +47,9 @@ chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
scheme: msg.scheme,
host: msg.host,
port: parseInt(`${msg.port}`)
}
},
// enable 127.0.0.1 && localhost to mitmproxy
bypassList: ["<-loopback>"]
}
},
scope: 'regular',
@@ -51,38 +73,12 @@ chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
break;
case ActionType.INJECT_SCRIPT:
(async () => {
try {
// 注入 JS 脚本
await chrome.scripting.executeScript({
target: {tabId: msg.tabId},
files: ['content.js']
});
// 发送消息
const response = await chrome.tabs.sendMessage(msg.tabId, {
type: ActionType.INJECT_SCRIPT,
value: msg.value
});
console.log("response", response);
if (response && response.action === ActionType.TO_EXTENSION_PAGE) {
await chrome.runtime.sendMessage(response);
}
} catch (err) {
console.error('Script or CSS injection failed:', err);
}
await injectScriptAndSendMessage(msg.tabId, {
type: ActionType.INJECT_SCRIPT,
value: msg.value
});
})();
break
}
})
const pageFunction = (code) => {
chrome.runtime.sendMessage({code}, response => {
if (response && response.success) {
console.log('Result:', response.result);
} else {
console.error('Error:', response.error);
}
});
}
+7 -3
View File
@@ -3,6 +3,7 @@
return;
}
window.contentScriptInjected = true;
window.badgeCount = 0;
// 检查并插入 CSS 样式
const styleId = 'injected-css-style';
if (!document.getElementById(styleId)) {
@@ -38,18 +39,21 @@
script.remove();
};
(document.head || document.documentElement).appendChild(script);
window.addEventListener('message', function onMessage(event) {
window.addEventListener('message', async function onMessage(event) {
if (event.source !== window || event.data.type !== 'FROM_INJECT_JS') {
return;
}
window.removeEventListener('message', onMessage);
// Send the result to the background script
// chrome.runtime.sendMessage({ action: 'yakit_to_extension_page', result: event.data.result });
window.badgeCount += 1;
// 直接向向发送端返回结果
sendResponse({action: 'yakit_to_extension_page', result: event.data.result});
// Send updated badge count to background script
await chrome.runtime.sendMessage({action: 'yakit_badge', data: window.badgeCount.toString()});
});
return true;
}
});
})()
+120
View File
@@ -0,0 +1,120 @@
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();
+175
View File
@@ -0,0 +1,175 @@
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(() => {
// 忽略接收者不存在的错误
});
}
async addAndEnableProxy(config) {
try {
// 先禁用所有其他代理
const existingConfigs = await this.getProxyConfigs();
for (const existingConfig of existingConfigs) {
if (existingConfig.enabled) {
await this.saveProxyConfigs([{
...existingConfig,
enabled: false
}]);
}
}
// 添加并启用新代理
await this.saveProxyConfigs([config]);
// 应用新代理
await chrome.proxy.settings.set({
value: {
mode: config.proxyType,
rules: {
singleProxy: {
scheme: config.scheme,
host: config.host,
port: config.port
}
}
},
scope: 'regular'
});
return { success: true };
} catch (error) {
console.error('Error in addAndEnableProxy:', error);
return { success: false, error };
}
}
}
export const proxyStore = new ProxyStore();
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

+1
View File
@@ -18,6 +18,7 @@
break;
case 'CONTENT_EVAL_CODE':
const code = event.data.value.code;
console.log(code)
result = (() => {
try {
return eval(code);
+31 -9
View File
@@ -1,8 +1,12 @@
{
"manifest_version": 3,
"name": "Yakit Chrome Endpoint",
"version": "1.0",
"version": "0.0.1",
"description": "A Endpoint for Yakit MITM or more",
"options_ui": {
"page": "proxy/options.html",
"open_in_tab": true
},
"action": {
"default_popup": "index.html",
"default_icon": {
@@ -11,18 +15,28 @@
"128": "/images/icon128.png"
}
},
"side_panel": {
"default_path": "index.html"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["http://mitm/"],
"run_at": "document_start",
"js": ["proxy/content.js"]
}
],
"permissions": [
"webNavigation",
"activeTab",
"scripting",
"tabs",
"proxy",
"storage",
"webRequest"
"sidePanel",
"webRequest",
"declarativeNetRequest",
"webNavigation",
"tabs"
],
"host_permissions": [
"<all_urls>"
@@ -30,9 +44,17 @@
"web_accessible_resources": [
{
"resources": ["inject.js"],
"matches": ["<all_urls>"],
"use_dynamic_url": true
"resources": [
"images/*",
"proxy/*"
],
"matches": ["<all_urls>"]
},
{
"resources": [
"/images/yak.svg"
],
"matches": ["<all_urls>"]
}
],
"icons": {
+552
View File
@@ -0,0 +1,552 @@
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 {proxyStore} from './db/proxy-store.js';
// 修改代理状态获取函数为 Promise 形式
function getProxySettings() {
return new Promise((resolve) => {
chrome.proxy.settings.get({}, resolve);
});
}
async function handleSetProxyConfig(config, sendResponse) {
try {
// 处理代理服务器的情况
if (config.proxyType === 'fixed_servers') {
// 固定代理服务器模式需要验证 host 和 port
if (!config || !config.host || !config.port) {
sendResponse({
success: false,
error: '无效的代理配置:缺少主机或端口'
});
return;
}
const proxyConfig = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: config.scheme || 'http',
host: config.host,
port: parseInt(config.port)
},
bypassList: config.bypassList || []
}
};
await new Promise((resolve) => {
chrome.proxy.settings.set({
value: proxyConfig,
scope: 'regular'
}, resolve);
});
const settings = await getProxySettings();
const isSuccess = settings.value.mode === "fixed_servers" &&
settings.value.rules.singleProxy.host === config.host &&
settings.value.rules.singleProxy.port === parseInt(config.port);
if (isSuccess) {
await proxyStore.setCurrentProxy({
...config,
timestamp: Date.now()
});
const configs = await proxyStore.getProxyConfigs();
const updatedConfigs = configs.map(c => ({
...c,
enabled: c.id === config.id
}));
await proxyStore.saveProxyConfigs(updatedConfigs);
console.log('Proxy successfully set:', settings.value);
sendResponse({success: true});
// 通知所有 content scripts 更新
await notifyProxyStatusChanged();
} else {
console.error('Proxy settings verification failed');
sendResponse({
success: false,
error: '代理设置验证失败'
});
}
return;
} else if (config.proxyType === 'pac_script') {
// PAC 脚本模式需要验证 pacScript
if (!config || !config.pacScript || !config.pacScript.data) {
sendResponse({
success: false,
error: '无效的 PAC 脚本配置'
});
return;
}
const proxyConfig = {
mode: "pac_script",
pacScript: config.pacScript
};
await new Promise((resolve) => {
chrome.proxy.settings.set({
value: proxyConfig,
scope: 'regular'
}, resolve);
});
const settings = await getProxySettings();
const isSuccess = settings.value.mode === "pac_script" &&
settings.value.pacScript &&
settings.value.pacScript.data;
if (isSuccess) {
await proxyStore.setCurrentProxy({
...config,
timestamp: Date.now()
});
const configs = await proxyStore.getProxyConfigs();
const updatedConfigs = configs.map(c => ({
...c,
enabled: c.id === config.id
}));
await proxyStore.saveProxyConfigs(updatedConfigs);
console.log('PAC script proxy successfully set:', settings.value);
sendResponse({success: true});
// 通知所有 content scripts 更新
await notifyProxyStatusChanged();
} else {
console.error('PAC script settings verification failed', {
expected: config,
actual: settings.value
});
sendResponse({
success: false,
error: '代理设置验证失败'
});
}
return;
} else if (config.proxyType === 'direct' || config.proxyType === 'system') {
// 直接连接或系统代理模式
const proxyConfig = {
mode: config.proxyType
};
await new Promise((resolve) => {
chrome.proxy.settings.set({
value: proxyConfig,
scope: 'regular'
}, resolve);
});
const settings = await getProxySettings();
const isSuccess = settings.value.mode === config.proxyType;
if (isSuccess) {
await proxyStore.setCurrentProxy({
...config,
timestamp: Date.now()
});
const configs = await proxyStore.getProxyConfigs();
const updatedConfigs = configs.map(c => ({
...c,
enabled: c.id === config.id
}));
await proxyStore.saveProxyConfigs(updatedConfigs);
console.log('Proxy successfully set:', settings.value);
sendResponse({success: true});
// 通知所有 content scripts 更新
await notifyProxyStatusChanged();
} else {
console.error('Proxy settings verification failed');
sendResponse({
success: false,
error: '代理设置验证失败'
});
}
return;
} else {
sendResponse({
success: false,
error: '不支持的代理类型'
});
return;
}
} catch (error) {
console.error('Error setting proxy:', error);
sendResponse({
success: false,
error: error.message || '设置代理时发生错误'
});
}
}
async function handleClearProxyConfig(sendResponse) {
try {
await chrome.proxy.settings.clear({
scope: 'regular'
});
// 获取所有配置并禁用
const configs = await proxyStore.getProxyConfigs();
const updatedConfigs = configs.map(config => ({
...config,
enabled: false
}));
await proxyStore.saveProxyConfigs(updatedConfigs);
sendResponse({ success: true });
// 通知所有 content scripts 更新
await notifyProxyStatusChanged();
} catch (error) {
console.error('Error clearing proxy config:', error);
sendResponse({ success: false, error: error.message });
}
}
async function handleGetProxyStatus(sendResponse) {
try {
const settings = await getProxySettings();
const currentProxy = await proxyStore.getCurrentProxy();
const status = {
enabled: settings.value.mode === "fixed_servers",
config: currentProxy || null,
mode: settings.value.mode
};
console.log('Current proxy status:', status);
sendResponse({
success: true,
data: status
});
} catch (error) {
console.error('Error getting proxy status:', error);
sendResponse({
success: false,
error: error.message || '获取代理状态时发生错误'
});
}
}
// 添加代理请求监听器
function setupProxyRequestListener() {
// 监听请求发送
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
// 使用非阻塞方式处理请求
queueProxyLog(details).catch(error => {
console.error('Error in proxy request listener:', error);
});
// 不需要返回值
},
{urls: ["<all_urls>"]}
);
// 监听请求错误
chrome.webRequest.onErrorOccurred.addListener(
(details) => {
// 使用非阻塞方式处理错误
queueProxyLog(details, new Error(details.error)).catch(error => {
console.error('Error in proxy error listener:', error);
});
},
{urls: ["<all_urls>"]}
);
}
// 使用队列处理日志
async function queueProxyLog(details, error = null) {
try {
// 检查代理状态
const settings = await getProxySettings();
if (settings.value.mode !== "fixed_servers") {
return;
}
// 获取当前代理配置
const currentProxy = await proxyStore.getCurrentProxy();
if (!currentProxy) {
return;
}
// 记录日志
await proxyLogs.logRequest(details, currentProxy, error);
} catch (error) {
console.error('Error in queueProxyLog:', error);
}
}
// 添加检查和设置初始代理的函数
async function checkAndSetInitialProxy() {
try {
// 确保默认配置存在
await ProxySettings.setDefaultConfigs();
// 获取当前的代理设置
const settings = await getProxySettings();
console.log('Current proxy settings:', settings);
// 检查是否存在固定代理服务器设置
if (settings.value.mode === "fixed_servers" &&
settings.value.rules &&
settings.value.rules.singleProxy) {
const proxy = settings.value.rules.singleProxy;
// 获取现有配置
const existingConfigs = await proxyStore.getProxyConfigs();
// 检查是否已存在相同的 MITM 配置
const existingMitm = existingConfigs.find(config =>
config.host === proxy.host &&
config.port === proxy.port &&
config.scheme === proxy.scheme
);
if (!existingMitm) {
// 创建新的 MITM 配置
const newConfig = {
id: Date.now().toString(),
name: "Yakit MITM",
proxyType: 'fixed_servers',
scheme: proxy.scheme || 'http',
host: proxy.host,
port: proxy.port,
enabled: true,
// https://bugs.chromium.org/p/chromium/issues/detail?id=899126#c17
bypassList: ["<-loopback>"],
matchList: []
};
// 添加到现有配置中
const updatedConfigs = [...existingConfigs, newConfig];
await proxyStore.saveProxyConfigs(updatedConfigs);
// 启用新配置
await handleSetProxyConfig(newConfig, () => {});
console.log('Added and enabled Yakit MITM config from existing proxy settings');
return;
}
}
// 如果没有检测到代理或已存在配置,则设置为系统代理
await handleSetProxyConfig({
id: 'system',
name: '[系统代理]',
proxyType: 'system',
enabled: true
}, () => {});
// 设置认证监听
await ProxyAuth.setupAuthListener();
} catch (error) {
console.error('Error during initialization:', error);
}
}
// 修改 setupProxyHandlers 函数
export function setupProxyHandlers() {
// 设置代理错误处理和认证
ProxyAuth.setupErrorHandler();
ProxyAuth.setupAuthListener();
// 设置代理请求监听器
setupProxyRequestListener();
// 消息监听器
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
console.log("Proxy message:", msg);
switch (msg.action) {
case ProxyActionType.SET_PROXY_CONFIG:
handleSetProxyConfig(msg.config, sendResponse);
return true;
case ProxyActionType.CLEAR_PROXY_CONFIG:
(async () => {
await handleClearProxyConfig(sendResponse);
})();
return true;
case ProxyActionType.GET_PROXY_STATUS:
handleGetProxyStatus(sendResponse);
return 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:
(async () => {
try {
const configs = await proxyStore.getProxyConfigs();
sendResponse({ success: true, data: configs });
} catch (error) {
console.error('Error getting proxy configs:', error);
sendResponse({ success: false, error: error.message });
}
})();
return true;
case ProxyActionType.ADD_PROXY_CONFIG:
proxyStore.getProxyConfigs().then(async configs => {
const newConfigs = [...configs, msg.config];
ProxyActionType
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
});
// 通知所有 content scripts 更新
await notifyProxyStatusChanged();
} catch (error) {
console.error('Error updating proxy configs:', error);
sendResponse({
success: false,
error: error.message || '更新代理配置失败'
});
}
})();
return true;
case ProxyActionType.OPEN_OPTIONS_PAGE:
// 打开选项页
chrome.tabs.create({
url: chrome.runtime.getURL('/proxy/options.html')
}).then(tab => {
if (msg.triggerAdd) {
// 如果需要触发添加代理,等待页面加载完成
const listener = (tabId, changeInfo) => {
if (tabId === tab.id && changeInfo.status === 'complete') {
chrome.tabs.onUpdated.removeListener(listener);
// 向选项页发送消息触发添加代理
chrome.tabs.sendMessage(tab.id, {
action: 'TRIGGER_ADD_PROXY'
});
}
};
chrome.tabs.onUpdated.addListener(listener);
}
});
sendResponse({ success: true });
return true;
}
});
// 在扩展启动时初始化
chrome.runtime.onInstalled.addListener(async () => {
await checkAndSetInitialProxy();
});
// 浏览器启动时初始化
chrome.runtime.onStartup.addListener(async () => {
await checkAndSetInitialProxy();
});
}
// 当代理状态改变时通知所有内容脚本
async function notifyProxyStatusChanged() {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
try {
chrome.tabs.sendMessage(tab.id, { action: 'PROXY_STATUS_CHANGED' });
} catch (error) {
// 忽略不支持的标签页
}
}
}
async function setProxyConfig(config) {
try {
let chromeProxyConfig;
if (config.proxyType === 'pac_script') {
chromeProxyConfig = {
mode: "pac_script",
pacScript: config.pacScript
};
} else if (config.proxyType === 'fixed_servers') {
chromeProxyConfig = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: config.scheme,
host: config.host,
port: config.port
},
bypassList: config.bypassList || []
}
};
} else {
chromeProxyConfig = {
mode: config.proxyType // direct, system, auto_detect
};
}
await chrome.proxy.settings.set({
value: chromeProxyConfig,
scope: 'regular'
});
return { success: true };
} catch (error) {
console.error('Failed to set proxy config:', error);
return { success: false, error: error.message };
}
}
+713
View File
@@ -0,0 +1,713 @@
console.log("Content script starting...");
// 代理操作类型常量
const ProxyActionType = {
SET_PROXY_CONFIG: 'SET_PROXY_CONFIG',
CLEAR_PROXY_CONFIG: 'CLEAR_PROXY_CONFIG',
GET_PROXY_STATUS: 'GET_PROXY_STATUS',
GET_PROXY_CONFIGS: 'GET_PROXY_CONFIGS',
UPDATE_PROXY_CONFIG: 'UPDATE_PROXY_CONFIG'
};
// 在文件顶部添加常量声明
const YAK_ICON_URL = chrome.runtime.getURL('/images/yak.svg');
// 添加一个通用的消息发送函数
async function sendMessageWithRetry(message, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await chrome.runtime.sendMessage(message);
} catch (error) {
console.warn(`Attempt ${i + 1} failed:`, error);
if (i === maxRetries - 1) {
throw error;
}
// 等待一小段时间后重试
await new Promise(resolve => setTimeout(resolve, 500));
}
}
}
// 获取当前代理状态
async function getCurrentProxy() {
try {
const response = await sendMessageWithRetry({
action: ProxyActionType.GET_PROXY_STATUS
});
if (!response || !response.success) {
console.log('No valid response from background script');
return { enable: false, proxy: '', currentMode: 'direct' };
}
const status = response.data;
console.log('Proxy status from background:', status);
// 返回当前模式
return {
enable: status.enabled,
proxy: status.mode === 'system' ? 'system' : '',
currentMode: status.mode || 'direct' // 使用 mode 来判断当前激活的代理
};
} catch (error) {
console.error('Error getting proxy status:', error);
return { enable: false, proxy: '', currentMode: 'direct' };
}
}
// 获取所有代理配置
async function getProxyConfigs() {
try {
const response = await sendMessageWithRetry({
action: ProxyActionType.GET_PROXY_CONFIGS
});
if (!response) {
console.log('No response from background script');
return [];
}
if (!response.success) {
console.error('Error in response:', response.error);
return [];
}
return response.data || [];
} catch (error) {
console.error('Error getting proxy configs:', error);
return [];
}
}
// 切换代理
async function switchProxy(config) {
try {
console.log('Switching proxy:', config);
// 根据配置类型构建正确的配置对象
let proxyConfig;
if (config.proxyType === 'system') {
proxyConfig = {
id: 'system',
name: '[系统代理]',
proxyType: 'system',
enabled: true
};
} else if (config.proxyType === 'direct') {
proxyConfig = {
id: 'direct',
name: '[直接连接]',
proxyType: 'direct',
enabled: false
};
} else {
proxyConfig = {
...config,
enabled: true
};
}
await sendMessageWithRetry({
action: ProxyActionType.SET_PROXY_CONFIG,
config: proxyConfig
});
await PanelManager.updatePanel();
} catch (error) {
console.error('Error switching proxy:', error);
}
}
// 清除代理
async function clearProxy() {
try {
const response = await sendMessageWithRetry({
action: ProxyActionType.SET_PROXY_CONFIG,
config: {
id: 'direct',
name: '[直接连接]',
proxyType: 'direct',
enabled: false
}
});
console.log('Clear proxy response:', response);
// 使用 PanelManager 的 updatePanel 方法
await PanelManager.updatePanel();
} catch (error) {
console.error('Error clearing proxy:', error);
}
}
// 修改 PanelManager
const PanelManager = {
panel: null,
messageListener: null,
_updating: false,
_updateQueue: Promise.resolve(),
_currentState: null, // 用于跟踪当前状态
_lastUpdate: null, // 添加最后更新时间戳
_lastState: null,
_pollingInterval: null,
init() {
// 确保 document.body 存在
if (!document.body) {
console.log('Body not ready, waiting...');
this.waitForBody();
return;
}
if (this.panel) {
console.log('Panel already exists, updating...');
this.updatePanel();
return;
}
console.log('Creating new panel...');
this.createPanel();
},
// 添加等待 body 的方法
waitForBody() {
if (document.body) {
this.init();
return;
}
console.log('Setting up MutationObserver for body');
const observer = new MutationObserver((mutations, obs) => {
if (document.body) {
console.log('Body found via observer');
obs.disconnect();
this.init();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
},
createPanel() {
if (!document.body) {
console.log('Body not available during panel creation');
return;
}
// 创建容器
const container = document.createElement('div');
container.id = 'yakit-proxy-panel';
// 创建 shadow DOM
const shadow = container.attachShadow({ mode: 'open' });
// 添加样式
const style = document.createElement('style');
style.textContent = `
.floating-panel {
position: fixed;
top: 20px;
right: 0;
width: 180px;
background: white;
border-radius: 8px 0 0 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 2147483647;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
transition: transform 0.3s ease;
}
.floating-panel.collapsed {
transform: translateX(100%);
}
.panel-header {
padding: 4px;
border-bottom: 1px solid #eee;
border-radius: 8px 0 0 0;
background: #f8f9fa;
display: flex;
justify-content: space-between;
align-items: center;
}
.collapse-trigger {
position: absolute;
left: -20px;
top: 0;
width: 20px;
height: 100%;
background: #f8f9fa;
border-radius: 8px 0 0 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
}
.collapse-trigger:hover {
background: #e9ecef;
}
.collapse-trigger::after {
content: '◀';
transition: transform 0.3s ease;
}
.floating-panel.collapsed .collapse-trigger::after {
transform: rotate(180deg);
}
.panel-content {
padding: 4px;
}
.proxy-item {
position: relative;
display: flex;
align-items: center;
padding: 4px 10px;
cursor: pointer;
transition: all 0.2s;
color: #666;
border-left: 3px solid transparent;
overflow: hidden;
}
.proxy-item > span {
position: relative;
z-index: 1;
}
.watermark-icon {
position: absolute;
right: 0;
width: 100%;
height: 100%;
opacity: 0.3;
pointer-events: none;
display: none;
object-fit: contain;
object-position: right center;
padding: 4px;
}
.proxy-item.active .watermark-icon {
display: block;
}
.proxy-item.active {
color: #ff6b00 !important;
background: #fff7e6;
border-left: 3px solid #ff6b00;
}
.proxy-item.active span:first-child {
color: #ff6b00 !important;
}
.proxy-item span:first-child {
margin-right: 8px;
font-size: 16px;
color: #666;
transition: color 0.2s;
}
.proxy-item:hover {
background: #f5f5f5;
}
.proxy-item:hover span:first-child {
color: #ff6b00;
}
.add-proxy {
display: flex;
align-items: center;
padding: 4px 10px;
color: #1890ff;
cursor: pointer;
border-top: 1px solid #eee;
transition: all 0.2s;
}
.add-proxy:hover {
background: #f5f5f5;
}
.settings {
padding: 4px 10px;
color: #666;
cursor: pointer;
border-top: 1px solid #eee;
transition: all 0.2s;
}
.settings:hover {
background: #f5f5f5;
}
.panel-header .header-content {
display: flex;
align-items: center;
gap: 8px;
padding: 0 4px;
}
.yak-icon {
width: 24px;
height: 24px;
object-fit: contain;
}
.divider {
height: 1px;
background-color: #eee;
margin: 4px 0;
}
`;
// 创建面板内容
const panel = document.createElement('div');
panel.className = 'floating-panel';
panel.innerHTML = `
<div class="collapse-trigger"></div>
<div class="panel-header">
<div class="header-content">
<img src="${YAK_ICON_URL}" class="yak-icon" alt="Yak" />
<span>代理设置</span>
</div>
</div>
<div class="panel-content">
<div class="proxy-item active">
<span>🟢</span>
<span>[直接连接]</span>
</div>
<div class="proxy-item">
<span>⚙️</span>
<span>[系统代理]</span>
</div>
<div class="divider"></div>
<div class="add-proxy">
<span></span>
<span>[添加代理...]</span>
</div>
<div class="settings">
<span>👨‍💻</span>
<span>选项</span>
</div>
</div>
`;
// 将样式和面板添加到 shadow DOM
shadow.appendChild(style);
shadow.appendChild(panel);
// 确保安全地添加到 body
try {
document.body.appendChild(container);
this.panel = container;
// 添加折叠触发器的点击事件
const floatingPanel = shadow.querySelector('.floating-panel');
const collapseTrigger = shadow.querySelector('.collapse-trigger');
collapseTrigger.addEventListener('click', () => {
floatingPanel.classList.toggle('collapsed');
});
// 设置消息监听和开始轮询
this.setupMessageListener();
// 初始更新面板
this.updatePanel();
// 添加页面卸载时的清理
window.addEventListener('unload', () => {
if (this._pollingInterval) {
clearInterval(this._pollingInterval);
}
if (this.messageListener) {
chrome.runtime.onMessage.removeListener(this.messageListener);
}
});
} catch (error) {
console.error('Error creating panel:', error);
}
},
setupMessageListener() {
if (this.messageListener) {
chrome.runtime.onMessage.removeListener(this.messageListener);
}
this.messageListener = async (message, sender) => {
// 只检查消息是否来自同一个扩展
if (sender.id !== chrome.runtime.id) {
return;
}
// 处理状态更新消息
if (message.action === 'PROXY_STATUS_CHANGED' ||
message.action === 'PROXY_CONFIGS_UPDATED') {
console.log('Received update message:', message, 'from:', sender);
await this.updatePanel();
}
};
chrome.runtime.onMessage.addListener(this.messageListener);
},
async updatePanel() {
if (!this.panel || !document.body.contains(this.panel)) {
console.log('Panel not in document, recreating...');
this.createPanel();
return;
}
const panel = this.panel.shadowRoot?.querySelector('.panel-content');
if (!panel) return;
// 使用更新队列确保更新按顺序执行
this._updateQueue = this._updateQueue.then(async () => {
if (this._updating) {
console.log('Update already in progress, skipping...');
return;
}
try {
this._updating = true;
// 获取最新状态
const [currentProxy, configs] = await Promise.all([
getCurrentProxy(),
getProxyConfigs()
]);
// 状态没有变化时不更新
const newState = JSON.stringify({ currentProxy, configs });
if (this._currentState === newState) {
console.log('State unchanged, skipping update');
return;
}
this._currentState = newState;
// 再次检查面板状态
if (!this.panel || !document.body.contains(this.panel)) {
console.log('Panel was removed during data fetch');
return;
}
console.log('Updating panel with:', { currentProxy, configs });
if (!Array.isArray(configs)) {
console.error('Invalid configs:', configs);
return;
}
// 保存当前激活的项
const currentActiveId = panel.querySelector('.proxy-item.active')?.dataset.id;
// 构建新的 HTML
const newHtml = this._buildPanelHtml(currentProxy, configs);
// 创建一个临时容器来比较内容
const temp = document.createElement('div');
temp.innerHTML = newHtml;
// 只在内容真正改变时更新
if (panel.innerHTML !== temp.innerHTML) {
requestAnimationFrame(() => {
panel.innerHTML = newHtml;
this._bindEventListeners(panel, configs, currentProxy);
// 验证更新后的状态
const newActiveId = panel.querySelector('.proxy-item.active')?.dataset.id;
if (currentActiveId !== newActiveId) {
console.log('Active state changed:', {
from: currentActiveId,
to: newActiveId
});
}
});
}
} catch (error) {
console.error('Error updating panel:', error);
} finally {
this._updating = false;
}
}).catch(error => {
console.error('Error in update queue:', error);
this._updating = false;
});
return this._updateQueue;
},
// 将 HTML 构建逻辑抽离成单独的方法
_buildPanelHtml(currentProxy, configs) {
let html = `
<div class="proxy-item ${currentProxy.currentMode === 'direct' ? 'active' : ''}"
data-id="direct"
title="直接连接">
<span style="color: ${currentProxy.currentMode === 'direct' ? '#ff6b00' : '#666'}">🟢</span>
<span>[直接连接]</span>
<img src="${YAK_ICON_URL}" class="watermark-icon" alt="" />
</div>
<div class="proxy-item ${currentProxy.currentMode === 'system' ? 'active' : ''}"
data-id="system"
title="系统代理">
<span style="color: ${currentProxy.currentMode === 'system' ? '#ff6b00' : '#666'}">⚙️</span>
<span>[系统代理]</span>
<img src="${YAK_ICON_URL}" class="watermark-icon" alt="" />
</div>
<div class="divider"></div>
`;
// 添加自定义代理配置
configs.forEach(config => {
if (config.proxyType !== 'direct' && config.proxyType !== 'system') {
// 判断是否激活:当前模式为 fixed_servers 且配置已启用
const isActive = currentProxy.currentMode === 'fixed_servers' &&
config.enabled;
const tooltipText = config.proxyType === 'pac_script'
? 'PAC Script'
: `${(config.proxyType || 'HTTP').toUpperCase()} ${config.host || ''}:${config.port || ''}`;
const proxyIcon = config.proxyType === 'pac_script' ? '📜' : '🌐';
html += `
<div class="proxy-item ${isActive ? 'active' : ''}"
data-id="${config.id}"
title="${tooltipText}">
<span style="color: ${isActive ? '#ff6b00' : '#666'}">${proxyIcon}</span>
<span>${config.name || '未命名代理'}</span>
<img src="${YAK_ICON_URL}" class="watermark-icon" alt="" />
</div>
`;
}
});
// 添加操作按钮
html += `
<div class="add-proxy">
<span></span>
<span>添加代理...</span>
</div>
<div class="settings">
<span>👨‍💻</span>
<span>选项</span>
</div>
`;
return html;
},
_bindEventListeners(panel, configs, currentProxy) {
// 代理项点击事件
panel.querySelectorAll('.proxy-item').forEach(item => {
const id = item.dataset.id;
// 使用事件委托来提高性能
const clickHandler = async (e) => {
e.stopPropagation();
if (this._updating) {
console.log('Panel is updating, ignoring click');
return;
}
// 添加点击反馈
const originalOpacity = item.style.opacity;
item.style.opacity = '0.7';
try {
// 立即更新 UI 状态,不等待响应
panel.querySelectorAll('.proxy-item').forEach(i => {
i.classList.remove('active');
i.querySelector('span').style.color = '#666';
});
item.classList.add('active');
item.querySelector('span').style.color = '#ff6b00';
if (id === 'direct') {
await clearProxy();
} else if (id === 'system') {
await switchProxy({
id: 'system',
name: '[系统代理]',
proxyType: 'system'
});
} else {
const config = configs.find(c => c.id === id);
if (config) {
await switchProxy(config);
}
}
} catch (error) {
console.error('Error handling proxy item click:', error);
// 发生错误时恢复原状
await this.updatePanel();
} finally {
item.style.opacity = originalOpacity;
}
};
// 使用 { once: true } 确保事件监听器不会重复
item.addEventListener('click', clickHandler, { once: true });
});
// 添加代理按钮
panel.querySelector('.add-proxy')?.addEventListener('click', async () => {
try {
await sendMessageWithRetry({
action: 'OPEN_OPTIONS_PAGE',
triggerAdd: true
});
} catch (error) {
console.error('Error handling add proxy:', error);
}
});
// 设置按钮
panel.querySelector('.settings')?.addEventListener('click', async () => {
try {
await sendMessageWithRetry({
action: 'OPEN_OPTIONS_PAGE'
});
} catch (error) {
console.error('Error opening options page:', error);
}
});
}
};
// 修改初始化调用
console.log("Setting up initialization...");
// 根据文档状态决定初始化方式
if (document.readyState === 'loading') {
console.log('Document still loading, waiting for DOMContentLoaded');
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded fired');
PanelManager.init();
});
} else {
console.log('Document already loaded, initializing immediately');
PanelManager.init();
}
// 保留 load 事件作为备份
window.addEventListener('load', () => {
console.log("Window load triggered");
if (!PanelManager.panel) {
PanelManager.init();
}
});
// 添加更详细的日志
console.log("Document readyState:", document.readyState);
console.log("Document body exists:", !!document.body);
console.log("Document documentElement exists:", !!document.documentElement);
+160
View File
@@ -0,0 +1,160 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.header h1 {
margin: 0;
font-size: 24px;
color: #333;
}
.proxy-item {
background: white;
border: 1px solid #e8e8e8;
border-radius: 4px;
margin-bottom: 16px;
padding: 16px;
transition: all 0.3s;
}
.proxy-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.09);
}
.proxy-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.proxy-name {
font-size: 14px;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
width: 200px;
}
.proxy-name:focus {
border-color: #40a9ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
}
.proxy-type-select,
.proxy-scheme,
.proxy-host,
.proxy-port {
width: 100%;
height: 32px;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
}
.proxy-type-select:focus,
.proxy-scheme:focus,
.proxy-host:focus,
.proxy-port:focus {
border-color: #40a9ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
}
.proxy-actions {
display: flex;
gap: 8px;
}
.proxy-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #666;
}
.btn-primary {
background: #1890ff;
border: none;
color: white;
padding: 4px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary:hover {
background: #40a9ff;
}
.btn-secondary {
background: white;
border: 1px solid #d9d9d9;
color: rgba(0,0,0,0.85);
padding: 4px 15px;
border-radius: 4px;
cursor: pointer;
margin-left: 8px;
}
.btn-secondary:hover {
border-color: #40a9ff;
color: #40a9ff;
}
.header-actions {
display: flex;
align-items: center;
}
.delete-btn {
padding: 4px 8px;
background: #ff4d4f;
}
.delete-btn:hover {
background: #ff7875;
}
.pac-script {
font-family: monospace;
min-height: 200px;
}
.options-page {
min-height: 100vh;
}
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>代理设置</title>
</head>
<body>
<div id="root"></div>
<script src="../options.bundle.js"></script>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
// 代理认证管理
import { proxyStore } from '../db/proxy-store.js';
export class ProxyAuth {
static async setupAuthListener() {
// 使用 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>"] }
);
}
static async saveAuthHandler(host, username, password) {
const handler = {
id: Date.now().toString(),
host,
username,
password
};
await proxyStore.saveAuthHandler(handler);
await this.setupAuthListener(); // 重新设置认证规则
}
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() {
// 使用 storage 记录错误
return {
logError: async (error) => {
const errors = await proxyStore.getErrors() || [];
errors.push({
timestamp: Date.now(),
error: error.message || error
});
await proxyStore.saveErrors(errors.slice(-100)); // 只保留最近100条错误记录
}
};
}
// 设置代理认证信息
static async setProxyAuth(username, password) {
try {
await proxyStore.saveAuth({ username, password, timestamp: Date.now() });
return true;
} catch (error) {
console.error('Error setting proxy auth:', error);
return false;
}
}
// 获取代理认证信息
static async getProxyAuth() {
try {
return await proxyStore.getAuth();
} catch (error) {
console.error('Error getting proxy auth:', error);
return null;
}
}
// 清除代理认证信息
static async clearProxyAuth() {
try {
await proxyStore.clearAuth();
return true;
} catch (error) {
console.error('Error clearing proxy auth:', error);
return false;
}
}
}
+150
View File
@@ -0,0 +1,150 @@
import { proxyStore } from '../db/proxy-store.js';
// 日志数据库管理
class ProxyLogs {
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';
}
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
};
// 使用 proxyStore 存储日志
await proxyStore.addLog(log);
this.notifyLogUpdate();
} catch (error) {
console.error('Error logging proxy request:', error);
}
}
async getLogs() {
return await proxyStore.getLogs();
}
async clearLogs() {
await proxyStore.clearLogs();
this.notifyLogUpdate();
}
notifyLogUpdate() {
// 通知前端日志已更新
chrome.runtime.sendMessage({
action: 'PROXY_LOGS_UPDATED'
}).catch(() => {
// 忽略接收者不存在的错误
});
}
}
export const proxyLogs = new ProxyLogs();
+51
View File
@@ -0,0 +1,51 @@
import { proxyStore } from '../db/proxy-store.js';
// 代理配置存储和管理
export class ProxySettings {
static async importSettings(settings) {
try {
if (Array.isArray(settings) && settings.every(s => s.proxyType)) {
await proxyStore.saveProxyConfigs(settings);
return {success: true};
}
return {success: false, error: "Invalid settings format"};
} catch (error) {
return {success: false, error: error.message};
}
}
static async exportSettings() {
try {
const configs = await proxyStore.getProxyConfigs();
return {success: true, settings: configs || []};
} catch (error) {
return {success: false, error: error.message};
}
}
static async setDefaultConfigs() {
const configs = await proxyStore.getProxyConfigs();
if (!configs || configs.length === 0) {
// 设置默认的直接连接配置
await proxyStore.saveProxyConfigs([
{
id: 'direct',
name: '直接连接',
proxyType: 'direct',
enabled: false
},
{
id: 'system',
name: '系统代理',
proxyType: 'system',
enabled: false
}
]);
}
// 确保日志存储已初始化
const logs = await proxyStore.getLogs(); // 使用 getLogs 而不是 getProxyLogs
if (!logs || logs.length === 0) {
await proxyStore.clearLogs(); // 初始化日志存储
}
}
}
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>My Sidepanel</title>
</head>
<body>
<h1>All sites sidepanel extension</h1>
<p>This side panel is enabled on all sites</p>
</body>
</html>
+53 -2
View File
@@ -1,5 +1,6 @@
export const ActionType = {
CONNECT: 'connect',
SEND_MESSAGE: 'send_message',
DISCONNECT: 'disconnect',
STATUS: 'status',
PROXY_STATUS: 'proxy_status',
@@ -7,6 +8,7 @@ export const ActionType = {
CLEAR_PROXY: 'clear_proxy',
INJECT_SCRIPT: 'yakit_inject_script',
TO_EXTENSION_PAGE: "yakit_to_extension_page",
BADGE_COUNT: "yakit_badge",
}
export class WebSocketManager {
@@ -25,6 +27,7 @@ export class WebSocketManager {
};
this.socket.onmessage = (event) => {
console.log("event", event)
this.handleMessage(event.data);
};
@@ -37,6 +40,19 @@ export class WebSocketManager {
};
}
sendMessage(message) {
if (this.isConnected()) {
try {
console.log("发射", message)
this.socket.send(JSON.stringify(message));
} catch (e) {
console.error("Error sending message:", e);
}
} else {
console.error("WebSocket is not connected.");
}
}
disconnectWebsocket() {
if (this.socket) {
try {
@@ -51,7 +67,7 @@ export class WebSocketManager {
}
startHeartbeat() {
this.intervalId = setInterval(() => this.heartbeat(), 3000);
this.intervalId = setInterval(() => this.heartbeat(), 25000);
}
stopHeartbeat() {
@@ -78,6 +94,41 @@ export class WebSocketManager {
}
handleMessage(message) {
console.log("message", message)
message = JSON.parse(message);
if (message && message.type === "eval") {
(async () => {
const [tab] = await getTab();
await injectScriptAndSendMessage(tab.id, {
type: ActionType.INJECT_SCRIPT,
value: {
mode: "CONTENT_EVAL_CODE", code: message.code,
}
});
})();
}
}
}
const getTab = async () => {
return chrome.tabs.query({active: true, lastFocusedWindow: true})
}
export const injectScriptAndSendMessage = async (tabId, message) => {
try {
// 注入 JS 脚本
await chrome.scripting.executeScript({
target: {tabId: tabId},
files: ['content.js']
});
// 发送消息
const response = await chrome.tabs.sendMessage(tabId, message);
console.log("response", response);
if (response && response.action === ActionType.TO_EXTENSION_PAGE) {
await chrome.runtime.sendMessage(response);
}
} catch (err) {
console.error('Script or CSS injection failed:', err);
}
}
+13
View File
@@ -0,0 +1,13 @@
// 这个是插件 background 中使用的 action 类型
// 和前端的 action 要保持一致
export const ProxyActionType = {
SET_PROXY_CONFIG: "SET_PROXY_CONFIG",
CLEAR_PROXY_CONFIG: "CLEAR_PROXY_CONFIG",
GET_PROXY_STATUS: "GET_PROXY_STATUS",
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",
OPEN_OPTIONS_PAGE: "OPEN_OPTIONS_PAGE",
};
-44
View File
@@ -1,44 +0,0 @@
.App {
width: 420px;
border-radius: 0px 0px 4px 4px;
border-right: 1px solid #EAECF3;
border-bottom: 1px solid #EAECF3;
border-left: 1px solid #EAECF3;
background: #fff;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+6 -3
View File
@@ -1,9 +1,11 @@
import React from "react";
import "./App.css";
// import "./App.css";
import {ConfigProvider} from "antd";
import {Contro} from "@components/Contro";
import {Proxifier} from "@components/Proxifier";
import {EvalInTab} from "@components/EvalInTab";
import {ProxySwitch} from "@components/ProxySwitch";
import './styles/global.css';
function App() {
return (
@@ -16,9 +18,10 @@ function App() {
>
<div className="App">
{/*<Contro/>*/}
<Proxifier/>
{/* <Proxifier/> */}
<ProxySwitch/>
<EvalInTab/>
{/* <EvalInTab/> */}
</div>
</ConfigProvider>
);
+10
View File
@@ -0,0 +1,10 @@
.add-proxy-form {
padding: 24px;
}
.form-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
+72
View File
@@ -0,0 +1,72 @@
import React from 'react';
import { Form, Input, Select, InputNumber, Button, message } from 'antd';
import { ProxyActionType } from '@/types/action';
import './index.css';
interface EditFormData {
name: string;
proxyType: string;
scheme?: string;
host?: string;
port?: number;
pacScript?: string;
}
export const AddProxyForm: React.FC = () => {
const [form] = Form.useForm<EditFormData>();
// 初始化表单
React.useEffect(() => {
form.setFieldsValue({
name: '',
proxyType: 'fixed_servers',
scheme: 'http',
host: '127.0.0.1',
port: 8080
});
}, []);
const handleSave = async () => {
try {
const values = await form.validateFields();
const newConfig = {
id: Date.now().toString(),
...values,
enabled: false
};
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.ADD_PROXY_CONFIG,
config: newConfig
});
if (response.success) {
message.success('添加成功');
window.close(); // 关闭窗口
}
} catch (error) {
console.error('Failed to add proxy:', error);
message.error('添加失败');
}
};
return (
<div className="add-proxy-form">
<Form
form={form}
layout="vertical"
onFinish={handleSave}
>
{/* 表单项与之前相同 */}
<Form.Item className="form-buttons">
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={() => window.close()}>
</Button>
</Form.Item>
</Form>
</div>
);
};
+6 -2
View File
@@ -13,9 +13,12 @@ export const EvalInTab: React.FC<EvalInTabProps> = () => {
useEffect(() => {
wsc.onWSCMessage((message) => {
console.log("message from content script:", message)
if (message.action === wsc.ActionType.TO_EXTENSION_PAGE) {
console.log("res:", message.result);
alert("from content script: " + JSON.stringify(message.result));
console.log("eval in tab:", message.result);
// alert("from content script: " + JSON.stringify(message.result));
// 发送结果
wsc.sendMessage({"type": "chrome-extension", res: message.result})
}
});
}, []);
@@ -29,6 +32,7 @@ export const EvalInTab: React.FC<EvalInTabProps> = () => {
value: {mode: "CONTENT_CALL_FUNCTION", fn_name: funcName, args: inputArgsData},
});
} catch (error) {
console.log("error:", error)
}
};
+290 -289
View File
@@ -1,310 +1,311 @@
import React, { useEffect, useState } from "react";
import { Space, Select, Input, Switch } from "antd";
import { PlusSmIcon, TrashIcon } from "@assets/icon/icon";
import { wsc } from "@network/chrome";
import React, {useEffect, useState} from "react";
import {Space, Select, Input, Switch} from "antd";
import {PlusSmIcon, TrashIcon} from "@assets/icon/icon";
import {wsc} from "@network/chrome";
import "./Proxifier.css";
type Scheme = "http" | "socks5";
interface ProxyConfig {
id: string;
scheme: Scheme;
host: string;
port: string;
hostStatus: "error" | "";
portStatus: "error" | "";
open: boolean;
proxy: string;
id: string;
scheme: Scheme;
host: string;
port: string;
hostStatus: "error" | "";
portStatus: "error" | "";
open: boolean;
proxy: string;
}
export interface ProxifierProps {}
export const Proxifier: React.FC<ProxifierProps> = () => {
const [proxyList, setProxyList] = useState<ProxyConfig[]>(() => {
const storageProxyList = localStorage.getItem("yakit-proxy-list") || "[]";
return JSON.parse(storageProxyList);
});
const [proxyList, setProxyList] = useState<ProxyConfig[]>(() => {
const storageProxyList = localStorage.getItem("yakit-proxy-list") || "[]";
return JSON.parse(storageProxyList);
});
useEffect(() => {
localStorage.setItem("yakit-proxy-list", JSON.stringify(proxyList));
}, [proxyList]);
useEffect(() => {
localStorage.setItem("yakit-proxy-list", JSON.stringify(proxyList));
}, [proxyList]);
const addNewProxyListItem = (
scheme: Scheme,
host: string,
port: string,
open: boolean,
proxy: string
) => {
const proxyItem: ProxyConfig = {
id: Math.random() + "",
scheme: scheme as Scheme,
host: host,
port: port,
hostStatus: "",
portStatus: "",
open: open,
proxy: proxy,
const addNewProxyListItem = (
scheme: Scheme,
host: string,
port: string,
open: boolean,
proxy: string
) => {
const proxyItem: ProxyConfig = {
id: Math.random() + "",
scheme: scheme as Scheme,
host: host,
port: port,
hostStatus: "",
portStatus: "",
open: open,
proxy: proxy,
};
return proxyItem;
};
return proxyItem;
};
const parseUrl = (url: string) => {
const regex = /^(.*?):\/\/(.*?):(\d+)/;
const match = url.match(regex);
if (match) {
const scheme = match[1];
const host = match[2];
const port = match[3];
return {
scheme,
host,
port,
};
} else {
return null; // 不匹配格式
}
};
useEffect(() => {
wsc.updateProxyStatus();
wsc.onProxyStatusMessage((msg) => {
if (msg.proxy === undefined || msg.enable === undefined) {
return;
}
if (msg.proxy === "" || msg.enable === false) {
if (proxyList.some((i) => i.open)) {
const copyProxyList = [...proxyList];
copyProxyList.forEach((i) => {
i.open = false;
});
setProxyList(copyProxyList);
}
return;
}
if (msg.proxy && msg.enable) {
const copyProxyList = [...proxyList];
let newProxyItem: ProxyConfig = undefined;
if (!copyProxyList.length) {
const urlObj = parseUrl(msg.proxy);
if (urlObj) {
newProxyItem = addNewProxyListItem(
urlObj.scheme as Scheme,
urlObj.host,
urlObj.port,
true,
msg.proxy
);
}
const parseUrl = (url: string) => {
const regex = /^(.*?):\/\/(.*?):(\d+)/;
const match = url.match(regex);
if (match) {
const scheme = match[1];
const host = match[2];
const port = match[3];
return {
scheme,
host,
port,
};
} else {
const proxyExist = copyProxyList.some((i) => i.proxy === msg.proxy);
if (proxyExist) {
const proxyOpen = copyProxyList.some(
(i) => i.open && i.proxy === msg.proxy
);
if (!proxyOpen) {
copyProxyList.forEach((i) => {
i.open = false;
});
for (let i = 0; i < copyProxyList.length; i++) {
if (copyProxyList[i].proxy === msg.proxy) {
copyProxyList[i].open = true;
break;
}
}
}
} else {
copyProxyList.forEach((i) => {
i.open = false;
});
const urlObj = parseUrl(msg.proxy);
if (urlObj) {
newProxyItem = addNewProxyListItem(
urlObj.scheme as Scheme,
urlObj.host,
urlObj.port,
true,
msg.proxy
);
}
}
return null; // 不匹配格式
}
};
if (newProxyItem) {
copyProxyList.unshift(newProxyItem);
useEffect(() => {
wsc.updateProxyStatus();
wsc.onProxyStatusMessage((msg) => {
console.log("msg", msg)
if (msg.proxy === undefined || msg.enable === undefined) {
return;
}
if (msg.proxy === "" || msg.enable === false) {
if (proxyList.some((i) => i.open)) {
const copyProxyList = [...proxyList];
copyProxyList.forEach((i) => {
i.open = false;
});
setProxyList(copyProxyList);
}
return;
}
if (msg.proxy && msg.enable) {
const copyProxyList = [...proxyList];
let newProxyItem: ProxyConfig = undefined;
if (!copyProxyList.length) {
const urlObj = parseUrl(msg.proxy);
if (urlObj) {
newProxyItem = addNewProxyListItem(
urlObj.scheme as Scheme,
urlObj.host,
urlObj.port,
true,
msg.proxy
);
}
} else {
const proxyExist = copyProxyList.some((i) => i.proxy === msg.proxy);
if (proxyExist) {
const proxyOpen = copyProxyList.some(
(i) => i.open && i.proxy === msg.proxy
);
if (!proxyOpen) {
copyProxyList.forEach((i) => {
i.open = false;
});
for (let i = 0; i < copyProxyList.length; i++) {
if (copyProxyList[i].proxy === msg.proxy) {
copyProxyList[i].open = true;
break;
}
}
}
} else {
copyProxyList.forEach((i) => {
i.open = false;
});
const urlObj = parseUrl(msg.proxy);
if (urlObj) {
newProxyItem = addNewProxyListItem(
urlObj.scheme as Scheme,
urlObj.host,
urlObj.port,
true,
msg.proxy
);
}
}
}
if (newProxyItem) {
copyProxyList.unshift(newProxyItem);
}
setProxyList(copyProxyList);
}
});
}, []);
const hostOnchange = (value: string, id: string) => {
const ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
const domainPattern = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const copyProxyList = structuredClone(proxyList);
if (value === "" || ipPattern.test(value) || domainPattern.test(value)) {
copyProxyList.forEach((i) => {
if (i.id === id) {
i.hostStatus = "";
i.host = value;
i.proxy = i.scheme + "://" + value + ":" + i.port;
}
});
} else {
copyProxyList.forEach((i) => {
if (i.id === id) {
i.hostStatus = "error";
i.host = value;
i.proxy = i.scheme + "://" + value + ":" + i.port;
}
});
}
setProxyList(copyProxyList);
}
});
}, []);
};
const hostOnchange = (value: string, id: string) => {
const ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
const domainPattern = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const copyProxyList = structuredClone(proxyList);
if (value === "" || ipPattern.test(value) || domainPattern.test(value)) {
copyProxyList.forEach((i) => {
if (i.id === id) {
i.hostStatus = "";
i.host = value;
i.proxy = i.scheme + "://" + value + ":" + i.port;
}
});
} else {
copyProxyList.forEach((i) => {
if (i.id === id) {
i.hostStatus = "error";
i.host = value;
i.proxy = i.scheme + "://" + value + ":" + i.port;
}
});
}
setProxyList(copyProxyList);
};
const portOnchange = (value: string, id: string) => {
const portNumber = parseInt(value, 10);
const copyProxyList = structuredClone(proxyList);
if (
value === "" ||
(/^\d+$/.test(value) && portNumber >= 0 && portNumber <= 65535)
) {
copyProxyList.forEach((i) => {
if (i.id === id) {
i.portStatus = "";
const port = value === "" ? "" : portNumber + "";
i.port = port;
i.proxy = i.scheme + "://" + i.host + ":" + port;
}
});
} else {
copyProxyList.forEach((i) => {
if (i.id === id) {
i.portStatus = "error";
i.port = value;
i.proxy = i.scheme + "://" + i.host + ":" + value;
}
});
}
setProxyList(copyProxyList);
};
return (
<div className="Prox">
<div className="Prox-title-wrap">
<div className="Prox-title-wrap-left">
<span className="prox-title"></span>
<span className="prox-number">{proxyList.length}</span>
</div>
<div
className="Prox-title-wrap-right"
onClick={() => {
setProxyList([
...proxyList,
addNewProxyListItem("http", "", "", false, "http://"),
]);
}}
>
<span className="Prox-add-text"></span>
<PlusSmIcon className="Prox-add-icon" />
</div>
</div>
<div className="Prox-list-wrap">
{proxyList.length ? (
proxyList.map((item) => (
<div className="Prox-list-item-wrap" key={item.id}>
<Space className="Prox-list-item-space">
<Space.Compact>
<Select
value={item.scheme}
style={{ width: 88 }}
disabled={item.open}
onChange={(value, option) => {
const copyProxyList = structuredClone(proxyList);
copyProxyList.forEach((i) => {
if (i.id === item.id) {
i.scheme = value;
i.proxy = value + "://" + i.host + ":" + i.port;
}
});
setProxyList(copyProxyList);
}}
>
<Select.Option value="http">HTTP</Select.Option>
<Select.Option value="socks5">Socks5</Select.Option>
</Select>
<Input
value={item.host}
style={{ width: 136 }}
disabled={item.open}
status={item.hostStatus}
onChange={(e) => hostOnchange(e.target.value, item.id)}
/>
<Input
value={item.port}
style={{ width: 64 }}
disabled={item.open}
status={item.portStatus}
onChange={(e) => portOnchange(e.target.value, item.id)}
/>
</Space.Compact>
</Space>
{!item.open && (
<TrashIcon
className="proxy-list-del-icon"
onClick={() => {
setProxyList(proxyList.filter((i) => i.id !== item.id));
}}
/>
)}
<Switch
checkedChildren="启"
unCheckedChildren="停"
value={item.open}
disabled={
item.hostStatus === "error" ||
item.portStatus === "error" ||
item.host === "" ||
item.port === ""
const portOnchange = (value: string, id: string) => {
const portNumber = parseInt(value, 10);
const copyProxyList = structuredClone(proxyList);
if (
value === "" ||
(/^\d+$/.test(value) && portNumber >= 0 && portNumber <= 65535)
) {
copyProxyList.forEach((i) => {
if (i.id === id) {
i.portStatus = "";
const port = value === "" ? "" : portNumber + "";
i.port = port;
i.proxy = i.scheme + "://" + i.host + ":" + port;
}
onChange={(checked: boolean) => {
wsc.clearProxy();
const copyProxyList = structuredClone(proxyList);
copyProxyList.forEach((i) => {
if (i.id === item.id) {
i.open = checked;
if (checked) {
wsc.setProxy(item.scheme, item.host, Number(item.port));
}
} else {
i.open = false;
}
});
setProxyList(copyProxyList);
}}
/>
});
} else {
copyProxyList.forEach((i) => {
if (i.id === id) {
i.portStatus = "error";
i.port = value;
i.proxy = i.scheme + "://" + i.host + ":" + value;
}
});
}
setProxyList(copyProxyList);
};
return (
<div className="Prox">
<div className="Prox-title-wrap">
<div className="Prox-title-wrap-left">
<span className="prox-title"></span>
<span className="prox-number">{proxyList.length}</span>
</div>
<div
className="Prox-title-wrap-right"
onClick={() => {
setProxyList([
...proxyList,
addNewProxyListItem("http", "", "", false, "http://"),
]);
}}
>
<span className="Prox-add-text"></span>
<PlusSmIcon className="Prox-add-icon"/>
</div>
</div>
))
) : (
<div
className="add-list"
onClick={() => {
setProxyList([
addNewProxyListItem(
"http",
"127.0.0.1",
"8083",
false,
"http://127.0.0.1:8083"
),
]);
}}
>
<PlusSmIcon className="add-list-icon" />
<span className="add-list-text"></span>
</div>
)}
</div>
</div>
);
<div className="Prox-list-wrap">
{proxyList.length ? (
proxyList.map((item) => (
<div className="Prox-list-item-wrap" key={item.id}>
<Space className="Prox-list-item-space">
<Space.Compact>
<Select
value={item.scheme}
style={{width: 88}}
disabled={item.open}
onChange={(value, option) => {
const copyProxyList = structuredClone(proxyList);
copyProxyList.forEach((i) => {
if (i.id === item.id) {
i.scheme = value;
i.proxy = value + "://" + i.host + ":" + i.port;
}
});
setProxyList(copyProxyList);
}}
>
<Select.Option value="http">HTTP</Select.Option>
<Select.Option value="socks5">Socks5</Select.Option>
</Select>
<Input
value={item.host}
style={{width: 136}}
disabled={item.open}
status={item.hostStatus}
onChange={(e) => hostOnchange(e.target.value, item.id)}
/>
<Input
value={item.port}
style={{width: 64}}
disabled={item.open}
status={item.portStatus}
onChange={(e) => portOnchange(e.target.value, item.id)}
/>
</Space.Compact>
</Space>
{!item.open && (
<TrashIcon
className="proxy-list-del-icon"
onClick={() => {
setProxyList(proxyList.filter((i) => i.id !== item.id));
}}
/>
)}
<Switch
checkedChildren="启"
unCheckedChildren="停"
value={item.open}
disabled={
item.hostStatus === "error" ||
item.portStatus === "error" ||
item.host === "" ||
item.port === ""
}
onChange={(checked: boolean) => {
wsc.clearProxy();
const copyProxyList = structuredClone(proxyList);
copyProxyList.forEach((i) => {
if (i.id === item.id) {
i.open = checked;
if (checked) {
wsc.setProxy(item.scheme, item.host, Number(item.port));
}
} else {
i.open = false;
}
});
setProxyList(copyProxyList);
}}
/>
</div>
))
) : (
<div
className="add-list"
onClick={() => {
setProxyList([
addNewProxyListItem(
"http",
"127.0.0.1",
"8083",
false,
"http://127.0.0.1:8083"
),
]);
}}
>
<PlusSmIcon className="add-list-icon"/>
<span className="add-list-text"></span>
</div>
)}
</div>
</div>
);
};
+235
View File
@@ -0,0 +1,235 @@
.proxy-container {
min-width: 200px;
background: white;
}
.proxy-menu {
border: none !important;
box-shadow: none !important;
}
.menu-item {
height: 40px !important;
line-height: 40px !important;
margin: 0 !important;
padding: 0 16px !important;
}
.menu-item .anticon {
font-size: 16px;
color: var(--yakit-primary);
margin-right: 8px;
}
.menu-item-label {
font-size: 14px;
color: #333;
}
.menu-item:hover {
background-color: var(--yakit-primary-5) !important;
}
.menu-item:hover .anticon,
.menu-item:hover .menu-item-label {
color: var(--yakit-primary) !important;
}
/* 选中状态 */
.menu-item.ant-menu-item-selected {
background-color: var(--yakit-primary) !important;
}
.menu-item.ant-menu-item-selected .anticon,
.menu-item.ant-menu-item-selected .menu-item-label {
color: white !important;
}
.menu-item.ant-menu-item-selected:hover {
background-color: var(--yakit-primary-hover) !important;
}
/* 分隔线 */
.ant-menu-item-divider {
margin: 4px 0 !important;
border-color: #EAECF3 !important;
}
/* 设置选项 */
.menu-item-setting {
border-top: 1px solid #EAECF3;
margin-top: 4px !important;
}
.menu-item-setting .anticon {
color: #666;
}
.menu-item-setting:hover {
background-color: var(--yakit-primary-5) !important;
}
.menu-item-setting:hover .anticon,
.menu-item-setting:hover .menu-item-label {
color: var(--yakit-primary) !important;
}
/* 调整图标大小和对齐 */
.anticon {
font-size: 16px;
}
/* 添加以下样式来确保下拉菜单显示在正确的位置 */
.ant-dropdown {
position: absolute !important;
top: 100% !important;
left: 0 !important;
width: 100% !important;
min-width: 200px !important;
}
.dropdown-content {
background: white;
border-radius: 4px;
box-shadow: 0 3px 6px -4px rgba(0,0,0,0.12),
0 6px 16px 0 rgba(0,0,0,0.08),
0 9px 28px 8px rgba(0,0,0,0.05);
padding: 4px;
}
/* 确保容器不会限制弹出层 */
.proxy-container {
min-width: 200px;
background: white;
border-radius: 4px;
position: static;
}
/* 添加这个样式来确保下拉菜单显示在正确的位置 */
body {
position: relative;
}
.ant-menu {
border: none !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
padding: 4px 0 !important;
width: 180px !important;
background: white !important;
}
.ant-menu-item {
height: 28px !important;
line-height: 28px !important;
margin: 0 !important;
padding: 0 16px !important;
}
.ant-menu-item:hover {
background-color: #f5f5f5 !important;
}
.menu-icon {
margin-right: 8px;
display: inline-flex;
align-items: center;
}
.menu-item-selected {
background-color: #e6f7ff !important;
}
.ant-menu-item-divider {
margin: 4px 0 !important;
height: 1px !important;
background-color: #f0f0f0 !important;
}
.ant-menu-item:last-child {
margin-top: 4px !important;
border-top: 1px solid #f0f0f0;
}
/* 移除多余的样式 */
.ant-menu-root {
background: transparent !important;
}
/* 调整整体容器大小 */
.ant-menu-root {
width: 180px !important;
min-height: auto !important;
}
/* 添加代理按钮样式 */
.menu-item-add {
color: #666 !important;
}
.menu-item-add:hover {
background-color: #f5f5f5 !important;
color: var(--yakit-primary) !important;
}
.menu-item-add .anticon {
color: #666;
}
.menu-item-add:hover .anticon {
color: var(--yakit-primary);
}
.menu-loading {
pointer-events: none;
opacity: 0.7;
}
.menu-item-loading {
transition: all 0.3s ease;
}
.menu-item-selected {
transition: all 0.3s ease;
background-color: var(--yakit-primary-5) !important;
}
/* 添加过渡效果 */
.ant-menu-item {
transition: all 0.3s ease !important;
}
.ant-menu-item .menu-icon {
transition: color 0.3s ease;
}
.proxy-switch-container {
position: relative;
width: 180px;
overflow: hidden;
}
.panel-watermark {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
opacity: 0.03;
pointer-events: none;
object-fit: contain;
object-position: right bottom;
z-index: 0;
}
/* 确保菜单项在水印上层 */
.ant-menu-item {
position: relative;
z-index: 1;
background: transparent !important;
}
/* 确保分割线在水印上层 */
.ant-menu-item-divider {
position: relative;
z-index: 1;
}
+307
View File
@@ -0,0 +1,307 @@
import React, {useEffect, useState} from "react";
import {Menu} from "antd";
import {GlobalOutlined, DisconnectOutlined, SettingOutlined, EditOutlined, PlusOutlined} from "@ant-design/icons";
import {ProxyActionType} from '@/types/action';
import "./index.css";
import type { MenuProps } from 'antd';
import type { ProxyConfig } from '@/types/proxy';
// 添加 YAK 图标 URL 常量
const YAK_ICON_URL = chrome.runtime.getURL('/images/yak.svg');
// 固定的代理模式
const FIXED_MODES = [
{
key: 'direct',
name: '[直接连接]',
icon: <DisconnectOutlined />,
color: '#666',
config: {
id: 'direct',
name: '[直接连接]',
proxyType: 'direct',
enabled: false
}
},
{
key: 'system',
name: '[系统代理]',
icon: <SettingOutlined />,
color: '#666',
config: {
id: 'system',
name: '[系统代理]',
proxyType: 'system',
enabled: false
}
}
];
interface CustomProxy {
key: string;
name: string;
color: string;
config: ProxyConfig;
enabled?: boolean;
}
interface ProxySwitchProps {
proxyConfigs: ProxyConfig[];
currentProxy: ProxyConfig | null;
onProxyChange: (config: ProxyConfig) => void;
}
export const ProxySwitch: React.FC<ProxySwitchProps> = () => {
const [initialized, setInitialized] = useState<boolean>(false);
const [currentMode, setCurrentMode] = useState<string>('');
const [customProxies, setCustomProxies] = useState<CustomProxy[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
// 修改存储变化监听
useEffect(() => {
const handleMessage = (message: any) => {
if (message.action === 'PROXY_CONFIGS_UPDATED' && message.source !== 'proxy_switch') {
loadCustomProxies();
}
};
chrome.runtime.onMessage.addListener(handleMessage);
return () => {
chrome.runtime.onMessage.removeListener(handleMessage);
};
}, []);
useEffect(() => {
const init = async () => {
// 修改初始化逻辑,避免并行请求
await loadProxyStatus();
await loadCustomProxies();
setInitialized(true);
};
init();
}, []);
const loadProxyStatus = async () => {
try {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.GET_PROXY_STATUS
});
if (!response) {
console.log('No response from background script');
return;
}
if (response.success) {
const activeMode = response.data.mode;
if (FIXED_MODES.some(mode => mode.key === activeMode)) {
setCurrentMode(activeMode);
}
}
} catch (error) {
console.error('Error loading proxy status:', error);
setCurrentMode('direct');
}
};
const loadCustomProxies = async () => {
try {
const DB_NAME = 'yaklang_extension';
const STORE_NAME = 'proxy_configs';
// 打开数据库
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
// 从数据库读取代理配置
const configs = await new Promise<ProxyConfig[]>((resolve, reject) => {
try {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result || []);
} catch (error) {
reject(error);
}
});
// 处理代理配置
const proxies = configs
.filter((proxy: ProxyConfig) => !FIXED_MODES.some(mode => mode.key === proxy.id))
.map((proxy: ProxyConfig): CustomProxy => ({
key: proxy.id,
name: proxy.name,
color: '#1890ff',
config: proxy,
enabled: proxy.enabled
}));
setCustomProxies(proxies);
const enabledProxy = configs.find((proxy: ProxyConfig) => proxy.enabled);
if (enabledProxy) {
setCurrentMode(enabledProxy.id);
}
} catch (error) {
console.error('Error loading custom proxies:', error);
setCustomProxies([]);
}
};
const handleModeChange = async (mode: string) => {
if (mode === 'setting' || mode === 'add') {
if (mode === 'setting') {
await chrome.runtime.openOptionsPage?.();
}
if (mode === 'add') {
try {
const [activeTab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
const optionsUrl = chrome.runtime.getURL('/proxy/options.html');
if (activeTab?.url === optionsUrl) {
chrome.tabs.sendMessage(activeTab.id!, {
action: 'TRIGGER_ADD_PROXY'
});
} else {
const tab = await chrome.tabs.create({
url: optionsUrl
});
const listener = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => {
if (tabId === tab.id && changeInfo.status === 'complete') {
chrome.tabs.onUpdated.removeListener(listener);
chrome.tabs.sendMessage(tab.id!, {
action: 'TRIGGER_ADD_PROXY'
});
}
};
chrome.tabs.onUpdated.addListener(listener);
}
} catch (error) {
console.error('Failed to get current tab:', error);
}
}
return;
}
try {
setIsLoading(true);
const fixedMode = FIXED_MODES.find(fixed => fixed.key === mode);
const customProxy = customProxies.find(proxy => proxy.key === mode);
const config = fixedMode?.config || customProxy?.config;
if (!config) {
console.error('No config found for mode:', mode);
return;
}
// 立即更新UI状态
setCurrentMode(mode);
if (customProxy) {
setCustomProxies(prev => prev.map(p => ({
...p,
enabled: p.key === mode
})));
}
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.SET_PROXY_CONFIG,
config,
// 添加一个标志,表示这是从 ProxySwitch 发起的更改
source: 'proxy_switch'
});
if (response?.success === false) {
throw new Error(response.error || '设置代理失败');
}
// 不需要重新加载,因为我们已经更新了本地状态
} catch (error) {
console.error('Error applying proxy config:', error);
// 发生错误时才重新加载以确保状态正确
await loadCustomProxies();
throw error;
} finally {
setIsLoading(false);
}
};
const menuItems: MenuProps['items'] = [
...FIXED_MODES.map(mode => ({
key: mode.key,
icon: <span className="menu-icon" style={{ color: mode.color }}>{mode.icon}</span>,
label: mode.name,
className: `${currentMode === mode.key ? 'menu-item-selected' : ''} ${isLoading ? 'menu-item-loading' : ''}`,
title: mode.name.replace(/[\[\]]/g, '')
})),
{ type: 'divider' },
...customProxies.map(proxy => ({
key: proxy.key,
icon: <span className="menu-icon" style={{ color: proxy.color }}>
{proxy.config.proxyType === 'pac_script' ? '📜' : <GlobalOutlined />}
</span>,
label: <span style={{
color: currentMode === proxy.key ? 'var(--yakit-primary)' : 'inherit',
opacity: isLoading ? 0.7 : 1
}}>{proxy.name}</span>,
className: `${currentMode === proxy.key ? 'menu-item-selected' : ''} ${isLoading ? 'menu-item-loading' : ''}`,
title: proxy.config.scheme
? `${proxy.config.scheme.toUpperCase()} ${proxy.config.host}:${proxy.config.port}`
: `${proxy.config.host}:${proxy.config.port}`
})),
{
key: 'add',
icon: <PlusOutlined />,
label: '添加代理...',
className: 'menu-item-add'
},
{ type: 'divider' },
{
key: 'setting',
icon: <EditOutlined />,
label: '选项'
}
];
return initialized ? (
<div className="proxy-switch-container" style={{ position: 'relative' }}>
<img
src={YAK_ICON_URL}
className="panel-watermark"
alt=""
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
height: '100%',
opacity: 0.1,
backgroundColor: '#fff7e6',
pointerEvents: 'none',
objectFit: 'contain',
objectPosition: 'right bottom',
zIndex: 0
}}
/>
<Menu
items={menuItems}
selectedKeys={[currentMode]}
onClick={({ key }) => !isLoading && handleModeChange(key)}
style={{ width: 180, position: 'relative', zIndex: 1, background: 'transparent' }}
className={isLoading ? 'menu-loading' : ''}
/>
</div>
) : (
<div style={{ width: 180, height: 100, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<span>...</span>
</div>
);
};
+7 -19
View File
@@ -4,11 +4,13 @@ html:root {
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
*,
@@ -17,23 +19,9 @@ body {
box-sizing: border-box;
}
::-webkit-scrollbar {
/*滚动条整体样式*/
width: 8px;
/*高宽分别对应横竖滚动条的尺寸*/
height: 8px;
}
::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 10px;
box-shadow: inset 0 0 5px rgba(193, 193, 193);
background: #c1c1c1;
}
::-webkit-scrollbar-track {
/*滚动条里面轨道*/
box-shadow: inset 0 0 5px rgba(193, 193, 193);
border-radius: 10px;
background: #ededed;
#root {
width: 180px;
height: fit-content;
background: white;
overflow: hidden;
}
-7
View File
@@ -1,7 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
// 获取根元素
const container = document.getElementById('root');
if (!container) throw new Error('Failed to find the root element');
// 创建根
const root = createRoot(container);
// 渲染应用,移除 StrictMode
root.render(<App />);
+9
View File
@@ -1,6 +1,8 @@
export namespace wsc {
export enum ActionType {
CONNECT = 'connect',
SEND_MESSAGE = 'send_message',
DISCONNECT = 'disconnect',
STATUS = 'status',
PROXY_STATUS = 'proxy_status',
@@ -19,6 +21,13 @@ export namespace wsc {
});
}
export function sendMessage(message: any) {
chrome.runtime.sendMessage({
action: ActionType.SEND_MESSAGE,
message: message,
});
}
export function disconnect() {
chrome.runtime.sendMessage({
action: ActionType.DISCONNECT,
@@ -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,65 @@
/* 表格样式 */
.proxy-table {
background: white;
border-radius: 8px;
}
/* 表头样式 */
.proxy-table .ant-table-thead > tr > th {
background: white !important;
color: #333;
font-weight: 500;
border-bottom: 1px solid var(--border-color);
padding: 12px 16px;
}
/* 斑马纹样式 */
.proxy-table .ant-table-tbody > tr:nth-child(even) {
background-color: #fafafa;
}
/* 单元格样式 */
.proxy-table .ant-table-tbody > tr > td {
border-bottom: 1px solid var(--border-color);
padding: 12px 16px;
}
/* 操作列图标样式 */
.action-icon {
font-size: 16px;
cursor: pointer;
color: #666;
transition: all 0.3s;
}
.action-icon:hover {
color: var(--yakit-primary);
transform: scale(1.1);
}
.action-icon.enabled {
color: var(--yakit-primary);
}
.action-icon.delete:hover {
color: #ff4d4f;
}
/* 间距调整 */
.ant-space-middle {
gap: 16px !important;
}
/* 添加按钮样式 */
.add-proxy-btn {
background-color: var(--yakit-primary);
color: white;
border: none;
border-radius: 4px;
margin-bottom: 16px;
}
.add-proxy-btn:hover {
background-color: var(--yakit-primary-hover) !important;
color: white !important;
}
@@ -0,0 +1,461 @@
import React, { useRef, useState, useEffect, useCallback } from 'react';
import { Card, Input, Space, Button, Select, InputNumber, Form, Table, Tooltip, Popover, Modal, message } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { DeleteOutlined, PlusOutlined, EditOutlined, CheckOutlined } from '@ant-design/icons';
import { ProxyConfig } from '@/types/proxy';
import './index.css';
import punycode from 'punycode';
interface ProxySettingsProps {
proxyConfigs: ProxyConfig[];
onAdd: (config: ProxyConfig) => void;
onChange: (configId: string, field: keyof ProxyConfig | 'config', value: any) => void;
onDelete: (configId: string) => void;
onApply: (configId: string) => Promise<void>;
onClear: (configId: string) => Promise<void>;
}
// 修改 EditFormData 接口
interface EditFormData {
name: string;
proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect";
scheme?: "http" | "https" | "socks4" | "socks5";
host?: string;
port?: number;
pacScript?: string;
bypassList?: string;
matchList?: string; // 仅用于 UI 编辑
proxyServer?: string; // 添加 proxyServer 字段,用于 PAC 脚本模式选择代理服务器
}
// 或者更好的方式是创建一个专门的类型
type ProxyConfigField = keyof ProxyConfig | 'config';
export const ProxySettings: React.FC<ProxySettingsProps> = ({
proxyConfigs,
onAdd,
onChange,
onDelete,
onApply,
onClear
}) => {
const [editingConfig, setEditingConfig] = useState<ProxyConfig | null>(null);
const [editModalVisible, setEditModalVisible] = useState(false);
const [form] = Form.useForm<EditFormData>();
// 添加 useEffect 来监听表单值变化
useEffect(() => {
if (editModalVisible && editingConfig) {
form.setFieldsValue({
name: editingConfig.name,
proxyType: editingConfig.proxyType,
scheme: editingConfig.scheme,
host: editingConfig.host,
port: editingConfig.port,
bypassList: editingConfig.bypassList?.join('\n') || '',
matchList: editingConfig.matchList?.join('\n') || '',
proxyServer: editingConfig.host && editingConfig.port
? `${editingConfig.host}:${editingConfig.port}`
: undefined
});
}
}, [editModalVisible, editingConfig, form]);
// 处理添加按钮点击
const handleAdd = useCallback(() => {
setEditingConfig({
id: Date.now().toString(),
name: '',
proxyType: 'fixed_servers',
scheme: 'http' as "http" | "https" | "socks4" | "socks5",
host: '127.0.0.1',
port: 8080,
enabled: false
});
setEditModalVisible(true);
}, []);
// 处理编辑按钮点击
const handleEdit = (record: ProxyConfig) => {
setEditingConfig(record);
setEditModalVisible(true);
};
// 添加一个函数来获取可用的代理服务器列表
const getAvailableProxies = (configs: ProxyConfig[]) => {
return configs
.filter(config => config.proxyType === 'fixed_servers')
.map(config => ({
label: `${config.name} (${config.scheme}://${config.host}:${config.port})`,
value: `${config.host}:${config.port}`,
config
}));
};
// 处理编辑保存
const handleEditSave = async () => {
try {
const values = await form.validateFields();
if (editingConfig) {
if (editingConfig.enabled) {
await onClear(editingConfig.id);
}
let updatedConfig: ProxyConfig;
if (values.proxyType === 'fixed_servers') {
// 处理固定代理服务器模式
const bypassList = values.bypassList
? values.bypassList.split('\n').map(line => line.trim()).filter(line => line.length > 0)
: ["localhost", "127.0.0.1"];
updatedConfig = {
id: editingConfig.id,
name: values.name,
enabled: editingConfig.enabled,
proxyType: 'fixed_servers',
scheme: values.scheme,
host: values.host,
port: values.port,
bypassList,
};
} else if (values.proxyType === 'pac_script') {
const domains = values.matchList
? values.matchList.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.map(domain => {
try {
// 如果域名包含非 ASCII 字符,转换为 Punycode
if (/[^\x00-\x7F]/.test(domain)) {
if (domain.startsWith('*.')) {
const suffix = domain.substring(2);
return '*.' + suffix.split('.').map(part => {
return /[^\x00-\x7F]/.test(part) ? 'xn--' + punycode.encode(part) : part;
}).join('.');
} else {
return domain.split('.').map(part => {
return /[^\x00-\x7F]/.test(part) ? 'xn--' + punycode.encode(part) : part;
}).join('.');
}
}
return domain;
} catch (error) {
console.error('Error encoding domain:', domain, error);
return domain;
}
})
: [];
// 从选择的代理服务器中获取配置
const [host, port] = values.proxyServer.split(':');
// 生成 PAC 脚本
const pacScriptContent = `
function FindProxyForURL(url, host) {
// Convert host to lowercase for case-insensitive matching
host = host.toLowerCase();
// Define domain patterns
var domains = ${JSON.stringify(domains)};
// Check each domain pattern
for (var i = 0; i < domains.length; i++) {
var pattern = domains[i].toLowerCase();
if (pattern.startsWith('*.')) {
var suffix = pattern.substring(2);
if (host === suffix || host.endsWith('.' + suffix)) {
return 'PROXY ${host}:${port}';
}
} else if (host === pattern) {
return 'PROXY ${host}:${port}';
}
}
return 'DIRECT';
}`;
updatedConfig = {
id: editingConfig.id,
name: values.name,
enabled: editingConfig.enabled,
proxyType: 'pac_script',
mode: 'pac_script',
// 保存代理服务器信息
host,
port: parseInt(port),
// 保存匹配域名列表
matchList: domains,
pacScript: {
data: pacScriptContent,
mandatory: true
}
};
} else {
// 处理其他模式
updatedConfig = {
id: editingConfig.id,
name: values.name,
enabled: editingConfig.enabled,
proxyType: values.proxyType,
bypassList: [], // 其他模式下设置为空数组
};
}
if (!proxyConfigs.find(config => config.id === editingConfig.id)) {
await onAdd(updatedConfig);
} else {
await onChange(editingConfig.id, 'config', updatedConfig);
}
setEditModalVisible(false);
setEditingConfig(null);
form.resetFields();
}
} catch (error) {
console.error('Validate Failed:', error);
message.error('保存失败,请检查表单');
}
};
// 处理模态框关闭
const handleModalClose = () => {
form.resetFields();
setEditModalVisible(false);
setEditingConfig(null);
};
const columns: ColumnsType<ProxyConfig> = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (text: string) => text
},
{
title: '类型',
dataIndex: 'proxyType',
key: 'proxyType',
render: (text: string) => {
const typeMap = {
direct: '直接连接',
fixed_servers: '代理服务器',
pac_script: 'PAC 脚本'
};
return typeMap[text as keyof typeof typeMap] || text;
}
},
{
title: '协议',
dataIndex: 'scheme',
key: 'scheme'
},
{
title: '主机',
dataIndex: 'host',
key: 'host'
},
{
title: '端口',
dataIndex: 'port',
key: 'port'
},
{
title: '操作',
key: 'action',
width: 120,
render: (_, record: ProxyConfig) => (
<Space size="middle">
<CheckOutlined
className={`action-icon ${record.enabled ? 'enabled' : ''}`}
onClick={async () => {
if (record.enabled) {
await onClear(record.id);
} else {
await onApply(record.id);
}
}}
/>
<EditOutlined
className="action-icon"
onClick={() => handleEdit(record)}
/>
{record.id !== 'direct' && (
<DeleteOutlined
className="action-icon delete"
onClick={() => onDelete(record.id)}
/>
)}
</Space>
)
}
];
const buttonRef = useRef(null);
// 过滤掉固定模式的代理
const filteredProxyConfigs = proxyConfigs.filter(
config => !['direct', 'system'].includes(config.id)
);
return (
<div>
<Space style={{ marginBottom: 16, justifyContent: 'flex-end', width: '100%' }}>
<Button
className="add-proxy-btn"
onClick={handleAdd}
icon={<PlusOutlined />}
>
</Button>
</Space>
<Table
className="proxy-table"
dataSource={filteredProxyConfigs}
columns={columns}
rowKey="id"
pagination={false}
bordered={false}
size="middle"
/>
<Modal
title="编辑代理配置"
open={editModalVisible}
onOk={handleEditSave}
onCancel={handleModalClose}
destroyOnClose
>
<Form
form={form}
layout="vertical"
preserve={false}
>
<Form.Item
name="name"
label="名称"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
name="proxyType"
label="类型"
rules={[{ required: true }]}
>
<Select
options={[
// { label: '直接连接', value: 'direct' },
{ label: '代理服务器', value: 'fixed_servers' },
{ label: 'PAC 脚本', value: 'pac_script' }
]}
onChange={(value) => {
// 当类型改变时,清除相关字段
if (value !== 'fixed_servers') {
form.setFieldsValue({
scheme: undefined,
host: undefined,
port: undefined
});
}
}}
/>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}
>
{({ getFieldValue }) => {
const proxyType = getFieldValue('proxyType');
if (proxyType === 'fixed_servers') {
return (
<>
<Form.Item
name="scheme"
label="协议"
rules={[{ required: true, message: '请选择协议' }]}
>
<Select
options={[
{ label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' },
{ label: 'SOCKS4', value: 'socks4' },
{ label: 'SOCKS5', value: 'socks5' }
]}
/>
</Form.Item>
<Form.Item
name="host"
label="主机"
rules={[{ required: true, message: '请输入主机地址' }]}
>
<Input placeholder="127.0.0.1" />
</Form.Item>
<Form.Item
name="port"
label="端口"
rules={[{ required: true, message: '请输入端口号' }]}
>
<InputNumber
min={1}
max={65535}
placeholder="8080"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
name="bypassList"
label="不经过代理的地址"
help="每行一个地址,支持通配符 *"
>
<Input.TextArea
rows={4}
placeholder={`例如:
localhost
127.0.0.1
*.example.com`}
/>
</Form.Item>
</>
);
} else if (proxyType === 'pac_script') {
const availableProxies = getAvailableProxies(proxyConfigs);
return (
<>
<Form.Item
name="matchList"
label="匹配域名"
help="每行一个域名,支持通配符 *"
rules={[{ required: true, message: '请输入至少一个匹配域名' }]}
>
<Input.TextArea
rows={4}
placeholder={`例如:
*.example.com
google.com
github.com`}
/>
</Form.Item>
<Form.Item
name="proxyServer"
label="选择代理服务器"
rules={[{ required: true, message: '请选择代理服务器' }]}
>
<Select
placeholder="选择一个代理服务器"
options={availableProxies}
/>
</Form.Item>
</>
);
}
return null;
}}
</Form.Item>
</Form>
</Modal>
</div>
);
};
@@ -0,0 +1,167 @@
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 messageListener = (message: any) => {
if (message.action === 'PROXY_CONFIGS_UPDATED') {
loadConfigs();
}
};
chrome.runtime.onMessage.addListener(messageListener);
return () => {
chrome.runtime.onMessage.removeListener(messageListener);
};
}, []);
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);
await chrome.runtime.sendMessage({
action: 'PROXY_CONFIGS_UPDATED'
});
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);
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 response = await chrome.runtime.sendMessage({
action: ProxyActionType.SET_PROXY_CONFIG,
config: config
});
if (response.success) {
await chrome.runtime.sendMessage({
action: 'PROXY_STATUS_CHANGED'
});
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
};
};
+111
View File
@@ -0,0 +1,111 @@
.options-page {
height: 100vh;
overflow: auto;
background: #f0f2f5;
}
.options-page .ant-layout-content {
padding: 24px;
}
.proxy-tabs {
background: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.options-page .ant-tabs-nav::before {
border-bottom-color: #f0f0f0;
}
.options-page .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: var(--yakit-primary);
}
.options-page .ant-tabs-ink-bar {
background: var(--yakit-primary);
}
.options-page .ant-card {
margin-bottom: 16px;
border-radius: 4px;
border: 1px solid #f0f0f0;
}
.options-page .ant-card-head {
border-bottom: none;
min-height: 48px;
}
.options-page .ant-card-body {
padding: 16px;
}
.options-page .ant-btn-primary {
background: var(--yakit-primary);
border-color: var(--yakit-primary);
}
.options-page .ant-btn-primary:hover {
background: var(--yakit-primary-hover);
border-color: var(--yakit-primary-hover);
}
.proxy-action-btn.ant-btn-default {
color: rgba(0, 0, 0, 0.65);
border-color: #d9d9d9;
background: #fff;
}
.proxy-action-btn.ant-btn-default:hover {
color: var(--yakit-primary);
border-color: var(--yakit-primary);
}
.proxy-action-btn.ant-btn-primary {
color: #fff;
background: var(--yakit-primary);
border-color: var(--yakit-primary);
}
.proxy-action-btn.ant-btn-primary.ant-btn-dangerous {
background: #ff4d4f;
border-color: #ff4d4f;
}
.proxy-action-btn.ant-btn-primary.ant-btn-dangerous:hover {
background: #ff7875;
border-color: #ff7875;
}
.options-page .ant-table-wrapper {
background: #fff;
padding: 24px;
border-radius: 8px;
}
.options-page .ant-input:focus,
.options-page .ant-input-focused {
border-color: var(--yakit-primary);
box-shadow: 0 0 0 2px var(--yakit-primary-5);
}
.options-page .ant-select-focused .ant-select-selector {
border-color: var(--yakit-primary) !important;
box-shadow: 0 0 0 2px var(--yakit-primary-5) !important;
}
.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;
}
+139
View File
@@ -0,0 +1,139 @@
import React, { useState, useEffect, useRef } from 'react';
import { Layout, Tabs, message } 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';
import { ProxyActionType } from '@/types/action';
const { Content } = Layout;
interface ProxySettingsProps {
proxyConfigs: ProxyConfig[];
onAdd: (config: ProxyConfig) => void;
onChange: (configId: string, field: keyof ProxyConfig | 'config', value: any) => void;
onDelete: (configId: string) => void;
onApply: (configId: string) => Promise<void>;
onClear: (configId: string) => Promise<void>;
}
export const OptionsPage: React.FC = () => {
const {
proxyConfigs,
handleAddProxy,
handleConfigChange: handleConfigChangeHook,
handleDeleteProxy,
handleApplyConfig,
handleClearProxy
} = useProxyConfigs();
const { proxyLogs, handleClearLogs } = useProxyLogs();
const [proxyConfigsState, setProxyConfigs] = useState<ProxyConfig[]>([]);
useEffect(() => {
setProxyConfigs(proxyConfigs);
}, [proxyConfigs]);
// 通知 background 页面已准备就绪
useEffect(() => {
chrome.runtime.sendMessage({ action: 'OPTIONS_PAGE_READY' });
const messageListener = (
message: any,
sender: chrome.runtime.MessageSender,
sendResponse: (response?: any) => void
) => {
if (message.action === 'TRIGGER_ADD_PROXY') {
const proxySettingsElement = document.querySelector('.add-proxy-btn');
if (proxySettingsElement) {
(proxySettingsElement as HTMLElement).click();
}
}
sendResponse();
};
chrome.runtime.onMessage.addListener(messageListener);
return () => {
chrome.runtime.onMessage.removeListener(messageListener);
};
}, []);
const handleAdd = async (config: ProxyConfig) => {
try {
await handleAddProxy(config);
} catch (error) {
console.error('Failed to add proxy:', error);
message.error('添加代理失败');
}
};
const handleConfigChange = async (configId: string, field: keyof ProxyConfig | 'config', value: any) => {
try {
const updatedConfigs = proxyConfigsState.map(config => {
if (config.id === configId) {
if (field === 'config') {
// 如果是整个配置更新
return value;
} else {
// 如果是单个字段更新
return {
...config,
[field]: value
};
}
}
return config;
});
// 更新 IndexedDB
await chrome.runtime.sendMessage({
action: ProxyActionType.UPDATE_PROXY_CONFIG,
configs: updatedConfigs
});
// 更新本地状态
setProxyConfigs(updatedConfigs);
message.success('更新配置成功');
} catch (error) {
console.error('Failed to update config:', error);
message.error('更新配置失败');
}
};
return (
<Layout style={{ height: '100vh' }}>
<Content style={{ padding: '24px' }}>
<Tabs
defaultActiveKey="1"
items={[
{
key: '1',
label: '代理设置',
children: (
<ProxySettings
proxyConfigs={proxyConfigsState}
onAdd={handleAdd}
onChange={handleConfigChange}
onDelete={handleDeleteProxy}
onApply={handleApplyConfig}
onClear={handleClearProxy}
/>
)
},
{
key: '2',
label: '代理日志',
children: (
<ProxyLogs
logs={proxyLogs}
onClearLogs={handleClearLogs}
/>
)
}
]}
/>
</Content>
</Layout>
);
};
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from 'antd';
import { OptionsPage } from './OptionsPage';
import '@/styles/global.css';
const container = document.getElementById('root');
if (!container) throw new Error('Failed to find the root element');
const root = createRoot(container);
root.render(
<App>
<OptionsPage />
</App>
);
+58
View File
@@ -0,0 +1,58 @@
:root {
--yakit-primary: #F28B44;
--yakit-primary-hover: #f4a061;
--yakit-primary-active: #e87633;
--yakit-primary-5: #fff5eb;
--yakit-primary-10: rgba(242, 139, 68, 0.1);
/* 添加其他全局变量 */
--border-color: #f0f0f0;
--text-color: #333;
--icon-color: #666;
/* 菜单相关变量 */
--menu-item-height: 28px;
--menu-padding: 4px;
--menu-width: 180px;
}
.ant-btn-primary {
background-color: var(--yakit-primary) !important;
}
.ant-btn-primary:hover {
background-color: var(--yakit-primary-hover) !important;
}
.ant-btn-primary:active {
background-color: var(--yakit-primary-active) !important;
}
.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background-color: var(--yakit-primary-5) !important;
}
.ant-select-focused .ant-select-selector,
.ant-input-focused,
.ant-input:focus,
.ant-input-number-focused,
.ant-input-number:focus {
border-color: var(--yakit-primary) !important;
box-shadow: 0 0 0 2px var(--yakit-primary-10) !important;
}
.ant-btn:not(.ant-btn-primary):hover {
color: var(--yakit-primary) !important;
border-color: var(--yakit-primary) !important;
}
/* 移除所有滚动条 */
::-webkit-scrollbar {
display: none;
}
/* 确保所有内容都在视口内 */
html, body {
overflow: hidden;
height: fit-content;
}
+24
View File
@@ -0,0 +1,24 @@
// export const ActionType = {
// CONNECT: "CONNECT",
// SEND_MESSAGE: "SEND_MESSAGE",
// DISCONNECT: "DISCONNECT",
// SET_PROXY: "SET_PROXY",
// CLEAR_PROXY: "CLEAR_PROXY",
// PROXY_STATUS: "PROXY_STATUS",
// INJECT_SCRIPT: "INJECT_SCRIPT"
// } as const;
// export type ActionType = typeof ActionType[keyof typeof ActionType];
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",
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];
+10
View File
@@ -0,0 +1,10 @@
declare namespace chrome.storage {
interface StorageChange {
oldValue?: any;
newValue?: any;
}
type StorageChanges = {
[key: string]: StorageChange;
};
}
+19
View File
@@ -0,0 +1,19 @@
export interface StorageChange<T = any> {
oldValue?: T;
newValue?: T;
}
export interface StorageChanges {
[key: string]: StorageChange;
}
export interface ProxyConfig {
host: string;
port: number;
scheme: 'http' | 'https' | 'socks5';
proxyType: 'fixed_servers';
enabled?: boolean;
id?: string;
name?: string;
timestamp?: number;
}
+45
View File
@@ -0,0 +1,45 @@
export interface PacScript {
data?: string;
url?: string;
mandatory?: boolean;
}
export interface ProxyConfig {
id: string;
name: string;
enabled: boolean;
proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect";
mode?: string;
host?: string;
port?: number;
scheme?: "http" | "https" | "socks4" | "socks5";
pacScript?: PacScript;
bypassList?: string[];
matchList?: 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';
}
+13 -3
View File
@@ -10,11 +10,21 @@
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"@*": ["src/*"]
}
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@assets/*": ["./src/assets/*"],
"@network/*": ["./src/network/*"],
"@types/*": ["./src/types/*"]
},
"typeRoots": [
"./node_modules/@types",
"./src/types"
],
"skipLibCheck": true,
"lib": ["dom", "dom.iterable", "esnext"]
},
"include": [
"./src/**/*",
"./src/**/*"
],
"exclude": [
"node_modules"
+30 -10
View File
@@ -6,10 +6,15 @@ const webpack = require('webpack');
module.exports = {
mode: 'development', // 设置模式为开发模式
entry: './src/index.jsx', // 指定入口文件
entry: {
main: './src/index.tsx',
options: './src/pages/options.tsx'
},
output: {
path: path.resolve(__dirname, 'build'), // 输出目录
filename: 'bundle.js', // 输出文件名
filename: '[name].bundle.js', // 输出文件名
publicPath: '/',
clean: true
},
plugins: [
new webpack.DefinePlugin({
@@ -18,16 +23,22 @@ module.exports = {
}),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html'
filename: 'index.html',
chunks: ['main']
}),
new HtmlWebpackPlugin({
template: './public/proxy/options.html',
filename: 'proxy/options.html',
chunks: ['options'],
publicPath: '../'
}),
new CopyWebpackPlugin({
patterns: [
// copy public assets exclude index.html
{
from: path.resolve(__dirname, 'public'),
to: path.resolve(__dirname, 'build'),
globOptions: {
ignore: ['**/index.html']
ignore: ['**/index.html', '**/proxy/options.html']
}
}
]
@@ -37,13 +48,20 @@ module.exports = {
module: {
rules: [
{
test: /\.css$/, // 匹配所有的 css 文件
use: ['style-loader', 'css-loader'] // 对匹配到的文件使用这两个 loader
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.tsx?$/, // 匹配TS和TSX文件
use: 'ts-loader',
exclude: /node_modules/,
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true // 添加这个选项可以加快编译速度
}
}
],
exclude: /node_modules/
},
{
test: /\.(js|jsx)$/, // 匹配JS和JSX文件
@@ -60,9 +78,11 @@ module.exports = {
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'], // 解析扩展(确保能够解析JS和JSX文件)
alias: {
'@': path.resolve(__dirname, './src'),
'@assets': path.resolve(__dirname, './src/assets'),
'@components': path.resolve(__dirname, './src/components'),
'@network': path.resolve(__dirname, './src/network'),
'@types': path.resolve(__dirname, './src/types'),
}
},
devtool: 'inline-source-map', // 生成内联源映射,便于调试
+10666
View File
File diff suppressed because it is too large Load Diff