Compare commits

...
Author SHA1 Message Date
go0p 82d1c066cd update version 2025-03-07 14:50:07 +08:00
go0p 2481b22d8e Improve proxy initialization with last used proxy restoration 2025-03-07 14:49:19 +08:00
go0p 6013a7ae4e Bump extension version to 0.0.2 2025-03-05 16:28:24 +08:00
go0p 8742711b68 Improve ProxySwitch menu item visual feedback with selected state indicators 2025-03-05 16:16:46 +08:00
go0p f463076def fix version 2025-03-04 16:14:13 +08:00
go0p 43743d729f Enhance webpack build script with production mode and progress tracking 2025-03-04 16:07:15 +08:00
go0p 8801de3441 update build script to use webpack directly 2025-03-04 16:07:15 +08:00
go0p 9a63f596b8 fix chrome doesn't give a damn about proxy on localhost 2025-03-04 16:07:15 +08:00
go0p 2253bdffb1 fix setting pac 2025-03-04 16:07:15 +08:00
go0p 322cd6dcd6 todo switch 2025-03-04 16:07:15 +08:00
go0p f0fb3f6cf0 fix popup options.html context 2025-03-04 16:07:15 +08:00
go0p 5066e9d89a add inject content ui 2025-03-04 16:07:15 +08:00
go0p 35184f4b25 yakit mitm config 2025-03-04 16:07:14 +08:00
go0p 1eee8217ec ui v0.0.2-beta 2025-03-04 16:07:14 +08:00
go0p 1149cfeb4d ui v0.0.1-beta 2025-03-04 16:07:14 +08:00
go0p cf10abc3f8 ui v0.0.1-beta 2025-03-04 16:07:14 +08:00
go0p 86dd6cd40c fix ui 2025-03-04 16:07:14 +08:00
go0p 06c053228c fix 2025-03-04 16:07:14 +08:00
go0p 6f82842c92 use indexedDB manager proxySwitch 2025-03-04 16:07:14 +08:00
go0p 1e25561fae add proxy switch demo 2025-03-04 16:07:14 +08:00
go0p 8c30607514 add proxy switch demo 2025-03-04 16:07:14 +08:00
go0p 692ab7760e fix inject 2025-03-04 16:07:14 +08:00
go0p 08796f91c4 feat(wsc) brute encrypt demo 2025-03-04 16:07:14 +08:00
go0p 7c3eb80872 feat(wsc) fuzzer demo 2025-03-04 16:07:14 +08:00
go0p 905298cbca Enhance release workflow with OSS upload and version tracking 2025-03-04 16:05:14 +08:00
go0p 41267ec7e2 Update release.yml 2025-03-02 15:31:01 +08:00
go0p e2d63a3bbd Update release.yml 2025-03-02 14:54:15 +08:00
go0p f9cfc1b568 add build action 2025-03-02 14:45:30 +08:00
go0p cb05ab70e3 format 2024-05-15 14:43:04 +08:00
go0p 207e8172f1 eval done && fix findDOMNode warning 2024-05-15 14:43:04 +08:00
go0p 6358fc720d 完善一下 2024-05-15 14:43:04 +08:00
song_xiao_lin 26c3bbc570 del: 删除无用代码 2024-05-15 14:43:04 +08:00
song_xiao_lin 5fbd51a073 del: 删除无用代码 2024-05-15 14:43:04 +08:00
song_xiao_lin 7c1cdd562c fix: 注入js 2024-05-15 14:43:04 +08:00
song_xiao_lin a4840c9c5e fix: 重复监听 2024-05-15 14:43:04 +08:00
go0p e150379c7f 调用active tab js func 2024-05-15 14:43:04 +08:00
go0p 0c8f82b441 fix 2024-05-15 14:43:04 +08:00
go0p fb0a20b065 eval in page context 2024-05-15 14:43:04 +08:00
song_xiao_lin c950ede429 pref 2024-05-15 14:43:04 +08:00
song_xiao_lin 50704b68ac feat: 删掉提示 2024-05-15 14:43:04 +08:00
song_xiao_lin 52a8d315d1 fix: 状态问题 2024-05-15 14:43:04 +08:00
song_xiao_lin ddf036da66 fix: 失败重连问题 2024-05-15 14:43:04 +08:00
song_xiao_lin ea338fe547 fix: 断开连接 2024-05-15 14:43:04 +08:00
song_xiao_lin ab4f587eda fix: 部分问题 2024-05-15 14:43:04 +08:00
song_xiao_lin 289dcdf030 fix: 修复重连问题 2024-05-15 14:43:04 +08:00
go0p ac36e0bfc7 inject script 2024-05-15 14:43:04 +08:00
go0p 8cdd5605bc inject script 2024-05-15 14:43:04 +08:00
go0p f6cf369b3a add inject code 2024-05-15 14:43:04 +08:00
go0p 8e154aae29 feat(wsc): fix 2024-05-15 14:43:04 +08:00
song_xiao_lin 523889014f zancun 2024-05-15 14:43:04 +08:00
song_xiao_lin 42b0e958ba feat: Contro逻辑调整 2024-05-15 14:43:04 +08:00
song_xiao_lin 8d61a5f5e8 style: icon点击样式给主题色 2024-05-15 14:43:04 +08:00
song_xiao_lin 0fddcc959c feat: Contro 2024-05-15 14:43:04 +08:00
song_xiao_lin 7af4124da8 feat: 代理功能 2024-05-15 14:43:04 +08:00
song_xiao_lin 06fff8f92e style: 样式调整 2024-05-15 14:43:04 +08:00
58 changed files with 16535 additions and 14611 deletions
+83
View File
@@ -0,0 +1,83 @@
name: Build and Release
on: workflow_dispatch
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build project
run: yarn build
- name: Get version
id: version
run: |
VERSION=$(jq -r '.version' build/manifest.json)
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "build_time=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
- name: Zip build artifacts
run: |
cd build
zip -r ../extension.zip .
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.version.outputs.version }}
release_name: Release v${{ steps.version.outputs.version }}
body: |
Branch: ${{ github.ref_name }}
Commit: ${{ github.sha }}
Build Time: ${{ steps.version.outputs.build_time }}
draft: false
prerelease: false
- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./extension.zip
asset_name: yakit-chrome-extension-v${{ steps.version.outputs.version }}.zip
asset_content_type: application/zip
- name: Upload Extension To OSS
uses: tvrcgo/upload-to-oss@master
with:
key-id: ${{ secrets.OSS_KEY_ID }}
key-secret: ${{ secrets.OSS_KEY_SECRET }}
region: oss-accelerate
bucket: yaklang
assets: |
extension.zip:/chrome-extension/yakit-chrome-extension-v${{ steps.version.outputs.version }}.zip
- name: Update OSS latest version file
run: echo ${{ steps.version.outputs.version }} > ./extension-version.txt
- name: Upload Version File to OSS
uses: tvrcgo/upload-to-oss@master
with:
key-id: ${{ secrets.OSS_KEY_ID }}
key-secret: ${{ secrets.OSS_KEY_SECRET }}
region: oss-accelerate
bucket: yaklang
assets: |
./extension-version.txt:/chrome-extension/latest-version.txt
+1 -1
View File
@@ -15,7 +15,7 @@ build.pem
# production
/build
/2.5.21_0
# misc
.DS_Store
.env.local
+3
View File
@@ -310,6 +310,9 @@ module.exports = function (webpackEnv) {
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
'@assets': path.resolve(__dirname, '../src/assets'),
'@components': path.resolve(__dirname, '../src/components'),
'@network': path.resolve(__dirname, '../src/network'),
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
+135 -14137
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -9,7 +9,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"ahooks": "^3.7.10",
"antd": "^5.15.3",
"antd": "^5.17.2",
"babel-jest": "^27.4.2",
"babel-plugin-named-asset-import": "^0.3.8",
"babel-preset-react-app": "^10.0.1",
@@ -17,6 +17,7 @@
"browserslist": "^4.18.1",
"camelcase": "^6.2.1",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"classnames": "^2.5.1",
"css-minimizer-webpack-plugin": "^3.2.0",
"dotenv": "^10.0.0",
"dotenv-expand": "^5.1.0",
@@ -29,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",
@@ -55,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"
},
@@ -137,6 +139,8 @@
"@babel/core": "^7.24.1",
"@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",
+74 -75
View File
@@ -1,85 +1,84 @@
let socket;
const connectWebsocket = url => {
disconnectWebsocket()
socket = new WebSocket(url);
socket.onopen = () => {
chrome.runtime.sendMessage({status: "connected"})
}
socket.onclose = () => {
chrome.runtime.sendMessage({status: "disconnected"})
}
}
import {ActionType, injectScriptAndSendMessage, WebSocketManager} from './socket.js';
import { setupProxyHandlers } from './proxy.js';
const disconnectWebsocket = () => {
if (socket) {
try {
socket.close()
socket = null;
} catch (e) {
console.info("Chrome Extension Background is loaded");
}
}
}
const websocketManager = new WebSocketManager();
heartbeat = () => {
if (socket) {
if (socket.readyState === WebSocket.OPEN) {
try {
socket.send(JSON.stringify({
"type": "heartbeat",
}))
} catch (e) {
console.error("Error sending heartbeat:", e);
}
} else {
disconnectWebsocket()
}
}
}
heartbeat()
setInterval(heartbeat, 3000)
// 设置代理处理器
setupProxyHandlers();
console.info("Chrome Extenstion Background is loaded")
// 添加点击事件处理
chrome.action.onClicked.addListener((tab) => {
// 打开侧边栏
chrome.sidePanel.open({windowId: tab.windowId}).catch(error => {
console.error('Error opening side panel:', error);
});
});
let proxyHost = "";
// 设置默认打开状态
chrome.sidePanel.setOptions({
enabled: true,
path: 'index.html'
}).catch(error => {
console.error('Error setting side panel options:', error);
});
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.action === "connect") {
console.info("Start to connect websocket")
const host = msg['host'] || "127.0.0.1"
const port = msg['port'] || 11212
connectWebsocket(`ws://${host}:${port}/?token=${"a"}`)
} else if (msg.action === 'disconnect') {
disconnectWebsocket()
} else if (msg.action === "status") {
if (socket) {
chrome.runtime.sendMessage({connected: true})
} else {
chrome.runtime.sendMessage({connected: false})
}
} else if (msg.action === 'setproxy') {
chrome.proxy.settings.set({
value: {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: msg.scheme,
host: msg.host,
port: parseInt(`${msg.port}`)
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=chrome`, port)
break;
case ActionType.SEND_MESSAGE:
websocketManager.sendMessage(msg.message);
break;
case ActionType.DISCONNECT:
websocketManager.disconnectWebsocket();
break;
case ActionType.SET_PROXY:
chrome.proxy.settings.set({
value: {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: msg.scheme,
host: msg.host,
port: parseInt(`${msg.port}`)
},
// enable 127.0.0.1 && localhost to mitmproxy
bypassList: ["<-loopback>"]
}
},
scope: 'regular',
});
break;
case ActionType.CLEAR_PROXY:
chrome.proxy.settings.clear({})
break;
case ActionType.PROXY_STATUS:
chrome.proxy.settings.get({}, function (details) {
if (details.value && details.value.mode === "fixed_servers") {
let proxyConfig = details.value.rules.singleProxy;
chrome.runtime.sendMessage({
enable: true,
proxy: `${proxyConfig.scheme}://${proxyConfig.host}:${proxyConfig.port}`
})
} else {
chrome.runtime.sendMessage({enable: false, proxy: ""})
}
},
scope: 'regular',
})
proxyHost = `${msg.scheme}://${msg.host}:${msg.port}`
} else if (msg.action === 'clearproxy') {
chrome.proxy.settings.clear({})
proxyHost = ""
} else if (msg.action === 'proxystatus') {
if (proxyHost !== '') {
chrome.runtime.sendMessage({enable: !!proxyHost, proxy: proxyHost})
} else {
chrome.runtime.sendMessage({enable: false, proxy: ""})
}
});
break;
case ActionType.INJECT_SCRIPT:
(async () => {
await injectScriptAndSendMessage(msg.tabId, {
type: ActionType.INJECT_SCRIPT,
value: msg.value
});
})();
break
}
})
+59
View File
@@ -0,0 +1,59 @@
(() => {
if (window.contentScriptInjected) {
return;
}
window.contentScriptInjected = true;
window.badgeCount = 0;
// 检查并插入 CSS 样式
const styleId = 'injected-css-style';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
body {
border: 3px solid red;
position: relative; /* Ensure the body is positioned to allow the pseudo-element */
}
body::after {
content: "Injection successful";
display: block;
position: fixed;
top: 10px;
right: 10px;
background: green;
color: white;
padding: 5px 10px;
font-size: 16px;
z-index: 1000;
}
`;
document.head.appendChild(style);
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'yakit_inject_script') {
const injectedScriptURL = chrome.runtime.getURL('inject.js');
const script = document.createElement('script');
script.src = injectedScriptURL;
script.onload = () => {
window.postMessage({type: request.value.mode, value: request.value}, '*');
script.remove();
};
(document.head || document.documentElement).appendChild(script);
window.addEventListener('message', async function onMessage(event) {
if (event.source !== window || event.data.type !== 'FROM_INJECT_JS') {
return;
}
window.removeEventListener('message', onMessage);
window.badgeCount += 1;
// 直接向向发送端返回结果
sendResponse({action: 'yakit_to_extension_page', result: event.data.result});
// Send updated badge count to background script
await chrome.runtime.sendMessage({action: 'yakit_badge', data: window.badgeCount.toString()});
});
return true;
}
});
})()
+120
View File
@@ -0,0 +1,120 @@
class Database {
constructor() {
this.DB_NAME = 'yaklang_extension';
this.DB_VERSION = 1;
this.stores = {
// 代理日志存储
PROXY_LOGS: 'proxy_logs',
// 代理配置列表存储
PROXY_CONFIGS: 'proxy_configs',
// 当前代理配置存储
CURRENT_PROXY: 'current_proxy',
// 代理认证信息存储
PROXY_AUTH: 'proxy_auth'
};
}
async initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 代理日志存储
if (!db.objectStoreNames.contains(this.stores.PROXY_LOGS)) {
const logsStore = db.createObjectStore(this.stores.PROXY_LOGS, { keyPath: 'id' });
logsStore.createIndex('timestamp', 'timestamp');
logsStore.createIndex('resourceType', 'resourceType');
logsStore.createIndex('status', 'status');
}
// 代理配置列表存储
if (!db.objectStoreNames.contains(this.stores.PROXY_CONFIGS)) {
const configsStore = db.createObjectStore(this.stores.PROXY_CONFIGS, { keyPath: 'id' });
configsStore.createIndex('name', 'name');
configsStore.createIndex('enabled', 'enabled');
}
// 当前代理配置存储
if (!db.objectStoreNames.contains(this.stores.CURRENT_PROXY)) {
db.createObjectStore(this.stores.CURRENT_PROXY);
}
// 代理认证信息存储
if (!db.objectStoreNames.contains(this.stores.PROXY_AUTH)) {
const authStore = db.createObjectStore(this.stores.PROXY_AUTH, { keyPath: 'id' });
authStore.createIndex('host', 'host');
}
};
});
}
async getStore(storeName, mode = 'readonly') {
const db = await this.initDB();
const tx = db.transaction(storeName, mode);
return tx.objectStore(storeName);
}
// CRUD 操作
async get(storeName, key) {
const store = await this.getStore(storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName) {
const store = await this.getStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async put(storeName, value, key = undefined) {
try {
const store = await this.getStore(storeName, 'readwrite');
return new Promise((resolve, reject) => {
const request = key ? store.put(value, key) : store.put(value);
request.onsuccess = () => {
console.log(`Successfully put data in ${storeName}:`, value);
resolve(request.result);
};
request.onerror = () => {
console.error(`Error putting data in ${storeName}:`, request.error);
reject(request.error);
};
});
} catch (error) {
console.error(`Error in put operation for ${storeName}:`, error);
throw error;
}
}
async delete(storeName, key) {
const store = await this.getStore(storeName, 'readwrite');
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async clear(storeName) {
const store = await this.getStore(storeName, 'readwrite');
return new Promise((resolve, reject) => {
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
export const db = new Database();
+175
View File
@@ -0,0 +1,175 @@
import { db } from './db.js';
class ProxyStore {
constructor() {
this.MAX_LOGS = 1000;
}
// 代理配置相关操作
async getProxyConfigs() {
return await db.getAll(db.stores.PROXY_CONFIGS);
}
async saveProxyConfigs(configs) {
try {
console.log('Saving proxy configs:', configs);
// 确保配置数组有效
if (!Array.isArray(configs)) {
throw new Error('配置必须是数组');
}
// 开始事务
const store = await db.getStore(db.stores.PROXY_CONFIGS, 'readwrite');
// 清除现有配置
await store.clear();
// 保存新配置
for (const config of configs) {
await store.put(config);
}
console.log('Proxy configs saved successfully');
this.notifyConfigUpdate();
return true;
} catch (error) {
console.error('Error saving proxy configs:', error);
throw error;
}
}
async getCurrentProxy() {
return await db.get(db.stores.CURRENT_PROXY, 'current');
}
async setCurrentProxy(proxy) {
await db.put(db.stores.CURRENT_PROXY, proxy, 'current');
}
async clearCurrentProxy() {
await db.delete(db.stores.CURRENT_PROXY, 'current');
}
// 代理日志相关操作
async getLogs() {
const logs = await db.getAll(db.stores.PROXY_LOGS);
return logs.sort((a, b) => b.timestamp - a.timestamp);
}
async addLog(log) {
await db.put(db.stores.PROXY_LOGS, log);
await this.cleanOldLogs();
}
async clearLogs() {
await db.clear(db.stores.PROXY_LOGS);
}
async cleanOldLogs() {
const store = await db.getStore(db.stores.PROXY_LOGS, 'readwrite');
const countRequest = store.count();
countRequest.onsuccess = () => {
if (countRequest.result > this.MAX_LOGS) {
const excess = countRequest.result - this.MAX_LOGS;
const cursorRequest = store.index('timestamp').openCursor();
let deleted = 0;
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor && deleted < excess) {
cursor.delete();
deleted++;
cursor.continue();
}
};
}
};
}
// 代理认证相关操作
async getAuthHandlers() {
return await db.getAll(db.stores.PROXY_AUTH);
}
async saveAuthHandler(handler) {
await db.put(db.stores.PROXY_AUTH, handler);
}
async deleteAuthHandler(id) {
await db.delete(db.stores.PROXY_AUTH, id);
}
async clearAuthHandlers() {
await db.clear(db.stores.PROXY_AUTH);
}
async getErrors() {
return await db.get(db.stores.PROXY_AUTH, 'errors') || [];
}
async saveErrors(errors) {
await db.put(db.stores.PROXY_AUTH, errors, 'errors');
}
async getAuth() {
return await db.get(db.stores.PROXY_AUTH, 'auth');
}
async saveAuth(auth) {
await db.put(db.stores.PROXY_AUTH, auth, 'auth');
}
async clearAuth() {
await db.delete(db.stores.PROXY_AUTH, 'auth');
}
notifyConfigUpdate() {
chrome.runtime.sendMessage({
action: 'PROXY_CONFIGS_UPDATED'
}).catch(() => {
// 忽略接收者不存在的错误
});
}
async addAndEnableProxy(config) {
try {
// 先禁用所有其他代理
const existingConfigs = await this.getProxyConfigs();
for (const existingConfig of existingConfigs) {
if (existingConfig.enabled) {
await this.saveProxyConfigs([{
...existingConfig,
enabled: false
}]);
}
}
// 添加并启用新代理
await this.saveProxyConfigs([config]);
// 应用新代理
await chrome.proxy.settings.set({
value: {
mode: config.proxyType,
rules: {
singleProxy: {
scheme: config.scheme,
host: config.host,
port: config.port
}
}
},
scope: 'regular'
});
return { success: true };
} catch (error) {
console.error('Error in addAndEnableProxy:', error);
return { success: false, error };
}
}
}
export const proxyStore = new ProxyStore();
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

-1
View File
@@ -7,7 +7,6 @@
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
(() => {
if (window.injectedMessageListener) {
return;
}
window.injectedMessageListener = true;
window.addEventListener('message', function onMessage(event) {
if (event.source !== window) {
return;
}
let result
switch (event.data.type) {
case 'CONTENT_CALL_FUNCTION':
const fn_name = event.data.value.fn_name;
const args = event.data.value.args;
result = window[fn_name](args);
window.postMessage({type: 'FROM_INJECT_JS', result: result}, '*');
break;
case 'CONTENT_EVAL_CODE':
const code = event.data.value.code;
console.log(code)
result = (() => {
try {
return eval(code);
} catch (e) {
// console.error("Error evaluating code:", e);
return e.toString();
}
})();
// console.log("CONTENT_EVAL_CODE result: ", result);
window.postMessage({type: 'FROM_INJECT_JS', result: result}, '*');
break;
default:
break;
}
});
})();
+40 -15
View File
@@ -1,8 +1,12 @@
{
"manifest_version": 3,
"name": "Yakit Chrome Endpoint",
"version": "1.0",
"version": "0.0.3",
"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,25 +15,46 @@
"128": "/images/icon128.png"
}
},
"side_panel": {
"default_path": "index.html"
},
"background": {
"service_worker": "background.js"
"service_worker": "background.js",
"type": "module"
},
"permissions": [
"activeTab",
"scripting",
"tabs",
"proxy",
"storage",
"webRequest"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
"matches": ["http://mitm/"],
"run_at": "document_start",
"js": ["proxy/content.js"]
}
],
"permissions": [
"proxy",
"storage",
"sidePanel",
"webRequest",
"declarativeNetRequest",
"webNavigation",
"tabs"
],
"host_permissions": [
"<all_urls>"
],
"web_accessible_resources": [
{
"resources": [
"images/*",
"proxy/*"
],
"js": [
"content.js"
]
"matches": ["<all_urls>"]
},
{
"resources": [
"/images/yak.svg"
],
"matches": ["<all_urls>"]
}
],
"icons": {
+559
View File
@@ -0,0 +1,559 @@
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 lastProxy = await proxyStore.getCurrentProxy();
if (lastProxy) {
// 如果有上次的配置,恢复它
console.log('Restoring last proxy configuration:', lastProxy);
await handleSetProxyConfig(lastProxy, () => {});
return;
}
// 获取当前的代理设置
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
}, () => {});
} 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 };
}
}
+714
View File
@@ -0,0 +1,714 @@
console.log("Content script starting...");
// 代理操作类型常量
const ProxyActionType = {
SET_PROXY_CONFIG: 'SET_PROXY_CONFIG',
CLEAR_PROXY_CONFIG: 'CLEAR_PROXY_CONFIG',
GET_PROXY_STATUS: 'GET_PROXY_STATUS',
GET_PROXY_CONFIGS: 'GET_PROXY_CONFIGS',
UPDATE_PROXY_CONFIG: 'UPDATE_PROXY_CONFIG'
};
// 在文件顶部添加常量声明
const YAK_ICON_URL = chrome.runtime.getURL('/images/yak.svg');
// 添加一个通用的消息发送函数
async function sendMessageWithRetry(message, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await chrome.runtime.sendMessage(message);
} catch (error) {
console.warn(`Attempt ${i + 1} failed:`, error);
if (i === maxRetries - 1) {
throw error;
}
// 等待一小段时间后重试
await new Promise(resolve => setTimeout(resolve, 500));
}
}
}
// 获取当前代理状态
async function getCurrentProxy() {
try {
const response = await sendMessageWithRetry({
action: ProxyActionType.GET_PROXY_STATUS
});
if (!response || !response.success) {
console.log('No valid response from background script');
return { enable: false, proxy: '', currentMode: 'direct' };
}
const status = response.data;
console.log('Proxy status from background:', status);
// 返回当前模式
return {
enable: status.enabled,
proxy: status.mode === 'system' ? 'system' : '',
currentMode: status.mode || 'direct' // 使用 mode 来判断当前激活的代理
};
} catch (error) {
console.error('Error getting proxy status:', error);
return { enable: false, proxy: '', currentMode: 'direct' };
}
}
// 获取所有代理配置
async function getProxyConfigs() {
try {
const response = await sendMessageWithRetry({
action: ProxyActionType.GET_PROXY_CONFIGS
});
if (!response) {
console.log('No response from background script');
return [];
}
if (!response.success) {
console.error('Error in response:', response.error);
return [];
}
return response.data || [];
} catch (error) {
console.error('Error getting proxy configs:', error);
return [];
}
}
// 切换代理
async function switchProxy(config) {
try {
console.log('Switching proxy:', config);
// 根据配置类型构建正确的配置对象
let proxyConfig;
if (config.proxyType === 'system') {
proxyConfig = {
id: 'system',
name: '[系统代理]',
proxyType: 'system',
enabled: true
};
} else if (config.proxyType === 'direct') {
proxyConfig = {
id: 'direct',
name: '[直接连接]',
proxyType: 'direct',
enabled: false
};
} else {
proxyConfig = {
...config,
enabled: true
};
}
// 发送配置更改消息
await sendMessageWithRetry({
action: ProxyActionType.SET_PROXY_CONFIG,
config: proxyConfig
});
await PanelManager.updatePanel();
} catch (error) {
console.error('Error switching proxy:', error);
}
}
// 清除代理
async function clearProxy() {
try {
const response = await sendMessageWithRetry({
action: ProxyActionType.SET_PROXY_CONFIG,
config: {
id: 'direct',
name: '[直接连接]',
proxyType: 'direct',
enabled: false
}
});
console.log('Clear proxy response:', response);
// 使用 PanelManager 的 updatePanel 方法
await PanelManager.updatePanel();
} catch (error) {
console.error('Error clearing proxy:', error);
}
}
// 修改 PanelManager
const PanelManager = {
panel: null,
messageListener: null,
_updating: false,
_updateQueue: Promise.resolve(),
_currentState: null, // 用于跟踪当前状态
_lastUpdate: null, // 添加最后更新时间戳
_lastState: null,
_pollingInterval: null,
init() {
// 确保 document.body 存在
if (!document.body) {
console.log('Body not ready, waiting...');
this.waitForBody();
return;
}
if (this.panel) {
console.log('Panel already exists, updating...');
this.updatePanel();
return;
}
console.log('Creating new panel...');
this.createPanel();
},
// 添加等待 body 的方法
waitForBody() {
if (document.body) {
this.init();
return;
}
console.log('Setting up MutationObserver for body');
const observer = new MutationObserver((mutations, obs) => {
if (document.body) {
console.log('Body found via observer');
obs.disconnect();
this.init();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
},
createPanel() {
if (!document.body) {
console.log('Body not available during panel creation');
return;
}
// 创建容器
const container = document.createElement('div');
container.id = 'yakit-proxy-panel';
// 创建 shadow DOM
const shadow = container.attachShadow({ mode: 'open' });
// 添加样式
const style = document.createElement('style');
style.textContent = `
.floating-panel {
position: fixed;
top: 20px;
right: 0;
width: 180px;
background: white;
border-radius: 8px 0 0 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 2147483647;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
transition: transform 0.3s ease;
}
.floating-panel.collapsed {
transform: translateX(100%);
}
.panel-header {
padding: 4px;
border-bottom: 1px solid #eee;
border-radius: 8px 0 0 0;
background: #f8f9fa;
display: flex;
justify-content: space-between;
align-items: center;
}
.collapse-trigger {
position: absolute;
left: -20px;
top: 0;
width: 20px;
height: 100%;
background: #f8f9fa;
border-radius: 8px 0 0 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
}
.collapse-trigger:hover {
background: #e9ecef;
}
.collapse-trigger::after {
content: '◀';
transition: transform 0.3s ease;
}
.floating-panel.collapsed .collapse-trigger::after {
transform: rotate(180deg);
}
.panel-content {
padding: 4px;
}
.proxy-item {
position: relative;
display: flex;
align-items: center;
padding: 4px 10px;
cursor: pointer;
transition: all 0.2s;
color: #666;
border-left: 3px solid transparent;
overflow: hidden;
}
.proxy-item > span {
position: relative;
z-index: 1;
}
.watermark-icon {
position: absolute;
right: 0;
width: 100%;
height: 100%;
opacity: 0.3;
pointer-events: none;
display: none;
object-fit: contain;
object-position: right center;
padding: 4px;
}
.proxy-item.active .watermark-icon {
display: block;
}
.proxy-item.active {
color: #ff6b00 !important;
background: #fff7e6;
border-left: 3px solid #ff6b00;
}
.proxy-item.active span:first-child {
color: #ff6b00 !important;
}
.proxy-item span:first-child {
margin-right: 8px;
font-size: 16px;
color: #666;
transition: color 0.2s;
}
.proxy-item:hover {
background: #f5f5f5;
}
.proxy-item:hover span:first-child {
color: #ff6b00;
}
.add-proxy {
display: flex;
align-items: center;
padding: 4px 10px;
color: #1890ff;
cursor: pointer;
border-top: 1px solid #eee;
transition: all 0.2s;
}
.add-proxy:hover {
background: #f5f5f5;
}
.settings {
padding: 4px 10px;
color: #666;
cursor: pointer;
border-top: 1px solid #eee;
transition: all 0.2s;
}
.settings:hover {
background: #f5f5f5;
}
.panel-header .header-content {
display: flex;
align-items: center;
gap: 8px;
padding: 0 4px;
}
.yak-icon {
width: 24px;
height: 24px;
object-fit: contain;
}
.divider {
height: 1px;
background-color: #eee;
margin: 4px 0;
}
`;
// 创建面板内容
const panel = document.createElement('div');
panel.className = 'floating-panel';
panel.innerHTML = `
<div class="collapse-trigger"></div>
<div class="panel-header">
<div class="header-content">
<img src="${YAK_ICON_URL}" class="yak-icon" alt="Yak" />
<span>代理设置</span>
</div>
</div>
<div class="panel-content">
<div class="proxy-item active">
<span>🟢</span>
<span>[直接连接]</span>
</div>
<div class="proxy-item">
<span>⚙️</span>
<span>[系统代理]</span>
</div>
<div class="divider"></div>
<div class="add-proxy">
<span></span>
<span>[添加代理...]</span>
</div>
<div class="settings">
<span>👨‍💻</span>
<span>选项</span>
</div>
</div>
`;
// 将样式和面板添加到 shadow DOM
shadow.appendChild(style);
shadow.appendChild(panel);
// 确保安全地添加到 body
try {
document.body.appendChild(container);
this.panel = container;
// 添加折叠触发器的点击事件
const floatingPanel = shadow.querySelector('.floating-panel');
const collapseTrigger = shadow.querySelector('.collapse-trigger');
collapseTrigger.addEventListener('click', () => {
floatingPanel.classList.toggle('collapsed');
});
// 设置消息监听和开始轮询
this.setupMessageListener();
// 初始更新面板
this.updatePanel();
// 添加页面卸载时的清理
window.addEventListener('unload', () => {
if (this._pollingInterval) {
clearInterval(this._pollingInterval);
}
if (this.messageListener) {
chrome.runtime.onMessage.removeListener(this.messageListener);
}
});
} catch (error) {
console.error('Error creating panel:', error);
}
},
setupMessageListener() {
if (this.messageListener) {
chrome.runtime.onMessage.removeListener(this.messageListener);
}
this.messageListener = async (message, sender) => {
// 只检查消息是否来自同一个扩展
if (sender.id !== chrome.runtime.id) {
return;
}
// 处理状态更新消息
if (message.action === 'PROXY_STATUS_CHANGED' ||
message.action === 'PROXY_CONFIGS_UPDATED') {
console.log('Received update message:', message, 'from:', sender);
await this.updatePanel();
}
};
chrome.runtime.onMessage.addListener(this.messageListener);
},
async updatePanel() {
if (!this.panel || !document.body.contains(this.panel)) {
console.log('Panel not in document, recreating...');
this.createPanel();
return;
}
const panel = this.panel.shadowRoot?.querySelector('.panel-content');
if (!panel) return;
// 使用更新队列确保更新按顺序执行
this._updateQueue = this._updateQueue.then(async () => {
if (this._updating) {
console.log('Update already in progress, skipping...');
return;
}
try {
this._updating = true;
// 获取最新状态
const [currentProxy, configs] = await Promise.all([
getCurrentProxy(),
getProxyConfigs()
]);
// 状态没有变化时不更新
const newState = JSON.stringify({ currentProxy, configs });
if (this._currentState === newState) {
console.log('State unchanged, skipping update');
return;
}
this._currentState = newState;
// 再次检查面板状态
if (!this.panel || !document.body.contains(this.panel)) {
console.log('Panel was removed during data fetch');
return;
}
console.log('Updating panel with:', { currentProxy, configs });
if (!Array.isArray(configs)) {
console.error('Invalid configs:', configs);
return;
}
// 保存当前激活的项
const currentActiveId = panel.querySelector('.proxy-item.active')?.dataset.id;
// 构建新的 HTML
const newHtml = this._buildPanelHtml(currentProxy, configs);
// 创建一个临时容器来比较内容
const temp = document.createElement('div');
temp.innerHTML = newHtml;
// 只在内容真正改变时更新
if (panel.innerHTML !== temp.innerHTML) {
requestAnimationFrame(() => {
panel.innerHTML = newHtml;
this._bindEventListeners(panel, configs, currentProxy);
// 验证更新后的状态
const newActiveId = panel.querySelector('.proxy-item.active')?.dataset.id;
if (currentActiveId !== newActiveId) {
console.log('Active state changed:', {
from: currentActiveId,
to: newActiveId
});
}
});
}
} catch (error) {
console.error('Error updating panel:', error);
} finally {
this._updating = false;
}
}).catch(error => {
console.error('Error in update queue:', error);
this._updating = false;
});
return this._updateQueue;
},
// 将 HTML 构建逻辑抽离成单独的方法
_buildPanelHtml(currentProxy, configs) {
let html = `
<div class="proxy-item ${currentProxy.currentMode === 'direct' ? 'active' : ''}"
data-id="direct"
title="直接连接">
<span style="color: ${currentProxy.currentMode === 'direct' ? '#ff6b00' : '#666'}">🟢</span>
<span>[直接连接]</span>
<img src="${YAK_ICON_URL}" class="watermark-icon" alt="" />
</div>
<div class="proxy-item ${currentProxy.currentMode === 'system' ? 'active' : ''}"
data-id="system"
title="系统代理">
<span style="color: ${currentProxy.currentMode === 'system' ? '#ff6b00' : '#666'}">⚙️</span>
<span>[系统代理]</span>
<img src="${YAK_ICON_URL}" class="watermark-icon" alt="" />
</div>
<div class="divider"></div>
`;
// 添加自定义代理配置
configs.forEach(config => {
if (config.proxyType !== 'direct' && config.proxyType !== 'system') {
// 判断是否激活:当前模式为 fixed_servers 且配置已启用
const isActive = currentProxy.currentMode === 'fixed_servers' &&
config.enabled;
const tooltipText = config.proxyType === 'pac_script'
? 'PAC Script'
: `${(config.proxyType || 'HTTP').toUpperCase()} ${config.host || ''}:${config.port || ''}`;
const proxyIcon = config.proxyType === 'pac_script' ? '📜' : '🌐';
html += `
<div class="proxy-item ${isActive ? 'active' : ''}"
data-id="${config.id}"
title="${tooltipText}">
<span style="color: ${isActive ? '#ff6b00' : '#666'}">${proxyIcon}</span>
<span>${config.name || '未命名代理'}</span>
<img src="${YAK_ICON_URL}" class="watermark-icon" alt="" />
</div>
`;
}
});
// 添加操作按钮
html += `
<div class="add-proxy">
<span></span>
<span>添加代理...</span>
</div>
<div class="settings">
<span>👨‍💻</span>
<span>选项</span>
</div>
`;
return html;
},
_bindEventListeners(panel, configs, currentProxy) {
// 代理项点击事件
panel.querySelectorAll('.proxy-item').forEach(item => {
const id = item.dataset.id;
// 使用事件委托来提高性能
const clickHandler = async (e) => {
e.stopPropagation();
if (this._updating) {
console.log('Panel is updating, ignoring click');
return;
}
// 添加点击反馈
const originalOpacity = item.style.opacity;
item.style.opacity = '0.7';
try {
// 立即更新 UI 状态,不等待响应
panel.querySelectorAll('.proxy-item').forEach(i => {
i.classList.remove('active');
i.querySelector('span').style.color = '#666';
});
item.classList.add('active');
item.querySelector('span').style.color = '#ff6b00';
if (id === 'direct') {
await clearProxy();
} else if (id === 'system') {
await switchProxy({
id: 'system',
name: '[系统代理]',
proxyType: 'system'
});
} else {
const config = configs.find(c => c.id === id);
if (config) {
await switchProxy(config);
}
}
} catch (error) {
console.error('Error handling proxy item click:', error);
// 发生错误时恢复原状
await this.updatePanel();
} finally {
item.style.opacity = originalOpacity;
}
};
// 使用 { once: true } 确保事件监听器不会重复
item.addEventListener('click', clickHandler, { once: true });
});
// 添加代理按钮
panel.querySelector('.add-proxy')?.addEventListener('click', async () => {
try {
await sendMessageWithRetry({
action: 'OPEN_OPTIONS_PAGE',
triggerAdd: true
});
} catch (error) {
console.error('Error handling add proxy:', error);
}
});
// 设置按钮
panel.querySelector('.settings')?.addEventListener('click', async () => {
try {
await sendMessageWithRetry({
action: 'OPEN_OPTIONS_PAGE'
});
} catch (error) {
console.error('Error opening options page:', error);
}
});
}
};
// 修改初始化调用
console.log("Setting up initialization...");
// 根据文档状态决定初始化方式
if (document.readyState === 'loading') {
console.log('Document still loading, waiting for DOMContentLoaded');
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded fired');
PanelManager.init();
});
} else {
console.log('Document already loaded, initializing immediately');
PanelManager.init();
}
// 保留 load 事件作为备份
window.addEventListener('load', () => {
console.log("Window load triggered");
if (!PanelManager.panel) {
PanelManager.init();
}
});
// 添加更详细的日志
console.log("Document readyState:", document.readyState);
console.log("Document body exists:", !!document.body);
console.log("Document documentElement exists:", !!document.documentElement);
+160
View File
@@ -0,0 +1,160 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.header h1 {
margin: 0;
font-size: 24px;
color: #333;
}
.proxy-item {
background: white;
border: 1px solid #e8e8e8;
border-radius: 4px;
margin-bottom: 16px;
padding: 16px;
transition: all 0.3s;
}
.proxy-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.09);
}
.proxy-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.proxy-name {
font-size: 14px;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
width: 200px;
}
.proxy-name:focus {
border-color: #40a9ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
}
.proxy-type-select,
.proxy-scheme,
.proxy-host,
.proxy-port {
width: 100%;
height: 32px;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
}
.proxy-type-select:focus,
.proxy-scheme:focus,
.proxy-host:focus,
.proxy-port:focus {
border-color: #40a9ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
}
.proxy-actions {
display: flex;
gap: 8px;
}
.proxy-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #666;
}
.btn-primary {
background: #1890ff;
border: none;
color: white;
padding: 4px 15px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary:hover {
background: #40a9ff;
}
.btn-secondary {
background: white;
border: 1px solid #d9d9d9;
color: rgba(0,0,0,0.85);
padding: 4px 15px;
border-radius: 4px;
cursor: pointer;
margin-left: 8px;
}
.btn-secondary:hover {
border-color: #40a9ff;
color: #40a9ff;
}
.header-actions {
display: flex;
align-items: center;
}
.delete-btn {
padding: 4px 8px;
background: #ff4d4f;
}
.delete-btn:hover {
background: #ff7875;
}
.pac-script {
font-family: monospace;
min-height: 200px;
}
.options-page {
min-height: 100vh;
}
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>代理设置</title>
</head>
<body>
<div id="root"></div>
<script src="../options.bundle.js"></script>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
// 代理认证管理
import { proxyStore } from '../db/proxy-store.js';
export class ProxyAuth {
static async setupAuthListener() {
// 使用 chrome.webRequest.onAuthRequired 的非阻塞版本
chrome.webRequest.onAuthRequired.addListener(
async (details) => {
try {
// 获取认证处理器
const handlers = await proxyStore.getAuthHandlers();
const handler = handlers.find(h =>
details.challenger?.host === h.host
);
if (handler) {
// 使用 declarativeNetRequest 规则来处理认证
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [handler.id],
addRules: [{
id: parseInt(handler.id),
priority: 1,
action: {
type: 'modifyHeaders',
requestHeaders: [
{
header: 'Proxy-Authorization',
operation: 'set',
value: 'Basic ' + btoa(`${handler.username}:${handler.password}`)
}
]
},
condition: {
domains: [handler.host],
resourceTypes: ['main_frame', 'sub_frame', 'stylesheet', 'script', 'image', 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', 'media', 'websocket', 'other']
}
}]
});
}
} catch (error) {
console.error('Auth error:', error);
}
},
{ urls: ["<all_urls>"] }
);
}
static async saveAuthHandler(host, username, password) {
const handler = {
id: Date.now().toString(),
host,
username,
password
};
await proxyStore.saveAuthHandler(handler);
await this.setupAuthListener(); // 重新设置认证规则
}
static async removeAuthHandler(host) {
const handlers = await proxyStore.getAuthHandlers();
const handler = handlers.find(h => h.host === host);
if (handler) {
await proxyStore.deleteAuthHandler(handler.id);
// 移除对应的认证规则
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [parseInt(handler.id)]
});
}
}
static setupErrorHandler() {
// 使用 storage 记录错误
return {
logError: async (error) => {
const errors = await proxyStore.getErrors() || [];
errors.push({
timestamp: Date.now(),
error: error.message || error
});
await proxyStore.saveErrors(errors.slice(-100)); // 只保留最近100条错误记录
}
};
}
// 设置代理认证信息
static async setProxyAuth(username, password) {
try {
await proxyStore.saveAuth({ username, password, timestamp: Date.now() });
return true;
} catch (error) {
console.error('Error setting proxy auth:', error);
return false;
}
}
// 获取代理认证信息
static async getProxyAuth() {
try {
return await proxyStore.getAuth();
} catch (error) {
console.error('Error getting proxy auth:', error);
return null;
}
}
// 清除代理认证信息
static async clearProxyAuth() {
try {
await proxyStore.clearAuth();
return true;
} catch (error) {
console.error('Error clearing proxy auth:', error);
return false;
}
}
}
+150
View File
@@ -0,0 +1,150 @@
import { proxyStore } from '../db/proxy-store.js';
// 日志数据库管理
class ProxyLogs {
async getResourceType(details) {
try {
// 首先检查请求类型
if (details.type) {
// 直接使用 Chrome 提供的类型
switch (details.type) {
case 'main_frame': return 'page';
case 'xmlhttprequest': {
// 检查请求头来区分 XHR 和 Fetch
const isFetch = details.requestHeaders?.some(
header => header.name.toLowerCase() === 'sec-fetch-mode' &&
header.value === 'cors'
);
return isFetch ? 'fetch' : 'xhr';
}
case 'script': return 'script';
case 'stylesheet': return 'stylesheet';
case 'image': return 'image';
case 'media': return 'media';
case 'font': return 'font';
case 'websocket': return 'websocket';
}
}
// 根据文件扩展名和内容类型判断
const contentType = details.requestHeaders?.find(
header => header.name.toLowerCase() === 'content-type'
)?.value || '';
const url = new URL(details.url);
const pathname = url.pathname.toLowerCase();
// 检查文件扩展名
if (pathname.endsWith('.js')) return 'script';
if (pathname.endsWith('.css')) return 'stylesheet';
if (/\.(png|jpg|jpeg|gif|webp|svg|ico)$/.test(pathname)) return 'image';
if (/\.(mp3|mp4|wav|ogg|webm)$/.test(pathname)) return 'media';
if (/\.(woff|woff2|ttf|eot|otf)$/.test(pathname)) return 'font';
// 根据内容类型判断
if (contentType) {
if (contentType.includes('javascript')) return 'script';
if (contentType.includes('css')) return 'stylesheet';
if (contentType.includes('image/')) return 'image';
if (contentType.includes('audio/') || contentType.includes('video/')) return 'media';
if (contentType.includes('font/') || contentType.includes('application/font')) return 'font';
if (contentType.includes('application/json')) return 'xhr';
if (contentType.includes('application/x-www-form-urlencoded')) return 'xhr';
}
// 检查 Accept 头
const acceptHeader = details.requestHeaders?.find(
header => header.name.toLowerCase() === 'accept'
)?.value || '';
if (acceptHeader) {
if (acceptHeader.includes('application/json')) return 'xhr';
if (acceptHeader.includes('text/javascript')) return 'script';
if (acceptHeader.includes('text/css')) return 'stylesheet';
if (acceptHeader.includes('image/')) return 'image';
}
console.log('Resource type detection:', {
url: details.url,
type: details.type,
contentType,
acceptHeader,
headers: details.requestHeaders
});
return 'other';
} catch (error) {
console.error('Error determining resource type:', error);
return 'other';
}
}
async logRequest(details, proxyConfig, error = null) {
try {
// 检查是否是扩展自身的请求
if (details.url.startsWith('chrome-extension://')) {
return;
}
// 获取资源类型
const resourceType = await this.getResourceType(details);
const log = {
id: Date.now().toString(),
timestamp: Date.now(),
url: details.url,
proxyId: proxyConfig.id,
proxyName: proxyConfig.name,
status: error ? 'error' : 'success',
errorMessage: error?.message,
method: details.method,
requestHeaders: details.requestHeaders?.reduce((acc, header) => {
acc[header.name] = header.value;
return acc;
}, {}),
requestBody: details.requestBody?.raw?.[0]?.bytes
? decodeURIComponent(String.fromCharCode.apply(null, new Uint8Array(details.requestBody.raw[0].bytes)))
: null,
responseHeaders: details.responseHeaders?.reduce((acc, header) => {
acc[header.name] = header.value;
return acc;
}, {}),
timing: {
startTime: details.timeStamp,
endTime: Date.now(),
duration: Date.now() - details.timeStamp
},
protocol: details.protocol || details.type,
ip: details.ip,
fromCache: details.fromCache,
resourceType
};
// 使用 proxyStore 存储日志
await proxyStore.addLog(log);
this.notifyLogUpdate();
} catch (error) {
console.error('Error logging proxy request:', error);
}
}
async getLogs() {
return await proxyStore.getLogs();
}
async clearLogs() {
await proxyStore.clearLogs();
this.notifyLogUpdate();
}
notifyLogUpdate() {
// 通知前端日志已更新
chrome.runtime.sendMessage({
action: 'PROXY_LOGS_UPDATED'
}).catch(() => {
// 忽略接收者不存在的错误
});
}
}
export const proxyLogs = new ProxyLogs();
+51
View File
@@ -0,0 +1,51 @@
import { proxyStore } from '../db/proxy-store.js';
// 代理配置存储和管理
export class ProxySettings {
static async importSettings(settings) {
try {
if (Array.isArray(settings) && settings.every(s => s.proxyType)) {
await proxyStore.saveProxyConfigs(settings);
return {success: true};
}
return {success: false, error: "Invalid settings format"};
} catch (error) {
return {success: false, error: error.message};
}
}
static async exportSettings() {
try {
const configs = await proxyStore.getProxyConfigs();
return {success: true, settings: configs || []};
} catch (error) {
return {success: false, error: error.message};
}
}
static async setDefaultConfigs() {
const configs = await proxyStore.getProxyConfigs();
if (!configs || configs.length === 0) {
// 设置默认的直接连接配置
await proxyStore.saveProxyConfigs([
{
id: 'direct',
name: '直接连接',
proxyType: 'direct',
enabled: false
},
{
id: 'system',
name: '系统代理',
proxyType: 'system',
enabled: false
}
]);
}
// 确保日志存储已初始化
const logs = await proxyStore.getLogs();
if (!logs || logs.length === 0) {
await proxyStore.clearLogs();
}
}
}
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>My Sidepanel</title>
</head>
<body>
<h1>All sites sidepanel extension</h1>
<p>This side panel is enabled on all sites</p>
</body>
</html>
+134
View File
@@ -0,0 +1,134 @@
export const ActionType = {
CONNECT: 'connect',
SEND_MESSAGE: 'send_message',
DISCONNECT: 'disconnect',
STATUS: 'status',
PROXY_STATUS: 'proxy_status',
SET_PROXY: 'set_proxy',
CLEAR_PROXY: 'clear_proxy',
INJECT_SCRIPT: 'yakit_inject_script',
TO_EXTENSION_PAGE: "yakit_to_extension_page",
BADGE_COUNT: "yakit_badge",
}
export class WebSocketManager {
constructor() {
this.socket = null;
this.intervalId = null;
}
connectWebsocket(url, port) {
this.disconnectWebsocket();
this.socket = new WebSocket(url);
this.socket.onopen = () => {
chrome.runtime.sendMessage({action: ActionType.STATUS, connected: true, port: port});
this.startHeartbeat();
};
this.socket.onmessage = (event) => {
console.log("event", event)
this.handleMessage(event.data);
};
this.socket.onclose = () => {
chrome.runtime.sendMessage({action: ActionType.STATUS, connected: false});
};
this.socket.onerror = (error) => {
console.error("WebSocket Error:", error);
};
}
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 {
this.socket.close();
chrome.runtime.sendMessage({action: ActionType.STATUS, connected: false});
} catch (e) {
console.error("Error closing websocket:", e);
}
this.socket = null;
this.stopHeartbeat();
}
}
startHeartbeat() {
this.intervalId = setInterval(() => this.heartbeat(), 25000);
}
stopHeartbeat() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
heartbeat() {
if (this.isConnected()) {
try {
this.socket.send(JSON.stringify({"type": "heartbeat"}));
} catch (e) {
console.error("Error sending heartbeat:", e);
}
} else {
this.disconnectWebsocket();
}
}
isConnected() {
return this.socket && this.socket.readyState === WebSocket.OPEN;
}
handleMessage(message) {
message = JSON.parse(message);
if (message && message.type === "eval") {
(async () => {
const [tab] = await getTab();
await injectScriptAndSendMessage(tab.id, {
type: ActionType.INJECT_SCRIPT,
value: {
mode: "CONTENT_EVAL_CODE", code: message.code,
}
});
})();
}
}
}
const getTab = async () => {
return chrome.tabs.query({active: true, lastFocusedWindow: true})
}
export const injectScriptAndSendMessage = async (tabId, message) => {
try {
// 注入 JS 脚本
await chrome.scripting.executeScript({
target: {tabId: tabId},
files: ['content.js']
});
// 发送消息
const response = await chrome.tabs.sendMessage(tabId, message);
console.log("response", response);
if (response && response.action === ActionType.TO_EXTENSION_PAGE) {
await chrome.runtime.sendMessage(response);
}
} catch (err) {
console.error('Script or CSS injection failed:', err);
}
}
+13
View File
@@ -0,0 +1,13 @@
// 这个是插件 background 中使用的 action 类型
// 和前端的 action 要保持一致
export const ProxyActionType = {
SET_PROXY_CONFIG: "SET_PROXY_CONFIG",
CLEAR_PROXY_CONFIG: "CLEAR_PROXY_CONFIG",
GET_PROXY_STATUS: "GET_PROXY_STATUS",
GET_PROXY_LOGS: "GET_PROXY_LOGS",
CLEAR_PROXY_LOGS: "CLEAR_PROXY_LOGS",
GET_PROXY_CONFIGS: "GET_PROXY_CONFIGS",
ADD_PROXY_CONFIG: "ADD_PROXY_CONFIG",
UPDATE_PROXY_CONFIG: "UPDATE_PROXY_CONFIG",
OPEN_OPTIONS_PAGE: "OPEN_OPTIONS_PAGE",
};
-38
View File
@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.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);
}
}
+23 -15
View File
@@ -1,21 +1,29 @@
import React from 'react';
import './App.css';
import {Layout, Row} from 'antd';
import {Controller} from "./components/Controller";
import {Proxifier} from "./components/Proxifier";
import React from "react";
// 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 (
<div className="App" style={{width: 300, backgroundColor: "#eee", padding: 8}}>
<Layout>
<Row>
<Controller/>
</Row>
<Row>
<Proxifier/>
</Row>
</Layout>
</div>
<ConfigProvider
theme={{
token: {
colorPrimary: "#F28B44",
},
}}
>
<div className="App">
{/*<Contro/>*/}
{/* <Proxifier/> */}
<ProxySwitch/>
{/* <EvalInTab/> */}
</div>
</ConfigProvider>
);
}
+168
View File
@@ -0,0 +1,168 @@
import Icon from "@ant-design/icons";
import { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon";
import React from "react";
interface IconProps extends CustomIconComponentProps {
onClick: (e: React.MouseEvent) => void;
ref?: any;
}
const X = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M4 12L12 4M4 4L12 12"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* @description Icon/Outline/x
*/
export const XIcon = (props: Partial<IconProps>) => {
return <Icon component={X} {...props} />;
};
const Check = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M3.33337 8.66669L6.00004 11.3334L12.6667 4.66669"
stroke="#56C991"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* @description Icon/Outline/check
*/
export const CheckIcon = (props: Partial<IconProps>) => {
return <Icon component={Check} {...props} />;
};
const PencilAlt = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M7.33329 3.33334H3.99996C3.26358 3.33334 2.66663 3.93029 2.66663 4.66667V12C2.66663 12.7364 3.26358 13.3333 3.99996 13.3333H11.3333C12.0697 13.3333 12.6666 12.7364 12.6666 12V8.66667M11.7238 2.39052C12.2445 1.86983 13.0887 1.86983 13.6094 2.39052C14.1301 2.91122 14.1301 3.75544 13.6094 4.27614L7.88557 10H5.99996L5.99996 8.11438L11.7238 2.39052Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* @description Icon/Outline/pencil-alt
*/
export const PencilAltIcon = (props: Partial<IconProps>) => {
return <Icon component={PencilAlt} {...props} />;
};
const Refresh = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M2.66663 2.66669V6.00002H3.0543M13.292 7.33335C12.964 4.70248 10.7197 2.66669 7.99996 2.66669C5.76171 2.66669 3.84549 4.04547 3.0543 6.00002M3.0543 6.00002H5.99996M13.3333 13.3334V10H12.9456M12.9456 10C12.1544 11.9546 10.2382 13.3334 7.99996 13.3334C5.28021 13.3334 3.03595 11.2976 2.70789 8.66669M12.9456 10H9.99996"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* @description Icon/Outline/refresh
*/
export const RefreshIcon = (props: Partial<IconProps>) => {
return <Icon component={Refresh} {...props} />;
};
const Exit = () => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.33333 6.66668C9.10226 6.78107 8.87965 6.90988 8.66667 7.0519C7.05869 8.12418 6 9.95027 6 12.0227C6 15.3239 8.68629 18 12 18C15.3137 18 18 15.3239 18 12.0227C18 9.95027 16.9413 8.12418 15.3333 7.0519C15.1204 6.90988 14.8977 6.78107 14.6667 6.66668M12 5.33334V10.6667"
stroke="#F7544A"
strokeLinecap="round"
/>
</svg>
);
/**
* @description 退出
*/
export const ExitIcon = (props: Partial<IconProps>) => {
return <Icon component={Exit} {...props} />;
};
const PlusSm = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M8 4V8M8 8V12M8 8H12M8 8L4 8"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* @description Icon/Outline/plus-sm
*/
export const PlusSmIcon = (props: Partial<IconProps>) => {
return <Icon component={PlusSm} {...props} />;
};
const Trash = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M12.6666 4.66667L12.0884 12.7617C12.0386 13.4594 11.458 14 10.7585 14H5.24145C4.54193 14 3.96135 13.4594 3.91151 12.7617L3.33329 4.66667M6.66663 7.33333V11.3333M9.33329 7.33333V11.3333M9.99996 4.66667V2.66667C9.99996 2.29848 9.70148 2 9.33329 2H6.66663C6.29844 2 5.99996 2.29848 5.99996 2.66667V4.66667M2.66663 4.66667H13.3333"
stroke="#F7544A"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
/**
* @description Icon/Outline/trash
*/
export const TrashIcon = (props: Partial<IconProps>) => {
return <Icon component={Trash} {...props} />;
};
+10
View File
@@ -0,0 +1,10 @@
.add-proxy-form {
padding: 24px;
}
.form-buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
+72
View File
@@ -0,0 +1,72 @@
import React from 'react';
import { Form, Input, Select, InputNumber, Button, message } from 'antd';
import { ProxyActionType } from '@/types/action';
import './index.css';
interface EditFormData {
name: string;
proxyType: string;
scheme?: string;
host?: string;
port?: number;
pacScript?: string;
}
export const AddProxyForm: React.FC = () => {
const [form] = Form.useForm<EditFormData>();
// 初始化表单
React.useEffect(() => {
form.setFieldsValue({
name: '',
proxyType: 'fixed_servers',
scheme: 'http',
host: '127.0.0.1',
port: 8080
});
}, []);
const handleSave = async () => {
try {
const values = await form.validateFields();
const newConfig = {
id: Date.now().toString(),
...values,
enabled: false
};
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.ADD_PROXY_CONFIG,
config: newConfig
});
if (response.success) {
message.success('添加成功');
window.close(); // 关闭窗口
}
} catch (error) {
console.error('Failed to add proxy:', error);
message.error('添加失败');
}
};
return (
<div className="add-proxy-form">
<Form
form={form}
layout="vertical"
onFinish={handleSave}
>
{/* 表单项与之前相同 */}
<Form.Item className="form-buttons">
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={() => window.close()}>
</Button>
</Form.Item>
</Form>
</div>
);
};
+73
View File
@@ -0,0 +1,73 @@
.Contro {
display: flex;
align-items: center;
height: 40px;
padding: 8px 16px;
font-size: 12px;
}
.Contro-error-bg {
background-color: rgba(244, 115, 107, .10);
}
.Contro-success-bg {
background-color: rgba(86, 201, 145, .10);
}
.Contro-cont-input {
height: 24px;
margin-right: 8px;
}
.Contro-lable {
color: #85899E;
}
.Contro-cont {
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
}
.Contro-cont-text-error {
color: #F6544A;
}
.Contro-cont-text-success {
color: #56C991;
}
.Contro-handle-icon {
display: flex;
align-items: center;
}
.Contro-handle-icon svg {
cursor: pointer;
}
.contro-handle-icon-check {
margin-left: 8px;
}
.icon-p {
display: inline-block;
padding: 4px;
}
.icon-p svg {
color: #85899E;
}
.Contro-cont span.anticon:hover {
background: #F0F1F3;
}
.grey-icon svg {
color: #85899E;
}
.icon-active:active svg {
color: #F28B44;
}
+191
View File
@@ -0,0 +1,191 @@
import React, {useEffect, useMemo, useRef, useState} from "react";
import {Divider, Tooltip, Input} from "antd";
import classNames from "classnames";
import {
CheckIcon,
ExitIcon,
PencilAltIcon,
RefreshIcon,
XIcon,
} from "@assets/icon/icon";
import {wsc} from "@network/chrome";
import "./Contro.css";
interface ControProps {
}
export const Contro: React.FC<ControProps> = () => {
const [isEdit, setIsEdit] = useState<boolean>(false);
const [connected, setConnected] = useState(false);
const [autoFindFailedReason, setAutoFindFailedReason] = useState<string>("");
const [enginePort, setEnginePort] = useState<string>("");
const [enginePortTemp, setEnginePortTemp] = useState<string>(enginePort);
const enginePortRef = useRef<string>(enginePort);
useEffect(() => {
enginePortRef.current = enginePort;
}, [enginePort]);
useEffect(() => {
const yakitConnectInfo = localStorage.getItem("yakit-connect");
if (!yakitConnectInfo) {
findPort(11212, 11222);
} else {
const {port, connected} = JSON.parse(yakitConnectInfo);
setConnected(connected);
setEnginePort(port + "");
setAutoFindFailedReason("");
}
wsc.onWSCMessage((message) => {
if (message.action === wsc.ActionType.STATUS) {
if (message.connected === false) {
handleConnectFail();
} else {
localStorage.setItem(
"yakit-connect",
JSON.stringify({connected: true, port: message.port})
);
setEnginePort(message.port + "");
setConnected(true);
setAutoFindFailedReason("");
}
}
});
}, []);
const findPort = (port: number, max: number) => {
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
ws.onclose = (e: CloseEvent) => {
if (enginePortRef.current) {
handleConnectFail();
return;
}
if (e.reason !== `FoundYakitWebSocketController` && port + 1 <= max) {
setTimeout(() => findPort(port + 1, max), 200);
}
if (port + 1 > max) {
setConnected(false);
setEnginePort("");
setAutoFindFailedReason("Cannot found Yakit");
localStorage.setItem("yakit-connect", "");
}
};
ws.onopen = () => {
setConnected(true);
setEnginePort(port + "");
ws.close(1000, "FoundYakitWebSocketController");
connectPort(port);
};
};
const handleConnectFail = () => {
setConnected(false);
setAutoFindFailedReason("Yakit WebSocket Controller Connect Fail");
localStorage.setItem("yakit-connect", "");
};
const connectPort = (port: number) => {
setAutoFindFailedReason("");
wsc.connect(port);
};
const safeConnected = useMemo(() => {
return connected && enginePort;
}, [connected, enginePort]);
const failConnected = useMemo(() => {
return !connected && autoFindFailedReason;
}, [connected, autoFindFailedReason]);
return (
<>
{safeConnected || failConnected ? (
<div
className={classNames("Contro", {
["Contro-success-bg"]: safeConnected,
["Contro-error-bg"]: failConnected,
})}
>
<div className="Contro-lable">Yakit </div>
<div className="Contro-cont">
{isEdit ? (
<>
<Input
rootClassName="Contro-cont-input"
value={enginePortTemp}
placeholder="输入范围 11212 - 11222"
onChange={(e) => {
const value = e.target.value;
setEnginePortTemp(value);
}}
/>
<div className="Contro-handle-icon">
<XIcon
className="grey-icon icon-p icon-active"
onClick={() => {
setIsEdit(false);
}}
/>
<CheckIcon
className="contro-handle-icon-check icon-p"
onClick={() => {
if (enginePortTemp) {
setIsEdit(false);
setEnginePort(enginePortTemp);
connectPort(Number(enginePortTemp));
}
}}
/>
</div>
</>
) : (
<>
<div
className={classNames("Contro-cont-text", {
["Contro-cont-text-success"]: safeConnected,
["Contro-cont-text-error"]: failConnected,
})}
>
{safeConnected && "已连接(" + enginePort + ""}
{failConnected &&
(enginePort
? autoFindFailedReason + "" + enginePort + ""
: autoFindFailedReason)}
</div>
<div className="Contro-handle-icon">
<Tooltip title="修改监听端口">
<PencilAltIcon
className="grey-icon icon-p icon-active"
onClick={() => {
setIsEdit(true);
setEnginePortTemp(enginePort);
}}
/>
</Tooltip>
<Divider type="vertical" style={{height: 16}}/>
{safeConnected && (
<ExitIcon onClick={() => wsc.disconnect()}/>
)}
{failConnected && (
<RefreshIcon
className="grey-icon icon-active"
onClick={() => {
if (enginePort) {
connectPort(Number(enginePort));
} else {
findPort(11212, 11222);
}
}}
/>
)}
</div>
</>
)}
</div>
</div>
) : null}
</>
);
};
-95
View File
@@ -1,95 +0,0 @@
import React, {useEffect} from "react";
import {Alert, Button, Card, Form, InputNumber} from "antd";
import {wsc} from "../network/chrome";
import {useGetState} from "ahooks";
export interface ControllerProp {
}
export const Controller: React.FC<ControllerProp> = (props) => {
const [findingPort, setFindingPort] = React.useState<boolean>(false)
const [autoFindFailedReason, setAutoFindFailedReason] = React.useState<string>("")
const [port, setPort] = React.useState<number>()
const [connected, setConnected, getConnected] = useGetState(false);
const [init, setInit] = React.useState(false)
useEffect(() => {
// control status
const updateStatus = () => {
wsc.updateWSCStatus()
}
updateStatus()
const id = setInterval(updateStatus, 500)
wsc.onWSCMessage((req: { connected: boolean }) => {
if (req['connected'] === undefined) {
return
}
setConnected(req.connected)
setTimeout(() => {
setInit(true)
}, 500)
})
return () => {
clearInterval(id)
}
}, [])
useEffect(() => {
if (!init || connected) {
return
}
const findPort = (port: number, max: number) => {
const ws = new WebSocket(`ws://127.0.0.1:${port}`)
ws.onclose = (e: CloseEvent) => {
if (e.reason !== `FoundYakitWebSocketController` && (port + 1 <= max)) {
setTimeout(() => findPort(port + 1, max), 300)
}
if (port + 1 > max) {
setFindingPort(false)
setAutoFindFailedReason("Cannot found Yakit or Yakit WebSocket Controller Port is not right")
}
}
ws.onopen = () => {
setFindingPort(false)
setPort(port)
ws.close(1000, "FoundYakitWebSocketController")
}
}
findPort(11212, 11222)
}, [connected, init])
const freeze = findingPort || (!init);
const safeConnected = connected && init;
return <Card size={"small"} extra={safeConnected ? <Button size={"small"} danger={true} onClick={() => {
wsc.disconnect()
}}>
Disconnect
</Button> : undefined}>
{connected && init ?
<Alert type={"success"} message={"Yakit Connected"}/> :
<Form
onSubmitCapture={e => {
e.preventDefault()
wsc.connect(port)
}}
size={"small"}
>
{autoFindFailedReason !== '' ? <Alert type={"error"} message={autoFindFailedReason}/> : undefined}
<Form.Item label={"Controller Port"}>
<InputNumber disabled={freeze} value={port} onChange={e => setPort(e)}/>
</Form.Item>
<Form.Item>
<Button size={"small"} type={"primary"} loading={freeze} htmlType={"submit"}>Connect to
Yakit</Button>
</Form.Item>
</Form>}
</Card>
};
+81
View File
@@ -0,0 +1,81 @@
import React, {useEffect, useState} from "react";
import {Button, Input} from "antd";
import TextArea from "antd/lib/input/TextArea";
import {wsc} from "@network/chrome";
interface EvalInTabProps {
}
export const EvalInTab: React.FC<EvalInTabProps> = () => {
const [funcName, setFuncName] = useState("");
const [inputArgsData, setInputArgsData] = useState("");
const [code, setCode] = useState("");
useEffect(() => {
wsc.onWSCMessage((message) => {
console.log("message from content script:", message)
if (message.action === wsc.ActionType.TO_EXTENSION_PAGE) {
console.log("eval in tab:", message.result);
// alert("from content script: " + JSON.stringify(message.result));
// 发送结果
wsc.sendMessage({"type": "chrome-extension", res: message.result})
}
});
}, []);
const handleClick = async () => {
try {
const [tab] = await wsc.getTab();
await chrome.runtime.sendMessage({
action: wsc.ActionType.INJECT_SCRIPT,
tabId: tab.id,
value: {mode: "CONTENT_CALL_FUNCTION", fn_name: funcName, args: inputArgsData},
});
} catch (error) {
console.log("error:", error)
}
};
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputArgsData(event.target.value);
};
const handleEvalCodeClick = async () => {
try {
const [tab] = await wsc.getTab();
await chrome.runtime.sendMessage({
action: wsc.ActionType.INJECT_SCRIPT,
tabId: tab.id,
value: {
mode: "CONTENT_EVAL_CODE",
code: code,
},
});
} catch (error) {
}
}
return (
<div>
<Input
value={funcName}
onChange={(e) => setFuncName(e.target.value)}
placeholder="Enter function name"
></Input>
<TextArea
value={inputArgsData}
onChange={handleInputChange}
placeholder="Enter function args"
/>
<Button onClick={handleClick}>eval func in tab</Button>
<TextArea
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter your expression (e.g., 22 * 33)"
/>
<Button onClick={handleEvalCodeClick}>eval code in tab</Button>
</div>
);
};
+113
View File
@@ -0,0 +1,113 @@
.Prox-title-wrap {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 24px 8px;
}
.Prox-title-wrap-left {
display: flex;
align-items: center;
}
.Prox-title-wrap-left .prox-title {
margin-right: 4px;
color: #31343F;
font-size: 14px;
font-weight: 600;
}
.Prox-title-wrap-left .prox-number {
display: inline-block;
width: 18px;
height: 16px;
text-align: center;
color: #85899E;
border-radius: 8px;
background: #F0F1F3;
font-size: 10px;
}
.Prox-title-wrap-right {
display: flex;
align-items: center;
cursor: pointer;
}
.Prox-title-wrap-right .Prox-add-text {
margin-right: 4px;
color: var(--yakit-primary);
font-size: 12px;
font-weight: 500;
}
.Prox-title-wrap-right .Prox-add-icon svg {
color: var(--yakit-primary);
}
.Prox-list-wrap {
overflow-y: auto;
max-height: 305px;
padding: 16px;
padding-top: 0;
}
.add-list {
display: flex;
justify-content: center;
align-items: center;
height: 28px;
cursor: pointer;
border: 1px solid #EAECF3;
border-radius: 4px;
background-color: #FFFFFF;
}
.add-list:hover {
border: 1px solid var(--yakit-primary);
}
.add-list .add-list-icon svg {
color: #85899E;
}
.Prox-list-wrap .add-list .add-list-text {
margin-left: 4px;
color: #31343F;
font-size: 14px;
font-weight: 600;
}
.Prox-list-wrap .Prox-list-item-wrap {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 8px;
border-radius: 8px;
}
.Prox-list-wrap .Prox-list-item-wrap:hover {
background-color: #F8F8F8;
}
.Prox-list-item-space .ant-space-item .ant-space-compact {
height: 28px;
}
.Prox-list-item-space .ant-space-item .ant-space-compact .ant-select-single {
height: 28px;
}
.Prox-list-wrap .Prox-list-item-wrap:hover .proxy-list-del-icon {
display: block;
}
.proxy-list-del-icon {
display: none;
cursor: pointer;
padding: 4px;
}
.proxy-list-del-icon:hover {
background: #F0F1F3;
}
+296 -78
View File
@@ -1,93 +1,311 @@
import React, {useEffect, useState} from "react";
import {Button, Card, Form, Input, InputNumber, Select, Space} from "antd";
import {wsc} from "../network/chrome";
export interface ProxifierProp {
}
const {Compact} = Space;
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 {
scheme: "http" | "socks5";
id: string;
scheme: Scheme;
host: string;
port: number;
port: string;
hostStatus: "error" | "";
portStatus: "error" | "";
open: boolean;
proxy: string;
}
export const Proxifier: React.FC<ProxifierProp> = (props) => {
const [config, setConfig] = useState<ProxyConfig>({
scheme: "http",
host: "127.0.0.1",
port: 8083
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 [init, setInit] = useState(false);
const [proxyEnable, setProxyEnable] = useState(false);
const [currentProxy, setCurrentProxy] = useState("");
useEffect(() => {
if (init) {
return
}
localStorage.setItem("yakit-proxy-list", JSON.stringify(proxyList));
}, [proxyList]);
wsc.onProxyStatusMessage(msg => {
if (msg['proxy'] === undefined || msg['enable'] === undefined) {
return
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;
};
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) => {
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;
}
setInit(true)
setProxyEnable(msg.enable)
setCurrentProxy(msg.proxy)
})
}, [init])
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
);
}
}
}
// proxy
useEffect(() => {
const update = () => {
wsc.updateProxyStatus()
}
update()
const id = setInterval(update, 500)
return () => {
clearInterval(id)
}
}, [])
if (newProxyItem) {
copyProxyList.unshift(newProxyItem);
}
setProxyList(copyProxyList);
}
});
}, []);
return <Card title={`Proxy Set: ${proxyEnable ? currentProxy : "Non-Config"}`} size={"small"} extra={<>{
proxyEnable ? <Button size={"small"} onClick={() => {
wsc.clearproxy()
}}></Button> : undefined
}</>}>
<Form size={"small"} onSubmitCapture={e => {
e.preventDefault()
setInit(false)
wsc.setproxy(config.scheme, config.host, config.port)
}} disabled={!init || proxyEnable}>
<Form.Item label={"Proxy Setting"}>
<Compact>
<Select
style={{width: 86}}
value={config.scheme}
onChange={e => (setConfig({...config, scheme: e}))}
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 === ""
}
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"
),
]);
}}
>
<Select.Option value={"http"}>HTTP</Select.Option>
<Select.Option value={"socks5"}>Socks5</Select.Option>
</Select>
<Input
style={{width: 100}}
placeholder={"ProxyHost"}
value={config.host}
onChange={e => (setConfig({...config, host: e.target.value}))}/>
<InputNumber
style={{width: 65}}
placeholder={"Port"}
value={config.port}
onChange={e => (setConfig({...config, port: e}))}
/>
</Compact>
</Form.Item>
<Form.Item>
<Button type={"primary"} htmlType={"submit"} loading={!init}>Enable Proxy</Button>
</Form.Item>
</Form>
</Card>
};
<PlusSmIcon className="add-list-icon"/>
<span className="add-list-text"></span>
</div>
)}
</div>
</div>
);
};
+235
View File
@@ -0,0 +1,235 @@
.proxy-container {
min-width: 200px;
background: white;
}
.proxy-menu {
border: none !important;
box-shadow: none !important;
}
.menu-item {
height: 40px !important;
line-height: 40px !important;
margin: 0 !important;
padding: 0 16px !important;
}
.menu-item .anticon {
font-size: 16px;
color: var(--yakit-primary);
margin-right: 8px;
}
.menu-item-label {
font-size: 14px;
color: #333;
}
.menu-item:hover {
background-color: var(--yakit-primary-5) !important;
}
.menu-item:hover .anticon,
.menu-item:hover .menu-item-label {
color: var(--yakit-primary) !important;
}
/* 选中状态 */
.menu-item.ant-menu-item-selected {
background-color: var(--yakit-primary) !important;
}
.menu-item.ant-menu-item-selected .anticon,
.menu-item.ant-menu-item-selected .menu-item-label {
color: white !important;
}
.menu-item.ant-menu-item-selected:hover {
background-color: var(--yakit-primary-hover) !important;
}
/* 分隔线 */
.ant-menu-item-divider {
margin: 4px 0 !important;
border-color: #EAECF3 !important;
}
/* 设置选项 */
.menu-item-setting {
border-top: 1px solid #EAECF3;
margin-top: 4px !important;
}
.menu-item-setting .anticon {
color: #666;
}
.menu-item-setting:hover {
background-color: var(--yakit-primary-5) !important;
}
.menu-item-setting:hover .anticon,
.menu-item-setting:hover .menu-item-label {
color: var(--yakit-primary) !important;
}
/* 调整图标大小和对齐 */
.anticon {
font-size: 16px;
}
/* 添加以下样式来确保下拉菜单显示在正确的位置 */
.ant-dropdown {
position: absolute !important;
top: 100% !important;
left: 0 !important;
width: 100% !important;
min-width: 200px !important;
}
.dropdown-content {
background: white;
border-radius: 4px;
box-shadow: 0 3px 6px -4px rgba(0,0,0,0.12),
0 6px 16px 0 rgba(0,0,0,0.08),
0 9px 28px 8px rgba(0,0,0,0.05);
padding: 4px;
}
/* 确保容器不会限制弹出层 */
.proxy-container {
min-width: 200px;
background: white;
border-radius: 4px;
position: static;
}
/* 添加这个样式来确保下拉菜单显示在正确的位置 */
body {
position: relative;
}
.ant-menu {
border: none !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
padding: 4px 0 !important;
width: 180px !important;
background: white !important;
}
.ant-menu-item {
height: 28px !important;
line-height: 28px !important;
margin: 0 !important;
padding: 0 16px !important;
}
.ant-menu-item:hover {
background-color: #f5f5f5 !important;
}
.menu-icon {
margin-right: 8px;
display: inline-flex;
align-items: center;
}
.menu-item-selected {
background-color: #e6f7ff !important;
}
.ant-menu-item-divider {
margin: 4px 0 !important;
height: 1px !important;
background-color: #f0f0f0 !important;
}
.ant-menu-item:last-child {
margin-top: 4px !important;
border-top: 1px solid #f0f0f0;
}
/* 移除多余的样式 */
.ant-menu-root {
background: transparent !important;
}
/* 调整整体容器大小 */
.ant-menu-root {
width: 180px !important;
min-height: auto !important;
}
/* 添加代理按钮样式 */
.menu-item-add {
color: #666 !important;
}
.menu-item-add:hover {
background-color: #f5f5f5 !important;
color: var(--yakit-primary) !important;
}
.menu-item-add .anticon {
color: #666;
}
.menu-item-add:hover .anticon {
color: var(--yakit-primary);
}
.menu-loading {
pointer-events: none;
opacity: 0.7;
}
.menu-item-loading {
transition: all 0.3s ease;
}
.menu-item-selected {
transition: all 0.3s ease;
background-color: var(--yakit-primary-5) !important;
}
/* 添加过渡效果 */
.ant-menu-item {
transition: all 0.3s ease !important;
}
.ant-menu-item .menu-icon {
transition: color 0.3s ease;
}
.proxy-switch-container {
position: relative;
width: 180px;
overflow: hidden;
}
.panel-watermark {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
opacity: 0.03;
pointer-events: none;
object-fit: contain;
object-position: right bottom;
z-index: 0;
}
/* 确保菜单项在水印上层 */
.ant-menu-item {
position: relative;
z-index: 1;
background: transparent !important;
}
/* 确保分割线在水印上层 */
.ant-menu-item-divider {
position: relative;
z-index: 1;
}
+311
View File
@@ -0,0 +1,311 @@
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: currentMode === mode.key ? 'var(--yakit-primary)' : mode.color
}}>{mode.icon}</span>,
label: `${mode.name}${currentMode === mode.key ? ' ✅' : ''}`,
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: currentMode === proxy.key ? 'var(--yakit-primary)' : 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}{currentMode === proxy.key ? ' ✅' : ''}</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>
);
};
+17 -3
View File
@@ -1,13 +1,27 @@
html:root {
--yakit-primary: #F28B44
}
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;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
*,
*::before,
*::after {
box-sizing: border-box;
}
#root {
width: 180px;
height: fit-content;
background: white;
overflow: hidden;
}
-7
View File
@@ -1,7 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
// 获取根元素
const container = document.getElementById('root');
if (!container) throw new Error('Failed to find the root element');
// 创建根
const root = createRoot(container);
// 渲染应用,移除 StrictMode
root.render(<App />);
+33 -11
View File
@@ -1,29 +1,48 @@
import {chrome} from "./chromeapi";
export namespace wsc {
export enum ActionType {
CONNECT = 'connect',
SEND_MESSAGE = 'send_message',
DISCONNECT = 'disconnect',
STATUS = 'status',
PROXY_STATUS = 'proxy_status',
SET_PROXY = 'set_proxy',
CLEAR_PROXY = 'clear_proxy',
INJECT_SCRIPT = 'yakit_inject_script',
// 用于接收来自content script的消息
TO_EXTENSION_PAGE = "yakit_to_extension_page",
}
export function connect(port: number, host?: string) {
chrome.runtime.sendMessage({
action: 'connect',
action: ActionType.CONNECT,
host: host || '127.0.0.1',
port: port,
});
}
export function sendMessage(message: any) {
chrome.runtime.sendMessage({
action: ActionType.SEND_MESSAGE,
message: message,
});
}
export function disconnect() {
chrome.runtime.sendMessage({
action: 'disconnect',
action: ActionType.DISCONNECT,
});
}
export function updateWSCStatus() {
chrome.runtime.sendMessage({
action: 'status',
action: ActionType.STATUS,
});
}
export function updateProxyStatus() {
chrome.runtime.sendMessage({
action: 'proxystatus',
action: ActionType.PROXY_STATUS,
});
}
@@ -35,13 +54,16 @@ export namespace wsc {
chrome.runtime.onMessage.addListener(onMessage)
}
export function setproxy(scheme: string, host: string, port: number) {
chrome.runtime.sendMessage({action: "setproxy", scheme, host, port})
export function setProxy(scheme: string, host: string, port: number) {
chrome.runtime.sendMessage({action: ActionType.SET_PROXY, scheme, host, port})
}
export function clearproxy() {
chrome.runtime.sendMessage({action: 'clearproxy'})
export function clearProxy() {
chrome.runtime.sendMessage({action: ActionType.CLEAR_PROXY})
}
// 获取当前tab
export async function getTab() {
return chrome.tabs.query({active: true, currentWindow: true})
}
}
-74
View File
@@ -1,74 +0,0 @@
export interface MockMessageSender {
id?: string;
}
export interface MockChromeRuntime {
sendMessage: (message: any, responseCallback?: (response: any) => void) => void;
onMessage: {
addListener: (callback: (message: any, sender: MockMessageSender, sendResponse: (response?: any) => void) => void) => void;
};
}
export interface MockChromeProxy {
settings: {
set: (value: { value: any, scope: string }) => void;
get: (details: { incognito?: boolean }, callback: (config: any) => void) => void;
};
}
export interface MockChromeTabs {
query: (queryInfo: any, callback: (result: any) => void) => void;
create: (createProperties: any, callback?: (tab: any) => void) => void;
}
export interface MockChrome {
runtime: MockChromeRuntime;
proxy: MockChromeProxy;
tabs: MockChromeTabs;
}
declare global {
interface Window {
chrome?: any;
}
}
export const chrome: MockChrome = typeof window.chrome !== 'undefined' && window.chrome.runtime ? window.chrome : {
runtime: {
sendMessage: (message, responseCallback) => {
console.log('Mock sendMessage called with:', message);
if (responseCallback) responseCallback('response from mock');
},
onMessage: {
addListener: (callback) => {
console.log('Mock onMessage.addListener called');
callback({connected: true}, undefined, undefined)
// You can simulate incoming messages here if needed
}
}
},
proxy: {
settings: {
set: (value) => {
console.log('Mock proxy settings set with:', value);
},
get: (details, callback) => {
console.log('Mock proxy settings get called');
// Simulate proxy settings response
callback({mode: 'direct'});
}
}
},
tabs: {
query: (queryInfo, callback) => {
console.log('Mock tabs.query called with:', queryInfo);
// Simulate a tab query response
callback([{id: 1, url: 'http://example.com', title: 'Example'}]);
},
create: (createProperties, callback) => {
console.log('Mock tabs.create called with:', createProperties);
// Simulate creating a tab
if (callback) callback({id: 2, url: createProperties.url});
}
}
};
@@ -0,0 +1,166 @@
import React from 'react';
import { Modal, Button, Space, Descriptions, Tabs, Card, Typography, message } from 'antd';
import { ProxyLog } from '@/types/proxy';
const { Text, Paragraph } = Typography;
interface LogDetailProps {
log: ProxyLog | null;
onClose: () => void;
}
export const LogDetail: React.FC<LogDetailProps> = ({ log, onClose }) => {
const renderHttpRequest = (log: ProxyLog) => {
if (!log) return '';
// 构建请求头
const headers = Object.entries(log.requestHeaders || {})
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
// 构建完整的 HTTP 请求
return `${log.method || 'GET'} ${log.url} ${log.protocol || 'HTTP/1.1'}
${headers}
${log.requestBody || ''}`;
};
const renderHttpResponse = (log: ProxyLog) => {
if (!log || !log.responseHeaders) return '';
// 构建响应头
const headers = Object.entries(log.responseHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
// 构建完整的 HTTP 响应
return `HTTP/1.1 ${log.status === 'success' ? '200 OK' : '500 Error'}
${headers}
${log.responseBody || ''}`;
};
const handleCopyRaw = () => {
if (!log) return;
navigator.clipboard.writeText(renderHttpRequest(log))
.then(() => message.success('已复制到剪贴板'))
.catch(() => message.error('复制失败'));
};
const formatProxyInfo = (log: ProxyLog) => {
if (!log) return '';
return `${log.proxyName}${log.host ? ` - ${log.host}:${log.port}` : ''}`;
};
return (
<Modal
title="请求详情"
open={!!log}
onCancel={onClose}
width={800}
footer={[
<Button key="copy" onClick={handleCopyRaw}>
</Button>,
<Button key="close" onClick={onClose}>
</Button>
]}
>
{log && (
<Space direction="vertical" style={{ width: '100%' }}>
<Descriptions bordered column={2}>
<Descriptions.Item label="请求时间">
{new Date(log.timestamp).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="代理服务器">
{formatProxyInfo(log)}
</Descriptions.Item>
<Descriptions.Item label="状态">
<Text type={log.status === 'success' ? 'success' : 'danger'}>
{log.status === 'success' ? '成功' : '失败'}
</Text>
</Descriptions.Item>
<Descriptions.Item label="响应时间">
{log.timing?.duration}ms
</Descriptions.Item>
{log.errorMessage && (
<Descriptions.Item label="错误信息" span={2}>
<Text type="danger">{log.errorMessage}</Text>
</Descriptions.Item>
)}
</Descriptions>
<Tabs
items={[
{
key: 'request',
label: '请求数据',
children: (
<Card size="small">
<pre style={{
background: '#f5f5f5',
padding: 16,
borderRadius: 4,
maxHeight: 400,
overflow: 'auto',
margin: 0
}}>
{renderHttpRequest(log)}
</pre>
</Card>
)
},
{
key: 'response',
label: '响应数据',
children: (
<Card size="small">
<pre style={{
background: '#f5f5f5',
padding: 16,
borderRadius: 4,
maxHeight: 400,
overflow: 'auto',
margin: 0
}}>
{renderHttpResponse(log)}
</pre>
</Card>
)
},
{
key: 'timing',
label: '性能数据',
children: (
<Card size="small">
<Descriptions bordered>
<Descriptions.Item label="开始时间">
{new Date(log.timing?.startTime || 0).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="结束时间">
{new Date(log.timing?.endTime || 0).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="总耗时">
{log.timing?.duration}ms
</Descriptions.Item>
<Descriptions.Item label="IP地址">
{log.ip || '-'}
</Descriptions.Item>
<Descriptions.Item label="协议">
{log.protocol || '-'}
</Descriptions.Item>
<Descriptions.Item label="缓存">
{log.fromCache ? '是' : '否'}
</Descriptions.Item>
</Descriptions>
</Card>
)
}
]}
/>
</Space>
)}
</Modal>
);
};
@@ -0,0 +1,131 @@
import React, { useState } from 'react';
import { Table, Button, Space } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { DeleteOutlined, FilterFilled } from '@ant-design/icons';
import { ProxyLog } from '@/types/proxy';
import { LogDetail } from './LogDetail';
interface ProxyLogsProps {
logs: ProxyLog[];
onClearLogs: () => void;
}
export const ProxyLogs: React.FC<ProxyLogsProps> = ({
logs,
onClearLogs
}) => {
const [selectedLog, setSelectedLog] = useState<ProxyLog | null>(null);
const [resourceFilter, setResourceFilter] = useState<string[]>([]);
const columns: ColumnsType<ProxyLog> = [
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
render: (timestamp: number) => new Date(timestamp).toLocaleString()
},
{
title: 'URL',
dataIndex: 'url',
key: 'url',
ellipsis: true
},
{
title: (
<Space>
{resourceFilter.length > 0 && <FilterFilled style={{ color: '#f50' }} />}
</Space>
),
dataIndex: 'resourceType',
key: 'resourceType',
render: (type: string) => {
const typeMap: Record<string, string> = {
xhr: 'XHR',
fetch: 'Fetch',
script: 'JS',
stylesheet: 'CSS',
image: 'Image',
other: 'Other'
};
return typeMap[type] || 'Other';
},
filters: [
{ text: 'XHR', value: 'xhr' },
{ text: 'Fetch', value: 'fetch' },
{ text: 'JS', value: 'script' },
{ text: 'CSS', value: 'stylesheet' },
{ text: 'Image', value: 'image' },
{ text: 'Other', value: 'other' }
],
filterMode: 'menu' as const,
filtered: resourceFilter.length > 0,
onFilter: (value: string, record: ProxyLog) => record.resourceType === value
},
{
title: '使用代理',
dataIndex: 'proxyName',
key: 'proxyName'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<span style={{ color: status === 'success' ? '#52c41a' : '#ff4d4f' }}>
{status === 'success' ? '成功' : '失败'}
</span>
)
},
{
title: '错误信息',
dataIndex: 'errorMessage',
key: 'errorMessage',
ellipsis: true
}
];
const filteredLogs = logs.filter(log => {
if (resourceFilter.length === 0) return true;
return resourceFilter.includes(log.resourceType || 'other');
});
return (
<Space direction="vertical" style={{ width: '100%' }}>
<div style={{
marginBottom: 16,
display: 'flex',
justifyContent: 'flex-end'
}}>
<Button
danger
onClick={onClearLogs}
icon={<DeleteOutlined />}
>
</Button>
</div>
<Table
dataSource={filteredLogs}
columns={columns}
onRow={(record) => ({
onClick: () => setSelectedLog(record),
style: { cursor: 'pointer' }
})}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
}}
rowKey="id"
/>
<LogDetail
log={selectedLog}
onClose={() => setSelectedLog(null)}
/>
</Space>
);
};
@@ -0,0 +1,65 @@
/* 表格样式 */
.proxy-table {
background: white;
border-radius: 8px;
}
/* 表头样式 */
.proxy-table .ant-table-thead > tr > th {
background: white !important;
color: #333;
font-weight: 500;
border-bottom: 1px solid var(--border-color);
padding: 12px 16px;
}
/* 斑马纹样式 */
.proxy-table .ant-table-tbody > tr:nth-child(even) {
background-color: #fafafa;
}
/* 单元格样式 */
.proxy-table .ant-table-tbody > tr > td {
border-bottom: 1px solid var(--border-color);
padding: 12px 16px;
}
/* 操作列图标样式 */
.action-icon {
font-size: 16px;
cursor: pointer;
color: #666;
transition: all 0.3s;
}
.action-icon:hover {
color: var(--yakit-primary);
transform: scale(1.1);
}
.action-icon.enabled {
color: var(--yakit-primary);
}
.action-icon.delete:hover {
color: #ff4d4f;
}
/* 间距调整 */
.ant-space-middle {
gap: 16px !important;
}
/* 添加按钮样式 */
.add-proxy-btn {
background-color: var(--yakit-primary);
color: white;
border: none;
border-radius: 4px;
margin-bottom: 16px;
}
.add-proxy-btn:hover {
background-color: var(--yakit-primary-hover) !important;
color: white !important;
}
@@ -0,0 +1,461 @@
import React, { useRef, useState, useEffect, useCallback } from 'react';
import { Card, Input, Space, Button, Select, InputNumber, Form, Table, Tooltip, Popover, Modal, message } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { DeleteOutlined, PlusOutlined, EditOutlined, CheckOutlined } from '@ant-design/icons';
import { ProxyConfig } from '@/types/proxy';
import './index.css';
import punycode from 'punycode';
interface ProxySettingsProps {
proxyConfigs: ProxyConfig[];
onAdd: (config: ProxyConfig) => void;
onChange: (configId: string, field: keyof ProxyConfig | 'config', value: any) => void;
onDelete: (configId: string) => void;
onApply: (configId: string) => Promise<void>;
onClear: (configId: string) => Promise<void>;
}
// 修改 EditFormData 接口
interface EditFormData {
name: string;
proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect";
scheme?: "http" | "https" | "socks4" | "socks5";
host?: string;
port?: number;
pacScript?: string;
bypassList?: string;
matchList?: string; // 仅用于 UI 编辑
proxyServer?: string; // 添加 proxyServer 字段,用于 PAC 脚本模式选择代理服务器
}
// 或者更好的方式是创建一个专门的类型
type ProxyConfigField = keyof ProxyConfig | 'config';
export const ProxySettings: React.FC<ProxySettingsProps> = ({
proxyConfigs,
onAdd,
onChange,
onDelete,
onApply,
onClear
}) => {
const [editingConfig, setEditingConfig] = useState<ProxyConfig | null>(null);
const [editModalVisible, setEditModalVisible] = useState(false);
const [form] = Form.useForm<EditFormData>();
// 添加 useEffect 来监听表单值变化
useEffect(() => {
if (editModalVisible && editingConfig) {
form.setFieldsValue({
name: editingConfig.name,
proxyType: editingConfig.proxyType,
scheme: editingConfig.scheme,
host: editingConfig.host,
port: editingConfig.port,
bypassList: editingConfig.bypassList?.join('\n') || '',
matchList: editingConfig.matchList?.join('\n') || '',
proxyServer: editingConfig.host && editingConfig.port
? `${editingConfig.host}:${editingConfig.port}`
: undefined
});
}
}, [editModalVisible, editingConfig, form]);
// 处理添加按钮点击
const handleAdd = useCallback(() => {
setEditingConfig({
id: Date.now().toString(),
name: '',
proxyType: 'fixed_servers',
scheme: 'http' as "http" | "https" | "socks4" | "socks5",
host: '127.0.0.1',
port: 8080,
enabled: false
});
setEditModalVisible(true);
}, []);
// 处理编辑按钮点击
const handleEdit = (record: ProxyConfig) => {
setEditingConfig(record);
setEditModalVisible(true);
};
// 添加一个函数来获取可用的代理服务器列表
const getAvailableProxies = (configs: ProxyConfig[]) => {
return configs
.filter(config => config.proxyType === 'fixed_servers')
.map(config => ({
label: `${config.name} (${config.scheme}://${config.host}:${config.port})`,
value: `${config.host}:${config.port}`,
config
}));
};
// 处理编辑保存
const handleEditSave = async () => {
try {
const values = await form.validateFields();
if (editingConfig) {
if (editingConfig.enabled) {
await onClear(editingConfig.id);
}
let updatedConfig: ProxyConfig;
if (values.proxyType === 'fixed_servers') {
// 处理固定代理服务器模式
const bypassList = values.bypassList
? values.bypassList.split('\n').map(line => line.trim()).filter(line => line.length > 0)
: ["localhost", "127.0.0.1"];
updatedConfig = {
id: editingConfig.id,
name: values.name,
enabled: editingConfig.enabled,
proxyType: 'fixed_servers',
scheme: values.scheme,
host: values.host,
port: values.port,
bypassList,
};
} else if (values.proxyType === 'pac_script') {
const domains = values.matchList
? values.matchList.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.map(domain => {
try {
// 如果域名包含非 ASCII 字符,转换为 Punycode
if (/[^\x00-\x7F]/.test(domain)) {
if (domain.startsWith('*.')) {
const suffix = domain.substring(2);
return '*.' + suffix.split('.').map(part => {
return /[^\x00-\x7F]/.test(part) ? 'xn--' + punycode.encode(part) : part;
}).join('.');
} else {
return domain.split('.').map(part => {
return /[^\x00-\x7F]/.test(part) ? 'xn--' + punycode.encode(part) : part;
}).join('.');
}
}
return domain;
} catch (error) {
console.error('Error encoding domain:', domain, error);
return domain;
}
})
: [];
// 从选择的代理服务器中获取配置
const [host, port] = values.proxyServer.split(':');
// 生成 PAC 脚本
const pacScriptContent = `
function FindProxyForURL(url, host) {
// Convert host to lowercase for case-insensitive matching
host = host.toLowerCase();
// Define domain patterns
var domains = ${JSON.stringify(domains)};
// Check each domain pattern
for (var i = 0; i < domains.length; i++) {
var pattern = domains[i].toLowerCase();
if (pattern.startsWith('*.')) {
var suffix = pattern.substring(2);
if (host === suffix || host.endsWith('.' + suffix)) {
return 'PROXY ${host}:${port}';
}
} else if (host === pattern) {
return 'PROXY ${host}:${port}';
}
}
return 'DIRECT';
}`;
updatedConfig = {
id: editingConfig.id,
name: values.name,
enabled: editingConfig.enabled,
proxyType: 'pac_script',
mode: 'pac_script',
// 保存代理服务器信息
host,
port: parseInt(port),
// 保存匹配域名列表
matchList: domains,
pacScript: {
data: pacScriptContent,
mandatory: true
}
};
} else {
// 处理其他模式
updatedConfig = {
id: editingConfig.id,
name: values.name,
enabled: editingConfig.enabled,
proxyType: values.proxyType,
bypassList: [], // 其他模式下设置为空数组
};
}
if (!proxyConfigs.find(config => config.id === editingConfig.id)) {
await onAdd(updatedConfig);
} else {
await onChange(editingConfig.id, 'config', updatedConfig);
}
setEditModalVisible(false);
setEditingConfig(null);
form.resetFields();
}
} catch (error) {
console.error('Validate Failed:', error);
message.error('保存失败,请检查表单');
}
};
// 处理模态框关闭
const handleModalClose = () => {
form.resetFields();
setEditModalVisible(false);
setEditingConfig(null);
};
const columns: ColumnsType<ProxyConfig> = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (text: string) => text
},
{
title: '类型',
dataIndex: 'proxyType',
key: 'proxyType',
render: (text: string) => {
const typeMap = {
direct: '直接连接',
fixed_servers: '代理服务器',
pac_script: 'PAC 脚本'
};
return typeMap[text as keyof typeof typeMap] || text;
}
},
{
title: '协议',
dataIndex: 'scheme',
key: 'scheme'
},
{
title: '主机',
dataIndex: 'host',
key: 'host'
},
{
title: '端口',
dataIndex: 'port',
key: 'port'
},
{
title: '操作',
key: 'action',
width: 120,
render: (_, record: ProxyConfig) => (
<Space size="middle">
<CheckOutlined
className={`action-icon ${record.enabled ? 'enabled' : ''}`}
onClick={async () => {
if (record.enabled) {
await onClear(record.id);
} else {
await onApply(record.id);
}
}}
/>
<EditOutlined
className="action-icon"
onClick={() => handleEdit(record)}
/>
{record.id !== 'direct' && (
<DeleteOutlined
className="action-icon delete"
onClick={() => onDelete(record.id)}
/>
)}
</Space>
)
}
];
const buttonRef = useRef(null);
// 过滤掉固定模式的代理
const filteredProxyConfigs = proxyConfigs.filter(
config => !['direct', 'system'].includes(config.id)
);
return (
<div>
<Space style={{ marginBottom: 16, justifyContent: 'flex-end', width: '100%' }}>
<Button
className="add-proxy-btn"
onClick={handleAdd}
icon={<PlusOutlined />}
>
</Button>
</Space>
<Table
className="proxy-table"
dataSource={filteredProxyConfigs}
columns={columns}
rowKey="id"
pagination={false}
bordered={false}
size="middle"
/>
<Modal
title="编辑代理配置"
open={editModalVisible}
onOk={handleEditSave}
onCancel={handleModalClose}
destroyOnClose
>
<Form
form={form}
layout="vertical"
preserve={false}
>
<Form.Item
name="name"
label="名称"
rules={[{ required: true }]}
>
<Input />
</Form.Item>
<Form.Item
name="proxyType"
label="类型"
rules={[{ required: true }]}
>
<Select
options={[
// { label: '直接连接', value: 'direct' },
{ label: '代理服务器', value: 'fixed_servers' },
{ label: 'PAC 脚本', value: 'pac_script' }
]}
onChange={(value) => {
// 当类型改变时,清除相关字段
if (value !== 'fixed_servers') {
form.setFieldsValue({
scheme: undefined,
host: undefined,
port: undefined
});
}
}}
/>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.proxyType !== currentValues.proxyType}
>
{({ getFieldValue }) => {
const proxyType = getFieldValue('proxyType');
if (proxyType === 'fixed_servers') {
return (
<>
<Form.Item
name="scheme"
label="协议"
rules={[{ required: true, message: '请选择协议' }]}
>
<Select
options={[
{ label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' },
{ label: 'SOCKS4', value: 'socks4' },
{ label: 'SOCKS5', value: 'socks5' }
]}
/>
</Form.Item>
<Form.Item
name="host"
label="主机"
rules={[{ required: true, message: '请输入主机地址' }]}
>
<Input placeholder="127.0.0.1" />
</Form.Item>
<Form.Item
name="port"
label="端口"
rules={[{ required: true, message: '请输入端口号' }]}
>
<InputNumber
min={1}
max={65535}
placeholder="8080"
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
name="bypassList"
label="不经过代理的地址"
help="每行一个地址,支持通配符 *"
>
<Input.TextArea
rows={4}
placeholder={`例如:
localhost
127.0.0.1
*.example.com`}
/>
</Form.Item>
</>
);
} else if (proxyType === 'pac_script') {
const availableProxies = getAvailableProxies(proxyConfigs);
return (
<>
<Form.Item
name="matchList"
label="匹配域名"
help="每行一个域名,支持通配符 *"
rules={[{ required: true, message: '请输入至少一个匹配域名' }]}
>
<Input.TextArea
rows={4}
placeholder={`例如:
*.example.com
google.com
github.com`}
/>
</Form.Item>
<Form.Item
name="proxyServer"
label="选择代理服务器"
rules={[{ required: true, message: '请选择代理服务器' }]}
>
<Select
placeholder="选择一个代理服务器"
options={availableProxies}
/>
</Form.Item>
</>
);
}
return null;
}}
</Form.Item>
</Form>
</Modal>
</div>
);
};
@@ -0,0 +1,167 @@
import { useState, useEffect } from 'react';
import { ProxyConfig } from '@/types/proxy';
import { ProxyActionType } from '@/types/action';
import { message } from 'antd';
export const useProxyConfigs = () => {
const [proxyConfigs, setProxyConfigs] = useState<ProxyConfig[]>([]);
useEffect(() => {
loadConfigs();
const messageListener = (message: any) => {
if (message.action === 'PROXY_CONFIGS_UPDATED') {
loadConfigs();
}
};
chrome.runtime.onMessage.addListener(messageListener);
return () => {
chrome.runtime.onMessage.removeListener(messageListener);
};
}, []);
const loadConfigs = async () => {
try {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.GET_PROXY_CONFIGS
});
if (response.success) {
setProxyConfigs(response.data || []);
}
} catch (error) {
console.error('Error loading configs:', error);
message.error('加载配置时发生错误');
}
};
const handleAddProxy = async (config: ProxyConfig) => {
try {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.ADD_PROXY_CONFIG,
config: config
});
if (response.success) {
message.success('添加代理成功');
loadConfigs();
} else {
message.error(response.error || '添加代理失败');
}
} catch (error) {
console.error('Error adding proxy:', error);
message.error('添加代理时发生错误');
}
};
const handleConfigChange = async (configId: string, field: keyof ProxyConfig, value: any) => {
try {
const updatedConfigs = proxyConfigs.map(config =>
config.id === configId ? { ...config, [field]: value } : config
);
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.UPDATE_PROXY_CONFIG,
configs: updatedConfigs
});
if (response?.success) {
setProxyConfigs(response.data || updatedConfigs);
await chrome.runtime.sendMessage({
action: 'PROXY_CONFIGS_UPDATED'
});
message.success('更新配置成功');
} else {
message.error(response?.error || '更新配置失败');
await loadConfigs();
}
} catch (error) {
console.error('Error updating config:', error);
message.error('更新配置时发生错误');
await loadConfigs();
}
};
const handleDeleteProxy = async (configId: string) => {
try {
console.log('Deleting proxy:', configId);
const updatedConfigs = proxyConfigs.filter(config => config.id !== configId);
console.log('Updated configs after delete:', updatedConfigs);
const response = await new Promise<any>((resolve) => {
chrome.runtime.sendMessage({
action: ProxyActionType.UPDATE_PROXY_CONFIG,
configs: updatedConfigs
}, (result) => {
console.log('Delete response received:', result);
resolve(result);
});
});
console.log('Delete response:', response);
if (response?.success) {
setProxyConfigs(response.data || updatedConfigs);
message.success('删除代理成功');
} else {
console.error('Delete failed:', response?.error);
message.error(response?.error || '删除代理失败');
await loadConfigs();
}
} catch (error) {
console.error('Error deleting proxy:', error);
message.error('删除代理时发生错误');
await loadConfigs();
}
};
const handleApplyConfig = async (configId: string) => {
try {
const config = proxyConfigs.find(c => c.id === configId);
if (!config) return;
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.SET_PROXY_CONFIG,
config: config
});
if (response.success) {
await chrome.runtime.sendMessage({
action: 'PROXY_STATUS_CHANGED'
});
message.success('代理设置已应用');
} else {
message.error(response.error || '应用代理设置失败');
}
} catch (error) {
console.error('Error applying proxy:', error);
message.error('应用代理设置时发生错误');
}
};
const handleClearProxy = async () => {
try {
const response = await chrome.runtime.sendMessage({
action: ProxyActionType.CLEAR_PROXY_CONFIG
});
if (response.success) {
message.success('已切换至直接连接');
} else {
message.error(response.error || '清除代理设置失败');
}
} catch (error) {
console.error('Error clearing proxy:', error);
message.error('清除代理设置时发生错误');
}
};
return {
proxyConfigs,
handleAddProxy,
handleConfigChange,
handleDeleteProxy,
handleApplyConfig,
handleClearProxy
};
};
@@ -0,0 +1,59 @@
import { useState, useEffect } from 'react';
import { ProxyLog } from '@/types/proxy';
import { ProxyActionType } from '@/types/action';
export const useProxyLogs = () => {
const [proxyLogs, setProxyLogs] = useState<ProxyLog[]>([]);
useEffect(() => {
loadLogs();
// 监听日志更新
const handleLogsUpdate = () => {
loadLogs();
};
chrome.runtime.onMessage.addListener((message) => {
if (message.action === 'PROXY_LOGS_UPDATED') {
handleLogsUpdate();
}
});
return () => {
chrome.runtime.onMessage.removeListener(handleLogsUpdate);
};
}, []);
const loadLogs = async () => {
try {
console.log('Fetching proxy logs...');
const response = await chrome.runtime.sendMessage({
action: 'GET_PROXY_LOGS'
});
console.log('Received response:', response);
if (response.success) {
setProxyLogs(response.data || []);
} else {
console.error('Failed to load logs:', response.error);
}
} catch (error) {
console.error('Error loading logs:', error);
}
};
const handleClearLogs = async () => {
try {
await chrome.runtime.sendMessage({
action: ProxyActionType.CLEAR_PROXY_LOGS
});
setProxyLogs([]);
} catch (error) {
console.error('Error clearing logs:', error);
}
};
return {
proxyLogs,
handleClearLogs
};
};
+111
View File
@@ -0,0 +1,111 @@
.options-page {
height: 100vh;
overflow: auto;
background: #f0f2f5;
}
.options-page .ant-layout-content {
padding: 24px;
}
.proxy-tabs {
background: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.options-page .ant-tabs-nav::before {
border-bottom-color: #f0f0f0;
}
.options-page .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: var(--yakit-primary);
}
.options-page .ant-tabs-ink-bar {
background: var(--yakit-primary);
}
.options-page .ant-card {
margin-bottom: 16px;
border-radius: 4px;
border: 1px solid #f0f0f0;
}
.options-page .ant-card-head {
border-bottom: none;
min-height: 48px;
}
.options-page .ant-card-body {
padding: 16px;
}
.options-page .ant-btn-primary {
background: var(--yakit-primary);
border-color: var(--yakit-primary);
}
.options-page .ant-btn-primary:hover {
background: var(--yakit-primary-hover);
border-color: var(--yakit-primary-hover);
}
.proxy-action-btn.ant-btn-default {
color: rgba(0, 0, 0, 0.65);
border-color: #d9d9d9;
background: #fff;
}
.proxy-action-btn.ant-btn-default:hover {
color: var(--yakit-primary);
border-color: var(--yakit-primary);
}
.proxy-action-btn.ant-btn-primary {
color: #fff;
background: var(--yakit-primary);
border-color: var(--yakit-primary);
}
.proxy-action-btn.ant-btn-primary.ant-btn-dangerous {
background: #ff4d4f;
border-color: #ff4d4f;
}
.proxy-action-btn.ant-btn-primary.ant-btn-dangerous:hover {
background: #ff7875;
border-color: #ff7875;
}
.options-page .ant-table-wrapper {
background: #fff;
padding: 24px;
border-radius: 8px;
}
.options-page .ant-input:focus,
.options-page .ant-input-focused {
border-color: var(--yakit-primary);
box-shadow: 0 0 0 2px var(--yakit-primary-5);
}
.options-page .ant-select-focused .ant-select-selector {
border-color: var(--yakit-primary) !important;
box-shadow: 0 0 0 2px var(--yakit-primary-5) !important;
}
.options-page .ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background-color: var(--yakit-primary-5);
}
/* 防止 Modal 出现时页面跳动 */
.ant-modal-wrap {
overflow: hidden;
}
.ant-modal-content {
max-height: 90vh;
overflow: auto;
}
+139
View File
@@ -0,0 +1,139 @@
import React, { useState, useEffect, useRef } from 'react';
import { Layout, Tabs, message } from 'antd';
import { ProxySettings } from './components/ProxySettings';
import { ProxyLogs } from './components/ProxyLogs';
import { useProxyConfigs } from './hooks/useProxyConfigs';
import { useProxyLogs } from './hooks/useProxyLogs';
import { ProxyConfig } from '@/types/proxy';
import { ProxyActionType } from '@/types/action';
const { Content } = Layout;
interface ProxySettingsProps {
proxyConfigs: ProxyConfig[];
onAdd: (config: ProxyConfig) => void;
onChange: (configId: string, field: keyof ProxyConfig | 'config', value: any) => void;
onDelete: (configId: string) => void;
onApply: (configId: string) => Promise<void>;
onClear: (configId: string) => Promise<void>;
}
export const OptionsPage: React.FC = () => {
const {
proxyConfigs,
handleAddProxy,
handleConfigChange: handleConfigChangeHook,
handleDeleteProxy,
handleApplyConfig,
handleClearProxy
} = useProxyConfigs();
const { proxyLogs, handleClearLogs } = useProxyLogs();
const [proxyConfigsState, setProxyConfigs] = useState<ProxyConfig[]>([]);
useEffect(() => {
setProxyConfigs(proxyConfigs);
}, [proxyConfigs]);
// 通知 background 页面已准备就绪
useEffect(() => {
chrome.runtime.sendMessage({ action: 'OPTIONS_PAGE_READY' });
const messageListener = (
message: any,
sender: chrome.runtime.MessageSender,
sendResponse: (response?: any) => void
) => {
if (message.action === 'TRIGGER_ADD_PROXY') {
const proxySettingsElement = document.querySelector('.add-proxy-btn');
if (proxySettingsElement) {
(proxySettingsElement as HTMLElement).click();
}
}
sendResponse();
};
chrome.runtime.onMessage.addListener(messageListener);
return () => {
chrome.runtime.onMessage.removeListener(messageListener);
};
}, []);
const handleAdd = async (config: ProxyConfig) => {
try {
await handleAddProxy(config);
} catch (error) {
console.error('Failed to add proxy:', error);
message.error('添加代理失败');
}
};
const handleConfigChange = async (configId: string, field: keyof ProxyConfig | 'config', value: any) => {
try {
const updatedConfigs = proxyConfigsState.map(config => {
if (config.id === configId) {
if (field === 'config') {
// 如果是整个配置更新
return value;
} else {
// 如果是单个字段更新
return {
...config,
[field]: value
};
}
}
return config;
});
// 更新 IndexedDB
await chrome.runtime.sendMessage({
action: ProxyActionType.UPDATE_PROXY_CONFIG,
configs: updatedConfigs
});
// 更新本地状态
setProxyConfigs(updatedConfigs);
message.success('更新配置成功');
} catch (error) {
console.error('Failed to update config:', error);
message.error('更新配置失败');
}
};
return (
<Layout style={{ height: '100vh' }}>
<Content style={{ padding: '24px' }}>
<Tabs
defaultActiveKey="1"
items={[
{
key: '1',
label: '代理设置',
children: (
<ProxySettings
proxyConfigs={proxyConfigsState}
onAdd={handleAdd}
onChange={handleConfigChange}
onDelete={handleDeleteProxy}
onApply={handleApplyConfig}
onClear={handleClearProxy}
/>
)
},
{
key: '2',
label: '代理日志',
children: (
<ProxyLogs
logs={proxyLogs}
onClearLogs={handleClearLogs}
/>
)
}
]}
/>
</Content>
</Layout>
);
};
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from 'antd';
import { OptionsPage } from './OptionsPage';
import '@/styles/global.css';
const container = document.getElementById('root');
if (!container) throw new Error('Failed to find the root element');
const root = createRoot(container);
root.render(
<App>
<OptionsPage />
</App>
);
+58
View File
@@ -0,0 +1,58 @@
:root {
--yakit-primary: #F28B44;
--yakit-primary-hover: #f4a061;
--yakit-primary-active: #e87633;
--yakit-primary-5: #fff5eb;
--yakit-primary-10: rgba(242, 139, 68, 0.1);
/* 添加其他全局变量 */
--border-color: #f0f0f0;
--text-color: #333;
--icon-color: #666;
/* 菜单相关变量 */
--menu-item-height: 28px;
--menu-padding: 4px;
--menu-width: 180px;
}
.ant-btn-primary {
background-color: var(--yakit-primary) !important;
}
.ant-btn-primary:hover {
background-color: var(--yakit-primary-hover) !important;
}
.ant-btn-primary:active {
background-color: var(--yakit-primary-active) !important;
}
.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background-color: var(--yakit-primary-5) !important;
}
.ant-select-focused .ant-select-selector,
.ant-input-focused,
.ant-input:focus,
.ant-input-number-focused,
.ant-input-number:focus {
border-color: var(--yakit-primary) !important;
box-shadow: 0 0 0 2px var(--yakit-primary-10) !important;
}
.ant-btn:not(.ant-btn-primary):hover {
color: var(--yakit-primary) !important;
border-color: var(--yakit-primary) !important;
}
/* 移除所有滚动条 */
::-webkit-scrollbar {
display: none;
}
/* 确保所有内容都在视口内 */
html, body {
overflow: hidden;
height: fit-content;
}
+24
View File
@@ -0,0 +1,24 @@
// export const ActionType = {
// CONNECT: "CONNECT",
// SEND_MESSAGE: "SEND_MESSAGE",
// DISCONNECT: "DISCONNECT",
// SET_PROXY: "SET_PROXY",
// CLEAR_PROXY: "CLEAR_PROXY",
// PROXY_STATUS: "PROXY_STATUS",
// INJECT_SCRIPT: "INJECT_SCRIPT"
// } as const;
// export type ActionType = typeof ActionType[keyof typeof ActionType];
export const ProxyActionType = {
SET_PROXY_CONFIG: "SET_PROXY_CONFIG",
CLEAR_PROXY_CONFIG: "CLEAR_PROXY_CONFIG",
GET_PROXY_STATUS: "GET_PROXY_STATUS",
CLEAR_PROXY_LOGS: "CLEAR_PROXY_LOGS",
GET_PROXY_LOGS: "GET_PROXY_LOGS",
GET_PROXY_CONFIGS: "GET_PROXY_CONFIGS",
ADD_PROXY_CONFIG: "ADD_PROXY_CONFIG",
UPDATE_PROXY_CONFIG: "UPDATE_PROXY_CONFIG"
} as const;
export type ProxyActionType = typeof ProxyActionType[keyof typeof ProxyActionType];
+10
View File
@@ -0,0 +1,10 @@
declare namespace chrome.storage {
interface StorageChange {
oldValue?: any;
newValue?: any;
}
type StorageChanges = {
[key: string]: StorageChange;
};
}
+19
View File
@@ -0,0 +1,19 @@
export interface StorageChange<T = any> {
oldValue?: T;
newValue?: T;
}
export interface StorageChanges {
[key: string]: StorageChange;
}
export interface ProxyConfig {
host: string;
port: number;
scheme: 'http' | 'https' | 'socks5';
proxyType: 'fixed_servers';
enabled?: boolean;
id?: string;
name?: string;
timestamp?: number;
}
+45
View File
@@ -0,0 +1,45 @@
export interface PacScript {
data?: string;
url?: string;
mandatory?: boolean;
}
export interface ProxyConfig {
id: string;
name: string;
enabled: boolean;
proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect";
mode?: string;
host?: string;
port?: number;
scheme?: "http" | "https" | "socks4" | "socks5";
pacScript?: PacScript;
bypassList?: string[];
matchList?: string[];
}
export interface ProxyLog {
id: string;
timestamp: number;
url: string;
proxyId: string;
proxyName: string;
status: 'success' | 'error';
errorMessage?: string;
method?: string;
requestHeaders?: Record<string, string>;
requestBody?: string;
responseHeaders?: Record<string, string>;
responseBody?: string;
timing?: {
startTime: number;
endTime: number;
duration: number;
};
protocol?: string;
ip?: string;
fromCache?: boolean;
host?: string;
port?: number;
resourceType?: 'xhr' | 'fetch' | 'script' | 'stylesheet' | 'image' | 'other';
}
+19 -3
View File
@@ -1,6 +1,5 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
@@ -8,9 +7,26 @@
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"@/*": ["./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/**/*"
],
"exclude": [
"node_modules"
]
}
}
+81 -56
View File
@@ -5,60 +5,85 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode: 'development', // 设置模式为开发模式
entry: './src/index.jsx', // 指定入口文件
output: {
path: path.resolve(__dirname, 'build'), // 输出目录
filename: 'bundle.js', // 输出文件名
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.BABEL_ENV': JSON.stringify(process.env.BABEL_ENV || 'development'),
}),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html'
}),
new CopyWebpackPlugin({
patterns: [
// copy public assets exclude index.html
{
from: path.resolve(__dirname, 'public'),
to: path.resolve(__dirname, 'build'),
globOptions: {
ignore: ['**/index.html']
}
}
]
})
],
watch: true, // 开启实时监控
module: {
rules: [
{
test: /\.css$/, // 匹配所有的 css 文件
use: ['style-loader', 'css-loader'] // 对匹配到的文件使用这两个 loader
},
{
test: /\.tsx?$/, // 匹配TS和TSX文件
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.(js|jsx)$/, // 匹配JS和JSX文件
exclude: /node_modules/, // 排除node_modules目录
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'] // 使用的babel预设
}
}
mode: 'development', // 设置模式为开发模式
entry: {
main: './src/index.tsx',
options: './src/pages/options.tsx'
},
output: {
path: path.resolve(__dirname, 'build'), // 输出目录
filename: '[name].bundle.js', // 输出文件名
publicPath: '/',
clean: true
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.BABEL_ENV': JSON.stringify(process.env.BABEL_ENV || 'development'),
}),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
chunks: ['main']
}),
new HtmlWebpackPlugin({
template: './public/proxy/options.html',
filename: 'proxy/options.html',
chunks: ['options'],
publicPath: '../'
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'public'),
to: path.resolve(__dirname, 'build'),
globOptions: {
ignore: ['**/index.html', '**/proxy/options.html']
}
}
]
})
],
watch: true, // 开启实时监控
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true // 添加这个选项可以加快编译速度
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'] // 解析扩展(确保能够解析JS和JSX文件)
},
devtool: 'inline-source-map', // 生成内联源映射,便于调试
};
}
],
exclude: /node_modules/
},
{
test: /\.(js|jsx)$/, // 匹配JS和JSX文件
exclude: /node_modules/, // 排除node_modules目录
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'] // 使用的babel预设
}
}
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'], // 解析扩展(确保能够解析JS和JSX文件)
alias: {
'@': path.resolve(__dirname, './src'),
'@assets': path.resolve(__dirname, './src/assets'),
'@components': path.resolve(__dirname, './src/components'),
'@network': path.resolve(__dirname, './src/network'),
'@types': path.resolve(__dirname, './src/types'),
}
},
devtool: 'inline-source-map', // 生成内联源映射,便于调试
};
+10666
View File
File diff suppressed because it is too large Load Diff