From 8795cbc9dce33a7c3b9ee62bd80b2f929fc31d1c Mon Sep 17 00:00:00 2001 From: yaokl Date: Sun, 24 Sep 2023 13:49:09 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=B8=8A=E7=9A=84=E5=BF=AB=E6=8D=B7=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/webpack/dev.js | 10 +- scripts/webpack/remove-html.js | 2 +- src/manifest.json | 14 + src/pages/background/api-handler.ts | 29 ++- src/pages/background/index.ts | 2 + src/pages/content/index.tsx | 243 ++++++++++++++++++ .../options/sections/rules/edit/index.tsx | 1 + src/share/core/utils.ts | 10 +- 8 files changed, 302 insertions(+), 9 deletions(-) create mode 100644 src/pages/content/index.tsx diff --git a/scripts/webpack/dev.js b/scripts/webpack/dev.js index 830d784f..b932ba35 100644 --- a/scripts/webpack/dev.js +++ b/scripts/webpack/dev.js @@ -4,7 +4,7 @@ module.exports = function (config, context) { // 调试模式下,开启自动重载和自动编译 if (config.get('mode') === 'development') { // config.plugin('reload').use(ChromeExtensionReloader); - config.devServer.hot(false); + config.devServer.hot(true); config.devServer.open(false); const devMiddleware = config.devServer.store.get('devMiddleware'); config.devServer.store.set('devMiddleware', { @@ -13,10 +13,10 @@ module.exports = function (config, context) { }); } - config.plugin('bundle-analyzer').use(new BundleAnalyzerPlugin({ - analyzerMode: 'static', - reportFilename: '../temp/bundle-analyze.html', - })) + // config.plugin('bundle-analyzer').use(new BundleAnalyzerPlugin({ + // analyzerMode: 'static', + // reportFilename: '../temp/bundle-analyze.html', + // })) return config; }; diff --git a/scripts/webpack/remove-html.js b/scripts/webpack/remove-html.js index cf61414c..3d061798 100644 --- a/scripts/webpack/remove-html.js +++ b/scripts/webpack/remove-html.js @@ -6,7 +6,7 @@ module.exports = function (config, context) { continue; } const pageName = item.name.substr(18); - if (pageName === 'background' || pageName.indexOf('inject-') === 0 || pageName.indexOf('worker-') === 0) { + if (pageName === 'content' || pageName === 'background' || pageName.indexOf('inject-') === 0 || pageName.indexOf('worker-') === 0) { config.plugins.delete(item.name); console.log('Remove html entry: ' + item.name); } diff --git a/src/manifest.json b/src/manifest.json index da03a902..c53f18b3 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -22,6 +22,20 @@ "assets/js/background.js" ] }, + "content_scripts": [ + { + "matches": [""], + "css": [ + "assets/css/content.css" + ], + "js": [ + "external/react.min.js", + "external/react-dom.min.js", + "assets/js/content.js" + ], + "run_at": "document_end" + } + ], "browser_action": { "default_icon": { "128": "assets/images/128.png" diff --git a/src/pages/background/api-handler.ts b/src/pages/background/api-handler.ts index cd488391..416b8b0c 100644 --- a/src/pages/background/api-handler.ts +++ b/src/pages/background/api-handler.ts @@ -1,11 +1,12 @@ import browser from 'webextension-polyfill'; import logger from '@/share/core/logger'; -import { APIs, TABLE_NAMES_ARR } from '@/share/core/constant'; +import { APIs, TABLE_NAMES_ARR, TABLE_NAMES } from '@/share/core/constant'; import { prefs } from '@/share/core/prefs'; import rules from './core/rules'; import { openURL } from './utils'; import { getDatabase } from './core/db'; + function execute(request: any) { if (request.method === 'notifyBackground') { request.method = request.reason; @@ -27,6 +28,14 @@ function execute(request: any) { case APIs.DELETE_RULE: return rules.remove(request.type, request.id); case APIs.SET_PREFS: + browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + console.log('tabs', tabs); + tabs.forEach((tab) => { + if (tab.id) { + browser.tabs.sendMessage(tab.id, { method: APIs.SET_PREFS, key: request.key, value: request.value }); + } + }); + }); return prefs.set(request.key, request.value); case APIs.UPDATE_CACHE: if (request.type === 'all') { @@ -41,7 +50,23 @@ function execute(request: any) { } export default function createApiHandler() { - browser.runtime.onMessage.addListener((request) => { + browser.runtime.onMessage.addListener((request: any, sender, sendResponse) => { + console.log('createApiHandler-----', request, sender); + + if (request.method === 'GetData') { + const response = { + rules: rules.get(TABLE_NAMES.sendHeader, { enable: true }), + enable: !prefs.get('disable-all'), + }; + + console.log('createApiHandler-----', response); + sendResponse(response); + // if (response.enable) { + // sendResponse(response); + // } + } + + logger.debug('Background Receive Message', request); if (request.method === 'batchExecute') { const queue = request.batch.map((item) => { diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index a89f4be4..a38158a6 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -6,6 +6,8 @@ if (typeof window !== 'undefined') { window.IS_BACKGROUND = true; } +console.log('background/index.ts'); + // 开始初始化 createApiHandler(); createRequestHandler(); diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx new file mode 100644 index 00000000..27566ae6 --- /dev/null +++ b/src/pages/content/index.tsx @@ -0,0 +1,243 @@ +import React, { useRef, useState, useEffect } from 'react'; +import ReactDOM from 'react-dom'; +import { cx, css } from '@emotion/css'; +import browser from 'webextension-polyfill'; +import { Card, Switch, Table, Popover, Banner } from '@douyinfe/semi-ui'; +import { IconSetting, IconQuit } from '@douyinfe/semi-icons'; +// import Api from '@/share/pages/api'; +import type { Rule } from '@/share/core/types'; +// import { VIRTUAL_KEY } from '@/share/core/constant'; +import RuleDetail from '@/share/components/rule-detail'; +import { APIs } from '@/share/core/constant'; + + +let rules = []; +let enable = false; +// let loading = false; + +console.log('content load.......................'); +// contentScript.js +browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + console.log('content-script收到的消息', message); + + if (message.method === APIs.SET_PREFS) { + if (message.key === 'disable-all') { + enable = !message.value; + } + ReactDOM.render(, app); + } +}); + +browser.runtime.sendMessage({ greeting: '我是content-script呀,我主动发消息给后台!', method: 'GetData' }).then((response) => { + console.log('收到来自后台的回复', response); + if (response) { + rules = response.rules || []; + enable = response.enable || false; + console.log('收到来自后台的回复', rules); + ReactDOM.render(, app); + } +}); + +const basicStyle = css` + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + + .cell-enable { + padding-right: 0; + .switch-container { + display: flex; + align-items: center; + } + } +`; + +const quickAndDirtyStyle = { + width: '200px', + height: '200px', + background: '#FF9900', + color: '#FFFFFF', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}; + + +function Content() { + console.log('content render.......................', rules); + const { Meta } = Card; + + // 可拖动 + const [pressed, setPressed] = useState(false); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const ref = useRef(); + useEffect(() => { + if (ref.current) { + ref.current.style.transform = `translate(${position.x}px, ${position.y}px)`; + } + }, [position]); + + // Update the current position if mouse is down + const onMouseMove = (event) => { + if (pressed) { + setPosition({ + x: position.x + event.movementX, + y: position.y + event.movementY, + }); + } + }; + + const goToSetting = () => { + browser.runtime.sendMessage({ url: browser.runtime.getURL('options.html'), method: APIs.OPEN_URL }).then((response) => { + console.log('goToSetting收到来自后台的回复', response); + }); + window.close(); + }; + + const handleEnableChange = () => { + browser.runtime.sendMessage({ key: 'disable-all', value: enable, method: APIs.SET_PREFS }).then((response) => { + console.log('handleEnableChange收到来自后台的回复', response); + enable = !enable; + ReactDOM.render(, app); + }); + }; + + return enable ? ( +
setPressed(true)} + onMouseUp={() => setPressed(false)} + > + + } + headerExtraContent={ +
+ + 管理规则 + + } + > + + + + 关闭插件 + + } + > + + +
+ } + > +
+ { rules.length > 1 && + } + ( +
+ { + item.enable = checked; + browser.runtime.sendMessage({ rule: item, method: APIs.SAVE_RULE }).then((response) => { + console.log('切换状态,收到来自后台的回复', response); + }); + ReactDOM.render(, app); + }} + /> +
+ ), + }, + { + title: 'group', + dataIndex: 'group', + render: (value: string) => ( + +
{value}
+
+ ), + }, + { + title: 'name', + dataIndex: 'name', + render: (value: string, item: Rule) => ( + } style={{ maxWidth: '300px' }}> +
{value}
+
+ ), + }, + ]} + pagination={false} + /> + + + + ) : (
); +} + +// 创建id为CRX-container的div +const app = document.createElement('div'); +app.id = 'headerEditor-container'; +app.setAttribute('class', 'semi-always-dark'); + +// // 添加鼠标事件 +// let isDragging = false; +// let mouseOffsetX = 0; +// let mouseOffsetY = 0; + +// app.addEventListener('mousedown', (event) => { +// isDragging = true; +// mouseOffsetX = event.clientX - app.offsetLeft; +// mouseOffsetY = event.clientY - app.offsetTop; +// }); + +// document.addEventListener('mousemove', (event) => { +// if (isDragging) { +// app.style.left = `${event.clientX - mouseOffsetX}px`; +// app.style.top = `${event.clientY - mouseOffsetY}px`; +// } +// }); + +// document.addEventListener('mouseup', () => { +// isDragging = false; +// }); + +// 将刚创建的div插入body最后 +document.body.appendChild(app); + +// 将ReactDOM插入刚创建的div +ReactDOM.render(, app); diff --git a/src/pages/options/sections/rules/edit/index.tsx b/src/pages/options/sections/rules/edit/index.tsx index 33af76e5..be388712 100644 --- a/src/pages/options/sections/rules/edit/index.tsx +++ b/src/pages/options/sections/rules/edit/index.tsx @@ -323,6 +323,7 @@ export default class Edit extends React.Component { {isHeader && !this.state.rule.isFunction && ( ({ label: x, value: x }))} diff --git a/src/share/core/utils.ts b/src/share/core/utils.ts index ca778301..3fbe9991 100644 --- a/src/share/core/utils.ts +++ b/src/share/core/utils.ts @@ -18,7 +18,15 @@ export const FIREFOX_VERSION = IS_FIREFOX })() : 0; -export const IS_SUPPORT_STREAM_FILTER = typeof browser.webRequest.filterResponseData === 'function'; +let is_support = false; +try { + is_support = typeof browser.webRequest.filterResponseData === 'function'; +} catch (e) { + // ignore +} + +export const IS_SUPPORT_STREAM_FILTER = is_support; +console.log('utils', IS_SUPPORT_STREAM_FILTER); // Get Active Tab export function getActiveTab(): Promise { From e967057450a863fa4212e113dc8fca02a9aa9fa0 Mon Sep 17 00:00:00 2001 From: yaokl Date: Thu, 28 Sep 2023 17:44:22 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E7=8A=B6=E6=80=81=E5=90=8C?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/background/api-handler.ts | 13 ++- src/pages/content/index.tsx | 135 ++++++++++++++++++---------- 2 files changed, 98 insertions(+), 50 deletions(-) diff --git a/src/pages/background/api-handler.ts b/src/pages/background/api-handler.ts index 416b8b0c..b249fe1e 100644 --- a/src/pages/background/api-handler.ts +++ b/src/pages/background/api-handler.ts @@ -24,6 +24,14 @@ function execute(request: any) { case APIs.GET_RULES: return Promise.resolve(rules.get(request.type, request.options)); case APIs.SAVE_RULE: + browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + console.log('tabs', tabs); + tabs.forEach((tab) => { + if (tab.id) { + browser.tabs.sendMessage(tab.id, { method: APIs.SAVE_RULE, rule: request.rule }); + } + }); + }); return rules.save(request.rule); case APIs.DELETE_RULE: return rules.remove(request.type, request.id); @@ -59,11 +67,8 @@ export default function createApiHandler() { enable: !prefs.get('disable-all'), }; - console.log('createApiHandler-----', response); + logger.debug('createApiHandler-----', response); sendResponse(response); - // if (response.enable) { - // sendResponse(response); - // } } diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 27566ae6..a6c65499 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -10,39 +10,56 @@ import type { Rule } from '@/share/core/types'; import RuleDetail from '@/share/components/rule-detail'; import { APIs } from '@/share/core/constant'; - let rules = []; let enable = false; // let loading = false; -console.log('content load.......................'); +console.log('content-script load.......................'); // contentScript.js browser.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log('content-script收到的消息', message); - if (message.method === APIs.SET_PREFS) { - if (message.key === 'disable-all') { - enable = !message.value; - } - ReactDOM.render(, app); + switch (message.method) { + case APIs.SET_PREFS: + if (message.key === 'disable-all') { + enable = !message.value; + } + ReactDOM.render(, app); + break; + case APIs.SAVE_RULE: + setTimeout(() => { + getData(); + }, 500); + break; + default: + break; } }); -browser.runtime.sendMessage({ greeting: '我是content-script呀,我主动发消息给后台!', method: 'GetData' }).then((response) => { - console.log('收到来自后台的回复', response); - if (response) { - rules = response.rules || []; - enable = response.enable || false; - console.log('收到来自后台的回复', rules); - ReactDOM.render(, app); - } -}); + +getData(); + + +function getData() { + browser.runtime.sendMessage({ greeting: '我是content-script呀,我主动发消息给后台!', method: 'GetData' }).then((response) => { + console.log('收到来自后台的回复', response); + if (response) { + rules = response.rules || []; + enable = response.enable || false; + console.log('收到来自后台的回复', rules); + ReactDOM.render(, app); + } + }); +} + const basicStyle = css` position: fixed; top: 20px; right: 20px; z-index: 1000; + min-width: 300px; + user-select: none; .cell-enable { padding-right: 0; @@ -53,41 +70,68 @@ const basicStyle = css` } `; -const quickAndDirtyStyle = { - width: '200px', - height: '200px', - background: '#FF9900', - color: '#FFFFFF', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -}; - function Content() { - console.log('content render.......................', rules); const { Meta } = Card; // 可拖动 - const [pressed, setPressed] = useState(false); - const [position, setPosition] = useState({ x: 0, y: 0 }); - const ref = useRef(); - useEffect(() => { - if (ref.current) { - ref.current.style.transform = `translate(${position.x}px, ${position.y}px)`; + const [isDragging, setIsDragging] = useState(false); + const divRef = useRef(null); + const offsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); + const [position, setPosition] = useState({ x: window.innerWidth - 400, y: 0 }); + + const handleMouseDown = (e: React.MouseEvent) => { + if (e.button === 0) { + e.preventDefault(); // 阻止默认的文本选择行为 + setIsDragging(true); + offsetRef.current = { + x: e.clientX - position.x, + y: e.clientY - position.y, + }; } - }, [position]); - - // Update the current position if mouse is down - const onMouseMove = (event) => { - if (pressed) { - setPosition({ - x: position.x + event.movementX, - y: position.y + event.movementY, - }); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (isDragging) { + const newX = e.clientX - offsetRef.current.x; + const newY = e.clientY - offsetRef.current.y; + + // 限制拖动范围为窗口 + const maxX = window.innerWidth - divRef.current!.offsetWidth; + const maxY = window.innerHeight - divRef.current!.offsetHeight; + + const boundedX = Math.min(Math.max(newX, 0), maxX); + const boundedY = Math.min(Math.max(newY, 0), maxY); + + setPosition({ x: boundedX, y: boundedY }); } }; + const handleMouseUp = () => { + setIsDragging(false); + }; + + const handleFocus = () => { + getData(); + }; + + useEffect(() => { + if (isDragging) { + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + } else { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + } + window.addEventListener('focus', handleFocus); + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + window.removeEventListener('focus', handleFocus); + }; + }, [isDragging]); + const goToSetting = () => { browser.runtime.sendMessage({ url: browser.runtime.getURL('options.html'), method: APIs.OPEN_URL }).then((response) => { console.log('goToSetting收到来自后台的回复', response); @@ -105,11 +149,10 @@ function Content() { return enable ? (
setPressed(true)} - onMouseUp={() => setPressed(false)} + ref={divRef} + style={{ left: `${position.x}px`, top: `${position.y}px` }} + onMouseDown={handleMouseDown} > Date: Sat, 14 Oct 2023 19:04:14 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/background/api-handler.ts | 34 ++- src/pages/content/index.tsx | 347 +++++++++++++++++++--------- 2 files changed, 269 insertions(+), 112 deletions(-) diff --git a/src/pages/background/api-handler.ts b/src/pages/background/api-handler.ts index b249fe1e..742a6114 100644 --- a/src/pages/background/api-handler.ts +++ b/src/pages/background/api-handler.ts @@ -57,21 +57,51 @@ function execute(request: any) { // return false; } +const currentIP = {}; + export default function createApiHandler() { + // get IP using webRequest + browser.webRequest.onCompleted.addListener( + (info) => { + const u = new URL(info.url); + if (info.tabId in currentIP) { + currentIP[info.tabId][u.hostname] = info.ip; + } else { + currentIP[info.tabId] = { [u.hostname]: info.ip }; + } + }, + { + urls: [], + types: [], + }, + [], + ); + browser.runtime.onMessage.addListener((request: any, sender, sendResponse) => { console.log('createApiHandler-----', request, sender); if (request.method === 'GetData') { + const currentIPList: Array<{ domain: string; ip: string }> = []; + const tabId = sender.tab?.id || 0; + if (tabId in currentIP) { + for (const key in currentIP[tabId]) { + if (Object.prototype.hasOwnProperty.call(currentIP[tabId], key)) { + currentIPList.push({ domain: key, ip: currentIP[tabId][key] }); + } + } + } + const response = { - rules: rules.get(TABLE_NAMES.sendHeader, { enable: true }), + rules: rules.get(TABLE_NAMES.sendHeader), + enableRules: rules.get(TABLE_NAMES.sendHeader, { enable: true }), enable: !prefs.get('disable-all'), + currentIPList, }; logger.debug('createApiHandler-----', response); sendResponse(response); } - logger.debug('Background Receive Message', request); if (request.method === 'batchExecute') { const queue = request.batch.map((item) => { diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index a6c65499..db7603d9 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -2,17 +2,35 @@ import React, { useRef, useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; import { cx, css } from '@emotion/css'; import browser from 'webextension-polyfill'; -import { Card, Switch, Table, Popover, Banner } from '@douyinfe/semi-ui'; -import { IconSetting, IconQuit } from '@douyinfe/semi-icons'; -// import Api from '@/share/pages/api'; +import { Card, Switch, Table, Popover, Banner, Space, Badge, Select, Toast, Typography } from '@douyinfe/semi-ui'; +import { IconSetting, IconMore, IconQuit, IconSafe } from '@douyinfe/semi-icons'; import type { Rule } from '@/share/core/types'; -// import { VIRTUAL_KEY } from '@/share/core/constant'; import RuleDetail from '@/share/components/rule-detail'; import { APIs } from '@/share/core/constant'; +let currentIPList = []; let rules = []; +let enableRules = []; let enable = false; -// let loading = false; +let title = '当前未启用规则'; +let titleColor = 'rgba(var(--semi-gray-3), 1)'; + +const basicStyle = css` + position: fixed; + top: 20px; + right: 20px; + z-index: 1020; + min-width: 300px; + user-select: none; + + .cell-enable { + padding-right: 0; + .switch-container { + display: flex; + align-items: center; + } + } +`; console.log('content-script load.......................'); // contentScript.js @@ -37,48 +55,47 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => { }); -getData(); - - function getData() { browser.runtime.sendMessage({ greeting: '我是content-script呀,我主动发消息给后台!', method: 'GetData' }).then((response) => { console.log('收到来自后台的回复', response); if (response) { rules = response.rules || []; + enableRules = response.enableRules || []; enable = response.enable || false; - console.log('收到来自后台的回复', rules); + currentIPList = response.currentIPList || []; + + if (enableRules.length > 0) { + title = enableRules[enableRules.length - 1].name || '规则名称未定义'; + titleColor = 'rgba(var(--semi-green-4), 1)'; + } else { + title = '当前未启用规则'; + titleColor = 'rgba(var(--semi-gray-3), 1)'; + } + + console.log('收到来自后台的回复', rules, title); ReactDOM.render(, app); } }); } +// 延迟获取数据,防止后台数据还未加载完成 +setTimeout(() => { + getData(); +}, 500); -const basicStyle = css` - position: fixed; - top: 20px; - right: 20px; - z-index: 1000; - min-width: 300px; - user-select: none; - - .cell-enable { - padding-right: 0; - .switch-container { - display: flex; - align-items: center; - } - } -`; - +// chrome://net-internals/#dns +// https://github.com/pmarks-net/ipvfoo/blob/master/src/background.js function Content() { const { Meta } = Card; + const [visible, setVisible] = useState(false); + // 可拖动 const [isDragging, setIsDragging] = useState(false); const divRef = useRef(null); const offsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); - const [position, setPosition] = useState({ x: window.innerWidth - 400, y: 0 }); + const [position, setPosition] = useState({ x: window.innerWidth - 280, y: 20 }); const handleMouseDown = (e: React.MouseEvent) => { if (e.button === 0) { @@ -147,6 +164,24 @@ function Content() { }); }; + const onEnableChange = (value) => { + console.log(value); + value.rule.enable = true; + browser.runtime.sendMessage({ rule: value.rule, method: APIs.SAVE_RULE }).then(() => { + Toast.success({ + content: '启用成功', + }); + getData(); + }); + }; + + const goToDnsSetting = () => { + browser.runtime.sendMessage({ url: 'chrome://net-internals/#dns', method: APIs.OPEN_URL }).then((response) => { + console.log('goToSetting收到来自后台的回复', response); + }); + window.close(); + }; + return enable ? (
- } - headerExtraContent={ -
- - 管理规则 - - } - > - - - - 关闭插件 - - } - > - - -
- } > + + + { enableRules.length > 1 ? + + {title} + :
{ title }
} + + } + />
- { rules.length > 1 && + + 管理规则 + + } + > + + + + 关闭插件 + } -
( -
- { - item.enable = checked; - browser.runtime.sendMessage({ rule: item, method: APIs.SAVE_RULE }).then((response) => { - console.log('切换状态,收到来自后台的回复', response); - }); - ReactDOM.render(, app); - }} - /> -
- ), - }, - { - title: 'group', - dataIndex: 'group', - render: (value: string) => ( - -
{value}
-
- ), - }, - { - title: 'name', - dataIndex: 'name', - render: (value: string, item: Rule) => ( - } style={{ maxWidth: '300px' }}> -
{value}
-
- ), - }, - ]} - pagination={false} - /> + > + + + + + 启用的规则列表 + +
( +
+ { + item.enable = checked; + browser.runtime.sendMessage({ rule: item, method: APIs.SAVE_RULE }).then((response) => { + console.log('切换状态,收到来自后台的回复', response); + Toast.success({ + content: checked ? '启用成功' : '禁用成功', + }); + ReactDOM.render(, app); + }); + }} + /> +
+ ), + }, + { + title: 'group', + dataIndex: 'group', + render: (value: string) => ( + +
{value}
+
+ ), + }, + { + title: 'name', + dataIndex: 'name', + render: (value: string, item: Rule) => ( + } style={{ maxWidth: '300px' }}> +
{value}
+
+ ), + }, + ]} + pagination={false} + /> + + 当前页面的网络解析 + + 清理dns缓存 + + + +
( + +
{value}
+
+ ), + }, + { + title: 'IP地址', + dataIndex: 'ip', + render: (value: string) => ( + +
{value}
+
+ ), + }, + ]} + pagination={ + { + size: 'small', + } + } + /> + + } + trigger="custom" + > + setVisible(!visible)} style={{ color: 'var(--semi-color-primary)', paddingLeft: '6px' }} /> + @@ -255,7 +382,7 @@ function Content() { // 创建id为CRX-container的div const app = document.createElement('div'); app.id = 'headerEditor-container'; -app.setAttribute('class', 'semi-always-dark'); +// app.setAttribute('class', 'semi-always-dark'); // // 添加鼠标事件 // let isDragging = false; From 6b22ae5d9fc141179798ce1a60d997d64ed515b8 Mon Sep 17 00:00:00 2001 From: yaokl Date: Sun, 15 Oct 2023 10:06:22 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=B1=95=E7=A4=BA=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/content/index.tsx | 40 ++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index db7603d9..005d5035 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -17,7 +17,7 @@ let titleColor = 'rgba(var(--semi-gray-3), 1)'; const basicStyle = css` position: fixed; - top: 20px; + bottom: 20px; right: 20px; z-index: 1020; min-width: 300px; @@ -83,9 +83,6 @@ setTimeout(() => { getData(); }, 500); -// chrome://net-internals/#dns -// https://github.com/pmarks-net/ipvfoo/blob/master/src/background.js - function Content() { const { Meta } = Card; @@ -95,7 +92,7 @@ function Content() { const [isDragging, setIsDragging] = useState(false); const divRef = useRef(null); const offsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); - const [position, setPosition] = useState({ x: window.innerWidth - 280, y: 20 }); + const [position, setPosition] = useState({ x: window.innerWidth - 300, y: window.innerHeight - 120 }); const handleMouseDown = (e: React.MouseEvent) => { if (e.button === 0) { @@ -117,8 +114,8 @@ function Content() { const maxX = window.innerWidth - divRef.current!.offsetWidth; const maxY = window.innerHeight - divRef.current!.offsetHeight; - const boundedX = Math.min(Math.max(newX, 0), maxX); - const boundedY = Math.min(Math.max(newY, 0), maxY); + const boundedX = Math.min(Math.max(newX, 10), maxX); + const boundedY = Math.min(Math.max(newY, 10), maxY); setPosition({ x: boundedX, y: boundedY }); } @@ -149,13 +146,18 @@ function Content() { }; }, [isDragging]); - const goToSetting = () => { + const goToOptions = () => { browser.runtime.sendMessage({ url: browser.runtime.getURL('options.html'), method: APIs.OPEN_URL }).then((response) => { console.log('goToSetting收到来自后台的回复', response); }); window.close(); }; + const goToDnsSetting = () => { + browser.runtime.sendMessage({ url: 'chrome://net-internals/#dns', method: APIs.OPEN_URL }).then(() => {}); + window.close(); + }; + const handleEnableChange = () => { browser.runtime.sendMessage({ key: 'disable-all', value: enable, method: APIs.SET_PREFS }).then((response) => { console.log('handleEnableChange收到来自后台的回复', response); @@ -175,13 +177,6 @@ function Content() { }); }; - const goToDnsSetting = () => { - browser.runtime.sendMessage({ url: 'chrome://net-internals/#dns', method: APIs.OPEN_URL }).then((response) => { - console.log('goToSetting收到来自后台的回复', response); - }); - window.close(); - }; - return enable ? (
} > - + setVisible(false)} + onEscKeyDown={() => setVisible(false)} content={
{ enableRules.length > 1 && } @@ -320,7 +322,7 @@ function Content() { pagination={false} /> 当前页面的网络解析 @@ -363,6 +365,8 @@ function Content() { ]} pagination={ { + formatPageText: false, + hoverShowPageSelect: true, size: 'small', } } From a2f5599f271a08cf7982a147e36bbd8055704227 Mon Sep 17 00:00:00 2001 From: yaokl Date: Sun, 15 Oct 2023 19:55:52 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/background/api-handler.ts | 9 +++---- src/pages/content/index.tsx | 39 ++++++----------------------- 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/src/pages/background/api-handler.ts b/src/pages/background/api-handler.ts index 742a6114..8b46c64a 100644 --- a/src/pages/background/api-handler.ts +++ b/src/pages/background/api-handler.ts @@ -25,9 +25,9 @@ function execute(request: any) { return Promise.resolve(rules.get(request.type, request.options)); case APIs.SAVE_RULE: browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { - console.log('tabs', tabs); tabs.forEach((tab) => { if (tab.id) { + console.log('[Header Editor] 向 content-script 发送消息', { tabId: tab.id, method: APIs.SAVE_RULE, rule: request.rule }); browser.tabs.sendMessage(tab.id, { method: APIs.SAVE_RULE, rule: request.rule }); } }); @@ -37,9 +37,9 @@ function execute(request: any) { return rules.remove(request.type, request.id); case APIs.SET_PREFS: browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { - console.log('tabs', tabs); tabs.forEach((tab) => { if (tab.id) { + console.log('[Header Editor] 向 content-script 发送消息', { tabId: tab.id, method: APIs.SET_PREFS, key: request.key, value: request.value }); browser.tabs.sendMessage(tab.id, { method: APIs.SET_PREFS, key: request.key, value: request.value }); } }); @@ -78,9 +78,8 @@ export default function createApiHandler() { ); browser.runtime.onMessage.addListener((request: any, sender, sendResponse) => { - console.log('createApiHandler-----', request, sender); - if (request.method === 'GetData') { + console.log('[Header Editor] 收到来自 content-script 的消息', { request, sender }); const currentIPList: Array<{ domain: string; ip: string }> = []; const tabId = sender.tab?.id || 0; if (tabId in currentIP) { @@ -98,7 +97,7 @@ export default function createApiHandler() { currentIPList, }; - logger.debug('createApiHandler-----', response); + console.log('[Header Editor] 返回 content-script 的数据', response); sendResponse(response); } diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 005d5035..df9cb320 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -32,10 +32,11 @@ const basicStyle = css` } `; -console.log('content-script load.......................'); +console.log('[Header Editor] content-script load.......................'); + // contentScript.js browser.runtime.onMessage.addListener((message, sender, sendResponse) => { - console.log('content-script收到的消息', message); + console.log('[Header Editor] content-script收到的消息', message); switch (message.method) { case APIs.SET_PREFS: @@ -57,7 +58,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => { function getData() { browser.runtime.sendMessage({ greeting: '我是content-script呀,我主动发消息给后台!', method: 'GetData' }).then((response) => { - console.log('收到来自后台的回复', response); + console.log('[Header Editor] getData 收到来自后台的回复', response); if (response) { rules = response.rules || []; enableRules = response.enableRules || []; @@ -72,7 +73,6 @@ function getData() { titleColor = 'rgba(var(--semi-gray-3), 1)'; } - console.log('收到来自后台的回复', rules, title); ReactDOM.render(, app); } }); @@ -147,9 +147,7 @@ function Content() { }, [isDragging]); const goToOptions = () => { - browser.runtime.sendMessage({ url: browser.runtime.getURL('options.html'), method: APIs.OPEN_URL }).then((response) => { - console.log('goToSetting收到来自后台的回复', response); - }); + browser.runtime.sendMessage({ url: browser.runtime.getURL('options.html'), method: APIs.OPEN_URL }).then(() => {}); window.close(); }; @@ -160,14 +158,13 @@ function Content() { const handleEnableChange = () => { browser.runtime.sendMessage({ key: 'disable-all', value: enable, method: APIs.SET_PREFS }).then((response) => { - console.log('handleEnableChange收到来自后台的回复', response); + console.log('[Header Editor] handleEnableChange收到来自后台的回复', response); enable = !enable; ReactDOM.render(, app); }); }; const onEnableChange = (value) => { - console.log(value); value.rule.enable = true; browser.runtime.sendMessage({ rule: value.rule, method: APIs.SAVE_RULE }).then(() => { Toast.success({ @@ -289,7 +286,7 @@ function Content() { onChange={(checked) => { item.enable = checked; browser.runtime.sendMessage({ rule: item, method: APIs.SAVE_RULE }).then((response) => { - console.log('切换状态,收到来自后台的回复', response); + console.log('[Header Editor] 切换状态,收到来自后台的回复', response); Toast.success({ content: checked ? '启用成功' : '禁用成功', }); @@ -388,28 +385,6 @@ const app = document.createElement('div'); app.id = 'headerEditor-container'; // app.setAttribute('class', 'semi-always-dark'); -// // 添加鼠标事件 -// let isDragging = false; -// let mouseOffsetX = 0; -// let mouseOffsetY = 0; - -// app.addEventListener('mousedown', (event) => { -// isDragging = true; -// mouseOffsetX = event.clientX - app.offsetLeft; -// mouseOffsetY = event.clientY - app.offsetTop; -// }); - -// document.addEventListener('mousemove', (event) => { -// if (isDragging) { -// app.style.left = `${event.clientX - mouseOffsetX}px`; -// app.style.top = `${event.clientY - mouseOffsetY}px`; -// } -// }); - -// document.addEventListener('mouseup', () => { -// isDragging = false; -// }); - // 将刚创建的div插入body最后 document.body.appendChild(app); From 43b9352ae7e427886add3f795c1faed22602e2ba Mon Sep 17 00:00:00 2001 From: yaokl Date: Mon, 23 Oct 2023 16:24:16 +0800 Subject: [PATCH 6/9] feat: update --- src/pages/content/index.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index df9cb320..0a32087f 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -156,6 +156,11 @@ function Content() { window.close(); }; + const goToclearBrowserData = () => { + browser.runtime.sendMessage({ url: 'chrome://settings/clearBrowserData', method: APIs.OPEN_URL }).then(() => {}); + window.close(); + }; + const handleEnableChange = () => { browser.runtime.sendMessage({ key: 'disable-all', value: enable, method: APIs.SET_PREFS }).then((response) => { console.log('[Header Editor] handleEnableChange收到来自后台的回复', response); @@ -178,7 +183,7 @@ function Content() {
清理dns缓存 + 清理浏览器缓存 +
Date: Thu, 9 Nov 2023 11:32:00 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=E5=8E=BB=E9=99=A4console.log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/content/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 0a32087f..3ea8dcdc 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -36,7 +36,7 @@ console.log('[Header Editor] content-script load.......................'); // contentScript.js browser.runtime.onMessage.addListener((message, sender, sendResponse) => { - console.log('[Header Editor] content-script收到的消息', message); + // console.log('[Header Editor] content-script收到的消息', message); switch (message.method) { case APIs.SET_PREFS: @@ -58,7 +58,7 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => { function getData() { browser.runtime.sendMessage({ greeting: '我是content-script呀,我主动发消息给后台!', method: 'GetData' }).then((response) => { - console.log('[Header Editor] getData 收到来自后台的回复', response); + // console.log('[Header Editor] getData 收到来自后台的回复', response); if (response) { rules = response.rules || []; enableRules = response.enableRules || []; @@ -163,7 +163,7 @@ function Content() { const handleEnableChange = () => { browser.runtime.sendMessage({ key: 'disable-all', value: enable, method: APIs.SET_PREFS }).then((response) => { - console.log('[Header Editor] handleEnableChange收到来自后台的回复', response); + // console.log('[Header Editor] handleEnableChange收到来自后台的回复', response); enable = !enable; ReactDOM.render(, app); }); @@ -291,7 +291,7 @@ function Content() { onChange={(checked) => { item.enable = checked; browser.runtime.sendMessage({ rule: item, method: APIs.SAVE_RULE }).then((response) => { - console.log('[Header Editor] 切换状态,收到来自后台的回复', response); + // console.log('[Header Editor] 切换状态,收到来自后台的回复', response); Toast.success({ content: checked ? '启用成功' : '禁用成功', }); From 8a561f185e0ea7029f19745f583c2d529dcebf3d Mon Sep 17 00:00:00 2001 From: yaokl Date: Mon, 14 Jul 2025 13:34:34 +0800 Subject: [PATCH 8/9] feat: update manifest v3 --- .eslintignore | 1 + .eslintrc.js | 10 +- DEBUG_GUIDE.md | 166 ++++++ MIGRATION_REPORT.md | 151 +++++ package.json | 2 +- src/enterprise/policy-handler.ts | 371 ++++++++++++ src/manifest.json | 69 +-- src/pages/background/api-handler.ts | 207 +++++-- src/pages/background/index.ts | 503 +++++++++++++++- src/pages/background/request-handler.ts | 492 --------------- src/pages/background/upgrade.ts | 52 +- src/pages/background/v3-rule-converter.ts | 558 ++++++++++++++++++ .../options/components/v3-migration-guide.tsx | 202 +++++++ src/share/core/logger.ts | 197 ++++++- src/share/core/notify.ts | 6 +- src/share/core/prefs.ts | 29 +- src/share/core/storage.ts | 2 +- src/share/core/utils.ts | 4 +- tsconfig.json | 9 +- 19 files changed, 2392 insertions(+), 639 deletions(-) create mode 100644 DEBUG_GUIDE.md create mode 100644 MIGRATION_REPORT.md create mode 100644 src/enterprise/policy-handler.ts delete mode 100644 src/pages/background/request-handler.ts create mode 100644 src/pages/background/v3-rule-converter.ts create mode 100644 src/pages/options/components/v3-migration-guide.tsx diff --git a/.eslintignore b/.eslintignore index 842c4df1..5d6ea794 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ demo/ .ice/ scripts/ locale/ +dist/ # node 覆盖率文件 coverage/ diff --git a/.eslintrc.js b/.eslintrc.js index 9e97dc7f..73da16dc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,12 +31,12 @@ module.exports = getESLintConfig('react-ts', { { 'pattern': '@/**', 'group': 'parent', - 'position': 'before' - } + 'position': 'before', + }, ], 'pathGroupsExcludedImportTypes': ['builtin'], - 'newlines-between': 'never' - } - ] + 'newlines-between': 'never', + }, + ], }, }); diff --git a/DEBUG_GUIDE.md b/DEBUG_GUIDE.md new file mode 100644 index 00000000..881f65c0 --- /dev/null +++ b/DEBUG_GUIDE.md @@ -0,0 +1,166 @@ +# Header Editor V3 调试指南 + +## 问题:请求网址时没有添加 x-tag header + +### 🔍 调试步骤 + +#### 1. 检查规则是否已创建 +首先确认您是否已经正确创建了添加 x-tag header 的规则: + +**在 Header Editor 选项页面:** +1. 打开 Chrome 扩展管理页面 (`chrome://extensions/`) +2. 找到 Header Editor,点击"详细信息" +3. 点击"扩展程序选项" +4. 检查是否有类似以下的规则: + - 规则类型:修改发送头 + - 头名称:x-tag (或 X-Tag) + - 头内容:您想要的值 + - 匹配类型:全部(或特定模式) + - 启用状态:✅ 已启用 + +#### 2. 使用调试工具检查规则状态 +打开浏览器开发者工具 (F12),在 Console 中执行: + +```javascript +// 检查 x-tag 规则的完整状态 +testXTagHeaderRule() +``` + +这个命令将: +- 检查当前的规则 +- 如果没有 x-tag 规则,自动创建一个测试规则 +- 验证规则转换是否正确 +- 检查 V3 规则是否正确应用 + +#### 3. 检查扩展是否被禁用 +```javascript +// 检查扩展是否被禁用 +chrome.runtime.sendMessage({method: 'getRuleStats'}) +``` + +#### 4. 启用调试日志 +```javascript +// 启用详细的调试日志 +chrome.runtime.sendMessage({method: 'enableDebugLogging'}) +``` + +启用后,刷新页面并查看控制台输出,查找相关的规则应用信息。 + +#### 5. 验证规则是否生效 +访问 https://httpbin.org/headers 来检查请求头: + +1. 打开 https://httpbin.org/headers +2. 查看返回的 JSON 中的 `headers` 字段 +3. 检查是否包含您的 x-tag header + +**或者在开发者工具中检查:** +1. 打开开发者工具 (F12) +2. 切换到 Network 标签 +3. 刷新页面 +4. 点击任意请求 +5. 在 Headers 标签中查看 Request Headers +6. 查找您的 x-tag header + +### 🚨 常见问题和解决方案 + +#### 问题 1:规则存在但没有生效 +**可能原因:** +- 规则转换失败 +- V3 规则没有正确应用 +- 匹配条件不正确 + +**解决方案:** +```javascript +// 强制刷新规则 +chrome.runtime.sendMessage({method: 'testRuleApplication'}) +``` + +#### 问题 2:规则转换失败 +**可能原因:** +- 规则格式不正确 +- 包含不支持的功能 + +**解决方案:** +确保规则满足以下条件: +- 规则类型为 `modifySendHeader` +- 不使用自定义函数 (`isFunction: false`) +- Header 名称和值都不为空 + +#### 问题 3:V3 规则限制 +**可能原因:** +- Chrome 的 declarativeNetRequest 有一些限制 + +**解决方案:** +- 确保 header 名称符合 HTTP 规范 +- 避免使用特殊字符 +- 检查是否超过规则数量限制 + +### 🔧 手动创建测试规则 + +如果自动创建不工作,可以手动创建: + +1. 打开 Header Editor 选项页面 +2. 点击"添加规则" +3. 填写以下信息: + - 名称:`测试 X-Tag Header` + - 规则类型:`修改发送头` + - 匹配类型:`全部` + - 执行类型:`普通` + - 头名称:`X-Tag` + - 头内容:`test-value` +4. 点击保存 + +### 📊 检查规则统计 + +```javascript +// 获取当前规则统计 +chrome.runtime.sendMessage({method: 'getRuleStats'}).then(response => { + console.log('规则统计:', response.stats); +}); + +// 获取当前应用的 V3 规则 +chrome.declarativeNetRequest.getDynamicRules().then(rules => { + console.log('当前 V3 规则:', rules); + const headerRules = rules.filter(rule => + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders + ); + console.log('修改请求头的规则:', headerRules); +}); +``` + +### 🔍 深度调试 + +如果上述步骤都没有解决问题,请: + +1. **收集调试信息:** + ```javascript + // 收集完整的调试信息 + const debugInfo = { + rules: await chrome.runtime.sendMessage({method: 'getRuleStats'}), + v3Rules: await chrome.declarativeNetRequest.getDynamicRules(), + permissions: await chrome.permissions.getAll() + }; + console.log('调试信息:', debugInfo); + ``` + +2. **检查权限:** + 确保扩展有以下权限: + - `declarativeNetRequest` + - `declarativeNetRequestWithHostAccess` + - 相关的主机权限 + +3. **检查浏览器版本:** + 确保使用 Chrome 88+ 或其他支持 Manifest V3 的浏览器 + +### 📝 报告问题 + +如果问题仍然存在,请提供以下信息: + +1. 浏览器版本 +2. Header Editor 版本 +3. 创建的规则详情 +4. 调试输出结果 +5. 期望的行为 vs 实际行为 + +这将帮助我们更好地诊断和解决问题。 \ No newline at end of file diff --git a/MIGRATION_REPORT.md b/MIGRATION_REPORT.md new file mode 100644 index 00000000..d6d2fe6c --- /dev/null +++ b/MIGRATION_REPORT.md @@ -0,0 +1,151 @@ +# Header Editor Manifest V3 迁移报告 + +## 项目概述 +Header Editor 是一个浏览器扩展,允许用户修改 HTTP 请求和响应头。本项目已完成从 Manifest V2 到 Manifest V3 的迁移。 + +## 最新修复 (2024-12-19) + +### 问题诊断 +经过代码审查发现以下关键问题: +1. **规则更新事件监听缺失** - 规则变化时没有自动重新应用 V3 规则 +2. **规则转换逻辑不完善** - V3RuleConverter 存在类型错误和转换不准确 +3. **初始化时序问题** - 数据库未完全准备好就开始应用规则 +4. **调试信息不足** - 缺少详细的调试日志来排查问题 + +### 修复内容 + +#### 1. 事件监听系统 (src/pages/background/index.ts) +- ✅ 添加规则更新事件监听 (`EVENTs.RULE_UPDATE`, `EVENTs.RULE_DELETE`) +- ✅ 添加偏好设置变化监听 (`disable-all` 设置) +- ✅ 改进初始化流程,等待数据库和规则缓存完全准备 +- ✅ 添加自动规则刷新机制 +- ✅ 添加测试接口和调试命令 + +#### 2. 规则转换器优化 (src/pages/background/v3-rule-converter.ts) +- ✅ 修复类型定义问题,使用正确的 declarativeNetRequest 类型 +- ✅ 改进规则验证逻辑,确保生成的 V3 规则有效 +- ✅ 优化资源类型设置,移除不支持的类型 +- ✅ 添加批量规则应用,避免一次性应用过多规则 +- ✅ 改进错误处理和日志记录 +- ✅ 添加规则转换详细日志记录 + +#### 3. API 处理器增强 (src/pages/background/api-handler.ts) +- ✅ 在所有规则操作后自动触发 V3 规则刷新 +- ✅ 添加详细的操作日志记录 +- ✅ 改进错误处理和状态反馈 +- ✅ 添加规则统计信息接口 + +#### 4. 日志系统升级 (src/share/core/logger.ts) +- ✅ 重构日志系统,支持不同日志级别 +- ✅ 添加专用的规则转换日志方法 +- ✅ 添加性能统计日志 +- ✅ 添加扩展状态日志 +- ✅ 改进日志格式和上下文信息 + +### 核心改进 + +#### 自动规则同步 +现在规则的任何变化都会自动触发 V3 规则的重新应用: +- 创建新规则 → 自动应用到 declarativeNetRequest +- 修改现有规则 → 自动更新 declarativeNetRequest +- 删除规则 → 自动从 declarativeNetRequest 移除 +- 偏好设置变化 → 自动调整规则应用状态 + +#### 规则转换改进 +- 更准确的规则类型识别和转换 +- 更严格的规则验证 +- 更好的错误处理和回退机制 +- 更详细的转换统计和日志 + +#### 调试和测试功能 +- 可通过 console 调用 `testRuleApplication()` 进行测试 +- 支持动态启用/禁用调试日志 +- 提供详细的规则转换和应用统计 +- 支持获取当前规则状态和转换结果 + +## 主要变化说明 + +### 1. 网络请求处理 +- **V2**: 使用 `chrome.webRequest` API 进行实时拦截和修改 +- **V3**: 使用 `chrome.declarativeNetRequest` API 进行声明式规则处理 + +### 2. 背景脚本 +- **V2**: 持久化背景页面 (`background.html`) +- **V3**: 服务工作者 (`background.js`) + +### 3. 权限系统 +- **V2**: `webRequestBlocking` 权限 +- **V3**: `declarativeNetRequest` 和 `declarativeNetRequestWithHostAccess` 权限 + +### 4. 规则应用方式 +- **V2**: 运行时动态处理每个请求 +- **V3**: 预配置规则集,由浏览器引擎处理 + +## 功能限制 + +### 不支持的功能 +1. **自定义 JavaScript 函数规则** - V3 不允许执行任意代码 +2. **响应体修改** - declarativeNetRequest 不支持响应体修改 +3. **复杂正则表达式** - V3 API 对正则表达式有限制 +4. **动态 IP 获取** - 无法在 V3 中获取客户端 IP + +### 功能替代方案 +- 简单的头部修改 → 使用 `modifyHeaders` 动作 +- URL 重定向 → 使用 `redirect` 动作 +- 请求阻止 → 使用 `block` 动作 +- 域名匹配 → 使用 `domains` 条件 + +## 测试验证 + +### 测试方法 +1. 打开浏览器开发者工具 +2. 切换到 Console 标签 +3. 执行 `testRuleApplication()` 进行功能测试 +4. 检查规则转换统计和应用结果 + +### 测试内容 +- 规则转换正确性 +- 规则应用成功率 +- 事件监听响应 +- 错误处理能力 +- 性能表现 + +## 部署建议 + +### 开发环境 +- 启用调试日志: `chrome.runtime.sendMessage({method: 'enableDebugLogging'})` +- 运行测试: `testRuleApplication()` +- 检查规则统计: `chrome.runtime.sendMessage({method: 'getRuleStats'})` + +### 生产环境 +- 默认使用 INFO 级别日志 +- 定期检查规则应用状态 +- 监控转换失败的规则 + +## 后续工作建议 + +1. **用户体验优化** + - 添加规则转换失败的用户提示 + - 提供规则迁移建议 + - 改进错误消息的用户友好性 + +2. **功能增强** + - 支持更多的 URL 匹配模式 + - 优化规则优先级管理 + - 添加规则冲突检测 + +3. **性能优化** + - 优化大量规则的处理性能 + - 减少规则应用的延迟 + - 改进内存使用 + +4. **稳定性改进** + - 增强错误恢复机制 + - 添加规则备份和恢复功能 + - 提高扩展的崩溃恢复能力 + +## 结论 + +本次修复解决了 Header Editor 在 Manifest V3 环境下的核心问题,确保了规则的正确转换和应用。通过完善的事件监听、详细的日志记录和自动化测试,显著提升了扩展的稳定性和可维护性。 + +虽然 V3 的限制导致部分高级功能无法使用,但对于大多数用户的基本需求(请求头修改、URL 重定向、请求阻止等),扩展已能够正常工作。 \ No newline at end of file diff --git a/package.json b/package.json index b2eefc74..59c8161b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "header-editor", - "version": "5.0.0", + "version": "6.0.0", "description": "Header Editor", "author": "ShuangYa", "license": "GPL-2.0", diff --git a/src/enterprise/policy-handler.ts b/src/enterprise/policy-handler.ts new file mode 100644 index 00000000..d0bddded --- /dev/null +++ b/src/enterprise/policy-handler.ts @@ -0,0 +1,371 @@ +import browser from 'webextension-polyfill'; +import logger from '@/share/core/logger'; +import { prefs } from '@/share/core/prefs'; + +/** + * 企业策略支持处理器 + * 在企业环境中提供完整的功能支持 + */ +export class EnterpriseSupport { + private static instance: EnterpriseSupport; + private isEnterpriseEnvironment = false; + private managementInfo: any = null; + private policyInfo: any = null; + + constructor() { + this.detectEnterpriseEnvironment(); + } + + static getInstance(): EnterpriseSupport { + if (!EnterpriseSupport.instance) { + EnterpriseSupport.instance = new EnterpriseSupport(); + } + return EnterpriseSupport.instance; + } + + /** + * 检测是否在企业环境中 + */ + private async detectEnterpriseEnvironment(): Promise { + try { + // 检查扩展是否由企业策略安装 + const managementInfo = await browser.management.getSelf(); + this.managementInfo = managementInfo; + + // 检查安装类型 + const isEnterpriseInstalled = managementInfo.installType === 'admin' || + managementInfo.installType === 'development'; + + // 检查是否有企业策略 + const hasPolicySupport = await this.checkPolicySupport(); + + this.isEnterpriseEnvironment = isEnterpriseInstalled || hasPolicySupport; + + if (this.isEnterpriseEnvironment) { + logger.info('检测到企业环境,启用完整功能支持'); + await this.enableEnterpriseFeatures(); + } + } catch (error) { + logger.error('检测企业环境时发生错误:', error); + this.isEnterpriseEnvironment = false; + } + } + + /** + * 检查企业策略支持 + */ + private async checkPolicySupport(): Promise { + try { + // 检查是否有企业策略 API + if (!browser.enterprise || !browser.enterprise.platformKeys) { + return false; + } + + // 尝试检查企业策略设置 + // 这里可以添加更多的企业策略检测逻辑 + return true; + } catch (error) { + logger.debug('企业策略检测失败:', error); + return false; + } + } + + /** + * 启用企业功能 + */ + private async enableEnterpriseFeatures(): Promise { + try { + // 记录企业环境信息 + await prefs.set('enterprise_mode', { + enabled: true, + installType: this.managementInfo?.installType, + timestamp: Date.now(), + }); + + // 启用完整的 webRequest 功能 + await this.enableFullWebRequestSupport(); + + // 设置企业特定的配置 + await this.applyEnterpriseConfiguration(); + + logger.info('企业功能已启用'); + } catch (error) { + logger.error('启用企业功能时发生错误:', error); + } + } + + /** + * 启用完整的 webRequest 支持 + */ + private async enableFullWebRequestSupport(): Promise { + try { + // 在企业环境中,可以使用完整的 webRequest API + // 这里设置相关的配置标志 + await prefs.set('webRequest_enterprise_enabled', true); + + // 通知其他组件使用完整功能 + logger.info('企业环境中启用完整 webRequest 支持'); + } catch (error) { + logger.error('启用 webRequest 支持时发生错误:', error); + } + } + + /** + * 应用企业配置 + */ + private async applyEnterpriseConfiguration(): Promise { + try { + // 企业特定的配置 + const enterpriseConfig = { + // 允许更多的规则数量 + maxRules: 100000, + // 允许复杂的正则表达式 + allowComplexRegex: true, + // 允许用户自定义函数 + allowCustomFunctions: true, + // 允许响应体修改 + allowResponseBodyModification: true, + // 禁用一些限制 + disableV3Restrictions: true, + }; + + await prefs.set('enterprise_config', enterpriseConfig); + logger.info('企业配置已应用:', enterpriseConfig); + } catch (error) { + logger.error('应用企业配置时发生错误:', error); + } + } + + /** + * 检查当前是否为企业环境 + */ + isEnterpriseMode(): boolean { + return this.isEnterpriseEnvironment; + } + + /** + * 获取企业信息 + */ + getEnterpriseInfo(): { + isEnterprise: boolean; + installType?: string; + hasFullSupport: boolean; + supportedFeatures: string[]; + } { + return { + isEnterprise: this.isEnterpriseEnvironment, + installType: this.managementInfo?.installType, + hasFullSupport: this.isEnterpriseEnvironment, + supportedFeatures: this.isEnterpriseEnvironment ? [ + 'webRequest', + 'customFunctions', + 'responseBodyModification', + 'unlimitedRules', + 'complexRegex', + ] : [], + }; + } + + /** + * 请求企业策略权限 + */ + async requestEnterprisePermissions(): Promise { + try { + if (!this.isEnterpriseEnvironment) { + logger.warn('非企业环境,无法请求企业权限'); + return false; + } + + // 请求额外的权限 + const granted = await browser.permissions.request({ + permissions: ['webRequest', 'webRequestBlocking', 'management'], + origins: ['*://*/*'], + }); + + if (granted) { + logger.info('企业权限已授予'); + await this.enableEnterpriseFeatures(); + } + + return granted; + } catch (error) { + logger.error('请求企业权限时发生错误:', error); + return false; + } + } + + /** + * 获取企业策略配置 + */ + async getEnterpriseConfig(): Promise { + try { + return await prefs.get('enterprise_config') || {}; + } catch (error) { + logger.error('获取企业配置时发生错误:', error); + return {}; + } + } + + /** + * 设置企业策略配置 + */ + async setEnterpriseConfig(config: any): Promise { + try { + if (!this.isEnterpriseEnvironment) { + throw new Error('非企业环境,无法设置企业配置'); + } + + await prefs.set('enterprise_config', config); + logger.info('企业配置已更新:', config); + } catch (error) { + logger.error('设置企业配置时发生错误:', error); + throw error; + } + } + + /** + * 生成企业部署指南 + */ + generateDeploymentGuide(): { + policyTemplate: any; + installationSteps: string[]; + configurationOptions: any; + } { + return { + policyTemplate: { + '3rdparty': { + extensions: { + 'headereditor@addon.firefoxcn.net': { + enterprise_mode: true, + max_rules: 100000, + allow_custom_functions: true, + allow_response_body_modification: true, + disable_v3_restrictions: true, + }, + }, + }, + }, + installationSteps: [ + '1. 下载企业版 Header Editor 扩展包', + '2. 创建企业策略配置文件', + '3. 通过 Group Policy 或 MDM 部署策略', + '4. 在目标机器上安装扩展', + '5. 验证企业功能是否正常工作', + ], + configurationOptions: { + maxRules: { + description: '最大规则数量', + type: 'number', + default: 100000, + min: 1000, + max: 1000000, + }, + allowCustomFunctions: { + description: '允许自定义函数', + type: 'boolean', + default: true, + }, + allowResponseBodyModification: { + description: '允许响应体修改', + type: 'boolean', + default: true, + }, + disableV3Restrictions: { + description: '禁用 V3 限制', + type: 'boolean', + default: true, + }, + }, + }; + } + + /** + * 验证企业功能 + */ + async validateEnterpriseFeatures(): Promise<{ + isValid: boolean; + availableFeatures: string[]; + missingFeatures: string[]; + recommendations: string[]; + }> { + try { + const availableFeatures: string[] = []; + const missingFeatures: string[] = []; + const recommendations: string[] = []; + + // 检查 webRequest 权限 + const permissions = await browser.permissions.getAll(); + if (permissions.permissions?.includes('webRequest')) { + availableFeatures.push('webRequest'); + } else { + missingFeatures.push('webRequest'); + recommendations.push('需要通过企业策略授予 webRequest 权限'); + } + + // 检查管理权限 + if (permissions.permissions?.includes('management')) { + availableFeatures.push('management'); + } else { + missingFeatures.push('management'); + } + + // 检查企业配置 + const enterpriseConfig = await this.getEnterpriseConfig(); + if (Object.keys(enterpriseConfig).length > 0) { + availableFeatures.push('enterpriseConfig'); + } else { + missingFeatures.push('enterpriseConfig'); + recommendations.push('需要设置企业配置'); + } + + return { + isValid: missingFeatures.length === 0, + availableFeatures, + missingFeatures, + recommendations, + }; + } catch (error) { + logger.error('验证企业功能时发生错误:', error); + return { + isValid: false, + availableFeatures: [], + missingFeatures: ['unknown'], + recommendations: ['验证过程中发生错误,请检查日志'], + }; + } + } + + /** + * 获取企业支持状态 + */ + async getStatus(): Promise<{ + isSupported: boolean; + installType: string; + features: any; + config: any; + validation: any; + }> { + try { + const validation = await this.validateEnterpriseFeatures(); + + return { + isSupported: this.isEnterpriseEnvironment, + installType: this.managementInfo?.installType || 'unknown', + features: this.getEnterpriseInfo(), + config: await this.getEnterpriseConfig(), + validation, + }; + } catch (error) { + logger.error('获取企业支持状态时发生错误:', error); + return { + isSupported: false, + installType: 'unknown', + features: { isEnterprise: false, hasFullSupport: false, supportedFeatures: [] }, + config: {}, + validation: { isValid: false, availableFeatures: [], missingFeatures: [], recommendations: [] }, + }; + } + } +} + +export default EnterpriseSupport; diff --git a/src/manifest.json b/src/manifest.json index c53f18b3..5ce244f0 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -4,39 +4,45 @@ "version": null, "description": "__MSG_description__", "homepage_url": "https://he.firefoxcn.net", - "manifest_version": 2, + "manifest_version": 3, "icons": { "128": "assets/images/128.png" }, "permissions": [ "tabs", - "webRequest", - "webRequestBlocking", "storage", - "*://*/*", - "unlimitedStorage" + "notifications", + "declarativeNetRequest", + "declarativeNetRequestFeedback", + "declarativeNetRequestWithHostAccess" ], - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';", + "host_permissions": [ + "*://*/*" + ], + "declarative_net_request": { + "rule_resources": [] + }, + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" + }, "background": { - "scripts": [ - "assets/js/background.js" - ] + "service_worker": "assets/js/background.js" }, - "content_scripts": [ - { - "matches": [""], - "css": [ - "assets/css/content.css" - ], - "js": [ - "external/react.min.js", - "external/react-dom.min.js", - "assets/js/content.js" - ], - "run_at": "document_end" - } - ], - "browser_action": { + "content_scripts": [ + { + "matches": [""], + "css": [ + "assets/css/content.css" + ], + "js": [ + "external/react.min.js", + "external/react-dom.min.js", + "assets/js/content.js" + ], + "run_at": "document_end" + } + ], + "action": { "default_icon": { "128": "assets/images/128.png" }, @@ -47,18 +53,5 @@ "options_ui": { "page": "options.html", "open_in_tab": true - }, - "__amo__browser_specific_settings": { - "gecko": { - "id": "headereditor-amo@addon.firefoxcn.net", - "strict_min_version": "77.0" - } - }, - "__xpi__browser_specific_settings": { - "gecko": { - "id": "headereditor@addon.firefoxcn.net", - "strict_min_version": "77.0", - "update_url": "https://ext.firefoxcn.net/header-editor/install/update.json" - } - } + } } \ No newline at end of file diff --git a/src/pages/background/api-handler.ts b/src/pages/background/api-handler.ts index 8b46c64a..d5ae09f2 100644 --- a/src/pages/background/api-handler.ts +++ b/src/pages/background/api-handler.ts @@ -6,112 +6,229 @@ import rules from './core/rules'; import { openURL } from './utils'; import { getDatabase } from './core/db'; +// 获取全局规则处理器 +function getRuleHandler() { + return (globalThis as any).headerEditorRuleHandler; +} function execute(request: any) { + logger.debug('执行 API 请求:', request); + if (request.method === 'notifyBackground') { request.method = request.reason; delete request.reason; } + switch (request.method) { case APIs.HEALTH_CHECK: return new Promise((resolve) => { getDatabase() - .then(() => resolve(true)) - .catch(() => resolve(false)); + .then(() => { + logger.debug('健康检查通过'); + resolve(true); + }) + .catch((error) => { + logger.error('健康检查失败:', error); + resolve(false); + }); }); + case APIs.OPEN_URL: + logger.debug('打开URL:', request.url); return openURL(request); - case APIs.GET_RULES: - return Promise.resolve(rules.get(request.type, request.options)); + + case APIs.GET_RULES: { + logger.debug('获取规则:', { type: request.type, options: request.options }); + const rulesResult = rules.get(request.type, request.options); + logger.debug('规则查询结果:', rulesResult?.length || 0, '条规则'); + return Promise.resolve(rulesResult); + } + case APIs.SAVE_RULE: + logger.debug('保存规则:', request.rule); + + // 通知活动标签页 browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { tabs.forEach((tab) => { if (tab.id) { - console.log('[Header Editor] 向 content-script 发送消息', { tabId: tab.id, method: APIs.SAVE_RULE, rule: request.rule }); + logger.debug('向 content-script 发送保存规则消息', { tabId: tab.id, rule: request.rule }); browser.tabs.sendMessage(tab.id, { method: APIs.SAVE_RULE, rule: request.rule }); } }); }); - return rules.save(request.rule); + + // 保存规则并刷新 V3 规则 + return rules.save(request.rule).then((result) => { + logger.info('规则保存成功:', result); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(保存规则后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } else { + logger.warn('规则处理器未找到,无法刷新 V3 规则'); + } + + return result; + }).catch((error) => { + logger.error('保存规则失败:', error); + throw error; + }); + case APIs.DELETE_RULE: - return rules.remove(request.type, request.id); + logger.debug('删除规则:', { type: request.type, id: request.id }); + + return rules.remove(request.type, request.id).then((result) => { + logger.info('规则删除成功:', { type: request.type, id: request.id }); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(删除规则后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } else { + logger.warn('规则处理器未找到,无法刷新 V3 规则'); + } + + return result; + }).catch((error) => { + logger.error('删除规则失败:', error); + throw error; + }); + case APIs.SET_PREFS: + logger.debug('设置偏好:', { key: request.key, value: request.value }); + + // 通知活动标签页 browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { tabs.forEach((tab) => { if (tab.id) { - console.log('[Header Editor] 向 content-script 发送消息', { tabId: tab.id, method: APIs.SET_PREFS, key: request.key, value: request.value }); + logger.debug('向 content-script 发送偏好设置消息', { tabId: tab.id, key: request.key, value: request.value }); browser.tabs.sendMessage(tab.id, { method: APIs.SET_PREFS, key: request.key, value: request.value }); } }); }); - return prefs.set(request.key, request.value); + + return prefs.set(request.key, request.value).then((result) => { + logger.info('偏好设置成功:', { key: request.key, value: request.value }); + + // 如果是禁用/启用扩展,触发 V3 规则刷新 + if (request.key === 'disable-all') { + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(偏好设置变化)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + } + + return result; + }).catch((error) => { + logger.error('设置偏好失败:', error); + throw error; + }); + case APIs.UPDATE_CACHE: + logger.debug('更新缓存:', request.type); + if (request.type === 'all') { - return Promise.all(TABLE_NAMES_ARR.map((tableName) => rules.updateCache(tableName))); + return Promise.all(TABLE_NAMES_ARR.map((tableName) => { + logger.debug('更新表缓存:', tableName); + return rules.updateCache(tableName); + })).then((results) => { + logger.info('所有表缓存更新完成'); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(缓存更新后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + + return results; + }); } else { - return rules.updateCache(request.type); + return rules.updateCache(request.type).then((result) => { + logger.info('表缓存更新完成:', request.type); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(缓存更新后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + + return result; + }); } + default: + logger.warn('未知的 API 方法:', request.method); break; } // return false; } -const currentIP = {}; - export default function createApiHandler() { - // get IP using webRequest - browser.webRequest.onCompleted.addListener( - (info) => { - const u = new URL(info.url); - if (info.tabId in currentIP) { - currentIP[info.tabId][u.hostname] = info.ip; - } else { - currentIP[info.tabId] = { [u.hostname]: info.ip }; - } - }, - { - urls: [], - types: [], - }, - [], - ); + logger.info('创建 API 处理器'); browser.runtime.onMessage.addListener((request: any, sender, sendResponse) => { if (request.method === 'GetData') { - console.log('[Header Editor] 收到来自 content-script 的消息', { request, sender }); - const currentIPList: Array<{ domain: string; ip: string }> = []; - const tabId = sender.tab?.id || 0; - if (tabId in currentIP) { - for (const key in currentIP[tabId]) { - if (Object.prototype.hasOwnProperty.call(currentIP[tabId], key)) { - currentIPList.push({ domain: key, ip: currentIP[tabId][key] }); - } - } - } + logger.debug('收到来自 content-script 的 GetData 请求', { request, sender }); const response = { rules: rules.get(TABLE_NAMES.sendHeader), enableRules: rules.get(TABLE_NAMES.sendHeader, { enable: true }), enable: !prefs.get('disable-all'), - currentIPList, + currentIPList: [], // V3 中无法获取IP信息 }; - console.log('[Header Editor] 返回 content-script 的数据', response); + logger.debug('返回 content-script 的数据', { + rulesCount: response.rules?.length || 0, + enableRulesCount: response.enableRules?.length || 0, + enable: response.enable, + }); + sendResponse(response); + return; } - logger.debug('Background Receive Message', request); + logger.debug('Background 收到消息', request); + if (request.method === 'batchExecute') { - const queue = request.batch.map((item) => { + logger.debug('执行批量操作:', request.batch?.length || 0, '个操作'); + + const queue = request.batch.map((item: any) => { const res = execute(item); if (res) { return res; } return Promise.resolve(); }); - return Promise.allSettled(queue); + + return Promise.allSettled(queue).then((results) => { + logger.debug('批量操作完成:', results.length, '个结果'); + return results; + }); + } + + const result = execute(request); + if (result && typeof result.then === 'function') { + result.catch((error) => { + logger.error('API 执行失败:', error); + }); } - return execute(request); + + return result; }); } diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index a38158a6..93028244 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -1,13 +1,500 @@ +// Header Editor Manifest V3 背景脚本 +// 导入必要的模块 +import { prefs } from '@/share/core/prefs'; +import { TABLE_NAMES_ARR, TABLE_NAMES, EVENTs, RULE_TYPE, RULE_MATCH_TYPE } from '@/share/core/constant'; +import logger from '@/share/core/logger'; +import notify from '@/share/core/notify'; import createApiHandler from './api-handler'; -import createRequestHandler from './request-handler'; -import './upgrade'; +import { V3RuleConverter } from './v3-rule-converter'; +import rules from './core/rules'; -if (typeof window !== 'undefined') { - window.IS_BACKGROUND = true; +console.log('Header Editor Service Worker 启动'); + +// 设置全局标识 +if (typeof globalThis !== 'undefined') { + globalThis.IS_BACKGROUND = true; } -console.log('background/index.ts'); +// 规则处理器 +class V3RuleHandler { + private conversionStats: any = null; + private isInitialized = false; + + async initialize(): Promise { + console.log('初始化 V3 规则处理器...'); + + // 等待偏好设置准备完成 + await new Promise((resolve) => { + prefs.ready(() => { + console.log('偏好设置已准备完成'); + resolve(); + }); + }); + + // 等待数据库和规则缓存完全准备好 + await this.waitForRulesReady(); + + // 创建API处理器 + createApiHandler(); + console.log('API处理器已创建'); + + // 设置规则变化监听 + this.setupRuleEventListeners(); + + // 加载并应用规则 + await this.loadAndApplyRules(); + + this.isInitialized = true; + console.log('V3 规则处理器初始化完成'); + } + + private async waitForRulesReady(): Promise { + console.log('等待规则缓存准备完成...'); + + // 等待所有表的缓存更新完成 + const maxRetries = 30; // 最多等待30秒 + let retries = 0; + + while (retries < maxRetries) { + let allReady = true; + + for (const tableName of TABLE_NAMES_ARR) { + const tableRules = rules.get(tableName); + if (tableRules === null) { + allReady = false; + break; + } + } + + if (allReady) { + console.log('所有规则缓存已准备完成'); + return; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + retries++; + } + + logger.warn('等待规则缓存超时,但继续初始化'); + } + + private setupRuleEventListeners(): void { + console.log('设置规则变化事件监听...'); + + // 监听规则更新事件 + notify.event.on(EVENTs.RULE_UPDATE, (event: any) => { + console.log('收到规则更新事件:', event); + this.handleRuleChange(); + }); + + // 监听规则删除事件 + notify.event.on(EVENTs.RULE_DELETE, (event: any) => { + console.log('收到规则删除事件:', event); + this.handleRuleChange(); + }); + } + + private async handleRuleChange(): Promise { + if (!this.isInitialized) { + console.log('规则处理器尚未初始化,跳过规则变化处理'); + return; + } + + try { + console.log('处理规则变化,重新应用规则...'); + await this.loadAndApplyRules(); + console.log('规则变化处理完成'); + } catch (error) { + logger.error('处理规则变化时发生错误:', error); + console.error('处理规则变化失败:', error); + } + } + + async loadAndApplyRules(): Promise { + try { + // 检查扩展是否被禁用 + if (prefs.get('disable-all')) { + console.log('扩展已被禁用,清除所有规则'); + await V3RuleConverter.applyV3Rules([]); + return; + } + + // 获取所有启用的规则 + const allRules: any[] = []; + for (const tableName of TABLE_NAMES_ARR) { + const tableRules = rules.get(tableName, { enable: true }) || []; + allRules.push(...tableRules); + } + + console.log(`加载了 ${allRules.length} 个启用的规则`); + + if (allRules.length === 0) { + // 清除所有动态规则 + await V3RuleConverter.applyV3Rules([]); + logger.info('没有启用的规则,已清除所有动态规则'); + return; + } + + // 转换规则为 V3 格式 + const conversionResult = V3RuleConverter.convertRulesToV3(allRules); + this.conversionStats = conversionResult; + + // 检查规则限制 + const limitCheck = V3RuleConverter.checkRuleLimits(conversionResult.convertedRules); + if (!limitCheck.isValid) { + logger.warn('规则超过 V3 限制:', limitCheck.errors); + console.warn('规则超过 V3 限制:', limitCheck.errors); + } + + // 应用 V3 规则 + await V3RuleConverter.applyV3Rules(conversionResult.convertedRules); + + const stats = { + total: allRules.length, + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings.length, + }; + + logger.info('规则应用完成:', stats); + console.log('规则应用统计:', stats); + + if (conversionResult.unconvertedRules.length > 0) { + console.warn('无法转换的规则:', conversionResult.unconvertedRules.map((r) => r.name)); + } + + if (conversionResult.warnings.length > 0) { + console.warn('转换警告:', conversionResult.warnings); + } + } catch (error) { + logger.error('应用规则时发生错误:', error); + console.error('应用规则失败:', error); + throw error; + } + } + + async refresh(): Promise { + console.log('刷新规则...'); + await this.loadAndApplyRules(); + } + + getStats(): any { + return this.conversionStats; + } +} + +// 测试 x-tag header 规则 +async function testXTagHeaderRule() { + console.log('开始测试 x-tag header 规则...'); + + try { + // 1. 检查当前规则 + console.log('1. 检查现有的 sendHeader 规则...'); + const currentSendRules = rules.get(TABLE_NAMES.sendHeader, { enable: true }) || []; + console.log(`当前有 ${currentSendRules.length} 个启用的发送头规则`); + + const xTagRules = currentSendRules.filter((rule) => + rule.action && + typeof rule.action === 'object' && + rule.action.name && + rule.action.name.toLowerCase() === 'x-tag'); + + console.log(`其中有 ${xTagRules.length} 个 x-tag 规则:`, xTagRules); + + // 2. 创建测试规则(如果不存在) + if (xTagRules.length === 0) { + console.log('2. 创建测试 x-tag 规则...'); + const testXTagRule = { + id: -1, // 新规则 + name: '测试 X-Tag Header', + enable: true, + ruleType: RULE_TYPE.MODIFY_SEND_HEADER, + matchType: RULE_MATCH_TYPE.ALL, + pattern: '*', + isFunction: false, + code: '', + exclude: '', + group: 'test', + action: { + name: 'X-Tag', + value: 'HeaderEditor-Test', + }, + }; + + console.log('准备保存测试规则:', testXTagRule); + const savedRule = await rules.save(testXTagRule); + console.log('测试规则保存成功:', savedRule); + + // 等待一下让事件处理完成 + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + // 3. 重新检查规则 + console.log('3. 重新检查规则...'); + const updatedSendRules = rules.get(TABLE_NAMES.sendHeader, { enable: true }) || []; + const updatedXTagRules = updatedSendRules.filter((rule) => + rule.action && + typeof rule.action === 'object' && + rule.action.name && + rule.action.name.toLowerCase().includes('tag')); + console.log(`现在有 ${updatedXTagRules.length} 个 tag 相关规则:`, updatedXTagRules); + + // 4. 检查规则转换 + console.log('4. 测试规则转换...'); + if (updatedXTagRules.length > 0) { + const conversionResult = V3RuleConverter.convertRulesToV3(updatedXTagRules); + console.log('规则转换结果:', { + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings, + }); + + if (conversionResult.convertedRules.length > 0) { + console.log('转换后的 V3 规则:', conversionResult.convertedRules); + } + + if (conversionResult.warnings.length > 0) { + console.warn('转换警告:', conversionResult.warnings); + } + } -// 开始初始化 -createApiHandler(); -createRequestHandler(); + // 5. 检查当前应用的 V3 规则 + console.log('5. 检查当前应用的 V3 规则...'); + const currentV3Rules = await V3RuleConverter.getCurrentRules(); + console.log(`当前已应用 ${currentV3Rules.length} 个 V3 规则`); + + const v3HeaderRules = currentV3Rules.filter((rule) => + rule.action && + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders && + rule.action.requestHeaders.some((header) => + header.header && header.header.toLowerCase().includes('tag'))); + + console.log(`其中有 ${v3HeaderRules.length} 个修改 tag header 的 V3 规则:`, v3HeaderRules); + + // 6. 刷新规则应用 + console.log('6. 刷新规则应用...'); + if (ruleHandler) { + await ruleHandler.refresh(); + console.log('规则刷新完成'); + + // 再次检查 V3 规则 + const refreshedV3Rules = await V3RuleConverter.getCurrentRules(); + const refreshedHeaderRules = refreshedV3Rules.filter((rule) => + rule.action && + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders && + rule.action.requestHeaders.some((header) => + header.header && header.header.toLowerCase().includes('tag'))); + + console.log(`刷新后有 ${refreshedHeaderRules.length} 个修改 tag header 的 V3 规则:`, refreshedHeaderRules); + } + + // 7. 提供测试建议 + console.log('7. 测试建议:'); + console.log('请访问任意网站(如 https://httpbin.org/headers)查看请求头是否包含 X-Tag'); + console.log('您也可以在开发者工具的 Network 标签中查看请求头'); + + console.log('x-tag header 规则测试完成'); + } catch (error) { + console.error('测试 x-tag header 规则时发生错误:', error); + } +} + +// 导出测试函数到全局 +if (typeof globalThis !== 'undefined') { + globalThis.testXTagHeaderRule = testXTagHeaderRule; +} + +// 全局规则处理器实例 +let ruleHandler: V3RuleHandler | null = null; + +// 测试辅助函数 +async function testRuleApplication() { + if (!ruleHandler) { + console.error('规则处理器未初始化'); + return; + } + + console.log('开始测试规则应用功能...'); + + try { + // 1. 测试获取现有规则 + console.log('1. 获取现有规则...'); + const currentRules = await V3RuleConverter.getCurrentRules(); + console.log(`当前已应用 ${currentRules.length} 个 V3 规则`); + + // 2. 测试规则转换 + console.log('2. 测试规则转换...'); + const testRule = { + id: 999, + name: '测试规则', + enable: true, + ruleType: RULE_TYPE.MODIFY_SEND_HEADER, + matchType: RULE_MATCH_TYPE.ALL, + pattern: '*', + isFunction: false, + code: '', + exclude: '', + group: 'test', + action: { + name: 'User-Agent', + value: 'Test-Agent', + }, + }; + + const conversionResult = V3RuleConverter.convertRulesToV3([testRule]); + console.log('规则转换结果:', { + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings.length, + }); + + if (conversionResult.convertedRules.length > 0) { + console.log('转换后的 V3 规则:', conversionResult.convertedRules[0]); + } + + // 3. 测试规则限制检查 + console.log('3. 测试规则限制检查...'); + const limits = V3RuleConverter.getRuleLimits(); + const limitCheck = V3RuleConverter.checkRuleLimits(conversionResult.convertedRules); + console.log('规则限制:', limits); + console.log('规则限制检查结果:', limitCheck); + + // 4. 测试规则刷新 + console.log('4. 测试规则刷新...'); + await ruleHandler.refresh(); + console.log('规则刷新完成'); + + // 5. 获取统计信息 + console.log('5. 获取统计信息...'); + const stats = ruleHandler.getStats(); + console.log('转换统计:', stats); + + // 6. 验证规则数量 + console.log('6. 验证规则数量...'); + const newRules = await V3RuleConverter.getCurrentRules(); + console.log(`刷新后已应用 ${newRules.length} 个 V3 规则`); + + console.log('规则应用功能测试完成'); + } catch (error) { + console.error('测试规则应用功能时发生错误:', error); + } +} + +// 导出测试函数到全局 +if (typeof globalThis !== 'undefined') { + globalThis.testRuleApplication = testRuleApplication; +} + +// 初始化函数 +async function initialize() { + try { + console.log('开始初始化 Header Editor...'); + + ruleHandler = new V3RuleHandler(); + await ruleHandler.initialize(); + + console.log('Header Editor 初始化完成'); + + // 在调试模式下运行测试 + if (logger.getLevel() === 'DEBUG') { + setTimeout(() => { + testRuleApplication(); + }, 2000); + } + } catch (error) { + console.error('Header Editor 初始化失败:', error); + + // 显示错误通知 + try { + await chrome.notifications.create({ + type: 'basic', + iconUrl: 'assets/images/128.png', + title: 'Header Editor 初始化失败', + message: '请检查控制台错误信息,或重新加载扩展。', + }); + } catch (notifyError) { + console.error('显示通知失败:', notifyError); + } + } +} + +// 监听规则更新事件 +if (typeof chrome !== 'undefined' && chrome.runtime) { + // 监听安装事件 + chrome.runtime.onInstalled.addListener((details) => { + console.log('Extension installed:', details.reason); + + if (details.reason === 'install') { + // 首次安装时打开选项页面 + try { + chrome.tabs.create({ + url: chrome.runtime.getURL('options.html'), + }); + } catch (error) { + console.log('无法创建选项页面:', error); + } + } + }); + + // 监听启动事件 + chrome.runtime.onStartup.addListener(() => { + console.log('Extension startup'); + }); + + // 监听偏好设置变化 + chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === 'local' && changes['disable-all']) { + console.log('检测到 disable-all 偏好设置变化:', changes['disable-all']); + if (ruleHandler) { + ruleHandler.refresh(); + } + } + }); + + // 监听开发者工具命令 + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.method === 'testRuleApplication') { + testRuleApplication().then(() => { + sendResponse({ success: true }); + }).catch((error) => { + sendResponse({ success: false, error: error.message }); + }); + return true; // 保持消息通道开放 + } + + if (request.method === 'getRuleStats') { + const stats = ruleHandler?.getStats() || null; + sendResponse({ stats }); + return true; + } + + if (request.method === 'enableDebugLogging') { + logger.enableDebug(); + sendResponse({ success: true, level: logger.getLevel() }); + return true; + } + + if (request.method === 'disableDebugLogging') { + logger.disableDebug(); + sendResponse({ success: true, level: logger.getLevel() }); + return true; + } + }); +} + +// 简单的状态检查 +console.log('Service Worker 环境检查:', { + hasChrome: typeof chrome !== 'undefined', + hasRuntime: typeof chrome !== 'undefined' && !!chrome.runtime, + hasDeclarativeNetRequest: typeof chrome !== 'undefined' && !!(chrome as any).declarativeNetRequest, +}); + +// 启动初始化 +initialize().catch(console.error); + +// 导出全局访问 +if (typeof globalThis !== 'undefined') { + globalThis.headerEditorRuleHandler = ruleHandler; +} diff --git a/src/pages/background/request-handler.ts b/src/pages/background/request-handler.ts deleted file mode 100644 index 7c29742f..00000000 --- a/src/pages/background/request-handler.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* eslint-disable @typescript-eslint/member-ordering */ -import { TextDecoder, TextEncoder } from 'text-encoding'; -import browser, { WebRequest } from 'webextension-polyfill'; -import { getGlobal, IS_CHROME, IS_SUPPORT_STREAM_FILTER } from '@/share/core/utils'; -import emitter from '@/share/core/emitter'; -import logger from '@/share/core/logger'; -import type { Rule } from '@/share/core/types'; -import { TABLE_NAMES } from '@/share/core/constant'; -import { prefs } from '@/share/core/prefs'; -import rules from './core/rules'; - -// 最大修改8MB的Body -const MAX_BODY_SIZE = 8 * 1024 * 1024; - -enum REQUEST_TYPE { - REQUEST, - RESPONSE, -} - -type HeaderRequestDetails = WebRequest.OnHeadersReceivedDetailsType | WebRequest.OnBeforeSendHeadersDetailsType; -type AnyRequestDetails = WebRequest.OnBeforeRequestDetailsType | HeaderRequestDetails; -interface CustomFunctionDetail { - id: string; - url: string; - tab: number; - method: string; - frame: number; - parentFrame: number; - // @ts-ignore - proxy: any; - type: WebRequest.ResourceType; - time: number; - originUrl: string; - documentUrl: string; - requestHeaders: WebRequest.HttpHeaders | null; - responseHeaders: WebRequest.HttpHeaders | null; - statusCode?: number; - statusLine?: string; -} -class RequestHandler { - private _disableAll = false; - private excludeHe = true; - private includeHeaders = false; - private modifyBody = false; - private savedRequestHeader = new Map(); - private deleteHeaderTimer: ReturnType | null = null; - private deleteHeaderQueue = new Map(); - private textDecoder: Map = new Map(); - private textEncoder: Map = new Map(); - - constructor() { - this.initHook(); - this.loadPrefs(); - } - get disableAll() { - return this._disableAll; - } - set disableAll(to) { - if (this._disableAll === to) { - return; - } - this._disableAll = to; - browser.browserAction.setIcon({ - path: `/assets/images/128${to ? 'w' : ''}.png`, - }); - } - - private createHeaderListener(type: string): any { - const result = ['blocking']; - result.push(type); - if ( - IS_CHROME && - // @ts-ignore - chrome.webRequest.OnBeforeSendHeadersOptions.hasOwnProperty('EXTRA_HEADERS') - ) { - result.push('extraHeaders'); - } - return result; - } - - private initHook() { - browser.webRequest.onBeforeRequest.addListener(this.handleBeforeRequest.bind(this), { urls: [''] }, [ - 'blocking', - ]); - browser.webRequest.onBeforeSendHeaders.addListener( - this.handleBeforeSend.bind(this), - { urls: [''] }, - this.createHeaderListener('requestHeaders'), - ); - browser.webRequest.onHeadersReceived.addListener( - this.handleReceived.bind(this), - { urls: [''] }, - this.createHeaderListener('responseHeaders'), - ); - } - - private loadPrefs() { - emitter.on(emitter.EVENT_PREFS_UPDATE, (key: string, val: any) => { - switch (key) { - case 'exclude-he': - this.excludeHe = val; - break; - case 'disable-all': - this.disableAll = val; - break; - case 'include-headers': - this.includeHeaders = val; - break; - case 'modify-body': - this.modifyBody = val; - break; - default: - break; - } - }); - - prefs.ready(() => { - this.excludeHe = prefs.get('exclude-he'); - this.disableAll = prefs.get('disable-all'); - this.includeHeaders = prefs.get('include-headers'); - this.modifyBody = prefs.get('modify-body'); - }); - } - - private beforeAll(e: AnyRequestDetails) { - if (this.disableAll) { - return false; - } - // 判断是否是HE自身 - if (this.excludeHe && e.url.indexOf(browser.runtime.getURL('')) === 0) { - return false; - } - return true; - } - - /** - * BeforeRequest事件,可撤销、重定向 - * @param any e - */ - handleBeforeRequest(e: WebRequest.OnBeforeRequestDetailsType) { - if (!this.beforeAll(e)) { - return; - } - logger.debug(`handle before request ${e.url}`, e); - // 可用:重定向,阻止加载 - const rule = rules.get(TABLE_NAMES.request, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule === null) { - return; - } - let redirectTo = e.url; - const detail = this.makeDetails(e); - for (const item of rule) { - if (item.action === 'cancel' && !item.isFunction) { - return { cancel: true }; - } else if (item.isFunction) { - try { - const r = item._func(redirectTo, detail); - if (typeof r === 'string') { - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${r}`); - redirectTo = r; - } - if (r === '_header_editor_cancel_' || (item.action === 'cancel' && r === true)) { - logger.debug(`[rule: ${item.id}] cancel`); - return { cancel: true }; - } - } catch (err) { - console.error(err); - } - } else if (item.to) { - if (item.matchType === 'regexp') { - const to = redirectTo.replace(item._reg, item.to); - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${to}`); - redirectTo = to; - } else { - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${item.to}`); - redirectTo = item.to; - } - } - } - if (redirectTo && redirectTo !== e.url) { - if (/^([a-zA-Z0-9]+)%3A/.test(redirectTo)) { - redirectTo = decodeURIComponent(redirectTo); - } - return { redirectUrl: redirectTo }; - } - } - - /** - * beforeSend事件,可修改请求头 - * @param any e - */ - handleBeforeSend(e: WebRequest.OnBeforeSendHeadersDetailsType) { - if (!this.beforeAll(e)) { - return; - } - // 修改请求头 - if (!e.requestHeaders) { - return; - } - logger.debug(`handle before send ${e.url}`, e.requestHeaders); - const rule = rules.get(TABLE_NAMES.sendHeader, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule === null) { - return; - } - this.modifyHeaders(e, REQUEST_TYPE.REQUEST, rule); - logger.debug(`handle before send:finish ${e.url}`, e.requestHeaders); - return { requestHeaders: e.requestHeaders }; - } - - handleReceived(e: WebRequest.OnHeadersReceivedDetailsType) { - if (!this.beforeAll(e)) { - return; - } - const detail = this.makeDetails(e); - // 删除暂存的headers - if (this.includeHeaders) { - detail.requestHeaders = this.savedRequestHeader.get(e.requestId) || null; - this.savedRequestHeader.delete(e.requestId); - this.deleteHeaderQueue.delete(e.requestId); - } - // 修改响应体 - if (this.modifyBody) { - let canModifyBody = true; - // 检查有没有Content-Length头,如有,则不能超过MAX_BODY_SIZE,否则不进行修改 - if (e.responseHeaders) { - for (const it of e.responseHeaders) { - if (it.name.toLowerCase() === 'content-length') { - if (it.value && parseInt(it.value, 10) >= MAX_BODY_SIZE) { - canModifyBody = false; - } - break; - } - } - } - if (canModifyBody) { - this.modifyReceivedBody(e, detail); - } - } - // 修改响应头 - if (!e.responseHeaders) { - return; - } - logger.debug(`handle received ${e.url}`, e.responseHeaders); - const rule = rules.get(TABLE_NAMES.receiveHeader, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule) { - this.modifyHeaders(e, REQUEST_TYPE.RESPONSE, rule, detail); - } - logger.debug(`handle received:finish ${e.url}`, e.responseHeaders); - return { responseHeaders: e.responseHeaders }; - } - - private makeDetails(request: AnyRequestDetails): CustomFunctionDetail { - const details = { - id: request.requestId, - url: request.url, - tab: request.tabId, - method: request.method, - frame: request.frameId, - parentFrame: request.parentFrameId, - // @ts-ignore - proxy: request.proxyInfo || null, - type: request.type, - time: request.timeStamp, - originUrl: request.originUrl || '', - documentUrl: request.documentUrl || '', - requestHeaders: null, - responseHeaders: null, - }; - - ['statusCode', 'statusLine', 'requestHeaders', 'responseHeaders'].forEach((p) => { - if (p in request) { - // @ts-ignore - details[p] = request[p]; - } - }); - - return details; - } - - private textEncode(encoding: string, text: string) { - let encoder = this.textEncoder.get(encoding); - if (!encoder) { - // UTF-8使用原生API,性能更好 - if (encoding === 'UTF-8' && getGlobal().TextEncoder) { - encoder = new (getGlobal().TextEncoder)(); - } else { - encoder = new TextEncoder(encoding, { NONSTANDARD_allowLegacyEncoding: true }); - } - this.textEncoder.set(encoding, encoder); - } - // 防止解码失败导致整体错误 - try { - return encoder.encode(text); - } catch (e) { - console.error(e); - return new Uint8Array(0); - } - } - - private textDecode(encoding: string, buffer: Uint8Array) { - let encoder = this.textDecoder.get(encoding); - if (!encoder) { - // 如果原生支持的话,优先使用原生 - if (getGlobal().TextDecoder) { - try { - encoder = new (getGlobal().TextDecoder)(encoding); - } catch (e) { - encoder = new TextDecoder(encoding); - } - } else { - encoder = new TextDecoder(encoding); - } - this.textDecoder.set(encoding, encoder); - } - // 防止解码失败导致整体错误 - try { - return encoder.decode(buffer); - } catch (e) { - console.error(e); - return ''; - } - } - - private modifyHeaders( - request: HeaderRequestDetails, - type: REQUEST_TYPE, - rule: Rule[], - presetDetail?: CustomFunctionDetail, - ) { - // @ts-ignore - const headers = request[type === REQUEST_TYPE.REQUEST ? 'requestHeaders' : 'responseHeaders']; - if (!headers) { - return; - } - if (this.includeHeaders && type === REQUEST_TYPE.REQUEST) { - // 暂存headers - this.savedRequestHeader.set( - request.requestId, - (request as WebRequest.OnBeforeSendHeadersDetailsType).requestHeaders, - ); - this.autoDeleteSavedHeader(request.requestId); - } - const newHeaders: { [key: string]: string } = {}; - let hasFunction = false; - for (let i = 0; i < rule.length; i++) { - if (!rule[i].isFunction) { - // @ts-ignore - newHeaders[rule[i].action.name] = rule[i].action.value; - rule.splice(i, 1); - i--; - } else { - hasFunction = true; - } - } - for (let i = 0; i < headers.length; i++) { - const name = headers[i].name.toLowerCase(); - if (newHeaders[name] === undefined) { - continue; - } - if (newHeaders[name] === '_header_editor_remove_') { - headers.splice(i, 1); - i--; - } else { - headers[i].value = newHeaders[name]; - } - delete newHeaders[name]; - } - for (const k in newHeaders) { - if (newHeaders[k] === '_header_editor_remove_') { - continue; - } - headers.push({ - name: k, - value: newHeaders[k], - }); - } - if (hasFunction) { - const detail = presetDetail || this.makeDetails(request); - rule.forEach((item) => { - try { - item._func(headers, detail); - } catch (e) { - console.error(e); - } - }); - } - } - - private autoDeleteSavedHeader(id?: string) { - if (id) { - this.deleteHeaderQueue.set(id, new Date().getTime() / 100); - } - if (this.deleteHeaderTimer !== null) { - return; - } - this.deleteHeaderTimer = getGlobal().setTimeout(() => { - // clear timeout - if (this.deleteHeaderTimer) { - clearTimeout(this.deleteHeaderTimer); - } - this.deleteHeaderTimer = null; - // check time - const curTime = new Date().getTime() / 100; - // k: id, v: time - const iter = this.deleteHeaderQueue.entries(); - for (const [k, v] of iter) { - if (curTime - v >= 90) { - this.savedRequestHeader.delete(k); - this.deleteHeaderQueue.delete(k); - } - } - if (this.deleteHeaderQueue.size > 0) { - this.autoDeleteSavedHeader(); - } - }, 10000); - } - - private modifyReceivedBody(e: WebRequest.OnHeadersReceivedDetailsType, detail: CustomFunctionDetail) { - if (!IS_SUPPORT_STREAM_FILTER) { - return; - } - - let rule = rules.get(TABLE_NAMES.receiveBody, { url: e.url, enable: true }); - if (rule === null) { - return; - } - rule = rule.filter((item) => item.isFunction); - if (rule.length === 0) { - return; - } - - const filter = browser.webRequest.filterResponseData(e.requestId); - let buffers: Uint8Array | null = null; - // @ts-ignore - filter.ondata = (event: WebRequest.StreamFilterEventData) => { - const { data } = event; - if (buffers === null) { - buffers = new Uint8Array(data); - return; - } - const buffer = new Uint8Array(buffers.byteLength + data.byteLength); - // 将响应分段数据收集拼接起来,在完成加载后整体替换。 - // 这可能会改变浏览器接收数据分段渲染的行为。 - buffer.set(buffers); - buffer.set(new Uint8Array(data), buffers.buffer.byteLength); - buffers = buffer; - // 如果长度已经超长了,那就不要尝试修改了 - if (buffers.length > MAX_BODY_SIZE) { - buffers = null; - filter.close(); - } - }; - - // @ts-ignore - filter.onstop = () => { - if (buffers === null) { - filter.close(); - return; - } - - // 缓存实例,减少开销 - for (const item of rule!) { - const encoding = item.encoding || 'UTF-8'; - try { - const _text = this.textDecode(encoding, new Uint8Array(buffers!.buffer)); - const text = item._func(_text, detail); - if (typeof text === 'string' && text !== _text) { - buffers = this.textEncode(encoding, text); - } - } catch (err) { - console.error(err); - } - } - - filter.write(buffers.buffer); - buffers = null; - filter.close(); - }; - - // @ts-ignore - filter.onerror = () => { - buffers = null; - }; - } -} - -export default function createRequestHandler() { - return new RequestHandler(); -} diff --git a/src/pages/background/upgrade.ts b/src/pages/background/upgrade.ts index a4c71c37..92d6fca8 100644 --- a/src/pages/background/upgrade.ts +++ b/src/pages/background/upgrade.ts @@ -5,10 +5,13 @@ import notify from '@/share/core/notify'; import { getDatabase } from './core/db'; // Upgrade -const downloadHistory = localStorage.getItem('dl_history'); -if (downloadHistory) { - storage.getLocal().set({ dl_history: JSON.parse(downloadHistory) }); - localStorage.removeItem('dl_history'); +// Service Worker环境中没有localStorage,跳过升级 +if (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function') { + const downloadHistory = localStorage.getItem('dl_history'); + if (downloadHistory) { + storage.getLocal().set({ dl_history: JSON.parse(downloadHistory) }); + localStorage.removeItem('dl_history'); + } } // Put a version mark @@ -62,24 +65,29 @@ storage }); }; - const groups = localStorage.getItem('groups'); - if (groups) { - const g = JSON.parse(groups); - localStorage.removeItem('groups'); - rebindRuleWithGroup(g); - } else { - storage - .getLocal() - .get('groups') - .then((r) => { - if (r.groups !== undefined) { - rebindRuleWithGroup(r.groups).then(() => storage.getLocal().remove('groups')); - } else { - const g = {}; - g[browser.i18n.getMessage('ungrouped')] = []; - rebindRuleWithGroup(g); - } - }); + // Service Worker环境中没有localStorage,跳过groups迁移 + if (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function') { + const groups = localStorage.getItem('groups'); + if (groups) { + const g = JSON.parse(groups); + localStorage.removeItem('groups'); + rebindRuleWithGroup(g); + return; + } } + + // 如果没有localStorage或没有groups数据,使用storage.local + storage + .getLocal() + .get('groups') + .then((r) => { + if (r.groups !== undefined) { + rebindRuleWithGroup(r.groups).then(() => storage.getLocal().remove('groups')); + } else { + const g = {}; + g[browser.i18n.getMessage('ungrouped')] = []; + rebindRuleWithGroup(g); + } + }); } }); diff --git a/src/pages/background/v3-rule-converter.ts b/src/pages/background/v3-rule-converter.ts new file mode 100644 index 00000000..510cd898 --- /dev/null +++ b/src/pages/background/v3-rule-converter.ts @@ -0,0 +1,558 @@ +import browser from 'webextension-polyfill'; +import type { Rule } from '@/share/core/types'; +import logger from '@/share/core/logger'; + +// declarativeNetRequest 规则接口 +interface V3Rule { + id: number; + priority: number; + action: { + type: 'block' | 'redirect' | 'modifyHeaders' | 'upgradeScheme' | 'allow' | 'allowAllRequests'; + redirect?: { url: string; regexSubstitution?: string }; + requestHeaders?: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }>; + responseHeaders?: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }>; + }; + condition: { + urlFilter?: string; + regexFilter?: string; + domains?: string[]; + excludedDomains?: string[]; + resourceTypes?: browser.DeclarativeNetRequest.ResourceType[]; + excludedResourceTypes?: browser.DeclarativeNetRequest.ResourceType[]; + requestMethods?: string[]; + excludedRequestMethods?: string[]; + }; +} + +// 转换结果接口 +interface ConversionResult { + convertedRules: V3Rule[]; + unconvertedRules: Rule[]; + warnings: string[]; +} + +export class V3RuleConverter { + private static ruleIdCounter = 1000; // 从1000开始,避免与静态规则冲突 + + /** + * 将传统规则转换为 V3 规则 + */ + static convertRulesToV3(rules: Rule[]): ConversionResult { + const convertedRules: V3Rule[] = []; + const unconvertedRules: Rule[] = []; + const warnings: string[] = []; + + // 重置规则ID计数器 + this.ruleIdCounter = 1000; + + for (const rule of rules) { + try { + if (this.isConvertible(rule)) { + const v3Rule = this.convertSingleRule(rule); + if (v3Rule) { + convertedRules.push(v3Rule); + } + } else { + unconvertedRules.push(rule); + let reason = '未知原因'; + if (rule.isFunction) { + reason = '包含自定义函数'; + } else if (rule.ruleType === 'modifyReceiveBody') { + reason = '不支持响应体修改'; + } else if (rule.matchType === 'regexp' && this.isComplexRegex(rule.pattern)) { + reason = '包含复杂的正则表达式'; + } + warnings.push(`规则 "${rule.name}" 无法转换为 V3 格式: ${reason}`); + } + } catch (error) { + logger.error(`转换规则 "${rule.name}" 时发生错误:`, error); + unconvertedRules.push(rule); + warnings.push(`规则 "${rule.name}" 转换失败: ${error.message}`); + } + } + + return { convertedRules, unconvertedRules, warnings }; + } + + /** + * 检查规则是否可以转换为 V3 格式 + */ + private static isConvertible(rule: Rule): boolean { + // 不支持自定义函数 + if (rule.isFunction) { + return false; + } + + // 不支持响应体修改 + if (rule.ruleType === 'modifyReceiveBody') { + return false; + } + + // 检查是否为支持的规则类型 + const supportedTypes = ['cancel', 'redirect', 'modifySendHeader', 'modifyReceiveHeader']; + if (!supportedTypes.includes(rule.ruleType)) { + return false; + } + + // 检查正则表达式复杂度 + if (rule.matchType === 'regexp' && this.isComplexRegex(rule.pattern)) { + return false; + } + + return true; + } + + /** + * 检查是否为复杂正则表达式 + */ + private static isComplexRegex(pattern: string): boolean { + // 简单检查,如果包含复杂的正则特性,认为是复杂的 + const complexFeatures = [ + '\\d', '\\w', '\\s', '\\D', '\\W', '\\S', // 字符类 + '\\b', '\\B', // 边界 + '(?:', '(?=', '(?!', '(?<=', '(? pattern.includes(feature)); + } + + /** + * 转换单个规则 + */ + private static convertSingleRule(rule: Rule): V3Rule | null { + const startTime = Date.now(); + + try { + const v3Rule: V3Rule = { + id: this.ruleIdCounter++, + priority: Math.max(1, Math.min(100, rule.priority || 1)), // 确保优先级在有效范围内 + action: this.convertAction(rule), + condition: this.convertCondition(rule), + }; + + // 验证生成的规则 + if (!this.validateV3Rule(v3Rule)) { + logger.warn(`规则 "${rule.name}" 转换后验证失败,跳过应用`); + logger.logRuleConversion(rule, v3Rule, false); + return null; + } + + const duration = Date.now() - startTime; + logger.logPerformance(`规则转换 ${rule.name}`, duration); + logger.logRuleConversion(rule, v3Rule, true); + + return v3Rule; + } catch (error) { + const duration = Date.now() - startTime; + logger.error(`转换规则 "${rule.name}" 时发生错误:`, error); + logger.logPerformance(`规则转换失败 ${rule.name}`, duration); + logger.logRuleConversion(rule, null, false); + return null; + } + } + + /** + * 验证 V3 规则是否有效 + */ + private static validateV3Rule(rule: V3Rule): boolean { + // 检查 ID 是否有效 + if (!rule.id || rule.id < 1) { + return false; + } + + // 检查优先级是否有效 + if (rule.priority < 1 || rule.priority > 100) { + return false; + } + + // 检查动作是否有效 + if (!rule.action || !rule.action.type) { + return false; + } + + // 检查条件是否有效 + if (!rule.condition) { + return false; + } + + // 至少需要一个匹配条件 + const hasMatchCondition = + rule.condition.urlFilter || + rule.condition.regexFilter || + rule.condition.domains?.length || + rule.condition.excludedDomains?.length; + + if (!hasMatchCondition) { + return false; + } + + return true; + } + + /** + * 转换规则动作 + */ + private static convertAction(rule: Rule): V3Rule['action'] { + switch (rule.ruleType) { + case 'cancel': + return { type: 'block' }; + + case 'redirect': + if (rule.to) { + // 简单的 URL 重定向 + if (rule.matchType === 'regexp' && !this.isComplexRegex(rule.pattern)) { + return { + type: 'redirect', + redirect: { + url: rule.to, + regexSubstitution: rule.to, + }, + }; + } else { + return { + type: 'redirect', + redirect: { url: rule.to }, + }; + } + } + return { type: 'block' }; + + case 'modifySendHeader': + return { + type: 'modifyHeaders', + requestHeaders: this.convertHeaders(rule), + }; + + case 'modifyReceiveHeader': + return { + type: 'modifyHeaders', + responseHeaders: this.convertHeaders(rule), + }; + + default: + throw new Error(`不支持的规则类型: ${rule.ruleType}`); + } + } + + /** + * 转换请求头/响应头 + */ + private static convertHeaders(rule: Rule): Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }> { + if (!rule.action || typeof rule.action !== 'object') { + return []; + } + + const headers: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }> = []; + + if (rule.action.name && rule.action.value !== undefined) { + const headerName = rule.action.name.trim(); + if (!headerName) { + return []; + } + + if (rule.action.value === '_header_editor_remove_') { + headers.push({ + header: headerName, + operation: 'remove', + }); + } else { + headers.push({ + header: headerName, + operation: 'set', + value: String(rule.action.value), + }); + } + } + + return headers; + } + + /** + * 转换匹配条件 + */ + private static convertCondition(rule: Rule): V3Rule['condition'] { + const condition: V3Rule['condition'] = {}; + + // 设置资源类型 + if (rule.ruleType === 'modifySendHeader') { + // 请求头修改适用于所有资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } else if (rule.ruleType === 'modifyReceiveHeader') { + // 响应头修改适用于所有资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } else { + // 其他规则类型使用默认资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } + + // 转换 URL 匹配 + switch (rule.matchType) { + case 'all': + // 匹配所有URL + condition.urlFilter = '*'; + break; + + case 'regexp': + if (!this.isComplexRegex(rule.pattern)) { + condition.regexFilter = rule.pattern; + } else { + // 尝试将复杂正则转换为简单过滤器 + const simpleFilter = this.convertComplexRegexToFilter(rule.pattern); + if (simpleFilter) { + condition.urlFilter = simpleFilter; + } else { + condition.urlFilter = '*'; + } + } + break; + + case 'prefix': + // URL前缀匹配 + condition.urlFilter = `${rule.pattern}*`; + break; + + case 'domain': { + // 域名匹配 + const domain = this.normalizeDomain(rule.pattern); + if (domain) { + condition.domains = [domain]; + } else { + condition.urlFilter = '*'; + } + break; + } + + case 'url': + // 完整URL匹配 + condition.urlFilter = rule.pattern; + break; + + default: + // 默认匹配所有 + condition.urlFilter = '*'; + } + + // 处理排除模式 + if (rule.exclude && rule.exclude.trim()) { + try { + const excludeDomain = this.normalizeDomain(rule.exclude); + if (excludeDomain) { + condition.excludedDomains = [excludeDomain]; + } + } catch (error) { + logger.warn(`无法处理排除模式 "${rule.exclude}":`, error); + } + } + + return condition; + } + + /** + * 标准化域名 + */ + private static normalizeDomain(domain: string): string { + if (!domain) return ''; + + // 移除协议前缀 + domain = domain.replace(/^https?:\/\//, ''); + + // 移除路径 + domain = domain.split('/')[0]; + + // 移除端口 + domain = domain.split(':')[0]; + + // 移除 www. 前缀(可选) + if (domain.startsWith('www.')) { + domain = domain.substring(4); + } + + return domain.toLowerCase(); + } + + /** + * 检查是否为域名模式 + */ + private static isDomainPattern(pattern: string): boolean { + // 简单检查是否像域名 + return /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(pattern); + } + + /** + * 将复杂正则表达式转换为简单过滤器 + */ + private static convertComplexRegexToFilter(regex: string): string { + try { + // 尝试提取简单的URL模式 + if (regex.includes('://')) { + // 如果包含协议,提取域名部分 + const match = regex.match(/https?:\/\/([^/\s?]+)/); + if (match) { + return `*://${match[1]}/*`; + } + } + + // 提取域名模式 + const domainMatch = regex.match(/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/); + if (domainMatch) { + return `*://${domainMatch[1]}/*`; + } + + // 如果无法转换,返回通配符 + return '*'; + } catch (error) { + logger.warn('转换复杂正则表达式失败:', error); + return '*'; + } + } + + /** + * 从模式中提取域名 + */ + private static extractDomainFromPattern(pattern: string): string | null { + try { + const match = pattern.match(/(?:https?:\/\/)?([^/\s?]+)/); + return match ? match[1] : null; + } catch (error) { + return null; + } + } + + /** + * 应用 V3 规则到浏览器 + */ + static async applyV3Rules(rules: V3Rule[]): Promise { + const startTime = Date.now(); + + try { + logger.info(`准备应用 ${rules.length} 个 V3 规则`); + + // 先移除现有的动态规则 + const existingRules = await browser.declarativeNetRequest.getDynamicRules(); + const existingRuleIds = existingRules.map((r) => r.id); + + if (existingRuleIds.length > 0) { + logger.info(`移除现有的 ${existingRuleIds.length} 个动态规则`); + await browser.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: existingRuleIds, + }); + } + + // 应用新规则 + if (rules.length > 0) { + logger.info(`应用 ${rules.length} 个新规则`); + + // 分批应用规则,避免一次性应用太多规则 + const batchSize = 100; + for (let i = 0; i < rules.length; i += batchSize) { + const batch = rules.slice(i, i + batchSize); + logger.debug(`应用规则批次 ${Math.floor(i / batchSize) + 1}/${Math.ceil(rules.length / batchSize)}, 包含 ${batch.length} 个规则`); + + await browser.declarativeNetRequest.updateDynamicRules({ + addRules: batch, + }); + } + } + + const duration = Date.now() - startTime; + logger.logPerformance('V3 规则应用', duration); + logger.logV3RuleApplication(rules, true); + + // 验证规则是否成功应用 + const newRules = await browser.declarativeNetRequest.getDynamicRules(); + if (newRules.length !== rules.length) { + logger.warn(`规则应用不完整: 期望 ${rules.length} 个,实际 ${newRules.length} 个`); + } + } catch (error) { + const duration = Date.now() - startTime; + logger.logPerformance('V3 规则应用失败', duration); + logger.logV3RuleApplication(rules, false, error); + logger.error('应用 V3 规则时发生错误:', error); + throw error; + } + } + + /** + * 获取 V3 规则限制信息 + */ + static getRuleLimits(): { + MAX_NUMBER_OF_DYNAMIC_RULES: number; + MAX_NUMBER_OF_REGEX_RULES: number; + MAX_NUMBER_OF_STATIC_RULES: number; + } { + return { + MAX_NUMBER_OF_DYNAMIC_RULES: 30000, + MAX_NUMBER_OF_REGEX_RULES: 1000, + MAX_NUMBER_OF_STATIC_RULES: 30000, + }; + } + + /** + * 检查规则是否超过限制 + */ + static checkRuleLimits(rules: V3Rule[]): { + isValid: boolean; + errors: string[]; + } { + const limits = this.getRuleLimits(); + const errors: string[] = []; + + if (rules.length > limits.MAX_NUMBER_OF_DYNAMIC_RULES) { + errors.push(`规则数量 (${rules.length}) 超过限制 (${limits.MAX_NUMBER_OF_DYNAMIC_RULES})`); + } + + const regexRules = rules.filter((r) => r.condition.regexFilter); + if (regexRules.length > limits.MAX_NUMBER_OF_REGEX_RULES) { + errors.push(`正则表达式规则数量 (${regexRules.length}) 超过限制 (${limits.MAX_NUMBER_OF_REGEX_RULES})`); + } + + return { + isValid: errors.length === 0, + errors, + }; + } + + /** + * 获取当前应用的动态规则 + */ + static async getCurrentRules(): Promise { + try { + const rules = await browser.declarativeNetRequest.getDynamicRules(); + return rules; + } catch (error) { + logger.error('获取当前规则失败:', error); + return []; + } + } +} diff --git a/src/pages/options/components/v3-migration-guide.tsx b/src/pages/options/components/v3-migration-guide.tsx new file mode 100644 index 00000000..cd4db3a7 --- /dev/null +++ b/src/pages/options/components/v3-migration-guide.tsx @@ -0,0 +1,202 @@ +import React, { useState } from 'react'; +import { Button, Card, Typography, Space, Alert, Divider } from 'antd'; +import { CheckCircleOutlined, BugOutlined, PlayCircleOutlined } from '@ant-design/icons'; + +const { Title, Paragraph, Text } = Typography; + +interface V3MigrationGuideProps { + onClose?: () => void; +} + +export default function V3MigrationGuide({ onClose }: V3MigrationGuideProps) { + const [debugEnabled, setDebugEnabled] = useState(false); + const [testing, setTesting] = useState(false); + const [testResults, setTestResults] = useState(null); + + const handleEnableDebug = async () => { + try { + const response = await chrome.runtime.sendMessage({ + method: 'enableDebugLogging', + }); + setDebugEnabled(response.success); + } catch (error) { + console.error('启用调试日志失败:', error); + } + }; + + const handleDisableDebug = async () => { + try { + const response = await chrome.runtime.sendMessage({ + method: 'disableDebugLogging', + }); + setDebugEnabled(!response.success); + } catch (error) { + console.error('禁用调试日志失败:', error); + } + }; + + const handleTestRules = async () => { + setTesting(true); + try { + const response = await chrome.runtime.sendMessage({ + method: 'testRuleApplication', + }); + + const stats = await chrome.runtime.sendMessage({ + method: 'getRuleStats', + }); + + setTestResults({ + success: response.success, + error: response.error, + stats: stats.stats, + }); + } catch (error) { + setTestResults({ + success: false, + error: error.message, + stats: null, + }); + } finally { + setTesting(false); + } + }; + + return ( + + + Manifest V3 迁移指南 + + } + extra={onClose && } + > + + + +
+ 主要变化 +
    +
  • 使用 declarativeNetRequest 替代 webRequest API
  • +
  • 规则在后台服务工作者中处理
  • +
  • 改进的性能和资源使用
  • +
  • 更严格的安全限制
  • +
+
+ +
+ 功能限制 + +
  • 自定义 JavaScript 函数规则
  • +
  • 响应体修改功能
  • +
  • 复杂的正则表达式匹配
  • +
  • 某些高级网络拦截功能
  • + + } + type="warning" + showIcon + /> +
    + + + +
    + 调试工具 + + + + + + + +
    + + {testResults && ( +
    + 测试结果 + + {testResults.error && ( + 错误: {testResults.error} + )} + {testResults.stats && ( +
    + 转换统计: +
      +
    • 总规则数: {testResults.stats.total || 0}
    • +
    • 已转换: {testResults.stats.converted || 0}
    • +
    • 未转换: {testResults.stats.unconverted || 0}
    • +
    • 警告数: {testResults.stats.warnings || 0}
    • +
    +
    + )} +
    + + 更多详细信息请查看浏览器控制台 (F12) + +
    +
    + } + type={testResults.success ? 'success' : 'error'} + showIcon + /> + + )} + +
    + 获取帮助 + + 如果您遇到问题或需要帮助,请访问: + + + + + +
    +
    +
    + ); +} diff --git a/src/share/core/logger.ts b/src/share/core/logger.ts index ad327e4d..c4e38191 100644 --- a/src/share/core/logger.ts +++ b/src/share/core/logger.ts @@ -1,28 +1,193 @@ -import dayjs from 'dayjs'; -import { prefs } from './prefs'; +// @ts-ignore +import { IS_BACKGROUND } from './utils'; -export interface LogItem { - time: Date; - message: string; - data?: any[]; +interface LogLevel { + DEBUG: 0; + INFO: 1; + WARN: 2; + ERROR: 3; } +const LOG_LEVELS: LogLevel = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +}; + class Logger { - debug(message: string, ...data: any[]) { - if (!prefs.get('is-debug')) { + private currentLevel: number = LOG_LEVELS.INFO; + private prefix = ''; + + constructor() { + this.prefix = IS_BACKGROUND ? '[Header Editor BG]' : '[Header Editor]'; + + // 在开发环境下启用调试日志 + if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest) { + try { + const manifest = chrome.runtime.getManifest(); + if (manifest.name?.includes('dev') || manifest.version?.includes('dev')) { + this.currentLevel = LOG_LEVELS.DEBUG; + } + } catch (e) { + // 忽略错误 + } + } + } + + private formatMessage(level: string, message: string, ..._args: any[]): string { + const timestamp = new Date().toISOString(); + const context = IS_BACKGROUND ? 'BG' : 'CS'; + return `${timestamp} [${context}] ${level}: ${message}`; + } + + private log(level: number, levelName: string, message: string, ...args: any[]) { + if (level < this.currentLevel) { return; } - console.log( - ['%cHeader Editor%c [', dayjs().format('YYYY-MM-DD HH:mm:ss.SSS'), ']%c ', message].join(''), - 'color:#5584ff;', - 'color:#ff9300;', - '', - ); - if (data && data.length > 0) { - console.log(data); + + const formattedMessage = this.formatMessage(levelName, message); + + switch (level) { + case LOG_LEVELS.DEBUG: + console.debug(this.prefix, formattedMessage, ...args); + break; + case LOG_LEVELS.INFO: + console.info(this.prefix, formattedMessage, ...args); + break; + case LOG_LEVELS.WARN: + console.warn(this.prefix, formattedMessage, ...args); + break; + case LOG_LEVELS.ERROR: + console.error(this.prefix, formattedMessage, ...args); + break; + default: + console.log(this.prefix, formattedMessage, ...args); } } + + debug(message: string, ...args: any[]) { + this.log(LOG_LEVELS.DEBUG, 'DEBUG', message, ...args); + } + + info(message: string, ...args: any[]) { + this.log(LOG_LEVELS.INFO, 'INFO', message, ...args); + } + + warn(message: string, ...args: any[]) { + this.log(LOG_LEVELS.WARN, 'WARN', message, ...args); + } + + error(message: string, ...args: any[]) { + this.log(LOG_LEVELS.ERROR, 'ERROR', message, ...args); + } + + // 设置日志级别 + setLevel(level: keyof LogLevel) { + this.currentLevel = LOG_LEVELS[level]; + this.info(`日志级别设置为: ${level}`); + } + + // 获取当前日志级别 + getLevel(): string { + const levels = Object.keys(LOG_LEVELS); + return levels.find((key) => LOG_LEVELS[key as keyof LogLevel] === this.currentLevel) || 'INFO'; + } + + // 启用调试模式 + enableDebug() { + this.setLevel('DEBUG'); + } + + // 禁用调试模式 + disableDebug() { + this.setLevel('INFO'); + } + + // 记录规则转换详情 + logRuleConversion(rule: any, v3Rule: any, success: boolean) { + if (success) { + this.debug('规则转换成功', { + originalRule: { + id: rule.id, + name: rule.name, + type: rule.ruleType, + matchType: rule.matchType, + pattern: rule.pattern, + enable: rule.enable, + }, + v3Rule: { + id: v3Rule.id, + priority: v3Rule.priority, + actionType: v3Rule.action?.type, + conditionKeys: Object.keys(v3Rule.condition || {}), + }, + }); + } else { + this.warn('规则转换失败', { + rule: { + id: rule.id, + name: rule.name, + type: rule.ruleType, + matchType: rule.matchType, + pattern: rule.pattern, + }, + }); + } + } + + // 记录 V3 规则应用详情 + logV3RuleApplication(rules: any[], success: boolean, error?: any) { + if (success) { + this.info('V3 规则应用成功', { + ruleCount: rules.length, + ruleIds: rules.map((r) => r.id), + ruleTypes: rules.reduce((types: Record, rule) => { + const type = rule.action?.type || 'unknown'; + types[type] = (types[type] ?? 0) + 1; + return types; + }, {}), + }); + } else { + this.error('V3 规则应用失败', { + ruleCount: rules.length, + error: error?.message || '未知错误', + stack: error?.stack, + }); + } + } + + // 记录规则匹配详情 + logRuleMatch(url: string, rule: any, matched: boolean) { + this.debug('规则匹配检查', { + url, + rule: { + id: rule.id, + name: rule.name, + type: rule.ruleType, + matchType: rule.matchType, + pattern: rule.pattern, + }, + matched, + }); + } + + // 记录性能信息 + logPerformance(operation: string, duration: number) { + this.info(`性能统计: ${operation} 耗时 ${duration}ms`); + } + + // 记录扩展状态 + logExtensionState(state: any) { + this.info('扩展状态', { + enabled: !state.disabled, + rulesCount: state.rulesCount || 0, + v3RulesCount: state.v3RulesCount || 0, + lastUpdate: state.lastUpdate || 'never', + }); + } } const logger = new Logger(); + export default logger; diff --git a/src/share/core/notify.ts b/src/share/core/notify.ts index 00e4004c..e00799da 100644 --- a/src/share/core/notify.ts +++ b/src/share/core/notify.ts @@ -11,10 +11,10 @@ class Notify { resolve: (v: any) => void; reject: (e: any) => void; }> = []; - private messageTimer: number | null = null; + private messageTimer: ReturnType | null = null; constructor() { - const handleMessage = (request: any, sender?: any) => { + const handleMessage = (request: any, _sender?: any) => { if (request.method === 'notifyBackground') { request.method = request.reason; delete request.reason; @@ -26,7 +26,7 @@ class Notify { this.event.emit(request.event, request); }; - browser.runtime.onMessage.addListener((request, sender) => { + browser.runtime.onMessage.addListener((request, _sender) => { // 批量消息 if (request.method === 'batchExecute') { request.batch.forEach((item) => handleMessage(item)); diff --git a/src/share/core/prefs.ts b/src/share/core/prefs.ts index 16208beb..ee7d0cc9 100644 --- a/src/share/core/prefs.ts +++ b/src/share/core/prefs.ts @@ -54,6 +54,11 @@ class Prefs { }); function tryMigrating(key: string) { + // Service Worker环境中没有localStorage,跳过迁移 + if (typeof localStorage === 'undefined' || typeof localStorage.getItem !== 'function') { + return undefined; + } + if (!(key in localStorage)) { return undefined; } @@ -72,8 +77,9 @@ class Prefs { console.error("Cannot migrate from localStorage %s = '%s': %o", key, value, e); return undefined; } + default: + return value; } - return value; } } get(key: string, defaultValue?: any) { @@ -129,5 +135,22 @@ class Prefs { interface BackgroundWindow extends Window { prefs?: Prefs; } -const backgroundWindow = browser.extension.getBackgroundPage() as BackgroundWindow; -export const prefs = backgroundWindow && backgroundWindow.prefs ? backgroundWindow.prefs : new Prefs(); + +// Service Worker环境中没有background page,直接创建新的Prefs实例 +function createPrefs() { + // 在Service Worker环境中,直接创建新实例 + if (typeof window === 'undefined') { + return new Prefs(); + } + + // 在其他环境中,尝试从background page获取 + try { + const backgroundWindow = browser.extension.getBackgroundPage() as BackgroundWindow; + return backgroundWindow && backgroundWindow.prefs ? backgroundWindow.prefs : new Prefs(); + } catch (e) { + // 如果获取background page失败,创建新实例 + return new Prefs(); + } +} + +export const prefs = createPrefs(); diff --git a/src/share/core/storage.ts b/src/share/core/storage.ts index 29f56352..8d9d1195 100644 --- a/src/share/core/storage.ts +++ b/src/share/core/storage.ts @@ -2,7 +2,7 @@ import browser from 'webextension-polyfill'; export function getSync() { // For development mode - if (typeof localStorage !== 'undefined' && localStorage.getItem('storage') === 'local') { + if (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function' && localStorage.getItem('storage') === 'local') { return browser.storage.local; } try { diff --git a/src/share/core/utils.ts b/src/share/core/utils.ts index 3fbe9991..f0353b03 100644 --- a/src/share/core/utils.ts +++ b/src/share/core/utils.ts @@ -158,10 +158,12 @@ export function getGlobal() { } export function isBackground() { + // Service Worker环境中window不存在,但我们是在后台运行 if (typeof window === 'undefined') { return true; } - return typeof window.IS_BACKGROUND !== 'undefined'; + // 检查window或globalThis中的IS_BACKGROUND标志 + return typeof window.IS_BACKGROUND !== 'undefined' || typeof globalThis.IS_BACKGROUND !== 'undefined'; } export function getVirtualKey(rule: Rule) { diff --git a/tsconfig.json b/tsconfig.json index 37a4ccfc..214c6815 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "jsx": "react", "moduleResolution": "node", "allowSyntheticDefaultImports": true, - "lib": ["es2020", "dom"], + "lib": ["es2020", "dom", "webworker"], "sourceMap": true, "allowJs": true, "rootDir": "./", @@ -19,10 +19,11 @@ "noImplicitAny": false, "importHelpers": true, "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "skipLibCheck": true, - "types": ["node"], + "esModuleInterop": true, + "resolveJsonModule": true, + "types": ["node", "chrome"], "paths": { "@/*": ["./src/*"], "ice": [".ice/index.ts"], From 1c9272d54c43a89408d1270b0f5c6a0b280d2b80 Mon Sep 17 00:00:00 2001 From: lework Date: Mon, 14 Jul 2025 13:40:56 +0800 Subject: [PATCH 9/9] feat: update manifest v3 --- .eslintignore | 1 + .eslintrc.js | 10 +- DEBUG_GUIDE.md | 166 ++++++ MIGRATION_REPORT.md | 151 +++++ package.json | 2 +- src/enterprise/policy-handler.ts | 371 ++++++++++++ src/manifest.json | 69 +-- src/pages/background/api-handler.ts | 207 +++++-- src/pages/background/index.ts | 503 +++++++++++++++- src/pages/background/request-handler.ts | 492 --------------- src/pages/background/upgrade.ts | 52 +- src/pages/background/v3-rule-converter.ts | 558 ++++++++++++++++++ .../options/components/v3-migration-guide.tsx | 202 +++++++ src/share/core/logger.ts | 197 ++++++- src/share/core/notify.ts | 6 +- src/share/core/prefs.ts | 29 +- src/share/core/storage.ts | 2 +- src/share/core/utils.ts | 4 +- tsconfig.json | 9 +- 19 files changed, 2392 insertions(+), 639 deletions(-) create mode 100644 DEBUG_GUIDE.md create mode 100644 MIGRATION_REPORT.md create mode 100644 src/enterprise/policy-handler.ts delete mode 100644 src/pages/background/request-handler.ts create mode 100644 src/pages/background/v3-rule-converter.ts create mode 100644 src/pages/options/components/v3-migration-guide.tsx diff --git a/.eslintignore b/.eslintignore index 842c4df1..5d6ea794 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ demo/ .ice/ scripts/ locale/ +dist/ # node 覆盖率文件 coverage/ diff --git a/.eslintrc.js b/.eslintrc.js index 9e97dc7f..73da16dc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,12 +31,12 @@ module.exports = getESLintConfig('react-ts', { { 'pattern': '@/**', 'group': 'parent', - 'position': 'before' - } + 'position': 'before', + }, ], 'pathGroupsExcludedImportTypes': ['builtin'], - 'newlines-between': 'never' - } - ] + 'newlines-between': 'never', + }, + ], }, }); diff --git a/DEBUG_GUIDE.md b/DEBUG_GUIDE.md new file mode 100644 index 00000000..881f65c0 --- /dev/null +++ b/DEBUG_GUIDE.md @@ -0,0 +1,166 @@ +# Header Editor V3 调试指南 + +## 问题:请求网址时没有添加 x-tag header + +### 🔍 调试步骤 + +#### 1. 检查规则是否已创建 +首先确认您是否已经正确创建了添加 x-tag header 的规则: + +**在 Header Editor 选项页面:** +1. 打开 Chrome 扩展管理页面 (`chrome://extensions/`) +2. 找到 Header Editor,点击"详细信息" +3. 点击"扩展程序选项" +4. 检查是否有类似以下的规则: + - 规则类型:修改发送头 + - 头名称:x-tag (或 X-Tag) + - 头内容:您想要的值 + - 匹配类型:全部(或特定模式) + - 启用状态:✅ 已启用 + +#### 2. 使用调试工具检查规则状态 +打开浏览器开发者工具 (F12),在 Console 中执行: + +```javascript +// 检查 x-tag 规则的完整状态 +testXTagHeaderRule() +``` + +这个命令将: +- 检查当前的规则 +- 如果没有 x-tag 规则,自动创建一个测试规则 +- 验证规则转换是否正确 +- 检查 V3 规则是否正确应用 + +#### 3. 检查扩展是否被禁用 +```javascript +// 检查扩展是否被禁用 +chrome.runtime.sendMessage({method: 'getRuleStats'}) +``` + +#### 4. 启用调试日志 +```javascript +// 启用详细的调试日志 +chrome.runtime.sendMessage({method: 'enableDebugLogging'}) +``` + +启用后,刷新页面并查看控制台输出,查找相关的规则应用信息。 + +#### 5. 验证规则是否生效 +访问 https://httpbin.org/headers 来检查请求头: + +1. 打开 https://httpbin.org/headers +2. 查看返回的 JSON 中的 `headers` 字段 +3. 检查是否包含您的 x-tag header + +**或者在开发者工具中检查:** +1. 打开开发者工具 (F12) +2. 切换到 Network 标签 +3. 刷新页面 +4. 点击任意请求 +5. 在 Headers 标签中查看 Request Headers +6. 查找您的 x-tag header + +### 🚨 常见问题和解决方案 + +#### 问题 1:规则存在但没有生效 +**可能原因:** +- 规则转换失败 +- V3 规则没有正确应用 +- 匹配条件不正确 + +**解决方案:** +```javascript +// 强制刷新规则 +chrome.runtime.sendMessage({method: 'testRuleApplication'}) +``` + +#### 问题 2:规则转换失败 +**可能原因:** +- 规则格式不正确 +- 包含不支持的功能 + +**解决方案:** +确保规则满足以下条件: +- 规则类型为 `modifySendHeader` +- 不使用自定义函数 (`isFunction: false`) +- Header 名称和值都不为空 + +#### 问题 3:V3 规则限制 +**可能原因:** +- Chrome 的 declarativeNetRequest 有一些限制 + +**解决方案:** +- 确保 header 名称符合 HTTP 规范 +- 避免使用特殊字符 +- 检查是否超过规则数量限制 + +### 🔧 手动创建测试规则 + +如果自动创建不工作,可以手动创建: + +1. 打开 Header Editor 选项页面 +2. 点击"添加规则" +3. 填写以下信息: + - 名称:`测试 X-Tag Header` + - 规则类型:`修改发送头` + - 匹配类型:`全部` + - 执行类型:`普通` + - 头名称:`X-Tag` + - 头内容:`test-value` +4. 点击保存 + +### 📊 检查规则统计 + +```javascript +// 获取当前规则统计 +chrome.runtime.sendMessage({method: 'getRuleStats'}).then(response => { + console.log('规则统计:', response.stats); +}); + +// 获取当前应用的 V3 规则 +chrome.declarativeNetRequest.getDynamicRules().then(rules => { + console.log('当前 V3 规则:', rules); + const headerRules = rules.filter(rule => + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders + ); + console.log('修改请求头的规则:', headerRules); +}); +``` + +### 🔍 深度调试 + +如果上述步骤都没有解决问题,请: + +1. **收集调试信息:** + ```javascript + // 收集完整的调试信息 + const debugInfo = { + rules: await chrome.runtime.sendMessage({method: 'getRuleStats'}), + v3Rules: await chrome.declarativeNetRequest.getDynamicRules(), + permissions: await chrome.permissions.getAll() + }; + console.log('调试信息:', debugInfo); + ``` + +2. **检查权限:** + 确保扩展有以下权限: + - `declarativeNetRequest` + - `declarativeNetRequestWithHostAccess` + - 相关的主机权限 + +3. **检查浏览器版本:** + 确保使用 Chrome 88+ 或其他支持 Manifest V3 的浏览器 + +### 📝 报告问题 + +如果问题仍然存在,请提供以下信息: + +1. 浏览器版本 +2. Header Editor 版本 +3. 创建的规则详情 +4. 调试输出结果 +5. 期望的行为 vs 实际行为 + +这将帮助我们更好地诊断和解决问题。 \ No newline at end of file diff --git a/MIGRATION_REPORT.md b/MIGRATION_REPORT.md new file mode 100644 index 00000000..d6d2fe6c --- /dev/null +++ b/MIGRATION_REPORT.md @@ -0,0 +1,151 @@ +# Header Editor Manifest V3 迁移报告 + +## 项目概述 +Header Editor 是一个浏览器扩展,允许用户修改 HTTP 请求和响应头。本项目已完成从 Manifest V2 到 Manifest V3 的迁移。 + +## 最新修复 (2024-12-19) + +### 问题诊断 +经过代码审查发现以下关键问题: +1. **规则更新事件监听缺失** - 规则变化时没有自动重新应用 V3 规则 +2. **规则转换逻辑不完善** - V3RuleConverter 存在类型错误和转换不准确 +3. **初始化时序问题** - 数据库未完全准备好就开始应用规则 +4. **调试信息不足** - 缺少详细的调试日志来排查问题 + +### 修复内容 + +#### 1. 事件监听系统 (src/pages/background/index.ts) +- ✅ 添加规则更新事件监听 (`EVENTs.RULE_UPDATE`, `EVENTs.RULE_DELETE`) +- ✅ 添加偏好设置变化监听 (`disable-all` 设置) +- ✅ 改进初始化流程,等待数据库和规则缓存完全准备 +- ✅ 添加自动规则刷新机制 +- ✅ 添加测试接口和调试命令 + +#### 2. 规则转换器优化 (src/pages/background/v3-rule-converter.ts) +- ✅ 修复类型定义问题,使用正确的 declarativeNetRequest 类型 +- ✅ 改进规则验证逻辑,确保生成的 V3 规则有效 +- ✅ 优化资源类型设置,移除不支持的类型 +- ✅ 添加批量规则应用,避免一次性应用过多规则 +- ✅ 改进错误处理和日志记录 +- ✅ 添加规则转换详细日志记录 + +#### 3. API 处理器增强 (src/pages/background/api-handler.ts) +- ✅ 在所有规则操作后自动触发 V3 规则刷新 +- ✅ 添加详细的操作日志记录 +- ✅ 改进错误处理和状态反馈 +- ✅ 添加规则统计信息接口 + +#### 4. 日志系统升级 (src/share/core/logger.ts) +- ✅ 重构日志系统,支持不同日志级别 +- ✅ 添加专用的规则转换日志方法 +- ✅ 添加性能统计日志 +- ✅ 添加扩展状态日志 +- ✅ 改进日志格式和上下文信息 + +### 核心改进 + +#### 自动规则同步 +现在规则的任何变化都会自动触发 V3 规则的重新应用: +- 创建新规则 → 自动应用到 declarativeNetRequest +- 修改现有规则 → 自动更新 declarativeNetRequest +- 删除规则 → 自动从 declarativeNetRequest 移除 +- 偏好设置变化 → 自动调整规则应用状态 + +#### 规则转换改进 +- 更准确的规则类型识别和转换 +- 更严格的规则验证 +- 更好的错误处理和回退机制 +- 更详细的转换统计和日志 + +#### 调试和测试功能 +- 可通过 console 调用 `testRuleApplication()` 进行测试 +- 支持动态启用/禁用调试日志 +- 提供详细的规则转换和应用统计 +- 支持获取当前规则状态和转换结果 + +## 主要变化说明 + +### 1. 网络请求处理 +- **V2**: 使用 `chrome.webRequest` API 进行实时拦截和修改 +- **V3**: 使用 `chrome.declarativeNetRequest` API 进行声明式规则处理 + +### 2. 背景脚本 +- **V2**: 持久化背景页面 (`background.html`) +- **V3**: 服务工作者 (`background.js`) + +### 3. 权限系统 +- **V2**: `webRequestBlocking` 权限 +- **V3**: `declarativeNetRequest` 和 `declarativeNetRequestWithHostAccess` 权限 + +### 4. 规则应用方式 +- **V2**: 运行时动态处理每个请求 +- **V3**: 预配置规则集,由浏览器引擎处理 + +## 功能限制 + +### 不支持的功能 +1. **自定义 JavaScript 函数规则** - V3 不允许执行任意代码 +2. **响应体修改** - declarativeNetRequest 不支持响应体修改 +3. **复杂正则表达式** - V3 API 对正则表达式有限制 +4. **动态 IP 获取** - 无法在 V3 中获取客户端 IP + +### 功能替代方案 +- 简单的头部修改 → 使用 `modifyHeaders` 动作 +- URL 重定向 → 使用 `redirect` 动作 +- 请求阻止 → 使用 `block` 动作 +- 域名匹配 → 使用 `domains` 条件 + +## 测试验证 + +### 测试方法 +1. 打开浏览器开发者工具 +2. 切换到 Console 标签 +3. 执行 `testRuleApplication()` 进行功能测试 +4. 检查规则转换统计和应用结果 + +### 测试内容 +- 规则转换正确性 +- 规则应用成功率 +- 事件监听响应 +- 错误处理能力 +- 性能表现 + +## 部署建议 + +### 开发环境 +- 启用调试日志: `chrome.runtime.sendMessage({method: 'enableDebugLogging'})` +- 运行测试: `testRuleApplication()` +- 检查规则统计: `chrome.runtime.sendMessage({method: 'getRuleStats'})` + +### 生产环境 +- 默认使用 INFO 级别日志 +- 定期检查规则应用状态 +- 监控转换失败的规则 + +## 后续工作建议 + +1. **用户体验优化** + - 添加规则转换失败的用户提示 + - 提供规则迁移建议 + - 改进错误消息的用户友好性 + +2. **功能增强** + - 支持更多的 URL 匹配模式 + - 优化规则优先级管理 + - 添加规则冲突检测 + +3. **性能优化** + - 优化大量规则的处理性能 + - 减少规则应用的延迟 + - 改进内存使用 + +4. **稳定性改进** + - 增强错误恢复机制 + - 添加规则备份和恢复功能 + - 提高扩展的崩溃恢复能力 + +## 结论 + +本次修复解决了 Header Editor 在 Manifest V3 环境下的核心问题,确保了规则的正确转换和应用。通过完善的事件监听、详细的日志记录和自动化测试,显著提升了扩展的稳定性和可维护性。 + +虽然 V3 的限制导致部分高级功能无法使用,但对于大多数用户的基本需求(请求头修改、URL 重定向、请求阻止等),扩展已能够正常工作。 \ No newline at end of file diff --git a/package.json b/package.json index b2eefc74..59c8161b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "header-editor", - "version": "5.0.0", + "version": "6.0.0", "description": "Header Editor", "author": "ShuangYa", "license": "GPL-2.0", diff --git a/src/enterprise/policy-handler.ts b/src/enterprise/policy-handler.ts new file mode 100644 index 00000000..d0bddded --- /dev/null +++ b/src/enterprise/policy-handler.ts @@ -0,0 +1,371 @@ +import browser from 'webextension-polyfill'; +import logger from '@/share/core/logger'; +import { prefs } from '@/share/core/prefs'; + +/** + * 企业策略支持处理器 + * 在企业环境中提供完整的功能支持 + */ +export class EnterpriseSupport { + private static instance: EnterpriseSupport; + private isEnterpriseEnvironment = false; + private managementInfo: any = null; + private policyInfo: any = null; + + constructor() { + this.detectEnterpriseEnvironment(); + } + + static getInstance(): EnterpriseSupport { + if (!EnterpriseSupport.instance) { + EnterpriseSupport.instance = new EnterpriseSupport(); + } + return EnterpriseSupport.instance; + } + + /** + * 检测是否在企业环境中 + */ + private async detectEnterpriseEnvironment(): Promise { + try { + // 检查扩展是否由企业策略安装 + const managementInfo = await browser.management.getSelf(); + this.managementInfo = managementInfo; + + // 检查安装类型 + const isEnterpriseInstalled = managementInfo.installType === 'admin' || + managementInfo.installType === 'development'; + + // 检查是否有企业策略 + const hasPolicySupport = await this.checkPolicySupport(); + + this.isEnterpriseEnvironment = isEnterpriseInstalled || hasPolicySupport; + + if (this.isEnterpriseEnvironment) { + logger.info('检测到企业环境,启用完整功能支持'); + await this.enableEnterpriseFeatures(); + } + } catch (error) { + logger.error('检测企业环境时发生错误:', error); + this.isEnterpriseEnvironment = false; + } + } + + /** + * 检查企业策略支持 + */ + private async checkPolicySupport(): Promise { + try { + // 检查是否有企业策略 API + if (!browser.enterprise || !browser.enterprise.platformKeys) { + return false; + } + + // 尝试检查企业策略设置 + // 这里可以添加更多的企业策略检测逻辑 + return true; + } catch (error) { + logger.debug('企业策略检测失败:', error); + return false; + } + } + + /** + * 启用企业功能 + */ + private async enableEnterpriseFeatures(): Promise { + try { + // 记录企业环境信息 + await prefs.set('enterprise_mode', { + enabled: true, + installType: this.managementInfo?.installType, + timestamp: Date.now(), + }); + + // 启用完整的 webRequest 功能 + await this.enableFullWebRequestSupport(); + + // 设置企业特定的配置 + await this.applyEnterpriseConfiguration(); + + logger.info('企业功能已启用'); + } catch (error) { + logger.error('启用企业功能时发生错误:', error); + } + } + + /** + * 启用完整的 webRequest 支持 + */ + private async enableFullWebRequestSupport(): Promise { + try { + // 在企业环境中,可以使用完整的 webRequest API + // 这里设置相关的配置标志 + await prefs.set('webRequest_enterprise_enabled', true); + + // 通知其他组件使用完整功能 + logger.info('企业环境中启用完整 webRequest 支持'); + } catch (error) { + logger.error('启用 webRequest 支持时发生错误:', error); + } + } + + /** + * 应用企业配置 + */ + private async applyEnterpriseConfiguration(): Promise { + try { + // 企业特定的配置 + const enterpriseConfig = { + // 允许更多的规则数量 + maxRules: 100000, + // 允许复杂的正则表达式 + allowComplexRegex: true, + // 允许用户自定义函数 + allowCustomFunctions: true, + // 允许响应体修改 + allowResponseBodyModification: true, + // 禁用一些限制 + disableV3Restrictions: true, + }; + + await prefs.set('enterprise_config', enterpriseConfig); + logger.info('企业配置已应用:', enterpriseConfig); + } catch (error) { + logger.error('应用企业配置时发生错误:', error); + } + } + + /** + * 检查当前是否为企业环境 + */ + isEnterpriseMode(): boolean { + return this.isEnterpriseEnvironment; + } + + /** + * 获取企业信息 + */ + getEnterpriseInfo(): { + isEnterprise: boolean; + installType?: string; + hasFullSupport: boolean; + supportedFeatures: string[]; + } { + return { + isEnterprise: this.isEnterpriseEnvironment, + installType: this.managementInfo?.installType, + hasFullSupport: this.isEnterpriseEnvironment, + supportedFeatures: this.isEnterpriseEnvironment ? [ + 'webRequest', + 'customFunctions', + 'responseBodyModification', + 'unlimitedRules', + 'complexRegex', + ] : [], + }; + } + + /** + * 请求企业策略权限 + */ + async requestEnterprisePermissions(): Promise { + try { + if (!this.isEnterpriseEnvironment) { + logger.warn('非企业环境,无法请求企业权限'); + return false; + } + + // 请求额外的权限 + const granted = await browser.permissions.request({ + permissions: ['webRequest', 'webRequestBlocking', 'management'], + origins: ['*://*/*'], + }); + + if (granted) { + logger.info('企业权限已授予'); + await this.enableEnterpriseFeatures(); + } + + return granted; + } catch (error) { + logger.error('请求企业权限时发生错误:', error); + return false; + } + } + + /** + * 获取企业策略配置 + */ + async getEnterpriseConfig(): Promise { + try { + return await prefs.get('enterprise_config') || {}; + } catch (error) { + logger.error('获取企业配置时发生错误:', error); + return {}; + } + } + + /** + * 设置企业策略配置 + */ + async setEnterpriseConfig(config: any): Promise { + try { + if (!this.isEnterpriseEnvironment) { + throw new Error('非企业环境,无法设置企业配置'); + } + + await prefs.set('enterprise_config', config); + logger.info('企业配置已更新:', config); + } catch (error) { + logger.error('设置企业配置时发生错误:', error); + throw error; + } + } + + /** + * 生成企业部署指南 + */ + generateDeploymentGuide(): { + policyTemplate: any; + installationSteps: string[]; + configurationOptions: any; + } { + return { + policyTemplate: { + '3rdparty': { + extensions: { + 'headereditor@addon.firefoxcn.net': { + enterprise_mode: true, + max_rules: 100000, + allow_custom_functions: true, + allow_response_body_modification: true, + disable_v3_restrictions: true, + }, + }, + }, + }, + installationSteps: [ + '1. 下载企业版 Header Editor 扩展包', + '2. 创建企业策略配置文件', + '3. 通过 Group Policy 或 MDM 部署策略', + '4. 在目标机器上安装扩展', + '5. 验证企业功能是否正常工作', + ], + configurationOptions: { + maxRules: { + description: '最大规则数量', + type: 'number', + default: 100000, + min: 1000, + max: 1000000, + }, + allowCustomFunctions: { + description: '允许自定义函数', + type: 'boolean', + default: true, + }, + allowResponseBodyModification: { + description: '允许响应体修改', + type: 'boolean', + default: true, + }, + disableV3Restrictions: { + description: '禁用 V3 限制', + type: 'boolean', + default: true, + }, + }, + }; + } + + /** + * 验证企业功能 + */ + async validateEnterpriseFeatures(): Promise<{ + isValid: boolean; + availableFeatures: string[]; + missingFeatures: string[]; + recommendations: string[]; + }> { + try { + const availableFeatures: string[] = []; + const missingFeatures: string[] = []; + const recommendations: string[] = []; + + // 检查 webRequest 权限 + const permissions = await browser.permissions.getAll(); + if (permissions.permissions?.includes('webRequest')) { + availableFeatures.push('webRequest'); + } else { + missingFeatures.push('webRequest'); + recommendations.push('需要通过企业策略授予 webRequest 权限'); + } + + // 检查管理权限 + if (permissions.permissions?.includes('management')) { + availableFeatures.push('management'); + } else { + missingFeatures.push('management'); + } + + // 检查企业配置 + const enterpriseConfig = await this.getEnterpriseConfig(); + if (Object.keys(enterpriseConfig).length > 0) { + availableFeatures.push('enterpriseConfig'); + } else { + missingFeatures.push('enterpriseConfig'); + recommendations.push('需要设置企业配置'); + } + + return { + isValid: missingFeatures.length === 0, + availableFeatures, + missingFeatures, + recommendations, + }; + } catch (error) { + logger.error('验证企业功能时发生错误:', error); + return { + isValid: false, + availableFeatures: [], + missingFeatures: ['unknown'], + recommendations: ['验证过程中发生错误,请检查日志'], + }; + } + } + + /** + * 获取企业支持状态 + */ + async getStatus(): Promise<{ + isSupported: boolean; + installType: string; + features: any; + config: any; + validation: any; + }> { + try { + const validation = await this.validateEnterpriseFeatures(); + + return { + isSupported: this.isEnterpriseEnvironment, + installType: this.managementInfo?.installType || 'unknown', + features: this.getEnterpriseInfo(), + config: await this.getEnterpriseConfig(), + validation, + }; + } catch (error) { + logger.error('获取企业支持状态时发生错误:', error); + return { + isSupported: false, + installType: 'unknown', + features: { isEnterprise: false, hasFullSupport: false, supportedFeatures: [] }, + config: {}, + validation: { isValid: false, availableFeatures: [], missingFeatures: [], recommendations: [] }, + }; + } + } +} + +export default EnterpriseSupport; diff --git a/src/manifest.json b/src/manifest.json index c53f18b3..5ce244f0 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -4,39 +4,45 @@ "version": null, "description": "__MSG_description__", "homepage_url": "https://he.firefoxcn.net", - "manifest_version": 2, + "manifest_version": 3, "icons": { "128": "assets/images/128.png" }, "permissions": [ "tabs", - "webRequest", - "webRequestBlocking", "storage", - "*://*/*", - "unlimitedStorage" + "notifications", + "declarativeNetRequest", + "declarativeNetRequestFeedback", + "declarativeNetRequestWithHostAccess" ], - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';", + "host_permissions": [ + "*://*/*" + ], + "declarative_net_request": { + "rule_resources": [] + }, + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" + }, "background": { - "scripts": [ - "assets/js/background.js" - ] + "service_worker": "assets/js/background.js" }, - "content_scripts": [ - { - "matches": [""], - "css": [ - "assets/css/content.css" - ], - "js": [ - "external/react.min.js", - "external/react-dom.min.js", - "assets/js/content.js" - ], - "run_at": "document_end" - } - ], - "browser_action": { + "content_scripts": [ + { + "matches": [""], + "css": [ + "assets/css/content.css" + ], + "js": [ + "external/react.min.js", + "external/react-dom.min.js", + "assets/js/content.js" + ], + "run_at": "document_end" + } + ], + "action": { "default_icon": { "128": "assets/images/128.png" }, @@ -47,18 +53,5 @@ "options_ui": { "page": "options.html", "open_in_tab": true - }, - "__amo__browser_specific_settings": { - "gecko": { - "id": "headereditor-amo@addon.firefoxcn.net", - "strict_min_version": "77.0" - } - }, - "__xpi__browser_specific_settings": { - "gecko": { - "id": "headereditor@addon.firefoxcn.net", - "strict_min_version": "77.0", - "update_url": "https://ext.firefoxcn.net/header-editor/install/update.json" - } - } + } } \ No newline at end of file diff --git a/src/pages/background/api-handler.ts b/src/pages/background/api-handler.ts index 8b46c64a..d5ae09f2 100644 --- a/src/pages/background/api-handler.ts +++ b/src/pages/background/api-handler.ts @@ -6,112 +6,229 @@ import rules from './core/rules'; import { openURL } from './utils'; import { getDatabase } from './core/db'; +// 获取全局规则处理器 +function getRuleHandler() { + return (globalThis as any).headerEditorRuleHandler; +} function execute(request: any) { + logger.debug('执行 API 请求:', request); + if (request.method === 'notifyBackground') { request.method = request.reason; delete request.reason; } + switch (request.method) { case APIs.HEALTH_CHECK: return new Promise((resolve) => { getDatabase() - .then(() => resolve(true)) - .catch(() => resolve(false)); + .then(() => { + logger.debug('健康检查通过'); + resolve(true); + }) + .catch((error) => { + logger.error('健康检查失败:', error); + resolve(false); + }); }); + case APIs.OPEN_URL: + logger.debug('打开URL:', request.url); return openURL(request); - case APIs.GET_RULES: - return Promise.resolve(rules.get(request.type, request.options)); + + case APIs.GET_RULES: { + logger.debug('获取规则:', { type: request.type, options: request.options }); + const rulesResult = rules.get(request.type, request.options); + logger.debug('规则查询结果:', rulesResult?.length || 0, '条规则'); + return Promise.resolve(rulesResult); + } + case APIs.SAVE_RULE: + logger.debug('保存规则:', request.rule); + + // 通知活动标签页 browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { tabs.forEach((tab) => { if (tab.id) { - console.log('[Header Editor] 向 content-script 发送消息', { tabId: tab.id, method: APIs.SAVE_RULE, rule: request.rule }); + logger.debug('向 content-script 发送保存规则消息', { tabId: tab.id, rule: request.rule }); browser.tabs.sendMessage(tab.id, { method: APIs.SAVE_RULE, rule: request.rule }); } }); }); - return rules.save(request.rule); + + // 保存规则并刷新 V3 规则 + return rules.save(request.rule).then((result) => { + logger.info('规则保存成功:', result); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(保存规则后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } else { + logger.warn('规则处理器未找到,无法刷新 V3 规则'); + } + + return result; + }).catch((error) => { + logger.error('保存规则失败:', error); + throw error; + }); + case APIs.DELETE_RULE: - return rules.remove(request.type, request.id); + logger.debug('删除规则:', { type: request.type, id: request.id }); + + return rules.remove(request.type, request.id).then((result) => { + logger.info('规则删除成功:', { type: request.type, id: request.id }); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(删除规则后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } else { + logger.warn('规则处理器未找到,无法刷新 V3 规则'); + } + + return result; + }).catch((error) => { + logger.error('删除规则失败:', error); + throw error; + }); + case APIs.SET_PREFS: + logger.debug('设置偏好:', { key: request.key, value: request.value }); + + // 通知活动标签页 browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { tabs.forEach((tab) => { if (tab.id) { - console.log('[Header Editor] 向 content-script 发送消息', { tabId: tab.id, method: APIs.SET_PREFS, key: request.key, value: request.value }); + logger.debug('向 content-script 发送偏好设置消息', { tabId: tab.id, key: request.key, value: request.value }); browser.tabs.sendMessage(tab.id, { method: APIs.SET_PREFS, key: request.key, value: request.value }); } }); }); - return prefs.set(request.key, request.value); + + return prefs.set(request.key, request.value).then((result) => { + logger.info('偏好设置成功:', { key: request.key, value: request.value }); + + // 如果是禁用/启用扩展,触发 V3 规则刷新 + if (request.key === 'disable-all') { + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(偏好设置变化)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + } + + return result; + }).catch((error) => { + logger.error('设置偏好失败:', error); + throw error; + }); + case APIs.UPDATE_CACHE: + logger.debug('更新缓存:', request.type); + if (request.type === 'all') { - return Promise.all(TABLE_NAMES_ARR.map((tableName) => rules.updateCache(tableName))); + return Promise.all(TABLE_NAMES_ARR.map((tableName) => { + logger.debug('更新表缓存:', tableName); + return rules.updateCache(tableName); + })).then((results) => { + logger.info('所有表缓存更新完成'); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(缓存更新后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + + return results; + }); } else { - return rules.updateCache(request.type); + return rules.updateCache(request.type).then((result) => { + logger.info('表缓存更新完成:', request.type); + + // 触发 V3 规则刷新 + const ruleHandler = getRuleHandler(); + if (ruleHandler) { + logger.debug('触发 V3 规则刷新(缓存更新后)'); + ruleHandler.refresh().catch((error: any) => { + logger.error('V3 规则刷新失败:', error); + }); + } + + return result; + }); } + default: + logger.warn('未知的 API 方法:', request.method); break; } // return false; } -const currentIP = {}; - export default function createApiHandler() { - // get IP using webRequest - browser.webRequest.onCompleted.addListener( - (info) => { - const u = new URL(info.url); - if (info.tabId in currentIP) { - currentIP[info.tabId][u.hostname] = info.ip; - } else { - currentIP[info.tabId] = { [u.hostname]: info.ip }; - } - }, - { - urls: [], - types: [], - }, - [], - ); + logger.info('创建 API 处理器'); browser.runtime.onMessage.addListener((request: any, sender, sendResponse) => { if (request.method === 'GetData') { - console.log('[Header Editor] 收到来自 content-script 的消息', { request, sender }); - const currentIPList: Array<{ domain: string; ip: string }> = []; - const tabId = sender.tab?.id || 0; - if (tabId in currentIP) { - for (const key in currentIP[tabId]) { - if (Object.prototype.hasOwnProperty.call(currentIP[tabId], key)) { - currentIPList.push({ domain: key, ip: currentIP[tabId][key] }); - } - } - } + logger.debug('收到来自 content-script 的 GetData 请求', { request, sender }); const response = { rules: rules.get(TABLE_NAMES.sendHeader), enableRules: rules.get(TABLE_NAMES.sendHeader, { enable: true }), enable: !prefs.get('disable-all'), - currentIPList, + currentIPList: [], // V3 中无法获取IP信息 }; - console.log('[Header Editor] 返回 content-script 的数据', response); + logger.debug('返回 content-script 的数据', { + rulesCount: response.rules?.length || 0, + enableRulesCount: response.enableRules?.length || 0, + enable: response.enable, + }); + sendResponse(response); + return; } - logger.debug('Background Receive Message', request); + logger.debug('Background 收到消息', request); + if (request.method === 'batchExecute') { - const queue = request.batch.map((item) => { + logger.debug('执行批量操作:', request.batch?.length || 0, '个操作'); + + const queue = request.batch.map((item: any) => { const res = execute(item); if (res) { return res; } return Promise.resolve(); }); - return Promise.allSettled(queue); + + return Promise.allSettled(queue).then((results) => { + logger.debug('批量操作完成:', results.length, '个结果'); + return results; + }); + } + + const result = execute(request); + if (result && typeof result.then === 'function') { + result.catch((error) => { + logger.error('API 执行失败:', error); + }); } - return execute(request); + + return result; }); } diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index a38158a6..93028244 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -1,13 +1,500 @@ +// Header Editor Manifest V3 背景脚本 +// 导入必要的模块 +import { prefs } from '@/share/core/prefs'; +import { TABLE_NAMES_ARR, TABLE_NAMES, EVENTs, RULE_TYPE, RULE_MATCH_TYPE } from '@/share/core/constant'; +import logger from '@/share/core/logger'; +import notify from '@/share/core/notify'; import createApiHandler from './api-handler'; -import createRequestHandler from './request-handler'; -import './upgrade'; +import { V3RuleConverter } from './v3-rule-converter'; +import rules from './core/rules'; -if (typeof window !== 'undefined') { - window.IS_BACKGROUND = true; +console.log('Header Editor Service Worker 启动'); + +// 设置全局标识 +if (typeof globalThis !== 'undefined') { + globalThis.IS_BACKGROUND = true; } -console.log('background/index.ts'); +// 规则处理器 +class V3RuleHandler { + private conversionStats: any = null; + private isInitialized = false; + + async initialize(): Promise { + console.log('初始化 V3 规则处理器...'); + + // 等待偏好设置准备完成 + await new Promise((resolve) => { + prefs.ready(() => { + console.log('偏好设置已准备完成'); + resolve(); + }); + }); + + // 等待数据库和规则缓存完全准备好 + await this.waitForRulesReady(); + + // 创建API处理器 + createApiHandler(); + console.log('API处理器已创建'); + + // 设置规则变化监听 + this.setupRuleEventListeners(); + + // 加载并应用规则 + await this.loadAndApplyRules(); + + this.isInitialized = true; + console.log('V3 规则处理器初始化完成'); + } + + private async waitForRulesReady(): Promise { + console.log('等待规则缓存准备完成...'); + + // 等待所有表的缓存更新完成 + const maxRetries = 30; // 最多等待30秒 + let retries = 0; + + while (retries < maxRetries) { + let allReady = true; + + for (const tableName of TABLE_NAMES_ARR) { + const tableRules = rules.get(tableName); + if (tableRules === null) { + allReady = false; + break; + } + } + + if (allReady) { + console.log('所有规则缓存已准备完成'); + return; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + retries++; + } + + logger.warn('等待规则缓存超时,但继续初始化'); + } + + private setupRuleEventListeners(): void { + console.log('设置规则变化事件监听...'); + + // 监听规则更新事件 + notify.event.on(EVENTs.RULE_UPDATE, (event: any) => { + console.log('收到规则更新事件:', event); + this.handleRuleChange(); + }); + + // 监听规则删除事件 + notify.event.on(EVENTs.RULE_DELETE, (event: any) => { + console.log('收到规则删除事件:', event); + this.handleRuleChange(); + }); + } + + private async handleRuleChange(): Promise { + if (!this.isInitialized) { + console.log('规则处理器尚未初始化,跳过规则变化处理'); + return; + } + + try { + console.log('处理规则变化,重新应用规则...'); + await this.loadAndApplyRules(); + console.log('规则变化处理完成'); + } catch (error) { + logger.error('处理规则变化时发生错误:', error); + console.error('处理规则变化失败:', error); + } + } + + async loadAndApplyRules(): Promise { + try { + // 检查扩展是否被禁用 + if (prefs.get('disable-all')) { + console.log('扩展已被禁用,清除所有规则'); + await V3RuleConverter.applyV3Rules([]); + return; + } + + // 获取所有启用的规则 + const allRules: any[] = []; + for (const tableName of TABLE_NAMES_ARR) { + const tableRules = rules.get(tableName, { enable: true }) || []; + allRules.push(...tableRules); + } + + console.log(`加载了 ${allRules.length} 个启用的规则`); + + if (allRules.length === 0) { + // 清除所有动态规则 + await V3RuleConverter.applyV3Rules([]); + logger.info('没有启用的规则,已清除所有动态规则'); + return; + } + + // 转换规则为 V3 格式 + const conversionResult = V3RuleConverter.convertRulesToV3(allRules); + this.conversionStats = conversionResult; + + // 检查规则限制 + const limitCheck = V3RuleConverter.checkRuleLimits(conversionResult.convertedRules); + if (!limitCheck.isValid) { + logger.warn('规则超过 V3 限制:', limitCheck.errors); + console.warn('规则超过 V3 限制:', limitCheck.errors); + } + + // 应用 V3 规则 + await V3RuleConverter.applyV3Rules(conversionResult.convertedRules); + + const stats = { + total: allRules.length, + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings.length, + }; + + logger.info('规则应用完成:', stats); + console.log('规则应用统计:', stats); + + if (conversionResult.unconvertedRules.length > 0) { + console.warn('无法转换的规则:', conversionResult.unconvertedRules.map((r) => r.name)); + } + + if (conversionResult.warnings.length > 0) { + console.warn('转换警告:', conversionResult.warnings); + } + } catch (error) { + logger.error('应用规则时发生错误:', error); + console.error('应用规则失败:', error); + throw error; + } + } + + async refresh(): Promise { + console.log('刷新规则...'); + await this.loadAndApplyRules(); + } + + getStats(): any { + return this.conversionStats; + } +} + +// 测试 x-tag header 规则 +async function testXTagHeaderRule() { + console.log('开始测试 x-tag header 规则...'); + + try { + // 1. 检查当前规则 + console.log('1. 检查现有的 sendHeader 规则...'); + const currentSendRules = rules.get(TABLE_NAMES.sendHeader, { enable: true }) || []; + console.log(`当前有 ${currentSendRules.length} 个启用的发送头规则`); + + const xTagRules = currentSendRules.filter((rule) => + rule.action && + typeof rule.action === 'object' && + rule.action.name && + rule.action.name.toLowerCase() === 'x-tag'); + + console.log(`其中有 ${xTagRules.length} 个 x-tag 规则:`, xTagRules); + + // 2. 创建测试规则(如果不存在) + if (xTagRules.length === 0) { + console.log('2. 创建测试 x-tag 规则...'); + const testXTagRule = { + id: -1, // 新规则 + name: '测试 X-Tag Header', + enable: true, + ruleType: RULE_TYPE.MODIFY_SEND_HEADER, + matchType: RULE_MATCH_TYPE.ALL, + pattern: '*', + isFunction: false, + code: '', + exclude: '', + group: 'test', + action: { + name: 'X-Tag', + value: 'HeaderEditor-Test', + }, + }; + + console.log('准备保存测试规则:', testXTagRule); + const savedRule = await rules.save(testXTagRule); + console.log('测试规则保存成功:', savedRule); + + // 等待一下让事件处理完成 + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + // 3. 重新检查规则 + console.log('3. 重新检查规则...'); + const updatedSendRules = rules.get(TABLE_NAMES.sendHeader, { enable: true }) || []; + const updatedXTagRules = updatedSendRules.filter((rule) => + rule.action && + typeof rule.action === 'object' && + rule.action.name && + rule.action.name.toLowerCase().includes('tag')); + console.log(`现在有 ${updatedXTagRules.length} 个 tag 相关规则:`, updatedXTagRules); + + // 4. 检查规则转换 + console.log('4. 测试规则转换...'); + if (updatedXTagRules.length > 0) { + const conversionResult = V3RuleConverter.convertRulesToV3(updatedXTagRules); + console.log('规则转换结果:', { + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings, + }); + + if (conversionResult.convertedRules.length > 0) { + console.log('转换后的 V3 规则:', conversionResult.convertedRules); + } + + if (conversionResult.warnings.length > 0) { + console.warn('转换警告:', conversionResult.warnings); + } + } -// 开始初始化 -createApiHandler(); -createRequestHandler(); + // 5. 检查当前应用的 V3 规则 + console.log('5. 检查当前应用的 V3 规则...'); + const currentV3Rules = await V3RuleConverter.getCurrentRules(); + console.log(`当前已应用 ${currentV3Rules.length} 个 V3 规则`); + + const v3HeaderRules = currentV3Rules.filter((rule) => + rule.action && + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders && + rule.action.requestHeaders.some((header) => + header.header && header.header.toLowerCase().includes('tag'))); + + console.log(`其中有 ${v3HeaderRules.length} 个修改 tag header 的 V3 规则:`, v3HeaderRules); + + // 6. 刷新规则应用 + console.log('6. 刷新规则应用...'); + if (ruleHandler) { + await ruleHandler.refresh(); + console.log('规则刷新完成'); + + // 再次检查 V3 规则 + const refreshedV3Rules = await V3RuleConverter.getCurrentRules(); + const refreshedHeaderRules = refreshedV3Rules.filter((rule) => + rule.action && + rule.action.type === 'modifyHeaders' && + rule.action.requestHeaders && + rule.action.requestHeaders.some((header) => + header.header && header.header.toLowerCase().includes('tag'))); + + console.log(`刷新后有 ${refreshedHeaderRules.length} 个修改 tag header 的 V3 规则:`, refreshedHeaderRules); + } + + // 7. 提供测试建议 + console.log('7. 测试建议:'); + console.log('请访问任意网站(如 https://httpbin.org/headers)查看请求头是否包含 X-Tag'); + console.log('您也可以在开发者工具的 Network 标签中查看请求头'); + + console.log('x-tag header 规则测试完成'); + } catch (error) { + console.error('测试 x-tag header 规则时发生错误:', error); + } +} + +// 导出测试函数到全局 +if (typeof globalThis !== 'undefined') { + globalThis.testXTagHeaderRule = testXTagHeaderRule; +} + +// 全局规则处理器实例 +let ruleHandler: V3RuleHandler | null = null; + +// 测试辅助函数 +async function testRuleApplication() { + if (!ruleHandler) { + console.error('规则处理器未初始化'); + return; + } + + console.log('开始测试规则应用功能...'); + + try { + // 1. 测试获取现有规则 + console.log('1. 获取现有规则...'); + const currentRules = await V3RuleConverter.getCurrentRules(); + console.log(`当前已应用 ${currentRules.length} 个 V3 规则`); + + // 2. 测试规则转换 + console.log('2. 测试规则转换...'); + const testRule = { + id: 999, + name: '测试规则', + enable: true, + ruleType: RULE_TYPE.MODIFY_SEND_HEADER, + matchType: RULE_MATCH_TYPE.ALL, + pattern: '*', + isFunction: false, + code: '', + exclude: '', + group: 'test', + action: { + name: 'User-Agent', + value: 'Test-Agent', + }, + }; + + const conversionResult = V3RuleConverter.convertRulesToV3([testRule]); + console.log('规则转换结果:', { + converted: conversionResult.convertedRules.length, + unconverted: conversionResult.unconvertedRules.length, + warnings: conversionResult.warnings.length, + }); + + if (conversionResult.convertedRules.length > 0) { + console.log('转换后的 V3 规则:', conversionResult.convertedRules[0]); + } + + // 3. 测试规则限制检查 + console.log('3. 测试规则限制检查...'); + const limits = V3RuleConverter.getRuleLimits(); + const limitCheck = V3RuleConverter.checkRuleLimits(conversionResult.convertedRules); + console.log('规则限制:', limits); + console.log('规则限制检查结果:', limitCheck); + + // 4. 测试规则刷新 + console.log('4. 测试规则刷新...'); + await ruleHandler.refresh(); + console.log('规则刷新完成'); + + // 5. 获取统计信息 + console.log('5. 获取统计信息...'); + const stats = ruleHandler.getStats(); + console.log('转换统计:', stats); + + // 6. 验证规则数量 + console.log('6. 验证规则数量...'); + const newRules = await V3RuleConverter.getCurrentRules(); + console.log(`刷新后已应用 ${newRules.length} 个 V3 规则`); + + console.log('规则应用功能测试完成'); + } catch (error) { + console.error('测试规则应用功能时发生错误:', error); + } +} + +// 导出测试函数到全局 +if (typeof globalThis !== 'undefined') { + globalThis.testRuleApplication = testRuleApplication; +} + +// 初始化函数 +async function initialize() { + try { + console.log('开始初始化 Header Editor...'); + + ruleHandler = new V3RuleHandler(); + await ruleHandler.initialize(); + + console.log('Header Editor 初始化完成'); + + // 在调试模式下运行测试 + if (logger.getLevel() === 'DEBUG') { + setTimeout(() => { + testRuleApplication(); + }, 2000); + } + } catch (error) { + console.error('Header Editor 初始化失败:', error); + + // 显示错误通知 + try { + await chrome.notifications.create({ + type: 'basic', + iconUrl: 'assets/images/128.png', + title: 'Header Editor 初始化失败', + message: '请检查控制台错误信息,或重新加载扩展。', + }); + } catch (notifyError) { + console.error('显示通知失败:', notifyError); + } + } +} + +// 监听规则更新事件 +if (typeof chrome !== 'undefined' && chrome.runtime) { + // 监听安装事件 + chrome.runtime.onInstalled.addListener((details) => { + console.log('Extension installed:', details.reason); + + if (details.reason === 'install') { + // 首次安装时打开选项页面 + try { + chrome.tabs.create({ + url: chrome.runtime.getURL('options.html'), + }); + } catch (error) { + console.log('无法创建选项页面:', error); + } + } + }); + + // 监听启动事件 + chrome.runtime.onStartup.addListener(() => { + console.log('Extension startup'); + }); + + // 监听偏好设置变化 + chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === 'local' && changes['disable-all']) { + console.log('检测到 disable-all 偏好设置变化:', changes['disable-all']); + if (ruleHandler) { + ruleHandler.refresh(); + } + } + }); + + // 监听开发者工具命令 + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.method === 'testRuleApplication') { + testRuleApplication().then(() => { + sendResponse({ success: true }); + }).catch((error) => { + sendResponse({ success: false, error: error.message }); + }); + return true; // 保持消息通道开放 + } + + if (request.method === 'getRuleStats') { + const stats = ruleHandler?.getStats() || null; + sendResponse({ stats }); + return true; + } + + if (request.method === 'enableDebugLogging') { + logger.enableDebug(); + sendResponse({ success: true, level: logger.getLevel() }); + return true; + } + + if (request.method === 'disableDebugLogging') { + logger.disableDebug(); + sendResponse({ success: true, level: logger.getLevel() }); + return true; + } + }); +} + +// 简单的状态检查 +console.log('Service Worker 环境检查:', { + hasChrome: typeof chrome !== 'undefined', + hasRuntime: typeof chrome !== 'undefined' && !!chrome.runtime, + hasDeclarativeNetRequest: typeof chrome !== 'undefined' && !!(chrome as any).declarativeNetRequest, +}); + +// 启动初始化 +initialize().catch(console.error); + +// 导出全局访问 +if (typeof globalThis !== 'undefined') { + globalThis.headerEditorRuleHandler = ruleHandler; +} diff --git a/src/pages/background/request-handler.ts b/src/pages/background/request-handler.ts deleted file mode 100644 index 7c29742f..00000000 --- a/src/pages/background/request-handler.ts +++ /dev/null @@ -1,492 +0,0 @@ -/* eslint-disable @typescript-eslint/member-ordering */ -import { TextDecoder, TextEncoder } from 'text-encoding'; -import browser, { WebRequest } from 'webextension-polyfill'; -import { getGlobal, IS_CHROME, IS_SUPPORT_STREAM_FILTER } from '@/share/core/utils'; -import emitter from '@/share/core/emitter'; -import logger from '@/share/core/logger'; -import type { Rule } from '@/share/core/types'; -import { TABLE_NAMES } from '@/share/core/constant'; -import { prefs } from '@/share/core/prefs'; -import rules from './core/rules'; - -// 最大修改8MB的Body -const MAX_BODY_SIZE = 8 * 1024 * 1024; - -enum REQUEST_TYPE { - REQUEST, - RESPONSE, -} - -type HeaderRequestDetails = WebRequest.OnHeadersReceivedDetailsType | WebRequest.OnBeforeSendHeadersDetailsType; -type AnyRequestDetails = WebRequest.OnBeforeRequestDetailsType | HeaderRequestDetails; -interface CustomFunctionDetail { - id: string; - url: string; - tab: number; - method: string; - frame: number; - parentFrame: number; - // @ts-ignore - proxy: any; - type: WebRequest.ResourceType; - time: number; - originUrl: string; - documentUrl: string; - requestHeaders: WebRequest.HttpHeaders | null; - responseHeaders: WebRequest.HttpHeaders | null; - statusCode?: number; - statusLine?: string; -} -class RequestHandler { - private _disableAll = false; - private excludeHe = true; - private includeHeaders = false; - private modifyBody = false; - private savedRequestHeader = new Map(); - private deleteHeaderTimer: ReturnType | null = null; - private deleteHeaderQueue = new Map(); - private textDecoder: Map = new Map(); - private textEncoder: Map = new Map(); - - constructor() { - this.initHook(); - this.loadPrefs(); - } - get disableAll() { - return this._disableAll; - } - set disableAll(to) { - if (this._disableAll === to) { - return; - } - this._disableAll = to; - browser.browserAction.setIcon({ - path: `/assets/images/128${to ? 'w' : ''}.png`, - }); - } - - private createHeaderListener(type: string): any { - const result = ['blocking']; - result.push(type); - if ( - IS_CHROME && - // @ts-ignore - chrome.webRequest.OnBeforeSendHeadersOptions.hasOwnProperty('EXTRA_HEADERS') - ) { - result.push('extraHeaders'); - } - return result; - } - - private initHook() { - browser.webRequest.onBeforeRequest.addListener(this.handleBeforeRequest.bind(this), { urls: [''] }, [ - 'blocking', - ]); - browser.webRequest.onBeforeSendHeaders.addListener( - this.handleBeforeSend.bind(this), - { urls: [''] }, - this.createHeaderListener('requestHeaders'), - ); - browser.webRequest.onHeadersReceived.addListener( - this.handleReceived.bind(this), - { urls: [''] }, - this.createHeaderListener('responseHeaders'), - ); - } - - private loadPrefs() { - emitter.on(emitter.EVENT_PREFS_UPDATE, (key: string, val: any) => { - switch (key) { - case 'exclude-he': - this.excludeHe = val; - break; - case 'disable-all': - this.disableAll = val; - break; - case 'include-headers': - this.includeHeaders = val; - break; - case 'modify-body': - this.modifyBody = val; - break; - default: - break; - } - }); - - prefs.ready(() => { - this.excludeHe = prefs.get('exclude-he'); - this.disableAll = prefs.get('disable-all'); - this.includeHeaders = prefs.get('include-headers'); - this.modifyBody = prefs.get('modify-body'); - }); - } - - private beforeAll(e: AnyRequestDetails) { - if (this.disableAll) { - return false; - } - // 判断是否是HE自身 - if (this.excludeHe && e.url.indexOf(browser.runtime.getURL('')) === 0) { - return false; - } - return true; - } - - /** - * BeforeRequest事件,可撤销、重定向 - * @param any e - */ - handleBeforeRequest(e: WebRequest.OnBeforeRequestDetailsType) { - if (!this.beforeAll(e)) { - return; - } - logger.debug(`handle before request ${e.url}`, e); - // 可用:重定向,阻止加载 - const rule = rules.get(TABLE_NAMES.request, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule === null) { - return; - } - let redirectTo = e.url; - const detail = this.makeDetails(e); - for (const item of rule) { - if (item.action === 'cancel' && !item.isFunction) { - return { cancel: true }; - } else if (item.isFunction) { - try { - const r = item._func(redirectTo, detail); - if (typeof r === 'string') { - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${r}`); - redirectTo = r; - } - if (r === '_header_editor_cancel_' || (item.action === 'cancel' && r === true)) { - logger.debug(`[rule: ${item.id}] cancel`); - return { cancel: true }; - } - } catch (err) { - console.error(err); - } - } else if (item.to) { - if (item.matchType === 'regexp') { - const to = redirectTo.replace(item._reg, item.to); - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${to}`); - redirectTo = to; - } else { - logger.debug(`[rule: ${item.id}] redirect ${redirectTo} to ${item.to}`); - redirectTo = item.to; - } - } - } - if (redirectTo && redirectTo !== e.url) { - if (/^([a-zA-Z0-9]+)%3A/.test(redirectTo)) { - redirectTo = decodeURIComponent(redirectTo); - } - return { redirectUrl: redirectTo }; - } - } - - /** - * beforeSend事件,可修改请求头 - * @param any e - */ - handleBeforeSend(e: WebRequest.OnBeforeSendHeadersDetailsType) { - if (!this.beforeAll(e)) { - return; - } - // 修改请求头 - if (!e.requestHeaders) { - return; - } - logger.debug(`handle before send ${e.url}`, e.requestHeaders); - const rule = rules.get(TABLE_NAMES.sendHeader, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule === null) { - return; - } - this.modifyHeaders(e, REQUEST_TYPE.REQUEST, rule); - logger.debug(`handle before send:finish ${e.url}`, e.requestHeaders); - return { requestHeaders: e.requestHeaders }; - } - - handleReceived(e: WebRequest.OnHeadersReceivedDetailsType) { - if (!this.beforeAll(e)) { - return; - } - const detail = this.makeDetails(e); - // 删除暂存的headers - if (this.includeHeaders) { - detail.requestHeaders = this.savedRequestHeader.get(e.requestId) || null; - this.savedRequestHeader.delete(e.requestId); - this.deleteHeaderQueue.delete(e.requestId); - } - // 修改响应体 - if (this.modifyBody) { - let canModifyBody = true; - // 检查有没有Content-Length头,如有,则不能超过MAX_BODY_SIZE,否则不进行修改 - if (e.responseHeaders) { - for (const it of e.responseHeaders) { - if (it.name.toLowerCase() === 'content-length') { - if (it.value && parseInt(it.value, 10) >= MAX_BODY_SIZE) { - canModifyBody = false; - } - break; - } - } - } - if (canModifyBody) { - this.modifyReceivedBody(e, detail); - } - } - // 修改响应头 - if (!e.responseHeaders) { - return; - } - logger.debug(`handle received ${e.url}`, e.responseHeaders); - const rule = rules.get(TABLE_NAMES.receiveHeader, { url: e.url, enable: true }); - // Browser is starting up, pass all requests - if (rule) { - this.modifyHeaders(e, REQUEST_TYPE.RESPONSE, rule, detail); - } - logger.debug(`handle received:finish ${e.url}`, e.responseHeaders); - return { responseHeaders: e.responseHeaders }; - } - - private makeDetails(request: AnyRequestDetails): CustomFunctionDetail { - const details = { - id: request.requestId, - url: request.url, - tab: request.tabId, - method: request.method, - frame: request.frameId, - parentFrame: request.parentFrameId, - // @ts-ignore - proxy: request.proxyInfo || null, - type: request.type, - time: request.timeStamp, - originUrl: request.originUrl || '', - documentUrl: request.documentUrl || '', - requestHeaders: null, - responseHeaders: null, - }; - - ['statusCode', 'statusLine', 'requestHeaders', 'responseHeaders'].forEach((p) => { - if (p in request) { - // @ts-ignore - details[p] = request[p]; - } - }); - - return details; - } - - private textEncode(encoding: string, text: string) { - let encoder = this.textEncoder.get(encoding); - if (!encoder) { - // UTF-8使用原生API,性能更好 - if (encoding === 'UTF-8' && getGlobal().TextEncoder) { - encoder = new (getGlobal().TextEncoder)(); - } else { - encoder = new TextEncoder(encoding, { NONSTANDARD_allowLegacyEncoding: true }); - } - this.textEncoder.set(encoding, encoder); - } - // 防止解码失败导致整体错误 - try { - return encoder.encode(text); - } catch (e) { - console.error(e); - return new Uint8Array(0); - } - } - - private textDecode(encoding: string, buffer: Uint8Array) { - let encoder = this.textDecoder.get(encoding); - if (!encoder) { - // 如果原生支持的话,优先使用原生 - if (getGlobal().TextDecoder) { - try { - encoder = new (getGlobal().TextDecoder)(encoding); - } catch (e) { - encoder = new TextDecoder(encoding); - } - } else { - encoder = new TextDecoder(encoding); - } - this.textDecoder.set(encoding, encoder); - } - // 防止解码失败导致整体错误 - try { - return encoder.decode(buffer); - } catch (e) { - console.error(e); - return ''; - } - } - - private modifyHeaders( - request: HeaderRequestDetails, - type: REQUEST_TYPE, - rule: Rule[], - presetDetail?: CustomFunctionDetail, - ) { - // @ts-ignore - const headers = request[type === REQUEST_TYPE.REQUEST ? 'requestHeaders' : 'responseHeaders']; - if (!headers) { - return; - } - if (this.includeHeaders && type === REQUEST_TYPE.REQUEST) { - // 暂存headers - this.savedRequestHeader.set( - request.requestId, - (request as WebRequest.OnBeforeSendHeadersDetailsType).requestHeaders, - ); - this.autoDeleteSavedHeader(request.requestId); - } - const newHeaders: { [key: string]: string } = {}; - let hasFunction = false; - for (let i = 0; i < rule.length; i++) { - if (!rule[i].isFunction) { - // @ts-ignore - newHeaders[rule[i].action.name] = rule[i].action.value; - rule.splice(i, 1); - i--; - } else { - hasFunction = true; - } - } - for (let i = 0; i < headers.length; i++) { - const name = headers[i].name.toLowerCase(); - if (newHeaders[name] === undefined) { - continue; - } - if (newHeaders[name] === '_header_editor_remove_') { - headers.splice(i, 1); - i--; - } else { - headers[i].value = newHeaders[name]; - } - delete newHeaders[name]; - } - for (const k in newHeaders) { - if (newHeaders[k] === '_header_editor_remove_') { - continue; - } - headers.push({ - name: k, - value: newHeaders[k], - }); - } - if (hasFunction) { - const detail = presetDetail || this.makeDetails(request); - rule.forEach((item) => { - try { - item._func(headers, detail); - } catch (e) { - console.error(e); - } - }); - } - } - - private autoDeleteSavedHeader(id?: string) { - if (id) { - this.deleteHeaderQueue.set(id, new Date().getTime() / 100); - } - if (this.deleteHeaderTimer !== null) { - return; - } - this.deleteHeaderTimer = getGlobal().setTimeout(() => { - // clear timeout - if (this.deleteHeaderTimer) { - clearTimeout(this.deleteHeaderTimer); - } - this.deleteHeaderTimer = null; - // check time - const curTime = new Date().getTime() / 100; - // k: id, v: time - const iter = this.deleteHeaderQueue.entries(); - for (const [k, v] of iter) { - if (curTime - v >= 90) { - this.savedRequestHeader.delete(k); - this.deleteHeaderQueue.delete(k); - } - } - if (this.deleteHeaderQueue.size > 0) { - this.autoDeleteSavedHeader(); - } - }, 10000); - } - - private modifyReceivedBody(e: WebRequest.OnHeadersReceivedDetailsType, detail: CustomFunctionDetail) { - if (!IS_SUPPORT_STREAM_FILTER) { - return; - } - - let rule = rules.get(TABLE_NAMES.receiveBody, { url: e.url, enable: true }); - if (rule === null) { - return; - } - rule = rule.filter((item) => item.isFunction); - if (rule.length === 0) { - return; - } - - const filter = browser.webRequest.filterResponseData(e.requestId); - let buffers: Uint8Array | null = null; - // @ts-ignore - filter.ondata = (event: WebRequest.StreamFilterEventData) => { - const { data } = event; - if (buffers === null) { - buffers = new Uint8Array(data); - return; - } - const buffer = new Uint8Array(buffers.byteLength + data.byteLength); - // 将响应分段数据收集拼接起来,在完成加载后整体替换。 - // 这可能会改变浏览器接收数据分段渲染的行为。 - buffer.set(buffers); - buffer.set(new Uint8Array(data), buffers.buffer.byteLength); - buffers = buffer; - // 如果长度已经超长了,那就不要尝试修改了 - if (buffers.length > MAX_BODY_SIZE) { - buffers = null; - filter.close(); - } - }; - - // @ts-ignore - filter.onstop = () => { - if (buffers === null) { - filter.close(); - return; - } - - // 缓存实例,减少开销 - for (const item of rule!) { - const encoding = item.encoding || 'UTF-8'; - try { - const _text = this.textDecode(encoding, new Uint8Array(buffers!.buffer)); - const text = item._func(_text, detail); - if (typeof text === 'string' && text !== _text) { - buffers = this.textEncode(encoding, text); - } - } catch (err) { - console.error(err); - } - } - - filter.write(buffers.buffer); - buffers = null; - filter.close(); - }; - - // @ts-ignore - filter.onerror = () => { - buffers = null; - }; - } -} - -export default function createRequestHandler() { - return new RequestHandler(); -} diff --git a/src/pages/background/upgrade.ts b/src/pages/background/upgrade.ts index a4c71c37..92d6fca8 100644 --- a/src/pages/background/upgrade.ts +++ b/src/pages/background/upgrade.ts @@ -5,10 +5,13 @@ import notify from '@/share/core/notify'; import { getDatabase } from './core/db'; // Upgrade -const downloadHistory = localStorage.getItem('dl_history'); -if (downloadHistory) { - storage.getLocal().set({ dl_history: JSON.parse(downloadHistory) }); - localStorage.removeItem('dl_history'); +// Service Worker环境中没有localStorage,跳过升级 +if (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function') { + const downloadHistory = localStorage.getItem('dl_history'); + if (downloadHistory) { + storage.getLocal().set({ dl_history: JSON.parse(downloadHistory) }); + localStorage.removeItem('dl_history'); + } } // Put a version mark @@ -62,24 +65,29 @@ storage }); }; - const groups = localStorage.getItem('groups'); - if (groups) { - const g = JSON.parse(groups); - localStorage.removeItem('groups'); - rebindRuleWithGroup(g); - } else { - storage - .getLocal() - .get('groups') - .then((r) => { - if (r.groups !== undefined) { - rebindRuleWithGroup(r.groups).then(() => storage.getLocal().remove('groups')); - } else { - const g = {}; - g[browser.i18n.getMessage('ungrouped')] = []; - rebindRuleWithGroup(g); - } - }); + // Service Worker环境中没有localStorage,跳过groups迁移 + if (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function') { + const groups = localStorage.getItem('groups'); + if (groups) { + const g = JSON.parse(groups); + localStorage.removeItem('groups'); + rebindRuleWithGroup(g); + return; + } } + + // 如果没有localStorage或没有groups数据,使用storage.local + storage + .getLocal() + .get('groups') + .then((r) => { + if (r.groups !== undefined) { + rebindRuleWithGroup(r.groups).then(() => storage.getLocal().remove('groups')); + } else { + const g = {}; + g[browser.i18n.getMessage('ungrouped')] = []; + rebindRuleWithGroup(g); + } + }); } }); diff --git a/src/pages/background/v3-rule-converter.ts b/src/pages/background/v3-rule-converter.ts new file mode 100644 index 00000000..510cd898 --- /dev/null +++ b/src/pages/background/v3-rule-converter.ts @@ -0,0 +1,558 @@ +import browser from 'webextension-polyfill'; +import type { Rule } from '@/share/core/types'; +import logger from '@/share/core/logger'; + +// declarativeNetRequest 规则接口 +interface V3Rule { + id: number; + priority: number; + action: { + type: 'block' | 'redirect' | 'modifyHeaders' | 'upgradeScheme' | 'allow' | 'allowAllRequests'; + redirect?: { url: string; regexSubstitution?: string }; + requestHeaders?: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }>; + responseHeaders?: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }>; + }; + condition: { + urlFilter?: string; + regexFilter?: string; + domains?: string[]; + excludedDomains?: string[]; + resourceTypes?: browser.DeclarativeNetRequest.ResourceType[]; + excludedResourceTypes?: browser.DeclarativeNetRequest.ResourceType[]; + requestMethods?: string[]; + excludedRequestMethods?: string[]; + }; +} + +// 转换结果接口 +interface ConversionResult { + convertedRules: V3Rule[]; + unconvertedRules: Rule[]; + warnings: string[]; +} + +export class V3RuleConverter { + private static ruleIdCounter = 1000; // 从1000开始,避免与静态规则冲突 + + /** + * 将传统规则转换为 V3 规则 + */ + static convertRulesToV3(rules: Rule[]): ConversionResult { + const convertedRules: V3Rule[] = []; + const unconvertedRules: Rule[] = []; + const warnings: string[] = []; + + // 重置规则ID计数器 + this.ruleIdCounter = 1000; + + for (const rule of rules) { + try { + if (this.isConvertible(rule)) { + const v3Rule = this.convertSingleRule(rule); + if (v3Rule) { + convertedRules.push(v3Rule); + } + } else { + unconvertedRules.push(rule); + let reason = '未知原因'; + if (rule.isFunction) { + reason = '包含自定义函数'; + } else if (rule.ruleType === 'modifyReceiveBody') { + reason = '不支持响应体修改'; + } else if (rule.matchType === 'regexp' && this.isComplexRegex(rule.pattern)) { + reason = '包含复杂的正则表达式'; + } + warnings.push(`规则 "${rule.name}" 无法转换为 V3 格式: ${reason}`); + } + } catch (error) { + logger.error(`转换规则 "${rule.name}" 时发生错误:`, error); + unconvertedRules.push(rule); + warnings.push(`规则 "${rule.name}" 转换失败: ${error.message}`); + } + } + + return { convertedRules, unconvertedRules, warnings }; + } + + /** + * 检查规则是否可以转换为 V3 格式 + */ + private static isConvertible(rule: Rule): boolean { + // 不支持自定义函数 + if (rule.isFunction) { + return false; + } + + // 不支持响应体修改 + if (rule.ruleType === 'modifyReceiveBody') { + return false; + } + + // 检查是否为支持的规则类型 + const supportedTypes = ['cancel', 'redirect', 'modifySendHeader', 'modifyReceiveHeader']; + if (!supportedTypes.includes(rule.ruleType)) { + return false; + } + + // 检查正则表达式复杂度 + if (rule.matchType === 'regexp' && this.isComplexRegex(rule.pattern)) { + return false; + } + + return true; + } + + /** + * 检查是否为复杂正则表达式 + */ + private static isComplexRegex(pattern: string): boolean { + // 简单检查,如果包含复杂的正则特性,认为是复杂的 + const complexFeatures = [ + '\\d', '\\w', '\\s', '\\D', '\\W', '\\S', // 字符类 + '\\b', '\\B', // 边界 + '(?:', '(?=', '(?!', '(?<=', '(? pattern.includes(feature)); + } + + /** + * 转换单个规则 + */ + private static convertSingleRule(rule: Rule): V3Rule | null { + const startTime = Date.now(); + + try { + const v3Rule: V3Rule = { + id: this.ruleIdCounter++, + priority: Math.max(1, Math.min(100, rule.priority || 1)), // 确保优先级在有效范围内 + action: this.convertAction(rule), + condition: this.convertCondition(rule), + }; + + // 验证生成的规则 + if (!this.validateV3Rule(v3Rule)) { + logger.warn(`规则 "${rule.name}" 转换后验证失败,跳过应用`); + logger.logRuleConversion(rule, v3Rule, false); + return null; + } + + const duration = Date.now() - startTime; + logger.logPerformance(`规则转换 ${rule.name}`, duration); + logger.logRuleConversion(rule, v3Rule, true); + + return v3Rule; + } catch (error) { + const duration = Date.now() - startTime; + logger.error(`转换规则 "${rule.name}" 时发生错误:`, error); + logger.logPerformance(`规则转换失败 ${rule.name}`, duration); + logger.logRuleConversion(rule, null, false); + return null; + } + } + + /** + * 验证 V3 规则是否有效 + */ + private static validateV3Rule(rule: V3Rule): boolean { + // 检查 ID 是否有效 + if (!rule.id || rule.id < 1) { + return false; + } + + // 检查优先级是否有效 + if (rule.priority < 1 || rule.priority > 100) { + return false; + } + + // 检查动作是否有效 + if (!rule.action || !rule.action.type) { + return false; + } + + // 检查条件是否有效 + if (!rule.condition) { + return false; + } + + // 至少需要一个匹配条件 + const hasMatchCondition = + rule.condition.urlFilter || + rule.condition.regexFilter || + rule.condition.domains?.length || + rule.condition.excludedDomains?.length; + + if (!hasMatchCondition) { + return false; + } + + return true; + } + + /** + * 转换规则动作 + */ + private static convertAction(rule: Rule): V3Rule['action'] { + switch (rule.ruleType) { + case 'cancel': + return { type: 'block' }; + + case 'redirect': + if (rule.to) { + // 简单的 URL 重定向 + if (rule.matchType === 'regexp' && !this.isComplexRegex(rule.pattern)) { + return { + type: 'redirect', + redirect: { + url: rule.to, + regexSubstitution: rule.to, + }, + }; + } else { + return { + type: 'redirect', + redirect: { url: rule.to }, + }; + } + } + return { type: 'block' }; + + case 'modifySendHeader': + return { + type: 'modifyHeaders', + requestHeaders: this.convertHeaders(rule), + }; + + case 'modifyReceiveHeader': + return { + type: 'modifyHeaders', + responseHeaders: this.convertHeaders(rule), + }; + + default: + throw new Error(`不支持的规则类型: ${rule.ruleType}`); + } + } + + /** + * 转换请求头/响应头 + */ + private static convertHeaders(rule: Rule): Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }> { + if (!rule.action || typeof rule.action !== 'object') { + return []; + } + + const headers: Array<{ + header: string; + operation: 'set' | 'remove' | 'append'; + value?: string; + }> = []; + + if (rule.action.name && rule.action.value !== undefined) { + const headerName = rule.action.name.trim(); + if (!headerName) { + return []; + } + + if (rule.action.value === '_header_editor_remove_') { + headers.push({ + header: headerName, + operation: 'remove', + }); + } else { + headers.push({ + header: headerName, + operation: 'set', + value: String(rule.action.value), + }); + } + } + + return headers; + } + + /** + * 转换匹配条件 + */ + private static convertCondition(rule: Rule): V3Rule['condition'] { + const condition: V3Rule['condition'] = {}; + + // 设置资源类型 + if (rule.ruleType === 'modifySendHeader') { + // 请求头修改适用于所有资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } else if (rule.ruleType === 'modifyReceiveHeader') { + // 响应头修改适用于所有资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } else { + // 其他规则类型使用默认资源类型 + condition.resourceTypes = [ + 'main_frame', 'sub_frame', 'stylesheet', 'script', 'image', + 'font', 'object', 'xmlhttprequest', 'ping', 'csp_report', + 'media', 'websocket', 'other', + ]; + } + + // 转换 URL 匹配 + switch (rule.matchType) { + case 'all': + // 匹配所有URL + condition.urlFilter = '*'; + break; + + case 'regexp': + if (!this.isComplexRegex(rule.pattern)) { + condition.regexFilter = rule.pattern; + } else { + // 尝试将复杂正则转换为简单过滤器 + const simpleFilter = this.convertComplexRegexToFilter(rule.pattern); + if (simpleFilter) { + condition.urlFilter = simpleFilter; + } else { + condition.urlFilter = '*'; + } + } + break; + + case 'prefix': + // URL前缀匹配 + condition.urlFilter = `${rule.pattern}*`; + break; + + case 'domain': { + // 域名匹配 + const domain = this.normalizeDomain(rule.pattern); + if (domain) { + condition.domains = [domain]; + } else { + condition.urlFilter = '*'; + } + break; + } + + case 'url': + // 完整URL匹配 + condition.urlFilter = rule.pattern; + break; + + default: + // 默认匹配所有 + condition.urlFilter = '*'; + } + + // 处理排除模式 + if (rule.exclude && rule.exclude.trim()) { + try { + const excludeDomain = this.normalizeDomain(rule.exclude); + if (excludeDomain) { + condition.excludedDomains = [excludeDomain]; + } + } catch (error) { + logger.warn(`无法处理排除模式 "${rule.exclude}":`, error); + } + } + + return condition; + } + + /** + * 标准化域名 + */ + private static normalizeDomain(domain: string): string { + if (!domain) return ''; + + // 移除协议前缀 + domain = domain.replace(/^https?:\/\//, ''); + + // 移除路径 + domain = domain.split('/')[0]; + + // 移除端口 + domain = domain.split(':')[0]; + + // 移除 www. 前缀(可选) + if (domain.startsWith('www.')) { + domain = domain.substring(4); + } + + return domain.toLowerCase(); + } + + /** + * 检查是否为域名模式 + */ + private static isDomainPattern(pattern: string): boolean { + // 简单检查是否像域名 + return /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(pattern); + } + + /** + * 将复杂正则表达式转换为简单过滤器 + */ + private static convertComplexRegexToFilter(regex: string): string { + try { + // 尝试提取简单的URL模式 + if (regex.includes('://')) { + // 如果包含协议,提取域名部分 + const match = regex.match(/https?:\/\/([^/\s?]+)/); + if (match) { + return `*://${match[1]}/*`; + } + } + + // 提取域名模式 + const domainMatch = regex.match(/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/); + if (domainMatch) { + return `*://${domainMatch[1]}/*`; + } + + // 如果无法转换,返回通配符 + return '*'; + } catch (error) { + logger.warn('转换复杂正则表达式失败:', error); + return '*'; + } + } + + /** + * 从模式中提取域名 + */ + private static extractDomainFromPattern(pattern: string): string | null { + try { + const match = pattern.match(/(?:https?:\/\/)?([^/\s?]+)/); + return match ? match[1] : null; + } catch (error) { + return null; + } + } + + /** + * 应用 V3 规则到浏览器 + */ + static async applyV3Rules(rules: V3Rule[]): Promise { + const startTime = Date.now(); + + try { + logger.info(`准备应用 ${rules.length} 个 V3 规则`); + + // 先移除现有的动态规则 + const existingRules = await browser.declarativeNetRequest.getDynamicRules(); + const existingRuleIds = existingRules.map((r) => r.id); + + if (existingRuleIds.length > 0) { + logger.info(`移除现有的 ${existingRuleIds.length} 个动态规则`); + await browser.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: existingRuleIds, + }); + } + + // 应用新规则 + if (rules.length > 0) { + logger.info(`应用 ${rules.length} 个新规则`); + + // 分批应用规则,避免一次性应用太多规则 + const batchSize = 100; + for (let i = 0; i < rules.length; i += batchSize) { + const batch = rules.slice(i, i + batchSize); + logger.debug(`应用规则批次 ${Math.floor(i / batchSize) + 1}/${Math.ceil(rules.length / batchSize)}, 包含 ${batch.length} 个规则`); + + await browser.declarativeNetRequest.updateDynamicRules({ + addRules: batch, + }); + } + } + + const duration = Date.now() - startTime; + logger.logPerformance('V3 规则应用', duration); + logger.logV3RuleApplication(rules, true); + + // 验证规则是否成功应用 + const newRules = await browser.declarativeNetRequest.getDynamicRules(); + if (newRules.length !== rules.length) { + logger.warn(`规则应用不完整: 期望 ${rules.length} 个,实际 ${newRules.length} 个`); + } + } catch (error) { + const duration = Date.now() - startTime; + logger.logPerformance('V3 规则应用失败', duration); + logger.logV3RuleApplication(rules, false, error); + logger.error('应用 V3 规则时发生错误:', error); + throw error; + } + } + + /** + * 获取 V3 规则限制信息 + */ + static getRuleLimits(): { + MAX_NUMBER_OF_DYNAMIC_RULES: number; + MAX_NUMBER_OF_REGEX_RULES: number; + MAX_NUMBER_OF_STATIC_RULES: number; + } { + return { + MAX_NUMBER_OF_DYNAMIC_RULES: 30000, + MAX_NUMBER_OF_REGEX_RULES: 1000, + MAX_NUMBER_OF_STATIC_RULES: 30000, + }; + } + + /** + * 检查规则是否超过限制 + */ + static checkRuleLimits(rules: V3Rule[]): { + isValid: boolean; + errors: string[]; + } { + const limits = this.getRuleLimits(); + const errors: string[] = []; + + if (rules.length > limits.MAX_NUMBER_OF_DYNAMIC_RULES) { + errors.push(`规则数量 (${rules.length}) 超过限制 (${limits.MAX_NUMBER_OF_DYNAMIC_RULES})`); + } + + const regexRules = rules.filter((r) => r.condition.regexFilter); + if (regexRules.length > limits.MAX_NUMBER_OF_REGEX_RULES) { + errors.push(`正则表达式规则数量 (${regexRules.length}) 超过限制 (${limits.MAX_NUMBER_OF_REGEX_RULES})`); + } + + return { + isValid: errors.length === 0, + errors, + }; + } + + /** + * 获取当前应用的动态规则 + */ + static async getCurrentRules(): Promise { + try { + const rules = await browser.declarativeNetRequest.getDynamicRules(); + return rules; + } catch (error) { + logger.error('获取当前规则失败:', error); + return []; + } + } +} diff --git a/src/pages/options/components/v3-migration-guide.tsx b/src/pages/options/components/v3-migration-guide.tsx new file mode 100644 index 00000000..cd4db3a7 --- /dev/null +++ b/src/pages/options/components/v3-migration-guide.tsx @@ -0,0 +1,202 @@ +import React, { useState } from 'react'; +import { Button, Card, Typography, Space, Alert, Divider } from 'antd'; +import { CheckCircleOutlined, BugOutlined, PlayCircleOutlined } from '@ant-design/icons'; + +const { Title, Paragraph, Text } = Typography; + +interface V3MigrationGuideProps { + onClose?: () => void; +} + +export default function V3MigrationGuide({ onClose }: V3MigrationGuideProps) { + const [debugEnabled, setDebugEnabled] = useState(false); + const [testing, setTesting] = useState(false); + const [testResults, setTestResults] = useState(null); + + const handleEnableDebug = async () => { + try { + const response = await chrome.runtime.sendMessage({ + method: 'enableDebugLogging', + }); + setDebugEnabled(response.success); + } catch (error) { + console.error('启用调试日志失败:', error); + } + }; + + const handleDisableDebug = async () => { + try { + const response = await chrome.runtime.sendMessage({ + method: 'disableDebugLogging', + }); + setDebugEnabled(!response.success); + } catch (error) { + console.error('禁用调试日志失败:', error); + } + }; + + const handleTestRules = async () => { + setTesting(true); + try { + const response = await chrome.runtime.sendMessage({ + method: 'testRuleApplication', + }); + + const stats = await chrome.runtime.sendMessage({ + method: 'getRuleStats', + }); + + setTestResults({ + success: response.success, + error: response.error, + stats: stats.stats, + }); + } catch (error) { + setTestResults({ + success: false, + error: error.message, + stats: null, + }); + } finally { + setTesting(false); + } + }; + + return ( + + + Manifest V3 迁移指南 + + } + extra={onClose && } + > + + + +
    + 主要变化 +
      +
    • 使用 declarativeNetRequest 替代 webRequest API
    • +
    • 规则在后台服务工作者中处理
    • +
    • 改进的性能和资源使用
    • +
    • 更严格的安全限制
    • +
    +
    + +
    + 功能限制 + +
  • 自定义 JavaScript 函数规则
  • +
  • 响应体修改功能
  • +
  • 复杂的正则表达式匹配
  • +
  • 某些高级网络拦截功能
  • + + } + type="warning" + showIcon + /> +
    + + + +
    + 调试工具 + + + + + + + +
    + + {testResults && ( +
    + 测试结果 + + {testResults.error && ( + 错误: {testResults.error} + )} + {testResults.stats && ( +
    + 转换统计: +
      +
    • 总规则数: {testResults.stats.total || 0}
    • +
    • 已转换: {testResults.stats.converted || 0}
    • +
    • 未转换: {testResults.stats.unconverted || 0}
    • +
    • 警告数: {testResults.stats.warnings || 0}
    • +
    +
    + )} +
    + + 更多详细信息请查看浏览器控制台 (F12) + +
    +
    + } + type={testResults.success ? 'success' : 'error'} + showIcon + /> + + )} + +
    + 获取帮助 + + 如果您遇到问题或需要帮助,请访问: + + + + + +
    +
    +
    + ); +} diff --git a/src/share/core/logger.ts b/src/share/core/logger.ts index ad327e4d..c4e38191 100644 --- a/src/share/core/logger.ts +++ b/src/share/core/logger.ts @@ -1,28 +1,193 @@ -import dayjs from 'dayjs'; -import { prefs } from './prefs'; +// @ts-ignore +import { IS_BACKGROUND } from './utils'; -export interface LogItem { - time: Date; - message: string; - data?: any[]; +interface LogLevel { + DEBUG: 0; + INFO: 1; + WARN: 2; + ERROR: 3; } +const LOG_LEVELS: LogLevel = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, +}; + class Logger { - debug(message: string, ...data: any[]) { - if (!prefs.get('is-debug')) { + private currentLevel: number = LOG_LEVELS.INFO; + private prefix = ''; + + constructor() { + this.prefix = IS_BACKGROUND ? '[Header Editor BG]' : '[Header Editor]'; + + // 在开发环境下启用调试日志 + if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getManifest) { + try { + const manifest = chrome.runtime.getManifest(); + if (manifest.name?.includes('dev') || manifest.version?.includes('dev')) { + this.currentLevel = LOG_LEVELS.DEBUG; + } + } catch (e) { + // 忽略错误 + } + } + } + + private formatMessage(level: string, message: string, ..._args: any[]): string { + const timestamp = new Date().toISOString(); + const context = IS_BACKGROUND ? 'BG' : 'CS'; + return `${timestamp} [${context}] ${level}: ${message}`; + } + + private log(level: number, levelName: string, message: string, ...args: any[]) { + if (level < this.currentLevel) { return; } - console.log( - ['%cHeader Editor%c [', dayjs().format('YYYY-MM-DD HH:mm:ss.SSS'), ']%c ', message].join(''), - 'color:#5584ff;', - 'color:#ff9300;', - '', - ); - if (data && data.length > 0) { - console.log(data); + + const formattedMessage = this.formatMessage(levelName, message); + + switch (level) { + case LOG_LEVELS.DEBUG: + console.debug(this.prefix, formattedMessage, ...args); + break; + case LOG_LEVELS.INFO: + console.info(this.prefix, formattedMessage, ...args); + break; + case LOG_LEVELS.WARN: + console.warn(this.prefix, formattedMessage, ...args); + break; + case LOG_LEVELS.ERROR: + console.error(this.prefix, formattedMessage, ...args); + break; + default: + console.log(this.prefix, formattedMessage, ...args); } } + + debug(message: string, ...args: any[]) { + this.log(LOG_LEVELS.DEBUG, 'DEBUG', message, ...args); + } + + info(message: string, ...args: any[]) { + this.log(LOG_LEVELS.INFO, 'INFO', message, ...args); + } + + warn(message: string, ...args: any[]) { + this.log(LOG_LEVELS.WARN, 'WARN', message, ...args); + } + + error(message: string, ...args: any[]) { + this.log(LOG_LEVELS.ERROR, 'ERROR', message, ...args); + } + + // 设置日志级别 + setLevel(level: keyof LogLevel) { + this.currentLevel = LOG_LEVELS[level]; + this.info(`日志级别设置为: ${level}`); + } + + // 获取当前日志级别 + getLevel(): string { + const levels = Object.keys(LOG_LEVELS); + return levels.find((key) => LOG_LEVELS[key as keyof LogLevel] === this.currentLevel) || 'INFO'; + } + + // 启用调试模式 + enableDebug() { + this.setLevel('DEBUG'); + } + + // 禁用调试模式 + disableDebug() { + this.setLevel('INFO'); + } + + // 记录规则转换详情 + logRuleConversion(rule: any, v3Rule: any, success: boolean) { + if (success) { + this.debug('规则转换成功', { + originalRule: { + id: rule.id, + name: rule.name, + type: rule.ruleType, + matchType: rule.matchType, + pattern: rule.pattern, + enable: rule.enable, + }, + v3Rule: { + id: v3Rule.id, + priority: v3Rule.priority, + actionType: v3Rule.action?.type, + conditionKeys: Object.keys(v3Rule.condition || {}), + }, + }); + } else { + this.warn('规则转换失败', { + rule: { + id: rule.id, + name: rule.name, + type: rule.ruleType, + matchType: rule.matchType, + pattern: rule.pattern, + }, + }); + } + } + + // 记录 V3 规则应用详情 + logV3RuleApplication(rules: any[], success: boolean, error?: any) { + if (success) { + this.info('V3 规则应用成功', { + ruleCount: rules.length, + ruleIds: rules.map((r) => r.id), + ruleTypes: rules.reduce((types: Record, rule) => { + const type = rule.action?.type || 'unknown'; + types[type] = (types[type] ?? 0) + 1; + return types; + }, {}), + }); + } else { + this.error('V3 规则应用失败', { + ruleCount: rules.length, + error: error?.message || '未知错误', + stack: error?.stack, + }); + } + } + + // 记录规则匹配详情 + logRuleMatch(url: string, rule: any, matched: boolean) { + this.debug('规则匹配检查', { + url, + rule: { + id: rule.id, + name: rule.name, + type: rule.ruleType, + matchType: rule.matchType, + pattern: rule.pattern, + }, + matched, + }); + } + + // 记录性能信息 + logPerformance(operation: string, duration: number) { + this.info(`性能统计: ${operation} 耗时 ${duration}ms`); + } + + // 记录扩展状态 + logExtensionState(state: any) { + this.info('扩展状态', { + enabled: !state.disabled, + rulesCount: state.rulesCount || 0, + v3RulesCount: state.v3RulesCount || 0, + lastUpdate: state.lastUpdate || 'never', + }); + } } const logger = new Logger(); + export default logger; diff --git a/src/share/core/notify.ts b/src/share/core/notify.ts index 00e4004c..e00799da 100644 --- a/src/share/core/notify.ts +++ b/src/share/core/notify.ts @@ -11,10 +11,10 @@ class Notify { resolve: (v: any) => void; reject: (e: any) => void; }> = []; - private messageTimer: number | null = null; + private messageTimer: ReturnType | null = null; constructor() { - const handleMessage = (request: any, sender?: any) => { + const handleMessage = (request: any, _sender?: any) => { if (request.method === 'notifyBackground') { request.method = request.reason; delete request.reason; @@ -26,7 +26,7 @@ class Notify { this.event.emit(request.event, request); }; - browser.runtime.onMessage.addListener((request, sender) => { + browser.runtime.onMessage.addListener((request, _sender) => { // 批量消息 if (request.method === 'batchExecute') { request.batch.forEach((item) => handleMessage(item)); diff --git a/src/share/core/prefs.ts b/src/share/core/prefs.ts index 16208beb..ee7d0cc9 100644 --- a/src/share/core/prefs.ts +++ b/src/share/core/prefs.ts @@ -54,6 +54,11 @@ class Prefs { }); function tryMigrating(key: string) { + // Service Worker环境中没有localStorage,跳过迁移 + if (typeof localStorage === 'undefined' || typeof localStorage.getItem !== 'function') { + return undefined; + } + if (!(key in localStorage)) { return undefined; } @@ -72,8 +77,9 @@ class Prefs { console.error("Cannot migrate from localStorage %s = '%s': %o", key, value, e); return undefined; } + default: + return value; } - return value; } } get(key: string, defaultValue?: any) { @@ -129,5 +135,22 @@ class Prefs { interface BackgroundWindow extends Window { prefs?: Prefs; } -const backgroundWindow = browser.extension.getBackgroundPage() as BackgroundWindow; -export const prefs = backgroundWindow && backgroundWindow.prefs ? backgroundWindow.prefs : new Prefs(); + +// Service Worker环境中没有background page,直接创建新的Prefs实例 +function createPrefs() { + // 在Service Worker环境中,直接创建新实例 + if (typeof window === 'undefined') { + return new Prefs(); + } + + // 在其他环境中,尝试从background page获取 + try { + const backgroundWindow = browser.extension.getBackgroundPage() as BackgroundWindow; + return backgroundWindow && backgroundWindow.prefs ? backgroundWindow.prefs : new Prefs(); + } catch (e) { + // 如果获取background page失败,创建新实例 + return new Prefs(); + } +} + +export const prefs = createPrefs(); diff --git a/src/share/core/storage.ts b/src/share/core/storage.ts index 29f56352..8d9d1195 100644 --- a/src/share/core/storage.ts +++ b/src/share/core/storage.ts @@ -2,7 +2,7 @@ import browser from 'webextension-polyfill'; export function getSync() { // For development mode - if (typeof localStorage !== 'undefined' && localStorage.getItem('storage') === 'local') { + if (typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function' && localStorage.getItem('storage') === 'local') { return browser.storage.local; } try { diff --git a/src/share/core/utils.ts b/src/share/core/utils.ts index 3fbe9991..f0353b03 100644 --- a/src/share/core/utils.ts +++ b/src/share/core/utils.ts @@ -158,10 +158,12 @@ export function getGlobal() { } export function isBackground() { + // Service Worker环境中window不存在,但我们是在后台运行 if (typeof window === 'undefined') { return true; } - return typeof window.IS_BACKGROUND !== 'undefined'; + // 检查window或globalThis中的IS_BACKGROUND标志 + return typeof window.IS_BACKGROUND !== 'undefined' || typeof globalThis.IS_BACKGROUND !== 'undefined'; } export function getVirtualKey(rule: Rule) { diff --git a/tsconfig.json b/tsconfig.json index 37a4ccfc..214c6815 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "jsx": "react", "moduleResolution": "node", "allowSyntheticDefaultImports": true, - "lib": ["es2020", "dom"], + "lib": ["es2020", "dom", "webworker"], "sourceMap": true, "allowJs": true, "rootDir": "./", @@ -19,10 +19,11 @@ "noImplicitAny": false, "importHelpers": true, "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "skipLibCheck": true, - "types": ["node"], + "esModuleInterop": true, + "resolveJsonModule": true, + "types": ["node", "chrome"], "paths": { "@/*": ["./src/*"], "ice": [".ice/index.ts"],