diff --git a/WebExtensions/Firefox/README.md b/WebExtensions/Firefox/README.md new file mode 100644 index 0000000..5a94848 --- /dev/null +++ b/WebExtensions/Firefox/README.md @@ -0,0 +1,102 @@ +# Firefox WebExtension 项目 + +## 项目概述 +本目录包含针对 Firefox 浏览器的 WebExtension 实现。源码包括 `manifest.json`、背景脚本 `background.js`、图标等资源。 + +## 操作系统与构建环境要求 +- **操作系统**:Windows 10/11(亦可在 macOS、Linux 上使用相同脚本的 Bash 版本) +- **Node.js**:版本 ≥ 18(推荐使用 LTS 版本) +- **npm**:版本 ≥ 9 +- **web-ext**:用于打包、签名和运行扩展的官方工具 + +## 所需程序及安装步骤 +1. **安装 Node.js 与 npm** + 前往 https://nodejs.org/ 下载并安装 LTS 版本,安装后 `node -v` 与 `npm -v` 应分别显示 ≥ 18 与 ≥ 9。 + +2. **全局安装 web-ext(如未安装)** + ```powershell + npm i -g web-ext + ``` + 安装完成后可通过 `web-ext --version` 验证。 + +## 构建脚本 +项目根目录下提供 `build.bat`(Windows)脚本,执行以下全部步骤: + +```bat +@echo off +setlocal + +rem ---- 1. 检查 Node 与 npm 版本 ---- +for /f "tokens=*" %%i in ('node -v') do set NODE_VER=%%i +for /f "tokens=*" %%i in ('npm -v') do set NPM_VER=%%i + +echo Detected Node version: %NODE_VER% +echo Detected npm version: %NPM_VER% + +rem 需要的最低版本 +set MIN_NODE=v18.0.0 +set MIN_NPM=9.0.0 + +rem 简单的版本比较(仅检查主版本号) +for /f "tokens=2 delims=v." %%i in ("%NODE_VER%") do set NODE_MAJOR=%%i +for /f "tokens=1 delims=." %%i in ("%MIN_NODE%") do set MIN_NODE_MAJOR=%%i + +for /f "tokens=1 delims=." %%i in ("%NPM_VER%") do set NPM_MAJOR=%%i +for /f "tokens=1 delims=." %%i in ("%MIN_NPM%") do set MIN_NPM_MAJOR=%%i + +if %NODE_MAJOR% LSS %MIN_NODE_MAJOR% ( + echo Error: Node version must be >= %MIN_NODE% + exit /b 1 +) + +if %NPM_MAJOR% LSS %MIN_NPM_MAJOR% ( + echo Error: npm version must be >= %MIN_NPM% + exit /b 1 +) + +rem ---- 2. 安装 web-ext(如果未全局安装)---- +where web-ext >nul 2>&1 +if errorlevel 1 ( + echo web-ext not found, installing globally... + npm i -g web-ext + if errorlevel 1 ( + echo Failed to install web-ext. + exit /b 1 + ) +) else ( + echo web-ext already installed. +) + +rem ---- 3. 执行构建 ---- +rem 创建输出目录 +if not exist dist mkdir dist + +echo Building Firefox extension... +web-ext build --source-dir . --artifacts-dir ./dist +if errorlevel 1 ( + echo Build failed. + exit /b 1 +) + +echo Build succeeded. Artifact saved in ./dist +endlocal +``` + +> **使用方法** +> 1. 将 `build.bat` 放置于 `Firefox` 目录根部。 +> 2. 在 PowerShell 或 CMD 中切换到该目录:`cd Firefox`。 +> 3. 运行脚本:`.\build.bat`。 +> 4. 成功后,`dist` 目录下会出现类似 `my-extension-0.0.1.xpi` 的文件,即可在 Firefox 中加载测试。 + +## 常见问题 +- **找不到 web-ext**:确保全局 `npm` 路径已加入系统 `PATH`,或手动执行 `npm i -g web-ext`。 +- **Node 版本过低**:请升级至 LTS 版本(≥ 18),或使用 nvm 管理多个版本。 +- **构建失败**:检查 `manifest.json` 是否符合 Firefox WebExtension 规范,尤其是 `manifest_version` 与权限声明。 + +--- + +## 任务进度 +- [x] 查看 Firefox 目录结构 +- [x] 编写 README.md(包含构建步骤、环境要求、所需程序及版本) +- [x] 编写 build.bat 脚本,实现完整的构建流程 +- [ ] 验证脚本在当前环境下可执行(如需,可自行运行 `Firefox\build.bat`) \ No newline at end of file diff --git a/WebExtensions/Firefox/background.js b/WebExtensions/Firefox/background.js new file mode 100644 index 0000000..08cf6bd --- /dev/null +++ b/WebExtensions/Firefox/background.js @@ -0,0 +1,318 @@ +// ws心跳间隔 +const TEN_SECONDS_MS = 10 * 1000; +// 焦点检测间隔 +const FOCUSED_CHECK_MS = 1000; +// 自动重新通知间隔 +const RENOTIFY_MS = 10 * 1000; +// 重连失败次数进入睡眠状态 +const RECONNECTFAIL_SLEEP = 5; +// Tai WS服务器URL +const WSURL = 'ws://127.0.0.1:8908/TaiWebSentry'; + +let webSocket = null; +let isConnected = false; +let isChromeFocused = true; +let autoReConnectIntervalId = null; +// 是否处于睡眠状态(来源Tai通知) +let isSleep = false; +// 焦点网页数据 +let activePage = { + url: '', + title: '', + icon: '', + startTime: '', + endTime: '', + duration: 0 +}; +// 重连失败次数 +let reconnectFail = 0; + +init(); + +/** + * 初始化插件(入口 + */ +function init() { + connect(); + startWatchFocus(); + startRenofity(); +} + +/** + * 连接Tai + */ +function connect() { + webSocket = new WebSocket(WSURL); + webSocket.onopen = (event) => { + isConnected = true; + isSleep = false; + reconnectFail = 0; + clearInterval(autoReConnectIntervalId); + chrome.browserAction.setIcon({ path: 'icons/socket-active.png' }); + keepAlive(); + console.log("已连接Tai!"); + }; + webSocket.onmessage = (event) => { + console.log(event.data); + if (event.data === 'sleep') { + isSleep = true; + calDuration(); + console.log("睡眠"); + } + else if (event.data === 'wake') { + isSleep = false; + console.log("唤醒"); + } + + }; + webSocket.onclose = (event) => { + isConnected = false; + chrome.browserAction.setIcon({ path: 'icons/socket-inactive.png' }); + console.warn('与Tai失去连接...'); + webSocket = null; + startAutoReConnect(); + }; +} + +function disconnect() { + if (isConnected && webSocket) { + webSocket.close(); + isConnected = false; + } +} + +function startAutoReConnect() { + clearInterval(autoReConnectIntervalId); + autoReConnectIntervalId = setInterval( + () => { + if (!isConnected) { + console.log("尝试重新连接Tai...") + connect(); + reconnectFail++; + // 达到重连失败阈值时进入睡眠状态 + if (reconnectFail >= RECONNECTFAIL_SLEEP && !isSleep) { + isSleep = true; + } + } + }, + TEN_SECONDS_MS + ); +} + +/** + * 保持心跳,防止插件掉线 + */ +function keepAlive() { + const keepAliveIntervalId = setInterval( + () => { + if (isConnected && webSocket) { + console.log('ping'); + webSocket.send('ping'); + } else { + clearInterval(keepAliveIntervalId); + } + }, + // It's important to pick an interval that's shorter than 30s, to + // avoid that the service worker becomes inactive. + TEN_SECONDS_MS + ); +} + + +// Chrome functions + +// 监听Chrome选项卡切换事件 +chrome.tabs.onActivated.addListener(async function + (e) { + const tab = await getTab(e.tabId); + onActivePage(tab); +} +); + +// 监听Chrome选项卡更新事件 +chrome.tabs.onUpdated.addListener(function + (tabId, changeInfo, tab) { + if (changeInfo.status == 'complete' && tab.active) { + onActivePage(tab); + } +} +); + +/** + * 通过TabId获取Tab + * @param {Number} tabId TabId + * @returns Tab + */ +function getTab(tabId) { + return new Promise((resolve, reject) => { + chrome.tabs.get( + tabId, + function (e) { + resolve(e); + } + ) + }); +} + +/** + * 指示当前浏览器是否处于焦点中 + * @returns 焦点中返回true + */ +function isFocused() { + return new Promise((resolve, reject) => { + chrome.windows.getCurrent(function (w) { + resolve(w.focused); + }); + }); + +} + +/** + * 获取当前焦点Tab + * @returns Tab Obj + */ +function getCurrentTab() { + return new Promise((resolve, reject) => { + chrome.tabs.query({ active: true }, function (tab) { + if (tab && tab.length > 0) { + resolve(tab[0]); + } + else { + reject(tab); + } + }); + }); +} + + +// Tai functions + +// 通知失败的集合 +let notifyFailList = []; + +/** + * 重新发送通知失败的数据(最早的一条) + */ +function renotify() { + if (isConnected && webSocket && notifyFailList.length > 0) { + const item = notifyFailList[0]; + notifyFailList.splice(0, 1); + notifyTai(item); + } +} + +/** + * 通知Tai更新数据 + * @param {*} data 统计数据 + */ +function notifyTai(data = { + Url, + Title, + Icon, + Duration, + ActiveTime +}) { + console.log("notify", data); + if (isConnected && webSocket) { + webSocket.send(JSON.stringify(data)); + } + else { + // let item = notifyFailList.find(s => s.Url === data.Url); + // if (item) { + // item.Duration += data.Duration; + // } + // else { + notifyFailList.push(data); + // } + + console.log("failList", notifyFailList) + } +} + +/** + * 响应网页焦点切换 + * @param {ChromeTab} tab Tab + */ +function onActivePage(tab) { + // 睡眠状态不处理 + if (isSleep) return; + + if (activePage && activePage.url) { + if (activePage.url !== tab.url) { + // 统计时间 + calDuration(); + setActive(tab); + } + } + else { + setActive(tab); + } +} + +/** + * 设置当前焦点网页数据 + * @param {*} tab Tab + */ +function setActive(tab) { + const { url, title, favIconUrl: icon } = tab; + if (url) { + activePage = { + url, title, icon, startTime: new Date().getTime() + }; + } + else { + activePage = null; + } +} + +/** + * 计算并通知焦点网页浏览时长 + */ +function calDuration() { + if (activePage && activePage.url) { + let duration = parseInt((new Date().getTime() - activePage.startTime) / 1000); + let activeTime = parseInt(activePage.startTime / 1000); + const { url: Url, title: Title, icon: Icon } = activePage; + activePage = null; + notifyTai({ Url, Title, Icon, Duration: duration, ActiveTime: activeTime }); + } +} + +/** + * 启动焦点监听(用于检测用户是否处于浏览器中 + */ +function startWatchFocus() { + console.log("开始监听焦点"); + setInterval( + async () => { + const focused = await isFocused(); + if (focused) { + if (!isChromeFocused) { + isChromeFocused = true; + // 重置统计 + const tab = await getCurrentTab(); + onActivePage(tab); + console.warn("重置统计") + } + } + else { + if (isChromeFocused) { + isChromeFocused = false; + // 更新时间 + calDuration(); + console.warn("更新时间") + } + } + }, + FOCUSED_CHECK_MS + ); +} + +/** + * 启动定时重新发送失败的数据通知 + */ +function startRenofity() { + setInterval(() => { + renotify(); + }, RENOTIFY_MS); +} \ No newline at end of file diff --git a/WebExtensions/Firefox/icon128.png b/WebExtensions/Firefox/icon128.png new file mode 100644 index 0000000..55bb63e Binary files /dev/null and b/WebExtensions/Firefox/icon128.png differ diff --git a/WebExtensions/Firefox/icon48.png b/WebExtensions/Firefox/icon48.png new file mode 100644 index 0000000..e5aa3a8 Binary files /dev/null and b/WebExtensions/Firefox/icon48.png differ diff --git a/WebExtensions/Firefox/icons/socket-active.png b/WebExtensions/Firefox/icons/socket-active.png new file mode 100644 index 0000000..55bb63e Binary files /dev/null and b/WebExtensions/Firefox/icons/socket-active.png differ diff --git a/WebExtensions/Firefox/icons/socket-inactive.png b/WebExtensions/Firefox/icons/socket-inactive.png new file mode 100644 index 0000000..c05a74c Binary files /dev/null and b/WebExtensions/Firefox/icons/socket-inactive.png differ diff --git a/WebExtensions/Firefox/manifest.json b/WebExtensions/Firefox/manifest.json new file mode 100644 index 0000000..5f0d994 --- /dev/null +++ b/WebExtensions/Firefox/manifest.json @@ -0,0 +1,31 @@ +{ + "name": "Tai Sentry", + "description": "Tai网页浏览数据统计拓展", + "version": "0.1", + "manifest_version": 2, + + "icons": { + "48": "icon48.png", + "128": "icon128.png" + }, + + "browser_action": { + "default_icon": "icons/socket-inactive.png" + }, + + "background": { + "scripts": ["background.js"] + }, + + "permissions": ["tabs"], + + "browser_specific_settings": { + "gecko": { + "id": "me@goodfish.site", + "strict_min_version": "142.0", + "data_collection_permissions": { + "required": ["none"] + } + } + } +} \ No newline at end of file diff --git a/WebExtensions/Firefox/web-ext-artifacts/tai_sentry-0.1.zip b/WebExtensions/Firefox/web-ext-artifacts/tai_sentry-0.1.zip new file mode 100644 index 0000000..458be6d Binary files /dev/null and b/WebExtensions/Firefox/web-ext-artifacts/tai_sentry-0.1.zip differ