mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f463076def | ||
|
|
43743d729f | ||
|
|
8801de3441 | ||
|
|
9a63f596b8 | ||
|
|
2253bdffb1 | ||
|
|
322cd6dcd6 | ||
|
|
f0fb3f6cf0 | ||
|
|
5066e9d89a | ||
|
|
35184f4b25 | ||
|
|
1eee8217ec | ||
|
|
1149cfeb4d | ||
|
|
cf10abc3f8 | ||
|
|
86dd6cd40c | ||
|
|
06c053228c | ||
|
|
6f82842c92 | ||
|
|
1e25561fae | ||
|
|
8c30607514 | ||
|
|
692ab7760e | ||
|
|
08796f91c4 | ||
|
|
7c3eb80872 |
+1
-1
@@ -15,7 +15,7 @@ build.pem
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
/2.5.21_0
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
|
||||
+3
-1
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
@@ -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 |
@@ -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
@@ -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
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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(); // 初始化日志存储
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.add-proxy-form {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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/>);
|
||||
@@ -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 />);
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
declare namespace chrome.storage {
|
||||
interface StorageChange {
|
||||
oldValue?: any;
|
||||
newValue?: any;
|
||||
}
|
||||
|
||||
type StorageChanges = {
|
||||
[key: string]: StorageChange;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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', // 生成内联源映射,便于调试
|
||||
|
||||
Reference in New Issue
Block a user